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:
Fleny
2026-01-17 21:54:15 +01:00
committed by GitHub
parent f713b4ab7b
commit 27c261fee2
403 changed files with 11250 additions and 11217 deletions

View File

@@ -1,23 +1,23 @@
// @ts-check
// If we are running in Bun or Deno, they have native TypeScript support with .js imports, node requires .ts imports
const supportsTypescript = 'Bun' in globalThis || 'Deno' in globalThis
const supportsTypescript = 'Bun' in globalThis || 'Deno' in globalThis;
/** @type {import("mocha").MochaInstanceOptions & Record<string, unknown>} */
const mochaConfig = {
timeout: 2000,
'watch-extensions': 'ts',
'watch-files': ['src', 'tests'],
}
};
if (!supportsTypescript) {
mochaConfig.require = ['ts-node/register']
mochaConfig.require = ['ts-node/register'];
// Node options
mochaConfig.loader = ['ts-node/esm']
mochaConfig.loader = ['ts-node/esm'];
// Node will output a ExperimentalWarning about --loader (--experimental-loader) and a DeprecationWarning because ts-node uses fs.Stat
mochaConfig['no-warnings'] = true
mochaConfig['enable-source-maps'] = true
mochaConfig['no-warnings'] = true;
mochaConfig['enable-source-maps'] = true;
}
module.exports = mochaConfig
module.exports = mochaConfig;

View File

@@ -38,7 +38,6 @@
"javascript": {
"formatter": {
"trailingCommas": "all",
"semicolons": "asNeeded",
"quoteStyle": "single"
}
},

View File

@@ -1,10 +1,10 @@
import fastifyEnv from '@fastify/env'
import fastifyHelmet from '@fastify/helmet'
import fastifyMultipart from '@fastify/multipart'
import fastify, { type FastifyInstance } from 'fastify'
import fastifyEnv from '@fastify/env';
import fastifyHelmet from '@fastify/helmet';
import fastifyMultipart from '@fastify/multipart';
import fastify, { type FastifyInstance } from 'fastify';
export const buildFastifyApp = async (): Promise<FastifyInstance> => {
const app = await fastify()
const app = await fastify();
await app.register(fastifyEnv, {
schema: {
@@ -25,28 +25,28 @@ export const buildFastifyApp = async (): Promise<FastifyInstance> => {
},
required: ['DISCORD_TOKEN', 'AUTHORIZATION_TOKEN'],
},
})
});
await app.register(fastifyHelmet)
app.register(fastifyMultipart, { attachFieldsToBody: true })
await app.register(fastifyHelmet);
app.register(fastifyMultipart, { attachFieldsToBody: true });
app.addHook('onRequest', async (request, reply) => {
if (request.headers.authorization !== request.server.config.AUTHORIZATION_TOKEN) {
reply.status(401).send({
message: 'Credentials not valid.',
})
});
}
})
});
return app
}
return app;
};
declare module 'fastify' {
interface FastifyInstance {
config: {
HOST: string
DISCORD_TOKEN: string
AUTHORIZATION_TOKEN: string
}
HOST: string;
DISCORD_TOKEN: string;
AUTHORIZATION_TOKEN: string;
};
}
}

View File

@@ -1,79 +1,79 @@
import { createRestManager, type RequestMethods } from '@discordeno/rest'
import type { MultipartFile, MultipartValue } from '@fastify/multipart'
import { buildFastifyApp } from './fastify.js'
import { createRestManager, type RequestMethods } from '@discordeno/rest';
import type { MultipartFile, MultipartValue } from '@fastify/multipart';
import { buildFastifyApp } from './fastify.js';
const app = await buildFastifyApp()
const app = await buildFastifyApp();
if (!app.config.DISCORD_TOKEN || !app.config.AUTHORIZATION_TOKEN) {
console.error('Missing environment variables. Both DISCORD_TOKEN and AUTHORIZATION_TOKEN are required.')
process.exit(1)
console.error('Missing environment variables. Both DISCORD_TOKEN and AUTHORIZATION_TOKEN are required.');
process.exit(1);
}
const discordRestManager = createRestManager({
token: app.config.DISCORD_TOKEN,
})
});
app.get('/timecheck', async (_request, reply) => {
reply.status(200).send({
message: Date.now(),
})
})
});
});
app.all('/*', async (request, reply) => {
let url = request.originalUrl
let url = request.originalUrl;
if (url.startsWith('/v')) {
url = url.slice(url.indexOf('/', 2))
url = url.slice(url.indexOf('/', 2));
}
const isMultipart = request.headers['content-type']?.startsWith('multipart/form-data')
const body = request.method !== 'GET' && request.method !== 'DELETE' ? request.body : undefined
const isMultipart = request.headers['content-type']?.startsWith('multipart/form-data');
const body = request.method !== 'GET' && request.method !== 'DELETE' ? request.body : undefined;
try {
const result = await discordRestManager.makeRequest(request.method as RequestMethods, url, {
body: isMultipart && body ? await parseMultiformBody(body) : body,
})
});
if (result) {
reply.status(200).send(result)
reply.status(200).send(result);
} else {
reply.status(204).send({})
reply.status(204).send({});
}
} catch (error) {
app.log.error(error)
app.log.error(error);
reply.status(500).send({
message: error,
})
});
}
})
});
try {
await app.listen({
host: app.config.HOST,
port: 8000,
})
console.log(`Proxy listening on port 8000`)
});
console.log(`Proxy listening on port 8000`);
} catch (error) {
app.log.error(error)
process.exit(1)
app.log.error(error);
process.exit(1);
}
async function parseMultiformBody(body: unknown): Promise<FormData> {
const form = new FormData()
const form = new FormData();
if (typeof body !== 'object' || !body) return form
if (typeof body !== 'object' || !body) return form;
for (const objectValue of Object.values(body)) {
const value = objectValue as MultipartFile | MultipartValue
const value = objectValue as MultipartFile | MultipartValue;
if (value.type === 'file') {
form.append(value.fieldname, new Blob([Uint8Array.from(await value.toBuffer())]), value.filename)
form.append(value.fieldname, new Blob([Uint8Array.from(await value.toBuffer())]), value.filename);
}
if (value.type === 'field' && typeof value.value === 'string') {
form.append(value.fieldname, value.value)
form.append(value.fieldname, value.value);
}
}
return form
return form;
}

View File

@@ -1,6 +1,6 @@
import { createBot, type logger as discordenoLogger, Intents, LogDepth } from '@discordeno/bot'
import { createProxyCache } from 'dd-cache-proxy'
import { configs } from './config.js'
import { createBot, type logger as discordenoLogger, Intents, LogDepth } from '@discordeno/bot';
import { createProxyCache } from 'dd-cache-proxy';
import { configs } from './config.js';
const rawBot = createBot({
token: configs.token,
@@ -38,7 +38,7 @@ const rawBot = createBot({
discriminator: true,
},
},
})
});
export const bot = createProxyCache(rawBot, {
desiredProps: {
@@ -50,7 +50,7 @@ export const bot = createProxyCache(rawBot, {
role: true,
default: false,
},
})
});
// By default, bot.logger will use an instance of the logger from @discordeno/bot, this logger supports depth and we need to change it, so we need to say to TS that we know what we are doing with as
;(bot.logger as typeof discordenoLogger).setDepth(LogDepth.Full)
(bot.logger as typeof discordenoLogger).setDepth(LogDepth.Full);

View File

@@ -1,21 +1,21 @@
import { type ApplicationCommandOption, type ApplicationCommandTypes, Collection } from '@discordeno/bot'
import type { bot } from './bot.js'
import { type ApplicationCommandOption, type ApplicationCommandTypes, Collection } from '@discordeno/bot';
import type { bot } from './bot.js';
export const commands = new Collection<string, Command>()
export const commands = new Collection<string, Command>();
export function createCommand(command: Command): void {
commands.set(command.name, command)
commands.set(command.name, command);
}
export interface Command {
/** The name of this command. */
name: string
name: string;
/** What does this command do? */
description: string
description: string;
/** The type of command this is. */
type: ApplicationCommandTypes
type: ApplicationCommandTypes;
/** The options for this command */
options?: ApplicationCommandOption[]
options?: ApplicationCommandOption[];
/** This will be executed when the command is run. */
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: Record<string, unknown>) => unknown
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: Record<string, unknown>) => unknown;
}

View File

@@ -1,15 +1,15 @@
import { ApplicationCommandTypes, createEmbeds, snowflakeToTimestamp } from '@discordeno/bot'
import { createCommand } from '../commands.js'
import { ApplicationCommandTypes, createEmbeds, snowflakeToTimestamp } from '@discordeno/bot';
import { createCommand } from '../commands.js';
createCommand({
name: 'ping',
description: 'See if the bot latency is okay',
type: ApplicationCommandTypes.ChatInput,
async execute(interaction) {
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
const embeds = createEmbeds().setTitle(`The bot ping is ${ping}ms`)
const embeds = createEmbeds().setTitle(`The bot ping is ${ping}ms`);
await interaction.respond({ embeds })
await interaction.respond({ embeds });
},
})
});

View File

@@ -1,7 +1,7 @@
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, createEmbeds, type Member, Permissions, type User } from '@discordeno/bot'
import { bot } from '../bot.js'
import { createCommand } from '../commands.js'
import { calculateMemberPermissions } from '../utils/permissions.js'
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, createEmbeds, type Member, Permissions, type User } from '@discordeno/bot';
import { bot } from '../bot.js';
import { createCommand } from '../commands.js';
import { calculateMemberPermissions } from '../utils/permissions.js';
createCommand({
name: 'warn',
@@ -23,58 +23,58 @@ createCommand({
],
async execute(interaction, options) {
if (!interaction.guildId || !interaction.member) {
await interaction.respond('This command can only be ran in guilds')
return
await interaction.respond('This command can only be ran in guilds');
return;
}
// Type based on the options declared above
const { user, reason } = options as { user: UserResolved; reason?: string }
const { user, reason } = options as { user: UserResolved; reason?: string };
const guild = await bot.cache.guilds.get(interaction.guildId)
const guild = await bot.cache.guilds.get(interaction.guildId);
if (!guild || !guild.roles) {
await interaction.respond('An error has occurred')
return
await interaction.respond('An error has occurred');
return;
}
await interaction.defer()
await interaction.defer();
const perms = new Permissions(await calculateMemberPermissions(guild, interaction.member))
const perms = new Permissions(await calculateMemberPermissions(guild, interaction.member));
const adminPerm = perms.has('ADMINISTRATOR')
const kickMembersPerm = adminPerm || perms.has('KICK_MEMBERS')
const adminPerm = perms.has('ADMINISTRATOR');
const kickMembersPerm = adminPerm || perms.has('KICK_MEMBERS');
if (!kickMembersPerm) {
await interaction.respond("You don't have the necessary permissions to warn a members (this command requires `Kick members`)")
return
await interaction.respond("You don't have the necessary permissions to warn a members (this command requires `Kick members`)");
return;
}
const embeds = createEmbeds()
.setTitle('Warned User:')
.setDescription(`User: <@${user.user.id}>\nReason: ${reason}`)
.setColor(0x00ff00)
.setTimestamp(Date.now())
.setTimestamp(Date.now());
const warnEmbeds = createEmbeds()
.setTitle('Warning:')
.setDescription(`You have been warned in **${guild.name}** for \`${reason}\``)
.setTimestamp(Date.now())
.setTimestamp(Date.now());
try {
const dmChannel = await bot.helpers.getDmChannel(user.user.id)
await bot.helpers.sendMessage(dmChannel.id, { embeds: warnEmbeds })
const dmChannel = await bot.helpers.getDmChannel(user.user.id);
await bot.helpers.sendMessage(dmChannel.id, { embeds: warnEmbeds });
} catch (error) {
bot.logger.error(`There was an error in the warn command:`, error)
bot.logger.error(`There was an error in the warn command:`, error);
await interaction.respond(`Could not warn user <@${user.user.id}> | They likely do not have their DMs open.`)
return
await interaction.respond(`Could not warn user <@${user.user.id}> | They likely do not have their DMs open.`);
return;
}
await interaction.respond({ embeds })
await interaction.respond({ embeds });
},
})
});
interface UserResolved {
user: User
member?: Member
user: User;
member?: Member;
}

View File

@@ -1,11 +1,11 @@
const token = process.env.TOKEN
const token = process.env.TOKEN;
if (!token) throw new Error('Missing TOKEN environment variable')
if (!token) throw new Error('Missing TOKEN environment variable');
export const configs: Config = {
token,
}
};
export interface Config {
token: string
token: string;
}

View File

@@ -1,22 +1,22 @@
import { commandOptionsParser, InteractionTypes } from '@discordeno/bot'
import { bot } from '../bot.js'
import { commands } from '../commands.js'
import { commandOptionsParser, InteractionTypes } from '@discordeno/bot';
import { bot } from '../bot.js';
import { commands } from '../commands.js';
bot.events.interactionCreate = async (interaction) => {
if (!interaction.data || interaction.type !== InteractionTypes.ApplicationCommand) return
if (!interaction.data || interaction.type !== InteractionTypes.ApplicationCommand) return;
const command = commands.get(interaction.data.name)
const command = commands.get(interaction.data.name);
if (!command) {
bot.logger.error(`Command ${interaction.data.name} not found`)
return
bot.logger.error(`Command ${interaction.data.name} not found`);
return;
}
const options = commandOptionsParser(interaction)
const options = commandOptionsParser(interaction);
try {
await command.execute(interaction, options)
await command.execute(interaction, options);
} catch (error) {
bot.logger.error(`There was an error running the ${command.name} command.`, error)
bot.logger.error(`There was an error running the ${command.name} command.`, error);
}
}
};

View File

@@ -1,8 +1,8 @@
import { bot } from '../bot.js'
import { bot } from '../bot.js';
bot.events.ready = ({ user, shardId }) => {
if (shardId === bot.gateway.lastShardId) {
// All shards are ready
bot.logger.info(`Successfully connected to the gateway as ${user.username}#${user.discriminator}`)
bot.logger.info(`Successfully connected to the gateway as ${user.username}#${user.discriminator}`);
}
}
};

View File

@@ -1,14 +1,14 @@
import 'dotenv/config'
import 'dotenv/config';
import { bot } from './bot.js'
import importDirectory from './utils/loader.js'
import { bot } from './bot.js';
import importDirectory from './utils/loader.js';
bot.logger.info('Starting bot...')
bot.logger.info('Starting bot...');
bot.logger.info('Loading commands...')
await importDirectory('./dist/commands')
bot.logger.info('Loading commands...');
await importDirectory('./dist/commands');
bot.logger.info('Loading events...')
await importDirectory('./dist/events')
bot.logger.info('Loading events...');
await importDirectory('./dist/events');
await bot.start()
await bot.start();

View File

@@ -1,16 +1,16 @@
import 'dotenv/config'
import 'dotenv/config';
import { bot } from './bot.js'
import importDirectory from './utils/loader.js'
import { updateApplicationCommands } from './utils/updateCommands.js'
import { bot } from './bot.js';
import importDirectory from './utils/loader.js';
import { updateApplicationCommands } from './utils/updateCommands.js';
bot.logger.info('Loading commands...')
await importDirectory('./dist/commands')
bot.logger.info('Loading commands...');
await importDirectory('./dist/commands');
bot.logger.info('Updating commands...')
await updateApplicationCommands()
bot.logger.info('Updating commands...');
await updateApplicationCommands();
bot.logger.info('Done!')
bot.logger.info('Done!');
// We need to manually exit as the REST Manager has timeouts that will keep NodeJS alive
process.exit()
process.exit();

View File

@@ -1,15 +1,15 @@
import { readdir } from 'node:fs/promises'
import { logger } from '@discordeno/bot'
import { readdir } from 'node:fs/promises';
import { logger } from '@discordeno/bot';
export default async function importDirectory(folder: string): Promise<void> {
const files = await readdir(folder, { recursive: true })
const files = await readdir(folder, { recursive: true });
for (const filename of files) {
if (!filename.endsWith('.js')) continue
if (!filename.endsWith('.js')) continue;
// Using `file://` and `process.cwd()` to avoid weird issues with relative paths and/or Windows
await import(`file://${process.cwd()}/${folder}/${filename}`).catch((x) =>
logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x),
)
);
}
}

View File

@@ -1,24 +1,24 @@
import assert from 'node:assert'
import { BitwisePermissionFlags } from '@discordeno/bot'
import type { bot } from '../bot.js'
import assert from 'node:assert';
import { BitwisePermissionFlags } from '@discordeno/bot';
import type { bot } from '../bot.js';
export async function calculateMemberPermissions(
guild: typeof bot.transformers.$inferredTypes.guild | typeof bot.cache.$inferredTypes.guild,
member: typeof bot.transformers.$inferredTypes.member,
) {
if (member.id === guild.ownerId) return 8n
if (member.id === guild.ownerId) return 8n;
let permissions = guild.roles?.get(guild.id)?.permissions.bitfield
const rolePerms = member.roles.map((x) => guild.roles?.get(x)?.permissions.bitfield).filter((x): x is bigint => x !== undefined)
let permissions = guild.roles?.get(guild.id)?.permissions.bitfield;
const rolePerms = member.roles.map((x) => guild.roles?.get(x)?.permissions.bitfield).filter((x): x is bigint => x !== undefined);
// Small hack to avoid calling assert with 0n
if (permissions === undefined) assert(permissions)
if (permissions === undefined) assert(permissions);
for (const rolePerm of rolePerms) {
permissions |= rolePerm
permissions |= rolePerm;
}
if ((permissions & BigInt(BitwisePermissionFlags.ADMINISTRATOR)) === BigInt(BitwisePermissionFlags.ADMINISTRATOR)) return 8n
if ((permissions & BigInt(BitwisePermissionFlags.ADMINISTRATOR)) === BigInt(BitwisePermissionFlags.ADMINISTRATOR)) return 8n;
return permissions
return permissions;
}

View File

@@ -1,6 +1,6 @@
import { bot } from '../bot.js'
import { commands } from '../commands.js'
import { bot } from '../bot.js';
import { commands } from '../commands.js';
export async function updateApplicationCommands(): Promise<void> {
await bot.helpers.upsertGlobalApplicationCommands(commands.array())
await bot.helpers.upsertGlobalApplicationCommands(commands.array());
}

View File

@@ -1,6 +1,6 @@
import { createBot, Intents } from '@discordeno/bot'
import { createProxyCache } from 'dd-cache-proxy'
import { configs } from './config.js'
import { createBot, Intents } from '@discordeno/bot';
import { createProxyCache } from 'dd-cache-proxy';
import { configs } from './config.js';
const rawBot = createBot({
token: configs.token,
@@ -22,7 +22,7 @@ const rawBot = createBot({
username: true,
},
},
})
});
export const bot = createProxyCache(rawBot, {
desiredProps: {
@@ -32,4 +32,4 @@ export const bot = createProxyCache(rawBot, {
guild: true,
default: false,
},
})
});

View File

@@ -1,27 +1,27 @@
import { type ApplicationCommandOption, type ApplicationCommandTypes, Collection } from '@discordeno/bot'
import type { bot } from './bot.js'
import { type ApplicationCommandOption, type ApplicationCommandTypes, Collection } from '@discordeno/bot';
import type { bot } from './bot.js';
export const commands = new Collection<string, Command>()
export const commands = new Collection<string, Command>();
export function createCommand(command: Command): void {
commands.set(command.name, command)
commands.set(command.name, command);
}
export interface Command {
name: string
description: string
usage?: string[]
options?: ApplicationCommandOption[]
type: ApplicationCommandTypes
name: string;
description: string;
usage?: string[];
options?: ApplicationCommandOption[];
type: ApplicationCommandTypes;
/** Defaults to `Guild` */
scope?: 'Global' | 'Guild'
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction) => unknown
subcommands?: Array<SubCommandGroup | SubCommand>
scope?: 'Global' | 'Guild';
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction) => unknown;
subcommands?: Array<SubCommandGroup | SubCommand>;
}
export type SubCommand = Omit<Command, 'subcommands'>
export type SubCommand = Omit<Command, 'subcommands'>;
export interface SubCommandGroup {
name: string
subCommands: SubCommand[]
name: string;
subCommands: SubCommand[];
}

View File

@@ -1,6 +1,6 @@
import { ApplicationCommandTypes, snowflakeToTimestamp } from '@discordeno/bot'
import { createCommand } from '../commands.js'
import { humanizeMilliseconds } from '../utils/helpers.js'
import { ApplicationCommandTypes, snowflakeToTimestamp } from '@discordeno/bot';
import { createCommand } from '../commands.js';
import { humanizeMilliseconds } from '../utils/helpers.js';
createCommand({
name: 'ping',
@@ -8,8 +8,8 @@ createCommand({
type: ApplicationCommandTypes.ChatInput,
scope: 'Global',
async execute(interaction) {
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
await interaction.respond(`🏓 Pong! Ping ${ping}ms (${humanizeMilliseconds(ping)})`)
await interaction.respond(`🏓 Pong! Ping ${ping}ms (${humanizeMilliseconds(ping)})`);
},
})
});

View File

@@ -1,12 +1,12 @@
const token = process.env.BOT_TOKEN
const token = process.env.BOT_TOKEN;
if (!token) throw new Error('Missing BOT_TOKEN environment variable')
if (!token) throw new Error('Missing BOT_TOKEN environment variable');
export const configs: Config = {
/** Get token from ENV variable */
token,
}
};
export interface Config {
token: string
token: string;
}

View File

@@ -1,4 +1,4 @@
import { bot } from '../bot.js'
import { updateGuildCommands } from '../utils/helpers.js'
import { bot } from '../bot.js';
import { updateGuildCommands } from '../utils/helpers.js';
bot.events.guildCreate = async (guild) => await updateGuildCommands(guild)
bot.events.guildCreate = async (guild) => await updateGuildCommands(guild);

View File

@@ -1,94 +1,94 @@
import { ApplicationCommandOptionTypes, hasProperty } from '@discordeno/bot'
import chalk from 'chalk'
import { bot } from '../bot.js'
import { commands } from '../commands.js'
import { getGuildFromId, isSubCommand, isSubCommandGroup } from '../utils/helpers.js'
import { createLogger } from '../utils/logger.js'
import { ApplicationCommandOptionTypes, hasProperty } from '@discordeno/bot';
import chalk from 'chalk';
import { bot } from '../bot.js';
import { commands } from '../commands.js';
import { getGuildFromId, isSubCommand, isSubCommandGroup } from '../utils/helpers.js';
import { createLogger } from '../utils/logger.js';
const logger = createLogger({ name: 'Event: InteractionCreate' })
const logger = createLogger({ name: 'Event: InteractionCreate' });
bot.events.interactionCreate = async (interaction) => {
if (!interaction.data || !interaction.id) return
if (!interaction.data || !interaction.id) return;
let guildName = 'Direct Message'
let guild = {} as typeof bot.transformers.$inferredTypes.guild
let guildName = 'Direct Message';
let guild = {} as typeof bot.transformers.$inferredTypes.guild;
// Set guild, if there was an error getting the guild, then just say it was a DM. (What else are we going to do?)
if (interaction.guildId) {
const guildOrVoid = await getGuildFromId(interaction.guildId).catch((err) => {
logger.error(err)
})
logger.error(err);
});
if (guildOrVoid) {
guild = guildOrVoid
guildName = guild.name
guild = guildOrVoid;
guildName = guild.name;
}
}
logger.info(
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.white(`Trigger`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
)
);
let command = commands.get(interaction.data.name)
let command = commands.get(interaction.data.name);
if (!command) {
logger.warn(
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.yellow(`Not Found`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
)
);
return
return;
}
if (interaction.data.options?.[0]) {
const optionType = interaction.data.options[0].type
const optionType = interaction.data.options[0].type;
if (optionType === ApplicationCommandOptionTypes.SubCommandGroup) {
// Check if command has subcommand and handle types
if (!command.subcommands) return
if (!command.subcommands) return;
// Try to find the subcommand group
const subCommandGroup = command.subcommands?.find((command) => command.name === interaction.data?.options?.[0].name)
if (!subCommandGroup) return
const subCommandGroup = command.subcommands?.find((command) => command.name === interaction.data?.options?.[0].name);
if (!subCommandGroup) return;
if (isSubCommand(subCommandGroup)) return
if (isSubCommand(subCommandGroup)) return;
// Get name of the command which we are looking for
const targetCmdName = interaction.data.options?.[0].options?.[0].name ?? interaction.data.options?.[0].options?.[0].name
if (!targetCmdName) return
const targetCmdName = interaction.data.options?.[0].options?.[0].name ?? interaction.data.options?.[0].options?.[0].name;
if (!targetCmdName) return;
// Try to find the command
command = subCommandGroup.subCommands.find((c) => c.name === targetCmdName)
command = subCommandGroup.subCommands.find((c) => c.name === targetCmdName);
}
if (optionType === ApplicationCommandOptionTypes.SubCommand) {
// Check if command has subcommand and handle types
if (!command?.subcommands) return
if (!command?.subcommands) return;
// Try to find the command
const found = command.subcommands.find((command) => command.name === interaction.data?.options?.[0].name)
if (!found) return
const found = command.subcommands.find((command) => command.name === interaction.data?.options?.[0].name);
if (!found) return;
if (isSubCommandGroup(found)) return
if (isSubCommandGroup(found)) return;
command = found
command = found;
}
}
try {
if (!command) throw new Error('Not command could be found')
if (!command) throw new Error('Not command could be found');
await command.execute(interaction)
await command.execute(interaction);
logger.info(
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.green(`Success`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
)
);
} catch (err) {
logger.error(
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.red(`Error`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
)
);
if (typeof err !== 'object' || !err || !hasProperty(err, 'message') || err.message === 'Not command could be found') return
if (typeof err !== 'object' || !err || !hasProperty(err, 'message') || err.message === 'Not command could be found') return;
logger.error(err)
logger.error(err);
}
}
};

