Skip to content

Commit d9cf4bd

Browse files
Merge pull request #168 from Pdzly/fix/starboard-embed
2 parents ca19091 + 8de981f commit d9cf4bd

2 files changed

Lines changed: 182 additions & 115 deletions

File tree

src/modules/starboard/starboard.listener.ts

Lines changed: 167 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { Embed, GuildMember, Message, SendableChannels } from "discord.js";
1+
import type {
2+
Embed,
3+
GuildMember,
4+
Message,
5+
SendableChannels,
6+
Snowflake,
7+
} from "discord.js";
28
import * as schedule from "node-schedule";
39
import { config } from "../../Config.js";
410
import { logger } from "../../logging.js";
@@ -10,7 +16,67 @@ import {
1016
createStarboardMessage,
1117
createStarboardMessageFromMessage,
1218
getStarboardMessageForOriginalMessageId,
13-
} from "./starboard.js";
19+
} from "./starboard.js"; // Debounce system for starboard reactions
20+
21+
// Debounce system for starboard reactions
22+
interface ReactionDebounceEntry {
23+
timeoutId: NodeJS.Timeout;
24+
addCount: number;
25+
removeCount: number;
26+
}
27+
28+
const reactionDebounceMap = new Map<Snowflake, ReactionDebounceEntry>();
29+
const DEBOUNCE_DELAY = 2000; // 2 seconds
30+
31+
export const debounceStarboardReaction = (
32+
messageId: Snowflake,
33+
isAdd: boolean,
34+
callback: () => Promise<void>,
35+
): void => {
36+
const existing = reactionDebounceMap.get(messageId);
37+
38+
if (existing) {
39+
// Clear the existing timeout
40+
clearTimeout(existing.timeoutId);
41+
42+
// Update counters
43+
if (isAdd) {
44+
existing.addCount++;
45+
} else {
46+
existing.removeCount++;
47+
}
48+
49+
// Check if we should cancel execution (equal adds and removes)
50+
const shouldCancel = existing.addCount === existing.removeCount;
51+
52+
if (shouldCancel) {
53+
// Remove from map and don't execute
54+
reactionDebounceMap.delete(messageId);
55+
return;
56+
}
57+
} else {
58+
// Create new entry
59+
reactionDebounceMap.set(messageId, {
60+
timeoutId: setTimeout(() => {}, 0), // Placeholder, will be replaced immediately
61+
addCount: isAdd ? 1 : 0,
62+
removeCount: isAdd ? 0 : 1,
63+
});
64+
}
65+
66+
const entry = reactionDebounceMap.get(messageId);
67+
if (!entry) {
68+
return;
69+
}
70+
71+
// Set new timeout
72+
entry.timeoutId = setTimeout(async () => {
73+
try {
74+
await callback();
75+
} finally {
76+
reactionDebounceMap.delete(messageId);
77+
}
78+
}, DEBOUNCE_DELAY);
79+
};
1480

1581
const messageFetcher = new MessageFetcher();
1682

