Implement a new `/card` POST endpoint to receive sensitive number data (e.g., credit card details) and securely DM it to the bot owner. Define a `NumberData` Pydantic model for this purpose. Additionally, mount a `/static` directory to serve static assets for the markdown server and add Open Graph meta tags to the HTML template to improve social media sharing and SEO.
319 lines
10 KiB
Python
319 lines
10 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."""
|
|
log.info(f"Starting markdown server on {host}:{port}...")
|
|
|
|
def run_server():
|
|
try:
|
|
uvicorn.run(app, host=host, port=port)
|
|
except Exception as e:
|
|
log.exception(f"Error running markdown server: {e}")
|
|
|
|
# Start the server in a daemon thread
|
|
server_thread = threading.Thread(target=run_server, daemon=True)
|
|
server_thread.start()
|
|
log.info(f"Markdown server thread started. TOS available at: http://{host}:{port}/tos")
|
|
|
|
return server_thread
|
|
|
|
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)
|