View File

@@ -1,11 +1,11 @@
import { ActivityTypes } from '@discordeno/bot'
import { bot } from '../bot.js'
import { createLogger } from '../utils/logger.js'
import { ActivityTypes } from '@discordeno/bot';
import { bot } from '../bot.js';
import { createLogger } from '../utils/logger.js';
const logger = createLogger({ name: 'Event: Ready' })
const logger = createLogger({ name: 'Event: Ready' });
bot.events.ready = async ({ shardId }) => {
logger.info('Bot Ready')
logger.info('Bot Ready');
await bot.gateway.editShardStatus(shardId, {
status: 'online',
@@ -18,5 +18,5 @@ bot.events.ready = async ({ shardId }) => {
},
},
],
})
}
});
};

View File

@@ -1,15 +1,15 @@
import 'dotenv/config'
import 'dotenv/config';
import { bot } from './bot.js'
import importDirectory from './utils/loader.js'
import logger from './utils/logger.js'
import { bot } from './bot.js';
import importDirectory from './utils/loader.js';
import logger from './utils/logger.js';
logger.info('Starting bot...')
logger.info('Starting bot...');
logger.info('Loading commands...')
await importDirectory('./dist/commands')
logger.info('Loading commands...');
await importDirectory('./dist/commands');
logger.info('Loading events...')
await importDirectory('./dist/events')
logger.info('Loading events...');
await importDirectory('./dist/events');
await bot.start()
await bot.start();

View File

@@ -1,16 +1,16 @@
import 'dotenv/config'
import 'dotenv/config';
import { bot } from './bot.js'
import { updateCommands } from './utils/helpers.js'
import importDirectory from './utils/loader.js'
import { bot } from './bot.js';
import { updateCommands } from './utils/helpers.js';
import importDirectory from './utils/loader.js';
bot.logger.info('Loading commands...')
await importDirectory('./dist/commands')
bot.logger.info('Loading commands...');
await importDirectory('./dist/commands');
bot.logger.info('Updating commands...')
await updateCommands()
bot.logger.info('Updating commands...');
await updateCommands();
bot.logger.info('Done!')
bot.logger.info('Done!');
// We need to manually exit as the REST Manager has timeouts that will keep NodeJS alive
process.exit()
process.exit();

View File

@@ -1,14 +1,14 @@
import { type CreateApplicationCommand, hasProperty } from '@discordeno/bot'
import { bot } from '../bot.js'
import { commands, type SubCommand, type SubCommandGroup } from '../commands.js'
import { createLogger } from './logger.js'
import { type CreateApplicationCommand, hasProperty } from '@discordeno/bot';
import { bot } from '../bot.js';
import { commands, type SubCommand, type SubCommandGroup } from '../commands.js';
import { createLogger } from './logger.js';
const logger = createLogger({ name: 'Helpers' })
const logger = createLogger({ name: 'Helpers' });
/** This function will update all commands, or the defined scope */
export async function updateCommands(scope?: 'Guild' | 'Global'): Promise<void> {
const globalCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = []
const perGuildCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = []
const globalCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = [];
const perGuildCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = [];
for (const command of commands.values()) {
if (command.scope === 'Guild') {
@@ -17,34 +17,34 @@ export async function updateCommands(scope?: 'Guild' | 'Global'): Promise<void>
description: command.description,
type: command.type,
options: command.options ? command.options : undefined,
})
});
} else {
globalCommands.push({
name: command.name,
description: command.description,
type: command.type,
options: command.options ? command.options : undefined,
})
});
}
}
if (globalCommands.length && (scope === 'Global' || scope === undefined)) {
logger.info('Updating Global Commands, changes should apply in short...')
await bot.helpers.upsertGlobalApplicationCommands(globalCommands).catch(logger.error)
logger.info('Updating Global Commands, changes should apply in short...');
await bot.helpers.upsertGlobalApplicationCommands(globalCommands).catch(logger.error);
}
if (perGuildCommands.length && (scope === 'Guild' || scope === undefined)) {
await Promise.all(
bot.cache.guilds.memory.map(async (guild) => {
await bot.helpers.upsertGuildApplicationCommands(guild.id, perGuildCommands)
await bot.helpers.upsertGuildApplicationCommands(guild.id, perGuildCommands);
}),
)
);
}
}
/** Update commands for a guild */
export async function updateGuildCommands(guild: typeof bot.transformers.$inferredTypes.guild): Promise<void> {
const perGuildCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = []
const perGuildCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = [];
for (const command of commands.values()) {
if (command.scope === 'Guild') {
@@ -53,49 +53,49 @@ export async function updateGuildCommands(guild: typeof bot.transformers.$inferr
description: command.description,
type: command.type,
options: command.options ? command.options : undefined,
})
});
}
}
if (perGuildCommands.length) {
await bot.helpers.upsertGuildApplicationCommands(guild.id, perGuildCommands)
await bot.helpers.upsertGuildApplicationCommands(guild.id, perGuildCommands);
}
}
export async function getGuildFromId(guildId: bigint) {
const cached = await bot.cache.guilds.get(guildId)
const cached = await bot.cache.guilds.get(guildId);
if (cached) return cached
if (cached) return cached;
return await bot.helpers.getGuild(guildId)
return await bot.helpers.getGuild(guildId);
}
export function humanizeMilliseconds(milliseconds: number): string {
// Gets ms into seconds
const time = milliseconds / 1000
if (time < 1) return '< 1s'
const time = milliseconds / 1000;
if (time < 1) return '< 1s';
const days = Math.floor(time / 86400)
const hours = Math.floor((time % 86400) / 3600)
const minutes = Math.floor(((time % 86400) % 3600) / 60)
const seconds = Math.floor(((time % 86400) % 3600) % 60)
const days = Math.floor(time / 86400);
const hours = Math.floor((time % 86400) / 3600);
const minutes = Math.floor(((time % 86400) % 3600) / 60);
const seconds = Math.floor(((time % 86400) % 3600) % 60);
const dayString = days ? `${days}d ` : ''
const hourString = hours ? `${hours}h ` : ''
const minuteString = minutes ? `${minutes}m ` : ''
const secondString = seconds ? `${seconds}s ` : ''
const dayString = days ? `${days}d ` : '';
const hourString = hours ? `${hours}h ` : '';
const minuteString = minutes ? `${minutes}m ` : '';
const secondString = seconds ? `${seconds}s ` : '';
return `${dayString}${hourString}${minuteString}${secondString}`
return `${dayString}${hourString}${minuteString}${secondString}`;
}
export function isSubCommand(data: SubCommand | SubCommandGroup): data is SubCommand {
return !hasProperty(data, 'subCommands')
return !hasProperty(data, 'subCommands');
}
export function isSubCommandGroup(data: SubCommand | SubCommandGroup): data is SubCommandGroup {
return hasProperty(data, 'subCommands')
return hasProperty(data, 'subCommands');
}
type MakeRequired<TObj, TKey extends keyof TObj> = TObj & {
[Key in TKey]-?: TObj[Key]
}
[Key in TKey]-?: TObj[Key];
};

View File

@@ -1,15 +1,15 @@
import { readdir } from 'node:fs/promises'
import logger from './logger.js'
import { readdir } from 'node:fs/promises';
import logger from './logger.js';
export default async function importDirectory(folder: string): Promise<void> {
const files = await readdir(folder, { recursive: true })
const files = await readdir(folder, { recursive: true });
for (const filename of files) {
if (!filename.endsWith('.js')) continue
if (!filename.endsWith('.js')) continue;
// Using `file://` and `process.cwd()` to avoid weird issues with relative paths and/or Windows
await import(`file://${process.cwd()}/${folder}/${filename}`).catch((x) =>
logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x),
)
);
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import chalk from 'chalk'
import chalk from 'chalk';
export enum LogLevels {
Debug,
@@ -15,70 +15,70 @@ const prefixes = new Map<LogLevels, string>([
[LogLevels.Warn, 'WARN'],
[LogLevels.Error, 'ERROR'],
[LogLevels.Fatal, 'FATAL'],
])
]);
const noColor: (str: string) => string = (msg) => msg
const noColor: (str: string) => string = (msg) => msg;
const colorFunctions = new Map<LogLevels, (str: string) => string>([
[LogLevels.Debug, chalk.gray],
[LogLevels.Info, chalk.cyan],
[LogLevels.Warn, chalk.yellow],
[LogLevels.Error, (str: string) => chalk.red(str)],
[LogLevels.Fatal, (str: string) => chalk.red.bold.italic(str)],
])
]);
export function createLogger({ logLevel = LogLevels.Info, name }: { logLevel?: LogLevels; name?: string } = {}): Logger {
function log(level: LogLevels, ...args: any[]): void {
if (level < logLevel) return
if (level < logLevel) return;
let color = colorFunctions.get(level)
if (!color) color = noColor
let color = colorFunctions.get(level);
if (!color) color = noColor;
const date = new Date()
const date = new Date();
const log = [
`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`,
color(prefixes.get(level) ?? 'DEBUG'),
name ? `${name} >` : '>',
...args,
]
];
switch (level) {
case LogLevels.Debug:
return console.debug(...log)
return console.debug(...log);
case LogLevels.Info:
return console.info(...log)
return console.info(...log);
case LogLevels.Warn:
return console.warn(...log)
return console.warn(...log);
case LogLevels.Error:
return console.error(...log)
return console.error(...log);
case LogLevels.Fatal:
return console.error(...log)
return console.error(...log);
default:
return console.log(...log)
return console.log(...log);
}
}
function setLevel(level: LogLevels): void {
logLevel = level
logLevel = level;
}
function debug(...args: any[]): void {
log(LogLevels.Debug, ...args)
log(LogLevels.Debug, ...args);
}
function info(...args: any[]): void {
log(LogLevels.Info, ...args)
log(LogLevels.Info, ...args);
}
function warn(...args: any[]): void {
log(LogLevels.Warn, ...args)
log(LogLevels.Warn, ...args);
}
function error(...args: any[]): void {
log(LogLevels.Error, ...args)
log(LogLevels.Error, ...args);
}
function fatal(...args: any[]): void {
log(LogLevels.Fatal, ...args)
log(LogLevels.Fatal, ...args);
}
return {
@@ -89,18 +89,18 @@ export function createLogger({ logLevel = LogLevels.Info, name }: { logLevel?: L
warn,
error,
fatal,
}
};
}
export const logger = createLogger({ name: 'Main' })
export default logger
export const logger = createLogger({ name: 'Main' });
export default logger;
export interface Logger {
log: (level: LogLevels, ...args: any[]) => void
debug: (...args: any[]) => void
info: (...args: any[]) => void
warn: (...args: any[]) => void
error: (...args: any[]) => void
fatal: (...args: any[]) => void
setLevel: (level: LogLevels) => void
log: (level: LogLevels, ...args: any[]) => void;
debug: (...args: any[]) => void;
info: (...args: any[]) => void;
warn: (...args: any[]) => void;
error: (...args: any[]) => void;
fatal: (...args: any[]) => void;
setLevel: (level: LogLevels) => void;
}

View File

@@ -1,7 +1,7 @@
import { Collection, createBot } from '@discordeno/bot'
import { DISCORD_TOKEN, GATEWAY_AUTHORIZATION, GATEWAY_INTENTS, GATEWAY_URL, REST_AUTHORIZATION, REST_URL } from '../config.js'
import type { ManagerGetShardInfoFromGuildId, ShardInfo, WorkerPresencesUpdate, WorkerShardPayload } from '../gateway/worker/types.js'
import type { Command } from './commands.js'
import { Collection, createBot } from '@discordeno/bot';
import { DISCORD_TOKEN, GATEWAY_AUTHORIZATION, GATEWAY_INTENTS, GATEWAY_URL, REST_AUTHORIZATION, REST_URL } from '../config.js';
import type { ManagerGetShardInfoFromGuildId, ShardInfo, WorkerPresencesUpdate, WorkerShardPayload } from '../gateway/worker/types.js';
import type { Command } from './commands.js';
const rawBot = createBot({
token: DISCORD_TOKEN,
@@ -28,19 +28,19 @@ const rawBot = createBot({
authorization: REST_AUTHORIZATION,
},
},
})
});
export const bot = rawBot as CustomBot
export const bot = rawBot as CustomBot;
// TEMPLATE-SETUP: If you want/need to add any custom properties on the Bot type, you can do it in these lines below and the `CustomBot` type below. Make sure to do it in both or else you will get an error by TypeScript
bot.commands = new Collection()
bot.commands = new Collection();
overrideGatewayImplementations(bot)
overrideGatewayImplementations(bot);
export type CustomBot = typeof rawBot & {
commands: Collection<string, Command>
}
commands: Collection<string, Command>;
};
// Override the default gateway functions to allow the methods on the gateway object to proxy the requests to the gateway proxy
function overrideGatewayImplementations(bot: CustomBot): void {
@@ -56,8 +56,8 @@ function overrideGatewayImplementations(bot: CustomBot): void {
'Content-Type': 'application/json',
Authorization: GATEWAY_AUTHORIZATION,
},
})
}
});
};
bot.gateway.editBotStatus = async (payload) => {
await fetch(GATEWAY_URL, {
@@ -70,8 +70,8 @@ function overrideGatewayImplementations(bot: CustomBot): void {
'Content-Type': 'application/json',
Authorization: GATEWAY_AUTHORIZATION,
},
})
}
});
};
}
export async function getShardInfoFromGuild(guildId?: bigint): Promise<Omit<ShardInfo, 'nonce'>> {
@@ -85,11 +85,11 @@ export async function getShardInfoFromGuild(guildId?: bigint): Promise<Omit<Shar
'Content-Type': 'application/json',
Authorization: GATEWAY_AUTHORIZATION,
},
})
});
const res = await req.json()
const res = await req.json();
if (req.ok) return res
if (req.ok) return res;
throw new Error(`There was an issue getting the shard info: ${res.error}`)
throw new Error(`There was an issue getting the shard info: ${res.error}`);
}

View File

@@ -5,49 +5,49 @@ import type {
CreateApplicationCommand,
DiscordApplicationCommandOption,
ParsedInteractionOption,
} from '@discordeno/bot'
import { bot } from './bot.js'
} from '@discordeno/bot';
import { bot } from './bot.js';
export default function createCommand<const TOptions extends CommandOptions>(command: Command<TOptions>): void {
bot.commands.set(command.name, command as Command)
bot.commands.set(command.name, command as Command);
}
export type Command<TOptions extends CommandOptions = CommandOptions> = CreateApplicationCommand & {
/** @inheritdoc */
options?: TOptions
options?: TOptions;
/**
* Should this command be only deployed on the Dev guild?
*
* @default false
*/
devOnly?: boolean
devOnly?: boolean;
/** Function to run when the interaction is executed */
run: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: GetCommandOptions<TOptions>) => unknown
run: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: GetCommandOptions<TOptions>) => unknown;
/** Function to run when an autocomplete interaction is fired */
autoComplete?: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: GetCommandOptions<TOptions>) => unknown
}
autoComplete?: (interaction: typeof bot.transformers.$inferredTypes.interaction, options: GetCommandOptions<TOptions>) => unknown;
};
export type GetCommandOptions<T extends CommandOptions> = T extends CommandOptions
? { [Prop in keyof BuildOptions<T> as Prop]: BuildOptions<T>[Prop] }
: never
: never;
export type CommandOption = Camelize<DiscordApplicationCommandOption>
export type CommandOptions = CommandOption[]
export type CommandOption = Camelize<DiscordApplicationCommandOption>;
export type CommandOptions = CommandOption[];
// Option parsing
type ResolvedValues = ParsedInteractionOption<ExtractDesiredProps<typeof bot>, ExtractDesiredBehavior<typeof bot>>[string]
type ResolvedValues = ParsedInteractionOption<ExtractDesiredProps<typeof bot>, ExtractDesiredBehavior<typeof bot>>[string];
// Using omit + exclude is a slight trick to avoid a type error on Pick
export type InteractionResolvedChannel = Omit<
typeof bot.transformers.$inferredTypes.channel,
Exclude<keyof typeof bot.transformers.$inferredTypes.channel, 'id' | 'name' | 'type' | 'permissions' | 'threadMetadata' | 'parentId'>
>
export type InteractionResolvedMember = Omit<typeof bot.transformers.$inferredTypes.member, 'user' | 'deaf' | 'mute'>
>;
export type InteractionResolvedMember = Omit<typeof bot.transformers.$inferredTypes.member, 'user' | 'deaf' | 'mute'>;
export interface InteractionResolvedUser {
user: typeof bot.transformers.$inferredTypes.user
member: InteractionResolvedMember
user: typeof bot.transformers.$inferredTypes.user;
member: InteractionResolvedMember;
}
/**
@@ -56,30 +56,30 @@ export interface InteractionResolvedUser {
* The entries are sorted based on the enum value
*/
interface TypeToResolvedMap {
[ApplicationCommandOptionTypes.String]: string
[ApplicationCommandOptionTypes.Integer]: number
[ApplicationCommandOptionTypes.Boolean]: boolean
[ApplicationCommandOptionTypes.User]: InteractionResolvedUser
[ApplicationCommandOptionTypes.Channel]: InteractionResolvedChannel
[ApplicationCommandOptionTypes.Role]: typeof bot.transformers.$inferredTypes.role
[ApplicationCommandOptionTypes.Mentionable]: typeof bot.transformers.$inferredTypes.role | InteractionResolvedUser
[ApplicationCommandOptionTypes.Number]: number
[ApplicationCommandOptionTypes.Attachment]: typeof bot.transformers.$inferredTypes.attachment
[ApplicationCommandOptionTypes.String]: string;
[ApplicationCommandOptionTypes.Integer]: number;
[ApplicationCommandOptionTypes.Boolean]: boolean;
[ApplicationCommandOptionTypes.User]: InteractionResolvedUser;
[ApplicationCommandOptionTypes.Channel]: InteractionResolvedChannel;
[ApplicationCommandOptionTypes.Role]: typeof bot.transformers.$inferredTypes.role;
[ApplicationCommandOptionTypes.Mentionable]: typeof bot.transformers.$inferredTypes.role | InteractionResolvedUser;
[ApplicationCommandOptionTypes.Number]: number;
[ApplicationCommandOptionTypes.Attachment]: typeof bot.transformers.$inferredTypes.attachment;
}
type ConvertTypeToResolved<T extends ApplicationCommandOptionTypes> = T extends keyof TypeToResolvedMap ? TypeToResolvedMap[T] : ResolvedValues
type ConvertTypeToResolved<T extends ApplicationCommandOptionTypes> = T extends keyof TypeToResolvedMap ? TypeToResolvedMap[T] : ResolvedValues;
type SubCommandApplicationCommand = ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup
type GetOptionName<T> = T extends { name: string } ? T['name'] : never
type SubCommandApplicationCommand = ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup;
type GetOptionName<T> = T extends { name: string } ? T['name'] : never;
type GetOptionValue<T> = T extends { type: ApplicationCommandOptionTypes; required?: boolean }
? T extends { type: SubCommandApplicationCommand; options?: CommandOptions }
? BuildOptions<T['options']>
: ConvertTypeToResolved<T['type']> | (T['required'] extends true ? never : undefined)
: never
: never;
type BuildOptions<T extends CommandOptions | undefined> = {
[Prop in keyof Omit<T, keyof unknown[]> as GetOptionName<T[Prop]>]: GetOptionValue<T[Prop]>
}
[Prop in keyof Omit<T, keyof unknown[]> as GetOptionName<T[Prop]>]: GetOptionValue<T[Prop]>;
};
type ExtractDesiredProps<T> = T extends Bot<infer Props, infer _Behavior> ? Props : never
type ExtractDesiredBehavior<T> = T extends Bot<infer _Props, infer Behavior> ? Behavior : never
type ExtractDesiredProps<T> = T extends Bot<infer Props, infer _Behavior> ? Props : never;
type ExtractDesiredBehavior<T> = T extends Bot<infer _Props, infer Behavior> ? Behavior : never;

View File

@@ -1,16 +1,16 @@
import { snowflakeToTimestamp } from '@discordeno/bot'
import { getShardInfoFromGuild } from '../bot.js'
import createCommand from '../commands.js'
import { snowflakeToTimestamp } from '@discordeno/bot';
import { getShardInfoFromGuild } from '../bot.js';
import createCommand from '../commands.js';
createCommand({
name: 'ping',
description: '🏓 Check whether the bot is online and responsive.',
async run(interaction) {
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
const shardInfo = await getShardInfoFromGuild(interaction.guildId)
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
const shardInfo = await getShardInfoFromGuild(interaction.guildId);
const shardPing = shardInfo.rtt === -1 ? '*Not yet available*' : `${shardInfo.rtt}ms`
const shardPing = shardInfo.rtt === -1 ? '*Not yet available*' : `${shardInfo.rtt}ms`;
await interaction.respond(`🏓 Pong! Gateway Latency: ${shardPing}, Roundtrip Latency: ${ping}ms. I am online and responsive! 🕙`)
await interaction.respond(`🏓 Pong! Gateway Latency: ${shardPing}, Roundtrip Latency: ${ping}ms. I am online and responsive! 🕙`);
},
})
});

View File

@@ -1,39 +1,39 @@
import { commandOptionsParser, InteractionTypes, LogLevels, type logger } from '@discordeno/bot'
import chalk from 'chalk'
import { bot } from '../../bot.js'
import { commandOptionsParser, InteractionTypes, LogLevels, type logger } from '@discordeno/bot';
import chalk from 'chalk';
import { bot } from '../../bot.js';
bot.events.interactionCreate = async (interaction) => {
const isAutocomplete = interaction.type === InteractionTypes.ApplicationCommandAutocomplete
const isCommandOrAutocomplete = interaction.type === InteractionTypes.ApplicationCommand || isAutocomplete
const isAutocomplete = interaction.type === InteractionTypes.ApplicationCommandAutocomplete;
const isCommandOrAutocomplete = interaction.type === InteractionTypes.ApplicationCommand || isAutocomplete;
if (!interaction.data || !isCommandOrAutocomplete) return
if (!interaction.data || !isCommandOrAutocomplete) return;
const command = bot.commands.get(interaction.data.name)
const command = bot.commands.get(interaction.data.name);
if (!command) {
logCommand(interaction, 'Missing', interaction.data.name)
await interaction.respond('❌ Something went wrong. I was not able to find this command.')
logCommand(interaction, 'Missing', interaction.data.name);
await interaction.respond('❌ Something went wrong. I was not able to find this command.');
return
return;
}
logCommand(interaction, 'Trigger', interaction.data.name)
logCommand(interaction, 'Trigger', interaction.data.name);
const options = commandOptionsParser(interaction)
const options = commandOptionsParser(interaction);
try {
if (isAutocomplete) {
await command.autoComplete?.(interaction, options)
await command.autoComplete?.(interaction, options);
} else {
await command.run(interaction, options)
await command.run(interaction, options);
}
logCommand(interaction, 'Success', interaction.data.name)
logCommand(interaction, 'Success', interaction.data.name);
} catch (error) {
logCommand(interaction, 'Failure', interaction.data.name, LogLevels.Error, error)
await interaction.respond('❌ Something went wrong. The command execution has thrown an error.')
logCommand(interaction, 'Failure', interaction.data.name, LogLevels.Error, error);
await interaction.respond('❌ Something went wrong. The command execution has thrown an error.');
}
}
};
function logCommand(
interaction: typeof bot.transformers.$inferredTypes.interaction,
@@ -42,11 +42,11 @@ function logCommand(
logLevel: LogLevels = LogLevels.Info,
...restArgs: unknown[]
): void {
const typeColor = ['Failure', 'Missing'].includes(type) ? chalk.red(type) : type === 'Success' ? chalk.green(type) : chalk.white(type)
const typeColor = ['Failure', 'Missing'].includes(type) ? chalk.red(type) : type === 'Success' ? chalk.green(type) : chalk.white(type);
const autocomplete = interaction.type === InteractionTypes.ApplicationCommandAutocomplete ? ' (AutoComplete) ' : ''
const command = `Command${autocomplete}: ${chalk.bgYellow.black(commandName || 'Unknown')} - ${chalk.bgBlack(typeColor)}`
const user = chalk.bgGreen.black(`@${interaction.user.username} (${interaction.user.id})`)
const guild = chalk.bgMagenta.black(interaction.guildId ? `guildId: ${interaction.guildId}` : 'DM')
;(bot.logger as typeof logger).log(logLevel, `${command} - By ${user} in ${guild}`, ...restArgs)
const autocomplete = interaction.type === InteractionTypes.ApplicationCommandAutocomplete ? ' (AutoComplete) ' : '';
const command = `Command${autocomplete}: ${chalk.bgYellow.black(commandName || 'Unknown')} - ${chalk.bgBlack(typeColor)}`;
const user = chalk.bgGreen.black(`@${interaction.user.username} (${interaction.user.id})`);
const guild = chalk.bgMagenta.black(interaction.guildId ? `guildId: ${interaction.guildId}` : 'DM');
(bot.logger as typeof logger).log(logLevel, `${command} - By ${user} in ${guild}`, ...restArgs);
}

View File

@@ -1,21 +1,21 @@
import { inspect } from 'node:util'
import { createEmbeds } from '@discordeno/bot'
import { BUGS_ERRORS_REPORT_WEBHOOK } from '../../config.js'
import { bot } from '../bot.js'
import { webhookURLToIDAndToken } from '../utils/webhook.js'
import { inspect } from 'node:util';
import { createEmbeds } from '@discordeno/bot';
import { BUGS_ERRORS_REPORT_WEBHOOK } from '../../config.js';
import { bot } from '../bot.js';
import { webhookURLToIDAndToken } from '../utils/webhook.js';
process.on('unhandledRejection', async (error) => {
bot.logger.error('An unhandled rejection occurred', error)
bot.logger.error('An unhandled rejection occurred', error);
if (!BUGS_ERRORS_REPORT_WEBHOOK || !error) return
if (!BUGS_ERRORS_REPORT_WEBHOOK || !error) return;
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK)
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK);
if (!id || !token) return
if (!id || !token) return;
const inspectedError = inspect(error)
const inspectedError = inspect(error);
const embeds = createEmbeds().setDescription(`\`\`\`${inspectedError}\`\`\``).setFooter('Unhandled rejection occurred').setTimestamp(Date.now())
const embeds = createEmbeds().setDescription(`\`\`\`${inspectedError}\`\`\``).setFooter('Unhandled rejection occurred').setTimestamp(Date.now());
await bot.helpers.executeWebhook(id, token, { embeds })
})
await bot.helpers.executeWebhook(id, token, { embeds });
});

