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