mirror of
https://github.com/discordeno/discordeno.git
synced 2026-09-17 08:47:22 +00:00
Merge branch 'main' of https://github.com/discordeno/discordeno
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { ButtonStyles, MessageComponentTypes, SelectOption, TextStyles } from '@discordeno/types'
|
||||
import type { ButtonStyles, ChannelTypes, MessageComponentTypes, SelectOption, TextStyles } from '@discordeno/types'
|
||||
import type { Bot } from '../index.js'
|
||||
import type { DiscordComponent } from '../typings.js'
|
||||
|
||||
@@ -17,6 +17,7 @@ export function transformComponent(bot: Bot, payload: DiscordComponent): Compone
|
||||
}
|
||||
: undefined,
|
||||
url: payload.url,
|
||||
channelTypes: payload.channel_types,
|
||||
options: payload.options?.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
@@ -68,6 +69,8 @@ export interface Component {
|
||||
}
|
||||
/** optional url for link-style buttons that can navigate a user to the web. Only type 5 Link buttons can have a url */
|
||||
url?: string
|
||||
/** List of channel types to include in a channel select menu options list */
|
||||
channelTypes?: ChannelTypes[]
|
||||
/** The choices! Maximum of 25 items. */
|
||||
options?: SelectOption[]
|
||||
/** A custom placeholder text if nothing is selected. Maximum 150 characters. */
|
||||
|
||||
@@ -60,7 +60,7 @@ export function transformGuild(bot: Bot, payload: { guild: DiscordGuild } & { sh
|
||||
banner: payload.guild.banner ? iconHashToBigInt(payload.guild.banner) : undefined,
|
||||
splash: payload.guild.splash ? iconHashToBigInt(payload.guild.splash) : undefined,
|
||||
channels: new Collection(
|
||||
payload.guild.channels?.map((channel) => {
|
||||
[...(payload.guild.channels ?? []), ...(payload.guild.threads ?? [])].map((channel) => {
|
||||
const result = bot.transformers.channel(bot, { channel, guildId })
|
||||
return [result.id, result]
|
||||
}),
|
||||
@@ -86,7 +86,6 @@ export function transformGuild(bot: Bot, payload: { guild: DiscordGuild } & { sh
|
||||
voiceStates: new Collection(
|
||||
(payload.guild.voice_states ?? []).map((vs) => bot.transformers.voiceState(bot, { voiceState: vs, guildId })).map((vs) => [vs.userId, vs]),
|
||||
),
|
||||
|
||||
id: guildId,
|
||||
// WEIRD EDGE CASE WITH BOT CREATED SERVERS
|
||||
ownerId: payload.guild.owner_id ? bot.transformers.snowflake(payload.guild.owner_id) : 0n,
|
||||
|
||||
@@ -23,10 +23,8 @@ import type { User } from './user.js'
|
||||
export interface Interaction extends BaseInteraction {
|
||||
/** The bot object */
|
||||
bot: Bot
|
||||
/** Whether or not this interaction has been replied to. */
|
||||
/** Whether or not this interaction has been responded to. */
|
||||
acknowledged: boolean
|
||||
/** Whether or not a modal has been shown for this interaction. */
|
||||
shownModal: boolean
|
||||
/** Id of the interaction */
|
||||
id: bigint
|
||||
/** Id of the application this interaction is for */
|
||||
@@ -84,41 +82,36 @@ const baseInteraction: Partial<Interaction> & BaseInteraction = {
|
||||
async respond(response, options) {
|
||||
let type = InteractionResponseTypes.ChannelMessageWithSource
|
||||
|
||||
// If user provides a string, change it to response object
|
||||
if (typeof response === 'string') {
|
||||
response = {
|
||||
content: response,
|
||||
}
|
||||
}
|
||||
// If user provides a string, change it to a response object
|
||||
if (typeof response === 'string') response = { content: response }
|
||||
// If user provides an object, determine if it should be an autocomplete or a modal response
|
||||
else {
|
||||
if (response.title) type = InteractionResponseTypes.Modal
|
||||
else if (this.type === InteractionTypes.ApplicationCommandAutocomplete) type = InteractionResponseTypes.ApplicationCommandAutocompleteResult
|
||||
}
|
||||
else if (response.title) type = InteractionResponseTypes.Modal
|
||||
else if (this.type === InteractionTypes.ApplicationCommandAutocomplete) type = InteractionResponseTypes.ApplicationCommandAutocompleteResult
|
||||
|
||||
// If user wants to send a private message
|
||||
if (type === InteractionResponseTypes.ChannelMessageWithSource && options?.isPrivate) response.flags = 64
|
||||
|
||||
// Since this has already been given a response, any further responses must be followups.
|
||||
if (this.acknowledged) return await this.bot?.rest.sendFollowupMessage(this.token!, response)
|
||||
if (this.shownModal && type === InteractionResponseTypes.Modal) throw new Error('Cannot respond to a modal interaction with another modal.')
|
||||
|
||||
// Modals cannot be chained
|
||||
if (this.type === InteractionTypes.ModalSubmit && type === InteractionResponseTypes.Modal)
|
||||
throw new Error('Cannot respond to a modal interaction with another modal.')
|
||||
|
||||
// Autocomplete response can only be used for autocomplete interactions
|
||||
if (this.type === InteractionTypes.ApplicationCommandAutocomplete && type !== InteractionResponseTypes.ApplicationCommandAutocompleteResult)
|
||||
throw new Error('Cannot respond to an autocomplete interaction with a modal or message.')
|
||||
|
||||
// If user has not already responded to this interaction we need to send an original response
|
||||
if (type === InteractionResponseTypes.Modal) this.shownModal = true
|
||||
if (type === InteractionResponseTypes.ChannelMessageWithSource) this.acknowledged = true
|
||||
|
||||
this.acknowledged = true
|
||||
return await this.bot?.rest.sendInteractionResponse(this.id!, this.token!, { type, data: response })
|
||||
},
|
||||
|
||||
async edit(response) {
|
||||
if (this.type === InteractionTypes.ApplicationCommandAutocomplete) throw new Error('Cannot edit an autocomplete interaction')
|
||||
|
||||
// If user provides a string, change it to response object
|
||||
if (typeof response === 'string') {
|
||||
response = {
|
||||
content: response,
|
||||
}
|
||||
}
|
||||
// If user provides a string, change it to a response object
|
||||
if (typeof response === 'string') response = { content: response }
|
||||
|
||||
return await this.bot!.rest.editOriginalInteractionResponse(this.token!, response)
|
||||
},
|
||||
@@ -128,14 +121,16 @@ const baseInteraction: Partial<Interaction> & BaseInteraction = {
|
||||
if (this.acknowledged) throw new Error('Cannot defer an already responded interaction')
|
||||
|
||||
// Determine the type of defer response
|
||||
let type: InteractionResponseTypes
|
||||
if (this.type === InteractionTypes.MessageComponent) type = InteractionResponseTypes.DeferredUpdateMessage
|
||||
else type = InteractionResponseTypes.DeferredChannelMessageWithSource
|
||||
const type =
|
||||
this.type === InteractionTypes.MessageComponent
|
||||
? InteractionResponseTypes.DeferredUpdateMessage
|
||||
: InteractionResponseTypes.DeferredChannelMessageWithSource
|
||||
|
||||
// If user wants to send a private message
|
||||
const data: InteractionCallbackData = {}
|
||||
if (isPrivate) data.flags = 64
|
||||
|
||||
this.acknowledged = true
|
||||
return await this.bot?.rest.sendInteractionResponse(this.id!, this.token!, { type, data })
|
||||
},
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export function transformComponentToDiscordComponent(bot: Bot, payload: Componen
|
||||
}
|
||||
: undefined,
|
||||
url: payload.url,
|
||||
channel_types: payload.channelTypes,
|
||||
options: payload.options?.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ApplicationCommandTypes,
|
||||
type AllowedMentions,
|
||||
type ButtonStyles,
|
||||
type ChannelTypes,
|
||||
type CreateApplicationCommand,
|
||||
type CreateContextApplicationCommand,
|
||||
type DiscordAllowedMentions,
|
||||
@@ -16,13 +17,13 @@ import {
|
||||
type DiscordUser,
|
||||
type FileContent,
|
||||
type InteractionResponseTypes,
|
||||
type MessageComponents,
|
||||
type MessageComponentTypes,
|
||||
type MessageComponents,
|
||||
type TextStyles,
|
||||
} from '@discordeno/types'
|
||||
import type * as handlers from './handlers/index.js'
|
||||
import type { Embed } from './transformers/embed.js'
|
||||
import type { ApplicationCommandOptionChoice } from './transformers/applicationCommandOptionChoice.js'
|
||||
import type { Embed } from './transformers/embed.js'
|
||||
|
||||
export function isContextApplicationCommand(command: CreateApplicationCommand): command is CreateContextApplicationCommand {
|
||||
return command.type === ApplicationCommandTypes.Message || command.type === ApplicationCommandTypes.User
|
||||
@@ -69,6 +70,8 @@ export interface DiscordComponent {
|
||||
}
|
||||
/** optional url for link-style buttons that can navigate a user to the web. Only type 5 Link buttons can have a url */
|
||||
url?: string
|
||||
/** List of channel types to include in a channel select menu options list */
|
||||
channel_types?: ChannelTypes[]
|
||||
/** The choices! Maximum of 25 items. */
|
||||
options?: DiscordSelectOption[]
|
||||
/** A custom placeholder text if nothing is selected. Maximum 150 characters. */
|
||||
@@ -205,7 +208,7 @@ export interface BotGatewayHandlerOptions {
|
||||
|
||||
export enum MessageFlags {
|
||||
/** Whether this message has been published to subscribed channels (via Channel Following) */
|
||||
Crossposted = 1 << 0,
|
||||
Crossposted = 1 << 0,
|
||||
/** Whether this message originated from a message in another channel (via Channel Following) */
|
||||
IsCrosspost = 1 << 1,
|
||||
/** Whether do not include any embeds when serializing this message */
|
||||
|
||||
@@ -12,11 +12,13 @@ import type {
|
||||
import { GatewayCloseEventCodes, GatewayIntents, GatewayOpcodes } from '@discordeno/types'
|
||||
import { Collection, LeakyBucket, camelize, delay, logger } from '@discordeno/utils'
|
||||
import { inflateSync } from 'node:zlib'
|
||||
import WebSocket from 'ws'
|
||||
import NodeWebSocket from 'ws'
|
||||
import type { RequestMemberRequest } from './manager.js'
|
||||
import type { BotStatusUpdate, ShardEvents, ShardGatewayConfig, ShardHeart, ShardSocketRequest, StatusUpdate, UpdateVoiceState } from './types.js'
|
||||
import { ShardSocketCloseCodes, ShardState } from './types.js'
|
||||
|
||||
declare let WebSocket: any
|
||||
|
||||
export class DiscordenoShard {
|
||||
/** The id of the shard */
|
||||
id: number
|
||||
@@ -33,7 +35,7 @@ export class DiscordenoShard {
|
||||
/** Current session id of the shard if present. */
|
||||
sessionId?: string
|
||||
/** This contains the WebSocket connection to Discord, if currently connected. */
|
||||
socket?: WebSocket
|
||||
socket?: NodeWebSocket
|
||||
/** Current internal state of the this. */
|
||||
state = ShardState.Offline
|
||||
/** The url provided by discord to use when resuming a connection for this this. */
|
||||
@@ -111,7 +113,7 @@ export class DiscordenoShard {
|
||||
|
||||
/** Close the socket connection to discord if present. */
|
||||
close(code: number, reason: string): void {
|
||||
if (this.socket?.readyState !== WebSocket.OPEN) return
|
||||
if (this.socket?.readyState !== NodeWebSocket.OPEN) return
|
||||
|
||||
this.socket?.close(code, reason)
|
||||
}
|
||||
@@ -129,13 +131,13 @@ export class DiscordenoShard {
|
||||
url.searchParams.set('v', this.gatewayConfig.version.toString())
|
||||
url.searchParams.set('encoding', 'json')
|
||||
|
||||
const socket = new WebSocket(url.toString())
|
||||
const socket: NodeWebSocket = process?.versions !== undefined ? new NodeWebSocket(url.toString()) : new WebSocket(url.toString())
|
||||
this.socket = socket
|
||||
|
||||
// TODO: proper event handling
|
||||
socket.onerror = (event) => console.log({ error: event, shardId: this.id })
|
||||
socket.onclose = async (event) => await this.handleClose(event)
|
||||
socket.onmessage = async (message) => await this.handleMessage(message)
|
||||
socket.onerror = (event: NodeWebSocket.ErrorEvent) => console.log({ error: event, shardId: this.id })
|
||||
socket.onclose = async (event: NodeWebSocket.CloseEvent) => await this.handleClose(event)
|
||||
socket.onmessage = async (message: NodeWebSocket.MessageEvent) => await this.handleMessage(message)
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
socket.onopen = () => {
|
||||
@@ -204,7 +206,7 @@ export class DiscordenoShard {
|
||||
|
||||
/** Check whether the connection to Discord is currently open. */
|
||||
isOpen(): boolean {
|
||||
return this.socket?.readyState === WebSocket.OPEN
|
||||
return this.socket?.readyState === NodeWebSocket.OPEN
|
||||
}
|
||||
|
||||
/** Attempt to resume the previous shards session with the gateway. */
|
||||
@@ -282,7 +284,7 @@ export class DiscordenoShard {
|
||||
}
|
||||
|
||||
/** Handle a gateway connection close. */
|
||||
async handleClose(close: WebSocket.CloseEvent): Promise<void> {
|
||||
async handleClose(close: NodeWebSocket.CloseEvent): Promise<void> {
|
||||
// gateway.debug("GW CLOSED", { shardId, payload: event });
|
||||
|
||||
this.stopHeartbeating()
|
||||
@@ -485,7 +487,7 @@ export class DiscordenoShard {
|
||||
}
|
||||
|
||||
/** Handle an incoming gateway message. */
|
||||
async handleMessage(message: WebSocket.MessageEvent): Promise<void> {
|
||||
async handleMessage(message: NodeWebSocket.MessageEvent): Promise<void> {
|
||||
let preProcessMessage = message.data
|
||||
|
||||
// If message compression is enabled,
|
||||
|
||||
@@ -32,7 +32,7 @@ export function createGatewayManager(options: CreateGatewayManagerOptions): Gate
|
||||
version: options.version ?? 10,
|
||||
connection: options.connection,
|
||||
totalShards: options.totalShards ?? options.connection.shards ?? 1,
|
||||
lastShardId: options.lastShardId ?? 0,
|
||||
lastShardId: options.lastShardId ?? (options.totalShards ? options.totalShards - 1 : (options.connection ? options.connection.shards - 1 : 0)),
|
||||
firstShardId: options.firstShardId ?? 0,
|
||||
totalWorkers: options.totalWorkers ?? 4,
|
||||
shardsPerWorker: options.shardsPerWorker ?? 25,
|
||||
|
||||
@@ -118,18 +118,29 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
const newObj: any = {}
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
// Keys that dont require snake casing
|
||||
if (['permissions', 'allow', 'deny'].includes(key) && obj[key] !== undefined) {
|
||||
newObj[key] = calculateBits(obj[key])
|
||||
continue
|
||||
const value = obj[key]
|
||||
|
||||
// Some falsy values should be allowed like null or 0
|
||||
if (value !== undefined) {
|
||||
switch (key) {
|
||||
case 'permissions':
|
||||
case 'allow':
|
||||
case 'deny':
|
||||
newObj[key] = calculateBits(value)
|
||||
continue
|
||||
case 'defaultMemberPermissions':
|
||||
newObj.default_member_permissions = calculateBits(value)
|
||||
continue
|
||||
case 'nameLocalizations':
|
||||
newObj.name_localizations = value
|
||||
continue
|
||||
case 'descriptionLocalizations':
|
||||
newObj.description_localizations = value
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'defaultMemberPermissions' && obj[key] !== undefined) {
|
||||
newObj.default_member_permissions = calculateBits(obj[key])
|
||||
continue
|
||||
}
|
||||
|
||||
newObj[camelToSnakeCase(key)] = rest.changeToDiscordFormat(obj[key])
|
||||
newObj[camelToSnakeCase(key)] = rest.changeToDiscordFormat(value)
|
||||
}
|
||||
|
||||
return newObj
|
||||
|
||||
@@ -1134,6 +1134,8 @@ export interface DiscordSelectMenuComponent {
|
||||
min_values?: number
|
||||
/** The maximum number of items that can be selected. Default 1. Between 1-25. */
|
||||
max_values?: number
|
||||
/** List of channel types to include in a channel select menu options list */
|
||||
channelTypes?: ChannelTypes[]
|
||||
/** The choices! Maximum of 25 items. */
|
||||
options: DiscordSelectOption[]
|
||||
}
|
||||
|
||||
@@ -195,6 +195,8 @@ export interface SelectMenuChannelsComponent {
|
||||
minValues?: number
|
||||
/** The maximum number of items that can be selected. Default 1. Between 1-25. */
|
||||
maxValues?: number
|
||||
/** List of channel types to include in the options list */
|
||||
channelTypes?: ChannelTypes[]
|
||||
/** Whether or not this select is disabled */
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user