Prevent Telegram flood control on all methods with chat_id as parameter

This commit is contained in:
Davte 2019-07-02 23:42:37 +02:00
parent 7582b4cce4
commit e47dd1d458

View File

@ -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)