major-domo-v2/commands/transactions/trade_channel_tracker.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

185 lines
6.1 KiB
Python

"""
Trade Channel Tracker
Provides persistent tracking of bot-created trade discussion channels using JSON file storage.
"""
import json
import logging
from datetime import datetime, UTC
from pathlib import Path
from typing import Dict, List, Optional, Any
import discord
from utils.logging import get_contextual_logger
logger = get_contextual_logger(f'{__name__}.TradeChannelTracker')
class TradeChannelTracker:
"""
Tracks bot-created trade discussion channels with JSON file persistence.
Features:
- Persistent storage across bot restarts
- Channel creation and tracking by trade ID
- Lookup by trade ID or channel ID
- Automatic stale entry removal
"""
def __init__(self, data_file: str = "data/trade_channels.json"):
"""
Initialize the trade channel tracker.
Args:
data_file: Path to the JSON data file
"""
self.data_file = Path(data_file)
self.data_file.parent.mkdir(exist_ok=True)
self._data: Dict[str, Any] = {}
self.load_data()
def load_data(self) -> None:
"""Load channel data from JSON file."""
try:
if self.data_file.exists():
with open(self.data_file, 'r') as f:
self._data = json.load(f)
logger.debug(f"Loaded {len(self._data.get('trade_channels', {}))} tracked trade channels")
else:
self._data = {"trade_channels": {}}
logger.info("No existing trade channel data found, starting fresh")
except Exception as e:
logger.error(f"Failed to load trade channel data: {e}")
self._data = {"trade_channels": {}}
def save_data(self) -> None:
"""Save channel data to JSON file."""
try:
with open(self.data_file, 'w') as f:
json.dump(self._data, f, indent=2, default=str)
logger.debug("Trade channel data saved successfully")
except Exception as e:
logger.error(f"Failed to save trade channel data: {e}")
def add_channel(
self,
channel: discord.TextChannel,
trade_id: str,
team1_abbrev: str,
team2_abbrev: str,
creator_id: int
) -> None:
"""
Add a new trade channel to tracking.
Args:
channel: Discord text channel object
trade_id: Unique trade identifier
team1_abbrev: First team abbreviation
team2_abbrev: Second team abbreviation
creator_id: Discord user ID who created the trade
"""
self._data.setdefault("trade_channels", {})[str(channel.id)] = {
"channel_id": str(channel.id),
"guild_id": str(channel.guild.id),
"name": channel.name,
"trade_id": trade_id,
"team1_abbrev": team1_abbrev,
"team2_abbrev": team2_abbrev,
"created_at": datetime.now(UTC).isoformat(),
"creator_id": str(creator_id)
}
self.save_data()
logger.info(f"Added trade channel to tracking: {channel.name} (ID: {channel.id}, Trade: {trade_id})")
def remove_channel(self, channel_id: int) -> None:
"""
Remove channel from tracking.
Args:
channel_id: Discord channel ID
"""
channels = self._data.get("trade_channels", {})
channel_key = str(channel_id)
if channel_key in channels:
channel_data = channels[channel_key]
trade_id = channel_data.get("trade_id", "unknown")
channel_name = channel_data["name"]
del channels[channel_key]
self.save_data()
logger.info(f"Removed trade channel from tracking: {channel_name} (ID: {channel_id}, Trade: {trade_id})")
def get_channel_by_trade_id(self, trade_id: str) -> Optional[Dict[str, Any]]:
"""
Get channel data for a specific trade.
Args:
trade_id: Trade identifier
Returns:
Channel data dictionary or None if not found
"""
channels = self._data.get("trade_channels", {})
for channel_data in channels.values():
if channel_data.get("trade_id") == trade_id:
return channel_data
return None
def get_channel_by_id(self, channel_id: int) -> Optional[Dict[str, Any]]:
"""
Get data for a specific tracked channel.
Args:
channel_id: Discord channel ID
Returns:
Channel data dictionary or None if not tracked
"""
channels = self._data.get("trade_channels", {})
return channels.get(str(channel_id))
def get_all_tracked_channels(self) -> List[Dict[str, Any]]:
"""
Get all currently tracked trade channels.
Returns:
List of all tracked channel data dictionaries
"""
return list(self._data.get("trade_channels", {}).values())
def cleanup_stale_entries(self, valid_channel_ids: List[int]) -> int:
"""
Remove tracking entries for channels that no longer exist.
Args:
valid_channel_ids: List of channel IDs that still exist in Discord
Returns:
Number of stale entries removed
"""
channels = self._data.get("trade_channels", {})
stale_entries = []
for channel_id_str, channel_data in channels.items():
try:
channel_id = int(channel_id_str)
if channel_id not in valid_channel_ids:
stale_entries.append(channel_id_str)
except (ValueError, TypeError):
logger.warning(f"Invalid channel ID in tracking data: {channel_id_str}")
stale_entries.append(channel_id_str)
# Remove stale entries
for channel_id_str in stale_entries:
channel_name = channels[channel_id_str].get("name", "unknown")
trade_id = channels[channel_id_str].get("trade_id", "unknown")
del channels[channel_id_str]
logger.info(f"Removed stale tracking entry: {channel_name} (ID: {channel_id_str}, Trade: {trade_id})")
if stale_entries:
self.save_data()
return len(stale_entries)