diff --git a/src/database.py b/src/database.py index e69abe1..f3ff3f7 100644 --- a/src/database.py +++ b/src/database.py @@ -2543,12 +2543,21 @@ class Database: if row: import json team_data = json.loads(row['team_data']) + + # Handle both new format (list) and old format (dict) + if isinstance(team_data, list): + pet_count = len(team_data) + elif isinstance(team_data, dict): + pet_count = len([p for p in team_data.values() if p]) + else: + pet_count = 0 + configs.append({ 'slot': slot, 'name': row['config_name'], 'team_data': team_data, 'updated_at': row['updated_at'], - 'pet_count': len([p for p in team_data.values() if p]) + 'pet_count': pet_count }) else: configs.append({ diff --git a/src/team_management.py b/src/team_management.py index af68f0e..aecab65 100644 --- a/src/team_management.py +++ b/src/team_management.py @@ -40,10 +40,14 @@ class TeamManagementService: if config: # team_data is already parsed by get_player_team_configurations team_data = config["team_data"] if config["team_data"] else {} + + # Use pet_count from database method which handles both formats + pet_count = config.get("pet_count", 0) + teams[f"slot_{i}"] = { "name": config.get("name", f"Team {i}"), "pets": team_data, - "count": len(team_data), + "count": pet_count, "last_updated": config.get("updated_at"), "is_active": False } diff --git a/webserver.py b/webserver.py index cade31a..a93d42f 100644 --- a/webserver.py +++ b/webserver.py @@ -746,9 +746,32 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): self.serve_locations() elif path == '/petdex': self.serve_petdex() + elif path.startswith('/teambuilder/') and '/config/load/' in path: + # Handle team configuration load: /teambuilder/{nickname}/config/load/{slot} + parts = path.split('/') + if len(parts) >= 6: + nickname = parts[2] + slot = parts[5] + self.handle_team_config_load(nickname, slot) + else: + self.send_error(400, "Invalid configuration load path") + elif path.startswith('/teambuilder/') and '/team/' in path: + # Handle individual team editor: /teambuilder/{nickname}/team/{slot} + parts = path.split('/') + if len(parts) >= 5: + nickname = parts[2] + team_identifier = parts[4] # Could be 1, 2, 3, or 'active' + self.serve_individual_team_editor(nickname, team_identifier) + else: + self.send_error(400, "Invalid team editor path") elif path.startswith('/teambuilder/'): - nickname = path[13:] # Remove '/teambuilder/' prefix - self.serve_teambuilder(nickname) + # Check if it's just the base teambuilder path (hub) + path_parts = path[13:].split('/') # Remove '/teambuilder/' prefix + if len(path_parts) == 1 and path_parts[0]: # Just nickname + nickname = path_parts[0] + self.serve_team_selection_hub(nickname) + else: + self.send_error(404, "Invalid teambuilder path") elif path.startswith('/testteambuilder/'): nickname = path[17:] # Remove '/testteambuilder/' prefix self.serve_test_teambuilder(nickname) @@ -779,7 +802,25 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): parsed_path = urlparse(self.path) path = parsed_path.path - if path.startswith('/teambuilder/') and path.endswith('/save'): + if path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/save'): + # Handle individual team save: /teambuilder/{nickname}/team/{slot}/save + parts = path.split('/') + if len(parts) >= 6: + nickname = parts[2] + team_slot = parts[4] + self.handle_individual_team_save(nickname, team_slot) + else: + self.send_error(400, "Invalid individual team save path") + elif path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/verify'): + # Handle individual team verify: /teambuilder/{nickname}/team/{slot}/verify + parts = path.split('/') + if len(parts) >= 6: + nickname = parts[2] + team_slot = parts[4] + self.handle_individual_team_verify(nickname, team_slot) + else: + self.send_error(400, "Invalid individual team verify path") + elif path.startswith('/teambuilder/') and path.endswith('/save'): nickname = path[13:-5] # Remove '/teambuilder/' prefix and '/save' suffix self.handle_team_save(nickname) elif path.startswith('/teambuilder/') and path.endswith('/verify'): @@ -835,6 +876,33 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): self.handle_team_config_apply(nickname, slot) else: self.send_error(400, "Invalid configuration apply path") + elif path.startswith('/teambuilder/') and '/swap/' in path: + # Handle team swapping: /teambuilder/{nickname}/swap/{slot} + parts = path.split('/') + if len(parts) >= 5: + nickname = parts[2] + slot = parts[4] + self.handle_team_swap_request(nickname, slot) + else: + self.send_error(400, "Invalid team swap path") + elif path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/save'): + # Handle individual team save: /teambuilder/{nickname}/team/{slot}/save + parts = path.split('/') + if len(parts) >= 6: + nickname = parts[2] + team_slot = parts[4] + self.handle_individual_team_save(nickname, team_slot) + else: + self.send_error(400, "Invalid individual team save path") + elif path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/verify'): + # Handle individual team verify: /teambuilder/{nickname}/team/{slot}/verify + parts = path.split('/') + if len(parts) >= 6: + nickname = parts[2] + team_slot = parts[4] + self.handle_individual_team_verify(nickname, team_slot) + else: + self.send_error(400, "Invalid individual team verify path") elif path == '/admin/auth': self.handle_admin_auth() elif path == '/admin/verify': @@ -5941,6 +6009,10 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): } }); + // Reset team state BEFORE loading new data + currentTeam = {}; + originalTeam = {}; + // Load team data from server for the selected slot if (teamSlot === 1) { // For Team 1, load current active pets (default behavior) @@ -5950,10 +6022,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): loadSavedTeamConfiguration(teamSlot); } - // Reset team state - currentTeam = {}; - originalTeam = {}; - // Re-initialize team state updateTeamState(); } @@ -7256,12 +7324,12 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): if (!currentEditingTeam) return; try {{ - const response = await fetch(`/testteambuilder/{nickname}/save`, {{ + const response = await fetch(`/teambuilder/{nickname}/save`, {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ - team_slot: currentEditingTeam, - team_data: currentTeamData + teamSlot: currentEditingTeam, + teamData: currentTeamData }}) }}); @@ -7285,7 +7353,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }} try {{ - const response = await fetch(`/testteambuilder/{nickname}/verify`, {{ + const response = await fetch(`/teambuilder/{nickname}/verify`, {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ pin: pin }}) @@ -7417,6 +7485,10 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): team_data = save_data team_slot = 1 + # Validate team slot + if not isinstance(team_slot, int) or team_slot < 1 or team_slot > 3: + return {"success": False, "error": "Invalid team slot. Must be 1, 2, or 3"} + # Get player player = await self.database.get_player(nickname) if not player: @@ -7787,8 +7859,9 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): team_slot = data.get('team_slot') team_data = data.get('team_data', {}) - if not team_slot or team_slot not in [1, 2, 3]: - self.send_json_response({"success": False, "error": "Invalid team slot"}, 400) + # Validate team slot + if not isinstance(team_slot, int) or team_slot < 1 or team_slot > 3: + self.send_json_response({"success": False, "error": "Invalid team slot. Must be 1, 2, or 3"}, 400) return except json.JSONDecodeError: @@ -7901,6 +7974,167 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): print(f"Error in _handle_test_team_verify_async: {e}") return {"success": False, "error": str(e)} + def handle_individual_team_save(self, nickname, team_slot): + """Handle individual team save request and generate PIN""" + try: + # Get POST data + content_length = int(self.headers.get('Content-Length', 0)) + if content_length == 0: + self.send_json_response({"success": False, "error": "No data provided"}, 400) + return + + post_data = self.rfile.read(content_length).decode('utf-8') + + try: + import json + data = json.loads(post_data) + team_identifier = data.get('team_identifier', team_slot) + is_active_team = data.get('is_active_team', False) + pets = data.get('pets', []) + + # Validate team slot + if team_slot not in ['1', '2', '3', 'active']: + self.send_json_response({"success": False, "error": "Invalid team slot"}, 400) + return + + # Convert team_slot to numeric for database operations + if team_slot == 'active': + team_slot_num = 1 + is_active_team = True + else: + team_slot_num = int(team_slot) + + except json.JSONDecodeError: + self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) + return + + # Run async operations + import asyncio + result = asyncio.run(self._handle_individual_team_save_async(nickname, team_slot_num, pets, is_active_team)) + + if result["success"]: + self.send_json_response({"requires_pin": True, "message": "PIN sent to IRC"}, 200) + else: + self.send_json_response(result, 400) + + except Exception as e: + print(f"Error in handle_individual_team_save: {e}") + self.send_json_response({"success": False, "error": "Internal server error"}, 500) + + async def _handle_individual_team_save_async(self, nickname, team_slot, pets, is_active_team): + """Async handler for individual team save""" + try: + # Get player + player = await self.server.database.get_player(nickname) + + if not player: + return {"success": False, "error": "Player not found"} + + # Validate pets exist and belong to player + if pets: + player_pets = await self.server.database.get_player_pets(player['id']) + player_pet_ids = [pet['id'] for pet in player_pets] + + for pet_data in pets: + pet_id = pet_data.get('pet_id') + if not pet_id: + continue + + # Convert pet_id to int for comparison with database IDs + try: + pet_id_int = int(pet_id) + except (ValueError, TypeError): + return {"success": False, "error": f"Invalid pet ID: {pet_id}"} + + # Check if pet belongs to player + if pet_id_int not in player_pet_ids: + return {"success": False, "error": f"Pet {pet_id} not found or doesn't belong to you"} + + # Convert pets array to the expected format for database + # Expected format: {"pet_id": position, "pet_id": position, ...} + team_changes = {} + if pets: # Ensure pets is not None or empty + for pet_data in pets: + if isinstance(pet_data, dict): # Ensure pet_data is a dictionary + pet_id = str(pet_data.get('pet_id')) # Ensure pet_id is string + position = pet_data.get('position', False) # Position or False for inactive + if pet_id and pet_id != 'None': # Only add valid pet IDs + team_changes[pet_id] = position + + + # Generate PIN and store pending change + import json + team_data = { + 'teamSlot': int(team_slot), # Convert to int and use expected key name + 'teamData': team_changes, # Use the dictionary format expected by database + 'is_active_team': is_active_team + } + + # Generate PIN + pin_result = await self.server.database.generate_verification_pin(player["id"], "team_change", json.dumps(team_data)) + pin_code = pin_result.get("pin_code") + + # Send PIN via IRC + self.send_pin_via_irc(nickname, pin_code) + + return {"success": True, "requires_pin": True} + + except Exception as e: + print(f"Error in _handle_individual_team_save_async: {e}") + return {"success": False, "error": str(e)} + + def handle_individual_team_verify(self, nickname, team_slot): + """Handle individual team PIN verification""" + try: + # Get PIN from request body + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + + import json + try: + data = json.loads(post_data.decode('utf-8')) + pin_code = data.get('pin', '').strip() + except json.JSONDecodeError: + self.send_json_response({"success": False, "error": "Invalid request data"}, 400) + return + + if not pin_code or len(pin_code) != 6: + self.send_json_response({"success": False, "error": "PIN must be 6 digits"}, 400) + return + + # Run async verification + import asyncio + result = asyncio.run(self._handle_individual_team_verify_async(nickname, pin_code)) + + if result["success"]: + self.send_json_response(result, 200) + else: + self.send_json_response(result, 400) + + except Exception as e: + print(f"Error in handle_individual_team_verify: {e}") + self.send_json_response({"success": False, "error": "Internal server error"}, 500) + + async def _handle_individual_team_verify_async(self, nickname, pin_code): + """Async handler for individual team PIN verification""" + try: + # Get player + player = await self.server.database.get_player(nickname) + if not player: + return {"success": False, "error": "Player not found"} + + # Verify PIN and apply changes using simplified method + result = await self.server.database.apply_individual_team_change(player["id"], pin_code) + + if result["success"]: + return {"success": True, "message": "Team saved successfully!"} + else: + return {"success": False, "error": result.get("error", "Invalid PIN")} + + except Exception as e: + print(f"Error in _handle_individual_team_verify_async: {e}") + return {"success": False, "error": str(e)} + def handle_pet_rename_request(self, nickname): """Handle pet rename request and generate PIN""" try: @@ -9927,6 +10161,1244 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): print(f"Error resetting rate limits: {e}") self.send_json_response({"success": False, "error": str(e)}, 500) + # ================================================================ + # NEW TEAM BUILDER METHODS - Separated Team Management + # ================================================================ + + def serve_team_selection_hub(self, nickname): + """Serve the team selection hub showing all teams with swap options.""" + try: + # Get database and bot from server + database = self.server.database if hasattr(self.server, 'database') else None + bot = self.server.bot if hasattr(self.server, 'bot') else None + + if not database: + self.send_error(500, "Database not available") + return + + # Get team management service + if not hasattr(self, 'team_service'): + from src.team_management import TeamManagementService + from src.pin_authentication import PinAuthenticationService + pin_service = PinAuthenticationService(database, bot) + self.team_service = TeamManagementService(database, pin_service) + + # Get team overview + import asyncio + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + player = loop.run_until_complete(database.get_player(nickname)) + if not player: + self.send_error(404, "Player not found") + return + + team_overview = loop.run_until_complete(self.team_service.get_team_overview(player["id"])) + if not team_overview["success"]: + self.send_error(500, f"Failed to load teams: {team_overview['error']}") + return + + teams = team_overview["teams"] + finally: + loop.close() + + # Generate team hub HTML + content = self.generate_team_hub_content(nickname, teams) + full_page = self.get_page_template(f"Team Management - {nickname}", content, "teambuilder") + + self.send_response(200) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.end_headers() + self.wfile.write(full_page.encode('utf-8')) + + except Exception as e: + print(f"Error serving team selection hub: {e}") + self.send_error(500, "Internal server error") + + def serve_individual_team_editor(self, nickname, team_identifier): + """Serve individual team editor page.""" + try: + # Get database and bot from server + database = self.server.database if hasattr(self.server, 'database') else None + bot = self.server.bot if hasattr(self.server, 'bot') else None + + if not database: + self.send_error(500, "Database not available") + return + + # Get team management service + if not hasattr(self, 'team_service'): + from src.team_management import TeamManagementService + from src.pin_authentication import PinAuthenticationService + pin_service = PinAuthenticationService(database, bot) + self.team_service = TeamManagementService(database, pin_service) + + # Get team data + import asyncio + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + player = loop.run_until_complete(database.get_player(nickname)) + if not player: + self.send_error(404, "Player not found") + return + + team_data = loop.run_until_complete( + self.team_service.get_individual_team_data(player["id"], team_identifier) + ) + if not team_data["success"]: + self.send_error(500, f"Failed to load team: {team_data['error']}") + return + + # Get player's pets for the editor + player_pets = loop.run_until_complete(database.get_player_pets(player["id"])) + finally: + loop.close() + + # Generate individual team editor HTML + content = self.generate_individual_team_editor_content(nickname, team_identifier, team_data, player_pets) + full_page = self.get_page_template(f"{team_data['team_name']} - {nickname}", content, "teambuilder") + + self.send_response(200) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.end_headers() + self.wfile.write(full_page.encode('utf-8')) + + except Exception as e: + print(f"Error serving individual team editor: {e}") + self.send_error(500, "Internal server error") + + def generate_team_hub_content(self, nickname, teams): + """Generate HTML content for team selection hub.""" + return f''' +
Manage your teams and swap configurations with PIN verification
+{"ā” Active battle team" if is_active_team else f"š¾ Saved team configuration (Slot {team_identifier})"}
+ ā Back to Hub +