Compare commits

...

22 Commits

Author SHA1 Message Date
semantic-release-bot
9f3295cc0f chore(release): 1.0.0-dev.9 [skip ci]
# @revanced/bot-websocket-api [1.0.0-dev.9](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.8...@revanced/bot-websocket-api@1.0.0-dev.9) (2024-08-03)

### Features

* **apis/websocket:** return `true` for data on a `TrainedMessage` packet ([65add4d](65add4dfee))
2024-08-03 19:53:18 +00:00
PalmDevs
4da6175cf5 fix(bots/discord): await when putting entries into db 2024-08-04 02:52:07 +07:00
PalmDevs
d90ad5c955 fix(packages/api): handle close event as a disconnect 2024-08-04 02:52:06 +07:00
PalmDevs
65add4dfee feat(apis/websocket): return true for data on a TrainedMessage packet 2024-08-04 02:52:04 +07:00
semantic-release-bot
2c2f6b76d4 chore(release): 1.0.0-dev.19 [skip ci]
# @revanced/discord-bot [1.0.0-dev.19](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.18...@revanced/discord-bot@1.0.0-dev.19) (2024-08-03)

### Bug Fixes

* **bots/discord:** correct whitelist logic ([49c29be](49c29bebfb))
2024-08-03 17:31:01 +00:00
PalmDevs
49c29bebfb fix(bots/discord): correct whitelist logic 2024-08-04 00:29:20 +07:00
semantic-release-bot
4e889d4991 chore(release): 1.0.0-dev.18 [skip ci]
# @revanced/discord-bot [1.0.0-dev.18](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.17...@revanced/discord-bot@1.0.0-dev.18) (2024-08-03)

### Bug Fixes

* **bots/discord:** set the `label` property correctly for message scans ([6d463df](6d463df586))
2024-08-03 15:24:18 +00:00
PalmDevs
6d463df586 fix(bots/discord): set the label property correctly for message scans 2024-08-03 22:22:42 +07:00
semantic-release-bot
2d8688bd4c chore(release): 1.0.0-dev.17 [skip ci]
# @revanced/discord-bot [1.0.0-dev.17](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.16...@revanced/discord-bot@1.0.0-dev.17) (2024-08-02)

### Bug Fixes

* **bots/discord/commands/eval:** evaluate in current context ([5925d90](5925d90209))
* **bots/discord:** send right response for after regexes ([a7688fa](a7688fa9b9))

### Features

* **bots/discord:** update example config file ([bc9951c](bc9951c9b5))
2024-08-02 18:40:39 +00:00
PalmDevs
bc9951c9b5 feat(bots/discord): update example config file 2024-08-03 01:36:19 +07:00
PalmDevs
a7688fa9b9 fix(bots/discord): send right response for after regexes 2024-08-03 01:31:45 +07:00
PalmDevs
5925d90209 fix(bots/discord/commands/eval): evaluate in current context 2024-08-03 01:31:44 +07:00
semantic-release-bot
5506518635 chore(release): 1.0.0-dev.16 [skip ci]
# @revanced/discord-bot [1.0.0-dev.16](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.15...@revanced/discord-bot@1.0.0-dev.16) (2024-08-02)

### Bug Fixes

* **bots/discord:** open database as read-write ([c366840](c36684091d))
* **bots/discord:** remove bad text channel checks ([f5939e2](f5939e2528))
* **bots/discord:** remove redundant footer for response embeds ([412e003](412e00317d))

### Features

* **bots/discord/commands:** add `reload` command ([6875b32](6875b32fd0))
2024-08-02 12:31:21 +00:00
PalmDevs
b9d08fff64 feat(bots/discord)!: add more attachment scan options 2024-08-02 19:26:20 +07:00
PalmDevs
6875b32fd0 feat(bots/discord/commands): add reload command 2024-08-02 19:26:20 +07:00
PalmDevs
c36684091d fix(bots/discord): open database as read-write 2024-08-02 19:26:19 +07:00
PalmDevs
f5939e2528 fix(bots/discord): remove bad text channel checks 2024-08-02 19:26:18 +07:00
PalmDevs
412e00317d fix(bots/discord): remove redundant footer for response embeds 2024-08-02 19:26:17 +07:00
PalmDevs
8fe78e424e fix(bots/discord)!: rename config replyToReplied to respondToReply 2024-08-02 19:26:12 +07:00
semantic-release-bot
9fe6b4ca70 chore(release): 1.0.0-dev.15 [skip ci]
# @revanced/discord-bot [1.0.0-dev.15](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.14...@revanced/discord-bot@1.0.0-dev.15) (2024-07-31)

### Bug Fixes

* **bots/discord:** import `config` from context ([763ef25](763ef253f9))

### Features

