@@ -14,6 +14,11 @@ import {
14
14
} from 'discord-interactions' ;
15
15
import { getRandomEmoji } from './utils.js' ;
16
16
17
+ // Global Variables:
18
+ let currentIndex = 0 ; // Current page index of the bot's response
19
+ let chunks ; // Message chunks (used when the response exceeds the character limit)
20
+ let dotInterval = null ;
21
+
17
22
// Create an express app
18
23
const app = express ( ) ;
19
24
// Get port, or default to 3000
@@ -25,6 +30,57 @@ app.get('/', (req, res) => {
25
30
} ) ;
26
31
27
32
// Helper functions below
33
+ function startLoadingDots ( endpoint , initialMessage ) {
34
+ let dotCount = 0 ;
35
+ let maxDots = 4
36
+
37
+ dotInterval = setInterval ( ( ) => {
38
+ dotCount = ( dotCount % maxDots ) + 1 ;
39
+ const loadingMessage = `${ initialMessage } ${ '.' . repeat ( dotCount ) } ` ;
40
+ const options = {
41
+ content : loadingMessage ,
42
+ flags : InteractionResponseFlags . EPHEMERAL ,
43
+ components : [ ] ,
44
+ } ;
45
+
46
+ sendResponse ( endpoint , options ) ;
47
+ } , 500 ) ; // Interval delay
48
+ }
49
+
50
+ function stopLoadingDots ( ) {
51
+ if ( dotInterval ) {
52
+ clearInterval ( dotInterval ) ;
53
+ }
54
+ }
55
+
56
+ function createMessageWithButtons ( index , chunks ) {
57
+ currentIndex = index ; // Set the global currentIndex to the current index
58
+ return {
59
+ content : chunks [ index ] ,
60
+ components : [
61
+ {
62
+ type : 1 , // Action Row container for buttons
63
+ components : [
64
+ {
65
+ type : 2 , // Button
66
+ label : 'Previous' ,
67
+ style : 1 , // Primary color (blurple)
68
+ custom_id : `prev_${ index } ` ,
69
+ disabled : index === 0 , // Disable if on the first chunk
70
+ } ,
71
+ {
72
+ type : 2 , // Button
73
+ label : 'Next' ,
74
+ style : 1 , // Primary color (blurple)
75
+ custom_id : `next_${ index } ` ,
76
+ disabled : index === chunks . length - 1 , // Disable if on the last chunk
77
+ } ,
78
+ ] ,
79
+ } ,
80
+ ] ,
81
+ } ;
82
+ }
83
+
28
84
async function sendPlaceholderResponse ( res , placeholderResponse ) {
29
85
await res . send ( {
30
86
type : InteractionResponseType . CHANNEL_MESSAGE_WITH_SOURCE ,
@@ -56,19 +112,53 @@ async function fetchAnswer(question) {
56
112
return rawResponse || 'No answer provided.' ;
57
113
}
58
114
59
- async function sendFollowUpResponse ( endpoint , content ) {
60
- await fetch ( `https://discord.com/api/v10/${ endpoint } ` , {
61
- method : 'PATCH' ,
62
- headers : {
63
- 'Authorization' : `Bot ${ process . env . DISCORD_TOKEN } ` ,
64
- 'Content-Type' : 'application/json' ,
65
- } ,
66
- body : JSON . stringify ( {
67
- content,
115
+ async function sendResponse ( endpoint , options ) {
116
+ try {
117
+ const response = await fetch ( `https://discord.com/api/v10/${ endpoint } ` , {
118
+ method : 'PATCH' ,
119
+ headers : {
120
+ 'Authorization' : `Bot ${ process . env . DISCORD_TOKEN } ` ,
121
+ 'Content-Type' : 'application/json' ,
122
+ } ,
123
+ body : JSON . stringify ( {
124
+ ...options
125
+ } ) ,
126
+ } ) ;
127
+
128
+ if ( ! response . ok ) {
129
+ console . error ( `Failed to send follow-up response. Status: ${ response . status } , StatusText: ${ response . statusText } ` ) ;
130
+ }
131
+ } catch ( error ) {
132
+ console . error ( 'Error sending follow-up response:' , error ) ;
133
+ }
134
+ }
135
+
136
+ async function sendFollowUpResponse ( endpoint , followUpMessage ) {
137
+ // Check if the follow-up message exceeds Discord's character limit (2000 characters)
138
+ if ( followUpMessage . length > 2000 ) {
139
+ // Split response into chunks of 2000 characters
140
+ chunks = followUpMessage . match ( / ( .| [ \r \n ] ) { 1 , 1990 } (? = \s | $ ) / g) || [ ] ;
141
+ // Send the first chunk with prev/next buttons
142
+ await sendResponse ( endpoint , createMessageWithButtons ( 0 , chunks ) ) ;
143
+ } else {
144
+ let options = {
145
+ content : followUpMessage ,
68
146
flags : InteractionResponseFlags . EPHEMERAL ,
69
147
components : [ ] ,
70
- } ) ,
71
- } ) ;
148
+ } ;
149
+ await sendResponse ( endpoint , options ) ;
150
+ }
151
+ }
152
+
153
+ async function fetchFollowUpMessage ( question , userId , endpoint ) {
154
+ try {
155
+ // Call an external API to fetch the answer
156
+ const answer = await fetchAnswer ( question ) ;
157
+ return `\n> ${ question } \n\nHere's what I found, <@${ userId } >:\n\n${ answer } ` ;
158
+ } catch ( error ) {
159
+ console . error ( 'Error fetching answer:' , error ) ;
160
+ return `\n> ${ question } \n\nSorry <@${ userId } >, I couldn't fetch an answer to your question. Please try again later.` ;
161
+ }
72
162
}
73
163
74
164
/**
@@ -97,42 +187,34 @@ app.post('/interactions', verifyKeyMiddleware(process.env.DISCORD_PUBLIC_KEY), a
97
187
if ( name === 'ask' ) {
98
188
const context = req . body . context ;
99
189
const userId = context === 0 ? req . body . member . user . id : req . body . user . id
100
-
101
190
const question = data . options [ 0 ] ?. value || 'No question provided' ;
102
- const endpoint = `webhooks/${ process . env . DISCORD_APP_ID } /${ req . body . token } /messages/@original` ;
191
+
192
+ // Sanitize token before use in endpoint
193
+ const token = req . body . token ;
194
+ const tokenRegex = / ^ [ A - Z a - z 0 - 9 - _ ] + $ / ;
195
+ if ( ! tokenRegex . test ( token ) ) {
196
+ return res . status ( 400 ) . json ( { error : 'Invalid token format' } ) ;
197
+ }
198
+
199
+ const endpoint = `webhooks/${ process . env . DISCORD_APP_ID } /${ token } /messages/@original` ;
103
200
const initialMessage = `\n> ${ question } \n\nLet me find the answer for you. This might take a moment`
201
+ let followUpMessage = "Something went wrong! Please try again later." ;
104
202
105
203
// Send a placeholder response
106
204
await sendPlaceholderResponse ( res , initialMessage ) ;
107
205
108
- // Show animated dots in the message while waiting
109
- let dotCount = 0 ;
110
- const maxDots = 4 ;
111
- let isFetching = true ;
112
-
113
- const interval = setInterval ( ( ) => {
114
- if ( isFetching ) {
115
- dotCount = ( dotCount % maxDots ) + 1 ;
116
- sendFollowUpResponse ( endpoint , `${ initialMessage } ${ '.' . repeat ( dotCount ) } ` ) ;
117
- }
118
- } , 500 ) ;
119
-
120
- // Create the follow-up response
121
- let followUpMessage ;
206
+ // Begin loading dots while fetching follow-up message
122
207
try {
123
- // Call an external API to fetch the answer
124
- const answer = await fetchAnswer ( question ) ;
125
- followUpMessage = `\n> ${ question } \n\nHere's what I found, <@${ userId } >:\n\n${ answer } ` ;
126
- } catch ( error ) {
127
- console . error ( 'Error fetching answer:' , error ) ;
128
- followUpMessage = `\n> ${ question } \n\nSorry <@${ userId } >, I couldn't fetch an answer to your question. Please try again later.` ;
208
+ startLoadingDots ( endpoint , initialMessage )
209
+ followUpMessage = await fetchFollowUpMessage ( question , userId , endpoint ) ;
129
210
} finally {
130
- // Ensure cleanup and state updates
131
- isFetching = false ; // Mark fetching as complete
132
- clearInterval ( interval ) ; // Stop the dot interval
211
+ stopLoadingDots ( )
133
212
}
134
213
135
- return sendFollowUpResponse ( endpoint , followUpMessage ) ;
214
+ // Send the follow-up response
215
+ sendFollowUpResponse ( endpoint , followUpMessage ) ;
216
+
217
+ return ;
136
218
}
137
219
138
220
// "test" command
@@ -151,6 +233,28 @@ app.post('/interactions', verifyKeyMiddleware(process.env.DISCORD_PUBLIC_KEY), a
151
233
return res . status ( 400 ) . json ( { error : 'unknown command' } ) ;
152
234
}
153
235
236
+ // Handle button interactions
237
+ if ( type === InteractionType . MESSAGE_COMPONENT ) {
238
+ const customId = data . custom_id ;
239
+
240
+ if ( customId . startsWith ( 'prev_' ) || customId . startsWith ( 'next_' ) ) {
241
+ const [ action , index ] = customId . split ( '_' ) ;
242
+ currentIndex = parseInt ( index , 10 ) ;
243
+
244
+ if ( action === 'prev' && currentIndex > 0 ) {
245
+ currentIndex -= 1 ;
246
+ } else if ( action === 'next' && currentIndex < chunks . length - 1 ) {
247
+ currentIndex += 1 ;
248
+ }
249
+
250
+ // Respond with the updated message chunk
251
+ return res . send ( {
252
+ type : InteractionResponseType . UPDATE_MESSAGE ,
253
+ data : createMessageWithButtons ( currentIndex , chunks ) ,
254
+ } ) ;
255
+ }
256
+ }
257
+
154
258
console . error ( 'unknown interaction type' , type ) ;
155
259
return res . status ( 400 ) . json ( { error : 'unknown interaction type' } ) ;
156
260
} ) ;
0 commit comments