View File

@@ -1,17 +1,17 @@
import fastify, { type FastifyInstance } from 'fastify'
import { EVENT_HANDLER_AUTHORIZATION } from '../config.js'
import fastify, { type FastifyInstance } from 'fastify';
import { EVENT_HANDLER_AUTHORIZATION } from '../config.js';
export function buildFastifyApp(): FastifyInstance {
const app = fastify()
const app = fastify();
// Authorization check
app.addHook('onRequest', async (req, res) => {
if (req.headers.authorization !== EVENT_HANDLER_AUTHORIZATION) {
res.status(401).send({
message: 'Credentials not valid.',
})
});
}
})
});
return app
return app;
}

View File

@@ -1,6 +1,6 @@
import { join as joinPath } from 'node:path'
import type { DiscordGatewayPayload, GatewayDispatchEventNames } from '@discordeno/bot'
import { connect as connectAmqp } from 'amqplib'
import { join as joinPath } from 'node:path';
import type { DiscordGatewayPayload, GatewayDispatchEventNames } from '@discordeno/bot';
import { connect as connectAmqp } from 'amqplib';
import {
EVENT_HANDLER_HOST,
EVENT_HANDLER_PORT,
@@ -8,81 +8,81 @@ import {
MESSAGEQUEUE_PASSWORD,
MESSAGEQUEUE_URL,
MESSAGEQUEUE_USERNAME,
} from '../config.js'
import { getDirnameFromFileUrl } from '../util.js'
import { bot } from './bot.js'
import { buildFastifyApp } from './fastify.js'
import importDirectory from './utils/loader.js'
} from '../config.js';
import { getDirnameFromFileUrl } from '../util.js';
import { bot } from './bot.js';
import { buildFastifyApp } from './fastify.js';
import importDirectory from './utils/loader.js';
// The importDirectory function uses 'readdir' that requires either a relative path compared to the process CWD or an absolute one, so to get one relative we need to use import.meta.url
const currentDirectory = getDirnameFromFileUrl(import.meta.url)
const currentDirectory = getDirnameFromFileUrl(import.meta.url);
await importDirectory(joinPath(currentDirectory, './commands'))
await importDirectory(joinPath(currentDirectory, './events'))
await importDirectory(joinPath(currentDirectory, './commands'));
await importDirectory(joinPath(currentDirectory, './events'));
if (MESSAGEQUEUE_ENABLE) {
await connectToRabbitMQ()
await connectToRabbitMQ();
}
const app = buildFastifyApp()
const app = buildFastifyApp();
app.get('/timecheck', async (_req, res) => {
res.status(200).send({ message: Date.now() })
})
res.status(200).send({ message: Date.now() });
});
app.post('/', async (req, res) => {
const body = req.body as GatewayEvent
const body = req.body as GatewayEvent;
try {
handleGatewayEvent(body.payload, body.shardId)
handleGatewayEvent(body.payload, body.shardId);
res.status(200).send()
res.status(200).send();
} catch (error) {
bot.logger.error('There was an error handling the incoming gateway command', error)
res.status(500).send()
bot.logger.error('There was an error handling the incoming gateway command', error);
res.status(500).send();
}
})
});
await app.listen({
host: EVENT_HANDLER_HOST,
port: EVENT_HANDLER_PORT,
})
});
bot.logger.info(`Bot event handler is listening on port ${EVENT_HANDLER_PORT}`)
bot.logger.info(`Bot event handler is listening on port ${EVENT_HANDLER_PORT}`);
async function handleGatewayEvent(payload: DiscordGatewayPayload, shardId: number): Promise<void> {
bot.events.raw?.(payload, shardId)
bot.events.raw?.(payload, shardId);
// If we don't have the event type we don't process it further
if (!payload.t) return
if (!payload.t) return;
// Run the dispatch check
await bot.events.dispatchRequirements?.(payload, shardId)
await bot.events.dispatchRequirements?.(payload, shardId);
bot.handlers[payload.t as GatewayDispatchEventNames]?.(bot, payload, shardId)
bot.handlers[payload.t as GatewayDispatchEventNames]?.(bot, payload, shardId);
}
async function connectToRabbitMQ(): Promise<void> {
const connection = await connectAmqp(`amqp://${MESSAGEQUEUE_USERNAME}:${MESSAGEQUEUE_PASSWORD}@${MESSAGEQUEUE_URL}`).catch((error) => {
bot.logger.error('Failed to connect to RabbitMQ, retrying in 1s.', error)
setTimeout(connectToRabbitMQ, 1000)
})
bot.logger.error('Failed to connect to RabbitMQ, retrying in 1s.', error);
setTimeout(connectToRabbitMQ, 1000);
});
if (!connection) return
if (!connection) return;
connection.on('close', () => {
setTimeout(connectToRabbitMQ, 1000)
})
setTimeout(connectToRabbitMQ, 1000);
});
connection.on('error', (error) => {
bot.logger.error('There was an error in the connection with RabbitMQ, reconnecting in 1s.', error)
setTimeout(connectToRabbitMQ, 1000)
})
bot.logger.error('There was an error in the connection with RabbitMQ, reconnecting in 1s.', error);
setTimeout(connectToRabbitMQ, 1000);
});
const channel = await connection.createChannel().catch((error) => {
bot.logger.error('There was an error creating the RabbitMQ channel', error)
})
bot.logger.error('There was an error creating the RabbitMQ channel', error);
});
if (!channel) return
if (!channel) return;
const exchange = await channel
.assertExchange('gatewayMessage', 'x-message-deduplication', {
@@ -93,31 +93,31 @@ async function connectToRabbitMQ(): Promise<void> {
},
})
.catch((error) => {
bot.logger.error('There was an error asserting the exchange', error)
})
bot.logger.error('There was an error asserting the exchange', error);
});
if (!exchange) return
if (!exchange) return;
await channel.assertQueue('gatewayMessageQueue').catch(bot.logger.error)
await channel.bindQueue('gatewayMessageQueue', 'gatewayMessage', '').catch(bot.logger.error)
await channel.assertQueue('gatewayMessageQueue').catch(bot.logger.error);
await channel.bindQueue('gatewayMessageQueue', 'gatewayMessage', '').catch(bot.logger.error);
await channel
.consume('gatewayMessageQueue', async (message) => {
if (!message) return
if (!message) return;
try {
const messageBody = JSON.parse(message.content.toString()) as GatewayEvent
const messageBody = JSON.parse(message.content.toString()) as GatewayEvent;
await handleGatewayEvent(messageBody.payload, messageBody.shardId)
await handleGatewayEvent(messageBody.payload, messageBody.shardId);
channel.ack(message)
channel.ack(message);
} catch (error) {
bot.logger.error('There was an error handling events received from RabbitMQ', error)
bot.logger.error('There was an error handling events received from RabbitMQ', error);
}
})
.catch(bot.logger.error)
.catch(bot.logger.error);
}
interface GatewayEvent {
payload: DiscordGatewayPayload
shardId: number
payload: DiscordGatewayPayload;
shardId: number;
}

View File

@@ -1,19 +1,19 @@
import 'dotenv/config'
import 'dotenv/config';
import { join as joinPath } from 'node:path'
import { getDirnameFromFileUrl } from '../util.js'
import { bot } from './bot.js'
import importDirectory from './utils/loader.js'
import { updateCommands } from './utils/updateCommands.js'
import { join as joinPath } from 'node:path';
import { getDirnameFromFileUrl } from '../util.js';
import { bot } from './bot.js';
import importDirectory from './utils/loader.js';
import { updateCommands } from './utils/updateCommands.js';
// The importDirectory function uses 'readdir' that requires either a relative path compared to the process CWD or an absolute one, so to get one relative we need to use import.meta.url
const currentDirectory = getDirnameFromFileUrl(import.meta.url)
const currentDirectory = getDirnameFromFileUrl(import.meta.url);
await importDirectory(joinPath(currentDirectory, './commands'))
await importDirectory(joinPath(currentDirectory, './commands'));
await updateCommands()
await updateCommands();
bot.logger.info('Done!')
bot.logger.info('Done!');
// We need to manually exit as the REST Manager has timeouts that will keep NodeJS alive
process.exit()
process.exit();

View File

@@ -1,13 +1,13 @@
import { readdir } from 'node:fs/promises'
import { bot } from '../bot.js'
import { readdir } from 'node:fs/promises';
import { bot } from '../bot.js';
export default async function importDirectory(folder: string): Promise<void> {
const files = await readdir(folder, { recursive: true })
const files = await readdir(folder, { recursive: true });
for (const filename of files) {
if (!filename.endsWith('.js')) continue
if (!filename.endsWith('.js')) continue;
// Using `file://` to avoid weird issues with paths on Windows
await import(`file://${folder}/${filename}`).catch((x) => bot.logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x))
await import(`file://${folder}/${filename}`).catch((x) => bot.logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x));
}
}

View File

@@ -1,19 +1,19 @@
import assert from 'node:assert'
import { DEV_SERVER_ID, DEVELOPMENT } from '../../config.js'
import { bot } from '../bot.js'
import assert from 'node:assert';
import { DEV_SERVER_ID, DEVELOPMENT } from '../../config.js';
import { bot } from '../bot.js';
export async function updateCommands(): Promise<void> {
bot.logger.info('Updating commands')
bot.logger.info('Updating commands');
const userCommands = bot.commands.filter((x) => !x.devOnly).array()
await bot.helpers.upsertGlobalApplicationCommands(userCommands)
const userCommands = bot.commands.filter((x) => !x.devOnly).array();
await bot.helpers.upsertGlobalApplicationCommands(userCommands);
if (DEVELOPMENT) {
assert(DEV_SERVER_ID, 'The DEV_SERVER_ID environment is missing')
assert(DEV_SERVER_ID, 'The DEV_SERVER_ID environment is missing');
bot.logger.info('Updating developer commands')
bot.logger.info('Updating developer commands');
const devCommands = bot.commands.filter((x) => x.devOnly ?? false).array()
await bot.helpers.upsertGuildApplicationCommands(DEV_SERVER_ID, devCommands)
const devCommands = bot.commands.filter((x) => x.devOnly ?? false).array();
await bot.helpers.upsertGuildApplicationCommands(DEV_SERVER_ID, devCommands);
}
}

View File

@@ -1,9 +1,9 @@
/** Get the webhook id and token from a webhook url. */
export function webhookURLToIDAndToken(url: string): { id?: string; token?: string } {
const [id, token] = url.substring(url.indexOf('webhooks/') + 9).split('/')
const [id, token] = url.substring(url.indexOf('webhooks/') + 9).split('/');
return {
id,
token,
}
};
}

View File

@@ -1,78 +1,78 @@
import 'dotenv/config'
import 'dotenv/config';
import { Intents } from '@discordeno/bot'
import { Intents } from '@discordeno/bot';
// #region Mapping of environment variables to javascript variables with some minimal parsing
// General Configurations
export const DEVELOPMENT = process.env.DEVELOPMENT === 'true'
export const DEV_SERVER_ID = process.env.DEV_SERVER_ID
export const DISCORD_TOKEN = assertEnv('DISCORD_TOKEN')
export const DEVELOPMENT = process.env.DEVELOPMENT === 'true';
export const DEV_SERVER_ID = process.env.DEV_SERVER_ID;
export const DISCORD_TOKEN = assertEnv('DISCORD_TOKEN');
// Bot Configuration
export const EVENT_HANDLER_AUTHORIZATION = assertEnv('EVENT_HANDLER_AUTHORIZATION')
export const EVENT_HANDLER_AUTHORIZATION = assertEnv('EVENT_HANDLER_AUTHORIZATION');
export const EVENT_HANDLER_HOST = assertEnv('EVENT_HANDLER_HOST')
export const EVENT_HANDLER_PORT = parseNumber(assertEnv('EVENT_HANDLER_PORT'), 'EVENT_HANDLER_PORT')
export const EVENT_HANDLER_HOST = assertEnv('EVENT_HANDLER_HOST');
export const EVENT_HANDLER_PORT = parseNumber(assertEnv('EVENT_HANDLER_PORT'), 'EVENT_HANDLER_PORT');
export const BUGS_ERRORS_REPORT_WEBHOOK = process.env.BUGS_ERRORS_REPORT_WEBHOOK
export const BUGS_ERRORS_REPORT_WEBHOOK = process.env.BUGS_ERRORS_REPORT_WEBHOOK;
// Rest Proxy Configurations
export const REST_AUTHORIZATION = assertEnv('REST_AUTHORIZATION')
export const REST_HOST = assertEnv('REST_HOST')
export const REST_PORT = parseNumber(assertEnv('REST_PORT'), 'REST_PORT')
export const REST_AUTHORIZATION = assertEnv('REST_AUTHORIZATION');
export const REST_HOST = assertEnv('REST_HOST');
export const REST_PORT = parseNumber(assertEnv('REST_PORT'), 'REST_PORT');
// Gateway Proxy Configurations
export const TOTAL_SHARDS = process.env.TOTAL_SHARDS ? parseNumber(process.env.TOTAL_SHARDS, 'TOTAL_SHARDS') : undefined
export const SHARDS_PER_WORKER = parseNumber(process.env.SHARDS_PER_WORKER ?? '16', 'SHARDS_PER_WORKER')
export const TOTAL_WORKERS = parseNumber(process.env.TOTAL_WORKERS ?? '4', 'TOTAL_WORKERS')
export const TOTAL_SHARDS = process.env.TOTAL_SHARDS ? parseNumber(process.env.TOTAL_SHARDS, 'TOTAL_SHARDS') : undefined;
export const SHARDS_PER_WORKER = parseNumber(process.env.SHARDS_PER_WORKER ?? '16', 'SHARDS_PER_WORKER');
export const TOTAL_WORKERS = parseNumber(process.env.TOTAL_WORKERS ?? '4', 'TOTAL_WORKERS');
export const GATEWAY_AUTHORIZATION = assertEnv('GATEWAY_AUTHORIZATION')
export const GATEWAY_HOST = assertEnv('GATEWAY_HOST')
export const GATEWAY_PORT = parseNumber(assertEnv('GATEWAY_PORT'), 'GATEWAY_PORT')
export const GATEWAY_AUTHORIZATION = assertEnv('GATEWAY_AUTHORIZATION');
export const GATEWAY_HOST = assertEnv('GATEWAY_HOST');
export const GATEWAY_PORT = parseNumber(assertEnv('GATEWAY_PORT'), 'GATEWAY_PORT');
// Message queue (RabbitMQ configuration)
export const MESSAGEQUEUE_ENABLE = process.env.MESSAGEQUEUE_ENABLE === 'true'
export const MESSAGEQUEUE_ENABLE = process.env.MESSAGEQUEUE_ENABLE === 'true';
export const MESSAGEQUEUE_URL = process.env.MESSAGEQUEUE_URL
export const MESSAGEQUEUE_USERNAME = process.env.MESSAGEQUEUE_USERNAME
export const MESSAGEQUEUE_PASSWORD = process.env.MESSAGEQUEUE_PASSWORD
export const MESSAGEQUEUE_URL = process.env.MESSAGEQUEUE_URL;
export const MESSAGEQUEUE_USERNAME = process.env.MESSAGEQUEUE_USERNAME;
export const MESSAGEQUEUE_PASSWORD = process.env.MESSAGEQUEUE_PASSWORD;
// Analytics (InfluxDB configuration)
export const INFLUX_ORG = process.env.INFLUX_ORG
export const INFLUX_BUCKET = process.env.INFLUX_BUCKET
export const INFLUX_TOKEN = process.env.INFLUX_TOKEN
export const INFLUX_URL = process.env.INFLUX_URL
export const INFLUX_ORG = process.env.INFLUX_ORG;
export const INFLUX_BUCKET = process.env.INFLUX_BUCKET;
export const INFLUX_TOKEN = process.env.INFLUX_TOKEN;
export const INFLUX_URL = process.env.INFLUX_URL;
export const INFLUX_ENABLED = INFLUX_URL && INFLUX_TOKEN && INFLUX_ORG && INFLUX_BUCKET
export const INFLUX_ENABLED = INFLUX_URL && INFLUX_TOKEN && INFLUX_ORG && INFLUX_BUCKET;
// #endregion
export const EVENT_HANDLER_URL = `http://${EVENT_HANDLER_HOST}:${EVENT_HANDLER_PORT}`
export const REST_URL = `http://${REST_HOST}:${REST_PORT}`
export const GATEWAY_URL = `http://${GATEWAY_HOST}:${GATEWAY_PORT}`
export const EVENT_HANDLER_URL = `http://${EVENT_HANDLER_HOST}:${EVENT_HANDLER_PORT}`;
export const REST_URL = `http://${REST_HOST}:${REST_PORT}`;
export const GATEWAY_URL = `http://${GATEWAY_HOST}:${GATEWAY_PORT}`;
// TEMPLATE-SETUP: Add/Remove the intents you need/don't need
export const GATEWAY_INTENTS = Intents.Guilds | Intents.GuildMessages
export const GATEWAY_INTENTS = Intents.Guilds | Intents.GuildMessages;
// Helper functions
function assertEnv(env: string): string {
if (process.env[env]) return process.env[env]
if (process.env[env]) return process.env[env];
throw new TypeError(`The '${env}' environment variable must be set`)
throw new TypeError(`The '${env}' environment variable must be set`);
}
function parseNumber(envValue: string, env: string): number {
const parsed = Number.parseInt(envValue)
const parsed = Number.parseInt(envValue);
if (Number.isFinite(parsed)) return parsed
if (Number.isFinite(parsed)) return parsed;
throw new TypeError(`The '${env}' environment variable must be a number`)
throw new TypeError(`The '${env}' environment variable must be a number`);
}

View File

@@ -1,17 +1,17 @@
import fastify, { type FastifyInstance } from 'fastify'
import { GATEWAY_AUTHORIZATION } from '../config.js'
import fastify, { type FastifyInstance } from 'fastify';
import { GATEWAY_AUTHORIZATION } from '../config.js';
export function buildFastifyApp(): FastifyInstance {
const app = fastify()
const app = fastify();
// Authorization check
app.addHook('onRequest', async (req, res) => {
if (req.headers.authorization !== GATEWAY_AUTHORIZATION) {
res.status(401).send({
message: 'Credentials not valid.',
})
});
}
})
});
return app
return app;
}

View File

@@ -1,12 +1,12 @@
import type { Worker } from 'node:worker_threads'
import { createGatewayManager, createLogger, createRestManager } from '@discordeno/bot'
import { DISCORD_TOKEN, GATEWAY_INTENTS, REST_AUTHORIZATION, REST_URL, SHARDS_PER_WORKER, TOTAL_SHARDS, TOTAL_WORKERS } from '../config.js'
import { promiseWithResolvers } from '../util.js'
import { createWorker } from './worker/createWorker.js'
import type { ManagerMessage, WorkerMessage } from './worker/types.js'
import type { Worker } from 'node:worker_threads';
import { createGatewayManager, createLogger, createRestManager } from '@discordeno/bot';
import { DISCORD_TOKEN, GATEWAY_INTENTS, REST_AUTHORIZATION, REST_URL, SHARDS_PER_WORKER, TOTAL_SHARDS, TOTAL_WORKERS } from '../config.js';
import { promiseWithResolvers } from '../util.js';
import { createWorker } from './worker/createWorker.js';
import type { ManagerMessage, WorkerMessage } from './worker/types.js';
export const workers = new Map<number, Worker>()
export const logger = createLogger({ name: 'GATEWAY' })
export const workers = new Map<number, Worker>();
export const logger = createLogger({ name: 'GATEWAY' });
const restManager = createRestManager({
token: DISCORD_TOKEN,
@@ -14,9 +14,9 @@ const restManager = createRestManager({
baseUrl: REST_URL,
authorization: REST_AUTHORIZATION,
},
})
});
const gatewayBotConfig = await restManager.getGatewayBot()
const gatewayBotConfig = await restManager.getGatewayBot();
const gatewayManager = createGatewayManager({
token: DISCORD_TOKEN,
@@ -28,96 +28,96 @@ const gatewayManager = createGatewayManager({
resharding: {
getSessionInfo: restManager.getGatewayBot,
},
})
});
gatewayManager.resharding.tellWorkerToPrepare = async (workerId, shardId, bucketId) => {
logger.info(`Tell worker to prepare, workerId: ${workerId}, shardId: ${shardId}, bucketId: ${bucketId}`)
logger.info(`Tell worker to prepare, workerId: ${workerId}, shardId: ${shardId}, bucketId: ${bucketId}`);
let worker = workers.get(workerId)
let worker = workers.get(workerId);
if (!worker) {
worker = createWorker(workerId)
workers.set(workerId, worker)
worker = createWorker(workerId);
workers.set(workerId, worker);
}
worker.postMessage({
type: 'PrepareShard',
shardId,
totalShards: gatewayManager.totalShards,
} satisfies WorkerMessage)
} satisfies WorkerMessage);
const { promise, resolve } = promiseWithResolvers<void>()
const { promise, resolve } = promiseWithResolvers<void>();
const waitForShardPrepared = (message: ManagerMessage) => {
if (message.type === 'ShardPrepared' && message.shardId === shardId) {
resolve()
resolve();
}
}
};
worker.on('message', waitForShardPrepared)
worker.on('message', waitForShardPrepared);
await promise
await promise;
worker.off('message', waitForShardPrepared)
}
worker.off('message', waitForShardPrepared);
};
gatewayManager.resharding.onReshardingSwitch = async () => {
logger.info('Resharding switch triggered, telling workers to switch the shards')
logger.info('Resharding switch triggered, telling workers to switch the shards');
for (const worker of workers.values()) {
worker.postMessage({
type: 'SwitchShards',
} satisfies WorkerMessage)
} satisfies WorkerMessage);
}
}
};
gatewayManager.tellWorkerToIdentify = async (workerId, shardId, bucketId) => {
logger.info(`Tell worker to identify, workerId: ${workerId}, shardId: ${shardId}, bucketId: ${bucketId}`)
logger.info(`Tell worker to identify, workerId: ${workerId}, shardId: ${shardId}, bucketId: ${bucketId}`);
const worker = workers.get(workerId) ?? createWorker(workerId)
workers.set(workerId, worker)
const worker = workers.get(workerId) ?? createWorker(workerId);
workers.set(workerId, worker);
worker.postMessage({
type: 'IdentifyShard',
shardId,
} satisfies WorkerMessage)
} satisfies WorkerMessage);
const { promise, resolve } = promiseWithResolvers<void>()
const { promise, resolve } = promiseWithResolvers<void>();
const waitForShardIdentified = (message: ManagerMessage) => {
if (message.type === 'ShardIdentified' && message.shardId === shardId) {
resolve()
resolve();
}
}
};
worker.on('message', waitForShardIdentified)
worker.on('message', waitForShardIdentified);
await promise
await promise;
worker.off('message', waitForShardIdentified)
}
worker.off('message', waitForShardIdentified);
};
gatewayManager.sendPayload = async (shardId, payload) => {
const workerId = gatewayManager.calculateWorkerId(shardId)
const worker = workers.get(workerId)
const workerId = gatewayManager.calculateWorkerId(shardId);
const worker = workers.get(workerId);
if (!worker) return
if (!worker) return;
worker.postMessage({
type: 'ShardPayload',
shardId,
payload,
} satisfies WorkerMessage)
}
} satisfies WorkerMessage);
};
gatewayManager.editBotStatus = async (payload) => {
const workersArray = Array.from(workers.values())
const workersArray = Array.from(workers.values());
for (const worker of workersArray) {
worker.postMessage({
type: 'EditShardsPresence',
payload,
} satisfies WorkerMessage)
} satisfies WorkerMessage);
}
}
};
export default gatewayManager
export default gatewayManager;

