328 lines
12 KiB
Python
328 lines
12 KiB
Python
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__)
|
|
|
|
# In-memory storage for commands (thread-safe)
|
|
command_storage = {}
|
|
storage_lock = Lock()
|
|
|
|
def cleanup_expired_commands():
|
|
"""Remove expired commands from storage"""
|
|
with storage_lock:
|
|
now = datetime.now(timezone.utc)
|
|
expired_keys = [key for key, value in command_storage.items()
|
|
if datetime.fromisoformat(value['expires_at']) < now]
|
|
for key in expired_keys:
|
|
del command_storage[key]
|
|
|
|
# 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
|
|
minecraft_projects = []
|
|
for key, info in all_projects.items():
|
|
if info.get('project_type') == 'minecraft' 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': 'Open Source',
|
|
'description': 'Contributing to GitHub repositories',
|
|
'icon': 'fab fa-github'
|
|
},
|
|
{
|
|
'title': 'Community',
|
|
'description': 'Supporting Discord communities',
|
|
'icon': 'fab fa-discord'
|
|
}
|
|
]
|
|
|
|
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/<project_id>')
|
|
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)
|
|
|
|
# Store command
|
|
with storage_lock:
|
|
command_storage[command_uuid] = {
|
|
'command': command,
|
|
'created_at': created_at.isoformat(),
|
|
'expires_at': expires_at.isoformat()
|
|
}
|
|
|
|
# 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/<command_uuid>')
|
|
def retrieve_command(command_uuid):
|
|
"""Retrieve stored command by UUID (JSON response for plugin)"""
|
|
cleanup_expired_commands()
|
|
|
|
# Keep lock for entire operation to prevent race conditions
|
|
with storage_lock:
|
|
command_data = command_storage.get(command_uuid)
|
|
|
|
if not command_data:
|
|
return jsonify({'error': 'Command not found or expired'}), 404
|
|
|
|
# Check if expired
|
|
if datetime.fromisoformat(command_data['expires_at']) < datetime.now(timezone.utc):
|
|
del command_storage[command_uuid]
|
|
return jsonify({'error': 'Command expired'}), 410
|
|
|
|
# Return a copy to avoid modification outside lock
|
|
return jsonify(dict(command_data)), 200
|
|
|
|
# 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)
|