This commit fixes two critical bugs in the trade system and adds a new feature for automatic channel updates. ## Bug Fixes ### 1. Trade Channel Creation Permission Error (Discord API 50013) **Issue**: Trade channels failed to create with "Missing Permissions" error **Root Cause**: Bot was attempting to grant itself manage_channels and manage_permissions in channel-specific overwrites. Discord prohibits bots from self-granting elevated permissions in channel overwrites. **Fix**: Removed manage_channels and manage_permissions from bot's channel-specific overwrites in trade_channels.py. Server-level permissions are sufficient for all channel management operations. **Files Changed**: - commands/transactions/trade_channels.py (lines 74-77) ### 2. TeamService Method Name AttributeError **Issue**: Bot crashed with AttributeError when adding players to trades **Root Cause**: Code called non-existent method team_service.get_team_by_id() The correct method name is team_service.get_team() **Fix**: Updated method call in trade_builder.py and all test mocks **Files Changed**: - services/trade_builder.py (line 201) - tests/test_services_trade_builder.py (all test mocks) ## New Features ### Smart Trade Channel Updates **Feature**: When trade commands are executed outside the dedicated trade channel, the trade embed is automatically posted to the trade channel (non-ephemeral) for visibility to all participants. **Behavior**: - Commands in trade channel: Only ephemeral response to user - Commands outside trade channel: Ephemeral response + public post to channel - Applies to: /trade add-team, /trade add-player, /trade supplementary, /trade view **Implementation**: - Added _get_trade_channel() helper method - Added _is_in_trade_channel() helper method - Added _post_to_trade_channel() helper method - Updated 4 trade commands to use smart posting logic **Files Changed**: - commands/transactions/trade.py (new helper methods + 4 command updates) ## Documentation Updates Updated comprehensive documentation for: - Trade channel permission requirements and troubleshooting - TeamService correct method names with examples - Smart channel update feature and behavior - Bug fix details and prevention strategies **Files Changed**: - commands/transactions/README.md - services/README.md ## Testing - All 18 trade builder tests pass - Updated test assertions to match new error message format 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
322 lines
12 KiB
Python
322 lines
12 KiB
Python
"""
|
|
Trade Channel Management
|
|
|
|
Handles creation and management of private text channels for trade discussions.
|
|
"""
|
|
from typing import Optional
|
|
|
|
import discord
|
|
|
|
from models.team import Team
|
|
from utils.logging import get_contextual_logger
|
|
from commands.transactions.trade_channel_tracker import TradeChannelTracker
|
|
|
|
logger = get_contextual_logger(f'{__name__}.TradeChannelManager')
|
|
|
|
|
|
class TradeChannelManager:
|
|
"""
|
|
Manages text channels for trade discussions between teams.
|
|
|
|
Features:
|
|
- Creates private channels with team-specific permissions
|
|
- Tracks channels for cleanup
|
|
- Handles permission setup for team roles
|
|
- Supports adding teams to existing channels for multi-team trades
|
|
"""
|
|
|
|
def __init__(self, tracker: TradeChannelTracker):
|
|
"""
|
|
Initialize the trade channel manager.
|
|
|
|
Args:
|
|
tracker: TradeChannelTracker instance for persistence
|
|
"""
|
|
self.tracker = tracker
|
|
self.logger = logger
|
|
|
|
async def create_trade_channel(
|
|
self,
|
|
guild: discord.Guild,
|
|
trade_id: str,
|
|
team1: Team,
|
|
team2: Team,
|
|
creator_id: int
|
|
) -> Optional[discord.TextChannel]:
|
|
"""
|
|
Create a private text channel for trade discussion.
|
|
|
|
Args:
|
|
guild: Discord guild where channel will be created
|
|
trade_id: Unique trade identifier
|
|
team1: First participating team
|
|
team2: Second participating team
|
|
creator_id: Discord user ID who initiated the trade
|
|
|
|
Returns:
|
|
Created TextChannel or None if creation failed
|
|
"""
|
|
# Get Transactions category
|
|
transactions_category = discord.utils.get(guild.categories, name="Transactions")
|
|
if not transactions_category:
|
|
self.logger.warning("'Transactions' category not found, channel will be created without category")
|
|
|
|
# Build channel name: trade-{team1}-{team2}-{short_id}
|
|
channel_name = f"trade-{team1.abbrev.lower()}-{team2.abbrev.lower()}-{trade_id[:4]}"
|
|
|
|
# Get team roles
|
|
team1_role = discord.utils.get(guild.roles, name=team1.lname)
|
|
team2_role = discord.utils.get(guild.roles, name=team2.lname)
|
|
|
|
# Setup permissions
|
|
overwrites = {
|
|
guild.default_role: discord.PermissionOverwrite(view_channel=False),
|
|
guild.me: discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
send_messages=True,
|
|
read_message_history=True
|
|
)
|
|
}
|
|
|
|
# Add team permissions if roles exist
|
|
roles_found = []
|
|
if team1_role:
|
|
overwrites[team1_role] = discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
send_messages=True,
|
|
read_message_history=True
|
|
)
|
|
roles_found.append(team1.lname)
|
|
else:
|
|
self.logger.warning(f"Role not found for team: {team1.lname}")
|
|
|
|
if team2_role:
|
|
overwrites[team2_role] = discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
send_messages=True,
|
|
read_message_history=True
|
|
)
|
|
roles_found.append(team2.lname)
|
|
else:
|
|
self.logger.warning(f"Role not found for team: {team2.lname}")
|
|
|
|
try:
|
|
self.logger.info(f"Attempting to create trade channel: {channel_name}")
|
|
self.logger.debug(f"Permissions configured for {len(overwrites)} roles/members")
|
|
|
|
# Create the text channel
|
|
channel = await guild.create_text_channel(
|
|
name=channel_name,
|
|
overwrites=overwrites,
|
|
category=transactions_category,
|
|
topic=f"Trade discussion: {team1.abbrev} ↔ {team2.abbrev} | Trade ID: {trade_id}"
|
|
)
|
|
|
|
self.logger.info(f"Successfully created channel: {channel.name} (ID: {channel.id})")
|
|
|
|
# Add to tracker
|
|
self.tracker.add_channel(
|
|
channel=channel,
|
|
trade_id=trade_id,
|
|
team1_abbrev=team1.abbrev,
|
|
team2_abbrev=team2.abbrev,
|
|
creator_id=creator_id
|
|
)
|
|
|
|
# Send welcome message mentioning the team roles
|
|
welcome_parts = ["Welcome to this trade discussion channel!"]
|
|
|
|
if team1_role and team2_role:
|
|
welcome_parts.append(f"{team1_role.mention} and {team2_role.mention}, you can use this private channel to discuss your trade.")
|
|
elif team1_role:
|
|
welcome_parts.append(f"{team1_role.mention}, you can use this private channel to discuss your trade.")
|
|
elif team2_role:
|
|
welcome_parts.append(f"{team2_role.mention}, you can use this private channel to discuss your trade.")
|
|
else:
|
|
welcome_parts.append("Both teams can use this private channel to discuss your trade.")
|
|
|
|
welcome_parts.append(f"\n**Trade ID:** `{trade_id}`")
|
|
welcome_message = "\n".join(welcome_parts)
|
|
|
|
try:
|
|
await channel.send(welcome_message)
|
|
except Exception as e:
|
|
self.logger.warning(f"Failed to send welcome message to trade channel: {e}")
|
|
|
|
self.logger.info(
|
|
f"Created trade channel: {channel.name} for trade {trade_id} "
|
|
f"({team1.abbrev} ↔ {team2.abbrev})"
|
|
)
|
|
|
|
return channel
|
|
|
|
except discord.Forbidden as e:
|
|
self.logger.error(
|
|
f"Missing permissions to create trade channel. "
|
|
f"Discord error: {e.text if hasattr(e, 'text') else str(e)}. "
|
|
f"Code: {e.code if hasattr(e, 'code') else 'unknown'}"
|
|
)
|
|
return None
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to create trade channel: {type(e).__name__}: {e}", exc_info=True)
|
|
return None
|
|
|
|
async def add_team_to_channel(
|
|
self,
|
|
guild: discord.Guild,
|
|
trade_id: str,
|
|
new_team: Team
|
|
) -> bool:
|
|
"""
|
|
Add a team to an existing trade channel's permissions.
|
|
|
|
Args:
|
|
guild: Discord guild containing the channel
|
|
trade_id: Trade identifier
|
|
new_team: Team to add to the channel
|
|
|
|
Returns:
|
|
True if team was added successfully, False otherwise
|
|
"""
|
|
# Find channel in tracker
|
|
channel_data = self.tracker.get_channel_by_trade_id(trade_id)
|
|
if not channel_data:
|
|
self.logger.warning(f"No tracked channel found for trade {trade_id}")
|
|
return False
|
|
|
|
channel_id = int(channel_data["channel_id"])
|
|
channel = guild.get_channel(channel_id)
|
|
|
|
if not channel or not isinstance(channel, discord.TextChannel):
|
|
self.logger.warning(f"Channel {channel_id} not found or is not a text channel")
|
|
return False
|
|
|
|
# Get the new team's role
|
|
team_role = discord.utils.get(guild.roles, name=new_team.lname)
|
|
if not team_role:
|
|
self.logger.warning(f"Role not found for team: {new_team.lname}")
|
|
return False
|
|
|
|
try:
|
|
# Add permissions for the new team
|
|
await channel.set_permissions(
|
|
team_role,
|
|
view_channel=True,
|
|
send_messages=True,
|
|
read_message_history=True,
|
|
reason=f"Added {new_team.abbrev} to trade {trade_id}"
|
|
)
|
|
|
|
# Update channel topic to include new team
|
|
current_topic = channel.topic or ""
|
|
if "Trade discussion:" in current_topic:
|
|
# Extract existing teams and add new one
|
|
topic_parts = current_topic.split("|")
|
|
teams_part = topic_parts[0].strip()
|
|
# Add new team abbreviation
|
|
updated_topic = f"{teams_part} + {new_team.abbrev} | Trade ID: {trade_id}"
|
|
await channel.edit(topic=updated_topic)
|
|
|
|
# Send welcome message for the new team
|
|
if team_role:
|
|
welcome_message = f"Welcome {team_role.mention}! You've been added to this trade discussion. This is now a multi-team trade."
|
|
else:
|
|
welcome_message = f"Welcome **{new_team.lname}**! You've been added to this trade discussion. This is now a multi-team trade."
|
|
|
|
try:
|
|
await channel.send(welcome_message)
|
|
except Exception as e:
|
|
self.logger.warning(f"Failed to send welcome message to trade channel: {e}")
|
|
|
|
self.logger.info(
|
|
f"Added team {new_team.abbrev} to trade channel {channel.name} (Trade: {trade_id})"
|
|
)
|
|
return True
|
|
|
|
except discord.Forbidden:
|
|
self.logger.error(f"Missing permissions to modify channel {channel_id}")
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to add team to channel {channel_id}: {e}")
|
|
return False
|
|
|
|
async def delete_trade_channel(self, guild: discord.Guild, trade_id: str) -> bool:
|
|
"""
|
|
Delete a trade channel by trade ID.
|
|
|
|
Args:
|
|
guild: Discord guild containing the channel
|
|
trade_id: Trade identifier
|
|
|
|
Returns:
|
|
True if channel was deleted, False otherwise
|
|
"""
|
|
# Find channel in tracker
|
|
channel_data = self.tracker.get_channel_by_trade_id(trade_id)
|
|
if not channel_data:
|
|
self.logger.debug(f"No tracked channel found for trade {trade_id}")
|
|
return False
|
|
|
|
channel_id = int(channel_data["channel_id"])
|
|
|
|
# Get the channel from Discord
|
|
channel = guild.get_channel(channel_id)
|
|
if not channel:
|
|
# Channel doesn't exist in Discord, just remove from tracker
|
|
self.logger.warning(f"Channel {channel_id} not found in Discord, removing from tracker")
|
|
self.tracker.remove_channel(channel_id)
|
|
return False
|
|
|
|
try:
|
|
# Delete the channel
|
|
await channel.delete(reason=f"Trade {trade_id} cleared")
|
|
|
|
# Remove from tracker
|
|
self.tracker.remove_channel(channel_id)
|
|
|
|
self.logger.info(f"Deleted trade channel for trade {trade_id}")
|
|
return True
|
|
|
|
except discord.Forbidden:
|
|
self.logger.error(f"Missing permissions to delete channel {channel_id}")
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to delete trade channel {channel_id}: {e}")
|
|
return False
|
|
|
|
async def delete_channel_by_id(self, guild: discord.Guild, channel_id: int) -> bool:
|
|
"""
|
|
Delete a trade channel by channel ID.
|
|
|
|
Args:
|
|
guild: Discord guild containing the channel
|
|
channel_id: Discord channel ID
|
|
|
|
Returns:
|
|
True if channel was deleted, False otherwise
|
|
"""
|
|
channel = guild.get_channel(channel_id)
|
|
if not channel:
|
|
self.logger.warning(f"Channel {channel_id} not found in Discord")
|
|
# Remove from tracker anyway if it exists
|
|
if self.tracker.get_channel_by_id(channel_id):
|
|
self.tracker.remove_channel(channel_id)
|
|
return False
|
|
|
|
try:
|
|
# Delete the channel
|
|
await channel.delete(reason="Trade channel cleanup")
|
|
|
|
# Remove from tracker
|
|
self.tracker.remove_channel(channel_id)
|
|
|
|
self.logger.info(f"Deleted trade channel {channel_id}")
|
|
return True
|
|
|
|
except discord.Forbidden:
|
|
self.logger.error(f"Missing permissions to delete channel {channel_id}")
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to delete trade channel {channel_id}: {e}")
|
|
return False
|