View File

@@ -1,72 +1,72 @@
import { GATEWAY_HOST, GATEWAY_PORT } from '../config.js'
import { promiseWithResolvers } from '../util.js'
import { buildFastifyApp } from './fastify.js'
import gatewayManager, { logger, workers } from './gatewayManager.js'
import { shardInfoRequests } from './worker/createWorker.js'
import type { ManagerGetShardInfoFromGuildId, ShardInfo, WorkerMessage, WorkerPresencesUpdate, WorkerShardPayload } from './worker/types.js'
import { GATEWAY_HOST, GATEWAY_PORT } from '../config.js';
import { promiseWithResolvers } from '../util.js';
import { buildFastifyApp } from './fastify.js';
import gatewayManager, { logger, workers } from './gatewayManager.js';
import { shardInfoRequests } from './worker/createWorker.js';
import type { ManagerGetShardInfoFromGuildId, ShardInfo, WorkerMessage, WorkerPresencesUpdate, WorkerShardPayload } from './worker/types.js';
const app = buildFastifyApp()
const app = buildFastifyApp();
app.get('/timecheck', (_req, res) => {
res.status(200).send({ message: Date.now() })
})
res.status(200).send({ message: Date.now() });
});
app.post('/', async (req, res) => {
if (!req.body) {
res.status(400).send({ message: 'Invalid body' })
return
res.status(400).send({ message: 'Invalid body' });
return;
}
const data = req.body as WorkerShardPayload | WorkerPresencesUpdate | ManagerGetShardInfoFromGuildId
const data = req.body as WorkerShardPayload | WorkerPresencesUpdate | ManagerGetShardInfoFromGuildId;
if (data.type === 'ShardPayload') {
await gatewayManager.sendPayload(data.shardId, data.payload)
return
await gatewayManager.sendPayload(data.shardId, data.payload);
return;
}
if (data.type === 'EditShardsPresence') {
await gatewayManager.editBotStatus(data.payload)
return
await gatewayManager.editBotStatus(data.payload);
return;
}
if (data.type === 'ShardInfoFromGuild') {
// If we don't have a guildId, we use shard 0
const shardId = data.guildId ? gatewayManager.calculateShardId(data.guildId) : 0
const workerId = gatewayManager.calculateWorkerId(shardId)
const worker = workers.get(workerId)
const shardId = data.guildId ? gatewayManager.calculateShardId(data.guildId) : 0;
const workerId = gatewayManager.calculateWorkerId(shardId);
const worker = workers.get(workerId);
if (!worker) {
await res.status(400).send({ error: `worker for shard ${shardId} not found` })
return
await res.status(400).send({ error: `worker for shard ${shardId} not found` });
return;
}
const nonce = crypto.randomUUID()
const nonce = crypto.randomUUID();
const { promise, resolve } = promiseWithResolvers<ShardInfo>()
const { promise, resolve } = promiseWithResolvers<ShardInfo>();
shardInfoRequests.set(nonce, resolve)
shardInfoRequests.set(nonce, resolve);
worker.postMessage({
type: 'GetShardInfo',
shardId,
nonce,
} satisfies WorkerMessage)
} satisfies WorkerMessage);
const shardInfo = await promise
const shardInfo = await promise;
await res.status(200).send({
shardId: shardInfo.shardId,
rtt: shardInfo.rtt,
} satisfies Omit<ShardInfo, 'nonce'>)
return
} satisfies Omit<ShardInfo, 'nonce'>);
return;
}
logger.warn(`Manager - Received unknown data type: ${(data as { type: string }).type}`)
})
logger.warn(`Manager - Received unknown data type: ${(data as { type: string }).type}`);
});
await app.listen({
host: GATEWAY_HOST,
port: GATEWAY_PORT,
})
});
logger.info(`Gateway manager listening on port ${GATEWAY_PORT}`)
logger.info(`Gateway manager listening on port ${GATEWAY_PORT}`);
await gatewayManager.spawnShards()
await gatewayManager.spawnShards();

View File

@@ -1,5 +1,5 @@
import { join as joinPath } from 'node:path'
import { Worker } from 'node:worker_threads'
import { join as joinPath } from 'node:path';
import { Worker } from 'node:worker_threads';
import {
DISCORD_TOKEN,
EVENT_HANDLER_AUTHORIZATION,
@@ -9,18 +9,18 @@ import {
MESSAGEQUEUE_PASSWORD,
MESSAGEQUEUE_URL,
MESSAGEQUEUE_USERNAME,
} from '../../config.js'
import { getDirnameFromFileUrl } from '../../util.js'
import gatewayManager, { logger } from '../gatewayManager.js'
import type { ManagerMessage, ShardInfo, WorkerCreateData, WorkerMessage } from './types.js'
} from '../../config.js';
import { getDirnameFromFileUrl } from '../../util.js';
import gatewayManager, { logger } from '../gatewayManager.js';
import type { ManagerMessage, ShardInfo, WorkerCreateData, WorkerMessage } from './types.js';
// the string is the nonce of the request
export const shardInfoRequests = new Map<string, (value: ShardInfo) => void>()
export const shardInfoRequests = new Map<string, (value: ShardInfo) => void>();
export function createWorker(workerId: number): Worker {
// the Worker constructor requires either a relative path compared to the process CWD or an absolute one, so to get one relative we need to use import.meta.url
const currentFolder = getDirnameFromFileUrl(import.meta.url)
const workerFilePath = joinPath(currentFolder, './worker.js')
const currentFolder = getDirnameFromFileUrl(import.meta.url);
const workerFilePath = joinPath(currentFolder, './worker.js');
const worker = new Worker(workerFilePath, {
workerData: {
@@ -43,32 +43,32 @@ export function createWorker(workerId: number): Worker {
url: MESSAGEQUEUE_URL,
},
} satisfies WorkerCreateData,
})
});
worker.on('message', async (message: ManagerMessage) => {
if (message.type === 'RequestIdentify') {
logger.info(`Requesting identify for shardId: #${message.shardId}`)
await gatewayManager.requestIdentify(message.shardId)
logger.info(`Requesting identify for shardId: #${message.shardId}`);
await gatewayManager.requestIdentify(message.shardId);
worker.postMessage({
type: 'AllowIdentify',
shardId: message.shardId,
} satisfies WorkerMessage)
} satisfies WorkerMessage);
return
return;
}
if (message.type === 'ShardInfo') {
shardInfoRequests.get(message.nonce)?.(message)
shardInfoRequests.delete(message.nonce)
return
shardInfoRequests.get(message.nonce)?.(message);
shardInfoRequests.delete(message.nonce);
return;
}
if (message.type === 'ShardIdentified') {
logger.info(`Shard #${message.shardId} identified`)
return
logger.info(`Shard #${message.shardId} identified`);
return;
}
logger.warn(`Worker - Received unknown message type: ${(message as { type: string }).type}`)
})
logger.warn(`Worker - Received unknown message type: ${(message as { type: string }).type}`);
});
return worker
return worker;
}

View File

@@ -1,6 +1,6 @@
import type { DiscordUpdatePresence, ShardSocketRequest } from '@discordeno/bot'
import type { DiscordUpdatePresence, ShardSocketRequest } from '@discordeno/bot';
export type ManagerMessage = ManagerRequestIdentify | ManagerShardIdentified | ManagerShardPrepared | ManagerShardInfo
export type ManagerMessage = ManagerRequestIdentify | ManagerShardIdentified | ManagerShardPrepared | ManagerShardInfo;
export type WorkerMessage =
| WorkerIdentifyShard
| WorkerPrepareShard
@@ -8,93 +8,93 @@ export type WorkerMessage =
| WorkerAllowIdentify
| WorkerShardPayload
| WorkerPresencesUpdate
| WorkerShardInfo
| WorkerShardInfo;
export interface WorkerIdentifyShard {
type: 'IdentifyShard'
shardId: number
type: 'IdentifyShard';
shardId: number;
}
export interface WorkerPrepareShard {
type: 'PrepareShard'
shardId: number
totalShards: number
type: 'PrepareShard';
shardId: number;
totalShards: number;
}
export interface WorkerSwitchShards {
type: 'SwitchShards'
type: 'SwitchShards';
}
export interface WorkerAllowIdentify {
type: 'AllowIdentify'
shardId: number
type: 'AllowIdentify';
shardId: number;
}
export interface ManagerRequestIdentify {
type: 'RequestIdentify'
shardId: number
type: 'RequestIdentify';
shardId: number;
}
export interface WorkerShardPayload {
type: 'ShardPayload'
shardId: number
payload: ShardSocketRequest
type: 'ShardPayload';
shardId: number;
payload: ShardSocketRequest;
}
export interface WorkerPresencesUpdate {
type: 'EditShardsPresence'
payload: DiscordUpdatePresence
type: 'EditShardsPresence';
payload: DiscordUpdatePresence;
}
export interface WorkerShardInfo {
type: 'GetShardInfo'
shardId: number
nonce: string
type: 'GetShardInfo';
shardId: number;
nonce: string;
}
export interface WorkerCreateData {
connectionData: {
intents: number
token: string
url: string
version: number
totalShards: number
}
intents: number;
token: string;
url: string;
version: number;
totalShards: number;
};
eventHandler: {
urls: string[]
authentication: string
}
workerId: number
urls: string[];
authentication: string;
};
workerId: number;
messageQueue: {
enabled: boolean
username?: string
password?: string
url?: string
}
enabled: boolean;
username?: string;
password?: string;
url?: string;
};
}
export interface ShardInfo {
shardId: number
rtt: number
shardId: number;
rtt: number;
// the nonce is to bind to the request
nonce: string
nonce: string;
}
export interface ManagerShardInfo extends ShardInfo {
type: 'ShardInfo'
type: 'ShardInfo';
}
export interface ManagerGetShardInfoFromGuildId {
type: 'ShardInfoFromGuild'
guildId: string | undefined
type: 'ShardInfoFromGuild';
guildId: string | undefined;
}
export interface ManagerShardIdentified {
type: 'ShardIdentified'
shardId: number
type: 'ShardIdentified';
shardId: number;
}
export interface ManagerShardPrepared {
type: 'ShardPrepared'
shardId: number
type: 'ShardPrepared';
shardId: number;
}

View File

@@ -1,125 +1,125 @@
import assert from 'node:assert'
import { createHash } from 'node:crypto'
import { workerData as _workerData, parentPort } from 'node:worker_threads'
import { type Camelize, createLogger, DiscordenoShard, type DiscordGatewayPayload, GatewayOpcodes, ShardSocketCloseCodes } from '@discordeno/bot'
import { type Channel as amqpChannel, connect as connectAmqp } from 'amqplib'
import { promiseWithResolvers } from '../../util.js'
import type { ManagerMessage, WorkerCreateData, WorkerMessage } from './types.js'
import assert from 'node:assert';
import { createHash } from 'node:crypto';
import { workerData as _workerData, parentPort } from 'node:worker_threads';
import { type Camelize, createLogger, DiscordenoShard, type DiscordGatewayPayload, GatewayOpcodes, ShardSocketCloseCodes } from '@discordeno/bot';
import { type Channel as amqpChannel, connect as connectAmqp } from 'amqplib';
import { promiseWithResolvers } from '../../util.js';
import type { ManagerMessage, WorkerCreateData, WorkerMessage } from './types.js';
assert(parentPort)
assert(parentPort);
const workerData: WorkerCreateData = _workerData
const workerData: WorkerCreateData = _workerData;
const logger = createLogger({ name: `Worker #${workerData.workerId}` })
const logger = createLogger({ name: `Worker #${workerData.workerId}` });
const identifyPromises = new Map<number, () => void>()
const shards = new Map<number, DiscordenoShard>()
const pendingShards = new Map<number, DiscordenoShard>()
const identifyPromises = new Map<number, () => void>();
const shards = new Map<number, DiscordenoShard>();
const pendingShards = new Map<number, DiscordenoShard>();
let totalShards = workerData.connectionData.totalShards
let totalShards = workerData.connectionData.totalShards;
let rabbitMQChannel: amqpChannel | undefined
let rabbitMQChannel: amqpChannel | undefined;
if (workerData.messageQueue.enabled) {
await connectToRabbitMQ()
await connectToRabbitMQ();
}
parentPort.on('message', async (message: WorkerMessage) => {
assert(parentPort)
assert(parentPort);
if (message.type === 'IdentifyShard') {
logger.info(`Starting to identify shard #${message.shardId}`)
const shard = shards.get(message.shardId) ?? createShard(message.shardId)
shards.set(message.shardId, shard)
logger.info(`Starting to identify shard #${message.shardId}`);
const shard = shards.get(message.shardId) ?? createShard(message.shardId);
shards.set(message.shardId, shard);
await shard.identify()
await shard.identify();
parentPort.postMessage({
type: 'ShardIdentified',
shardId: message.shardId,
} satisfies ManagerMessage)
} satisfies ManagerMessage);
return
return;
}
if (message.type === 'PrepareShard') {
logger.info(`Preparing shard #${message.shardId}`)
totalShards = message.totalShards
let shard = pendingShards.get(message.shardId)
logger.info(`Preparing shard #${message.shardId}`);
totalShards = message.totalShards;
let shard = pendingShards.get(message.shardId);
if (!shard) {
shard = createShard(message.shardId)
pendingShards.set(message.shardId, shard)
shard = createShard(message.shardId);
pendingShards.set(message.shardId, shard);
}
// Ignore the events
// TODO: If you need 'gateway.resharding.updateGuildsShardId' it you can listen to only the ready event and use the data from that event for the function call
shard.events.message = () => {}
shard.events.message = () => {};
await shard.identify()
await shard.identify();
parentPort.postMessage({
type: 'ShardPrepared',
shardId: message.shardId,
} satisfies ManagerMessage)
} satisfies ManagerMessage);
return
return;
}
if (message.type === 'SwitchShards') {
logger.info('Switching shards')
logger.info('Switching shards');
// Change the message event for all shards
for (const shard of pendingShards.values()) {
shard.events.message = handleShardMessageEvent
shard.events.message = handleShardMessageEvent;
}
// Old shards stop processing events
for (const shard of shards.values()) {
const oldHandler = shard.events.message
const oldHandler = shard.events.message;
shard.events.message = async function (_, message) {
// Member checks need to continue but others can stop
if (message.t === 'GUILD_MEMBERS_CHUNK') {
oldHandler?.(shard, message)
oldHandler?.(shard, message);
}
}
};
}
// Shutdown the old shards
const shardsToShutdown = Array.from(shards.values())
const shardsToShutdown = Array.from(shards.values());
// Move the pending shards to the active shards
shards.clear()
shards.clear();
for (const [shardId, shard] of pendingShards.entries()) {
shards.set(shardId, shard)
pendingShards.delete(shardId)
shards.set(shardId, shard);
pendingShards.delete(shardId);
}
// Shutdown the old shards
const promises = shardsToShutdown.map(async (shard) => {
await shard.close(ShardSocketCloseCodes.Resharded, 'Shard is being resharded')
logger.info(`Shard #${shard.id} has been shutdown`)
})
await shard.close(ShardSocketCloseCodes.Resharded, 'Shard is being resharded');
logger.info(`Shard #${shard.id} has been shutdown`);
});
await Promise.all(promises)
await Promise.all(promises);
return
return;
}
if (message.type === 'AllowIdentify') {
identifyPromises.get(message.shardId)?.()
identifyPromises.delete(message.shardId)
identifyPromises.get(message.shardId)?.();
identifyPromises.delete(message.shardId);
return
return;
}
if (message.type === 'ShardPayload') {
const shard = shards.get(message.shardId)
const shard = shards.get(message.shardId);
if (!shard) return
if (!shard) return;
await shard.send(message.payload)
await shard.send(message.payload);
return
return;
}
if (message.type === 'EditShardsPresence') {
const shardsArray = Array.from(shards.values())
const shardsArray = Array.from(shards.values());
const promises = shardsArray.map(async (shard) => {
await shard.send({
op: GatewayOpcodes.PresenceUpdate,
@@ -129,11 +129,11 @@ parentPort.on('message', async (message: WorkerMessage) => {
activities: message.payload.activities,
status: message.payload.status,
},
})
})
});
});
await Promise.all(promises)
return
await Promise.all(promises);
return;
}
if (message.type === 'GetShardInfo') {
const status = {
@@ -141,15 +141,15 @@ parentPort.on('message', async (message: WorkerMessage) => {
shardId: message.shardId,
rtt: shards.get(message.shardId)?.heart.rtt ?? -1,
nonce: message.nonce,
} satisfies ManagerMessage
} satisfies ManagerMessage;
parentPort?.postMessage(status)
parentPort?.postMessage(status);
return
return;
}
logger.warn(`Received unknown message type: ${(message as { type: string }).type}`)
})
logger.warn(`Received unknown message type: ${(message as { type: string }).type}`);
});
function createShard(shardId: number): DiscordenoShard {
const shard = new DiscordenoShard({
@@ -169,62 +169,62 @@ function createShard(shardId: number): DiscordenoShard {
version: workerData.connectionData.version,
transportCompression: null,
},
})
});
shard.requestIdentify = async () => {
assert(parentPort)
assert(parentPort);
const { promise, resolve } = promiseWithResolvers<void>()
const { promise, resolve } = promiseWithResolvers<void>();
parentPort.postMessage({
type: 'RequestIdentify',
shardId,
} satisfies ManagerMessage)
} satisfies ManagerMessage);
identifyPromises.set(shardId, resolve)
identifyPromises.set(shardId, resolve);
return await promise
}
return await promise;
};
// We do not want to camelize the packet, so we need to override the function as the default behavior is to camelize
shard.forwardToBot = (packet) => {
shard.events.message?.(shard, packet)
}
shard.events.message?.(shard, packet);
};
shard.events.message = handleShardMessageEvent
shard.events.message = handleShardMessageEvent;
return shard
return shard;
}
async function handleShardMessageEvent(shard: DiscordenoShard, payload: Camelize<DiscordGatewayPayload>) {
const body = JSON.stringify({ payload, shardId: shard.id })
const body = JSON.stringify({ payload, shardId: shard.id });
if (workerData.messageQueue.enabled) {
if (!rabbitMQChannel) {
logger.error('The RabbitMQ channel has not been created. The event will be lost')
return
logger.error('The RabbitMQ channel has not been created. The event will be lost');
return;
}
const message = Buffer.from(body)
const discordData = JSON.stringify(payload.d)
const message = Buffer.from(body);
const discordData = JSON.stringify(payload.d);
const deduplicationHash = createHash('sha1')
deduplicationHash.update(discordData)
const deduplicationHash = createHash('sha1');
deduplicationHash.update(discordData);
rabbitMQChannel.publish('gatewayMessage', '', message, {
contentType: 'application/json',
headers: {
'x-deduplication-header': deduplicationHash.digest('hex'),
},
})
});
return
return;
}
const url = workerData.eventHandler.urls[shard.id % workerData.eventHandler.urls.length]
const url = workerData.eventHandler.urls[shard.id % workerData.eventHandler.urls.length];
if (!url) {
logger.error('No url found to send events to')
return
logger.error('No url found to send events to');
return;
}
await fetch(url, {
@@ -234,35 +234,35 @@ async function handleShardMessageEvent(shard: DiscordenoShard, payload: Camelize
'Content-Type': 'application/json',
Authorization: workerData.eventHandler.authentication,
},
}).catch((error) => logger.error('Failed to send events to the bot code', error))
}).catch((error) => logger.error('Failed to send events to the bot code', error));
}
async function connectToRabbitMQ(): Promise<void> {
rabbitMQChannel = undefined
const messageQueue = workerData.messageQueue
rabbitMQChannel = undefined;
const messageQueue = workerData.messageQueue;
const connection = await connectAmqp(`amqp://${messageQueue.username}:${messageQueue.password}@${messageQueue.url}`).catch((error) => {
logger.error('Failed to connect to RabbitMQ, retrying in 1s.', error)
setTimeout(connectToRabbitMQ, 1000)
})
logger.error('Failed to connect to RabbitMQ, retrying in 1s.', error);
setTimeout(connectToRabbitMQ, 1000);
});
if (!connection) return
if (!connection) return;
connection.on('close', () => {
rabbitMQChannel = undefined
setTimeout(connectToRabbitMQ, 1000)
})
rabbitMQChannel = undefined;
setTimeout(connectToRabbitMQ, 1000);
});
connection.on('error', (error) => {
rabbitMQChannel = undefined
logger.error('There was an error in the connection with RabbitMQ, reconnecting in 1s.', error)
setTimeout(connectToRabbitMQ, 1000)
})
rabbitMQChannel = undefined;
logger.error('There was an error in the connection with RabbitMQ, reconnecting in 1s.', error);
setTimeout(connectToRabbitMQ, 1000);
});
const channel = await connection.createChannel().catch((error) => {
logger.error('There was an error creating the RabbitMQ channel', error)
})
logger.error('There was an error creating the RabbitMQ channel', error);
});
if (!channel) return
if (!channel) return;
const exchange = await channel
.assertExchange('gatewayMessage', 'x-message-deduplication', {
@@ -273,10 +273,10 @@ async function connectToRabbitMQ(): Promise<void> {
},
})
.catch((error) => {
logger.error('There was an error asserting the exchange', error)
})
logger.error('There was an error asserting the exchange', error);
});
if (!exchange) return
if (!exchange) return;
rabbitMQChannel = channel
rabbitMQChannel = channel;
}

View File

@@ -1,39 +1,39 @@
import fastifyMultipart, { type MultipartFile, type MultipartValue } from '@fastify/multipart'
import fastify, { type FastifyInstance } from 'fastify'
import { REST_AUTHORIZATION } from '../config.js'
import fastifyMultipart, { type MultipartFile, type MultipartValue } from '@fastify/multipart';
import fastify, { type FastifyInstance } from 'fastify';
import { REST_AUTHORIZATION } from '../config.js';
export function buildFastifyApp(): FastifyInstance {
const app = fastify()
const app = fastify();
app.register(fastifyMultipart, { attachFieldsToBody: true })
app.register(fastifyMultipart, { attachFieldsToBody: true });
// Authorization check
app.addHook('onRequest', async (req, res) => {
if (req.headers.authorization !== REST_AUTHORIZATION) {
res.status(401).send({
message: 'Credentials not valid.',
})
});
}
})
});
return app
return app;
}
export async function parseMultiformBody(body: unknown): Promise<FormData> {
const form = new FormData()
const form = new FormData();
if (typeof body !== 'object' || !body) return form
if (typeof body !== 'object' || !body) return form;
for (const objectValue of Object.values(body)) {
const value = objectValue as MultipartFile | MultipartValue
const value = objectValue as MultipartFile | MultipartValue;
if (value.type === 'file') {
form.append(value.fieldname, new Blob([await value.toBuffer()]), value.filename)
form.append(value.fieldname, new Blob([await value.toBuffer()]), value.filename);
}
if (value.type === 'field' && typeof value.value === 'string') {
form.append(value.fieldname, value.value)
form.append(value.fieldname, value.value);
}
}
return form
return form;
}

View File

@@ -1,48 +1,48 @@
import type { RequestMethods } from '@discordeno/bot'
import { REST_HOST, REST_PORT } from '../config.js'
import { buildFastifyApp, parseMultiformBody } from './fastify.js'
import restManager, { logger } from './restManager.js'
import type { RequestMethods } from '@discordeno/bot';
import { REST_HOST, REST_PORT } from '../config.js';
import { buildFastifyApp, parseMultiformBody } from './fastify.js';
import restManager, { logger } from './restManager.js';
const app = buildFastifyApp()
const app = buildFastifyApp();
app.get('/timecheck', async (_req, res) => {
res.status(200).send({ message: Date.now() })
})
res.status(200).send({ message: Date.now() });
});
app.all('/*', async (req, res) => {
let url = req.originalUrl
let url = req.originalUrl;
if (url.startsWith('/v')) {
url = url.slice(url.indexOf('/', 2))
url = url.slice(url.indexOf('/', 2));
}
const isMultipart = req.headers['content-type']?.startsWith('multipart/form-data')
const hasBody = req.method !== 'GET' && req.method !== 'DELETE'
const body = hasBody ? (isMultipart ? await parseMultiformBody(req.body) : req.body) : undefined
const isMultipart = req.headers['content-type']?.startsWith('multipart/form-data');
const hasBody = req.method !== 'GET' && req.method !== 'DELETE';
const body = hasBody ? (isMultipart ? await parseMultiformBody(req.body) : req.body) : undefined;
try {
const result = await restManager.makeRequest(req.method as RequestMethods, url, {
body,
})
});
if (result) {
res.status(200).send(result)
return
res.status(200).send(result);
return;
}
res.status(204).send({})
res.status(204).send({});
} catch (error) {
logger.error(error)
logger.error(error);
res.status(500).send({
message: error,
})
});
}
})
});
await app.listen({
host: REST_HOST,
port: REST_PORT,
})
});
logger.info(`REST Proxy listening on port ${REST_PORT}`)
logger.info(`REST Proxy listening on port ${REST_PORT}`);

