major-domo-v2/commands/transactions/trade_channels.py
Cal Corum 5c7f2d916b CLAUDE: Fix trade system bugs and add smart channel updates
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>
2025-10-09 20:55:19 -05:00

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