tao-shen Claude Opus 4.6 commited on
Commit
95579c1
Β·
1 Parent(s): 4fb4a97

feat: telegram-proxy.cjs intercepts fetch() to bypass HF DNS block

Browse files

HF 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 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. Probe multiple API bases at
108
- # startup and pick the first one that responds to getMe. The working base
109
- # is then injected into the OpenClaw Telegram channel config.
 
 
 
 
 
 
 
 
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", # official
116
- "https://telegram-api.mykdigi.com", # known mirror
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 = f"{base}/bot{bot_token}/getMe"
130
  try:
131
  req = urllib.request.Request(url, method="GET")
132
  resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
133
- data = json.loads(resp.read().decode())
134
- if data.get("ok"):
135
- bot_name = data.get("result", {}).get("username", "unknown")
136
- print(f"[TELEGRAM] βœ“ API base works: {base} (bot: @{bot_name})")
137
- return base.rstrip("/")
 
138
  except Exception as e:
139
  reason = str(e)[:80]
140
- print(f"[TELEGRAM] βœ— API base failed: {base} ({reason})")
141
  continue
142
 
143
- print("[TELEGRAM] WARNING: All API bases failed! Telegram will not work.")
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
- # HF Spaces blocks api.telegram.org DNS. Probe mirrors and set
506
- # the working base URL in the channel config so grammY uses it.
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
+ }