Exemplo n.º 1
0
Arquivo: bot.py Projeto: TmLev/dal
def main() -> None:
    # Logging
    logging.basicConfig(
        level=logging.DEBUG if config.DEBUG else logging.INFO,
        format="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s",
    )

    # Bot
    bot = Bot(token=os.environ.get("TOKEN"))
    dp = Dispatcher(bot, storage=MemoryStorage())

    handlers.setup(dp)
    middleware.setup(dp)

    # Server
    app = web.Application()
    app.add_routes(uniforms.routes)

    # Run everything
    loop = asyncio.get_event_loop()
    loop.run_until_complete(dp.skip_updates())
    loop.create_task(dp.start_polling())
    web.run_app(
        app=app,
        host="0.0.0.0",
        port=config.TGBOT_PORT,
    )
Exemplo n.º 2
0
    async def on_startup():
        bot = Bot(token=os.getenv("API_TOKEN"), parse_mode="Markdown")
        dp = Dispatcher()

        loop = asyncio.get_event_loop()

        include_routers(dp)
        app.state.bot = bot

        loop.create_task(dp.start_polling(bot))
Exemplo n.º 3
0
async def main():
    bot = Bot(token=settings.telegram_token)
    dp = Dispatcher(bot, storage=MemoryStorage())

    dp.middleware.setup(LoggingMiddleware())
    dp.middleware.setup(AccessMiddleware(settings.telegram_chat_id))

    register_handlers_notify(dp)
    register_handlers_bonds(dp)

    await set_commands(bot)

    notify_listener = RedisListener()

    try:
        await asyncio.gather(dp.start_polling(),
                             notify_listener.start(bot=bot, queue=settings.redis_notification_queue))
    finally:
        await dp.storage.close()
        await dp.storage.wait_closed()
        await bot.close()
