Update app/telegram_bot.py
Browse files- app/telegram_bot.py +83 -132
app/telegram_bot.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
"""
|
| 2 |
-
Telegram Bot β PraisonChat
|
| 3 |
-
|
| 4 |
"""
|
| 5 |
-
import os, json, asyncio, base64, traceback
|
| 6 |
import httpx
|
| 7 |
import config as cfg
|
| 8 |
|
|
@@ -10,39 +10,49 @@ TELEGRAM_API = "https://api.telegram.org/bot{token}/{method}"
|
|
| 10 |
_histories: dict = {}
|
| 11 |
|
| 12 |
|
| 13 |
-
def _url(method
|
| 14 |
return TELEGRAM_API.format(token=cfg.get_telegram_token(), method=method)
|
| 15 |
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
r = await c.post(_url(method), **kwargs)
|
| 20 |
return r.json()
|
| 21 |
|
| 22 |
|
| 23 |
-
async def send_message(chat_id
|
| 24 |
-
if not text.strip(): return True
|
| 25 |
chunks = [text[i:i+3900] for i in range(0, len(text), 3900)]
|
| 26 |
for chunk in chunks:
|
| 27 |
try:
|
| 28 |
-
r = await _post("sendMessage",
|
| 29 |
-
json={"chat_id": chat_id, "text": chunk, "parse_mode": parse_mode})
|
| 30 |
if not r.get("ok"):
|
| 31 |
await _post("sendMessage", json={"chat_id": chat_id, "text": chunk})
|
| 32 |
except Exception as e:
|
| 33 |
-
print(f"[TG]
|
| 34 |
return True
|
| 35 |
|
| 36 |
|
| 37 |
-
async def send_typing(chat_id
|
| 38 |
try:
|
| 39 |
-
await _post("sendChatAction",
|
| 40 |
-
json={"chat_id": chat_id, "action": "typing"})
|
| 41 |
except Exception:
|
| 42 |
pass
|
| 43 |
|
| 44 |
|
| 45 |
-
async def send_voice(chat_id
|
| 46 |
try:
|
| 47 |
audio_bytes = base64.b64decode(audio_b64)
|
| 48 |
async with httpx.AsyncClient(timeout=60) as c:
|
|
@@ -51,25 +61,24 @@ async def send_voice(chat_id: int, audio_b64: str) -> bool:
|
|
| 51 |
data={"chat_id": str(chat_id)})
|
| 52 |
return r.json().get("ok", False)
|
| 53 |
except Exception as e:
|
| 54 |
-
print(f"[TG]
|
| 55 |
return False
|
| 56 |
|
| 57 |
|
| 58 |
-
async def send_photo(chat_id
|
| 59 |
try:
|
| 60 |
img_bytes = base64.b64decode(img_b64)
|
| 61 |
-
mime = f"image/{ext}" if ext else "image/png"
|
| 62 |
async with httpx.AsyncClient(timeout=30) as c:
|
| 63 |
r = await c.post(_url("sendPhoto"),
|
| 64 |
-
files={"photo": (filename, img_bytes,
|
| 65 |
data={"chat_id": str(chat_id), "caption": f"πΌοΈ {filename}"})
|
| 66 |
return r.json().get("ok", False)
|
| 67 |
except Exception as e:
|
| 68 |
-
print(f"[TG]
|
| 69 |
return False
|
| 70 |
|
| 71 |
|
| 72 |
-
async def send_document(chat_id
|
| 73 |
try:
|
| 74 |
file_bytes = base64.b64decode(file_b64)
|
| 75 |
async with httpx.AsyncClient(timeout=60) as c:
|
|
@@ -78,58 +87,48 @@ async def send_document(chat_id: int, file_b64: str, filename: str) -> bool:
|
|
| 78 |
data={"chat_id": str(chat_id), "caption": f"π {filename}"})
|
| 79 |
return r.json().get("ok", False)
|
| 80 |
except Exception as e:
|
| 81 |
-
print(f"[TG]
|
| 82 |
return False
|
| 83 |
|
| 84 |
|
| 85 |
-
async def set_webhook(base_url
|
| 86 |
url = base_url.rstrip("/") + "/telegram/webhook"
|
| 87 |
return await _post("setWebhook",
|
| 88 |
json={"url": url, "allowed_updates": ["message"], "drop_pending_updates": True})
|
| 89 |
|
| 90 |
|
| 91 |
-
async def delete_webhook()
|
| 92 |
return await _post("deleteWebhook", json={})
|
| 93 |
|
| 94 |
|
| 95 |
-
async def get_bot_info()
|
| 96 |
-
return await _post("getMe")
|
| 97 |
|
| 98 |
|
| 99 |
-
async def get_webhook_info()
|
| 100 |
-
return await _post("getWebhookInfo")
|
| 101 |
|
| 102 |
|
| 103 |
-
async def handle_update(update
|
| 104 |
from agent_system import orchestrator
|
| 105 |
|
| 106 |
msg = update.get("message") or update.get("edited_message")
|
| 107 |
-
if not msg:
|
| 108 |
-
return
|
| 109 |
|
| 110 |
chat_id = msg["chat"]["id"]
|
| 111 |
-
text = msg.get("text",
|
| 112 |
-
username = msg.get("from",
|
| 113 |
|
| 114 |
-
if not text:
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
print(f"[TG] {username} ({chat_id}): {text[:80]}")
|
| 118 |
|
| 119 |
# Commands
|
| 120 |
if text == "/start":
|
| 121 |
await send_message(chat_id,
|
| 122 |
-
f"π Hello {username}! I'm *PraisonChat*
|
| 123 |
-
"I
|
| 124 |
-
"
|
| 125 |
-
"
|
| 126 |
-
"π Real code execution\n"
|
| 127 |
-
"π¦ Auto-install any Python package\n"
|
| 128 |
-
"π Voice audio generation\n"
|
| 129 |
-
"πΌοΈ Image generation/processing\n"
|
| 130 |
-
"π File creation & download\n\n"
|
| 131 |
-
"Just type anything β I'll write code to make it happen!\n\n"
|
| 132 |
-
"/clear - Clear history | /help - Help")
|
| 133 |
return
|
| 134 |
|
| 135 |
if text == "/clear":
|
|
@@ -137,29 +136,31 @@ async def handle_update(update: dict, api_key: str, model: str):
|
|
| 137 |
await send_message(chat_id, "β
History cleared!")
|
| 138 |
return
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
if text == "/help":
|
| 141 |
await send_message(chat_id,
|
| 142 |
-
"
|
| 143 |
-
"β’
|
| 144 |
-
"β’
|
| 145 |
-
"β’
|
| 146 |
-
"β’
|
| 147 |
-
"β’
|
| 148 |
-
"β’
|
| 149 |
-
"
|
| 150 |
-
"I write *real Python code* and execute it β not simulated!")
|
| 151 |
return
|
| 152 |
|
| 153 |
-
# Immediate ack
|
| 154 |
await send_typing(chat_id)
|
| 155 |
-
await send_message(chat_id, "β³
|
| 156 |
|
| 157 |
-
history
|
| 158 |
full_response = ""
|
| 159 |
audio_b64 = None
|
| 160 |
has_media = False
|
| 161 |
-
activity_msgs = []
|
| 162 |
-
code_outputs = []
|
| 163 |
|
| 164 |
try:
|
| 165 |
async for chunk_json in orchestrator.stream_response(text, history, api_key, model):
|
|
@@ -168,106 +169,56 @@ async def handle_update(update: dict, api_key: str, model: str):
|
|
| 168 |
except Exception:
|
| 169 |
continue
|
| 170 |
|
| 171 |
-
t = ev.get("type",
|
| 172 |
-
|
| 173 |
-
if t == "thinking":
|
| 174 |
-
pass
|
| 175 |
-
|
| 176 |
-
elif t == "code_executing":
|
| 177 |
-
idx = ev.get("index", 0)
|
| 178 |
-
activity_msgs.append(f"π Running code block {idx+1}β¦")
|
| 179 |
-
|
| 180 |
-
elif t == "code_result":
|
| 181 |
-
stdout = ev.get("stdout", "").strip()
|
| 182 |
-
ok = ev.get("ok", False)
|
| 183 |
-
if stdout and len(stdout) < 400:
|
| 184 |
-
code_outputs.append(f"{'β
' if ok else 'β'} ```\n{stdout}\n```")
|
| 185 |
-
elif not ok:
|
| 186 |
-
stderr = ev.get("stderr","")[:200]
|
| 187 |
-
code_outputs.append(f"β Error: `{stderr}`")
|
| 188 |
-
|
| 189 |
-
elif t == "pkg_install":
|
| 190 |
-
pkg = ev.get("package", "")
|
| 191 |
-
ok = ev.get("ok", False)
|
| 192 |
-
if pkg:
|
| 193 |
-
activity_msgs.append(f"{'π¦β
' if ok else 'π¦β'} `{pkg}`")
|
| 194 |
-
|
| 195 |
-
elif t == "agent_created":
|
| 196 |
-
activity_msgs.append(f"π€ Agent: *{ev.get('name','')}*")
|
| 197 |
-
|
| 198 |
-
elif t == "token":
|
| 199 |
-
full_response += ev.get("content", "")
|
| 200 |
|
|
|
|
|
|
|
| 201 |
elif t == "audio_response":
|
| 202 |
audio_b64 = ev.get("audio_b64")
|
| 203 |
-
has_media
|
| 204 |
-
|
| 205 |
elif t == "image_response":
|
| 206 |
-
img_b64 = ev.get("image_b64",
|
| 207 |
-
filename = ev.get("filename",
|
| 208 |
-
ext = ev.get("ext",
|
| 209 |
if img_b64:
|
| 210 |
await send_typing(chat_id)
|
| 211 |
await send_photo(chat_id, img_b64, filename, ext)
|
| 212 |
has_media = True
|
| 213 |
-
|
| 214 |
elif t == "file_response":
|
| 215 |
-
file_b64 = ev.get("file_b64",
|
| 216 |
-
filename = ev.get("filename",
|
| 217 |
if file_b64:
|
| 218 |
await send_typing(chat_id)
|
| 219 |
await send_document(chat_id, file_b64, filename)
|
| 220 |
has_media = True
|
| 221 |
-
|
| 222 |
-
elif t == "voice_fallback":
|
| 223 |
-
vtext = ev.get("text", "")
|
| 224 |
-
if vtext:
|
| 225 |
-
full_response += f"\n\nπ _{vtext[:300]}_"
|
| 226 |
-
|
| 227 |
elif t == "error":
|
| 228 |
-
|
| 229 |
-
print(f"[TG] Error: {err}")
|
| 230 |
-
await send_message(chat_id, f"β Error: {err[:300]}")
|
| 231 |
return
|
| 232 |
|
| 233 |
except Exception as e:
|
| 234 |
-
print(f"[TG]
|
| 235 |
await send_message(chat_id, f"β Error: {str(e)[:200]}")
|
| 236 |
return
|
| 237 |
|
| 238 |
-
# Send
|
| 239 |
-
if activity_msgs:
|
| 240 |
-
await send_message(chat_id, "\n".join(activity_msgs[:6]))
|
| 241 |
-
|
| 242 |
-
# Send code outputs if short
|
| 243 |
-
if code_outputs and not full_response:
|
| 244 |
-
await send_message(chat_id, "\n\n".join(code_outputs[:3]))
|
| 245 |
-
|
| 246 |
-
# Send main text response
|
| 247 |
clean = (full_response
|
| 248 |
.replace("**","*")
|
| 249 |
-
.replace("
|
| 250 |
-
.replace("</execute>","\n```")
|
| 251 |
-
.replace("[VOICE:","").strip())
|
| 252 |
-
# Remove VOICE] tags
|
| 253 |
import re
|
| 254 |
-
clean = re.sub(r'
|
| 255 |
|
| 256 |
if clean:
|
| 257 |
await send_message(chat_id, clean)
|
| 258 |
-
elif not has_media
|
| 259 |
-
await send_message(chat_id, "_(
|
| 260 |
|
| 261 |
-
# Send voice
|
| 262 |
if audio_b64:
|
| 263 |
await send_typing(chat_id)
|
| 264 |
-
|
| 265 |
-
if not ok:
|
| 266 |
-
await send_message(chat_id, "β οΈ Could not send voice file")
|
| 267 |
|
| 268 |
# Update history
|
| 269 |
-
history.append({"role":
|
| 270 |
-
history.append({"role":
|
| 271 |
_histories[chat_id] = history[-24:]
|
| 272 |
|
| 273 |
-
print(f"[TG]
|
|
|
|
| 1 |
"""
|
| 2 |
+
Telegram Bot β PraisonChat v6
|
| 3 |
+
Fixed: DNS error handling, env var fallback, robust connection
|
| 4 |
"""
|
| 5 |
+
import os, json, asyncio, base64, traceback, socket
|
| 6 |
import httpx
|
| 7 |
import config as cfg
|
| 8 |
|
|
|
|
| 10 |
_histories: dict = {}
|
| 11 |
|
| 12 |
|
| 13 |
+
def _url(method):
|
| 14 |
return TELEGRAM_API.format(token=cfg.get_telegram_token(), method=method)
|
| 15 |
|
| 16 |
|
| 17 |
+
def check_telegram_reachable() -> tuple[bool, str]:
|
| 18 |
+
"""Check if api.telegram.org is reachable via DNS."""
|
| 19 |
+
try:
|
| 20 |
+
socket.setdefaulttimeout(5)
|
| 21 |
+
socket.gethostbyname("api.telegram.org")
|
| 22 |
+
return True, "OK"
|
| 23 |
+
except socket.gaierror as e:
|
| 24 |
+
return False, f"DNS error: api.telegram.org not reachable from this server. Error: {e}"
|
| 25 |
+
except Exception as e:
|
| 26 |
+
return False, str(e)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
async def _post(method, timeout=15, **kwargs):
|
| 30 |
+
async with httpx.AsyncClient(timeout=timeout) as c:
|
| 31 |
r = await c.post(_url(method), **kwargs)
|
| 32 |
return r.json()
|
| 33 |
|
| 34 |
|
| 35 |
+
async def send_message(chat_id, text, parse_mode="Markdown"):
|
| 36 |
+
if not text or not text.strip(): return True
|
| 37 |
chunks = [text[i:i+3900] for i in range(0, len(text), 3900)]
|
| 38 |
for chunk in chunks:
|
| 39 |
try:
|
| 40 |
+
r = await _post("sendMessage", json={"chat_id": chat_id, "text": chunk, "parse_mode": parse_mode})
|
|
|
|
| 41 |
if not r.get("ok"):
|
| 42 |
await _post("sendMessage", json={"chat_id": chat_id, "text": chunk})
|
| 43 |
except Exception as e:
|
| 44 |
+
print(f"[TG] send error: {e}")
|
| 45 |
return True
|
| 46 |
|
| 47 |
|
| 48 |
+
async def send_typing(chat_id):
|
| 49 |
try:
|
| 50 |
+
await _post("sendChatAction", json={"chat_id": chat_id, "action": "typing"})
|
|
|
|
| 51 |
except Exception:
|
| 52 |
pass
|
| 53 |
|
| 54 |
|
| 55 |
+
async def send_voice(chat_id, audio_b64):
|
| 56 |
try:
|
| 57 |
audio_bytes = base64.b64decode(audio_b64)
|
| 58 |
async with httpx.AsyncClient(timeout=60) as c:
|
|
|
|
| 61 |
data={"chat_id": str(chat_id)})
|
| 62 |
return r.json().get("ok", False)
|
| 63 |
except Exception as e:
|
| 64 |
+
print(f"[TG] voice error: {e}")
|
| 65 |
return False
|
| 66 |
|
| 67 |
|
| 68 |
+
async def send_photo(chat_id, img_b64, filename, ext):
|
| 69 |
try:
|
| 70 |
img_bytes = base64.b64decode(img_b64)
|
|
|
|
| 71 |
async with httpx.AsyncClient(timeout=30) as c:
|
| 72 |
r = await c.post(_url("sendPhoto"),
|
| 73 |
+
files={"photo": (filename, img_bytes, f"image/{ext}")},
|
| 74 |
data={"chat_id": str(chat_id), "caption": f"πΌοΈ {filename}"})
|
| 75 |
return r.json().get("ok", False)
|
| 76 |
except Exception as e:
|
| 77 |
+
print(f"[TG] photo error: {e}")
|
| 78 |
return False
|
| 79 |
|
| 80 |
|
| 81 |
+
async def send_document(chat_id, file_b64, filename):
|
| 82 |
try:
|
| 83 |
file_bytes = base64.b64decode(file_b64)
|
| 84 |
async with httpx.AsyncClient(timeout=60) as c:
|
|
|
|
| 87 |
data={"chat_id": str(chat_id), "caption": f"π {filename}"})
|
| 88 |
return r.json().get("ok", False)
|
| 89 |
except Exception as e:
|
| 90 |
+
print(f"[TG] doc error: {e}")
|
| 91 |
return False
|
| 92 |
|
| 93 |
|
| 94 |
+
async def set_webhook(base_url):
|
| 95 |
url = base_url.rstrip("/") + "/telegram/webhook"
|
| 96 |
return await _post("setWebhook",
|
| 97 |
json={"url": url, "allowed_updates": ["message"], "drop_pending_updates": True})
|
| 98 |
|
| 99 |
|
| 100 |
+
async def delete_webhook():
|
| 101 |
return await _post("deleteWebhook", json={})
|
| 102 |
|
| 103 |
|
| 104 |
+
async def get_bot_info():
|
| 105 |
+
return await _post("getMe", timeout=10)
|
| 106 |
|
| 107 |
|
| 108 |
+
async def get_webhook_info():
|
| 109 |
+
return await _post("getWebhookInfo", timeout=10)
|
| 110 |
|
| 111 |
|
| 112 |
+
async def handle_update(update, api_key, model):
|
| 113 |
from agent_system import orchestrator
|
| 114 |
|
| 115 |
msg = update.get("message") or update.get("edited_message")
|
| 116 |
+
if not msg: return
|
|
|
|
| 117 |
|
| 118 |
chat_id = msg["chat"]["id"]
|
| 119 |
+
text = msg.get("text","").strip()
|
| 120 |
+
username = msg.get("from",{}).get("first_name","User")
|
| 121 |
|
| 122 |
+
if not text: return
|
| 123 |
+
print(f"[TG] {username} ({chat_id}): {text[:60]}")
|
|
|
|
|
|
|
| 124 |
|
| 125 |
# Commands
|
| 126 |
if text == "/start":
|
| 127 |
await send_message(chat_id,
|
| 128 |
+
f"π Hello {username}! I'm *PraisonChat* π¦\n\n"
|
| 129 |
+
"I'm an autonomous AI agent with real code execution.\n\n"
|
| 130 |
+
"Commands: /clear /help /status\n\n"
|
| 131 |
+
"Just type anything!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
return
|
| 133 |
|
| 134 |
if text == "/clear":
|
|
|
|
| 136 |
await send_message(chat_id, "β
History cleared!")
|
| 137 |
return
|
| 138 |
|
| 139 |
+
if text == "/status":
|
| 140 |
+
await send_message(chat_id,
|
| 141 |
+
f"β
Online\nModel: `{model}`\n"
|
| 142 |
+
f"API: `{'set β
' if api_key else 'missing β'}`")
|
| 143 |
+
return
|
| 144 |
+
|
| 145 |
if text == "/help":
|
| 146 |
await send_message(chat_id,
|
| 147 |
+
"*PraisonChat* can:\n"
|
| 148 |
+
"β’ π Search the web in real-time\n"
|
| 149 |
+
"β’ π Tell you date and time\n"
|
| 150 |
+
"β’ π Execute Python code\n"
|
| 151 |
+
"β’ π Generate voice audio\n"
|
| 152 |
+
"β’ π Create charts and images\n"
|
| 153 |
+
"β’ π€ Spawn sub-agents\n\n"
|
| 154 |
+
"Try: `What time is it?` or `Search for latest AI news`")
|
|
|
|
| 155 |
return
|
| 156 |
|
|
|
|
| 157 |
await send_typing(chat_id)
|
| 158 |
+
await send_message(chat_id, "β³ Working on itβ¦")
|
| 159 |
|
| 160 |
+
history = _histories.get(chat_id, [])
|
| 161 |
full_response = ""
|
| 162 |
audio_b64 = None
|
| 163 |
has_media = False
|
|
|
|
|
|
|
| 164 |
|
| 165 |
try:
|
| 166 |
async for chunk_json in orchestrator.stream_response(text, history, api_key, model):
|
|
|
|
| 169 |
except Exception:
|
| 170 |
continue
|
| 171 |
|
| 172 |
+
t = ev.get("type","")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
+
if t == "token":
|
| 175 |
+
full_response += ev.get("content","")
|
| 176 |
elif t == "audio_response":
|
| 177 |
audio_b64 = ev.get("audio_b64")
|
| 178 |
+
has_media = True
|
|
|
|
| 179 |
elif t == "image_response":
|
| 180 |
+
img_b64 = ev.get("image_b64","")
|
| 181 |
+
filename = ev.get("filename","image.png")
|
| 182 |
+
ext = ev.get("ext","png")
|
| 183 |
if img_b64:
|
| 184 |
await send_typing(chat_id)
|
| 185 |
await send_photo(chat_id, img_b64, filename, ext)
|
| 186 |
has_media = True
|
|
|
|
| 187 |
elif t == "file_response":
|
| 188 |
+
file_b64 = ev.get("file_b64","")
|
| 189 |
+
filename = ev.get("filename","file")
|
| 190 |
if file_b64:
|
| 191 |
await send_typing(chat_id)
|
| 192 |
await send_document(chat_id, file_b64, filename)
|
| 193 |
has_media = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
elif t == "error":
|
| 195 |
+
await send_message(chat_id, f"β {ev.get('message','Error')[:300]}")
|
|
|
|
|
|
|
| 196 |
return
|
| 197 |
|
| 198 |
except Exception as e:
|
| 199 |
+
print(f"[TG] exception: {e}\n{traceback.format_exc()}")
|
| 200 |
await send_message(chat_id, f"β Error: {str(e)[:200]}")
|
| 201 |
return
|
| 202 |
|
| 203 |
+
# Send text response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
clean = (full_response
|
| 205 |
.replace("**","*")
|
| 206 |
+
.replace("[SPEAK:","").replace("]",""))
|
|
|
|
|
|
|
|
|
|
| 207 |
import re
|
| 208 |
+
clean = re.sub(r'<[^>]+>.*?</[^>]+>', '', clean, flags=re.DOTALL).strip()
|
| 209 |
|
| 210 |
if clean:
|
| 211 |
await send_message(chat_id, clean)
|
| 212 |
+
elif not has_media:
|
| 213 |
+
await send_message(chat_id, "_(Done β no text output)_")
|
| 214 |
|
|
|
|
| 215 |
if audio_b64:
|
| 216 |
await send_typing(chat_id)
|
| 217 |
+
await send_voice(chat_id, audio_b64)
|
|
|
|
|
|
|
| 218 |
|
| 219 |
# Update history
|
| 220 |
+
history.append({"role":"user","content":text})
|
| 221 |
+
history.append({"role":"assistant","content":full_response or "(media)"})
|
| 222 |
_histories[chat_id] = history[-24:]
|
| 223 |
|
| 224 |
+
print(f"[TG] Replied to {username}: {len(full_response)} chars")
|