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