├── .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 |
--------------------------------------------------------------------------------