示例#1
0
def start_console_client(db_user, chat_id: int) -> None:
    if not stateful_chat_is_enabled():
        cancel_command(chat_id)
        raise Exception("Stateful chat is required to be enabled")

    client = yandex_oauth.YandexOAuthConsoleClient()
    result = None

    try:
        result = client.before_user_interaction(db_user)
    except Exception as error:
        cancel_command(chat_id)
        raise error

    status = result["status"]

    if (status == yandex_oauth.OperationStatus.HAVE_ACCESS_TOKEN):
        message_have_access_token(chat_id)
    elif (status == yandex_oauth.OperationStatus.ACCESS_TOKEN_REFRESHED):
        message_access_token_refreshed(chat_id)
    elif (status == yandex_oauth.OperationStatus.CONTINUE_TO_URL):
        # only in that case further user actions is needed
        message_grant_access_code(chat_id, result["url"], result["lifetime"])
        # we will pass this state later
        set_user_chat_data(db_user.telegram_id, chat_id,
                           "yandex_oauth_console_client_state",
                           result["state"], result["lifetime"])
        # on next plain text message we will handle provided code
        set_disposable_handler(db_user.telegram_id, chat_id,
                               CommandName.YD_AUTH.value,
                               [DispatcherEvent.PLAIN_TEXT.value],
                               result["lifetime"])
    else:
        cancel_command(chat_id)
        raise Exception("Unknown operation status")
def handle(*args, **kwargs):
    message = kwargs.get("message", g.telegram_message)
    user_id = kwargs.get("user_id", g.telegram_user.id)
    chat_id = kwargs.get("chat_id", g.telegram_chat.id)
    folder_name = extract_absolute_path(message,
                                        CommandName.CREATE_FOLDER.value,
                                        kwargs.get("route_source"))

    if not folder_name:
        if stateful_chat_is_enabled():
            set_disposable_handler(
                user_id, chat_id, CommandName.CREATE_FOLDER.value, [
                    DispatcherEvent.PLAIN_TEXT.value,
                    DispatcherEvent.BOT_COMMAND.value,
                    DispatcherEvent.EMAIL.value, DispatcherEvent.HASHTAG.value,
                    DispatcherEvent.URL.value
                ], current_app.config["RUNTIME_DISPOSABLE_HANDLER_EXPIRE"])

        return request_absolute_folder_name(chat_id)

    user = g.db_user
    access_token = user.yandex_disk_token.get_access_token()
    last_status_code = None

    try:
        last_status_code = create_folder(user_access_token=access_token,
                                         folder_name=folder_name)
    except YandexAPIRequestError as error:
        cancel_command(chat_id)
        raise error
    except YandexAPICreateFolderError as error:
        send_yandex_disk_error(chat_id, str(error))

        # it is expected error and should be
        # logged only to user
        return

    text = None

    if (last_status_code == 201):
        text = "Created"
    elif (last_status_code == 409):
        text = "Already exists"
    else:
        text = f"Unknown operation status: {last_status_code}"

    telegram.send_message(chat_id=chat_id, text=text)
