from __future__ import annotations from typing import Literal, Optional from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse from pydantic import BaseModel, HttpUrl from app.core.config import get_settings from app.legal_docs import ( PRIVACY_POLICY_MD, TERMS_OF_USE_MD, choose_content_by_lang, render_as_simple_html, split_bilingual_markdown, ) router = APIRouter(prefix="/v1/legal", tags=["legal"]) INTERNAL_SENTINEL = "__internal__" # 技术支持页文案(EN / TC) SUPPORT_MD = """Dear Mama | Technical Support Last updated: February 2026 If you need technical support for Dear Mama, please contact us: - Support email: leitinglan@project-c.org Support scope (including but not limited to): - App installation or update issues - App crashes, freezes, or abnormal behavior - Notification or widget related issues - Difficulty accessing privacy policy or terms pages To help us process your request faster, please include: - Device model and OS version - App version - A brief issue description and occurrence time - Screenshots or screen recording (if available) Service commitment: - We generally respond within 3-5 business days - Emergency availability may vary by holidays and local time --- Dear Mama|技術支援 最後更新日期:2026 年 2 月 如需 Dear Mama 的技術支援,請透過以下方式聯絡我們: - 支援信箱:leitinglan@project-c.org 支援範圍(包含但不限於): - App 安裝或更新問題 - App 閃退、卡頓或異常行為 - 通知或小組件相關問題 - 隱私政策或使用條款頁面無法開啟 為了加快處理,建議提供: - 裝置型號與系統版本 - App 版本號 - 問題描述與發生時間 - 截圖或螢幕錄影(如有) 服務承諾: - 一般會在 3-5 個工作日內回覆 - 節假日或時區差異時,回覆時間可能延長 """ # 当前多语言仅支持 EN / TC(繁体) ResolvedLang = Literal["en", "tc"] BILINGUAL_CONTENT_LANGUAGE = "en, zh-Hant" class LegalLinksResponse(BaseModel): privacyPolicyUrl: HttpUrl termsOfUseUrl: HttpUrl resolvedLang: ResolvedLang def _resolve_lang(accept_language: Optional[str]) -> ResolvedLang: """ 从 Accept-Language 里做一个轻量语言解析。 说明: - 只关心:en / tc(繁体) - 解析失败或缺失:回退 en """ if not accept_language: return "en" # 按语言优先级顺序逐项解析,避免“只要包含 zh 就全判 tc”。 # 例如:en-US,en;q=0.9,zh-CN;q=0.8 应命中 en,而不是 tc。 items = [seg.strip().lower() for seg in accept_language.split(",") if seg.strip()] for item in items: lang_tag = item.split(";", 1)[0].strip() if not lang_tag: continue if lang_tag == "tc": return "tc" if lang_tag.startswith(("zh-hant", "zh-tw", "zh-hk", "zh-mo")): return "tc" if lang_tag.startswith("zh"): return "tc" if lang_tag.startswith("en"): return "en" return "en" def _pick_urls(lang: ResolvedLang) -> tuple[str, str, ResolvedLang]: settings = get_settings() if lang == "tc": privacy = settings.legal_privacy_url_tc or settings.legal_privacy_url_en terms = settings.legal_terms_url_tc or settings.legal_terms_url_en # 如果未配置 tc 链接: # - 若走内置页面(__internal__),可直接展示 tc 内容,因此 resolved=tc # - 否则回退到 en 链接,因此 resolved=en if settings.legal_privacy_url_tc or settings.legal_terms_url_tc: resolved: ResolvedLang = "tc" elif privacy == INTERNAL_SENTINEL or terms == INTERNAL_SENTINEL: resolved = "tc" else: resolved = "en" return privacy, terms, resolved return settings.legal_privacy_url_en, settings.legal_terms_url_en, "en" def _join_base_url(base_url: str, path: str) -> str: base = (base_url or "").rstrip("/") p = (path or "").strip() if not p.startswith("/"): p = "/" + p return base + p def _normalize_internal_url(request: Request, url: str, internal_path: str) -> str: """ 将内置哨兵值替换为当前服务的可访问绝对 URL。 """ if url != INTERNAL_SENTINEL: return url return _join_base_url(str(request.base_url), internal_path) def _build_bilingual_content(text: str) -> str: """ 固定输出双语协议内容:EN 在前,TC 在后。 若缺少 TC,则仅返回 EN。 """ en, tc = split_bilingual_markdown(text) if not tc: return en return f"{en}\n\n---\n{tc}" @router.get("/links", response_model=LegalLinksResponse) async def get_legal_links(request: Request) -> LegalLinksResponse: """ 获取协议链接(隐私协议 / 使用协议)。 - 语言来源:Accept-Language - 默认兜底:en """ accept_language = request.headers.get("accept-language") lang = _resolve_lang(accept_language) privacy, terms, resolved = _pick_urls(lang) privacy = _normalize_internal_url(request, privacy, "/v1/legal/privacy") terms = _normalize_internal_url(request, terms, "/v1/legal/terms") return LegalLinksResponse(privacyPolicyUrl=privacy, termsOfUseUrl=terms, resolvedLang=resolved) @router.get("/privacy", response_class=HTMLResponse) async def get_privacy_policy(request: Request) -> HTMLResponse: """ 内置隐私协议页面(用于未配置外部托管链接时的兜底)。 """ accept_language = request.headers.get("accept-language") if accept_language: lang = _resolve_lang(accept_language) content, resolved = choose_content_by_lang(PRIVACY_POLICY_MD, lang) title = "Dear Mama | Privacy Policy" if resolved == "en" else "Dear Mama|隱私權政策" html_lang = "en" if resolved == "en" else "zh-Hant" page = render_as_simple_html(title=title, content=content, html_lang=html_lang) return HTMLResponse(content=page, headers={"Content-Language": html_lang}) content = _build_bilingual_content(PRIVACY_POLICY_MD) title = "Dear Mama | Privacy Policy / 隱私權政策" page = render_as_simple_html(title=title, content=content, html_lang="en") return HTMLResponse(content=page, headers={"Content-Language": BILINGUAL_CONTENT_LANGUAGE}) @router.get("/terms", response_class=HTMLResponse) async def get_terms_of_use(request: Request) -> HTMLResponse: """ 内置使用协议页面(用于未配置外部托管链接时的兜底)。 """ accept_language = request.headers.get("accept-language") if accept_language: lang = _resolve_lang(accept_language) content, resolved = choose_content_by_lang(TERMS_OF_USE_MD, lang) title = "Dear Mama – Terms of Use" if resolved == "en" else "Dear Mama 使用條款" html_lang = "en" if resolved == "en" else "zh-Hant" page = render_as_simple_html(title=title, content=content, html_lang=html_lang) return HTMLResponse(content=page, headers={"Content-Language": html_lang}) content = _build_bilingual_content(TERMS_OF_USE_MD) title = "Dear Mama – Terms of Use / 使用條款" page = render_as_simple_html(title=title, content=content, html_lang="en") return HTMLResponse(content=page, headers={"Content-Language": BILINGUAL_CONTENT_LANGUAGE}) @router.get("/support", response_class=HTMLResponse) async def get_support_page(request: Request) -> HTMLResponse: """ 技术支持页面(用于 App 审核的可访问 URL)。 """ accept_language = request.headers.get("accept-language") if accept_language: lang = _resolve_lang(accept_language) content, resolved = choose_content_by_lang(SUPPORT_MD, lang) title = "Dear Mama | Technical Support" if resolved == "en" else "Dear Mama|技術支援" html_lang = "en" if resolved == "en" else "zh-Hant" page = render_as_simple_html(title=title, content=content, html_lang=html_lang) return HTMLResponse(content=page, headers={"Content-Language": html_lang}) content = _build_bilingual_content(SUPPORT_MD) title = "Dear Mama | Technical Support / 技術支援" page = render_as_simple_html(title=title, content=content, html_lang="en") return HTMLResponse(content=page, headers={"Content-Language": BILINGUAL_CONTENT_LANGUAGE})