- 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>
174 lines
4.6 KiB
Python
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
|