Skip to content

Commit 879d5d2

Browse files
Add remove-unmatched command to delete users without matches in Matcherino; implement confirmation process and logging
1 parent 3673209 commit 879d5d2

File tree

2 files changed

+246
-164
lines changed

2 files changed

+246
-164
lines changed

cogs/matcherino_cog.py

Lines changed: 246 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class MatcherinoCog(commands.Cog):
1414

1515
def __init__(self, bot):
1616
self.bot = bot
17+
self._remove_unmatched_users = {} # Store users to remove per interaction ID
1718

1819
@app_commands.command(name="match-free-agents", description="Match free agents from Matcherino with Discord users")
1920
@app_commands.default_permissions(administrator=True)
@@ -113,60 +114,88 @@ async def match_participants_with_db_users(self, participants, db_users):
113114

114115
# Track participant names that have been processed
115116
processed_participants = set()
117+
118+
# Find our target user for debugging
119+
target_user = next((user for user in db_users if user.get('username') == '0cxld'), None)
120+
if target_user:
121+
logger.info("=== Target User Info ===")
122+
logger.info(f"Discord: {target_user.get('username')}")
123+
logger.info(f"Discord ID: {target_user.get('user_id')}")
124+
logger.info(f"Matcherino: {target_user.get('matcherino_username')}")
116125

117126
logger.info(f"Starting matching process with {len(participants)} participants and {len(db_users)} database users")
118127

119128
# Pre-process db_users into dictionaries for O(1) lookups
120-
# 1. Dictionary for exact matches (lowercase full username -> user)
129+
# Dictionary mapping full lowercase matcherino username to user
121130
exact_match_dict = {}
122-
# 2. Dictionary for name-only matches (lowercase name part -> list of users)
131+
# Dictionary mapping lowercase name (without ID) to list of users
123132
name_match_dict = {}
124133

125134
for user in db_users:
126-
matcherino_username = user.get('matcherino_username', '').strip()
135+
matcherino_username = user.get('matcherino_username', '').strip().lower()
127136
if not matcherino_username:
128137
logger.warning(f"User {user.get('username')} has empty Matcherino username")
129138
continue
130-
131-
logger.debug(f"Processing DB user: Discord={user.get('username')}, Matcherino={matcherino_username}")
132-
133-
# Store for exact match lookup
134-
exact_match_dict[matcherino_username.lower()] = user
135139

136-
# Store for name-only match lookup
137-
name_part = matcherino_username.split('#')[0].strip().lower()
140+
# Extra logging for our target user
141+
if user.get('username') == '0cxld':
142+
logger.info("=== Processing Target User ===")
143+
logger.info(f"Original matcherino_username: {matcherino_username}")
144+
145+
# Store full username for exact matches
146+
exact_match_dict[matcherino_username] = user
147+
148+
# Store base name for name-only matches
149+
name_part = matcherino_username.split('#')[0].strip()
138150
if name_part not in name_match_dict:
139151
name_match_dict[name_part] = []
140152
name_match_dict[name_part].append(user)
153+
154+
# More logging for target user
155+
if user.get('username') == '0cxld':
156+
logger.info(f"Stored in exact_match_dict with key: {matcherino_username}")
157+
logger.info(f"Stored in name_match_dict with key: {name_part}")
141158

142159
logger.info(f"Built lookup dictionaries: {len(exact_match_dict)} exact usernames, {len(name_match_dict)} base names")
143160

161+
# If we found our target user, check the dictionaries
162+
if target_user:
163+
target_matcherino = target_user.get('matcherino_username', '').lower()
164+
target_name_part = target_matcherino.split('#')[0].strip()
165+
logger.info("=== Checking Target User in Dictionaries ===")
166+
logger.info(f"Looking for exact match with: {target_matcherino}")
167+
logger.info(f"Looking for name match with: {target_name_part}")
168+
logger.info(f"Found in exact_match_dict: {target_matcherino in exact_match_dict}")
169+
logger.info(f"Found in name_match_dict: {target_name_part in name_match_dict}")
170+
144171
# Process each participant once with O(1) lookups
145172
for participant in participants:
146-
participant_name = participant.get('name', '').strip()
173+
participant_name = participant.get('name', '').strip().lower()
147174
game_username = participant.get('game_username', '').strip()
148175

