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 6ee688b..b4f32d2 Binary files a/bun.lockb and b/bun.lockb differ 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" } } }