11import discord
2- from discord .ext import commands
2+ from discord .ext import commands , tasks
33import json
44from pathlib import Path
55from datetime import datetime , timedelta
66import asyncio
7+ from collections import defaultdict
78from cogs .logging .logger import CogLogger
89
910logger = CogLogger ('VoteBans' )
@@ -20,13 +21,216 @@ def __init__(self, bot):
2021 self .data_path = Path ("data/votebans.json" )
2122 self .vote_data = self .load_data ()
2223
23- # Rate limiting control
24+ # Rate limiting and caching
2425 self .message_edit_queue = asyncio .Queue ()
2526 self .last_edit_time = {}
26- self .edit_cooldown = 2.0 # 2 seconds between edits per message
27+ self .edit_cooldown = 2.0
28+ self .reaction_cache = defaultdict (dict ) # message_id: {emoji: [user_ids]}
29+ self .last_reaction_check = {}
2730
28- # Start the message edit processor
31+ # Start processors
2932 self .bot .loop .create_task (self .process_message_edits ())
33+ self .verify_reactions .start ()
34+
35+ def cog_unload (self ):
36+ self .verify_reactions .cancel ()
37+
38+ @tasks .loop (minutes = 5 ) # Reduced from 30 seconds to 5 minutes
39+ async def verify_reactions (self ):
40+ """Efficiently verify reactions with caching and batching"""
41+ logger .debug ("Running optimized reaction verification..." )
42+
43+ # Process in batches to avoid rate limits
44+ active_votes = [v for v in self .vote_data .values () if not v .get ("completed" , True )]
45+ for i in range (0 , len (active_votes ), 5 ): # Process 5 votes at a time
46+ batch = active_votes [i :i + 5 ]
47+ await self ._process_batch (batch )
48+ await asyncio .sleep (10 ) # Space out batches
49+
50+ async def _process_batch (self , batch ):
51+ """Process a batch of votes efficiently"""
52+ for vote_info in batch :
53+ try :
54+ message_id = vote_info ["message_id" ]
55+ channel_id = vote_info ["channel_id" ]
56+
57+ # Check if we recently processed this message
58+ if self .last_reaction_check .get (message_id , 0 ) > datetime .now ().timestamp () - 300 :
59+ continue
60+
61+ channel = self .bot .get_channel (channel_id )
62+ if not channel :
63+ continue
64+
65+ message = await self .safe_fetch_message (channel , message_id )
66+ if not message :
67+ vote_info ["completed" ] = True
68+ continue
69+
70+ # Get reactions efficiently
71+ yes_reaction = next ((r for r in message .reactions if str (r .emoji ) == "✅" ), None )
72+ no_reaction = next ((r for r in message .reactions if str (r .emoji ) == "❌" ), None )
73+
74+ # Update cache
75+ cache_entry = {}
76+ if yes_reaction :
77+ cache_entry ["✅" ] = [user .id async for user in yes_reaction .users () if not user .bot ]
78+ if no_reaction :
79+ cache_entry ["❌" ] = [user .id async for user in no_reaction .users () if not user .bot ]
80+ self .reaction_cache [message_id ] = cache_entry
81+
82+ # Update stored votes if different
83+ current_yes = set (cache_entry .get ("✅" , []))
84+ current_no = set (cache_entry .get ("❌" , []))
85+ stored_yes = set (vote_info ["votes" ]["✅" ])
86+ stored_no = set (vote_info ["votes" ]["❌" ])
87+
88+ if current_yes != stored_yes or current_no != stored_no :
89+ vote_info ["votes" ]["✅" ] = list (current_yes )
90+ vote_info ["votes" ]["❌" ] = list (current_no )
91+
92+ total_votes = len (current_yes ) + len (current_no )
93+ if total_votes >= self .required_votes :
94+ await self .complete_vote (str (vote_info ["user_id" ]), message )
95+ else :
96+ await self .queue_message_edit (message_id , channel_id ,
97+ await self .create_vote_embed (vote_info ))
98+
99+ self .last_reaction_check [message_id ] = datetime .now ().timestamp ()
100+
101+ except Exception as e :
102+ logger .error (f"Error in batch processing: { e } " )
103+ continue
104+
105+ @commands .Cog .listener ()
106+ async def on_raw_reaction_add (self , payload ):
107+ """Handle reaction adds with cache support"""
108+ if not await self .should_process_reaction (payload ):
109+ return
110+
111+ emoji = str (payload .emoji )
112+ message_id = payload .message_id
113+
114+ # Update cache immediately
115+ if message_id in self .reaction_cache :
116+ if emoji in self .reaction_cache [message_id ]:
117+ if payload .user_id not in self .reaction_cache [message_id ][emoji ]:
118+ self .reaction_cache [message_id ][emoji ].append (payload .user_id )
119+ else :
120+ self .reaction_cache [message_id ][emoji ] = [payload .user_id ]
121+
122+ await self .process_reaction_change (payload , added = True )
123+
124+ @commands .Cog .listener ()
125+ async def on_raw_reaction_remove (self , payload ):
126+ """Handle reaction removes with cache support"""
127+ if not await self .should_process_reaction (payload ):
128+ return
129+
130+ emoji = str (payload .emoji )
131+ message_id = payload .message_id
132+
133+ # Update cache immediately
134+ if message_id in self .reaction_cache and emoji in self .reaction_cache [message_id ]:
135+ if payload .user_id in self .reaction_cache [message_id ][emoji ]:
136+ self .reaction_cache [message_id ][emoji ].remove (payload .user_id )
137+
138+ await self .process_reaction_change (payload , added = False )
139+
140+ async def should_process_reaction (self , payload ):
141+ """Check if we should process this reaction event"""
142+ if payload .guild_id not in self .main_guilds :
143+ return False
144+ if payload .user_id == self .bot .user .id :
145+ return False
146+
147+ # Find the vote
148+ vote_info = next ((v for v in self .vote_data .values ()
149+ if v .get ("message_id" ) == payload .message_id ), None )
150+
151+ if not vote_info or vote_info .get ("completed" , True ):
152+ return False
153+
154+ return str (payload .emoji ) in ["✅" , "❌" ]
155+
156+ async def process_reaction_change (self , payload , added ):
157+ """Process reaction changes efficiently"""
158+ emoji = str (payload .emoji )
159+ opposite = "❌" if emoji == "✅" else "✅"
160+ message_id = payload .message_id
161+
162+ # Find the vote
163+ user_id_str , vote_info = next (((k , v ) for k , v in self .vote_data .items ()
164+ if v .get ("message_id" ) == message_id ), (None , None ))
165+ if not vote_info :
166+ return
167+
168+ # Update vote counts
169+ if added :
170+ if payload .user_id in vote_info ["votes" ][opposite ]:
171+ vote_info ["votes" ][opposite ].remove (payload .user_id )
172+ if payload .user_id not in vote_info ["votes" ][emoji ]:
173+ vote_info ["votes" ][emoji ].append (payload .user_id )
174+ else :
175+ if payload .user_id in vote_info ["votes" ][emoji ]:
176+ vote_info ["votes" ][emoji ].remove (payload .user_id )
177+
178+ self .save_data ()
179+
180+ # Get channel and message from cache if possible
181+ channel = self .bot .get_channel (payload .channel_id )
182+ if not channel :
183+ return
184+
185+ # Update embed
186+ await self .queue_message_edit (message_id , payload .channel_id ,
187+ await self .create_vote_embed (vote_info ))
188+
189+ # Check completion
190+ total_votes = len (vote_info ["votes" ]["✅" ]) + len (vote_info ["votes" ]["❌" ])
191+ if total_votes >= self .required_votes :
192+ message = await self .safe_fetch_message (channel , message_id )
193+ if message :
194+ await self .complete_vote (user_id_str , message )
195+
196+ async def create_vote_embed (self , vote_info ):
197+ """Create an embed from vote info (reused from your original code)"""
198+ user = self .bot .get_user (vote_info ["user_id" ])
199+ user_name = user .display_name if user else f"User { vote_info ['user_id' ]} "
200+
201+ yes_votes = len (vote_info ["votes" ]["✅" ])
202+ no_votes = len (vote_info ["votes" ]["❌" ])
203+
204+ embed = discord .Embed (
205+ title = f"Vote Ban: { user_name } " ,
206+ description = (
207+ f"**Reason:** { vote_info ['reason' ]} \n \n "
208+ f"Vote ✅ to ban ({ yes_votes } ), ❌ to keep ({ no_votes } )\n "
209+ f"{ self .required_votes } votes needed to decide"
210+ ),
211+ color = discord .Colour .random (),
212+ timestamp = datetime .now ()
213+ )
214+
215+ if user and user .avatar :
216+ embed .set_thumbnail (url = user .avatar .url )
217+
218+ # Add advocates if any
219+ advocate_text = []
220+ for advocate_id , advocate_data in vote_info .get ("advocates" , {}).items ():
221+ try :
222+ timestamp = int (datetime .fromisoformat (advocate_data ['timestamp' ]).timestamp ())
223+ advocate_text .append (
224+ f"• **{ advocate_data ['username' ]} ** - \" { advocate_data ['reason' ]} \" "
225+ f"(<t:{ timestamp } :R>)"
226+ )
227+ except (ValueError , KeyError ):
228+ advocate_text .append (f"• **{ advocate_data .get ('username' , 'Unknown' )} ** - \" { advocate_data .get ('reason' , 'No reason' )} \" " )
229+
230+ if advocate_text :
231+ embed .add_field (name = "Advocates" , value = "\n " .join (advocate_text [:10 ]), inline = False )
232+
233+ return embed
30234
31235 def load_data (self ):
32236 try :
@@ -381,102 +585,6 @@ async def update_vote_embed(self, vote_info, message):
381585
382586 await self .queue_message_edit (message .id , message .channel .id , embed )
383587
384- @commands .Cog .listener ()
385- async def on_raw_reaction_add (self , payload ):
386- if payload .guild_id not in self .main_guilds :
387- return
388-
389- if payload .user_id == self .bot .user .id :
390- return
391-
392- # Find vote by message_id
393- vote_info = None
394- user_id_str = None
395- for uid , data in self .vote_data .items ():
396- if data .get ("message_id" ) == payload .message_id :
397- vote_info = data
398- user_id_str = uid
399- break
400-
401- if not vote_info or vote_info .get ("completed" , True ):
402- return
403-
404- emoji = str (payload .emoji )
405- if emoji not in ["✅" , "❌" ]:
406- return
407-
408- guild = self .bot .get_guild (payload .guild_id )
409- if not guild :
410- return
411-
412- member = guild .get_member (payload .user_id )
413- if not member or await self .is_staff (member ):
414- return
415-
416- channel = self .bot .get_channel (payload .channel_id )
417- if not channel :
418- return
419-
420- message = await self .safe_fetch_message (channel , payload .message_id )
421- if not message :
422- return
423-
424- # Remove user from opposite vote and add to current
425- opposite = "❌" if emoji == "✅" else "✅"
426- if payload .user_id in vote_info ["votes" ][opposite ]:
427- vote_info ["votes" ][opposite ].remove (payload .user_id )
428- try :
429- await message .remove_reaction (opposite , member )
430- except discord .HTTPException :
431- pass
432-
433- if payload .user_id not in vote_info ["votes" ][emoji ]:
434- vote_info ["votes" ][emoji ].append (payload .user_id )
435-
436- self .save_data ()
437-
438- # Update embed
439- await self .update_vote_embed (vote_info , message )
440-
441- # Complete vote if needed
442- total_votes = len (vote_info ["votes" ]["✅" ]) + len (vote_info ["votes" ]["❌" ])
443- if total_votes >= self .required_votes :
444- await self .complete_vote (user_id_str , message )
445-
446- @commands .Cog .listener ()
447- async def on_raw_reaction_remove (self , payload ):
448- if payload .guild_id not in self .main_guilds :
449- return
450-
451- if payload .user_id == self .bot .user .id :
452- return
453-
454- # Find vote by message_id
455- vote_info = None
456- for uid , data in self .vote_data .items ():
457- if data .get ("message_id" ) == payload .message_id :
458- vote_info = data
459- break
460-
461- if not vote_info or vote_info .get ("completed" , True ):
462- return
463-
464- emoji = str (payload .emoji )
465- if emoji not in ["✅" , "❌" ]:
466- return
467-
468- # Remove user from vote list if they exist
469- if payload .user_id in vote_info ["votes" ][emoji ]:
470- vote_info ["votes" ][emoji ].remove (payload .user_id )
471- self .save_data ()
472-
473- # Update embed
474- channel = self .bot .get_channel (payload .channel_id )
475- if channel :
476- message = await self .safe_fetch_message (channel , payload .message_id )
477- if message :
478- await self .update_vote_embed (vote_info , message )
479-
480588 async def complete_vote (self , user_id_str , message ):
481589 """Complete a vote and apply the result"""
482590 vote_info = self .vote_data [user_id_str ]
0 commit comments