Compare commits

...
309 Commits
Author SHA1 Message Date
1tzEmerald 01f9ebe387 removed junk 2026-08-28 17:49:56 +01:00
1tzEmerald a746c468e5 26.2 final push 2026-08-28 17:48:09 +01:00
1tzEmerald 2017c33756 update 2026-08-12 00:12:18 +01:00
1tzEmerald 4eef1867e3 26.2 pre3 2026-08-10 20:15:00 +01:00
1tzEmerald a7598d26aa Fix BETTERSQLLITE ISSUE 2026-08-06 17:29:37 +01:00
1tzEmerald 762fcf45c3 26.2-pre-2 2026-08-06 16:30:04 +01:00
1tzEmerald ea2a8c97ae Removed optional addons by default 2026-07-26 21:23:16 +01:00
1tzEmerald 004b638dbf Dashboard 1.0 Release 2026-07-26 20:33:00 +01:00
1tzEmerald 69371ae619 API Update 2026-07-26 14:21:07 +01:00
1tzEmerald b145d5fa01 update API 2026-06-26 13:55:24 +01:00
1tzEmerald 8c9fcb32e0 update 2026-06-26 13:54:59 +01:00
1tzEmerald 17d45de378 removed junk 2026-06-26 13:53:54 +01:00
1tzEmerald b96c60912c Merge pull request #211 from C-h-a-r/release
Accent Colour Updated
2026-06-20 22:53:22 +01:00
Char e97e281bda Merge branch 'release' of https://github.com/C-h-a-r/SupportBot-Dashboard into release 2026-06-20 14:30:49 +01:00
Char 00cebb7bb5 accent colour updated 2026-06-20 14:30:23 +01:00
1tzEmerald 014f263bf1 Merge pull request #210 from C-h-a-r/release
Self Hosted Dashboard
2026-06-02 17:34:40 +01:00
1tzEmerald 942d71040b Merge branch '26.2' into release 2026-06-02 13:09:54 +01:00
1tzEmerald 92d21e7fc0 Update README.md 2026-05-27 20:46:27 +01:00
Char 07e4705a6a files 2026-05-26 20:22:57 +01:00
Char 6320296f0f updates 2026-05-26 20:11:22 +01:00
Char f4b14e6c01 Add Dockerfile for Node.js application setup 2026-05-24 14:04:37 +01:00
Char 7e22831d08 v26.3 2026-05-19 23:22:11 +01:00
Char 9332fc9254 v26.2 2026-05-16 17:36:50 +01:00
Char 018c949104 transcripts 2026-05-16 17:35:39 +01:00
Char b55976508e public files 2026-05-16 16:46:44 +01:00
Char 264dcb3053 added dashboard files 2026-05-16 16:44:04 +01:00
Char 22c33c0f7a dashboard v1 2026-05-16 15:10:27 +01:00
Nyo 57d420a9f4 Update supportbot.yml 2026-05-12 16:31:37 +02:00
Nyo f8cc08ba3e Bump version from 26.0.0-prerelease-1 to 26.1.1 2026-05-12 16:31:23 +02:00
Nyo 60d5c4cb1f Change event name from 'ready' to 'clientReady' 2026-05-12 16:30:29 +02:00
Nyo 0288e2f4de Normalize channel, role, and category name comparisons 2026-05-12 16:30:12 +02:00
Nyo 33d5a79759 Refactor image handling in buildModernComponents
Refactored the buildModernComponents function to use MediaGalleryBuilder for image handling instead of TextDisplayBuilder. Cleaned up code formatting and removed unnecessary whitespace.
2026-05-12 16:29:54 +02:00
1tzEmerald 30b89a3cf9 Update supportbot.yml 2026-04-25 22:31:11 +01:00
1tzEmerald 427dc2d040 Merge pull request #209 from Emerald-Services/Dev-(26.1)
26.1
2026-04-25 21:57:00 +01:00
1tzEmerald 033f32d596 26.1 Dev-6
- Further Dashboard Improvements
- Bug Fixes
2026-04-21 13:37:39 +01:00
1tzEmerald d5fa42b55d Merge pull request #206 from Emerald-Services/26.0
26.0
2026-03-27 16:45:39 +00:00
1tzEmerald 4bb35b1e50 Update account setup instructions for SupportBot AI 2026-03-27 16:31:40 +00:00
1tzEmerald d451857d7b Update SupportBot version in configuration file 2026-03-27 16:31:07 +00:00
1tzEmerald 78b804176a 26.0 2026-03-27 16:25:59 +00:00
1tzEmerald 15b780ab62 26.0-build-2 2026-03-11 18:58:15 +00:00
1tzEmerald 9448c4e3d6 26.0-build-1 2026-03-10 20:02:43 +00:00
1tzEmerald 4af9759700 Update SupportBot version to v8.5 2026-02-14 13:32:59 +00:00
1tzEmerald 835cbbe937 Delete package-lock.json 2026-02-14 13:32:10 +00:00
1tzEmerald 491bb9ba65 . 2026-02-14 13:14:05 +00:00
1tzEmerald ced29535f9 Merge pull request #201 from Emerald-Services/8.5
8.5 Update
2026-02-14 13:11:56 +00:00
1tzEmerald d99e9a105b 8.5 Update
Added TicketRename command
2026-02-14 13:05:07 +00:00
1tzEmerald c72c7c5c34 Merge pull request #200 from Emerald-Services/1tzemerald-patch-3
Delete .idea directory
2026-02-03 16:37:22 +00:00
1tzEmerald 23960bc30d Delete .idea directory 2026-02-03 16:37:08 +00:00
1tzEmerald abb48c281b Merge branch 'release' of https://github.com/Emerald-Services/SupportBot into release 2026-02-03 16:33:08 +00:00
1tzEmerald 7acb825713 Added rename command 2026-02-03 16:33:02 +00:00
1tzEmerald dd2f329485 Merge pull request #199 from Emerald-Services/1tzemerald-patch-2
Update Node.JS version for SupportBot 8.x
2025-11-22 12:52:33 +00:00
1tzEmerald 75577ec0bd Update Node.JS version for SupportBot 8.x 2025-11-22 12:52:23 +00:00
1tzEmerald 7366bcda71 Merge pull request #198 from Emerald-Services/1tzemerald-patch-1
Change support status for SupportBot 7.7.3
2025-11-22 12:51:59 +00:00
1tzEmerald 23e43cfad3 Change support status for SupportBot 7.7.3
Updated support status for SupportBot versions.
2025-11-22 12:51:47 +00:00
Nyo ad9a8085aa Merge pull request #197 from Emerald-Services/ItsNyoty-patch-1
Fix link format for BuiltByBit in README.md
2025-09-30 18:50:12 +02:00
Nyo 2f23e8f23d Fix link format for BuiltByBit in README.md 2025-09-30 18:49:52 +02:00
1tzEmerald 4c07b8a236 Merge pull request #196 from Emerald-Services/dev
8.4.1 guilldMemberAdd update.
2025-09-25 13:16:42 +01:00
1tzEmerald 571bd586fa 8.4.1 guilldMemberAdd update.
Added if statement to .setAccentColor allowing you to remove the color from the config.
2025-09-25 13:16:20 +01:00
1tzEmerald eac2d88873 Merge pull request #195 from Emerald-Services/dev
8.4.1 small fix
2025-09-25 13:10:23 +01:00
1tzEmerald 93c793f393 Merge branch 'dev' of https://github.com/Emerald-Services/SupportBot into dev 2025-09-25 13:09:09 +01:00
1tzEmerald 1aa97b63f9 8.4.1 small fix 2025-09-25 13:09:00 +01:00
1tzEmerald c1b76a2b84 Merge pull request #194 from Emerald-Services/dev
8.4 from Dev to Release
2025-09-23 23:26:30 +01:00
1tzEmerald a1429aa723 Merge branch 'release' into dev 2025-09-23 23:26:13 +01:00
1tzEmerald 2de7c7b19c 8.4 2025-09-23 23:23:19 +01:00
1tzEmerald 7383bd2c48 8.4-DEV-2
Added Buttons to WelcomeMessage
Added Components v2 Support for Welcome Message
Added improved logging system
2025-09-22 18:36:04 +01:00
1tzEmerald a05d65099b 8.4 DEV 1
Changed JSON to SQLlite
Added Suggestion Forums
Added /suggestadmin
2025-09-20 23:09:22 +01:00
1tzEmerald c6db929d12 8.4-Dev 1
Added Suggestions mode switch
2025-08-07 13:54:35 +01:00
1tzEmerald dc88f6c51a Merge pull request #186 from chorimethod/release
Added "InvalidChannel" message (required by suggest.js), fixed a double quote that was preventing the bot to start correctly.
2025-07-14 16:37:45 +01:00
Nyo d69fb0e26d Update supportbot.yml 2025-05-22 20:30:06 +02:00
chorisancio3d 4b2d53d3f4 Added "InvalidChannel" message (required by suggest.js)
Fixed a double quote that was preventing the bot to start correctly.
2025-02-03 21:43:03 -03:00
1tzEmerald e3fa8b6d72 Merge pull request #185 from Emerald-Services/dev
Update close.js
2024-11-10 13:31:17 +00:00
1tzEmerald 5c6a9f7306 Update close.js 2024-11-10 13:30:42 +00:00
1tzEmerald 71e33dbf66 Update close.js 2024-11-10 13:30:27 +00:00
1tzEmerald d468ab1575 Delete Data/Profiles directory 2024-11-10 13:18:50 +00:00
1tzEmerald 74482af471 Merge pull request #184 from Emerald-Services/dev
Dev
2024-11-09 13:04:16 +00:00
1tzEmerald 0b8b092cb9 Update profile.js 2024-11-09 13:03:55 +00:00
1tzEmerald 3cdfaef942 Update profile.js 2024-11-09 13:03:41 +00:00
1tzEmerald fcf441a190 Update close.js 2024-11-09 02:46:35 +00:00
1tzEmerald 5a1c4ad99c Update close.js 2024-11-09 02:46:25 +00:00
1tzEmerald d9cb72f22e Update close.js 2024-11-09 02:44:48 +00:00
1tzEmerald 58eb37124e Update close.js 2024-11-09 02:44:39 +00:00
1tzEmerald afeeda3a8d Merge pull request #183 from Emerald-Services/dev
Dev
2024-11-09 02:38:20 +00:00
1tzEmerald fece90d2bd Update close.js 2024-11-09 02:37:26 +00:00
1tzEmerald 357a85fa3d Update close.js 2024-11-09 02:37:14 +00:00
1tzEmerald f34158caa6 Update close.js 2024-11-09 02:32:52 +00:00
1tzEmerald 1bb343e49a Update ticket.js 2024-11-09 02:32:28 +00:00
1tzEmerald dffa5d4161 Update close.js 2024-11-09 02:32:06 +00:00
1tzEmerald 13590b08a8 Update ticket.js 2024-11-09 01:07:16 +00:00
1tzEmerald 6e14ce209d Merge pull request #182 from Emerald-Services/dev
8.3 Dev -> Release
2024-11-09 00:45:40 +00:00
1tzEmerald 72b8c2247e Merge branch 'release' into dev 2024-11-09 00:45:28 +00:00
1tzEmerald 925504a928 Merge branch 'dev' of https://github.com/Emerald-Services/SupportBot into dev 2024-11-09 00:37:53 +00:00
1tzEmerald 236d67707f 8.3 Update 2024-11-09 00:37:51 +00:00
1tzEmerald 753f432c95 Merge pull request #179 from Emerald-Services/1tzemerald-patch-4
Update package.json
2024-10-11 15:04:37 +01:00
1tzEmerald c824bc51b2 Update package.json 2024-10-11 15:03:47 +01:00
1tzEmerald 39ee540b55 Merge pull request #177 from Emerald-Services/dev
8.2
2024-10-11 15:00:05 +01:00
1tzEmerald 903b214051 Merge branch 'release' into dev 2024-10-11 14:59:50 +01:00
1tzEmerald ac0daf5fe7 8.2 2024-10-10 19:14:45 +01:00
ItsNyoty 80e38a07e9 Merge pull request #175 from Emerald-Services/ItsNyoty-patch-1
Update README.md
2024-09-19 13:06:05 +02:00
ItsNyoty d2598e09f7 Update README.md 2024-09-19 13:05:26 +02:00
ItsNyoty 28aab47150 Update README.md 2024-09-19 13:04:44 +02:00
1tzEmerald 81bda67ae4 Merge pull request #172 from Emerald-Services/1tzemerald-patch-3
Update supportbot.yml
2024-07-30 17:58:41 +01:00
1tzEmerald 25bbd1ff85 Update supportbot.yml 2024-07-30 17:58:01 +01:00
1tzEmerald 164d695a5f Merge pull request #171 from Emerald-Services/1tzemerald-patch-2
Create 0000000000000000.json
2024-07-30 17:57:33 +01:00
1tzEmerald 57e4a3e77c Create 0000000000000000.json 2024-07-30 17:57:12 +01:00
1tzEmerald a6ac532b2b Merge pull request #169 from Emerald-Services/dev
8.0.1 Dev -> Release Branch
2024-07-30 17:47:13 +01:00
1tzEmerald 4f14722e14 8.0.1 Update 2024-07-30 17:45:35 +01:00
1tzEmerald 9a6eece7a1 v8.1 2024-07-09 22:49:49 +01:00
1tzEmerald fd65d0a9a7 8.1-BETA 2024-07-03 13:46:57 +01:00
ItsNyoty 48500fe8a4 Update README.md 2024-07-03 13:42:46 +02:00
1tzEmerald 8d8d9e4bee 8.0.1 Bug Fix 2024-06-13 18:01:41 +01:00
1tzEmerald dcbddf9f23 8.0.1 - Bug Fix 2024-06-13 17:36:44 +01:00
1tzEmerald 7449c0a61a License files 2024-06-12 12:55:45 +01:00
1tzEmerald c95b8ccb32 SupportBot 8.0 2024-06-12 12:54:31 +01:00
1tzEmerald 2c5bfe8e33 Merge pull request #135 from ItsNyoty/patch-1
Update messages.yml
2022-09-05 13:42:52 +01:00
ItsNyoty a3bbd97333 Update messages.yml
TicketExists wasn't defined before
2022-08-29 14:58:01 +02:00
1tzEmerald 4ea488951d Delete oldconfig.yml 2022-08-12 01:38:07 +01:00
1tzEmerald 481424d5a6 Update LICENSE.md 2022-08-12 01:37:49 +01:00
1tzEmerald fe82e96cc1 Update LICENSE.md 2022-08-12 01:37:31 +01:00
1tzEmerald 976258c959 Delete .gitignore 2022-08-12 01:37:14 +01:00
1tzEmerald 83cf0689e2 Delete .DS_Store 2022-08-12 01:37:00 +01:00
ItzEmerald c911cb4423 v8.0 New configuration files 2022-08-12 01:35:36 +01:00
ItzEmerald 4eadef8b65 8.0 Dev 1.2
Reworked configuration files & completed upgrade to V14
2022-08-11 19:19:29 +01:00
ItzEmerald 4e081358ff v8.0-Dev-1.1 2022-07-23 07:50:12 +01:00
ItzEmerald 209ec0b435 v8.0 - Dev-1 2022-07-22 13:19:05 +01:00
Xiled Emerald 6d363abb54 Merge pull request #129 from SypherRed/dev
Better looking readme
2022-07-01 22:19:03 +01:00
SypherRed 6f8b84aeda Update README.md 2022-07-01 23:16:16 +02:00
SypherRed 5b8c5da33a Update README.md 2022-07-01 23:14:01 +02:00
Xiled Emerald b6ef0f95cb Merge pull request #126 from SypherRed/release
Muted Role check and issue fix
2022-06-30 15:49:14 +01:00
SypherRed cce1480a42 remove empty declaration
AboutMe was removed in a previous version.
2022-06-29 13:12:56 +02:00
ItzEmerald 44fbe9f2d6 FINAL 7.7 2022-06-28 03:15:52 +01:00
ItzEmerald 29db6c7d67 7.7 FINAL BETA 2022-06-28 03:14:54 +01:00
SypherRed 26e9b56961 Issue fix with Muted Role check
Fixed the issue to recognize a Muted Role and made changes to ready.js accordingly
2022-06-27 19:03:34 +02:00
SypherRed 979271cfb5 beta push of muted check
push of a check if author has a muted role on ticket opening, if so, decline the interaction

Also either check for missing role or not
2022-06-23 20:58:03 +02:00
ItzEmerald 69f0b00ed3 7.7 2022-06-23 00:05:08 +01:00
ItzEmerald c1a1839457 7.7 BETA 3 2022-06-22 23:30:59 +01:00
Xiled Emerald dd4b53800e Merge pull request #124 from Emerald-Services/add-license-1
Create LICENSE.md
2022-06-22 23:05:38 +01:00
Xiled Emerald 1fc94528fc Merge pull request #123 from SypherRed/release
embed fix and updates
2022-06-22 23:05:25 +01:00
SypherRed bb1a817980 fix previous push of embed
fixed the if-statement of embed
2022-06-22 20:59:45 +02:00
SypherRed 53305acbc7 Fixed embed command
Added a check if the field options are used, if not leave it ignored and don't add some.

This was fixed due to ticket-0038.
2022-06-22 18:21:25 +02:00
Xiled Emerald d0f1bae0bd Create LICENSE.md 2022-06-22 02:24:19 +01:00
SypherRed c0cf3b1bb7 Update several files
* added to .gitignore ".DS_Store"

* changed commented head in "suggest.js" and "translate.js"

* Removed "AboutMe" due it to being unfunctional and can be set in the Developer Portal

* Removed the icon in "ticket-panel.yml"

* Updated dependencies and changed version to match current
2022-06-21 09:54:22 +02:00
Xiled Emerald ce67b668d5 Update Client.js 2022-06-21 01:42:59 +01:00
Xiled Emerald 3273cfd0e1 Update help.js 2022-06-21 01:41:53 +01:00
Xiled Emerald fd992a6514 Update supportbot.yml 2022-06-21 01:41:17 +01:00
Xiled Emerald 550d681fe1 Merge pull request #122 from RandomTomato/release
Added 3 new settings in supportbot.yml for more customization
2022-06-21 01:39:35 +01:00
ItzEmerald 7d29226953 BETA 2 - V7.7 2022-06-21 01:38:22 +01:00
BuildTools 38da72ca91 Added 3 new settings in supportbot.yml for more customization 2022-06-18 11:19:12 +09:30
Xiled Emerald eab11361cb Update ticket-panel-id.json 2022-06-16 22:30:13 +01:00
Xiled Emerald b281bba36f Merge pull request #120 from Emerald-Services/beta
7.6 Release
2022-06-16 22:26:50 +01:00
ItzEmerald 5e38573aed 7.6 2022-06-16 22:12:14 +01:00
ItzEmerald da7b6d487c 7.6-Pre1
Whats New?
  - Ability to add as many departments as you need (Don't go to crazy 😂 )
  - Merged Sypher's Translate & UserInfo Addon into Features

------------

  - Add a role automatically when a user joins the server
  - Announce a welcome message with full cusomtization
  - Announce a leave message with full cusomtization
  - Added "ephermal" to majority messages.

Install modules use - npm install
2022-06-16 02:31:58 +01:00
Xiled Emerald 6bf4e5f4bb Update README.md 2022-06-14 23:02:12 +01:00
Xiled Emerald 213f532a3c Merge pull request #114 from Emerald-Services/dev
7.5 push
2022-02-27 04:37:24 +00:00
ItzEmerald 45bd88e4d2 7.5 commit 2022-02-27 04:30:15 +00:00
Xiled Emerald 4eb8658459 Update supportbot.yml 2022-02-27 00:39:36 +00:00
Noah b37118fee2 Fix addon handler 2022-02-25 21:57:54 -06:00
Noah 026caba68a Fix setFooters 2022-02-25 21:55:33 -06:00
Noah 2003f8801a Fixed ticket perms 2022-02-25 21:51:22 -06:00
Noah 473bacf13a Removed Guild 2022-02-17 19:57:37 -06:00
Noah 8b3f2bc61a Fix missing user or subuser 2022-02-17 19:57:11 -06:00
Noah 3aa373c33c fix bad code 2022-02-11 23:47:06 -06:00
Noah 9b6263e583 Fix DM help menu 2022-02-11 23:41:02 -06:00
Noah 3e066d08ce New Departments 2022-02-11 23:34:38 -06:00
Noah aa48e28162 Fix Typos 2022-02-04 20:00:31 -06:00
Noah dc660ed94a Bug Fixes 2022-02-04 17:56:11 -06:00
Noah e02ecf9553 Bug fixes 2022-02-04 17:55:02 -06:00
Noah 23acc0926f Fix BitField Error 2022-02-04 17:49:02 -06:00
Xiled Emerald 3fc1cd18d5 Merge pull request #109 from Emerald-Services/dev
7.4.2 Pull Request
2022-02-03 17:26:54 +00:00
Noah ebe29b403e Prettier 2022-01-30 00:07:04 -06:00
Noah dcba3312dd Misc 2022-01-29 23:57:10 -06:00
Noah 7fddeeec37 Change Max to 100 2022-01-10 17:48:31 -06:00
Noah b5341b023f Fix embed command 2021-12-29 10:20:29 -06:00
Noah bbb7443ef2 Fix suggest command 2021-12-28 19:06:35 -06:00
Noah 22df347bb9 Merge pull request #108 from astranger1k/dev
Updated Documentation URLs + Added invite URL to log upon launch.
2021-12-23 09:18:47 -06:00
Kieran Davies 9aed6b8f35 Updated Documentation URLs + Added invite URL to log upon launch. 2021-12-23 11:48:51 +00:00
Noah 9e7ec9f1bd 7.4.2 2021-12-19 13:06:32 -06:00
Noah 373f43eac5 Channel can now be id or name 2021-12-19 13:03:33 -06:00
Noah 6c91b2791a Fix users can't send messages 2021-12-19 13:02:43 -06:00
Noah bde62a431f Merge branch 'dev' into release 2021-12-18 22:59:41 -06:00
Noah 615a074540 move filter 2021-12-18 22:59:05 -06:00
Noah 09b335ac46 Update ticket.js 2021-12-18 22:42:51 -06:00
Noah 2701880c77 Merge pull request #106 from Emerald-Services/dev
v7.4.1 Release
2021-12-18 22:37:29 -06:00
Noah 24c8f2fb38 v7.4.1 2021-12-18 22:28:08 -06:00
Noah c3ec158ac3 Fix id 2021-12-18 22:23:18 -06:00
Noah b3690222d0 Fixed id 2021-12-18 22:18:05 -06:00
Noah 4d7583785b Fixed ticket id 2021-12-18 22:14:11 -06:00
Noah 17ad1a15db Delete .DS_Store 2021-12-18 21:32:21 -06:00
Noah 4c89de9e76 Merge pull request #103 from Emerald-Services/dev
v7.4 Release
2021-12-18 21:31:56 -06:00
Noah 5f25af41f9 Merge branch 'release' into dev 2021-12-18 21:30:53 -06:00
Noah 3a3ee357fc Update packages 2021-12-18 21:17:06 -06:00
Noah 08a6d27903 Change to ephemeral 2021-12-18 20:56:30 -06:00
Noah 33f7675aac Added Max Ticket Count + Timeout 2021-12-18 20:54:01 -06:00
Noah 1178a0a611 Add StaffOnly close option 2021-12-18 19:39:59 -06:00
Noah 96ded77bec Fixed perms bug 2021-12-18 19:24:39 -06:00
Noah e83ca8cb2b add guild 2021-12-18 18:58:02 -06:00
Noah e507176d39 New addon handler 2021-12-18 18:57:45 -06:00
Noah 4e554b8bee rename slashCommandOptions to options, and permission to permissions 2021-12-18 18:57:26 -06:00
Noah bc649caa99 remove unused code 2021-12-18 18:56:25 -06:00
Noah f7a4c7b616 rename var + remove unused code 2021-12-18 18:56:02 -06:00
Noah 2c7be5882c Added getRole, getChannel, getCategory functions 2021-12-15 00:19:40 -06:00
Noah 6107d268f7 Fixed close + Send transcript to subusers 2021-12-14 18:04:38 -06:00
Noah 7760699155 Update node.js.yml 2021-12-04 21:30:48 -06:00
Noah acf349c72e Fix test mode 2021-12-04 21:26:55 -06:00
Noah 8096e5b882 Add npm test 2021-12-04 21:25:02 -06:00
Noah 873cfc7dc2 Merge branch 'dev' of https://github.com/Emerald-Services/SupportBot into dev 2021-12-04 21:22:10 -06:00
Noah 038174bc7e temp 2021-12-04 21:22:08 -06:00
Noah 14881096bb Update node.js.yml 2021-12-04 21:22:07 -06:00
Noah ed1905d8f4 Added test mode 2021-12-04 21:19:51 -06:00
Noah 72c0a2efa3 Update node.js.yml 2021-12-04 21:07:27 -06:00
Noah e599d48713 Update node.js.yml 2021-12-04 21:05:36 -06:00
Noah 7f44634d4f Delete prettier.yml 2021-12-04 21:04:47 -06:00
Noah 44170caac6 Update supportbot.yml 2021-12-04 21:01:34 -06:00
Noah adedfadd92 Prettier 2021-12-04 20:58:19 -06:00
Noah 6b01d0c8fe Possibly fix /close? 2021-12-04 20:57:23 -06:00
Noah d4d96f59ab Update 2021-12-04 20:25:49 -06:00
Noah a1bf7ed6bd Fix syntax 2021-12-04 20:25:35 -06:00
Noah 306e20efc9 Merge pull request #99 from DarkerInker/patch-1
wrong way to send embeds
2021-12-04 20:21:07 -06:00
Noah f593f6e0a8 Update close.js 2021-12-04 20:20:55 -06:00
DarkerInker 74646a4318 wrong way to send embeds
You are using the old way to send embeds. I'm not sure how you forgot about this one area but everyone makes mistakes :D
2021-12-02 13:52:07 -06:00
Noah 8bfcba8832 Merge pull request #97 from SypherRed/dev
fixed help command
2021-11-28 23:35:31 -06:00
SypherRed 4ba5621a4d Merge branch 'dev' into dev 2021-11-28 14:55:48 +01:00
N0ahMC 1f928324c2 Prettified Code! 2021-11-26 19:18:15 +00:00
Noah 3771872cd2 Update prettier.yml 2021-11-26 13:17:57 -06:00
Noah f359a60343 Update node.js.yml 2021-11-26 13:16:15 -06:00
Noah f53afd7bc9 Create prettier.yml 2021-11-26 13:13:15 -06:00
Noah e7dcc2ab29 Update help.js 2021-11-26 12:56:21 -06:00
SypherRed 0118b9e591 fixed help command 2021-11-25 01:51:56 +01:00
Noah ffe42f9777 Update ticket.js 2021-11-24 18:14:07 -06:00
Noah 5a467599e9 Update hello.js 2021-11-24 00:15:16 -06:00
Noah c56e78484a Update dependencies 2021-11-23 20:29:22 -06:00
Noah 1ee0d4a09e Update close.js 2021-11-23 18:09:06 -06:00
Noah ffdce5820b Fix close button 2021-11-23 18:08:45 -06:00
Noah 9fed504f37 Fixed /close 2021-11-23 17:37:25 -06:00
Noah 6c5799be0d Fix ID 2021-11-23 17:32:57 -06:00
Noah b6747a0340 Made warning ephemeral 2021-11-23 16:51:28 -06:00
Noah cd6df02e30 Changed TicketLog_Title 2021-11-23 15:50:56 -06:00
Noah edbef6a36f Fixed close command 2021-11-23 15:49:37 -06:00
Noah b6a0016f5f Changed TicketData Structure 2021-11-23 15:48:45 -06:00
Noah e740c17837 Update package-lock.json 2021-11-21 18:42:24 -06:00
Noah 7eb949f109 Better finds 2021-11-21 18:42:18 -06:00
Noah 02394e310d Exit if joins or leaves guild 2021-11-17 19:04:38 -06:00
Noah ef83832819 Added missing role and channel check 2021-11-17 19:04:20 -06:00
Noah a82101ab30 Register commands in Guild 2021-11-17 19:03:35 -06:00
Noah 007c78930e Added Guild to config 2021-11-17 19:03:11 -06:00
Noah 300c83512c Fixed token, and changed version for future 2021-11-11 17:18:30 -06:00
Noah b674872c56 Fixed Archive Bug 2021-11-10 21:41:12 -06:00
Noah 12a7680275 Fix spelling 2021-11-10 21:23:07 -06:00
Noah c9cf7c942b Prettier 2021-11-10 21:22:08 -06:00
Noah 8e0326c8f8 Removed HelpMenu 2021-11-10 21:21:50 -06:00
Noah 13972119a8 Update package-lock.json 2021-11-10 21:21:05 -06:00
Noah acf165a119 Add Button Emoji 2021-11-10 21:18:49 -06:00
Xiled Emerald bdff7c6069 Update supportbot.yml 2021-11-07 21:39:53 +00:00
Xiled Emerald e2e3376c87 Merge pull request #91 from Emerald-Services/dev
Dev
2021-10-29 23:38:32 +01:00
Xiled Emerald 3c613088e5 Delete supportbot.yml 2021-10-29 23:38:18 +01:00
Xiled Emerald 7e9ece50a0 Update issue templates 2021-10-29 22:40:12 +01:00
Xiled Emerald af81b6536a Update issue templates 2021-10-29 22:38:13 +01:00
ItzEmerald 4cd3ecf81f 7.3 2021-10-29 19:12:11 +01:00
Noah 31f6b9e08e Fix suggestions 2021-10-28 19:08:06 -05:00
Noah 02de920cb3 Update package.json 2021-10-28 18:39:06 -05:00
Noah 84c0905297 Update version numbers 2021-10-28 18:07:43 -05:00
Noah 250583f859 Delete commands directory 2021-10-28 18:04:26 -05:00
Noah ee7d42196d Create remove.js 2021-10-28 18:03:51 -05:00
Noah a397de2b9f Create close.js 2021-10-28 18:03:27 -05:00
Noah 0610df9400 Create add.js 2021-10-28 18:03:06 -05:00
Noah ed583deb31 Updated css 2021-10-27 20:44:10 -05:00
Noah e039048fc6 Updated role defaults 2021-10-27 20:24:26 -05:00
Noah f2ea9650c4 Renamed file 2021-10-27 20:24:10 -05:00
Noah 1e89057006 Added font 2021-10-27 20:19:33 -05:00
Noah 019bb4842f Fixed bug 2021-10-27 20:19:27 -05:00
Noah 4cb45beded Fixed bug 2021-10-27 20:18:06 -05:00
Noah 8a2059f087 Fixed suggest command bug 2021-10-27 20:05:45 -05:00
Noah a6b105a2e1 Updated config defaults 2021-10-27 19:48:00 -05:00
Noah 6c14a6a06c Rename 2021-10-27 19:39:07 -05:00
Noah cddd5d0026 Moved configs from Data to Configs 2021-10-27 19:30:10 -05:00
Noah 78dea91e1d Prettier 2021-10-27 19:20:10 -05:00
Xiled Emerald 3df591ec52 Update supportbot.yml 2021-10-25 23:19:07 +01:00
Xiled Emerald 1c4032caa6 Create suggest.js 2021-10-25 23:18:28 +01:00
Noah 465c422171 Update README.md 2021-10-25 00:16:00 -05:00
Noah 284e8dc6f5 Fixed spelling 2021-10-25 00:15:54 -05:00
Noah 3fc905c82e Update supportbot.yml 2021-10-25 00:03:28 -05:00
Noah 55a42b88ae Ran prettier 2021-10-25 00:02:23 -05:00
Noah bd26162e4a Fixed bug 2021-10-25 00:02:17 -05:00
Xiled Emerald 62e7e33406 Update Client.js 2021-10-25 00:13:27 +01:00
Xiled Emerald 2f0ddcec3f Merge pull request #77 from 1tzemerald/SupportBot-BETA
SupportBot 7.1 merge to main branch
2021-10-24 22:56:37 +01:00
Xiled Emerald 7cdfa68e0e Delete supportbot-config.yml 2021-10-24 22:56:14 +01:00
Xiled Emerald f8939a964d Delete .DS_Store 2021-10-24 22:54:15 +01:00
Xiled Emerald 7830e5dda5 Add files via upload 2021-10-24 22:48:25 +01:00
Xiled Emerald 6aa2f22294 Create ready.js 2021-10-24 22:47:42 +01:00
Xiled Emerald 50b14e3d44 Add files via upload 2021-10-24 22:46:36 +01:00
Xiled Emerald e09c2192c2 Delete events directory 2021-10-24 22:45:44 +01:00
Xiled Emerald b725d2cc62 Delete messageReactionAdd.js 2021-10-24 22:44:51 +01:00
Xiled Emerald abf3f617a4 Delete ticket.js 2021-10-24 22:44:29 +01:00
Xiled Emerald 1eda7e66fe Delete ping.js 2021-10-24 22:44:11 +01:00
ItzEmerald b5000531c4 Merge branch 'SupportBot-BETA' of https://github.com/1tzemerald/SupportBot into SupportBot-BETA 2021-10-24 22:42:49 +01:00
ItzEmerald d4f95e752b SupportBot-7.2-Release 2021-10-24 22:41:46 +01:00
Noah e31c611f70 Removed prefix 2021-10-23 20:17:58 -05:00
Noah 085481bee6 Spelling Mistakes 2021-10-23 20:15:07 -05:00
Noah cf610fbe49 Ran prettier 2021-10-23 20:12:04 -05:00
Noah b681eb3cd2 Create .gitignore 2021-10-23 20:10:49 -05:00
Noah f8ffdf0e81 Delete node_modules 2021-10-23 20:10:16 -05:00
Noah ba38bce1da Updated structures 2021-10-23 20:09:15 -05:00
Noah 3a39b05fe4 Updated packages 2021-10-23 20:08:55 -05:00
Noah 27d2982b28 Removed text commands 2021-10-23 20:07:37 -05:00
Noah 33e4d97be5 Updated for slash commands 2021-10-23 20:04:02 -05:00
Xiled Emerald 654064d7c5 Create messageReactionAdd.js 2021-10-23 22:44:59 +01:00
Noah aeb0da9e0d Removed console.log 2021-10-23 16:26:07 -05:00
Noah cb1a2fe067 Fixed Slash commands 2021-10-23 16:23:02 -05:00
Alexersion 7ad5016966 Unneeded workspace updates removed. 2021-10-13 11:12:08 -04:00
Xiled Emerald 155b768f73 Update supportbot-config.yml 2021-10-04 13:56:30 +01:00
1346 changed files with 19707 additions and 162535 deletions

No files matched your search

Vendored
BIN
View File
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
Configs/supportbot.yml filter=supportbot-token
Configs/api.yml filter=api-secrets
-32
View File
@@ -1,32 +0,0 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x, 15.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm install discord.js --save
- run: npm install js-yaml
- run: npm run build --if-present
- run: node index.js
+21
View File
@@ -0,0 +1,21 @@
node_modules
# Local database, runtime data, and transcripts
Data/*.db
Data/*.db-shm
Data/*.db-wal
Data/Transcripts/*
Data/dashboard-groups.json
Data/dashboard-users.json
Data/transcript-settings.json
# Local log files
Logs/Output/*
Logs/Errors/*
Logs/Warns/*
# Dashboard UI source at repo root only (do not ignore public/dashboard/)
/dashboard/
# Local secrets — use *.example as templates
Configs/api.yml
+2135
View File
File diff suppressed because it is too large. Load diff
+26
View File
@@ -0,0 +1,26 @@
const Discord = require("discord.js");
const fs = require("fs");
const yaml = require("js-yaml");
const { Command } = require('../Structures/Addon.js'); // Adjust the path as needed
const supportbot = require("../Structures/ConfigStore").supportbot;
module.exports = new Command({
name: "addon",
description: "This is a sample addon!",
options: [],
permissions: ["Administrator"],
async run(interaction) {
let disableCommand = true;
const PingEmbed = new Discord.EmbedBuilder()
.setDescription(
`This is a sample addon.\n\nPong! \`${interaction.client.ws.ping} ms\``
)
.setColor(supportbot.Embed.Colours.General);
interaction.reply({
embeds: [PingEmbed],
});
},
});
-34
View File
@@ -1,34 +0,0 @@
// SupportBot | Emerald Services
// Example (default) Addon
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const cmdconfig = yaml.load(fs.readFileSync('./Data/commands.yml', 'utf8'));
// Your Addon Config file! Saved in /Addons/Settings/YourAddonConig.yml
// const exampleaddon = yaml.load(fs.readFileSync('./Addons/Settings/YourAddonConig.yml', 'utf8'));
const Command = require("../Structures/Command.js");
module.exports = new Command({
name: "hello",
description: "hello",
type: "BOTH",
slashCommandOptions: [],
permission: "SEND_MESSAGES",
async run(message, args, client) {
const Embed = new Discord.MessageEmbed()
.setDescription("Hello World!")
.setColor(supportbot.InfoColour)
message.channel.send({
embeds: [Embed]
})
}
});
+122
View File
@@ -0,0 +1,122 @@
// SupportBot | Emerald Services
// AI Command (Consolidated)
const fs = require("fs");
const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, MessageFlags } = require("discord.js");
const yaml = require("js-yaml");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
const Command = require("../../Structures/Command.js");
const AIDatabase = require("../../Structures/AIDatabase.js");
module.exports = new Command({
name: cmdconfig.AI?.Command || "ai",
description: cmdconfig.AI?.Description || "Manage SupportBot AI settings and memory.",
type: ApplicationCommandType.ChatInput,
options: [
{
name: cmdconfig.AI?.Train?.Command || "train",
description: cmdconfig.AI?.Train?.Description || "Train the AI by adding knowledge to its database.",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "content",
description: "The text/knowledge you want the bot to learn",
type: ApplicationCommandOptionType.String,
required: true,
},
],
},
{
name: cmdconfig.AI?.Knowledge?.Command || "knowledge",
description: cmdconfig.AI?.Knowledge?.Description || "Manage the AI knowledge base.",
type: ApplicationCommandOptionType.SubcommandGroup,
options: [
{
name: cmdconfig.AI?.Knowledge?.Clear?.Command || "clear",
description: cmdconfig.AI?.Knowledge?.Clear?.Description || "Clear all trained knowledge from the AI.",
type: ApplicationCommandOptionType.Subcommand,
},
],
},
{
name: cmdconfig.AI?.Memory?.Command || "memory",
description: cmdconfig.AI?.Memory?.Description || "Manage the AI conversational memory.",
type: ApplicationCommandOptionType.SubcommandGroup,
options: [
{
name: cmdconfig.AI?.Memory?.Clear?.Command || "clear",
description: cmdconfig.AI?.Memory?.Clear?.Description || "Clear recent conversation memory for this channel.",
type: ApplicationCommandOptionType.Subcommand,
},
],
},
],
permissions: cmdconfig.AI?.Permission || ["Administrator"],
async run(interaction) {
if (!supportbotai.Enabled) {
return interaction.reply({
content: "SupportBot AI is currently disabled.",
flags: MessageFlags.Ephemeral,
});
}
const subcommandGroup = interaction.options.getSubcommandGroup(false);
const subcommand = interaction.options.getSubcommand(false);
try {
if (subcommand === "train" || subcommand === cmdconfig.AI?.Train?.Command) {
const content = interaction.options.getString("content");
AIDatabase.addKnowledge({
sourceType: "user_command",
sourceName: interaction.user.tag,
title: "Trained Knowledge",
content: content,
});
const SuccessEmbed = new EmbedBuilder()
.setDescription(`✅ **Knowledge Added!**\n\nThe AI has learned the following information:\n\`\`\`${content}\`\`\``)
.setColor(supportbot.Embed.Colours.Success);
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
}
if (subcommandGroup === (cmdconfig.AI?.Knowledge?.Command || "knowledge")) {
if (subcommand === (cmdconfig.AI?.Knowledge?.Clear?.Command || "clear")) {
AIDatabase.clearKnowledge();
const SuccessEmbed = new EmbedBuilder()
.setDescription(`✅ **Knowledge Cleared!**\n\nAll manually trained knowledge has been wiped from the database.`)
.setColor(supportbot.Embed.Colours.Success);
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
}
}
if (subcommandGroup === (cmdconfig.AI?.Memory?.Command || "memory")) {
if (subcommand === (cmdconfig.AI?.Memory?.Clear?.Command || "clear")) {
AIDatabase.clearConversations(interaction.channel.id);
const SuccessEmbed = new EmbedBuilder()
.setDescription(`✅ **Memory Cleared!**\n\nThe AI's recent conversation memory in this channel has been wiped. It will no longer remember previous messages sent here.`)
.setColor(supportbot.Embed.Colours.Success);
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
}
}
} catch (error) {
console.error("AI Command Error:", error);
const ErrorEmbed = new EmbedBuilder()
.setDescription(`❌ **Error:** Failed to process the AI command.`)
.setColor(supportbot.Embed.Colours.Error);
return interaction.reply({ embeds: [ErrorEmbed], flags: MessageFlags.Ephemeral });
}
},
});
+62
View File
@@ -0,0 +1,62 @@
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const Command = require("../../Structures/Command.js");
module.exports = new Command({
name: cmdconfig.Help.Command,
description: cmdconfig.Help.Description,
type: Discord.ApplicationCommandType.ChatInput,
options: [],
permissions: cmdconfig.Help.Permission,
async run(interaction) {
const { getRole } = interaction.client;
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
if (!SupportStaff || !Admin) {
return interaction.reply(
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
);
}
// Separate regular commands and addon commands
let botCommands = "";
let addonCommands = "";
interaction.client.commands.forEach((command) => {
if (command.permissions) {
if (command.addon) {
addonCommands += `\`${command.name}\` `;
} else {
botCommands += `\`${command.name}\` `;
}
}
});
// Create embed
const HelpEmbed1 = new Discord.EmbedBuilder()
.setTitle(supportbot.General.Name + " Commands")
.setThumbnail(interaction.user.displayAvatarURL())
.setColor(supportbot.Embed.Colours.General)
.addFields({
name: "🖥️ Commands\n",
value: botCommands || "No commands available.",
inline: false,
});
HelpEmbed1.setFooter({
text: supportbot.Embed.Footer,
iconURL: interaction.user.displayAvatarURL(),
});
await interaction.reply({
flags: Discord.MessageFlags.Ephemeral,
embeds: [HelpEmbed1],
});
},
});
+40
View File
@@ -0,0 +1,40 @@
// SupportBot | Emerald Services
// Info Command
const Discord = require("discord.js");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const msgconfig = require("../../Structures/ConfigStore").messages;
const Command = require("../../Structures/Command.js");
module.exports = new Command({
name: cmdconfig.Info.Command,
description: cmdconfig.Info.Description,
options: [],
permissions: cmdconfig.Info.Permission,
async run(interaction) {
let disableCommand = true;
const InfoButton = new Discord.ButtonBuilder()
.setLabel(msgconfig.Info.Button)
.setURL(msgconfig.Info.URL)
.setStyle("Link");
const inforow = new Discord.ActionRowBuilder().addComponents(InfoButton);
const InfoEmbed = new Discord.EmbedBuilder()
.setURL(msgconfig.Info.URL)
.setTitle(msgconfig.Info.Title)
.setDescription(msgconfig.Info.Description)
.setColor(msgconfig.Info.Colour)
interaction.reply({
embeds: [InfoEmbed],
components: [inforow],
});
},
});
+30
View File
@@ -0,0 +1,30 @@
// SupportBot | Emerald Services
// Ping Command
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const Command = require("../../Structures/Command.js");
module.exports = new Command({
name: cmdconfig.Ping.Command,
description: cmdconfig.Ping.Description,
options: [],
permissions: cmdconfig.Ping.Permission,
async run(interaction) {
const PingEmbed = new Discord.EmbedBuilder()
.setDescription(
`:ping_pong: **Ping:** \`${interaction.client.ws.ping} ms\``,
)
.setColor(supportbot.Embed.Colours.General);
await interaction.reply({
embeds: [PingEmbed],
});
},
});
+503
View File
@@ -0,0 +1,503 @@
const {
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ApplicationCommandType,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ApplicationCommandOptionType,
MessageFlags,
ContainerBuilder,
TextDisplayBuilder,
SeparatorBuilder,
ThumbnailBuilder,
MediaGalleryBuilder,
MediaGalleryItemBuilder,
} = require("discord.js");
const fs = require("fs");
const yaml = require("js-yaml");
const db = require("../../Structures/Database.js");
const Command = require("../../Structures/Command.js");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const msgconfig = require("../../Structures/ConfigStore").messages;
const clockedInUsers = new Set();
function safeGetProfile(userId) {
try {
if (typeof db.getProfile !== "function") {
console.error("db.getProfile is not a function");
return {
bio: "",
timezone: "",
clockedIn: false,
};
}
const profile = db.getProfile(userId);
return {
bio: profile?.bio || "",
timezone: profile?.timezone || "",
clockedIn: Boolean(profile?.clockedIn),
};
} catch (error) {
console.error("Error getting profile:", error);
return {
bio: "",
timezone: "",
clockedIn: false,
};
}
}
function safeSetClockedIn(userId, value) {
try {
if (typeof db.setClockedIn !== "function") {
console.error("db.setClockedIn is not a function");
return false;
}
db.setClockedIn(userId, value);
return true;
} catch (error) {
console.error("Error setting clocked in status:", error);
return false;
}
}
function buildProfileContainer({
viewingUser,
viewingUserId,
bio,
timezone,
clockedIn,
isStaff,
}) {
const container = new ContainerBuilder();
if (supportbot.Embed?.Colours?.General) {
container.setAccentColor(
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
);
}
const avatarUrl = viewingUser.displayAvatarURL({ size: 128, extension: "png" });
const gallery = new MediaGalleryBuilder().addItems(
new MediaGalleryItemBuilder().setURL(avatarUrl)
);
container.addMediaGalleryComponents(gallery);
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`## ${viewingUser.username}'s Profile`)
);
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
[
`**Bio:** ${bio || "No bio set."}`,
`**Timezone:** ${timezone || "No timezone set."}`,
...(isStaff
? [`**Clocked In:** ${clockedIn ? "✅ Clocked In" : "❌ Clocked Out"}`]
: []),
].join("\n")
)
);
return container;
}
function buildProfileButtons({ isOwnProfile, isStaff, clockedIn }) {
const row = new ActionRowBuilder();
if (isOwnProfile) {
row.addComponents(
new ButtonBuilder()
.setCustomId("editProfile")
.setLabel("Edit Profile")
.setStyle(ButtonStyle.Secondary),
);
if (isStaff) {
row.addComponents(
new ButtonBuilder()
.setCustomId("clockInOut")
.setLabel(clockedIn ? "Clock Out" : "Clock In")
.setStyle(ButtonStyle.Secondary),
);
}
}
if (isStaff && supportbot.Ticket?.ClaimTickets?.Enabled) {
row.addComponents(
new ButtonBuilder()
.setCustomId("viewTicketStats")
.setLabel("View Ticket Stats")
.setStyle(ButtonStyle.Secondary),
);
}
return row.components.length ? [row] : [];
}
function buildTicketStatsContainer(viewingUserId, ticketsClaimed, ticketsOpen, avgMinutes) {
const container = new ContainerBuilder();
if (supportbot.Embed?.Colours?.General) {
container.setAccentColor(
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
);
}
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`# ${msgconfig.TicketStats.Title || "Ticket Stats"}`),
);
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
[
`> Ticket stats for <@${viewingUserId}>`,
``,
`**${msgconfig.TicketStats.OpenTickets || "Open Tickets"}:** ${ticketsOpen.length}`,
`**${msgconfig.TicketStats.TotalTickets || "Total Tickets"}:** ${ticketsClaimed.length}`,
`**${msgconfig.TicketStats.ResponseTime || "Average Response Time"}:** ${avgMinutes} minutes`,
].join("\n"),
),
);
return container;
}
function getStaffTicketStats(viewingUserId) {
const allTickets = typeof db.getAllTickets === "function" ? db.getAllTickets() : [];
const ticketsClaimed = allTickets.filter(
(t) => t.claimedBy === viewingUserId || t.claimed_by === viewingUserId,
);
const ticketsOpen = ticketsClaimed.filter(
(t) => (t.status || "").toLowerCase() === "open",
);
let totalResponseTime = 0;
let responseCount = 0;
ticketsClaimed.forEach((t) => {
const claimedAt = t.claimedAt || t.claimed_at;
const createdAt = t.createdAt || t.created_at;
if (claimedAt && createdAt) {
const diff = new Date(claimedAt).getTime() - new Date(createdAt).getTime();
if (diff > 0) {
totalResponseTime += diff;
responseCount++;
}
}
});
const avgMinutes = responseCount
? Math.round(totalResponseTime / responseCount / 60000)
: 0;
return { ticketsClaimed, ticketsOpen, avgMinutes };
}
function buildOpenTicketsContainer(ticketsOpen) {
const container = new ContainerBuilder();
if (supportbot.Embed?.Colours?.General) {
container.setAccentColor(
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
);
}
container.addTextDisplayComponents(new TextDisplayBuilder().setContent(`# Active Tickets`));
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
ticketsOpen.length
? ticketsOpen.map((t) => `<#${t.ticket_id || t.id}>`).join("\n")
: "No active tickets.",
),
);
return container;
}
module.exports = new Command({
name: cmdconfig.Profile.Command,
description: cmdconfig.Profile.Description,
type: ApplicationCommandType.ChatInput,
permissions: cmdconfig.Profile.Permission,
options: [
{
name: "user",
description: "The user to view the profile of",
type: ApplicationCommandOptionType.User,
required: false,
},
],
async run(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
const viewingUser = interaction.options.getUser("user") || interaction.user;
const viewingUserId = viewingUser.id;
const interactionUserId = interaction.user.id;
const member = await interaction.guild.members.fetch(viewingUserId);
const { getRole } = interaction.client;
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
if (!Staff || !Admin) {
return interaction.editReply({
content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
});
}
const isStaff = member.roles.cache.has(Staff.id) || member.roles.cache.has(Admin.id);
const isOwnProfile = interactionUserId === viewingUserId;
if (typeof db.ensureProfile === "function") {
db.ensureProfile(viewingUserId);
}
let { bio, timezone, clockedIn } = safeGetProfile(viewingUserId);
const profileContainer = buildProfileContainer({
viewingUser,
viewingUserId,
bio,
timezone,
clockedIn,
isStaff,
});
const buttonRows = buildProfileButtons({
isOwnProfile,
isStaff,
clockedIn,
});
await interaction.editReply({
flags: MessageFlags.IsComponentsV2,
components: [profileContainer, ...buttonRows],
});
const filter = (i) =>
[
"editProfile",
"clockInOut",
"viewTicketStats",
"showOpenTickets",
"backToStats",
"backToProfile",
].includes(i.customId) && i.user.id === interactionUserId;
const collector = interaction.channel.createMessageComponentCollector({
filter,
time: 60000,
});
collector.on("collect", async (i) => {
if (i.customId === "editProfile") {
const modal = new ModalBuilder()
.setCustomId("editProfileModal")
.setTitle("Edit Profile")
.addComponents(
new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId("bio")
.setLabel("Bio")
.setStyle(TextInputStyle.Paragraph)
.setPlaceholder("Enter your bio")
.setValue(String(bio || ""))
.setRequired(false),
),
new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId("timezone")
.setLabel("Timezone")
.setStyle(TextInputStyle.Short)
.setPlaceholder("Enter your timezone")
.setValue(String(timezone || ""))
.setRequired(false),
),
);
return i.showModal(modal);
}
if (i.customId === "clockInOut") {
const newStatus = !clockedIn;
const saved = safeSetClockedIn(viewingUserId, newStatus);
if (!saved) {
return i.reply({
content:
"Could not update clock in status because the profile database is not available.",
flags: MessageFlags.Ephemeral,
});
}
clockedIn = newStatus;
if (newStatus) clockedInUsers.add(viewingUserId);
else clockedInUsers.delete(viewingUserId);
const updatedProfileContainer = buildProfileContainer({
viewingUser,
viewingUserId,
bio,
timezone,
clockedIn,
isStaff,
});
const updatedButtonRows = buildProfileButtons({
isOwnProfile,
isStaff,
clockedIn,
});
await i.update({
flags: MessageFlags.IsComponentsV2,
components: [updatedProfileContainer, ...updatedButtonRows],
});
return interaction.followUp({
content: newStatus
? "You have successfully clocked in."
: "You have successfully clocked out.",
flags: MessageFlags.Ephemeral,
});
}
if (i.customId === "viewTicketStats") {
const { ticketsClaimed, ticketsOpen, avgMinutes } = getStaffTicketStats(viewingUserId);
const statsContainer = buildTicketStatsContainer(
viewingUserId,
ticketsClaimed,
ticketsOpen,
avgMinutes,
);
const statsRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("showOpenTickets")
.setLabel("Show Open Tickets")
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId("backToProfile")
.setLabel("Back to Profile")
.setStyle(ButtonStyle.Secondary),
);
return i.update({
flags: MessageFlags.IsComponentsV2,
components: [statsContainer, statsRow],
});
}
if (i.customId === "showOpenTickets") {
const { ticketsOpen } = getStaffTicketStats(viewingUserId);
const openContainer = buildOpenTicketsContainer(ticketsOpen);
const backRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("backToStats")
.setLabel("Back to Stats")
.setStyle(ButtonStyle.Secondary),
);
return i.update({
flags: MessageFlags.IsComponentsV2,
components: [openContainer, backRow],
});
}
if (i.customId === "backToStats") {
const { ticketsClaimed, ticketsOpen, avgMinutes } = getStaffTicketStats(viewingUserId);
const statsContainer = buildTicketStatsContainer(
viewingUserId,
ticketsClaimed,
ticketsOpen,
avgMinutes,
);
const statsRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("showOpenTickets")
.setLabel("Show Open Tickets")
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId("backToProfile")
.setLabel("Back to Profile")
.setStyle(ButtonStyle.Secondary),
);
return i.update({
flags: MessageFlags.IsComponentsV2,
components: [statsContainer, statsRow],
});
}
if (i.customId === "backToProfile") {
const updatedProfileContainer = buildProfileContainer({
viewingUser,
viewingUserId,
bio,
timezone,
clockedIn,
isStaff,
});
const updatedButtonRows = buildProfileButtons({
isOwnProfile,
isStaff,
clockedIn,
});
return i.update({
flags: MessageFlags.IsComponentsV2,
components: [updatedProfileContainer, ...updatedButtonRows],
});
}
});
} catch (error) {
console.error("Error executing profile command:", error);
if (interaction.deferred || interaction.replied) {
return interaction
.editReply({
content: "An error occurred while loading the profile.",
components: [],
})
.catch(() => {});
}
return interaction
.reply({
content: "An error occurred while loading the profile.",
flags: MessageFlags.Ephemeral,
})
.catch(() => {});
}
},
});
module.exports.clockedInUsers = clockedInUsers;
+206
View File
@@ -0,0 +1,206 @@
// SupportBot | Emerald Services
// Translate Command
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const { translate } = require("@vitalets/google-translate-api");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const msgconfig = require("../../Structures/ConfigStore").messages;
const Command = require("../../Structures/Command.js");
const LANGUAGES = [
{ name: "Auto Detect", value: "auto" },
{ name: "English", value: "en" },
{ name: "Spanish", value: "es" },
{ name: "French", value: "fr" },
{ name: "German", value: "de" },
{ name: "Italian", value: "it" },
{ name: "Portuguese", value: "pt" },
{ name: "Dutch", value: "nl" },
{ name: "Polish", value: "pl" },
{ name: "Russian", value: "ru" },
{ name: "Ukrainian", value: "uk" },
{ name: "Turkish", value: "tr" },
{ name: "Arabic", value: "ar" },
{ name: "Hebrew", value: "he" },
{ name: "Hindi", value: "hi" },
{ name: "Japanese", value: "ja" },
{ name: "Korean", value: "ko" },
{ name: "Chinese (Simplified)", value: "zh-cn" },
{ name: "Chinese (Traditional)", value: "zh-tw" },
{ name: "Thai", value: "th" },
{ name: "Vietnamese", value: "vi" },
{ name: "Indonesian", value: "id" },
{ name: "Greek", value: "el" },
{ name: "Swedish", value: "sv" },
{ name: "Romanian", value: "ro" },
];
function buildContainer(title, lines = [], color = null) {
const container = new Discord.ContainerBuilder();
if (color) {
container.setAccentColor(parseInt(color.replace("#", ""), 16));
}
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`));
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n")));
return container;
}
function trim(text, max = 1800) {
if (text.length <= max) return text;
return text.substring(0, max) + "...";
}
module.exports = new Command({
name: cmdconfig.Translate.Command,
description: cmdconfig.Translate.Description,
type: Discord.ApplicationCommandType.ChatInput,
options: [
{
type: Discord.ApplicationCommandOptionType.String,
name: "from",
description: "Language to translate FROM",
required: true,
choices: LANGUAGES,
},
{
type: Discord.ApplicationCommandOptionType.String,
name: "to",
description: "Language to translate TO",
required: true,
choices: LANGUAGES.filter((l) => l.value !== "auto"),
},
{
type: Discord.ApplicationCommandOptionType.String,
name: "text",
description: "Text to translate",
required: true,
},
],
permissions: cmdconfig.Translate.Permission,
async run(interaction) {
const { getRole, getChannel } = interaction.client;
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
if (!Staff || !Admin) {
return interaction.reply({
content: "Some roles seem to be missing!",
flags: Discord.MessageFlags.Ephemeral,
});
}
if (
!interaction.member.roles.cache.has(Staff.id) &&
!interaction.member.roles.cache.has(Admin.id)
) {
return interaction.reply({
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
components: [
buildContainer(
"Invalid Permissions",
[
msgconfig.Error.IncorrectPerms,
"",
`Required role: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
],
supportbot.Embed.Colours.Warn,
),
],
});
}
await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
const fromLang = interaction.options.getString("from");
const toLang = interaction.options.getString("to");
const text = interaction.options.getString("text");
const translateLog = await getChannel(supportbot.Translate?.TranslateLog, interaction.guild);
let result;
try {
result = await translate(text, {
from: fromLang === "auto" ? undefined : fromLang,
to: toLang,
});
} catch (err) {
console.error("Translate error:", err);
return interaction.editReply({
flags: Discord.MessageFlags.IsComponentsV2,
components: [
buildContainer(
"Translation Failed",
["The translation service returned an error."],
supportbot.Embed.Colours.Warn,
),
],
});
}
const detectedLanguage = result.from?.language?.iso || result.from?.language || "unknown";
const container = buildContainer(
"Translation",
[
`**Requested By:** <@${interaction.user.id}>`,
`**From:** \`${fromLang === "auto" ? detectedLanguage : fromLang}\``,
`**To:** \`${toLang}\``,
"",
"**Original Text**",
trim(text),
"",
"**Translated Text**",
trim(result.text),
],
supportbot.Embed.Colours.Success,
);
await interaction.editReply({
flags: Discord.MessageFlags.IsComponentsV2,
components: [container],
});
if (translateLog) {
const logContainer = buildContainer(
"Translation Log",
[
`User: <@${interaction.user.id}>`,
`Channel: <#${interaction.channel.id}>`,
`From: ${fromLang}`,
`To: ${toLang}`,
"",
trim(text),
"",
trim(result.text),
],
supportbot.Embed.Colours.General,
);
translateLog
.send({
flags: Discord.MessageFlags.IsComponentsV2,
components: [logContainer],
})
.catch(() => {});
}
},
});
+147
View File
@@ -0,0 +1,147 @@
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const path = require("path");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const msgconfig = require("../../Structures/ConfigStore").messages;
const Command = require("../../Structures/Command.js");
const DB = require("../../Structures/Database.js");
module.exports = new Command({
name: "suggest",
description: cmdconfig.Suggestion?.Description || "Submit a new suggestion",
type: Discord.ApplicationCommandType.ChatInput,
options: [
{
name: "text",
description: "Your suggestion text",
type: Discord.ApplicationCommandOptionType.String,
required: true
}
],
async run(interaction) {
const suggestion = interaction.options.getString("text");
const mode = supportbot.Suggestions.Mode || "Channel";
const embed = new Discord.EmbedBuilder()
.addFields(
{ name: "Suggestion", value: suggestion, inline: true },
{ name: "From", value: `<@${interaction.user.id}>` },
{ name: `${supportbot.Suggestions.UpvoteEmoji} ${supportbot.Suggestions.UpvoteTitle}`, value: "0", inline: true },
{ name: `${supportbot.Suggestions.DownvoteEmoji} ${supportbot.Suggestions.DownvoteTitle}`, value: "0", inline: true }
)
.setThumbnail(interaction.user.displayAvatarURL())
.setColor(supportbot.Embed.Colours.General);
const row = new Discord.ActionRowBuilder().addComponents(
new Discord.ButtonBuilder()
.setCustomId("upvote")
.setEmoji(supportbot.Suggestions.UpvoteEmoji)
.setStyle(supportbot.Suggestions.Buttons.Upvote),
new Discord.ButtonBuilder()
.setCustomId("downvote")
.setEmoji(supportbot.Suggestions.DownvoteEmoji)
.setStyle(supportbot.Suggestions.Buttons.Downvote),
new Discord.ButtonBuilder()
.setCustomId("removevote")
.setLabel(supportbot.Suggestions.Buttons.RemoveVote_Title)
.setStyle(supportbot.Suggestions.Buttons.RemoveVote)
);
if (mode === "Forum") {
const forum = await interaction.guild.channels
.fetch(supportbot.Suggestions.ForumChannel)
.catch(() => null);
if (!forum || forum.type !== Discord.ChannelType.GuildForum) {
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle("⚠ Missing Forum Channel")
.setDescription(msgconfig.Error.MissingChannel)
.setColor(supportbot.Embed.Colours.Error)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
const appliedTags = supportbot.Suggestions.Tags?.Created
? [supportbot.Suggestions.Tags.Created]
: [];
const thread = await forum.threads.create({
name: suggestion.slice(0, 100),
message: {
content: "\u200B",
embeds: [embed],
components: [row]
},
appliedTags,
autoArchiveDuration: Discord.ThreadAutoArchiveDuration.OneDay,
reason: "New Suggestion"
});
embed.setFooter({
text: `${supportbot.Embed.Footer} | Suggestion ID: ${thread.id}`,
iconURL: interaction.user.displayAvatarURL()
});
const starter = await thread.fetchStarterMessage();
await starter.edit({ embeds: [embed], components: [row] });
DB.add(thread.id, interaction.user.id, suggestion);
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle(msgconfig.Suggestions.Sent_Title)
.setDescription(`${msgconfig.Suggestions.Sent}`)
.addFields({ name: 'Posted in:', value: `<#${forum.id}>` })
.setColor(supportbot.Embed.Colours.Success)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
// ---------- Channel Mode ----------
const channel = await interaction.guild.channels
.fetch(supportbot.Suggestions.Channel)
.catch(() => null);
if (!channel) {
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle("⚠ Missing Suggestion Channel")
.setDescription(msgconfig.Error.MissingChannel)
.setColor(supportbot.Embed.Colours.Error)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
const message = await channel.send({ embeds: [embed], components: [row] });
embed.setFooter({
text: `${supportbot.Embed.Footer} | Suggestion ID: ${message.id}`,
iconURL: interaction.user.displayAvatarURL()
});
await message.edit({ embeds: [embed], components: [row] });
DB.add(message.id, interaction.user.id, suggestion);
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle(msgconfig.Suggestions.Sent_Title)
.setDescription(`${msgconfig.Suggestions.Sent}`)
.addFields({ name: 'Posted in:', value: `<#${channel.id}>` })
.setColor(supportbot.Embed.Colours.Success)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
});
+183
View File
@@ -0,0 +1,183 @@
const Discord = require("discord.js");
const fs = require("fs");
const yaml = require("js-yaml");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const msgconfig = require("../../Structures/ConfigStore").messages;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const SuggestionDB = require("../../Structures/Database.js");
const Command = require("../../Structures/Command.js");
module.exports = new Command({
name: cmdconfig.SuggestionAdmin.Command,
description: cmdconfig.SuggestionAdmin.Description,
type: Discord.ApplicationCommandType.ChatInput,
permissions: cmdconfig.SuggestionAdmin.Permission,
options: [
{
name: "status",
description: "New status",
type: Discord.ApplicationCommandOptionType.String,
required: true,
choices: [
{ name: "Solved", value: "Solved" },
{ name: "Denied", value: "Denied" },
{ name: "Considering", value: "Considering" }
]
},
{
name: "id",
description: "Message ID (required in Channel mode)",
type: Discord.ApplicationCommandOptionType.String,
required: false
}
],
async run(interaction) {
const status = interaction.options.getString("status");
const idOption = interaction.options.getString("id");
const mode = supportbot.Suggestions.Mode || "Channel";
const { getRole } = interaction.client;
const staffRole = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
const adminRole = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
if (
!interaction.member.roles.cache.has(staffRole?.id) &&
!interaction.member.roles.cache.has(adminRole?.id)
) {
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle("Invalid Permissions!")
.setDescription(
`${msgconfig.Error.IncorrectPerms}\nRequired: ${supportbot.Roles.StaffMember.Staff} or ${supportbot.Roles.StaffMember.Admin}`
)
.setColor(supportbot.Embed.Colours.Warn)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
if (mode === "Forum") {
const thread = interaction.channel;
if (!thread.isThread() || thread.parentId !== supportbot.Suggestions.ForumChannel) {
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle("Not a Suggestion Thread")
.setDescription(msgconfig.Error.MissingChannel)
.setColor(supportbot.Embed.Colours.Error)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
const tagMap = {
Solved: supportbot.Suggestions.Tags.Solved,
Denied: supportbot.Suggestions.Tags.Denied,
Considering: supportbot.Suggestions.Tags.Considering
};
const tagId = tagMap[status];
if (!tagId) {
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle("Tag Missing")
.setDescription("No forum tag configured for this status.")
.setColor(supportbot.Embed.Colours.Error)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
await thread.setAppliedTags([tagId]).catch(() => null);
SuggestionDB.setStatus(thread.id, status);
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle("Suggestion Updated")
.setDescription(`Status set to **${status}**.`)
.setColor(supportbot.Embed.Colours.Success)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
if (!idOption) {
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle("Message ID Required")
.setDescription("Provide the suggestion’s message ID when in Channel mode.")
.setColor(supportbot.Embed.Colours.Error)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
const channel = await interaction.guild.channels
.fetch(supportbot.Suggestions.Channel)
.catch(() => null);
if (!channel) {
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle("Missing Suggestion Channel")
.setDescription(msgconfig.Error.MissingChannel)
.setColor(supportbot.Embed.Colours.Error)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
const targetMsg = await channel.messages.fetch(idOption).catch(() => null);
if (!targetMsg) {
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle("Message Not Found")
.setDescription("Could not fetch a suggestion with that ID.")
.setColor(supportbot.Embed.Colours.Error)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
const oldEmbed = targetMsg.embeds[0];
if (!oldEmbed) {
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle("No Embed Found")
.setDescription("The target message has no suggestion embed.")
.setColor(supportbot.Embed.Colours.Error)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
const colorMap = {
Solved: supportbot.Embed.Colours.Success,
Denied: supportbot.Embed.Colours.Error,
Considering: supportbot.Embed.Colours.Warn
};
const newEmbed = Discord.EmbedBuilder.from(oldEmbed)
.setColor(colorMap[status])
.addFields({ name: "Status", value: status, inline: true });
await targetMsg.edit({ embeds: [newEmbed], components: targetMsg.components });
SuggestionDB.setStatus(idOption, status);
return interaction.reply({
embeds: [
new Discord.EmbedBuilder()
.setTitle("Suggestion Updated")
.setDescription(`Suggestion **${idOption}** marked as **${status}**.`)
.setColor(supportbot.Embed.Colours.Success)
],
flags: Discord.MessageFlags.Ephemeral,
});
}
});
-75
View File
@@ -1,75 +0,0 @@
// SupportBot | Emerald Services
// Info Command
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const cmdconfig = yaml.load(fs.readFileSync('./Data/commands.yml', 'utf8'));
const Command = require("../Structures/Command.js");
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
module.exports = new Command({
name: cmdconfig.TicketAdd,
description: cmdconfig.TicketAddDesc,
type: "BOTH",
slashCommandOptions: [],
permission: "SEND_MESSAGES",
async run(message, args, client) {
let SupportStaff = message.guild.roles.cache.find(SupportTeam => SupportTeam.name === supportbot.Staff) || message.guild.roles.cache.find(SupportTeam => SupportTeam.id === supportbot.Staff)
let Admins = message.guild.roles.cache.find(AdminUser => AdminUser.name === supportbot.Admin) || message.guild.roles.cache.find(AdminUser => AdminUser.id === supportbot.Admin)
const NoPerms = new Discord.MessageEmbed()
.setTitle("Invalid Permissions!")
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${supportbot.Staff}\` or \`${supportbot.Admin}\``)
.setColor(supportbot.WarningColour)
if (message.member.roles.cache.has(SupportStaff.id) || message.member.roles.cache.has(Admins.id)) {
if (!message.channel.name.startsWith( `${supportbot.TicketChannel}-` )) {
const Exists = new Discord.MessageEmbed()
.setTitle("No Ticket Found!")
.setDescription(`${supportbot.NoValidTicket}`)
.setColor(supportbot.WarningColour);
message.channel.send({ embeds: [Exists] });
return;
}
let uMember = message.mentions.users.first();
const UserNotExist = new Discord.MessageEmbed()
.setTitle("User Not Found!")
.setDescription(`${supportbot.UserNotFound}\n\nTry Again:\`${supportbot.Prefix}${cmdconfig.TicketAdd} <user#0000>\``)
.setColor(supportbot.ErrorColour)
if (!uMember) return message.channel.send({ embeds: [UserNotExist] });
message.channel.permissionOverwrites.edit(uMember, {
VIEW_CHANNEL: true,
READ_MESSAGE_HISTORY: true,
SEND_MESSAGES: true,
});
const Complete = new Discord.MessageEmbed()
.setTitle("User Added!")
.setDescription(supportbot.AddedUser.replace(/%user%/g, uMember.id))
.setTimestamp()
.setColor(supportbot.EmbedColour)
message.channel.send({ embeds: [Complete] });
} else {
return message.channel.send({ embeds: [NoPerms] });
}
}
});
-75
View File
@@ -1,75 +0,0 @@
// SupportBot | Emerald Services
// Info Command
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const cmdconfig = yaml.load(fs.readFileSync('./Data/commands.yml', 'utf8'));
const Command = require("../Structures/Command.js");
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
module.exports = new Command({
name: cmdconfig.TicketRemove,
description: cmdconfig.TicketRemoveDesc,
type: "BOTH",
slashCommandOptions: [],
permission: "SEND_MESSAGES",
async run(message, args, client) {
let SupportStaff = message.guild.roles.cache.find(SupportTeam => SupportTeam.name === supportbot.Staff) || message.guild.roles.cache.find(SupportTeam => SupportTeam.id === supportbot.Staff)
let Admins = message.guild.roles.cache.find(AdminUser => AdminUser.name === supportbot.Admin) || message.guild.roles.cache.find(AdminUser => AdminUser.id === supportbot.Admin)
const NoPerms = new Discord.MessageEmbed()
.setTitle("Invalid Permissions!")
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${supportbot.Staff}\` or \`${supportbot.Admin}\``)
.setColor(supportbot.WarningColour)
if (message.member.roles.cache.has(SupportStaff.id) || message.member.roles.cache.has(Admins.id)) {
if (!message.channel.name.startsWith( `${supportbot.TicketChannel}-` )) {
const Exists = new Discord.MessageEmbed()
.setTitle("No Ticket Found!")
.setDescription(`${supportbot.NoValidTicket}`)
.setColor(supportbot.WarningColour);
message.channel.send({ embeds: [Exists] });
return;
}
let uMember = message.mentions.users.first();
const UserNotExist = new Discord.MessageEmbed()
.setTitle("User Not Found!")
.setDescription(`${supportbot.UserNotFound}\n\nTry Again:\`${supportbot.Prefix}${cmdconfig.TicketRemove} <user#0000>\``)
.setColor(supportbot.ErrorColour)
if (!uMember) return message.channel.send({ embeds: [UserNotExist] });
message.channel.permissionOverwrites.edit(uMember, {
VIEW_CHANNEL: false,
READ_MESSAGE_HISTORY: false,
SEND_MESSAGES: false,
});
const Complete = new Discord.MessageEmbed()
.setTitle("User Removed!")
.setDescription(supportbot.RemovedUser.replace(/%user%/g, uMember.id))
.setTimestamp()
.setColor(supportbot.EmbedColour)
message.channel.send({ embeds: [Complete] });
} else {
return message.channel.send({ embeds: [NoPerms] });
}
}
});
+494
View File
@@ -0,0 +1,494 @@
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const db = require("../../Structures/Database.js");
const TicketManager = require("../../Structures/TicketManager.js");
const Command = require("../../Structures/Command.js");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const msgconfig = require("../../Structures/ConfigStore").messages;
function getDepartmentConfig(departmentKey) {
return supportbot.Ticket?.DepartmentSystem?.Departments?.[departmentKey] || null;
}
function getTicketUserId(ticket) {
return ticket?.user || ticket?.user_id || null;
}
function buildContainer(title, lines = []) {
const container = new Discord.ContainerBuilder();
if (supportbot.Embed?.Colours?.General) {
container.setAccentColor(
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
);
}
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`));
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
const textContent = lines.filter(Boolean).join("\n").trim();
if (textContent.length > 0) {
container.addTextDisplayComponents(
new Discord.TextDisplayBuilder().setContent(textContent),
);
}
return container;
}
function buildReviewSummaryContainer(ticket, rating, comment) {
const departmentConfig = ticket.department ? getDepartmentConfig(ticket.department) : null;
const departmentName = departmentConfig?.Name || ticket.department || null;
const departmentEmoji = departmentConfig?.Emoji || "🎫";
const claimedByEnabled = supportbot.Ticket?.ClaimTickets?.Enabled === true;
const claimedBy = claimedByEnabled && ticket.claimedBy ? `<@${ticket.claimedBy}>` : null;
const ticketUserId = getTicketUserId(ticket);
const lines = [
`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** ${ticketUserId ? `<@${ticketUserId}>` : "Unknown User"}`,
];
if (claimedBy) {
lines.unshift(`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedStaffTitle}** ${claimedBy}`);
}
if (supportbot.Ticket?.DepartmentSystem?.Enabled === true && departmentName) {
lines.push(`**Department** ${departmentEmoji} ${departmentName}`);
}
lines.push("");
lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.RatingTitle}**`);
lines.push(`${msgconfig.ReviewSystem.ReviewEmbed.ReviewEmoji.repeat(Number(rating))}`);
lines.push("");
lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.CommentTitle}**`);
lines.push(comment || "No Comment Provided");
const container = new Discord.ContainerBuilder();
if (msgconfig.ReviewSystem?.ReviewEmbed?.Color) {
container.setAccentColor(
parseInt(String(msgconfig.ReviewSystem.ReviewEmbed.Color).replace("#", ""), 16),
);
} else if (supportbot.Embed?.Colours?.General) {
container.setAccentColor(
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
);
}
container.addTextDisplayComponents(
new Discord.TextDisplayBuilder().setContent(`## ${msgconfig.ReviewSystem.ReviewEmbed.Title}`),
);
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n")));
return container;
}
async function collectReview(interaction, ticket) {
const ticketUserId = getTicketUserId(ticket);
if (!ticketUserId) return null;
const ratingContainer = buildContainer(msgconfig.ReviewSystem.Rate.Title, [
msgconfig.ReviewSystem.Rate.Description,
"",
]);
const ratingRow = new Discord.ActionRowBuilder().addComponents(
new Discord.StringSelectMenuBuilder()
.setCustomId(`closeReviewRating:${interaction.id}`)
.setPlaceholder("Select a rating")
.addOptions([
{ label: msgconfig.ReviewSystem.Stars.One, value: "1" },
{ label: msgconfig.ReviewSystem.Stars.Two, value: "2" },
{ label: msgconfig.ReviewSystem.Stars.Three, value: "3" },
{ label: msgconfig.ReviewSystem.Stars.Four, value: "4" },
{ label: msgconfig.ReviewSystem.Stars.Five, value: "5" },
]),
);
ratingContainer.addActionRowComponents(ratingRow);
const ratingPrompt = await interaction.followUp({
flags: Discord.MessageFlags.IsComponentsV2,
components: [ratingContainer],
});
let rating = null;
try {
const ratingInteraction = await ratingPrompt.awaitMessageComponent({
componentType: Discord.ComponentType.StringSelect,
time: 120000,
filter: (i) =>
i.customId === `closeReviewRating:${interaction.id}` && i.user.id === ticketUserId,
});
rating = ratingInteraction.values[0];
await ratingInteraction.deferUpdate().catch(() => {});
} catch {
return null;
}
const commentContainer = buildContainer(msgconfig.ReviewSystem.Comment.Title, [
msgconfig.ReviewSystem.Comment.Description,
"",
]);
const commentRow = new Discord.ActionRowBuilder().addComponents(
new Discord.ButtonBuilder()
.setCustomId(`closeReviewWrite:${interaction.id}`)
.setLabel("Write Comment")
.setStyle(Discord.ButtonStyle.Secondary),
new Discord.ButtonBuilder()
.setCustomId(`closeReviewSkip:${interaction.id}`)
.setLabel("Skip Comment")
.setStyle(Discord.ButtonStyle.Danger),
);
commentContainer.addActionRowComponents(commentRow);
const commentPrompt = await interaction.followUp({
flags: Discord.MessageFlags.IsComponentsV2,
components: [commentContainer],
});
let comment = "No Comment Provided";
try {
const buttonInteraction = await commentPrompt.awaitMessageComponent({
componentType: Discord.ComponentType.Button,
time: 120000,
filter: (i) =>
(i.customId === `closeReviewWrite:${interaction.id}` ||
i.customId === `closeReviewSkip:${interaction.id}`) &&
i.user.id === ticketUserId,
});
if (buttonInteraction.customId === `closeReviewSkip:${interaction.id}`) {
await buttonInteraction.deferUpdate().catch(() => {});
return { rating, comment };
}
await buttonInteraction
.update({
flags: Discord.MessageFlags.IsComponentsV2,
components: [
buildContainer(msgconfig.ReviewSystem.Comment.Title, [
`Requested from: <@${ticketUserId}>`,
"",
"Please send your review comment in the channel now.",
"Type `no` to skip.",
]),
],
})
.catch(() => {});
const collected = await interaction.channel.awaitMessages({
max: 1,
time: 120000,
filter: (m) => m.author.id === ticketUserId,
});
const message = collected.first();
if (message) {
const rawComment = message.content?.trim();
if (rawComment && rawComment.toLowerCase() !== "no") {
comment = rawComment;
}
await message.delete().catch(() => {});
}
} catch {
return { rating, comment };
}
return { rating, comment };
}
function getTranscriptAttachmentFromResult(result) {
if (!result) return null;
if (typeof result === "string") {
return result;
}
if (result.filePath) return result.filePath;
if (result.attachment) return result.attachment;
if (result.path) return result.path;
return null;
}
async function maybeDMTranscript(interaction, ticket, transcriptResult) {
if (supportbot.Ticket?.UserManagement?.DMTranscript !== true) {
return;
}
const ticketUserId = getTicketUserId(ticket);
if (!ticketUserId) return;
const ticketUser = await interaction.client.users.fetch(ticketUserId).catch(() => null);
if (!ticketUser) return;
const attachmentPath = getTranscriptAttachmentFromResult(transcriptResult);
if (!attachmentPath) return;
try {
await ticketUser.send({
content: `Here is a copy of your ticket transcript from \`${interaction.guild.name}\`.`,
files: [attachmentPath],
});
} catch (error) {
console.error("Failed to DM transcript to user:", error);
}
}
async function collectCloseConfirmation(interaction, ticket, reason) {
const ticketUserId = getTicketUserId(ticket);
const lines = [
"Are you sure you want to close this ticket?",
"",
`**Ticket:** ${interaction.channel.name}`,
`**Reason:** ${reason || "No Reason Provided."}`,
];
if (ticketUserId) {
lines.push(`**Ticket User:** <@${ticketUserId}>`);
}
if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
lines.push("");
lines.push("A review will be requested before the ticket is closed.");
}
const confirmContainer = buildContainer("Close Confirmation", lines);
const confirmRow = new Discord.ActionRowBuilder().addComponents(
new Discord.ButtonBuilder()
.setCustomId(`confirmClose:${interaction.id}`)
.setLabel("Confirm Close")
.setStyle(Discord.ButtonStyle.Danger),
new Discord.ButtonBuilder()
.setCustomId(`cancelClose:${interaction.id}`)
.setLabel("Cancel")
.setStyle(Discord.ButtonStyle.Secondary),
);
confirmContainer.addActionRowComponents(confirmRow);
const confirmMessage = await interaction.editReply({
flags: Discord.MessageFlags.IsComponentsV2,
components: [confirmContainer],
});
try {
const buttonInteraction = await confirmMessage.awaitMessageComponent({
componentType: Discord.ComponentType.Button,
time: 120000,
filter: (i) =>
(i.customId === `confirmClose:${interaction.id}` ||
i.customId === `cancelClose:${interaction.id}`) &&
i.user.id === interaction.user.id,
});
if (buttonInteraction.customId === `cancelClose:${interaction.id}`) {
await buttonInteraction
.update({
flags: Discord.MessageFlags.IsComponentsV2,
components: [
buildContainer("Close Cancelled", ["The ticket close process has been cancelled."]),
],
})
.catch(() => {});
return false;
}
await buttonInteraction.deferUpdate().catch(() => {});
return true;
} catch {
await interaction
.editReply({
flags: Discord.MessageFlags.IsComponentsV2,
components: [buildContainer("Close Cancelled", ["The close confirmation timed out."])],
})
.catch(() => {});
return false;
}
}
async function postReview(interaction, ticket, reviewData) {
const { getChannel } = interaction.client;
const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild);
if (!reviewChannel || !reviewData) return;
const reviewContainer = buildReviewSummaryContainer(
ticket,
reviewData.rating,
reviewData.comment,
);
await reviewChannel
.send({
flags: Discord.MessageFlags.IsComponentsV2,
components: [reviewContainer],
})
.catch(() => {});
}
async function handleCloseTicket(interaction, reason, ticket) {
try {
if (typeof db.updateTicketStatus === "function") {
db.updateTicketStatus(interaction.channel.id, "closed");
}
const transcriptResult = await TicketManager.createTranscript(interaction, ticket, reason);
await maybeDMTranscript(interaction, ticket, transcriptResult);
await interaction
.editReply({
flags: Discord.MessageFlags.IsComponentsV2,
components: [
buildContainer("Ticket Closed", [
`**Ticket:** ${interaction.channel.name}`,
`**Closed By:** <@${interaction.user.id}>`,
`**Reason:** ${reason || "No Reason Provided."}`,
]),
],
})
.catch(() => {});
await interaction.channel.delete().catch(() => {});
} catch (err) {
console.error("Error handling ticket close:", err);
if (interaction.deferred || interaction.replied) {
await interaction
.editReply({
content: "An error occurred while closing the ticket.",
components: [],
})
.catch(() => {});
return;
}
await interaction
.reply({
content: "An error occurred while closing the ticket.",
flags: Discord.MessageFlags.Ephemeral,
})
.catch(() => {});
}
}
module.exports = new Command({
name: cmdconfig.CloseTicket.Command,
description: cmdconfig.CloseTicket.Description,
type: Discord.ApplicationCommandType.ChatInput,
options: [
{
name: "reason",
description: "Ticket Close Reason",
type: Discord.ApplicationCommandOptionType.String,
},
],
permissions: cmdconfig.CloseTicket.Permission,
async run(interaction) {
const { getRole } = interaction.client;
if (supportbot.Ticket.Close.StaffOnly) {
const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
if (!SupportStaff || !Admin) {
return interaction.reply({
content: "Some roles seem to be missing! Please check for errors when starting the bot.",
flags: Discord.MessageFlags.Ephemeral,
});
}
if (
!interaction.member.roles.cache.has(SupportStaff.id) &&
!interaction.member.roles.cache.has(Admin.id)
) {
return interaction.reply({
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
components: [
buildContainer("Invalid Permissions!", [
msgconfig.Error.IncorrectPerms,
"",
`Role Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
]),
],
});
}
}
const isThread = interaction.channel.type === Discord.ChannelType.PrivateThread;
if (
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
(supportbot.Ticket.TicketType === "channels" &&
interaction.channel.type !== Discord.ChannelType.GuildText)
) {
return interaction.reply({
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
components: [
buildContainer("Invalid Channel!", [
`This command can only be used in a ${
supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"
}.`,
]),
],
});
}
await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
const ticket = db.getTicket(interaction.channel.id);
if (!ticket) {
return interaction.editReply({
flags: Discord.MessageFlags.IsComponentsV2,
components: [buildContainer("No Ticket Found!", [msgconfig.Error.NoValidTicket])],
});
}
const reason = interaction.options?.getString("reason") || "No Reason Provided.";
const confirmed = await collectCloseConfirmation(interaction, ticket, reason);
if (!confirmed) {
return;
}
if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
const reviewData = await collectReview(interaction, ticket);
if (reviewData) {
await postReview(interaction, ticket, reviewData);
}
await handleCloseTicket(interaction, reason, ticket);
return;
}
await handleCloseTicket(interaction, reason, ticket);
},
});
+664
View File
@@ -0,0 +1,664 @@
const fs = require("fs");
const yaml = require("js-yaml");
const {
EmbedBuilder,
ApplicationCommandOptionType,
ApplicationCommandType,
ChannelType,
MessageFlags,
ModalBuilder,
ActionRowBuilder,
TextInputBuilder,
TextInputStyle,
} = require("discord.js");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const msgconfig = require("../../Structures/ConfigStore").messages;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const TicketManager = require("../../Structures/TicketManager.js");
const Command = require("../../Structures/Command.js");
const db = require("../../Structures/Database.js");
function getDepartments() {
return supportbot.Ticket?.DepartmentSystem?.Departments || {};
}
function getPriorities() {
return supportbot.Ticket?.PrioritySystem?.Priorities || {};
}
function prioritiesEnabled() {
return (
supportbot.Ticket?.TicketType !== "threads" &&
supportbot.Ticket?.PrioritySystem?.Enabled === true
);
}
function departmentsEnabled() {
return (
supportbot.Ticket?.TicketType !== "threads" &&
supportbot.Ticket?.DepartmentSystem?.Enabled === true
);
}
function buildTicketChannelName(ticketNumberID, priorityKey, departmentConfig) {
const defaultPrefix = supportbot.Ticket?.ChannelPrefix || supportbot.Ticket?.Channel || "ticket-";
const departmentPrefix = departmentConfig?.ChannelPrefix || defaultPrefix;
const safePrefix = String(departmentPrefix).trim() || "ticket-";
const baseName = `${safePrefix}${ticketNumberID}`;
if (!prioritiesEnabled() || !priorityKey) {
return baseName;
}
return `${baseName}-${String(priorityKey).toLowerCase()}`;
}
module.exports = new Command({
name: cmdconfig.TicketManage?.Command || "ticketmanage",
description: cmdconfig.TicketManage?.Description || "Manage tickets",
type: ApplicationCommandType.ChatInput,
permissions: cmdconfig.TicketManage?.Permission || ["ManageChannels"],
options: [
{
name: "transfer",
description: "Transfer a ticket to another department",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "department",
description: "Department to transfer this ticket to",
type: ApplicationCommandOptionType.String,
required: true,
choices: Object.entries(getDepartments())
.slice(0, 25)
.map(([key, dept]) => ({
name: dept.Name || key,
value: key,
})),
},
{
name: "priority",
description: "Optional new priority",
type: ApplicationCommandOptionType.String,
required: false,
choices: Object.entries(getPriorities())
.slice(0, 25)
.map(([key, priority]) => ({
name: priority.Name || key,
value: key,
})),
},
],
},
{
name: "priority",
description: "Change a ticket priority",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "level",
description: "New priority level",
type: ApplicationCommandOptionType.String,
required: true,
choices: Object.entries(getPriorities())
.slice(0, 25)
.map(([key, priority]) => ({
name: priority.Name || key,
value: key,
})),
},
],
},
{
name: cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd",
description:
cmdconfig.TicketManage?.ForceAdd?.Description || "Force add a user to the current ticket",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "user",
description: "The user to add",
type: ApplicationCommandOptionType.User,
required: true,
},
],
},
{
name: cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove",
description:
cmdconfig.TicketManage?.ForceRemove?.Description || "Remove a user from the current ticket",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "user",
description: "The user to remove",
type: ApplicationCommandOptionType.User,
required: true,
},
],
},
{
name: cmdconfig.TicketManage?.Rename?.Command || "rename",
description: cmdconfig.TicketManage?.Rename?.Description || "Rename the current ticket",
type: ApplicationCommandOptionType.Subcommand,
options: [],
},
{
name: cmdconfig.TicketManage?.Close?.Command || "close",
description: cmdconfig.TicketManage?.Close?.Description || "Close the current ticket",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "reason",
description: "Reason for closing the ticket",
type: ApplicationCommandOptionType.String,
required: false,
},
],
},
],
async run(interaction) {
try {
const subcommand = interaction.options.getSubcommand(false);
const ticket = db.getTicket(interaction.channel.id);
if (!ticket) {
return interaction.reply({
content: "This channel is not a ticket.",
flags: MessageFlags.Ephemeral,
});
}
const { getRole } = interaction.client;
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild);
if (!SupportStaff || !Admin || !Moderator) {
const missingRolesEmbed = new EmbedBuilder()
.setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!")
.setColor(supportbot.Embed.Colours.Warn);
return interaction.reply({
embeds: [missingRolesEmbed],
flags: MessageFlags.Ephemeral,
});
}
const NoPerms = new EmbedBuilder()
.setDescription(
msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!",
)
.setColor(supportbot.Embed.Colours.Warn);
if (
!interaction.member.roles.cache.has(Admin.id) &&
!interaction.member.roles.cache.has(Moderator.id) &&
(!supportbot.Roles?.Mod?.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))
) {
return interaction.reply({
embeds: [NoPerms],
flags: MessageFlags.Ephemeral,
});
}
if (subcommand === "transfer") {
if (!departmentsEnabled()) {
return interaction.reply({
content: "Departments are disabled, or ticket mode is set to threads.",
flags: MessageFlags.Ephemeral,
});
}
if (interaction.channel.type !== ChannelType.GuildText) {
return interaction.reply({
content: "Ticket transfer only works for ticket channels.",
flags: MessageFlags.Ephemeral,
});
}
const departmentKey = interaction.options.getString("department");
const requestedPriority = interaction.options.getString("priority");
const departments = getDepartments();
const departmentConfig = departments[departmentKey];
if (!departmentConfig) {
return interaction.reply({
content: "That department does not exist.",
flags: MessageFlags.Ephemeral,
});
}
const targetCategoryId = departmentConfig.Category;
const targetRoleId = departmentConfig.Role || null;
if (!targetCategoryId) {
return interaction.reply({
content: "That department does not have a category configured.",
flags: MessageFlags.Ephemeral,
});
}
const targetCategory = interaction.guild.channels.cache.get(targetCategoryId);
if (!targetCategory) {
return interaction.reply({
content: "The target department category could not be found.",
flags: MessageFlags.Ephemeral,
});
}
let finalPriority = ticket.priority || null;
if (requestedPriority) {
finalPriority = requestedPriority;
} else if (!finalPriority && departmentConfig.DefaultPriority) {
finalPriority = departmentConfig.DefaultPriority;
}
const oldDepartmentKey = ticket.department || null;
const oldDepartmentConfig = oldDepartmentKey ? departments[oldDepartmentKey] : null;
const oldRoleId = oldDepartmentConfig?.Role || null;
await interaction.channel.setParent(targetCategory.id);
if (oldRoleId && oldRoleId !== targetRoleId) {
await interaction.channel.permissionOverwrites.delete(oldRoleId).catch(() => { });
}
if (targetRoleId) {
await interaction.channel.permissionOverwrites.create(targetRoleId, {
ViewChannel: true,
SendMessages: true,
ReadMessageHistory: true,
});
}
if (typeof db.updateTicketDepartment === "function") {
db.updateTicketDepartment(interaction.channel.id, departmentKey);
}
if (finalPriority && typeof db.updateTicketPriority === "function") {
db.updateTicketPriority(interaction.channel.id, finalPriority);
}
const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, "");
const newChannelName = buildTicketChannelName(
ticketNumber,
finalPriority,
departmentConfig,
);
if (interaction.channel.name !== newChannelName) {
await interaction.channel.setName(newChannelName).catch(() => { });
}
const deptEmoji = departmentConfig.Emoji || "🎫";
const deptName = departmentConfig.Name || departmentKey;
let description = `> **Department:** ${deptEmoji} ${deptName}`;
if (finalPriority && prioritiesEnabled()) {
const priorityConfig = getPriorities()[finalPriority];
const priorityEmoji = priorityConfig?.Emoji || "🟡";
const priorityName = priorityConfig?.Name || finalPriority;
description += `\n> **Priority:** ${priorityEmoji} ${priorityName}`;
}
description += `\n> **Transferred By:** <@${interaction.user.id}>`;
const embed = new EmbedBuilder()
.setTitle("Ticket Updated")
.setDescription(description)
.setColor(supportbot.Embed.Colours.Success);
return interaction.reply({
embeds: [embed],
});
}
if (subcommand === "priority") {
if (!prioritiesEnabled()) {
return interaction.reply({
content: "Priority is disabled, or ticket mode is set to threads.",
flags: MessageFlags.Ephemeral,
});
}
const level = interaction.options.getString("level");
const priorityConfig = getPriorities()[level];
if (!priorityConfig) {
return interaction.reply({
content: "That priority does not exist.",
flags: MessageFlags.Ephemeral,
});
}
if (typeof db.updateTicketPriority === "function") {
db.updateTicketPriority(interaction.channel.id, level);
}
const departments = getDepartments();
const departmentConfig = ticket.department ? departments[ticket.department] : null;
const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, "");
const newChannelName = buildTicketChannelName(ticketNumber, level, departmentConfig);
if (interaction.channel.name !== newChannelName) {
await interaction.channel.setName(newChannelName).catch(() => { });
}
const priorityEmoji = priorityConfig.Emoji || "🟡";
const priorityName = priorityConfig.Name || level;
const embed = new EmbedBuilder()
.setTitle("Ticket Updated")
.setDescription(
`> **Priority:** ${priorityEmoji} ${priorityName}\n> **Updated By:** <@${interaction.user.id}>`,
)
.setColor(supportbot.Embed.Colours.Success);
return interaction.reply({
embeds: [embed],
});
}
if (subcommand === (cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd")) {
const userToAdd = interaction.options.getUser("user");
const ticketChannel = interaction.channel;
if (
(supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
(supportbot.Ticket.TicketType === "channels" &&
ticketChannel.type !== ChannelType.GuildText)
) {
const onlyInTicket = new EmbedBuilder()
.setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
.setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
.setColor(supportbot.Embed.Colours.Error);
return interaction.reply({
embeds: [onlyInTicket],
flags: MessageFlags.Ephemeral,
});
}
const ticketRow = db.getTicket(ticketChannel.id);
if (!ticketRow) {
const notFound = new EmbedBuilder()
.setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
.setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
.setColor(supportbot.Embed.Colours.Error);
return interaction.reply({
embeds: [notFound],
flags: MessageFlags.Ephemeral,
});
}
try {
if (supportbot.Ticket.TicketType === "threads") {
const botMember = await ticketChannel.members
.fetch(interaction.client.user.id)
.catch(() => null);
if (!botMember) await ticketChannel.join();
await new Promise((res) => setTimeout(res, 1000));
await ticketChannel.members.add(userToAdd.id);
} else {
await ticketChannel.permissionOverwrites.create(userToAdd.id, {
ViewChannel: true,
SendMessages: true,
ReadMessageHistory: true,
});
}
db.addUserToTicket(ticketChannel.id, userToAdd.id);
const addedEmbed = new EmbedBuilder()
.setTitle(msgconfig.ForceAddUser.Added_Title)
.setDescription(
msgconfig.ForceAddUser.Added_Description.replace("%username%", userToAdd.username),
)
.setColor(supportbot.Embed.Colours.Success);
await interaction.reply({
embeds: [addedEmbed],
flags: MessageFlags.Ephemeral,
});
if (supportbot.Ticket?.UserManagement?.DMOnAdd !== false) {
const addedToTicketEmbed = new EmbedBuilder()
.setTitle(msgconfig.ForceAddUser.AddedToTicket_Title)
.setDescription(
msgconfig.ForceAddUser.AddedToTicket_Description.replace(
"%channel_link%",
`[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`,
),
)
.setColor(supportbot.Embed.Colours.General);
await userToAdd.send({ embeds: [addedToTicketEmbed] }).catch(() => { });
}
} catch (err) {
console.error("Error adding user to the ticket:", err);
const errorEmbed = new EmbedBuilder()
.setTitle(msgconfig.ForceAddUser.Error_Title)
.setDescription(msgconfig.ForceAddUser.Error_Adding)
.setColor(supportbot.Embed.Colours.Error);
await interaction.reply({
embeds: [errorEmbed],
flags: MessageFlags.Ephemeral,
});
}
return;
}
if (subcommand === (cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove")) {
const userToRemove = interaction.options.getUser("user");
const ticketChannel = interaction.channel;
if (
(supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
(supportbot.Ticket.TicketType === "channels" &&
ticketChannel.type !== ChannelType.GuildText)
) {
const onlyInTicket = new EmbedBuilder()
.setTitle(msgconfig.RemoveUser.NotInTicket_Title)
.setDescription(msgconfig.RemoveUser.NotInTicket_Description)
.setColor(supportbot.Embed.Colours.Error);
return interaction.reply({
embeds: [onlyInTicket],
flags: MessageFlags.Ephemeral,
});
}
try {
if (supportbot.Ticket.TicketType === "threads") {
await ticketChannel.members.remove(userToRemove.id);
} else {
await ticketChannel.permissionOverwrites.delete(userToRemove.id);
}
db.removeUserFromTicket(ticketChannel.id, userToRemove.id);
const removedEmbed = new EmbedBuilder()
.setTitle(msgconfig.RemoveUser.Removed_Title)
.setDescription(msgconfig.RemoveUser.Removed_Message)
.setColor(supportbot.Embed.Colours.Success);
await interaction.reply({
embeds: [removedEmbed],
flags: MessageFlags.Ephemeral,
});
if (supportbot.Ticket?.UserManagement?.DMOnRemove !== false) {
const removedFromTicketEmbed = new EmbedBuilder()
.setTitle(msgconfig.RemoveUser.RemovedFromTicket_Title)
.setDescription(
msgconfig.RemoveUser.RemovedFromTicket_Description.replace(
"%channel_link%",
`[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`,
),
)
.setColor(supportbot.Embed.Colours.General);
await userToRemove.send({ embeds: [removedFromTicketEmbed] }).catch(() => { });
}
} catch (err) {
console.error("Error removing user from the ticket:", err);
const errorEmbed = new EmbedBuilder()
.setTitle(msgconfig.RemoveUser.Error_Title)
.setDescription(msgconfig.RemoveUser.Error_Removing)
.setColor(supportbot.Embed.Colours.Error);
await interaction.reply({
embeds: [errorEmbed],
flags: MessageFlags.Ephemeral,
});
}
return;
}
if (subcommand === (cmdconfig.TicketManage?.Rename?.Command || "rename")) {
const modal = new ModalBuilder()
.setCustomId("renameTicketModal")
.setTitle("Rename Channel")
.addComponents(
new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId("renameTicket")
.setLabel("New Channel Name")
.setStyle(TextInputStyle.Short)
.setRequired(true),
),
);
return interaction.showModal(modal);
}
if (subcommand === (cmdconfig.TicketManage?.Close?.Command || "close")) {
const reason = interaction.options.getString("reason") || "Closed by staff.";
if (supportbot.Ticket.Close.StaffOnly) {
let CloseSupportStaff = await getRole(
supportbot.Roles.StaffMember.Staff,
interaction.guild,
);
let CloseAdmin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
if (!CloseSupportStaff || !CloseAdmin) {
return interaction.reply({
content:
"Some roles seem to be missing! Please check for errors when starting the bot.",
flags: MessageFlags.Ephemeral,
});
}
const noClosePerms = new EmbedBuilder()
.setTitle("Invalid Permissions!")
.setDescription(
`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
)
.setColor(supportbot.Embed.Colours.Warn);
if (
!interaction.member.roles.cache.has(CloseSupportStaff.id) &&
!interaction.member.roles.cache.has(CloseAdmin.id)
) {
return interaction.reply({
embeds: [noClosePerms],
flags: MessageFlags.Ephemeral,
});
}
}
const isThread = interaction.channel.type === ChannelType.PrivateThread;
if (
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
(supportbot.Ticket.TicketType === "channels" &&
interaction.channel.type !== ChannelType.GuildText)
) {
const notTicketChannel = new EmbedBuilder()
.setTitle("Invalid Channel!")
.setDescription(
`This command can only be used in a ${supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"}.`,
)
.setColor(supportbot.Embed.Colours.Warn);
return interaction.reply({
embeds: [notTicketChannel],
flags: MessageFlags.Ephemeral,
});
}
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const ticketData = db.getTicket(interaction.channel.id);
if (!ticketData) {
const exists = new EmbedBuilder()
.setTitle("No Ticket Found!")
.setDescription(msgconfig.Error.NoValidTicket)
.setColor(supportbot.Embed.Colours.Warn);
return interaction.followUp({
embeds: [exists],
flags: MessageFlags.Ephemeral,
});
}
try {
await TicketManager.createTranscript(interaction, ticketData, reason);
await interaction.followUp({
content: "Ticket closed successfully.",
flags: MessageFlags.Ephemeral,
});
await interaction.channel.delete().catch(() => { });
} catch (err) {
console.error("Error closing ticket:", err);
await interaction.followUp({
content: "An error occurred while closing the ticket.",
flags: MessageFlags.Ephemeral,
});
}
return;
}
return interaction.reply({
content: "Unknown ticket management action.",
flags: MessageFlags.Ephemeral,
});
} catch (error) {
console.error("Error in ticket manage command:", error);
if (!interaction.replied && !interaction.deferred) {
return interaction.reply({
content: "An error occurred while managing the ticket.",
flags: MessageFlags.Ephemeral,
});
}
}
},
});
File diff suppressed because it is too large. Load diff
+343
View File
@@ -0,0 +1,343 @@
// SupportBot | Emerald Services
// Ticket Panel Command
const fs = require("fs");
const {
ApplicationCommandType,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags,
ContainerBuilder,
TextDisplayBuilder,
MediaGalleryBuilder,
SeparatorBuilder,
SectionBuilder,
StringSelectMenuBuilder,
ThumbnailBuilder,
EmbedBuilder,
} = require("discord.js");
const yaml = require("js-yaml");
const panelconfig = require("../../Structures/ConfigStore").ticketPanel;
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const Command = require("../../Structures/Command.js");
const db = require("../../Structures/Database.js");
function parseButtonStyle(style) {
switch (String(style || "1")) {
case "1":
return ButtonStyle.Primary;
case "2":
return ButtonStyle.Secondary;
case "3":
return ButtonStyle.Danger;
case "4":
return ButtonStyle.Success;
default:
return ButtonStyle.Primary;
}
}
function getPanelMessage(key, fallback) {
return panelconfig.Messages?.[key] || fallback;
}
module.exports = new Command({
name: cmdconfig.TicketPanel.Command,
description: cmdconfig.TicketPanel.Description,
type: ApplicationCommandType.ChatInput,
options: [],
permissions: cmdconfig.TicketPanel.Permission,
async run(interaction) {
if (!panelconfig.Panel) {
console.log(
"\u001b[31m",
`[TICKET PANEL]`,
"\u001b[33m",
"Ticket Panel is not setup, You can set this in",
`\u001b[31m`,
"/Configs/ticket-panel.yml",
`\n `,
);
return interaction.reply({
content: getPanelMessage(
"NotConfigured",
"Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.",
),
flags: MessageFlags.Ephemeral,
});
}
const { getChannel } = interaction.client;
const channel = await getChannel(supportbot.Ticket.TicketHome, interaction.guild);
if (!channel) {
console.log(`[TICKET PANEL] TicketHome channel not found. Please check your config file.`);
return interaction.reply({
content: getPanelMessage(
"InvalidChannel",
"TicketHome channel not found. Please check your config file.",
),
flags: MessageFlags.Ephemeral,
});
}
const panelRow = db.getTicketPanel();
if (panelRow) {
try {
const oldMessage = await channel.messages.fetch(panelRow.message_id);
if (oldMessage) {
await oldMessage.delete().catch(() => { });
}
} catch (err) {
console.log(`[TICKET PANEL] Previous panel message not found, creating a new one.`);
}
}
const departmentSystem = supportbot.Ticket?.DepartmentSystem || {};
const prioritySystem = supportbot.Ticket?.PrioritySystem || {};
const usingThreads = supportbot.Ticket?.TicketType === "threads";
const departmentsEnabled = departmentSystem.Enabled === true && !usingThreads;
const prioritiesEnabled = prioritySystem.Enabled === true && !usingThreads;
const departments = departmentSystem.Departments || {};
if (usingThreads && (departmentSystem.Enabled || prioritySystem.Enabled)) {
console.log(
"[SupportBot] Departments and Priority are disabled because TicketType is set to 'threads'.",
);
}
const departmentOptions = Object.entries(departments)
.filter(([_, dept]) => dept && dept.Enabled !== false)
.slice(0, 25)
.map(([key, dept]) => ({
label: dept.Name || key,
description:
dept.PanelDescription ||
dept.Description ||
panelconfig.DepartmentMenu?.DefaultDescription ||
`Open a ${String(dept.Name || key).toLowerCase()} ticket`,
value: key,
emoji:
dept.Emoji ||
panelconfig.DepartmentMenu?.DefaultEmoji ||
panelconfig.Button?.Emoji ||
undefined,
}));
const shouldUseDepartments = departmentsEnabled && departmentOptions.length > 0;
let openerComponent;
if (shouldUseDepartments) {
openerComponent = new StringSelectMenuBuilder()
.setCustomId(panelconfig.DepartmentMenu?.CustomId || "ticketdepartmentselect")
.setPlaceholder(
panelconfig.DepartmentMenu?.Placeholder ||
departmentSystem.Placeholder ||
panelconfig.Button?.Text ||
"Choose a ticket department",
)
.addOptions(departmentOptions);
} else {
openerComponent = new ButtonBuilder()
.setCustomId(panelconfig.Button?.CustomId || "createticket")
.setLabel(panelconfig.Button?.Text || "Create Ticket")
.setStyle(parseButtonStyle(panelconfig.Button?.Color));
if (panelconfig.Button?.Emoji) {
openerComponent.setEmoji(panelconfig.Button.Emoji);
}
}
const panelContainer = new ContainerBuilder();
if (panelconfig.Style?.Color) {
panelContainer.setAccentColor(parseInt(String(panelconfig.Style.Color).replace("#", ""), 16));
}
const layout = String(panelconfig.Style?.Layout || "3");
const showImage = panelconfig.Settings?.Image === true && !!panelconfig.Style?.Image;
const showTopTitle = panelconfig.Settings?.TopTitle === true;
const showTopDivider = panelconfig.Settings?.TopDivider === true;
const showBottomTitle = panelconfig.Settings?.BottomTitle === true;
const showBottomDivider = panelconfig.Settings?.BottomDivider === true;
const panelTitle = new TextDisplayBuilder().setContent(
panelconfig.Style?.Title || "## SupportBot Ticket Creation",
);
const rawTitle = (panelconfig.Style?.Title || "SupportBot Ticket Creation").replace(/^#+\s*/, "");
const panelDesc = new TextDisplayBuilder().setContent(
panelconfig.Style?.Description || "Click on the button below to create a support ticket.",
);
if (showTopTitle) {
panelContainer.addTextDisplayComponents(panelTitle);
}
if (showTopDivider) {
panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
}
if (layout === "1" || layout === "3") {
panelContainer.addTextDisplayComponents(panelDesc);
}
if (showImage && layout === "2") {
const panelImage = new MediaGalleryBuilder().addItems([
{
media: {
url: panelconfig.Style.Image,
},
},
]);
panelContainer.addMediaGalleryComponents(panelImage);
}
if (layout === "1") {
const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
if (!shouldUseDepartments) {
middleSection.setButtonAccessory(openerComponent);
panelContainer.addSectionComponents(middleSection);
} else {
panelContainer.addSectionComponents(middleSection);
panelContainer.addActionRowComponents(
new ActionRowBuilder().addComponents(openerComponent),
);
}
}
if (showImage && layout === "3") {
const panelImage = new MediaGalleryBuilder().addItems([
{
media: {
url: panelconfig.Style.Image,
},
},
]);
panelContainer.addMediaGalleryComponents(panelImage);
}
if (showBottomTitle) {
panelContainer.addTextDisplayComponents(panelTitle);
}
if (showBottomDivider) {
panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
}
if (layout === "2") {
const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
if (!shouldUseDepartments) {
middleSection.setButtonAccessory(openerComponent);
panelContainer.addSectionComponents(middleSection);
} else {
panelContainer.addSectionComponents(middleSection);
panelContainer.addActionRowComponents(
new ActionRowBuilder().addComponents(openerComponent),
);
}
}
if (layout === "3") {
panelContainer.addActionRowComponents(new ActionRowBuilder().addComponents(openerComponent));
}
if (layout === "4") {
// Create a classic Discord Embed for Layout 4
const oldEmbed = new EmbedBuilder()
.setTitle(rawTitle)
.setDescription(panelconfig.Style?.Description || "Click on the button below to create a support ticket.");
if (panelconfig.Style?.Color) {
oldEmbed.setColor(panelconfig.Style.Color);
}
if (showImage && panelconfig.Style?.Image) {
oldEmbed.setThumbnail(panelconfig.Style.Image);
}
try {
const message = await channel.send({
embeds: [oldEmbed],
components: [new ActionRowBuilder().addComponents(openerComponent)],
});
db.saveTicketPanel(message.id, channel.id);
return interaction.reply({
content: shouldUseDepartments
? getPanelMessage(
"PanelSentWithDepartments",
"Ticket panel message with department selection has been sent!",
)
: getPanelMessage("PanelSent", "Ticket panel message has been sent!"),
flags: MessageFlags.Ephemeral,
});
} catch (e) {
console.log("Error sending message:", e);
return interaction.reply({
content: getPanelMessage(
"SendError",
"An error occurred while sending the ticket panel message.",
),
flags: MessageFlags.Ephemeral,
});
}
}
if (showImage && layout === "1") {
const panelImage = new MediaGalleryBuilder().addItems([
{
media: {
url: panelconfig.Style.Image,
},
},
]);
panelContainer.addMediaGalleryComponents(panelImage);
}
try {
const message = await channel.send({
flags: MessageFlags.IsComponentsV2,
components: [panelContainer],
});
db.saveTicketPanel(message.id, channel.id);
return interaction.reply({
content: shouldUseDepartments
? getPanelMessage(
"PanelSentWithDepartments",
"Ticket panel message with department selection has been sent!",
)
: getPanelMessage("PanelSent", "Ticket panel message has been sent!"),
flags: MessageFlags.Ephemeral,
});
} catch (e) {
console.log("Error sending message:", e);
return interaction.reply({
content: getPanelMessage(
"SendError",
"An error occurred while sending the ticket panel message.",
),
flags: MessageFlags.Ephemeral,
});
}
},
});
+468
View File
@@ -0,0 +1,468 @@
const {
ApplicationCommandType,
ApplicationCommandOptionType,
ActionRowBuilder,
ButtonBuilder,
EmbedBuilder,
StringSelectMenuBuilder,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ButtonStyle,
ComponentType,
MessageFlags,
} = require('discord.js');
const Command = require('../../Structures/Command.js');
const fs = require('fs');
const yaml = require('js-yaml');
const supportbot = require('../../Structures/ConfigStore').supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const msgconfig = require("../../Structures/ConfigStore").messages;
function parseColor(color) {
if (!color) return 0x5865F2;
if (typeof color === 'number') return color;
if (typeof color !== 'string') return 0x5865F2;
color = color.replace('#', '');
if (/^[0-9A-F]{6}$/i.test(color)) {
return parseInt(color, 16);
}
return 0x5865F2;
}
module.exports = new Command({
name: cmdconfig.Embed.Command,
description: cmdconfig.Embed.Description,
type: ApplicationCommandType.ChatInput,
options: [
{
name: 'channel',
description: 'Select channel to send the message',
type: ApplicationCommandOptionType.Channel,
required: true
}
],
permissions: cmdconfig.Embed.Permission,
async run(interaction) {
const { getRole } = interaction.client;
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
if (!SupportStaff || !Admin) {
return interaction.reply('Some roles seem to be missing!\nPlease check for errors when starting the bot.');
}
if (!interaction.member.roles.cache.has(SupportStaff.id) &&
!interaction.member.roles.cache.has(Admin.id)) {
const NoPerms = new EmbedBuilder()
.setTitle('Invalid Permissions!')
.setDescription(`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``)
.setColor(0xFF0000);
return interaction.reply({ embeds: [NoPerms], flags: MessageFlags.Ephemeral });
}
const channel = interaction.options.getChannel('channel');
const messageData = {
type: 'embed',
title: 'New Embed',
description: 'Click the buttons below to edit this embed',
color: 0x5865F2,
fields: [],
footer: null,
thumbnail: null,
image: null,
timestamp: false
};
function createEmbed() {
const embed = new EmbedBuilder()
.setTitle(messageData.title)
.setDescription(messageData.description)
.setColor(messageData.color);
if (messageData.fields.length > 0) {
embed.addFields(messageData.fields);
}
if (messageData.footer) {
embed.setFooter({ text: messageData.footer });
}
if (messageData.thumbnail) {
embed.setThumbnail(messageData.thumbnail);
}
if (messageData.image) {
embed.setImage(messageData.image);
}
if (messageData.timestamp) {
embed.setTimestamp();
}
return embed;
}
const typeRow = new ActionRowBuilder()
.addComponents(
new StringSelectMenuBuilder()
.setCustomId('message_type')
.setPlaceholder('Select message type')
.addOptions([
{
label: 'Embed Message',
description: 'Send as an embedded message',
value: 'embed'
},
{
label: 'Text Message',
description: 'Send as a plain text message',
value: 'text'
}
])
);
function createButtons(messageType) {
const buttons = [];
if (messageType === 'embed') {
const row1 = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('edit_title')
.setLabel('Edit Title')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId('edit_description')
.setLabel('Edit Description')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId('edit_color')
.setLabel('Edit Color')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId('add_field')
.setLabel('Add Field')
.setStyle(ButtonStyle.Secondary)
);
const row2 = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('edit_footer')
.setLabel('Edit Footer')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId('edit_image')
.setLabel('Add Image')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId('edit_thumbnail')
.setLabel('Add Thumbnail')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId('toggle_timestamp')
.setLabel('Toggle Timestamp')
.setStyle(ButtonStyle.Secondary)
);
buttons.push(row1, row2);
} else {
const textRow = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('edit_description')
.setLabel('Edit Text')
.setStyle(ButtonStyle.Secondary)
);
buttons.push(textRow);
}
// Send button always shows
const sendRow = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('send_message')
.setLabel('Send')
.setStyle(ButtonStyle.Success)
);
buttons.push(sendRow);
return buttons;
}
async function updatePreview(i) {
if (messageData.type === 'embed') {
await i.update({
content: 'Customize your message:',
embeds: [createEmbed()],
components: [typeRow, ...createButtons('embed')]
});
} else {
await i.update({
content: messageData.description,
embeds: [],
components: [typeRow, ...createButtons('text')]
});
}
}
const message = await interaction.reply({
content: messageData.type === 'text' ? messageData.description : 'Customize your message:',
components: [typeRow, ...createButtons(messageData.type)],
embeds: messageData.type === 'embed' ? [createEmbed()] : [],
flags: MessageFlags.Ephemeral
});
const collector = message.createMessageComponentCollector({
time: 900000
});
collector.on('collect', async (i) => {
if (i.user.id !== interaction.user.id) {
return i.reply({
content: 'You cannot edit this message.',
flags: MessageFlags.Ephemeral
});
}
switch (i.customId) {
case 'message_type':
messageData.type = i.values[0];
await updatePreview(i);
break;
case 'edit_description':
const descModal = new ModalBuilder()
.setCustomId('desc_modal')
.setTitle(messageData.type === 'embed' ? 'Edit Description' : 'Edit Text');
descModal.addComponents(
new ActionRowBuilder()
.addComponents(
new TextInputBuilder()
.setCustomId('desc_input')
.setLabel(messageData.type === 'embed' ? 'Description' : 'Message Text')
.setStyle(TextInputStyle.Paragraph)
.setValue(messageData.description)
.setMaxLength(4000)
)
);
await i.showModal(descModal);
break;
case 'edit_title':
const titleModal = new ModalBuilder()
.setCustomId('title_modal')
.setTitle('Edit Title');
titleModal.addComponents(
new ActionRowBuilder()
.addComponents(
new TextInputBuilder()
.setCustomId('title_input')
.setLabel('Title')
.setStyle(TextInputStyle.Short)
.setValue(messageData.title)
.setMaxLength(256)
)
);
await i.showModal(titleModal);
break;
case 'edit_color':
const colorModal = new ModalBuilder()
.setCustomId('color_modal')
.setTitle('Edit Color');
colorModal.addComponents(
new ActionRowBuilder()
.addComponents(
new TextInputBuilder()
.setCustomId('color_input')
.setLabel('Color (HEX)')
.setStyle(TextInputStyle.Short)
.setPlaceholder('#FF0000')
.setMinLength(6)
.setMaxLength(7)
.setValue(messageData.color.toString(16).padStart(6, '0'))
)
);
await i.showModal(colorModal);
break;
case 'add_field':
const fieldModal = new ModalBuilder()
.setCustomId('field_modal')
.setTitle('Add Field');
fieldModal.addComponents(
new ActionRowBuilder()
.addComponents(
new TextInputBuilder()
.setCustomId('field_name')
.setLabel('Field Name')
.setStyle(TextInputStyle.Short)
.setMaxLength(256)
),
new ActionRowBuilder()
.addComponents(
new TextInputBuilder()
.setCustomId('field_value')
.setLabel('Field Value')
.setStyle(TextInputStyle.Paragraph)
.setMaxLength(1024)
),
new ActionRowBuilder()
.addComponents(
new TextInputBuilder()
.setCustomId('field_inline')
.setLabel('Inline? (true/false)')
.setStyle(TextInputStyle.Short)
.setValue('false')
.setPlaceholder('true or false')
)
);
await i.showModal(fieldModal);
break;
case 'edit_footer':
const footerModal = new ModalBuilder()
.setCustomId('footer_modal')
.setTitle('Edit Footer');
footerModal.addComponents(
new ActionRowBuilder()
.addComponents(
new TextInputBuilder()
.setCustomId('footer_input')
.setLabel('Footer Text')
.setStyle(TextInputStyle.Short)
.setValue(messageData.footer || '')
.setMaxLength(2048)
)
);
await i.showModal(footerModal);
break;
case 'edit_image':
const imageModal = new ModalBuilder()
.setCustomId('image_modal')
.setTitle('Add Image');
imageModal.addComponents(
new ActionRowBuilder()
.addComponents(
new TextInputBuilder()
.setCustomId('image_input')
.setLabel('Image URL')
.setStyle(TextInputStyle.Short)
.setValue(messageData.image || '')
.setPlaceholder('https://example.com/image.png')
)
);
await i.showModal(imageModal);
break;
case 'edit_thumbnail':
const thumbnailModal = new ModalBuilder()
.setCustomId('thumbnail_modal')
.setTitle('Add Thumbnail');
thumbnailModal.addComponents(
new ActionRowBuilder()
.addComponents(
new TextInputBuilder()
.setCustomId('thumbnail_input')
.setLabel('Thumbnail URL')
.setStyle(TextInputStyle.Short)
.setValue(messageData.thumbnail || '')
.setPlaceholder('https://example.com/thumbnail.png')
)
);
await i.showModal(thumbnailModal);
break;
case 'toggle_timestamp':
messageData.timestamp = !messageData.timestamp;
await updatePreview(i);
break;
case 'send_message':
try {
if (messageData.type === 'embed') {
await channel.send({ embeds: [createEmbed()] });
} else {
await channel.send(messageData.description);
}
await i.update({
content: 'Message sent successfully!',
components: [],
embeds: []
});
collector.stop();
} catch (error) {
await i.reply({
content: 'Failed to send message. Please check channel permissions.',
flags: MessageFlags.Ephemeral
});
}
break;
}
});
// Handle modal submissions
interaction.client.on('interactionCreate', async (modal) => {
if (!modal.isModalSubmit()) return;
if (modal.user.id !== interaction.user.id) return;
switch (modal.customId) {
case 'title_modal':
messageData.title = modal.fields.getTextInputValue('title_input');
break;
case 'desc_modal':
messageData.description = modal.fields.getTextInputValue('desc_input');
break;
case 'color_modal':
messageData.color = parseColor(modal.fields.getTextInputValue('color_input'));
break;
case 'field_modal':
const inlineValue = modal.fields.getTextInputValue('field_inline').toLowerCase();
messageData.fields.push({
name: modal.fields.getTextInputValue('field_name'),
value: modal.fields.getTextInputValue('field_value'),
inline: inlineValue === 'true'
});
break;
case 'footer_modal':
messageData.footer = modal.fields.getTextInputValue('footer_input');
break;
case 'image_modal':
messageData.image = modal.fields.getTextInputValue('image_input');
break;
case 'thumbnail_modal':
messageData.thumbnail = modal.fields.getTextInputValue('thumbnail_input');
break;
default:
return;
}
await updatePreview(modal);
});
collector.on('end', () => {
interaction.editReply({
content: 'Message editor timed out.',
components: [],
embeds: []
}).catch(() => {});
});
}
});
+235
View File
@@ -0,0 +1,235 @@
const fs = require("fs");
const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, MessageFlags } = require("discord.js");
const yaml = require("js-yaml");
const db = require("../../Structures/Database.js");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
const msgconfig = require("../../Structures/ConfigStore").messages;
const Command = require("../../Structures/Command.js");
// Function to chunk array into smaller arrays
function chunkArray(arr, size) {
const chunks = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
module.exports = new Command({
name: cmdconfig.Mod.Command,
description: cmdconfig.Mod.Description || "Moderation commands",
type: ApplicationCommandType.ChatInput,
options: [
{
name: cmdconfig.Mod.TicketBlacklist.Command,
description: cmdconfig.Mod.TicketBlacklist.Description || "Ticket Blacklist Commands",
type: ApplicationCommandOptionType.SubcommandGroup,
options: [
{
name: cmdconfig.Mod.TicketBlacklist.Add.Command,
description: msgconfig.Mod.TicketBlacklist.Add.Description || "Add a user to the blacklist",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: 'user',
description: 'The user to blacklist',
type: ApplicationCommandOptionType.User,
required: true
},
{
name: 'reason',
description: 'Reason for blacklisting',
type: ApplicationCommandOptionType.String,
required: false
}
]
},
{
name: cmdconfig.Mod.TicketBlacklist.Remove.Command,
description: cmdconfig.Mod.TicketBlacklist.Remove.Description || "Remove a user from the blacklist",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: 'user',
description: 'The user to remove from the blacklist',
type: ApplicationCommandOptionType.User,
required: true
},
{
name: 'reason',
description: 'Reason for unblacklisting',
type: ApplicationCommandOptionType.String,
required: false
}
]
},
{
name: "view",
description: msgconfig.Mod.TicketBlacklist.View.Description || "View all blacklisted users",
type: ApplicationCommandOptionType.Subcommand
}
]
}
],
async run(interaction) {
const subcommandGroup = interaction.options.getSubcommandGroup();
const subcommand = interaction.options.getSubcommand();
const user = interaction.options.getUser('user');
const reason = interaction.options.getString('reason') || "No reason provided";
try {
const blacklistedUsers = typeof db.getBlacklistedUsers === "function" ? db.getBlacklistedUsers() : [];
if (subcommand === cmdconfig.Mod.TicketBlacklist.Add.Command) {
if (db.isUserBlacklisted && db.isUserBlacklisted(user.id)) {
const alreadyBlacklistedEmbed = new EmbedBuilder()
.setDescription(msgconfig.Mod.TicketBlacklist.Add.AlreadyBlacklisted.replace("{userTag}", user.tag) || ":x: User is already blacklisted.")
.setColor(supportbot.Embed.Colours.Warn);
return interaction.reply({ embeds: [alreadyBlacklistedEmbed], flags: MessageFlags.Ephemeral });
}
if (typeof db.addBlacklistedUser === "function") {
db.addBlacklistedUser(user.id, reason, interaction.user.tag);
}
const successEmbed = new EmbedBuilder()
.setDescription(msgconfig.Mod.TicketBlacklist.Add.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been blacklisted.")
.setColor(supportbot.Embed.Colours.Success);
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
if (blacklistChannel) {
const blacklistLogEmbed = new EmbedBuilder()
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
.addFields({ name: "Action", value: "\`\`\`Added to blacklist\`\`\`", inline: false })
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
}
return interaction.reply({
embeds: [successEmbed],
flags: MessageFlags.Ephemeral
});
} else if (subcommand === cmdconfig.Mod.TicketBlacklist.Remove.Command) {
if (db.isUserBlacklisted && !db.isUserBlacklisted(user.id)) {
const notBlacklistedEmbed = new EmbedBuilder()
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.NotBlacklisted.replace("{userTag}", user.tag) || ":x: User is not blacklisted.")
.setColor(supportbot.Embed.Colours.Warn);
return interaction.reply({ embeds: [notBlacklistedEmbed], flags: MessageFlags.Ephemeral });
}
if (typeof db.removeBlacklistedUser === "function") {
db.removeBlacklistedUser(user.id);
}
const removedEmbed = new EmbedBuilder()
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been removed from the blacklist.")
.setColor(supportbot.Embed.Colours.Success);
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
if (blacklistChannel) {
const blacklistLogEmbed = new EmbedBuilder()
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
.addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false })
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
}
return interaction.reply({ embeds: [removedEmbed], flags: MessageFlags.Ephemeral });
} else if (subcommand === "view") {
if (blacklistedUsers.length === 0) {
const noBlacklistedUsersEmbed = new EmbedBuilder()
.setDescription(msgconfig.Mod.TicketBlacklist.View.NoBlacklistedUsers || "No users are blacklisted.")
.setColor(supportbot.Embed.Colours.Success);
return interaction.reply({ embeds: [noBlacklistedUsersEmbed], flags: MessageFlags.Ephemeral });
}
const chunkedUsers = chunkArray(blacklistedUsers, 5);
let currentPage = 0;
const createEmbed = (page) => {
return new EmbedBuilder()
.setTitle(msgconfig.Mod.TicketBlacklist.View.EmbedTitle || "Blacklisted Users")
.setDescription(chunkedUsers[page].map(id => `<@${id}>`).join("\n"))
.setColor(supportbot.Embed.Colours.Success)
.setFooter({ text: `Page ${page + 1} of ${chunkedUsers.length}` });
};
const buttons = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('previous')
.setLabel('Previous')
.setStyle(ButtonStyle.Secondary)
.setDisabled(currentPage === 0),
new ButtonBuilder()
.setCustomId('next')
.setLabel('Next')
.setStyle(ButtonStyle.Secondary)
.setDisabled(currentPage === chunkedUsers.length - 1)
);
const response = await interaction.reply({
embeds: [createEmbed(currentPage)],
components: chunkedUsers.length > 1 ? [buttons] : [],
flags: MessageFlags.Ephemeral
});
if (chunkedUsers.length <= 1) return;
const collector = response.createMessageComponentCollector({
componentType: ComponentType.Button,
time: 60000
});
collector.on('collect', async (i) => {
if (i.user.id !== interaction.user.id) {
return i.reply({
content: 'You cannot use these buttons.',
flags: MessageFlags.Ephemeral
});
}
if (i.customId === 'previous' && currentPage > 0) {
currentPage--;
} else if (i.customId === 'next' && currentPage < chunkedUsers.length - 1) {
currentPage++;
}
buttons.components[0].setDisabled(currentPage === 0);
buttons.components[1].setDisabled(currentPage === chunkedUsers.length - 1);
await i.update({
embeds: [createEmbed(currentPage)],
components: [buttons]
});
});
collector.on('end', () => {
buttons.components.forEach(button => button.setDisabled(true));
interaction.editReply({
components: [buttons]
}).catch(() => {});
});
}
} catch (error) {
console.error("Error in mod command:", error);
const errorEmbed = new EmbedBuilder()
.setDescription(msgconfig.Error.ActionFailed || "An error occurred while processing your request.")
.setColor(supportbot.Embed.Colours.Warn);
interaction.reply({ embeds: [errorEmbed], flags: MessageFlags.Ephemeral });
}
}
});
+220
View File
@@ -0,0 +1,220 @@
const Command = require("../../Structures/Command.js");
const Discord = require("discord.js");
const db = require("../../Structures/Database.js");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
async function loadSettings() {
return typeof db.getSettings === "function" ? db.getSettings() : {};
}
async function saveSettings(settings) {
if (typeof db.saveSettings === "function") {
db.saveSettings(settings);
}
}
module.exports = new Command({
name: cmdconfig.Settings.Command,
description: cmdconfig.Settings.Description,
permissions: cmdconfig.Settings.Permission,
options: [
{
name: 'setstatus',
type: Discord.ApplicationCommandOptionType.Subcommand,
description: 'Set the bot status',
options: [
{
name: 'status',
type: Discord.ApplicationCommandOptionType.String,
description: 'Choose the bot status',
required: true,
choices: [
{ name: 'Online 🟢', value: 'online' },
{ name: 'Idle 🌙', value: 'idle' },
{ name: 'Do Not Disturb ⛔', value: 'dnd' },
{ name: 'Invisible 👻', value: 'invisible' }
],
},
],
},
{
name: 'setactivity',
type: Discord.ApplicationCommandOptionType.Subcommand,
description: 'Set the bot activity',
options: [
{
name: 'type',
type: Discord.ApplicationCommandOptionType.String,
description: 'Choose the activity type',
required: true,
choices: [
{ name: 'Playing 🎮', value: 'PLAYING' },
{ name: 'Watching 👀', value: 'WATCHING' },
{ name: 'Listening 🎧', value: 'LISTENING' },
{ name: 'Competing 🏆', value: 'COMPETING' }
],
},
{
name: 'message',
type: Discord.ApplicationCommandOptionType.String,
description: 'The activity message',
required: true,
},
],
},
{
name: 'streammode',
type: Discord.ApplicationCommandOptionType.Subcommand,
description: 'Set the bot streaming status',
options: [
{
name: 'toggle',
type: Discord.ApplicationCommandOptionType.String,
description: 'Turn streaming mode on or off',
required: true,
choices: [
{ name: 'On', value: 'on' },
{ name: 'Off', value: 'off' }
],
},
{
name: 'twitchurl',
type: Discord.ApplicationCommandOptionType.String,
description: 'Twitch stream URL (Required if turning on)',
required: false,
},
],
},
{
name: 'setnickname',
type: Discord.ApplicationCommandOptionType.Subcommand,
description: 'Set the bot nickname',
options: [
{
name: 'nickname',
type: Discord.ApplicationCommandOptionType.String,
description: 'The new nickname for the bot',
required: true,
},
],
},
],
async run(interaction) {
const subcommand = interaction.options.getSubcommand();
const settings = await loadSettings();
if (subcommand === 'setstatus') {
const status = interaction.options.getString('status');
try {
interaction.client.user.setPresence({
status: status,
activities: interaction.client.user.presence.activities || [],
});
settings.status = status;
await saveSettings(settings);
const embed = new Discord.EmbedBuilder()
.setDescription(`Status set to **${status.charAt(0).toUpperCase() + status.slice(1)}**`)
.setColor(supportbot.Embed.Colours.General);
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
} catch (error) {
return interaction.reply({ content: `Failed to set status: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
}
}
if (subcommand === 'setactivity') {
const activityType = interaction.options.getString('type');
const activityMessage = interaction.options.getString('message');
try {
interaction.client.user.setPresence({
activities: [{ name: activityMessage, type: Discord.ActivityType[activityType] }],
status: interaction.client.user.presence.status || 'online',
});
settings.activity = { type: activityType, message: activityMessage };
await saveSettings(settings);
const embed = new Discord.EmbedBuilder()
.setDescription(`Activity set to **${activityType.toLowerCase()} ${activityMessage}**`)
.setColor(supportbot.Embed.Colours.General);
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
} catch (error) {
return interaction.reply({ content: `Failed to set activity: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
}
} else if (subcommand === 'streammode') {
const toggle = interaction.options.getString('toggle');
const twitchUrl = interaction.options.getString('twitchurl');
if (toggle === 'on') {
if (!twitchUrl) {
return interaction.reply({ content: 'Please provide a Twitch URL to stream.', flags: Discord.MessageFlags.Ephemeral });
}
try {
interaction.client.user.setPresence({
activities: [{ name: 'Streaming', type: Discord.ActivityType.Streaming, url: twitchUrl }],
status: 'online',
});
settings.streaming = { active: true, url: twitchUrl };
await saveSettings(settings);
const embed = new Discord.EmbedBuilder()
.setDescription(`**Streaming mode enabled!**\nStreaming: [Twitch](${twitchUrl})`)
.setColor(supportbot.Embed.Colours.General);
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
} catch (error) {
return interaction.reply({ content: `Failed to enable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
}
} else if (toggle === 'off') {
try {
interaction.client.user.setPresence({
activities: interaction.client.user.presence.activities.filter(a => a.type !== Discord.ActivityType.Streaming),
status: settings.status || 'online',
});
settings.streaming = { active: false };
await saveSettings(settings);
const embed = new Discord.EmbedBuilder()
.setDescription(`**Streaming mode disabled!**`)
.setColor(supportbot.Embed.Colours.General);
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
} catch (error) {
return interaction.reply({ content: `Failed to disable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
}
}
} else if (subcommand === 'setnickname') {
const nickname = interaction.options.getString('nickname');
try {
const guild = interaction.guild;
const botMember = guild.members.cache.get(interaction.client.user.id);
await botMember.setNickname(nickname);
settings.nickname = nickname;
await saveSettings(settings);
const embed = new Discord.EmbedBuilder()
.setDescription(`Bot nickname set to **${nickname}**`)
.setColor(supportbot.Embed.Colours.General);
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
} catch (error) {
return interaction.reply({ content: `Failed to set nickname: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
}
} else {
return interaction.reply({ content: 'Invalid command usage.', flags: Discord.MessageFlags.Ephemeral });
}
},
});
-174
View File
@@ -1,174 +0,0 @@
// SupportBot | Emerald Services
// Close Ticket Command
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const cmdconfig = yaml.load(fs.readFileSync('./Data/commands.yml', 'utf8'));
const Command = require("../Structures/Command.js");
const TicketNumberID = require('../Structures/TicketID.js');
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
module.exports = new Command({
name: cmdconfig.CloseTicket,
description: cmdconfig.CloseTicketDesc,
type: "BOTH",
slashCommandOptions: [],
permission: "SEND_MESSAGES",
async run(message, args, client) {
if (!message.channel.name.startsWith( `${supportbot.TicketChannel}-` )) {
const Exists = new Discord.MessageEmbed()
.setTitle("No Ticket Found!")
.setDescription(`${supportbot.NoValidTicket}`)
.setColor(supportbot.WarningColour);
message.channel.send({ embed: Exists });
return;
}
if (supportbot.CloseConfirmation) {
const CloseTicketRequest = new Discord.MessageEmbed()
.setTitle(`**${supportbot.ClosingTicket}**`)
.setDescription(`Please confirm by repeating the following word.. \`${supportbot.ClosingConfirmation_Word}\` `)
.setColor(supportbot.EmbedColour);
message.channel.send(CloseTicketRequest).then(m => {
message.channel.awaitMessages( response => response.content.toLowerCase() === supportbot.ClosingConfirmation_Word, {
max: 1,
time: 20000,
errors: [ 'time' ],
}).then( collected => {
let logChannel = message.guild.channels.cache.find(channel => channel.name === supportbot.TranscriptLog) || message.guild.channels.cache.find(channel => channel.id === supportbot.TranscriptLog)
let user = message.author;
let reason = args.join(" ") || "No Reason Provided.";
let name = message.channel.name;
let ticketChannel = message.channel;
message.channel.send(`**${supportbot.ClosingTicket}**`)
.then(() => {
const logEmbed = new Discord.MessageEmbed()
.setTitle(supportbot.TranscriptTitle)
.setColor(supportbot.EmbedColour)
.setFooter(supportbot.EmbedFooter)
.addField("Closed By", message.author.tag)
.addField("Reason", reason);
message.channel.messages.fetch({ limit: 100 }).then( msgs => {
let html = '';
msgs = msgs.sort( ( a, b ) => a.createdTimestamp - b.createdTimestamp );
html += `<strong>Server Name:</strong> ${message.guild.name}<br>`;
html += `<strong>Ticket:</strong> ${ticketChannel.name}<br>`;
html += `<strong>Message:</strong> ${msgs.size} Messages<br><br><br>`;
msgs.forEach( msg => {
if ( msg.content ) {
html += `<strong>User:</strong> ${msg.author.tag}<br>`;
html += `<strong>Message:</strong> ${msg.content}<br>`;
html += `-----<br><br>`;
}
});
logChannel.send({embeds: [logEmbed]}).catch(err => {
message.reply(err)
})
let file = new Discord.MessageAttachment( Buffer.from( html ), `${name}.html` )
logChannel.send({embeds: [logEmbed], files: [file]}).catch(err => {
message.reply(err)
})
message.channel.delete().catch(error => {
if (error.code !== Discord.Constants.APIErrors.UNKNOWN_CHANNEL) {
console.error('Failed to delete the message:', error);
}
});
})
})
})
}).catch( () => {
interaction.m.edit({content: 'The request to close the ticket has timed out.'}).then(m2 => setTimeout(() => channel.delete(3000)) );
});
}
if (!supportbot.CloseConfirmation) {
let logChannel = message.guild.channels.cache.find(channel => channel.name === supportbot.TranscriptLog) || message.guild.channels.cache.find(channel => channel.id === supportbot.TranscriptLog)
let user = message.author;
let reason = args.join(" ") || "No Reason Provided.";
let name = message.channel.name;
let ticketChannel = message.channel;
message.channel.send(`**${supportbot.ClosingTicket}**`)
.then(() => {
const logEmbed = new Discord.MessageEmbed()
.setTitle(supportbot.TranscriptTitle)
.setColor(supportbot.EmbedColour)
.setFooter(supportbot.EmbedFooter)
.addField("Closed By", message.author.tag)
.addField("Reason", reason);
message.channel.messages.fetch({ limit: 100 }).then( msgs => {
let html = '';
msgs = msgs.sort( ( a, b ) => a.createdTimestamp - b.createdTimestamp );
html += `<strong>Server Name:</strong> ${message.guild.name}<br>`;
html += `<strong>Ticket:</strong> ${ticketChannel.name}<br>`;
html += `<strong>Message:</strong> ${msgs.size} Messages<br><br><br>`;
msgs.forEach( msg => {
if ( msg.content ) {
html += `<strong>User:</strong> ${msg.author.tag}<br>`;
html += `<strong>Message:</strong> ${msg.content}<br>`;
html += `-----<br><br>`;
}
});
logChannel.send({embeds: [logEmbed]}).catch(err => {
message.reply(err)
})
let file = new Discord.MessageAttachment( Buffer.from( html ), `${name}.html` )
logChannel.send({embeds: [logEmbed], files: [file]}).catch(err => {
message.reply(err)
})
message.channel.delete().catch(error => {
if (error.code !== Discord.Constants.APIErrors.UNKNOWN_CHANNEL) {
console.error('Failed to delete the message:', error);
}
});
})
}).catch( () => {
interaction.m.edit({content: 'The request to close the ticket has timed out.'}).then(m2 => setTimeout(() => channel.delete(3000)) );
});
}
}
});
-86
View File
@@ -1,86 +0,0 @@
// SupportBot | Emerald Services
// Close Ticket Command
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const cmdconfig = yaml.load(fs.readFileSync('./Data/commands.yml', 'utf8'));
const Command = require("../Structures/Command.js");
const TicketNumberID = require('../Structures/TicketID.js');
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
module.exports = new Command({
name: cmdconfig.HelpCommand,
description: cmdconfig.HelpCommandDesc,
type: "BOTH",
slashCommandOptions: [],
permission: "SEND_MESSAGES",
async run(message, args, client) {
let SupportStaff = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Staff) || message.guild.roles.cache.find(staffRole => staffRole.id === supportbot.Staff);
let AdminStaff = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Admin) || message.guild.roles.cache.find(adminRole => adminRole.id === supportbot.Admin);
if (supportbot.HelpMenu) {
let botCommands = "";
botCommands += `**${supportbot.Prefix}${cmdconfig.HelpCommand}** ${cmdconfig.HelpCommandDesc}\n`;
botCommands += `**${supportbot.Prefix}${cmdconfig.InfoCommand}** ${cmdconfig.InfoCommandDesc}\n`;
botCommands += `**${supportbot.Prefix}${cmdconfig.PingCommand}** ${cmdconfig.PingCommandDesc}\n`
let ticketCommands = "";
ticketCommands += `**${supportbot.Prefix}${cmdconfig.OpenTicket}** ${cmdconfig.OpenTicketDesc}\n`;
ticketCommands += `**${supportbot.Prefix}${cmdconfig.CloseTicket}** ${cmdconfig.CloseTicketDesc}\n`
let staffCommands = "";
staffCommands += `**${supportbot.Prefix}${cmdconfig.TicketAdd}** ${cmdconfig.TicketAddDesc}\n`
staffCommands += `**${supportbot.Prefix}${cmdconfig.TicketRemove}** ${cmdconfig.TicketRemoveDesc}\n`
const HelpEmbed1 = new Discord.MessageEmbed()
.setTitle(supportbot.Name)
.setThumbnail(message.author.displayAvatarURL())
.addFields(
{ name: '🖥️ General Commands\n', value: `${botCommands}\n`, inline: false },
{ name: '🎫 Support Commands\n', value: `${ticketCommands}\n`, inline: false },
)
.setColor(supportbot.EmbedColour)
.setFooter(supportbot.EmbedFooter, message.author.displayAvatarURL());
if (message.member.roles.cache.has(SupportStaff.id)) {
HelpEmbed1.addFields(
{ name: '🔐 Staff Commands\n', value: `${staffCommands}\n`, inline: false },
)
} else {
if ((message.member.roles.cache.has(AdminStaff.id))) {
HelpEmbed1.addFields(
{ name: '🔐 Staff Commands\n', value: `${staffCommands}\n`, inline: false },
)
}
}
if (supportbot.SendHelpPage === "dm") {
message.author.send({
embeds: [HelpEmbed1]
});
}
if (supportbot.SendHelpPage === "channel") {
message.channel.send({
embeds: [HelpEmbed1]
});
}
}
}
});
-42
View File
@@ -1,42 +0,0 @@
// SupportBot | Emerald Services
// Info Command
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const cmdconfig = yaml.load(fs.readFileSync('./Data/commands.yml', 'utf8'));
const Command = require("../Structures/Command.js");
module.exports = new Command({
name: cmdconfig.InfoCommand,
description: cmdconfig.InfoCommandDesc,
type: "BOTH",
slashCommandOptions: [],
permission: "SEND_MESSAGES",
async run(message, args, client) {
const InfoButton = new Discord.MessageButton()
.setLabel(supportbot.InfoButtonText)
.setURL(supportbot.InfoURL)
.setStyle('LINK')
const inforow = new Discord.MessageActionRow()
.addComponents(InfoButton)
const InfoEmbed = new Discord.MessageEmbed()
.setURL(supportbot.InfoURL)
.setTitle(supportbot.InfoTitle)
.setDescription(supportbot.AboutMe)
.setColor(supportbot.InfoColour)
message.channel.send({
embeds: [InfoEmbed], components: [inforow]
})
}
});
+63
View File
@@ -0,0 +1,63 @@
# ___ _ ___ _
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
# |_| |_|
#
# SupportBot created by Emerald Services
# Installed with MIT License
#
# Discord Support: https://discord.gg/emeraldsrv
# Official Website & Marketplace: https://emeraldsrv.com
#
# Messages Configuration (api.yml)
# -------------------------------
# API Server Configuration
# -------------------------------
API:
Enabled: true
Port: 3000
SecretKey: "SECRET_KEY"
# Set to 1 when the dashboard is behind nginx, Cloudflare, Pterodactyl proxy, etc.
# so http/https is detected from X-Forwarded-Proto.
TrustProxy: false
EmeraldAPIKey: "EMERALDSRV_API_KEY"
# This is required for the one-click addon installer. Get API key from https://emeraldsrv.dev/
OAuth:
Enabled: true
ClientId: "CLIENT_ID"
ClientSecret: "CLIENT_SECRET"
# Get Client ID and Client Secret from https://discord.com/developers/applications
# Canonical callback URL (host + path). Protocol (http/https) can follow the
# browser when UseRequestOrigin is true (default).
RedirectUri: "http://localhost:3000/api/auth/discord/callback"
# Optional: register extra callback URLs in Discord and list them here.
# RedirectUris:
# - "https://bot.example.com/api/auth/discord/callback"
# UseRequestOrigin: true
# CookieSecure: false
OwnerUserIds:
- "OWNER_USER_ID"
AllowedUserIds: []
# Dashboard Features
LiveChat:
Mode: "webhook"
# "webhook" - Webhook to send messages with staff member's avatar and Discord username
# "bot" - Send messages as Bot account with discord username prefix.
# "bot_noprefix" - Send messages as Bot account without discord username prefix.
+196
View File
@@ -0,0 +1,196 @@
# ___ _ ___ _
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
# |_| |_|
#
# SupportBot created by Emerald Services
# Installed with MIT License
#
# Discord Support: https://discord.gg/emeraldsrv
# Official Documentation: https://docs.emeraldsrv.com
# Addons Marketplace: https://community.emeraldsrv.com
#
# Commands Configuration (commands.yml)
Help:
Command: "help"
Description: "Get a list of all the commands."
Permission: ["SendMessages"]
Enabled: true
Info:
Command: "info"
Description: "Get information about the **ServerName**."
Permission: ["SendMessages"]
Enabled: true
TicketPanel:
Command: "ticketpanel"
Description: "Create the ticket creation panel"
Permission: ["Administrator"]
Enabled: true
Profile:
Command: "profile"
Description: "Shows the profile for a discord member."
Permission: ["SendMessages"]
# Command is automatically disabled when the TicketClaims is disabled.
OpenTicket:
Command: "ticket"
Description: "Create a support ticket."
Permission: ["SendMessages"]
Enabled: true
AddUser:
Command: "add"
Description: "Add a user to a support ticket."
Permission: ["SendMessages"]
Enabled: true
CloseTicket:
Command: "close"
Description: "Close a support ticket"
Permission: ["SendMessages"]
Enabled: true
TicketManage:
Command: "ticketmanage"
Description: "Manage an existing support ticket."
Permission: ["SendMessages"]
Enabled: true
Close:
Command: "close"
Description: "Close a support ticket."
ForceAdd:
Command: "forceadd"
Description: "Add a user to a support ticket by bypassing the invite system."
ForceRemove:
Command: "forceremove"
Description: "Remove a user from a support ticket by bypassing the normal restrictions."
Rename:
Command: "rename"
Description: "Rename a support ticket by bypassing the normal restrictions."
Suggestion:
Command: "suggest"
Description: "Create a suggestion."
Permission: ["SendMessages"]
Enabled: true
SuggestionAdmin:
Command: "suggestadmin"
Description: "Update a suggestion’s status."
Permission: ['SendMessages']
# Disable "Suggestion" to disable this command.
Embed:
Command: "embed"
Description: "Build a discord embed."
Permission: ["SendMessages"]
Enabled: true
Translate:
Command: "translate"
Description: "Translate something to english"
Permission: ["SendMessages"]
Enabled: true
UserInfo:
Command: "userinfo"
Description: "Get info about a user."
Permission: ["SendMessages"]
Enabled: true
Ping:
Command: "ping"
Description: "Pong."
Permission: ["SendMessages"]
Enabled: true
# ---------------------------------------
# Bot Moderator Commands
# ---------------------------------------
Mod:
Command: "mod"
Description: "Execute a moderation action."
Permission: ["BanMembers", "KickMembers"]
Enabled: true
TicketBlacklist:
Command: "ticket-blacklist"
Description: "Blacklist a user from creating Tickets."
Add:
Command: "add"
Description: "Add a user to the blacklist."
Remove:
Command: "remove"
Description: "Remove a user from the blacklist."
View:
Command: "view"
Description: "View all blacklisted users."
# ---------------------------------------
# Bot Administrator Commands
# ---------------------------------------
Settings:
Command: "settings"
Description: "Update bot settings."
Permission: ["Administrator"]
Enabled: true
AI:
Command: "ai"
Description: "Manage SupportBot AI settings and memory."
Permission: ["Administrator"]
Enabled: true
Train:
Command: "train"
Description: "Train the AI by adding knowledge to its database."
Knowledge:
Command: "knowledge"
Description: "Manage the AI knowledge base."
Clear:
Command: "clear"
Description: "Clear all trained knowledge from the AI."
Memory:
Command: "memory"
Description: "Manage the AI conversational memory."
Clear:
Command: "clear"
Description: "Clear recent conversation memory for this channel."
# ---------------------------------------
# Command Description Configuration
# ---------------------------------------
BlacklistDesc: "Blacklist a user from creating Tickets."
TicketAddDesc: "Add a user to a support ticket."
TicketRemoveDesc: "Remove a user from a support ticket."
TranslateCommandDesc: "Translates the language provided, along with the abbreviation (2 char) long language code"
OpenTicketDesc: "Open a new support ticket."
CloseTicketDesc: "Close your support ticket."
InfoCommandDesc: "Get information about the brand."
PingCommandDesc: "Pong!"
HelpCommandDesc: "Get a list of all the commands."
EmbedCommandDesc: "Send a message as an embed."
SuggestCommandDesc: "Suggest a feature for the server."
UserInfoCommandDesc: "Get info about a user."
TicketManageDesc: "Manage an existing support ticket."
TicketManageCloseDesc: "Close a support ticket."
TicketManageForceAddDesc: "Add a user to a support ticket by bypassing the invite system."
TicketManageForceRemoveDesc: "Remove a user from a support ticket by bypassing the normal restrictions."
TicketManageForceRenameDesc: "Rename a support ticket by bypassing the normal restrictions."
+325
View File
@@ -0,0 +1,325 @@
# ___ _ ___ _
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
# |_| |_|
#
# SupportBot created by Emerald Services
# Installed with MIT License
#
# Discord Support: https://discord.gg/emeraldsrv
# Official Documentation: https://docs.emeraldsrv.com
# Addons Marketplace: https://community.emeraldsrv.com
#
# Messages Configuration (messages.yml)
# ----------------------------
# Info Command
# ----------------------------
Info:
Title: "About Us"
Description: "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand."
Button: "Visit Site"
URL: "https://emeraldsrv.dev"
Colour: "#45b463"
# ----------------------------
# Welcome/Leave Messages
# ----------------------------
Welcome:
Enabled: true
LayoutStyle: "classic"
# Available Layout Styles
# "modern" - For the new Discord v2 components
# "classic" - Use the classic Discord look.
Embed:
Colour: "#45b463"
Title: "Welcome!"
Message: ":smile: %joined_user% has just landed!"
Icon:
Mode: "USER" # USER | BOT | CUSTOM
CustomURL: "https://example.com/custom.png"
Image:
Enabled: false
URL: "https://example.com/banner.png"
Buttons:
Enabled: true
List:
- label: "Website"
style: "Link"
url: "https://emeraldsrv.dev"
emoji: "⌨️"
Modern:
Colour: "#45b463"
Title: "**Welcome to ServerName**"
Body: ":smile: %joined_user% has just landed!\n\n🔨 **Server Rules:** <#CHANNEL_ID>"
Icon:
Mode: "BOT" # USER | BOT | CUSTOM
CustomURL: "https://example.com/custom-icon.png"
Image:
Enabled: false
URL: "https://example.com/banner.png"
Buttons:
- label: "BuiltByBit"
style: "Link"
url: "https://your-website.com/how-to-play-java"
emoji: "⌨️"
- label: "GitHub"
style: "Link"
url: "https://your-website.com/how-to-play-bedrock"
emoji: "👾"
- label: "Tebex"
style: "Link"
url: "https://your-store.tebex.io/"
emoji: "🛒"
Leave:
Embed:
Colour: "#f74343"
Title: "Goodbye!"
Message: ":slight_frown: %left_user% has recently dropped out!"
Thumbnail: "BOT"
ImageEnabled: false
ImageURL: "https://animagelink.png/"
# ----------------------------
# Suggestion Messages
# ----------------------------
Suggestions:
Sent_Title: "Suggestion Sent!"
Sent: ":white_check_mark: **You have successfully sent a suggestion.**"
NoSelfVote: ":x: **You cannot vote on your own suggestion.**"
RemoveVoteButton: "Remove Vote"
# ----------------------------
# Ticket Messages
# ----------------------------
Ticket:
# Available Placeholders:
# %ticketusername% - User who created the ticket.
# <@%ticketauthor%> - Tags the user who created the ticket.
# <#%ticketid%> - Mentions the ticket channel.
# You can use Markdown formatting in this message:
# # Heading 1
# ## Heading 2
# ### Heading 3
# **Bold** text
# *Italic* text
# -# Small text
# - Bullet points
# Links: [text](https://example.com)
TicketAuthorTitle: "-# %ticketusername%'s Ticket"
TicketTitle: "## Support Ticket"
TicketMessage: "Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you."
TicketThumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png"
TicketCreatedAlert: ":ticket: Ticked Created: <#%ticketid%>"
TicketExists: "❌ Ticket already exists"
TicketClaim: ":white_check_mark: Ticket claimed by" #The user who claimed the ticket will be added to the end of this message
TicketUnclaim: ":white_check_mark: Ticket unclaimed by" #The user who claimed the ticket will be added to the end of this message
TicketArchived: ":white_check_mark: Ticket archived."
TicketUnarchived: ":white_check_mark: Ticket unarchived."
TicketLocked: ":lock: Ticket is locked."
DisableInvites: "🔒 Invites have been disabled for this ticket."
EnableInvites: "🔓 Invites have been enabled for this ticket."
# Available Placeholders:
# <@%user%> - Tags the user who was added to the ticket.
Blacklisted: ":x: You are not allowed to open a ticket"
Muted: ":x: You are `Muted` and therefore not allowed to open any tickets"
InvalidSubject: "No Reason Provided!"
ConfirmClose: "Please confirm closing the ticket by clicking the button below."
CloseTimeout: "Ticket close confirmation timed out."
RenamedSuccess: "🏷️ Ticket renamed successfully!"
ClaimTickets:
ClaimTitle: "Incoming Support Request!"
ClaimMessage: "A new ticket has been created by <@!%user%>. Click the button below to claim this ticket."
ClaimMessage_Edit: "<#%channel%> was claimed by <@!%user%>."
ClaimedTitle: "Ticket Claimed!"
Claimed: "You have successfully claimed this ticket. <#%channel%>"
NoPermsToClaimTitle: "Error, no permissions!"
NoPermsToClaim: ":x: **Err!** You do not permissions to claim this ticket."
ReassigningTitle: "Reassigning!..."
ReassigningMessage: "<@%user%> did not claim the ticket. **Reassigning...**"
TicketQuestions:
Details:
Title: "Ticket Details"
Description: "Questions by <@%user%>" # Use <@%user%> to display the username.
AIMode:
Title: "SupportBot AI"
Description: "Use the buttons below to enable or disable AI chat in this ticket."
EnabledMessage: "AI Mode has been enabled for this ticket."
DisabledMessage: "AI Mode has been disabled for this ticket."
ThreadWelcomeTitle: "SupportBot AI"
ThreadWelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue."
EnableButton: "Enable AI Mode"
DisableButton: "Disable AI Mode"
TicketNotFound: "Ticket not found."
EnableError: "There was an error enabling SupportBot AI."
DisableError: "There was an error disabling SupportBot AI."
# ----------------------------
# Review Messages
# ----------------------------
ReviewSystem:
Rate:
Title: "Review us!"
Description: "Please rate our support for this ticket (1-5 stars)."
Stars:
One: "⭐ 1 Star"
Two: "⭐⭐ 2 Stars"
Three: "⭐⭐⭐ 3 Stars"
Four: "⭐⭐⭐⭐ 4 Stars"
Five: "⭐⭐⭐⭐⭐ 5 Stars"
Comment:
Title: "Write a comment."
Description: "Write a comment about your experience."
ReviewEmbed:
Title: "Ticket Review"
CommentTitle: "Comment"
RatingTitle: "Rating"
ReviewedStaffTitle: "Reviewed Staff:"
ReviewedByTitle: "Reviewed By:"
ReviewEmoji: "⭐"
Color: "#f7a943"
# ----------------------------
# Mod Command Messages
# ----------------------------
Mod:
TicketBlacklist:
Add:
Description: "Add a user to the ticket blacklist"
UserDescription: "User to blacklist"
AlreadyBlacklisted: ":x: {userTag} is already blacklisted."
Success: ":white_check_mark: {userTag} has been added to the blacklist. They can no longer use the ticket system."
Remove:
Description: "Remove a user from the ticket blacklist"
UserDescription: "User to remove from blacklist"
NotBlacklisted: ":x: {userTag} is not blacklisted."
Success: ":white_check_mark: {userTag} has been removed from the blacklist. They can now use the ticket system again."
View:
Description: "View the list of all blacklisted users"
NoBlacklistedUsers: ":information_source: There are currently no users on the blacklist."
# ----------------------------
# Logging Messages
# ----------------------------
TicketLog:
Title: "Ticket Log"
Colour: "#0cc559"
TicketBlacklistLog:
Title: "Ticket Blacklist Log"
Colour: "#0cc559"
# ----------------------------
# Misc Messages
# ----------------------------
AddUser:
NotInTicket_Title: "Ticket Invitation"
NotInTicket_Description: "This command can only be used within a ticket thread."
Invite_Title: "User Invited"
Invite_Description: "%username% has been invited to join the ticket."
DM_Title: "Ticket Invitation"
DM_Description: "You have been invited to join the ticket %channelname%. Do you accept?"
Accept_Label: "Accept"
Reject_Label: "Reject"
ViewTicket_Label: "Go to Ticket"
Accepted_Title: "Invitation Accepted"
Accepted_Message: "You have been added to the ticket."
AddedToTicket_Title: "Added to Ticket"
AddedToTicket_Description: "You have been added to the ticket %channel_link%."
# Added Message for inside the ticket, this informs everyone inside the ticket that the user has been added.
Added_Title: "User Added"
Added_Description: "Successfully added **%username%** to the ticket."
Declined_Title: "Invitation Declined"
Declined_Message: "You have declined the invitation."
NoResponse_Title: "No Response"
NoResponse_Message: "You did not respond in time. Please contact staff if you wish to join the ticket."
Error_Title: "Error"
Error_Adding: "There was an error adding you to the ticket. Please try again."
DM_Error_Title: "Error"
DM_Error_Description: "Unable to send a DM to %username%. Please ensure your DMs are enabled."
Unexpected_Error_Title: "Error"
Unexpected_Error_Description: "An unexpected error occurred: %error%"
RemoveUser:
NotInTicket_Title: "Error"
NotInTicket_Description: "This command can only be used in ticket threads."
Removed_Title: "User Removed"
Removed_Message: "The user has been successfully removed from the ticket."
RemovedFromTicket_Title: "Removed from Ticket"
RemovedFromTicket_Description: "You have been removed from the ticket channel %channel_link%."
Error_Title: "Error"
Error_Removing: "There was an error removing the user from the ticket."
ForceAddUser:
NotInTicket_Title: "Not a Ticket"
NotInTicket_Description: "This command can only be used within a ticket thread."
Added_Title: "User Added"
Added_Description: "Successfully added %username% to the ticket."
AddedToTicket_Title: "You have been added to a Ticket"
AddedToTicket_Description: "You have been added to the ticket channel [%channel_link%]."
Error_Title: "Error"
Error_Adding: "There was an error adding the user to the ticket."
Error:
IncorrectPermissions: ":x: **Error!** You do not have the correct permissions to use this command."
MissingChannel: ":x: **Error!** Required channel not found. Please ensure it is set up correctly!"
UserNotFound: ":x: **Error!** Specified user not found. Please check if they are in this server."
CommandNotAllowedHere: ":warning: This command cannot be used in this channel."
EntryAlreadyExists: ":warning: Cannot complete the action, as the entry already exists."
EntryDoesNotExist: ":warning: Cannot complete the action, as the entry does not exist."
ActionFailed: ":x: **Error!** An error occurred while processing your request. Please try again later."
InvalidChannel: ":x: **Error!** The suggestions channel has not been set up correctly."
+87
View File
@@ -0,0 +1,87 @@
# -------------------------
# SupportBot AI
#
# In order to use the SupportBot AI, You need to setup an OpenAI or Groq Developer Account.
# this can be done at https://platform.openai.com/ or https://groq.com/
# -------------------------
Enabled: true
General:
Provider: "openai"
Model: "MODEL"
Model_API_Key: "MODEL_API_KEY"
# Providers Available:
# "openai" - Use OpenAI's GPT models. Example: "gpt-5.4" "gpt-5-mini"
# "groq" - Use Groq's GroqAI models. Example: "meta-llama/llama-4-scout-17b-16e-instruct"
# "claude" - Use Anthropic's Claude models. Example: "claude-sonnet-4-6"
Tokens: 1200
Temperature: 0.4
Name: "SupportBot AI"
PastebinAPI_Key: "YOUR_PASTEBIN_API_KEY"
PastebinAPI_URL: "https://pastebin.com/api/api_post.php"
Style:
MessageType: "components_v2"
# Available Styles for AI:
# "embed" - AI responses will be sent as Discord embeds.
# "text" - AI responses will be sent as plain messages.
# "components_v2" - AI responses will be sent with the new components style.
Features:
SuggestTickets: true # Should SupportBot AI suggest creating a ticket if the user needs more help?
Channels:
AIChannel: "CHANNEL_ID"
Embed:
Color: "#45b463"
Footer: "SupportBot AI"
Memory:
Enabled: true
StoreFacts: true
# StoreFacts - Enables saving important facts the AI learns.
StoreSummaries: true
# StoreSummaries - Enables saving conversation summaries.
MaxRecentMessages: 15
# MaxRecentMessages - Number of recent messages used for context.
Scope: "guild" # channel | guild
# Available Scopes for Memory:
# "channel" - Memory is per channel only.
# "guild" - Memory is shared across the server.
CustomInstructions:
- "You are SupportBot AI made by Emerald Services"
- "You are built with DiscordJS/NodeJS and currently running on version 26.2"
- "You're current build can be found here
https://github.com/Emerald-Services/SupportBot"
- "You're main build can be found here
https://builtbybit.com/resources/supportbot-free-advanced-ticket-bot.8940/
and Polymart at
https://polymart.org/product/518/supportbot-1-discord-ticket-bot"
- "SupportBot includes all-new AI features such as a memory system that allows
you to remember facts and summaries of conversations, which can be used to
provide better responses."
- "SupportBot AI also has a tickets AI Mode which is enabled in
supportbot.yml, which allows you to assist users in support tickets and
provide better responses based on the context of the ticket."
- "Your main purpose is to assist users in the support server and provide
helpful responses to their questions and issues. Always be accurate and if
you do not have answer then simply ask the person to create a ticket if they
need more help."
#- "Add custom instructions here"
Messages:
ErrorResponse: "I couldn't generate a response."
AIError: "There was an error contacting the AI provider."
AIDisabled: "AI is currently disabled."
MessageTooLong: "My response was too long, so I uploaded it here: {pastebin}"
SuggestTicketText: "-# Need more help? React with 🎫 to open a ticket."
SuggestTicketReaction: "🎫"
+389
View File
@@ -0,0 +1,389 @@
# ___ _ ___ _
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
# |_| |_|
#
# SupportBot created by Emerald Services
# Installed with MIT License
#
# Discord Support: https://discord.gg/emeraldsrv
# Website: https://emeraldsrv.dev
#
# SupportBot Configuration (supportbot.yml)
SupportBot_Version: "v26.2"
# -------------------------
# General
# -------------------------
General:
Name: "SupportBot"
Token: "YOUR_BOT_TOKEN_HERE"
Addons:
Enabled: true
GuildId: "YOUR_GUILD_ID_HERE"
# -------------------------
# Database Configuration
# -------------------------
Database:
# Driver options: "mysql" (works for MariaDB & MySQL) or "sqlite"
Driver: "mysql"
# Connection Settings for MariaDB / MySQL (used when Driver is "mysql" or "mariadb")
MySQL:
Host: "localhost"
Port: 3306
User: "root"
Password: "password"
Database: "supportbot"
ConnectionLimit: 10
# Connection Settings for SQLite (used when Driver is "sqlite")
SQLite:
File: "./Data/supportbot.db"
# Activity
# -------------------------
Activity:
Status: "SupportBot"
Type: "Watching"
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
# Available Types:
# Playing
# Watching
# Listening
# Competing
# Streaming (URL required)
# You can also update the activity via Discord using: /settings
# -------------------------
# Embeds
# -------------------------
Embed:
Colours:
General: "#0cc559"
Success: "#04b545"
Error: "#d13030"
Warn: "#fca117"
Footer: "SupportBot | Developed by Emerald Development"
# Use 0x<hexcode> to change the colour of the embeds.
# -------------------------
# Roles
# -------------------------
Roles:
AutoRole:
Enabled: true
Role: "ROLE_ID"
StaffMember:
Staff: "ROLE_ID"
Admin: "ROLE_ID"
Moderator: "ROLE_ID"
Mod:
AllowSupportStaff: true
# Allow Support Staff to use the /mod command. (More in detail customisation in commands.yml)
# ----------------------------
# System Messages
# ----------------------------
Welcome:
Enabled: false
Channel: "CHANNEL_ID"
Leave:
Enabled: false
Channel: "CHANNEL_ID"
# ----------------------------
# Suggestions
# ----------------------------
Suggestions:
Mode: "Channel" # Channel or Forum
ForumChannel: "FORUM_CHANNEL_ID" # A forum channel, disabled if mode is set to Channel.
Channel: "CHANNEL_ID" # A text channel, disabled if mode is set to Forum.
UpvoteEmoji: "✅"
DownvoteEmoji: "❌"
UpvoteTitle: "Yes"
DownvoteTitle: "No"
OwnSuggestion: true
# If false this will not allow the user to vote for a suggestion they created
# If true this will allow the user to vote for a suggestion they created
ShowUsers: true
Threads:
Enabled: true
Title: "Suggestion-releated Thread"
Reason: "A thread releted to this suggestion"
Buttons:
Upvote: "2"
Downvote: "2"
RemoveVote: "4"
RemoveVote_Title: "Remove Vote"
# Available Button Styles
# 1 - Blurple
# 2 - Grey
# 3 - Green
# 4 - Red
Tags:
Created: "TAG_ID"
Considering: "TAG_ID"
Solved: "TAG_ID"
Denied: "TAG_ID"
# -------------------------
# Tickets
# -------------------------
VoiceTickets:
Name: "%username%'s VC"
Category: "CATEGORY_ID"
Ticket:
TicketType: "channels"
# Available TicketTypes:
# channels - Tickets will be created as channels in the TicketChannelsCategory
# threads - Tickets will be created as threads through the "TicketHome" channel.
UserManagement:
DMOnAdd: true # DM the user when they are added to the ticket
DMOnRemove: true # DM the user when they are removed from the ticket
DMTranscript: true # DM the user a transcript of the ticket when it is closed
TicketHome: "CHANNEL_ID"
# What is TicketHome?
# - Channel where the Ticket creation panel is sent.
# - Channel where ticket threads are made. (only if ticket threads is enabled above.)
# For TicketType: channels
TicketChannelsCategory: "CATEGORY_ID"
TicketChannelsCategory2: "CATEGORY_ID" # This will be used if the first category has reached the max channel limit.
TicketArchiveCategory: "CATEGORY_ID"
# ---
Channel: "ticket-" # The prefix for ticket channel names
TicketsPerUser: 100
TicketReason: true
# If ticket reason is set to false, It will not prompt to create a reason
TicketSubject: "description"
# Available TicketSubjects:
# "description" will display the ticket reason within the channel description
# "embed" will display the ticket reason within the ticket embed
Timeout: 5
# Set to true to allow all staff to access the ticket
Close:
StaffOnly: true
# Set to false is anyone is able to close a ticket.
Invites:
# Invites is for ticket threads.
StaffOnly: true
ClaimTickets:
Enabled: false
Channel: "CHANNEL_ID"
# Channel where the ticket assignments are sent for staff to claim.
ClaimAcceptTime: 120000
# How long the assigned user has to accept the ticket before it gets re-assigned to someone else.
# 120000 = 2 Minutes
# 180000 = 3 Minutes
# 240000 = 4 Minutes
# 300000 = 5 Minutes
# Time in Mliseconds
ButtonTitle: "Claim Ticket"
ButtonEmoji: "🎫"
Button: "1"
# Available Button Styles
# 1 - Blurple
# 2 - Grey
# 3 - Green
# 4 - Red
ReviewSystem:
Enabled: false
Channel: "CHANNEL_ID"
UseModal: true
Questions:
# Default questions asked when a ticket is created, these are used if departments are disabled.
Enabled: false
List:
- "Please provide your account email?"
- "What is the nature of your issue?"
- "Have you tried resolving the issue yourself?"
Log:
TicketDataLog: "CHANNEL_ID"
TickeDataTitle: "Ticket Data"
TicketBlacklistLog: "CHANNEL_ID"
AIMode:
Enabled: true
ThreadName: "Chat with SupportBot AI"
WelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue."
# -------------------------
# Ticket Departments
# -------------------------
DepartmentSystem:
Enabled: false
Placeholder: "Choose a ticket department"
DefaultDepartment: "general"
Departments:
general:
Name: "General Support"
Description: "Open a general support ticket"
Emoji: "🎫"
Category: "CATEGORY_ID"
Role: "ROLE_ID"
ChannelPrefix: "support-"
DefaultPriority: "normal"
Questions:
- "What do you need help with?"
- "When did the issue start?"
purchase:
Name: "Purchase Support"
Description: "Get help with store purchases"
Emoji: "💳"
Category: "CATEGORY_ID"
Role: "ROLE_ID"
ChannelPrefix: "purchase-"
DefaultPriority: "high"
Questions:
- "What is your order ID?"
- "What product did you purchase?"
- "What payment issue are you having?"
reports:
Name: "Player Report"
Description: "Report a player for breaking the rules"
Emoji: "🚨"
Category: "CATEGORY_ID"
Role: "ROLE_ID"
ChannelPrefix: "report-"
DefaultPriority: "normal"
Questions:
- "Who are you reporting?"
- "What happened?"
- "Do you have evidence?"
appeals:
Name: "Ban Appeals"
Description: "Appeal a server punishment"
Emoji: "⚖️"
Category: "CATEGORY_ID"
Role: "ROLE_ID"
ChannelPrefix: "appeals-"
DefaultPriority: "low"
Questions:
- "What punishment are you appealing?"
- "Why should it be reviewed?"
- "Anything else staff should know?"
PrioritySystem:
Enabled: false
DefaultPriority: "Normal"
AllowUsersToChooseOnOpen: true
Priorities:
low:
Name: "Low"
Emoji: "🟢"
normal:
Name: "Normal"
Emoji: "🟡"
high:
Name: "High"
Emoji: "🟠"
urgent:
Name: "Urgent"
Emoji: "🔴"
# -------------------------
# Buttons
# -------------------------
Buttons:
General:
Delete: "🗑️"
DeleteStyle: "4"
Voice:
TicketDeleteText: "Delete Voice Channel"
Tickets:
ClaimEmoji: "🧑‍💻"
ClaimStyle: "1"
Unarchive: "Unarchive Ticket"
Unarchive_Emoji: "♻️"
Unarchive_Style: "2"
Unlock: "Unlock Ticket"
Unlock_Emoji: "🔓"
Unlock_Style: "2"
# Available Button Styles
# 1 - Blurple
# 2 - Grey
# 3 - Green
# 4 - Red
# -------------------------
# Select Menus
# -------------------------
SelectMenus:
Tickets:
PanelEmoji: "⚙️"
CloseEmoji: "🗑️"
ArchiveEmoji: "♻️"
LockEmoji: "🔐"
RenameEmoji: "🏷️"
VCEmoji: "🔊"
EnableInvitesEmoji: "📤"
DisableInvitesEmoji: "📥"
# -------------------------
# Logging
# -------------------------
MessageDelete:
Channel: "CHANNEL_ID"
Colour: "#d92525"
MessageUpdate:
Channel: "CHANNEL_ID"
Colour: "#f2a024"
Translate:
TranslateLog: "CHANNEL_ID"
+67
View File
@@ -0,0 +1,67 @@
# ___ _ ___ _
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
# |_| |_|
#
# SupportBot created by Emerald Services
# Installed with MIT License
#
# Discord Support: https://discord.gg/emeraldsrv
# Official Documentation: https://docs.emeraldsrv.com
# Addons Marketplace: https://community.emeraldsrv.com
#
# Ticket Creation Panel Configuration (ticket-panel.yml)
# -------------------------------
# Ticket Panel Configuration
# -------------------------------
Panel: true
Style:
Title: "## 📬 Open Ticket"
Description: "Tap the button to open a support ticket, Our team will respond as
soon as possible."
Image: "https://cdn.1tzemerald.space/Emerald%20Services/SBPoster.png"
Color: "#45b463"
Layout: "3"
# Available Layout Styles
# "1" - Style 1 - Title & Text, Button at the top -- Image at the bottom
# "2" - Style 2 - Title & Text, Button at the bottom -- Image at the top
# "3" - Style 3 - The classic look - Title & Text at the top -- Image in the middle -- Button at the bottom
# "4" - Style 4 - Classic Embed (Title & Text, Image on right, Button at bottom)
Button:
Text: "Open"
Emoji: "🛟"
Color: "2"
CustomId: "createticket"
# Available Button Styles
# 1 - Blurple
# 2 - Grey
# 3 - Red
# 4 - Green
DepartmentMenu:
CustomId: "ticketdepartmentselect"
Placeholder: "Choose a ticket department"
DefaultDescription: "Open a support ticket"
DefaultEmoji: "🎫"
Settings:
Image: true
TopTitle: true
TopDivider: true
BottomTitle: false
BottomDivider: false
Messages:
NotConfigured: "Ticket Panel is not set up. Please configure it in
`/Configs/ticket-panel.yml`."
InvalidChannel: "TicketHome channel not found. Please check your config file."
PanelSent: "Ticket panel message has been sent!"
PanelSentWithDepartments: "Ticket panel message with department selection has been sent!"
SendError: "An error occurred while sending the ticket panel message."
+33
View File
@@ -0,0 +1,33 @@
Theme:
Background: '#0e0e0e'
Panel: '#1d1d1d'
PanelSecondary: '#222222'
PanelTertiary: '#1d2938'
Border: '#2e2e2e'
Text: '#e6edf3'
Muted: '#606060'
Accent: '#396ff8'
AccentSoft: rgba(34, 197, 94, 0.12)
AccentBorder: rgba(34, 197, 94, 0.25)
AccentText: '#8df0ad'
Link: '#396ff8'
AttachmentLink: '#396ff8'
CodeBackground: '#0e0e0e'
Typography:
FontFamily: Poppins
Header:
Title: Ticket Transcript
Subtitle: Exported conversation log for support ticket **{{ticketName}}**
Badge: Closed Transcript
ShowMetaCards: true
Meta:
ChannelLabel: Ticket Name
TicketIdLabel: Ticket ID
MessageCountLabel: Message Count
CloseReasonLabel: Close Reason
DefaultCloseReason: No Reason Provided.
Messages:
SectionTitle: Messages
EmptyState: No messages were found in this ticket.
Advanced:
CustomCss: ''
-1
View File
@@ -1 +0,0 @@
{}
-37
View File
@@ -1,37 +0,0 @@
# ___ _ ___ _
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
# |_| |_|
#
# SupportBot created by Emerald Services
# Installed with MIT License
#
# Discord Support: https:#emeraldsrv.dev/discord
# Community Resources: https:#emeraldsrv.dev/resources
# -------------------------------
# Commamnd Name Configuration
# -------------------------------
TicketAdd: "ticketadd"
TicketRemove: "ticketremove"
OpenTicket: "ticket"
CloseTicket: "close"
InfoCommand: "info"
PingCommand: "ping"
HelpCommand: "help"
# ---------------------------------------
# Commamnd Description Configuration
# ---------------------------------------
TicketAddDesc: "Add a user to a support ticket."
TicketRemoveDesc: "Remove a user from a support ticket."
OpenTicketDesc: "Open a new support ticket."
CloseTicketDesc: "Close your support ticket."
InfoCommandDesc: "Get information about the brand."
PingCommandDesc: "Pong!"
HelpCommandDesc: "Get a list of all the commands."
-189
View File
@@ -1,189 +0,0 @@
# ___ _ ___ _
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
# |_| |_|
#
# SupportBot created by Emerald Services
# Installed with MIT License
#
# Discord Support: https:#emeraldsrv.dev/discord
# Community Resources: https:#emeraldsrv.dev/resources
SupportBot_Version: "7.2 pre-3"
# -------------------------
# General Configuration
# -------------------------
Name: "SupportBot"
Prefix: "!"
Token: "BOT_TOKEN"
# -------------------------
# Activity Configuration
# -------------------------
BotActivity: "Need Help? -ticket"
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
# Set The ActivityType to "STREAMING" for a streaming status!
ActivityType: "PLAYING"
# Activity status can either be PLAYING, WATCHING, LISTENING or STREAMING.
# -------------------------
# Embed Configuration
# -------------------------
EmbedColour: "#f74343"
EmbedFooter: "SupportBot | An Advanced Ticket System"
ErrorColour: "#d12519"
SuccessColour: "#04b545"
WarningColour: "#fca117"
# -------------------------
# Information Configuration
# -------------------------
InfoTitle: "Information"
AboutMe: "Hi, I'm SupportBot an advanced discord ticket bot with 100% customisation! [Download](https://emeraldsrv.dev/download)"
InfoButtonText: "Visit Site"
InfoURL: "https://emeraldsrv.dev"
InfoColour: "#f74343"
# -------------------------
# Help Configuration
# -------------------------
HelpMenu: true # Do not change!
SendHelpPage: "channel" # Set to channel or dm
# -------------------------
# Role Configuration
# -------------------------
AutoRole: true
AutoRole_Role: "Member"
LockdownRole: "@everyone"
Staff: "Support Team"
Admin: "Project Developer"
TicketBlackListRole: "Blacklisted"
# -------------------------
# Tickets Configuration
# -------------------------
TicketCategory: "▬▬ TICKETS ▬▬"
TicketChannel: "ticket"
TicketLog: "ticket-log"
TranscriptLog: "ticket-data"
TranscriptTitle: "Ticket Transcript"
TicketLog_Title: "Ticket Log"
TicketBlackListMessage: "You are not allowed to open a ticket"
TicketSubject: "description"
# "description" will displays the ticket reason within the channel description
# "embed" will displays the ticket reason within the ticket embed
AllowTicketMentions: true
# Set to true to allow the ticket to mention the author and the support team.
# Set to false to dis-allow mentions within tickets.
# --- Creation Config ---
# --- Start ---
# Available Placeholders:
# %ticketusername% - User who created the ticket.
# <@%ticketauthor%> - Tags the user who created the ticket.
# <#%ticketid%> - Mentions the ticket channel.
Ticket_Title: "Server Support | %ticketusername%"
TicketMessage: "Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you."
Ticket_Thumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png"
NoTicketSubject: "No Reason Provided!"
TicketCreatedAlert: ":ticket: Your support ticket has been created: <#%ticketid%>"
AddedUser: "👍 <@%user%> has been added to this ticket"
RemovedUser: "👍 <@%user%> has been removed from this ticket"
# --- Buttons Config ---
# --- Start ---
# Discord Button Colour Selection
# PRIMARY - Blurple
# DANGER - Red
# SUCCESS - Green
# Secondary - Grey
TicketLockReaction: "🔒"
TicketDeleteReaction: "⛔"
LockTicketCategory: "Archive"
TicketCloseEmoji: "❌"
TicketDeleteEmoji: "⛔"
TicketLockEmoji: "🔒"
TicketCloseColour: "DANGER"
TicketDeleteColour: "DANGER"
TicketLockColour: "PRIMARY"
TicketDept1Colour: "PRIMARY"
TicketDept2Colour: "PRIMARY"
TicketDept3Colour: "PRIMARY"
TicketDept1Emoji: "1️⃣"
TicketDept2Emoji: "2️⃣"
TicketDept3Emoji: "3️⃣"
TranscriptSavedMessage: "📝 **Transcript Saved!**"
TicketLockMessage: "❌ **Ticket Locked!**"
TicketDeleteMessage: "⚠ **Ticket Deleting!**" # Tickets deleting after 5 seconds by default.
TicketDeleteTime: 5000 # Time must be set in miliseconds.
# --- Departments Config ---
# --- Start ---
TicketDepartments: true
AllowStaff: true
# If set to false users with the "Staff" role will not be able to view tickets
# without a valid department role set below.
# Set to false to allow "Staff" to see tickets whether its a department for them or not.
DepartmentTitle_1: "General Support"
DepartmentTitle_2: "Billing Support"
DepartmentTitle_3: "Admin Team"
# Ticket department title, This is the title for a ticket department to help users know what is available.
DepartmentRole_1: "Support Team"
DepartmentRole_2: "Support Team"
DepartmentRole_3: "Project Developer"
# Ticket roles are the roles that will be able to gain access to tickets depending on what department has been choosen.
# --- Closing Config ---
# --- Start ---
ClosingTicket: ":warning: You have requested to close your support ticket."
ClosingConfirmation_Word: "confirm"
CloseConfirmation: false
# ---------------------------
# Error / Warning Messages
# ---------------------------
IncorrectPerms: ":x: **Err!** You do not have the correct permissions to use this command."
InvalidChannel: ":x: **Err!** Channel not found. Please create it!"
UserNotFound: ":x: **Err!** This user doesn't exist, Are they in this server?"
NoValidTicket: ":warning: You cannot execute this command here."
TicketExists: ":warning: This ticket cannot be opened, It already exists!"
-1
View File
@@ -1 +0,0 @@
TicketPanelID: '896035054235574362'
-42
View File
@@ -1,42 +0,0 @@
# ___ _ ___ _
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
# |_| |_|
#
# SupportBot created by Emerald Services
# Installed with MIT License
#
# Discord Support: https:#emeraldsrv.dev/discord
# Community Resources: https:#emeraldsrv.dev/resources
# -------------------------------
# Ticket Panel Configuration
# -------------------------------
TicketPanel: true
# ----------------------------------------
# Ticket Panel Message Configuration
# ----------------------------------------
Channel: "create-ticket"
ButtonEmoji: "📩"
ButtonLabel: "Open Ticket"
ButtonColour: "SUCCESS"
# Colour Options:
# -> PRIMARY - Blurple
# -> SECONDARY - Grey
# -> SUCCESS - Green
# -> DANGER - Red
PanelTitle: "Need Help? Create a Support Ticket."
TicketPanel_Description: true
TicketPanel_Thumbnail: false
TicketPanel_Image: false
PanelMessage: "Click the button to open a new support ticket."
PanelThumbnail: "https://i.imgur.com/ieeZejl.png"
PanelImage: "https://i.imgur.com/5NkcmDq.png"
+82
View File
@@ -0,0 +1,82 @@
// Emerald Services | SupportBot
// Claude integration for SupportBot AI
const fs = require("fs");
const yaml = require("js-yaml");
const axios = require("axios");
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
function extractTextFromMessage(message) {
if (!message) return "";
if (typeof message === "string") {
return message.trim();
}
if (message.text) {
return message.text.trim();
}
if (Array.isArray(message)) {
return message
.map((part) => {
if (typeof part === "string") return part;
if (part?.type === "text" && typeof part.text === "string") {
return part.text;
}
return "";
})
.join("")
.trim();
}
return "";
}
async function chat({ messages, temperature, maxTokens }) {
const model = supportbotai.General.Model;
const apiKey = supportbotai.General.Model_API_Key;
const systemMessages = messages.filter((m) => m.role === "system");
const systemPrompt = systemMessages.map((m) => m.content).join("\n\n");
const userAssistantMessages = messages.filter((m) => m.role !== "system");
const response = await axios.post(
"https://api.anthropic.com/v1/messages",
{
model,
system: systemPrompt || undefined,
messages: userAssistantMessages,
temperature: temperature || supportbotai.General.Temperature || 0.4,
max_tokens: maxTokens || supportbotai.General.Tokens || 800,
},
{
headers: {
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
}
);
const data = response.data;
const content = extractTextFromMessage(data.content);
if (!content) {
console.error("Claude raw response:", JSON.stringify(data, null, 2));
throw new Error(
`Claude returned empty response. stop_reason=${data?.stop_reason || "unknown"}`,
);
}
return {
content,
raw: data,
};
}
module.exports = {
chat,
};
+70
View File
@@ -0,0 +1,70 @@
// Emerald Services | SupportBot
// Groq integration for SupportBot AI
const fs = require("fs");
const yaml = require("js-yaml");
const OpenAI = require("openai");
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
const client = new OpenAI({
apiKey: supportbotai.General.Model_API_Key,
baseURL: "https://api.groq.com/openai/v1",
});
function extractTextFromMessage(message) {
if (!message) return "";
if (typeof message.content === "string") {
return message.content.trim();
}
if (Array.isArray(message.content)) {
return message.content
.map((part) => {
if (typeof part === "string") return part;
if (part?.type === "text" && typeof part.text === "string") {
return part.text;
}
if (typeof part?.text?.value === "string") {
return part.text.value;
}
return "";
})
.join("")
.trim();
}
return "";
}
async function chat({ messages, temperature, maxTokens }) {
const model = supportbotai.General.Model;
const response = await client.chat.completions.create({
model,
messages,
temperature: temperature || supportbotai.General.Temperature || 0.4,
max_tokens: maxTokens || supportbotai.General.Tokens || 800,
});
const choice = response?.choices?.[0];
const message = choice?.message;
const content = extractTextFromMessage(message);
if (!content) {
console.error("Groq raw response:", JSON.stringify(response, null, 2));
throw new Error(
`Groq returned empty response. finish_reason=${choice?.finish_reason || "unknown"}`,
);
}
return {
content,
raw: response,
};
}
module.exports = {
chat,
};
+66
View File
@@ -0,0 +1,66 @@
// Emerald Services | SupportBot
// Open AI integration for SupportBot AI
const fs = require("fs");
const yaml = require("js-yaml");
const OpenAI = require("openai");
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
const client = new OpenAI({
apiKey: supportbotai.General.Model_API_Key,
});
function extractTextFromMessage(message) {
if (!message) return "";
if (typeof message.content === "string") {
return message.content.trim();
}
if (Array.isArray(message.content)) {
return message.content
.map((part) => {
if (typeof part === "string") return part;
if (part?.type === "text" && typeof part.text === "string")
return part.text;
if (typeof part?.text?.value === "string") return part.text.value;
return "";
})
.join("")
.trim();
}
return "";
}
async function chat({ messages, temperature, maxTokens }) {
const model = supportbotai.General.Model;
const response = await client.chat.completions.create({
model,
messages,
temperature: temperature || supportbotai.General.Temperature || 0.4,
max_completion_tokens: maxTokens || supportbotai.General.Tokens || 800,
});
const choice = response?.choices?.[0];
const message = choice?.message;
const content = extractTextFromMessage(message);
if (!content) {
console.error("OpenAI raw response:", JSON.stringify(response, null, 2));
throw new Error(
`OpenAI returned empty response. finish_reason=${choice?.finish_reason || "unknown"}`,
);
}
return {
content,
raw: response,
};
}
module.exports = {
chat,
};
+31
View File
@@ -0,0 +1,31 @@
const Event = require("../Structures/Event.js");
const {
getConfiguredGuildId,
syncGuildHealth,
logGuildWarning,
} = require("../Structures/GuildManager.js");
module.exports = new Event("guildCreate", async (client, guild) => {
const configuredId = getConfiguredGuildId();
if (!configuredId) {
logGuildWarning(
client,
`Joined "${guild.name}" (${guild.id}). Set General.GuildId in config to lock the bot to one server.`,
);
syncGuildHealth(client);
return;
}
if (guild.id === configuredId) {
syncGuildHealth(client);
return;
}
logGuildWarning(
client,
`Joined unauthorized server "${guild.name}" (${guild.id}). Leave it from the dashboard or remove the invite.`,
);
syncGuildHealth(client);
});
+23
View File
@@ -0,0 +1,23 @@
const Event = require("../Structures/Event.js");
const {
getConfiguredGuildId,
syncGuildHealth,
logGuildWarning,
} = require("../Structures/GuildManager.js");
module.exports = new Event("guildDelete", async (client, guild) => {
const configuredId = getConfiguredGuildId();
if (configuredId && guild.id === configuredId) {
logGuildWarning(
client,
`Left the configured server "${guild.name}" (${guild.id}). Re-invite the bot or update General.GuildId.`,
);
} else if (!configuredId) {
logGuildWarning(client, `Left server "${guild.name}" (${guild.id}).`);
} else {
logGuildWarning(client, `Left server "${guild.name}" (${guild.id}).`);
}
syncGuildHealth(client);
});
+151
View File
@@ -0,0 +1,151 @@
const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags,
ContainerBuilder,
TextDisplayBuilder,
SectionBuilder,
ThumbnailBuilder,
MediaGalleryBuilder
} = require("discord.js");
const Event = require("../Structures/Event.js");
const supportbot = require("../Structures/ConfigStore").supportbot;
const msgconfig = require("../Structures/ConfigStore").messages;
function getWelcomeThumbnail(client, member, iconCfg) {
switch ((iconCfg?.Mode || "").toUpperCase()) {
case "USER":
return member.user.displayAvatarURL();
case "BOT":
return client.user.displayAvatarURL();
case "CUSTOM":
return iconCfg.CustomURL;
default:
return null;
}
}
function buildModernComponents(client, member) {
const container = new ContainerBuilder();
if (msgconfig.Welcome.Modern.Colour) {
container.setAccentColor(parseInt(msgconfig.Welcome.Modern.Colour.replace("#", ""), 16));
}
const section = new SectionBuilder().addTextDisplayComponents(
new TextDisplayBuilder().setContent(
msgconfig.Welcome.Modern.Title.replace(/%joined_user%/g, member.user)
),
new TextDisplayBuilder().setContent(
msgconfig.Welcome.Modern.Body.replace(/%joined_user%/g, member.user)
)
);
const thumbUrl = getWelcomeThumbnail(client, member, msgconfig.Welcome.Modern.Icon);
if (thumbUrl) {
section.setThumbnailAccessory(
new ThumbnailBuilder()
.setURL(thumbUrl)
);
}
container.addSectionComponents(section);
// FIXED: Using MediaGalleryBuilder instead of TextDisplayBuilder for images
if (msgconfig.Welcome.Modern.Image?.Enabled && msgconfig.Welcome.Modern.Image.URL) {
container.addMediaGalleryComponents(
new MediaGalleryBuilder().addItems([
{
media: {
url: msgconfig.Welcome.Modern.Image.URL
}
}
])
);
}
if (Array.isArray(msgconfig.Welcome.Modern.Buttons)) {
const row = new ActionRowBuilder();
msgconfig.Welcome.Modern.Buttons.slice(0, 5).forEach(b => {
const btn = new ButtonBuilder()
.setLabel(b.label)
.setStyle(ButtonStyle[b.style])
.setEmoji(b.emoji || null);
if (b.style === "Link") btn.setURL(b.url);
else if (b.customId) btn.setCustomId(b.customId);
row.addComponents(btn);
});
container.addActionRowComponents(row);
}
return [container];
}
module.exports = new Event("guildMemberAdd", async (client, member) => {
if (!supportbot.Welcome.Enabled) return;
const WelcomeChannel =
member.guild.channels.cache.get(supportbot.Welcome.Channel) ||
member.guild.channels.cache.find(c => c.name === supportbot.Welcome.Channel);
if (!WelcomeChannel) return;
const style = (msgconfig.Welcome.LayoutStyle || "classic").toLowerCase();
if (style === "classic") {
const WelcomeEmbed = new EmbedBuilder()
.setColor(msgconfig.Welcome.Embed.Colour)
.setTitle(msgconfig.Welcome.Embed.Title || null)
.setDescription(
msgconfig.Welcome.Embed.Message.replace(/%joined_user%/g, member.user)
)
.setTimestamp();
const thumb = getWelcomeThumbnail(client, member, msgconfig.Welcome.Embed.Icon);
if (thumb) WelcomeEmbed.setThumbnail(thumb);
if (msgconfig.Welcome.Embed.Image?.Enabled && msgconfig.Welcome.Embed.Image.URL) {
WelcomeEmbed.setImage(msgconfig.Welcome.Embed.Image.URL);
}
const components = [];
if (
msgconfig.Welcome.Embed.Buttons?.Enabled &&
Array.isArray(msgconfig.Welcome.Embed.Buttons.List)
) {
const row = new ActionRowBuilder();
msgconfig.Welcome.Embed.Buttons.List.slice(0, 5).forEach(b => {
const btn = new ButtonBuilder()
.setLabel(b.label)
.setStyle(ButtonStyle[b.style])
.setEmoji(b.emoji || null);
if (b.style === "Link") btn.setURL(b.url);
else if (b.customId) btn.setCustomId(b.customId);
row.addComponents(btn);
});
components.push(row);
}
await WelcomeChannel.send({ embeds: [WelcomeEmbed], components });
} else if (style === "modern") {
const components = buildModernComponents(client, member);
if (components.length > 0) {
await WelcomeChannel.send({
components,
flags: MessageFlags.IsComponentsV2
});
}
}
if (supportbot.Roles.AutoRole.Enabled) {
const role =
member.guild.roles.cache.get(supportbot.Roles.AutoRole.Role) ||
member.guild.roles.cache.find(r => r.name === supportbot.Roles.AutoRole.Role);
if (role) await member.roles.add(role);
}
console.log("\u001b[32m", "[+]", "\u001b[33m", `${member.user.username} joined the server!`);
});
+40
View File
@@ -0,0 +1,40 @@
const { EmbedBuilder } = require("discord.js");
const fs = require("fs");
const yaml = require("js-yaml");
const Event = require("../Structures/Event.js");
const supportbot = require("../Structures/ConfigStore").supportbot;
const msgconfig = require("../Structures/ConfigStore").messages;
module.exports = new Event("guildMemberRemove", async (client, member) => {
if (!supportbot.Leave || !supportbot.Leave.Enabled) return;
const leaveChannel =
member.guild.channels.cache.get(supportbot.Leave.Channel) ||
member.guild.channels.cache.find(c => c.name === supportbot.Leave.Channel);
if (!leaveChannel) return;
const leaveEmbed = new EmbedBuilder()
.setColor(msgconfig.Leave.Embed.Colour)
.setTitle(msgconfig.Leave.Embed.Title)
.setDescription(
msgconfig.Leave.Embed.Message.replace(/%left_user%/g, member.user.toString())
)
.setTimestamp();
if (msgconfig.Leave.Embed.Thumbnail === "BOT") {
leaveEmbed.setThumbnail(client.user.displayAvatarURL());
} else if (msgconfig.Leave.Embed.Thumbnail === "USER") {
leaveEmbed.setThumbnail(member.user.displayAvatarURL());
}
if (msgconfig.Leave.Embed.ImageEnabled) {
leaveEmbed.setImage(msgconfig.Leave.Embed.ImageURL);
}
await leaveChannel.send({ embeds: [leaveEmbed] });
console.log(`[-] ${member.user.tag} left the server`);
});
File diff suppressed because it is too large. Load diff
+664
View File
@@ -0,0 +1,664 @@
const Discord = require("discord.js");
const axios = require("axios");
const supportbotai = require("../Structures/ConfigStore").supportbotAi;
const supportbot = require("../Structures/ConfigStore").supportbot;
const Event = require("../Structures/Event.js");
const AIDatabase = require("../Structures/AIDatabase.js");
const db = require("../Structures/Database.js");
const openai = require("./Models/openai.js");
const groq = require("./Models/groq.js");
const claude = require("./Models/claude.js");
const PASTEBIN_API_KEY = supportbotai.General.PastebinAPI_Key;
const PASTEBIN_API_URL = supportbotai.General.PastebinAPI_URL;
const CHANNELS = [supportbotai.Channels.AIChannel].filter(Boolean);
async function createPaste(content) {
try {
if (!PASTEBIN_API_KEY || !PASTEBIN_API_URL) return null;
const response = await axios.post(
PASTEBIN_API_URL,
new URLSearchParams({
api_dev_key: PASTEBIN_API_KEY,
api_option: "paste",
api_paste_code: content,
api_paste_private: 1,
api_paste_expire_date: "1D",
api_paste_format: "text",
}),
);
return response.data;
} catch (error) {
console.error("Pastebin Error:", error);
return null;
}
}
function getProvider() {
const provider = String(
supportbotai.General.Provider || "openai",
).toLowerCase();
switch (provider) {
case "openai":
return openai;
case "groq":
return groq;
case "claude":
return claude;
default:
throw new Error(`Unsupported AI provider: ${provider}`);
}
}
function formatTicketQuestionAnswers(questionAnswers) {
if (!Array.isArray(questionAnswers) || questionAnswers.length === 0) {
return "No ticket question answers were saved.";
}
return questionAnswers
.map((item, index) => {
const question = item?.question || `Question ${index + 1}`;
const answer = item?.answer || "No answer provided";
return `${index + 1}. ${question}\nAnswer: ${answer}`;
})
.join("\n\n");
}
function buildTicketContext(ticket, message) {
if (!ticket) return "";
return `
Ticket Information:
Ticket ID: ${ticket.ticket_id || ticket.id || "Unknown"}
Subject: ${ticket.subject || ticket.reason || "No subject provided"}
Department: ${ticket.department || "Not set"}
Priority: ${ticket.priority || "Not set"}
Created By: ${ticket.user_id || ticket.user || "Unknown"}
Current User: ${message?.author?.tag || "Unknown"} (${message?.author?.id || "Unknown"})
Ticket Question Answers:
${formatTicketQuestionAnswers(ticket.questionAnswers)}
Use this information when helping the user with their ticket.
Do not invent missing ticket details.
`;
}
function buildSystemPrompt(message) {
const sections = [];
if (Array.isArray(supportbotai.CustomInstructions)) {
sections.push(...supportbotai.CustomInstructions);
}
sections.push(
`I am currently powered by the ${supportbotai.General.Provider || "Unknown"} provider, running the ${supportbotai.General.Model || "Unknown"} model.`,
);
sections.push(
`The current guild is: ${message.guild?.name || "Unknown Guild"}`,
);
sections.push(
`The current channel is: #${message.channel?.name || "unknown-channel"}`,
);
return sections.join("\n- ").replace(/^/, "- ");
}
function buildConversationFromMemory(currentMessage) {
const memoryConfig = supportbotai.Memory || {};
const memoryEnabled = memoryConfig.Enabled !== false;
const maxRecentMessages = memoryConfig.MaxRecentMessages || 15;
const memoryScope = String(memoryConfig.Scope || "guild").toLowerCase();
const conversation = [
{
role: "system",
content: buildSystemPrompt(currentMessage),
},
];
if (!memoryEnabled) return conversation;
const recentMessages =
typeof AIDatabase.getRecentMessagesByScope === "function"
? AIDatabase.getRecentMessagesByScope({
guildId: currentMessage.guild?.id || null,
channelId: currentMessage.channel?.id || null,
limit: maxRecentMessages,
scope: memoryScope,
})
: AIDatabase.getRecentMessages(
currentMessage.channel?.id,
maxRecentMessages,
);
const latestSummary =
memoryConfig.StoreSummaries !== false
? typeof AIDatabase.getLatestSummaryByScope === "function"
? AIDatabase.getLatestSummaryByScope({
guildId: currentMessage.guild?.id || null,
channelId: currentMessage.channel?.id || null,
scope: memoryScope,
})
: AIDatabase.getLatestSummary(currentMessage.channel?.id)
: null;
const facts =
memoryConfig.StoreFacts !== false
? typeof AIDatabase.getFactsByScope === "function"
? AIDatabase.getFactsByScope({
guildId: currentMessage.guild?.id || null,
channelId: currentMessage.channel?.id || null,
scope: memoryScope,
})
: AIDatabase.getFacts(currentMessage.channel?.id)
: [];
const userFacts =
memoryConfig.StoreFacts !== false &&
typeof AIDatabase.getUserFactsByScope === "function"
? AIDatabase.getUserFactsByScope({
guildId: currentMessage.guild?.id || null,
userId: currentMessage.author?.id || null,
scope: memoryScope,
})
: memoryConfig.StoreFacts !== false &&
typeof AIDatabase.getUserFacts === "function"
? AIDatabase.getUserFacts(
currentMessage.guild?.id,
currentMessage.author?.id,
)
: [];
const knowledgeBase = typeof AIDatabase.getKnowledge === "function"
? AIDatabase.getKnowledge(25)
: [];
if (latestSummary?.summary) {
conversation.push({
role: "system",
content: `Shared memory summary (${memoryScope} scope):\n${latestSummary.summary}`,
});
}
if (facts.length > 0) {
const factText = facts
.map(
(fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`,
)
.join("\n");
conversation.push({
role: "system",
content: `Known shared facts (${memoryScope} scope):\n${factText}`,
});
}
if (userFacts.length > 0) {
const userFactText = userFacts
.map(
(fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`,
)
.join("\n");
conversation.push({
role: "system",
content: `Known facts about this user across chats:\n${userFactText}`,
});
}
if (knowledgeBase.length > 0) {
const knowledgeText = knowledgeBase
.map((k) => `- ${k.content}`)
.join("\n");
conversation.push({
role: "system",
content: `Server Knowledge Base (use this to answer questions):\n${knowledgeText}`,
});
}
for (const msg of recentMessages) {
conversation.push({
role: msg.role === "assistant" ? "assistant" : "user",
content: msg.content,
});
}
return conversation;
}
function getTicketContextForMessage(message) {
if (!message?.channel) return null;
if (typeof db.getTicketByAIChannel === "function") {
const aiTicket = db.getTicketByAIChannel(message.channel.id);
if (aiTicket) return aiTicket;
}
if (typeof db.getTicket === "function") {
const directTicket = db.getTicket(message.channel.id);
if (directTicket) return directTicket;
}
if (message.channel.isThread() && typeof db.getTicket === "function") {
const parentTicket = db.getTicket(message.channel.parentId);
if (parentTicket) return parentTicket;
}
return null;
}
function getAIMessageType() {
return String(supportbotai.Style?.MessageType || "embed").toLowerCase();
}
function shouldSuggestTicket(message, ticket) {
if (supportbotai.Features?.SuggestTickets !== true) return false;
if (ticket) return false;
if (message.channel.isThread()) return false;
return CHANNELS.includes(message.channel.id);
}
function getSuggestTicketText() {
return (
supportbotai.Messages?.SuggestTicketText ||
"-# Need more help? React with 🎫 to open a ticket."
);
}
async function sendAIResponse({
client,
message,
replyContent,
suggestTicket = false,
}) {
const messageType = getAIMessageType();
const ticketHint = suggestTicket ? `\n\n${getSuggestTicketText()}` : "";
if (messageType === "text") {
return message.reply({
content: `${replyContent}${ticketHint}`,
});
}
if (messageType === "components_v2") {
const {
ContainerBuilder,
TextDisplayBuilder,
SeparatorBuilder,
MessageFlags,
} = Discord;
const container = new ContainerBuilder();
if (supportbot.Embed?.Colours?.General) {
container.setAccentColor(
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
);
}
const title = new TextDisplayBuilder().setContent(
`**${supportbotai.General?.Name || "SupportBot AI"}**`,
);
const body = new TextDisplayBuilder().setContent(replyContent);
container.addTextDisplayComponents(title);
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
container.addTextDisplayComponents(body);
if (suggestTicket) {
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(getSuggestTicketText()),
);
}
return message.reply({
flags: MessageFlags.IsComponentsV2,
components: [container],
});
}
const embed = new Discord.EmbedBuilder()
.setAuthor({
name: supportbotai.General.Name,
iconURL: client.user.displayAvatarURL(),
})
.setDescription(`${replyContent}${ticketHint}`)
.setColor(supportbotai.Embed.Color)
.setFooter({
text: supportbotai.Embed.Footer,
iconURL: message.author.displayAvatarURL(),
})
.setTimestamp();
return message.reply({ embeds: [embed] });
}
function rememberUserFacts(message) {
if (!message?.guild || !message?.author || !message?.content) return;
const content = message.content.trim();
const nameMatch = content.match(/^my name is\s+(.+)$/i);
if (nameMatch && typeof AIDatabase.saveUserFact === "function") {
AIDatabase.saveUserFact({
guildId: message.guild.id,
userId: message.author.id,
factType: "profile",
factKey: "name",
factValue: nameMatch[1].trim(),
});
}
const nicknameMatch = content.match(/^i(?:'| a)?m\s+(.+)$/i);
if (
nicknameMatch &&
nicknameMatch[1] &&
nicknameMatch[1].length <= 32 &&
typeof AIDatabase.saveUserFact === "function"
) {
AIDatabase.saveUserFact({
guildId: message.guild.id,
userId: message.author.id,
factType: "profile",
factKey: "display_name",
factValue: nicknameMatch[1].trim(),
});
}
const usesMatch = content.match(/^i use\s+(.+)$/i);
if (usesMatch && typeof AIDatabase.saveUserFact === "function") {
AIDatabase.saveUserFact({
guildId: message.guild.id,
userId: message.author.id,
factType: "preference",
factKey: "uses",
factValue: usesMatch[1].trim(),
});
}
}
module.exports = new Event("messageCreate", async (client, message) => {
if (message.guild && typeof db.getTicket === "function") {
const activeTicket = db.getTicket(message.channel.id);
if (activeTicket && global.apiServer) {
global.apiServer.broadcast("ticket_message", {
ticketId: message.channel.id,
message: {
id: message.id,
author: {
id: message.author.id,
username: message.author.username,
globalName: message.author.globalName || message.author.username,
avatar: message.author.displayAvatarURL({ extension: "png", size: 64 }),
bot: message.author.bot
},
content: (() => {
if (message.content && message.content.trim()) return message.content;
if (Array.isArray(message.components) && message.components.length > 0) {
const texts = [];
for (const comp of message.components) {
const data = typeof comp.toJSON === "function" ? comp.toJSON() : comp;
if (data.content) texts.push(data.content);
if (Array.isArray(data.components)) {
for (const child of data.components) {
const childData = typeof child.toJSON === "function" ? child.toJSON() : child;
if (childData.content) texts.push(childData.content);
if (childData.label && (childData.type === 10 || childData.type === "TEXT_DISPLAY")) {
texts.push(childData.label);
}
}
}
}
if (texts.length > 0) return texts.join("\n");
}
return message.content || "";
})(),
containerColor: (() => {
if (Array.isArray(message.components)) {
for (const comp of message.components) {
const data = typeof comp.toJSON === "function" ? comp.toJSON() : comp;
if (data.accent_color) {
return `#${data.accent_color.toString(16).padStart(6, "0")}`;
}
}
}
return null;
})(),
embeds: (message.embeds || []).map(e => ({
title: e.title || null,
description: e.description || null,
color: e.color ? `#${e.color.toString(16).padStart(6, "0")}` : "#5865F2",
author: e.author ? { name: e.author.name, iconURL: e.author.iconURL } : null,
fields: (e.fields || []).map(f => ({ name: f.name, value: f.value, inline: f.inline })),
footer: e.footer ? { text: e.footer.text, iconURL: e.footer.iconURL } : null,
thumbnail: e.thumbnail?.url ? { url: e.thumbnail.url } : null,
image: e.image?.url ? { url: e.image.url } : null,
timestamp: e.timestamp || null
})),
components: (message.components || []).map(c => {
const data = typeof c.toJSON === "function" ? c.toJSON() : c;
return {
type: data.type,
components: (data.components || []).map(sub => {
const subData = typeof sub.toJSON === "function" ? sub.toJSON() : sub;
return {
type: subData.type,
label: subData.label || subData.content || null,
style: subData.style || null,
url: subData.url || null,
customId: subData.custom_id || subData.customId || null,
emoji: subData.emoji ? (subData.emoji.name || subData.emoji.id) : null
};
})
};
}),
attachments: Array.from((message.attachments || []).values()).map(a => ({
id: a.id,
name: a.name,
url: a.url,
contentType: a.contentType
})),
createdAt: message.createdTimestamp
}
});
}
}
if (message.author.bot || !message.guild) return;
let shouldRespond =
CHANNELS.includes(message.channelId) ||
message.mentions.users.has(client.user.id);
if (!shouldRespond && message.channel.isThread()) {
let ticket = null;
if (typeof db.getTicketByAIChannel === "function") {
ticket = db.getTicketByAIChannel(message.channel.id);
}
if (!ticket && typeof db.getTicket === "function") {
ticket = db.getTicket(message.channel.id);
}
if (
!ticket &&
message.channel.parentId &&
typeof db.getTicket === "function"
) {
ticket = db.getTicket(message.channel.parentId);
}
if (
ticket &&
ticket.aiModeEnabled &&
ticket.aiChannelId === message.channel.id
) {
shouldRespond = true;
}
}
if (!shouldRespond) return;
if (!supportbotai.Enabled) {
const embed = new Discord.EmbedBuilder()
.setAuthor({
name: supportbotai.General.Name,
iconURL: client.user.displayAvatarURL(),
})
.setDescription(
supportbotai.Messages?.AIDisabled || "AI is currently disabled.",
)
.setColor(supportbot.Embed.Colours.Error)
.setFooter({
text: supportbotai.Embed.Footer,
iconURL: message.author.displayAvatarURL(),
});
return message.reply({ embeds: [embed] });
}
const typingInterval = setInterval(() => {
message.channel.sendTyping().catch(() => { });
}, 5000);
try {
const memoryConfig = supportbotai.Memory || {};
const memoryEnabled = memoryConfig.Enabled !== false;
if (memoryEnabled) {
AIDatabase.addMessage({
guildId: message.guild.id,
channelId: message.channel.id,
userId: message.author.id,
role: "user",
content: message.content,
});
rememberUserFacts(message);
}
const ticket = getTicketContextForMessage(message);
const conversation = buildConversationFromMemory(message);
if (ticket) {
conversation.push({
role: "system",
content: buildTicketContext(ticket, message),
});
}
const provider = getProvider();
const response = await provider.chat({
messages: conversation,
temperature: supportbotai.General.Temperature || 0.4,
maxTokens: supportbotai.General.Tokens || 800,
});
clearInterval(typingInterval);
if (!response?.content) {
const embed = new Discord.EmbedBuilder()
.setDescription(
supportbotai.Messages?.ErrorResponse ||
"I couldn't generate a response.",
)
.setColor(supportbotai.Embed.Color);
return message.reply({ embeds: [embed] });
}
const replyContent = response.content;
if (memoryEnabled) {
AIDatabase.addMessage({
guildId: message.guild.id,
channelId: message.channel.id,
userId: client.user.id,
role: "assistant",
content: replyContent,
});
}
if (replyContent.length > 4000) {
const pasteLink = await createPaste(replyContent);
if (pasteLink) {
const embed = new Discord.EmbedBuilder()
.setDescription(
(
supportbotai.Messages?.MessageTooLong ||
"My response was too long, so I uploaded it here: {pastebin}"
).replace("{pastebin}", pasteLink),
)
.setColor(supportbotai.Embed.Color);
return message.reply({ embeds: [embed] });
}
return message.reply({
content: replyContent.slice(0, 1900),
});
}
const suggestTicket = shouldSuggestTicket(message, ticket);
const sentMessage = await sendAIResponse({
client,
message,
replyContent,
suggestTicket: false,
});
if (
shouldSuggestTicket(message, ticket) &&
supportbotai.Features?.SuggestTickets === true
) {
const hintMessage = await message.channel.send(
supportbotai.Messages?.SuggestTicketText ||
"Need more help? React with 🎫 to open a ticket.",
);
await hintMessage
.react(supportbotai.Messages?.SuggestTicketReaction || "🎫")
.catch(() => { });
setTimeout(
() => {
hintMessage.delete().catch(() => { });
},
5 * 60 * 1000,
);
}
} catch (error) {
clearInterval(typingInterval);
console.error("AI Error:", error);
const embed = new Discord.EmbedBuilder()
.setDescription(
supportbotai.Messages?.AIError ||
supportbotai.Messages?.ErrorResponse ||
"There was an error contacting the AI provider.",
)
.setColor(supportbot.Embed.Colours.Error);
message.reply({ embeds: [embed] }).catch(() => { });
}
});
+36
View File
@@ -0,0 +1,36 @@
const { EmbedBuilder } = require('discord.js');
const fs = require("fs");
const yaml = require("js-yaml");
const Event = require("../Structures/Event.js");
const supportbot = require("../Structures/ConfigStore").supportbot;
const msgconfig = require("../Structures/ConfigStore").messages;
module.exports = new Event("messageDelete", async (client, message) => {
if (!message.guild) return;
const author = message.author || (message.partial ? await message.fetch().then(m => m.author).catch(() => null) : null);
if (!author || author.bot) return;
const content = message.content || (message.attachments.size > 0 ? "Contains attachment(s)" : "*No content*");
const deleteLogChannel = message.guild.channels.cache.get(supportbot.MessageDelete.Channel) ||
message.guild.channels.cache.find(c => c.name === supportbot.MessageDelete.Channel);
const deletedMessageEmbed = new EmbedBuilder()
.setColor(supportbot.MessageDelete.Colour)
.setTitle("Message Deleted")
.setDescription(`> **Channel:** <#${message.channel.id}>\n> **Message ID:** ${message.id}\n> **Message author:** ${author.tag} (${author.id})\n> **Message Created:** <t:${Math.floor(message.createdTimestamp / 1000)}:F>`)
.addFields({ name: "Deleted Content", value: content, inline: false })
.setThumbnail(author.displayAvatarURL())
.setTimestamp();
if (message.attachments.size > 0) {
const attachment = message.attachments.first();
deletedMessageEmbed.addFields({ name: "Attachment", value: `[${attachment.name}](${attachment.proxyURL})` });
}
if (deleteLogChannel) {
deleteLogChannel.send({ embeds: [deletedMessageEmbed] });
}
});
+63 -35
View File
@@ -1,40 +1,68 @@
// SupportBot | Emerald Services
// Ready Event
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const cmdconfig = yaml.load(fs.readFileSync('./Data/commands.yml', 'utf8'));
const panelconfig = yaml.load(fs.readFileSync('./Data/ticket-panel.yml', 'utf8'));
const supportbot = require("../Structures/ConfigStore").supportbot;
const supportbotai = require("../Structures/ConfigStore").supportbotAi;
const cmdconfig = require("../Structures/ConfigStore").commands;
const Event = require("../Structures/Event.js");
module.exports = new Event("messageReactionAdd", async (client) => {
module.exports = new Event(
"messageReactionAdd",
async (client, reaction, user) => {
try {
if (user.bot) return;
let message = {
guild: reaction.message.channel.guild,
author: user,
content: "N/A",
channel: reaction.message.channel
if (reaction.partial) {
await reaction.fetch().catch(() => null);
}
const message = reaction.message;
if (!message || !message.guild) return;
const reactionEmoji = reaction.emoji?.name || reaction.emoji?.id || null;
const expectedReaction =
supportbotai.Messages?.SuggestTicketReaction || "🎫";
if (reactionEmoji !== expectedReaction) return;
if (supportbotai.Features?.SuggestTickets !== true) return;
if (message.author?.id !== client.user.id) return;
const expectedText =
supportbotai.Messages?.SuggestTicketText ||
"Need more help? React with 🎫 to open a ticket.";
if ((message.content || "").trim() !== expectedText.trim()) return;
await message.delete().catch(() => {});
const cmd = client.commands.get(cmdconfig.OpenTicket.Command);
if (!cmd) {
console.error("Open ticket command not found.");
return;
}
const member = await message.guild.members
.fetch(user.id)
.catch(() => null);
if (!member) return;
const fakeInteraction = {
user,
member,
guild: message.guild,
channel: message.channel,
client,
replied: true,
deferred: false,
reason: `Opened from AI suggestion in #${message.channel.name}`,
options: {
getString: () => null,
},
reply: async () => null,
};
await cmd.run(fakeInteraction);
} catch (error) {
console.error("Error handling AI ticket reaction:", error);
}
if (panelconfig.TicketPanel) {
if (reaction.message.id === panelconfig.TicketPanelID) {
if (reaction.emoji.name == panelconfig.ButtonEmoji || reaction.emoji.id == panelconfig.ButtonEmoji) {
await reaction.users.remove(user.id)
try {
const cmd = client.commands.get(cmdconfig.OpenTicket);
if(!cmd) return;
cmd.run(message, message.content);
} catch (error) {
console.error(error);
}
}
}
}
});
},
);
+57
View File
@@ -0,0 +1,57 @@
const { EmbedBuilder } = require('discord.js');
const fs = require("fs");
const yaml = require("js-yaml");
const Event = require("../Structures/Event.js");
// Load the configuration files
const supportbot = require("../Structures/ConfigStore").supportbot;
const msgconfig = require("../Structures/ConfigStore").messages;
module.exports = new Event("messageUpdate", async (client, oldMessage, newMessage) => {
// Check if the message is from a guild and not from a bot
if (!newMessage.guild || newMessage.author.bot) return;
// Fetch old message if it's not cached (important for message content)
if (!oldMessage.content && oldMessage.partial) {
try {
oldMessage = await oldMessage.fetch();
} catch (err) {
console.error('Error fetching old message:', err);
return;
}
}
// Fallback if content is missing
const oldMessageContent = oldMessage.content || (oldMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*');
const newMessageContent = newMessage.content || (newMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*');
// Get the update log channel either by ID or by name
const updateLogChannel = newMessage.guild.channels.cache.get(supportbot.MessageUpdate.Channel) ||
newMessage.guild.channels.cache.find(channel => channel.name === supportbot.MessageUpdate.Channel);
// Create the embed for the updated message
const updatedMessageEmbed = new EmbedBuilder()
.setColor(supportbot.MessageUpdate.Colour)
.setTitle("Message Updated")
.setDescription(`> **Channel:** <#${newMessage.channel.id}>\n> **Message ID:** [${newMessage.id}](https://discord.com/channels/${newMessage.guild.id}/${newMessage.channel.id}/${newMessage.id})\n> **Message author:** ${newMessage.author.tag} (${newMessage.author.id})\n> **Message Created:** <t:${Math.floor(oldMessage.createdTimestamp / 1000)}:F>`)
.addFields(
{ name: 'Old Message', value: oldMessageContent, inline: false },
{ name: 'New Message', value: newMessageContent, inline: false }
)
.setThumbnail(newMessage.author.displayAvatarURL())
.setTimestamp();
// If the new message contains an attachment, add it to the embed
if (newMessage.attachments.size > 0) {
const attachment = newMessage.attachments.first();
updatedMessageEmbed.addFields({ name: 'Attachment', value: `[${attachment.name}](${attachment.proxyURL})` });
}
// Send the embed to the update log channel if the channel is found
if (updateLogChannel) {
updateLogChannel.send({
embeds: [updatedMessageEmbed]
});
}
});
+8
View File
@@ -0,0 +1,8 @@
const Event = require("../Structures/Event.js");
const { runBotReady } = require("../Structures/BotStartup.js");
module.exports = new Event("clientReady", async (client) => {
// restartDiscordBot runs startup manually — clientReady often does not re-fire anyway
if (client.__explicitStartup) return;
await runBotReady(client, { clearConsole: true, logCommands: false });
});
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Tanner Nielsen
Copyright (c) 2026 Emerald Services
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+85
View File
@@ -0,0 +1,85 @@
<h1 align="center">
<br>
<a href="https://github.com/Emerald-Services/SupportBot"><img src="https://cdn.1tzemerald.space/Emerald%20Services/SBThreadHeader.png"></a>
<br>
SupportBot
<br>
</h1>
<div align="center">
<a href="https://opensource.org/licenses/MIT">
<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="shield.png">
</a>
<a href="https://github.com/discordjs">
<img src="https://img.shields.io/badge/DISCORD.JS%20-%20v14-blue?style=for-the-badge" alt="shield.png">
</a>
<a href="https://github.com/nodejs/node">
<img src="https://img.shields.io/badge/NODE.JS%20-%20v20.x-green?style=for-the-badge" alt="shield.png">
</a>
<a href="https://github.com/Emerald-Services/SupportBot/releases">
<img src="https://img.shields.io/github/downloads/Emerald-Services/SupportBot/total?style=for-the-badge" alt="shield.png">
</a>
## Supported Versions
| SupportBot | Node.JS | Discord.JS | Supported |
|------------|---------|------------|------------|
| 26.x | 22.x | 14.x | ✅ Yes |
| 8.x | 22.x | 14.x | ❌ No |
| 7.x | 16.x | 13.x | ❌ No |
| Older | 10.x | 13.x | ❌ No |
</div>
<p align="center">
<a href="https://emeraldsrv.dev/">Website</a>
•
<a href="https://github.com/Emerald-Services/SupportBot/wiki">Wiki</a>
<br>
<a href="#about">About</a>
•
<a href="#support">Support</a>
•
<a href="#license">License</a>
•
<a href="https://github.com/Emerald-Services/SupportBot/wiki/Third-Party-Developer">Third-Party</a>
•
<a href="#contributors">Contributors</a>
</p>
## About
SupportBot is an Open-Source discord bot built with [discord.js](https://github.com/discordjs/discord.js). It's primarily focused to help servers organise and manage their support system in a unique way with full customisation to match your brand.
## Support
<a href="https://emeraldsrv.dev/discord">
<img alt="Discord" src="https://img.shields.io/discord/597779408295821323?logo=discord&logoColor=white&style=for-the-badge">
</a>
## License
Released under the [MIT](https://opensource.org/licenses/MIT) license.
## Third-Party
- [Verified Addons](https://emeraldsrv.dev)
- [BuiltByBit](https://builtbybit.com/resources/supportbot-advanced-ticket-bot.8940/)
- [Voxel](https://polymart.org/resource/supportbot-1-discord-ticket-bot.518)
## Contributors
<a href="https://github.com/Emerald-Services/SupportBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Emerald-Services/SupportBot">
</a>
## Hosting
You can use our [Pterodactyl egg](https://github.com/Emerald-Services/Pterodactyl-Egg) to host SupportBot on your own panel.
+167
View File
@@ -0,0 +1,167 @@
const fs = require('fs');
const path = require('path');
const DB_PATH = path.join(__dirname, '../Data/ai-memory.json');
function load() {
try {
const raw = fs.readFileSync(DB_PATH, 'utf8');
return JSON.parse(raw);
} catch (e) {
return {
ai_conversations: [],
ai_summaries: [],
ai_facts: [],
ai_knowledge: [],
ai_sessions: [],
};
}
}
function save(state) {
try {
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
fs.writeFileSync(DB_PATH, JSON.stringify(state, null, 2), 'utf8');
} catch (e) {
console.error('Failed to persist ai-memory:', e);
}
}
const state = load();
function nextId(arr) {
return arr.length === 0 ? 1 : Math.max(...arr.map(a => a.id || 0)) + 1;
}
module.exports = {
saveUserFact({ guildId, userId, factType, factKey, factValue }) {
const existing = state.ai_facts.find(f => f.guild_id === (guildId || null) && f.user_id === (userId || null) && f.fact_type === factType && f.fact_key === factKey);
if (existing) {
existing.fact_value = factValue;
existing.updated_at = Date.now();
save(state);
return;
}
const id = nextId(state.ai_facts);
state.ai_facts.push({ id, guild_id: guildId || null, channel_id: null, user_id: userId || null, fact_type: factType, fact_key: factKey, fact_value: factValue, created_at: Date.now(), updated_at: Date.now() });
save(state);
},
getUserFacts(guildId, userId) {
return state.ai_facts.filter(f => f.guild_id === (guildId || null) && f.user_id === (userId || null)).sort((a,b) => b.updated_at - a.updated_at);
},
getUserFactsByScope({ guildId = null, userId = null, scope = "guild" }) {
if (!userId) return [];
if (scope === "global") {
return state.ai_facts.filter(f => f.user_id === (userId || null)).sort((a,b) => b.updated_at - a.updated_at);
}
return state.ai_facts.filter(f => f.guild_id === (guildId || null) && f.user_id === (userId || null)).sort((a,b) => b.updated_at - a.updated_at);
},
addMessage({ guildId, channelId, userId, role, content }) {
const id = nextId(state.ai_conversations);
state.ai_conversations.push({ id, guild_id: guildId || null, channel_id: channelId || null, user_id: userId || null, role, content, created_at: Date.now() });
save(state);
},
getRecentMessages(channelId, limit = 15) {
const msgs = state.ai_conversations.filter(m => m.channel_id === channelId).sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
return msgs;
},
getRecentMessagesByScope({ guildId = null, channelId = null, limit = 15, scope = "guild" }) {
if (scope === "channel") {
return state.ai_conversations.filter(m => m.channel_id === (channelId || null)).sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
}
if (scope === "global") {
return state.ai_conversations.sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
}
return state.ai_conversations.filter(m => m.guild_id === (guildId || null)).sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
},
saveSummary({ guildId, channelId, summary }) {
const existing = state.ai_summaries.find(s => s.guild_id === (guildId || null) && s.channel_id === (channelId || null));
if (existing) {
existing.summary = summary;
existing.updated_at = Date.now();
save(state);
return;
}
const id = nextId(state.ai_summaries);
state.ai_summaries.push({ id, guild_id: guildId || null, channel_id: channelId || null, summary, created_at: Date.now(), updated_at: Date.now() });
save(state);
},
getLatestSummary(channelId) {
const arr = state.ai_summaries.filter(s => s.channel_id === channelId).sort((a,b) => b.updated_at - a.updated_at);
return arr.length ? arr[0] : null;
},
getLatestSummaryByScope({ guildId = null, channelId = null, scope = "guild" }) {
if (scope === "channel") {
const arr = state.ai_summaries.filter(s => s.channel_id === (channelId || null)).sort((a,b) => b.updated_at - a.updated_at);
return arr.length ? arr[0] : null;
}
if (scope === "global") {
const arr = state.ai_summaries.sort((a,b) => b.updated_at - a.updated_at);
return arr.length ? arr[0] : null;
}
const arr = state.ai_summaries.filter(s => s.guild_id === (guildId || null)).sort((a,b) => b.updated_at - a.updated_at);
return arr.length ? arr[0] : null;
},
saveFact({ guildId, channelId, factType, factKey, factValue }) {
const existing = state.ai_facts.find(f => f.guild_id === (guildId || null) && f.channel_id === (channelId || null) && f.fact_type === factType && f.fact_key === factKey);
if (existing) {
existing.fact_value = factValue;
existing.updated_at = Date.now();
save(state);
return;
}
const id = nextId(state.ai_facts);
state.ai_facts.push({ id, guild_id: guildId || null, channel_id: channelId || null, user_id: null, fact_type: factType, fact_key: factKey, fact_value: factValue, created_at: Date.now(), updated_at: Date.now() });
save(state);
},
getFacts(channelId) {
return state.ai_facts.filter(f => f.channel_id === channelId).sort((a,b) => b.updated_at - a.updated_at);
},
getFactsByScope({ guildId = null, channelId = null, scope = "guild" }) {
if (scope === "channel") {
return state.ai_facts.filter(f => f.channel_id === (channelId || null) && f.user_id == null).sort((a,b) => b.updated_at - a.updated_at);
}
if (scope === "global") {
return state.ai_facts.filter(f => f.user_id == null).sort((a,b) => b.updated_at - a.updated_at);
}
return state.ai_facts.filter(f => f.guild_id === (guildId || null) && f.user_id == null).sort((a,b) => b.updated_at - a.updated_at);
},
addKnowledge({ sourceType, sourceName, title, content }) {
const id = nextId(state.ai_knowledge);
state.ai_knowledge.push({ id, source_type: sourceType, source_name: sourceName, title, content, created_at: Date.now(), updated_at: Date.now() });
save(state);
},
getKnowledge(limit = 25) {
return state.ai_knowledge.sort((a,b) => b.updated_at - a.updated_at).slice(0, limit);
},
clearKnowledge() {
state.ai_knowledge = [];
save(state);
},
clearConversations(channelId) {
if (!channelId) return;
state.ai_conversations = state.ai_conversations.filter(c => c.channel_id !== channelId);
save(state);
},
};
+27
View File
@@ -0,0 +1,27 @@
// SupportBot | Emerald Services
// Addon Structure
class Command {
constructor(options) {
this.name = options.name;
this.description = options.description;
this.permissions = options.permissions;
this.options = options.options || [];
this.run = options.run;
this.defaultPermission = true;
}
// Method to dynamically update options
setOptions(options) {
this.options = options;
}
}
class Event {
constructor(event, run) {
this.event = event;
this.run = run;
}
}
module.exports = { Command, Event };
+185
View File
@@ -0,0 +1,185 @@
const fs = require("fs");
const path = require("path");
const axios = require("axios");
const { withTimeout } = require("./asyncTimeout.js");
const AdmZip = require("adm-zip");
const yaml = require("js-yaml");
const EMERALD_API = "https://emeraldsrv.dev/api/v1";
function getApiKey() {
try {
if (fs.existsSync("./Configs/api.yml")) {
const config = yaml.load(fs.readFileSync("./Configs/api.yml", "utf8"));
return config?.API?.EmeraldAPIKey || "";
}
} catch (e) {
console.error("Failed to read api.yml:", e);
}
return "";
}
function ensureAddonDirs() {
if (!fs.existsSync("./Addons")) {
fs.mkdirSync("./Addons", { recursive: true });
}
if (!fs.existsSync("./Addons/Configs")) {
fs.mkdirSync("./Addons/Configs", { recursive: true });
}
}
async function listCatalogAddons() {
const key = getApiKey();
if (!key) throw new Error("EmeraldAPIKey not configured in api.yml");
const res = await withTimeout(
axios.get(`${EMERALD_API}/resources`, {
headers: { Authorization: `Bearer ${key}` },
timeout: 15_000,
}),
18_000,
"Emerald API request timed out",
);
if (res.data.status !== "success") {
throw new Error(res.data.message || "Failed to fetch catalog");
}
return res.data.data.map(item => ({
id: item.id.toString(),
name: item.title,
slug: item.slug,
description: `Emerald Services Resource - ${item.slug}`,
price: item.price,
is_external: item.is_external,
external_store: item.external_store,
repositoryUrl: item.external_url || `https://emeraldsrv.dev`,
jsFiles: [],
configFiles: []
})).sort((a, b) => a.name.localeCompare(b.name));
}
async function downloadFile(id) {
const key = getApiKey();
const res = await withTimeout(
axios.get(`${EMERALD_API}/download?id=${id}`, {
headers: { Authorization: `Bearer ${key}` },
responseType: "arraybuffer",
timeout: 30_000
}),
35_000,
"Addon file download timed out",
);
return Buffer.from(res.data);
}
function listLocalAddonJs() {
ensureAddonDirs();
return fs
.readdirSync("./Addons")
.filter((f) => f.endsWith(".js") && f !== "addon.js");
}
function listLocalAddonConfigs() {
ensureAddonDirs();
const dir = "./Addons/Configs";
if (!fs.existsSync(dir)) return [];
return fs
.readdirSync(dir)
.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml") || f.endsWith(".json"));
}
function isAddonInstalled(catalogEntry) {
const localJs = listLocalAddonJs();
return localJs.some(f => f.includes(catalogEntry.slug) || f.includes(catalogEntry.id.toString()) || f.includes(catalogEntry.name.replace(/ /g, '')));
}
async function listInstalledAddons(catalog) {
const localJs = listLocalAddonJs();
const installed = [];
for (const file of localJs) {
const name = file.replace(/\.js$/, "");
const match = catalog.find(c => c.slug === name || c.id.toString() === name || c.name.replace(/ /g, '') === name);
installed.push({
id: match ? match.id : name,
name: match ? match.name : name,
jsFiles: [file],
configFiles: listLocalAddonConfigs().filter(f => f.startsWith(name)),
custom: !match
});
}
return installed;
}
async function installAddonFromCatalog(addonId) {
const catalog = await listCatalogAddons();
const addon = catalog.find(c => c.id === addonId.toString());
if (!addon) {
throw new Error(`Addon not found in catalog`);
}
ensureAddonDirs();
const content = await downloadFile(addon.id);
const zip = new AdmZip(content);
zip.extractAllTo("./Addons", true);
return {
addon: addon,
filesWritten: [],
};
}
function readAddonConfig(filename) {
const safe = path.basename(filename);
if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) {
throw new Error("Invalid config filename");
}
const filePath = path.join("./Addons/Configs", safe);
if (!fs.existsSync(filePath)) {
throw new Error("Addon config not found");
}
const raw = fs.readFileSync(filePath, "utf8");
if (safe.endsWith(".json")) {
return { filename: safe, format: "json", data: JSON.parse(raw), raw };
}
const yaml = require("js-yaml");
return { filename: safe, format: "yaml", data: yaml.load(raw) || {}, raw };
}
function writeAddonConfig(filename, data) {
const safe = path.basename(filename);
if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) {
throw new Error("Invalid config filename");
}
ensureAddonDirs();
const filePath = path.join("./Addons/Configs", safe);
let content;
if (safe.endsWith(".json")) {
content = `${JSON.stringify(data, null, 2)}\n`;
} else {
const yaml = require("js-yaml");
content = yaml.dump(data, { lineWidth: 120, noRefs: true });
}
fs.writeFileSync(filePath, content);
return { filename: safe, path: filePath.replace(/\\/g, "/") };
}
module.exports = {
listCatalogAddons,
listInstalledAddons,
isAddonInstalled,
installAddonFromCatalog,
listLocalAddonConfigs,
readAddonConfig,
writeAddonConfig,
};
-24
View File
@@ -1,24 +0,0 @@
// SupportBot | Emerald Services
// Addon Structure
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
function RunFunction(message, args, client) {}
class Addon {
constructor(options) {
this.name = options.name;
this.description = options.description;
this.permission = options.permission;
this.type = ["BOTH", "SLASH", "TEXT"].includes(options.type) ? options.type : "TEXT";
this.slashCommandOptions = options.slashCommandOptions || [];
this.run = options.run;
}
}
module.exports = Addon;
+6
View File
@@ -0,0 +1,6 @@
function getBotInviteUrl(clientId) {
if (!clientId) return null;
return `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=8&scope=bot%20applications.commands`;
}
module.exports = { getBotInviteUrl };
+150
View File
@@ -0,0 +1,150 @@
const fs = require("fs");
const path = require("path");
const configStore = require("./ConfigStore.js");
const { getPrimaryGuild } = require("./GuildManager.js");
const {
applyPresence,
runBotReady,
logSlashCommandsRegistered,
} = require("./BotStartup.js");
function walk(dir, ext = ".js", fileList = []) {
if (!fs.existsSync(dir)) return fileList;
for (const file of fs.readdirSync(dir)) {
const full = path.join(dir, file);
if (fs.statSync(full).isDirectory()) {
walk(full, ext, fileList);
} else if (file.endsWith(ext)) {
fileList.push(full.replace(/\\/g, "/"));
}
}
return fileList;
}
async function refreshSlashCommands(client) {
if (!client?.application) return { count: 0 };
const cmdconfig = configStore.commands;
const commandFiles = walk("./Commands");
client.commands.clear();
for (const file of commandFiles) {
const resolved = path.resolve(file);
if (require.cache[resolved]) {
delete require.cache[resolved];
}
}
let count = 0;
for (const file of commandFiles) {
const command = require(path.resolve(file));
const matchingConfig = Object.values(cmdconfig).find(
(entry) =>
entry &&
typeof entry === "object" &&
entry.Command &&
String(entry.Command).toLowerCase() ===
String(command.name).toLowerCase(),
);
if (matchingConfig && matchingConfig.Enabled === false) {
continue;
}
client.commands.set(command.name, command);
count++;
}
const payload = client.commands.map((cmd) => cmd.data ?? cmd);
for (const guild of client.guilds.cache.values()) {
await guild.commands.set(payload);
}
return { count };
}
async function reloadBot(client, options = {}) {
const { fileKey, refreshCommands = true } = options;
let reloaded;
if (fileKey) {
configStore.reloadFile(fileKey);
reloaded = [fileKey];
} else {
reloaded = configStore.reloadBotConfigs();
}
applyPresence(client);
let commands = null;
if (refreshCommands && (!fileKey || fileKey === "commands")) {
commands = await refreshSlashCommands(client);
}
return {
reloaded,
commands,
};
}
/**
* Reload configs from disk, reconnect the Discord session, and refresh presence/commands.
* API and dashboard keep running on the same process.
*/
async function restartDiscordBot(client) {
if (!client) {
const err = new Error("Bot client not available");
err.code = "NO_CLIENT";
throw err;
}
const reloaded = configStore.reloadBotConfigs();
const token = configStore.supportbot?.General?.Token;
if (!token || String(token).trim() === "" || token === "BOT_TOKEN") {
const err = new Error("Invalid or missing bot token in supportbot.yml");
err.code = "INVALID_TOKEN";
throw err;
}
console.log("[BotReload] Restarting Discord bot after config change…");
client.__restarting = true;
if (client.user) {
try {
await client.destroy();
} catch (destroyErr) {
console.warn("[BotReload] destroy:", destroyErr.message);
}
}
client.__explicitStartup = true;
try {
try {
await client.login(token);
} catch (loginErr) {
console.error("[BotReload] login failed:", loginErr.message);
const err = new Error(`Could not start bot: ${loginErr.message}`);
err.code = "LOGIN_FAILED";
throw err;
}
// clientReady does not fire again after destroy()+login() — run startup explicitly
await runBotReady(client, { clearConsole: true, logCommands: false });
const commands = await refreshSlashCommands(client);
logSlashCommandsRegistered(client);
console.log("[BotReload] Bot restarted successfully");
return { restarted: true, reloaded, commands };
} finally {
client.__explicitStartup = false;
client.__restarting = false;
}
}
module.exports = {
applyPresence,
refreshSlashCommands,
reloadBot,
restartDiscordBot,
};
+200
View File
@@ -0,0 +1,200 @@
const Discord = require("discord.js");
const configStore = require("./ConfigStore.js");
const {
getPrimaryGuild,
syncGuildHealth,
} = require("./GuildManager.js");
const ACTIVITY_TYPES = {
PLAYING: Discord.ActivityType.Playing,
WATCHING: Discord.ActivityType.Watching,
LISTENING: Discord.ActivityType.Listening,
COMPETING: Discord.ActivityType.Competing,
STREAMING: Discord.ActivityType.Streaming,
};
function applyPresence(client) {
if (!client?.user) return;
const activity = configStore.supportbot?.Activity;
const typeKey = activity?.Type?.toUpperCase();
const type = ACTIVITY_TYPES[typeKey] ?? Discord.ActivityType.Playing;
const name = activity?.Status || "Online";
const activityPayload = { name, type };
if (typeKey === "STREAMING" && activity?.StreamingURL) {
activityPayload.url = activity.StreamingURL;
}
client.user.setPresence({
activities: [activityPayload],
status: "online",
});
}
const c = {
reset: "\x1b[0m",
gray: "\x1b[90m",
green: "\x1b[32m",
yellow: "\x1b[33m",
bold: "\x1b[1m",
white: "\x1b[37m",
};
function line() {
console.log(
`${c.gray}────────────────────────────────────────────────────────${c.reset}`,
);
}
function section(title) {
console.log(`\n${c.green}${title}${c.reset}`);
console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`);
}
function info(label, value) {
console.log(`${c.gray}${label.padEnd(14)}${c.reset} ${value}`);
}
function status(label, ok, details = "") {
const icon = ok ? `${c.green}✓${c.reset}` : `${c.yellow}!${c.reset}`;
console.log(` ${icon} ${label}${details ? ` (${details})` : ""}`);
}
async function logStartup(client, options = {}) {
const { clearConsole = true } = options;
const supportbot = configStore.supportbot;
if (clearConsole) {
console.clear();
}
const botName = supportbot?.General?.Name || "SupportBot";
const botVersion = supportbot?.SupportBot_Version || "Unknown";
const { getBotInviteUrl } = require("./BotInvite.js");
const inviteUrl = getBotInviteUrl(client.user.id);
line();
console.log(`${c.bold}${c.green}SupportBot${c.reset}`);
console.log(`${c.gray}Emerald Services${c.reset}`);
line();
section("System");
info("Bot Name", botName);
info("Version", botVersion);
info("Node.js", process.version);
info("Discord.js", `v${Discord.version}`);
const db = require("./Database.js");
const dbInfo = db.getDatabaseInfo ? db.getDatabaseInfo() : null;
if (dbInfo) {
const dbDisplay = dbInfo.type === "mysql"
? `${dbInfo.driver} (${dbInfo.host}:${dbInfo.port}/${dbInfo.database})`
: `${dbInfo.driver} (${dbInfo.file})`;
info("Database", dbDisplay);
}
info("Logged in as", client.user.tag);
info("Guilds", `${client.guilds.cache.size}`);
section("Links");
info("Docs", "https://emerald-services.gitbook.io/");
info("Discord", "https://dsc.gg/emerald-dev");
if (configStore.api?.API?.Enabled) {
const port = configStore.api?.API?.Port || 3000;
info("Dashboard", `http://localhost:${port}`);
}
info("Invite", inviteUrl);
console.log("");
}
function checkSetup() {
const { isSetupComplete } = require("./DashboardSetup.js");
if (!isSetupComplete()) {
const port = configStore.api?.API?.Port || 25575;
section("Setup Required");
status("Onboarding setup incomplete", false, `http://localhost:${port}/setup`);
console.log("");
}
}
async function checkConfigs(client) {
const supportbot = configStore.supportbot;
const health = syncGuildHealth(client);
if (health.status !== "ok") {
section("Guild");
status("Guild setup", false, health.message || health.status);
console.log("");
}
const guild = getPrimaryGuild(client);
if (!guild) return;
section("Config Check");
const roles = [
supportbot?.Roles?.StaffMember?.Admin,
supportbot?.Roles?.StaffMember?.Staff,
supportbot?.Roles?.AutoRole?.Role,
].filter(Boolean);
const channels = [supportbot?.Ticket?.TicketHome].filter(Boolean);
const [roleResults, channelResults] = await Promise.all([
Promise.all(roles.map((role) => client.getRole(role, guild))),
Promise.all(channels.map((channel) => client.getChannel(channel, guild))),
]);
const missingRoles = roles.filter((_, i) => !roleResults[i]);
const missingChannels = channels.filter((_, i) => !channelResults[i]);
if (!missingRoles.length && !missingChannels.length) {
status("All configurations validated successfully", true);
} else {
if (missingRoles.length) {
status("Missing roles", false, missingRoles.join(", "));
}
if (missingChannels.length) {
status("Missing channels", false, missingChannels.join(", "));
}
}
console.log("");
}
function logSlashCommandsRegistered(client) {
const guild = getPrimaryGuild(client);
if (!guild) return;
console.log(
`\n${c.green}✓${c.reset} ${c.white}Slash Commands Registered${c.reset} ${c.gray}for ${guild.name}${c.reset}`,
);
}
/**
* Same work as Events/ready.js — safe to call after config reload reconnects.
*/
async function runBotReady(client, options = {}) {
const { clearConsole = true, logCommands = false } = options;
syncGuildHealth(client);
applyPresence(client);
await logStartup(client, { clearConsole });
await checkConfigs(client);
if (logCommands) {
logSlashCommandsRegistered(client);
}
checkSetup();
}
module.exports = {
applyPresence,
logStartup,
checkConfigs,
logSlashCommandsRegistered,
runBotReady,
};
+186 -96
View File
@@ -1,103 +1,193 @@
// SupportBot | Emerald Services
// Client Structure
const fs = require("fs");
const path = require("path");
const Discord = require("discord.js");
const Command = require("./Command.js");
const Event = require("./Event.js");
const intents = new Discord.Intents(32767);
const { GatewayIntentBits, Partials } = require("discord.js");
const { Command, Event } = require("./Addon.js");
const configStore = require("./ConfigStore.js");
const { getPrimaryGuild } = require("./GuildManager.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const supportbot = configStore.supportbot;
const cmdconfig = configStore.commands;
class Client extends Discord.Client {
constructor() {
super({ intents });
const c = {
reset: "\x1b[0m",
gray: "\x1b[90m",
green: "\x1b[32m",
yellow: "\x1b[33m",
cyan: "\x1b[36m",
red: "\x1b[31m",
white: "\x1b[37m",
bold: "\x1b[1m",
};
this.commands = new Discord.Collection();
this.prefix = supportbot.Prefix;
}
start(token) {
const commandFiles = fs.readdirSync("./Commands")
.filter(file => file.endsWith(".js"));
const commands = commandFiles.map(file => require(`../Commands/${file}`));
console.log(`\u001b[34;1m`, '▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬')
commands.forEach(cmd => {
console.log(`\u001b[31;1m`, `${cmd.name}`, `\u001b[32;1m`, 'Loaded');
this.commands.set(cmd.name, cmd);
});
console.log(`\u001b[34;1m`, '▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬')
// Addons
const addonFiles = fs.readdirSync("./Addons")
.filter(file => file.endsWith(".js"));
const addons = addonFiles.map(file => require(`../Addons/${file}`));
console.log(' ')
console.log(`\u001b[34;1m`, '▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬')
addons.forEach(addon => {
console.log(`\u001b[31;1m`, `${addon.name}`, `\u001b[32;1m`, 'Loaded');
this.commands.set(addon.name, addon);
});
console.log(`\u001b[34;1m`, '▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬')
console.log(' ')
// Slash Commands
const slashCommands = commands
.filter(cmd => ["BOTH", "SLASH"].includes(cmd.type))
.map(cmd => ({
name: cmd.name.toLowerCase(),
description: cmd.description,
permissions: [],
options: cmd.slashCommandOptions,
defaultPermission: true
}));
this.removeAllListeners();
this.on("ready", async () => {
const cmds = await this.application.commands.set(slashCommands);
console.log(' ')
console.log(`\u001b[34;1m`, '▬▬▬▬▬▬▬ Slash Command ▬▬▬▬▬▬▬'),
console.log(' ')
cmds.forEach(cmd =>
console.log(`Slash Command ${cmd.name} registered`),
)
})
console.log(' ')
console.log(`\u001b[34;1m`, '▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬')
fs.readdirSync("./Events")
.filter(file => file.endsWith(".js"))
.forEach(file => {
const event = require(`../Events/${file}`);
console.log(`\u001b[31;1m`, `${event.event}`, `\u001b[32;1m`, 'Loaded');
this.on(event.event, event.run.bind(null, this));
});
console.log(`\u001b[34;1m`, '▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬')
console.log(' ')
this.login(token);
}
function line() {
console.log(`${c.gray}────────────────────────────────────────────────────────${c.reset}`);
}
module.exports = Client;
function section(title) {
console.log(`\n${c.green}${title}${c.reset}`);
console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`);
}
function walk(dir, ext = ".js", fileList = []) {
const files = fs.readdirSync(dir);
for (const file of files) {
const full = `${dir}/${file}`;
const stat = fs.statSync(full);
if (stat.isDirectory()) {
walk(full, ext, fileList);
} else if (file.endsWith(ext)) {
fileList.push(full);
}
}
return fileList;
}
class Client extends Discord.Client {
constructor() {
super({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildEmojisAndStickers,
GatewayIntentBits.MessageContent,
],
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
});
this.commands = new Discord.Collection();
}
async getChannel(channel, guild) {
if (!channel) return null;
const channelStr = String(channel).toLowerCase();
return guild.channels.cache.find(
(c) =>
(c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) &&
(c.id === channel || (c.name && c.name.toLowerCase() === channelStr)),
);
}
async getRole(role, guild) {
if (!role) return null;
const roleStr = String(role).toLowerCase();
return guild.roles.cache.find(
(r) => r.id === role || (r.name && r.name.toLowerCase() === roleStr),
);
}
async getCategory(category, guild) {
if (!category) return null;
const catStr = String(category).toLowerCase();
return guild.channels.cache.find(
(c) =>
c.type === Discord.ChannelType.GuildCategory &&
(c.id === category || (c.name && c.name.toLowerCase() === catStr)),
);
}
async start(token) {
const commandFiles = walk("./Commands");
const allCommands = commandFiles.map((file) => ({
file,
command: require(`../${file}`),
}));
const enabledCommands = allCommands.filter(({ command }) => {
const matchingConfig = Object.values(cmdconfig).find(
(entry) =>
entry &&
typeof entry === "object" &&
entry.Command &&
String(entry.Command).toLowerCase() === String(command.name).toLowerCase(),
);
if (!matchingConfig) {
return true;
}
return matchingConfig.Enabled !== false;
});
section("Commands");
enabledCommands.forEach(({ command, file }) => {
console.log(
`${c.green}✓${c.reset} ${c.white}${command.name}${c.reset} ${c.gray}(${path.basename(file)})${c.reset}`,
);
this.commands.set(command.name, command);
});
if (supportbot.General.Addons.Enabled) {
const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js"));
section("Addons");
addonFiles.forEach((file) => {
let addon = require(`../Addons/${file}`);
// Handle single or multiple addons per file
const addonArray = Array.isArray(addon) ? addon : [addon];
addonArray.forEach((item, index) => {
// If name is missing, use filename (handling index for arrays)
if (!item.name) {
item.name = index === 0 ? file.split(".")[0] : `${file.split(".")[0]}_${index}`;
}
console.log(`${c.green}✓${c.reset} ${c.white}${item.name}${c.reset}`);
if (item instanceof Command) {
this.commands.set(item.name, item);
}
if (item.events && Array.isArray(item.events)) {
item.events.forEach((event) => {
if (event instanceof Event) {
this.on(event.event, (...args) => event.run(this, ...args));
}
});
}
// If it's a direct Event instance
if (item instanceof Event) {
this.on(item.event, (...args) => item.run(this, ...args));
}
});
});
}
this.on("clientReady", async () => {
const guild = getPrimaryGuild(this);
if (!guild) return;
await guild.commands.set(this.commands);
const { logSlashCommandsRegistered } = require("./BotStartup.js");
logSlashCommandsRegistered(this);
});
section("Events");
fs.readdirSync("./Events")
.filter((file) => file.endsWith(".js"))
.forEach((file) => {
const event = require(`../Events/${file}`);
console.log(
`${c.green}✓${c.reset} ${c.white}${event.event}${c.reset} ${c.gray}(${file})${c.reset}`,
);
this.on(event.event, (...args) => event.run(this, ...args));
});
console.log("");
if (process.argv[2] !== "test") {
await this.login(token);
} else {
console.log(`${c.red}RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start${c.reset}`);
}
}
}
module.exports = Client;
+9 -19
View File
@@ -1,24 +1,14 @@
// SupportBot | Emerald Services
// Command Structure
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
function RunFunction(message, args, client) {}
class Command {
constructor(options) {
this.name = options.name;
this.description = options.description;
this.permission = options.permission;
this.type = ["BOTH", "SLASH", "TEXT"].includes(options.type) ? options.type : "TEXT";
this.slashCommandOptions = options.slashCommandOptions || [];
this.run = options.run;
}
constructor(options) {
this.name = options.name;
this.description = options.description;
this.permissions = options.permissions || [];
this.options = options.options || [];
this.run = options.run;
this.defaultPermission = true;
}
}
module.exports = Command;
module.exports = Command;
+89
View File
@@ -0,0 +1,89 @@
// Live config objects — reload mutates in place so existing references stay valid.
const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
const CONFIG_DIR = path.join(__dirname, "../Configs");
const FILE_KEYS = [
"supportbot",
"commands",
"messages",
"ticket-panel",
"supportbot-ai",
"api",
];
function readConfig(name) {
return yaml.load(fs.readFileSync(path.join(CONFIG_DIR, `${name}.yml`), "utf8"));
}
function deepAssign(target, source) {
if (!target || typeof target !== "object" || Array.isArray(target)) {
return source;
}
if (!source || typeof source !== "object" || Array.isArray(source)) {
return source;
}
for (const key of Object.keys(source)) {
const next = source[key];
if (
next &&
typeof next === "object" &&
!Array.isArray(next) &&
target[key] &&
typeof target[key] === "object" &&
!Array.isArray(target[key])
) {
deepAssign(target[key], next);
} else {
target[key] = next;
}
}
for (const key of Object.keys(target)) {
if (!(key in source)) {
delete target[key];
}
}
return target;
}
const store = {};
for (const name of FILE_KEYS) {
store[name] = readConfig(name);
}
function reloadFile(fileKey) {
if (!FILE_KEYS.includes(fileKey)) {
throw new Error(`Unknown config: ${fileKey}`);
}
const fresh = readConfig(fileKey);
deepAssign(store[fileKey], fresh);
return store[fileKey];
}
function reloadBotConfigs() {
const reloaded = [];
for (const name of FILE_KEYS) {
if (name === "api") continue;
reloadFile(name);
reloaded.push(name);
}
return reloaded;
}
module.exports = {
supportbot: store.supportbot,
commands: store.commands,
messages: store.messages,
ticketPanel: store["ticket-panel"],
supportbotAi: store["supportbot-ai"],
api: store.api,
store,
reloadFile,
reloadBotConfigs,
readConfig,
};
+229
View File
@@ -0,0 +1,229 @@
/**
* Dashboard alert registry — server-side providers for actionable banners.
*
* Register a provider to surface new alerts anywhere in the dashboard:
*
* const { dashboardAlerts } = require("./DashboardAlerts.js");
* dashboardAlerts.register("my-addon", (ctx) => {
* if (!ctx.configStore.myAddon?.Configured) {
* return [{
* id: "my-addon:setup",
* severity: "warning",
* category: "my-addon",
* title: "My addon needs setup",
* message: "Finish configuration to enable this feature.",
* action: { label: "Configure", href: "/configs?file=my-addon" },
* }];
* }
* return [];
* });
*/
const { evaluateGuildHealth } = require("./GuildManager.js");
const { evaluateAllModules } = require("./ModuleStatus.js");
const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
const MODULE_LINKS = {
tickets: {
title: "Ticket system",
href: "/configs?file=ticket-panel",
},
supportbot: {
title: "Core bot settings",
href: "/configs?file=supportbot",
},
transcripts: {
title: "Ticket transcripts",
href: "/transcripts",
},
suggestions: {
title: "Suggestions",
href: "/configs?file=messages",
},
ai: {
title: "AI assistant",
href: "/configs?file=supportbot-ai",
},
commands: {
title: "Commands",
href: "/configs?file=commands",
},
};
class DashboardAlertRegistry {
constructor() {
/** @type {Map<string, (ctx: DashboardAlertContext) => DashboardAlert[] | Promise<DashboardAlert[]>>} */
this.providers = new Map();
}
/**
* @param {string} id Unique provider id (used for debugging / overrides)
* @param {(ctx: DashboardAlertContext) => DashboardAlert[] | Promise<DashboardAlert[]>} fn
*/
register(id, fn) {
this.providers.set(id, fn);
return this;
}
unregister(id) {
this.providers.delete(id);
}
/** @param {DashboardAlertContext} ctx */
async evaluate(ctx) {
const alerts = [];
for (const fn of this.providers.values()) {
const result = await fn(ctx);
if (Array.isArray(result) && result.length) {
alerts.push(...result);
}
}
return sortAlerts(alerts);
}
}
function sortAlerts(alerts) {
return [...alerts].sort((a, b) => {
const sa = SEVERITY_ORDER[a.severity] ?? 9;
const sb = SEVERITY_ORDER[b.severity] ?? 9;
if (sa !== sb) return sa - sb;
return a.title.localeCompare(b.title);
});
}
function guildSeverity(status) {
if (status === "offline") return "error";
return "warning";
}
function shouldSkipModuleAlert(moduleId, info) {
if (info.status === "done") return true;
if (moduleId === "ai" && info.subtitle === "AI is disabled in config") return true;
// Transcripts are automatic — empty archive is normal, not a setup step.
if (moduleId === "transcripts") return true;
// Partially configured modules are tracked on Overview — only banner when nothing is done yet.
if (info.status === "progress" && (info.passed ?? 0) > 0) return true;
return false;
}
function moduleSeverity(moduleId, info) {
if (info.status === "todo") {
return moduleId === "tickets" || moduleId === "supportbot" ? "warning" : "info";
}
return "info";
}
function evaluateGuildAlerts(ctx) {
const { client } = ctx;
if (!client?.user) {
return [
{
id: "guild:offline",
severity: "error",
category: "guild",
title: "Bot offline",
message: "The bot is not connected to Discord. Check your token and restart the server.",
action: { label: "Bot config", href: "/configs?file=supportbot" },
meta: { status: "offline" },
},
];
}
const health = evaluateGuildHealth(client);
if (health.status === "ok") return [];
return [
{
id: "guild:health",
severity: guildSeverity(health.status),
category: "guild",
title: "Server setup required",
message: health.message,
action: { label: "Bot config", href: "/configs?file=supportbot" },
meta: {
status: health.status,
configuredGuildId: health.configuredGuildId,
configuredGuild: health.configuredGuild,
extraGuilds: health.extraGuilds,
},
},
];
}
function evaluateModuleAlerts(ctx) {
const { configStore, transcriptCount = 0 } = ctx;
const modules = evaluateAllModules(configStore, { transcriptCount });
const alerts = [];
for (const [moduleId, info] of Object.entries(modules)) {
if (shouldSkipModuleAlert(moduleId, info)) continue;
const link = MODULE_LINKS[moduleId] || {
title: moduleId,
href: "/configs",
};
const progress =
info.total > 0 ? `${info.passed ?? 0} of ${info.total} checks complete` : null;
alerts.push({
id: `module:${moduleId}`,
severity: moduleSeverity(moduleId, info),
category: "modules",
title:
info.status === "todo"
? `${link.title} — setup required`
: `${link.title} — in progress`,
message: info.subtitle,
action: { label: "Open module", href: link.href },
meta: {
moduleId,
status: info.status,
passed: info.passed,
total: info.total,
progress,
},
});
}
return alerts;
}
const dashboardAlerts = new DashboardAlertRegistry();
dashboardAlerts.register("guild", evaluateGuildAlerts);
dashboardAlerts.register("modules", evaluateModuleAlerts);
/**
* @typedef {Object} DashboardAlertAction
* @property {string} label
* @property {string} href
*/
/**
* @typedef {Object} DashboardAlert
* @property {string} id
* @property {"error"|"warning"|"info"} severity
* @property {string} category
* @property {string} title
* @property {string} message
* @property {DashboardAlertAction} [action]
* @property {Record<string, unknown>} [meta]
*/
/**
* @typedef {Object} DashboardAlertContext
* @property {import("discord.js").Client} [client]
* @property {import("./ConfigStore.js")} configStore
* @property {number} [transcriptCount]
*/
async function evaluateDashboardAlerts(ctx) {
return dashboardAlerts.evaluate(ctx);
}
module.exports = {
dashboardAlerts,
evaluateDashboardAlerts,
sortAlerts,
MODULE_LINKS,
};
+57
View File
@@ -0,0 +1,57 @@
const { fullPermissions, hasPermission, canAccessConfig } = require("./DashboardPermissions.js");
function grantServiceAccess(req) {
req.dashboardService = true;
req.dashboardPermissions = fullPermissions();
req.dashboardRole = "service";
req.dashboardIsOwner = true;
}
function forbid(res, message = "Forbidden") {
return res.status(403).json({ success: false, error: message });
}
function createPermissionMiddleware(getPermissions) {
return function requirePermission(permission) {
return (req, res, next) => {
if (req.dashboardService) return next();
const perms = getPermissions(req) || req.dashboardPermissions;
if (hasPermission(perms, permission)) return next();
return forbid(res);
};
};
}
function createConfigMiddleware(getPermissions) {
return function requireConfigAccess(mode = "view") {
return (req, res, next) => {
if (req.dashboardService) return next();
const perms = getPermissions(req) || req.dashboardPermissions;
const file = req.params?.file || req.body?.filename;
if (!file) return next();
if (!canAccessConfig(perms, file, mode)) return forbid(res);
return next();
};
};
}
function assertConfigFilesEditable(permissions, files, res) {
if (!permissions) return forbid(res);
for (const file of files) {
if (!canAccessConfig(permissions, file, "edit")) {
return forbid(res, `No permission to edit ${file}.yml`);
}
}
return null;
}
module.exports = {
grantServiceAccess,
forbid,
createPermissionMiddleware,
createConfigMiddleware,
assertConfigFilesEditable,
fullPermissions,
hasPermission,
canAccessConfig,
};
+168
View File
@@ -0,0 +1,168 @@
const fs = require('fs');
const path = require('path');
const BRANDING_DIR = path.join(__dirname, '../Data/branding');
const BRANDING_FILE = path.join(BRANDING_DIR, 'branding.json');
const FAVICON_BASENAME = 'favicon';
const DEFAULTS = {
title: 'SupportBot',
pageTitle: 'SupportBot Dashboard',
faviconFile: null,
faviconMime: null,
updatedAt: null,
};
const ALLOWED_MIME = new Set([
'image/png',
'image/jpeg',
'image/x-icon',
'image/vnd.microsoft.icon',
'image/svg+xml',
'image/webp',
]);
const MIME_EXT = {
'image/png': '.png',
'image/jpeg': '.jpg',
'image/x-icon': '.ico',
'image/vnd.microsoft.icon': '.ico',
'image/svg+xml': '.svg',
'image/webp': '.webp',
};
function ensureDir() {
if (!fs.existsSync(BRANDING_DIR)) {
fs.mkdirSync(BRANDING_DIR, { recursive: true });
}
}
function readBranding() {
ensureDir();
if (!fs.existsSync(BRANDING_FILE)) {
return { ...DEFAULTS };
}
try {
const raw = JSON.parse(fs.readFileSync(BRANDING_FILE, 'utf8'));
return {
title: String(raw.title || DEFAULTS.title).slice(0, 64),
pageTitle: String(raw.pageTitle || DEFAULTS.pageTitle).slice(0, 80),
faviconFile: raw.faviconFile || null,
faviconMime: raw.faviconMime || null,
updatedAt: raw.updatedAt || null,
};
} catch {
return { ...DEFAULTS };
}
}
function writeBranding(data) {
ensureDir();
const next = {
title: String(data.title || DEFAULTS.title).slice(0, 64),
pageTitle: String(data.pageTitle || DEFAULTS.pageTitle).slice(0, 80),
faviconFile: data.faviconFile ?? null,
faviconMime: data.faviconMime ?? null,
updatedAt: Date.now(),
};
fs.writeFileSync(BRANDING_FILE, JSON.stringify(next, null, 2), 'utf8');
return next;
}
function publicPayload(branding) {
const hasFavicon = Boolean(branding.faviconFile);
return {
title: branding.title,
pageTitle: branding.pageTitle,
hasFavicon,
faviconUrl: hasFavicon ? '/api/branding/favicon' : null,
updatedAt: branding.updatedAt,
};
}
function updateBranding({ title, pageTitle }) {
const current = readBranding();
return writeBranding({
...current,
title: title !== undefined ? title : current.title,
pageTitle: pageTitle !== undefined ? pageTitle : current.pageTitle,
});
}
function removeExistingFavicons() {
if (!fs.existsSync(BRANDING_DIR)) return;
for (const file of fs.readdirSync(BRANDING_DIR)) {
if (file.startsWith(`${FAVICON_BASENAME}.`)) {
fs.unlinkSync(path.join(BRANDING_DIR, file));
}
}
}
function saveFaviconFromBase64(dataUrl) {
const match = String(dataUrl || '').match(
/^data:([\w/+.-]+);base64,([A-Za-z0-9+/=]+)$/,
);
if (!match) {
throw new Error('Invalid image data. Upload a PNG, ICO, SVG, or WebP file.');
}
const mime = match[1];
if (!ALLOWED_MIME.has(mime)) {
throw new Error('Unsupported image type. Use PNG, ICO, SVG, JPEG, or WebP.');
}
const buffer = Buffer.from(match[2], 'base64');
if (buffer.length > 512 * 1024) {
throw new Error('Favicon must be 512 KB or smaller.');
}
ensureDir();
removeExistingFavicons();
const ext = MIME_EXT[mime] || '.png';
const filename = `${FAVICON_BASENAME}${ext}`;
const filePath = path.join(BRANDING_DIR, filename);
fs.writeFileSync(filePath, buffer);
const current = readBranding();
return writeBranding({
...current,
faviconFile: filename,
faviconMime: mime,
});
}
function clearFavicon() {
removeExistingFavicons();
const current = readBranding();
return writeBranding({
...current,
faviconFile: null,
faviconMime: null,
});
}
function getFaviconPath() {
const branding = readBranding();
if (!branding.faviconFile) return null;
const filePath = path.join(BRANDING_DIR, branding.faviconFile);
if (!fs.existsSync(filePath)) return null;
return { filePath, mime: branding.faviconMime || 'image/png' };
}
function resetBranding() {
clearFavicon();
return writeBranding({ ...DEFAULTS });
}
module.exports = {
readBranding,
writeBranding,
publicPayload,
updateBranding,
saveFaviconFromBase64,
clearFavicon,
getFaviconPath,
resetBranding,
DEFAULTS,
};
+202
View File
@@ -0,0 +1,202 @@
// SupportBot | Emerald Services
const fs = require("fs");
const path = require("path");
const { fullPermissions, normalizePermissions } = require("./DashboardPermissions.js");
const GROUPS_FILE = path.join(__dirname, "../Data/dashboard-groups.json");
const DEFAULT_GROUPS = [
{
id: "owner",
name: "Owner",
description: "Full system owner access to all features and configs",
color: "#10B981", // Emerald Green
discordRoleId: null,
isSystem: true,
permissions: fullPermissions(),
},
{
id: "admin",
name: "Admin",
description: "Full administrative access including user and group management",
color: "#3B82F6", // Blue
discordRoleId: null,
isSystem: true,
permissions: fullPermissions(false, false),
},
{
id: "support",
name: "Support Staff",
description: "Access to Overview, Live Tickets, and Transcripts",
color: "#8B5CF6", // Purple
discordRoleId: null,
isSystem: false,
permissions: {
overview: true,
tickets: true,
logs: false,
transcripts: true,
settings: { view: false, update: false },
configs: {
supportbot: { view: false, edit: false },
"ticket-panel": { view: false, edit: false },
commands: { view: false, edit: false },
messages: { view: false, edit: false },
"supportbot-ai": { view: false, edit: false },
api: { view: false, edit: false },
},
users: { view: false, manage: false },
},
},
{
id: "viewer",
name: "Viewer",
description: "Read-only access to dashboard and logs",
color: "#6B7280", // Gray
discordRoleId: null,
isSystem: false,
permissions: {
overview: true,
tickets: true,
logs: true,
transcripts: true,
settings: { view: true, update: false },
configs: {
supportbot: { view: true, edit: false },
"ticket-panel": { view: true, edit: false },
commands: { view: true, edit: false },
messages: { view: true, edit: false },
"supportbot-ai": { view: true, edit: false },
api: { view: true, edit: false },
},
users: { view: false, manage: false },
},
},
];
function ensureStorage() {
const dir = path.dirname(GROUPS_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (!fs.existsSync(GROUPS_FILE)) {
const initialData = {
version: 1,
groups: DEFAULT_GROUPS,
};
fs.writeFileSync(GROUPS_FILE, JSON.stringify(initialData, null, 2), "utf8");
}
}
function loadStore() {
ensureStorage();
try {
const raw = fs.readFileSync(GROUPS_FILE, "utf8");
const parsed = JSON.parse(raw);
if (parsed && Array.isArray(parsed.groups)) {
return parsed.groups;
}
} catch (err) {
console.error("[GroupStore] Error loading groups file:", err.message);
}
return DEFAULT_GROUPS;
}
function saveStore(groups) {
ensureStorage();
const payload = {
version: 1,
groups,
updatedAt: Date.now(),
};
fs.writeFileSync(GROUPS_FILE, JSON.stringify(payload, null, 2), "utf8");
}
function listGroups() {
return loadStore();
}
function getGroup(id) {
const groups = loadStore();
return groups.find((g) => g.id === id) || null;
}
function createGroup(payload = {}) {
const groups = loadStore();
const name = (payload.name || "New Group").trim();
const slug = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "_");
let newId = slug || `group_${Date.now()}`;
let counter = 1;
while (groups.some((g) => g.id === newId)) {
newId = `${slug}_${counter++}`;
}
const newGroup = {
id: newId,
name,
description: (payload.description || "").trim(),
color: payload.color || "#10B981",
discordRoleId: payload.discordRoleId ? String(payload.discordRoleId).trim() : null,
isSystem: false,
permissions: normalizePermissions(payload.permissions),
createdAt: Date.now(),
updatedAt: Date.now(),
};
groups.push(newGroup);
saveStore(groups);
return newGroup;
}
function updateGroup(id, payload = {}) {
if (id === "owner") {
throw new Error('System group "Owner" cannot be modified.');
}
const groups = loadStore();
const index = groups.findIndex((g) => g.id === id);
if (index === -1) {
throw new Error(`Group with ID "${id}" not found.`);
}
const existing = groups[index];
const updatedGroup = {
...existing,
name: payload.name !== undefined ? payload.name.trim() : existing.name,
description: payload.description !== undefined ? payload.description.trim() : existing.description,
color: payload.color || existing.color,
discordRoleId: payload.discordRoleId !== undefined ? (payload.discordRoleId ? String(payload.discordRoleId).trim() : null) : existing.discordRoleId,
permissions: payload.permissions ? normalizePermissions(payload.permissions) : existing.permissions,
updatedAt: Date.now(),
};
groups[index] = updatedGroup;
saveStore(groups);
return updatedGroup;
}
function deleteGroup(id) {
const groups = loadStore();
const group = groups.find((g) => g.id === id);
if (!group) {
throw new Error(`Group with ID "${id}" not found.`);
}
if (group.isSystem) {
throw new Error(`System group "${group.name}" cannot be deleted.`);
}
const filtered = groups.filter((g) => g.id !== id);
saveStore(filtered);
return true;
}
module.exports = {
listGroups,
getGroup,
createGroup,
updateGroup,
deleteGroup,
DEFAULT_GROUPS,
};
+150
View File
@@ -0,0 +1,150 @@
const CONFIG_FILES = [
"supportbot",
"ticket-panel",
"commands",
"messages",
"supportbot-ai",
"api",
];
const ROLES = ["owner", "admin", "moderator", "editor", "viewer", "custom"];
function configMap(view, edit) {
const configs = {};
for (const file of CONFIG_FILES) {
configs[file] = { view: Boolean(view), edit: Boolean(edit) };
}
return configs;
}
function fullPermissions(rawYaml = true, userManage = true) {
return {
overview: true,
tickets: true,
logs: true,
transcripts: true,
rawYaml: Boolean(rawYaml),
settings: { view: true, update: true },
configs: configMap(true, true),
users: { view: true, manage: Boolean(userManage) },
};
}
function permissionsFromRole(role, customPermissions) {
switch (role) {
case "owner":
return fullPermissions(true, true);
case "admin":
return fullPermissions(false, false);
case "moderator":
return {
overview: true,
tickets: true,
logs: true,
transcripts: true,
rawYaml: false,
settings: { view: true, update: false },
configs: configMap(true, false),
users: { view: false, manage: false },
};
case "editor":
return {
overview: true,
tickets: true,
logs: true,
transcripts: false,
rawYaml: false,
settings: { view: true, update: false },
configs: configMap(true, true),
users: { view: false, manage: false },
};
case "viewer":
return {
overview: true,
tickets: true,
logs: true,
transcripts: true,
rawYaml: false,
settings: { view: true, update: false },
configs: configMap(true, false),
users: { view: false, manage: false },
};
case "custom":
return normalizePermissions(customPermissions);
default:
return permissionsFromRole("viewer");
}
}
function normalizePermissions(input) {
const base = permissionsFromRole("viewer");
if (!input || typeof input !== "object") return base;
return {
overview: input.overview ?? base.overview,
tickets: input.tickets ?? input.overview ?? base.tickets,
logs: input.logs ?? base.logs,
transcripts: input.transcripts ?? base.transcripts,
rawYaml: Boolean(input.rawYaml ?? input.configs?.rawYaml ?? base.rawYaml),
settings: {
view: input.settings?.view ?? base.settings.view,
update: input.settings?.update ?? base.settings.update,
},
configs: CONFIG_FILES.reduce((acc, file) => {
acc[file] = {
view: input.configs?.[file]?.view ?? base.configs[file].view,
edit: input.configs?.[file]?.edit ?? base.configs[file].edit,
};
return acc;
}, {}),
users: {
view: input.users?.view ?? base.users.view,
manage: input.users?.manage ?? base.users.manage,
},
};
}
function hasPermission(permissions, check) {
if (!permissions) return false;
if (check === "overview") return Boolean(permissions.overview);
if (check === "tickets") return Boolean(permissions.tickets ?? permissions.overview);
if (check === "logs") return Boolean(permissions.logs);
if (check === "transcripts") return Boolean(permissions.transcripts);
if (check === "configs.raw_yaml" || check === "configs.rawYaml" || check === "rawYaml") {
return Boolean(permissions.rawYaml ?? permissions.configs?.rawYaml ?? false);
}
if (check === "settings.view") return Boolean(permissions.settings?.view);
if (check === "settings.update") return Boolean(permissions.settings?.update);
if (check === "users.view") return Boolean(permissions.users?.view);
if (check === "users.manage") return Boolean(permissions.users?.manage);
const configView = check.match(/^configs\.([a-z0-9-]+)\.view$/);
if (configView) {
const file = configView[1];
return Boolean(permissions.configs?.[file]?.view);
}
const configEdit = check.match(/^configs\.([a-z0-9-]+)\.edit$/);
if (configEdit) {
const file = configEdit[1];
return Boolean(permissions.configs?.[file]?.edit);
}
return false;
}
function canAccessConfig(permissions, file, mode = "view") {
if (!CONFIG_FILES.includes(file)) return false;
return hasPermission(permissions, `configs.${file}.${mode}`);
}
module.exports = {
CONFIG_FILES,
ROLES,
fullPermissions,
permissionsFromRole,
normalizePermissions,
hasPermission,
canAccessConfig,
};
+75
View File
@@ -0,0 +1,75 @@
const crypto = require("crypto");
const SESSION_COOKIE = "sb_dashboard";
const SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
function signSession(payload, secret) {
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
const sig = crypto
.createHmac("sha256", secret)
.update(body)
.digest("base64url");
return `${body}.${sig}`;
}
function verifySession(token, secret) {
if (!token || typeof token !== "string") return null;
const dot = token.lastIndexOf(".");
if (dot <= 0) return null;
const body = token.slice(0, dot);
const sig = token.slice(dot + 1);
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("base64url");
if (sig.length !== expected.length) return null;
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return null;
}
try {
const payload = JSON.parse(
Buffer.from(body, "base64url").toString("utf8"),
);
if (!payload?.userId || !payload?.exp) return null;
if (Date.now() > payload.exp) return null;
return payload;
} catch {
return null;
}
}
function createSessionPayload(user) {
return {
userId: user.id,
username: user.username,
globalName: user.global_name || null,
avatar: user.avatar || null,
exp: Date.now() + SESSION_MAX_AGE_MS,
};
}
function sessionCookieOptions(maxAgeMs = SESSION_MAX_AGE_MS, opts = {}) {
const secure =
opts.secure !== undefined
? Boolean(opts.secure)
: process.env.NODE_ENV === "production";
return {
httpOnly: true,
secure,
sameSite: "lax",
maxAge: maxAgeMs,
path: "/",
};
}
module.exports = {
SESSION_COOKIE,
SESSION_MAX_AGE_MS,
signSession,
verifySession,
createSessionPayload,
sessionCookieOptions,
};
+510
View File
@@ -0,0 +1,510 @@
const fs = require("fs");
const path = require("path");
const axios = require("axios");
const YAML = require("yaml");
const configStore = require("./ConfigStore.js");
const { CALLBACK_PATH } = require("./DiscordOAuth.js");
const DISCORD_API = "https://discord.com/api/v10";
const CONFIG_DIR = path.join(__dirname, "../Configs");
const API_FILE = path.join(CONFIG_DIR, "api.yml");
const SUPPORTBOT_FILE = path.join(CONFIG_DIR, "supportbot.yml");
const PLACEHOLDER_PATTERNS = [
/DISCORD_CLIENT_ID/i,
/DISCORD_CLIENT_SECRET/i,
/YOUR_DISCORD_USER_ID/i,
/CHANGE_ME/i,
/BOT_TOKEN/i,
/PUT_RANDOM_STRING/i,
];
function isEmpty(value) {
if (value == null) return true;
if (typeof value === "boolean") return false;
if (typeof value === "number") return false;
return String(value).trim() === "";
}
function isPlaceholder(value) {
if (isEmpty(value)) return true;
const s = String(value).trim();
return PLACEHOLDER_PATTERNS.some((p) => p.test(s));
}
function isDiscordSnowflake(value) {
return /^\d{17,20}$/.test(String(value).trim());
}
function isValidBotToken(value) {
if (isPlaceholder(value)) return false;
const s = String(value).trim();
return s.length >= 50 && s.includes(".");
}
function isValidSecretKey(value) {
if (isPlaceholder(value)) return false;
return String(value).trim().length >= 16;
}
function readApiRoot() {
if (!fs.existsSync(API_FILE)) return null;
try {
const raw = YAML.parse(fs.readFileSync(API_FILE, "utf8"));
return raw?.API ?? raw ?? null;
} catch {
return null;
}
}
function readSupportbotRoot() {
if (!fs.existsSync(SUPPORTBOT_FILE)) return {};
try {
return YAML.parse(fs.readFileSync(SUPPORTBOT_FILE, "utf8")) || {};
} catch {
return {};
}
}
function getOwnerIds(oauth) {
const ids = [];
for (const id of oauth?.OwnerUserIds || []) {
const s = String(id).trim();
if (s) ids.push(s);
}
if (ids.length === 0) {
for (const id of oauth?.AllowedUserIds || []) {
const s = String(id).trim();
if (s) ids.push(s);
}
}
return ids;
}
function buildSteps(apiRoot, supportbot) {
const oauth = apiRoot?.OAuth || {};
const token = supportbot?.General?.Token;
const dbConfig = supportbot?.Database || {};
return [
{
id: "botToken",
title: "Bot token",
description: "From the Discord Developer Portal → your application → Bot → Reset Token.",
complete: isValidBotToken(token),
},
{
id: "database",
title: "Database storage",
description: "Choose SQLite or MySQL database engine for storing bot data.",
complete: Boolean(dbConfig.Driver),
},
{
id: "secretKey",
title: "Dashboard session secret",
description: "Random string used to sign login cookies. Keep it private.",
complete: isValidSecretKey(apiRoot?.SecretKey),
},
{
id: "oauth",
title: "Discord OAuth app",
description: "Same application as your bot — Client ID and Client Secret from OAuth2.",
complete:
!isPlaceholder(oauth.ClientId) &&
!isPlaceholder(oauth.ClientSecret) &&
isDiscordSnowflake(oauth.ClientId),
},
{
id: "redirectUri",
title: "OAuth redirect URL",
description: `Add this exact URL under OAuth2 → Redirects in the Discord portal.`,
complete: isValidRedirectUriConfigured(oauth.RedirectUri),
},
{
id: "owners",
title: "Dashboard owners",
description: "Discord user IDs that can sign in and manage the dashboard (at least one).",
complete: getOwnerIds(oauth).length > 0 && getOwnerIds(oauth).every(isDiscordSnowflake),
},
{
id: "emeraldApi",
title: "Emerald API",
description: "Optional. Used for the one-click addon installer.",
complete: true,
},
];
}
function isValidRedirectUriConfigured(uri) {
if (isPlaceholder(uri)) return false;
try {
const u = new URL(String(uri).trim());
if (!["http:", "https:"].includes(u.protocol)) return false;
return u.pathname.replace(/\/$/, "") === CALLBACK_PATH.replace(/\/$/, "");
} catch {
return false;
}
}
function isSetupComplete() {
const apiRoot = readApiRoot();
if (!apiRoot?.Enabled) return false;
const oauth = apiRoot.OAuth || {};
if (!oauth.Enabled) return false;
const steps = buildSteps(apiRoot, readSupportbotRoot());
return steps.every((s) => s.complete);
}
function getSetupStatus(req) {
const apiRoot = readApiRoot() || {};
const steps = buildSteps(apiRoot, readSupportbotRoot());
const complete = steps.every((s) => s.complete);
let suggestedRedirectUri = null;
if (req) {
const proto =
req.get("x-forwarded-proto")?.split(",")[0]?.trim() ||
req.protocol ||
"http";
const host = req.get("x-forwarded-host")?.split(",")[0]?.trim() || req.get("host");
if (host) {
suggestedRedirectUri = `${proto}://${host}${CALLBACK_PATH}`;
}
}
if (!suggestedRedirectUri) {
suggestedRedirectUri = `http://localhost:${apiRoot.Port || 3000}${CALLBACK_PATH}`;
}
return {
complete,
steps,
suggestedRedirectUri,
oauthCallbackPath: CALLBACK_PATH,
};
}
async function validateBotToken(token) {
const trimmed = String(token || "").trim();
if (!isValidBotToken(trimmed)) {
return { ok: false, error: "Enter a valid bot token from the Discord Developer Portal." };
}
try {
const [userRes, appRes] = await Promise.all([
axios.get(`${DISCORD_API}/users/@me`, {
headers: { Authorization: `Bot ${trimmed}` },
timeout: 12_000,
}),
axios.get(`${DISCORD_API}/oauth2/applications/@me`, {
headers: { Authorization: `Bot ${trimmed}` },
timeout: 12_000,
}),
]);
return {
ok: true,
bot: {
id: userRes.data.id,
username: userRes.data.username,
discriminator: userRes.data.discriminator,
},
applicationId: String(appRes.data.id),
};
} catch (err) {
const status = err.response?.status;
if (status === 401) {
return { ok: false, error: "Discord rejected this bot token (401 Unauthorized)." };
}
return {
ok: false,
error: err.response?.data?.message || err.message || "Could not verify bot token.",
};
}
}
async function validateOAuthCredentials(clientId, clientSecret, botToken) {
const id = String(clientId || "").trim();
const secret = String(clientSecret || "").trim();
if (!isDiscordSnowflake(id)) {
return { ok: false, error: "Client ID must be a numeric Discord application ID." };
}
if (isPlaceholder(secret) || secret.length < 20) {
return { ok: false, error: "Enter the OAuth Client Secret from the Discord portal." };
}
if (botToken) {
const botCheck = await validateBotToken(botToken);
if (botCheck.ok && botCheck.applicationId && botCheck.applicationId !== id) {
return {
ok: false,
error: `Client ID does not match the bot application (${botCheck.applicationId}). Use the same app's credentials.`,
};
}
}
try {
await axios.post(
`${DISCORD_API}/oauth2/token`,
new URLSearchParams({ grant_type: "client_credentials", scope: "identify" }),
{
auth: { username: id, password: secret },
headers: { "Content-Type": "application/x-www-form-urlencoded" },
timeout: 12_000,
},
);
return { ok: true, clientId: id };
} catch (err) {
const status = err.response?.status;
if (botToken) {
const botCheck = await validateBotToken(botToken);
if (botCheck.ok && botCheck.applicationId === id) {
return {
ok: true,
clientId: id,
warning:
"Could not verify the client secret with Discord (token endpoint). ID matches your bot — double-check the secret in the portal.",
};
}
}
if (status === 401 || status === 400) {
return { ok: false, error: "Invalid Client ID or Client Secret." };
}
return {
ok: false,
error: err.response?.data?.error_description || err.message || "OAuth validation failed.",
};
}
}
function validateRedirectUri(uri) {
const trimmed = String(uri || "").trim();
if (!isValidRedirectUriConfigured(trimmed)) {
return {
ok: false,
error: `Redirect URL must end with ${CALLBACK_PATH} and use http or https.`,
};
}
return { ok: true, redirectUri: trimmed };
}
async function validateOwnerUserIds(ownerIds, botToken) {
const ids = (Array.isArray(ownerIds) ? ownerIds : [ownerIds])
.map((id) => String(id).trim())
.filter(Boolean);
if (ids.length === 0) {
return { ok: false, error: "Add at least one Discord user ID." };
}
const invalid = ids.filter((id) => !isDiscordSnowflake(id));
if (invalid.length) {
return { ok: false, error: `Invalid user ID: ${invalid[0]}` };
}
if (!botToken || !isValidBotToken(botToken)) {
return { ok: true, ownerUserIds: ids, warning: "User IDs format OK (bot token not verified yet)." };
}
const users = [];
try {
for (const id of ids) {
const res = await axios.get(`${DISCORD_API}/users/${id}`, {
headers: { Authorization: `Bot ${String(botToken).trim()}` },
timeout: 12_000,
});
users.push({
id: res.data.id,
username: res.data.username,
globalName: res.data.global_name || null,
});
}
return { ok: true, ownerUserIds: ids, users };
} catch (err) {
if (err.response?.status === 404) {
return { ok: false, error: "One or more user IDs were not found. Check Developer Mode → Copy User ID." };
}
if (err.response?.status === 403) {
return {
ok: false,
error: "Bot could not look up users. Ensure the bot is in a server with those users, or verify IDs manually.",
};
}
return { ok: false, error: err.message || "Failed to verify owner user IDs." };
}
}
function validateSecretKey(secretKey) {
if (!isValidSecretKey(secretKey)) {
return { ok: false, error: "Use at least 16 characters (not CHANGE_ME)." };
}
return { ok: true };
}
async function validateDatabase(dbDriver, dbHost, dbPort, dbUser, dbPassword, dbDatabase) {
const driver = String(dbDriver || "sqlite").toLowerCase();
if (driver === "mysql" || driver === "mariadb") {
const host = String(dbHost || "").trim();
const port = Number(dbPort) || 3306;
const user = String(dbUser || "").trim();
const password = String(dbPassword || "");
const database = String(dbDatabase || "").trim();
if (isEmpty(host)) return { ok: false, error: "MySQL Host is required." };
if (isEmpty(user)) return { ok: false, error: "MySQL User is required." };
if (isEmpty(database)) return { ok: false, error: "MySQL Database Name is required." };
try {
const mysql = require("mysql2/promise");
const connection = await mysql.createConnection({
host,
port,
user,
password,
database,
connectTimeout: 5000,
});
await connection.end();
return { ok: true, driver: "mysql" };
} catch (err) {
return {
ok: false,
error: `MySQL Connection Failed: ${err.message || err.code || "Could not connect to database host."}`,
};
}
}
return { ok: true, driver: "sqlite" };
}
async function validateSetupPayload(payload) {
const results = {};
if (payload.botToken != null) {
results.botToken = await validateBotToken(payload.botToken);
}
if (payload.dbDriver != null) {
results.database = await validateDatabase(
payload.dbDriver,
payload.dbHost,
payload.dbPort,
payload.dbUser,
payload.dbPassword,
payload.dbDatabase,
);
}
if (payload.secretKey != null) {
results.secretKey = validateSecretKey(payload.secretKey);
}
if (payload.clientId != null && payload.clientSecret != null) {
results.oauth = await validateOAuthCredentials(
payload.clientId,
payload.clientSecret,
payload.botToken,
);
}
if (payload.redirectUri != null) {
results.redirectUri = validateRedirectUri(payload.redirectUri);
}
if (payload.ownerUserIds != null) {
results.owners = await validateOwnerUserIds(payload.ownerUserIds, payload.botToken);
}
if (payload.emeraldApiKey != null) {
const key = String(payload.emeraldApiKey).trim();
if (key === "") {
results.emeraldApi = { ok: true };
} else if (key.length < 10) {
results.emeraldApi = { ok: false, error: "Invalid API key." };
} else {
results.emeraldApi = { ok: true };
}
}
const allOk = Object.values(results).every((r) => r?.ok);
return { ok: allOk, results };
}
function ensureApiFile() {
if (fs.existsSync(API_FILE)) return;
const example = path.join(CONFIG_DIR, "api.yml.example");
if (fs.existsSync(example)) {
fs.copyFileSync(example, API_FILE);
configStore.reloadFile("api");
} else {
throw new Error("Configs/api.yml is missing. Copy Configs/api.yml.example to api.yml.");
}
}
function applySetup(payload) {
ensureApiFile();
const ownerIds = (Array.isArray(payload.ownerUserIds)
? payload.ownerUserIds
: String(payload.ownerUserIds || "").split(/[\s,]+/)
)
.map((id) => String(id).trim())
.filter(Boolean);
const apiDoc = YAML.parseDocument(fs.readFileSync(API_FILE, "utf8"));
if (!apiDoc.get("API")) apiDoc.set("API", {});
apiDoc.setIn(["API", "Enabled"], true);
apiDoc.setIn(["API", "SecretKey"], String(payload.secretKey).trim());
if (payload.emeraldApiKey != null) {
apiDoc.setIn(["API", "EmeraldAPIKey"], String(payload.emeraldApiKey).trim());
}
if (payload.port != null && payload.port !== "") {
apiDoc.setIn(["API", "Port"], Number(payload.port) || 3000);
}
if (!apiDoc.getIn(["API", "OAuth"])) apiDoc.setIn(["API", "OAuth"], {});
apiDoc.setIn(["API", "OAuth", "Enabled"], true);
apiDoc.setIn(["API", "OAuth", "ClientId"], String(payload.clientId).trim());
apiDoc.setIn(["API", "OAuth", "ClientSecret"], String(payload.clientSecret).trim());
apiDoc.setIn(["API", "OAuth", "RedirectUri"], String(payload.redirectUri).trim());
apiDoc.setIn(["API", "OAuth", "OwnerUserIds"], ownerIds);
apiDoc.setIn(["API", "OAuth", "AllowedUserIds"], []);
fs.writeFileSync(API_FILE, apiDoc.toString());
const sbDoc = YAML.parseDocument(fs.readFileSync(SUPPORTBOT_FILE, "utf8"));
if (payload.botToken != null && String(payload.botToken).trim() !== "") {
sbDoc.setIn(["General", "Token"], String(payload.botToken).trim());
}
if (payload.dbDriver != null) {
const driver = String(payload.dbDriver).toLowerCase() === "mysql" ? "mysql" : "sqlite";
sbDoc.setIn(["Database", "Driver"], driver);
if (driver === "mysql") {
sbDoc.setIn(["Database", "MySQL", "Host"], String(payload.dbHost || "127.0.0.1").trim());
sbDoc.setIn(["Database", "MySQL", "Port"], Number(payload.dbPort) || 3306);
sbDoc.setIn(["Database", "MySQL", "User"], String(payload.dbUser || "").trim());
sbDoc.setIn(["Database", "MySQL", "Password"], String(payload.dbPassword || ""));
sbDoc.setIn(["Database", "MySQL", "Database"], String(payload.dbDatabase || "").trim());
} else {
sbDoc.setIn(["Database", "SQLite", "File"], "./Data/supportbot.db");
}
}
fs.writeFileSync(SUPPORTBOT_FILE, sbDoc.toString());
configStore.reloadFile("api");
configStore.reloadFile("supportbot");
return {
ownerUserIds: ownerIds,
api: configStore.api?.API || configStore.api,
};
}
module.exports = {
isSetupComplete,
getSetupStatus,
validateBotToken,
validateOAuthCredentials,
validateRedirectUri,
validateOwnerUserIds,
validateSecretKey,
validateSetupPayload,
applySetup,
};
+311
View File
@@ -0,0 +1,311 @@
const fs = require("fs");
const path = require("path");
const {
CONFIG_FILES,
ROLES,
fullPermissions,
permissionsFromRole,
normalizePermissions,
hasPermission,
canAccessConfig,
} = require("./DashboardPermissions.js");
const { discordAvatarUrl } = require("./DiscordOAuth.js");
const STORE_PATH = path.join(__dirname, "../Data/dashboard-users.json");
function ensureDataDir() {
const dir = path.dirname(STORE_PATH);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function emptyStore() {
return { version: 1, users: {} };
}
function readStore() {
ensureDataDir();
if (!fs.existsSync(STORE_PATH)) return emptyStore();
try {
const raw = JSON.parse(fs.readFileSync(STORE_PATH, "utf8"));
if (!raw.users || typeof raw.users !== "object") return emptyStore();
return raw;
} catch {
return emptyStore();
}
}
function writeStore(store) {
ensureDataDir();
fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), "utf8");
}
function getOwnerIds(oauth) {
const owners = new Set();
for (const id of oauth?.OwnerUserIds || []) owners.add(String(id));
for (const id of oauth?.AllowedUserIds || []) owners.add(String(id));
return owners;
}
function isYamlOwner(userId, oauth) {
return getOwnerIds(oauth).has(String(userId));
}
function bootstrapFromOAuth(oauth) {
const store = readStore();
const owners = getOwnerIds(oauth);
let changed = false;
for (const id of owners) {
if (!store.users[id]) {
store.users[id] = {
id,
username: null,
globalName: null,
avatar: null,
role: "owner",
permissions: null,
enabled: true,
createdAt: Date.now(),
updatedAt: Date.now(),
createdBy: "system",
};
changed = true;
}
}
if (changed) writeStore(store);
return store;
}
function syncDiscordProfile(userId, profile) {
const store = readStore();
const entry = store.users[userId];
if (!entry) return;
entry.username = profile.username ?? entry.username;
entry.globalName = profile.globalName ?? entry.globalName;
entry.avatar = profile.avatar ?? entry.avatar;
entry.updatedAt = Date.now();
writeStore(store);
}
const { getGroup } = require("./DashboardGroupStore.js");
function resolvePermissions(entry, oauth, userId) {
if (isYamlOwner(userId, oauth)) {
return { permissions: fullPermissions(), role: "owner", isOwner: true, groupId: "owner" };
}
if (!entry?.enabled) return null;
const groupId = entry.groupId || entry.role || "support";
const group = getGroup(groupId);
if (entry.role === "custom" && entry.permissions) {
return { permissions: normalizePermissions(entry.permissions), role: "custom", isOwner: false, groupId };
}
const permissions = group ? group.permissions : permissionsFromRole(entry.role || "viewer");
return { permissions, role: entry.role || groupId, isOwner: false, groupId };
}
function getAccess(userId, oauth) {
bootstrapFromOAuth(oauth);
const store = readStore();
const id = String(userId);
if (isYamlOwner(id, oauth)) {
const entry = store.users[id];
return {
entry: entry || { id, role: "owner" },
permissions: fullPermissions(),
role: "owner",
isOwner: true,
yamlOwner: true,
};
}
const entry = store.users[id];
const resolved = resolvePermissions(entry, oauth, id);
if (!resolved) return null;
return {
entry,
permissions: resolved.permissions,
role: resolved.role,
isOwner: false,
yamlOwner: false,
};
}
function canLogin(userId, oauth) {
return Boolean(getAccess(userId, oauth));
}
function formatPublicUser(entry, access, discordProfile) {
const id = String(entry?.id || discordProfile?.id);
const avatarHash = entry?.avatar ?? discordProfile?.avatar ?? null;
return {
id,
username: entry?.username || discordProfile?.username || "Unknown",
globalName: entry?.globalName ?? discordProfile?.global_name ?? null,
avatar: avatarHash
? discordAvatarUrl({ id, avatar: avatarHash })
: discordProfile
? discordAvatarUrl(discordProfile)
: discordAvatarUrl({ id, avatar: null }),
role: access.role,
isOwner: access.isOwner,
permissions: access.permissions,
};
}
function listUsers(oauth, discordClient) {
bootstrapFromOAuth(oauth);
const store = readStore();
const yamlOwners = getOwnerIds(oauth);
let updated = false;
const usersList = Object.values(store.users).map((entry) => {
if (discordClient?.users?.cache) {
const cached = discordClient.users.cache.get(String(entry.id));
if (cached && (!entry.username || !entry.avatar)) {
entry.username = cached.username || entry.username;
entry.globalName = cached.globalName || cached.username || entry.globalName;
entry.avatar = cached.avatar || entry.avatar;
updated = true;
}
}
const access = resolvePermissions(entry, oauth, entry.id) || {
permissions: permissionsFromRole("viewer"),
role: entry.role,
isOwner: false,
};
return {
id: entry.id,
username: entry.username || (yamlOwners.has(String(entry.id)) ? "Owner" : "User"),
globalName: entry.globalName || entry.username || null,
avatar: entry.avatar
? discordAvatarUrl({ id: entry.id, avatar: entry.avatar })
: discordAvatarUrl({ id: entry.id, avatar: null }),
role: yamlOwners.has(String(entry.id)) ? "owner" : entry.role || entry.groupId || "support",
groupId: entry.groupId || entry.role || "support",
permissions:
entry.role === "custom"
? normalizePermissions(entry.permissions)
: access.permissions,
enabled: entry.enabled !== false,
isOwner: yamlOwners.has(String(entry.id)),
yamlOwner: yamlOwners.has(String(entry.id)),
createdAt: entry.createdAt,
updatedAt: entry.updatedAt,
};
});
if (updated) {
writeStore(store);
}
return usersList;
}
function addUser(payload, actorId, oauth) {
const id = String(payload.id || "").trim();
if (!/^\d{17,20}$/.test(id)) {
throw new Error("Invalid Discord user ID");
}
const role = payload.role || payload.groupId || "support";
const groupId = payload.groupId || payload.role || "support";
const store = readStore();
if (store.users[id]) throw new Error("User already exists");
store.users[id] = {
id,
username: payload.username || null,
globalName: payload.globalName || null,
avatar: null,
role,
groupId,
permissions:
role === "custom" ? normalizePermissions(payload.permissions) : null,
enabled: payload.enabled !== false,
createdAt: Date.now(),
updatedAt: Date.now(),
createdBy: String(actorId),
};
writeStore(store);
return listUsers(oauth).find((u) => u.id === id);
}
function updateUser(id, payload, actorId, oauth) {
const store = readStore();
const entry = store.users[String(id)];
if (!entry) throw new Error("User not found");
if (isYamlOwner(id, oauth) && payload.enabled === false) {
throw new Error("Cannot disable an owner defined in api.yml");
}
if (payload.groupId) {
entry.groupId = payload.groupId;
if (!payload.role) entry.role = payload.groupId;
}
if (payload.role) {
entry.role = payload.role;
if (!payload.groupId) entry.groupId = payload.role;
}
if (payload.enabled !== undefined) entry.enabled = Boolean(payload.enabled);
if (payload.username !== undefined) entry.username = payload.username;
if (payload.globalName !== undefined) entry.globalName = payload.globalName;
if (payload.permissions !== undefined) {
entry.permissions = normalizePermissions(payload.permissions);
if (entry.role !== "custom") entry.role = "custom";
}
if (entry.role === "custom" && payload.permissions === undefined && payload.role !== "custom") {
entry.permissions = null;
}
entry.updatedAt = Date.now();
writeStore(store);
return listUsers(oauth).find((u) => u.id === String(id));
}
function removeUser(id, oauth) {
if (isYamlOwner(id, oauth)) {
throw new Error("Remove this user from OwnerUserIds in api.yml instead");
}
const store = readStore();
if (!store.users[String(id)]) throw new Error("User not found");
delete store.users[String(id)];
writeStore(store);
}
module.exports = {
CONFIG_FILES,
STORE_PATH,
getOwnerIds,
isYamlOwner,
bootstrapFromOAuth,
syncDiscordProfile,
getAccess,
canLogin,
formatPublicUser,
listUsers,
addUser,
updateUser,
removeUser,
hasPermission,
canAccessConfig,
ROLES,
};
File diff suppressed because it is too large. Load diff
+233
View File
@@ -0,0 +1,233 @@
const crypto = require("crypto");
const axios = require("axios");
const DISCORD_API = "https://discord.com/api";
const OAUTH_STATE_COOKIE = "sb_oauth_state";
const OAUTH_REDIRECT_COOKIE = "sb_oauth_redirect";
const OAUTH_STATE_MAX_AGE_MS = 10 * 60 * 1000;
const CALLBACK_PATH = "/api/auth/discord/callback";
function discordAvatarUrl(user) {
if (user.avatar) {
const ext = user.avatar.startsWith("a_") ? "gif" : "png";
return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=128`;
}
const index = Number((BigInt(user.id) >> 22n) % 6n);
return `https://cdn.discordapp.com/embed/avatars/${index}.png`;
}
function getTrustProxySetting(apiConfig) {
if (apiConfig?.TrustProxy === false) return false;
if (typeof apiConfig?.TrustProxy === "number") return apiConfig.TrustProxy;
if (apiConfig?.TrustProxy === true) return 1;
return false;
}
function getRequestProto(req, apiConfig) {
if (apiConfig?.TrustProxy !== false) {
const forwarded = req.get("x-forwarded-proto");
if (forwarded) {
return forwarded.split(",")[0].trim().toLowerCase();
}
}
return (req.protocol || "http").toLowerCase();
}
function getRequestHost(req) {
const forwarded = req.get("x-forwarded-host");
if (forwarded) {
return forwarded.split(",")[0].trim();
}
return req.get("host") || "";
}
function getRequestOrigin(req, apiConfig) {
const proto = getRequestProto(req, apiConfig);
const host = getRequestHost(req);
if (!host) return null;
return `${proto}://${host}`;
}
function normalizeRedirectUri(uri) {
try {
const u = new URL(uri);
u.hash = "";
u.search = "";
return u.toString();
} catch {
return null;
}
}
function redirectUriMatchesHostPath(a, b) {
try {
const ua = new URL(a);
const ub = new URL(b);
return ua.host === ub.host && ua.pathname === ub.pathname;
} catch {
return false;
}
}
function collectConfiguredRedirectUris(oauth) {
const list = [];
if (oauth?.RedirectUri) {
const normalized = normalizeRedirectUri(oauth.RedirectUri);
if (normalized) list.push(normalized);
}
if (Array.isArray(oauth?.RedirectUris)) {
for (const entry of oauth.RedirectUris) {
const normalized = normalizeRedirectUri(entry);
if (normalized && !list.includes(normalized)) {
list.push(normalized);
}
}
}
return list;
}
/**
* Pick the OAuth redirect_uri for this request. When UseRequestOrigin is enabled
* (default), uses the same host/path as RedirectUri but the protocol the user
* actually used (http vs https). Register every variant in the Discord portal.
*/
function resolveOAuthRedirectUri(req, oauth, apiConfig) {
const configured = collectConfiguredRedirectUris(oauth);
const fallback = configured[0] || oauth?.RedirectUri || "";
const useRequestOrigin = oauth?.UseRequestOrigin !== false;
if (!useRequestOrigin || !req) {
return fallback;
}
const origin = getRequestOrigin(req, apiConfig);
if (!origin) return fallback;
const dynamic = `${origin}${CALLBACK_PATH}`;
const normalizedDynamic = normalizeRedirectUri(dynamic);
if (!normalizedDynamic) return fallback;
if (configured.includes(normalizedDynamic)) {
return normalizedDynamic;
}
const matchesConfigured = configured.some((uri) =>
redirectUriMatchesHostPath(uri, normalizedDynamic),
);
if (matchesConfigured) {
return normalizedDynamic;
}
return fallback;
}
function isSecureRequest(req, oauth, apiConfig) {
if (oauth?.CookieSecure === true) return true;
if (oauth?.CookieSecure === false) return false;
if (req) {
if (typeof req.secure === "boolean") return req.secure;
return getRequestProto(req, apiConfig) === "https";
}
return process.env.NODE_ENV === "production";
}
function buildAuthorizeUrl(oauth, state, redirectUri) {
const params = new URLSearchParams({
client_id: oauth.ClientId,
redirect_uri: redirectUri,
response_type: "code",
scope: "identify",
state,
prompt: "none",
});
return `${DISCORD_API}/oauth2/authorize?${params.toString()}`;
}
async function exchangeCode(oauth, code, redirectUri) {
const body = new URLSearchParams({
client_id: oauth.ClientId,
client_secret: oauth.ClientSecret,
grant_type: "authorization_code",
code,
redirect_uri: redirectUri,
});
const { data } = await axios.post(`${DISCORD_API}/oauth2/token`, body, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
timeout: 15000,
});
return data.access_token;
}
async function fetchDiscordUser(accessToken) {
const { data } = await axios.get(`${DISCORD_API}/users/@me`, {
headers: { Authorization: `Bearer ${accessToken}` },
timeout: 15000,
});
return data;
}
function createOAuthState() {
return crypto.randomBytes(24).toString("hex");
}
function oauthStateCookieOptions(req, oauth, apiConfig) {
return {
httpOnly: true,
secure: isSecureRequest(req, oauth, apiConfig),
sameSite: "lax",
maxAge: OAUTH_STATE_MAX_AGE_MS,
path: "/",
};
}
function oauthRedirectCookieOptions(req, oauth, apiConfig) {
return oauthStateCookieOptions(req, oauth, apiConfig);
}
function isOAuthConfigured(oauth) {
return Boolean(
oauth?.Enabled &&
oauth.ClientId &&
oauth.ClientSecret &&
oauth.RedirectUri,
);
}
function isUserAllowed(userId, oauth) {
const allowed = oauth?.AllowedUserIds;
if (!Array.isArray(allowed) || allowed.length === 0) {
return false;
}
return allowed.map(String).includes(String(userId));
}
function formatPublicUser(user) {
return {
id: user.id,
username: user.username,
globalName: user.global_name || null,
avatar: discordAvatarUrl(user),
};
}
module.exports = {
OAUTH_STATE_COOKIE,
OAUTH_REDIRECT_COOKIE,
OAUTH_STATE_MAX_AGE_MS,
CALLBACK_PATH,
getTrustProxySetting,
resolveOAuthRedirectUri,
isSecureRequest,
buildAuthorizeUrl,
exchangeCode,
fetchDiscordUser,
createOAuthState,
oauthStateCookieOptions,
oauthRedirectCookieOptions,
isOAuthConfigured,
isUserAllowed,
formatPublicUser,
discordAvatarUrl,
};
+5 -15
View File
@@ -1,21 +1,11 @@
// SupportBot | Emerald Services
// Event Structure
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
function RunFunction(client, ...eventArgs) {}
class Event {
constructor(event, runFunction) {
this.event = event;
this.run = runFunction;
}
constructor(event, run) {
this.event = event;
this.run = run;
}
}
module.exports = Event;
module.exports = Event;
+277
View File
@@ -0,0 +1,277 @@
const { ChannelType } = require("discord.js");
const configStore = require("./ConfigStore.js");
const { withTimeout } = require("./asyncTimeout.js");
const RESOURCES_CACHE_MS = 90 * 1000;
/** Bump when resource payload shape changes (e.g. added emojis). */
const RESOURCES_CACHE_VERSION = 2;
let resourcesCache = { guildId: null, data: null, at: 0, version: 0 };
let resourcesInflight = null;
function getConfiguredGuildId() {
const id = configStore.supportbot?.General?.GuildId;
if (!id || id === "GUILD_ID" || String(id).trim() === "") return null;
return String(id).trim();
}
function setConfiguredGuildId(guildId) {
if (!configStore.supportbot.General) {
configStore.supportbot.General = {};
}
configStore.supportbot.General.GuildId = guildId ? String(guildId) : "";
}
function getPrimaryGuild(client) {
if (!client) return null;
const configuredId = getConfiguredGuildId();
if (configuredId) {
const match = client.guilds.cache.get(configuredId);
if (match) return match;
}
return client.guilds.cache.first() || null;
}
function evaluateGuildHealth(client) {
const configuredId = getConfiguredGuildId();
const guilds = [...client.guilds.cache.values()];
if (!configuredId) {
return {
status: "missing_config",
configuredGuildId: null,
message:
"No guild ID is set. Add your Discord server ID under Bot config → General.",
configuredGuild: null,
extraGuilds: guilds.map(formatGuildSummary),
};
}
const configuredGuild = client.guilds.cache.get(configuredId) || null;
const extraGuilds = guilds
.filter((g) => g.id !== configuredId)
.map(formatGuildSummary);
if (!configuredGuild) {
return {
status: "not_in_guild",
configuredGuildId: configuredId,
message: `The bot is not in the configured server (${configuredId}). Invite it or update the guild ID.`,
configuredGuild: null,
extraGuilds: guilds.map(formatGuildSummary),
};
}
if (extraGuilds.length > 0) {
return {
status: "extra_guilds",
configuredGuildId: configuredId,
message: `The bot is in ${extraGuilds.length} extra server(s). Leave them so only your configured server remains.`,
configuredGuild: formatGuildSummary(configuredGuild),
extraGuilds,
};
}
return {
status: "ok",
configuredGuildId: configuredId,
message: null,
configuredGuild: formatGuildSummary(configuredGuild),
extraGuilds: [],
};
}
function formatGuildSummary(guild) {
return {
id: guild.id,
name: guild.name,
memberCount: guild.memberCount ?? null,
icon: guild.iconURL?.({ size: 128 }) ?? null,
};
}
function syncGuildHealth(client) {
if (!client?.user) {
client.guildHealth = {
status: "offline",
configuredGuildId: getConfiguredGuildId(),
message: "Bot is not connected to Discord.",
configuredGuild: null,
extraGuilds: [],
};
return client.guildHealth;
}
client.guildHealth = evaluateGuildHealth(client);
return client.guildHealth;
}
function logGuildWarning(client, message) {
const name = client?.user?.username || "Bot";
console.warn(`\x1b[33m[Guild]\x1b[0m ${name}: ${message}`);
}
async function leaveGuild(client, guildId) {
const guild = client.guilds.cache.get(String(guildId));
if (!guild) {
throw new Error("Bot is not in that server.");
}
const configuredId = getConfiguredGuildId();
if (configuredId && guild.id === configuredId) {
throw new Error("Cannot leave the configured server. Change the guild ID first.");
}
await guild.leave();
syncGuildHealth(client);
return syncGuildHealth(client);
}
function buildResourcesFromGuild(guild) {
const roles = [...guild.roles.cache.values()]
.filter((r) => r.id !== guild.id)
.sort((a, b) => b.position - a.position)
.map((r) => ({
id: r.id,
name: r.name,
color: r.hexColor || null,
}));
const channels = [...guild.channels.cache.values()]
.filter(
(c) =>
c.type === ChannelType.GuildText ||
c.type === ChannelType.GuildVoice ||
c.type === ChannelType.GuildAnnouncement ||
c.type === ChannelType.GuildStageVoice ||
c.type === ChannelType.GuildForum,
)
.sort((a, b) => a.name.localeCompare(b.name))
.map((c) => ({
id: c.id,
name: c.name,
type: c.type,
}));
const categories = [...guild.channels.cache.values()]
.filter((c) => c.type === ChannelType.GuildCategory)
.sort((a, b) => a.name.localeCompare(b.name))
.map((c) => ({
id: c.id,
name: c.name,
}));
const emojis = [...guild.emojis.cache.values()]
.sort((a, b) => a.name.localeCompare(b.name))
.map((e) => {
const animated = Boolean(e.animated);
const ext = animated ? "gif" : "png";
return {
id: e.id,
name: e.name,
animated,
url:
e.imageURL({ extension: ext, size: 64 }) ||
`https://cdn.discordapp.com/emojis/${e.id}.${ext}?size=64`,
};
});
return {
guildId: guild.id,
guildName: guild.name,
guildIcon: guild.iconURL({ extension: "png", size: 64 }) || null,
roles,
channels,
categories,
emojis,
};
}
async function fetchGuildResourcesFromDiscord(client, guildId) {
const id = guildId || getConfiguredGuildId();
if (!id) return null;
const guild = client.guilds.cache.get(id);
if (!guild) return null;
try {
await withTimeout(
Promise.all([guild.channels.fetch(), guild.roles.fetch()]),
12_000,
"Discord channel/role fetch timed out",
);
} catch (err) {
console.warn("[Guild] Failed to fetch channels/roles:", err.message);
if (guild.roles.cache.size === 0 && guild.channels.cache.size === 0) {
return null;
}
}
try {
await withTimeout(guild.emojis.fetch(), 12_000, "Discord emoji fetch timed out");
} catch (err) {
console.warn("[Guild] Failed to fetch guild emojis:", err.message);
}
return buildResourcesFromGuild(guild);
}
async function fetchGuildResources(client, guildId) {
const id = guildId || getConfiguredGuildId();
if (!id || !client?.user) return null;
const now = Date.now();
if (
resourcesCache.data &&
resourcesCache.version === RESOURCES_CACHE_VERSION &&
resourcesCache.guildId === id &&
now - resourcesCache.at < RESOURCES_CACHE_MS
) {
return resourcesCache.data;
}
if (resourcesInflight) {
try {
return await resourcesInflight;
} catch {
/* fall through to retry */
}
}
resourcesInflight = (async () => {
const data = await fetchGuildResourcesFromDiscord(client, id);
if (data) {
resourcesCache = {
guildId: id,
data,
at: Date.now(),
version: RESOURCES_CACHE_VERSION,
};
}
return data;
})();
try {
return await resourcesInflight;
} finally {
resourcesInflight = null;
}
}
function clearGuildResourcesCache() {
resourcesCache = { guildId: null, data: null, at: 0, version: 0 };
resourcesInflight = null;
}
module.exports = {
getConfiguredGuildId,
setConfiguredGuildId,
getPrimaryGuild,
evaluateGuildHealth,
syncGuildHealth,
logGuildWarning,
leaveGuild,
fetchGuildResources,
clearGuildResourcesCache,
};
+181
View File
@@ -0,0 +1,181 @@
const fs = require("fs");
const path = require("path");
const LOG_ROOT = path.resolve(__dirname, "../Logs");
const LOG_TYPES = ["Output", "Warn", "Error"];
function todayDate() {
return new Date().toISOString().split("T")[0];
}
function getLogPath(type, date = todayDate()) {
return path.join(LOG_ROOT, type, `${type}-${date}.log`);
}
/** Prefer today's file; fall back to the newest log in the folder. */
function resolveLogPath(type) {
const todayPath = getLogPath(type);
if (fs.existsSync(todayPath)) return todayPath;
const dir = path.join(LOG_ROOT, type);
if (!fs.existsSync(dir)) return todayPath;
const files = fs
.readdirSync(dir)
.filter((f) => f.endsWith(".log"))
.sort()
.reverse();
if (!files.length) return todayPath;
return path.join(dir, files[0]);
}
function stripAnsi(text) {
return String(text).replace(/\x1b\[[0-9;]*m/g, "");
}
function parseLogLine(line, type) {
const trimmed = line.trimEnd();
if (!trimmed) return null;
const match = trimmed.match(/^\[([^\]]+)\]\s?(.*)$/);
if (match) {
return {
type,
timestamp: match[1],
message: stripAnsi(match[2]),
};
}
return {
type,
timestamp: null,
message: stripAnsi(trimmed),
};
}
function readFromByte(filePath, fromByte) {
if (!fs.existsSync(filePath)) {
return { lines: [], position: 0 };
}
const stat = fs.statSync(filePath);
if (stat.size === 0) {
return { lines: [], position: 0 };
}
const start = Math.max(0, Math.min(fromByte, stat.size));
if (start >= stat.size) {
return { lines: [], position: stat.size };
}
const fd = fs.openSync(filePath, "r");
const buffer = Buffer.alloc(stat.size - start);
fs.readSync(fd, buffer, 0, buffer.length, start);
fs.closeSync(fd);
const text = buffer.toString("utf8");
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
return { lines, position: stat.size };
}
function readLastLines(filePath, maxLines) {
if (!fs.existsSync(filePath)) {
return { lines: [], position: 0 };
}
const stat = fs.statSync(filePath);
if (stat.size === 0) {
return { lines: [], position: 0 };
}
const chunkSize = Math.min(stat.size, 512 * 1024);
const start = Math.max(0, stat.size - chunkSize);
const fd = fs.openSync(filePath, "r");
const buffer = Buffer.alloc(chunkSize);
fs.readSync(fd, buffer, 0, chunkSize, start);
fs.closeSync(fd);
const lines = buffer
.toString("utf8")
.split(/\r?\n/)
.filter((l) => l.length > 0);
const slice = lines.slice(-maxLines);
return { lines: slice, position: stat.size };
}
/**
* @param {string[]} types
* @param {Record<string, number>} cursor byte offsets per type
* @param {number} initialLines lines to load when cursor is empty
*/
function fetchLogs(types, cursor = {}, initialLines = 300) {
const validTypes = types.filter((t) => LOG_TYPES.includes(t));
const entries = [];
const nextCursor = { ...cursor };
for (const type of validTypes) {
const filePath = resolveLogPath(type);
const prevPos = cursor[type] ?? 0;
let lines = [];
let position = 0;
if (!fs.existsSync(filePath)) {
nextCursor[type] = 0;
continue;
}
if (prevPos === 0 && initialLines > 0) {
const tail = readLastLines(filePath, initialLines);
lines = tail.lines;
position = tail.position;
} else if (prevPos > 0) {
const chunk = readFromByte(filePath, prevPos);
lines = chunk.lines;
position = chunk.position;
} else {
// No tail requested and no cursor — skip content, anchor at EOF
position = fs.statSync(filePath).size;
}
nextCursor[type] = position;
for (const line of lines) {
const entry = parseLogLine(line, type);
if (entry) entries.push(entry);
}
}
entries.sort((a, b) => {
const ta = a.timestamp ? Date.parse(a.timestamp) : 0;
const tb = b.timestamp ? Date.parse(b.timestamp) : 0;
return ta - tb;
});
return { entries, cursor: nextCursor };
}
function getLogMeta() {
const date = todayDate();
return LOG_TYPES.map((type) => {
const filePath = resolveLogPath(type);
const exists = fs.existsSync(filePath) && fs.statSync(filePath).size > 0;
return {
type,
date,
file: exists ? path.basename(filePath) : null,
size: exists ? fs.statSync(filePath).size : 0,
};
});
}
module.exports = {
LOG_ROOT,
LOG_TYPES,
fetchLogs,
getLogMeta,
getLogPath,
resolveLogPath,
};
+270
View File
@@ -0,0 +1,270 @@
const fs = require("fs");
const path = require("path");
const PLACEHOLDER_PATTERNS = [
/ROLE_ID/i,
/CHANNEL_ID/i,
/CATEGORY_ID/i,
/TAG_ID/i,
/FORUM_CHANNEL_ID/i,
/BOT_TOKEN/i,
/MODEL_API_KEY/i,
/PASTEBIN_API_KEY/i,
/YOUR_/i,
/PUT_RANDOM_STRING/i,
/example\.com/i,
];
function isEmpty(value) {
if (value == null) return true;
if (typeof value === "boolean") return false;
if (typeof value === "number") return false;
return String(value).trim() === "";
}
function isPlaceholder(value) {
if (isEmpty(value)) return true;
const s = String(value).trim();
return PLACEHOLDER_PATTERNS.some((p) => p.test(s));
}
function isDiscordId(value) {
return /^\d{17,20}$/.test(String(value).trim());
}
function isConfiguredId(value) {
return isDiscordId(value) && !isPlaceholder(value);
}
function isValidToken(value) {
if (isPlaceholder(value)) return false;
const s = String(value).trim();
return s.length >= 50 && s.includes(".");
}
function scoreChecks(checks) {
const total = checks.length;
const passed = checks.filter((c) => c.ok).length;
if (total === 0) {
return { status: "done", subtitle: "Ready", passed: 0, total: 0 };
}
if (passed === 0) {
const hint = checks.find((c) => !c.ok)?.hint ?? "Not configured yet";
return { status: "todo", subtitle: hint, passed, total };
}
if (passed === total) {
return { status: "done", subtitle: "Configured and ready", passed, total };
}
const next = checks.find((c) => !c.ok);
return {
status: "progress",
subtitle: next?.hint ?? `${passed} of ${total} steps complete`,
passed,
total,
};
}
function evaluateTickets(supportbot, ticketPanel) {
const ticket = supportbot?.Ticket ?? {};
const ticketType = String(ticket.TicketType || "channels").toLowerCase();
return scoreChecks([
{
ok: ticketPanel?.Panel === true,
hint: "Enable the ticket panel",
},
{
ok: isConfiguredId(ticket.TicketHome),
hint: "Set TicketHome channel ID",
},
{
ok:
ticketType !== "channels" ||
isConfiguredId(ticket.TicketChannelsCategory),
hint: "Set ticket category ID",
},
{
ok: !isPlaceholder(ticketPanel?.Button?.Text),
hint: "Configure panel button text",
},
]);
}
function evaluateSupportbot(supportbot) {
const roles = supportbot?.Roles?.StaffMember ?? {};
const welcome = supportbot?.Welcome ?? {};
return scoreChecks([
{ ok: isValidToken(supportbot?.General?.Token), hint: "Add your bot token" },
{
ok:
isConfiguredId(roles.Staff) ||
isConfiguredId(roles.Admin) ||
isConfiguredId(roles.Moderator),
hint: "Set staff role IDs",
},
{
ok: !welcome.Enabled || isConfiguredId(welcome.Channel),
hint: "Set welcome channel ID",
},
{
ok: !isPlaceholder(supportbot?.Activity?.Status),
hint: "Set bot activity status",
},
]);
}
function evaluateTranscripts(supportbot, transcriptCount) {
const ticketReady = isConfiguredId(supportbot?.Ticket?.TicketHome);
if (!ticketReady) {
// Ticket setup is tracked by the tickets module — avoid a misleading transcripts alert.
return {
status: "done",
subtitle: "Available after ticket channels are configured",
passed: 0,
total: 0,
};
}
if (transcriptCount > 0) {
return {
status: "done",
subtitle: `${transcriptCount} transcript${transcriptCount === 1 ? "" : "s"} saved`,
passed: 1,
total: 1,
};
}
return {
status: "done",
subtitle: "No transcripts yet — they are created when tickets close",
passed: 1,
total: 1,
};
}
function evaluateSuggestions(supportbot) {
const sug = supportbot?.Suggestions ?? {};
const mode = String(sug.Mode || "Channel").toLowerCase();
const channelOk =
mode === "forum"
? isConfiguredId(sug.ForumChannel)
: isConfiguredId(sug.Channel);
return scoreChecks([
{
ok: channelOk,
hint:
mode === "forum"
? "Set suggestions forum channel"
: "Set suggestions channel ID",
},
{
ok: !isPlaceholder(sug.UpvoteEmoji) && !isPlaceholder(sug.DownvoteEmoji),
hint: "Configure voting emojis",
},
]);
}
function evaluateAi(supportbotAi) {
if (supportbotAi?.Enabled !== true) {
return {
status: "progress",
subtitle: "AI is disabled in config",
passed: 0,
total: 3,
};
}
return scoreChecks([
{
ok: !isPlaceholder(supportbotAi?.General?.Model_API_Key),
hint: "Add your model API key",
},
{
ok: isConfiguredId(supportbotAi?.Channels?.AIChannel),
hint: "Set AI channel ID",
},
{
ok: !isPlaceholder(supportbotAi?.General?.Model),
hint: "Choose an AI model",
},
]);
}
function evaluateCommands(commands) {
const entries = Object.entries(commands || {}).filter(
([, v]) => v && typeof v === "object" && "Enabled" in v,
);
if (!entries.length) {
return { status: "todo", subtitle: "No commands configured", passed: 0, total: 0 };
}
const enabled = entries.filter(([, v]) => v.Enabled !== false).length;
const total = entries.length;
const ratio = enabled / total;
if (ratio >= 0.85) {
return {
status: "done",
subtitle: `${enabled} of ${total} commands enabled`,
passed: enabled,
total,
};
}
if (enabled === 0) {
return {
status: "todo",
subtitle: "Enable slash commands",
passed: 0,
total,
};
}
return {
status: "progress",
subtitle: `${enabled} of ${total} commands enabled`,
passed: enabled,
total,
};
}
function countTranscripts() {
const dir = path.resolve(__dirname, "../Data/Transcripts");
if (!fs.existsSync(dir)) return 0;
return fs
.readdirSync(dir)
.filter((f) => f.endsWith("-transcript.html")).length;
}
function evaluateAllModules(configStore, options = {}) {
const supportbot = configStore.supportbot;
const ticketPanel = configStore.ticketPanel;
const supportbotAi = configStore.supportbotAi;
const commands = configStore.commands;
const transcriptCount =
options.transcriptCount ?? countTranscripts();
return {
tickets: evaluateTickets(supportbot, ticketPanel),
supportbot: evaluateSupportbot(supportbot),
transcripts: evaluateTranscripts(supportbot, transcriptCount),
suggestions: evaluateSuggestions(supportbot),
ai: evaluateAi(supportbotAi),
commands: evaluateCommands(commands),
};
}
module.exports = {
evaluateAllModules,
isPlaceholder,
isConfiguredId,
};
+63
View File
@@ -0,0 +1,63 @@
const notificationStore = require("./NotificationStore.js");
const { fetchUpdateInfo } = require("./UpdateChecker.js");
function notifyBotRestart(success, errorMessage) {
if (success) {
return notificationStore.add({
type: "bot",
title: "Bot restarted",
message:
"Discord bot reconnected with your latest config. API and dashboard stayed online.",
});
}
return notificationStore.add({
type: "error",
title: "Bot failed to restart",
message: errorMessage || "Check the logs for details.",
href: "/logs",
});
}
function notifyUpdateAvailable(info) {
return notificationStore.add({
id: "update-available",
type: "update",
title: "Update available",
message: `Version ${info.latest} is ready (you are on ${info.current}).`,
href: "/settings",
});
}
function notifyUpdateInstalled(message) {
return notificationStore.add({
type: "update",
title: "Update installed",
message:
message ||
"Files were updated. Restart your server (npm start) to finish applying changes.",
href: "/settings",
});
}
function clearUpdateAvailable() {
notificationStore.dismiss("update-available");
}
async function runUpdateCheck() {
const info = await fetchUpdateInfo();
if (info.updateAvailable) {
notifyUpdateAvailable(info);
} else {
clearUpdateAvailable();
}
return info;
}
module.exports = {
notifyBotRestart,
notifyUpdateAvailable,
notifyUpdateInstalled,
clearUpdateAvailable,
runUpdateCheck,
};
+86
View File
@@ -0,0 +1,86 @@
const MAX_NOTIFICATIONS = 50;
/** @type {Map<string, object>} */
const byId = new Map();
/** @type {string[]} */
let order = [];
/**
* @param {object} input
* @param {string} [input.id] Stable id replaces existing (e.g. update-available)
*/
function add(input) {
const id =
input.id ||
`n-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const item = {
id,
type: input.type || "info",
title: input.title || "Notification",
message: input.message || "",
href: input.href || null,
createdAt: input.createdAt || Date.now(),
read: false,
};
if (byId.has(id)) {
order = order.filter((x) => x !== id);
}
byId.set(id, item);
order.unshift(id);
while (order.length > MAX_NOTIFICATIONS) {
const removed = order.pop();
if (removed) byId.delete(removed);
}
return item;
}
function list() {
return order.map((id) => byId.get(id)).filter(Boolean);
}
function unreadCount() {
let n = 0;
for (const item of byId.values()) {
if (!item.read) n++;
}
return n;
}
function markRead(ids) {
if (ids === "all") {
for (const item of byId.values()) {
item.read = true;
}
return;
}
for (const id of ids) {
const item = byId.get(id);
if (item) item.read = true;
}
}
function dismiss(id) {
if (!byId.has(id)) return false;
byId.delete(id);
order = order.filter((x) => x !== id);
return true;
}
function clear() {
byId.clear();
order = [];
}
module.exports = {
add,
list,
unreadCount,
markRead,
dismiss,
clear,
};
+32 -62
View File
@@ -1,76 +1,46 @@
// SupportBot | Emerald Services
// Ticket ID Structure
const fs = require("fs");
const Discord = require("discord.js");
const Command = require("./Command.js");
const Client = require("./Client.js");
const Event = require("./Event.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const db = require("./Database");
class TicketNumberID {
static pad(guildId) {
let n = this.get(guildId) + '';
let z = '0';
let width = 4;
setTimeout(() => {
this.set(guildId, this.get(guildId) + 1);
}, 50);
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
}
static async pad() {
let num = await this.get();
let n = num + "";
await this.set(num + 1);
return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n;
}
static get(guildId) {
let ticketnumberID = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
if (!ticketnumberID[guildId]) {
ticketnumberID[guildId] = {
id: 0
}
fs.writeFileSync("./Data/TicketData.json", JSON.stringify(ticketnumberID, null, 4), err => {
if (err) console.log(err);
});
return 0;
} else {
return ticketnumberID[guildId].id;
}
}
static set(guildId, value) {
let ticketnumberID = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
ticketnumberID[guildId] = {
id: value
static async get() {
const settings = db.getSettings ? db.getSettings() : {};
if (typeof settings.ticket_counter === "number") {
return settings.ticket_counter;
}
fs.writeFileSync("./Data/TicketData.json", JSON.stringify(ticketnumberID, null, 4), err => {
if (err) console.log(err);
});
return value;
}
static reset(guildId) {
let ticketnumberID = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
ticketnumberID[guildId] = {
id: 0
const row = db.getTicketPanel ? db.getTicketPanel() : null;
if (row && typeof row.message_id === "string" && row.message_id.startsWith("panel-")) {
const parsed = parseInt(row.message_id.replace("panel-", ""), 10);
if (!isNaN(parsed)) return parsed;
}
fs.writeFileSync("./Data/TicketData.json", JSON.stringify(ticketnumberID, null, 4), err => {
if (err) console.log(err);
});
if (row && typeof row.id === "number" && row.id < 1000000) {
return row.id;
}
return 0;
}
}
static async set(value) {
const settings = db.getSettings ? db.getSettings() : {};
settings.ticket_counter = value;
if (db.saveSettings) db.saveSettings(settings);
if (db.saveTicketPanel) db.saveTicketPanel(`panel-${value}`, "none");
return value;
}
static async reset() {
return await this.set(0);
}
}
module.exports = TicketNumberID;
module.exports = TicketNumberID;
+293
View File
@@ -0,0 +1,293 @@
const fs = require("fs");
const path = require("path");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const db = require("./Database.js");
const supportbot = require("./ConfigStore").supportbot;
const msgconfig = require("./ConfigStore").messages;
const { createTranscriptHTML } = require("./TranscriptTemplate.js");
function escapeHtml(str = "") {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function parseMarkdown(content = "") {
let safe = escapeHtml(content);
safe = safe.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
safe = safe.replace(/`(.*?)`/g, "<code>$1</code>");
safe = safe.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
safe = safe.replace(/\*(.*?)\*/g, "<em>$1</em>");
safe = safe.replace(/_(.*?)_/g, "<em>$1</em>");
safe = safe.replace(
/\[(.*?)\]\((.*?)\)/g,
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
);
return safe.replace(/\n/g, "<br>");
}
function formatDate(iso) {
try {
return new Date(iso).toLocaleString("en-GB", {
dateStyle: "medium",
timeStyle: "short",
});
} catch {
return iso;
}
}
function getFileTypeInfo(att) {
const fileName = String(att.name || "attachment").toLowerCase();
if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) {
return "image";
}
if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) {
return "video";
}
if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) {
return "audio";
}
return "file";
}
function renderAttachment(att) {
const url = escapeHtml(att.url || "");
const name = escapeHtml(att.name || "attachment");
const type = getFileTypeInfo(att);
if (type === "image") {
return `
<div class="attachment">
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
<div class="attachment-preview attachment-image">
<img src="${url}" alt="${name}" loading="lazy">
</div>
</div>
`;
}
if (type === "video") {
return `
<div class="attachment">
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
<div class="attachment-preview">
<video controls preload="metadata">
<source src="${url}">
</video>
</div>
</div>
`;
}
if (type === "audio") {
return `
<div class="attachment">
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
<div class="attachment-preview">
<audio controls preload="metadata">
<source src="${url}">
</audio>
</div>
</div>
`;
}
return `
<div class="attachment">
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
</div>
`;
}
function renderEmbeds(embeds = []) {
return embeds
.map((embed) => {
const fields = (embed.fields || [])
.map(
(field) => `
<div class="embed-field">
<div class="embed-field-name">${escapeHtml(field.name || "Field")}</div>
<div class="embed-field-value">${parseMarkdown(field.value || "")}</div>
</div>
`,
)
.join("");
return `
<div class="discord-embed">
${embed.title ? `<div class="discord-embed-title">${escapeHtml(embed.title)}</div>` : ""}
${embed.description ? `<div class="discord-embed-description">${parseMarkdown(embed.description)}</div>` : ""}
${fields}
</div>
`;
})
.join("");
}
async function createTranscript(interaction, ticketInput, reasonInput) {
let ticketRecord = typeof ticketInput === "object" && ticketInput !== null ? ticketInput : null;
let closeReason = typeof ticketInput === "string" ? ticketInput : reasonInput;
if (!ticketRecord && typeof db.getTicket === "function") {
ticketRecord = db.getTicket(interaction.channel.id) || {};
}
const { getChannel } = interaction.client;
const transcriptChannel = await getChannel(
supportbot.Ticket.Log.TicketDataLog,
interaction.guild,
);
if (!transcriptChannel) {
throw new Error("Transcript log channel missing or inaccessible.");
}
let allMessages = [];
let lastId = null;
while (true) {
const options = { limit: 100 };
if (lastId) options.before = lastId;
const messages = await interaction.channel.messages.fetch(options);
if (messages.size === 0) break;
allMessages = [...allMessages, ...messages.values()];
lastId = messages.last().id;
if (messages.size < 100) break;
}
allMessages.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
const transcriptData = allMessages.map((msg) => ({
content: msg.content || "",
username: msg.author.username,
userId: msg.author.id,
avatar: msg.author.displayAvatarURL({ extension: "png", size: 128 }),
timestamp: msg.createdAt.toISOString(),
attachments: Array.from(msg.attachments.values()).map((att) => ({
url: att.url,
name: att.name || "attachment",
contentType: att.contentType || null,
size: att.size || 0,
})),
embeds: msg.embeds.map((embed) => ({
title: embed.title || "",
description: embed.description || "",
fields: embed.fields || [],
})),
}));
if (typeof db.updateTicketStatus === "function") {
db.updateTicketStatus(interaction.channel.id, "closed");
}
let ticketUserId = ticketRecord?.user_id || ticketRecord?.user || ticketRecord?.author_id;
if (!ticketUserId && interaction.channel.permissionOverwrites) {
const memberOverwrite = interaction.channel.permissionOverwrites.cache.find(
(pow) => pow.type === 1 && pow.id !== interaction.client.user.id
);
if (memberOverwrite) {
ticketUserId = memberOverwrite.id;
}
}
if (!ticketUserId && allMessages.length > 0) {
const firstNonBotMsg = allMessages.find((m) => !m.author.bot);
if (firstNonBotMsg) {
ticketUserId = firstNonBotMsg.author.id;
}
}
const tUser =
(ticketUserId ? interaction.client.users.cache.get(ticketUserId) : null) ||
(ticketUserId ? await interaction.client.users.fetch(ticketUserId).catch(() => null) : null);
const transcriptEmbed = new Discord.EmbedBuilder()
.setTitle(msgconfig.TicketLog.Title)
.setColor(msgconfig.TicketLog.Colour)
.setFooter({
text: supportbot.Embed.Footer,
iconURL: interaction.user.displayAvatarURL(),
})
.setDescription(
`> **Ticket:** ${interaction.channel.name} (\`${interaction.channel.id}\`)\n` +
`> **User:** ${tUser ? `<@${tUser.id}> (\`${tUser.tag}\`)` : "Unknown User"}\n` +
`> **Closed by:** <@${interaction.user.id}>\n` +
`> **Message Count:** ${transcriptData.length}`,
)
.addFields({
name: "Reason",
value: `\`\`\`${closeReason || "No Reason Provided."}\`\`\``,
inline: false,
});
try {
const configStore = require("./ConfigStore");
const transcriptStore = require("./TranscriptStore");
const apiConfig = configStore.api?.API;
const tSettings = transcriptStore.getSettings();
if (apiConfig?.Enabled !== false) {
const port = apiConfig?.Port || 3000;
const isPublic = Boolean(tSettings?.publicAccess?.enabled);
const transcriptUrl = `http://localhost:${port}/transcripts/${interaction.channel.id}`;
transcriptEmbed.addFields({
name: isPublic ? "Public Web Transcript" : "Dashboard Transcript",
value: `[${isPublic ? "Click here to view public transcript" : "Click here to view in Dashboard"}](${transcriptUrl})`,
inline: false,
});
}
} catch (err) {
console.warn("[TicketManager] Could not add web transcript URL:", err.message);
}
const transcriptDir = path.join(process.cwd(), "./Data/Transcripts");
if (!fs.existsSync(transcriptDir)) {
fs.mkdirSync(transcriptDir, { recursive: true });
}
const htmlFileName = `${interaction.channel.id}-transcript.html`;
const htmlPath = path.join(transcriptDir, htmlFileName);
const html = createTranscriptHTML(
{
id: interaction.channel.id,
name: interaction.channel.name,
messages: transcriptData,
},
closeReason,
);
fs.writeFileSync(htmlPath, html, "utf8");
await transcriptChannel.send({
embeds: [transcriptEmbed],
files: [
new Discord.AttachmentBuilder(htmlPath, {
name: `SPOILER_${htmlFileName}`,
}),
],
});
return { htmlPath };
}
module.exports = {
createTranscript,
};
+223
View File
@@ -0,0 +1,223 @@
const fs = require('fs');
const path = require('path');
const TRANSCRIPT_DIR = path.join(__dirname, '../Data/Transcripts');
const SETTINGS_FILE = path.join(__dirname, '../Data/transcript-settings.json');
const DEFAULT_SETTINGS = {
autoDelete: {
enabled: false,
afterDays: 30,
},
publicAccess: {
enabled: false,
},
};
let purgeTimer = null;
function ensureTranscriptDir() {
if (!fs.existsSync(TRANSCRIPT_DIR)) {
fs.mkdirSync(TRANSCRIPT_DIR, { recursive: true });
}
}
function getSettings() {
try {
if (!fs.existsSync(SETTINGS_FILE)) {
return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
}
const raw = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
const days = Number(raw?.autoDelete?.afterDays);
return {
autoDelete: {
enabled: Boolean(raw?.autoDelete?.enabled),
afterDays:
Number.isFinite(days) && days >= 1
? Math.min(Math.floor(days), 3650)
: DEFAULT_SETTINGS.autoDelete.afterDays,
},
publicAccess: {
enabled: Boolean(raw?.publicAccess?.enabled),
},
};
} catch {
return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
}
}
function saveSettings(patch) {
const current = getSettings();
const next = {
autoDelete: {
enabled:
patch?.autoDelete?.enabled !== undefined
? Boolean(patch.autoDelete.enabled)
: current.autoDelete.enabled,
afterDays:
patch?.autoDelete?.afterDays !== undefined
? Math.min(
Math.max(Math.floor(Number(patch.autoDelete.afterDays) || 1), 1),
3650,
)
: current.autoDelete.afterDays,
},
publicAccess: {
enabled:
patch?.publicAccess?.enabled !== undefined
? Boolean(patch.publicAccess.enabled)
: current.publicAccess.enabled,
},
};
const dir = path.dirname(SETTINGS_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2), 'utf8');
return next;
}
function normalizeId(rawId) {
return String(rawId).replace(/[^0-9]/g, '');
}
function readTranscriptFile(rawId) {
const id = normalizeId(rawId);
if (!id) return null;
ensureTranscriptDir();
const filePath = path.join(TRANSCRIPT_DIR, `${id}-transcript.html`);
if (!fs.existsSync(filePath)) return null;
const content = fs.readFileSync(filePath, 'utf8');
const titleMatch = content.match(/<title>\s*Transcript\s*-\s*([^<]+)\s*<\/title>/i);
return {
id,
filePath,
filename: `${id}-transcript.html`,
content,
ticketName: titleMatch ? titleMatch[1].trim() : null,
};
}
function listTranscripts() {
ensureTranscriptDir();
if (!fs.existsSync(TRANSCRIPT_DIR)) {
return [];
}
return fs
.readdirSync(TRANSCRIPT_DIR)
.filter((f) => f.endsWith('-transcript.html'))
.map((f) => {
const id = f.replace('-transcript.html', '');
const stats = fs.statSync(path.join(TRANSCRIPT_DIR, f));
const parsed = readTranscriptFile(id);
return {
id,
filename: f,
createdAt: stats.mtime.toISOString(),
size: stats.size,
ticketName: parsed?.ticketName ?? null,
};
})
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}
function deleteTranscript(rawId) {
const id = normalizeId(rawId);
if (!id) return false;
const filePath = path.join(TRANSCRIPT_DIR, `${id}-transcript.html`);
if (!fs.existsSync(filePath)) return false;
fs.unlinkSync(filePath);
return true;
}
function purgeExpiredTranscripts() {
const settings = getSettings();
if (!settings.autoDelete.enabled) {
return { deleted: 0, skipped: true };
}
ensureTranscriptDir();
const maxAgeMs = settings.autoDelete.afterDays * 24 * 60 * 60 * 1000;
const cutoff = Date.now() - maxAgeMs;
let deleted = 0;
for (const f of fs.readdirSync(TRANSCRIPT_DIR)) {
if (!f.endsWith('-transcript.html')) continue;
const filePath = path.join(TRANSCRIPT_DIR, f);
const stats = fs.statSync(filePath);
if (stats.mtime.getTime() < cutoff) {
fs.unlinkSync(filePath);
deleted += 1;
}
}
return { deleted, skipped: false };
}
function notFoundHtml(message = 'Transcript not found') {
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Transcript</title>
<style>body{font-family:system-ui,sans-serif;padding:2rem;background:#0b0f14;color:#e6edf3;margin:0}</style></head>
<body><h1>${message}</h1></body></html>`;
}
function servePublicTranscript(req, res) {
const settings = getSettings();
if (!settings.publicAccess.enabled) {
return res.status(404).type('html').send(notFoundHtml('Transcript not found'));
}
const transcript = readTranscriptFile(req.params.id);
if (!transcript) {
return res.status(404).type('html').send(notFoundHtml('Transcript not found'));
}
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=120');
return res.send(transcript.content);
}
function startAutoDeleteScheduler(intervalMs = 60 * 60 * 1000) {
if (purgeTimer) return;
const run = () => {
try {
const result = purgeExpiredTranscripts();
if (result.deleted > 0) {
console.log(
`[Transcripts] Auto-deleted ${result.deleted} transcript(s) older than ${getSettings().autoDelete.afterDays} day(s)`,
);
}
} catch (err) {
console.error('[Transcripts] Auto-delete failed:', err.message);
}
};
run();
purgeTimer = setInterval(run, intervalMs);
if (typeof purgeTimer.unref === 'function') {
purgeTimer.unref();
}
}
function getPublicUrl(req, id) {
const settings = getSettings();
if (!settings.publicAccess.enabled) return null;
const host = req.get('host');
const proto = req.get('x-forwarded-proto') || req.protocol || 'http';
return `${proto}://${host}/transcripts/${id}`;
}
module.exports = {
TRANSCRIPT_DIR,
DEFAULT_SETTINGS,
getSettings,
saveSettings,
readTranscriptFile,
listTranscripts,
deleteTranscript,
purgeExpiredTranscripts,
servePublicTranscript,
startAutoDeleteScheduler,
getPublicUrl,
};
+721
View File
@@ -0,0 +1,721 @@
const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
const {
normalizeFontFamily,
resolveTypography,
} = require("./googleFonts.js");
const TEMPLATE_PATH = path.join(__dirname, "../Configs/transcript-template.yml");
let cachedTemplate = null;
function escapeHtml(str = "") {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function parseMarkdown(content = "") {
let safe = escapeHtml(content);
safe = safe.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
safe = safe.replace(/`(.*?)`/g, "<code>$1</code>");
safe = safe.replace(/\|\|(.*?)\|\|/g, '<span class="spoiler">$1</span>');
safe = safe.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
safe = safe.replace(/\*(.*?)\*/g, "<em>$1</em>");
safe = safe.replace(/_(.*?)_/g, "<em>$1</em>");
safe = safe.replace(/~~(.*?)~~/g, "<del>$1</del>");
safe = safe.replace(/__(.*?)__/g, "<u>$1</u>");
safe = safe.replace(/&lt;@!?(\d+)&gt;/g, '<span class="mention">@User</span>');
safe = safe.replace(/&lt;@&amp;(\d+)&gt;/g, '<span class="mention">@Role</span>');
safe = safe.replace(
/\[(.*?)\]\((.*?)\)/g,
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
);
return safe.replace(/\n/g, "<br>");
}
function formatDate(iso) {
try {
return new Date(iso).toLocaleString("en-GB", {
dateStyle: "medium",
timeStyle: "short",
});
} catch {
return iso;
}
}
function getFileTypeInfo(att) {
const fileName = String(att.name || "attachment").toLowerCase();
if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) {
return "image";
}
if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) {
return "video";
}
if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) {
return "audio";
}
return "file";
}
function renderAttachment(att) {
const url = escapeHtml(att.url || "");
const name = escapeHtml(att.name || "attachment");
const type = getFileTypeInfo(att);
if (type === "image") {
return `
<div class="attachment">
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
<div class="attachment-preview attachment-image">
<img src="${url}" alt="${name}" loading="lazy">
</div>
</div>
`;
}
if (type === "video") {
return `
<div class="attachment">
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
<div class="attachment-preview">
<video controls preload="metadata">
<source src="${url}">
</video>
</div>
</div>
`;
}
if (type === "audio") {
return `
<div class="attachment">
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
<div class="attachment-preview">
<audio controls preload="metadata">
<source src="${url}">
</audio>
</div>
</div>
`;
}
return `
<div class="attachment">
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
</div>
`;
}
function renderEmbeds(embeds = []) {
return embeds
.map((embed) => {
const color = embed.color || "#5865F2";
const fields = (embed.fields || [])
.map(
(field) => `
<div class="embed-field ${field.inline ? 'inline-field' : ''}">
<div class="embed-field-name">${escapeHtml(field.name || "Field")}</div>
<div class="embed-field-value">${parseMarkdown(field.value || "")}</div>
</div>
`,
)
.join("");
const author = embed.author ? `
<div class="embed-author">
${embed.author.iconURL ? `<img src="${escapeHtml(embed.author.iconURL)}" class="embed-author-icon" />` : ''}
<span>${escapeHtml(embed.author.name || '')}</span>
</div>
` : '';
const thumbnail = embed.thumbnail?.url ? `
<img src="${escapeHtml(embed.thumbnail.url)}" class="embed-thumbnail" />
` : '';
const image = embed.image?.url ? `
<div class="embed-image"><img src="${escapeHtml(embed.image.url)}" /></div>
` : '';
const footer = (embed.footer || embed.timestamp) ? `
<div class="embed-footer">
${embed.footer?.iconURL ? `<img src="${escapeHtml(embed.footer.iconURL)}" class="embed-footer-icon" />` : ''}
${embed.footer?.text ? `<span>${escapeHtml(embed.footer.text)}</span>` : ''}
</div>
` : '';
return `
<div class="discord-embed" style="border-left-color: ${escapeHtml(color)}; border-left-width: 4px; border-left-style: solid; position: relative;">
${author}
${thumbnail}
${embed.title ? `<div class="discord-embed-title">${parseMarkdown(embed.title)}</div>` : ""}
${embed.description ? `<div class="discord-embed-description">${parseMarkdown(embed.description)}</div>` : ""}
${fields ? `<div class="embed-fields-grid">${fields}</div>` : ""}
${image}
${footer}
</div>
`;
})
.join("");
}
function deepMerge(base, patch) {
const out = structuredClone(base);
for (const [key, val] of Object.entries(patch || {})) {
if (
val &&
typeof val === "object" &&
!Array.isArray(val) &&
out[key] &&
typeof out[key] === "object" &&
!Array.isArray(out[key])
) {
out[key] = deepMerge(out[key], val);
} else {
out[key] = val;
}
}
return out;
}
function getDefaultTemplate() {
return {
Theme: {
Background: "#0b0f14",
Panel: "#111821",
PanelSecondary: "#182230",
PanelTertiary: "#1d2938",
Border: "#283548",
Text: "#e6edf3",
Muted: "#9fb0c3",
Accent: "#22c55e",
AccentSoft: "rgba(34, 197, 94, 0.12)",
AccentBorder: "rgba(34, 197, 94, 0.25)",
AccentText: "#8df0ad",
Link: "#8fd9ff",
AttachmentLink: "#8fd9a8",
CodeBackground: "#0f1720",
},
Typography: {
FontFamily: "Inter",
},
Header: {
Title: "Ticket Transcript",
Subtitle:
"Exported conversation log for support ticket **{{ticketName}}**",
Badge: "Closed Transcript",
ShowMetaCards: true,
},
Meta: {
ChannelLabel: "Channel",
TicketIdLabel: "Ticket ID",
MessageCountLabel: "Message Count",
CloseReasonLabel: "Close Reason",
DefaultCloseReason: "No Reason Provided.",
},
Messages: {
SectionTitle: "Messages",
EmptyState: "No messages were found in this ticket.",
},
Advanced: {
CustomCss: "",
},
};
}
function normalizeTemplate(raw) {
const merged = deepMerge(getDefaultTemplate(), raw || {});
if (merged.Typography) {
merged.Typography.FontFamily = normalizeFontFamily(
merged.Typography.FontFamily,
);
}
return merged;
}
function loadTemplateFromDisk() {
try {
if (!fs.existsSync(TEMPLATE_PATH)) {
const defaults = getDefaultTemplate();
fs.mkdirSync(path.dirname(TEMPLATE_PATH), { recursive: true });
fs.writeFileSync(TEMPLATE_PATH, yaml.dump(defaults), "utf8");
return defaults;
}
const parsed = yaml.load(fs.readFileSync(TEMPLATE_PATH, "utf8"));
return normalizeTemplate(parsed);
} catch (err) {
console.error("[TranscriptTemplate] Failed to load template:", err.message);
return getDefaultTemplate();
}
}
function getTemplate() {
if (!cachedTemplate) {
cachedTemplate = loadTemplateFromDisk();
}
return structuredClone(cachedTemplate);
}
function reloadTemplate() {
cachedTemplate = loadTemplateFromDisk();
return getTemplate();
}
function saveTemplate(data) {
const merged = normalizeTemplate(data);
fs.mkdirSync(path.dirname(TEMPLATE_PATH), { recursive: true });
fs.writeFileSync(TEMPLATE_PATH, yaml.dump(merged), "utf8");
cachedTemplate = merged;
return merged;
}
function applyPlaceholders(text, ctx) {
return String(text || "")
.replace(/\{\{ticketName\}\}/g, ctx.name || "")
.replace(/\{\{ticketId\}\}/g, ctx.id || "")
.replace(/\{\{messageCount\}\}/g, String(ctx.messageCount ?? 0))
.replace(/\{\{reason\}\}/g, ctx.reason || "");
}
function formatHeaderText(text, ctx) {
const withPlaceholders = applyPlaceholders(text, ctx);
const escaped = escapeHtml(withPlaceholders);
return escaped.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
}
function buildStyles(template) {
const t = template.Theme;
const { fontFamilyCss } = resolveTypography(template.Typography?.FontFamily);
const custom = String(template.Advanced?.CustomCss || "").trim();
return `
:root {
--bg: ${t.Background};
--panel: ${t.Panel};
--panel-2: ${t.PanelSecondary};
--panel-3: ${t.PanelTertiary};
--border: ${t.Border};
--text: ${t.Text};
--muted: ${t.Muted};
--accent: ${t.Accent};
--accent-soft: ${t.AccentSoft};
--accent-border: ${t.AccentBorder};
--accent-text: ${t.AccentText};
--link: ${t.Link};
--attachment-link: ${t.AttachmentLink};
--code: ${t.CodeBackground};
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
font-family: ${fontFamilyCss};
}
body { padding: 32px 20px; }
.page { max-width: 1100px; margin: 0 auto; }
.header {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 16px;
padding: 24px;
margin-bottom: 18px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
flex-wrap: wrap;
}
.title-wrap h1 {
margin: 0;
font-size: 1.8rem;
line-height: 1.2;
font-weight: 800;
letter-spacing: -0.02em;
}
.title-wrap p {
margin: 8px 0 0;
color: var(--muted);
font-size: 0.98rem;
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--accent-soft);
color: var(--accent-text);
border: 1px solid var(--accent-border);
padding: 8px 12px;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 700;
white-space: nowrap;
}
.meta-grid {
margin-top: 18px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.meta-card {
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px;
}
.meta-label {
color: var(--muted);
font-size: 0.82rem;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 700;
}
.meta-value {
font-size: 0.98rem;
font-weight: 600;
word-break: break-word;
}
.messages-wrap {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
}
.messages-header {
padding: 18px 20px;
border-bottom: 1px solid var(--border);
background: var(--panel-2);
font-weight: 800;
font-size: 1rem;
}
.message {
display: flex;
gap: 14px;
padding: 18px 20px;
border-bottom: 1px solid var(--border);
}
.message:last-child { border-bottom: none; }
.avatar {
width: 42px;
height: 42px;
min-width: 42px;
border-radius: 50%;
object-fit: cover;
background: var(--panel-3);
border: 1px solid var(--border);
}
.message-body { min-width: 0; flex: 1; }
.message-header {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.username {
font-weight: 800;
color: var(--text);
font-size: 0.98rem;
}
.userid {
color: var(--muted);
font-size: 0.8rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.timestamp {
color: var(--muted);
font-size: 0.82rem;
margin-left: auto;
}
.content {
color: var(--text);
font-size: 0.96rem;
line-height: 1.6;
word-break: break-word;
}
.content:empty { display: none; }
.discord-embed {
margin-top: 12px;
background: var(--panel-2);
border: 1px solid var(--border);
border-left: 4px solid var(--accent);
border-radius: 10px;
padding: 12px 14px;
}
.discord-embed-title { font-weight: 800; margin-bottom: 6px; }
.discord-embed-description { color: var(--text); line-height: 1.5; }
.embed-author { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 0.9em; margin-bottom: 6px; }
.embed-author-icon { width: 18px; height: 18px; border-radius: 50%; }
.embed-fields-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.06); }
.embed-thumbnail { position: absolute; top: 12px; right: 12px; width: 44px; height: 44px; border-radius: 6px; object-fit: cover; }
.embed-image img { width: 100%; max-height: 300px; object-fit: cover; border-radius: 8px; margin-top: 10px; }
.embed-footer { display: flex; align-items: center; gap: 6px; font-size: 0.8em; color: var(--muted); margin-top: 8px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.04); }
.embed-footer-icon { width: 14px; height: 14px; border-radius: 50%; }
.mention { background: rgba(88, 101, 242, 0.2); color: #5865F2; padding: 2px 6px; border-radius: 4px; font-weight: 600; font-size: 0.9em; }
.spoiler { background: #202225; color: transparent; border-radius: 4px; padding: 0 4px; cursor: pointer; }
.spoiler:hover { color: inherit; background: rgba(255,255,255,0.1); }
.embed-field {
margin-top: 6px;
}
.embed-field-name { font-weight: 700; margin-bottom: 4px; font-size: 0.85em; text-transform: uppercase; color: var(--muted); }
.embed-field-value { color: var(--text); line-height: 1.5; }
.attachment {
margin-top: 12px;
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px;
}
.attachment-file {
display: inline-block;
color: var(--attachment-link);
text-decoration: none;
font-weight: 700;
word-break: break-word;
}
.attachment-file:hover { text-decoration: underline; }
.attachment-preview { margin-top: 10px; }
.attachment-preview img,
.attachment-preview video {
width: 100%;
max-width: 560px;
border-radius: 10px;
border: 1px solid var(--border);
display: block;
background: #0a0f15;
}
.attachment-preview audio {
width: 100%;
max-width: 560px;
display: block;
}
code {
background: var(--code);
border: 1px solid var(--border);
padding: 2px 6px;
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.9em;
}
pre {
background: var(--code);
border: 1px solid var(--border);
padding: 12px;
border-radius: 10px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
}
pre code { background: transparent; border: none; padding: 0; }
a { color: var(--link); }
.empty-state { padding: 24px 20px; color: var(--muted); }
@media (max-width: 700px) {
body { padding: 18px 12px; }
.header, .messages-wrap { border-radius: 12px; }
.message { padding: 14px; }
.timestamp { width: 100%; margin-left: 0; }
}
${custom}
`;
}
function getSampleTicket() {
return {
id: "123456789012345678",
name: "ticket-support-demo",
messages: [
{
username: "SupportBot",
userId: "100000000000000001",
avatar: "https://cdn.discordapp.com/embed/avatars/0.png",
timestamp: new Date().toISOString(),
content: "Hello! Thanks for opening a ticket. How can we help?",
embeds: [],
attachments: [],
},
{
username: "Member",
userId: "100000000000000002",
avatar: "https://cdn.discordapp.com/embed/avatars/1.png",
timestamp: new Date(Date.now() - 60000).toISOString(),
content: "I need help with my purchase — order **#48291**.",
embeds: [
{
title: "Order details",
description: "Payment received · awaiting delivery",
fields: [{ name: "Status", value: "Pending" }],
},
],
attachments: [],
},
],
};
}
function createTranscriptHTML(ticket, reason, templateInput) {
const template = templateInput
? normalizeTemplate(templateInput)
: getTemplate();
const header = template.Header;
const meta = template.Meta;
const messagesCfg = template.Messages;
const closeReason = reason || meta.DefaultCloseReason;
const ctx = {
name: ticket.name,
id: ticket.id,
messageCount: ticket.messages.length,
reason: closeReason,
};
const { headLinks } = resolveTypography(template.Typography?.FontFamily);
const metaSection = header.ShowMetaCards
? `
<div class="meta-grid">
<div class="meta-card">
<div class="meta-label">${escapeHtml(meta.ChannelLabel)}</div>
<div class="meta-value">${escapeHtml(ticket.name)}</div>
</div>
<div class="meta-card">
<div class="meta-label">${escapeHtml(meta.TicketIdLabel)}</div>
<div class="meta-value">${escapeHtml(ticket.id)}</div>
</div>
<div class="meta-card">
<div class="meta-label">${escapeHtml(meta.MessageCountLabel)}</div>
<div class="meta-value">${ticket.messages.length}</div>
</div>
<div class="meta-card">
<div class="meta-label">${escapeHtml(meta.CloseReasonLabel)}</div>
<div class="meta-value">${escapeHtml(closeReason)}</div>
</div>
</div>
`
: "";
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - ${escapeHtml(ticket.name)}</title>
${headLinks}
<style>${buildStyles(template)}</style>
</head>
<body>
<div class="page">
<section class="header">
<div class="header-top">
<div class="title-wrap">
<h1>${escapeHtml(header.Title)}</h1>
<p>${formatHeaderText(header.Subtitle, ctx)}</p>
</div>
<div class="badge">${escapeHtml(header.Badge)}</div>
</div>
${metaSection}
</section>
<section class="messages-wrap">
<div class="messages-header">${escapeHtml(messagesCfg.SectionTitle)}</div>
${
ticket.messages.length
? ticket.messages
.map(
(msg) => `
<article class="message">
<img src="${escapeHtml(msg.avatar || "")}" alt="Avatar" class="avatar">
<div class="message-body">
<div class="message-header">
<span class="username">${escapeHtml(msg.username || "Unknown User")}</span>
<span class="userid">${escapeHtml(msg.userId || "Unknown ID")}</span>
<span class="timestamp">${formatDate(msg.timestamp)}</span>
</div>
<div class="content">${parseMarkdown(msg.content || "")}</div>
${renderEmbeds(msg.embeds || [])}
${(msg.attachments || []).map(renderAttachment).join("")}
</div>
</article>
`,
)
.join("")
: `<div class="empty-state">${escapeHtml(messagesCfg.EmptyState)}</div>`
}
</section>
</div>
</body>
</html>`;
}
function renderPreview(templateInput) {
const sample = getSampleTicket();
return createTranscriptHTML(
sample,
"Resolved — thank you for contacting support.",
templateInput,
);
}
module.exports = {
TEMPLATE_PATH,
getDefaultTemplate,
getTemplate,
reloadTemplate,
saveTemplate,
normalizeTemplate,
getSampleTicket,
createTranscriptHTML,
renderPreview,
};
+73
View File
@@ -0,0 +1,73 @@
const fs = require("fs");
const path = require("path");
const REPOSITORIES = [
{ id: "bot", name: "SupportBot", repo: "Emerald-Services/SupportBot", branch: "release" },
{ id: "dashboard", name: "SupportBot Dashboard", repo: "Emerald-Services/SupportBot-Dashboard", branch: "release" }
];
function compareVersions(a, b) {
const pa = String(a)
.replace(/^v/i, "")
.split(".")
.map((n) => parseInt(n, 10) || 0);
const pb = String(b)
.replace(/^v/i, "")
.split(".")
.map((n) => parseInt(n, 10) || 0);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
}
return 0;
}
async function fetchUpdateInfo() {
const axios = require("axios");
const pkgPath = path.join(__dirname, "../package.json");
const currentPkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
const currentVersion = currentPkg.version;
const checks = [];
for (const target of REPOSITORIES) {
let installedVersion = target.id === "bot" ? currentPkg.version : (currentPkg.dashboardVersion || "1.0.0");
let latest = installedVersion;
try {
const pkgUrl = target.id === 'dashboard'
? `https://raw.githubusercontent.com/${target.repo}/${target.branch}/dashboard/package.json`
: `https://raw.githubusercontent.com/${target.repo}/${target.branch}/package.json`;
const remoteRes = await axios.get(pkgUrl, {
timeout: 15000,
headers: { "User-Agent": "SupportBot-Updater" },
});
if (remoteRes.data && remoteRes.data.version) {
latest = remoteRes.data.version;
}
} catch (e) {
console.warn(`[Updater] Failed to check ${target.repo} for updates.`);
}
const updateAvailable = compareVersions(latest, installedVersion) > 0;
checks.push({
id: target.id,
name: target.name,
current: installedVersion,
latest,
updateAvailable,
repository: `https://github.com/${target.repo}`,
branch: target.branch,
zipUrl: `https://github.com/${target.repo}/archive/refs/heads/${target.branch}.zip`,
});
}
return checks;
}
module.exports = {
compareVersions,
fetchUpdateInfo,
REPOSITORIES,
};
+14
View File
@@ -0,0 +1,14 @@
function withTimeout(promise, ms, message) {
let timer;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new Error(message || `Timed out after ${ms}ms`));
}, ms);
});
return Promise.race([promise, timeout]).finally(() => {
clearTimeout(timer);
});
}
module.exports = { withTimeout };
+84
View File
@@ -0,0 +1,84 @@
/** Curated Google Fonts for transcript HTML (keep in sync with dashboard/src/lib/google-fonts.ts). */
const SYSTEM_FALLBACK =
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
const GOOGLE_FONTS = [
{ id: "Inter", label: "Inter" },
{ id: "Roboto", label: "Roboto" },
{ id: "Open Sans", label: "Open Sans" },
{ id: "Lato", label: "Lato" },
{ id: "Montserrat", label: "Montserrat" },
{ id: "Poppins", label: "Poppins" },
{ id: "Nunito", label: "Nunito" },
{ id: "Raleway", label: "Raleway" },
{ id: "Work Sans", label: "Work Sans" },
{ id: "Source Sans 3", label: "Source Sans 3" },
{ id: "DM Sans", label: "DM Sans" },
{ id: "Manrope", label: "Manrope" },
{ id: "Outfit", label: "Outfit" },
{ id: "Rubik", label: "Rubik" },
{ id: "Ubuntu", label: "Ubuntu" },
{ id: "Fira Sans", label: "Fira Sans" },
{ id: "PT Sans", label: "PT Sans" },
{ id: "Noto Sans", label: "Noto Sans" },
{ id: "Oswald", label: "Oswald" },
{ id: "Merriweather", label: "Merriweather" },
{ id: "Playfair Display", label: "Playfair Display" },
{ id: "Libre Baskerville", label: "Libre Baskerville" },
{ id: "JetBrains Mono", label: "JetBrains Mono" },
{ id: "Fira Code", label: "Fira Code" },
];
const GOOGLE_FONT_IDS = new Set(GOOGLE_FONTS.map((f) => f.id));
function normalizeFontFamily(fontFamily) {
if (!fontFamily || typeof fontFamily !== "string") return "Inter";
const trimmed = fontFamily.trim();
if (trimmed === "system") return "system";
if (!trimmed.includes(",")) {
return GOOGLE_FONT_IDS.has(trimmed) ? trimmed : "Inter";
}
const match = trimmed.match(/^["']?([^"',]+)["']?/);
const first = match?.[1]?.trim();
if (first && GOOGLE_FONT_IDS.has(first)) return first;
return "Inter";
}
function googleFontsCssUrl(familyIds) {
const families = familyIds
.map((id) => {
const encoded = encodeURIComponent(id).replace(/%20/g, "+");
return `family=${encoded}:wght@400;600;700;800`;
})
.join("&");
return `https://fonts.googleapis.com/css2?${families}&display=swap`;
}
function resolveTypography(fontFamily) {
const id = normalizeFontFamily(fontFamily);
if (id === "system") {
return {
fontId: "system",
fontFamilyCss: SYSTEM_FALLBACK,
headLinks: "",
};
}
const url = googleFontsCssUrl([id]);
return {
fontId: id,
fontFamilyCss: `"${id}", ${SYSTEM_FALLBACK}`,
headLinks: `<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="${url}" rel="stylesheet">`,
};
}
module.exports = {
GOOGLE_FONTS,
GOOGLE_FONT_IDS,
SYSTEM_FALLBACK,
normalizeFontFamily,
googleFontsCssUrl,
resolveTypography,
};
-30
View File
@@ -1,30 +0,0 @@
// SupportBot | Emerald Services
// Ping Command
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const cmdconfig = yaml.load(fs.readFileSync('./Data/commands.yml', 'utf8'));
const Command = require("../Structures/Command.js");
module.exports = new Command({
name: cmdconfig.PingCommand,
description: cmdconfig.PingCommandDesc,
type: "BOTH",
slashCommandOptions: [],
permission: "SEND_MESSAGES",
async run(message, args, client) {
const PingEmbed = new Discord.MessageEmbed()
.setDescription(`:ping_pong: **Ping:** \`${client.ws.ping} ms\``)
.setColor(supportbot.EmbedColour)
message.channel.send({
embeds: [PingEmbed]
});
}
});
-245
View File
@@ -1,245 +0,0 @@
// SupportBot | Emerald Services
// Ticket Command
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const cmdconfig = yaml.load(fs.readFileSync('./Data/commands.yml', 'utf8'));
const Command = require("../Structures/Command.js");
const TicketNumberID = require('../Structures/TicketID.js');
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
module.exports = new Command({
name: cmdconfig.OpenTicket,
description: cmdconfig.OpenTicketDesc,
type: "BOTH",
slashCommandOptions: [],
permission: "SEND_MESSAGES",
async run(message, args, client) {
let reactionUser = message.guild.members.cache.get(message.author.id)
if (reactionUser.roles.cache.find(role => role.name === supportbot.TicketBlackListRole) || reactionUser.roles.cache.find(role => role.id === supportbot.TicketBlackListRole)) {
return message.channel.send({content:`<@${message.author.id}> ${supportbot.TicketBlackListMessage}`}).then(msg => {
setTimeout(() => msg.delete(), 3500)
})
}
// Ticket ID
let ticketNumberID = TicketNumberID.pad(message.guild.id);
// Ticket Subject
const TicketSubject = args || supportbot.NoTicketSubject;
const TicketExists = new Discord.MessageEmbed()
.setTitle("Ticket Exists!")
.setDescription(`${supportbot.TicketExists}`)
.setColor(supportbot.WarningColour)
if (message.guild.channels.cache.find(ticketChannel => ticketChannel.name === `${supportbot.TicketChannel}-${ticketNumberID}`)) {
return message.channel.send({ embeds: [TicketExists] });
}
const Staff = message.guild.roles.cache.find(SupportTeam => SupportTeam.name === supportbot.Staff) || message.guild.roles.cache.find(SupportTeam => SupportTeam.id === supportbot.Staff)
const Admins = message.guild.roles.cache.find(AdminUser => AdminUser.name === supportbot.Admin) || message.guild.roles.cache.find(AdminUser => AdminUser.id === supportbot.Admin)
const DeptRole1 = message.guild.roles.cache.find(DepartmentRole => DepartmentRole.name === `${supportbot.DepartmentRole_1}`) || message.guild.roles.cache.find(DepartmentRole => DepartmentRole.id === `${supportbot.DepartmentRole_1}`)
const DeptRole2 = message.guild.roles.cache.find(DepartmentRole => DepartmentRole.name === `${supportbot.DepartmentRole_2}`) || message.guild.roles.cache.find(DepartmentRole => DepartmentRole.id === `${supportbot.DepartmentRole_2}`)
const DeptRole3 = message.guild.roles.cache.find(DepartmentRole => DepartmentRole.name === `${supportbot.DepartmentRole_3}`) || message.guild.roles.cache.find(DepartmentRole => DepartmentRole.id === `${supportbot.DepartmentRole_3}`)
const Author = message.author
const Everyone = message.guild.id
const ticketChannel = await message.guild.channels.create(`${supportbot.TicketChannel}-${ticketNumberID}`, {
type: "GUILD_TEXT",
})
if (message.channel.name === supportbot.ReactionChannel || message.channel.id === supportbot.ReactionChannel) {
const CreatedTicket = new Discord.MessageEmbed()
.setDescription(supportbot.TicketCreatedAlert.replace(/%ticketauthor%/g, message.author.id).replace(/%ticketid%/g, ticketChannel.id).replace(/%ticketusername%/g, message.author.username))
.setColor(supportbot.EmbedColour)
message.channel.send({embeds: [CreatedTicket]}).then(msg => {
setTimeout(() => msg.delete(), 5000)
})
} else {
const CreatedTicket = new Discord.MessageEmbed()
.setDescription(supportbot.TicketCreatedAlert.replace(/%ticketauthor%/g, message.author.id).replace(/%ticketid%/g, ticketChannel.id).replace(/%ticketusername%/g, message.author.username))
.setColor(supportbot.EmbedColour)
message.channel.send({embeds: [CreatedTicket]}).then(msg => setTimeout(() => msg.delete(), 5000));
}
let AllowArr = [Staff, Staff, Admins, Author]
await asyncForEach(AllowArr, (index) => {
ticketChannel.permissionOverwrites.create(index, {VIEW_CHANNEL: true})
})
let DenyArr = [Everyone]
await asyncForEach(DenyArr, (index) => {
ticketChannel.permissionOverwrites.create(index, {VIEW_CHANNEL: false})
})
let TicketCategory = message.guild.channels.cache.find(category => category.name === supportbot.TicketCategory) || message.guild.channels.cache.find(category => category.id === supportbot.TicketCategory)
if(TicketCategory) {
ticketChannel.setParent(TicketCategory.id)
}
if (supportbot.AllowTicketMentions) {
ticketChannel.send({content:`${message.author}`})
}
const TicketMessage = new Discord.MessageEmbed()
.setTitle(supportbot.Ticket_Title.replace(/%ticketauthor%/g, message.author.id).replace(/%ticketid%/g, ticketChannel.id).replace(/%ticketusername%/g, message.author.username))
.setDescription(supportbot.TicketMessage.replace(/%ticketauthor%/g, message.author.id).replace(/%ticketid%/g, ticketChannel.id).replace(/%ticketusername%/g, message.author.username))
.setColor(supportbot.EmbedColour)
if (supportbot.TicketSubject === "embed") {
if (TicketSubject != 'No Reason Provided.') {
TicketMessage.addFields({ name: 'Reason', value: TicketSubject })
}
}
if (supportbot.TicketSubject === "description") {
if (TicketSubject != 'No Reason Provided.') {
ticketChannel.setTopic(`Reason: ${TicketSubject} - User ID: ${message.author.id} - Ticket: ${ticketChannel.name}`)
}
}
if (supportbot.TicketDepartments) {
TicketMessage.addFields({ name: 'Departments', value: ` **${supportbot.DepartmentTitle_1}**\n **${supportbot.DepartmentTitle_2}**\n **${supportbot.DepartmentTitle_3}**` })
}
const CloseButton = new Discord.MessageButton()
.setCustomId('ticketclose')
.setLabel('Close')
.setStyle(supportbot.TicketCloseColour)
.setEmoji(supportbot.TicketCloseEmoji)
const LockButton = new Discord.MessageButton()
.setCustomId('ticketlock')
.setLabel('Lock')
.setStyle(supportbot.TicketLockColour)
.setEmoji(supportbot.TicketLockEmoji)
const row2 = new Discord.MessageActionRow()
.addComponents(CloseButton, LockButton)
if(supportbot.TicketDepartments) {
let DenyArr = [DeptRole1, DeptRole2, DeptRole3]
await asyncForEach(DenyArr, (index) => {
ticketChannel.permissionOverwrites.create(index, {VIEW_CHANNEL: false})
})
const Department1Button = new Discord.MessageButton()
.setCustomId('Department1')
.setLabel(`${supportbot.DepartmentTitle_1}`)
.setStyle(supportbot.TicketDept1Colour)
.setEmoji(supportbot.TicketDept1Emoji)
const Department2Button = new Discord.MessageButton()
.setCustomId('Department2')
.setLabel(`${supportbot.DepartmentTitle_2}`)
.setStyle(supportbot.TicketDept2Colour)
.setEmoji(supportbot.TicketDept2Emoji)
const Department3Button = new Discord.MessageButton()
.setCustomId('Department3')
.setLabel(`${supportbot.DepartmentTitle_3}`)
.setStyle(supportbot.TicketDept3Colour)
.setEmoji(supportbot.TicketDept3Emoji)
const row = new Discord.MessageActionRow()
.addComponents(Department1Button, Department2Button, Department3Button)
const m = await ticketChannel.send({embeds: [TicketMessage], components: [row]})
const filter = i => {
i.deferUpdate();
return i.user.id === message.author.id;
};
const collector = m.createMessageComponentCollector({ filter, max: 1, componentType: 'BUTTON', time: 60000 });
collector.on('collect',async (i) => {
if(i.customId === 'Department1') {
ticketChannel.permissionOverwrites.edit(DeptRole1, {
VIEW_CHANNEL: true,
READ_MESSAGE_HISTORY: true,
SEND_MESSAGES: true,
})
if(!supportbot.AllowStaff) {
ticketChannel.permissionOverwrites.edit(Staff, { VIEW_CHANNEL: false, READ_MESSAGE_HISTORY: false, SEND_MESSAGES: false })
}
const Department1 = new Discord.MessageEmbed()
.setDescription(`> Thank for reaching out to the **${supportbot.DepartmentTitle_1} Department**. Please provide us information regarding your query.`)
.setColor(supportbot.EmbedColour)
ticketChannel.send({ embeds: [Department1] });
if (supportbot.AllowTicketMentions) {
ticketChannel.send({content: `@here`})
}
}
i.message.edit({embeds: [TicketMessage], components: [row2]})
if(i.customId === 'Department2') {
ticketChannel.permissionOverwrites.edit(DeptRole2, {
VIEW_CHANNEL: true,
READ_MESSAGE_HISTORY: true,
SEND_MESSAGES: true,
})
if(!supportbot.AllowStaff) {
ticketChannel.permissionOverwrites.edit(Staff, { VIEW_CHANNEL: false, READ_MESSAGE_HISTORY: false, SEND_MESSAGES: false })
}
const Department2 = new Discord.MessageEmbed()
.setDescription(`> Thank for reaching out to the **${supportbot.DepartmentTitle_2} Department**. Please provide us information regarding your query.`)
.setColor(supportbot.EmbedColour)
ticketChannel.send({ embeds: [Department2] });
if (supportbot.AllowTicketMentions) {
ticketChannel.send({content: `@here`})
}
i.message.edit({embeds: [TicketMessage], components: [row2]})
}
if(i.customId === 'Department3') {
ticketChannel.permissionOverwrites.edit(DeptRole3, {
VIEW_CHANNEL: true,
READ_MESSAGE_HISTORY: true,
SEND_MESSAGES: true,
})
if(!supportbot.AllowStaff) {
ticketChannel.permissionOverwrites.edit(Staff, { VIEW_CHANNEL: false, READ_MESSAGE_HISTORY: false, SEND_MESSAGES: false })
}
const Department3 = new Discord.MessageEmbed()
.setDescription(`> Thank for reaching out to the **${supportbot.DepartmentTitle_3} Department**. Please provide us information regarding your query.`)
.setColor(supportbot.EmbedColour)
ticketChannel.send({ embeds: [Department3] });
if (supportbot.AllowTicketMentions) {
ticketChannel.send({content: `@here`})
}
i.message.edit({embeds: [TicketMessage], components: [row2]})
}
})
} else {
ticketChannel.send({embeds: [TicketMessage], components: [row2]})
}
}
});
-166
View File
@@ -1,166 +0,0 @@
// SupportBot | Emerald Services
// Interaction Create Event
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const cmdconfig = yaml.load(fs.readFileSync('./Data/commands.yml', 'utf8'));
const Event = require("../Structures/Event.js");
module.exports = new Event("interactionCreate", (client, interaction) => {
if (interaction.isCommand()) return;
const command = client.commands.find(cmd => cmd.name.toLowerCase() == interaction.commandName);
if (interaction.user.bot || !interaction.isCommand() || !interaction.guild)
return;
const args = [
interaction.commandName,
...client.commands
.find(cmd => cmd.name.toLowerCase() == interaction.commandName)
.slashCommandOptions.map(v => `${interaction.options.get(v.name).value}`)
];
const NotValid = new Discord.MessageEmbed()
.setDescription(`:x: \`Invalid Command\` `)
.setColor(supportbot.ErrorColour)
if (!command) return interaction.reply({
embeds: [NotValid]
});
const permission = interaction.member.permissions.has(command.permission);
const ValidPerms = new Discord.MessageEmbed()
.setDescription(":x: \`Invalid Permissions\` Do you have the correct permissions to execute this command?")
.setColor(supportbot.ErrorColour)
if (!permission)
return interaction.reply({
embeds: [ValidPerms]
});
command.run(interaction, args, client);
if(interaction.isButton()) {
if(interaction.customId === 'openticket') {
interaction.message.fetch()
interaction.deferUpdate()
let message = {
guild: client.guilds.cache.get(interaction.message.guildId),
author: client.users.cache.get(interaction.user.id),
content: "N/A",
channel: client.channels.cache.get(interaction.message.channelId)
}
try {
const cmd = client.commands.get(cmdconfig.OpenTicket);
if(!cmd) return;
cmd.run(message, message.content)
}
catch (error){
console.error(error)
}
}
if(interaction.customId === 'ticketclose') {
if(interaction.channel.name.startsWith(`${supportbot.TicketChannel}-`)) {
interaction.message.fetch()
interaction.deferUpdate()
const DeleteTicketEmbed = new Discord.MessageEmbed()
.setDescription(supportbot.TicketDeleteMessage)
.setColor(supportbot.EmbedColour)
interaction.channel.send({embeds: [DeleteTicketEmbed]})
let logChannel = interaction.guild.channels.cache.find(channel => channel.name === supportbot.TranscriptLog) || interaction.guild.channels.cache.find(channel => channel.id === supportbot.TranscriptLog)
let user = interaction.user
let name = interaction.channel.name;
let ticketChannel = interaction.channel
let reason = "No Reason Provided."
interaction.channel.send({content: `**${supportbot.ClosingTicket}**`})
.then(() => {
const logEmbed = new Discord.MessageEmbed()
.setTitle(supportbot.TranscriptTitle)
.setColor(supportbot.EmbedColour)
.setFooter(supportbot.EmbedFooter)
.addField("Closed By", user.username)
.addField("Reason", reason)
interaction.channel.messages.fetch({limit: 100}).then(msgs => {
let html = ''
msgs = msgs.sort( ( a, b ) => a.createdTimestamp - b.createdTimestamp );
html += `<strong>Server Name:</strong> ${interaction.guild.name}<br>`
html += `<strong>Ticket:</strong> ${ticketChannel.name}<br>`
html += `<strong>Message:</strong> ${msgs.size} Messages<br><br><br>`
msgs.forEach(msg => {
if(msg.content) {
html += `<strong>User:</strong> ${msg.author.tag}<br>`;
html += `<strong>Message:</strong> ${msg.content}<br>`;
html += `-----<br><br>`;
}
})
const TranscriptSavedEmbed = new Discord.MessageEmbed()
.setDescription(supportbot.TranscriptSavedMessage)
.setColor(supportbot.SuccessColour)
interaction.channel.send({embeds: [TranscriptSavedEmbed]})
let file = new Discord.MessageAttachment( Buffer.from( html ), `${name}.html` )
logChannel.send({embeds: [logEmbed], files: [file]})
setTimeout(() => interaction.channel.delete(), supportbot.TicketDeleteTime)
})
})
}
}
if(interaction.customId === 'ticketlock') {
if(interaction.channel.name.startsWith(`${supportbot.TicketChannel}`)) {
interaction.message.fetch()
interaction.deferUpdate()
let Admin = interaction.guild.roles.cache.find(AdminUser => AdminUser.name === supportbot.Admin) || interaction.guild.roles.cache.find(AdminUser => AdminUser.id === supportbot.Admin)
let all = interaction.channel.permissionOverwrites.cache
let parent = interaction.guild.channels.cache.find(c => c.name === `${supportbot.LockTicketCategory}`) || interaction.guild.channels.cache.find(c => c.id === `${supportbot.LockTicketCategory}`)
if(parent) {
interaction.channel.setParent(parent.id)
}
all.forEach(perm => {
interaction.channel.permissionOverwrites.delete(perm.id)
})
interaction.channel.permissionOverwrites.create(Admin.id, {VIEW_CHANNEL: true})
interaction.channel.permissionOverwrites.create(interaction.guild.id, {VIEW_CHANNEL: false})
const ticketDeleteButton = new Discord.MessageButton()
.setCustomId('ticketclose')
.setLabel('Close')
.setStyle(supportbot.TicketDeleteColour)
.setEmoji(supportbot.TicketDeleteEmoji)
const row = new Discord.MessageActionRow()
.addComponents(ticketDeleteButton)
const ArchiveEmbed = new Discord.MessageEmbed()
.setTitle("Archived")
.setDescription(`Archived ${interaction.channel.name}`)
.setColor(`${supportbot.SuccessColour}`)
interaction.channel.send({embeds: [ArchiveEmbed]})
interaction.message.edit({components: [row]})
}
}
}
});
-50
View File
@@ -1,50 +0,0 @@
// SupportBot | Emerald Services
// Message Create Event
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const Event = require("../Structures/Event.js");
module.exports = new Event("messageCreate", (client, message) => {
if (message.author.bot) return;
if (!message.content.startsWith(client.prefix)) return;
const args = message.content.substring(client.prefix.length).split(/ +/);
const command = client.commands.find(cmd => cmd.name == args[0]);
const NotValid = new Discord.MessageEmbed()
.setDescription(`:x: \`${args[0]}\` is an Invalid Command `)
.setColor(supportbot.ErrorColour)
if (!command) return message.reply({
embeds: [NotValid]
});
const OnlySlashCmd = new Discord.MessageEmbed()
.setDescription(`:warning: That command is only useable as a \`Slash Command\` `)
.setColor(supportbot.WarnColour)
if (!["BOTH", "TEXT"].includes(command.type))
return message.reply({
embeds: [OnlySlashCmd]
})
const permission = message.member.permissions.has(command.permission, true);
const ValidPerms = new Discord.MessageEmbed()
.setDescription(":x: Do you have the \`${command.permission}\` permissions to execute this command?")
.setColor(supportbot.ErrorColour)
if (!permission)
return message.reply(
`You do not have the permission \`${command.permission}\` to run this command!`
);
command.run(message, args, client);
});
-93
View File
@@ -1,93 +0,0 @@
// SupportBot | Emerald Services
// Ready Event
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const panelconfig = yaml.load(fs.readFileSync('./Data/ticket-panel.yml', 'utf8'));
const Event = require("../Structures/Event.js");
module.exports = new Event("ready", client => {
client.user.setActivity(supportbot.BotActivity, {
type: supportbot.ActivityType,
url: supportbot.StreamingURL
});
console.log(`\u001b[32m`, `┏━━━┓╋╋╋╋╋╋╋╋╋╋╋╋╋┏┓┏━━┓╋╋╋┏┓`)
console.log(`\u001b[32m`, `┃┏━┓┃╋╋╋╋╋╋╋╋╋╋╋╋┏┛┗┫┏┓┃╋╋┏┛┗┓`)
console.log(`\u001b[32m`, `┃┗━━┳┓┏┳━━┳━━┳━━┳┻┓┏┫┗┛┗┳━┻┓┏┛`)
console.log(`\u001b[32m`, `┗━━┓┃┃┃┃┏┓┃┏┓┃┏┓┃┏┫┃┃┏━┓┃┏┓┃┃`)
console.log(`\u001b[32m`, `┃┗━┛┃┗┛┃┗┛┃┗┛┃┗┛┃┃┃┗┫┗━┛┃┗┛┃┗┓`)
console.log(`\u001b[32m`, `┗━━━┻━━┫┏━┫┏━┻━━┻┛┗━┻━━━┻━━┻━┛`)
console.log(`\u001b[32m`, `┗╋╋╋╋╋╋╋┃┃╋┃┃`)
console.log(`\u001b[32m`, `╋╋╋╋╋╋╋┗┛╋┗┛`)
console.log(` `)
console.log(`\u001b[31m`, `――――――――――――――――――――――――――――――――――――――――――――`)
console.log(` `)
console.log(`\u001b[33m`, `${supportbot.Name} v${supportbot.SupportBot_Version}`, `\u001b[36m`, `Connected to Discord`)
console.log(`\u001b[33m`, `Resources:`, `\u001b[36m`, `https://emeraldsrv.dev/resources`)
console.log(`\u001b[33m`, `Documentation:`, `\u001b[36m`, `https://supportbot.emeraldsrv.dev`)
console.log(`\u001b[33m`, `Discord:`, `\u001b[36m`, `https://emeraldsrv.dev/discord`)
console.log(`\u001b[33m`, `Hosting:`, `\u001b[36m`, `https://emeraldsrv.dev/hosting`)
console.log(` `)
console.log(`\u001b[37m`, `SupportBot proudly created by Emerald Services`)
console.log(` `)
console.log(`\u001b[31m`, `――――――――――――――――――――――――――――――――――――――――――――`)
if (panelconfig.TicketPanel) {
let chan1 = client.channels.cache.find(channel => channel.name === panelconfig.Channel)
if (!chan1) {
console.log('\u001b[33m', "[WARN] Ticket reaction panel is not setup, You can do so via the configuration file!")
return false;
}
const panelid = yaml.load(fs.readFileSync('./Data/ticket-panel-id.yml', 'utf8'));
chan1.messages.fetch(panelid.TicketPanelID).catch(async r => {
let embed = new Discord.MessageEmbed()
.setTitle(`${panelconfig.PanelTitle}`)
.setColor(supportbot.SuccessColour)
.setFooter(supportbot.EmbedFooter);
if (panelconfig.TicketPanel_Description) {
embed.setDescription(panelconfig.PanelMessage)
}
if (panelconfig.TicketPanel_Thumbnail) {
embed.setThumbnail(panelconfig.PanelThumbnail)
}
if (panelconfig.TicketPanel_Image) {
embed.setImage(panelconfig.PanelImage)
}
let button = new Discord.MessageButton()
.setLabel(panelconfig.ButtonLabel)
.setCustomId('openticket')
.setStyle(panelconfig.ButtonColour)
let row = new Discord.MessageActionRow()
.addComponents(button)
await chan1.send({
embeds: [embed],
components: [row]
}).then(r => {
let data = {
"TicketPanelID": `${r.id}`
}
let yamlStr = yaml.dump(data);
fs.writeFileSync('./Data/ticket-panel-id.yml', yamlStr, 'utf8');
}).catch(e => {
console.log("Raw"+e)
})
})
}
});
+88 -11
View File
@@ -1,24 +1,101 @@
// ___ _ ___ _
// / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
// \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
// <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
// |_| |_|
// ___ _ ___ _
// / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
// \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
// <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
// |_| |_|
//
// SupportBot created by Emerald Services
// Installed with MIT License
//
// Discord Support: https://emeraldsrv.dev/discord
// Community Resources: https://emeraldsrv.dev/resources
// Community Resources: https://community.emeraldsrv.dev
const fs = require("fs");
const path = require("path");
const Discord = require("discord.js");
const yaml = require('js-yaml');
const supportbot = yaml.load(fs.readFileSync('./Data/supportbot.yml', 'utf8'));
const configStore = require("./Structures/ConfigStore.js");
const Client = require("./Structures/Client.js");
const client = new Client({});
const client = new Client({
intents: ["Guilds", "GuildMembers", "GuildMessages", "MessageContent"],
});
client.start(supportbot.Token);
const { isSetupComplete } = require("./Structures/DashboardSetup.js");
const APIServer = require("./API/server.js");
const api = new APIServer(client);
client.apiServer = api;
const port = api.config?.Port || 25575;
if (api.config?.Enabled) {
api.start(port);
if (!isSetupComplete()) {
console.log(`\n\x1b[33m\x1b[1m[Setup Required] Go to http://localhost:${port}/setup to set up your bot and dashboard.\x1b[0m\n`);
}
} else {
console.warn("[Dashboard] API disabled in Configs/api.yml");
}
const token = configStore.supportbot?.General?.Token;
const isPlaceholderToken = !token || token.includes("BOT_TOKEN") || token.includes("CHANGE_ME") || token.length < 30;
if (isPlaceholderToken) {
console.warn(`\x1b[33m[SupportBot] Bot token is not configured yet. Complete setup at http://localhost:${port}/setup\x1b[0m`);
} else {
client.start(token);
}
// SupportBot - New Logging System
const logTypes = ["Output", "Warn", "Error"];
if (!fs.existsSync("./Logs")) {
fs.mkdirSync("./Logs");
}
logTypes.forEach((type) => {
const dir = `./Logs/${type}`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
});
function logToFile(type, data) {
const date = new Date().toISOString().split("T")[0];
const file = path.join(`./Logs/${type}`, `${type}-${date}.log`);
const timestamp = new Date().toISOString();
fs.appendFileSync(file, `[${timestamp}] ${data}\n`);
if (global.apiServer) {
global.apiServer.broadcast("log", {
type,
timestamp,
message: String(data)
});
}
}
const origLog = console.log;
console.log = (...args) => {
origLog(...args);
logToFile("Output", args.join(" "));
};
const origWarn = console.warn;
console.warn = (...args) => {
origWarn(...args);
logToFile("Warn", args.join(" "));
};
const origError = console.error;
console.error = (...args) => {
origError(...args);
logToFile("Error", args.join(" "));
};
process.on("unhandledRejection", (reason) => {
console.error("Unhandled Rejection:", reason);
});
process.on("uncaughtException", (err) => {
console.error("Uncaught Exception:", err);
});
Generated Vendored
BIN
View File
Binary file not shown.
Generated Vendored
-1
View File
@@ -1 +0,0 @@
../js-yaml/bin/js-yaml.js
-297
View File
@@ -1,297 +0,0 @@
{
"name": "supportbot",
"version": "7.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"node_modules/@discordjs/builders": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.5.0.tgz",
"integrity": "sha512-HP5y4Rqw68o61Qv4qM5tVmDbWi4mdTFftqIOGRo33SNPpLJ1Ga3KEIR2ibKofkmsoQhEpLmopD1AZDs3cKpHuw==",
"dependencies": {
"@sindresorhus/is": "^4.0.1",
"discord-api-types": "^0.22.0",
"ow": "^0.27.0",
"ts-mixer": "^6.0.0",
"tslib": "^2.3.0"
},
"engines": {
"node": ">=14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@discordjs/collection": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.2.1.tgz",
"integrity": "sha512-vhxqzzM8gkomw0TYRF3tgx7SwElzUlXT/Aa41O7mOcyN6wIJfj5JmDWaO5XGKsGSsNx7F3i5oIlrucCCWV1Nog==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@discordjs/form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@sapphire/async-queue": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.4.tgz",
"integrity": "sha512-fFrlF/uWpGOX5djw5Mu2Hnnrunao75WGey0sP0J3jnhmrJ5TAPzHYOmytD5iN/+pMxS+f+u/gezqHa9tPhRHEA==",
"engines": {
"node": ">=14",
"npm": ">=6"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz",
"integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@types/node": {
"version": "16.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz",
"integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ=="
},
"node_modules/@types/ws": {
"version": "7.4.7",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
"integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/discord-api-types": {
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.22.0.tgz",
"integrity": "sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg==",
"engines": {
"node": ">=12"
}
},
"node_modules/discord.js": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.1.0.tgz",
"integrity": "sha512-gxO4CXKdHpqA+WKG+f5RNnd3srTDj5uFJHgOathksDE90YNq/Qijkd2WlMgTTMS6AJoEnHxI7G9eDQHCuZ+xDA==",
"dependencies": {
"@discordjs/builders": "^0.5.0",
"@discordjs/collection": "^0.2.1",
"@discordjs/form-data": "^3.0.1",
"@sapphire/async-queue": "^1.1.4",
"@types/ws": "^7.4.7",
"discord-api-types": "^0.22.0",
"node-fetch": "^2.6.1",
"ws": "^7.5.1"
},
"engines": {
"node": ">=16.6.0",
"npm": ">=7.0.0"
}
},
"node_modules/dot-prop": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
"dependencies": {
"is-obj": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"engines": {
"node": ">=8"
}
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"node_modules/mime-db": {
"version": "1.50.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz",
"integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.33",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz",
"integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==",
"dependencies": {
"mime-db": "1.50.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/node-fetch": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz",
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/ow": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz",
"integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==",
"dependencies": {
"@sindresorhus/is": "^4.0.1",
"callsites": "^3.1.0",
"dot-prop": "^6.0.1",
"lodash.isequal": "^4.5.0",
"type-fest": "^1.2.1",
"vali-date": "^1.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"node_modules/ts-mixer": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.0.tgz",
"integrity": "sha512-nXIb1fvdY5CBSrDIblLn73NW0qRDk5yJ0Sk1qPBF560OdJfQp9jhl+0tzcY09OZ9U+6GpeoI9RjwoIKFIoB9MQ=="
},
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/type-fest": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/vali-date": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
"integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}
BIN
View File
Binary file not shown.
-190
View File
@@ -1,190 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2021 Vlad Frangu
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-33
View File
@@ -1,33 +0,0 @@
<div align="center">
<br />
<p>
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
</p>
<br />
<p>
<a href="https://discord.gg/djs"><img src="https://img.shields.io/discord/222078108977594368?color=5865F2&logo=discord&logoColor=white" alt="Discord server" /></a>
<a href="https://github.com/discordjs/builders/actions"><img src="https://github.com/discordjs/builders/workflows/Tests/badge.svg" alt="Test status" /></a>
<a href="https://github.com/discordjs/builders/actions"><img src="https://github.com/discordjs/builders/workflows/Lint/badge.svg" alt="Lint status" /></a>
<a href="https://codecov.io/gh/discordjs/builders"><img src="https://codecov.io/gh/discordjs/builders/branch/main/graph/badge.svg" alt="Code coverage" /></a>
</p>
</div>
## Installation
Install with [npm](https://www.npmjs.com/) / [yarn](https://yarnpkg.com) / [pnpm](https://pnpm.js.org/):
```sh
npm install @discordjs/builders
yarn add @discordjs/builders
pnpm add @discordjs/builders
```
## Contribution
See [Contributing Guide](https://github.com/discordjs/builders/blob/main/.github/CONTRIBUTING.md).
## Examples
Here are some examples for the builders and utilities you can find in this package:
- [Slash Command Builders](./docs/examples/Slash%20Command%20Builders.md)
-12
View File
@@ -1,12 +0,0 @@
export * from './messages/formatters';
export * as SlashCommandAssertions from './interactions/slashCommands/Assertions';
export * from './interactions/slashCommands/SlashCommandBuilder';
export * from './interactions/slashCommands/SlashCommandSubcommands';
export * from './interactions/slashCommands/options/boolean';
export * from './interactions/slashCommands/options/channel';
export * from './interactions/slashCommands/options/integer';
export * from './interactions/slashCommands/options/mentionable';
export * from './interactions/slashCommands/options/role';
export * from './interactions/slashCommands/options/string';
export * from './interactions/slashCommands/options/user';
//# sourceMappingURL=index.d.ts.map
Loaded 100 of 1346 files, more files were not shown because too many files have changed in this diff. Show more