Queer European MD passionate about IT
Kaynağa Gözat

Prevent Telegram flood control on all methods with chat_id as parameter

Davte 5 yıl önce
ebeveyn
işleme
e47dd1d458
1 değiştirilmiş dosya ile 133 ekleme ve 2 silme
  1. 133 2
      davtelepot/api.py

+ 133 - 2
davtelepot/api.py

@@ -6,6 +6,7 @@ A simple aiohttp asyncronous web client is used to make requests.
 
 # Standard library modules
 import asyncio
+import datetime
 import json
 import logging
 
@@ -55,17 +56,57 @@ class TelegramBot(object):
             close=False
         )
     }
+    _absolute_cooldown_timedelta = datetime.timedelta(seconds=1/30)
+    _per_chat_cooldown_timedelta = datetime.timedelta(seconds=1)
+    _allowed_messages_per_group_per_minute = 20
 
     def __init__(self, token):
         """Set bot token and store HTTP sessions."""
         self._token = token
         self.sessions = dict()
+        self._flood_wait = 0
+        self.last_sending_time = dict(
+            absolute=(
+                datetime.datetime.now()
+                - self.absolute_cooldown_timedelta
+            )
+        )
 
     @property
     def token(self):
         """Telegram API bot token."""
         return self._token
 
+    @property
+    def flood_wait(self):
+        """Seconds to wait before next API requests."""
+        return self._flood_wait
+
+    @property
+    def absolute_cooldown_timedelta(self):
+        """Return time delta to wait between messages (any chat).
+
+        Return class value (all bots have the same limits).
+        """
+        return self.__class__._absolute_cooldown_timedelta
+
+    @property
+    def per_chat_cooldown_timedelta(self):
+        """Return time delta to wait between messages in a chat.
+
+        Return class value (all bots have the same limits).
+        """
+        return self.__class__._per_chat_cooldown_timedelta
+
+    @property
+    def allowed_messages_per_group_per_minute(self):
+        """Return maximum number of messages allowed in a group per minute.
+
+        Group, supergroup and channels are considered.
+        Return class value (all bots have the same limits).
+        """
+        return self.__class__._allowed_messages_per_group_per_minute
+
     @staticmethod
     def check_telegram_api_json(response):
         """Take a json Telegram response, check it and return its content.
@@ -135,6 +176,80 @@ class TelegramBot(object):
             session_must_be_closed = True
         return session, session_must_be_closed
 
+    def set_flood_wait(self, flood_wait):
+        """Wait `flood_wait` seconds before next request."""
+        self._flood_wait = flood_wait
+
+    async def prevent_flooding(self, chat_id):
+        """Await until request may be sent safely.
+
+        Telegram flood control won't allow too many API requests in a small
+            period.
+        Exact limits are unknown, but less than 30 total private chat messages
+            per second, less than 1 private message per chat and less than 20
+            group chat messages per chat per minute should be safe.
+        """
+        now = datetime.datetime.now
+        if type(chat_id) is int and chat_id > 0:
+            while (
+                now() < (
+                    self.last_sending_time['absolute']
+                    + self.absolute_cooldown_timedelta
+                )
+            ) or (
+                chat_id in self.last_sending_time
+                and (
+                    now() < (
+                        self.last_sending_time[chat_id]
+                        + self.per_chat_cooldown_timedelta
+                    )
+                )
+            ):
+                await asyncio.sleep(
+                    self.absolute_cooldown_timedelta.seconds
+                )
+            self.last_sending_time[chat_id] = now()
+        else:
+            while (
+                now() < (
+                    self.last_sending_time['absolute']
+                    + self.absolute_cooldown_timedelta
+                )
+            ) or (
+                chat_id in self.last_sending_time
+                and len(
+                    [
+                        sending_datetime
+                        for sending_datetime in self.last_sending_time[chat_id]
+                        if sending_datetime >= (
+                            now()
+                            - datetime.timedelta(minutes=1)
+                        )
+                    ]
+                ) >= self.allowed_messages_per_group_per_minute
+            ) or (
+                chat_id in self.last_sending_time
+                and len(self.last_sending_time[chat_id]) > 0
+                and now() < (
+                    self.last_sending_time[chat_id][-1]
+                    + self.per_chat_cooldown_timedelta
+                )
+            ):
+                await asyncio.sleep(0.5)
+            if chat_id not in self.last_sending_time:
+                self.last_sending_time[chat_id] = []
+            self.last_sending_time[chat_id].append(now())
+            self.last_sending_time[chat_id] = [
+                sending_datetime
+                for sending_datetime in self.last_sending_time[chat_id]
+                if sending_datetime >= (
+                    now()
+                    - self.longest_cooldown_timedelta
+                )
+            ]
+        self.last_sending_time['absolute'] = now()
+        return
+
     async def api_request(self, method, parameters={}, exclude=[]):
         """Return the result of a Telegram bot API request, or an Exception.
 
@@ -142,9 +257,11 @@ class TelegramBot(object):
             will be closed on `Bot.app.cleanup`.
         Result may be a Telegram API json response, None, or Exception.
         """
-        # TODO prevent Telegram flood control
         response_object = None
         session, session_must_be_closed = self.get_session(method)
+        # Prevent Telegram flood control for all methodsd having a `chat_id`
+        if 'chat_id' in parameters:
+            await self.prevent_flooding(parameters['chat_id'])
         parameters = self.adapt_parameters(parameters, exclude=exclude)
         try:
             async with session.post(
@@ -157,7 +274,21 @@ class TelegramBot(object):
                         await response.json()  # Telegram returns json objects
                     )
                 except TelegramError as e:
-                    logging.error(f"API {e}")
+                    logging.error(f"API error response - {e}")
+                    if e.code == 420:  # Flood error!
+                        try:
+                            flood_wait = int(
+                                e.description.split('_')[-1]
+                            ) + 30
+                        except Exception as e:
+                            logging.error(f"{e}")
+                            flood_wait = 5*60
+                        logging.critical(
+                            "Telegram antiflood control triggered!\n"
+                            f"Wait {flood_wait} seconds before making another "
+                            "request"
+                        )
+                        self.set_flood_wait(flood_wait)
                     return e
                 except Exception as e:
                     logging.error(f"{e}", exc_info=True)