mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-04-28 11:55:20 +00:00
a4d5f5478b
Introduced a new command structure for RandomLine entries in the website generation process, allowing for dynamic rendering of random lines based on configuration. Updated the configuration file to include a new category option for commands and enhanced documentation to reflect these changes. This addition improves the command reference organization and user experience on the website.
2846 lines
92 KiB
Python
Executable File
2846 lines
92 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Generate Command Reference Website
|
|
Creates a single-page HTML website with bot introduction and command reference
|
|
"""
|
|
|
|
import argparse
|
|
import configparser
|
|
import html
|
|
import logging
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
from collections import defaultdict
|
|
from contextlib import closing
|
|
from typing import Any, Optional
|
|
|
|
# Import bot modules with error handling
|
|
try:
|
|
# Add project root to path
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from modules.config_validation import strip_optional_quotes
|
|
from modules.db_manager import DBManager
|
|
from modules.plugin_loader import PluginLoader
|
|
from modules.utils import resolve_path
|
|
except ImportError as e:
|
|
print("Error: Missing required dependencies.")
|
|
print(f"Details: {e}")
|
|
print("\nPlease install bot dependencies by running:")
|
|
print(" pip install -r requirements.txt")
|
|
print("\nOr install the missing module directly:")
|
|
if 'pytz' in str(e):
|
|
print(" pip install pytz")
|
|
sys.exit(1)
|
|
|
|
|
|
class MinimalBot:
|
|
"""Minimal bot mock for plugin loading without full bot initialization"""
|
|
|
|
def __init__(self, config, logger, db_manager=None):
|
|
self.config = config
|
|
self.logger = logger
|
|
self.db_manager = db_manager
|
|
|
|
# Dummy translator
|
|
class DummyTranslator:
|
|
def translate(self, key, **kwargs):
|
|
return key
|
|
def get_value(self, key):
|
|
return None
|
|
|
|
self.translator = DummyTranslator()
|
|
self.command_manager = None # Will be set after plugin loading
|
|
|
|
|
|
# Style definitions for website themes
|
|
# Each style contains CSS custom property values that will be injected into the :root selector
|
|
STYLES = {
|
|
'default': {
|
|
'name': 'Modern Dark',
|
|
'description': 'Dark theme with gradients and modern cards (current default)',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap',
|
|
'css_vars': """--bg-primary: #0a0e14;
|
|
--bg-secondary: #111820;
|
|
--bg-card: #151c25;
|
|
--bg-card-hover: #1a232e;
|
|
--accent-blue: #00d4ff;
|
|
--accent-cyan: #00ffc8;
|
|
--accent-orange: #ff8a00;
|
|
--accent-purple: #a855f7;
|
|
--accent-red: #ff4757;
|
|
--accent-yellow: #ffd700;
|
|
--text-primary: #e8edf4;
|
|
--text-secondary: #8892a4;
|
|
--text-muted: #6b7280;
|
|
--border-subtle: rgba(255,255,255,0.06);
|
|
--glow-blue: rgba(0, 212, 255, 0.15);
|
|
--glow-cyan: rgba(0, 255, 200, 0.1);"""
|
|
},
|
|
'minimalist': {
|
|
'name': 'Minimalist Clean',
|
|
'description': 'Light theme with clean typography and whitespace',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
|
|
'css_vars': """--bg-primary: #ffffff;
|
|
--bg-secondary: #ffffff;
|
|
--bg-card: #ffffff;
|
|
--bg-card-hover: #f8f9fa;
|
|
--accent-blue: #0052cc;
|
|
--accent-cyan: #006699;
|
|
--accent-orange: #d65d0e;
|
|
--accent-purple: #6b21a8;
|
|
--accent-red: #b91c1c;
|
|
--accent-yellow: #a16207;
|
|
--text-primary: #1a1a1a;
|
|
--text-secondary: #4a4a4a;
|
|
--text-muted: #6b6b6b;
|
|
--border-subtle: rgba(0,0,0,0.15);
|
|
--glow-blue: rgba(0,82,204,0.1);
|
|
--glow-cyan: rgba(0,102,153,0.08);""",
|
|
'css_overrides': """
|
|
/* Pure white background - remove atmospheric effects */
|
|
.atmosphere,
|
|
.grid-overlay {
|
|
display: none !important;
|
|
}
|
|
|
|
body {
|
|
background: #ffffff !important;
|
|
}
|
|
|
|
/* Ultra-minimal: square everything */
|
|
*,
|
|
*::before,
|
|
*::after,
|
|
.container,
|
|
.intro-section,
|
|
.sidebar-nav,
|
|
.sidebar-nav a,
|
|
.command-card,
|
|
.command-keyword,
|
|
.command-usage,
|
|
.channel-section,
|
|
.channel-card,
|
|
button,
|
|
input {
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
/* Clean Inter font everywhere */
|
|
body,
|
|
h1, h2, h3, h4, h5, h6,
|
|
p, a, span, div {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
|
|
}
|
|
|
|
/* Remove all shadows */
|
|
.command-card,
|
|
.intro-section,
|
|
.sidebar-nav,
|
|
.command-keyword,
|
|
.command-usage {
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
/* Thin, clean borders */
|
|
.command-card {
|
|
border: 1px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.intro-section {
|
|
border: 1px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.sidebar-nav {
|
|
border-right: 1px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.command-keyword {
|
|
border: 1px solid currentColor !important;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.command-usage {
|
|
border-left: 2px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
/* Minimal hover effects - just background color */
|
|
.command-card:hover {
|
|
box-shadow: none !important;
|
|
transform: none !important;
|
|
}
|
|
|
|
.sidebar-nav a:hover {
|
|
background: var(--bg-card-hover) !important;
|
|
}
|
|
|
|
/* Typography: lighter weight, more spacing */
|
|
h1 {
|
|
font-weight: 600;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
h2, h3 {
|
|
font-weight: 500;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.command-name {
|
|
font-weight: 600;
|
|
}
|
|
|
|
p, .command-description {
|
|
font-weight: 400;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Remove all animations and transitions for instant feedback */
|
|
*, *::before, *::after {
|
|
animation: none !important;
|
|
transition: background-color 0.1s ease !important;
|
|
}
|
|
|
|
/* Clean spacing */
|
|
.command-card {
|
|
padding: 1.5rem !important;
|
|
}
|
|
|
|
/* Subtle, clean keyword badges */
|
|
.command-keyword {
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
"""
|
|
},
|
|
'terminal': {
|
|
'name': 'Terminal/Hacker',
|
|
'description': 'Green/amber on black, monospace, retro terminal aesthetic',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap',
|
|
'css_vars': """--bg-primary: #000000;
|
|
--bg-secondary: #0a0a0a;
|
|
--bg-card: #0f0f0f;
|
|
--bg-card-hover: #1a1a1a;
|
|
--accent-blue: #00ff00;
|
|
--accent-cyan: #00ff00;
|
|
--accent-orange: #ffb000;
|
|
--accent-purple: #00ff00;
|
|
--accent-red: #ff0000;
|
|
--accent-yellow: #ffff00;
|
|
--text-primary: #00ff00;
|
|
--text-secondary: #00aa00;
|
|
--text-muted: #008800;
|
|
--border-subtle: rgba(0,255,0,0.3);
|
|
--glow-blue: rgba(0,255,0,0.2);
|
|
--glow-cyan: rgba(0,255,0,0.15);""",
|
|
'css_overrides': """
|
|
/* Terminal monospace font everywhere */
|
|
body,
|
|
h1, h2, h3, h4, h5, h6,
|
|
p, a, span, div,
|
|
.command-name,
|
|
.command-description,
|
|
.command-usage,
|
|
.command-keyword,
|
|
.sidebar-nav a {
|
|
font-family: 'JetBrains Mono', 'Courier New', monospace !important;
|
|
}
|
|
|
|
/* CRT scanline effect */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: repeating-linear-gradient(
|
|
0deg,
|
|
rgba(0, 255, 0, 0.03),
|
|
rgba(0, 255, 0, 0.03) 1px,
|
|
transparent 1px,
|
|
transparent 2px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
}
|
|
|
|
/* Terminal text glow */
|
|
h1, h2, h3,
|
|
.command-name {
|
|
text-shadow: 0 0 10px rgba(0, 255, 0, 0.8);
|
|
}
|
|
|
|
.command-keyword,
|
|
.command-usage {
|
|
text-shadow: 0 0 5px rgba(0, 255, 0, 0.6);
|
|
}
|
|
|
|
/* Cursor blink effect after usage example */
|
|
.command-card:hover .command-usage::after {
|
|
content: '▮';
|
|
display: inline-block;
|
|
margin-left: 0.5rem;
|
|
animation: blink 1s step-end infinite;
|
|
color: var(--accent-blue);
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 50% { opacity: 1; }
|
|
51%, 100% { opacity: 0; }
|
|
}
|
|
|
|
/* Terminal prompt style for usage */
|
|
.command-usage::before {
|
|
content: '$ ';
|
|
color: var(--accent-yellow);
|
|
font-weight: bold;
|
|
}
|
|
"""
|
|
},
|
|
'glass': {
|
|
'name': 'Glass/Glassmorphism',
|
|
'description': 'Frosted glass cards with blur effects and colorful gradients',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap',
|
|
'css_vars': """--bg-primary: linear-gradient(135deg, #4c5fd7 0%, #764ba2 100%);
|
|
--bg-secondary: rgba(255,255,255,0.05);
|
|
--bg-card: rgba(255,255,255,0.1);
|
|
--bg-card-hover: rgba(255,255,255,0.15);
|
|
--accent-blue: #a8daff;
|
|
--accent-cyan: #a8fff4;
|
|
--accent-orange: #ffc085;
|
|
--accent-purple: #d4b3ff;
|
|
--accent-red: #ff9eb5;
|
|
--accent-yellow: #ffe8a8;
|
|
--text-primary: #ffffff;
|
|
--text-secondary: rgba(255,255,255,0.8);
|
|
--text-muted: rgba(255,255,255,0.6);
|
|
--border-subtle: rgba(255,255,255,0.2);
|
|
--glow-blue: rgba(168,218,255,0.2);
|
|
--glow-cyan: rgba(168,255,244,0.15);"""
|
|
},
|
|
'neon': {
|
|
'name': 'Neon/Cyberpunk',
|
|
'description': 'Bright neon colors, dark backgrounds, futuristic aesthetic',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&display=swap',
|
|
'css_vars': """--bg-primary: #0a0014;
|
|
--bg-secondary: #150028;
|
|
--bg-card: #1a0033;
|
|
--bg-card-hover: #25004d;
|
|
--accent-blue: #00f5ff;
|
|
--accent-cyan: #00f5ff;
|
|
--accent-orange: #ff69b4;
|
|
--accent-purple: #bf40bf;
|
|
--accent-red: #ff1493;
|
|
--accent-yellow: #ffd700;
|
|
--text-primary: #ffffff;
|
|
--text-secondary: #e9d5ff;
|
|
--text-muted: #d8b4fe;
|
|
--border-subtle: rgba(157,78,221,0.3);
|
|
--glow-blue: rgba(0,245,255,0.4);
|
|
--glow-cyan: rgba(247,37,133,0.3);""",
|
|
'css_overrides': """
|
|
/* Neon cyberpunk font */
|
|
body,
|
|
h1, h2, h3, h4, h5, h6,
|
|
.command-name,
|
|
.sidebar-nav a {
|
|
font-family: 'Orbitron', sans-serif !important;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Intense neon glow on headings */
|
|
h1 {
|
|
text-shadow:
|
|
0 0 10px var(--accent-cyan),
|
|
0 0 20px var(--accent-cyan),
|
|
0 0 30px var(--accent-cyan),
|
|
0 0 40px var(--accent-blue);
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
h2, h3 {
|
|
text-shadow:
|
|
0 0 10px var(--accent-purple),
|
|
0 0 20px var(--accent-purple);
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.command-name {
|
|
text-shadow:
|
|
0 0 10px var(--accent-cyan),
|
|
0 0 20px var(--accent-cyan);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* Neon border glow on cards - subtle */
|
|
.command-card {
|
|
border: 2px solid var(--accent-cyan);
|
|
box-shadow:
|
|
0 0 5px var(--glow-blue),
|
|
inset 0 0 5px rgba(0,245,255,0.05);
|
|
}
|
|
|
|
.command-card:hover {
|
|
border-color: var(--accent-orange);
|
|
box-shadow:
|
|
0 0 10px var(--accent-cyan),
|
|
0 0 20px var(--accent-orange),
|
|
inset 0 0 10px rgba(255,105,180,0.1);
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.intro-section {
|
|
border: 2px solid var(--accent-purple);
|
|
box-shadow:
|
|
0 0 8px var(--accent-purple),
|
|
inset 0 0 8px rgba(191,64,191,0.05);
|
|
}
|
|
|
|
/* Neon keyword badges */
|
|
.command-keyword {
|
|
text-shadow: 0 0 8px currentColor;
|
|
border: 1px solid currentColor;
|
|
box-shadow:
|
|
0 0 5px currentColor,
|
|
inset 0 0 5px currentColor;
|
|
}
|
|
|
|
/* Glowing usage examples */
|
|
.command-usage {
|
|
border-left: 3px solid var(--accent-cyan);
|
|
box-shadow: -3px 0 10px var(--glow-blue);
|
|
text-shadow: 0 0 5px rgba(0,245,255,0.5);
|
|
}
|
|
|
|
/* Animated neon flicker effect */
|
|
@keyframes neon-flicker {
|
|
0%, 100% { opacity: 1; }
|
|
41%, 43% { opacity: 0.8; }
|
|
45%, 47% { opacity: 0.9; }
|
|
49% { opacity: 0.85; }
|
|
}
|
|
|
|
h1, .command-name {
|
|
animation: neon-flicker 3s infinite;
|
|
}
|
|
|
|
/* Cyberpunk grid overlay */
|
|
body::after {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-image:
|
|
linear-gradient(rgba(0,245,255,0.03) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(0,245,255,0.03) 1px, transparent 1px);
|
|
background-size: 50px 50px;
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Ensure content is above grid */
|
|
.container {
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* Sidebar neon accent */
|
|
.sidebar-nav {
|
|
border-right: 2px solid var(--accent-cyan);
|
|
box-shadow: 2px 0 15px var(--glow-blue);
|
|
}
|
|
|
|
.sidebar-nav a:hover {
|
|
text-shadow: 0 0 10px var(--accent-cyan);
|
|
background: rgba(0,245,255,0.1);
|
|
}
|
|
"""
|
|
},
|
|
'brutalist': {
|
|
'name': 'Brutalist/Bold',
|
|
'description': 'High contrast, bold typography, thick borders, geometric',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700;900&display=swap',
|
|
'css_vars': """--bg-primary: #ffffff;
|
|
--bg-secondary: #f0f0f0;
|
|
--bg-card: #ffffff;
|
|
--bg-card-hover: #f5f5f5;
|
|
--accent-blue: #0000cc;
|
|
--accent-cyan: #006666;
|
|
--accent-orange: #cc5200;
|
|
--accent-purple: #5c00b8;
|
|
--accent-red: #cc0000;
|
|
--accent-yellow: #997700;
|
|
--text-primary: #000000;
|
|
--text-secondary: #1a1a1a;
|
|
--text-muted: #4d4d4d;
|
|
--border-subtle: rgba(0,0,0,1);
|
|
--glow-blue: rgba(0,0,204,0.2);
|
|
--glow-cyan: rgba(0,102,102,0.2);""",
|
|
'css_overrides': """
|
|
/* BRUTALIST: Square everything aggressively */
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
/* Space Grotesk everywhere, heavy weights */
|
|
body,
|
|
h1, h2, h3, h4, h5, h6,
|
|
p, a, span, div {
|
|
font-family: 'Space Grotesk', sans-serif !important;
|
|
}
|
|
|
|
/* MASSIVE, BOLD headings */
|
|
h1 {
|
|
font-weight: 900 !important;
|
|
font-size: 4rem !important;
|
|
letter-spacing: -0.05em;
|
|
text-transform: uppercase;
|
|
line-height: 0.9;
|
|
}
|
|
|
|
.category-title {
|
|
font-weight: 900 !important;
|
|
font-size: 2.5rem !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: -0.03em;
|
|
border-bottom: 8px solid #000000 !important;
|
|
padding-bottom: 0.5rem !important;
|
|
}
|
|
|
|
.category-title .anchor-link {
|
|
text-decoration: none;
|
|
color: #000000;
|
|
}
|
|
|
|
.command-name {
|
|
font-weight: 900 !important;
|
|
font-size: 1.5rem !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
/* THICK borders everywhere */
|
|
.command-card {
|
|
border: 6px solid #000000 !important;
|
|
box-shadow: 12px 12px 0 #000000 !important;
|
|
}
|
|
|
|
.command-card:hover {
|
|
box-shadow: 16px 16px 0 #000000 !important;
|
|
transform: translate(-4px, -4px) !important;
|
|
}
|
|
|
|
/* Brutal intro box */
|
|
.header-content {
|
|
border: 6px solid #000000 !important;
|
|
box-shadow: 12px 12px 0 #000000 !important;
|
|
border-radius: 0 !important;
|
|
background: #ffffff !important;
|
|
}
|
|
|
|
.header-content::before {
|
|
display: none !important;
|
|
}
|
|
|
|
header {
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
/* Bold, chunky keyword badges */
|
|
.command-keyword {
|
|
font-weight: 700 !important;
|
|
border: 3px solid #000000 !important;
|
|
padding: 0.5rem 1rem !important;
|
|
background: #ffffff;
|
|
text-transform: uppercase;
|
|
font-size: 0.75rem !important;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.command-keyword:nth-child(odd) {
|
|
background: #000000;
|
|
color: #ffffff;
|
|
}
|
|
|
|
/* Heavy usage box */
|
|
.command-usage {
|
|
border: 4px solid #000000 !important;
|
|
background: #f0f0f0 !important;
|
|
padding: 1rem !important;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* Aggressive sidebar - SQUARE */
|
|
.sidebar-nav {
|
|
border-right: 8px solid #000000 !important;
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
.sidebar-nav a {
|
|
font-weight: 700 !important;
|
|
text-transform: uppercase;
|
|
font-size: 0.85rem !important;
|
|
letter-spacing: 0.02em;
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
.sidebar-nav a:hover {
|
|
background: #000000 !important;
|
|
color: #ffffff !important;
|
|
}
|
|
|
|
/* Remove blue hover bar on command cards */
|
|
.command-card::before {
|
|
display: none !important;
|
|
}
|
|
|
|
/* CHANNEL CARDS - same brutal treatment */
|
|
.channel-card {
|
|
border: 6px solid #000000 !important;
|
|
box-shadow: 12px 12px 0 #000000 !important;
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
.channel-card:hover {
|
|
box-shadow: 16px 16px 0 #000000 !important;
|
|
transform: translate(-4px, -4px) !important;
|
|
}
|
|
|
|
.channel-name {
|
|
font-weight: 900 !important;
|
|
font-size: 1.5rem !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.channel-description {
|
|
font-weight: 500 !important;
|
|
}
|
|
|
|
.channel-section h2 {
|
|
font-weight: 900 !important;
|
|
font-size: 2.5rem !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: -0.03em;
|
|
border-bottom: 8px solid #000000 !important;
|
|
padding-bottom: 0.5rem !important;
|
|
}
|
|
|
|
/* Remove all smooth transitions - instant feedback */
|
|
* {
|
|
transition: none !important;
|
|
}
|
|
|
|
/* Heavy typography throughout */
|
|
p, .command-description {
|
|
font-weight: 500 !important;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Brutal intro section */
|
|
.intro {
|
|
font-weight: 700 !important;
|
|
font-size: 1.2rem !important;
|
|
}
|
|
"""
|
|
},
|
|
'gradient': {
|
|
'name': 'Gradient/Modern',
|
|
'description': 'Colorful gradients, smooth transitions, vibrant colors',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap',
|
|
'css_vars': """--bg-primary: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
|
--bg-secondary: rgba(0,0,0,0.08);
|
|
--bg-card: rgba(255,255,255,0.95);
|
|
--bg-card-hover: rgba(255,255,255,0.98);
|
|
--accent-blue: #4338ca;
|
|
--accent-cyan: #0891b2;
|
|
--accent-orange: #ea580c;
|
|
--accent-purple: #7c3aed;
|
|
--accent-red: #dc2626;
|
|
--accent-yellow: #ca8a04;
|
|
--text-primary: #1a1a1a;
|
|
--text-secondary: #374151;
|
|
--text-muted: #6b7280;
|
|
--border-subtle: rgba(0,0,0,0.15);
|
|
--glow-blue: rgba(79,70,229,0.3);
|
|
--glow-cyan: rgba(8,145,178,0.25);""",
|
|
'css_overrides': """
|
|
/* Vibrant gradient borders */
|
|
.command-card {
|
|
border: 2px solid transparent;
|
|
background:
|
|
linear-gradient(white, white) padding-box,
|
|
linear-gradient(135deg, #667eea, #764ba2, #f093fb) border-box;
|
|
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.15);
|
|
background-origin: border-box;
|
|
background-clip: padding-box, border-box;
|
|
}
|
|
|
|
.command-card:hover {
|
|
box-shadow: 0 8px 30px rgba(102, 126, 234, 0.25), 0 0 0 1px rgba(102, 126, 234, 0.1);
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.intro-section {
|
|
border: 2px solid transparent;
|
|
background:
|
|
linear-gradient(white, white) padding-box,
|
|
linear-gradient(135deg, #667eea, #00d4ff) border-box;
|
|
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.15);
|
|
}
|
|
|
|
/* Gradient text for headings */
|
|
h1 {
|
|
background: linear-gradient(135deg, #667eea, #764ba2, #f093fb);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
/* Keep section headers (category titles) visible with high contrast */
|
|
.category-title .anchor-link {
|
|
background: linear-gradient(135deg, #1a1a1a, #374151);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
text-decoration: none;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.command-name {
|
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* Colorful keyword badges with gradients */
|
|
.command-keyword {
|
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
color: white;
|
|
border: none;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.command-keyword:nth-child(2) {
|
|
background: linear-gradient(135deg, #f093fb, #667eea);
|
|
}
|
|
|
|
.command-keyword:nth-child(3) {
|
|
background: linear-gradient(135deg, #764ba2, #f093fb);
|
|
}
|
|
|
|
.command-keyword:nth-child(4) {
|
|
background: linear-gradient(135deg, #00d4ff, #667eea);
|
|
}
|
|
|
|
.command-keyword:nth-child(5) {
|
|
background: linear-gradient(135deg, #f093fb, #00d4ff);
|
|
}
|
|
|
|
/* Animated gradient on hover */
|
|
@keyframes gradient-shift {
|
|
0% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
100% { background-position: 0% 50%; }
|
|
}
|
|
|
|
.command-card:hover .command-name {
|
|
background: linear-gradient(135deg, #667eea, #764ba2, #f093fb, #00d4ff);
|
|
background-size: 300% 300%;
|
|
animation: gradient-shift 3s ease infinite;
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
/* Gradient usage box */
|
|
.command-usage {
|
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05), rgba(240, 147, 251, 0.05));
|
|
border-left: 3px solid;
|
|
border-image: linear-gradient(135deg, #667eea, #f093fb) 1;
|
|
}
|
|
|
|
/* Sidebar gradient accent */
|
|
.sidebar-nav {
|
|
border-right: 3px solid transparent;
|
|
border-image: linear-gradient(180deg, #667eea, #764ba2, #f093fb) 1;
|
|
}
|
|
|
|
.sidebar-nav a:hover {
|
|
background: linear-gradient(90deg, rgba(102, 126, 234, 0.1), transparent);
|
|
border-left: 3px solid #667eea;
|
|
padding-left: 1.5rem;
|
|
}
|
|
"""
|
|
},
|
|
'pixel': {
|
|
'name': 'Pixel/Retro',
|
|
'description': 'Pixel art aesthetic, squared boxes, bright retro gaming colors',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap',
|
|
'css_vars': """--bg-primary: #2b2d42;
|
|
--bg-secondary: #3a3d5c;
|
|
--bg-card: #4a4e69;
|
|
--bg-card-hover: #5a5f7e;
|
|
--accent-blue: #00b4d8;
|
|
--accent-cyan: #00f5d4;
|
|
--accent-orange: #ff6b35;
|
|
--accent-purple: #b185db;
|
|
--accent-red: #ef476f;
|
|
--accent-yellow: #ffd60a;
|
|
--text-primary: #ffffff;
|
|
--text-secondary: #e0e0e0;
|
|
--text-muted: #a8a8a8;
|
|
--border-subtle: rgba(255,255,255,0.3);
|
|
--glow-blue: rgba(0,180,216,0.4);
|
|
--glow-cyan: rgba(0,245,212,0.3);""",
|
|
'css_overrides': """
|
|
/* Pixel art: remove ALL rounded corners */
|
|
*,
|
|
*::before,
|
|
*::after,
|
|
.container,
|
|
.intro-section,
|
|
.sidebar-nav,
|
|
.sidebar-nav a,
|
|
.command-card,
|
|
.command-keyword,
|
|
.command-usage,
|
|
.channel-section,
|
|
.channel-card,
|
|
.mobile-menu-toggle,
|
|
button,
|
|
input {
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
/* Chunky pixel borders */
|
|
.command-card {
|
|
border: 3px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.intro-section {
|
|
border: 3px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.sidebar-nav {
|
|
border: 3px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.command-keyword {
|
|
border: 2px solid currentColor !important;
|
|
}
|
|
|
|
.command-usage {
|
|
border: 2px solid var(--accent-cyan) !important;
|
|
}
|
|
|
|
.channel-section {
|
|
border: 3px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
/* Pixel font sizing adjustments */
|
|
body {
|
|
font-family: 'Press Start 2P', cursive !important;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.5rem !important;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 1.2rem !important;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 1rem !important;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.command-name {
|
|
font-size: 1.1rem !important;
|
|
}
|
|
|
|
p, .command-description, .command-usage {
|
|
font-size: 0.75rem !important;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.command-keyword {
|
|
font-size: 0.65rem !important;
|
|
padding: 4px 8px;
|
|
}
|
|
|
|
.sidebar-nav a {
|
|
font-size: 0.7rem !important;
|
|
}
|
|
|
|
/* Retro box shadow - hard edges */
|
|
.command-card {
|
|
box-shadow: 4px 4px 0 var(--border-subtle) !important;
|
|
}
|
|
|
|
.command-card:hover {
|
|
box-shadow: 8px 8px 0 var(--accent-cyan) !important;
|
|
transform: translate(-2px, -2px) !important;
|
|
}
|
|
|
|
.intro-section {
|
|
box-shadow: 4px 4px 0 var(--border-subtle) !important;
|
|
}
|
|
|
|
/* Remove smooth transitions for pixel feel */
|
|
* {
|
|
transition: none !important;
|
|
}
|
|
"""
|
|
}
|
|
}
|
|
|
|
|
|
def setup_logging():
|
|
"""Setup basic logging"""
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(levelname)s: %(message)s'
|
|
)
|
|
return logging.getLogger(__name__)
|
|
|
|
|
|
def read_config(config_file: str = "config.ini") -> configparser.ConfigParser:
|
|
"""Read and parse config.ini file"""
|
|
config = configparser.ConfigParser()
|
|
if not os.path.exists(config_file):
|
|
raise FileNotFoundError(f"Config file not found: {config_file}")
|
|
config.read(config_file)
|
|
return config
|
|
|
|
|
|
def get_bot_name(config: configparser.ConfigParser) -> str:
|
|
"""Extract bot name from config"""
|
|
return config.get('Bot', 'bot_name', fallback='MeshCore Bot')
|
|
|
|
|
|
def get_admin_commands(config: configparser.ConfigParser) -> list[str]:
|
|
"""Extract admin commands from config"""
|
|
if not config.has_section('Admin_ACL'):
|
|
return []
|
|
admin_commands_str = config.get('Admin_ACL', 'admin_commands', fallback='')
|
|
if not admin_commands_str:
|
|
return []
|
|
return [cmd.strip() for cmd in admin_commands_str.split(',') if cmd.strip()]
|
|
|
|
|
|
def get_website_intro(config: configparser.ConfigParser) -> str:
|
|
"""Get custom introduction text from config, or use default"""
|
|
if config.has_section('Website'):
|
|
intro = config.get('Website', 'introduction_text', fallback='')
|
|
if intro:
|
|
return intro
|
|
|
|
# Default introduction (first person from bot's perspective)
|
|
bot_name = get_bot_name(config)
|
|
return f"Hi, I'm {bot_name}! I provide various commands to help you interact with the mesh network. Use the commands below to get started."
|
|
|
|
|
|
def get_website_title(config: configparser.ConfigParser) -> str:
|
|
"""Get website title from config, or use default"""
|
|
if config.has_section('Website'):
|
|
title = config.get('Website', 'website_title', fallback='')
|
|
if title:
|
|
return title
|
|
|
|
bot_name = get_bot_name(config)
|
|
return f"{bot_name} - Command Reference"
|
|
|
|
|
|
class WebsiteRandomLineCommand:
|
|
"""Command-like object used to render RandomLine entries on the website."""
|
|
|
|
def __init__(
|
|
self,
|
|
key: str,
|
|
triggers: list[str],
|
|
category: str,
|
|
usage: str,
|
|
description: str,
|
|
allowed_channels: Optional[list[str]] = None
|
|
):
|
|
self.name = key
|
|
self.keywords = triggers
|
|
self.category = category
|
|
self.description = description
|
|
self.allowed_channels = allowed_channels
|
|
self._usage = usage
|
|
|
|
def get_usage_info(self) -> dict[str, Any]:
|
|
return {
|
|
'usage': self._usage,
|
|
'short_description': self.description,
|
|
'description': self.description,
|
|
'examples': [],
|
|
'parameters': [],
|
|
'subcommands': [],
|
|
}
|
|
|
|
|
|
def normalize_category_name(category_name: str) -> str:
|
|
"""Normalize category names to lowercase underscore style."""
|
|
return category_name.strip().lower().replace('-', '_').replace(' ', '_')
|
|
|
|
|
|
def get_randomline_commands(config: configparser.ConfigParser) -> dict[str, Any]:
|
|
"""Build website command entries from [RandomLine] triggers."""
|
|
randomline_commands: dict[str, Any] = {}
|
|
if not config.has_section('RandomLine'):
|
|
return randomline_commands
|
|
|
|
command_prefix = config.get('Bot', 'command_prefix', fallback='').strip()
|
|
|
|
for option, value in config.items('RandomLine'):
|
|
if not option.startswith('triggers.'):
|
|
continue
|
|
|
|
key = option.split('.', 1)[1].strip()
|
|
if not key:
|
|
continue
|
|
|
|
triggers = [trigger.strip() for trigger in value.split(',') if trigger.strip()]
|
|
if not triggers:
|
|
continue
|
|
|
|
category_override = config.get('RandomLine', f'category.{key}', fallback='').strip()
|
|
category = normalize_category_name(category_override) if category_override else 'fun'
|
|
|
|
display_trigger = triggers[0]
|
|
usage = f"{command_prefix}{display_trigger}" if command_prefix else display_trigger
|
|
|
|
channel_opt = config.get('RandomLine', f'channel.{key}', fallback='').strip()
|
|
if not channel_opt:
|
|
channel_opt = config.get('RandomLine', f'channels.{key}', fallback='').strip()
|
|
allowed_channels = [ch.strip() for ch in channel_opt.split(',') if ch.strip()] if channel_opt else None
|
|
|
|
description = "Returns a random line from a configured text list."
|
|
randomline_commands[f"randomline.{key}"] = WebsiteRandomLineCommand(
|
|
key=key,
|
|
triggers=triggers,
|
|
category=category,
|
|
usage=usage,
|
|
description=description,
|
|
allowed_channels=allowed_channels,
|
|
)
|
|
|
|
return randomline_commands
|
|
|
|
|
|
def load_channels_from_config(config: configparser.ConfigParser) -> dict[str, dict[str, str]]:
|
|
"""Load channels from Channels_List section, grouped by category
|
|
|
|
Returns:
|
|
Dict with structure: {
|
|
'general': {'#channel': 'description', ...},
|
|
'category': {'#channel': 'description', ...},
|
|
...
|
|
}
|
|
"""
|
|
channels = {'general': {}}
|
|
|
|
if not config.has_section('Channels_List'):
|
|
return channels
|
|
|
|
for key, description in config.items('Channels_List'):
|
|
key = key.strip()
|
|
description = description.strip()
|
|
|
|
if not key or not description:
|
|
continue
|
|
|
|
# Check if it's a categorized channel (has dot notation)
|
|
if '.' in key:
|
|
category = key.split('.')[0]
|
|
channel_name = key.split('.', 1)[1]
|
|
|
|
if category not in channels:
|
|
channels[category] = {}
|
|
|
|
# Add # prefix if not present
|
|
display_name = channel_name if channel_name.startswith('#') else f"#{channel_name}"
|
|
channels[category][display_name] = description
|
|
else:
|
|
# General channel (no category)
|
|
display_name = key if key.startswith('#') else f"#{key}"
|
|
channels['general'][display_name] = description
|
|
|
|
# Remove empty categories
|
|
return {cat: chans for cat, chans in channels.items() if chans}
|
|
|
|
|
|
def get_database_path(config: configparser.ConfigParser, bot_root: str) -> Optional[str]:
|
|
"""Get database path from config"""
|
|
db_path = config.get('Bot', 'db_path', fallback='meshcore_bot.db')
|
|
if db_path:
|
|
return resolve_path(db_path, bot_root)
|
|
return None
|
|
|
|
|
|
def get_command_popularity(db_path: Optional[str], commands: dict[str, Any]) -> dict[str, int]:
|
|
"""Get command usage counts from database"""
|
|
popularity = defaultdict(int)
|
|
|
|
if not db_path or not os.path.exists(db_path):
|
|
return popularity
|
|
|
|
try:
|
|
with closing(sqlite3.connect(db_path)) as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Check if command_stats table exists
|
|
cursor.execute("""
|
|
SELECT name FROM sqlite_master
|
|
WHERE type='table' AND name='command_stats'
|
|
""")
|
|
if not cursor.fetchone():
|
|
return popularity
|
|
|
|
# Query command usage
|
|
cursor.execute("""
|
|
SELECT command_name, COUNT(*) as count
|
|
FROM command_stats
|
|
GROUP BY command_name
|
|
""")
|
|
|
|
for row in cursor.fetchall():
|
|
command_name = row[0]
|
|
count = row[1]
|
|
|
|
# Map to primary command name if it's a keyword/alias
|
|
primary_name = command_name
|
|
for cmd_name, cmd_instance in commands.items():
|
|
if hasattr(cmd_instance, 'keywords') and command_name.lower() in [k.lower() for k in cmd_instance.keywords]:
|
|
primary_name = cmd_instance.name if hasattr(cmd_instance, 'name') else cmd_name
|
|
break
|
|
if hasattr(cmd_instance, 'name') and cmd_instance.name == command_name:
|
|
primary_name = cmd_instance.name
|
|
break
|
|
|
|
popularity[primary_name] += count
|
|
except Exception as e:
|
|
logging.warning(f"Could not query command popularity: {e}")
|
|
|
|
return popularity
|
|
|
|
|
|
def filter_commands(commands: dict[str, Any], admin_commands: list[str]) -> dict[str, Any]:
|
|
"""Filter out admin and hidden commands"""
|
|
filtered = {}
|
|
|
|
# Categories to exclude from public reference
|
|
excluded_categories = {'hidden', 'admin', 'system', 'management', 'special'}
|
|
|
|
for cmd_name, cmd_instance in commands.items():
|
|
# Skip commands in excluded categories
|
|
if hasattr(cmd_instance, 'category') and cmd_instance.category in excluded_categories:
|
|
continue
|
|
|
|
# Get primary command name
|
|
primary_name = cmd_instance.name if hasattr(cmd_instance, 'name') else cmd_name
|
|
|
|
# Skip admin commands by name (check both dict key and primary name)
|
|
if cmd_name in admin_commands or primary_name in admin_commands:
|
|
continue
|
|
|
|
# Skip commands that require admin access
|
|
if hasattr(cmd_instance, 'requires_admin_access') and cmd_instance.requires_admin_access():
|
|
continue
|
|
|
|
# Skip commands with no keywords (automatic/system commands)
|
|
if hasattr(cmd_instance, 'keywords') and not cmd_instance.keywords:
|
|
continue
|
|
|
|
filtered[cmd_name] = cmd_instance
|
|
|
|
return filtered
|
|
|
|
|
|
def get_default_command_order() -> list[str]:
|
|
"""Get default command ordering when no stats available"""
|
|
return [
|
|
'help', 'ping', 'test', # Basic
|
|
'wx', 'aqi', # Weather
|
|
'sun', 'moon', 'solar', # Solar
|
|
'sports', 'stats', # Popular features
|
|
]
|
|
|
|
|
|
def sort_commands_by_popularity(commands: dict[str, Any], popularity: dict[str, int]) -> list[tuple[str, Any]]:
|
|
"""Sort commands by popularity, with fallback to default order"""
|
|
default_order = get_default_command_order()
|
|
|
|
# Create list of (name, instance, priority) tuples
|
|
command_list = []
|
|
for cmd_name, cmd_instance in commands.items():
|
|
primary_name = cmd_instance.name if hasattr(cmd_instance, 'name') else cmd_name
|
|
|
|
# Get popularity count
|
|
count = popularity.get(primary_name, 0)
|
|
|
|
# Get default priority (lower is better)
|
|
try:
|
|
default_priority = default_order.index(primary_name)
|
|
except ValueError:
|
|
default_priority = 999 # Not in default order
|
|
|
|
# Priority: popularity count (descending), then default order, then alphabetical
|
|
priority = (-count, default_priority, primary_name.lower())
|
|
command_list.append((priority, cmd_name, cmd_instance))
|
|
|
|
# Sort by priority
|
|
command_list.sort(key=lambda x: x[0])
|
|
|
|
# Return (name, instance) tuples
|
|
return [(name, instance) for _, name, instance in command_list]
|
|
|
|
|
|
def escape_html(text: str) -> str:
|
|
"""Escape HTML special characters"""
|
|
return html.escape(str(text))
|
|
|
|
|
|
def get_channel_info(cmd_instance: Any, monitor_channels: list[str]) -> Optional[str]:
|
|
"""Get channel restriction information for a command"""
|
|
allowed_channels = getattr(cmd_instance, 'allowed_channels', None)
|
|
requires_dm = getattr(cmd_instance, 'requires_dm', False)
|
|
|
|
# If command has specific allowed channels configured
|
|
if allowed_channels is not None:
|
|
if allowed_channels == []:
|
|
# Empty list means DM only (channels explicitly disabled)
|
|
return "DM only"
|
|
elif len(allowed_channels) > 0:
|
|
# Format channel names (add # if not present)
|
|
formatted_channels = []
|
|
for ch in allowed_channels:
|
|
ch = ch.strip()
|
|
if not ch.startswith('#'):
|
|
formatted_channels.append(f"#{ch}")
|
|
else:
|
|
formatted_channels.append(ch)
|
|
if len(formatted_channels) == 1:
|
|
return f"Channel: {', '.join(formatted_channels)}"
|
|
else:
|
|
return f"Channels: {', '.join(formatted_channels)}"
|
|
|
|
# If command requires DM and has no channel override, it's DM only
|
|
if requires_dm:
|
|
return "DM only"
|
|
|
|
# No restrictions - works in all monitored channels (don't show anything)
|
|
return None
|
|
|
|
|
|
def format_monitor_channels(monitor_channels: list[str], html: bool = False) -> str:
|
|
"""Format monitor channels for display
|
|
|
|
Args:
|
|
monitor_channels: List of channel names
|
|
html: If True, wrap channel names in span tags for highlighting
|
|
"""
|
|
if not monitor_channels:
|
|
return ""
|
|
|
|
formatted = []
|
|
for ch in monitor_channels:
|
|
ch = ch.strip()
|
|
if not ch.startswith('#'):
|
|
channel_name = f"#{ch}"
|
|
else:
|
|
channel_name = ch
|
|
|
|
if html:
|
|
# Wrap in span for inline highlighting
|
|
formatted.append(f'<span class="channel-highlight">{escape_html(channel_name)}</span>')
|
|
else:
|
|
formatted.append(channel_name)
|
|
|
|
if len(formatted) == 1:
|
|
return formatted[0]
|
|
elif len(formatted) == 2:
|
|
return f"{formatted[0]} or {formatted[1]}"
|
|
else:
|
|
return ", ".join(formatted[:-1]) + f", or {formatted[-1]}"
|
|
|
|
|
|
def generate_html(bot_name: str, title: str, introduction: str, commands: list[tuple[str, Any]], monitor_channels: list[str] = None, channels_data: dict[str, dict[str, str]] = None, style: str = 'default') -> str:
|
|
"""Generate the HTML content"""
|
|
|
|
if monitor_channels is None:
|
|
monitor_channels = []
|
|
|
|
if channels_data is None:
|
|
channels_data = {}
|
|
|
|
# Format monitor channels message to append to introduction
|
|
channels_suffix = ""
|
|
if monitor_channels:
|
|
formatted_channels = format_monitor_channels(monitor_channels, html=True)
|
|
channels_suffix = f" I'll answer you if you send a message in {formatted_channels}."
|
|
|
|
# Group commands by category
|
|
categories = defaultdict(list)
|
|
for cmd_name, cmd_instance in commands:
|
|
category = getattr(cmd_instance, 'category', 'general')
|
|
categories[category].append((cmd_name, cmd_instance))
|
|
|
|
# Category display names
|
|
category_names = {
|
|
'basic': 'Basic Commands',
|
|
'weather': 'Weather Commands',
|
|
'solar': 'Solar & Astronomical',
|
|
'sports': 'Sports',
|
|
'games': 'Games & Entertainment',
|
|
'fun': 'Fun Commands',
|
|
'entertainment': 'Entertainment',
|
|
'meshcore_info': 'Mesh Network Info',
|
|
'analytics': 'Analytics',
|
|
'emergency': 'Emergency',
|
|
'special': 'Special Commands',
|
|
'general': 'General Commands',
|
|
}
|
|
|
|
# Build navigation sidebar items
|
|
nav_items = []
|
|
|
|
# Add command categories to nav (basic first, then alphabetically)
|
|
sorted_categories = sorted(categories.keys())
|
|
if 'basic' in sorted_categories:
|
|
sorted_categories.remove('basic')
|
|
sorted_categories.insert(0, 'basic')
|
|
|
|
for category in sorted_categories:
|
|
category_display = category_names.get(category, category.title().replace('_', ' '))
|
|
category_id = category.lower().replace('_', '-').replace(' ', '-')
|
|
nav_items.append(('commands', category_display, f'commands-{category_id}'))
|
|
|
|
# Add channels section to nav if available
|
|
if channels_data:
|
|
nav_items.append(('channels', 'Available Channels', 'channels'))
|
|
|
|
# Add channel subcategories
|
|
sorted_channel_categories = ['general'] + sorted([c for c in channels_data if c != 'general'])
|
|
for category in sorted_channel_categories:
|
|
if category not in channels_data or not channels_data[category]:
|
|
continue
|
|
category_display = category.title().replace('_', ' ') if category != 'general' else 'General Channels'
|
|
category_id = category.lower().replace('_', '-').replace(' ', '-')
|
|
nav_items.append(('channels-sub', category_display, f'channels-{category_id}'))
|
|
|
|
# Build navigation sidebar HTML
|
|
nav_html = '<nav class="sidebar-nav">\n'
|
|
nav_html += ' <div class="nav-header">\n'
|
|
nav_html += ' <h3>Navigation</h3>\n'
|
|
nav_html += ' </div>\n'
|
|
nav_html += ' <ul class="nav-list">\n'
|
|
|
|
# Add Commands section
|
|
if nav_items:
|
|
nav_html += ' <li class="nav-section-header">Commands</li>\n'
|
|
nav_html += ' <ul class="nav-sublist">\n'
|
|
|
|
for nav_type, display_name, anchor_id in nav_items:
|
|
if nav_type == 'commands':
|
|
nav_html += f' <li><a href="#{anchor_id}" class="nav-link nav-sublink">{escape_html(display_name)}</a></li>\n'
|
|
elif nav_type == 'channels':
|
|
# Close commands section and start channels
|
|
nav_html += ' </ul>\n'
|
|
nav_html += ' <li class="nav-section-header">Channels</li>\n'
|
|
nav_html += ' <ul class="nav-sublist">\n'
|
|
nav_html += f' <li><a href="#{anchor_id}" class="nav-link nav-sublink">{escape_html(display_name)}</a></li>\n'
|
|
elif nav_type == 'channels-sub':
|
|
nav_html += f' <li><a href="#{anchor_id}" class="nav-link nav-sublink">{escape_html(display_name)}</a></li>\n'
|
|
|
|
nav_html += ' </ul>\n'
|
|
|
|
nav_html += ' </ul>\n'
|
|
nav_html += '</nav>\n'
|
|
|
|
# Build command HTML (basic first, then alphabetically)
|
|
commands_html = ""
|
|
sorted_command_categories = sorted(categories.keys())
|
|
if 'basic' in sorted_command_categories:
|
|
sorted_command_categories.remove('basic')
|
|
sorted_command_categories.insert(0, 'basic')
|
|
|
|
for category in sorted_command_categories:
|
|
category_commands = categories[category]
|
|
category_display = category_names.get(category, category.title().replace('_', ' '))
|
|
# Create anchor ID from category (lowercase, replace spaces with hyphens)
|
|
category_id = category.lower().replace('_', '-').replace(' ', '-')
|
|
|
|
commands_html += f'<div class="category-section" id="commands-{category_id}">\n'
|
|
commands_html += f' <h2 class="category-title"><a href="#commands-{category_id}" class="anchor-link">{escape_html(category_display)}</a></h2>\n'
|
|
commands_html += ' <div class="commands-grid">\n'
|
|
|
|
for cmd_name, cmd_instance in category_commands:
|
|
primary_name = cmd_instance.name if hasattr(cmd_instance, 'name') else cmd_name
|
|
keywords = getattr(cmd_instance, 'keywords', [])
|
|
|
|
# Filter out the primary name from keywords to avoid duplication
|
|
aliases = [k for k in keywords if k.lower() != primary_name.lower()]
|
|
|
|
# Get channel restriction info
|
|
channel_info = get_channel_info(cmd_instance, monitor_channels)
|
|
|
|
# Get usage information including usage syntax, examples, parameters, and sub-commands
|
|
try:
|
|
usage_info = cmd_instance.get_usage_info()
|
|
usage_syntax = usage_info.get('usage', '')
|
|
usage_info.get('examples', [])
|
|
parameters = usage_info.get('parameters', [])
|
|
subcommands = usage_info.get('subcommands', [])
|
|
# Use short_description for website if available, otherwise fallback to description
|
|
short_desc = usage_info.get('short_description', '')
|
|
description = short_desc if short_desc else usage_info.get('description', 'No description available')
|
|
except Exception:
|
|
usage_syntax = ''
|
|
parameters = []
|
|
subcommands = []
|
|
description = getattr(cmd_instance, 'description', 'No description available')
|
|
|
|
commands_html += ' <div class="command-card">\n'
|
|
commands_html += ' <div class="command-header">\n'
|
|
commands_html += f' <h3 class="command-name">{escape_html(primary_name)}</h3>\n'
|
|
if aliases:
|
|
commands_html += ' <div class="command-keywords">\n'
|
|
# Show first 5 aliases
|
|
visible_aliases = aliases[:5]
|
|
hidden_aliases = aliases[5:]
|
|
|
|
for alias in visible_aliases:
|
|
commands_html += f' <span class="keyword-badge">{escape_html(alias)}</span>\n'
|
|
|
|
if hidden_aliases:
|
|
# Store hidden aliases in data attribute and create expandable badge
|
|
hidden_aliases_json = escape_html(','.join(hidden_aliases))
|
|
commands_html += f' <span class="keyword-badge keyword-expand" data-hidden="{hidden_aliases_json}" data-command="{escape_html(primary_name)}">+{len(hidden_aliases)} more</span>\n'
|
|
|
|
commands_html += ' </div>\n'
|
|
commands_html += ' </div>\n'
|
|
commands_html += f' <p class="command-description">{escape_html(description)}</p>\n'
|
|
|
|
# Render usage, examples, parameters, subcommands
|
|
try:
|
|
|
|
# Render usage syntax
|
|
if usage_syntax:
|
|
commands_html += f' <div class="command-usage"><code>{escape_html(usage_syntax)}</code></div>\n'
|
|
|
|
# Render parameters
|
|
if parameters:
|
|
commands_html += ' <div class="command-params">\n'
|
|
commands_html += ' <div class="params-header">Parameters:</div>\n'
|
|
for param in parameters:
|
|
param_name = param.get('name', '')
|
|
param_desc = param.get('description', '')
|
|
if param_name and param_desc:
|
|
commands_html += ' <div class="param-item">\n'
|
|
commands_html += f' <span class="param-name">{escape_html(param_name)}</span>\n'
|
|
commands_html += f' <span class="param-desc">{escape_html(param_desc)}</span>\n'
|
|
commands_html += ' </div>\n'
|
|
commands_html += ' </div>\n'
|
|
|
|
# Render subcommands
|
|
if subcommands:
|
|
commands_html += ' <div class="command-subcommands">\n'
|
|
commands_html += ' <div class="subcommands-header">Sub-commands:</div>\n'
|
|
for subcmd in subcommands:
|
|
subcmd_name = subcmd.get('name', '')
|
|
subcmd_desc = subcmd.get('description', '')
|
|
if subcmd_name and subcmd_desc:
|
|
commands_html += ' <div class="subcommand-item">\n'
|
|
commands_html += f' <span class="subcommand-name">{escape_html(subcmd_name)}</span>\n'
|
|
commands_html += f' <span class="subcommand-desc">{escape_html(subcmd_desc)}</span>\n'
|
|
commands_html += ' </div>\n'
|
|
commands_html += ' </div>\n'
|
|
except Exception:
|
|
# Silently fail if get_usage_info() is not available or fails
|
|
pass
|
|
|
|
if channel_info:
|
|
commands_html += f' <div class="command-channels">{escape_html(channel_info)}</div>\n'
|
|
commands_html += ' </div>\n'
|
|
|
|
commands_html += ' </div>\n'
|
|
commands_html += '</div>\n'
|
|
|
|
# Build channels HTML if channels are available
|
|
channels_html = ""
|
|
if channels_data:
|
|
channels_html += '<div class="category-section" id="channels">\n'
|
|
channels_html += ' <h2 class="category-title"><a href="#channels" class="anchor-link">Available Channels</a></h2>\n'
|
|
channels_html += ' <p class="channels-intro">These are semi-public channels that are in use on our local mesh! Join them to connect with others with common interests! <br/> To add these channels to your client, click on the three dot menu and select "Add Channel".</p>\n'
|
|
|
|
# Sort categories: general first, then alphabetically
|
|
sorted_categories = ['general'] + sorted([c for c in channels_data if c != 'general'])
|
|
|
|
for category in sorted_categories:
|
|
if category not in channels_data or not channels_data[category]:
|
|
continue
|
|
|
|
category_display = category.title().replace('_', ' ') if category != 'general' else 'General Channels'
|
|
category_id = category.lower().replace('_', '-').replace(' ', '-')
|
|
channels_html += f' <div class="channel-category" id="channels-{category_id}">\n'
|
|
channels_html += f' <h3 class="channel-category-title"><a href="#channels-{category_id}" class="anchor-link">{escape_html(category_display)}</a></h3>\n'
|
|
channels_html += ' <div class="channels-grid">\n'
|
|
|
|
# Sort channels alphabetically
|
|
sorted_channels = sorted(channels_data[category].items())
|
|
for channel_name, description in sorted_channels:
|
|
channels_html += ' <div class="channel-card">\n'
|
|
channels_html += f' <div class="channel-name">{escape_html(channel_name)}</div>\n'
|
|
channels_html += f' <div class="channel-description">{escape_html(description)}</div>\n'
|
|
channels_html += ' </div>\n'
|
|
|
|
channels_html += ' </div>\n'
|
|
channels_html += ' </div>\n'
|
|
|
|
channels_html += '</div>\n'
|
|
|
|
html_content = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{escape_html(title)}</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="{STYLES[style]['fonts_url']}" rel="stylesheet">
|
|
<style>
|
|
:root {{
|
|
{STYLES[style]['css_vars']}
|
|
}}
|
|
|
|
* {{
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}}
|
|
|
|
html {{
|
|
scroll-behavior: smooth;
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Outfit', sans-serif;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
line-height: 1.6;
|
|
}}
|
|
|
|
/* Atmospheric background */
|
|
.atmosphere {{
|
|
position: fixed;
|
|
inset: 0;
|
|
background:
|
|
radial-gradient(ellipse 80% 50% at 20% 0%, rgba(0, 212, 255, 0.08) 0%, transparent 50%),
|
|
radial-gradient(ellipse 60% 40% at 80% 100%, rgba(0, 255, 200, 0.05) 0%, transparent 50%),
|
|
radial-gradient(ellipse 100% 100% at 50% 50%, var(--bg-primary) 0%, #060a0f 100%);
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
}}
|
|
|
|
/* Subtle grid overlay */
|
|
.grid-overlay {{
|
|
position: fixed;
|
|
inset: 0;
|
|
background-image:
|
|
linear-gradient(rgba(255,255,255,0.01) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(255,255,255,0.01) 1px, transparent 1px);
|
|
background-size: 60px 60px;
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
}}
|
|
|
|
.container {{
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
padding: 3rem 2rem;
|
|
min-height: 100vh;
|
|
position: relative;
|
|
z-index: 1;
|
|
display: grid;
|
|
grid-template-columns: 280px 1fr;
|
|
gap: 3rem;
|
|
}}
|
|
|
|
.sidebar-nav {{
|
|
position: sticky;
|
|
top: 2rem;
|
|
height: fit-content;
|
|
max-height: calc(100vh - 4rem);
|
|
overflow-y: auto;
|
|
background: var(--bg-card);
|
|
border-radius: 16px;
|
|
border: 1px solid var(--border-subtle);
|
|
padding: 1.5rem;
|
|
z-index: 100;
|
|
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
opacity: 1;
|
|
filter: none;
|
|
}}
|
|
|
|
.mobile-menu-toggle {{
|
|
display: none;
|
|
position: fixed;
|
|
top: 1.5rem;
|
|
right: 1.5rem;
|
|
z-index: 1001;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: 12px;
|
|
padding: 0.75rem;
|
|
cursor: pointer;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 48px;
|
|
height: 48px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
transition: all 0.3s ease;
|
|
}}
|
|
|
|
.mobile-menu-toggle:hover {{
|
|
background: var(--bg-card-hover);
|
|
border-color: var(--accent-cyan);
|
|
}}
|
|
|
|
.mobile-menu-toggle.active {{
|
|
background: var(--bg-card-hover);
|
|
border-color: var(--accent-cyan);
|
|
}}
|
|
|
|
.hamburger {{
|
|
width: 24px;
|
|
height: 2px;
|
|
background: var(--accent-cyan);
|
|
border-radius: 2px;
|
|
transition: all 0.3s ease;
|
|
}}
|
|
|
|
.mobile-menu-toggle.active .hamburger:nth-child(1) {{
|
|
transform: rotate(45deg) translate(7px, 7px);
|
|
}}
|
|
|
|
.mobile-menu-toggle.active .hamburger:nth-child(2) {{
|
|
opacity: 0;
|
|
}}
|
|
|
|
.mobile-menu-toggle.active .hamburger:nth-child(3) {{
|
|
transform: rotate(-45deg) translate(7px, -7px);
|
|
}}
|
|
|
|
.sidebar-overlay {{
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
z-index: 1000;
|
|
backdrop-filter: none;
|
|
-webkit-backdrop-filter: none;
|
|
pointer-events: none;
|
|
}}
|
|
|
|
.sidebar-overlay.visible {{
|
|
pointer-events: auto;
|
|
}}
|
|
|
|
@media (max-width: 1200px) {{
|
|
.sidebar-overlay.visible {{
|
|
/* Don't cover the sidebar area - start overlay after sidebar width */
|
|
left: 280px !important;
|
|
}}
|
|
}}
|
|
|
|
@media (min-width: 1201px) {{
|
|
.sidebar-overlay {{
|
|
backdrop-filter: blur(4px);
|
|
-webkit-backdrop-filter: blur(4px);
|
|
}}
|
|
}}
|
|
|
|
@media (max-width: 1200px) {{
|
|
.sidebar-overlay {{
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
z-index: 1000 !important;
|
|
}}
|
|
|
|
.sidebar-overlay.visible {{
|
|
pointer-events: auto !important;
|
|
}}
|
|
|
|
/* Ensure sidebar is always clickable above overlay */
|
|
.sidebar-nav {{
|
|
pointer-events: auto !important;
|
|
}}
|
|
|
|
.sidebar-nav * {{
|
|
pointer-events: auto !important;
|
|
}}
|
|
}}
|
|
|
|
.sidebar-overlay.visible {{
|
|
display: block;
|
|
}}
|
|
|
|
.sidebar-nav::-webkit-scrollbar {{
|
|
width: 6px;
|
|
}}
|
|
|
|
.sidebar-nav::-webkit-scrollbar-track {{
|
|
background: var(--bg-secondary);
|
|
border-radius: 3px;
|
|
}}
|
|
|
|
.sidebar-nav::-webkit-scrollbar-thumb {{
|
|
background: var(--border-subtle);
|
|
border-radius: 3px;
|
|
}}
|
|
|
|
.sidebar-nav::-webkit-scrollbar-thumb:hover {{
|
|
background: rgba(255,255,255,0.15);
|
|
}}
|
|
|
|
.nav-header {{
|
|
margin-bottom: 1.5rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
}}
|
|
|
|
.nav-header h3 {{
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
font-size: 0.85rem;
|
|
}}
|
|
|
|
.nav-list {{
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}}
|
|
|
|
.nav-section-header {{
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
color: var(--text-muted);
|
|
font-weight: 600;
|
|
margin-top: 1.5rem;
|
|
margin-bottom: 0.75rem;
|
|
padding-left: 0.5rem;
|
|
}}
|
|
|
|
.nav-section-header:first-child {{
|
|
margin-top: 0;
|
|
}}
|
|
|
|
.nav-sublist {{
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0 0 1rem 0;
|
|
padding-left: 0.5rem;
|
|
}}
|
|
|
|
.nav-link {{
|
|
display: block;
|
|
padding: 0.6rem 0.75rem;
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
border-radius: 8px;
|
|
font-size: 0.9rem;
|
|
transition: all 0.2s ease;
|
|
margin-bottom: 0.25rem;
|
|
cursor: pointer;
|
|
-webkit-tap-highlight-color: rgba(0, 255, 200, 0.2);
|
|
}}
|
|
|
|
.nav-link:hover {{
|
|
background: var(--bg-card-hover);
|
|
color: var(--accent-cyan);
|
|
transform: translateX(4px);
|
|
}}
|
|
|
|
.nav-link:active,
|
|
.nav-link.active {{
|
|
background: rgba(0, 255, 200, 0.1);
|
|
color: var(--accent-cyan);
|
|
border-left: 3px solid var(--accent-cyan);
|
|
padding-left: calc(0.75rem - 3px);
|
|
}}
|
|
|
|
.nav-sublink {{
|
|
padding-left: 1.25rem;
|
|
font-size: 0.85rem;
|
|
color: var(--text-muted);
|
|
}}
|
|
|
|
.nav-sublink:hover {{
|
|
color: var(--accent-blue);
|
|
}}
|
|
|
|
.main-content {{
|
|
min-width: 0;
|
|
}}
|
|
|
|
header {{
|
|
margin-bottom: 4rem;
|
|
padding: 0;
|
|
}}
|
|
|
|
.header-content {{
|
|
background: var(--bg-card);
|
|
border-radius: 20px;
|
|
border: 1px solid var(--border-subtle);
|
|
padding: 3rem 2.5rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.header-content::before {{
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, var(--accent-blue), var(--accent-cyan));
|
|
}}
|
|
|
|
.header-title {{
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}}
|
|
|
|
.logo-icon {{
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 3.5rem;
|
|
flex-shrink: 0;
|
|
line-height: 1;
|
|
}}
|
|
|
|
h1 {{
|
|
font-size: 3rem;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
color: var(--accent-cyan);
|
|
letter-spacing: -0.02em;
|
|
font-family: 'Outfit', sans-serif;
|
|
}}
|
|
|
|
.intro {{
|
|
font-size: 1.1rem;
|
|
color: var(--text-secondary);
|
|
max-width: 900px;
|
|
line-height: 1.8;
|
|
margin-top: 0.5rem;
|
|
}}
|
|
|
|
.channel-highlight {{
|
|
color: var(--accent-cyan);
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.category-section {{
|
|
margin-bottom: 3rem;
|
|
}}
|
|
|
|
.category-title {{
|
|
font-size: 1.75rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1.5rem;
|
|
color: var(--text-primary);
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
letter-spacing: -0.01em;
|
|
}}
|
|
|
|
.category-title .anchor-link,
|
|
.channel-category-title .anchor-link {{
|
|
color: inherit;
|
|
text-decoration: none;
|
|
position: relative;
|
|
display: inline-block;
|
|
}}
|
|
|
|
.category-title .anchor-link:hover,
|
|
.channel-category-title .anchor-link:hover {{
|
|
color: var(--accent-cyan);
|
|
}}
|
|
|
|
.category-title .anchor-link::before {{
|
|
content: '#';
|
|
position: absolute;
|
|
left: -1.5rem;
|
|
opacity: 0;
|
|
color: var(--accent-blue);
|
|
font-weight: 400;
|
|
transition: opacity 0.2s ease;
|
|
}}
|
|
|
|
.category-title:hover .anchor-link::before,
|
|
.channel-category-title:hover .anchor-link::before {{
|
|
opacity: 0.5;
|
|
}}
|
|
|
|
.category-section,
|
|
.channel-category {{
|
|
scroll-margin-top: 2rem;
|
|
}}
|
|
|
|
.commands-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 1.25rem;
|
|
margin-bottom: 2rem;
|
|
}}
|
|
|
|
.command-card {{
|
|
background: var(--bg-card);
|
|
border-radius: 16px;
|
|
padding: 1.5rem;
|
|
border: 1px solid var(--border-subtle);
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.command-card::before {{
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, var(--accent-blue), transparent);
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}}
|
|
|
|
.command-card:hover {{
|
|
background: var(--bg-card-hover);
|
|
border-color: rgba(255,255,255,0.1);
|
|
transform: translateY(-2px);
|
|
}}
|
|
|
|
.command-card:hover::before {{
|
|
opacity: 1;
|
|
}}
|
|
|
|
.command-header {{
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
margin-bottom: 1rem;
|
|
}}
|
|
|
|
.command-name {{
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
color: var(--accent-blue);
|
|
margin: 0;
|
|
letter-spacing: -0.01em;
|
|
}}
|
|
|
|
.command-keywords {{
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}}
|
|
|
|
.keyword-badge {{
|
|
background: rgba(0, 212, 255, 0.1);
|
|
color: var(--accent-cyan);
|
|
padding: 0.35rem 0.75rem;
|
|
border-radius: 8px;
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
border: 1px solid rgba(0, 212, 255, 0.2);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}}
|
|
|
|
.keyword-expand {{
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}}
|
|
|
|
.keyword-expand:hover {{
|
|
background: rgba(0, 212, 255, 0.2);
|
|
border-color: var(--accent-cyan);
|
|
transform: scale(1.05);
|
|
}}
|
|
|
|
.keyword-expand.expanded {{
|
|
display: none;
|
|
}}
|
|
|
|
.keyword-hidden {{
|
|
display: none;
|
|
}}
|
|
|
|
.keyword-hidden.visible {{
|
|
display: inline-block;
|
|
}}
|
|
|
|
.command-description {{
|
|
color: var(--text-secondary);
|
|
line-height: 1.7;
|
|
margin-bottom: 0.75rem;
|
|
font-size: 0.95rem;
|
|
}}
|
|
|
|
.command-usage {{
|
|
margin: 0.75rem 0;
|
|
padding: 0.6rem 0.9rem;
|
|
background: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
border-left: 3px solid var(--accent-blue);
|
|
}}
|
|
|
|
.command-usage code {{
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 0.85rem;
|
|
color: var(--accent-cyan);
|
|
}}
|
|
|
|
.command-params {{
|
|
margin-top: 0.75rem;
|
|
padding-top: 0.75rem;
|
|
border-top: 1px solid var(--border-subtle);
|
|
}}
|
|
|
|
.params-header {{
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
margin-bottom: 0.5rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}}
|
|
|
|
.param-item {{
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.4rem;
|
|
font-size: 0.85rem;
|
|
align-items: flex-start;
|
|
}}
|
|
|
|
.param-name {{
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: var(--accent-orange);
|
|
font-weight: 500;
|
|
min-width: 80px;
|
|
flex-shrink: 0;
|
|
}}
|
|
|
|
.param-desc {{
|
|
color: var(--text-secondary);
|
|
flex: 1;
|
|
line-height: 1.4;
|
|
}}
|
|
|
|
.command-subcommands {{
|
|
margin-top: 1rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid var(--border-subtle);
|
|
}}
|
|
|
|
.subcommands-header {{
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
margin-bottom: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}}
|
|
|
|
.subcommand-item {{
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.9rem;
|
|
align-items: flex-start;
|
|
}}
|
|
|
|
.subcommand-name {{
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: var(--accent-cyan);
|
|
font-weight: 500;
|
|
min-width: 100px;
|
|
flex-shrink: 0;
|
|
}}
|
|
|
|
.subcommand-desc {{
|
|
color: var(--text-secondary);
|
|
flex: 1;
|
|
line-height: 1.5;
|
|
}}
|
|
|
|
.command-channels {{
|
|
color: var(--accent-cyan);
|
|
font-size: 0.85rem;
|
|
margin-top: 0.75rem;
|
|
padding: 0.6rem 0.9rem;
|
|
background: rgba(0, 255, 200, 0.08);
|
|
border-radius: 8px;
|
|
border-left: 3px solid var(--accent-cyan);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.channels-intro {{
|
|
color: var(--text-secondary);
|
|
font-size: 1rem;
|
|
margin-bottom: 2rem;
|
|
line-height: 1.7;
|
|
}}
|
|
|
|
.channel-category {{
|
|
margin-bottom: 2.5rem;
|
|
}}
|
|
|
|
.channel-category-title {{
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: var(--accent-blue);
|
|
margin-bottom: 1rem;
|
|
letter-spacing: -0.01em;
|
|
}}
|
|
|
|
.channel-category-title .anchor-link::before {{
|
|
content: '#';
|
|
position: absolute;
|
|
left: -1.2rem;
|
|
opacity: 0;
|
|
color: var(--accent-blue);
|
|
font-weight: 400;
|
|
transition: opacity 0.2s ease;
|
|
}}
|
|
|
|
.channels-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 1rem;
|
|
}}
|
|
|
|
.channel-card {{
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
padding: 1.25rem;
|
|
border: 1px solid var(--border-subtle);
|
|
transition: all 0.3s ease;
|
|
}}
|
|
|
|
.channel-card:hover {{
|
|
background: var(--bg-card-hover);
|
|
border-color: rgba(0, 212, 255, 0.3);
|
|
transform: translateY(-2px);
|
|
}}
|
|
|
|
.channel-name {{
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
color: var(--accent-cyan);
|
|
margin-bottom: 0.5rem;
|
|
}}
|
|
|
|
.channel-description {{
|
|
color: var(--text-secondary);
|
|
font-size: 0.9rem;
|
|
line-height: 1.6;
|
|
}}
|
|
|
|
footer {{
|
|
text-align: center;
|
|
margin-top: 4rem;
|
|
padding: 2rem;
|
|
color: var(--text-muted);
|
|
font-size: 0.9rem;
|
|
border-top: 1px solid var(--border-subtle);
|
|
}}
|
|
|
|
@media (max-width: 1200px) {{
|
|
.commands-grid {{
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
}}
|
|
}}
|
|
|
|
@media (max-width: 1200px) {{
|
|
/* Disable all backdrop filters on mobile */
|
|
* {{
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
}}
|
|
|
|
.container {{
|
|
grid-template-columns: 1fr !important;
|
|
}}
|
|
|
|
.mobile-menu-toggle {{
|
|
display: flex;
|
|
}}
|
|
|
|
.sidebar-nav {{
|
|
position: fixed !important;
|
|
top: 0 !important;
|
|
left: -320px !important;
|
|
width: 280px !important;
|
|
height: 100vh !important;
|
|
max-height: 100vh !important;
|
|
margin: 0 !important;
|
|
padding-top: 4rem !important;
|
|
border-radius: 0 !important;
|
|
border-left: none !important;
|
|
border-top: none !important;
|
|
border-bottom: none !important;
|
|
border-right: 1px solid var(--border-subtle) !important;
|
|
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
|
z-index: 1002 !important;
|
|
opacity: 1 !important;
|
|
filter: none !important;
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
transform: translateZ(0) !important;
|
|
will-change: left !important;
|
|
isolation: isolate !important;
|
|
background: var(--bg-card) !important;
|
|
visibility: visible !important;
|
|
-webkit-font-smoothing: antialiased !important;
|
|
-moz-osx-font-smoothing: grayscale !important;
|
|
overflow-y: auto !important;
|
|
overflow-x: hidden !important;
|
|
-webkit-overflow-scrolling: touch !important;
|
|
pointer-events: auto !important;
|
|
touch-action: pan-y !important;
|
|
-webkit-transform: translateZ(0) !important;
|
|
transform: translateZ(0) !important;
|
|
}}
|
|
|
|
.sidebar-nav.open {{
|
|
left: 0 !important;
|
|
}}
|
|
|
|
.sidebar-nav * {{
|
|
opacity: 1 !important;
|
|
filter: none !important;
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
visibility: visible !important;
|
|
-webkit-font-smoothing: antialiased !important;
|
|
-moz-osx-font-smoothing: grayscale !important;
|
|
pointer-events: auto !important;
|
|
}}
|
|
|
|
.sidebar-nav .nav-header h3 {{
|
|
color: #e8edf4 !important;
|
|
opacity: 1 !important;
|
|
text-shadow: none !important;
|
|
}}
|
|
|
|
.sidebar-nav .nav-link {{
|
|
color: #8892a4 !important;
|
|
opacity: 1 !important;
|
|
text-shadow: none !important;
|
|
pointer-events: auto !important;
|
|
cursor: pointer !important;
|
|
-webkit-tap-highlight-color: rgba(0, 255, 200, 0.2) !important;
|
|
}}
|
|
|
|
.sidebar-nav .nav-link:hover,
|
|
.sidebar-nav .nav-link:active {{
|
|
color: #00ffc8 !important;
|
|
opacity: 1 !important;
|
|
}}
|
|
|
|
.sidebar-nav .nav-section-header {{
|
|
color: #8892a4 !important;
|
|
opacity: 1 !important;
|
|
text-shadow: none !important;
|
|
}}
|
|
|
|
.main-content {{
|
|
margin-top: 0;
|
|
}}
|
|
}}
|
|
|
|
@media (min-width: 1201px) {{
|
|
.sidebar-nav {{
|
|
position: sticky !important;
|
|
top: 2rem !important;
|
|
left: auto !important;
|
|
width: auto !important;
|
|
height: fit-content !important;
|
|
max-height: calc(100vh - 4rem) !important;
|
|
margin: 0 !important;
|
|
padding: 1.5rem !important;
|
|
border-radius: 16px !important;
|
|
border: 1px solid var(--border-subtle) !important;
|
|
z-index: 100 !important;
|
|
opacity: 1 !important;
|
|
filter: none !important;
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
transform: none !important;
|
|
will-change: auto !important;
|
|
isolation: auto !important;
|
|
background: var(--bg-card) !important;
|
|
visibility: visible !important;
|
|
}}
|
|
}}
|
|
|
|
@media (max-width: 768px) {{
|
|
.container {{
|
|
padding: 2rem 1rem;
|
|
}}
|
|
|
|
.header-content {{
|
|
padding: 2rem 1.5rem;
|
|
}}
|
|
|
|
.header-title {{
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 0.75rem;
|
|
}}
|
|
|
|
.logo-icon {{
|
|
width: 48px;
|
|
height: 48px;
|
|
font-size: 1.5rem;
|
|
}}
|
|
|
|
h1 {{
|
|
font-size: 2.25rem;
|
|
}}
|
|
|
|
.commands-grid {{
|
|
grid-template-columns: 1fr;
|
|
}}
|
|
|
|
.sidebar-nav {{
|
|
padding: 1rem;
|
|
width: calc(100vw - 3rem);
|
|
left: calc(-100vw + 3rem);
|
|
}}
|
|
|
|
.sidebar-nav.open {{
|
|
left: 0;
|
|
}}
|
|
|
|
.mobile-menu-toggle {{
|
|
top: 1rem;
|
|
right: 1rem;
|
|
}}
|
|
}}
|
|
|
|
/* Style-specific CSS overrides */
|
|
{STYLES[style].get('css_overrides', '')}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="atmosphere"></div>
|
|
<div class="grid-overlay"></div>
|
|
|
|
<div class="sidebar-overlay"></div>
|
|
|
|
<button class="mobile-menu-toggle" aria-label="Toggle navigation menu">
|
|
<span class="hamburger"></span>
|
|
<span class="hamburger"></span>
|
|
<span class="hamburger"></span>
|
|
</button>
|
|
|
|
<div class="container">
|
|
{nav_html}
|
|
|
|
<div class="main-content">
|
|
<header>
|
|
<div class="header-content">
|
|
<div class="header-title">
|
|
<h1>{escape_html(bot_name)}</h1>
|
|
</div>
|
|
<div class="intro">
|
|
{escape_html(introduction).replace(chr(10), '<br>')}{channels_suffix}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
{commands_html}
|
|
{channels_html}
|
|
</main>
|
|
|
|
<footer>
|
|
<p>Generated command reference for {escape_html(bot_name)}</p>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Handle mobile menu toggle
|
|
(function() {{
|
|
'use strict';
|
|
|
|
const menuToggle = document.querySelector('.mobile-menu-toggle');
|
|
const sidebar = document.querySelector('.sidebar-nav');
|
|
const overlay = document.querySelector('.sidebar-overlay');
|
|
|
|
if (menuToggle && sidebar) {{
|
|
function toggleMenu() {{
|
|
const isOpen = sidebar.classList.contains('open');
|
|
if (isOpen) {{
|
|
menuToggle.classList.remove('active');
|
|
sidebar.classList.remove('open');
|
|
if (overlay) {{
|
|
overlay.classList.remove('visible');
|
|
}}
|
|
}} else {{
|
|
menuToggle.classList.add('active');
|
|
sidebar.classList.add('open');
|
|
if (overlay) {{
|
|
overlay.classList.add('visible');
|
|
}}
|
|
}}
|
|
}}
|
|
|
|
function closeMenu() {{
|
|
menuToggle.classList.remove('active');
|
|
sidebar.classList.remove('open');
|
|
if (overlay) {{
|
|
overlay.classList.remove('visible');
|
|
}}
|
|
}}
|
|
|
|
menuToggle.addEventListener('click', function(e) {{
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
toggleMenu();
|
|
return false;
|
|
}});
|
|
|
|
// Handle overlay clicks - overlay now only covers area after sidebar
|
|
if (overlay) {{
|
|
overlay.addEventListener('click', function(e) {{
|
|
closeMenu();
|
|
}});
|
|
}}
|
|
|
|
// Handle nav link clicks - don't prevent default, let browser handle navigation
|
|
const navLinks = sidebar.querySelectorAll('.nav-link');
|
|
navLinks.forEach(link => {{
|
|
link.addEventListener('click', function(e) {{
|
|
if (window.innerWidth <= 1200) {{
|
|
// Close menu after a delay to allow navigation
|
|
setTimeout(function() {{
|
|
closeMenu();
|
|
}}, 200);
|
|
}}
|
|
}});
|
|
}});
|
|
}}
|
|
|
|
// Handle keyword expansion
|
|
const expandButtons = document.querySelectorAll('.keyword-expand');
|
|
|
|
expandButtons.forEach(button => {{
|
|
button.addEventListener('click', function() {{
|
|
const hiddenAliases = this.getAttribute('data-hidden').split(',');
|
|
const commandKeywords = this.closest('.command-keywords');
|
|
|
|
// Hide the expand button
|
|
this.classList.add('expanded');
|
|
|
|
// Add hidden aliases as visible badges
|
|
hiddenAliases.forEach(alias => {{
|
|
const badge = document.createElement('span');
|
|
badge.className = 'keyword-badge keyword-hidden visible';
|
|
badge.textContent = alias.trim();
|
|
commandKeywords.appendChild(badge);
|
|
}});
|
|
}});
|
|
}});
|
|
}})();
|
|
</script>
|
|
</body>
|
|
</html>"""
|
|
|
|
return html_content
|
|
|
|
|
|
def list_styles():
|
|
"""Print available styles with descriptions."""
|
|
print("Available styles:\n")
|
|
|
|
# Calculate max name length for alignment
|
|
max_len = max(len(name) for name in STYLES)
|
|
|
|
for style_name, style_info in STYLES.items():
|
|
padding = ' ' * (max_len - len(style_name))
|
|
print(f" {style_name}{padding} {style_info['name']}")
|
|
print(f" {' ' * max_len} {style_info['description']}\n")
|
|
|
|
|
|
def generate_samples(config_file):
|
|
"""Generate sample HTML files for all styles with an index page."""
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger.info(f"Reading config from {config_file}")
|
|
config = read_config(config_file)
|
|
|
|
# Get bot root directory
|
|
bot_root = os.path.dirname(os.path.abspath(config_file))
|
|
if not bot_root:
|
|
bot_root = os.getcwd()
|
|
|
|
# Get bot information
|
|
bot_name = get_bot_name(config)
|
|
admin_commands = get_admin_commands(config)
|
|
title = get_website_title(config)
|
|
introduction = get_website_intro(config)
|
|
|
|
# Get monitor channels (quoted or unquoted)
|
|
monitor_channels_str = strip_optional_quotes(config.get('Channels', 'monitor_channels', fallback=''))
|
|
monitor_channels = [ch.strip() for ch in monitor_channels_str.split(',') if ch.strip()]
|
|
|
|
# Load channels from config
|
|
channels_data = load_channels_from_config(config)
|
|
|
|
# Setup minimal bot for plugin loading
|
|
minimal_bot = MinimalBot(config, logger)
|
|
|
|
# Initialize database manager if database exists
|
|
db_path = get_database_path(config, bot_root)
|
|
if db_path and os.path.exists(db_path):
|
|
try:
|
|
minimal_bot.db_manager = DBManager(minimal_bot, db_path)
|
|
except Exception as e:
|
|
logger.warning(f"Could not load database: {e}")
|
|
minimal_bot.db_manager = None
|
|
else:
|
|
minimal_bot.db_manager = None
|
|
|
|
# Load plugins
|
|
plugin_loader = PluginLoader(minimal_bot)
|
|
commands = plugin_loader.load_all_plugins()
|
|
|
|
# Filter out admin and hidden commands
|
|
public_commands = {
|
|
name: cmd for name, cmd in commands.items()
|
|
if name not in admin_commands and not getattr(cmd, 'hidden', False)
|
|
}
|
|
public_commands.update(get_randomline_commands(config))
|
|
|
|
# Sort commands
|
|
sorted_commands = sorted(public_commands.items(), key=lambda x: x[0])
|
|
|
|
# Create website directory
|
|
output_dir = os.path.join(bot_root, 'website')
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# Generate a page for each style
|
|
generated_files = []
|
|
for style_name in STYLES:
|
|
logger.info(f"Generating {style_name}.html...")
|
|
|
|
html_content = generate_html(
|
|
bot_name=bot_name,
|
|
title=title,
|
|
introduction=introduction,
|
|
commands=sorted_commands,
|
|
monitor_channels=monitor_channels,
|
|
channels_data=channels_data,
|
|
style=style_name
|
|
)
|
|
|
|
output_path = os.path.join(output_dir, f'{style_name}.html')
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
generated_files.append(style_name)
|
|
logger.info(f" ✓ {output_path}")
|
|
|
|
# Generate index.html with links to all styles
|
|
logger.info("Generating index.html...")
|
|
|
|
index_html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{escape_html(bot_name)} - Style Samples</title>
|
|
<style>
|
|
* {{
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}}
|
|
|
|
body {{
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
line-height: 1.6;
|
|
padding: 3rem 2rem;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
background: #f5f5f5;
|
|
}}
|
|
|
|
h1 {{
|
|
font-size: 2.5rem;
|
|
margin-bottom: 0.5rem;
|
|
color: #1a1a1a;
|
|
}}
|
|
|
|
p {{
|
|
color: #666;
|
|
margin-bottom: 2rem;
|
|
}}
|
|
|
|
.styles-grid {{
|
|
display: grid;
|
|
gap: 1rem;
|
|
}}
|
|
|
|
.style-link {{
|
|
display: block;
|
|
padding: 1.5rem;
|
|
background: white;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
transition: all 0.2s ease;
|
|
}}
|
|
|
|
.style-link:hover {{
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
border-color: #0052cc;
|
|
}}
|
|
|
|
.style-name {{
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: #0052cc;
|
|
margin-bottom: 0.25rem;
|
|
}}
|
|
|
|
.style-description {{
|
|
color: #666;
|
|
font-size: 0.875rem;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>{escape_html(bot_name)} Style Samples</h1>
|
|
<p>Browse different visual styles for the command documentation.</p>
|
|
|
|
<div class="styles-grid">
|
|
"""
|
|
|
|
for style_name in STYLES:
|
|
style_info = STYLES[style_name]
|
|
index_html += f""" <a href="{style_name}.html" class="style-link">
|
|
<div class="style-name">{escape_html(style_info['name'])}</div>
|
|
<div class="style-description">{escape_html(style_info['description'])}</div>
|
|
</a>
|
|
"""
|
|
|
|
index_html += """ </div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
index_path = os.path.join(output_dir, 'index.html')
|
|
with open(index_path, 'w', encoding='utf-8') as f:
|
|
f.write(index_html)
|
|
|
|
logger.info(f" ✓ {index_path}")
|
|
logger.info(f"\nGenerated {len(generated_files)} style samples + index.html in {output_dir}")
|
|
print(f"\n✓ Sample files generated in {output_dir}/")
|
|
print(" - index.html (style browser)")
|
|
for style_name in generated_files:
|
|
print(f" - {style_name}.html")
|
|
|
|
|
|
def main():
|
|
"""Main function"""
|
|
logger = setup_logging()
|
|
|
|
# Parse command-line arguments
|
|
parser = argparse.ArgumentParser(
|
|
description='Generate a static website for the bot commands and channels.',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
)
|
|
|
|
parser.add_argument(
|
|
'config',
|
|
nargs='?',
|
|
default='config.ini',
|
|
help='Path to config.ini file (default: config.ini)'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--style',
|
|
choices=list(STYLES.keys()),
|
|
default='default',
|
|
help='CSS style theme for the website (default: default)'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--list-styles',
|
|
action='store_true',
|
|
help='List all available styles and exit'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--sample',
|
|
action='store_true',
|
|
help='Generate sample pages for all styles with an index.html'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Handle --list-styles flag
|
|
if args.list_styles:
|
|
list_styles()
|
|
sys.exit(0)
|
|
|
|
# Handle --sample flag
|
|
if args.sample:
|
|
generate_samples(args.config)
|
|
sys.exit(0)
|
|
|
|
config_file = args.config
|
|
style = args.style
|
|
|
|
try:
|
|
# Read config
|
|
logger.info(f"Reading config from {config_file}")
|
|
config = read_config(config_file)
|
|
|
|
# Get bot root directory
|
|
bot_root = os.path.dirname(os.path.abspath(config_file))
|
|
if not bot_root:
|
|
bot_root = os.getcwd()
|
|
|
|
# Get bot information
|
|
bot_name = get_bot_name(config)
|
|
admin_commands = get_admin_commands(config)
|
|
introduction = get_website_intro(config)
|
|
title = get_website_title(config)
|
|
|
|
# Get monitor channels for channel restriction display (quoted or unquoted)
|
|
monitor_channels_str = strip_optional_quotes(config.get('Channels', 'monitor_channels', fallback=''))
|
|
monitor_channels = [ch.strip() for ch in monitor_channels_str.split(',') if ch.strip()]
|
|
|
|
# Load channels from Channels_List section
|
|
channels_data = load_channels_from_config(config)
|
|
logger.info(f"Loaded {sum(len(chans) for chans in channels_data.values())} channels from {len(channels_data)} categories")
|
|
|
|
logger.info(f"Bot name: {bot_name}")
|
|
logger.info(f"Admin commands: {admin_commands}")
|
|
logger.info(f"Monitor channels: {monitor_channels}")
|
|
|
|
# Setup minimal bot for plugin loading
|
|
minimal_bot = MinimalBot(config, logger)
|
|
|
|
# Initialize database manager if database exists
|
|
db_path = get_database_path(config, bot_root)
|
|
if db_path and os.path.exists(db_path):
|
|
try:
|
|
minimal_bot.db_manager = DBManager(minimal_bot, db_path)
|
|
logger.info(f"Database loaded: {db_path}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not load database: {e}")
|
|
minimal_bot.db_manager = None
|
|
else:
|
|
logger.info("No database found, using default command ordering")
|
|
minimal_bot.db_manager = None
|
|
|
|
# Load plugins
|
|
logger.info("Loading command plugins...")
|
|
plugin_loader = PluginLoader(minimal_bot)
|
|
commands = plugin_loader.load_all_plugins()
|
|
logger.info(f"Loaded {len(commands)} commands")
|
|
|
|
# Filter out admin and hidden commands
|
|
filtered_commands = filter_commands(commands, admin_commands)
|
|
filtered_commands.update(get_randomline_commands(config))
|
|
logger.info(f"Filtered to {len(filtered_commands)} public commands")
|
|
|
|
# Log which commands are included
|
|
included_names = sorted([cmd.name if hasattr(cmd, 'name') else name for name, cmd in filtered_commands.items()])
|
|
logger.info(f"Included commands: {', '.join(included_names)}")
|
|
|
|
# Log which commands were excluded (for debugging)
|
|
excluded = []
|
|
for cmd_name, cmd_instance in commands.items():
|
|
if cmd_name not in filtered_commands:
|
|
primary_name = cmd_instance.name if hasattr(cmd_instance, 'name') else cmd_name
|
|
category = getattr(cmd_instance, 'category', 'unknown')
|
|
excluded.append(f"{primary_name} ({category})")
|
|
if excluded:
|
|
logger.debug(f"Excluded commands: {', '.join(excluded)}")
|
|
|
|
# Get command popularity
|
|
popularity = get_command_popularity(db_path, filtered_commands)
|
|
|
|
# Sort commands by popularity
|
|
sorted_commands = sort_commands_by_popularity(filtered_commands, popularity)
|
|
|
|
# Generate HTML
|
|
logger.info("Generating HTML...")
|
|
html_content = generate_html(bot_name, title, introduction, sorted_commands, monitor_channels, channels_data, style)
|
|
|
|
# Create website directory
|
|
website_dir = os.path.join(bot_root, "website")
|
|
os.makedirs(website_dir, exist_ok=True)
|
|
|
|
# Write HTML file
|
|
output_file = os.path.join(website_dir, "index.html")
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
logger.info(f"Website generated successfully: {output_file}")
|
|
print(f"\n✓ Website generated: {output_file}")
|
|
print(f" Bot: {bot_name}")
|
|
print(f" Commands: {len(sorted_commands)}")
|
|
print(f" Output: {output_file}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating website: {e}", exc_info=True)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|