from __future__ import annotations import json import time from pathlib import Path from typing import Any from urllib import request from config import BotConfig from telegram_api import TelegramAPI STATE_FILE = Path(__file__).resolve().parent.parent / ".new-qwen" / "telegram-state.json" def load_state() -> dict[str, Any]: if not STATE_FILE.exists(): return {"offset": None, "sessions": {}, "auth_flows": {}} return json.loads(STATE_FILE.read_text(encoding="utf-8")) def save_state(state: dict[str, Any]) -> None: STATE_FILE.parent.mkdir(parents=True, exist_ok=True) STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8") def post_json(url: str, payload: dict[str, Any]) -> dict[str, Any]: data = json.dumps(payload).encode("utf-8") req = request.Request( url, data=data, headers={"Content-Type": "application/json"}, method="POST", ) with request.urlopen(req, timeout=300) as response: return json.loads(response.read().decode("utf-8")) def get_json(url: str) -> dict[str, Any]: with request.urlopen(url, timeout=60) as response: return json.loads(response.read().decode("utf-8")) def send_text_chunks(api: TelegramAPI, chat_id: int, text: str) -> None: normalized = text or "Пустой ответ." chunk_size = 3800 for start in range(0, len(normalized), chunk_size): api.send_message(chat_id, normalized[start : start + chunk_size]) def ensure_auth(api: TelegramAPI, config: BotConfig, state: dict[str, Any], chat_id: int) -> bool: status = get_json(f"{config.server_url}/api/v1/auth/status") if status.get("authenticated"): return True started = post_json(f"{config.server_url}/api/v1/auth/device/start", {}) state.setdefault("auth_flows", {})[str(chat_id)] = started["flow_id"] api.send_message( chat_id, "Qwen OAuth не настроен.\n" f"Откройте ссылку:\n{started['verification_uri_complete']}\n\n" f"Потом отправьте /auth_check {started['flow_id']}", ) return False def handle_message(api: TelegramAPI, config: BotConfig, state: dict[str, Any], message: dict[str, Any]) -> None: chat_id = message["chat"]["id"] user_id = str(message.get("from", {}).get("id", chat_id)) text = (message.get("text") or "").strip() if not text: api.send_message(chat_id, "Поддерживаются только текстовые сообщения.") return session_key = f"{chat_id}:{user_id}" session_id = state.setdefault("sessions", {}).get(session_key) state.setdefault("auth_flows", {}) if text == "/start": api.send_message( chat_id, "new-qwen bot готов.\nКоманды: /help, /auth, /status, /session, /clear.", ) return if text == "/help": api.send_message( chat_id, "Команды:\n" "/auth - начать Qwen OAuth\n" "/auth_check [flow_id] - проверить авторизацию\n" "/status - статус OAuth и сервера\n" "/session - показать текущую сессию\n" "/clear - очистить контекст", ) return if text == "/auth": started = post_json(f"{config.server_url}/api/v1/auth/device/start", {}) state["auth_flows"][str(chat_id)] = started["flow_id"] api.send_message( chat_id, "Откройте ссылку для авторизации Qwen OAuth:\n" f"{started['verification_uri_complete']}\n\n" f"После подтверждения отправьте /auth_check {started['flow_id']}", ) return if text.startswith("/auth_check"): parts = text.split(maxsplit=1) flow_id = parts[1] if len(parts) == 2 else state["auth_flows"].get(str(chat_id)) if not flow_id: api.send_message(chat_id, "Нет активного flow_id. Сначала вызови /auth.") return result = post_json( f"{config.server_url}/api/v1/auth/device/poll", {"flow_id": flow_id}, ) if result.get("done"): state["auth_flows"].pop(str(chat_id), None) api.send_message(chat_id, "Qwen OAuth успешно настроен.") else: api.send_message(chat_id, "Авторизация ещё не завершена. Повторите команду через пару секунд.") return if text == "/status": status = get_json(f"{config.server_url}/api/v1/auth/status") send_text_chunks( api, chat_id, "Сервер доступен.\n" f"OAuth: {'configured' if status.get('authenticated') else 'not configured'}\n" f"resource_url: {status.get('resource_url')}\n" f"expires_at: {status.get('expires_at')}", ) return if text == "/session": if not session_id: api.send_message(chat_id, "У этого чата ещё нет активной сессии.") return session = post_json( f"{config.server_url}/api/v1/session/get", {"session_id": session_id}, ) send_text_chunks( api, chat_id, "session_id: {session_id}\nmessages: {count}\nlast_answer: {last_answer}".format( session_id=session_id, count=len(session.get("messages", [])), last_answer=session.get("last_answer") or "-", ), ) return if text == "/clear": if session_id: post_json(f"{config.server_url}/api/v1/session/clear", {"session_id": session_id}) state["sessions"].pop(session_key, None) api.send_message(chat_id, "Контекст сессии очищен.") return if not ensure_auth(api, config, state, chat_id): return api.send_message(chat_id, "Обрабатываю запрос...") result = post_json( f"{config.server_url}/api/v1/chat", { "session_id": session_id, "user_id": user_id, "message": text, }, ) state["sessions"][session_key] = result["session_id"] answer = result.get("answer") or "Пустой ответ от модели." send_text_chunks(api, chat_id, answer) def main() -> None: config = BotConfig.load() api = TelegramAPI(config.token) state = load_state() print("new-qwen bot polling started") while True: try: updates = api.get_updates(state.get("offset"), config.poll_timeout) for update in updates: state["offset"] = update["update_id"] + 1 message = update.get("message") if message: handle_message(api, config, state, message) save_state(state) except Exception as exc: print(f"bot loop error: {exc}") time.sleep(3) if __name__ == "__main__": main()