""" api/routes/snitch.py - API endpoints for snitch system """ import flask import jwt import os from datetime import datetime import core.postgres as postgres import core.snitch as snitch_core JWT_SECRET = os.getenv("JWT_SECRET") def _get_user_uuid(request): """Extract and validate user UUID from JWT token.""" auth_header = request.headers.get("Authorization", "") if not auth_header.startswith("Bearer "): return None token = auth_header[7:] try: payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) return payload.get("sub") except jwt.ExpiredSignatureError: return None except jwt.InvalidTokenError: return None def register(app): @app.route("/api/snitch/settings", methods=["GET"]) def get_snitch_settings(): """Get user's snitch settings.""" user_uuid = _get_user_uuid(flask.request) if not user_uuid: return flask.jsonify({"error": "Unauthorized"}), 401 settings = snitch_core.get_snitch_settings(user_uuid) if not settings: # Return defaults return flask.jsonify( { "snitch_enabled": False, "trigger_after_nags": 4, "trigger_after_missed_doses": 1, "max_snitches_per_day": 2, "require_consent": True, "consent_given": False, "snitch_cooldown_hours": 4, } ), 200 return flask.jsonify( { "snitch_enabled": settings.get("snitch_enabled", False), "trigger_after_nags": settings.get("trigger_after_nags", 4), "trigger_after_missed_doses": settings.get( "trigger_after_missed_doses", 1 ), "max_snitches_per_day": settings.get("max_snitches_per_day", 2), "require_consent": settings.get("require_consent", True), "consent_given": settings.get("consent_given", False), "snitch_cooldown_hours": settings.get("snitch_cooldown_hours", 4), } ), 200 @app.route("/api/snitch/settings", methods=["PUT"]) def update_snitch_settings(): """Update user's snitch settings.""" user_uuid = _get_user_uuid(flask.request) if not user_uuid: return flask.jsonify({"error": "Unauthorized"}), 401 data = flask.request.get_json() if not data: return flask.jsonify({"error": "No data provided"}), 400 # Build update data update_data = { "snitch_enabled": data.get("snitch_enabled", False), "trigger_after_nags": data.get("trigger_after_nags", 4), "trigger_after_missed_doses": data.get("trigger_after_missed_doses", 1), "max_snitches_per_day": data.get("max_snitches_per_day", 2), "require_consent": data.get("require_consent", True), "consent_given": data.get("consent_given", False), "snitch_cooldown_hours": data.get("snitch_cooldown_hours", 4), "updated_at": datetime.utcnow(), } # Check if settings exist existing = snitch_core.get_snitch_settings(user_uuid) if existing: postgres.update("snitch_settings", update_data, {"user_uuid": user_uuid}) else: update_data["user_uuid"] = user_uuid update_data["created_at"] = datetime.utcnow() postgres.insert("snitch_settings", update_data) return flask.jsonify({"success": True}), 200 @app.route("/api/snitch/consent", methods=["POST"]) def give_consent(): """Give or revoke consent for snitching.""" user_uuid = _get_user_uuid(flask.request) if not user_uuid: return flask.jsonify({"error": "Unauthorized"}), 401 data = flask.request.get_json() consent_given = data.get("consent_given", False) snitch_core.update_consent(user_uuid, consent_given) return flask.jsonify({"success": True, "consent_given": consent_given}), 200 @app.route("/api/snitch/contacts", methods=["GET"]) def get_snitch_contacts(): """Get user's snitch contacts.""" user_uuid = _get_user_uuid(flask.request) if not user_uuid: return flask.jsonify({"error": "Unauthorized"}), 401 contacts = snitch_core.get_snitch_contacts(user_uuid, active_only=False) return flask.jsonify( [ { "id": c.get("id"), "contact_name": c.get("contact_name"), "contact_type": c.get("contact_type"), "contact_value": c.get("contact_value"), "priority": c.get("priority", 1), "notify_all": c.get("notify_all", False), "is_active": c.get("is_active", True), } for c in contacts ] ), 200 @app.route("/api/snitch/contacts", methods=["POST"]) def add_snitch_contact(): """Add a new snitch contact.""" user_uuid = _get_user_uuid(flask.request) if not user_uuid: return flask.jsonify({"error": "Unauthorized"}), 401 data = flask.request.get_json() # Validate required fields required = ["contact_name", "contact_type", "contact_value"] for field in required: if not data.get(field): return flask.jsonify({"error": f"Missing required field: {field}"}), 400 # Validate contact_type if data["contact_type"] not in ["discord", "email", "sms"]: return flask.jsonify( {"error": "contact_type must be discord, email, or sms"} ), 400 contact_data = { "user_uuid": user_uuid, "contact_name": data["contact_name"], "contact_type": data["contact_type"], "contact_value": data["contact_value"], "priority": data.get("priority", 1), "notify_all": data.get("notify_all", False), "is_active": data.get("is_active", True), "created_at": datetime.utcnow(), } result = postgres.insert("snitch_contacts", contact_data) return flask.jsonify( {"success": True, "contact_id": result.get("id") if result else None} ), 201 @app.route("/api/snitch/contacts/", methods=["PUT"]) def update_snitch_contact(contact_id): """Update a snitch contact.""" user_uuid = _get_user_uuid(flask.request) if not user_uuid: return flask.jsonify({"error": "Unauthorized"}), 401 data = flask.request.get_json() # Check contact exists and belongs to user contacts = postgres.select( "snitch_contacts", {"id": contact_id, "user_uuid": user_uuid} ) if not contacts: return flask.jsonify({"error": "Contact not found"}), 404 update_data = {} if "contact_name" in data: update_data["contact_name"] = data["contact_name"] if "contact_type" in data: if data["contact_type"] not in ["discord", "email", "sms"]: return flask.jsonify({"error": "Invalid contact_type"}), 400 update_data["contact_type"] = data["contact_type"] if "contact_value" in data: update_data["contact_value"] = data["contact_value"] if "priority" in data: update_data["priority"] = data["priority"] if "notify_all" in data: update_data["notify_all"] = data["notify_all"] if "is_active" in data: update_data["is_active"] = data["is_active"] if update_data: postgres.update("snitch_contacts", update_data, {"id": contact_id}) return flask.jsonify({"success": True}), 200 @app.route("/api/snitch/contacts/", methods=["DELETE"]) def delete_snitch_contact(contact_id): """Delete a snitch contact.""" user_uuid = _get_user_uuid(flask.request) if not user_uuid: return flask.jsonify({"error": "Unauthorized"}), 401 # Check contact exists and belongs to user contacts = postgres.select( "snitch_contacts", {"id": contact_id, "user_uuid": user_uuid} ) if not contacts: return flask.jsonify({"error": "Contact not found"}), 404 postgres.delete("snitch_contacts", {"id": contact_id}) return flask.jsonify({"success": True}), 200 @app.route("/api/snitch/history", methods=["GET"]) def get_snitch_history(): """Get user's snitch history.""" user_uuid = _get_user_uuid(flask.request) if not user_uuid: return flask.jsonify({"error": "Unauthorized"}), 401 days = flask.request.args.get("days", 7, type=int) history = snitch_core.get_snitch_history(user_uuid, days) return flask.jsonify( [ { "id": h.get("id"), "contact_id": h.get("contact_id"), "medication_id": h.get("medication_id"), "trigger_reason": h.get("trigger_reason"), "snitch_count_today": h.get("snitch_count_today"), "sent_at": h.get("sent_at").isoformat() if h.get("sent_at") else None, "delivered": h.get("delivered"), } for h in history ] ), 200 @app.route("/api/snitch/test", methods=["POST"]) def test_snitch(): """Test snitch functionality (sends to first contact only).""" user_uuid = _get_user_uuid(flask.request) if not user_uuid: return flask.jsonify({"error": "Unauthorized"}), 401 # Get first active contact contacts = snitch_core.get_snitch_contacts(user_uuid, active_only=True) if not contacts: return flask.jsonify({"error": "No active contacts configured"}), 400 # Send test message contact = contacts[0] test_message = f"🧪 This is a test snitch notification for {contact.get('contact_name')}. If you're receiving this, the snitch system is working!" # Send to the contact (not the current user) if contact.get("contact_type") == "discord": # For Discord, we need the bot to send the DM # Store in a queue for the bot to pick up # For now, return success but note it requires bot implementation return flask.jsonify( { "success": True, "message": f"Test queued for {contact.get('contact_name')} (Discord: {contact.get('contact_value')}). Note: Actual Discord DM delivery requires bot implementation.", } ) else: # For email/SMS, would use external providers return flask.jsonify( { "success": True, "message": f"Test configured for {contact.get('contact_name')} via {contact.get('contact_type')}. Note: {contact.get('contact_type')} delivery requires external provider setup.", } ), 200