diff --git a/.cache.sqlite b/.cache.sqlite index 04d5f61..4c5f972 100644 Binary files a/.cache.sqlite and b/.cache.sqlite differ diff --git a/modules/commands/sports_command.py b/modules/commands/sports_command.py index a51c5c9..3bebeaa 100644 --- a/modules/commands/sports_command.py +++ b/modules/commands/sports_command.py @@ -37,7 +37,8 @@ class SportsCommand(BaseCommand): } # Custom team abbreviations to distinguish between leagues - TEAM_ABBREVIATIONS = { + # Only use -W suffixes for women's leagues + WOMENS_TEAM_ABBREVIATIONS = { # NWSL teams - use custom abbreviations to distinguish from MLS '21422': 'LA-W', # Angel City FC (Women's) '22187': 'BAY-W', # Bay FC (Women's) @@ -418,6 +419,45 @@ class SportsCommand(BaseCommand): channels_str = self.bot.config.get('Sports', 'channels', fallback='') return [channel.strip() for channel in channels_str.split(',') if channel.strip()] + def is_womens_league(self, sport: str, league: str) -> bool: + """Check if the league is a women's league""" + womens_leagues = { + ('basketball', 'wnba'), + ('soccer', 'usa.nwsl') + } + return (sport, league) in womens_leagues + + def get_team_abbreviation(self, team_id: str, team_abbreviation: str, sport: str, league: str) -> str: + """Get team abbreviation, using -W suffix only for women's leagues""" + if self.is_womens_league(sport, league): + return self.WOMENS_TEAM_ABBREVIATIONS.get(team_id, team_abbreviation) + else: + return team_abbreviation + + def format_clean_date_time(self, dt) -> str: + """Format date and time without leading zeros""" + month = dt.month + day = dt.day + minute = dt.minute + ampm = dt.strftime("%p") + + # Convert to 12-hour format + hour_12 = dt.hour + if hour_12 == 0: + hour_12 = 12 + elif hour_12 > 12: + hour_12 = hour_12 - 12 + + # Remove leading zeros + time_str = f"{month}/{day} {hour_12}:{minute:02d} {ampm}" + return time_str + + def format_clean_date(self, dt) -> str: + """Format date without leading zeros""" + month = dt.month + day = dt.day + return f"{month}/{day}" + def matches_keyword(self, message: MeshMessage) -> bool: """Check if this command matches the message content - sports must be first word""" if not self.keywords: @@ -745,7 +785,7 @@ class SportsCommand(BaseCommand): # Parse all games and sort by time game_data = [] for event in events: - game_info = self.parse_league_game_event(event, league_info['sport']) + game_info = self.parse_league_game_event(event, league_info['sport'], league_info['league']) if game_info: game_data.append(game_info) @@ -777,7 +817,7 @@ class SportsCommand(BaseCommand): self.logger.error(f"Error fetching league scores: {e}") return f"Error fetching {league_info['sport']} data" - def parse_league_game_event(self, event: Dict, sport: str) -> Optional[Dict]: + def parse_league_game_event(self, event: Dict, sport: str, league: str) -> Optional[Dict]: """Parse a league game event and return structured data with timestamp for sorting""" try: competitions = event.get('competitions', []) @@ -799,8 +839,10 @@ class SportsCommand(BaseCommand): away_team = team2 if team1.get('homeAway') == 'home' else team1 home_team_id = home_team.get('team', {}).get('id', '') away_team_id = away_team.get('team', {}).get('id', '') - home_name = self.TEAM_ABBREVIATIONS.get(home_team_id, home_team.get('team', {}).get('abbreviation', 'UNK')) - away_name = self.TEAM_ABBREVIATIONS.get(away_team_id, away_team.get('team', {}).get('abbreviation', 'UNK')) + home_abbreviation = home_team.get('team', {}).get('abbreviation', 'UNK') + away_abbreviation = away_team.get('team', {}).get('abbreviation', 'UNK') + home_name = self.get_team_abbreviation(home_team_id, home_abbreviation, sport, league) + away_name = self.get_team_abbreviation(away_team_id, away_abbreviation, sport, league) home_score = home_team.get('score', '0') away_score = away_team.get('score', '0') @@ -859,7 +901,7 @@ class SportsCommand(BaseCommand): try: dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) local_dt = dt.astimezone() - time_str = local_dt.strftime("%m/%d %I:%M %p") + time_str = self.format_clean_date_time(local_dt) if sport == 'soccer': formatted = f"@{home_name} vs. {away_name} ({time_str})" else: @@ -886,11 +928,35 @@ class SportsCommand(BaseCommand): timestamp = -2 # Halftime games second priority after live games elif status_name == 'STATUS_FULL_TIME': # Soccer game is finished - put these last - formatted = f"@{home_name} {home_score}-{away_score} {away_name} (FT)" + # Check if game was played today or on a different day + date_suffix = "" + if date_str: + try: + dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) + local_dt = dt.astimezone() + today = datetime.now().date() + game_date = local_dt.date() + if game_date != today: + date_suffix = f", {self.format_clean_date(local_dt)}" + except: + pass + formatted = f"@{home_name} {home_score}-{away_score} {away_name} (FT{date_suffix})" timestamp = 9999999998 # Final games second to last elif status_name == 'STATUS_FINAL': # Other sports game is finished - put these last - formatted = f"{away_name} {away_score}-{home_score} @{home_name} (F)" + # Check if game was played today or on a different day + date_suffix = "" + if date_str: + try: + dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) + local_dt = dt.astimezone() + today = datetime.now().date() + game_date = local_dt.date() + if game_date != today: + date_suffix = f", {self.format_clean_date(local_dt)}" + except: + pass + formatted = f"{away_name} {away_score}-{home_score} @{home_name} (F{date_suffix})" timestamp = 9999999998 # Final games second to last else: @@ -964,7 +1030,7 @@ class SportsCommand(BaseCommand): # Find games involving the team for event in events: - game_data = self.parse_game_event_with_timestamp(event, team_info['team_id'], team_info['sport']) + game_data = self.parse_game_event_with_timestamp(event, team_info['team_id'], team_info['sport'], team_info['league']) if game_data: return game_data @@ -974,7 +1040,7 @@ class SportsCommand(BaseCommand): self.logger.error(f"Error fetching team game data: {e}") return None - def parse_game_event_with_timestamp(self, event: Dict, team_id: str, sport: str) -> Optional[Dict]: + def parse_game_event_with_timestamp(self, event: Dict, team_id: str, sport: str, league: str) -> Optional[Dict]: """Parse a game event and return structured data with timestamp for sorting""" try: competitions = event.get('competitions', []) @@ -1005,8 +1071,10 @@ class SportsCommand(BaseCommand): away_team = other_team if our_team.get('homeAway') == 'home' else our_team home_team_id = home_team.get('team', {}).get('id', '') away_team_id = away_team.get('team', {}).get('id', '') - home_name = self.TEAM_ABBREVIATIONS.get(home_team_id, home_team.get('team', {}).get('abbreviation', 'UNK')) - away_name = self.TEAM_ABBREVIATIONS.get(away_team_id, away_team.get('team', {}).get('abbreviation', 'UNK')) + home_abbreviation = home_team.get('team', {}).get('abbreviation', 'UNK') + away_abbreviation = away_team.get('team', {}).get('abbreviation', 'UNK') + home_name = self.get_team_abbreviation(home_team_id, home_abbreviation, sport, league) + away_name = self.get_team_abbreviation(away_team_id, away_abbreviation, sport, league) home_score = home_team.get('score', '0') away_score = away_team.get('score', '0') @@ -1072,7 +1140,7 @@ class SportsCommand(BaseCommand): try: dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) local_dt = dt.astimezone() - time_str = local_dt.strftime("%m/%d %I:%M %p") + time_str = self.format_clean_date_time(local_dt) if sport == 'soccer': formatted = f"@{home_name} vs. {away_name} ({time_str})" else: @@ -1099,11 +1167,35 @@ class SportsCommand(BaseCommand): timestamp = -2 # Halftime games second priority after live games elif status_name == 'STATUS_FULL_TIME': # Soccer game is finished - put these last - formatted = f"@{home_name} {home_score}-{away_score} {away_name} (FT)" + # Check if game was played today or on a different day + date_suffix = "" + if date_str: + try: + dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) + local_dt = dt.astimezone() + today = datetime.now().date() + game_date = local_dt.date() + if game_date != today: + date_suffix = f", {self.format_clean_date(local_dt)}" + except: + pass + formatted = f"@{home_name} {home_score}-{away_score} {away_name} (FT{date_suffix})" timestamp = 9999999998 # Final games second to last elif status_name == 'STATUS_FINAL': # Other sports game is finished - put these last - formatted = f"{away_name} {away_score}-{home_score} @{home_name} (F)" + # Check if game was played today or on a different day + date_suffix = "" + if date_str: + try: + dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) + local_dt = dt.astimezone() + today = datetime.now().date() + game_date = local_dt.date() + if game_date != today: + date_suffix = f", {self.format_clean_date(local_dt)}" + except: + pass + formatted = f"{away_name} {away_score}-{home_score} @{home_name} (F{date_suffix})" timestamp = 9999999998 # Final games second to last else: @@ -1205,7 +1297,7 @@ class SportsCommand(BaseCommand): dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) # Convert to local time (assuming Pacific for Seattle teams) local_dt = dt.astimezone() - time_str = local_dt.strftime("%m/%d %I:%M %p") + time_str = self.format_clean_date_time(local_dt) return f"{away_team_name} @ {home_team_name} ({time_str})" except: return f"{away_team_name} @ {home_team_name} (TBD)" @@ -1217,10 +1309,36 @@ class SportsCommand(BaseCommand): return f"{our_team_name} {our_score}-{other_score} @{other_team_name} (HT)" elif status_name == 'STATUS_FULL_TIME': # Soccer game is finished - return f"{our_team_name} {our_score}-{other_score} @{other_team_name} (FT)" + # Check if game was played today or on a different day + date_str = event.get('date', '') + date_suffix = "" + if date_str: + try: + dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) + local_dt = dt.astimezone() + today = datetime.now().date() + game_date = local_dt.date() + if game_date != today: + date_suffix = f", {self.format_clean_date(local_dt)}" + except: + pass + return f"{our_team_name} {our_score}-{other_score} @{other_team_name} (FT{date_suffix})" elif status_name == 'STATUS_FINAL': # Other sports game is finished - return f"{our_team_name} {our_score}-{other_score} @{other_team_name} (F)" + # Check if game was played today or on a different day + date_str = event.get('date', '') + date_suffix = "" + if date_str: + try: + dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) + local_dt = dt.astimezone() + today = datetime.now().date() + game_date = local_dt.date() + if game_date != today: + date_suffix = f", {self.format_clean_date(local_dt)}" + except: + pass + return f"{our_team_name} {our_score}-{other_score} @{other_team_name} (F{date_suffix})" else: # Other status diff --git a/modules/commands/test_command.py b/modules/commands/test_command.py index 7fab09a..07dd04b 100644 --- a/modules/commands/test_command.py +++ b/modules/commands/test_command.py @@ -21,10 +21,21 @@ class TestCommand(BaseCommand): def get_help_text(self) -> str: return self.description + def clean_content(self, content: str) -> str: + """Clean content by removing control characters and normalizing whitespace""" + import re + # Remove control characters (except newline, tab, carriage return) + cleaned = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', content) + # Normalize whitespace + cleaned = ' '.join(cleaned.split()) + return cleaned + def matches_keyword(self, message: MeshMessage) -> bool: """Override to implement special test keyword matching with optional phrase""" + # Clean content to remove control characters and normalize whitespace + content = self.clean_content(message.content) + # Strip exclamation mark if present (for command-style messages) - content = message.content.strip() if content.startswith('!'): content = content[1:].strip() @@ -46,8 +57,10 @@ class TestCommand(BaseCommand): def format_response(self, message: MeshMessage, response_format: str) -> str: """Override to handle phrase extraction""" + # Clean content to remove control characters and normalize whitespace + content = self.clean_content(message.content) + # Strip exclamation mark if present (for command-style messages) - content = message.content.strip() if content.startswith('!'): content = content[1:].strip()