Exemplo n.º 1
0
class ArticleBot:
    def __init__(self, token: str, database: Database,
                 requests_session: Session, translate: dict) -> None:
        self.bot = TeleBot(token)
        self.requests_session = requests_session
        self.translate = translate['translate']
        self.translate_language = translate['to']
        self.translator = translate['translator']
        self.translate_links = translate['translate_links']
        self.database = database

    def _send_separator(self, chat_id: Union[int, str],
                        article_id: str) -> None:
        logger.debug('Bot: User {0} - {1} - Try to send separator...'.format(
            str(chat_id), article_id))
        self.bot.send_message(chat_id, '-' * 10)
        logger.debug('Bot: User {0} - {1} -  Separator sented'.format(
            str(chat_id), article_id))

    def _send_article_keywords(self, chat_id: Union[int, str],
                               article_match_words: list[str],
                               article_id: str) -> None:
        re_text = 'Ключевые слова:\n' + \
            (', '.join(article_match_words)
                if article_match_words else "Не обнаружено.")

        logger.debug('Bot: User {0} - {1} - Try to send key words...'.format(
            str(chat_id), article_id))
        self.bot.send_message(chat_id, re_text)
        logger.debug('Bot: User {0} - {1} -  Key words sented'.format(
            str(chat_id), article_id))

    def _send_article_translate_link(self, chat_id: Union[int, str],
                                     article_id: str, article_language: str,
                                     article_source: str) -> None:
        translate_link = 'https://translate.google.com/?source=gtx_c#view=home&op=translate&sl={0}&tl={1}&text={2}'.format(
            article_language, self.translate_language,
            urllib.parse.quote(article_source))
        translate_link_text = f'[Перевод статьи на Google Translate]({translate_link})'

        logger.debug(
            'Bot: User {0} - {1} - Try to send translate link...'.format(
                str(chat_id), article_id))
        self.bot.send_message(chat_id,
                              translate_link_text,
                              disable_web_page_preview=True,
                              parse_mode='Markdown')
        logger.debug('Bot: User {0} - {1} -  Translate link sented'.format(
            str(chat_id), article_id))

    def _send_full_article_text(self, chat_id: Union[int, str],
                                article_id: str, text: str) -> None:
        try:
            logger.debug(
                'Bot: User {0} - {1} - Try to send full text message...'.
                format(str(chat_id), article_id))
            self.bot.send_message(chat_id, text, disable_web_page_preview=True)
            logger.debug('Bot: User {0} - {1} - Message sented'.format(
                str(chat_id), article_id))
        except Exception as error:
            logger.exception(error)

    def _send_big_message_parts(self, chat_id: Union[int, str],
                                article_id: str, text: str) -> None:
        message_part = 0
        for x in range(0, len(text), 4096):
            try:
                logger.debug(
                    'Bot: User {0} - {1} - Try to send text message part...'.
                    format(str(chat_id), article_id))
                self.bot.send_message(chat_id,
                                      text[x:x + 4096],
                                      disable_web_page_preview=True)
                logger.debug(
                    'Bot: User {0} - {1} - Message part sented'.format(
                        str(chat_id), article_id))
            except Exception as error:
                logger.exception(error)

            message_part += 1

    def _send_article_images(
            self,
            chat_id: Union[str, int],
            article_id: str,
            images_group: list[InputMediaPhoto],
            forward: bool,
            forward_images_message: Union[None,
                                          list] = None) -> Union[list, None]:
        try:
            if not forward:
                logger.debug('Bot: User {0} - {1} - Try to send photos'.format(
                    str(chat_id), article_id))
                message_to_forward = self.bot.send_media_group(
                    chat_id, images_group)
                logger.debug('Bot: User {0} - {1} - Photos sented'.format(
                    str(chat_id), article_id))
                return message_to_forward
            else:
                logger.debug(
                    'Bot: User {0} - {1} - Try to forward photos'.format(
                        str(chat_id), article_id))
                self.bot.send_media_group(chat_id, [
                    InputMediaPhoto(image.photo[0].file_id)
                    for image in forward_images_message
                ])
                logger.debug('Bot: User {0} - {1} - Photos forwarded'.format(
                    str(chat_id), article_id))
                return None

        except Exception as error:
            logger.exception(error)

    def _translate_article_text(self, article: Article) -> dict:
        translated = {'article_title': None, 'article_text': None}

        if (article.text or article.title) and (
                article.language !=
                self.translate_language) and self.translate:
            try:
                if article.title:
                    translated['article_title'] = self.translator.translate(
                        article.title,
                        '-'.join([article.language,
                                  self.translate_language]))['text'][0]
                if article.text:
                    translated['article_text'] = self.translator.translate(
                        article.text,
                        '-'.join([article.language,
                                  self.translate_language]))['text'][0]
            except:
                logger.debug(
                    f'Bot: Article {article.id} - Can"t translate text using Yandex.Translate'
                )

        return translated

    def send_article(self, article: Article) -> int:
        current_users = self.database.get_users_list()
        if not current_users:
            return 1

        logger.info('Bot: {0} - Try to send article to all users...'.format(
            article.id))
        images_to_send = []

        for image_link in tools.delete_duplicates([article.main_image_link] +
                                                  article.article_images)[:7]:
            if image_link:
                images_to_send.append(InputMediaPhoto(image_link))

        translated = self._translate_article_text(article)

        if not self.translate or (article.language == self.translate_language):
            article_title = article.title
            article_text = article.text
        else:
            article_title = translated['article_title'] or article.title
            article_text = translated['article_text'] or article.text

        text = 'Источник: {2} {3}\n\nДата публикации: {1}\n\n{0}\n\n{4}'.format(
            article_title, article.publish_date, article.source_name,
            article.source, article_text)

        is_forward = False
        message_to_forward = None

        for user_id in current_users:
            try:
                self.bot.send_chat_action(user_id, 'typing')
            except apihelper.ApiTelegramException:
                current_users.remove(user_id)
                logger.warning(
                    'Bot: User {user_id} has been removed from subs')
                continue

            self._send_separator(chat_id=user_id, article_id=article.id)

            if article.send_key_words:
                self._send_article_keywords(
                    chat_id=user_id,
                    article_match_words=article.match_words,
                    article_id=article.id)

            if self.translate_links and (article.language !=
                                         self.translate_language):
                self._send_article_translate_link(
                    chat_id=user_id,
                    article_id=article.id,
                    article_language=article.language,
                    article_source=article.source)

            if len(text) > 4096:
                self._send_big_message_parts(chat_id=user_id,
                                             article_id=article.id,
                                             text=text)
            else:
                self._send_full_article_text(chat_id=user_id,
                                             article_id=article.id,
                                             text=text)
            if images_to_send:
                self.bot.send_chat_action(user_id, 'upload_photo')

                if not is_forward:
                    is_forward = True
                    message_to_forward = self._send_article_images(
                        chat_id=user_id,
                        article_id=article.id,
                        images_group=images_to_send,
                        forward=False)
                else:
                    self._send_article_images(
                        chat_id=user_id,
                        article_id=article.id,
                        images_group=images_to_send,
                        forward=True,
                        forward_images_message=message_to_forward)

            logger.info(f'Bot: {article.id} - Article sent to user {user_id}')

        return 0
