From eadd7a9c3b7facb265f6f2ef2eb7c1ba584fe254 Mon Sep 17 00:00:00 2001 From: lavadk Date: Fri, 9 Apr 2021 00:31:50 +0300 Subject: [PATCH] /update_markup, imrovements in syncrhonizer, EnablingManager --- liker/command/handler_set_reactions.py | 13 ++--- liker/command/handler_update_markup.py | 64 +++++++++++++++++++++ liker/command/set_reactions_utils.py | 58 ------------------- liker/custom_markup/channel_post_handler.py | 22 ++++--- liker/custom_markup/markup_synchronizer.py | 53 +++++++++-------- liker/custom_markup/markup_utils.py | 44 ++++++++------ liker/enabling_manager.py | 58 +++++++++++++++++++ liker/setup/dependencies.py | 6 +- tengine | 2 +- 9 files changed, 199 insertions(+), 121 deletions(-) create mode 100644 liker/command/handler_update_markup.py delete mode 100644 liker/command/set_reactions_utils.py create mode 100644 liker/enabling_manager.py diff --git a/liker/command/handler_set_reactions.py b/liker/command/handler_set_reactions.py index 9e9943f..3f285fd 100644 --- a/liker/command/handler_set_reactions.py +++ b/liker/command/handler_set_reactions.py @@ -5,7 +5,7 @@ from tengine import Config from liker.state.enabled_channels import EnabledChannels -from liker.command import set_reactions_utils +from liker.enabling_manager import EnablingManager logger = logging.getLogger(__file__) @@ -14,6 +14,7 @@ class CommandHandlerSetReactions(CommandHandler): enabled_channels = inject.attr(EnabledChannels) telegram_bot = inject.attr(TelegramBot) config = inject.attr(Config) + enabling_manager = inject.attr(EnablingManager) def get_cards(self) -> Iterable[CommandCard]: return [CommandCard(command_str='/set_reactions', @@ -24,6 +25,7 @@ def get_cards(self) -> Iterable[CommandCard]: def handle(self, config: Config, chat_id, + message: Message, args: Namespace, telegram_bot: TelegramBot, command_parser: CommandParser): @@ -45,12 +47,9 @@ def handle(self, text='channel_id should be a number or start from @') return - set_successfully = set_reactions_utils.try_set_reactions(config=self.config, - telegram_bot=self.telegram_bot, - enabled_channels=self.enabled_channels, - channel_id=channel_id, - reactions=reactions, - reply_to_chat_id=chat_id) + set_successfully = self.enabling_manager.try_set_reactions(channel_id=channel_id, + reactions=reactions, + reply_to_chat_id=chat_id) if not set_successfully: return diff --git a/liker/command/handler_update_markup.py b/liker/command/handler_update_markup.py new file mode 100644 index 0000000..8897a7e --- /dev/null +++ b/liker/command/handler_update_markup.py @@ -0,0 +1,64 @@ +import inject +import logging +from tengine.command.command_handler import * +from tengine import TelegramBot +from tengine import Config +from telebot.types import InlineKeyboardMarkup + +from liker.state.enabled_channels import EnabledChannels +from liker.state.space_state import SpaceState +from liker.custom_markup import markup_utils +from liker.setup import constants + +logger = logging.getLogger(__file__) + + +class CommandHandlerUpdateMarkup(CommandHandler): + telegram_bot = inject.attr(TelegramBot) + enabled_channels = inject.attr(EnabledChannels) + space_state = inject.attr(SpaceState) + + def get_cards(self) -> Iterable[CommandCard]: + return [CommandCard(command_str='/update_markup', + description='Set buttons according to reactions enabled', + is_admin=False), + ] + + def handle(self, + config: Config, + chat_id, + message: Message, + args: Namespace, + telegram_bot: TelegramBot, + command_parser: CommandParser): + if args.command == '/update_markup': + ref_message: Message = message.reply_to_message + if (ref_message is None) or (ref_message.forward_from_chat is None): + telegram_bot.send_text(chat_id=chat_id, + text='Send /update_markup in comments to target channel post') + return + + channel_id = ref_message.forward_from_chat.id + if not self.enabled_channels.is_enabled(str(channel_id)): + telegram_bot.send_text(chat_id=chat_id, + text='Liker is not enabled for the given channel') + return + + channel_message_id = ref_message.forward_from_message_id + str_trail_markup = self.space_state \ + .ensure_channel_state(str(channel_id)) \ + .markup_trail \ + .try_get(str(channel_message_id)) + trail_markup = None if (str_trail_markup is None) else InlineKeyboardMarkup.de_json(str_trail_markup) + channel_dict = self.enabled_channels.get_channel_dict(str(channel_id)) + enabled_reactions = channel_dict['reactions'] + reply_markup = markup_utils.extend_reply_markup(current_markup=trail_markup, + enabled_reactions=enabled_reactions, + handler=constants.CHANNEL_POST_HANDLER, + case_id='') + self.telegram_bot.bot.edit_message_reply_markup(chat_id=channel_id, + message_id=channel_message_id, + reply_markup=reply_markup) + else: + raise ValueError(f'Unhandled command: {args.command}') + diff --git a/liker/command/set_reactions_utils.py b/liker/command/set_reactions_utils.py deleted file mode 100644 index e75bf78..0000000 --- a/liker/command/set_reactions_utils.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import Optional -import logging -from telebot.apihelper import ApiTelegramException -from tengine import Config, telegram_utils, TelegramBot - -from liker.state.enabled_channels import EnabledChannels - -logging = logging.getLogger(__file__) - - -def try_set_reactions(config: Config, - telegram_bot: TelegramBot, - enabled_channels: EnabledChannels, - channel_id, - reactions: list, - reply_to_chat_id: Optional[int]) -> bool: - enable_only_for = config['enable_only_for'] - if enable_only_for and (telegram_utils.to_int_chat_id_if_possible(channel_id) not in enable_only_for): - _try_reply(telegram_bot=telegram_bot, - chat_id=reply_to_chat_id, - text=f'Cannot enable for channel {channel_id}') - return False - - try: - channel_info = telegram_bot.bot.get_chat(channel_id) - except ApiTelegramException: - logging.info('Cannot get channel info, bot is not an admin in there') - _try_reply(telegram_bot=telegram_bot, - chat_id=reply_to_chat_id, - text=f'Add bot as an administrator to {channel_id}') - return False - - channel_id_int = channel_info.id - - linked_chat_id = channel_info.linked_chat_id - if linked_chat_id is not None: - try: - linked_chat_admins = telegram_bot.bot.get_chat_administrators(linked_chat_id) - if not linked_chat_admins: - raise ValueError('Got empty list of administrators') - except (ApiTelegramException, ValueError) as ex: - logging.info(f'Bot is not an admin in linked chat: {ex}') - _try_reply(telegram_bot=telegram_bot, - chat_id=reply_to_chat_id, - text=f'Add bot as an administrator to the channel discussion group') - return False - - enabled_channels.update_channel_dict(str_channel_id=str(channel_id_int), - reactions=reactions, - linked_chat_id=linked_chat_id) - return True - - -def _try_reply(telegram_bot: TelegramBot, - chat_id: Optional[int], - text: str): - if chat_id is not None: - telegram_bot.send_text(chat_id=chat_id, text=text) diff --git a/liker/custom_markup/channel_post_handler.py b/liker/custom_markup/channel_post_handler.py index befb3c6..17d0adc 100644 --- a/liker/custom_markup/channel_post_handler.py +++ b/liker/custom_markup/channel_post_handler.py @@ -10,7 +10,7 @@ from liker.custom_markup.markup_synchronizer import MarkupSynchronizer from liker.setup import constants from liker.custom_markup import markup_utils -from liker.command import set_reactions_utils +from liker.enabling_manager import EnablingManager logger = logging.getLogger(__file__) @@ -23,18 +23,16 @@ class ChannelPostHandler(TelegramInboxHandler): space_state = inject.attr(SpaceState) markup_synchronizer = inject.attr(MarkupSynchronizer) abuse_detector = inject.attr(AbuseDetector) + enabling_manager = inject.attr(EnablingManager) def channel_post(self, channel_post: types.Message) -> bool: channel_id: int = channel_post.chat.id str_channel_id = str(channel_id) if not self.enabled_channels.is_enabled(str_channel_id): - did_enabled = set_reactions_utils.try_set_reactions(config=self.config, - telegram_bot=self.telegram_bot, - enabled_channels=self.enabled_channels, - channel_id=channel_id, - reactions=constants.DEFAULT_REACTIONS, - reply_to_chat_id=None) + did_enabled = self.enabling_manager.try_set_reactions(channel_id=channel_id, + reactions=constants.DEFAULT_REACTIONS, + reply_to_chat_id=None) if not did_enabled: return False else: @@ -44,10 +42,10 @@ def channel_post(self, channel_post: types.Message) -> bool: channel_dict = self.enabled_channels.get_channel_dict(str_channel_id) enabled_reactions = channel_dict['reactions'] - reply_markup = markup_utils.build_reply_markup(enabled_reactions=enabled_reactions, - state_dict=None, - handler=constants.CHANNEL_POST_HANDLER, - case_id='') + reply_markup = markup_utils.extend_reply_markup(current_markup=None, + enabled_reactions=enabled_reactions, + handler=constants.CHANNEL_POST_HANDLER, + case_id='') self.markup_synchronizer.add(channel_id=channel_id, message_id=message_id, reply_markup=reply_markup, @@ -100,7 +98,7 @@ def callback_query(self, callback_query: types.CallbackQuery) -> bool: if reply_markup_new.to_json() == reply_markup_telegram.to_json(): self.markup_synchronizer.try_remove(channel_id=channel_id, message_id=message_id) - logger.debug(f'Dequieuing markup as it was returned to original state') + logger.debug(f'De-queuing markup as it was returned to original state') else: self.markup_synchronizer.add(channel_id=channel_id, message_id=message_id, reply_markup=reply_markup_new) diff --git a/liker/custom_markup/markup_synchronizer.py b/liker/custom_markup/markup_synchronizer.py index 7e8634c..6803252 100644 --- a/liker/custom_markup/markup_synchronizer.py +++ b/liker/custom_markup/markup_synchronizer.py @@ -73,42 +73,49 @@ def update(self): ch_state = self.space_state.ensure_channel_state(str(ch_id)) ch_queue = ch_state.markup_queue.ensure_queue() while ch_queue: - # If we made 'rate_per_minute' updates per last minute -- don't send more updates - if len(upd_times) >= rate_per_minute: - break + m_id_str = None + reply_markup_str = None + try: + m_id_str = list(ch_queue.keys())[0] + reply_markup_str = ch_queue[m_id_str] + + # If we made 'rate_per_minute' updates per last minute -- don't send more updates + if len(upd_times) >= rate_per_minute: + break - # Make elastic delay time - slowdown_factor = len(upd_times) / rate_per_minute - cur_timeout = rate_min_seconds + slowdown_factor * rate_span - logger.debug(f'queue timeout: {cur_timeout}') - if cur_timeout > dt_to_consume: - break + # Make elastic delay time + slowdown_factor = len(upd_times) / rate_per_minute + cur_timeout = rate_min_seconds + slowdown_factor * rate_span + logger.debug(f'queue timeout: {cur_timeout}') + if cur_timeout > dt_to_consume: + break - m_id_str = list(ch_queue.keys())[0] - reply_markup_str = ch_queue[m_id_str] - m_id = int(m_id_str) + m_id = int(m_id_str) - reply_markup = InlineKeyboardMarkup.de_json(reply_markup_str) + reply_markup = InlineKeyboardMarkup.de_json(reply_markup_str) + + dt_to_consume -= cur_timeout + upd_times.append(cur_time) - dt_to_consume -= cur_timeout - upd_times.append(cur_time) - try: self.telegram_bot.bot.edit_message_reply_markup(chat_id=ch_id, message_id=m_id, reply_markup=reply_markup) + # We don't break loop for all exceptions except TOO_MANY_REQUESTS to avoid infitie error loop except ApiTelegramException as ex: - if ex.error_code == telegram_error.BAD_REQUEST: - logger.error(f'Bad params in reply markup, ignoring it: {ex}') - elif ex.error_code == telegram_error.TOO_MANY_REQUESTS: + if ex.error_code == telegram_error.TOO_MANY_REQUESTS: logger.error(f'Got TOO_MANY_REQUESTS error, will skip current channel update: {ex}') break else: - raise ex + logger.exception(ex) + except Exception as ex: + logger.exception(ex) # We delete markup from the queue only after it's synchronized - ch_state.markup_trail.add(str_message_id=m_id_str, - str_markup=reply_markup_str) - del ch_queue[m_id_str] + if m_id_str is not None: + if reply_markup_str is not None: + ch_state.markup_trail.add(str_message_id=m_id_str, + str_markup=reply_markup_str) + del ch_queue[m_id_str] self.channel_update_times[ch_id] = upd_times ch_state.markup_queue.update_queue(ch_queue) diff --git a/liker/custom_markup/markup_utils.py b/liker/custom_markup/markup_utils.py index 04ef1a2..a56938f 100644 --- a/liker/custom_markup/markup_utils.py +++ b/liker/custom_markup/markup_utils.py @@ -1,8 +1,10 @@ import logging from telebot import types -from typing import Optional, Iterable +from typing import Iterable, List, Optional from tengine import telegram_utils +from liker.setup import constants + logger = logging.getLogger(__file__) @@ -24,27 +26,31 @@ def _num_str_to_number(num_str): return result -def build_reply_markup(enabled_reactions: list, - state_dict: Optional[dict], - handler: str, - case_id: str) -> types.InlineKeyboardMarkup: - state_reactions = {} - if (state_dict is not None) and ('reactions' in state_dict): - state_reactions = state_dict['reactions'] +def extend_reply_markup(current_markup: Optional[types.InlineKeyboardMarkup], + enabled_reactions: list, + handler: str, + case_id: str, + include_comment=True) -> types.InlineKeyboardMarkup: + current_buttons: List[types.InlineKeyboardButton] = [] if (current_markup is None) \ + else list(iterate_markup_buttons(current_markup)) + + if include_comment \ + and (constants.COMMENT_TEXT not in enabled_reactions) \ + and any((b for b in current_buttons if constants.COMMENT_TEXT in b.text)): + enabled_reactions = enabled_reactions.copy() + enabled_reactions.append(constants.COMMENT_TEXT) + buttons_obj = [] for r in enabled_reactions: - if r in state_reactions: - counter = state_reactions[r] - text = f'{r}{counter}' - else: + cur_btn = next((b for b in current_buttons if r in b.text), None) + if cur_btn is None: text = f'{r}' - data = telegram_utils.encode_button_data(handler=handler, - case_id=case_id, - response=r) - b = types.InlineKeyboardButton(text=text, - callback_data=data) - buttons_obj.append(b) - + data = telegram_utils.encode_button_data(handler=handler, + case_id=case_id, + response=r) + cur_btn = types.InlineKeyboardButton(text=text, + callback_data=data) + buttons_obj.append(cur_btn) return markup_from_buttons(buttons_obj) diff --git a/liker/enabling_manager.py b/liker/enabling_manager.py new file mode 100644 index 0000000..5b0843d --- /dev/null +++ b/liker/enabling_manager.py @@ -0,0 +1,58 @@ +from typing import Optional +import logging +import inject +from telebot.apihelper import ApiTelegramException +from tengine import Config, telegram_utils, TelegramBot + +from liker.state.enabled_channels import EnabledChannels + +logger = logging.getLogger(__file__) + + +class EnablingManager: + config = inject.attr(Config) + telegram_bot = inject.attr(TelegramBot) + enabled_channels = inject.attr(EnabledChannels) + + def try_set_reactions(self, + channel_id, + reactions: list, + reply_to_chat_id: Optional[int]) -> bool: + enable_only_for = self.config['enable_only_for'] + if enable_only_for and (telegram_utils.to_int_chat_id_if_possible(channel_id) not in enable_only_for): + self._try_reply(chat_id=reply_to_chat_id, + text=f'Cannot enable for channel {channel_id}') + return False + + try: + channel_info = self.telegram_bot.bot.get_chat(channel_id) + except ApiTelegramException: + logging.info('Cannot get channel info, bot is not an admin in there') + self._try_reply(chat_id=reply_to_chat_id, + text=f'Add bot as an administrator to {channel_id}') + return False + + channel_id_int = channel_info.id + + linked_chat_id = channel_info.linked_chat_id + if linked_chat_id is not None: + try: + linked_chat_admins = self.telegram_bot.bot.get_chat_administrators(linked_chat_id) + if not linked_chat_admins: + raise ValueError('Got empty list of administrators') + except (ApiTelegramException, ValueError) as ex: + logging.info(f'Bot is not an admin in linked chat: {ex}') + self._try_reply(chat_id=reply_to_chat_id, + text=f'Add bot as an administrator to the channel discussion group') + return False + + self.enabled_channels.update_channel_dict(str_channel_id=str(channel_id_int), + reactions=reactions, + linked_chat_id=linked_chat_id) + return True + + def _try_reply(self, + chat_id: Optional[int], + text: str): + if chat_id is not None: + self.telegram_bot.send_text(chat_id=chat_id, text=text) diff --git a/liker/setup/dependencies.py b/liker/setup/dependencies.py index cc10308..194ba3c 100644 --- a/liker/setup/dependencies.py +++ b/liker/setup/dependencies.py @@ -10,6 +10,8 @@ from liker.custom_markup.channel_post_handler import ChannelPostHandler from liker.custom_markup.comment_handler import CommentHandler from liker.command.handler_set_reactions import CommandHandlerSetReactions +from liker.enabling_manager import EnablingManager +from liker.command.handler_update_markup import CommandHandlerUpdateMarkup def bind_app_dependencies(binder: Binder): @@ -31,7 +33,8 @@ def bind_app_dependencies(binder: Binder): handler_classes=[CommandHandlerEssentials, CommandHandlerPassword, CommandHandlerConfig, - CommandHandlerSetReactions], + CommandHandlerSetReactions, + CommandHandlerUpdateMarkup], params=command_params, telegram_bot=inject.instance(TelegramBot))) binder.bind_to_constructor(MessagesLogger, @@ -56,3 +59,4 @@ def bind_app_dependencies(binder: Binder): abuse_threshold=constants.ABUSE_THRESHOLD)) binder.bind_to_constructor(AbuseJanitor, lambda: AbuseJanitor(abuse_detector=inject.instance(AbuseDetector), period_seconds=constants.ABUSE_JANITOR_SECONDS)) + binder.bind_to_constructor(EnablingManager, lambda: EnablingManager()) diff --git a/tengine b/tengine index d022cc6..e97b63c 160000 --- a/tengine +++ b/tengine @@ -1 +1 @@ -Subproject commit d022cc6ad7a3507c2be4721fe37a80783f434208 +Subproject commit e97b63ca246b4f7221c82c8302bf1ec201d0c60f