Skip to content
This repository was archived by the owner on Oct 8, 2026. It is now read-only.

Commit d257418

Browse files
authored
Merge pull request #16 from kaiden694/dev
feat(mail): add Cloud Mail provider + fix registration HTTP timeout
2 parents a758017 + 6018bac commit d257418

17 files changed

Lines changed: 14223 additions & 69 deletions

‎.env.example‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ GROK2API_DELETE_INVALID_REFRESH=1
194194
# GROK2API_SSO_HTTP_TIMEOUT=12
195195

196196
# ── Protocol registration (optional) ──────────────────
197-
# Mail provider: moemail | yyds | gptmail (keys are stored separately in DB)
197+
# Mail provider: moemail | yyds | gptmail | cfmail | tempmail | cloudmail (keys are stored separately in DB)
198198
# GROK2API_MAIL_PROVIDER=moemail
199199
# MoeMail (self-hosted base URL required):
200200
# GROK2API_MOEMAIL_API_KEY=
@@ -210,6 +210,12 @@ GROK2API_DELETE_INVALID_REFRESH=1
210210
# GROK2API_CFMAIL_BASE_URL=https://your-worker.example.workers.dev
211211
# GROK2API_CFMAIL_API_KEY=your-admin-password
212212
# GROK2API_CFMAIL_DOMAIN=example.com
213+
# Cloud Mail (maillab/cloud-mail — self-hosted Worker origin + admin_email:password):
214+
# https://github.com/maillab/cloud-mail
215+
# GROK2API_MAIL_PROVIDER=cloudmail
216+
# GROK2API_CLOUDMAIL_BASE_URL=https://skymail.example.com
217+
# GROK2API_CLOUDMAIL_API_KEY=admin@example.com:your-admin-password
218+
# GROK2API_CLOUDMAIL_DOMAIN=example.com
213219
# Captcha provider: local (inline in main container) | yescaptcha
214220
# GROK2API_CAPTCHA_PROVIDER=local
215221
# Inline solver default (same container loopback):

‎grok2api/admin/settings_store.py‎

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,19 +1833,22 @@ def apply_runtime_settings_to_modules() -> None:
18331833
# Per-provider base URLs for self-hosted services (do not share one field).
18341834
"moemail_base_url",
18351835
"cfmail_base_url",
1836+
"cloudmail_base_url",
18361837
# Active key (derived from selected provider). Kept for adapter/env compat.
18371838
"api_key",
18381839
# Per-provider secrets — all persist in DB so switching provider keeps keys.
18391840
"moemail_api_key",
18401841
"yyds_api_key",
18411842
"gptmail_api_key",
18421843
"cfmail_api_key",
1844+
"cloudmail_api_key",
18431845
# Active domain + per-provider domains (same pattern as keys).
18441846
"domain",
18451847
"moemail_domain",
18461848
"yyds_domain",
18471849
"gptmail_domain",
18481850
"cfmail_domain",
1851+
"cloudmail_domain",
18491852
"prefix",
18501853
"expiry_ms",
18511854
"captcha_provider",
@@ -1868,6 +1871,7 @@ def apply_runtime_settings_to_modules() -> None:
18681871
"yyds_api_key",
18691872
"gptmail_api_key",
18701873
"cfmail_api_key",
1874+
"cloudmail_api_key",
18711875
"yescaptcha_key",
18721876
"proxy_password",
18731877
}
@@ -1878,20 +1882,23 @@ def apply_runtime_settings_to_modules() -> None:
18781882
"yyds": "yyds_api_key",
18791883
"gptmail": "gptmail_api_key",
18801884
"cfmail": "cfmail_api_key",
1885+
"cloudmail": "cloudmail_api_key",
18811886
}
18821887

18831888
_MAIL_PROVIDER_DOMAIN_FIELDS = {
18841889
"moemail": "moemail_domain",
18851890
"yyds": "yyds_domain",
18861891
"gptmail": "gptmail_domain",
18871892
"cfmail": "cfmail_domain",
1893+
"cloudmail": "cloudmail_domain",
18881894
}
18891895