Exemplo n.º 2
0
class ColorCodeBot:
    def __init__(self,
                 api_key: str,
                 lang: Mapping[str, str],
                 theme_image_ids: tuple[str],
                 keyboards: Mapping[str, InlineKeyboardMarkup],
                 guesslang_syntaxes: Mapping[str, str],
                 *args: Any,
                 admin_chat_id: Optional[str] = None,
                 db_path: str = str(
                     local.path(__file__).up() / 'user_themes.sqlite'),
                 **kwargs: Any):
        self.lang = lang
        self.theme_image_ids = theme_image_ids
        self.kb = keyboards
        self.guesslang_syntaxes = guesslang_syntaxes
        self.admin_chat_id = admin_chat_id
        self.db_path = db_path
        self.user_themes = KeyValue(key_field=IntegerField(primary_key=True),
                                    value_field=CharField(),
                                    database=APSWDatabase(db_path))
        self.log = mk_logger()
        self.bot = TeleBot(api_key, *args, **kwargs)
        self.register_handlers()
        self.guesser = Guess()

    def register_handlers(self):
        self.welcome = self.bot.message_handler(commands=['start', 'help'])(
            self.welcome)
        self.browse_themes = self.bot.message_handler(
            commands=['theme', 'themes'])(self.browse_themes)
        self.mk_theme_previews = self.bot.message_handler(
            commands=['previews'])(self.mk_theme_previews)
        self.intake_snippet = self.bot.message_handler(
            func=lambda m: m.content_type == 'text')(self.intake_snippet)
        self.recv_photo = self.bot.message_handler(content_types=['photo'])(
            self.recv_photo)
        self.restore_kb = self.bot.callback_query_handler(
            lambda q: yload(q.data)['action'] == 'restore')(self.restore_kb)
        self.set_snippet_filetype = self.bot.callback_query_handler(
            lambda q: yload(q.data)['action'] == 'set ext')(
                self.set_snippet_filetype)
        self.set_theme = self.bot.callback_query_handler(
            lambda q: yload(q.data)['action'] == 'set theme')(self.set_theme)
        self.send_photo_elsewhere = self.bot.inline_handler(
            lambda q: q.query.startswith("img "))(self.send_photo_elsewhere)
        self.switch_from_inline = self.bot.inline_handler(lambda q: True)(
            self.switch_from_inline)

    @retry
    def switch_from_inline(self, inline_query: InlineQuery):
        self.log.msg("receiving inline query",
                     user_id=inline_query.from_user.id,
                     user_first_name=inline_query.from_user.first_name,
                     query=inline_query.query)
        self.bot.answer_inline_query(
            inline_query.id, [],
            switch_pm_text=self.lang['switch to direct'],
            switch_pm_parameter='x')

    @retry
    def welcome(self, message: Message):
        self.log.msg("introducing myself",
                     user_id=message.from_user.id,
                     user_first_name=message.from_user.first_name,
                     chat_id=message.chat.id)
        self.bot.reply_to(
            message,
            self.lang['welcome'],
            reply_markup=ForceReply(
                input_field_placeholder=self.lang['input field placeholder']))

    @retry
    def mk_theme_previews(self, message: Message):
        if not self.admin_chat_id or str(
                message.chat.id) != self.admin_chat_id:
            self.log.msg("naughty preview attempt",
                         user_id=message.from_user.id,
                         user_first_name=message.from_user.first_name,
                         chat_id=message.chat.id,
                         admin_chat_id=self.admin_chat_id)
            return
        sample_code = dedent("""
            # palinDay :: Int -> [ISO Date]
            def palinDay(y):
                '''A possibly empty list containing the palindromic
                   date for the given year, if such a date exists.
                '''
                s = str(y)
                r = s[::-1]
                iso = '-'.join([s, r[0:2], r[2:]])
                try:
                    datetime.strptime(iso, '%Y-%m-%d')
                    return [iso]
                except ValueError:
                    return []
        """)
        for button in chain.from_iterable(self.kb['theme'].keyboard):
            theme = button.text
            html = mk_html(f"# {theme}{sample_code}", 'py', theme)
            with local.tempdir() as folder:
                png_path = mk_png(html, folder=folder)
                send_image(bot=self.bot,
                           chat_id=message.chat.id,
                           png_path=png_path,
                           reply_msg_id=message.message_id)

    @retry
    def browse_themes(self, message: Message):
        self.log.msg("browsing themes",
                     user_id=message.from_user.id,
                     user_first_name=message.from_user.first_name,
                     chat_id=message.chat.id)
        albums = [
            self.theme_image_ids[i:i + 10]
            for i in range(0, len(self.theme_image_ids), 10)
        ]
        for album in albums:
            self.bot.send_media_group(message.chat.id,
                                      map(InputMediaPhoto, album),
                                      reply_to_message_id=message.message_id)
        self.bot.reply_to(message,
                          self.lang['select theme'],
                          reply_markup=self.kb['theme'])

    @retry
    def set_theme(self, cb_query: CallbackQuery):
        data = yload(cb_query.data)
        user = cb_query.message.reply_to_message.from_user
        self.log.msg("setting theme",
                     user_id=user.id,
                     user_first_name=user.first_name,
                     theme=data['theme'],
                     chat_id=cb_query.message.chat.id)
        self.bot.edit_message_reply_markup(cb_query.message.chat.id,
                                           cb_query.message.message_id,
                                           reply_markup=minikb('theme'))
        self.user_themes[user.id] = data['theme']
        self.bot.answer_callback_query(
            cb_query.id,
            text=self.lang['acknowledge theme'].format(data['theme']))
        if self.admin_chat_id:
            with open(self.db_path, 'rb') as doc:
                self.bot.send_document(self.admin_chat_id, doc)

    def guess_ext(self,
                  code: str,
                  probability_min: float = .12) -> Optional[str]:
        syntax, probability = self.guesser.probabilities(code)[0]
        ext = self.guesslang_syntaxes.get(syntax)
        self.log.msg("guessed syntax",
                     probability_min=probability_min,
                     probability=probability,
                     syntax=syntax,
                     ext=ext)
        if probability >= probability_min:
            return ext
        for start, ext in {
                '{': 'json',
                '---\n': 'yaml',
                '[[': 'toml',
                '[': 'ini',
                '<?php': 'php',
                '<': 'xml',
                '-- ': 'lua'
        }.items():
            if code.startswith(start):
                return ext

    @retry
    def intake_snippet(self, message: Message):
        self.log.msg("receiving code",
                     user_id=message.from_user.id,
                     user_first_name=message.from_user.first_name,
                     chat_id=message.chat.id)
        ext = self.guess_ext(message.text)
        if ext:
            kb_msg = self.bot.reply_to(
                message,
                f"{self.lang['query ext']}\n\n{self.lang['guessed syntax'].format(ext)}",
                reply_markup=minikb('syntax', self.lang['syntax picker']),
                parse_mode='Markdown',
                disable_web_page_preview=True)
            self.set_snippet_filetype(cb_query=None,
                                      query_message=kb_msg,
                                      ext=ext)
        else:
            self.bot.reply_to(message,
                              self.lang['query ext'],
                              reply_markup=self.kb['syntax'],
                              parse_mode='Markdown',
                              disable_web_page_preview=True)

    @retry
    def send_photo_elsewhere(self, inline_query: InlineQuery):
        file_id = inline_query.query.split('img ', 1)[-1]
        self.log.msg("creating inline query result",
                     file_id=file_id,
                     file_info=self.bot.get_file(file_id))
        self.bot.answer_inline_query(inline_query.id, [
            InlineQueryResultCachedPhoto(
                id=str(uuid4()), photo_file_id=file_id, title="Send Image")
        ],
                                     is_personal=True)

    @retry
    def restore_kb(self, cb_query: CallbackQuery):
        data = yload(cb_query.data)
        self.bot.edit_message_reply_markup(
            cb_query.message.chat.id,
            cb_query.message.message_id,
            reply_markup=self.kb[data['kb_name']])
        self.bot.answer_callback_query(cb_query.id)

    @retry
    def set_snippet_filetype(self,
                             cb_query: Optional[CallbackQuery] = None,
                             query_message: Optional[Message] = None,
                             ext: Optional[str] = None):
        if cb_query:
            query_message = cb_query.message
            ext = yload(cb_query.data)['ext']
        elif not (query_message and ext):
            raise Exception(
                "Either cb_query or both query_message and ext are required")
        self.log.msg("colorizing code",
                     user_id=query_message.reply_to_message.from_user.id,
                     user_first_name=query_message.reply_to_message.from_user.
                     first_name,
                     syntax=ext,
                     chat_id=query_message.chat.id)
        if cb_query:
            self.bot.edit_message_reply_markup(query_message.chat.id,
                                               query_message.message_id,
                                               reply_markup=minikb(
                                                   'syntax',
                                                   self.lang['syntax picker']))
        snippet = query_message.reply_to_message
        theme = self.user_themes.get(snippet.from_user.id,
                                     'base16/gruvbox-dark-hard')

        html = mk_html(snippet.text, ext, theme)
        send_html(bot=self.bot,
                  chat_id=snippet.chat.id,
                  html=html,
                  reply_msg_id=snippet.message_id)

        with local.tempdir() as folder:
            png_path = mk_png(html, folder=folder)
            did_send = False
            if len(snippet.text.splitlines()) <= 30:
                try:
                    photo_msg = send_image(bot=self.bot,
                                           chat_id=snippet.chat.id,
                                           png_path=png_path,
                                           reply_msg_id=snippet.message_id)
                except ApiException as e:
                    self.log.error("failed to send compressed image",
                                   exc_info=e,
                                   chat_id=snippet.chat.id)
                else:
                    did_send = True
                    kb_to_chat = InlineKeyboardMarkup()
                    kb_to_chat.add(
                        InlineKeyboardButton(
                            self.lang['send to chat'],
                            switch_inline_query=
                            f"img {photo_msg.photo[-1].file_id}"))
                    self.bot.edit_message_reply_markup(photo_msg.chat.id,
                                                       photo_msg.message_id,
                                                       reply_markup=kb_to_chat)
            if not did_send:
                send_image(bot=self.bot,
                           chat_id=snippet.chat.id,
                           png_path=png_path,
                           reply_msg_id=snippet.message_id,
                           compress=False)

        if cb_query:
            self.bot.answer_callback_query(cb_query.id)

    def recv_photo(self, message: Message):
        self.log.msg('received photo',
                     file_id=message.photo[0].file_id,
                     user_id=message.from_user.id,
                     user_first_name=message.from_user.first_name,
                     chat_id=message.chat.id)