Compare commits

..

34 Commits

Author SHA1 Message Date
semantic-release-bot
e748a4da92 chore(release): 1.0.0-dev.7 [skip ci]
# @revanced/discord-bot [1.0.0-dev.7](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.6...@revanced/discord-bot@1.0.0-dev.7) (2024-07-25)

### Bug Fixes

* **bot/discord:** start remove preset timeout for `role-preset` command ([cbf9116](cbf91162e2))
* **bots/discord:** only check for member permissions when specified while correcting responses ([b79a1c7](b79a1c7575))
* **bots/discord:** set timeout for eligible mutes to unmute faster ([1f5c5a9](1f5c5a92a6))

### Features

* **bots/discord:** add `replyToReplied` option in response config ([27662ed](27662ed91a))
2024-07-25 18:38:09 +00:00
PalmDevs
300e5cff3b ci(bots/discord): fix freezing after generating db schemas 2024-07-26 01:36:52 +07:00
PalmDevs
6685ffb855 chore: update lockfile 2024-07-26 01:30:33 +07:00
PalmDevs
cbf91162e2 fix(bot/discord): start remove preset timeout for role-preset command 2024-07-26 01:25:52 +07:00
PalmDevs
bd906fbf54 fix(bots/discord)!: remove guilds config in favor of upcoming impl 2024-07-26 01:25:51 +07:00
PalmDevs
27662ed91a feat(bots/discord): add replyToReplied option in response config 2024-07-26 01:25:50 +07:00
PalmDevs
d0acab1915 feat(bots/discord)!: add admin config 2024-07-26 01:25:49 +07:00
PalmDevs
e86180fe29 feat(bots/discord)!: allow message scan response to be message payloads 2024-07-26 01:25:48 +07:00
PalmDevs
1f5c5a92a6 fix(bots/discord): set timeout for eligible mutes to unmute faster 2024-07-26 01:25:47 +07:00
PalmDevs
b79a1c7575 fix(bots/discord): only check for member permissions when specified while correcting responses 2024-07-26 01:25:46 +07:00
PalmDevs
3559ed1cb5 ci(bots/discord): patch drizzle-kit to stop using node, decreases image size 2024-07-26 01:25:45 +07:00
semantic-release-bot
042b155b5e chore(release): 1.0.0-dev.6 [skip ci]
# @revanced/discord-bot [1.0.0-dev.6](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.5...@revanced/discord-bot@1.0.0-dev.6) (2024-07-23)

### Bug Fixes

* **bots/discord:** ci issues causing database to not be auto generated ([673aa18](673aa189be))
2024-07-23 20:59:49 +00:00
PalmDevs
673aa189be fix(bots/discord): ci issues causing database to not be auto generated 2024-07-24 03:57:33 +07:00
PalmDevs
c503a86c53 ci(release): also update bun lockfile to prevent install freezes 2024-07-24 03:36:32 +07:00
semantic-release-bot
1bd973ea6c chore(release): 1.0.0-dev.5 [skip ci]
# @revanced/discord-bot [1.0.0-dev.5](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.4...@revanced/discord-bot@1.0.0-dev.5) (2024-07-23)
2024-07-23 20:31:04 +00:00
semantic-release-bot
4bb965e9ff chore(release): 1.0.0-dev.5 [skip ci]
# @revanced/bot-websocket-api [1.0.0-dev.5](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.4...@revanced/bot-websocket-api@1.0.0-dev.5) (2024-07-23)
2024-07-23 20:30:32 +00:00
PalmDevs
a8ceeb29ae chore: update lockfile 2024-07-24 03:29:32 +07:00
PalmDevs
96a6540434 build(Needs bump): revert building with bun explicitly
Building with only Bun causes compatibility issues, like Drizzle Kit not being to generate any schema for the database of the Discord bot.
2024-07-24 03:25:30 +07:00
PalmDevs
e02c86a9c4 ci(release): add time limit for job 2024-07-24 03:05:54 +07:00
semantic-release-bot
e82f2ab34b chore(release): 1.0.0-dev.4 [skip ci]
# @revanced/discord-bot [1.0.0-dev.4](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.3...@revanced/discord-bot@1.0.0-dev.4) (2024-07-23)

### Bug Fixes

* **bots/discord:** wrong database schema path ([875bd20](875bd209b2))
2024-07-23 20:04:31 +00:00
PalmDevs
2a6f3c3013 chore: update lockfile 2024-07-24 03:03:19 +07:00
PalmDevs
875bd209b2 fix(bots/discord): wrong database schema path 2024-07-24 01:12:30 +07:00
semantic-release-bot
2b601b1a1d chore(release): 1.0.0-dev.3 [skip ci]
# @revanced/discord-bot [1.0.0-dev.3](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.2...@revanced/discord-bot@1.0.0-dev.3) (2024-07-23)

### Bug Fixes

* **bots/discord:** revert dist denesting, fixes config not found ([0d4898d](0d4898dae8))
2024-07-23 15:42:43 +00:00
semantic-release-bot
164570d176 chore(release): 1.0.0-dev.4 [skip ci]
# @revanced/bot-websocket-api [1.0.0-dev.4](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.3...@revanced/bot-websocket-api@1.0.0-dev.4) (2024-07-23)

### Bug Fixes

