@@ -7,6 +7,18 @@ import type { DiscordAccountConfig } from "../../config/types.js";
77import { danger } from "../../globals.js" ;
88import type { RuntimeEnv } from "../../runtime.js" ;
99
10+ const DISCORD_GATEWAY_BOT_URL = "https://discord.com/api/v10/gateway/bot" ;
11+ const DEFAULT_DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/" ;
12+
13+ type DiscordGatewayMetadataResponse = Pick < Response , "ok" | "status" | "text" > ;
14+ type DiscordGatewayFetchInit = Record < string , unknown > & {
15+ headers ?: Record < string , string > ;
16+ } ;
17+ type DiscordGatewayFetch = (
18+ input : string ,
19+ init ?: DiscordGatewayFetchInit ,
20+ ) => Promise < DiscordGatewayMetadataResponse > ;
21+
1022export function resolveDiscordGatewayIntents (
1123 intentsConfig ?: import ( "../../config/types.discord.js" ) . DiscordIntentsConfig ,
1224) : number {
@@ -27,6 +39,138 @@ export function resolveDiscordGatewayIntents(
2739 return intents ;
2840}
2941
42+ function summarizeGatewayResponseBody ( body : string ) : string {
43+ const normalized = body . trim ( ) . replace ( / \s + / g, " " ) ;
44+ if ( ! normalized ) {
45+ return "<empty>" ;
46+ }
47+ return normalized . slice ( 0 , 240 ) ;
48+ }
49+
50+ function isTransientDiscordGatewayResponse ( status : number , body : string ) : boolean {
51+ if ( status >= 500 ) {
52+ return true ;
53+ }
54+ const normalized = body . toLowerCase ( ) ;
55+ return (
56+ normalized . includes ( "upstream connect error" ) ||
57+ normalized . includes ( "disconnect/reset before headers" ) ||
58+ normalized . includes ( "reset reason:" )
59+ ) ;
60+ }
61+
62+ function createGatewayMetadataError ( params : {
63+ detail : string ;
64+ transient : boolean ;
65+ cause ?: unknown ;
66+ } ) : Error {
67+ if ( params . transient ) {
68+ return new Error ( "Failed to get gateway information from Discord: fetch failed" , {
69+ cause : params . cause ?? new Error ( params . detail ) ,
70+ } ) ;
71+ }
72+ return new Error ( `Failed to get gateway information from Discord: ${ params . detail } ` , {
73+ cause : params . cause ,
74+ } ) ;
75+ }
76+
77+ async function fetchDiscordGatewayInfo ( params : {
78+ token : string ;
79+ fetchImpl : DiscordGatewayFetch ;
80+ fetchInit ?: DiscordGatewayFetchInit ;
81+ } ) : Promise < APIGatewayBotInfo > {
82+ let response : DiscordGatewayMetadataResponse ;
83+ try {
84+ response = await params . fetchImpl ( DISCORD_GATEWAY_BOT_URL , {
85+ ...params . fetchInit ,
86+ headers : {
87+ ...params . fetchInit ?. headers ,
88+ Authorization : `Bot ${ params . token } ` ,
89+ } ,
90+ } ) ;
91+ } catch ( error ) {
92+ throw createGatewayMetadataError ( {
93+ detail : error instanceof Error ? error . message : String ( error ) ,
94+ transient : true ,
95+ cause : error ,
96+ } ) ;
97+ }
98+
99+ let body : string ;
100+ try {
101+ body = await response . text ( ) ;
102+ } catch ( error ) {
103+ throw createGatewayMetadataError ( {
104+ detail : error instanceof Error ? error . message : String ( error ) ,
105+ transient : true ,
106+ cause : error ,
107+ } ) ;
108+ }
109+ const summary = summarizeGatewayResponseBody ( body ) ;
110+ const transient = isTransientDiscordGatewayResponse ( response . status , body ) ;
111+
112+ if ( ! response . ok ) {
113+ throw createGatewayMetadataError ( {
114+ detail : `Discord API /gateway/bot failed (${ response . status } ): ${ summary } ` ,
115+ transient,
116+ } ) ;
117+ }
118+
119+ try {
120+ const parsed = JSON . parse ( body ) as Partial < APIGatewayBotInfo > ;
121+ return {
122+ ...parsed ,
123+ url :
124+ typeof parsed . url === "string" && parsed . url . trim ( )
125+ ? parsed . url
126+ : DEFAULT_DISCORD_GATEWAY_URL ,
127+ } as APIGatewayBotInfo ;
128+ } catch ( error ) {
129+ throw createGatewayMetadataError ( {
130+ detail : `Discord API /gateway/bot returned invalid JSON: ${ summary } ` ,
131+ transient,
132+ cause : error ,
133+ } ) ;
134+ }
135+ }
136+
137+ function createGatewayPlugin ( params : {
138+ options : {
139+ reconnect : { maxAttempts : number } ;
140+ intents : number ;
141+ autoInteractions : boolean ;
142+ } ;
143+ fetchImpl : DiscordGatewayFetch ;
144+ fetchInit ?: DiscordGatewayFetchInit ;
145+ wsAgent ?: HttpsProxyAgent < string > ;
146+ } ) : GatewayPlugin {
147+ class SafeGatewayPlugin extends GatewayPlugin {
148+ constructor ( ) {
149+ super ( params . options ) ;
150+ }
151+
152+ override async registerClient ( client : Parameters < GatewayPlugin [ "registerClient" ] > [ 0 ] ) {
153+ if ( ! this . gatewayInfo ) {
154+ this . gatewayInfo = await fetchDiscordGatewayInfo ( {
155+ token : client . options . token ,
156+ fetchImpl : params . fetchImpl ,
157+ fetchInit : params . fetchInit ,
158+ } ) ;
159+ }
160+ return super . registerClient ( client ) ;
161+ }
162+
163+ override createWebSocket ( url : string ) {
164+ if ( ! params . wsAgent ) {
165+ return super . createWebSocket ( url ) ;
166+ }
167+ return new WebSocket ( url , { agent : params . wsAgent } ) ;
168+ }
169+ }
170+
171+ return new SafeGatewayPlugin ( ) ;
172+ }
173+
30174export function createDiscordGatewayPlugin ( params : {
31175 discordConfig : DiscordAccountConfig ;
32176 runtime : RuntimeEnv ;
@@ -40,7 +184,10 @@ export function createDiscordGatewayPlugin(params: {
40184 } ;
41185
42186 if ( ! proxy ) {
43- return new GatewayPlugin ( options ) ;
187+ return createGatewayPlugin ( {
188+ options,
189+ fetchImpl : ( input , init ) => fetch ( input , init as RequestInit ) ,
190+ } ) ;
44191 }
45192
46193 try {
@@ -49,39 +196,17 @@ export function createDiscordGatewayPlugin(params: {
49196
50197 params . runtime . log ?.( "discord: gateway proxy enabled" ) ;
51198
52- class ProxyGatewayPlugin extends GatewayPlugin {
53- constructor ( ) {
54- super ( options ) ;
55- }
56-
57- override async registerClient ( client : Parameters < GatewayPlugin [ "registerClient" ] > [ 0 ] ) {
58- if ( ! this . gatewayInfo ) {
59- try {
60- const response = await undiciFetch ( "https://discord.com/api/v10/gateway/bot" , {
61- headers : {
62- Authorization : `Bot ${ client . options . token } ` ,
63- } ,
64- dispatcher : fetchAgent ,
65- } as Record < string , unknown > ) ;
66- this . gatewayInfo = ( await response . json ( ) ) as APIGatewayBotInfo ;
67- } catch ( error ) {
68- throw new Error (
69- `Failed to get gateway information from Discord: ${ error instanceof Error ? error . message : String ( error ) } ` ,
70- { cause : error } ,
71- ) ;
72- }
73- }
74- return super . registerClient ( client ) ;
75- }
76-
77- override createWebSocket ( url : string ) {
78- return new WebSocket ( url , { agent : wsAgent } ) ;
79- }
80- }
81-
82- return new ProxyGatewayPlugin ( ) ;
199+ return createGatewayPlugin ( {
200+ options,
201+ fetchImpl : ( input , init ) => undiciFetch ( input , init ) ,
202+ fetchInit : { dispatcher : fetchAgent } ,
203+ wsAgent,
204+ } ) ;
83205 } catch ( err ) {
84206 params . runtime . error ?.( danger ( `discord: invalid gateway proxy: ${ String ( err ) } ` ) ) ;
85- return new GatewayPlugin ( options ) ;
207+ return createGatewayPlugin ( {
208+ options,
209+ fetchImpl : ( input , init ) => fetch ( input , init as RequestInit ) ,
210+ } ) ;
86211 }
87212}
0 commit comments