├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitattributes
├── .github
├── CODEOWNERS
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
├── labels.yml
└── workflows
│ ├── codeql-analysis.yml
│ ├── label-sync.yml
│ └── sentry-release.yml
├── .gitignore
├── .npmrc.example
├── .sapphirerc.json
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── env.example
├── package-lock.json
├── package.json
├── prisma
├── migrations
│ ├── 20230703123936_init
│ │ └── migration.sql
│ ├── 20230704101742_add_guild_logs_model
│ │ └── migration.sql
│ ├── 20230704124416_add_member_and_user_models
│ │ └── migration.sql
│ ├── 20230704134426_fixes
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
├── src
├── RTByte.ts
├── commands
│ ├── Developer
│ │ └── stats.ts
│ ├── Moderation
│ │ ├── channelinfo.ts
│ │ ├── roleinfo.ts
│ │ ├── send.ts
│ │ └── userinfo.ts
│ └── Standard
│ │ ├── ping.ts
│ │ ├── quote.ts
│ │ ├── roles.ts
│ │ └── weather.ts
├── config.example.ts
├── lib
│ ├── RTByteClient.ts
│ ├── extensions
│ │ ├── GuildLogEmbed.ts
│ │ ├── RTByteCommand.ts
│ │ ├── RTByteEmbed.ts
│ │ └── index.ts
│ ├── setup.ts
│ └── util
│ │ ├── Sanitizer
│ │ ├── clean.ts
│ │ └── initClean.ts
│ │ ├── common
│ │ └── times.ts
│ │ ├── constants.ts
│ │ ├── decorators
│ │ └── routeAuthenticated.ts
│ │ ├── functions
│ │ ├── initialize.ts
│ │ └── permissions.ts
│ │ └── util.ts
├── listeners
│ ├── guilds
│ │ ├── channels
│ │ │ ├── channelCreateLog.ts
│ │ │ ├── channelDeleteLog.ts
│ │ │ └── channelUpdateLog.ts
│ │ ├── emojis
│ │ │ ├── guildEmojiCreateLog.ts
│ │ │ ├── guildEmojiDeleteLog.ts
│ │ │ └── guildEmojiUpdateLog.ts
│ │ ├── events
│ │ │ ├── guildScheduledEventCreateLog.ts
│ │ │ ├── guildScheduledEventDeleteLog.ts
│ │ │ └── guildScheduledEventUpdateLog.ts
│ │ ├── guildCreate.ts
│ │ ├── guildLogCreate.ts
│ │ ├── guildUpdateLog.ts
│ │ ├── initializeMember.ts
│ │ ├── interactions
│ │ │ └── interactionCreate.ts
│ │ ├── invites
│ │ │ ├── inviteCreateLog.ts
│ │ │ └── inviteDeleteLog.ts
│ │ ├── members
│ │ │ ├── guildMemberAdd.ts
│ │ │ ├── guildMemberAddLog.ts
│ │ │ ├── guildMemberRemoveLog.ts
│ │ │ ├── guildMemberUpdate.ts
│ │ │ └── guildMemberUpdateLog.ts
│ │ ├── messages
│ │ │ ├── messageCreate.ts
│ │ │ ├── messageDeleteLog.ts
│ │ │ └── messageUpdateLog.ts
│ │ ├── roles
│ │ │ ├── roleCreateLog.ts
│ │ │ ├── roleDeleteLog.ts
│ │ │ └── roleUpdateLog.ts
│ │ ├── stages
│ │ │ ├── stageInstanceCreateLog.ts
│ │ │ ├── stageInstanceDeleteLog.ts
│ │ │ └── stageInstanceUpdateLog.ts
│ │ ├── stickers
│ │ │ ├── stickerCreateLog.ts
│ │ │ ├── stickerDeleteLog.ts
│ │ │ └── stickerUpdateLog.ts
│ │ └── threads
│ │ │ ├── threadCreateLog.ts
│ │ │ ├── threadDeleteLog.ts
│ │ │ └── threadUpdateLog.ts
│ └── ready.ts
├── routes
│ ├── guilds
│ │ ├── channels
│ │ │ ├── channel.ts
│ │ │ └── channels.ts
│ │ └── settings
│ │ │ ├── guildSettingsInfoLogs.ts
│ │ │ ├── guildSettingsModActions.ts
│ │ │ └── settings.ts
│ ├── oauth
│ │ └── refresh.ts
│ └── users
│ │ └── settings.ts
├── transformers
│ └── loginDataGuilds.ts
└── tsconfig.json
├── tsconfig.base.json
└── tsconfig.eslint.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 |
9 | [*.{js,ts}]
10 | indent_size = 4
11 | indent_style = tab
12 | block_comment_start = /*
13 | block_comment = *
14 | block_comment_end = */
15 |
16 | [*.{md,rmd,mkd,mkdn,mdwn,mdown,markdown,litcoffee}]
17 | tab_width = 4
18 | trim_trailing_whitespace = false
19 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sapphire",
3 | "rules": {
4 | "@typescript-eslint/no-namespace": "off",
5 | "@typescript-eslint/no-throw-literal": "off",
6 | "prettier/prettier": 0
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Set @RTByte/developers as default code owner
2 | * @RTByte/bot-team
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: rtbyte
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a sinlge Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a bug report to help us improve the bot
4 | title: ''
5 | labels: 'Bug: Unverified'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Run '...'
16 | 4. See error
17 |
18 | **Expected behavior**
19 | A clear and concise description of what you expected to happen.
20 |
21 | **Screenshots**
22 | If applicable, add screenshots to help explain your problem.
23 |
24 | **Additional context**
25 | Add any other context about the problem here.
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest a new feature for the bot
4 | title: ''
5 | labels: 'Meta: Feature, Type: Proposal'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the feature you'd like to see.**
11 | A clear and concise description of the new feature you'd like to see implemented in the bot
12 |
13 | **Additional context**
14 | Add any other context or screenshots about the feature request here.
15 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/.github/labels.yml:
--------------------------------------------------------------------------------
1 | - name: 'Bug: Fixed'
2 | description: Issues that report bugs and have been fixed.
3 | color: 0e8a16
4 | - name: 'Bug: Cannot reproduce'
5 | description: Issues that report bugs that cannot be reproduced.
6 | color: d73a4a
7 | - name: 'Bug: Confirmed'
8 | description: Issues that report confirmed bugs.
9 | color: d73a4a
10 | - name: 'Bug: Unverified'
11 | description: Issues that report unverified bugs. Pending inspection.
12 | color: d73a4a
13 | - name: 'Meta: Bugfix'
14 | description: PRs that fix bugs or issues.
15 | color: ace209
16 | - name: 'Meta: Cleanup'
17 | description: Issues and PRs related to code cleanup.
18 | color: ffff00
19 | - name: 'Meta: Dependencies'
20 | description: Issues and PRs related dependencies.
21 | color: ffff00
22 | - name: 'Meta: Documentation'
23 | description: Issues and PRs related to documentation.
24 | color: ffff00
25 | - name: 'Meta: Feature'
26 | description: Issues and PRs related to new features.
27 | color: ffff00
28 | - name: 'Meta: GitHub'
29 | description: Issues and PRs related to GitHub.
30 | color: ffff00
31 | - name: 'Meta: Refactor'
32 | description: Issues and PRs related to refactors.
33 | color: ffff00
34 | - name: 'Meta: Unhelpful'
35 | description: Issues and PRs that nit-pick. Basically, don't be a twat.
36 | color: 93174b
37 | - name: 'Priority: Critical'
38 | description: Issues that must be fixed or PRs that must be finished and merged with critical priority.
39 | color: b60205
40 | - name: 'Priority: High'
41 | description: Issues that must be fixed or PRs that must be finished and merged with high priority.
42 | color: d93f0b
43 | - name: 'Priority: Medium'
44 | description: Issues that must be fixed or PRs that must be finished and merged with medium priority.
45 | color: fbca04
46 | - name: 'Priority: Low'
47 | description: Issues that must be fixed or PRs that must be finished and merged with low priority.
48 | color: 0e8a16
49 | - name: 'Status: Blocked'
50 | description: PRs that are blocked by other issues/PRs.
51 | color: b60205
52 | - name: 'Status: Denied'
53 | description: PRs that are blocked by other issues/PRs.
54 | color: b60205
55 | - name: 'Status: Duplicate'
56 | description: Issues and PRs that are duplicated.
57 | color: 7765c6
58 | - name: 'Status: Help wanted'
59 | description: Issues that need assistance from volunteers or PRs that need help to proceed.
60 | color: "037772"
61 | - name: 'Status: Needs testing'
62 | description: PRs that need testing from the author or volunteers.
63 | color: "037772"
64 | - name: 'Status: Ready for review'
65 | description: PRs that are ready for codeowner review.
66 | color: 0eea00
67 | - name: 'Status: Ready to merge'
68 | description: PRs that are ready to merge.
69 | color: 0eea00
70 | - name: 'Status: Stalled'
71 | description: Issues and PRs that are being set aside for now or on hold.
72 | color: fbca04
73 | - name: 'Status: WIP'
74 | description: Issues and PRs that are still a work in progress.
75 | color: b21a1a
76 | - name: 'Type: Consistency'
77 | description: Issues and PRs related to code consistency.
78 | color: 0075c1
79 | - name: 'Type: Dependencies'
80 | description: PRs that update a dependency file.
81 | color: 0025ff
82 | - name: 'Type: Enhancement'
83 | description: Issues and PRs related to feature enhancement.
84 | color: 0075c1
85 | - name: 'Type: Maintenance'
86 | description: Issues and PRs related to the maintenance of a feature or function.
87 | color: 0075c1
88 | - name: 'Type: Proposal'
89 | description: Issues that request a new feature or a change to the bot.
90 | color: ffffff
91 | - name: 'Type: Question'
92 | description: Issues that do not belong in the issue tracker and should be asked in https://rtbyte.xyz/discord.
93 | color: f28ef0
94 | - name: 'Type: Security'
95 | description: Pull requests that address a security vulnerability.
96 | color: ee0701
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: Code Scanning
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | CodeQL:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # renovate: tag=v2
15 | with:
16 | submodules: true
17 |
18 | # Initializes the CodeQL tools for scanning.
19 | - name: Initialize CodeQL
20 | uses: github/codeql-action/init@v1
21 | with:
22 | languages: typescript
23 |
24 | - name: Perform CodeQL Analysis
25 | uses: github/codeql-action/analyze@v1
26 |
--------------------------------------------------------------------------------
/.github/workflows/label-sync.yml:
--------------------------------------------------------------------------------
1 | name: Label sync
2 |
3 | on:
4 | issues:
5 | label:
6 | push:
7 | branches:
8 | - master
9 | paths:
10 | - '.github/labels.yml'
11 |
12 | jobs:
13 | labeler:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout Project
17 | uses: actions/checkout@v2
18 | - name: Run Label Sync
19 | uses: crazy-max/ghaction-github-labeler@v2
20 | env:
21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 |
--------------------------------------------------------------------------------
/.github/workflows/sentry-release.yml:
--------------------------------------------------------------------------------
1 | name: Create Sentry release
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | env:
9 | GITHUB_TOKEN: ${{ github.token }}
10 |
11 | jobs:
12 | sentry-release:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v2
17 |
18 | - name: Get release
19 | id: get_release
20 | uses: pozetroninc/github-action-get-latest-release@master
21 | with:
22 | owner: RTByte
23 | repo: rtbyte
24 | excludes: prerelease, draft
25 |
26 | - name: Create Sentry release
27 | uses: getsentry/action-release@v1
28 | env:
29 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
30 | SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
31 | SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
32 | with:
33 | environment: production
34 | version: ${{ steps.get_release.outputs.release }}
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 |
4 | # Keep environment variables and config out of version control
5 | .env
6 | config.ts
7 | .DS_Store
8 | .npmrc
--------------------------------------------------------------------------------
/.npmrc.example:
--------------------------------------------------------------------------------
1 | @RTByte:registry=https://npm.pkg.github.com
2 | always-auth=true
3 | //npm.pkg.github.com/:_authToken=""
4 |
--------------------------------------------------------------------------------
/.sapphirerc.json:
--------------------------------------------------------------------------------
1 | {
2 | "projectLanguage": "ts",
3 | "locations": {
4 | "base": "src",
5 | "arguments": "arguments",
6 | "commands": "commands",
7 | "listeners": "listeners",
8 | "preconditions": "preconditions",
9 | "interaction-handlers": "interaction-handlers",
10 | "routes": "routes"
11 | },
12 | "customFileTemplates": {
13 | "enabled": false,
14 | "location": ""
15 | }
16 | }
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint", "eg2.vscode-npm-script", "editorconfig.editorconfig", "prisma.prisma"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.validate": ["typescript"],
3 | "editor.tabSize": 4,
4 | "editor.useTabStops": true,
5 | "editor.insertSpaces": false,
6 | "editor.detectIndentation": false,
7 | "files.eol": "\n",
8 | "search.exclude": {
9 | "**/node_modules": true,
10 | "**/dist/": true,
11 | "**/.git/": true
12 | },
13 | "editor.codeActionsOnSave": {
14 | "source.fixAll": true,
15 | "source.fixAll.eslint": true,
16 | "source.organizeImports": true
17 | },
18 | "[prisma]": {
19 | "editor.defaultFormatter": "Prisma.prisma"
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 RTByte
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 |
5 | # RTByte
6 |
7 | [](https://github.com/RTByte/rtbyte/releases)
8 | [](https://github.com/rtbyte/rtbyte/blob/main/LICENSE.md)
9 |
10 | [](https://github.com/RTByte/RTByte/issues)
11 | [](https://github.com/RTByte/RTByte/pulls)
12 | [](https://github.com/RTByte/rtbyte#contributors-)
13 | [](https://translate.rtbyte.xyz)
14 |
15 | [](https://rtbyte.xyz/discord)
16 | [](https://twitter.com/rtbyte)
17 | ======
18 |
19 |
20 | ## Description
21 |
22 | RTByte is an open-source modular multipurpose Discord bot built on the incredible [Sapphire] framework for [discord.js]. It brings a ton of features to help you run and manage your server. With an easy setup, you'll be up and running within minutes.
23 |
24 | For more information about the project, and a link to add the bot to your server, please visit [rtbyte.xyz]. For support, please join our [Discord] server.
25 |
26 | ## Development
27 |
28 | ### Requirements
29 |
30 | - [`Node.js`]: Node.js is required to run RTByte.
31 | - [`PostgreSQL`]: Open-source relational database.
32 | - [`Prisma`]: TypeScript ORM.
33 |
34 | ### Optionals
35 |
36 | - [`Sentry`]: Error monitoring & tracking (not yet implemented in current version).
37 | - [`Google Maps Platform`]: Geocoding API.
38 | - [`OpenWeather`]: Weather API.
39 |
40 | ### A note regarding self-hosting RTByte
41 |
42 | While RTByte is, and always will be, open-source, we're not very supportive of the idea of others self-hosting the bot. While you're completely free to host RTByte yourself, *you will not receive any support from us* in doing so.
43 |
44 | Like many other open-source Discord bots, RTByte hasn't been built with the idea of self-hosting in mind. We use many different services to ensure we're able to deliver the best solution available.
45 |
46 | - RTByte uses several external APIs. You'd need to create API keys in these for these to be able to fully use any features that may need them.
47 | - RTByte uses [`PostgreSQL`], an open-source relational database, to store persistent data. [`Prisma`], a TypeScript ORM, is used to interface with said database.
48 | - RTByte uses [`Sentry`] to track and monitor errors. Sentry is a paid service for which we've been granted an open-source license.
49 |
50 | You can add RTByte to your server by visiting [rtbyte.xyz/invite].
51 |
52 | ## Contributors ✨
53 |
54 | Thanks goes to these wonderful people:
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | [Sapphire]: https://github.com/sapphire-project/framework
63 | [discord.js]: https://discord.com
64 | [rtbyte.xyz]: https://rtbyte.xyz
65 | [Discord]: https://rtbyte.xyz/discord
66 | [`Node.js`]: https://nodejs.org
67 | [`PostgreSQL`]: https://www.postgresql.org
68 | [`Prisma`]: https://www.prisma.io
69 | [`Sentry`]: https://sentry.io
70 | [`Genius`]: https://genius.com/developers
71 | [`Google Maps Platform`]: https://cloud.google.com/maps-platform/
72 | [`OpenWeather`]: https://openweathermap.org
73 | [`Twitch`]: https://dev.twitch.tv
74 | [rtbyte.xyz/invite]: https://rtbyte.xyz/invite
75 | [emoji key]: https://allcontributors.org/docs/en/emoji-key
76 |
--------------------------------------------------------------------------------
/env.example:
--------------------------------------------------------------------------------
1 | BOT_TOKEN = ""
2 | ENCRYPTION_KEY = ""
3 | SENTRY_TOKEN = ""
4 | DATABASE_URL = ""
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rtbyte",
3 | "description": "An open-source modular multipurpose Discord bot built on the incredible Sapphire framework for discord.js",
4 | "version": "2.1.0",
5 | "main": "./dist/RTByte.js",
6 | "type": "module",
7 | "author": "The RTByte Team (https://rtbyte.xyz)",
8 | "contributors": [
9 | "Rasmus Gerdin (https://rasmusgerdin.com)",
10 | "Michael Cumbers (https://michaelcumbers.ca)"
11 | ],
12 | "license": "MIT",
13 | "homepage": "https://github.com/RTByte/rtbyte/",
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/RTByte/rtbyte.git"
17 | },
18 | "scripts": {
19 | "build": "tsc -b src",
20 | "dev": "npm run build && npm run start",
21 | "start": "node --enable-source-maps dist/RTByte.js",
22 | "prisma-build": "npx prisma generate",
23 | "prisma-migrate": "npx prisma migrate dev --name init"
24 | },
25 | "imports": {
26 | "#root/*": "./dist/*.js",
27 | "#lib/*": "./dist/lib/*.js",
28 | "#utils/*": "./dist/lib/util/*.js"
29 | },
30 | "dependencies": {
31 | "@prisma/client": "^5.2.0",
32 | "@sapphire/decorators": "^6.0.2",
33 | "@sapphire/fetch": "^2.4.1",
34 | "@sapphire/framework": "^4.5.2",
35 | "@sapphire/plugin-api": "^5.1.1",
36 | "@sapphire/plugin-logger": "^3.0.6",
37 | "@sapphire/plugin-subcommands": "^4.1.1",
38 | "@sapphire/time-utilities": "^1.7.10",
39 | "@sapphire/utilities": "^3.13.0",
40 | "colorette": "^2.0.20",
41 | "discord-api-types": "^0.37.54",
42 | "discord.js": "^14.13.0",
43 | "dotenv": "^16.3.1",
44 | "reflect-metadata": "^0.1.13"
45 | },
46 | "devDependencies": {
47 | "@sapphire/eslint-config": "^5.0.1",
48 | "@sapphire/ts-config": "^4.0.1",
49 | "@types/node": "^20.5.3",
50 | "eslint": "^8.47.0",
51 | "prisma": "^5.2.0",
52 | "ts-node": "^10.9.1",
53 | "typescript": "^5.1.6"
54 | }
55 | }
--------------------------------------------------------------------------------
/prisma/migrations/20230703123936_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "client" (
3 | "id" TEXT NOT NULL,
4 | "restarts" INTEGER NOT NULL DEFAULT 0,
5 | "last_restart" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
6 | "guild_blacklist" TEXT[],
7 |
8 | CONSTRAINT "client_pkey" PRIMARY KEY ("id")
9 | );
10 |
11 | -- CreateTable
12 | CREATE TABLE "guild" (
13 | "id" TEXT NOT NULL,
14 | "log_channel" TEXT,
15 | "joinable_roles" TEXT[],
16 |
17 | CONSTRAINT "guild_pkey" PRIMARY KEY ("id")
18 | );
19 |
--------------------------------------------------------------------------------
/prisma/migrations/20230704101742_add_guild_logs_model/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `log_channel` on the `guild` table. All the data in the column will be lost.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "guild" DROP COLUMN "log_channel";
9 |
10 | -- CreateTable
11 | CREATE TABLE "guild_logs" (
12 | "guild_id" TEXT NOT NULL,
13 | "log_channel" TEXT,
14 | "logs_enabled" BOOLEAN NOT NULL DEFAULT false,
15 | "logs_channels" BOOLEAN NOT NULL DEFAULT true,
16 | "logs_emoji" BOOLEAN NOT NULL DEFAULT true,
17 | "logs_members" BOOLEAN NOT NULL DEFAULT true,
18 | "logs_events" BOOLEAN NOT NULL DEFAULT false,
19 | "logs_guild" BOOLEAN NOT NULL DEFAULT true,
20 | "logs_invites" BOOLEAN NOT NULL DEFAULT false,
21 | "logs_messages" BOOLEAN NOT NULL DEFAULT true,
22 | "logs_roles" BOOLEAN NOT NULL DEFAULT true,
23 | "logs_stages" BOOLEAN NOT NULL DEFAULT false,
24 | "logs_stickers" BOOLEAN NOT NULL DEFAULT true,
25 | "logs_threads" BOOLEAN NOT NULL DEFAULT true,
26 |
27 | CONSTRAINT "guild_logs_pkey" PRIMARY KEY ("guild_id")
28 | );
29 |
30 | -- AddForeignKey
31 | ALTER TABLE "guild_logs" ADD CONSTRAINT "guild_logs_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "guild"("id") ON DELETE CASCADE ON UPDATE CASCADE;
32 |
--------------------------------------------------------------------------------
/prisma/migrations/20230704124416_add_member_and_user_models/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "guild" ADD COLUMN "userId" TEXT;
3 |
4 | -- CreateTable
5 | CREATE TABLE "member" (
6 | "user_id" TEXT NOT NULL,
7 | "guild_id" TEXT NOT NULL,
8 | "times_joined" INTEGER NOT NULL DEFAULT 1,
9 |
10 | CONSTRAINT "member_pkey" PRIMARY KEY ("user_id","guild_id")
11 | );
12 |
13 | -- CreateTable
14 | CREATE TABLE "user" (
15 | "id" TEXT NOT NULL,
16 | "previous_usernames" TEXT[],
17 |
18 | CONSTRAINT "user_pkey" PRIMARY KEY ("id")
19 | );
20 |
21 | -- AddForeignKey
22 | ALTER TABLE "member" ADD CONSTRAINT "member_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
23 |
24 | -- AddForeignKey
25 | ALTER TABLE "member" ADD CONSTRAINT "member_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "guild"("id") ON DELETE CASCADE ON UPDATE CASCADE;
26 |
--------------------------------------------------------------------------------
/prisma/migrations/20230704134426_fixes/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `userId` on the `guild` table. All the data in the column will be lost.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "guild" DROP COLUMN "userId";
9 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/src/RTByte.ts:
--------------------------------------------------------------------------------
1 | import { RTByteClient } from '#lib/RTByteClient';
2 | import '#lib/setup';
3 | import { TOKENS } from '#root/config';
4 |
5 | const client = new RTByteClient;
6 |
7 | const main = async () => {
8 | try {
9 | await client.login(TOKENS.BOT_TOKEN);
10 | } catch (error) {
11 | client.logger.fatal(error);
12 | client.destroy();
13 | process.exit(1);
14 | }
15 | };
16 |
17 | void main();
18 |
--------------------------------------------------------------------------------
/src/commands/Developer/stats.ts:
--------------------------------------------------------------------------------
1 | import { RTByteEmbed } from '#lib/extensions/RTByteEmbed';
2 | import { ApplyOptions } from '@sapphire/decorators';
3 | import { Command } from '@sapphire/framework';
4 | import { memoryUsage } from 'node:process';
5 |
6 | @ApplyOptions({
7 | description: 'See statistics about the Bot.'
8 | })
9 | export class UserCommand extends Command {
10 | public override registerApplicationCommands(registry: Command.Registry) {
11 | registry.registerChatInputCommand((builder) =>
12 | builder //
13 | .setName(this.name)
14 | .setDescription(this.description)
15 | .addBooleanOption((option) =>
16 | option
17 | .setName('ephemeral')
18 | .setDescription('Whether or not the message should be shown only to you (default false)')
19 | )
20 | );
21 | }
22 |
23 | public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
24 | // Check to see if response should be ephemeral
25 | const ephemeral = interaction.options.getBoolean('ephemeral') ?? false;
26 | await interaction.deferReply({ ephemeral, fetchReply: true });
27 |
28 | // How many Guilds the bot is in
29 | const guildCount = this.container.client.guilds.cache.size;
30 |
31 | // Combined number of users of all guilds the bot is in
32 | let memberCount = 0;
33 | for await (const guild of this.container.client.guilds.cache) {
34 | memberCount += guild[1].memberCount;
35 | }
36 |
37 | // Counts for both global and guild-specific application commands
38 | let globalAppCommandCount = 0;
39 | let guildAppCommandCount = 0;
40 | for await (const appCommand of this.container.client.application?.commands.cache ?? []) {
41 | if (appCommand[1].guild) {
42 | guildAppCommandCount++;
43 | continue;
44 | }
45 | globalAppCommandCount++;
46 | }
47 |
48 | // Memory usage on host machine for this process
49 | const { heapTotal } = memoryUsage();
50 | const megabytesUsed = (heapTotal / 1000000).toFixed(2);
51 |
52 | const clientData = await this.container.prisma.clientSettings.findFirst();
53 | const lastRestart: Date = clientData?.restarts[clientData.restarts.length - 1] ?? new Date();
54 |
55 | // Build reply embed
56 | const embed = new RTByteEmbed()
57 | .setTitle(`${this.container.client.user?.username} Stats`)
58 | .setThumbnail(this.container.client.user?.avatarURL() ?? null)
59 | .addBlankField({ name: '**Userbase Stats:**', value: '', inline: false })
60 | .addFields({ name: 'Guilds', value: `${guildCount}`, inline: true })
61 | .addFields({ name: 'Members', value: `${memberCount}`, inline: true })
62 | .addBlankField()
63 | .addBlankField({ name: '**Server Stats:**', value: '', inline: false })
64 | .addFields({ name: 'Memory Use', value: `${megabytesUsed}MB`, inline: true })
65 | .addFields({ name: 'Last Restart', value: ``, inline: true })
66 | .addBlankField()
67 | .addBlankField({ name: '**Registered Application Commands:**', value: '', inline: false })
68 | .addFields({ name: 'Global Commands', value: `${globalAppCommandCount}`, inline: true })
69 | .addFields({ name: 'Guild Commands', value: `${guildAppCommandCount}`, inline: true });
70 |
71 | return interaction.followUp({ content: '', embeds: [embed], ephemeral });
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/commands/Moderation/channelinfo.ts:
--------------------------------------------------------------------------------
1 | import { RTByteCommand } from '#lib/extensions/RTByteCommand';
2 | import { RTByteEmbed } from '#lib/extensions/RTByteEmbed';
3 | import { minutes, seconds } from '#utils/common/times';
4 | import { Emojis } from '#utils/constants';
5 | import { ApplyOptions } from '@sapphire/decorators';
6 | import { type ChatInputCommand } from '@sapphire/framework';
7 | import { DurationFormatter } from '@sapphire/time-utilities';
8 | import { inlineCodeBlock } from '@sapphire/utilities';
9 | import { ChannelType, PermissionFlagsBits } from 'discord.js';
10 |
11 | @ApplyOptions({
12 | description: 'Retrieve information about a channel'
13 | })
14 | export class UserCommand extends RTByteCommand {
15 | public override registerApplicationCommands(registry: ChatInputCommand.Registry) {
16 | registry.registerChatInputCommand((builder) =>
17 | builder
18 | .setName(this.name)
19 | .setDescription(this.description)
20 | .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels)
21 | .setDMPermission(false)
22 | .addChannelOption((option) =>
23 | option
24 | .setName('channel')
25 | .setDescription('The channel to fetch information for')
26 | .setRequired(true)
27 | )
28 | .addBooleanOption((option) =>
29 | option
30 | .setName('ephemeral')
31 | .setDescription('Whether or not the message should be shown only to you (default false)')
32 | ), {
33 | idHints: [
34 | // Dev bot command
35 | '1127290502476210260',
36 | ],
37 | });
38 | }
39 |
40 | public async chatInputRun(interaction: ChatInputCommand.Interaction) {
41 | // Check to see if response should be ephemeral
42 | const ephemeral = interaction.options.getBoolean('ephemeral') ?? false;
43 | await interaction.deferReply({ ephemeral, fetchReply: true });
44 |
45 | // Fetch targetChannel from Discord
46 | const targetChannel = interaction.guild?.channels.resolve(interaction.options.getChannel('channel')?.id as string);
47 | if (!targetChannel) return interaction.followUp({ content: `${Emojis.X} Unable to fetch information for ${targetChannel}, please try again later.`, ephemeral });
48 |
49 | // Fetch this Guild's log settings
50 | const guildLogSettings = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: interaction.guild?.id } });
51 |
52 | // Gather Info for Response Embed
53 | const channelInfo = [];
54 | if (targetChannel.type === ChannelType.GuildForum) {
55 | if (targetChannel.defaultReactionEmoji) channelInfo.push(`${Emojis.Bullet}Default reaction: ${targetChannel.guild.emojis.resolve(targetChannel.defaultReactionEmoji.id as string) ?? targetChannel.defaultReactionEmoji.name}`);
56 | if (targetChannel.rateLimitPerUser) channelInfo.push(`${Emojis.Bullet}Posts slowmode: ${inlineCodeBlock(new DurationFormatter().format(seconds(targetChannel.rateLimitPerUser)))}`);
57 | if (targetChannel.defaultThreadRateLimitPerUser) channelInfo.push(`${Emojis.Bullet}Messages slowmode: ${inlineCodeBlock(new DurationFormatter().format(seconds(targetChannel.defaultThreadRateLimitPerUser)))}`);
58 | if (targetChannel.defaultForumLayout) {
59 | const forumLayout = ['Not set', 'List view', 'Gallery view'];
60 | channelInfo.push(`${Emojis.Bullet}Default layout: ${inlineCodeBlock(`${forumLayout[targetChannel.defaultForumLayout]}`)}`);
61 | }
62 | if (targetChannel.defaultSortOrder) {
63 | const sortOrder = ['Recent activity', 'Creation time'];
64 | channelInfo.push(`${Emojis.Bullet}Sort order: ${inlineCodeBlock(`${sortOrder[targetChannel.defaultSortOrder!]}`)}`)
65 | }
66 | if (targetChannel.nsfw) channelInfo.push(`${Emojis.Bullet}Age-restricted: ${Emojis.ToggleOn}`);
67 | if (targetChannel.defaultAutoArchiveDuration) channelInfo.push(`${Emojis.Bullet}Hide after inactivity: ${inlineCodeBlock(`${new DurationFormatter().format(minutes(targetChannel.defaultAutoArchiveDuration ?? 4320))}`)}`);
68 | }
69 | // Show whether the targetChannel is designated as the Info Log Channel for the Guild
70 | if (guildLogSettings?.infoLogChannel === targetChannel.id) channelInfo.push(`${Emojis.Bullet}RTByte log channel`);
71 |
72 | // Create Response Embed
73 | const embed = new RTByteEmbed()
74 | .setDescription(`<#${targetChannel.id}> ${inlineCodeBlock(`${targetChannel?.id}`)}`)
75 | .setThumbnail(interaction.guild?.iconURL() ?? null);
76 |
77 | if (targetChannel.parent) embed.addFields({ name: 'Category', value: inlineCodeBlock(targetChannel.parent.name), inline: true });
78 | embed.addFields({ name: 'Created', value: ``, inline: true });
79 |
80 | // Add Forum-Specific Info to Response Embed
81 | if (targetChannel.type === ChannelType.GuildForum) {
82 | if (targetChannel.topic) embed.addFields({ name: 'Post guidelines', value: inlineCodeBlock(targetChannel.topic), inline: true });
83 | if (targetChannel.availableTags) embed.addFields({ name: 'Tags', value: targetChannel.availableTags.map(tag => `${tag.emoji ? targetChannel.guild.emojis.resolve(tag.emoji.id as string) ?? tag.emoji.name : ''} ${inlineCodeBlock(tag.name)}`).join(', '), inline: true });
84 | }
85 |
86 | // Add extra information gathered in channelInfo
87 | if (channelInfo.length) embed.addFields({ name: 'Details', value: channelInfo.join('\n') });
88 |
89 | // Send response
90 | return interaction.followUp({ content: '', embeds: [embed], ephemeral });
91 | }
92 | }
--------------------------------------------------------------------------------
/src/commands/Moderation/roleinfo.ts:
--------------------------------------------------------------------------------
1 | import { RTByteCommand } from '#lib/extensions/RTByteCommand';
2 | import { RTByteEmbed } from '#lib/extensions/RTByteEmbed';
3 | import { Emojis } from '#utils/constants';
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { type ChatInputCommand } from '@sapphire/framework';
6 | import { inlineCodeBlock } from '@sapphire/utilities';
7 | import { PermissionFlagsBits } from 'discord.js';
8 |
9 | @ApplyOptions({
10 | description: 'Retrieve information about a role'
11 | })
12 | export class UserCommand extends RTByteCommand {
13 | public override registerApplicationCommands(registry: ChatInputCommand.Registry) {
14 | registry.registerChatInputCommand((builder) =>
15 | builder
16 | .setName(this.name)
17 | .setDescription(this.description)
18 | .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles)
19 | .setDMPermission(false)
20 | .addRoleOption((option) =>
21 | option
22 | .setName('role')
23 | .setDescription('The role to fetch information for')
24 | .setRequired(true)
25 | )
26 | .addBooleanOption((option) =>
27 | option
28 | .setName('ephemeral')
29 | .setDescription('Whether or not the message should be shown only to you (default false)')
30 | ), {
31 | idHints: [
32 | // Dev bot command
33 | '1124670120934002728',
34 | ],
35 | });
36 | }
37 |
38 | public async chatInputRun(interaction: ChatInputCommand.Interaction) {
39 | // Check to see if response should be ephemeral
40 | const ephemeral = interaction.options.getBoolean('ephemeral') ?? false;
41 | await interaction.deferReply({ ephemeral, fetchReply: true });
42 |
43 | // Fetch targetRole from Discord
44 | const targetRole = interaction.guild?.roles.resolve(interaction.options.getRole('role')?.id as string);
45 | if (!targetRole) return interaction.followUp({ content: `${Emojis.X} Unable to fetch information for ${targetRole}, please try again later.`, ephemeral });
46 |
47 | // Find targetRole's position in Guild Roles
48 | const rolesSorted = interaction.guild?.roles.cache.sort((roleA, roleB) => roleA.position - roleB.position).reverse();
49 |
50 | // Gather Info for Response Embed
51 | const roleInfo = [];
52 | if (targetRole?.mentionable) roleInfo.push(`${Emojis.Bullet}Mentionable`);
53 | if (targetRole?.hoist) roleInfo.push(`${Emojis.Bullet}Displayed separately`);
54 | if (targetRole.tags?.premiumSubscriberRole) roleInfo.push(`${Emojis.Bullet}Received when boosting server`);
55 | if (targetRole.tags?.botId) roleInfo.push(`${Emojis.Bullet}Managed by <@${targetRole.tags.botId}>`);
56 |
57 | // Create Response Embed
58 | const embed = new RTByteEmbed()
59 | .setDescription(`${targetRole?.unicodeEmoji ?? ''}${targetRole} ${inlineCodeBlock(`${targetRole?.id}`)}`)
60 | .setThumbnail(targetRole?.iconURL() ?? interaction.guild?.iconURL() ?? null)
61 | .setColor(targetRole?.color as number)
62 | .addFields(
63 | { name: 'Members', value: inlineCodeBlock(`${targetRole?.members.size}`), inline: true },
64 | { name: 'Color', value: inlineCodeBlock(`${targetRole?.hexColor}`), inline: true },
65 | { name: 'Hierarchy', value: inlineCodeBlock(`${rolesSorted!.map(r => r.position).indexOf(targetRole?.position as number) + 1}`), inline: true },
66 | { name: 'Created', value: `` }
67 | );
68 |
69 | // Add additional info to Response Embed
70 | if (roleInfo.length) embed.addFields({ name: 'Details', value: roleInfo.join('\n') });
71 |
72 | // Send Response Embed
73 | return interaction.followUp({ content: '', embeds: [embed], ephemeral });
74 | }
75 | }
--------------------------------------------------------------------------------
/src/commands/Moderation/send.ts:
--------------------------------------------------------------------------------
1 | import { RTByteCommand } from '#lib/extensions/RTByteCommand';
2 | import { Emojis } from '#utils/constants';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { type ChatInputCommand } from '@sapphire/framework';
5 | import { inlineCodeBlock } from '@sapphire/utilities';
6 | import { ChannelType, PermissionFlagsBits } from 'discord.js';
7 |
8 | @ApplyOptions({
9 | description: 'Sends a message to the specified channel as the bot'
10 | })
11 | export class UserCommand extends RTByteCommand {
12 | public override registerApplicationCommands(registry: ChatInputCommand.Registry) {
13 | registry.registerChatInputCommand((builder) =>
14 | builder
15 | .setName(this.name)
16 | .setDescription(this.description)
17 | .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
18 | .setDMPermission(false)
19 | .addChannelOption((option) =>
20 | option
21 | .setName('channel')
22 | .setDescription('The channel to send a message to')
23 | .setRequired(true)
24 | )
25 | .addStringOption((option) =>
26 | option
27 | .setName('message')
28 | .setDescription('The message you want to send')
29 | .setRequired(true)
30 | ), {
31 | idHints: [
32 | // Dev bot command
33 | '1124787510304850103',
34 | ],
35 | });
36 | }
37 |
38 | public async chatInputRun(interaction: ChatInputCommand.Interaction) {
39 | await interaction.deferReply({ ephemeral: true });
40 |
41 | const channel = interaction.guild?.channels.resolve(interaction.options.getChannel('channel')?.id as string);
42 | const messageInput = interaction.options.getString('message') as string;
43 | if (channel?.type !== ChannelType.GuildText) return interaction.followUp({ content: `${Emojis.X} Messages cannot be sent to ${channel}.` });
44 |
45 | await channel.send({ content: messageInput});
46 | return interaction.followUp({ content: `${Emojis.Check} Sent ${inlineCodeBlock(messageInput)} to <#${channel.id}>!`});
47 | }
48 | }
--------------------------------------------------------------------------------
/src/commands/Moderation/userinfo.ts:
--------------------------------------------------------------------------------
1 | import { RTByteCommand } from '#lib/extensions/RTByteCommand';
2 | import { RTByteEmbed } from '#lib/extensions/RTByteEmbed';
3 | import { Colors, Emojis } from '#utils/constants';
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { type ChatInputCommand } from '@sapphire/framework';
6 | import { inlineCodeBlock } from '@sapphire/utilities';
7 | import { GuildMember, GuildMemberFlags, PermissionFlagsBits, UserFlags, UserFlagsBitField } from 'discord.js';
8 |
9 | @ApplyOptions({
10 | description: 'Retrieve information about a user'
11 | })
12 | export class UserCommand extends RTByteCommand {
13 | public override registerApplicationCommands(registry: ChatInputCommand.Registry) {
14 | registry.registerChatInputCommand((builder) =>
15 | builder
16 | .setName(this.name)
17 | .setDescription(this.description)
18 | .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
19 | .setDMPermission(false)
20 | .addUserOption((option) =>
21 | option
22 | .setName('member')
23 | .setDescription('The member to fetch information for')
24 | .setRequired(true)
25 | )
26 | .addBooleanOption((option) =>
27 | option
28 | .setName('ephemeral')
29 | .setDescription('Whether or not the message should be shown only to you (default false)')
30 | ), {
31 | idHints: [
32 | // Dev bot command
33 | '1124750920090124388',
34 | ],
35 | });
36 | }
37 |
38 | public async chatInputRun(interaction: ChatInputCommand.Interaction) {
39 | const ephemeral = interaction.options.getBoolean('ephemeral') ?? false;
40 | await interaction.deferReply({ ephemeral, fetchReply: true });
41 |
42 | const member = interaction.guild?.members.resolve(interaction.options.getUser('member')?.id as string);
43 | if (!member) return interaction.followUp({ content: `${Emojis.X} Unable to fetch information for the specified member, please try again later.`, ephemeral });
44 |
45 | const roles = member?.roles.cache.filter(role => role.name !== '@everyone');
46 | const joinPosition = interaction.guild?.members.cache.sort((memberA, memberB) => Number(memberA.joinedTimestamp) - Number(memberB.joinedTimestamp)).map(mbr => mbr).indexOf(member as GuildMember);
47 | // TODO: Disabling previous usernames for now... We need to encrypt them at rest, and provide options for users and guilds to opt-out of having them tracked.
48 | // const dbUser = await this.container.prisma.user.findUnique({ where: { id: member.user.id }, select: { previousUsernames: true }});
49 | // const previousUsernames = dbUser?.previousUsernames.filter(name => name !== member.user.username).map(name => inlineCodeBlock(name)).join(' ');
50 |
51 | const embed = new RTByteEmbed()
52 | .setDescription(`${member} ${inlineCodeBlock(`${member?.id}`)}`)
53 | .setThumbnail(member?.displayAvatarURL() ?? null)
54 | .setColor(member?.roles.highest.color ?? Colors.White)
55 | .addFields(
56 | { name: 'Join position', value: inlineCodeBlock(`${joinPosition! + 1}`), inline: true },
57 | { name: 'Joined', value: ``, inline: true },
58 | { name: 'Registered', value: ``, inline: true },
59 | );
60 |
61 | if (roles?.map(role => role).length) embed.addFields({ name: `Roles (${roles?.size})`, value: roles!.map(role => role).join(' ') });
62 | // if (dbUser && dbUser?.previousUsernames.length > 1) embed.addFields({ name: 'Previous usernames', value: previousUsernames! });
63 |
64 | const userInfo = [];
65 | if (member.isCommunicationDisabled()) userInfo.push(`${Emojis.Bullet}Currently timed out, will be removed `);
66 | if (member?.premiumSinceTimestamp) userInfo.push(`${Emojis.Bullet}Nitro boosting since `);
67 | if (member?.user.bot) userInfo.push(`${Emojis.Bullet}${member.user.flags?.has(UserFlagsBitField.Flags.VerifiedBot) ? 'Verified bot' : 'Bot'}`);
68 | if (member?.user.flags?.has(UserFlags.Staff)) userInfo.push(`${Emojis.Bullet}Discord staff`);
69 | if (member?.user.flags?.has(UserFlags.Partner)) userInfo.push(`${Emojis.Bullet}Partnered server owner`);
70 | if (member?.user.flags?.has(UserFlags.ActiveDeveloper)) userInfo.push(`${Emojis.Bullet}Active developer`);
71 | if (member?.flags.has(GuildMemberFlags.DidRejoin)) userInfo.push(`${Emojis.Bullet}Has rejoined`);
72 | if (userInfo.length) embed.addFields({ name: 'Details', value: userInfo.join('\n') });
73 |
74 | return interaction.followUp({ content: '', embeds: [embed], ephemeral });
75 | }
76 | }
--------------------------------------------------------------------------------
/src/commands/Standard/ping.ts:
--------------------------------------------------------------------------------
1 | import { RTByteCommand } from '#lib/extensions/RTByteCommand';
2 | import { Emojis } from '#utils/constants';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { isMessageInstance } from '@sapphire/discord.js-utilities';
5 | import { type ChatInputCommand } from '@sapphire/framework';
6 |
7 | @ApplyOptions({
8 | description: 'Pings bot'
9 | })
10 | export class UserCommand extends RTByteCommand {
11 | public override registerApplicationCommands(registry: ChatInputCommand.Registry) {
12 | registry.registerChatInputCommand((builder) => builder.setName(this.name).setDescription(this.description), {
13 | idHints: [
14 | // Dev bot command
15 | '1048932252282798131',
16 | ],
17 | });
18 | }
19 |
20 | public async chatInputRun(interaction: ChatInputCommand.Interaction) {
21 | const msg = await interaction.deferReply({ ephemeral: true, fetchReply: true });
22 |
23 | if (isMessageInstance(msg)) {
24 | const diff = msg.createdTimestamp - interaction.createdTimestamp;
25 | const ping = Math.round(this.container.client.ws.ping);
26 | return interaction.editReply(`${Emojis.Check} Pong! \`Bot: ${diff}ms\` \`API: ${ping}ms\``);
27 | }
28 |
29 | return interaction.editReply(`${Emojis.X} Failed to retrieve ping.`);
30 | }
31 | }
--------------------------------------------------------------------------------
/src/commands/Standard/quote.ts:
--------------------------------------------------------------------------------
1 | import { RTByteCommand } from '#lib/extensions/RTByteCommand';
2 | import { RTByteEmbed } from '#lib/extensions/RTByteEmbed';
3 | import { Colors } from "#utils/constants";
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { type ChatInputCommand, type ContextMenuCommand } from '@sapphire/framework';
6 | import { ApplicationCommandType, PermissionFlagsBits } from 'discord.js';
7 |
8 | @ApplyOptions({
9 | description: 'Quote a message'
10 | })
11 | export class UserCommand extends RTByteCommand {
12 | public override registerApplicationCommands(registry: ChatInputCommand.Registry) {
13 | registry.registerContextMenuCommand((builder) =>
14 | builder
15 | .setName('Quote')
16 | .setType(ApplicationCommandType.Message)
17 | .setDefaultMemberPermissions(PermissionFlagsBits.ReadMessageHistory), {
18 | idHints: [
19 | // Dev bot command
20 | '1123679487456976938'
21 | ]
22 | }
23 | );
24 | registry.registerChatInputCommand((builder) =>
25 | builder
26 | .setName(this.name)
27 | .setDescription('To quote a message, right click it, then click Apps > Quote'), {
28 | idHints: [
29 | // Dev bot command
30 | '1124655414970163270'
31 | ]
32 | }
33 | );
34 | }
35 |
36 | public async contextMenuRun(interaction: ContextMenuCommand.Interaction) {
37 | await interaction.deferReply({ ephemeral: false, fetchReply: true });
38 |
39 | if (interaction.isMessageContextMenuCommand() && interaction.targetMessage) {
40 |
41 | const message = interaction.targetMessage;
42 | const embed = new RTByteEmbed()
43 | .setAuthor({
44 | name: message.author.username,
45 | url: `https://discord.com/users/${message.author.id}`,
46 | iconURL: message.author.displayAvatarURL()
47 | })
48 | .setDescription(message.content)
49 | .setColor(message.member?.displayColor ?? Colors.White)
50 | .setTimestamp(message.createdTimestamp);
51 |
52 |
53 | await interaction.followUp({ embeds: [embed] });
54 | }
55 | }
56 |
57 | public async chatInputRun(interaction: ChatInputCommand.Interaction) {
58 | return interaction.reply({ content: 'ℹ️ To quote a message, right click it, then click **Apps** > **Quote**.', ephemeral: true });
59 | }
60 | }
--------------------------------------------------------------------------------
/src/commands/Standard/roles.ts:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | --- Temporarily disabling this command while I think about probably just doing reaction/interaction embeds instead ---
4 |
5 | import { RTByteSubCommand } from '#lib/extensions/RTByteCommand';
6 | import { RTByteEmbed } from '#lib/extensions/RTByteEmbed';
7 | import { Emojis } from '#utils/constants';
8 | import { ApplyOptions } from '@sapphire/decorators';
9 | import { type ChatInputCommand } from '@sapphire/framework';
10 | import type { Subcommand } from '@sapphire/plugin-subcommands';
11 | import { inlineCodeBlock } from '@sapphire/utilities';
12 | import type { AutocompleteInteraction, RoleResolvable } from 'discord.js';
13 |
14 | @ApplyOptions({
15 | description: 'Lets members join/leave roles that have been set as joinable on their own',
16 | subcommands: [
17 | { name: 'list', chatInputRun: 'chatInputList' },
18 | { name: 'join', chatInputRun: 'chatInputJoin' },
19 | { name: 'leave', chatInputRun: 'chatInputLeave' }
20 | ]
21 | })
22 | export class UserCommand extends RTByteSubCommand {
23 | public override registerApplicationCommands(registry: ChatInputCommand.Registry) {
24 | registry.registerChatInputCommand((builder) =>
25 | builder
26 | .setName(this.name)
27 | .setDescription(this.description)
28 | .setDMPermission(false)
29 | .addSubcommand((command) =>
30 | command
31 | .setName('list')
32 | .setDescription('List all joinable roles for the server')
33 | )
34 | .addSubcommand((command) =>
35 | command
36 | .setName('join')
37 | .setDescription('Join a role that has been set as joinable')
38 | .addStringOption((option) =>
39 | option
40 | .setName('role')
41 | .setDescription('The role you want to join')
42 | .setRequired(true)
43 | .setAutocomplete(true)
44 | )
45 | )
46 | .addSubcommand((command) =>
47 | command
48 | .setName('leave')
49 | .setDescription('Leave a role that has been set as joinable')
50 | .addStringOption((option) =>
51 | option
52 | .setName('role')
53 | .setDescription('The role you want to leave')
54 | .setRequired(true)
55 | .setAutocomplete(true)
56 | )
57 | ), {
58 | idHints: [
59 | // Dev bot command
60 | '1124416818036097135',
61 | ],
62 | });
63 | }
64 |
65 | public async chatInputList(interaction: ChatInputCommand.Interaction) {
66 | await interaction.deferReply({ ephemeral: true, fetchReply: true });
67 |
68 | const dbGuild = await this.container.prisma.guild.findUnique({ where: { id: interaction.guild?.id } });
69 | const joinableRoles = dbGuild?.joinableRoles;
70 | if (!joinableRoles?.length) return interaction.editReply(`${Emojis.X} No roles have been set as joinable.`);
71 |
72 | const roles = [];
73 | for (const role of joinableRoles) {
74 | const resolvedRole = interaction.guild?.roles.resolve(role);
75 | if (!resolvedRole) break;
76 | roles.push(`${Emojis.Bullet}${resolvedRole} - ${resolvedRole.members.size} member${resolvedRole.members.size > 1 ? 's' : resolvedRole.members.size === 0 ? '' : '' }`);
77 | }
78 |
79 | const embed = new RTByteEmbed()
80 | .setAuthor({ name: 'Joinable roles', iconURL: interaction.guild?.iconURL() ?? undefined})
81 | .setDescription(roles.join('\n'));
82 |
83 | return interaction.editReply({ content: '', embeds: [embed] });
84 | }
85 |
86 | public async chatInputJoin(interaction: ChatInputCommand.Interaction) {
87 | await interaction.deferReply({ ephemeral: true, fetchReply: true });
88 |
89 | const roleInput = interaction.options.getString('role');
90 | if (roleInput === 'none') return interaction.followUp(`${Emojis.X} No roles have been set as joinable.`);
91 |
92 | const member = interaction.guild?.members.resolve(interaction.member?.user.id as string);
93 | const resolvedRole = interaction.guild?.roles.cache.find(role => role.name === roleInput);
94 | if (!resolvedRole) return interaction.followUp(`${Emojis.X} The specified role could not be resolved.`);
95 |
96 | await member?.roles.add(resolvedRole as RoleResolvable, 'Joinable roles: member joined role');
97 | return interaction.followUp(`${Emojis.Check} Joined ${inlineCodeBlock(resolvedRole.name)}!`);
98 | }
99 |
100 | public async chatInputLeave(interaction: ChatInputCommand.Interaction) {
101 | await interaction.deferReply({ ephemeral: true, fetchReply: true });
102 |
103 | const roleInput = interaction.options.getString('role');
104 | if (roleInput === 'none') return interaction.followUp(`${Emojis.X} None of your roles are set as joinable.`);
105 |
106 | const member = interaction.guild?.members.resolve(interaction.member?.user.id as string);
107 | const resolvedRole = interaction.guild?.roles.cache.find(role => role.name === roleInput);
108 | if (!resolvedRole) return interaction.followUp(`${Emojis.X} The specified role could not be resolved.`);
109 |
110 | await member?.roles.remove(resolvedRole, 'Joinable roles: member left role');
111 | return interaction.followUp(`${Emojis.Check} Left ${inlineCodeBlock(resolvedRole.name)}`);
112 | }
113 |
114 | public async autocompleteRun(interaction: AutocompleteInteraction) {
115 | const focusedOption = interaction.options.getFocused(true);
116 | const subcommand = interaction.options.getSubcommand(true);
117 |
118 | if (focusedOption.name === 'role' && subcommand === 'join') {
119 | const dbGuild = await this.container.prisma.guild.findUnique({ where: { id: interaction.guild?.id } });
120 | const member = interaction.guild?.members.resolve(interaction.user.id);
121 | const joinableRoles = dbGuild?.joinableRoles.filter(r => !member?.roles.cache.has(r));
122 | if (!joinableRoles?.length) return interaction.respond([{ name: 'None available', value: 'none' }]);
123 |
124 | const roles = [];
125 | for (const role of joinableRoles) {
126 | const resolvedRole = interaction.guild?.roles.resolve(role);
127 | if (!resolvedRole) break;
128 | roles.push(resolvedRole.name);
129 | }
130 |
131 | return interaction.respond(
132 | roles.map(role => ({ name: role, value: role}))
133 | );
134 | }
135 |
136 | if (focusedOption.name === 'role' && subcommand === 'leave') {
137 | const dbGuild = await this.container.prisma.guild.findUnique({ where: { id: interaction.guild?.id } });
138 | const joinableRoles = dbGuild?.joinableRoles;
139 | const member = interaction.guild?.members.resolve(interaction.member?.user.id as string);
140 |
141 | const leavableRoles = member?.roles.cache.filter(role => joinableRoles?.includes(role.id)).map(role => ({ name: role.name, value: role.name }));
142 | if (!leavableRoles?.length) return interaction.respond([{ name: 'None available', value: 'none' }]);
143 |
144 | return interaction.respond(leavableRoles);
145 | }
146 | }
147 | }
148 | */
--------------------------------------------------------------------------------
/src/commands/Standard/weather.ts:
--------------------------------------------------------------------------------
1 | import { RTByteCommand } from "#lib/extensions/RTByteCommand";
2 | import { RTByteEmbed } from "#lib/extensions/RTByteEmbed";
3 | import { API_KEYS } from "#root/config";
4 | import { ApplyOptions } from "@sapphire/decorators";
5 | import { FetchResultTypes, fetch } from '@sapphire/fetch';
6 | import type { ChatInputCommand } from "@sapphire/framework";
7 | import { codeBlock, inlineCodeBlock } from "@sapphire/utilities";
8 |
9 | @ApplyOptions({
10 | description: 'Fetch the current weather for a specified location'
11 | })
12 | export class UserCommand extends RTByteCommand {
13 | public override registerApplicationCommands(registry: ChatInputCommand.Registry) {
14 | registry.registerChatInputCommand((builder) =>
15 | builder
16 | .setName(this.name)
17 | .setDescription(this.description)
18 | .addStringOption((option) =>
19 | option
20 | .setName('location')
21 | .setDescription('The location to fetch the current weather for')
22 | .setRequired(true)
23 | )
24 | .addStringOption((option) =>
25 | option
26 | .setName('units')
27 | .setDescription('Specify if the result should use Celsius & the Metric System or Fahrenheit & the Imperial System')
28 | .setRequired(false)
29 | .addChoices(
30 | { name: 'celsius & metric', value: 'metric' },
31 | { name: 'fahrenheit & imperial', value: 'imperial' }
32 | )
33 | ), {
34 | idHints: [
35 | // Dev bot command
36 | '1122151286683476068'
37 | ]
38 | }
39 | );
40 | }
41 |
42 | public async chatInputRun(interaction: ChatInputCommand.Interaction) {
43 | const locationInput = interaction.options.getString('location') as string;
44 |
45 | await interaction.deferReply({ ephemeral: false, fetchReply: true });
46 |
47 | const encodedLocation = encodeURI(locationInput);
48 | const units = interaction.options.getString('units') || 'metric'
49 | const speedUnits = ['m/s', 'mph'];
50 | const directions = ['north', 'northeast', 'east', 'southeast', 'south', 'southwest', 'west', 'northwest'];
51 |
52 | // Geocode location using Google Maps, then sort into object
53 | const googleMaps = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodedLocation}&key=${API_KEYS.GOOGLE_MAPS}`;
54 | const { results } = await fetch(googleMaps, FetchResultTypes.JSON);
55 | // Inform user if specified location cannot be resolved
56 | if (!results.length) return interaction.editReply(`${inlineCodeBlock(locationInput)} could not be resolved to a location.`);
57 | const location = {
58 | lat: results[0].geometry.location.lat,
59 | lon: results[0].geometry.location.lng,
60 | name: results[0].formatted_address,
61 | country: results[0].address_components.find(entry => entry.types.includes('country'))?.short_name,
62 | mapsLink: `https://www.google.com/maps/@${results[0].geometry.location.lat},${results[0].geometry.location.lng},13z`
63 | }
64 |
65 | // Fetch weather for geocoded location using OpenWeather, then sort into object
66 | const openWeather = `https://api.openweathermap.org/data/2.5/onecall?lat=${location.lat}&lon=${location.lon}&units=${units}&appid=${API_KEYS.OPENWEATHER}`;
67 | const { timezone, current, alerts } = await fetch(openWeather, FetchResultTypes.JSON);
68 | if (!current.temp) return interaction.editReply(`Unable to fetch the weather for ${inlineCodeBlock(location.name)} (input was ${inlineCodeBlock(locationInput)}).`);
69 | const weather = {
70 | localTime: new Date(current.dt * 1000).toLocaleString(interaction.locale, { timeZone: timezone, hour: 'numeric', minute: 'numeric' }),
71 | temperature: Math.round(current.temp),
72 | feelsLike: Math.round(current.feels_like),
73 | humidity: `${current.humidity}%`,
74 | uvIndex: current.uvi,
75 | windDirection: directions[Math.round(((current.wind_deg %= 360 ) < 0 ? current.wind_deg + 360 : current.wind_deg) / 45) % 8],
76 | windSpeed: `${current.wind_speed} ${units === 'imperial' ? speedUnits[1] : speedUnits[0]}`,
77 | weather: current.weather[0].description.charAt(0).toUpperCase() + current.weather[0].description.slice(1),
78 | icon: current.weather[0].icon
79 | }
80 |
81 | // Build embed from all collected data
82 | const embed = new RTByteEmbed()
83 | .setAuthor({
84 | name: location.name,
85 | url: location.mapsLink,
86 | iconURL: location.country ? `https://flagcdn.com/w40/${location.country.toLowerCase()}.png` : undefined
87 | })
88 | .setThumbnail(`http://openweathermap.org/img/wn/${weather.icon}@4x.png`)
89 | .addFields(
90 | { name: 'Local time', value: inlineCodeBlock(weather.localTime), inline: true },
91 | { name: 'Weather', value: weather.weather, inline: true },
92 | { name: 'Temperature', value: `${weather.temperature}° ${units === 'imperial' ? 'F' : 'C'} (feels like ${weather.feelsLike}°)`, inline: true },
93 | { name: 'UV index', value: inlineCodeBlock(`${weather.uvIndex}`), inline: true },
94 | { name: 'Wind', value: `${weather.windSpeed} ${weather.windDirection}`, inline: true },
95 | { name: 'Humidity', value: inlineCodeBlock(weather.humidity), inline: true }
96 | );
97 |
98 | if (alerts) {
99 | const alert = {
100 | sender: alerts[0].sender_name,
101 | event: alerts[0].event.charAt(0).toLocaleUpperCase() + alerts[0].event.slice(1),
102 | start: new Date(alerts[0].start * 1000).toLocaleString(interaction.locale, { timeZone: timezone }),
103 | end: new Date(alerts[0].end * 1000).toLocaleString(interaction.locale, { timeZone: timezone }),
104 | desciption: alerts[0].description
105 | };
106 |
107 | embed.addBlankField();
108 | if (alert.event) embed.addFields({ name: '⚠️ Weather alert', value: alert.event});
109 | if (alert.sender) embed.addFields({ name: 'Sender', value: alert.sender });
110 | if (alert.desciption) embed.addFields({ name: 'Description', value: codeBlock('', alert.desciption) });
111 | if (alert.start) embed.addFields({ name: 'From', value: inlineCodeBlock(alert.start), inline: true });
112 | if (alert.end) embed.addFields({ name: 'Until', value: inlineCodeBlock(alert.end), inline: true });
113 | }
114 |
115 | return interaction.editReply({ content: '', embeds: [embed] });
116 | }
117 | }
118 |
119 | export interface GoogleMapsResultOk {
120 | results: [
121 | {
122 | address_components: [
123 | {
124 | long_name: string,
125 | short_name: string,
126 | types: string[]
127 | }
128 | ]
129 | formatted_address: string,
130 | geometry: {
131 | location: {
132 | lat: number,
133 | lng: number
134 | }
135 | }
136 | }
137 | ]
138 | }
139 |
140 | export interface OpenWeatherResultOk {
141 | timezone: string,
142 | current: {
143 | dt: number
144 | temp: number,
145 | feels_like: number,
146 | humidity: number,
147 | uvi: number,
148 | wind_speed: number,
149 | wind_deg: number
150 | weather: [
151 | {
152 | description: string,
153 | icon: string
154 | }
155 | ]
156 | },
157 | alerts: [
158 | {
159 | sender_name: string,
160 | event: string,
161 | start: number,
162 | end: number,
163 | description: string,
164 | tags: string[]
165 | }
166 | ]
167 | }
168 |
--------------------------------------------------------------------------------
/src/config.example.ts:
--------------------------------------------------------------------------------
1 | import { transformLoginDataGuilds } from '#root/transformers/loginDataGuilds';
2 | import { LogLevel } from '@sapphire/framework';
3 | import { GatewayIntentBits, OAuth2Scopes, Partials, type ClientOptions } from 'discord.js';
4 |
5 | export const DEV = process.env.NODE_ENV !== 'production';
6 |
7 | export const CLIENT_ID = '';
8 | export const CONTROL_GUILD = '';
9 | export const OWNERS: string[] = [''];
10 | export const PREFIX = '=';
11 | export const VERSION = '0.0.0';
12 | export const INIT_ALL_USERS = false;
13 | export const INIT_ALL_MEMBERS = false;
14 |
15 | export const CLIENT_OPTIONS: ClientOptions = {
16 | caseInsensitiveCommands: true,
17 | caseInsensitivePrefixes: true,
18 | defaultPrefix: PREFIX,
19 | regexPrefix: /^(hey +)?bot[,! ]/i,
20 | shards: 'auto',
21 | intents: [
22 | GatewayIntentBits.Guilds,
23 | GatewayIntentBits.GuildMembers,
24 | GatewayIntentBits.GuildModeration,
25 | GatewayIntentBits.GuildEmojisAndStickers,
26 | GatewayIntentBits.GuildIntegrations,
27 | GatewayIntentBits.GuildWebhooks,
28 | GatewayIntentBits.GuildInvites,
29 | GatewayIntentBits.GuildVoiceStates,
30 | GatewayIntentBits.GuildPresences,
31 | GatewayIntentBits.GuildMessages,
32 | GatewayIntentBits.GuildMessageReactions,
33 | GatewayIntentBits.MessageContent,
34 | GatewayIntentBits.GuildScheduledEvents,
35 | GatewayIntentBits.AutoModerationConfiguration,
36 | GatewayIntentBits.AutoModerationExecution
37 | ],
38 | loadDefaultErrorListeners: false,
39 | partials: [Partials.Channel, Partials.GuildMember, Partials.GuildScheduledEvent, Partials.Message, Partials.Reaction, Partials.User],
40 | presence: {
41 | activities: [
42 | {
43 | name: '',
44 | type: 3
45 | }
46 | ]
47 | },
48 | logger: {
49 | level: DEV ? LogLevel.Debug : LogLevel.Info
50 | },
51 | api: {
52 | auth: {
53 | id: CLIENT_ID,
54 | secret: '',
55 | cookie: 'RTBYTE_AUTH',
56 | redirect: '',
57 | scopes: [OAuth2Scopes.Identify, OAuth2Scopes.Guilds, OAuth2Scopes.GuildsMembersRead],
58 | transformers: [transformLoginDataGuilds]
59 | },
60 | prefix: '/',
61 | origin: '*',
62 | listenOptions: {
63 | port: 4000
64 | }
65 | }
66 | };
67 |
68 | export const API_KEYS = {
69 | GOOGLE_MAPS: '',
70 | OPENWEATHER: ''
71 | }
72 |
73 | export const TOKENS = {
74 | BOT_TOKEN: '',
75 | SENTRY_DNS: '',
76 | };
77 |
--------------------------------------------------------------------------------
/src/lib/RTByteClient.ts:
--------------------------------------------------------------------------------
1 | import { CLIENT_OPTIONS } from "#root/config";
2 | import { PrismaClient } from "@prisma/client";
3 | import { SapphireClient } from '@sapphire/framework';
4 |
5 | export class RTByteClient extends SapphireClient {
6 | public constructor() {
7 | super(CLIENT_OPTIONS);
8 | }
9 |
10 | public async login(token?: string) {
11 | this.logger.info('Connecting to Discord...');
12 | return super.login(token);
13 | }
14 |
15 | public destroy() {
16 | return super.destroy();
17 | }
18 | }
19 |
20 | declare module 'discord.js' {
21 | export interface Client {
22 | prisma: PrismaClient;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/lib/extensions/GuildLogEmbed.ts:
--------------------------------------------------------------------------------
1 | import { RTByteEmbed } from "#lib/extensions/RTByteEmbed";
2 | import { Colors } from "#utils/constants";
3 | import { Events } from "@sapphire/framework";
4 |
5 | export class GuildLogEmbed extends RTByteEmbed {
6 | public setType(type: string) {
7 | switch(type) {
8 | case Events.AutoModerationRuleCreate:
9 | case Events.ChannelCreate:
10 | case Events.GuildEmojiCreate:
11 | case Events.GuildMemberAdd:
12 | case Events.GuildMembersChunk:
13 | case Events.GuildRoleCreate:
14 | case Events.GuildScheduledEventCreate:
15 | case Events.GuildScheduledEventUserAdd:
16 | case Events.GuildStickerCreate:
17 | case Events.InviteCreate:
18 | case Events.StageInstanceCreate:
19 | case Events.ThreadCreate:
20 | this.setColor(Colors.Green);
21 | break;
22 | case Events.AutoModerationRuleDelete:
23 | case Events.ChannelDelete:
24 | case Events.GuildEmojiDelete:
25 | case Events.GuildMemberRemove:
26 | case Events.GuildRoleDelete:
27 | case Events.GuildScheduledEventDelete:
28 | case Events.GuildScheduledEventUserRemove:
29 | case Events.GuildStickerDelete:
30 | case Events.InviteDelete:
31 | case Events.MessageBulkDelete:
32 | case Events.MessageDelete:
33 | case Events.MessageReactionRemoveAll:
34 | case Events.MessageReactionRemoveEmoji:
35 | case Events.StageInstanceDelete:
36 | case Events.ThreadDelete:
37 | this.setColor(Colors.Red);
38 | break;
39 | case Events.AutoModerationRuleUpdate:
40 | case Events.ChannelUpdate:
41 | case Events.GuildEmojiUpdate:
42 | case Events.GuildMemberUpdate:
43 | case Events.GuildRoleUpdate:
44 | case Events.GuildScheduledEventUpdate:
45 | case Events.GuildStickerUpdate:
46 | case Events.GuildUpdate:
47 | case Events.MessageUpdate:
48 | case Events.StageInstanceUpdate:
49 | case Events.ThreadUpdate:
50 | this.setColor(Colors.Yellow);
51 | break;
52 | }
53 |
54 | return this;
55 | }
56 | }
--------------------------------------------------------------------------------
/src/lib/extensions/RTByteCommand.ts:
--------------------------------------------------------------------------------
1 | import { Command } from '@sapphire/framework';
2 | import { Subcommand } from '@sapphire/plugin-subcommands';
3 | import { PermissionFlagsBits, PermissionsBitField } from 'discord.js';
4 |
5 | export abstract class RTByteCommand extends Command {
6 | public constructor(context: Command.Context, options: Command.Options) {
7 | const resolvedPermissions = new PermissionsBitField(options.requiredClientPermissions).add(PermissionFlagsBits.EmbedLinks);
8 |
9 | super(context, {
10 | requiredClientPermissions: resolvedPermissions,
11 | ...options
12 | });
13 | }
14 | }
15 |
16 | export abstract class RTByteSubCommand extends Subcommand {
17 | public constructor(context: Command.Context, options: Command.Options) {
18 | const resolvedPermissions = new PermissionsBitField(options.requiredClientPermissions).add(PermissionFlagsBits.EmbedLinks);
19 |
20 | super(context, {
21 | requiredClientPermissions: resolvedPermissions,
22 | ...options
23 | });
24 | }
25 | }
--------------------------------------------------------------------------------
/src/lib/extensions/RTByteEmbed.ts:
--------------------------------------------------------------------------------
1 | import { Colors, ZeroWidthSpace } from "#utils/constants";
2 | import { EmbedBuilder, type APIEmbedField } from "discord.js";
3 |
4 | export class RTByteEmbed extends EmbedBuilder {
5 | public constructor() {
6 | super();
7 | this.setColor(Colors.White)
8 | this.setTimestamp()
9 | }
10 |
11 | public addBlankField(fields?: APIEmbedField) {
12 | if (!fields) return this.addFields({ name: ZeroWidthSpace, value: ZeroWidthSpace });
13 |
14 | const fieldName: string = fields.name.length ? fields.name : ZeroWidthSpace;
15 | const fieldValue: string = fields.value.length ? fields.value : ZeroWidthSpace;
16 | const fieldInline: boolean = fields.inline ?? false;
17 | return this.addFields({ name: fieldName, value: fieldValue, inline: fieldInline })
18 | }
19 | }
--------------------------------------------------------------------------------
/src/lib/extensions/index.ts:
--------------------------------------------------------------------------------
1 | export * from '#lib/extensions/RTByteCommand';
2 | export * from '#lib/extensions/RTByteEmbed';
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/setup.ts:
--------------------------------------------------------------------------------
1 | // Unless explicitly defined, set NODE_ENV as development:
2 | process.env.NODE_ENV ??= 'development';
3 |
4 | import '#utils/Sanitizer/initClean';
5 | import { PrismaClient } from '@prisma/client';
6 | import { container } from '@sapphire/framework';
7 | import '@sapphire/plugin-api/register';
8 | import '@sapphire/plugin-logger/register';
9 | import { createColors } from 'colorette';
10 | import { inspect } from 'util';
11 |
12 | // TODO: Implement prisma-field-encryption
13 | const prisma = new PrismaClient();
14 |
15 | inspect.defaultOptions.depth = 1;
16 | createColors({ useColor: true });
17 | container.prisma = prisma;
18 |
19 | declare module '@sapphire/pieces' {
20 | interface Container {
21 | prisma: typeof prisma;
22 | }
23 | }
--------------------------------------------------------------------------------
/src/lib/util/Sanitizer/clean.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * MIT License
4 | *
5 | * Copyright (c) 2017-2019 dirigeants
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | * @url https://github.com/dirigeants/utils/blob/cc5699d98c92356a483b2b2192ba446173294fe4/src/lib/clean.ts
25 | */
26 | import { regExpEsc } from '@sapphire/utilities';
27 |
28 | let sensitivePattern: string | RegExp;
29 | const zws = String.fromCharCode(8203);
30 |
31 | /**
32 | * Cleans sensitive info from strings
33 | * @since 0.0.1
34 | * @param text The text to clean
35 | */
36 | export function clean(text: string) {
37 | if (typeof sensitivePattern === 'undefined') {
38 | throw new Error('initClean must be called before running this.');
39 | }
40 | return text.replace(sensitivePattern, '「redacted」').replace(/`/g, `\`${zws}`).replace(/@/g, `@${zws}`);
41 | }
42 |
43 | /**
44 | * Initializes the sensitive patterns for clean()
45 | * @param tokens The tokens to clean
46 | */
47 | export function initClean(tokens: readonly string[]) {
48 | sensitivePattern = new RegExp(tokens.map(regExpEsc).join('|'), 'gi');
49 | }
50 |
--------------------------------------------------------------------------------
/src/lib/util/Sanitizer/initClean.ts:
--------------------------------------------------------------------------------
1 | import { TOKENS } from '#root/config';
2 | import { initClean } from '#utils/Sanitizer/clean';
3 | import { isNullishOrEmpty } from '@sapphire/utilities';
4 |
5 | const secrets = new Set();
6 |
7 | for (const [value] of Object.entries(TOKENS)) {
8 | if (isNullishOrEmpty(value)) continue;
9 |
10 | secrets.add(value);
11 | }
12 |
13 | initClean([...secrets]);
14 |
--------------------------------------------------------------------------------
/src/lib/util/common/times.ts:
--------------------------------------------------------------------------------
1 | import { Time } from '@sapphire/time-utilities';
2 |
3 | /**
4 | * Converts a number of seconds to milliseconds.
5 | * @param seconds The amount of seconds
6 | * @returns The amount of milliseconds `seconds` equals to.
7 | */
8 | export function seconds(seconds: number): number {
9 | return seconds * Time.Second;
10 | }
11 |
12 | /**
13 | * Converts a number of minutes to milliseconds.
14 | * @param minutes The amount of minutes
15 | * @returns The amount of milliseconds `minutes` equals to.
16 | */
17 | export function minutes(minutes: number): number {
18 | return minutes * Time.Minute;
19 | }
20 |
21 | /**
22 | * Converts a number of hours to milliseconds.
23 | * @param hours The amount of hours
24 | * @returns The amount of milliseconds `hours` equals to.
25 | */
26 | export function hours(hours: number): number {
27 | return hours * Time.Hour;
28 | }
29 |
30 | /**
31 | * Converts a number of days to milliseconds.
32 | * @param days The amount of days
33 | * @returns The amount of milliseconds `days` equals to.
34 | */
35 | export function days(days: number): number {
36 | return days * Time.Day;
37 | }
38 |
39 | /**
40 | * Converts a number of weeks to milliseconds.
41 | * @param weeks The amount of weeks
42 | * @returns The amount of milliseconds `weeks` equals to.
43 | */
44 | export function weeks(weeks: number): number {
45 | return weeks * Time.Week;
46 | }
47 |
48 | /**
49 | * Converts a number of months to milliseconds.
50 | * @param months The amount of months
51 | * @returns The amount of milliseconds `months` equals to.
52 | */
53 | export function months(months: number): number {
54 | return months * Time.Month;
55 | }
56 |
57 | /**
58 | * Converts a number of years to milliseconds.
59 | * @param years The amount of years
60 | * @returns The amount of milliseconds `years` equals to.
61 | */
62 | export function years(years: number): number {
63 | return years * Time.Year;
64 | }
65 |
--------------------------------------------------------------------------------
/src/lib/util/constants.ts:
--------------------------------------------------------------------------------
1 | export const ZeroWidthSpace = '\u200B';
2 |
3 | export const enum Colors {
4 | Green = '#2DC770',
5 | Red = '#F23A38',
6 | White = '#FEFEFE',
7 | Yellow = '#F0A431'
8 | }
9 |
10 | export const enum Emojis {
11 | Bullet = '<:bulletPoint:1127188144803041320>',
12 | Check = '<:checkMark:1127182446971072643>',
13 | ToggleOff = '<:toggleOff:1127185093379751976>',
14 | ToggleOn = '<:toggleOn:1127182742514311229>',
15 | Warning = '<:rtbyte_warning:898950475628552223>',
16 | X = '<:xMark:1127184643460960286>'
17 | }
--------------------------------------------------------------------------------
/src/lib/util/decorators/routeAuthenticated.ts:
--------------------------------------------------------------------------------
1 | import { createFunctionPrecondition } from '@sapphire/decorators';
2 | import { ApiRequest, ApiResponse, HttpCodes } from '@sapphire/plugin-api';
3 |
4 | export const authenticated = () =>
5 | createFunctionPrecondition(
6 | (request: ApiRequest) => Boolean(request.auth?.token),
7 | (_request: ApiRequest, response: ApiResponse) => response.error(HttpCodes.Unauthorized)
8 | );
--------------------------------------------------------------------------------
/src/lib/util/functions/initialize.ts:
--------------------------------------------------------------------------------
1 | import { container } from '@sapphire/framework';
2 | import { bold, gray } from 'colorette';
3 | import { Guild, User } from "discord.js";
4 |
5 | export async function initializeGuild(guild: Guild) {
6 | const { logger, prisma } = container;
7 | const clientSettings = await prisma.clientSettings.findFirst();
8 |
9 | if (clientSettings?.guildBlacklist.includes(guild.id)) {
10 | await guild.leave();
11 | logger.debug(`Guild ${bold(guild.name)} (${gray(guild.id)}) is on the guild blacklist, leaving...`);
12 | }
13 |
14 | if (clientSettings?.userBlacklist.includes(guild.ownerId)) {
15 | await guild.leave();
16 | logger.debug(`Guild ${bold(guild.name)} (${gray(guild.id)}) is owned by user (${guild.ownerId}) on global blacklist, leaving...`);
17 | }
18 |
19 | // Check if entry exists for guild. If not, create it
20 | const guildInfo = await prisma.guild.findUnique({ where: { id: guild.id } });
21 | const guildSettings = await prisma.guildSettings.findUnique({ where: { id: guild.id } });
22 | const guildSettingsChatFilter = await prisma.guildSettingsChatFilter.findUnique({ where: { id: guild.id } });
23 | const guildSettingsLogs = await prisma.guildSettingsInfoLogs.findUnique({ where: { id: guild.id } });
24 | const guildSettingsModActions = await prisma.guildSettingsModActions.findUnique({ where: { id: guild.id } });
25 |
26 | if (!guildInfo || !guildSettings || !guildSettingsChatFilter || !guildSettingsLogs || !guildSettingsModActions) {
27 | logger.debug(`Initializing guild ${bold(guild.name)} (${gray(guild.id)})...`)
28 |
29 | if (!guildInfo) {
30 | await prisma.guild.create({
31 | data: {
32 | id: guild.id
33 | }
34 | }).catch(e => {
35 | logger.error(`Failed to initialize guild info for ${bold(guild.name)} (${gray(guild.id)}), error below.`);
36 | logger.error(e);
37 | });
38 | }
39 |
40 | if (!guildSettings) {
41 | await prisma.guildSettings.create({
42 | data: {
43 | id: guild.id
44 | }
45 | }).catch(e => {
46 | logger.error(`Failed to initialize guildSettings for ${bold(guild.name)} (${gray(guild.id)}), error below.`);
47 | logger.error(e);
48 | });
49 | }
50 |
51 | if (!guildSettingsChatFilter) {
52 | await prisma.guildSettingsChatFilter.create({
53 | data: {
54 | id: guild.id
55 | }
56 | }).catch(e => {
57 | logger.error(`Failed to initialize guildSettingsChatFilter for ${bold(guild.name)} (${gray(guild.id)}), error below.`);
58 | logger.error(e);
59 | });
60 | }
61 |
62 | if (!guildSettingsLogs) {
63 | await prisma.guildSettingsInfoLogs.create({
64 | data: {
65 | id: guild.id
66 | }
67 | }).catch(e => {
68 | logger.error(`Failed to initialize guildSettingsLogs for ${bold(guild.name)} (${gray(guild.id)}), error below.`);
69 | logger.error(e);
70 | });
71 | }
72 |
73 | if (!guildSettingsModActions) {
74 | await prisma.guildSettingsModActions.create({
75 | data: {
76 | id: guild.id
77 | }
78 | }).catch(e => {
79 | logger.error(`Failed to initialize guildSettingsModActions for ${bold(guild.name)} (${gray(guild.id)}), error below.`);
80 | logger.error(e);
81 | });
82 | }
83 |
84 | }
85 |
86 | logger.debug(`Verified initialization of guild ${bold(guild.name)} (${gray(guild.id)})`);
87 | }
88 |
89 | export async function initializeUser(user?: User, userID?: string) {
90 | const { logger, prisma } = container;
91 | if (!user && !userID) {
92 | throw logger.error(`Failed to initialize user info. No user identifier specified.`);
93 | }
94 |
95 | const clientSettings = await prisma.clientSettings.findFirst();
96 |
97 | if (user || !userID) userID = user?.id;
98 |
99 | logger.debug(`Initializing user ${user ? bold(user.username) : '...'} (${gray(userID!)})...`);
100 |
101 | if (clientSettings?.userBlacklist.includes(userID!)) logger.debug(`User ${user ? bold(user.username) : '...'} (${gray(userID!)}) is on the user blacklist...`);
102 |
103 | const userInfo = await prisma.user.findUnique({ where: { id: userID } });
104 | const userSettings = await prisma.userSettings.findUnique({ where: { id: userID } });
105 |
106 | if (!userInfo) {
107 | await prisma.user.create({
108 | data: {
109 | id: userID!
110 | }
111 | }).catch(e => {
112 | logger.error(`Failed to initialize user info for ${user ? bold(user.username) : '...'} (${gray(userID!)}), error below.`);
113 | logger.error(e);
114 | });
115 | }
116 |
117 | if (!userSettings) {
118 | await prisma.userSettings.create({
119 | data: {
120 | id: userID!
121 | }
122 | }).catch(e => {
123 | logger.error(`Failed to initialize user settings for ${user ? bold(user.username) : '...'} (${gray(userID!)}), error below.`);
124 | logger.error(e);
125 | });
126 | }
127 |
128 | return logger.debug(`Verified initialization of user ${user ? bold(user.username) : '...'} (${gray(userID!)})`);
129 | }
130 |
131 | export async function initializeMember(user: User, guild: Guild) {
132 | const { logger, prisma } = container;
133 | await initializeUser(user);
134 | const clientSettings = await prisma.clientSettings.findFirst();
135 |
136 | logger.debug(`Initializing member ${bold(user.username)} (${gray(user.id)}) in guild ${bold(guild.name)} (${gray(guild.id)})...`);
137 |
138 | if (clientSettings?.userBlacklist.includes(user.id)) logger.debug(`User ${bold(user.username)} (${gray(user.id)}) is on the user blacklist...`);
139 |
140 | const memberInfo = await prisma.member.findFirst({ where: { userID: user.id, guildID: guild.id } });
141 |
142 | const member = await guild.members.fetch(user.id);
143 |
144 | if (!memberInfo) {
145 | const joinTimes: Date[] = [];
146 | if (member && member.joinedAt) joinTimes.push(member.joinedAt);
147 | await prisma.member.create({
148 | data: {
149 | userID: `${user.id}`,
150 | guildID: `${guild.id}`,
151 | joinTimes
152 | }
153 | }).catch(e => {
154 | logger.error(`Failed to initialize member info for ${bold(user.username)} (${gray(user.id)}) in guild ${bold(guild.name)} (${gray(guild.id)}), error below.`);
155 | return logger.error(e);
156 | });
157 | }
158 |
159 | return logger.debug(`Verified initialization of member ${bold(user.username)} (${gray(user.id)}) in guild ${bold(guild.name)} (${gray(guild.id)})`);
160 | }
--------------------------------------------------------------------------------
/src/lib/util/functions/permissions.ts:
--------------------------------------------------------------------------------
1 | import { getPermissionString } from "#utils/util";
2 | import { GuildMember, PermissionsBitField } from "discord.js";
3 |
4 | export function isModerator(member: GuildMember) {
5 | return isGuildOwner(member) || checkModerator(member) || checkAdministrator(member);
6 | }
7 |
8 | export function isAdmin(member: GuildMember) {
9 | return isGuildOwner(member) || checkAdministrator(member);
10 | }
11 |
12 | export function isGuildOwner(member: GuildMember) {
13 | return member.id === member.guild.ownerId;
14 | }
15 |
16 | export function checkRoleHierarchy(member: GuildMember, executor: GuildMember) {
17 | return member.roles.highest.position < executor.roles.highest.position;
18 | }
19 |
20 | function checkModerator(member: GuildMember) {
21 | return member.permissions.has(PermissionsBitField.Flags.KickMembers);
22 | }
23 |
24 | function checkAdministrator(member: GuildMember) {
25 | return member.permissions.has(PermissionsBitField.Flags.ManageGuild);
26 | }
27 |
28 | export function getPermissionDifference(oldPermissions: PermissionsBitField, permissions: PermissionsBitField) {
29 | const added = oldPermissions.missing(permissions).map(perm => getPermissionString(perm));
30 | const removed = permissions.missing(oldPermissions).map(perm => getPermissionString(perm));
31 | const differences = {
32 | added,
33 | removed
34 | }
35 |
36 | return differences;
37 | }
38 |
--------------------------------------------------------------------------------
/src/lib/util/util.ts:
--------------------------------------------------------------------------------
1 | import { container } from "@sapphire/framework";
2 | import { inlineCodeBlock } from "@sapphire/utilities";
3 | import { PermissionFlagsBits, StageChannel, VoiceChannel, type AuditLogEvent, type Guild, type Message } from "discord.js";
4 |
5 | /**
6 | * Get the executor user from the last audit log entry of specific type
7 | * @param action The audit log type to fetch
8 | * @param guild The Guild object to get audit logs for
9 | * @returns Executor User object from the last audit log entry of specific type.
10 | */
11 | export async function getAuditLogExecutor(action: AuditLogEvent, guild: Guild) {
12 | if (!guild.members.cache.get(container.client.user!.id)?.permissions.has(PermissionFlagsBits.ViewAuditLog)) return;
13 |
14 | return (await guild.fetchAuditLogs({ type: action })).entries.first()?.executor;
15 | }
16 |
17 | /**
18 | * Get the content from a message.
19 | * @param message The Message instance to get the content from
20 | */
21 | export function getContent(message: Message): string | null {
22 | if (message.content) return message.content;
23 | for (const embed of message.embeds) {
24 | if (embed.description) return embed.description;
25 | if (embed.fields.length) return embed.fields[0].value;
26 | }
27 | return null;
28 | }
29 |
30 | /**
31 | * Get the content from a message.
32 | * @param channel The Stage or Voice channel to get the region override from
33 | */
34 | export function getRegionOverride(channel: StageChannel | VoiceChannel) {
35 | switch (channel.rtcRegion) {
36 | case 'brazil':
37 | return `🇧🇷 ${inlineCodeBlock(`Brazil`)}`
38 | case 'hongkong':
39 | return `🇭🇰 ${inlineCodeBlock(`Hong Kong`)}`
40 | case 'india':
41 | return `🇮🇳 ${inlineCodeBlock(`India`)}`
42 | case 'japan':
43 | return `🇯🇵 ${inlineCodeBlock(`Japan`)}`
44 | case 'rotterdam':
45 | return `🇳🇱 ${inlineCodeBlock(`Rotterdam`)}`
46 | case 'russia':
47 | return `🇷🇺 ${inlineCodeBlock(`Russia`)}`
48 | case 'singapore':
49 | return `🇸🇬 ${inlineCodeBlock(`Singapore`)}`
50 | case 'southafrica':
51 | return `🇿🇦 ${inlineCodeBlock(`South Africa`)}`
52 | case 'sydney':
53 | return `🇦🇺 ${inlineCodeBlock(`Sydney`)}`
54 | case 'us-cental':
55 | return `🇺🇸 ${inlineCodeBlock(`US Central`)}`
56 | case 'us-east':
57 | return `🇺🇸 ${inlineCodeBlock(`US East`)}`
58 | case 'us-south':
59 | return `🇺🇸 ${inlineCodeBlock(`US South`)}`
60 | case 'us-west':
61 | return `🇺🇸 ${inlineCodeBlock(`US West`)}`
62 | default:
63 | return `🗺️ ${inlineCodeBlock(`Automatic`)}`
64 | }
65 | }
66 |
67 | /**
68 | * Get the content from a message.
69 | * @param permission The permission to get the string for
70 | */
71 | export function getPermissionString(permission: string) {
72 | switch (permission) {
73 | case 'ViewChannel': return inlineCodeBlock('View channels');
74 | case 'ManageChannels': return inlineCodeBlock('Manage channels');
75 | case 'ManageRoles': return inlineCodeBlock('Manage roles');
76 | case 'ManageGuildExpressions': return inlineCodeBlock('Manage expressions');
77 | case 'ViewAuditLog': return inlineCodeBlock('View audit log');
78 | case 'ViewGuildInsights': return inlineCodeBlock('View server insights');
79 | case 'ManageWebhooks': return inlineCodeBlock('Manage webhooks');
80 | case 'ManageGuild': return inlineCodeBlock('Manage server');
81 | case 'CreateInstantInvite': return inlineCodeBlock('Create invite');
82 | case 'ChangeNickname': return inlineCodeBlock('Change nickname');
83 | case 'ManageNicknames': return inlineCodeBlock('Manage nicknames');
84 | case 'KickMembers': return inlineCodeBlock('Kick members');
85 | case 'BanMembers': return inlineCodeBlock('Ban members');
86 | case 'ModerateMembers': return inlineCodeBlock('Timeout members');
87 | case 'SendMessages': return inlineCodeBlock('Send messages');
88 | case 'SendMessagesInThreads': return inlineCodeBlock('Send messages in threads');
89 | case 'CreatePublicThreads': return inlineCodeBlock('Create public threads');
90 | case 'CreatePrivateThreads': return inlineCodeBlock('Create private threads');
91 | case 'EmbedLinks': return inlineCodeBlock('Embed links');
92 | case 'AttachFiles': return inlineCodeBlock('Attach files');
93 | case 'AddReactions': return inlineCodeBlock('Add reactions');
94 | case 'UseExternalEmojis': return inlineCodeBlock('Use external emoji');
95 | case 'UseExternalStickers': return inlineCodeBlock('User external stickers');
96 | case 'MentionEveryone': return inlineCodeBlock('Mention @everyone, @here, and all roles');
97 | case 'ManageMessages': return inlineCodeBlock('Manage messages');
98 | case 'ManageThreads': return inlineCodeBlock('Manage threads');
99 | case 'ReadMessageHistory': return inlineCodeBlock('Read message history');
100 | case 'SendTTSMessages': return inlineCodeBlock('Send text-to-speech messages');
101 | case 'UseApplicationCommands': return inlineCodeBlock('Use application commands');
102 | case 'SendVoiceMessages': return inlineCodeBlock('Send voice messages');
103 | case 'Connect': return inlineCodeBlock('Connect');
104 | case 'Speak': return inlineCodeBlock('Speak');
105 | case 'Stream': return inlineCodeBlock('Video');
106 | case 'UseEmbeddedActivities': return inlineCodeBlock('Use activities');
107 | case 'UseSoundboard': return inlineCodeBlock('Use soundboard');
108 | case 'UseExternalSounds': return inlineCodeBlock('Use external sounds');
109 | case 'UseVAD': return inlineCodeBlock('Use voice activity');
110 | case 'PrioritySpeaker': return inlineCodeBlock('Priority speaker');
111 | case 'MuteMembers': return inlineCodeBlock('Mute members');
112 | case 'DeafenMembers': return inlineCodeBlock('Deafen members');
113 | case 'MoveMembers': return inlineCodeBlock('Move members');
114 | case 'RequestToSpeak': return inlineCodeBlock('Request to speak');
115 | case 'ManageEvents': return inlineCodeBlock('Manage events');
116 | case 'Administrator': return inlineCodeBlock('Administrator');
117 | default: return undefined;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/listeners/guilds/channels/channelCreateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, ChannelType, GuildChannel, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.ChannelCreate })
9 | export class UserEvent extends Listener {
10 | public async run(channel: GuildChannel) {
11 | if (isNullish(channel.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: channel.guild.id } });
14 | if (!guildSettingsInfoLogs?.channelCreateLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = channel.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.ChannelCreate, channel.guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(channel, executor));
20 | }
21 |
22 | private generateGuildLog(channel: GuildChannel, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: `${channel.name}`,
26 | url: `https://discord.com/channels/${channel.guildId}/${channel.id}`,
27 | iconURL: channel.guild.iconURL() ?? undefined
28 | })
29 | .setDescription(inlineCodeBlock(channel.id))
30 | .setFooter({ text: `Channel created ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
31 | .setType(Events.ChannelCreate);
32 |
33 | if (channel.parent) embed.addFields({ name: 'Category', value: inlineCodeBlock(channel.parent.name), inline: true });
34 |
35 | let footerChannelType;
36 | // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
37 | switch (channel.type) {
38 | case ChannelType.GuildAnnouncement:
39 | footerChannelType = 'Announcement channel';
40 | break;
41 | case ChannelType.GuildCategory:
42 | footerChannelType = 'Category';
43 | break;
44 | case ChannelType.GuildForum:
45 | footerChannelType = 'Forum channel';
46 | break;
47 | case ChannelType.GuildStageVoice:
48 | footerChannelType = 'Stage channel';
49 | break;
50 | case ChannelType.GuildText:
51 | footerChannelType = 'Text channel';
52 | break;
53 | case ChannelType.GuildVoice:
54 | footerChannelType = 'Voice channel';
55 | break;
56 | }
57 | embed.setFooter({ text: `${footerChannelType} created ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() });
58 |
59 | return [embed]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/listeners/guilds/channels/channelDeleteLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, ChannelType, GuildChannel, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.ChannelDelete })
9 | export class UserEvent extends Listener {
10 | public async run(channel: GuildChannel) {
11 | if (isNullish(channel.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: channel.guild.id } });
14 | if (!guildSettingsInfoLogs?.channelDeleteLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = channel.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.ChannelDelete, channel.guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(channel, executor));
20 | }
21 |
22 | private generateGuildLog(channel: GuildChannel, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: `${channel.name}`,
26 | iconURL: channel.guild.iconURL() ?? undefined
27 | })
28 | .setDescription(inlineCodeBlock(channel.id))
29 | .setFooter({ text: `Channel deleted ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
30 | .setType(Events.ChannelDelete);
31 |
32 | if (channel.parent) embed.addFields({ name: 'Category', value: inlineCodeBlock(channel.parent.name), inline: true });
33 | if (channel?.createdTimestamp) embed.addFields({ name: 'Created', value: ``, inline: true });
34 |
35 | let footerChannelType;
36 | // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
37 | switch (channel.type) {
38 | case ChannelType.GuildAnnouncement:
39 | footerChannelType = 'Announcement channel';
40 | break;
41 | case ChannelType.GuildCategory:
42 | footerChannelType = 'Category';
43 | break;
44 | case ChannelType.GuildForum:
45 | footerChannelType = 'Forum channel';
46 | break;
47 | case ChannelType.GuildStageVoice:
48 | footerChannelType = 'Stage channel';
49 | break;
50 | case ChannelType.GuildText:
51 | footerChannelType = 'Text channel';
52 | break;
53 | case ChannelType.GuildVoice:
54 | footerChannelType = 'Voice channel';
55 | break;
56 | }
57 | embed.setFooter({ text: `${footerChannelType} deleted ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() });
58 |
59 | return [embed]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/listeners/guilds/channels/channelUpdateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { minutes, seconds } from '#utils/common/times';
3 | import { Emojis } from '#utils/constants';
4 | import { getAuditLogExecutor, getRegionOverride } from '#utils/util';
5 | import { ApplyOptions } from '@sapphire/decorators';
6 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
7 | import { DurationFormatter } from '@sapphire/time-utilities';
8 | import { codeBlock, inlineCodeBlock, isNullish } from '@sapphire/utilities';
9 | import { AuditLogEvent, BaseGuildTextChannel, CategoryChannel, ChannelType, ForumChannel, NewsChannel, StageChannel, TextChannel, User, VoiceChannel } from 'discord.js';
10 |
11 | type GuildBasedChannel = CategoryChannel | NewsChannel | StageChannel | TextChannel | VoiceChannel | ForumChannel
12 |
13 | @ApplyOptions({ event: Events.ChannelUpdate })
14 | export class UserEvent extends Listener {
15 | public async run(oldChannel: GuildBasedChannel, channel: GuildBasedChannel) {
16 | if (isNullish(channel.id)) return;
17 |
18 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: channel.guild.id } });
19 | if (!guildSettingsInfoLogs?.channelUpdateLog || !guildSettingsInfoLogs.infoLogChannel) return;
20 |
21 | const logChannel = channel.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
22 | const executor = await getAuditLogExecutor(AuditLogEvent.ChannelUpdate, channel.guild);
23 |
24 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(oldChannel, channel, executor));
25 | }
26 |
27 | private generateGuildLog(oldChannel: GuildBasedChannel, channel: GuildBasedChannel, executor: User | null | undefined) {
28 | const embed = new GuildLogEmbed()
29 | .setAuthor({
30 | name: `${channel.name}`,
31 | url: `https://discord.com/channels/${channel.guildId}/${channel.id}`,
32 | iconURL: channel.guild.iconURL() ?? undefined
33 | })
34 | .setDescription(inlineCodeBlock(channel.id))
35 | .setFooter({ text: `Channel updated ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
36 | .setType(Events.ChannelUpdate);
37 |
38 | if (channel.parent) embed.addFields({ name: 'Category', value: inlineCodeBlock(channel.parent.name), inline: true });
39 |
40 | const changes = [];
41 | let footerChannelType;
42 |
43 | if (oldChannel.name !== channel.name) changes.push(`${Emojis.Bullet}**Name**: ${inlineCodeBlock(`${oldChannel.name}`)} to ${inlineCodeBlock(`${channel.name}`)}`);
44 | if (oldChannel.parent !== channel.parent) changes.push(`${Emojis.Bullet}**Category**: ${inlineCodeBlock(`${oldChannel.parent}`)} to ${inlineCodeBlock(`${channel.parent}`)}`);
45 | if (oldChannel.type !== channel.type) changes.push(`${Emojis.Bullet}**Announcement channel**: ${channel.type === ChannelType.GuildAnnouncement ? Emojis.ToggleOn : Emojis.ToggleOff}`);
46 |
47 | // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
48 | switch (channel.type) {
49 | case ChannelType.GuildAnnouncement:
50 | footerChannelType = 'Announcement channel';
51 | break;
52 | case ChannelType.GuildCategory:
53 | footerChannelType = 'Category';
54 | break;
55 | case ChannelType.GuildForum:
56 | footerChannelType = 'Forum channel';
57 | break;
58 | case ChannelType.GuildStageVoice:
59 | footerChannelType = 'Stage channel';
60 | break;
61 | case ChannelType.GuildText:
62 | footerChannelType = 'Text channel';
63 | break;
64 | case ChannelType.GuildVoice:
65 | footerChannelType = 'Voice channel';
66 | break;
67 | }
68 |
69 | if (oldChannel.type === ChannelType.GuildForum && channel.type === ChannelType.GuildForum) {
70 | const forumLayout = ['Not set', 'List view', 'Gallery view'];
71 | const sortOrder = ['Recent activity', 'Creation time'];
72 | if (oldChannel.defaultReactionEmoji?.id !== channel.defaultReactionEmoji?.id) changes.push(`${Emojis.Bullet}**Default reaction**: ${oldChannel.defaultReactionEmoji ? channel.guild.emojis.resolve(oldChannel.defaultReactionEmoji.id as string) ?? oldChannel.defaultReactionEmoji.name : inlineCodeBlock('Not set')} to ${channel.defaultReactionEmoji ? channel.guild.emojis.resolve(channel.defaultReactionEmoji.id as string) ?? channel.defaultReactionEmoji.name : inlineCodeBlock('Not set')}`);
73 | if (oldChannel.rateLimitPerUser !== channel.rateLimitPerUser) changes.push(`${Emojis.Bullet}**Posts slowmode**: ${inlineCodeBlock(`${oldChannel.rateLimitPerUser ? new DurationFormatter().format(seconds(oldChannel.rateLimitPerUser)) : 'Off'}`)} to ${inlineCodeBlock(`${channel.rateLimitPerUser ? new DurationFormatter().format(seconds(channel.rateLimitPerUser)) : 'Off'}`)}`);
74 | if (oldChannel.defaultThreadRateLimitPerUser !== channel.defaultThreadRateLimitPerUser) changes.push(`${Emojis.Bullet}**Messages slowmode**: ${inlineCodeBlock(`${oldChannel.defaultThreadRateLimitPerUser ? new DurationFormatter().format(seconds(oldChannel.defaultThreadRateLimitPerUser)) : 'Off'}`)} to ${inlineCodeBlock(`${channel.defaultThreadRateLimitPerUser ? new DurationFormatter().format(seconds(channel.defaultThreadRateLimitPerUser)) : 'Off'}`)}`);
75 | if (oldChannel.defaultForumLayout !== channel.defaultForumLayout) changes.push(`${Emojis.Bullet}**Default layout**: ${inlineCodeBlock(`${forumLayout[oldChannel.defaultForumLayout]}`)} to ${inlineCodeBlock(`${forumLayout[channel.defaultForumLayout]}`)}`);
76 | if (oldChannel.defaultSortOrder && oldChannel.defaultSortOrder !== channel.defaultSortOrder) changes.push(`${Emojis.Bullet}**Sort order**: ${inlineCodeBlock(`${sortOrder[oldChannel.defaultSortOrder]}`)} to ${inlineCodeBlock(`${sortOrder[channel.defaultSortOrder!]}`)}`);
77 | if (oldChannel.nsfw !== channel.nsfw) changes.push(`${Emojis.Bullet}**Age-restricted**: ${channel.nsfw ? Emojis.ToggleOn : Emojis.ToggleOff}`);
78 | if (oldChannel.defaultAutoArchiveDuration !== channel.defaultAutoArchiveDuration) changes.push(`${Emojis.Bullet}**Hide after inactivity**: ${inlineCodeBlock(`${new DurationFormatter().format(minutes(oldChannel.defaultAutoArchiveDuration ?? 4320))}`)} to ${inlineCodeBlock(`${new DurationFormatter().format(minutes(channel.defaultAutoArchiveDuration ?? 4320))}`)}`);
79 | if (oldChannel.topic && oldChannel.topic !== channel.topic) changes.push(`${Emojis.Bullet}**Post guidelines**:\n${oldChannel.topic ? codeBlock(`${oldChannel.topic}`) : codeBlock('Not set')}to\n${channel.topic ? codeBlock(`${channel.topic}`) : codeBlock('Not set')}`);
80 | }
81 |
82 | const videoQualityMode = ['', 'Auto', '720p'];
83 | if (oldChannel.type === ChannelType.GuildStageVoice && channel.type === ChannelType.GuildStageVoice) {
84 | if (oldChannel.bitrate !== channel.bitrate) changes.push(`${Emojis.Bullet}**Bitrate**: ${inlineCodeBlock(`${oldChannel.bitrate / 1000}kbps`)} to ${inlineCodeBlock(`${channel.bitrate / 1000}kbps`)}`);
85 | if (oldChannel.rateLimitPerUser !== channel.rateLimitPerUser) changes.push(`${Emojis.Bullet}**Slowmode**: ${inlineCodeBlock(`${oldChannel.rateLimitPerUser ? new DurationFormatter().format(seconds(oldChannel.rateLimitPerUser)) : 'Off'}`)} to ${inlineCodeBlock(`${channel.rateLimitPerUser ? new DurationFormatter().format(seconds(channel.rateLimitPerUser)) : 'Off'}`)}`);
86 | if (oldChannel.nsfw !== channel.nsfw) changes.push(`${Emojis.Bullet}**Age-restricted**: ${channel.nsfw ? Emojis.ToggleOn : Emojis.ToggleOff}`);
87 | if (oldChannel.videoQualityMode && oldChannel.videoQualityMode !== channel.videoQualityMode) changes.push(`${Emojis.Bullet}**Video quality**: ${inlineCodeBlock(`${videoQualityMode[oldChannel.videoQualityMode]}`)} to ${inlineCodeBlock(`${videoQualityMode[channel.videoQualityMode!]}`)}`);
88 | if (oldChannel.userLimit !== channel.userLimit) changes.push(`${Emojis.Bullet}**User limit**: ${inlineCodeBlock(`${oldChannel.userLimit}`)} to ${inlineCodeBlock(`${channel.userLimit}`)}`);
89 | if (oldChannel.rtcRegion !== channel.rtcRegion) changes.push(`${Emojis.Bullet}**Region override**: ${getRegionOverride(oldChannel)} to ${getRegionOverride(channel)}`);
90 | }
91 |
92 | if (oldChannel.type === ChannelType.GuildText && channel.type === ChannelType.GuildText) {
93 | if (oldChannel.rateLimitPerUser !== channel.rateLimitPerUser) changes.push(`${Emojis.Bullet}**Slowmode**: ${inlineCodeBlock(`${oldChannel.rateLimitPerUser ? new DurationFormatter().format(seconds(oldChannel.rateLimitPerUser)) : 'Off'}`)} to ${inlineCodeBlock(`${channel.rateLimitPerUser ? new DurationFormatter().format(seconds(channel.rateLimitPerUser)) : 'Off'}`)}`);
94 | if (oldChannel.nsfw !== channel.nsfw) changes.push(`${Emojis.Bullet}**Age-restricted**: ${channel.nsfw ? Emojis.ToggleOn : Emojis.ToggleOff}`);
95 | if (oldChannel.defaultAutoArchiveDuration !== channel.defaultAutoArchiveDuration) changes.push(`${Emojis.Bullet}**Hide after inactivity**: ${inlineCodeBlock(`${new DurationFormatter().format(minutes(oldChannel.defaultAutoArchiveDuration ?? 4320))}`)} to ${inlineCodeBlock(`${new DurationFormatter().format(minutes(channel.defaultAutoArchiveDuration ?? 4320))}`)}`);
96 | if (oldChannel.topic && oldChannel.topic !== channel.topic) changes.push(`${Emojis.Bullet}**Topic**:\n${oldChannel.topic ? codeBlock(`${oldChannel.topic}`) : codeBlock('Not set')}to\n${channel.topic ? codeBlock(`${channel.topic}`) : codeBlock('Not set')}`);
97 | }
98 |
99 | if (oldChannel.type === ChannelType.GuildVoice && channel.type === ChannelType.GuildVoice) {
100 | if (oldChannel.rateLimitPerUser !== channel.rateLimitPerUser) changes.push(`${Emojis.Bullet}**Slowmode**: ${inlineCodeBlock(`${oldChannel.rateLimitPerUser ? new DurationFormatter().format(seconds(oldChannel.rateLimitPerUser)) : 'Off'}`)} to ${inlineCodeBlock(`${channel.rateLimitPerUser ? new DurationFormatter().format(seconds(channel.rateLimitPerUser)) : 'Off'}`)}`);
101 | if (oldChannel.nsfw !== channel.nsfw) changes.push(`${Emojis.Bullet}**Age-restricted**: ${channel.nsfw ? Emojis.ToggleOn : Emojis.ToggleOff}`);
102 | if (oldChannel.bitrate !== channel.bitrate) changes.push(`${Emojis.Bullet}**Bitrate**: ${inlineCodeBlock(`${oldChannel.bitrate / 1000}kbps`)} to ${inlineCodeBlock(`${channel.bitrate / 1000}kbps`)}`);
103 | if (oldChannel.videoQualityMode && oldChannel.videoQualityMode !== channel.videoQualityMode) changes.push(`${Emojis.Bullet}**Video quality**: ${inlineCodeBlock(`${videoQualityMode[oldChannel.videoQualityMode]}`)} to ${inlineCodeBlock(`${videoQualityMode[channel.videoQualityMode!]}`)}`);
104 | if (oldChannel.userLimit !== channel.userLimit) changes.push(`${Emojis.Bullet}**User limit**: ${inlineCodeBlock(`${oldChannel.userLimit}`)} to ${inlineCodeBlock(`${channel.userLimit}`)}`);
105 | if (oldChannel.rtcRegion !== channel.rtcRegion) changes.push(`${Emojis.Bullet}**Region override**: ${getRegionOverride(oldChannel)} to ${getRegionOverride(channel)}`);
106 | }
107 |
108 | if ((oldChannel.parent || channel.parent) && (oldChannel.permissionsLocked !== channel.permissionsLocked)) changes.push(`${Emojis.Bullet}**Permissions synced with category**: ${channel.permissionsLocked ? Emojis.Check : Emojis.X}`);
109 | if (oldChannel.permissionOverwrites.cache !== channel.permissionOverwrites.cache) {
110 |
111 | }
112 |
113 | if (changes.length) embed.addFields({ name: 'Changes', value: changes.join('\n') });
114 | embed.setFooter({ text: `${footerChannelType} updated ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() });
115 |
116 | if (changes.length) return [embed];
117 | return [];
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/listeners/guilds/emojis/guildEmojiCreateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, GuildEmoji, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.GuildEmojiCreate })
9 | export class UserEvent extends Listener {
10 | public async run(emoji: GuildEmoji) {
11 | if (isNullish(emoji.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: emoji.guild.id } });
14 | if (!guildSettingsInfoLogs?.emojiCreateLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = emoji.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.EmojiCreate, emoji.guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(emoji, executor));
20 | }
21 |
22 | private generateGuildLog(emoji: GuildEmoji, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: `:${emoji.name}:`,
26 | url: emoji.url,
27 | iconURL: emoji.url
28 | })
29 | .setDescription(inlineCodeBlock(emoji.id))
30 | .setThumbnail(emoji.url)
31 | .setFooter({ text: `Emoji created ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
32 | .setType(Events.GuildEmojiCreate);
33 |
34 | return [embed]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/listeners/guilds/emojis/guildEmojiDeleteLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, GuildEmoji, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.GuildEmojiDelete })
9 | export class UserEvent extends Listener {
10 | public async run(emoji: GuildEmoji) {
11 | if (isNullish(emoji.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: emoji.guild.id } });
14 | if (!guildSettingsInfoLogs?.emojiDeleteLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = emoji.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.EmojiDelete, emoji.guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(emoji, executor));
20 | }
21 |
22 | private generateGuildLog(emoji: GuildEmoji, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: `:${emoji.name}:`,
26 | url: emoji.url,
27 | iconURL: emoji.url
28 | })
29 | .setDescription(inlineCodeBlock(emoji.id))
30 | .setThumbnail(emoji.url)
31 | .setFooter({ text: `Emoji deleted ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
32 | .setType(Events.GuildEmojiDelete);
33 |
34 | if (emoji?.createdTimestamp) embed.addFields({ name: 'Created', value: ``, inline: true });
35 |
36 | return [embed]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/listeners/guilds/emojis/guildEmojiUpdateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { Emojis } from '#utils/constants';
3 | import { getAuditLogExecutor } from '#utils/util';
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
6 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
7 | import { AuditLogEvent, BaseGuildTextChannel, GuildEmoji, User } from 'discord.js';
8 |
9 | @ApplyOptions({ event: Events.GuildEmojiUpdate })
10 | export class UserEvent extends Listener {
11 | public async run(oldEmoji: GuildEmoji, emoji: GuildEmoji) {
12 | if (isNullish(emoji.id)) return;
13 |
14 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: emoji.guild.id } });
15 | if (!guildSettingsInfoLogs?.emojiUpdateLog || !guildSettingsInfoLogs.infoLogChannel) return;
16 |
17 | const logChannel = emoji.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
18 | const executor = await getAuditLogExecutor(AuditLogEvent.EmojiUpdate, emoji.guild);
19 |
20 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(oldEmoji, emoji, executor));
21 | }
22 |
23 | private generateGuildLog(oldEmoji: GuildEmoji, emoji: GuildEmoji, executor: User | null | undefined) {
24 | const embed = new GuildLogEmbed()
25 | .setAuthor({
26 | name: `:${emoji.name}:`,
27 | url: emoji.url,
28 | iconURL: emoji.url
29 | })
30 | .setDescription(inlineCodeBlock(emoji.id))
31 | .setThumbnail(emoji.url)
32 | .setFooter({ text: `Emoji updated ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
33 | .setType(Events.GuildEmojiUpdate);
34 |
35 | const changes = [];
36 | if (oldEmoji.name !== emoji.name) changes.push(`${Emojis.Bullet}**Name**: ${inlineCodeBlock(`${oldEmoji.name}`)} to ${inlineCodeBlock(`${emoji.name}`)}`);
37 | if (changes.length) embed.addFields({ name: 'Changes', value: changes.join('\n') });
38 |
39 | return [embed]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/listeners/guilds/events/guildScheduledEventCreateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, ChannelType, Guild, GuildScheduledEvent, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.GuildScheduledEventCreate })
9 | export class UserEvent extends Listener {
10 | public async run(event: GuildScheduledEvent) {
11 | if (isNullish(event.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: event.guild?.id } });
14 | if (!guildSettingsInfoLogs?.guildScheduledEventCreateLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = event.guild?.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.GuildScheduledEventCreate, event.guild as Guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(event, executor));
20 | }
21 |
22 | private generateGuildLog(event: GuildScheduledEvent, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: event.name,
26 | url: event.url,
27 | iconURL: event.guild?.iconURL() ?? undefined
28 | })
29 | .setDescription(`${inlineCodeBlock(event.id)}\n**Description**:\n${event.description}`)
30 | .setFooter({ text: `Event created ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
31 | .setType(Events.GuildScheduledEventCreate);
32 |
33 | if (event.channel) embed.addFields({ name: event.channel.type === ChannelType.GuildVoice ? 'Voice channel' : 'Stage channel', value: `<#${event.channelId}>`, inline: true });
34 | if (event.entityMetadata?.location) embed.addFields({ name: 'Location', value: event.entityMetadata.location });
35 | if (event.scheduledStartTimestamp) embed.addFields({ name: 'Scheduled to start', value: ``, inline: true });
36 | if (event.scheduledEndTimestamp) embed.addFields({ name: 'Scheduled to end', value: ``, inline: true });
37 | if (event.image) embed.setImage(event.coverImageURL())
38 |
39 | return [embed]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/listeners/guilds/events/guildScheduledEventDeleteLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, ChannelType, Guild, GuildScheduledEvent, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.GuildScheduledEventDelete })
9 | export class UserEvent extends Listener {
10 | public async run(event: GuildScheduledEvent) {
11 | if (isNullish(event.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: event.guild?.id } });
14 | if (!guildSettingsInfoLogs?.guildScheduledEventDeleteLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = event.guild?.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.GuildScheduledEventCreate, event.guild as Guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(event, executor));
20 | }
21 |
22 | private generateGuildLog(event: GuildScheduledEvent, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: event.name,
26 | url: event.url,
27 | iconURL: event.guild?.iconURL() ?? undefined
28 | })
29 | .setDescription(`${inlineCodeBlock(event.id)}\n**Description**:\n${event.description}`)
30 | .setFooter({ text: `Event cancelled ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
31 | .setType(Events.GuildScheduledEventDelete);
32 |
33 | if (event.channel) embed.addFields({ name: event.channel.type === ChannelType.GuildVoice ? 'Voice channel' : 'Stage channel', value: `<#${event.channelId}>`, inline: true });
34 | if (event.entityMetadata?.location) embed.addFields({ name: 'Location', value: event.entityMetadata.location, inline: true });
35 | if (event.scheduledStartTimestamp) embed.addFields({ name: 'Scheduled to start', value: ``, inline: true });
36 | if (event.scheduledEndTimestamp) embed.addFields({ name: 'Scheduled to end', value: ``, inline: true });
37 | if (event.userCount) embed.addFields({ name: 'Interested users', value: inlineCodeBlock(`${event.userCount}`) });
38 | if (event.image) embed.setImage(event.coverImageURL());
39 |
40 | return [embed]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/listeners/guilds/events/guildScheduledEventUpdateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { Emojis } from '#utils/constants';
3 | import { getAuditLogExecutor } from '#utils/util';
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
6 | import { codeBlock, inlineCodeBlock, isNullish } from '@sapphire/utilities';
7 | import { AuditLogEvent, BaseGuildTextChannel, Guild, GuildScheduledEvent, User } from 'discord.js';
8 |
9 | @ApplyOptions({ event: Events.GuildScheduledEventUpdate })
10 | export class UserEvent extends Listener {
11 | public async run(oldEvent: GuildScheduledEvent, event: GuildScheduledEvent) {
12 | if (isNullish(event.id)) return;
13 |
14 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: event.guild?.id } });
15 | if (!guildSettingsInfoLogs?.guildScheduledEventUpdateLog || !guildSettingsInfoLogs.infoLogChannel) return;
16 |
17 | const logChannel = event.guild?.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
18 | const executor = await getAuditLogExecutor(AuditLogEvent.GuildScheduledEventCreate, event.guild as Guild);
19 |
20 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(oldEvent, event, executor));
21 | }
22 |
23 | private generateGuildLog(oldEvent: GuildScheduledEvent, event: GuildScheduledEvent, executor: User | null | undefined) {
24 | const embed = new GuildLogEmbed()
25 | .setAuthor({
26 | name: event.name,
27 | url: event.url,
28 | iconURL: event.guild?.iconURL() ?? undefined
29 | })
30 | .setDescription(inlineCodeBlock(event.id))
31 | .setFooter({ text: `Event edited ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
32 | .setType(Events.GuildScheduledEventUpdate);
33 |
34 | const changes = [];
35 | const oldEndTimestamp = oldEvent.scheduledEndTimestamp ? `` : inlineCodeBlock('Not set');
36 | const endTimestamp = event.scheduledEndTimestamp ? `` : inlineCodeBlock('Not set');
37 | if ((oldEvent.entityType !== event.entityType) || oldEvent.channel !== event.channel) {
38 | const oldEventLocation = oldEvent.entityMetadata?.location ? inlineCodeBlock(oldEvent.entityMetadata.location) : `<#${oldEvent.channelId}>`;
39 | const eventLocation = event.entityMetadata?.location ? inlineCodeBlock(event.entityMetadata.location) : `<#${event.channelId}>`;
40 | changes.push(`${Emojis.Bullet}**Location**: ${oldEventLocation} to ${eventLocation}`);
41 | }
42 | if (oldEvent.name !== event.name) changes.push(`${Emojis.Bullet}**Name**: ${inlineCodeBlock(`${oldEvent.name}`)} to ${inlineCodeBlock(`${event.name}`)}`);
43 | if (oldEvent.scheduledStartTimestamp !== event.scheduledStartTimestamp) changes.push(`${Emojis.Bullet}**Scheduled start**: to `);
44 | if (oldEvent.scheduledEndTimestamp !== event.scheduledEndTimestamp) changes.push(`${Emojis.Bullet}**Scheduled end**: ${oldEndTimestamp} to ${endTimestamp}`);
45 | if (oldEvent.image !== event.image) changes.push(`${Emojis.Bullet}**Cover image**: ${oldEvent.coverImageURL() ? `[${inlineCodeBlock('click to view')}](${oldEvent.coverImageURL()})` : inlineCodeBlock('Not set')} to ${event.coverImageURL() ? `[${inlineCodeBlock('click to view')}](${event.coverImageURL()})` : inlineCodeBlock('Not set')}`);
46 | if (oldEvent.description !== event.description) changes.push(`${Emojis.Bullet}**Description**:\n${oldEvent.description ? codeBlock(`${oldEvent.description}`) : codeBlock('Not set')}to\n${event.description ? codeBlock(`${event.description}`) : codeBlock('Not set')}`);
47 | if (changes.length) embed.addFields({ name: 'Changes', value: changes.join('\n') });
48 |
49 | return [embed]
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/listeners/guilds/guildCreate.ts:
--------------------------------------------------------------------------------
1 | import { initializeGuild } from '#utils/functions/initialize';
2 | import { ApplyOptions } from '@sapphire/decorators';
3 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
4 | import { bold, gray } from 'colorette';
5 | import { Guild } from 'discord.js';
6 |
7 | @ApplyOptions({ event: Events.GuildCreate })
8 | export class UserEvent extends Listener {
9 | public async run(guild: Guild) {
10 | if (!guild.available) return;
11 |
12 | this.container.logger.info(`Bot added to guild ${bold(guild.name)} (${gray(guild.id)})`)
13 |
14 | await initializeGuild(guild);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/listeners/guilds/guildLogCreate.ts:
--------------------------------------------------------------------------------
1 | import type { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { ApplyOptions } from '@sapphire/decorators';
3 | import { Listener, type ListenerOptions } from '@sapphire/framework';
4 | import { isNullish } from '@sapphire/utilities';
5 | import type { BaseGuildTextChannel } from 'discord.js';
6 |
7 | @ApplyOptions({ event: 'guildLogCreate' })
8 | export class UserEvent extends Listener {
9 | public run(logChannel: BaseGuildTextChannel | null, logEmbeds: GuildLogEmbed[]) {
10 | if (isNullish(logChannel)) return;
11 | if (!logEmbeds.length) return;
12 |
13 | return logChannel.send({ embeds: logEmbeds });
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/listeners/guilds/guildUpdateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { seconds } from '#utils/common/times';
3 | import { Emojis } from '#utils/constants';
4 | import { getAuditLogExecutor } from '#utils/util';
5 | import { ApplyOptions } from '@sapphire/decorators';
6 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
7 | import { DurationFormatter } from '@sapphire/time-utilities';
8 | import { codeBlock, inlineCodeBlock, isNullish } from '@sapphire/utilities';
9 | import { AuditLogEvent, BaseGuildTextChannel, Guild, GuildFeature, User } from 'discord.js';
10 |
11 | @ApplyOptions({ event: Events.GuildUpdate })
12 | export class UserEvent extends Listener {
13 | public async run(oldGuild: Guild, guild: Guild) {
14 | if (isNullish(guild.id)) return;
15 |
16 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: guild.id } });
17 | if (!guildSettingsInfoLogs?.guildUpdateLog || !guildSettingsInfoLogs?.infoLogChannel) return;
18 |
19 | const infoLogChannel = guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
20 | const executor = await getAuditLogExecutor(AuditLogEvent.GuildUpdate, guild);
21 |
22 | return this.container.client.emit('guildLogCreate', infoLogChannel, this.generateGuildLog(oldGuild, guild, executor));
23 | }
24 |
25 | private generateGuildLog(oldGuild: Guild, guild: Guild, executor: User | null | undefined) {
26 | const embed = new GuildLogEmbed()
27 | .setAuthor({
28 | name: guild.name,
29 | url: guild.vanityURLCode ? `https://discord.gg/${guild.vanityURLCode}` : undefined,
30 | iconURL: guild.iconURL() ?? undefined
31 | })
32 | .setDescription(inlineCodeBlock(guild.id))
33 | .setFooter({ text: `Server updated ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
34 | .setType(Events.GuildUpdate);
35 |
36 | const changes = [];
37 | if (oldGuild.name !== guild.name) changes.push(`${Emojis.Bullet}**Name**: ${inlineCodeBlock(`${oldGuild.name}`)} to ${inlineCodeBlock(`${guild.name}`)}`);
38 | if (oldGuild.afkChannel !== guild.afkChannel) changes.push(`${Emojis.Bullet}**Inactive channel**: ${oldGuild.afkChannelId ? `<#${oldGuild.afkChannelId}>` : inlineCodeBlock('No inactive channel')} to ${guild.afkChannelId ? `<#${guild.afkChannelId}>` : inlineCodeBlock('No inactive channel')}`);
39 | if (oldGuild.afkTimeout !== guild.afkTimeout) changes.push(`${Emojis.Bullet}**Inactive timeout**: ${inlineCodeBlock(`${oldGuild.afkTimeout ? new DurationFormatter().format(seconds(oldGuild.afkTimeout)) : 'Disabled'}`)} to ${inlineCodeBlock(`${guild.afkTimeout ? new DurationFormatter().format(seconds(guild.afkTimeout)) : 'Disabled'}`)}`);
40 | if (oldGuild.systemChannel !== guild.systemChannel) changes.push(`${Emojis.Bullet}**System messages channel**: ${oldGuild.systemChannel ? `<#${oldGuild.systemChannelId}>` : inlineCodeBlock('No system messages')} to ${guild.systemChannel ? `<#${guild.systemChannelId}>` : inlineCodeBlock('No system messages')}`);
41 | // TODO: implement changes for system channel flags
42 | if (oldGuild.defaultMessageNotifications !== guild.defaultMessageNotifications) {
43 | const defaultMessageNotifications = ['All messages', 'Only @mentions'];
44 | changes.push(`${Emojis.Bullet}**Default notification settings**: ${inlineCodeBlock(`${defaultMessageNotifications[oldGuild.defaultMessageNotifications]}`)} to ${inlineCodeBlock(`${defaultMessageNotifications[guild.defaultMessageNotifications]}`)}`);
45 | }
46 | if (oldGuild.premiumProgressBarEnabled !== guild.premiumProgressBarEnabled) changes.push(`${Emojis.Bullet}**Show boost progress bar**: ${guild.premiumProgressBarEnabled ? Emojis.ToggleOn : Emojis.ToggleOff}`)
47 | if (oldGuild.banner !== guild.banner) changes.push(`${Emojis.Bullet}**Banner background**: [${inlineCodeBlock('click to view')}](${oldGuild.bannerURL()}) to [${inlineCodeBlock('click to view')}](${guild.bannerURL()})`);
48 | if (oldGuild.splash !== guild.splash) changes.push(`${Emojis.Bullet}**Invite background**: [${inlineCodeBlock('click to view')}](${oldGuild.splashURL()}) to [${inlineCodeBlock('click to view')}](${guild.splashURL()})`);
49 | if (oldGuild.widgetEnabled !== null && oldGuild.widgetEnabled !== guild.widgetEnabled) changes.push(`${Emojis.Bullet}**Widget**: ${guild.widgetEnabled ? Emojis.ToggleOn : Emojis.ToggleOff}`);
50 | if (oldGuild.widgetChannel !== guild.widgetChannel) changes.push(`${Emojis.Bullet}**Invite channel**: ${oldGuild.widgetChannel ? `<#${oldGuild.widgetChannelId}>` : inlineCodeBlock('No invite')} to ${guild.widgetChannel ? `<#${guild.widgetChannelId}>` : inlineCodeBlock('No invite')}`);
51 | if (oldGuild.vanityURLCode !== guild.vanityURLCode) changes.push(`${Emojis.Bullet}**Custom invite link**: [${inlineCodeBlock(`discord.gg/${oldGuild.vanityURLCode}`)}](https://discord.gg/${oldGuild.vanityURLCode}) to [${inlineCodeBlock(`discord.gg/${guild.vanityURLCode}`)}](https://discord.gg/${guild.vanityURLCode})`);
52 | if (oldGuild.verificationLevel !== guild.verificationLevel) {
53 | const verificationLevel = ['', 'Low', 'Medium', 'High', 'Highest'];
54 | changes.push(`${Emojis.Bullet}**Verification level**: ${inlineCodeBlock(`${verificationLevel[oldGuild.verificationLevel]}`)} to ${inlineCodeBlock(`${verificationLevel[guild.verificationLevel]}`)}`);
55 | }
56 | if (oldGuild.features !== guild.features) {
57 | if (oldGuild.features.includes(GuildFeature.PreviewEnabled) !== guild.features.includes(GuildFeature.PreviewEnabled)) changes.push(`${Emojis.Bullet}**Members must accept rules before they can talk or DM**: ${guild.features.includes(GuildFeature.PreviewEnabled) ? Emojis.ToggleOn : Emojis.ToggleOff}`);
58 | }
59 | if (oldGuild.explicitContentFilter !== guild.explicitContentFilter) {
60 | const explicitContentFilter = ['Do not filter', 'Filter for members without roles', 'Filter for all members'];
61 | changes.push(`${Emojis.Bullet}**Explicit image filter**: ${inlineCodeBlock(`${explicitContentFilter[oldGuild.explicitContentFilter]}`)} to ${inlineCodeBlock(`${explicitContentFilter[guild.explicitContentFilter]}`)}`);
62 | }
63 | if (oldGuild.mfaLevel !== guild.mfaLevel) changes.push(`${Emojis.Bullet}**2FA required for moderator actions**: ${guild.mfaLevel === 1 ? Emojis.ToggleOn : Emojis.ToggleOff}`);
64 | if (oldGuild.rulesChannel !== guild.rulesChannel) changes.push(`${Emojis.Bullet}**Rules channel**: ${oldGuild.rulesChannel ? `<#${oldGuild.rulesChannelId}>` : inlineCodeBlock('Not set')} to ${guild.rulesChannel ? `<#${guild.rulesChannelId}>` : inlineCodeBlock('Not set')}`);
65 | if (oldGuild.publicUpdatesChannel !== guild.publicUpdatesChannel) changes.push(`${Emojis.Bullet}**Community updates channel**: ${oldGuild.publicUpdatesChannel ? `<#${oldGuild.publicUpdatesChannelId}>` : inlineCodeBlock('Not set')} to ${guild.publicUpdatesChannel ? `<#${guild.publicUpdatesChannelId}>` : inlineCodeBlock('Not set')}`);
66 | if (oldGuild.safetyAlertsChannel !== guild.safetyAlertsChannel) changes.push(`${Emojis.Bullet}**Safety notifications channel**: ${oldGuild.safetyAlertsChannel ? `<#${oldGuild.safetyAlertsChannelId}>` : inlineCodeBlock('Disabled')} to ${guild.safetyAlertsChannel ? `<#${guild.safetyAlertsChannelId}>` : inlineCodeBlock('Disabled')}`);
67 | if (oldGuild.preferredLocale !== guild.preferredLocale) changes.push(`${Emojis.Bullet}**Primary language**: ${oldGuild.preferredLocale} to ${guild.preferredLocale}`);
68 | if (oldGuild.description !== guild.description) changes.push(`${Emojis.Bullet}**Description**:\n${oldGuild.description ? codeBlock(`${oldGuild.description}`) : codeBlock('Not set')}to\n${guild.description ? codeBlock(`${guild.description}`) : codeBlock('Not set')}`);
69 | if (oldGuild.features !== guild.features) {
70 | if (oldGuild.features.includes(GuildFeature.Community) !== guild.features.includes(GuildFeature.Community)) changes.push(`${Emojis.Bullet}**Community**: ${guild.features.includes(GuildFeature.Community) ? Emojis.ToggleOn : Emojis.ToggleOff}`);
71 | }
72 | if (oldGuild.partnered !== guild.partnered) changes.push(`${Emojis.Bullet}**Partnered**: ${guild.partnered ? Emojis.ToggleOn : Emojis.ToggleOff}`);
73 | if (oldGuild.features !== guild.features) {
74 | if (oldGuild.features.includes(GuildFeature.Discoverable) !== guild.features.includes(GuildFeature.Discoverable)) changes.push(`${Emojis.Bullet}**Discoverable**: ${guild.features.includes(GuildFeature.Discoverable) ? Emojis.ToggleOn : Emojis.ToggleOff}`);
75 | if (oldGuild.features.includes(GuildFeature.WelcomeScreenEnabled) !== guild.features.includes(GuildFeature.WelcomeScreenEnabled)) changes.push(`${Emojis.Bullet}**Welcome screen**: ${guild.features.includes(GuildFeature.WelcomeScreenEnabled) ? Emojis.ToggleOn : Emojis.ToggleOff}`);
76 | }
77 | if (changes.length) embed.addFields({ name: 'Changes', value: changes.join('\n') });
78 |
79 | if (changes.length) return [embed];
80 | return [];
81 | }
82 | }
--------------------------------------------------------------------------------
/src/listeners/guilds/initializeMember.ts:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Temporarily disabing this listener while I evaluate whether it's redundant now that we have #util/functions/initialize/initializeMember()
4 |
5 | import { ApplyOptions } from '@sapphire/decorators';
6 | import { Listener, type ListenerOptions } from '@sapphire/framework';
7 | import { bold, gray } from 'colorette';
8 | import { Guild, User } from 'discord.js';
9 |
10 | @ApplyOptions({ event: 'initializeMember' })
11 | export class UserEvent extends Listener {
12 | public async run(guild: Guild, user: User) {
13 | this.container.logger.info(`Initializing member ${bold(user.username)} (${gray(user.id)}) for ${bold(guild.name)} (${gray(guild.id)})...`);
14 |
15 | await this.container.prisma.member.create({
16 | data: {
17 | guild: {
18 | connect: {
19 | id: guild.id
20 | }
21 | },
22 | user: {
23 | connectOrCreate: {
24 | where: { id: user.id },
25 | create: {
26 | id: user.id,
27 | previousUsernames: user.username
28 | }
29 | }
30 | }
31 | },
32 | include: { user: true }
33 | }).catch(e => {
34 | this.container.logger.error(`Failed to initialize member ${bold(user.username)} (${gray(user.id)}) for ${bold(guild.name)} (${gray(guild.id)}), error below.`);
35 | return this.container.logger.error(e);
36 | });
37 |
38 | return this.container.logger.info(`Verified initialization of member ${bold(user.username)} (${gray(user.id)}) for ${bold(guild.name)} (${gray(guild.id)})`);
39 | }
40 | }
41 | */
--------------------------------------------------------------------------------
/src/listeners/guilds/interactions/interactionCreate.ts:
--------------------------------------------------------------------------------
1 | import { initializeUser } from '#utils/functions/initialize';
2 | import { ApplyOptions } from '@sapphire/decorators';
3 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
4 | import type { BaseInteraction } from 'discord.js';
5 |
6 | @ApplyOptions({ event: Events.InteractionCreate })
7 | export class UserEvent extends Listener {
8 | public async run(interaction: BaseInteraction) {
9 | const dbMember = await this.container.prisma.userSettings.findFirst({ where: { id: interaction.user.id } });
10 | if (!dbMember) await initializeUser(interaction.user);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/listeners/guilds/invites/inviteCreateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { Emojis } from '#utils/constants';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { BaseGuildTextChannel, ChannelType, Guild, Invite, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.InviteCreate })
9 | export class UserEvent extends Listener {
10 | public async run(invite: Invite) {
11 | if (isNullish(invite.guild)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: invite.guild.id } });
14 | if (!guildSettingsInfoLogs?.inviteCreateLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const guild = this.container.client.guilds.resolve(invite.guild.id);
17 | const fetchedInvite = await guild?.invites.fetch({ code: invite.code });
18 | const logChannel = guild?.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
19 | const executor = invite.inviter;
20 |
21 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(fetchedInvite, guild, executor));
22 | }
23 |
24 | private generateGuildLog(invite: Invite | undefined, guild: Guild | null, executor: User | null | undefined) {
25 | const embed = new GuildLogEmbed()
26 | .setAuthor({
27 | name: `${invite?.code}`,
28 | url: `https://discord.gg/${invite?.code}`,
29 | iconURL: guild?.iconURL() as string
30 | })
31 | .setDescription(`[${inlineCodeBlock(`discord.gg/${invite?.code}`)}](https://discord.gg/${invite?.code})`)
32 | .setFooter({ text: `Invite created ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
33 | .setType(Events.InviteCreate);
34 |
35 | let channelType;
36 | switch (invite?.channel?.type) {
37 | case ChannelType.GuildAnnouncement:
38 | channelType = 'Announcement channel';
39 | break;
40 | case ChannelType.GuildForum:
41 | channelType = 'Forum channel';
42 | break;
43 | case ChannelType.GuildStageVoice:
44 | channelType = 'Stage channel';
45 | break;
46 | case ChannelType.GuildText:
47 | channelType = 'Text channel';
48 | break;
49 | case ChannelType.GuildVoice:
50 | channelType = 'Voice channel';
51 | break;
52 | default:
53 | break;
54 | }
55 | if (channelType) embed.addFields({ name: channelType, value: `<#${invite?.channelId}>`, inline: true });
56 | if (invite?.expiresTimestamp) embed.addFields({ name: 'Expires', value: ``, inline: true });
57 | if (invite?.maxUses) embed.addFields({ name: 'Uses', value: `${inlineCodeBlock(`${invite.uses}/${invite.maxUses}`)}`, inline: true });
58 | if (invite?.guildScheduledEvent) embed.addFields({ name: 'Associated event', value: `[${inlineCodeBlock(`${invite.guildScheduledEvent.name}`)}](${invite.guildScheduledEvent.url})`, inline: true });
59 |
60 | const details = [];
61 | if (invite?.temporary) details.push(`${Emojis.Bullet}${inlineCodeBlock(`Grants temporary membership`)}`);
62 | if (details.length) embed.addFields({ name: 'Details', value: details.join('\n') });
63 |
64 | return [embed]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/listeners/guilds/invites/inviteDeleteLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { Emojis } from '#utils/constants';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { BaseGuildTextChannel, ChannelType, Guild, Invite, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.InviteDelete })
9 | export class UserEvent extends Listener {
10 | public async run(invite: Invite) {
11 | if (isNullish(invite.guild)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: invite.guild.id } });
14 | if (!guildSettingsInfoLogs?.inviteDeleteLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const guild = this.container.client.guilds.resolve(invite.guild.id);
17 | const fetchedInvite = await guild?.invites.fetch({ code: invite.code });
18 | const logChannel = guild?.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
19 | const executor = invite.inviter;
20 |
21 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(fetchedInvite, guild, executor));
22 | }
23 |
24 | private generateGuildLog(invite: Invite | undefined, guild: Guild | null, executor: User | null | undefined) {
25 | const embed = new GuildLogEmbed()
26 | .setAuthor({
27 | name: `${invite?.code}`,
28 | url: `https://discord.gg/${invite?.code}`,
29 | iconURL: guild?.iconURL() as string
30 | })
31 | .setDescription(`[${inlineCodeBlock(`discord.gg/${invite?.code}`)}](https://discord.gg/${invite?.code})`)
32 | .setFooter({ text: `Invite deleted ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
33 | .setType(Events.InviteDelete);
34 |
35 | let channelType;
36 | switch (invite?.channel?.type) {
37 | case ChannelType.GuildAnnouncement:
38 | channelType = 'Announcement channel';
39 | break;
40 | case ChannelType.GuildForum:
41 | channelType = 'Forum channel';
42 | break;
43 | case ChannelType.GuildStageVoice:
44 | channelType = 'Stage channel';
45 | break;
46 | case ChannelType.GuildText:
47 | channelType = 'Text channel';
48 | break;
49 | case ChannelType.GuildVoice:
50 | channelType = 'Voice channel';
51 | break;
52 | default:
53 | break;
54 | }
55 | if (channelType) embed.addFields({ name: channelType, value: `<#${invite?.channelId}>`, inline: true });
56 | if (invite?.createdTimestamp) embed.addFields({ name: 'Created', value: ``, inline: true });
57 | if (invite?.maxUses) embed.addFields({ name: 'Uses', value: `${inlineCodeBlock(`${invite.uses}/${invite.maxUses}`)}`, inline: true });
58 |
59 | const details = [];
60 | if (invite?.temporary) details.push(`${Emojis.Bullet}${inlineCodeBlock(`Grants temporary membership`)}`);
61 | if (details.length) embed.addFields({ name: 'Details', value: details.join('\n') });
62 |
63 | return [embed]
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/listeners/guilds/members/guildMemberAdd.ts:
--------------------------------------------------------------------------------
1 | import { initializeMember } from '#utils/functions/initialize';
2 | import { ApplyOptions } from '@sapphire/decorators';
3 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
4 | import { isNullish } from '@sapphire/utilities';
5 | import { GuildMember } from 'discord.js';
6 |
7 | @ApplyOptions({ event: Events.GuildMemberAdd })
8 | export class UserEvent extends Listener {
9 | public async run(member: GuildMember) {
10 | if (isNullish(member.id)) return;
11 | if (member.user.bot) return;
12 |
13 | let memberData = await this.container.prisma.member.findFirst({ where: { userID: member.id, guildID: member.guild.id } });
14 | if (!memberData) {
15 | await initializeMember(member.user, member.guild);
16 | memberData = await this.container.prisma.member.findFirst({ where: { userID: member.id, guildID: member.guild.id } });
17 | }
18 |
19 | const joinTimes = memberData?.joinTimes;
20 | joinTimes?.push(new Date(Date.now()));
21 | const usernameHistory = memberData?.usernameHistory;
22 | if (!usernameHistory?.includes(member.user.username)) usernameHistory?.push(member.user.username);
23 | const displayNameHistory = memberData?.displayNameHistory;
24 | if (!displayNameHistory?.includes(member.displayName)) displayNameHistory?.push(member.displayName);
25 |
26 | await this.container.prisma.member.update({ where: { id: memberData?.id }, data: { joinTimes, usernameHistory, displayNameHistory } });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/listeners/guilds/members/guildMemberAddLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { ApplyOptions } from '@sapphire/decorators';
3 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
4 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
5 | import { BaseGuildTextChannel, GuildMember } from 'discord.js';
6 |
7 | @ApplyOptions({ event: Events.GuildMemberAdd })
8 | export class UserEvent extends Listener {
9 | public async run(member: GuildMember) {
10 | if (isNullish(member.id)) return;
11 | if (member.user.bot) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: member.guild.id } });
14 | if (!guildSettingsInfoLogs?.guildMemberAddLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = member.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 |
18 | return this.container.client.emit('guildLogCreate', logChannel, (await this.generateGuildLog(member)));
19 | }
20 |
21 | private generateGuildLog(member: GuildMember) {
22 | const embed = new GuildLogEmbed()
23 | .setAuthor({
24 | name: member.user.username,
25 | url: `https://discord.com/users/${member.user.id}`,
26 | iconURL: member.user.displayAvatarURL()
27 | })
28 | .setDescription(inlineCodeBlock(member.user.id))
29 | .addFields({ name: 'Registered', value: ``, inline: true })
30 | .setFooter({ text: 'User joined' })
31 | .setType(Events.GuildMemberAdd);
32 | // TODO Track previous times users have joined/left server
33 | // TODO Track previous usernames/nicknames users have had while on this server
34 | return [embed]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/listeners/guilds/members/guildMemberRemoveLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { ApplyOptions } from '@sapphire/decorators';
3 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
4 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
5 | import { BaseGuildTextChannel, GuildMember } from 'discord.js';
6 |
7 | @ApplyOptions({ event: Events.GuildMemberRemove })
8 | export class UserEvent extends Listener {
9 | public async run(member: GuildMember) {
10 | if (isNullish(member.id)) return;
11 | if (member.user.bot) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: member.guild.id } });
14 | if (!guildSettingsInfoLogs?.guildMemberRemoveLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = member.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 |
18 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(member));
19 | }
20 |
21 | private generateGuildLog(member: GuildMember) {
22 | const embed = new GuildLogEmbed()
23 | .setAuthor({
24 | name: member.user.username,
25 | url: `https://discord.com/users/${member.user.id}`,
26 | iconURL: member.user.displayAvatarURL()
27 | })
28 | .setDescription(inlineCodeBlock(member.user.id))
29 | .addFields({ name: 'Joined', value: ``, inline: true })
30 | .setFooter({ text: 'User left' })
31 | .setType(Events.GuildMemberRemove);
32 |
33 | return [embed]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/listeners/guilds/members/guildMemberUpdate.ts:
--------------------------------------------------------------------------------
1 | import { initializeMember } from '#utils/functions/initialize';
2 | import { ApplyOptions } from '@sapphire/decorators';
3 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
4 | import { isNullish } from '@sapphire/utilities';
5 | import { GuildMember } from 'discord.js';
6 |
7 | @ApplyOptions({ event: Events.GuildMemberUpdate })
8 | export class UserEvent extends Listener {
9 | public async run(oldMember: GuildMember, member: GuildMember) {
10 | if (isNullish(member.id)) return;
11 | if (member.user.bot) return;
12 |
13 | let memberData = await this.container.prisma.member.findFirst({ where: { userID: member.id, guildID: member.guild.id } });
14 | if (!memberData) {
15 | await initializeMember(member.user, member.guild);
16 | memberData = await this.container.prisma.member.findFirst({ where: { userID: member.id, guildID: member.guild.id } });
17 | }
18 |
19 | const usernameHistory = memberData?.usernameHistory;
20 | const displayNameHistory = memberData?.displayNameHistory;
21 |
22 | if (oldMember.user.username !== member.user.username) usernameHistory?.push(oldMember.user.username);
23 | if (oldMember.displayName !== member.displayName) displayNameHistory?.push(oldMember.displayName);
24 |
25 | await this.container.prisma.member.update({ where: { id: memberData?.id }, data: { usernameHistory, displayNameHistory } });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/listeners/guilds/members/guildMemberUpdateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { Emojis } from '#utils/constants';
3 | import { getAuditLogExecutor } from '#utils/util';
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
6 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
7 | import { AuditLogEvent, BaseGuildTextChannel, GuildMember, User } from 'discord.js';
8 |
9 | @ApplyOptions({ event: Events.GuildMemberUpdate })
10 | export class UserEvent extends Listener {
11 | public async run(oldMember: GuildMember, member: GuildMember) {
12 | if (isNullish(member.id)) return;
13 |
14 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: member.guild.id } });
15 | if (!guildSettingsInfoLogs?.guildMemberUpdateLog || !guildSettingsInfoLogs.infoLogChannel) return;
16 |
17 | const logChannel = member.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
18 | const executor = await getAuditLogExecutor(AuditLogEvent.MemberUpdate, member.guild);
19 |
20 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(oldMember, member, executor));
21 | }
22 |
23 | private generateGuildLog(oldMember: GuildMember, member: GuildMember, executor: User | null | undefined) {
24 | const embed = new GuildLogEmbed()
25 | .setAuthor({
26 | name: member.user.username,
27 | url: `https://discord.com/users/${member.user.id}`,
28 | iconURL: member.user.displayAvatarURL()
29 | })
30 | .setDescription(inlineCodeBlock(member.id))
31 | .setFooter({ text: `Member updated ${isNullish(executor) ? '' : executor.id === member.id ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor.id === member.id ? undefined : executor?.displayAvatarURL() })
32 | .setType(Events.GuildMemberUpdate);
33 |
34 | const changes = [];
35 | if (oldMember.displayName !== member.displayName) changes.push(`${Emojis.Bullet}**Display name**: ${inlineCodeBlock(`${oldMember.displayName}`)} to ${inlineCodeBlock(`${member.displayName}`)}`);
36 | if (oldMember.roles.cache !== member.roles.cache) {
37 | const oldRoles = oldMember.roles.cache;
38 | const roles = member.roles.cache;
39 | const addedRoles = [];
40 | const removedRoles = [];
41 | for (const [key, role] of roles.entries()) {
42 | if (!oldRoles.has(key)) addedRoles.push(`${role}`);
43 | }
44 | for (const [key, role] of oldRoles.entries()) {
45 | if (!roles.has(key)) removedRoles.push(`${role}`);
46 | }
47 |
48 | if (addedRoles.length) changes.push(`${Emojis.Bullet}**Role${addedRoles.length > 1 ? 's' : ''} added**: ${addedRoles.join(' ')}`);
49 | if (removedRoles.length) changes.push(`${Emojis.Bullet}**Role${removedRoles.length > 1 ? 's' : ''} removed**: ${removedRoles.join(' ')}`);
50 | }
51 | if (changes.length) embed.addFields({ name: 'Changes', value: changes.join('\n') });
52 |
53 | return [embed]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/listeners/guilds/messages/messageCreate.ts:
--------------------------------------------------------------------------------
1 | import { initializeMember } from '#utils/functions/initialize';
2 | import { ApplyOptions } from '@sapphire/decorators';
3 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
4 | import { isNullish } from '@sapphire/utilities';
5 | import { Message } from 'discord.js';
6 |
7 | @ApplyOptions({ event: Events.MessageCreate })
8 | export class UserEvent extends Listener {
9 | public async run(message: Message) {
10 | if (isNullish(message.author.id)) return;
11 | if (message.author.bot) return;
12 |
13 | if (message.guild) {
14 | let memberData = await this.container.prisma.member.findFirst({ where: { userID: message.author.id, guildID: message.guild.id } });
15 | if (!memberData) {
16 | await initializeMember(message.author, message.guild);
17 | memberData = await this.container.prisma.member.findFirst({ where: { userID: message.author.id, guildID: message.guild.id } });
18 | }
19 | }
20 |
21 |
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/listeners/guilds/messages/messageDeleteLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getContent } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { cutText, isNullish } from '@sapphire/utilities';
6 | import { BaseGuildTextChannel, Message } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.MessageDelete })
9 | export class UserEvent extends Listener {
10 | public async run(message: Message) {
11 | if (isNullish(message.id)) return;
12 | if (isNullish(message.guild)) return;
13 |
14 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: message.guild?.id } });
15 | if (!guildSettingsInfoLogs?.messageDeleteLog || !guildSettingsInfoLogs.infoLogChannel) return;
16 |
17 | const logChannel = message.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(message));
20 | }
21 |
22 | private generateGuildLog(message: Message) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: message.author.username,
26 | url: `https://discord.com/users/${message.author.id}`,
27 | iconURL: message.author.displayAvatarURL()
28 | })
29 | .setDescription(`<#${message.channel.id}>`)
30 | .setFooter({ text: 'Message deleted' })
31 | .setType(Events.MessageDelete);
32 |
33 | const messageContent = getContent(message);
34 | if (messageContent) embed.addFields({ name: 'Message', value: cutText(messageContent, 1024) });
35 | if (message?.createdTimestamp) embed.addFields({ name: 'Sent', value: ``, inline: true });
36 |
37 | return [embed]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/listeners/guilds/messages/messageUpdateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getContent } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { cutText, inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { BaseGuildTextChannel, Message } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.MessageUpdate })
9 | export class UserEvent extends Listener {
10 | public async run(oldMessage: Message, message: Message) {
11 | if (isNullish(message.id)) return;
12 | if (isNullish(message.guild)) return;
13 | if (message.author.bot) return;
14 |
15 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: message.guild?.id } });
16 | if (!guildSettingsInfoLogs?.messageUpdateLog || !guildSettingsInfoLogs.infoLogChannel) return;
17 |
18 | const logChannel = message.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
19 |
20 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(oldMessage, message));
21 | }
22 |
23 | private generateGuildLog(oldMessage: Message, message: Message) {
24 | const embed = new GuildLogEmbed()
25 | .setAuthor({
26 | name: message.author.username,
27 | url: `https://discord.com/users/${message.author.id}`,
28 | iconURL: message.author.displayAvatarURL()
29 | })
30 | .setDescription(`<#${message.channel.id}> - [${inlineCodeBlock(`go to message`)}](${message.url})`)
31 | .setFooter({ text: 'Message edited' })
32 | .setType(Events.MessageUpdate);
33 |
34 | const oldMessageContent = getContent(oldMessage);
35 | const messageContent = getContent(message);
36 | if (oldMessageContent !== messageContent) {
37 | if (oldMessageContent) embed.addFields({ name: 'Before', value: cutText(oldMessageContent, 1024) });
38 | if (messageContent) embed.addFields({ name: 'After', value: cutText(messageContent, 1024) });
39 | }
40 |
41 | if (!embed.data.fields?.length) return;
42 | return [embed]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/listeners/guilds/roles/roleCreateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, Role, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.GuildRoleCreate })
9 | export class UserEvent extends Listener {
10 | public async run(role: Role) {
11 | if (isNullish(role.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: role.guild.id } });
14 | if (!guildSettingsInfoLogs?.roleCreateLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = role.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.RoleCreate, role.guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(role, executor));
20 | }
21 |
22 | private generateGuildLog(role: Role, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: `${role?.unicodeEmoji ?? ''} ${role.name}`,
26 | iconURL: role.guild.iconURL() ?? undefined
27 | })
28 | .setDescription(inlineCodeBlock(role.id))
29 | .setFooter({ text: `Role created ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
30 | .setType(Events.GuildRoleCreate);
31 |
32 | return [embed]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/listeners/guilds/roles/roleDeleteLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, Role, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.GuildRoleDelete })
9 | export class UserEvent extends Listener {
10 | public async run(role: Role) {
11 | if (isNullish(role.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: role.guild.id } });
14 | if (!guildSettingsInfoLogs?.roleDeleteLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = role.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.ChannelDelete, role.guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(role, executor));
20 | }
21 |
22 | private generateGuildLog(role: Role, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: `${role?.unicodeEmoji ?? ''} ${role.name}`,
26 | iconURL: role.guild.iconURL() ?? undefined
27 | })
28 | .setDescription(inlineCodeBlock(role.id))
29 | .setFooter({ text: `Role deleted ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
30 | .setType(Events.GuildRoleDelete);
31 |
32 | if (role?.createdTimestamp) embed.addFields({ name: 'Created', value: ``, inline: true });
33 |
34 | return [embed]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/listeners/guilds/roles/roleUpdateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { Emojis } from '#utils/constants';
3 | import { getPermissionDifference } from '#utils/functions/permissions';
4 | import { getAuditLogExecutor } from '#utils/util';
5 | import { ApplyOptions } from '@sapphire/decorators';
6 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
7 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
8 | import { AuditLogEvent, BaseGuildTextChannel, Role, User } from 'discord.js';
9 |
10 | @ApplyOptions({ event: Events.GuildRoleUpdate })
11 | export class UserEvent extends Listener {
12 | public async run(oldRole: Role, role: Role) {
13 | if (isNullish(role.id)) return;
14 |
15 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: role.guild.id } });
16 | if (!guildSettingsInfoLogs?.roleUpdateLog || !guildSettingsInfoLogs.infoLogChannel) return;
17 |
18 | const logChannel = role.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
19 | const executor = await getAuditLogExecutor(AuditLogEvent.RoleUpdate, role.guild);
20 |
21 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(oldRole, role, executor));
22 | }
23 |
24 | private generateGuildLog(oldRole: Role, role: Role, executor: User | null | undefined) {
25 | const embed = new GuildLogEmbed()
26 | .setAuthor({
27 | name: `${role?.unicodeEmoji ?? ''} ${role.name}`,
28 | iconURL: role.guild.iconURL() ?? undefined
29 | })
30 | .setDescription(inlineCodeBlock(role.id))
31 | .setThumbnail(role?.iconURL() ?? role.guild?.iconURL() ?? null)
32 | .setFooter({ text: `Role updated ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
33 | .setType(Events.GuildRoleUpdate);
34 |
35 | const changes = [];
36 | if (oldRole.name !== role.name) changes.push(`${Emojis.Bullet}**Name**: ${inlineCodeBlock(`${oldRole.name}`)} to ${inlineCodeBlock(`${role.name}`)}`);
37 | if (oldRole.color !== role.color) changes.push(`${Emojis.Bullet}**Color**: ${inlineCodeBlock(`${oldRole.hexColor}`)} to ${inlineCodeBlock(`${role.hexColor}`)}`);
38 | if (oldRole.icon !== role.icon) changes.push(`${Emojis.Bullet}**Icon**: ${oldRole.iconURL() ? `[${inlineCodeBlock('click to view')}](${oldRole.iconURL()})` : inlineCodeBlock('Not set')} to ${role.iconURL() ? `[${inlineCodeBlock('click to view')}](${role.iconURL()})` : inlineCodeBlock('Not set')}`);
39 | if (oldRole.hoist !== role.hoist) changes.push(`${Emojis.Bullet}**Displayed separately**: ${role.hoist ? Emojis.ToggleOn : Emojis.ToggleOff}`);
40 | if (oldRole.mentionable !== role.mentionable) changes.push(`${Emojis.Bullet}**Mentionable**: ${role.mentionable ? Emojis.ToggleOn : Emojis.ToggleOff}`);
41 | if (oldRole.permissions !== role.permissions) {
42 | const permissionDifference = getPermissionDifference(oldRole.permissions, role.permissions);
43 | if (permissionDifference.added.length) changes.push(`${Emojis.Bullet}**Permissions added**: ${permissionDifference.added.join(' ')}`);
44 | if (permissionDifference.removed.length) changes.push(`${Emojis.Bullet}**Permissions removed**: ${permissionDifference.removed.join(' ')}`);
45 | }
46 | if (oldRole.managed !== role.managed) changes.push(`${Emojis.Bullet}**Managed by external service**: ${role.managed ? Emojis.ToggleOn : Emojis.ToggleOff}`);
47 | if (changes.length) embed.addFields({ name: 'Changes', value: changes.join('\n') });
48 |
49 | return [embed]
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/listeners/guilds/stages/stageInstanceCreateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { cutText, inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, Guild, StageInstance, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.StageInstanceCreate })
9 | export class UserEvent extends Listener {
10 | public async run(stage: StageInstance) {
11 | if (isNullish(stage.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: stage.guild?.id } });
14 | if (!guildSettingsInfoLogs?.stageInstanceCreateLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = stage.guild?.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.StageInstanceCreate, stage.guild as Guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(stage, executor));
20 | }
21 |
22 | private generateGuildLog(stage: StageInstance, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: cutText(stage.topic, 256),
26 | url: `https://discord.com/channels/${stage.guildId}/${stage.channelId}`,
27 | iconURL: stage.guild?.iconURL() ?? undefined
28 | })
29 | .setDescription(inlineCodeBlock(stage.id))
30 | .addFields({ name: 'Stage channel', value: `<#${stage.channelId}>`, inline: true })
31 | .setFooter({ text: `Stage started ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
32 | .setType(Events.StageInstanceCreate);
33 |
34 | if (stage.guildScheduledEvent) embed.addFields({ name: 'Associated event', value: `[${inlineCodeBlock(`${stage.guildScheduledEvent.name}`)}](${stage.guildScheduledEvent.url})`, inline: true });
35 |
36 | return [embed];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/listeners/guilds/stages/stageInstanceDeleteLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { cutText, inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, Guild, StageInstance, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.StageInstanceDelete })
9 | export class UserEvent extends Listener {
10 | public async run(stage: StageInstance) {
11 | if (isNullish(stage.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: stage.guild?.id } });
14 | if (!guildSettingsInfoLogs?.stageInstanceDeleteLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = stage.guild?.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.StageInstanceDelete, stage.guild as Guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(stage, executor));
20 | }
21 |
22 | private generateGuildLog(stage: StageInstance, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: cutText(stage.topic, 256),
26 | url: `https://discord.com/channels/${stage.guildId}/${stage.channelId}`,
27 | iconURL: stage.guild?.iconURL() ?? undefined
28 | })
29 | .setDescription(inlineCodeBlock(stage.id))
30 | .addFields({ name: 'Stage channel', value: `<#${stage.channelId}>`, inline: true })
31 | .addFields({ name: 'Started', value: ``, inline: true })
32 | .setFooter({ text: `Stage ended ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
33 | .setType(Events.StageInstanceDelete);
34 |
35 | if (stage.guildScheduledEvent) embed.addFields({ name: 'Associated event', value: `[${inlineCodeBlock(`${stage.guildScheduledEvent.name}`)}](${stage.guildScheduledEvent.url})` });
36 |
37 | return [embed];
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/listeners/guilds/stages/stageInstanceUpdateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { Emojis } from '#utils/constants';
3 | import { getAuditLogExecutor } from '#utils/util';
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
6 | import { codeBlock, cutText, inlineCodeBlock, isNullish } from '@sapphire/utilities';
7 | import { AuditLogEvent, BaseGuildTextChannel, Guild, StageInstance, User } from 'discord.js';
8 |
9 | @ApplyOptions({ event: Events.StageInstanceUpdate })
10 | export class UserEvent extends Listener {
11 | public async run(oldStage: StageInstance, stage: StageInstance) {
12 | if (isNullish(stage.id)) return;
13 |
14 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: stage.guild?.id } });
15 | if (!guildSettingsInfoLogs?.stageInstanceUpdateLog || !guildSettingsInfoLogs.infoLogChannel) return;
16 |
17 | const logChannel = stage.guild?.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
18 | const executor = await getAuditLogExecutor(AuditLogEvent.RoleUpdate, stage.guild as Guild);
19 |
20 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(oldStage, stage, executor));
21 | }
22 |
23 | private generateGuildLog(oldStage: StageInstance, stage: StageInstance, executor: User | null | undefined) {
24 | const embed = new GuildLogEmbed()
25 | .setAuthor({
26 | name: cutText(stage.topic, 256),
27 | url: `https://discord.com/channels/${stage.guildId}/${stage.channelId}`,
28 | iconURL: stage.guild?.iconURL() ?? undefined
29 | })
30 | .setDescription(inlineCodeBlock(stage.id))
31 | .setFooter({ text: `Stage updated ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
32 | .setType(Events.GuildRoleUpdate);
33 |
34 | if (stage.guildScheduledEvent) embed.addFields({ name: 'Associated event', value: `[${inlineCodeBlock(`${stage.guildScheduledEvent.name}`)}](${stage.guildScheduledEvent.url})`, inline: true });
35 |
36 | const changes = [];
37 | const privacyLevels = ['', 'Public', 'Members only']
38 | if (oldStage.topic !== stage.topic) changes.push(`${Emojis.Bullet}**Topic**:\n${codeBlock(`${oldStage.topic}`)}to\n${codeBlock(`${stage.topic}`)}`);
39 | if (oldStage.privacyLevel !== stage.privacyLevel) changes.push(`${Emojis.Bullet}**Privacy**: ${privacyLevels[oldStage.privacyLevel]} to ${privacyLevels[stage.privacyLevel]}`);
40 | if (changes.length) embed.addFields({ name: 'Changes', value: changes.join('\n') });
41 |
42 | return [embed]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/listeners/guilds/stickers/stickerCreateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, Guild, Sticker, StickerFormatType, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.GuildStickerCreate })
9 | export class UserEvent extends Listener {
10 | public async run(sticker: Sticker) {
11 | if (isNullish(sticker.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: sticker.guild?.id } });
14 | if (!guildSettingsInfoLogs?.stickerCreateLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = sticker.guild?.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.EmojiCreate, sticker.guild as Guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(sticker, executor));
20 | }
21 |
22 | private generateGuildLog(sticker: Sticker, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: sticker.name,
26 | url: sticker.url,
27 | iconURL: sticker.url
28 | })
29 | .setDescription(inlineCodeBlock(sticker.id))
30 | .setThumbnail(sticker.url)
31 | .addFields({ name: 'Format', value: inlineCodeBlock(StickerFormatType[sticker.format]), inline: true })
32 | .setFooter({ text: `Sticker created ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
33 | .setType(Events.GuildStickerCreate);
34 |
35 | if (sticker.tags) embed.addFields({ name: 'Emoji', value: `:${sticker.tags}:`, inline: true });
36 |
37 | return [embed]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/listeners/guilds/stickers/stickerDeleteLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, Guild, Sticker, StickerFormatType, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.GuildStickerDelete })
9 | export class UserEvent extends Listener {
10 | public async run(sticker: Sticker) {
11 | if (isNullish(sticker.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: sticker.guild?.id } });
14 | if (!guildSettingsInfoLogs?.stickerDeleteLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = sticker.guild?.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.EmojiCreate, sticker.guild as Guild);
18 |
19 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(sticker, executor));
20 | }
21 |
22 | private generateGuildLog(sticker: Sticker, executor: User | null | undefined) {
23 | const embed = new GuildLogEmbed()
24 | .setAuthor({
25 | name: sticker.name,
26 | url: sticker.url,
27 | iconURL: sticker.url
28 | })
29 | .setDescription(inlineCodeBlock(sticker.id))
30 | .setThumbnail(sticker.url)
31 | .addFields({ name: 'Format', value: inlineCodeBlock(StickerFormatType[sticker.format]), inline: true })
32 | .setFooter({ text: `Sticker deleted ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
33 | .setType(Events.GuildStickerDelete);
34 |
35 | if (sticker.tags) embed.addFields({ name: 'Emoji', value: `:${sticker.tags}:`, inline: true });
36 | if (sticker?.createdTimestamp) embed.addFields({ name: 'Created', value: ``, inline: true });
37 |
38 | return [embed]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/listeners/guilds/stickers/stickerUpdateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { Emojis } from '#utils/constants';
3 | import { getAuditLogExecutor } from '#utils/util';
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
6 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
7 | import { AuditLogEvent, BaseGuildTextChannel, Guild, Sticker, User } from 'discord.js';
8 |
9 | @ApplyOptions({ event: Events.GuildStickerUpdate })
10 | export class UserEvent extends Listener {
11 | public async run(oldSticker: Sticker, sticker: Sticker) {
12 | if (isNullish(sticker.id)) return;
13 |
14 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: sticker.guild?.id } });
15 | if (!guildSettingsInfoLogs?.stickerUpdateLog || !guildSettingsInfoLogs.infoLogChannel) return;
16 |
17 | const logChannel = sticker.guild?.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
18 | const executor = await getAuditLogExecutor(AuditLogEvent.EmojiCreate, sticker.guild as Guild);
19 |
20 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(oldSticker, sticker, executor));
21 | }
22 |
23 | private generateGuildLog(oldSticker: Sticker, sticker: Sticker, executor: User | null | undefined) {
24 | const embed = new GuildLogEmbed()
25 | .setAuthor({
26 | name: sticker.name,
27 | url: sticker.url,
28 | iconURL: sticker.url
29 | })
30 | .setDescription(inlineCodeBlock(sticker.id))
31 | .setThumbnail(sticker.url)
32 | .setFooter({ text: `Sticker updated ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
33 | .setType(Events.GuildStickerUpdate);
34 |
35 | const changes = [];
36 | if (oldSticker.name !== sticker.name) changes.push(`${Emojis.Bullet}**Name changed**: ${inlineCodeBlock(`${oldSticker.name}`)} to ${inlineCodeBlock(`${sticker.name}`)}`);
37 | if (oldSticker.tags !== sticker.tags) changes.push(`${Emojis.Bullet}**Emoji changed**: :${oldSticker.tags}: to :${sticker.tags}:`);
38 | if (oldSticker.description !== sticker.description) changes.push(`${Emojis.Bullet}**Description changed**: ${inlineCodeBlock(`${oldSticker.description || 'Not set'}`)} to ${inlineCodeBlock(`${sticker.description || 'Not set'}`)}`);
39 | if (changes.length) embed.addFields({ name: 'Changes', value: changes.join('\n') });
40 |
41 | return [embed]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/listeners/guilds/threads/threadCreateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, ChannelType, ThreadChannel, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.ThreadCreate })
9 | export class UserEvent extends Listener {
10 | public async run(thread: ThreadChannel) {
11 | if (isNullish(thread.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: thread.guild.id } });
14 | if (!guildSettingsInfoLogs?.threadCreateLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = thread.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.ThreadCreate, thread.guild);
18 | const isForumThread = thread.parent?.type === ChannelType.GuildForum;
19 |
20 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(thread, executor, isForumThread));
21 | }
22 |
23 | private generateGuildLog(thread: ThreadChannel, executor: User | null | undefined, isForumThread: boolean) {
24 | const postOrThread = isForumThread ? 'Post' : 'Thread';
25 | const embed = new GuildLogEmbed()
26 | .setAuthor({
27 | name: thread.name,
28 | url: `https://discord.com/channels/${thread.guildId}/${thread.id}`,
29 | iconURL: thread.guild.iconURL() ?? undefined
30 | })
31 | .setDescription(inlineCodeBlock(thread.id))
32 | .setFooter({ text: `${postOrThread} created ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
33 | .setType(Events.ThreadCreate);
34 |
35 | if (thread.parent) embed.addFields({ name: `${thread.parent.type === ChannelType.GuildAnnouncement ? 'Announcement' : 'Text'} channel`, value: `<#${thread.parentId}>`, inline: true });
36 | if (thread.appliedTags.length && thread.parent?.type === ChannelType.GuildForum) {
37 | const appliedTags = thread.parent.availableTags.filter(tag => thread.appliedTags.includes(tag.id)).map(tag => `${tag.emoji ? `${thread.guild.emojis.resolve(tag.emoji.id as string)} ` ?? `${tag.emoji.name} ` : ''}${inlineCodeBlock(tag.name)}`).join(' ');
38 | embed.addFields({ name: 'Applied tags', value: appliedTags });
39 | }
40 |
41 | return [embed]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/listeners/guilds/threads/threadDeleteLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { getAuditLogExecutor } from '#utils/util';
3 | import { ApplyOptions } from '@sapphire/decorators';
4 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
5 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
6 | import { AuditLogEvent, BaseGuildTextChannel, ChannelType, ThreadChannel, User } from 'discord.js';
7 |
8 | @ApplyOptions({ event: Events.ThreadDelete })
9 | export class UserEvent extends Listener {
10 | public async run(thread: ThreadChannel) {
11 | if (isNullish(thread.id)) return;
12 |
13 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: thread.guild.id } });
14 | if (!guildSettingsInfoLogs?.threadDeleteLog || !guildSettingsInfoLogs.infoLogChannel) return;
15 |
16 | const logChannel = thread.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
17 | const executor = await getAuditLogExecutor(AuditLogEvent.ThreadDelete, thread.guild);
18 | const isForumThread = thread.parent?.type === ChannelType.GuildForum;
19 |
20 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(thread, executor, isForumThread));
21 | }
22 |
23 | private generateGuildLog(thread: ThreadChannel, executor: User | null | undefined, isForumThread: boolean) {
24 | const postOrThread = isForumThread ? 'Post' : 'Thread';
25 | const embed = new GuildLogEmbed()
26 | .setAuthor({
27 | name: thread.name,
28 | url: `https://discord.com/channels/${thread.guildId}/${thread.id}`,
29 | iconURL: thread.guild.iconURL() ?? undefined
30 | })
31 | .setDescription(inlineCodeBlock(thread.id))
32 | .setFooter({ text: `${postOrThread} deleted ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
33 | .setType(Events.ThreadDelete);
34 |
35 | if (thread.parent) embed.addFields({ name: `${thread.parent.type === ChannelType.GuildAnnouncement ? 'Announcement' : 'Text'} channel`, value: `<#${thread.parentId}>`, inline: true });
36 | if (thread.ownerId) embed.addFields({ name: 'Started by', value: `<@${thread.ownerId}>`, inline: true });
37 | if (thread.totalMessageSent) embed.addFields({ name: 'Total messages', value: inlineCodeBlock(`${thread.totalMessageSent}`), inline: true });
38 | if (thread?.createdTimestamp) embed.addFields({ name: 'Created', value: ``, inline: true });
39 | if (thread.appliedTags.length && thread.parent?.type === ChannelType.GuildForum) {
40 | const appliedTags = thread.parent.availableTags.filter(tag => thread.appliedTags.includes(tag.id)).map(tag => `${tag.emoji ? `${thread.guild.emojis.resolve(tag.emoji.id as string)} ` ?? `${tag.emoji.name} ` : ''}${inlineCodeBlock(tag.name)}`).join(' ');
41 | embed.addFields({ name: 'Applied tags', value: appliedTags });
42 | }
43 |
44 | return [embed]
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/listeners/guilds/threads/threadUpdateLog.ts:
--------------------------------------------------------------------------------
1 | import { GuildLogEmbed } from '#lib/extensions/GuildLogEmbed';
2 | import { minutes, seconds } from '#utils/common/times';
3 | import { Emojis } from '#utils/constants';
4 | import { getAuditLogExecutor } from '#utils/util';
5 | import { ApplyOptions } from '@sapphire/decorators';
6 | import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
7 | import { DurationFormatter } from '@sapphire/time-utilities';
8 | import { inlineCodeBlock, isNullish } from '@sapphire/utilities';
9 | import { AuditLogEvent, BaseGuildTextChannel, ChannelType, ForumChannel, ThreadChannel, User } from 'discord.js';
10 |
11 | @ApplyOptions({ event: Events.ThreadUpdate })
12 | export class UserEvent extends Listener {
13 | public async run(oldThread: ThreadChannel, thread: ThreadChannel) {
14 | if (isNullish(thread.id)) return;
15 |
16 | const guildSettingsInfoLogs = await this.container.prisma.guildSettingsInfoLogs.findUnique({ where: { id: thread.guild.id } });
17 | if (!guildSettingsInfoLogs?.threadUpdateLog || !guildSettingsInfoLogs.infoLogChannel) return;
18 |
19 | const logChannel = thread.guild.channels.resolve(guildSettingsInfoLogs.infoLogChannel) as BaseGuildTextChannel;
20 | const executor = await getAuditLogExecutor(AuditLogEvent.ThreadUpdate, thread.guild);
21 | const isForumThread = thread.parent?.type === ChannelType.GuildForum;
22 |
23 | return this.container.client.emit('guildLogCreate', logChannel, this.generateGuildLog(oldThread, thread, executor, isForumThread));
24 | }
25 |
26 | private generateGuildLog(oldThread: ThreadChannel, thread: ThreadChannel, executor: User | null | undefined, isForumThread: boolean) {
27 | const postOrThread = isForumThread ? 'Post' : 'Thread';
28 | const embed = new GuildLogEmbed()
29 | .setAuthor({
30 | name: thread.name,
31 | url: `https://discord.com/channels/${thread.guildId}/${thread.id}`,
32 | iconURL: thread.guild.iconURL() ?? undefined
33 | })
34 | .setDescription(inlineCodeBlock(thread.id))
35 | .setFooter({ text: `${postOrThread} edited ${isNullish(executor) ? '' : `by ${executor.username}`}`, iconURL: isNullish(executor) ? undefined : executor?.displayAvatarURL() })
36 | .setType(Events.ThreadUpdate);
37 |
38 | if (thread.parent) embed.addFields({ name: `${thread.parent.type === ChannelType.GuildAnnouncement ? 'Announcement' : 'Text'} channel`, value: `<#${thread.parentId}>`, inline: true });
39 | if (thread.ownerId) embed.addFields({ name: 'Started by', value: `<@${thread.ownerId}>`, inline: true });
40 |
41 | const changes = [];
42 | if (oldThread.name !== thread.name) changes.push(`${Emojis.Bullet}**Name**: ${inlineCodeBlock(`${oldThread.name}`)} to ${inlineCodeBlock(`${thread.name}`)}`);
43 | if (oldThread.rateLimitPerUser !== thread.rateLimitPerUser) changes.push(`${Emojis.Bullet}**Slowmode**: ${inlineCodeBlock(`${oldThread.rateLimitPerUser ? new DurationFormatter().format(seconds(oldThread.rateLimitPerUser)) : 'Off'}`)} to ${inlineCodeBlock(`${thread.rateLimitPerUser ? new DurationFormatter().format(seconds(thread.rateLimitPerUser)) : 'Off'}`)}`);
44 | if (oldThread.autoArchiveDuration !== thread.autoArchiveDuration) changes.push(`${Emojis.Bullet}**Hide after inactivity**: ${inlineCodeBlock(`${new DurationFormatter().format(minutes(oldThread.autoArchiveDuration ?? 4320))}`)} to ${inlineCodeBlock(`${new DurationFormatter().format(minutes(thread.autoArchiveDuration ?? 4320))}`)}`);
45 | if (thread.parent?.type === ChannelType.GuildForum && oldThread.appliedTags !== thread.appliedTags) {
46 | const tagDifference = this.getTagDifference(thread.parent, oldThread.appliedTags, thread.appliedTags);
47 | if (tagDifference.added.length) changes.push(`${Emojis.Bullet}**Tags added**: ${tagDifference.added.join(', ')}`);
48 | if (tagDifference.removed.length) changes.push(`${Emojis.Bullet}**Tags removed**: ${tagDifference.removed.join(', ')}`);
49 | }
50 | if (changes.length) embed.addFields({ name: 'Changes', value: changes.join('\n') });
51 |
52 | return [embed];
53 | }
54 |
55 | private getTagDifference(forumChannel: ForumChannel, oldTag: string[], tag: string[]) {
56 | const oldTags = forumChannel.availableTags.filter(t => oldTag.includes(t.id));
57 | const tags = forumChannel.availableTags.filter(t => tag.includes(t.id));
58 | const added = tags.filter(t => !oldTags.includes(t)).map(t => `${t.emoji ? forumChannel.guild.emojis.resolve(t.emoji.id as string) ?? t.emoji.name : ''} ${inlineCodeBlock(t.name)}`);
59 | const removed = oldTags.filter(t => !tags.includes(t)).map(t => `${t.emoji ? forumChannel.guild.emojis.resolve(t.emoji.id as string) ?? t.emoji.name : ''} ${inlineCodeBlock(t.name)}`);
60 |
61 | const differences = {
62 | added,
63 | removed
64 | }
65 |
66 | return differences;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/listeners/ready.ts:
--------------------------------------------------------------------------------
1 | import { CONTROL_GUILD, DEV, INIT_ALL_MEMBERS, INIT_ALL_USERS, TOKENS, VERSION } from '#root/config';
2 | import { initializeGuild, initializeMember, initializeUser } from '#utils/functions/initialize';
3 | import type { ListenerOptions, PieceContext } from '@sapphire/framework';
4 | import { Listener, Store } from '@sapphire/framework';
5 | import { bgRed, blue, gray, green, red, whiteBright, yellow } from 'colorette';
6 |
7 | export class UserEvent extends Listener {
8 | private readonly style = DEV ? yellow : blue;
9 |
10 | public constructor(context: PieceContext, options?: ListenerOptions) {
11 | super(context, {
12 | ...options,
13 | once: true
14 | });
15 | }
16 |
17 | public async run() {
18 | this.printBanner();
19 | this.printStoreDebugInformation();
20 |
21 | await this.clientValidation();
22 | await this.guildValidation();
23 | if (INIT_ALL_USERS) await this.userValidation();
24 | if (INIT_ALL_MEMBERS) await this.memberValidation();
25 | }
26 |
27 | private printBanner() {
28 | const success = green('+');
29 | const failure = red('-');
30 |
31 | const line01 = whiteBright(String.raw` .▄##╗▄▄`);
32 | const line02 = whiteBright(String.raw` .S▓█████▓▓▒▄`);
33 | const line03 = whiteBright(String.raw` w┌╫▓╣██████▓▓▓▒ ▒▓▓▓▓▓▓▄, ▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▒ ▓▓, ▓▓ ▒▓▓▓▓▓▓▓▌ ▓▓▓▓▓▓▓▌`);
34 | const line04 = whiteBright(String.raw` ▄▄▄▓███${red('▀╫╠╠╠╫')}▓▓▓▓▌ ▓█ ╫██ ██▄ ██ └██ └██▄┌██╙ ╫█▌ .██`);
35 | const line05 = whiteBright(String.raw`║█████${red('▀╠╠╠▒▒')}▓▓▓▓▓▓▓ ▓██▓▓▓█▀ ██ ██▓▓███▄ ▀██▀ ╫█▌ .███████═`);
36 | const line06 = whiteBright(String.raw`║████${red('▌╠╠╠╠╫')}▓▓▓▓▓▓▓▌ ▓█ ╙██▄ ██ ██ ██ ██ ╫█▌ .██`);
37 | const line07 = whiteBright(String.raw` ▀█▓▓▓${red('▒╠╠╢')}▓▓▓▓▓▓▓▓= ▓█ ▀██ ██ ██▓▓▓██▀ ██ ╫█▌ .██▓▓▓▓▓▓`);
38 | const line08 = whiteBright(String.raw` ╜▓▓▓▓▓▓▓▓▓▓▓▓▓▀`);
39 | const line09 = whiteBright(String.raw` └╜▀▀▓▓▓▓▀▀╙`);
40 |
41 | // Offset Pad
42 | const pad = ' '.repeat(7);
43 | const longPad = ' '.repeat(26);
44 | const connectionPad = ' '.repeat(12);
45 |
46 | console.log(
47 | String.raw`
48 | ${line01} ${pad} ${bgRed(` ${VERSION} `)} ${DEV ? ` ${longPad}${bgRed(' > DEVELOPMENT MODE ')}` : ''}
49 | ${line02}
50 | ${line03}
51 | ${line04}
52 | ${line05}
53 | ${line06}
54 | ${line07}
55 | ${line08}
56 | ${line09} ${pad}[${success}] Gateway ${connectionPad}[${success}] Prisma ${connectionPad}[${TOKENS.SENTRY_TOKEN ? success : failure}] Sentry
57 | `.trim()
58 | );
59 | }
60 |
61 | private printStoreDebugInformation() {
62 | const { client, logger } = this.container;
63 | const stores = [...client.stores.values()];
64 | const last = stores.pop()!;
65 |
66 | for (const store of stores) logger.info(this.styleStore(store, false));
67 | logger.info(this.styleStore(last, true));
68 | }
69 |
70 | private styleStore(store: Store, last: boolean) {
71 | return gray(`${last ? '└─' : '├─'} Loaded ${this.style(store.size.toString().padEnd(3, ' '))} ${store.name}.`);
72 | }
73 |
74 | private async clientValidation() {
75 | const { client, logger, prisma } = this.container;
76 |
77 | logger.info('Starting Client validation...');
78 |
79 | // Update stats if client model exists, create db entry if not
80 | if (client.id) {
81 | const clientData = await prisma.clientSettings.findFirst();
82 | if (!clientData) await prisma.clientSettings.create({ data: { id: client.id } });
83 |
84 | const restarts = clientData?.restarts;
85 | restarts?.push(new Date(Date.now()));
86 | await prisma.clientSettings.update({ where: { id: client.id }, data: { restarts } })
87 | }
88 |
89 | logger.info('Client validated!');
90 | }
91 |
92 | private async guildValidation() {
93 | const { client, logger } = this.container;
94 |
95 | if (!CONTROL_GUILD) {
96 | logger.fatal('A control guild has not been set - shutting down...');
97 | return client.destroy();
98 | }
99 | if (!client.guilds.cache.has(CONTROL_GUILD)) {
100 | logger.fatal('RTByte has not been added to the configured control guild - shutting down...');
101 | return client.destroy();
102 | }
103 |
104 | logger.info('Starting guild validation...');
105 |
106 | for (const guildCollection of client.guilds.cache) {
107 | const guild = guildCollection[1];
108 | await initializeGuild(guild);
109 | }
110 |
111 | logger.info('All guilds validated!');
112 | }
113 |
114 | private async userValidation() {
115 | const { client, logger } = this.container;
116 |
117 | logger.info('Starting user validation...');
118 |
119 | for (const userCollection of client.users.cache) {
120 | const user = userCollection[1];
121 | await initializeUser(user);
122 | }
123 |
124 | logger.info('All users validated!');
125 | }
126 |
127 | private async memberValidation() {
128 | const { client, logger } = this.container;
129 |
130 | logger.info('Starting member validation...');
131 |
132 | for (const guildCollection of client.guilds.cache) {
133 | const guild = guildCollection[1];
134 |
135 | for (const memberCollection of guild.members.cache) {
136 | const member = memberCollection[1];
137 | await initializeMember(member.user, guild);
138 | }
139 | }
140 |
141 | logger.info('All members validated!');
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/routes/guilds/channels/channel.ts:
--------------------------------------------------------------------------------
1 | import { authenticated } from '#root/lib/util/decorators/routeAuthenticated';
2 | import { ApplyOptions } from '@sapphire/decorators';
3 | import { HttpCodes, Route, methods, type ApiRequest, type ApiResponse } from '@sapphire/plugin-api';
4 | import { PermissionsBitField } from 'discord.js';
5 |
6 | @ApplyOptions({
7 | name: 'guildChannel',
8 | route: 'guilds/:guild/channels/:channel'
9 | })
10 |
11 | export class UserRoute extends Route {
12 | public constructor(context: Route.Context, options: Route.Options) {
13 | super(context, {
14 | ...options
15 | });
16 | }
17 |
18 | @authenticated()
19 | public async [methods.GET](request: ApiRequest, response: ApiResponse) {
20 | const requestAuth = request.auth!
21 | const { client } = this.container;
22 |
23 | // Fetch the Guild this request is for
24 | const guild = await client.guilds.fetch(request.params.guild);
25 | if (!guild) return response.error(HttpCodes.NotFound);
26 |
27 | // Fetch the GuildMember who sent the request
28 | const member = await guild.members.fetch(requestAuth.id);
29 | const canManageServer: boolean = guild.ownerId === member.id || member.permissions.has(PermissionsBitField.Flags.ManageGuild) || member.permissions.has(PermissionsBitField.Flags.Administrator);
30 | if (!canManageServer) return response.error(HttpCodes.Unauthorized);
31 |
32 | // Grab channel from cache
33 | const channel = await guild.channels.fetch(request.params.channel);
34 | if (!channel) return response.error(HttpCodes.NotFound);
35 |
36 | // Return channel
37 | return response.json({ data: { channel } });
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/src/routes/guilds/channels/channels.ts:
--------------------------------------------------------------------------------
1 | import { authenticated } from '#root/lib/util/decorators/routeAuthenticated';
2 | import { ApplyOptions } from '@sapphire/decorators';
3 | import { HttpCodes, Route, methods, type ApiRequest, type ApiResponse } from '@sapphire/plugin-api';
4 | import { isNullishOrEmpty } from '@sapphire/utilities';
5 | import { PermissionsBitField } from 'discord.js';
6 |
7 | @ApplyOptions({
8 | name: 'guildChannels',
9 | route: 'guilds/:guildID/channels'
10 | })
11 |
12 | export class UserRoute extends Route {
13 | public constructor(context: Route.Context, options: Route.Options) {
14 | super(context, {
15 | ...options
16 | });
17 | }
18 |
19 | @authenticated()
20 | public async [methods.GET](request: ApiRequest, response: ApiResponse) {
21 | const requestAuth = request.auth!
22 | const { client } = this.container;
23 |
24 | // Fetch the Guild this request is for
25 | const guild = await client.guilds.fetch(request.params.guildID);
26 | if (!guild) return response.error(HttpCodes.NotFound);
27 |
28 | // Fetch the GuildMember who sent the request
29 | const member = await guild.members.fetch(requestAuth.id);
30 | const canManageServer: boolean = guild.ownerId === member.id || member.permissions.has(PermissionsBitField.Flags.ManageGuild) || member.permissions.has(PermissionsBitField.Flags.Administrator);
31 | if (!canManageServer) return response.error(HttpCodes.Unauthorized);
32 |
33 | // Start our channels collection
34 | let channels = guild.channels.cache;
35 |
36 | // Get query Params
37 | const queryParams = request.query;
38 | // If channel type specified, filter by it
39 | if (!isNullishOrEmpty(queryParams.type)) {
40 | const paramType: number = parseInt(queryParams.type as string, 10);
41 | channels = channels.filter((channel) => channel.type === paramType);
42 | }
43 | // If channel name specified, filter by it
44 | if (!isNullishOrEmpty(queryParams.name)) {
45 | const paramName: string = queryParams.name as string;
46 | channels = channels.filter((channel) => channel.name === paramName);
47 | }
48 | // If no channels are left after filtering, Error 404
49 | if (!channels.size) return response.error(HttpCodes.NotFound);
50 |
51 | // Return collection of channels
52 | return response.json({ data: { channels } });
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/src/routes/guilds/settings/guildSettingsInfoLogs.ts:
--------------------------------------------------------------------------------
1 | import { authenticated } from '#root/lib/util/decorators/routeAuthenticated';
2 | import { initializeGuild } from '#utils/functions/initialize';
3 | import type { GuildSettingsInfoLogs } from '@prisma/client';
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { HttpCodes, Route, methods, type ApiRequest, type ApiResponse } from '@sapphire/plugin-api';
6 | import { PermissionsBitField } from 'discord.js';
7 |
8 | @ApplyOptions({
9 | name: 'guildSettingsInfoLogs',
10 | route: 'guilds/:id/settings/info-logs'
11 | })
12 |
13 | export class UserRoute extends Route {
14 | public constructor(context: Route.Context, options: Route.Options) {
15 | super(context, {
16 | ...options
17 | });
18 | }
19 |
20 | @authenticated()
21 | public async [methods.GET](request: ApiRequest, response: ApiResponse) {
22 | const requestAuth = request.auth!
23 | const { client } = this.container;
24 |
25 | // Fetch the Guild this request is for
26 | const guild = await client.guilds.fetch(request.params.id);
27 | if (!guild) response.error(HttpCodes.NotFound);
28 |
29 | // Fetch the GuildMember who sent the request
30 | const member = await guild.members.fetch(requestAuth.id);
31 | const canManageServer: boolean = guild.ownerId === member.id || member.permissions.has(PermissionsBitField.Flags.ManageGuild) || member.permissions.has(PermissionsBitField.Flags.Administrator);
32 | if (!canManageServer) response.error(HttpCodes.Unauthorized);
33 |
34 | // Fetch the GuildSettingsInfoLogs from the database
35 | const { prisma } = this.container;
36 | let guildSettingsInfoLogs = await prisma.guildSettingsInfoLogs.findFirst({ where: { id: request.params.id } });
37 |
38 | // Initialize guild if it hasn't been initialized yet
39 | if (!guildSettingsInfoLogs) {
40 | await initializeGuild(guild);
41 | guildSettingsInfoLogs = await prisma.guildSettingsInfoLogs.findFirst({ where: { id: request.params.id } });
42 | }
43 |
44 | return response.json({ data: { guildSettingsInfoLogs } });
45 | }
46 |
47 | @authenticated()
48 | public async [methods.POST](request: ApiRequest, response: ApiResponse) {
49 | const requestAuth = request.auth!
50 | const { prisma, client } = this.container;
51 |
52 | // Fetch the Guild this request is for
53 | const guild = await client.guilds.fetch(request.params.id);
54 | if (!guild) response.error(HttpCodes.NotFound);
55 |
56 | // Fetch the GuildMember who sent the request
57 | const member = await guild.members.fetch(requestAuth.id);
58 | const canManageServer: boolean = guild.ownerId === member.id || member.permissions.has(PermissionsBitField.Flags.Administrator) || member.permissions.has(PermissionsBitField.Flags.ManageGuild);
59 | if (!canManageServer) response.error(HttpCodes.Unauthorized);
60 |
61 | // Get the settings submitted to us from the client
62 | const body = request.body as any;
63 | const submittedSettings: any = body.data.guildSettingsInfoLogs as object;
64 |
65 | // Bad Request if URL Params ID doesn't match submitted settings ID
66 | if (request.params.id !== submittedSettings.id) response.error(HttpCodes.BadRequest);
67 |
68 | // Fetch current guild settings
69 | let guildSettingsInfoLogs = await prisma.guildSettingsInfoLogs.findFirst({ where: { id: submittedSettings.id } });
70 |
71 | // Initialize guild if it hasn't been initialized yet
72 | if (!guildSettingsInfoLogs) {
73 | await initializeGuild(guild);
74 | guildSettingsInfoLogs = await prisma.guildSettingsInfoLogs.findFirst({ where: { id: submittedSettings.id } });
75 | }
76 |
77 | // Iterate through local settings, building a list of updated fields
78 | const updateSettings: any = {};
79 | for (const key in guildSettingsInfoLogs) {
80 | if (submittedSettings[key] !== guildSettingsInfoLogs[key as keyof GuildSettingsInfoLogs]) updateSettings[key] = submittedSettings[key];
81 | }
82 |
83 | // Update local settings in database
84 | const updatedSettings = await prisma.guildSettingsInfoLogs.update({ where: { id: submittedSettings.id }, data: updateSettings });
85 |
86 | // Send newly-updated settings back to client
87 | response.json({ data: { guildSettingsInfoLogs: updatedSettings } });
88 | }
89 | }
--------------------------------------------------------------------------------
/src/routes/guilds/settings/guildSettingsModActions.ts:
--------------------------------------------------------------------------------
1 | import { authenticated } from '#root/lib/util/decorators/routeAuthenticated';
2 | import { initializeGuild } from '#utils/functions/initialize';
3 | import type { GuildSettingsModActions } from '@prisma/client';
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { HttpCodes, Route, methods, type ApiRequest, type ApiResponse } from '@sapphire/plugin-api';
6 | import { PermissionsBitField } from 'discord.js';
7 |
8 | @ApplyOptions({
9 | name: 'guildSettingsModActions',
10 | route: 'guilds/:id/settings/mod-actions'
11 | })
12 |
13 | export class UserRoute extends Route {
14 | public constructor(context: Route.Context, options: Route.Options) {
15 | super(context, {
16 | ...options
17 | });
18 | }
19 |
20 | @authenticated()
21 | public async [methods.GET](request: ApiRequest, response: ApiResponse) {
22 | const requestAuth = request.auth!
23 | const { client } = this.container;
24 |
25 | // Fetch the Guild this request is for
26 | const guild = await client.guilds.fetch(request.params.id);
27 | if (!guild) response.error(HttpCodes.NotFound);
28 |
29 | // Fetch the GuildMember who sent the request
30 | const member = await guild.members.fetch(requestAuth.id);
31 | const canManageServer: boolean = guild.ownerId === member.id || member.permissions.has(PermissionsBitField.Flags.ManageGuild) || member.permissions.has(PermissionsBitField.Flags.Administrator);
32 | if (!canManageServer) response.error(HttpCodes.Unauthorized);
33 |
34 | // Fetch the GuildSettingsModActions from the database
35 | const { prisma } = this.container;
36 | let guildSettingsModActions = await prisma.guildSettingsModActions.findFirst({ where: { id: request.params.id } });
37 |
38 | // Initialize guild if it hasn't been initialized yet
39 | if (!guildSettingsModActions) {
40 | await initializeGuild(guild);
41 | guildSettingsModActions = await prisma.guildSettingsModActions.findFirst({ where: { id: request.params.id } });
42 | }
43 |
44 | return response.json({ data: { guildSettingsModActions } });
45 | }
46 |
47 | @authenticated()
48 | public async [methods.POST](request: ApiRequest, response: ApiResponse) {
49 | const requestAuth = request.auth!
50 | const { prisma, client } = this.container;
51 |
52 | // Fetch the Guild this request is for
53 | const guild = await client.guilds.fetch(request.params.id);
54 | if (!guild) response.error(HttpCodes.NotFound);
55 |
56 | // Fetch the GuildMember who sent the request
57 | const member = await guild.members.fetch(requestAuth.id);
58 | const canManageServer: boolean = guild.ownerId === member.id || member.permissions.has(PermissionsBitField.Flags.Administrator) || member.permissions.has(PermissionsBitField.Flags.ManageGuild);
59 | if (!canManageServer) response.error(HttpCodes.Unauthorized);
60 |
61 | // Get the settings submitted to us from the client
62 | const body = request.body as any;
63 | const submittedSettings: any = body.data.guildSettingsModActions as object;
64 |
65 | // Bad Request if URL Params ID doesn't match submitted settings ID
66 | if (request.params.id !== submittedSettings.id) response.error(HttpCodes.BadRequest);
67 |
68 | // Fetch current guild settings
69 | let guildSettingsModActions = await prisma.guildSettingsModActions.findFirst({ where: { id: submittedSettings.id } });
70 |
71 | // Initialize guild if it hasn't been initialized yet
72 | if (!guildSettingsModActions) {
73 | await initializeGuild(guild);
74 | guildSettingsModActions = await prisma.guildSettingsModActions.findFirst({ where: { id: submittedSettings.id } });
75 | }
76 |
77 | // Iterate through local settings, building a list of updated fields
78 | const updateSettings: any = {};
79 | for (const key in guildSettingsModActions) {
80 | if (submittedSettings[key] !== guildSettingsModActions[key as keyof GuildSettingsModActions]) updateSettings[key] = submittedSettings[key];
81 | }
82 |
83 | // Update local settings in database
84 | const updatedSettings = await prisma.guildSettingsModActions.update({ where: { id: submittedSettings.id }, data: updateSettings });
85 |
86 | // Send newly-updated settings back to client
87 | response.json({ data: { guildSettingsModActions: updatedSettings } });
88 | }
89 | }
--------------------------------------------------------------------------------
/src/routes/guilds/settings/settings.ts:
--------------------------------------------------------------------------------
1 | import { authenticated } from '#root/lib/util/decorators/routeAuthenticated';
2 | import { initializeGuild } from '#utils/functions/initialize';
3 | import type { GuildSettings } from '@prisma/client';
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { HttpCodes, Route, methods, type ApiRequest, type ApiResponse } from '@sapphire/plugin-api';
6 | import { PermissionsBitField } from 'discord.js';
7 |
8 | @ApplyOptions({
9 | name: 'guildSettings',
10 | route: 'guilds/:id/settings'
11 | })
12 |
13 | export class UserRoute extends Route {
14 | public constructor(context: Route.Context, options: Route.Options) {
15 | super(context, {
16 | ...options
17 | });
18 | }
19 |
20 | @authenticated()
21 | public async [methods.GET](request: ApiRequest, response: ApiResponse) {
22 | const requestAuth = request.auth!
23 | const { client } = this.container;
24 |
25 | // Fetch the Guild this request is for
26 | const guild = await client.guilds.fetch(request.params.id);
27 | if (!guild) response.error(HttpCodes.NotFound);
28 |
29 | // Fetch the GuildMember who sent the request
30 | const member = await guild.members.fetch(requestAuth.id);
31 | const canManageServer: boolean = guild.ownerId === member.id || member.permissions.has(PermissionsBitField.Flags.ManageGuild) || member.permissions.has(PermissionsBitField.Flags.Administrator);
32 | if (!canManageServer) response.error(HttpCodes.Unauthorized);
33 |
34 | // Fetch the GuildSettings from the database
35 | const { prisma } = this.container;
36 | let guildSettings = await prisma.guildSettings.findFirst({ where: { id: request.params.id } });
37 |
38 | // Initialize guild if it hasn't been initialized yet
39 | if (!guildSettings) {
40 | await initializeGuild(guild);
41 | guildSettings = await prisma.guildSettings.findFirst({ where: { id: request.params.id } });
42 | }
43 |
44 | return response.json({ data: { guildSettings } });
45 | }
46 |
47 | @authenticated()
48 | public async [methods.POST](request: ApiRequest, response: ApiResponse) {
49 | const requestAuth = request.auth!
50 | const { prisma, client } = this.container;
51 |
52 | // Fetch the Guild this request is for
53 | const guild = await client.guilds.fetch(request.params.id);
54 | if (!guild) response.error(HttpCodes.NotFound);
55 |
56 | // Fetch the GuildMember who sent the request
57 | const member = await guild.members.fetch(requestAuth.id);
58 | const canManageServer: boolean = guild.ownerId === member.id || member.permissions.has(PermissionsBitField.Flags.Administrator) || member.permissions.has(PermissionsBitField.Flags.ManageGuild);
59 | if (!canManageServer) response.error(HttpCodes.Unauthorized);
60 |
61 | // Get the settings submitted to us from the client
62 | const body = request.body as any;
63 | const submittedSettings: any = body.data.guildSettings as object;
64 |
65 | // Bad Request if URL Params ID doesn't match submitted settings ID
66 | if (request.params.id !== submittedSettings.id) response.error(HttpCodes.BadRequest);
67 |
68 | // Fetch current guild settings
69 | let guildSettings = await prisma.guildSettings.findFirst({ where: { id: submittedSettings.id } });
70 |
71 | // Initialize guild if it hasn't been initialized yet
72 | if (!guildSettings) {
73 | await initializeGuild(guild);
74 | guildSettings = await prisma.guildSettings.findFirst({ where: { id: submittedSettings.id } });
75 | }
76 |
77 | // Iterate through local settings, building a list of updated fields
78 | const updateSettings: any = {};
79 | for (const key in guildSettings) {
80 | if (submittedSettings[key] !== guildSettings[key as keyof GuildSettings]) updateSettings[key] = submittedSettings[key];
81 | }
82 |
83 | // Update local settings in database
84 | const updatedSettings = await prisma.guildSettings.update({ where: { id: submittedSettings.id }, data: updateSettings });
85 |
86 | // Send newly-updated settings back to client
87 | response.json({ data: { guildSettings: updatedSettings } });
88 | }
89 | }
--------------------------------------------------------------------------------
/src/routes/oauth/refresh.ts:
--------------------------------------------------------------------------------
1 | import { ApplyOptions } from '@sapphire/decorators';
2 | import { fetch, FetchResultTypes } from '@sapphire/fetch';
3 | import { HttpCodes, methods, MimeTypes, Route, type ApiRequest, type ApiResponse } from '@sapphire/plugin-api';
4 | import { Time } from '@sapphire/time-utilities';
5 | import { OAuth2Routes, type RESTPostOAuth2AccessTokenResult } from 'discord.js';
6 | import { stringify } from 'node:querystring';
7 |
8 | @ApplyOptions({
9 | name: 'oauth/refresh',
10 | route: 'oauth/refresh'
11 | })
12 |
13 | export class RefreshRoute extends Route {
14 | public constructor(context: Route.Context, options: Route.Options) {
15 | super(context, {
16 | ...options
17 | });
18 | }
19 |
20 | public async [methods.POST](request: ApiRequest, response: ApiResponse) {
21 | if (!request.auth) return response.error(HttpCodes.Unauthorized);
22 |
23 | const requestAuth = request.auth;
24 | const serverAuth = this.container.server.auth!;
25 |
26 | let authToken = requestAuth.token;
27 |
28 | // If the token expires in a day, refresh
29 | if (Date.now() + Time.Day >= requestAuth.expires) {
30 | const body = await this.refreshToken(requestAuth.id, requestAuth.refresh);
31 | if (body !== null) {
32 | const authentication = serverAuth.encrypt({
33 | id: requestAuth.id,
34 | token: body.access_token,
35 | refresh: body.refresh_token,
36 | expires: Date.now() + body.expires_in * 1000
37 | });
38 |
39 | response.cookies.add(serverAuth.cookie, authentication, { maxAge: body.expires_in });
40 | authToken = body.access_token;
41 | }
42 | }
43 |
44 | // Refresh the user's data
45 | try {
46 | return response.json(await serverAuth.fetchData(authToken));
47 | } catch (error) {
48 | this.container.logger.fatal(error);
49 | return response.error(HttpCodes.InternalServerError);
50 | }
51 | }
52 |
53 | private async refreshToken(id: string, refreshToken: string): Promise {
54 | const { logger, server } = this.container;
55 | try {
56 | logger.debug(`Refreshing Token for ${id}`);
57 | return await fetch(
58 | OAuth2Routes.tokenURL,
59 | {
60 | method: 'POST',
61 | body: stringify({
62 | client_id: server.auth!.id,
63 | client_secret: server.auth!.secret,
64 | grant_type: 'refresh_token',
65 | refresh_token: refreshToken,
66 | redirect_uri: server.auth!.redirect,
67 | scope: server.auth!.scopes
68 | }),
69 | headers: {
70 | 'Content-Type': MimeTypes.ApplicationFormUrlEncoded
71 | }
72 | },
73 | FetchResultTypes.JSON
74 | );
75 | } catch (error) {
76 | logger.fatal(error);
77 | return null;
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/src/routes/users/settings.ts:
--------------------------------------------------------------------------------
1 | import { authenticated } from '#root/lib/util/decorators/routeAuthenticated';
2 | import { initializeUser } from '#utils/functions/initialize';
3 | import type { UserSettings } from '@prisma/client';
4 | import { ApplyOptions } from '@sapphire/decorators';
5 | import { HttpCodes, Route, methods, type ApiRequest, type ApiResponse } from '@sapphire/plugin-api';
6 |
7 | @ApplyOptions({
8 | name: 'userSettings',
9 | route: 'users/:id/settings'
10 | })
11 |
12 | export class UserRoute extends Route {
13 | public constructor(context: Route.Context, options: Route.Options) {
14 | super(context, {
15 | ...options
16 | });
17 | }
18 |
19 | @authenticated()
20 | public async [methods.GET](request: ApiRequest, response: ApiResponse) {
21 | const requestAuth = request.auth!
22 | // Unauthorized for fetching any UserSettings except your own
23 | if (requestAuth.id !== request.params.id) return response.error(HttpCodes.Unauthorized);
24 |
25 | // Fetch current user settings
26 | const { prisma, client } = this.container;
27 | let userSettings = await prisma.userSettings.findFirst({ where: { id: request.params.id } });
28 |
29 | if (!userSettings) {
30 | await initializeUser(await client.users.fetch(requestAuth.id), requestAuth.id);
31 | userSettings = await prisma.userSettings.findFirst({ where: { id: request.params.id } });
32 | }
33 |
34 | return response.json({ data: { userSettings } });
35 | }
36 |
37 | @authenticated()
38 | public async [methods.POST](request: ApiRequest, response: ApiResponse) {
39 | const requestAuth = request.auth!
40 | // Unauthorized for fetching any UserSettings except your own
41 | if (requestAuth.id !== request.params.id) return response.error(HttpCodes.Unauthorized);
42 |
43 | // Get the settings submitted to us from the client
44 | const body = request.body as any;
45 | const submittedSettings: any = body.data.userSettings as object;
46 |
47 | // Bad Request if URL Params ID doesn't match submitted settings ID
48 | if (request.params.id !== submittedSettings.id) response.error(HttpCodes.BadRequest);
49 |
50 | // Fetch current user settings
51 | const { prisma, client } = this.container;
52 | let userSettings = await prisma.userSettings.findFirst({ where: { id: submittedSettings.id } });
53 |
54 | if (!userSettings) {
55 | await initializeUser(await client.users.fetch(requestAuth.id), requestAuth.id);
56 | userSettings = await prisma.userSettings.findFirst({ where: { id: submittedSettings.id } });
57 | }
58 |
59 | const updateSettings: any = {};
60 | for (const key in userSettings) {
61 | if (submittedSettings[key] !== userSettings[key as keyof UserSettings]) updateSettings[key] = submittedSettings[key];
62 | }
63 |
64 | const updatedSettings = await prisma.userSettings.update({ where: { id: submittedSettings.id }, data: updateSettings });
65 |
66 | response.json({ data: { userSettings: updatedSettings } });
67 | }
68 | }
--------------------------------------------------------------------------------
/src/transformers/loginDataGuilds.ts:
--------------------------------------------------------------------------------
1 | import { container } from '@sapphire/framework';
2 | import type { LoginData } from '@sapphire/plugin-api';
3 | import { type RESTAPIPartialCurrentUserGuild } from 'discord.js';
4 |
5 | interface TransformedLoginData extends LoginData {
6 | transformedGuilds?: (RESTAPIPartialCurrentUserGuild & { botInGuild: boolean })[] | null;
7 | }
8 |
9 | export function transformLoginDataGuilds(loginData: LoginData): TransformedLoginData {
10 | const { client } = container;
11 |
12 | const transformedGuilds = loginData.guilds?.map((guild) => {
13 | const cachedGuild = client.guilds.cache.get(guild.id);
14 | const canManageServer: boolean = guild.owner || ((parseInt(guild.permissions, 10) & 0x20) !== 0) || ((parseInt(guild.permissions, 10) & 0x8) !== 0);
15 |
16 | return {
17 | ...guild,
18 | botInGuild: typeof cachedGuild !== 'undefined',
19 | canManageServer
20 | };
21 | });
22 |
23 | loginData.guilds = transformedGuilds;
24 | return { ...loginData };
25 | }
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../dist",
5 | "rootDir": ".",
6 | "baseUrl": ".",
7 | "tsBuildInfoFile": "../dist/.tsbuildinfo",
8 | "ignoreDeprecations": "5.0",
9 | "experimentalDecorators": true,
10 | "moduleResolution": "Node16",
11 | "paths": {
12 | "#root/*": [
13 | "*"
14 | ],
15 | "#lib/*": [
16 | "lib/*"
17 | ],
18 | "#utils/*": [
19 | "lib/util/*"
20 | ]
21 | },
22 | "composite": true,
23 | },
24 | "include": [
25 | ".",
26 | "**/*.json"
27 | ],
28 | "exclude": [
29 | "tsconfig.json"
30 | ]
31 | }
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sapphire/ts-config",
3 | "compilerOptions": {
4 | "sourceMap": true,
5 | "outDir": "dist",
6 | "strict": true,
7 | "lib": ["esnext"],
8 | "esModuleInterop": true
9 | }
10 | }
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------