View File

@@ -1,15 +1,15 @@
import type { RestManager } from '@discordeno/bot'
import { InfluxDB, Point } from '@influxdata/influxdb-client'
import { INFLUX_BUCKET, INFLUX_ENABLED, INFLUX_ORG, INFLUX_TOKEN, INFLUX_URL } from '../config.js'
import type { RestManager } from '@discordeno/bot';
import { InfluxDB, Point } from '@influxdata/influxdb-client';
import { INFLUX_BUCKET, INFLUX_ENABLED, INFLUX_ORG, INFLUX_TOKEN, INFLUX_URL } from '../config.js';
export const influxDB = INFLUX_ENABLED ? new InfluxDB({ url: INFLUX_URL!, token: INFLUX_TOKEN! }) : undefined
export const influx = INFLUX_ENABLED && influxDB ? influxDB.getWriteApi(INFLUX_ORG!, INFLUX_BUCKET!) : undefined
export const influxDB = INFLUX_ENABLED ? new InfluxDB({ url: INFLUX_URL!, token: INFLUX_TOKEN! }) : undefined;
export const influx = INFLUX_ENABLED && influxDB ? influxDB.getWriteApi(INFLUX_ORG!, INFLUX_BUCKET!) : undefined;
export const setupRestAnalyticsHooks = (rest: RestManager, logger: RestManager['logger']): void => {
// If influxdb data is provided, enable analytics in this proxy.
if (!influx) return
if (!influx) return;
const originalSendRequest = rest.sendRequest
const originalSendRequest = rest.sendRequest;
rest.sendRequest = async (options) => {
const fetchingPoint = new Point('restEvents')
@@ -17,31 +17,31 @@ export const setupRestAnalyticsHooks = (rest: RestManager, logger: RestManager['
.stringField('type', 'REQUEST_FETCHING')
.tag('method', options.method)
.tag('route', options.route)
.tag('bucket', options.bucketId ?? 'NA')
.tag('bucket', options.bucketId ?? 'NA');
influx.writePoint(fetchingPoint)
influx.writePoint(fetchingPoint);
await originalSendRequest(options)
await originalSendRequest(options);
const fetchedPoint = new Point('restEvents')
.timestamp(new Date())
.stringField('type', 'REQUEST_FETCHED')
.tag('method', options.method)
.tag('route', options.route)
.tag('bucket', options.bucketId ?? 'NA')
.tag('bucket', options.bucketId ?? 'NA');
// FIXME: rest.sendRequest returns Promise<void>, so there is no way currently to get the response status
// .intField('status', response.status)
influx.writePoint(fetchedPoint)
}
influx.writePoint(fetchedPoint);
};
setInterval(async () => {
logger.info('Influx - Saving events...')
logger.info('Influx - Saving events...');
try {
await influx.flush()
logger.info('Influx - events saved!')
await influx.flush();
logger.info('Influx - events saved!');
} catch (error) {
logger.error('Influx - error saving events!', error)
logger.error('Influx - error saving events!', error);
}
}, 30_000 /* 30s */)
}
}, 30_000 /* 30s */);
};

View File

@@ -1,13 +1,13 @@
import { createLogger, createRestManager } from '@discordeno/bot'
import { DISCORD_TOKEN } from '../config.js'
import { setupRestAnalyticsHooks } from './influx.js'
import { createLogger, createRestManager } from '@discordeno/bot';
import { DISCORD_TOKEN } from '../config.js';
import { setupRestAnalyticsHooks } from './influx.js';
const manager = createRestManager({
token: DISCORD_TOKEN,
})
});
export const logger = createLogger({ name: 'REST' })
export const logger = createLogger({ name: 'REST' });
setupRestAnalyticsHooks(manager, logger)
setupRestAnalyticsHooks(manager, logger);
export default manager
export default manager;

View File

@@ -1,25 +1,25 @@
import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
// This is a "polyfill" for the `Promise.withResolves`, while node 22 does support it, node 18 does not and this template does support node 18
export function promiseWithResolvers<T>(): PromiseWithResolvers<T> {
let resolve!: (data: T | PromiseLike<T>) => void
let reject!: (reason?: any) => void
let resolve!: (data: T | PromiseLike<T>) => void;
let reject!: (reason?: any) => void;
const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve
reject = _reject
})
resolve = _resolve;
reject = _reject;
});
return { promise, resolve, reject }
return { promise, resolve, reject };
}
// This re-creates the `__dirname` that exists in CommonJS, in ESM node added `import.meta.dirname` but it is for node 20+, so we need this util to support node 18
export function getDirnameFromFileUrl(url: string): string {
return dirname(fileURLToPath(url))
return dirname(fileURLToPath(url));
}
export interface PromiseWithResolvers<T> {
promise: Promise<T>
resolve: (data: T | PromiseLike<T>) => void
reject: (reason?: any) => void
promise: Promise<T>;
resolve: (data: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
}

View File

@@ -1,6 +1,6 @@
import { Collection, createBot, Intents } from '@discordeno/bot'
import { configs } from './config.js'
import type { Command } from './types/commands.js'
import { Collection, createBot, Intents } from '@discordeno/bot';
import { configs } from './config.js';
import type { Command } from './types/commands.js';
const rawBot = createBot({
token: configs.token,
@@ -13,11 +13,11 @@ const rawBot = createBot({
token: true,
},
},
})
});
export const bot = rawBot as BotWithCommands
export const bot = rawBot as BotWithCommands;
// Create the command collection
bot.commands = new Collection()
bot.commands = new Collection();
export type BotWithCommands = typeof rawBot & { commands: Collection<string, Command> }
export type BotWithCommands = typeof rawBot & { commands: Collection<string, Command> };

View File

@@ -1,6 +1,6 @@
import { bot } from './bot.js'
import type { Command } from './types/commands.js'
import { bot } from './bot.js';
import type { Command } from './types/commands.js';
export function createCommand(command: Command): void {
bot.commands.set(command.name, command)
bot.commands.set(command.name, command);
}

View File

@@ -1,13 +1,13 @@
import { ApplicationCommandTypes, snowflakeToTimestamp } from '@discordeno/bot'
import { createCommand } from '../commands.js'
import { ApplicationCommandTypes, snowflakeToTimestamp } from '@discordeno/bot';
import { createCommand } from '../commands.js';
createCommand({
name: 'ping',
description: 'Ping the Bot!',
type: ApplicationCommandTypes.ChatInput,
async execute(interaction) {
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
await interaction.respond(`🏓 Pong! ${ping}ms`)
await interaction.respond(`🏓 Pong! ${ping}ms`);
},
})
});

View File

@@ -1,17 +1,17 @@
const token = process.env.BOT_TOKEN
const devGuildId = process.env.DEV_GUILD_ID
const token = process.env.BOT_TOKEN;
const devGuildId = process.env.DEV_GUILD_ID;
if (!token) throw new Error('Missing BOT_TOKEN environment variable')
if (!devGuildId) throw new Error('Missing DEV_GUILD_ID environment variable')
if (!token) throw new Error('Missing BOT_TOKEN environment variable');
if (!devGuildId) throw new Error('Missing DEV_GUILD_ID environment variable');
export const configs: Config = {
/** Get token from ENV variable */
token,
/** The server id where you develop your bot and want dev commands created. */
devGuildId: BigInt(devGuildId),
}
};
export interface Config {
token: string
devGuildId: bigint
token: string;
devGuildId: bigint;
}

View File

@@ -1,15 +1,15 @@
import { InteractionTypes } from '@discordeno/bot'
import { bot } from '../bot.js'
import logger from '../utils/logger.js'
import { InteractionTypes } from '@discordeno/bot';
import { bot } from '../bot.js';
import logger from '../utils/logger.js';
bot.events.interactionCreate = (interaction) => {
if (!interaction.data) return
if (!interaction.data) return;
switch (interaction.type) {
case InteractionTypes.ApplicationCommand:
logger.info(`[Application Command] ${interaction.data.name} command executed.`)
logger.info(`[Application Command] ${interaction.data.name} command executed.`);
bot.commands.get(interaction.data.name)?.execute(interaction)
break
bot.commands.get(interaction.data.name)?.execute(interaction);
break;
}
}
};

View File

@@ -1,17 +1,17 @@
import { bot } from '../bot.js'
import logger from '../utils/logger.js'
import { bot } from '../bot.js';
import logger from '../utils/logger.js';
bot.events.ready = ({ shardId }) => {
logger.info(`[READY] Shard ${shardId} is ready!`)
logger.info(`[READY] Shard ${shardId} is ready!`);
if (shardId === bot.gateway.lastShardId) {
botFullyReady()
botFullyReady();
}
}
};
// This function lets you run custom code when all your bot's shards are online.
function botFullyReady(): void {
// Do stuff that you want that get execute only when the bot is fully online.
logger.info('[READY] Bot is fully online.')
logger.info('[READY] Bot is fully online.');
}

View File

@@ -1,15 +1,15 @@
import 'dotenv/config'
import 'dotenv/config';
import { bot } from './bot.js'
import importDirectory from './utils/loader.js'
import logger from './utils/logger.js'
import { bot } from './bot.js';
import importDirectory from './utils/loader.js';
import logger from './utils/logger.js';
logger.info('Starting bot...')
logger.info('Starting bot...');
logger.info('Loading commands...')
await importDirectory('./dist/commands')
logger.info('Loading commands...');
await importDirectory('./dist/commands');
logger.info('Loading events...')
await importDirectory('./dist/events')
logger.info('Loading events...');
await importDirectory('./dist/events');
await bot.start()
await bot.start();

View File

@@ -1,16 +1,16 @@
import 'dotenv/config'
import 'dotenv/config';
import importDirectory from './utils/loader.js'
import logger from './utils/logger.js'
import { updateApplicationCommands } from './utils/updateCommands.js'
import importDirectory from './utils/loader.js';
import logger from './utils/logger.js';
import { updateApplicationCommands } from './utils/updateCommands.js';
logger.info('Loading commands...')
await importDirectory('./dist/commands')
logger.info('Loading commands...');
await importDirectory('./dist/commands');
logger.info('Updating commands...')
await updateApplicationCommands()
logger.info('Updating commands...');
await updateApplicationCommands();
logger.info('Done!')
logger.info('Done!');
// We need to manually exit as the REST Manager has timeouts that will keep NodeJS alive
process.exit()
process.exit();

View File

@@ -1,17 +1,17 @@
import type { ApplicationCommandOption, ApplicationCommandTypes } from '@discordeno/bot'
import type { bot } from '../bot.js'
import type { ApplicationCommandOption, ApplicationCommandTypes } from '@discordeno/bot';
import type { bot } from '../bot.js';
export interface Command {
/** The name of this command. */
name: string
name: string;
/** What does this command do? */
description: string
description: string;
/** The type of command this is. */
type: ApplicationCommandTypes
type: ApplicationCommandTypes;
/** Whether or not this command is for the dev server only. */
devOnly?: boolean
devOnly?: boolean;
/** The options for this command */
options?: ApplicationCommandOption[]
options?: ApplicationCommandOption[];
/** This will be executed when the command is run. */
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction) => unknown
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction) => unknown;
}

View File

@@ -1,15 +1,15 @@
import { readdir } from 'node:fs/promises'
import logger from './logger.js'
import { readdir } from 'node:fs/promises';
import logger from './logger.js';
export default async function importDirectory(folder: string): Promise<void> {
const files = await readdir(folder, { recursive: true })
const files = await readdir(folder, { recursive: true });
for (const filename of files) {
if (!filename.endsWith('.js')) continue
if (!filename.endsWith('.js')) continue;
// Using `file://` and `process.cwd()` to avoid weird issues with relative paths and/or Windows
await import(`file://${process.cwd()}/${folder}/${filename}`).catch((x) =>
logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x),
)
);
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import chalk from 'chalk'
import chalk from 'chalk';
export enum LogLevels {
Debug,
@@ -15,70 +15,70 @@ const prefixes = new Map<LogLevels, string>([
[LogLevels.Warn, 'WARN'],
[LogLevels.Error, 'ERROR'],
[LogLevels.Fatal, 'FATAL'],
])
]);
const noColor: (str: string) => string = (msg) => msg
const noColor: (str: string) => string = (msg) => msg;
const colorFunctions = new Map<LogLevels, (str: string) => string>([
[LogLevels.Debug, chalk.gray],
[LogLevels.Info, chalk.cyan],
[LogLevels.Warn, chalk.yellow],
[LogLevels.Error, (str: string) => chalk.red(str)],
[LogLevels.Fatal, (str: string) => chalk.red.bold.italic(str)],
])
]);
export function createLogger({ logLevel = LogLevels.Info, name }: { logLevel?: LogLevels; name?: string } = {}): Logger {
function log(level: LogLevels, ...args: any[]): void {
if (level < logLevel) return
if (level < logLevel) return;
let color = colorFunctions.get(level)
if (!color) color = noColor
let color = colorFunctions.get(level);
if (!color) color = noColor;
const date = new Date()
const date = new Date();
const log = [
`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`,
color(prefixes.get(level) ?? 'DEBUG'),
name ? `${name} >` : '>',
...args,
]
];
switch (level) {
case LogLevels.Debug:
return console.debug(...log)
return console.debug(...log);
case LogLevels.Info:
return console.info(...log)
return console.info(...log);
case LogLevels.Warn:
return console.warn(...log)
return console.warn(...log);
case LogLevels.Error:
return console.error(...log)
return console.error(...log);
case LogLevels.Fatal:
return console.error(...log)
return console.error(...log);
default:
return console.log(...log)
return console.log(...log);
}
}
function setLevel(level: LogLevels): void {
logLevel = level
logLevel = level;
}
function debug(...args: any[]): void {
log(LogLevels.Debug, ...args)
log(LogLevels.Debug, ...args);
}
function info(...args: any[]): void {
log(LogLevels.Info, ...args)
log(LogLevels.Info, ...args);
}
function warn(...args: any[]): void {
log(LogLevels.Warn, ...args)
log(LogLevels.Warn, ...args);
}
function error(...args: any[]): void {
log(LogLevels.Error, ...args)
log(LogLevels.Error, ...args);
}
function fatal(...args: any[]): void {
log(LogLevels.Fatal, ...args)
log(LogLevels.Fatal, ...args);
}
return {
@@ -89,18 +89,18 @@ export function createLogger({ logLevel = LogLevels.Info, name }: { logLevel?: L
warn,
error,
fatal,
}
};
}
export const logger = createLogger({ name: 'Main' })
export default logger
export const logger = createLogger({ name: 'Main' });
export default logger;
export interface Logger {
log: (level: LogLevels, ...args: any[]) => void
debug: (...args: any[]) => void
info: (...args: any[]) => void
warn: (...args: any[]) => void
error: (...args: any[]) => void
fatal: (...args: any[]) => void
setLevel: (level: LogLevels) => void
log: (level: LogLevels, ...args: any[]) => void;
debug: (...args: any[]) => void;
info: (...args: any[]) => void;
warn: (...args: any[]) => void;
error: (...args: any[]) => void;
fatal: (...args: any[]) => void;
setLevel: (level: LogLevels) => void;
}

View File

@@ -1,5 +1,5 @@
import { bot } from '../bot.js'
import { configs } from '../config.js'
import { bot } from '../bot.js';
import { configs } from '../config.js';
export async function updateApplicationCommands(): Promise<void> {
await bot.helpers.upsertGlobalApplicationCommands(
@@ -7,7 +7,7 @@ export async function updateApplicationCommands(): Promise<void> {
// ONLY GLOBAL COMMANDS
.filter((command) => !command.devOnly)
.array(),
)
);
await bot.helpers.upsertGuildApplicationCommands(
configs.devGuildId,
@@ -15,5 +15,5 @@ export async function updateApplicationCommands(): Promise<void> {
// ONLY GLOBAL COMMANDS
.filter((command) => !!command.devOnly)
.array(),
)
);
}

View File

@@ -1,12 +1,12 @@
import 'dotenv/config'
import 'dotenv/config';
import { createBot } from '@discordeno/bot'
import events from './events/index.js'
import { createBot } from '@discordeno/bot';
import events from './events/index.js';
const token = process.env.TOKEN
const token = process.env.TOKEN;
// Ensure the existence of the TOKEN env
if (!token) throw new Error('The TOKEN environment variable needs to be defined.')
if (!token) throw new Error('The TOKEN environment variable needs to be defined.');
export const bot = createBot({
token,
@@ -41,6 +41,6 @@ export const bot = createBot({
id: true,
},
},
})
});
bot.events = events
bot.events = events;

View File

@@ -1,12 +1,12 @@
import { EventEmitter } from 'node:events'
import { EventEmitter } from 'node:events';
// Extremely minimal collector class
export default class ItemCollector<T> extends EventEmitter {
onItem(callback: (item: T) => unknown): void {
this.on('item', callback)
this.on('item', callback);
}
collect(item: T): void {
this.emit('item', item)
this.emit('item', item);
}
}

View File

@@ -1,12 +1,12 @@
import type { CreateSlashApplicationCommand } from '@discordeno/types'
import type { bot } from '../bot.js'
import roles from './roles.js'
import type { CreateSlashApplicationCommand } from '@discordeno/types';
import type { bot } from '../bot.js';
import roles from './roles.js';
export const commands = new Map<string, Command>([roles].map((cmd) => [cmd.name, cmd]))
export const commands = new Map<string, Command>([roles].map((cmd) => [cmd.name, cmd]));
export default commands
export default commands;
export interface Command extends CreateSlashApplicationCommand {
/** Handler that will be executed when this command is triggered */
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction, args: Record<string, unknown>) => Promise<unknown>
execute: (interaction: typeof bot.transformers.$inferredTypes.interaction, args: Record<string, unknown>) => Promise<unknown>;
}

View File

@@ -1,4 +1,4 @@
import assert from 'node:assert'
import assert from 'node:assert';
import {
type ActionRow,
type ButtonComponent,
@@ -6,12 +6,12 @@ import {
MessageComponentTypes,
type SelectMenuComponent,
TextStyles,
} from '@discordeno/bot'
import { ApplicationCommandOptionTypes, ButtonStyles } from '@discordeno/types'
import { bot } from '../bot.js'
import ItemCollector from '../collector.js'
import { collectors } from '../events/interactionCreate.js'
import type { Command } from './index.js'
} from '@discordeno/bot';
import { ApplicationCommandOptionTypes, ButtonStyles } from '@discordeno/types';
import { bot } from '../bot.js';
import ItemCollector from '../collector.js';
import { collectors } from '../events/interactionCreate.js';
import type { Command } from './index.js';
const command: Command = {
name: 'roles',
@@ -73,22 +73,22 @@ const command: Command = {
if (args.reactions?.create) {
// Ensure that there is a channelId
if (!interaction.channelId) {
await interaction.respond('Could not get the current channel.', { isPrivate: true })
return
await interaction.respond('Could not get the current channel.', { isPrivate: true });
return;
}
// This array is used to store all the roles for this reaction roles
let roles = [args.reactions.create]
let roles = [args.reactions.create];
// Send the message that uses will use to get the role
const roleMessage = await bot.helpers.sendMessage(interaction.channelId, {
content: 'Pick your roles',
components: getRoleButtons(roles),
})
});
// Create a copy of the actionRow for the main message
// NOTE: we use a copy so when we edit this actionRow the edits don't get applied to all the command executions, only this one, for example we do disable some buttons in some conditional cases
const messageActionRow = structuredClone(messageActionRowTemplate)
const messageActionRow = structuredClone(messageActionRowTemplate);
const message = await interaction.respond(
{
@@ -96,230 +96,230 @@ const command: Command = {
components: [messageActionRow],
},
{ isPrivate: true, withResponse: true },
)
);
if (!message) {
await interaction.respond('❌ Unable to send the message correctly. Cancelling', { isPrivate: true })
return
await interaction.respond('❌ Unable to send the message correctly. Cancelling', { isPrivate: true });
return;
}
assert('resource' in message && message.resource?.message)
assert('resource' in message && message.resource?.message);
// Create the collector for the menu
const itemCollector = new ItemCollector<typeof bot.transformers.$inferredTypes.interaction>()
collectors.add(itemCollector)
const itemCollector = new ItemCollector<typeof bot.transformers.$inferredTypes.interaction>();
collectors.add(itemCollector);
// For the new reaction role, we need to keep track of what the user gave us
let partialRoleInfo: Partial<(typeof roles)[number]> | undefined
let partialRoleInfo: Partial<(typeof roles)[number]> | undefined;
itemCollector.onItem(async (i) => {
// We need to verify the interaction is for us.
if (i.message?.id !== message.resource?.message?.id) {
return
return;
}
// Save button
if (i.data?.customId === 'reactionRoles-save') {
// Remove this item collector from the list of collectors (we aren't correcting anymore)
collectors.delete(itemCollector)
collectors.delete(itemCollector);
// Delete the edit message
await i.deferEdit()
await i.delete()
await i.deferEdit();
await i.delete();
return
return;
}
// New button
if (i.data?.customId === 'reactionRoles-add') {
partialRoleInfo = {}
partialRoleInfo = {};
// Ask the user for the role
await i.edit({ content: 'Pick a role for the new reaction role', components: [selectRoleActionRow] })
return
await i.edit({ content: 'Pick a role for the new reaction role', components: [selectRoleActionRow] });
return;
}
// New button - role select menu
if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-role') {
const roleToAdd = i.data?.resolved?.roles?.first()
const roleToAdd = i.data?.resolved?.roles?.first();
// Verify that we could get the role from discord
if (!roleToAdd) {
throw new Error('Unable to get the information for the role to add')
throw new Error('Unable to get the information for the role to add');
}
// Save it to our partial role information
partialRoleInfo.role = roleToAdd
partialRoleInfo.role = roleToAdd;
// Ask the user for the color of the button
await i.edit({
content: 'Pick a color for the reaction role',
components: [selectColorActionRow],
})
});
return
return;
}
// New button - color select menu
if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-color') {
const color = parseInt(i.data?.values?.[0] ?? 'NaN')
const color = parseInt(i.data?.values?.[0] ?? 'NaN');
// Verify that we could get the color information
if (isNaN(color)) {
throw new Error('Unable to get the information for the role to add')
throw new Error('Unable to get the information for the role to add');
}
// Save the color to our partial
partialRoleInfo.color = color
partialRoleInfo.color = color;
// Ask the user to input the emoji and optionally a label for the button
await i.respond({
title: 'Pick an emoji and label for the reaction role',
components: [selectEmojiActionRow, selectLabelActionRow],
customId: 'reactionRoles-add-modal',
})
});
return
return;
}
// New button - emoji & label modal
if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-modal') {
// Ensure that we can get the channelId from the interaction
if (!interaction.channelId) {
throw new Error('Unable to get current channel')
throw new Error('Unable to get current channel');
}
// Get the data from discord
const emoji = i.data.components?.[0]?.components?.[0].value
const label = i.data.components?.[1]?.components?.[0].value
const emoji = i.data.components?.[0]?.components?.[0].value;
const label = i.data.components?.[1]?.components?.[0].value;
// Verify that the emoji was given
if (!emoji) {
throw new Error('Unable to get the information for the role to add')
throw new Error('Unable to get the information for the role to add');
}
// Save them to our partial
partialRoleInfo.emoji = emoji
partialRoleInfo.label = label
partialRoleInfo.emoji = emoji;
partialRoleInfo.label = label;
// Save role and display the new message editing the old one
// We are sure that in this place the entire object has been assembled
roles.push(partialRoleInfo as (typeof roles)[number])
roles.push(partialRoleInfo as (typeof roles)[number]);
await bot.helpers.editMessage(interaction.channelId, roleMessage.id, {
components: getRoleButtons(roles),
})
});
// Clear our partial roleInfo, we are done with it
partialRoleInfo = undefined
partialRoleInfo = undefined;
// In case the delete button was disabled (all the roles were deleted) re-enable it
messageActionRow.components[1]!.disabled = false
messageActionRow.components[1]!.disabled = false;
// Discord imposes a limit of 5 action rows and 5 buttons for actionRow = 25 buttons max
// more than 25 will give an error, so we disable the new button
if (roles.length === 25) {
const button = messageActionRow.components[0] as ButtonComponent
button.disabled = true
const button = messageActionRow.components[0] as ButtonComponent;
button.disabled = true;
}
// Show again the main edit menu
await interaction.edit({
content: 'Use the buttons in this message to edit the message below.',
components: [messageActionRow],
})
});
// Respond to the modal. A modal submit (type 5) interaction can't edit the original response
await i.respond('Reaction role created successfully. You can use the message above to add/remove a role', { isPrivate: true })
await i.respond('Reaction role created successfully. You can use the message above to add/remove a role', { isPrivate: true });
return
return;
}
// Remove button
if (i.data?.customId === 'reactionRoles-remove') {
// Clone the actionRow for the remove select menu, this is to prevent unwanted data to appear to other users
const removeActionRow = structuredClone(removeActionRowTemplate)
const selectMenu = removeActionRow.components[0] as SelectMenuComponent
const removeActionRow = structuredClone(removeActionRowTemplate);
const selectMenu = removeActionRow.components[0] as SelectMenuComponent;
// Add the possible values for this select menu
for (const roleInfo of roles) {
selectMenu.options.push({
label: `${roleInfo.emoji} ${roleInfo.label ?? ''}`,
value: roleInfo.role.id.toString(),
})
});
}
// Ask the user for what reaction role they want to remove
await i.edit({
content: 'Select what reaction role to remove',
components: [removeActionRow],
})
});
return
return;
}
// Remove button - role select menu
if (i.data?.customId === 'reactionRoles-remove-selectMenu') {
// Ensure that we can get the channelId from the interaction
if (!interaction.channelId) {
throw new Error('Unable to get current channel')
throw new Error('Unable to get current channel');
}
// Get the role to delete from discord
const roleToRemove = i.data?.values?.[0]
const roleToRemove = i.data?.values?.[0];
// Ensure we got it
if (!roleToRemove) {
throw new Error('Unable to get the role to remove')
throw new Error('Unable to get the role to remove');
}
await i.deferEdit()
await i.deferEdit();
// Remove the role from the list
roles = roles.filter((roleInfo) => roleInfo.role.id.toString() !== roleToRemove)
roles = roles.filter((roleInfo) => roleInfo.role.id.toString() !== roleToRemove);
// Edit the main button
await bot.helpers.editMessage(interaction.channelId, roleMessage.id, {
components: getRoleButtons(roles),
})
});
// If the new button was disabled (we were at 25 buttons) we re-enable it
const button = messageActionRow.components[0] as ButtonComponent
button.disabled = false
const button = messageActionRow.components[0] as ButtonComponent;
button.disabled = false;
// If we are at 0 roles, and the user tried to delete a role they will get locked in the menu, so we disable it
if (roles.length === 0) {
messageActionRow.components[1]!.disabled = true
messageActionRow.components[1]!.disabled = true;
}
// Show the main edit ui (new, remove, save)
await i.edit({
content: 'Use the buttons in this message to edit the message below.',
components: [messageActionRow],
})
});
return
return;
}
// We don't know what code to run for this interaction
throw new Error('Unknown button')
})
throw new Error('Unknown button');
});
}
},
}
};
export default command
export default command;
// Interface to type the arguments that we receive from discord
interface CommandArgs {
reactions?: {
create?: {
role: typeof bot.transformers.$inferredTypes.role
emoji: string
color: ButtonStyles
label?: string
}
}
role: typeof bot.transformers.$inferredTypes.role;
emoji: string;
color: ButtonStyles;
label?: string;
};
};
}
// Templates/ActionRows for the command to then be referenced in the various part of the code
@@ -357,7 +357,7 @@ const messageActionRowTemplate: ActionRow = {
label: 'Save',
},
],
} as const
} as const;
const removeActionRowTemplate: ActionRow = {
type: MessageComponentTypes.ActionRow,
@@ -371,7 +371,7 @@ const removeActionRowTemplate: ActionRow = {
options: [],
},
],
} as const
} as const;
const selectRoleActionRow: ActionRow = {
type: MessageComponentTypes.ActionRow,
@@ -384,7 +384,7 @@ const selectRoleActionRow: ActionRow = {
placeholder: 'Select a role',
},
],
} as const
} as const;
const selectColorActionRow: ActionRow = {
type: MessageComponentTypes.ActionRow,
@@ -400,7 +400,7 @@ const selectColorActionRow: ActionRow = {
],
},
],
} as const
} as const;
const selectEmojiActionRow: ActionRow = {
type: MessageComponentTypes.ActionRow,
@@ -413,7 +413,7 @@ const selectEmojiActionRow: ActionRow = {
required: true,
},
],
} as const
} as const;
const selectLabelActionRow: ActionRow = {
type: MessageComponentTypes.ActionRow,
@@ -427,37 +427,37 @@ const selectLabelActionRow: ActionRow = {
maxLength: 80,
},
],
} as const
} as const;
// Function to get all the actionRows with buttons for the reaction roles message
function getRoleButtons(
roles: Array<{
role: typeof bot.transformers.$inferredTypes.role
emoji: string
color: ButtonStyles
label?: string | undefined
role: typeof bot.transformers.$inferredTypes.role;
emoji: string;
color: ButtonStyles;
label?: string | undefined;
}>,
): ActionRow[] {
const actionRows: ActionRow[] = []
const actionRows: ActionRow[] = [];
// If there aren't any roles, we don't need any buttons
if (roles.length === 0) return actionRows
if (roles.length === 0) return actionRows;
// We add the components later, so we need to make typescript know that we are sure that it will be a compatibile components array
actionRows.push({ type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] })
actionRows.push({ type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] });
for (const roleInfo of roles) {
let actionRow = actionRows.at(-1)
let actionRow = actionRows.at(-1);
// Ensure that we were able to get the actionRow
if (!actionRow) {
throw new Error('Unable to get actionRow')
throw new Error('Unable to get actionRow');
}
// If the actionRow is full (has 5 buttons) add a new one
if (actionRow.components.length === 5) {
actionRow = { type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] }
actionRows.push(actionRow)
actionRow = { type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] };
actionRows.push(actionRow);
}
// Add the new button to this actionRow
@@ -469,8 +469,8 @@ function getRoleButtons(
},
label: roleInfo.label,
customId: `reactionRoles-role-${roleInfo.role.id}`,
})
});
}
return actionRows
return actionRows;
}

