From cf46a7b1deef7557b3a65e604fb8e9412e21b849 Mon Sep 17 00:00:00 2001 From: agessaman Date: Tue, 30 Sep 2025 20:48:20 -0700 Subject: [PATCH] Enhance SportsCommand with women's league support and date formatting improvements --- .cache.sqlite | Bin 151552 -> 151552 bytes modules/commands/sports_command.py | 154 +++++++++++++++++++++++++---- modules/commands/test_command.py | 17 +++- 3 files changed, 151 insertions(+), 20 deletions(-) diff --git a/.cache.sqlite b/.cache.sqlite index 04d5f616b56b2fef85d059377057cc28c36f93f6..4c5f9722b178cff7aa29ad5d3c107896e3517a3f 100644 GIT binary patch delta 1836 zcmai!du&rx9LLY;bGx^@r);AGmv!9(GJ$pX_TJt{8Ql~1&?&O<4^tViRa^)&aYHuE zY!wn@@|eePH(bId1PE$asjjF9LQsj8Wj;b=3*n{wVMu@o1QTVR+wR3iLerl9?&-HD z_xCxUbN#*X{@(cAsR>m8fO+H-eRbwYuqs&BhiGzXM~&K~16!0Wo87G((NIKV->w1 zS)U8}wXDl6uk)>Oi~L$aaaU@SE_sd8AStrQxqQl6WnEe5@Ip}smxV6p9*3|3cjY|| z%ki1KV!gy9Vl$hEWj52!ynq&?Hl~~@U_>UD$wn#2h!`dnT|z%GiNQcmLSmfW0N0Ov z{X=E682k1yjD=U%m4|K=I0_G$u0ZR4)9xXY z8Bk^e6%0JohpBq_pE1ld!+(s&`Yi@qnK4(bG)%vJ>`Y!o`!gE7;M=TH0Pd)rAHpI! za%*@!K?TZh4&$LV30t>X>OU_go3-ND=;$cV|5`}u>XW42+x=Y;sh9sqYT)-SCH3Nnk`DgD(Ge?T;&rpB@@gNQu$TtAXYjq`Yt0-UrG`Wt=CFNuPpXsxd8W-!~6%Q{v z-LfdNEDok7nXmusdeAl$z^C7wttGh0geZ#mPHOwys-}&L94^IC-SC>jMRtx1u~0ZL zA6|!PoFIq-KYv5h##df#ZmDlrlFPYRryz5R$V=fs4lH^?248K>GLenx*t|@QYlKaD z$62hlHNpz|x7jgX%M&$TB@M|FtoC+aEEs2XK5}@H6{sy4XEnw%$_juhh0|Hpto);B zI;$YkZkdVBi`Izq}Fd zgL>lLfFGLb!lv8sA&P^P5l}{hH5f1$!q&U+o^ejc=E|s6(~pX#73%GJKu%|WaU^o@ zOi%Y>wev1qLgxfWqG}!ZYy=Mtm%t5b+C7+3^}c3Q%p#C7h0Na?y)iOi2ARRgi5X;e zW}U-f3W`S{fEPcy`jG z@nr~_D==Mj%5G5-Sou+tTx6PcDZCo^9lFE)c4&cVPYh-no!XHP#V%-()a% zY%Vc&G*{rH<|f8*b-x$*t)-F3UuPl_-1$_FdUSy5;oLsD7-+{ua9imE}Edmb{&UB1)1w2K8D+AAfpK zgBpzpQzC9YD%=dJSW^XgGbXQN4*QDu%l zN7|)eRjto=W1`7N50!VKNuSTBDHUzT(nN!#O^6gai>CcGG`RT+p+?aIgd9wvoPb`d{t-A=D2 L!0Nh#g*xm1ihMBI delta 1071 zcmaiye`p(J7{}lDe&om9<(_D}rnW3ivQ#@{>D}cnxyF$+UH|B);B*dTPFJBDHNuLp zDJ!E*aDPmK+Iq^{l<8W9ZA?MaxTXr*+|>Os- zrPGhb7#u7uZkff{w8q;vx3nS%t>1Ha>S#~U8iKEBV9UPRG z+P;omM>K*)>u;;`D7VCLxe+E+IOW3A^%g;tE;OMg+hwzo#(0ku2P{8{5!5t zjZp74s-KvrIvR~qLjK3^RQJD-_V)@%KyKth9DO5n=1i!|wOc?E66yDv@2A;iGO~er z*sZA%UDFjqw~Ebm&aZx+72dn`8M*ii_Pf?1ie~-RJWLMU#6Ct-t=!Cf>A6==F%H^j z{*^cp63igmclZx-B*8@O7W%Et`Mg^-B6`eFq8)2xbGJVDwXI~A-MALgJL2a3x~fsF z`z(<$xsYPES|?WO%IC+igh^kLN%D~a3UPYWG3Vfsz;i+|x5Ac%eteI|LE;+1gKX!$ zU1VqoZx`NaUKpS~BX1+Q*2I{W_mH&jOB+uW=l=aCJE^YXoG9F|{WY9N*Ktl1US7wE z(9!?ke0taG>3`!iWgxj5H~?xqp>!lPEfP~TLsw|`^$zif!F`=47hl3YP|f>sT{r$g z@PMMVaXm@pae(aYW$M|>>YP2~psXhJSVA!)suqtLMs+0DzcFv?^HcU;HA(AHjSj3f z?FXH;tp6R&i^@zEJNiC!WWDwkUxBk6^H;lMPr-f!CnrtixUv*x?uuncCvzA62BZ{C Ar2qf` 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()