Exemplo n.º 4
0
class OdesliBot:
    """Odesli Telegram bot."""

    #: If this string is in an incoming message, the message won't be processed
    SKIP_MARK = '!skip'
    #: Time to wait before retrying and API call if 429 code was returned
    API_RETRY_TIME = 5
    #: Max retries count
    API_MAX_RETRIES = 5
    #: Telegram API retry time
    TG_RETRY_TIME = 1
    #: Max reties count in case of Telegram API connection error (None is
    #: unlimited)
    TG_MAX_RETRIES = None
    #: Welcome message template
    WELCOME_MSG_TEMPLATE = (
        "I'm an (unofficial) Odesli Bot. You can send me a link to a song on "
        'any supported music streaming platform and I will reply with links '
        'from all the other platforms. I work in group chats as well. In a '
        'group chat I will also try to delete original message so that the '
        'chat remains tidy (you must promote me to admin to enable this).\n'
        '\n'
        '<b>Supported platforms:</b> {supported_platforms}.\n'
        '\n'
        'The bot is open source. More info on '
        '<a href="https://github.com/9dogs/tg-odesli-bot">GitHub</a>.\n'
        'Powered by a great <a href="https://odesli.co/">Odesli</a> service.')

    def __init__(self, config: Config = None, *, loop=None):
        """Initialize the bot.

        :param config: configuration
        :param loop: event loop
        """
        #: Configuration
        self.config = config or Config.load()
        #: Logger
        self.logger = structlog.get_logger('tg_odesli_bot')
        self.logger_var = contextvars.ContextVar('logger', default=self.logger)
        #: Event loop
        self._loop = loop or asyncio.get_event_loop()
        #: Cache
        self.cache = caches.get('default')
        #: Telegram connect retries count
        self._tg_retries = 0

    async def init(self):
        """Initialize the bot (async part)."""
        #: HTTP session
        self.session = aiohttp.ClientSession(connector=TCPConnector(limit=10))
        #: aiogram bot instance
        self.bot = Bot(token=self.config.TG_API_TOKEN)
        #: Bot's dispatcher
        self.dispatcher = Dispatcher(self.bot)
        #: API ready event (used for requests throttling)
        self._api_ready = asyncio.Event()
        self._api_ready.set()
        # Setup logging middleware
        self._logging_middleware = LoggingMiddleware(self.logger_var)
        self.dispatcher.middleware.setup(self._logging_middleware)
        # Add handlers
        self._add_handlers()

    def _add_handlers(self):
        """Add messages and commands handlers."""
        self.dispatcher.message_handler(commands=['start', 'help'])(
            self.send_welcome)
        self.dispatcher.message_handler()(self.handle_message)
        self.dispatcher.inline_handler()(self.handle_inline_query)

    async def send_welcome(self, message: types.Message):
        """Send a welcome message.

        :param message: incoming message
        """
        _logger = self.logger_var.get()
        _logger.debug('Sending a welcome message')
        supported_platforms = []
        for platform in PLATFORMS.values():
            supported_platforms.append(platform.name)
        welcome_msg = self.WELCOME_MSG_TEMPLATE.format(
            supported_platforms=' | '.join(supported_platforms))
        await message.reply(text=welcome_msg, parse_mode='HTML', reply=False)

    def _replace_urls_with_footnotes(self, message: str,
                                     song_infos: Tuple[SongInfo, ...]) -> str:
        """Replace song URLs in message with footnotes.

        E.g. "this song is awesome <link>" will be transformed to "this song
        is awesome [1]".

        :param message: original message text
        :param song_infos: list of SongInfo metadata objects
        :return: transformed message
        """
        # Check if message consists only of a song URL and return empty string
        # if so
        _test_message = message
        for song_info in song_infos:
            for url in song_info.urls_in_text:
                _test_message = _test_message.replace(url, '')
        if not _test_message.strip():
            return ''
        # Else replace song URLs with [1], [2] etc
        for index, song_info in enumerate(song_infos, start=1):
            for url in song_info.urls_in_text:
                message = message.replace(url, f'[{index}]')
        return message

    def extract_song_urls(self, text: str) -> List[SongUrl]:
        """Extract song URLs from text for each registered platform.

        :param text: message text
        :return: list of SongURLs
        """
        urls = []
        for platform_key, platform in PLATFORMS.items():
            for match in platform.url_re.finditer(text):
                platform_url = SongUrl(
                    platform_key=platform_key,
                    platform_name=platform.name,
                    url=match.group(0),
                )
                urls.append(platform_url)
        return urls

    def _merge_same_songs(
            self, song_infos: Tuple[SongInfo, ...]) -> Tuple[SongInfo, ...]:
        """Merge SongInfo objects if two or more links point to the same
        song.

        Use identifiers provided by Odesli API to find identical song even
        though they can be linked from different platforms.

        :param song_infos: tuple of SongInfo objects found in a message
        :return: tuple of merged SongInfo objects
        """
        merged_song_info_indexes: Set[int] = set()
        for idx1, song_info1 in enumerate(song_infos):
            # Skip empty SongInfos
            if not song_info1.ids:
                continue
            if idx1 in merged_song_info_indexes:
                continue
            ids1 = song_info1.ids
            for idx2, song_info2 in enumerate(song_infos):
                if (song_info2 is song_info1
                        or idx2 in merged_song_info_indexes
                        or not song_info2.ids):
                    continue
                ids2 = song_info2.ids
                if ids1 & ids2:
                    song_info1.ids = ids1 | ids2
                    if song_info1.urls and song_info2.urls:
                        song_info1.urls = {
                            **song_info1.urls,
                            **song_info2.urls,
                        }
                    song_info1.urls_in_text = (song_info1.urls_in_text
                                               | song_info2.urls_in_text)
                    merged_song_info_indexes.add(idx2)
        merged_song_infos = tuple(song_info
                                  for idx, song_info in enumerate(song_infos)
                                  if idx not in merged_song_info_indexes)
        return merged_song_infos

    async def _find_songs(self, text: str) -> Tuple[SongInfo, ...]:
        """Find song info based on given text.

        :param text: message text
        :return: tuple of SongInfo instances
        :raise NotFound: if no song info found in message or via Odesli API
        """
        # Extract song URLs from the message
        song_urls = self.extract_song_urls(text)
        if not song_urls:
            raise NotFound('No song URLs found in message')
        # Get songs information by its URLs via Odesli service API
        song_infos = await asyncio.gather(
            *[self.find_song_by_url(song_url) for song_url in song_urls])
        # Do not reply to the message if all song infos are empty
        if not any(song_info.ids for song_info in song_infos):
            raise NotFound('Cannot find info for any song')
        # Merge song infos if different platform links point to the same song
        song_infos = self._merge_same_songs(tuple(song_infos))
        return song_infos

    def _format_urls(self,
                     song_info: SongInfo,
                     separator: str = ' | ') -> Tuple[str, str]:
        """Format platform URLs into a single HTML string.

        :param song_info: SongInfo metadata
        :param separator: separator for platform URLs
        :return: HTML string e.g.
            <a href="1">Deezer</a> | <a href="2">Google Music</a> ...
        """
        platform_urls = song_info.urls or {}
        reply_urls, platform_names = [], []
        for platform_name, url in platform_urls.items():
            reply_urls.append(f'<a href="{url}">{platform_name}</a>')
            platform_names.append(platform_name)
        formatted_urls = separator.join(reply_urls)
        formatted_platforms = separator.join(platform_names)
        return formatted_urls, formatted_platforms

    def _compose_reply(
        self,
        song_infos: Tuple[SongInfo, ...],
        message_text: str,
        message: Message,
        append_index: bool,
    ) -> str:
        """Compose a reply.  For group chats original message is included in
        reply with song URLs replaces with its indexes.  If original message
        consists only of a single link the index is omitted.

        <b>@test_user wrote:</b> check this one [1]
        1. Artist - Song
        <a href="url1">Deezer</a>
        ...

        :param song_infos: list of songs metadata
        :param message: incoming message
        :param message_text: incoming message text with song URLs replaced
            with its indexes
        :param append_index: append index
        :return: reply text
        """
        # Quote the original message for group chats
        if message.chat.type != ChatType.PRIVATE:
            reply_list = [
                f'<b>@{message.from_user.username} wrote:</b> {message_text}'
            ]
        else:
            reply_list = [message_text]
        for index, song_info in enumerate(song_infos, start=1):
            # Use original URL if we failed to find that song via Odesli API
            if not song_info.ids:
                urls_in_text = song_info.urls_in_text.pop()
                reply_list.append(f'{index}. {urls_in_text}')
                continue
            if append_index:
                reply_list.append(
                    f'{index}. {song_info.artist} - {song_info.title}')
            else:
                reply_list.append(f'{song_info.artist} - {song_info.title}')
            platform_urls, __ = self._format_urls(song_info)
            reply_list.append(platform_urls)
        reply = '\n'.join(reply_list).strip()
        return reply

    async def handle_inline_query(self, inline_query: InlineQuery):
        """Handle inline query.

        :param inline_query: query
        """
        logger = self.logger_var.get()
        query = inline_query.query
        logger.info('Inline request', query=query)
        if not query:
            await self.bot.answer_inline_query(inline_query.id, results=[])
            return
        try:
            song_infos = await self._find_songs(query)
        except NotFound:
            await self.bot.answer_inline_query(inline_query.id, results=[])
            return
        articles = []
        for song_info in song_infos:
            # Use hashed concatenated IDs as a result id
            id_ = ''.join(song_info.ids)
            result_id = hashlib.md5(id_.encode()).hexdigest()
            title = f'{song_info.artist} - {song_info.title}'
            platform_urls, platform_names = self._format_urls(song_info)
            reply_text = f'{title}\n{platform_urls}'
            reply = InputTextMessageContent(reply_text, parse_mode='HTML')
            article = InlineQueryResultArticle(
                id=result_id,
                title=title,
                input_message_content=reply,
                thumb_url=song_info.thumbnail_url,
                description=platform_names,
            )
            articles.append(article)
        await self.bot.answer_inline_query(inline_query.id, results=articles)

    async def handle_message(self, message: types.Message):
        """Handle incoming message.

        :param message: incoming message
        """
        logger = self.logger_var.get()
        # Check if message should be processed
        if self.SKIP_MARK in message.text:
            logger.debug('Message is skipped due to skip mark')
            return
        try:
            song_infos = await self._find_songs(message.text)
        except NotFound as exc:
            logger.debug(exc)
            return
        # Replace original URLs in message with footnotes (e.g. [1], [2], ...)
        prepared_message_text = self._replace_urls_with_footnotes(
            message.text, song_infos)
        if prepared_message_text:
            prepared_message_text += '\n'
        # Compose reply text
        append_index = bool(len(song_infos) > 1 or prepared_message_text)
        reply_text = self._compose_reply(
            song_infos=song_infos,
            message_text=prepared_message_text,
            message=message,
            append_index=append_index,
        )
        await message.reply(text=reply_text, parse_mode='HTML', reply=False)
        # In group chat try to delete original message
        if message.chat.type != ChatType.PRIVATE:
            try:
                await message.delete()
            except MessageCantBeDeleted as exc:
                logger.warning('Cannot delete message', exc_info=exc)

    @staticmethod
    def normalize_url(url):
        """Strip "utm_" parameters from URL.

        Used in caching to increase cache density.

        :param url: url
        :return: normalized URL
        """
        parsed = urlparse(url)
        query_dict = parse_qs(parsed.query, keep_blank_values=True)
        filtered_params = {
            k: v
            for k, v in query_dict.items() if not k.startswith('utm_')
        }
        normalized_url = urlunparse([
            parsed.scheme,
            parsed.netloc,
            parsed.path,
            parsed.params,
            urlencode(filtered_params, doseq=True),
            parsed.fragment,
        ])
        return normalized_url

    async def find_song_by_url(self, song_url: SongUrl):
        """Make an API call to Odesli service and return song data for
        supported services.

        :param song_url: SongURL object
        :return: Odesli response
        """
        logger = self.logger_var.get()
        # Normalize URL for cache querying
        normalized_url = self.normalize_url(song_url.url)
        params = {'url': normalized_url}
        if self.config.ODESLI_API_KEY:
            params['api_key'] = self.config.ODESLI_API_KEY
        logger = logger.bind(url=self.config.ODESLI_API_URL, params=params)
        # Create empty SongInfo to use in case of API error
        song_info = SongInfo(set(), None, None, None, None, {song_url.url})
        _retries = 0
        while _retries < self.API_MAX_RETRIES:
            # Try to get data from cache.  Do it inside a loop in case other
            # task retrieves the data and sets cache
            cached = await self.cache.get(normalized_url)
            if cached:
                logger.debug('Returning data from cache')
                song_info = SongInfo(
                    ids=cached.ids,
                    title=cached.title,
                    artist=cached.artist,
                    thumbnail_url=cached.thumbnail_url,
                    urls=cached.urls,
                    urls_in_text={song_url.url},
                )
                return song_info
            try:
                # Wait if requests are being throttled
                if not self._api_ready.is_set():
                    logger.info('Waiting for the API')
                    await self._api_ready.wait()
                # Query the API
                async with self.session.get(self.config.ODESLI_API_URL,
                                            params=params) as resp:
                    if resp.status != HTTPStatus.OK:
                        # Throttle requests and retry if 429
                        if resp.status == HTTPStatus.TOO_MANY_REQUESTS:
                            logger.warning(
                                'Too many requests, retrying in %d sec',
                                self.API_RETRY_TIME,
                            )
                            # Stop all requests and wait before retry
                            self._api_ready.clear()
                            await asyncio.sleep(self.API_RETRY_TIME)
                            self._api_ready.set()
                            continue
                        # Return empty response if API error
                        else:
                            logger.error('API error', status_code=resp.status)
                            break
                    response = await resp.json()
                    logger.debug('Got Odesli API response', response=response)
                    schema = ApiResponseSchema(unknown='EXCLUDE')
                    try:
                        data = schema.load(response)
                    except ValidationError as exc:
                        logger.error('Invalid response data', exc_info=exc)
                    else:
                        song_info = self.process_api_response(
                            data, song_url.url)
                        # Cache processed data
                        await self.cache.set(normalized_url, song_info)
                    break
            except ClientConnectionError as exc:
                _retries += 1
                logger.error(
                    'Connection error, retrying in %d sec',
                    self.API_RETRY_TIME,
                    exc_info=exc,
                    retries=_retries,
                )
                await asyncio.sleep(self.API_RETRY_TIME)
        return song_info

    def _filter_platform_urls(self, platform_urls: dict) -> dict:
        """Filter and reorder platform URLs according to `PLATFORMS` registry.

        :param platform_urls: dictionary of {platform_key: platform_urls}
        :return: dictionary of filtered and ordered platform URLs
        """
        logger = self.logger_var.get()
        logger = logger.bind(data=platform_urls)
        urls = []
        for platform_key, platform in PLATFORMS.items():
            if platform_key not in platform_urls:
                logger.info('No URL for platform in data',
                            platform_key=platform_key)
                continue
            urls.append(
                (platform.order, platform.name, platform_urls[platform_key]))
        # Reorder platform URLs
        platform_urls = {
            name: url
            for order, name, url in sorted(urls, key=lambda x: x[0])
        }
        return platform_urls

    def process_api_response(self, data: dict, url: str) -> SongInfo:
        """Process Odesli API data creating SongInfo metadata object.

        :param data: deserialized Odesli data
        :param url: original URL in message text
        :return: song info object
        """
        #: Set of song identifiers
        ids = set()
        titles, artists = [], []
        thumbnail_url = None
        for song_entity in data['songs'].values():
            ids.add(song_entity['id'])
            titles.append(song_entity['title'])
            artists.append(song_entity['artist'])
            # Pick the first thumbnail URL
            if song_entity.get('thumbnail_url') and not thumbnail_url:
                thumbnail_url = song_entity['thumbnail_url']
        platform_urls = {}
        for platform_key, link_entity in data['links'].items():
            platform_urls[platform_key] = link_entity['url']
        platform_urls = self._filter_platform_urls(platform_urls)
        # Pick most common title and artist
        titles_counter = Counter(titles)
        title = titles_counter.most_common(1)[0][0]
        artist_counter = Counter(artists)
        artist = artist_counter.most_common(1)[0][0]
        song_info = SongInfo(
            ids=ids,
            title=title,
            artist=artist,
            thumbnail_url=thumbnail_url,
            urls=platform_urls,
            urls_in_text={url},
        )
        return song_info

    async def _start(self):
        """Start polling.  Retry if cannot connect to Telegram servers.

        Mimics `aiogram.executor.start_polling` functionality.
        """
        await self.init()
        try:
            await self.dispatcher.skip_updates()
            self._loop.create_task(self.dispatcher.start_polling())
        except (
                ConnectionResetError,
                NetworkError,
                ClientConnectionError,
        ) as exc:
            self.logger.info(
                'Connection error, retrying in %d sec',
                self.TG_RETRY_TIME,
                exc_info=exc,
                retries=self._tg_retries,
            )
            if (self.TG_MAX_RETRIES is None
                    or self._tg_retries < self.TG_MAX_RETRIES):
                self._tg_retries += 1
                await asyncio.sleep(self.TG_RETRY_TIME)
                asyncio.create_task(self._start())
            else:
                self.logger.info('Max retries count reached, exiting')
                await self.stop()
                self._loop.stop()
        else:
            self.logger.info('Bot started')

    def start(self):
        """Start the bot."""
        self.logger.info('Starting polling...')
        self._loop.create_task(self._start())
        try:
            self._loop.run_forever()
        except KeyboardInterrupt:
            self._loop.create_task(self.stop())

    async def stop(self):
        """Stop the bot."""
        self.logger.info('Stopping...')
        await self.cache.clear()
        await self.session.close()
        await self.bot.close()