149176
if not participant_name:
150177
logger.warning("Found participant with empty name, skipping")
151178
continue
152179

153-
if participant_name.lower() in processed_participants:
180+
if participant_name in processed_participants:
154181
logger.debug(f"Participant {participant_name} already processed, skipping")
155182
continue
156183

157-
logger.debug(f"Processing participant: {participant_name} (Game username: {game_username})")
158-
159-
# Format for exact match: displayName#userId
160-
expected_full_username = f"{participant_name}#{participant.get('user_id', '')}"
161-
expected_full_username_lower = expected_full_username.lower()
162-
163-
logger.debug(f"Checking for exact match with: {expected_full_username}")
184+
# Extra logging for participants that might match our target user
185+
if target_user:
186+
target_matcherino = target_user.get('matcherino_username', '').lower()
187+
target_name_part = target_matcherino.split('#')[0].strip()
188+
if (participant_name == target_matcherino):
189+
logger.info("=== Found Potential Matching Participant ===")
190+
logger.info(f"Participant name: {participant_name}")
191+
logger.info(f"Game username: {game_username}")
192+
logger.info(f"User ID: {participant.get('user_id', '')}")
164193

165-
# Check for exact match with O(1) lookup
166-
if expected_full_username_lower in exact_match_dict:
167-
user = exact_match_dict[expected_full_username_lower]
194+
# Check for exact match with O(1) lookup using full username
195+
if participant_name in exact_match_dict:
196+
user = exact_match_dict[participant_name]
168197
if user['user_id'] not in matched_discord_ids:
169-
logger.info(f"Found exact match: '{user.get('matcherino_username', '')}' matches with '{expected_full_username}'")
198+
# logger.info(f"Found exact match: '{user.get('matcherino_username', '')}' matches with '{participant_name}'")
170199
exact_matches.append({
171200
'participant': participant_name,
172201
'participant_id': participant.get('user_id', ''),
@@ -177,27 +206,21 @@ async def match_participants_with_db_users(self, participants, db_users):
177206
'db_matcherino_username': user.get('matcherino_username', '')
178207
})
179208
matched_discord_ids.add(user['user_id'])
180-
processed_participants.add(participant_name.lower())
209+
processed_participants.add(participant_name)
181210
continue
182-
else:
183-
logger.debug(f"Found exact match but Discord ID {user['user_id']} already matched")
184211

185212
# If no exact match, try name-only match
186-
name_only = participant_name.split('#')[0].strip().lower()
187-
logger.debug(f"Trying name-only match with: {name_only}")
188-
potential_matches = name_match_dict.get(name_only, [])
189-
190-
if potential_matches:
191-
logger.debug(f"Found {len(potential_matches)} potential name-only matches for {name_only}")
213+
name_part = participant_name.split('#')[0].strip()
214+
potential_matches = name_match_dict.get(name_part, [])
192215

193216
# Filter out already matched users
194217
potential_matches = [user for user in potential_matches if user['user_id'] not in matched_discord_ids]
195-
logger.debug(f"After filtering matched users: {len(potential_matches)} potential matches remain")
196218

219+
# Add to appropriate match category
197220
if len(potential_matches) == 1:
198221
# Single name match found
199222
match = potential_matches[0]
200-
logger.info(f"Found name-only match: '{match.get('matcherino_username', '')}' base name matches with '{participant_name}'")
223+
# logger.info(f"Found name-only match: '{match.get('matcherino_username', '')}' base name matches with '{participant_name}'")
201224
name_only_matches.append({
202225
'participant': participant_name,
203226
'participant_tag': game_username,
@@ -208,7 +231,7 @@ async def match_participants_with_db_users(self, participants, db_users):
208231
'db_matcherino_username': match.get('matcherino_username', '')
209232
})
210233
matched_discord_ids.add(match['user_id'])
211-
processed_participants.add(participant_name.lower())
234+
processed_participants.add(participant_name)
212235
elif len(potential_matches) > 1:
213236
# Multiple potential matches - ambiguous
214237
logger.info(f"Found ambiguous match: {participant_name} matches with multiple users")
@@ -221,7 +244,25 @@ async def match_participants_with_db_users(self, participants, db_users):
221244
'matcherino_username': user.get('matcherino_username', '')
222245
} for user in potential_matches]
223246
})
224-
processed_participants.add(participant_name.lower())
247+
processed_participants.add(participant_name)
248+
249+
# After processing all participants, check if our target user was matched
250+
if target_user:
251+
target_id = target_user['user_id']
252+
logger.info("=== Final Match Status for Target User ===")
253+
logger.info(f"Target user matched: {target_id in matched_discord_ids}")
254+
if target_id in matched_discord_ids:
255+
if any(m['discord_id'] == target_id for m in exact_matches):
256+
logger.info("Matched via exact match")
257+
elif any(m['discord_id'] == target_id for m in name_only_matches):
258+
logger.info("Matched via name-only match")
259+
else:
260+
logger.info("User was not matched at all")
261+
logger.info("Checking processed participants...")
262+
target_matcherino = target_user.get('matcherino_username', '').lower()
263+
target_name_part = target_matcherino.split('#')[0].strip()
264+
logger.info(f"Target name processed: {target_matcherino in processed_participants}")
265+
logger.info(f"Target base name processed: {target_name_part in [p.split('#')[0].strip() for p in processed_participants]}")
225266