@@ -153,7 +219,7 @@ export const StarboardListener: EventListener = {
153219
},
154220
async messageReactionAdd(_, reaction) {
155221
let message = reaction.message;
156-
if (message.partial) message = await reaction.message.fetch();
222+
if (message.partial) message = await message.fetch();
157223
if (
158224
!message.inGuild() ||
159225
message.author.bot ||
@@ -162,67 +228,76 @@ export const StarboardListener: EventListener = {
162228
reaction.emoji.name !== config.starboard.emojiId
163229
)
164230
return;
165-
await reaction.fetch();
166-
const count = reaction.count || 1;
167-
if (count >= config.starboard.threshold) {
168-
const starboardChannel = await message.guild.channels.fetch(
169-
config.starboard.channel,
170-
);
171231

172-
if (!starboardChannel?.isTextBased() || !starboardChannel.isSendable()) {
173-
logger.error(
174-
"Starboard channel not found, not a text channel or not sendable",
232+
debounceStarboardReaction(message.id, true, async () => {
233+
reaction = await reaction.fetch();
234+
const count = reaction.count || 1;
235+
console.log(count, count >= config.starboard.threshold);
236+
237+
if (count >= config.starboard.threshold) {
238+
const starboardChannel = await message.guild.channels.fetch(
239+
config.starboard.channel,
175240
);
176-
return;
177-
}
178-
const existingStarboardMessage =
179-
await getStarboardMessageForOriginalMessageId(message.id);
180-
try {
181-
const member = await getMember(message);
182241

183-
if (!member) {
184-
logger.info(
185-
"Member not found for reaction message id %s, skipping",
186-
message.id,
187-
);
188-
return;
189-
}
190-
if (existingStarboardMessage) {
191-
// Already on the starboard so update it
192-
await updateStarboardMessage(
193-
starboardChannel,
194-
existingStarboardMessage,
195-
message,
196-
member,
197-
count,
242+
if (
243+
!starboardChannel?.isTextBased() ||
244+
!starboardChannel.isSendable()
245+
) {
246+
logger.error(
247+
"Starboard channel not found, not a text channel or not sendable",
198248
);
199249
return;
200250
}
201251

202-
const starboardMessageContent = await createStarboardMessageFromMessage(
203-
message,
204-
member,
205-
count,
206-
);
252+
const existingStarboardMessage =
253+
await getStarboardMessageForOriginalMessageId(message.id);
254+
try {
255+
const member = await getMember(message);
207256

208-
const starboardMessage = await starboardChannel.send({
209-
...starboardMessageContent,
210-
allowedMentions: {
211-
parse: [],
212-
},
213-
});
214-
if (!existingStarboardMessage) {
215-
await createStarboardMessage(
216-
message.id,
217-
message.channelId,
218-
starboardMessage.id,
219-
);
257+
if (!member) {
258+
logger.info(
259+
"Member not found for reaction message id %s, skipping",
260+
message.id,
261+
);
262+
return;
263+
}
264+
265+
if (existingStarboardMessage) {
266+
// Already on the starboard so update it
267+
await updateStarboardMessage(
268+
starboardChannel,
269+
existingStarboardMessage,
270+
message,
271+
member,
272+
count,
273+
);
274+
return;
275+
}
276+
277+
const starboardMessageContent =
278+
await createStarboardMessageFromMessage(message, member, count);
279+
280+
const starboardMessage = await starboardChannel.send({
281+
...starboardMessageContent,
282+
allowedMentions: {
283+
parse: [],
284+
},
285+
});
286+
287+
if (!existingStarboardMessage) {
288+
await createStarboardMessage(
289+
message.id,
290+
message.channelId,
291+
starboardMessage.id,
292+
);
293+
}
294+
} catch (error) {
295+
logger.error("Error sending starboard message", error);
220296
}
221-
} catch (error) {
222-
logger.error("Error sending starboard message", error);
223297
}
224-
}
298+
});
225299
},
300+
226301
async messageReactionRemove(_, reaction) {
227302
let message = reaction.message;
228303
if (message.partial) message = await reaction.message.fetch();
@@ -234,47 +309,52 @@ export const StarboardListener: EventListener = {
234309
reaction.emoji.name !== config.starboard.emojiId
235310
)
236311
return;
237-
await reaction.fetch();
238-
const count = reaction.count || 0;
239-
const existingStarboardMessage =
240-
await getStarboardMessageForOriginalMessageId(message.id);
241-
if (!existingStarboardMessage) return;
242-
try {
243-
const member = await getMember(message);
244312

245-
if (!member) {
246-
logger.info(
247-
"Member not found for reaction message id: %s",
248-
reaction.message.id,
249-
);
250-
return;
251-
}
313+
debounceStarboardReaction(message.id, false, async () => {
314+
reaction = await reaction.fetch();
315+
const count = reaction.count || 0;
252316

253-
if (existingStarboardMessage) {
254-
const starboardChannel = await message.guild.channels.fetch(
255-
config.starboard.channel,
256-
);
257-
if (
258-
!starboardChannel?.isTextBased() ||
259-
!starboardChannel.isSendable()
260-
) {
261-
logger.error(
262-
"Starboard channel not found, not a text channel or not sendable",
317+
const existingStarboardMessage =
318+
await getStarboardMessageForOriginalMessageId(message.id);
319+
if (!existingStarboardMessage) return;
320+
321+
try {
322+
const member = await getMember(message);
323+
324+
if (!member) {
325+
logger.info(
326+
"Member not found for reaction message id: %s",
327+
reaction.message.id,
263328
);
264329
return;
265330
}
266331

267-
await updateStarboardMessage(
268-
starboardChannel,
269-
existingStarboardMessage,
270-
message,
271-
member,
272-
count,
273-
);
332+
if (existingStarboardMessage) {
333+
const starboardChannel = await message.guild.channels.fetch(
334+
config.starboard.channel,
335+
);
336+
if (
337+
!starboardChannel?.isTextBased() ||
338+
!starboardChannel.isSendable()
339+
) {
340+
logger.error(
341+
"Starboard channel not found, not a text channel or not sendable",
342+
);
343+
return;
344+
}
345+
346+
await updateStarboardMessage(
347+
starboardChannel,
348+
existingStarboardMessage,
349+
message,
350+
member,
351+
count,
352+
);
353+
}
354+
} catch (error) {
355+
logger.error("Error sending starboard message", error);
274356
}
275-
} catch (error) {
276-
logger.error("Error sending starboard message", error);
277-
}
357+
});
278358
},
279359
};
280360

src/modules/starboard/starboard.ts

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,10 @@ export const extractEmbedAndFilesFromMessage: (
6161
message: Message,
6262
member: GuildMember,
6363
stars: number,
64-
starboardMessage?: Message,
6564
) => Promise<{
6665
embed: EmbedBuilder;
6766
files?: AttachmentBuilder[];
68-
}> = async (
69-
message: Message,
70-
member: GuildMember,
71-
stars: number,
72-
starboardMessage?: Message,
73-
) => {
67+
}> = async (message: Message, member: GuildMember, stars: number) => {
7468
const embed = createStandardEmbed(member)
7569
.setColor(getColorForStars(stars))
7670
.setAuthor({
@@ -81,20 +75,18 @@ export const extractEmbedAndFilesFromMessage: (
8175
.setURL(message.url)
8276
.setDescription(message.content.length > 0 ? message.content : null);
8377
const files: AttachmentBuilder[] = [];
84-
if (!starboardMessage) {
85-
const imageOrGif = await getImageOrGifEmbed(message);
86-
if (imageOrGif) {
87-
if ("gifBuffer" in imageOrGif) {
88-
files.push(
89-
new AttachmentBuilder(imageOrGif.gifBuffer, {
90-
name: imageOrGif.gifName,
91-
}),
92-
);
93-
embed.setImage(`attachment://${imageOrGif.gifName}`);
94-
} else if (imageOrGif.url) {
95-
// If we have a regular image URL, use it directly
96-
embed.setImage(imageOrGif.url);
97-
}
78+
const imageOrGif = await getImageOrGifEmbed(message);
79+
if (imageOrGif) {
80+
if ("gifBuffer" in imageOrGif) {
81+
files.push(
82+
new AttachmentBuilder(imageOrGif.gifBuffer, {
83+
name: imageOrGif.gifName,
84+
}),
85+
);
86+
embed.setImage(`attachment://${imageOrGif.gifName}`);
87+
} else if (imageOrGif.url) {
88+
// If we have a regular image URL, use it directly
89+
embed.setImage(imageOrGif.url);
9890
}
9991
}
10092

@@ -129,7 +121,6 @@ export const createStarboardMessageFromMessage: (
129121
referencedMessage,
130122
referencedMessage.member,
131123
-1,
132-
starboardMessage,
133124
);
134125

135126
referencedEmbed.setAuthor({
@@ -146,12 +137,7 @@ export const createStarboardMessageFromMessage: (
146137
}
147138
}
148139
const { embed, files: mainmessageFiles } =
149-
await extractEmbedAndFilesFromMessage(
150-
message,
151-
member,
152-
stars,
153-
starboardMessage,
154-
);
140+
await extractEmbedAndFilesFromMessage(message, member, stars);
155141
embeds.push(embed);
156142
if (mainmessageFiles) {
157143
files.push(...mainmessageFiles);
@@ -185,6 +171,7 @@ export const getImageOrGifEmbed: (message: Message) => Promise<
185171
const embeds = message.embeds.filter(
186172
(emb) => emb.data.type === "image" || emb.data.type === "gifv",
187173
);
174+
console.log(embeds);
188175
if (embeds.length > 0) {
189176
const embed = embeds[0];
190177

0 commit comments

Comments
 (0)