View File

@@ -1,10 +1,10 @@
import type { bot } from '../bot.js'
import { event as interactionCreateEvent } from './interactionCreate.js'
import { event as readyEvent } from './ready.js'
import type { bot } from '../bot.js';
import { event as interactionCreateEvent } from './interactionCreate.js';
import { event as readyEvent } from './ready.js';
export const events = {
interactionCreate: interactionCreateEvent,
ready: readyEvent,
} as typeof bot.events
} as typeof bot.events;
export default events
export default events;

View File

@@ -1,59 +1,59 @@
import { commandOptionsParser, InteractionTypes, MessageComponentTypes } from '@discordeno/bot'
import { bot } from '../bot.js'
import type ItemCollector from '../collector.js'
import commands from '../commands/index.js'
import { commandOptionsParser, InteractionTypes, MessageComponentTypes } from '@discordeno/bot';
import { bot } from '../bot.js';
import type ItemCollector from '../collector.js';
import commands from '../commands/index.js';
export const collectors = new Set<ItemCollector<typeof bot.transformers.$inferredTypes.interaction>>()
export const collectors = new Set<ItemCollector<typeof bot.transformers.$inferredTypes.interaction>>();
export const event: typeof bot.events.interactionCreate = async (interaction) => {
// Give to all the collectors the interaction to use
for (const collector of collectors) {
collector.collect(interaction)
collector.collect(interaction);
}
// If the interaction is a command check if it is a command and run it
if (interaction.type === InteractionTypes.ApplicationCommand) {
if (!interaction.data) return
if (!interaction.data) return;
const command = commands.get(interaction.data.name)
if (!command) return
const command = commands.get(interaction.data.name);
if (!command) return;
try {
await command.execute(interaction, commandOptionsParser(interaction))
await command.execute(interaction, commandOptionsParser(interaction));
} catch (error) {
console.error(error)
console.error(error);
}
}
// If the interaction is a button it might be the button press on our reaction role message
if (interaction.type === InteractionTypes.MessageComponent && interaction.data?.componentType === MessageComponentTypes.Button) {
// The interaction is not a button press on the role button
if (!interaction.data?.customId?.startsWith('reactionRoles-role-')) return
if (!interaction.guildId || !interaction.member) return
if (!interaction.data?.customId?.startsWith('reactionRoles-role-')) return;
if (!interaction.guildId || !interaction.member) return;
// Remove the prefix and get the roleId
const roleId = BigInt(interaction.data.customId.slice('reactionRoles-role-'.length))
const roleId = BigInt(interaction.data.customId.slice('reactionRoles-role-'.length));
// Check if we need to remove or add the role to the user
const alreadyHasRole = !!interaction.member.roles.find((role) => role === roleId)
const alreadyHasRole = !!interaction.member.roles.find((role) => role === roleId);
try {
if (alreadyHasRole) {
await bot.helpers.removeRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`)
await interaction.respond(`I removed from you the <@&${roleId}> role.`, { isPrivate: true })
return
await bot.helpers.removeRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`);
await interaction.respond(`I removed from you the <@&${roleId}> role.`, { isPrivate: true });
return;
}
// You will get an invalid request made if the bot attempts to give a bot role, a role higher then him hightest role, a link role or if it does not have the Manage Roles permission
// This could be prevented by checking for the roles that the bot owns and the role that the bot is trying to add
await bot.helpers.addRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`)
await interaction.respond(`I added to you the <@&${roleId}> role.`, { isPrivate: true })
await bot.helpers.addRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`);
await interaction.respond(`I added to you the <@&${roleId}> role.`, { isPrivate: true });
} catch {
// Respond with an error message
await interaction.respond(
'I could not give you the role. Possible reasons are:\n- My permissions are not configured correctly, make sure i have the `Manage Roles` permission\n- The role is **above** my hightest role in the server setup\n- The role does not exist or is non-manageable (for example: bot roles, link roles or @everyone)',
{ isPrivate: true },
)
);
}
}
}
};

View File

@@ -1,6 +1,6 @@
import { bot } from '../bot.js'
import { bot } from '../bot.js';
export const event: typeof bot.events.ready = () => {
// Print to the console when the bot has connected to discord and is ready to handle the events
bot.logger.info('The bot is ready!')
}
bot.logger.info('The bot is ready!');
};

View File

@@ -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);

View File

@@ -1,12 +1,12 @@
import 'dotenv/config'
import 'dotenv/config';
import { bot } from './bot.js'
import commands from './commands/index.js'
import { bot } from './bot.js';
import commands from './commands/index.js';
const guildId = 'REPLACE WITH YOUR GUILD ID'
const guildId = 'REPLACE WITH YOUR GUILD ID';
await bot.rest
.upsertGuildApplicationCommands(guildId, [...commands.values()])
.catch((e) => bot.logger.error('There was an error when updating the global commands', e))
.catch((e) => bot.logger.error('There was an error when updating the global commands', e));
process.exit(0)
process.exit(0);

View File

@@ -1,20 +1,20 @@
import type { CreateGatewayManagerOptions, GatewayManager } from '@discordeno/gateway'
import { createGatewayManager, ShardSocketCloseCodes } from '@discordeno/gateway'
import type { CreateRestManagerOptions, RestManager } from '@discordeno/rest'
import { createRestManager } from '@discordeno/rest'
import type { BigString, GatewayDispatchEventNames, GatewayIntents, RecursivePartial } from '@discordeno/types'
import { createLogger, getBotIdFromToken, type logger } from '@discordeno/utils'
import type { CreateGatewayManagerOptions, GatewayManager } from '@discordeno/gateway';
import { createGatewayManager, ShardSocketCloseCodes } from '@discordeno/gateway';
import type { CreateRestManagerOptions, RestManager } from '@discordeno/rest';
import { createRestManager } from '@discordeno/rest';
import type { BigString, GatewayDispatchEventNames, GatewayIntents, RecursivePartial } from '@discordeno/types';
import { createLogger, getBotIdFromToken, type logger } from '@discordeno/utils';
import type {
CompleteDesiredProperties,
DesiredPropertiesBehavior,
SetupDesiredProps,
TransformersDesiredProperties,
TransformersObjects,
} from './desiredProperties.js'
import type { EventHandlers } from './events.js'
import { type BotGatewayHandler, createBotGatewayHandlers, type GatewayHandlers } from './handlers.js'
import { type BotHelpers, createBotHelpers } from './helpers.js'
import { createTransformers, type Transformers } from './transformers.js'
} from './desiredProperties.js';
import type { EventHandlers } from './events.js';
import { type BotGatewayHandler, createBotGatewayHandlers, type GatewayHandlers } from './handlers.js';
import { type BotHelpers, createBotHelpers } from './helpers.js';
import { createTransformers, type Transformers } from './transformers.js';
/**
* Create a bot object that will maintain the rest and gateway connection.
@@ -27,44 +27,44 @@ import { createTransformers, type Transformers } from './transformers.js'
export function createBot<
TProps extends TransformersDesiredProperties,
TBehavior extends DesiredPropertiesBehavior = DesiredPropertiesBehavior.RemoveKey,
>(options: CreateBotOptions<TProps, TBehavior>): Bot<TProps, TBehavior>
>(options: CreateBotOptions<TProps, TBehavior>): Bot<TProps, TBehavior>;
export function createBot<
TProps extends RecursivePartial<TransformersDesiredProperties>,
TBehavior extends DesiredPropertiesBehavior = DesiredPropertiesBehavior.RemoveKey,
>(options: CreateBotOptions<TProps, TBehavior>): Bot<CompleteDesiredProperties<TProps>, TBehavior>
>(options: CreateBotOptions<TProps, TBehavior>): Bot<CompleteDesiredProperties<TProps>, TBehavior>;
export function createBot<
TProps extends RecursivePartial<TransformersDesiredProperties>,
TBehavior extends DesiredPropertiesBehavior = DesiredPropertiesBehavior.RemoveKey,
>(options: CreateBotOptions<TProps, TBehavior>): Bot<CompleteDesiredProperties<TProps>, TBehavior> {
type CompleteProps = CompleteDesiredProperties<TProps>
type TypedBot = Bot<CompleteProps, TBehavior>
type CompleteProps = CompleteDesiredProperties<TProps>;
type TypedBot = Bot<CompleteProps, TBehavior>;
if (!options.transformers) options.transformers = {}
if (!options.rest) options.rest = { token: options.token, applicationId: options.applicationId }
if (!options.rest.token) options.rest.token = options.token
if (!options.rest.logger && options.loggerFactory) options.rest.logger = options.loggerFactory('REST')
if (!options.gateway) options.gateway = { token: options.token }
if (!options.gateway.token) options.gateway.token = options.token
if (!options.gateway.events) options.gateway.events = {}
if (!options.gateway.logger && options.loggerFactory) options.gateway.logger = options.loggerFactory('GATEWAY')
if (!options.transformers) options.transformers = {};
if (!options.rest) options.rest = { token: options.token, applicationId: options.applicationId };
if (!options.rest.token) options.rest.token = options.token;
if (!options.rest.logger && options.loggerFactory) options.rest.logger = options.loggerFactory('REST');
if (!options.gateway) options.gateway = { token: options.token };
if (!options.gateway.token) options.gateway.token = options.token;
if (!options.gateway.events) options.gateway.events = {};
if (!options.gateway.logger && options.loggerFactory) options.gateway.logger = options.loggerFactory('GATEWAY');
if (!options.gateway.events.message) {
options.gateway.events.message = async (shard, data) => {
// TRIGGER RAW EVENT
bot.events.raw?.(data, shard.id)
bot.events.raw?.(data, shard.id);
if (!data.t) return
if (!data.t) return;
// RUN DISPATCH CHECK
await bot.events.dispatchRequirements?.(data, shard.id)
bot.handlers[data.t as GatewayDispatchEventNames]?.(bot, data, shard.id)
}
await bot.events.dispatchRequirements?.(data, shard.id);
bot.handlers[data.t as GatewayDispatchEventNames]?.(bot, data, shard.id);
};
}
options.gateway.intents = options.intents
;(options.transformers as Transformers<CompleteProps, TBehavior>).desiredProperties = options.desiredProperties as CompleteProps
options.gateway.intents = options.intents;
(options.transformers as Transformers<CompleteProps, TBehavior>).desiredProperties = options.desiredProperties as CompleteProps;
const id = getBotIdFromToken(options.token)
const id = getBotIdFromToken(options.token);
const bot: TypedBot = {
id,
@@ -79,63 +79,63 @@ export function createBot<
helpers: {} as BotHelpers<CompleteProps, TBehavior>,
async start() {
if (!options.gateway?.connection) {
bot.gateway.connection = await bot.rest.getSessionInfo()
bot.gateway.connection = await bot.rest.getSessionInfo();
// Check for overrides in the configuration
if (!options.gateway?.url) bot.gateway.url = bot.gateway.connection.url
if (!options.gateway?.url) bot.gateway.url = bot.gateway.connection.url;
if (!options.gateway?.totalShards) bot.gateway.totalShards = bot.gateway.connection.shards
if (!options.gateway?.totalShards) bot.gateway.totalShards = bot.gateway.connection.shards;
if (!options.gateway?.lastShardId && !options.gateway?.totalShards) bot.gateway.lastShardId = bot.gateway.connection.shards - 1
if (!options.gateway?.lastShardId && !options.gateway?.totalShards) bot.gateway.lastShardId = bot.gateway.connection.shards - 1;
}
if (!bot.gateway.resharding.getSessionInfo) {
bot.gateway.resharding.getSessionInfo = async () => {
return await bot.rest.getGatewayBot()
}
return await bot.rest.getGatewayBot();
};
}
await bot.gateway.spawnShards()
await bot.gateway.spawnShards();
},
async shutdown() {
return await bot.gateway.shutdown(ShardSocketCloseCodes.Shutdown, 'User requested bot stop')
return await bot.gateway.shutdown(ShardSocketCloseCodes.Shutdown, 'User requested bot stop');
},
}
};
bot.helpers = createBotHelpers(bot)
if (options.applicationId) bot.applicationId = bot.transformers.snowflake(options.applicationId)
bot.helpers = createBotHelpers(bot);
if (options.applicationId) bot.applicationId = bot.transformers.snowflake(options.applicationId);
return bot
return bot;
}
export interface CreateBotOptions<TProps extends RecursivePartial<TransformersDesiredProperties>, TBehavior extends DesiredPropertiesBehavior> {
/** The bot's token. */
token: string
token: string;
/** Application Id of the bot incase it is an old bot token. */
applicationId?: BigString
applicationId?: BigString;
/** The bot's intents that will be used to make a connection with discords gateway. */
intents?: GatewayIntents
intents?: GatewayIntents;
/** Any options you wish to provide to the rest manager. */
rest?: Omit<CreateRestManagerOptions, 'token'> & Partial<Pick<CreateRestManagerOptions, 'token'>>
rest?: Omit<CreateRestManagerOptions, 'token'> & Partial<Pick<CreateRestManagerOptions, 'token'>>;
/** Any options you wish to provide to the gateway manager. */
gateway?: Omit<CreateGatewayManagerOptions, 'token'> & Partial<Pick<CreateGatewayManagerOptions, 'token'>>
gateway?: Omit<CreateGatewayManagerOptions, 'token'> & Partial<Pick<CreateGatewayManagerOptions, 'token'>>;
/** The event handlers. */
events?: Partial<EventHandlers<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>>
events?: Partial<EventHandlers<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>>;
/** The functions that should transform discord objects to discordeno shaped objects. */
transformers?: RecursivePartial<Omit<Transformers<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>, 'desiredProperties'>>
transformers?: RecursivePartial<Omit<Transformers<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>, 'desiredProperties'>>;
/** The handler functions that should handle incoming discord payloads from gateway and call an event. */
handlers?: Partial<Record<GatewayDispatchEventNames, BotGatewayHandler<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>>>
handlers?: Partial<Record<GatewayDispatchEventNames, BotGatewayHandler<CompleteDesiredProperties<NoInfer<TProps>>, TBehavior>>>;
/**
* Set the desired properties for the bot
*/
desiredProperties: TProps
desiredProperties: TProps;
/**
* Set the desired properties behavior for undesired properties
*
* @default DesiredPropertiesBehavior.RemoveKey
*/
desiredPropertiesBehavior?: TBehavior
desiredPropertiesBehavior?: TBehavior;
/**
* This factory will be invoked to create the logger for gateway, rest and bot
*
@@ -144,7 +144,7 @@ export interface CreateBotOptions<TProps extends RecursivePartial<TransformersDe
*
* This function will be invoked 3 times, one with the name of `REST`, one with `GATEWAY` and the third one with name `BOT`
*/
loggerFactory?: (name: 'REST' | 'GATEWAY' | 'BOT') => Pick<typeof logger, 'debug' | 'info' | 'warn' | 'error' | 'fatal'>
loggerFactory?: (name: 'REST' | 'GATEWAY' | 'BOT') => Pick<typeof logger, 'debug' | 'info' | 'warn' | 'error' | 'fatal'>;
}
export interface Bot<
@@ -152,28 +152,28 @@ export interface Bot<
TBehavior extends DesiredPropertiesBehavior = DesiredPropertiesBehavior.RemoveKey,
> {
/** The id of the bot. */
id: bigint
id: bigint;
/** The application id of the bot. This is usually the same as id but in the case of old bots can be different. */
applicationId: bigint
applicationId: bigint;
/** The rest manager. */
rest: RestManager
rest: RestManager;
/** The gateway manager. */
gateway: GatewayManager
gateway: GatewayManager;
/** The event handlers. */
events: Partial<EventHandlers<TProps, TBehavior>>
events: Partial<EventHandlers<TProps, TBehavior>>;
/** A logger utility to make it easy to log nice and useful things in the bot code. */
logger: Pick<typeof logger, 'debug' | 'info' | 'warn' | 'error' | 'fatal'>
logger: Pick<typeof logger, 'debug' | 'info' | 'warn' | 'error' | 'fatal'>;
/** The functions that should transform discord objects to discordeno shaped objects. */
transformers: Transformers<TProps, TBehavior> & {
$inferredTypes: {
[K in keyof TransformersObjects]: SetupDesiredProps<TransformersObjects[K], TProps, TBehavior>
}
}
[K in keyof TransformersObjects]: SetupDesiredProps<TransformersObjects[K], TProps, TBehavior>;
};
};
/** The handler functions that should handle incoming discord payloads from gateway and call an event. */
handlers: GatewayHandlers<TProps, TBehavior>
helpers: BotHelpers<TProps, TBehavior>
handlers: GatewayHandlers<TProps, TBehavior>;
helpers: BotHelpers<TProps, TBehavior>;
/** Start the bot connection to the gateway. */
start: () => Promise<void>
start: () => Promise<void>;
/** Shuts down all the bot connections to the gateway. */
shutdown: () => Promise<void>
shutdown: () => Promise<void>;
}

View File

@@ -1,6 +1,6 @@
import { ApplicationCommandOptionTypes } from '@discordeno/types'
import type { CompleteDesiredProperties, DesiredPropertiesBehavior, SetupDesiredProps, TransformersDesiredProperties } from './desiredProperties.js'
import type { Attachment, Channel, Interaction, InteractionDataOption, Member, Role, User } from './transformers/types.js'
import { ApplicationCommandOptionTypes } from '@discordeno/types';
import type { CompleteDesiredProperties, DesiredPropertiesBehavior, SetupDesiredProps, TransformersDesiredProperties } from './desiredProperties.js';
import type { Attachment, Channel, Interaction, InteractionDataOption, Member, Role, User } from './transformers/types.js';
export function commandOptionsParser<
TProps extends TransformersDesiredProperties & { interaction: { data: true } },
@@ -11,51 +11,51 @@ export function commandOptionsParser<
Interaction,
CompleteDesiredProperties<{ interaction: { data: true } }>,
DesiredPropertiesBehavior.RemoveKey
>
>;
if (!interaction.data) return {}
if (!options) options = interaction.data.options ?? []
if (!interaction.data) return {};
if (!options) options = interaction.data.options ?? [];
const args: ParsedInteractionOption<TProps, TBehavior> = {}
const args: ParsedInteractionOption<TProps, TBehavior> = {};
for (const option of options) {
switch (option.type) {
case ApplicationCommandOptionTypes.SubCommandGroup:
case ApplicationCommandOptionTypes.SubCommand:
args[option.name] = commandOptionsParser(interaction, option.options) as InteractionResolvedData<TProps, TBehavior>
break
args[option.name] = commandOptionsParser(interaction, option.options) as InteractionResolvedData<TProps, TBehavior>;
break;
case ApplicationCommandOptionTypes.Channel:
args[option.name] = interaction.data.resolved?.channels?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>
break
args[option.name] = interaction.data.resolved?.channels?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>;
break;
case ApplicationCommandOptionTypes.Role:
args[option.name] = interaction.data.resolved?.roles?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>
break
args[option.name] = interaction.data.resolved?.roles?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>;
break;
case ApplicationCommandOptionTypes.User:
args[option.name] = {
user: interaction.data.resolved?.users?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>,
member: interaction.data.resolved?.members?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>,
}
break
};
break;
case ApplicationCommandOptionTypes.Attachment:
args[option.name] = interaction.data.resolved?.attachments?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>
break
args[option.name] = interaction.data.resolved?.attachments?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>;
break;
case ApplicationCommandOptionTypes.Mentionable:
// Mentionable are roles or users
args[option.name] = (interaction.data.resolved?.roles?.get(BigInt(option.value!)) as ParsedInteractionOption<TProps, TBehavior>[string]) ?? {
user: interaction.data.resolved?.users?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>,
member: interaction.data.resolved?.members?.get(BigInt(option.value!)) as InteractionResolvedData<TProps, TBehavior>,
}
break
};
break;
default:
args[option.name] = option.value as InteractionResolvedData<TProps, TBehavior>
args[option.name] = option.value as InteractionResolvedData<TProps, TBehavior>;
}
}
return args
return args;
}
export interface ParsedInteractionOption<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> {
[key: string]: InteractionResolvedData<TProps, TBehavior>
[key: string]: InteractionResolvedData<TProps, TBehavior>;
}
export type InteractionResolvedData<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> =
@@ -66,11 +66,11 @@ export type InteractionResolvedData<TProps extends TransformersDesiredProperties
| InteractionResolvedDataChannel<TProps, TBehavior>
| SetupDesiredProps<Role, TProps, TBehavior>
| SetupDesiredProps<Attachment, TProps, TBehavior>
| ParsedInteractionOption<TProps, TBehavior>
| ParsedInteractionOption<TProps, TBehavior>;
export interface InteractionResolvedDataUser<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> {
user: SetupDesiredProps<User, TProps, TBehavior>
member: InteractionResolvedDataMember<TProps, TBehavior>
user: SetupDesiredProps<User, TProps, TBehavior>;
member: InteractionResolvedDataMember<TProps, TBehavior>;
}
export type InteractionResolvedDataChannel<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> = Pick<
@@ -92,17 +92,17 @@ export type InteractionResolvedDataChannel<TProps extends TransformersDesiredPro
| 'position'
| 'threadMetadata'
>
>
>;
export type InteractionResolvedDataMember<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> = Omit<
SetupDesiredProps<Member, TProps, TBehavior>,
'user' | 'deaf' | 'mute'
>
>;
/** @deprecated Use {@link InteractionResolvedDataUser} */
export interface InteractionResolvedUser {
user: User
member: InteractionResolvedMember
user: User;
member: InteractionResolvedMember;
}
/** @deprecated Use {@link InteractionResolvedDataChannel} */
@@ -122,7 +122,7 @@ export type InteractionResolvedChannel = Pick<
| 'topic'
| 'position'
| 'threadMetadata'
>
>;
/** @deprecated Use {@link InteractionResolvedDataMember} */
export type InteractionResolvedMember = Omit<Member, 'user' | 'deaf' | 'mute'>
export type InteractionResolvedMember = Omit<Member, 'user' | 'deaf' | 'mute'>;

