├── .deepsource.toml ├── .ecosystem.config.cjs ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierrc ├── .release-it.json ├── .vscode ├── command.code-snippets ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── rankBg.png ├── biome.json ├── bun.lock ├── crowdin.yml ├── dev └── docker │ ├── docker-compose.postgres.yml │ └── docker-compose.yml ├── eslint.config.js ├── grafana └── interchat-dashboard.json ├── locales ├── da.yml ├── de.yml ├── en.yml ├── es.yml ├── et.yml ├── fi.yml ├── fr.yml ├── hi.yml ├── id.yml ├── it.yml ├── ja.yml ├── ko.yml ├── nl.yml ├── pl.yml ├── pt.yml ├── ro.yml ├── ru.yml ├── sv.yml ├── tr.yml ├── uk.yml └── zh.yml ├── package.json ├── prisma ├── schema-mongo.prisma ├── schema.prisma └── seed │ ├── achievements.ts │ └── tutorials.ts ├── prometheus-template.yml ├── scripts ├── genLocaleTypes.js ├── generate-prometheus-config.js ├── licence.js ├── migrate-ban-data.js ├── migrate-to-postgres.js ├── migrateAntiSwear.js ├── syncCommands.js ├── syncEmojis.js └── utils.js ├── src ├── api │ ├── index.ts │ ├── middleware │ │ └── validation.ts │ └── schemas │ │ ├── reactions.ts │ │ ├── vote.ts │ │ └── webhook.ts ├── client.ts ├── commands │ ├── Config │ │ ├── badges.ts │ │ ├── config │ │ │ ├── index.ts │ │ │ └── set-invite.ts │ │ └── set │ │ │ ├── index.ts │ │ │ ├── language.ts │ │ │ └── reply_mentions.ts │ ├── Hub │ │ ├── blacklist │ │ │ ├── index.ts │ │ │ ├── list.ts │ │ │ ├── server.ts │ │ │ └── user.ts │ │ ├── connect.ts │ │ ├── disconnect.ts │ │ ├── hub │ │ │ ├── announce.ts │ │ │ ├── browse.ts │ │ │ ├── config │ │ │ │ ├── anti-swear.ts │ │ │ │ ├── appealCooldown.ts │ │ │ │ ├── logging.ts │ │ │ │ ├── rules.ts │ │ │ │ ├── settings.ts │ │ │ │ └── welcome.ts │ │ │ ├── create.ts │ │ │ ├── delete.ts │ │ │ ├── edit.ts │ │ │ ├── index.ts │ │ │ ├── infractions.ts │ │ │ ├── invite │ │ │ │ ├── create.ts │ │ │ │ ├── list.ts │ │ │ │ └── revoke.ts │ │ │ ├── leaderboard │ │ │ │ └── calls.ts │ │ │ ├── moderator │ │ │ │ ├── add.ts │ │ │ │ ├── edit.ts │ │ │ │ ├── list.ts │ │ │ │ └── remove.ts │ │ │ ├── servers.ts │ │ │ ├── transfer.ts │ │ │ └── visibility.ts │ │ ├── unblacklist │ │ │ ├── index.ts │ │ │ ├── server.ts │ │ │ └── user.ts │ │ └── warn.ts │ ├── Information │ │ ├── about.ts │ │ ├── achievements.ts │ │ ├── help.ts │ │ ├── invite.ts │ │ ├── profile.ts │ │ ├── rank.ts │ │ ├── rules.ts │ │ ├── stats.ts │ │ ├── support.ts │ │ ├── tutorial │ │ │ ├── index.ts │ │ │ ├── list.ts │ │ │ ├── resume.ts │ │ │ ├── setup.ts │ │ │ └── start.ts │ │ └── vote.ts │ ├── Main │ │ ├── connection │ │ │ ├── edit.ts │ │ │ ├── index.ts │ │ │ ├── list.ts │ │ │ ├── pause.ts │ │ │ └── unpause.ts │ │ ├── deleteMsg.ts │ │ ├── editMsg.ts │ │ ├── inbox.ts │ │ ├── joinserver.ts │ │ ├── leaderboard │ │ │ ├── achievements.ts │ │ │ ├── calls.ts │ │ │ ├── index.ts │ │ │ ├── messages.ts │ │ │ └── votes.ts │ │ ├── messageInfo.ts │ │ ├── modActions.ts │ │ ├── report.ts │ │ └── setup.ts │ ├── Staff │ │ ├── ban.ts │ │ ├── bans.ts │ │ ├── debug │ │ │ ├── fix-server.ts │ │ │ └── index.ts │ │ ├── dev │ │ │ ├── index.ts │ │ │ └── send-alert.ts │ │ ├── find │ │ │ ├── index.ts │ │ │ ├── server.ts │ │ │ └── user.ts │ │ ├── leave.ts │ │ ├── recluster.ts │ │ ├── unban.ts │ │ └── view_reported_call.ts │ └── Userphone │ │ ├── call.ts │ │ ├── hangup.ts │ │ └── skip.ts ├── config │ └── contentFilter.ts ├── core │ ├── BaseClient.ts │ ├── BaseCommand.ts │ ├── BaseEventListener.ts │ ├── CommandContext │ │ ├── ComponentContext.ts │ │ ├── Context.ts │ │ ├── ContextOpts.ts │ │ ├── InteractionContext.ts │ │ └── PrefixContext.ts │ ├── Factory.ts │ └── FileLoader.ts ├── decorators │ └── RegisterInteractionHandler.ts ├── events │ ├── guildCreate.ts │ ├── guildDelete.ts │ ├── guildUpdate.ts │ ├── interactionCreate.ts │ ├── messageCreate.ts │ ├── messageDelete.ts │ ├── messageReactionAdd.ts │ ├── messageUpdate.ts │ ├── ready.ts │ └── webhooksUpdate.ts ├── index.ts ├── instrument.ts ├── interactions │ ├── AchievementFilter.ts │ ├── BanConfirmation.ts │ ├── BlacklistAppeal.ts │ ├── BlacklistCommandHandler.ts │ ├── CallRating.ts │ ├── HubLeaveConfirm.ts │ ├── InactiveConnect.ts │ ├── ModPanel.ts │ ├── ModPanelBanFlow.ts │ ├── NetworkReaction.ts │ ├── ReportAction.ts │ ├── ReportActionButtons.ts │ ├── ReportCall.ts │ ├── ReportMessage.ts │ ├── RulesScreening.ts │ ├── ServerBanModal.ts │ ├── ShowInboxButton.ts │ ├── ShowModPanel.ts │ ├── UnbanConfirmation.ts │ ├── UserBanModal.ts │ └── WarnModal.ts ├── managers │ ├── AntiSpamManager.ts │ ├── AntiSwearManager.ts │ ├── BlacklistManager.ts │ ├── CacheManager.ts │ ├── ConnectionManager.ts │ ├── ContentFilterManager.ts │ ├── HubConnectionsManager.ts │ ├── HubLogManager.ts │ ├── HubManager.ts │ ├── HubModeratorManager.ts │ ├── HubSettingsManager.ts │ ├── InfractionManager.ts │ ├── ServerBanManager.ts │ ├── TutorialManager.ts │ ├── UserBanManager.ts │ ├── VoteManager.ts │ └── tutorial │ │ ├── TutorialInteractionHandler.ts │ │ ├── TutorialListBuilder.ts │ │ ├── TutorialManager.ts │ │ ├── TutorialUIBuilder.ts │ │ └── index.ts ├── modules │ ├── BaseCommands │ │ └── BaseTutorialCommand.ts │ ├── BitFields.ts │ ├── HelpCommand │ │ ├── DataManager.ts │ │ └── HelpCommandUI.ts │ ├── HubValidator.ts │ ├── Loaders │ │ ├── EventLoader.ts │ │ └── InteractionLoader.ts │ ├── NSFWDetection.ts │ └── Pagination.ts ├── scheduled │ ├── startTasks.ts │ └── tasks │ │ ├── cleanupOldMessages.ts │ │ ├── deleteExpiredInvites.ts │ │ ├── expireTemporaryBans.ts │ │ ├── pauseIdleConnections.ts │ │ ├── storeMsgTimestamps.ts │ │ ├── syncBotlistStats.ts │ │ └── updateBlacklists.ts ├── services │ ├── AchievementService.ts │ ├── BroadcastService.ts │ ├── CallService.ts │ ├── CooldownService.ts │ ├── HubJoinService.ts │ ├── HubService.ts │ ├── LevelingService.ts │ ├── MainMetricsService.ts │ ├── MessageFormattingService.ts │ ├── MessageProcessor.ts │ ├── MessageService.ts │ ├── ReportService.ts │ ├── ReputationService.ts │ ├── SchedulerService.ts │ ├── ShardMetricsService.ts │ ├── TutorialService.ts │ ├── UserDbService.ts │ └── formatters │ │ ├── CompactMsgFormatter.ts │ │ └── EmbedMsgFormatter.ts ├── types │ ├── ConnectionTypes.d.ts │ ├── CustomClientProps.d.ts │ ├── TopGGPayload.d.ts │ ├── TranslationKeys.d.ts │ └── Utils.d.ts └── utils │ ├── AchievementUtils.ts │ ├── BadgeUtils.ts │ ├── BanUtils.ts │ ├── ChannelUtls.ts │ ├── CommandUtils.ts │ ├── ComponentUtils.ts │ ├── ConnectedListUtils.ts │ ├── Constants.ts │ ├── ContextUtils.ts │ ├── CustomID.ts │ ├── Db.ts │ ├── DesignSystem.ts │ ├── EmbedUtils.ts │ ├── EmojiUtils.ts │ ├── ErrorUtils.ts │ ├── GuildUtils.ts │ ├── ImageUtils.ts │ ├── JSON │ └── emojis.json │ ├── Leaderboard.ts │ ├── Loaders.ts │ ├── Locale.ts │ ├── Logger.ts │ ├── ProfileUtils.ts │ ├── Redis.ts │ ├── Utils.ts │ ├── VotingUtils.ts │ ├── calculateLevel.ts │ ├── hub │ ├── logger │ │ ├── AntiSwearAlert.ts │ │ ├── Appeals.ts │ │ ├── ContentFilter.ts │ │ ├── Default.ts │ │ ├── JoinLeave.ts │ │ ├── ModLogs.ts │ │ ├── Report.ts │ │ └── Warns.ts │ ├── settings.ts │ └── utils.ts │ ├── moderation │ ├── antiSwear.ts │ ├── blacklistUtils.ts │ ├── deleteMessage.ts │ ├── editMessage.ts │ ├── infractionUtils.ts │ ├── modPanel │ │ ├── handlers │ │ │ ├── RemoveReactionsHandler.ts │ │ │ ├── blacklistHandler.ts │ │ │ ├── deleteMsgHandler.ts │ │ │ ├── serverBanHandler.ts │ │ │ ├── userBanHandler.ts │ │ │ ├── viewInfractions.ts │ │ │ └── warnHandler.ts │ │ └── utils.ts │ └── warnUtils.ts │ ├── network │ ├── Types.d.ts │ ├── antiSwearChecks.ts │ ├── buildConnectionAssets.ts │ ├── buildConnectionAssetsV2.ts │ ├── messageUtils.ts │ ├── runChecks.ts │ ├── storeMessageData.ts │ └── utils.ts │ ├── reaction │ ├── helpers.ts │ ├── reactions.ts │ └── sortReactions.ts │ ├── report │ └── ReportReasons.ts │ └── ui │ └── PaginationManager.ts └── tsconfig.json /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "javascript" 5 | 6 | [analyzers.meta] 7 | environment = [ 8 | "nodejs", 9 | "mongo" 10 | ] -------------------------------------------------------------------------------- /.ecosystem.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'interchat', 5 | script: '.', 6 | env: { 7 | NODE_ENV: 'production', 8 | }, 9 | autorestart: true, 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | ko_fi: interchat 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | # polar: # Replace with a single Polar username 13 | # buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | # thanks_dev: # Replace with a single thanks.dev username 15 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.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 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.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/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: pull_request 3 | 4 | jobs: 5 | tests: 6 | name: Lint Check 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: oven-sh/setup-bun@v2 12 | 13 | - name: Install dependencies 14 | run: bun add eslint 15 | 16 | - name: Lint files 17 | run: bun eslint --no-fix src/ 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | logLevel: 6 | description: "Log level" 7 | required: true 8 | default: "warning" 9 | 10 | permissions: 11 | contents: read # for checkout 12 | 13 | jobs: 14 | release: 15 | name: Release 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write # to be able to publish a GitHub release 19 | issues: write # to be able to comment on released issues 20 | pull-requests: write # to be able to comment on released pull requests 21 | id-token: write # to enable use of OIDC for npm provenance 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: "lts/*" 31 | - name: Install dependencies 32 | run: npm i semantic-release 33 | - name: Release 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | run: npx semantic-release 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules/ 3 | .eslintcache 4 | tsconfig.tsbuildinfo 5 | 6 | # logs 7 | /logs/ 8 | 9 | # Configuration 10 | .env* 11 | 12 | # Unit test coverage 13 | __tests__/ 14 | 15 | # Unit test coverage reports 16 | coverage/ 17 | 18 | # Transpiled source code (production build) 19 | build/ 20 | 21 | # misc 22 | /Privacy.md 23 | .turbo 24 | 25 | # Sentry Auth Token 26 | .sentryclirc 27 | 28 | # Redis dev 29 | dump.rdb 30 | 31 | # dev dbs 32 | dev/docker/db* 33 | 34 | emojis/ 35 | 36 | prometheus.yml 37 | docs/ 38 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | bun lint-staged 2 | bun typecheck -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "printWidth": 100, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "npm": { 3 | "publish": false 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/command.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Define an InterChat Command": { 3 | "scope": "javascript,typescript", 4 | "prefix": ["command", "discord command"], 5 | "body": [ 6 | "import BaseCommand from '#src/core/BaseCommand.js';", 7 | "import Context from '#src/core/CommandContext/Context.js';", 8 | 9 | "export default class $1 extends BaseCommand {", 10 | "\tconstructor() {", 11 | "\t\tsuper({", 12 | "\t\t\tname: '$2',", 13 | "\t\t\tdescription: '$3',", 14 | "\t\t});", 15 | "\t}", 16 | 17 | "\tasync execute(ctx: Context) {", 18 | "\t\t$4", 19 | "\t}", 20 | "}", 21 | ], 22 | "description": "Create a slash command with a name and description.", 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/src/index.ts", 12 | "preLaunchTask": "tsc: build - tsconfig.json", 13 | "autoAttachChildProcesses": true, 14 | "outFiles": ["${workspaceFolder}/build/**/*.js"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | 6 | "eslint.validate": ["javascript", "typescript"], 7 | "eslint.useFlatConfig": true, 8 | "typescript.tsdk": "node_modules/typescript/lib", 9 | "typescript.preferences.importModuleSpecifier": "non-relative", 10 | "sonarlint.connectedMode.project": { 11 | "connectionId": "discord-interchat", 12 | "projectKey": "Discord-InterChat_InterChat" 13 | }, 14 | "codescene.previewCodeHealthMonitoring": true 15 | } 16 | -------------------------------------------------------------------------------- /assets/rankBg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interchatapp/InterChat/5bb0422ed1112f45b08f1efffcc49a20632d314b/assets/rankBg.png -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /locales/en.yml 3 | translation: /locales/%two_letters_code%.yml 4 | -------------------------------------------------------------------------------- /dev/docker/docker-compose.postgres.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:16-alpine 4 | environment: 5 | POSTGRES_USER: postgres 6 | POSTGRES_PASSWORD: postgres 7 | POSTGRES_DB: interchat 8 | ports: 9 | - "5432:5432" 10 | volumes: 11 | - "./db/postgres:/var/lib/postgresql/data" 12 | healthcheck: 13 | test: ["CMD-SHELL", "pg_isready -U postgres"] 14 | interval: 5s 15 | timeout: 5s 16 | retries: 5 17 | 18 | # PostgreSQL admin interface 19 | pgadmin: 20 | image: dpage/pgadmin4 21 | environment: 22 | PGADMIN_DEFAULT_EMAIL: admin@interchat.tech 23 | PGADMIN_DEFAULT_PASSWORD: admin 24 | ports: 25 | - "5050:80" 26 | depends_on: 27 | - postgres 28 | -------------------------------------------------------------------------------- /dev/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | safe-content-ai: 3 | ports: 4 | - 8000:8000 5 | image: steelcityamir/safe-content-ai:latest 6 | 7 | # redis with multithread support 8 | keydb: 9 | image: eqalpha/keydb 10 | ports: 11 | - 127.0.0.1:6379:6379 12 | 13 | prometheus: 14 | image: prom/prometheus:latest 15 | volumes: 16 | - ../../prometheus.yml:/etc/prometheus/prometheus.yml:ro # Updated path to point to file in parent directory 17 | command: 18 | - '--config.file=/etc/prometheus/prometheus.yml' 19 | - '--web.enable-lifecycle' 20 | ports: 21 | - "9090:9090" 22 | restart: unless-stopped 23 | extra_hosts: 24 | - "host.docker.internal:host-gateway" 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interchat", 3 | "private": true, 4 | "version": "5.0.0", 5 | "description": "A growing Discord bot which provides inter-server chat!", 6 | "main": "build/index.js", 7 | "license": "AGPL-3.0-only", 8 | "scripts": { 9 | "start": "node .", 10 | "start:prod": "pm2 start .ecosystem.config.js", 11 | "dev": "nodemon --exec \"bun run build && bun start\" --ext ts,js,json --ignore build/", 12 | "build": "tsc --build", 13 | "typecheck": "tsc --noEmit", 14 | "locale-types": "bun scripts/genLocaleTypes.js", 15 | "sync:commands": "bun scripts/syncCommands.js", 16 | "sync:emojis": "bun scripts/syncEmojis.js", 17 | "release": "release-it", 18 | "lint": "eslint --cache --fix ./src", 19 | "prepare": "husky" 20 | }, 21 | "sponsor": { 22 | "url": "https://ko-fi.com/interchat" 23 | }, 24 | "type": "module", 25 | "dependencies": { 26 | "@hono/node-server": "^1.14.3", 27 | "@hono/zod-validator": "^0.5.0", 28 | "@prisma/client": "^6.8.2", 29 | "@sentry/node": "^9.22.0", 30 | "canvas": "^3.1.0", 31 | "common-tags": "^1.8.2", 32 | "discord-hybrid-sharding": "^2.2.6", 33 | "discord.js": "^14.19.3", 34 | "dotenv": "^16.5.0", 35 | "hono": "^4.7.10", 36 | "husky": "^9.1.7", 37 | "ioredis": "^5.6.1", 38 | "js-yaml": "^4.1.0", 39 | "lodash": "^4.17.21", 40 | "lz-string": "^1.5.0", 41 | "ms": "^2.1.3", 42 | "prom-client": "^15.1.3", 43 | "reflect-metadata": "^0.2.2", 44 | "winston": "^3.17.0", 45 | "zod": "^3.25.28" 46 | }, 47 | "devDependencies": { 48 | "@stylistic/eslint-plugin": "^4.4.0", 49 | "@types/common-tags": "^1.8.4", 50 | "@types/js-yaml": "^4.0.9", 51 | "@types/lodash": "^4.17.17", 52 | "@types/ms": "^2.1.0", 53 | "cz-conventional-changelog": "^3.3.0", 54 | "eslint": "^9.27.0", 55 | "lint-staged": "^16.0.0", 56 | "nodemon": "^3.1.10", 57 | "prettier": "^3.5.3", 58 | "prisma": "^6.8.2", 59 | "release-it": "^19.0.2", 60 | "source-map-support": "^0.5.21", 61 | "typescript": "5.8.3", 62 | "typescript-eslint": "^8.32.1" 63 | }, 64 | "config": { 65 | "commitizen": { 66 | "path": "./node_modules/cz-conventional-changelog" 67 | } 68 | }, 69 | "lint-staged": { 70 | "*.ts": [ 71 | "eslint --cache --fix" 72 | ] 73 | }, 74 | "imports": { 75 | "#src/*.js": "./build/*.js", 76 | "#utils/*.js": "./build/utils/*.js" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /prometheus-template.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | remote_write: 6 | - url: ${GRAFANA_CLOUD_URL} 7 | basic_auth: 8 | username: "${GRAFANA_CLOUD_USERNAME}" 9 | password: ${GRAFANA_CLOUD_API_KEY} 10 | 11 | scrape_configs: 12 | - job_name: 'interchat' 13 | static_configs: 14 | - targets: ['host.docker.internal:${PORT}'] 15 | metrics_path: '/metrics' 16 | - job_name: 'prometheus' 17 | static_configs: 18 | - targets: ['localhost:9090'] -------------------------------------------------------------------------------- /scripts/generate-prometheus-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import fs from 'fs'; 19 | import dotenv from 'dotenv'; 20 | 21 | dotenv.config(); 22 | 23 | const template = fs.readFileSync('prometheus-template.yml', 'utf8'); 24 | 25 | const config = template 26 | .replace('${GRAFANA_CLOUD_URL}', process.env.GRAFANA_CLOUD_URL) 27 | .replace('${GRAFANA_CLOUD_USERNAME}', process.env.GRAFANA_CLOUD_USERNAME) 28 | .replace('${GRAFANA_CLOUD_API_KEY}', process.env.GRAFANA_CLOUD_API_KEY) 29 | .replace('${PORT}', process.env.PORT); 30 | 31 | fs.writeFileSync('prometheus.yml', config); 32 | -------------------------------------------------------------------------------- /scripts/licence.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) ${new Date().getFullYear()} InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | -------------------------------------------------------------------------------- /scripts/migrateAntiSwear.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @ts-check 3 | 4 | /* 5 | * Copyright (C) 2025 InterChat 6 | * 7 | * InterChat is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as published 9 | * by the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * InterChat is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with InterChat. If not, see . 19 | */ 20 | 21 | import db from '../build/utils/Db.js'; 22 | import Logger from '../build/utils/Logger.js'; 23 | import { sanitizeWords } from '../build/utils/moderation/antiSwear.js'; 24 | 25 | /** 26 | * Migrates data from the old BlockWord model to the new AntiSwearRule and AntiSwearPattern models 27 | * for the improved anti-swear system 28 | */ 29 | async function migrateAntiSwearRules() { 30 | try { 31 | Logger.info('Starting migration of anti-swear rules to the improved system...'); 32 | 33 | // Get all existing rules 34 | const oldRules = await db.blockWord.findMany(); 35 | Logger.info(`Found ${oldRules.length} old anti-swear rules to migrate to the improved system`); 36 | 37 | let successCount = 0; 38 | let errorCount = 0; 39 | 40 | for (const oldRule of oldRules) { 41 | try { 42 | // Create new rule 43 | const newRule = await db.antiSwearRule.create({ 44 | data: { 45 | hubId: oldRule.hubId, 46 | name: oldRule.name, 47 | createdBy: oldRule.createdBy, 48 | actions: oldRule.actions, 49 | createdAt: oldRule.createdAt, 50 | updatedAt: oldRule.updatedAt, 51 | }, 52 | }); 53 | 54 | // Split words and create patterns 55 | const words = oldRule.words 56 | .split(',') 57 | .map((w) => w.trim()) 58 | .filter((w) => w); 59 | 60 | await db.antiSwearPattern.createMany({ 61 | data: words.map((word) => ({ 62 | ruleId: newRule.id, 63 | pattern: sanitizeWords(word.replaceAll('.*', '*').replaceAll('\\', '')), 64 | isRegex: word.includes('*'), 65 | })), 66 | }); 67 | 68 | successCount++; 69 | Logger.debug(`Migrated anti-swear rule "${oldRule.name}" with ${words.length} patterns`); 70 | } catch (error) { 71 | errorCount++; 72 | Logger.error(`Failed to migrate anti-swear rule "${oldRule.name}":`, error); 73 | } 74 | } 75 | 76 | Logger.info( 77 | `Anti-swear migration completed: ${successCount} rules migrated successfully, ${errorCount} failed`, 78 | ); 79 | } catch (error) { 80 | Logger.error('Anti-swear migration failed:', error); 81 | } 82 | } 83 | 84 | // Run the migration 85 | migrateAntiSwearRules() 86 | .then(() => { 87 | Logger.info('Anti-swear migration script completed'); 88 | process.exit(0); 89 | }) 90 | .catch((error) => { 91 | Logger.error('Anti-swear migration script failed:', error); 92 | process.exit(1); 93 | }); 94 | -------------------------------------------------------------------------------- /src/api/middleware/validation.ts: -------------------------------------------------------------------------------- 1 | import type { ZodSchema } from 'zod'; 2 | import { zValidator as honoZodValidator } from '@hono/zod-validator'; 3 | import { HTTPException } from 'hono/http-exception'; 4 | import Logger from '#utils/Logger.js'; 5 | 6 | /** 7 | * Creates a middleware that validates the request body against a Zod schema 8 | * @param schema The Zod schema to validate against 9 | * @returns A middleware function 10 | */ 11 | export const validateBody = (schema: T) => 12 | honoZodValidator('json', schema, (result) => { 13 | if (!result.success) { 14 | Logger.warn('Validation error: %O', result.error); 15 | throw new HTTPException(400, { 16 | message: 'Invalid request body', 17 | cause: result.error, 18 | }); 19 | } 20 | }); 21 | 22 | /** 23 | * Creates a middleware that validates query parameters against a Zod schema 24 | * @param schema The Zod schema to validate against 25 | * @returns A middleware function 26 | */ 27 | export const validateQuery = (schema: T) => 28 | honoZodValidator('query', schema, (result) => { 29 | if (!result.success) { 30 | Logger.warn('Validation error: %O', result.error); 31 | throw new HTTPException(400, { 32 | message: 'Invalid query parameters', 33 | cause: result.error, 34 | }); 35 | } 36 | }); 37 | 38 | /** 39 | * Creates a middleware that validates path parameters against a Zod schema 40 | * @param schema The Zod schema to validate against 41 | * @returns A middleware function 42 | */ 43 | export const validateParams = (schema: T) => 44 | honoZodValidator('param', schema, (result) => { 45 | if (!result.success) { 46 | Logger.warn('Validation error: %O', result.error); 47 | throw new HTTPException(400, { 48 | message: 'Invalid path parameters', 49 | cause: result.error, 50 | }); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /src/api/schemas/reactions.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | /** 4 | * Schema for validating reaction update payloads 5 | */ 6 | export const reactionsUpdateSchema = z.object({ 7 | messageId: z.string().regex(/^\d+$/, 'Message ID must be a valid Discord snowflake'), 8 | reactions: z.record(z.array(z.string().regex(/^\d+$/, 'User ID must be a valid Discord snowflake'))), 9 | }); 10 | 11 | export type ReactionsUpdatePayload = z.infer; 12 | -------------------------------------------------------------------------------- /src/api/schemas/vote.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import type { WebhookPayload } from '#types/TopGGPayload.d.ts'; 3 | 4 | /** 5 | * Schema for validating top.gg vote webhook payloads 6 | */ 7 | export const votePayloadSchema = z.object({ 8 | bot: z.string().regex(/^\d+$/, 'Bot ID must be a valid Discord snowflake').optional(), 9 | guild: z.string().regex(/^\d+$/, 'Guild ID must be a valid Discord snowflake').optional(), 10 | user: z.string().regex(/^\d+$/, 'User ID must be a valid Discord snowflake'), 11 | type: z.enum(['upvote', 'test']), 12 | isWeekend: z.boolean().optional(), 13 | query: z.union([ 14 | z.string(), 15 | z.record(z.string()), 16 | ]).optional().default(''), 17 | }); 18 | 19 | export type VotePayload = z.infer; 20 | 21 | /** 22 | * Convert a validated Zod payload to the WebhookPayload type 23 | */ 24 | export const toWebhookPayload = (data: VotePayload): WebhookPayload => ({ 25 | bot: data.bot, 26 | guild: data.guild, 27 | user: data.user, 28 | type: data.type, 29 | isWeekend: data.isWeekend, 30 | query: data.query || '', 31 | }); 32 | -------------------------------------------------------------------------------- /src/api/schemas/webhook.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | /** 4 | * Schema for validating webhook send payloads 5 | */ 6 | export const webhookSchema = z.object({ 7 | webhookUrl: z.string().url('Invalid webhook URL'), 8 | data: z.record(z.any()), 9 | }); 10 | 11 | export type WebhookPayload = z.infer; 12 | 13 | /** 14 | * Schema for validating webhook message operations 15 | */ 16 | export const webhookMessageSchema = z.object({ 17 | webhookUrl: z.string().url('Invalid webhook URL'), 18 | messageId: z.string().regex(/^\d+$/, 'Message ID must be a valid Discord snowflake'), 19 | threadId: z.string().regex(/^\d+$/, 'Thread ID must be a valid Discord snowflake').optional(), 20 | action: z.enum(['fetch', 'edit']), 21 | data: z.record(z.any()).optional(), 22 | }); 23 | 24 | export type WebhookMessagePayload = z.infer; 25 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import '#src/instrument.js'; 19 | import InterChatClient from '#src/core/BaseClient.js'; 20 | import Logger from '#utils/Logger.js'; 21 | import 'dotenv/config'; 22 | 23 | const client = new InterChatClient(); 24 | 25 | client.on('debug', (debug) => Logger.debug(debug)); 26 | client.rest.on('restDebug', (debug) => Logger.debug(debug)); 27 | client.rest.on('rateLimited', (data) => Logger.warn('Rate limited: %O', data)); 28 | 29 | process.on('uncaughtException', (error) => Logger.error(error)); 30 | 31 | client.start(); 32 | -------------------------------------------------------------------------------- /src/commands/Config/badges.ts: -------------------------------------------------------------------------------- 1 | import BaseCommand from '#src/core/BaseCommand.js'; 2 | import type Context from '#src/core/CommandContext/Context.js'; 3 | import UserDbService from '#src/services/UserDbService.js'; 4 | import { ApplicationCommandOptionType } from 'discord.js'; 5 | 6 | export default class BadgesCommand extends BaseCommand { 7 | constructor() { 8 | super({ 9 | name: 'badges', 10 | description: '🏅 Configure your badge display preferences', 11 | types: { slash: true, prefix: true }, 12 | options: [ 13 | { 14 | type: ApplicationCommandOptionType.Boolean, 15 | name: 'show', 16 | description: 'Whether to show or hide your badges in messages', 17 | required: true, 18 | }, 19 | ], 20 | }); 21 | } 22 | 23 | async execute(ctx: Context): Promise { 24 | await ctx.deferReply({ flags: ['Ephemeral'] }); 25 | 26 | const showBadges = ctx.options.getBoolean('show', true); 27 | await new UserDbService().upsertUser(ctx.user.id, { 28 | showBadges, 29 | name: ctx.user.username, 30 | image: ctx.user.displayAvatarURL(), 31 | }); 32 | 33 | await ctx.replyEmbed(showBadges ? 'badges.shown' : 'badges.hidden', { 34 | t: { emoji: ctx.getEmoji('tick_icon') }, 35 | flags: ['Ephemeral'], 36 | edit: true, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/Config/config/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import ConfigSetInviteSubcommand from '#src/commands/Config/config/set-invite.js'; 19 | import BaseCommand from '#src/core/BaseCommand.js'; 20 | import { PermissionsBitField } from 'discord.js'; 21 | export default class ConfigCommand extends BaseCommand { 22 | constructor() { 23 | super({ 24 | name: 'config', 25 | description: 'Configure Server settings for InterChat.', 26 | types: { slash: true, prefix: true }, 27 | defaultPermissions: new PermissionsBitField('ManageMessages'), 28 | subcommands: { 29 | 'set-invite': new ConfigSetInviteSubcommand(), 30 | }, 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/Config/config/set-invite.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BaseCommand from '#src/core/BaseCommand.js'; 19 | import Context from '#src/core/CommandContext/Context.js'; 20 | import db from '#src/utils/Db.js'; 21 | import { ApplicationCommandOptionType, Invite } from 'discord.js'; 22 | 23 | export default class ConfigSetInviteSubcommand extends BaseCommand { 24 | constructor() { 25 | super({ 26 | name: 'set-invite', 27 | description: 28 | 'Set the invite link for the server. People can use it to join through InterChat leaderboards.', 29 | types: { slash: true, prefix: true }, 30 | options: [ 31 | { 32 | type: ApplicationCommandOptionType.String, 33 | name: 'invite', 34 | description: 'The invite link to set for the server. (Leave empty to remove)', 35 | required: false, 36 | }, 37 | ], 38 | }); 39 | } 40 | async execute(ctx: Context) { 41 | if (!ctx.inGuild()) return; 42 | await ctx.deferReply(); 43 | 44 | const inviteLink = ctx.options.getString('invite'); 45 | if (!inviteLink?.length) { 46 | await db.serverData.upsert({ 47 | where: { id: ctx.guild.id }, 48 | create: { id: ctx.guildId }, 49 | update: { inviteCode: null }, 50 | }); 51 | 52 | await ctx.replyEmbed('config.setInvite.removed', { 53 | edit: true, 54 | t: { emoji: ctx.getEmoji('tick_icon') }, 55 | }); 56 | return; 57 | } 58 | 59 | const inviteCode = inviteLink.match(Invite.InvitesPattern)?.[1]; 60 | if (!inviteCode) { 61 | await ctx.replyEmbed('config.setInvite.invalid', { 62 | edit: true, 63 | t: { emoji: ctx.getEmoji('x_icon') }, 64 | }); 65 | return; 66 | } 67 | 68 | const inviteInGuild = (await ctx.guild.invites.fetch()).get(inviteCode); 69 | if (!inviteInGuild) { 70 | await ctx.replyEmbed('config.setInvite.notFromServer', { 71 | edit: true, 72 | t: { emoji: ctx.getEmoji('x_icon') }, 73 | }); 74 | return; 75 | } 76 | 77 | await db.serverData.upsert({ 78 | where: { id: ctx.guild.id }, 79 | create: { id: ctx.guildId, inviteCode }, 80 | update: { inviteCode }, 81 | }); 82 | 83 | await ctx.replyEmbed('config.setInvite.success', { 84 | edit: true, 85 | t: { emoji: ctx.getEmoji('tick_icon') }, 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/commands/Config/set/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import SetLanguage from '#src/commands/Config/set/language.js'; 19 | import ReplyMention from '#src/commands/Config/set/reply_mentions.js'; 20 | import BaseCommand from '#src/core/BaseCommand.js'; 21 | 22 | export default class SetCommand extends BaseCommand { 23 | constructor() { 24 | super({ 25 | name: 'set', 26 | description: 'Set your preferences', 27 | types: { 28 | slash: true, 29 | }, 30 | subcommands: { 31 | language: new SetLanguage(), 32 | reply_mentions: new ReplyMention(), 33 | }, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/Config/set/language.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BaseCommand from '#src/core/BaseCommand.js'; 19 | import type Context from '#src/core/CommandContext/Context.js'; 20 | import AchievementService from '#src/services/AchievementService.js'; 21 | import UserDbService from '#src/services/UserDbService.js'; 22 | import { fetchUserLocale } from '#src/utils/Utils.js'; 23 | import { type supportedLocaleCodes, supportedLocales, t } from '#utils/Locale.js'; 24 | import { ApplicationCommandOptionType } from 'discord.js'; 25 | 26 | const currSupportedLangs = ['en', 'hi', 'es', 'pt', 'zh', 'ru', 'et'] as const; 27 | 28 | export default class SetLanguage extends BaseCommand { 29 | constructor() { 30 | super({ 31 | name: 'language', 32 | description: '🈂️ Set the language in which I should respond to you', 33 | types: { slash: true }, 34 | options: [ 35 | { 36 | name: 'lang', 37 | description: 'The language to set', 38 | type: ApplicationCommandOptionType.String, 39 | required: true, 40 | choices: currSupportedLangs.map((l) => ({ 41 | name: supportedLocales[l].name, 42 | value: l, 43 | })), 44 | }, 45 | ], 46 | }); 47 | } 48 | 49 | async execute(ctx: Context) { 50 | const locale = ctx.options.getString('lang') as supportedLocaleCodes | undefined; 51 | 52 | if (!locale || !Object.keys(supportedLocales).includes(locale)) { 53 | await ctx.reply({ 54 | content: t('errors.invalidLangCode', await fetchUserLocale(ctx.user.id), { 55 | emoji: ctx.getEmoji('info'), 56 | }), 57 | flags: ['Ephemeral'], 58 | }); 59 | return; 60 | } 61 | 62 | const { id, username } = ctx.user; 63 | const userService = new UserDbService(); 64 | await userService.upsertUser(id, { locale, name: username, image: ctx.user.avatarURL() }); 65 | 66 | // Track language change for Polyglot achievement 67 | const achievementService = new AchievementService(); 68 | await achievementService.processEvent('language_change', { 69 | userId: id, 70 | language: locale, 71 | }, ctx.client); 72 | 73 | const langInfo = supportedLocales[locale]; 74 | const lang = `${langInfo.emoji} ${langInfo.name}`; 75 | 76 | await ctx.reply({ 77 | content: ctx.getEmoji('tick_icon') + t('language.set', locale, { lang }), 78 | flags: ['Ephemeral'], 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/commands/Config/set/reply_mentions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import UserDbService from '#src/services/UserDbService.js'; 19 | import type Context from '#src/core/CommandContext/Context.js'; 20 | import BaseCommand from '#src/core/BaseCommand.js'; 21 | import { ApplicationCommandOptionType } from 'discord.js'; 22 | 23 | export default class ReplyMention extends BaseCommand { 24 | constructor() { 25 | super({ 26 | name: 'reply_mentions', 27 | description: '🔔 Get pinged when someone replies to your messages.', 28 | types: { slash: true }, 29 | options: [ 30 | { 31 | type: ApplicationCommandOptionType.Boolean, 32 | name: 'enable', 33 | description: 'Enable this setting', 34 | required: true, 35 | }, 36 | ], 37 | }); 38 | } 39 | 40 | async execute(ctx: Context) { 41 | 42 | const userService = new UserDbService(); 43 | const dbUser = await userService.getUser(ctx.user.id); 44 | 45 | const mentionOnReply = ctx.options.getBoolean('enable') ?? false; 46 | if (!dbUser) { 47 | await userService.createUser({ 48 | id: ctx.user.id, 49 | image: ctx.user.avatarURL(), 50 | name: ctx.user.username, 51 | mentionOnReply, 52 | }); 53 | } 54 | else { 55 | await userService.updateUser(ctx.user.id, { mentionOnReply }); 56 | } 57 | 58 | await ctx.replyEmbed( 59 | `${ctx.getEmoji('tick')} You will ${mentionOnReply ? 'now' : '**no longer**'} get pinged when someone replies to your messages.`, 60 | { flags: ['Ephemeral'] }, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/commands/Hub/blacklist/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BlacklistListSubcommand from '#src/commands/Hub/blacklist/list.js'; 19 | import BlacklistServerSubcommand from '#src/commands/Hub/blacklist/server.js'; 20 | import BlacklistUserSubcommand from '#src/commands/Hub/blacklist/user.js'; 21 | import BaseCommand from '#src/core/BaseCommand.js'; 22 | 23 | export default class BlacklistCommand extends BaseCommand { 24 | constructor() { 25 | super({ 26 | name: 'blacklist', 27 | description: 'Mute/Ban a user or server from your hub.', 28 | types: { prefix: true, slash: true }, 29 | subcommands: { 30 | user: new BlacklistUserSubcommand(), 31 | server: new BlacklistServerSubcommand(), 32 | list: new BlacklistListSubcommand(), 33 | }, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/Hub/hub/browse.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BaseCommand from '#src/core/BaseCommand.js'; 19 | import type Context from '#src/core/CommandContext/Context.js'; 20 | import Constants from '#utils/Constants.js'; 21 | import { stripIndents } from 'common-tags'; 22 | 23 | export default class BrowseCommand extends BaseCommand { 24 | constructor() { 25 | super({ 26 | name: 'browse', 27 | description: '🔍 Browse public hubs and join them!', 28 | types: { slash: true, prefix: true }, 29 | }); 30 | } 31 | 32 | async execute(ctx: Context): Promise { 33 | await ctx.reply({ 34 | content: stripIndents` 35 | ### [🔍 Use the hub-browser on the website!](${Constants.Links.Website}/hubs) 36 | Hey there! This command has been moved to InterChat's website: ${Constants.Links.Website}/hubs as it is much easier to use there with a better interface and more features! 37 | 38 | ${ctx.getEmoji('wand_icon')} **Pro tip:** Check out our full [dashboard](${Constants.Links.Website}/dashboard) to manage your hubs, view analytics, and configure settings visually! 39 | `, 40 | flags: ['Ephemeral'], 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/Hub/hub/invite/revoke.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import HubCommand from '#src/commands/Hub/hub/index.js'; 19 | import BaseCommand from '#src/core/BaseCommand.js'; 20 | import type Context from '#src/core/CommandContext/Context.js'; 21 | import { HubService } from '#src/services/HubService.js'; 22 | import db from '#src/utils/Db.js'; 23 | import { escapeRegexChars } from '#src/utils/Utils.js'; 24 | import { ApplicationCommandOptionType, type AutocompleteInteraction } from 'discord.js'; 25 | 26 | export default class HubInviteRevokeSubcommand extends BaseCommand { 27 | private readonly hubService = new HubService(); 28 | 29 | constructor() { 30 | super({ 31 | name: 'revoke', 32 | description: '🚫 Revoke an invite code to your hub', 33 | types: { slash: true, prefix: true }, 34 | options: [ 35 | { 36 | type: ApplicationCommandOptionType.String, 37 | name: 'code', 38 | description: 'The invite code', 39 | required: true, 40 | }, 41 | ], 42 | }); 43 | } 44 | public async execute(ctx: Context) { 45 | const code = ctx.options.getString('code', true); 46 | 47 | const inviteInDb = await db.hubInvite.findFirst({ 48 | where: { 49 | code, 50 | hub: { 51 | OR: [ 52 | { ownerId: ctx.user.id }, 53 | { 54 | moderators: { 55 | some: { userId: ctx.user.id, role: 'MANAGER' }, 56 | }, 57 | }, 58 | ], 59 | }, 60 | }, 61 | }); 62 | 63 | if (!inviteInDb) { 64 | await ctx.replyEmbed('hub.invite.revoke.invalidCode', { 65 | t: { emoji: ctx.getEmoji('x_icon') }, 66 | flags: ['Ephemeral'], 67 | }); 68 | return; 69 | } 70 | 71 | await db.hubInvite.delete({ where: { code } }); 72 | await ctx.replyEmbed('hub.invite.revoke.success', { 73 | t: { 74 | emoji: ctx.getEmoji('tick_icon'), 75 | inviteCode: code, 76 | }, 77 | flags: ['Ephemeral'], 78 | }); 79 | } 80 | async autocomplete(interaction: AutocompleteInteraction): Promise { 81 | const focusedValue = escapeRegexChars(interaction.options.getFocused()); 82 | const hubChoices = await HubCommand.getModeratedHubs( 83 | focusedValue, 84 | interaction.user.id, 85 | this.hubService, 86 | ); 87 | 88 | await interaction.respond( 89 | hubChoices.map((hub) => ({ 90 | name: hub.data.name, 91 | value: hub.data.name, 92 | })), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/commands/Hub/hub/moderator/list.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import HubCommand, { hubOption } from '#src/commands/Hub/hub/index.js'; 19 | import BaseCommand from '#src/core/BaseCommand.js'; 20 | import type Context from '#src/core/CommandContext/Context.js'; 21 | import { HubService } from '#src/services/HubService.js'; 22 | import { runHubRoleChecksAndReply } from '#src/utils/hub/utils.js'; 23 | import { t } from '#src/utils/Locale.js'; 24 | import { Role } from '#src/generated/prisma/client/client.js'; 25 | import { EmbedBuilder, type AutocompleteInteraction } from 'discord.js'; 26 | 27 | export default class HubModeratorListSubcommand extends BaseCommand { 28 | private readonly hubService = new HubService(); 29 | 30 | constructor() { 31 | super({ 32 | name: 'list', 33 | description: '📜 List all moderators on a hub', 34 | types: { slash: true, prefix: true }, 35 | options: [hubOption], 36 | }); 37 | } 38 | 39 | public async execute(ctx: Context) { 40 | const hubName = ctx.options.getString('hub', true); 41 | const hub = hubName 42 | ? (await this.hubService.findHubsByName(hubName)).at(0) 43 | : undefined; 44 | if ( 45 | !hub || 46 | !(await runHubRoleChecksAndReply(hub, ctx, { 47 | checkIfManager: true, 48 | })) 49 | ) return; 50 | 51 | const locale = await ctx.getLocale(); 52 | const moderators = await hub.moderators.fetchAll(); 53 | await ctx.reply({ 54 | embeds: [ 55 | new EmbedBuilder() 56 | .setTitle('Hub Moderators') 57 | .setDescription( 58 | moderators.size > 0 59 | ? moderators 60 | .map( 61 | (mod, index) => 62 | `${index + 1}. <@${mod.userId}> - ${ 63 | mod.role === Role.MODERATOR 64 | ? 'Moderator' 65 | : 'Hub Manager' 66 | }`, 67 | ) 68 | .join('\n') 69 | : t('hub.moderator.noModerators', locale, { 70 | emoji: ctx.getEmoji('x_icon'), 71 | }), 72 | ) 73 | .setColor('Aqua') 74 | .setTimestamp(), 75 | ], 76 | flags: ['Ephemeral'], 77 | }); 78 | } 79 | 80 | async autocomplete(interaction: AutocompleteInteraction) { 81 | return await HubCommand.handleManagerCmdAutocomplete(interaction, this.hubService); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/commands/Hub/unblacklist/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import UnblacklistserverSubcommand from '#src/commands/Hub/unblacklist/server.js'; 19 | import UnblacklistUserSubcommand from '#src/commands/Hub/unblacklist/user.js'; 20 | import BaseCommand from '#src/core/BaseCommand.js'; 21 | 22 | export default class UnblacklistCommand extends BaseCommand { 23 | constructor() { 24 | super({ 25 | name: 'unblacklist', 26 | description: 'Unblacklist a user or server from your hub.', 27 | types: { prefix: true, slash: true }, 28 | subcommands: { 29 | user: new UnblacklistUserSubcommand(), 30 | server: new UnblacklistserverSubcommand(), 31 | }, 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/Information/invite.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BaseCommand from '#src/core/BaseCommand.js'; 19 | import type Context from '#src/core/CommandContext/Context.js'; 20 | import { fetchUserLocale } from '#src/utils/Utils.js'; 21 | import Constants from '#utils/Constants.js'; 22 | import { t } from '#utils/Locale.js'; 23 | 24 | export default class Invite extends BaseCommand { 25 | constructor() { 26 | super({ 27 | name: 'invite', 28 | description: '👋 Invite me to your server!', 29 | types: { slash: true, prefix: true }, 30 | }); 31 | } 32 | async execute(ctx: Context) { 33 | const locale = await fetchUserLocale(ctx.user.id); 34 | await ctx.reply({ 35 | content: t('invite', locale, { 36 | support: Constants.Links.SupportInvite, 37 | invite: Constants.Links.AppDirectory, 38 | invite_emoji: ctx.getEmoji('plus_icon'), 39 | support_emoji: ctx.getEmoji('code_icon'), 40 | }), 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/Information/profile.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BaseCommand from '#src/core/BaseCommand.js'; 19 | import Context from '#src/core/CommandContext/Context.js'; 20 | import { buildProfileEmbed } from '#src/utils/ProfileUtils.js'; 21 | import { ApplicationCommandOptionType } from 'discord.js'; 22 | 23 | export default class ProfileCommand extends BaseCommand { 24 | constructor() { 25 | super({ 26 | name: 'profile', 27 | description: 'View your profile or someone else\'s InterChat profile.', 28 | types: { slash: true, prefix: true }, 29 | options: [ 30 | { 31 | type: ApplicationCommandOptionType.User, 32 | name: 'user', 33 | description: 'The user to view the profile of.', 34 | required: false, 35 | }, 36 | ], 37 | }); 38 | } 39 | async execute(ctx: Context) { 40 | const user = (await ctx.options.getUser('user')) ?? ctx.user; 41 | 42 | const profileEmbed = await buildProfileEmbed(user, ctx.client); 43 | if (!profileEmbed) { 44 | await ctx.reply('User not found.'); 45 | return; 46 | } 47 | 48 | await ctx.reply({ embeds: [profileEmbed] }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/commands/Information/tutorial/list.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type Context from '#src/core/CommandContext/Context.js'; 19 | import BaseTutorialCommand from '#src/modules/BaseCommands/BaseTutorialCommand.js'; 20 | import { MessageFlags } from 'discord.js'; 21 | 22 | export default class ListCommand extends BaseTutorialCommand { 23 | constructor() { 24 | super({ 25 | name: 'list', 26 | description: 'List all available tutorials', 27 | types: { slash: true, prefix: true }, 28 | }); 29 | } 30 | 31 | async execute(ctx: Context): Promise { 32 | const tutorialManager = this.getTutorialManager(ctx.client); 33 | const { container, actionRow } = await tutorialManager.createTutorialListContainer(ctx, 0); 34 | 35 | await ctx.reply({ 36 | components: [container, ...(actionRow ? [actionRow] : [])], 37 | flags: [MessageFlags.IsComponentsV2], 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/Information/tutorial/resume.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type Context from '#src/core/CommandContext/Context.js'; 19 | import BaseTutorialCommand from '#src/modules/BaseCommands/BaseTutorialCommand.js'; 20 | 21 | export default class ResumeCommand extends BaseTutorialCommand { 22 | constructor() { 23 | super({ 24 | name: 'resume', 25 | description: 'Resume your last tutorial', 26 | types: { slash: true, prefix: true }, 27 | }); 28 | } 29 | 30 | async execute(ctx: Context): Promise { 31 | const userProgress = await this.tutorialService.getUserTutorials(ctx.user.id); 32 | 33 | // Find the most recent in-progress tutorial 34 | const inProgressTutorials = userProgress.filter((p) => !p.completed); 35 | 36 | if (inProgressTutorials.length === 0) { 37 | await this.handleNoTutorialsInProgress(ctx); 38 | return; 39 | } 40 | 41 | // Sort by most recently started 42 | inProgressTutorials.sort((a, b) => 43 | new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime(), 44 | ); 45 | 46 | const mostRecent = inProgressTutorials[0]; 47 | const tutorialManager = this.getTutorialManager(ctx.client); 48 | await tutorialManager.resumeTutorial(ctx, mostRecent.tutorialId); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/commands/Information/tutorial/setup.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type Context from '#src/core/CommandContext/Context.js'; 19 | import BaseTutorialCommand from '#src/modules/BaseCommands/BaseTutorialCommand.js'; 20 | 21 | export default class SetupCommand extends BaseTutorialCommand { 22 | constructor() { 23 | super({ 24 | name: 'setup', 25 | description: 'Start the server setup tutorial (for admins)', 26 | types: { slash: true, prefix: true }, 27 | }); 28 | } 29 | 30 | async execute(ctx: Context): Promise { 31 | // Find the setup tutorial 32 | const setupTutorial = await this.tutorialService.getTutorialByName('Server Setup Guide'); 33 | 34 | if (!setupTutorial) { 35 | await this.handleTutorialNotFound(ctx, 'Server Setup Guide'); 36 | return; 37 | } 38 | 39 | const tutorialManager = this.getTutorialManager(ctx.client); 40 | await tutorialManager.startTutorial(ctx, setupTutorial.id); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/Information/tutorial/start.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type Context from '#src/core/CommandContext/Context.js'; 19 | import BaseTutorialCommand from '#src/modules/BaseCommands/BaseTutorialCommand.js'; 20 | import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js'; 21 | 22 | export default class StartCommand extends BaseTutorialCommand { 23 | constructor() { 24 | super({ 25 | name: 'start', 26 | description: 'Start a specific tutorial', 27 | types: { slash: true, prefix: true }, 28 | options: [ 29 | { 30 | name: 'tutorial', 31 | description: 'The tutorial to start', 32 | type: ApplicationCommandOptionType.String, 33 | required: true, 34 | autocomplete: true, 35 | }, 36 | ], 37 | }); 38 | } 39 | 40 | async execute(ctx: Context): Promise { 41 | const tutorialId = ctx.options.getString('tutorial', true); 42 | const tutorialManager = this.getTutorialManager(ctx.client); 43 | await tutorialManager.startTutorial(ctx, tutorialId); 44 | } 45 | 46 | async autocomplete(interaction: AutocompleteInteraction): Promise { 47 | const focusedOption = interaction.options.getFocused(true); 48 | 49 | if (focusedOption.name === 'tutorial') { 50 | const tutorials = await this.tutorialService.getAllTutorials(); 51 | const userProgress = await this.tutorialService.getUserTutorials(interaction.user.id); 52 | 53 | const choices = tutorials.map((tutorial) => { 54 | const progress = userProgress.find((p) => p.tutorialId === tutorial.id); 55 | let prefix = ''; 56 | 57 | if (progress?.completed) { 58 | prefix = '✅ '; 59 | } 60 | else if (progress) { 61 | prefix = '▶️ '; 62 | } 63 | 64 | return { 65 | name: `${prefix}${tutorial.name} (${tutorial.estimatedTimeMinutes} min)`, 66 | value: tutorial.id, 67 | }; 68 | }); 69 | 70 | const filtered = choices.filter((choice) => 71 | choice.name.toLowerCase().includes(focusedOption.value.toLowerCase()), 72 | ); 73 | 74 | await interaction.respond(filtered.slice(0, 25)); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/commands/Main/connection/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import ConnectionEditSubcommand from '#src/commands/Main/connection/edit.js'; 19 | import ConnectionListSubcommand from '#src/commands/Main/connection/list.js'; 20 | import ConnectionPauseSubcommand from '#src/commands/Main/connection/pause.js'; 21 | import ConnectionUnpauseSubcommand from '#src/commands/Main/connection/unpause.js'; 22 | import BaseCommand from '#src/core/BaseCommand.js'; 23 | import db from '#utils/Db.js'; 24 | import { escapeRegexChars } from '#utils/Utils.js'; 25 | import { 26 | type AutocompleteInteraction, 27 | PermissionsBitField, 28 | } from 'discord.js'; 29 | 30 | export default class ConnectionCommand extends BaseCommand { 31 | constructor() { 32 | super({ 33 | name: 'connection', 34 | description: 'Pause, unpause or edit your connections to hubs in this server.', 35 | types: { prefix: true, slash: true }, 36 | defaultPermissions: new PermissionsBitField('SendMessages'), 37 | contexts: { guildOnly: true }, 38 | subcommands: { 39 | edit: new ConnectionEditSubcommand(), 40 | pause: new ConnectionPauseSubcommand(), 41 | unpause: new ConnectionUnpauseSubcommand(), 42 | list: new ConnectionListSubcommand(), 43 | }, 44 | }); 45 | } 46 | 47 | static async autocomplete(interaction: AutocompleteInteraction): Promise { 48 | const focusedValue = escapeRegexChars(interaction.options.getFocused()); 49 | 50 | const isInDb = await db.connection.findMany({ 51 | where: { 52 | serverId: interaction.guild?.id, 53 | OR: [ 54 | { channelId: { contains: focusedValue } }, 55 | { hub: { name: { contains: focusedValue } } }, 56 | ], 57 | }, 58 | select: { channelId: true, hub: true }, 59 | take: 25, 60 | }); 61 | 62 | const filtered = isInDb?.map(async ({ channelId, hub }) => { 63 | const channel = await interaction.guild?.channels.fetch(channelId).catch(() => null); 64 | return { 65 | name: `${hub?.name} | #${channel?.name ?? channelId}`, 66 | value: channelId, 67 | }; 68 | }); 69 | 70 | await interaction.respond(await Promise.all(filtered)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/commands/Main/leaderboard/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import AchievementsLeaderboardCommand from '#src/commands/Main/leaderboard/achievements.js'; 19 | import CallsLeaderboardCommand from '#src/commands/Main/leaderboard/calls.js'; 20 | import MessagesLeaderboardCommand from '#src/commands/Main/leaderboard/messages.js'; 21 | import VotesLeaderboardCommand from '#src/commands/Main/leaderboard/votes.js'; 22 | import BaseCommand from '#src/core/BaseCommand.js'; 23 | export default class LeaderboardCommand extends BaseCommand { 24 | constructor() { 25 | super({ 26 | name: 'leaderboard', 27 | description: 'View various leaderboards for InterChat.', 28 | types: { slash: true, prefix: true }, 29 | subcommands: { 30 | messages: new MessagesLeaderboardCommand(), 31 | calls: new CallsLeaderboardCommand(), 32 | votes: new VotesLeaderboardCommand(), 33 | achievements: new AchievementsLeaderboardCommand(), 34 | }, 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/Main/leaderboard/votes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BaseCommand from '#src/core/BaseCommand.js'; 19 | import type Context from '#src/core/CommandContext/Context.js'; 20 | import { UIComponents } from '#src/utils/DesignSystem.js'; 21 | import { formatVotingLeaderboard, getVotingLeaderboard } from '#src/utils/Leaderboard.js'; 22 | import { ContainerBuilder, MessageFlags, TextDisplayBuilder } from 'discord.js'; 23 | 24 | export default class VotesLeaderboardCommand extends BaseCommand { 25 | constructor() { 26 | super({ 27 | name: 'votes', 28 | description: 'Shows the global voting leaderboard for InterChat.', 29 | types: { slash: true, prefix: true }, 30 | }); 31 | } 32 | 33 | async execute(ctx: Context) { 34 | const leaderboard = await getVotingLeaderboard(10); 35 | const leaderboardTable = await formatVotingLeaderboard(leaderboard, ctx.client); 36 | 37 | // Create UI components helper 38 | const ui = new UIComponents(ctx.client); 39 | const container = new ContainerBuilder(); 40 | 41 | // Add header 42 | container.addTextDisplayComponents( 43 | ui.createHeader( 44 | 'Global Voting Leaderboard', 45 | 'Vote on top.gg to get on the leaderboard!', 46 | 'topggSparkles', 47 | ), 48 | ); 49 | 50 | // Add leaderboard content 51 | container.addTextDisplayComponents( 52 | new TextDisplayBuilder().setContent( 53 | leaderboardTable.length > 0 ? leaderboardTable : 'No voting data available.', 54 | ), 55 | ); 56 | 57 | await ctx.reply({ 58 | components: [container], 59 | flags: [MessageFlags.IsComponentsV2], 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/Main/report.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BaseCommand from '#src/core/BaseCommand.js'; 19 | import type Context from '#src/core/CommandContext/Context.js'; 20 | import { buildReportReasonDropdown } from '#src/interactions/ReportMessage.js'; 21 | import { findOriginalMessage, getBroadcasts } from '#src/utils/network/messageUtils.js'; 22 | import { fetchUserLocale } from '#src/utils/Utils.js'; 23 | import { ApplicationCommandOptionType, ApplicationCommandType } from 'discord.js'; 24 | 25 | export default class ReportPrefixCommand extends BaseCommand { 26 | constructor() { 27 | super({ 28 | name: 'report', 29 | description: 'Report a message', 30 | types: { 31 | slash: true, 32 | prefix: true, 33 | contextMenu: { name: 'Report Message', type: ApplicationCommandType.Message }, 34 | }, 35 | options: [ 36 | { 37 | name: 'message', 38 | description: 'The message to report', 39 | type: ApplicationCommandOptionType.String, 40 | required: true, 41 | }, 42 | ], 43 | }); 44 | } 45 | 46 | async execute(ctx: Context) { 47 | const targetMsg = await ctx.getTargetMessage('message'); 48 | const originalMsg = targetMsg ? await findOriginalMessage(targetMsg.id) : null; 49 | const broadcastMsgs = originalMsg ? await getBroadcasts(originalMsg.id) : null; 50 | 51 | if (!broadcastMsgs || !originalMsg || !targetMsg) { 52 | await ctx.reply('Please provide a valid message ID or link.'); 53 | return; 54 | } 55 | 56 | const reportedMsgId = 57 | Object.values(broadcastMsgs).find((m) => m.messageId === targetMsg.id)?.messageId ?? 58 | originalMsg.id; 59 | 60 | if (!reportedMsgId) { 61 | await ctx.reply('Please provide a valid message ID or link.'); 62 | return; 63 | } 64 | 65 | const locale = await fetchUserLocale(ctx.user.id); 66 | const selectMenu = buildReportReasonDropdown(reportedMsgId, locale); 67 | 68 | await ctx.reply({ 69 | content: `${ctx.getEmoji('info_icon')} Please select a reason for your report:`, 70 | components: [selectMenu], 71 | flags: ['Ephemeral'], 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/commands/Staff/debug/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import FixServerCommand from './fix-server.js'; 19 | import BaseCommand from '#src/core/BaseCommand.js'; 20 | 21 | export default class DebugCommand extends BaseCommand { 22 | constructor() { 23 | super({ 24 | staffOnly: true, 25 | name: 'debug', 26 | description: 'Debug commands for fixing issues', 27 | types: { 28 | slash: true, 29 | prefix: true, 30 | }, 31 | subcommands: { 32 | 'fix-server': new FixServerCommand(), 33 | }, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/Staff/dev/index.ts: -------------------------------------------------------------------------------- 1 | import DevAnnounceCommand from './send-alert.js'; 2 | import BaseCommand from '#src/core/BaseCommand.js'; 3 | 4 | export default class DevCommand extends BaseCommand { 5 | constructor() { 6 | super({ 7 | staffOnly: true, 8 | name: 'dev', 9 | description: 'ooh spooky', 10 | types: { 11 | slash: true, 12 | prefix: true, 13 | }, 14 | subcommands: { 15 | 'send-alert': new DevAnnounceCommand(), 16 | }, 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/Staff/find/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import FindServerSubcommand from '#src/commands/Staff/find/server.js'; 19 | import FindUserSubcommand from '#src/commands/Staff/find/user.js'; 20 | import BaseCommand from '#src/core/BaseCommand.js'; 21 | 22 | export default class Find extends BaseCommand { 23 | constructor() { 24 | super({ 25 | name: 'find', 26 | description: 'Find a user/server (Staff Only).', 27 | staffOnly: true, 28 | types: { slash: true, prefix: true }, 29 | subcommands: { 30 | server: new FindServerSubcommand(), 31 | user: new FindUserSubcommand(), 32 | }, 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/Staff/leave.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BaseCommand from '#src/core/BaseCommand.js'; 19 | import type Context from '#src/core/CommandContext/Context.js'; 20 | import { isDev, resolveEval } from '#utils/Utils.js'; 21 | import { ApplicationCommandOptionType, type Guild } from 'discord.js'; 22 | 23 | export default class Respawn extends BaseCommand { 24 | constructor() { 25 | super({ 26 | name: 'leave', 27 | description: 'Make me leave a server (dev only)', 28 | staffOnly: true, 29 | types: { slash: true, prefix: true }, 30 | options: [ 31 | { 32 | type: ApplicationCommandOptionType.String, 33 | name: 'server_id', 34 | description: 'The ID of the server to leave.', 35 | required: true, 36 | }, 37 | ], 38 | }); 39 | } 40 | async execute(ctx: Context) { 41 | if (!isDev(ctx.user.id)) { 42 | await ctx.reply({ 43 | content: `${ctx.getEmoji('dnd_anim')} You are not authorized to use this command.`, 44 | flags: ['Ephemeral'], 45 | }); 46 | return; 47 | } 48 | 49 | const guildId = ctx.options.getString('server_id', true); 50 | const leftGuild = resolveEval( 51 | (await ctx.client.cluster.broadcastEval( 52 | async (client, _serverId) => { 53 | const guild = client.guilds.cache.get(_serverId); 54 | 55 | return guild ? await guild.leave() : undefined; 56 | }, 57 | { guildId, context: guildId }, 58 | )) as (Guild | undefined)[], 59 | ); 60 | 61 | await ctx.reply( 62 | `${ctx.getEmoji('tick')} Successfully Left guild ${leftGuild?.name} (${leftGuild?.id})`, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/commands/Staff/recluster.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BaseCommand from '#src/core/BaseCommand.js'; 19 | import { isDev } from '#utils/Utils.js'; 20 | import type Context from '#src/core/CommandContext/Context.js'; 21 | 22 | export default class Respawn extends BaseCommand { 23 | constructor() { 24 | super({ 25 | name: 'recluster', 26 | description: 'Reboot the bot', 27 | staffOnly: true, 28 | types: { slash: true, prefix: true }, 29 | }); 30 | } 31 | 32 | async execute(ctx: Context) { 33 | if (!isDev(ctx.user.id)) { 34 | await ctx.reply({ content: 'No u', flags: ['Ephemeral'] }); 35 | return; 36 | } 37 | 38 | await ctx.reply({ 39 | content: `${ctx.getEmoji('tick')} I'll be back!`, 40 | flags: ['Ephemeral'], 41 | }); 42 | ctx.client.cluster.send('recluster'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/BaseEventListener.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { Awaitable, Client, ClientEvents } from 'discord.js'; 19 | import { type EmojiKeys, getEmoji } from '#src/utils/EmojiUtils.js'; 20 | 21 | export type EventParams = { 22 | [K in keyof ClientEvents]: ClientEvents[K]; 23 | }; 24 | 25 | export default abstract class BaseEventListener { 26 | abstract name: K; 27 | 28 | protected readonly client: Client | null; 29 | 30 | constructor(client: Client | null) { 31 | this.client = client; 32 | } 33 | 34 | protected getEmoji(name: EmojiKeys): string { 35 | if (!this.client?.isReady()) return ''; 36 | return getEmoji(name, this.client); 37 | } 38 | 39 | abstract execute(...args: EventParams[K]): Awaitable; 40 | } 41 | -------------------------------------------------------------------------------- /src/core/CommandContext/InteractionContext.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import Context from '#src/core/CommandContext/Context.js'; 19 | import type { 20 | APIModalInteractionResponseCallbackData, 21 | ChatInputCommandInteraction, 22 | ContextMenuCommandInteraction, 23 | InteractionEditReplyOptions, 24 | InteractionReplyOptions, 25 | InteractionResponse, 26 | JSONEncodable, 27 | Message, 28 | ModalComponentData, 29 | } from 'discord.js'; 30 | 31 | export default class InteractionContext extends Context<{ 32 | interaction: ChatInputCommandInteraction | ContextMenuCommandInteraction; 33 | responseType: Message | InteractionResponse; 34 | }> { 35 | 36 | public get deferred() { 37 | return this.interaction.deferred; 38 | } 39 | 40 | public get replied() { 41 | return this.interaction.replied; 42 | } 43 | 44 | public async deferReply(opts?: { flags?: ['Ephemeral'] }) { 45 | return await this.interaction.deferReply({ flags: opts?.flags }); 46 | } 47 | 48 | public async reply(data: string | InteractionReplyOptions) { 49 | if (this.interaction.replied || this.interaction.deferred) { 50 | return await this.interaction.followUp(data); 51 | } 52 | 53 | return await this.interaction.reply(data); 54 | } 55 | 56 | public async deleteReply() { 57 | await this.interaction.deleteReply(); 58 | } 59 | 60 | public async editReply( 61 | data: string | InteractionEditReplyOptions, 62 | ): Promise | InteractionResponse> { 63 | return await this.interaction.editReply(data); 64 | } 65 | 66 | public async showModal( 67 | data: 68 | | JSONEncodable 69 | | ModalComponentData 70 | | APIModalInteractionResponseCallbackData, 71 | ) { 72 | await this.interaction.showModal(data); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/core/Factory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type InterChatClient from './BaseClient.js'; 19 | 20 | export default abstract class Factory { 21 | protected readonly client: InterChatClient; 22 | 23 | constructor(client: InterChatClient) { 24 | this.client = client; 25 | } 26 | 27 | protected getClient(): InterChatClient { 28 | return this.client; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/decorators/RegisterInteractionHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 19 | import type { Awaitable, MessageComponentInteraction, ModalSubmitInteraction } from 'discord.js'; 20 | import 'reflect-metadata'; 21 | 22 | /** 23 | * Function signature for interaction handlers 24 | * Supports both the new ComponentContext and the legacy raw interaction 25 | */ 26 | export type InteractionFunction = ( 27 | contextOrInteraction: ComponentContext, 28 | rawInteraction?: MessageComponentInteraction | ModalSubmitInteraction, 29 | ) => Awaitable; 30 | 31 | /** 32 | * Decorator to call a specified method when an interaction is created (ie. interactionCreate event) 33 | * @param prefix The prefix for the custom ID 34 | * @param suffix The suffix for the custom ID (optional) 35 | */ 36 | export function RegisterInteractionHandler(prefix: string, suffix = ''): MethodDecorator { 37 | return (targetClass, propertyKey: string | symbol) => { 38 | const realSuffix = suffix ? `:${suffix}` : ''; 39 | const customId = `${prefix}${realSuffix}`; 40 | 41 | const newMeta = [{ customId, methodName: propertyKey }]; 42 | const existing = Reflect.getMetadata('interactions', targetClass.constructor); 43 | 44 | const metadata = existing ? [...existing, ...newMeta] : newMeta; 45 | Reflect.defineMetadata('interactions', metadata, targetClass.constructor); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/events/guildDelete.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { Guild } from 'discord.js'; 19 | import BaseEventListener from '#src/core/BaseEventListener.js'; 20 | import { deleteConnections } from '#utils/ConnectedListUtils.js'; 21 | import { logGuildLeave } from '#utils/GuildUtils.js'; 22 | import Logger from '#utils/Logger.js'; 23 | import { logGuildLeaveToHub } from '#utils/hub/logger/JoinLeave.js'; 24 | import db from '#src/utils/Db.js'; 25 | 26 | export default class Ready extends BaseEventListener<'guildDelete'> { 27 | readonly name = 'guildDelete'; 28 | public async execute(guild: Guild) { 29 | if (!guild.available) return; 30 | 31 | Logger.info(`Left ${guild.name} (${guild.id})`); 32 | 33 | const deletedConnections = await deleteConnections({ serverId: guild.id }); 34 | 35 | deletedConnections.forEach(async (connection) => { 36 | if (connection) await logGuildLeaveToHub(connection.hubId, guild); 37 | }); 38 | 39 | 40 | const infraction = await db.infraction.findFirst({ where: { serverId: guild.id } }); 41 | // only delete guild if it doesn't have any infractions 42 | if (!infraction) await db.serverData.deleteMany({ where: { id: guild.id } }); 43 | 44 | logGuildLeave(guild); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/events/guildUpdate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BaseEventListener from '#src/core/BaseEventListener.js'; 19 | import db from '#src/utils/Db.js'; 20 | import { type Guild } from 'discord.js'; 21 | 22 | export default class Ready extends BaseEventListener<'guildCreate'> { 23 | readonly name = 'guildCreate'; 24 | public async execute(guild: Guild) { 25 | // store guild in database 26 | await db.serverData.upsert({ 27 | where: { id: guild.id }, 28 | create: { id: guild.id, name: guild.name, iconUrl: guild.iconURL() }, 29 | update: { name: guild.name, iconUrl: guild.iconURL() }, 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/events/ready.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { ActivityType, type Client } from 'discord.js'; 19 | import BaseEventListener from '#src/core/BaseEventListener.js'; 20 | import updateBlacklists from '#src/scheduled/tasks/updateBlacklists.js'; 21 | import Scheduler from '#src/services/SchedulerService.js'; 22 | import Logger from '#utils/Logger.js'; 23 | 24 | export default class Ready extends BaseEventListener<'ready'> { 25 | readonly name = 'ready'; 26 | public execute(client: Client) { 27 | Logger.info(`Logged in as ${client.user.tag}!`); 28 | 29 | client.application.emojis.fetch(); 30 | 31 | if (client.cluster.id === 0) { 32 | Logger.debug(`Cluster ${client.cluster.id} is updating blacklists...`); 33 | updateBlacklists(client).then(() => 34 | Logger.debug(`Cluster ${client.cluster.id} has finished updating blacklists!`), 35 | ); 36 | 37 | Logger.debug(`Cluster ${client.cluster.id} is setting up recurring tasks...`); 38 | const scheduler = new Scheduler(); 39 | scheduler.stopTask('deleteExpiredBlacklists'); 40 | scheduler.addRecurringTask('deleteExpiredBlacklists', 30 * 1000, () => 41 | updateBlacklists(client), 42 | ); 43 | Logger.debug(`Cluster ${client.cluster.id} has set up recurring tasks!`); 44 | } 45 | 46 | client.user.setActivity({ 47 | name: `/setup | Cluster ${client.cluster.id}`, 48 | type: ActivityType.Watching, 49 | }); 50 | 51 | client.shardMetrics.updateGuildCount(client.guilds.cache.size); 52 | client.ws.shards.forEach((shard) => client.shardMetrics.updateShardStatus(shard.id, true)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/events/webhooksUpdate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { 19 | ForumChannel, 20 | MediaChannel, 21 | NewsChannel, 22 | TextChannel, 23 | VoiceChannel, 24 | } from 'discord.js'; 25 | import BaseEventListener from '#src/core/BaseEventListener.js'; 26 | import { updateConnection } from '#utils/ConnectedListUtils.js'; 27 | import db from '#utils/Db.js'; 28 | import { t } from '#utils/Locale.js'; 29 | import Logger from '#utils/Logger.js'; 30 | 31 | export default class Ready extends BaseEventListener<'webhooksUpdate'> { 32 | readonly name = 'webhooksUpdate'; 33 | public async execute( 34 | channel: NewsChannel | TextChannel | VoiceChannel | ForumChannel | MediaChannel, 35 | ) { 36 | try { 37 | const connection = await db.connection.findFirst({ 38 | where: { OR: [{ channelId: channel.id }, { parentId: channel.id }], connected: true }, 39 | }); 40 | 41 | if (!connection) return; 42 | 43 | Logger.info(`Webhook for ${channel.id} was updated`); 44 | 45 | const webhooks = await channel.fetchWebhooks(); 46 | const webhook = webhooks.find((w) => w.url === connection.webhookURL); 47 | 48 | // only continue if webhook was deleted 49 | if (webhook) return; 50 | 51 | // disconnect the channel 52 | await updateConnection({ id: connection.id }, { connected: false }); 53 | 54 | // send an alert to the channel 55 | const networkChannel = connection.parentId 56 | ? await channel.client.channels.fetch(connection.channelId) 57 | : channel; 58 | 59 | if (networkChannel?.isSendable()) { 60 | await networkChannel.send( 61 | t('global.webhookNoLongerExists', 'en', { 62 | emoji: this.getEmoji('info'), 63 | }), 64 | ); 65 | } 66 | } 67 | catch (error) { 68 | Logger.error('WebhooksUpdateError:', error); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import '#src/instrument.js'; 19 | import { ClusterManager, HeartbeatManager, ReClusterManager } from 'discord-hybrid-sharding'; 20 | import MainMetricsService from '#src/services/MainMetricsService.js'; 21 | import startTasks from '#src/scheduled/startTasks.js'; 22 | import Logger from '#utils/Logger.js'; 23 | import 'dotenv/config'; 24 | import { startApi } from '#src/api/index.js'; 25 | import db from '#src/utils/Db.js'; 26 | 27 | const shardsPerClusters = 6; 28 | const clusterManager = new ClusterManager('build/client.js', { 29 | token: process.env.DISCORD_TOKEN, 30 | totalShards: 'auto', 31 | totalClusters: 'auto', 32 | shardsPerClusters, 33 | }); 34 | 35 | // Set up metrics service with cluster manager 36 | const metrics = new MainMetricsService(clusterManager, db); 37 | startApi(metrics, clusterManager); 38 | 39 | clusterManager.extend(new HeartbeatManager({ interval: 10 * 1000, maxMissedHeartbeats: 2 })); 40 | clusterManager.extend(new ReClusterManager()); 41 | 42 | clusterManager.on('clusterReady', (cluster) => { 43 | Logger.info( 44 | `Cluster ${cluster.id} is ready with shards ${cluster.shardList[0]}...${cluster.shardList.at(-1)}.`, 45 | ); 46 | 47 | if (cluster.id === clusterManager.totalClusters - 1) { 48 | startTasks(clusterManager); 49 | } 50 | 51 | cluster.on('message', async (message) => { 52 | if (message === 'recluster') { 53 | Logger.info('Recluster requested, starting recluster...'); 54 | const recluster = await clusterManager.recluster?.start({ 55 | restartMode: 'rolling', 56 | totalShards: 'auto', 57 | shardsPerClusters, 58 | }); 59 | 60 | if (recluster?.success) Logger.info('Recluster completed successfully.'); 61 | else Logger.error('Failed to recluster!'); 62 | } 63 | }); 64 | }); 65 | 66 | // spawn clusters and start the api that handles nsfw filter and votes 67 | clusterManager.spawn({ timeout: -1 }); 68 | -------------------------------------------------------------------------------- /src/instrument.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node'; 2 | import Constants from '#utils/Constants.js'; 3 | import 'dotenv/config'; 4 | 5 | if (!Constants.isDevBuild) { 6 | Sentry.init({ 7 | dsn: process.env.SENTRY_DSN, 8 | release: `interchat@${Constants.ProjectVersion}`, 9 | tracesSampleRate: 1.0, 10 | profilesSampleRate: 1.0, 11 | maxValueLength: 1000, 12 | integrations: [ 13 | Sentry.onUncaughtExceptionIntegration({ 14 | exitEvenIfOtherHandlersAreRegistered: false, 15 | }), 16 | ], 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/interactions/CallRating.ts: -------------------------------------------------------------------------------- 1 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 2 | import { RegisterInteractionHandler } from '#src/decorators/RegisterInteractionHandler.js'; 3 | import { CallService } from '#src/services/CallService.js'; 4 | import { ReputationService } from '#src/services/ReputationService.js'; 5 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 6 | import getRedis from '#src/utils/Redis.js'; 7 | 8 | export default class CallRatingHandler { 9 | @RegisterInteractionHandler('rate_call') 10 | async execute(ctx: ComponentContext) { 11 | const { suffix: rating, args: [callId] } = ctx.customId; 12 | 13 | const x_icon = getEmoji('x_icon', ctx.client); 14 | 15 | if (!callId) { 16 | await ctx.reply({ 17 | content: `${x_icon} Invalid rating button. Please try again.`, 18 | flags: ['Ephemeral'], 19 | }); 20 | return; 21 | } 22 | 23 | const callService = new CallService(ctx.client); 24 | const callData = await callService.getEndedCallData(callId); 25 | 26 | if (!callData) { 27 | await ctx.reply({ 28 | content: `${x_icon} Unable to find call data. The call might have ended too long ago.`, 29 | flags: ['Ephemeral'], 30 | }); 31 | return; 32 | } 33 | 34 | // Check if user already rated this call 35 | const ratingKey = `call:rating:${callData.callId}:${ctx.user.id}`; 36 | const hasRated = await getRedis().get(ratingKey); 37 | 38 | if (hasRated) { 39 | await ctx.reply({ 40 | content: `${x_icon} You have already rated this call.`, 41 | flags: ['Ephemeral'], 42 | }); 43 | return; 44 | } 45 | 46 | // Find the other channel's participants 47 | const otherChannelParticipants = callData.participants.find( 48 | (p) => p.channelId !== ctx.channelId, 49 | ); 50 | 51 | if (!otherChannelParticipants || otherChannelParticipants.users.size === 0) { 52 | await ctx.reply({ 53 | content: `${x_icon} Unable to find participants from the other channel.`, 54 | flags: ['Ephemeral'], 55 | }); 56 | return; 57 | } 58 | 59 | // Handle the rating 60 | const reputationService = new ReputationService(); 61 | const ratingValue = rating === 'like' ? 1 : -1; 62 | 63 | for (const userId of otherChannelParticipants.users) { 64 | await reputationService.addRating(userId, ratingValue, { 65 | callId: callData.callId, 66 | raterId: ctx.user.id, 67 | }); 68 | } 69 | 70 | // Mark this call as rated by this user 71 | await getRedis().set(ratingKey, '1', 'EX', 3600 * 24); // 24 hour expiry 72 | 73 | const tick_icon = getEmoji('tick_icon', ctx.client); 74 | 75 | await ctx.reply({ 76 | content: `${tick_icon} Thanks for rating! Your **${rating === 'like' ? 'positive' : 'negative'}** feedback has been recorded for ${otherChannelParticipants.users.size} participant${otherChannelParticipants.users.size > 1 ? 's' : ''}.`, 77 | flags: ['Ephemeral'], 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/interactions/HubLeaveConfirm.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 19 | import { RegisterInteractionHandler } from '#src/decorators/RegisterInteractionHandler.js'; 20 | import { HubService } from '#src/services/HubService.js'; 21 | import { CustomID } from '#src/utils/CustomID.js'; 22 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 23 | import { t } from '#src/utils/Locale.js'; 24 | import { fetchUserLocale } from '#src/utils/Utils.js'; 25 | import { logGuildLeaveToHub } from '#src/utils/hub/logger/JoinLeave.js'; 26 | import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; 27 | 28 | export const hubLeaveConfirmButtons = (channelId: string, hubId: string) => 29 | new ActionRowBuilder().addComponents([ 30 | new ButtonBuilder() 31 | .setCustomId(new CustomID('hub_leave:yes', [channelId, hubId]).toString()) 32 | .setLabel('Yes') 33 | .setStyle(ButtonStyle.Success), 34 | new ButtonBuilder() 35 | .setCustomId(new CustomID('hub_leave:no', [channelId, hubId]).toString()) 36 | .setLabel('No') 37 | .setStyle(ButtonStyle.Danger), 38 | ]); 39 | 40 | export default class ModActionsButton { 41 | private readonly hubService = new HubService(); 42 | 43 | @RegisterInteractionHandler('hub_leave') 44 | async handler(ctx: ComponentContext): Promise { 45 | const [channelId, hubId] = ctx.customId.args; 46 | 47 | if (ctx.customId.suffix === 'no') { 48 | await ctx.deferUpdate(); 49 | await ctx.deleteReply(); 50 | return; 51 | } 52 | 53 | const locale = await fetchUserLocale(ctx.user.id); 54 | 55 | const hub = await this.hubService.fetchHub(hubId); 56 | const success = await hub?.connections.deleteConnection(channelId); 57 | 58 | if (!hub || !success) { 59 | await ctx.editReply({ 60 | content: t('hub.leave.noHub', locale, { 61 | emoji: getEmoji('x_icon', ctx.client), 62 | }), 63 | embeds: [], 64 | components: [], 65 | }); 66 | } 67 | 68 | await ctx.editReply({ 69 | content: t('hub.leave.success', locale, { 70 | channel: `<#${channelId}>`, 71 | emoji: getEmoji('tick_icon', ctx.client), 72 | }), 73 | embeds: [], 74 | components: [], 75 | }); 76 | 77 | // log server leave 78 | if (ctx.guild) { 79 | await logGuildLeaveToHub(hubId, ctx.guild); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/interactions/ServerBanModal.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 19 | import { RegisterInteractionHandler } from '#src/decorators/RegisterInteractionHandler.js'; 20 | import { handleServerBan } from '#src/utils/BanUtils.js'; 21 | 22 | export default class ServerBanModalHandler { 23 | @RegisterInteractionHandler('serverBanModal') 24 | async handleModal(ctx: ComponentContext): Promise { 25 | if (!ctx.isModalSubmit()) return; 26 | 27 | const [serverId] = ctx.customId.args; 28 | 29 | const server = await ctx.client.guilds.fetch(serverId).catch(() => null); 30 | const reason = ctx.getModalFieldValue('reason'); 31 | 32 | await handleServerBan(ctx, serverId, server?.name || serverId, reason); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/interactions/ShowInboxButton.ts: -------------------------------------------------------------------------------- 1 | import { showInbox } from '#src/commands/Main/inbox.js'; 2 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 3 | import { RegisterInteractionHandler } from '#src/decorators/RegisterInteractionHandler.js'; 4 | import UserDbService from '#src/services/UserDbService.js'; 5 | import { CustomID } from '#src/utils/CustomID.js'; 6 | import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; 7 | 8 | export const openInboxButton = 9 | new ActionRowBuilder().addComponents( 10 | new ButtonBuilder() 11 | .setCustomId(new CustomID().setIdentifier('openInbox').toString()) 12 | .setLabel('Open Inbox') 13 | .setEmoji('📬') 14 | .setStyle(ButtonStyle.Secondary), 15 | ); 16 | 17 | export default class OpenInboxButtonHandler { 18 | private readonly userDbService = new UserDbService(); 19 | 20 | @RegisterInteractionHandler('openInbox') 21 | async execute(ctx: ComponentContext) { 22 | await showInbox(ctx, { userDbService: this.userDbService, ephemeral: true }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/interactions/ShowModPanel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 19 | import { RegisterInteractionHandler } from '#src/decorators/RegisterInteractionHandler.js'; 20 | import { buildModPanel } from '#src/interactions/ModPanel.js'; 21 | import { HubService } from '#src/services/HubService.js'; 22 | import { CustomID } from '#src/utils/CustomID.js'; 23 | import db from '#src/utils/Db.js'; 24 | import { InfoEmbed } from '#src/utils/EmbedUtils.js'; 25 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 26 | import { isStaffOrHubMod } from '#src/utils/hub/utils.js'; 27 | import { findOriginalMessage } from '#src/utils/network/messageUtils.js'; 28 | import { ButtonBuilder, ButtonStyle } from 'discord.js'; 29 | 30 | export const modPanelButton = (targetMsgId: string, emoji: string, opts?: { label?: string }) => 31 | new ButtonBuilder() 32 | .setCustomId( 33 | new CustomID() 34 | .setIdentifier('showModPanel') 35 | .setArgs(targetMsgId) 36 | .toString(), 37 | ) 38 | .setStyle(ButtonStyle.Danger) 39 | .setLabel(opts?.label ?? 'Mod Panel') 40 | .setEmoji(emoji); 41 | 42 | export default class ModActionsButton { 43 | @RegisterInteractionHandler('showModPanel') 44 | async handler(ctx: ComponentContext): Promise { 45 | await ctx.deferUpdate(); 46 | 47 | const [messageId] = ctx.customId.args; 48 | 49 | const originalMessage = await findOriginalMessage(messageId); 50 | 51 | const hubService = new HubService(db); 52 | const hub = originalMessage ? await hubService.fetchHub(originalMessage?.hubId) : null; 53 | 54 | if (!originalMessage || !hub || !(await isStaffOrHubMod(ctx.user.id, hub))) { 55 | await ctx.editReply({ components: [] }); 56 | await ctx.reply({ 57 | embeds: [ 58 | new InfoEmbed({ 59 | description: `${getEmoji('slash', ctx.client)} Message was deleted.`, 60 | }), 61 | ], 62 | flags: ['Ephemeral'], 63 | }); 64 | return; 65 | } 66 | 67 | if (!(await isStaffOrHubMod(ctx.user.id, hub))) return; 68 | 69 | const panel = await buildModPanel(ctx, originalMessage); 70 | await ctx.reply({ 71 | components: [panel.container, ...panel.buttons], 72 | flags: ['Ephemeral', 'IsComponentsV2'], 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/interactions/UserBanModal.ts: -------------------------------------------------------------------------------- 1 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 2 | import { RegisterInteractionHandler } from '#src/decorators/RegisterInteractionHandler.js'; 3 | import { handleBan } from '#src/utils/BanUtils.js'; 4 | 5 | export default class WarnModalHandler { 6 | @RegisterInteractionHandler('userBanModal') 7 | async handleModal(ctx: ComponentContext): Promise { 8 | if (!ctx.isModalSubmit()) return; 9 | 10 | const [userId] = ctx.customId.args; 11 | 12 | const user = await ctx.client.users.fetch(userId).catch(() => null); 13 | const reason = ctx.getModalFieldValue('reason'); 14 | 15 | await handleBan(ctx, userId, user, reason); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/interactions/WarnModal.ts: -------------------------------------------------------------------------------- 1 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 2 | import { RegisterInteractionHandler } from '#src/decorators/RegisterInteractionHandler.js'; 3 | import { buildModPanel } from '#src/interactions/ModPanel.js'; 4 | import { t } from '#src/utils/Locale.js'; 5 | import { warnUser } from '#src/utils/moderation/warnUtils.js'; 6 | import { findOriginalMessage } from '#src/utils/network/messageUtils.js'; 7 | 8 | export default class WarnModalHandler { 9 | /** 10 | * ### WARNING: 11 | * This function does NOT check if the user is hub mod. It is assumed that 12 | * the interaction will only be called by `interactions/ModPanel.ts`, 13 | * and that it has already done the necessary checks. 14 | */ 15 | @RegisterInteractionHandler('warnModal') 16 | async handleModal(ctx: ComponentContext) { 17 | await ctx.deferUpdate(); 18 | 19 | if (!ctx.isModalSubmit()) return; 20 | 21 | const [userId, hubId] = ctx.customId.args; 22 | const reason = ctx.getModalFieldValue('reason'); 23 | 24 | await warnUser({ 25 | userId, 26 | hubId, 27 | reason, 28 | moderatorId: ctx.user.id, 29 | client: ctx.client, 30 | }); 31 | 32 | const originalMsg = await findOriginalMessage(userId); 33 | if (!originalMsg) { 34 | return; 35 | } 36 | 37 | await ctx.reply({ 38 | content: t('warn.success', await ctx.getLocale(), { 39 | emoji: ctx.getEmoji('tick_icon'), 40 | name: (await ctx.client.users.fetch(userId)).username, 41 | }), 42 | flags: ['Ephemeral'], 43 | }); 44 | 45 | const { container, buttons } = await buildModPanel(ctx, originalMsg); 46 | await ctx.editReply({ components: [container, ...buttons], flags: ['IsComponentsV2'] }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/managers/HubConnectionsManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { Connection, Prisma } from '#src/generated/prisma/client/client.js'; 19 | import ConnectionManager from '#src/managers/ConnectionManager.js'; 20 | import type HubManager from '#src/managers/HubManager.js'; 21 | import db from '#src/utils/Db.js'; 22 | 23 | export default class HubConnectionsManager { 24 | private readonly hub: HubManager; 25 | 26 | constructor(hub: HubManager) { 27 | this.hub = hub; 28 | } 29 | 30 | async fetchCount() { 31 | return await db.connection.count({ where: { hubId: this.hub.id } }); 32 | } 33 | 34 | async fetch(channelId: string): Promise; 35 | async fetch(): Promise; 36 | async fetch(channelId?: string): Promise { 37 | if (channelId) { 38 | return this.fetchConnection(channelId); 39 | } 40 | 41 | return await this.fetchConnections(); 42 | } 43 | 44 | async createConnection(data: Prisma.ConnectionCreateInput): Promise { 45 | const existingConnection = await this.fetch(data.channelId); 46 | if (existingConnection) { 47 | return null; 48 | } 49 | 50 | const connection = await db.connection.create({ data }); 51 | return new ConnectionManager(connection); 52 | } 53 | 54 | async deleteConnection(channelId: string): Promise { 55 | const connection = await this.fetch(channelId); 56 | if (!connection) { 57 | return null; 58 | } 59 | 60 | await connection.disconnect(); 61 | return connection; 62 | } 63 | 64 | async setConnection(connection: Connection): Promise { 65 | return new ConnectionManager(connection); 66 | } 67 | 68 | private async fetchConnections(): Promise { 69 | const connections = await db.connection.findMany({ 70 | where: { hubId: this.hub.id }, 71 | }); 72 | 73 | if (connections.length === 0) { 74 | return []; 75 | } 76 | 77 | return this.createManagersFromConnections(connections); 78 | } 79 | 80 | private async fetchConnection(channelId: string): Promise { 81 | const connection = await db.connection.findUnique({ 82 | where: { channelId }, 83 | }); 84 | 85 | if (!connection || connection.hubId !== this.hub.id) { 86 | return null; 87 | } 88 | 89 | return this.setConnection(connection); 90 | } 91 | 92 | private createManagersFromConnections(connections: Connection[]): ConnectionManager[] { 93 | return connections.map((conn) => new ConnectionManager(conn)); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/managers/TutorialManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | // Re-export from the new location 19 | export { TutorialManager } from './tutorial/TutorialManager.js'; 20 | -------------------------------------------------------------------------------- /src/managers/tutorial/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | export * from './TutorialManager.js'; 19 | export * from './TutorialUIBuilder.js'; 20 | export * from './TutorialInteractionHandler.js'; 21 | export * from './TutorialListBuilder.js'; 22 | -------------------------------------------------------------------------------- /src/modules/BaseCommands/BaseTutorialCommand.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BaseCommand, { CommandConfig } from '#src/core/BaseCommand.js'; 19 | import type Context from '#src/core/CommandContext/Context.js'; 20 | import { TutorialManager } from '#src/managers/tutorial/index.js'; 21 | import TutorialService from '#src/services/TutorialService.js'; 22 | import type { Client } from 'discord.js'; 23 | 24 | /** 25 | * Base class for all tutorial-related commands 26 | * Provides shared functionality and services 27 | */ 28 | export default abstract class BaseTutorialCommand extends BaseCommand { 29 | protected readonly tutorialService: TutorialService; 30 | 31 | constructor(options: CommandConfig) { 32 | super(options); 33 | this.tutorialService = new TutorialService(); 34 | } 35 | 36 | /** 37 | * Get a TutorialManager instance 38 | * @param client Discord client 39 | * @returns TutorialManager instance 40 | */ 41 | protected getTutorialManager(client: Client): TutorialManager { 42 | return new TutorialManager(client); 43 | } 44 | 45 | /** 46 | * Handle tutorial not found error 47 | * @param ctx Command context 48 | * @param tutorialName Optional tutorial name for more specific error 49 | * @returns Promise 50 | */ 51 | protected async handleTutorialNotFound(ctx: Context, tutorialName?: string): Promise { 52 | const message = tutorialName 53 | ? `${ctx.getEmoji('x_icon')} Tutorial "${tutorialName}" not found. Please try \`/tutorial list\` to see available tutorials.` 54 | : `${ctx.getEmoji('x_icon')} Tutorial not found. Please try \`/tutorial list\` to see available tutorials.`; 55 | 56 | await ctx.reply({ 57 | content: message, 58 | flags: ['Ephemeral'], 59 | }); 60 | } 61 | 62 | /** 63 | * Handle no tutorials in progress error 64 | * @param ctx Command context 65 | * @returns Promise 66 | */ 67 | protected async handleNoTutorialsInProgress(ctx: Context): Promise { 68 | await ctx.reply({ 69 | content: `${ctx.getEmoji('info')} You don't have any tutorials in progress. Use \`/tutorial list\` to see available tutorials.`, 70 | flags: ['Ephemeral'], 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/modules/BitFields.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { BitField } from 'discord.js'; 19 | 20 | export const HubSettingsBits = { 21 | Reactions: 1 << 0, 22 | HideLinks: 1 << 1, 23 | SpamFilter: 1 << 2, 24 | BlockInvites: 1 << 3, 25 | UseNicknames: 1 << 4, 26 | BlockNSFW: 1 << 5, 27 | } as const; 28 | 29 | export type HubSettingsString = keyof typeof HubSettingsBits; 30 | export type SerializedHubSettings = Record; 31 | 32 | export class HubSettingsBitField extends BitField { 33 | public static readonly Flags = HubSettingsBits; 34 | 35 | /** 36 | * Toggles the specified hub settings. 37 | * If the settings are already present, they will be removed. 38 | * If the settings are not present, they will be added. 39 | * @param setting - The hub settings to toggle. 40 | * @returns The updated hub settings. 41 | */ 42 | public toggle(...setting: HubSettingsString[]) { 43 | return this.has(setting) ? this.remove(setting) : this.add(setting); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/Loaders/EventLoader.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { dirname, join } from 'node:path'; 19 | import { fileURLToPath } from 'node:url'; 20 | import { type Client, type ClientEvents, Collection } from 'discord.js'; 21 | import type InterChatClient from '#src/core/BaseClient.js'; 22 | import type BaseEventListener from '#src/core/BaseEventListener.js'; 23 | import { FileLoader } from '#src/core/FileLoader.js'; 24 | 25 | const __dirname = dirname(fileURLToPath(import.meta.url)); 26 | 27 | export default class EventLoader { 28 | private readonly listeners: Map> = new Collection(); 29 | private readonly client: Client; 30 | private readonly fileLoader: FileLoader; 31 | public readonly folderPath = join(__dirname, '..', '..', 'events'); 32 | 33 | constructor(client: InterChatClient) { 34 | this.client = client; 35 | this.fileLoader = new FileLoader(this.folderPath); 36 | } 37 | 38 | /** Loads all event listeners from the `events` directory. */ 39 | public async load(): Promise { 40 | await this.fileLoader.loadFiles(this.registerListener.bind(this)); 41 | } 42 | 43 | private async registerListener(filePath: string) { 44 | const { default: Listener } = await FileLoader.import<{ 45 | default: new (client: Client) => BaseEventListener; 46 | }>(filePath); 47 | 48 | const listener = new Listener(this.client); 49 | this.listeners.set(listener.name, listener); 50 | this.client.on(listener.name, listener.execute.bind(listener)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/modules/Loaders/InteractionLoader.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { 19 | type Class, 20 | FileLoader, 21 | loadMetadata, 22 | type ResourceLoader, 23 | } from '#src/core/FileLoader.js'; 24 | import type { InteractionFunction } from '#src/decorators/RegisterInteractionHandler.js'; 25 | import Logger from '#utils/Logger.js'; 26 | import type { Collection } from 'discord.js'; 27 | import { join } from 'node:path'; 28 | 29 | const __dirname = new URL('.', import.meta.url).pathname; 30 | export class InteractionLoader implements ResourceLoader { 31 | private readonly map: Collection; 32 | private readonly fileLoader: FileLoader; 33 | 34 | constructor(map: Collection) { 35 | this.map = map; 36 | this.fileLoader = new FileLoader(join(__dirname, '..', '..', 'interactions'), { 37 | recursive: true, 38 | }); 39 | } 40 | 41 | async load(): Promise { 42 | Logger.debug('Loading interactions'); 43 | await this.fileLoader.loadFiles(this.processFile.bind(this)); 44 | Logger.debug('Finished loading interactions'); 45 | } 46 | 47 | private async processFile(filePath: string): Promise { 48 | Logger.debug(`Importing interaction file: ${filePath}`); 49 | const imported = await FileLoader.import<{ default: Class }>(filePath); 50 | const interactionHandler = new imported.default(); 51 | loadMetadata(interactionHandler, this.map); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/modules/NSFWDetection.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | export interface NSFWPrediction { 19 | is_nsfw: boolean; 20 | confidence_percentage: number; 21 | } 22 | 23 | export default class NSFWDetector { 24 | readonly imageURL: string; 25 | private readonly endpoint = 'http://localhost:8000/v1/detect/urls'; 26 | constructor(imageURL: string) { 27 | this.imageURL = imageURL; 28 | } 29 | 30 | async analyze(): Promise { 31 | const res = await fetch(this.endpoint, { 32 | method: 'POST', 33 | headers: { 'Content-Type': 'application/json' }, 34 | body: JSON.stringify({ urls: [this.imageURL] }), 35 | }); 36 | 37 | const data = await res.json(); 38 | if (res.status !== 200) throw new Error('Failed to analyze image:', data); 39 | return new NSFWResult(data); 40 | } 41 | } 42 | 43 | class NSFWResult { 44 | private readonly rawResults; 45 | constructor(rawResults: NSFWPrediction[]) { 46 | this.rawResults = rawResults; 47 | } 48 | 49 | get isNSFW(): boolean { 50 | return this.rawResults.some((result) => result.is_nsfw); 51 | } 52 | 53 | get confidence(): number { 54 | return ( 55 | this.rawResults.reduce((acc, result) => acc + result.confidence_percentage, 0) / 56 | this.rawResults.length 57 | ); 58 | } 59 | 60 | public exceedsSafeThresh(minConfidence = 80): boolean { 61 | // currently only checking the first prediction 62 | // since we never broadcast multiple images at once 63 | const prediction = this.rawResults[0]; 64 | return Boolean(prediction.confidence_percentage >= minConfidence); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/scheduled/startTasks.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { ClusterManager } from 'discord-hybrid-sharding'; 19 | import deleteExpiredInvites from '#src/scheduled/tasks/deleteExpiredInvites.js'; 20 | // import pauseIdleConnections from '#src/scheduled/tasks/pauseIdleConnections.js'; 21 | import storeMsgTimestamps from '#src/scheduled/tasks/storeMsgTimestamps.js'; 22 | import syncBotlistStats from '#src/scheduled/tasks/syncBotlistStats.js'; 23 | import cleanupOldMessages from '#src/scheduled/tasks/cleanupOldMessages.js'; 24 | import expireTemporaryBans from '#src/scheduled/tasks/expireTemporaryBans.js'; 25 | import Scheduler from '#src/services/SchedulerService.js'; 26 | import Constants from '#src/utils/Constants.js'; 27 | import Logger from '#src/utils/Logger.js'; 28 | 29 | export default function startTasks(clusterManager: ClusterManager) { 30 | // pauseIdleConnections().catch(Logger.error); 31 | deleteExpiredInvites().catch(Logger.error); 32 | 33 | const scheduler = new Scheduler(); 34 | 35 | // store network message timestamps to Connection every minute 36 | scheduler.addRecurringTask('storeMsgTimestamps', 10 * 60 * 1000, storeMsgTimestamps); 37 | 38 | // Expire temporary bans every 5 minutes 39 | scheduler.addRecurringTask('expireTemporaryBans', 5 * 60 * 1000, expireTemporaryBans); 40 | 41 | // Run cleanup tasks every hour 42 | scheduler.addRecurringTask('cleanupTasks', 60 * 60 * 1000, () => { 43 | deleteExpiredInvites().catch(Logger.error); 44 | // pauseIdleConnections().catch(Logger.error); 45 | }); 46 | 47 | // Clean up old messages every 12 hours 48 | scheduler.addRecurringTask('cleanupOldMessages', 12 * 60 * 60 * 1000, () => { 49 | cleanupOldMessages().catch(Logger.error); 50 | }); 51 | 52 | // production only tasks 53 | if (!Constants.isDevBuild) { 54 | scheduler.addRecurringTask('syncBotlistStats', 10 * 60 * 10_000, async () => { 55 | const servers = await clusterManager.fetchClientValues('guilds.cache.size'); 56 | const serverCount = servers.reduce((p: number, n: number) => p + n, 0); 57 | syncBotlistStats(serverCount); 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/scheduled/tasks/cleanupOldMessages.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import messageService from '#src/services/MessageService.js'; 19 | import Logger from '#src/utils/Logger.js'; 20 | 21 | /** 22 | * Scheduled task to clean up old messages from the database. 23 | * Deletes messages older than 24 hours. 24 | */ 25 | export default async () => { 26 | try { 27 | Logger.info('Starting cleanup of old messages...'); 28 | 29 | // Delete messages older than 24 hours 30 | const deletedCount = await messageService.deleteOldMessages(24); 31 | 32 | Logger.info(`Successfully deleted ${deletedCount} old messages from the database.`); 33 | } 34 | catch (error) { 35 | Logger.error('Error cleaning up old messages:', error); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/scheduled/tasks/deleteExpiredInvites.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import db from '#utils/Db.js'; 19 | 20 | export default async () => { 21 | const olderThan1h = new Date(Date.now() - 60 * 60 * 1_000); 22 | await db.hubInvite.deleteMany({ where: { expires: { lte: olderThan1h } } }).catch(() => null); 23 | }; 24 | -------------------------------------------------------------------------------- /src/scheduled/tasks/expireTemporaryBans.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BanManager from '#src/managers/UserBanManager.js'; 19 | import Logger from '#src/utils/Logger.js'; 20 | 21 | /** 22 | * Scheduled task to expire temporary bans that have reached their expiration time 23 | * This task runs every 5 minutes to ensure timely ban expiration 24 | */ 25 | export default async (): Promise => { 26 | try { 27 | const banManager = new BanManager(); 28 | const expiredCount = await banManager.expireTemporaryBans(); 29 | 30 | if (expiredCount > 0) { 31 | Logger.info(`Expired ${expiredCount} temporary bans`); 32 | } 33 | } 34 | catch (error) { 35 | Logger.error('Error expiring temporary bans:', error); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/scheduled/tasks/storeMsgTimestamps.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import db from '#src/utils/Db.js'; 19 | import getRedis from '#src/utils/Redis.js'; 20 | import { updateConnection } from '#utils/ConnectedListUtils.js'; 21 | import { RedisKeys } from '#utils/Constants.js'; 22 | import Logger from '#utils/Logger.js'; 23 | 24 | export default async () => { 25 | const exists = await getRedis().exists(`${RedisKeys.msgTimestamp}`); 26 | if (!exists) return; 27 | 28 | const timestampsObj = await getRedis().hgetall(`${RedisKeys.msgTimestamp}`); 29 | const hubTimestamps = new Map(); 30 | 31 | for (const [channelId, timestamp] of Object.entries(timestampsObj)) { 32 | const parsedTimestamp = Number.parseInt(timestamp); 33 | 34 | // Update connection's lastActive 35 | await updateConnection({ channelId }, { lastActive: new Date(parsedTimestamp) }); 36 | Logger.debug(`Stored message timestamps for channel ${channelId} from cache to db.`); 37 | 38 | // Get hubId for this connection 39 | const connection = await db.connection.findFirst({ 40 | where: { channelId }, 41 | select: { hubId: true }, 42 | }); 43 | if (connection) { 44 | const currentTimestamp = hubTimestamps.get(connection.hubId); 45 | // Only update if this timestamp is higher than what we already have 46 | if (!currentTimestamp || parsedTimestamp > currentTimestamp) { 47 | hubTimestamps.set(connection.hubId, parsedTimestamp); 48 | } 49 | } 50 | 51 | await getRedis().hdel(`${RedisKeys.msgTimestamp}`, channelId); 52 | } 53 | 54 | // Update hubs with their latest activity timestamps 55 | for (const [hubId, timestamp] of hubTimestamps) { 56 | await db.hub.update({ 57 | where: { id: hubId }, 58 | data: { lastActive: new Date(timestamp) }, 59 | }); 60 | Logger.debug(`Updated lastActive for hub ${hubId} to ${new Date(timestamp)}`); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /src/scheduled/tasks/syncBotlistStats.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { handleError } from '#src/utils/Utils.js'; 19 | import Logger from '#utils/Logger.js'; 20 | 21 | const logPostError = (error: unknown) => { 22 | handleError(error, { comment: 'Failed to update top.gg stats' }); 23 | }; 24 | 25 | const logPostSuccess = (server_count: number) => { 26 | Logger.info(`[TopGGPostStats]: Updated top.gg stats with ${server_count} guilds`); 27 | }; 28 | 29 | export default async (server_count: number) => { 30 | if (process.env.CLIENT_ID !== '769921109209907241') { 31 | Logger.warn( 32 | '[TopGGPostStats]: CLIENT_ID environment variable does not match InterChat\'s actual ID.', 33 | ); 34 | return; 35 | } 36 | 37 | await fetch('https://top.gg/api/bots/769921109209907241/stats', { 38 | method: 'POST', 39 | body: JSON.stringify({ server_count }), 40 | headers: { 41 | 'Content-Type': 'application/json', 42 | Authorization: process.env.TOPGG_API_KEY as string, 43 | }, 44 | }) 45 | .then(async (res) => { 46 | 47 | if (res.status !== 200) { 48 | const data = await res.json().catch(() => res.statusText); 49 | logPostError(data); 50 | return; 51 | } 52 | 53 | logPostSuccess(server_count); 54 | }) 55 | .catch(logPostError); 56 | }; 57 | -------------------------------------------------------------------------------- /src/scheduled/tasks/updateBlacklists.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { Client } from 'discord.js'; 19 | import BlacklistManager from '#src/managers/BlacklistManager.js'; 20 | import HubManager from '#src/managers/HubManager.js'; 21 | import db from '#utils/Db.js'; 22 | import { logServerUnblacklist, logUserUnblacklist } from '#utils/hub/logger/ModLogs.js'; 23 | 24 | export default async (client: Client) => { 25 | const allInfractions = await db.infraction.findMany({ 26 | where: { status: 'ACTIVE', expiresAt: { not: null, lte: new Date() } }, 27 | include: { hub: true }, 28 | }); 29 | 30 | allInfractions?.forEach(async (infrac) => { 31 | const type = infrac.userId ? 'user' : 'server'; 32 | const targetId = infrac.userId ?? infrac.serverId!; 33 | 34 | const blacklistManager = new BlacklistManager(type, targetId); 35 | await blacklistManager.removeBlacklist(infrac.hubId); 36 | 37 | if (client.user) { 38 | const opts = { 39 | id: targetId, 40 | mod: client.user, 41 | reason: 'Blacklist duration expired.', 42 | }; 43 | if (type === 'user') { 44 | await logUserUnblacklist(client, new HubManager(infrac.hub), opts); 45 | } 46 | else if (type === 'server') { 47 | await logServerUnblacklist(client, new HubManager(infrac.hub), opts); 48 | } 49 | } 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /src/services/CooldownService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { RedisKeys } from '#utils/Constants.js'; 19 | import getRedis from '#utils/Redis.js'; 20 | 21 | /** Manage and store individual cooldowns */ 22 | export default class CooldownService { 23 | private readonly redisClient = getRedis(); 24 | private readonly prefix = RedisKeys.cooldown; 25 | 26 | private getKey(id: string) { 27 | return `${this.prefix}:${id}`; 28 | } 29 | /** 30 | * Set a cooldown 31 | * @param id A unique id for the cooldown. Eg. : 32 | * @param ms The duration of the cooldown in milliseconds 33 | */ 34 | public async setCooldown(id: string, ms: number) { 35 | await this.redisClient.set(this.getKey(id), Date.now() + ms, 'PX', ms); 36 | } 37 | 38 | /** Get a cooldown */ 39 | public async getCooldown(id: string) { 40 | return Number.parseInt((await this.redisClient.get(this.getKey(id))) || '0'); 41 | } 42 | 43 | /** Delete a cooldown */ 44 | public async deleteCooldown(id: string) { 45 | await this.redisClient.del(this.getKey(id)); 46 | } 47 | 48 | /** Get the remaining cooldown in milliseconds */ 49 | public async getRemainingCooldown(id: string) { 50 | const cooldown = await this.getCooldown(id); 51 | return cooldown ? cooldown - Date.now() : 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/services/MessageFormattingService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { Hub } from '#src/generated/prisma/client/client.js'; 19 | import { RequiredConnectionData } from '#src/services/MessageProcessor.js'; 20 | import type { BroadcastOpts, ReferredMsgData } from '#utils/network/Types.js'; 21 | import { 22 | type ActionRowBuilder, 23 | type ButtonBuilder, 24 | type Message, 25 | type WebhookMessageCreateOptions, 26 | userMention, 27 | } from 'discord.js'; 28 | import { CompactMessageFormatter } from './formatters/CompactMsgFormatter.js'; 29 | import { EmbedMessageFormatter } from './formatters/EmbedMsgFormatter.js'; 30 | 31 | export interface MessageFormatterStrategy { 32 | format( 33 | message: Message, 34 | connection: RequiredConnectionData, 35 | opts: DefaultFormaterOpts, 36 | ): WebhookMessageCreateOptions; 37 | } 38 | 39 | export type DefaultFormaterOpts = BroadcastOpts & { 40 | username: string; 41 | referredContent: string | undefined; 42 | servername: string; 43 | author: { 44 | username: string; 45 | avatarURL: string; 46 | }; 47 | hub: Hub; 48 | jumpButton?: ActionRowBuilder[]; 49 | badges?: string; 50 | }; 51 | 52 | export default class MessageFormattingService { 53 | private readonly strategy: MessageFormatterStrategy; 54 | private readonly connection: RequiredConnectionData; 55 | 56 | constructor(connection: RequiredConnectionData) { 57 | this.strategy = connection.compact 58 | ? new CompactMessageFormatter() 59 | : new EmbedMessageFormatter(); 60 | this.connection = connection; 61 | } 62 | 63 | format(message: Message, opts: DefaultFormaterOpts): WebhookMessageCreateOptions { 64 | const formatted = this.strategy.format(message, this.connection, opts); 65 | return this.addReplyMention(formatted, this.connection, opts.referredMsgData); 66 | } 67 | private addReplyMention( 68 | messageFormat: WebhookMessageCreateOptions, 69 | connection: RequiredConnectionData, 70 | referredMsgData?: ReferredMsgData, 71 | ): WebhookMessageCreateOptions { 72 | if (referredMsgData && connection.serverId === referredMsgData.dbReferrence?.guildId) { 73 | const { dbReferredAuthor, dbReferrence } = referredMsgData; 74 | const replyMention = dbReferredAuthor?.mentionOnReply ? userMention(dbReferredAuthor.id) : ''; 75 | 76 | messageFormat.content = `${replyMention} ${messageFormat.content ?? ''}`; 77 | messageFormat.allowedMentions = { 78 | ...messageFormat.allowedMentions, 79 | users: [...(messageFormat.allowedMentions?.users ?? []), dbReferrence.authorId], 80 | }; 81 | } 82 | 83 | return messageFormat; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/services/ReputationService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import db from '#src/utils/Db.js'; 19 | import { CallRatingStatus, type User } from '#src/generated/prisma/client/client.js'; 20 | import type { Snowflake } from 'discord.js'; 21 | 22 | export class ReputationService { 23 | async addRating( 24 | userId: Snowflake, 25 | rating: number, 26 | { callId, raterId }: { callId: string; raterId: string }, 27 | ): Promise { 28 | // Update reputation in database 29 | await db.user.upsert({ 30 | where: { id: userId }, 31 | create: { 32 | id: userId, 33 | reputation: rating, 34 | }, 35 | update: { 36 | reputation: { increment: rating }, 37 | }, 38 | }); 39 | 40 | // Log the rating 41 | await db.callRating.create({ 42 | data: { 43 | callId, 44 | raterId, 45 | targetId: userId, 46 | rating: rating > 0 ? CallRatingStatus.LIKE : CallRatingStatus.DISLIKE, 47 | timestamp: new Date(), 48 | }, 49 | }); 50 | } 51 | 52 | async getReputation(userId: Snowflake, userData?: User): Promise { 53 | const user = userData ?? await db.user.findUnique({ 54 | where: { id: userId }, 55 | select: { reputation: true }, 56 | }); 57 | return user?.reputation ?? 0; 58 | } 59 | 60 | async getTopReputation(limit = 10) { 61 | return await db.user.findMany({ 62 | where: { reputation: { gt: 0 } }, 63 | select: { id: true, reputation: true }, 64 | orderBy: { reputation: 'desc' }, 65 | take: limit, 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/services/ShardMetricsService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type InterChatClient from '#src/core/BaseClient.js'; 19 | 20 | export interface ShardMetricsData { 21 | metric: string; 22 | value?: number; 23 | labels: Record; 24 | } 25 | 26 | export interface ShardMetric { 27 | type: string; 28 | data: ShardMetricsData; 29 | } 30 | 31 | export class ShardMetricsService { 32 | private static instance: ShardMetricsService; 33 | private client: InterChatClient; 34 | 35 | private constructor(client: InterChatClient) { 36 | this.client = client; 37 | setInterval(() => { 38 | this.client.cluster.send({ 39 | type: 'METRICS', 40 | data: { 41 | metric: 'cluster_memory_mb', 42 | value: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), 43 | labels: { cluster: this.client.cluster.id.toString() }, 44 | }, 45 | }); 46 | }, 15000); 47 | } 48 | 49 | public static init(client: InterChatClient): ShardMetricsService { 50 | if (!ShardMetricsService.instance) { 51 | ShardMetricsService.instance = new ShardMetricsService(client); 52 | } 53 | return ShardMetricsService.instance; 54 | } 55 | 56 | public incrementCommand(commandName: string): void { 57 | this.client.cluster.send({ 58 | type: 'METRICS', 59 | data: { 60 | metric: 'command', 61 | labels: { command: commandName }, 62 | }, 63 | }); 64 | } 65 | 66 | public incrementMessage(hubName: string): void { 67 | this.client.cluster.send({ 68 | type: 'METRICS', 69 | data: { 70 | metric: 'message', 71 | labels: { 72 | hub: hubName, 73 | cluster: this.client.cluster.id.toString(), 74 | }, 75 | }, 76 | }); 77 | } 78 | 79 | public updateGuildCount(count: number): void { 80 | this.client.cluster.send({ 81 | type: 'METRICS', 82 | data: { 83 | metric: 'guilds', 84 | value: count, 85 | labels: { 86 | cluster: this.client.cluster.id.toString(), 87 | }, 88 | }, 89 | }); 90 | } 91 | 92 | public updateShardStatus(shardId: number, status: boolean): void { 93 | this.client.cluster.send({ 94 | type: 'METRICS', 95 | data: { 96 | metric: 'shard_status', 97 | value: status ? 1 : 0, 98 | labels: { 99 | cluster: this.client.cluster.id.toString(), 100 | shard: shardId.toString(), 101 | }, 102 | }, 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/services/formatters/CompactMsgFormatter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { Connection } from '#src/generated/prisma/client/client.js'; 19 | import { EmbedBuilder, type Message, type WebhookMessageCreateOptions } from 'discord.js'; 20 | import Constants from '#src/utils/Constants.js'; 21 | import type { DefaultFormaterOpts, MessageFormatterStrategy } from '../MessageFormattingService.js'; 22 | 23 | export class CompactMessageFormatter implements MessageFormatterStrategy { 24 | format( 25 | message: Message, 26 | connection: Connection, 27 | opts: DefaultFormaterOpts, 28 | ): WebhookMessageCreateOptions { 29 | const contents = { 30 | normal: message.content, 31 | referred: opts.referredContent, 32 | }; 33 | const { referredAuthor } = opts.referredMsgData; 34 | 35 | 36 | // discord displays either an embed or an attachment url in a compact message (embeds take priority, so image will not display) 37 | // which is why if there is an image, we don't send the reply embed. Reply button remains though 38 | const replyEmbed = 39 | contents.referred && !opts.attachmentURL 40 | ? [ 41 | new EmbedBuilder() 42 | .setDescription(contents.referred) 43 | .setAuthor({ 44 | name: referredAuthor?.username.slice(0, 30) ?? 'Unknown User', 45 | iconURL: referredAuthor?.displayAvatarURL(), 46 | }) 47 | .setColor(Constants.Colors.invisible), 48 | ] 49 | : undefined; 50 | 51 | const { author, servername, jumpButton } = opts; 52 | 53 | // compact mode doesn't need new attachment url for tenor and direct image links 54 | // we can just slap them right in the content without any problems 55 | // [] has an empty char in between its not magic kthxbye 56 | const attachmentURL = 57 | message.attachments.size > 0 || message.stickers.size > 0 58 | ? `\n[⁥](${opts.attachmentURL})` 59 | : ''; 60 | 61 | return { 62 | username: `@${author.username} • ${servername}`, 63 | avatarURL: author.avatarURL, 64 | embeds: replyEmbed, 65 | components: jumpButton, 66 | content: `${opts.badges}${contents.normal} ${attachmentURL}`, 67 | threadId: connection.parentId ? connection.channelId : undefined, 68 | allowedMentions: { parse: [] }, 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/services/formatters/EmbedMsgFormatter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { Connection } from '#src/generated/prisma/client/client.js'; 19 | import type { Message, WebhookMessageCreateOptions } from 'discord.js'; 20 | import { buildNetworkEmbed } from '#src/utils/network/utils.js'; 21 | import type { DefaultFormaterOpts, MessageFormatterStrategy } from '../MessageFormattingService.js'; 22 | 23 | export class EmbedMessageFormatter implements MessageFormatterStrategy { 24 | format( 25 | message: Message, 26 | connection: Connection, 27 | opts: DefaultFormaterOpts, 28 | ): WebhookMessageCreateOptions { 29 | const embed = buildNetworkEmbed(message, opts.username, { 30 | attachmentURL: opts.attachmentURL, 31 | referredContent: opts.referredContent, 32 | embedCol: opts.embedColor, 33 | badges: opts.badges, 34 | }); 35 | 36 | return { 37 | components: opts.jumpButton, 38 | embeds: [embed], 39 | username: `${opts.hub.name}`, 40 | avatarURL: opts.hub.iconUrl, 41 | threadId: connection.parentId ? connection.channelId : undefined, 42 | allowedMentions: { parse: [] }, 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/types/ConnectionTypes.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type ConnectionManager from '#src/managers/ConnectionManager.js'; 19 | import type HubManager from '#src/managers/HubManager.js'; 20 | 21 | export interface ConnectionData { 22 | connection: ConnectionManager; 23 | hubConnections: ConnectionManager[]; 24 | hub: HubManager; 25 | } 26 | -------------------------------------------------------------------------------- /src/types/CustomClientProps.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type BaseCommand from '#src/core/BaseCommand.js'; 19 | import type BasePrefixCommand from '#src/core/BasePrefixCommand.js'; 20 | import type { InteractionFunction } from '#src/decorators/RegisterInteractionHandler.js'; 21 | import type AntiSpamManager from '#src/managers/AntiSpamManager.js'; 22 | import type EventLoader from '#src/modules/Loaders/EventLoader.js'; 23 | import type CooldownService from '#src/services/CooldownService.js'; 24 | import type Scheduler from '#src/services/SchedulerService.js'; 25 | import { ShardMetricsService } from '#src/services/ShardMetricsService.js'; 26 | import type { ClusterClient } from 'discord-hybrid-sharding'; 27 | import type { 28 | Collection, 29 | ForumChannel, 30 | MediaChannel, 31 | NewsChannel, 32 | Snowflake, 33 | TextChannel, 34 | } from 'discord.js'; 35 | 36 | export type RemoveMethods = { 37 | [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? never : RemoveMethods; 38 | }; 39 | 40 | export type ThreadParentChannel = NewsChannel | TextChannel | ForumChannel | MediaChannel; 41 | 42 | declare module 'discord.js' { 43 | export interface Client { 44 | readonly version: string; 45 | readonly development: boolean; 46 | readonly description: string; 47 | readonly commands: Collection; 48 | readonly interactions: Collection; 49 | readonly prefixCommands: Collection; 50 | 51 | readonly eventLoader: EventLoader; 52 | readonly commandCooldowns: CooldownService; 53 | readonly reactionCooldowns: Collection; 54 | readonly cluster: ClusterClient; 55 | readonly antiSpamManager: AntiSpamManager; 56 | 57 | readonly shardMetrics: ShardMetricsService; 58 | 59 | fetchGuild(guildId: Snowflake): Promise | undefined>; 60 | getScheduler(): Scheduler; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/types/TopGGPayload.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { Snowflake } from 'discord.js'; 19 | 20 | export interface WebhookPayload { 21 | /** If webhook is a bot: ID of the bot that received a vote */ 22 | bot?: Snowflake; 23 | /** If webhook is a server: ID of the server that received a vote */ 24 | guild?: Snowflake; 25 | /** ID of the user who voted */ 26 | user: Snowflake; 27 | /** 28 | * The type of the vote (should always be "upvote" except when using the test 29 | * button it's "test") 30 | */ 31 | type: 'upvote' | 'test'; 32 | /** 33 | * Whether the weekend multiplier is in effect, meaning users votes count as 34 | * two 35 | */ 36 | isWeekend?: boolean; 37 | /** Query parameters in vote page in a key to value object */ 38 | query: 39 | | { 40 | [key: string]: string; 41 | } 42 | | string; 43 | } 44 | -------------------------------------------------------------------------------- /src/types/Utils.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | export type ReactionArray = { [key: string]: Snowflake[] }; 19 | export type RemoveMethods = { 20 | [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? never : RemoveMethods; 21 | }; 22 | 23 | export type ThreadParentChannel = NewsChannel | TextChannel | ForumChannel | MediaChannel; 24 | 25 | export type ConvertDatesToString = T extends Date 26 | ? string 27 | : T extends Array 28 | ? Array> 29 | : T extends object 30 | ? { [K in keyof T]: ConvertDatesToString } 31 | : T; 32 | -------------------------------------------------------------------------------- /src/utils/ChannelUtls.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { APIChannel, Channel, GuildTextBasedChannel } from 'discord.js'; 19 | 20 | export const isGuildTextBasedChannel = ( 21 | channel: Channel | APIChannel | null | undefined, 22 | ): channel is GuildTextBasedChannel => 23 | Boolean(channel && 'isTextBased' in channel && channel.isTextBased() && !channel.isDMBased()); 24 | -------------------------------------------------------------------------------- /src/utils/ComponentUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 19 | import Constants from '#utils/Constants.js'; 20 | import { 21 | type ActionRow, 22 | ActionRowBuilder, 23 | ButtonBuilder, 24 | ButtonStyle, 25 | type Client, 26 | ComponentType, 27 | type MessageActionRowComponent, 28 | type Snowflake, 29 | messageLink, 30 | } from 'discord.js'; 31 | 32 | export const greyOutButton = (row: ActionRowBuilder, disableElement: number) => { 33 | row.components.forEach((c) => c.setDisabled(false)); 34 | row.components[disableElement].setDisabled(true); 35 | }; 36 | export const greyOutButtons = (rows: ActionRowBuilder[]) => { 37 | for (const row of rows) { 38 | row.components.forEach((c) => c.setDisabled(true)); 39 | } 40 | }; 41 | 42 | export const generateJumpButton = ( 43 | client: Client, 44 | referredAuthorUsername: string, 45 | opts: { messageId: Snowflake; channelId: Snowflake; serverId: Snowflake }, 46 | ) => 47 | // create a jump to reply button 48 | new ActionRowBuilder().addComponents( 49 | new ButtonBuilder() 50 | .setStyle(ButtonStyle.Link) 51 | .setEmoji(getEmoji('reply', client)) 52 | .setURL(messageLink(opts.channelId, opts.messageId, opts.serverId)) 53 | .setLabel( 54 | referredAuthorUsername.length >= 80 55 | ? `@${referredAuthorUsername.slice(0, 76)}...` 56 | : `@${referredAuthorUsername}`, 57 | ), 58 | ); 59 | 60 | export const disableAllComponents = ( 61 | components: ActionRow[], 62 | disableLinks = false, 63 | ) => 64 | components.map((row) => { 65 | const jsonRow = row.toJSON(); 66 | for (const component of jsonRow.components) { 67 | if ( 68 | !disableLinks && 69 | component.type === ComponentType.Button && 70 | component.style === ButtonStyle.Link // leave link buttons enabled 71 | ) { 72 | component.disabled = false; 73 | } 74 | else { 75 | component.disabled = true; 76 | } 77 | } 78 | 79 | return jsonRow; 80 | }); 81 | 82 | export const donateButton = new ButtonBuilder() 83 | .setLabel('Donate') 84 | .setURL(`${Constants.Links.Website}/donate`) 85 | .setEmoji('💗') 86 | .setStyle(ButtonStyle.Link); 87 | -------------------------------------------------------------------------------- /src/utils/ContextUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type BaseCommand from '#src/core/BaseCommand.js'; 19 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 20 | import InteractionContext from '#src/core/CommandContext/InteractionContext.js'; 21 | import PrefixContext from '#src/core/CommandContext/PrefixContext.js'; 22 | import type { 23 | ChatInputCommandInteraction, 24 | ContextMenuCommandInteraction, 25 | Message, 26 | MessageComponentInteraction, 27 | ModalSubmitInteraction, 28 | } from 'discord.js'; 29 | 30 | /** 31 | * Create a context object from a message 32 | * @param message The message to create a context from 33 | * @param command The command associated with this context 34 | * @param args The command arguments 35 | * @returns A PrefixContext instance 36 | */ 37 | export function createPrefixContext( 38 | message: Message, 39 | command: BaseCommand, 40 | args: string[], 41 | ): PrefixContext { 42 | return new PrefixContext(message, command, args); 43 | } 44 | 45 | /** 46 | * Create a context object from a slash command or context menu interaction 47 | * @param interaction The interaction to create a context from 48 | * @param command The command associated with this context 49 | * @returns An InteractionContext instance 50 | */ 51 | export function createInteractionContext( 52 | interaction: ChatInputCommandInteraction | ContextMenuCommandInteraction, 53 | command: BaseCommand, 54 | ): InteractionContext { 55 | return new InteractionContext(interaction, command); 56 | } 57 | 58 | /** 59 | * Create a context object from a component interaction 60 | * @param interaction The interaction to create a context from 61 | * @param command The command associated with this context (optional) 62 | * @returns A ComponentContext instance 63 | */ 64 | export function createComponentContext( 65 | interaction: MessageComponentInteraction | ModalSubmitInteraction, 66 | command?: BaseCommand, 67 | ): ComponentContext { 68 | return new ComponentContext(interaction, command); 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/Db.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { PrismaClient } from '#src/generated/prisma/client/client.js'; 19 | 20 | const db = new PrismaClient({ log: [{ emit: 'event', level: 'query' }] }); 21 | 22 | export default db; 23 | -------------------------------------------------------------------------------- /src/utils/EmbedUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { stripIndents } from 'common-tags'; 19 | import { 20 | type APIEmbed, 21 | type Client, 22 | Colors, 23 | EmbedBuilder, 24 | type EmbedData, 25 | codeBlock, 26 | resolveColor, 27 | } from 'discord.js'; 28 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 29 | import Constants from '#utils/Constants.js'; 30 | 31 | export class InfoEmbed extends EmbedBuilder { 32 | constructor(data?: EmbedData | APIEmbed) { 33 | super({ 34 | color: resolveColor(Constants.Colors.primary), 35 | ...data, 36 | }); 37 | } 38 | 39 | removeTitle(): this { 40 | super.setTitle(null); 41 | return this; 42 | } 43 | 44 | setTitle(title?: string | null): this { 45 | if (title) super.setTitle(title); 46 | return this; 47 | } 48 | } 49 | 50 | export class ErrorEmbed extends EmbedBuilder { 51 | private errorCode: string | null = null; 52 | constructor(client: Client, data?: { errorCode?: string }) { 53 | super({ 54 | title: `${getEmoji('x_icon', client)} Unexpected Error Occurred`, 55 | color: Colors.Red, 56 | }); 57 | 58 | if (data?.errorCode) this.setErrorCode(data.errorCode); 59 | } 60 | 61 | setErrorCode(errorCode: string | null): this { 62 | this.errorCode = errorCode; 63 | 64 | if (!errorCode) return this; 65 | 66 | return super.setDescription(stripIndents` 67 | ${this.data.description ?? ''} 68 | 69 | Please join our [support server](https://discord.gg/interchat) and report the following error code: 70 | ${codeBlock(errorCode)} 71 | `); 72 | } 73 | 74 | setDescription(description: string): this { 75 | super.setDescription(description); 76 | if (this.errorCode) this.setErrorCode(this.errorCode); 77 | 78 | return this; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/utils/EmojiUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { Client } from 'discord.js'; 19 | import emojis from './JSON/emojis.json' with { type: 'json' }; 20 | 21 | export type EmojiKeys = keyof typeof emojis; 22 | 23 | export const getEmoji = (name: EmojiKeys, client: Client): string => { 24 | const emojiId = client.application?.emojis.cache.findKey((emoji) => emoji.name === name); 25 | if (!emojiId) return ''; 26 | return `<:${name}:${emojiId}>`; 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/ImageUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import Constants from '#utils/Constants.js'; 19 | import Logger from '#utils/Logger.js'; 20 | import { CanvasRenderingContext2D } from 'canvas'; 21 | 22 | /** 23 | * Returns the URL of an attachment in a message, if it exists. 24 | * @param message The message to search for an attachment URL. 25 | * @returns The URL of the attachment, or null if no attachment is found. 26 | */ 27 | export const getAttachmentURL = async (string: string) => { 28 | // Image URLs 29 | const URLMatch = string.match(Constants.Regex.StaticImageUrl); 30 | if (URLMatch) return URLMatch[0]; 31 | 32 | // Tenor Gifs 33 | const gifMatch = string.match(Constants.Regex.TenorLinks); 34 | if (!gifMatch) return null; 35 | 36 | try { 37 | if (!process.env.TENOR_KEY) throw new TypeError('Tenor API key not found in .env file.'); 38 | const id = gifMatch[0].split('-').at(-1); 39 | const url = `https://g.tenor.com/v1/gifs?ids=${id}&key=${process.env.TENOR_KEY}`; 40 | const gifJSON = (await (await fetch(url)).json()) as { 41 | results: { media: { gif: { url: string } }[] }[]; 42 | }; 43 | 44 | return gifJSON.results.at(0)?.media.at(0)?.gif.url as string | null; 45 | } 46 | catch (e) { 47 | Logger.error(e); 48 | return null; 49 | } 50 | }; 51 | 52 | export const stripTenorLinks = (content: string, imgUrl: string) => 53 | content.replace(Constants.Regex.TenorLinks, '').replace(imgUrl, ''); 54 | 55 | export const drawRankProgressBar = ( 56 | ctx: CanvasRenderingContext2D, 57 | x: number, 58 | y: number, 59 | width: number, 60 | height: number, 61 | progress: number, 62 | backgroundColor = '#484b4e', 63 | progressColor = Constants.Colors.primary, 64 | ): void => { 65 | // Draw background 66 | ctx.fillStyle = backgroundColor; 67 | ctx.beginPath(); 68 | ctx.roundRect(x, y, width, height, height / 2); 69 | ctx.fill(); 70 | 71 | // Draw progress 72 | const progressWidth = (width - 4) * Math.min(Math.max(progress, 0), 1); 73 | if (progressWidth > 0) { 74 | ctx.fillStyle = progressColor; 75 | ctx.beginPath(); 76 | ctx.roundRect(x + 2, y + 2, progressWidth, height - 4, (height - 4) / 2); 77 | ctx.fill(); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /src/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { createLogger, format, transports } from 'winston'; 19 | import 'source-map-support/register.js'; 20 | 21 | const custom = format.printf( 22 | (info) => 23 | `\x1b[2;37m${info.timestamp}\x1b[0m ${info.level}: ${info.message} ${info.stack ? `\n${info.stack}` : ''}`, 24 | ); 25 | 26 | const combinedFormat = format.combine( 27 | format.errors({ stack: true }), 28 | format.splat(), 29 | format.timestamp({ format: 'DD/MM/YY-HH:mm:ss' }), 30 | format((info) => { 31 | info.level = info.level.toUpperCase(); 32 | return info; 33 | })(), 34 | custom, 35 | ); 36 | 37 | const loggerConig = { 38 | format: combinedFormat, 39 | transports: [ 40 | new transports.Console({ 41 | format: format.combine(format.colorize(), custom), 42 | level: process.env.DEBUG === 'true' ? 'debug' : 'info', 43 | }), 44 | new transports.File({ filename: 'logs/error.log', level: 'error' }), 45 | new transports.File({ 46 | filename: 'logs/info.log', 47 | format: format.combine( 48 | format((info) => (info.level === 'INFO' ? info : false))(), 49 | combinedFormat, 50 | ), 51 | }), 52 | ], 53 | }; 54 | 55 | if (process.env.DEBUG === 'true') { 56 | loggerConig.transports.push( 57 | new transports.File({ 58 | filename: 'logs/debug.log', 59 | level: 'debug', 60 | format: format.combine( 61 | format((info) => (info.level === 'DEBUG' ? info : false))(), 62 | combinedFormat, 63 | ), 64 | }), 65 | ); 66 | } 67 | 68 | export default createLogger(loggerConig); 69 | -------------------------------------------------------------------------------- /src/utils/ProfileUtils.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, User, Client, time } from 'discord.js'; 2 | import { getBadges, getVoterBadge, formatBadges } from '#utils/BadgeUtils.js'; 3 | import UserDbService from '#src/services/UserDbService.js'; 4 | import { ReputationService } from '#src/services/ReputationService.js'; 5 | import { getUserLeaderboardRank } from '#src/utils/Leaderboard.js'; 6 | import { fetchUserData } from '#src/utils/Utils.js'; 7 | import Constants from '#utils/Constants.js'; 8 | import db from '#utils/Db.js'; 9 | 10 | export async function buildProfileEmbed(user: User, client: Client) { 11 | const userData = await fetchUserData(user.id); 12 | if (!userData) return null; 13 | 14 | const badges = getBadges(user.id, client); 15 | const hasVoted = await new UserDbService().userVotedToday(user.id, userData); 16 | if (hasVoted) badges.push(getVoterBadge(client)); 17 | 18 | const reputationService = new ReputationService(); 19 | const reputation = await reputationService.getReputation(user.id, userData); 20 | 21 | return new EmbedBuilder() 22 | .setDescription(`### @${user.username} ${formatBadges(badges)}`) 23 | .addFields([ 24 | { 25 | name: 'Badges', 26 | value: badges.map((b) => `${b.emoji} ${b.name} - ${b.description}`).join('\n') || 'No badges', 27 | inline: false, 28 | }, 29 | { 30 | name: 'Reputation', 31 | value: `${reputation >= 0 ? '👍' : '👎'} ${reputation}`, 32 | inline: true, 33 | }, 34 | { 35 | name: 'Leaderboard Rank', 36 | value: `#${(await getUserLeaderboardRank(user.id)) ?? 'Unranked'}`, 37 | inline: true, 38 | }, 39 | { 40 | name: 'Total Messages', 41 | value: `${userData.messageCount}`, 42 | inline: true, 43 | }, 44 | { 45 | name: 'User Since', 46 | value: `${time(Math.round(userData.createdAt.getTime() / 1000), 'D')}`, 47 | inline: true, 48 | }, 49 | { 50 | name: 'Hubs Owned', 51 | value: `${(await db.hub.findMany({ where: { ownerId: user.id, private: false } })).map((h) => h.name).join(', ')}`, 52 | inline: true, 53 | }, 54 | { 55 | name: 'User ID', 56 | value: user.id, 57 | inline: true, 58 | }, 59 | ]) 60 | .setColor(Constants.Colors.invisible) 61 | .setThumbnail(user.displayAvatarURL()); 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/Redis.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { Redis } from 'ioredis'; 19 | 20 | // when run usin scripts like registerCmds 21 | let redisClient: Redis; 22 | 23 | export const getRedis = () => { 24 | if (!redisClient) redisClient = new Redis(process.env.REDIS_URI as string); 25 | return redisClient; 26 | }; 27 | 28 | export default getRedis; 29 | -------------------------------------------------------------------------------- /src/utils/calculateLevel.ts: -------------------------------------------------------------------------------- 1 | export const calculateRequiredXP = (level: number): number => 50 * level ** 2 + 25 * level; // arcane formula 2 | -------------------------------------------------------------------------------- /src/utils/hub/logger/ContentFilter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 19 | import Logger from '#src/utils/Logger.js'; 20 | import Constants from '#utils/Constants.js'; 21 | import { stripIndents } from 'common-tags'; 22 | import { EmbedBuilder, type Message } from 'discord.js'; 23 | import { sendLog } from './Default.js'; 24 | 25 | /** 26 | * Log a blocked message to the appropriate log channel 27 | * @param message The message that was blocked 28 | * @param category The category of prohibited content 29 | * @param client The Discord client 30 | */ 31 | export const logBlockedMessage = async (message: Message): Promise => { 32 | try { 33 | const client = message.client; 34 | // Create an embed for the log 35 | const embed = new EmbedBuilder() 36 | .setTitle(`${getEmoji('exclamation', client)} Content Filter: Message Blocked`) 37 | .setColor(Constants.Colors.error) 38 | .setDescription( 39 | stripIndents` 40 | A message was blocked by the content filter during a call. 41 | 42 | ${getEmoji('dot', client)} **User:** ${message.author.username} (${message.author.id}) 43 | ${getEmoji('dot', client)} **Server:** ${message.guild?.name || 'Unknown'} (${message.guildId}) 44 | ${getEmoji('dot', client)} **Channel:** <#${message.channelId}> 45 | ${getEmoji('dot', client)} **Time:** 46 | `, 47 | ) 48 | .setFooter({ 49 | text: `Message ID: ${message.id}`, 50 | iconURL: message.author.displayAvatarURL(), 51 | }) 52 | .setTimestamp(); 53 | 54 | // Send to system log channel if configured 55 | // Note: In a real implementation, you might want to send this to a specific moderation channel 56 | const systemLogChannelId = process.env.SYSTEM_LOG_CHANNEL; 57 | if (systemLogChannelId) { 58 | await sendLog(client.cluster, systemLogChannelId, embed); 59 | } 60 | } 61 | catch (error) { 62 | Logger.error('Failed to log blocked message:', error); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/utils/hub/logger/JoinLeave.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { stripIndents } from 'common-tags'; 19 | import { EmbedBuilder, type Guild } from 'discord.js'; 20 | import HubLogManager from '#src/managers/HubLogManager.js'; 21 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 22 | import { getHubConnections } from '#utils/ConnectedListUtils.js'; 23 | import Constants from '#utils/Constants.js'; 24 | import { sendLog } from './Default.js'; 25 | 26 | export const logJoinToHub = async ( 27 | hubId: string, 28 | server: Guild, 29 | opt?: { totalConnections: number; hubName: string }, 30 | ) => { 31 | const logManager = await HubLogManager.create(hubId); 32 | if (!logManager.config.joinLeavesChannelId) return; 33 | 34 | const dotBlueEmoji = getEmoji('dot', server.client); 35 | const owner = await server.fetchOwner(); 36 | const embed = new EmbedBuilder() 37 | .setTitle('New Server Joined') 38 | .setDescription( 39 | stripIndents` 40 | ${dotBlueEmoji} **Server:** ${server.name} (${server.id}) 41 | ${dotBlueEmoji} **Owner:** ${owner.user.tag} (${server.ownerId}) 42 | ${dotBlueEmoji} **Member Count:** ${server.memberCount} 43 | `, 44 | ) 45 | .setColor(Constants.Colors.primary) 46 | .setThumbnail(server.iconURL()) 47 | .setFooter({ 48 | text: `We have ${opt?.totalConnections} server(s) connected to ${opt?.hubName} now!`, 49 | }); 50 | 51 | await sendLog(server.client.cluster, logManager.config.joinLeavesChannelId, embed); 52 | }; 53 | 54 | export const logGuildLeaveToHub = async (hubId: string, server: Guild) => { 55 | const logManager = await HubLogManager.create(hubId); 56 | if (!logManager.config.joinLeavesChannelId) return; 57 | 58 | const owner = await server.client.users.fetch(server.ownerId).catch(() => null); 59 | const totalConnections = (await getHubConnections(hubId))?.reduce( 60 | (total, c) => total + (c.connected ? 1 : 0), 61 | 0, 62 | ); 63 | 64 | const dotRedEmoji = getEmoji('dotRed', server.client); 65 | 66 | const embed = new EmbedBuilder() 67 | .setTitle('Server Left') 68 | .setDescription( 69 | stripIndents` 70 | ${dotRedEmoji} **Server:** ${server.name} (${server.id}) 71 | ${dotRedEmoji} **Owner:** ${owner?.username} (${server.ownerId}) 72 | ${dotRedEmoji} **Member Count:** ${server.memberCount} 73 | `, 74 | ) 75 | .setColor('Red') 76 | .setThumbnail(server.iconURL()) 77 | .setFooter({ 78 | text: `We now have ${totalConnections} server(s) connected to the hub now!`, 79 | }); 80 | 81 | await sendLog(server.client.cluster, logManager.config.joinLeavesChannelId, embed); 82 | }; 83 | -------------------------------------------------------------------------------- /src/utils/hub/logger/Warns.ts: -------------------------------------------------------------------------------- 1 | import HubLogManager from '#src/managers/HubLogManager.js'; 2 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 3 | import { t } from '#src/utils/Locale.js'; 4 | import { EmbedBuilder, type User } from 'discord.js'; 5 | import { sendLog } from './Default.js'; 6 | 7 | export const logWarn = async ( 8 | hubId: string, 9 | opts: { 10 | warnedUser: User; 11 | moderator: User; 12 | reason: string; 13 | }, 14 | ) => { 15 | const logManager = await HubLogManager.create(hubId); 16 | if (!logManager.config.modLogsChannelId) return; 17 | 18 | const { warnedUser, moderator, reason } = opts; 19 | const arrow = getEmoji('dot', moderator.client); 20 | 21 | const embed = new EmbedBuilder() 22 | .setTitle( 23 | t('warn.log.title', 'en', { 24 | emoji: getEmoji('exclamation', moderator.client), 25 | }), 26 | ) 27 | .setColor('Yellow') 28 | .setDescription( 29 | t('warn.log.description', 'en', { 30 | arrow, 31 | user: warnedUser.username, 32 | userId: warnedUser.id, 33 | moderator: moderator.username, 34 | modId: moderator.id, 35 | reason, 36 | }), 37 | ) 38 | .setTimestamp() 39 | .setFooter({ 40 | text: t('warn.log.footer', 'en', { 41 | moderator: moderator.username, 42 | }), 43 | iconURL: moderator.displayAvatarURL(), 44 | }); 45 | 46 | await sendLog(moderator.client.cluster, logManager.config.modLogsChannelId, embed); 47 | }; 48 | -------------------------------------------------------------------------------- /src/utils/hub/settings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { ActionRowBuilder, type Client, type Snowflake, StringSelectMenuBuilder } from 'discord.js'; 19 | import type { SerializedHubSettings } from '#src/modules/BitFields.js'; 20 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 21 | import { CustomID } from '#utils/CustomID.js'; 22 | 23 | export const buildSettingsMenu = ( 24 | rawSettings: SerializedHubSettings, 25 | hubId: string, 26 | userId: Snowflake, 27 | client: Client, 28 | ) => 29 | new ActionRowBuilder().addComponents( 30 | new StringSelectMenuBuilder() 31 | .setCustomId( 32 | new CustomID() 33 | .setIdentifier('hubEdit', 'settingsSelect') 34 | .setArgs(userId) 35 | .setArgs(hubId) 36 | .toString(), 37 | ) 38 | .setPlaceholder('Select an option') 39 | .addOptions( 40 | Object.entries(rawSettings).map(([setting, isEnabled]) => { 41 | const emoji = isEnabled ? getEmoji('x_icon', client) : getEmoji('tick_icon', client); 42 | return { 43 | label: `${isEnabled ? 'Disable' : 'Enable'} ${setting}`, 44 | value: setting, 45 | emoji, 46 | }; 47 | }), 48 | ), 49 | ); 50 | -------------------------------------------------------------------------------- /src/utils/moderation/deleteMessage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import { type Snowflake, WebhookClient } from 'discord.js'; 19 | import { 20 | deleteMessageCache, 21 | getBroadcasts, 22 | } from '#src/utils/network/messageUtils.js'; 23 | import { getHubConnections } from '#utils/ConnectedListUtils.js'; 24 | import { RedisKeys } from '#utils/Constants.js'; 25 | import getRedis from '#utils/Redis.js'; 26 | import { Broadcast } from '#src/generated/prisma/client/index.js'; 27 | 28 | export const setDeleteLock = async (messageId: string) => { 29 | const redis = getRedis(); 30 | const key = `${RedisKeys.msgDeleteInProgress}:${messageId}` as const; 31 | const alreadyLocked = await redis.get(key); 32 | if (alreadyLocked !== 't') await redis.set(key, 't', 'EX', 900); // 15 mins 33 | }; 34 | 35 | export const deleteMessageFromHub = async ( 36 | hubId: string, 37 | originalMsgId: string, 38 | dbMessagesToDelete?: Broadcast[], 39 | ) => { 40 | let msgsToDelete = dbMessagesToDelete; 41 | if (!dbMessagesToDelete) { 42 | msgsToDelete = await getBroadcasts(originalMsgId); 43 | } 44 | 45 | if (!msgsToDelete?.length) return { deletedCount: 0, totalCount: 0 }; 46 | 47 | await setDeleteLock(originalMsgId); 48 | 49 | let deletedCount = 0; 50 | const hubConnections = await getHubConnections(hubId); 51 | const hubConnectionsMap = new Map(hubConnections?.map((c) => [c.channelId, c])); 52 | 53 | for await (const dbMsg of msgsToDelete) { 54 | const connection = hubConnectionsMap.get(dbMsg.channelId); 55 | if (!connection) continue; 56 | 57 | const webhook = new WebhookClient({ url: connection.webhookURL }); 58 | const threadId = connection.parentId ? connection.channelId : undefined; 59 | await webhook.deleteMessage(dbMsg.id, threadId).catch(() => null); 60 | deletedCount++; 61 | } 62 | 63 | await getRedis().del(`${RedisKeys.msgDeleteInProgress}:${originalMsgId}`); 64 | deleteMessageCache(originalMsgId); 65 | return { deletedCount, totalCount: msgsToDelete.length }; 66 | }; 67 | 68 | export const isDeleteInProgress = async (originalMsgId: Snowflake) => { 69 | const res = await getRedis().get(`${RedisKeys.msgDeleteInProgress}:${originalMsgId}`); 70 | return res === 't'; 71 | }; 72 | -------------------------------------------------------------------------------- /src/utils/moderation/modPanel/handlers/RemoveReactionsHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 19 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 20 | import { type ModAction, replyWithUnknownMessage } from '#src/utils/moderation/modPanel/utils.js'; 21 | import { getOriginalMessage } from '#src/utils/network/messageUtils.js'; 22 | import { fetchUserLocale } from '#src/utils/Utils.js'; 23 | import type { ReactionArray } from '#types/Utils.d.ts'; 24 | import { updateReactions } from '#utils/reaction/reactions.js'; 25 | import sortReactions from '#utils/reaction/sortReactions.js'; 26 | import type { Snowflake } from 'discord.js'; 27 | 28 | export default class RemoveReactionsHandler implements ModAction { 29 | async handle(ctx: ComponentContext, originalMsgId: Snowflake) { 30 | await ctx.deferReply({ flags: ['Ephemeral'] }); 31 | 32 | const originalMsg = await getOriginalMessage(originalMsgId); 33 | if (!originalMsg) { 34 | await replyWithUnknownMessage(ctx, { 35 | locale: await fetchUserLocale(ctx.user.id), 36 | }); 37 | return; 38 | } 39 | 40 | let reactions: ReactionArray; 41 | 42 | try { 43 | reactions = originalMsg.reactions ? originalMsg.reactions as { [key: string]: string[] } : {}; 44 | } 45 | catch { 46 | // Fallback to empty object if parsing fails 47 | reactions = {}; 48 | } 49 | 50 | if (!sortReactions(reactions).length) { 51 | await ctx.reply({ 52 | content: `${getEmoji('slash', ctx.client)} No reactions to remove.`, 53 | flags: ['Ephemeral'], 54 | }); 55 | return; 56 | } 57 | 58 | await updateReactions(originalMsg, {}); 59 | 60 | await ctx.reply({ 61 | content: `${getEmoji('tick_icon', ctx.client)} Reactions removed.`, 62 | flags: ['Ephemeral'], 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/moderation/modPanel/handlers/serverBanHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 19 | import { type ModAction, replyWithUnknownMessage } from '#src/utils/moderation/modPanel/utils.js'; 20 | import { getOriginalMessage } from '#src/utils/network/messageUtils.js'; 21 | import { checkIfStaff } from '#src/utils/Utils.js'; 22 | 23 | import type { supportedLocaleCodes } from '#utils/Locale.js'; 24 | import type { Snowflake } from 'discord.js'; 25 | 26 | export default class ServerBanHandler implements ModAction { 27 | async handle( 28 | ctx: ComponentContext, 29 | originalMsgId: Snowflake, 30 | locale: supportedLocaleCodes, 31 | ) { 32 | const originalMsg = await getOriginalMessage(originalMsgId); 33 | 34 | if (!originalMsg) { 35 | await replyWithUnknownMessage(ctx, { locale }); 36 | return; 37 | } 38 | 39 | if (!checkIfStaff(ctx.user.id)) { 40 | await ctx.reply({ 41 | content: 'You do not have permission to ban servers.', 42 | flags: ['Ephemeral'], 43 | }); 44 | return; 45 | } 46 | 47 | // Import and use the ban flow handler directly 48 | const { default: ModPanelBanFlowHandler } = await import('#src/interactions/ModPanelBanFlow.js'); 49 | const banFlowHandler = new ModPanelBanFlowHandler(); 50 | 51 | // Call the ban type selection directly 52 | await banFlowHandler.showBanTypeSelection(ctx, originalMsg.guildId, originalMsgId, 'server'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/moderation/modPanel/handlers/userBanHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 19 | import { type ModAction, replyWithUnknownMessage } from '#src/utils/moderation/modPanel/utils.js'; 20 | import { getOriginalMessage } from '#src/utils/network/messageUtils.js'; 21 | import { checkIfStaff } from '#src/utils/Utils.js'; 22 | import type { supportedLocaleCodes } from '#utils/Locale.js'; 23 | import type { Snowflake } from 'discord.js'; 24 | 25 | export default class UserBanHandler implements ModAction { 26 | async handle( 27 | ctx: ComponentContext, 28 | originalMsgId: Snowflake, 29 | locale: supportedLocaleCodes, 30 | ) { 31 | const originalMsg = await getOriginalMessage(originalMsgId); 32 | 33 | if (!originalMsg) { 34 | await replyWithUnknownMessage(ctx, { locale }); 35 | return; 36 | } 37 | 38 | if (originalMsg.authorId === ctx.user.id) { 39 | await ctx.reply({ 40 | content: 'Let\'s not go there. <:bruhcat:1256859727158050838>', 41 | flags: ['Ephemeral'], 42 | }); 43 | return; 44 | } 45 | 46 | if (!checkIfStaff(ctx.user.id)) { 47 | await ctx.reply({ 48 | content: 'You do not have permission to ban users.', 49 | flags: ['Ephemeral'], 50 | }); 51 | return; 52 | } 53 | 54 | // Import and use the ban flow handler directly 55 | const { default: ModPanelBanFlowHandler } = await import('#src/interactions/ModPanelBanFlow.js'); 56 | const banFlowHandler = new ModPanelBanFlowHandler(); 57 | 58 | // Call the ban type selection directly 59 | await banFlowHandler.showBanTypeSelection(ctx, originalMsg.authorId, originalMsgId, 'user'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/moderation/modPanel/handlers/viewInfractions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 19 | import InfractionManager from '#src/managers/InfractionManager.js'; 20 | import { Pagination } from '#src/modules/Pagination.js'; 21 | import { type ModAction, replyWithUnknownMessage } from '#src/utils/moderation/modPanel/utils.js'; 22 | import { getOriginalMessage } from '#src/utils/network/messageUtils.js'; 23 | import type { supportedLocaleCodes } from '#utils/Locale.js'; 24 | import { buildInfractionListEmbeds } from '#utils/moderation/infractionUtils.js'; 25 | import type { Snowflake } from 'discord.js'; 26 | 27 | export default class ViewInfractionsHandler implements ModAction { 28 | async handle( 29 | ctx: ComponentContext, 30 | originalMsgId: Snowflake, 31 | locale: supportedLocaleCodes, 32 | ) { 33 | await ctx.deferReply({ flags: ['Ephemeral'] }); 34 | 35 | const originalMsg = await getOriginalMessage(originalMsgId); 36 | 37 | if (!originalMsg) { 38 | await replyWithUnknownMessage(ctx, { locale }); 39 | return; 40 | } 41 | 42 | const user = await ctx.client.users.fetch(originalMsg.authorId).catch(() => null); 43 | if (!user) { 44 | await replyWithUnknownMessage(ctx, { locale }); 45 | return; 46 | } 47 | 48 | const infractionManager = new InfractionManager('user', originalMsg.authorId); 49 | const infractions = await infractionManager.getHubInfractions(originalMsg.hubId); 50 | const targetName = user.username ?? 'Unknown User.'; 51 | const iconURL = user.displayAvatarURL(); 52 | 53 | const embeds = await buildInfractionListEmbeds( 54 | ctx.client, 55 | targetName, 56 | infractions, 57 | 'user', 58 | iconURL, 59 | ); 60 | 61 | new Pagination(ctx.client).addPages(embeds).run(ctx.interaction, { deleteOnEnd: true }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/moderation/modPanel/handlers/warnHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 19 | import { HubService } from '#src/services/HubService.js'; 20 | import { InfoEmbed } from '#src/utils/EmbedUtils.js'; 21 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 22 | import { t } from '#src/utils/Locale.js'; 23 | import type { ModAction } from '#src/utils/moderation/modPanel/utils.js'; 24 | import { getOriginalMessage } from '#src/utils/network/messageUtils.js'; 25 | import { CustomID } from '#utils/CustomID.js'; 26 | import type { supportedLocaleCodes } from '#utils/Locale.js'; 27 | import { 28 | ActionRowBuilder, 29 | ModalBuilder, 30 | TextInputBuilder, 31 | TextInputStyle, 32 | } from 'discord.js'; 33 | 34 | export default class WarnHandler implements ModAction { 35 | private readonly hubService = new HubService(); 36 | async handle( 37 | ctx: ComponentContext, 38 | originalMsgId: string, 39 | locale: supportedLocaleCodes, 40 | ): Promise { 41 | const originalMsg = await getOriginalMessage(originalMsgId); 42 | 43 | if (!originalMsg) { 44 | const errorEmbed = new InfoEmbed().setDescription( 45 | t('errors.messageNotSentOrExpired', locale, { 46 | emoji: getEmoji('x_icon', ctx.client), 47 | }), 48 | ); 49 | await ctx.reply({ embeds: [errorEmbed], flags: ['Ephemeral'] }); 50 | return; 51 | } 52 | 53 | const hub = await this.hubService.fetchHub(originalMsg.hubId); 54 | if (!hub) return; 55 | 56 | const modal = new ModalBuilder() 57 | .setTitle(t('warn.modal.title', locale)) 58 | .setCustomId( 59 | new CustomID() 60 | .setIdentifier('warnModal') 61 | .setArgs(originalMsg.authorId, originalMsg.hubId) 62 | .toString(), 63 | ) 64 | .addComponents( 65 | new ActionRowBuilder().addComponents( 66 | new TextInputBuilder() 67 | .setCustomId('reason') 68 | .setLabel(t('warn.modal.reason.label', locale)) 69 | .setPlaceholder(t('warn.modal.reason.placeholder', locale)) 70 | .setStyle(TextInputStyle.Paragraph) 71 | .setMaxLength(500), 72 | ), 73 | ); 74 | 75 | await ctx.showModal(modal); 76 | // modal will be handled by WarnModalHandler in interactions/WarnModal.ts 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/moderation/modPanel/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import ComponentContext from '#src/core/CommandContext/ComponentContext.js'; 19 | import Context from '#src/core/CommandContext/Context.js'; 20 | import { getEmoji } from '#src/utils/EmojiUtils.js'; 21 | import type { Message as MessageDB } from '#src/generated/prisma/client/client.js'; 22 | import { type supportedLocaleCodes } from '#utils/Locale.js'; 23 | import type { Snowflake } from 'discord.js'; 24 | 25 | export interface ModAction { 26 | handle( 27 | ctx: ComponentContext, 28 | originalMsgId: Snowflake, 29 | locale: supportedLocaleCodes, 30 | ): Promise; 31 | handleModal?( 32 | ctx: ComponentContext, 33 | originalMsg: MessageDB, 34 | locale: supportedLocaleCodes, 35 | ): Promise; 36 | } 37 | 38 | interface ReplyWithUnknownMessageOpts { 39 | locale?: supportedLocaleCodes; 40 | edit?: boolean; 41 | } 42 | 43 | export async function replyWithUnknownMessage( 44 | ctx: Context, 45 | opts: ReplyWithUnknownMessageOpts = {}, 46 | ) { 47 | const { edit = false } = opts; 48 | 49 | const emoji = getEmoji('x_icon', ctx.client); 50 | await ctx.replyEmbed('errors.unknownNetworkMessage', { 51 | t: { emoji }, 52 | flags: ['Ephemeral'], 53 | edit, 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/moderation/warnUtils.ts: -------------------------------------------------------------------------------- 1 | import InfractionManager from '#src/managers/InfractionManager.js'; 2 | import { logWarn } from '#src/utils/hub/logger/Warns.js'; 3 | import { type Client } from 'discord.js'; 4 | 5 | interface WarnUserOptions { 6 | userId: string; 7 | hubId: string; 8 | reason: string; 9 | moderatorId: string; 10 | client: Client; 11 | } 12 | 13 | export async function warnUser({ userId, hubId, reason, moderatorId, client }: WarnUserOptions) { 14 | const infractionManager = new InfractionManager('user', userId); 15 | 16 | // Create warning infraction 17 | await infractionManager.addInfraction('WARNING', { 18 | hubId, 19 | reason, 20 | moderatorId, 21 | expiresAt: null, 22 | }); 23 | 24 | // Log the warning 25 | const [moderator, warnedUser] = await Promise.all([ 26 | client.users.fetch(moderatorId), 27 | client.users.fetch(userId), 28 | ]); 29 | 30 | await logWarn(hubId, { 31 | warnedUser, 32 | moderator, 33 | reason, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/network/Types.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import type { UserData } from '#src/generated/prisma/client/client.js'; 19 | import type { Collection, HexColorString, Message, User } from 'discord.js'; 20 | import type { Broadcast, OriginalMessage } from '#src/utils/network/messageUtils.js'; 21 | 22 | export interface ReferredMsgData { 23 | dbReferrence: (OriginalMessage & { broadcastMsgs: Collection }) | null; 24 | referredAuthor: User | null; 25 | dbReferredAuthor: UserData | null; 26 | referredMessage?: Message; 27 | } 28 | 29 | export interface BroadcastOpts { 30 | referredMsgData: ReferredMsgData; 31 | embedColor?: HexColorString; 32 | attachmentURL?: string | null; 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/reaction/helpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | import BlacklistManager from '#src/managers/BlacklistManager.js'; 19 | 20 | import { isBlacklisted } from '#utils/moderation/blacklistUtils.js'; 21 | 22 | /** 23 | * Checks if a user or server is blacklisted in a given hub. 24 | * @param hubId - The ID of the hub to check in. 25 | * @param guildId - The ID of the guild to check for blacklist. 26 | * @param userId - The ID of the user to check for blacklist. 27 | * @returns An object containing whether the user and/or server is blacklisted in the hub. 28 | */ 29 | export const checkBlacklists = async ( 30 | hubId: string, 31 | guildId: string | null, 32 | userId: string | null, 33 | ) => { 34 | const userBlacklistManager = userId ? new BlacklistManager('user', userId) : undefined; 35 | const guildBlacklistManager = guildId ? new BlacklistManager('server', guildId) : undefined; 36 | 37 | const userBlacklist = await userBlacklistManager?.fetchBlacklist(hubId); 38 | const serverBlacklist = await guildBlacklistManager?.fetchBlacklist(hubId); 39 | 40 | return { 41 | userBlacklisted: isBlacklisted(userBlacklist ?? null), 42 | serverBlacklisted: isBlacklisted(serverBlacklist ?? null), 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/utils/reaction/sortReactions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 InterChat 3 | * 4 | * InterChat is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * InterChat is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with InterChat. If not, see . 16 | */ 17 | 18 | /** 19 | * Sorts the reactions object based on the reaction counts. 20 | * @param reactions - The reactions object to be sorted. 21 | * @returns The sorted reactions object in the form of an array. 22 | * The array is sorted in descending order based on the length of the reaction arrays. 23 | * Each element of the array is a tuple containing the reaction and its corresponding array of user IDs. 24 | * 25 | * **Before:** 26 | * ```ts 27 | * { '👍': ['10201930193'], '👎': ['10201930193', '10201930194'] } 28 | * ``` 29 | * **After:** 30 | * ```ts 31 | * [ [ '👎', ['10201930193', '10201930194'] ], [ '👍', ['10201930193'] ] ] 32 | * ``` 33 | * */ 34 | export default (reactions: { [key: string]: string[] }): [string, string[]][] => { 35 | const idk = Object.entries(reactions).sort((a, b) => b[1].length - a[1].length); 36 | return idk; 37 | }; 38 | -------------------------------------------------------------------------------- /src/utils/report/ReportReasons.ts: -------------------------------------------------------------------------------- 1 | import { type supportedLocaleCodes, t } from '#utils/Locale.js'; 2 | import type { SelectMenuComponentOptionData } from 'discord.js'; 3 | 4 | export type ReportReason = 5 | | 'spam' 6 | | 'advertising' 7 | | 'nsfw' 8 | | 'harassment' 9 | | 'hate_speech' 10 | | 'scam' 11 | | 'illegal' 12 | | 'personal_info' 13 | | 'impersonation' 14 | | 'breaks_hub_rules' 15 | | 'trolling' 16 | | 'misinformation' 17 | | 'gore_violence' 18 | | 'raid_organizing' 19 | | 'underage'; 20 | 21 | export function getReasonFromKey(key: ReportReason, locale: supportedLocaleCodes): string { 22 | return t(`report.reasons.${key}`, locale); 23 | } 24 | 25 | export function getReportReasons(locale: supportedLocaleCodes): SelectMenuComponentOptionData[] { 26 | return [ 27 | { value: 'spam', label: t('report.reasons.spam', locale) }, 28 | { value: 'advertising', label: t('report.reasons.advertising', locale) }, 29 | { value: 'nsfw', label: t('report.reasons.nsfw', locale) }, 30 | { value: 'harassment', label: t('report.reasons.harassment', locale) }, 31 | { value: 'hate_speech', label: t('report.reasons.hate_speech', locale) }, 32 | { value: 'scam', label: t('report.reasons.scam', locale) }, 33 | { value: 'illegal', label: t('report.reasons.illegal', locale) }, 34 | { value: 'personal_info', label: t('report.reasons.personal_info', locale) }, 35 | { value: 'impersonation', label: t('report.reasons.impersonation', locale) }, 36 | { value: 'breaks_hub_rules', label: t('report.reasons.breaks_hub_rules', locale) }, 37 | { value: 'trolling', label: t('report.reasons.trolling', locale) }, 38 | { value: 'misinformation', label: t('report.reasons.misinformation', locale) }, 39 | { value: 'gore_violence', label: t('report.reasons.gore_violence', locale) }, 40 | { value: 'raid_organizing', label: t('report.reasons.raid_organizing', locale) }, 41 | { value: 'underage', label: t('report.reasons.underage', locale) }, 42 | ]; 43 | } 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "composite": true, 5 | "target": "ESNext", 6 | "module": "NodeNext", 7 | "moduleResolution": "NodeNext", 8 | "resolveJsonModule": true, 9 | "allowJs": false, 10 | "declaration": true, 11 | "baseUrl": ".", 12 | "rootDir": "./src", 13 | "outDir": "./build", 14 | "esModuleInterop": true, 15 | "strict": true, 16 | "inlineSourceMap": true, 17 | "skipLibCheck": true, 18 | "useUnknownInCatchVariables": false, 19 | "experimentalDecorators": true, 20 | "emitDecoratorMetadata": true, 21 | "strictNullChecks": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "paths": { 24 | "#src/*": ["./src/*"], 25 | "#utils/*": ["./src/utils/*"], 26 | "#types/*.d.ts": ["src/types/*.d.ts"] 27 | } 28 | }, 29 | "include": ["src/**/*.ts", "src/**/*.json"], 30 | "exclude": ["node_modules", "build"], 31 | "packageManager": "bun@1.2.1" 32 | } 33 | --------------------------------------------------------------------------------