Exemplo n.º 5
0
    icon = "http://openweathermap.org/img/wn/" + (
        r["weather"][0]["icon"]) + "@4x.png"

    emoji2 = ""
    if r["main"]["feels_like"] < 0:
        emoji2 = "🤧"
    elif r["main"]["feels_like"] <= 0 and r["main"]["feels_like"] < r["main"][
            "temp"]:
        emoji2 = "😟"
    elif r["main"]["feels_like"] > r["main"]["temp"]:
        emoji2 = "😌"
    id = "1"
    warmIcon = "https://memepedia.ru/wp-content/uploads/2018/07/cover-3-1.jpg"
    coldIcon = "https://risovach.ru/upload/2016/01/mem/kot_102256910_orig_.jpg"

    title = "Температура в місті " + city + ": " + temperature + emoji
    inputTextMessageContent = InputTextMessageContent(
        "Зараз температура у місті " + city + " : " + temperature + emoji +
        description + feelsLike + emoji2 + tempMin + tempMax + pressure +
        humidity + windSpeed + windDeg + visibility)
    item = InlineQueryResultArticle(
        id=id,
        title=title,
        input_message_content=inputTextMessageContent,
        thumb_url=icon,
        description="Твій персональний бот-синоптик")
    await bot.answer_inline_query(inlineQuery.id, results=[item], cache_time=1)