* **apis/websocket:** hardcoded paths in tesseract worker builds ([38e00eb](38e00eb4e5))
2024-07-23 15:42:10 +00:00
PalmDevs
0d4898dae8 fix(bots/discord): revert dist denesting, fixes config not found 2024-07-23 22:41:15 +07:00
PalmDevs
38e00eb4e5 fix(apis/websocket): hardcoded paths in tesseract worker builds 2024-07-23 22:38:23 +07:00
semantic-release-bot
3117af5497 chore(release): 1.0.0-dev.2 [skip ci]
# @revanced/discord-bot [1.0.0-dev.2](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.1...@revanced/discord-bot@1.0.0-dev.2) (2024-07-23)

### Features

* **bots/discord:** don't nest builds in src directory, autogen db when missing ([4834685](4834685186))
2024-07-23 14:16:41 +00:00
PalmDevs
6a87464b40 chore(bots/discord): fix build script oversight 2024-07-23 21:15:07 +07:00
semantic-release-bot
5db076aee5 chore(release): 1.0.0-dev.3 [skip ci]
# @revanced/bot-websocket-api [1.0.0-dev.3](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.2...@revanced/bot-websocket-api@1.0.0-dev.3) (2024-07-23)

### Bug Fixes

* **apis/websocket:** build and runtime issues ([89d8ab1](89d8ab1ee5))
2024-07-23 14:05:57 +00:00
PalmDevs
12f5aaf70f chore: update dependencies 2024-07-23 21:01:05 +07:00
PalmDevs
94d0dcc32b fix(packages/api): misleading errors being thrown 2024-07-23 21:01:04 +07:00
PalmDevs
4834685186 feat(bots/discord): don't nest builds in src directory, autogen db when missing 2024-07-23 21:01:04 +07:00
PalmDevs
ffe0a3ddf5 chore(bots/discord): change config to a js file 2024-07-23 21:01:03 +07:00
PalmDevs
89d8ab1ee5 fix(apis/websocket): build and runtime issues 2024-07-23 21:01:02 +07:00
37 changed files with 368 additions and 191 deletions

View File

@@ -14,6 +14,7 @@ jobs:
permissions:
contents: read
packages: write
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4

View File

