From 0f47b8abd61527665d44e8f4ccbfc63933d27ffa Mon Sep 17 00:00:00 2001
From: Phil <phil@thesatelliteoflove.com>
Date: Mon, 6 Jan 2025 19:32:31 -0700
Subject: [PATCH] initial commit

---
 dockerfile         |  23 +++++++
 micropub_server.py | 152 +++++++++++++++++++++++++++++++++++++++++++++
 requirements.txt   |   5 ++
 3 files changed, 180 insertions(+)
 create mode 100644 dockerfile
 create mode 100644 micropub_server.py
 create mode 100644 requirements.txt

diff --git a/dockerfile b/dockerfile
new file mode 100644
index 0000000..3cdbbbc
--- /dev/null
+++ b/dockerfile
@@ -0,0 +1,23 @@
+# Use a lightweight Python image
+FROM python:3.10-slim
+
+# Set the working directory
+WORKDIR /app
+
+# Install dependencies
+COPY requirements.txt ./
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy the application code
+COPY . .
+
+# Set environment variables (optional for local testing)
+ENV FLASK_ENV=production
+ENV HOST=0.0.0.0
+ENV PORT=5000
+
+# Expose the Flask port
+EXPOSE 5000
+
+# Run the application
+CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "micropub_server:app"]
diff --git a/micropub_server.py b/micropub_server.py
new file mode 100644
index 0000000..5a111a5
--- /dev/null
+++ b/micropub_server.py
@@ -0,0 +1,152 @@
+import os
+import base64
+import mimetypes
+import logging
+from datetime import datetime
+from flask import Flask, request, jsonify
+from werkzeug.utils import secure_filename
+from dotenv import load_dotenv
+import requests
+
+# Load environment variables
+load_dotenv()
+
+# Configuration from environment variables
+GITEA_API_URL = os.environ.get("GITEA_API_URL", "https://your-gitea-instance/api/v1")
+GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
+REPO_OWNER = os.environ.get("REPO_OWNER", "default-owner")
+REPO_NAME = os.environ.get("REPO_NAME", "default-repo")
+CONTENT_PATH = os.environ.get("CONTENT_PATH", "content")
+MEDIA_DIR = os.environ.get("MEDIA_DIR", "static/images")
+BRANCH = os.environ.get("BRANCH", "main")
+TOKEN_ENDPOINT = os.environ.get("TOKEN_ENDPOINT", "https://tokens.indieauth.com/token")
+
+# Initialize Flask app
+app = Flask(__name__)
+
+# Logging configuration
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+# Helper: Call Gitea API
+def gitea_api_request(method, endpoint, data=None):
+    url = f"{GITEA_API_URL}{endpoint}"
+    headers = {"Authorization": f"token {GITEA_TOKEN}"}
+    try:
+        response = requests.request(method, url, headers=headers, json=data)
+        response.raise_for_status()
+        return response.json()
+    except requests.exceptions.RequestException as e:
+        logger.error(f"Gitea API request failed: {e}")
+        raise
+
+# Helper: Validate IndieAuth token
+def validate_token(token):
+    try:
+        response = requests.post(TOKEN_ENDPOINT, data={"token": token})
+        response.raise_for_status()
+        return response.json()  # Should contain "me" (authenticated user's URL)
+    except requests.exceptions.RequestException as e:
+        logger.error(f"Token validation failed: {e}")
+        return None
+
+# Upload content to Gitea
+def upload_to_gitea(filepath, content, commit_message):
+    encoded_content = base64.b64encode(content.encode() if isinstance(content, str) else content).decode()
+    endpoint = f"/repos/{REPO_OWNER}/{REPO_NAME}/contents/{filepath}"
+    data = {
+        "content": encoded_content,
+        "message": commit_message,
+        "branch": BRANCH,
+    }
+    return gitea_api_request("POST", endpoint, data)
+
+# Micropub endpoint
+@app.route("/micropub", methods=["POST", "GET"])
+def micropub():
+    token = request.headers.get("Authorization", "").replace("Bearer ", "")
+    if not token:
+        return jsonify({"error": "Missing authorization token"}), 401
+
+    user = validate_token(token)
+    if not user:
+        return jsonify({"error": "Invalid token"}), 403
+
+    if request.method == "GET":
+        return jsonify({
+            "media-endpoint": "/micropub/media",
+            "configurations": {},
+            "actions": ["create", "update", "delete"],
+        })
+
+    data = request.form
+    if data.get("h") == "entry":
+        return create_post(data, user)
+
+    return jsonify({"error": "Unsupported Micropub request"}), 400
+
+# Create a new post
+def create_post(data, user):
+    title = data.get("name", "Untitled Post")
+    content = data.get("content", "")
+    photo = data.get("photo")  # Optional: URL of uploaded photo
+    slug = data.get("slug", title.lower().replace(" ", "-"))
+    date = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
+
+    md_content = f"""---
+title: "{title}"
+date: {date}
+author: "{user.get('me')}"
+---
+"""
+    if photo:
+        md_content += f"![Image]({photo})\n\n"
+    md_content += content
+
+    filepath = f"{CONTENT_PATH}/{slug}.md"
+    try:
+        response = upload_to_gitea(filepath, md_content, f"Create post: {title}")
+        logger.info(f"Post created: {response['content']['html_url']}")
+        return jsonify({"success": True, "location": response["content"]["html_url"]}), 201
+    except Exception as e:
+        logger.error(f"Failed to create post: {e}")
+        return jsonify({"error": "Failed to create post"}), 500
+
+# Media upload endpoint
+@app.route("/micropub/media", methods=["POST"])
+def media_upload():
+    token = request.headers.get("Authorization", "").replace("Bearer ", "")
+    if not token:
+        return jsonify({"error": "Missing authorization token"}), 401
+
+    user = validate_token(token)
+    if not user:
+        return jsonify({"error": "Invalid token"}), 403
+
+    if "file" not in request.files:
+        return jsonify({"error": "No file provided"}), 400
+
+    file = request.files["file"]
+    if file.filename == "":
+        return jsonify({"error": "Empty filename"}), 400
+
+    filename = secure_filename(file.filename)
+    mimetype = mimetypes.guess_type(filename)[0]
+
+    if mimetype not in ["image/png", "image/jpeg", "image/gif"]:
+        return jsonify({"error": "Invalid file type"}), 400
+
+    try:
+        response = upload_to_gitea(
+            f"{MEDIA_DIR}/{filename}", file.read(), f"Upload media: {filename}"
+        )
+        media_url = f"/{MEDIA_DIR}/{filename}"
+        logger.info(f"Media uploaded: {media_url}")
+        return jsonify({"success": True, "url": media_url}), 201
+    except Exception as e:
+        logger.error(f"Failed to upload media: {e}")
+        return jsonify({"error": "Failed to upload media"}), 500
+
+# Run the app
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", port=5000)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..f0e66ae
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+flask
+requests
+python-dotenv
+gunicorn
+werkzeug