@@ -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"\n Role 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+
423633async def setup (bot ):
424634 await bot .add_cog (MatcherinoCog (bot ))
0 commit comments