Skip to content

Commit a498dc8

Browse files
committed
> Votebans bug cleaning & optimization
1 parent acfc4b0 commit a498dc8

1 file changed

Lines changed: 208 additions & 100 deletions

File tree

cogs/bronx/VoteBans.py

Lines changed: 208 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import discord
2-
from discord.ext import commands
2+
from discord.ext import commands, tasks
33
import json
44
from pathlib import Path
55
from datetime import datetime, timedelta
66
import asyncio
7+
from collections import defaultdict
78
from cogs.logging.logger import CogLogger
89

910
logger = 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

Comments
 (0)