├── .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 | ![RTByte logo](https://rtbyte.xyz/android-chrome-192x192.png) 4 | 5 | # RTByte 6 | 7 | [![GitHub package.json version](https://img.shields.io/github/package-json/v/rtbyte/rtbyte)](https://github.com/RTByte/rtbyte/releases) 8 | [![GitHub](https://img.shields.io/github/license/rtbyte/rtbyte)](https://github.com/rtbyte/rtbyte/blob/main/LICENSE.md) 9 | 10 | [![Open Issues](https://img.shields.io/github/issues/RTByte/RTByte.svg)](https://github.com/RTByte/RTByte/issues) 11 | [![Open PRS](https://img.shields.io/github/issues-pr/RTByte/RTByte.svg)](https://github.com/RTByte/RTByte/pulls) 12 | [![Github All Contributors](https://img.shields.io/github/all-contributors/rtbyte/rtbyte)](https://github.com/RTByte/rtbyte#contributors-) 13 | [![Crowdin](https://badges.crowdin.net/rtbyte/localized.svg)](https://translate.rtbyte.xyz) 14 | 15 | [![Discord](https://img.shields.io/discord/450163430373064704.svg?colorB=7289da&label=discord&logo=Discord&logoColor=fff&style=flat)](https://rtbyte.xyz/discord) 16 | [![Twitter](https://badgen.net/twitter/follow/rtbyte/?icon=twitter&label=@rtbyte)](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 | --------------------------------------------------------------------------------