mirror of
https://github.com/ReVanced/revanced-bots.git
synced 2026-01-11 21:56:17 +00:00
Compare commits
9 Commits
@revanced/
...
@revanced/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82fac783ea | ||
|
|
dd21a5abad | ||
|
|
80aeb19020 | ||
|
|
6c8dce0593 | ||
|
|
9897f244e0 | ||
|
|
eaa25f2eb5 | ||
|
|
a976dd2acc | ||
|
|
c567ef25c6 | ||
|
|
de8bef6520 |
@@ -1,3 +1,25 @@
|
||||
# @revanced/discord-bot [1.0.0-dev.22](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.21...@revanced/discord-bot@1.0.0-dev.22) (2024-08-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bots/discord:** parse larger units of durations, fix wrong timestamp in mod embed ([6c8dce0](https://github.com/revanced/revanced-helper/commit/6c8dce059366a6ef85f5b8b1794c056515b9f5b6))
|
||||
* **bots/discord:** provide discord token for `reload` command ([dd21a5a](https://github.com/revanced/revanced-helper/commit/dd21a5abad560f3d00b8c58912786d4b6bd520e9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **bots/discord:** add code to actually scan text files correctly ([80aeb19](https://github.com/revanced/revanced-helper/commit/80aeb1902063140a2e78cfaed9424e5101ab03f1))
|
||||
|
||||
# @revanced/discord-bot [1.0.0-dev.21](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.20...@revanced/discord-bot@1.0.0-dev.21) (2024-08-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bots/discord:** correct sticky messages logic ([de8bef6](https://github.com/revanced/revanced-helper/commit/de8bef6520d53a1299f0478458320a7eb75c5e1d))
|
||||
* **bots/discord:** make `/eval` work ([eaa25f2](https://github.com/revanced/revanced-helper/commit/eaa25f2eb58a9e2d25bb98633ad668485e099714))
|
||||
* **bots/discord:** some configuration values not applying after running `/reload` ([a976dd2](https://github.com/revanced/revanced-helper/commit/a976dd2accc4b74914651245acde0979c30c92f5))
|
||||
|
||||
# @revanced/discord-bot [1.0.0-dev.20](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.19...@revanced/discord-bot@1.0.0-dev.20) (2024-08-03)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@revanced/discord-bot",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"version": "1.0.0-dev.20",
|
||||
"version": "1.0.0-dev.22",
|
||||
"description": "🤖 Discord bot assisting ReVanced",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { inspect } from 'util'
|
||||
import { runInThisContext } from 'vm'
|
||||
import { ApplicationCommandOptionType } from 'discord.js'
|
||||
|
||||
import { AdminCommand } from '$/classes/Command'
|
||||
@@ -26,7 +25,14 @@ export default new AdminCommand({
|
||||
embeds: [
|
||||
createSuccessEmbed('Evaluate', `\`\`\`js\n${code}\`\`\``).addFields({
|
||||
name: 'Result',
|
||||
value: `\`\`\`js\n${inspect(runInThisContext(code), { depth: 1, showHidden, getters: true, numericSeparator: true, showProxy: true })}\`\`\``,
|
||||
// biome-ignore lint/security/noGlobalEval: This is fine as it's an admin command
|
||||
value: `\`\`\`js\n${inspect(eval(code), {
|
||||
depth: 1,
|
||||
showHidden,
|
||||
getters: true,
|
||||
numericSeparator: true,
|
||||
showProxy: true,
|
||||
})}\`\`\``,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
@@ -7,11 +7,31 @@ export default new AdminCommand({
|
||||
name: 'reload',
|
||||
description: 'Reload configuration',
|
||||
async execute(context, trigger) {
|
||||
const { api, logger, discord } = context
|
||||
context.config = ((await import(join(dirname(Bun.main), '..', 'config.js'))) as { default: Config }).default
|
||||
|
||||
await trigger.reply({
|
||||
content: 'Reloaded configuration',
|
||||
ephemeral: true,
|
||||
})
|
||||
if ('deferReply' in trigger) await trigger.deferReply({ ephemeral: true })
|
||||
|
||||
logger.info('Reinitializing API client to reload configuration...')
|
||||
await api.client.ws.setOptions(
|
||||
{
|
||||
url: context.config.api.url,
|
||||
},
|
||||
false,
|
||||
)
|
||||
api.intentionallyDisconnecting = true
|
||||
api.client.disconnect(true)
|
||||
api.disconnectCount = 0
|
||||
api.intentionallyDisconnecting = false
|
||||
await api.client.connect()
|
||||
|
||||
logger.info('Reinitializing Discord client to reload configuration...')
|
||||
await discord.client.destroy()
|
||||
// discord.client.token only gets set whenever a new Client is intialized
|
||||
// so that's why we need to provide the token here :/
|
||||
await discord.client.login(process.env['DISCORD_TOKEN'])
|
||||
|
||||
// @ts-expect-error: TypeScript dum
|
||||
await trigger[('deferReply' in trigger ? 'editReply' : 'reply')]({ content: 'Reloaded configuration' })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -60,7 +60,7 @@ export default new ModerationCommand({
|
||||
await applyRolePreset(member, 'mute', expires)
|
||||
await sendModerationReplyAndLogs(
|
||||
interaction,
|
||||
createModerationActionEmbed('Muted', user, executor.user, reason, duration),
|
||||
createModerationActionEmbed('Muted', user, executor.user, reason, Math.ceil(expires / 1000)),
|
||||
)
|
||||
|
||||
if (duration)
|
||||
|
||||
@@ -83,6 +83,6 @@ export default new ModerationCommand({
|
||||
removeRolePreset(member, preset)
|
||||
}, expires)
|
||||
|
||||
await sendPresetReplyAndLogs(apply ? 'apply' : 'remove', trigger, executor, user, preset, expires)
|
||||
await sendPresetReplyAndLogs(apply ? 'apply' : 'remove', trigger, executor, user, preset, expires ? Math.ceil(expires / 1000) : undefined)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -90,13 +90,20 @@ export const discord = {
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
forceSendTimerActive?: boolean
|
||||
timeoutMs: number
|
||||
forceSendMs?: number
|
||||
/**
|
||||
* Chat is active, so force send timer is also active
|
||||
*/
|
||||
forceTimerActive: boolean
|
||||
/**
|
||||
* There was a message sent, so the timer is active
|
||||
*/
|
||||
timerActive: boolean
|
||||
timerMs: number
|
||||
forceTimerMs?: number
|
||||
send: (forced?: boolean) => Promise<void>
|
||||
currentMessage?: Message<true>
|
||||
interval?: NodeJS.Timeout
|
||||
forceSendInterval?: NodeJS.Timeout
|
||||
timer?: NodeJS.Timeout
|
||||
forceTimer?: NodeJS.Timeout
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
@@ -59,26 +59,38 @@ withContext(on, 'messageCreate', async (context, msg) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.attachments.size > 0 && config.attachments?.scanAttachments) {
|
||||
if (msg.attachments.size && config.attachments?.scanAttachments) {
|
||||
logger.debug(`Classifying message attachments for ${msg.id}`)
|
||||
|
||||
for (const attachment of msg.attachments.values()) {
|
||||
const mimeType = attachment.contentType?.split(';')?.[0]
|
||||
if (!mimeType) return void logger.warn(`No MIME type for attachment: ${attachment.url}`)
|
||||
|
||||
if (
|
||||
config.attachments.allowedMimeTypes &&
|
||||
!config.attachments.allowedMimeTypes.includes(attachment.contentType!)
|
||||
!config.attachments.allowedMimeTypes.includes(mimeType)
|
||||
) {
|
||||
logger.debug(`Disallowed MIME type for attachment: ${attachment.url}, ${attachment.contentType}`)
|
||||
logger.debug(`Disallowed MIME type for attachment: ${attachment.url}, ${mimeType}`)
|
||||
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}`)
|
||||
const isTextFile = mimeType.startsWith('text/')
|
||||
|
||||
if (isTextFile && attachment.size > (config.attachments.maxTextFileSize ?? 512 * 1000)) {
|
||||
logger.debug(`Attachment ${attachment.url} is too large be to scanned, size is ${attachment.size}`)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const { text: content } = await api.client.parseImage(attachment.url)
|
||||
const { response } = await getResponseFromText(content, filteredResponses, context, true)
|
||||
let response: Awaited<ReturnType<typeof getResponseFromText>>['response'] | undefined
|
||||
|
||||
if (isTextFile) {
|
||||
const content = await (await fetch(attachment.url)).text()
|
||||
response = await getResponseFromText(content, filteredResponses, context, { skipApiRequest: true }).then(it => it.response)
|
||||
} else {
|
||||
const { text: content } = await api.client.parseImage(attachment.url)
|
||||
response = await getResponseFromText(content, filteredResponses, context, { onlyImageTriggers: true }).then(it => it.response)
|
||||
}
|
||||
|
||||
if (response) {
|
||||
logger.debug(`Response found for attachment: ${attachment.url}`)
|
||||
@@ -89,8 +101,8 @@ withContext(on, 'messageCreate', async (context, msg) => {
|
||||
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
logger.error(`Failed to parse image: ${attachment.url}`)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to parse attachment: ${attachment.url}`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,24 +7,31 @@ withContext(on, 'messageCreate', async ({ discord, logger }, msg) => {
|
||||
const store = discord.stickyMessages[msg.guildId]?.[msg.channelId]
|
||||
if (!store) return
|
||||
|
||||
if (!store.interval) store.interval = setTimeout(store.send, store.timeoutMs) as NodeJS.Timeout
|
||||
// If there isn't a timer, start it up
|
||||
store.timerActive = true
|
||||
if (!store.timer) store.timer = setTimeout(store.send, store.timerMs) as NodeJS.Timeout
|
||||
else {
|
||||
store.interval.refresh()
|
||||
// If there is a timer, but it isn't active, restart it
|
||||
if (!store.timerActive) store.timer.refresh()
|
||||
// If there is a timer and it is active, but the force timer isn't active...
|
||||
else if (!store.forceTimerActive && store.forceTimerMs) {
|
||||
logger.debug(`Channel ${msg.channelId} in guild ${msg.guildId} is active, starting force send timer and clearing existing timer`)
|
||||
|
||||
if (!store.forceSendTimerActive && store.forceSendMs) {
|
||||
logger.debug(`Channel ${msg.channelId} in guild ${msg.guildId} is active, starting force send timer`)
|
||||
// Clear the timer
|
||||
clearTimeout(store.timer)
|
||||
store.timerActive = false
|
||||
store.forceTimerActive = true
|
||||
|
||||
store.forceSendTimerActive = true
|
||||
|
||||
if (!store.forceSendInterval)
|
||||
store.forceSendInterval = setTimeout(
|
||||
// (Re)start the force timer
|
||||
if (!store.forceTimer)
|
||||
store.forceTimer = setTimeout(
|
||||
() =>
|
||||
store.send(true).then(() => {
|
||||
store.forceSendTimerActive = false
|
||||
store.forceTimerActive = false
|
||||
}),
|
||||
store.forceSendMs,
|
||||
store.forceTimerMs,
|
||||
) as NodeJS.Timeout
|
||||
else store.forceSendInterval.refresh()
|
||||
else store.forceTimer.refresh()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,11 +14,16 @@ export default withContext(on, 'ready', async ({ config, discord, logger }, clie
|
||||
if (config.stickyMessages)
|
||||
for (const [guildId, channels] of Object.entries(config.stickyMessages)) {
|
||||
const guild = await client.guilds.fetch(guildId)
|
||||
// In case of configuration refresh, this will not be nullable
|
||||
const oldStore = discord.stickyMessages[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
|
||||
if (!channel?.isTextBased())
|
||||
return void logger.warn(
|
||||
`Channel ${channelId} in guild ${guildId} is not a text channel, sticky messages will not be sent`,
|
||||
)
|
||||
|
||||
const send = async (forced = false) => {
|
||||
try {
|
||||
@@ -33,17 +38,19 @@ export default withContext(on, 'ready', async ({ config, discord, logger }, clie
|
||||
await store.currentMessage?.delete().catch()
|
||||
store.currentMessage = msg
|
||||
|
||||
if (!forced) {
|
||||
clearTimeout(store.forceSendInterval)
|
||||
// Clear any remaining timers
|
||||
clearTimeout(store.timer)
|
||||
clearTimeout(store.forceTimer)
|
||||
store.forceTimerActive = store.timerActive = false
|
||||
|
||||
if (!forced)
|
||||
logger.debug(
|
||||
`Timeout ended for sticky message in channel ${channelId} in guild ${guildId}, channel is inactive`,
|
||||
)
|
||||
} else {
|
||||
clearTimeout(store.interval)
|
||||
else
|
||||
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) {
|
||||
@@ -53,13 +60,20 @@ export default withContext(on, 'ready', async ({ config, discord, logger }, clie
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set up the store
|
||||
discord.stickyMessages[guildId]![channelId] = {
|
||||
forceSendMs: forceSendTimeout,
|
||||
timeoutMs: timeout,
|
||||
forceTimerActive: false,
|
||||
timerActive: false,
|
||||
forceTimerMs: forceSendTimeout,
|
||||
timerMs: timeout,
|
||||
send,
|
||||
forceSendTimerActive: false,
|
||||
// If the store exists before the configuration refresh, take its current message
|
||||
currentMessage: oldStore?.[channelId]?.currentMessage
|
||||
}
|
||||
|
||||
// Send a new sticky message immediately, as well as deleting the old/outdated message, if it exists
|
||||
await send()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export const getResponseFromText = async (
|
||||
responses: ConfigMessageScanResponse[],
|
||||
// Just to be safe that we will never use data from the context parameter
|
||||
{ api, logger }: Omit<typeof import('src/context'), 'config'>,
|
||||
ocrMode = false,
|
||||
flags: { onlyImageTriggers?: boolean; skipApiRequest?: boolean } = {}
|
||||
): Promise<
|
||||
Omit<ConfigMessageScanResponse, 'triggers'> & { label?: string; triggers?: ConfigMessageScanResponse['triggers'] }
|
||||
> => {
|
||||
@@ -31,7 +31,7 @@ export const getResponseFromText = async (
|
||||
triggers: { text: textTriggers, image: imageTriggers },
|
||||
} = trigger
|
||||
|
||||
if (ocrMode) {
|
||||
if (flags.onlyImageTriggers) {
|
||||
if (imageTriggers)
|
||||
for (const regex of imageTriggers)
|
||||
if (regex.test(content)) {
|
||||
@@ -57,7 +57,7 @@ export const getResponseFromText = async (
|
||||
}
|
||||
|
||||
// If none of the regexes match, we can search for labels immediately
|
||||
if (!responseConfig.triggers && !ocrMode) {
|
||||
if (!responseConfig.triggers && !flags.onlyImageTriggers && !flags.skipApiRequest) {
|
||||
logger.debug('No match from before regexes, doing NLP')
|
||||
const scan = await api.client.parseText(content)
|
||||
if (scan.labels.length) {
|
||||
@@ -87,7 +87,7 @@ export const getResponseFromText = async (
|
||||
}
|
||||
|
||||
// If we still don't have a response config, we can match all regexes after the initial label trigger
|
||||
if (!responseConfig.triggers && ocrMode) {
|
||||
if (!responseConfig.triggers && flags.onlyImageTriggers) {
|
||||
logger.debug('No match from NLP, doing after regexes')
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
const {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { config, logger } from '$/context'
|
||||
import decancer from 'decancer'
|
||||
import type { ChatInputCommandInteraction, EmbedBuilder, Guild, GuildMember, Message, User } from 'discord.js'
|
||||
import type { CommandInteraction, EmbedBuilder, Guild, GuildMember, Message, User } from 'discord.js'
|
||||
import { applyReferenceToModerationActionEmbed, createModerationActionEmbed } from './embeds'
|
||||
|
||||
const PresetLogAction = {
|
||||
@@ -10,7 +10,7 @@ const PresetLogAction = {
|
||||
|
||||
export const sendPresetReplyAndLogs = (
|
||||
action: keyof typeof PresetLogAction,
|
||||
interaction: ChatInputCommandInteraction | Message,
|
||||
interaction: CommandInteraction | Message,
|
||||
executor: GuildMember,
|
||||
user: User,
|
||||
preset: string,
|
||||
@@ -24,7 +24,7 @@ export const sendPresetReplyAndLogs = (
|
||||
)
|
||||
|
||||
export const sendModerationReplyAndLogs = async (
|
||||
interaction: ChatInputCommandInteraction | Message,
|
||||
interaction: CommandInteraction | Message,
|
||||
embed: EmbedBuilder,
|
||||
) => {
|
||||
const reply = await interaction.reply({ embeds: [embed] }).then(it => it.fetch())
|
||||
|
||||
@@ -6,9 +6,9 @@ import { and, eq } from 'drizzle-orm'
|
||||
// TODO: Fix this type
|
||||
type PresetKey = string
|
||||
|
||||
export const applyRolePreset = async (member: GuildMember, presetName: PresetKey, untilMs: number) => {
|
||||
export const applyRolePreset = async (member: GuildMember, presetName: PresetKey, expires: number) => {
|
||||
const afterInsert = await applyRolesUsingPreset(presetName, member, true)
|
||||
const until = untilMs === Infinity ? null : Math.ceil(untilMs / 1000)
|
||||
const until = expires === Infinity ? null : Math.ceil(expires / 1000)
|
||||
|
||||
await database
|
||||
.insert(appliedPresets)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
export const parseDuration = (duration: string) => {
|
||||
if (!duration.length) return Number.NaN
|
||||
const matches = duration.match(/(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?/)!
|
||||
const matches = duration.match(/(?:(\d+y)?(\d+M)?(\d+w)?(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)/)!
|
||||
|
||||
const [, days, hours, minutes, seconds] = matches.map(Number)
|
||||
const [, years, months, weeks, days, hours, minutes, seconds] = matches.map(Number)
|
||||
return (
|
||||
(days || 0) * 24 * 60 * 60 * 1000 +
|
||||
(hours || 0) * 60 * 60 * 1000 +
|
||||
(minutes || 0) * 60 * 1000 +
|
||||
(seconds || 0) * 1000
|
||||
(years || 0) * 290304e5 +
|
||||
(months || 0) * 24192e5 +
|
||||
(weeks || 0) * 6048e5 +
|
||||
(days || 0) * 864e5 +
|
||||
(hours || 0) * 36e5 +
|
||||
(minutes || 0) * 6e4 +
|
||||
(seconds || 0) * 1e3
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { type RawData, WebSocket } from 'ws'
|
||||
* This is the only relevant class for the time being. But in the future, there may be more classes to handle different protocols of the API.
|
||||
*/
|
||||
export class ClientWebSocketManager {
|
||||
readonly url: string
|
||||
url: string
|
||||
timeout: number
|
||||
|
||||
connecting = false
|
||||
@@ -33,6 +33,21 @@ export class ClientWebSocketManager {
|
||||
this.timeout = options.timeout ?? 10000
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL to connect to
|
||||
*
|
||||
* **Requires a reconnect to take effect**
|
||||
*/
|
||||
async setOptions({ url, timeout }: Partial<ClientWebSocketManagerOptions>, autoReconnect = true) {
|
||||
if (url) this.url = url
|
||||
this.timeout = timeout ?? this.timeout
|
||||
|
||||
if (autoReconnect) {
|
||||
this.disconnect(true)
|
||||
await this.connect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the WebSocket API
|
||||
* @returns A promise that resolves when the client is ready
|
||||
|
||||
Reference in New Issue
Block a user