Messengers.telegram
Реализация Telegram-бота для отправки уведомлений.
Взаимодействует с Telegram Bot API: отправляет сообщения, поддерживает long polling для получения входящих команд, обрабатывает очередь сообщений в отдельном потоке.
1#!/usr/bin/env python3 2""" 3Реализация Telegram-бота для отправки уведомлений. 4 5Взаимодействует с Telegram Bot API: отправляет сообщения, 6поддерживает long polling для получения входящих команд, 7обрабатывает очередь сообщений в отдельном потоке. 8""" 9 10import threading 11import time 12 13import requests 14from loguru import logger 15 16from Messengers.bot import Bot as MessengerBot 17 18 19class TelegramTimeouts: 20 """Хранит настройки таймаутов для Telegram Bot API. 21 22 Позволяет раздельно настроить таймаут для обычных запросов 23 и для long polling (getUpdates). 24 25 Attributes: 26 request_timeout: Таймаут для обычных запросов (сек). 27 polling_timeout: Таймаут для long polling (сек). 28 """ 29 30 REQUEST_TIMEOUT = 10 31 POLLING_TIMEOUT = 5 32 33 def __init__( 34 self, 35 request_timeout: int = REQUEST_TIMEOUT, 36 polling_timeout: int = POLLING_TIMEOUT, 37 ): 38 """Инициализирует настройки таймаутов. 39 40 Args: 41 request_timeout: Таймаут для обычных запросов (сек). 42 polling_timeout: Таймаут для long polling (сек). 43 """ 44 self.request_timeout = request_timeout 45 self.polling_timeout = polling_timeout 46 47 48class TelegramBot(MessengerBot): 49 """Реализация бота для Telegram Bot API. 50 51 Поддерживает отправку сообщений, ответы на команды /start и /chatid, 52 long polling для получения обновлений, работает в фоновом потоке. 53 54 Attributes: 55 queue: Очередь сообщений для отправки. 56 token: Токен Telegram-бота. 57 chat_id: ID чата для отправки уведомлений. 58 base_url: Базовый URL Telegram Bot API. 59 """ 60 61 def __init__( 62 self, 63 name: str, 64 token: str, 65 chat_id: str, 66 timeouts: TelegramTimeouts | None = None, 67 polling_enabled: bool = False, 68 polling_interval: int = 1, 69 ): 70 """Инициализирует Telegram-бота. 71 72 Args: 73 name: Имя бота. 74 token: Токен Telegram-бота (полученный от BotFather). 75 chat_id: ID чата для отправки сообщений. 76 timeouts: Настройки таймаутов. 77 polling_enabled: Включить long polling для получения команд. 78 polling_interval: Интервал между проверками обновлений. 79 """ 80 super().__init__(name) 81 self.queue: list[str] = [] 82 self.token = token 83 self.chat_id = chat_id 84 self.base_url = f"https://api.telegram.org/bot{self.token}" 85 86 self._validate_token() 87 88 self.thread = None 89 self._is_running = False 90 self._stop_requested = False 91 self._polling_interval = polling_interval 92 self._polling_enabled = polling_enabled 93 94 if timeouts is None: 95 timeouts = TelegramTimeouts() 96 self._request_timeout = timeouts.REQUEST_TIMEOUT 97 self._polling_timeout = timeouts.POLLING_TIMEOUT 98 99 def _validate_token(self): 100 """Проверяет валидность токена Telegram-бота. 101 102 Выполняет запрос getMe к Telegram API. В случае неверного 103 токена выбрасывает ValueError. 104 105 Raises: 106 ValueError: Если токен недействителен. 107 requests.RequestException: При ошибке соединения. 108 """ 109 try: 110 response = requests.get(f"{self.base_url}/getMe", timeout=10) 111 if response.status_code == 200: 112 data = response.json() 113 if data.get("ok"): 114 logger.debug(f"Токен валиден. Бот: @{data['result']['username']}") 115 return 116 logger.error(f"Неверный токен: {response.text}") 117 raise ValueError(f"Invalid Telegram token: {response.text}") 118 except requests.RequestException as e: 119 logger.error(f"Ошибка проверки токена: {e}") 120 raise 121 122 def _make_request( 123 self, method: str, params: dict | None = None, timeout: float | None = None 124 ): 125 """Выполняет запрос к Telegram Bot API. 126 127 Args: 128 method: Метод Telegram API (например, sendMessage, getUpdates). 129 params: Параметры запроса. 130 timeout: Таймаут запроса (если None, используется request_timeout). 131 132 Returns: 133 dict или list: Результат запроса или None при ошибке. 134 """ 135 if timeout is None: 136 timeout = self._request_timeout 137 138 try: 139 url = f"{self.base_url}/{method}" 140 141 # Для getUpdates используем более длинный таймаут, но с контролем 142 if method == "getUpdates" and params is not None and "timeout" in params: 143 # Используем таймаут из параметров, но не более заданного значения 144 actual_timeout = min(params["timeout"] + 5, timeout) # +5 секунд буфер 145 else: 146 actual_timeout = timeout 147 148 response = requests.post(url, json=params, timeout=actual_timeout) 149 150 if response.status_code == 200: 151 data = response.json() 152 if data.get("ok"): 153 return data["result"] 154 else: 155 error_description = data.get("description", "Unknown error") 156 logger.error(f"Telegram API error in {method}: {error_description}") 157 return None 158 else: 159 logger.error( 160 f"HTTP error {response.status_code} in {method}: {response.text}" 161 ) 162 return None 163 164 except requests.exceptions.Timeout: 165 # Таймаут - это нормально для long polling, не логируем как ошибку 166 if method == "getUpdates": 167 logger.debug("getUpdates timeout (expected for long polling)") 168 else: 169 logger.warning(f"Request timeout in {method}") 170 return None 171 172 except requests.exceptions.ConnectionError as e: 173 logger.warning(f"Connection error in {method}: {e}") 174 return None 175 176 except requests.RequestException as e: 177 logger.warning(f"Request error in {method}: {e}") 178 return None 179 180 def send_message(self, text: str): 181 """Отправляет сообщение в чат (публичный метод). 182 183 Добавляет сообщение в очередь и запускает обработку очереди. 184 185 Args: 186 text: Текст сообщения. 187 """ 188 self.queue.append(text) 189 self.process_queue() 190 191 def _send_message(self, text: str): 192 """Отправляет сообщение в чат через Telegram API. 193 194 Args: 195 text: Текст сообщения. 196 197 Returns: 198 bool: True если сообщение отправлено успешно, False при ошибке. 199 """ 200 if not self._is_running: 201 logger.warning("Бот не запущен, невозможно отправить сообщение") 202 return False 203 204 params = {"chat_id": self.chat_id, "text": text, "parse_mode": "HTML"} 205 206 result = self._make_request("sendMessage", params) 207 if result: 208 logger.debug(f"Сообщение отправлено в {self.chat_id}") 209 return True 210 else: 211 self.queue.append(text) 212 logger.error(f"Не удалось отправить сообщение в {self.chat_id}") 213 return False 214 215 def process_queue(self): 216 """Обрабатывает очередь сообщений для отправки. 217 218 Пытается отправить все сообщения из очереди по порядку. 219 При первой неудачной попытке отправка прекращается. 220 """ 221 if not self.queue: 222 return 223 224 logger.debug(f"Попытка отправки {len(self.queue)} сообщений из очереди") 225 for _ in range(len(self.queue)): 226 message = self.queue[0] 227 if self._send_message(message): 228 self.queue.pop(0) 229 else: 230 break # Прекращаем попытки при первой неудаче 231 232 def _handle_update(self, update): 233 """Обрабатывает входящее обновление от Telegram. 234 235 Поддерживает команды: 236 - /start — подтверждение активности бота 237 - /chatid — получение ID текущего чата 238 239 Args: 240 update: Объект обновления от Telegram API. 241 """ 242 try: 243 if "message" in update: 244 message = update["message"] 245 text = message.get("text", "") 246 chat_id = message["chat"]["id"] 247 248 # Обработка команды /start 249 if text == "/start": 250 self._make_request( 251 "sendMessage", {"chat_id": chat_id, "text": "Бот активен!"} 252 ) 253 254 # Обработка команды /chatid 255 elif text == "/chatid": 256 self._make_request( 257 "sendMessage", 258 {"chat_id": chat_id, "text": f"Chat ID: {chat_id}"}, 259 ) 260 261 # Ловим любые ошибки обработки обновления — не должны ломать polling 262 except Exception as e: 263 logger.error(f"Ошибка обработки сообщения: {e}") 264 265 def _get_updates(self, offset: int | None = None): 266 """Получает обновления от Telegram через getUpdates. 267 268 Args: 269 offset: Идентификатор последнего обработанного update_id + 1. 270 271 Returns: 272 list: Список обновлений или None при ошибке. 273 """ 274 params = { 275 "timeout": self._polling_timeout, # Короткий таймаут для частых проверок 276 "allowed_updates": ["message"], 277 } 278 279 if offset: 280 params["offset"] = offset 281 282 return self._make_request( 283 "getUpdates", params, timeout=self._polling_timeout + 5 284 ) 285 286 def _run_polling(self): 287 """Запускает основной цикл long polling с устойчивостью к ошибкам. 288 289 При последовательных ошибках (более max_consecutive_errors) 290 делает паузу перед повторными попытками. 291 """ 292 logger.debug("Запускаем polling...") 293 offset = None 294 consecutive_errors = 0 295 max_consecutive_errors = 3 296 297 while not self._stop_requested: 298 try: 299 updates = self._get_updates(offset) 300 301 if updates is not None: # None означает ошибку 302 if updates: # Пустой список - нет сообщений, это нормально 303 for update in updates: 304 offset = update["update_id"] + 1 305 self._handle_update(update) 306 307 # Сбрасываем счетчик ошибок при успешном запросе 308 consecutive_errors = 0 309 310 # Короткая пауза после успешного запроса 311 time.sleep(0.1) 312 313 else: 314 # Ошибка запроса 315 consecutive_errors += 1 316 logger.warning( 317 f"Ошибка получения updates ({consecutive_errors}/{max_consecutive_errors})" 318 ) 319 320 if consecutive_errors >= max_consecutive_errors: 321 logger.error("Слишком много ошибок подряд, пауза...") 322 time.sleep(5) 323 consecutive_errors = 0 # Сбрасываем после паузы 324 else: 325 time.sleep(1) # Короткая пауза перед повторной попыткой 326 327 # Ловим любые ошибки, чтобы не останавливать polling-цикл 328 except Exception as e: 329 logger.error(f"Неожиданная ошибка в polling цикле: {e}") 330 time.sleep(5) # Длинная пауза при критической ошибке 331 332 def _run_bot(self): 333 """Запускает бота и цикл polling (если включён) в текущем потоке. 334 335 Устанавливает флаг _is_running и запускает polling, 336 если polling_enabled = True. 337 """ 338 self._is_running = True 339 self._stop_requested = False 340 341 logger.debug("Бот запущен") 342 try: 343 if self._polling_enabled: 344 self._run_polling() 345 # Ловим любые ошибки, чтобы поток бота корректно завершился 346 except Exception as e: 347 if not self._stop_requested: 348 logger.error(f"Бот упал с ошибкой: {e}") 349 finally: 350 self._is_running = False 351 logger.debug("Бот полностью остановлен") 352 353 def start(self): 354 """Запускает бота в фоновом потоке (daemon). 355 356 Returns: 357 threading.Thread: Поток бота или None, если бот уже запущен. 358 """ 359 if self._is_running: 360 logger.warning("Бот уже запущен") 361 return 362 363 self.thread = threading.Thread(target=self._run_bot, daemon=True) 364 self.thread.start() 365 366 # Ждем инициализации 367 time.sleep(2) 368 if self._is_running: 369 logger.info(f'Бот "{self.name}" запущен в фоновом режиме') 370 return self.thread 371 372 def stop(self): 373 """Корректно останавливает бота. 374 375 Устанавливает флаг остановки и ожидает завершения потока 376 до max_wait секунд. 377 """ 378 if not self._is_running: 379 logger.warning("Бот уже остановлен") 380 return 381 382 logger.debug("Запрашиваем остановку бота...") 383 self._stop_requested = True 384 385 # Ждем завершения 386 max_wait = 5 387 start_time = time.time() 388 389 while self._is_running and (time.time() - start_time) < max_wait: 390 time.sleep(0.1) 391 392 if self._is_running: 393 logger.warning(f"Бот не остановился в течение {max_wait} секунд") 394 else: 395 logger.info(f'Бот "{self.name}" успешно остановлен') 396 397 def is_running(self): 398 """Проверяет, запущен ли бот. 399 400 Returns: 401 bool: True если бот запущен, False в противном случае. 402 """ 403 return self._is_running 404 405 def get_bot_info(self): 406 """Получает информацию о боте через API getMe. 407 408 Returns: 409 dict: Информация о боте или None при ошибке. 410 """ 411 return self._make_request("getMe") 412 413 def test_connection(self): 414 """Тестирует соединение с Telegram Bot API. 415 416 Returns: 417 bool: True если соединение установлено, False при ошибке. 418 """ 419 info = self.get_bot_info() 420 if info: 421 logger.info(f"Бот подключен: @{info['username']} ({info['first_name']})") 422 return True 423 else: 424 logger.error("Не удалось подключиться к боту") 425 return False
20class TelegramTimeouts: 21 """Хранит настройки таймаутов для Telegram Bot API. 22 23 Позволяет раздельно настроить таймаут для обычных запросов 24 и для long polling (getUpdates). 25 26 Attributes: 27 request_timeout: Таймаут для обычных запросов (сек). 28 polling_timeout: Таймаут для long polling (сек). 29 """ 30 31 REQUEST_TIMEOUT = 10 32 POLLING_TIMEOUT = 5 33 34 def __init__( 35 self, 36 request_timeout: int = REQUEST_TIMEOUT, 37 polling_timeout: int = POLLING_TIMEOUT, 38 ): 39 """Инициализирует настройки таймаутов. 40 41 Args: 42 request_timeout: Таймаут для обычных запросов (сек). 43 polling_timeout: Таймаут для long polling (сек). 44 """ 45 self.request_timeout = request_timeout 46 self.polling_timeout = polling_timeout
Хранит настройки таймаутов для Telegram Bot API.
Позволяет раздельно настроить таймаут для обычных запросов и для long polling (getUpdates).
Attributes: request_timeout: Таймаут для обычных запросов (сек). polling_timeout: Таймаут для long polling (сек).
34 def __init__( 35 self, 36 request_timeout: int = REQUEST_TIMEOUT, 37 polling_timeout: int = POLLING_TIMEOUT, 38 ): 39 """Инициализирует настройки таймаутов. 40 41 Args: 42 request_timeout: Таймаут для обычных запросов (сек). 43 polling_timeout: Таймаут для long polling (сек). 44 """ 45 self.request_timeout = request_timeout 46 self.polling_timeout = polling_timeout
Инициализирует настройки таймаутов.
Args: request_timeout: Таймаут для обычных запросов (сек). polling_timeout: Таймаут для long polling (сек).
49class TelegramBot(MessengerBot): 50 """Реализация бота для Telegram Bot API. 51 52 Поддерживает отправку сообщений, ответы на команды /start и /chatid, 53 long polling для получения обновлений, работает в фоновом потоке. 54 55 Attributes: 56 queue: Очередь сообщений для отправки. 57 token: Токен Telegram-бота. 58 chat_id: ID чата для отправки уведомлений. 59 base_url: Базовый URL Telegram Bot API. 60 """ 61 62 def __init__( 63 self, 64 name: str, 65 token: str, 66 chat_id: str, 67 timeouts: TelegramTimeouts | None = None, 68 polling_enabled: bool = False, 69 polling_interval: int = 1, 70 ): 71 """Инициализирует Telegram-бота. 72 73 Args: 74 name: Имя бота. 75 token: Токен Telegram-бота (полученный от BotFather). 76 chat_id: ID чата для отправки сообщений. 77 timeouts: Настройки таймаутов. 78 polling_enabled: Включить long polling для получения команд. 79 polling_interval: Интервал между проверками обновлений. 80 """ 81 super().__init__(name) 82 self.queue: list[str] = [] 83 self.token = token 84 self.chat_id = chat_id 85 self.base_url = f"https://api.telegram.org/bot{self.token}" 86 87 self._validate_token() 88 89 self.thread = None 90 self._is_running = False 91 self._stop_requested = False 92 self._polling_interval = polling_interval 93 self._polling_enabled = polling_enabled 94 95 if timeouts is None: 96 timeouts = TelegramTimeouts() 97 self._request_timeout = timeouts.REQUEST_TIMEOUT 98 self._polling_timeout = timeouts.POLLING_TIMEOUT 99 100 def _validate_token(self): 101 """Проверяет валидность токена Telegram-бота. 102 103 Выполняет запрос getMe к Telegram API. В случае неверного 104 токена выбрасывает ValueError. 105 106 Raises: 107 ValueError: Если токен недействителен. 108 requests.RequestException: При ошибке соединения. 109 """ 110 try: 111 response = requests.get(f"{self.base_url}/getMe", timeout=10) 112 if response.status_code == 200: 113 data = response.json() 114 if data.get("ok"): 115 logger.debug(f"Токен валиден. Бот: @{data['result']['username']}") 116 return 117 logger.error(f"Неверный токен: {response.text}") 118 raise ValueError(f"Invalid Telegram token: {response.text}") 119 except requests.RequestException as e: 120 logger.error(f"Ошибка проверки токена: {e}") 121 raise 122 123 def _make_request( 124 self, method: str, params: dict | None = None, timeout: float | None = None 125 ): 126 """Выполняет запрос к Telegram Bot API. 127 128 Args: 129 method: Метод Telegram API (например, sendMessage, getUpdates). 130 params: Параметры запроса. 131 timeout: Таймаут запроса (если None, используется request_timeout). 132 133 Returns: 134 dict или list: Результат запроса или None при ошибке. 135 """ 136 if timeout is None: 137 timeout = self._request_timeout 138 139 try: 140 url = f"{self.base_url}/{method}" 141 142 # Для getUpdates используем более длинный таймаут, но с контролем 143 if method == "getUpdates" and params is not None and "timeout" in params: 144 # Используем таймаут из параметров, но не более заданного значения 145 actual_timeout = min(params["timeout"] + 5, timeout) # +5 секунд буфер 146 else: 147 actual_timeout = timeout 148 149 response = requests.post(url, json=params, timeout=actual_timeout) 150 151 if response.status_code == 200: 152 data = response.json() 153 if data.get("ok"): 154 return data["result"] 155 else: 156 error_description = data.get("description", "Unknown error") 157 logger.error(f"Telegram API error in {method}: {error_description}") 158 return None 159 else: 160 logger.error( 161 f"HTTP error {response.status_code} in {method}: {response.text}" 162 ) 163 return None 164 165 except requests.exceptions.Timeout: 166 # Таймаут - это нормально для long polling, не логируем как ошибку 167 if method == "getUpdates": 168 logger.debug("getUpdates timeout (expected for long polling)") 169 else: 170 logger.warning(f"Request timeout in {method}") 171 return None 172 173 except requests.exceptions.ConnectionError as e: 174 logger.warning(f"Connection error in {method}: {e}") 175 return None 176 177 except requests.RequestException as e: 178 logger.warning(f"Request error in {method}: {e}") 179 return None 180 181 def send_message(self, text: str): 182 """Отправляет сообщение в чат (публичный метод). 183 184 Добавляет сообщение в очередь и запускает обработку очереди. 185 186 Args: 187 text: Текст сообщения. 188 """ 189 self.queue.append(text) 190 self.process_queue() 191 192 def _send_message(self, text: str): 193 """Отправляет сообщение в чат через Telegram API. 194 195 Args: 196 text: Текст сообщения. 197 198 Returns: 199 bool: True если сообщение отправлено успешно, False при ошибке. 200 """ 201 if not self._is_running: 202 logger.warning("Бот не запущен, невозможно отправить сообщение") 203 return False 204 205 params = {"chat_id": self.chat_id, "text": text, "parse_mode": "HTML"} 206 207 result = self._make_request("sendMessage", params) 208 if result: 209 logger.debug(f"Сообщение отправлено в {self.chat_id}") 210 return True 211 else: 212 self.queue.append(text) 213 logger.error(f"Не удалось отправить сообщение в {self.chat_id}") 214 return False 215 216 def process_queue(self): 217 """Обрабатывает очередь сообщений для отправки. 218 219 Пытается отправить все сообщения из очереди по порядку. 220 При первой неудачной попытке отправка прекращается. 221 """ 222 if not self.queue: 223 return 224 225 logger.debug(f"Попытка отправки {len(self.queue)} сообщений из очереди") 226 for _ in range(len(self.queue)): 227 message = self.queue[0] 228 if self._send_message(message): 229 self.queue.pop(0) 230 else: 231 break # Прекращаем попытки при первой неудаче 232 233 def _handle_update(self, update): 234 """Обрабатывает входящее обновление от Telegram. 235 236 Поддерживает команды: 237 - /start — подтверждение активности бота 238 - /chatid — получение ID текущего чата 239 240 Args: 241 update: Объект обновления от Telegram API. 242 """ 243 try: 244 if "message" in update: 245 message = update["message"] 246 text = message.get("text", "") 247 chat_id = message["chat"]["id"] 248 249 # Обработка команды /start 250 if text == "/start": 251 self._make_request( 252 "sendMessage", {"chat_id": chat_id, "text": "Бот активен!"} 253 ) 254 255 # Обработка команды /chatid 256 elif text == "/chatid": 257 self._make_request( 258 "sendMessage", 259 {"chat_id": chat_id, "text": f"Chat ID: {chat_id}"}, 260 ) 261 262 # Ловим любые ошибки обработки обновления — не должны ломать polling 263 except Exception as e: 264 logger.error(f"Ошибка обработки сообщения: {e}") 265 266 def _get_updates(self, offset: int | None = None): 267 """Получает обновления от Telegram через getUpdates. 268 269 Args: 270 offset: Идентификатор последнего обработанного update_id + 1. 271 272 Returns: 273 list: Список обновлений или None при ошибке. 274 """ 275 params = { 276 "timeout": self._polling_timeout, # Короткий таймаут для частых проверок 277 "allowed_updates": ["message"], 278 } 279 280 if offset: 281 params["offset"] = offset 282 283 return self._make_request( 284 "getUpdates", params, timeout=self._polling_timeout + 5 285 ) 286 287 def _run_polling(self): 288 """Запускает основной цикл long polling с устойчивостью к ошибкам. 289 290 При последовательных ошибках (более max_consecutive_errors) 291 делает паузу перед повторными попытками. 292 """ 293 logger.debug("Запускаем polling...") 294 offset = None 295 consecutive_errors = 0 296 max_consecutive_errors = 3 297 298 while not self._stop_requested: 299 try: 300 updates = self._get_updates(offset) 301 302 if updates is not None: # None означает ошибку 303 if updates: # Пустой список - нет сообщений, это нормально 304 for update in updates: 305 offset = update["update_id"] + 1 306 self._handle_update(update) 307 308 # Сбрасываем счетчик ошибок при успешном запросе 309 consecutive_errors = 0 310 311 # Короткая пауза после успешного запроса 312 time.sleep(0.1) 313 314 else: 315 # Ошибка запроса 316 consecutive_errors += 1 317 logger.warning( 318 f"Ошибка получения updates ({consecutive_errors}/{max_consecutive_errors})" 319 ) 320 321 if consecutive_errors >= max_consecutive_errors: 322 logger.error("Слишком много ошибок подряд, пауза...") 323 time.sleep(5) 324 consecutive_errors = 0 # Сбрасываем после паузы 325 else: 326 time.sleep(1) # Короткая пауза перед повторной попыткой 327 328 # Ловим любые ошибки, чтобы не останавливать polling-цикл 329 except Exception as e: 330 logger.error(f"Неожиданная ошибка в polling цикле: {e}") 331 time.sleep(5) # Длинная пауза при критической ошибке 332 333 def _run_bot(self): 334 """Запускает бота и цикл polling (если включён) в текущем потоке. 335 336 Устанавливает флаг _is_running и запускает polling, 337 если polling_enabled = True. 338 """ 339 self._is_running = True 340 self._stop_requested = False 341 342 logger.debug("Бот запущен") 343 try: 344 if self._polling_enabled: 345 self._run_polling() 346 # Ловим любые ошибки, чтобы поток бота корректно завершился 347 except Exception as e: 348 if not self._stop_requested: 349 logger.error(f"Бот упал с ошибкой: {e}") 350 finally: 351 self._is_running = False 352 logger.debug("Бот полностью остановлен") 353 354 def start(self): 355 """Запускает бота в фоновом потоке (daemon). 356 357 Returns: 358 threading.Thread: Поток бота или None, если бот уже запущен. 359 """ 360 if self._is_running: 361 logger.warning("Бот уже запущен") 362 return 363 364 self.thread = threading.Thread(target=self._run_bot, daemon=True) 365 self.thread.start() 366 367 # Ждем инициализации 368 time.sleep(2) 369 if self._is_running: 370 logger.info(f'Бот "{self.name}" запущен в фоновом режиме') 371 return self.thread 372 373 def stop(self): 374 """Корректно останавливает бота. 375 376 Устанавливает флаг остановки и ожидает завершения потока 377 до max_wait секунд. 378 """ 379 if not self._is_running: 380 logger.warning("Бот уже остановлен") 381 return 382 383 logger.debug("Запрашиваем остановку бота...") 384 self._stop_requested = True 385 386 # Ждем завершения 387 max_wait = 5 388 start_time = time.time() 389 390 while self._is_running and (time.time() - start_time) < max_wait: 391 time.sleep(0.1) 392 393 if self._is_running: 394 logger.warning(f"Бот не остановился в течение {max_wait} секунд") 395 else: 396 logger.info(f'Бот "{self.name}" успешно остановлен') 397 398 def is_running(self): 399 """Проверяет, запущен ли бот. 400 401 Returns: 402 bool: True если бот запущен, False в противном случае. 403 """ 404 return self._is_running 405 406 def get_bot_info(self): 407 """Получает информацию о боте через API getMe. 408 409 Returns: 410 dict: Информация о боте или None при ошибке. 411 """ 412 return self._make_request("getMe") 413 414 def test_connection(self): 415 """Тестирует соединение с Telegram Bot API. 416 417 Returns: 418 bool: True если соединение установлено, False при ошибке. 419 """ 420 info = self.get_bot_info() 421 if info: 422 logger.info(f"Бот подключен: @{info['username']} ({info['first_name']})") 423 return True 424 else: 425 logger.error("Не удалось подключиться к боту") 426 return False
Реализация бота для Telegram Bot API.
Поддерживает отправку сообщений, ответы на команды /start и /chatid, long polling для получения обновлений, работает в фоновом потоке.
Attributes: queue: Очередь сообщений для отправки. token: Токен Telegram-бота. chat_id: ID чата для отправки уведомлений. base_url: Базовый URL Telegram Bot API.
62 def __init__( 63 self, 64 name: str, 65 token: str, 66 chat_id: str, 67 timeouts: TelegramTimeouts | None = None, 68 polling_enabled: bool = False, 69 polling_interval: int = 1, 70 ): 71 """Инициализирует Telegram-бота. 72 73 Args: 74 name: Имя бота. 75 token: Токен Telegram-бота (полученный от BotFather). 76 chat_id: ID чата для отправки сообщений. 77 timeouts: Настройки таймаутов. 78 polling_enabled: Включить long polling для получения команд. 79 polling_interval: Интервал между проверками обновлений. 80 """ 81 super().__init__(name) 82 self.queue: list[str] = [] 83 self.token = token 84 self.chat_id = chat_id 85 self.base_url = f"https://api.telegram.org/bot{self.token}" 86 87 self._validate_token() 88 89 self.thread = None 90 self._is_running = False 91 self._stop_requested = False 92 self._polling_interval = polling_interval 93 self._polling_enabled = polling_enabled 94 95 if timeouts is None: 96 timeouts = TelegramTimeouts() 97 self._request_timeout = timeouts.REQUEST_TIMEOUT 98 self._polling_timeout = timeouts.POLLING_TIMEOUT
Инициализирует Telegram-бота.
Args: name: Имя бота. token: Токен Telegram-бота (полученный от BotFather). chat_id: ID чата для отправки сообщений. timeouts: Настройки таймаутов. polling_enabled: Включить long polling для получения команд. polling_interval: Интервал между проверками обновлений.
181 def send_message(self, text: str): 182 """Отправляет сообщение в чат (публичный метод). 183 184 Добавляет сообщение в очередь и запускает обработку очереди. 185 186 Args: 187 text: Текст сообщения. 188 """ 189 self.queue.append(text) 190 self.process_queue()
Отправляет сообщение в чат (публичный метод).
Добавляет сообщение в очередь и запускает обработку очереди.
Args: text: Текст сообщения.
216 def process_queue(self): 217 """Обрабатывает очередь сообщений для отправки. 218 219 Пытается отправить все сообщения из очереди по порядку. 220 При первой неудачной попытке отправка прекращается. 221 """ 222 if not self.queue: 223 return 224 225 logger.debug(f"Попытка отправки {len(self.queue)} сообщений из очереди") 226 for _ in range(len(self.queue)): 227 message = self.queue[0] 228 if self._send_message(message): 229 self.queue.pop(0) 230 else: 231 break # Прекращаем попытки при первой неудаче
Обрабатывает очередь сообщений для отправки.
Пытается отправить все сообщения из очереди по порядку. При первой неудачной попытке отправка прекращается.
354 def start(self): 355 """Запускает бота в фоновом потоке (daemon). 356 357 Returns: 358 threading.Thread: Поток бота или None, если бот уже запущен. 359 """ 360 if self._is_running: 361 logger.warning("Бот уже запущен") 362 return 363 364 self.thread = threading.Thread(target=self._run_bot, daemon=True) 365 self.thread.start() 366 367 # Ждем инициализации 368 time.sleep(2) 369 if self._is_running: 370 logger.info(f'Бот "{self.name}" запущен в фоновом режиме') 371 return self.thread
Запускает бота в фоновом потоке (daemon).
Returns: threading.Thread: Поток бота или None, если бот уже запущен.
373 def stop(self): 374 """Корректно останавливает бота. 375 376 Устанавливает флаг остановки и ожидает завершения потока 377 до max_wait секунд. 378 """ 379 if not self._is_running: 380 logger.warning("Бот уже остановлен") 381 return 382 383 logger.debug("Запрашиваем остановку бота...") 384 self._stop_requested = True 385 386 # Ждем завершения 387 max_wait = 5 388 start_time = time.time() 389 390 while self._is_running and (time.time() - start_time) < max_wait: 391 time.sleep(0.1) 392 393 if self._is_running: 394 logger.warning(f"Бот не остановился в течение {max_wait} секунд") 395 else: 396 logger.info(f'Бот "{self.name}" успешно остановлен')
Корректно останавливает бота.
Устанавливает флаг остановки и ожидает завершения потока до max_wait секунд.
398 def is_running(self): 399 """Проверяет, запущен ли бот. 400 401 Returns: 402 bool: True если бот запущен, False в противном случае. 403 """ 404 return self._is_running
Проверяет, запущен ли бот.
Returns: bool: True если бот запущен, False в противном случае.
406 def get_bot_info(self): 407 """Получает информацию о боте через API getMe. 408 409 Returns: 410 dict: Информация о боте или None при ошибке. 411 """ 412 return self._make_request("getMe")
Получает информацию о боте через API getMe.
Returns: dict: Информация о боте или None при ошибке.
414 def test_connection(self): 415 """Тестирует соединение с Telegram Bot API. 416 417 Returns: 418 bool: True если соединение установлено, False при ошибке. 419 """ 420 info = self.get_bot_info() 421 if info: 422 logger.info(f"Бот подключен: @{info['username']} ({info['first_name']})") 423 return True 424 else: 425 logger.error("Не удалось подключиться к боту") 426 return False
Тестирует соединение с Telegram Bot API.
Returns: bool: True если соединение установлено, False при ошибке.