515 lines
18 KiB
Python
515 lines
18 KiB
Python
import os
|
|
import sys
|
|
import subprocess
|
|
import time
|
|
import signal
|
|
import atexit
|
|
import threading
|
|
import logging
|
|
import uvicorn
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s:%(levelname)s:%(name)s: %(message)s')
|
|
log = logging.getLogger(__name__)
|
|
|
|
# Try to import markdown, but provide a fallback if it's not available
|
|
try:
|
|
import markdown
|
|
import markdown.extensions.fenced_code
|
|
import markdown.extensions.tables
|
|
MARKDOWN_AVAILABLE = True
|
|
except ImportError:
|
|
MARKDOWN_AVAILABLE = False
|
|
log.warning("markdown package not available. Will serve raw markdown files.")
|
|
|
|
# Create the FastAPI app
|
|
app = FastAPI(title="Markdown Server", docs_url=None, redoc_url=None)
|
|
|
|
# Mount static files directory
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
# Define the HTML template for rendering markdown
|
|
# Using double curly braces to escape them in the CSS
|
|
HTML_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{title}</title>
|
|
<meta property="og:title" content="{og_title}" />
|
|
<meta property="og:description" content="{og_description}" />
|
|
<meta property="og:type" content="{og_type}" />
|
|
<meta property="og:url" content="{og_url}" />
|
|
<meta property="og:image" content="{og_image}" />
|
|
<style>
|
|
body {{
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}}
|
|
h1 {{
|
|
border-bottom: 2px solid #eaecef;
|
|
padding-bottom: 0.3em;
|
|
color: #24292e;
|
|
}}
|
|
h2 {{
|
|
border-bottom: 1px solid #eaecef;
|
|
padding-bottom: 0.3em;
|
|
color: #24292e;
|
|
}}
|
|
code {{
|
|
background-color: #f6f8fa;
|
|
padding: 0.2em 0.4em;
|
|
border-radius: 3px;
|
|
font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
|
|
}}
|
|
pre {{
|
|
background-color: #f6f8fa;
|
|
padding: 16px;
|
|
border-radius: 6px;
|
|
overflow: auto;
|
|
}}
|
|
pre code {{
|
|
background-color: transparent;
|
|
padding: 0;
|
|
}}
|
|
a {{
|
|
color: #0366d6;
|
|
text-decoration: none;
|
|
}}
|
|
a:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
table {{
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
margin-bottom: 16px;
|
|
}}
|
|
table, th, td {{
|
|
border: 1px solid #dfe2e5;
|
|
}}
|
|
th, td {{
|
|
padding: 8px 16px;
|
|
text-align: left;
|
|
}}
|
|
th {{
|
|
background-color: #f6f8fa;
|
|
}}
|
|
tr:nth-child(even) {{
|
|
background-color: #f6f8fa;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{content}
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# Function to read and convert markdown to HTML
|
|
def render_markdown(file_path, title, og_title, og_description, og_type, og_url, og_image):
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
md_content = f.read()
|
|
|
|
if MARKDOWN_AVAILABLE:
|
|
# Convert markdown to HTML with extensions
|
|
html_content = markdown.markdown(
|
|
md_content,
|
|
extensions=[
|
|
'markdown.extensions.fenced_code',
|
|
'markdown.extensions.tables',
|
|
'markdown.extensions.toc'
|
|
]
|
|
)
|
|
else:
|
|
# Simple fallback if markdown package is not available
|
|
# Just wrap the content in <pre> tags to preserve formatting
|
|
html_content = f"<pre style='white-space: pre-wrap;'>{md_content}</pre>"
|
|
|
|
# Insert the HTML content into the template
|
|
return HTML_TEMPLATE.format(
|
|
title=title,
|
|
content=html_content,
|
|
og_title=og_title,
|
|
og_description=og_description,
|
|
og_type=og_type,
|
|
og_url=og_url,
|
|
og_image=og_image
|
|
)
|
|
except Exception as e:
|
|
return HTML_TEMPLATE.format(
|
|
title="Error",
|
|
content=f"<h1>Error</h1><p>Failed to render markdown: {str(e)}</p>",
|
|
og_title="Error",
|
|
og_description="Failed to render content.",
|
|
og_type="website",
|
|
og_url="",
|
|
og_image=""
|
|
)
|
|
|
|
# Routes for TOS and Privacy Policy
|
|
@app.get("/tos", response_class=HTMLResponse)
|
|
async def get_tos(request: Request):
|
|
base_url = str(request.base_url)
|
|
return render_markdown(
|
|
"TOS.md",
|
|
"Terms of Service",
|
|
og_title="Terms of Service - Discord Bot",
|
|
og_description="Read the Terms of Service for our Discord Bot.",
|
|
og_type="article",
|
|
og_url=f"{base_url}tos",
|
|
og_image=f"{base_url}static/images/bot_logo.png" # Assuming a static folder for images
|
|
)
|
|
|
|
@app.get("/privacy", response_class=HTMLResponse)
|
|
async def get_privacy(request: Request):
|
|
base_url = str(request.base_url)
|
|
return render_markdown(
|
|
"PRIVACY_POLICY.md",
|
|
"Privacy Policy",
|
|
og_title="Privacy Policy - Discord Bot",
|
|
og_description="Understand how your data is handled by our Discord Bot.",
|
|
og_type="article",
|
|
og_url=f"{base_url}privacy",
|
|
og_image=f"{base_url}static/images/bot_logo.png" # Assuming a static folder for images
|
|
)
|
|
|
|
# Root route that redirects to TOS
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def root(request: Request):
|
|
return """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Bot Legal Documents</title>
|
|
<style>
|
|
body {{
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
text-align: center;
|
|
}}
|
|
h1 {{
|
|
margin-bottom: 30px;
|
|
}}
|
|
.links {{
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 20px;
|
|
margin-top: 30px;
|
|
}}
|
|
.link-button {{
|
|
display: inline-block;
|
|
padding: 10px 20px;
|
|
background-color: #0366d6;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
}}
|
|
.link-button:hover {{
|
|
background-color: #0056b3;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Discord Bot Legal Documents</h1>
|
|
<p>Please review our Terms of Service and Privacy Policy.</p>
|
|
<div class="links">
|
|
<a href="/tos" class="link-button">Terms of Service</a>
|
|
<a href="/privacy" class="link-button">Privacy Policy</a>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# Function to start the server in a thread
|
|
def start_markdown_server_in_thread(host="0.0.0.0", port=5006):
|
|
"""Start the markdown server in a separate thread using a different approach
|
|
that doesn't conflict with the API server's Uvicorn instance."""
|
|
log.info(f"Starting markdown server on {host}:{port}...")
|
|
|
|
# Create a custom server that doesn't use Uvicorn directly
|
|
from starlette.applications import Starlette
|
|
from starlette.routing import Mount
|
|
from starlette.responses import HTMLResponse
|
|
import socket
|
|
import threading
|
|
|
|
# Create a simple HTTP server to handle requests
|
|
class MarkdownServer(threading.Thread):
|
|
def __init__(self, host, port):
|
|
super().__init__(daemon=True)
|
|
self.host = host
|
|
self.port = port
|
|
self.running = False
|
|
self.server_socket = None
|
|
|
|
def run(self):
|
|
self.running = True
|
|
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
|
try:
|
|
self.server_socket.bind((self.host, self.port))
|
|
self.server_socket.listen(5)
|
|
log.info(f"Markdown server listening on {self.host}:{self.port}")
|
|
|
|
while self.running:
|
|
# Accept connections and handle them in a new thread
|
|
try:
|
|
client_socket, addr = self.server_socket.accept()
|
|
client_thread = threading.Thread(
|
|
target=self.handle_request,
|
|
args=(client_socket, addr),
|
|
daemon=True
|
|
)
|
|
client_thread.start()
|
|
except Exception as e:
|
|
if self.running: # Only log if we're still supposed to be running
|
|
log.exception(f"Error accepting connection: {e}")
|
|
except Exception as e:
|
|
log.exception(f"Error starting markdown server: {e}")
|
|
finally:
|
|
if self.server_socket:
|
|
self.server_socket.close()
|
|
|
|
def handle_request(self, client_socket, addr):
|
|
try:
|
|
# Read the HTTP request
|
|
request_data = client_socket.recv(1024).decode('utf-8')
|
|
request_lines = request_data.split('\n')
|
|
if not request_lines:
|
|
return
|
|
|
|
# Parse the request line
|
|
request_line = request_lines[0].strip()
|
|
parts = request_line.split()
|
|
if len(parts) < 2:
|
|
return
|
|
|
|
method, path = parts[0], parts[1]
|
|
|
|
# Simple routing
|
|
if path == '/' or path == '/index.html':
|
|
response = self.serve_root()
|
|
elif path == '/tos' or path == '/tos.html':
|
|
response = self.serve_tos()
|
|
elif path == '/privacy' or path == '/privacy.html':
|
|
response = self.serve_privacy()
|
|
else:
|
|
response = self.serve_404()
|
|
|
|
# Send the response
|
|
client_socket.sendall(response.encode('utf-8'))
|
|
except Exception as e:
|
|
log.exception(f"Error handling request: {e}")
|
|
finally:
|
|
client_socket.close()
|
|
|
|
def serve_root(self):
|
|
html = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Bot Legal Documents</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
text-align: center;
|
|
}
|
|
h1 {
|
|
margin-bottom: 30px;
|
|
}
|
|
.links {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 20px;
|
|
margin-top: 30px;
|
|
}
|
|
.link-button {
|
|
display: inline-block;
|
|
padding: 10px 20px;
|
|
background-color: #0366d6;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
}
|
|
.link-button:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Discord Bot Legal Documents</h1>
|
|
<p>Please review our Terms of Service and Privacy Policy.</p>
|
|
<div class="links">
|
|
<a href="/tos" class="link-button">Terms of Service</a>
|
|
<a href="/privacy" class="link-button">Privacy Policy</a>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
return "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" + html
|
|
|
|
def serve_tos(self):
|
|
try:
|
|
with open("TOS.md", 'r', encoding='utf-8') as f:
|
|
md_content = f.read()
|
|
|
|
if MARKDOWN_AVAILABLE:
|
|
html_content = markdown.markdown(
|
|
md_content,
|
|
extensions=[
|
|
'markdown.extensions.fenced_code',
|
|
'markdown.extensions.tables',
|
|
'markdown.extensions.toc'
|
|
]
|
|
)
|
|
else:
|
|
html_content = f"<pre style='white-space: pre-wrap;'>{md_content}</pre>"
|
|
|
|
html = HTML_TEMPLATE.format(
|
|
title="Terms of Service",
|
|
content=html_content,
|
|
og_title="Terms of Service - Discord Bot",
|
|
og_description="Read the Terms of Service for our Discord Bot.",
|
|
og_type="article",
|
|
og_url=f"http://{self.host}:{self.port}/tos",
|
|
og_image=f"http://{self.host}:{self.port}/static/images/bot_logo.png"
|
|
)
|
|
return "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" + html
|
|
except Exception as e:
|
|
log.exception(f"Error serving TOS: {e}")
|
|
return "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/html\r\n\r\n<h1>Error</h1><p>Failed to render TOS</p>"
|
|
|
|
def serve_privacy(self):
|
|
try:
|
|
with open("PRIVACY_POLICY.md", 'r', encoding='utf-8') as f:
|
|
md_content = f.read()
|
|
|
|
if MARKDOWN_AVAILABLE:
|
|
html_content = markdown.markdown(
|
|
md_content,
|
|
extensions=[
|
|
'markdown.extensions.fenced_code',
|
|
'markdown.extensions.tables',
|
|
'markdown.extensions.toc'
|
|
]
|
|
)
|
|
else:
|
|
html_content = f"<pre style='white-space: pre-wrap;'>{md_content}</pre>"
|
|
|
|
html = HTML_TEMPLATE.format(
|
|
title="Privacy Policy",
|
|
content=html_content,
|
|
og_title="Privacy Policy - Discord Bot",
|
|
og_description="Understand how your data is handled by our Discord Bot.",
|
|
og_type="article",
|
|
og_url=f"http://{self.host}:{self.port}/privacy",
|
|
og_image=f"http://{self.host}:{self.port}/static/images/bot_logo.png"
|
|
)
|
|
return "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" + html
|
|
except Exception as e:
|
|
log.exception(f"Error serving Privacy Policy: {e}")
|
|
return "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/html\r\n\r\n<h1>Error</h1><p>Failed to render Privacy Policy</p>"
|
|
|
|
def serve_404(self):
|
|
html = "<h1>404 Not Found</h1><p>The requested page was not found.</p>"
|
|
return "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n" + html
|
|
|
|
def stop(self):
|
|
self.running = False
|
|
if self.server_socket:
|
|
self.server_socket.close()
|
|
|
|
# Start the server
|
|
server = MarkdownServer(host, port)
|
|
server.start()
|
|
log.info(f"Markdown server thread started. TOS available at: http://{host}:{port}/tos")
|
|
|
|
return server
|
|
|
|
def start_server():
|
|
"""Start the markdown server as a background process (legacy method)."""
|
|
print("Starting markdown server on port 5006...")
|
|
|
|
# Get the directory of this script
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# Start the server as a subprocess
|
|
server_process = subprocess.Popen(
|
|
[sys.executable, os.path.join(script_dir, "markdown_server.py")],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
universal_newlines=True
|
|
)
|
|
|
|
# Register a function to terminate the server when this script exits
|
|
def cleanup():
|
|
if server_process.poll() is None: # If process is still running
|
|
print("Stopping markdown server...")
|
|
server_process.terminate()
|
|
try:
|
|
server_process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
print("Server didn't terminate gracefully, forcing...")
|
|
server_process.kill()
|
|
|
|
atexit.register(cleanup)
|
|
|
|
# Handle signals
|
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
signal.signal(sig, lambda signum, frame: sys.exit(0))
|
|
|
|
# Wait a moment for the server to start
|
|
time.sleep(2)
|
|
|
|
# Check if the server started successfully
|
|
if server_process.poll() is not None:
|
|
print("Failed to start server. Exit code:", server_process.returncode)
|
|
output, _ = server_process.communicate()
|
|
print("Server output:", output)
|
|
return False
|
|
|
|
print(f"Markdown server running on http://localhost:5006")
|
|
print("TOS available at: http://localhost:5006/tos")
|
|
print("Privacy Policy available at: http://localhost:5006/privacy")
|
|
|
|
return True
|
|
|
|
def run_as_daemon():
|
|
"""Run the server as a daemon process."""
|
|
if start_server():
|
|
# Keep the script running to maintain the server
|
|
try:
|
|
while True:
|
|
time.sleep(60) # Sleep to reduce CPU usage
|
|
except KeyboardInterrupt:
|
|
print("Received keyboard interrupt. Shutting down...")
|
|
sys.exit(0)
|
|
|
|
if __name__ == "__main__":
|
|
# If run directly, start the server in the main thread
|
|
log.info("Starting markdown server on port 5006...")
|
|
uvicorn.run(app, host="0.0.0.0", port=5006)
|