18901896
# Self-hosted providers keep independent base URLs so switching never overwrites
1891-
# another provider's host (MoeMail vs Cloudflare Temp Email).
1897+
# another provider's host (MoeMail vs Cloudflare Temp Email vs Cloud Mail).
18921898
_MAIL_PROVIDER_BASE_URL_FIELDS = {
18931899
"moemail": "moemail_base_url",
18941900
"cfmail": "cfmail_base_url",
1901+
"cloudmail": "cloudmail_base_url",
18951902
}
18961903

18971904

@@ -1983,6 +1990,27 @@ def _env_registration_defaults() -> dict[str, Any]:
19831990
if cf_base:
19841991
# Dedicated CF host only — never overwrite MoeMail base_url.
19851992
out["cfmail_base_url"] = cf_base
1993+
cm_key = (
1994+
os.environ.get("GROK2API_CLOUDMAIL_API_KEY")
1995+
or os.environ.get("CLOUDMAIL_API_KEY")
1996+
or ""
1997+
).strip()
1998+
if cm_key:
1999+
out["cloudmail_api_key"] = cm_key
2000+
cm_base = (
2001+
os.environ.get("GROK2API_CLOUDMAIL_BASE_URL")
2002+
or os.environ.get("CLOUDMAIL_BASE_URL")
2003+
or ""
2004+
).strip()
2005+
if cm_base:
2006+
out["cloudmail_base_url"] = cm_base
2007+
cm_dom = (
2008+
os.environ.get("GROK2API_CLOUDMAIL_DOMAIN")
2009+
or os.environ.get("CLOUDMAIL_DOMAIN")
2010+
or ""
2011+
).strip().lstrip("@").strip(".")
2012+
if cm_dom:
2013+
out["cloudmail_domain"] = cm_dom
19862014
# Prefer multi-line pool env when present; keep single-proxy fallback.
19872015
pool_env = (
19882016
os.environ.get("GROK2API_XAI_PROXY_POOL")
@@ -2078,11 +2106,13 @@ def _pick_domain(key: str) -> str:
20782106
legacy_base_url = _pick_str("base_url", 256)
20792107
cfg["moemail_base_url"] = _pick_str("moemail_base_url", 256)
20802108
cfg["cfmail_base_url"] = _pick_str("cfmail_base_url", 256)
2109+
cfg["cloudmail_base_url"] = _pick_str("cloudmail_base_url", 256)
20812110
legacy_api_key = _pick_str("api_key", 512)
20822111
cfg["moemail_api_key"] = _pick_str("moemail_api_key", 512)
20832112
cfg["yyds_api_key"] = _pick_str("yyds_api_key", 512)
20842113
cfg["gptmail_api_key"] = _pick_str("gptmail_api_key", 512)
20852114
cfg["cfmail_api_key"] = _pick_str("cfmail_api_key", 512)
2115+
cfg["cloudmail_api_key"] = _pick_str("cloudmail_api_key", 512)
20862116
# Do NOT env-fill legacy domain into every provider — only use explicit src.
20872117
if "domain" in src:
20882118
legacy_domain = str(src.get("domain") or "").strip().lstrip("@").strip(".")[:128]
@@ -2092,10 +2122,12 @@ def _pick_domain(key: str) -> str:
20922122
cfg["yyds_domain"] = _pick_domain("yyds_domain")
20932123
cfg["gptmail_domain"] = _pick_domain("gptmail_domain")
20942124
cfg["cfmail_domain"] = _pick_domain("cfmail_domain")
2125+
cfg["cloudmail_domain"] = _pick_domain("cloudmail_domain")
20952126
cfg["prefix"] = _pick_str("prefix", 64)
20962127
try:
20972128
from grok2api.upstream.moemail import (
20982129
normalize_cfmail_base_url,
2130+
normalize_cloudmail_base_url,
20992131
normalize_gptmail_base_url,
21002132
normalize_mail_provider,
21012133
normalize_yyds_base_url,
@@ -2105,13 +2137,15 @@ def _pick_domain(key: str) -> str:
21052137
normalize_yyds_base_url = None # type: ignore[assignment]
21062138
normalize_gptmail_base_url = None # type: ignore[assignment]
21072139
normalize_cfmail_base_url = None # type: ignore[assignment]
2140+
normalize_cloudmail_base_url = None # type: ignore[assignment]
21082141

21092142
mail_raw = _pick_str("mail_provider", 32).lower()
21102143
# Prefer explicit mail_provider; only use base_url as a hint when empty.
21112144
if normalize_mail_provider is not None:
21122145
cfg["mail_provider"] = normalize_mail_provider(
21132146
mail_raw or None,
21142147
base_url=legacy_base_url
2148+
or cfg.get("cloudmail_base_url")
21152149
or cfg.get("cfmail_base_url")
21162150
or cfg.get("moemail_base_url")
21172151
or None,
@@ -2123,10 +2157,12 @@ def _pick_domain(key: str) -> str:
21232157
cfg["mail_provider"] = "gptmail"
21242158
elif mail_raw in {"cfmail", "cloudflare", "cloudflare_temp_email", "awsl"}:
21252159
cfg["mail_provider"] = "cfmail"
2160+
elif mail_raw in {"cloudmail", "cloud-mail", "skymail", "maillab"}:
2161+
cfg["mail_provider"] = "cloudmail"
21262162
else:
21272163
cfg["mail_provider"] = (
21282164
mail_raw
2129-
if mail_raw in {"moemail", "yyds", "gptmail", "cfmail"}
2165+
if mail_raw in {"moemail", "yyds", "gptmail", "cfmail", "cloudmail"}
21302166
else "moemail"
21312167
)
21322168

@@ -2179,11 +2215,13 @@ def _pick_domain(key: str) -> str:
21792215
# selected self-hosted provider so adapter/env keep working.
21802216
if normalize_cfmail_base_url is not None and cfg.get("cfmail_base_url"):
21812217
cfg["cfmail_base_url"] = normalize_cfmail_base_url(cfg["cfmail_base_url"])
2218+
if normalize_cloudmail_base_url is not None and cfg.get("cloudmail_base_url"):
2219+
cfg["cloudmail_base_url"] = normalize_cloudmail_base_url(cfg["cloudmail_base_url"])
21822220
if cfg.get("moemail_base_url"):
21832221
cfg["moemail_base_url"] = str(cfg["moemail_base_url"]).strip().rstrip("/")
21842222

21852223
# YYDS / GPTMail use fixed official hosts — no user URL required.
2186-
# CF Temp Email / MoeMail are self-hosted and keep independent slots.
2224+
# CF Temp Email / MoeMail / Cloud Mail are self-hosted and keep independent slots.
21872225
if cfg["mail_provider"] == "yyds":
21882226
cfg["base_url"] = (
21892227
normalize_yyds_base_url(None)
@@ -2204,6 +2242,15 @@ def _pick_domain(key: str) -> str:
22042242
else (str(raw_base).strip() or "https://temp-email-api.awsl.uk")
22052243
)
22062244
cfg["base_url"] = cfg["cfmail_base_url"]
2245+
elif cfg["mail_provider"] == "cloudmail":
2246+
raw_base = cfg.get("cloudmail_base_url") or ""
2247+
if normalize_cloudmail_base_url is not None:
2248+
cfg["cloudmail_base_url"] = normalize_cloudmail_base_url(
2249+
str(raw_base) if raw_base else None
2250+
)
2251+
else:
2252+
cfg["cloudmail_base_url"] = str(raw_base).strip().rstrip("/")
2253+
cfg["base_url"] = cfg["cloudmail_base_url"]
22072254
else:
22082255
# MoeMail: active base_url mirrors moemail_base_url.
22092256
cfg["base_url"] = str(cfg.get("moemail_base_url") or "").strip()
@@ -2314,13 +2361,15 @@ def get_registration_config(*, include_secrets: bool = True) -> dict[str, Any]:
23142361
has_yyds = bool(cfg.get("yyds_api_key"))
23152362
has_gpt = bool(cfg.get("gptmail_api_key"))
23162363
has_cf = bool(cfg.get("cfmail_api_key"))
2364+
has_cm = bool(cfg.get("cloudmail_api_key"))
23172365
has_active = bool(cfg.get("api_key"))
23182366
public["configured"] = {
23192367
"moemail": has_moemail,
23202368
"yyds": has_yyds,
23212369
"gptmail": has_gpt,
23222370
"cfmail": has_cf,
2323-
"mail": has_active or has_moemail or has_yyds or has_gpt or has_cf,
2371+
"cloudmail": has_cm,
2372+
"mail": has_active or has_moemail or has_yyds or has_gpt or has_cf or has_cm,
23242373
"yescaptcha": bool(cfg.get("yescaptcha_key")),
23252374
"local_solver": bool(cfg.get("local_solver_url")),
23262375
"captcha": (
@@ -2537,14 +2586,17 @@ def set_registration_config(
25372586
"yyds_domain",
25382587
"gptmail_domain",
25392588
"cfmail_domain",
2589+
"cloudmail_domain",
25402590
"api_key",
25412591
"moemail_api_key",
25422592
"yyds_api_key",
25432593
"gptmail_api_key",
25442594
"cfmail_api_key",
2595+
"cloudmail_api_key",
25452596
"base_url",
25462597
"moemail_base_url",
25472598
"cfmail_base_url",
2599+
"cloudmail_base_url",
25482600
# Proxy pool: empty means "cleared" so env cannot revive a deleted list.
25492601
"proxy",
25502602
"proxy_username",
@@ -2560,13 +2612,13 @@ def set_registration_config(
25602612
for k in ("expiry_ms", "count", "concurrency", "stagger_ms", "probe_delay_sec"):
25612613
cleaned[k] = cfg[k]
25622614
# Always persist active + per-provider domain slots (including empty).
2563-
for k in ("domain", "moemail_domain", "yyds_domain", "gptmail_domain", "cfmail_domain"):
2615+
for k in ("domain", "moemail_domain", "yyds_domain", "gptmail_domain", "cfmail_domain", "cloudmail_domain"):
25642616
cleaned[k] = str(cfg.get(k) or "").strip().lstrip("@").strip(".")
25652617
# Always persist active + per-provider keys (including empty after clear).
2566-
for k in ("api_key", "moemail_api_key", "yyds_api_key", "gptmail_api_key", "cfmail_api_key"):
2618+
for k in ("api_key", "moemail_api_key", "yyds_api_key", "gptmail_api_key", "cfmail_api_key", "cloudmail_api_key"):
25672619
cleaned[k] = str(cfg.get(k) or "").strip()
25682620
# Always persist active + per-provider base URLs (including empty after clear).
2569-
for k in ("base_url", "moemail_base_url", "cfmail_base_url"):
2621+
for k in ("base_url", "moemail_base_url", "cfmail_base_url", "cloudmail_base_url"):
25702622
cleaned[k] = str(cfg.get(k) or "").strip().rstrip("/")
25712623

25722624
_set_setting_value("registration_config", cleaned)
@@ -2590,19 +2642,22 @@ def _set_env(name: str, value: str) -> None:
25902642
os.environ.pop(name, None)
25912643

25922644
mail_provider = str(cfg.get("mail_provider") or "moemail").strip().lower()
2593-
if mail_provider not in {"moemail", "yyds", "gptmail", "cfmail"}:
2645+
if mail_provider not in {"moemail", "yyds", "gptmail", "cfmail", "cloudmail"}:
25942646
mail_provider = "moemail"
25952647
# Prefer per-provider key; fall back to legacy api_key.
25962648
slot = _MAIL_PROVIDER_KEY_FIELDS.get(mail_provider, "moemail_api_key")
25972649
api_key = str(cfg.get(slot) or cfg.get("api_key") or "").strip()
25982650
moe_base = str(cfg.get("moemail_base_url") or "").strip().rstrip("/")
25992651
cf_base = str(cfg.get("cfmail_base_url") or "").strip().rstrip("/")
2652+
cm_base = str(cfg.get("cloudmail_base_url") or "").strip().rstrip("/")
26002653
if mail_provider == "yyds":
26012654
base_url = "https://maliapi.215.im"
26022655
elif mail_provider == "gptmail":
26032656
base_url = "https://mail.chatgpt.org.uk"
26042657
elif mail_provider == "cfmail":
26052658
base_url = cf_base or str(cfg.get("base_url") or "").strip().rstrip("/")
2659+
elif mail_provider == "cloudmail":
2660+
base_url = cm_base or str(cfg.get("base_url") or "").strip().rstrip("/")
26062661
else:
26072662
base_url = moe_base or str(cfg.get("base_url") or "").strip().rstrip("/")
26082663
dslot = _MAIL_PROVIDER_DOMAIN_FIELDS.get(mail_provider, "moemail_domain")
@@ -2631,28 +2686,41 @@ def _set_env(name: str, value: str) -> None:
26312686
ykey = str(cfg.get("yyds_api_key") or "").strip()
26322687
gkey = str(cfg.get("gptmail_api_key") or "").strip()
26332688
ckey = str(cfg.get("cfmail_api_key") or "").strip()
2689+
cmkey = str(cfg.get("cloudmail_api_key") or "").strip()
26342690
_set_env("GROK2API_MOEMAIL_ONLY_API_KEY", mkey)
26352691
_set_env("GROK2API_YYDS_API_KEY", ykey)
26362692
_set_env("YYDS_API_KEY", ykey)
26372693
_set_env("GROK2API_GPTMAIL_API_KEY", gkey)
26382694
_set_env("GPTMAIL_API_KEY", gkey)
26392695
_set_env("GROK2API_CFMAIL_API_KEY", ckey)
26402696
_set_env("CFMAIL_API_KEY", ckey)
2641-
# Keep MoeMail / CF hosts in dedicated env slots — never overwrite one with
2642-
# the other when switching the active provider.
2697+
_set_env("GROK2API_CLOUDMAIL_API_KEY", cmkey)
2698+
_set_env("CLOUDMAIL_API_KEY", cmkey)
2699+
# Keep MoeMail / CF / Cloud Mail hosts in dedicated env slots — never
2700+
# overwrite one with the other when switching the active provider.
26432701
_set_env("GROK2API_MOEMAIL_BASE_URL", moe_base if mail_provider == "moemail" else moe_base)
26442702
_set_env("GROK2API_CFMAIL_BASE_URL", cf_base)
26452703
_set_env("CFMAIL_BASE_URL", cf_base)
2704+
_set_env("GROK2API_CLOUDMAIL_BASE_URL", cm_base)
2705+
_set_env("CLOUDMAIL_BASE_URL", cm_base)
26462706
if base_url:
26472707
# Active adapter host (helpers still read MOEMAIL_BASE_URL historically).
2648-
_set_env("GROK2API_MOEMAIL_BASE_URL", base_url if mail_provider != "cfmail" else moe_base)
2708+
_set_env("GROK2API_MOEMAIL_BASE_URL", base_url if mail_provider not in {"cfmail", "cloudmail"} else moe_base)
26492709
if mail_provider == "cfmail":
26502710
_set_env("GROK2API_CFMAIL_BASE_URL", base_url)
26512711
_set_env("CFMAIL_BASE_URL", base_url)
26522712
# Active helpers fall back to MOEMAIL_BASE_URL — mirror CF host only
26532713
# while CF is selected so create/fetch hit the right Workers URL.
26542714
_set_env("GROK2API_MOEMAIL_BASE_URL", base_url)
26552715
_set_env("MOEMAIL_BASE_URL", base_url)
2716+
elif mail_provider == "cloudmail":
2717+
_set_env("GROK2API_CLOUDMAIL_BASE_URL", base_url)
2718+
_set_env("CLOUDMAIL_BASE_URL", base_url)
2719+
# Active helpers fall back to MOEMAIL_BASE_URL — mirror Cloud Mail
2720+
# host only while Cloud Mail is selected so create/fetch hit the
2721+
# right Worker origin.
2722+
_set_env("GROK2API_MOEMAIL_BASE_URL", base_url)
2723+
_set_env("MOEMAIL_BASE_URL", base_url)
26562724
elif mail_provider == "moemail":
26572725
_set_env("MOEMAIL_BASE_URL", base_url)
26582726
_set_env("GROK2API_MOEMAIL_DOMAIN", domain if mail_provider == "moemail" else str(cfg.get("moemail_domain") or "").strip())
@@ -2663,6 +2731,13 @@ def _set_env(name: str, value: str) -> None:
26632731
# Keep dedicated CF domain env in sync with stored slot even when inactive.
26642732
_set_env("GROK2API_CFMAIL_DOMAIN", str(cfg.get("cfmail_domain") or "").strip())
26652733
_set_env("CFMAIL_DOMAIN", str(cfg.get("cfmail_domain") or "").strip())
2734+
if mail_provider == "cloudmail":
2735+
_set_env("GROK2API_CLOUDMAIL_DOMAIN", domain)
2736+
_set_env("CLOUDMAIL_DOMAIN", domain)
2737+
else:
2738+
# Keep dedicated Cloud Mail domain env in sync with stored slot even when inactive.
2739+
_set_env("GROK2API_CLOUDMAIL_DOMAIN", str(cfg.get("cloudmail_domain") or "").strip())
2740+
_set_env("CLOUDMAIL_DOMAIN", str(cfg.get("cloudmail_domain") or "").strip())
26662741
try:
26672742
probe_delay = int(cfg.get("probe_delay_sec", 30))
26682743
except (TypeError, ValueError):

‎grok2api/upstream/grok_build_adapter.py‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,15 @@ def _resolve_mail_credentials(
402402
if cfg.get("cfmail_domain") not in (None, ""):
403403
dom = str(cfg.get("cfmail_domain") or "").strip()
404404
base = str(cfg.get("cfmail_base_url") or base).strip()
405+
elif prov == "cloudmail":
406+
# Cloud Mail (maillab/cloud-mail): admin_email:password credential +
407+
# self-hosted Worker origin. The active key/base were mirrored into
408+
# moemail_* slots by registration_service.start_job; on resume, also
409+
# consult the dedicated cloudmail_* slots in the reg_config snapshot.
410+
key = str(cfg.get("cloudmail_api_key") or key).strip()
411+
if cfg.get("cloudmail_domain") not in (None, ""):
412+
dom = str(cfg.get("cloudmail_domain") or "").strip()
413+
base = str(cfg.get("cloudmail_base_url") or base).strip()
405414
elif prov == "tempmail":
406415
# Free tier: no key. Optional Bearer key for Plus/Ultra.
407416
key = str(cfg.get("tempmail_api_key") or key).strip()
@@ -1336,17 +1345,19 @@ def _make_email_receiver(
13361345
"Save the key in 协议注册配置 (each provider keeps its own key; TempMail.lol free needs none)."
13371346
)
13381347
# Multi-domain config (newlines/commas): rotate by domain_index in batch jobs.
1339-
# Empty list => provider auto-pick (YYDS/GPTMail/CF) or MOEMAIL_DOMAIN fallback.
1348+
# Empty list => provider auto-pick (YYDS/GPTMail/CF/Cloud Mail) or MOEMAIL_DOMAIN fallback.
13401349
domains = parse_domain_list(domain)
13411350
if domains:
13421351
if domain_index is not None:
13431352
dom = pick_domain_from_list(domain, index=domain_index, strategy="round_robin")
13441353
else:
13451354
dom = pick_domain_from_list(domain, strategy="random")
13461355
else:
1347-
# YYDS/GPTMail/CFMail: empty domain means provider-side auto/random pick.
1348-
# Never bleed MoeMail's MOEMAIL_DOMAIN (default example.com) into them.
1349-
if prov in {"yyds", "gptmail", "cfmail", "tempmail"}:
1356+
# YYDS/GPTMail/CFMail/Cloud Mail: empty domain means provider-side
1357+
# auto/random pick (Cloud Mail calls /setting/websiteConfig inside
1358+
# cloudmail_create_mailbox). Never bleed MoeMail's MOEMAIL_DOMAIN
1359+
# (default example.com) into them.
1360+
if prov in {"yyds", "gptmail", "cfmail", "tempmail", "cloudmail"}:
13501361
dom = ""
13511362
else:
13521363
dom = (domain or MOEMAIL_DOMAIN or "").strip().lstrip("@").strip(".")
@@ -1390,6 +1401,12 @@ def __init__(
13901401
default_base = "https://temp-email-api.awsl.uk"
13911402
elif provider == "tempmail":
13921403
default_base = "https://api.tempmail.lol"
1404+
elif provider == "cloudmail":
1405+
# Cloud Mail is self-hosted; base_url is required and validated
1406+
# upstream. Empty default here keeps the receiver honest if the
1407+
# caller forgot to pass base_url (will surface as a network error
1408+
# rather than silently hitting the wrong host).
1409+
default_base = ""
13931410
else:
13941411
default_base = "https://moemail.521884.xyz"
13951412
self.base_url = base_url or default_base

0 commit comments

Comments
 (0)