Skip to content

Commit 9bf9af9

Browse files
authored
Merge pull request #47 from DefangLabs/linda-discord-bot-char-limit
Fix Discord bot character limit
2 parents 5f0f75b + 60bcb93 commit 9bf9af9

File tree

3 files changed

+143
-39
lines changed

3 files changed

+143
-39
lines changed

discord-bot/app.js

Lines changed: 141 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import {
1414
} from 'discord-interactions';
1515
import { 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
1823
const 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+
2884
async 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-Za-z0-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
});

discord-bot/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

discord-bot/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"license": "MIT",
1818
"dependencies": {
1919
"discord-interactions": "^4.0.0",
20-
"dotenv": "^16.0.3",
20+
"dotenv": "^16.4.7",
2121
"express": "^4.18.2"
2222
},
2323
"devDependencies": {

0 commit comments

Comments
 (0)