Hfzune commited on
Commit
9402e9b
ยท
verified ยท
1 Parent(s): 54967c2

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +334 -0
app.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, time, threading, asyncio, html, logging
19
+ from typing import Optional
20
+ import httpx
21
+ from fastapi import FastAPI, Request
22
+ from fastapi.responses import JSONResponse
23
+ from starlette.routing import Route as StarletteRoute
24
+ import gradio as gr
25
+ import spaces
26
+
27
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
28
+ # 2. Config
29
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
30
+ BOT_TOKEN = os.environ["BOT_TOKEN"]
31
+ WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "1221")
32
+ SPACE_URL = os.environ["SPACE_URL"].rstrip("/")
33
+ EXTERNAL_API = os.environ["EXTERNAL_API_URL"].rstrip("/")
34
+ EXTERNAL_KEY = os.environ.get("EXTERNAL_API_KEY", "@kzr0x")
35
+ TELEGRAM_PROXY = os.environ.get("TELEGRAM_PROXY", "").rstrip("/")
36
+
37
+ TG_API = (TELEGRAM_PROXY + "/bot" + BOT_TOKEN) if TELEGRAM_PROXY else f"https://api.telegram.org/bot{BOT_TOKEN}"
38
+
39
+ RATE_LIMIT_SEC = 5
40
+ PAGE_SIZE = 4
41
+ QUERY_TIMEOUT = 30
42
+ HTTP_TIMEOUT = httpx.Timeout(30.0, connect=10.0)
43
+
44
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
45
+ log = logging.getLogger("bot")
46
+
47
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
48
+ # 3. External API client
49
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
50
+ _api_client: Optional[httpx.AsyncClient] = None
51
+
52
+ async def _get_api_client() -> httpx.AsyncClient:
53
+ global _api_client
54
+ if _api_client is None or _api_client.is_closed:
55
+ _api_client = httpx.AsyncClient(timeout=HTTP_TIMEOUT, follow_redirects=True)
56
+ return _api_client
57
+
58
+ async def search_phone(number: str) -> list[dict]:
59
+ """Call the external API with the required key parameter."""
60
+ client = await _get_api_client()
61
+ params = {
62
+ "key": EXTERNAL_KEY, # <-- REQUIRED: fixes the 422 error
63
+ "number": number, # <-- matches original app.py's parameter name
64
+ "limit": 10,
65
+ }
66
+ try:
67
+ r = await client.get(f"{EXTERNAL_API}/search", params=params)
68
+ log.info("External API %s โ†’ %s", r.url, r.status_code)
69
+ r.raise_for_status()
70
+ data = r.json()
71
+ except httpx.TimeoutException:
72
+ raise RuntimeError("External API timed out")
73
+ except httpx.HTTPStatusError as e:
74
+ raise RuntimeError(f"External API error {e.response.status_code}")
75
+ except Exception as e:
76
+ raise RuntimeError(f"External API call failed: {e}")
77
+
78
+ # Normalise response โ€” the API may return various shapes
79
+ if isinstance(data, dict):
80
+ results = data.get("results") or data.get("data") or []
81
+ # If the API returns icmr_data / hitek_data separately, merge them
82
+ if not results:
83
+ results = (data.get("icmr_data") or []) + (data.get("hitek_data") or [])
84
+ elif isinstance(data, list):
85
+ results = data
86
+ else:
87
+ results = []
88
+
89
+ normalised = []
90
+ for row in results:
91
+ if not isinstance(row, dict):
92
+ continue
93
+ normalised.append({
94
+ "name": row.get("name") or row.get("Name") or "",
95
+ "fathersName": row.get("fathersName") or row.get("father_name") or "",
96
+ "phoneNumber": row.get("phoneNumber") or row.get("phone") or number,
97
+ "aadharNumber": row.get("aadharNumber") or row.get("aadhaar") or "",
98
+ "otherNumber": row.get("otherNumber") or row.get("other_number") or "",
99
+ "address": row.get("address") or "",
100
+ "district": row.get("district") or "",
101
+ "pincode": row.get("pincode") or "",
102
+ "state": row.get("state") or "",
103
+ "town": row.get("town") or "",
104
+ "source": row.get("source") or "",
105
+ })
106
+ return normalised
107
+
108
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
109
+ # 4. Telegram helpers
110
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
111
+ async def tg(method: str, **payload):
112
+ client = await _get_api_client()
113
+ url = f"{TG_API}/{method}"
114
+ r = await client.post(url, json=payload)
115
+ try:
116
+ r.raise_for_status()
117
+ except httpx.HTTPStatusError:
118
+ log.warning("Telegram %s failed: %s %s", method, r.status_code, r.text[:200])
119
+ return r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
120
+
121
+ async def send(chat_id: int, text: str, **kw):
122
+ kw.setdefault("parse_mode", "HTML")
123
+ kw.setdefault("disable_web_page_preview", True)
124
+ return await tg("sendMessage", chat_id=chat_id, text=text, **kw)
125
+
126
+ async def edit(chat_id: int, message_id: int, text: str, **kw):
127
+ kw.setdefault("parse_mode", "HTML")
128
+ kw.setdefault("disable_web_page_preview", True)
129
+ return await tg("editMessageText", chat_id=chat_id, message_id=message_id, text=text, **kw)
130
+
131
+ async def answer_callback(callback_id: str, **kw):
132
+ return await tg("answerCallbackQuery", callback_query_id=callback_id, **kw)
133
+
134
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
135
+ # 5. Formatting
136
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
137
+ def _esc(v) -> str:
138
+ return html.escape(str(v)) if v is not None else ""
139
+
140
+ def render_record(rec: dict) -> str:
141
+ lines = [
142
+ f"๐Ÿ‘ค <b>Name:</b> {_esc(rec['name']) or 'โ€”'}",
143
+ f"๐Ÿ‘จ <b>Father:</b> {_esc(rec['fathersName']) or 'โ€”'}",
144
+ f"๐Ÿ“ฑ <b>Phone:</b> {_esc(rec['phoneNumber'])}",
145
+ f"๐Ÿ†” <b>Aadhaar:</b> {_esc(rec['aadharNumber']) or 'โ€”'}",
146
+ f"๐Ÿ“ž <b>Other:</b> {_esc(rec['otherNumber']) or 'โ€”'}",
147
+ f"๐Ÿ  <b>Address:</b> {_esc(rec['address']) or 'โ€”'}",
148
+ f"๐Ÿ™ <b>Town:</b> {_esc(rec['town']) or 'โ€”'}",
149
+ f"๐Ÿ“ <b>District:</b> {_esc(rec['district']) or 'โ€”'}",
150
+ f"๐Ÿ“ฎ <b>Pincode:</b> {_esc(rec['pincode']) or 'โ€”'}",
151
+ f"๐Ÿ—บ <b>State:</b> {_esc(rec['state']) or 'โ€”'}",
152
+ f"๐Ÿ—‚ <b>Source:</b> {_esc(rec['source']) or 'โ€”'}",
153
+ ]
154
+ return "\n".join(lines)
155
+
156
+ def render_page(number: str, records: list[dict], page: int) -> str:
157
+ total = len(records)
158
+ start = page * PAGE_SIZE
159
+ chunk = records[start:start + PAGE_SIZE]
160
+ if not chunk:
161
+ return "No more results."
162
+ header = f"๐Ÿ”Ž <b>Results for</b> <code>{_esc(number)}</code> โ€” page {page + 1}/{(total - 1) // PAGE_SIZE + 1}\n\n"
163
+ body = "\n\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n\n".join(render_record(r) for r in chunk)
164
+ return header + body
165
+
166
+ def pagination_kb(number: str, page: int, total: int):
167
+ nav = []
168
+ if page > 0:
169
+ nav.append({"text": "โ—€ Prev", "callback_data": f"pg|{number}|{page - 1}"})
170
+ if (page + 1) * PAGE_SIZE < total:
171
+ nav.append({"text": "Next โ–ถ", "callback_data": f"pg|{number}|{page + 1}"})
172
+ return {"inline_keyboard": [nav]} if nav else None
173
+
174
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
175
+ # 6. State
176
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
177
+ _last_search: dict[int, float] = {}
178
+ _search_lock = threading.Lock()
179
+
180
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
181
+ # 7. Handlers
182
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
183
+ async def handle_message(update: dict):
184
+ msg = update.get("message") or update.get("edited_message")
185
+ if not msg:
186
+ return
187
+ chat_id = msg["chat"]["id"]
188
+ text = (msg.get("text") or "").strip()
189
+
190
+ if text.startswith("/"):
191
+ cmd = text.split()[0].lower()
192
+ if cmd == "/start":
193
+ await send(chat_id,
194
+ "๐Ÿ‘‹ <b>Welcome!</b>\n\n"
195
+ "Send me a <b>10โ€‘digit mobile number</b> and I'll look it up."
196
+ )
197
+ return
198
+ if cmd == "/status":
199
+ await send(chat_id,
200
+ "โœ… <b>Bot alive</b>\n"
201
+ f"โฑ Rate limit: {RATE_LIMIT_SEC}s\n"
202
+ f"๐Ÿ“„ Page size: {PAGE_SIZE}"
203
+ )
204
+ return
205
+ return
206
+
207
+ if not (text.isdigit() and len(text) == 10):
208
+ await send(chat_id, "โ— Please send a <b>10โ€‘digit mobile number</b> only.")
209
+ return
210
+
211
+ now = time.time()
212
+ with _search_lock:
213
+ last = _last_search.get(chat_id, 0)
214
+ if now - last < RATE_LIMIT_SEC:
215
+ wait = int(RATE_LIMIT_SEC - (now - last)) + 1
216
+ await send(chat_id, f"โณ Please wait <b>{wait}s</b> before searching again.")
217
+ return
218
+ _last_search[chat_id] = now
219
+
220
+ await send(chat_id, f"๐Ÿ” <b>Searching</b> <code>{_esc(text)}</code> โ€ฆ")
221
+
222
+ try:
223
+ records = await asyncio.wait_for(search_phone(text), timeout=QUERY_TIMEOUT)
224
+ except asyncio.TimeoutError:
225
+ await send(chat_id, "โฐ Search timed out. Try again in a moment.")
226
+ return
227
+ except RuntimeError as e:
228
+ await send(chat_id, f"โš ๏ธ Search failed: <code>{_esc(str(e))}</code>")
229
+ return
230
+
231
+ if not records:
232
+ await send(chat_id, f"โŒ No records found for <code>{_esc(text)}</code>.")
233
+ return
234
+
235
+ kb = pagination_kb(text, 0, len(records))
236
+ await send(chat_id, render_page(text, records, 0), reply_markup=kb)
237
+
238
+ async def handle_callback(update: dict):
239
+ cb = update.get("callback_query")
240
+ if not cb:
241
+ return
242
+ data = cb.get("data") or ""
243
+ chat_id = cb["message"]["chat"]["id"]
244
+ message_id = cb["message"]["message_id"]
245
+ await answer_callback(cb["id"])
246
+
247
+ if not data.startswith("pg|"):
248
+ return
249
+ _, number, page_s = data.split("|", 2)
250
+ page = int(page_s)
251
+
252
+ try:
253
+ records = await asyncio.wait_for(search_phone(number), timeout=QUERY_TIMEOUT)
254
+ except Exception:
255
+ await edit(chat_id, message_id, "โš ๏ธ Could not load that page. Try again.")
256
+ return
257
+
258
+ if not records:
259
+ await edit(chat_id, message_id, "โŒ No records found.")
260
+ return
261
+
262
+ kb = pagination_kb(number, page, len(records))
263
+ await edit(chat_id, message_id, render_page(number, records, page), reply_markup=kb)
264
+
265
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
266
+ # 8. FastAPI routes
267
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
268
+ async def webhook_handler(request: Request):
269
+ if request.headers.get("x-telegram-bot-api-secret-token") != WEBHOOK_SECRET:
270
+ return JSONResponse({"ok": False}, status_code=403)
271
+ try:
272
+ update = await request.json()
273
+ except Exception:
274
+ return JSONResponse({"ok": True})
275
+
276
+ try:
277
+ if "message" in update or "edited_message" in update:
278
+ await handle_message(update)
279
+ elif "callback_query" in update:
280
+ await handle_callback(update)
281
+ except Exception:
282
+ log.exception("Handler error")
283
+ return JSONResponse({"ok": True})
284
+
285
+ async def health_handler(request: Request):
286
+ return JSONResponse({"status": "ok", "time": time.time()})
287
+
288
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
289
+ # 9. Gradio UI (ZeroGPU registration)
290
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
291
+ @spaces.GPU(duration=10)
292
+ def _zerogpu_register():
293
+ return "gpu-ready"
294
+
295
+ with gr.Blocks(title="ICMR Lookup Bot") as demo:
296
+ gr.Markdown("## ICMR Lookup Bot\nTelegram webhook endpoint is live.")
297
+ _btn = gr.Button("init-gpu", visible=False)
298
+ _btn.click(fn=_zerogpu_register, inputs=None, outputs=None)
299
+
300
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
301
+ # 10. Launch + inject routes
302
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
303
+ demo.launch(
304
+ server_name="0.0.0.0",
305
+ server_port=7860,
306
+ share=False,
307
+ ssr_mode=False,
308
+ prevent_thread_lock=True,
309
+ )
310
+
311
+ live_app = demo.app
312
+ live_app.router.routes.insert(0, StarletteRoute("/webhook", webhook_handler, methods=["POST"]))
313
+ live_app.router.routes.insert(0, StarletteRoute("/health", health_handler, methods=["GET"]))
314
+ live_app.build_middleware_stack()
315
+
316
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
317
+ # 11. Set webhook
318
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
319
+ async def _set_webhook():
320
+ await asyncio.sleep(3)
321
+ client = await _get_api_client()
322
+ try:
323
+ r = await client.post(f"{TG_API}/setWebhook", json={
324
+ "url": f"{SPACE_URL}/webhook",
325
+ "secret_token": WEBHOOK_SECRET,
326
+ "allowed_updates": ["message", "edited_message", "callback_query"],
327
+ })
328
+ log.info("setWebhook โ†’ %s", r.text)
329
+ except Exception:
330
+ log.exception("setWebhook failed")
331
+
332
+ threading.Thread(target=lambda: asyncio.run(_set_webhook()), daemon=True).start()
333
+
334
+ demo.block_thread()