Feedback reply: light Markdown formatting (bold / bullets / heading)
Per Codex: a constrained Markdown-ish composer rather than contenteditable. * goodnews/markup.render_reply_html — escapes everything first, then introduces only a tiny whitelist (**bold**, - bullets, #/##/### headings, paragraphs, line breaks). No links, attributes, inline styles, or raw HTML passthrough. * feedback_replies.message_html column (+ live migration); replies store both the Markdown text and the rendered HTML. * email_send.send_feedback_reply now sends multipart text/plain + text/html (the sanitized render, wrapped in a trusted email template). * Frontend: textarea + a small toolbar (Bold / • List / H) that inserts Markdown; the reply thread renders the server-sanitized HTML. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+8
-5
@@ -32,6 +32,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
|
||||
from . import auth, email_send, feeds, oauth_google, queries, share, summarize
|
||||
from .markup import render_reply_html
|
||||
from .db import connect
|
||||
from .filters import filter_articles, prefs_from_json
|
||||
from .hero import safe_to_lead
|
||||
@@ -808,7 +809,7 @@ def create_app() -> FastAPI:
|
||||
ids = [it["id"] for it in items]
|
||||
ph = ",".join("?" * len(ids))
|
||||
reps = conn.execute(
|
||||
f"SELECT id, feedback_id, message, sent_to, sent_at FROM feedback_replies "
|
||||
f"SELECT id, feedback_id, message, message_html, sent_to, sent_at FROM feedback_replies "
|
||||
f"WHERE feedback_id IN ({ph}) ORDER BY sent_at",
|
||||
ids,
|
||||
).fetchall()
|
||||
@@ -855,24 +856,26 @@ def create_app() -> FastAPI:
|
||||
if not fb["contact_email"]:
|
||||
raise HTTPException(status_code=400, detail="No reply address for this feedback.")
|
||||
admin_id, to, original = admin["id"], fb["contact_email"], fb["message"]
|
||||
reply_html = render_reply_html(message) # tiny safe Markdown subset
|
||||
# 2. Send with no DB connection held; only record a reply that actually
|
||||
# went out, so the UI can keep the draft on failure.
|
||||
try:
|
||||
email_send.send_feedback_reply(to, message, original)
|
||||
email_send.send_feedback_reply(to, message, reply_html, original)
|
||||
except Exception as exc: # noqa: BLE001 — surface any SMTP failure to the admin
|
||||
raise HTTPException(status_code=502, detail=f"Could not send the reply: {exc}")
|
||||
# 3. Record the sent reply + mark the item read.
|
||||
with get_conn() as conn:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO feedback_replies (feedback_id, user_id, message, sent_to) VALUES (?, ?, ?, ?)",
|
||||
(fid, admin_id, message, to),
|
||||
"INSERT INTO feedback_replies (feedback_id, user_id, message, message_html, sent_to) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(fid, admin_id, message, reply_html, to),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE feedback SET read_at = COALESCE(read_at, CURRENT_TIMESTAMP) WHERE id = ?", (fid,)
|
||||
)
|
||||
conn.commit()
|
||||
reply = conn.execute(
|
||||
"SELECT id, feedback_id, message, sent_to, sent_at FROM feedback_replies WHERE id = ?",
|
||||
"SELECT id, feedback_id, message, message_html, sent_to, sent_at FROM feedback_replies WHERE id = ?",
|
||||
(cur.lastrowid,),
|
||||
).fetchone()
|
||||
return {"ok": True, "reply": dict(reply)}
|
||||
|
||||
Reference in New Issue
Block a user