major-domo-v2/utils/discord_helpers.py
Cal Corum 8907841ec6 CLAUDE: Refactor scorebug display and add dynamic channel visibility
- Created utils/scorebug_helpers.py with shared scorebug functions
  - create_scorebug_embed(): Unified embed creation for command and background task
  - create_team_progress_bar(): Win probability visualization

- Fixed win probability bar to show dark blocks weighted toward winning team
  - Arrow extends from the side with advantage
  - Home winning: "POR ░▓▓▓▓▓▓▓▓▓► WV  95.0%"
  - Away winning: "POR ◄▓▓▓▓▓▓▓░░░ WV  30.0%"

- Changed embed color from score-based to win probability-based
  - Embed shows color of team favored to win, not necessarily winning
  - Creates fun psychological element showing momentum/advantage

- Added dynamic channel visibility for #live-sba-scores
  - Channel visible to @everyone when active games exist
  - Channel hidden when no games are active
  - Bot retains access via role permissions
  - Added set_channel_visibility() to utils/discord_helpers.py

- Eliminated ~220 lines of duplicate code across files
  - Removed duplicate embed creation from commands/gameplay/scorebug.py
  - Removed duplicate embed creation from tasks/live_scorebug_tracker.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 16:58:21 -05:00

174 lines
4.6 KiB
Python

"""
Discord Helper Utilities
Common Discord-related helper functions for channel lookups,
message sending, and formatting.
"""
from typing import Optional, List
import discord
from discord.ext import commands
from models.play import Play
from models.team import Team
from utils.logging import get_contextual_logger
logger = get_contextual_logger(__name__)
async def get_channel_by_name(
bot: commands.Bot,
channel_name: str
) -> Optional[discord.TextChannel]:
"""
Get a text channel by name from the configured guild.
Args:
bot: Discord bot instance
channel_name: Name of the channel to find
Returns:
TextChannel if found, None otherwise
"""
from config import get_config
config = get_config()
guild_id = config.guild_id
if not guild_id:
logger.error("GUILD_ID not configured")
return None
guild = bot.get_guild(guild_id)
if not guild:
logger.error(f"Guild {guild_id} not found")
return None
channel = discord.utils.get(guild.text_channels, name=channel_name)
if not channel:
logger.warning(f"Channel '{channel_name}' not found in guild {guild_id}")
return None
return channel
async def send_to_channel(
bot: commands.Bot,
channel_name: str,
content: Optional[str] = None,
embed: Optional[discord.Embed] = None
) -> bool:
"""
Send a message to a channel by name.
Args:
bot: Discord bot instance
channel_name: Name of the channel
content: Text content to send
embed: Embed to send
Returns:
True if message sent successfully, False otherwise
"""
channel = await get_channel_by_name(bot, channel_name)
if not channel:
logger.error(f"Cannot send to channel '{channel_name}' - not found")
return False
try:
# Build kwargs to avoid passing None for embed
kwargs = {}
if content is not None:
kwargs['content'] = content
if embed is not None:
kwargs['embed'] = embed
await channel.send(**kwargs)
logger.info(f"Sent message to #{channel_name}")
return True
except Exception as e:
logger.error(f"Failed to send message to #{channel_name}: {e}")
return False
def format_key_plays(
plays: List[Play],
away_team: Team,
home_team: Team
) -> str:
"""
Format top plays into embed field text.
Args:
plays: List of Play objects (should be sorted by WPA)
away_team: Away team object
home_team: Home team object
Returns:
Formatted string for embed field, or empty string if no plays
"""
if not plays:
return ""
key_plays_text = ""
for play in plays:
# Use the Play.descriptive_text() method (already includes score)
play_description = play.descriptive_text(away_team, home_team)
key_plays_text += f"{play_description}\n"
return key_plays_text
async def set_channel_visibility(
channel: discord.TextChannel,
visible: bool,
reason: Optional[str] = None
) -> bool:
"""
Set channel visibility for @everyone.
The bot's permissions are based on its role, not @everyone, so the bot
will retain access even when @everyone view permission is removed.
Args:
channel: Discord text channel to modify
visible: If True, grant @everyone view permission; if False, deny it
reason: Optional reason for audit log
Returns:
True if permissions updated successfully, False otherwise
"""
try:
guild = channel.guild
everyone_role = guild.default_role
if visible:
# Grant @everyone permission to view channel
default_reason = "Channel made visible to all members"
await channel.set_permissions(
everyone_role,
view_channel=True,
reason=reason or default_reason
)
logger.info(f"Set #{channel.name} to VISIBLE for @everyone")
else:
# Remove @everyone view permission
default_reason = "Channel hidden from members"
await channel.set_permissions(
everyone_role,
view_channel=False,
reason=reason or default_reason
)
logger.info(f"Set #{channel.name} to HIDDEN for @everyone")
return True
except discord.Forbidden:
logger.error(f"Missing permissions to modify #{channel.name} permissions")
return False
except Exception as e:
logger.error(f"Error setting channel visibility for #{channel.name}: {e}")
return False