import os import json import uuid from pathlib import Path from datetime import datetime, timedelta, timezone from threading import Lock from flask import Flask, render_template, jsonify, send_from_directory, request app = Flask(__name__) # File-based storage for commands (works across multiple workers) STORAGE_DIR = Path(__file__).parent / 'command_storage' STORAGE_DIR.mkdir(exist_ok=True) storage_lock = Lock() def cleanup_expired_commands(): """Remove expired command files""" now = datetime.now(timezone.utc) for file_path in STORAGE_DIR.glob('*.json'): try: with open(file_path, 'r') as f: data = json.load(f) if datetime.fromisoformat(data['expires_at']) < now: file_path.unlink() except: pass # Ignore corrupted files # Load projects from version.json def load_projects(): """Load all projects from version.json""" json_path = Path(__file__).parent / 'versions' / 'version.json' with open(json_path, 'r', encoding='utf-8') as f: return json.load(f) def get_project_data(project_key, project_info): """Convert JSON project data to template format""" # Determine project type and compatibility project_type = project_info.get('project_type', 'minecraft') # Get compatibility info compat = None if 'mc_compat' in project_info: compat = project_info['mc_compat'].get('stable', '') elif 'velocity_compat' in project_info: compat = project_info['velocity_compat'].get('stable', '') # Determine display type if project_type in ['minecraft', 'spigot', 'paper', 'bukkit']: display_type = 'Minecraft Plugin' elif project_type == 'velocity': display_type = 'Velocity Plugin' elif project_type == 'discord': display_type = 'Discord Bot' else: display_type = project_type.title() # Get project status (global project status, not version status) project_status = project_info.get('project_status', 'active') # Get version info (prefer beta if specified, otherwise stable) stable_version = project_info.get('stable', 'N/A') beta_version = project_info.get('beta') # Build project dict return { 'id': project_key, 'name': project_info.get('name', project_key.title()), 'tagline': project_info.get('tagline', ''), 'type': display_type, 'description': project_info.get('description', ''), 'long_description': project_info.get('long_description', ''), 'icon': project_info.get('icon', 'fas fa-cube'), 'color': project_type, 'status': project_status.title(), 'version': stable_version, 'beta_version': beta_version, 'compatibility': compat, 'server_types': project_info.get('server_types', []), 'technologies': project_info.get('technologies', []), 'features': project_info.get('features', []), 'commands': project_info.get('commands', []), 'installation': project_info.get('installation', []), 'technical_highlights': project_info.get('technical_highlights', []), 'links': project_info.get('links', {}), 'download': project_info.get('download', {}), 'downloads': project_info.get('stats', {}).get('downloads', 0) if project_info.get('stats', {}).get('downloads') else None, 'users': None, 'url': f'/projects/{project_key}' } # Main routes @app.route('/') def home(): """Homepage with overview of Devanturas and featured projects""" all_projects = load_projects() # Convert to template format - show featured projects (first 6) projects = [] for key, info in list(all_projects.items())[:6]: if info.get('name'): # Only show complete projects projects.append(get_project_data(key, info)) return render_template('index.html', projects=projects) @app.route('/projects') def projects(): """Overview of all projects""" all_projects = load_projects() # Convert all projects to template format projects_list = [] for key, info in all_projects.items(): if info.get('name'): # Only show complete projects projects_list.append(get_project_data(key, info)) return render_template('projects.html', projects=projects_list) @app.route('/minecraft') def minecraft(): """Dedicated Minecraft development page""" all_projects = load_projects() # Filter minecraft plugins only (velocity, spigot, paper, bukkit, etc.) minecraft_project_types = ['minecraft', 'velocity', 'spigot', 'paper', 'bukkit', 'purpur'] minecraft_projects = [] for key, info in all_projects.items(): if info.get('project_type') in minecraft_project_types and info.get('name'): proj = { 'name': info.get('name'), 'description': info.get('description', ''), 'version': info.get('stable', 'N/A'), 'supported_versions': info.get('mc_compat', {}).get('stable', 'N/A'), 'downloads': info.get('stats', {}).get('downloads', 'Available'), 'url': f'/projects/{key}' } minecraft_projects.append(proj) minecraft_info = { 'experience': '7+ Years', 'preferred_apis': ['Spigot API', 'Paper API', 'Bukkit API', 'Velocity API'], 'specialties': ['Plugin Development', 'Server Optimization', 'Custom Commands', 'Permission Systems'], 'server_types': ['Spigot', 'Paper', 'Bukkit', 'Velocity'] } return render_template('minecraft.html', projects=minecraft_projects, info=minecraft_info) @app.route('/about') def about(): """About SimolZimol and Devanturas""" skills = { 'Minecraft Development': [ 'Spigot & Paper Plugin Development', 'Velocity Proxy Plugin Development', 'Java Programming', 'Server Administration', 'Performance Optimization', 'Custom Commands & GUIs', 'Permission System Integration' ], 'Discord Development': [ 'Discord.py Bot Development', 'Database Integration (MySQL)', 'Asynchronous Programming', 'Community Management Tools', 'Slash Commands & Interactions', 'Docker Deployment' ], 'General Programming': [ 'Java, Python', 'Database Design', 'RESTful APIs', 'Git Version Control', 'Docker & Containerization', 'Web Development (Flask)' ] } achievements = [ { 'title': 'Published Plugins', 'description': 'Active on Modrinth, Hangar & SpigotMC', 'icon': 'fas fa-download' }, { 'title': 'Multus Discord Bot', 'description': 'In active development since 2020 for Ludi et Historia', 'icon': 'fab fa-discord' }, { 'title': 'Open Source', 'description': 'Contributing to GitHub repositories', 'icon': 'fab fa-github' }, { 'title': '9+ Years Experience', 'description': 'Developing plugins and bots since 2017', 'icon': 'fas fa-calendar-alt' } ] return render_template('about.html', skills=skills, achievements=achievements) @app.route('/contact') def contact(): """Contact information and links""" contact_links = [ { 'name': 'GitHub', 'url': 'https://github.com/SimolZimol/', 'description': 'Open source projects and code repositories', 'icon': 'fab fa-github', 'color': 'github' }, { 'name': 'Discord', 'url': 'https://discordapp.com/users/253922739709018114', 'description': 'Direct contact for questions and support', 'icon': 'fab fa-discord', 'color': 'discord' }, { 'name': 'Modrinth', 'url': 'https://modrinth.com/user/SimolZimol', 'description': 'Minecraft plugins on Modrinth platform', 'icon': 'fas fa-cube', 'color': 'modrinth' }, { 'name': 'SpigotMC', 'url': 'https://www.spigotmc.org/members/simolzimol.123456/', 'description': 'Minecraft plugins and resources', 'icon': 'fas fa-plug', 'color': 'spigot' } ] return render_template('contact.html', links=contact_links) # Healthcheck endpoint for platforms like Coolify @app.route('/health') def health(): return jsonify(status='ok'), 200 # Dynamic project detail pages @app.route('/projects/') def project_detail(project_id): """Dynamic project detail page""" all_projects = load_projects() if project_id not in all_projects: return "Project not found", 404 project_info = all_projects[project_id] project = get_project_data(project_id, project_info) return render_template('project_detail.html', project=project) # Item Editor Command Storage @app.route('/projects/itemeditor/storage') def itemeditor_command_storage(): """Item Editor Command Storage page""" return render_template('itemeditor_command_storage.html') @app.route('/projects/itemeditor/storage', methods=['POST']) def store_command(): """Store a command and return UUID link""" cleanup_expired_commands() try: data = request.get_json() command = data.get('command', '').strip() if not command: return jsonify({'success': False, 'error': 'Command is required'}), 400 if len(command) > 10000: return jsonify({'success': False, 'error': 'Command exceeds maximum length of 10,000 characters'}), 400 # Generate UUID command_uuid = str(uuid.uuid4()) # Calculate expiry (30 minutes) - use UTC created_at = datetime.now(timezone.utc) expires_at = created_at + timedelta(minutes=30) # Prepare data command_data = { 'command': command, 'created_at': created_at.isoformat(), 'expires_at': expires_at.isoformat() } # Store to file storage_file = STORAGE_DIR / f'{command_uuid}.json' with storage_lock: with open(storage_file, 'w', encoding='utf-8') as f: json.dump(command_data, f) # Generate URL - use HTTPS if behind proxy scheme = request.headers.get('X-Forwarded-Proto', 'https' if request.is_secure else 'http') host = request.headers.get('X-Forwarded-Host', request.host) retrieval_url = f"{scheme}://{host}/projects/itemeditor/storage/{command_uuid}" return jsonify({ 'success': True, 'uuid': command_uuid, 'url': retrieval_url, 'expires_at': expires_at.isoformat() }), 201 except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/projects/itemeditor/storage/') def retrieve_command(command_uuid): """Retrieve stored command by UUID (JSON response for plugin)""" cleanup_expired_commands() storage_file = STORAGE_DIR / f'{command_uuid}.json' if not storage_file.exists(): return jsonify({'error': 'Command not found or expired'}), 404 try: with open(storage_file, 'r', encoding='utf-8') as f: command_data = json.load(f) # Check if expired if datetime.fromisoformat(command_data['expires_at']) < datetime.now(timezone.utc): storage_file.unlink() return jsonify({'error': 'Command expired'}), 410 return jsonify(command_data), 200 except Exception as e: return jsonify({'error': 'Failed to read command'}), 500 # Serve /versions as JSON @app.route('/versions') def versions(): return send_from_directory('versions', 'version.json', mimetype='application/json') if __name__ == '__main__': # Allow overriding via environment (e.g., Coolify sets PORT) port = int(os.getenv('PORT', '5000')) debug = os.getenv('FLASK_DEBUG', 'false').lower() == 'true' app.run(debug=debug, host='0.0.0.0', port=port)