226267
# Collect unmatched participants and users in a single pass
227268
unmatched_participants = [
@@ -420,5 +461,174 @@ async def list_unmatched_command(self, interaction: discord.Interaction):
420461
logger.error(f"Error listing unmatched participants: {e}", exc_info=True)
421462
await interaction.followup.send(f"An error occurred: {str(e)}", ephemeral=True)
422463

464+
@app_commands.command(name="remove-unmatched", description="Remove users from the registration database who aren't on Matcherino")
465+
@app_commands.default_permissions(administrator=True)
466+
async def remove_unmatched_command(self, interaction: discord.Interaction):
467+
"""Remove users from the registration database who aren't found in the Matcherino tournament."""
468+
if not self.bot.TOURNAMENT_ID:
469+
await interaction.response.send_message("MATCHERINO_TOURNAMENT_ID is not set. Please set it in the .env file.", ephemeral=True)
470+
return
471+
472+
await interaction.response.defer(ephemeral=True)
473+
474+
try:
475+
logger.info("Starting unmatched user removal process")
476+
477+
# Get all registered users with their Matcherino usernames
478+
db_users = await self.bot.db.get_all_matcherino_usernames()
479+
if not db_users:
480+
await interaction.followup.send("No users with Matcherino usernames found in database.", ephemeral=True)
481+
return
482+
483+
# Fetch all participants from Matcherino
484+
async with MatcherinoScraper() as scraper:
485+
participants = await scraper.get_tournament_participants(self.bot.TOURNAMENT_ID)
486+
487+
if not participants:
488+
await interaction.followup.send("No participants found in the Matcherino tournament.", ephemeral=True)
489+
return
490+
491+
# Create sets for O(1) lookups
492+
matcherino_participants = {
493+
f"{p['name']}#{p['user_id']}".lower(): p
494+
for p in participants
495+
if p['name'] and p['user_id']
496+
}
497+
498+
users_to_remove = []
499+
for user in db_users:
500+
matcherino_username = user.get('matcherino_username', '').strip().lower()
501+
if not matcherino_username:
502+
continue
503+
504+
if matcherino_username not in matcherino_participants:
505+
users_to_remove.append(user)
506+
507+
if not users_to_remove:
508+
await interaction.followup.send("No unmatched users found to remove.", ephemeral=True)
509+
return
510+
511+
# Create preview file
512+
preview_content = ["Users that will be unregistered:", ""]
513+
for user in users_to_remove:
514+
preview_content.append(f"• {user['username']} (Discord ID: {user['user_id']}, Matcherino: {user['matcherino_username']})")
515+
516+
preview_file = discord.File(
517+
io.BytesIO("\n".join(preview_content).encode("utf-8")),
518+
filename="users_to_remove.txt"
519+
)
520+
521+
# Store users to remove for this interaction
522+
self._remove_unmatched_users[str(interaction.id)] = users_to_remove
523+
524+
# Create confirm/cancel buttons
525+
confirm_button = discord.ui.Button(
526+
style=discord.ButtonStyle.danger,
527+
label=f"Confirm Remove ({len(users_to_remove)} users)",
528+
custom_id=f"remove_unmatched_confirm_{interaction.id}"
529+
)
530+
cancel_button = discord.ui.Button(
531+
style=discord.ButtonStyle.secondary,
532+
label="Cancel",
533+
custom_id=f"remove_unmatched_cancel_{interaction.id}"
534+
)
535+
536+
view = discord.ui.View()
537+
view.add_item(confirm_button)
538+
view.add_item(cancel_button)
539+
540+
await interaction.followup.send(
541+
f"Found {len(users_to_remove)} users to remove. Please review the attached file and confirm the action.",
542+
file=preview_file,
543+
view=view,
544+
ephemeral=True
545+
)
546+
547+
except Exception as e:
548+
logger.error(f"Error in remove-unmatched preview: {e}", exc_info=True)
549+
await interaction.followup.send(f"An error occurred: {str(e)}", ephemeral=True)
550+
551+
@commands.Cog.listener()
552+
async def on_interaction(self, interaction: discord.Interaction):
553+
"""Handle button interactions for remove-unmatched command"""
554+
if not interaction.data or not isinstance(interaction.data, dict):
555+
return
556+
557+
custom_id = interaction.data.get("custom_id", "")
558+
if not custom_id:
559+
return
560+
561+
# Handle confirmation/cancellation
562+
if custom_id.startswith("remove_unmatched_"):
563+
await interaction.response.defer(ephemeral=True)
564+
original_interaction_id = custom_id.split("_")[-1]
565+
users_to_remove = self._remove_unmatched_users.get(original_interaction_id)
566+
567+
if not users_to_remove:
568+
await interaction.followup.send("This confirmation has expired. Please run the command again.", ephemeral=True)
569+
return
570+
571+
if custom_id.startswith("remove_unmatched_cancel_"):
572+
# Clean up stored data
573+
del self._remove_unmatched_users[original_interaction_id]
574+
await interaction.followup.send("Operation cancelled.", ephemeral=True)
575+
return
576+
577+
if custom_id.startswith("remove_unmatched_confirm_"):
578+
try:
579+
# Find the "Registered" role
580+
guild = interaction.guild
581+
registered_role = discord.utils.get(guild.roles, name="Registered")
582+
583+
# Remove unmatched users from database and remove their roles
584+
roles_removed = 0
585+
users_not_found = 0
586+
role_errors = 0
587+
588+
for user in users_to_remove:
589+
# Remove from database
590+
await self.bot.db.unregister_user(user['user_id'])
591+
592+
# Remove the "Registered" role if it exists
593+
if registered_role:
594+
try:
595+
member = await guild.fetch_member(user['user_id'])
596+
if member and registered_role in member.roles:
597+
await member.remove_roles(registered_role)
598+
roles_removed += 1
599+
logger.info(f"Removed 'Registered' role from user {user['username']} ({user['user_id']})")
600+
except discord.NotFound:
601+
users_not_found += 1
602+
logger.warning(f"User {user['username']} ({user['user_id']}) not found in guild")
603+
except discord.Forbidden:
604+
role_errors += 1
605+
logger.error(f"Bot doesn't have permission to remove roles from {user['username']} ({user['user_id']})")
606+
except Exception as e:
607+
role_errors += 1
608+
logger.error(f"Error removing role from {user['username']} ({user['user_id']}): {e}")
609+
610+
# Clean up stored data
611+
del self._remove_unmatched_users[original_interaction_id]
612+
613+
# Create result message
614+
status = []
615+
status.append(f"Successfully removed {len(users_to_remove)} users from the registration database.")
616+
if registered_role:
617+
status.append(f"\nRole status:")
618+
status.append(f"• Successfully removed 'Registered' role from {roles_removed} users")
619+
if users_not_found > 0:
620+
status.append(f"• {users_not_found} users were not found in the server")
621+
if role_errors > 0:
622+
status.append(f"• Failed to remove roles from {role_errors} users (see logs)")
623+
624+
await interaction.followup.send("\n".join(status), ephemeral=True)
625+
626+
except Exception as e:
627+
logger.error(f"Error removing unmatched users: {e}", exc_info=True)
628+
await interaction.followup.send(f"An error occurred: {str(e)}", ephemeral=True)
629+
# Clean up stored data even if there's an error
630+
if original_interaction_id in self._remove_unmatched_users:
631+
del self._remove_unmatched_users[original_interaction_id]
632+
423633
async def setup(bot):
424634
await bot.add_cog(MatcherinoCog(bot))

0 commit comments

Comments
 (0)