Rich chat messages (reactions, replies, files, images); RSS poll countdown; Speakers' Corner directory page updates
- Chat: extract reactions, quoted replies, file/image data in _normalize_item
- Chat: render emoji reaction pills, reply-quote blocks, inline image previews, file blocks with Accept/Download
- Chat: reply UI (hover → set reply → preview bar above compose → send with quotedItemId)
- Chat: emoji picker strip (6 quick-react emojis) on message hover
- Chat: POST /react and POST /file/{id}/receive and GET /file/{id}/download endpoints
- Chat: file decryption via core.chat_read_file (native libsimplex FFI), served with correct MIME type
- List: RSS bot cards show live next-poll countdown (ticks every second via status API poll_next field)
- Directory: rename SimpleXXX → Speakers' Corner Online Directory throughout
- Directory: add hero banner image, About page link, QR popout, title hyperlink
- Directory: new about.html — Online Safety Act, Digital ID, 65k arrests stat, community rules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,10 @@ import os
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import mimetypes
|
||||
|
||||
from fastapi import FastAPI, Form, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
@@ -316,14 +318,61 @@ async def chat_send(request: Request, profile_id: int, chat_type: str, chat_id:
|
||||
text = data.get("text", "").strip()
|
||||
if not text:
|
||||
raise HTTPException(400, "text required")
|
||||
reply_to_id = data.get("reply_to_id")
|
||||
try:
|
||||
await pm.send_to_chat(profile_id, chat_type, chat_id, text)
|
||||
await pm.send_to_chat(profile_id, chat_type, chat_id, text, reply_to_id=reply_to_id)
|
||||
except Exception as e:
|
||||
log.error("chat send failed (profile=%s %s/%s): %s", profile_id, chat_type, chat_id, e)
|
||||
return JSONResponse({"ok": False, "error": str(e)})
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
@app.post("/api/profiles/{profile_id}/chat/{chat_type}/{chat_id}/react")
|
||||
async def chat_react(request: Request, profile_id: int, chat_type: str, chat_id: int):
|
||||
_require_auth(request)
|
||||
data = await request.json()
|
||||
item_id = data.get("item_id")
|
||||
emoji = data.get("emoji", "")
|
||||
add = data.get("add", True)
|
||||
if not item_id or not emoji:
|
||||
raise HTTPException(400, "item_id and emoji required")
|
||||
try:
|
||||
await pm.send_reaction(profile_id, chat_type, chat_id, item_id, emoji, add)
|
||||
except Exception as e:
|
||||
log.error("react failed: %s", e)
|
||||
return JSONResponse({"ok": False, "error": str(e)})
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
@app.post("/api/profiles/{profile_id}/file/{file_id}/receive")
|
||||
async def file_receive(request: Request, profile_id: int, file_id: int):
|
||||
_require_auth(request)
|
||||
try:
|
||||
await pm.accept_file(profile_id, file_id)
|
||||
except Exception as e:
|
||||
log.error("file receive failed: %s", e)
|
||||
return JSONResponse({"ok": False, "error": str(e)})
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
@app.get("/api/profiles/{profile_id}/file/{file_id}/download")
|
||||
async def file_download(request: Request, profile_id: int, file_id: int):
|
||||
_require_auth(request)
|
||||
try:
|
||||
data, filename = await pm.read_file_bytes(profile_id, file_id)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(404, str(e))
|
||||
except Exception as e:
|
||||
log.error("file download failed: %s", e)
|
||||
raise HTTPException(500, str(e))
|
||||
mime = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
||||
return Response(
|
||||
content=data,
|
||||
media_type=mime,
|
||||
headers={"Content-Disposition": f'inline; filename="{filename}"'},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/profiles/{profile_id}/chat/{chat_type}/{chat_id}/clear")
|
||||
async def chat_clear(request: Request, profile_id: int, chat_type: str, chat_id: int):
|
||||
_require_auth(request)
|
||||
@@ -425,6 +474,7 @@ async def profile_status(request: Request, profile_id: int):
|
||||
"contacts": len(bot.contacts) if bot else 0,
|
||||
"groups": len(bot.groups) if bot else 0,
|
||||
"log": bot.log_lines[-20:] if bot else [],
|
||||
"poll_next": bot.poll_next if bot else 0,
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user