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