Hfzune commited on
Commit
025fd0a
·
verified ·
1 Parent(s): ffbf38c

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -689
app.py DELETED
@@ -1,689 +0,0 @@
1
- # ══════════════════════════════════════════════════════════════════
2
- # CRITICAL SHIM — MUST BE THE FIRST EXECUTABLE CODE
3
- # ══════════════════════════════════════════════════════════════════
4
- import huggingface_hub
5
-
6
- if not hasattr(huggingface_hub, "HfFolder"):
7
- class _HfFolder:
8
- @staticmethod
9
- def get_token():
10
- try: return huggingface_hub.get_token()
11
- except Exception: return None
12
- @staticmethod
13
- def save_token(token):
14
- try: huggingface_hub.login(token)
15
- except Exception: pass
16
- @staticmethod
17
- def delete_token():
18
- try: huggingface_hub.logout()
19
- except Exception: pass
20
- huggingface_hub.HfFolder = _HfFolder
21
-
22
- # ── Now safe to import gradio ─────────────────────────────────────
23
- import os
24
- import asyncio
25
- import html
26
- import re
27
- import time
28
- import uuid
29
- import threading
30
- import traceback
31
- import logging
32
- from typing import Optional
33
-
34
- import duckdb
35
- import httpx
36
- import gradio as gr
37
- from fastapi import Request
38
- from fastapi.responses import JSONResponse
39
- from starlette.routing import Route as StarletteRoute
40
-
41
- # ─────────────────────────────────────────────
42
- # CONFIG
43
- # ─────────────────────────────────────────────
44
- BOT_TOKEN = os.environ.get("BOT_TOKEN", "").strip()
45
- HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
46
- WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "1221").strip()
47
- SPACE_URL = os.environ.get("SPACE_URL", "").strip().rstrip("/")
48
- TELEGRAM_PROXY = os.environ.get("TELEGRAM_PROXY", "").strip().rstrip("/")
49
-
50
- TELEGRAM_API = (
51
- f"{TELEGRAM_PROXY}/bot{BOT_TOKEN}" if TELEGRAM_PROXY
52
- else f"https://api.telegram.org/bot{BOT_TOKEN}"
53
- )
54
-
55
- logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
56
- log = logging.getLogger("bot")
57
-
58
- # ─────────────────────────────────────────────
59
- # DATASETS & API FALLBACKS
60
- # ─────────────────────────────────────────────
61
- # 1. ICMR Dataset
62
- ICMR_URLS = [
63
- "https://huggingface.co/datasets/Kzr0xx/Icmr-and-hitek/resolve/main/part1.parquet",
64
- "https://huggingface.co/datasets/Kzr0xx/Icmr-and-hitek/resolve/main/part2a.parquet",
65
- "https://huggingface.co/datasets/Kzr0xx/Icmr-and-hitek/resolve/main/part2b_new.parquet",
66
- ]
67
-
68
- # 2. HiTeck Dataset (Only streaming the 7 phone index files, ignoring Aadhaar)
69
- HITECK_URLS = [
70
- f"https://huggingface.co/datasets/Hfzune/HiTeckgroop/resolve/main/idx_phone.{i}.parquet"
71
- for i in range(7)
72
- ]
73
-
74
- # Combined list for DuckDB
75
- PARQUET_URLS = ICMR_URLS + HITECK_URLS
76
-
77
- # API Fallback Endpoints (Used when DuckDB is warming up)
78
- API_850CR = "https://sauravsingh2111-850crfastsearch.hf.space/search"
79
- API_INDDATA = "https://tfqdeadlo-inddataapi.hf.space/search"
80
-
81
- PAGE_SIZE = 4
82
- FETCH_LIMIT = 40
83
- QUERY_TIMEOUT = 90
84
- USER_COOLDOWN = 5
85
- CACHE_TTL = 300
86
-
87
- # Only 10-digit mobile numbers are searched
88
- PHONE_REGEX = re.compile(r"^\d{10}$")
89
-
90
- # Truncation per column (keeps pages under Telegram's 4096-char limit)
91
- TRUNC = {
92
- "name": 80, "fathersName": 80, "address": 200,
93
- "phoneNumber": 40, "aadharNumber": 40, "otherNumber": 40,
94
- "district": 40, "pincode": 15, "state": 40, "town": 40, "source": 30,
95
- }
96
-
97
- # ─────────────────────────────────────────────
98
- # DUCKDB ENGINE
99
- # ─────────────────────────────────────────────
100
- _con = None
101
- _con_lock = threading.Lock()
102
- _con_ready = threading.Event()
103
- _query_lock = threading.Lock()
104
- _warmed = threading.Event()
105
- _warm_thread_started = False
106
-
107
- def get_con() -> duckdb.DuckDBPyConnection:
108
- global _con
109
- with _con_lock:
110
- if _con is not None:
111
- return _con
112
- log.info("🔧 Initializing DuckDB...")
113
- c = duckdb.connect(":memory:")
114
- c.execute("INSTALL httpfs; LOAD httpfs;")
115
- if HF_TOKEN:
116
- c.execute(f"CREATE SECRET hf (TYPE HUGGINGFACE, TOKEN '{HF_TOKEN}');")
117
- c.execute("SET enable_object_cache=true;")
118
- c.execute("SET preserve_insertion_order=false;")
119
- c.execute("SET threads=6;") # Increased threads for 10 files
120
- c.execute("SET memory_limit='12GB';")
121
- urls = ", ".join(f"'{u}'" for u in PARQUET_URLS)
122
- try:
123
- c.execute(f"SELECT COUNT(*) FROM read_parquet([{urls}], union_by_name=true) WHERE 1=0")
124
- log.info("✅ DuckDB connection ready")
125
- except Exception as e:
126
- log.warning(f"⚠️ Warm-up metadata warning: {e}")
127
- _con = c
128
- _con_ready.set()
129
- return _con
130
-
131
- def warm_phone_cache() -> None:
132
- """Forces DuckDB to read the entire phoneNumber column across all 10 files."""
133
- global _warm_thread_started
134
- if _warmed.is_set():
135
- return
136
- _warm_thread_started = True
137
- try:
138
- log.info("🔥 Warm-up starting (may take 3-10 minutes for 200GB+)...")
139
- t0 = time.time()
140
- con = get_con()
141
- urls = ", ".join(f"'{u}'" for u in PARQUET_URLS)
142
- with _query_lock:
143
- con.execute(f"""
144
- SELECT SUM(HASH(CAST(phoneNumber AS VARCHAR)))
145
- FROM read_parquet([{urls}], union_by_name=true)
146
- WHERE phoneNumber IS NOT NULL
147
- """).fetchall()
148
- elapsed = time.time() - t0
149
- _warmed.set()
150
- log.info(f"✅ Warm-up complete in {elapsed:.1f}s")
151
- except Exception as e:
152
- log.error(f"⚠️ Warm-up failed: {e}")
153
- _warm_thread_started = False # Allow retry
154
-
155
- def run_search_safe(phone: str, limit: int = FETCH_LIMIT, timeout_secs: int = QUERY_TIMEOUT) -> list:
156
- """Search by 10-digit phone in DuckDB. Cancel-on-timeout via con.interrupt()."""
157
- con = get_con()
158
- with _query_lock:
159
- stopped = threading.Event()
160
- def watchdog():
161
- if not stopped.wait(timeout_secs):
162
- log.warning(f"⏱️ Timeout {timeout_secs}s — interrupting DuckDB")
163
- try: con.interrupt()
164
- except Exception as e: log.warning(f"⚠️ interrupt() failed: {e}")
165
- wd = threading.Thread(target=watchdog, daemon=True)
166
- wd.start()
167
- try:
168
- urls = ", ".join(f"'{u}'" for u in PARQUET_URLS)
169
- sql = f"""
170
- SELECT name, fathersName, phoneNumber, aadharNumber, otherNumber,
171
- address, district, pincode, state, town, source
172
- FROM read_parquet([{urls}], union_by_name=true)
173
- WHERE CAST(phoneNumber AS VARCHAR) = ?
174
- LIMIT {int(limit)}
175
- """
176
- cur = con.execute(sql, [phone])
177
- cols = [d[0] for d in cur.description]
178
- return [dict(zip(cols, r)) for r in cur.fetchall()]
179
- except duckdb.InterruptException:
180
- raise TimeoutError(f"Search timed out after {timeout_secs}s")
181
- finally:
182
- stopped.set()
183
-
184
- # ─────────────────────────────────────────────
185
- # API FALLBACK ENGINE
186
- # ─────────────────────────────────────────────
187
- async def _q_cr850(client, number):
188
- """850CR Fast — GET /search/{number}"""
189
- try:
190
- r = await client.get(f"{API_850CR}/{number}", timeout=40.0)
191
- if r.status_code != 200:
192
- return []
193
- data = r.json()
194
- if not data.get("success") or not data.get("results"):
195
- return []
196
- out = []
197
- for item in data["results"]:
198
- addr = item.get("address") or {}
199
- full = addr.get("full_address", "") if isinstance(addr, dict) else ""
200
- if full: full = full.lstrip(", ").strip()
201
- out.append({
202
- "name": item.get("name"),
203
- "fathersName": item.get("father_name"),
204
- "phoneNumber": number,
205
- "aadharNumber": None,
206
- "otherNumber": item.get("alternate_number"),
207
- "address": full,
208
- "district": None,
209
- "pincode": None,
210
- "state": item.get("state"),
211
- "town": None,
212
- "source": "850CR Fast"
213
- })
214
- return out
215
- except Exception as e:
216
- log.warning(f"850CR API failed: {e}")
217
- return []
218
-
219
- async def _q_inddata(client, number):
220
- """IndData 1.4B — GET /search?mobile={number}"""
221
- try:
222
- r = await client.get(API_INDDATA, params={"mobile": number}, timeout=30.0)
223
- if r.status_code != 200:
224
- return []
225
- data = r.json()
226
- rows = data.get("data") if isinstance(data, dict) else None
227
- if not isinstance(rows, list):
228
- return []
229
- out = []
230
- for item in rows:
231
- out.append({
232
- "name": item.get("name"),
233
- "fathersName": item.get("fname"),
234
- "phoneNumber": number,
235
- "aadharNumber": None,
236
- "otherNumber": None,
237
- "address": item.get("address"),
238
- "district": None,
239
- "pincode": None,
240
- "state": None,
241
- "town": None,
242
- "source": "IndData 1.4B"
243
- })
244
- return out
245
- except Exception as e:
246
- log.warning(f"IndData API failed: {e}")
247
- return []
248
-
249
- async def search_apis(number: str) -> list:
250
- """Fallback to APIs when DuckDB is not warmed up."""
251
- log.info(f"🔄 DuckDB not warmed. Falling back to APIs for {number}")
252
- async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
253
- results = await asyncio.gather(
254
- _q_cr850(client, number),
255
- _q_inddata(client, number),
256
- return_exceptions=True
257
- )
258
- merged = []
259
- for res in results:
260
- if isinstance(res, list):
261
- merged.extend(res)
262
- return merged
263
-
264
- # ─────────────────────────────────────────────
265
- # DEDUPLICATION & FORMATTING
266
- # ─────────────────────────────────────────────
267
- def _norm(s) -> str:
268
- if not s: return ""
269
- return re.sub(r"\s+", " ", str(s).strip().lower())
270
-
271
- def _dedup_key(rec: dict):
272
- name = _norm(rec.get("name"))
273
- father = _norm(rec.get("fathersName"))
274
- addr = _norm(rec.get("address"))
275
- addr_n = re.sub(r"[^a-z0-9]+", " ", addr).strip()
276
-
277
- if name and addr_n: return ("na", name, addr_n)
278
- if name and father: return ("nf", name, father)
279
- if addr_n: return ("a", addr_n)
280
- return None
281
-
282
- def dedup_records(records: list) -> list:
283
- seen = {}
284
- result = []
285
- for rec in records:
286
- key = _dedup_key(rec)
287
- if key is None:
288
- result.append(rec)
289
- continue
290
- idx = seen.get(key)
291
- if idx is None:
292
- seen[key] = len(result)
293
- result.append(rec)
294
- else:
295
- # Keep the one with more filled fields
296
- existing = result[idx]
297
- existing_fields = sum(1 for v in existing.values() if v)
298
- new_fields = sum(1 for v in rec.values() if v)
299
- if new_fields > existing_fields:
300
- result[idx] = rec
301
- return result
302
-
303
- def _tv(v, n: int) -> str:
304
- if v is None: return ""
305
- s = str(v).strip()
306
- if s.lower() in ("", "null", "none", "nan"): return ""
307
- return s if len(s) <= n else s[: n - 1] + "…"
308
-
309
- FIELD_LABELS = [
310
- ("name", "👤", "Name"),
311
- ("fathersName", "👨", "Father"),
312
- ("phoneNumber", "📞", "Phone"),
313
- ("otherNumber", "📱", "Other"),
314
- ("aadharNumber","🆔", "Aadhaar"),
315
- ("address", "🏠", "Address"),
316
- ("district", "🏙️", "District"),
317
- ("pincode", "📮", "Pincode"),
318
- ("state", "🗺️", "State"),
319
- ("town", "🌆", "Town"),
320
- ("source", "📂", "Source"),
321
- ]
322
-
323
- def format_result(idx: int, row: dict) -> str:
324
- lines = [f"<b>━━━ #{idx} ━━━</b>"]
325
- for key, emoji, label in FIELD_LABELS:
326
- v = _tv(row.get(key), TRUNC.get(key, 60))
327
- if v:
328
- lines.append(f"{emoji} <b>{label}:</b> <code>{html.escape(v)}</code>")
329
- return "\n".join(lines)
330
-
331
- def render_page(results, page, total_pages, phone, elapsed, source_note=""):
332
- start = page * PAGE_SIZE
333
- chunk = results[start:start + PAGE_SIZE]
334
- head = (
335
- f"📱 <b>Phone:</b> <code>{html.escape(phone)}</code>\n"
336
- f"📄 <b>Page {page+1}/{total_pages}</b> · "
337
- f"{len(results)} result(s) · ⏱️ {elapsed:.1f}s"
338
- )
339
- if source_note:
340
- head += f"\n⚠️ <i>{source_note}</i>"
341
- body = "\n\n".join(format_result(start + i + 1, r) for i, r in enumerate(chunk))
342
- return f"{head}\n\n{body}"
343
-
344
- def page_keyboard(search_id, page, total_pages):
345
- nav = []
346
- if page > 0:
347
- nav.append({"text": "◀ Prev", "callback_data": f"p:{search_id}:{page-1}"})
348
- nav.append({"text": f"{page+1} / {total_pages}", "callback_data": "noop"})
349
- if page < total_pages - 1:
350
- nav.append({"text": "Next ▶", "callback_data": f"p:{search_id}:{page+1}"})
351
- return {"inline_keyboard": [nav]}
352
-
353
- # ─────────────────────────────────────────────
354
- # TELEGRAM API HELPERS
355
- # ─────────────────────────────────────────────
356
- async def tg(method: str, payload: dict, timeout: float = 30) -> dict:
357
- try:
358
- async with httpx.AsyncClient(timeout=timeout) as client:
359
- r = await client.post(f"{TELEGRAM_API}/{method}", json=payload)
360
- return r.json()
361
- except Exception as e:
362
- log.error(f"❌ tg({method}) error: {e!r}")
363
- return {"ok": False, "error": str(e)}
364
-
365
- async def send(chat_id, text, **kw):
366
- kw.setdefault("parse_mode", "HTML")
367
- kw.setdefault("disable_web_page_preview", True)
368
- res = await tg("sendMessage", {"chat_id": chat_id, "text": text, **kw})
369
- if res.get("ok"): return res["result"]["message_id"]
370
- log.warning(f"⚠️ sendMessage failed: {res}")
371
- return None
372
-
373
- async def edit(chat_id, message_id, text, **kw):
374
- kw.setdefault("parse_mode", "HTML")
375
- kw.setdefault("disable_web_page_preview", True)
376
- res = await tg("editMessageText", {"chat_id": chat_id, "message_id": message_id, "text": text, **kw})
377
- if not res.get("ok") and "message is not modified" not in str(res):
378
- log.warning(f"⚠️ editMessageText failed: {res}")
379
-
380
- # ─────────────────────────────────────────────
381
- # STATE MANAGEMENT
382
- # ─────────────────────────────────────────────
383
- SEARCH_CACHE = {}
384
- USER_LAST = {}
385
- STATS = {"queries": 0, "timeouts": 0, "errors": 0, "started": time.time()}
386
-
387
- def cache_gc():
388
- while True:
389
- time.sleep(300)
390
- now = time.time()
391
- dead = [k for k, v in SEARCH_CACHE.items() if now - v["ts"] > CACHE_TTL]
392
- for k in dead: SEARCH_CACHE.pop(k, None)
393
-
394
- # ─────────────────────────────────────────────
395
- # HANDLERS
396
- # ─────────────────────────────────────────────
397
- WELCOME = (
398
- "👋 <b>Phone Lookup Bot</b>\n\n"
399
- "Send me any <b>10-digit mobile number</b> and I'll return every "
400
- "record we have on file.\n\n"
401
- "Example: <code>9876543210</code>\n\n"
402
- "<i>Commands:</i>\n"
403
- "• /start — show this message\n"
404
- "• /status — bot health\n"
405
- )
406
-
407
- async def handle_message(msg: dict):
408
- try:
409
- chat_id = msg["chat"]["id"]
410
- text = (msg.get("text") or "").strip()
411
- log.info(f"💬 Message from {chat_id}: {text[:50]!r}")
412
- if not text: return
413
-
414
- # ── /status ─────────────────────────────
415
- if text == "/status":
416
- uptime = (time.time() - STATS["started"]) / 60
417
- await send(chat_id, (
418
- f"<b>📊 Bot Status</b>\n\n"
419
- f"⏱️ Uptime: <code>{uptime:.1f} min</code>\n"
420
- f"🔌 DuckDB: <code>{_con_ready.is_set()}</code>\n"
421
- f"🔥 Cache warmed: <code>{_warmed.is_set()}</code>\n"
422
- f"🔍 Queries: <code>{STATS['queries']}</code>\n"
423
- f"⏰ Timeouts: <code>{STATS['timeouts']}</code>\n"
424
- f"❌ Errors: <code>{STATS['errors']}</code>\n"
425
- f"💾 Cached searches: <code>{len(SEARCH_CACHE)}</code>"
426
- ))
427
- return
428
-
429
- # ── /start ──────────────────────────────
430
- if text in ("/start", "/help"):
431
- await send(chat_id, WELCOME)
432
- if not _warmed.is_set():
433
- if not _warm_thread_started:
434
- threading.Thread(target=warm_phone_cache, daemon=True).start()
435
- return
436
-
437
- # ── Validate 10-digit phone ─────────────
438
- cleaned = "".join(ch for ch in text if ch.isdigit())
439
- if not PHONE_REGEX.match(cleaned):
440
- await send(chat_id, "❌ <b>Invalid input</b>\n\nSend exactly a <b>10-digit mobile number</b>.\nExample: <code>9876543210</code>")
441
- return
442
-
443
- phone = cleaned
444
-
445
- # ── Cooldown ────────────────────────────
446
- now = time.time()
447
- if now - USER_LAST.get(chat_id, 0) < USER_COOLDOWN:
448
- wait = USER_COOLDOWN - (now - USER_LAST.get(chat_id, 0))
449
- await send(chat_id, f"⏳ Wait {wait:.0f}s before next search.")
450
- return
451
- USER_LAST[chat_id] = now
452
-
453
- # ── Ensure warm-up is running ───────────
454
- if not _warmed.is_set() and not _warm_thread_started:
455
- threading.Thread(target=warm_phone_cache, daemon=True).start()
456
-
457
- # ── Placeholder ─────────────────────────
458
- placeholder = await send(
459
- chat_id,
460
- f"🔍 <b>Searching…</b>\n\n📱 <code>{html.escape(phone)}</code>\n\n"
461
- f"<i>First query after restart may take up to {QUERY_TIMEOUT}s. Repeats are fast.</i>"
462
- )
463
- if placeholder is None: return
464
-
465
- # ── Run Search (DuckDB with API Fallback) ─
466
- loop = asyncio.get_running_loop()
467
- t0 = time.time()
468
- source_note = ""
469
-
470
- try:
471
- if not _warmed.is_set():
472
- # Fallback to APIs if warming
473
- results = await search_apis(phone)
474
- source_note = "DuckDB warming in background. Showing API fallback results."
475
- else:
476
- # Use DuckDB
477
- results = await asyncio.wait_for(
478
- loop.run_in_executor(None, run_search_safe, phone, FETCH_LIMIT),
479
- timeout=QUERY_TIMEOUT + 10,
480
- )
481
- except asyncio.TimeoutError:
482
- STATS["timeouts"] += 1
483
- await edit(chat_id, placeholder, f"⏱️ <b>Timed out after {QUERY_TIMEOUT}s</b>\n\nTry the same number again — cache is warming.")
484
- return
485
- except TimeoutError:
486
- STATS["timeouts"] += 1
487
- await edit(chat_id, placeholder, f"⏱️ <b>Timed out after {QUERY_TIMEOUT}s</b>\n\nTry the same number again.")
488
- return
489
- except Exception as e:
490
- STATS["errors"] += 1
491
- log.error(f"❌ search error: {e!r}\n{traceback.format_exc()}")
492
- await edit(chat_id, placeholder, f"❌ <b>Error</b>\n<code>{html.escape(str(e)[:200])}</code>")
493
- return
494
-
495
- elapsed = time.time() - t0
496
- STATS["queries"] += 1
497
- log.info(f"✅ Search returned {len(results)} rows in {elapsed:.1f}s")
498
-
499
- # ── Deduplicate ──────────────────────────
500
- results = dedup_records(results)
501
- log.info(f"🧹 Deduplicated to {len(results)} rows")
502
-
503
- # ── No results ──────────────────────────
504
- if not results:
505
- await edit(chat_id, placeholder, f"❌ <b>No record found</b>\n\n📱 <code>{html.escape(phone)}</code>\n⏱️ {elapsed:.1f}s")
506
- return
507
-
508
- # ── Paginate ────────────────────────────
509
- search_id = uuid.uuid4().hex[:10]
510
- total_pages = (len(results) + PAGE_SIZE - 1) // PAGE_SIZE
511
- SEARCH_CACHE[search_id] = {
512
- "ts": time.time(),
513
- "results": results,
514
- "query": phone,
515
- "elapsed": elapsed,
516
- "source_note": source_note
517
- }
518
- await edit(
519
- chat_id, placeholder,
520
- render_page(results, 0, total_pages, phone, elapsed, source_note),
521
- reply_markup=page_keyboard(search_id, 0, total_pages),
522
- )
523
- except Exception as e:
524
- log.error(f"❌ handle_message fatal: {e!r}\n{traceback.format_exc()}")
525
-
526
- async def handle_callback(cb: dict):
527
- try:
528
- data = cb.get("data", "")
529
- cb_id = cb["id"]
530
- msg = cb.get("message") or {}
531
- chat_id = msg.get("chat", {}).get("id")
532
- message_id = msg.get("message_id")
533
-
534
- if not chat_id or not message_id:
535
- await tg("answerCallbackQuery", {"callback_query_id": cb_id})
536
- return
537
- if data == "noop" or not data.startswith("p:"):
538
- await tg("answerCallbackQuery", {"callback_query_id": cb_id})
539
- return
540
-
541
- try:
542
- _, search_id, page_str = data.split(":")
543
- page = int(page_str)
544
- except Exception:
545
- await tg("answerCallbackQuery", {"callback_query_id": cb_id})
546
- return
547
-
548
- entry = SEARCH_CACHE.get(search_id)
549
- if not entry:
550
- await tg("answerCallbackQuery", {"callback_query_id": cb_id, "text": "Session expired — search again.", "show_alert": True})
551
- return
552
-
553
- results = entry["results"]
554
- total_pages = (len(results) + PAGE_SIZE - 1) // PAGE_SIZE
555
- page = max(0, min(page, total_pages - 1))
556
-
557
- await edit(
558
- chat_id, message_id,
559
- render_page(results, page, total_pages, entry["query"], entry["elapsed"], entry.get("source_note", "")),
560
- reply_markup=page_keyboard(search_id, page, total_pages),
561
- )
562
- await tg("answerCallbackQuery", {"callback_query_id": cb_id})
563
- except Exception as e:
564
- log.error(f"❌ handle_callback fatal: {e!r}\n{traceback.format_exc()}")
565
-
566
- # ─────────────────────────────────────────────
567
- # WEBHOOK & HEALTH
568
- # ─────────────────────────────────────────────
569
- async def set_webhook():
570
- if not SPACE_URL: return
571
- url = f"{SPACE_URL}/webhook"
572
- payload = {
573
- "url": url,
574
- "secret_token": WEBHOOK_SECRET,
575
- "drop_pending_updates": True,
576
- "allowed_updates": ["message", "callback_query"],
577
- }
578
- for attempt in range(1, 4):
579
- try:
580
- async with httpx.AsyncClient(timeout=30) as client:
581
- r = await client.post(f"{TELEGRAM_API}/setWebhook", json=payload)
582
- data = r.json()
583
- log.info(f"🔗 setWebhook → {data}")
584
- if data.get("ok"): return
585
- except Exception as e:
586
- log.warning(f"⚠️ setWebhook attempt {attempt}: {e}")
587
- await asyncio.sleep(3)
588
-
589
- async def _health_endpoint(request: Request):
590
- return JSONResponse({
591
- "status": "ok",
592
- "duckdb_ready": _con_ready.is_set(),
593
- "warmed": _warmed.is_set(),
594
- "cached_searches": len(SEARCH_CACHE),
595
- "bot_token_set": bool(BOT_TOKEN),
596
- "stats": STATS,
597
- })
598
-
599
- async def webhook_handler(request: Request):
600
- if WEBHOOK_SECRET:
601
- got = request.headers.get("x-telegram-bot-api-secret-token", "")
602
- if got != WEBHOOK_SECRET:
603
- return JSONResponse({"ok": False}, status_code=403)
604
- try:
605
- update = await request.json()
606
- except Exception:
607
- return JSONResponse({"ok": True})
608
-
609
- if "message" in update:
610
- asyncio.create_task(handle_message(update["message"]))
611
- elif "callback_query" in update:
612
- asyncio.create_task(handle_callback(update["callback_query"]))
613
- return JSONResponse({"ok": True})
614
-
615
- # ─────────────────────────────────────────────
616
- # GRADIO UI & LAUNCH
617
- # ─────────────────────────────────────────────
618
- with gr.Blocks(title="Phone Lookup Bot") as demo:
619
- gr.Markdown("# 📞 Phone Lookup Bot")
620
- gr.Markdown(
621
- "Telegram bot that streams `Kzr0xx/Icmr-and-hitek` and `Hfzune/HiTeckgroop` "
622
- "from HuggingFace on demand, with API fallback during warm-up."
623
- )
624
- gr.Markdown("### Status")
625
- status_box = gr.JSON(label="Health")
626
- demo.load(
627
- fn=lambda: {
628
- "status": "ok",
629
- "duckdb_ready": _con_ready.is_set(),
630
- "warmed": _warmed.is_set(),
631
- "cached_searches": len(SEARCH_CACHE),
632
- },
633
- outputs=status_box,
634
- )
635
- try:
636
- import spaces
637
- @spaces.GPU(duration=10)
638
- def _zerogpu_register():
639
- return "gpu-ready"
640
- _zerogpu_btn = gr.Button("init-gpu", visible=False)
641
- _zerogpu_btn.click(fn=_zerogpu_register, inputs=None, outputs=None)
642
- log.info("✅ @spaces.GPU registered")
643
- except Exception as e:
644
- log.warning(f"⚠️ ZeroGPU registration: {e}")
645
-
646
- # ─────────────────────────────────────────────
647
- # STARTUP THREADS
648
- # ─────────────────────────────────────────────
649
- threading.Thread(target=cache_gc, daemon=True).start()
650
- threading.Thread(target=get_con, daemon=True).start()
651
- threading.Thread(target=warm_phone_cache, daemon=True).start()
652
-
653
- # ─────────────────────────────────────────────
654
- # LAUNCH & ROUTE INJECTION
655
- # ─────────────────────────────────────────────
656
- demo.launch(
657
- server_name="0.0.0.0",
658
- server_port=7860,
659
- share=False,
660
- ssr_mode=False,
661
- prevent_thread_lock=True,
662
- )
663
-
664
- log.info("🔎 Launch returned — injecting routes")
665
- live_app = demo.app
666
-
667
- live_app.router.routes.insert(0, StarletteRoute("/webhook", webhook_handler, methods=["POST"]))
668
- live_app.router.routes.insert(0, StarletteRoute("/health", _health_endpoint, methods=["GET"]))
669
-
670
- try:
671
- live_app.build_middleware_stack()
672
- log.info("✅ Middleware stack rebuilt")
673
- except Exception as e:
674
- log.warning(f"⚠️ Stack rebuild: {e}")
675
-
676
- log.info("📋 First 5 routes:")
677
- for route in live_app.router.routes[:5]:
678
- path = getattr(route, "path", "?")
679
- methods = getattr(route, "methods", None)
680
- log.info(f" {sorted(methods) if methods else '?'} {path}")
681
-
682
- def _register_webhook_later():
683
- loop = asyncio.new_event_loop()
684
- asyncio.set_event_loop(loop)
685
- loop.run_until_complete(set_webhook())
686
-
687
- threading.Thread(target=_register_webhook_later, daemon=True).start()
688
-
689
- demo.block_thread()