# -*- coding: utf-8 -*- import codecs import datetime import logging import re import traceback from time import sleep from typing import List import appglobals import helpers import mdformat import settings import util from custemoji import Emoji from dialog import messages from models import Bot, Country from models import Category from models import Notifications from models import Statistic from models.channel import Channel from models.revision import Revision from telegram import InlineKeyboardButton, InlineKeyboardMarkup from telegram.error import BadRequest, TelegramError from telegram.ext.dispatcher import run_async from util import restricted from logzero import logger as log def _format_category_bots(category): cat_bots = Bot.of_category_without_new(category) text = '*' + str(category) + '*\n' text += '\n'.join([str(b) for b in cat_bots]) return text class BotList: FILES_ROOT = appglobals.ROOT_DIR + '/files/' INTRO_GIF = appglobals.ROOT_DIR + "/assets/gif/animation.gif" NEW_BOTS_FILE = FILES_ROOT + 'new_bots_list.txt' CATEGORY_LIST_FILE = FILES_ROOT + 'category_list.txt' ENGLISH_INTRO_TEXT = FILES_ROOT + 'intro_en.txt' SPANISH_INTRO_TEXT = FILES_ROOT + 'intro_es.txt' def __init__(self, bot, update, channel, resend, silent): if not channel: self.notify_admin_err( "I don't know the channel `{}`. Please make sure I am an admin there, " "and send a random message so that I can remember the channel meta data.".format( self.channel.username)) return self.bot = bot self.update = update self.channel = channel self.resend = resend self.silent = silent self.sent = dict() self.sent['category'] = list() self.chat_id = update.effective_chat.id self.message_id = util.mid_from_update(update) def notify_admin(self, txt): self.bot.formatter.send_or_edit(self.chat_id, Emoji.HOURGLASS_WITH_FLOWING_SAND + ' ' + txt, to_edit=self.message_id, disable_web_page_preview=True, disable_notification=False) def notify_admin_err(self, txt): self.bot.formatter.send_or_edit(self.chat_id, util.failure(txt), to_edit=self.message_id, disable_web_page_preview=True, disable_notification=False) def _delete_message(self, message_id): self.bot.delete_message(self.channel.chat_id, message_id) def _save_channel(self): self.channel.save() @staticmethod def create_hyperlink(message_id): return 'https://t.me/{}/{}'.format(settings.SELF_CHANNEL_USERNAME, message_id) @property def portal_markup(self): buttons = [ InlineKeyboardButton("🔺 1️⃣ Categories 📚 🔺", url=BotList.create_hyperlink(self.channel.category_list_mid)), InlineKeyboardButton("▫️ 2️⃣ BotList Bot 🤖 ▫️", url='https://t.me/botlistbot?start'), InlineKeyboardButton("▫️ 3️⃣ BotList Chat 👥💬 ▫️", url='https://t.me/botlistchat'), # InlineKeyboardButton("Add to Group 🤖", # url='https://t.me/botlistbot?startgroup=start'), ] return InlineKeyboardMarkup(util.build_menu(buttons, 1)) @staticmethod def _read_file(filename): with codecs.open(filename, 'r', 'utf-8') as f: return f.read() def send_or_edit(self, text, message_id, reply_markup=None): sleep(3) try: if self.resend: return util.send_md_message(self.bot, self.channel.chat_id, text, timeout=120, disable_notification=True, reply_markup=reply_markup) else: if reply_markup: return self.bot.formatter.send_or_edit(self.channel.chat_id, text, to_edit=message_id, timeout=120, disable_web_page_preview=True, disable_notification=True, reply_markup=reply_markup) else: return self.bot.formatter.send_or_edit(self.channel.chat_id, text, to_edit=message_id, timeout=120, disable_web_page_preview=True, disable_notification=True) except BadRequest as e: if 'chat not found' in e.message.lower(): self.notify_admin_err( "I can't reach BotList Bot with chat-id `{}` (CHAT NOT FOUND error). " "There's probably something wrong with the database.".format( self.channel.chat_id)) raise e if 'message not modified' in e.message.lower(): return None else: log.error(e) raise e def update_intro(self): if self.resend: self.notify_admin("Sending intro GIF...") self.bot.sendDocument(self.channel.chat_id, open(self.INTRO_GIF, 'rb'), timeout=120) sleep(1) intro_en = self._read_file(self.ENGLISH_INTRO_TEXT) intro_es = self._read_file(self.SPANISH_INTRO_TEXT) self.notify_admin("Sending english channel intro text...") msg_en = self.send_or_edit(intro_en, self.channel.intro_en_mid) self.notify_admin("Sending spanish channel intro text...") msg_es = self.send_or_edit(intro_es, self.channel.intro_es_mid) if msg_en: self.sent['intro_en'] = "English intro sent" self.channel.intro_en_mid = msg_en.message_id if msg_es: self.channel.intro_es_mid = msg_es.message_id self.sent['intro_es'] = "Spanish intro sent" self._save_channel() def update_new_bots_list(self): text = self._read_file(self.NEW_BOTS_FILE) # insert spaces and the name of the bot new_bots_joined = Bot.get_new_bots_markdown() text = text.format(new_bots_joined) msg = self.send_or_edit(text, self.channel.new_bots_mid) self.sent['new_bots_list'] = "List of new bots sent" if msg: self.channel.new_bots_mid = msg.message_id self._save_channel() def update_category_list(self): self.notify_admin('Sending category list...') # generate category links to previous messages all_categories = '\n'.join(["[{}](https://t.me/{}/{})".format( str(c), self.channel.username, c.current_message_id ) for c in Category.select_all()]) url_stub = 'https://t.me/{}/'.format(self.channel.username) category_list = self._read_file(self.CATEGORY_LIST_FILE) # Insert placeholders text = category_list.format( url_stub + str(self.channel.intro_en_mid), url_stub + str(self.channel.intro_es_mid), all_categories, url_stub + str(self.channel.new_bots_mid) ) msg = self.send_or_edit(text, self.channel.category_list_mid) if msg: self.channel.category_list_mid = msg.message_id self.sent['category_list'] = "Category Links sent" self._save_channel() def update_categories(self, categories: List[Category]): self.notify_admin( "Updating BotList categories to Revision {}...".format(Revision.get_instance().nr)) for cat in categories: text = _format_category_bots(cat) log.info(f"Updating category {cat.name}...") msg = self.send_or_edit(text, cat.current_message_id) if msg: cat.current_message_id = msg.message_id self.sent['category'].append("{} {}".format( 'Resent' if self.resend else 'Updated', cat )) cat.save() self._save_channel() # Add "share", "up", and "down" buttons for i in range(0, len(categories)): buttons = list() if i > 0: # Not first category # Add "Up" button buttons.append(InlineKeyboardButton( "🔺", url=BotList.create_hyperlink(categories[i - 1].current_message_id))) buttons.append( InlineKeyboardButton("Share", url="https://t.me/{}?start={}".format( settings.SELF_BOT_NAME, categories[i].id))) if i < len(categories) - 1: # Not last category buttons.append(InlineKeyboardButton( "🔻", url=BotList.create_hyperlink(categories[i + 1].current_message_id))) reply_markup = InlineKeyboardMarkup([buttons]) log.info(f"Adding buttons to message with category {categories[i].name}...") self.bot.edit_message_reply_markup( self.channel.chat_id, categories[i].current_message_id, reply_markup=reply_markup, timeout=60) def send_footer(self): num_bots = Bot.select_approved().count() self.notify_admin('Sending footer...') # add footer as notification footer = '\n```' footer += '\n' + mdformat.centered( "• @BotList •\n{}\n{} bots".format( datetime.date.today().strftime("%Y-%m-%d"), num_bots )) footer += '```' if self.resend or not self.silent: try: self._delete_message(self.channel.footer_mid) except BadRequest as e: pass footer_to_edit = None else: footer_to_edit = self.channel.footer_mid footer_msg = self.bot.formatter.send_or_edit(self.channel.chat_id, footer, to_edit=footer_to_edit, timeout=120, disable_notifications=self.silent, reply_markup=self.portal_markup) if footer_msg: self.channel.footer_mid = footer_msg.message_id self.sent['footer'] = "Footer sent" self._save_channel() def finish(self): # set last update self.channel.last_update = datetime.date.today() self._save_channel() new_bots = Bot.select_new_bots() if not self.silent and len(new_bots) > 0: self.notify_admin("Sending notifications to subscribers...") subscribers = Notifications.select().where(Notifications.enabled == True) notification_count = 0 for sub in subscribers: try: util.send_md_message(self.bot, sub.chat_id, messages.BOTLIST_UPDATE_NOTIFICATION.format( n_bots=len(new_bots), new_bots=Bot.get_new_bots_markdown())) notification_count += 1 sub.last_notification = datetime.date.today() sub.save() except TelegramError: pass self.sent['notifications'] = "Notifications sent to {} users.".format( notification_count) changes_made = len(self.sent) > 1 or len(self.sent['category']) > 0 if changes_made: text = util.success('{}{}'.format('BotList updated successfully:\n\n', mdformat.results_list(self.sent))) else: text = mdformat.none_action("No changes were necessary.") log.info(self.sent) self.bot.formatter.send_or_edit(self.chat_id, text, to_edit=self.message_id) def delete_full_botlist(self): all_cats = Category.select_all() start = all_cats[0].current_message_id - 3 # Some wiggle room and GIF end = all_cats[-1].current_message_id + 4 # Some wiggle room self.notify_admin("Deleting all messages...") for m in range(start, end): try: self.bot.delete_message(self.channel.chat_id, m) except BadRequest as e: pass @restricted(strict=True) @run_async def send_botlist(bot, update, resend=False, silent=False): log.info("Re-sending BotList..." if resend else "Updating BotList...") channel = helpers.get_channel() revision = Revision.get_instance() revision.nr += 1 revision.save() all_categories = Category.select_all() botlist = BotList(bot, update, channel, resend, silent) if resend: botlist.delete_full_botlist() botlist.update_intro() botlist.update_categories(all_categories) botlist.update_new_bots_list() botlist.update_category_list() botlist.send_footer() botlist.finish() channel.save() Statistic.of(update, 'send', 'botlist (resend: {})'.format(str(resend)), Statistic.IMPORTANT) def new_channel_post(bot, update, photo=None): post = update.channel_post if post.chat.username != settings.SELF_CHANNEL_USERNAME: return text = post.text channel, created = Channel.get_or_create(chat_id=post.chat_id, username=post.chat.username) if created: channel.save() category_list = '•Share your bots to the @BotListChat using the hashtag #new' in text intro = 'Hi! Welcome' in text category = text[0] == '•' and not category_list new_bots_list = 'NEW→' in text # TODO: is this a document? if photo: pass elif category: try: # get the category meta data meta = re.match(r'•(.*?)([A-Z].*):(?:\n(.*):)?', text).groups() if len(meta) < 2: raise ValueError("Category could not get parsed.") emojis = str.strip(meta[0]) name = str.strip(meta[1]) extra = str.strip(meta[2]) if meta[2] else None try: cat = Category.get(name=name) except Category.DoesNotExist: cat = Category(name=name) cat.emojis = emojis cat.extra = extra cat.save() # get the bots in that category bots = re.findall(r'^(🆕)?.*(@\w+)( .+)?$', text, re.MULTILINE) languages = Country.select().execute() for b in bots: username = b[1] try: new_bot = Bot.by_username(username) except Bot.DoesNotExist: new_bot = Bot(username=username) new_bot.category = cat new_bot.inlinequeries = "🔎" in b[2] new_bot.official = "🔹" in b[2] extra = re.findall(r'(\[.*\])', b[2]) if extra: new_bot.extra = extra[0] # find language for lang in languages: if lang.emoji in b[2]: new_bot.country = lang if b[0]: new_bot.date_added = datetime.date.today() else: new_bot.date_added = datetime.date.today() - datetime.timedelta(days=31) new_bot.save() except AttributeError: log.error("Error parsing the following text:\n" + text)