Add complete snitch feature with contact management, consent system, and notification delivery
This commit is contained in:
@@ -22,6 +22,7 @@ import api.routes.preferences as preferences_routes
|
||||
import api.routes.rewards as rewards_routes
|
||||
import api.routes.victories as victories_routes
|
||||
import api.routes.adaptive_meds as adaptive_meds_routes
|
||||
import api.routes.snitch as snitch_routes
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
CORS(app)
|
||||
@@ -39,6 +40,7 @@ ROUTE_MODULES = [
|
||||
rewards_routes,
|
||||
victories_routes,
|
||||
adaptive_meds_routes,
|
||||
snitch_routes,
|
||||
]
|
||||
|
||||
|
||||
|
||||
293
api/routes/snitch.py
Normal file
293
api/routes/snitch.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
api/routes/snitch.py - API endpoints for snitch system
|
||||
"""
|
||||
|
||||
import flask
|
||||
import jwt
|
||||
import os
|
||||
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": flask.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"] = flask.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": flask.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/<contact_id>", 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/<contact_id>", 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!"
|
||||
|
||||
# Use notification system for test
|
||||
import core.notifications as notifications
|
||||
|
||||
user_settings = notifications.getNotificationSettings(user_uuid)
|
||||
|
||||
if user_settings:
|
||||
notifications._sendToEnabledChannels(
|
||||
user_settings, test_message, user_uuid=user_uuid
|
||||
)
|
||||
return flask.jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Test sent to {contact.get('contact_name')} via {contact.get('contact_type')}",
|
||||
}
|
||||
), 200
|
||||
else:
|
||||
return flask.jsonify({"error": "No notification settings configured"}), 400
|
||||
Reference in New Issue
Block a user