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