diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..534d1ce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.9-slim + +WORKDIR /openguestbook + +RUN pip install flask flask-cors requests gunicorn + +COPY . . + +CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"] \ No newline at end of file diff --git a/README.md b/README.md index 1b28f5a..bc444ba 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,38 @@ -# OpenGuestbook +# 📖 OpenGuestbook +![Human coded](https://img.shields.io/badge/human-coded-green?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1wZXJzb24tc3RhbmRpbmctaWNvbiBsdWNpZGUtcGVyc29uLXN0YW5kaW5nIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjUiIHI9IjEiLz48cGF0aCBkPSJtOSAyMCAzLTYgMyA2Ii8+PHBhdGggZD0ibTYgOCA2IDIgNi0yIi8+PHBhdGggZD0iTTEyIDEwdjQiLz48L3N2Zz4=) +[![License: GNU General Public License v3.0](https://img.shields.io/badge/License-GNU%20GPL--v3.0-yellow)](https://www.gnu.org/licenses/gpl-3.0.en.html#license-text) +OpenGuestbook is a self-hosted lightweight Guestbook for small static websites made using python and flask. OpenGuestbook does not use databases or an admin panel for managing entries. Instead it saves each guestbook entry as a file with a human readable format in a /guestbook folder in your server. The comments can be moderated by deleting or editing files manually. + +## Features +- No login required +- No database required +- Limited total comments per day. +- Easy setup, and low CPU and RAM use. +- Privacy Friendly: No tracking, no cookies, AD blocker friendly. +- Notification support via Notfy.sh! + +## Deployment +OpenGuestbook comes in two parts, a backend that needs to be hosted on a server, and a JS file that cn be embedded directly on your website. +### Backend deployment with Docker +For production, running via Docker Compose is recommended. + +#### 1) Create docker-compose.yml +```yaml +version: '3.8' +services: + guestbook: + build: . + container_name: OpenGuestbook + restart: always + ports: + - "5000:5000" + volumes: + - ./guestbook_data:/openguestbook/guestbook # Persist comments on host to make it easily reachable + environment: + - FRONTEND_URL= # if a frontend url is not provided the app will default the allow all utl's which is not recommended! + - NTFY_TOPIC= # for psuh notification support (optional) +``` + +## Dependencies +## Configuration \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..61723f4 --- /dev/null +++ b/app.py @@ -0,0 +1,118 @@ +import os +import json +import time +import requests +from datetime import datetime +from flask import Flask, request, jsonify +from flask_cors import CORS + +app = Flask(__name__) + +# configure daily limit and data directory. +DAILY_LIMIT = 50 +DATA_DIR = 'guestbook' + +currentDate = datetime.now().strftime('%Y-%m-%d') +submissionCountDay = 0 + + +frontend_url = os.environ.get("FRONTEND_URL","*") + +topic = os.environ.get("NTFY_TOPIC") + +CORS(app, resources={r"/*": {"origins": frontend_url}}) + +if not os.path.exists(DATA_DIR): + os.makedirs(DATA_DIR) + +@app.route('/comments', methods=['GET']) +def getComments(): + comments = [] + try: + files = sorted([f for f in os.listdir(DATA_DIR) if f.endswith('.json')], reverse=True) + + for filename in files: + filepath = os.path.join(DATA_DIR, filename) + with open(filepath, 'r', encoding='utf-8') as f: + try: + data = json.load(f) + comments.append(data) + except json.JSONDecodeError: + continue + return jsonify(comments) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/comments', methods=['POST']) +def addComment(): + global currentDate, submissionCountDay + + # Check date + today_str = datetime.now().strftime('%Y-%m-%d') + if today_str != currentDate: + currentDate = today_str + submissionCountDay = 0 + + # Check limit + if submissionCountDay >= DAILY_LIMIT: + return jsonify({"error": "Guestbook full for the day, try tomorrow!"}), 403 + + data = request.json + name = data.get('name', '').strip() + message = data.get('message', '').strip() + website = data.get('website', '').strip() + + if not name or not message: + return jsonify({"error": "Missing fields"}), 400 + + #URL cleanup + if website: + # If forgot http://, add it for them + if not website.startswith(('http://', 'https://')): + website = 'https://' + website + + entry = { + 'name': name, + 'message': message, + 'website': website, + 'date': time.strftime("%d-%m-%Y %H:%M") + } + + now = datetime.now() + readable_time = now.strftime('%Y-%m-%d_%H-%M-%S') + + + + filename = f"{readable_time}.json" + filepath = os.path.join(DATA_DIR, filename) + + try: + with open(filepath, 'x', encoding='utf-8') as f: + json.dump(entry, f) + except FileExistsError: + filename = f"{readable_time}_2.json" + filepath = os.path.join(DATA_DIR, filename) + with open(filepath, 'x', encoding='utf-8') as f: + json.dump(entry, f) + + + send_ntfy_notification(name, message) + + + submissionCountDay += 1 + + return jsonify({"status": "success"}) + +def send_ntfy_notification(name, message): + if not topic: + return + try: + requests.post(f"https://ntfy.sh/{topic}", + data=f"{name} wrote: {message}", + headers={ + "Title": "Someone Signed Your Guestbook!" + }) + except Exception as e: + print(f"Notification failed: {e}") +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) \ No newline at end of file