major-domo-v2/services/chart_service.py
Cal Corum 58b0c2d221 CLAUDE: Add multi-image support for charts
Image Version: 2.0.3

Added ability to add charts with multiple images and manage them incrementally.

Service Layer Changes (chart_service.py):
- Added add_image_to_chart() method to append URLs to existing charts
- Added remove_image_from_chart() method to remove specific URLs
- Validation to prevent duplicate URLs in charts
- Protection against removing the last image from a chart

Command Layer Changes (charts.py):
- Modified /chart-manage add to accept comma-separated URLs
  - Parse and strip URLs from comma-delimited string
  - Shows image count in success message
  - Displays first image in response embed
- Added /chart-manage add-image command for incremental additions
- Added /chart-manage remove-image command to remove specific URLs
- All commands use chart autocomplete for easy selection
- Admin/Help Editor permission checks on all management commands

Usage Examples:
  # Add chart with multiple images in one command
  /chart-manage add defense "Defense Chart" gameplay "https://example.com/def1.png, https://example.com/def2.png"

  # Add additional images later
  /chart-manage add-image defense https://example.com/def3.png

  # Remove specific image
  /chart-manage remove-image defense https://example.com/def2.png

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 22:00:19 -05:00

372 lines
11 KiB
Python

"""
Chart Service for managing gameplay charts and infographics.
This service handles loading, saving, and managing chart definitions
from the JSON configuration file.
"""
import json
import logging
from pathlib import Path
from typing import Optional, Dict, List, Any
from dataclasses import dataclass
from exceptions import BotException
logger = logging.getLogger(__name__)
@dataclass
class Chart:
"""Represents a gameplay chart or infographic."""
key: str
name: str
category: str
description: str
urls: List[str]
def to_dict(self) -> Dict[str, Any]:
"""Convert chart to dictionary (excluding key)."""
return {
'name': self.name,
'category': self.category,
'description': self.description,
'urls': self.urls
}
class ChartService:
"""Service for managing gameplay charts and infographics."""
CHARTS_FILE = Path(__file__).parent.parent / 'data' / 'charts.json'
def __init__(self):
"""Initialize the chart service."""
self._charts: Dict[str, Chart] = {}
self._categories: Dict[str, str] = {}
self._load_charts()
def _load_charts(self) -> None:
"""Load charts from JSON file."""
try:
if not self.CHARTS_FILE.exists():
logger.warning(f"Charts file not found: {self.CHARTS_FILE}")
self._charts = {}
self._categories = {}
return
with open(self.CHARTS_FILE, 'r') as f:
data = json.load(f)
# Load categories
self._categories = data.get('categories', {})
# Load charts
charts_data = data.get('charts', {})
for key, chart_data in charts_data.items():
self._charts[key] = Chart(
key=key,
name=chart_data['name'],
category=chart_data['category'],
description=chart_data.get('description', ''),
urls=chart_data.get('urls', [])
)
logger.info(f"Loaded {len(self._charts)} charts from {self.CHARTS_FILE}")
except Exception as e:
logger.error(f"Failed to load charts: {e}", exc_info=True)
self._charts = {}
self._categories = {}
def _save_charts(self) -> None:
"""Save charts to JSON file."""
try:
# Ensure data directory exists
self.CHARTS_FILE.parent.mkdir(parents=True, exist_ok=True)
# Build data structure
data = {
'charts': {
key: chart.to_dict()
for key, chart in self._charts.items()
},
'categories': self._categories
}
# Write to file
with open(self.CHARTS_FILE, 'w') as f:
json.dump(data, f, indent=2)
logger.info(f"Saved {len(self._charts)} charts to {self.CHARTS_FILE}")
except Exception as e:
logger.error(f"Failed to save charts: {e}", exc_info=True)
raise BotException(f"Failed to save charts: {str(e)}")
def get_chart(self, chart_key: str) -> Optional[Chart]:
"""
Get a chart by its key.
Args:
chart_key: The chart key/identifier
Returns:
Chart object if found, None otherwise
"""
return self._charts.get(chart_key)
def get_all_charts(self) -> List[Chart]:
"""
Get all available charts.
Returns:
List of all Chart objects
"""
return list(self._charts.values())
def get_charts_by_category(self, category: str) -> List[Chart]:
"""
Get all charts in a specific category.
Args:
category: The category to filter by
Returns:
List of charts in the specified category
"""
return [
chart for chart in self._charts.values()
if chart.category == category
]
def get_chart_keys(self) -> List[str]:
"""
Get all chart keys for autocomplete.
Returns:
Sorted list of chart keys
"""
return sorted(self._charts.keys())
def get_categories(self) -> Dict[str, str]:
"""
Get all categories.
Returns:
Dictionary mapping category keys to display names
"""
return self._categories.copy()
def add_chart(self, key: str, name: str, category: str,
urls: List[str], description: str = "") -> None:
"""
Add a new chart.
Args:
key: Unique identifier for the chart
name: Display name for the chart
category: Category the chart belongs to
urls: List of image URLs for the chart
description: Optional description of the chart
Raises:
BotException: If chart key already exists
"""
if key in self._charts:
raise BotException(f"Chart '{key}' already exists")
self._charts[key] = Chart(
key=key,
name=name,
category=category,
description=description,
urls=urls
)
self._save_charts()
logger.info(f"Added chart: {key}")
def update_chart(self, key: str, name: Optional[str] = None,
category: Optional[str] = None, urls: Optional[List[str]] = None,
description: Optional[str] = None) -> None:
"""
Update an existing chart.
Args:
key: Chart key to update
name: New name (optional)
category: New category (optional)
urls: New URLs (optional)
description: New description (optional)
Raises:
BotException: If chart doesn't exist
"""
if key not in self._charts:
raise BotException(f"Chart '{key}' not found")
chart = self._charts[key]
if name is not None:
chart.name = name
if category is not None:
chart.category = category
if urls is not None:
chart.urls = urls
if description is not None:
chart.description = description
self._save_charts()
logger.info(f"Updated chart: {key}")
def remove_chart(self, key: str) -> None:
"""
Remove a chart.
Args:
key: Chart key to remove
Raises:
BotException: If chart doesn't exist
"""
if key not in self._charts:
raise BotException(f"Chart '{key}' not found")
del self._charts[key]
self._save_charts()
logger.info(f"Removed chart: {key}")
def add_image_to_chart(self, key: str, url: str) -> None:
"""
Add an additional image URL to an existing chart.
Args:
key: Chart key to add image to
url: Image URL to append
Raises:
BotException: If chart doesn't exist or URL already exists
"""
if key not in self._charts:
raise BotException(f"Chart '{key}' not found")
chart = self._charts[key]
# Check if URL already exists
if url in chart.urls:
raise BotException(f"Image URL already exists in chart '{key}'")
chart.urls.append(url)
self._save_charts()
logger.info(f"Added image to chart: {key} (now {len(chart.urls)} images)")
def remove_image_from_chart(self, key: str, url: str) -> None:
"""
Remove an image URL from an existing chart.
Args:
key: Chart key to remove image from
url: Image URL to remove
Raises:
BotException: If chart doesn't exist, URL not found, or would leave chart with no images
"""
if key not in self._charts:
raise BotException(f"Chart '{key}' not found")
chart = self._charts[key]
# Check if URL exists
if url not in chart.urls:
raise BotException(f"Image URL not found in chart '{key}'")
# Don't allow removing the last image
if len(chart.urls) <= 1:
raise BotException(f"Cannot remove the last image from chart '{key}'")
chart.urls.remove(url)
self._save_charts()
logger.info(f"Removed image from chart: {key} (now {len(chart.urls)} images)")
def add_category(self, key: str, display_name: str) -> None:
"""
Add a new category.
Args:
key: Unique identifier for the category (e.g., 'gameplay')
display_name: Display name for the category (e.g., 'Gameplay Charts')
Raises:
BotException: If category key already exists
"""
if key in self._categories:
raise BotException(f"Category '{key}' already exists")
self._categories[key] = display_name
self._save_charts()
logger.info(f"Added category: {key} - {display_name}")
def remove_category(self, key: str) -> None:
"""
Remove a category.
Args:
key: Category key to remove
Raises:
BotException: If category doesn't exist or charts are using it
"""
if key not in self._categories:
raise BotException(f"Category '{key}' not found")
# Check if any charts use this category
charts_using = [c for c in self._charts.values() if c.category == key]
if charts_using:
chart_names = ", ".join([c.name for c in charts_using])
raise BotException(
f"Cannot remove category '{key}' - used by {len(charts_using)} chart(s): {chart_names}"
)
del self._categories[key]
self._save_charts()
logger.info(f"Removed category: {key}")
def update_category(self, key: str, display_name: str) -> None:
"""
Update category display name.
Args:
key: Category key to update
display_name: New display name
Raises:
BotException: If category doesn't exist
"""
if key not in self._categories:
raise BotException(f"Category '{key}' not found")
old_name = self._categories[key]
self._categories[key] = display_name
self._save_charts()
logger.info(f"Updated category: {key} from '{old_name}' to '{display_name}'")
def reload_charts(self) -> None:
"""Reload charts from the JSON file."""
self._load_charts()
# Global chart service instance
_chart_service: Optional[ChartService] = None
def get_chart_service() -> ChartService:
"""
Get the global chart service instance.
Returns:
ChartService instance
"""
global _chart_service
if _chart_service is None:
_chart_service = ChartService()
return _chart_service