Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -28,16 +28,15 @@ app.add_middleware(
|
|
| 28 |
PERSISTENT_ROOT = os.environ.get("PERSISTENT_DIR", "/data")
|
| 29 |
CHAT_DIR = os.path.join(PERSISTENT_ROOT, "chat")
|
| 30 |
if not os.path.isdir(PERSISTENT_ROOT):
|
| 31 |
-
# Fall back locally if /data isn't available
|
| 32 |
CHAT_DIR = os.path.join(".", "data", "chat")
|
| 33 |
os.makedirs(CHAT_DIR, exist_ok=True)
|
| 34 |
|
| 35 |
def _chat_file_for(video_id: str) -> str:
|
| 36 |
-
# Stable filename
|
| 37 |
h = hashlib.sha256(video_id.encode("utf-8")).hexdigest()[:32]
|
| 38 |
return os.path.join(CHAT_DIR, f"{h}.jsonl")
|
| 39 |
|
| 40 |
-
#
|
| 41 |
app.state.chat_locks = {}
|
| 42 |
|
| 43 |
def _lock_for(path: str) -> asyncio.Lock:
|
|
@@ -82,35 +81,67 @@ class NewMessage(BaseModel):
|
|
| 82 |
text: str = Field(..., min_length=1, max_length=5000)
|
| 83 |
|
| 84 |
@app.get("/chat/{video_id}")
|
| 85 |
-
async def get_messages(video_id: str):
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
|
| 95 |
-
async def send_message(video_id: str, msg: ChatMessage, request: Request):
|
| 96 |
-
if video_id not in app.state.chat_storage:
|
| 97 |
-
app.state.chat_storage[video_id] = []
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
"video_id": video_id,
|
| 102 |
-
"author":
|
| 103 |
-
"text":
|
| 104 |
-
"created_at":
|
| 105 |
-
"ip":
|
| 106 |
-
"ua": request.headers.get("user-agent", "")
|
| 107 |
}
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
| 111 |
|
| 112 |
# ---------------------------
|
| 113 |
-
# (
|
| 114 |
# ---------------------------
|
| 115 |
BASE_62_MAP = {c: i for i, c in enumerate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")}
|
| 116 |
|
|
|
|
| 28 |
PERSISTENT_ROOT = os.environ.get("PERSISTENT_DIR", "/data")
|
| 29 |
CHAT_DIR = os.path.join(PERSISTENT_ROOT, "chat")
|
| 30 |
if not os.path.isdir(PERSISTENT_ROOT):
|
|
|
|
| 31 |
CHAT_DIR = os.path.join(".", "data", "chat")
|
| 32 |
os.makedirs(CHAT_DIR, exist_ok=True)
|
| 33 |
|
| 34 |
def _chat_file_for(video_id: str) -> str:
|
| 35 |
+
# Stable filename to avoid path traversal
|
| 36 |
h = hashlib.sha256(video_id.encode("utf-8")).hexdigest()[:32]
|
| 37 |
return os.path.join(CHAT_DIR, f"{h}.jsonl")
|
| 38 |
|
| 39 |
+
# Async file locks
|
| 40 |
app.state.chat_locks = {}
|
| 41 |
|
| 42 |
def _lock_for(path: str) -> asyncio.Lock:
|
|
|
|
| 81 |
text: str = Field(..., min_length=1, max_length=5000)
|
| 82 |
|
| 83 |
@app.get("/chat/{video_id}")
|
| 84 |
+
async def get_messages(video_id: str, limit: int = 50, since: Optional[str] = None):
|
| 85 |
+
"""
|
| 86 |
+
Fetch messages for a video.
|
| 87 |
+
- limit: max messages (default 50)
|
| 88 |
+
- since: ISO8601 timestamp; return only messages newer than this
|
| 89 |
+
"""
|
| 90 |
+
limit = max(1, min(limit, 200))
|
| 91 |
+
path = _chat_file_for(video_id)
|
| 92 |
+
items = await _read_jsonl(path)
|
| 93 |
+
|
| 94 |
+
if since:
|
| 95 |
+
try:
|
| 96 |
+
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
| 97 |
+
items = [
|
| 98 |
+
m for m in items
|
| 99 |
+
if datetime.fromisoformat(str(m.get("created_at", "")).replace("Z", "+00:00")) > since_dt
|
| 100 |
+
]
|
| 101 |
+
except Exception:
|
| 102 |
+
pass # Ignore bad since param
|
| 103 |
|
| 104 |
+
items.sort(key=lambda m: m.get("created_at", ""))
|
| 105 |
+
if len(items) > limit:
|
| 106 |
+
items = items[-limit:]
|
| 107 |
|
| 108 |
+
return {"video_id": video_id, "count": len(items), "messages": items}
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
+
@app.post("/chat/{video_id}")
|
| 111 |
+
async def post_message(video_id: str, msg: NewMessage, request: Request):
|
| 112 |
+
"""
|
| 113 |
+
Append a message to a video's chat.
|
| 114 |
+
Body: { "author": "Ross", "text": "hello" }
|
| 115 |
+
"""
|
| 116 |
+
path = _chat_file_for(video_id)
|
| 117 |
+
lock = _lock_for(path)
|
| 118 |
+
|
| 119 |
+
author = _valid_author(msg.author)
|
| 120 |
+
text = _valid_text(msg.text)
|
| 121 |
+
if not text:
|
| 122 |
+
return {"ok": False, "error": "Empty message"}
|
| 123 |
+
|
| 124 |
+
created = _now_iso()
|
| 125 |
+
mid = hashlib.sha1(f"{video_id}|{author}|{created}|{text}".encode("utf-8")).hexdigest()[:16]
|
| 126 |
+
ip = request.client.host if request and request.client else None
|
| 127 |
+
|
| 128 |
+
record = {
|
| 129 |
+
"id": mid,
|
| 130 |
"video_id": video_id,
|
| 131 |
+
"author": author, # ✅ FIXED: uses frontend-provided name
|
| 132 |
+
"text": text,
|
| 133 |
+
"created_at": created,
|
| 134 |
+
"ip": ip,
|
| 135 |
+
"ua": request.headers.get("user-agent", "")[:200]
|
| 136 |
}
|
| 137 |
|
| 138 |
+
async with lock:
|
| 139 |
+
await _append_jsonl(path, record)
|
| 140 |
+
|
| 141 |
+
return {"ok": True, "message": record}
|
| 142 |
|
| 143 |
# ---------------------------
|
| 144 |
+
# (Original iCloud album endpoints)
|
| 145 |
# ---------------------------
|
| 146 |
BASE_62_MAP = {c: i for i, c in enumerate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")}
|
| 147 |
|