@@ -1,3 +1,19 @@
# @revanced/bot-websocket-api [1.0.0-dev.5](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.4...@revanced/bot-websocket-api@1.0.0-dev.5) (2024-07-23)
# @revanced/bot-websocket-api [1.0.0-dev.4](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.3...@revanced/bot-websocket-api@1.0.0-dev.4) (2024-07-23)
### Bug Fixes
* **apis/websocket:** hardcoded paths in tesseract worker builds ([38e00eb](https://github.com/revanced/revanced-helper/commit/38e00eb4e59c763bd74d27b9b9b482ea66e4dcf4))
# @revanced/bot-websocket-api [1.0.0-dev.3](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.2...@revanced/bot-websocket-api@1.0.0-dev.3) (2024-07-23)
### Bug Fixes
* **apis/websocket:** build and runtime issues ([89d8ab1](https://github.com/revanced/revanced-helper/commit/89d8ab1ee58278a9a96cdc31c679d0a0a0d865af))
# @revanced/bot-websocket-api [1.0.0-dev.2](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.1...@revanced/bot-websocket-api@1.0.0-dev.2) (2024-07-22)

View File

@@ -6,7 +6,7 @@ FROM base AS build
WORKDIR /build
COPY . .
RUN bun install --frozen-lockfile
RUN cd apis/websocket && bun --bun run build
RUN cd apis/websocket && bun run build
FROM base AS release

View File

@@ -2,7 +2,7 @@
"name": "@revanced/bot-websocket-api",
"type": "module",
"private": true,
"version": "1.0.0-dev.2",
"version": "1.0.0-dev.5",
"description": "🧦 WebSocket API server for bots assisting ReVanced",
"main": "dist/index.js",
"scripts": {

View File

@@ -1,29 +1,34 @@
import { createLogger } from '@revanced/bot-shared'
import { cp, rm } from 'fs/promises'
async function build(): Promise<void> {
const logger = createLogger()
const logger = createLogger()
logger.info('Cleaning previous build...')
await rm('./dist', { recursive: true })
logger.info('Cleaning previous build...')
await rm('./dist', { recursive: true })
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',
minify: true,
sourcemap: 'external',
})
logger.info('Copying Tesseract.js WASM...')
await cp('../../node_modules/tesseract.js-core', './dist/worker/core', { recursive: true })
logger.info('Building Tesseract.js worker...')
await Bun.build({
entrypoints: ['../../node_modules/tesseract.js/src/worker-script/node/index.js'],
external: ['tesseract.js-core/*'],
target: 'bun',
outdir: './dist/worker',
minify: true,
sourcemap: 'external',
})
logger.info('Building WebSocket API...')
await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
target: 'bun',
})
}
// Tesseract.js is really bad for minification
// It forcefully requires this core module to be present which contains the WASM files
logger.info('Copying Tesseract.js Core...')
await cp('../../node_modules/tesseract.js-core', './dist/node_modules/tesseract.js-core', { recursive: true })
await build()
logger.info('Copying config...')
await cp('config.json', 'dist/config.json')

View File

@@ -55,12 +55,9 @@ export interface WitMessageResponse {
const TesseractWorkerDirPath = joinPath(import.meta.dir, 'worker')
const TesseractWorkerPath = joinPath(TesseractWorkerDirPath, 'index.js')
const TesseractCorePath = joinPath(TesseractWorkerDirPath, 'core')
export const tesseract = await createTesseractWorker(
'eng',
OEM.DEFAULT,
(await pathExists(TesseractWorkerDirPath))
? { workerPath: TesseractWorkerPath, corePath: TesseractCorePath }
: undefined,
(await pathExists(TesseractWorkerDirPath)) ? { workerPath: TesseractWorkerPath } : undefined,
)

View File

@@ -181,6 +181,7 @@ config.ts
*.db
*.sqlite
*.sqlite3
.drizzle
# Auto-generated files
src/commands/index.ts

View File

@@ -1,3 +1,47 @@
# @revanced/discord-bot [1.0.0-dev.7](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.6...@revanced/discord-bot@1.0.0-dev.7) (2024-07-25)
### Bug Fixes
* **bot/discord:** start remove preset timeout for `role-preset` command ([cbf9116](https://github.com/revanced/revanced-helper/commit/cbf91162e27dd4c1ecb976927ab708f1d882abca))
* **bots/discord:** only check for member permissions when specified while correcting responses ([b79a1c7](https://github.com/revanced/revanced-helper/commit/b79a1c7575e94c3e62654c87775cac497be4a50a))
* **bots/discord:** set timeout for eligible mutes to unmute faster ([1f5c5a9](https://github.com/revanced/revanced-helper/commit/1f5c5a92a639973b83a1204355538936e69a4454))
### Features
* **bots/discord:** add `replyToReplied` option in response config ([27662ed](https://github.com/revanced/revanced-helper/commit/27662ed91a79bfac7d3f091834e859a7b57366ce))
# @revanced/discord-bot [1.0.0-dev.6](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.5...@revanced/discord-bot@1.0.0-dev.6) (2024-07-23)
### Bug Fixes
* **bots/discord:** ci issues causing database to not be auto generated ([673aa18](https://github.com/revanced/revanced-helper/commit/673aa189bef1009a3e32ba3b1291a5ee84f2def3))
# @revanced/discord-bot [1.0.0-dev.5](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.4...@revanced/discord-bot@1.0.0-dev.5) (2024-07-23)
# @revanced/discord-bot [1.0.0-dev.4](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.3...@revanced/discord-bot@1.0.0-dev.4) (2024-07-23)
### Bug Fixes
* **bots/discord:** wrong database schema path ([875bd20](https://github.com/revanced/revanced-helper/commit/875bd209b252566414bf89349839cabc01697e1c))
# @revanced/discord-bot [1.0.0-dev.3](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.2...@revanced/discord-bot@1.0.0-dev.3) (2024-07-23)
### Bug Fixes
* **bots/discord:** revert dist denesting, fixes config not found ([0d4898d](https://github.com/revanced/revanced-helper/commit/0d4898dae8b26f8466d3f6b8f62875866f581644))
# @revanced/discord-bot [1.0.0-dev.2](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.1...@revanced/discord-bot@1.0.0-dev.2) (2024-07-23)
### Features
* **bots/discord:** don't nest builds in src directory, autogen db when missing ([4834685](https://github.com/revanced/revanced-helper/commit/48346851864c4d4b6276388644dd24ce16222b3e))
# @revanced/discord-bot 1.0.0-dev.1 (2024-07-22)

View File

@@ -6,7 +6,7 @@ FROM base AS build
WORKDIR /build
COPY . .
RUN bun install --frozen-lockfile
RUN cd bots/discord && bun --bun run build
RUN cd bots/discord && bun run build
FROM base AS release

View File

@@ -1,8 +1,15 @@
import type { Config } from './config.schema'
// @ts-check
/**
* @type {import('./config.schema').Config}
*/
export default {
owners: ['USER_ID_HERE'],
guilds: ['GUILD_ID_HERE'],
admin: {
users: ['USER_ID_HERE'],
roles: {
GUILD_ID_HERE: ['ROLE_ID_HERE'],
},
},
moderation: {
cure: {
defaultName: 'Server member',
@@ -52,12 +59,16 @@ export default {
text: [/^regexp?$/, { label: 'label', threshold: 0.85 }],
},
response: {
title: 'Embed title',
description: 'Embed description',
fields: [
embeds: [
{
name: 'Field name',
value: 'Field value',
title: 'Embed title',
description: 'Embed description',
fields: [
{
name: 'Field name',
value: 'Field value',
},
],
},
],
},
@@ -70,4 +81,4 @@ export default {
disconnectLimit: 3,
disconnectRetryInterval: 10000,
},
} satisfies Config as Config
}

View File

@@ -1,8 +1,10 @@
import type { APIEmbed } from 'discord.js'
import type { BaseMessageOptions } from 'discord.js'
export type Config = {
owners: string[]
guilds: string[]
admin?: {
users?: string[]
roles?: Record<string, string[]>
}
moderation?: {
roles: string[]
cure?: {
@@ -57,6 +59,7 @@ export type ConfigMessageScanResponse = {
}
filterOverride?: NonNullable<Config['messageScan']>['filter']
response: ConfigMessageScanResponseMessage | null
replyToReplied?: boolean
}
export type ConfigMessageScanResponseLabelConfig = {
@@ -70,4 +73,4 @@ export type ConfigMessageScanResponseLabelConfig = {
threshold: number
}
export type ConfigMessageScanResponseMessage = APIEmbed
export type ConfigMessageScanResponseMessage = BaseMessageOptions

View File

@@ -29,9 +29,8 @@ The distribution files will be placed inside the `dist` directory. Inside will i
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
1. [Build the bot as seen in the previous step](#-building)
2. 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.**
@@ -40,7 +39,7 @@ To deploy the bot, you'll need to:
bun run scripts/reload-slash-commands.ts
```
4. Copy contents of the `dist` directory
3. Copy contents of the `dist` directory
```sh
# For instance, we'll copy them both to /usr/src/discord-bot
@@ -48,18 +47,10 @@ To deploy the bot, you'll need to:
cp -R ./dist/* /usr/src/discord-bot
```
5. Copy the empty database (or use your own existing database)
```sh
# By default, the build script creates the database called "db.sqlite3"
# Unless you specify otherwise via the "DATABASE_PATH" environment variable
cp ./db.sqlite3 /usr/src/discord-bot
```
6. Configure environment variables
4. 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
5. Finally, run the bot
```sh
cd /usr/src/discord-bot

View File

@@ -3,6 +3,7 @@ import { defineConfig } from 'drizzle-kit'
export default defineConfig({
dialect: 'sqlite',
schema: './src/database/schemas.ts',
out: './.drizzle',
dbCredentials: {
url: process.env['DATABASE_PATH'] ? `file:./${process.env['DATABASE_PATH']}` : 'file:./db.sqlite3',
},

View File

@@ -2,17 +2,16 @@
"name": "@revanced/discord-bot",
"type": "module",
"private": true,
"version": "1.0.0-dev.1",
"version": "1.0.0-dev.7",
"description": "🤖 Discord bot assisting ReVanced",
"main": "src/index.ts",
"scripts": {
"register": "bun run scripts/reload-slash-commands.ts",
"start": "bun prepare && bun run src/index.ts",
"dev": "bun prepare && 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",
"build": "bun prepare && bun run scripts/build.ts",
"watch": "bun dev",
"prepare": "bun run scripts/generate-indexes.ts && drizzle-kit push"
"prepare": "bun run scripts/generate-indexes.ts && bunx drizzle-kit generate --name=schema"
},
"repository": {
"type": "git",

View File

@@ -0,0 +1,23 @@
import { createLogger } from '@revanced/bot-shared'
import { cp, rename, rm } from 'fs/promises'
const logger = createLogger()
logger.warn('Cleaning previous build...')
await rm('./dist', { recursive: true })
logger.info('Building bot...')
await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist/src',
target: 'bun',
external: ['./config.js'],
minify: true,
sourcemap: 'external',
})
logger.info('Copying config...')
await cp('config.js', 'dist/config.js')
logger.info('Copying database schema...')
await rename('.drizzle', 'dist/.drizzle')

View File

@@ -1,50 +1,53 @@
import { REST } from '@discordjs/rest'
import { getMissingEnvironmentVariables } from '@revanced/bot-shared'
import { Routes } from 'discord-api-types/v9'
import type {
RESTGetCurrentApplicationResult,
RESTPutAPIApplicationCommandsResult,
RESTPutAPIApplicationGuildCommandsResult,
} from 'discord.js'
import { config, discord, logger } from '../src/context'
console.log('Deprecated. New implementation to be done.')
process.exit(1)
// Check if token exists
// import { REST } from '@discordjs/rest'
// import { getMissingEnvironmentVariables } from '@revanced/bot-shared'
// import { Routes } from 'discord-api-types/v9'
// import type {
// RESTGetCurrentApplicationResult,
// RESTPutAPIApplicationCommandsResult,
// RESTPutAPIApplicationGuildCommandsResult,
// } from 'discord.js'
// import { config, discord, logger } from '../src/context'
const missingEnvs = getMissingEnvironmentVariables(['DISCORD_TOKEN'])
if (missingEnvs.length) {
for (const env of missingEnvs) logger.fatal(`${env} is not defined in environment variables`)
process.exit(1)
}
// // Check if token exists
// Group commands by global and guild
// const missingEnvs = getMissingEnvironmentVariables(['DISCORD_TOKEN'])
// if (missingEnvs.length) {
// for (const env of missingEnvs) logger.fatal(`${env} is not defined in environment variables`)
// process.exit(1)
// }
const { global: globalCommands = [], guild: guildCommands = [] } = Object.groupBy(Object.values(discord.commands), c =>
c.global ? 'global' : 'guild',
)
// // Group commands by global and guild
// Set commands
// const { global: globalCommands = [], guild: guildCommands = [] } = Object.groupBy(Object.values(discord.commands), c =>
// c.global ? 'global' : 'guild',
// )
const rest = new REST({ version: '10' }).setToken(process.env['DISCORD_TOKEN']!)
// // Set commands
try {
const app = (await rest.get(Routes.currentApplication())) as RESTGetCurrentApplicationResult
const data = (await rest.put(Routes.applicationCommands(app.id), {
body: globalCommands.map(({ data }) => {
if (!data.dm_permission) data.dm_permission = true
logger.warn(`Command ${data.name} has no dm_permission set, forcing to true as it is a global command`)
return data
}),
})) as RESTPutAPIApplicationCommandsResult
// const rest = new REST({ version: '10' }).setToken(process.env['DISCORD_TOKEN']!)
logger.info(`Reloaded ${data.length} global commands`)
// try {
// const app = (await rest.get(Routes.currentApplication())) as RESTGetCurrentApplicationResult
// const data = (await rest.put(Routes.applicationCommands(app.id), {
// body: globalCommands.map(({ data }) => {
// if (!data.dm_permission) data.dm_permission = true
// logger.warn(`Command ${data.name} has no dm_permission set, forcing to true as it is a global command`)
// return data
// }),
// })) as RESTPutAPIApplicationCommandsResult
for (const guildId of config.guilds) {
const data = (await rest.put(Routes.applicationGuildCommands(app.id, guildId), {
body: guildCommands.map(x => x.data),
})) as RESTPutAPIApplicationGuildCommandsResult
// logger.info(`Reloaded ${data.length} global commands`)
logger.info(`Reloaded ${data.length} guild commands for guild ${guildId}`)
}
} catch (e) {
logger.fatal(e)
}
// for (const guildId of config.guilds) {
// const data = (await rest.put(Routes.applicationGuildCommands(app.id, guildId), {
// body: guildCommands.map(x => x.data),
// })) as RESTPutAPIApplicationGuildCommandsResult
// logger.info(`Reloaded ${data.length} guild commands for guild ${guildId}`)
// }
// } catch (e) {
// logger.fatal(e)
// }

View File

@@ -12,7 +12,7 @@ export default {
.setDMPermission(true)
.toJSON(),
ownerOnly: true,
adminOnly: true,
global: true,
async execute(_, interaction) {

View File

@@ -25,7 +25,7 @@ export default {
.setDMPermission(true)
.toJSON(),
ownerOnly: true,
adminOnly: true,
global: true,
async execute(_, interaction) {

View File

@@ -11,7 +11,7 @@ export default {
.setDMPermission(true)
.toJSON(),
ownerOnly: true,
adminOnly: true,
global: true,
async execute({ api, logger }, interaction) {

View File

@@ -1,7 +1,7 @@
import { SlashCommandBuilder } from 'discord.js'
import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { applyRolePreset } from '$/utils/discord/rolePresets'
import { applyRolePreset, removeRolePreset } from '$/utils/discord/rolePresets'
import type { Command } from '../types'
import { config } from '$/context'
@@ -24,7 +24,7 @@ export default {
global: false,
async execute({ logger }, interaction, { userIsOwner }) {
async execute({ logger }, interaction, { isExecutorBotAdmin: isExecutorAdmin }) {
const user = interaction.options.getUser('member', true)
const reason = interaction.options.getString('reason') ?? 'No reason provided'
const duration = interaction.options.getString('duration')
@@ -48,7 +48,7 @@ export default {
if (!member.manageable)
throw new CommandError(CommandErrorType.Generic, 'This user cannot be managed by the bot.')
if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0 && !userIsOwner)
if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0 && !isExecutorAdmin)
throw new CommandError(
CommandErrorType.InvalidUser,
'You cannot mute a user with a role equal to or higher than yours.',
@@ -60,6 +60,11 @@ export default {
createModerationActionEmbed('Muted', user, interaction.user, reason, durationMs),
)
if (durationMs)
setTimeout(() => {
removeRolePreset(member, 'mute')
}, durationMs)
logger.info(
`Moderator ${interaction.user.tag} (${interaction.user.id}) muted ${user.tag} (${user.id}) until ${expires} because ${reason}`,
)

View File

@@ -35,7 +35,7 @@ export default {
global: false,
async execute({ logger }, interaction, { userIsOwner }) {
async execute({ logger }, interaction, { isExecutorBotAdmin: isExecutorAdmin }) {
const action = interaction.options.getString('action', true) as 'apply' | 'remove'
const user = interaction.options.getUser('member', true)
const preset = interaction.options.getString('preset', true)
@@ -61,7 +61,7 @@ export default {
'The duration must be at least 1 millisecond long.',
)
if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0 && !userIsOwner)
if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0 && !isExecutorAdmin)
throw new CommandError(
CommandErrorType.InvalidUser,
'You cannot apply a role preset to a user with a role equal to or higher than yours.',
@@ -79,6 +79,11 @@ export default {
)
}
if (expires)
setTimeout(() => {
removeRolePreset(member, preset)
}, expires)
await sendPresetReplyAndLogs(action, interaction, user, preset, expires)
},
} satisfies Command

View File

@@ -39,10 +39,10 @@ export type Command = {
roles?: string[]
}
/**
* Whether this command can only be used by bot owners.
* Whether this command can only be used by bot admins.
* @default false
*/
ownerOnly?: boolean
adminOnly?: 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.
@@ -52,5 +52,5 @@ export type Command = {
}
export interface Info {
userIsOwner: boolean
isExecutorBotAdmin: boolean
}

View File

@@ -1,4 +1,6 @@
import { Database } from 'bun:sqlite'
import { existsSync, readFileSync, readdirSync } from 'fs'
import { join } from 'path'
import { Client as APIClient } from '@revanced/bot-api'
import { createLogger } from '@revanced/bot-shared'
import { ActivityType, Client as DiscordClient, Partials } from 'discord.js'
@@ -29,7 +31,33 @@ export const api = {
disconnectCount: 0,
}
const db = new Database(process.env['DATABASE_PATH'])
const DatabasePath = process.env['DATABASE_PATH']
const DatabaseSchemaDir = join(import.meta.dir, '..', '.drizzle')
let dbSchemaFileName: string | undefined
if (DatabasePath && !existsSync(DatabasePath)) {
logger.warn('Database file not found, trying to create from schema...')
try {
const file = readdirSync(DatabaseSchemaDir, { withFileTypes: true })
.filter(file => file.isFile() && file.name.endsWith('.sql'))
.sort()
.at(-1)
if (!file) throw new Error('No schema file found')
dbSchemaFileName = file.name
logger.debug(`Using schema file: ${dbSchemaFileName}`)
} catch (e) {
logger.fatal('Could not create database from schema, check if the schema file exists and is accessible')
logger.fatal(e)
process.exit(1)
}
}
const db = new Database(DatabasePath)
if (dbSchemaFileName) db.run(readFileSync(join(DatabaseSchemaDir, dbSchemaFileName)).toString())
export const database = drizzle(db, {
schema: schemas,

View File

@@ -1,4 +1,5 @@
import CommandError from '$/classes/CommandError'
import { isAdmin } from '$/utils/discord/permissions'
import { createErrorEmbed, createStackTraceEmbed } from '$utils/discord/embeds'
import { on, withContext } from '$utils/discord/events'
@@ -11,14 +12,14 @@ withContext(on, 'interactionCreate', async (context, interaction) => {
logger.debug(`Command ${interaction.commandName} being invoked by ${interaction.user.tag}`)
if (!command) return void logger.error(`Command ${interaction.commandName} not implemented but registered!!!`)
const isOwner = config.owners.includes(interaction.user.id)
const isExecutorBotAdmin = isAdmin(await interaction.guild?.members.fetch(interaction.user.id) || interaction.user, config.admin)
/**
* Owner check
* Admin check
*/
if (command.ownerOnly && !isOwner)
if (command.adminOnly && !isExecutorBotAdmin)
return void (await interaction.reply({
embeds: [createErrorEmbed('Massive skill issue', 'This command can only be used by the bot owners.')],
embeds: [createErrorEmbed('Massive skill issue', 'This command can only be used by the bot admins.')],
ephemeral: true,
}))
@@ -39,7 +40,7 @@ withContext(on, 'interactionCreate', async (context, interaction) => {
*/
if (interaction.inGuild()) {
// Bot owners get bypass
if (command.memberRequirements && !isOwner) {
if (command.memberRequirements && !isExecutorBotAdmin) {
const { permissions = 0n, roles = [], mode } = command.memberRequirements
const member = await interaction.guild!.members.fetch(interaction.user.id)
@@ -69,7 +70,7 @@ withContext(on, 'interactionCreate', async (context, interaction) => {
try {
logger.debug(`Command ${interaction.commandName} being executed`)
await command.execute(context, interaction, { userIsOwner: isOwner })
await command.execute(context, interaction, { isExecutorBotAdmin })
} catch (err) {
logger.error(`Error while executing command ${interaction.commandName}:`, err)
await interaction[interaction.replied ? 'followUp' : 'reply']({

View File

@@ -21,13 +21,15 @@ withContext(on, 'messageCreate', async (context, msg) => {
try {
logger.debug(`Classifying message ${msg.id}`)
const { response, label } = await getResponseFromText(msg.content, filteredResponses, context)
const { response, label, replyToReplied } = await getResponseFromText(msg.content, filteredResponses, context)
if (response) {
logger.debug('Response found')
const reply = await msg.reply({
embeds: [createMessageScanResponseEmbed(response, label ? 'nlp' : 'match')],
const toReply = replyToReplied ? await msg.fetchReference() : msg
const reply = await toReply.reply({
...response,
embeds: response.embeds?.map(it => createMessageScanResponseEmbed(it, label ? 'nlp' : 'match')),
})
if (label)
@@ -64,7 +66,8 @@ withContext(on, 'messageCreate', async (context, msg) => {
if (response) {
logger.debug(`Response found for attachment: ${attachment.url}`)
await msg.reply({
embeds: [createMessageScanResponseEmbed(response, 'ocr')],
...response,
embeds: response.embeds?.map(it => createMessageScanResponseEmbed(it, 'ocr')),
})
break

View File

@@ -14,6 +14,7 @@ import type { ConfigMessageScanResponseLabelConfig } from '$/../config.schema'
import { responses } from '$/database/schemas'
import { handleUserResponseCorrection } from '$/utils/discord/messageScan'
import { eq } from 'drizzle-orm'
import { isAdmin } from '$/utils/discord/permissions'
const PossibleReactions = Object.values(Reactions) as string[]
@@ -32,7 +33,7 @@ withContext(on, 'messageReactionAdd', async (context, rct, user) => {
if (reactionMessage.author.id !== reaction.client.user!.id) return
if (!PossibleReactions.includes(reaction.emoji.name!)) return
if (!config.owners.includes(user.id)) {
if (!isAdmin(reactionMessage.member || reactionMessage.author, config.admin)) {
// User is in guild, and config has member requirements
if (
reactionMessage.inGuild() &&
@@ -46,7 +47,12 @@ withContext(on, 'messageReactionAdd', async (context, rct, user) => {
const member = await reactionMessage.guild.members.fetch(user.id)
const { permissions, roles } = allowedMembers
if (!(member.permissions.has(permissions ?? 0n) || roles?.some(role => member.roles.cache.has(role))))
if (
!(
(permissions ? member.permissions.has(permissions) : false) ||
roles?.some(role => member.roles.cache.has(role))
)
)
return
} else if (allowedUsers) {
if (!allowedUsers.includes(user.id)) return

View File

@@ -1,19 +0,0 @@
import type { Command } from '$commands/types'
import { listAllFilesRecursive } from '$utils/fs'
export const loadCommands = async (dir: string) => {
const commandsMap: Record<string, Command> = {}
const files = listAllFilesRecursive(dir)
const commands = await Promise.all(
files.map(async file => {
const command = await import(file)
return command.default
}),
)
for (const command of commands) {
if (command) commandsMap[command.data.name] = command
}
return commandsMap
}

View File

@@ -24,16 +24,19 @@ export const createSuccessEmbed = (title: string | null, description?: string) =
)
export const createMessageScanResponseEmbed = (
response: ConfigMessageScanResponseMessage,
response: NonNullable<ConfigMessageScanResponseMessage['embeds']>[number],
mode: 'ocr' | 'nlp' | 'match',
) => {
// biome-ignore lint/style/noParameterAssign: While this is confusing, it is fine for this purpose
if ('toJSON' in response) response = response.toJSON()
const embed = new EmbedBuilder().setTitle(response.title ?? null)
if (response.description) embed.setDescription(response.description)
if (response.fields) embed.addFields(response.fields)
embed.setFooter({
text: `ReVanced • Done via ${MessageScanHumanizedMode[mode]}`,
text: `ReVanced • Via ${MessageScanHumanizedMode[mode]}`,
iconURL: ReVancedLogoURL,
})

View File

@@ -2,8 +2,7 @@ import { type Response, responses } from '$/database/schemas'
import type {
Config,
ConfigMessageScanResponse,
ConfigMessageScanResponseLabelConfig,
ConfigMessageScanResponseMessage,
ConfigMessageScanResponseLabelConfig
} from 'config.schema'
import type { Message, PartialUser, User } from 'discord.js'
import { eq } from 'drizzle-orm'
@@ -15,9 +14,12 @@ export const getResponseFromText = async (
// Just to be safe that we will never use data from the context parameter
{ api, logger }: Omit<typeof import('src/context'), 'config'>,
ocrMode = false,
) => {
let label: string | undefined
let response: ConfigMessageScanResponseMessage | undefined | null
): Promise<ConfigMessageScanResponse & { label?: string }> => {
let responseConfig: Awaited<ReturnType<typeof getResponseFromText>> = {
triggers: {},
response: null
}
const firstLabelIndexes: number[] = []
// Test if all regexes before a label trigger is matched
@@ -25,29 +27,28 @@ export const getResponseFromText = async (
const trigger = responses[i]!
// Filter override check is not neccessary here, we are already passing responses that match the filter
// from the messageCreate handler
// from the messageCreate handler, see line 17 of messageCreate handler
const {
triggers: { text: textTriggers, image: imageTriggers },
response: resp,
triggers: { text: textTriggers, image: imageTriggers }
} = trigger
if (response) break
if (responseConfig) break
if (ocrMode) {
if (imageTriggers)
for (const regex of imageTriggers)
if (regex.test(content)) {
logger.debug(`Message matched regex (OCR mode): ${regex.source}`)
response = resp
responseConfig = trigger
break
}
} else
for (let j = 0; j < textTriggers!.length; j++) {
const trigger = textTriggers![j]!
const regex = textTriggers![j]!
if (trigger instanceof RegExp) {
if (trigger.test(content)) {
logger.debug(`Message matched regex (before mode): ${trigger.source}`)
response = resp
if (regex instanceof RegExp) {
if (regex.test(content)) {
logger.debug(`Message matched regex (before mode): ${regex.source}`)
responseConfig = trigger
break
}
} else {
@@ -58,7 +59,7 @@ export const getResponseFromText = async (
}
// If none of the regexes match, we can search for labels immediately
if (!response && !ocrMode) {
if (!responseConfig && !ocrMode) {
logger.debug('No match from before regexes, doing NLP')
const scan = await api.client.parseText(content)
if (scan.labels.length) {
@@ -76,24 +77,22 @@ export const getResponseFromText = async (
if (!labelConfig) {
logger.warn(`No label config found for label ${matchedLabel.name}`)
return { response: null, label: undefined }
return responseConfig
}
if (matchedLabel.confidence >= triggerConfig!.threshold) {
logger.debug('Label confidence is enough')
label = matchedLabel.name
response = labelConfig.response
responseConfig = labelConfig
}
}
}
// If we still don't have a label, we can match all regexes after the initial label trigger
if (!response) {
// If we still don't have a response config, we can match all regexes after the initial label trigger
if (!responseConfig) {
logger.debug('No match from NLP, doing after regexes')
for (let i = 0; i < responses.length; i++) {
const {
triggers: { text: textTriggers },
response: resp,
triggers: { text: textTriggers }
} = responses[i]!
const firstLabelIndex = firstLabelIndexes[i] ?? -1
@@ -103,7 +102,7 @@ export const getResponseFromText = async (
if (trigger instanceof RegExp) {
if (trigger.test(content)) {
logger.debug(`Message matched regex (after mode): ${trigger.source}`)
response = resp
responseConfig = responses[i]!
break
}
}
@@ -111,10 +110,7 @@ export const getResponseFromText = async (
}
}
return {
response,
label,
}
return responseConfig
}
export const shouldScanMessage = (
@@ -158,8 +154,10 @@ export const handleUserResponseCorrection = async (
correctedById: user.id,
})
.where(eq(responses.replyId, response.replyId))
await reply.edit({
embeds: [createMessageScanResponseEmbed(correctLabelResponse.response, 'nlp')],
...correctLabelResponse.response,
embeds: correctLabelResponse.response.embeds?.map(it => createMessageScanResponseEmbed(it, 'nlp')),
})
}

View File

@@ -0,0 +1,11 @@
import { GuildMember, type User } from 'discord.js'
import type { Config } from 'config.schema'
export const isAdmin = (userOrMember: User | GuildMember, adminConfig: Config['admin']) => {
return adminConfig?.users?.includes(userOrMember.id) || (userOrMember instanceof GuildMember && isMemberAdmin(userOrMember, adminConfig))
}
export const isMemberAdmin = (member: GuildMember, adminConfig: Config['admin']) => {
const roles = new Set(member.roles.cache.keys())
return Boolean(adminConfig?.roles?.[member.guild.id]?.some(role => roles.has(role)))
}

View File

@@ -9,6 +9,7 @@
"composite": false,
"exactOptionalPropertyTypes": false,
"esModuleInterop": true,
"allowJs": true,
"paths": {
"$/*": ["./src/*"],
"$constants": ["./src/constants"],

BIN
bun.lockb

Binary file not shown.

View File

@@ -41,11 +41,11 @@
"@tsconfig/strictest": "^2.0.5",
"@types/bun": "^1.1.6",
"conventional-changelog-conventionalcommits": "^7.0.2",
"lefthook": "^1.7.4",
"lefthook": "^1.7.5",
"portainer-service-webhook": "https://github.com/newarifrh/portainer-service-webhook#v1",
"semantic-release": "^24.0.0",
"turbo": "^2.0.9",
"typescript": "^5.5.3"
"typescript": "^5.5.4"
},
"trustedDependencies": [
"@biomejs/biome",
@@ -54,6 +54,7 @@
"lefthook"
],
"patchedDependencies": {
"@semantic-release/npm@12.0.1": "patches/@semantic-release%2Fnpm@12.0.1.patch"
"@semantic-release/npm@12.0.1": "patches/@semantic-release%2Fnpm@12.0.1.patch",
"drizzle-kit@0.22.8": "patches/drizzle-kit@0.22.8.patch"
}
}

View File

@@ -53,23 +53,34 @@ export class ClientWebSocketManager {
}
}, this.timeout)
this.#socket.on('open', () => {
this.disconnected = false
clearTimeout(timeout)
this.#listen()
rs()
})
this.#socket.on('error', err => {
clearTimeout(timeout)
const errorBeforeReadyHandler = (err: Error) => {
cleanup()
throw err
})
}
this.#socket.on('close', (code, reason) => {
const closeBeforeReadyHandler = (code: number, reason: Buffer) => {
clearTimeout(timeout)
this._handleDisconnect(code, reason.toString())
throw new Error('WebSocket connection closed before ready')
})
}
const readyHandler = () => {
this.disconnected = false
cleanup()
this.#listen()
rs()
}
const cleanup = () => {
this.#socket.off('open', readyHandler)
this.#socket.off('close', closeBeforeReadyHandler)
this.#socket.off('error', errorBeforeReadyHandler)
clearTimeout(timeout)
}
this.#socket.on('open', readyHandler)
this.#socket.on('error', errorBeforeReadyHandler)
this.#socket.on('close', closeBeforeReadyHandler)
} catch (e) {
rj(e)
}

View File

@@ -1,8 +1,14 @@
diff --git a/node_modules/@semantic-release/npm/.bun-tag-3853154e196b7721 b/.bun-tag-3853154e196b7721
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/node_modules/@semantic-release/npm/.bun-tag-550461f23a8ec245 b/.bun-tag-550461f23a8ec245
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/node_modules/@semantic-release/npm/.bun-tag-c9c8130945517add b/.bun-tag-c9c8130945517add
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/lib/prepare.js b/lib/prepare.js
index 3e76bec44cf595a1b4141728336bed904d4d518d..c6baf4e8de9bdf7536f9ad2e9eb9c360e031c7b5 100644
index 3e76bec44cf595a1b4141728336bed904d4d518d..4b25ca64879bbee2a600f2b23b738c86136ad9c6 100644
--- a/lib/prepare.js
+++ b/lib/prepare.js
@@ -1,6 +1,7 @@
@@ -14,7 +20,7 @@ index 3e76bec44cf595a1b4141728336bed904d4d518d..c6baf4e8de9bdf7536f9ad2e9eb9c360
export default async function (
npmrc,
@@ -11,19 +12,12 @@ export default async function (
@@ -11,19 +12,13 @@ export default async function (
logger.log("Write version %s to package.json in %s", version, basePath);
@@ -36,10 +42,11 @@ index 3e76bec44cf595a1b4141728336bed904d4d518d..c6baf4e8de9bdf7536f9ad2e9eb9c360
- await versionResult;
+ await writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, detectIndent(pkgJsonRaw).indent))
+ await execa("bun", ["install"]);
if (tarballDir) {
logger.log("Creating npm package version %s", version);
@@ -38,7 +32,7 @@ export default async function (
@@ -38,7 +33,7 @@ export default async function (
// Only move the tarball if we need to
// Fixes: https://github.com/semantic-release/npm/issues/169
if (tarballSource !== tarballDestination) {

View File

@@ -0,0 +1,21 @@
diff --git a/bin.cjs b/bin.cjs
index 142ed9c20f28dc1080bebfb52325fa308c6cb771..9d3bea0787f6c05df11567c6821bc85743286340 100644
--- a/bin.cjs
+++ b/bin.cjs
@@ -22053,7 +22053,7 @@ var init_sqliteImports = __esm({
const { unregister } = await safeRegister();
for (let i2 = 0; i2 < imports.length; i2++) {
const it = imports[i2];
- const i0 = require(`${it}`);
+ const i0 = await import(`${it}`);
const prepared = prepareFromExports3(i0);
tables.push(...prepared.tables);
}
@@ -129572,6 +129572,7 @@ var generateCommand = new Command("generate").option("--dialect <dialect>", "Dat
} else {
assertUnreachable(dialect7);
}
+ process.exit(0);
});
var migrateCommand = new Command("migrate").option(
"--config <config>",

View File

@@ -31,7 +31,7 @@ const Options = {
[
'@semantic-release/git',
{
assets: ['CHANGELOG.md', 'package.json'],
assets: ['CHANGELOG.md', 'package.json', '../../bun.lockb'],
},
],
[