From b4c7a2cc7d1a370f6f3fa56d560e81832e22f1ba Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Mon, 26 May 2025 12:20:37 +1000 Subject: [PATCH] feat: Initial prototype --- db.py | 55 +++++++++ kite_locations.json | 37 ++++++ server.py | 134 +++++++++++++++++++-- templates/assets/css/index.css | 128 +++++++++++++++++++- templates/index.html | 205 ++++++++++++++++++++++++++++++++- 5 files changed, 547 insertions(+), 12 deletions(-) create mode 100644 db.py create mode 100644 kite_locations.json diff --git a/db.py b/db.py new file mode 100644 index 0000000..c906a60 --- /dev/null +++ b/db.py @@ -0,0 +1,55 @@ +import os +import json +from datetime import datetime + +DB_FILE = "kite_locations.json" + +def get_db_path(): + """Get the path to the database file""" + # Check if there's a data directory we should use + if os.path.isdir("/data"): + return os.path.join("/data", DB_FILE) + return os.path.join(os.path.dirname(__file__), DB_FILE) + +def load_locations(): + """Load all kite locations from the database""" + db_path = get_db_path() + + if not os.path.exists(db_path): + return [] + + try: + with open(db_path, "r") as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + return [] + +def save_location(name, description="", rating=None): + """Save a new kite location to the database + + Args: + name (str): Name of the kite flying location + description (str, optional): Description of the location + rating (int, optional): Rating from 1-5 stars + + Returns: + dict: The saved location object + """ + locations = load_locations() + + # Create new location entry + new_location = { + "name": name, + "description": description, + "rating": rating, + "date_added": datetime.now().isoformat() + } + + # Add to list and save + locations.append(new_location) + + db_path = get_db_path() + with open(db_path, "w") as f: + json.dump(locations, f, indent=2) + + return new_location diff --git a/kite_locations.json b/kite_locations.json new file mode 100644 index 0000000..9c5e037 --- /dev/null +++ b/kite_locations.json @@ -0,0 +1,37 @@ +[ + { + "name": "Arboretum", + "description": "Lots of wind", + "date_added": "2025-05-26T11:56:29.508930" + }, + { + "name": "Park", + "description": "Still but still works", + "rating": 3, + "date_added": "2025-05-26T12:02:15.338125" + }, + { + "name": "The Lake", + "description": "", + "rating": 5, + "date_added": "2025-05-26T12:02:42.853429" + }, + { + "name": "Test", + "description": "Test", + "rating": 3, + "date_added": "2025-05-26T12:16:19.288452" + }, + { + "name": "Test", + "description": "", + "rating": 3, + "date_added": "2025-05-26T12:18:16.435249" + }, + { + "name": " ", + "description": "Hello", + "rating": 3, + "date_added": "2025-05-26T12:18:29.691415" + } +] \ No newline at end of file diff --git a/server.py b/server.py index 240a1a8..328b1a1 100644 --- a/server.py +++ b/server.py @@ -1,4 +1,4 @@ -from functools import cache +from functools import cache, lru_cache import json from flask import ( Flask, @@ -15,17 +15,87 @@ import json import requests from datetime import datetime import dotenv +import db # Import our new db module +import re dotenv.load_dotenv() app = Flask(__name__) +# Use PurgoMalum API for profanity checking +PROFANITY_API_URL = "https://vector.profanity.dev" + + +@lru_cache(maxsize=1000) # Cache results to avoid repeated API calls +def contains_profanity(text): + """Check if text contains profanity using the PurgoMalum API""" + if not text: + return False + + try: + # Call the API + response = requests.post( + f"{PROFANITY_API_URL}", + json={"message": text}, + timeout=5 # Set a timeout for the request + ) + + # Check if the API returned success + if response.status_code == 200: + data = response.json() + # Check if the response indicates profanity + return data.get("isProfanity", False) + + # Fall back to a basic check if API fails + print(f"Warning: Profanity API returned status code {response.status_code}") + return False + except requests.RequestException as e: + # Handle timeouts, connection errors, etc. + print(f"Warning: Profanity API request failed: {e}") + return False + + +def is_json_safe(text): + """Ensure text is safe for JSON serialization""" + if not text: + return True + + # Check for control characters that could break JSON + if re.search(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", text): + return False + + # Check if text would be valid in JSON (simplified) + try: + # Test serialize to ensure valid JSON characters + json.dumps(text) + return True + except: + return False + + +def sanitize_text_input(text): + """Sanitize text for JSON storage""" + if not text: + return "" + + # Replace problematic characters for JSON + # This is a simplified approach - more complex sanitization could be added + text = text.replace("\0", "").strip() + + # Limit length to prevent abuse + max_length = 500 + if len(text) > max_length: + text = text[:max_length] + + return text + def find(name, path): for root, dirs, files in os.walk(path): if name in files: return os.path.join(root, name) + # Assets routes @app.route("/assets/") def send_assets(path): @@ -66,8 +136,6 @@ def wellknown(path): return make_response( req.content, 200, {"Content-Type": req.headers["Content-Type"]} ) - - # endregion @@ -76,6 +144,61 @@ def wellknown(path): def index(): return render_template("index.html") +# Kite Watch API Routes +@app.route("/api/locations", methods=["GET"]) +def get_locations(): + locations = db.load_locations() + return jsonify(locations) + +@app.route("/api/locations", methods=["POST"]) +def add_location(): + data = request.json + if not data or "name" not in data: + return jsonify({"error": "Name is required"}), 400 + + name = data.get("name") + description = data.get("description", "") + rating = data.get("rating") + + # Validate required fields + if len(name.strip()) < 5: + return jsonify({"error": "Location name must be at least 5 characters long"}), 400 + if len(name.strip()) > 100: + return jsonify({"error": "Location name is too long (max 100 characters)"}), 400 + if len(description.strip()) > 2000: + return jsonify({"error": "Notes are too long (max 2000 characters)"}), 400 + if not name.strip(): + return jsonify({"error": "Location name cannot be empty"}), 400 + + + # Check for profanity + if contains_profanity(name): + return jsonify({"error": "Location name contains inappropriate language"}), 400 + + if contains_profanity(description): + return jsonify({"error": "Notes contain inappropriate language"}), 400 + + # Validate for JSON safety + if not is_json_safe(name) or not is_json_safe(description): + return jsonify({"error": "Input contains invalid characters"}), 400 + + # Sanitize inputs + name = sanitize_text_input(name) + description = sanitize_text_input(description) + + # Validate rating + if rating is not None: + try: + rating = int(rating) + if not (1 <= rating <= 5): + return jsonify({"error": "Rating must be between 1 and 5"}), 400 + except ValueError: + return jsonify({"error": "Rating must be a number"}), 400 + else: + return jsonify({"error": "Rating is required"}), 400 + + location = db.save_location(name, description, rating) + return jsonify(location), 201 @app.route("/") def catch_all(path: str): @@ -97,8 +220,6 @@ def catch_all(path: str): return send_file(filename) return render_template("404.html"), 404 - - # endregion @@ -107,8 +228,7 @@ def catch_all(path: str): @app.errorhandler(404) def not_found(e): return render_template("404.html"), 404 - - # endregion + if __name__ == "__main__": app.run(debug=True, port=5000, host="0.0.0.0") diff --git a/templates/assets/css/index.css b/templates/assets/css/index.css index 9635f9c..b122db8 100644 --- a/templates/assets/css/index.css +++ b/templates/assets/css/index.css @@ -1,20 +1,146 @@ body { background-color: #000000; color: #ffffff; + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + line-height: 1.6; } + h1 { font-size: 50px; margin: 0; padding: 0; } + +h2 { + margin-top: 0; + color: #4dabf7; +} + +.header { + background-color: #111; + padding: 2rem 0; + text-align: center; + border-bottom: 3px solid #4dabf7; +} + +.tagline { + color: #adb5bd; + font-size: 1.2rem; + margin-top: 0.5rem; +} + +.container { + max-width: 800px; + margin: 2rem auto; + padding: 0 1rem; +} + +.card { + background-color: #111; + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.form-group { + margin-bottom: 1rem; +} + +label { + display: block; + margin-bottom: 0.5rem; +} + +input, textarea { + width: 100%; + padding: 0.75rem; + background-color: #222; + border: 1px solid #444; + border-radius: 4px; + color: #fff; + font-size: 1rem; + box-sizing: border-box; +} + +textarea { + min-height: 100px; + resize: vertical; +} + +button { + background-color: #4dabf7; + color: #000; + border: none; + padding: 0.75rem 1.5rem; + font-size: 1rem; + font-weight: bold; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.2s; +} + +button:hover { + background-color: #74c0fc; +} + +button:disabled { + background-color: #495057; + cursor: not-allowed; +} + +.location-item { + border-bottom: 1px solid #333; + padding: 1rem 0; +} + +.location-item:last-child { + border-bottom: none; +} + +.location-item h3 { + margin: 0 0 0.5rem 0; + color: #4dabf7; +} + +.date { + color: #adb5bd; + font-size: 0.9rem; + margin-top: 0.5rem; +} + +.loading { + text-align: center; + color: #adb5bd; +} + .centre { margin-top: 10%; text-align: center; } + a { - color: #ffffff; + color: #4dabf7; text-decoration: none; } + a:hover { text-decoration: underline; +} + +/* Responsive adjustments */ +@media (max-width: 600px) { + h1 { + font-size: 36px; + } + + .container { + padding: 0 0.5rem; + } + + .card { + padding: 1rem; + } } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index bb349f8..fadf07b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,16 +4,213 @@ - Nathan.Woodburn/ + Kite Watch - Nathan.Woodburn/ -
-
-

Nathan.Woodburn/

+
+

Kite Watch

+

Find perfect places to fly your kite!

+ +
+
+

Add a Kite Flying Location

+ +
+
+ + +
+
+ + +
+
+ +
+ + 3 +
+
+ +
+
+ +
+

Great Places to Fly Kites

+
+

Loading locations...

+
+
+
+ + + + \ No newline at end of file