asyncio.run(dispatcher.start_polling())
Exemplo n.º 6
0
class TelegramBotService:
    def __init__(
            self,
            redis_host,
            redis_port,
            redis_db,
            redis_password,
            config:
        TelegramBotServiceConfig = default_telegram_bot_service_config,
            loop: Optional[asyncio.AbstractEventLoop] = None):
        self._config = config
        self.loop = loop or asyncio.get_event_loop()
        self.redis_host = redis_host
        self.redis_port = int(redis_port)
        self.redis_db = int(redis_db)
        self.redis_password = redis_password
        self._storage = RedisStorage(host=self.redis_host,
                                     port=self.redis_port,
                                     db=self.redis_db,
                                     password=self.redis_password)

        self._bot = Bot(token=self._config.token,
                        proxy=self._config.proxy,
                        loop=self.loop)
        self._dispatcher = Dispatcher(self._bot,
                                      loop=self.loop,
                                      storage=self._storage)
        self._executor = Executor(self._dispatcher,
                                  skip_updates=True,
                                  loop=self.loop)

    async def run_bot_task(self):
        logger.info('Bot polling started')

        self._dispatcher.register_message_handler(self._bot_start,
                                                  commands=['start'])
        self._dispatcher.register_message_handler(self._show_menu,
                                                  commands=['menu'],
                                                  state='*')

        self._dispatcher.register_message_handler(self._registration_step_1,
                                                  state=Registration.step_1)

        self._dispatcher.register_callback_query_handler(self._show_links,
                                                         text='links')
        self._dispatcher.register_callback_query_handler(self._book,
                                                         text='book')
        # self._dispatcher.register_callback_query_handler(self._edit_order, text='show_orders')

        self._dispatcher.register_callback_query_handler(self._book_step_1,
                                                         state=Book.step_1)
        self._dispatcher.register_callback_query_handler(self._book_step_2_1,
                                                         text='done',
                                                         state=Book.step_2)
        self._dispatcher.register_callback_query_handler(self._book_step_2_2,
                                                         text='cancel',
                                                         state=Book.step_2)

        self._executor._prepare_polling()
        await self._executor._startup_polling()
        self.loop.create_task(
            self._dispatcher.start_polling(reset_webhook=True))

    async def _bot_start(self, message: Message):
        telegram_id = message.chat.id

        check_admin = await Admin.query.where(
            Admin.telegram_id == str(telegram_id)).gino.first()
        if check_admin:
            await message.answer(f'Здравствуй, {check_admin.name}')
            await self._bot.send_message(
                telegram_id,
                'Введите команду /menu посмотреть список доступных функций')
            return

        check_user = await User.query.where(
            User.telegram_id == str(telegram_id)).gino.first()
        if check_user:
            await message.answer(f'Здравствуйте, {check_user.name}')
            await self._bot.send_message(
                telegram_id,
                'Введите команду /menu посмотреть список доступных функций')

        if all([(not check_user), (not check_admin)]):
            await message.answer(
                'Здравствуйте, мы с Вами не знакомы. \n'
                'Введите ваше ФИО, номер телефона, рост и вес.')
            example_registration = ' '.join([
                'Пример:', 'Иванов Иван Иванович', '+79020007126', '175', '80'
            ])
            await self._bot.send_message(chat_id=telegram_id,
                                         text=example_registration)
            await Registration.step_1.set()

    async def _show_menu(self, message: Message):
        telegram_id = message.chat.id

        state = self._dispatcher.current_state(user=telegram_id)
        await state.reset_state()
        check_admin = await Admin.query.where(
            Admin.telegram_id == str(telegram_id)).gino.first()
        if check_admin:
            inline_menu = await get_kb_menu_for_admin()
            await message.answer(
                'Здравствуйте, админстратор. Выберите интересующий вас пункт из меню ниже:',
                reply_markup=inline_menu)

        check_customer = await User.query.where(
            User.telegram_id == str(telegram_id)).gino.first()
        if check_customer:
            inline_menu = await get_kb_menu_for_customer()
            await message.answer(
                'Выберите интересующий вас пункт из меню ниже:',
                reply_markup=inline_menu)

        if all([(not check_customer), (not check_admin)]):
            await message.answer(
                'Здравствуйте, мы с Вами не знакомы. \n'
                'Введите ваше ФИО, номер телефона, рост и вес.')
            example_registration = ' '.join([
                'Пример:', 'Иванов Иван Иванович', '+79020007126', '175', '80'
            ])
            await self._bot.send_message(chat_id=telegram_id,
                                         text=example_registration)
            await Registration.step_1.set()

    # async def _edit_order(self, callback_query: CallbackQuery):
    #     telegram_id = callback_query.id
    #
    #     await callback_query.answer(cache_time=60)
    #     await self._bot.answer_callback_query(callback_query.id)
    #     await callback_query.message.edit_reply_markup(reply_markup=None)
    #
    #     inline_menu = await get_kb_orders_menu()
    #     await self._bot.send_message(telegram_id, 'Выберите интересующую вас заявку:',
    #                                  reply_markup=inline_menu)

    async def _registration_step_1(self, message: Message, state: FSMContext):
        telegram_id = message.chat.id

        message_user = message.text.split(' ')
        if len(message_user) != 6:
            await message.answer('Введите как показано в примере')
            return
        name_message_user = message_user[0] + ' ' + message_user[
            1] + ' ' + message_user[2]
        phone_number_message_user = message_user[3]
        height_message_user = message_user[4]
        weight_message_user = message_user[5]

        new_user = await User.create(name=name_message_user,
                                     phone_number=phone_number_message_user,
                                     height=height_message_user,
                                     weight=weight_message_user,
                                     telegram_id=str(telegram_id))

        result = f"""
Данные успешно сохранены!
Ваше ФИО: {new_user.name}
Ваш номер телефона: {new_user.phone_number}
Ваш вес: {new_user.weight} кг
Ваш рост: {new_user.height} см"""

        await self._bot.send_message(telegram_id, result)
        await self._bot.send_message(
            telegram_id,
            'Введите команду /menu посмотреть список доступных функций')
        await state.finish()

    async def _show_links(self, callback_query: CallbackQuery):
        telegram_id = callback_query.from_user.id

        await callback_query.answer(cache_time=60)
        await self._bot.answer_callback_query(callback_query.id)
        await callback_query.message.edit_reply_markup(reply_markup=None)

        inline_kb = await get_kb_out_links()
        await self._bot.send_message(telegram_id,
                                     'Мы в соцсетях!',
                                     reply_markup=inline_kb)

    async def _book(self, callback_query: CallbackQuery):
        telegram_id = callback_query.from_user.id

        await callback_query.answer(cache_time=60)
        await self._bot.answer_callback_query(callback_query.id)
        await callback_query.message.edit_reply_markup(reply_markup=None)

        inline_kb = await get_kb_items_to_book()
        await self._bot.send_message(telegram_id,
                                     'Выберите интересующий вас инвентарь:',
                                     reply_markup=inline_kb)

        await Book.step_1.set()

    async def _book_step_1(self, callback_query: CallbackQuery):
        call = callback_query.data
        telegram_id = callback_query.from_user.id

        await callback_query.answer(cache_time=60)
        await self._bot.answer_callback_query(callback_query.id)
        await callback_query.message.edit_reply_markup(reply_markup=None)

        inline_kb = await get_kb_order()

        item_data = await Item.query.where(Item.data == str(call)).gino.first()
        item_name = item_data.name
        item_price = item_data.price

        await Order.create(telegram_id=str(telegram_id),
                           ordered_item=item_name,
                           status='in treatment')

        await self._bot.send_message(
            telegram_id, f'Вы выбрали: {item_name}.\n'
            f'Стоимость бронирования этого инвентаря: {item_price}.\n'
            f'Хотите подать заявку?',
            reply_markup=inline_kb)
        await Book.step_2.set()

    async def _book_step_2_1(self, callback_query: CallbackQuery,
                             state: FSMContext):
        telegram_id = callback_query.from_user.id

        await callback_query.answer(cache_time=60)
        await self._bot.answer_callback_query(callback_query.id)
        await callback_query.message.edit_reply_markup(reply_markup=None)

        right_order = await Order.query.where(
            Order.telegram_id == str(telegram_id)).gino.first()
        user_data = await User.query.where(User.telegram_id == str(telegram_id)
                                           ).gino.first()

        await self._bot.send_message(telegram_id,
                                     'Заявка подана. Ожидайте звонка!')
        order_text = f"""
Поступила заявка. Информация о заказчике:
Имя: {user_data.name}.
Номер телефона: {user_data.phone_number}.
Заявка на следующий инвентарь: {right_order.ordered_item}.
Заказчик ждет вашего звонка!"""
        await self._bot.send_message(227448700, order_text)
        await state.finish()

    async def _book_step_2_2(self, callback_query: CallbackQuery,
                             state: FSMContext):
        telegram_id = callback_query.from_user.id

        await callback_query.answer(cache_time=60)
        await self._bot.answer_callback_query(callback_query.id)
        await callback_query.message.edit_reply_markup(reply_markup=None)

        await self._bot.send_message(telegram_id, 'Заявка сброшена.')
        await self._bot.send_message(
            telegram_id,
            'Введите команду /menu посмотреть список доступных функций')

        wrong_order = await Order.query.where(
            Order.telegram_id == str(telegram_id)).gino.first()
        wrong_order.delete()

        await state.finish()
