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