Files
mindfulness/server/app/api/v1/legal.py
2026-02-25 17:46:59 +08:00

243 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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})