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 = """ {title} {content} """ # 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
 tags to preserve formatting
            html_content = f"
{md_content}
" # 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"

Error

Failed to render markdown: {str(e)}

", 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 """ Bot Legal Documents

Discord Bot Legal Documents

Please review our Terms of Service and Privacy Policy.

""" # 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 = """ Bot Legal Documents

Discord Bot Legal Documents

Please review our Terms of Service and Privacy Policy.

""" 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"
{md_content}
" 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

Error

Failed to render TOS

" 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"
{md_content}
" 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

Error

Failed to render Privacy Policy

" def serve_404(self): html = "

404 Not Found

The requested page was not found.

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