chore: use alternative ways to bundle

This commit is contained in:
PalmDevs
2024-07-04 21:02:01 +07:00
parent ebf1ac7c08
commit 9b6ba56d99
50 changed files with 443 additions and 399 deletions

View File

@@ -1,5 +1,2 @@
# Safety measures, do not remove
IS_USING_DOT_ENV=1
# Your Wit.ai token
WIT_AI_TOKEN="YOUR_TOKEN_HERE"

View File

@@ -1,8 +0,0 @@
{
"$schema": "./config.schema.json",
"address": "127.0.0.1",
"port": 3000,
"ocrConcurrentQueues": 3,
"logLevel": "log"
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"
}
}

View File

@@ -0,0 +1,23 @@
import { createLogger } from '@revanced/bot-shared'
import { cp } from 'fs/promises'
async function build(): Promise<void> {
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')

View File

@@ -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) {

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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<MyDatabase>(
// 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
```

View File

@@ -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"
}
}

View File

@@ -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'))

View File

@@ -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({

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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()

View File

@@ -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<SlashCommandBuilder['toJSON']>
// The function has to return void or Promise<void>
// 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> | 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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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'

View File

@@ -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()

View File

@@ -0,0 +1,56 @@
import type { SlashCommandBuilder } from '@discordjs/builders'
import type { ChatInputCommandInteraction } from 'discord.js'
// Temporary system
export type Command = {
data: ReturnType<SlashCommandBuilder['toJSON']>
// The function has to return void or Promise<void>
// 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> | 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
}

View File

@@ -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<Command>(commands).map((cmd) => [cmd.data.name, cmd])) as Record<string, Command>,
} as const

View File

@@ -0,0 +1,5 @@
// AUTO-GENERATED BY A SCRIPT, DON'T TOUCH
import './ready'
import './disconnect'
import './index'

View File

@@ -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'

View File

@@ -0,0 +1,2 @@
import './discord'
import './api'

View File

@@ -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()

View File

@@ -1,5 +0,0 @@
type IfExtends<T, U, True, False> = T extends U ? True : False
type IfTrue<Condition, True, False> = IfExtends<Condition, true, True, False>
type EmptyObject<K = PropertyKey> = Record<K, never>
type ValuesOf<T> = T[keyof T]
type MaybeArray<T> = T | T[]

View File

@@ -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) => {

View File

@@ -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')}`,
)
}

View File

@@ -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"]
}

BIN
bun.lockb Executable file → Normal file

Binary file not shown.

View File

@@ -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

View File

@@ -6,14 +6,14 @@
"license": "GPL-3.0-or-later",
"type": "module",
"author": "Palm <contact@palmdevs.me> (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 <contact@palmdevs.me> (https://palmdevs.me)",
"ReVanced <nosupport@revanced.app> (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"]

View File

@@ -31,6 +31,7 @@
"ws": "^8.17.1"
},
"devDependencies": {
"@types/ws": "^8.5.10",
"typed-emitter": "^2.1.0"
}
}

View File

@@ -7,5 +7,6 @@
"module": "ESNext",
"composite": true,
"noEmit": false
}
},
"include": ["src/**/*.ts"]
}

View File

@@ -7,5 +7,6 @@
"module": "ESNext",
"composite": true,
"noEmit": false
}
},
"include": ["src/**/*.ts"]
}

View File

@@ -21,5 +21,7 @@
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"allowImportingTsExtensions": false
}
},
"include": ["./**/*"],
"exclude": ["./packages/**/*"]
}

View File

@@ -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"
}
}
}