|
@@ -6,6 +6,7 @@ A simple aiohttp asyncronous web client is used to make requests.
|
|
|
|
|
|
# Standard library modules
|
|
# Standard library modules
|
|
import asyncio
|
|
import asyncio
|
|
|
|
+import datetime
|
|
import json
|
|
import json
|
|
import logging
|
|
import logging
|
|
|
|
|
|
@@ -55,17 +56,57 @@ class TelegramBot(object):
|
|
close=False
|
|
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):
|
|
def __init__(self, token):
|
|
"""Set bot token and store HTTP sessions."""
|
|
"""Set bot token and store HTTP sessions."""
|
|
self._token = token
|
|
self._token = token
|
|
self.sessions = dict()
|
|
self.sessions = dict()
|
|
|
|
+ self._flood_wait = 0
|
|
|
|
+ self.last_sending_time = dict(
|
|
|
|
+ absolute=(
|
|
|
|
+ datetime.datetime.now()
|
|
|
|
+ - self.absolute_cooldown_timedelta
|
|
|
|
+ )
|
|
|
|
+ )
|
|
|
|
|
|
@property
|
|
@property
|
|
def token(self):
|
|
def token(self):
|
|
"""Telegram API bot token."""
|
|
"""Telegram API bot token."""
|
|
return self._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
|
|
@staticmethod
|
|
def check_telegram_api_json(response):
|
|
def check_telegram_api_json(response):
|
|
"""Take a json Telegram response, check it and return its content.
|
|
"""Take a json Telegram response, check it and return its content.
|
|
@@ -135,6 +176,80 @@ class TelegramBot(object):
|
|
session_must_be_closed = True
|
|
session_must_be_closed = True
|
|
return session, session_must_be_closed
|
|
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=[]):
|
|
async def api_request(self, method, parameters={}, exclude=[]):
|
|
"""Return the result of a Telegram bot API request, or an Exception.
|
|
"""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`.
|
|
will be closed on `Bot.app.cleanup`.
|
|
Result may be a Telegram API json response, None, or Exception.
|
|
Result may be a Telegram API json response, None, or Exception.
|
|
"""
|
|
"""
|
|
- # TODO prevent Telegram flood control
|
|
|
|
response_object = None
|
|
response_object = None
|
|
session, session_must_be_closed = self.get_session(method)
|
|
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)
|
|
parameters = self.adapt_parameters(parameters, exclude=exclude)
|
|
try:
|
|
try:
|
|
async with session.post(
|
|
async with session.post(
|
|
@@ -157,7 +274,21 @@ class TelegramBot(object):
|
|
await response.json() # Telegram returns json objects
|
|
await response.json() # Telegram returns json objects
|
|
)
|
|
)
|
|
except TelegramError as e:
|
|
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
|
|
return e
|
|
except Exception as e:
|
|
except Exception as e:
|
|
logging.error(f"{e}", exc_info=True)
|
|
logging.error(f"{e}", exc_info=True)
|