├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.local ├── LICENSE ├── README.md ├── THIRD-PARTY-LICENSE ├── crowdin.yml ├── deno.json ├── deno.lock ├── drizzle.config.ts ├── fly.toml ├── litefs.yml └── src ├── api └── mod.ts ├── arguments └── commandName.ts ├── auto ├── etf2l.ts ├── etf2lMatches.ts ├── logs.ts ├── rgl.ts ├── steam-connect-fromlink.ts ├── steam-connect.ts └── tftv.ts ├── commands ├── Admin │ ├── language.ts │ ├── prefix.ts │ └── webhooks.ts ├── General │ ├── commands.ts │ ├── help.ts │ ├── info.ts │ └── profile.ts ├── Tf2 │ ├── link.ts │ ├── log.ts │ └── pushcart.ts └── User │ └── webhook.ts ├── config.ts ├── drizzle ├── 0000_far_redwing.sql ├── 0001_rename-languages.sql ├── 0002_normalize-timestamps.sql ├── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ ├── 0002_snapshot.json │ └── _journal.json └── schema.ts ├── index.ts ├── interaction-handlers ├── createUserWebhook.ts ├── deleteUserWebhook.ts └── setGuildLanguage.ts ├── languages ├── de │ ├── arguments.json │ ├── auto │ │ ├── connect.json │ │ ├── etf2l.json │ │ ├── logs.json │ │ ├── rgl.json │ │ ├── tftv.json │ │ └── ugc.json │ ├── commands │ │ ├── 8ball.json │ │ ├── avatar.json │ │ ├── bruh.json │ │ ├── changelog.json │ │ ├── choose.json │ │ ├── commands.json │ │ ├── dashboard.json │ │ ├── findping.json │ │ ├── help.json │ │ ├── info.json │ │ ├── invite.json │ │ ├── language.json │ │ ├── link.json │ │ ├── log.json │ │ ├── prefix.json │ │ ├── profile.json │ │ ├── purge.json │ │ ├── pushcart.json │ │ ├── restrict.json │ │ ├── rtd.json │ │ ├── settings.json │ │ ├── snipe.json │ │ ├── translate.json │ │ ├── unrestrict.json │ │ └── webhook.json │ ├── globals.json │ ├── preconditions.json │ └── system.json ├── en-US │ ├── arguments.json │ ├── auto │ │ ├── connect.json │ │ ├── etf2l.json │ │ ├── logs.json │ │ ├── rgl.json │ │ ├── tftv.json │ │ └── ugc.json │ ├── commands │ │ ├── commands.json │ │ ├── help.json │ │ ├── info.json │ │ ├── invite.json │ │ ├── language.json │ │ ├── link.json │ │ ├── log.json │ │ ├── prefix.json │ │ ├── profile.json │ │ ├── pushcart.json │ │ ├── webhook.json │ │ └── webhooks.json │ ├── globals.json │ ├── preconditions.json │ └── system.json ├── es-ES │ ├── commands │ │ ├── 8ball.json │ │ ├── bruh.json │ │ ├── changelog.json │ │ ├── choose.json │ │ ├── commands.json │ │ ├── dashboard.json │ │ ├── findping.json │ │ ├── info.json │ │ ├── link.json │ │ ├── log.json │ │ ├── prefix.json │ │ ├── profile.json │ │ ├── purge.json │ │ ├── pushcart.json │ │ ├── restrict.json │ │ ├── settings.json │ │ ├── snipe.json │ │ ├── translate.json │ │ └── unrestrict.json │ └── system.json ├── fi │ └── commands │ │ └── bruh.json ├── fr │ └── commands │ │ ├── 8ball.json │ │ └── bruh.json ├── index.ts ├── pl │ └── commands │ │ └── bruh.json └── ru │ ├── auto │ ├── connect.json │ ├── etf2l.json │ ├── logs.json │ ├── rgl.json │ ├── tftv.json │ └── ugc.json │ └── commands │ ├── 8ball.json │ ├── avatar.json │ ├── bruh.json │ ├── changelog.json │ ├── choose.json │ ├── commands.json │ ├── findping.json │ ├── help.json │ ├── info.json │ ├── invite.json │ ├── link.json │ ├── log.json │ ├── prefix.json │ ├── profile.json │ ├── purge.json │ └── pushcart.json ├── lib ├── PayloadClient.ts ├── i18n │ ├── CommandHelper.ts │ ├── all │ │ ├── index.ts │ │ └── keys │ │ │ ├── all.ts │ │ │ ├── arguments.ts │ │ │ ├── auto │ │ │ ├── connect.ts │ │ │ ├── etf2l.ts │ │ │ ├── index.ts │ │ │ ├── logs.ts │ │ │ ├── rgl.ts │ │ │ ├── tftv.ts │ │ │ └── ugc.ts │ │ │ ├── commands │ │ │ ├── commands.ts │ │ │ ├── help.ts │ │ │ ├── index.ts │ │ │ ├── info.ts │ │ │ ├── invite.ts │ │ │ ├── language.ts │ │ │ ├── link.ts │ │ │ ├── log.ts │ │ │ ├── prefix.ts │ │ │ ├── profile.ts │ │ │ ├── pushcart.ts │ │ │ ├── settings.ts │ │ │ ├── webhook.ts │ │ │ └── webhooks.ts │ │ │ ├── globals.ts │ │ │ ├── preconditions.ts │ │ │ └── system.ts │ └── mapping.ts ├── structs │ ├── AutoResponse │ │ ├── AutoResponse.ts │ │ └── AutoResponseStore.ts │ ├── ServiceController.ts │ └── commands │ │ ├── PayloadArgs.ts │ │ └── PayloadCommand.ts ├── types │ ├── Augments.d.ts │ ├── index.ts │ └── utils.ts └── utils │ ├── clientOptions.ts │ ├── colors.ts │ ├── database.ts │ ├── getSteamId.ts │ ├── random.ts │ ├── setup.ts │ └── webhook-helper.ts ├── listeners ├── commands │ ├── messageCommandDenied.ts │ └── messageCommandError.ts ├── errors │ ├── error.ts │ └── rateLimit.ts ├── mentionPrefixOnly.ts ├── messages │ ├── messageCreate.ts │ └── messageDelete.ts ├── misc │ └── debug.ts └── ready.ts └── preconditions └── OwnerOnly.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | ** 2 | *.db 3 | *.db-* 4 | 5 | # Folders 6 | !src/** 7 | 8 | # Files 9 | !deno.json 10 | !deno.lock 11 | !litefs.yml 12 | !fly.toml 13 | !package.json -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: c43721 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 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. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | 9 | jobs: 10 | docker: 11 | env: 12 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 13 | CI: true 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v1 23 | 24 | - name: Docker Meta 25 | id: meta 26 | uses: docker/metadata-action@v3 27 | with: 28 | images: ghcr.io/${{ github.repository }} 29 | tags: | 30 | type=raw,value=edge,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} 31 | 32 | - name: Login to GitHub Container Registry 33 | uses: docker/login-action@v1 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Build and Push 40 | uses: docker/build-push-action@v2 41 | with: 42 | context: . 43 | push: ${{ github.event_name != 'pull_request' }} 44 | tags: ${{ steps.meta.outputs.tags }} 45 | labels: ${{ steps.meta.outputs.labels }} 46 | cache-from: type=gha 47 | cache-to: type=gha,mode=max 48 | build-args: | 49 | BUILT_AT=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} 50 | VERSION=${{ github.sha }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Code 2 | /dist 3 | /node_modules 4 | 5 | # Stores and ENV 6 | .env* 7 | *.db 8 | *.db-* 9 | 10 | # IDE - VSCode 11 | .vscode/* 12 | !.vscode/settings.json 13 | !.vscode/tasks.json 14 | !.vscode/launch.json 15 | !.vscode/extensions.json 16 | 17 | # OS 18 | .DS_Store -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "request": "launch", 6 | "name": "Debug Payload", 7 | "type": "node", 8 | "program": "${workspaceFolder}/src/index.ts", 9 | "cwd": "${workspaceFolder}", 10 | "env": {}, 11 | "runtimeExecutable": "deno", 12 | "runtimeArgs": [ 13 | "run", 14 | "--inspect-wait", 15 | "--allow-all" 16 | ], 17 | "attachSimplePort": 9229 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | Contributions and feedback on your experience of using this software are welcome. 4 | 5 | This includes bug reports, feature requests, ideas, pull requests, and examples of how you have used this software. 6 | 7 | Please raise any significant new functionality or breaking changes using GitHub issues or Discussions before making your Pull Request. 8 | 9 | ## For contributors 10 | 11 | Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to submit a Pull Request. 12 | 13 | ## Pull Requests 14 | 15 | - The latest changes are always in `main`, so please target `main` when you want to make a change. 16 | - Pull Requests need approval of a member of the Payload organization. 17 | - We use prettier for linting, and there is a `.prettierrc` file to help standardize formats. 18 | - We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request. 19 | 20 | ## Setting up local environment 21 | 22 | Quick start: 23 | 24 | 1. Clone the repo 25 | 26 | 2. Install packages using `yarn`: 27 | 28 | ```sh 29 | yarn 30 | ``` 31 | 32 | 3. Populate `.env`: 33 | 34 | Copy `.env.example` to `.env`, and add your env variables for each provider you want to test. 35 | 36 | > NOTE: You must have a working Postgres database to connect to. 37 | 38 | 1. Start the dev application/server: 39 | 40 | ```sh 41 | yarn dev 42 | ``` 43 | 44 | The bot application will be available on `http://localhost:8080` 45 | 46 | That's it! 🎉 47 | 48 | ### Testing 49 | 50 | Starting out, tests will be scarce. We intend to add tests for most major API endpoints and for integration testing. 51 | 52 | ## For maintainers 53 | 54 | For versioning, please study the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) site to understand how to write a good commit message. We also use semver for versioning, but this will not be mirrored in the package.json (as it would break the cache!) 55 | 56 | When accepting Pull Requests, make sure the following: 57 | 58 | - Make sure you merge contributor PRs into `dev`. 59 | - Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice. 60 | - Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged). 61 | 62 | ## Recommended Scopes 63 | 64 | A typical conventional commit looks like this: 65 | 66 | ``` 67 | type(scope): title 68 | 69 | body 70 | ``` 71 | 72 | Scope is the part that will help grouping the different commit types in the release notes. 73 | 74 | Some recommended scopes are: 75 | 76 | - **docs** - Documentation or github-related commits (eg.: "feat(docs): Add CONTRIBUTING", "chore(docs): Fix README" 77 | - **deps** - Adding/removing/updating a dependency (eg.: "chore(deps): add X") 78 | 79 | This is not an exhaustive list, so if you feel a commit is suited for a specific scope, feel free to add that scope. 80 | 81 | > NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). We strongly suggest against leaving out the scope however. 82 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM denoland/deno:alpine-2.3.5 AS build 2 | 3 | USER deno 4 | 5 | WORKDIR /app 6 | 7 | COPY . . 8 | 9 | RUN deno cache --frozen src/index.ts 10 | 11 | FROM denoland/deno:alpine-2.3.5 12 | 13 | ENV FLY="true" 14 | ENV LITEFS_DIR="/litefs/data" 15 | ENV DATABASE_FILENAME="sqlite.db" 16 | ENV DATABASE_PATH="$LITEFS_DIR/$DATABASE_FILENAME" 17 | ENV DATABASE_URL="file:$DATABASE_PATH" 18 | ENV NODE_ENV="production" 19 | ENV PORT=3000 20 | ENV HOST="0.0.0.0" 21 | ENV PREVIEW_URL="http://payload-screenshot.flycast" 22 | ENV DENO_NO_UPDATE_CHECK=1 23 | ENV DENO_NO_PROMPT=1 24 | 25 | RUN apk add ca-certificates fuse3 sqlite 26 | 27 | # prepare for litefs 28 | COPY --from=flyio/litefs:0.5.0 /usr/local/bin/litefs /usr/local/bin/litefs 29 | ADD litefs.yml /etc/litefs.yml 30 | RUN mkdir -p /data ${LITEFS_DIR} 31 | 32 | # add shortcut for connecting to database CLI 33 | RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/db && chmod +x /usr/local/bin/db 34 | 35 | WORKDIR /app 36 | 37 | COPY --from=build $DENO_DIR $DENO_DIR 38 | COPY --from=build /app . 39 | 40 | ENTRYPOINT ["litefs", "mount"] -------------------------------------------------------------------------------- /Dockerfile.local: -------------------------------------------------------------------------------- 1 | FROM denoland/deno:alpine-2.3.3 AS build 2 | 3 | WORKDIR /app 4 | 5 | USER deno 6 | 7 | COPY . . 8 | 9 | RUN deno cache --frozen src/index.ts 10 | 11 | FROM denoland/deno:alpine-2.3.3 12 | 13 | WORKDIR /app 14 | 15 | ENV DATA_DIR="/data" 16 | ENV DATABASE_FILENAME="data.db" 17 | ENV DATABASE_PATH="$DATA_DIR/$DATABASE_FILENAME" 18 | ENV DATABASE_URL="file:$DATABASE_PATH" 19 | ENV NODE_ENV="production" 20 | ENV PORT=3000 21 | ENV HOST="0.0.0.0" 22 | ENV PREVIEW_URL="http://localhost:8000" 23 | ENV DENO_NO_UPDATE_CHECK=1 24 | ENV DENO_NO_PROMPT=1 25 | 26 | # add shortcut for connecting to database CLI 27 | RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/db && chmod +x /usr/local/bin/db 28 | 29 | USER deno 30 | 31 | COPY --from=build $DENO_DIR $DENO_DIR 32 | COPY --from=build /app . 33 | 34 | VOLUME [ "/data" ] 35 | 36 | ENTRYPOINT ["deno", "task", "start"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 c43721 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 | payload.tf logo 4 | 5 |

6 | 7 |

payload-neo

8 | 9 |

Payload Discord Bot

10 | 11 |

12 | MIT license 13 |

