retried with custom-api widgets
This commit is contained in:
@@ -990,6 +990,263 @@ data:
|
||||
}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
# ================================
|
||||
# User Data Management (Notes, Todos, Motivation)
|
||||
# ================================
|
||||
# File structure per user: /data/userdata-{username}.json
|
||||
# {
|
||||
# "notes": "free text...",
|
||||
# "todos": [{"id": "uuid", "text": "...", "done": false, "created": "..."}],
|
||||
# "motivation": ["quote1", "quote2", ...]
|
||||
# }
|
||||
|
||||
import uuid as uuid_lib
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
|
||||
class TodoItem(BaseModel):
|
||||
text: str
|
||||
done: bool = False
|
||||
|
||||
class MotivationItem(BaseModel):
|
||||
text: str
|
||||
|
||||
class NotesUpdate(BaseModel):
|
||||
content: str
|
||||
|
||||
def _userdata_path(user: str) -> Path:
|
||||
"""Get the path to user data file."""
|
||||
# Sanitize username to prevent path traversal
|
||||
safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user)
|
||||
if not safe_user:
|
||||
raise HTTPException(status_code=400, detail="Invalid username")
|
||||
return DATA_DIR / f"userdata-{safe_user}.json"
|
||||
|
||||
def _load_userdata(user: str) -> dict:
|
||||
"""Load user data or return default structure."""
|
||||
default = {
|
||||
"notes": "",
|
||||
"todos": [],
|
||||
"motivation": [
|
||||
"Believe in yourself!",
|
||||
"Every day is a new opportunity.",
|
||||
"You've got this!",
|
||||
"Small steps lead to big changes.",
|
||||
"Stay focused, stay positive."
|
||||
]
|
||||
}
|
||||
path = _userdata_path(user)
|
||||
data = _load_json(path, default)
|
||||
# Ensure all keys exist
|
||||
for key in default:
|
||||
if key not in data:
|
||||
data[key] = default[key]
|
||||
return data
|
||||
|
||||
def _save_userdata(user: str, data: dict) -> None:
|
||||
"""Save user data to file."""
|
||||
path = _userdata_path(user)
|
||||
_save_json(path, data)
|
||||
|
||||
def _verify_key(key: str = Query(default="")):
|
||||
"""Verify API key for write operations."""
|
||||
if not GLANCE_HELPER_KEY:
|
||||
return True # No key configured, allow all
|
||||
if key != GLANCE_HELPER_KEY:
|
||||
raise HTTPException(status_code=403, detail="Invalid API key")
|
||||
return True
|
||||
|
||||
# ========== GET ALL USER DATA ==========
|
||||
@APP.get("/userdata/{user}")
|
||||
def get_userdata(user: str):
|
||||
"""Get all user data (notes, todos, motivation)."""
|
||||
data = _load_userdata(user)
|
||||
return Response(
|
||||
content=json.dumps(data, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
# ========== NOTES ==========
|
||||
@APP.get("/userdata/{user}/notes")
|
||||
def get_notes(user: str):
|
||||
"""Get user notes."""
|
||||
data = _load_userdata(user)
|
||||
return Response(
|
||||
content=json.dumps({"notes": data.get("notes", "")}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
@APP.post("/userdata/{user}/notes")
|
||||
def update_notes(user: str, body: NotesUpdate, key: str = Query(default="")):
|
||||
"""Update user notes."""
|
||||
_verify_key(key)
|
||||
data = _load_userdata(user)
|
||||
data["notes"] = body.content
|
||||
_save_userdata(user, data)
|
||||
return Response(
|
||||
content=json.dumps({"success": True, "notes": data["notes"]}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
# ========== TODOS ==========
|
||||
@APP.get("/userdata/{user}/todos")
|
||||
def get_todos(user: str):
|
||||
"""Get user todos."""
|
||||
data = _load_userdata(user)
|
||||
return Response(
|
||||
content=json.dumps({
|
||||
"todos": data.get("todos", []),
|
||||
"count": len(data.get("todos", []))
|
||||
}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
@APP.post("/userdata/{user}/todos")
|
||||
def add_todo(user: str, body: TodoItem, key: str = Query(default="")):
|
||||
"""Add a new todo item."""
|
||||
_verify_key(key)
|
||||
data = _load_userdata(user)
|
||||
|
||||
new_todo = {
|
||||
"id": str(uuid_lib.uuid4())[:8],
|
||||
"text": body.text.strip(),
|
||||
"done": body.done,
|
||||
"created": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
|
||||
if not data.get("todos"):
|
||||
data["todos"] = []
|
||||
data["todos"].append(new_todo)
|
||||
_save_userdata(user, data)
|
||||
|
||||
return Response(
|
||||
content=json.dumps({"success": True, "todo": new_todo}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
@APP.put("/userdata/{user}/todos/{todo_id}")
|
||||
def update_todo(user: str, todo_id: str, body: TodoItem, key: str = Query(default="")):
|
||||
"""Update a todo item (toggle done, update text)."""
|
||||
_verify_key(key)
|
||||
data = _load_userdata(user)
|
||||
|
||||
for todo in data.get("todos", []):
|
||||
if todo.get("id") == todo_id:
|
||||
todo["text"] = body.text.strip()
|
||||
todo["done"] = body.done
|
||||
_save_userdata(user, data)
|
||||
return Response(
|
||||
content=json.dumps({"success": True, "todo": todo}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=404, detail="Todo not found")
|
||||
|
||||
@APP.delete("/userdata/{user}/todos/{todo_id}")
|
||||
def delete_todo(user: str, todo_id: str, key: str = Query(default="")):
|
||||
"""Delete a todo item."""
|
||||
_verify_key(key)
|
||||
data = _load_userdata(user)
|
||||
|
||||
original_count = len(data.get("todos", []))
|
||||
data["todos"] = [t for t in data.get("todos", []) if t.get("id") != todo_id]
|
||||
|
||||
if len(data["todos"]) == original_count:
|
||||
raise HTTPException(status_code=404, detail="Todo not found")
|
||||
|
||||
_save_userdata(user, data)
|
||||
return Response(
|
||||
content=json.dumps({"success": True, "deleted": todo_id}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
# ========== MOTIVATION ==========
|
||||
@APP.get("/userdata/{user}/motivation")
|
||||
def get_motivation(user: str):
|
||||
"""Get all motivation quotes."""
|
||||
data = _load_userdata(user)
|
||||
quotes = data.get("motivation", [])
|
||||
return Response(
|
||||
content=json.dumps({
|
||||
"quotes": quotes,
|
||||
"count": len(quotes)
|
||||
}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
@APP.get("/userdata/{user}/motivation/random")
|
||||
def get_random_motivation(user: str):
|
||||
"""Get a random motivation quote."""
|
||||
data = _load_userdata(user)
|
||||
quotes = data.get("motivation", [])
|
||||
|
||||
if not quotes:
|
||||
return Response(
|
||||
content=json.dumps({
|
||||
"quote": "Add some motivation quotes!",
|
||||
"index": -1,
|
||||
"total": 0
|
||||
}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
idx = random.randint(0, len(quotes) - 1)
|
||||
return Response(
|
||||
content=json.dumps({
|
||||
"quote": quotes[idx],
|
||||
"index": idx,
|
||||
"total": len(quotes)
|
||||
}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
@APP.post("/userdata/{user}/motivation")
|
||||
def add_motivation(user: str, body: MotivationItem, key: str = Query(default="")):
|
||||
"""Add a new motivation quote."""
|
||||
_verify_key(key)
|
||||
data = _load_userdata(user)
|
||||
|
||||
if not data.get("motivation"):
|
||||
data["motivation"] = []
|
||||
|
||||
quote_text = body.text.strip()
|
||||
if quote_text and quote_text not in data["motivation"]:
|
||||
data["motivation"].append(quote_text)
|
||||
_save_userdata(user, data)
|
||||
|
||||
return Response(
|
||||
content=json.dumps({
|
||||
"success": True,
|
||||
"quotes": data["motivation"],
|
||||
"count": len(data["motivation"])
|
||||
}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
@APP.delete("/userdata/{user}/motivation/{index}")
|
||||
def delete_motivation(user: str, index: int, key: str = Query(default="")):
|
||||
"""Delete a motivation quote by index."""
|
||||
_verify_key(key)
|
||||
data = _load_userdata(user)
|
||||
quotes = data.get("motivation", [])
|
||||
|
||||
if index < 0 or index >= len(quotes):
|
||||
raise HTTPException(status_code=404, detail="Quote not found")
|
||||
|
||||
deleted = quotes.pop(index)
|
||||
_save_userdata(user, data)
|
||||
|
||||
return Response(
|
||||
content=json.dumps({
|
||||
"success": True,
|
||||
"deleted": deleted,
|
||||
"quotes": quotes,
|
||||
"count": len(quotes)
|
||||
}, ensure_ascii=False),
|
||||
media_type="application/json; charset=utf-8"
|
||||
)
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -1022,7 +1279,7 @@ spec:
|
||||
apt-get update;
|
||||
apt-get install -y --no-install-recommends curl ca-certificates iputils-ping dnsutils tzdata net-tools;
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
pip install --no-cache-dir fastapi uvicorn requests beautifulsoup4 prometheus-client icalendar python-dateutil;
|
||||
pip install --no-cache-dir fastapi uvicorn requests beautifulsoup4 prometheus-client icalendar python-dateutil pydantic;
|
||||
python -c "import uvicorn; uvicorn.run('app:APP', host='0.0.0.0', port=8000)"
|
||||
command:
|
||||
- /bin/sh
|
||||
|
||||
Reference in New Issue
Block a user