* **bots/discord:** add sticky messages ([bf66155](bf661556e1))
2024-07-31 19:31:21 +00:00
PalmDevs
bf661556e1 feat(bots/discord): add sticky messages 2024-08-01 02:29:49 +07:00
PalmDevs
763ef253f9 fix(bots/discord): import config from context 2024-07-31 21:25:12 +07:00
26 changed files with 301 additions and 103 deletions

View File

@@ -1,3 +1,10 @@
# @revanced/bot-websocket-api [1.0.0-dev.9](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.8...@revanced/bot-websocket-api@1.0.0-dev.9) (2024-08-03)
### Features
* **apis/websocket:** return `true` for data on a `TrainedMessage` packet ([65add4d](https://github.com/revanced/revanced-helper/commit/65add4dfeed2fa067c2c8e2377f7d01d505ade54))
# @revanced/bot-websocket-api [1.0.0-dev.8](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.7...@revanced/bot-websocket-api@1.0.0-dev.8) (2024-07-31) # @revanced/bot-websocket-api [1.0.0-dev.8](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.7...@revanced/bot-websocket-api@1.0.0-dev.8) (2024-07-31)

View File

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

View File

@@ -18,7 +18,7 @@ const trainMessageEventHandler: EventHandler<ClientOperation.TrainMessage> = asy
client.send( client.send(
{ {
op: ServerOperation.TrainedMessage, op: ServerOperation.TrainedMessage,
d: null, d: true,
}, },
nextSeq, nextSeq,
) )

View File

@@ -174,9 +174,6 @@ dist
# Finder (MacOS) folder config # Finder (MacOS) folder config
.DS_Store .DS_Store
# Config
config.ts
# DB # DB
*.db *.db
*.sqlite *.sqlite

View File

@@ -1,3 +1,56 @@
# @revanced/discord-bot [1.0.0-dev.19](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.18...@revanced/discord-bot@1.0.0-dev.19) (2024-08-03)
### Bug Fixes
* **bots/discord:** correct whitelist logic ([49c29be](https://github.com/revanced/revanced-helper/commit/49c29bebfbe348ae4e2cc1b3a83bfa41eb26ccd1))
# @revanced/discord-bot [1.0.0-dev.18](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.17...@revanced/discord-bot@1.0.0-dev.18) (2024-08-03)
### Bug Fixes
* **bots/discord:** set the `label` property correctly for message scans ([6d463df](https://github.com/revanced/revanced-helper/commit/6d463df586dee5dd8fe8d6cff1c5316f7809b32a))
# @revanced/discord-bot [1.0.0-dev.17](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.16...@revanced/discord-bot@1.0.0-dev.17) (2024-08-02)
### Bug Fixes
* **bots/discord/commands/eval:** evaluate in current context ([5925d90](https://github.com/revanced/revanced-helper/commit/5925d902095acef5f6396ca03583a9cbb0862498))
* **bots/discord:** send right response for after regexes ([a7688fa](https://github.com/revanced/revanced-helper/commit/a7688fa9b91919a87f74071b502cd0a87cd1c1fa))
### Features
* **bots/discord:** update example config file ([bc9951c](https://github.com/revanced/revanced-helper/commit/bc9951c9b5e007c3e1b3076aa0966ccf29bb18bc))
# @revanced/discord-bot [1.0.0-dev.16](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.15...@revanced/discord-bot@1.0.0-dev.16) (2024-08-02)
### Bug Fixes
* **bots/discord:** open database as read-write ([c366840](https://github.com/revanced/revanced-helper/commit/c36684091dddf67880505dc459e4334a8a5492f4))
* **bots/discord:** remove bad text channel checks ([f5939e2](https://github.com/revanced/revanced-helper/commit/f5939e25288fea2022fdeec9085ecb9ffada6111))
* **bots/discord:** remove redundant footer for response embeds ([412e003](https://github.com/revanced/revanced-helper/commit/412e00317d1eaca23e9c1375e16f94a5f2fa8d86))
### Features
* **bots/discord/commands:** add `reload` command ([6875b32](https://github.com/revanced/revanced-helper/commit/6875b32fd0c6ce3034da9dc6c704d425afb26f2e))
# @revanced/discord-bot [1.0.0-dev.15](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.14...@revanced/discord-bot@1.0.0-dev.15) (2024-07-31)
### Bug Fixes
* **bots/discord:** import `config` from context ([763ef25](https://github.com/revanced/revanced-helper/commit/763ef253f9d4ff70a8b79969a7f4f41cba7f3c59))
### Features
* **bots/discord:** add sticky messages ([bf66155](https://github.com/revanced/revanced-helper/commit/bf661556e131bf0ef24e47f658fbcd701960e312))
# @revanced/discord-bot [1.0.0-dev.14](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.13...@revanced/discord-bot@1.0.0-dev.14) (2024-07-31) # @revanced/discord-bot [1.0.0-dev.14](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.13...@revanced/discord-bot@1.0.0-dev.14) (2024-07-31)

View File

@@ -11,8 +11,21 @@ export default {
GUILD_ID_HERE: ['ROLE_ID_HERE'], GUILD_ID_HERE: ['ROLE_ID_HERE'],
}, },
}, },
stickyMessages: {
GUILD_ID_HERE: {
CHANNEL_ID_HERE: {
message: {
content: 'This is a sticky message!',
},
timeout: 60000,
forceSendTimeout: 300000,
}
}
},
moderation: { moderation: {
cure: { cure: {
minimumNameLength: 3,
removeCharactersRegex: /[^a-zA-Z0-9 \-_]/g,
defaultName: 'Server member', defaultName: 'Server member',
}, },
roles: ['ROLE_ID_HERE'], roles: ['ROLE_ID_HERE'],
@@ -61,7 +74,11 @@ export default {
}, },
}, },
}, },
allowedAttachmentMimeTypes: ['image/jpeg', 'image/png', 'image/webp'], attachments: {
scanAttachments: true,
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'text/plain'],
maxTextFileSize: 512000
},
responses: [ responses: [
{ {
filterOverride: { filterOverride: {

View File

@@ -6,6 +6,7 @@ export type Config = {
users?: string[] users?: string[]
roles?: Record<string, string[]> roles?: Record<string, string[]>
} }
stickyMessages?: Record<string, Record<string, StickyMessageConfig>>
moderation?: { moderation?: {
roles: string[] roles: string[]
cure?: { cure?: {
@@ -25,7 +26,11 @@ export type Config = {
messageScan?: { messageScan?: {
scanBots?: boolean scanBots?: boolean
scanOutsideGuilds?: boolean scanOutsideGuilds?: boolean
allowedAttachmentMimeTypes: string[] attachments?: {
scanAttachments?: boolean
allowedMimeTypes?: string[]
maxTextFileSize?: number
}
filter?: { filter?: {
whitelist?: Filter whitelist?: Filter
blacklist?: Filter blacklist?: Filter
@@ -50,6 +55,12 @@ export type Config = {
} }
} }
export type StickyMessageConfig = {
timeout: number
forceSendTimeout?: number
message: BaseMessageOptions
}
export type RolePresetConfig = { export type RolePresetConfig = {
give: string[] give: string[]
take: string[] take: string[]
@@ -62,7 +73,7 @@ export type ConfigMessageScanResponse = {
} }
filterOverride?: NonNullable<Config['messageScan']>['filter'] filterOverride?: NonNullable<Config['messageScan']>['filter']
response: ConfigMessageScanResponseMessage | null response: ConfigMessageScanResponseMessage | null
replyToReplied?: boolean respondToReply?: boolean
} }
export type ConfigMessageScanResponseLabelConfig = { export type ConfigMessageScanResponseLabelConfig = {

View File

@@ -2,7 +2,7 @@
"name": "@revanced/discord-bot", "name": "@revanced/discord-bot",
"type": "module", "type": "module",
"private": true, "private": true,
"version": "1.0.0-dev.14", "version": "1.0.0-dev.19",
"description": "🤖 Discord bot assisting ReVanced", "description": "🤖 Discord bot assisting ReVanced",
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {

View File

@@ -1,5 +1,5 @@
import { inspect } from 'util' import { inspect } from 'util'
import { runInNewContext } from 'vm' import { runInThisContext } from 'vm'
import { ApplicationCommandOptionType } from 'discord.js' import { ApplicationCommandOptionType } from 'discord.js'
import { AdminCommand } from '$/classes/Command' import { AdminCommand } from '$/classes/Command'
@@ -20,13 +20,13 @@ export default new AdminCommand({
required: false, required: false,
}, },
}, },
async execute(context, trigger, { code, 'show-hidden': showHidden }) { async execute(_, trigger, { code, 'show-hidden': showHidden }) {
await trigger.reply({ await trigger.reply({
ephemeral: true, ephemeral: true,
embeds: [ embeds: [
createSuccessEmbed('Evaluate', `\`\`\`js\n${code}\`\`\``).addFields({ createSuccessEmbed('Evaluate', `\`\`\`js\n${code}\`\`\``).addFields({
name: 'Result', name: 'Result',
value: `\`\`\`js\n${inspect(runInNewContext(code, { client: trigger.client, context, trigger }), { depth: 1, showHidden, getters: true, numericSeparator: true, showProxy: true })}\`\`\``, value: `\`\`\`js\n${inspect(runInThisContext(code), { depth: 1, showHidden, getters: true, numericSeparator: true, showProxy: true })}\`\`\``,
}), }),
], ],
}) })

View File

@@ -0,0 +1,17 @@
import { AdminCommand } from '$/classes/Command'
import { join, dirname } from 'path'
import type { Config } from 'config.schema'
export default new AdminCommand({
name: 'reload',
description: 'Reload configuration',
async execute(context, trigger) {
context.config = ((await import(join(dirname(Bun.main), '..', 'config.js'))) as { default: Config }).default
await trigger.reply({
content: 'Reloaded configuration',
ephemeral: true,
})
},
})

View File

@@ -1,4 +1,4 @@
import { EmbedBuilder, GuildChannel } from 'discord.js' import { EmbedBuilder } from 'discord.js'
import { ModerationCommand } from '$/classes/Command' import { ModerationCommand } from '$/classes/Command'
import CommandError, { CommandErrorType } from '$/classes/CommandError' import CommandError, { CommandErrorType } from '$/classes/CommandError'
@@ -31,7 +31,7 @@ export default new ModerationCommand({
throw new CommandError(CommandErrorType.MissingArgument, 'Either `amount` or `until` must be provided.') throw new CommandError(CommandErrorType.MissingArgument, 'Either `amount` or `until` must be provided.')
const channel = interaction.channel! const channel = interaction.channel!
if (!(channel.isTextBased() && channel instanceof GuildChannel)) if (!channel.isTextBased())
throw new CommandError(CommandErrorType.InvalidChannel, 'The supplied channel is not a text channel.') throw new CommandError(CommandErrorType.InvalidChannel, 'The supplied channel is not a text channel.')
const embed = applyCommonEmbedStyles( const embed = applyCommonEmbedStyles(

View File

@@ -28,7 +28,7 @@ export default new ModerationCommand({
if (!channel?.isTextBased() || channel.isDMBased()) if (!channel?.isTextBased() || channel.isDMBased())
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidChannel, CommandErrorType.InvalidChannel,
'The supplied channel is not a text channel or does not exist.', 'The supplied channel is not a text channel.',
) )
if (Number.isNaN(duration)) throw new CommandError(CommandErrorType.InvalidDuration, 'Invalid duration.') if (Number.isNaN(duration)) throw new CommandError(CommandErrorType.InvalidDuration, 'Invalid duration.')

View File

@@ -4,12 +4,6 @@ export const MessageScanLabeledResponseReactions = {
delete: '❌', delete: '❌',
} as const } as const
export const MessageScanHumanizedMode = {
ocr: 'image recognition',
nlp: 'text analysis',
match: 'pattern matching',
} as const
export const DefaultEmbedColor = '#4E98F0' export const DefaultEmbedColor = '#4E98F0'
export const ReVancedLogoURL = export const ReVancedLogoURL =
'https://media.discordapp.net/attachments/1095487869923119144/1115436493050224660/revanced-logo.png' 'https://media.discordapp.net/attachments/1095487869923119144/1115436493050224660/revanced-logo.png'

View File

@@ -3,7 +3,7 @@ import { existsSync, readFileSync, readdirSync } from 'fs'
import { join } from 'path' import { join } from 'path'
import { Client as APIClient } from '@revanced/bot-api' import { Client as APIClient } from '@revanced/bot-api'
import { createLogger } from '@revanced/bot-shared' import { createLogger } from '@revanced/bot-shared'
import { Client as DiscordClient, Partials } from 'discord.js' import { Client as DiscordClient, type Message, Partials } from 'discord.js'
import { drizzle } from 'drizzle-orm/bun-sqlite' import { drizzle } from 'drizzle-orm/bun-sqlite'
// Export some things first, as commands require them // Export some things first, as commands require them
@@ -56,7 +56,7 @@ if (DatabasePath && !existsSync(DatabasePath)) {
} }
} }
const db = new Database(DatabasePath) const db = new Database(DatabasePath, { readwrite: true, create: true })
if (dbSchemaFileName) db.run(readFileSync(join(DatabaseSchemaDir, dbSchemaFileName)).toString()) if (dbSchemaFileName) db.run(readFileSync(join(DatabaseSchemaDir, dbSchemaFileName)).toString())
export const database = drizzle(db, { export const database = drizzle(db, {
@@ -85,4 +85,19 @@ export const discord = {
string, string,
Command<boolean, CommandOptionsOptions | undefined, boolean> Command<boolean, CommandOptionsOptions | undefined, boolean>
>, >,
stickyMessages: {} as Record<
string,
Record<
string,
{
forceSendTimerActive?: boolean
timeoutMs: number
forceSendMs?: number
send: (forced?: boolean) => Promise<void>
currentMessage?: Message<true>
interval?: NodeJS.Timeout
forceSendInterval?: NodeJS.Timeout
}
>
>,
} as const } as const

View File

@@ -24,7 +24,7 @@ withContext(on, 'messageCreate', async (context, msg) => {
try { try {
logger.debug(`Classifying message ${msg.id}`) logger.debug(`Classifying message ${msg.id}`)
const { response, label, replyToReplied } = await getResponseFromText( const { response, label, respondToReply } = await getResponseFromText(
msg.content, msg.content,
filteredResponses, filteredResponses,
context, context,
@@ -33,14 +33,14 @@ withContext(on, 'messageCreate', async (context, msg) => {
if (response) { if (response) {
logger.debug('Response found') logger.debug('Response found')
const toReply = replyToReplied ? (msg.reference?.messageId ? await msg.fetchReference() : msg) : msg const toReply = respondToReply ? (msg.reference?.messageId ? await msg.fetchReference() : msg) : msg
const reply = await toReply.reply({ const reply = await toReply.reply({
...response, ...response,
embeds: response.embeds?.map(it => createMessageScanResponseEmbed(it, label ? 'nlp' : 'match')), embeds: response.embeds?.map(createMessageScanResponseEmbed),
}) })
if (label) if (label) {
db.insert(responses).values({ await db.insert(responses).values({
replyId: reply.id, replyId: reply.id,
channelId: reply.channel.id, channelId: reply.channel.id,
guildId: reply.guild!.id, guildId: reply.guild!.id,
@@ -49,7 +49,6 @@ withContext(on, 'messageCreate', async (context, msg) => {
content: msg.content, content: msg.content,
}) })
if (label) {
for (const reaction of Object.values(MessageScanLabeledResponseReactions)) { for (const reaction of Object.values(MessageScanLabeledResponseReactions)) {
await reply.react(reaction) await reply.react(reaction)
} }
@@ -60,11 +59,22 @@ withContext(on, 'messageCreate', async (context, msg) => {
} }
} }
if (msg.attachments.size > 0) { if (msg.attachments.size > 0 && config.attachments?.scanAttachments) {
logger.debug(`Classifying message attachments for ${msg.id}`) logger.debug(`Classifying message attachments for ${msg.id}`)
for (const attachment of msg.attachments.values()) { for (const attachment of msg.attachments.values()) {
if (attachment.contentType && !config.allowedAttachmentMimeTypes.includes(attachment.contentType)) continue if (
config.attachments.allowedMimeTypes &&
!config.attachments.allowedMimeTypes.includes(attachment.contentType!)
) {
logger.debug(`Disallowed MIME type for attachment: ${attachment.url}, ${attachment.contentType}`)
continue
}
if (attachment.contentType?.startsWith('text/') && attachment.size > (config.attachments.maxTextFileSize ?? 512 * 1000)) {
logger.debug(`Attachment ${attachment.url} is too large be to scanned, size is ${attachment.size}`)
continue
}
try { try {
const { text: content } = await api.client.parseImage(attachment.url) const { text: content } = await api.client.parseImage(attachment.url)
@@ -74,7 +84,7 @@ withContext(on, 'messageCreate', async (context, msg) => {
logger.debug(`Response found for attachment: ${attachment.url}`) logger.debug(`Response found for attachment: ${attachment.url}`)
await msg.reply({ await msg.reply({
...response, ...response,
embeds: response.embeds?.map(it => createMessageScanResponseEmbed(it, 'ocr')), embeds: response.embeds?.map(createMessageScanResponseEmbed),
}) })
break break

View File

@@ -0,0 +1,30 @@
import { on, withContext } from '$utils/discord/events'
withContext(on, 'messageCreate', async ({ discord, logger }, msg) => {
if (!msg.inGuild()) return
if (msg.author.id === msg.client.user.id) return
const store = discord.stickyMessages[msg.guildId]?.[msg.channelId]
if (!store) return
if (!store.interval) store.interval = setTimeout(store.send, store.timeoutMs) as NodeJS.Timeout
else {
store.interval.refresh()
if (!store.forceSendTimerActive && store.forceSendMs) {
logger.debug(`Channel ${msg.channelId} in guild ${msg.guildId} is active, starting force send timer`)
store.forceSendTimerActive = true
if (!store.forceSendInterval)
store.forceSendInterval = setTimeout(
() =>
store.send(true).then(() => {
store.forceSendTimerActive = false
}),
store.forceSendMs,
) as NodeJS.Timeout
else store.forceSendInterval.refresh()
}
}
})

View File

@@ -20,7 +20,8 @@ const PossibleReactions = Object.values(Reactions) as string[]
withContext(on, 'messageReactionAdd', async (context, rct, user) => { withContext(on, 'messageReactionAdd', async (context, rct, user) => {
if (user.bot) return if (user.bot) return
await rct.users.remove(user.id)
const { database: db, logger, config } = context const { database: db, logger, config } = context
const { messageScan: msConfig } = config const { messageScan: msConfig } = config
@@ -35,10 +36,7 @@ withContext(on, 'messageReactionAdd', async (context, rct, user) => {
if (!isAdmin(reactionMessage.member || reactionMessage.author)) { if (!isAdmin(reactionMessage.member || reactionMessage.author)) {
// User is in guild, and config has member requirements // User is in guild, and config has member requirements
if ( if (reactionMessage.inGuild() && msConfig.humanCorrections.allow) {
reactionMessage.inGuild() &&
(msConfig.humanCorrections.allow?.members || msConfig.humanCorrections.allow?.users)
) {
const { const {
allow: { users: allowedUsers, members: allowedMembers }, allow: { users: allowedUsers, members: allowedMembers },
} = msConfig.humanCorrections } = msConfig.humanCorrections
@@ -54,20 +52,19 @@ withContext(on, 'messageReactionAdd', async (context, rct, user) => {
) )
) )
return return
} else if (allowedUsers) { } else if (!allowedUsers?.includes(user.id)) return
if (!allowedUsers.includes(user.id)) return } else
} else { return void logger.warn(
return void logger.warn( 'No member or user requirements set for human corrections, all requests will be ignored',
'No member or user requirements set for human corrections, all requests will be ignored', )
)
}
}
} }
// Sanity check // Sanity check
const response = await db.query.responses.findFirst({ where: eq(responses.replyId, rct.message.id) }) const response = await db.query.responses.findFirst({ where: eq(responses.replyId, rct.message.id) })
if (!response || response.correctedById) return if (!response || response.correctedById) return
logger.debug(`User ${user.id} is trying to correct the response ${rct.message.id}`)
const handleCorrection = (label: string) => const handleCorrection = (label: string) =>
handleUserResponseCorrection(context, response, reactionMessage, label, user) handleUserResponseCorrection(context, response, reactionMessage, label, user)

View File

@@ -1,14 +1,68 @@
import { database, logger } from '$/context' import { database, logger } from '$/context'
import { appliedPresets } from '$/database/schemas' import { appliedPresets } from '$/database/schemas'
import { applyCommonEmbedStyles } from '$/utils/discord/embeds'
import { on, withContext } from '$/utils/discord/events'
import { removeRolePreset } from '$/utils/discord/rolePresets' import { removeRolePreset } from '$/utils/discord/rolePresets'
import type { Client } from 'discord.js'
import { lt } from 'drizzle-orm' import { lt } from 'drizzle-orm'
import { on, withContext } from 'src/utils/discord/events'
export default withContext(on, 'ready', ({ config, logger }, client) => { import type { Client } from 'discord.js'
export default withContext(on, 'ready', async ({ config, discord, logger }, client) => {
logger.info(`Connected to Discord API, logged in as ${client.user.displayName} (@${client.user.tag})!`) logger.info(`Connected to Discord API, logged in as ${client.user.displayName} (@${client.user.tag})!`)
logger.info(`Bot is in ${client.guilds.cache.size} guilds`) logger.info(`Bot is in ${client.guilds.cache.size} guilds`)
if (config.stickyMessages)
for (const [guildId, channels] of Object.entries(config.stickyMessages)) {
const guild = await client.guilds.fetch(guildId)
discord.stickyMessages[guildId] = {}
for (const [channelId, { message, timeout, forceSendTimeout }] of Object.entries(channels)) {
const channel = await guild.channels.fetch(channelId)
if (!channel?.isTextBased()) return
const send = async (forced = false) => {
try {
const msg = await channel.send({
...message,
embeds: message.embeds?.map(it => applyCommonEmbedStyles(it, true, true, true)),
})
const store = discord.stickyMessages[guildId]![channelId]
if (!store) return
await store.currentMessage?.delete().catch()
store.currentMessage = msg
if (!forced) {
clearTimeout(store.forceSendInterval)
logger.debug(
`Timeout ended for sticky message in channel ${channelId} in guild ${guildId}, channel is inactive`,
)
} else {
clearTimeout(store.interval)
logger.debug(
`Forced send timeout for sticky message in channel ${channelId} in guild ${guildId} ended, channel is too active`,
)
}
logger.debug(`Sent sticky message to channel ${channelId} in guild ${guildId}`)
} catch (e) {
logger.error(
`Error while sending sticky message to channel ${channelId} in guild ${guildId}:`,
e,
)
}
}
discord.stickyMessages[guildId]![channelId] = {
forceSendMs: forceSendTimeout,
timeoutMs: timeout,
send,
forceSendTimerActive: false,
}
}
}
if (config.rolePresets) { if (config.rolePresets) {
removeExpiredPresets(client) removeExpiredPresets(client)
setTimeout(() => removeExpiredPresets(client), config.rolePresets.checkExpiredEvery) setTimeout(() => removeExpiredPresets(client), config.rolePresets.checkExpiredEvery)

View File

@@ -1,5 +1,5 @@
import { DefaultEmbedColor, MessageScanHumanizedMode, ReVancedLogoURL } from '$/constants' import { DefaultEmbedColor, ReVancedLogoURL } from '$/constants'
import { EmbedBuilder, type EmbedField, type User } from 'discord.js' import { type APIEmbed, EmbedBuilder, type EmbedField, type JSONEncodable, type User } from 'discord.js'
import type { ConfigMessageScanResponseMessage } from '../../../config.schema' import type { ConfigMessageScanResponseMessage } from '../../../config.schema'
export const createErrorEmbed = (title: string | null, description?: string) => export const createErrorEmbed = (title: string | null, description?: string) =>
@@ -25,23 +25,7 @@ export const createSuccessEmbed = (title: string | null, description?: string) =
export const createMessageScanResponseEmbed = ( export const createMessageScanResponseEmbed = (
response: NonNullable<ConfigMessageScanResponseMessage['embeds']>[number], response: NonNullable<ConfigMessageScanResponseMessage['embeds']>[number],
mode: 'ocr' | 'nlp' | 'match', ) => applyCommonEmbedStyles(response, true, true, true)
) => {
// 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 • Via ${MessageScanHumanizedMode[mode]}`,
iconURL: ReVancedLogoURL,
})
return applyCommonEmbedStyles(embed, true, true, true)
}
export const createModerationActionEmbed = ( export const createModerationActionEmbed = (
action: string, action: string,
@@ -77,19 +61,23 @@ export const applyReferenceToModerationActionEmbed = (embed: EmbedBuilder, refer
} }
export const applyCommonEmbedStyles = ( export const applyCommonEmbedStyles = (
embed: EmbedBuilder, embed: EmbedBuilder | JSONEncodable<APIEmbed> | APIEmbed,
setThumbnail = false, setThumbnail = false,
setFooter = false, setFooter = false,
setColor = false, setColor = false,
) => { ) => {
// biome-ignore lint/style/noParameterAssign: While this is confusing, it is fine for this purpose
if ('toJSON' in embed) embed = embed.toJSON()
const builder = new EmbedBuilder(embed)
if (setFooter) if (setFooter)
embed.setFooter({ builder.setFooter({
text: 'ReVanced', text: 'ReVanced',
iconURL: ReVancedLogoURL, iconURL: ReVancedLogoURL,
}) })
if (setColor) embed.setColor(DefaultEmbedColor) if (setColor) builder.setColor(DefaultEmbedColor)
if (setThumbnail) embed.setThumbnail(ReVancedLogoURL) if (setThumbnail) builder.setThumbnail(ReVancedLogoURL)
return embed return builder
} }

View File

@@ -64,29 +64,30 @@ export const getResponseFromText = async (
const matchedLabel = scan.labels[0]! const matchedLabel = scan.labels[0]!
logger.debug(`Message matched label with confidence: ${matchedLabel.name}, ${matchedLabel.confidence}`) logger.debug(`Message matched label with confidence: ${matchedLabel.name}, ${matchedLabel.confidence}`)
let triggerConfig: ConfigMessageScanResponseLabelConfig | undefined let trigger: ConfigMessageScanResponseLabelConfig | undefined
const labelConfig = responses.find(x => { const response = responses.find(x => {
const config = x.triggers.text!.find( const config = x.triggers.text!.find(
(x): x is ConfigMessageScanResponseLabelConfig => 'label' in x && x.label === matchedLabel.name, (x): x is ConfigMessageScanResponseLabelConfig => 'label' in x && x.label === matchedLabel.name,
) )
if (config) triggerConfig = config if (config) trigger = config
return config return config
}) })
if (!labelConfig) { if (!response) {
logger.warn(`No label config found for label ${matchedLabel.name}`) logger.warn(`No response config found for label ${matchedLabel.name}`)
// This returns the default value set in line 17, which means no response matched
return responseConfig return responseConfig
} }
if (matchedLabel.confidence >= triggerConfig!.threshold) { if (matchedLabel.confidence >= trigger!.threshold) {
logger.debug('Label confidence is enough') logger.debug('Label confidence is enough')
responseConfig = labelConfig responseConfig = { ...responseConfig, ...response, label: trigger!.label }
} }
} }
} }
// If we still don't have a response config, we can match all regexes after the initial label trigger // If we still don't have a response config, we can match all regexes after the initial label trigger
if (!responseConfig.triggers) { if (!responseConfig.triggers && ocrMode) {
logger.debug('No match from NLP, doing after regexes') logger.debug('No match from NLP, doing after regexes')
for (let i = 0; i < responses.length; i++) { for (let i = 0; i < responses.length; i++) {
const { const {
@@ -94,8 +95,8 @@ export const getResponseFromText = async (
} = responses[i]! } = responses[i]!
const firstLabelIndex = firstLabelIndexes[i] ?? -1 const firstLabelIndex = firstLabelIndexes[i] ?? -1
for (let i = firstLabelIndex + 1; i < textTriggers!.length; i++) { for (let j = firstLabelIndex + 1; j < textTriggers!.length; j++) {
const trigger = textTriggers![i]! const trigger = textTriggers![j]!
if (trigger instanceof RegExp) { if (trigger instanceof RegExp) {
if (trigger.test(content)) { if (trigger.test(content)) {
@@ -121,17 +122,19 @@ export const messageMatchesFilter = (message: Message, filter: NonNullable<Confi
// If matches whitelist but also matches blacklist, will return false // If matches whitelist but also matches blacklist, will return false
// If matches only whitelist, will return true // If matches only whitelist, will return true
// If matches neither, will return true // If matches neither, will return true
return whitelist return (
? (whitelist.channels?.includes(message.channelId) ?? true) || (whitelist
(whitelist.roles?.some(role => memberRoles.has(role)) ?? true) || ? whitelist.channels?.includes(message.channelId) ||
(whitelist.users?.includes(message.author.id) ?? true) whitelist.roles?.some(role => memberRoles.has(role)) ||
: true && whitelist.users?.includes(message.author.id)
!( : true) &&
blacklist && !(
(blacklist.channels?.includes(message.channelId) || blacklist &&
blacklist.roles?.some(role => memberRoles.has(role)) || (blacklist.channels?.includes(message.channelId) ||
blacklist.users?.includes(message.author.id)) blacklist.roles?.some(role => memberRoles.has(role)) ||
) blacklist.users?.includes(message.author.id))
)
)
} }
export const handleUserResponseCorrection = async ( export const handleUserResponseCorrection = async (
@@ -158,7 +161,7 @@ export const handleUserResponseCorrection = async (
await reply.edit({ await reply.edit({
...correctLabelResponse.response, ...correctLabelResponse.response,
embeds: correctLabelResponse.response.embeds?.map(it => createMessageScanResponseEmbed(it, 'nlp')), embeds: correctLabelResponse.response.embeds?.map(createMessageScanResponseEmbed),
}) })
} }

View File

@@ -38,7 +38,7 @@ export const getLogChannel = async (guild: Guild) => {
try { try {
const channel = await guild.channels.fetch(logConfig.thread ?? logConfig.channel) const channel = await guild.channels.fetch(logConfig.thread ?? logConfig.channel)
if (!channel || !channel.isTextBased()) if (!channel?.isTextBased())
return void logger.warn('The moderation log channel does not exist, skipping logging') return void logger.warn('The moderation log channel does not exist, skipping logging')
return channel return channel

View File

@@ -1,5 +1,5 @@
import { GuildMember, type User } from 'discord.js' import { GuildMember, type User } from 'discord.js'
import config from '../../../config' import { config } from '../../context'
export const isAdmin = (userOrMember: User | GuildMember) => { export const isAdmin = (userOrMember: User | GuildMember) => {
return ( return (

BIN
bun.lockb

Binary file not shown.

View File

@@ -77,9 +77,13 @@ export class ClientWebSocketManager {
} catch (e) { } catch (e) {
rj(e) rj(e)
} }
}).finally(() => {
this.connecting = false
}) })
.then(() => {
this.#socket.on('close', (code, reason) => this._handleDisconnect(code, reason.toString()))
})
.finally(() => {
this.connecting = false
})
} }
/** /**

View File

@@ -1,9 +1,9 @@
import { import {
url,
type AnySchema, type AnySchema,
type NullSchema, type NullSchema,
type ObjectSchema, type ObjectSchema,
type Output, type Output,
type BooleanSchema,
array, array,
enum_, enum_,
null_, null_,
@@ -11,6 +11,8 @@ import {
parse, parse,
special, special,
string, string,
boolean,
url,
// merge // merge
} from 'valibot' } from 'valibot'
import DisconnectReason from '../constants/DisconnectReason' import DisconnectReason from '../constants/DisconnectReason'
@@ -26,8 +28,7 @@ export const PacketSchema = special<Packet>(input => {
'op' in input && 'op' in input &&
typeof input.op === 'number' && typeof input.op === 'number' &&
input.op in Operation && input.op in Operation &&
'd' in input && 'd' in input
typeof input.d === 'object'
) { ) {
if (input.op in ServerOperation && !('s' in input && typeof input.s === 'number')) return false if (input.op in ServerOperation && !('s' in input && typeof input.s === 'number')) return false
@@ -62,7 +63,7 @@ export const PacketDataSchemas = {
[ServerOperation.Disconnect]: object({ [ServerOperation.Disconnect]: object({
reason: enum_(DisconnectReason), reason: enum_(DisconnectReason),
}), }),
[ServerOperation.TrainedMessage]: null_(), [ServerOperation.TrainedMessage]: boolean(),
[ServerOperation.TrainMessageFailed]: null_(), [ServerOperation.TrainMessageFailed]: null_(),
[ClientOperation.ParseText]: object({ [ClientOperation.ParseText]: object({
@@ -78,7 +79,7 @@ export const PacketDataSchemas = {
} as const satisfies Record< } as const satisfies Record<
Operation, Operation,
// biome-ignore lint/suspicious/noExplicitAny: This is a schema, it's not possible to type it // biome-ignore lint/suspicious/noExplicitAny: This is a schema, it's not possible to type it
ObjectSchema<any> | AnySchema | NullSchema ObjectSchema<any> | AnySchema | NullSchema | BooleanSchema
> >
export type Packet<TOp extends Operation = Operation> = TOp extends ServerOperation export type Packet<TOp extends Operation = Operation> = TOp extends ServerOperation