def handle(*args, **kwargs):
    """
    Handles `/unpublish` command.
    """
    message = kwargs.get("message", g.telegram_message)
    user_id = kwargs.get("user_id", g.telegram_user.id)
    chat_id = kwargs.get("chat_id", g.telegram_chat.id)
    path = extract_absolute_path(message, CommandName.UNPUBLISH.value,
                                 kwargs.get("route_source"))

    if not path:
        if stateful_chat_is_enabled():
            set_disposable_handler(
                user_id, chat_id, CommandName.UNPUBLISH.value, [
                    DispatcherEvent.PLAIN_TEXT.value,
                    DispatcherEvent.BOT_COMMAND.value,
                    DispatcherEvent.EMAIL.value, DispatcherEvent.HASHTAG.value,
                    DispatcherEvent.URL.value
                ], current_app.config["RUNTIME_DISPOSABLE_HANDLER_EXPIRE"])

        return request_absolute_path(chat_id)

    user = g.db_user
    access_token = user.yandex_disk_token.get_access_token()

    try:
        unpublish_item(access_token, path)
    except YandexAPIRequestError as error:
        cancel_command(chat_id)
        raise error
    except YandexAPIUnpublishItemError as error:
        send_yandex_disk_error(chat_id, str(error))

        # it is expected error and should be
        # logged only to user
        return

    telegram.send_message(chat_id=chat_id, text="Unpublished")
    def set_disposable_handler(self, user_id: int, chat_id: int) -> None:
        """
        Sets disposable handler.

        It is means that next message with matched
        `self.dispatcher_events` will be forwarded to
        `self.telegram_command`.

        - will be used when user didn't sent any
        suitable data for handling.

        :param user_id:
        Telegram ID of current user.
        :param chat_id:
        Telegram ID of current chat.
        """
        if not stateful_chat_is_enabled():
            return

        expire = current_app.config["RUNTIME_DISPOSABLE_HANDLER_EXPIRE"]

        set_disposable_handler(user_id, chat_id, self.telegram_command,
                               self.dispatcher_events, expire)
def intellectual_dispatch(message: TelegramMessage) -> Callable:
    """
    Intellectual dispatch to handlers of a message.
    Provides support for stateful chat (if Redis is enabled).

    Priority of handlers:
    1) if disposable handler exists and message events matches,
    then only that handler will be called and after removed.
    That disposable handler will be associated with message date.
    2) if subscribed handlers exists, then only ones with events
    matched to message events will be called. If nothing is matched,
    then forwarding to № 3
    3) attempt to get first `bot_command` entity from message.
    It will be treated as direct command. That direct command will be
    associated with message date. If nothing found, then forwarding to № 4
    4) attempt to get command based on message date.
    For example, when `/upload_photo` was used for message that was
    sent on `1607677734` date, and after that separate message was
    sent on same `1607677734` date, then it is the case.
    If nothing found, then forwarding to № 5
    5) guessing of command that user assumed based on
    content of message

    Events matching:
    - if at least one event matched, then that handler will be
    marked as "matched".

    If stateful chat not enabled, then № 1 and № 2 will be skipped.

    Note: there can be multiple handlers picked for single message.
    Order of execution not determined.

    :param message:
    Incoming Telegram message.

    :returns:
    It is guaranteed that most appropriate callable handler that
    not raises an error will be returned. Function arguments already
    configured, but you can also provided your own through `*args`
    and `**kwargs`. You should call this function in order to run
    handlers (there can be multiple handlers in one return function).
    """
    user_id = message.get_user().id
    chat_id = message.get_chat().id
    message_date = message.get_date()
    is_stateful_chat = stateful_chat_is_enabled()
    disposable_handler = None
    subscribed_handlers = None

    if is_stateful_chat:
        disposable_handler = get_disposable_handler(user_id, chat_id)
        subscribed_handlers = get_subscribed_handlers(user_id, chat_id)

    message_events = (detect_events(message) if
                      (disposable_handler or subscribed_handlers) else None)
    handler_names = deque()
    route_source = None

    if disposable_handler:
        match = match_events(message_events, disposable_handler["events"])

        if match:
            route_source = RouteSource.DISPOSABLE_HANDLER
            handler_names.append(disposable_handler["name"])
            delete_disposable_handler(user_id, chat_id)

    if (subscribed_handlers and not handler_names):
        for handler in subscribed_handlers:
            match = match_events(message_events, handler["events"])

            if match:
                route_source = RouteSource.SUBSCRIBED_HANDLER
                handler_names.append(handler["name"])

    if not handler_names:
        command = message.get_entity_value("bot_command")

        if command:
            route_source = RouteSource.DIRECT_COMMAND
            handler_names.append(command)

    should_bind_command_to_date = (is_stateful_chat
                                   and (route_source
                                        in (RouteSource.DISPOSABLE_HANDLER,
                                            RouteSource.DIRECT_COMMAND)))
    should_get_command_by_date = (is_stateful_chat and (not handler_names))

    if should_bind_command_to_date:
        # we expect only one active command
        command = handler_names[0]

        # we need to handle cases when user forwards
        # many separate messages (one with direct command and
        # others without any command but with some attachments).
        # These messages will be sended by Telegram one by one
        # (it is means we got separate direct command and
        # separate attachments without that any commands).
        # We also using `RouteSource.DISPOSABLE_HANDLER`
        # because user can start command without any attachments,
        # but forward multiple attachments at once or send
        # media group (media group messages have same date).
        bind_command_to_date(user_id, chat_id, message_date, command)
    elif should_get_command_by_date:
        command = get_command_by_date(user_id, chat_id, message_date)

        if command:
            route_source = RouteSource.SAME_DATE_COMMAND
            handler_names.append(command)

    if not handler_names:
        route_source = RouteSource.GUESSED_COMMAND
        handler_names.append(guess_bot_command(message))

    def method(*args, **kwargs):
        for handler_name in handler_names:
            handler_method = direct_dispatch(handler_name)

            try:
                handler_method(*args,
                               **kwargs,
                               user_id=user_id,
                               chat_id=chat_id,
                               message=message,
                               route_source=route_source,
                               message_events=message_events)
            except Exception as error:
                print(f"{handler_name}: {error}", "\n", traceback.format_exc())

    return method
