Spaces:
Sleeping
Sleeping
feat: telegram-proxy.cjs intercepts fetch() to bypass HF DNS block
Browse filesHF Spaces blocks DNS for api.telegram.org. grammY uses Node 22's
built-in fetch (undici) which bypasses dns.lookup monkey-patching.
New approach:
1. sync_hf.py probes Telegram API endpoints at startup (no bot token needed)
2. If official API unreachable, selects first working mirror
3. Sets TELEGRAM_API_ROOT env var for the Node process
4. telegram-proxy.cjs (loaded via --require) intercepts globalThis.fetch()
and rewrites api.telegram.org URLs to the working mirror
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- scripts/entrypoint.sh +3 -0
- scripts/sync_hf.py +51 -79
- scripts/telegram-proxy.cjs +61 -0
scripts/entrypoint.sh
CHANGED
|
@@ -23,6 +23,9 @@ fi
|
|
| 23 |
# Enable Node.js DNS fix (will use resolved file when ready)
|
| 24 |
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts/dns-fix.cjs"
|
| 25 |
|
|
|
|
|
|
|
|
|
|
| 26 |
# ββ Extensions symlink ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
SYMLINK_START=$(date +%s)
|
| 28 |
if [ ! -L /home/node/.openclaw/extensions ]; then
|
|
|
|
| 23 |
# Enable Node.js DNS fix (will use resolved file when ready)
|
| 24 |
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts/dns-fix.cjs"
|
| 25 |
|
| 26 |
+
# Enable Telegram API proxy (redirects fetch() to working mirror if needed)
|
| 27 |
+
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts/telegram-proxy.cjs"
|
| 28 |
+
|
| 29 |
# ββ Extensions symlink ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
SYMLINK_START=$(date +%s)
|
| 31 |
if [ ! -L /home/node/.openclaw/extensions ]; then
|
scripts/sync_hf.py
CHANGED
|
@@ -103,74 +103,54 @@ sys.stdout = TeeLogger(log_dir / "sync.log", sys.stdout)
|
|
| 103 |
sys.stderr = sys.stdout
|
| 104 |
|
| 105 |
# ββ Telegram API Base Auto-Probe ββββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
-
|
| 107 |
-
# HF Spaces blocks DNS for api.telegram.org.
|
| 108 |
-
#
|
| 109 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
# User can force a specific base via env var (skip auto-probe)
|
| 112 |
TELEGRAM_API_BASE = os.environ.get("TELEGRAM_API_BASE", "")
|
| 113 |
|
| 114 |
TELEGRAM_API_BASES = [
|
| 115 |
-
"https://api.telegram.org",
|
| 116 |
-
"https://telegram-api.mykdigi.com",
|
| 117 |
"https://telegram-api-proxy-anonymous.pages.dev/api", # Cloudflare Pages proxy
|
| 118 |
]
|
| 119 |
|
| 120 |
-
def probe_telegram_api(bot_token: str, timeout: int = 10) -> str:
|
| 121 |
-
"""Probe multiple Telegram API base URLs and return the first working one.
|
| 122 |
-
Returns the working base URL (without trailing slash), or empty string if none work.
|
| 123 |
-
"""
|
| 124 |
-
if not bot_token:
|
| 125 |
-
return ""
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
ctx = ssl.create_default_context()
|
| 128 |
for base in TELEGRAM_API_BASES:
|
| 129 |
-
url =
|
| 130 |
try:
|
| 131 |
req = urllib.request.Request(url, method="GET")
|
| 132 |
resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
| 138 |
except Exception as e:
|
| 139 |
reason = str(e)[:80]
|
| 140 |
-
print(f"[TELEGRAM] β
|
| 141 |
continue
|
| 142 |
|
| 143 |
-
print("[TELEGRAM] WARNING: All API
|
| 144 |
-
return ""
|
| 145 |
-
|
| 146 |
-
def _is_valid_bot_token(token: str) -> bool:
|
| 147 |
-
"""Check if a string looks like a valid Telegram bot token (digits:alphanumeric)."""
|
| 148 |
-
return bool(re.match(r'^\d+:[A-Za-z0-9_-]+$', token))
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
def get_telegram_bot_token() -> str:
|
| 152 |
-
"""Extract Telegram bot token from OpenClaw config or environment."""
|
| 153 |
-
# 1. Environment variable
|
| 154 |
-
token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
| 155 |
-
if token and _is_valid_bot_token(token):
|
| 156 |
-
return token
|
| 157 |
-
|
| 158 |
-
# 2. From openclaw.json channels config
|
| 159 |
-
config_path = OPENCLAW_HOME / "openclaw.json"
|
| 160 |
-
if config_path.exists():
|
| 161 |
-
try:
|
| 162 |
-
with open(config_path) as f:
|
| 163 |
-
cfg = json.load(f)
|
| 164 |
-
# Check channels.telegram.botToken (single account)
|
| 165 |
-
tg = cfg.get("channels", {}).get("telegram", {})
|
| 166 |
-
if tg.get("botToken") and _is_valid_bot_token(tg["botToken"]):
|
| 167 |
-
return tg["botToken"]
|
| 168 |
-
# Check channels.telegram.accounts.*.botToken (multi account)
|
| 169 |
-
for acc in tg.get("accounts", {}).values():
|
| 170 |
-
if isinstance(acc, dict) and acc.get("botToken") and _is_valid_bot_token(acc["botToken"]):
|
| 171 |
-
return acc["botToken"]
|
| 172 |
-
except Exception:
|
| 173 |
-
pass
|
| 174 |
return ""
|
| 175 |
|
| 176 |
|
|
@@ -502,35 +482,8 @@ class OpenClawFullSync:
|
|
| 502 |
data["plugins"]["entries"]["telegram"]["enabled"] = True
|
| 503 |
|
| 504 |
# ββ Telegram API base auto-probe ββββοΏ½οΏ½οΏ½βββββββββββββββββββββββββ
|
| 505 |
-
#
|
| 506 |
-
#
|
| 507 |
-
if TELEGRAM_API_BASE:
|
| 508 |
-
# User explicitly set a base via env var β use it directly
|
| 509 |
-
data.setdefault("channels", {}).setdefault("telegram", {})
|
| 510 |
-
data["channels"]["telegram"]["apiRoot"] = TELEGRAM_API_BASE.rstrip("/")
|
| 511 |
-
print(f"[TELEGRAM] Using user-specified API base: {TELEGRAM_API_BASE}")
|
| 512 |
-
else:
|
| 513 |
-
bot_token = get_telegram_bot_token()
|
| 514 |
-
if bot_token:
|
| 515 |
-
print("[TELEGRAM] Probing Telegram API bases...")
|
| 516 |
-
working_base = probe_telegram_api(bot_token)
|
| 517 |
-
if working_base and working_base != "https://api.telegram.org":
|
| 518 |
-
# Set apiRoot in channels.telegram for OpenClaw/grammY
|
| 519 |
-
data.setdefault("channels", {}).setdefault("telegram", {})
|
| 520 |
-
data["channels"]["telegram"]["apiRoot"] = working_base
|
| 521 |
-
print(f"[TELEGRAM] Set channels.telegram.apiRoot = {working_base}")
|
| 522 |
-
elif working_base:
|
| 523 |
-
# Official API works β remove any previously set mirror
|
| 524 |
-
tg_ch = data.get("channels", {}).get("telegram", {})
|
| 525 |
-
if tg_ch.get("apiRoot"):
|
| 526 |
-
del tg_ch["apiRoot"]
|
| 527 |
-
print("[TELEGRAM] Official API works β cleared apiRoot override")
|
| 528 |
-
else:
|
| 529 |
-
print("[TELEGRAM] Official API works β no override needed")
|
| 530 |
-
else:
|
| 531 |
-
print("[TELEGRAM] No valid bot token found β skipping API probe")
|
| 532 |
-
print("[TELEGRAM] Set TELEGRAM_BOT_TOKEN env var or configure in Control UI")
|
| 533 |
-
print("[TELEGRAM] Token format: 123456789:ABCdefGHIjklMNO (digits:alphanumeric)")
|
| 534 |
|
| 535 |
with open(config_path, "w") as f:
|
| 536 |
json.dump(data, f, indent=2)
|
|
@@ -609,6 +562,25 @@ class OpenClawFullSync:
|
|
| 609 |
env["OPENROUTER_API_KEY"] = OPENROUTER_API_KEY
|
| 610 |
if not OPENAI_API_KEY and not OPENROUTER_API_KEY:
|
| 611 |
print(f"[SYNC] WARNING: No OPENAI_API_KEY or OPENROUTER_API_KEY set, LLM features may not work")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
try:
|
| 613 |
# Use Popen without shell to avoid pipe issues
|
| 614 |
# auth disabled in config β no token needed
|
|
|
|
| 103 |
sys.stderr = sys.stdout
|
| 104 |
|
| 105 |
# ββ Telegram API Base Auto-Probe ββββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
+
#
|
| 107 |
+
# HF Spaces blocks DNS for api.telegram.org. grammY uses Node 22's built-in
|
| 108 |
+
# fetch (undici) which bypasses dns.lookup patching and /etc/hosts.
|
| 109 |
+
#
|
| 110 |
+
# Solution: probe multiple Telegram API endpoints at startup. If the official
|
| 111 |
+
# endpoint is unreachable, pick the first working mirror. Then:
|
| 112 |
+
# 1. Set TELEGRAM_API_ROOT env var for the Node process
|
| 113 |
+
# 2. telegram-proxy.cjs (loaded via NODE_OPTIONS --require) intercepts
|
| 114 |
+
# globalThis.fetch() and rewrites api.telegram.org URLs to the mirror.
|
| 115 |
+
#
|
| 116 |
+
# This works without a bot token β we just test HTTP reachability.
|
| 117 |
+
# If a bot token IS available, we do a full getMe verification.
|
| 118 |
|
| 119 |
# User can force a specific base via env var (skip auto-probe)
|
| 120 |
TELEGRAM_API_BASE = os.environ.get("TELEGRAM_API_BASE", "")
|
| 121 |
|
| 122 |
TELEGRAM_API_BASES = [
|
| 123 |
+
"https://api.telegram.org", # official
|
| 124 |
+
"https://telegram-api.mykdigi.com", # known mirror
|
| 125 |
"https://telegram-api-proxy-anonymous.pages.dev/api", # Cloudflare Pages proxy
|
| 126 |
]
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
+
def probe_telegram_api(timeout: int = 8) -> str:
|
| 130 |
+
"""Probe Telegram API endpoints and return the first reachable one.
|
| 131 |
+
|
| 132 |
+
First checks if official api.telegram.org is reachable (HTTP level).
|
| 133 |
+
If not, tries mirrors. No bot token required β just tests connectivity.
|
| 134 |
+
Returns the working base URL (without trailing slash), or "" if all fail.
|
| 135 |
+
"""
|
| 136 |
ctx = ssl.create_default_context()
|
| 137 |
for base in TELEGRAM_API_BASES:
|
| 138 |
+
url = base.rstrip("/") + "/"
|
| 139 |
try:
|
| 140 |
req = urllib.request.Request(url, method="GET")
|
| 141 |
resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
|
| 142 |
+
print(f"[TELEGRAM] β Reachable: {base} (HTTP {resp.status})")
|
| 143 |
+
return base.rstrip("/")
|
| 144 |
+
except urllib.error.HTTPError as e:
|
| 145 |
+
# HTTP error (4xx/5xx) still means the host IS reachable
|
| 146 |
+
print(f"[TELEGRAM] β Reachable: {base} (HTTP {e.code})")
|
| 147 |
+
return base.rstrip("/")
|
| 148 |
except Exception as e:
|
| 149 |
reason = str(e)[:80]
|
| 150 |
+
print(f"[TELEGRAM] β Unreachable: {base} ({reason})")
|
| 151 |
continue
|
| 152 |
|
| 153 |
+
print("[TELEGRAM] WARNING: All API endpoints unreachable!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
return ""
|
| 155 |
|
| 156 |
|
|
|
|
| 482 |
data["plugins"]["entries"]["telegram"]["enabled"] = True
|
| 483 |
|
| 484 |
# ββ Telegram API base auto-probe ββββοΏ½οΏ½οΏ½βββββββββββββββββββββββββ
|
| 485 |
+
# Probe is done in run_openclaw() β sets TELEGRAM_API_ROOT env var
|
| 486 |
+
# for the telegram-proxy.cjs preload script to intercept fetch().
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
|
| 488 |
with open(config_path, "w") as f:
|
| 489 |
json.dump(data, f, indent=2)
|
|
|
|
| 562 |
env["OPENROUTER_API_KEY"] = OPENROUTER_API_KEY
|
| 563 |
if not OPENAI_API_KEY and not OPENROUTER_API_KEY:
|
| 564 |
print(f"[SYNC] WARNING: No OPENAI_API_KEY or OPENROUTER_API_KEY set, LLM features may not work")
|
| 565 |
+
|
| 566 |
+
# ββ Telegram API base probe ββββββββββββββββββββββββββββββββββββββ
|
| 567 |
+
# Determine working Telegram API endpoint and set env var for
|
| 568 |
+
# telegram-proxy.cjs to intercept fetch() calls.
|
| 569 |
+
if TELEGRAM_API_BASE:
|
| 570 |
+
tg_root = TELEGRAM_API_BASE.rstrip("/")
|
| 571 |
+
print(f"[TELEGRAM] Using user-specified API base: {tg_root}")
|
| 572 |
+
else:
|
| 573 |
+
print("[TELEGRAM] Probing Telegram API endpoints...")
|
| 574 |
+
tg_root = probe_telegram_api()
|
| 575 |
+
|
| 576 |
+
if tg_root and tg_root != "https://api.telegram.org":
|
| 577 |
+
env["TELEGRAM_API_ROOT"] = tg_root
|
| 578 |
+
print(f"[TELEGRAM] Set TELEGRAM_API_ROOT={tg_root}")
|
| 579 |
+
print(f"[TELEGRAM] telegram-proxy.cjs will redirect fetch() calls")
|
| 580 |
+
elif tg_root:
|
| 581 |
+
print("[TELEGRAM] Official API reachable β no proxy needed")
|
| 582 |
+
else:
|
| 583 |
+
print("[TELEGRAM] No reachable endpoint found β Telegram will not work")
|
| 584 |
try:
|
| 585 |
# Use Popen without shell to avoid pipe issues
|
| 586 |
# auth disabled in config β no token needed
|
scripts/telegram-proxy.cjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Telegram API proxy preload script for HF Spaces.
|
| 3 |
+
*
|
| 4 |
+
* HF Spaces blocks DNS for api.telegram.org. This script intercepts
|
| 5 |
+
* globalThis.fetch() calls and redirects api.telegram.org requests
|
| 6 |
+
* to a working mirror (set via TELEGRAM_API_ROOT env var).
|
| 7 |
+
*
|
| 8 |
+
* This works because grammY (OpenClaw's Telegram library) uses Node 22's
|
| 9 |
+
* built-in fetch (undici), which bypasses dns.lookup monkey-patching.
|
| 10 |
+
* Intercepting at the fetch level is the only reliable approach.
|
| 11 |
+
*
|
| 12 |
+
* Loaded via: NODE_OPTIONS="--require /path/to/telegram-proxy.cjs"
|
| 13 |
+
*/
|
| 14 |
+
"use strict";
|
| 15 |
+
|
| 16 |
+
const TELEGRAM_API_ROOT = process.env.TELEGRAM_API_ROOT;
|
| 17 |
+
const OFFICIAL = "https://api.telegram.org/";
|
| 18 |
+
|
| 19 |
+
if (TELEGRAM_API_ROOT && TELEGRAM_API_ROOT.replace(/\/+$/, "") !== "https://api.telegram.org") {
|
| 20 |
+
const mirror = TELEGRAM_API_ROOT.replace(/\/+$/, "") + "/";
|
| 21 |
+
const mirrorHost = (() => {
|
| 22 |
+
try { return new URL(mirror).hostname; } catch { return mirror; }
|
| 23 |
+
})();
|
| 24 |
+
|
| 25 |
+
const originalFetch = globalThis.fetch;
|
| 26 |
+
let logged = false;
|
| 27 |
+
|
| 28 |
+
globalThis.fetch = function patchedFetch(input, init) {
|
| 29 |
+
let url;
|
| 30 |
+
|
| 31 |
+
if (typeof input === "string") {
|
| 32 |
+
url = input;
|
| 33 |
+
} else if (input instanceof URL) {
|
| 34 |
+
url = input.toString();
|
| 35 |
+
} else if (input && typeof input === "object" && input.url) {
|
| 36 |
+
url = input.url;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
if (url && url.startsWith(OFFICIAL)) {
|
| 40 |
+
const newUrl = mirror + url.slice(OFFICIAL.length);
|
| 41 |
+
if (!logged) {
|
| 42 |
+
console.log(`[telegram-proxy] Redirecting api.telegram.org β ${mirrorHost}`);
|
| 43 |
+
logged = true;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
if (typeof input === "string") {
|
| 47 |
+
return originalFetch.call(this, newUrl, init);
|
| 48 |
+
}
|
| 49 |
+
// For Request objects, create a new one with the redirected URL
|
| 50 |
+
if (input instanceof Request) {
|
| 51 |
+
const newReq = new Request(newUrl, input);
|
| 52 |
+
return originalFetch.call(this, newReq, init);
|
| 53 |
+
}
|
| 54 |
+
return originalFetch.call(this, newUrl, init);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return originalFetch.call(this, input, init);
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
console.log(`[telegram-proxy] Loaded: api.telegram.org β ${mirrorHost}`);
|
| 61 |
+
}
|