From 9b6ba56d999b6a7d24532fcddd56bb12a2aa666d Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Thu, 4 Jul 2024 21:02:01 +0700 Subject: [PATCH] chore: use alternative ways to bundle --- apis/websocket/.env.example | 3 - apis/websocket/config.revanced.json | 8 -- apis/websocket/docs/1_configuration.md | 2 +- apis/websocket/docs/2_running.md | 54 ----------- .../websocket/docs/2_running_and_deploying.md | 59 ++++++++++++ apis/websocket/package.json | 6 +- apis/websocket/scripts/build.ts | 23 +++++ apis/websocket/src/index.ts | 17 ++-- apis/websocket/tsconfig.json | 5 +- bots/discord/config.revanced.ts | 41 -------- bots/discord/docs/1_configuration.md | 44 +++++++-- bots/discord/docs/2_adding_autoresponses.md | 2 +- bots/discord/docs/3_running.md | 24 ----- bots/discord/docs/3_running_and_deploying.md | 68 ++++++++++++++ bots/discord/docs/4_commands_and_events.md | 39 +++++--- bots/discord/docs/5_databases.md | 88 ------------------ bots/discord/package.json | 19 ++-- bots/discord/scripts/generate-indexes.ts | 6 ++ bots/discord/src/commands/development/eval.ts | 7 +- .../commands/development/exception-test.ts | 39 +++----- bots/discord/src/commands/development/stop.ts | 10 +- bots/discord/src/commands/fun/coinflip.ts | 2 +- bots/discord/src/commands/fun/reply.ts | 2 +- bots/discord/src/commands/index.ts | 70 +++----------- bots/discord/src/commands/moderation/ban.ts | 2 +- bots/discord/src/commands/moderation/cure.ts | 2 +- bots/discord/src/commands/moderation/mute.ts | 2 +- bots/discord/src/commands/moderation/purge.ts | 2 +- .../src/commands/moderation/role-preset.ts | 2 +- .../src/commands/moderation/slowmode.ts | 2 +- bots/discord/src/commands/moderation/unban.ts | 2 +- .../discord/src/commands/moderation/unmute.ts | 2 +- bots/discord/src/commands/types.ts | 56 +++++++++++ bots/discord/src/context.ts | 12 ++- bots/discord/src/events/api/index.ts | 5 + bots/discord/src/events/discord/index.ts | 10 ++ bots/discord/src/events/register.ts | 2 + bots/discord/src/index.ts | 12 +-- bots/discord/src/types.d.ts | 5 - bots/discord/src/utils/discord/commands.ts | 2 +- bots/discord/src/utils/fs.ts | 21 ++++- bots/discord/tsconfig.json | 16 +++- bun.lockb | Bin 118328 -> 119372 bytes docs/0_development_environment.md | 19 ++-- package.json | 11 ++- packages/api/package.json | 1 + packages/api/tsconfig.json | 3 +- packages/shared/tsconfig.json | 3 +- tsconfig.json | 4 +- turbo.json | 6 +- 50 files changed, 443 insertions(+), 399 deletions(-) delete mode 100755 apis/websocket/config.revanced.json delete mode 100644 apis/websocket/docs/2_running.md create mode 100644 apis/websocket/docs/2_running_and_deploying.md create mode 100644 apis/websocket/scripts/build.ts delete mode 100644 bots/discord/config.revanced.ts delete mode 100644 bots/discord/docs/3_running.md create mode 100644 bots/discord/docs/3_running_and_deploying.md delete mode 100644 bots/discord/docs/5_databases.md create mode 100644 bots/discord/scripts/generate-indexes.ts create mode 100644 bots/discord/src/commands/types.ts create mode 100644 bots/discord/src/events/api/index.ts create mode 100644 bots/discord/src/events/discord/index.ts create mode 100644 bots/discord/src/events/register.ts delete mode 100644 bots/discord/src/types.d.ts mode change 100755 => 100644 bun.lockb diff --git a/apis/websocket/.env.example b/apis/websocket/.env.example index 9e40ca4..abfe5e7 100755 --- a/apis/websocket/.env.example +++ b/apis/websocket/.env.example @@ -1,5 +1,2 @@ -# Safety measures, do not remove -IS_USING_DOT_ENV=1 - # Your Wit.ai token WIT_AI_TOKEN="YOUR_TOKEN_HERE" diff --git a/apis/websocket/config.revanced.json b/apis/websocket/config.revanced.json deleted file mode 100755 index ffb38a0..0000000 --- a/apis/websocket/config.revanced.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "./config.schema.json", - - "address": "127.0.0.1", - "port": 3000, - "ocrConcurrentQueues": 3, - "logLevel": "log" -} diff --git a/apis/websocket/docs/1_configuration.md b/apis/websocket/docs/1_configuration.md index 54999e5..3becf24 100644 --- a/apis/websocket/docs/1_configuration.md +++ b/apis/websocket/docs/1_configuration.md @@ -42,4 +42,4 @@ The possible levels (sorted by their importance descendingly) are: The next page will tell you how to run and bundle the server. -Continue: [🏃🏻‍♂️ Running the server](./2_running.md) +Continue: [🏃🏻‍♂️ Running the server](./2_running_and_deploying.md) diff --git a/apis/websocket/docs/2_running.md b/apis/websocket/docs/2_running.md deleted file mode 100644 index 5d3b99a..0000000 --- a/apis/websocket/docs/2_running.md +++ /dev/null @@ -1,54 +0,0 @@ -# 🏃🏻‍♂️ Running the server - -There are many methods to run the server. Choose one that suits best for the situation. - -## 👷🏻 Development mode (recommended) - -There will be no compilation step, and Bun will automatically watch changes and restart the server for you. - -You can quickly start the server by running: - -```sh -bun dev -``` - -## 🌐 Production mode - -Production mode runs no different from the development server, it simply has less debugging information printed to console by default. However, more production-specific features may come. - -To start the server in production mode, you'll have to: - -1. Set the `NODE_ENV` environment variable to `production` - - > It is very possible to set the value in the `.env` file and let Bun load it, **but it is recommended to set the variable before Bun even starts**. - -2. Start the server - ```sh - bun dev - ``` - -## 📦 Building - -If you're looking to build and host the server somewhere else, you can run: - -```sh -bun bundle -``` - -The files will be placed in the `dist` directory. **Configurations and `.env` files will NOT be copied automatically.** - -To start up the server, you'll need to install `tesseract.js` first. -```sh -bun install tesseract.js -# or -bun install tesseract.js -g - -# Run the server -bun run index.js -``` - -## ⏭️ What's next - -The next page will tell you about packets. - -Continue: [📨 Packets](./3_packets.md) diff --git a/apis/websocket/docs/2_running_and_deploying.md b/apis/websocket/docs/2_running_and_deploying.md new file mode 100644 index 0000000..ae57227 --- /dev/null +++ b/apis/websocket/docs/2_running_and_deploying.md @@ -0,0 +1,59 @@ +# 🏃🏻‍♂️ Running and deploying the server + +There are many methods to run the server. Choose one that suits best for the situation. + +## 👷🏻 Development mode + +There will be no compilation step, and Bun will automatically watch changes and restart the server for you. + +You can quickly start the server by running: + +```sh +bun dev +``` + +## 📦 Building + +If you're looking to build and host the server somewhere else, you can run: + +```sh +bun run build +``` + +The distribution files will be placed inside the `dist` directory. Inside will include: + +- The default configuration for the API +- Compiled source files of the API + +You'll need to also copy the `node_modules` directory dereferenced if you want to run the distribution files somewhere else. + +## ✈️ Deploying + +To deploy the API, you'll need to: + +1. [Build the API as seen in the previous step](#-building) + +2. Copy contents of the `dist` directory + + ```sh + # For instance, we'll copy them both to /usr/src/api + cp -R ./dist/* /usr/src/api + ``` + +3. Replace the default configuration *(optional)* + +4. Configure environment variables + As seen in [`.env.example`](../.env.example). You can also optionally use a `.env` file which **Bun will automatically load**. + +5. Finally, you can run the API using these commands + + ```sh + cd /usr/src/api + bun run index.js + ``` + +## ⏭️ What's next + +The next page will tell you about packets. + +Continue: [📨 Packets](./3_packets.md) diff --git a/apis/websocket/package.json b/apis/websocket/package.json index 5257e09..75718a1 100755 --- a/apis/websocket/package.json +++ b/apis/websocket/package.json @@ -6,7 +6,7 @@ "description": "🧦 WebSocket API server for bots assisting ReVanced", "main": "dist/index.js", "scripts": { - "bundle": "bun build src/index.ts --outdir=dist --target=bun -e tesseract.js", + "bundle": "bun run scripts/build.ts", "dev": "bun run src/index.ts --watch", "build": "bun bundle", "watch": "bun dev" @@ -30,9 +30,11 @@ "@revanced/bot-shared": "workspace:*", "@sapphire/async-queue": "^1.5.2", "chalk": "^5.3.0", - "tesseract.js": "^5.1.0" + "tesseract.js": "^5.1.0", + "ws": "^8.17.1" }, "devDependencies": { + "@types/ws": "^8.5.10", "typed-emitter": "^2.1.0" } } diff --git a/apis/websocket/scripts/build.ts b/apis/websocket/scripts/build.ts new file mode 100644 index 0000000..6a2d348 --- /dev/null +++ b/apis/websocket/scripts/build.ts @@ -0,0 +1,23 @@ +import { createLogger } from '@revanced/bot-shared' +import { cp } from 'fs/promises' + +async function build(): Promise { + const logger = createLogger() + + logger.info('Building Tesseract.js worker...') + await Bun.build({ + entrypoints: ['../../node_modules/tesseract.js/src/worker-script/node/index.js'], + target: 'bun', + outdir: './dist/worker', + }) + + logger.info('Building WebSocket API...') + await Bun.build({ + entrypoints: ['./src/index.ts'], + outdir: './dist', + target: 'bun', + }) +} + +await build() +await cp('config.json', 'dist/config.json') diff --git a/apis/websocket/src/index.ts b/apis/websocket/src/index.ts index 110c82e..571ad1a 100755 --- a/apis/websocket/src/index.ts +++ b/apis/websocket/src/index.ts @@ -1,6 +1,8 @@ -import { createWorker as createTesseractWorker } from 'tesseract.js' +import { OEM, createWorker as createTesseractWorker } from 'tesseract.js' +import { join as joinPath } from 'path' import { inspect as inspectObject } from 'util' +import { exists as pathExists } from 'fs/promises' import Client from './classes/Client' @@ -36,10 +38,6 @@ if (!['development', 'production'].includes(environment)) { logger.info(`Running in ${environment} mode...`) -if (environment === 'production' && process.env['IS_USING_DOT_ENV']) { - logger.warn('You seem to be using .env files, this is generally not a good idea in production...') -} - if (!process.env['WIT_AI_TOKEN']) { logger.error('WIT_AI_TOKEN is not defined in the environment variables') process.exit(1) @@ -47,7 +45,14 @@ if (!process.env['WIT_AI_TOKEN']) { // Workers and API clients -const tesseract = await createTesseractWorker('eng') +const TesseractWorkerPath = joinPath(import.meta.dir, 'worker', 'index.js') +const TesseractCompiledWorkerExists = await pathExists(TesseractWorkerPath) +const tesseract = await createTesseractWorker( + 'eng', + OEM.DEFAULT, + TesseractCompiledWorkerExists ? { workerPath: TesseractWorkerPath } : undefined, +) + const wit = { token: process.env['WIT_AI_TOKEN']!, async fetch(route: string, options?: RequestInit) { diff --git a/apis/websocket/tsconfig.json b/apis/websocket/tsconfig.json index b80117e..1810ebe 100755 --- a/apis/websocket/tsconfig.json +++ b/apis/websocket/tsconfig.json @@ -7,8 +7,9 @@ "target": "ESNext", "lib": ["ESNext"], "composite": false, - "skipLibCheck": true + "skipLibCheck": true, + "resolveJsonModule": true }, "exclude": ["node_modules", "dist"], - "include": ["./*.json", "src/**/*.ts"] + "include": ["./*.json", "src/**/*.ts", "scripts/**/*.ts"] } diff --git a/bots/discord/config.revanced.ts b/bots/discord/config.revanced.ts deleted file mode 100644 index 09c4314..0000000 --- a/bots/discord/config.revanced.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { PermissionFlagsBits } from 'discord.js' -import type { Config } from './config.schema' - -export default { - owners: ['629368283354628116', '737323631117598811', '282584705218510848'], - guilds: ['952946952348270622'], - messageScan: { - filter: { - // Team, Mod, Immunity - roles: ['952987191401926697', '955220417969262612', '1027874293192863765'], - users: [], - // Team, Development - channels: ['952987428786941952', '953965039105232906'], - whitelist: false, - }, - humanCorrections: { - falsePositiveLabel: 'false_positive', - allow: { - members: { - // Team, Supporter - roles: ['952987191401926697', '1019903194941362198'], - permissions: PermissionFlagsBits.ManageMessages, - }, - }, - }, - allowedAttachmentMimeTypes: ['image/jpeg', 'image/png', 'image/webp'], - responses: [ - { - triggers: { - text: [{ label: 'false_positive', threshold: 0 }], - }, - response: null, - }, - ], - }, - logLevel: 'debug', - api: { - url: 'ws://127.0.0.1:3000', - disconnectLimit: 3, - }, -} satisfies Config as Config diff --git a/bots/discord/docs/1_configuration.md b/bots/discord/docs/1_configuration.md index 8cc2631..09106d0 100644 --- a/bots/discord/docs/1_configuration.md +++ b/bots/discord/docs/1_configuration.md @@ -1,18 +1,22 @@ # ⚙️ Configuration +This page tells you how to configure the bot. + +## 📄 JSON config + See [`config.ts`](../config.ts). --- -### `config.owners` +#### `config.owners` User IDs of the owners of the bot. Only add owners when needed. -### `config.guilds` +#### `config.guilds` Servers the bot is allowed to be and register commands in. -### `config.logLevel` +#### `config.logLevel` The level of logs to print to console. If the level is more important or equally important to set level, it will be forwarded to the console. @@ -26,14 +30,42 @@ The possible levels (sorted by their importance descendingly) are: - `log` - `debug` -### `config.api.websocketUrl` +#### `config.api.url` -The WebSocket URL to connect to (including port). Soon auto-discovery will be implemented. +WebSocket URL to connect to (including port). Soon auto-discovery will be implemented. -### `config.messageScan` +#### `config.api.disconnectLimit` + +Amount of times to allow disconnecting before exiting with code `1`. + +#### `config.messageScan` [Please see the next page.](./2_adding_autoresponses.md) +#### `config.moderation` + +TBD. + +#### `config.rolePresets` + +TBD. + +## 🟰 Environment variables + +See [`.env.example`](../.env.example). +You can set environment variables in your shell or use a `.env` file which **Bun will automatically load**. + +--- + +#### `DISCORD_TOKEN` + +The Discord bot token. + +#### `DATABASE_URL` + +The database URL, since we're using SQLite, we'll be using the `file` protocol. +Example values are: `file:./revanced.db`, `file:./db.sqlite`, `file:./discord_bot.sqlite` + ## ⏭️ What's next The next page will tell you how to configure auto-responses. diff --git a/bots/discord/docs/2_adding_autoresponses.md b/bots/discord/docs/2_adding_autoresponses.md index bb028a8..5d6a504 100644 --- a/bots/discord/docs/2_adding_autoresponses.md +++ b/bots/discord/docs/2_adding_autoresponses.md @@ -86,4 +86,4 @@ filterOverride: { The next page will tell you how to run and bundle the bot. -Continue: [🏃🏻‍♂️ Running the bot](./3_running.md) +Continue: [🏃🏻‍♂️ Running the bot](./3_running_and_deploying.md) diff --git a/bots/discord/docs/3_running.md b/bots/discord/docs/3_running.md deleted file mode 100644 index f82e2ad..0000000 --- a/bots/discord/docs/3_running.md +++ /dev/null @@ -1,24 +0,0 @@ -# 🏃🏻‍♂️ Running the bot - -There are two methods to run the bot. Choose one that suits best for the situation. - -## 👷🏻 Development mode (recommended) - -There will be no compilation step, and Bun will automatically watch changes and restart the bot for you. - -You can quickly start the bot by running: - -```sh -bun dev -``` - -## 📦 Building - -There's unfortunately no way to build/bundle the bot yet due to how dynamic imports currently work, though we have a few ideas that may work. -As a workaround, you can zip up the whole project, unzip, and run it in development mode using Bun. - -## ⏭️ What's next - -The next page will tell you how to add commands and listen to events to the bot. - -Continue: [✨ Adding commands and listening to events](./4_commands_and_events.md) diff --git a/bots/discord/docs/3_running_and_deploying.md b/bots/discord/docs/3_running_and_deploying.md new file mode 100644 index 0000000..e191359 --- /dev/null +++ b/bots/discord/docs/3_running_and_deploying.md @@ -0,0 +1,68 @@ +# 🏃🏻‍♂️ Running and deploying + +There are two methods to run the bot. Choose one that suits best for the situation. + +## 👷🏻 Development mode (recommended) + +There will be no compilation step, and Bun will automatically watch changes and restart the bot for you. + +You can quickly start the bot by running: + +```sh +bun dev +``` + +## 📦 Building + +To build the bot, you can run: + +```sh +bun run build +``` + +The distribution files will be placed inside the `dist` directory. Inside will include: + +- The default configuration for the bot +- An empty database for the bot with schemas configured +- Compiled source files of the bot + +## ✈️ Deploying + +To deploy the bot, you'll need to: + +1. Replace the `config.ts` file with your own configuration _(optional)_ +2. [Build the bot as seen in the previous step](#-building) +3. Run the `reload-slash-commands` script + This is to ensure all commands are registered, so they can be used. + **It may take up to 2 hours until **global** commands are updated. This is a Discord limitation.** + + ```sh + # Assuming you're in the workspace's root (NOT REPOSITORY ROOT) + bun run scripts/reload-slash-commands.ts + ``` + +4. Copy contents of the `dist` directory + + ```sh + # For instance, we'll copy them both to /usr/src/discord-bot + # Assuming you're in the workspace's root (NOT REPOSITORY ROOT) + cp -R ./dist/* /usr/src/discord-bot + ``` + +5. Replace the default empty database with your own _(optional)_ + +6. Configure environment variables + As seen in [`.env.example`](../.env.example). You can also optionally use a `.env` file which **Bun will automatically load**. + +7. Finally, run the bot + + ```sh + cd /usr/src/discord-bot + bun run src/index.js + ``` + +## ⏭️ What's next + +The next page will tell you how to add commands and listen to events to the bot. + +Continue: [✨ Adding commands and listening to events](./4_commands_and_events.md) diff --git a/bots/discord/docs/4_commands_and_events.md b/bots/discord/docs/4_commands_and_events.md index fd1df88..e1bb033 100644 --- a/bots/discord/docs/4_commands_and_events.md +++ b/bots/discord/docs/4_commands_and_events.md @@ -74,34 +74,49 @@ export default { Events are a bit different. We have 2 different event systems for both Discord API and our own bot API. This means the [`src/events`](../src/events) directory will have 2 separate directories inside. They are specific to the respective API, but the utility functions make the experience with both of them very similar. -To start adding events, you can use this template: +To start adding events, you can use these templates: + +##### Discord event template ```ts -// For Discord events (remove functions you do not use) -import { on, once } from '$utils/discord/events' +import { on, once, withContext } from '$utils/discord/events' -// You will have auto-complete and types for all of them, don't worry! -// WARNING: The first argument is the `context` object for Discord events -// This is intended by design because Discord events usually always use it. -on('eventName', async (context, arg1, arg2, ...) => { - // Do something in here when the event is triggered +on('eventName', async (arg1, arg2, ...) => { + // Do something when the event is triggered +}) + +once('eventName', async (arg1, arg2, ...) => { + // Do something for only a single time after it's triggered, never again +}) + +withContext(on, 'eventName', async (context, arg1, arg2, ...) => { + // Do some other thing that requires the context object }) ``` +##### API events template + ```ts -// For "Helper" events (remove functions you do not use) import { on, once } from '$utils/api/events' -// You will have auto-complete and types for all of them, don't worry! on('eventName', async (arg1, arg2, ...) => { - // Do something in here when the event is triggered + // Do something when the event is triggered +}) + +once('eventName', async (arg1, arg2, ...) => { + // Do something for only a single time after it's triggered, never again }) ``` API events are stored in [`src/events/api`](../src/events/api), and Discord events are in [`src/events/discord`](../src/events/discord). +### 📛 Event file naming conventions + +Since a single event file can have multiple listeners, you should name exactly what the file handles. +For example, when a nickname change happens, a member joins, or a member sends a message, the bot is required to cure their nickname. Therefore we would name the event file `curedRequired.ts`. + > [!NOTE] -> If you need multiple event listeners for the same exact event, you can put them in a directory with the event name and rename the listeners to what they handle specifically. You can see how we do it in [`src/events/discord/interactionCreate`](../src/events/discord/interactionCreate). +> If you need multiple event listeners for the same exact event **but also need more abstraction**, you can put them in a directory with the event name and rename the listeners to what they handle specifically. You can see how we do it in [`src/events/discord/interactionCreate`](../src/events/discord/interactionCreate). ## ⏭️ What's next diff --git a/bots/discord/docs/5_databases.md b/bots/discord/docs/5_databases.md deleted file mode 100644 index d08e974..0000000 --- a/bots/discord/docs/5_databases.md +++ /dev/null @@ -1,88 +0,0 @@ -# 🫙 Storing data - -We use SQLite to store every piece of persistent data. By using Bun, we get access to the `bun:sqlite` module which allows us to easily do SQLite operations. - -## 🪄 Creating a database - -You can easily create a database by initializing the `BasicDatabase` class: - -```ts -interface MyDatabase { - field: string - key: string -} - -const db = new BasicDatabase( - // File path - 'database_file.db', - // Database schema, in SQL - `field TEXT NOT NULL, key TEXT PRIMARY KEY NOT NULL`, - // Custom table name (optional, defaults to 'data'), - 'data' -) -``` - -## 📝 Writing data - -Initializing `MyDatabase` will immediately create/open the `database_file.db` file. To write data, you can use the `insert` or `update` method: - -```ts -const key = 'my key' -const field = 'some data' - -// Order is according to the schema -// db.insert(...columns) -db.insert(field, key) - -const field2 = 'some other data' - -// db.update(data, filter) -db.update({ - field: field2 -}, `key = ${key}`) -``` - -You can also delete a row: - -```ts -db.delete(`key = ${key}`) - -console.log(db.select(`key = ${key}`)) // null -``` - -## 👀 Reading data - -To get data using a filter, you can use the `select` method: - -```ts -// We insert it back -db.insert(field, key) - -const data = db.select('*', `key = ${key}`) -console.log(data) // { key: 'my key', field: 'some other data' } - -const { key: someKey } = db.select('key', `field = '${field2}'`) -console.log(someKey) // 'my key' -``` - - -If the existing abstractions aren't enough, you can also use the `run`, `prepare`, or `query` method: - -```ts -// Enable WAL -db.run('PRAGMA journal_mode=WAL') - -const selectFromKey = db.prepare('SELECT * FROM data WHERE key = $key') - -console.log( - selectFromKey.get({ - $key: key - }) -) // { key: 'my key', field: 'some other data' } - -console.log( - selectFromKey.get({ - $key: 'non existent key' - }) -) // null -``` diff --git a/bots/discord/package.json b/bots/discord/package.json index 077ac98..780b8a9 100644 --- a/bots/discord/package.json +++ b/bots/discord/package.json @@ -4,13 +4,15 @@ "private": true, "version": "0.1.0", "description": "🤖 Discord bot assisting ReVanced", - "main": "dist/index.js", + "main": "src/index.ts", "scripts": { "register": "bun run scripts/reload-slash-commands.ts", - "dev": "bun --watch src/index.ts", - "prepare": "drizzle-kit push", - "build": "tsc", - "watch": "bun dev" + "start": "bun run scripts/generate-indexes.ts && bun run src/index.ts", + "dev": "bun run scripts/generate-indexes.ts && bun --watch src/index.ts", + "build:config": "bun build config.ts --outdir=dist", + "build": "bun prepare && bun build:config && bun build src/index.ts -e ./config.js --target=bun --outdir=dist/src && DATABASE_URL=dist/db.sqlite3 drizzle-kit push", + "watch": "bun dev", + "prepare": "bun run scripts/generate-indexes.ts" }, "repository": { "type": "git", @@ -28,15 +30,18 @@ }, "homepage": "https://github.com/revanced/revanced-helper#readme", "dependencies": { + "@discordjs/builders": "^1.8.2", + "@discordjs/rest": "^2.3.0", + "@libsql/client": "^0.6.2", "@revanced/bot-api": "workspace:*", "@revanced/bot-shared": "workspace:*", "chalk": "^5.3.0", "decancer": "^3.2.2", "discord.js": "^14.15.3", + "drizzle-kit": "^0.22.7", "drizzle-orm": "^0.31.2" }, "devDependencies": { - "@libsql/client": "^0.6.2", - "drizzle-kit": "^0.22.7" + "discord-api-types": "^0.37.91" } } diff --git a/bots/discord/scripts/generate-indexes.ts b/bots/discord/scripts/generate-indexes.ts new file mode 100644 index 0000000..bf2762d --- /dev/null +++ b/bots/discord/scripts/generate-indexes.ts @@ -0,0 +1,6 @@ +import { join } from 'path' +import { generateCommandsIndex, generateEventsIndex } from '../src/utils/fs' + +await generateCommandsIndex(join(import.meta.dir, '../src/commands')) +await generateEventsIndex(join(import.meta.dir, '../src/events/discord')) +await generateEventsIndex(join(import.meta.dir, '../src/events/api')) diff --git a/bots/discord/src/commands/development/eval.ts b/bots/discord/src/commands/development/eval.ts index f145a79..223d66d 100644 --- a/bots/discord/src/commands/development/eval.ts +++ b/bots/discord/src/commands/development/eval.ts @@ -2,12 +2,12 @@ import { inspect } from 'util' import { SlashCommandBuilder } from 'discord.js' import { createSuccessEmbed } from '$/utils/discord/embeds' -import type { Command } from '..' +import type { Command } from '../types' export default { data: new SlashCommandBuilder() .setName('eval') - .setDescription('Evaluates something') + .setDescription('Make the bot less sentient by evaluating code') .addStringOption(option => option.setName('code').setDescription('The code to evaluate').setRequired(true)) .setDMPermission(true) .toJSON(), @@ -15,8 +15,7 @@ export default { ownerOnly: true, global: true, - // @ts-expect-error: Needed for science - async execute(context, interaction) { + async execute(_, interaction) { const code = interaction.options.getString('code', true) await interaction.reply({ diff --git a/bots/discord/src/commands/development/exception-test.ts b/bots/discord/src/commands/development/exception-test.ts index fb9440a..70dfbae 100644 --- a/bots/discord/src/commands/development/exception-test.ts +++ b/bots/discord/src/commands/development/exception-test.ts @@ -1,41 +1,26 @@ import { SlashCommandBuilder } from 'discord.js' import CommandError, { CommandErrorType } from '$/classes/CommandError' -import type { Command } from '..' +import type { Command } from '../types' export default { data: new SlashCommandBuilder() .setName('exception-test') - .setDescription('throw up pls') + .setDescription('Makes the bot intentionally hate you by throwing an exception') .addStringOption(option => option .setName('type') .setDescription('The type of exception to throw') - .addChoices({ - name: 'process exception', - value: 'Process', - }) - .addChoices({ - name: 'generic error', - value: 'Generic', - }) - .addChoices({ - name: 'invalid argument', - value: 'InvalidArgument', - }) - .addChoices({ - name: 'invalid channel', - value: 'InvalidChannel', - }) - .addChoices({ - name: 'invalid user', - value: 'InvalidUser', - }) - .addChoices({ - name: 'invalid duration', - value: 'InvalidDuration', - }) - .setRequired(true), + .setRequired(true) + .addChoices( + Object.keys(CommandErrorType).map( + k => + ({ + name: k, + value: k, + }) as const, + ), + ), ) .setDMPermission(true) .toJSON(), diff --git a/bots/discord/src/commands/development/stop.ts b/bots/discord/src/commands/development/stop.ts index 1627d54..2d08763 100644 --- a/bots/discord/src/commands/development/stop.ts +++ b/bots/discord/src/commands/development/stop.ts @@ -1,9 +1,15 @@ import { SlashCommandBuilder } from 'discord.js' -import type { Command } from '..' +import type { Command } from '../types' export default { - data: new SlashCommandBuilder().setName('stop').setDescription('Stops the bot').setDMPermission(true).toJSON(), + data: new SlashCommandBuilder() + .setName('stop') + .setDescription( + "You don't want to run this unless the bot starts to go insane, and like, you really need to stop it.", + ) + .setDMPermission(true) + .toJSON(), ownerOnly: true, global: true, diff --git a/bots/discord/src/commands/fun/coinflip.ts b/bots/discord/src/commands/fun/coinflip.ts index 5b724f5..e6e7a95 100644 --- a/bots/discord/src/commands/fun/coinflip.ts +++ b/bots/discord/src/commands/fun/coinflip.ts @@ -2,7 +2,7 @@ import { applyCommonEmbedStyles } from '$/utils/discord/embeds' import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' -import type { Command } from '..' +import type { Command } from '../types' export default { data: new SlashCommandBuilder().setName('coinflip').setDescription('Do a coinflip!').setDMPermission(true).toJSON(), diff --git a/bots/discord/src/commands/fun/reply.ts b/bots/discord/src/commands/fun/reply.ts index 73fe802..4a5c530 100644 --- a/bots/discord/src/commands/fun/reply.ts +++ b/bots/discord/src/commands/fun/reply.ts @@ -1,7 +1,7 @@ import { SlashCommandBuilder, type TextBasedChannel } from 'discord.js' import { config } from '$/context' -import type { Command } from '..' +import type { Command } from '../types' export default { data: new SlashCommandBuilder() diff --git a/bots/discord/src/commands/index.ts b/bots/discord/src/commands/index.ts index e782811..3bd4ce2 100644 --- a/bots/discord/src/commands/index.ts +++ b/bots/discord/src/commands/index.ts @@ -1,56 +1,16 @@ -import type { SlashCommandBuilder } from '@discordjs/builders' -import type { ChatInputCommandInteraction } from 'discord.js' +// AUTO-GENERATED BY A SCRIPT, DON'T TOUCH -// Temporary system -export type Command = { - data: ReturnType - // The function has to return void or Promise - // because TS may complain about some code paths not returning a value - /** - * The function to execute when this command is triggered - * @param interaction The interaction that triggered this command - */ - execute: ( - context: typeof import('../context'), - interaction: ChatInputCommandInteraction, - info: Info, - ) => Promise | void - memberRequirements?: { - /** - * The mode to use when checking for requirements. - * - `all` means that the user needs meet all requirements specified. - * - `any` means that the user needs to meet any of the requirements specified. - * - * @default "all" - */ - mode?: 'all' | 'any' - /** - * The permissions required to use this command (in BitFields). - * - * - **0n** means that everyone can use this command. - * - **-1n** means that only bot owners can use this command. - * @default -1n - */ - permissions?: bigint - /** - * The roles required to use this command. - * By default, this is set to `[]`. - */ - roles?: string[] - } - /** - * Whether this command can only be used by bot owners. - * @default false - */ - ownerOnly?: boolean - /** - * Whether to register this command as a global slash command. - * This is set to `false` and commands will be registered in allowed guilds only by default. - * @default false - */ - global?: boolean -} - -export interface Info { - userIsOwner: boolean -} +import './index' +import './fun/reply' +import './fun/coinflip' +import './development/eval' +import './development/stop' +import './development/exception-test' +import './moderation/purge' +import './moderation/cure' +import './moderation/role-preset' +import './moderation/mute' +import './moderation/unmute' +import './moderation/unban' +import './moderation/slowmode' +import './moderation/ban' diff --git a/bots/discord/src/commands/moderation/ban.ts b/bots/discord/src/commands/moderation/ban.ts index 42d8f57..1b702b9 100644 --- a/bots/discord/src/commands/moderation/ban.ts +++ b/bots/discord/src/commands/moderation/ban.ts @@ -1,6 +1,6 @@ import { SlashCommandBuilder } from 'discord.js' -import type { Command } from '..' +import type { Command } from '../types' import CommandError, { CommandErrorType } from '$/classes/CommandError' import { config } from '$/context' diff --git a/bots/discord/src/commands/moderation/cure.ts b/bots/discord/src/commands/moderation/cure.ts index 60d9a0b..c00270f 100644 --- a/bots/discord/src/commands/moderation/cure.ts +++ b/bots/discord/src/commands/moderation/cure.ts @@ -1,6 +1,6 @@ import { SlashCommandBuilder } from 'discord.js' -import type { Command } from '..' +import type { Command } from '../types' import { config } from '$/context' import { createSuccessEmbed } from '$/utils/discord/embeds' diff --git a/bots/discord/src/commands/moderation/mute.ts b/bots/discord/src/commands/moderation/mute.ts index fa6ea26..af3b26f 100644 --- a/bots/discord/src/commands/moderation/mute.ts +++ b/bots/discord/src/commands/moderation/mute.ts @@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js' import CommandError, { CommandErrorType } from '$/classes/CommandError' import { applyRolePreset } from '$/utils/discord/rolePresets' -import type { Command } from '..' +import type { Command } from '../types' import { config } from '$/context' import { createModerationActionEmbed } from '$/utils/discord/embeds' diff --git a/bots/discord/src/commands/moderation/purge.ts b/bots/discord/src/commands/moderation/purge.ts index b1eb0e4..75eb398 100644 --- a/bots/discord/src/commands/moderation/purge.ts +++ b/bots/discord/src/commands/moderation/purge.ts @@ -4,7 +4,7 @@ import CommandError, { CommandErrorType } from '$/classes/CommandError' import { config } from '$/context' import { applyCommonEmbedStyles } from '$/utils/discord/embeds' -import type { Command } from '..' +import type { Command } from '../types' export default { data: new SlashCommandBuilder() diff --git a/bots/discord/src/commands/moderation/role-preset.ts b/bots/discord/src/commands/moderation/role-preset.ts index f8eac4b..0d4cbff 100644 --- a/bots/discord/src/commands/moderation/role-preset.ts +++ b/bots/discord/src/commands/moderation/role-preset.ts @@ -4,7 +4,7 @@ import CommandError, { CommandErrorType } from '$/classes/CommandError' import { sendPresetReplyAndLogs } from '$/utils/discord/moderation' import { applyRolePreset, removeRolePreset } from '$/utils/discord/rolePresets' import { parseDuration } from '$/utils/duration' -import type { Command } from '..' +import type { Command } from '../types' export default { data: new SlashCommandBuilder() diff --git a/bots/discord/src/commands/moderation/slowmode.ts b/bots/discord/src/commands/moderation/slowmode.ts index 61b49da..8cc3815 100644 --- a/bots/discord/src/commands/moderation/slowmode.ts +++ b/bots/discord/src/commands/moderation/slowmode.ts @@ -5,7 +5,7 @@ import { SlashCommandBuilder } from 'discord.js' import CommandError, { CommandErrorType } from '$/classes/CommandError' import { config } from '$/context' -import type { Command } from '..' +import type { Command } from '../types' export default { data: new SlashCommandBuilder() diff --git a/bots/discord/src/commands/moderation/unban.ts b/bots/discord/src/commands/moderation/unban.ts index 9adbe89..5bcfa93 100644 --- a/bots/discord/src/commands/moderation/unban.ts +++ b/bots/discord/src/commands/moderation/unban.ts @@ -1,6 +1,6 @@ import { SlashCommandBuilder } from 'discord.js' -import type { Command } from '..' +import type { Command } from '../types' import { config } from '$/context' import { createModerationActionEmbed } from '$/utils/discord/embeds' diff --git a/bots/discord/src/commands/moderation/unmute.ts b/bots/discord/src/commands/moderation/unmute.ts index 29860ea..2255a09 100644 --- a/bots/discord/src/commands/moderation/unmute.ts +++ b/bots/discord/src/commands/moderation/unmute.ts @@ -7,7 +7,7 @@ import { createModerationActionEmbed } from '$/utils/discord/embeds' import { sendModerationReplyAndLogs } from '$/utils/discord/moderation' import { removeRolePreset } from '$/utils/discord/rolePresets' import { and, eq } from 'drizzle-orm' -import type { Command } from '..' +import type { Command } from '../types' export default { data: new SlashCommandBuilder() diff --git a/bots/discord/src/commands/types.ts b/bots/discord/src/commands/types.ts new file mode 100644 index 0000000..e782811 --- /dev/null +++ b/bots/discord/src/commands/types.ts @@ -0,0 +1,56 @@ +import type { SlashCommandBuilder } from '@discordjs/builders' +import type { ChatInputCommandInteraction } from 'discord.js' + +// Temporary system +export type Command = { + data: ReturnType + // The function has to return void or Promise + // because TS may complain about some code paths not returning a value + /** + * The function to execute when this command is triggered + * @param interaction The interaction that triggered this command + */ + execute: ( + context: typeof import('../context'), + interaction: ChatInputCommandInteraction, + info: Info, + ) => Promise | void + memberRequirements?: { + /** + * The mode to use when checking for requirements. + * - `all` means that the user needs meet all requirements specified. + * - `any` means that the user needs to meet any of the requirements specified. + * + * @default "all" + */ + mode?: 'all' | 'any' + /** + * The permissions required to use this command (in BitFields). + * + * - **0n** means that everyone can use this command. + * - **-1n** means that only bot owners can use this command. + * @default -1n + */ + permissions?: bigint + /** + * The roles required to use this command. + * By default, this is set to `[]`. + */ + roles?: string[] + } + /** + * Whether this command can only be used by bot owners. + * @default false + */ + ownerOnly?: boolean + /** + * Whether to register this command as a global slash command. + * This is set to `false` and commands will be registered in allowed guilds only by default. + * @default false + */ + global?: boolean +} + +export interface Info { + userIsOwner: boolean +} diff --git a/bots/discord/src/context.ts b/bots/discord/src/context.ts index 9568561..2908e4f 100644 --- a/bots/discord/src/context.ts +++ b/bots/discord/src/context.ts @@ -4,13 +4,15 @@ import { createLogger } from '@revanced/bot-shared' import { ActivityType, Client as DiscordClient, Partials } from 'discord.js' import { drizzle } from 'drizzle-orm/bun-sqlite' -import config from '../config' +// Export config first, as commands require them +import config from '../config.js' +export { config } + +import * as commands from './commands' import * as schemas from './database/schemas' -import { loadCommands } from '$utils/discord/commands' -import { pathJoinCurrentDir } from '$utils/fs' +import type { Command } from './commands/types' -export { config } export const logger = createLogger({ level: config.logLevel === 'none' ? Number.MAX_SAFE_INTEGER : config.logLevel, }) @@ -59,5 +61,5 @@ export const discord = { ], }, }), - commands: await loadCommands(pathJoinCurrentDir(import.meta.url, 'commands')), + commands: Object.fromEntries(Object.values(commands).map((cmd) => [cmd.data.name, cmd])) as Record, } as const diff --git a/bots/discord/src/events/api/index.ts b/bots/discord/src/events/api/index.ts new file mode 100644 index 0000000..80e2524 --- /dev/null +++ b/bots/discord/src/events/api/index.ts @@ -0,0 +1,5 @@ +// AUTO-GENERATED BY A SCRIPT, DON'T TOUCH + +import './ready' +import './disconnect' +import './index' diff --git a/bots/discord/src/events/discord/index.ts b/bots/discord/src/events/discord/index.ts new file mode 100644 index 0000000..4f305b0 --- /dev/null +++ b/bots/discord/src/events/discord/index.ts @@ -0,0 +1,10 @@ +// AUTO-GENERATED BY A SCRIPT, DON'T TOUCH + +import './ready' +import './cureRequired' +import './index' +import './messageCreate/messageScanRequired' +import './messageReactionAdd/correctResponse' +import './interactionCreate/chatCommand' +import './interactionCreate/correctResponse' +import './guildMemberAdd/applyRolePresets' diff --git a/bots/discord/src/events/register.ts b/bots/discord/src/events/register.ts new file mode 100644 index 0000000..fbab0e6 --- /dev/null +++ b/bots/discord/src/events/register.ts @@ -0,0 +1,2 @@ +import './discord' +import './api' diff --git a/bots/discord/src/index.ts b/bots/discord/src/index.ts index 8b80c9f..1588c24 100644 --- a/bots/discord/src/index.ts +++ b/bots/discord/src/index.ts @@ -1,7 +1,8 @@ import { api, discord, logger } from '$/context' -import { listAllFilesRecursive, pathJoinCurrentDir } from '$utils/fs' import { getMissingEnvironmentVariables } from '@revanced/bot-shared' +import './events/register' + // Check if token exists const missingEnvs = getMissingEnvironmentVariables(['DISCORD_TOKEN', 'DATABASE_URL']) if (missingEnvs.length) { @@ -9,14 +10,5 @@ if (missingEnvs.length) { process.exit(1) } -for (const event of listAllFilesRecursive(pathJoinCurrentDir(import.meta.url, 'events', 'api'))) { - await import(event) -} - api.client.connect() - -for (const event of listAllFilesRecursive(pathJoinCurrentDir(import.meta.url, 'events', 'discord'))) { - await import(event) -} - discord.client.login() diff --git a/bots/discord/src/types.d.ts b/bots/discord/src/types.d.ts deleted file mode 100644 index 24ab764..0000000 --- a/bots/discord/src/types.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -type IfExtends = T extends U ? True : False -type IfTrue = IfExtends -type EmptyObject = Record -type ValuesOf = T[keyof T] -type MaybeArray = T | T[] diff --git a/bots/discord/src/utils/discord/commands.ts b/bots/discord/src/utils/discord/commands.ts index 5c4e454..188946f 100644 --- a/bots/discord/src/utils/discord/commands.ts +++ b/bots/discord/src/utils/discord/commands.ts @@ -1,4 +1,4 @@ -import type { Command } from '$commands' +import type { Command } from '$commands/types' import { listAllFilesRecursive } from '$utils/fs' export const loadCommands = async (dir: string) => { diff --git a/bots/discord/src/utils/fs.ts b/bots/discord/src/utils/fs.ts index 0fa0b7f..f3661a2 100644 --- a/bots/discord/src/utils/fs.ts +++ b/bots/discord/src/utils/fs.ts @@ -1,11 +1,22 @@ -import { readdirSync } from 'fs' -import { dirname, join } from 'path' -import { fileURLToPath } from 'bun' +import { readdirSync, writeFileSync } from 'fs' +import { join, relative } from 'path' export const listAllFilesRecursive = (dir: string): string[] => readdirSync(dir, { recursive: true, withFileTypes: true }) .filter(x => x.isFile()) .map(x => join(x.parentPath, x.name)) -export const pathJoinCurrentDir = (importMetaUrl: string, ...objects: [string, ...string[]]) => - join(dirname(fileURLToPath(importMetaUrl)), ...objects) +export const generateCommandsIndex = (dirPath: string) => generateIndexes(dirPath, x => !x.endsWith('types.ts')) + +export const generateEventsIndex = (dirPath: string) => generateIndexes(dirPath) + +const generateIndexes = async (dirPath: string, pathFilter?: (path: string) => boolean) => { + const files = listAllFilesRecursive(dirPath) + .filter(x => (x.endsWith('.ts') && !x.endsWith('index.ts') && pathFilter ? pathFilter(x) : true)) + .map(x => relative(dirPath, x)) + + writeFileSync( + join(dirPath, 'index.ts'), + `// AUTO-GENERATED BY A SCRIPT, DON'T TOUCH\n\n${files.map(c => `import './${c.split('.').at(-2)}'`).join('\n')}`, + ) +} diff --git a/bots/discord/tsconfig.json b/bots/discord/tsconfig.json index 8176edc..890c538 100755 --- a/bots/discord/tsconfig.json +++ b/bots/discord/tsconfig.json @@ -18,8 +18,18 @@ "$commands": ["./src/commands/index.ts"], "$commands/*": ["./src/commands/*"] }, - "skipLibCheck": true + "skipLibCheck": true, + "plugins": [ + { + "transform": "typescript-transform-path-rewrite" + } + ] }, - "exclude": ["node_modules", "dist"], - "include": ["./**/*.ts", "./*.ts"] + "exclude": [ + "node_modules", + "dist", + "./config.schema.ts", + "./drizzle.config.ts" + ], + "include": ["./src/**/*.ts", "./scripts/**/*.ts"] } diff --git a/bun.lockb b/bun.lockb old mode 100755 new mode 100644 index 6ee688bd593127a4800e3e919ceebc9f6aeee04a..b4f32d2ffdae65493b8468c1d067190580eac1f2 GIT binary patch delta 24791 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 aT~ancP1ti0G{&CjbEY0}EOJ delta 24193 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%0D{QNAU!%anZsd5S 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 diff --git a/docs/0_development_environment.md b/docs/0_development_environment.md index 87af6c1..817d41a 100644 --- a/docs/0_development_environment.md +++ b/docs/0_development_environment.md @@ -17,17 +17,24 @@ To start developing, you'll need to set up the development environment first. 3. Install dependencies - ```sh - bun install - ``` + ```sh + bun install + ``` -4. Build packages/libraries +4. Install Git hooks for linting (optional, but recommended) ```sh - bun run build + bunx lefthook install ``` -5. Change your directory to a project's root +5. Build packages/libraries + + ```sh + bun run build:packages + ``` + +6. Change your directory to a project's root + ```sh # WebSocket API cd apis/websocket diff --git a/package.json b/package.json index 99d2a3c..db9b265 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,14 @@ "license": "GPL-3.0-or-later", "type": "module", "author": "Palm (https://palmdevs.me)", - "workspaces": ["apis/*", "bots/*", "packages/*"], + "workspaces": ["packages/*", "apis/*", "bots/*"], "scripts": { "build": "turbo run build", + "build:packages": "turbo run build --filter=\"./packages/*\"", "watch": "turbo run watch", - "flint": "biome check --apply .", + "flint": "biome check --write .", "flint:check": "biome check .", - "clint": "commitlint --edit", - "prepare": "lefthook install" + "clint": "commitlint --edit" }, "homepage": "https://github.com/revanced/revanced-helper#readme", "repository": { @@ -27,6 +27,7 @@ "Palm (https://palmdevs.me)", "ReVanced (https://revanced.app)" ], + "packageManager": "pnpm@9.4.0", "devDependencies": { "@biomejs/biome": "^1.8.2", "@commitlint/cli": "^19.3.0", @@ -38,7 +39,7 @@ "concurrently": "^8.2.2", "conventional-changelog-conventionalcommits": "^7.0.2", "lefthook": "^1.6.18", - "turbo": "^1.13.4", + "turbo": "2", "typescript": "^5.5.2" }, "trustedDependencies": ["@biomejs/biome", "esbuild", "lefthook"] diff --git a/packages/api/package.json b/packages/api/package.json index aa22783..884446b 100755 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -31,6 +31,7 @@ "ws": "^8.17.1" }, "devDependencies": { + "@types/ws": "^8.5.10", "typed-emitter": "^2.1.0" } } diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 6c0164e..1cd3a5f 100755 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -7,5 +7,6 @@ "module": "ESNext", "composite": true, "noEmit": false - } + }, + "include": ["src/**/*.ts"] } diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 6c0164e..1cd3a5f 100755 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -7,5 +7,6 @@ "module": "ESNext", "composite": true, "noEmit": false - } + }, + "include": ["src/**/*.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 9962c1d..3280e50 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,5 +21,7 @@ "allowSyntheticDefaultImports": true, "isolatedModules": true, "allowImportingTsExtensions": false - } + }, + "include": ["./**/*"], + "exclude": ["./packages/**/*"] } diff --git a/turbo.json b/turbo.json index 7cacc10..b31dd44 100755 --- a/turbo.json +++ b/turbo.json @@ -1,14 +1,14 @@ { "$schema": "https://turbo.build/schema.json", - "pipeline": { + "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"], - "outputMode": "errors-only" + "outputLogs": "errors-only" }, "watch": { "dependsOn": ["^watch"], - "outputMode": "errors-only" + "outputLogs": "errors-only" } } }