05-sep: [proyecto2] bot Telegram para OpenCode en VPS (serve + bot + systemd, listo para token)
This commit is contained in:
607
vps_opencode_bot/opencode_bot.py
Normal file
607
vps_opencode_bot/opencode_bot.py
Normal file
@@ -0,0 +1,607 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
opencode_bot.py — Bot de Telegram que usa OpenCode (opencode serve) como
|
||||
interfaz de codificacion desde el movil.
|
||||
|
||||
Corre en el VPS junto a `opencode serve`. Long-polling a Telegram, whitelist
|
||||
por chat_id, una sesion persistente por chat y proyecto, entrega de codigo
|
||||
como <pre> y ficheros como documento adjunto.
|
||||
|
||||
Configuracion: variables en el .env del mismo directorio (ver .env.ejemplo).
|
||||
"""
|
||||
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
import requests
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
ENV = {}
|
||||
try:
|
||||
for linea in open(os.path.join(BASE, ".env"), encoding="utf-8"):
|
||||
linea = linea.strip()
|
||||
if linea and not linea.startswith("#") and "=" in linea:
|
||||
k, v = linea.split("=", 1)
|
||||
ENV[k.strip()] = v.strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
TOKEN = ENV.get("TELEGRAM_BOT_TOKEN", "")
|
||||
ALLOWED = ENV.get("TELEGRAM_CHAT_ID", "")
|
||||
OC_HOST = ENV.get("OC_HOST", "127.0.0.1")
|
||||
OC_PORT = ENV.get("OC_PORT", "4097")
|
||||
OC_USER = ENV.get("OC_USER", "opencode")
|
||||
OC_PASS = ENV.get("OC_PASS", "")
|
||||
GH = ENV.get("GH_ROOT", "/home/Github")
|
||||
REPO_ENV = ENV.get("REPO_ENV_FILE", "/etc/opencode-tg/repo.env")
|
||||
ESTADO_FILE = os.path.join(BASE, "estado.json")
|
||||
OFFSET_FILE = os.path.join(BASE, "offset.txt")
|
||||
LOG_FILE = os.path.join(BASE, "opencode_bot.log")
|
||||
|
||||
API = f"https://api.telegram.org/bot{TOKEN}"
|
||||
OC = f"http://{OC_HOST}:{OC_PORT}"
|
||||
AUTH = (OC_USER, OC_PASS)
|
||||
|
||||
bloqueos = {} # chat_id -> True (procesando mensaje)
|
||||
sesiones = {} # chat_id -> {'repo': slug, 'session': id, 'model': obj|None}
|
||||
repos_cache = [] # lista de {'slug','dir'}
|
||||
|
||||
|
||||
def log(linea):
|
||||
try:
|
||||
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
||||
f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {linea}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def cargar_estado():
|
||||
global sesiones
|
||||
try:
|
||||
sesiones = json.load(open(ESTADO_FILE, encoding="utf-8"))
|
||||
except Exception:
|
||||
sesiones = {}
|
||||
|
||||
|
||||
def guardar_estado():
|
||||
try:
|
||||
json.dump(sesiones, open(ESTADO_FILE, "w", encoding="utf-8"),
|
||||
ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
log(f"estado error: {e}")
|
||||
|
||||
|
||||
def cargar_offset():
|
||||
try:
|
||||
return int(open(OFFSET_FILE).read().strip())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def guardar_offset(offset):
|
||||
try:
|
||||
open(OFFSET_FILE, "w").write(str(offset))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def tg(method, data=None, files=None):
|
||||
try:
|
||||
return requests.post(f"{API}/{method}", data=data, files=files,
|
||||
timeout=40).json()
|
||||
except Exception as e:
|
||||
log(f"tg {method} error: {e}")
|
||||
return {"ok": False}
|
||||
|
||||
|
||||
def enviar(chat_id, texto, parse="HTML"):
|
||||
for i in range(0, len(texto), 3900):
|
||||
tg("sendMessage", {"chat_id": chat_id, "text": texto[i:i + 3900],
|
||||
"parse_mode": parse})
|
||||
time.sleep(0.3)
|
||||
|
||||
|
||||
def enviar_doc(chat_id, ruta, caption=None):
|
||||
if not os.path.isfile(ruta):
|
||||
return
|
||||
try:
|
||||
with open(ruta, "rb") as f:
|
||||
tg("sendDocument", {"chat_id": chat_id, "caption": caption},
|
||||
{"document": (os.path.basename(ruta), f)})
|
||||
except Exception as e:
|
||||
log(f"doc error {ruta}: {e}")
|
||||
|
||||
|
||||
def teclado(filas):
|
||||
return {"inline_keyboard": filas}
|
||||
|
||||
|
||||
def tg_con_teclado(chat_id, texto, filas):
|
||||
tg("sendMessage", {"chat_id": chat_id, "text": texto, "parse_mode": "HTML",
|
||||
"reply_markup": json.dumps(teclado(filas))})
|
||||
|
||||
|
||||
def editar(chat_id, mid, texto, filas=None):
|
||||
data = {"chat_id": chat_id, "message_id": mid, "text": texto,
|
||||
"parse_mode": "HTML"}
|
||||
if filas is not None:
|
||||
data["reply_markup"] = json.dumps(teclado(filas))
|
||||
tg("editMessageText", data)
|
||||
|
||||
|
||||
def typing(chat_id):
|
||||
"""Manda sendChatAction 'typing' cada 4 s hasta que se detenga."""
|
||||
stop = threading.Event()
|
||||
|
||||
def loop():
|
||||
while not stop.wait(4):
|
||||
tg("sendChatAction", {"chat_id": chat_id, "action": "typing"})
|
||||
|
||||
h = threading.Thread(target=loop, daemon=True)
|
||||
h.start()
|
||||
return stop
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- API --
|
||||
|
||||
def oc_get(ruta, timeout=15):
|
||||
try:
|
||||
r = requests.get(f"{OC}{ruta}", auth=AUTH, timeout=timeout)
|
||||
return r.status_code, r.json() if r.status_code == 200 else None
|
||||
except Exception as e:
|
||||
return 0, str(e)
|
||||
|
||||
|
||||
def oc_post(ruta, payload, timeout=600):
|
||||
try:
|
||||
r = requests.post(f"{OC}{ruta}", auth=AUTH, json=payload,
|
||||
timeout=timeout)
|
||||
try:
|
||||
return r.status_code, r.json()
|
||||
except Exception:
|
||||
return r.status_code, r.text
|
||||
except Exception as e:
|
||||
return 0, str(e)
|
||||
|
||||
|
||||
def health_ok():
|
||||
code, _ = oc_get("/global/health", timeout=5)
|
||||
return code == 200
|
||||
|
||||
|
||||
def listar_repos():
|
||||
"""Repos disponibles: directorios de GH con .git."""
|
||||
global repos_cache
|
||||
lista = []
|
||||
try:
|
||||
for nombre in sorted(os.listdir(GH)):
|
||||
ruta = os.path.join(GH, nombre)
|
||||
if os.path.isdir(os.path.join(ruta, ".git")):
|
||||
lista.append({"slug": nombre, "dir": ruta})
|
||||
except Exception as e:
|
||||
log(f"repos error: {e}")
|
||||
repos_cache = lista
|
||||
return lista
|
||||
|
||||
|
||||
def repo_activo():
|
||||
"""Lee el repo en el que corre el serve (fichero del servicio)."""
|
||||
try:
|
||||
for linea in open(REPO_ENV):
|
||||
if linea.startswith("REPO_DIR="):
|
||||
ruta = linea.strip().split("=", 1)[1]
|
||||
return os.path.basename(ruta.rstrip("/")), ruta
|
||||
except Exception:
|
||||
pass
|
||||
return None, None
|
||||
|
||||
|
||||
def crear_sesion(titulo=None):
|
||||
payload = {}
|
||||
if titulo:
|
||||
payload["title"] = titulo[:80]
|
||||
code, data = oc_post("/session", payload, timeout=40)
|
||||
if code != 200 or not data or not data.get("id"):
|
||||
return None, data
|
||||
return data["id"], data
|
||||
|
||||
|
||||
def enviar_mensaje(session_id, texto, modelo=None, timeout=900):
|
||||
parts = [{"type": "text", "text": texto}]
|
||||
payload = {"parts": parts}
|
||||
if modelo:
|
||||
# el body.model exige modelID + providerID
|
||||
payload["model"] = {
|
||||
"providerID": modelo.get("providerID"),
|
||||
"modelID": modelo.get("id"),
|
||||
}
|
||||
code, data = oc_post(f"/session/{session_id}/message", payload,
|
||||
timeout=timeout)
|
||||
if code != 200:
|
||||
return None, data
|
||||
return data, None
|
||||
|
||||
|
||||
def listar_modelos():
|
||||
"""Devuelve lista de {id, providerID, name} de providers conectados."""
|
||||
code, data = oc_get("/config/providers")
|
||||
modelos = []
|
||||
if code != 200 or not data:
|
||||
return modelos
|
||||
for p in data.get("providers", []):
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
for mid, mobj in (p.get("models") or {}).items():
|
||||
if mobj.get("status") == "active":
|
||||
modelos.append({
|
||||
"id": mid,
|
||||
"providerID": p.get("id"),
|
||||
"name": mobj.get("name") or mid,
|
||||
})
|
||||
return modelos
|
||||
|
||||
|
||||
# ------------------------------------------------------------ formateo ----
|
||||
|
||||
def _escape(t):
|
||||
return html.escape(t, quote=False)
|
||||
|
||||
|
||||
def _trocear_codigo(texto):
|
||||
"""Trocea en [('txt', str) | ('code', str)] por bloques ```."""
|
||||
partes = []
|
||||
resto = texto
|
||||
while "```" in resto:
|
||||
idx = resto.index("```")
|
||||
if idx > 0:
|
||||
partes.append(("txt", resto[:idx]))
|
||||
resto = resto[idx + 3:]
|
||||
fin = resto.find("```")
|
||||
if fin == -1:
|
||||
partes.append(("txt", resto))
|
||||
return partes
|
||||
partes.append(("code", resto[:fin]))
|
||||
resto = resto[fin + 3:]
|
||||
if resto:
|
||||
partes.append(("txt", resto))
|
||||
return partes
|
||||
|
||||
|
||||
def formatear_html(texto):
|
||||
"""Convierte la respuesta del modelo a HTML simple seguro."""
|
||||
salida = []
|
||||
for tipo, bloque in _trocear_codigo(texto):
|
||||
if tipo == "code":
|
||||
salida.append(f"<pre>{_escape(bloque.strip())}</pre>")
|
||||
continue
|
||||
bloque = _escape(bloque)
|
||||
bloque = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", bloque)
|
||||
bloque = re.sub(r"`([^`]+)`", r"<code>\1</code>", bloque)
|
||||
salida.append(bloque)
|
||||
return "\n".join(salida)
|
||||
|
||||
|
||||
def extraer_respuesta(data):
|
||||
"""Devuelve (texto, adjuntos[path...]) desde la respuesta del modelo."""
|
||||
texto = ""
|
||||
adjuntos = []
|
||||
for p in data.get("parts") or []:
|
||||
t = p.get("type")
|
||||
if t == "text" and p.get("text"):
|
||||
texto += ("" if not texto else "\n") + p["text"]
|
||||
return texto, adjuntos
|
||||
|
||||
|
||||
def _rutas_de_partes(hist, t0):
|
||||
"""Recoge rutas de ficheros tocados tras t0 desde el historial."""
|
||||
rutas = []
|
||||
for item in hist or []:
|
||||
info = item.get("info") or {}
|
||||
if info.get("role") != "assistant":
|
||||
continue
|
||||
for p in item.get("parts") or []:
|
||||
t = p.get("type")
|
||||
inicio = (p.get("time") or {}).get("start") or 0
|
||||
if t0 and inicio and inicio < t0:
|
||||
continue
|
||||
if t in ("file", "patch"):
|
||||
for clave in ("path", "filepath"):
|
||||
if p.get(clave):
|
||||
rutas.append(p[clave])
|
||||
elif t == "tool":
|
||||
estado = p.get("state")
|
||||
if isinstance(estado, dict):
|
||||
archivos = ((estado.get("metadata") or {}).get("files")
|
||||
or [])
|
||||
for a in archivos:
|
||||
if a.get("filePath"):
|
||||
rutas.append(a["filePath"])
|
||||
# dedupe manteniendo orden
|
||||
vistos = set()
|
||||
unicas = []
|
||||
for r in rutas:
|
||||
if r not in vistos:
|
||||
vistos.add(r)
|
||||
unicas.append(r)
|
||||
return unicas
|
||||
|
||||
|
||||
# ------------------------------------------------------------- flujo -----
|
||||
|
||||
def ok_sesion(chat):
|
||||
"""Devuelve (session_id, repo_actual) garantizando sesion creada."""
|
||||
st = sesiones.get(str(chat)) or {}
|
||||
slug, ruta = repo_activo()
|
||||
st_repo = st.get("repo")
|
||||
if not slug:
|
||||
return None, None
|
||||
if st_repo != slug or not st.get("session"):
|
||||
sid, err = crear_sesion()
|
||||
if not sid:
|
||||
return None, err
|
||||
st = {"repo": slug, "session": sid, "model": st.get("model")}
|
||||
sesiones[str(chat)] = st
|
||||
guardar_estado()
|
||||
return st["session"], None
|
||||
|
||||
|
||||
def procesar_texto(chat, texto):
|
||||
sid, err = ok_sesion(chat)
|
||||
if not sid:
|
||||
enviar(chat, f"❌ No hay sesión OpenCode disponible: {err}")
|
||||
return
|
||||
st = sesiones.get(str(chat)) or {}
|
||||
modelo = st.get("model")
|
||||
stop = typing(chat)
|
||||
t0 = int(time.time() * 1000)
|
||||
try:
|
||||
data, err = enviar_mensaje(sid, texto, modelo=modelo)
|
||||
finally:
|
||||
stop.set()
|
||||
if data is None:
|
||||
enviar(chat, f"❌ Error del servidor OpenCode: {err}")
|
||||
return
|
||||
rtexto, _ = extraer_respuesta(data)
|
||||
# ficheros tocados en este turno
|
||||
adjuntos = []
|
||||
try:
|
||||
code, hist = oc_get(f"/session/{sid}/message?limit=8")
|
||||
if code == 200 and isinstance(hist, list):
|
||||
adjuntos = [r for r in _rutas_de_partes(hist, t0)
|
||||
if os.path.isfile(r)]
|
||||
except Exception as e:
|
||||
log(f"adjuntos error: {e}")
|
||||
if rtexto:
|
||||
enviar(chat, formatear_html(rtexto))
|
||||
if adjuntos:
|
||||
enviar(chat, f"📎 {len(adjuntos)} fichero(s) del turno:")
|
||||
for ruta in adjuntos[:6]:
|
||||
enviar_doc(chat, ruta)
|
||||
if not rtexto and not adjuntos:
|
||||
enviar(chat, "🤔 (sin respuesta de texto. Revisa la sesión en el TUI)")
|
||||
|
||||
|
||||
def cambiar_repo(chat, slug):
|
||||
ruta = os.path.join(GH, slug)
|
||||
if not os.path.isdir(os.path.join(ruta, ".git")):
|
||||
enviar(chat, f"❌ Repo no válido: {slug}")
|
||||
return
|
||||
try:
|
||||
with open(REPO_ENV, "w") as f:
|
||||
f.write(f"REPO_DIR={ruta}\n")
|
||||
subprocess.run(["systemctl", "restart", "opencode-serve"],
|
||||
timeout=60, capture_output=True)
|
||||
except Exception as e:
|
||||
log(f"cambiar_repo error: {e}")
|
||||
enviar(chat, f"❌ No pude reiniciar el serve: {e}")
|
||||
return
|
||||
for t in range(20):
|
||||
if health_ok():
|
||||
break
|
||||
time.sleep(1)
|
||||
# sesion nueva en el repo nuevo
|
||||
st = sesiones.get(str(chat)) or {}
|
||||
sid, err = crear_sesion()
|
||||
if sid:
|
||||
sesiones[str(chat)] = {"repo": slug, "session": sid,
|
||||
"model": st.get("model")}
|
||||
guardar_estado()
|
||||
enviar(chat, f"📂 Repo activo: <b>{slug}</b>")
|
||||
if not sid:
|
||||
enviar(chat, f"⚠️ El serve no respondió aún: {err}")
|
||||
|
||||
|
||||
# ------------------------------------------------------------ comandos ----
|
||||
|
||||
def cmd_ayuda(chat):
|
||||
enviar(chat, (
|
||||
"<b>🤖 OpenCode por Telegram</b>\n"
|
||||
"Escribe cualquier cosa y lo hará el agente en el repo activo.\n\n"
|
||||
"<b>Comandos:</b>\n"
|
||||
"<code>/nueva [texto]</code> — nueva sesión (borra contexto)\n"
|
||||
"<code>/proyecto</code> — elegir repo de trabajo\n"
|
||||
"<code>/modelo</code> — elegir modelo\n"
|
||||
"<code>/estado</code> — repo y modelo actuales\n"
|
||||
"<code>/ayuda</code> — esta ayuda\n\n"
|
||||
"Los ficheros que cree te llegarán como documentos."
|
||||
))
|
||||
|
||||
|
||||
def cmd_estado(chat):
|
||||
slug, ruta = repo_activo()
|
||||
st = sesiones.get(str(chat)) or {}
|
||||
modelo = (st.get("model") or {}).get("name") or "por defecto"
|
||||
sid = st.get("session") or "—"
|
||||
enviar(chat, (
|
||||
"📂 <b>Estado</b>\n"
|
||||
f"Repo: <b>{slug}</b> ({ruta})\n"
|
||||
f"Sesión: <code>{sid[:20]}…</code>\n"
|
||||
f"Modelo: <b>{modelo}</b>\n\n"
|
||||
"Usa <code>/proyecto</code> para cambiar de repo."
|
||||
))
|
||||
|
||||
|
||||
def cmd_nueva(chat, resto):
|
||||
st = sesiones.get(str(chat)) or {}
|
||||
slug, _ = repo_activo()
|
||||
sid, err = crear_sesion()
|
||||
if not sid:
|
||||
enviar(chat, f"❌ No pude crear sesión: {err}")
|
||||
return
|
||||
sesiones[str(chat)] = {"repo": slug, "session": sid, "model": st.get("model")}
|
||||
guardar_estado()
|
||||
if resto:
|
||||
procesar_texto(chat, resto)
|
||||
else:
|
||||
enviar(chat, "🧠 Sesión nueva creada. Dime qué hago.")
|
||||
|
||||
|
||||
def cmd_proyecto(chat):
|
||||
lista = listar_repos()
|
||||
if not lista:
|
||||
enviar(chat, "No hay repos en GH_ROOT.")
|
||||
return
|
||||
slug_act, _ = repo_activo()
|
||||
filas = []
|
||||
for i, r in enumerate(lista[:14]):
|
||||
marca = " ✅" if r["slug"] == slug_act else ""
|
||||
filas.append([{"text": r["slug"] + marca,
|
||||
"callback_data": f"cb:repo:{i}"}])
|
||||
tg_con_teclado(chat, "📂 Elige repo de trabajo:", filas)
|
||||
|
||||
|
||||
def cmd_modelo(chat):
|
||||
modelos = listar_modelos()
|
||||
if not modelos:
|
||||
enviar(chat, "No hay modelos disponibles (¿serve arrancado?).")
|
||||
return
|
||||
st = sesiones.get(str(chat)) or {}
|
||||
actual = (st.get("model") or {}).get("id")
|
||||
filas = []
|
||||
for m in modelos[:30]:
|
||||
marca = " ✅" if m["id"] == actual else ""
|
||||
texto = f"{m['name']}{marca}"
|
||||
filas.append([{"text": texto[:55],
|
||||
"callback_data": f"cb:modelo:{m['providerID']}:{m['id']}"}])
|
||||
filas.append([{"text": "↩️ Por defecto", "callback_data": "cb:modelo:def"}])
|
||||
tg_con_teclado(chat, "🧠 Elige modelo (se aplica a partir del próximo mensaje):", filas)
|
||||
|
||||
|
||||
def responder_callback(callback):
|
||||
cid = callback.get("id")
|
||||
data = callback.get("data") or ""
|
||||
msg = callback.get("message") or {}
|
||||
chat = (msg.get("chat") or {}).get("id")
|
||||
mid = msg.get("message_id")
|
||||
tg("answerCallbackQuery", {"callback_query_id": cid})
|
||||
if not data.startswith("cb:"):
|
||||
return
|
||||
partes = data.split(":")
|
||||
if partes[1] == "repo":
|
||||
try:
|
||||
idx = int(partes[2])
|
||||
r = repos_cache[idx]
|
||||
except Exception:
|
||||
return
|
||||
editar(chat, mid, f"Cambiando a <b>{r['slug']}</b>…")
|
||||
cambiar_repo(chat, r["slug"])
|
||||
elif partes[1] == "modelo":
|
||||
if len(partes) == 3 and partes[2] == "def":
|
||||
st = sesiones.get(str(chat)) or {}
|
||||
st["model"] = None
|
||||
sesiones[str(chat)] = st
|
||||
guardar_estado()
|
||||
editar(chat, mid, "🧠 Modelo: <b>por defecto</b> (config del repo)")
|
||||
return
|
||||
provider, mid_modelo = partes[2], ":".join(partes[3:])
|
||||
modelos = listar_modelos()
|
||||
elegido = next((m for m in modelos
|
||||
if m["providerID"] == provider and m["id"] == mid_modelo),
|
||||
None)
|
||||
if not elegido:
|
||||
return
|
||||
st = sesiones.get(str(chat)) or {}
|
||||
st["model"] = elegido
|
||||
sesiones[str(chat)] = st
|
||||
guardar_estado()
|
||||
editar(chat, mid, f"🧠 Modelo: <b>{elegido['name']}</b>")
|
||||
|
||||
|
||||
def procesar(texto, chat):
|
||||
texto = texto.strip()
|
||||
bajo = texto.lower()
|
||||
if not bajo.startswith("/"):
|
||||
# pendiente? No; todo texto normal es una peticion para OpenCode.
|
||||
if bloqueos.get(chat):
|
||||
enviar(chat, "⏳ Aún estoy procesando tu último mensaje…")
|
||||
return
|
||||
bloqueos[chat] = True
|
||||
try:
|
||||
procesar_texto(chat, texto)
|
||||
finally:
|
||||
bloqueos[chat] = False
|
||||
return
|
||||
|
||||
partes = texto.split(None, 1)
|
||||
comando = partes[0].split("@", 1)[0].lower()
|
||||
resto = partes[1].strip() if len(partes) > 1 else ""
|
||||
if comando in ("/start", "/ayuda", "/help"):
|
||||
cmd_ayuda(chat)
|
||||
elif comando in ("/nueva", "/nuevo"):
|
||||
cmd_nueva(chat, resto)
|
||||
elif comando in ("/proyecto", "/repo"):
|
||||
cmd_proyecto(chat)
|
||||
elif comando == "/modelo":
|
||||
cmd_modelo(chat)
|
||||
elif comando == "/estado":
|
||||
cmd_estado(chat)
|
||||
else:
|
||||
enviar(chat, "No reconozco ese comando. Usa /ayuda.")
|
||||
|
||||
|
||||
def main():
|
||||
if not TOKEN:
|
||||
print("Falta TELEGRAM_BOT_TOKEN en .env")
|
||||
return
|
||||
cargar_estado()
|
||||
listar_repos()
|
||||
log("Bot iniciado")
|
||||
offset = cargar_offset()
|
||||
while True:
|
||||
try:
|
||||
params = {"timeout": 30}
|
||||
if offset:
|
||||
params["offset"] = offset
|
||||
r = requests.get(f"{API}/getUpdates", params=params,
|
||||
timeout=45).json()
|
||||
for upd in r.get("result", []):
|
||||
offset = upd["update_id"] + 1
|
||||
guardar_offset(offset)
|
||||
if upd.get("callback_query"):
|
||||
responder_callback(upd["callback_query"])
|
||||
continue
|
||||
msg = upd.get("message")
|
||||
if not msg:
|
||||
continue
|
||||
chat = str(msg["chat"]["id"])
|
||||
if ALLOWED and chat not in ALLOWED.split(","):
|
||||
log(f"Bloqueado chat {chat}")
|
||||
continue
|
||||
texto = msg.get("text")
|
||||
if texto:
|
||||
threading.Thread(target=procesar, args=(texto, chat),
|
||||
daemon=True).start()
|
||||
except Exception as e:
|
||||
log(f"error loop: {e}")
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user