@@ -14,6 +14,11 @@ import {
1414} from 'discord-interactions' ;
1515import { getRandomEmoji } from './utils.js' ;
1616
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+
1722// Create an express app
1823const app = express ( ) ;
1924// Get port, or default to 3000
@@ -25,6 +30,57 @@ app.get('/', (req, res) => {
2530} ) ;
2631
2732// 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+
2884async function sendPlaceholderResponse ( res , placeholderResponse ) {
2985 await res . send ( {
3086 type : InteractionResponseType . CHANNEL_MESSAGE_WITH_SOURCE ,
@@ -56,19 +112,53 @@ async function fetchAnswer(question) {
56112 return rawResponse || 'No answer provided.' ;
57113}
58114
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 ,
68146 flags : InteractionResponseFlags . EPHEMERAL ,
69147 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+ }
72162}
73163
74164/**
@@ -97,42 +187,34 @@ app.post('/interactions', verifyKeyMiddleware(process.env.DISCORD_PUBLIC_KEY), a
97187 if ( name === 'ask' ) {
98188 const context = req . body . context ;
99189 const userId = context === 0 ? req . body . member . user . id : req . body . user . id
100-
101190 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` ;
103200 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." ;
104202
105203 // Send a placeholder response
106204 await sendPlaceholderResponse ( res , initialMessage ) ;
107205
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
122207 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 ) ;
129210 } finally {
130- // Ensure cleanup and state updates
131- isFetching = false ; // Mark fetching as complete
132- clearInterval ( interval ) ; // Stop the dot interval
211+ stopLoadingDots ( )
133212 }
134213
135- return sendFollowUpResponse ( endpoint , followUpMessage ) ;
214+ // Send the follow-up response
215+ sendFollowUpResponse ( endpoint , followUpMessage ) ;
216+
217+ return ;
136218 }
137219
138220 // "test" command
@@ -151,6 +233,28 @@ app.post('/interactions', verifyKeyMiddleware(process.env.DISCORD_PUBLIC_KEY), a
151233 return res . status ( 400 ) . json ( { error : 'unknown command' } ) ;
152234 }
153235
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+
154258 console . error ( 'unknown interaction type' , type ) ;
155259 return res . status ( 400 ) . json ( { error : 'unknown interaction type' } ) ;
156260} ) ;
0 commit comments