Files
flowdeck/app/routers/emoji.py
T
bruno da3e09c215
FlowDeck CI / lint (push) Successful in 45s
FlowDeck CI / test (push) Failing after 3h6m55s
FlowDeck CI / docker (push) Skipped
feat(icon): Notion-style icon picker + custom workspace emojis; fix side peek file content & ctx menu overflow
2026-09-12 20:05:53 -04:00

101 lines
3.5 KiB
Python

"""FlowDeck — custom workspace emojis (v5.6.0).
Uploaded emoji images stored per-workspace and usable as page icons. Kept on a
prefix-less router so the paths stay ``/api/custom-emojis`` (the board router's
``/{owner}/{repo}`` HTML catch-all would otherwise shadow them).
"""
from __future__ import annotations
import datetime
import re
from pathlib import Path
from fastapi import APIRouter, HTTPException, Request
from app.db import get_conn
router = APIRouter(tags=["emojis"])
_IMAGE_EXTS = {"png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "avif"}
def _upload_root() -> Path:
import os
return Path(os.environ.get("FLOWDECK_DATA_DIR", "/data"))
def _active_ws(request: Request) -> int:
"""Workspace id from the active-workspace cookie, fallback 1."""
try:
ws_id = int(request.cookies.get("flowdeck_workspace", "") or 0)
if ws_id > 0:
return ws_id
except (ValueError, TypeError):
pass
return 1
@router.get("/api/custom-emojis")
async def list_custom_emojis(request: Request):
"""List the current workspace's custom emojis."""
ws_id = _active_ws(request)
with get_conn() as conn:
rows = conn.execute(
"SELECT id, name, url FROM custom_emojis WHERE workspace_id=? ORDER BY created_at DESC",
(ws_id,),
).fetchall()
return {"status": "ok", "emojis": [dict(r) for r in rows]}
@router.post("/api/custom-emojis")
async def create_custom_emoji(request: Request):
"""Upload a custom emoji image (multipart: ``name`` + ``file``)."""
form = await request.form()
name = (form.get("name") or "").strip()[:40] or "emoji"
upload = form.get("file")
if upload is None or not hasattr(upload, "filename"):
raise HTTPException(400, "file field required")
original = (upload.filename or "emoji.png").replace("\\", "/").rsplit("/", 1)[-1]
safe = re.sub(r"[^A-Za-z0-9._-]", "_", original)[:80]
ext = safe.rsplit(".", 1)[-1].lower() if "." in safe else "png"
if ext not in _IMAGE_EXTS:
raise HTTPException(400, "Unsupported image format")
ws_id = _active_ws(request)
stamp = datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S%f")
folder = _upload_root() / f"uploads/workspace_{ws_id}"
folder.mkdir(parents=True, exist_ok=True)
final = f"emoji_{stamp}_{safe}"
(folder / final).write_bytes(await upload.read())
url = f"/api/files/{ws_id}/{final}"
with get_conn() as conn:
cur = conn.execute(
"INSERT INTO custom_emojis (workspace_id, name, url) VALUES (?, ?, ?)",
(ws_id, name, url),
)
conn.commit()
emoji_id = cur.lastrowid
return {"status": "ok", "emoji": {"id": emoji_id, "name": name, "url": url}}
@router.delete("/api/custom-emojis/{emoji_id}")
async def delete_custom_emoji(request: Request, emoji_id: int):
"""Delete a custom emoji (and its stored file)."""
ws_id = _active_ws(request)
with get_conn() as conn:
row = conn.execute(
"SELECT url FROM custom_emojis WHERE id=? AND workspace_id=?",
(emoji_id, ws_id),
).fetchone()
if not row:
raise HTTPException(404, "emoji not found")
conn.execute("DELETE FROM custom_emojis WHERE id=?", (emoji_id,))
conn.commit()
try:
fname = (row["url"] or "").rsplit("/", 1)[-1]
if fname:
(_upload_root() / f"uploads/workspace_{ws_id}" / fname).unlink(missing_ok=True)
except OSError:
pass
return {"status": "ok", "id": emoji_id}