View File

@@ -1,4 +1,4 @@
export const SLASH_COMMANDS_NAME_REGEX = /^[-_ʼ\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u
export const CONTEXT_MENU_COMMANDS_NAME_REGEX = /^[\w-\s]{1,32}$/
export const CHANNEL_MENTION_REGEX = /<#[0-9]+>/g
export const DISCORD_SNOWFLAKE_REGEX = /^(?<id>\d{17,19})$/
export const SLASH_COMMANDS_NAME_REGEX = /^[-_ʼ\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u;
export const CONTEXT_MENU_COMMANDS_NAME_REGEX = /^[\w-\s]{1,32}$/;
export const CHANNEL_MENTION_REGEX = /<#[0-9]+>/g;
export const DISCORD_SNOWFLAKE_REGEX = /^(?<id>\d{17,19})$/;

View File

@@ -1,7 +1,7 @@
import type { RecursivePartial } from '@discordeno/types'
import type { Collection } from '@discordeno/utils'
import type { Bot } from './bot.js'
import type { InteractionResolvedDataChannel, InteractionResolvedDataMember } from './commandOptionsParser.js'
import type { RecursivePartial } from '@discordeno/types';
import type { Collection } from '@discordeno/utils';
import type { Bot } from './bot.js';
import type { InteractionResolvedDataChannel, InteractionResolvedDataMember } from './commandOptionsParser.js';
import type {
ActivityInstance,
ActivityLocation,
@@ -56,7 +56,7 @@ import type {
UserPrimaryGuild,
VoiceState,
Webhook,
} from './transformers/types.js'
} from './transformers/types.js';
/**
* All the objects that support desired properties
@@ -64,59 +64,59 @@ import type {
* @private This is subject to breaking changes at any time
*/
export interface TransformersObjects {
activityInstance: ActivityInstance
activityLocation: ActivityLocation
attachment: Attachment
avatarDecorationData: AvatarDecorationData
channel: Channel
collectibles: Collectibles
component: Component
defaultReactionEmoji: DefaultReactionEmoji
emoji: Emoji
entitlement: Entitlement
forumTag: ForumTag
guild: Guild
guildOnboarding: GuildOnboarding
guildOnboardingPrompt: GuildOnboardingPrompt
guildOnboardingPromptOption: GuildOnboardingPromptOption
incidentsData: IncidentsData
interaction: Interaction
interactionCallback: InteractionCallback
interactionCallbackResponse: InteractionCallbackResponse
interactionResource: InteractionResource
invite: Invite
inviteStageInstance: InviteStageInstance
lobby: Lobby
lobbyMember: LobbyMember
mediaGalleryItem: MediaGalleryItem
member: Member
message: Message
messageCall: MessageCall
messageInteraction: MessageInteraction
messageInteractionMetadata: MessageInteractionMetadata
messagePin: MessagePin
messageReference: MessageReference
messageSnapshot: MessageSnapshot
nameplate: Nameplate
poll: Poll
pollAnswer: PollAnswer
pollAnswerCount: PollAnswerCount
pollMedia: PollMedia
pollResult: PollResult
role: Role
roleColors: RoleColors
scheduledEvent: ScheduledEvent
scheduledEventRecurrenceRule: ScheduledEventRecurrenceRule
sku: Sku
soundboardSound: SoundboardSound
stageInstance: StageInstance
sticker: Sticker
subscription: Subscription
unfurledMediaItem: UnfurledMediaItem
user: User
userPrimaryGuild: UserPrimaryGuild
voiceState: VoiceState
webhook: Webhook
activityInstance: ActivityInstance;
activityLocation: ActivityLocation;
attachment: Attachment;
avatarDecorationData: AvatarDecorationData;
channel: Channel;
collectibles: Collectibles;
component: Component;
defaultReactionEmoji: DefaultReactionEmoji;
emoji: Emoji;
entitlement: Entitlement;
forumTag: ForumTag;
guild: Guild;
guildOnboarding: GuildOnboarding;
guildOnboardingPrompt: GuildOnboardingPrompt;
guildOnboardingPromptOption: GuildOnboardingPromptOption;
incidentsData: IncidentsData;
interaction: Interaction;
interactionCallback: InteractionCallback;
interactionCallbackResponse: InteractionCallbackResponse;
interactionResource: InteractionResource;
invite: Invite;
inviteStageInstance: InviteStageInstance;
lobby: Lobby;
lobbyMember: LobbyMember;
mediaGalleryItem: MediaGalleryItem;
member: Member;
message: Message;
messageCall: MessageCall;
messageInteraction: MessageInteraction;
messageInteractionMetadata: MessageInteractionMetadata;
messagePin: MessagePin;
messageReference: MessageReference;
messageSnapshot: MessageSnapshot;
nameplate: Nameplate;
poll: Poll;
pollAnswer: PollAnswer;
pollAnswerCount: PollAnswerCount;
pollMedia: PollMedia;
pollResult: PollResult;
role: Role;
roleColors: RoleColors;
scheduledEvent: ScheduledEvent;
scheduledEventRecurrenceRule: ScheduledEventRecurrenceRule;
sku: Sku;
soundboardSound: SoundboardSound;
stageInstance: StageInstance;
sticker: Sticker;
subscription: Subscription;
unfurledMediaItem: UnfurledMediaItem;
user: User;
userPrimaryGuild: UserPrimaryGuild;
voiceState: VoiceState;
webhook: Webhook;
}
// NOTE: the top-level objects need both the dependencies and alwaysPresents even if empty when the key is specified, this is due the extends & nullability on DesiredPropertiesMetadata
@@ -130,107 +130,107 @@ export interface TransformersObjects {
export interface TransformersDesiredPropertiesMetadata extends DesiredPropertiesMetadata {
channel: {
dependencies: {
archived: ['toggles']
invitable: ['toggles']
locked: ['toggles']
nsfw: ['toggles']
newlyCreated: ['toggles']
managed: ['toggles']
}
alwaysPresents: ['toggles', 'internalOverwrites', 'internalThreadMetadata']
}
archived: ['toggles'];
invitable: ['toggles'];
locked: ['toggles'];
nsfw: ['toggles'];
newlyCreated: ['toggles'];
managed: ['toggles'];
};
alwaysPresents: ['toggles', 'internalOverwrites', 'internalThreadMetadata'];
};
guild: {
dependencies: {
threads: ['channels']
features: ['toggles']
}
alwaysPresents: []
}
threads: ['channels'];
features: ['toggles'];
};
alwaysPresents: [];
};
interaction: {
dependencies: {
respond: ['type', 'token', 'id']
edit: ['type', 'token', 'id']
deferEdit: ['type', 'token', 'id']
defer: ['type', 'token', 'id']
delete: ['type', 'token']
}
alwaysPresents: ['bot', 'acknowledged']
}
respond: ['type', 'token', 'id'];
edit: ['type', 'token', 'id'];
deferEdit: ['type', 'token', 'id'];
defer: ['type', 'token', 'id'];
delete: ['type', 'token'];
};
alwaysPresents: ['bot', 'acknowledged'];
};
member: {
dependencies: {
deaf: ['toggles']
mute: ['toggles']
pending: ['toggles']
flags: ['toggles']
didRejoin: ['toggles']
startedOnboarding: ['toggles']
bypassesVerification: ['toggles']
completedOnboarding: ['toggles']
}
alwaysPresents: []
}
deaf: ['toggles'];
mute: ['toggles'];
pending: ['toggles'];
flags: ['toggles'];
didRejoin: ['toggles'];
startedOnboarding: ['toggles'];
bypassesVerification: ['toggles'];
completedOnboarding: ['toggles'];
};
alwaysPresents: [];
};
message: {
dependencies: {
crossposted: ['flags']
ephemeral: ['flags']
failedToMentionSomeRolesInThread: ['flags']
hasThread: ['flags']
isCrosspost: ['flags']
loading: ['flags']
mentionedUserIds: ['mentions']
mentionEveryone: ['bitfield']
pinned: ['bitfield']
sourceMessageDeleted: ['flags']
suppressEmbeds: ['flags']
suppressNotifications: ['flags']
timestamp: ['id']
tts: ['bitfield']
urgent: ['flags']
}
alwaysPresents: ['bitfield', 'flags']
}
crossposted: ['flags'];
ephemeral: ['flags'];
failedToMentionSomeRolesInThread: ['flags'];
hasThread: ['flags'];
isCrosspost: ['flags'];
loading: ['flags'];
mentionedUserIds: ['mentions'];
mentionEveryone: ['bitfield'];
pinned: ['bitfield'];
sourceMessageDeleted: ['flags'];
suppressEmbeds: ['flags'];
suppressNotifications: ['flags'];
timestamp: ['id'];
tts: ['bitfield'];
urgent: ['flags'];
};
alwaysPresents: ['bitfield', 'flags'];
};
role: {
dependencies: {
hoist: ['toggles']
managed: ['toggles']
mentionable: ['toggles']
premiumSubscriber: ['toggles']
availableForPurchase: ['toggles']
guildConnections: ['toggles']
}
alwaysPresents: ['internalTags']
}
hoist: ['toggles'];
managed: ['toggles'];
mentionable: ['toggles'];
premiumSubscriber: ['toggles'];
availableForPurchase: ['toggles'];
guildConnections: ['toggles'];
};
alwaysPresents: ['internalTags'];
};
user: {
dependencies: {
tag: ['username', 'discriminator']
bot: ['toggles']
system: ['toggles']
mfaEnabled: ['toggles']
verified: ['toggles']
avatarUrl: ['avatar', 'id']
displayName: ['username', 'globalName']
defaultAvatarUrl: ['id', 'discriminator']
displayAvatarUrl: ['avatar', 'id', 'discriminator']
createdTimestamp: ['id']
}
alwaysPresents: []
}
tag: ['username', 'discriminator'];
bot: ['toggles'];
system: ['toggles'];
mfaEnabled: ['toggles'];
verified: ['toggles'];
avatarUrl: ['avatar', 'id'];
displayName: ['username', 'globalName'];
defaultAvatarUrl: ['id', 'discriminator'];
displayAvatarUrl: ['avatar', 'id', 'discriminator'];
createdTimestamp: ['id'];
};
alwaysPresents: [];
};
emoji: {
dependencies: {
animated: ['toggles']
available: ['toggles']
managed: ['toggles']
requireColons: ['toggles']
}
alwaysPresents: ['toggles']
}
animated: ['toggles'];
available: ['toggles'];
managed: ['toggles'];
requireColons: ['toggles'];
};
alwaysPresents: ['toggles'];
};
}
export function createDesiredPropertiesObject<T extends RecursivePartial<TransformersDesiredProperties>, TDefault extends boolean = false>(
@@ -859,35 +859,35 @@ export function createDesiredPropertiesObject<T extends RecursivePartial<Transfo
flags: defaultValue,
...desiredProperties.lobbyMember,
},
} satisfies TransformersDesiredProperties as CompleteDesiredProperties<T, TDefault>
} satisfies TransformersDesiredProperties as CompleteDesiredProperties<T, TDefault>;
}
/** @private This is subject to breaking changes without notices */
export type KeyByValue<TObj, TValue> = {
[Key in keyof TObj]: TObj[Key] extends TValue ? Key : never
}[keyof TObj]
[Key in keyof TObj]: TObj[Key] extends TValue ? Key : never;
}[keyof TObj];
/** @private This is subject to breaking changes without notices */
export type Complete<TObj, TDefault> = {
[K in keyof TObj]-?: undefined extends TObj[K] ? TDefault : Exclude<TObj[K], undefined>
}
[K in keyof TObj]-?: undefined extends TObj[K] ? TDefault : Exclude<TObj[K], undefined>;
};
/** @private This is subject to breaking changes without notices */
export type JoinTuple<T extends string[], TDelimiter extends string> = T extends readonly [infer F extends string, ...infer R extends string[]]
? R['length'] extends 0
? F
: `${F}${TDelimiter}${JoinTuple<R, TDelimiter>}`
: ''
: '';
/** @private This is subject to breaking changes without notices */
export type DesiredPropertiesMetadata = {
[K in keyof TransformersObjects]: {
dependencies?: {
[Key in keyof TransformersObjects[K]]?: (keyof TransformersObjects[K])[]
}
alwaysPresents?: (keyof TransformersObjects[K])[]
}
}
[Key in keyof TransformersObjects[K]]?: (keyof TransformersObjects[K])[];
};
alwaysPresents?: (keyof TransformersObjects[K])[];
};
};
/** @private This is subject to breaking changes without notices */
export type DesirableProperties<
@@ -901,24 +901,24 @@ export type DesirableProperties<
| (keyof T extends NonNullable<TransformersDesiredPropertiesMetadata[TKey]['alwaysPresents']>[number]
? never
: NonNullable<TransformersDesiredPropertiesMetadata[TKey]['alwaysPresents']>[number])
>
>;
/** @private This is subject to breaking changes without notices */
export type DesiredPropertiesMapper<T extends TransformersObjects[keyof TransformersObjects]> = {
[Key in DesirableProperties<T>]: boolean
}
[Key in DesirableProperties<T>]: boolean;
};
declare const TypeErrorSymbol: unique symbol
declare const TypeErrorSymbol: unique symbol;
/** @private This is subject to breaking changes without notices */
export interface DesiredPropertiesError<T extends string> {
[TypeErrorSymbol]: T
[TypeErrorSymbol]: T;
}
/** @private This is subject to breaking changes without notices */
export type AreDependenciesSatisfied<T, TDependencies extends Record<string, string[]> | undefined, TProps> = {
[K in keyof T]: IsKeyDesired<T[K], TDependencies, TProps> extends true ? true : false
}
[K in keyof T]: IsKeyDesired<T[K], TDependencies, TProps> extends true ? true : false;
};
/** @private This is subject to breaking changes without notices */
export type IsKeyDesired<TKey, TDependencies extends Record<string, string[]> | undefined, TProps> = TKey extends keyof TProps // The key has a desired props?
@@ -937,7 +937,7 @@ export type IsKeyDesired<TKey, TDependencies extends Record<string, string[]> |
: // No, this is a key to not include
DesiredPropertiesError<`This property depends on the following properties: ${JoinTuple<NonNullable<TDependencies>[TKey], ', '>}. Not all of these props are set as desired in desiredProperties option in createBot(), so you can't use it. More info here: https://discordeno.js.org/desired-props`>
: // No, we include it but it does not have neither props nor dependencies
true
true;
/** The behavior it should be used when resolving an undesired property */
export enum DesiredPropertiesBehavior {
@@ -954,7 +954,7 @@ export type RemoveKeyIfUndesired<Key, T, TProps extends TransformersDesiredPrope
TProps[KeyByValue<TransformersObjects, T>]
> extends true
? Key
: never
: never;
/** @private This is subject to breaking changes without notices */
export type GetErrorWhenUndesired<
@@ -968,10 +968,10 @@ export type GetErrorWhenUndesired<
TransformersDesiredPropertiesMetadata[KeyByValue<TransformersObjects, T>]['dependencies'],
TProps[KeyByValue<TransformersObjects, T>]
>,
> = TIsDesired extends true ? TransformProperty<T[Key], TProps, TBehavior> : TIsDesired
> = TIsDesired extends true ? TransformProperty<T[Key], TProps, TBehavior> : TIsDesired;
/** @private This is subject to breaking changes without notices */
export type IsObject<T> = T extends object ? (T extends Function ? false : true) : false
export type IsObject<T> = T extends object ? (T extends Function ? false : true) : false;
// If the object is a transformed object, a collection of transformed object or an array of transformed objects we need to apply the desired props to them as well
// NOTE: changing the order of these ternaries can cause bugs, for this reason we check in this order:
@@ -1015,7 +1015,7 @@ export type TransformProperty<T, TProps extends TransformersDesiredProperties, T
? // Yes, we need to ensure nested inside there aren't transformed objects
{ [K in keyof T]: TransformProperty<T[K], TProps, TBehavior> }
: // No, this is a normal value such as string / bigint / number
T
T;
/**
* Apply desired properties to a transformer object.
@@ -1031,17 +1031,17 @@ export type SetupDesiredProps<
: Key]: // When the behavior is to change the type we use the GetErrorWhenUndesired type helper else apply the desired props to the key and return
TBehavior extends DesiredPropertiesBehavior.ChangeType
? GetErrorWhenUndesired<Key, T, TProps, TBehavior>
: TransformProperty<T[Key], TProps, TBehavior>
}
: TransformProperty<T[Key], TProps, TBehavior>;
};
/**
* The desired properties for each transformer object.
*/
export type TransformersDesiredProperties = {
[Key in keyof TransformersObjects]: DesiredPropertiesMapper<TransformersObjects[Key]>
}
[Key in keyof TransformersObjects]: DesiredPropertiesMapper<TransformersObjects[Key]>;
};
/** @private This is subject to breaking changes without notices */
export type CompleteDesiredProperties<T extends RecursivePartial<TransformersDesiredProperties>, TTDefault extends boolean = false> = {
[K in keyof TransformersDesiredProperties]: Complete<Partial<TransformersDesiredProperties[K]> & T[K], TTDefault>
}
[K in keyof TransformersDesiredProperties]: Complete<Partial<TransformersDesiredProperties[K]> & T[K], TTDefault>;
};

View File