Exemplo n.º 7
0
async def _on_startup_polling(dp: Dispatcher, pool: Pool = init_pool()):
    if config.SKIP_UPDATES:
        await dp.skip_updates()
    await on_startup(dp, pool)
    loop = asyncio.get_event_loop()
    loop.create_task(dp.start_polling())
Exemplo n.º 8
0
class InlineManager:
    def __init__(self, client, db, allmodules) -> None:
        """Initialize InlineManager to create forms"""
        self._client = client
        self._db = db
        self._allmodules = allmodules

        self._token = db.get("geektg.inline", "bot_token", False)

        self._forms = {}
        self._galleries = {}
        self._custom_map = {}

        self.fsm = {}

        self._markup_ttl = 60 * 60 * 24

        self.init_complete = False

    def ss(self, user: Union[str, int], state: Union[str, bool]) -> bool:
        if not isinstance(user, (str, int)):
            logger.error(
                f"Invalid type for `user` in `ss` (expected `str or int` got `{type(user)}`)"
            )
            return False

        if not isinstance(state, (str, bool)):
            logger.error(
                f"Invalid type for `state` in `ss` (expected `str or bool` got `{type(state)}`)"
            )
            return False

        if state:
            self.fsm[str(user)] = state
        elif str(user) in self.fsm:
            del self.fsm[str(user)]

        return True

    def gs(self, user: Union[str, int]) -> Union[bool, str]:
        if not isinstance(user, (str, int)):
            logger.error(
                f"Invalid type for `user` in `gs` (expected `str or int` got `{type(user)}`)"
            )
            return False

        return self.fsm.get(str(user), False)

    def check_inline_security(self, func, user):
        """Checks if user with id `user` is allowed to run function `func`"""
        allow = user in [
            self._me
        ] + self._client.dispatcher.security._owner  # skipcq: PYL-W0212

        if not hasattr(func, "__doc__") or not func.__doc__ or allow:
            return allow

        doc = func.__doc__

        for line in doc.splitlines():
            line = line.strip()
            if line.startswith("@allow:"):
                allow_line = line.split(":")[1].strip()

                # First we check for possible group limits
                # like `sudo`, `support`, `all`. Then check
                # for the occurrence of user in overall string
                # This allows dev to use any delimiter he wants
                if ("all" in allow_line or "sudo" in allow_line
                        and user in self._client.dispatcher.security._sudo
                        or "support" in allow_line
                        and user in self._client.dispatcher.security._support
                        or str(user) in allow_line):
                    allow = True

        # But don't hurry to return value, we need to check,
        # if there are any limits
        for line in doc.splitlines():
            line = line.strip()
            if line.startswith("@restrict:"):
                restrict = line.split(":")[1].strip()

                if ("all" in restrict or "sudo" in restrict
                        and user in self._client.dispatcher.security._sudo
                        or "support" in restrict
                        and user in self._client.dispatcher.security._support
                        or str(user) in restrict):
                    allow = True

        return allow

    async def _create_bot(self) -> None:
        # This is called outside of conversation, so we can start the new one
        # We create new bot
        logger.info("User don't have bot, attempting creating new one")
        async with self._client.conversation("@BotFather",
                                             exclusive=False) as conv:
            m = await conv.send_message("/newbot")
            r = await conv.get_response()

            if "20" in r.raw_text:
                return False

            await m.delete()
            await r.delete()

            # Set its name to user's name + GeekTG Userbot
            m = await conv.send_message(f"🤖 GeekTG Userbot of {self._name}")
            r = await conv.get_response()

            await m.delete()
            await r.delete()

            # Generate and set random username for bot
            uid = rand(6)
            username = f"GeekTG_{uid}_Bot"

            m = await conv.send_message(username)
            r = await conv.get_response()

            await m.delete()
            await r.delete()

            # Set bot profile pic
            m = await conv.send_message("/setuserpic")
            r = await conv.get_response()

            await m.delete()
            await r.delete()

            m = await conv.send_message(username)
            r = await conv.get_response()

            await m.delete()
            await r.delete()

            try:
                m = await conv.send_file(photo)
                r = await conv.get_response()
            except Exception:
                # In case user was not able to send photo to
                # BotFather, it is not a critical issue, so
                # just ignore it
                m = await conv.send_message("/cancel")
                r = await conv.get_response()

            await m.delete()
            await r.delete()

        # Re-attempt search. If it won't find newly created (or not created?) bot
        # it will return `False`, that's why `init_complete` will be `False`
        return await self._assert_token(False)

    async def _assert_token(self,
                            create_new_if_needed=True,
                            revoke_token=False) -> None:
        # If the token is set in db
        if self._token:
            # Just return `True`
            return True

        logger.info(
            "Bot token not found in db, attempting search in BotFather")
        # Start conversation with BotFather to attempt search
        async with self._client.conversation("@BotFather",
                                             exclusive=False) as conv:
            # Wrap it in try-except in case user banned BotFather
            try:
                # Try sending command
                m = await conv.send_message("/token")
            except YouBlockedUserError:
                # If user banned BotFather, unban him
                await self._client(UnblockRequest(id="@BotFather"))
                # And resend message
                m = await conv.send_message("/token")

            r = await conv.get_response()

            await m.delete()
            await r.delete()

            # User do not have any bots yet, so just create new one
            if not hasattr(r, "reply_markup") or not hasattr(
                    r.reply_markup, "rows"):
                # Cancel current conversation (search)
                # bc we don't need it anymore
                await conv.cancel_all()

                return await self._create_bot(
                ) if create_new_if_needed else False

            for row in r.reply_markup.rows:
                for button in row.buttons:
                    if re.search(r"@geektg_[0-9a-zA-Z]{6}_bot", button.text,
                                 re.I):
                        m = await conv.send_message(button.text)
                        r = await conv.get_response()

                        if revoke_token:
                            await m.delete()
                            await r.delete()

                            m = await conv.send_message("/revoke")
                            r = await conv.get_response()

                            await m.delete()
                            await r.delete()

                            m = await conv.send_message(button.text)
                            r = await conv.get_response()

                        token = r.raw_text.splitlines()[1]

                        # Save token to database, now this bot is ready-to-use
                        self._db.set("geektg.inline", "bot_token", token)
                        self._token = token

                        await m.delete()
                        await r.delete()

                        # Enable inline mode or change its
                        # placeholder in case it is not set
                        m = await conv.send_message("/setinline")
                        r = await conv.get_response()

                        await m.delete()
                        await r.delete()

                        m = await conv.send_message(button.text)
                        r = await conv.get_response()

                        await m.delete()
                        await r.delete()

                        m = await conv.send_message("GeekQuery")
                        r = await conv.get_response()

                        await m.delete()
                        await r.delete()

                        m = await conv.send_message("/setinlinefeedback")
                        r = await conv.get_response()

                        await m.delete()
                        await r.delete()

                        m = await conv.send_message(button.text)
                        r = await conv.get_response()

                        await m.delete()
                        await r.delete()

                        m = await conv.send_message("Enabled")
                        r = await conv.get_response()

                        await m.delete()
                        await r.delete()

                        # Set bot profile pic
                        m = await conv.send_message("/setuserpic")
                        r = await conv.get_response()

                        await m.delete()
                        await r.delete()

                        m = await conv.send_message(button.text)
                        r = await conv.get_response()

                        await m.delete()
                        await r.delete()

                        m = await conv.send_file(photo)
                        r = await conv.get_response()

                        await m.delete()
                        await r.delete()

                        # Return `True` to say, that everything is okay
                        return True

        # And we are not returned after creation
        return await self._create_bot() if create_new_if_needed else False

    async def _cleaner(self) -> None:
        """Cleans outdated _forms"""
        while True:
            for form_uid, form in self._forms.copy().items():
                if form["ttl"] < time.time():
                    del self._forms[form_uid]

            await asyncio.sleep(10)

    async def _reassert_token(self) -> None:
        is_token_asserted = await self._assert_token(revoke_token=True)
        if not is_token_asserted:
            self.init_complete = False
        else:
            await self._register_manager(ignore_token_checks=True)

    async def _dp_revoke_token(self, inited=True) -> None:
        if inited:
            await self._stop()
            logger.error(
                "Got polling conflict. Attempting token revocation...")

        self._db.set("geektg.inline", "bot_token", None)
        self._token = None
        if inited:
            asyncio.ensure_future(self._reassert_token())
        else:
            return await self._reassert_token()

    async def _register_manager(self,
                                after_break=False,
                                ignore_token_checks=False) -> None:
        # Get info about user to use it in this class
        me = await self._client.get_me()
        self._me = me.id
        self._name = get_display_name(me)

        if not ignore_token_checks:
            # Assert that token is set to valid, and if not,
            # set `init_complete` to `False` and return
            is_token_asserted = await self._assert_token()
            if not is_token_asserted:
                self.init_complete = False
                return

        # We successfully asserted token, so set `init_complete` to `True`
        self.init_complete = True

        # Create bot instance and dispatcher
        self.bot = Bot(token=self._token)
        self._bot = self.bot  # This is a temporary alias so the
        # developers can adapt their code
        self._dp = Dispatcher(self.bot)

        # Get bot username to call inline queries
        try:
            self.bot_username = (await self.bot.get_me()).username
            self._bot_username = self.bot_username  # This is a temporary alias so the
            # developers can adapt their code
        except Unauthorized:
            logger.critical("Token expired, revoking...")
            return await self._dp_revoke_token(False)

        # Start the bot in case it can send you messages
        try:
            m = await self._client.send_message(self.bot_username, "/start")
        except (InputUserDeactivatedError, ValueError):
            self._db.set("geektg.inline", "bot_token", None)
            self._token = False

            if not after_break:
                return await self._register_manager(True)

            self.init_complete = False
            return False
        except Exception:
            self.init_complete = False
            logger.critical("Initialization of inline manager failed!")
            logger.exception("due to")
            return False

        await self._client.delete_messages(self.bot_username, m)

        # Register required event handlers inside aiogram
        self._dp.register_inline_handler(self._inline_handler,
                                         lambda inline_query: True)
        self._dp.register_callback_query_handler(self._callback_query_handler,
                                                 lambda query: True)
        self._dp.register_chosen_inline_handler(
            self._chosen_inline_handler, lambda chosen_inline_query: True)
        self._dp.register_message_handler(self._message_handler,
                                          lambda *args: True,
                                          content_types=["any"])

        old = self.bot.get_updates
        revoke = self._dp_revoke_token

        async def new(*args, **kwargs):
            nonlocal revoke, old
            try:
                return await old(*args, **kwargs)
            except aiogram.utils.exceptions.TerminatedByOtherGetUpdates:
                await revoke()
            except aiogram.utils.exceptions.Unauthorized:
                logger.critical("Got Unauthorized")
                await self._stop()

        self.bot.get_updates = new

        # Start polling as the separate task, just in case we will need
        # to force stop this coro. It should be cancelled only by `stop`
        # because it stops the bot from getting updates
        self._task = asyncio.ensure_future(self._dp.start_polling())
        self._cleaner_task = asyncio.ensure_future(self._cleaner())

    async def _message_handler(self, message: AiogramMessage) -> None:
        """Processes incoming messages"""
        if message.chat.type != "private":
            return

        for mod in self._allmodules.modules:
            if not hasattr(mod, "aiogram_watcher"):
                continue

            setattr(message, "answer",
                    functools.partial(answer, mod=self, message=message))

            try:
                await mod.aiogram_watcher(message)
            except BaseException:
                logger.exception("Error on running aiogram watcher!")

    async def _stop(self) -> None:
        self._task.cancel()
        self._dp.stop_polling()
        self._cleaner_task.cancel()

    def _generate_markup(self, form_uid: Union[str,
                                               list]) -> InlineKeyboardMarkup:
        """Generate markup for form"""
        markup = InlineKeyboardMarkup()

        for row in (self._forms[form_uid]["buttons"] if isinstance(
                form_uid, str) else form_uid):
            for button in row:
                if "callback" in button and "_callback_data" not in button:
                    button["_callback_data"] = rand(30)

                if "input" in button and "_switch_query" not in button:
                    button["_switch_query"] = rand(10)

        for row in (self._forms[form_uid]["buttons"] if isinstance(
                form_uid, str) else form_uid):
            line = []
            for button in row:
                try:
                    if "url" in button:
                        line += [
                            InlineKeyboardButton(button["text"],
                                                 url=button.get("url", None))
                        ]
                    elif "callback" in button:
                        line += [
                            InlineKeyboardButton(
                                button["text"],
                                callback_data=button["_callback_data"])
                        ]
                    elif "input" in button:
                        line += [
                            InlineKeyboardButton(
                                button["text"],
                                switch_inline_query_current_chat=button[
                                    "_switch_query"] + " ",
                            )
                        ]
                    elif "data" in button:
                        line += [
                            InlineKeyboardButton(button["text"],
                                                 callback_data=button["data"])
                        ]
                    else:
                        logger.warning("Button have not been added to "
                                       "form, because it is not structured "
                                       f"properly. {button}")
                except KeyError:
                    logger.exception(
                        "Error while forming markup! Probably, you "
                        "passed wrong type combination for button. "
                        "Contact developer of module.")
                    return False

            markup.row(*line)

        return markup

    async def _inline_handler(self, inline_query: InlineQuery) -> None:
        """Inline query handler (forms' calls)"""
        # Retrieve query from passed object
        query = inline_query.query

        # If we didn't get any query, return help
        if not query:
            _help = ""
            for mod in self._allmodules.modules:
                if (not hasattr(mod, "inline_handlers")
                        or not isinstance(mod.inline_handlers, dict)
                        or not mod.inline_handlers):
                    continue

                _ihandlers = dict(mod.inline_handlers.items())
                for name, fun in _ihandlers.items():
                    # If user doesn't have enough permissions
                    # to run this inline command, do not show it
                    # in help
                    if not self.check_inline_security(
                            fun, inline_query.from_user.id):
                        continue

                    # Retrieve docs from func
                    doc = utils.escape_html("\n".join([
                        line.strip()
                        for line in inspect.getdoc(fun).splitlines()
                        if not line.strip().startswith("@")
                    ]))

                    _help += f"🎹 <code>@{self.bot_username} {name}</code> - {doc}\n"

            await inline_query.answer(
                [
                    InlineQueryResultArticle(
                        id=rand(20),
                        title="Show available inline commands",
                        description=
                        f"You have {len(_help.splitlines())} available command(-s)",
                        input_message_content=InputTextMessageContent(
                            f"<b>ℹ️ Available inline commands:</b>\n\n{_help}",
                            "HTML",
                            disable_web_page_preview=True,
                        ),
                        thumb_url=
                        "https://img.icons8.com/fluency/50/000000/info-squared.png",
                        thumb_width=128,
                        thumb_height=128,
                    )
                ],
                cache_time=0,
            )

            return

        # First, dispatch all registered inline handlers
        for mod in self._allmodules.modules:
            if (not hasattr(mod, "inline_handlers")
                    or not isinstance(mod.inline_handlers, dict)
                    or not mod.inline_handlers):
                continue

            instance = GeekInlineQuery(inline_query)

            for query_text, query_func in mod.inline_handlers.items():
                if inline_query.query.split()[0].lower(
                ) == query_text.lower() and self.check_inline_security(
                        query_func, inline_query.from_user.id):
                    try:
                        await query_func(instance)
                    except BaseException:
                        logger.exception("Error on running inline watcher!")

        # Process forms
        for form in self._forms.copy().values():
            for button in array_sum(form.get("buttons", [])):
                if ("_switch_query" in button and "input" in button
                        and button["_switch_query"] == query.split()[0]
                        and inline_query.from_user.id in [self._me] + self.
                        _client.dispatcher.security._owner  # skipcq: PYL-W0212
                        + form["always_allow"]):
                    await inline_query.answer(
                        [
                            InlineQueryResultArticle(
                                id=rand(20),
                                title=button["input"],
                                description=
                                "⚠️ Please, do not remove identifier!",
                                input_message_content=InputTextMessageContent(
                                    "🔄 <b>Transferring value to userbot...</b>\n"
                                    "<i>This message is gonna be deleted...</i>",
                                    "HTML",
                                    disable_web_page_preview=True,
                                ),
                            )
                        ],
                        cache_time=60,
                    )
                    return

        # Process galleries
        for gallery in self._galleries.copy().values():
            if (inline_query.from_user.id in [
                    self._me
            ] + self._client.dispatcher.security._owner  # skipcq: PYL-W0212
                    + gallery["always_allow"] and query == gallery["uid"]):
                markup = InlineKeyboardMarkup()
                markup.add(
                    InlineKeyboardButton(
                        "Next ➡️", callback_data=gallery["btn_call_data"]))

                caption = gallery["caption"]
                caption = caption() if callable(caption) else caption

                await inline_query.answer(
                    [
                        InlineQueryResultPhoto(
                            id=rand(20),
                            title="Toss a coin",
                            photo_url=gallery["photo_url"],
                            thumb_url=gallery["photo_url"],
                            caption=caption,
                            description=caption,
                            reply_markup=markup,
                            parse_mode="HTML",
                        )
                    ],
                    cache_time=0,
                )
                return

        # If we don't know, what this query is for, just ignore it
        if query not in self._forms:
            return

        # Otherwise, answer it with templated form
        await inline_query.answer(
            [
                InlineQueryResultArticle(
                    id=rand(20),
                    title="GeekTG",
                    input_message_content=InputTextMessageContent(
                        self._forms[query]["text"],
                        "HTML",
                        disable_web_page_preview=True,
                    ),
                    reply_markup=self._generate_markup(query),
                )
            ],
            cache_time=60,
        )

    async def _callback_query_handler(
            self,
            query: CallbackQuery,
            reply_markup: List[List[dict]] = None) -> None:
        """Callback query handler (buttons' presses)"""
        if reply_markup is None:
            reply_markup = []

        # First, dispatch all registered callback handlers
        for mod in self._allmodules.modules:
            if (not hasattr(mod, "callback_handlers")
                    or not isinstance(mod.callback_handlers, dict)
                    or not mod.callback_handlers):
                continue

            for query_func in mod.callback_handlers.values():
                if self.check_inline_security(query_func, query.from_user.id):
                    try:
                        await query_func(query)
                    except Exception:
                        logger.exception("Error on running callback watcher!")
                        await query.answer(
                            "Error occured while processing request. More info in logs",
                            show_alert=True,
                        )
                        return

        for form_uid, form in self._forms.copy().items():
            for button in array_sum(form.get("buttons", [])):
                if button.get("_callback_data", None) == query.data:
                    if (form["force_me"] and query.from_user.id != self._me
                            and query.from_user.id not in self._client.
                            dispatcher.security._owner  # skipcq: PYL-W0212
                            and query.from_user.id
                            not in form["always_allow"]):
                        await query.answer(
                            "You are not allowed to press this button!")
                        return

                    query.delete = functools.partial(delete,
                                                     self=self,
                                                     form=form,
                                                     form_uid=form_uid)
                    query.unload = functools.partial(unload,
                                                     self=self,
                                                     form_uid=form_uid)
                    query.edit = functools.partial(edit,
                                                   self=self,
                                                   query=query,
                                                   form=form,
                                                   form_uid=form_uid)

                    query.form = {"id": form_uid, **form}

                    try:
                        return await button["callback"](
                            query,
                            *button.get("args", []),
                            **button.get("kwargs", {}),
                        )
                    except Exception:
                        logger.exception("Error on running callback watcher!")
                        await query.answer(
                            "Error occurred while "
                            "processing request. "
                            "More info in logs",
                            show_alert=True,
                        )
                        return

                    del self._forms[form_uid]

        if query.data in self._custom_map:
            if (self._custom_map[query.data]["force_me"]
                    and query.from_user.id != self._me
                    and query.from_user.id not in self._client.dispatcher.
                    security._owner  # skipcq: PYL-W0212
                    and query.from_user.id
                    not in self._custom_map[query.data]["always_allow"]):
                await query.answer("You are not allowed to press this button!")
                return

            await self._custom_map[query.data]["handler"](query)
            return

    async def _chosen_inline_handler(
            self, chosen_inline_query: ChosenInlineResult) -> None:
        query = chosen_inline_query.query

        for form_uid, form in self._forms.copy().items():
            for button in array_sum(form.get("buttons", [])):
                if ("_switch_query" in button and "input" in button
                        and button["_switch_query"] == query.split()[0] and
                        chosen_inline_query.from_user.id in [self._me] + self.
                        _client.dispatcher.security._owner  # skipcq: PYL-W0212
                        + form["always_allow"]):

                    query = query.split(
                        maxsplit=1)[1] if len(query.split()) > 1 else ""

                    call = InlineCall()

                    call.delete = functools.partial(delete,
                                                    self=self,
                                                    form=form,
                                                    form_uid=form_uid)
                    call.unload = functools.partial(unload,
                                                    self=self,
                                                    form_uid=form_uid)
                    call.edit = functools.partial(
                        edit,
                        self=self,
                        query=chosen_inline_query,
                        form=form,
                        form_uid=form_uid,
                    )

                    try:
                        return await button["handler"](
                            call,
                            query,
                            *button.get("args", []),
                            **button.get("kwargs", {}),
                        )
                    except Exception:
                        logger.exception(
                            "Exception while running chosen query watcher!")
                        return

    async def form(
        self,
        text: str,
        message: Union[Message, int],
        reply_markup: List[List[dict]] = None,
        force_me: bool = True,
        always_allow: List[int] = None,
        ttl: Union[int, bool] = False,
    ) -> Union[str, bool]:
        """Creates inline form with callback
        Args:
                text
                        Content of inline form. HTML markup support
                message
                        Where to send inline. Can be either `Message` or `int`
                reply_markup
                        List of buttons to insert in markup. List of dicts with
                        keys: text, callback
                force_me
                        Either this form buttons must be pressed only by owner scope or no
                always_allow
                        Users, that are allowed to press buttons in addition to previous rules
                ttl
                        Time, when the form is going to be unloaded. Unload means, that the form
                        buttons with inline queries and callback queries will become unusable, but
                        buttons with type url will still work as usual. Pay attention, that ttl can't
                        be bigger, than default one (1 day) and must be either `int` or `False`
        """

        if reply_markup is None:
            reply_markup = []

        if always_allow is None:
            always_allow = []

        if not isinstance(text, str):
            logger.error("Invalid type for `text`")
            return False

        if not isinstance(message, (Message, int)):
            logger.error("Invalid type for `message`")
            return False

        if not isinstance(reply_markup, list):
            logger.error("Invalid type for `reply_markup`")
            return False

        if not all(
                all(isinstance(button, dict) for button in row)
                for row in reply_markup):
            logger.error(
                "Invalid type for one of the buttons. It must be `dict`")
            return False

        if not all(
                all("url" in button or "callback" in button
                    or "input" in button or "data" in button for button in row)
                for row in reply_markup):
            logger.error("Invalid button specified. "
                         "Button must contain one of the following fields:\n"
                         "  - `url`\n"
                         "  - `callback`\n"
                         "  - `input`\n"
                         "  - `data`")
            return False

        if not isinstance(force_me, bool):
            logger.error("Invalid type for `force_me`")
            return False

        if not isinstance(always_allow, list):
            logger.error("Invalid type for `always_allow`")
            return False

        if not isinstance(ttl, int) and ttl:
            logger.error("Invalid type for `ttl`")
            return False

        if isinstance(ttl, int) and (ttl > self._markup_ttl or ttl < 10):
            ttl = self._markup_ttl
            logger.debug("Defaulted ttl, because it breaks out of limits")

        form_uid = rand(30)

        self._forms[form_uid] = {
            "text": text,
            "buttons": reply_markup,
            "ttl": round(time.time()) + ttl or self._markup_ttl,
            "force_me": force_me,
            "always_allow": always_allow,
            "chat": None,
            "message_id": None,
            "uid": form_uid,
        }

        try:
            q = await self._client.inline_query(self.bot_username, form_uid)
            m = await q[0].click(
                utils.get_chat_id(message)
                if isinstance(message, Message) else message,
                reply_to=message.reply_to_msg_id if isinstance(
                    message, Message) else None,
            )
        except Exception:
            msg = ("🚫 <b>A problem occurred with inline bot "
                   "while processing query. Check logs for "
                   "further info.</b>")

            del self._forms[form_uid]
            if isinstance(message, Message):
                await (message.edit if message.out else message.respond)(msg)
            else:
                await self._client.send_message(message, msg)

            return False

        self._forms[form_uid]["chat"] = utils.get_chat_id(m)
        self._forms[form_uid]["message_id"] = m.id
        if isinstance(message, Message):
            await message.delete()

        return form_uid

    async def gallery(
        self,
        caption: Union[str, FunctionType],
        message: Union[Message, int],
        next_handler: FunctionType,
        force_me: bool = False,
        always_allow: bool = False,
        ttl: int = False,
    ) -> Union[bool, str]:  # sourcery skip: raise-specific-error
        """
        Processes inline gallery
            caption
                    Caption for photo, or callable, returning caption
            message
                    Where to send inline. Can be either `Message` or `int`
            next_handler
                    Callback function, which must return url for next photo
            force_me
                    Either this form buttons must be pressed only by owner scope or no
            always_allow
                    Users, that are allowed to press buttons in addition to previous rules
            ttl
                    Time, when the form is going to be unloaded. Unload means, that the form
                    buttons with inline queries and callback queries will become unusable, but
                    buttons with type url will still work as usual. Pay attention, that ttl can't
                    be bigger, than default one (1 day) and must be either `int` or `False`
        """

        if not isinstance(caption, str) and not callable(caption):
            logger.error("Invalid type for `caption`")
            return False

        if not isinstance(message, (Message, int)):
            logger.error("Invalid type for `message`")
            return False

        if not isinstance(force_me, bool):
            logger.error("Invalid type for `force_me`")
            return False

        if always_allow and not isinstance(always_allow, list):
            logger.error("Invalid type for `always_allow`")
            return False

        if not always_allow:
            always_allow = []

        if not isinstance(ttl, int) and ttl:
            logger.error("Invalid type for `ttl`")
            return False

        if isinstance(ttl, int) and (ttl > self._markup_ttl or ttl < 10):
            ttl = self._markup_ttl
            logger.debug("Defaulted ttl, because it breaks out of limits")

        gallery_uid = rand(30)
        btn_call_data = rand(16)

        try:
            photo_url = await next_handler()
            if not isinstance(photo_url, str):
                raise Exception(
                    f"Got invalid result from `next_handler`. Expected `str`, got `{type(photo_url)}`"
                )
        except Exception:
            logger.exception("Error while parsing first photo in gallery")
            return False

        self._galleries[gallery_uid] = {
            "caption": caption,
            "ttl": round(time.time()) + ttl or self._markup_ttl,
            "force_me": force_me,
            "always_allow": always_allow,
            "chat": None,
            "message_id": None,
            "uid": gallery_uid,
            "photo_url": photo_url,
            "next_handler": next_handler,
            "btn_call_data": btn_call_data,
        }

        self._custom_map[btn_call_data] = {
            "handler":
            asyncio.coroutine(
                functools.partial(
                    custom_next_handler,
                    func=next_handler,
                    self=self,
                    btn_call_data=btn_call_data,
                    caption=caption,
                )),
            "always_allow":
            always_allow,
            "force_me":
            force_me,
        }

        try:
            q = await self._client.inline_query(self.bot_username, gallery_uid)
            m = await q[0].click(
                utils.get_chat_id(message)
                if isinstance(message, Message) else message,
                reply_to=message.reply_to_msg_id if isinstance(
                    message, Message) else None,
            )
        except Exception:
            msg = ("🚫 <b>A problem occurred with inline bot "
                   "while processing query. Check logs for "
                   "further info.</b>")

            del self._galleries[gallery_uid]
            if isinstance(message, Message):
                await (message.edit if message.out else message.respond)(msg)
            else:
                await self._client.send_message(message, msg)

            return False

        self._galleries[gallery_uid]["chat"] = utils.get_chat_id(m)
        self._galleries[gallery_uid]["message_id"] = m.id
        if isinstance(message, Message):
            await message.delete()

        return gallery_uid