14 | 15 | # Description 16 | 17 | Payload is the Discord bot for TF2 players. Features a leaderboard, logs.tf and 18 | team preview screenshots, and other useful commands. 19 | 20 | ## Running 21 | 22 | You can run the service locally using [Deno](https://docs.deno.com/runtime/): 23 | 24 | ```cmd 25 | deno task dev 26 | ``` 27 | 28 | This will install all dependencies. 29 | 30 | ### Production 31 | 32 | This service is ran using Docker. To run locally, first build the service: 33 | 34 | ```cmd 35 | docker build -f Dockerfile.local . -t "payload-neo" 36 | ``` 37 | 38 | Then run the container: 39 | 40 | ```cmd 41 | docker run --name "payload" --init -it --rm -v ./data.db:/data/data.db --env-file=.env.docker payload-neo 42 | ``` 43 | 44 | If you're using windows, use an explicit path for the volume: 45 | 46 | ```cmd 47 | docker run --name "payload" --init -it --rm -v ${PWD}/data.db:/data/data.db --env-file=.env.docker payload-neo 48 | ``` 49 | 50 | # Issues, Questions 51 | 52 | Any issues or questions should be posted on GitHub issues, where they can be 53 | more easily tracked. Feature requests are welcome! 54 | 55 | # Support this Project 56 | 57 | You may back me on my [Patreon](https://www.patreon.com/c43721). Direct 58 | sponsorship of this project can be discussed on Discord (24#7644) or by another 59 | medium. 60 | 61 | # Contributing 62 | 63 | Before contributing, please make sure no one else has stated against your 64 | proposal. Otherwise, make a Pull Request detailing your proposal and any 65 | relevant code changes. 66 | 67 | # Useful Links 68 | 69 | - [Main Page](https://payload.tf/) 70 | - [Invite](https://payload.tf/invite) 71 | - [Discord](https://payload.tf/discord) 72 | - [Translation](https://crowdin.com/project/payload) 73 | 74 | # License 75 | 76 | This project is [MIT licensed](LICENSE). 77 | -------------------------------------------------------------------------------- /THIRD-PARTY-LICENSE: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Skyra Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | project_id: "395009" 2 | api_token_env: CROWDIN_PERSONAL_TOKEN 3 | commit_message: "chore(i18n): translations for %original_file_name% (%language%)" 4 | pull_request_title: "chore(i18n): update localization files" 5 | append_commit_message: false 6 | files: 7 | - source: /src/languages/en-US/**/*.json 8 | translation: /src/languages/%locale%/**/%original_file_name% 9 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "unstable": ["temporal"], 4 | "tasks": { 5 | "start": "deno run -NRES --allow-ffi src/index.ts", 6 | "dev": "deno run -A --env-file=.env src/index.ts" 7 | }, 8 | "imports": { 9 | "@discordjs/builders": "npm:@discordjs/builders@^1.11.1", 10 | "@libsql/client": "npm:@libsql/client@^0.15.4", 11 | "@node-steam/id": "npm:@node-steam/id@^1.2.0", 12 | "@sapphire/decorators": "npm:@sapphire/decorators@^6.1.1", 13 | "@sapphire/discord.js-utilities": "npm:@sapphire/discord.js-utilities@^7.3.1", 14 | "@sapphire/fetch": "npm:@sapphire/fetch@^3.0.5", 15 | "@sapphire/framework": "npm:@sapphire/framework@^5.3.4", 16 | "@sapphire/lexure": "npm:@sapphire/lexure@^1.1.10", 17 | "@sapphire/pieces": "npm:@sapphire/pieces@^4.3.2", 18 | "@sapphire/plugin-editable-commands": "npm:@sapphire/plugin-editable-commands@^4.0.3", 19 | "@sapphire/plugin-hmr": "npm:@sapphire/plugin-hmr@^3.0.2", 20 | "@sapphire/plugin-i18next": "npm:@sapphire/plugin-i18next@^7.1.3", 21 | "@sapphire/plugin-logger": "npm:@sapphire/plugin-logger@^4.0.2", 22 | "@sapphire/plugin-subcommands": "npm:@sapphire/plugin-subcommands@^7.0.1", 23 | "@sapphire/ratelimits": "npm:@sapphire/ratelimits@^2.4.11", 24 | "@sapphire/time-utilities": "npm:@sapphire/time-utilities@^1.7.14", 25 | "@sapphire/utilities": "npm:@sapphire/utilities@^3.18.1", 26 | "@skyra/env-utilities": "npm:@skyra/env-utilities@^2.0.0", 27 | "@std/fmt": "jsr:@std/fmt@^1.0.7", 28 | "@tf2software/logstf": "jsr:@tf2software/logstf@^0.0.4", 29 | "arktype": "npm:arktype@^2.1.19", 30 | "cheerio": "npm:cheerio@^1.0.0", 31 | "discord-api-types": "npm:discord-api-types@0.38.1", 32 | "discord.js": "npm:discord.js@^14.19.1", 33 | "drizzle-orm": "npm:drizzle-orm@^0.43.1", 34 | "gamedig": "npm:gamedig@^5.3.0", 35 | "generate-password": "npm:generate-password@^1.7.1", 36 | "html-to-text": "npm:html-to-text@^9.0.5", 37 | "i18next": "npm:i18next@^25.0.1", 38 | "reflect-metadata": "npm:reflect-metadata@^0.2.2", 39 | "tslib": "npm:tslib@^2.8.1", 40 | 41 | "@types/html-to-text": "npm:@types/html-to-text@^9.0.4", 42 | "drizzle-kit": "npm:drizzle-kit@^0.31.0", 43 | 44 | "#root/": "./src/", 45 | "#api/": "./src/lib/api/", 46 | "#lib/": "./src/lib/", 47 | "#lib/utils/": "./src/lib/utils/", 48 | "#lib/types": "./src/lib/types/index.ts", 49 | "#lib/models": "./src/lib/models/index.ts", 50 | "#lib/i18n/all": "./src/lib/i18n/all/index.ts", 51 | "#utils/": "./src/lib/utils/" 52 | }, 53 | "compilerOptions": { 54 | "emitDecoratorMetadata": true, 55 | "experimentalDecorators": true 56 | }, 57 | "fmt": { 58 | "lineWidth": 240 59 | }, 60 | "lint": { 61 | "exclude": ["src/drizzle/*", "src/languages/*"] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | export default { 4 | dialect: "sqlite", 5 | out: "./src/drizzle", 6 | schema: "./src/drizzle/schema.ts", 7 | dbCredentials: { 8 | url: Deno.env.get("DATABASE_PATH")!, 9 | }, 10 | verbose: true, 11 | strict: true, 12 | } satisfies Config; 13 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | app = "payload" 2 | primary_region = "ord" 3 | 4 | [[mounts]] 5 | source = "data" 6 | destination = "/data" 7 | 8 | [[services]] 9 | protocol = "tcp" 10 | internal_port = 3000 11 | processes = ["app"] 12 | 13 | [[services.ports]] 14 | port = 80 15 | handlers = ["http"] 16 | force_https = true 17 | 18 | [[services.ports]] 19 | port = 443 20 | handlers = ["tls", "http"] 21 | 22 | [[services.tcp_checks]] 23 | interval = "15s" 24 | timeout = "2s" 25 | grace_period = "1s" 26 | restart_limit = 0 27 | -------------------------------------------------------------------------------- /litefs.yml: -------------------------------------------------------------------------------- 1 | fuse: 2 | dir: "${LITEFS_DIR}" 3 | 4 | data: 5 | dir: "/data/litefs" 6 | 7 | exec: 8 | - cmd: "deno task start" 9 | 10 | proxy: 11 | addr: ":${INTERNAL_PORT}" 12 | target: "localhost:${PORT}" 13 | db: "${DATABASE_FILENAME}" 14 | 15 | lease: 16 | type: "consul" 17 | candidate: ${FLY_REGION == PRIMARY_REGION} 18 | promote: true 19 | advertise-url: "http://${HOSTNAME}.vm.${FLY_APP_NAME}.internal:20202" 20 | 21 | consul: 22 | url: "${FLY_CONSUL_URL}" 23 | key: "litefs/${FLY_APP_NAME}" 24 | -------------------------------------------------------------------------------- /src/api/mod.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | import { isNullish, isNullOrUndefinedOrEmpty } from "@sapphire/utilities"; 3 | import { sendLogPreview } from "#utils/webhook-helper.ts"; 4 | import { container } from "@sapphire/pieces"; 5 | import { webhook } from "#root/drizzle/schema.ts"; 6 | import { eq } from "drizzle-orm"; 7 | import { envParseInteger, envParseString } from "@skyra/env-utilities"; 8 | 9 | const steamRoute = new URLPattern({ pathname: "/api/steam" }); 10 | const webhookRenderUrlOld = new URLPattern({ 11 | pathname: "/api/v1/webhooks/logs", 12 | }); 13 | const webhookRenderUrl = new URLPattern({ pathname: "/api/webhooks/logs" }); 14 | 15 | const logSchema = type({ 16 | ip: "string", 17 | pw: "string?", 18 | }); 19 | 20 | const webhookSchema = type({ 21 | demosId: "string | number.integer >= 0", 22 | logsId: "string | number.integer >= 0", 23 | }); 24 | 25 | const hostname = envParseString("HOST"); 26 | const port = envParseInteger("PORT"); 27 | 28 | export function serve() { 29 | return Deno.serve({ hostname, port }, async (req) => { 30 | const url = new URL(req.url); 31 | 32 | if (req.method === "GET" && steamRoute.test(req.url)) { 33 | const result = logSchema(Object.fromEntries(url.searchParams)); 34 | 35 | if (result instanceof type.errors) { 36 | return Response.json({ 37 | error: "Bad request", 38 | message: result.summary, 39 | }); 40 | } 41 | 42 | return Response.redirect( 43 | `steam://connect/${result.ip}${ 44 | result.pw != null ? `/${result.pw}` : "" 45 | }`, 46 | 302, 47 | ); 48 | } 49 | 50 | if ( 51 | req.method === "POST" && 52 | (webhookRenderUrl.test(req.url) || webhookRenderUrlOld.test(req.url)) 53 | ) { 54 | const headerAuth = req.headers.get("Authorization"); 55 | 56 | if (isNullish(headerAuth)) { 57 | return Response.json({ message: "Unauthorized" }, { status: 401 }); 58 | } 59 | 60 | const data = await container.database 61 | .select({ type: webhook.type, id: webhook.id }) 62 | .from(webhook) 63 | .where(eq(webhook.value, headerAuth)); 64 | 65 | if (isNullOrUndefinedOrEmpty(data)) { 66 | return Response.json({ message: "Not found" }, { status: 404 }); 67 | } 68 | 69 | const body = await req.json(); 70 | const result = webhookSchema(body); 71 | 72 | if (result instanceof type.errors) { 73 | return Response.json({ 74 | error: "Bad request", 75 | message: result.summary, 76 | }); 77 | } 78 | 79 | await sendLogPreview(container.client, { 80 | demosId: result.demosId.toString(), 81 | logsId: result.logsId.toString(), 82 | targetId: data[0].id, 83 | // deno-lint-ignore no-explicit-any 84 | webhookTarget: data[0].type as any, 85 | }); 86 | 87 | return new Response(null, { status: 204 }); 88 | } 89 | 90 | return new Response(null, { status: 404 }); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /src/arguments/commandName.ts: -------------------------------------------------------------------------------- 1 | import { LanguageKeys } from "#lib/i18n/all"; 2 | import type { AutoCommand } from "#lib/structs/AutoResponse/AutoResponse.ts"; 3 | import type { PayloadCommand } from "#lib/structs/commands/PayloadCommand.ts"; 4 | import { Argument, Command, type ArgumentContext } from "@sapphire/framework"; 5 | 6 | type CommandReturn = PayloadCommand | AutoCommand | undefined; 7 | 8 | export class UserArgument extends Argument { 9 | public run(parameter: string, context: ArgumentContext) { 10 | const commands = this.container.stores.get("commands"); 11 | const autoCommands = this.container.stores.get("auto"); 12 | 13 | let found = commands.get(parameter.toLowerCase()) as CommandReturn; 14 | 15 | found ??= autoCommands.get(parameter.toLowerCase()) as CommandReturn; 16 | 17 | if (found) { 18 | return this.ok(found); 19 | } 20 | 21 | // make this identifier something in the future 22 | return this.error({ parameter, identifier: LanguageKeys.Arguments.Command, context }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/auto/etf2l.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AutoCommand, 3 | type AutoCommandOptions, 4 | } from "#lib/structs/AutoResponse/AutoResponse.ts"; 5 | import { ApplyOptions } from "@sapphire/decorators"; 6 | import PayloadColors from "#utils/colors.ts"; 7 | import { AttachmentBuilder, EmbedBuilder, Message } from "discord.js"; 8 | import { LanguageKeys } from "#lib/i18n/all"; 9 | import { BucketScope } from "@sapphire/framework"; 10 | import { send } from "@sapphire/plugin-editable-commands"; 11 | import { Buffer } from "node:buffer"; 12 | 13 | @ApplyOptions({ 14 | description: LanguageKeys.Auto.Etf2l.Etf2lDescription, 15 | cooldownDelay: 2500, 16 | cooldownScope: BucketScope.Guild, 17 | cooldownLimit: 1, 18 | regex: /https:\/\/etf2l.org\/teams\/\d+/, 19 | }) 20 | export default class UserAutoCommand extends AutoCommand { 21 | // @ts-ignore have to do this 22 | async messageRun( 23 | msg: Message, 24 | args: AutoCommand.Args, 25 | { matched }: AutoCommand.Context, 26 | ) { 27 | const preview = await fetch( 28 | `${Deno.env.get("PREVIEW_URL")!}/v0/etf2l/teams`, 29 | { 30 | method: "POST", 31 | body: JSON.stringify({ url: matched }), 32 | }, 33 | ); 34 | 35 | const arrayBuffer = await preview.arrayBuffer(); 36 | 37 | const att = new AttachmentBuilder(Buffer.from(arrayBuffer), { 38 | name: "team.png", 39 | }); 40 | 41 | const embed = new EmbedBuilder({ 42 | color: PayloadColors.Command, 43 | title: args.t(LanguageKeys.Auto.Etf2l.Etf2lEmbedTitle), 44 | url: matched, 45 | image: { url: "attachment://team.png" }, 46 | footer: { 47 | text: args.t(LanguageKeys.Globals.AutoEmbedFooter, { name: this.name }), 48 | }, 49 | timestamp: new Date(), 50 | }); 51 | 52 | await send(msg, { embeds: [embed], files: [att] }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/auto/etf2lMatches.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AutoCommand, 3 | type AutoCommandOptions, 4 | } from "#lib/structs/AutoResponse/AutoResponse.ts"; 5 | import { ApplyOptions } from "@sapphire/decorators"; 6 | import PayloadColors from "#utils/colors.ts"; 7 | import { AttachmentBuilder, EmbedBuilder, Message } from "discord.js"; 8 | import { LanguageKeys } from "#lib/i18n/all"; 9 | import { send } from "@sapphire/plugin-editable-commands"; 10 | import { Buffer } from "node:buffer"; 11 | 12 | @ApplyOptions({ 13 | description: LanguageKeys.Auto.Etf2l.Etf2lMatchesDescription, 14 | regex: /etf2l.org\/matches\/\d+/, 15 | }) 16 | export default class UserAutoCommand extends AutoCommand { 17 | // @ts-ignore have to do this 18 | async messageRun( 19 | msg: Message, 20 | args: AutoCommand.Args, 21 | { matched }: AutoCommand.Context, 22 | ) { 23 | const preview = await fetch( 24 | `${Deno.env.get("PREVIEW_URL")!}/v0/etf2l/matches`, 25 | { 26 | method: "POST", 27 | body: JSON.stringify({ url: matched }), 28 | }, 29 | ); 30 | 31 | const arrayBuffer = await preview.arrayBuffer(); 32 | 33 | const att = new AttachmentBuilder(Buffer.from(arrayBuffer), { 34 | name: "match.png", 35 | }); 36 | 37 | const embed = new EmbedBuilder({ 38 | color: PayloadColors.Command, 39 | title: args.t(LanguageKeys.Auto.Etf2l.Etf2lMatchesEmbedTitle), 40 | url: `https://${matched}`, 41 | image: { url: "attachment://match.png" }, 42 | footer: { 43 | text: args.t(LanguageKeys.Globals.AutoEmbedFooter, { name: this.name }), 44 | }, 45 | timestamp: new Date(), 46 | }); 47 | 48 | await send(msg, { embeds: [embed], files: [att] }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/auto/logs.ts: -------------------------------------------------------------------------------- 1 | import { AutoCommand, type AutoCommandOptions } from "#lib/structs/AutoResponse/AutoResponse.ts"; 2 | import { ApplyOptions } from "@sapphire/decorators"; 3 | import PayloadColors from "#utils/colors.ts"; 4 | import { AttachmentBuilder, EmbedBuilder, Message } from "discord.js"; 5 | import { BucketScope } from "@sapphire/framework"; 6 | import { LanguageKeys } from "#lib/i18n/all"; 7 | import { send } from "@sapphire/plugin-editable-commands"; 8 | import { Buffer } from "node:buffer"; 9 | 10 | @ApplyOptions({ 11 | description: LanguageKeys.Auto.Logs.Description, 12 | cooldownDelay: 2500, 13 | cooldownScope: BucketScope.Guild, 14 | cooldownLimit: 1, 15 | regex: /http(s|):\/\/(www\.|)logs\.tf\/\d+/, 16 | }) 17 | export default class UserAutoCommand extends AutoCommand { 18 | // @ts-ignore have to do this 19 | async messageRun(msg: Message, args: AutoCommand.Args, { matched }: AutoCommand.Context) { 20 | const preview = await fetch( 21 | `${Deno.env.get("PREVIEW_URL")!}/v0/logstf`, 22 | { 23 | method: "POST", 24 | body: JSON.stringify({ url: matched }), 25 | }, 26 | ); 27 | 28 | const arrayBuffer = await preview.arrayBuffer(); 29 | 30 | const att = new AttachmentBuilder(Buffer.from(arrayBuffer), { name: "log.webp" }); 31 | 32 | const embed = new EmbedBuilder({ 33 | color: PayloadColors.Command, 34 | title: args.t(LanguageKeys.Auto.Logs.EmbedTitle), 35 | url: matched, 36 | image: { url: "attachment://log.webp" }, 37 | footer: { 38 | text: args.t(LanguageKeys.Globals.AutoEmbedFooter, { name: this.name }), 39 | }, 40 | timestamp: new Date(), 41 | }); 42 | 43 | await send(msg, { embeds: [embed], files: [att] }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/auto/rgl.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AutoCommand, 3 | type AutoCommandOptions, 4 | } from "#lib/structs/AutoResponse/AutoResponse.ts"; 5 | import { ApplyOptions } from "@sapphire/decorators"; 6 | import PayloadColors from "#utils/colors.ts"; 7 | import { AttachmentBuilder, EmbedBuilder, Message } from "discord.js"; 8 | import { LanguageKeys } from "#lib/i18n/all"; 9 | import { BucketScope } from "@sapphire/framework"; 10 | import { send } from "@sapphire/plugin-editable-commands"; 11 | import { Buffer } from "node:buffer"; 12 | 13 | @ApplyOptions({ 14 | description: LanguageKeys.Auto.RGL.RGLDescription, 15 | cooldownDelay: 2500, 16 | cooldownScope: BucketScope.Guild, 17 | cooldownLimit: 1, 18 | regex: /rgl\.gg\/Public\/Team(\.aspx)*\?t=\d+\&r=\d+/, 19 | }) 20 | export default class UserAutoCommand extends AutoCommand { 21 | // @ts-ignore have to do this 22 | async messageRun( 23 | msg: Message, 24 | args: AutoCommand.Args, 25 | { matched }: AutoCommand.Context, 26 | ) { 27 | const preview = await fetch( 28 | `${Deno.env.get("PREVIEW_URL")!}/v0/rgl/teams`, 29 | { 30 | method: "POST", 31 | body: JSON.stringify({ url: matched }), 32 | }, 33 | ); 34 | 35 | const arrayBuffer = await preview.arrayBuffer(); 36 | 37 | const att = new AttachmentBuilder( 38 | Buffer.from(arrayBuffer), 39 | { 40 | name: "team.webp", 41 | }, 42 | ); 43 | 44 | const embed = new EmbedBuilder({ 45 | color: PayloadColors.Command, 46 | title: args.t(LanguageKeys.Auto.RGL.RGLEmbedTitle), 47 | url: `https://${matched}`, 48 | image: { url: "attachment://team.webp" }, 49 | footer: { 50 | text: args.t(LanguageKeys.Globals.AutoEmbedFooter, { name: this.name }), 51 | }, 52 | timestamp: new Date(), 53 | }); 54 | 55 | await send(msg, { embeds: [embed], files: [att] }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/auto/steam-connect-fromlink.ts: -------------------------------------------------------------------------------- 1 | import { AutoCommand, type AutoCommandOptions } from "#lib/structs/AutoResponse/AutoResponse.ts"; 2 | import { ApplyOptions } from "@sapphire/decorators"; 3 | import { EmbedColors } from "#utils/colors.ts"; 4 | import { GameDig } from "gamedig"; 5 | import { Message, EmbedBuilder } from "discord.js"; 6 | import { LanguageKeys } from "#lib/i18n/all"; 7 | import { send } from "@sapphire/plugin-editable-commands"; 8 | 9 | @ApplyOptions({ 10 | description: LanguageKeys.Auto.Connect.Description, 11 | regex: /steam:\/\/connect\/(\w+\.)+\w+(:\d+)?\/.+([^\n`$])/, 12 | }) 13 | export default class UserAutoCommand extends AutoCommand { 14 | // @ts-ignore have to do this 15 | async messageRun(msg: Message, args: AutoCommand.Args, { matched }: AutoCommand.Context) { 16 | const parts = matched.trim().replace("steam://connect/", "").split("/"); 17 | 18 | const ip = parts[0]; 19 | const ipNoPort = ip.split(":")[0]; 20 | const port = ip.split(":")[1] || "27015"; 21 | const password = decodeURIComponent(parts[1]); 22 | 23 | const title = `${ip}/${encodeURIComponent(password)}`; 24 | 25 | const embed = new EmbedBuilder({ 26 | title: title.length > 250 ? title.slice(0, 250) : title, 27 | url: `https://api.payload.tf/api/steam?ip=${ip}&pw=${encodeURIComponent(password)}`, 28 | }); 29 | 30 | const connectInfoEmbed = await send(msg, { embeds: [embed] }); 31 | 32 | try { 33 | const { name, maxplayers, players } = await GameDig.query({ 34 | type: "tf2", 35 | host: ipNoPort, 36 | port: parseInt(port), 37 | }); 38 | 39 | embed.setColor(EmbedColors.Green); 40 | embed.setDescription(`${name}\n${players.length}/${maxplayers} ${args.t(LanguageKeys.Auto.Connect.Players)}`); 41 | } catch { 42 | embed.setColor(EmbedColors.Red); 43 | embed.setDescription(args.t(LanguageKeys.Auto.Connect.Offline)); 44 | } 45 | 46 | await connectInfoEmbed.edit({ embeds: [embed] }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/auto/steam-connect.ts: -------------------------------------------------------------------------------- 1 | import { AutoCommand, type AutoCommandOptions } from "#lib/structs/AutoResponse/AutoResponse.ts"; 2 | import { ApplyOptions } from "@sapphire/decorators"; 3 | import { EmbedColors } from "#utils/colors.ts"; 4 | import { GameDig } from "gamedig"; 5 | import { Message, EmbedBuilder } from "discord.js"; 6 | import { LanguageKeys } from "#lib/i18n/all"; 7 | import { send } from "@sapphire/plugin-editable-commands"; 8 | 9 | @ApplyOptions({ 10 | description: LanguageKeys.Auto.Connect.LinkDescription, 11 | regex: /connect (https?:\/\/)?(.+\.)+\w+(:\d+)?; ?password .+([^\n`$])/, 12 | }) 13 | export default class UserAutoCommand extends AutoCommand { 14 | // @ts-ignore have to do this 15 | async messageRun(msg: Message, args: AutoCommand.Args, { matched }: AutoCommand.Context) { 16 | const connectInfo = matched.trim(); 17 | const parts = connectInfo.split(";"); 18 | 19 | const ip = parts[0].replace(/^connect (https?:\/\/)?/, ""); 20 | const ipNoPort = ip.split(":")[0]; 21 | const port = ip.split(":")[1] || "27015"; 22 | const password = parts 23 | .slice(1) 24 | .join(";") 25 | .replace(/"|;$/g, "") 26 | .replace(/^ ?password /, ""); 27 | 28 | const title = `${ip}/${encodeURIComponent(password)}`; 29 | 30 | const embed = new EmbedBuilder({ 31 | title: title.length > 250 ? title.slice(0, 250) : title, 32 | url: `https://api.payload.tf/api/steam?ip=${ip}&pw=${encodeURIComponent(password)}`, 33 | }); 34 | 35 | const connectInfoEmbed = await send(msg, { embeds: [embed] }); 36 | 37 | try { 38 | const { name, maxplayers, players } = await GameDig.query({ 39 | type: "tf2", 40 | socketTimeout: 5000, 41 | attemptTimeout: 5000, 42 | host: ipNoPort, 43 | port: parseInt(port, 10), 44 | }); 45 | 46 | embed.setColor(EmbedColors.Green); 47 | embed.setDescription(`${name}\n${players.length}/${maxplayers} ${args.t(LanguageKeys.Auto.Connect.Players)}`); 48 | } catch { 49 | embed.setColor(EmbedColors.Red); 50 | embed.setDescription(args.t(LanguageKeys.Auto.Connect.Offline)); 51 | } 52 | 53 | await connectInfoEmbed.edit({ embeds: [embed] }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/auto/tftv.ts: -------------------------------------------------------------------------------- 1 | import { AutoCommand, type AutoCommandOptions } from "#lib/structs/AutoResponse/AutoResponse.ts"; 2 | import { ApplyOptions } from "@sapphire/decorators"; 3 | import PayloadColors from "#utils/colors.ts"; 4 | import { Message, EmbedBuilder } from "discord.js"; 5 | import { load } from "cheerio"; 6 | import { htmlToText } from "html-to-text"; 7 | import { LanguageKeys } from "#lib/i18n/all"; 8 | import { send } from "@sapphire/plugin-editable-commands"; 9 | import { fetch, FetchResultTypes } from "@sapphire/fetch"; 10 | 11 | @ApplyOptions({ 12 | description: LanguageKeys.Auto.Tftv.Description, 13 | regex: /(?teamfortress\.tv\/\d+\/[\w-]+)(?\/\?page=\d)?(?#\d+)*/, 14 | }) 15 | export default class UserAutoCommand extends AutoCommand { 16 | override async messageRun(msg: Message, { t }: AutoCommand.Args) { 17 | const match = this.getMatch(msg); 18 | const allMatches = msg.content.match(this.regex)!; 19 | 20 | const baseUrl = allMatches.groups!.base; 21 | 22 | let page = parseInt(allMatches.groups!.page?.replace("/?page=", "") ?? "0", 10); 23 | 24 | const post = parseInt(allMatches.groups!.post?.replace("#", ""), 10); 25 | 26 | if (!page && post > 30) { 27 | page = Math.floor(post / 30) + 1; 28 | } 29 | 30 | const url = `https://${baseUrl}${page > 0 ? `/?page=${page}#${post}` : `#${post}`}`; 31 | 32 | const data = await fetch(url, FetchResultTypes.Text); 33 | 34 | const $ = load(data); 35 | 36 | const title = $(".thread-header-title").text().trim(); 37 | 38 | const needFindChild = !!match.split("#")?.[1]; 39 | 40 | let $post = $(`#thread-container > .post:nth-child(1)`); 41 | 42 | let frags = $("#thread-frag-count").text().trim(); 43 | const htmlBody = $post.find(".post-body"); 44 | 45 | const anchorRules = { 46 | selectors: [ 47 | { 48 | selector: "a", 49 | options: { hideLinkHrefIfSameAsText: true }, 50 | }, 51 | ], 52 | }; 53 | 54 | let body = htmlToText(htmlBody.html()!, anchorRules); 55 | 56 | let author = $post.find(".post-header .post-author").text().trim(); 57 | 58 | if (needFindChild) { 59 | const postNumber = match.split("#")[1]; 60 | $post = $(`#thread-container > .post > a#${postNumber}`).parent(); 61 | 62 | if (!$post.children().length && page > 1) { 63 | await send(msg, t(LanguageKeys.Auto.Tftv.NoPostFound, { post: url })); 64 | } 65 | 66 | if (!$post.children().length) { 67 | $post = $(`#thread-container > .post:last-child`); 68 | } 69 | 70 | frags = $post.find(`.post-frag-count`).text().trim(); 71 | const postBody = $post.find(".post-body"); 72 | body = htmlToText(postBody.html()!, anchorRules); 73 | author = $post.find(".post-header .post-author").text().trim(); 74 | } 75 | 76 | const dateSelector = $post.find(".post-footer .js-date-toggle").attr("title"); 77 | const date = dateSelector ? dateSelector.replace(/at (\d+:\d+).+$/, "$1") : "N/A"; 78 | 79 | const embed = new EmbedBuilder({ 80 | title, 81 | url, 82 | color: PayloadColors.User, 83 | timestamp: new Date(date), 84 | description: `${author}\n\n${body.length > 700 ? `${body.slice(0, 700)}...\n[read more](${url})` : body}`, 85 | footer: { 86 | text: `${frags} frags`, 87 | }, 88 | }); 89 | 90 | await send(msg, { embeds: [embed] }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/commands/Admin/language.ts: -------------------------------------------------------------------------------- 1 | import { ApplyOptions } from "@sapphire/decorators"; 2 | import { ActionRowBuilder, EmbedBuilder, InteractionContextType, StringSelectMenuBuilder } from "discord.js"; 3 | import PayloadColors from "#utils/colors.ts"; 4 | import { inlineCode } from "@discordjs/builders"; 5 | import { LanguageKeys } from "#lib/i18n/all"; 6 | import { Subcommand } from "@sapphire/plugin-subcommands"; 7 | import { Command, CommandOptionsRunTypeEnum } from "@sapphire/framework"; 8 | import { fetchT, getLocalizedData } from "@sapphire/plugin-i18next"; 9 | import { PermissionFlagsBits } from "discord-api-types/v10"; 10 | import { guild } from "#root/drizzle/schema.ts"; 11 | import { eq } from "drizzle-orm"; 12 | import { PayloadCommand } from "#lib/structs/commands/PayloadCommand.ts"; 13 | 14 | @ApplyOptions({ 15 | description: LanguageKeys.Commands.Language.Description, 16 | detailedDescription: LanguageKeys.Commands.Language.DetailedDescription, 17 | runIn: [CommandOptionsRunTypeEnum.GuildText], 18 | }) 19 | export class UserCommand extends PayloadCommand { 20 | override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { 21 | const t = await fetchT(interaction); 22 | 23 | const [g] = await this.database 24 | .select({ language: guild.language }) 25 | .from(guild) 26 | .where(eq(guild.id, interaction.guildId!)); 27 | 28 | const languageSelector = new ActionRowBuilder().addComponents( 29 | new StringSelectMenuBuilder() 30 | .addOptions( 31 | { label: t(LanguageKeys.Commands.Language.English), value: "en-US" }, 32 | { label: t(LanguageKeys.Commands.Language.Spanish), value: "es-ES" }, 33 | { label: t(LanguageKeys.Commands.Language.German), value: "de" }, 34 | { label: t(LanguageKeys.Commands.Language.Finnish), value: "fi" }, 35 | { label: t(LanguageKeys.Commands.Language.French), value: "fr" }, 36 | { label: t(LanguageKeys.Commands.Language.Russian), value: "ru" }, 37 | { label: t(LanguageKeys.Commands.Language.Polish), value: "pl" }, 38 | ) 39 | .setCustomId("set-guild-language") 40 | .setPlaceholder(t(LanguageKeys.Commands.Language.SelectLanguage)), 41 | ); 42 | 43 | const embed = new EmbedBuilder({ 44 | title: t(LanguageKeys.Commands.Language.SelectLanguage), 45 | description: t(LanguageKeys.Commands.Language.CurrentLanguage, { 46 | language: inlineCode(g?.language ?? "en-US"), 47 | }), 48 | timestamp: new Date(), 49 | color: PayloadColors.Admin, 50 | }); 51 | 52 | await interaction.reply({ 53 | embeds: [embed], 54 | components: [languageSelector], 55 | ephemeral: true, 56 | }); 57 | 58 | return; 59 | } 60 | 61 | public override registerApplicationCommands(registry: Command.Registry) { 62 | const rootNameLocalizations = getLocalizedData(LanguageKeys.Commands.Language.Name); 63 | const rootDescriptionLocalizations = getLocalizedData(this.description); 64 | 65 | registry.registerChatInputCommand((builder) => 66 | builder 67 | .setName(this.name) 68 | .setDescription(rootDescriptionLocalizations.localizations["en-US"]!) 69 | .setContexts(InteractionContextType.Guild) 70 | .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) 71 | .setDescriptionLocalizations(rootDescriptionLocalizations.localizations) 72 | .setNameLocalizations(rootNameLocalizations.localizations) 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/commands/Admin/prefix.ts: -------------------------------------------------------------------------------- 1 | import { ApplyOptions, RequiresGuildContext, RequiresUserPermissions } from "@sapphire/decorators"; 2 | import { EmbedBuilder, Message } from "discord.js"; 3 | import { send } from "@sapphire/plugin-editable-commands"; 4 | import config from "#root/config.ts"; 5 | import PayloadColors from "#utils/colors.ts"; 6 | import { inlineCode } from "@discordjs/builders"; 7 | import { LanguageKeys } from "#lib/i18n/all"; 8 | import { Subcommand, type SubcommandMappingArray } from "@sapphire/plugin-subcommands"; 9 | import { Args, CommandOptionsRunTypeEnum } from "@sapphire/framework"; 10 | import { fetchT } from "@sapphire/plugin-i18next"; 11 | import { PermissionFlagsBits } from "discord-api-types/v9"; 12 | import { guild } from "#root/drizzle/schema.ts"; 13 | import { eq } from "drizzle-orm"; 14 | 15 | @ApplyOptions({ 16 | description: LanguageKeys.Commands.Prefix.Description, 17 | detailedDescription: LanguageKeys.Commands.Prefix.DetailedDescription, 18 | runIn: [CommandOptionsRunTypeEnum.GuildText], 19 | }) 20 | export class UserCommand extends Subcommand { 21 | private readonly database = this.container.database; 22 | private readonly t = async (msg: Message) => await fetchT(msg); 23 | 24 | readonly subcommandMappings: SubcommandMappingArray = [ 25 | { 26 | name: "view", 27 | type: "method", 28 | messageRun: (msg) => this.view(msg), 29 | default: true, 30 | }, 31 | { 32 | name: "set", 33 | type: "method", 34 | messageRun: (msg, args) => this.set(msg, args), 35 | }, 36 | { 37 | name: "update", 38 | type: "method", 39 | messageRun: (msg, args) => this.set(msg, args), 40 | }, 41 | { 42 | name: "delete", 43 | type: "method", 44 | messageRun: (msg) => this.delete(msg), 45 | }, 46 | { 47 | name: "remove", 48 | type: "method", 49 | messageRun: (msg) => this.delete(msg), 50 | }, 51 | ]; 52 | 53 | @RequiresGuildContext() 54 | async view(msg: Message) { 55 | const [g] = await this.database 56 | .select({ prefix: guild.language }) 57 | .from(guild) 58 | .where(eq(guild.id, msg.guildId!)); 59 | 60 | const t = await this.t(msg); 61 | 62 | const content = t(LanguageKeys.Commands.Prefix.CurrentPrefix, { 63 | prefix: inlineCode(g?.prefix ?? config.PREFIX), 64 | }); 65 | 66 | return await send(msg, content); 67 | } 68 | 69 | @RequiresGuildContext() 70 | @RequiresUserPermissions([PermissionFlagsBits.ManageGuild]) 71 | async set(msg: Message, args: Args) { 72 | const [g] = await this.database 73 | .select({ guildPrefix: guild.language }) 74 | .from(guild) 75 | .where(eq(guild.id, msg.guildId!)); 76 | 77 | const prefix = await args.pick("string").catch(() => null); 78 | 79 | const t = await this.t(msg); 80 | 81 | if (!prefix) { 82 | return await send(msg, t(LanguageKeys.Commands.Prefix.SetNeedsArgs)); 83 | } 84 | 85 | if (g?.guildPrefix === prefix) { 86 | return await send(msg, t(LanguageKeys.Commands.Prefix.SetSamePrefix)); 87 | } 88 | 89 | await this.database 90 | .update(guild) 91 | .set({ 92 | prefix, 93 | }) 94 | .where(eq(guild.id, msg.guildId!)); 95 | 96 | const embed = new EmbedBuilder({ 97 | author: { 98 | name: msg.author.tag, 99 | iconURL: msg.author.displayAvatarURL(), 100 | }, 101 | title: t(LanguageKeys.Commands.Prefix.SetPrefixEmbedTitle, { 102 | user: msg.author.tag, 103 | }), 104 | description: t(LanguageKeys.Commands.Prefix.SetPrefixEmbedDesc, { 105 | old: inlineCode(g?.guildPrefix ?? config.PREFIX), 106 | new: inlineCode(prefix), 107 | }), 108 | timestamp: new Date(), 109 | color: PayloadColors.Admin, 110 | }); 111 | 112 | return await send(msg, { embeds: [embed] }); 113 | } 114 | 115 | @RequiresGuildContext() 116 | @RequiresUserPermissions([PermissionFlagsBits.ManageGuild]) 117 | async delete(msg: Message) { 118 | const [g] = await this.database 119 | .select({ guildPrefix: guild.language }) 120 | .from(guild) 121 | .where(eq(guild.id, msg.guildId!)); 122 | 123 | const t = await this.t(msg); 124 | 125 | if (g?.guildPrefix === config.PREFIX) { 126 | return await send(msg, t(LanguageKeys.Commands.Prefix.DeleteAlreadyDefault)); 127 | } 128 | 129 | const embed = new EmbedBuilder({ 130 | author: { 131 | name: msg.author.tag, 132 | iconURL: msg.author.displayAvatarURL(), 133 | }, 134 | title: t(LanguageKeys.Commands.Prefix.SetPrefixEmbedTitle, { 135 | user: msg.author.tag, 136 | }), 137 | description: t(LanguageKeys.Commands.Prefix.SetPrefixEmbedDesc, { 138 | old: inlineCode(g.guildPrefix), 139 | new: inlineCode(config.PREFIX), 140 | }), 141 | timestamp: new Date(), 142 | color: PayloadColors.Admin, 143 | }); 144 | 145 | await this.database 146 | .update(guild) 147 | .set({ 148 | prefix: config.PREFIX, 149 | }) 150 | .where(eq(guild.id, msg.guildId!)); 151 | 152 | return await send(msg, { embeds: [embed] }); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/commands/General/commands.ts: -------------------------------------------------------------------------------- 1 | import type { CommandOptions } from "@sapphire/framework"; 2 | import { ApplyOptions } from "@sapphire/decorators"; 3 | import { EmbedBuilder, Message } from "discord.js"; 4 | import { send } from "@sapphire/plugin-editable-commands"; 5 | import PayloadColors from "#utils/colors.ts"; 6 | import { inlineCode } from "@discordjs/builders"; 7 | import { PayloadCommand } from "#lib/structs/commands/PayloadCommand.ts"; 8 | import { LanguageKeys } from "#lib/i18n/all"; 9 | 10 | @ApplyOptions({ 11 | description: LanguageKeys.Commands.Commands.Description, 12 | detailedDescription: LanguageKeys.Commands.Commands.DetailedDescription, 13 | }) 14 | export class UserCommand extends PayloadCommand { 15 | override async messageRun(msg: Message, args: PayloadCommand.Args) { 16 | const { stores } = this.container; 17 | 18 | const commands = [...stores.get("commands").values()]; 19 | const autoCommands = [...stores.get("auto").values()]; 20 | 21 | const embed = new EmbedBuilder({ 22 | title: args.t(LanguageKeys.Commands.Commands.EmbedTitle), 23 | color: PayloadColors.User, 24 | fields: [ 25 | { 26 | name: args.t(LanguageKeys.Commands.Commands.Commands), 27 | value: commands.map((c) => inlineCode(c.name)).join(", "), 28 | }, 29 | { 30 | name: args.t(LanguageKeys.Commands.Commands.AutoCommands), 31 | value: autoCommands.map((ac) => inlineCode(ac.name)).join(", "), 32 | }, 33 | ], 34 | }); 35 | 36 | await send(msg, { embeds: [embed] }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/General/help.ts: -------------------------------------------------------------------------------- 1 | import type { CommandOptions, MessageCommandContext } from "@sapphire/framework"; 2 | import { ApplyOptions } from "@sapphire/decorators"; 3 | import { EmbedBuilder, Message } from "discord.js"; 4 | import { send } from "@sapphire/plugin-editable-commands"; 5 | import PayloadColors from "#utils/colors.ts"; 6 | import { PayloadCommand } from "#lib/structs/commands/PayloadCommand.ts"; 7 | import { LanguageKeys } from "#lib/i18n/all"; 8 | import { BuildCommandHelp, type LanguageHelpDisplayOptions } from "#lib/i18n/CommandHelper.ts"; 9 | 10 | @ApplyOptions({ 11 | description: LanguageKeys.Commands.Help.Description, 12 | detailedDescription: LanguageKeys.Commands.Help.DetailedDescription, 13 | aliases: ["h"], 14 | }) 15 | export class UserCommand extends PayloadCommand { 16 | override async messageRun( 17 | msg: Message, 18 | args: PayloadCommand.Args, 19 | context: MessageCommandContext, 20 | ) { 21 | if (args.finished) { 22 | // Just send the commands command 23 | const allCommands = this.container.stores.get("commands"); 24 | const runCommand = allCommands.get("commands"); 25 | 26 | await runCommand?.messageRun?.(msg, args, context); 27 | return; 28 | } 29 | 30 | const command = await args.pick("commandName"); 31 | 32 | const translatedCases = args.t(LanguageKeys.System.HelpTitles); 33 | 34 | const builder = new BuildCommandHelp() 35 | .setDescription(translatedCases.description) 36 | .setAliases(translatedCases.aliases) 37 | .setUsages(translatedCases.usages) 38 | .setDetails(translatedCases.moreDetails); 39 | 40 | const detailedDescription = args.t( 41 | command.detailedDescription.toString(), 42 | ) as LanguageHelpDisplayOptions; 43 | 44 | const content = builder.display( 45 | command.name, 46 | this.getAliases(command), 47 | detailedDescription, 48 | context.commandPrefix, 49 | args.t(command.description), 50 | ); 51 | 52 | const embed = new EmbedBuilder({ 53 | title: command.name, 54 | color: PayloadColors.Command, 55 | description: content, 56 | }); 57 | 58 | await send(msg, { embeds: [embed] }); 59 | } 60 | 61 | private getAliases(command: PayloadCommand) { 62 | if (command.aliases) { 63 | return command.aliases.join(", "); 64 | } 65 | 66 | return null; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/commands/General/info.ts: -------------------------------------------------------------------------------- 1 | import type { Command, CommandOptions } from "@sapphire/framework"; 2 | import { ApplyOptions } from "@sapphire/decorators"; 3 | import { EmbedBuilder } from "discord.js"; 4 | import { PayloadCommand } from "#lib/structs/commands/PayloadCommand.ts"; 5 | import { LanguageKeys } from "#lib/i18n/all"; 6 | import PayloadColors from "#utils/colors.ts"; 7 | import { fetchT, getLocalizedData } from "@sapphire/plugin-i18next"; 8 | 9 | @ApplyOptions({ 10 | description: LanguageKeys.Commands.Info.Description, 11 | detailedDescription: LanguageKeys.Commands.Info.DetailedDescription, 12 | }) 13 | export class UserCommand extends PayloadCommand { 14 | override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { 15 | const { client } = this.container; 16 | const t = await fetchT(interaction); 17 | 18 | const membersServing = client.guilds.cache.reduce((acc, val) => acc + (val.memberCount ?? 0), 0); 19 | 20 | const guildsServing = client.guilds.cache.size; 21 | 22 | const embed = new EmbedBuilder({ 23 | title: t(LanguageKeys.Commands.Info.EmbedTitle, { 24 | users: membersServing, 25 | servers: guildsServing, 26 | }), 27 | description: t(LanguageKeys.Commands.Info.EmbedDescription), 28 | color: PayloadColors.Payload, 29 | }); 30 | 31 | await interaction.reply({ embeds: [embed] }); 32 | } 33 | 34 | public override registerApplicationCommands(registry: Command.Registry) { 35 | const rootNameLocalizations = getLocalizedData(LanguageKeys.Commands.Info.Name); 36 | const rootDescriptionLocalizations = getLocalizedData(this.description); 37 | 38 | registry.registerChatInputCommand((builder) => 39 | builder 40 | .setName(this.name) 41 | .setDescription(rootDescriptionLocalizations.localizations["en-US"]!) 42 | .setDescriptionLocalizations(rootDescriptionLocalizations.localizations) 43 | .setNameLocalizations(rootNameLocalizations.localizations) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/General/profile.ts: -------------------------------------------------------------------------------- 1 | import type { CommandOptions } from "@sapphire/framework"; 2 | import { ApplyOptions } from "@sapphire/decorators"; 3 | import { send } from "@sapphire/plugin-editable-commands"; 4 | import { EmbedBuilder, Message } from "discord.js"; 5 | import PayloadColors from "#utils/colors.ts"; 6 | import { PayloadCommand } from "#lib/structs/commands/PayloadCommand.ts"; 7 | import { LanguageKeys } from "#lib/i18n/all"; 8 | import { eq } from "drizzle-orm"; 9 | import { user } from "#root/drizzle/schema.ts"; 10 | 11 | @ApplyOptions({ 12 | description: LanguageKeys.Commands.Profile.Description, 13 | detailedDescription: LanguageKeys.Commands.Profile.DetailedDescription, 14 | }) 15 | export class UserCommand extends PayloadCommand { 16 | override async messageRun(msg: Message, args: PayloadCommand.Args) { 17 | const targetUser = await args.pick("user").catch(() => msg.author); 18 | 19 | const { t } = args; 20 | 21 | const [u] = await this.database 22 | .select({ steamId: user.steamId, legacyPushed: user.legacyPushed }) 23 | .from(user) 24 | .where(eq(user.id, targetUser.id)); 25 | 26 | const botT = t(LanguageKeys.Commands.Profile.Bot); 27 | const pointsT = t(LanguageKeys.Commands.Profile.Points); 28 | 29 | const description = ` 30 | ${botT}: ${targetUser.bot ? "Yes" : "No"} 31 | ID: ${targetUser.id} 32 | Steam ID: ${u?.steamId ?? "NOT SET"} 33 | ${pointsT}: ${u?.legacyPushed ?? 0} 34 | `; 35 | 36 | const embed = new EmbedBuilder({ 37 | title: targetUser.tag, 38 | description, 39 | color: PayloadColors.User, 40 | thumbnail: { 41 | url: targetUser.displayAvatarURL(), 42 | }, 43 | }); 44 | 45 | await send(msg, { 46 | embeds: [embed], 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/commands/Tf2/link.ts: -------------------------------------------------------------------------------- 1 | import type { Command, CommandOptions } from "@sapphire/framework"; 2 | import { ApplyOptions } from "@sapphire/decorators"; 3 | import { getSteamIdFromArgs } from "#utils/getSteamId.ts"; 4 | import { PayloadCommand } from "#lib/structs/commands/PayloadCommand.ts"; 5 | import { LanguageKeys } from "#lib/i18n/all"; 6 | import { eq } from "drizzle-orm"; 7 | import { user } from "#root/drizzle/schema.ts"; 8 | import { fetchT, getLocalizedData } from "@sapphire/plugin-i18next"; 9 | import { MessageFlags } from "discord-api-types/v10"; 10 | import { isNullishOrEmpty } from "@sapphire/utilities"; 11 | 12 | @ApplyOptions({ 13 | description: LanguageKeys.Commands.Link.Description, 14 | detailedDescription: LanguageKeys.Commands.Link.DetailedDescription, 15 | }) 16 | export class UserCommand extends PayloadCommand { 17 | override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { 18 | const t = await fetchT(interaction); 19 | const steamId = interaction.options.getString("steam-id")?.trim(); 20 | 21 | if (isNullishOrEmpty(steamId)) { 22 | await interaction.reply({ 23 | flags: MessageFlags.Ephemeral, 24 | content: t(LanguageKeys.Commands.Link.MissingId), 25 | }); 26 | 27 | return; 28 | } 29 | 30 | const testResult = await getSteamIdFromArgs(steamId); 31 | 32 | if (testResult === null) { 33 | await interaction.reply({ 34 | flags: MessageFlags.Ephemeral, 35 | content: t(LanguageKeys.Commands.Link.MalformedId), 36 | }); 37 | 38 | return; 39 | } 40 | 41 | await this.database 42 | .update(user) 43 | .set({ steamId }) 44 | .where(eq(user.id, interaction.user.id)); 45 | 46 | await interaction.reply({ 47 | flags: MessageFlags.Ephemeral, 48 | content: t(LanguageKeys.Commands.Link.Success, { steamId: testResult }), 49 | }); 50 | 51 | return; 52 | } 53 | 54 | public override registerApplicationCommands(registry: Command.Registry) { 55 | const rootNameLocalizations = getLocalizedData(LanguageKeys.Commands.Link.Name); 56 | const rootDescriptionLocalizations = getLocalizedData(this.description); 57 | 58 | const steamIdNameLocalizations = getLocalizedData(LanguageKeys.Commands.Link.SteamIdName); 59 | const steamIdDescriptionLocalizations = getLocalizedData(LanguageKeys.Commands.Link.SteamIdDescription); 60 | 61 | registry.registerChatInputCommand((builder) => 62 | builder 63 | .setName(this.name) 64 | .setDescription(rootDescriptionLocalizations.localizations["en-US"]!) 65 | .setDescriptionLocalizations(rootDescriptionLocalizations.localizations) 66 | .setNameLocalizations(rootNameLocalizations.localizations) 67 | .addStringOption( 68 | (input) => 69 | input 70 | .setName("steam-id") 71 | .setDescription(steamIdNameLocalizations.localizations["en-US"]!) 72 | .setRequired(true) 73 | .setNameLocalizations(steamIdNameLocalizations.localizations) 74 | .setDescriptionLocalizations(steamIdDescriptionLocalizations.localizations), 75 | ) 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/commands/Tf2/log.ts: -------------------------------------------------------------------------------- 1 | import { Command, type CommandOptions } from "@sapphire/framework"; 2 | import { ApplyOptions } from "@sapphire/decorators"; 3 | import { AttachmentBuilder, EmbedBuilder, MessageFlags } from "discord.js"; 4 | import { PayloadCommand } from "#lib/structs/commands/PayloadCommand.ts"; 5 | import { LanguageKeys } from "#lib/i18n/all"; 6 | import { eq } from "drizzle-orm"; 7 | import { search } from "@tf2software/logstf"; 8 | import { user } from "#root/drizzle/schema.ts"; 9 | import PayloadColors from "#utils/colors.ts"; 10 | import { Buffer } from "node:buffer"; 11 | import { fetchT, getLocalizedData } from "@sapphire/plugin-i18next"; 12 | 13 | @ApplyOptions({ 14 | description: LanguageKeys.Commands.Log.Description, 15 | detailedDescription: LanguageKeys.Commands.Log.DetailedDescription, 16 | }) 17 | export class UserCommand extends PayloadCommand { 18 | override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { 19 | const t = await fetchT(interaction); 20 | 21 | const [{ steamId }] = await this.database 22 | .select({ steamId: user.steamId }) 23 | .from(user) 24 | .where(eq(user.id, interaction.user.id)); 25 | 26 | if (steamId == null) { 27 | await interaction.reply({ 28 | flags: MessageFlags.Ephemeral, 29 | content: t(LanguageKeys.Commands.Log.NoIdLinked, { user: interaction.user.tag }), 30 | }); 31 | return; 32 | } 33 | 34 | const { logs } = await search({ limit: 1, player: [steamId] }); 35 | 36 | if (!logs.length) { 37 | await interaction.reply({ 38 | flags: MessageFlags.Ephemeral, 39 | content: t(LanguageKeys.Commands.Log.NoHistory), 40 | }); 41 | return; 42 | } 43 | 44 | const response = await interaction.deferReply({ flags: MessageFlags.Ephemeral }); 45 | 46 | const logID = logs[0].id; 47 | const logsUrl = `http://logs.tf/${logID}#${steamId}`; 48 | 49 | const preview = await fetch( 50 | `${Deno.env.get("PREVIEW_URL")!}/v0/logstf`, 51 | { 52 | method: "POST", 53 | body: JSON.stringify({ url: logsUrl }), 54 | }, 55 | ); 56 | 57 | const arrayBuffer = await preview.arrayBuffer(); 58 | 59 | const att = new AttachmentBuilder(Buffer.from(arrayBuffer), { 60 | name: "log.webp", 61 | }); 62 | 63 | const embed = new EmbedBuilder({ 64 | color: PayloadColors.Command, 65 | title: t(LanguageKeys.Auto.Logs.EmbedTitle), 66 | url: logsUrl, 67 | image: { url: "attachment://log.webp" }, 68 | timestamp: new Date(), 69 | }); 70 | 71 | await response.edit({ 72 | embeds: [embed], 73 | files: [att], 74 | }); 75 | } 76 | 77 | public override registerApplicationCommands(registry: Command.Registry) { 78 | const rootNameLocalizations = getLocalizedData(LanguageKeys.Commands.Log.Name); 79 | const rootDescriptionLocalizations = getLocalizedData(this.description); 80 | 81 | registry.registerChatInputCommand((builder) => 82 | builder 83 | .setName(this.name) 84 | .setDescription(rootDescriptionLocalizations.localizations["en-US"]!) 85 | .setDescriptionLocalizations(rootDescriptionLocalizations.localizations) 86 | .setNameLocalizations(rootNameLocalizations.localizations) 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/commands/User/webhook.ts: -------------------------------------------------------------------------------- 1 | import { Command, type CommandOptions, CommandOptionsRunTypeEnum } from "@sapphire/framework"; 2 | import { ApplyOptions } from "@sapphire/decorators"; 3 | import { ActionRowBuilder, ButtonBuilder, ButtonStyle, codeBlock, InteractionContextType, MessageFlags, TextDisplayBuilder } from "discord.js"; 4 | import { isNullOrUndefinedOrEmpty } from "@sapphire/utilities"; 5 | import { PayloadCommand } from "#lib/structs/commands/PayloadCommand.ts"; 6 | import { LanguageKeys } from "#lib/i18n/all"; 7 | import { webhook } from "#root/drizzle/schema.ts"; 8 | import { eq } from "drizzle-orm"; 9 | import { fetchT, getLocalizedData } from "@sapphire/plugin-i18next"; 10 | import { ContainerBuilder } from "discord.js"; 11 | 12 | @ApplyOptions({ 13 | description: LanguageKeys.Commands.Webhook.Description, 14 | detailedDescription: LanguageKeys.Commands.Webhook.DetailedDescription, 15 | runIn: [CommandOptionsRunTypeEnum.Dm], 16 | }) 17 | export class UserCommand extends PayloadCommand { 18 | override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { 19 | const t = await fetchT(interaction); 20 | 21 | const [v] = await this.database 22 | .select({ value: webhook.value }) 23 | .from(webhook) 24 | .where(eq(webhook.id, interaction.user.id)); 25 | 26 | if (isNullOrUndefinedOrEmpty(v)) { 27 | const createWebhookButton = new ActionRowBuilder().addComponents( 28 | new ButtonBuilder({ 29 | label: t(LanguageKeys.Commands.Webhook.CreateWebhook), 30 | customId: "create-webhook-for-user", 31 | style: ButtonStyle.Primary, 32 | }), 33 | ); 34 | 35 | const description = new TextDisplayBuilder() 36 | .setContent(t(LanguageKeys.Commands.Webhook.NoWebhook)); 37 | 38 | const container = new ContainerBuilder() 39 | .addTextDisplayComponents(description) 40 | .addActionRowComponents(createWebhookButton); 41 | 42 | await interaction.reply({ 43 | flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2, 44 | components: [container], 45 | }); 46 | return; 47 | } 48 | 49 | const deleteWebhookButton = new ActionRowBuilder().addComponents( 50 | new ButtonBuilder({ 51 | label: t(LanguageKeys.Commands.Webhook.DeleteWebhook), 52 | customId: "delete-webhook-for-user", 53 | style: ButtonStyle.Danger, 54 | }), 55 | ); 56 | 57 | const description = new TextDisplayBuilder() 58 | .setContent(t(LanguageKeys.Commands.Webhook.EmbedDescription, { secret: codeBlock(v.value) })); 59 | 60 | const container = new ContainerBuilder() 61 | .addTextDisplayComponents(description) 62 | .addActionRowComponents(deleteWebhookButton); 63 | 64 | await interaction.reply({ 65 | flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2, 66 | components: [container], 67 | }); 68 | } 69 | 70 | public override registerApplicationCommands(registry: Command.Registry) { 71 | const descriptionLocalizations = getLocalizedData(this.description); 72 | const nameLocalizations = getLocalizedData(LanguageKeys.Commands.Webhook.Name); 73 | 74 | registry.registerChatInputCommand((builder) => 75 | builder 76 | .setName(this.name) 77 | .setDescription(descriptionLocalizations.localizations["en-US"]!) 78 | .setContexts(InteractionContextType.BotDM) 79 | .setDescriptionLocalizations(descriptionLocalizations.localizations) 80 | .setNameLocalizations(nameLocalizations.localizations) 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | /** 3 | * Default prefix 4 | */ 5 | PREFIX: "pls ", 6 | } as const; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/drizzle/0000_far_redwing.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `Guild` ( 2 | `id` text PRIMARY KEY NOT NULL, 3 | `prefix` text DEFAULT 'pls ' NOT NULL, 4 | `language` text DEFAULT 'en-US' NOT NULL, 5 | `legacyPushed` integer, 6 | `webhookId` text, 7 | FOREIGN KEY (`webhookId`) REFERENCES `Webhook`(`id`) ON UPDATE cascade ON DELETE set null 8 | ); 9 | --> statement-breakpoint 10 | CREATE TABLE IF NOT EXISTS `Pushcart` ( 11 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 12 | `userId` text NOT NULL, 13 | `guildId` text NOT NULL, 14 | `pushed` integer NOT NULL, 15 | `timestamp` numeric 16 | ); 17 | --> statement-breakpoint 18 | CREATE TABLE IF NOT EXISTS `User` ( 19 | `id` text PRIMARY KEY NOT NULL, 20 | `legacyPushed` integer, 21 | `steamId` text, 22 | `webhookId` text, 23 | FOREIGN KEY (`webhookId`) REFERENCES `Webhook`(`id`) ON UPDATE cascade ON DELETE set null 24 | ); 25 | --> statement-breakpoint 26 | CREATE TABLE IF NOT EXISTS `Webhook` ( 27 | `id` text PRIMARY KEY NOT NULL, 28 | `value` text NOT NULL, 29 | `type` text NOT NULL, 30 | `createdAt` numeric 31 | ); 32 | --> statement-breakpoint 33 | CREATE UNIQUE INDEX IF NOT EXISTS `Webhook_value_key` ON `Webhook` (`value`); 34 | -------------------------------------------------------------------------------- /src/drizzle/0001_rename-languages.sql: -------------------------------------------------------------------------------- 1 | -- Custom SQL migration file, put your code below! -- 2 | 3 | UPDATE Guild SET language = substr(lower(language), 1, 2) WHERE language NOT IN ('es-ES', 'en-US'); 4 | -------------------------------------------------------------------------------- /src/drizzle/0002_normalize-timestamps.sql: -------------------------------------------------------------------------------- 1 | -- UPDATE Pushcart SET timestamp = timestamp * 1000 WHERE id > 23322; 2 | -- UPDATE Webhook SET createdAt = createdAt * 1000 WHERE id > 59; -------------------------------------------------------------------------------- /src/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "c1fbe079-cf0d-48ee-9ed9-af299162450b", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "Guild": { 8 | "name": "Guild", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "text", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "prefix": { 18 | "name": "prefix", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false, 23 | "default": "'pls '" 24 | }, 25 | "language": { 26 | "name": "language", 27 | "type": "text", 28 | "primaryKey": false, 29 | "notNull": true, 30 | "autoincrement": false, 31 | "default": "'en-US'" 32 | }, 33 | "legacyPushed": { 34 | "name": "legacyPushed", 35 | "type": "integer", 36 | "primaryKey": false, 37 | "notNull": false, 38 | "autoincrement": false 39 | }, 40 | "webhookId": { 41 | "name": "webhookId", 42 | "type": "text", 43 | "primaryKey": false, 44 | "notNull": false, 45 | "autoincrement": false 46 | } 47 | }, 48 | "indexes": {}, 49 | "foreignKeys": { 50 | "Guild_webhookId_Webhook_id_fk": { 51 | "name": "Guild_webhookId_Webhook_id_fk", 52 | "tableFrom": "Guild", 53 | "tableTo": "Webhook", 54 | "columnsFrom": [ 55 | "webhookId" 56 | ], 57 | "columnsTo": [ 58 | "id" 59 | ], 60 | "onDelete": "set null", 61 | "onUpdate": "cascade" 62 | } 63 | }, 64 | "compositePrimaryKeys": {}, 65 | "uniqueConstraints": {}, 66 | "checkConstraints": {} 67 | }, 68 | "Pushcart": { 69 | "name": "Pushcart", 70 | "columns": { 71 | "id": { 72 | "name": "id", 73 | "type": "integer", 74 | "primaryKey": true, 75 | "notNull": true, 76 | "autoincrement": true 77 | }, 78 | "userId": { 79 | "name": "userId", 80 | "type": "text", 81 | "primaryKey": false, 82 | "notNull": true, 83 | "autoincrement": false 84 | }, 85 | "guildId": { 86 | "name": "guildId", 87 | "type": "text", 88 | "primaryKey": false, 89 | "notNull": true, 90 | "autoincrement": false 91 | }, 92 | "pushed": { 93 | "name": "pushed", 94 | "type": "integer", 95 | "primaryKey": false, 96 | "notNull": true, 97 | "autoincrement": false 98 | }, 99 | "timestamp": { 100 | "name": "timestamp", 101 | "type": "numeric", 102 | "primaryKey": false, 103 | "notNull": true, 104 | "autoincrement": false, 105 | "default": "(cast(strftime('%s','now') as int))" 106 | } 107 | }, 108 | "indexes": {}, 109 | "foreignKeys": {}, 110 | "compositePrimaryKeys": {}, 111 | "uniqueConstraints": {}, 112 | "checkConstraints": {} 113 | }, 114 | "User": { 115 | "name": "User", 116 | "columns": { 117 | "id": { 118 | "name": "id", 119 | "type": "text", 120 | "primaryKey": true, 121 | "notNull": true, 122 | "autoincrement": false 123 | }, 124 | "legacyPushed": { 125 | "name": "legacyPushed", 126 | "type": "integer", 127 | "primaryKey": false, 128 | "notNull": false, 129 | "autoincrement": false 130 | }, 131 | "steamId": { 132 | "name": "steamId", 133 | "type": "text", 134 | "primaryKey": false, 135 | "notNull": false, 136 | "autoincrement": false 137 | }, 138 | "webhookId": { 139 | "name": "webhookId", 140 | "type": "text", 141 | "primaryKey": false, 142 | "notNull": false, 143 | "autoincrement": false 144 | } 145 | }, 146 | "indexes": {}, 147 | "foreignKeys": { 148 | "User_webhookId_Webhook_id_fk": { 149 | "name": "User_webhookId_Webhook_id_fk", 150 | "tableFrom": "User", 151 | "tableTo": "Webhook", 152 | "columnsFrom": [ 153 | "webhookId" 154 | ], 155 | "columnsTo": [ 156 | "id" 157 | ], 158 | "onDelete": "set null", 159 | "onUpdate": "cascade" 160 | } 161 | }, 162 | "compositePrimaryKeys": {}, 163 | "uniqueConstraints": {}, 164 | "checkConstraints": {} 165 | }, 166 | "Webhook": { 167 | "name": "Webhook", 168 | "columns": { 169 | "id": { 170 | "name": "id", 171 | "type": "text", 172 | "primaryKey": true, 173 | "notNull": true, 174 | "autoincrement": false 175 | }, 176 | "value": { 177 | "name": "value", 178 | "type": "text", 179 | "primaryKey": false, 180 | "notNull": true, 181 | "autoincrement": false 182 | }, 183 | "type": { 184 | "name": "type", 185 | "type": "text", 186 | "primaryKey": false, 187 | "notNull": true, 188 | "autoincrement": false 189 | }, 190 | "createdAt": { 191 | "name": "createdAt", 192 | "type": "numeric", 193 | "primaryKey": false, 194 | "notNull": true, 195 | "autoincrement": false, 196 | "default": "(cast(strftime('%s','now') as int))" 197 | } 198 | }, 199 | "indexes": { 200 | "Webhook_value_key": { 201 | "name": "Webhook_value_key", 202 | "columns": [ 203 | "value" 204 | ], 205 | "isUnique": true 206 | } 207 | }, 208 | "foreignKeys": {}, 209 | "compositePrimaryKeys": {}, 210 | "uniqueConstraints": {}, 211 | "checkConstraints": {} 212 | } 213 | }, 214 | "views": {}, 215 | "enums": {}, 216 | "_meta": { 217 | "schemas": {}, 218 | "tables": {}, 219 | "columns": {} 220 | }, 221 | "internal": { 222 | "indexes": {} 223 | } 224 | } -------------------------------------------------------------------------------- /src/drizzle/meta/0001_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "72154605-4801-44fe-b416-f9918ca9bea9", 3 | "prevId": "c1fbe079-cf0d-48ee-9ed9-af299162450b", 4 | "version": "6", 5 | "dialect": "sqlite", 6 | "tables": { 7 | "Guild": { 8 | "name": "Guild", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "text", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "prefix": { 18 | "name": "prefix", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false, 23 | "default": "'pls '" 24 | }, 25 | "language": { 26 | "name": "language", 27 | "type": "text", 28 | "primaryKey": false, 29 | "notNull": true, 30 | "autoincrement": false, 31 | "default": "'en-US'" 32 | }, 33 | "legacyPushed": { 34 | "name": "legacyPushed", 35 | "type": "integer", 36 | "primaryKey": false, 37 | "notNull": false, 38 | "autoincrement": false 39 | }, 40 | "webhookId": { 41 | "name": "webhookId", 42 | "type": "text", 43 | "primaryKey": false, 44 | "notNull": false, 45 | "autoincrement": false 46 | } 47 | }, 48 | "indexes": {}, 49 | "foreignKeys": { 50 | "Guild_webhookId_Webhook_id_fk": { 51 | "name": "Guild_webhookId_Webhook_id_fk", 52 | "tableFrom": "Guild", 53 | "columnsFrom": [ 54 | "webhookId" 55 | ], 56 | "tableTo": "Webhook", 57 | "columnsTo": [ 58 | "id" 59 | ], 60 | "onUpdate": "cascade", 61 | "onDelete": "set null" 62 | } 63 | }, 64 | "compositePrimaryKeys": {}, 65 | "uniqueConstraints": {}, 66 | "checkConstraints": {} 67 | }, 68 | "Pushcart": { 69 | "name": "Pushcart", 70 | "columns": { 71 | "id": { 72 | "name": "id", 73 | "type": "integer", 74 | "primaryKey": true, 75 | "notNull": true, 76 | "autoincrement": true 77 | }, 78 | "userId": { 79 | "name": "userId", 80 | "type": "text", 81 | "primaryKey": false, 82 | "notNull": true, 83 | "autoincrement": false 84 | }, 85 | "guildId": { 86 | "name": "guildId", 87 | "type": "text", 88 | "primaryKey": false, 89 | "notNull": true, 90 | "autoincrement": false 91 | }, 92 | "pushed": { 93 | "name": "pushed", 94 | "type": "integer", 95 | "primaryKey": false, 96 | "notNull": true, 97 | "autoincrement": false 98 | }, 99 | "timestamp": { 100 | "name": "timestamp", 101 | "type": "numeric", 102 | "primaryKey": false, 103 | "notNull": true, 104 | "autoincrement": false, 105 | "default": "(cast(strftime('%s','now') as int))" 106 | } 107 | }, 108 | "indexes": {}, 109 | "foreignKeys": {}, 110 | "compositePrimaryKeys": {}, 111 | "uniqueConstraints": {}, 112 | "checkConstraints": {} 113 | }, 114 | "User": { 115 | "name": "User", 116 | "columns": { 117 | "id": { 118 | "name": "id", 119 | "type": "text", 120 | "primaryKey": true, 121 | "notNull": true, 122 | "autoincrement": false 123 | }, 124 | "legacyPushed": { 125 | "name": "legacyPushed", 126 | "type": "integer", 127 | "primaryKey": false, 128 | "notNull": false, 129 | "autoincrement": false 130 | }, 131 | "steamId": { 132 | "name": "steamId", 133 | "type": "text", 134 | "primaryKey": false, 135 | "notNull": false, 136 | "autoincrement": false 137 | }, 138 | "webhookId": { 139 | "name": "webhookId", 140 | "type": "text", 141 | "primaryKey": false, 142 | "notNull": false, 143 | "autoincrement": false 144 | } 145 | }, 146 | "indexes": {}, 147 | "foreignKeys": { 148 | "User_webhookId_Webhook_id_fk": { 149 | "name": "User_webhookId_Webhook_id_fk", 150 | "tableFrom": "User", 151 | "columnsFrom": [ 152 | "webhookId" 153 | ], 154 | "tableTo": "Webhook", 155 | "columnsTo": [ 156 | "id" 157 | ], 158 | "onUpdate": "cascade", 159 | "onDelete": "set null" 160 | } 161 | }, 162 | "compositePrimaryKeys": {}, 163 | "uniqueConstraints": {}, 164 | "checkConstraints": {} 165 | }, 166 | "Webhook": { 167 | "name": "Webhook", 168 | "columns": { 169 | "id": { 170 | "name": "id", 171 | "type": "text", 172 | "primaryKey": true, 173 | "notNull": true, 174 | "autoincrement": false 175 | }, 176 | "value": { 177 | "name": "value", 178 | "type": "text", 179 | "primaryKey": false, 180 | "notNull": true, 181 | "autoincrement": false 182 | }, 183 | "type": { 184 | "name": "type", 185 | "type": "text", 186 | "primaryKey": false, 187 | "notNull": true, 188 | "autoincrement": false 189 | }, 190 | "createdAt": { 191 | "name": "createdAt", 192 | "type": "numeric", 193 | "primaryKey": false, 194 | "notNull": true, 195 | "autoincrement": false, 196 | "default": "(cast(strftime('%s','now') as int))" 197 | } 198 | }, 199 | "indexes": { 200 | "Webhook_value_key": { 201 | "name": "Webhook_value_key", 202 | "columns": [ 203 | "value" 204 | ], 205 | "isUnique": true 206 | } 207 | }, 208 | "foreignKeys": {}, 209 | "compositePrimaryKeys": {}, 210 | "uniqueConstraints": {}, 211 | "checkConstraints": {} 212 | } 213 | }, 214 | "views": {}, 215 | "enums": {}, 216 | "_meta": { 217 | "columns": {}, 218 | "schemas": {}, 219 | "tables": {} 220 | }, 221 | "internal": { 222 | "indexes": {} 223 | } 224 | } -------------------------------------------------------------------------------- /src/drizzle/meta/0002_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "61c68193-c10a-46e8-8e89-fb53bdb2994e", 3 | "prevId": "72154605-4801-44fe-b416-f9918ca9bea9", 4 | "version": "6", 5 | "dialect": "sqlite", 6 | "tables": { 7 | "Guild": { 8 | "name": "Guild", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "text", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "prefix": { 18 | "name": "prefix", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false, 23 | "default": "'pls '" 24 | }, 25 | "language": { 26 | "name": "language", 27 | "type": "text", 28 | "primaryKey": false, 29 | "notNull": true, 30 | "autoincrement": false, 31 | "default": "'en-US'" 32 | }, 33 | "legacyPushed": { 34 | "name": "legacyPushed", 35 | "type": "integer", 36 | "primaryKey": false, 37 | "notNull": false, 38 | "autoincrement": false 39 | }, 40 | "webhookId": { 41 | "name": "webhookId", 42 | "type": "text", 43 | "primaryKey": false, 44 | "notNull": false, 45 | "autoincrement": false 46 | } 47 | }, 48 | "indexes": {}, 49 | "foreignKeys": { 50 | "Guild_webhookId_Webhook_id_fk": { 51 | "name": "Guild_webhookId_Webhook_id_fk", 52 | "tableFrom": "Guild", 53 | "columnsFrom": [ 54 | "webhookId" 55 | ], 56 | "tableTo": "Webhook", 57 | "columnsTo": [ 58 | "id" 59 | ], 60 | "onUpdate": "cascade", 61 | "onDelete": "set null" 62 | } 63 | }, 64 | "compositePrimaryKeys": {}, 65 | "uniqueConstraints": {}, 66 | "checkConstraints": {} 67 | }, 68 | "Pushcart": { 69 | "name": "Pushcart", 70 | "columns": { 71 | "id": { 72 | "name": "id", 73 | "type": "integer", 74 | "primaryKey": true, 75 | "notNull": true, 76 | "autoincrement": true 77 | }, 78 | "userId": { 79 | "name": "userId", 80 | "type": "text", 81 | "primaryKey": false, 82 | "notNull": true, 83 | "autoincrement": false 84 | }, 85 | "guildId": { 86 | "name": "guildId", 87 | "type": "text", 88 | "primaryKey": false, 89 | "notNull": true, 90 | "autoincrement": false 91 | }, 92 | "pushed": { 93 | "name": "pushed", 94 | "type": "integer", 95 | "primaryKey": false, 96 | "notNull": true, 97 | "autoincrement": false 98 | }, 99 | "timestamp": { 100 | "name": "timestamp", 101 | "type": "numeric", 102 | "primaryKey": false, 103 | "notNull": true, 104 | "autoincrement": false, 105 | "default": "(cast(strftime('%s','now') as int))" 106 | } 107 | }, 108 | "indexes": {}, 109 | "foreignKeys": {}, 110 | "compositePrimaryKeys": {}, 111 | "uniqueConstraints": {}, 112 | "checkConstraints": {} 113 | }, 114 | "User": { 115 | "name": "User", 116 | "columns": { 117 | "id": { 118 | "name": "id", 119 | "type": "text", 120 | "primaryKey": true, 121 | "notNull": true, 122 | "autoincrement": false 123 | }, 124 | "legacyPushed": { 125 | "name": "legacyPushed", 126 | "type": "integer", 127 | "primaryKey": false, 128 | "notNull": false, 129 | "autoincrement": false 130 | }, 131 | "steamId": { 132 | "name": "steamId", 133 | "type": "text", 134 | "primaryKey": false, 135 | "notNull": false, 136 | "autoincrement": false 137 | }, 138 | "webhookId": { 139 | "name": "webhookId", 140 | "type": "text", 141 | "primaryKey": false, 142 | "notNull": false, 143 | "autoincrement": false 144 | } 145 | }, 146 | "indexes": {}, 147 | "foreignKeys": { 148 | "User_webhookId_Webhook_id_fk": { 149 | "name": "User_webhookId_Webhook_id_fk", 150 | "tableFrom": "User", 151 | "columnsFrom": [ 152 | "webhookId" 153 | ], 154 | "tableTo": "Webhook", 155 | "columnsTo": [ 156 | "id" 157 | ], 158 | "onUpdate": "cascade", 159 | "onDelete": "set null" 160 | } 161 | }, 162 | "compositePrimaryKeys": {}, 163 | "uniqueConstraints": {}, 164 | "checkConstraints": {} 165 | }, 166 | "Webhook": { 167 | "name": "Webhook", 168 | "columns": { 169 | "id": { 170 | "name": "id", 171 | "type": "text", 172 | "primaryKey": true, 173 | "notNull": true, 174 | "autoincrement": false 175 | }, 176 | "value": { 177 | "name": "value", 178 | "type": "text", 179 | "primaryKey": false, 180 | "notNull": true, 181 | "autoincrement": false 182 | }, 183 | "type": { 184 | "name": "type", 185 | "type": "text", 186 | "primaryKey": false, 187 | "notNull": true, 188 | "autoincrement": false 189 | }, 190 | "createdAt": { 191 | "name": "createdAt", 192 | "type": "numeric", 193 | "primaryKey": false, 194 | "notNull": true, 195 | "autoincrement": false, 196 | "default": "(cast(strftime('%s','now') as int))" 197 | } 198 | }, 199 | "indexes": { 200 | "Webhook_value_key": { 201 | "name": "Webhook_value_key", 202 | "columns": [ 203 | "value" 204 | ], 205 | "isUnique": true 206 | } 207 | }, 208 | "foreignKeys": {}, 209 | "compositePrimaryKeys": {}, 210 | "uniqueConstraints": {}, 211 | "checkConstraints": {} 212 | } 213 | }, 214 | "views": {}, 215 | "enums": {}, 216 | "_meta": { 217 | "columns": {}, 218 | "schemas": {}, 219 | "tables": {} 220 | }, 221 | "internal": { 222 | "indexes": {} 223 | } 224 | } -------------------------------------------------------------------------------- /src/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1731797902181, 9 | "tag": "0000_far_redwing", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "6", 15 | "when": 1731798010720, 16 | "tag": "0001_rename-languages", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "6", 22 | "when": 1745711020876, 23 | "tag": "0002_normalize-timestamps", 24 | "breakpoints": true 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /src/drizzle/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | integer, 3 | numeric, 4 | sqliteTable, 5 | text, 6 | uniqueIndex, 7 | } from "drizzle-orm/sqlite-core"; 8 | 9 | export const guild = sqliteTable("Guild", { 10 | id: text("id").primaryKey().notNull(), 11 | prefix: text("prefix").default("pls ").notNull(), 12 | language: text("language").default("en-US").notNull(), 13 | legacyPushed: integer("legacyPushed"), 14 | webhookId: text("webhookId").references(() => webhook.id, { 15 | onDelete: "set null", 16 | onUpdate: "cascade", 17 | }), 18 | }); 19 | 20 | export const user = sqliteTable("User", { 21 | id: text("id").primaryKey().notNull(), 22 | legacyPushed: integer("legacyPushed"), 23 | steamId: text("steamId"), 24 | webhookId: text("webhookId").references(() => webhook.id, { 25 | onDelete: "set null", 26 | onUpdate: "cascade", 27 | }), 28 | }); 29 | 30 | export const webhook = sqliteTable( 31 | "Webhook", 32 | { 33 | id: text("id").primaryKey().notNull(), 34 | value: text("value").notNull(), 35 | type: text("type").notNull(), 36 | createdAt: numeric("createdAt") 37 | .notNull(), 38 | }, 39 | (table) => { 40 | return { 41 | valueKey: uniqueIndex("Webhook_value_key").on(table.value), 42 | }; 43 | }, 44 | ); 45 | 46 | export const pushcart = sqliteTable("Pushcart", { 47 | id: integer("id").primaryKey({ autoIncrement: true }).notNull(), 48 | userId: text("userId").notNull(), 49 | guildId: text("guildId").notNull(), 50 | pushed: integer("pushed").notNull(), 51 | timestamp: numeric("timestamp") 52 | .notNull(), 53 | }); 54 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "#utils/setup.ts"; 2 | import { container } from "@sapphire/framework"; 3 | import { PayloadClient } from "#lib/PayloadClient.ts"; 4 | 5 | const client = new PayloadClient(); 6 | 7 | try { 8 | await client.login(); 9 | } catch (error) { 10 | container.logger.error(error); 11 | client.destroy(); 12 | Deno.exit(1); 13 | } 14 | -------------------------------------------------------------------------------- /src/interaction-handlers/createUserWebhook.ts: -------------------------------------------------------------------------------- 1 | import { LanguageKeys } from "#lib/i18n/all"; 2 | import { user, webhook } from "#root/drizzle/schema.ts"; 3 | import { InteractionHandler, InteractionHandlerTypes } from "@sapphire/framework"; 4 | import { fetchT } from "@sapphire/plugin-i18next"; 5 | import { generate } from "generate-password"; 6 | import { ActionRowBuilder, ButtonBuilder, type ButtonInteraction, ButtonStyle, codeBlock, ContainerBuilder } from "discord.js"; 7 | import { eq } from "drizzle-orm"; 8 | import { isNullOrUndefinedOrEmpty } from "@sapphire/utilities"; 9 | import { MessageFlags } from "discord.js"; 10 | import { TextDisplayBuilder } from "discord.js"; 11 | 12 | export class ButtonHandler extends InteractionHandler { 13 | public constructor( 14 | ctx: InteractionHandler.LoaderContext, 15 | options: InteractionHandler.Options, 16 | ) { 17 | super(ctx, { 18 | ...options, 19 | interactionHandlerType: InteractionHandlerTypes.Button, 20 | }); 21 | } 22 | 23 | async #updateInteractionWithDelete(interaction: ButtonInteraction, webhookSecret: string) { 24 | const t = await fetchT(interaction); 25 | 26 | const deleteWebhookButton = new ActionRowBuilder().addComponents( 27 | new ButtonBuilder({ 28 | label: t(LanguageKeys.Commands.Webhook.DeleteWebhook), 29 | customId: "delete-webhook-for-user", 30 | style: ButtonStyle.Danger, 31 | }), 32 | ); 33 | 34 | const description = new TextDisplayBuilder() 35 | .setContent(t(LanguageKeys.Commands.Webhook.EmbedDescription, { secret: codeBlock(webhookSecret) })); 36 | 37 | const container = new ContainerBuilder() 38 | .addTextDisplayComponents(description) 39 | .addActionRowComponents(deleteWebhookButton); 40 | 41 | await interaction.update({ 42 | flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2, 43 | components: [container], 44 | }); 45 | } 46 | 47 | public override parse(interaction: ButtonInteraction) { 48 | if (interaction.customId !== "create-webhook-for-user") { 49 | return this.none(); 50 | } 51 | 52 | return this.some(); 53 | } 54 | 55 | public async run(interaction: ButtonInteraction) { 56 | const [userWebhook] = await this.container.database 57 | .select({ secret: webhook.value }) 58 | .from(webhook) 59 | .where(eq(webhook.id, interaction.user.id)); 60 | 61 | if (!isNullOrUndefinedOrEmpty(userWebhook?.secret)) { 62 | await this.#updateInteractionWithDelete(interaction, userWebhook.secret); 63 | 64 | return; 65 | } 66 | 67 | const secret = generate({ numbers: true, length: 24 }); 68 | 69 | const [createdWebhook] = await this.container.database 70 | .insert(webhook) 71 | .values({ 72 | id: interaction.user.id, 73 | type: "users", 74 | value: secret, 75 | createdAt: Temporal.Now.instant().epochMilliseconds.toString(), 76 | }) 77 | .returning(); 78 | 79 | await this.container.database 80 | .insert(user) 81 | .values({ 82 | id: interaction.user.id, 83 | webhookId: createdWebhook.id, 84 | }) 85 | .onConflictDoUpdate({ 86 | set: { 87 | webhookId: createdWebhook.id, 88 | }, 89 | target: user.id, 90 | }); 91 | 92 | await this.#updateInteractionWithDelete(interaction, createdWebhook.value); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/interaction-handlers/deleteUserWebhook.ts: -------------------------------------------------------------------------------- 1 | import { LanguageKeys } from "#lib/i18n/all"; 2 | import { webhook } from "#root/drizzle/schema.ts"; 3 | import { InteractionHandler, InteractionHandlerTypes } from "@sapphire/framework"; 4 | import { fetchT } from "@sapphire/plugin-i18next"; 5 | import { ActionRowBuilder, ButtonBuilder, type ButtonInteraction, ButtonStyle, ContainerBuilder, MessageFlags, TextDisplayBuilder } from "discord.js"; 6 | import { eq } from "drizzle-orm"; 7 | 8 | export class ButtonHandler extends InteractionHandler { 9 | public constructor(ctx: InteractionHandler.LoaderContext, options: InteractionHandler.Options) { 10 | super(ctx, { 11 | ...options, 12 | interactionHandlerType: InteractionHandlerTypes.Button, 13 | }); 14 | } 15 | 16 | public override parse(interaction: ButtonInteraction) { 17 | if (interaction.customId !== "delete-webhook-for-user") { 18 | return this.none(); 19 | } 20 | 21 | return this.some(); 22 | } 23 | 24 | public async run(interaction: ButtonInteraction) { 25 | const t = await fetchT(interaction); 26 | 27 | const createWebhookButton = new ActionRowBuilder().addComponents( 28 | new ButtonBuilder({ 29 | label: t(LanguageKeys.Commands.Webhook.CreateWebhook), 30 | customId: "create-webhook-for-user", 31 | style: ButtonStyle.Primary, 32 | }), 33 | ); 34 | 35 | const description = new TextDisplayBuilder() 36 | .setContent(t(LanguageKeys.Commands.Webhook.NoWebhook)); 37 | 38 | const container = new ContainerBuilder() 39 | .addTextDisplayComponents(description) 40 | .addActionRowComponents(createWebhookButton); 41 | 42 | await this.container.database.delete(webhook) 43 | .where(eq(webhook.id, interaction.user.id)); 44 | 45 | await interaction.update({ 46 | flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2, 47 | components: [container], 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/interaction-handlers/setGuildLanguage.ts: -------------------------------------------------------------------------------- 1 | import { LanguageKeys } from "#lib/i18n/all/index.ts"; 2 | import { guild } from "#root/drizzle/schema.ts"; 3 | import { InteractionHandler, InteractionHandlerTypes } from "@sapphire/framework"; 4 | import { fetchT } from "@sapphire/plugin-i18next"; 5 | import { isNullOrUndefinedOrEmpty } from "@sapphire/utilities"; 6 | import { inlineCode, type ButtonInteraction, type StringSelectMenuInteraction } from "discord.js"; 7 | 8 | export class ButtonHandler extends InteractionHandler { 9 | public constructor(ctx: InteractionHandler.LoaderContext, options: InteractionHandler.Options) { 10 | super(ctx, { 11 | ...options, 12 | interactionHandlerType: InteractionHandlerTypes.SelectMenu, 13 | }); 14 | } 15 | 16 | public override parse(interaction: ButtonInteraction) { 17 | if (interaction.customId !== "set-guild-language") { 18 | return this.none(); 19 | } 20 | 21 | return this.some(); 22 | } 23 | 24 | public async run(interaction: StringSelectMenuInteraction) { 25 | const t = await fetchT(interaction); 26 | const value = interaction.values[0]; 27 | 28 | if (isNullOrUndefinedOrEmpty(value)) { 29 | return; 30 | } 31 | 32 | await this.container.database 33 | .insert(guild) 34 | .values({ 35 | id: interaction.guildId!, 36 | language: value, 37 | }) 38 | .onConflictDoUpdate({ 39 | set: { 40 | language: value, 41 | }, 42 | target: guild.id, 43 | }); 44 | 45 | await interaction.reply({ 46 | content: t(LanguageKeys.Commands.Language.SetLanguage, { language: inlineCode(value) }), 47 | ephemeral: true, 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/languages/de/arguments.json: -------------------------------------------------------------------------------- 1 | { 2 | "booleanError": "Ich konnte `{{ parameter }}` nicht zu einem Boolean auflösen, der einzig mögliche Wert ist: {{ possibles, list }}", 3 | "missing": "Dir fehlt ein Argument! Verwende `{{ commandContext.commandPrefix }}help {{ command.name }}` um herauszufinden, wie man diesen Befehl verwendet.", 4 | "channelError": "Ich konnte `{{ parameter }}` nicht zu einem Kanal auflösen. Stelle sicher das du den Namen oder die ID richtig angegeben hast!", 5 | "command": "Ich konnte `{{ parameter }}` nicht zu einem Befehl auflösen. Stelle sicher das du den Namen oder einen der Aliase richtig angegeben hast!", 6 | "dateError": "Ich konnte `{{ parameter }}` nicht zu einem Datum auflösen. Hier sind eine paar gültige Formate:\n\n$t(arguments:dateFormats)", 7 | "dateTooEarly": "Der Parameter `{{ parameter }}` wurde zu einem Datum früher als {{minimum, dateTime} aufgelöst, was nicht erlaubt ist!", 8 | "dateTooFar": "Der Parameter `{{ parameter }}` wurde zu einem Datum älter als {{maximum, dateTime}} aufgelöst, was nicht erlaubt ist!", 9 | "dmChannelError": "Ich konnte `{{ parameter }}` nicht zu einem DM-Kanal auflösen, stelle sicher, dass du seine ID korrekt eingegeben hast!", 10 | "floatError": "Ich konnte `{{ parameter }}` nicht zu einer Nummer auflösen!", 11 | "floatTooLarge": "Der Parameter `{{ parameter }}` ist zu hoch! Bitte geben Sie einen Wert kleiner als {{ maximum }} an!", 12 | "floatTooSmall": "Der Parameter `{{ parameter }}` ist zu niedrig! Bitte geben Sie einen Wert größer als {{ minimum }} an!", 13 | "guildChannelError": "Ich konnte `{{ parameter }}` nicht zu einem Kanal von diesem Server auflösen, bitte stelle sicher, dass du den Namen oder die ID korrekt eingegeben hast!", 14 | "guildChannelMissingGuildError": "Ich konnte `{{ parameter }}` nicht auflösen, da dieses Argument in einem Serverkanal ausgeführt werden muss.", 15 | "guildPrivateThreadChannelError": "Ich konnte `{{ parameter }}` nicht zu einem privaten Thread-Kanal auflösen, bitte stelle sicher, dass du den Namen oder die ID korrekt eingegeben hast!", 16 | "guildPublicThreadChannelError": "Ich konnte `{{ parameter }}` nicht in einen öffentlichen Thread-Kanal auflösen, bitte stelle sicher, dass du den Namen oder die ID korrekt eingegeben hast!", 17 | "guildStageVoiceChannelError": "Ich konnte `{{ parameter }}` nicht zu einem Stage-Sprachkanal auflösen, bitte stelle sicher, dass du den Namen oder die ID korrekt eingegeben hast!", 18 | "guildTextChannelError": "Ich konnte `{{ parameter }}` nicht zu einem Textkanal auflösen, bitte stelle sicher, dass du den Namen oder die ID korrekt eingegeben hast!", 19 | "guildThreadChannelError": "Ich konnte `{{ parameter }}` nicht zu einem Thread-Kanal auflösen, bitte stelle sicher, dass du den Namen oder die ID korrekt eingegeben hast!", 20 | "guildVoiceChannelError": "Ich konnte `{{ parameter }}` nicht zu einem Sprachkanal auflösen, bitte stelle sicher, dass du den Namen oder die ID korrekt eingegeben hast!", 21 | "guildNewsChannelError": "Ich konnte `{{ parameter }}` nicht zu einem Ankündigungskanal auflösen, bitte stelle sicher, dass du den Namen oder die ID korrekt eingegeben hast!", 22 | "hyperlinkError": "Ich konnte `{{ parameter }}` nicht zu einen Hyperlink auflösen! Sie sind normalerweise formatiert wie `https://discord.com`.", 23 | "integerError": "Ich konnte `{{ parameter }}` nicht zu einer ganzen Zahl auflösen!", 24 | "integerTooLarge": "Der Parameter `{{ parameter }}` ist zu hoch! Bitte geben Sie einen Wert kleiner als {{ maximum }} an!", 25 | "integerTooSmall": "Der Parameter `{{ parameter }}` ist zu niedrig! Bitte geben Sie einen Wert größer als {{ minimum }} an!", 26 | "memberError": "Ich konnte `{{ parameter }}` nicht auf ein Mitglied von diesem Server auflösen, bitte stelle sicher, dass du den Namen oder die ID korrekt eingegeben hast!", 27 | "memberMissingGuild": "Ich konnte `{{ parameter }}` nicht auflösen, da dieses Argument in einem Serverkanal ausgeführt werden muss.", 28 | "messageError": "Ich konnte `{{ parameter }}` nicht zu einer Nachricht auflösen!", 29 | "numberError": "Ich konnte `{{ parameter }}` nicht zu einer Nummer auflösen!", 30 | "numberTooLarge": "Der Parameter `{{ parameter }}` ist zu hoch! Bitte geben Sie einen Wert kleiner als {{ maximum }} an!", 31 | "numberTooSmall": "Der Parameter `{{ parameter }}` ist zu niedrig! Bitte geben Sie einen Wert größer als {{ minimum }} an!", 32 | "roleError": "Ich konnte `{{ parameter }}` nicht zu einer Rolle auflösen, bitte stelle sicher, dass du den Namen oder die ID korrekt eingegeben hast!", 33 | "stringTooLong": "Der Parameter `{{ parameter }}` ist zu lang! Er muss weniger als {{ maximum }} Zeichen haben!", 34 | "stringTooShort": "Der Parameter `{{ parameter }}` ist zu kurz! Er muss mindestens {{ minimum }} Zeichen haben!", 35 | "userError": "Ich konnte `{{ parameter }}` nicht an einen Benutzer auflösen, bitte stelle sicher, dass du seine ID korrekt eingegeben hast!" 36 | } 37 | -------------------------------------------------------------------------------- /src/languages/de/auto/connect.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Stellt eine kurze Beschreibung eines Servers mit einem praktischen Link zur Verbindung dar!", 3 | "descriptionLink": "Stellt eine kurze Beschreibung eines Servers mit einem praktischen Link zur Verbindung dar!", 4 | "players": "Spieler", 5 | "offline": "Server ist offline (Hat er schon gestartet?)" 6 | } 7 | -------------------------------------------------------------------------------- /src/languages/de/auto/etf2l.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Zeigt eine Vorschau eines ETF2L-Teams an", 3 | "matchesDescription": "Zeigt eine Vorschau eines ETF2L-Spiels an", 4 | "embedTitle": "ETF2L Teamvorschau", 5 | "matchEmbedTitle": "ETF2L Spielvorschau" 6 | } 7 | -------------------------------------------------------------------------------- /src/languages/de/auto/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Zeigt eine Vorschau eines logs.tf Spiels an", 3 | "embedTitle": "Logs.tf Spielvorschau" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/de/auto/rgl.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Zeigt eine Vorschau eines RGL-Teams an", 3 | "embedTitle": "RGL Team Vorschau" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/de/auto/tftv.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Zeigt eine Vorschau eines TFTV-Beitrags an", 3 | "noPostFound": "Der Beitrag {{ post }} konnte nicht gefunden weren" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/de/auto/ugc.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Zeigt eine Vorschau eines UGC-Teams an", 3 | "embedTitle": "UGC Team Vorschau" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/de/commands/8ball.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Stellt dem magischen 8-Ball eine Frage.", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "Du musst eine Frage angeben." 8 | }, 9 | "noQuestion": "Du musst mir eine Frage stellen!" 10 | } 11 | -------------------------------------------------------------------------------- /src/languages/de/commands/avatar.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Zeigt das Profilbild eines Benutzers an", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "Erwähne einen Benutzer, oder gebe die ID eines Benutzers an und dieser Befehl wird dessen Avatar zurückgeben. Standardmäßig wird der Autor verwendet, wenn kein Benutzer angeben wird." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/languages/de/commands/bruh.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Bruh", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "Wenn Sie einen Benutzer in diesem Befehl erwähnen, wird er ihn anpingen." 8 | }, 9 | "noMention": "bruh", 10 | "mention": "bruh {{ mention }}" 11 | } 12 | -------------------------------------------------------------------------------- /src/languages/de/commands/changelog.json: -------------------------------------------------------------------------------- 1 | { 2 | "invalidFormat": "Ungültiges Versionsformat", 3 | "invalidVersion": "Ungültige Version" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/de/commands/choose.json: -------------------------------------------------------------------------------- 1 | { 2 | "noOptions": "Ich habe nichts zum Auswählen!", 3 | "chosen_one": "Ich wähle {{ options }}!", 4 | "chosen_other": "Ich habe Folgendes ausgewählt: {{ options }}" 5 | } 6 | -------------------------------------------------------------------------------- /src/languages/de/commands/commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Eine Liste der verfügbaren Befehle für Payload", 3 | "detailedDescription": { 4 | "usages": [], 5 | "details": "Hilfreiche Liste von Befehlen. Nützlich zum einschränken von Befehlen, vor allem über die du nicht weißt und nicht willst das andere Personen sie nutzen." 6 | }, 7 | "embedTitle": "Liste der Befehle", 8 | "commands": "Befehle", 9 | "auto": "Auto-Befehle" 10 | } 11 | -------------------------------------------------------------------------------- /src/languages/de/commands/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "button": "Zum Dashboard", 3 | "content": "Zum Dashboard" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/de/commands/findping.json: -------------------------------------------------------------------------------- 1 | { 2 | "noPings": "Du wurdest noch nicht erwähnt" 3 | } 4 | -------------------------------------------------------------------------------- /src/languages/de/commands/help.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Gibt Hilfe zu einem Befehl an.", 3 | "detailedDescription": { 4 | "usages": [ 5 | "[command]" 6 | ], 7 | "details": "Brauchst du Hilfe bei einem Befehl? Dies gibt Dir die Verwendung des Befehls mit den Argumenten, die er akzeptieren kann, sowie einige zusätzliche Details über den Befehl." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/languages/de/commands/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Informationen zu Payload", 3 | "detailedDescription": { 4 | "usages": [], 5 | "details": "Grundlegende Informationen und Links zu Payload" 6 | }, 7 | "embedTitle": "Bediene {{ users }} Benutzer auf {{ servers }} Servern!", 8 | "embedDescription": "Trete dem offiziellen Payload Discord Server bei für Hilfe und Anregungen: https://payload.tf/discord\n\nLade Payload auf deinen Server ein: https://payload.tf/invite\nHelfe bei der Übersetzung von Payload: https://crowdin.com/project/payload" 9 | } 10 | -------------------------------------------------------------------------------- /src/languages/de/commands/invite.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Lade Payload auf deinen Discord ein!", 3 | "detailedDescription": { 4 | "usages": [], 5 | "details": "Lade mich auf deinen Discord ein!" 6 | }, 7 | "button": "Klicke hier" 8 | } 9 | -------------------------------------------------------------------------------- /src/languages/de/commands/language.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Verwaltet deine Sprache für diesen Server", 3 | "detailedDescription": { 4 | "usages": [ 5 | "", 6 | "setze ", 7 | "aktualisiere ", 8 | "delete", 9 | "reset", 10 | "view" 11 | ], 12 | "details": "Verwalte das Präfix deines Servers. Du kannst es jederzeit mit den `delete` oder `reset` Argumenten zurücksetzen." 13 | }, 14 | "currentLanguage": "Die aktuelle Sprache ist: {{ language }}", 15 | "setNeedsArgs": "Bitte gebe eine neue Sprache an!", 16 | "setSameLanguage": "Deine neue Sprache ist die gleiche wie die alte Sprache!", 17 | "setLanguageEmbedTitle": "Sprache aktualisiert von {{ user }}", 18 | "setLanguageEmbedDesc": "Sprache von {{ old }} auf {{ new }} aktualisiert", 19 | "deleteAlreadyDefault": "Deine Sprache ist bereits der Standard!" 20 | } 21 | -------------------------------------------------------------------------------- /src/languages/de/commands/link.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Verknüpft eine SteamID des Benutzers mit Payload", 3 | "detailedDescription": { 4 | "usages": [ 5 | "[steamid]" 6 | ], 7 | "details": "Um dich richtig zu verknüpfen, musst du zuerst eine valide SteamID, in jeglichem Format, angeben. Wenn du willst kannst du auch deinen Steam Community ID link angeben z.B " 6 | ], 7 | "details": "Ruft den letzten Log eines Benutzers ab. **Anmerkung**: Der Angegebene Benutzer **muss** zuerst seine SteamID über Payload verknüpft haben." 8 | }, 9 | "noIdLinked": "{{ user }} hat seine SteamID nicht verlinkt!", 10 | "noHistory": "Spieler hat keinen Log-Verlauf!", 11 | "button": "Besuche Log" 12 | } 13 | -------------------------------------------------------------------------------- /src/languages/de/commands/prefix.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Verwaltet dein Präfix für diesen Server", 3 | "detailedDescription": { 4 | "usages": [ 5 | "", 6 | "set ", 7 | "update ", 8 | "delete", 9 | "reset", 10 | "view" 11 | ], 12 | "details": "Verwalte das Präfix deines Servers. Du kannst es jederzeit mit den `delete` oder `reset` Argumenten zurücksetzen." 13 | }, 14 | "currentPrefix": "Das aktuelle Präfix ist: {{ prefix }}", 15 | "setNeedsArgs": "Bitte gebe ein neues Präfix an!", 16 | "setSamePrefix": "Dein neues Präfix ist das gleiche wie das alte Präfix!", 17 | "setPrefixEmbedTitle": "Präfix aktualisiert von {{ user }}", 18 | "setPrefixEmbedDesc": "Präfix wurde aktualisiert von {{ old }} auf {{ new }}", 19 | "deleteAlreadyDefault": "Dein Präfix ist bereits der Standard!" 20 | } 21 | -------------------------------------------------------------------------------- /src/languages/de/commands/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Grundlegende Informationen über das Profil eines Benutzers", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "Möchtest du grundlegende Informationen über einen Benutzer, wie z.B. deren pushcart Punkte, abrufen? Dann kannst du diesen Befehl verwenden um zu erfahren ob sie dir auch wirklich helfen die Lore zu schieben." 8 | }, 9 | "bot": "Bot", 10 | "points": "Punkte" 11 | } 12 | -------------------------------------------------------------------------------- /src/languages/de/commands/purge.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Löscht Nachrichten in Masse in einem Text-Kanal", 3 | "detailedDescription": { 4 | "usages": [ 5 | " [...mentions]" 6 | ], 7 | "details": "Löscht von Nachrichten, maximal 100, aus dem aktuellen Kanal. **Tipp**: Du kannst auch Benutzer erwähnen die Auswahl zu filtern!" 8 | }, 9 | "deleted_one": "🗑 Es wurde **{{ count }}** Nachricht in {{ seconds }} Sekunden gelöscht.", 10 | "deleted_other": "🗑 Es wurden **{{ count }}** Nachrichten in {{ seconds }} Sekunden gelöscht." 11 | } 12 | -------------------------------------------------------------------------------- /src/languages/de/commands/pushcart.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Pushcart Rangliste", 3 | "detailedDescription": { 4 | "usages": [ 5 | "", 6 | "Rangliste", 7 | "Statistiken", 8 | "Rang " 9 | ], 10 | "details": "Schiebe die Lore! Dies ist eine globale Rangliste, und Du kannst die aktuellen Ränge jederzeit unter Verwendung der aufgeführten Unterbefehle ansehen." 11 | }, 12 | "cooldown": "Du musst {{ seconds }} Sekunden warten bevor du die Lore erneut schieben kannst!", 13 | "maxpoints": "Du hast die maximale Anzahl an Punkten für heute erreicht. Komm zurück in {{ expires }}!", 14 | "pushSuccess": "{{ PUSHCART_EMOJI }} Schiebt die Lore **{{ units }}** Fuß vorwärts ({{ total, number }} insgesamt).", 15 | "rank_zero": "{{ name }} hat die Lore noch nicht geschoben.", 16 | "rank_one": "{{ rank }}: {{ name }} ({{ count, number }})", 17 | "rank_other": "{{ rank }}: {{ name }} ({{ count, number }})", 18 | "statsTitle": "{{ name }}'s Pushcart-Statistik", 19 | "topPushers": "Top Pushers", 20 | "noPushesYet": "Noch niemand hat die Lore geschoben. Sei der Erste!", 21 | "activePushers": "Die aktivsten Pusher", 22 | "distinctPushersTitle": "Eindeutige Pusher", 23 | "distinctPushers_one": "Nur **{{ count, number }}** Benutzer haben die Lore geschoben. Traurig!", 24 | "distinctPushers_other": "Es gibt **{{ count, number }}** eindeutige Pusher", 25 | "totalPushedTitle": "Anzahl der Pushes", 26 | "totalPushed_one": "Der Lore wurde **{{ count, number }}** mal geschoben", 27 | "totalPushed_other": "Der Lore wurde **{{ count, number }}** mal geschoben", 28 | "totalUnitsPushedTitle": "Anzahl der gepushten Einheiten", 29 | "totalUnitsPushed_one": "Nur **{{ count, number }}** Einheit wurde geschoben", 30 | "totalUnitsPushed_other": "Insgesamt wurden**{{ count, number }}** Einheiten geschoben", 31 | "leaderboardEmbedTitle": "Pushcart Rangliste", 32 | "serverEmbedTitle": "Pushcart Server Rangliste" 33 | } 34 | -------------------------------------------------------------------------------- /src/languages/de/commands/restrict.json: -------------------------------------------------------------------------------- 1 | { 2 | "noCommands": "Du musst angeben, welche Befehle eingeschränkt werden sollen!", 3 | "restrictSuccess": "Folgende Befehle wurden einschränkt: {{ commands }}" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/de/commands/rtd.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Wirft einen Würfel", 3 | "detailedDescription": { 4 | "usages": [ 5 | "", 6 | " " 7 | ], 8 | "details": "Würfel! Du kannst Einstellen wie viele Seiten die Würfel haben, sowie wie oft sie geworfen werden sollen." 9 | } 10 | } -------------------------------------------------------------------------------- /src/languages/de/commands/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "button": "Benutzereinstellungen", 3 | "message": "Aktualisiere deine Einstellungen über das Dashboard!" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/de/commands/snipe.json: -------------------------------------------------------------------------------- 1 | { 2 | "noMessages": "Keine nachrichten zum anzeigen!", 3 | "aboveCacheAmount": "Snipe-Cache geht nicht so weit!" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/de/commands/translate.json: -------------------------------------------------------------------------------- 1 | { 2 | "noPhrase": "Es wurde keine Eingabe angegeben", 3 | "title": "Ergebnis übersetzen", 4 | "footer": "Übersetzt von Google Translate", 5 | "error": "Fehler während der Übersetzung" 6 | } 7 | -------------------------------------------------------------------------------- /src/languages/de/commands/unrestrict.json: -------------------------------------------------------------------------------- 1 | { 2 | "noCommands": "Du musst angeben, welche Befehle uneingeschränkt sein sollen!", 3 | "unrestrictSuccess": "Uneingeschränkt: {{ commands }}" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/de/commands/webhook.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook", 3 | "description": "Stellt eine Reihe von Aktionen bereit, um mit der Payload Webhook Integration zu arbeiten", 4 | "detailedDescription": { 5 | "usages": [], 6 | "details": "Siehe mehr auf (Github)[https://github.com/payload-bot/payload-logs-plugin]" 7 | }, 8 | "embedTitle": "Deine Webhook", 9 | "noWebhook": "Noch kein Webhook erstellt", 10 | "createWebhook": "Webhook erstellen", 11 | "deleteWebhook": "Webhook löschen", 12 | "deletedWebhook": "Gelöschte Webhook" 13 | } -------------------------------------------------------------------------------- /src/languages/de/globals.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": "Ich vermisse dafür einen Schlüssel. Melde dich bei uns damit es gefixt wird!", 3 | "autoEmbedFooter": "Dargestekkt von autoresponse {{ name }}" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/de/preconditions.json: -------------------------------------------------------------------------------- 1 | { 2 | "clientPermissions": "Ich habe nicht genügend Berechtigungen! Ich brauche: {{ missing }}", 3 | "cooldown": "Du hast vor kurzem diesen Befehl benutzt. Du kannst diesen Befehlt erneut ausführen in {{ remaining }}.", 4 | "dmOnly": "Dieser Befehl kann nur in DM-Kanälen benutzt werden.", 5 | "guildNewsOnly": "Dieser Befehl kann nur in Ankündigungs-Kanälen verwendet werden.", 6 | "guildNewsThreadOnly": "Dieser Befehl kann nur in Ankündiguns-Thread-Kanälen benutzt werden.", 7 | "guildOnly": "Dieser Befehl kann nur in Server-Kanälen benutzt werden.", 8 | "guildPrivateThreadOnly": "Dieser Befehl kann nur in privaten Thread-Kanälen benutzt werden.", 9 | "guildPublicThreadOnly": "Dieser Befehl kann nur in öffentlichen Thread-Kanälen benutzt werden.", 10 | "guildTextOnly": "Dieser Befehl kann nur in Text-Kanälen benutzt werden.", 11 | "nsfw": "Du darfst NSFW-Befehle in diesem Kanal nicht verwenden!", 12 | "threadOnly": "Dieser Befehl kann nur in Thread-Kanälen benutzt werden.", 13 | "userPermissions": "Du hast nicht genügend Berechtigungen! Du brauchst: {{ missing }}" 14 | } 15 | -------------------------------------------------------------------------------- /src/languages/de/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "helpTitles": { 3 | "description": "**Beschreibung**", 4 | "aliases": "**Aliase**", 5 | "usages": "**Verwendungen**", 6 | "moreDetails": "**Weitere Details**" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/languages/en-US/arguments.json: -------------------------------------------------------------------------------- 1 | { 2 | "booleanError": "I could not resolve `{{ parameter }}` to a boolean, the only possible value is: {{ possibles, list }}", 3 | "missing": "You're missing an argument! You should do `{{ commandContext.commandPrefix }}help {{ command.name }}` to find out how to use this command.", 4 | "channelError": "I could not resolve `{{ parameter }}` to a channel, make sure you typed its name or ID correctly!", 5 | "command": "I could not resolve `{{ parameter }}` to a command! Make sure you typed its name or one of its aliases correctly!", 6 | "dateError": "I could not resolve `{ {parameter}}` to a date, there are some of the valid formats:\n\n$t(arguments:dateFormats)", 7 | "dateTooEarly": "The parameter `{{ parameter }}` resolved to a date earlier than {{minimum, dateTime}}, which is not allowed!", 8 | "dateTooFar": "The parameter `{{ parameter }}` resolved to a date older than {{maximum, dateTime}}, which is not allowed!", 9 | "dmChannelError": "I could not resolve `{{ parameter }}` to a DM channel, make sure you typed its ID correctly!", 10 | "floatError": "I could not resolve `{{ parameter }}` to a number!", 11 | "floatTooLarge": "The parameter `{{ parameter }}` is too high! Please provid a value less than {{ maximum }}!", 12 | "floatTooSmall": "The parameter `{{ parameter }}` is too low! Please provide a value greater than {{ minimum }}!", 13 | "guildChannelError": "I could not resolve `{{ parameter }}` to a channel from this server, please make sure you typed its name or ID correctly!", 14 | "guildChannelMissingGuildError": "I was not able to resolve `{{ parameter }}` because this argument requires to be run in a server channel.", 15 | "guildPrivateThreadChannelError": "I could not resolve `{{ parameter }}` to a private thread channel, please make sure you typed its name or ID correctly!", 16 | "guildPublicThreadChannelError": "I could not resolve `{{ parameter }}` to a public thread channel, please make sure you typed its name or ID correctly!", 17 | "guildStageVoiceChannelError": "I could not resolve `{{ parameter }}` to a stage voice channel, please make sure you typed its name or ID correctly!", 18 | "guildTextChannelError": "I could not resolve `{{ parameter }}` to a text channel, please make sure you typed its name or ID correctly!", 19 | "guildThreadChannelError": "I could not resolve `{{ parameter }}` to a thread channel, please make sure you typed its name or ID correctly!", 20 | "guildVoiceChannelError": "I could not resolve `{{ parameter }}` to a voice channel, please make sure you typed its name or ID correctly!", 21 | "guildNewsChannelError": "I could not resolve `{{ parameter }}` to an announcement channel, please make sure you typed its name or ID correctly!", 22 | "hyperlinkError": "I could not resolve `{{ parameter }}` to an hyperlink! They're usually formatted like `https://discord.com`.", 23 | "integerError": "I could not resolve `{{ parameter }}` to an integer!", 24 | "integerTooLarge": "The parameter `{{ parameter }}` is too high! Please provid a value less than {{ maximum }}!", 25 | "integerTooSmall": "The parameter `{{ parameter }}` is too low! Please provide a value greater than {{ minimum }}!", 26 | "memberError": "I could not resolve `{{ parameter }}` to a member from this server, please make sure you typed their name or ID correctly!", 27 | "memberMissingGuild": "I was not able to resolve `{{ parameter }}` because this argument requires to be run in a server channel.", 28 | "messageError": "I could not resolve `{{ parameter }}` to a message!", 29 | "numberError": "I could not resolve `{{ parameter }}` to a number!", 30 | "numberTooLarge": "The parameter `{{ parameter }}` is too high! Please provid a value less than {{ maximum }}!", 31 | "numberTooSmall": "The parameter `{{ parameter }}` is too low! Please provide a value greater than {{ minimum }}!", 32 | "roleError": "I could not resolve `{{ parameter }}` to a role, please make sure you typed its name or ID correctly!", 33 | "stringTooLong": "The parameter `{{ parameter }}` is too long! It needs to have less than {{ maximum }} character(s)!", 34 | "stringTooShort": "The parameter `{{ parameter }}` is too short! It needs to have at least {{ minimum }} character(s)!", 35 | "userError": "I could not resolve `{{ parameter }}` to a user, please make sure you typed their ID correctly!" 36 | } 37 | -------------------------------------------------------------------------------- /src/languages/en-US/auto/connect.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Renders a short description of a server, with a handy link to connect to it!", 3 | "descriptionLink": "Renders a short description of a server, with a handy link to connect to it!", 4 | "players": "players", 5 | "offline": "Server is offline (Has it started yet?)" 6 | } 7 | -------------------------------------------------------------------------------- /src/languages/en-US/auto/etf2l.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Renders a preview of an ETF2L team", 3 | "matchesDescription": "Renders a preview of an ETF2L match", 4 | "embedTitle": "ETF2L Team Preview", 5 | "matchEmbedTitle": "ETF2L Match Preview" 6 | } 7 | -------------------------------------------------------------------------------- /src/languages/en-US/auto/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Renders a preview of a logs.tf game", 3 | "embedTitle": "Logs.tf Match Preview" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/en-US/auto/rgl.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Renders a preview of an RGL team", 3 | "embedTitle": "RGL Team Preview" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/en-US/auto/tftv.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Renders a preview of a TFTV post", 3 | "noPostFound": "Could not find post {{ post }}" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/en-US/auto/ugc.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Renders a preview of a UGC team", 3 | "embedTitle": "UGC Team Preview" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/en-US/commands/commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "A list of commands available in Payload", 3 | "detailedDescription": { 4 | "usages": [], 5 | "details": "Handly list of commands. Useful for restricting or unrestricting commands you don't know about, but don't want people to use." 6 | }, 7 | "embedTitle": "List of Commands", 8 | "commands": "Commands", 9 | "auto": "Auto Commands" 10 | } 11 | -------------------------------------------------------------------------------- /src/languages/en-US/commands/help.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Gets help on a command.", 3 | "detailedDescription": { 4 | "usages": [ 5 | "[command]" 6 | ], 7 | "details": "Need some help on a command? This will give you the usages of the command with the arguments it can accept, and some additional details about the command." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/languages/en-US/commands/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "info", 3 | "description": "Information about Payload", 4 | "detailedDescription": { 5 | "usages": [], 6 | "details": "Basic information and links for Payload" 7 | }, 8 | "embedTitle": "Serving {{ users }} users in {{ servers }} servers!", 9 | "embedDescription": "Join the official Payload discord server for help and suggestions: https://discord.com/invite/gYnnMYz\nHelp translate for Payload: https://crowdin.com/project/payload" 10 | } 11 | -------------------------------------------------------------------------------- /src/languages/en-US/commands/invite.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Invite Payload to your Discord!", 3 | "detailedDescription": { 4 | "usages": [], 5 | "details": "Invite me to your Discord!" 6 | }, 7 | "button": "Click here" 8 | } 9 | -------------------------------------------------------------------------------- /src/languages/en-US/commands/language.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "language", 3 | "description": "Manage the language for the server", 4 | "detailedDescription": { 5 | "usages": [], 6 | "details": "Manage your server's language" 7 | }, 8 | "currentLanguage": "The current language is: {{ language }}", 9 | "selectLanguage": "Select a language", 10 | "setLanguage": "New language set: {{ language }}", 11 | "english": "English", 12 | "spanish": "Spanish", 13 | "german": "German", 14 | "finnish": "Finnish", 15 | "french": "French", 16 | "polish": "Polish", 17 | "russian": "Russian" 18 | } 19 | -------------------------------------------------------------------------------- /src/languages/en-US/commands/link.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "link", 3 | "description": "Links a user's SteamID to Payload", 4 | "detailedDescription": { 5 | "usages": [ 6 | "[steamid]" 7 | ], 8 | "details": "To link properly, you must provide a valid SteamID, in any format. If you want, you also may post your Steam Community ID link, something like: " 7 | ], 8 | "details": "Grabs the latest log of the user, defaulting to the author if no mentions provided. **Note**: The targeted user **must** have linked their SteamID through Payload first." 9 | }, 10 | "noIdLinked": "You do not have your steam id link. Use /link.", 11 | "noHistory": "You do not have any log history", 12 | "button": "View Log" 13 | } 14 | -------------------------------------------------------------------------------- /src/languages/en-US/commands/prefix.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Manages your prefix for this server", 3 | "detailedDescription": { 4 | "usages": [ 5 | "", 6 | "set ", 7 | "update ", 8 | "delete", 9 | "reset", 10 | "view" 11 | ], 12 | "details": "Manage your server's prefix. You may reset it at anytime using the `delete` or `reset` arguments." 13 | }, 14 | "currentPrefix": "The current prefix is: {{ prefix }}", 15 | "setNeedsArgs": "Please specify a new prefix!", 16 | "setSamePrefix": "Your new prefix is the same as the old prefix!", 17 | "setPrefixEmbedTitle": "Prefix updated by {{ user }}", 18 | "setPrefixEmbedDesc": "Prefix updated from {{ old }} to {{ new }}", 19 | "deleteAlreadyDefault": "Your prefix is already the default!" 20 | } 21 | -------------------------------------------------------------------------------- /src/languages/en-US/commands/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Basic information about a user's profile", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "Want some basic information of a user, such as their pushcart points? You can use this command to view if they're really helping you push the cart." 8 | }, 9 | "bot": "Bot", 10 | "points": "Points" 11 | } 12 | -------------------------------------------------------------------------------- /src/languages/en-US/commands/pushcart.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Pushcart leaderboard", 3 | "detailedDescription": { 4 | "usages": [ 5 | "", 6 | "leaderboard", 7 | "stats", 8 | "rank " 9 | ], 10 | "details": "Push the cart! This is a global leaderboard, and you can view current rankings at anytime using the listed subcommands." 11 | }, 12 | "cooldown": "You must wait {{ seconds }} seconds before pushing the cart again!", 13 | "maxpoints": "You have reached the max number of points for today. Come back in {{ expires }}!", 14 | "pushSuccess": "{{ PUSHCART_EMOJI }} Pushed the cart forward **{{ units }}** feet ({{ total, number }} total).", 15 | "rank_zero": "{{ name }} has not pushed the cart.", 16 | "rank_one": "{{ rank }}: {{ name }} ({{ count, number }})", 17 | "rank_other": "{{ rank }}: {{ name }} ({{ count, number }})", 18 | "statsTitle": "{{ name }}'s Pushcart Statistics", 19 | "topPushers": "Top Pushers", 20 | "noPushesYet": "No one has pushed the cart yet. Be the first one!", 21 | "activePushers": "Most Active Pushers", 22 | "distinctPushersTitle": "Unique Pushers", 23 | "distinctPushers_one": "Only **{{ count, number }}** user has pushed the cart. Sad!", 24 | "distinctPushers_other": "There are **{{ count, number }}** unique pushers", 25 | "totalPushedTitle": "Number of Pushes", 26 | "totalPushed_one": "The cart has been pushed **{{ count, number }}** time", 27 | "totalPushed_other": "The cart has been pushed **{{ count, number }}** times", 28 | "totalUnitsPushedTitle": "Number of Units Pushed", 29 | "totalUnitsPushed_one": "Only **{{ count, number }}** unit was pushed", 30 | "totalUnitsPushed_other": "In total, **{{ count, number }}** units have been pushed", 31 | "leaderboardEmbedTitle": "Pushcart Leaderboard", 32 | "serverEmbedTitle": "Pushcart Server Leaderboard" 33 | } 34 | -------------------------------------------------------------------------------- /src/languages/en-US/commands/webhook.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook", 3 | "description": "Provides a set of actions to work with the Payload webhook integration", 4 | "detailedDescription": { 5 | "usages": [], 6 | "details": "See more on (Github)[https://github.com/payload-bot/payload-logs-plugin]" 7 | }, 8 | "embedTitle": "Your Webhook", 9 | "embedDescription": "You can find the plugin and instructions on [Github](https://github.com/payload-bot/payload-logs-plugin).\n\nUse the following value as your webhook secret: {{ secret }}", 10 | "noWebhook": "No webhook created yet", 11 | "createWebhook": "Create Webhook", 12 | "deleteWebhook": "Delete Webhook", 13 | "deletedWebhook": "Deleted webhook" 14 | } -------------------------------------------------------------------------------- /src/languages/en-US/commands/webhooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhooks", 3 | "description": "Provides a set of actions to work with the Payload webhook integration", 4 | "detailedDescription": { 5 | "usages": [], 6 | "details": "See more on (Github)[https://github.com/payload-bot/payload-logs-plugin]" 7 | }, 8 | "addName": "add", 9 | "addDescription": "Creates a webhook for a given channel", 10 | "addChannelIdName": "channelid", 11 | "addChannelIdDescription": "The channel id to send webhooks to", 12 | "addFailed": "Failed to create the webhook", 13 | "removeName": "remove", 14 | "removeDescription": "Removes the webhook", 15 | "showName": "show", 16 | "showDescription": "Shows the webhook secret for the channel" 17 | } -------------------------------------------------------------------------------- /src/languages/en-US/globals.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": "I am missing a key for this, contact us to get it fixed!", 3 | "autoEmbedFooter": "Rendered by autoresponse {{ name }}" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/en-US/preconditions.json: -------------------------------------------------------------------------------- 1 | { 2 | "clientPermissions": "I don't have sufficient permissions! I'm missing: {{ missing }}", 3 | "cooldown": "You have just used this command. You can use this command again in {{ remaining }}.", 4 | "dmOnly": "This command can only be used in DM channels.", 5 | "guildNewsOnly": "This command can only be used in announcement channels.", 6 | "guildNewsThreadOnly": "This command can only be used in announcement thread channels.", 7 | "guildOnly": "This command can only be used in server channels.", 8 | "guildPrivateThreadOnly": "This command can only be used in private thread channels.", 9 | "guildPublicThreadOnly": "This command can only be used in public thread channels.", 10 | "guildTextOnly": "This command can only be used in text channels.", 11 | "nsfw": "You may not use NSFW commands in this channel!", 12 | "threadOnly": "This command can only be used in thread channels.", 13 | "userPermissions": "You don't have sufficient permissions! You're missing: {{ missing }}" 14 | } 15 | -------------------------------------------------------------------------------- /src/languages/en-US/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "helpTitles": { 3 | "description": "**Description**", 4 | "aliases": "**Aliases**", 5 | "usages": "**Usages**", 6 | "moreDetails": "**More Details**" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/8ball.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Pregunta al 8ball mágico.", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "El pregunta no debe estar vacío." 8 | }, 9 | "noQuestion": "¡Necesitas hacerme una pregunta!" 10 | } 11 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/bruh.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "bruh", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "Si mencionas a un usuario en este comando, lo hará ping." 8 | }, 9 | "noMention": "bruh", 10 | "mention": "bruh {{ mention }}" 11 | } 12 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/changelog.json: -------------------------------------------------------------------------------- 1 | { 2 | "invalidFormat": "Formato de versión inválido", 3 | "invalidVersion": "Versión inválida" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/choose.json: -------------------------------------------------------------------------------- 1 | { 2 | "noOptions": "¡No tengo nada entre que elegir!", 3 | "chosen_one": "¡Elijo {{ options }}!", 4 | "chosen_other": "He elegido lo siguiente: {{ options }}" 5 | } 6 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "embedTitle": "Lista de Comandos", 3 | "commands": "Comandos", 4 | "auto": "Comando Automáticos" 5 | } 6 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "button": "Visitar Panel de Control", 3 | "content": "Visitar Panel de Control" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/findping.json: -------------------------------------------------------------------------------- 1 | { 2 | "noPings": "No has sido mencionado aún" 3 | } 4 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "info", 3 | "description": "Información básica y enlaces para Payload", 4 | "detailedDescription": { 5 | "usages": [], 6 | "details": "Información básica y enlaces para Payload" 7 | }, 8 | "embedTitle": "Sirviendo a {{ users }} usuarios en {{ servers }} servidores", 9 | "embedDescription": "Únete al servidor oficial de Discord de Payload para obtener ayuda y dar sugerencias: https://discord.com/invite/gYnnMYz\\nAyuda a traducir a Payload: https://crowdin.com/project/payload" 10 | } 11 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/link.json: -------------------------------------------------------------------------------- 1 | { 2 | "missingId": "¡Debes proveer una SteamID!", 3 | "malformedId": "¡Eso no es una SteamID!", 4 | "success": "¡Nueva SteamID establecida exitosamente!" 5 | } 6 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/log.json: -------------------------------------------------------------------------------- 1 | { 2 | "noIdLinked": "¡{{ user }} no tiene su SteamID vinculada!", 3 | "noHistory": "¡El jugador no tiene historial de logs!", 4 | "button": "Visitar Log" 5 | } 6 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/prefix.json: -------------------------------------------------------------------------------- 1 | { 2 | "currentPrefix": "El prefijo actual es: {{ prefix }}", 3 | "setNeedsArgs": "Por favor, especifique un nuevo prefijo!", 4 | "setSamePrefix": "El prefijo nuevo es igual al anterior!", 5 | "setPrefixEmbedTitle": "El prefijo fue actualizado por {{ user }}", 6 | "setPrefixEmbedDesc": "El prefijo ha sido actualizado de {{ old }} a {{ new }}", 7 | "deleteAlreadyDefault": "¡Tu prefijo ya es el predeterminado!" 8 | } 9 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "bot": "Bot", 3 | "points": "Puntos" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/purge.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Elimina los mensajes en masa en un canal de texto", 3 | "detailedDescription": { 4 | "usages": [ 5 | " [...mentions]" 6 | ], 7 | "details": "Elimina de mensajes, hasta un máximo de 100, del canal actual. **Consejo**: ¡También puedes mencionar a los usuarios para filtrar la eliminación por!" 8 | }, 9 | "deleted_one": "🗑 Se han eliminado **{{ count }}** mensajes en {{ seconds }} segundos.", 10 | "deleted_other": "🗑 Se han eliminado **{{ count }}** mensajes en {{ seconds }} segundos." 11 | } 12 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/pushcart.json: -------------------------------------------------------------------------------- 1 | { 2 | "cooldown": "Debes esperar {{ seconds }} segundo(s) antes de empujar el carro nuevamente!", 3 | "maxpoints": "Has alcanzado la máxima cantidad de puntos por hoy. ¡Vuelve en {{ expires }}!", 4 | "pushSuccess": "<:payload:656955124098269186> Ha empujado el carrito **{{ units }}** pies ({{ total }} total).", 5 | "noAmount": "Por favor, especifique la cantidad de puntos a regalar", 6 | "noTargetUser": "¡Debes especificar a quien le estás dando estos puntos!", 7 | "notEnoughCreds": "¡No tienes tantos puntos para regalar!", 8 | "giftSuccess_one": "🎁¡{{ from }} le ha regalado **{{ count }}** punto(s) a {{ to }}!", 9 | "giftSuccess_other": "🎁¡{{ from }} le ha regalado **{{ count }}** punto(s) a {{ to }}!", 10 | "leaderboardEmbedTitle": "Leaderboard de Empuja-Carritos", 11 | "serverEmbedTitle": "Leaderboard de Empuja-Carritos de Servidores" 12 | } 13 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/restrict.json: -------------------------------------------------------------------------------- 1 | { 2 | "noCommands": "¡Debes especificar que comandos quieres restringir!", 3 | "restrictSuccess": "Se han restringido los siguientes comandos: {{ commands }}" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "button": "Ajustes de Usuario", 3 | "message": "¡Actualiza tu configuración desde el panel de control!" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/snipe.json: -------------------------------------------------------------------------------- 1 | { 2 | "noMessages": "¡No hay mensajes para snipear!", 3 | "aboveCacheAmount": "¡La caché de sniping no va tan lejos!" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/translate.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Traducir una frase al inglés", 3 | "detailedDescription": { 4 | "usages": [ 5 | "[phrase]" 6 | ], 7 | "details": "Traducir una frase usando Google Translate." 8 | }, 9 | "noPhrase": "No se proporcionó ninguna entrada", 10 | "title": "Traducir Resultado", 11 | "footer": "Traducido por Google Translate", 12 | "error": "Error al traducir" 13 | } 14 | -------------------------------------------------------------------------------- /src/languages/es-ES/commands/unrestrict.json: -------------------------------------------------------------------------------- 1 | { 2 | "noCommands": "¡Debes especificar que comandos quieres removar las restricciónes!", 3 | "unrestrictSuccess": "Se ha removido la restricción de los siguientes comandos: {{ commands }}" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/es-ES/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "helpTitles": { 3 | "description": "**Descripción**", 4 | "aliases": "**Alias**", 5 | "usages": "**Usos**", 6 | "moreDetails": "**Más Detalles**" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/languages/fi/commands/bruh.json: -------------------------------------------------------------------------------- 1 | { 2 | "noMention": "bruh", 3 | "mention": "bruh {{ mention }}" 4 | } -------------------------------------------------------------------------------- /src/languages/fr/commands/8ball.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Posez une question à la boule magique.", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "La question ne doit pas être vide." 8 | }, 9 | "noQuestion": "Vous devez me donner une question!" 10 | } 11 | -------------------------------------------------------------------------------- /src/languages/fr/commands/bruh.json: -------------------------------------------------------------------------------- 1 | { 2 | "noMention": "bruh", 3 | "mention": "bruh {{ mention }}" 4 | } -------------------------------------------------------------------------------- /src/languages/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/payload-bot/payload-neo/1cad8185e14815b48ed6442aeac8097cbc335410/src/languages/index.ts -------------------------------------------------------------------------------- /src/languages/pl/commands/bruh.json: -------------------------------------------------------------------------------- 1 | { 2 | "noMention": "bruh", 3 | "mention": "bruh {{ mention }}" 4 | } -------------------------------------------------------------------------------- /src/languages/ru/auto/connect.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Отображает краткое описание сервера с удобной ссылкой для подключения к нему!", 3 | "descriptionLink": "Отображает краткое описание сервера с удобной ссылкой для подключения к нему!", 4 | "players": "игроки", 5 | "offline": "Сервер не в сети (Запущен ли он сейчас?)" 6 | } 7 | -------------------------------------------------------------------------------- /src/languages/ru/auto/etf2l.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Отображает предварительный просмотр команды ETF2L", 3 | "matchesDescription": "Отображает предварительный просмотр ETF2L матча", 4 | "embedTitle": "Предпросмотр команды ETF2L", 5 | "matchEmbedTitle": "Предпросмотр матча ETF2L" 6 | } 7 | -------------------------------------------------------------------------------- /src/languages/ru/auto/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Отображает просмотр logs.tf", 3 | "embedTitle": "Превью матча через Logs.tf" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/ru/auto/rgl.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Отображает предварительный просмотр команды UGC", 3 | "embedTitle": "Предпросмотр команды RGL" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/ru/auto/tftv.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Отображает предпросмотр поста TFTV", 3 | "noPostFound": "Не удалось найти пост {{ post }}" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/ru/auto/ugc.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Отображает предварительный просмотр команды UGC", 3 | "embedTitle": "Предпросмотр команды UGC" 4 | } 5 | -------------------------------------------------------------------------------- /src/languages/ru/commands/8ball.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Задай шару-предсказателю вопрос.", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "Поле не должно быть пустым." 8 | }, 9 | "noQuestion": "Вам нужно задать у меня вопрос!" 10 | } 11 | -------------------------------------------------------------------------------- /src/languages/ru/commands/avatar.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Показать аватар пользователя", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "Упомяните пользователя или используйте идентификатор пользователя, и эта команда покажет его аватар. По умолчанию используется автор, если пользователь не предоставлен." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/languages/ru/commands/bruh.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Чел", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "Если вы упоминаете пользователя в этой команде, его упомянет бот." 8 | }, 9 | "noMention": "чел", 10 | "mention": "чел {{ mention }}" 11 | } 12 | -------------------------------------------------------------------------------- /src/languages/ru/commands/changelog.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Получает список изменений в Payload", 3 | "detailedDescription": { 4 | "usages": [ 5 | "[version]" 6 | ], 7 | "details": "Хотите заглянуть в список изменений Payload? Здесь вы можете заполучить последнюю версию или любую предыдущую рабочую версию." 8 | }, 9 | "invalidFormat": "Недопустимый формат запроса", 10 | "invalidVersion": "Неверная версия" 11 | } 12 | -------------------------------------------------------------------------------- /src/languages/ru/commands/choose.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Случайно выбирает элемент из списка вариантов", 3 | "detailedDescription": { 4 | "usages": [ 5 | "[num] " 6 | ], 7 | "details": "Первый аргумент — это количество вариантов выбора, по умолчанию **1**.\nОпции — это повторяющаяся строка.\n**Подсказка**: Для использования предложений используйте «кавычки для группировки слов!»" 8 | }, 9 | "noOptions": "Мне нечего выбрать!", 10 | "chosen_one": "Я выбираю {{ options }}!", 11 | "chosen_other": "Я выбрал следующее: {{ options }}" 12 | } 13 | -------------------------------------------------------------------------------- /src/languages/ru/commands/commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Список команд, доступных в Payload", 3 | "detailedDescription": { 4 | "usages": [], 5 | "details": "Удобный список команд. Полезно для ограничения/разрешения команд, о которых вы не знаете, но не хотите, чтобы люди пользовались ими." 6 | }, 7 | "embedTitle": "Список команд", 8 | "commands": "Команды", 9 | "auto": "Автоматические команды" 10 | } 11 | -------------------------------------------------------------------------------- /src/languages/ru/commands/findping.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Находит пользователя, если вас упомянул \"призрак\"", 3 | "detailedDescription": { 4 | "usages": [], 5 | "details": "Проходит через лист и находит, если ты был упомянут \"призраком\" за последние **15** минут." 6 | }, 7 | "noPings": "Вы еще не были упомянуты" 8 | } 9 | -------------------------------------------------------------------------------- /src/languages/ru/commands/help.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Предоставляет справку о команде.", 3 | "detailedDescription": { 4 | "usages": [ 5 | "[command]" 6 | ], 7 | "details": "Нужна помощь по команде? Это даст вам возможность использовать команду с аргументами, которые она может принять, как и некоторые дополнительные сведения о команде." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/languages/ru/commands/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Информация о Payload", 3 | "detailedDescription": { 4 | "usages": [], 5 | "details": "Основная информация и ссылки для Payload" 6 | }, 7 | "embedTitle": "Обслуживаем {{ users }} пользователей на {{ servers }} серверах!", 8 | "embedDescription": "Присоединяйтесь к официальному серверу Discord для помощи и предложений: https://payload.tf/discord\n\nПригласите Payload на ваш сервер: https://payload.tf/invite\nСправка по переводу для Payload: https://crowdin.com/project/payload" 9 | } 10 | -------------------------------------------------------------------------------- /src/languages/ru/commands/invite.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Пригласите Payload в свой Discord!", 3 | "detailedDescription": { 4 | "usages": [], 5 | "details": "Пригласите меня в свой Discord!" 6 | }, 7 | "button": "Нажмите здесь" 8 | } 9 | -------------------------------------------------------------------------------- /src/languages/ru/commands/link.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Прикрепляет ссылку на SteamID пользователя к Payload", 3 | "detailedDescription": { 4 | "usages": [ 5 | "[steamid]" 6 | ], 7 | "details": "Для правильного прикрепления ссылки вы должны предоставить правильный SteamID в любом формате. Если вы хотите, вы также можете опубликовать вашу ссылку Steam Community ID, что-то вроде " 6 | ], 7 | "details": "Задает последний лог пользователя; если никто не упомянут, показывает последний лог автора. **Примечание**: пользователь **должен** сначала связать свой SteamID с Payload." 8 | }, 9 | "noIdLinked": "{{ user }} не имеет привязанной ссылки на SteamID!", 10 | "noHistory": "У игрока нет никакой истории логов!", 11 | "button": "Посетить логи" 12 | } 13 | -------------------------------------------------------------------------------- /src/languages/ru/commands/prefix.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Управляет вашим префиксом для этого сервера", 3 | "detailedDescription": { 4 | "usages": [ 5 | "", 6 | "set ", 7 | "update ", 8 | "delete", 9 | "reset", 10 | "view" 11 | ], 12 | "details": "Управление префиксом вашего сервера. Вы можете сбросить его в любое время, используя команды `delete` или `reset`." 13 | }, 14 | "currentPrefix": "Текущий префикс: {{ prefix }}", 15 | "setNeedsArgs": "Пожалуйста, укажите новый префикс!", 16 | "setSamePrefix": "Ваш новый префикс такой же, как и старый префикс!", 17 | "setPrefixEmbedTitle": "Префикс обновлён пользователем {{ user }}", 18 | "setPrefixEmbedDesc": "Префикс обновлён с {{ old }} на {{ new }}", 19 | "deleteAlreadyDefault": "Ваш префикс уже используется по умолчанию!" 20 | } 21 | -------------------------------------------------------------------------------- /src/languages/ru/commands/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Показывает информацию о вашем профиле", 3 | "detailedDescription": { 4 | "usages": [ 5 | "" 6 | ], 7 | "details": "Хотите некоторые основные сведения о пользователе, такие как их баллы в pushcart? Вы можете использовать эту команду для просмотра, чтобы узнать, действительно ли они помогают толкать вагонетку." 8 | }, 9 | "bot": "Бот", 10 | "points": "Баллы" 11 | } 12 | -------------------------------------------------------------------------------- /src/languages/ru/commands/purge.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Удаляет сообщения группами в текстовом канале", 3 | "detailedDescription": { 4 | "usages": [ 5 | " [...mentions]" 6 | ], 7 | "details": "Удаляет сообщений, максимальное количество удаляемых сообщений -- 100. **Подсказка**: Вы также можете упомянуть пользователей для фильтрации по удалению!" 8 | }, 9 | "deleted_one": "🗑 Удалено **{{ count }}** сообщение за {{ seconds }} секунд.", 10 | "deleted_other": "🗑 Удалено **{{ count }}** сообщений за {{ seconds }} секунд." 11 | } 12 | -------------------------------------------------------------------------------- /src/languages/ru/commands/pushcart.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Список лидеров Pushcart", 3 | "detailedDescription": { 4 | "usages": [ 5 | "", 6 | "gift [user]", 7 | "leaderboard", 8 | "servers", 9 | "rank " 10 | ], 11 | "details": "Толкай вагонетку! Это глобальная таблица лидеров, и вы можете просматривать текущий рейтинг в любое время, используя команды, перечисленные в списке." 12 | }, 13 | "cooldown": "Вы должны подождать {{ seconds }} секунд, прежде чем снова толкать вагонетку!", 14 | "maxpoints": "Вы достигли максимального количества баллов за сегодня. Возвращайтесь через {{ expires }}!", 15 | "pushSuccess": "{{ PUSHCART_EMOJI }} Протолкнул вагонетку на **{{ units }}** футов (всего {{ total }}).", 16 | "noAmount": "Пожалуйста, укажите количество баллов для подарка", 17 | "noTargetUser": "Вам нужно указать, кому дарить эти баллы!", 18 | "notEnoughCreds": "У вас недостаточно очков!", 19 | "giftSuccess_one": "🎁 {{ from }} подарил **{{ count }}** очко {{ to }}!", 20 | "giftSuccess_other": "🎁 {{ from }} подарил **{{ count }}** очков {{ to }}!", 21 | "leaderboardEmbedTitle": "Список лидеров Pushcart", 22 | "serverEmbedTitle": "Список серверов Pushcart" 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/PayloadClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationCommandRegistries, 3 | container, 4 | RegisterBehavior, 5 | SapphireClient, 6 | } from "@sapphire/framework"; 7 | import type { Message } from "discord.js"; 8 | import { CLIENT_OPTIONS } from "#utils/clientOptions.ts"; 9 | import config from "#root/config.ts"; 10 | import { AutoResponseStore } from "./structs/AutoResponse/AutoResponseStore.ts"; 11 | import connect from "#utils/database.ts"; 12 | import { join } from "node:path"; 13 | import { guild } from "#root/drizzle/schema.ts"; 14 | import { eq } from "drizzle-orm"; 15 | import { serve } from "../api/mod.ts"; 16 | 17 | export class PayloadClient extends SapphireClient { 18 | public override dev = Deno.env.get("NODE_ENV") !== "production"; 19 | 20 | constructor() { 21 | super(CLIENT_OPTIONS); 22 | this.stores.register( 23 | new AutoResponseStore().registerPath( 24 | join(import.meta.dirname!, "..", "..", "auto"), 25 | ), 26 | ); 27 | } 28 | 29 | public override fetchPrefix = async (msg: Message) => { 30 | if (msg.guildId) { 31 | const [data] = await container.database 32 | .select({ prefix: guild.prefix }) 33 | .from(guild) 34 | .where(eq(guild.id, msg.guildId)); 35 | 36 | return data?.prefix ?? config.PREFIX; 37 | } 38 | 39 | return [config.PREFIX, ""]; 40 | }; 41 | 42 | public override async login(token?: string) { 43 | if (this.dev) { 44 | ApplicationCommandRegistries.setDefaultBehaviorWhenNotIdentical( 45 | RegisterBehavior.BulkOverwrite, 46 | ); 47 | } 48 | 49 | connect(); 50 | 51 | const server = serve(); 52 | container.denoServer = server; 53 | 54 | const response = await super.login(token); 55 | 56 | return response; 57 | } 58 | 59 | public override async destroy() { 60 | await container.denoServer?.shutdown(); 61 | 62 | await super.destroy(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/lib/i18n/CommandHelper.ts: -------------------------------------------------------------------------------- 1 | export class BuildCommandHelp { 2 | private aliases: string = null!; 3 | private usages: string = null!; 4 | private details: string = null!; 5 | private description: string = null!; 6 | 7 | public setAliases(text: string) { 8 | this.aliases = text; 9 | return this; 10 | } 11 | 12 | public setUsages(text: string) { 13 | this.usages = text; 14 | return this; 15 | } 16 | 17 | public setDetails(text: string) { 18 | this.details = text; 19 | return this; 20 | } 21 | 22 | public setDescription(text: string) { 23 | this.description = text; 24 | return this; 25 | } 26 | 27 | public display( 28 | name: string, 29 | aliases: string | null, 30 | options: LanguageHelpDisplayOptions, 31 | prefixUsed: string, 32 | description: string, 33 | ) { 34 | const { usages = [], details } = options; 35 | const output: string[] = []; 36 | 37 | // Simple Description 38 | if (description) { 39 | output.push(this.description, description, ""); 40 | } 41 | 42 | // Usages 43 | if (usages.length) { 44 | output.push( 45 | this.usages, 46 | ...usages.map(usage => `→ ${prefixUsed}${name}${usage.length === 0 ? "" : ` *${usage}*`}`), 47 | "", 48 | ); 49 | } 50 | 51 | // Aliases 52 | if (aliases !== null) { 53 | output.push(`${this.aliases}: ${aliases}`, ""); 54 | } 55 | 56 | // Extended help 57 | if (details) { 58 | output.push(this.details, details, ""); 59 | } 60 | 61 | return output.join("\n"); 62 | } 63 | } 64 | 65 | export interface LanguageHelpDisplayOptions { 66 | usages?: string[]; 67 | details?: string; 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/i18n/all/index.ts: -------------------------------------------------------------------------------- 1 | export * as LanguageKeys from "./keys/all.ts"; 2 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/all.ts: -------------------------------------------------------------------------------- 1 | export * as Commands from "./commands/index.ts"; 2 | export * as Auto from "./auto/index.ts"; 3 | export * as Preconditions from "./preconditions.ts"; 4 | export * as Arguments from "./arguments.ts"; 5 | export * as System from "./system.ts"; 6 | export * as Globals from "./globals.ts"; 7 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/arguments.ts: -------------------------------------------------------------------------------- 1 | import { FT, T } from "#lib/types"; 2 | 3 | export const ArgsMissing = T("arguments:missing"); 4 | export const BooleanError = FT<{ parameter: string; possibles: string[]; count: number }>("arguments:booleanError"); 5 | export const ChannelError = FT<{ parameter: string }>("arguments:channelError"); 6 | export const Command = FT<{ parameter: string }>("arguments:command"); 7 | export const DateError = FT<{ parameter: string }>("arguments:dateError"); 8 | export const DateTooFar = FT<{ parameter: string; maximum: number }>("arguments:dateTooFar"); 9 | export const DateTooEarly = FT<{ parameter: string; minimum: number }>("arguments:dateTooEarly"); 10 | export const DmChannelError = FT<{ parameter: string }>("arguments:dmChannelError"); 11 | export const Duration = FT<{ parameter: string }>("arguments:duration"); 12 | export const FloatError = FT<{ parameter: string }>("arguments:floatError"); 13 | export const FloatTooLarge = FT<{ parameter: string; maximum: number }>("arguments:floatTooLarge"); 14 | export const FloatTooSmall = FT<{ parameter: string; minimum: number }>("arguments:floatTooSmall"); 15 | export const GuildChannelError = FT<{ parameter: string }>("arguments:guildChannelError"); 16 | export const GuildChannelMissingGuildError = FT<{ parameter: string }>("arguments:guildChannelMissingGuildError"); 17 | export const GuildPrivateThreadChannelError = FT<{ parameter: string }>("arguments:guildPrivateThreadChannelError"); 18 | export const GuildPublicThreadChannelError = FT<{ parameter: string }>("arguments:guildPublicThreadChannelError"); 19 | export const GuildStageVoiceChannelError = FT<{ parameter: string }>("arguments:guildStageVoiceChannelError"); 20 | export const GuildTextChannelError = FT<{ parameter: string }>("arguments:guildTextChannelError"); 21 | export const GuildThreadChannelError = T("arguments:guildThreadChannelError"); 22 | export const GuildVoiceChannelError = FT<{ parameter: string }>("arguments:guildVoiceChannelError"); 23 | export const GuildNewsChannelError = FT<{ parameter: string }>("arguments:guildNewsChannelError"); 24 | export const HyperlinkError = FT<{ parameter: string }>("arguments:hyperlinkError"); 25 | export const IntegerError = FT<{ parameter: string }>("arguments:integerError"); 26 | export const IntegerTooLarge = FT<{ parameter: string; maximum: number }>("arguments:integerTooLarge"); 27 | export const IntegerTooSmall = FT<{ parameter: string; minimum: number }>("arguments:integerTooSmall"); 28 | export const MemberError = FT<{ parameter: string }>("arguments:memberError"); 29 | export const MemberMissingGuild = FT<{ parameter: string }>("arguments:memberMissingGuild"); 30 | export const MessageError = FT<{ parameter: string }>("arguments:messageError"); 31 | export const NumberError = FT<{ parameter: string }>("arguments:numberError"); 32 | export const NumberTooLarge = FT<{ parameter: string; maximum: number }>("arguments:numberTooLarge"); 33 | export const NumberTooSmall = FT<{ parameter: string; minimum: number }>("arguments:numberTooSmall"); 34 | export const RoleError = FT<{ parameter: string }>("arguments:roleError"); 35 | export const StringTooLong = FT<{ parameter: string; maximum: number }>("arguments:stringTooLong"); 36 | export const StringTooShort = FT<{ parameter: string; minimum: number }>("arguments:stringTooShort"); 37 | export const UserError = FT<{ parameter: string }>("arguments:userError"); 38 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/auto/connect.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const Description = T("auto/connect:description"); 4 | export const LinkDescription = T("auto/connect:descriptionLink"); 5 | export const Players = T("auto/connect:players"); 6 | export const Offline = T("auto/connect:offline"); 7 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/auto/etf2l.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const Etf2lDescription = T("auto/etf2l:description"); 4 | export const Etf2lMatchesDescription = T("auto/etf2l:matchesDescription"); 5 | export const Etf2lEmbedTitle = T("auto/etf2l:embedTitle"); 6 | export const Etf2lMatchesEmbedTitle = T("auto/etf2l:matchEmbedTitle"); 7 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/auto/index.ts: -------------------------------------------------------------------------------- 1 | export * as Etf2l from "./etf2l.ts"; 2 | export * as RGL from "./rgl.ts"; 3 | export * as Logs from "./logs.ts"; 4 | export * as UGC from "./ugc.ts"; 5 | export * as Tftv from "./tftv.ts"; 6 | export * as Connect from "./connect.ts"; 7 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/auto/logs.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const Description = T("auto/logs:description"); 4 | export const EmbedTitle = T("auto/logs:embedTitle"); 5 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/auto/rgl.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const RGLDescription = T("auto/rgl:description"); 4 | export const RGLEmbedTitle = T("auto/rgl:embedTitle"); 5 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/auto/tftv.ts: -------------------------------------------------------------------------------- 1 | import { FT, T } from "#lib/types"; 2 | 3 | export const Description = T("auto/tftv:description"); 4 | export const NoPostFound = FT<{ post: string }, string>("auto/tftv:noPostFound"); 5 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/auto/ugc.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const Description = T("auto/ugc:description"); 4 | export const EmbedTitle = T("auto/ugc:embedTitle"); 5 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/commands.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const Description = T("commands/commands:description"); 4 | export const DetailedDescription = T("commands/commands:detailedDescription"); 5 | export const EmbedTitle = T("commands/commands:embedTitle"); 6 | export const Commands = T("commands/commands:commands"); 7 | export const AutoCommands = T("commands/commands:auto"); 8 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/help.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const Description = T("commands/help:description"); 4 | export const DetailedDescription = T("commands/help:detailedDescription"); 5 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * as Prefix from "./prefix.ts"; 2 | export * as Pushcart from "./pushcart.ts"; 3 | export * as Commands from "./commands.ts"; 4 | export * as Info from "./info.ts"; 5 | export * as Log from "./log.ts"; 6 | export * as Language from "./language.ts"; 7 | export * as Link from "./link.ts"; 8 | export * as Profile from "./profile.ts"; 9 | export * as Settings from "./settings.ts"; 10 | export * as Invite from "./invite.ts"; 11 | export * as Help from "./help.ts"; 12 | export * as Webhook from "./webhook.ts"; 13 | export * as Webhooks from "./webhooks.ts"; 14 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/info.ts: -------------------------------------------------------------------------------- 1 | import { T, FT } from "#lib/types"; 2 | 3 | export const Name = T("commands/info:name"); 4 | export const Description = T("commands/info:description"); 5 | export const DetailedDescription = T("commands/info:detailedDescription"); 6 | export const EmbedTitle = FT<{ users: number; servers: number }, string>("commands/info:embedTitle"); 7 | export const EmbedDescription = T("commands/info:embedDescription"); 8 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/invite.ts: -------------------------------------------------------------------------------- 1 | import { T, FT } from "#lib/types"; 2 | 3 | export const Description = T("commands/invite:description"); 4 | export const DetailedDescription = T("commands/invite:detailedDescription"); 5 | export const Button = FT<{ url: string }, string>("commands/invite:button"); 6 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/language.ts: -------------------------------------------------------------------------------- 1 | import { FT, T } from "#lib/types"; 2 | 3 | export const Name = T("commands/language:name"); 4 | export const Description = T("commands/language:description"); 5 | export const DetailedDescription = T("commands/language:detailedDescription"); 6 | 7 | export const SelectLanguage = T("commands/language:selectLanguage"); 8 | export const SetLanguage = FT("commands/language:setLanguage"); 9 | export const CurrentLanguage = FT("commands/language:currentLanguage"); 10 | 11 | export const English = T("commands/language:english"); 12 | export const Spanish = T("commands/language:spanish"); 13 | export const German = T("commands/language:german"); 14 | export const Finnish = T("commands/language:finnish"); 15 | export const French = T("commands/language:french"); 16 | export const Polish = T("commands/language:polish"); 17 | export const Russian = T("commands/language:russian"); 18 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/link.ts: -------------------------------------------------------------------------------- 1 | import { FT, T } from "#lib/types"; 2 | 3 | export const Name = T("commands/link:name"); 4 | export const SteamIdName = T("commands/link:steamIdName"); 5 | export const Description = T("commands/link:description"); 6 | export const SteamIdDescription = T("commands/link:steamIdDescription"); 7 | export const DetailedDescription = T("commands/link:detailedDescription"); 8 | export const MissingId = T("commands/link:missingId"); 9 | export const MalformedId = T("commands/link:malformedId"); 10 | export const Success = FT<{ steamId: string }>("commands/link:success"); 11 | export const Delete = T("commands/link:delete"); 12 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/log.ts: -------------------------------------------------------------------------------- 1 | import { FT, T } from "#lib/types"; 2 | 3 | export const Name = T("commands/log:name"); 4 | export const Description = T("commands/log:description"); 5 | export const DetailedDescription = T("commands/log:detailedDescription"); 6 | export const NoHistory = T("commands/log:noHistory"); 7 | export const NoIdLinked = FT<{ user: string }, string>("commands/log:noIdLinked"); 8 | export const Button = FT<{ user: string }, string>("commands/log:button"); 9 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/prefix.ts: -------------------------------------------------------------------------------- 1 | import { FT, T } from "#lib/types"; 2 | 3 | export const Description = T("commands/prefix:description"); 4 | export const DetailedDescription = T("commands/prefix:detailedDescription"); 5 | export const CurrentPrefix = FT<{ prefix: string }, string>("commands/prefix:currentPrefix"); 6 | export const SetNeedsArgs = T("commands/prefix:setNeedsArgs"); 7 | export const SetSamePrefix = T("commands/prefix:setSamePrefix"); 8 | export const SetPrefixEmbedTitle = FT<{ user: string }, string>("commands/prefix:setPrefixEmbedTitle"); 9 | export const SetPrefixEmbedDesc = FT<{ old: string; new: string }, string>("commands/prefix:setPrefixEmbedDesc"); 10 | export const DeleteAlreadyDefault = T("commands/prefix:deleteAlreadyDefault"); 11 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/profile.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const Description = T("commands/profile:description"); 4 | export const DetailedDescription = T("commands/profile:detailedDescription"); 5 | export const Bot = T("commands/profile:bot"); 6 | export const Points = T("commands/profile:points"); 7 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/pushcart.ts: -------------------------------------------------------------------------------- 1 | import { T, FT } from "#lib/types"; 2 | 3 | export const Description = T("commands/pushcart:description"); 4 | export const DetailedDescription = T("commands/pushcart:detailedDescription"); 5 | 6 | export const Cooldown = FT<{ seconds: string }, string>("commands/pushcart:cooldown"); 7 | export const Maxpoints = FT<{ expires: string }, string>("commands/pushcart:maxpoints"); 8 | export const PushSuccess = FT<{ units: string; total: string }, string>("commands/pushcart:pushSuccess"); 9 | 10 | export const StatsTitle = FT<{ leaderboardString: string }, string>("commands/pushcart:statsTitle"); 11 | export const TopPushers = FT<{ leaderboardString: string }, string>("commands/pushcart:topPushers"); 12 | export const ActivePushers = FT<{ leaderboardString: string }, string>("commands/pushcart:activePushers"); 13 | 14 | export const TotalUnitsPushedTitle = FT<{ leaderboardString: string }, string>( 15 | "commands/pushcart:totalUnitsPushedTitle", 16 | ); 17 | export const TotalUnitsPushed = FT<{ leaderboardString: string }, string>("commands/pushcart:totalUnitsPushed"); 18 | export const TotalPushedTitle = FT<{ leaderboardString: string }, string>("commands/pushcart:totalPushedTitle"); 19 | export const TotalPushed = FT<{ leaderboardString: string }, string>("commands/pushcart:totalPushed"); 20 | export const DistinctPushersTitle = FT<{ leaderboardString: string }, string>("commands/pushcart:distinctPushersTitle"); 21 | export const DistinctPushers = FT<{ leaderboardString: string }, string>("commands/pushcart:distinctPushers"); 22 | 23 | export const RankString = FT<{ name: string; rank?: number; count: number }, string>("commands/pushcart:rank"); 24 | export const NoPushesYet = T("commands/pushcart:noPushesYet"); 25 | 26 | export const LeaderboardEmbedTitle = T("commands/pushcart:leaderboardEmbedTitle"); 27 | export const ServerEmbedTitle = T("commands/pushcart:serverEmbedTitle"); 28 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/settings.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const Description = T("commands/settings:description"); 4 | export const DetailedDescription = T("commands/settings:detailedDescription"); 5 | export const Button = T("commands/settings:button"); 6 | export const Message = T("commands/settings:message"); 7 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/webhook.ts: -------------------------------------------------------------------------------- 1 | import { FT, T } from "#lib/types"; 2 | 3 | export const Name = T("commands/webhook:name"); 4 | export const Description = T("commands/webhook:description"); 5 | export const DetailedDescription = T("commands/webhook:detailedDescription"); 6 | export const EmbedTitle = T("commands/webhook:embedTitle"); 7 | export const EmbedDescription = FT<{ secret: string }>("commands/webhook:embedDescription"); 8 | export const NoWebhook = T("commands/webhook:noWebhook"); 9 | export const CreateWebhook = T("commands/webhook:createWebhook"); 10 | export const DeleteWebhook = T("commands/webhook:deleteWebhook"); 11 | export const DeletedWebhook = T("commands/webhook:deletedWebhook"); 12 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/commands/webhooks.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const Name = T("commands/webhooks:name"); 4 | export const Description = T("commands/webhooks:description"); 5 | export const DetailedDescription = T("commands/webhooks:detailedDescription"); 6 | 7 | export const AddName = T("commands/webhooks:addName"); 8 | export const AddDescription = T("commands/webhooks:addDescription"); 9 | export const AddChannelIdName = T("commands/webhooks:addChannelIdName"); 10 | export const AddChannelIdDescription = T("commands/webhooks:addChannelIdDescription"); 11 | export const AddFailed = T("commands/webhooks:addFailed"); 12 | export const RemoveName = T("commands/webhooks:removeName"); 13 | export const RemoveDescription = T("commands/webhooks:removeDescription"); 14 | export const ShowName = T("commands/webhooks:showName"); 15 | export const ShowDescription = T("commands/webhooks:showDescription"); -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/globals.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const Default = T("globals:default"); 4 | export const AutoEmbedFooter = T("globals:autoEmbedFooter"); 5 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/preconditions.ts: -------------------------------------------------------------------------------- 1 | import { FT, T } from "#lib/types"; 2 | 3 | export const ClientPermissions = FT<{ missing: string[] }>("preconditions:clientPermissions"); 4 | export const Cooldown = FT<{ remaining: number }>("preconditions:cooldown"); 5 | export const DisabledGlobal = T("preconditions:disabledGlobal"); 6 | export const DmOnly = T("preconditions:dmOnly"); 7 | export const GuildNewsOnly = T("preconditions:guildNewsOnly"); 8 | export const GuildNewsThreadOnly = T("preconditions:guildNewsThreadOnly"); 9 | export const GuildOnly = T("preconditions:guildOnly"); 10 | export const GuildPrivateThreadOnly = T("preconditions:guildPrivateThreadOnly"); 11 | export const GuildPublicThreadOnly = T("preconditions:guildPublicThreadOnly"); 12 | export const GuildTextOnly = T("preconditions:guildTextOnly"); 13 | export const Nsfw = T("preconditions:nsfw"); 14 | export const ThreadOnly = T("preconditions:threadOnly"); 15 | export const UserPermissions = FT<{ missing: string[] }>("preconditions:userPermissions"); 16 | -------------------------------------------------------------------------------- /src/lib/i18n/all/keys/system.ts: -------------------------------------------------------------------------------- 1 | import { T } from "#lib/types"; 2 | 3 | export const HelpTitles = T<{ 4 | description: string; 5 | aliases: string; 6 | usages: string; 7 | moreDetails: string; 8 | }>("system:helpTitles"); 9 | -------------------------------------------------------------------------------- /src/lib/i18n/mapping.ts: -------------------------------------------------------------------------------- 1 | import { Identifiers } from "@sapphire/framework"; 2 | import { LanguageKeys } from "./all/index.ts"; 3 | 4 | export function mapIdentifier(identifier: string): string { 5 | switch (identifier) { 6 | case Identifiers.ArgumentBooleanError: 7 | case Identifiers.ArgumentChannelError: 8 | case Identifiers.ArgumentDateError: 9 | case Identifiers.ArgumentDateTooEarly: 10 | case Identifiers.ArgumentDateTooFar: 11 | case Identifiers.ArgumentDMChannelError: 12 | case Identifiers.ArgumentFloatError: 13 | case Identifiers.ArgumentFloatTooLarge: 14 | case Identifiers.ArgumentFloatTooSmall: 15 | case Identifiers.ArgumentGuildCategoryChannelError: 16 | case Identifiers.ArgumentGuildChannelError: 17 | case Identifiers.ArgumentGuildChannelMissingGuildError: 18 | case Identifiers.ArgumentGuildNewsChannelError: 19 | case Identifiers.ArgumentGuildNewsThreadChannelError: 20 | case Identifiers.ArgumentGuildPrivateThreadChannelError: 21 | case Identifiers.ArgumentGuildPublicThreadChannelError: 22 | case Identifiers.ArgumentGuildStageVoiceChannelError: 23 | case Identifiers.ArgumentGuildTextChannelError: 24 | case Identifiers.ArgumentGuildThreadChannelError: 25 | case Identifiers.ArgumentGuildVoiceChannelError: 26 | case Identifiers.ArgumentHyperlinkError: 27 | case Identifiers.ArgumentIntegerError: 28 | case Identifiers.ArgumentIntegerTooLarge: 29 | case Identifiers.ArgumentIntegerTooSmall: 30 | case Identifiers.ArgumentMemberError: 31 | case Identifiers.ArgumentMemberMissingGuild: 32 | case Identifiers.ArgumentMessageError: 33 | case Identifiers.ArgumentNumberError: 34 | case Identifiers.ArgumentNumberTooLarge: 35 | case Identifiers.ArgumentNumberTooSmall: 36 | case Identifiers.ArgumentRoleError: 37 | case Identifiers.ArgumentRoleMissingGuild: 38 | case Identifiers.ArgumentStringTooLong: 39 | case Identifiers.ArgumentStringTooShort: 40 | case Identifiers.ArgumentUserError: 41 | return `arguments:${identifier}`; 42 | case Identifiers.ArgsMissing: 43 | return LanguageKeys.Arguments.ArgsMissing; 44 | case Identifiers.CommandDisabled: 45 | return LanguageKeys.Preconditions.DisabledGlobal; 46 | case Identifiers.PreconditionCooldown: 47 | return LanguageKeys.Preconditions.Cooldown; 48 | case Identifiers.PreconditionDMOnly: 49 | return LanguageKeys.Preconditions.DmOnly; 50 | case Identifiers.PreconditionGuildNewsOnly: 51 | return LanguageKeys.Preconditions.GuildNewsOnly; 52 | case Identifiers.PreconditionGuildNewsThreadOnly: 53 | return LanguageKeys.Preconditions.GuildNewsThreadOnly; 54 | case Identifiers.PreconditionGuildOnly: 55 | return LanguageKeys.Preconditions.GuildOnly; 56 | case Identifiers.PreconditionGuildPrivateThreadOnly: 57 | return LanguageKeys.Preconditions.GuildPrivateThreadOnly; 58 | case Identifiers.PreconditionGuildPublicThreadOnly: 59 | return LanguageKeys.Preconditions.GuildPublicThreadOnly; 60 | case Identifiers.PreconditionGuildTextOnly: 61 | return LanguageKeys.Preconditions.GuildTextOnly; 62 | case Identifiers.PreconditionNSFW: 63 | return LanguageKeys.Preconditions.Nsfw; 64 | case Identifiers.PreconditionClientPermissions: 65 | return LanguageKeys.Preconditions.ClientPermissions; 66 | case Identifiers.PreconditionUserPermissions: 67 | return LanguageKeys.Preconditions.UserPermissions; 68 | case Identifiers.PreconditionThreadOnly: 69 | return LanguageKeys.Preconditions.ThreadOnly; 70 | default: 71 | return identifier; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/lib/structs/AutoResponse/AutoResponse.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderPieceContext, MessageCommandContext } from "@sapphire/framework"; 2 | import type { Message } from "discord.js"; 3 | import { PayloadCommand } from "../commands/PayloadCommand.ts"; 4 | 5 | export abstract class AutoCommand extends PayloadCommand { 6 | public regex: RegExp; 7 | 8 | constructor(context: LoaderPieceContext<"commands">, options: AutoCommandOptions) { 9 | super(context, { ...options, typing: true }); 10 | this.regex = options.regex; 11 | } 12 | 13 | /** 14 | * Returns whether or not this command should handle this message 15 | * @param msg Message 16 | * @returns {Boolean} 17 | * @description Returns if this command should run or not 18 | */ 19 | public shouldRun(msg: Message): boolean { 20 | return msg.content.match(this.regex)?.[0] ? true : false; 21 | } 22 | 23 | /** 24 | * Gets the resulting match for the command's regex 25 | * @param msg Message 26 | * @returns {string | null} 27 | * @description Gets the resulting match of the Regex defined in the command 28 | */ 29 | public getMatch(msg: Message): string | undefined { 30 | return msg.content.match(this.regex)?.[0]; 31 | } 32 | } 33 | 34 | export interface AutoCommandContext extends Pick { 35 | matched: string; 36 | } 37 | 38 | export interface AutoCommandOptions extends PayloadCommand.Options { 39 | regex: RegExp; 40 | } 41 | 42 | // deno-lint-ignore no-namespace 43 | export namespace AutoCommand { 44 | export type Options = AutoCommandOptions; 45 | export type Args = PayloadCommand.Args; 46 | export type Context = AutoCommandContext; 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/structs/AutoResponse/AutoResponseStore.ts: -------------------------------------------------------------------------------- 1 | import { AliasStore } from "@sapphire/framework"; 2 | import { AutoCommand } from "./AutoResponse.ts"; 3 | 4 | export class AutoResponseStore extends AliasStore { 5 | constructor() { 6 | super(AutoCommand, { name: "auto" }); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/structs/ServiceController.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderPieceContext } from "@sapphire/framework"; 2 | import { type ApiResponse, Route, type RouteOptions } from "@sapphire/plugin-api"; 3 | import { isNullish } from "@sapphire/utilities"; 4 | 5 | export abstract class ServiceController extends Route { 6 | protected logger = this.container.logger; 7 | protected client = this.container.client; 8 | protected database = this.container.database; 9 | 10 | public constructor(context: LoaderPieceContext<"routes">, options: RouteOptions) { 11 | super(context, options); 12 | } 13 | 14 | public notFoundIfNull(data: TObj | null, response: ApiResponse) { 15 | if (isNullish(data)) { 16 | return response.notFound(); 17 | } else { 18 | return response.ok(data); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/structs/commands/PayloadArgs.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from "discord.js"; 2 | import type { TFunction } from "i18next"; 3 | import type { ArgumentStream } from "@sapphire/lexure"; 4 | import { Args, type MessageCommand } from "@sapphire/framework"; 5 | 6 | export class PayloadArgs extends Args { 7 | public t: TFunction; 8 | 9 | public constructor( 10 | message: Message, 11 | command: MessageCommand, 12 | parser: ArgumentStream, 13 | context: MessageCommand.RunContext, 14 | t: TFunction, 15 | ) { 16 | super(message, command, parser, context); 17 | this.t = t; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/structs/commands/PayloadCommand.ts: -------------------------------------------------------------------------------- 1 | import { type MessageCommand, type LoaderPieceContext, UserError } from "@sapphire/framework"; 2 | import { fetchT } from "@sapphire/plugin-i18next"; 3 | import { Subcommand } from "@sapphire/plugin-subcommands"; 4 | import type { Message } from "discord.js"; 5 | import { Parser, ArgumentStream } from "@sapphire/lexure"; 6 | import { PayloadArgs } from "./PayloadArgs.ts"; 7 | import { LibSQLDatabase } from "drizzle-orm/libsql"; 8 | 9 | export abstract class PayloadCommand extends Subcommand { 10 | public readonly hidden: boolean; 11 | protected readonly database: LibSQLDatabase; 12 | public constructor(context: LoaderPieceContext<"commands">, options: PayloadCommand.Options) { 13 | super(context, options); 14 | 15 | this.hidden = options.hidden ?? false; 16 | this.database = this.container.database; 17 | } 18 | 19 | public override async messagePreParse( 20 | message: Message, 21 | parameters: string, 22 | context: MessageCommand.RunContext, 23 | ): Promise { 24 | const parser = new Parser(this.strategy); 25 | const args = new ArgumentStream(parser.run(this.lexer.run(parameters))); 26 | 27 | return new PayloadArgs(message, this, args, context, await fetchT(message)); 28 | } 29 | 30 | protected error(identifier: string | UserError, context?: unknown): never { 31 | throw typeof identifier === "string" ? new UserError({ identifier, context }) : identifier; 32 | } 33 | } 34 | 35 | // deno-lint-ignore no-namespace 36 | export namespace PayloadCommand { 37 | /** 38 | * The PayloadCommand Options 39 | */ 40 | export type Options = Subcommand.Options & { 41 | hidden?: boolean; 42 | }; 43 | 44 | export type Args = PayloadArgs; 45 | export type Context = Subcommand.Context; 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/types/Augments.d.ts: -------------------------------------------------------------------------------- 1 | import type { CustomFunctionGet, CustomGet } from "#lib/types"; 2 | import type { PayloadCommand } from "#lib/structs/commands/PayloadCommand.ts"; 3 | import type { ArrayString, IntegerString } from "@skyra/env-utilities"; 4 | import { LibSQLDatabase } from "drizzle-orm/libsql"; 5 | import { AutoResponseStore } from "#lib/structs/AutoResponse/AutoResponseStore.ts"; 6 | import type { TFunction, TOptions, TOptionsBase } from "i18next"; 7 | 8 | export type O = object; 9 | 10 | declare module "discord.js" { 11 | interface Client { 12 | readonly dev: boolean; 13 | } 14 | } 15 | 16 | declare module "@sapphire/framework" { 17 | interface Preconditions { 18 | OwnerOnly: never; 19 | } 20 | 21 | interface ArgType { 22 | commandName: PayloadCommand; 23 | } 24 | 25 | interface StoreRegistryEntries { 26 | auto: AutoResponseStore; 27 | } 28 | 29 | export interface Args { 30 | t: TFunction; 31 | } 32 | } 33 | 34 | declare module "@sapphire/pieces" { 35 | interface Container { 36 | database: LibSQLDatabase; 37 | denoServer?: Deno.HttpServer; 38 | } 39 | } 40 | 41 | declare module "i18next" { 42 | export interface TFunction { 43 | ns?: string; 44 | 45 | ( 46 | key: CustomGet, 47 | options?: TOptionsBase | string, 48 | ): TReturn; 49 | 50 | ( 51 | key: CustomGet, 52 | defaultValue: TReturn, 53 | options?: TOptionsBase | string, 54 | ): TReturn; 55 | 56 | ( 57 | key: CustomFunctionGet, 58 | options?: TOptions, 59 | ): TReturn; 60 | 61 | ( 62 | key: CustomFunctionGet, 63 | defaultValue: TReturn, 64 | options?: TOptions, 65 | ): TReturn; 66 | } 67 | } 68 | 69 | declare module "@skyra/env-utilities" { 70 | interface Env { 71 | /** 72 | * The list of bot owners 73 | * @deprecated Fetch from application owners instead 74 | */ 75 | OWNERS: ArrayString; 76 | 77 | /** 78 | * The port to run the HTTP server 79 | */ 80 | PORT: IntegerString; 81 | 82 | /** 83 | * The url of the database to use 84 | */ 85 | DATABASE_URL: string; 86 | 87 | /** 88 | * The ip of the api to listen on 89 | */ 90 | HOST: string; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./utils.ts"; 2 | -------------------------------------------------------------------------------- /src/lib/types/utils.ts: -------------------------------------------------------------------------------- 1 | export type CustomGet = K & { __type__: TCustom }; 2 | 3 | export function T(k: string): CustomGet { 4 | return k as CustomGet; 5 | } 6 | 7 | export type CustomFunctionGet = K & { 8 | __args__: TArgs; 9 | __return__: TReturn; 10 | }; 11 | 12 | export function FT(k: string): CustomFunctionGet { 13 | return k as CustomFunctionGet; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/utils/clientOptions.ts: -------------------------------------------------------------------------------- 1 | import config from "#root/config.ts"; 2 | import { guild } from "#root/drizzle/schema.ts"; 3 | import { container, LogLevel } from "@sapphire/framework"; 4 | import type { 5 | InternationalizationContext, 6 | InternationalizationOptions, 7 | } from "@sapphire/plugin-i18next"; 8 | import { 9 | ActivityType, 10 | type ClientOptions, 11 | GatewayIntentBits, 12 | Options, 13 | Partials, 14 | type PresenceData, 15 | } from "discord.js"; 16 | import { eq } from "drizzle-orm"; 17 | import { join } from "node:path"; 18 | 19 | const isProduction = Deno.env.get("NODE_ENV") === "production"; 20 | 21 | function makeLogger() { 22 | return { 23 | level: isProduction ? LogLevel.Info : LogLevel.Debug, 24 | }; 25 | } 26 | 27 | function getPresence(): PresenceData { 28 | return { 29 | activities: [ 30 | { 31 | name: `payload.tf`, 32 | type: ActivityType.Playing, 33 | }, 34 | ], 35 | }; 36 | } 37 | 38 | function parseI18N(): InternationalizationOptions { 39 | return { 40 | defaultLanguageDirectory: join(Deno.cwd(), "src", "languages"), 41 | fetchLanguage: async (msg: InternationalizationContext) => { 42 | if (msg.guild) { 43 | const [data] = await container.database 44 | .select({ language: guild.language }) 45 | .from(guild) 46 | .where(eq(guild.id, msg.guild.id)); 47 | 48 | return data?.language ?? "en-US"; 49 | } 50 | 51 | return "en-US"; 52 | }, 53 | i18next: (_, languages) => ({ 54 | fallbackLng: "en-US", 55 | preload: languages, 56 | supportedLngs: languages, 57 | load: "all", 58 | lng: "en-US", 59 | returnObjects: true, 60 | interpolation: { 61 | escapeValue: false, 62 | defaultVariables: { 63 | PUSHCART_EMOJI: "<:payload:656955124098269186>", 64 | }, 65 | }, 66 | overloadTranslationOptionHandler: (args) => ({ 67 | defaultValue: args[1] ?? "globals:default", 68 | }), 69 | }), 70 | hmr: { 71 | enabled: !isProduction, 72 | }, 73 | }; 74 | } 75 | 76 | export const CLIENT_OPTIONS: ClientOptions = { 77 | baseUserDirectory: "src/", 78 | caseInsensitivePrefixes: true, 79 | caseInsensitiveCommands: false, 80 | loadMessageCommandListeners: true, 81 | enableLoaderTraceLoggings: true, 82 | loadSubcommandErrorListeners: true, 83 | loadDefaultErrorListeners: true, 84 | preventFailedToFetchLogForGuilds: true, 85 | defaultPrefix: config.PREFIX, 86 | partials: [Partials.Channel], 87 | intents: [ 88 | // Message content 89 | GatewayIntentBits.MessageContent, 90 | 91 | // Need to parse DMS 92 | GatewayIntentBits.DirectMessages, 93 | 94 | // Regex commands 95 | GatewayIntentBits.GuildMessages, 96 | 97 | // General guilds 98 | GatewayIntentBits.Guilds, 99 | ], 100 | logger: makeLogger(), 101 | presence: getPresence(), 102 | i18n: parseI18N(), 103 | makeCache: Options.cacheWithLimits({ 104 | ...Options.DefaultMakeCacheSettings, 105 | ReactionManager: { 106 | maxSize: 10, 107 | }, 108 | GuildMemberManager: { 109 | maxSize: 25, 110 | keepOverLimit: (member) => member.id === member.client.user.id, 111 | }, 112 | GuildMessageManager: { 113 | maxSize: 150, 114 | }, 115 | MessageManager: { 116 | maxSize: 250, 117 | }, 118 | }), 119 | sweepers: { 120 | ...Options.DefaultSweeperSettings, 121 | }, 122 | hmr: { 123 | enabled: !isProduction, 124 | }, 125 | }; 126 | -------------------------------------------------------------------------------- /src/lib/utils/colors.ts: -------------------------------------------------------------------------------- 1 | export enum EmbedColors { 2 | Default = 0, 3 | Black = 0, 4 | Aqua = 1752220, 5 | Green = 3066993, 6 | Blue = 3447003, 7 | Purple = 10181046, 8 | Gold = 15844367, 9 | YELLOW = 16776960, 10 | Orange = 15105570, 11 | Red = 15158332, 12 | GREY = 9807270, 13 | DarkerGrey = 8359053, 14 | Navy = 3426654, 15 | DarkAqua = 1146986, 16 | DarkGreen = 2067276, 17 | DarkBlue = 2123412, 18 | DarkPurple = 7419530, 19 | DarkGold = 12745742, 20 | DarkOrange = 11027200, 21 | DarkRed = 10038562, 22 | DarkGrey = 9936031, 23 | LightGrey = 12370112, 24 | DarkNavy = 2899536, 25 | LiminousVividPink = 16580705, 26 | DarkVividPink = 12320855, 27 | } 28 | 29 | enum PayloadColors { 30 | Admin = EmbedColors.Red, 31 | Payload = EmbedColors.YELLOW, 32 | User = 29913, 33 | Command = 16098851, 34 | Other = EmbedColors.LightGrey, 35 | } 36 | 37 | export default PayloadColors; 38 | -------------------------------------------------------------------------------- /src/lib/utils/database.ts: -------------------------------------------------------------------------------- 1 | import { container } from "@sapphire/pieces"; 2 | import { drizzle } from "drizzle-orm/libsql"; 3 | import { createClient } from "@libsql/client/sqlite3"; 4 | import { envParseString } from "@skyra/env-utilities"; 5 | 6 | const databaseUrl = envParseString("DATABASE_URL"); 7 | 8 | export default function connect() { 9 | const client = createClient({ url: databaseUrl }); 10 | 11 | const db = drizzle(client); 12 | 13 | container.database = db; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/utils/getSteamId.ts: -------------------------------------------------------------------------------- 1 | import { fetch, FetchResultTypes } from "@sapphire/fetch"; 2 | import { ID } from "@node-steam/id"; 3 | import { isNullishOrEmpty } from "@sapphire/utilities"; 4 | 5 | /** 6 | * Tests and retrieves an ID64 from a string 7 | * @param id SteamId to test 8 | * @returns {string|null} Result 9 | */ 10 | export async function getSteamIdFromArgs(id: string) { 11 | if (isNullishOrEmpty(id)) return null; 12 | 13 | let steamID: ID; 14 | 15 | try { 16 | steamID = new ID(id); 17 | } catch { 18 | try { 19 | const data = await fetch(`https://steamcommunity.com/id/${id}`, FetchResultTypes.Text); 20 | 21 | const cssSteamID = data.match(/(765611\d{11})/); 22 | 23 | if (!cssSteamID) return null; 24 | 25 | return (cssSteamID[0].match(/(765611\d{11})/) as RegExpMatchArray)[0]; 26 | } catch { 27 | return null; 28 | } 29 | } 30 | 31 | if (!steamID.isValid()) return null; 32 | 33 | return steamID.getSteamID64(); 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/utils/random.ts: -------------------------------------------------------------------------------- 1 | export function random(min: number, max: number) { 2 | min = Math.ceil(min); 3 | max = Math.floor(max); 4 | return Math.floor(Math.random() * (max - min + 1)) + min; 5 | } 6 | 7 | export function weightedRandom(min: number, max: number, distributionCalculator: (num: number) => number): number; 8 | export function weightedRandom(numberSpread: Array): number; 9 | export function weightedRandom(numberSpeadBlueprints: NumberSpeadBlueprints): number; 10 | 11 | export function weightedRandom( 12 | minOrNumberSpreadOrNumberSpeadBlueprints: number | Array | NumberSpeadBlueprints, 13 | max?: number, 14 | distributionCalculator?: (num: number) => number, 15 | ) { 16 | if (typeof minOrNumberSpreadOrNumberSpeadBlueprints == "number") { 17 | const min = minOrNumberSpreadOrNumberSpeadBlueprints; 18 | 19 | const numberSpread: Array = []; 20 | 21 | for (let i = min; i <= max!; i++) { 22 | numberSpread.push(...new Array(distributionCalculator!(i)).fill(i)); 23 | } 24 | 25 | return numberSpread[random(0, numberSpread.length - 1)]; 26 | } else if (minOrNumberSpreadOrNumberSpeadBlueprints.some((val: number | object) => !!Number(val))) { 27 | const numberSpread = minOrNumberSpreadOrNumberSpeadBlueprints as Array; 28 | 29 | return numberSpread[random(0, numberSpread.length - 1)]; 30 | } else { 31 | const numberSpeadBlueprints = minOrNumberSpreadOrNumberSpeadBlueprints as NumberSpeadBlueprints; 32 | 33 | const numberSpread: Array = []; 34 | 35 | for (let i = 0; i < numberSpeadBlueprints.length; i++) { 36 | numberSpread.push(...new Array(numberSpeadBlueprints[i].weight).fill(numberSpeadBlueprints[i].number)); 37 | } 38 | 39 | return numberSpread[random(0, numberSpread.length - 1)]; 40 | } 41 | } 42 | 43 | type NumberSpeadBlueprints = Array<{ 44 | number: number; 45 | weight: number; 46 | }>; 47 | -------------------------------------------------------------------------------- /src/lib/utils/setup.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | 3 | import "@sapphire/plugin-editable-commands/register"; 4 | import "@sapphire/plugin-logger/register"; 5 | import "@sapphire/plugin-i18next/register"; 6 | import "@sapphire/plugin-hmr/register"; 7 | 8 | import { setup } from "@skyra/env-utilities"; 9 | 10 | setup(new URL("../../../.env", import.meta.url)); 11 | -------------------------------------------------------------------------------- /src/lib/utils/webhook-helper.ts: -------------------------------------------------------------------------------- 1 | import { EmbedColors } from "#utils/colors.ts"; 2 | import { container } from "@sapphire/pieces"; 3 | import { 4 | type Client, 5 | type TextChannel, 6 | type User, 7 | type MessageComponent, 8 | ChannelType, 9 | PermissionFlagsBits, 10 | AttachmentBuilder, 11 | EmbedBuilder, 12 | ActionRowBuilder, 13 | ButtonBuilder, 14 | ButtonStyle, 15 | } from "discord.js"; 16 | import { Buffer } from "node:buffer"; 17 | 18 | type TargetReturnType = TextChannel | User | null; 19 | type WebhookTargetType = "channels" | "users"; 20 | 21 | type LogPreviewArgs = { 22 | webhookTarget: WebhookTargetType; 23 | targetId: string; 24 | logsId: string; 25 | demosId?: string | null; 26 | }; 27 | 28 | export async function checkTarget(client: Client, target: WebhookTargetType, id: string) { 29 | if (target === "users") { 30 | return true; 31 | } 32 | 33 | const targetTextChannel = await client.channels.fetch(id).catch(() => null); 34 | 35 | if (!targetTextChannel) { 36 | // Targeted channel does not exist 37 | return false; 38 | } 39 | 40 | // Targeted channel is not a text channel 41 | if (!targetTextChannel.isTextBased()) { 42 | return false; 43 | } 44 | 45 | if (targetTextChannel.type !== ChannelType.GuildText) { 46 | return false; 47 | } 48 | 49 | const member = await (await client.guilds.fetch(targetTextChannel.guild.id)).members.fetch({ user: client.id! }); 50 | 51 | // I'm not in this guild? 52 | if (!member) { 53 | return false; 54 | } 55 | 56 | if (!targetTextChannel.permissionsFor(member).has(PermissionFlagsBits.SendMessages)) { 57 | return false; 58 | } 59 | 60 | return true; 61 | } 62 | 63 | export async function sendLogPreview(client: Client, { logsId, targetId, demosId, webhookTarget }: LogPreviewArgs) { 64 | const target = (await client[webhookTarget].fetch(targetId).catch(() => null)) as TargetReturnType; 65 | 66 | if (!target) { 67 | throw new Error("Bad discord target id"); 68 | } 69 | 70 | const logsUrl = `https://logs.tf/${logsId}`; 71 | 72 | const preview = await fetch( 73 | `${Deno.env.get("PREVIEW_URL")!}/v0/logstf`, 74 | { 75 | method: "POST", 76 | body: JSON.stringify({ url: logsUrl }), 77 | }, 78 | ); 79 | 80 | const arrayBuffer = await preview.arrayBuffer(); 81 | 82 | const att = new AttachmentBuilder(Buffer.from(arrayBuffer), { name: "log.webp" }); 83 | 84 | const embed = new EmbedBuilder({ 85 | title: "Logs.tf Preview", 86 | footer: { 87 | text: "Rendered from Webhook", 88 | }, 89 | image: { 90 | url: "attachment://log.png", 91 | }, 92 | url: logsUrl, 93 | color: EmbedColors.Green, 94 | timestamp: new Date(), 95 | }); 96 | 97 | const components = []; 98 | 99 | if (demosId != null) { 100 | const demosTfUrl = `https://demos.tf/${demosId}`; 101 | 102 | components.push( 103 | new ActionRowBuilder().addComponents([ 104 | new ButtonBuilder().setURL(demosTfUrl).setLabel("Link to Demo").setStyle(ButtonStyle.Link), 105 | ]), 106 | ); 107 | } 108 | 109 | try { 110 | await sendWebhook(target, embed, att, components); 111 | } catch (err) { 112 | throw err; 113 | } 114 | } 115 | 116 | export async function sendTest(client: Client, scope: WebhookTargetType, id: string) { 117 | if (id == null) { 118 | return false; 119 | } 120 | 121 | const target = (await client[scope].fetch(id).catch(() => null)) as TargetReturnType; 122 | 123 | if (target == null) { 124 | return false; 125 | } 126 | 127 | const embed = new EmbedBuilder({ 128 | title: "Webhook Test", 129 | description: "You will receive log previews in this channel", 130 | footer: { 131 | text: "Rendered from Webhook", 132 | }, 133 | color: EmbedColors.Green, 134 | timestamp: new Date(), 135 | }); 136 | 137 | try { 138 | const data = await sendWebhook(target, embed, null, null); 139 | return data != null; 140 | } catch (err) { 141 | container.logger.error(err); 142 | return false; 143 | } 144 | } 145 | 146 | async function sendWebhook( 147 | target: TextChannel | User, 148 | embed: EmbedBuilder, 149 | attachment: AttachmentBuilder | null, 150 | components: MessageComponent[] | null, 151 | ) { 152 | return await target.send({ 153 | embeds: [embed], 154 | files: attachment ? [attachment] : undefined, 155 | // deno-lint-ignore no-explicit-any 156 | components: components as any, 157 | }); 158 | } 159 | -------------------------------------------------------------------------------- /src/listeners/commands/messageCommandDenied.ts: -------------------------------------------------------------------------------- 1 | import { mapIdentifier } from "../../lib/i18n/mapping.ts"; 2 | import { type MessageCommandDeniedPayload, Events, Listener, UserError } from "@sapphire/framework"; 3 | import { send } from "@sapphire/plugin-editable-commands"; 4 | import { resolveKey } from "@sapphire/plugin-i18next"; 5 | import type { Message } from "discord.js"; 6 | 7 | export class UserListener extends Listener { 8 | public async run(error: UserError, { message, command }: MessageCommandDeniedPayload) { 9 | if (Reflect.get(Object(error.context), "silent")) return; 10 | 11 | const indentifier = mapIdentifier(error.identifier); 12 | // deno-lint-ignore no-explicit-any 13 | const content = await resolveKey(message, indentifier, { message, command, ...(error.context as any) }); 14 | 15 | return await this.alert(message, content as string); 16 | } 17 | 18 | private async alert(message: Message, content: string) { 19 | return await send(message, { 20 | content, 21 | allowedMentions: { users: [message.author.id], roles: [] }, 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/listeners/commands/messageCommandError.ts: -------------------------------------------------------------------------------- 1 | import { Args, ArgumentError, type MessageCommandErrorPayload, Events, Listener, UserError } from "@sapphire/framework"; 2 | import { DiscordAPIError, HTTPError, Message } from "discord.js"; 3 | import { RESTJSONErrorCodes } from "discord-api-types/rest/v9"; 4 | import type { TFunction } from "@sapphire/plugin-i18next"; 5 | import { mapIdentifier } from "#lib/i18n/mapping.ts"; 6 | import { cutText } from "@sapphire/utilities"; 7 | import type { PayloadArgs } from "#lib/structs/commands/PayloadArgs.ts"; 8 | import { send } from "@sapphire/plugin-editable-commands"; 9 | 10 | const ignoredCodes = [RESTJSONErrorCodes.UnknownChannel, RESTJSONErrorCodes.UnknownMessage]; 11 | 12 | export class UserListener extends Listener { 13 | async run(error: Error, { message, args }: MessageCommandErrorPayload) { 14 | const { client, logger } = this.container; 15 | const t = (args as PayloadArgs).t; 16 | 17 | if (typeof error === "string") return logger.error(`Unhandled string error:\n${error}`); 18 | if (error instanceof ArgumentError) return await this.argumentError(message, t, error); 19 | if (error instanceof UserError) return await this.userError(message, t, error); 20 | 21 | // Extract useful information about the DiscordAPIError 22 | if (error instanceof DiscordAPIError || error instanceof HTTPError) { 23 | if (this.isSilencedError(args as Args, error)) return; 24 | client.emit(Events.Error, error); 25 | } else { 26 | logger.warn(`${this.getWarnError(message)} (${message.author.id}) | ${error.constructor.name}`); 27 | logger.info(error.stack); 28 | } 29 | 30 | return undefined; 31 | } 32 | 33 | private isSilencedError(args: Args, error: DiscordAPIError | HTTPError) { 34 | return ( 35 | // If it's an unknown channel or an unknown message, ignore: 36 | ignoredCodes.includes(error.status) || 37 | // If it's a DM message reply after a block, ignore: 38 | this.isDirectMessageReplyAfterBlock(args, error) 39 | ); 40 | } 41 | 42 | private getWarnError(message: Message) { 43 | return `ERROR: /${message.guild ? `${message.guild.id}/${message.channel.id}` : `DM/${message.author.id}`}/${ 44 | message.id 45 | }`; 46 | } 47 | 48 | private isDirectMessageReplyAfterBlock(args: Args, error: DiscordAPIError | HTTPError) { 49 | // When sending a message to a user who has blocked the bot, Discord replies with 50007 "Cannot send messages to this user": 50 | if (error.status !== RESTJSONErrorCodes.CannotSendMessagesToThisUser) return false; 51 | 52 | // If it's not a Direct Message, return false: 53 | if (args.message.guild !== null) return false; 54 | 55 | // If the query was made to the message's channel, then it was a DM response: 56 | return error.url === `/channels/${args.message.channel.id}/messages`; 57 | } 58 | 59 | private async argumentError(message: Message, t: TFunction, error: ArgumentError) { 60 | const argument = error.argument.name; 61 | const identifier = mapIdentifier(error.identifier); 62 | const parameter = error.parameter.replaceAll("`", "῾"); 63 | const content = t(identifier, { 64 | ...error, 65 | ...(error.context as object), 66 | argument, 67 | parameter: cutText(parameter, 50), 68 | }); 69 | return await send(message, content); 70 | } 71 | 72 | private async userError(message: Message, t: TFunction, error: UserError) { 73 | if (Reflect.get(Object(error.context), "silent")) return; 74 | 75 | const identifier = mapIdentifier(error.identifier); 76 | 77 | return await send( 78 | message, 79 | t(identifier, { 80 | // deno-lint-ignore no-explicit-any 81 | ...(error.context as any), 82 | }) as unknown as string, 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/listeners/errors/error.ts: -------------------------------------------------------------------------------- 1 | import { Listener } from "@sapphire/framework"; 2 | import { DiscordAPIError, HTTPError } from "discord.js"; 3 | 4 | const NEWLINE = "\n"; 5 | 6 | export class UserListener extends Listener { 7 | public run(error: Error) { 8 | const { logger } = this.container; 9 | 10 | if (error instanceof DiscordAPIError) { 11 | logger.warn( 12 | `[API ERROR] [CODE: ${error.code}] ${error.message}${NEWLINE}${" ".repeat(12)}[PATH: ${error.method} ${ 13 | error.url 14 | }]`, 15 | ); 16 | logger.fatal(error.stack ? error.stack : error.cause ? error.cause : error); 17 | } else if (error instanceof HTTPError) { 18 | logger.warn( 19 | `[HTTP ERROR] [CODE: ${error.status}] ${error.message}${NEWLINE}${" ".repeat(12)}[PATH: ${error.method} ${ 20 | error.url 21 | }]`, 22 | ); 23 | logger.fatal(error); 24 | } else { 25 | logger.error(error); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/listeners/errors/rateLimit.ts: -------------------------------------------------------------------------------- 1 | import { ApplyOptions } from "@sapphire/decorators"; 2 | import { Listener, type ListenerOptions } from "@sapphire/framework"; 3 | import type { RateLimitData } from "discord.js"; 4 | 5 | @ApplyOptions({ 6 | emitter: "rest", 7 | }) 8 | export class UserListener extends Listener { 9 | public run(rateLimitData: RateLimitData) { 10 | const { logger } = this.container; 11 | 12 | logger.warn( 13 | `[RATE LIMIT ${rateLimitData.global ? "(GLOBAL)" : ""}] [PATH: ${rateLimitData.url} LIMIT: ${ 14 | rateLimitData.limit 15 | }] RESET: ${rateLimitData.timeToReset}`, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/listeners/mentionPrefixOnly.ts: -------------------------------------------------------------------------------- 1 | import { LanguageKeys } from "#lib/i18n/all"; 2 | import config from "#root/config.ts"; 3 | import { guild } from "#root/drizzle/schema.ts"; 4 | import { inlineCode } from "@discordjs/builders"; 5 | import { Events, Listener } from "@sapphire/framework"; 6 | import { send } from "@sapphire/plugin-editable-commands"; 7 | import { fetchT } from "@sapphire/plugin-i18next"; 8 | import type { Message } from "discord.js"; 9 | import { eq } from "drizzle-orm"; 10 | 11 | export class UserListener extends Listener { 12 | public async run(msg: Message) { 13 | const [g] = await this.container.database 14 | .select({ prefix: guild.prefix }) 15 | .from(guild) 16 | .where(eq(guild.id, msg.guildId!)); 17 | 18 | const t = await fetchT(msg); 19 | 20 | const content = t(LanguageKeys.Commands.Prefix.CurrentPrefix, { 21 | prefix: inlineCode(g?.prefix ?? config.PREFIX), 22 | }); 23 | 24 | return await send(msg, content); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/listeners/messages/messageCreate.ts: -------------------------------------------------------------------------------- 1 | import { Events, Listener } from "@sapphire/framework"; 2 | import type { Message } from "discord.js"; 3 | 4 | export class UserListener extends Listener { 5 | public async run(message: Message) { 6 | if (message.webhookId !== null) return; 7 | if (message.system) return; 8 | if (message.author.bot) return; 9 | 10 | const { client } = this.container; 11 | 12 | const autoResponses = client.stores.get("auto"); 13 | 14 | // Check auto responses 15 | // @TODO: Split into listners 16 | for (const autoResponse of autoResponses.values()) { 17 | const doesMatch = autoResponse.shouldRun(message); 18 | 19 | if (!doesMatch) continue; 20 | 21 | const context = { 22 | commandName: autoResponse.name, 23 | matched: autoResponse.getMatch(message), 24 | prefix: autoResponse.getMatch(message)!, 25 | commandPrefix: autoResponse.getMatch(message)!, 26 | }; 27 | 28 | // Run global preconditions: 29 | const globalResult = await this.container.stores 30 | .get("preconditions") 31 | .messageRun(message, autoResponse, { message, command: autoResponse }); 32 | 33 | if (globalResult.isErr()) { 34 | this.container.client.emit(Events.MessageCommandDenied, globalResult.unwrapErr(), { 35 | message, 36 | command: autoResponse, 37 | context, 38 | parameters: "", 39 | }); 40 | } 41 | 42 | // Run command-specific preconditions: 43 | const localResult = await autoResponse.preconditions.messageRun(message, autoResponse, { 44 | message, 45 | command: autoResponse, 46 | }); 47 | 48 | if (localResult.isErr()) { 49 | this.container.client.emit(Events.MessageCommandDenied, localResult.unwrapErr(), { 50 | message, 51 | command: autoResponse, 52 | context, 53 | parameters: "", 54 | }); 55 | 56 | return; 57 | } 58 | 59 | if (message.channel.isSendable()) { 60 | await message.channel.sendTyping(); 61 | } 62 | 63 | const args = await autoResponse.messagePreParse(message, message.content, context); 64 | 65 | return await autoResponse.messageRun(message, args, context); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/listeners/messages/messageDelete.ts: -------------------------------------------------------------------------------- 1 | import { Events, Listener } from "@sapphire/framework"; 2 | import { free } from "@sapphire/plugin-editable-commands"; 3 | import type { Message } from "discord.js"; 4 | 5 | export class UserListener extends Listener { 6 | public run(message: Message) { 7 | free(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/listeners/misc/debug.ts: -------------------------------------------------------------------------------- 1 | import { Events, Listener } from "@sapphire/framework"; 2 | 3 | export class UserListener extends Listener { 4 | public run(message: string) { 5 | if (!this.container.client.dev) { 6 | return; 7 | } 8 | 9 | const { logger } = this.container; 10 | 11 | logger.debug(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/listeners/ready.ts: -------------------------------------------------------------------------------- 1 | import { ApplyOptions } from "@sapphire/decorators"; 2 | import { 3 | Events, 4 | Listener, 5 | type ListenerOptions, 6 | Piece, 7 | Store, 8 | } from "@sapphire/framework"; 9 | import { 10 | blue, 11 | brightMagenta, 12 | gray, 13 | green, 14 | magenta, 15 | white, 16 | yellow, 17 | } from "@std/fmt/colors"; 18 | import type { TFunction } from "@sapphire/plugin-i18next"; 19 | 20 | @ApplyOptions({ 21 | once: true, 22 | }) 23 | export class ReadyEvent extends Listener { 24 | private readonly DEV = this.container.client.dev; 25 | private readonly style = this.DEV ? yellow : blue; 26 | 27 | override run() { 28 | this.printBanner(); 29 | this.printStoreDebugInformation(); 30 | } 31 | 32 | private printBanner() { 33 | const success = green("+"); 34 | 35 | const llc = this.DEV ? brightMagenta : white; 36 | const blc = this.DEV ? magenta : blue; 37 | 38 | const line01 = llc(""); 39 | const line02 = llc(""); 40 | const line03 = llc(""); 41 | 42 | // Offset Pad 43 | const pad = " ".repeat(7); 44 | 45 | console.log( 46 | String.raw` 47 | ${line01} ${pad}${blc(`Payload Version ${Deno.env.get("VERSION") ?? "DEV"}`)} 48 | ${line02} ${pad}[${success}] Gateway 49 | ${line02} ${pad}[${success}] SQL 50 | ${line02} ${pad}[${success}] API 51 | ${line03}${` ${pad}${blc("<")}${llc("/")}${blc(">")} ${ 52 | llc(this.DEV ? "DEVELOPMENT" : "PRODUCTION") 53 | }`} 54 | `.trim(), 55 | ); 56 | } 57 | 58 | private printStoreDebugInformation() { 59 | const { client, logger, i18n } = this.container; 60 | const stores = [...client.stores.values()]; 61 | 62 | for (const store of stores) logger.info(this.styleStore(store)); 63 | 64 | logger.info(this.styleLanguages(i18n.languages)); 65 | } 66 | 67 | private styleStore(store: Store) { 68 | return gray( 69 | `${"├─"} Loaded ${ 70 | this.style(store.size.toString().padEnd(3, " ")) 71 | } ${store.name}.`, 72 | ); 73 | } 74 | 75 | private styleLanguages(languages: Map) { 76 | return gray( 77 | `└─ Loaded ${ 78 | this.style(languages.size.toString().padEnd(3, " ")) 79 | } languages.`, 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/preconditions/OwnerOnly.ts: -------------------------------------------------------------------------------- 1 | import { AllFlowsPrecondition } from "@sapphire/framework"; 2 | import { envParseArray } from "@skyra/env-utilities"; 3 | import type { CommandInteraction, ContextMenuCommandInteraction, Message } from "discord.js"; 4 | 5 | const OWNERS = envParseArray("OWNERS"); 6 | 7 | export class UserPrecondition extends AllFlowsPrecondition { 8 | public override messageRun = (msg: Message) => this.#checkOwner(msg.author.id); 9 | 10 | public override chatInputRun = (interaction: CommandInteraction) => this.#checkOwner(interaction.user.id); 11 | 12 | public override contextMenuRun = (interaction: ContextMenuCommandInteraction) => 13 | this.#checkOwner(interaction.user.id); 14 | 15 | #checkOwner(authorId: string) { 16 | return OWNERS.includes(authorId) ? this.ok() : this.error({ context: { silent: true } }); 17 | } 18 | } 19 | --------------------------------------------------------------------------------