@@ -1,6 +1,6 @@
import type { DiscordGatewayPayload, DiscordRateLimited, DiscordReady, DiscordVoiceChannelEffectAnimationType } from '@discordeno/types'
import type { Collection } from '@discordeno/utils'
import type { DesiredPropertiesBehavior, SetupDesiredProps, TransformersDesiredProperties } from './desiredProperties.js'
import type { DiscordGatewayPayload, DiscordRateLimited, DiscordReady, DiscordVoiceChannelEffectAnimationType } from '@discordeno/types';
import type { Collection } from '@discordeno/utils';
import type { DesiredPropertiesBehavior, SetupDesiredProps, TransformersDesiredProperties } from './desiredProperties.js';
import type {
AuditLogEntry,
AutoModerationActionExecution,
@@ -24,137 +24,137 @@ import type {
ThreadMember,
User,
VoiceState,
} from './transformers/types.js'
} from './transformers/types.js';
export type EventHandlers<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> = {
applicationCommandPermissionsUpdate: (command: GuildApplicationCommandPermissions) => unknown
guildAuditLogEntryCreate: (log: AuditLogEntry, guildId: bigint) => unknown
automodRuleCreate: (rule: AutoModerationRule) => unknown
automodRuleUpdate: (rule: AutoModerationRule) => unknown
automodRuleDelete: (rule: AutoModerationRule) => unknown
automodActionExecution: (payload: AutoModerationActionExecution) => unknown
threadCreate: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
threadDelete: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
applicationCommandPermissionsUpdate: (command: GuildApplicationCommandPermissions) => unknown;
guildAuditLogEntryCreate: (log: AuditLogEntry, guildId: bigint) => unknown;
automodRuleCreate: (rule: AutoModerationRule) => unknown;
automodRuleUpdate: (rule: AutoModerationRule) => unknown;
automodRuleDelete: (rule: AutoModerationRule) => unknown;
automodActionExecution: (payload: AutoModerationActionExecution) => unknown;
threadCreate: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
threadDelete: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
threadListSync: (payload: {
guildId: bigint
channelIds?: bigint[]
threads: SetupDesiredProps<Channel, TProps, TBehavior>[]
members: ThreadMember[]
}) => unknown
threadMemberUpdate: (payload: { id: bigint; guildId: bigint; joinedTimestamp: number; flags: number }) => unknown
threadMembersUpdate: (payload: { id: bigint; guildId: bigint; addedMembers?: ThreadMember[]; removedMemberIds?: bigint[] }) => unknown
threadUpdate: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
scheduledEventCreate: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown
scheduledEventUpdate: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown
scheduledEventDelete: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown
scheduledEventUserAdd: (payload: { guildScheduledEventId: bigint; guildId: bigint; userId: bigint }) => unknown
scheduledEventUserRemove: (payload: { guildScheduledEventId: bigint; guildId: bigint; userId: bigint }) => unknown
guildId: bigint;
channelIds?: bigint[];
threads: SetupDesiredProps<Channel, TProps, TBehavior>[];
members: ThreadMember[];
}) => unknown;
threadMemberUpdate: (payload: { id: bigint; guildId: bigint; joinedTimestamp: number; flags: number }) => unknown;
threadMembersUpdate: (payload: { id: bigint; guildId: bigint; addedMembers?: ThreadMember[]; removedMemberIds?: bigint[] }) => unknown;
threadUpdate: (thread: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
scheduledEventCreate: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown;
scheduledEventUpdate: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown;
scheduledEventDelete: (event: SetupDesiredProps<ScheduledEvent, TProps, TBehavior>) => unknown;
scheduledEventUserAdd: (payload: { guildScheduledEventId: bigint; guildId: bigint; userId: bigint }) => unknown;
scheduledEventUserRemove: (payload: { guildScheduledEventId: bigint; guildId: bigint; userId: bigint }) => unknown;
ready: (
payload: {
shardId: number
v: number
user: SetupDesiredProps<User, TProps, TBehavior>
guilds: bigint[]
sessionId: string
shard?: number[]
applicationId: bigint
shardId: number;
v: number;
user: SetupDesiredProps<User, TProps, TBehavior>;
guilds: bigint[];
sessionId: string;
shard?: number[];
applicationId: bigint;
},
rawPayload: DiscordReady,
) => unknown
resumed: (shardId: number) => unknown
rateLimited: (data: DiscordRateLimited, shardId: number) => unknown
interactionCreate: (interaction: SetupDesiredProps<Interaction, TProps, TBehavior>) => unknown
integrationCreate: (integration: Integration) => unknown
integrationDelete: (payload: { id: bigint; guildId: bigint; applicationId?: bigint }) => unknown
integrationUpdate: (payload: { guildId: bigint }) => unknown
inviteCreate: (invite: SetupDesiredProps<Invite, TProps, TBehavior>) => unknown
inviteDelete: (payload: { channelId: bigint; guildId?: bigint; code: string }) => unknown
guildMemberAdd: (member: SetupDesiredProps<Member, TProps, TBehavior>, user: SetupDesiredProps<User, TProps, TBehavior>) => unknown
guildMemberRemove: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown
guildMemberUpdate: (member: SetupDesiredProps<Member, TProps, TBehavior>, user: SetupDesiredProps<User, TProps, TBehavior>) => unknown
guildStickersUpdate: (payload: { guildId: bigint; stickers: SetupDesiredProps<Sticker, TProps, TBehavior>[] }) => unknown
messageCreate: (message: SetupDesiredProps<Message, TProps, TBehavior>) => unknown
messageDelete: (payload: { id: bigint; channelId: bigint; guildId?: bigint }, message?: SetupDesiredProps<Message, TProps, TBehavior>) => unknown
messageDeleteBulk: (payload: { ids: bigint[]; channelId: bigint; guildId?: bigint }) => unknown
messageUpdate: (message: SetupDesiredProps<Message, TProps, TBehavior>) => unknown
) => unknown;
resumed: (shardId: number) => unknown;
rateLimited: (data: DiscordRateLimited, shardId: number) => unknown;
interactionCreate: (interaction: SetupDesiredProps<Interaction, TProps, TBehavior>) => unknown;
integrationCreate: (integration: Integration) => unknown;
integrationDelete: (payload: { id: bigint; guildId: bigint; applicationId?: bigint }) => unknown;
integrationUpdate: (payload: { guildId: bigint }) => unknown;
inviteCreate: (invite: SetupDesiredProps<Invite, TProps, TBehavior>) => unknown;
inviteDelete: (payload: { channelId: bigint; guildId?: bigint; code: string }) => unknown;
guildMemberAdd: (member: SetupDesiredProps<Member, TProps, TBehavior>, user: SetupDesiredProps<User, TProps, TBehavior>) => unknown;
guildMemberRemove: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown;
guildMemberUpdate: (member: SetupDesiredProps<Member, TProps, TBehavior>, user: SetupDesiredProps<User, TProps, TBehavior>) => unknown;
guildStickersUpdate: (payload: { guildId: bigint; stickers: SetupDesiredProps<Sticker, TProps, TBehavior>[] }) => unknown;
messageCreate: (message: SetupDesiredProps<Message, TProps, TBehavior>) => unknown;
messageDelete: (payload: { id: bigint; channelId: bigint; guildId?: bigint }, message?: SetupDesiredProps<Message, TProps, TBehavior>) => unknown;
messageDeleteBulk: (payload: { ids: bigint[]; channelId: bigint; guildId?: bigint }) => unknown;
messageUpdate: (message: SetupDesiredProps<Message, TProps, TBehavior>) => unknown;
reactionAdd: (payload: {
userId: bigint
channelId: bigint
messageId: bigint
guildId?: bigint
member?: SetupDesiredProps<Member, TProps, TBehavior>
user?: SetupDesiredProps<User, TProps, TBehavior>
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>
messageAuthorId?: bigint
burst: boolean
burstColors?: string[]
}) => unknown
userId: bigint;
channelId: bigint;
messageId: bigint;
guildId?: bigint;
member?: SetupDesiredProps<Member, TProps, TBehavior>;
user?: SetupDesiredProps<User, TProps, TBehavior>;
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>;
messageAuthorId?: bigint;
burst: boolean;
burstColors?: string[];
}) => unknown;
reactionRemove: (payload: {
userId: bigint
channelId: bigint
messageId: bigint
guildId?: bigint
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>
burst: boolean
}) => unknown
userId: bigint;
channelId: bigint;
messageId: bigint;
guildId?: bigint;
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>;
burst: boolean;
}) => unknown;
reactionRemoveEmoji: (payload: {
channelId: bigint
messageId: bigint
guildId?: bigint
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>
}) => unknown
reactionRemoveAll: (payload: { channelId: bigint; messageId: bigint; guildId?: bigint }) => unknown
presenceUpdate: (presence: PresenceUpdate) => unknown
channelId: bigint;
messageId: bigint;
guildId?: bigint;
emoji: SetupDesiredProps<Emoji, TProps, TBehavior>;
}) => unknown;
reactionRemoveAll: (payload: { channelId: bigint; messageId: bigint; guildId?: bigint }) => unknown;
presenceUpdate: (presence: PresenceUpdate) => unknown;
voiceChannelEffectSend: (payload: {
channelId: bigint
guildId: bigint
userId: bigint
emoji?: SetupDesiredProps<Emoji, TProps, TBehavior>
animationType?: DiscordVoiceChannelEffectAnimationType
animationId?: number
soundId?: bigint | number
soundVolume?: number
}) => unknown
voiceServerUpdate: (payload: { token: string; endpoint?: string; guildId: bigint }) => unknown
voiceStateUpdate: (voiceState: SetupDesiredProps<VoiceState, TProps, TBehavior>) => unknown
channelCreate: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
dispatchRequirements: (data: DiscordGatewayPayload, shardId: number) => unknown
channelDelete: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
channelPinsUpdate: (data: { guildId?: bigint; channelId: bigint; lastPinTimestamp?: number }) => unknown
channelUpdate: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown
stageInstanceCreate: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown
stageInstanceDelete: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown
stageInstanceUpdate: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown
guildEmojisUpdate: (payload: { guildId: bigint; emojis: Collection<bigint, SetupDesiredProps<Emoji, TProps, TBehavior>> }) => unknown
guildBanAdd: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown
guildBanRemove: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown
guildCreate: (guild: SetupDesiredProps<Guild, TProps, TBehavior>) => unknown
guildDelete: (data: { id: bigint; unavailable: boolean }, shardId: number) => unknown
guildUpdate: (guild: SetupDesiredProps<Guild, TProps, TBehavior>) => unknown
raw: (data: DiscordGatewayPayload, shardId: number) => unknown
roleCreate: (role: SetupDesiredProps<Role, TProps, TBehavior>) => unknown
roleDelete: (payload: { guildId: bigint; roleId: bigint }) => unknown
roleUpdate: (role: SetupDesiredProps<Role, TProps, TBehavior>) => unknown
webhooksUpdate: (payload: { channelId: bigint; guildId: bigint }) => unknown
botUpdate: (user: SetupDesiredProps<User, TProps, TBehavior>) => unknown
channelId: bigint;
guildId: bigint;
userId: bigint;
emoji?: SetupDesiredProps<Emoji, TProps, TBehavior>;
animationType?: DiscordVoiceChannelEffectAnimationType;
animationId?: number;
soundId?: bigint | number;
soundVolume?: number;
}) => unknown;
voiceServerUpdate: (payload: { token: string; endpoint?: string; guildId: bigint }) => unknown;
voiceStateUpdate: (voiceState: SetupDesiredProps<VoiceState, TProps, TBehavior>) => unknown;
channelCreate: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
dispatchRequirements: (data: DiscordGatewayPayload, shardId: number) => unknown;
channelDelete: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
channelPinsUpdate: (data: { guildId?: bigint; channelId: bigint; lastPinTimestamp?: number }) => unknown;
channelUpdate: (channel: SetupDesiredProps<Channel, TProps, TBehavior>) => unknown;
stageInstanceCreate: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown;
stageInstanceDelete: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown;
stageInstanceUpdate: (data: { id: bigint; guildId: bigint; channelId: bigint; topic: string }) => unknown;
guildEmojisUpdate: (payload: { guildId: bigint; emojis: Collection<bigint, SetupDesiredProps<Emoji, TProps, TBehavior>> }) => unknown;
guildBanAdd: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown;
guildBanRemove: (user: SetupDesiredProps<User, TProps, TBehavior>, guildId: bigint) => unknown;
guildCreate: (guild: SetupDesiredProps<Guild, TProps, TBehavior>) => unknown;
guildDelete: (data: { id: bigint; unavailable: boolean }, shardId: number) => unknown;
guildUpdate: (guild: SetupDesiredProps<Guild, TProps, TBehavior>) => unknown;
raw: (data: DiscordGatewayPayload, shardId: number) => unknown;
roleCreate: (role: SetupDesiredProps<Role, TProps, TBehavior>) => unknown;
roleDelete: (payload: { guildId: bigint; roleId: bigint }) => unknown;
roleUpdate: (role: SetupDesiredProps<Role, TProps, TBehavior>) => unknown;
webhooksUpdate: (payload: { channelId: bigint; guildId: bigint }) => unknown;
botUpdate: (user: SetupDesiredProps<User, TProps, TBehavior>) => unknown;
typingStart: (payload: {
guildId: bigint | undefined
channelId: bigint
userId: bigint
timestamp: number
member: SetupDesiredProps<Member, TProps, TBehavior> | undefined
}) => unknown
entitlementCreate: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown
entitlementUpdate: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown
entitlementDelete: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown
subscriptionCreate: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown
subscriptionUpdate: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown
subscriptionDelete: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown
messagePollVoteAdd: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown
messagePollVoteRemove: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown
soundboardSoundCreate: (payload: SetupDesiredProps<SoundboardSound, TProps, TBehavior>) => unknown
soundboardSoundUpdate: (payload: SetupDesiredProps<SoundboardSound, TProps, TBehavior>) => unknown
soundboardSoundDelete: (payload: { soundId: bigint; guildId: bigint }) => unknown
soundboardSoundsUpdate: (payload: { soundboardSounds: SetupDesiredProps<SoundboardSound, TProps, TBehavior>[]; guildId: bigint }) => unknown
soundboardSounds: (payload: { soundboardSounds: SetupDesiredProps<SoundboardSound, TProps, TBehavior>[]; guildId: bigint }) => unknown
}
guildId: bigint | undefined;
channelId: bigint;
userId: bigint;
timestamp: number;
member: SetupDesiredProps<Member, TProps, TBehavior> | undefined;
}) => unknown;
entitlementCreate: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown;
entitlementUpdate: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown;
entitlementDelete: (entitlement: SetupDesiredProps<Entitlement, TProps, TBehavior>) => unknown;
subscriptionCreate: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown;
subscriptionUpdate: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown;
subscriptionDelete: (subscription: SetupDesiredProps<Subscription, TProps, TBehavior>) => unknown;
messagePollVoteAdd: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown;
messagePollVoteRemove: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown;
soundboardSoundCreate: (payload: SetupDesiredProps<SoundboardSound, TProps, TBehavior>) => unknown;
soundboardSoundUpdate: (payload: SetupDesiredProps<SoundboardSound, TProps, TBehavior>) => unknown;
soundboardSoundDelete: (payload: { soundId: bigint; guildId: bigint }) => unknown;
soundboardSoundsUpdate: (payload: { soundboardSounds: SetupDesiredProps<SoundboardSound, TProps, TBehavior>[]; guildId: bigint }) => unknown;
soundboardSounds: (payload: { soundboardSounds: SetupDesiredProps<SoundboardSound, TProps, TBehavior>[]; guildId: bigint }) => unknown;
};

View File

@@ -1,12 +1,12 @@
import type { DiscordGatewayPayload, GatewayDispatchEventNames } from '@discordeno/types'
import type { Bot } from './bot.js'
import type { DesiredPropertiesBehavior, TransformersDesiredProperties } from './desiredProperties.js'
import * as handlers from './handlers/index.js'
import type { DiscordGatewayPayload, GatewayDispatchEventNames } from '@discordeno/types';
import type { Bot } from './bot.js';
import type { DesiredPropertiesBehavior, TransformersDesiredProperties } from './desiredProperties.js';
import * as handlers from './handlers/index.js';
export function createBotGatewayHandlers<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior>(
options: Partial<GatewayHandlers<TProps, TBehavior>>,
): GatewayHandlers<TProps, TBehavior> {
const _options = options as Partial<GatewayHandlers<TransformersDesiredProperties, DesiredPropertiesBehavior.RemoveKey>>
const _options = options as Partial<GatewayHandlers<TransformersDesiredProperties, DesiredPropertiesBehavior.RemoveKey>>;
return {
APPLICATION_COMMAND_PERMISSIONS_UPDATE: _options.APPLICATION_COMMAND_PERMISSIONS_UPDATE ?? handlers.handleApplicationCommandPermissionsUpdate,
@@ -85,16 +85,16 @@ export function createBotGatewayHandlers<TProps extends TransformersDesiredPrope
GUILD_SOUNDBOARD_SOUND_UPDATE: _options.GUILD_SOUNDBOARD_SOUND_UPDATE ?? handlers.handleGuildSoundboardSoundUpdate,
GUILD_SOUNDBOARD_SOUNDS_UPDATE: _options.GUILD_SOUNDBOARD_SOUNDS_UPDATE ?? handlers.handleGuildSoundboardSoundsUpdate,
SOUNDBOARD_SOUNDS: _options.SOUNDBOARD_SOUNDS ?? handlers.handleSoundboardSounds,
} satisfies GatewayHandlers<TransformersDesiredProperties, DesiredPropertiesBehavior.RemoveKey> as unknown as GatewayHandlers<TProps, TBehavior>
} satisfies GatewayHandlers<TransformersDesiredProperties, DesiredPropertiesBehavior.RemoveKey> as unknown as GatewayHandlers<TProps, TBehavior>;
}
export type GatewayHandlers<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> = Record<
GatewayDispatchEventNames,
BotGatewayHandler<TProps, TBehavior>
>
>;
export type BotGatewayHandler<TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior> = (
bot: Bot<TProps, TBehavior>,
data: DiscordGatewayPayload,
shardId: number,
) => unknown
) => unknown;

View File

@@ -1,11 +1,11 @@
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleChannelCreate(bot: Bot, payload: DiscordGatewayPayload, _shardId: number): Promise<void> {
if (!bot.events.channelCreate) return
if (!bot.events.channelCreate) return;
const data = payload.d as DiscordChannel
const channel = bot.transformers.channel(bot, data, { guildId: data.guild_id })
const data = payload.d as DiscordChannel;
const channel = bot.transformers.channel(bot, data, { guildId: data.guild_id });
bot.events.channelCreate(channel)
bot.events.channelCreate(channel);
}

View File

@@ -1,10 +1,10 @@
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleChannelDelete(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.channelDelete) return
if (!bot.events.channelDelete) return;
const payload = data.d as DiscordChannel
const payload = data.d as DiscordChannel;
bot.events.channelDelete(bot.transformers.channel(bot, payload, { guildId: payload.guild_id }))
bot.events.channelDelete(bot.transformers.channel(bot, payload, { guildId: payload.guild_id }));
}

View File

@@ -1,14 +1,14 @@
import type { DiscordChannelPinsUpdate, DiscordGatewayPayload } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordChannelPinsUpdate, DiscordGatewayPayload } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleChannelPinsUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.channelPinsUpdate) return
if (!bot.events.channelPinsUpdate) return;
const payload = data.d as DiscordChannelPinsUpdate
const payload = data.d as DiscordChannelPinsUpdate;
bot.events.channelPinsUpdate({
guildId: payload.guild_id ? bot.transformers.snowflake(payload.guild_id) : undefined,
channelId: bot.transformers.snowflake(payload.channel_id),
lastPinTimestamp: payload.last_pin_timestamp ? Date.parse(payload.last_pin_timestamp) : undefined,
})
});
}

View File

@@ -1,11 +1,11 @@
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleChannelUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.channelUpdate) return
if (!bot.events.channelUpdate) return;
const payload = data.d as DiscordChannel
const channel = bot.transformers.channel(bot, payload)
const payload = data.d as DiscordChannel;
const channel = bot.transformers.channel(bot, payload);
bot.events.channelUpdate(channel)
bot.events.channelUpdate(channel);
}

View File

@@ -1,15 +1,15 @@
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleStageInstanceCreate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.stageInstanceCreate) return
if (!bot.events.stageInstanceCreate) return;
const payload = data.d as DiscordStageInstance
const payload = data.d as DiscordStageInstance;
bot.events.stageInstanceCreate({
id: bot.transformers.snowflake(payload.id),
guildId: bot.transformers.snowflake(payload.guild_id),
channelId: bot.transformers.snowflake(payload.channel_id),
topic: payload.topic,
})
});
}

View File

@@ -1,15 +1,15 @@
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleStageInstanceDelete(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.stageInstanceDelete) return
if (!bot.events.stageInstanceDelete) return;
const payload = data.d as DiscordStageInstance
const payload = data.d as DiscordStageInstance;
bot.events.stageInstanceDelete({
id: bot.transformers.snowflake(payload.id),
guildId: bot.transformers.snowflake(payload.guild_id),
channelId: bot.transformers.snowflake(payload.channel_id),
topic: payload.topic,
})
});
}

View File

@@ -1,15 +1,15 @@
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordGatewayPayload, DiscordStageInstance } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleStageInstanceUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.stageInstanceUpdate) return
if (!bot.events.stageInstanceUpdate) return;
const payload = data.d as DiscordStageInstance
const payload = data.d as DiscordStageInstance;
bot.events.stageInstanceUpdate({
id: bot.transformers.snowflake(payload.id),
guildId: bot.transformers.snowflake(payload.guild_id),
channelId: bot.transformers.snowflake(payload.channel_id),
topic: payload.topic,
})
});
}

View File

@@ -1,10 +1,10 @@
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleThreadCreate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.threadCreate) return
if (!bot.events.threadCreate) return;
const payload = data.d as DiscordChannel
const payload = data.d as DiscordChannel;
bot.events.threadCreate(bot.transformers.channel(bot, payload))
bot.events.threadCreate(bot.transformers.channel(bot, payload));
}

View File

@@ -1,10 +1,10 @@
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleThreadDelete(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.threadDelete) return
if (!bot.events.threadDelete) return;
const payload = data.d as DiscordChannel
const payload = data.d as DiscordChannel;
bot.events.threadDelete(bot.transformers.channel(bot, payload))
bot.events.threadDelete(bot.transformers.channel(bot, payload));
}

View File

@@ -1,12 +1,12 @@
import type { DiscordGatewayPayload, DiscordThreadListSync } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordGatewayPayload, DiscordThreadListSync } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleThreadListSync(bot: Bot, data: DiscordGatewayPayload): Promise<any> {
if (!bot.events.threadListSync) return
if (!bot.events.threadListSync) return;
const payload = data.d as DiscordThreadListSync
const payload = data.d as DiscordThreadListSync;
const guildId = bot.transformers.snowflake(payload.guild_id)
const guildId = bot.transformers.snowflake(payload.guild_id);
bot.events.threadListSync({
guildId,
@@ -18,5 +18,5 @@ export async function handleThreadListSync(bot: Bot, data: DiscordGatewayPayload
joinTimestamp: Date.parse(member.join_timestamp),
flags: member.flags,
})),
})
});
}

View File

@@ -1,15 +1,15 @@
import type { DiscordGatewayPayload, DiscordThreadMembersUpdate } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordGatewayPayload, DiscordThreadMembersUpdate } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleThreadMembersUpdate(bot: Bot, data: DiscordGatewayPayload, _shardId: number): Promise<void> {
if (!bot.events.threadMembersUpdate) return
if (!bot.events.threadMembersUpdate) return;
const payload = data.d as DiscordThreadMembersUpdate
const payload = data.d as DiscordThreadMembersUpdate;
bot.events.threadMembersUpdate({
id: bot.transformers.snowflake(payload.id),
guildId: bot.transformers.snowflake(payload.guild_id),
addedMembers: payload.added_members?.map((member) => bot.transformers.threadMember?.(bot, member, { guildId: payload.guild_id })),
removedMemberIds: payload.removed_member_ids?.map((id) => bot.transformers.snowflake(id)),
})
});
}

View File

@@ -1,15 +1,15 @@
import type { DiscordGatewayPayload, DiscordThreadMemberUpdate } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordGatewayPayload, DiscordThreadMemberUpdate } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleThreadMemberUpdate(bot: Bot, data: DiscordGatewayPayload, _shardId: number): Promise<void> {
if (!bot.events.threadMemberUpdate) return
if (!bot.events.threadMemberUpdate) return;
const payload = data.d as DiscordThreadMemberUpdate
const payload = data.d as DiscordThreadMemberUpdate;
bot.events.threadMemberUpdate({
id: bot.transformers.snowflake(payload.id),
guildId: bot.transformers.snowflake(payload.guild_id),
joinedTimestamp: Date.parse(payload.join_timestamp),
flags: payload.flags,
})
});
}

View File

@@ -1,10 +1,10 @@
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordChannel, DiscordGatewayPayload } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleThreadUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.threadUpdate) return
if (!bot.events.threadUpdate) return;
const payload = data.d as DiscordChannel
const payload = data.d as DiscordChannel;
bot.events.threadUpdate(bot.transformers.channel(bot, payload))
bot.events.threadUpdate(bot.transformers.channel(bot, payload));
}

View File

@@ -1,13 +1,13 @@
export * from './CHANNEL_CREATE.js'
export * from './CHANNEL_DELETE.js'
export * from './CHANNEL_PINS_UPDATE.js'
export * from './CHANNEL_UPDATE.js'
export * from './STAGE_INSTANCE_CREATE.js'
export * from './STAGE_INSTANCE_DELETE.js'
export * from './STAGE_INSTANCE_UPDATE.js'
export * from './THREAD_CREATE.js'
export * from './THREAD_DELETE.js'
export * from './THREAD_LIST_SYNC.js'
export * from './THREAD_MEMBER_UPDATE.js'
export * from './THREAD_MEMBERS_UPDATE.js'
export * from './THREAD_UPDATE.js'
export * from './CHANNEL_CREATE.js';
export * from './CHANNEL_DELETE.js';
export * from './CHANNEL_PINS_UPDATE.js';
export * from './CHANNEL_UPDATE.js';
export * from './STAGE_INSTANCE_CREATE.js';
export * from './STAGE_INSTANCE_DELETE.js';
export * from './STAGE_INSTANCE_UPDATE.js';
export * from './THREAD_CREATE.js';
export * from './THREAD_DELETE.js';
export * from './THREAD_LIST_SYNC.js';
export * from './THREAD_MEMBER_UPDATE.js';
export * from './THREAD_MEMBERS_UPDATE.js';
export * from './THREAD_UPDATE.js';

View File

@@ -1,14 +1,14 @@
import type { DiscordGatewayPayload, DiscordGuildEmojisUpdate } from '@discordeno/types'
import { Collection } from '@discordeno/utils'
import type { Bot } from '../../bot.js'
import type { DiscordGatewayPayload, DiscordGuildEmojisUpdate } from '@discordeno/types';
import { Collection } from '@discordeno/utils';
import type { Bot } from '../../bot.js';
export async function handleGuildEmojisUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.guildEmojisUpdate) return
if (!bot.events.guildEmojisUpdate) return;
const payload = data.d as DiscordGuildEmojisUpdate
const payload = data.d as DiscordGuildEmojisUpdate;
bot.events.guildEmojisUpdate({
guildId: bot.transformers.snowflake(payload.guild_id),
emojis: new Collection(payload.emojis.map((emoji) => [bot.transformers.snowflake(emoji.id!), bot.transformers.emoji(bot, emoji)])),
})
});
}

View File

@@ -1 +1 @@
export * from './GUILD_EMOJIS_UPDATE.js'
export * from './GUILD_EMOJIS_UPDATE.js';

View File

@@ -1,9 +1,9 @@
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleEntitlementCreate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.entitlementCreate) return
if (!bot.events.entitlementCreate) return;
const payload = data.d as DiscordEntitlement
bot.events.entitlementCreate(bot.transformers.entitlement(bot, payload))
const payload = data.d as DiscordEntitlement;
bot.events.entitlementCreate(bot.transformers.entitlement(bot, payload));
}

View File

@@ -1,9 +1,9 @@
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleEntitlementDelete(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.entitlementDelete) return
if (!bot.events.entitlementDelete) return;
const payload = data.d as DiscordEntitlement
bot.events.entitlementDelete(bot.transformers.entitlement(bot, payload))
const payload = data.d as DiscordEntitlement;
bot.events.entitlementDelete(bot.transformers.entitlement(bot, payload));
}

View File

@@ -1,9 +1,9 @@
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordEntitlement, DiscordGatewayPayload } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleEntitlementUpdate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.entitlementUpdate) return
if (!bot.events.entitlementUpdate) return;
const payload = data.d as DiscordEntitlement
bot.events.entitlementUpdate(bot.transformers.entitlement(bot, payload))
const payload = data.d as DiscordEntitlement;
bot.events.entitlementUpdate(bot.transformers.entitlement(bot, payload));
}

View File

@@ -1,3 +1,3 @@
export * from './ENTITLEMENT_CREATE.js'
export * from './ENTITLEMENT_DELETE.js'
export * from './ENTITLEMENT_UPDATE.js'
export * from './ENTITLEMENT_CREATE.js';
export * from './ENTITLEMENT_DELETE.js';
export * from './ENTITLEMENT_UPDATE.js';

View File

@@ -1,10 +1,10 @@
import type { DiscordAuditLogEntry, DiscordGatewayPayload } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordAuditLogEntry, DiscordGatewayPayload } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleGuildAuditLogEntryCreate(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.guildAuditLogEntryCreate) return
if (!bot.events.guildAuditLogEntryCreate) return;
// TODO: better type here
const payload = data.d as DiscordAuditLogEntry & { guild_id: string }
bot.events.guildAuditLogEntryCreate(bot.transformers.auditLogEntry(bot, payload), bot.transformers.snowflake(payload.guild_id))
const payload = data.d as DiscordAuditLogEntry & { guild_id: string };
bot.events.guildAuditLogEntryCreate(bot.transformers.auditLogEntry(bot, payload), bot.transformers.snowflake(payload.guild_id));
}

View File

@@ -1,9 +1,9 @@
import type { DiscordGatewayPayload, DiscordGuildBanAddRemove } from '@discordeno/types'
import type { Bot } from '../../bot.js'
import type { DiscordGatewayPayload, DiscordGuildBanAddRemove } from '@discordeno/types';
import type { Bot } from '../../bot.js';
export async function handleGuildBanAdd(bot: Bot, data: DiscordGatewayPayload): Promise<void> {
if (!bot.events.guildBanAdd) return
if (!bot.events.guildBanAdd) return;
const payload = data.d as DiscordGuildBanAddRemove
bot.events.guildBanAdd(bot.transformers.user(bot, payload.user), bot.transformers.snowflake(payload.guild_id))
const payload = data.d as DiscordGuildBanAddRemove;
bot.events.guildBanAdd(bot.transformers.user(bot, payload.user), bot.transformers.snowflake(payload.guild_id));
}

Some files were not shown because too many files have changed in this diff Show More