Rhinox13/chatapi
0
1from __future__ import annotations
2
3import os
4from dataclasses import dataclass
5from pathlib import Path
6from typing import Any
7
8
9def _repo_root() -> Path:
10 return Path(__file__).resolve().parents[1]
11
12
13def _parse_env_file(path: Path) -> dict[str, str]:
14 values: dict[str, str] = {}
15 if not path.exists():
16 return values
17 for raw_line in path.read_text(encoding="utf-8").splitlines():
18 line = raw_line.strip()
19 if not line or line.startswith("#") or "=" not in line:
20 continue
21 key, value = line.split("=", 1)
22 key = key.strip()
23 value = value.strip().strip('"').strip("'")
24 if key and key not in os.environ:
25 values[key] = value
26 return values
27
28
29def _load_env_files() -> None:
30 candidates = [
31 _repo_root() / ".env",
32 _repo_root() / "backend" / ".env",
33 ]
34 external_env_file = os.environ.get("CHATAPI_ENV_FILE", "").strip()
35 if external_env_file:
36 candidates.append(Path(external_env_file).expanduser())
37 merged: dict[str, str] = {}
38 for candidate in candidates:
39 merged.update(_parse_env_file(candidate))
40 for key, value in merged.items():
41 os.environ.setdefault(key, value)
42
43
44_load_env_files()
45
46
47def _first_non_empty(*keys: str, default: str = "") -> str:
48 for key in keys:
49 value = os.environ.get(key, "").strip()
50 if value:
51 return value
52 return default
53
54
55def _split_csv(raw: str) -> list[str]:
56 return [item.strip() for item in raw.split(",") if item.strip()]
57
58
59@dataclass(frozen=True)
60class Settings:
61 admin_username: str
62 admin_password: str
63 session_secret: str
64 data_dir: Path
65 db_path: Path
66 uploads_img_dir: Path
67 cors_origins: list[str]
68 host: str
69 port: int
70 debug: bool
71 tls_cert_file: Path | None
72 tls_key_file: Path | None
73 web_dist_dir: Path | None
74 smtp_host: str
75 smtp_port: int
76 smtp_username: str
77 smtp_password: str
78 email_from: str
79 smtp_use_tls: bool
80 resend_api_key: str
81 brevo_api_key: str
82 brevo_from_name: str
83 tencentcloud_secret_id: str
84 tencentcloud_secret_key: str
85 tencentcloud_ses_region: str
86 tencentcloud_template_id: str
87 geetest_captcha_id: str
88 geetest_captcha_key: str
89 geetest_api_server: str
90
91 @classmethod
92 def from_env(cls) -> "Settings":
93 repo_root = _repo_root()
94 data_dir = Path(_first_non_empty("CHATAPI_DATA_DIR", "DATA_DIR", default=str(repo_root / "data")))
95 if not data_dir.is_absolute():
96 data_dir = (repo_root / data_dir).resolve()
97 db_path = Path(_first_non_empty("CHATAPI_DB_PATH", default=str(data_dir / "chatapi.sqlite3")))
98 if not db_path.is_absolute():
99 db_path = (repo_root / db_path).resolve()
100 uploads_img_dir = data_dir / "uploads" / "imgs"
101 cors_raw = _first_non_empty(
102 "CHATAPI_CORS_ORIGINS",
103 "CORS_ORIGINS",
104 default="http://localhost:5173,http://127.0.0.1:5173",
105 )
106 tls_cert_raw = _first_non_empty(
107 "CHATAPI_TLS_CERT_FILE",
108 "TLS_CERT_FILE",
109 default="",
110 )
111 tls_key_raw = _first_non_empty(
112 "CHATAPI_TLS_KEY_FILE",
113 "TLS_KEY_FILE",
114 default="",
115 )
116 web_dist_raw = _first_non_empty(
117 "CHATAPI_WEB_DIST_DIR",
118 "WEB_DIST_DIR",
119 default="",
120 )
121
122 def _resolve_optional_path(raw: str) -> Path | None:
123 if not raw:
124 return None
125 path = Path(raw)
126 if not path.is_absolute():
127 path = (repo_root / path).resolve()
128 return path
129
130 return cls(
131 admin_username=_first_non_empty("CHATAPI_ADMIN_USERNAME", "CHATAPI_USERNAME", "ADMIN_USERNAME", default="admin"),
132 admin_password=_first_non_empty("CHATAPI_ADMIN_PASSWORD", "CHATAPI_PASSWORD", "ADMIN_PASSWORD", default="change-me"),
133 session_secret=_first_non_empty(
134 "CHATAPI_SESSION_SECRET",
135 "ADMIN_SESSION_SECRET",
136 default="",
137 ),
138 data_dir=data_dir,
139 db_path=db_path,
140 uploads_img_dir=uploads_img_dir,
141 cors_origins=_split_csv(cors_raw),
142 host=_first_non_empty("CHATAPI_HOST", "BACKEND_HOST", default="0.0.0.0"),
143 port=int(_first_non_empty("CHATAPI_PORT", "BACKEND_PORT", default="5000")),
144 debug=_first_non_empty("CHATAPI_DEBUG", "FLASK_DEBUG", default="0") == "1",
145 tls_cert_file=_resolve_optional_path(tls_cert_raw),
146 tls_key_file=_resolve_optional_path(tls_key_raw),
147 web_dist_dir=_resolve_optional_path(web_dist_raw),
148 smtp_host=_first_non_empty("CHATAPI_SMTP_HOST", "SMTP_HOST", default=""),
149 smtp_port=int(_first_non_empty("CHATAPI_SMTP_PORT", "SMTP_PORT", default="587")),
150 smtp_username=_first_non_empty("CHATAPI_SMTP_USERNAME", "SMTP_USERNAME", default=""),
151 smtp_password=_first_non_empty("CHATAPI_SMTP_PASSWORD", "SMTP_PASSWORD", default=""),
152 email_from=_first_non_empty("CHATAPI_EMAIL_FROM", default=""),
153 smtp_use_tls=_first_non_empty("CHATAPI_SMTP_USE_TLS", "SMTP_USE_TLS", default="1") == "1",
154 resend_api_key=_first_non_empty("CHATAPI_RESEND_API_KEY", "RESEND_API_KEY", default=""),
155 brevo_api_key=_first_non_empty("CHATAPI_BREVO_API_KEY", "BREVO_API_KEY", default=""),
156 brevo_from_name=_first_non_empty("CHATAPI_BREVO_FROM_NAME", "BREVO_FROM_NAME", default="ChatAPI"),
157 tencentcloud_secret_id=_first_non_empty(
158 "CHATAPI_TENCENTCLOUD_SECRET_ID",
159 "TENCENTCLOUD_SECRET_ID",
160 default="",
161 ),
162 tencentcloud_secret_key=_first_non_empty(
163 "CHATAPI_TENCENTCLOUD_SECRET_KEY",
164 "TENCENTCLOUD_SECRET_KEY",
165 default="",
166 ),
167 tencentcloud_ses_region=_first_non_empty(
168 "CHATAPI_TENCENTCLOUD_SES_REGION",
169 "TENCENTCLOUD_SES_REGION",
170 default="ap-guangzhou",
171 ),
172 tencentcloud_template_id=_first_non_empty(
173 "CHATAPI_TENCENTCLOUD_TEMPLATE_ID",
174 "TENCENTCLOUD_TEMPLATE_ID",
175 default="",
176 ),
177 geetest_captcha_id=_first_non_empty("CHATAPI_GEETEST_CAPTCHA_ID", "GEETEST_CAPTCHA_ID", default=""),
178 geetest_captcha_key=_first_non_empty("CHATAPI_GEETEST_CAPTCHA_KEY", "GEETEST_CAPTCHA_KEY", default=""),
179 geetest_api_server=_first_non_empty("CHATAPI_GEETEST_API_SERVER", "GEETEST_API_SERVER", default="http://gcaptcha4.geetest.com"),
180 )
181
182
183settings = Settings.from_env()
184 