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
class TelegramTimeouts:
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 (сек).

TelegramTimeouts(request_timeout: int = 10, polling_timeout: int = 5)
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 (сек).

REQUEST_TIMEOUT = 10
POLLING_TIMEOUT = 5
request_timeout
polling_timeout
class TelegramBot(Messengers.bot.Bot):
 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.

TelegramBot( name: str, token: str, chat_id: str, timeouts: TelegramTimeouts | None = None, polling_enabled: bool = False, polling_interval: int = 1)
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: Интервал между проверками обновлений.

queue: list[str]
token
chat_id
base_url
thread
def send_message(self, text: str):
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: Текст сообщения.

def process_queue(self):
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  # Прекращаем попытки при первой неудаче

Обрабатывает очередь сообщений для отправки.

Пытается отправить все сообщения из очереди по порядку. При первой неудачной попытке отправка прекращается.

def start(self):
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, если бот уже запущен.

def stop(self):
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 секунд.

def is_running(self):
398    def is_running(self):
399        """Проверяет, запущен ли бот.
400
401        Returns:
402            bool: True если бот запущен, False в противном случае.
403        """
404        return self._is_running

Проверяет, запущен ли бот.

Returns: bool: True если бот запущен, False в противном случае.

def get_bot_info(self):
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 при ошибке.

def test_connection(self):
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 при ошибке.

Inherited Members
Messengers.bot.Bot
name