Petbot/webserver.py
megaproxy 3f3b66bfaa Update all web links to use petz.rdx4.com domain
🌐 Updated URLs:
- \!help command now points to http://petz.rdx4.com/help
- \!pets command now points to http://petz.rdx4.com/player/{nickname}
- README.md updated to reference petz.rdx4.com
- Web server startup messages show both local and public URLs

🔧 Changes:
- modules/core_commands.py: Updated help URL
- modules/pet_management.py: Updated player profile URL
- webserver.py: Added public URL display in startup messages
- README.md: Updated web interface section

Users will now receive public URLs that work externally instead of localhost URLs.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 00:27:53 +01:00

1494 lines
No EOL
50 KiB
Python

#!/usr/bin/env python3
"""
PetBot Web Server
Provides web interface for bot data including help, player stats, and pet collections
"""
import os
import sys
import asyncio
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
from threading import Thread
import time
# Add the project directory to the path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from src.database import Database
class PetBotRequestHandler(BaseHTTPRequestHandler):
"""HTTP request handler for PetBot web server"""
def do_GET(self):
"""Handle GET requests"""
parsed_path = urlparse(self.path)
path = parsed_path.path
# Route handling
if path == '/':
self.serve_index()
elif path == '/help':
self.serve_help()
elif path == '/players':
self.serve_players()
elif path.startswith('/player/'):
nickname = path[8:] # Remove '/player/' prefix
self.serve_player_profile(nickname)
elif path == '/leaderboard':
self.serve_leaderboard()
elif path == '/locations':
self.serve_locations()
else:
self.send_error(404, "Page not found")
def serve_index(self):
"""Serve the main index page"""
html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PetBot Game Hub</title>
<style>
:root {
--bg-primary: #0f0f23;
--bg-secondary: #1e1e3f;
--text-primary: #cccccc;
--text-accent: #66ff66;
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
body {
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
.header {
text-align: center;
background: var(--gradient-primary);
color: white;
padding: 40px;
border-radius: 20px;
margin-bottom: 40px;
}
.header h1 {
margin: 0;
font-size: 3em;
font-weight: 700;
}
.nav-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.nav-card {
background: var(--bg-secondary);
padding: 30px;
border-radius: 15px;
text-align: center;
text-decoration: none;
color: var(--text-primary);
transition: transform 0.3s ease;
}
.nav-card:hover {
transform: translateY(-5px);
color: var(--text-accent);
}
.nav-card h3 {
margin: 0 0 15px 0;
color: var(--text-accent);
font-size: 1.5em;
}
.status {
background: var(--bg-secondary);
padding: 20px;
border-radius: 15px;
text-align: center;
}
</style>
</head>
<body>
<div class="header">
<h1>🐾 PetBot Game Hub</h1>
<p>Welcome to the PetBot web interface!</p>
<p><em>Connect to irc.libera.chat #petz to play</em></p>
</div>
<div class="nav-grid">
<a href="/help" class="nav-card">
<h3>📚 Command Help</h3>
<p>Complete reference for all bot commands, battle mechanics, and game features</p>
</a>
<a href="/players" class="nav-card">
<h3>👥 Player List</h3>
<p>View all registered players and their basic stats</p>
</a>
<a href="/leaderboard" class="nav-card">
<h3>🏆 Leaderboard</h3>
<p>Top players by level, pets caught, and achievements earned</p>
</a>
<a href="/locations" class="nav-card">
<h3>🗺️ Locations</h3>
<p>Explore all game locations and see what pets can be found where</p>
</a>
</div>
<div class="status">
<p><strong>🤖 Bot Status:</strong> Online and ready for commands!</p>
<p>Use <code>!help</code> in #petz for quick command reference</p>
</div>
</body>
</html>"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_help(self):
"""Serve the help page"""
try:
with open('help.html', 'r') as f:
content = f.read()
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(content.encode())
except FileNotFoundError:
self.send_error(404, "Help file not found")
def serve_players(self):
"""Serve the players page with real data"""
# Get database instance from the server class
database = self.server.database if hasattr(self.server, 'database') else None
if not database:
self.serve_error_page("Players", "Database not available")
return
# Fetch players data
try:
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
players_data = loop.run_until_complete(self.fetch_players_data(database))
loop.close()
self.serve_players_data(players_data)
except Exception as e:
print(f"Error fetching players data: {e}")
self.serve_error_page("Players", f"Error loading players: {str(e)}")
async def fetch_players_data(self, database):
"""Fetch all players data from database"""
try:
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
# Get all players with basic stats
cursor = await db.execute("""
SELECT p.nickname, p.level, p.experience, p.money, p.created_at,
l.name as location_name,
(SELECT COUNT(*) FROM pets WHERE player_id = p.id) as pet_count,
(SELECT COUNT(*) FROM pets WHERE player_id = p.id AND is_active = 1) as active_pets,
(SELECT COUNT(*) FROM player_achievements WHERE player_id = p.id) as achievement_count
FROM players p
LEFT JOIN locations l ON p.current_location_id = l.id
ORDER BY p.level DESC, p.experience DESC
""")
rows = await cursor.fetchall()
# Convert SQLite rows to dictionaries properly
players = []
for row in rows:
player_dict = {
'nickname': row[0],
'level': row[1],
'experience': row[2],
'money': row[3],
'created_at': row[4],
'location_name': row[5],
'pet_count': row[6],
'active_pets': row[7],
'achievement_count': row[8]
}
players.append(player_dict)
return players
except Exception as e:
print(f"Database error fetching players: {e}")
return []
def serve_players_data(self, players_data):
"""Serve players page with real data"""
# Build players table HTML
if players_data:
players_html = ""
for i, player in enumerate(players_data, 1):
rank_emoji = {"1": "🥇", "2": "🥈", "3": "🥉"}.get(str(i), f"{i}.")
players_html += f"""
<tr onclick="window.location.href='/player/{player['nickname']}'" style="cursor: pointer;">
<td><strong>{rank_emoji}</strong></td>
<td><a href="/player/{player['nickname']}" style="color: var(--text-accent); text-decoration: none;"><strong>{player['nickname']}</strong></a></td>
<td>{player['level']}</td>
<td>{player['experience']}</td>
<td>${player['money']}</td>
<td>{player['pet_count']}</td>
<td>{player['active_pets']}</td>
<td>{player['achievement_count']}</td>
<td>{player.get('location_name', 'Unknown')}</td>
</tr>"""
else:
players_html = """
<tr>
<td colspan="9" style="text-align: center; padding: 40px;">
No players found. Be the first to use !start in #petz!
</td>
</tr>"""
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PetBot - Players</title>
<style>
:root {{
--bg-primary: #0f0f23;
--bg-secondary: #1e1e3f;
--bg-tertiary: #2a2a4a;
--text-primary: #cccccc;
--text-secondary: #999999;
--text-accent: #66ff66;
--border-color: #333366;
--hover-color: #3a3a5a;
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
}}
body {{
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}}
.header {{
text-align: center;
background: var(--gradient-primary);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
box-shadow: var(--shadow-dark);
}}
.header h1 {{
margin: 0;
font-size: 2.5em;
font-weight: 700;
}}
.back-link {{
color: var(--text-accent);
text-decoration: none;
margin-bottom: 20px;
display: inline-block;
font-weight: 500;
}}
.back-link:hover {{
text-decoration: underline;
}}
.section {{
background: var(--bg-secondary);
border-radius: 15px;
overflow: hidden;
box-shadow: var(--shadow-dark);
border: 1px solid var(--border-color);
}}
.section-header {{
background: var(--gradient-primary);
color: white;
padding: 20px 25px;
font-size: 1.3em;
font-weight: 700;
}}
.section-content {{
padding: 25px;
}}
.players-table {{
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: var(--bg-tertiary);
border-radius: 8px;
overflow: hidden;
}}
.players-table th, .players-table td {{
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}}
.players-table th {{
background: var(--bg-primary);
color: var(--text-accent);
font-weight: 600;
font-size: 0.9em;
}}
.players-table tr:hover {{
background: var(--hover-color);
}}
.stats-summary {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}}
.stat-card {{
background: var(--bg-tertiary);
padding: 20px;
border-radius: 10px;
text-align: center;
border: 1px solid var(--border-color);
}}
.stat-value {{
font-size: 2em;
font-weight: bold;
color: var(--text-accent);
margin-bottom: 5px;
}}
.stat-label {{
color: var(--text-secondary);
font-size: 0.9em;
}}
</style>
</head>
<body>
<a href="/" class="back-link">← Back to Game Hub</a>
<div class="header">
<h1>👥 Registered Players</h1>
<p>All trainers on their pet collection journey</p>
</div>
<div class="section">
<div class="section-header">📊 Server Statistics</div>
<div class="section-content">
<div class="stats-summary">
<div class="stat-card">
<div class="stat-value">{len(players_data)}</div>
<div class="stat-label">Total Players</div>
</div>
<div class="stat-card">
<div class="stat-value">{sum(p['pet_count'] for p in players_data)}</div>
<div class="stat-label">Total Pets Caught</div>
</div>
<div class="stat-card">
<div class="stat-value">{sum(p['achievement_count'] for p in players_data)}</div>
<div class="stat-label">Total Achievements</div>
</div>
<div class="stat-card">
<div class="stat-value">{max((p['level'] for p in players_data), default=0)}</div>
<div class="stat-label">Highest Level</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-header">🏆 Player Rankings</div>
<div class="section-content">
<table class="players-table">
<thead>
<tr>
<th>Rank</th>
<th>Player</th>
<th>Level</th>
<th>Experience</th>
<th>Money</th>
<th>Pets</th>
<th>Active</th>
<th>Achievements</th>
<th>Location</th>
</tr>
</thead>
<tbody>
{players_html}
</tbody>
</table>
<div style="text-align: center; margin-top: 20px; color: var(--text-secondary);">
<p>💡 Click on any player name to view their detailed profile</p>
</div>
</div>
</div>
</body>
</html>"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_error_page(self, page_name, error_msg):
"""Serve a generic error page"""
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PetBot - Error</title>
<style>
:root {{
--bg-primary: #0f0f23;
--bg-secondary: #1e1e3f;
--text-primary: #cccccc;
--text-accent: #66ff66;
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}}
body {{
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
text-align: center;
}}
.header {{
background: var(--gradient-primary);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
}}
.back-link {{
color: var(--text-accent);
text-decoration: none;
margin-bottom: 20px;
display: inline-block;
}}
.error-message {{
background: var(--bg-secondary);
padding: 40px;
border-radius: 15px;
border: 2px solid #ff4444;
}}
</style>
</head>
<body>
<a href="/" class="back-link">← Back to Game Hub</a>
<div class="header">
<h1>⚠️ Error Loading {page_name}</h1>
</div>
<div class="error-message">
<h2>Unable to load page</h2>
<p>{error_msg}</p>
<p>Please try again later or contact an administrator.</p>
</div>
</body>
</html>"""
self.send_response(500)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_leaderboard(self):
"""Serve the leaderboard page - redirect to players for now"""
# For now, leaderboard is the same as players page since they're ranked
# In the future, this could have different categories
self.send_response(302) # Temporary redirect
self.send_header('Location', '/players')
self.end_headers()
def serve_locations(self):
"""Serve the locations page with real data"""
# Get database instance from the server class
database = self.server.database if hasattr(self.server, 'database') else None
if not database:
self.serve_error_page("Locations", "Database not available")
return
# Fetch locations data
try:
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
locations_data = loop.run_until_complete(self.fetch_locations_data(database))
loop.close()
self.serve_locations_data(locations_data)
except Exception as e:
print(f"Error fetching locations data: {e}")
self.serve_error_page("Locations", f"Error loading locations: {str(e)}")
async def fetch_locations_data(self, database):
"""Fetch all locations and their spawn data from database"""
try:
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
# Get all locations
cursor = await db.execute("""
SELECT l.*,
GROUP_CONCAT(ps.name || ' (' || ps.type1 ||
CASE WHEN ps.type2 IS NOT NULL THEN '/' || ps.type2 ELSE '' END || ')') as spawns
FROM locations l
LEFT JOIN location_spawns ls ON l.id = ls.location_id
LEFT JOIN pet_species ps ON ls.species_id = ps.id
GROUP BY l.id
ORDER BY l.id
""")
rows = await cursor.fetchall()
# Convert SQLite rows to dictionaries properly
locations = []
for row in rows:
location_dict = {
'id': row[0],
'name': row[1],
'description': row[2],
'level_min': row[3],
'level_max': row[4],
'spawns': row[5] if len(row) > 5 else None
}
locations.append(location_dict)
return locations
except Exception as e:
print(f"Database error fetching locations: {e}")
return []
def serve_locations_data(self, locations_data):
"""Serve locations page with real data"""
# Build locations HTML
locations_html = ""
if locations_data:
for location in locations_data:
spawns = location.get('spawns', 'No pets found')
if not spawns or spawns == 'None':
spawns = "No pets spawn here yet"
# Split spawns into a readable list
spawn_list = spawns.split(',') if spawns != "No pets spawn here yet" else []
spawn_badges = ""
for spawn in spawn_list[:6]: # Limit to first 6 for display
spawn_badges += f'<span class="spawn-badge">{spawn.strip()}</span>'
if len(spawn_list) > 6:
spawn_badges += f'<span class="spawn-badge">+{len(spawn_list) - 6} more</span>'
if not spawn_badges:
spawn_badges = '<em style="color: var(--text-secondary);">No pets spawn here yet</em>'
locations_html += f"""
<div class="location-card">
<div class="location-header">
<h3>🗺️ {location['name']}</h3>
<div class="location-id">ID: {location['id']}</div>
</div>
<div class="location-description">
{location['description']}
</div>
<div class="location-levels">
<strong>Level Range:</strong> {location['level_min']}-{location['level_max']}
</div>
<div class="location-spawns">
<strong>Wild Pets:</strong><br>
{spawn_badges}
</div>
</div>"""
else:
locations_html = """
<div class="location-card">
<div class="location-header">
<h3>No Locations Found</h3>
</div>
<div class="location-description">
No game locations are configured yet.
</div>
</div>"""
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PetBot - Locations</title>
<style>
:root {{
--bg-primary: #0f0f23;
--bg-secondary: #1e1e3f;
--bg-tertiary: #2a2a4a;
--text-primary: #cccccc;
--text-secondary: #999999;
--text-accent: #66ff66;
--border-color: #333366;
--hover-color: #3a3a5a;
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
}}
body {{
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}}
.header {{
text-align: center;
background: var(--gradient-primary);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
box-shadow: var(--shadow-dark);
}}
.header h1 {{
margin: 0;
font-size: 2.5em;
font-weight: 700;
}}
.back-link {{
color: var(--text-accent);
text-decoration: none;
margin-bottom: 20px;
display: inline-block;
font-weight: 500;
}}
.back-link:hover {{
text-decoration: underline;
}}
.locations-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 25px;
margin-top: 30px;
}}
.location-card {{
background: var(--bg-secondary);
border-radius: 15px;
overflow: hidden;
box-shadow: var(--shadow-dark);
border: 1px solid var(--border-color);
transition: transform 0.3s ease;
}}
.location-card:hover {{
transform: translateY(-5px);
}}
.location-header {{
background: var(--gradient-primary);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}}
.location-header h3 {{
margin: 0;
font-size: 1.3em;
font-weight: 700;
}}
.location-id {{
background: rgba(255,255,255,0.2);
padding: 5px 10px;
border-radius: 20px;
font-size: 0.8em;
}}
.location-description {{
padding: 20px;
color: var(--text-primary);
font-style: italic;
border-bottom: 1px solid var(--border-color);
}}
.location-spawns {{
padding: 20px;
}}
.spawn-badge {{
background: var(--bg-tertiary);
color: var(--text-accent);
padding: 4px 10px;
border-radius: 12px;
font-size: 0.85em;
margin: 3px;
display: inline-block;
border: 1px solid var(--border-color);
}}
.info-section {{
background: var(--bg-secondary);
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
box-shadow: var(--shadow-dark);
border: 1px solid var(--border-color);
}}
.info-section h2 {{
color: var(--text-accent);
margin-top: 0;
}}
</style>
</head>
<body>
<a href="/" class="back-link">← Back to Game Hub</a>
<div class="header">
<h1>🗺️ Game Locations</h1>
<p>Explore all areas and discover what pets await you!</p>
</div>
<div class="info-section">
<h2>🎯 How Locations Work</h2>
<p><strong>Travel:</strong> Use <code>!travel &lt;location&gt;</code> to move between areas</p>
<p><strong>Explore:</strong> Use <code>!explore</code> to find wild pets in your current location</p>
<p><strong>Unlock:</strong> Some locations require achievements - catch specific pet types to unlock new areas!</p>
<p><strong>Weather:</strong> Check <code>!weather</code> for conditions that boost certain pet spawn rates</p>
</div>
<div class="locations-grid">
{locations_html}
</div>
<div class="info-section">
<p style="text-align: center; color: var(--text-secondary); margin: 0;">
💡 Use <code>!wild &lt;location&gt;</code> in #petz to see what pets spawn in a specific area
</p>
</div>
</body>
</html>"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_player_profile(self, nickname):
"""Serve individual player profile page"""
# URL decode the nickname in case it has special characters
from urllib.parse import unquote
nickname = unquote(nickname)
# Get database instance from the server class
database = self.server.database if hasattr(self.server, 'database') else None
if not database:
self.serve_player_error(nickname, "Database not available")
return
# Fetch player data
try:
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
player_data = loop.run_until_complete(self.fetch_player_data(database, nickname))
loop.close()
if player_data is None:
self.serve_player_not_found(nickname)
return
self.serve_player_data(nickname, player_data)
except Exception as e:
print(f"Error fetching player data for {nickname}: {e}")
self.serve_player_error(nickname, f"Error loading player data: {str(e)}")
return
async def fetch_player_data(self, database, nickname):
"""Fetch all player data from database"""
try:
# Get player info
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
# Get player basic info
cursor = await db.execute("""
SELECT p.*, l.name as location_name, l.description as location_desc
FROM players p
LEFT JOIN locations l ON p.current_location_id = l.id
WHERE p.nickname = ?
""", (nickname,))
player = await cursor.fetchone()
if not player:
return None
# Convert to dict manually
player_dict = {
'id': player[0],
'nickname': player[1],
'created_at': player[2],
'last_active': player[3],
'level': player[4],
'experience': player[5],
'money': player[6],
'current_location_id': player[7],
'location_name': player[8],
'location_desc': player[9]
}
# Get player pets
cursor = await db.execute("""
SELECT p.*, ps.name as species_name, ps.type1, ps.type2
FROM pets p
JOIN pet_species ps ON p.species_id = ps.id
WHERE p.player_id = ?
ORDER BY p.is_active DESC, p.level DESC, p.id ASC
""", (player_dict['id'],))
pets_rows = await cursor.fetchall()
pets = []
for row in pets_rows:
pet_dict = {
'id': row[0], 'player_id': row[1], 'species_id': row[2],
'nickname': row[3], 'level': row[4], 'experience': row[5],
'hp': row[6], 'max_hp': row[7], 'attack': row[8],
'defense': row[9], 'speed': row[10], 'happiness': row[11],
'caught_at': row[12], 'is_active': row[13],
'species_name': row[14], 'type1': row[15], 'type2': row[16]
}
pets.append(pet_dict)
# Get player achievements
cursor = await db.execute("""
SELECT pa.*, a.name as achievement_name, a.description as achievement_desc
FROM player_achievements pa
JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.player_id = ?
ORDER BY pa.completed_at DESC
""", (player_dict['id'],))
achievements_rows = await cursor.fetchall()
achievements = []
for row in achievements_rows:
achievement_dict = {
'id': row[0], 'player_id': row[1], 'achievement_id': row[2],
'completed_at': row[3], 'achievement_name': row[4], 'achievement_desc': row[5]
}
achievements.append(achievement_dict)
# Get player inventory
cursor = await db.execute("""
SELECT i.name, i.description, i.category, i.rarity, pi.quantity
FROM player_inventory pi
JOIN items i ON pi.item_id = i.id
WHERE pi.player_id = ?
ORDER BY i.rarity DESC, i.name ASC
""", (player_dict['id'],))
inventory_rows = await cursor.fetchall()
inventory = []
for row in inventory_rows:
item_dict = {
'name': row[0], 'description': row[1], 'category': row[2],
'rarity': row[3], 'quantity': row[4]
}
inventory.append(item_dict)
return {
'player': player_dict,
'pets': pets,
'achievements': achievements,
'inventory': inventory
}
except Exception as e:
print(f"Database error fetching player {nickname}: {e}")
return None
def serve_player_not_found(self, nickname):
"""Serve player not found page"""
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PetBot - Player Not Found</title>
<style>
:root {{
--bg-primary: #0f0f23;
--bg-secondary: #1e1e3f;
--text-primary: #cccccc;
--text-accent: #66ff66;
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}}
body {{
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
text-align: center;
}}
.header {{
background: var(--gradient-primary);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
}}
.back-link {{
color: var(--text-accent);
text-decoration: none;
margin-bottom: 20px;
display: inline-block;
}}
.error-message {{
background: var(--bg-secondary);
padding: 40px;
border-radius: 15px;
border: 2px solid #ff4444;
}}
</style>
</head>
<body>
<a href="/" class="back-link">← Back to Game Hub</a>
<div class="header">
<h1>🚫 Player Not Found</h1>
</div>
<div class="error-message">
<h2>Player "{nickname}" not found</h2>
<p>This player hasn't started their journey yet or doesn't exist.</p>
<p>Players can use <code>!start</code> in #petz to begin their adventure!</p>
</div>
</body>
</html>"""
self.send_response(404)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_player_error(self, nickname, error_msg):
"""Serve player error page"""
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PetBot - Error</title>
<style>
:root {{
--bg-primary: #0f0f23;
--bg-secondary: #1e1e3f;
--text-primary: #cccccc;
--text-accent: #66ff66;
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}}
body {{
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
text-align: center;
}}
.header {{
background: var(--gradient-primary);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
}}
.back-link {{
color: var(--text-accent);
text-decoration: none;
margin-bottom: 20px;
display: inline-block;
}}
.error-message {{
background: var(--bg-secondary);
padding: 40px;
border-radius: 15px;
border: 2px solid #ff4444;
}}
</style>
</head>
<body>
<a href="/" class="back-link">← Back to Game Hub</a>
<div class="header">
<h1>⚠️ Error</h1>
</div>
<div class="error-message">
<h2>Unable to load player data</h2>
<p>{error_msg}</p>
<p>Please try again later or contact an administrator.</p>
</div>
</body>
</html>"""
self.send_response(500)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_player_data(self, nickname, player_data):
"""Serve player profile page with real data"""
player = player_data['player']
pets = player_data['pets']
achievements = player_data['achievements']
inventory = player_data.get('inventory', [])
# Calculate stats
active_pets = [pet for pet in pets if pet['is_active']]
total_pets = len(pets)
active_count = len(active_pets)
# Build pets table HTML
pets_html = ""
if pets:
for pet in pets:
status = "⭐ Active" if pet['is_active'] else "📦 Storage"
status_class = "pet-active" if pet['is_active'] else "pet-stored"
name = pet['nickname'] or pet['species_name']
type_str = pet['type1']
if pet['type2']:
type_str += f"/{pet['type2']}"
pets_html += f"""
<tr>
<td class="{status_class}">{status}</td>
<td><strong>{name}</strong></td>
<td>{pet['species_name']}</td>
<td><span class="type-badge">{type_str}</span></td>
<td>{pet['level']}</td>
<td>{pet['hp']}/{pet['max_hp']}</td>
<td>ATK: {pet['attack']} | DEF: {pet['defense']} | SPD: {pet['speed']}</td>
</tr>"""
else:
pets_html = """
<tr>
<td colspan="7" style="text-align: center; padding: 40px;">
No pets found. Use !explore and !catch to start your collection!
</td>
</tr>"""
# Build achievements HTML
achievements_html = ""
if achievements:
for achievement in achievements:
achievements_html += f"""
<div style="background: var(--bg-tertiary); padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid var(--text-accent);">
<strong>🏆 {achievement['achievement_name']}</strong><br>
<small>{achievement['achievement_desc']}</small><br>
<em style="color: var(--text-secondary);">Earned: {achievement['completed_at']}</em>
</div>"""
else:
achievements_html = """
<div style="text-align: center; padding: 40px; color: var(--text-secondary);">
No achievements yet. Keep exploring and catching pets to earn achievements!
</div>"""
# Build inventory HTML
inventory_html = ""
if inventory:
rarity_symbols = {
"common": "",
"uncommon": "",
"rare": "",
"epic": "",
"legendary": ""
}
rarity_colors = {
"common": "#ffffff",
"uncommon": "#1eff00",
"rare": "#0070dd",
"epic": "#a335ee",
"legendary": "#ff8000"
}
for item in inventory:
symbol = rarity_symbols.get(item['rarity'], "")
color = rarity_colors.get(item['rarity'], "#ffffff")
quantity_str = f" x{item['quantity']}" if item['quantity'] > 1 else ""
inventory_html += f"""
<div style="background: var(--bg-tertiary); padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid {color};">
<strong style="color: {color};">{symbol} {item['name']}{quantity_str}</strong><br>
<small>{item['description']}</small><br>
<em style="color: var(--text-secondary);">Category: {item['category'].replace('_', ' ').title()} | Rarity: {item['rarity'].title()}</em>
</div>"""
else:
inventory_html = """
<div style="text-align: center; padding: 40px; color: var(--text-secondary);">
No items yet. Try exploring to find useful items!
</div>"""
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PetBot - {nickname}'s Profile</title>
<style>
:root {{
--bg-primary: #0f0f23;
--bg-secondary: #1e1e3f;
--bg-tertiary: #2a2a4a;
--text-primary: #cccccc;
--text-secondary: #999999;
--text-accent: #66ff66;
--border-color: #333366;
--hover-color: #3a3a5a;
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
}}
body {{
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}}
.header {{
text-align: center;
background: var(--gradient-primary);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
box-shadow: var(--shadow-dark);
}}
.header h1 {{
margin: 0;
font-size: 2.5em;
font-weight: 700;
}}
.back-link {{
color: var(--text-accent);
text-decoration: none;
margin-bottom: 20px;
display: inline-block;
font-weight: 500;
}}
.back-link:hover {{
text-decoration: underline;
}}
.section {{
background: var(--bg-secondary);
margin-bottom: 30px;
border-radius: 15px;
overflow: hidden;
box-shadow: var(--shadow-dark);
border: 1px solid var(--border-color);
}}
.section-header {{
background: var(--gradient-primary);
color: white;
padding: 20px 25px;
font-size: 1.3em;
font-weight: 700;
}}
.section-content {{
padding: 25px;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 20px;
}}
.stat-card {{
background: var(--bg-tertiary);
padding: 20px;
border-radius: 10px;
text-align: center;
border: 1px solid var(--border-color);
}}
.stat-value {{
font-size: 2em;
font-weight: bold;
color: var(--text-accent);
margin-bottom: 5px;
}}
.stat-label {{
color: var(--text-secondary);
font-size: 0.9em;
}}
.pets-table {{
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: var(--bg-tertiary);
border-radius: 8px;
overflow: hidden;
}}
.pets-table th, .pets-table td {{
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}}
.pets-table th {{
background: var(--bg-primary);
color: var(--text-accent);
font-weight: 600;
}}
.pets-table tr:hover {{
background: var(--hover-color);
}}
.pet-active {{
color: var(--text-accent);
font-weight: bold;
}}
.pet-stored {{
color: var(--text-secondary);
}}
.type-badge {{
background: var(--bg-primary);
color: var(--text-accent);
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
margin-right: 5px;
}}
</style>
</head>
<body>
<a href="/" class="back-link">← Back to Game Hub</a>
<div class="header">
<h1>🐾 {nickname}'s Profile</h1>
<p>Level {player['level']} Trainer</p>
<p><em>Currently in {player.get('location_name', 'Unknown Location')}</em></p>
</div>
<div class="section">
<div class="section-header">📊 Player Statistics</div>
<div class="section-content">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{player['level']}</div>
<div class="stat-label">Level</div>
</div>
<div class="stat-card">
<div class="stat-value">{player['experience']}</div>
<div class="stat-label">Experience</div>
</div>
<div class="stat-card">
<div class="stat-value">${player['money']}</div>
<div class="stat-label">Money</div>
</div>
<div class="stat-card">
<div class="stat-value">{total_pets}</div>
<div class="stat-label">Pets Caught</div>
</div>
<div class="stat-card">
<div class="stat-value">{active_count}</div>
<div class="stat-label">Active Pets</div>
</div>
<div class="stat-card">
<div class="stat-value">{len(achievements)}</div>
<div class="stat-label">Achievements</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-header">🐾 Pet Collection</div>
<div class="section-content">
<table class="pets-table">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th>Species</th>
<th>Type</th>
<th>Level</th>
<th>HP</th>
<th>Stats</th>
</tr>
</thead>
<tbody>
{pets_html}
</tbody>
</table>
</div>
</div>
<div class="section">
<div class="section-header">🏆 Achievements</div>
<div class="section-content">
{achievements_html}
</div>
</div>
<div class="section">
<div class="section-header">🎒 Inventory</div>
<div class="section-content">
{inventory_html}
</div>
</div>
</body>
</html>"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def log_message(self, format, *args):
"""Override to reduce logging noise"""
pass
class PetBotWebServer:
def __init__(self, database, port=8080):
self.database = database
self.port = port
self.server = None
def run(self):
"""Start the HTTP web server"""
print(f"🌐 Starting PetBot web server on http://0.0.0.0:{self.port}")
print(f"📡 Accessible from WSL at: http://172.27.217.61:{self.port}")
print(f"📡 Accessible from Windows at: http://localhost:{self.port}")
self.server = HTTPServer(('0.0.0.0', self.port), PetBotRequestHandler)
# Pass database to the server so handlers can access it
self.server.database = self.database
self.server.serve_forever()
def start_in_thread(self):
"""Start the web server in a background thread"""
thread = Thread(target=self.run, daemon=True)
thread.start()
print(f"✅ Web server started at http://localhost:{self.port}")
print(f"🌐 Public access at: http://petz.rdx4.com/")
return thread
def run_standalone():
"""Run the web server standalone for testing"""
print("🐾 PetBot Web Server (Standalone Mode)")
print("=" * 40)
# Initialize database
database = Database()
# Note: In standalone mode, we can't easily run async init
# This is mainly for testing the web routes
# Start web server
server = PetBotWebServer(database)
print("🚀 Starting web server...")
print("📝 Available routes:")
print(" http://localhost:8080/ - Game Hub (local)")
print(" http://localhost:8080/help - Command Help (local)")
print(" http://localhost:8080/players - Player List (local)")
print(" http://localhost:8080/leaderboard - Leaderboard (local)")
print(" http://localhost:8080/locations - Locations (local)")
print("")
print("🌐 Public URLs:")
print(" http://petz.rdx4.com/ - Game Hub")
print(" http://petz.rdx4.com/help - Command Help")
print(" http://petz.rdx4.com/players - Player List")
print(" http://petz.rdx4.com/leaderboard - Leaderboard")
print(" http://petz.rdx4.com/locations - Locations")
print("")
print("Press Ctrl+C to stop")
try:
server.run()
except KeyboardInterrupt:
print("\n✅ Web server stopped")
if __name__ == "__main__":
run_standalone()