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" ;
28import * as schedule from "node-schedule" ;
39import { config } from "../../Config.js" ;
410import { 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
1581const 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
0 commit comments