mirror of
https://github.com/discordeno/discordeno.git
synced 2026-05-21 02:40:08 +00:00
formatter: Use semicolons (#4686)
I prefer semicolors, they also help avoiding certain pitfalls in JavaScript/TypeScript, such as the following code sample: ```js const xyz = "test" (something.else as string) = "another" ``` This results in a TypeError: "test" is not a function, this is because js thinks we are trying to call the string "test" as a function. To fix this it requires a `;` somewhere before the `(`, such as `;(something ... ` which in my opinion is ugly and less clean overall.
This commit is contained in:
14
.mocharc.cjs
14
.mocharc.cjs
@@ -1,23 +1,23 @@
|
||||
// @ts-check
|
||||
|
||||
// If we are running in Bun or Deno, they have native TypeScript support with .js imports, node requires .ts imports
|
||||
const supportsTypescript = 'Bun' in globalThis || 'Deno' in globalThis
|
||||
const supportsTypescript = 'Bun' in globalThis || 'Deno' in globalThis;
|
||||
|
||||
/** @type {import("mocha").MochaInstanceOptions & Record<string, unknown>} */
|
||||
const mochaConfig = {
|
||||
timeout: 2000,
|
||||
'watch-extensions': 'ts',
|
||||
'watch-files': ['src', 'tests'],
|
||||
}
|
||||
};
|
||||
|
||||
if (!supportsTypescript) {
|
||||
mochaConfig.require = ['ts-node/register']
|
||||
mochaConfig.require = ['ts-node/register'];
|
||||
|
||||
// Node options
|
||||
mochaConfig.loader = ['ts-node/esm']
|
||||
mochaConfig.loader = ['ts-node/esm'];
|
||||
// Node will output a ExperimentalWarning about --loader (--experimental-loader) and a DeprecationWarning because ts-node uses fs.Stat
|
||||
mochaConfig['no-warnings'] = true
|
||||
mochaConfig['enable-source-maps'] = true
|
||||
mochaConfig['no-warnings'] = true;
|
||||
mochaConfig['enable-source-maps'] = true;
|
||||
}
|
||||
|
||||
module.exports = mochaConfig
|
||||
module.exports = mochaConfig;
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "asNeeded",
|
||||
"quoteStyle": "single"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import fastifyEnv from '@fastify/env'
|
||||
import fastifyHelmet from '@fastify/helmet'
|
||||
import fastifyMultipart from '@fastify/multipart'
|
||||
import fastify, { type FastifyInstance } from 'fastify'
|
||||
import fastifyEnv from '@fastify/env';
|
||||
import fastifyHelmet from '@fastify/helmet';
|
||||
import fastifyMultipart from '@fastify/multipart';
|
||||
import fastify, { type FastifyInstance } from 'fastify';
|
||||
|
||||
export const buildFastifyApp = async (): Promise<FastifyInstance> => {
|
||||
const app = await fastify()
|
||||
const app = await fastify();
|
||||
|
||||
await app.register(fastifyEnv, {
|
||||
schema: {
|
||||
@@ -25,28 +25,28 @@ export const buildFastifyApp = async (): Promise<FastifyInstance> => {
|
||||
},
|
||||
required: ['DISCORD_TOKEN', 'AUTHORIZATION_TOKEN'],
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
await app.register(fastifyHelmet)
|
||||
app.register(fastifyMultipart, { attachFieldsToBody: true })
|
||||
await app.register(fastifyHelmet);
|
||||
app.register(fastifyMultipart, { attachFieldsToBody: true });
|
||||
|
||||
app.addHook('onRequest', async (request, reply) => {
|
||||
if (request.headers.authorization !== request.server.config.AUTHORIZATION_TOKEN) {
|
||||
reply.status(401).send({
|
||||
message: 'Credentials not valid.',
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return app
|
||||
}
|
||||
return app;
|
||||
};
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
config: {
|
||||
HOST: string
|
||||
DISCORD_TOKEN: string
|
||||
AUTHORIZATION_TOKEN: string
|
||||
}
|
||||
HOST: string;
|
||||
DISCORD_TOKEN: string;
|
||||
AUTHORIZATION_TOKEN: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +1,79 @@
|
||||
import { createRestManager, type RequestMethods } from '@discordeno/rest'
|
||||
import type { MultipartFile, MultipartValue } from '@fastify/multipart'
|
||||
import { buildFastifyApp } from './fastify.js'
|
||||
import { createRestManager, type RequestMethods } from '@discordeno/rest';
|
||||
import type { MultipartFile, MultipartValue } from '@fastify/multipart';
|
||||
import { buildFastifyApp } from './fastify.js';
|
||||
|
||||
const app = await buildFastifyApp()
|
||||
const app = await buildFastifyApp();
|
||||
|
||||
if (!app.config.DISCORD_TOKEN || !app.config.AUTHORIZATION_TOKEN) {
|
||||
console.error('Missing environment variables. Both DISCORD_TOKEN and AUTHORIZATION_TOKEN are required.')
|
||||
process.exit(1)
|
||||
console.error('Missing environment variables. Both DISCORD_TOKEN and AUTHORIZATION_TOKEN are required.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const discordRestManager = createRestManager({
|
||||
token: app.config.DISCORD_TOKEN,
|
||||
})
|
||||
});
|
||||
|
||||
app.get('/timecheck', async (_request, reply) => {
|
||||
reply.status(200).send({
|
||||
message: Date.now(),
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
app.all('/*', async (request, reply) => {
|
||||
let url = request.originalUrl
|
||||
let url = request.originalUrl;
|
||||
|
||||
if (url.startsWith('/v')) {
|
||||
url = url.slice(url.indexOf('/', 2))
|
||||
url = url.slice(url.indexOf('/', 2));
|
||||
}
|
||||
|
||||
const isMultipart = request.headers['content-type']?.startsWith('multipart/form-data')
|
||||
const body = request.method !== 'GET' && request.method !== 'DELETE' ? request.body : undefined
|
||||
const isMultipart = request.headers['content-type']?.startsWith('multipart/form-data');
|
||||
const body = request.method !== 'GET' && request.method !== 'DELETE' ? request.body : undefined;
|
||||
|
||||
try {
|
||||
const result = await discordRestManager.makeRequest(request.method as RequestMethods, url, {
|
||||
body: isMultipart && body ? await parseMultiformBody(body) : body,
|
||||
})
|
||||
});
|
||||
|
||||
if (result) {
|
||||
reply.status(200).send(result)
|
||||
reply.status(200).send(result);
|
||||
} else {
|
||||
reply.status(204).send({})
|
||||
reply.status(204).send({});
|
||||
}
|
||||
} catch (error) {
|
||||
app.log.error(error)
|
||||
app.log.error(error);
|
||||
|
||||
reply.status(500).send({
|
||||
message: error,
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
try {
|
||||
await app.listen({
|
||||
host: app.config.HOST,
|
||||
port: 8000,
|
||||
})
|
||||
console.log(`Proxy listening on port 8000`)
|
||||
});
|
||||
console.log(`Proxy listening on port 8000`);
|
||||
} catch (error) {
|
||||
app.log.error(error)
|
||||
process.exit(1)
|
||||
app.log.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function parseMultiformBody(body: unknown): Promise<FormData> {
|
||||
const form = new FormData()
|
||||
const form = new FormData();
|
||||
|
||||
if (typeof body !== 'object' || !body) return form
|
||||
if (typeof body !== 'object' || !body) return form;
|
||||
|
||||
for (const objectValue of Object.values(body)) {
|
||||
const value = objectValue as MultipartFile | MultipartValue
|
||||
const value = objectValue as MultipartFile | MultipartValue;
|
||||
|
||||
if (value.type === 'file') {
|
||||
form.append(value.fieldname, new Blob([Uint8Array.from(await value.toBuffer())]), value.filename)
|
||||
form.append(value.fieldname, new Blob([Uint8Array.from(await value.toBuffer())]), value.filename);
|
||||
}
|
||||
if (value.type === 'field' && typeof value.value === 'string') {
|
||||
form.append(value.fieldname, value.value)
|
||||
form.append(value.fieldname, value.value);
|
||||
}
|
||||
}
|
||||
|
||||
return form
|
||||
return form;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createBot, type logger as discordenoLogger, Intents, LogDepth } from '@discordeno/bot'
|
||||
import { createProxyCache } from 'dd-cache-proxy'
|
||||
import { configs } from './config.js'
|
||||
import { createBot, type logger as discordenoLogger, Intents, LogDepth } from '@discordeno/bot';
|
||||
import { createProxyCache } from 'dd-cache-proxy';
|
||||
import { configs } from './config.js';
|
||||
|
||||
const rawBot = createBot({
|
||||
token: configs.token,
|
||||
@@ -38,7 +38,7 @@ const rawBot = createBot({
|
||||
discriminator: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export const bot = createProxyCache(rawBot, {
|
||||
desiredProps: {
|
||||
@@ -50,7 +50,7 @@ export const bot = createProxyCache(rawBot, {
|
||||
role: true,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// By default, bot.logger will use an instance of the logger from @discordeno/bot, this logger supports depth and we need to change it, so we need to say to TS that we know what we are doing with as
|
||||
;(bot.logger as typeof discordenoLogger).setDepth(LogDepth.Full)
|
||||
(bot.logger as typeof discordenoLogger).setDepth(LogDepth.Full);
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { type ApplicationCommandOption, type ApplicationCommandTypes, Collection } from '@discordeno/bot'
|
||||
import type { bot } from './bot.js'
|
||||
import { type ApplicationCommandOption, type ApplicationCommandTypes, Collection } from '@discordeno/bot';
|
||||
import type { bot } from './bot.js';
|
||||
|
||||
export const commands = new Collection<string, Command>()
|
||||
export const commands = new Collection<string, Command>();
|
||||
|
||||
export function createCommand(command: Command): void {
|
||||
commands.set(command.name, command)
|
||||
commands.set(command.name, command);
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
/** The name of this command. */
|
||||
name: string
|
||||
name: string;
|
||||
/** What does this command do? */
|
||||
description: string
|
||||
description: string;
|
||||
/** The type of command this is. */
|
||||
type: ApplicationCommandTypes
|
||||
type: ApplicationCommandTypes;
|
||||
/** The options for this command */
|
||||
options?: ApplicationCommandOption[]
|
||||
options?: ApplicationCommandOption[];
|
||||
/** This will be executed when the command is run. */
|
||||
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: Record<string, unknown>) => unknown
|
||||
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: Record<string, unknown>) => unknown;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ApplicationCommandTypes, createEmbeds, snowflakeToTimestamp } from '@discordeno/bot'
|
||||
import { createCommand } from '../commands.js'
|
||||
import { ApplicationCommandTypes, createEmbeds, snowflakeToTimestamp } from '@discordeno/bot';
|
||||
import { createCommand } from '../commands.js';
|
||||
|
||||
createCommand({
|
||||
name: 'ping',
|
||||
description: 'See if the bot latency is okay',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
async execute(interaction) {
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
|
||||
|
||||
const embeds = createEmbeds().setTitle(`The bot ping is ${ping}ms`)
|
||||
const embeds = createEmbeds().setTitle(`The bot ping is ${ping}ms`);
|
||||
|
||||
await interaction.respond({ embeds })
|
||||
await interaction.respond({ embeds });
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, createEmbeds, type Member, Permissions, type User } from '@discordeno/bot'
|
||||
import { bot } from '../bot.js'
|
||||
import { createCommand } from '../commands.js'
|
||||
import { calculateMemberPermissions } from '../utils/permissions.js'
|
||||
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, createEmbeds, type Member, Permissions, type User } from '@discordeno/bot';
|
||||
import { bot } from '../bot.js';
|
||||
import { createCommand } from '../commands.js';
|
||||
import { calculateMemberPermissions } from '../utils/permissions.js';
|
||||
|
||||
createCommand({
|
||||
name: 'warn',
|
||||
@@ -23,58 +23,58 @@ createCommand({
|
||||
],
|
||||
async execute(interaction, options) {
|
||||
if (!interaction.guildId || !interaction.member) {
|
||||
await interaction.respond('This command can only be ran in guilds')
|
||||
return
|
||||
await interaction.respond('This command can only be ran in guilds');
|
||||
return;
|
||||
}
|
||||
|
||||
// Type based on the options declared above
|
||||
const { user, reason } = options as { user: UserResolved; reason?: string }
|
||||
const { user, reason } = options as { user: UserResolved; reason?: string };
|
||||
|
||||
const guild = await bot.cache.guilds.get(interaction.guildId)
|
||||
const guild = await bot.cache.guilds.get(interaction.guildId);
|
||||
|
||||
if (!guild || !guild.roles) {
|
||||
await interaction.respond('An error has occurred')
|
||||
return
|
||||
await interaction.respond('An error has occurred');
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.defer()
|
||||
await interaction.defer();
|
||||
|
||||
const perms = new Permissions(await calculateMemberPermissions(guild, interaction.member))
|
||||
const perms = new Permissions(await calculateMemberPermissions(guild, interaction.member));
|
||||
|
||||
const adminPerm = perms.has('ADMINISTRATOR')
|
||||
const kickMembersPerm = adminPerm || perms.has('KICK_MEMBERS')
|
||||
const adminPerm = perms.has('ADMINISTRATOR');
|
||||
const kickMembersPerm = adminPerm || perms.has('KICK_MEMBERS');
|
||||
|
||||
if (!kickMembersPerm) {
|
||||
await interaction.respond("You don't have the necessary permissions to warn a members (this command requires `Kick members`)")
|
||||
return
|
||||
await interaction.respond("You don't have the necessary permissions to warn a members (this command requires `Kick members`)");
|
||||
return;
|
||||
}
|
||||
|
||||
const embeds = createEmbeds()
|
||||
.setTitle('Warned User:')
|
||||
.setDescription(`User: <@${user.user.id}>\nReason: ${reason}`)
|
||||
.setColor(0x00ff00)
|
||||
.setTimestamp(Date.now())
|
||||
.setTimestamp(Date.now());
|
||||
|
||||
const warnEmbeds = createEmbeds()
|
||||
.setTitle('Warning:')
|
||||
.setDescription(`You have been warned in **${guild.name}** for \`${reason}\``)
|
||||
.setTimestamp(Date.now())
|
||||
.setTimestamp(Date.now());
|
||||
|
||||
try {
|
||||
const dmChannel = await bot.helpers.getDmChannel(user.user.id)
|
||||
await bot.helpers.sendMessage(dmChannel.id, { embeds: warnEmbeds })
|
||||
const dmChannel = await bot.helpers.getDmChannel(user.user.id);
|
||||
await bot.helpers.sendMessage(dmChannel.id, { embeds: warnEmbeds });
|
||||
} catch (error) {
|
||||
bot.logger.error(`There was an error in the warn command:`, error)
|
||||
bot.logger.error(`There was an error in the warn command:`, error);
|
||||
|
||||
await interaction.respond(`Could not warn user <@${user.user.id}> | They likely do not have their DMs open.`)
|
||||
return
|
||||
await interaction.respond(`Could not warn user <@${user.user.id}> | They likely do not have their DMs open.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.respond({ embeds })
|
||||
await interaction.respond({ embeds });
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
interface UserResolved {
|
||||
user: User
|
||||
member?: Member
|
||||
user: User;
|
||||
member?: Member;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const token = process.env.TOKEN
|
||||
const token = process.env.TOKEN;
|
||||
|
||||
if (!token) throw new Error('Missing TOKEN environment variable')
|
||||
if (!token) throw new Error('Missing TOKEN environment variable');
|
||||
|
||||
export const configs: Config = {
|
||||
token,
|
||||
}
|
||||
};
|
||||
|
||||
export interface Config {
|
||||
token: string
|
||||
token: string;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { commandOptionsParser, InteractionTypes } from '@discordeno/bot'
|
||||
import { bot } from '../bot.js'
|
||||
import { commands } from '../commands.js'
|
||||
import { commandOptionsParser, InteractionTypes } from '@discordeno/bot';
|
||||
import { bot } from '../bot.js';
|
||||
import { commands } from '../commands.js';
|
||||
|
||||
bot.events.interactionCreate = async (interaction) => {
|
||||
if (!interaction.data || interaction.type !== InteractionTypes.ApplicationCommand) return
|
||||
if (!interaction.data || interaction.type !== InteractionTypes.ApplicationCommand) return;
|
||||
|
||||
const command = commands.get(interaction.data.name)
|
||||
const command = commands.get(interaction.data.name);
|
||||
|
||||
if (!command) {
|
||||
bot.logger.error(`Command ${interaction.data.name} not found`)
|
||||
return
|
||||
bot.logger.error(`Command ${interaction.data.name} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const options = commandOptionsParser(interaction)
|
||||
const options = commandOptionsParser(interaction);
|
||||
|
||||
try {
|
||||
await command.execute(interaction, options)
|
||||
await command.execute(interaction, options);
|
||||
} catch (error) {
|
||||
bot.logger.error(`There was an error running the ${command.name} command.`, error)
|
||||
}
|
||||
bot.logger.error(`There was an error running the ${command.name} command.`, error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { bot } from '../bot.js'
|
||||
import { bot } from '../bot.js';
|
||||
|
||||
bot.events.ready = ({ user, shardId }) => {
|
||||
if (shardId === bot.gateway.lastShardId) {
|
||||
// All shards are ready
|
||||
bot.logger.info(`Successfully connected to the gateway as ${user.username}#${user.discriminator}`)
|
||||
}
|
||||
bot.logger.info(`Successfully connected to the gateway as ${user.username}#${user.discriminator}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'dotenv/config'
|
||||
import 'dotenv/config';
|
||||
|
||||
import { bot } from './bot.js'
|
||||
import importDirectory from './utils/loader.js'
|
||||
import { bot } from './bot.js';
|
||||
import importDirectory from './utils/loader.js';
|
||||
|
||||
bot.logger.info('Starting bot...')
|
||||
bot.logger.info('Starting bot...');
|
||||
|
||||
bot.logger.info('Loading commands...')
|
||||
await importDirectory('./dist/commands')
|
||||
bot.logger.info('Loading commands...');
|
||||
await importDirectory('./dist/commands');
|
||||
|
||||
bot.logger.info('Loading events...')
|
||||
await importDirectory('./dist/events')
|
||||
bot.logger.info('Loading events...');
|
||||
await importDirectory('./dist/events');
|
||||
|
||||
await bot.start()
|
||||
await bot.start();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'dotenv/config'
|
||||
import 'dotenv/config';
|
||||
|
||||
import { bot } from './bot.js'
|
||||
import importDirectory from './utils/loader.js'
|
||||
import { updateApplicationCommands } from './utils/updateCommands.js'
|
||||
import { bot } from './bot.js';
|
||||
import importDirectory from './utils/loader.js';
|
||||
import { updateApplicationCommands } from './utils/updateCommands.js';
|
||||
|
||||
bot.logger.info('Loading commands...')
|
||||
await importDirectory('./dist/commands')
|
||||
bot.logger.info('Loading commands...');
|
||||
await importDirectory('./dist/commands');
|
||||
|
||||
bot.logger.info('Updating commands...')
|
||||
await updateApplicationCommands()
|
||||
bot.logger.info('Updating commands...');
|
||||
await updateApplicationCommands();
|
||||
|
||||
bot.logger.info('Done!')
|
||||
bot.logger.info('Done!');
|
||||
|
||||
// We need to manually exit as the REST Manager has timeouts that will keep NodeJS alive
|
||||
process.exit()
|
||||
process.exit();
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import { logger } from '@discordeno/bot'
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { logger } from '@discordeno/bot';
|
||||
|
||||
export default async function importDirectory(folder: string): Promise<void> {
|
||||
const files = await readdir(folder, { recursive: true })
|
||||
const files = await readdir(folder, { recursive: true });
|
||||
|
||||
for (const filename of files) {
|
||||
if (!filename.endsWith('.js')) continue
|
||||
if (!filename.endsWith('.js')) continue;
|
||||
|
||||
// Using `file://` and `process.cwd()` to avoid weird issues with relative paths and/or Windows
|
||||
await import(`file://${process.cwd()}/${folder}/${filename}`).catch((x) =>
|
||||
logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import assert from 'node:assert'
|
||||
import { BitwisePermissionFlags } from '@discordeno/bot'
|
||||
import type { bot } from '../bot.js'
|
||||
import assert from 'node:assert';
|
||||
import { BitwisePermissionFlags } from '@discordeno/bot';
|
||||
import type { bot } from '../bot.js';
|
||||
|
||||
export async function calculateMemberPermissions(
|
||||
guild: typeof bot.transformers.$inferredTypes.guild | typeof bot.cache.$inferredTypes.guild,
|
||||
member: typeof bot.transformers.$inferredTypes.member,
|
||||
) {
|
||||
if (member.id === guild.ownerId) return 8n
|
||||
if (member.id === guild.ownerId) return 8n;
|
||||
|
||||
let permissions = guild.roles?.get(guild.id)?.permissions.bitfield
|
||||
const rolePerms = member.roles.map((x) => guild.roles?.get(x)?.permissions.bitfield).filter((x): x is bigint => x !== undefined)
|
||||
let permissions = guild.roles?.get(guild.id)?.permissions.bitfield;
|
||||
const rolePerms = member.roles.map((x) => guild.roles?.get(x)?.permissions.bitfield).filter((x): x is bigint => x !== undefined);
|
||||
|
||||
// Small hack to avoid calling assert with 0n
|
||||
if (permissions === undefined) assert(permissions)
|
||||
if (permissions === undefined) assert(permissions);
|
||||
|
||||
for (const rolePerm of rolePerms) {
|
||||
permissions |= rolePerm
|
||||
permissions |= rolePerm;
|
||||
}
|
||||
|
||||
if ((permissions & BigInt(BitwisePermissionFlags.ADMINISTRATOR)) === BigInt(BitwisePermissionFlags.ADMINISTRATOR)) return 8n
|
||||
if ((permissions & BigInt(BitwisePermissionFlags.ADMINISTRATOR)) === BigInt(BitwisePermissionFlags.ADMINISTRATOR)) return 8n;
|
||||
|
||||
return permissions
|
||||
return permissions;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { bot } from '../bot.js'
|
||||
import { commands } from '../commands.js'
|
||||
import { bot } from '../bot.js';
|
||||
import { commands } from '../commands.js';
|
||||
|
||||
export async function updateApplicationCommands(): Promise<void> {
|
||||
await bot.helpers.upsertGlobalApplicationCommands(commands.array())
|
||||
await bot.helpers.upsertGlobalApplicationCommands(commands.array());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createBot, Intents } from '@discordeno/bot'
|
||||
import { createProxyCache } from 'dd-cache-proxy'
|
||||
import { configs } from './config.js'
|
||||
import { createBot, Intents } from '@discordeno/bot';
|
||||
import { createProxyCache } from 'dd-cache-proxy';
|
||||
import { configs } from './config.js';
|
||||
|
||||
const rawBot = createBot({
|
||||
token: configs.token,
|
||||
@@ -22,7 +22,7 @@ const rawBot = createBot({
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export const bot = createProxyCache(rawBot, {
|
||||
desiredProps: {
|
||||
@@ -32,4 +32,4 @@ export const bot = createProxyCache(rawBot, {
|
||||
guild: true,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { type ApplicationCommandOption, type ApplicationCommandTypes, Collection } from '@discordeno/bot'
|
||||
import type { bot } from './bot.js'
|
||||
import { type ApplicationCommandOption, type ApplicationCommandTypes, Collection } from '@discordeno/bot';
|
||||
import type { bot } from './bot.js';
|
||||
|
||||
export const commands = new Collection<string, Command>()
|
||||
export const commands = new Collection<string, Command>();
|
||||
|
||||
export function createCommand(command: Command): void {
|
||||
commands.set(command.name, command)
|
||||
commands.set(command.name, command);
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
name: string
|
||||
description: string
|
||||
usage?: string[]
|
||||
options?: ApplicationCommandOption[]
|
||||
type: ApplicationCommandTypes
|
||||
name: string;
|
||||
description: string;
|
||||
usage?: string[];
|
||||
options?: ApplicationCommandOption[];
|
||||
type: ApplicationCommandTypes;
|
||||
/** Defaults to `Guild` */
|
||||
scope?: 'Global' | 'Guild'
|
||||
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction) => unknown
|
||||
subcommands?: Array<SubCommandGroup | SubCommand>
|
||||
scope?: 'Global' | 'Guild';
|
||||
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction) => unknown;
|
||||
subcommands?: Array<SubCommandGroup | SubCommand>;
|
||||
}
|
||||
|
||||
export type SubCommand = Omit<Command, 'subcommands'>
|
||||
export type SubCommand = Omit<Command, 'subcommands'>;
|
||||
|
||||
export interface SubCommandGroup {
|
||||
name: string
|
||||
subCommands: SubCommand[]
|
||||
name: string;
|
||||
subCommands: SubCommand[];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApplicationCommandTypes, snowflakeToTimestamp } from '@discordeno/bot'
|
||||
import { createCommand } from '../commands.js'
|
||||
import { humanizeMilliseconds } from '../utils/helpers.js'
|
||||
import { ApplicationCommandTypes, snowflakeToTimestamp } from '@discordeno/bot';
|
||||
import { createCommand } from '../commands.js';
|
||||
import { humanizeMilliseconds } from '../utils/helpers.js';
|
||||
|
||||
createCommand({
|
||||
name: 'ping',
|
||||
@@ -8,8 +8,8 @@ createCommand({
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
scope: 'Global',
|
||||
async execute(interaction) {
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
|
||||
|
||||
await interaction.respond(`🏓 Pong! Ping ${ping}ms (${humanizeMilliseconds(ping)})`)
|
||||
await interaction.respond(`🏓 Pong! Ping ${ping}ms (${humanizeMilliseconds(ping)})`);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const token = process.env.BOT_TOKEN
|
||||
const token = process.env.BOT_TOKEN;
|
||||
|
||||
if (!token) throw new Error('Missing BOT_TOKEN environment variable')
|
||||
if (!token) throw new Error('Missing BOT_TOKEN environment variable');
|
||||
|
||||
export const configs: Config = {
|
||||
/** Get token from ENV variable */
|
||||
token,
|
||||
}
|
||||
};
|
||||
|
||||
export interface Config {
|
||||
token: string
|
||||
token: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { bot } from '../bot.js'
|
||||
import { updateGuildCommands } from '../utils/helpers.js'
|
||||
import { bot } from '../bot.js';
|
||||
import { updateGuildCommands } from '../utils/helpers.js';
|
||||
|
||||
bot.events.guildCreate = async (guild) => await updateGuildCommands(guild)
|
||||
bot.events.guildCreate = async (guild) => await updateGuildCommands(guild);
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
import { ApplicationCommandOptionTypes, hasProperty } from '@discordeno/bot'
|
||||
import chalk from 'chalk'
|
||||
import { bot } from '../bot.js'
|
||||
import { commands } from '../commands.js'
|
||||
import { getGuildFromId, isSubCommand, isSubCommandGroup } from '../utils/helpers.js'
|
||||
import { createLogger } from '../utils/logger.js'
|
||||
import { ApplicationCommandOptionTypes, hasProperty } from '@discordeno/bot';
|
||||
import chalk from 'chalk';
|
||||
import { bot } from '../bot.js';
|
||||
import { commands } from '../commands.js';
|
||||
import { getGuildFromId, isSubCommand, isSubCommandGroup } from '../utils/helpers.js';
|
||||
import { createLogger } from '../utils/logger.js';
|
||||
|
||||
const logger = createLogger({ name: 'Event: InteractionCreate' })
|
||||
const logger = createLogger({ name: 'Event: InteractionCreate' });
|
||||
|
||||
bot.events.interactionCreate = async (interaction) => {
|
||||
if (!interaction.data || !interaction.id) return
|
||||
if (!interaction.data || !interaction.id) return;
|
||||
|
||||
let guildName = 'Direct Message'
|
||||
let guild = {} as typeof bot.transformers.$inferredTypes.guild
|
||||
let guildName = 'Direct Message';
|
||||
let guild = {} as typeof bot.transformers.$inferredTypes.guild;
|
||||
|
||||
// Set guild, if there was an error getting the guild, then just say it was a DM. (What else are we going to do?)
|
||||
if (interaction.guildId) {
|
||||
const guildOrVoid = await getGuildFromId(interaction.guildId).catch((err) => {
|
||||
logger.error(err)
|
||||
})
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
if (guildOrVoid) {
|
||||
guild = guildOrVoid
|
||||
guildName = guild.name
|
||||
guild = guildOrVoid;
|
||||
guildName = guild.name;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.white(`Trigger`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
);
|
||||
|
||||
let command = commands.get(interaction.data.name)
|
||||
let command = commands.get(interaction.data.name);
|
||||
|
||||
if (!command) {
|
||||
logger.warn(
|
||||
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.yellow(`Not Found`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
);
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (interaction.data.options?.[0]) {
|
||||
const optionType = interaction.data.options[0].type
|
||||
const optionType = interaction.data.options[0].type;
|
||||
|
||||
if (optionType === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
// Check if command has subcommand and handle types
|
||||
if (!command.subcommands) return
|
||||
if (!command.subcommands) return;
|
||||
|
||||
// Try to find the subcommand group
|
||||
const subCommandGroup = command.subcommands?.find((command) => command.name === interaction.data?.options?.[0].name)
|
||||
if (!subCommandGroup) return
|
||||
const subCommandGroup = command.subcommands?.find((command) => command.name === interaction.data?.options?.[0].name);
|
||||
if (!subCommandGroup) return;
|
||||
|
||||
if (isSubCommand(subCommandGroup)) return
|
||||
if (isSubCommand(subCommandGroup)) return;
|
||||
|
||||
// Get name of the command which we are looking for
|
||||
const targetCmdName = interaction.data.options?.[0].options?.[0].name ?? interaction.data.options?.[0].options?.[0].name
|
||||
if (!targetCmdName) return
|
||||
const targetCmdName = interaction.data.options?.[0].options?.[0].name ?? interaction.data.options?.[0].options?.[0].name;
|
||||
if (!targetCmdName) return;
|
||||
|
||||
// Try to find the command
|
||||
command = subCommandGroup.subCommands.find((c) => c.name === targetCmdName)
|
||||
command = subCommandGroup.subCommands.find((c) => c.name === targetCmdName);
|
||||
}
|
||||
|
||||
if (optionType === ApplicationCommandOptionTypes.SubCommand) {
|
||||
// Check if command has subcommand and handle types
|
||||
if (!command?.subcommands) return
|
||||
if (!command?.subcommands) return;
|
||||
|
||||
// Try to find the command
|
||||
const found = command.subcommands.find((command) => command.name === interaction.data?.options?.[0].name)
|
||||
if (!found) return
|
||||
const found = command.subcommands.find((command) => command.name === interaction.data?.options?.[0].name);
|
||||
if (!found) return;
|
||||
|
||||
if (isSubCommandGroup(found)) return
|
||||
if (isSubCommandGroup(found)) return;
|
||||
|
||||
command = found
|
||||
command = found;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!command) throw new Error('Not command could be found')
|
||||
if (!command) throw new Error('Not command could be found');
|
||||
|
||||
await command.execute(interaction)
|
||||
await command.execute(interaction);
|
||||
|
||||
logger.info(
|
||||
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.green(`Success`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.red(`Error`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
);
|
||||
|
||||
if (typeof err !== 'object' || !err || !hasProperty(err, 'message') || err.message === 'Not command could be found') return
|
||||
if (typeof err !== 'object' || !err || !hasProperty(err, 'message') || err.message === 'Not command could be found') return;
|
||||
|
||||
logger.error(err)
|
||||
}
|
||||
logger.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ActivityTypes } from '@discordeno/bot'
|
||||
import { bot } from '../bot.js'
|
||||
import { createLogger } from '../utils/logger.js'
|
||||
import { ActivityTypes } from '@discordeno/bot';
|
||||
import { bot } from '../bot.js';
|
||||
import { createLogger } from '../utils/logger.js';
|
||||
|
||||
const logger = createLogger({ name: 'Event: Ready' })
|
||||
const logger = createLogger({ name: 'Event: Ready' });
|
||||
|
||||
bot.events.ready = async ({ shardId }) => {
|
||||
logger.info('Bot Ready')
|
||||
logger.info('Bot Ready');
|
||||
|
||||
await bot.gateway.editShardStatus(shardId, {
|
||||
status: 'online',
|
||||
@@ -18,5 +18,5 @@ bot.events.ready = async ({ shardId }) => {
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'dotenv/config'
|
||||
import 'dotenv/config';
|
||||
|
||||
import { bot } from './bot.js'
|
||||
import importDirectory from './utils/loader.js'
|
||||
import logger from './utils/logger.js'
|
||||
import { bot } from './bot.js';
|
||||
import importDirectory from './utils/loader.js';
|
||||
import logger from './utils/logger.js';
|
||||
|
||||
logger.info('Starting bot...')
|
||||
logger.info('Starting bot...');
|
||||
|
||||
logger.info('Loading commands...')
|
||||
await importDirectory('./dist/commands')
|
||||
logger.info('Loading commands...');
|
||||
await importDirectory('./dist/commands');
|
||||
|
||||
logger.info('Loading events...')
|
||||
await importDirectory('./dist/events')
|
||||
logger.info('Loading events...');
|
||||
await importDirectory('./dist/events');
|
||||
|
||||
await bot.start()
|
||||
await bot.start();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'dotenv/config'
|
||||
import 'dotenv/config';
|
||||
|
||||
import { bot } from './bot.js'
|
||||
import { updateCommands } from './utils/helpers.js'
|
||||
import importDirectory from './utils/loader.js'
|
||||
import { bot } from './bot.js';
|
||||
import { updateCommands } from './utils/helpers.js';
|
||||
import importDirectory from './utils/loader.js';
|
||||
|
||||
bot.logger.info('Loading commands...')
|
||||
await importDirectory('./dist/commands')
|
||||
bot.logger.info('Loading commands...');
|
||||
await importDirectory('./dist/commands');
|
||||
|
||||
bot.logger.info('Updating commands...')
|
||||
await updateCommands()
|
||||
bot.logger.info('Updating commands...');
|
||||
await updateCommands();
|
||||
|
||||
bot.logger.info('Done!')
|
||||
bot.logger.info('Done!');
|
||||
|
||||
// We need to manually exit as the REST Manager has timeouts that will keep NodeJS alive
|
||||
process.exit()
|
||||
process.exit();
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { type CreateApplicationCommand, hasProperty } from '@discordeno/bot'
|
||||
import { bot } from '../bot.js'
|
||||
import { commands, type SubCommand, type SubCommandGroup } from '../commands.js'
|
||||
import { createLogger } from './logger.js'
|
||||
import { type CreateApplicationCommand, hasProperty } from '@discordeno/bot';
|
||||
import { bot } from '../bot.js';
|
||||
import { commands, type SubCommand, type SubCommandGroup } from '../commands.js';
|
||||
import { createLogger } from './logger.js';
|
||||
|
||||
const logger = createLogger({ name: 'Helpers' })
|
||||
const logger = createLogger({ name: 'Helpers' });
|
||||
|
||||
/** This function will update all commands, or the defined scope */
|
||||
export async function updateCommands(scope?: 'Guild' | 'Global'): Promise<void> {
|
||||
const globalCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = []
|
||||
const perGuildCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = []
|
||||
const globalCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = [];
|
||||
const perGuildCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = [];
|
||||
|
||||
for (const command of commands.values()) {
|
||||
if (command.scope === 'Guild') {
|
||||
@@ -17,34 +17,34 @@ export async function updateCommands(scope?: 'Guild' | 'Global'): Promise<void>
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
})
|
||||
});
|
||||
} else {
|
||||
globalCommands.push({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (globalCommands.length && (scope === 'Global' || scope === undefined)) {
|
||||
logger.info('Updating Global Commands, changes should apply in short...')
|
||||
await bot.helpers.upsertGlobalApplicationCommands(globalCommands).catch(logger.error)
|
||||
logger.info('Updating Global Commands, changes should apply in short...');
|
||||
await bot.helpers.upsertGlobalApplicationCommands(globalCommands).catch(logger.error);
|
||||
}
|
||||
|
||||
if (perGuildCommands.length && (scope === 'Guild' || scope === undefined)) {
|
||||
await Promise.all(
|
||||
bot.cache.guilds.memory.map(async (guild) => {
|
||||
await bot.helpers.upsertGuildApplicationCommands(guild.id, perGuildCommands)
|
||||
await bot.helpers.upsertGuildApplicationCommands(guild.id, perGuildCommands);
|
||||
}),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Update commands for a guild */
|
||||
export async function updateGuildCommands(guild: typeof bot.transformers.$inferredTypes.guild): Promise<void> {
|
||||
const perGuildCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = []
|
||||
const perGuildCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = [];
|
||||
|
||||
for (const command of commands.values()) {
|
||||
if (command.scope === 'Guild') {
|
||||
@@ -53,49 +53,49 @@ export async function updateGuildCommands(guild: typeof bot.transformers.$inferr
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (perGuildCommands.length) {
|
||||
await bot.helpers.upsertGuildApplicationCommands(guild.id, perGuildCommands)
|
||||
await bot.helpers.upsertGuildApplicationCommands(guild.id, perGuildCommands);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGuildFromId(guildId: bigint) {
|
||||
const cached = await bot.cache.guilds.get(guildId)
|
||||
const cached = await bot.cache.guilds.get(guildId);
|
||||
|
||||
if (cached) return cached
|
||||
if (cached) return cached;
|
||||
|
||||
return await bot.helpers.getGuild(guildId)
|
||||
return await bot.helpers.getGuild(guildId);
|
||||
}
|
||||
|
||||
export function humanizeMilliseconds(milliseconds: number): string {
|
||||
// Gets ms into seconds
|
||||
const time = milliseconds / 1000
|
||||
if (time < 1) return '< 1s'
|
||||
const time = milliseconds / 1000;
|
||||
if (time < 1) return '< 1s';
|
||||
|
||||
const days = Math.floor(time / 86400)
|
||||
const hours = Math.floor((time % 86400) / 3600)
|
||||
const minutes = Math.floor(((time % 86400) % 3600) / 60)
|
||||
const seconds = Math.floor(((time % 86400) % 3600) % 60)
|
||||
const days = Math.floor(time / 86400);
|
||||
const hours = Math.floor((time % 86400) / 3600);
|
||||
const minutes = Math.floor(((time % 86400) % 3600) / 60);
|
||||
const seconds = Math.floor(((time % 86400) % 3600) % 60);
|
||||
|
||||
const dayString = days ? `${days}d ` : ''
|
||||
const hourString = hours ? `${hours}h ` : ''
|
||||
const minuteString = minutes ? `${minutes}m ` : ''
|
||||
const secondString = seconds ? `${seconds}s ` : ''
|
||||
const dayString = days ? `${days}d ` : '';
|
||||
const hourString = hours ? `${hours}h ` : '';
|
||||
const minuteString = minutes ? `${minutes}m ` : '';
|
||||
const secondString = seconds ? `${seconds}s ` : '';
|
||||
|
||||
return `${dayString}${hourString}${minuteString}${secondString}`
|
||||
return `${dayString}${hourString}${minuteString}${secondString}`;
|
||||
}
|
||||
|
||||
export function isSubCommand(data: SubCommand | SubCommandGroup): data is SubCommand {
|
||||
return !hasProperty(data, 'subCommands')
|
||||
return !hasProperty(data, 'subCommands');
|
||||
}
|
||||
|
||||
export function isSubCommandGroup(data: SubCommand | SubCommandGroup): data is SubCommandGroup {
|
||||
return hasProperty(data, 'subCommands')
|
||||
return hasProperty(data, 'subCommands');
|
||||
}
|
||||
|
||||
type MakeRequired<TObj, TKey extends keyof TObj> = TObj & {
|
||||
[Key in TKey]-?: TObj[Key]
|
||||
}
|
||||
[Key in TKey]-?: TObj[Key];
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import logger from './logger.js'
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import logger from './logger.js';
|
||||
|
||||
export default async function importDirectory(folder: string): Promise<void> {
|
||||
const files = await readdir(folder, { recursive: true })
|
||||
const files = await readdir(folder, { recursive: true });
|
||||
|
||||
for (const filename of files) {
|
||||
if (!filename.endsWith('.js')) continue
|
||||
if (!filename.endsWith('.js')) continue;
|
||||
|
||||
// Using `file://` and `process.cwd()` to avoid weird issues with relative paths and/or Windows
|
||||
await import(`file://${process.cwd()}/${folder}/${filename}`).catch((x) =>
|
||||
logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import chalk from 'chalk'
|
||||
import chalk from 'chalk';
|
||||
|
||||
export enum LogLevels {
|
||||
Debug,
|
||||
@@ -15,70 +15,70 @@ const prefixes = new Map<LogLevels, string>([
|
||||
[LogLevels.Warn, 'WARN'],
|
||||
[LogLevels.Error, 'ERROR'],
|
||||
[LogLevels.Fatal, 'FATAL'],
|
||||
])
|
||||
]);
|
||||
|
||||
const noColor: (str: string) => string = (msg) => msg
|
||||
const noColor: (str: string) => string = (msg) => msg;
|
||||
const colorFunctions = new Map<LogLevels, (str: string) => string>([
|
||||
[LogLevels.Debug, chalk.gray],
|
||||
[LogLevels.Info, chalk.cyan],
|
||||
[LogLevels.Warn, chalk.yellow],
|
||||
[LogLevels.Error, (str: string) => chalk.red(str)],
|
||||
[LogLevels.Fatal, (str: string) => chalk.red.bold.italic(str)],
|
||||
])
|
||||
]);
|
||||
|
||||
export function createLogger({ logLevel = LogLevels.Info, name }: { logLevel?: LogLevels; name?: string } = {}): Logger {
|
||||
function log(level: LogLevels, ...args: any[]): void {
|
||||
if (level < logLevel) return
|
||||
if (level < logLevel) return;
|
||||
|
||||
let color = colorFunctions.get(level)
|
||||
if (!color) color = noColor
|
||||
let color = colorFunctions.get(level);
|
||||
if (!color) color = noColor;
|
||||
|
||||
const date = new Date()
|
||||
const date = new Date();
|
||||
const log = [
|
||||
`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`,
|
||||
color(prefixes.get(level) ?? 'DEBUG'),
|
||||
name ? `${name} >` : '>',
|
||||
...args,
|
||||
]
|
||||
];
|
||||
|
||||
switch (level) {
|
||||
case LogLevels.Debug:
|
||||
return console.debug(...log)
|
||||
return console.debug(...log);
|
||||
case LogLevels.Info:
|
||||
return console.info(...log)
|
||||
return console.info(...log);
|
||||
case LogLevels.Warn:
|
||||
return console.warn(...log)
|
||||
return console.warn(...log);
|
||||
case LogLevels.Error:
|
||||
return console.error(...log)
|
||||
return console.error(...log);
|
||||
case LogLevels.Fatal:
|
||||
return console.error(...log)
|
||||
return console.error(...log);
|
||||
default:
|
||||
return console.log(...log)
|
||||
return console.log(...log);
|
||||
}
|
||||
}
|
||||
|
||||
function setLevel(level: LogLevels): void {
|
||||
logLevel = level
|
||||
logLevel = level;
|
||||
}
|
||||
|
||||
function debug(...args: any[]): void {
|
||||
log(LogLevels.Debug, ...args)
|
||||
log(LogLevels.Debug, ...args);
|
||||
}
|
||||
|
||||
function info(...args: any[]): void {
|
||||
log(LogLevels.Info, ...args)
|
||||
log(LogLevels.Info, ...args);
|
||||
}
|
||||
|
||||
function warn(...args: any[]): void {
|
||||
log(LogLevels.Warn, ...args)
|
||||
log(LogLevels.Warn, ...args);
|
||||
}
|
||||
|
||||
function error(...args: any[]): void {
|
||||
log(LogLevels.Error, ...args)
|
||||
log(LogLevels.Error, ...args);
|
||||
}
|
||||
|
||||
function fatal(...args: any[]): void {
|
||||
log(LogLevels.Fatal, ...args)
|
||||
log(LogLevels.Fatal, ...args);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -89,18 +89,18 @@ export function createLogger({ logLevel = LogLevels.Info, name }: { logLevel?: L
|
||||
warn,
|
||||
error,
|
||||
fatal,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const logger = createLogger({ name: 'Main' })
|
||||
export default logger
|
||||
export const logger = createLogger({ name: 'Main' });
|
||||
export default logger;
|
||||
|
||||
export interface Logger {
|
||||
log: (level: LogLevels, ...args: any[]) => void
|
||||
debug: (...args: any[]) => void
|
||||
info: (...args: any[]) => void
|
||||
warn: (...args: any[]) => void
|
||||
error: (...args: any[]) => void
|
||||
fatal: (...args: any[]) => void
|
||||
setLevel: (level: LogLevels) => void
|
||||
log: (level: LogLevels, ...args: any[]) => void;
|
||||
debug: (...args: any[]) => void;
|
||||
info: (...args: any[]) => void;
|
||||
warn: (...args: any[]) => void;
|
||||
error: (...args: any[]) => void;
|
||||
fatal: (...args: any[]) => void;
|
||||
setLevel: (level: LogLevels) => void;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Collection, createBot } from '@discordeno/bot'
|
||||
import { DISCORD_TOKEN, GATEWAY_AUTHORIZATION, GATEWAY_INTENTS, GATEWAY_URL, REST_AUTHORIZATION, REST_URL } from '../config.js'
|
||||
import type { ManagerGetShardInfoFromGuildId, ShardInfo, WorkerPresencesUpdate, WorkerShardPayload } from '../gateway/worker/types.js'
|
||||
import type { Command } from './commands.js'
|
||||
import { Collection, createBot } from '@discordeno/bot';
|
||||
import { DISCORD_TOKEN, GATEWAY_AUTHORIZATION, GATEWAY_INTENTS, GATEWAY_URL, REST_AUTHORIZATION, REST_URL } from '../config.js';
|
||||
import type { ManagerGetShardInfoFromGuildId, ShardInfo, WorkerPresencesUpdate, WorkerShardPayload } from '../gateway/worker/types.js';
|
||||
import type { Command } from './commands.js';
|
||||
|
||||
const rawBot = createBot({
|
||||
token: DISCORD_TOKEN,
|
||||
@@ -28,19 +28,19 @@ const rawBot = createBot({
|
||||
authorization: REST_AUTHORIZATION,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export const bot = rawBot as CustomBot
|
||||
export const bot = rawBot as CustomBot;
|
||||
|
||||
// TEMPLATE-SETUP: If you want/need to add any custom properties on the Bot type, you can do it in these lines below and the `CustomBot` type below. Make sure to do it in both or else you will get an error by TypeScript
|
||||
|
||||
bot.commands = new Collection()
|
||||
bot.commands = new Collection();
|
||||
|
||||
overrideGatewayImplementations(bot)
|
||||
overrideGatewayImplementations(bot);
|
||||
|
||||
export type CustomBot = typeof rawBot & {
|
||||
commands: Collection<string, Command>
|
||||
}
|
||||
commands: Collection<string, Command>;
|
||||
};
|
||||
|
||||
// Override the default gateway functions to allow the methods on the gateway object to proxy the requests to the gateway proxy
|
||||
function overrideGatewayImplementations(bot: CustomBot): void {
|
||||
@@ -56,8 +56,8 @@ function overrideGatewayImplementations(bot: CustomBot): void {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: GATEWAY_AUTHORIZATION,
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
bot.gateway.editBotStatus = async (payload) => {
|
||||
await fetch(GATEWAY_URL, {
|
||||
@@ -70,8 +70,8 @@ function overrideGatewayImplementations(bot: CustomBot): void {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: GATEWAY_AUTHORIZATION,
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export async function getShardInfoFromGuild(guildId?: bigint): Promise<Omit<ShardInfo, 'nonce'>> {
|
||||
@@ -85,11 +85,11 @@ export async function getShardInfoFromGuild(guildId?: bigint): Promise<Omit<Shar
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: GATEWAY_AUTHORIZATION,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const res = await req.json()
|
||||
const res = await req.json();
|
||||
|
||||
if (req.ok) return res
|
||||
if (req.ok) return res;
|
||||
|
||||
throw new Error(`There was an issue getting the shard info: ${res.error}`)
|
||||
throw new Error(`There was an issue getting the shard info: ${res.error}`);
|
||||
}
|
||||
|
||||
@@ -5,49 +5,49 @@ import type {
|
||||
CreateApplicationCommand,
|
||||
DiscordApplicationCommandOption,
|
||||
ParsedInteractionOption,
|
||||
} from '@discordeno/bot'
|
||||
import { bot } from './bot.js'
|
||||
} from '@discordeno/bot';
|
||||
import { bot } from './bot.js';
|
||||
|
||||
export default function createCommand<const TOptions extends CommandOptions>(command: Command<TOptions>): void {
|
||||
bot.commands.set(command.name, command as Command)
|
||||
bot.commands.set(command.name, command as Command);
|
||||
}
|
||||
|
||||
export type Command<TOptions extends CommandOptions = CommandOptions> = CreateApplicationCommand & {
|
||||
/** @inheritdoc */
|
||||
options?: TOptions
|
||||
options?: TOptions;
|
||||
/**
|
||||
* Should this command be only deployed on the Dev guild?
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
devOnly?: boolean
|
||||
devOnly?: boolean;
|
||||
/** Function to run when the interaction is executed */
|
||||
run: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: GetCommandOptions<TOptions>) => unknown
|
||||
run: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: GetCommandOptions<TOptions>) => unknown;
|
||||
/** Function to run when an autocomplete interaction is fired */
|
||||
autoComplete?: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: GetCommandOptions<TOptions>) => unknown
|
||||
}
|
||||
autoComplete?: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: GetCommandOptions<TOptions>) => unknown;
|
||||
};
|
||||
|
||||
export type GetCommandOptions<T extends CommandOptions> = T extends CommandOptions
|
||||
? { [Prop in keyof BuildOptions<T> as Prop]: BuildOptions<T>[Prop] }
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type CommandOption = Camelize<DiscordApplicationCommandOption>
|
||||
export type CommandOptions = CommandOption[]
|
||||
export type CommandOption = Camelize<DiscordApplicationCommandOption>;
|
||||
export type CommandOptions = CommandOption[];
|
||||
|
||||
// Option parsing
|
||||
|
||||
type ResolvedValues = ParsedInteractionOption<ExtractDesiredProps<typeof bot>, ExtractDesiredBehavior<typeof bot>>[string]
|
||||
type ResolvedValues = ParsedInteractionOption<ExtractDesiredProps<typeof bot>, ExtractDesiredBehavior<typeof bot>>[string];
|
||||
|
||||
// Using omit + exclude is a slight trick to avoid a type error on Pick
|
||||
export type InteractionResolvedChannel = Omit<
|
||||
typeof bot.transformers.$inferredTypes.channel,
|
||||
Exclude<keyof typeof bot.transformers.$inferredTypes.channel, 'id' | 'name' | 'type' | 'permissions' | 'threadMetadata' | 'parentId'>
|
||||
>
|
||||
export type InteractionResolvedMember = Omit<typeof bot.transformers.$inferredTypes.member, 'user' | 'deaf' | 'mute'>
|
||||
>;
|
||||
export type InteractionResolvedMember = Omit<typeof bot.transformers.$inferredTypes.member, 'user' | 'deaf' | 'mute'>;
|
||||
|
||||
export interface InteractionResolvedUser {
|
||||
user: typeof bot.transformers.$inferredTypes.user
|
||||
member: InteractionResolvedMember
|
||||
user: typeof bot.transformers.$inferredTypes.user;
|
||||
member: InteractionResolvedMember;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,30 +56,30 @@ export interface InteractionResolvedUser {
|
||||
* The entries are sorted based on the enum value
|
||||
*/
|
||||
interface TypeToResolvedMap {
|
||||
[ApplicationCommandOptionTypes.String]: string
|
||||
[ApplicationCommandOptionTypes.Integer]: number
|
||||
[ApplicationCommandOptionTypes.Boolean]: boolean
|
||||
[ApplicationCommandOptionTypes.User]: InteractionResolvedUser
|
||||
[ApplicationCommandOptionTypes.Channel]: InteractionResolvedChannel
|
||||
[ApplicationCommandOptionTypes.Role]: typeof bot.transformers.$inferredTypes.role
|
||||
[ApplicationCommandOptionTypes.Mentionable]: typeof bot.transformers.$inferredTypes.role | InteractionResolvedUser
|
||||
[ApplicationCommandOptionTypes.Number]: number
|
||||
[ApplicationCommandOptionTypes.Attachment]: typeof bot.transformers.$inferredTypes.attachment
|
||||
[ApplicationCommandOptionTypes.String]: string;
|
||||
[ApplicationCommandOptionTypes.Integer]: number;
|
||||
[ApplicationCommandOptionTypes.Boolean]: boolean;
|
||||
[ApplicationCommandOptionTypes.User]: InteractionResolvedUser;
|
||||
[ApplicationCommandOptionTypes.Channel]: InteractionResolvedChannel;
|
||||
[ApplicationCommandOptionTypes.Role]: typeof bot.transformers.$inferredTypes.role;
|
||||
[ApplicationCommandOptionTypes.Mentionable]: typeof bot.transformers.$inferredTypes.role | InteractionResolvedUser;
|
||||
[ApplicationCommandOptionTypes.Number]: number;
|
||||
[ApplicationCommandOptionTypes.Attachment]: typeof bot.transformers.$inferredTypes.attachment;
|
||||
}
|
||||
|
||||
type ConvertTypeToResolved<T extends ApplicationCommandOptionTypes> = T extends keyof TypeToResolvedMap ? TypeToResolvedMap[T] : ResolvedValues
|
||||
type ConvertTypeToResolved<T extends ApplicationCommandOptionTypes> = T extends keyof TypeToResolvedMap ? TypeToResolvedMap[T] : ResolvedValues;
|
||||
|
||||
type SubCommandApplicationCommand = ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup
|
||||
type GetOptionName<T> = T extends { name: string } ? T['name'] : never
|
||||
type SubCommandApplicationCommand = ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup;
|
||||
type GetOptionName<T> = T extends { name: string } ? T['name'] : never;
|
||||
type GetOptionValue<T> = T extends { type: ApplicationCommandOptionTypes; required?: boolean }
|
||||
? T extends { type: SubCommandApplicationCommand; options?: CommandOptions }
|
||||
? BuildOptions<T['options']>
|
||||
: ConvertTypeToResolved<T['type']> | (T['required'] extends true ? never : undefined)
|
||||
: never
|
||||
: never;
|
||||
|
||||
type BuildOptions<T extends CommandOptions | undefined> = {
|
||||
[Prop in keyof Omit<T, keyof unknown[]> as GetOptionName<T[Prop]>]: GetOptionValue<T[Prop]>
|
||||
}
|
||||
[Prop in keyof Omit<T, keyof unknown[]> as GetOptionName<T[Prop]>]: GetOptionValue<T[Prop]>;
|
||||
};
|
||||
|
||||
type ExtractDesiredProps<T> = T extends Bot<infer Props, infer _Behavior> ? Props : never
|
||||
type ExtractDesiredBehavior<T> = T extends Bot<infer _Props, infer Behavior> ? Behavior : never
|
||||
type ExtractDesiredProps<T> = T extends Bot<infer Props, infer _Behavior> ? Props : never;
|
||||
type ExtractDesiredBehavior<T> = T extends Bot<infer _Props, infer Behavior> ? Behavior : never;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { snowflakeToTimestamp } from '@discordeno/bot'
|
||||
import { getShardInfoFromGuild } from '../bot.js'
|
||||
import createCommand from '../commands.js'
|
||||
import { snowflakeToTimestamp } from '@discordeno/bot';
|
||||
import { getShardInfoFromGuild } from '../bot.js';
|
||||
import createCommand from '../commands.js';
|
||||
|
||||
createCommand({
|
||||
name: 'ping',
|
||||
description: '🏓 Check whether the bot is online and responsive.',
|
||||
async run(interaction) {
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
|
||||
const shardInfo = await getShardInfoFromGuild(interaction.guildId)
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
|
||||
const shardInfo = await getShardInfoFromGuild(interaction.guildId);
|
||||
|
||||
const shardPing = shardInfo.rtt === -1 ? '*Not yet available*' : `${shardInfo.rtt}ms`
|
||||
const shardPing = shardInfo.rtt === -1 ? '*Not yet available*' : `${shardInfo.rtt}ms`;
|
||||
|
||||
await interaction.respond(`🏓 Pong! Gateway Latency: ${shardPing}, Roundtrip Latency: ${ping}ms. I am online and responsive! 🕙`)
|
||||
await interaction.respond(`🏓 Pong! Gateway Latency: ${shardPing}, Roundtrip Latency: ${ping}ms. I am online and responsive! 🕙`);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { commandOptionsParser, InteractionTypes, LogLevels, type logger } from '@discordeno/bot'
|
||||
import chalk from 'chalk'
|
||||
import { bot } from '../../bot.js'
|
||||
import { commandOptionsParser, InteractionTypes, LogLevels, type logger } from '@discordeno/bot';
|
||||
import chalk from 'chalk';
|
||||
import { bot } from '../../bot.js';
|
||||
|
||||
bot.events.interactionCreate = async (interaction) => {
|
||||
const isAutocomplete = interaction.type === InteractionTypes.ApplicationCommandAutocomplete
|
||||
const isCommandOrAutocomplete = interaction.type === InteractionTypes.ApplicationCommand || isAutocomplete
|
||||
const isAutocomplete = interaction.type === InteractionTypes.ApplicationCommandAutocomplete;
|
||||
const isCommandOrAutocomplete = interaction.type === InteractionTypes.ApplicationCommand || isAutocomplete;
|
||||
|
||||
if (!interaction.data || !isCommandOrAutocomplete) return
|
||||
if (!interaction.data || !isCommandOrAutocomplete) return;
|
||||
|
||||
const command = bot.commands.get(interaction.data.name)
|
||||
const command = bot.commands.get(interaction.data.name);
|
||||
|
||||
if (!command) {
|
||||
logCommand(interaction, 'Missing', interaction.data.name)
|
||||
await interaction.respond('❌ Something went wrong. I was not able to find this command.')
|
||||
logCommand(interaction, 'Missing', interaction.data.name);
|
||||
await interaction.respond('❌ Something went wrong. I was not able to find this command.');
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
logCommand(interaction, 'Trigger', interaction.data.name)
|
||||
logCommand(interaction, 'Trigger', interaction.data.name);
|
||||
|
||||
const options = commandOptionsParser(interaction)
|
||||
const options = commandOptionsParser(interaction);
|
||||
|
||||
try {
|
||||
if (isAutocomplete) {
|
||||
await command.autoComplete?.(interaction, options)
|
||||
await command.autoComplete?.(interaction, options);
|
||||
} else {
|
||||
await command.run(interaction, options)
|
||||
await command.run(interaction, options);
|
||||
}
|
||||
|
||||
logCommand(interaction, 'Success', interaction.data.name)
|
||||
logCommand(interaction, 'Success', interaction.data.name);
|
||||
} catch (error) {
|
||||
logCommand(interaction, 'Failure', interaction.data.name, LogLevels.Error, error)
|
||||
await interaction.respond('❌ Something went wrong. The command execution has thrown an error.')
|
||||
}
|
||||
logCommand(interaction, 'Failure', interaction.data.name, LogLevels.Error, error);
|
||||
await interaction.respond('❌ Something went wrong. The command execution has thrown an error.');
|
||||
}
|
||||
};
|
||||
|
||||
function logCommand(
|
||||
interaction: typeof bot.transformers.$inferredTypes.interaction,
|
||||
@@ -42,11 +42,11 @@ function logCommand(
|
||||
logLevel: LogLevels = LogLevels.Info,
|
||||
...restArgs: unknown[]
|
||||
): void {
|
||||
const typeColor = ['Failure', 'Missing'].includes(type) ? chalk.red(type) : type === 'Success' ? chalk.green(type) : chalk.white(type)
|
||||
const typeColor = ['Failure', 'Missing'].includes(type) ? chalk.red(type) : type === 'Success' ? chalk.green(type) : chalk.white(type);
|
||||
|
||||
const autocomplete = interaction.type === InteractionTypes.ApplicationCommandAutocomplete ? ' (AutoComplete) ' : ''
|
||||
const command = `Command${autocomplete}: ${chalk.bgYellow.black(commandName || 'Unknown')} - ${chalk.bgBlack(typeColor)}`
|
||||
const user = chalk.bgGreen.black(`@${interaction.user.username} (${interaction.user.id})`)
|
||||
const guild = chalk.bgMagenta.black(interaction.guildId ? `guildId: ${interaction.guildId}` : 'DM')
|
||||
;(bot.logger as typeof logger).log(logLevel, `${command} - By ${user} in ${guild}`, ...restArgs)
|
||||
const autocomplete = interaction.type === InteractionTypes.ApplicationCommandAutocomplete ? ' (AutoComplete) ' : '';
|
||||
const command = `Command${autocomplete}: ${chalk.bgYellow.black(commandName || 'Unknown')} - ${chalk.bgBlack(typeColor)}`;
|
||||
const user = chalk.bgGreen.black(`@${interaction.user.username} (${interaction.user.id})`);
|
||||
const guild = chalk.bgMagenta.black(interaction.guildId ? `guildId: ${interaction.guildId}` : 'DM');
|
||||
(bot.logger as typeof logger).log(logLevel, `${command} - By ${user} in ${guild}`, ...restArgs);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { inspect } from 'node:util'
|
||||
import { createEmbeds } from '@discordeno/bot'
|
||||
import { BUGS_ERRORS_REPORT_WEBHOOK } from '../../config.js'
|
||||
import { bot } from '../bot.js'
|
||||
import { webhookURLToIDAndToken } from '../utils/webhook.js'
|
||||
import { inspect } from 'node:util';
|
||||
import { createEmbeds } from '@discordeno/bot';
|
||||
import { BUGS_ERRORS_REPORT_WEBHOOK } from '../../config.js';
|
||||
import { bot } from '../bot.js';
|
||||
import { webhookURLToIDAndToken } from '../utils/webhook.js';
|
||||
|
||||
process.on('unhandledRejection', async (error) => {
|
||||
bot.logger.error('An unhandled rejection occurred', error)
|
||||
bot.logger.error('An unhandled rejection occurred', error);
|
||||
|
||||
if (!BUGS_ERRORS_REPORT_WEBHOOK || !error) return
|
||||
if (!BUGS_ERRORS_REPORT_WEBHOOK || !error) return;
|
||||
|
||||
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK)
|
||||
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK);
|
||||
|
||||
if (!id || !token) return
|
||||
if (!id || !token) return;
|
||||
|
||||
const inspectedError = inspect(error)
|
||||
const inspectedError = inspect(error);
|
||||
|
||||
const embeds = createEmbeds().setDescription(`\`\`\`${inspectedError}\`\`\``).setFooter('Unhandled rejection occurred').setTimestamp(Date.now())
|
||||
const embeds = createEmbeds().setDescription(`\`\`\`${inspectedError}\`\`\``).setFooter('Unhandled rejection occurred').setTimestamp(Date.now());
|
||||
|
||||
await bot.helpers.executeWebhook(id, token, { embeds })
|
||||
})
|
||||
await bot.helpers.executeWebhook(id, token, { embeds });
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import fastify, { type FastifyInstance } from 'fastify'
|
||||
import { EVENT_HANDLER_AUTHORIZATION } from '../config.js'
|
||||
import fastify, { type FastifyInstance } from 'fastify';
|
||||
import { EVENT_HANDLER_AUTHORIZATION } from '../config.js';
|
||||
|
||||
export function buildFastifyApp(): FastifyInstance {
|
||||
const app = fastify()
|
||||
const app = fastify();
|
||||
|
||||
// Authorization check
|
||||
app.addHook('onRequest', async (req, res) => {
|
||||
if (req.headers.authorization !== EVENT_HANDLER_AUTHORIZATION) {
|
||||
res.status(401).send({
|
||||
message: 'Credentials not valid.',
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return app
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join as joinPath } from 'node:path'
|
||||
import type { DiscordGatewayPayload, GatewayDispatchEventNames } from '@discordeno/bot'
|
||||
import { connect as connectAmqp } from 'amqplib'
|
||||
import { join as joinPath } from 'node:path';
|
||||
import type { DiscordGatewayPayload, GatewayDispatchEventNames } from '@discordeno/bot';
|
||||
import { connect as connectAmqp } from 'amqplib';
|
||||
import {
|
||||
EVENT_HANDLER_HOST,
|
||||
EVENT_HANDLER_PORT,
|
||||
@@ -8,81 +8,81 @@ import {
|
||||
MESSAGEQUEUE_PASSWORD,
|
||||
MESSAGEQUEUE_URL,
|
||||
MESSAGEQUEUE_USERNAME,
|
||||
} from '../config.js'
|
||||
import { getDirnameFromFileUrl } from '../util.js'
|
||||
import { bot } from './bot.js'
|
||||
import { buildFastifyApp } from './fastify.js'
|
||||
import importDirectory from './utils/loader.js'
|
||||
} from '../config.js';
|
||||
import { getDirnameFromFileUrl } from '../util.js';
|
||||
import { bot } from './bot.js';
|
||||
import { buildFastifyApp } from './fastify.js';
|
||||
import importDirectory from './utils/loader.js';
|
||||
|
||||
// The importDirectory function uses 'readdir' that requires either a relative path compared to the process CWD or an absolute one, so to get one relative we need to use import.meta.url
|
||||
const currentDirectory = getDirnameFromFileUrl(import.meta.url)
|
||||
const currentDirectory = getDirnameFromFileUrl(import.meta.url);
|
||||
|
||||
await importDirectory(joinPath(currentDirectory, './commands'))
|
||||
await importDirectory(joinPath(currentDirectory, './events'))
|
||||
await importDirectory(joinPath(currentDirectory, './commands'));
|
||||
await importDirectory(joinPath(currentDirectory, './events'));
|
||||
|
||||
if (MESSAGEQUEUE_ENABLE) {
|
||||
await connectToRabbitMQ()
|
||||
await connectToRabbitMQ();
|
||||
}
|
||||
|
||||
const app = buildFastifyApp()
|
||||
const app = buildFastifyApp();
|
||||
|
||||
app.get('/timecheck', async (_req, res) => {
|
||||
res.status(200).send({ message: Date.now() })
|
||||
})
|
||||
res.status(200).send({ message: Date.now() });
|
||||
});
|
||||
|
||||
app.post('/', async (req, res) => {
|
||||
const body = req.body as GatewayEvent
|
||||
const body = req.body as GatewayEvent;
|
||||
|
||||
try {
|
||||
handleGatewayEvent(body.payload, body.shardId)
|
||||
handleGatewayEvent(body.payload, body.shardId);
|
||||
|
||||
res.status(200).send()
|
||||
res.status(200).send();
|
||||
} catch (error) {
|
||||
bot.logger.error('There was an error handling the incoming gateway command', error)
|
||||
res.status(500).send()
|
||||
bot.logger.error('There was an error handling the incoming gateway command', error);
|
||||
res.status(500).send();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
await app.listen({
|
||||
host: EVENT_HANDLER_HOST,
|
||||
port: EVENT_HANDLER_PORT,
|
||||
})
|
||||
});
|
||||
|
||||
bot.logger.info(`Bot event handler is listening on port ${EVENT_HANDLER_PORT}`)
|
||||
bot.logger.info(`Bot event handler is listening on port ${EVENT_HANDLER_PORT}`);
|
||||
|
||||
async function handleGatewayEvent(payload: DiscordGatewayPayload, shardId: number): Promise<void> {
|
||||
bot.events.raw?.(payload, shardId)
|
||||
bot.events.raw?.(payload, shardId);
|
||||
|
||||
// If we don't have the event type we don't process it further
|
||||
if (!payload.t) return
|
||||
if (!payload.t) return;
|
||||
|
||||
// Run the dispatch check
|
||||
await bot.events.dispatchRequirements?.(payload, shardId)
|
||||
await bot.events.dispatchRequirements?.(payload, shardId);
|
||||
|
||||
bot.handlers[payload.t as GatewayDispatchEventNames]?.(bot, payload, shardId)
|
||||
bot.handlers[payload.t as GatewayDispatchEventNames]?.(bot, payload, shardId);
|
||||
}
|
||||
|
||||
async function connectToRabbitMQ(): Promise<void> {
|
||||
const connection = await connectAmqp(`amqp://${MESSAGEQUEUE_USERNAME}:${MESSAGEQUEUE_PASSWORD}@${MESSAGEQUEUE_URL}`).catch((error) => {
|
||||
bot.logger.error('Failed to connect to RabbitMQ, retrying in 1s.', error)
|
||||
setTimeout(connectToRabbitMQ, 1000)
|
||||
})
|
||||
bot.logger.error('Failed to connect to RabbitMQ, retrying in 1s.', error);
|
||||
setTimeout(connectToRabbitMQ, 1000);
|
||||
});
|
||||
|
||||
if (!connection) return
|
||||
if (!connection) return;
|
||||
|
||||
connection.on('close', () => {
|
||||
setTimeout(connectToRabbitMQ, 1000)
|
||||
})
|
||||
setTimeout(connectToRabbitMQ, 1000);
|
||||
});
|
||||
connection.on('error', (error) => {
|
||||
bot.logger.error('There was an error in the connection with RabbitMQ, reconnecting in 1s.', error)
|
||||
setTimeout(connectToRabbitMQ, 1000)
|
||||
})
|
||||
bot.logger.error('There was an error in the connection with RabbitMQ, reconnecting in 1s.', error);
|
||||
setTimeout(connectToRabbitMQ, 1000);
|
||||
});
|
||||
|
||||
const channel = await connection.createChannel().catch((error) => {
|
||||
bot.logger.error('There was an error creating the RabbitMQ channel', error)
|
||||
})
|
||||
bot.logger.error('There was an error creating the RabbitMQ channel', error);
|
||||
});
|
||||
|
||||
if (!channel) return
|
||||
if (!channel) return;
|
||||
|
||||
const exchange = await channel
|
||||
.assertExchange('gatewayMessage', 'x-message-deduplication', {
|
||||
@@ -93,31 +93,31 @@ async function connectToRabbitMQ(): Promise<void> {
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
bot.logger.error('There was an error asserting the exchange', error)
|
||||
})
|
||||
bot.logger.error('There was an error asserting the exchange', error);
|
||||
});
|
||||
|
||||
if (!exchange) return
|
||||
if (!exchange) return;
|
||||
|
||||
await channel.assertQueue('gatewayMessageQueue').catch(bot.logger.error)
|
||||
await channel.bindQueue('gatewayMessageQueue', 'gatewayMessage', '').catch(bot.logger.error)
|
||||
await channel.assertQueue('gatewayMessageQueue').catch(bot.logger.error);
|
||||
await channel.bindQueue('gatewayMessageQueue', 'gatewayMessage', '').catch(bot.logger.error);
|
||||
await channel
|
||||
.consume('gatewayMessageQueue', async (message) => {
|
||||
if (!message) return
|
||||
if (!message) return;
|
||||
|
||||
try {
|
||||
const messageBody = JSON.parse(message.content.toString()) as GatewayEvent
|
||||
const messageBody = JSON.parse(message.content.toString()) as GatewayEvent;
|
||||
|
||||
await handleGatewayEvent(messageBody.payload, messageBody.shardId)
|
||||
await handleGatewayEvent(messageBody.payload, messageBody.shardId);
|
||||
|
||||
channel.ack(message)
|
||||
channel.ack(message);
|
||||
} catch (error) {
|
||||
bot.logger.error('There was an error handling events received from RabbitMQ', error)
|
||||
bot.logger.error('There was an error handling events received from RabbitMQ', error);
|
||||
}
|
||||
})
|
||||
.catch(bot.logger.error)
|
||||
.catch(bot.logger.error);
|
||||
}
|
||||
|
||||
interface GatewayEvent {
|
||||
payload: DiscordGatewayPayload
|
||||
shardId: number
|
||||
payload: DiscordGatewayPayload;
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import 'dotenv/config'
|
||||
import 'dotenv/config';
|
||||
|
||||
import { join as joinPath } from 'node:path'
|
||||
import { getDirnameFromFileUrl } from '../util.js'
|
||||
import { bot } from './bot.js'
|
||||
import importDirectory from './utils/loader.js'
|
||||
import { updateCommands } from './utils/updateCommands.js'
|
||||
import { join as joinPath } from 'node:path';
|
||||
import { getDirnameFromFileUrl } from '../util.js';
|
||||
import { bot } from './bot.js';
|
||||
import importDirectory from './utils/loader.js';
|
||||
import { updateCommands } from './utils/updateCommands.js';
|
||||
|
||||
// The importDirectory function uses 'readdir' that requires either a relative path compared to the process CWD or an absolute one, so to get one relative we need to use import.meta.url
|
||||
const currentDirectory = getDirnameFromFileUrl(import.meta.url)
|
||||
const currentDirectory = getDirnameFromFileUrl(import.meta.url);
|
||||
|
||||
await importDirectory(joinPath(currentDirectory, './commands'))
|
||||
await importDirectory(joinPath(currentDirectory, './commands'));
|
||||
|
||||
await updateCommands()
|
||||
await updateCommands();
|
||||
|
||||
bot.logger.info('Done!')
|
||||
bot.logger.info('Done!');
|
||||
|
||||
// We need to manually exit as the REST Manager has timeouts that will keep NodeJS alive
|
||||
process.exit()
|
||||
process.exit();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import { bot } from '../bot.js'
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { bot } from '../bot.js';
|
||||
|
||||
export default async function importDirectory(folder: string): Promise<void> {
|
||||
const files = await readdir(folder, { recursive: true })
|
||||
const files = await readdir(folder, { recursive: true });
|
||||
|
||||
for (const filename of files) {
|
||||
if (!filename.endsWith('.js')) continue
|
||||
if (!filename.endsWith('.js')) continue;
|
||||
|
||||
// Using `file://` to avoid weird issues with paths on Windows
|
||||
await import(`file://${folder}/${filename}`).catch((x) => bot.logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x))
|
||||
await import(`file://${folder}/${filename}`).catch((x) => bot.logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import assert from 'node:assert'
|
||||
import { DEV_SERVER_ID, DEVELOPMENT } from '../../config.js'
|
||||
import { bot } from '../bot.js'
|
||||
import assert from 'node:assert';
|
||||
import { DEV_SERVER_ID, DEVELOPMENT } from '../../config.js';
|
||||
import { bot } from '../bot.js';
|
||||
|
||||
export async function updateCommands(): Promise<void> {
|
||||
bot.logger.info('Updating commands')
|
||||
bot.logger.info('Updating commands');
|
||||
|
||||
const userCommands = bot.commands.filter((x) => !x.devOnly).array()
|
||||
await bot.helpers.upsertGlobalApplicationCommands(userCommands)
|
||||
const userCommands = bot.commands.filter((x) => !x.devOnly).array();
|
||||
await bot.helpers.upsertGlobalApplicationCommands(userCommands);
|
||||
|
||||
if (DEVELOPMENT) {
|
||||
assert(DEV_SERVER_ID, 'The DEV_SERVER_ID environment is missing')
|
||||
assert(DEV_SERVER_ID, 'The DEV_SERVER_ID environment is missing');
|
||||
|
||||
bot.logger.info('Updating developer commands')
|
||||
bot.logger.info('Updating developer commands');
|
||||
|
||||
const devCommands = bot.commands.filter((x) => x.devOnly ?? false).array()
|
||||
await bot.helpers.upsertGuildApplicationCommands(DEV_SERVER_ID, devCommands)
|
||||
const devCommands = bot.commands.filter((x) => x.devOnly ?? false).array();
|
||||
await bot.helpers.upsertGuildApplicationCommands(DEV_SERVER_ID, devCommands);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/** Get the webhook id and token from a webhook url. */
|
||||
export function webhookURLToIDAndToken(url: string): { id?: string; token?: string } {
|
||||
const [id, token] = url.substring(url.indexOf('webhooks/') + 9).split('/')
|
||||
const [id, token] = url.substring(url.indexOf('webhooks/') + 9).split('/');
|
||||
|
||||
return {
|
||||
id,
|
||||
token,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
import 'dotenv/config'
|
||||
import 'dotenv/config';
|
||||
|
||||
import { Intents } from '@discordeno/bot'
|
||||
import { Intents } from '@discordeno/bot';
|
||||
|
||||
// #region Mapping of environment variables to javascript variables with some minimal parsing
|
||||
|
||||
// General Configurations
|
||||
|
||||
export const DEVELOPMENT = process.env.DEVELOPMENT === 'true'
|
||||
export const DEV_SERVER_ID = process.env.DEV_SERVER_ID
|
||||
export const DISCORD_TOKEN = assertEnv('DISCORD_TOKEN')
|
||||
export const DEVELOPMENT = process.env.DEVELOPMENT === 'true';
|
||||
export const DEV_SERVER_ID = process.env.DEV_SERVER_ID;
|
||||
export const DISCORD_TOKEN = assertEnv('DISCORD_TOKEN');
|
||||
|
||||
// Bot Configuration
|
||||
|
||||
export const EVENT_HANDLER_AUTHORIZATION = assertEnv('EVENT_HANDLER_AUTHORIZATION')
|
||||
export const EVENT_HANDLER_AUTHORIZATION = assertEnv('EVENT_HANDLER_AUTHORIZATION');
|
||||
|
||||
export const EVENT_HANDLER_HOST = assertEnv('EVENT_HANDLER_HOST')
|
||||
export const EVENT_HANDLER_PORT = parseNumber(assertEnv('EVENT_HANDLER_PORT'), 'EVENT_HANDLER_PORT')
|
||||
export const EVENT_HANDLER_HOST = assertEnv('EVENT_HANDLER_HOST');
|
||||
export const EVENT_HANDLER_PORT = parseNumber(assertEnv('EVENT_HANDLER_PORT'), 'EVENT_HANDLER_PORT');
|
||||
|
||||
export const BUGS_ERRORS_REPORT_WEBHOOK = process.env.BUGS_ERRORS_REPORT_WEBHOOK
|
||||
export const BUGS_ERRORS_REPORT_WEBHOOK = process.env.BUGS_ERRORS_REPORT_WEBHOOK;
|
||||
|
||||
// Rest Proxy Configurations
|
||||
|
||||
export const REST_AUTHORIZATION = assertEnv('REST_AUTHORIZATION')
|
||||
export const REST_HOST = assertEnv('REST_HOST')
|
||||
export const REST_PORT = parseNumber(assertEnv('REST_PORT'), 'REST_PORT')
|
||||
export const REST_AUTHORIZATION = assertEnv('REST_AUTHORIZATION');
|
||||
export const REST_HOST = assertEnv('REST_HOST');
|
||||
export const REST_PORT = parseNumber(assertEnv('REST_PORT'), 'REST_PORT');
|
||||
|
||||
// Gateway Proxy Configurations
|
||||
|
||||
export const TOTAL_SHARDS = process.env.TOTAL_SHARDS ? parseNumber(process.env.TOTAL_SHARDS, 'TOTAL_SHARDS') : undefined
|
||||
export const SHARDS_PER_WORKER = parseNumber(process.env.SHARDS_PER_WORKER ?? '16', 'SHARDS_PER_WORKER')
|
||||
export const TOTAL_WORKERS = parseNumber(process.env.TOTAL_WORKERS ?? '4', 'TOTAL_WORKERS')
|
||||
export const TOTAL_SHARDS = process.env.TOTAL_SHARDS ? parseNumber(process.env.TOTAL_SHARDS, 'TOTAL_SHARDS') : undefined;
|
||||
export const SHARDS_PER_WORKER = parseNumber(process.env.SHARDS_PER_WORKER ?? '16', 'SHARDS_PER_WORKER');
|
||||
export const TOTAL_WORKERS = parseNumber(process.env.TOTAL_WORKERS ?? '4', 'TOTAL_WORKERS');
|
||||
|
||||
export const GATEWAY_AUTHORIZATION = assertEnv('GATEWAY_AUTHORIZATION')
|
||||
export const GATEWAY_HOST = assertEnv('GATEWAY_HOST')
|
||||
export const GATEWAY_PORT = parseNumber(assertEnv('GATEWAY_PORT'), 'GATEWAY_PORT')
|
||||
export const GATEWAY_AUTHORIZATION = assertEnv('GATEWAY_AUTHORIZATION');
|
||||
export const GATEWAY_HOST = assertEnv('GATEWAY_HOST');
|
||||
export const GATEWAY_PORT = parseNumber(assertEnv('GATEWAY_PORT'), 'GATEWAY_PORT');
|
||||
|
||||
// Message queue (RabbitMQ configuration)
|
||||
|
||||
export const MESSAGEQUEUE_ENABLE = process.env.MESSAGEQUEUE_ENABLE === 'true'
|
||||
export const MESSAGEQUEUE_ENABLE = process.env.MESSAGEQUEUE_ENABLE === 'true';
|
||||
|
||||
export const MESSAGEQUEUE_URL = process.env.MESSAGEQUEUE_URL
|
||||
export const MESSAGEQUEUE_USERNAME = process.env.MESSAGEQUEUE_USERNAME
|
||||
export const MESSAGEQUEUE_PASSWORD = process.env.MESSAGEQUEUE_PASSWORD
|
||||
export const MESSAGEQUEUE_URL = process.env.MESSAGEQUEUE_URL;
|
||||
export const MESSAGEQUEUE_USERNAME = process.env.MESSAGEQUEUE_USERNAME;
|
||||
export const MESSAGEQUEUE_PASSWORD = process.env.MESSAGEQUEUE_PASSWORD;
|
||||
|
||||
// Analytics (InfluxDB configuration)
|
||||
|
||||
export const INFLUX_ORG = process.env.INFLUX_ORG
|
||||
export const INFLUX_BUCKET = process.env.INFLUX_BUCKET
|
||||
export const INFLUX_TOKEN = process.env.INFLUX_TOKEN
|
||||
export const INFLUX_URL = process.env.INFLUX_URL
|
||||
export const INFLUX_ORG = process.env.INFLUX_ORG;
|
||||
export const INFLUX_BUCKET = process.env.INFLUX_BUCKET;
|
||||
export const INFLUX_TOKEN = process.env.INFLUX_TOKEN;
|
||||
export const INFLUX_URL = process.env.INFLUX_URL;
|
||||
|
||||
export const INFLUX_ENABLED = INFLUX_URL && INFLUX_TOKEN && INFLUX_ORG && INFLUX_BUCKET
|
||||
export const INFLUX_ENABLED = INFLUX_URL && INFLUX_TOKEN && INFLUX_ORG && INFLUX_BUCKET;
|
||||
|
||||
// #endregion
|
||||
|
||||
export const EVENT_HANDLER_URL = `http://${EVENT_HANDLER_HOST}:${EVENT_HANDLER_PORT}`
|
||||
export const REST_URL = `http://${REST_HOST}:${REST_PORT}`
|
||||
export const GATEWAY_URL = `http://${GATEWAY_HOST}:${GATEWAY_PORT}`
|
||||
export const EVENT_HANDLER_URL = `http://${EVENT_HANDLER_HOST}:${EVENT_HANDLER_PORT}`;
|
||||
export const REST_URL = `http://${REST_HOST}:${REST_PORT}`;
|
||||
export const GATEWAY_URL = `http://${GATEWAY_HOST}:${GATEWAY_PORT}`;
|
||||
|
||||
// TEMPLATE-SETUP: Add/Remove the intents you need/don't need
|
||||
export const GATEWAY_INTENTS = Intents.Guilds | Intents.GuildMessages
|
||||
export const GATEWAY_INTENTS = Intents.Guilds | Intents.GuildMessages;
|
||||
|
||||
// Helper functions
|
||||
|
||||
function assertEnv(env: string): string {
|
||||
if (process.env[env]) return process.env[env]
|
||||
if (process.env[env]) return process.env[env];
|
||||
|
||||
throw new TypeError(`The '${env}' environment variable must be set`)
|
||||
throw new TypeError(`The '${env}' environment variable must be set`);
|
||||
}
|
||||
|
||||
function parseNumber(envValue: string, env: string): number {
|
||||
const parsed = Number.parseInt(envValue)
|
||||
const parsed = Number.parseInt(envValue);
|
||||
|
||||
if (Number.isFinite(parsed)) return parsed
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
|
||||
throw new TypeError(`The '${env}' environment variable must be a number`)
|
||||
throw new TypeError(`The '${env}' environment variable must be a number`);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import fastify, { type FastifyInstance } from 'fastify'
|
||||
import { GATEWAY_AUTHORIZATION } from '../config.js'
|
||||
import fastify, { type FastifyInstance } from 'fastify';
|
||||
import { GATEWAY_AUTHORIZATION } from '../config.js';
|
||||
|
||||
export function buildFastifyApp(): FastifyInstance {
|
||||
const app = fastify()
|
||||
const app = fastify();
|
||||
|
||||
// Authorization check
|
||||
app.addHook('onRequest', async (req, res) => {
|
||||
if (req.headers.authorization !== GATEWAY_AUTHORIZATION) {
|
||||
res.status(401).send({
|
||||
message: 'Credentials not valid.',
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return app
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Worker } from 'node:worker_threads'
|
||||
import { createGatewayManager, createLogger, createRestManager } from '@discordeno/bot'
|
||||
import { DISCORD_TOKEN, GATEWAY_INTENTS, REST_AUTHORIZATION, REST_URL, SHARDS_PER_WORKER, TOTAL_SHARDS, TOTAL_WORKERS } from '../config.js'
|
||||
import { promiseWithResolvers } from '../util.js'
|
||||
import { createWorker } from './worker/createWorker.js'
|
||||
import type { ManagerMessage, WorkerMessage } from './worker/types.js'
|
||||
import type { Worker } from 'node:worker_threads';
|
||||
import { createGatewayManager, createLogger, createRestManager } from '@discordeno/bot';
|
||||
import { DISCORD_TOKEN, GATEWAY_INTENTS, REST_AUTHORIZATION, REST_URL, SHARDS_PER_WORKER, TOTAL_SHARDS, TOTAL_WORKERS } from '../config.js';
|
||||
import { promiseWithResolvers } from '../util.js';
|
||||
import { createWorker } from './worker/createWorker.js';
|
||||
import type { ManagerMessage, WorkerMessage } from './worker/types.js';
|
||||
|
||||
export const workers = new Map<number, Worker>()
|
||||
export const logger = createLogger({ name: 'GATEWAY' })
|
||||
export const workers = new Map<number, Worker>();
|
||||
export const logger = createLogger({ name: 'GATEWAY' });
|
||||
|
||||
const restManager = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
@@ -14,9 +14,9 @@ const restManager = createRestManager({
|
||||
baseUrl: REST_URL,
|
||||
authorization: REST_AUTHORIZATION,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const gatewayBotConfig = await restManager.getGatewayBot()
|
||||
const gatewayBotConfig = await restManager.getGatewayBot();
|
||||
|
||||
const gatewayManager = createGatewayManager({
|
||||
token: DISCORD_TOKEN,
|
||||
@@ -28,96 +28,96 @@ const gatewayManager = createGatewayManager({
|
||||
resharding: {
|
||||
getSessionInfo: restManager.getGatewayBot,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
gatewayManager.resharding.tellWorkerToPrepare = async (workerId, shardId, bucketId) => {
|
||||
logger.info(`Tell worker to prepare, workerId: ${workerId}, shardId: ${shardId}, bucketId: ${bucketId}`)
|
||||
logger.info(`Tell worker to prepare, workerId: ${workerId}, shardId: ${shardId}, bucketId: ${bucketId}`);
|
||||
|
||||
let worker = workers.get(workerId)
|
||||
let worker = workers.get(workerId);
|
||||
if (!worker) {
|
||||
worker = createWorker(workerId)
|
||||
workers.set(workerId, worker)
|
||||
worker = createWorker(workerId);
|
||||
workers.set(workerId, worker);
|
||||
}
|
||||
|
||||
worker.postMessage({
|
||||
type: 'PrepareShard',
|
||||
shardId,
|
||||
totalShards: gatewayManager.totalShards,
|
||||
} satisfies WorkerMessage)
|
||||
} satisfies WorkerMessage);
|
||||
|
||||
const { promise, resolve } = promiseWithResolvers<void>()
|
||||
const { promise, resolve } = promiseWithResolvers<void>();
|
||||
|
||||
const waitForShardPrepared = (message: ManagerMessage) => {
|
||||
if (message.type === 'ShardPrepared' && message.shardId === shardId) {
|
||||
resolve()
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
worker.on('message', waitForShardPrepared)
|
||||
worker.on('message', waitForShardPrepared);
|
||||
|
||||
await promise
|
||||
await promise;
|
||||
|
||||
worker.off('message', waitForShardPrepared)
|
||||
}
|
||||
worker.off('message', waitForShardPrepared);
|
||||
};
|
||||
|
||||
gatewayManager.resharding.onReshardingSwitch = async () => {
|
||||
logger.info('Resharding switch triggered, telling workers to switch the shards')
|
||||
logger.info('Resharding switch triggered, telling workers to switch the shards');
|
||||
|
||||
for (const worker of workers.values()) {
|
||||
worker.postMessage({
|
||||
type: 'SwitchShards',
|
||||
} satisfies WorkerMessage)
|
||||
}
|
||||
} satisfies WorkerMessage);
|
||||
}
|
||||
};
|
||||
|
||||
gatewayManager.tellWorkerToIdentify = async (workerId, shardId, bucketId) => {
|
||||
logger.info(`Tell worker to identify, workerId: ${workerId}, shardId: ${shardId}, bucketId: ${bucketId}`)
|
||||
logger.info(`Tell worker to identify, workerId: ${workerId}, shardId: ${shardId}, bucketId: ${bucketId}`);
|
||||
|
||||
const worker = workers.get(workerId) ?? createWorker(workerId)
|
||||
workers.set(workerId, worker)
|
||||
const worker = workers.get(workerId) ?? createWorker(workerId);
|
||||
workers.set(workerId, worker);
|
||||
|
||||
worker.postMessage({
|
||||
type: 'IdentifyShard',
|
||||
shardId,
|
||||
} satisfies WorkerMessage)
|
||||
} satisfies WorkerMessage);
|
||||
|
||||
const { promise, resolve } = promiseWithResolvers<void>()
|
||||
const { promise, resolve } = promiseWithResolvers<void>();
|
||||
|
||||
const waitForShardIdentified = (message: ManagerMessage) => {
|
||||
if (message.type === 'ShardIdentified' && message.shardId === shardId) {
|
||||
resolve()
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
worker.on('message', waitForShardIdentified)
|
||||
worker.on('message', waitForShardIdentified);
|
||||
|
||||
await promise
|
||||
await promise;
|
||||
|
||||
worker.off('message', waitForShardIdentified)
|
||||
}
|
||||
worker.off('message', waitForShardIdentified);
|
||||
};
|
||||
|
||||
gatewayManager.sendPayload = async (shardId, payload) => {
|
||||
const workerId = gatewayManager.calculateWorkerId(shardId)
|
||||
const worker = workers.get(workerId)
|
||||
const workerId = gatewayManager.calculateWorkerId(shardId);
|
||||
const worker = workers.get(workerId);
|
||||
|
||||
if (!worker) return
|
||||
if (!worker) return;
|
||||
|
||||
worker.postMessage({
|
||||
type: 'ShardPayload',
|
||||
shardId,
|
||||
payload,
|
||||
} satisfies WorkerMessage)
|
||||
}
|
||||
} satisfies WorkerMessage);
|
||||
};
|
||||
|
||||
gatewayManager.editBotStatus = async (payload) => {
|
||||
const workersArray = Array.from(workers.values())
|
||||
const workersArray = Array.from(workers.values());
|
||||
|
||||
for (const worker of workersArray) {
|
||||
worker.postMessage({
|
||||
type: 'EditShardsPresence',
|
||||
payload,
|
||||
} satisfies WorkerMessage)
|
||||
}
|
||||
} satisfies WorkerMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export default gatewayManager
|
||||
export default gatewayManager;
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
import { GATEWAY_HOST, GATEWAY_PORT } from '../config.js'
|
||||
import { promiseWithResolvers } from '../util.js'
|
||||
import { buildFastifyApp } from './fastify.js'
|
||||
import gatewayManager, { logger, workers } from './gatewayManager.js'
|
||||
import { shardInfoRequests } from './worker/createWorker.js'
|
||||
import type { ManagerGetShardInfoFromGuildId, ShardInfo, WorkerMessage, WorkerPresencesUpdate, WorkerShardPayload } from './worker/types.js'
|
||||
import { GATEWAY_HOST, GATEWAY_PORT } from '../config.js';
|
||||
import { promiseWithResolvers } from '../util.js';
|
||||
import { buildFastifyApp } from './fastify.js';
|
||||
import gatewayManager, { logger, workers } from './gatewayManager.js';
|
||||
import { shardInfoRequests } from './worker/createWorker.js';
|
||||
import type { ManagerGetShardInfoFromGuildId, ShardInfo, WorkerMessage, WorkerPresencesUpdate, WorkerShardPayload } from './worker/types.js';
|
||||
|
||||
const app = buildFastifyApp()
|
||||
const app = buildFastifyApp();
|
||||
|
||||
app.get('/timecheck', (_req, res) => {
|
||||
res.status(200).send({ message: Date.now() })
|
||||
})
|
||||
res.status(200).send({ message: Date.now() });
|
||||
});
|
||||
|
||||
app.post('/', async (req, res) => {
|
||||
if (!req.body) {
|
||||
res.status(400).send({ message: 'Invalid body' })
|
||||
return
|
||||
res.status(400).send({ message: 'Invalid body' });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = req.body as WorkerShardPayload | WorkerPresencesUpdate | ManagerGetShardInfoFromGuildId
|
||||
const data = req.body as WorkerShardPayload | WorkerPresencesUpdate | ManagerGetShardInfoFromGuildId;
|
||||
|
||||
if (data.type === 'ShardPayload') {
|
||||
await gatewayManager.sendPayload(data.shardId, data.payload)
|
||||
return
|
||||
await gatewayManager.sendPayload(data.shardId, data.payload);
|
||||
return;
|
||||
}
|
||||
if (data.type === 'EditShardsPresence') {
|
||||
await gatewayManager.editBotStatus(data.payload)
|
||||
return
|
||||
await gatewayManager.editBotStatus(data.payload);
|
||||
return;
|
||||
}
|
||||
if (data.type === 'ShardInfoFromGuild') {
|
||||
// If we don't have a guildId, we use shard 0
|
||||
const shardId = data.guildId ? gatewayManager.calculateShardId(data.guildId) : 0
|
||||
const workerId = gatewayManager.calculateWorkerId(shardId)
|
||||
const worker = workers.get(workerId)
|
||||
const shardId = data.guildId ? gatewayManager.calculateShardId(data.guildId) : 0;
|
||||
const workerId = gatewayManager.calculateWorkerId(shardId);
|
||||
const worker = workers.get(workerId);
|
||||
|
||||
if (!worker) {
|
||||
await res.status(400).send({ error: `worker for shard ${shardId} not found` })
|
||||
return
|
||||
await res.status(400).send({ error: `worker for shard ${shardId} not found` });
|
||||
return;
|
||||
}
|
||||
|
||||
const nonce = crypto.randomUUID()
|
||||
const nonce = crypto.randomUUID();
|
||||
|
||||
const { promise, resolve } = promiseWithResolvers<ShardInfo>()
|
||||
const { promise, resolve } = promiseWithResolvers<ShardInfo>();
|
||||
|
||||
shardInfoRequests.set(nonce, resolve)
|
||||
shardInfoRequests.set(nonce, resolve);
|
||||
|
||||
worker.postMessage({
|
||||
type: 'GetShardInfo',
|
||||
shardId,
|
||||
nonce,
|
||||
} satisfies WorkerMessage)
|
||||
} satisfies WorkerMessage);
|
||||
|
||||
const shardInfo = await promise
|
||||
const shardInfo = await promise;
|
||||
|
||||
await res.status(200).send({
|
||||
shardId: shardInfo.shardId,
|
||||
rtt: shardInfo.rtt,
|
||||
} satisfies Omit<ShardInfo, 'nonce'>)
|
||||
return
|
||||
} satisfies Omit<ShardInfo, 'nonce'>);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`Manager - Received unknown data type: ${(data as { type: string }).type}`)
|
||||
})
|
||||
logger.warn(`Manager - Received unknown data type: ${(data as { type: string }).type}`);
|
||||
});
|
||||
|
||||
await app.listen({
|
||||
host: GATEWAY_HOST,
|
||||
port: GATEWAY_PORT,
|
||||
})
|
||||
});
|
||||
|
||||
logger.info(`Gateway manager listening on port ${GATEWAY_PORT}`)
|
||||
logger.info(`Gateway manager listening on port ${GATEWAY_PORT}`);
|
||||
|
||||
await gatewayManager.spawnShards()
|
||||
await gatewayManager.spawnShards();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { join as joinPath } from 'node:path'
|
||||
import { Worker } from 'node:worker_threads'
|
||||
import { join as joinPath } from 'node:path';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
import {
|
||||
DISCORD_TOKEN,
|
||||
EVENT_HANDLER_AUTHORIZATION,
|
||||
@@ -9,18 +9,18 @@ import {
|
||||
MESSAGEQUEUE_PASSWORD,
|
||||
MESSAGEQUEUE_URL,
|
||||
MESSAGEQUEUE_USERNAME,
|
||||
} from '../../config.js'
|
||||
import { getDirnameFromFileUrl } from '../../util.js'
|
||||
import gatewayManager, { logger } from '../gatewayManager.js'
|
||||
import type { ManagerMessage, ShardInfo, WorkerCreateData, WorkerMessage } from './types.js'
|
||||
} from '../../config.js';
|
||||
import { getDirnameFromFileUrl } from '../../util.js';
|
||||
import gatewayManager, { logger } from '../gatewayManager.js';
|
||||
import type { ManagerMessage, ShardInfo, WorkerCreateData, WorkerMessage } from './types.js';
|
||||
|
||||
// the string is the nonce of the request
|
||||
export const shardInfoRequests = new Map<string, (value: ShardInfo) => void>()
|
||||
export const shardInfoRequests = new Map<string, (value: ShardInfo) => void>();
|
||||
|
||||
export function createWorker(workerId: number): Worker {
|
||||
// the Worker constructor requires either a relative path compared to the process CWD or an absolute one, so to get one relative we need to use import.meta.url
|
||||
const currentFolder = getDirnameFromFileUrl(import.meta.url)
|
||||
const workerFilePath = joinPath(currentFolder, './worker.js')
|
||||
const currentFolder = getDirnameFromFileUrl(import.meta.url);
|
||||
const workerFilePath = joinPath(currentFolder, './worker.js');
|
||||
|
||||
const worker = new Worker(workerFilePath, {
|
||||
workerData: {
|
||||
@@ -43,32 +43,32 @@ export function createWorker(workerId: number): Worker {
|
||||
url: MESSAGEQUEUE_URL,
|
||||
},
|
||||
} satisfies WorkerCreateData,
|
||||
})
|
||||
});
|
||||
|
||||
worker.on('message', async (message: ManagerMessage) => {
|
||||
if (message.type === 'RequestIdentify') {
|
||||
logger.info(`Requesting identify for shardId: #${message.shardId}`)
|
||||
await gatewayManager.requestIdentify(message.shardId)
|
||||
logger.info(`Requesting identify for shardId: #${message.shardId}`);
|
||||
await gatewayManager.requestIdentify(message.shardId);
|
||||
|
||||
worker.postMessage({
|
||||
type: 'AllowIdentify',
|
||||
shardId: message.shardId,
|
||||
} satisfies WorkerMessage)
|
||||
} satisfies WorkerMessage);
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (message.type === 'ShardInfo') {
|
||||
shardInfoRequests.get(message.nonce)?.(message)
|
||||
shardInfoRequests.delete(message.nonce)
|
||||
return
|
||||
shardInfoRequests.get(message.nonce)?.(message);
|
||||
shardInfoRequests.delete(message.nonce);
|
||||
return;
|
||||
}
|
||||
if (message.type === 'ShardIdentified') {
|
||||
logger.info(`Shard #${message.shardId} identified`)
|
||||
return
|
||||
logger.info(`Shard #${message.shardId} identified`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`Worker - Received unknown message type: ${(message as { type: string }).type}`)
|
||||
})
|
||||
logger.warn(`Worker - Received unknown message type: ${(message as { type: string }).type}`);
|
||||
});
|
||||
|
||||
return worker
|
||||
return worker;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DiscordUpdatePresence, ShardSocketRequest } from '@discordeno/bot'
|
||||
import type { DiscordUpdatePresence, ShardSocketRequest } from '@discordeno/bot';
|
||||
|
||||
export type ManagerMessage = ManagerRequestIdentify | ManagerShardIdentified | ManagerShardPrepared | ManagerShardInfo
|
||||
export type ManagerMessage = ManagerRequestIdentify | ManagerShardIdentified | ManagerShardPrepared | ManagerShardInfo;
|
||||
export type WorkerMessage =
|
||||
| WorkerIdentifyShard
|
||||
| WorkerPrepareShard
|
||||
@@ -8,93 +8,93 @@ export type WorkerMessage =
|
||||
| WorkerAllowIdentify
|
||||
| WorkerShardPayload
|
||||
| WorkerPresencesUpdate
|
||||
| WorkerShardInfo
|
||||
| WorkerShardInfo;
|
||||
|
||||
export interface WorkerIdentifyShard {
|
||||
type: 'IdentifyShard'
|
||||
shardId: number
|
||||
type: 'IdentifyShard';
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
export interface WorkerPrepareShard {
|
||||
type: 'PrepareShard'
|
||||
shardId: number
|
||||
totalShards: number
|
||||
type: 'PrepareShard';
|
||||
shardId: number;
|
||||
totalShards: number;
|
||||
}
|
||||
|
||||
export interface WorkerSwitchShards {
|
||||
type: 'SwitchShards'
|
||||
type: 'SwitchShards';
|
||||
}
|
||||
|
||||
export interface WorkerAllowIdentify {
|
||||
type: 'AllowIdentify'
|
||||
shardId: number
|
||||
type: 'AllowIdentify';
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
export interface ManagerRequestIdentify {
|
||||
type: 'RequestIdentify'
|
||||
shardId: number
|
||||
type: 'RequestIdentify';
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
export interface WorkerShardPayload {
|
||||
type: 'ShardPayload'
|
||||
shardId: number
|
||||
payload: ShardSocketRequest
|
||||
type: 'ShardPayload';
|
||||
shardId: number;
|
||||
payload: ShardSocketRequest;
|
||||
}
|
||||
|
||||
export interface WorkerPresencesUpdate {
|
||||
type: 'EditShardsPresence'
|
||||
payload: DiscordUpdatePresence
|
||||
type: 'EditShardsPresence';
|
||||
payload: DiscordUpdatePresence;
|
||||
}
|
||||
|
||||
export interface WorkerShardInfo {
|
||||
type: 'GetShardInfo'
|
||||
shardId: number
|
||||
nonce: string
|
||||
type: 'GetShardInfo';
|
||||
shardId: number;
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
export interface WorkerCreateData {
|
||||
connectionData: {
|
||||
intents: number
|
||||
token: string
|
||||
url: string
|
||||
version: number
|
||||
totalShards: number
|
||||
}
|
||||
intents: number;
|
||||
token: string;
|
||||
url: string;
|
||||
version: number;
|
||||
totalShards: number;
|
||||
};
|
||||
eventHandler: {
|
||||
urls: string[]
|
||||
authentication: string
|
||||
}
|
||||
workerId: number
|
||||
urls: string[];
|
||||
authentication: string;
|
||||
};
|
||||
workerId: number;
|
||||
messageQueue: {
|
||||
enabled: boolean
|
||||
username?: string
|
||||
password?: string
|
||||
url?: string
|
||||
}
|
||||
enabled: boolean;
|
||||
username?: string;
|
||||
password?: string;
|
||||
url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ShardInfo {
|
||||
shardId: number
|
||||
rtt: number
|
||||
shardId: number;
|
||||
rtt: number;
|
||||
// the nonce is to bind to the request
|
||||
nonce: string
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
export interface ManagerShardInfo extends ShardInfo {
|
||||
type: 'ShardInfo'
|
||||
type: 'ShardInfo';
|
||||
}
|
||||
|
||||
export interface ManagerGetShardInfoFromGuildId {
|
||||
type: 'ShardInfoFromGuild'
|
||||
guildId: string | undefined
|
||||
type: 'ShardInfoFromGuild';
|
||||
guildId: string | undefined;
|
||||
}
|
||||
|
||||
export interface ManagerShardIdentified {
|
||||
type: 'ShardIdentified'
|
||||
shardId: number
|
||||
type: 'ShardIdentified';
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
export interface ManagerShardPrepared {
|
||||
type: 'ShardPrepared'
|
||||
shardId: number
|
||||
type: 'ShardPrepared';
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
@@ -1,125 +1,125 @@
|
||||
import assert from 'node:assert'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { workerData as _workerData, parentPort } from 'node:worker_threads'
|
||||
import { type Camelize, createLogger, DiscordenoShard, type DiscordGatewayPayload, GatewayOpcodes, ShardSocketCloseCodes } from '@discordeno/bot'
|
||||
import { type Channel as amqpChannel, connect as connectAmqp } from 'amqplib'
|
||||
import { promiseWithResolvers } from '../../util.js'
|
||||
import type { ManagerMessage, WorkerCreateData, WorkerMessage } from './types.js'
|
||||
import assert from 'node:assert';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { workerData as _workerData, parentPort } from 'node:worker_threads';
|
||||
import { type Camelize, createLogger, DiscordenoShard, type DiscordGatewayPayload, GatewayOpcodes, ShardSocketCloseCodes } from '@discordeno/bot';
|
||||
import { type Channel as amqpChannel, connect as connectAmqp } from 'amqplib';
|
||||
import { promiseWithResolvers } from '../../util.js';
|
||||
import type { ManagerMessage, WorkerCreateData, WorkerMessage } from './types.js';
|
||||
|
||||
assert(parentPort)
|
||||
assert(parentPort);
|
||||
|
||||
const workerData: WorkerCreateData = _workerData
|
||||
const workerData: WorkerCreateData = _workerData;
|
||||
|
||||
const logger = createLogger({ name: `Worker #${workerData.workerId}` })
|
||||
const logger = createLogger({ name: `Worker #${workerData.workerId}` });
|
||||
|
||||
const identifyPromises = new Map<number, () => void>()
|
||||
const shards = new Map<number, DiscordenoShard>()
|
||||
const pendingShards = new Map<number, DiscordenoShard>()
|
||||
const identifyPromises = new Map<number, () => void>();
|
||||
const shards = new Map<number, DiscordenoShard>();
|
||||
const pendingShards = new Map<number, DiscordenoShard>();
|
||||
|
||||
let totalShards = workerData.connectionData.totalShards
|
||||
let totalShards = workerData.connectionData.totalShards;
|
||||
|
||||
let rabbitMQChannel: amqpChannel | undefined
|
||||
let rabbitMQChannel: amqpChannel | undefined;
|
||||
|
||||
if (workerData.messageQueue.enabled) {
|
||||
await connectToRabbitMQ()
|
||||
await connectToRabbitMQ();
|
||||
}
|
||||
|
||||
parentPort.on('message', async (message: WorkerMessage) => {
|
||||
assert(parentPort)
|
||||
assert(parentPort);
|
||||
|
||||
if (message.type === 'IdentifyShard') {
|
||||
logger.info(`Starting to identify shard #${message.shardId}`)
|
||||
const shard = shards.get(message.shardId) ?? createShard(message.shardId)
|
||||
shards.set(message.shardId, shard)
|
||||
logger.info(`Starting to identify shard #${message.shardId}`);
|
||||
const shard = shards.get(message.shardId) ?? createShard(message.shardId);
|
||||
shards.set(message.shardId, shard);
|
||||
|
||||
await shard.identify()
|
||||
await shard.identify();
|
||||
|
||||
parentPort.postMessage({
|
||||
type: 'ShardIdentified',
|
||||
shardId: message.shardId,
|
||||
} satisfies ManagerMessage)
|
||||
} satisfies ManagerMessage);
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (message.type === 'PrepareShard') {
|
||||
logger.info(`Preparing shard #${message.shardId}`)
|
||||
totalShards = message.totalShards
|
||||
let shard = pendingShards.get(message.shardId)
|
||||
logger.info(`Preparing shard #${message.shardId}`);
|
||||
totalShards = message.totalShards;
|
||||
let shard = pendingShards.get(message.shardId);
|
||||
if (!shard) {
|
||||
shard = createShard(message.shardId)
|
||||
pendingShards.set(message.shardId, shard)
|
||||
shard = createShard(message.shardId);
|
||||
pendingShards.set(message.shardId, shard);
|
||||
}
|
||||
|
||||
// Ignore the events
|
||||
// TODO: If you need 'gateway.resharding.updateGuildsShardId' it you can listen to only the ready event and use the data from that event for the function call
|
||||
shard.events.message = () => {}
|
||||
shard.events.message = () => {};
|
||||
|
||||
await shard.identify()
|
||||
await shard.identify();
|
||||
|
||||
parentPort.postMessage({
|
||||
type: 'ShardPrepared',
|
||||
shardId: message.shardId,
|
||||
} satisfies ManagerMessage)
|
||||
} satisfies ManagerMessage);
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (message.type === 'SwitchShards') {
|
||||
logger.info('Switching shards')
|
||||
logger.info('Switching shards');
|
||||
|
||||
// Change the message event for all shards
|
||||
for (const shard of pendingShards.values()) {
|
||||
shard.events.message = handleShardMessageEvent
|
||||
shard.events.message = handleShardMessageEvent;
|
||||
}
|
||||
|
||||
// Old shards stop processing events
|
||||
for (const shard of shards.values()) {
|
||||
const oldHandler = shard.events.message
|
||||
const oldHandler = shard.events.message;
|
||||
|
||||
shard.events.message = async function (_, message) {
|
||||
// Member checks need to continue but others can stop
|
||||
if (message.t === 'GUILD_MEMBERS_CHUNK') {
|
||||
oldHandler?.(shard, message)
|
||||
}
|
||||
oldHandler?.(shard, message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Shutdown the old shards
|
||||
const shardsToShutdown = Array.from(shards.values())
|
||||
const shardsToShutdown = Array.from(shards.values());
|
||||
|
||||
// Move the pending shards to the active shards
|
||||
shards.clear()
|
||||
shards.clear();
|
||||
for (const [shardId, shard] of pendingShards.entries()) {
|
||||
shards.set(shardId, shard)
|
||||
pendingShards.delete(shardId)
|
||||
shards.set(shardId, shard);
|
||||
pendingShards.delete(shardId);
|
||||
}
|
||||
|
||||
// Shutdown the old shards
|
||||
const promises = shardsToShutdown.map(async (shard) => {
|
||||
await shard.close(ShardSocketCloseCodes.Resharded, 'Shard is being resharded')
|
||||
logger.info(`Shard #${shard.id} has been shutdown`)
|
||||
})
|
||||
await shard.close(ShardSocketCloseCodes.Resharded, 'Shard is being resharded');
|
||||
logger.info(`Shard #${shard.id} has been shutdown`);
|
||||
});
|
||||
|
||||
await Promise.all(promises)
|
||||
await Promise.all(promises);
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (message.type === 'AllowIdentify') {
|
||||
identifyPromises.get(message.shardId)?.()
|
||||
identifyPromises.delete(message.shardId)
|
||||
identifyPromises.get(message.shardId)?.();
|
||||
identifyPromises.delete(message.shardId);
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (message.type === 'ShardPayload') {
|
||||
const shard = shards.get(message.shardId)
|
||||
const shard = shards.get(message.shardId);
|
||||
|
||||
if (!shard) return
|
||||
if (!shard) return;
|
||||
|
||||
await shard.send(message.payload)
|
||||
await shard.send(message.payload);
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (message.type === 'EditShardsPresence') {
|
||||
const shardsArray = Array.from(shards.values())
|
||||
const shardsArray = Array.from(shards.values());
|
||||
const promises = shardsArray.map(async (shard) => {
|
||||
await shard.send({
|
||||
op: GatewayOpcodes.PresenceUpdate,
|
||||
@@ -129,11 +129,11 @@ parentPort.on('message', async (message: WorkerMessage) => {
|
||||
activities: message.payload.activities,
|
||||
status: message.payload.status,
|
||||
},
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(promises)
|
||||
return
|
||||
await Promise.all(promises);
|
||||
return;
|
||||
}
|
||||
if (message.type === 'GetShardInfo') {
|
||||
const status = {
|
||||
@@ -141,15 +141,15 @@ parentPort.on('message', async (message: WorkerMessage) => {
|
||||
shardId: message.shardId,
|
||||
rtt: shards.get(message.shardId)?.heart.rtt ?? -1,
|
||||
nonce: message.nonce,
|
||||
} satisfies ManagerMessage
|
||||
} satisfies ManagerMessage;
|
||||
|
||||
parentPort?.postMessage(status)
|
||||
parentPort?.postMessage(status);
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`Received unknown message type: ${(message as { type: string }).type}`)
|
||||
})
|
||||
logger.warn(`Received unknown message type: ${(message as { type: string }).type}`);
|
||||
});
|
||||
|
||||
function createShard(shardId: number): DiscordenoShard {
|
||||
const shard = new DiscordenoShard({
|
||||
@@ -169,62 +169,62 @@ function createShard(shardId: number): DiscordenoShard {
|
||||
version: workerData.connectionData.version,
|
||||
transportCompression: null,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
shard.requestIdentify = async () => {
|
||||
assert(parentPort)
|
||||
assert(parentPort);
|
||||
|
||||
const { promise, resolve } = promiseWithResolvers<void>()
|
||||
const { promise, resolve } = promiseWithResolvers<void>();
|
||||
|
||||
parentPort.postMessage({
|
||||
type: 'RequestIdentify',
|
||||
shardId,
|
||||
} satisfies ManagerMessage)
|
||||
} satisfies ManagerMessage);
|
||||
|
||||
identifyPromises.set(shardId, resolve)
|
||||
identifyPromises.set(shardId, resolve);
|
||||
|
||||
return await promise
|
||||
}
|
||||
return await promise;
|
||||
};
|
||||
|
||||
// We do not want to camelize the packet, so we need to override the function as the default behavior is to camelize
|
||||
shard.forwardToBot = (packet) => {
|
||||
shard.events.message?.(shard, packet)
|
||||
}
|
||||
shard.events.message?.(shard, packet);
|
||||
};
|
||||
|
||||
shard.events.message = handleShardMessageEvent
|
||||
shard.events.message = handleShardMessageEvent;
|
||||
|
||||
return shard
|
||||
return shard;
|
||||
}
|
||||
|
||||
async function handleShardMessageEvent(shard: DiscordenoShard, payload: Camelize<DiscordGatewayPayload>) {
|
||||
const body = JSON.stringify({ payload, shardId: shard.id })
|
||||
const body = JSON.stringify({ payload, shardId: shard.id });
|
||||
|
||||
if (workerData.messageQueue.enabled) {
|
||||
if (!rabbitMQChannel) {
|
||||
logger.error('The RabbitMQ channel has not been created. The event will be lost')
|
||||
return
|
||||
logger.error('The RabbitMQ channel has not been created. The event will be lost');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = Buffer.from(body)
|
||||
const discordData = JSON.stringify(payload.d)
|
||||
const message = Buffer.from(body);
|
||||
const discordData = JSON.stringify(payload.d);
|
||||
|
||||
const deduplicationHash = createHash('sha1')
|
||||
deduplicationHash.update(discordData)
|
||||
const deduplicationHash = createHash('sha1');
|
||||
deduplicationHash.update(discordData);
|
||||
|
||||
rabbitMQChannel.publish('gatewayMessage', '', message, {
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
'x-deduplication-header': deduplicationHash.digest('hex'),
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const url = workerData.eventHandler.urls[shard.id % workerData.eventHandler.urls.length]
|
||||
const url = workerData.eventHandler.urls[shard.id % workerData.eventHandler.urls.length];
|
||||
if (!url) {
|
||||
logger.error('No url found to send events to')
|
||||
return
|
||||
logger.error('No url found to send events to');
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch(url, {
|
||||
@@ -234,35 +234,35 @@ async function handleShardMessageEvent(shard: DiscordenoShard, payload: Camelize
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: workerData.eventHandler.authentication,
|
||||
},
|
||||
}).catch((error) => logger.error('Failed to send events to the bot code', error))
|
||||
}).catch((error) => logger.error('Failed to send events to the bot code', error));
|
||||
}
|
||||
|
||||
async function connectToRabbitMQ(): Promise<void> {
|
||||
rabbitMQChannel = undefined
|
||||
const messageQueue = workerData.messageQueue
|
||||
rabbitMQChannel = undefined;
|
||||
const messageQueue = workerData.messageQueue;
|
||||
|
||||
const connection = await connectAmqp(`amqp://${messageQueue.username}:${messageQueue.password}@${messageQueue.url}`).catch((error) => {
|
||||
logger.error('Failed to connect to RabbitMQ, retrying in 1s.', error)
|
||||
setTimeout(connectToRabbitMQ, 1000)
|
||||
})
|
||||
logger.error('Failed to connect to RabbitMQ, retrying in 1s.', error);
|
||||
setTimeout(connectToRabbitMQ, 1000);
|
||||
});
|
||||
|
||||
if (!connection) return
|
||||
if (!connection) return;
|
||||
|
||||
connection.on('close', () => {
|
||||
rabbitMQChannel = undefined
|
||||
setTimeout(connectToRabbitMQ, 1000)
|
||||
})
|
||||
rabbitMQChannel = undefined;
|
||||
setTimeout(connectToRabbitMQ, 1000);
|
||||
});
|
||||
connection.on('error', (error) => {
|
||||
rabbitMQChannel = undefined
|
||||
logger.error('There was an error in the connection with RabbitMQ, reconnecting in 1s.', error)
|
||||
setTimeout(connectToRabbitMQ, 1000)
|
||||
})
|
||||
rabbitMQChannel = undefined;
|
||||
logger.error('There was an error in the connection with RabbitMQ, reconnecting in 1s.', error);
|
||||
setTimeout(connectToRabbitMQ, 1000);
|
||||
});
|
||||
|
||||
const channel = await connection.createChannel().catch((error) => {
|
||||
logger.error('There was an error creating the RabbitMQ channel', error)
|
||||
})
|
||||
logger.error('There was an error creating the RabbitMQ channel', error);
|
||||
});
|
||||
|
||||
if (!channel) return
|
||||
if (!channel) return;
|
||||
|
||||
const exchange = await channel
|
||||
.assertExchange('gatewayMessage', 'x-message-deduplication', {
|
||||
@@ -273,10 +273,10 @@ async function connectToRabbitMQ(): Promise<void> {
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('There was an error asserting the exchange', error)
|
||||
})
|
||||
logger.error('There was an error asserting the exchange', error);
|
||||
});
|
||||
|
||||
if (!exchange) return
|
||||
if (!exchange) return;
|
||||
|
||||
rabbitMQChannel = channel
|
||||
rabbitMQChannel = channel;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import fastifyMultipart, { type MultipartFile, type MultipartValue } from '@fastify/multipart'
|
||||
import fastify, { type FastifyInstance } from 'fastify'
|
||||
import { REST_AUTHORIZATION } from '../config.js'
|
||||
import fastifyMultipart, { type MultipartFile, type MultipartValue } from '@fastify/multipart';
|
||||
import fastify, { type FastifyInstance } from 'fastify';
|
||||
import { REST_AUTHORIZATION } from '../config.js';
|
||||
|
||||
export function buildFastifyApp(): FastifyInstance {
|
||||
const app = fastify()
|
||||
const app = fastify();
|
||||
|
||||
app.register(fastifyMultipart, { attachFieldsToBody: true })
|
||||
app.register(fastifyMultipart, { attachFieldsToBody: true });
|
||||
|
||||
// Authorization check
|
||||
app.addHook('onRequest', async (req, res) => {
|
||||
if (req.headers.authorization !== REST_AUTHORIZATION) {
|
||||
res.status(401).send({
|
||||
message: 'Credentials not valid.',
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return app
|
||||
return app;
|
||||
}
|
||||
|
||||
export async function parseMultiformBody(body: unknown): Promise<FormData> {
|
||||
const form = new FormData()
|
||||
const form = new FormData();
|
||||
|
||||
if (typeof body !== 'object' || !body) return form
|
||||
if (typeof body !== 'object' || !body) return form;
|
||||
|
||||
for (const objectValue of Object.values(body)) {
|
||||
const value = objectValue as MultipartFile | MultipartValue
|
||||
const value = objectValue as MultipartFile | MultipartValue;
|
||||
|
||||
if (value.type === 'file') {
|
||||
form.append(value.fieldname, new Blob([await value.toBuffer()]), value.filename)
|
||||
form.append(value.fieldname, new Blob([await value.toBuffer()]), value.filename);
|
||||
}
|
||||
if (value.type === 'field' && typeof value.value === 'string') {
|
||||
form.append(value.fieldname, value.value)
|
||||
form.append(value.fieldname, value.value);
|
||||
}
|
||||
}
|
||||
|
||||
return form
|
||||
return form;
|
||||
}
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import type { RequestMethods } from '@discordeno/bot'
|
||||
import { REST_HOST, REST_PORT } from '../config.js'
|
||||
import { buildFastifyApp, parseMultiformBody } from './fastify.js'
|
||||
import restManager, { logger } from './restManager.js'
|
||||
import type { RequestMethods } from '@discordeno/bot';
|
||||
import { REST_HOST, REST_PORT } from '../config.js';
|
||||
import { buildFastifyApp, parseMultiformBody } from './fastify.js';
|
||||
import restManager, { logger } from './restManager.js';
|
||||
|
||||
const app = buildFastifyApp()
|
||||
const app = buildFastifyApp();
|
||||
|
||||
app.get('/timecheck', async (_req, res) => {
|
||||
res.status(200).send({ message: Date.now() })
|
||||
})
|
||||
res.status(200).send({ message: Date.now() });
|
||||
});
|
||||
|
||||
app.all('/*', async (req, res) => {
|
||||
let url = req.originalUrl
|
||||
let url = req.originalUrl;
|
||||
|
||||
if (url.startsWith('/v')) {
|
||||
url = url.slice(url.indexOf('/', 2))
|
||||
url = url.slice(url.indexOf('/', 2));
|
||||
}
|
||||
|
||||
const isMultipart = req.headers['content-type']?.startsWith('multipart/form-data')
|
||||
const hasBody = req.method !== 'GET' && req.method !== 'DELETE'
|
||||
const body = hasBody ? (isMultipart ? await parseMultiformBody(req.body) : req.body) : undefined
|
||||
const isMultipart = req.headers['content-type']?.startsWith('multipart/form-data');
|
||||
const hasBody = req.method !== 'GET' && req.method !== 'DELETE';
|
||||
const body = hasBody ? (isMultipart ? await parseMultiformBody(req.body) : req.body) : undefined;
|
||||
|
||||
try {
|
||||
const result = await restManager.makeRequest(req.method as RequestMethods, url, {
|
||||
body,
|
||||
})
|
||||
});
|
||||
|
||||
if (result) {
|
||||
res.status(200).send(result)
|
||||
return
|
||||
res.status(200).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(204).send({})
|
||||
res.status(204).send({});
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
logger.error(error);
|
||||
|
||||
res.status(500).send({
|
||||
message: error,
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
await app.listen({
|
||||
host: REST_HOST,
|
||||
port: REST_PORT,
|
||||
})
|
||||
});
|
||||
|
||||
logger.info(`REST Proxy listening on port ${REST_PORT}`)
|
||||
logger.info(`REST Proxy listening on port ${REST_PORT}`);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { RestManager } from '@discordeno/bot'
|
||||
import { InfluxDB, Point } from '@influxdata/influxdb-client'
|
||||
import { INFLUX_BUCKET, INFLUX_ENABLED, INFLUX_ORG, INFLUX_TOKEN, INFLUX_URL } from '../config.js'
|
||||
import type { RestManager } from '@discordeno/bot';
|
||||
import { InfluxDB, Point } from '@influxdata/influxdb-client';
|
||||
import { INFLUX_BUCKET, INFLUX_ENABLED, INFLUX_ORG, INFLUX_TOKEN, INFLUX_URL } from '../config.js';
|
||||
|
||||
export const influxDB = INFLUX_ENABLED ? new InfluxDB({ url: INFLUX_URL!, token: INFLUX_TOKEN! }) : undefined
|
||||
export const influx = INFLUX_ENABLED && influxDB ? influxDB.getWriteApi(INFLUX_ORG!, INFLUX_BUCKET!) : undefined
|
||||
export const influxDB = INFLUX_ENABLED ? new InfluxDB({ url: INFLUX_URL!, token: INFLUX_TOKEN! }) : undefined;
|
||||
export const influx = INFLUX_ENABLED && influxDB ? influxDB.getWriteApi(INFLUX_ORG!, INFLUX_BUCKET!) : undefined;
|
||||
|
||||
export const setupRestAnalyticsHooks = (rest: RestManager, logger: RestManager['logger']): void => {
|
||||
// If influxdb data is provided, enable analytics in this proxy.
|
||||
if (!influx) return
|
||||
if (!influx) return;
|
||||
|
||||
const originalSendRequest = rest.sendRequest
|
||||
const originalSendRequest = rest.sendRequest;
|
||||
|
||||
rest.sendRequest = async (options) => {
|
||||
const fetchingPoint = new Point('restEvents')
|
||||
@@ -17,31 +17,31 @@ export const setupRestAnalyticsHooks = (rest: RestManager, logger: RestManager['
|
||||
.stringField('type', 'REQUEST_FETCHING')
|
||||
.tag('method', options.method)
|
||||
.tag('route', options.route)
|
||||
.tag('bucket', options.bucketId ?? 'NA')
|
||||
.tag('bucket', options.bucketId ?? 'NA');
|
||||
|
||||
influx.writePoint(fetchingPoint)
|
||||
influx.writePoint(fetchingPoint);
|
||||
|
||||
await originalSendRequest(options)
|
||||
await originalSendRequest(options);
|
||||
|
||||
const fetchedPoint = new Point('restEvents')
|
||||
.timestamp(new Date())
|
||||
.stringField('type', 'REQUEST_FETCHED')
|
||||
.tag('method', options.method)
|
||||
.tag('route', options.route)
|
||||
.tag('bucket', options.bucketId ?? 'NA')
|
||||
.tag('bucket', options.bucketId ?? 'NA');
|
||||
// FIXME: rest.sendRequest returns Promise<void>, so there is no way currently to get the response status
|
||||
// .intField('status', response.status)
|
||||
|
||||
influx.writePoint(fetchedPoint)
|
||||
}
|
||||
influx.writePoint(fetchedPoint);
|
||||
};
|
||||
|
||||
setInterval(async () => {
|
||||
logger.info('Influx - Saving events...')
|
||||
logger.info('Influx - Saving events...');
|
||||
try {
|
||||
await influx.flush()
|
||||
logger.info('Influx - events saved!')
|
||||
await influx.flush();
|
||||
logger.info('Influx - events saved!');
|
||||
} catch (error) {
|
||||
logger.error('Influx - error saving events!', error)
|
||||
}
|
||||
}, 30_000 /* 30s */)
|
||||
logger.error('Influx - error saving events!', error);
|
||||
}
|
||||
}, 30_000 /* 30s */);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createLogger, createRestManager } from '@discordeno/bot'
|
||||
import { DISCORD_TOKEN } from '../config.js'
|
||||
import { setupRestAnalyticsHooks } from './influx.js'
|
||||
import { createLogger, createRestManager } from '@discordeno/bot';
|
||||
import { DISCORD_TOKEN } from '../config.js';
|
||||
import { setupRestAnalyticsHooks } from './influx.js';
|
||||
|
||||
const manager = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
})
|
||||
});
|
||||
|
||||
export const logger = createLogger({ name: 'REST' })
|
||||
export const logger = createLogger({ name: 'REST' });
|
||||
|
||||
setupRestAnalyticsHooks(manager, logger)
|
||||
setupRestAnalyticsHooks(manager, logger);
|
||||
|
||||
export default manager
|
||||
export default manager;
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// This is a "polyfill" for the `Promise.withResolves`, while node 22 does support it, node 18 does not and this template does support node 18
|
||||
export function promiseWithResolvers<T>(): PromiseWithResolvers<T> {
|
||||
let resolve!: (data: T | PromiseLike<T>) => void
|
||||
let reject!: (reason?: any) => void
|
||||
let resolve!: (data: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: any) => void;
|
||||
const promise = new Promise<T>((_resolve, _reject) => {
|
||||
resolve = _resolve
|
||||
reject = _reject
|
||||
})
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
|
||||
return { promise, resolve, reject }
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
// This re-creates the `__dirname` that exists in CommonJS, in ESM node added `import.meta.dirname` but it is for node 20+, so we need this util to support node 18
|
||||
export function getDirnameFromFileUrl(url: string): string {
|
||||
return dirname(fileURLToPath(url))
|
||||
return dirname(fileURLToPath(url));
|
||||
}
|
||||
|
||||
export interface PromiseWithResolvers<T> {
|
||||
promise: Promise<T>
|
||||
resolve: (data: T | PromiseLike<T>) => void
|
||||
reject: (reason?: any) => void
|
||||
promise: Promise<T>;
|
||||
resolve: (data: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Collection, createBot, Intents } from '@discordeno/bot'
|
||||
import { configs } from './config.js'
|
||||
import type { Command } from './types/commands.js'
|
||||
import { Collection, createBot, Intents } from '@discordeno/bot';
|
||||
import { configs } from './config.js';
|
||||
import type { Command } from './types/commands.js';
|
||||
|
||||
const rawBot = createBot({
|
||||
token: configs.token,
|
||||
@@ -13,11 +13,11 @@ const rawBot = createBot({
|
||||
token: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export const bot = rawBot as BotWithCommands
|
||||
export const bot = rawBot as BotWithCommands;
|
||||
|
||||
// Create the command collection
|
||||
bot.commands = new Collection()
|
||||
bot.commands = new Collection();
|
||||
|
||||
export type BotWithCommands = typeof rawBot & { commands: Collection<string, Command> }
|
||||
export type BotWithCommands = typeof rawBot & { commands: Collection<string, Command> };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { bot } from './bot.js'
|
||||
import type { Command } from './types/commands.js'
|
||||
import { bot } from './bot.js';
|
||||
import type { Command } from './types/commands.js';
|
||||
|
||||
export function createCommand(command: Command): void {
|
||||
bot.commands.set(command.name, command)
|
||||
bot.commands.set(command.name, command);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ApplicationCommandTypes, snowflakeToTimestamp } from '@discordeno/bot'
|
||||
import { createCommand } from '../commands.js'
|
||||
import { ApplicationCommandTypes, snowflakeToTimestamp } from '@discordeno/bot';
|
||||
import { createCommand } from '../commands.js';
|
||||
|
||||
createCommand({
|
||||
name: 'ping',
|
||||
description: 'Ping the Bot!',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
async execute(interaction) {
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
|
||||
|
||||
await interaction.respond(`🏓 Pong! ${ping}ms`)
|
||||
await interaction.respond(`🏓 Pong! ${ping}ms`);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
const token = process.env.BOT_TOKEN
|
||||
const devGuildId = process.env.DEV_GUILD_ID
|
||||
const token = process.env.BOT_TOKEN;
|
||||
const devGuildId = process.env.DEV_GUILD_ID;
|
||||
|
||||
if (!token) throw new Error('Missing BOT_TOKEN environment variable')
|
||||
if (!devGuildId) throw new Error('Missing DEV_GUILD_ID environment variable')
|
||||
if (!token) throw new Error('Missing BOT_TOKEN environment variable');
|
||||
if (!devGuildId) throw new Error('Missing DEV_GUILD_ID environment variable');
|
||||
|
||||
export const configs: Config = {
|
||||
/** Get token from ENV variable */
|
||||
token,
|
||||
/** The server id where you develop your bot and want dev commands created. */
|
||||
devGuildId: BigInt(devGuildId),
|
||||
}
|
||||
};
|
||||
|
||||
export interface Config {
|
||||
token: string
|
||||
devGuildId: bigint
|
||||
token: string;
|
||||
devGuildId: bigint;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { InteractionTypes } from '@discordeno/bot'
|
||||
import { bot } from '../bot.js'
|
||||
import logger from '../utils/logger.js'
|
||||
import { InteractionTypes } from '@discordeno/bot';
|
||||
import { bot } from '../bot.js';
|
||||
import logger from '../utils/logger.js';
|
||||
|
||||
bot.events.interactionCreate = (interaction) => {
|
||||
if (!interaction.data) return
|
||||
if (!interaction.data) return;
|
||||
|
||||
switch (interaction.type) {
|
||||
case InteractionTypes.ApplicationCommand:
|
||||
logger.info(`[Application Command] ${interaction.data.name} command executed.`)
|
||||
logger.info(`[Application Command] ${interaction.data.name} command executed.`);
|
||||
|
||||
bot.commands.get(interaction.data.name)?.execute(interaction)
|
||||
break
|
||||
}
|
||||
bot.commands.get(interaction.data.name)?.execute(interaction);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { bot } from '../bot.js'
|
||||
import logger from '../utils/logger.js'
|
||||
import { bot } from '../bot.js';
|
||||
import logger from '../utils/logger.js';
|
||||
|
||||
bot.events.ready = ({ shardId }) => {
|
||||
logger.info(`[READY] Shard ${shardId} is ready!`)
|
||||
logger.info(`[READY] Shard ${shardId} is ready!`);
|
||||
|
||||
if (shardId === bot.gateway.lastShardId) {
|
||||
botFullyReady()
|
||||
}
|
||||
botFullyReady();
|
||||
}
|
||||
};
|
||||
|
||||
// This function lets you run custom code when all your bot's shards are online.
|
||||
function botFullyReady(): void {
|
||||
// Do stuff that you want that get execute only when the bot is fully online.
|
||||
|
||||
logger.info('[READY] Bot is fully online.')
|
||||
logger.info('[READY] Bot is fully online.');
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'dotenv/config'
|
||||
import 'dotenv/config';
|
||||
|
||||
import { bot } from './bot.js'
|
||||
import importDirectory from './utils/loader.js'
|
||||
import logger from './utils/logger.js'
|
||||
import { bot } from './bot.js';
|
||||
import importDirectory from './utils/loader.js';
|
||||
import logger from './utils/logger.js';
|
||||
|
||||
logger.info('Starting bot...')
|
||||
logger.info('Starting bot...');
|
||||
|
||||
logger.info('Loading commands...')
|
||||
await importDirectory('./dist/commands')
|
||||
logger.info('Loading commands...');
|
||||
await importDirectory('./dist/commands');
|
||||
|
||||
logger.info('Loading events...')
|
||||
await importDirectory('./dist/events')
|
||||
logger.info('Loading events...');
|
||||
await importDirectory('./dist/events');
|
||||
|
||||
await bot.start()
|
||||
await bot.start();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'dotenv/config'
|
||||
import 'dotenv/config';
|
||||
|
||||
import importDirectory from './utils/loader.js'
|
||||
import logger from './utils/logger.js'
|
||||
import { updateApplicationCommands } from './utils/updateCommands.js'
|
||||
import importDirectory from './utils/loader.js';
|
||||
import logger from './utils/logger.js';
|
||||
import { updateApplicationCommands } from './utils/updateCommands.js';
|
||||
|
||||
logger.info('Loading commands...')
|
||||
await importDirectory('./dist/commands')
|
||||
logger.info('Loading commands...');
|
||||
await importDirectory('./dist/commands');
|
||||
|
||||
logger.info('Updating commands...')
|
||||
await updateApplicationCommands()
|
||||
logger.info('Updating commands...');
|
||||
await updateApplicationCommands();
|
||||
|
||||
logger.info('Done!')
|
||||
logger.info('Done!');
|
||||
|
||||
// We need to manually exit as the REST Manager has timeouts that will keep NodeJS alive
|
||||
process.exit()
|
||||
process.exit();
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { ApplicationCommandOption, ApplicationCommandTypes } from '@discordeno/bot'
|
||||
import type { bot } from '../bot.js'
|
||||
import type { ApplicationCommandOption, ApplicationCommandTypes } from '@discordeno/bot';
|
||||
import type { bot } from '../bot.js';
|
||||
|
||||
export interface Command {
|
||||
/** The name of this command. */
|
||||
name: string
|
||||
name: string;
|
||||
/** What does this command do? */
|
||||
description: string
|
||||
description: string;
|
||||
/** The type of command this is. */
|
||||
type: ApplicationCommandTypes
|
||||
type: ApplicationCommandTypes;
|
||||
/** Whether or not this command is for the dev server only. */
|
||||
devOnly?: boolean
|
||||
devOnly?: boolean;
|
||||
/** The options for this command */
|
||||
options?: ApplicationCommandOption[]
|
||||
options?: ApplicationCommandOption[];
|
||||
/** This will be executed when the command is run. */
|
||||
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction) => unknown
|
||||
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction) => unknown;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import logger from './logger.js'
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import logger from './logger.js';
|
||||
|
||||
export default async function importDirectory(folder: string): Promise<void> {
|
||||
const files = await readdir(folder, { recursive: true })
|
||||
const files = await readdir(folder, { recursive: true });
|
||||
|
||||
for (const filename of files) {
|
||||
if (!filename.endsWith('.js')) continue
|
||||
if (!filename.endsWith('.js')) continue;
|
||||
|
||||
// Using `file://` and `process.cwd()` to avoid weird issues with relative paths and/or Windows
|
||||
await import(`file://${process.cwd()}/${folder}/${filename}`).catch((x) =>
|
||||
logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import chalk from 'chalk'
|
||||
import chalk from 'chalk';
|
||||
|
||||
export enum LogLevels {
|
||||
Debug,
|
||||
@@ -15,70 +15,70 @@ const prefixes = new Map<LogLevels, string>([
|
||||
[LogLevels.Warn, 'WARN'],
|
||||
[LogLevels.Error, 'ERROR'],
|
||||
[LogLevels.Fatal, 'FATAL'],
|
||||
])
|
||||
]);
|
||||
|
||||
const noColor: (str: string) => string = (msg) => msg
|
||||
const noColor: (str: string) => string = (msg) => msg;
|
||||
const colorFunctions = new Map<LogLevels, (str: string) => string>([
|
||||
[LogLevels.Debug, chalk.gray],
|
||||
[LogLevels.Info, chalk.cyan],
|
||||
[LogLevels.Warn, chalk.yellow],
|
||||
[LogLevels.Error, (str: string) => chalk.red(str)],
|
||||
[LogLevels.Fatal, (str: string) => chalk.red.bold.italic(str)],
|
||||
])
|
||||
]);
|
||||
|
||||
export function createLogger({ logLevel = LogLevels.Info, name }: { logLevel?: LogLevels; name?: string } = {}): Logger {
|
||||
function log(level: LogLevels, ...args: any[]): void {
|
||||
if (level < logLevel) return
|
||||
if (level < logLevel) return;
|
||||
|
||||
let color = colorFunctions.get(level)
|
||||
if (!color) color = noColor
|
||||
let color = colorFunctions.get(level);
|
||||
if (!color) color = noColor;
|
||||
|
||||
const date = new Date()
|
||||
const date = new Date();
|
||||
const log = [
|
||||
`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`,
|
||||
color(prefixes.get(level) ?? 'DEBUG'),
|
||||
name ? `${name} >` : '>',
|
||||
...args,
|
||||
]
|
||||
];
|
||||
|
||||
switch (level) {
|
||||
case LogLevels.Debug:
|
||||
return console.debug(...log)
|
||||
return console.debug(...log);
|
||||
case LogLevels.Info:
|
||||
return console.info(...log)
|
||||
return console.info(...log);
|
||||
case LogLevels.Warn:
|
||||
return console.warn(...log)
|
||||
return console.warn(...log);
|
||||
case LogLevels.Error:
|
||||
return console.error(...log)
|
||||
return console.error(...log);
|
||||
case LogLevels.Fatal:
|
||||
return console.error(...log)
|
||||
return console.error(...log);
|
||||
default:
|
||||
return console.log(...log)
|
||||
return console.log(...log);
|
||||
}
|
||||
}
|
||||
|
||||
function setLevel(level: LogLevels): void {
|
||||
logLevel = level
|
||||
logLevel = level;
|
||||
}
|
||||
|
||||
function debug(...args: any[]): void {
|
||||
log(LogLevels.Debug, ...args)
|
||||
log(LogLevels.Debug, ...args);
|
||||
}
|
||||
|
||||
function info(...args: any[]): void {
|
||||
log(LogLevels.Info, ...args)
|
||||
log(LogLevels.Info, ...args);
|
||||
}
|
||||
|
||||
function warn(...args: any[]): void {
|
||||
log(LogLevels.Warn, ...args)
|
||||
log(LogLevels.Warn, ...args);
|
||||
}
|
||||
|
||||
function error(...args: any[]): void {
|
||||
log(LogLevels.Error, ...args)
|
||||
log(LogLevels.Error, ...args);
|
||||
}
|
||||
|
||||
function fatal(...args: any[]): void {
|
||||
log(LogLevels.Fatal, ...args)
|
||||
log(LogLevels.Fatal, ...args);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -89,18 +89,18 @@ export function createLogger({ logLevel = LogLevels.Info, name }: { logLevel?: L
|
||||
warn,
|
||||
error,
|
||||
fatal,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const logger = createLogger({ name: 'Main' })
|
||||
export default logger
|
||||
export const logger = createLogger({ name: 'Main' });
|
||||
export default logger;
|
||||
|
||||
export interface Logger {
|
||||
log: (level: LogLevels, ...args: any[]) => void
|
||||
debug: (...args: any[]) => void
|
||||
info: (...args: any[]) => void
|
||||
warn: (...args: any[]) => void
|
||||
error: (...args: any[]) => void
|
||||
fatal: (...args: any[]) => void
|
||||
setLevel: (level: LogLevels) => void
|
||||
log: (level: LogLevels, ...args: any[]) => void;
|
||||
debug: (...args: any[]) => void;
|
||||
info: (...args: any[]) => void;
|
||||
warn: (...args: any[]) => void;
|
||||
error: (...args: any[]) => void;
|
||||
fatal: (...args: any[]) => void;
|
||||
setLevel: (level: LogLevels) => void;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { bot } from '../bot.js'
|
||||
import { configs } from '../config.js'
|
||||
import { bot } from '../bot.js';
|
||||
import { configs } from '../config.js';
|
||||
|
||||
export async function updateApplicationCommands(): Promise<void> {
|
||||
await bot.helpers.upsertGlobalApplicationCommands(
|
||||
@@ -7,7 +7,7 @@ export async function updateApplicationCommands(): Promise<void> {
|
||||
// ONLY GLOBAL COMMANDS
|
||||
.filter((command) => !command.devOnly)
|
||||
.array(),
|
||||
)
|
||||
);
|
||||
|
||||
await bot.helpers.upsertGuildApplicationCommands(
|
||||
configs.devGuildId,
|
||||
@@ -15,5 +15,5 @@ export async function updateApplicationCommands(): Promise<void> {
|
||||
// ONLY GLOBAL COMMANDS
|
||||
.filter((command) => !!command.devOnly)
|
||||
.array(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'dotenv/config'
|
||||
import 'dotenv/config';
|
||||
|
||||
import { createBot } from '@discordeno/bot'
|
||||
import events from './events/index.js'
|
||||
import { createBot } from '@discordeno/bot';
|
||||
import events from './events/index.js';
|
||||
|
||||
const token = process.env.TOKEN
|
||||
const token = process.env.TOKEN;
|
||||
|
||||
// Ensure the existence of the TOKEN env
|
||||
if (!token) throw new Error('The TOKEN environment variable needs to be defined.')
|
||||
if (!token) throw new Error('The TOKEN environment variable needs to be defined.');
|
||||
|
||||
export const bot = createBot({
|
||||
token,
|
||||
@@ -41,6 +41,6 @@ export const bot = createBot({
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
bot.events = events
|
||||
bot.events = events;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
// Extremely minimal collector class
|
||||
export default class ItemCollector<T> extends EventEmitter {
|
||||
onItem(callback: (item: T) => unknown): void {
|
||||
this.on('item', callback)
|
||||
this.on('item', callback);
|
||||
}
|
||||
|
||||
collect(item: T): void {
|
||||
this.emit('item', item)
|
||||
this.emit('item', item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { CreateSlashApplicationCommand } from '@discordeno/types'
|
||||
import type { bot } from '../bot.js'
|
||||
import roles from './roles.js'
|
||||
import type { CreateSlashApplicationCommand } from '@discordeno/types';
|
||||
import type { bot } from '../bot.js';
|
||||
import roles from './roles.js';
|
||||
|
||||
export const commands = new Map<string, Command>([roles].map((cmd) => [cmd.name, cmd]))
|
||||
export const commands = new Map<string, Command>([roles].map((cmd) => [cmd.name, cmd]));
|
||||
|
||||
export default commands
|
||||
export default commands;
|
||||
|
||||
export interface Command extends CreateSlashApplicationCommand {
|
||||
/** Handler that will be executed when this command is triggered */
|
||||
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction, args: Record<string, unknown>) => Promise<unknown>
|
||||
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction, args: Record<string, unknown>) => Promise<unknown>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import assert from 'node:assert'
|
||||
import assert from 'node:assert';
|
||||
import {
|
||||
type ActionRow,
|
||||
type ButtonComponent,
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
MessageComponentTypes,
|
||||
type SelectMenuComponent,
|
||||
TextStyles,
|
||||
} from '@discordeno/bot'
|
||||
import { ApplicationCommandOptionTypes, ButtonStyles } from '@discordeno/types'
|
||||
import { bot } from '../bot.js'
|
||||
import ItemCollector from '../collector.js'
|
||||
import { collectors } from '../events/interactionCreate.js'
|
||||
import type { Command } from './index.js'
|
||||
} from '@discordeno/bot';
|
||||
import { ApplicationCommandOptionTypes, ButtonStyles } from '@discordeno/types';
|
||||
import { bot } from '../bot.js';
|
||||
import ItemCollector from '../collector.js';
|
||||
import { collectors } from '../events/interactionCreate.js';
|
||||
import type { Command } from './index.js';
|
||||
|
||||
const command: Command = {
|
||||
name: 'roles',
|
||||
@@ -73,22 +73,22 @@ const command: Command = {
|
||||
if (args.reactions?.create) {
|
||||
// Ensure that there is a channelId
|
||||
if (!interaction.channelId) {
|
||||
await interaction.respond('Could not get the current channel.', { isPrivate: true })
|
||||
return
|
||||
await interaction.respond('Could not get the current channel.', { isPrivate: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// This array is used to store all the roles for this reaction roles
|
||||
let roles = [args.reactions.create]
|
||||
let roles = [args.reactions.create];
|
||||
|
||||
// Send the message that uses will use to get the role
|
||||
const roleMessage = await bot.helpers.sendMessage(interaction.channelId, {
|
||||
content: 'Pick your roles',
|
||||
components: getRoleButtons(roles),
|
||||
})
|
||||
});
|
||||
|
||||
// Create a copy of the actionRow for the main message
|
||||
// NOTE: we use a copy so when we edit this actionRow the edits don't get applied to all the command executions, only this one, for example we do disable some buttons in some conditional cases
|
||||
const messageActionRow = structuredClone(messageActionRowTemplate)
|
||||
const messageActionRow = structuredClone(messageActionRowTemplate);
|
||||
|
||||
const message = await interaction.respond(
|
||||
{
|
||||
@@ -96,230 +96,230 @@ const command: Command = {
|
||||
components: [messageActionRow],
|
||||
},
|
||||
{ isPrivate: true, withResponse: true },
|
||||
)
|
||||
);
|
||||
|
||||
if (!message) {
|
||||
await interaction.respond('❌ Unable to send the message correctly. Cancelling', { isPrivate: true })
|
||||
return
|
||||
await interaction.respond('❌ Unable to send the message correctly. Cancelling', { isPrivate: true });
|
||||
return;
|
||||
}
|
||||
|
||||
assert('resource' in message && message.resource?.message)
|
||||
assert('resource' in message && message.resource?.message);
|
||||
|
||||
// Create the collector for the menu
|
||||
const itemCollector = new ItemCollector<typeof bot.transformers.$inferredTypes.interaction>()
|
||||
collectors.add(itemCollector)
|
||||
const itemCollector = new ItemCollector<typeof bot.transformers.$inferredTypes.interaction>();
|
||||
collectors.add(itemCollector);
|
||||
|
||||
// For the new reaction role, we need to keep track of what the user gave us
|
||||
let partialRoleInfo: Partial<(typeof roles)[number]> | undefined
|
||||
let partialRoleInfo: Partial<(typeof roles)[number]> | undefined;
|
||||
|
||||
itemCollector.onItem(async (i) => {
|
||||
// We need to verify the interaction is for us.
|
||||
if (i.message?.id !== message.resource?.message?.id) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// Save button
|
||||
if (i.data?.customId === 'reactionRoles-save') {
|
||||
// Remove this item collector from the list of collectors (we aren't correcting anymore)
|
||||
collectors.delete(itemCollector)
|
||||
collectors.delete(itemCollector);
|
||||
|
||||
// Delete the edit message
|
||||
await i.deferEdit()
|
||||
await i.delete()
|
||||
await i.deferEdit();
|
||||
await i.delete();
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// New button
|
||||
if (i.data?.customId === 'reactionRoles-add') {
|
||||
partialRoleInfo = {}
|
||||
partialRoleInfo = {};
|
||||
|
||||
// Ask the user for the role
|
||||
await i.edit({ content: 'Pick a role for the new reaction role', components: [selectRoleActionRow] })
|
||||
return
|
||||
await i.edit({ content: 'Pick a role for the new reaction role', components: [selectRoleActionRow] });
|
||||
return;
|
||||
}
|
||||
|
||||
// New button - role select menu
|
||||
if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-role') {
|
||||
const roleToAdd = i.data?.resolved?.roles?.first()
|
||||
const roleToAdd = i.data?.resolved?.roles?.first();
|
||||
|
||||
// Verify that we could get the role from discord
|
||||
if (!roleToAdd) {
|
||||
throw new Error('Unable to get the information for the role to add')
|
||||
throw new Error('Unable to get the information for the role to add');
|
||||
}
|
||||
|
||||
// Save it to our partial role information
|
||||
partialRoleInfo.role = roleToAdd
|
||||
partialRoleInfo.role = roleToAdd;
|
||||
|
||||
// Ask the user for the color of the button
|
||||
await i.edit({
|
||||
content: 'Pick a color for the reaction role',
|
||||
components: [selectColorActionRow],
|
||||
})
|
||||
});
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// New button - color select menu
|
||||
if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-color') {
|
||||
const color = parseInt(i.data?.values?.[0] ?? 'NaN')
|
||||
const color = parseInt(i.data?.values?.[0] ?? 'NaN');
|
||||
|
||||
// Verify that we could get the color information
|
||||
if (isNaN(color)) {
|
||||
throw new Error('Unable to get the information for the role to add')
|
||||
throw new Error('Unable to get the information for the role to add');
|
||||
}
|
||||
|
||||
// Save the color to our partial
|
||||
partialRoleInfo.color = color
|
||||
partialRoleInfo.color = color;
|
||||
|
||||
// Ask the user to input the emoji and optionally a label for the button
|
||||
await i.respond({
|
||||
title: 'Pick an emoji and label for the reaction role',
|
||||
components: [selectEmojiActionRow, selectLabelActionRow],
|
||||
customId: 'reactionRoles-add-modal',
|
||||
})
|
||||
});
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// New button - emoji & label modal
|
||||
if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-modal') {
|
||||
// Ensure that we can get the channelId from the interaction
|
||||
if (!interaction.channelId) {
|
||||
throw new Error('Unable to get current channel')
|
||||
throw new Error('Unable to get current channel');
|
||||
}
|
||||
|
||||
// Get the data from discord
|
||||
const emoji = i.data.components?.[0]?.components?.[0].value
|
||||
const label = i.data.components?.[1]?.components?.[0].value
|
||||
const emoji = i.data.components?.[0]?.components?.[0].value;
|
||||
const label = i.data.components?.[1]?.components?.[0].value;
|
||||
|
||||
// Verify that the emoji was given
|
||||
if (!emoji) {
|
||||
throw new Error('Unable to get the information for the role to add')
|
||||
throw new Error('Unable to get the information for the role to add');
|
||||
}
|
||||
|
||||
// Save them to our partial
|
||||
partialRoleInfo.emoji = emoji
|
||||
partialRoleInfo.label = label
|
||||
partialRoleInfo.emoji = emoji;
|
||||
partialRoleInfo.label = label;
|
||||
|
||||
// Save role and display the new message editing the old one
|
||||
|
||||
// We are sure that in this place the entire object has been assembled
|
||||
roles.push(partialRoleInfo as (typeof roles)[number])
|
||||
roles.push(partialRoleInfo as (typeof roles)[number]);
|
||||
|
||||
await bot.helpers.editMessage(interaction.channelId, roleMessage.id, {
|
||||
components: getRoleButtons(roles),
|
||||
})
|
||||
});
|
||||
|
||||
// Clear our partial roleInfo, we are done with it
|
||||
partialRoleInfo = undefined
|
||||
partialRoleInfo = undefined;
|
||||
// In case the delete button was disabled (all the roles were deleted) re-enable it
|
||||
messageActionRow.components[1]!.disabled = false
|
||||
messageActionRow.components[1]!.disabled = false;
|
||||
|
||||
// Discord imposes a limit of 5 action rows and 5 buttons for actionRow = 25 buttons max
|
||||
// more than 25 will give an error, so we disable the new button
|
||||
if (roles.length === 25) {
|
||||
const button = messageActionRow.components[0] as ButtonComponent
|
||||
button.disabled = true
|
||||
const button = messageActionRow.components[0] as ButtonComponent;
|
||||
button.disabled = true;
|
||||
}
|
||||
|
||||
// Show again the main edit menu
|
||||
await interaction.edit({
|
||||
content: 'Use the buttons in this message to edit the message below.',
|
||||
components: [messageActionRow],
|
||||
})
|
||||
});
|
||||
|
||||
// Respond to the modal. A modal submit (type 5) interaction can't edit the original response
|
||||
await i.respond('Reaction role created successfully. You can use the message above to add/remove a role', { isPrivate: true })
|
||||
await i.respond('Reaction role created successfully. You can use the message above to add/remove a role', { isPrivate: true });
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove button
|
||||
if (i.data?.customId === 'reactionRoles-remove') {
|
||||
// Clone the actionRow for the remove select menu, this is to prevent unwanted data to appear to other users
|
||||
const removeActionRow = structuredClone(removeActionRowTemplate)
|
||||
const selectMenu = removeActionRow.components[0] as SelectMenuComponent
|
||||
const removeActionRow = structuredClone(removeActionRowTemplate);
|
||||
const selectMenu = removeActionRow.components[0] as SelectMenuComponent;
|
||||
|
||||
// Add the possible values for this select menu
|
||||
for (const roleInfo of roles) {
|
||||
selectMenu.options.push({
|
||||
label: `${roleInfo.emoji} ${roleInfo.label ?? ''}`,
|
||||
value: roleInfo.role.id.toString(),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Ask the user for what reaction role they want to remove
|
||||
await i.edit({
|
||||
content: 'Select what reaction role to remove',
|
||||
components: [removeActionRow],
|
||||
})
|
||||
});
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove button - role select menu
|
||||
if (i.data?.customId === 'reactionRoles-remove-selectMenu') {
|
||||
// Ensure that we can get the channelId from the interaction
|
||||
if (!interaction.channelId) {
|
||||
throw new Error('Unable to get current channel')
|
||||
throw new Error('Unable to get current channel');
|
||||
}
|
||||
|
||||
// Get the role to delete from discord
|
||||
const roleToRemove = i.data?.values?.[0]
|
||||
const roleToRemove = i.data?.values?.[0];
|
||||
|
||||
// Ensure we got it
|
||||
if (!roleToRemove) {
|
||||
throw new Error('Unable to get the role to remove')
|
||||
throw new Error('Unable to get the role to remove');
|
||||
}
|
||||
|
||||
await i.deferEdit()
|
||||
await i.deferEdit();
|
||||
|
||||
// Remove the role from the list
|
||||
roles = roles.filter((roleInfo) => roleInfo.role.id.toString() !== roleToRemove)
|
||||
roles = roles.filter((roleInfo) => roleInfo.role.id.toString() !== roleToRemove);
|
||||
|
||||
// Edit the main button
|
||||
await bot.helpers.editMessage(interaction.channelId, roleMessage.id, {
|
||||
components: getRoleButtons(roles),
|
||||
})
|
||||
});
|
||||
|
||||
// If the new button was disabled (we were at 25 buttons) we re-enable it
|
||||
const button = messageActionRow.components[0] as ButtonComponent
|
||||
button.disabled = false
|
||||
const button = messageActionRow.components[0] as ButtonComponent;
|
||||
button.disabled = false;
|
||||
|
||||
// If we are at 0 roles, and the user tried to delete a role they will get locked in the menu, so we disable it
|
||||
if (roles.length === 0) {
|
||||
messageActionRow.components[1]!.disabled = true
|
||||
messageActionRow.components[1]!.disabled = true;
|
||||
}
|
||||
|
||||
// Show the main edit ui (new, remove, save)
|
||||
await i.edit({
|
||||
content: 'Use the buttons in this message to edit the message below.',
|
||||
components: [messageActionRow],
|
||||
})
|
||||
});
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't know what code to run for this interaction
|
||||
throw new Error('Unknown button')
|
||||
})
|
||||
throw new Error('Unknown button');
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default command
|
||||
export default command;
|
||||
|
||||
// Interface to type the arguments that we receive from discord
|
||||
interface CommandArgs {
|
||||
reactions?: {
|
||||
create?: {
|
||||
role: typeof bot.transformers.$inferredTypes.role
|
||||
emoji: string
|
||||
color: ButtonStyles
|
||||
label?: string
|
||||
}
|
||||
}
|
||||
role: typeof bot.transformers.$inferredTypes.role;
|
||||
emoji: string;
|
||||
color: ButtonStyles;
|
||||
label?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Templates/ActionRows for the command to then be referenced in the various part of the code
|
||||
@@ -357,7 +357,7 @@ const messageActionRowTemplate: ActionRow = {
|
||||
label: 'Save',
|
||||
},
|
||||
],
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
const removeActionRowTemplate: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
@@ -371,7 +371,7 @@ const removeActionRowTemplate: ActionRow = {
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
const selectRoleActionRow: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
@@ -384,7 +384,7 @@ const selectRoleActionRow: ActionRow = {
|
||||
placeholder: 'Select a role',
|
||||
},
|
||||
],
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
const selectColorActionRow: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
@@ -400,7 +400,7 @@ const selectColorActionRow: ActionRow = {
|
||||
],
|
||||
},
|
||||
],
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
const selectEmojiActionRow: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
@@ -413,7 +413,7 @@ const selectEmojiActionRow: ActionRow = {
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
const selectLabelActionRow: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
@@ -427,37 +427,37 @@ const selectLabelActionRow: ActionRow = {
|
||||
maxLength: 80,
|
||||
},
|
||||
],
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
// Function to get all the actionRows with buttons for the reaction roles message
|
||||
function getRoleButtons(
|
||||
roles: Array<{
|
||||
role: typeof bot.transformers.$inferredTypes.role
|
||||
emoji: string
|
||||
color: ButtonStyles
|
||||
label?: string | undefined
|
||||
role: typeof bot.transformers.$inferredTypes.role;
|
||||
emoji: string;
|
||||
color: ButtonStyles;
|
||||
label?: string | undefined;
|
||||
}>,
|
||||
): ActionRow[] {
|
||||
const actionRows: ActionRow[] = []
|
||||
const actionRows: ActionRow[] = [];
|
||||
|
||||
// If there aren't any roles, we don't need any buttons
|
||||
if (roles.length === 0) return actionRows
|
||||
if (roles.length === 0) return actionRows;
|
||||
|
||||
// We add the components later, so we need to make typescript know that we are sure that it will be a compatibile components array
|
||||
actionRows.push({ type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] })
|
||||
actionRows.push({ type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] });
|
||||
|
||||
for (const roleInfo of roles) {
|
||||
let actionRow = actionRows.at(-1)
|
||||
let actionRow = actionRows.at(-1);
|
||||
|
||||
// Ensure that we were able to get the actionRow
|
||||
if (!actionRow) {
|
||||
throw new Error('Unable to get actionRow')
|
||||
throw new Error('Unable to get actionRow');
|
||||
}
|
||||
|
||||
// If the actionRow is full (has 5 buttons) add a new one
|
||||
if (actionRow.components.length === 5) {
|
||||
actionRow = { type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] }
|
||||
actionRows.push(actionRow)
|
||||
actionRow = { type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] };
|
||||
actionRows.push(actionRow);
|
||||
}
|
||||
|
||||
// Add the new button to this actionRow
|
||||
@@ -469,8 +469,8 @@ function getRoleButtons(
|
||||
},
|
||||
label: roleInfo.label,
|
||||
customId: `reactionRoles-role-${roleInfo.role.id}`,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return actionRows
|
||||
return actionRows;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { bot } from '../bot.js'
|
||||
import { event as interactionCreateEvent } from './interactionCreate.js'
|
||||
import { event as readyEvent } from './ready.js'
|
||||
import type { bot } from '../bot.js';
|
||||
import { event as interactionCreateEvent } from './interactionCreate.js';
|
||||
import { event as readyEvent } from './ready.js';
|
||||
|
||||
export const events = {
|
||||
interactionCreate: interactionCreateEvent,
|
||||
ready: readyEvent,
|
||||
} as typeof bot.events
|
||||
} as typeof bot.events;
|
||||
|
||||
export default events
|
||||
export default events;
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
import { commandOptionsParser, InteractionTypes, MessageComponentTypes } from '@discordeno/bot'
|
||||
import { bot } from '../bot.js'
|
||||
import type ItemCollector from '../collector.js'
|
||||
import commands from '../commands/index.js'
|
||||
import { commandOptionsParser, InteractionTypes, MessageComponentTypes } from '@discordeno/bot';
|
||||
import { bot } from '../bot.js';
|
||||
import type ItemCollector from '../collector.js';
|
||||
import commands from '../commands/index.js';
|
||||
|
||||
export const collectors = new Set<ItemCollector<typeof bot.transformers.$inferredTypes.interaction>>()
|
||||
export const collectors = new Set<ItemCollector<typeof bot.transformers.$inferredTypes.interaction>>();
|
||||
|
||||
export const event: typeof bot.events.interactionCreate = async (interaction) => {
|
||||
// Give to all the collectors the interaction to use
|
||||
for (const collector of collectors) {
|
||||
collector.collect(interaction)
|
||||
collector.collect(interaction);
|
||||
}
|
||||
|
||||
// If the interaction is a command check if it is a command and run it
|
||||
if (interaction.type === InteractionTypes.ApplicationCommand) {
|
||||
if (!interaction.data) return
|
||||
if (!interaction.data) return;
|
||||
|
||||
const command = commands.get(interaction.data.name)
|
||||
if (!command) return
|
||||
const command = commands.get(interaction.data.name);
|
||||
if (!command) return;
|
||||
|
||||
try {
|
||||
await command.execute(interaction, commandOptionsParser(interaction))
|
||||
await command.execute(interaction, commandOptionsParser(interaction));
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// If the interaction is a button it might be the button press on our reaction role message
|
||||
if (interaction.type === InteractionTypes.MessageComponent && interaction.data?.componentType === MessageComponentTypes.Button) {
|
||||
// The interaction is not a button press on the role button
|
||||
if (!interaction.data?.customId?.startsWith('reactionRoles-role-')) return
|
||||
if (!interaction.guildId || !interaction.member) return
|
||||
if (!interaction.data?.customId?.startsWith('reactionRoles-role-')) return;
|
||||
if (!interaction.guildId || !interaction.member) return;
|
||||
|
||||
// Remove the prefix and get the roleId
|
||||
const roleId = BigInt(interaction.data.customId.slice('reactionRoles-role-'.length))
|
||||
const roleId = BigInt(interaction.data.customId.slice('reactionRoles-role-'.length));
|
||||
|
||||
// Check if we need to remove or add the role to the user
|
||||
const alreadyHasRole = !!interaction.member.roles.find((role) => role === roleId)
|
||||
const alreadyHasRole = !!interaction.member.roles.find((role) => role === roleId);
|
||||
|
||||
try {
|
||||
if (alreadyHasRole) {
|
||||
await bot.helpers.removeRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`)
|
||||
await interaction.respond(`I removed from you the <@&${roleId}> role.`, { isPrivate: true })
|
||||
return
|
||||
await bot.helpers.removeRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`);
|
||||
await interaction.respond(`I removed from you the <@&${roleId}> role.`, { isPrivate: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// You will get an invalid request made if the bot attempts to give a bot role, a role higher then him hightest role, a link role or if it does not have the Manage Roles permission
|
||||
// This could be prevented by checking for the roles that the bot owns and the role that the bot is trying to add
|
||||
await bot.helpers.addRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`)
|
||||
await interaction.respond(`I added to you the <@&${roleId}> role.`, { isPrivate: true })
|
||||
await bot.helpers.addRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`);
|
||||
await interaction.respond(`I added to you the <@&${roleId}> role.`, { isPrivate: true });
|
||||
} catch {
|
||||
// Respond with an error message
|
||||
await interaction.respond(
|
||||
'I could not give you the role. Possible reasons are:\n- My permissions are not configured correctly, make sure i have the `Manage Roles` permission\n- The role is **above** my hightest role in the server setup\n- The role does not exist or is non-manageable (for example: bot roles, link roles or @everyone)',
|
||||
{ isPrivate: true },
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { bot } from '../bot.js'
|
||||
import { bot } from '../bot.js';
|
||||
|
||||
export const event: typeof bot.events.ready = () => {
|
||||
// Print to the console when the bot has connected to discord and is ready to handle the events
|
||||
bot.logger.info('The bot is ready!')
|
||||
}
|
||||
bot.logger.info('The bot is ready!');
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { bot } from './bot.js'
|
||||
import { bot } from './bot.js';
|
||||
|
||||
await bot.start()
|
||||
await bot.start();
|
||||
|
||||
process.on('unhandledRejection', bot.logger.error)
|
||||
process.on('unhandledRejection', bot.logger.error);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'dotenv/config'
|
||||
import 'dotenv/config';
|
||||
|
||||
import { bot } from './bot.js'
|
||||
import commands from './commands/index.js'
|
||||
import { bot } from './bot.js';
|
||||
import commands from './commands/index.js';
|
||||
|
||||
const guildId = 'REPLACE WITH YOUR GUILD ID'
|
||||
const guildId = 'REPLACE WITH YOUR GUILD ID';
|
||||
|
||||
await bot.rest
|
||||
.upsertGuildApplicationCommands(guildId, [...commands.values()])
|
||||
.catch((e) => bot.logger.error('There was an error when updating the global commands', e))
|
||||
.catch((e) => bot.logger.error('There was an error when updating the global commands', e));
|
||||
|
||||
process.exit(0)
|
||||
process.exit(0);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { CreateGatewayManagerOptions, GatewayManager } from '@discordeno/gateway'
|
||||
import { createGatewayManager, ShardSocketCloseCodes } from '@discordeno/gateway'
|
||||
import type { CreateRestManagerOptions, RestManager } from '@discordeno/rest'
|
||||
import { createRestManager } from '@discordeno/rest'
|
||||
import type { BigString, GatewayDispatchEventNames, GatewayIntents, RecursivePartial } from '@discordeno/types'
|
||||
import { createLogger, getBotIdFromToken, type logger } from '@discordeno/utils'
|
||||
import type { CreateGatewayManagerOptions, GatewayManager } from '@discordeno/gateway';
|
||||
import { createGatewayManager, ShardSocketCloseCodes } from '@discordeno/gateway';
|
||||
import type { CreateRestManagerOptions, RestManager } from '@discordeno/rest';
|
||||
import { createRestManager } from '@discordeno/rest';
|
||||
import type { BigString, GatewayDispatchEventNames, GatewayIntents, RecursivePartial } from '@discordeno/types';
|
||||
import { createLogger, getBotIdFromToken, type logger } from '@discordeno/utils';
|
||||
import type {
|
||||
CompleteDesiredProperties,
|
||||
DesiredPropertiesBehavior,
|
||||
SetupDesiredProps,
|
||||
TransformersDesiredProperties,
|
||||
TransformersObjects,
|
||||
} from './desiredProperties.js'
|
||||
import type { EventHandlers } from './events.js'
|
||||
import { type BotGatewayHandler, createBotGatewayHandlers, type GatewayHandlers } from './handlers.js'
|
||||
import { type BotHelpers, createBotHelpers } from './helpers.js'
|
||||
import { createTransformers, type Transformers } from './transformers.js'
|
||||
} from './desiredProperties.js';
|
||||
import type { EventHandlers } from './events.js';
|
||||
import { type BotGatewayHandler, createBotGatewayHandlers, type GatewayHandlers } from './handlers.js';
|
||||
import { type BotHelpers, createBotHelpers } from './helpers.js';
|
||||
import { createTransformers, type Transformers } from './transformers.js';
|
||||
|
||||
/**
|
||||
* Create a bot object that will maintain the rest and gateway connection.
|
||||
@@ -27,44 +27,44 @@ import { createTransformers, type Transformers } from './transformers.js'
|
||||
export function createBot<
|
||||
TProps extends TransformersDesiredProperties,
|
||||
TBehavior extends DesiredPropertiesBehavior = DesiredPropertiesBehavior.RemoveKey,
|
||||
>(options: CreateBotOptions<TProps, TBehavior>): Bot<TProps, TBehavior>
|
||||
>(options: CreateBotOptions<TProps, TBehavior>): Bot<TProps, TBehavior>;
|
||||
export function createBot<
|
||||
TProps extends RecursivePartial<TransformersDesiredProperties>,
|
||||
TBehavior extends DesiredPropertiesBehavior = DesiredPropertiesBehavior.RemoveKey,
|
||||
>(options: CreateBotOptions<TProps, TBehavior>): Bot<CompleteDesiredProperties<TProps>, TBehavior>
|
||||
>(options: CreateBotOptions<TProps, TBehavior>): Bot<CompleteDesiredProperties<TProps>, TBehavior>;
|
||||
|
||||
export function createBot<
|
||||
TProps extends RecursivePartial<TransformersDesiredProperties>,
|
||||
TBehavior extends DesiredPropertiesBehavior = DesiredPropertiesBehavior.RemoveKey,
|
||||
>(options: CreateBotOptions<TProps, TBehavior>): Bot<CompleteDesiredProperties<TProps>, TBehavior> {
|
||||
type CompleteProps = CompleteDesiredProperties<TProps>
|
||||
type TypedBot = Bot<CompleteProps, TBehavior>
|
||||
type CompleteProps = CompleteDesiredProperties<TProps>;
|
||||
type TypedBot = Bot<CompleteProps, TBehavior>;
|
||||
|
||||
if (!options.transformers) options.transformers = {}
|
||||
if (!options.rest) options.rest = { token: options.token, applicationId: options.applicationId }
|
||||
if (!options.rest.token) options.rest.token = options.token
|
||||
if (!options.rest.logger && options.loggerFactory) options.rest.logger = options.loggerFactory('REST')
|
||||
if (!options.gateway) options.gateway = { token: options.token }
|
||||
if (!options.gateway.token) options.gateway.token = options.token
|
||||
if (!options.gateway.events) options.gateway.events = {}
|
||||
if (!options.gateway.logger && options.loggerFactory) options.gateway.logger = options.loggerFactory('GATEWAY')
|
||||
if (!options.transformers) options.transformers = {};
|
||||
if (!options.rest) options.rest = { token: options.token, applicationId: options.applicationId };
|
||||
if (!options.rest.token) options.rest.token = options.token;
|
||||
if (!options.rest.logger && options.loggerFactory) options.rest.logger = options.loggerFactory('REST');
|
||||
if (!options.gateway) options.gateway = { token: options.token };
|
||||
if (!options.gateway.token) options.gateway.token = options.token;
|
||||
if (!options.gateway.events) options.gateway.events = {};
|
||||
if (!options.gateway.logger && options.loggerFactory) options.gateway.logger = options.loggerFactory('GATEWAY');
|
||||
if (!options.gateway.events.message) {
|
||||
options.gateway.events.message = async (shard, data) => {
|
||||
// TRIGGER RAW EVENT
|
||||
bot.events.raw?.(data, shard.id)
|
||||
bot.events.raw?.(data, shard.id);
|
||||
|
||||
if (!data.t) return
|
||||
if (!data.t) return;
|
||||
|
||||
// RUN DISPATCH CHECK
|
||||
await bot.events.dispatchRequirements?.(data, shard.id)
|
||||
bot.handlers[data.t as GatewayDispatchEventNames]?.(bot, data, shard.id)
|
||||
}
|
||||
await bot.events.dispatchRequirements?.(data, shard.id);
|
||||
bot.handlers[data.t as GatewayDispatchEventNames]?.(bot, data, shard.id);
|
||||
};
|
||||
}
|
||||
|
||||
options.gateway.intents = options.intents
|
||||
;(options.transformers as Transformers<CompleteProps, TBehavior>).desiredProperties = options.desiredProperties as CompleteProps
|
||||
options.gateway.intents = options.intents;
|
||||
(options.transformers as Transformers<CompleteProps, TBehavior>).desiredProperties = options.desiredProperties as CompleteProps;
|
||||
|
||||
const id = getBotIdFromToken(options.token)
|
||||
const id = getBotIdFromToken(options.token);
|
||||
|
||||
const bot: TypedBot = {
|
||||
id,
|
||||
@@ -79,63 +79,63 @@ export function createBot<
|
||||
helpers: {} as BotHelpers<CompleteProps, TBehavior>,
|
||||
async start() {
|
||||
if (!options.gateway?.connection) {
|
||||
bot.gateway.connection = await bot.rest.getSessionInfo()
|
||||
bot.gateway.connection = await bot.rest.getSessionInfo();
|
||||
|
||||
// Check for overrides in the configuration
|
||||
if (!options.gateway?.url) bot.gateway.url = bot.gateway.connection.url
|
||||
if (!options.gateway?.url) bot.gateway.url = bot.gateway.connection.url;
|
||||
|
||||
if (!options.gateway?.totalShards) bot.gateway.totalShards = bot.gateway.connection.shards
|
||||
if (!options.gateway?.totalShards) bot.gateway.totalShards = bot.gateway.connection.shards;
|
||||
|
||||
if (!options.gateway?.lastShardId && !options.gateway?.totalShards) bot.gateway.lastShardId = bot.gateway.connection.shards - 1
|
||||
if (!options.gateway?.lastShardId && !options.gateway?.totalShards) bot.gateway.lastShardId = bot.gateway.connection.shards - 1;
|
||||
}
|
||||
|
||||
if (!bot.gateway.resharding.getSessionInfo) {
|
||||
bot.gateway.resharding.getSessionInfo = async () => {
|
||||
return await bot.rest.getGatewayBot()
|
||||
}
|
||||
return await bot.rest.getGatewayBot();
|
||||
};
|
||||
}
|
||||
|
||||
await bot.gateway.spawnShards()
|
||||
await bot.gateway.spawnShards();
|
||||
},
|
||||
|
||||
async shutdown() {
|
||||
return await bot.gateway.shutdown(ShardSocketCloseCodes.Shutdown, 'User requested bot stop')
|
||||
return await bot.gateway.shutdown(ShardSocketCloseCodes.Shutdown, 'User requested bot stop');
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
bot.helpers = createBotHelpers(bot)
|
||||
if (options.applicationId) bot.applicationId = bot.transformers.snowflake(options.applicationId)
|
||||
bot.helpers = createBotHelpers(bot);
|
||||
if (options.applicationId) bot.applicationId = bot.transformers.snowflake(options.applicationId);
|
||||
|
||||
return bot
|
||||
return bot;
|
||||
}
|
||||
|
||||
export interface CreateBotOptions<TProps extends RecursivePartial<TransformersDesiredProperties>, TBehavior extends DesiredPropertiesBehavior> {
|
||||
/** The bot's token. */
|
||||
token: string
|
||||
token: string;
|
||||
/** Application Id of the bot incase it is an old bot token. */
|
||||
applicationId?: BigString
|
||||
applicationId?: BigString;
|
||||
/** The bot's intents that will be used to make a connection with discords gateway. */
|
||||
intents?: GatewayIntents
|
||||
intents?: GatewayIntents;
|
||||
/** Any options you wish to provide to the rest manager. */
|
||||
rest?: Omit<CreateRestManagerOptions, 'token'> & Partial<Pick<CreateRestManagerOptions, 'token'>>
|
||||
rest?: Omit<CreateRestManagerOptions, 'token'> & Partial<Pick<CreateRestManagerOptions, 'token'>>;
|
||||
/** Any options you wish to provide to the gateway manager. */
|
||||
gateway?: Omit<CreateGatewayManagerOptions, 'token'> & Partial<Pick<CreateGatewayManagerOptions, 'token'>>
|
||||
gateway?: Omit<CreateGatewayManagerOptions, 'token'> & Partial<Pick<CreateGatewayManagerOptions, 'token'>>;
|
||||
/** The event handlers. */
|
||||
events?: Partial<EventHandlers<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>>
|
||||
events?: Partial<EventHandlers<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>>;
|
||||
/** The functions that should transform discord objects to discordeno shaped objects. */
|
||||
transformers?: RecursivePartial<Omit<Transformers<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>, 'desiredProperties'>>
|
||||
transformers?: RecursivePartial<Omit<Transformers<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>, 'desiredProperties'>>;
|
||||
/** The handler functions that should handle incoming discord payloads from gateway and call an event. */
|
||||
handlers?: Partial<Record<GatewayDispatchEventNames, BotGatewayHandler<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>>>
|
||||
handlers?: Partial<Record<GatewayDispatchEventNames, BotGatewayHandler<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>>>;
|
||||
/**
|
||||
* Set the desired properties for the bot
|
||||
*/
|
||||
desiredProperties: TProps
|
||||
desiredProperties: TProps;
|
||||
/**
|
||||
* Set the desired properties behavior for undesired properties
|
||||
*
|
||||
* @default DesiredPropertiesBehavior.RemoveKey
|
||||
*/
|
||||
desiredPropertiesBehavior?: TBehavior
|
||||
desiredPropertiesBehavior?: TBehavior;
|
||||
/**
|
||||
* This factory will be invoked to create the logger for gateway, rest and bot
|
||||
*
|
||||
@@ -144,7 +144,7 @@ export interface CreateBotOptions<TProps extends RecursivePartial<TransformersDe
|
||||
*
|
||||
* This function will be invoked 3 times, one with the name of `REST`, one with `GATEWAY` and the third one with name `BOT`
|
||||
*/
|
||||
loggerFactory?: (name: 'REST' | 'GATEWAY' | 'BOT') => Pick<typeof logger, 'debug' | 'info' | 'warn' | 'error' | 'fatal'>
|
||||
loggerFactory?: (name: 'REST' | 'GATEWAY' | 'BOT') => Pick<typeof logger, 'debug' | 'info' | 'warn' | 'error' | 'fatal'>;
|
||||
}
|
||||
|
||||
export interface Bot<
|
||||
@@ -152,28 +152,28 @@ export interface Bot<
|
||||
TBehavior extends DesiredPropertiesBehavior = DesiredPropertiesBehavior.RemoveKey,
|
||||
> {
|
||||
/** The id of the bot. */
|
||||
id: bigint
|
||||
id: bigint;
|
||||
/** The application id of the bot. This is usually the same as id but in the case of old bots can be different. */
|
||||
applicationId: bigint
|
||||
applicationId: bigint;
|
||||
/** The rest manager. */
|
||||
rest: RestManager
|
||||
rest: RestManager;
|
||||
/** The gateway manager. */
|
||||
gateway: GatewayManager
|
||||
gateway: GatewayManager;
|
||||
/** The event handlers. */
|
||||
events: Partial<EventHandlers<TProps, TBehavior>>
|
||||
events: Partial<EventHandlers<TProps, TBehavior>>;
|
||||
/** A logger utility to make it easy to log nice and useful things in the bot code. */
|
||||
logger: Pick<typeof logger, 'debug' | 'info' | 'warn' | 'error' | 'fatal'>
|
||||
logger: Pick<typeof logger, 'debug' | 'info' | 'warn' | 'error' | 'fatal'>;
|
||||
/** The functions that should transform discord objects to discordeno shaped objects. */
|
||||
transformers: Transformers<TProps, TBehavior> & {
|
||||
$inferredTypes: {
|
||||
[K in keyof TransformersObjects]: SetupDesiredProps<TransformersObjects[K], TProps, TBehavior>
|
||||
}
|
||||
}
|
||||
[K in keyof TransformersObjects]: SetupDesiredProps<TransformersObjects[K], TProps, TBehavior>;
|
||||
};
|
||||
};
|
||||
/** The handler functions that should handle incoming discord payloads from gateway and call an event. */
|
||||
handlers: GatewayHandlers<TProps, TBehavior>
|
||||
helpers: BotHelpers<TProps, TBehavior>
|
||||
handlers: GatewayHandlers<TProps, TBehavior>;
|
||||
helpers: BotHelpers<TProps, TBehavior>;
|
||||
/** Start the bot connection to the gateway. */
|
||||
start: () => Promise<void>
|
||||
start: () => Promise<void>;
|
||||
/** Shuts down all the bot connections to the gateway. */
|
||||
shutdown: () => Promise<void>
|
||||
shutdown: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApplicationCommandOptionTypes } from '@discordeno/types'
|
||||
import type { CompleteDesiredProperties, DesiredPropertiesBehavior, SetupDesiredProps, TransformersDesiredProperties } from './desiredProperties.js'
|
||||
import type { Attachment, Channel, Interaction, InteractionDataOption, Member, Role, User } from './transformers/types.js'
|
||||
import { ApplicationCommandOptionTypes } from '@discordeno/types';
|
||||
import type { CompleteDesiredProperties, DesiredPropertiesBehavior, SetupDesiredProps, TransformersDesiredProperties } from './desiredProperties.js';
|
||||
import type { Attachment, Channel, Interaction, InteractionDataOption, Member, Role, User } from './transformers/types.js';
|
||||
|
||||
export function commandOptionsParser<
|
||||
TProps extends TransformersDesiredProperties & { interaction: { data: true } },
|
||||
@@ -11,51 +11,51 @@ export function commandOptionsParser<
|
||||
Interaction,
|
||||
CompleteDesiredProperties<{ interaction: { data: true } }>,
|
||||
DesiredPropertiesBehavior.RemoveKey
|
||||
>
|
||||
>;
|
||||
|
||||
if (!interaction.data) return {}
|
||||
if (!options) options = interaction.data.options ?? []
|
||||
if (!interaction.data) return {};
|
||||
if (!options) options = interaction.data.options ?? [];
|
||||
|
||||
const args: ParsedInteractionOption<TProps, TBehavior> = {}
|
||||
const args: ParsedInteractionOption<TProps, TBehavior> = {};
|
||||
|
||||
for (const option of options) {
|
||||
switch (option.type) {
|
||||
case ApplicationCommandOptionTypes.SubCommandGroup:
|
||||
case ApplicationCommandOptionTypes.SubCommand:
|
||||
args[option.name] = commandOptionsParser(interaction, option.options) as InteractionResolvedData<TProps, TBehavior>
|
||||
break
|
||||
args[option.name] = commandOptionsParser(interaction, option.options) as InteractionResolvedData<TProps, TBehavior>;
|
||||
break;
|
||||
case ApplicationCommandOptionTypes.Channel:
|
||||
args[option.name] = interaction.data.resolved?.channels?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>
|
||||
break
|
||||
args[option.name] = interaction.data.resolved?.channels?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>;
|
||||
break;
|
||||
case ApplicationCommandOptionTypes.Role:
|
||||
args[option.name] = interaction.data.resolved?.roles?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>
|
||||
break
|
||||
args[option.name] = interaction.data.resolved?.roles?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>;
|
||||
break;
|
||||
case ApplicationCommandOptionTypes.User:
|
||||
args[option.name] = {
|
||||
user: interaction.data.resolved?.users?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>,
|
||||
member: interaction.data.resolved?.members?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>,
|
||||
}
|
||||
break
|
||||
};
|
||||
break;
|
||||
case ApplicationCommandOptionTypes.Attachment:
|
||||
args[option.name] = interaction.data.resolved?.attachments?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>
|
||||
break
|
||||
args[option.name] = interaction.data.resolved?.attachments?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>;
|
||||
break;
|
||||
case ApplicationCommandOptionTypes.Mentionable:
|
||||
// Mentionable are roles or users
|
||||
args[option.name] = (interaction.data.resolved?.roles?.get(BigInt(option.value!)) as ParsedInteractionOption<TProps, TBehavior>[string]) ?? {
|
||||
user: interaction.data.resolved?.users?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>,
|
||||
member: interaction.data.resolved?.members?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>,
|
||||
}
|
||||
break
|
||||
};
|
||||
break;
|
||||
default:
|
||||
args[option.name] = option.value as InteractionResolvedData<TProps, TBehavior>
|
||||
args[option.name] = option.value as InteractionResolvedData<TProps, TBehavior>;
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
return args;
|
||||
}
|
||||
|
||||
export interface ParsedInteractionOption<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> {
|
||||
[key: string]: InteractionResolvedData<TProps, TBehavior>
|
||||
[key: string]: InteractionResolvedData<TProps, TBehavior>;
|
||||
}
|
||||
|
||||
export type InteractionResolvedData<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> =
|
||||
@@ -66,11 +66,11 @@ export type InteractionResolvedData<TProps extends TransformersDesiredProperties
|
||||
| InteractionResolvedDataChannel<TProps, TBehavior>
|
||||
| SetupDesiredProps<Role, TProps, TBehavior>
|
||||
| SetupDesiredProps<Attachment, TProps, TBehavior>
|
||||
| ParsedInteractionOption<TProps, TBehavior>
|
||||
| ParsedInteractionOption<TProps, TBehavior>;
|
||||
|
||||
export interface InteractionResolvedDataUser<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> {
|
||||
user: SetupDesiredProps<User, TProps, TBehavior>
|
||||
member: InteractionResolvedDataMember<TProps, TBehavior>
|
||||
user: SetupDesiredProps<User, TProps, TBehavior>;
|
||||
member: InteractionResolvedDataMember<TProps, TBehavior>;
|
||||
}
|
||||
|
||||
export type InteractionResolvedDataChannel<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> = Pick<
|
||||
@@ -92,17 +92,17 @@ export type InteractionResolvedDataChannel<TProps extends TransformersDesiredPro
|
||||
| 'position'
|
||||
| 'threadMetadata'
|
||||
>
|
||||
>
|
||||
>;
|
||||
|
||||
export type InteractionResolvedDataMember<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> = Omit<
|
||||
SetupDesiredProps<Member, TProps, TBehavior>,
|
||||
'user' | 'deaf' | 'mute'
|
||||
>
|
||||
>;
|
||||
|
||||
/** @deprecated Use {@link InteractionResolvedDataUser} */
|
||||
export interface InteractionResolvedUser {
|
||||
user: User
|
||||
member: InteractionResolvedMember
|
||||
user: User;
|
||||
member: InteractionResolvedMember;
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link InteractionResolvedDataChannel} */
|
||||
@@ -122,7 +122,7 @@ export type InteractionResolvedChannel = Pick<
|
||||
| 'topic'
|
||||
| 'position'
|
||||
| 'threadMetadata'
|
||||
>
|
||||
>;
|
||||
|
||||
/** @deprecated Use {@link InteractionResolvedDataMember} */
|
||||
export type InteractionResolvedMember = Omit<Member, 'user' | 'deaf' | 'mute'>
|
||||
export type InteractionResolvedMember = Omit<Member, 'user' | 'deaf' | 'mute'>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const SLASH_COMMANDS_NAME_REGEX = /^[-_ʼ\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u
|
||||
export const CONTEXT_MENU_COMMANDS_NAME_REGEX = /^[\w-\s]{1,32}$/
|
||||
export const CHANNEL_MENTION_REGEX = /<#[0-9]+>/g
|
||||
export const DISCORD_SNOWFLAKE_REGEX = /^(?<id>\d{17,19})$/
|
||||
export const SLASH_COMMANDS_NAME_REGEX = /^[-_ʼ\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u;
|
||||
export const CONTEXT_MENU_COMMANDS_NAME_REGEX = /^[\w-\s]{1,32}$/;
|
||||
export const CHANNEL_MENTION_REGEX = /<#[0-9]+>/g;
|
||||
export const DISCORD_SNOWFLAKE_REGEX = /^(?<id>\d{17,19})$/;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RecursivePartial } from '@discordeno/types'
|
||||
import type { Collection } from '@discordeno/utils'
|
||||
import type { Bot } from './bot.js'
|
||||
import type { InteractionResolvedDataChannel, InteractionResolvedDataMember } from './commandOptionsParser.js'
|
||||
import type { RecursivePartial } from '@discordeno/types';
|
||||
import type { Collection } from '@discordeno/utils';
|
||||
import type { Bot } from './bot.js';
|
||||
import type { InteractionResolvedDataChannel, InteractionResolvedDataMember } from './commandOptionsParser.js';
|
||||
import type {
|
||||
ActivityInstance,
|
||||
ActivityLocation,
|
||||
@@ -56,7 +56,7 @@ import type {
|
||||
UserPrimaryGuild,
|
||||
VoiceState,
|
||||
Webhook,
|
||||
} from './transformers/types.js'
|
||||
} from './transformers/types.js';
|
||||
|
||||
/**
|
||||
* All the objects that support desired properties
|
||||
@@ -64,59 +64,59 @@ import type {
|
||||
* @private This is subject to breaking changes at any time
|
||||
*/
|
||||
export interface TransformersObjects {
|
||||
activityInstance: ActivityInstance
|
||||
activityLocation: ActivityLocation
|
||||
attachment: Attachment
|
||||
avatarDecorationData: AvatarDecorationData
|
||||
channel: Channel
|
||||
collectibles: Collectibles
|
||||
component: Component
|
||||
defaultReactionEmoji: DefaultReactionEmoji
|
||||
emoji: Emoji
|
||||
entitlement: Entitlement
|
||||
forumTag: ForumTag
|
||||
guild: Guild
|
||||
guildOnboarding: GuildOnboarding
|
||||
guildOnboardingPrompt: GuildOnboardingPrompt
|
||||
guildOnboardingPromptOption: GuildOnboardingPromptOption
|
||||
incidentsData: IncidentsData
|
||||
interaction: Interaction
|
||||
interactionCallback: InteractionCallback
|
||||
interactionCallbackResponse: InteractionCallbackResponse
|
||||
interactionResource: InteractionResource
|
||||
invite: Invite
|
||||
inviteStageInstance: InviteStageInstance
|
||||
lobby: Lobby
|
||||
lobbyMember: LobbyMember
|
||||
mediaGalleryItem: MediaGalleryItem
|
||||
member: Member
|
||||
message: Message
|
||||
messageCall: MessageCall
|
||||
messageInteraction: MessageInteraction
|
||||
messageInteractionMetadata: MessageInteractionMetadata
|
||||
messagePin: MessagePin
|
||||
messageReference: MessageReference
|
||||
messageSnapshot: MessageSnapshot
|
||||
nameplate: Nameplate
|
||||
poll: Poll
|
||||
pollAnswer: PollAnswer
|
||||
pollAnswerCount: PollAnswerCount
|
||||
pollMedia: PollMedia
|
||||
pollResult: PollResult
|
||||
role: Role
|
||||
roleColors: RoleColors
|
||||
scheduledEvent: ScheduledEvent
|
||||
scheduledEventRecurrenceRule: ScheduledEventRecurrenceRule
|
||||
sku: Sku
|
||||
soundboardSound: SoundboardSound
|
||||
stageInstance: StageInstance
|
||||
sticker: Sticker
|
||||
subscription: Subscription
|
||||
unfurledMediaItem: UnfurledMediaItem
|
||||
user: User
|
||||
userPrimaryGuild: UserPrimaryGuild
|
||||
voiceState: VoiceState
|
||||
webhook: Webhook
|
||||
activityInstance: ActivityInstance;
|
||||
activityLocation: ActivityLocation;
|
||||
attachment: Attachment;
|
||||
avatarDecorationData: AvatarDecorationData;
|
||||
channel: Channel;
|
||||
collectibles: Collectibles;
|
||||
component: Component;
|
||||
defaultReactionEmoji: DefaultReactionEmoji;
|
||||
emoji: Emoji;
|
||||
entitlement: Entitlement;
|
||||
forumTag: ForumTag;
|
||||
guild: Guild;
|
||||
guildOnboarding: GuildOnboarding;
|
||||
guildOnboardingPrompt: GuildOnboardingPrompt;
|
||||
guildOnboardingPromptOption: GuildOnboardingPromptOption;
|
||||
incidentsData: IncidentsData;
|
||||
interaction: Interaction;
|
||||
interactionCallback: InteractionCallback;
|
||||
interactionCallbackResponse: InteractionCallbackResponse;
|
||||
interactionResource: InteractionResource;
|
||||
invite: Invite;
|
||||
inviteStageInstance: InviteStageInstance;
|
||||
lobby: Lobby;
|
||||
lobbyMember: LobbyMember;
|
||||
mediaGalleryItem: MediaGalleryItem;
|
||||
member: Member;
|
||||
message: Message;
|
||||
messageCall: MessageCall;
|
||||
messageInteraction: MessageInteraction;
|
||||
messageInteractionMetadata: MessageInteractionMetadata;
|
||||
messagePin: MessagePin;
|
||||
messageReference: MessageReference;
|
||||
messageSnapshot: MessageSnapshot;
|
||||
nameplate: Nameplate;
|
||||
poll: Poll;
|
||||
pollAnswer: PollAnswer;
|
||||
pollAnswerCount: PollAnswerCount;
|
||||
pollMedia: PollMedia;
|
||||
pollResult: PollResult;
|
||||
role: Role;
|
||||
roleColors: RoleColors;
|
||||
scheduledEvent: ScheduledEvent;
|
||||
scheduledEventRecurrenceRule: ScheduledEventRecurrenceRule;
|
||||
sku: Sku;
|
||||
soundboardSound: SoundboardSound;
|
||||
stageInstance: StageInstance;
|
||||
sticker: Sticker;
|
||||
subscription: Subscription;
|
||||
unfurledMediaItem: UnfurledMediaItem;
|
||||
user: User;
|
||||
userPrimaryGuild: UserPrimaryGuild;
|
||||
voiceState: VoiceState;
|
||||
webhook: Webhook;
|
||||
}
|
||||
|
||||
// NOTE: the top-level objects need both the dependencies and alwaysPresents even if empty when the key is specified, this is due the extends & nullability on DesiredPropertiesMetadata
|
||||
@@ -130,107 +130,107 @@ export interface TransformersObjects {
|
||||
export interface TransformersDesiredPropertiesMetadata extends DesiredPropertiesMetadata {
|
||||
channel: {
|
||||
dependencies: {
|
||||
archived: ['toggles']
|
||||
invitable: ['toggles']
|
||||
locked: ['toggles']
|
||||
nsfw: ['toggles']
|
||||
newlyCreated: ['toggles']
|
||||
managed: ['toggles']
|
||||
}
|
||||
alwaysPresents: ['toggles', 'internalOverwrites', 'internalThreadMetadata']
|
||||
}
|
||||
archived: ['toggles'];
|
||||
invitable: ['toggles'];
|
||||
locked: ['toggles'];
|
||||
nsfw: ['toggles'];
|
||||
newlyCreated: ['toggles'];
|
||||
managed: ['toggles'];
|
||||
};
|
||||
alwaysPresents: ['toggles', 'internalOverwrites', 'internalThreadMetadata'];
|
||||
};
|
||||
|
||||
guild: {
|
||||
dependencies: {
|
||||
threads: ['channels']
|
||||
features: ['toggles']
|
||||
}
|
||||
alwaysPresents: []
|
||||
}
|
||||
threads: ['channels'];
|
||||
features: ['toggles'];
|
||||
};
|
||||
alwaysPresents: [];
|
||||
};
|
||||
|
||||
interaction: {
|
||||
dependencies: {
|
||||
respond: ['type', 'token', 'id']
|
||||
edit: ['type', 'token', 'id']
|
||||
deferEdit: ['type', 'token', 'id']
|
||||
defer: ['type', 'token', 'id']
|
||||
delete: ['type', 'token']
|
||||
}
|
||||
alwaysPresents: ['bot', 'acknowledged']
|
||||
}
|
||||
respond: ['type', 'token', 'id'];
|
||||
edit: ['type', 'token', 'id'];
|
||||
deferEdit: ['type', 'token', 'id'];
|
||||
defer: ['type', 'token', 'id'];
|
||||
delete: ['type', 'token'];
|
||||
};
|
||||
alwaysPresents: ['bot', 'acknowledged'];
|
||||
};
|
||||
|
||||
member: {
|
||||
dependencies: {
|
||||
deaf: ['toggles']
|
||||
mute: ['toggles']
|
||||
pending: ['toggles']
|
||||
flags: ['toggles']
|
||||
didRejoin: ['toggles']
|
||||
startedOnboarding: ['toggles']
|
||||
bypassesVerification: ['toggles']
|
||||
completedOnboarding: ['toggles']
|
||||
}
|
||||
alwaysPresents: []
|
||||
}
|
||||
deaf: ['toggles'];
|
||||
mute: ['toggles'];
|
||||
pending: ['toggles'];
|
||||
flags: ['toggles'];
|
||||
didRejoin: ['toggles'];
|
||||
startedOnboarding: ['toggles'];
|
||||
bypassesVerification: ['toggles'];
|
||||
completedOnboarding: ['toggles'];
|
||||
};
|
||||
alwaysPresents: [];
|
||||
};
|
||||
|
||||
message: {
|
||||
dependencies: {
|
||||
crossposted: ['flags']
|
||||
ephemeral: ['flags']
|
||||
failedToMentionSomeRolesInThread: ['flags']
|
||||
hasThread: ['flags']
|
||||
isCrosspost: ['flags']
|
||||
loading: ['flags']
|
||||
mentionedUserIds: ['mentions']
|
||||
mentionEveryone: ['bitfield']
|
||||
pinned: ['bitfield']
|
||||
sourceMessageDeleted: ['flags']
|
||||
suppressEmbeds: ['flags']
|
||||
suppressNotifications: ['flags']
|
||||
timestamp: ['id']
|
||||
tts: ['bitfield']
|
||||
urgent: ['flags']
|
||||
}
|
||||
alwaysPresents: ['bitfield', 'flags']
|
||||
}
|
||||
crossposted: ['flags'];
|
||||
ephemeral: ['flags'];
|
||||
failedToMentionSomeRolesInThread: ['flags'];
|
||||
hasThread: ['flags'];
|
||||
isCrosspost: ['flags'];
|
||||
loading: ['flags'];
|
||||
mentionedUserIds: ['mentions'];
|
||||
mentionEveryone: ['bitfield'];
|
||||
pinned: ['bitfield'];
|
||||
sourceMessageDeleted: ['flags'];
|
||||
suppressEmbeds: ['flags'];
|
||||
suppressNotifications: ['flags'];
|
||||
timestamp: ['id'];
|
||||
tts: ['bitfield'];
|
||||
urgent: ['flags'];
|
||||
};
|
||||
alwaysPresents: ['bitfield', 'flags'];
|
||||
};
|
||||
|
||||
role: {
|
||||
dependencies: {
|
||||
hoist: ['toggles']
|
||||
managed: ['toggles']
|
||||
mentionable: ['toggles']
|
||||
premiumSubscriber: ['toggles']
|
||||
availableForPurchase: ['toggles']
|
||||
guildConnections: ['toggles']
|
||||
}
|
||||
alwaysPresents: ['internalTags']
|
||||
}
|
||||
hoist: ['toggles'];
|
||||
managed: ['toggles'];
|
||||
mentionable: ['toggles'];
|
||||
premiumSubscriber: ['toggles'];
|
||||
availableForPurchase: ['toggles'];
|
||||
guildConnections: ['toggles'];
|
||||
};
|
||||
alwaysPresents: ['internalTags'];
|
||||
};
|
||||
|
||||
user: {
|
||||
dependencies: {
|
||||
tag: ['username', 'discriminator']
|
||||
bot: ['toggles']
|
||||
system: ['toggles']
|
||||
mfaEnabled: ['toggles']
|
||||
verified: ['toggles']
|
||||
avatarUrl: ['avatar', 'id']
|
||||
displayName: ['username', 'globalName']
|
||||
defaultAvatarUrl: ['id', 'discriminator']
|
||||
displayAvatarUrl: ['avatar', 'id', 'discriminator']
|
||||
createdTimestamp: ['id']
|
||||
}
|
||||
alwaysPresents: []
|
||||
}
|
||||
tag: ['username', 'discriminator'];
|
||||
bot: ['toggles'];
|
||||
system: ['toggles'];
|
||||
mfaEnabled: ['toggles'];
|
||||
verified: ['toggles'];
|
||||
avatarUrl: ['avatar', 'id'];
|
||||
displayName: ['username', 'globalName'];
|
||||
defaultAvatarUrl: ['id', 'discriminator'];
|
||||
displayAvatarUrl: ['avatar', 'id', 'discriminator'];
|
||||
createdTimestamp: ['id'];
|
||||
};
|
||||
alwaysPresents: [];
|
||||
};
|
||||
|
||||
emoji: {
|
||||
dependencies: {
|
||||
animated: ['toggles']
|
||||
available: ['toggles']
|
||||
managed: ['toggles']
|
||||
requireColons: ['toggles']
|
||||
}
|
||||
alwaysPresents: ['toggles']
|
||||
}
|
||||
animated: ['toggles'];
|
||||
available: ['toggles'];
|
||||
managed: ['toggles'];
|
||||
requireColons: ['toggles'];
|
||||
};
|
||||
alwaysPresents: ['toggles'];
|
||||
};
|
||||
}
|
||||
|
||||
export function createDesiredPropertiesObject<T extends RecursivePartial<TransformersDesiredProperties>, TDefault extends boolean = false>(
|
||||
@@ -859,35 +859,35 @@ export function createDesiredPropertiesObject<T extends RecursivePartial<Transfo
|
||||
flags: defaultValue,
|
||||
...desiredProperties.lobbyMember,
|
||||
},
|
||||
} satisfies TransformersDesiredProperties as CompleteDesiredProperties<T, TDefault>
|
||||
} satisfies TransformersDesiredProperties as CompleteDesiredProperties<T, TDefault>;
|
||||
}
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export type KeyByValue<TObj, TValue> = {
|
||||
[Key in keyof TObj]: TObj[Key] extends TValue ? Key : never
|
||||
}[keyof TObj]
|
||||
[Key in keyof TObj]: TObj[Key] extends TValue ? Key : never;
|
||||
}[keyof TObj];
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export type Complete<TObj, TDefault> = {
|
||||
[K in keyof TObj]-?: undefined extends TObj[K] ? TDefault : Exclude<TObj[K], undefined>
|
||||
}
|
||||
[K in keyof TObj]-?: undefined extends TObj[K] ? TDefault : Exclude<TObj[K], undefined>;
|
||||
};
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export type JoinTuple<T extends string[], TDelimiter extends string> = T extends readonly [infer F extends string, ...infer R extends string[]]
|
||||
? R['length'] extends 0
|
||||
? F
|
||||
: `${F}${TDelimiter}${JoinTuple<R, TDelimiter>}`
|
||||
: ''
|
||||
: '';
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export type DesiredPropertiesMetadata = {
|
||||
[K in keyof TransformersObjects]: {
|
||||
dependencies?: {
|
||||
[Key in keyof TransformersObjects[K]]?: (keyof TransformersObjects[K])[]
|
||||
}
|
||||
alwaysPresents?: (keyof TransformersObjects[K])[]
|
||||
}
|
||||
}
|
||||
[Key in keyof TransformersObjects[K]]?: (keyof TransformersObjects[K])[];
|
||||
};
|
||||
alwaysPresents?: (keyof TransformersObjects[K])[];
|
||||
};
|
||||
};
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export type DesirableProperties<
|
||||
@@ -901,24 +901,24 @@ export type DesirableProperties<
|
||||
| (keyof T extends NonNullable<TransformersDesiredPropertiesMetadata[TKey]['alwaysPresents']>[number]
|
||||
? never
|
||||
: NonNullable<TransformersDesiredPropertiesMetadata[TKey]['alwaysPresents']>[number])
|
||||
>
|
||||
>;
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export type DesiredPropertiesMapper<T extends TransformersObjects[keyof TransformersObjects]> = {
|
||||
[Key in DesirableProperties<T>]: boolean
|
||||
}
|
||||
[Key in DesirableProperties<T>]: boolean;
|
||||
};
|
||||
|
||||
declare const TypeErrorSymbol: unique symbol
|
||||
declare const TypeErrorSymbol: unique symbol;
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export interface DesiredPropertiesError<T extends string> {
|
||||
[TypeErrorSymbol]: T
|
||||
[TypeErrorSymbol]: T;
|
||||
}
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export type AreDependenciesSatisfied<T, TDependencies extends Record<string, string[]> | undefined, TProps> = {
|
||||
[K in keyof T]: IsKeyDesired<T[K], TDependencies, TProps> extends true ? true : false
|
||||
}
|
||||
[K in keyof T]: IsKeyDesired<T[K], TDependencies, TProps> extends true ? true : false;
|
||||
};
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export type IsKeyDesired<TKey, TDependencies extends Record<string, string[]> | undefined, TProps> = TKey extends keyof TProps // The key has a desired props?
|
||||
@@ -937,7 +937,7 @@ export type IsKeyDesired<TKey, TDependencies extends Record<string, string[]> |
|
||||
: // No, this is a key to not include
|
||||
DesiredPropertiesError<`This property depends on the following properties: ${JoinTuple<NonNullable<TDependencies>[TKey], ', '>}. Not all of these props are set as desired in desiredProperties option in createBot(), so you can't use it. More info here: https://discordeno.js.org/desired-props`>
|
||||
: // No, we include it but it does not have neither props nor dependencies
|
||||
true
|
||||
true;
|
||||
|
||||
/** The behavior it should be used when resolving an undesired property */
|
||||
export enum DesiredPropertiesBehavior {
|
||||
@@ -954,7 +954,7 @@ export type RemoveKeyIfUndesired<Key, T, TProps extends TransformersDesiredPrope
|
||||
TProps[KeyByValue<TransformersObjects, T>]
|
||||
> extends true
|
||||
? Key
|
||||
: never
|
||||
: never;
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export type GetErrorWhenUndesired<
|
||||
@@ -968,10 +968,10 @@ export type GetErrorWhenUndesired<
|
||||
TransformersDesiredPropertiesMetadata[KeyByValue<TransformersObjects, T>]['dependencies'],
|
||||
TProps[KeyByValue<TransformersObjects, T>]
|
||||
>,
|
||||
> = TIsDesired extends true ? TransformProperty<T[Key], TProps, TBehavior> : TIsDesired
|
||||
> = TIsDesired extends true ? TransformProperty<T[Key], TProps, TBehavior> : TIsDesired;
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export type IsObject<T> = T extends object ? (T extends Function ? false : true) : false
|
||||
export type IsObject<T> = T extends object ? (T extends Function ? false : true) : false;
|
||||
|
||||
// If the object is a transformed object, a collection of transformed object or an array of transformed objects we need to apply the desired props to them as well
|
||||
// NOTE: changing the order of these ternaries can cause bugs, for this reason we check in this order:
|
||||
@@ -1015,7 +1015,7 @@ export type TransformProperty<T, TProps extends TransformersDesiredProperties, T
|
||||
? // Yes, we need to ensure nested inside there aren't transformed objects
|
||||
{ [K in keyof T]: TransformProperty<T[K], TProps, TBehavior> }
|
||||
: // No, this is a normal value such as string / bigint / number
|
||||
T
|
||||
T;
|
||||
|
||||
/**
|
||||
* Apply desired properties to a transformer object.
|
||||
@@ -1031,17 +1031,17 @@ export type SetupDesiredProps<
|
||||
: Key]: // When the behavior is to change the type we use the GetErrorWhenUndesired type helper else apply the desired props to the key and return
|
||||
TBehavior extends DesiredPropertiesBehavior.ChangeType
|
||||
? GetErrorWhenUndesired<Key, T, TProps, TBehavior>
|
||||
: TransformProperty<T[Key], TProps, TBehavior>
|
||||
}
|
||||
: TransformProperty<T[Key], TProps, TBehavior>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The desired properties for each transformer object.
|
||||
*/
|
||||
export type TransformersDesiredProperties = {
|
||||
[Key in keyof TransformersObjects]: DesiredPropertiesMapper<TransformersObjects[Key]>
|
||||
}
|
||||
[Key in keyof TransformersObjects]: DesiredPropertiesMapper<TransformersObjects[Key]>;
|
||||
};
|
||||
|
||||
/** @private This is subject to breaking changes without notices */
|
||||
export type CompleteDesiredProperties<T extends RecursivePartial<TransformersDesiredProperties>, TTDefault extends boolean = false> = {
|
||||
[K in keyof TransformersDesiredProperties]: Complete<Partial<TransformersDesiredProperties[K]> & T[K], TTDefault>
|
||||
}
|
||||
[K in keyof TransformersDesiredProperties]: Complete<Partial<TransformersDesiredProperties[K]> & T[K], TTDefault>;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DiscordGatewayPayload, DiscordRateLimited, DiscordReady, DiscordVoiceChannelEffectAnimationType } from '@discordeno/types'
|
||||
import type { Collection } from '@discordeno/utils'
|
||||
import type { DesiredPropertiesBehavior, SetupDesiredProps, TransformersDesiredProperties } from './desiredProperties.js'
|
||||
import type { DiscordGatewayPayload, DiscordRateLimited, DiscordReady, DiscordVoiceChannelEffectAnimationType } from '@discordeno/types';
|
||||
import type { Collection } from '@discordeno/utils';
|
||||
import type { DesiredPropertiesBehavior, SetupDesiredProps, TransformersDesiredProperties } from './desiredProperties.js';
|
||||
import type {
|
||||
AuditLogEntry,
|
||||
AutoModerationActionExecution,
|
||||
@@ -24,137 +24,137 @@ import type {
|
||||
ThreadMember,
|
||||
User,
|
||||
VoiceState,
|
||||
} from './transformers/types.js'
|
||||
} from './transformers/types.js';
|
||||
|
||||
export type EventHandlers<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> = {
|
||||
applicationCommandPermissionsUpdate: (command: GuildApplicationCommandPermissions) => unknown
|
||||
guildAuditLogEntryCreate: (log: AuditLogEntry, guildId: bigint) => unknown
|
||||
automodRuleCreate: (rule: AutoModerationRule) => unknown
|
||||
automodRuleUpdate: (rule: AutoModerationRule) => unknown
|
||||
automodRuleDelete: (rule: AutoModerationRule) => unknown
|
||||
automodActionExecution: (payload: AutoModerationActionExecution) => unknown
|
||||
threadCreate: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
|
||||
threadDelete: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
|
||||
applicationCommandPermissionsUpdate: (command: GuildApplicationCommandPermissions) => unknown;
|
||||
guildAuditLogEntryCreate: (log: AuditLogEntry, guildId: bigint) => unknown;
|
||||
automodRuleCreate: (rule: AutoModerationRule) => unknown;
|
||||
automodRuleUpdate: (rule: AutoModerationRule) => unknown;
|
||||
automodRuleDelete: (rule: AutoModerationRule) => unknown;
|
||||
automodActionExecution: (payload: AutoModerationActionExecution) => unknown;
|
||||
threadCreate: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
|
||||
threadDelete: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
|
||||
threadListSync: (payload: {
|
||||
guildId: bigint
|
||||
channelIds?: bigint[]
|
||||
threads: SetupDesiredProps<Channel, TProps, TBehavior>[]
|
||||
members: ThreadMember[]
|
||||
}) => unknown
|
||||
threadMemberUpdate: (payload: { id: bigint; guildId: bigint; joinedTimestamp: number; flags: number }) => unknown
|
||||
threadMembersUpdate: (payload: { id: bigint; guildId: bigint; addedMembers?: ThreadMember[]; removedMemberIds?: bigint[] }) => unknown
|
||||
threadUpdate: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
|
||||
scheduledEventCreate: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown
|
||||
scheduledEventUpdate: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown
|
||||
scheduledEventDelete: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown
|
||||
scheduledEventUserAdd: (payload: { guildScheduledEventId: bigint; guildId: bigint; userId: bigint }) => unknown
|
||||
scheduledEventUserRemove: (payload: { guildScheduledEventId: bigint; guildId: bigint; userId: bigint }) => unknown
|
||||
guildId: bigint;
|
||||
channelIds?: bigint[];
|
||||
threads: SetupDesiredProps<Channel, TProps, TBehavior>[];
|
||||
members: ThreadMember[];
|
||||
}) => unknown;
|
||||
threadMemberUpdate: (payload: { id: bigint; guildId: bigint; joinedTimestamp: number; flags: number }) => unknown;
|
||||
threadMembersUpdate: (payload: { id: bigint; guildId: bigint; addedMembers?: ThreadMember[]; removedMemberIds?: bigint[] }) => unknown;
|
||||
threadUpdate: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
|
||||
scheduledEventCreate: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown;
|
||||
scheduledEventUpdate: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown;
|
||||
scheduledEventDelete: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown;
|
||||
scheduledEventUserAdd: (payload: { guildScheduledEventId: bigint; guildId: bigint; userId: bigint }) => unknown;
|
||||
scheduledEventUserRemove: (payload: { guildScheduledEventId: bigint; guildId: bigint; userId: bigint }) => unknown;
|
||||
ready: (
|
||||
payload: {
|
||||
shardId: number
|
||||
v: number
|
||||
user: SetupDesiredProps<User, TProps, TBehavior>
|
||||
guilds: bigint[]
|
||||
sessionId: string
|
||||
shard?: number[]
|
||||
applicationId: bigint
|
||||
shardId: number;
|
||||
v: number;
|
||||
user: SetupDesiredProps<User, TProps, TBehavior>;
|
||||
guilds: bigint[];
|
||||
sessionId: string;
|
||||
shard?: number[];
|
||||
applicationId: bigint;
|
||||
},
|
||||
rawPayload: DiscordReady,
|
||||
) => unknown
|
||||
resumed: (shardId: number) => unknown
|
||||
rateLimited: (data: DiscordRateLimited, shardId: number) => unknown
|
||||
interactionCreate: (interaction: SetupDesiredProps<Interaction, TProps, TBehavior>) => unknown
|
||||
integrationCreate: (integration: Integration) => unknown
|
||||
integrationDelete: (payload: { id: bigint; guildId: bigint; applicationId?: bigint }) => unknown
|
||||
integrationUpdate: (payload: { guildId: bigint }) => unknown
|
||||
inviteCreate: (invite: SetupDesiredProps<Invite, TProps, TBehavior>) => unknown
|
||||
inviteDelete: (payload: { channelId: bigint; guildId?: bigint; code: string }) => unknown
|
||||
guildMemberAdd: (member: SetupDesiredProps<Member, TProps, TBehavior>, user: SetupDesiredProps<User, TProps, TBehavior>) => unknown
|
||||
guildMemberRemove: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown
|
||||
guildMemberUpdate: (member: SetupDesiredProps<Member, TProps, TBehavior>, user: SetupDesiredProps<User, TProps, TBehavior>) => unknown
|
||||
guildStickersUpdate: (payload: { guildId: bigint; stickers: SetupDesiredProps<Sticker, TProps, TBehavior>[] }) => unknown
|
||||
messageCreate: (message: SetupDesiredProps<Message, TProps, TBehavior>) => unknown
|
||||
messageDelete: (payload: { id: bigint; channelId: bigint; guildId?: bigint }, message?: SetupDesiredProps<Message, TProps, TBehavior>) => unknown
|
||||
messageDeleteBulk: (payload: { ids: bigint[]; channelId: bigint; guildId?: bigint }) => unknown
|
||||
messageUpdate: (message: SetupDesiredProps<Message, TProps, TBehavior>) => unknown
|
||||
) => unknown;
|
||||
resumed: (shardId: number) => unknown;
|
||||
rateLimited: (data: DiscordRateLimited, shardId: number) => unknown;
|
||||
interactionCreate: (interaction: SetupDesiredProps<Interaction, TProps, TBehavior>) => unknown;
|
||||
integrationCreate: (integration: Integration) => unknown;
|
||||
integrationDelete: (payload: { id: bigint; guildId: bigint; applicationId?: bigint }) => unknown;
|
||||
integrationUpdate: (payload: { guildId: bigint }) => unknown;
|
||||
inviteCreate: (invite: SetupDesiredProps<Invite, TProps, TBehavior>) => unknown;
|
||||
inviteDelete: (payload: { channelId: bigint; guildId?: bigint; code: string }) => unknown;
|
||||
guildMemberAdd: (member: SetupDesiredProps<Member, TProps, TBehavior>, user: SetupDesiredProps<User, TProps, TBehavior>) => unknown;
|
||||
guildMemberRemove: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown;
|
||||
guildMemberUpdate: (member: SetupDesiredProps<Member, TProps, TBehavior>, user: SetupDesiredProps<User, TProps, TBehavior>) => unknown;
|
||||
guildStickersUpdate: (payload: { guildId: bigint; stickers: SetupDesiredProps<Sticker, TProps, TBehavior>[] }) => unknown;
|
||||
messageCreate: (message: SetupDesiredProps<Message, TProps, TBehavior>) => unknown;
|
||||
messageDelete: (payload: { id: bigint; channelId: bigint; guildId?: bigint }, message?: SetupDesiredProps<Message, TProps, TBehavior>) => unknown;
|
||||
messageDeleteBulk: (payload: { ids: bigint[]; channelId: bigint; guildId?: bigint }) => unknown;
|
||||
messageUpdate: (message: SetupDesiredProps<Message, TProps, TBehavior>) => unknown;
|
||||
reactionAdd: (payload: {
|
||||
userId: bigint
|
||||
channelId: bigint
|
||||
messageId: bigint
|
||||
guildId?: bigint
|
||||
member?: SetupDesiredProps<Member, TProps, TBehavior>
|
||||
user?: SetupDesiredProps<User, TProps, TBehavior>
|
||||
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>
|
||||
messageAuthorId?: bigint
|
||||
burst: boolean
|
||||
burstColors?: string[]
|
||||
}) => unknown
|
||||
userId: bigint;
|
||||
channelId: bigint;
|
||||
messageId: bigint;
|
||||
guildId?: bigint;
|
||||
member?: SetupDesiredProps<Member, TProps, TBehavior>;
|
||||
user?: SetupDesiredProps<User, TProps, TBehavior>;
|
||||
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>;
|
||||
messageAuthorId?: bigint;
|
||||
burst: boolean;
|
||||
burstColors?: string[];
|
||||
}) => unknown;
|
||||
reactionRemove: (payload: {
|
||||
userId: bigint
|
||||
channelId: bigint
|
||||
messageId: bigint
|
||||
guildId?: bigint
|
||||
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>
|
||||
burst: boolean
|
||||
}) => unknown
|
||||
userId: bigint;
|
||||
channelId: bigint;
|
||||
messageId: bigint;
|
||||
guildId?: bigint;
|
||||
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>;
|
||||
burst: boolean;
|
||||
}) => unknown;
|
||||
reactionRemoveEmoji: (payload: {
|
||||
channelId: bigint
|
||||
messageId: bigint
|
||||
guildId?: bigint
|
||||
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>
|
||||
}) => unknown
|
||||
reactionRemoveAll: (payload: { channelId: bigint; messageId: bigint; guildId?: bigint }) => unknown
|
||||
presenceUpdate: (presence: PresenceUpdate) => unknown
|
||||
channelId: bigint;
|
||||
messageId: bigint;
|
||||
guildId?: bigint;
|
||||
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>;
|
||||
}) => unknown;
|
||||
reactionRemoveAll: (payload: { channelId: bigint; messageId: bigint; guildId?: bigint }) => unknown;
|
||||
presenceUpdate: (presence: PresenceUpdate) => unknown;
|
||||
voiceChannelEffectSend: (payload: {
|
||||
channelId: bigint
|
||||
guildId: bigint
|
||||
userId: bigint
|
||||
emoji?: SetupDesiredProps<Emoji, TProps, TBehavior>
|
||||
animationType?: DiscordVoiceChannelEffectAnimationType
|
||||
animationId?: number
|
||||
soundId?: bigint | number
|
||||
soundVolume?: number
|
||||
}) => unknown
|
||||
voiceServerUpdate: (payload: { token: string; endpoint?: string; guildId: bigint }) => unknown
|
||||
voiceStateUpdate: (voiceState: SetupDesiredProps<VoiceState, TProps, TBehavior>) => unknown
|
||||
channelCreate: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
|
||||
dispatchRequirements: (data: DiscordGatewayPayload, shardId: number) => unknown
|
||||
channelDelete: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
|
||||
channelPinsUpdate: (data: { guildId?: bigint; channelId: bigint; lastPinTimestamp?: number }) => unknown
|
||||
channelUpdate: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
|
||||
stageInstanceCreate: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown
|
||||
stageInstanceDelete: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown
|
||||
stageInstanceUpdate: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown
|
||||
guildEmojisUpdate: (payload: { guildId: bigint; emojis: Collection<bigint, SetupDesiredProps<Emoji, TProps, TBehavior>> }) => unknown
|
||||
guildBanAdd: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown
|
||||
guildBanRemove: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown
|
||||
guildCreate: (guild: SetupDesiredProps<Guild, TProps, TBehavior>) => unknown
|
||||
guildDelete: (data: { id: bigint; unavailable: boolean }, shardId: number) => unknown
|
||||
guildUpdate: (guild: SetupDesiredProps<Guild, TProps, TBehavior>) => unknown
|
||||
raw: (data: DiscordGatewayPayload, shardId: number) => unknown
|
||||
roleCreate: (role: SetupDesiredProps<Role, TProps, TBehavior>) => unknown
|
||||
roleDelete: (payload: { guildId: bigint; roleId: bigint }) => unknown
|
||||
roleUpdate: (role: SetupDesiredProps<Role, TProps, TBehavior>) => unknown
|
||||
webhooksUpdate: (payload: { channelId: bigint; guildId: bigint }) => unknown
|
||||
botUpdate: (user: SetupDesiredProps<User, TProps, TBehavior>) => unknown
|
||||
channelId: bigint;
|
||||
guildId: bigint;
|
||||
userId: bigint;
|
||||
emoji?: SetupDesiredProps<Emoji, TProps, TBehavior>;
|
||||
animationType?: DiscordVoiceChannelEffectAnimationType;
|
||||
animationId?: number;
|
||||
soundId?: bigint | number;
|
||||
soundVolume?: number;
|
||||
}) => unknown;
|
||||
voiceServerUpdate: (payload: { token: string; endpoint?: string; guildId: bigint }) => unknown;
|
||||
voiceStateUpdate: (voiceState: SetupDesiredProps<VoiceState, TProps, TBehavior>) => unknown;
|
||||
channelCreate: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
|
||||
dispatchRequirements: (data: DiscordGatewayPayload, shardId: number) => unknown;
|
||||
channelDelete: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
|
||||
channelPinsUpdate: (data: { guildId?: bigint; channelId: bigint; lastPinTimestamp?: number }) => unknown;
|
||||
channelUpdate: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
|
||||
stageInstanceCreate: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown;
|
||||
stageInstanceDelete: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown;
|
||||
stageInstanceUpdate: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown;
|
||||
guildEmojisUpdate: (payload: { guildId: bigint; emojis: Collection<bigint, SetupDesiredProps<Emoji, TProps, TBehavior>> }) => unknown;
|
||||
guildBanAdd: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown;
|
||||
guildBanRemove: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown;
|
||||
guildCreate: (guild: SetupDesiredProps<Guild, TProps, TBehavior>) => unknown;
|
||||
guildDelete: (data: { id: bigint; unavailable: boolean }, shardId: number) => unknown;
|
||||
guildUpdate: (guild: SetupDesiredProps<Guild, TProps, TBehavior>) => unknown;
|
||||
raw: (data: DiscordGatewayPayload, shardId: number) => unknown;
|
||||
roleCreate: (role: SetupDesiredProps<Role, TProps, TBehavior>) => unknown;
|
||||
roleDelete: (payload: { guildId: bigint; roleId: bigint }) => unknown;
|
||||
roleUpdate: (role: SetupDesiredProps<Role, TProps, TBehavior>) => unknown;
|
||||
webhooksUpdate: (payload: { channelId: bigint; guildId: bigint }) => unknown;
|
||||
botUpdate: (user: SetupDesiredProps<User, TProps, TBehavior>) => unknown;
|
||||
typingStart: (payload: {
|
||||
guildId: bigint | undefined
|
||||
channelId: bigint
|
||||
userId: bigint
|
||||
timestamp: number
|
||||
member: SetupDesiredProps<Member, TProps, TBehavior> | undefined
|
||||
}) => unknown
|
||||
entitlementCreate: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown
|
||||
entitlementUpdate: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown
|
||||
entitlementDelete: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown
|
||||
subscriptionCreate: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown
|
||||
subscriptionUpdate: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown
|
||||
subscriptionDelete: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown
|
||||
messagePollVoteAdd: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown
|
||||
messagePollVoteRemove: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown
|
||||
soundboardSoundCreate: (payload: SetupDesiredProps<SoundboardSound, TProps, TBehavior>) => unknown
|
||||
soundboardSoundUpdate: (payload: SetupDesiredProps<SoundboardSound, TProps, TBehavior>) => unknown
|
||||
soundboardSoundDelete: (payload: { soundId: bigint; guildId: bigint }) => unknown
|
||||
soundboardSoundsUpdate: (payload: { soundboardSounds: SetupDesiredProps<SoundboardSound, TProps, TBehavior>[]; guildId: bigint }) => unknown
|
||||
soundboardSounds: (payload: { soundboardSounds: SetupDesiredProps<SoundboardSound, TProps, TBehavior>[]; guildId: bigint }) => unknown
|
||||
}
|
||||
guildId: bigint | undefined;
|
||||
channelId: bigint;
|
||||
userId: bigint;
|
||||
timestamp: number;
|
||||
member: SetupDesiredProps<Member, TProps, TBehavior> | undefined;
|
||||
}) => unknown;
|
||||
entitlementCreate: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown;
|
||||
entitlementUpdate: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown;
|
||||
entitlementDelete: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown;
|
||||
subscriptionCreate: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown;
|
||||
subscriptionUpdate: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown;
|
||||
subscriptionDelete: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown;
|
||||
messagePollVoteAdd: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown;
|
||||
messagePollVoteRemove: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown;
|
||||
soundboardSoundCreate: (payload: SetupDesiredProps<SoundboardSound, TProps, TBehavior>) => unknown;
|
||||
soundboardSoundUpdate: (payload: SetupDesiredProps<SoundboardSound, TProps, TBehavior>) => unknown;
|
||||
soundboardSoundDelete: (payload: { soundId: bigint; guildId: bigint }) => unknown;
|
||||
soundboardSoundsUpdate: (payload: { soundboardSounds: SetupDesiredProps<SoundboardSound, TProps, TBehavior>[]; guildId: bigint }) => unknown;
|
||||
soundboardSounds: (payload: { soundboardSounds: SetupDesiredProps<SoundboardSound, TProps, TBehavior>[]; guildId: bigint }) => unknown;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { DiscordGatewayPayload, GatewayDispatchEventNames } from '@discordeno/types'
|
||||
import type { Bot } from './bot.js'
|
||||
import type { DesiredPropertiesBehavior, TransformersDesiredProperties } from './desiredProperties.js'
|
||||
import * as handlers from './handlers/index.js'
|
||||
import type { DiscordGatewayPayload, GatewayDispatchEventNames } from '@discordeno/types';
|
||||
import type { Bot } from './bot.js';
|
||||
import type { DesiredPropertiesBehavior, TransformersDesiredProperties } from './desiredProperties.js';
|
||||
import * as handlers from './handlers/index.js';
|
||||
|
||||
export function createBotGatewayHandlers<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior>(
|
||||
options: Partial<GatewayHandlers<TProps, TBehavior>>,
|
||||
): GatewayHandlers<TProps, TBehavior> {
|
||||
const _options = options as Partial<GatewayHandlers<TransformersDesiredProperties, DesiredPropertiesBehavior.RemoveKey>>
|
||||
const _options = options as Partial<GatewayHandlers<TransformersDesiredProperties, DesiredPropertiesBehavior.RemoveKey>>;
|
||||
|
||||
return {
|
||||
APPLICATION_COMMAND_PERMISSIONS_UPDATE: _options.APPLICATION_COMMAND_PERMISSIONS_UPDATE ?? handlers.handleApplicationCommandPermissionsUpdate,
|
||||
@@ -85,16 +85,16 @@ export function createBotGatewayHandlers<TProps extends TransformersDesiredPrope
|
||||
GUILD_SOUNDBOARD_SOUND_UPDATE: _options.GUILD_SOUNDBOARD_SOUND_UPDATE ?? handlers.handleGuildSoundboardSoundUpdate,
|
||||
GUILD_SOUNDBOARD_SOUNDS_UPDATE: _options.GUILD_SOUNDBOARD_SOUNDS_UPDATE ?? handlers.handleGuildSoundboardSoundsUpdate,
|
||||
SOUNDBOARD_SOUNDS: _options.SOUNDBOARD_SOUNDS ?? handlers.handleSoundboardSounds,
|
||||
} satisfies GatewayHandlers<TransformersDesiredProperties, DesiredPropertiesBehavior.RemoveKey> as unknown as GatewayHandlers<TProps, TBehavior>
|
||||
} satisfies GatewayHandlers<TransformersDesiredProperties, DesiredPropertiesBehavior.RemoveKey> as unknown as GatewayHandlers<TProps, TBehavior>;
|
||||
}
|
||||
|
||||
export type GatewayHandlers<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> = Record<
|
||||
GatewayDispatchEventNames,
|
||||
BotGatewayHandler<TProps, TBehavior>
|
||||
>
|
||||
>;
|
||||
|
||||
export type BotGatewayHandler<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> = (
|
||||
bot: Bot<TProps, TBehavior>,
|
||||
data: DiscordGatewayPayload,
|
||||
shardId: number,
|
||||
) => unknown
|
||||
) => unknown;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleChannelCreate(bot: Bot, payload: DiscordGatewayPayload, _shardId: number): Promise<void> {
|
||||
if (!bot.events.channelCreate) return
|
||||
if (!bot.events.channelCreate) return;
|
||||
|
||||
const data = payload.d as DiscordChannel
|
||||
const channel = bot.transformers.channel(bot, data, { guildId: data.guild_id })
|
||||
const data = payload.d as DiscordChannel;
|
||||
const channel = bot.transformers.channel(bot, data, { guildId: data.guild_id });
|
||||
|
||||
bot.events.channelCreate(channel)
|
||||
bot.events.channelCreate(channel);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleChannelDelete(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.channelDelete) return
|
||||
if (!bot.events.channelDelete) return;
|
||||
|
||||
const payload = data.d as DiscordChannel
|
||||
const payload = data.d as DiscordChannel;
|
||||
|
||||
bot.events.channelDelete(bot.transformers.channel(bot, payload, { guildId: payload.guild_id }))
|
||||
bot.events.channelDelete(bot.transformers.channel(bot, payload, { guildId: payload.guild_id }));
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { DiscordChannelPinsUpdate, DiscordGatewayPayload } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordChannelPinsUpdate, DiscordGatewayPayload } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleChannelPinsUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.channelPinsUpdate) return
|
||||
if (!bot.events.channelPinsUpdate) return;
|
||||
|
||||
const payload = data.d as DiscordChannelPinsUpdate
|
||||
const payload = data.d as DiscordChannelPinsUpdate;
|
||||
|
||||
bot.events.channelPinsUpdate({
|
||||
guildId: payload.guild_id ? bot.transformers.snowflake(payload.guild_id) : undefined,
|
||||
channelId: bot.transformers.snowflake(payload.channel_id),
|
||||
lastPinTimestamp: payload.last_pin_timestamp ? Date.parse(payload.last_pin_timestamp) : undefined,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleChannelUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.channelUpdate) return
|
||||
if (!bot.events.channelUpdate) return;
|
||||
|
||||
const payload = data.d as DiscordChannel
|
||||
const channel = bot.transformers.channel(bot, payload)
|
||||
const payload = data.d as DiscordChannel;
|
||||
const channel = bot.transformers.channel(bot, payload);
|
||||
|
||||
bot.events.channelUpdate(channel)
|
||||
bot.events.channelUpdate(channel);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleStageInstanceCreate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.stageInstanceCreate) return
|
||||
if (!bot.events.stageInstanceCreate) return;
|
||||
|
||||
const payload = data.d as DiscordStageInstance
|
||||
const payload = data.d as DiscordStageInstance;
|
||||
|
||||
bot.events.stageInstanceCreate({
|
||||
id: bot.transformers.snowflake(payload.id),
|
||||
guildId: bot.transformers.snowflake(payload.guild_id),
|
||||
channelId: bot.transformers.snowflake(payload.channel_id),
|
||||
topic: payload.topic,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleStageInstanceDelete(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.stageInstanceDelete) return
|
||||
if (!bot.events.stageInstanceDelete) return;
|
||||
|
||||
const payload = data.d as DiscordStageInstance
|
||||
const payload = data.d as DiscordStageInstance;
|
||||
|
||||
bot.events.stageInstanceDelete({
|
||||
id: bot.transformers.snowflake(payload.id),
|
||||
guildId: bot.transformers.snowflake(payload.guild_id),
|
||||
channelId: bot.transformers.snowflake(payload.channel_id),
|
||||
topic: payload.topic,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleStageInstanceUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.stageInstanceUpdate) return
|
||||
if (!bot.events.stageInstanceUpdate) return;
|
||||
|
||||
const payload = data.d as DiscordStageInstance
|
||||
const payload = data.d as DiscordStageInstance;
|
||||
|
||||
bot.events.stageInstanceUpdate({
|
||||
id: bot.transformers.snowflake(payload.id),
|
||||
guildId: bot.transformers.snowflake(payload.guild_id),
|
||||
channelId: bot.transformers.snowflake(payload.channel_id),
|
||||
topic: payload.topic,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleThreadCreate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.threadCreate) return
|
||||
if (!bot.events.threadCreate) return;
|
||||
|
||||
const payload = data.d as DiscordChannel
|
||||
const payload = data.d as DiscordChannel;
|
||||
|
||||
bot.events.threadCreate(bot.transformers.channel(bot, payload))
|
||||
bot.events.threadCreate(bot.transformers.channel(bot, payload));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleThreadDelete(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.threadDelete) return
|
||||
if (!bot.events.threadDelete) return;
|
||||
|
||||
const payload = data.d as DiscordChannel
|
||||
const payload = data.d as DiscordChannel;
|
||||
|
||||
bot.events.threadDelete(bot.transformers.channel(bot, payload))
|
||||
bot.events.threadDelete(bot.transformers.channel(bot, payload));
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { DiscordGatewayPayload, DiscordThreadListSync } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordGatewayPayload, DiscordThreadListSync } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleThreadListSync(bot: Bot, data: DiscordGatewayPayload): Promise<any> {
|
||||
if (!bot.events.threadListSync) return
|
||||
if (!bot.events.threadListSync) return;
|
||||
|
||||
const payload = data.d as DiscordThreadListSync
|
||||
const payload = data.d as DiscordThreadListSync;
|
||||
|
||||
const guildId = bot.transformers.snowflake(payload.guild_id)
|
||||
const guildId = bot.transformers.snowflake(payload.guild_id);
|
||||
|
||||
bot.events.threadListSync({
|
||||
guildId,
|
||||
@@ -18,5 +18,5 @@ export async function handleThreadListSync(bot: Bot, data: DiscordGatewayPayload
|
||||
joinTimestamp: Date.parse(member.join_timestamp),
|
||||
flags: member.flags,
|
||||
})),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { DiscordGatewayPayload, DiscordThreadMembersUpdate } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordGatewayPayload, DiscordThreadMembersUpdate } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleThreadMembersUpdate(bot: Bot, data: DiscordGatewayPayload, _shardId: number): Promise<void> {
|
||||
if (!bot.events.threadMembersUpdate) return
|
||||
if (!bot.events.threadMembersUpdate) return;
|
||||
|
||||
const payload = data.d as DiscordThreadMembersUpdate
|
||||
const payload = data.d as DiscordThreadMembersUpdate;
|
||||
|
||||
bot.events.threadMembersUpdate({
|
||||
id: bot.transformers.snowflake(payload.id),
|
||||
guildId: bot.transformers.snowflake(payload.guild_id),
|
||||
addedMembers: payload.added_members?.map((member) => bot.transformers.threadMember?.(bot, member, { guildId: payload.guild_id })),
|
||||
removedMemberIds: payload.removed_member_ids?.map((id) => bot.transformers.snowflake(id)),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { DiscordGatewayPayload, DiscordThreadMemberUpdate } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordGatewayPayload, DiscordThreadMemberUpdate } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleThreadMemberUpdate(bot: Bot, data: DiscordGatewayPayload, _shardId: number): Promise<void> {
|
||||
if (!bot.events.threadMemberUpdate) return
|
||||
if (!bot.events.threadMemberUpdate) return;
|
||||
|
||||
const payload = data.d as DiscordThreadMemberUpdate
|
||||
const payload = data.d as DiscordThreadMemberUpdate;
|
||||
|
||||
bot.events.threadMemberUpdate({
|
||||
id: bot.transformers.snowflake(payload.id),
|
||||
guildId: bot.transformers.snowflake(payload.guild_id),
|
||||
joinedTimestamp: Date.parse(payload.join_timestamp),
|
||||
flags: payload.flags,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleThreadUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.threadUpdate) return
|
||||
if (!bot.events.threadUpdate) return;
|
||||
|
||||
const payload = data.d as DiscordChannel
|
||||
const payload = data.d as DiscordChannel;
|
||||
|
||||
bot.events.threadUpdate(bot.transformers.channel(bot, payload))
|
||||
bot.events.threadUpdate(bot.transformers.channel(bot, payload));
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
export * from './CHANNEL_CREATE.js'
|
||||
export * from './CHANNEL_DELETE.js'
|
||||
export * from './CHANNEL_PINS_UPDATE.js'
|
||||
export * from './CHANNEL_UPDATE.js'
|
||||
export * from './STAGE_INSTANCE_CREATE.js'
|
||||
export * from './STAGE_INSTANCE_DELETE.js'
|
||||
export * from './STAGE_INSTANCE_UPDATE.js'
|
||||
export * from './THREAD_CREATE.js'
|
||||
export * from './THREAD_DELETE.js'
|
||||
export * from './THREAD_LIST_SYNC.js'
|
||||
export * from './THREAD_MEMBER_UPDATE.js'
|
||||
export * from './THREAD_MEMBERS_UPDATE.js'
|
||||
export * from './THREAD_UPDATE.js'
|
||||
export * from './CHANNEL_CREATE.js';
|
||||
export * from './CHANNEL_DELETE.js';
|
||||
export * from './CHANNEL_PINS_UPDATE.js';
|
||||
export * from './CHANNEL_UPDATE.js';
|
||||
export * from './STAGE_INSTANCE_CREATE.js';
|
||||
export * from './STAGE_INSTANCE_DELETE.js';
|
||||
export * from './STAGE_INSTANCE_UPDATE.js';
|
||||
export * from './THREAD_CREATE.js';
|
||||
export * from './THREAD_DELETE.js';
|
||||
export * from './THREAD_LIST_SYNC.js';
|
||||
export * from './THREAD_MEMBER_UPDATE.js';
|
||||
export * from './THREAD_MEMBERS_UPDATE.js';
|
||||
export * from './THREAD_UPDATE.js';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { DiscordGatewayPayload, DiscordGuildEmojisUpdate } from '@discordeno/types'
|
||||
import { Collection } from '@discordeno/utils'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordGatewayPayload, DiscordGuildEmojisUpdate } from '@discordeno/types';
|
||||
import { Collection } from '@discordeno/utils';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleGuildEmojisUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.guildEmojisUpdate) return
|
||||
if (!bot.events.guildEmojisUpdate) return;
|
||||
|
||||
const payload = data.d as DiscordGuildEmojisUpdate
|
||||
const payload = data.d as DiscordGuildEmojisUpdate;
|
||||
|
||||
bot.events.guildEmojisUpdate({
|
||||
guildId: bot.transformers.snowflake(payload.guild_id),
|
||||
emojis: new Collection(payload.emojis.map((emoji) => [bot.transformers.snowflake(emoji.id!), bot.transformers.emoji(bot, emoji)])),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './GUILD_EMOJIS_UPDATE.js'
|
||||
export * from './GUILD_EMOJIS_UPDATE.js';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleEntitlementCreate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.entitlementCreate) return
|
||||
if (!bot.events.entitlementCreate) return;
|
||||
|
||||
const payload = data.d as DiscordEntitlement
|
||||
bot.events.entitlementCreate(bot.transformers.entitlement(bot, payload))
|
||||
const payload = data.d as DiscordEntitlement;
|
||||
bot.events.entitlementCreate(bot.transformers.entitlement(bot, payload));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleEntitlementDelete(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.entitlementDelete) return
|
||||
if (!bot.events.entitlementDelete) return;
|
||||
|
||||
const payload = data.d as DiscordEntitlement
|
||||
bot.events.entitlementDelete(bot.transformers.entitlement(bot, payload))
|
||||
const payload = data.d as DiscordEntitlement;
|
||||
bot.events.entitlementDelete(bot.transformers.entitlement(bot, payload));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleEntitlementUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.entitlementUpdate) return
|
||||
if (!bot.events.entitlementUpdate) return;
|
||||
|
||||
const payload = data.d as DiscordEntitlement
|
||||
bot.events.entitlementUpdate(bot.transformers.entitlement(bot, payload))
|
||||
const payload = data.d as DiscordEntitlement;
|
||||
bot.events.entitlementUpdate(bot.transformers.entitlement(bot, payload));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './ENTITLEMENT_CREATE.js'
|
||||
export * from './ENTITLEMENT_DELETE.js'
|
||||
export * from './ENTITLEMENT_UPDATE.js'
|
||||
export * from './ENTITLEMENT_CREATE.js';
|
||||
export * from './ENTITLEMENT_DELETE.js';
|
||||
export * from './ENTITLEMENT_UPDATE.js';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { DiscordAuditLogEntry, DiscordGatewayPayload } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordAuditLogEntry, DiscordGatewayPayload } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleGuildAuditLogEntryCreate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.guildAuditLogEntryCreate) return
|
||||
if (!bot.events.guildAuditLogEntryCreate) return;
|
||||
|
||||
// TODO: better type here
|
||||
const payload = data.d as DiscordAuditLogEntry & { guild_id: string }
|
||||
bot.events.guildAuditLogEntryCreate(bot.transformers.auditLogEntry(bot, payload), bot.transformers.snowflake(payload.guild_id))
|
||||
const payload = data.d as DiscordAuditLogEntry & { guild_id: string };
|
||||
bot.events.guildAuditLogEntryCreate(bot.transformers.auditLogEntry(bot, payload), bot.transformers.snowflake(payload.guild_id));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { DiscordGatewayPayload, DiscordGuildBanAddRemove } from '@discordeno/types'
|
||||
import type { Bot } from '../../bot.js'
|
||||
import type { DiscordGatewayPayload, DiscordGuildBanAddRemove } from '@discordeno/types';
|
||||
import type { Bot } from '../../bot.js';
|
||||
|
||||
export async function handleGuildBanAdd(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
|
||||
if (!bot.events.guildBanAdd) return
|
||||
if (!bot.events.guildBanAdd) return;
|
||||
|
||||
const payload = data.d as DiscordGuildBanAddRemove
|
||||
bot.events.guildBanAdd(bot.transformers.user(bot, payload.user), bot.transformers.snowflake(payload.guild_id))
|
||||
const payload = data.d as DiscordGuildBanAddRemove;
|
||||
bot.events.guildBanAdd(bot.transformers.user(bot, payload.user), bot.transformers.snowflake(payload.guild_id));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user