diff --git a/src/bot.ts b/src/bot.ts index 6a26e58e0..25bf69bdc 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,4 +1,10 @@ import { getGatewayBot } from "./helpers/misc/get_gateway_bot.ts"; +import { + DiscordGatewayIntents, + DiscordGetGatewayBot, + DiscordIdentify, +} from "./types/gateway.ts"; +import { CamelCaseProps } from "./types/util.ts"; import { baseEndpoints, GATEWAY_VERSION } from "./util/constants.ts"; import { spawnShards } from "./ws/shard_manager.ts"; @@ -9,13 +15,14 @@ export let applicationID = ""; export let eventHandlers: EventHandlers = {}; -export let botGatewayData: DiscordBotGatewayData; +export let botGatewayData: DiscordGetGatewayBot; export let proxyWSURL = `wss://gateway.discord.gg`; export let lastShardID = 0; export const identifyPayload: DiscordIdentify = { token: "", compress: true, + large_threshold: 50, properties: { $os: "linux", $browser: "Discordeno", @@ -38,7 +45,12 @@ export async function startBot(config: BotConfig) { proxyWSURL = botGatewayData.url; identifyPayload.token = config.token; identifyPayload.intents = config.intents.reduce( - (bits, next) => (bits |= typeof next === "string" ? Intents[next] : next), + ( + bits, + next, + ) => (bits |= typeof next === "string" + ? DiscordGatewayIntents[next] + : next), 0, ); lastShardID = botGatewayData.shards; @@ -87,7 +99,12 @@ export async function startBigBrainBot(data: BigBrainBotConfig) { } identifyPayload.intents = data.intents.reduce( - (bits, next) => (bits |= typeof next === "string" ? Intents[next] : next), + ( + bits, + next, + ) => (bits |= typeof next === "string" + ? DiscordGatewayIntents[next] + : next), 0, ); @@ -105,3 +122,195 @@ export async function startBigBrainBot(data: BigBrainBotConfig) { : botGatewayData.shards), ); } + +export interface EventHandlers { + rateLimit?: (data: RateLimitData) => unknown; + /** Sent when a new Slash Command is created, relevant to the current user. */ + applicationCommandCreate?: ( + data: CamelCaseProps, + ) => unknown; + /** Sent when a Slash Command relevant to the current user is updated. */ + applicationCommandUpdate?: ( + data: CamelCaseProps, + ) => unknown; + /** Sent when a Slash Command relevant to the current user is deleted. */ + applicationCommandDelete?: ( + data: CamelCaseProps, + ) => unknown; + /** Sent when properties about the user change. */ + botUpdate?: (user: UserPayload) => unknown; + /** Sent when a new guild channel is created, relevant to the current user. */ + channelCreate?: (channel: Channel) => unknown; + /** Sent when a channel is updated. This is not sent when the field `last_message_id` is altered. To keep track of the `last_message_id` changes, you must listen for `MESSAGE_CREATE` events. */ + channelUpdate?: (channel: Channel, cachedChannel: Channel) => unknown; + /** Sent when a channel relevant to the current user is deleted. */ + channelDelete?: (channel: Channel) => unknown; + /** Sent when a message is pinned or unpinned in a text channel. This is not sent when a pinned message is deleted. */ + channelPinsUpdate?: ( + channel: Channel, + guild?: Guild, + lastPinTimestamp?: string | null, + ) => unknown; + debug?: (args: DebugArg) => unknown; + dispatchRequirements?: (data: DiscordPayload, shardID: number) => unknown; + /** Sent when a user is banned from a guild. */ + guildBanAdd?: (guild: Guild, user: UserPayload, member?: Member) => unknown; + /** Sent when a user is unbanned from a guild. */ + guildBanRemove?: ( + guild: Guild, + user: UserPayload, + member?: Member, + ) => unknown; + /** + * This event can be sent in three different scenarios: + * 1. When a user is initially connecting, to lazily load and backfill information for all unavailable guilds sent in the `READY` event. Guilds that are unavailable due to an outage will send a `GUILD_DELETE` event. + * 2. When a Guild becomes available again to the client. + * 3. When the current user joins a new Guild. + */ + guildCreate?: (guild: Guild) => unknown; + guildLoaded?: (guild: Guild) => unknown; + /** Sent when a guild is updated. */ + guildUpdate?: (guild: Guild, changes: GuildUpdateChange[]) => unknown; + /** Sent when a guild becomes or was already unavailable due to an outage, or when the user leaves or is removed from a guild. If the `unavailable` field is not set, the user was removed from the guild. */ + guildDelete?: (guild: Guild) => unknown; + /** Sent when a guild's emojis have been updated. */ + guildEmojisUpdate?: ( + guild: Guild, + emojis: Collection, + cachedEmojis: Collection, + ) => unknown; + /** Sent when a guild integration is updated. */ + guildIntegrationsUpdate?: (guild: Guild) => unknown; + /** Sent when a new user joins a guild. */ + guildMemberAdd?: (guild: Guild, member: Member) => unknown; + /** Sent when a user is removed from a guild (leave/kick/ban). */ + guildMemberRemove?: ( + guild: Guild, + user: UserPayload, + member?: Member, + ) => unknown; + /** Sent when a guild member is updated. This will also fire when the user object of a guild member changes. */ + guildMemberUpdate?: ( + guild: Guild, + member: Member, + cachedMember?: Member, + ) => unknown; + heartbeat?: () => unknown; + /** Sent when a user in a guild uses a Slash Command. */ + interactionCreate?: ( + data: Omit & { member: Member }, + ) => unknown; + /** Sent when a message is created. */ + messageCreate?: (message: Message) => unknown; + /** Sent when a message is deleted. */ + messageDelete?: (partial: PartialMessage, message?: Message) => unknown; + /** Sent when a message is updated. */ + messageUpdate?: (message: Message, cachedMessage: OldMessage) => unknown; + nicknameUpdate?: ( + guild: Guild, + member: Member, + nickname: string, + oldNickname?: string, + ) => unknown; + /** A user's presence is their current state on a guild. This event is sent when a user's presence or info, such as name or avatar, is updated. */ + presenceUpdate?: ( + presence: PresenceUpdatePayload, + oldPresence?: PresenceUpdatePayload, + ) => unknown; + raw?: (data: DiscordPayload) => unknown; + rawGateway?: (data: unknown) => unknown; + ready?: () => unknown; + /** Sent when a user adds a reaction to a message. */ + reactionAdd?: ( + payload: MessageReactionUncachedPayload, + emoji: ReactionPayload, + userID: string, + message?: Message, + ) => unknown; + /** Sent when a user removes a reaction from a message. */ + reactionRemove?: ( + payload: MessageReactionUncachedPayload, + emoji: ReactionPayload, + userID: string, + message?: Message, + ) => unknown; + /** Sent when a user explicitly removes all reactions from a message. */ + reactionRemoveAll?: (data: BaseMessageReactionPayload) => unknown; + /** Sent when a bot removes all instances of a given emoji from the reactions of a message. */ + reactionRemoveEmoji?: (data: MessageReactionRemoveEmojiPayload) => unknown; + /** Sent when a guild role is created. */ + roleCreate?: (guild: Guild, role: Role) => unknown; + /** Sent when a guild role is deleted. */ + roleDelete?: (guild: Guild, role: Role) => unknown; + /** Sent when a guild role is updated. */ + roleUpdate?: (guild: Guild, role: Role, cachedRole: Role) => unknown; + roleGained?: (guild: Guild, member: Member, roleID: string) => unknown; + roleLost?: (guild: Guild, member: Member, roleID: string) => unknown; + shardReady?: (shardID: number) => unknown; + shardFailedToLoad?: ( + shardID: number, + guildIDs: Set, + ) => unknown; + /** Sent when a user starts typing in a channel. */ + typingStart?: (data: TypingStartPayload) => unknown; + voiceChannelJoin?: (member: Member, channelID: string) => unknown; + voiceChannelLeave?: (member: Member, channelID: string) => unknown; + voiceChannelSwitch?: ( + member: Member, + channelID: string, + oldChannelID: string, + ) => unknown; + /** Sent when someone joins/leaves/moves voice channels. */ + voiceStateUpdate?: ( + member: Member, + voiceState: VoiceStateUpdatePayload, + ) => unknown; + /** Sent when a guild's voice server is updated. This is sent when initially connecting to voice, and when the current voice instance fails over to a new server. */ + voiceServerUpdate?: ( + token: string, + guild: Guild, + endpoint: string, + ) => unknown; + /** Sent when a guild channel's webhook is created, updated, or deleted. */ + webhooksUpdate?: (channelID: string, guildID: string) => unknown; + /** Sent when a member has passed the guild's Membership Screening requirements */ + membershipScreeningPassed?: (guild: Guild, member: Member) => unknown; + /** Sent when an integration is created on a server such as twitch, youtube etc.. */ + integrationCreate?: ( + data: CamelCaseProps, + ) => unknown; + /** Sent when an integration is updated. */ + integrationUpdate?: ( + data: CamelCaseProps, + ) => unknown; + /** Sent when an integration is deleted. */ + integrationDelete?: ( + data: CamelCaseProps, + ) => undefined; + /** Sent when a new invite to a channel is created. */ + inviteCreate?: (data: CamelCaseProps) => unknown; + /** Sent when an invite is deleted. */ + inviteDelete?: (data: CamelCaseProps) => unknown; +} + +export interface BotConfig { + token: string; + compress?: boolean; + intents: (DiscordGatewayIntents | keyof typeof DiscordGatewayIntents)[]; + eventHandlers?: EventHandlers; +} + +export interface BigBrainBotConfig extends BotConfig { + /** The first shard to start at for this worker. Use this to control which shards to run in each worker. */ + firstShardID: number; + /** The last shard to start for this worker. By default it will be 25 + the firstShardID. */ + lastShardID?: number; + /** This can be used to forward the ws handling to a proxy. */ + wsURL?: string; + /** This can be used to forward the REST handling to a proxy. */ + restURL?: string; + /** This can be used to forward the CDN handling to a proxy. */ + cdnURL?: string; + /** This is the authorization header that your rest proxy will validate */ + restAuthorization?: string; +} diff --git a/src/types/gateway.ts b/src/types/gateway.ts new file mode 100644 index 000000000..bdf098d7e --- /dev/null +++ b/src/types/gateway.ts @@ -0,0 +1,270 @@ +/** https://discord.com/developers/docs/topics/gateway#payloads-gateway-payload-structure */ +export interface DiscordGatewayPayload { + /** opcode for the payload */ + op: number; + /** Event data */ + d: unknown | null; + /** Sequence number, used for resuming sessions and heartbeats */ + s: number | null; + /** The event name for this payload */ + t: string | null; +} + +/** https://discord.com/developers/docs/topics/gateway#connecting-gateway-url-params */ +export interface DiscordGatewayURLParams { + /** Gateway version to use */ + v: string; + /** The encoding of received gateway packets */ + encoding: string; + /** The (optional) compression of gateway packets */ + compress?: string; +} + +/** https://discord.com/developers/docs/topics/gateway#gateway-intents */ +export enum DiscordGatewayIntents { + /** + * - GUILD_CREATE + * - GUILD_UPDATE + * - GUILD_DELETE + * - GUILD_ROLE_CREATE + * - GUILD_ROLE_UPDATE + * - GUILD_ROLE_DELETE + * - CHANNEL_CREATE + * - CHANNEL_UPDATE + * - CHANNEL_DELETE + * - CHANNEL_PINS_UPDATE + */ + GUILDS = 1 << 0, + /** + * - GUILD_MEMBER_ADD + * - GUILD_MEMBER_UPDATE + * - GUILD_MEMBER_REMOVE + */ + GUILD_MEMBERS = 1 << 1, + /** + * - GUILD_BAN_ADD + * - GUILD_BAN_REMOVE + */ + GUILD_BANS = 1 << 2, + /** - GUILD_EMOJIS_UPDATE */ + GUILD_EMOJIS = 1 << 3, + /** - GUILD_INTEGRATIONS_UPDATE */ + GUILD_INTEGRATIONS = 1 << 4, + /** - WEBHOOKS_UPDATE */ + GUILD_WEBHOOKS = 1 << 5, + /** + * - INVITE_CREATE + * - INVITE_DELETE + */ + GUILD_INVITES = 1 << 6, + /** - VOICE_STATE_UPDATE */ + GUILD_VOICE_STATES = 1 << 7, + /** - PRESENCE_UPDATE */ + GUILD_PRESENCES = 1 << 8, + /** + * - MESSAGE_CREATE + * - MESSAGE_UPDATE + * - MESSAGE_DELETE + * - MESSAGE_DELETE_BULK + */ + GUILD_MESSAGES = 1 << 9, + /** + * - MESSAGE_REACTION_ADD + * - MESSAGE_REACTION_REMOVE + * - MESSAGE_REACTION_REMOVE_ALL + * - MESSAGE_REACTION_REMOVE_EMOJI + */ + GUILD_MESSAGE_REACTIONS = 1 << 10, + /** - TYPING_START */ + GUILD_MESSAGE_TYPING = 1 << 11, + /** + * - MESSAGE_CREATE + * - MESSAGE_UPDATE + * - MESSAGE_DELETE + * - CHANNEL_PINS_UPDATE + */ + DIRECT_MESSAGES = 1 << 12, + /** + * - MESSAGE_REACTION_ADD + * - MESSAGE_REACTION_REMOVE + * - MESSAGE_REACTION_REMOVE_ALL + * - MESSAGE_REACTION_REMOVE_EMOJI + */ + DIRECT_MESSAGE_REACTIONS = 1 << 13, + /** - TYPING_START */ + DIRECT_MESSAGE_TYPING = 1 << 14, +} + +/** https://discord.com/developers/docs/topics/gateway#identify */ +export interface DiscordIdentify { + /** Authentication token */ + token: string; + /** Connection properties */ + properties: DiscordIdentifyConnectionProperties; + /** Whether this connection supports compression of packets */ + compress?: boolean; + /** Value between 50 and 250, total number of members where the gateway will stop sending offline members in the guild member list */ + large_threshold: number; + /** Used for Guild Sharding */ + shard?: [number, number]; + /** Presence structure for initial presence information */ + presence?: DiscordUpdateStatus; + /** Enables dispatching of guild subscription events (presence and typing events) */ + guild_subscriptions?: boolean; + /** The Gateway Intents you wish to receive */ + intents: number; +} + +/** https://discord.com/developers/docs/topics/gateway#identify-identify-connection-properties */ +export interface DiscordIdentifyConnectionProperties { + /** Operating system */ + $os: string; + /** Library name */ + $browser: string; + /** Library name */ + $device: string; +} + +/** https://discord.com/developers/docs/topics/gateway#resume */ +export interface DiscordResume { + /** Session token */ + token: string; + /** Session id */ + session_id: string; + /** Last sequence number received */ + seq: number; +} + +/** https://discord.com/developers/docs/topics/gateway#request-guild-members */ +export interface DiscordRequestGuildMembers { + /** id of the guild to get members for */ + guild_id: string; + /** String that username starts with, or an empty string to return all members */ + query?: string; + /** Maximum number of members to send matching the query; a limit of 0 can be used with an empty string query to return all members */ + limit: number; + /** Used to specify if we want the presences of the matched members */ + presences?: boolean; + /** Used to specify which users you wish to fetch */ + user_ids?: string[]; + /** Nonce to identify the Guild Members Chunk response */ + nonce?: string; +} + +/** https://discord.com/developers/docs/topics/gateway#update-voice-state */ +export interface DiscordUpdateVoiceState { + /** id of the guild */ + guild_id: string; + /** id of the voice channel client wants to join (null if disconnecting) */ + channel_id: string | null; + /** Is the client muted */ + self_mute: boolean; + /** Is the client deafened */ + self_deaf: boolean; +} + +/** https://discord.com/developers/docs/topics/gateway#update-status */ +export interface DiscordUpdateStatus { + /** Unix time (in milliseconds) of when the client went idle, or null if the client is not idle */ + since?: number; + /** null, or the user's activities */ + activities: DiscordActivity[] | null; + /** The user's new status */ + status: DiscordStatusTypes; + /** Whether or not the client is afk */ + afk: boolean; +} + +/** https://discord.com/developers/docs/topics/gateway#update-status-status-types */ +export type DiscordStatusTypes = + | "online" + | "dnd" + | "idle" + | "invisible" + | "offline"; + +/** https://discord.com/developers/docs/topics/gateway#hello */ +export interface DiscordHello { + /** The interval (in milliseconds) the client should heartbeat with */ + heartbeat_interval: number; +} + +/** https://discord.com/developers/docs/topics/gateway#ready */ +export interface DiscordReady { + /** Gateway version */ + v: number; + /** Information about the user including email */ + user: DiscordUser; + /** Empty array */ + private_channels: []; + /** The guilds the user is in */ + guilds: DiscordUnavailableGuild[]; + /** Used for resuming connections */ + session_id: string; + /** The shard information associated with this session, if sent when identifying */ + shard?: [number, number]; + /** Contains id and flags */ + application: + & Partial + & Pick; +} + +/** https://discord.com/developers/docs/topics/gateway#channel-pins-update */ +export interface DiscordChannelPinsUpdate { + /** The id of the guild */ + guild_id?: string; + /** The id of the channel */ + channel_id: string; + /** The time at which the most recent pinned message was pinned */ + last_pin_timestamp?: string | null; +} + +/** https://discord.com/developers/docs/topics/gateway#guild-ban-add */ +export interface DiscordGuildBanAddRemove { + /** id of the guild */ + guild_id: string; + /** The (un)banned user */ + user: DiscordUser; +} + +/** https://discord.com/developers/docs/topics/gateway#guild-emojis-update */ +export interface GuildEmojisUpdate { + /** id of the guild */ + guild_id: string; + /** Array of emojis */ + emojis: DiscordEmoji[]; +} + +/** https://discord.com/developers/docs/topics/gateway#guild-integrations-update */ +export interface DiscordIntegrationsUpdate { + /** id of the guild whose integrations were updated */ + guild_id: string; +} + +/** https://discord.com/developers/docs/topics/gateway#guild-member-add */ +export interface DiscordGuildMemberAdd extends DiscordMember { + /** id of the guild */ + guild_id: string; +} + +/** https://discord.com/developers/docs/topics/gateway#get-gateway-bot */ +export interface DiscordGetGatewayBot { + /** The WSS URL that can be used for connecting to the gateway */ + url: string; + /** The recommended number of shards to use when connecting */ + shards: number; + /** Information on the current session start limit */ + session_start_limit: DiscordSessionStartLimit; +} + +/** https://discord.com/developers/docs/topics/gateway#session-start-limit-object */ +export interface DiscordSessionStartLimit { + /** The total number of session starts the current user is allowed */ + total: number; + /** The remaining number of session starts the current user is allowed */ + remaining: number; + /** The number of milliseconds after which the limit resets */ + reset_after: number; + /** The number of identify requests allowed per 5 seconds */ + max_concurrency: number; +}