示例#6
0
def handle(*args, **kwargs):
    """
    Handles `/element_info` command.
    """
    message = kwargs.get("message", g.telegram_message)
    user_id = kwargs.get("user_id", g.telegram_user.id)
    chat_id = kwargs.get("chat_id", g.telegram_chat.id)
    path = extract_absolute_path(message, CommandName.ELEMENT_INFO.value,
                                 kwargs.get("route_source"))

    if not path:
        if stateful_chat_is_enabled():
            set_disposable_handler(
                user_id, chat_id, CommandName.ELEMENT_INFO.value, [
                    DispatcherEvent.PLAIN_TEXT.value,
                    DispatcherEvent.BOT_COMMAND.value,
                    DispatcherEvent.EMAIL.value, DispatcherEvent.HASHTAG.value,
                    DispatcherEvent.URL.value
                ], current_app.config["RUNTIME_DISPOSABLE_HANDLER_EXPIRE"])

        return request_absolute_path(chat_id)

    user = g.db_user
    access_token = user.yandex_disk_token.get_access_token()
    info = None

    try:
        info = get_element_info(access_token, path, get_public_info=True)
    except YandexAPIRequestError as error:
        cancel_command(chat_id)
        raise error
    except YandexAPIGetElementInfoError as error:
        send_yandex_disk_error(chat_id, str(error))

        # it is expected error and should be
        # logged only to user
        return

    text = create_element_info_html_text(info, True)
    params = {
        "chat_id": chat_id,
        "text": text,
        "parse_mode": "HTML",
        "disable_web_page_preview": True
    }
    download_url = info.get("file")

    if download_url:
        params["reply_markup"] = {
            "inline_keyboard": [[{
                "text": "Download",
                "url": download_url
            }]]
        }

    # We will send message without preview,
    # because it can take a while to download
    # preview file and send it. We will
    # send it later if it is available.
    telegram.send_message(**params)

    preview_url = info.get("preview")

    if preview_url:
        filename = info.get("name", "preview.jpg")
        arguments = (preview_url, filename, access_token, chat_id)

        if task_queue.is_enabled:
            job_timeout = current_app.config[
                "RUNTIME_ELEMENT_INFO_WORKER_JOB_TIMEOUT"]
            ttl = current_app.config["RUNTIME_ELEMENT_INFO_WORKER_TTL"]

            task_queue.enqueue(send_preview,
                               args=arguments,
                               description=CommandName.ELEMENT_INFO.value,
                               job_timeout=job_timeout,
                               ttl=ttl,
                               result_ttl=0,
                               failure_ttl=0)
        else:
            # NOTE: current thread will
            # be blocked for a while
            send_preview(*arguments)