diff --git a/bots/discord/config.example.ts b/bots/discord/config.example.ts index c4d1787..ed8a747 100644 --- a/bots/discord/config.example.ts +++ b/bots/discord/config.example.ts @@ -66,7 +66,13 @@ export type ConfigMessageScanResponse = { } export type ConfigMessageScanResponseLabelConfig = { + /** + * Label name + */ label: string + /** + * Confidence threshold + */ threshold: number } diff --git a/bots/discord/docs/1_configuration.md b/bots/discord/docs/1_configuration.md new file mode 100644 index 0000000..27e180c --- /dev/null +++ b/bots/discord/docs/1_configuration.md @@ -0,0 +1,124 @@ +# ⚙️ Configuration + +This is the default configuration (provided in [config.ts](../config.ts)): + +```ts +export default { + owners: ["USER_ID_HERE"], + allowedGuilds: ["GUILD_ID_HERE"], + messageScan: { + channels: ["CHANNEL_ID_HERE"], + roles: ["ROLE_ID_HERE"], + users: ["USER_ID_HERE"], + whitelist: false, + humanCorrections: { + falsePositiveLabel: "false_positive", + allowUsers: ["USER_ID_HERE"], + memberRequirements: { + permissions: 8n, + roles: ["ROLE_ID_HERE"], + }, + }, + allowedAttachmentMimeTypes: ["image/jpeg", "image/png", "image/webp"], + responses: [ + { + triggers: [/^regexp?$/, { label: "label", threshold: 0.85 }], + response: { + title: "Embed title", + description: "Embed description", + fields: [ + { + name: "Field name", + value: "Field value", + }, + ], + }, + }, + ], + }, + logLevel: "log", + api: { + websocketUrl: "ws://127.0.0.1:3000", + }, +} as Config; +``` + +This may look very overwhelming but configurating it is pretty easy. + +--- + +### `config.owners` + +User IDs of the owners of the bot. They'll be able to execute specific commands that others can't and take control of the bot. + +### `config.allowedGuilds` + +Servers the bot is allowed to be and register commands in. The bot will leave servers that are not in this list automatically once detected. + +### `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. + +The possible levels (sorted by their importance descendingly) are: + +- `none` +- `fatal` +- `error` +- `warn` +- `info` +- `log` +- `debug` + +### `config.api.websocketUrl` + +The WebSocket URL to connect to (including port). + +### `config.messageScan` + +Message scan configuration. + +##### `config.messageScan.roles` & `config.messageScan.users` & `config.messageScan.channels` + +Roles, users, and channels which will be affected by the blacklist/whitelist rule. + +##### `config.messageScan.whitelist` + +Whether to use whitelist (`true`) or blacklist (`false`) mode. + +- Blacklist mode **will refuse** to scan messages of any roles or users who **are** in the list above. +- Whitelist mode **will refuse** to scan messages of any roles or users who **aren't** in the list above. + +##### `config.messageScan.responses` + +An array containing response configurations. A response can be triggered by multiple ways[^1], which can be specified in the `response.triggers` field. +The `response` field contains the embed data that the bot should send. If it is set to `null`, the bot will not send a response or delete the current response if editing. + +```ts +{ + triggers: [ + /cool regex/i, + { + label: 'some_label', + threshold: 0.8, + }, + ], + response: { + title: 'Embed title', + description: 'Embed description', + fields: [ + { + name: 'Field name', + value: 'Field value', + }, + ], + } +} +``` + +[^1]: Possible triggers are regular expressions or [label configurations](../config.example.ts#68). + +## ⏭️ What's next + +The next page will tell you how to run and bundle the bot. + +Continue: [🏃🏻‍♂️ Running the bot](./2_running.md) diff --git a/bots/discord/docs/2_running.md b/bots/discord/docs/2_running.md new file mode 100644 index 0000000..835e8bd --- /dev/null +++ b/bots/discord/docs/2_running.md @@ -0,0 +1,24 @@ +# 🏃🏻‍♂️ 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](./3_commands_and_events.md) diff --git a/bots/discord/docs/3_commands_and_events.md b/bots/discord/docs/3_commands_and_events.md new file mode 100644 index 0000000..da9ac83 --- /dev/null +++ b/bots/discord/docs/3_commands_and_events.md @@ -0,0 +1,110 @@ +# ✨ Adding commands and listening to events + +Adding commands and listening to new events is easy once you learn the project's structure. + +## 🗄️ Project structure + +In the source directory, you'll find multiple other directories: + +- [Commands](#💬-commands) are located in `src/commands` +- [Events](#🚩-events) are located in `src/events` +- [Utility functions](../src/utils) are located in `src/utils` + +You'll also find multiple files: + +- [`index.ts`](../src/index.ts) is the entry of the bot +- [`context.ts`](../src/context.ts) is the context object that will be referenced in multiple places + +## 💬 Commands + +> [!IMPORTANT] +> You are currently developing with the temporary system which isn't very great in terms of development experience. +> A new system will be made and pushed soon and all commands will be migrated to it. + +If you feel the need to categorize commands into directories, you absolutely can, as the system does not restrict subdirectories. + +You can start developing commands with this template: + +```ts +// src/commands/my-command.ts + +import { SlashCommandBuilder } from "discord.js"; +import type { Command } from "."; + +export default { + data: new SlashCommandBuilder() + .setName("my-command") + .setDescription("My cool command") + // Allowing this command to be used in DMs + .setDMPermission(true) + // DO NOT forget this line! + .toJSON(), + + // Member requirements, will only apply to + memberRequirements: { + // Match mode, can be `all` or `any` (`all` by default) + // - All mode means all of the following conditions have to match + // - Any mode means one of the following conditions have to match + mode: "all", + // This will always match in Any mode, which means the member must have one of these roles to pass + roles: ["955220417969262612", "973886585294704640"], + // Permissions required to execute this command + // -1n means bot owners only (default for security reasons) + permissions: -1n, + }, + + // Whether this command should be able to be executed by only bot owners + // (true by default) + ownerOnly: false, + + // Whether to register this command globally + // This is turned off by default for security reasons + global: true, + + // What to do when this command executes + async execute(_context, interaction) { + await interaction.reply({ + content: "Hello!", + }); + }, +} satisfies Command; +``` + +## 🚩 Events + +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: + +```ts +// For Discord events (remove functions you do not use) +import { on, once } 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 +}) +``` + +```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 +}) +``` + +API events are stored in [`src/events/api`](../src/events/api), and Discord events are in [`src/events/discord`](../src/events/discord). + +> [!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). + +## ⏭️ What's next + +The next page will tell you how to create and interact with a database. + +Continue: [🫙 Storing data](./4_databases.md) diff --git a/bots/discord/docs/4_databases.md b/bots/discord/docs/4_databases.md new file mode 100644 index 0000000..d08e974 --- /dev/null +++ b/bots/discord/docs/4_databases.md @@ -0,0 +1,88 @@ +# 🫙 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/docs/README.md b/bots/discord/docs/README.md new file mode 100644 index 0000000..6386ce1 --- /dev/null +++ b/bots/discord/docs/README.md @@ -0,0 +1,17 @@ +# 🤖 ReVanced Discord Bot + +This documentation explains how to start developing, and how to configure the bot. + +## 📖 Table of contents + +0. [🏗️ Set up the development environment (if you haven't already)](../../../docs/0_development_environment.md) +1. [⚙️ Configuration](./1_configuration.md) +2. [🏃🏻‍♂️ Running the server](./2_running.md) +3. [🗣️ Command and events](./3_commands_and_events.md) +4. [🫙 Storing data](./4_databases.md) + +## ⏭️ Start here + +The next page will tell you how to configure the bot. + +Continue: [⚙️ Configuration](./1_configuration.md)