discordbot/run_markdown_server.py
2025-06-05 21:31:06 -06:00

536 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)