Hfzune commited on
Commit
dfabb56
·
verified ·
1 Parent(s): d1f6a1c

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -694
app.py DELETED
@@ -1,694 +0,0 @@
1
- # ─────────────────────────────────────────────────────────────
2
- # 0. HfFolder shim (must be first executable code)
3
- # ─────────────────────────────────────────────────────────────
4
- import huggingface_hub
5
- if not hasattr(huggingface_hub, "HfFolder"):
6
- class _HfFolder:
7
- @staticmethod
8
- def get_token(): return huggingface_hub.get_token()
9
- @staticmethod
10
- def save_token(token): huggingface_hub.login(token)
11
- @staticmethod
12
- def delete_token(): huggingface_hub.logout()
13
- huggingface_hub.HfFolder = _HfFolder
14
-
15
- # ─────────────────────────────────────────────────────────────
16
- # 1. Imports
17
- # ─────────────────────────────────────────────────────────────
18
- import os
19
- import re
20
- import time
21
- import html
22
- import asyncio
23
- import logging
24
- import threading
25
- from typing import Optional
26
-
27
- import httpx
28
- from fastapi import Request
29
- from fastapi.responses import JSONResponse
30
- from starlette.routing import Route as StarletteRoute
31
- import gradio as gr
32
- import spaces
33
-
34
- # ─────────────────────────────────────────────────────────────
35
- # 2. Config
36
- # ─────────────────────────────────────────────────────────────
37
- BOT_TOKEN = os.environ["BOT_TOKEN"]
38
- WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "1221")
39
- SPACE_URL = os.environ["SPACE_URL"].rstrip("/")
40
- TELEGRAM_PROXY = os.environ.get("TELEGRAM_PROXY", "").rstrip("/")
41
-
42
- ADMIN_IDS = [int(x) for x in os.environ.get("ADMIN_IDS", "").split(",") if x.strip().isdigit()]
43
- AUTH_ENABLED = len(ADMIN_IDS) > 0
44
-
45
- TG_API = (TELEGRAM_PROXY + "/bot" + BOT_TOKEN) if TELEGRAM_PROXY \
46
- else f"https://api.telegram.org/bot{BOT_TOKEN}"
47
-
48
- RATE_LIMIT_SEC = 5
49
- PAGE_SIZE = 4
50
- QUERY_TIMEOUT = 45
51
- HTTP_TIMEOUT = httpx.Timeout(45.0, connect=10.0)
52
-
53
- logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
54
- log = logging.getLogger("bot")
55
-
56
- # ─────────────────────────────────────────────────────────────
57
- # 3. HTTP clients (one AsyncClient per running event loop)
58
- # ─────────────────────────────────────────────────────────────
59
- _api_clients: dict[int, httpx.AsyncClient] = {}
60
- _clients_lock = threading.Lock()
61
-
62
-
63
- async def _get_api_client() -> httpx.AsyncClient:
64
- loop = asyncio.get_running_loop()
65
- key = id(loop)
66
- client = _api_clients.get(key)
67
- if client is None or client.is_closed:
68
- client = httpx.AsyncClient(timeout=HTTP_TIMEOUT, follow_redirects=True)
69
- with _clients_lock:
70
- _api_clients[key] = client
71
- return client
72
-
73
-
74
- # ─────────────────────────────────────────────────────────────
75
- # 4. External data sources
76
- # ─────────────────────────────────────────────────────────────
77
- def _clean(v):
78
- """Strip 'null', 'N/A', empty strings → return None."""
79
- if v is None:
80
- return None
81
- s = str(v).strip()
82
- if s in ("", "null", "N/A", "None", "-"):
83
- return None
84
- return s
85
-
86
-
87
- async def _q_cr850(client: httpx.AsyncClient, number: str) -> list[dict]:
88
- url = f"https://sauravsingh2111-850crfastsearch.hf.space/search/{number}"
89
- try:
90
- r = await client.get(url, timeout=35.0)
91
- if r.status_code != 200:
92
- log.info("cr850 → %s", r.status_code)
93
- return []
94
- data = r.json()
95
- if not data.get("success") or not data.get("results"):
96
- return []
97
- out = []
98
- for item in data["results"]:
99
- addr = item.get("address") or {}
100
- full = None
101
- if isinstance(addr, dict):
102
- full = addr.get("full_address")
103
- if full:
104
- full = full.lstrip(", ").strip()
105
- out.append({
106
- "source": "850CR Fast",
107
- "fields": {
108
- "Name": _clean(item.get("name")),
109
- "Father": _clean(item.get("father_name")),
110
- "Address": _clean(full),
111
- "Alternate": _clean(item.get("alternate_number")),
112
- "Circle": _clean(item.get("circle")),
113
- "Operator": _clean(item.get("operator")),
114
- "State": _clean(item.get("state")),
115
- "Email": _clean(item.get("email")),
116
- },
117
- })
118
- return out
119
- except Exception as e:
120
- log.warning("cr850 failed: %s", e)
121
- return []
122
-
123
-
124
- async def _q_inddata(client: httpx.AsyncClient, number: str) -> list[dict]:
125
- url = "https://tfqdeadlo-inddataapi.hf.space/search"
126
- try:
127
- r = await client.get(url, params={"mobile": number}, timeout=25.0)
128
- if r.status_code != 200:
129
- log.info("inddata → %s", r.status_code)
130
- return []
131
- data = r.json()
132
- rows = data.get("data") if isinstance(data, dict) else None
133
- if not isinstance(rows, list):
134
- return []
135
- out = []
136
- for item in rows:
137
- out.append({
138
- "source": "IndData 1.4B",
139
- "fields": {
140
- "Mobile": _clean(item.get("mobile")),
141
- "Name": _clean(item.get("name")),
142
- "Father": _clean(item.get("fname")),
143
- "Address": _clean(item.get("address")),
144
- "Email": _clean(item.get("email")),
145
- "ID": _clean(item.get("id")),
146
- },
147
- })
148
- return out
149
- except Exception as e:
150
- log.warning("inddata failed: %s", e)
151
- return []
152
-
153
-
154
- async def _q_hiteck_main(client: httpx.AsyncClient, number: str) -> list[dict]:
155
- url = "https://hiteckgroup-hiteckgroup.hf.space/search"
156
- try:
157
- r = await client.get(url, params={"mobile": number}, timeout=25.0)
158
- if r.status_code != 200:
159
- log.info("hiteck_main → %s", r.status_code)
160
- return []
161
- data = r.json()
162
- rows = data.get("data") if isinstance(data, dict) else None
163
- if not isinstance(rows, list):
164
- return []
165
- out = []
166
- for item in rows:
167
- addr = item.get("address")
168
- if addr:
169
- addr = addr.replace("!", ", ")
170
- out.append({
171
- "source": "HiTeck 1.78B",
172
- "fields": {
173
- "Mobile": _clean(item.get("mobile")),
174
- "Name": _clean(item.get("name")),
175
- "Father": _clean(item.get("fname")),
176
- "Address": _clean(addr),
177
- "Alternate": _clean(item.get("alt")),
178
- "Circle": _clean(item.get("circle")),
179
- "Email": _clean(item.get("email")),
180
- },
181
- })
182
- return out
183
- except Exception as e:
184
- log.warning("hiteck_main failed: %s", e)
185
- return []
186
-
187
-
188
- async def _q_hiteck_big(client: httpx.AsyncClient, number: str) -> list[dict]:
189
- url = "https://hiteckgroup-hiteckgroupp.hf.space/search"
190
- try:
191
- r = await client.get(url, params={"q": number}, timeout=30.0)
192
- if r.status_code != 200:
193
- log.info("hiteck_big → %s", r.status_code)
194
- return []
195
- data = r.json()
196
- rows = data.get("results") if isinstance(data, dict) else None
197
- if not isinstance(rows, list):
198
- return []
199
- out = []
200
- for item in rows:
201
- out.append({
202
- "source": "HiTeck 5B",
203
- "fields": {
204
- "Name": _clean(item.get("name")),
205
- "Father": _clean(item.get("fathersName")),
206
- "Phone": _clean(item.get("phoneNumber")),
207
- "Aadhaar": _clean(item.get("aadharNumber")),
208
- "Other": _clean(item.get("otherNumber")),
209
- "Address": _clean(item.get("address")),
210
- "District": _clean(item.get("district")),
211
- "State": _clean(item.get("state")),
212
- "Town": _clean(item.get("town")),
213
- "Pincode": _clean(item.get("pincode")),
214
- "Source": _clean(item.get("source")),
215
- },
216
- })
217
- return out
218
- except Exception as e:
219
- log.warning("hiteck_big failed: %s", e)
220
- return []
221
-
222
-
223
- # ─────────────────────────────────────────────────────────────
224
- # 4b. De-duplication
225
- # ─────────────────────────────────────────────────────────────
226
- def _norm(s) -> str:
227
- """Lowercase, strip, collapse whitespace."""
228
- if not s:
229
- return ""
230
- return re.sub(r"\s+", " ", str(s).strip().lower())
231
-
232
-
233
- def _dedup_key(rec: dict):
234
- """Build a fingerprint for dedup. Returns None if the record can't be keyed."""
235
- f = rec.get("fields", {})
236
- name = _norm(f.get("Name"))
237
- father = _norm(f.get("Father"))
238
- if name or father:
239
- return ("nf", name, father)
240
- addr = _norm(f.get("Address"))
241
- if addr:
242
- addr = re.sub(r"[^a-z0-9]+", " ", addr).strip()
243
- if addr:
244
- return ("a", addr)
245
- return None
246
-
247
-
248
- def _dedup(records: list[dict]) -> list[dict]:
249
- """Remove duplicates. Keeps the record with the most non-null fields."""
250
- seen: dict = {} # key -> index in `result`
251
- result: list[dict] = []
252
-
253
- for rec in records:
254
- key = _dedup_key(rec)
255
- if key is None:
256
- # Can't dedup — keep as-is
257
- result.append(rec)
258
- continue
259
-
260
- idx = seen.get(key)
261
- if idx is None:
262
- seen[key] = len(result)
263
- result.append(rec)
264
- else:
265
- # Collision: keep the record with more populated fields
266
- existing = result[idx]
267
- if len(rec.get("fields", {})) > len(existing.get("fields", {})):
268
- result[idx] = rec
269
-
270
- return result
271
-
272
-
273
- async def search_phone(number: str) -> list[dict]:
274
- """Query all 4 APIs in parallel, merge, and de-duplicate."""
275
- client = await _get_api_client()
276
- tasks = [
277
- _q_cr850(client, number),
278
- _q_inddata(client, number),
279
- _q_hiteck_main(client, number),
280
- _q_hiteck_big(client, number),
281
- ]
282
- chunks = await asyncio.gather(*tasks, return_exceptions=True)
283
-
284
- merged: list[dict] = []
285
- for chunk in chunks:
286
- if isinstance(chunk, list):
287
- merged.extend(chunk)
288
-
289
- before = len(merged)
290
- deduped = _dedup(merged)
291
- after = len(deduped)
292
- if before != after:
293
- log.info("Dedup: %d → %d (removed %d duplicates)", before, after, before - after)
294
-
295
- return deduped
296
-
297
-
298
- # ─────────────────────────────────────────────────────────────
299
- # 5. Telegram API helpers
300
- # ─────────────────────────────────────────────────────────────
301
- async def tg(method: str, **payload):
302
- client = await _get_api_client()
303
- url = f"{TG_API}/{method}"
304
- r = await client.post(url, json=payload)
305
- if r.status_code >= 400:
306
- log.warning("Telegram %s failed: %s %s", method, r.status_code, r.text[:200])
307
- try:
308
- return r.json()
309
- except Exception:
310
- return {}
311
-
312
-
313
- async def send(chat_id: int, text: str, **kw):
314
- kw.setdefault("parse_mode", "HTML")
315
- kw.setdefault("disable_web_page_preview", True)
316
- try:
317
- return await tg("sendMessage", chat_id=chat_id, text=text, **kw)
318
- except Exception:
319
- log.exception("send failed")
320
- return {}
321
-
322
-
323
- async def edit(chat_id: int, message_id: int, text: str, **kw):
324
- kw.setdefault("parse_mode", "HTML")
325
- kw.setdefault("disable_web_page_preview", True)
326
- try:
327
- return await tg("editMessageText", chat_id=chat_id, message_id=message_id, text=text, **kw)
328
- except Exception:
329
- log.exception("edit failed")
330
- return {}
331
-
332
-
333
- async def answer_callback(callback_id: str, **kw):
334
- try:
335
- return await tg("answerCallbackQuery", callback_query_id=callback_id, **kw)
336
- except Exception:
337
- log.exception("answer_callback failed")
338
- return {}
339
-
340
-
341
- # ─────────────────────────────────────────────────────────────
342
- # 6. Formatting
343
- # ─────────────────────────────────────────────────────────────
344
- def _esc(v) -> str:
345
- return html.escape(str(v)) if v is not None else ""
346
-
347
-
348
- def render_record(rec: dict) -> str:
349
- src = _esc(rec.get("source", "Unknown"))
350
- fields = rec.get("fields", {})
351
- lines = [f"🗂 <b>{src}</b>"]
352
- for k, v in fields.items():
353
- if v is None or v == "":
354
- continue
355
- lines.append(f"• <b>{_esc(k)}:</b> {_esc(v)}")
356
- if len(lines) == 1:
357
- lines.append("• <i>no data</i>")
358
- return "\n".join(lines)
359
-
360
-
361
- def render_page(number: str, records: list[dict], page: int) -> str:
362
- total = len(records)
363
- start = page * PAGE_SIZE
364
- chunk = records[start:start + PAGE_SIZE]
365
- if not chunk:
366
- return "No more results."
367
- pages = (total - 1) // PAGE_SIZE + 1
368
- header = (
369
- f"🔎 <b>Results for</b> <code>{_esc(number)}</code> — "
370
- f"page {page + 1}/{pages} ({total} record{'s' if total != 1 else ''})\n\n"
371
- )
372
- body = "\n\n━━━━━━━━━━━━━━━━━━\n\n".join(render_record(r) for r in chunk)
373
- return header + body
374
-
375
-
376
- def pagination_kb(number: str, page: int, total: int):
377
- nav = []
378
- if page > 0:
379
- nav.append({"text": "◀ Prev", "callback_data": f"pg|{number}|{page - 1}"})
380
- if (page + 1) * PAGE_SIZE < total:
381
- nav.append({"text": "Next ▶", "callback_data": f"pg|{number}|{page + 1}"})
382
- return {"inline_keyboard": [nav]} if nav else None
383
-
384
-
385
- # ─────────────────────────────────────────────────────────────
386
- # 7. Per-user state
387
- # ─────────────────────────────────────────────────────────────
388
- _last_search: dict[int, float] = {}
389
- _search_lock = threading.Lock()
390
-
391
- _admin_states: dict[int, str] = {}
392
- _authorized_users: set[int] = set()
393
- _custom_apis: list[str] = []
394
- _admin_lock = threading.Lock()
395
-
396
-
397
- # ─────────────────────────────────────────────────────────────
398
- # 8. Admin helpers
399
- # ─────────────────────────────────────────────────────────────
400
- def _is_admin(user_id: int) -> bool:
401
- return user_id in ADMIN_IDS
402
-
403
-
404
- def _is_authorized(user_id: int) -> bool:
405
- if not AUTH_ENABLED:
406
- return True
407
- with _admin_lock:
408
- return user_id in _authorized_users or _is_admin(user_id)
409
-
410
-
411
- def _admin_menu_kb():
412
- return {
413
- "inline_keyboard": [
414
- [{"text": "➕ Add User", "callback_data": "admin|add_user"}],
415
- [{"text": "➖ Remove User", "callback_data": "admin|remove_user"}],
416
- [{"text": "📋 List Users", "callback_data": "admin|list_users"}],
417
- [{"text": "🔌 Add API", "callback_data": "admin|add_api"}],
418
- [{"text": "🗑 Clear APIs", "callback_data": "admin|clear_apis"}],
419
- [{"text": "📋 List APIs", "callback_data": "admin|list_apis"}],
420
- ]
421
- }
422
-
423
-
424
- # ─────────────────────────────────────────────────────────────
425
- # 9. Message handlers
426
- # ─────────────────────────────────────────────────────────────
427
- async def handle_message(update: dict):
428
- msg = update.get("message") or update.get("edited_message")
429
- if not msg:
430
- return
431
- chat_id = msg["chat"]["id"]
432
- user_id = msg["from"]["id"]
433
- text = (msg.get("text") or "").strip()
434
-
435
- # Admin input capture
436
- with _admin_lock:
437
- state = _admin_states.get(user_id)
438
- if state and _is_admin(user_id):
439
- if state == "await_user_id":
440
- if text.isdigit():
441
- with _admin_lock:
442
- _authorized_users.add(int(text))
443
- _admin_states.pop(user_id, None)
444
- await send(chat_id, f"✅ User <code>{text}</code> authorized.")
445
- else:
446
- await send(chat_id, "❗ Send a numeric Telegram user ID.")
447
- return
448
- if state == "await_api_url":
449
- if text.startswith("http"):
450
- with _admin_lock:
451
- _custom_apis.append(text.rstrip("/"))
452
- _admin_states.pop(user_id, None)
453
- await send(chat_id, f"✅ API added: <code>{_esc(text)}</code>")
454
- else:
455
- await send(chat_id, "❗ Send a valid http(s) URL.")
456
- return
457
-
458
- # Commands
459
- if text.startswith("/"):
460
- cmd = text.split()[0].lower()
461
-
462
- if cmd == "/start":
463
- await send(
464
- chat_id,
465
- "👋 <b>Welcome!</b>\n\n"
466
- "Send me a <b>10‑digit mobile number</b> and I'll search across multiple databases.\n\n"
467
- "Commands:\n"
468
- "/start — this message\n"
469
- "/status — bot health\n"
470
- "/myid — show your Telegram user ID",
471
- )
472
- return
473
-
474
- if cmd == "/myid":
475
- await send(chat_id, f"🆔 Your Telegram user ID: <code>{user_id}</code>")
476
- return
477
-
478
- if cmd == "/status":
479
- await send(
480
- chat_id,
481
- "✅ <b>Bot alive</b>\n"
482
- f"🗂 Sources: 4 (dedup ON)\n"
483
- f"⏱ Rate limit: {RATE_LIMIT_SEC}s\n"
484
- f"📄 Page size: {PAGE_SIZE}",
485
- )
486
- return
487
-
488
- if cmd == "/nemesis":
489
- if not _is_admin(user_id):
490
- await send(chat_id, "⛔ Unauthorized.")
491
- return
492
- await send(chat_id, "🛡 <b>Admin Panel</b>\nChoose an action:", reply_markup=_admin_menu_kb())
493
- return
494
-
495
- return
496
-
497
- if not _is_authorized(user_id):
498
- await send(chat_id, "⛔ You are not authorized to use this bot.")
499
- return
500
-
501
- if not (text.isdigit() and len(text) == 10):
502
- await send(chat_id, "❗ Please send a <b>10‑digit mobile number</b> only.")
503
- return
504
-
505
- now = time.time()
506
- with _search_lock:
507
- last = _last_search.get(chat_id, 0)
508
- if now - last < RATE_LIMIT_SEC:
509
- wait = int(RATE_LIMIT_SEC - (now - last)) + 1
510
- await send(chat_id, f"⏳ Please wait <b>{wait}s</b> before searching again.")
511
- return
512
- _last_search[chat_id] = now
513
-
514
- await send(chat_id, f"🔍 <b>Searching</b> <code>{_esc(text)}</code> across 4 databases …")
515
-
516
- try:
517
- records = await asyncio.wait_for(search_phone(text), timeout=QUERY_TIMEOUT)
518
- except asyncio.TimeoutError:
519
- await send(chat_id, "⏰ Search timed out. Try again in a moment.")
520
- return
521
- except Exception:
522
- log.exception("search_phone unexpected error")
523
- await send(chat_id, "⚠️ Unexpected error during search.")
524
- return
525
-
526
- if not records:
527
- await send(chat_id, f"❌ No records found for <code>{_esc(text)}</code>.")
528
- return
529
-
530
- kb = pagination_kb(text, 0, len(records))
531
- await send(chat_id, render_page(text, records, 0), reply_markup=kb)
532
-
533
-
534
- async def handle_callback(update: dict):
535
- cb = update.get("callback_query")
536
- if not cb:
537
- return
538
- data = cb.get("data") or ""
539
- chat_id = cb["message"]["chat"]["id"]
540
- user_id = cb["from"]["id"]
541
- message_id = cb["message"]["message_id"]
542
- await answer_callback(cb["id"])
543
-
544
- if data.startswith("admin|"):
545
- if not _is_admin(user_id):
546
- await edit(chat_id, message_id, "⛔ Unauthorized.")
547
- return
548
- action = data.split("|", 1)[1]
549
-
550
- if action == "add_user":
551
- with _admin_lock:
552
- _admin_states[user_id] = "await_user_id"
553
- await edit(chat_id, message_id, "Send the Telegram user ID to authorize:")
554
- return
555
-
556
- if action == "remove_user":
557
- with _admin_lock:
558
- users = sorted(_authorized_users)
559
- if not users:
560
- await edit(chat_id, message_id, "No authorized users.", reply_markup=_admin_menu_kb())
561
- else:
562
- txt = "Authorized users:\n" + "\n".join(f"<code>{u}</code>" for u in users)
563
- await edit(chat_id, message_id, txt, reply_markup=_admin_menu_kb())
564
- return
565
-
566
- if action == "list_users":
567
- with _admin_lock:
568
- users = sorted(_authorized_users)
569
- txt = "Authorized users:\n" + ("\n".join(f"<code>{u}</code>" for u in users) if users else "—")
570
- await edit(chat_id, message_id, txt, reply_markup=_admin_menu_kb())
571
- return
572
-
573
- if action == "add_api":
574
- with _admin_lock:
575
- _admin_states[user_id] = "await_api_url"
576
- await edit(chat_id, message_id, "Send the API base URL to add:")
577
- return
578
-
579
- if action == "clear_apis":
580
- with _admin_lock:
581
- _custom_apis.clear()
582
- await edit(chat_id, message_id, "✅ All custom APIs cleared.", reply_markup=_admin_menu_kb())
583
- return
584
-
585
- if action == "list_apis":
586
- with _admin_lock:
587
- apis = list(_custom_apis)
588
- txt = "Custom APIs:\n" + ("\n".join(f"<code>{_esc(a)}</code>" for a in apis) if apis else "—")
589
- await edit(chat_id, message_id, txt, reply_markup=_admin_menu_kb())
590
- return
591
-
592
- return
593
-
594
- if not data.startswith("pg|"):
595
- return
596
-
597
- try:
598
- _, number, page_s = data.split("|", 2)
599
- page = int(page_s)
600
- except Exception:
601
- return
602
-
603
- try:
604
- records = await asyncio.wait_for(search_phone(number), timeout=QUERY_TIMEOUT)
605
- except Exception:
606
- log.exception("callback search failed")
607
- await edit(chat_id, message_id, "⚠️ Could not load that page. Try again.")
608
- return
609
-
610
- if not records:
611
- await edit(chat_id, message_id, "❌ No records found.")
612
- return
613
-
614
- kb = pagination_kb(number, page, len(records))
615
- await edit(chat_id, message_id, render_page(number, records, page), reply_markup=kb)
616
-
617
-
618
- # ─────────────────────────────────────────────────────────────
619
- # 10. FastAPI routes
620
- # ─────────────────────────────────────────────────────────────
621
- async def webhook_handler(request: Request):
622
- if request.headers.get("x-telegram-bot-api-secret-token") != WEBHOOK_SECRET:
623
- return JSONResponse({"ok": False}, status_code=403)
624
- try:
625
- update = await request.json()
626
- except Exception:
627
- return JSONResponse({"ok": True})
628
-
629
- try:
630
- if "message" in update or "edited_message" in update:
631
- await handle_message(update)
632
- elif "callback_query" in update:
633
- await handle_callback(update)
634
- except Exception:
635
- log.exception("Handler error")
636
-
637
- return JSONResponse({"ok": True})
638
-
639
-
640
- async def health_handler(request: Request):
641
- return JSONResponse({"status": "ok", "time": time.time()})
642
-
643
-
644
- # ─────────────────────────────────────────────────────────────
645
- # 11. Gradio UI (ZeroGPU registration)
646
- # ─────────────────────────────────────────────────────────────
647
- @spaces.GPU(duration=10)
648
- def _zerogpu_register():
649
- return "gpu-ready"
650
-
651
-
652
- with gr.Blocks(title="Phone Lookup Bot") as demo:
653
- gr.Markdown("## Phone Lookup Bot\nTelegram webhook endpoint is live.")
654
- _btn = gr.Button("init-gpu", visible=False)
655
- _btn.click(fn=_zerogpu_register, inputs=None, outputs=None)
656
-
657
-
658
- # ─────────────────────────────────────────────────────────────
659
- # 12. Launch + inject routes
660
- # ─────────────────────────────────────────────────────────────
661
- demo.launch(
662
- server_name="0.0.0.0",
663
- server_port=7860,
664
- share=False,
665
- ssr_mode=False,
666
- prevent_thread_lock=True,
667
- )
668
-
669
- live_app = demo.app
670
- live_app.router.routes.insert(0, StarletteRoute("/webhook", webhook_handler, methods=["POST"]))
671
- live_app.router.routes.insert(0, StarletteRoute("/health", health_handler, methods=["GET"]))
672
- live_app.build_middleware_stack()
673
-
674
-
675
- # ─────────────────────────────────────────────────────────────
676
- # 13. setWebhook (sync — own thread)
677
- # ─────────────────────────────────────────────────────────────
678
- def _set_webhook_sync():
679
- time.sleep(3)
680
- try:
681
- with httpx.Client(timeout=15.0) as c:
682
- r = c.post(f"{TG_API}/setWebhook", json={
683
- "url": f"{SPACE_URL}/webhook",
684
- "secret_token": WEBHOOK_SECRET,
685
- "allowed_updates": ["message", "edited_message", "callback_query"],
686
- })
687
- log.info("setWebhook → %s", r.text)
688
- except Exception:
689
- log.exception("setWebhook failed")
690
-
691
-
692
- threading.Thread(target=_set_webhook_sync, daemon=True).start()
693
-
694
- demo.block_thread()