//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2021
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "telegram-bot-api/Client.h"

#include "telegram-bot-api/ClientParameters.h"

#include "td/actor/PromiseFuture.h"
#include "td/actor/SleepActor.h"

#include "td/db/TQueue.h"

#include "td/utils/algorithm.h"
#include "td/utils/base64.h"
#include "td/utils/filesystem.h"
#include "td/utils/HttpUrl.h"
#include "td/utils/JsonBuilder.h"
#include "td/utils/logging.h"
#include "td/utils/misc.h"
#include "td/utils/PathView.h"
#include "td/utils/port/path.h"
#include "td/utils/port/Stat.h"
#include "td/utils/Random.h"
#include "td/utils/Slice.h"
#include "td/utils/SliceBuilder.h"
#include "td/utils/Span.h"
#include "td/utils/StackAllocator.h"
#include "td/utils/Status.h"
#include "td/utils/StringBuilder.h"
#include "td/utils/Time.h"
#include "td/utils/utf8.h"

#include <cstdlib>

namespace telegram_bot_api {

using td::Jsonable;
using td::JsonValueScope;
using td::JsonValue;

using td_api::make_object;
using td_api::move_object_as;

void Client::fail_query_with_error(PromisedQueryPtr query, int32 error_code, Slice error_message,
                                   Slice default_message) {
  if (error_code == 429) {
    Slice prefix = "Too Many Requests: retry after ";
    if (begins_with(error_message, prefix)) {
      auto r_retry_after = td::to_integer_safe<int>(error_message.substr(prefix.size()));
      if (r_retry_after.is_ok() && r_retry_after.ok() > 0) {
        return query->set_retry_after_error(r_retry_after.ok());
      }
    }
    LOG(ERROR) << "Wrong error message: " << error_message << " from " << *query;
    return fail_query(500, error_message, std::move(query));
  }
  int32 real_error_code = error_code;
  Slice real_error_message = error_message;
  if (error_code < 300 || error_code == 404) {
    if (error_code <= 0) {
      LOG(ERROR) << "Receive error \"" << real_error_message << "\" with code " << error_code << " from " << *query;
    }

    error_code = 400;
  }
  if (error_code == 400) {
    if (!default_message.empty()) {
      error_message = default_message;
    }
    if (error_message == "MESSAGE_NOT_MODIFIED") {
      error_message = Slice(
          "message is not modified: specified new message content and reply markup are exactly the same as a current "
          "content and reply markup of the message");
    } else if (error_message == "WC_CONVERT_URL_INVALID" || error_message == "EXTERNAL_URL_INVALID") {
      error_message = "Wrong HTTP URL specified";
    } else if (error_message == "WEBPAGE_CURL_FAILED") {
      error_message = "Failed to get HTTP URL content";
    } else if (error_message == "WEBPAGE_MEDIA_EMPTY") {
      error_message = "Wrong type of the web page content";
    } else if (error_message == "MEDIA_GROUPED_INVALID") {
      error_message = "Can't use the media of the specified type in the album";
    } else if (error_message == "REPLY_MARKUP_TOO_LONG") {
      error_message = Slice("reply markup is too long");
    } else if (error_message == "INPUT_USER_DEACTIVATED") {
      error_code = 403;
      error_message = Slice("Forbidden: user is deactivated");
    } else if (error_message == "USER_IS_BLOCKED") {
      error_code = 403;
      error_message = Slice("bot was blocked by the user");
    } else if (error_message == "USER_ADMIN_INVALID") {
      error_code = 400;
      error_message = Slice("user is an administrator of the chat");
    } else if (error_message == "File generation failed") {
      error_code = 400;
      error_message = Slice("can't upload file by URL");
    } else if (error_message == "CHAT_ABOUT_NOT_MODIFIED") {
      error_code = 400;
      error_message = Slice("chat description is not modified");
    } else if (error_message == "PACK_SHORT_NAME_INVALID") {
      error_code = 400;
      error_message = Slice("invalid sticker set name is specified");
    } else if (error_message == "PACK_SHORT_NAME_OCCUPIED") {
      error_code = 400;
      error_message = Slice("sticker set name is already occupied");
    } else if (error_message == "STICKER_EMOJI_INVALID") {
      error_code = 400;
      error_message = Slice("invalid sticker emojis");
    } else if (error_message == "QUERY_ID_INVALID") {
      error_code = 400;
      error_message = Slice("query is too old and response timeout expired or query ID is invalid");
    } else if (error_message == "MESSAGE_DELETE_FORBIDDEN") {
      error_code = 400;
      error_message = Slice("message can't be deleted");
    }
  }
  Slice prefix;
  switch (error_code) {
    case 400:
      prefix = Slice("Bad Request");
      break;
    case 401:
      prefix = Slice("Unauthorized");
      break;
    case 403:
      prefix = Slice("Forbidden");
      break;
    case 500:
      prefix = Slice("Internal Server Error");
      if (real_error_message != Slice("Request aborted")) {
        LOG(ERROR) << "Receive Internal Server Error \"" << real_error_message << "\" from " << *query;
      }
      break;
    default:
      LOG(ERROR) << "Unsupported error " << real_error_code << ": " << real_error_message << " from " << *query;
      return fail_query(400, PSLICE() << "Bad Request: " << error_message, std::move(query));
  }

  if (begins_with(error_message, prefix)) {
    return fail_query(error_code, error_message, std::move(query));
  } else {
    td::string error_str = prefix.str();
    if (error_message.empty()) {
      LOG(ERROR) << "Empty error message with code " << real_error_code << " from " << *query;
    } else {
      error_str += ": ";
      if (error_message.size() >= 2u &&
          (error_message[1] == '_' || ('A' <= error_message[1] && error_message[1] <= 'Z'))) {
        error_str += error_message.str();
      } else {
        error_str += td::to_lower(error_message[0]);
        error_str += error_message.substr(1).str();
      }
    }
    return fail_query(error_code, error_str, std::move(query));
  }
}

void Client::fail_query_with_error(PromisedQueryPtr &&query, object_ptr<td_api::error> error, Slice default_message) {
  fail_query_with_error(std::move(query), error->code_, error->message_, default_message);
}

Client::Client(td::ActorShared<> parent, const td::string &bot_token, bool is_test_dc, int64 tqueue_id,
               std::shared_ptr<const ClientParameters> parameters, td::ActorId<BotStatActor> stat_actor)
    : parent_(std::move(parent))
    , bot_token_(bot_token)
    , bot_token_id_("<unknown>")
    , is_test_dc_(is_test_dc)
    , tqueue_id_(tqueue_id)
    , parameters_(std::move(parameters))
    , stat_actor_(std::move(stat_actor)) {
  messages_lru_root_.lru_next = &messages_lru_root_;
  messages_lru_root_.lru_prev = &messages_lru_root_;

  static auto is_inited = init_methods();
  CHECK(is_inited);
}

bool Client::init_methods() {
  methods_.emplace("getme", &Client::process_get_me_query);
  methods_.emplace("getmycommands", &Client::process_get_my_commands_query);
  methods_.emplace("setmycommands", &Client::process_set_my_commands_query);
  methods_.emplace("deletemycommands", &Client::process_delete_my_commands_query);
  methods_.emplace("getuserprofilephotos", &Client::process_get_user_profile_photos_query);
  methods_.emplace("sendmessage", &Client::process_send_message_query);
  methods_.emplace("sendanimation", &Client::process_send_animation_query);
  methods_.emplace("sendaudio", &Client::process_send_audio_query);
  methods_.emplace("senddice", &Client::process_send_dice_query);
  methods_.emplace("senddocument", &Client::process_send_document_query);
  methods_.emplace("sendphoto", &Client::process_send_photo_query);
  methods_.emplace("sendsticker", &Client::process_send_sticker_query);
  methods_.emplace("sendvideo", &Client::process_send_video_query);
  methods_.emplace("sendvideonote", &Client::process_send_video_note_query);
  methods_.emplace("sendvoice", &Client::process_send_voice_query);
  methods_.emplace("sendgame", &Client::process_send_game_query);
  methods_.emplace("sendinvoice", &Client::process_send_invoice_query);
  methods_.emplace("sendlocation", &Client::process_send_location_query);
  methods_.emplace("sendvenue", &Client::process_send_venue_query);
  methods_.emplace("sendcontact", &Client::process_send_contact_query);
  methods_.emplace("sendpoll", &Client::process_send_poll_query);
  methods_.emplace("stoppoll", &Client::process_stop_poll_query);
  methods_.emplace("copymessage", &Client::process_copy_message_query);
  methods_.emplace("forwardmessage", &Client::process_forward_message_query);
  methods_.emplace("sendmediagroup", &Client::process_send_media_group_query);
  methods_.emplace("sendchataction", &Client::process_send_chat_action_query);
  methods_.emplace("editmessagetext", &Client::process_edit_message_text_query);
  methods_.emplace("editmessagelivelocation", &Client::process_edit_message_live_location_query);
  methods_.emplace("stopmessagelivelocation", &Client::process_edit_message_live_location_query);
  methods_.emplace("editmessagemedia", &Client::process_edit_message_media_query);
  methods_.emplace("editmessagecaption", &Client::process_edit_message_caption_query);
  methods_.emplace("editmessagereplymarkup", &Client::process_edit_message_reply_markup_query);
  methods_.emplace("deletemessage", &Client::process_delete_message_query);
  methods_.emplace("setgamescore", &Client::process_set_game_score_query);
  methods_.emplace("getgamehighscores", &Client::process_get_game_high_scores_query);
  methods_.emplace("answerinlinequery", &Client::process_answer_inline_query_query);
  methods_.emplace("answercallbackquery", &Client::process_answer_callback_query_query);
  methods_.emplace("answershippingquery", &Client::process_answer_shipping_query_query);
  methods_.emplace("answerprecheckoutquery", &Client::process_answer_pre_checkout_query_query);
  methods_.emplace("exportchatinvitelink", &Client::process_export_chat_invite_link_query);
  methods_.emplace("createchatinvitelink", &Client::process_create_chat_invite_link_query);
  methods_.emplace("editchatinvitelink", &Client::process_edit_chat_invite_link_query);
  methods_.emplace("revokechatinvitelink", &Client::process_revoke_chat_invite_link_query);
  methods_.emplace("getchat", &Client::process_get_chat_query);
  methods_.emplace("setchatphoto", &Client::process_set_chat_photo_query);
  methods_.emplace("deletechatphoto", &Client::process_delete_chat_photo_query);
  methods_.emplace("setchattitle", &Client::process_set_chat_title_query);
  methods_.emplace("setchatpermissions", &Client::process_set_chat_permissions_query);
  methods_.emplace("setchatdescription", &Client::process_set_chat_description_query);
  methods_.emplace("pinchatmessage", &Client::process_pin_chat_message_query);
  methods_.emplace("unpinchatmessage", &Client::process_unpin_chat_message_query);
  methods_.emplace("unpinallchatmessages", &Client::process_unpin_all_chat_messages_query);
  methods_.emplace("setchatstickerset", &Client::process_set_chat_sticker_set_query);
  methods_.emplace("deletechatstickerset", &Client::process_delete_chat_sticker_set_query);
  methods_.emplace("getchatmember", &Client::process_get_chat_member_query);
  methods_.emplace("getchatadministrators", &Client::process_get_chat_administrators_query);
  methods_.emplace("getchatmembercount", &Client::process_get_chat_member_count_query);
  methods_.emplace("getchatmemberscount", &Client::process_get_chat_member_count_query);
  methods_.emplace("leavechat", &Client::process_leave_chat_query);
  methods_.emplace("promotechatmember", &Client::process_promote_chat_member_query);
  methods_.emplace("setchatadministratorcustomtitle", &Client::process_set_chat_administrator_custom_title_query);
  methods_.emplace("banchatmember", &Client::process_ban_chat_member_query);
  methods_.emplace("kickchatmember", &Client::process_ban_chat_member_query);
  methods_.emplace("restrictchatmember", &Client::process_restrict_chat_member_query);
  methods_.emplace("unbanchatmember", &Client::process_unban_chat_member_query);
  methods_.emplace("getstickerset", &Client::process_get_sticker_set_query);
  methods_.emplace("uploadstickerfile", &Client::process_upload_sticker_file_query);
  methods_.emplace("createnewstickerset", &Client::process_create_new_sticker_set_query);
  methods_.emplace("addstickertoset", &Client::process_add_sticker_to_set_query);
  methods_.emplace("setstickersetthumb", &Client::process_set_sticker_set_thumb_query);
  methods_.emplace("setstickerpositioninset", &Client::process_set_sticker_position_in_set_query);
  methods_.emplace("deletestickerfromset", &Client::process_delete_sticker_from_set_query);
  methods_.emplace("setpassportdataerrors", &Client::process_set_passport_data_errors_query);
  methods_.emplace("sendcustomrequest", &Client::process_send_custom_request_query);
  methods_.emplace("answercustomquery", &Client::process_answer_custom_query_query);
  methods_.emplace("getupdates", &Client::process_get_updates_query);
  methods_.emplace("setwebhook", &Client::process_set_webhook_query);
  methods_.emplace("deletewebhook", &Client::process_set_webhook_query);
  methods_.emplace("getwebhookinfo", &Client::process_get_webhook_info_query);
  methods_.emplace("getfile", &Client::process_get_file_query);
  return true;
}

class Client::JsonFile : public Jsonable {
 public:
  JsonFile(const td_api::file *file, const Client *client) : file_(file), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    client_->json_store_file(object, file_, true);
  }

 private:
  const td_api::file *file_;
  const Client *client_;
};

class Client::JsonDatedFile : public Jsonable {
 public:
  JsonDatedFile(const td_api::datedFile *file, const Client *client) : file_(file), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    client_->json_store_file(object, file_->file_.get());
    object("file_date", file_->date_);
  }

 private:
  const td_api::datedFile *file_;
  const Client *client_;
};

class Client::JsonDatedFiles : public Jsonable {
 public:
  JsonDatedFiles(const td::vector<object_ptr<td_api::datedFile>> &files, const Client *client)
      : files_(files), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto array = scope->enter_array();
    for (auto &file : files_) {
      array << JsonDatedFile(file.get(), client_);
    }
  }

 private:
  const td::vector<object_ptr<td_api::datedFile>> &files_;
  const Client *client_;
};

class Client::JsonUser : public Jsonable {
 public:
  JsonUser(int32 user_id, const Client *client, bool full_bot_info = false)
      : user_id_(user_id), client_(client), full_bot_info_(full_bot_info) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    auto user_info = client_->get_user_info(user_id_);
    object("id", user_id_);
    bool is_bot = user_info != nullptr && user_info->type == UserInfo::Type::Bot;
    object("is_bot", td::JsonBool(is_bot));
    object("first_name", user_info == nullptr ? "" : user_info->first_name);
    if (user_info != nullptr && !user_info->last_name.empty()) {
      object("last_name", user_info->last_name);
    }
    if (user_info != nullptr && !user_info->username.empty()) {
      object("username", user_info->username);
    }
    if (user_info != nullptr && !user_info->language_code.empty()) {
      object("language_code", user_info->language_code);
    }
    if (is_bot && full_bot_info_) {
      object("can_join_groups", td::JsonBool(user_info->can_join_groups));
      object("can_read_all_group_messages", td::JsonBool(user_info->can_read_all_group_messages));
      object("supports_inline_queries", td::JsonBool(user_info->is_inline_bot));
    }
  }

 private:
  int32 user_id_;
  const Client *client_;
  bool full_bot_info_;
};

class Client::JsonUsers : public Jsonable {
 public:
  JsonUsers(const td::vector<int32> &user_ids, const Client *client) : user_ids_(user_ids), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto array = scope->enter_array();
    for (auto &user_id : user_ids_) {
      array << JsonUser(user_id, client_);
    }
  }

 private:
  const td::vector<int32> &user_ids_;
  const Client *client_;
};

class Client::JsonEntity : public Jsonable {
 public:
  JsonEntity(const td_api::textEntity *entity, const Client *client) : entity_(entity), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("offset", entity_->offset_);
    object("length", entity_->length_);
    switch (entity_->type_->get_id()) {
      case td_api::textEntityTypeMention::ID:
        object("type", "mention");
        break;
      case td_api::textEntityTypeHashtag::ID:
        object("type", "hashtag");
        break;
      case td_api::textEntityTypeCashtag::ID:
        object("type", "cashtag");
        break;
      case td_api::textEntityTypeBotCommand::ID:
        object("type", "bot_command");
        break;
      case td_api::textEntityTypeUrl::ID:
        object("type", "url");
        break;
      case td_api::textEntityTypeEmailAddress::ID:
        object("type", "email");
        break;
      case td_api::textEntityTypePhoneNumber::ID:
        object("type", "phone_number");
        break;
      case td_api::textEntityTypeBankCardNumber::ID:
        object("type", "bank_card_number");
        break;
      case td_api::textEntityTypeBold::ID:
        object("type", "bold");
        break;
      case td_api::textEntityTypeItalic::ID:
        object("type", "italic");
        break;
      case td_api::textEntityTypeUnderline::ID:
        object("type", "underline");
        break;
      case td_api::textEntityTypeStrikethrough::ID:
        object("type", "strikethrough");
        break;
      case td_api::textEntityTypeCode::ID:
        object("type", "code");
        break;
      case td_api::textEntityTypePre::ID:
        object("type", "pre");
        break;
      case td_api::textEntityTypePreCode::ID: {
        auto entity = static_cast<const td_api::textEntityTypePreCode *>(entity_->type_.get());
        object("type", "pre");
        object("language", entity->language_);
        break;
      }
      case td_api::textEntityTypeTextUrl::ID: {
        auto entity = static_cast<const td_api::textEntityTypeTextUrl *>(entity_->type_.get());
        object("type", "text_link");
        object("url", entity->url_);
        break;
      }
      case td_api::textEntityTypeMentionName::ID: {
        auto entity = static_cast<const td_api::textEntityTypeMentionName *>(entity_->type_.get());
        object("type", "text_mention");
        object("user", JsonUser(entity->user_id_, client_));
        break;
      }
      default:
        UNREACHABLE();
    }
  }

 private:
  const td_api::textEntity *entity_;
  const Client *client_;
};

class Client::JsonVectorEntities : public Jsonable {
 public:
  JsonVectorEntities(const td::vector<object_ptr<td_api::textEntity>> &entities, const Client *client)
      : entities_(entities), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto array = scope->enter_array();
    for (auto &entity : entities_) {
      if (entity->type_->get_id() != td_api::textEntityTypeBankCardNumber::ID) {
        array << JsonEntity(entity.get(), client_);
      }
    }
  }

 private:
  const td::vector<object_ptr<td_api::textEntity>> &entities_;
  const Client *client_;
};

class Client::JsonLocation : public Jsonable {
 public:
  explicit JsonLocation(const td_api::location *location, double expires_in = 0.0, int32 live_period = 0,
                        int32 heading = 0, int32 proximity_alert_radius = 0)
      : location_(location)
      , expires_in_(expires_in)
      , live_period_(live_period)
      , heading_(heading)
      , proximity_alert_radius_(proximity_alert_radius) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("latitude", location_->latitude_);
    object("longitude", location_->longitude_);
    if (expires_in_ > 0.0) {
      object("live_period", live_period_);
      if (heading_ > 0) {
        object("heading", heading_);
      }
      if (proximity_alert_radius_ > 0) {
        object("proximity_alert_radius", proximity_alert_radius_);
      }
    }
    if (location_->horizontal_accuracy_ > 0) {
      object("horizontal_accuracy", location_->horizontal_accuracy_);
    }
  }

 private:
  const td_api::location *location_;
  double expires_in_;
  int32 live_period_;
  int32 heading_;
  int32 proximity_alert_radius_;
};

class Client::JsonChatPermissions : public Jsonable {
 public:
  explicit JsonChatPermissions(const td_api::chatPermissions *chat_permissions) : chat_permissions_(chat_permissions) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    Client::json_store_permissions(object, chat_permissions_);
  }

 private:
  const td_api::chatPermissions *chat_permissions_;
};

class Client::JsonChatPhotoInfo : public Jsonable {
 public:
  explicit JsonChatPhotoInfo(const td_api::chatPhotoInfo *chat_photo) : chat_photo_(chat_photo) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("small_file_id", chat_photo_->small_->remote_->id_);
    object("small_file_unique_id", chat_photo_->small_->remote_->unique_id_);
    object("big_file_id", chat_photo_->big_->remote_->id_);
    object("big_file_unique_id", chat_photo_->big_->remote_->unique_id_);
  }

 private:
  const td_api::chatPhotoInfo *chat_photo_;
};

class Client::JsonChatLocation : public Jsonable {
 public:
  explicit JsonChatLocation(const td_api::chatLocation *chat_location) : chat_location_(chat_location) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("location", JsonLocation(chat_location_->location_.get()));
    object("address", chat_location_->address_);
  }

 private:
  const td_api::chatLocation *chat_location_;
};

class Client::JsonChatInviteLink : public Jsonable {
 public:
  JsonChatInviteLink(const td_api::chatInviteLink *chat_invite_link, const Client *client)
      : chat_invite_link_(chat_invite_link), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("invite_link", chat_invite_link_->invite_link_);
    object("creator", JsonUser(chat_invite_link_->creator_user_id_, client_));
    if (chat_invite_link_->expire_date_ != 0) {
      object("expire_date", chat_invite_link_->expire_date_);
    }
    if (chat_invite_link_->member_limit_ != 0) {
      object("member_limit", chat_invite_link_->member_limit_);
    }
    object("is_primary", td::JsonBool(chat_invite_link_->is_primary_));
    object("is_revoked", td::JsonBool(chat_invite_link_->is_revoked_));
  }

 private:
  const td_api::chatInviteLink *chat_invite_link_;
  const Client *client_;
};

class Client::JsonMessage : public Jsonable {
 public:
  JsonMessage(const MessageInfo *message, bool need_reply, const td::string &source, const Client *client)
      : message_(message), need_reply_(need_reply), source_(source), client_(client) {
  }
  void store(JsonValueScope *scope) const;

 private:
  const MessageInfo *message_;
  bool need_reply_;
  const td::string &source_;
  const Client *client_;

  void add_caption(td::JsonObjectScope &object, const object_ptr<td_api::formattedText> &caption) const {
    CHECK(caption != nullptr);
    if (!caption->text_.empty()) {
      object("caption", caption->text_);

      if (!caption->entities_.empty()) {
        object("caption_entities", JsonVectorEntities(caption->entities_, client_));
      }
    }
  }
};

class Client::JsonChat : public Jsonable {
 public:
  JsonChat(int64 chat_id, bool is_full, const Client *client, int64 pinned_message_id = -1)
      : chat_id_(chat_id), is_full_(is_full), client_(client), pinned_message_id_(pinned_message_id) {
  }
  void store(JsonValueScope *scope) const {
    auto chat_info = client_->get_chat(chat_id_);
    CHECK(chat_info != nullptr);
    auto object = scope->enter_object();
    object("id", chat_id_);
    switch (chat_info->type) {
      case ChatInfo::Type::Private: {
        auto user_info = client_->get_user_info(chat_info->user_id);
        CHECK(user_info != nullptr);
        object("first_name", user_info->first_name);
        if (!user_info->last_name.empty()) {
          object("last_name", user_info->last_name);
        }
        if (!user_info->username.empty()) {
          object("username", user_info->username);
        }
        object("type", "private");
        if (is_full_) {
          if (!user_info->bio.empty()) {
            object("bio", user_info->bio);
          }
        }
        break;
      }
      case ChatInfo::Type::Group: {
        object("title", chat_info->title);
        object("type", "group");

        const auto *permissions = chat_info->permissions.get();

        auto group_info = client_->get_group_info(chat_info->group_id);
        CHECK(group_info != nullptr);
        if (is_full_) {
          if (!group_info->description.empty()) {
            object("description", group_info->description);
          }
          if (!group_info->invite_link.empty()) {
            object("invite_link", group_info->invite_link);
          }
          object("permissions", JsonChatPermissions(permissions));
        }
        auto everyone_is_administrator = permissions->can_send_messages_ && permissions->can_send_media_messages_ &&
                                         permissions->can_send_polls_ && permissions->can_send_other_messages_ &&
                                         permissions->can_add_web_page_previews_ && permissions->can_change_info_ &&
                                         permissions->can_invite_users_ && permissions->can_pin_messages_;
        object("all_members_are_administrators", td::JsonBool(everyone_is_administrator));
        break;
      }
      case ChatInfo::Type::Supergroup: {
        object("title", chat_info->title);

        auto supergroup_info = client_->get_supergroup_info(chat_info->supergroup_id);
        CHECK(supergroup_info != nullptr);
        if (!supergroup_info->username.empty()) {
          object("username", supergroup_info->username);
        }

        if (supergroup_info->is_supergroup) {
          object("type", "supergroup");
        } else {
          object("type", "channel");
        }
        if (is_full_) {
          if (!supergroup_info->description.empty()) {
            object("description", supergroup_info->description);
          }
          if (!supergroup_info->invite_link.empty()) {
            object("invite_link", supergroup_info->invite_link);
          }
          if (supergroup_info->sticker_set_id != 0) {
            auto sticker_set_name = client_->get_sticker_set_name(supergroup_info->sticker_set_id);
            if (!sticker_set_name.empty()) {
              object("sticker_set_name", sticker_set_name);
            } else {
              LOG(ERROR) << "Not found chat sticker set " << supergroup_info->sticker_set_id;
            }
          }
          if (supergroup_info->can_set_sticker_set) {
            object("can_set_sticker_set", td::JsonTrue());
          }
          if (supergroup_info->is_supergroup) {
            object("permissions", JsonChatPermissions(chat_info->permissions.get()));
          }
          if (supergroup_info->slow_mode_delay != 0) {
            object("slow_mode_delay", supergroup_info->slow_mode_delay);
          }
          if (supergroup_info->linked_chat_id != 0) {
            object("linked_chat_id", supergroup_info->linked_chat_id);
          }
          if (supergroup_info->location != nullptr) {
            object("location", JsonChatLocation(supergroup_info->location.get()));
          }
        }
        break;
      }
      case ChatInfo::Type::Unknown:
      default:
        UNREACHABLE();
    }
    if (is_full_) {
      if (chat_info->photo != nullptr) {
        object("photo", JsonChatPhotoInfo(chat_info->photo.get()));
      }
      if (pinned_message_id_ != 0) {
        CHECK(pinned_message_id_ != -1);
        const MessageInfo *pinned_message = client_->get_message(chat_id_, pinned_message_id_);
        if (pinned_message != nullptr) {
          object("pinned_message", JsonMessage(pinned_message, false, "pin in JsonChat", client_));
        } else {
          LOG(ERROR) << "Pinned unknown, inaccessible or deleted message " << pinned_message_id_;
        }
      }
      if (chat_info->message_auto_delete_time != 0) {
        object("message_auto_delete_time", chat_info->message_auto_delete_time);
      }
    }
  }

 private:
  int64 chat_id_;
  bool is_full_;
  const Client *client_;
  int64 pinned_message_id_;
};

class Client::JsonMessageSender : public Jsonable {
 public:
  JsonMessageSender(const td_api::MessageSender *sender, const Client *client) : sender_(sender), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    CHECK(sender_ != nullptr);
    switch (sender_->get_id()) {
      case td_api::messageSenderUser::ID: {
        auto sender_user_id = static_cast<const td_api::messageSenderUser *>(sender_)->user_id_;
        JsonUser(sender_user_id, client_).store(scope);
        break;
      }
      case td_api::messageSenderChat::ID: {
        auto sender_chat_id = static_cast<const td_api::messageSenderChat *>(sender_)->chat_id_;
        JsonChat(sender_chat_id, false, client_).store(scope);
        break;
      }
      default:
        UNREACHABLE();
    }
  }

 private:
  const td_api::MessageSender *sender_;
  const Client *client_;
};

class Client::JsonMessages : public Jsonable {
 public:
  explicit JsonMessages(const td::vector<td::string> &messages) : messages_(messages) {
  }
  void store(JsonValueScope *scope) const {
    auto array = scope->enter_array();
    for (auto &message : messages_) {
      array << td::JsonRaw(message);
    }
  }

 private:
  const td::vector<td::string> &messages_;
};

class Client::JsonAnimation : public Jsonable {
 public:
  JsonAnimation(const td_api::animation *animation, bool as_document, const Client *client)
      : animation_(animation), as_document_(as_document), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    if (!animation_->file_name_.empty()) {
      object("file_name", animation_->file_name_);
    }
    if (!animation_->mime_type_.empty()) {
      object("mime_type", animation_->mime_type_);
    }
    if (!as_document_) {
      object("duration", animation_->duration_);
      object("width", animation_->width_);
      object("height", animation_->height_);
    }
    client_->json_store_thumbnail(object, animation_->thumbnail_.get());
    client_->json_store_file(object, animation_->animation_.get());
  }

 private:
  const td_api::animation *animation_;
  bool as_document_;
  const Client *client_;
};

class Client::JsonAudio : public Jsonable {
 public:
  JsonAudio(const td_api::audio *audio, const Client *client) : audio_(audio), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("duration", audio_->duration_);
    if (!audio_->file_name_.empty()) {
      object("file_name", audio_->file_name_);
    }
    if (!audio_->mime_type_.empty()) {
      object("mime_type", audio_->mime_type_);
    }
    if (!audio_->title_.empty()) {
      object("title", audio_->title_);
    }
    if (!audio_->performer_.empty()) {
      object("performer", audio_->performer_);
    }
    client_->json_store_thumbnail(object, audio_->album_cover_thumbnail_.get());
    client_->json_store_file(object, audio_->audio_.get());
  }

 private:
  const td_api::audio *audio_;
  const Client *client_;
};

class Client::JsonDocument : public Jsonable {
 public:
  JsonDocument(const td_api::document *document, const Client *client) : document_(document), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    if (!document_->file_name_.empty()) {
      object("file_name", document_->file_name_);
    }
    if (!document_->mime_type_.empty()) {
      object("mime_type", document_->mime_type_);
    }
    client_->json_store_thumbnail(object, document_->thumbnail_.get());
    client_->json_store_file(object, document_->document_.get());
  }

 private:
  const td_api::document *document_;
  const Client *client_;
};

class Client::JsonPhotoSize : public Jsonable {
 public:
  JsonPhotoSize(const td_api::photoSize *photo_size, const Client *client) : photo_size_(photo_size), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    client_->json_store_file(object, photo_size_->photo_.get());
    object("width", photo_size_->width_);
    object("height", photo_size_->height_);
  }

 private:
  const td_api::photoSize *photo_size_;
  const Client *client_;
};

class Client::JsonThumbnail : public Jsonable {
 public:
  JsonThumbnail(const td_api::thumbnail *thumbnail, const Client *client) : thumbnail_(thumbnail), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    client_->json_store_file(object, thumbnail_->file_.get());
    object("width", thumbnail_->width_);
    object("height", thumbnail_->height_);
  }

 private:
  const td_api::thumbnail *thumbnail_;
  const Client *client_;
};

class Client::JsonPhoto : public Jsonable {
 public:
  JsonPhoto(const td_api::photo *photo, const Client *client) : photo_(photo), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto array = scope->enter_array();
    for (auto &photo_size : photo_->sizes_) {
      if (photo_size->type_ != "i" && photo_size->type_ != "t" && !photo_size->photo_->remote_->id_.empty()) {
        array << JsonPhotoSize(photo_size.get(), client_);
      }
    }
  }

 private:
  const td_api::photo *photo_;
  const Client *client_;
};

class Client::JsonChatPhoto : public Jsonable {
 public:
  JsonChatPhoto(const td_api::chatPhoto *photo, const Client *client) : photo_(photo), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto array = scope->enter_array();
    for (auto &photo_size : photo_->sizes_) {
      if (photo_size->type_ != "i" && photo_size->type_ != "t" && !photo_size->photo_->remote_->id_.empty()) {
        array << JsonPhotoSize(photo_size.get(), client_);
      }
    }
  }

 private:
  const td_api::chatPhoto *photo_;
  const Client *client_;
};

class Client::JsonMaskPosition : public Jsonable {
 public:
  explicit JsonMaskPosition(const td_api::maskPosition *mask_position) : mask_position_(mask_position) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("point", Client::MASK_POINTS[Client::mask_point_to_index(mask_position_->point_)]);
    object("x_shift", mask_position_->x_shift_);
    object("y_shift", mask_position_->y_shift_);
    object("scale", mask_position_->scale_);
  }

 private:
  const td_api::maskPosition *mask_position_;
};

class Client::JsonSticker : public Jsonable {
 public:
  JsonSticker(const td_api::sticker *sticker, const Client *client) : sticker_(sticker), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("width", sticker_->width_);
    object("height", sticker_->height_);
    if (!sticker_->emoji_.empty()) {
      object("emoji", sticker_->emoji_);
    }
    auto set_name = client_->get_sticker_set_name(sticker_->set_id_);
    if (!set_name.empty()) {
      object("set_name", set_name);
    }
    object("is_animated", td::JsonBool(sticker_->is_animated_));
    if (sticker_->mask_position_ != nullptr) {
      object("mask_position", JsonMaskPosition(sticker_->mask_position_.get()));
    }
    client_->json_store_thumbnail(object, sticker_->thumbnail_.get());
    client_->json_store_file(object, sticker_->sticker_.get());
  }

 private:
  const td_api::sticker *sticker_;
  const Client *client_;
};

class Client::JsonStickers : public Jsonable {
 public:
  JsonStickers(const td::vector<object_ptr<td_api::sticker>> &stickers, const Client *client)
      : stickers_(stickers), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto array = scope->enter_array();
    for (auto &sticker : stickers_) {
      array << JsonSticker(sticker.get(), client_);
    }
  }

 private:
  const td::vector<object_ptr<td_api::sticker>> &stickers_;
  const Client *client_;
};

class Client::JsonVideo : public Jsonable {
 public:
  JsonVideo(const td_api::video *video, const Client *client) : video_(video), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("duration", video_->duration_);
    object("width", video_->width_);
    object("height", video_->height_);
    if (!video_->file_name_.empty()) {
      object("file_name", video_->file_name_);
    }
    if (!video_->mime_type_.empty()) {
      object("mime_type", video_->mime_type_);
    }
    client_->json_store_thumbnail(object, video_->thumbnail_.get());
    client_->json_store_file(object, video_->video_.get());
  }

 private:
  const td_api::video *video_;
  const Client *client_;
};

class Client::JsonVideoNote : public Jsonable {
 public:
  JsonVideoNote(const td_api::videoNote *video_note, const Client *client) : video_note_(video_note), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("duration", video_note_->duration_);
    object("length", video_note_->length_);
    client_->json_store_thumbnail(object, video_note_->thumbnail_.get());
    client_->json_store_file(object, video_note_->video_.get());
  }

 private:
  const td_api::videoNote *video_note_;
  const Client *client_;
};

class Client::JsonVoiceNote : public Jsonable {
 public:
  JsonVoiceNote(const td_api::voiceNote *voice_note, const Client *client) : voice_note_(voice_note), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("duration", voice_note_->duration_);
    if (!voice_note_->mime_type_.empty()) {
      object("mime_type", voice_note_->mime_type_);
    }
    client_->json_store_file(object, voice_note_->voice_.get());
  }

 private:
  const td_api::voiceNote *voice_note_;
  const Client *client_;
};

class Client::JsonVenue : public Jsonable {
 public:
  explicit JsonVenue(const td_api::venue *venue) : venue_(venue) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("location", JsonLocation(venue_->location_.get()));
    object("title", venue_->title_);
    object("address", venue_->address_);
    if (venue_->provider_ == "foursquare") {
      if (!venue_->id_.empty()) {
        object("foursquare_id", venue_->id_);
      }
      if (!venue_->type_.empty()) {
        object("foursquare_type", venue_->type_);
      }
    }
    if (venue_->provider_ == "gplaces") {
      if (!venue_->id_.empty()) {
        object("google_place_id", venue_->id_);
      }
      if (!venue_->type_.empty()) {
        object("google_place_type", venue_->type_);
      }
    }
  }

 private:
  const td_api::venue *venue_;
};

class Client::JsonContact : public Jsonable {
 public:
  explicit JsonContact(const td_api::contact *contact) : contact_(contact) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("phone_number", contact_->phone_number_);
    object("first_name", contact_->first_name_);
    if (!contact_->last_name_.empty()) {
      object("last_name", contact_->last_name_);
    }
    if (!contact_->vcard_.empty()) {
      object("vcard", contact_->vcard_);
    }
    if (contact_->user_id_) {
      object("user_id", contact_->user_id_);
    }
  }

 private:
  const td_api::contact *contact_;
};

class Client::JsonDice : public Jsonable {
 public:
  JsonDice(const td::string &emoji, int32 value) : emoji_(emoji), value_(value) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("emoji", emoji_);
    object("value", value_);
  }

 private:
  const td::string &emoji_;
  int32 value_;
};

class Client::JsonGame : public Jsonable {
 public:
  JsonGame(const td_api::game *game, const Client *client) : game_(game), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("title", game_->title_);
    if (!game_->text_->text_.empty()) {
      object("text", game_->text_->text_);
    }
    if (!game_->text_->entities_.empty()) {
      object("text_entities", JsonVectorEntities(game_->text_->entities_, client_));
    }
    object("description", game_->description_);
    CHECK(game_->photo_ != nullptr);
    object("photo", JsonPhoto(game_->photo_.get(), client_));
    if (game_->animation_ != nullptr) {
      object("animation", JsonAnimation(game_->animation_.get(), false, client_));
    }
  }

 private:
  const td_api::game *game_;
  const Client *client_;
};

class Client::JsonInvoice : public Jsonable {
 public:
  explicit JsonInvoice(const td_api::messageInvoice *invoice) : invoice_(invoice) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("title", invoice_->title_);
    object("description", invoice_->description_);
    object("start_parameter", invoice_->start_parameter_);
    object("currency", invoice_->currency_);
    object("total_amount", invoice_->total_amount_);
    // skip photo
    // skip is_test
    // skip need_shipping_address
    // skip receipt_message_id
  }

 private:
  const td_api::messageInvoice *invoice_;
};

class Client::JsonPollOption : public Jsonable {
 public:
  explicit JsonPollOption(const td_api::pollOption *option) : option_(option) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("text", option_->text_);
    object("voter_count", option_->voter_count_);
    // ignore is_chosen
  }

 private:
  const td_api::pollOption *option_;
};

class Client::JsonPoll : public Jsonable {
 public:
  JsonPoll(const td_api::poll *poll, const Client *client) : poll_(poll), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("id", td::to_string(poll_->id_));
    object("question", poll_->question_);
    object("options", td::json_array(poll_->options_, [](auto &option) { return JsonPollOption(option.get()); }));
    object("total_voter_count", poll_->total_voter_count_);
    if (poll_->open_period_ != 0 && poll_->close_date_ != 0) {
      object("open_period", poll_->open_period_);
      object("close_date", poll_->close_date_);
    }
    object("is_closed", td::JsonBool(poll_->is_closed_));
    object("is_anonymous", td::JsonBool(poll_->is_anonymous_));
    switch (poll_->type_->get_id()) {
      case td_api::pollTypeQuiz::ID: {
        object("type", "quiz");
        object("allows_multiple_answers", td::JsonFalse());
        auto quiz = static_cast<const td_api::pollTypeQuiz *>(poll_->type_.get());
        int32 correct_option_id = quiz->correct_option_id_;
        if (correct_option_id != -1) {
          object("correct_option_id", correct_option_id);
        }
        auto *explanation = quiz->explanation_.get();
        if (!explanation->text_.empty()) {
          object("explanation", explanation->text_);
          object("explanation_entities", JsonVectorEntities(explanation->entities_, client_));
        }
        break;
      }
      case td_api::pollTypeRegular::ID:
        object("type", "regular");
        object("allows_multiple_answers",
               td::JsonBool(static_cast<const td_api::pollTypeRegular *>(poll_->type_.get())->allow_multiple_answers_));
        break;
      default:
        UNREACHABLE();
    }
  }

 private:
  const td_api::poll *poll_;
  const Client *client_;
};

class Client::JsonPollAnswer : public Jsonable {
 public:
  JsonPollAnswer(const td_api::updatePollAnswer *poll_answer, const Client *client)
      : poll_answer_(poll_answer), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("poll_id", td::to_string(poll_answer_->poll_id_));
    object("user", JsonUser(poll_answer_->user_id_, client_));
    object("option_ids", td::json_array(poll_answer_->option_ids_, [](int32 option_id) { return option_id; }));
  }

 private:
  const td_api::updatePollAnswer *poll_answer_;
  const Client *client_;
};

class Client::JsonAddress : public Jsonable {
 public:
  explicit JsonAddress(const td_api::address *address) : address_(address) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("country_code", address_->country_code_);
    object("state", address_->state_);
    object("city", address_->city_);
    object("street_line1", address_->street_line1_);
    object("street_line2", address_->street_line2_);
    object("post_code", address_->postal_code_);
  }

 private:
  const td_api::address *address_;
};

class Client::JsonOrderInfo : public Jsonable {
 public:
  explicit JsonOrderInfo(const td_api::orderInfo *order_info) : order_info_(order_info) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    if (!order_info_->name_.empty()) {
      object("name", order_info_->name_);
    }
    if (!order_info_->phone_number_.empty()) {
      object("phone_number", order_info_->phone_number_);
    }
    if (!order_info_->email_address_.empty()) {
      object("email", order_info_->email_address_);
    }
    if (order_info_->shipping_address_ != nullptr) {
      object("shipping_address", JsonAddress(order_info_->shipping_address_.get()));
    }
  }

 private:
  const td_api::orderInfo *order_info_;
};

class Client::JsonSuccessfulPaymentBot : public Jsonable {
 public:
  explicit JsonSuccessfulPaymentBot(const td_api::messagePaymentSuccessfulBot *successful_payment)
      : successful_payment_(successful_payment) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("currency", successful_payment_->currency_);
    object("total_amount", successful_payment_->total_amount_);
    if (!td::check_utf8(successful_payment_->invoice_payload_)) {
      LOG(WARNING) << "Receive non-UTF-8 invoice payload";
      object("invoice_payload", td::JsonRawString(successful_payment_->invoice_payload_));
    } else {
      object("invoice_payload", successful_payment_->invoice_payload_);
    }
    if (!successful_payment_->shipping_option_id_.empty()) {
      object("shipping_option_id", successful_payment_->shipping_option_id_);
    }
    if (successful_payment_->order_info_ != nullptr) {
      object("order_info", JsonOrderInfo(successful_payment_->order_info_.get()));
    }

    object("telegram_payment_charge_id", successful_payment_->telegram_payment_charge_id_);
    object("provider_payment_charge_id", successful_payment_->provider_payment_charge_id_);
  }

 private:
  const td_api::messagePaymentSuccessfulBot *successful_payment_;
};

class Client::JsonEncryptedPassportElement : public Jsonable {
 public:
  JsonEncryptedPassportElement(const td_api::encryptedPassportElement *element, const Client *client)
      : element_(element), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    auto id = element_->type_->get_id();
    object("type", Client::get_passport_element_type(id));
    switch (id) {
      case td_api::passportElementTypePhoneNumber::ID:
        object("phone_number", element_->value_);
        break;
      case td_api::passportElementTypeEmailAddress::ID:
        object("email", element_->value_);
        break;
      case td_api::passportElementTypePersonalDetails::ID:
      case td_api::passportElementTypePassport::ID:
      case td_api::passportElementTypeDriverLicense::ID:
      case td_api::passportElementTypeIdentityCard::ID:
      case td_api::passportElementTypeInternalPassport::ID:
      case td_api::passportElementTypeAddress::ID:
        object("data", td::base64_encode(element_->data_));
        break;
    }
    switch (id) {
      case td_api::passportElementTypeUtilityBill::ID:
      case td_api::passportElementTypeBankStatement::ID:
      case td_api::passportElementTypeRentalAgreement::ID:
      case td_api::passportElementTypePassportRegistration::ID:
      case td_api::passportElementTypeTemporaryRegistration::ID:
        object("files", JsonDatedFiles(element_->files_, client_));
        if (!element_->translation_.empty()) {
          object("translation", JsonDatedFiles(element_->translation_, client_));
        }
        break;
    }
    switch (id) {
      case td_api::passportElementTypePassport::ID:
      case td_api::passportElementTypeDriverLicense::ID:
      case td_api::passportElementTypeIdentityCard::ID:
      case td_api::passportElementTypeInternalPassport::ID:
        CHECK(element_->front_side_ != nullptr);
        object("front_side", JsonDatedFile(element_->front_side_.get(), client_));
        if (element_->reverse_side_ != nullptr) {
          CHECK(id == td_api::passportElementTypeIdentityCard::ID ||
                id == td_api::passportElementTypeDriverLicense::ID);
          object("reverse_side", JsonDatedFile(element_->reverse_side_.get(), client_));
        } else {
          CHECK(id == td_api::passportElementTypePassport::ID || id == td_api::passportElementTypeInternalPassport::ID);
        }
        if (element_->selfie_ != nullptr) {
          object("selfie", JsonDatedFile(element_->selfie_.get(), client_));
        }
        if (!element_->translation_.empty()) {
          object("translation", JsonDatedFiles(element_->translation_, client_));
        }
        break;
    }
    object("hash", td::base64_encode(element_->hash_));
  }

 private:
  const td_api::encryptedPassportElement *element_;
  const Client *client_;
};

class Client::JsonEncryptedCredentials : public Jsonable {
 public:
  explicit JsonEncryptedCredentials(const td_api::encryptedCredentials *credentials) : credentials_(credentials) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("data", td::base64_encode(credentials_->data_));
    object("hash", td::base64_encode(credentials_->hash_));
    object("secret", td::base64_encode(credentials_->secret_));
  }

 private:
  const td_api::encryptedCredentials *credentials_;
};

class Client::JsonPassportData : public Jsonable {
 public:
  JsonPassportData(const td_api::messagePassportDataReceived *passport_data, const Client *client)
      : passport_data_(passport_data), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("data", td::json_array(passport_data_->elements_, [client = client_](auto &element) {
             return JsonEncryptedPassportElement(element.get(), client);
           }));
    object("credentials", JsonEncryptedCredentials(passport_data_->credentials_.get()));
  }

 private:
  const td_api::messagePassportDataReceived *passport_data_;
  const Client *client_;
};

class Client::JsonProximityAlertTriggered : public Jsonable {
 public:
  JsonProximityAlertTriggered(const td_api::messageProximityAlertTriggered *proximity_alert_triggered,
                              const Client *client)
      : proximity_alert_triggered_(proximity_alert_triggered), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("traveler", JsonMessageSender(proximity_alert_triggered_->traveler_.get(), client_));
    object("watcher", JsonMessageSender(proximity_alert_triggered_->watcher_.get(), client_));
    object("distance", proximity_alert_triggered_->distance_);
  }

 private:
  const td_api::messageProximityAlertTriggered *proximity_alert_triggered_;
  const Client *client_;
};

class Client::JsonVoiceChatScheduled : public Jsonable {
 public:
  explicit JsonVoiceChatScheduled(const td_api::messageVoiceChatScheduled *voice_chat_scheduled)
      : voice_chat_scheduled_(voice_chat_scheduled) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("start_date", voice_chat_scheduled_->start_date_);
  }

 private:
  const td_api::messageVoiceChatScheduled *voice_chat_scheduled_;
};

class Client::JsonVoiceChatStarted : public Jsonable {
 public:
  explicit JsonVoiceChatStarted(const td_api::messageVoiceChatStarted *voice_chat_started)
      : voice_chat_started_(voice_chat_started) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
  }

 private:
  const td_api::messageVoiceChatStarted *voice_chat_started_;
};

class Client::JsonVoiceChatEnded : public Jsonable {
 public:
  explicit JsonVoiceChatEnded(const td_api::messageVoiceChatEnded *voice_chat_ended)
      : voice_chat_ended_(voice_chat_ended) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("duration", voice_chat_ended_->duration_);
  }

 private:
  const td_api::messageVoiceChatEnded *voice_chat_ended_;
};

class Client::JsonInviteVoiceChatParticipants : public Jsonable {
 public:
  JsonInviteVoiceChatParticipants(const td_api::messageInviteVoiceChatParticipants *invite_voice_chat_participants,
                                  const Client *client)
      : invite_voice_chat_participants_(invite_voice_chat_participants), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("users", JsonUsers(invite_voice_chat_participants_->user_ids_, client_));
  }

 private:
  const td_api::messageInviteVoiceChatParticipants *invite_voice_chat_participants_;
  const Client *client_;
};

class Client::JsonChatSetTtl : public Jsonable {
 public:
  explicit JsonChatSetTtl(const td_api::messageChatSetTtl *chat_set_ttl) : chat_set_ttl_(chat_set_ttl) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("message_auto_delete_time", chat_set_ttl_->ttl_);
  }

 private:
  const td_api::messageChatSetTtl *chat_set_ttl_;
};

class Client::JsonCallbackGame : public Jsonable {
 public:
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
  }
};

class Client::JsonInlineKeyboardButton : public Jsonable {
 public:
  explicit JsonInlineKeyboardButton(const td_api::inlineKeyboardButton *button) : button_(button) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("text", button_->text_);
    switch (button_->type_->get_id()) {
      case td_api::inlineKeyboardButtonTypeUrl::ID: {
        auto type = static_cast<const td_api::inlineKeyboardButtonTypeUrl *>(button_->type_.get());
        object("url", type->url_);
        break;
      }
      case td_api::inlineKeyboardButtonTypeLoginUrl::ID: {
        auto type = static_cast<const td_api::inlineKeyboardButtonTypeLoginUrl *>(button_->type_.get());
        object("url", type->url_);
        break;
      }
      case td_api::inlineKeyboardButtonTypeCallback::ID:
      case td_api::inlineKeyboardButtonTypeCallbackWithPassword::ID: {
        auto data = get_callback_data(button_->type_);
        if (!td::check_utf8(data)) {
          object("callback_data", "INVALID");
        } else {
          object("callback_data", data);
        }
        break;
      }
      case td_api::inlineKeyboardButtonTypeCallbackGame::ID:
        object("callback_game", JsonCallbackGame());
        break;
      case td_api::inlineKeyboardButtonTypeSwitchInline::ID: {
        auto type = static_cast<const td_api::inlineKeyboardButtonTypeSwitchInline *>(button_->type_.get());
        if (type->in_current_chat_) {
          object("switch_inline_query_current_chat", type->query_);
        } else {
          object("switch_inline_query", type->query_);
        }
        break;
      }
      case td_api::inlineKeyboardButtonTypeBuy::ID:
        object("pay", td::JsonTrue());
        break;
      default:
        UNREACHABLE();
        break;
    }
  }

 private:
  const td_api::inlineKeyboardButton *button_;
};

class Client::JsonInlineKeyboard : public Jsonable {
 public:
  explicit JsonInlineKeyboard(const td_api::replyMarkupInlineKeyboard *inline_keyboard)
      : inline_keyboard_(inline_keyboard) {
  }
  void store(JsonValueScope *scope) const {
    auto array = scope->enter_array();
    for (auto &row : inline_keyboard_->rows_) {
      array << td::json_array(row, [](auto &button) { return JsonInlineKeyboardButton(button.get()); });
    }
  }

 private:
  const td_api::replyMarkupInlineKeyboard *inline_keyboard_;
};

class Client::JsonReplyMarkup : public Jsonable {
 public:
  explicit JsonReplyMarkup(const td_api::ReplyMarkup *reply_markup) : reply_markup_(reply_markup) {
  }
  void store(JsonValueScope *scope) const {
    CHECK(reply_markup_->get_id() == td_api::replyMarkupInlineKeyboard::ID);
    auto object = scope->enter_object();
    object("inline_keyboard",
           JsonInlineKeyboard(static_cast<const td_api::replyMarkupInlineKeyboard *>(reply_markup_)));
  }

 private:
  const td_api::ReplyMarkup *reply_markup_;
};

void Client::JsonMessage::store(JsonValueScope *scope) const {
  CHECK(message_ != nullptr);
  auto object = scope->enter_object();
  object("message_id", as_client_message_id(message_->id));
  if (message_->sender_user_id != 0) {
    object("from", JsonUser(message_->sender_user_id, client_));
  }
  if (!message_->author_signature.empty()) {
    object("author_signature", message_->author_signature);
  }
  if (message_->sender_chat_id != 0) {
    object("sender_chat", JsonChat(message_->sender_chat_id, false, client_));
  }
  object("chat", JsonChat(message_->chat_id, false, client_));
  object("date", message_->date);
  if (message_->edit_date > 0) {
    object("edit_date", message_->edit_date);
  }
  if (message_->initial_send_date > 0) {
    if (message_->initial_sender_user_id != 0) {
      object("forward_from", JsonUser(message_->initial_sender_user_id, client_));
    }
    if (message_->initial_sender_chat_id != 0) {
      object("forward_from_chat", JsonChat(message_->initial_sender_chat_id, false, client_));
    }
    if (message_->initial_chat_id != 0) {
      object("forward_from_chat", JsonChat(message_->initial_chat_id, false, client_));
      if (message_->initial_message_id != 0) {
        object("forward_from_message_id", as_client_message_id(message_->initial_message_id));
      }
    }
    if (!message_->initial_author_signature.empty()) {
      object("forward_signature", message_->initial_author_signature);
    }
    if (!message_->initial_sender_name.empty()) {
      object("forward_sender_name", message_->initial_sender_name);
    }
    object("forward_date", message_->initial_send_date);
  }
  if (message_->reply_to_message_id > 0 && need_reply_ && !message_->is_reply_to_message_deleted) {
    const MessageInfo *reply_to_message = client_->get_message(message_->chat_id, message_->reply_to_message_id);
    if (reply_to_message != nullptr) {
      object("reply_to_message", JsonMessage(reply_to_message, false, "reply in " + source_, client_));
    } else {
      LOG(WARNING) << "Replied to unknown or deleted message " << message_->reply_to_message_id << " in chat "
                   << message_->chat_id << " while storing " << source_ << " " << message_->id;
    }
  }
  if (message_->media_album_id != 0) {
    object("media_group_id", td::to_string(message_->media_album_id));
  }
  switch (message_->content->get_id()) {
    case td_api::messageText::ID: {
      auto message_text = static_cast<const td_api::messageText *>(message_->content.get());
      object("text", message_text->text_->text_);
      if (!message_text->text_->entities_.empty()) {
        object("entities", JsonVectorEntities(message_text->text_->entities_, client_));
      }
      break;
    }
    case td_api::messageAnimation::ID: {
      auto message_animation = static_cast<const td_api::messageAnimation *>(message_->content.get());
      object("animation", JsonAnimation(message_animation->animation_.get(), false, client_));
      object("document", JsonAnimation(message_animation->animation_.get(), true, client_));
      add_caption(object, message_animation->caption_);
      break;
    }
    case td_api::messageAudio::ID: {
      auto message_audio = static_cast<const td_api::messageAudio *>(message_->content.get());
      object("audio", JsonAudio(message_audio->audio_.get(), client_));
      add_caption(object, message_audio->caption_);
      break;
    }
    case td_api::messageDocument::ID: {
      auto message_document = static_cast<const td_api::messageDocument *>(message_->content.get());
      object("document", JsonDocument(message_document->document_.get(), client_));
      add_caption(object, message_document->caption_);
      break;
    }
    case td_api::messagePhoto::ID: {
      auto message_photo = static_cast<const td_api::messagePhoto *>(message_->content.get());
      if (message_photo->photo_ == nullptr) {
        LOG(ERROR) << "Got empty messagePhoto";
        break;
      }
      object("photo", JsonPhoto(message_photo->photo_.get(), client_));
      add_caption(object, message_photo->caption_);
      break;
    }
    case td_api::messageSticker::ID: {
      auto message_sticker = static_cast<const td_api::messageSticker *>(message_->content.get());
      object("sticker", JsonSticker(message_sticker->sticker_.get(), client_));
      break;
    }
    case td_api::messageVideo::ID: {
      auto message_video = static_cast<const td_api::messageVideo *>(message_->content.get());
      object("video", JsonVideo(message_video->video_.get(), client_));
      add_caption(object, message_video->caption_);
      break;
    }
    case td_api::messageVideoNote::ID: {
      auto message_video_note = static_cast<const td_api::messageVideoNote *>(message_->content.get());
      object("video_note", JsonVideoNote(message_video_note->video_note_.get(), client_));
      break;
    }
    case td_api::messageVoiceNote::ID: {
      auto message_voice_note = static_cast<const td_api::messageVoiceNote *>(message_->content.get());
      object("voice", JsonVoiceNote(message_voice_note->voice_note_.get(), client_));
      add_caption(object, message_voice_note->caption_);
      break;
    }
    case td_api::messageContact::ID: {
      auto message_contact = static_cast<const td_api::messageContact *>(message_->content.get());
      object("contact", JsonContact(message_contact->contact_.get()));
      break;
    }
    case td_api::messageDice::ID: {
      auto message_dice = static_cast<const td_api::messageDice *>(message_->content.get());
      object("dice", JsonDice(message_dice->emoji_, message_dice->value_));
      break;
    }
    case td_api::messageGame::ID: {
      auto message_game = static_cast<const td_api::messageGame *>(message_->content.get());
      object("game", JsonGame(message_game->game_.get(), client_));
      break;
    }
    case td_api::messageInvoice::ID: {
      auto message_invoice = static_cast<const td_api::messageInvoice *>(message_->content.get());
      object("invoice", JsonInvoice(message_invoice));
      break;
    }
    case td_api::messageLocation::ID: {
      auto message_location = static_cast<const td_api::messageLocation *>(message_->content.get());
      object("location", JsonLocation(message_location->location_.get(), message_location->expires_in_,
                                      message_location->live_period_, message_location->heading_,
                                      message_location->proximity_alert_radius_));
      break;
    }
    case td_api::messageVenue::ID: {
      auto message_venue = static_cast<const td_api::messageVenue *>(message_->content.get());
      object("location", JsonLocation(message_venue->venue_->location_.get()));
      object("venue", JsonVenue(message_venue->venue_.get()));
      break;
    }
    case td_api::messagePoll::ID: {
      auto message_poll = static_cast<const td_api::messagePoll *>(message_->content.get());
      object("poll", JsonPoll(message_poll->poll_.get(), client_));
      break;
    }
    case td_api::messageChatAddMembers::ID: {
      auto message_add_members = static_cast<const td_api::messageChatAddMembers *>(message_->content.get());
      int32 user_id = client_->choose_added_member_id(message_add_members);
      if (user_id > 0) {
        object("new_chat_participant", JsonUser(user_id, client_));
        object("new_chat_member", JsonUser(user_id, client_));
        object("new_chat_members", JsonUsers(message_add_members->member_user_ids_, client_));
      } else {
        LOG(ERROR) << "Can't choose added member for new_chat_member field";
      }
      break;
    }
    case td_api::messageChatJoinByLink::ID: {
      if (message_->sender_user_id > 0) {
        object("new_chat_participant", JsonUser(message_->sender_user_id, client_));
        object("new_chat_member", JsonUser(message_->sender_user_id, client_));
        object("new_chat_members", JsonUsers({message_->sender_user_id}, client_));
      }
      break;
    }
    case td_api::messageChatDeleteMember::ID: {
      auto message_delete_member = static_cast<const td_api::messageChatDeleteMember *>(message_->content.get());
      int32 user_id = message_delete_member->user_id_;
      object("left_chat_participant", JsonUser(user_id, client_));
      object("left_chat_member", JsonUser(user_id, client_));
      break;
    }
    case td_api::messageChatChangeTitle::ID: {
      auto message_change_title = static_cast<const td_api::messageChatChangeTitle *>(message_->content.get());
      object("new_chat_title", message_change_title->title_);
      break;
    }
    case td_api::messageChatChangePhoto::ID: {
      auto message_change_photo = static_cast<const td_api::messageChatChangePhoto *>(message_->content.get());
      if (message_change_photo->photo_ == nullptr) {
        LOG(ERROR) << "Got empty messageChatChangePhoto";
        break;
      }
      object("new_chat_photo", JsonChatPhoto(message_change_photo->photo_.get(), client_));
      break;
    }
    case td_api::messageChatDeletePhoto::ID:
      object("delete_chat_photo", td::JsonTrue());
      break;
    case td_api::messageBasicGroupChatCreate::ID:
      object("group_chat_created", td::JsonTrue());
      break;
    case td_api::messageSupergroupChatCreate::ID: {
      auto chat = client_->get_chat(message_->chat_id);
      if (chat->type != ChatInfo::Type::Supergroup) {
        LOG(ERROR) << "Receive messageSupergroupChatCreate in the non-supergroup chat " << message_->chat_id;
        break;
      }
      auto supergroup_info = client_->get_supergroup_info(chat->supergroup_id);
      CHECK(supergroup_info != nullptr);
      if (supergroup_info->is_supergroup) {
        object("supergroup_chat_created", td::JsonTrue());
      } else {
        object("channel_chat_created", td::JsonTrue());
      }
      break;
    }
    case td_api::messageChatUpgradeTo::ID: {
      auto message_chat_upgrade_to = static_cast<const td_api::messageChatUpgradeTo *>(message_->content.get());
      auto chat_id = get_supergroup_chat_id(message_chat_upgrade_to->supergroup_id_);
      object("migrate_to_chat_id", td::JsonLong(chat_id));
      break;
    }
    case td_api::messageChatUpgradeFrom::ID: {
      auto message_chat_upgrade_from = static_cast<const td_api::messageChatUpgradeFrom *>(message_->content.get());
      auto chat_id = get_basic_group_chat_id(message_chat_upgrade_from->basic_group_id_);
      object("migrate_from_chat_id", td::JsonLong(chat_id));
      break;
    }
    case td_api::messagePinMessage::ID: {
      auto message_pin_message = static_cast<const td_api::messagePinMessage *>(message_->content.get());
      auto message_id = message_pin_message->message_id_;
      if (message_id > 0) {
        const MessageInfo *pinned_message = client_->get_message(message_->chat_id, message_id);
        if (pinned_message != nullptr) {
          object("pinned_message", JsonMessage(pinned_message, false, "pin in " + source_, client_));
        } else {
          LOG_IF(ERROR, need_reply_) << "Pinned unknown, inaccessible or deleted message " << message_id;
        }
      }
      break;
    }
    case td_api::messageGameScore::ID:
      break;
    case td_api::messagePaymentSuccessful::ID:
      break;
    case td_api::messagePaymentSuccessfulBot::ID: {
      auto message_payment_sent_bot = static_cast<const td_api::messagePaymentSuccessfulBot *>(message_->content.get());
      object("successful_payment", JsonSuccessfulPaymentBot(message_payment_sent_bot));
      break;
    }
    case td_api::messageCall::ID:
      break;
    case td_api::messageScreenshotTaken::ID:
      break;
    case td_api::messageChatSetTtl::ID: {
      auto content = static_cast<const td_api::messageChatSetTtl *>(message_->content.get());
      object("message_auto_delete_timer_changed", JsonChatSetTtl(content));
      break;
    }
    case td_api::messageUnsupported::ID:
      break;
    case td_api::messageContactRegistered::ID:
      break;
    case td_api::messageExpiredPhoto::ID:
      break;
    case td_api::messageExpiredVideo::ID:
      break;
    case td_api::messageCustomServiceAction::ID:
      break;
    case td_api::messageWebsiteConnected::ID: {
      auto chat = client_->get_chat(message_->chat_id);
      if (chat->type != ChatInfo::Type::Private) {
        break;
      }

      auto message_website_connected = static_cast<const td_api::messageWebsiteConnected *>(message_->content.get());
      if (!message_website_connected->domain_name_.empty()) {
        object("connected_website", message_website_connected->domain_name_);
      }
      break;
    }
    case td_api::messagePassportDataSent::ID:
      break;
    case td_api::messagePassportDataReceived::ID: {
      auto message_passport_data_received =
          static_cast<const td_api::messagePassportDataReceived *>(message_->content.get());
      object("passport_data", JsonPassportData(message_passport_data_received, client_));
      break;
    }
    case td_api::messageProximityAlertTriggered::ID: {
      auto content = static_cast<const td_api::messageProximityAlertTriggered *>(message_->content.get());
      object("proximity_alert_triggered", JsonProximityAlertTriggered(content, client_));
      break;
    }
    case td_api::messageVoiceChatScheduled::ID: {
      auto content = static_cast<const td_api::messageVoiceChatScheduled *>(message_->content.get());
      object("voice_chat_scheduled", JsonVoiceChatScheduled(content));
      break;
    }
    case td_api::messageVoiceChatStarted::ID: {
      auto content = static_cast<const td_api::messageVoiceChatStarted *>(message_->content.get());
      object("voice_chat_started", JsonVoiceChatStarted(content));
      break;
    }
    case td_api::messageVoiceChatEnded::ID: {
      auto content = static_cast<const td_api::messageVoiceChatEnded *>(message_->content.get());
      object("voice_chat_ended", JsonVoiceChatEnded(content));
      break;
    }
    case td_api::messageInviteVoiceChatParticipants::ID: {
      auto content = static_cast<const td_api::messageInviteVoiceChatParticipants *>(message_->content.get());
      object("voice_chat_participants_invited", JsonInviteVoiceChatParticipants(content, client_));
      break;
    }
    default:
      UNREACHABLE();
  }
  if (message_->reply_markup != nullptr) {
    object("reply_markup", JsonReplyMarkup(message_->reply_markup.get()));
  }
  if (message_->via_bot_user_id > 0) {
    object("via_bot", JsonUser(message_->via_bot_user_id, client_));
  }
}

class Client::JsonDeletedMessage : public Jsonable {
 public:
  JsonDeletedMessage(int64 chat_id, int64 message_id, const Client *client)
      : chat_id_(chat_id), message_id_(message_id), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("message_id", as_client_message_id(message_id_));
    object("chat", JsonChat(chat_id_, false, client_));
    object("date", 0);
  }

 private:
  int64 chat_id_;
  int64 message_id_;
  const Client *client_;
};

class Client::JsonMessageId : public Jsonable {
 public:
  explicit JsonMessageId(int64 message_id) : message_id_(message_id) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("message_id", as_client_message_id(message_id_));
  }

 private:
  int64 message_id_;
};

class Client::JsonInlineQuery : public Jsonable {
 public:
  JsonInlineQuery(int64 inline_query_id, int32 sender_user_id, const td_api::location *user_location,
                  const td_api::ChatType *chat_type, const td::string &query, const td::string &offset,
                  const Client *client)
      : inline_query_id_(inline_query_id)
      , sender_user_id_(sender_user_id)
      , user_location_(user_location)
      , chat_type_(chat_type)
      , query_(query)
      , offset_(offset)
      , client_(client) {
  }

  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("id", td::to_string(inline_query_id_));
    object("from", JsonUser(sender_user_id_, client_));
    if (user_location_ != nullptr) {
      object("location", JsonLocation(user_location_));
    }
    if (chat_type_ != nullptr) {
      auto chat_type = [&] {
        switch (chat_type_->get_id()) {
          case td_api::chatTypePrivate::ID: {
            auto type = static_cast<const td_api::chatTypePrivate *>(chat_type_);
            if (type->user_id_ == sender_user_id_) {
              return "sender";
            }
            return "private";
          }
          case td_api::chatTypeBasicGroup::ID:
            return "group";
          case td_api::chatTypeSupergroup::ID: {
            auto type = static_cast<const td_api::chatTypeSupergroup *>(chat_type_);
            if (type->is_channel_) {
              return "channel";
            } else {
              return "supergroup";
            }
          }
          case td_api::chatTypeSecret::ID:
            return "";
          default:
            UNREACHABLE();
            return "";
        }
      }();
      if (chat_type[0] != '\0') {
        object("chat_type", chat_type);
      }
    }
    object("query", query_);
    object("offset", offset_);
  }

 private:
  int64 inline_query_id_;
  int32 sender_user_id_;
  const td_api::location *user_location_;
  const td_api::ChatType *chat_type_;
  const td::string &query_;
  const td::string &offset_;
  const Client *client_;
};

class Client::JsonChosenInlineResult : public Jsonable {
 public:
  JsonChosenInlineResult(int32 sender_user_id, const td_api::location *user_location, const td::string &query,
                         const td::string &result_id, const td::string &inline_message_id, const Client *client)
      : sender_user_id_(sender_user_id)
      , user_location_(user_location)
      , query_(query)
      , result_id_(result_id)
      , inline_message_id_(inline_message_id)
      , client_(client) {
  }

  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("from", JsonUser(sender_user_id_, client_));
    if (user_location_ != nullptr) {
      object("location", JsonLocation(user_location_));
    }
    if (!inline_message_id_.empty()) {
      object("inline_message_id", inline_message_id_);
    }
    object("query", query_);
    object("result_id", result_id_);
  }

 private:
  int32 sender_user_id_;
  const td_api::location *user_location_;
  const td::string &query_;
  const td::string &result_id_;
  const td::string &inline_message_id_;
  const Client *client_;
};

class Client::JsonCallbackQuery : public Jsonable {
 public:
  JsonCallbackQuery(int64 callback_query_id, int32 sender_user_id, int64 chat_id, int64 message_id,
                    const MessageInfo *message_info, int64 chat_instance, td_api::CallbackQueryPayload *payload,
                    const Client *client)
      : callback_query_id_(callback_query_id)
      , sender_user_id_(sender_user_id)
      , chat_id_(chat_id)
      , message_id_(message_id)
      , message_info_(message_info)
      , chat_instance_(chat_instance)
      , payload_(payload)
      , client_(client) {
  }

  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("id", td::to_string(callback_query_id_));
    object("from", JsonUser(sender_user_id_, client_));
    if (message_info_ != nullptr) {
      object("message", JsonMessage(message_info_, true, "callback query", client_));
    } else {
      object("message", JsonDeletedMessage(chat_id_, message_id_, client_));
    }
    object("chat_instance", td::to_string(chat_instance_));
    client_->json_store_callback_query_payload(object, payload_);
  }

 private:
  int64 callback_query_id_;
  int32 sender_user_id_;
  int64 chat_id_;
  int64 message_id_;
  const MessageInfo *message_info_;
  int64 chat_instance_;
  td_api::CallbackQueryPayload *payload_;
  const Client *client_;
};

class Client::JsonInlineCallbackQuery : public Jsonable {
 public:
  JsonInlineCallbackQuery(int64 callback_query_id, int32 sender_user_id, const td::string &inline_message_id,
                          int64 chat_instance, td_api::CallbackQueryPayload *payload, const Client *client)
      : callback_query_id_(callback_query_id)
      , sender_user_id_(sender_user_id)
      , inline_message_id_(inline_message_id)
      , chat_instance_(chat_instance)
      , payload_(payload)
      , client_(client) {
  }

  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("id", td::to_string(callback_query_id_));
    object("from", JsonUser(sender_user_id_, client_));
    object("inline_message_id", inline_message_id_);
    object("chat_instance", td::to_string(chat_instance_));
    client_->json_store_callback_query_payload(object, payload_);
  }

 private:
  int64 callback_query_id_;
  int32 sender_user_id_;
  const td::string &inline_message_id_;
  int64 chat_instance_;
  td_api::CallbackQueryPayload *payload_;
  const Client *client_;
};

class Client::JsonShippingQuery : public Jsonable {
 public:
  JsonShippingQuery(const td_api::updateNewShippingQuery *query, const Client *client)
      : query_(query), client_(client) {
  }

  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("id", td::to_string(query_->id_));
    object("from", JsonUser(query_->sender_user_id_, client_));
    if (!td::check_utf8(query_->invoice_payload_)) {
      LOG(WARNING) << "Receive non-UTF-8 invoice payload";
      object("invoice_payload", td::JsonRawString(query_->invoice_payload_));
    } else {
      object("invoice_payload", query_->invoice_payload_);
    }
    object("shipping_address", JsonAddress(query_->shipping_address_.get()));
  }

 private:
  const td_api::updateNewShippingQuery *query_;
  const Client *client_;
};

class Client::JsonPreCheckoutQuery : public Jsonable {
 public:
  JsonPreCheckoutQuery(const td_api::updateNewPreCheckoutQuery *query, const Client *client)
      : query_(query), client_(client) {
  }

  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("id", td::to_string(query_->id_));
    object("from", JsonUser(query_->sender_user_id_, client_));
    object("currency", query_->currency_);
    object("total_amount", query_->total_amount_);
    if (!td::check_utf8(query_->invoice_payload_)) {
      LOG(WARNING) << "Receive non-UTF-8 invoice payload";
      object("invoice_payload", td::JsonRawString(query_->invoice_payload_));
    } else {
      object("invoice_payload", query_->invoice_payload_);
    }
    if (!query_->shipping_option_id_.empty()) {
      object("shipping_option_id", query_->shipping_option_id_);
    }
    if (query_->order_info_ != nullptr) {
      object("order_info", JsonOrderInfo(query_->order_info_.get()));
    }
  }

 private:
  const td_api::updateNewPreCheckoutQuery *query_;
  const Client *client_;
};

class Client::JsonCustomJson : public Jsonable {
 public:
  explicit JsonCustomJson(const td::string &json) : json_(json) {
  }

  void store(JsonValueScope *scope) const {
    *scope << td::JsonRaw(json_);
  }

 private:
  const td::string &json_;
};

class Client::JsonBotCommand : public Jsonable {
 public:
  explicit JsonBotCommand(const td_api::botCommand *command) : command_(command) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("command", command_->command_);
    object("description", command_->description_);
  }

 private:
  const td_api::botCommand *command_;
};

class Client::JsonChatPhotos : public Jsonable {
 public:
  JsonChatPhotos(const td_api::chatPhotos *photos, const Client *client) : photos_(photos), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("total_count", photos_->total_count_);
    object("photos", td::json_array(photos_->photos_,
                                    [client = client_](auto &photo) { return JsonChatPhoto(photo.get(), client); }));
  }

 private:
  const td_api::chatPhotos *photos_;
  const Client *client_;
};

class Client::JsonChatMember : public Jsonable {
 public:
  JsonChatMember(const td_api::chatMember *member, Client::ChatType chat_type, const Client *client)
      : member_(member), chat_type_(chat_type), client_(client) {
  }

  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    CHECK(member_->member_id_ != nullptr);
    switch (member_->member_id_->get_id()) {
      case td_api::messageSenderUser::ID: {
        auto user_id = static_cast<const td_api::messageSenderUser *>(member_->member_id_.get())->user_id_;
        object("user", JsonUser(user_id, client_));
        break;
      }
      case td_api::messageSenderChat::ID:
        break;
      default:
        UNREACHABLE();
    }
    object("status", Client::get_chat_member_status(member_->status_));
    switch (member_->status_->get_id()) {
      case td_api::chatMemberStatusCreator::ID: {
        auto creator = static_cast<const td_api::chatMemberStatusCreator *>(member_->status_.get());
        if (!creator->custom_title_.empty()) {
          object("custom_title", creator->custom_title_);
        }
        object("is_anonymous", td::JsonBool(creator->is_anonymous_));
        // object("is_member", creator->is_member_); only creator itself knows that he is a left creator
        break;
      }
      case td_api::chatMemberStatusAdministrator::ID: {
        auto administrator = static_cast<const td_api::chatMemberStatusAdministrator *>(member_->status_.get());
        object("can_be_edited", td::JsonBool(administrator->can_be_edited_));
        object("can_manage_chat", td::JsonBool(administrator->can_manage_chat_));
        object("can_change_info", td::JsonBool(administrator->can_change_info_));
        if (chat_type_ == Client::ChatType::Channel) {
          object("can_post_messages", td::JsonBool(administrator->can_post_messages_));
          object("can_edit_messages", td::JsonBool(administrator->can_edit_messages_));
        }
        object("can_delete_messages", td::JsonBool(administrator->can_delete_messages_));
        object("can_invite_users", td::JsonBool(administrator->can_invite_users_));
        object("can_restrict_members", td::JsonBool(administrator->can_restrict_members_));
        if (chat_type_ == Client::ChatType::Group || chat_type_ == Client::ChatType::Supergroup) {
          object("can_pin_messages", td::JsonBool(administrator->can_pin_messages_));
        }
        object("can_promote_members", td::JsonBool(administrator->can_promote_members_));
        object("can_manage_voice_chats", td::JsonBool(administrator->can_manage_voice_chats_));
        if (!administrator->custom_title_.empty()) {
          object("custom_title", administrator->custom_title_);
        }
        object("is_anonymous", td::JsonBool(administrator->is_anonymous_));
        break;
      }
      case td_api::chatMemberStatusMember::ID:
        break;
      case td_api::chatMemberStatusRestricted::ID:
        if (chat_type_ == Client::ChatType::Supergroup) {
          auto restricted = static_cast<const td_api::chatMemberStatusRestricted *>(member_->status_.get());
          object("until_date", restricted->restricted_until_date_);
          Client::json_store_permissions(object, restricted->permissions_.get());
          object("is_member", td::JsonBool(restricted->is_member_));
        }
        break;
      case td_api::chatMemberStatusLeft::ID:
        break;
      case td_api::chatMemberStatusBanned::ID: {
        auto banned = static_cast<const td_api::chatMemberStatusBanned *>(member_->status_.get());
        object("until_date", banned->banned_until_date_);
        break;
      }
      default:
        UNREACHABLE();
    }
  }

 private:
  const td_api::chatMember *member_;
  Client::ChatType chat_type_;
  const Client *client_;
};

class Client::JsonChatMembers : public Jsonable {
 public:
  JsonChatMembers(const td::vector<object_ptr<td_api::chatMember>> &members, Client::ChatType chat_type,
                  bool administrators_only, const Client *client)
      : members_(members), chat_type_(chat_type), administrators_only_(administrators_only), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto array = scope->enter_array();
    for (auto &member : members_) {
      CHECK(member != nullptr);
      CHECK(member->member_id_ != nullptr);
      if (member->member_id_->get_id() != td_api::messageSenderUser::ID) {
        continue;
      }
      auto user_id = static_cast<const td_api::messageSenderUser *>(member->member_id_.get())->user_id_;
      auto user_info = client_->get_user_info(user_id);
      bool is_member_bot = user_info != nullptr && user_info->type == UserInfo::Type::Bot;
      if (is_member_bot && user_id != client_->my_id_) {
        continue;
      }
      if (administrators_only_) {
        auto status = Client::get_chat_member_status(member->status_);
        if (status != "creator" && status != "administrator") {
          continue;
        }
      }
      array << JsonChatMember(member.get(), chat_type_, client_);
    }
  }

 private:
  const td::vector<object_ptr<td_api::chatMember>> &members_;
  Client::ChatType chat_type_;
  bool administrators_only_;
  const Client *client_;
};

class Client::JsonChatMemberUpdated : public Jsonable {
 public:
  JsonChatMemberUpdated(const td_api::updateChatMember *update, const Client *client)
      : update_(update), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("chat", JsonChat(update_->chat_id_, false, client_));
    object("from", JsonUser(update_->actor_user_id_, client_));
    object("date", update_->date_);
    auto chat_type = client_->get_chat_type(update_->chat_id_);
    object("old_chat_member", JsonChatMember(update_->old_chat_member_.get(), chat_type, client_));
    object("new_chat_member", JsonChatMember(update_->new_chat_member_.get(), chat_type, client_));
    if (update_->invite_link_ != nullptr) {
      object("invite_link", JsonChatInviteLink(update_->invite_link_.get(), client_));
    }
  }

 private:
  const td_api::updateChatMember *update_;
  const Client *client_;
};

class Client::JsonGameHighScore : public Jsonable {
 public:
  JsonGameHighScore(const td_api::gameHighScore *score, const Client *client) : score_(score), client_(client) {
  }

  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("position", score_->position_);
    object("user", JsonUser(score_->user_id_, client_));
    object("score", score_->score_);
  }

 private:
  const td_api::gameHighScore *score_;
  const Client *client_;
};

class Client::JsonUpdateTypes : public Jsonable {
 public:
  explicit JsonUpdateTypes(td::uint32 update_types) : update_types_(update_types) {
  }
  void store(JsonValueScope *scope) const {
    auto array = scope->enter_array();
    for (int32 i = 0; i < static_cast<int32>(UpdateType::Size); i++) {
      if (((update_types_ >> i) & 1) != 0) {
        auto update_type = static_cast<UpdateType>(i);
        if (update_type != UpdateType::CustomEvent && update_type != UpdateType::CustomQuery) {
          array << get_update_type_name(update_type);
        }
      }
    }
  }

 private:
  td::uint32 update_types_;
};

class Client::JsonWebhookInfo : public Jsonable {
 public:
  explicit JsonWebhookInfo(const Client *client) : client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    td::CSlice url = client_->webhook_url_;
    if (td::check_utf8(url)) {
      object("url", url);
    } else {
      object("url", td::JsonRawString(url));
    }
    object("has_custom_certificate", td::JsonBool(client_->has_webhook_certificate_));
    object("pending_update_count", td::narrow_cast<int32>(client_->get_pending_update_count()));
    if (client_->last_webhook_error_date_ > 0) {
      object("last_error_date", client_->last_webhook_error_date_);
      td::CSlice error_message = client_->last_webhook_error_.message();
      if (td::check_utf8(error_message)) {
        object("last_error_message", error_message);
      } else {
        object("last_error_message", td::JsonRawString(error_message));
      }
    }
    if (client_->webhook_max_connections_ > 0) {
      object("max_connections", client_->webhook_max_connections_);
    }
    if (!url.empty()) {
      object("ip_address", client_->webhook_ip_address_.empty() ? "<unknown>" : client_->webhook_ip_address_);
    }
    if (client_->allowed_update_types_ != DEFAULT_ALLOWED_UPDATE_TYPES) {
      object("allowed_updates", JsonUpdateTypes(client_->allowed_update_types_));
    }
  }

 private:
  const Client *client_;
};

class Client::JsonStickerSet : public Jsonable {
 public:
  JsonStickerSet(const td_api::stickerSet *sticker_set, const Client *client)
      : sticker_set_(sticker_set), client_(client) {
  }
  void store(JsonValueScope *scope) const {
    auto object = scope->enter_object();
    if (sticker_set_->id_ == Client::GREAT_MINDS_SET_ID) {
      object("name", GREAT_MINDS_SET_NAME);
    } else {
      object("name", sticker_set_->name_);
    }
    object("title", sticker_set_->title_);
    if (sticker_set_->thumbnail_ != nullptr) {
      client_->json_store_thumbnail(object, sticker_set_->thumbnail_.get());
    }
    object("is_animated", td::JsonBool(sticker_set_->is_animated_));
    object("contains_masks", td::JsonBool(sticker_set_->is_masks_));
    object("stickers", JsonStickers(sticker_set_->stickers_, client_));
  }

 private:
  const td_api::stickerSet *sticker_set_;
  const Client *client_;
};

class Client::TdOnOkCallback : public TdQueryCallback {
 public:
  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      auto error = move_object_as<td_api::error>(result);
      if (error->code_ != 401 && error->code_ != 406 && error->code_ != 500) {
        LOG(ERROR) << "Query has failed: " << td::oneline(to_string(error));
      }
    }
  }
};

class Client::TdOnAuthorizationCallback : public TdQueryCallback {
 public:
  explicit TdOnAuthorizationCallback(Client *client) : client_(client) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    bool was_ready = client_->authorization_state_->get_id() != td_api::authorizationStateWaitPhoneNumber::ID;
    if (result->get_id() == td_api::error::ID) {
      auto error = move_object_as<td_api::error>(result);
      if (error->code_ == 429 || error->code_ >= 500 || (error->code_ != 401 && was_ready)) {
        // try again
        return client_->on_update_authorization_state();
      }

      LOG(WARNING) << "Logging out due to " << td::oneline(to_string(error));
      client_->log_out(error->message_ == "API_ID_INVALID");
    } else if (was_ready) {
      client_->on_update_authorization_state();
    }
  }

 private:
  Client *client_;
};

class Client::TdOnInitCallback : public TdQueryCallback {
 public:
  explicit TdOnInitCallback(Client *client) : client_(client) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      LOG(WARNING) << "Failed to initialize due to " << td::oneline(to_string(result));
      client_->close();
    }
  }

 private:
  Client *client_;
};

class Client::TdOnGetUserProfilePhotosCallback : public TdQueryCallback {
 public:
  TdOnGetUserProfilePhotosCallback(const Client *client, PromisedQueryPtr query)
      : client_(client), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::chatPhotos::ID);
    auto profile_photos = move_object_as<td_api::chatPhotos>(result);
    answer_query(JsonChatPhotos(profile_photos.get(), client_), std::move(query_));
  }

 private:
  const Client *client_;
  PromisedQueryPtr query_;
};

class Client::TdOnSendMessageCallback : public TdQueryCallback {
 public:
  TdOnSendMessageCallback(Client *client, PromisedQueryPtr query) : client_(client), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::message::ID);
    auto query_id = client_->get_send_message_query_id(std::move(query_), false);
    client_->on_sent_message(move_object_as<td_api::message>(result), query_id);
  }

 private:
  Client *client_;
  PromisedQueryPtr query_;
};

class Client::TdOnSendMessageAlbumCallback : public TdQueryCallback {
 public:
  TdOnSendMessageAlbumCallback(Client *client, PromisedQueryPtr query) : client_(client), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::messages::ID);
    auto messages = move_object_as<td_api::messages>(result);
    auto query_id = client_->get_send_message_query_id(std::move(query_), true);
    for (auto &message : messages->messages_) {
      client_->on_sent_message(std::move(message), query_id);
    }
  }

 private:
  Client *client_;
  PromisedQueryPtr query_;
};

class Client::TdOnDeleteFailedToSendMessageCallback : public TdQueryCallback {
 public:
  TdOnDeleteFailedToSendMessageCallback(Client *client, int64 chat_id, int64 message_id)
      : client_(client)
      , chat_id_(chat_id)
      , message_id_(message_id)
      , old_chat_description_(client->get_chat_description(chat_id)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      auto error = move_object_as<td_api::error>(result);
      if (error->code_ != 401) {
        LOG(ERROR) << "Can't delete failed to send message " << message_id_ << " because of "
                   << td::oneline(to_string(error)) << " in " << client_->get_chat_description(chat_id_)
                   << ". Old chat description: " << old_chat_description_;
      }
      return;
    }

    CHECK(result->get_id() == td_api::ok::ID);
    if (client_->get_message(chat_id_, message_id_) != nullptr) {
      LOG(ERROR) << "Have cache for message " << message_id_ << " in the chat " << chat_id_;
      client_->delete_message(chat_id_, message_id_, false);
    }
  }

 private:
  Client *client_;
  int64 chat_id_;
  int64 message_id_;
  td::string old_chat_description_;
};

class Client::TdOnEditMessageCallback : public TdQueryCallback {
 public:
  TdOnEditMessageCallback(const Client *client, PromisedQueryPtr query) : client_(client), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::message::ID);
    auto message = move_object_as<td_api::message>(result);
    int64 chat_id = message->chat_id_;
    int64 message_id = message->id_;

    auto message_info = client_->get_message(chat_id, message_id);
    if (message_info == nullptr) {
      return fail_query_with_error(std::move(query_), 400, "message not found");
    }
    message_info->is_content_changed = false;
    answer_query(JsonMessage(message_info, false, "edited message", client_), std::move(query_));
  }

 private:
  const Client *client_;
  PromisedQueryPtr query_;
};

class Client::TdOnEditInlineMessageCallback : public TdQueryCallback {
 public:
  explicit TdOnEditInlineMessageCallback(PromisedQueryPtr query) : query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::ok::ID);
    answer_query(td::JsonTrue(), std::move(query_));
  }

 private:
  PromisedQueryPtr query_;
};

class Client::TdOnStopPollCallback : public TdQueryCallback {
 public:
  TdOnStopPollCallback(const Client *client, int64 chat_id, int64 message_id, PromisedQueryPtr query)
      : client_(client), chat_id_(chat_id), message_id_(message_id), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::ok::ID);
    auto message_info = client_->get_message(chat_id_, message_id_);
    if (message_info == nullptr) {
      return fail_query_with_error(std::move(query_), 400, "message not found");
    }
    if (message_info->content->get_id() != td_api::messagePoll::ID) {
      LOG(ERROR) << "Poll not found in " << message_id_ << " in " << chat_id_;
      return fail_query_with_error(std::move(query_), 400, "message poll not found");
    }
    auto message_poll = static_cast<const td_api::messagePoll *>(message_info->content.get());
    answer_query(JsonPoll(message_poll->poll_.get(), client_), std::move(query_));
  }

 private:
  const Client *client_;
  int64 chat_id_;
  int64 message_id_;
  PromisedQueryPtr query_;
};

class Client::TdOnOkQueryCallback : public TdQueryCallback {
 public:
  explicit TdOnOkQueryCallback(PromisedQueryPtr query) : query_(std::move(query)) {
    CHECK(query_ != nullptr);
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::ok::ID);
    answer_query(td::JsonTrue(), std::move(query_));
  }

 private:
  PromisedQueryPtr query_;
};

template <class OnSuccess>
class Client::TdOnCheckUserCallback : public TdQueryCallback {
 public:
  TdOnCheckUserCallback(const Client *client, PromisedQueryPtr query, OnSuccess on_success)
      : client_(client), query_(std::move(query)), on_success_(std::move(on_success)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result), "user not found");
    }

    CHECK(result->get_id() == td_api::user::ID);
    auto user = move_object_as<td_api::user>(result);
    auto user_info = client_->get_user_info(user->id_);
    CHECK(user_info != nullptr);  // it must have already been got through updates

    return client_->check_user_read_access(user_info, std::move(query_), std::move(on_success_));
  }

 private:
  const Client *client_;
  PromisedQueryPtr query_;
  OnSuccess on_success_;
};

template <class OnSuccess>
class Client::TdOnCheckUserNoFailCallback : public TdQueryCallback {
 public:
  TdOnCheckUserNoFailCallback(PromisedQueryPtr query, OnSuccess on_success)
      : query_(std::move(query)), on_success_(std::move(on_success)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    on_success_(std::move(query_));
  }

 private:
  PromisedQueryPtr query_;
  OnSuccess on_success_;
};

template <class OnSuccess>
class Client::TdOnCheckChatCallback : public TdQueryCallback {
 public:
  TdOnCheckChatCallback(const Client *client, bool only_supergroup, AccessRights access_rights, PromisedQueryPtr query,
                        OnSuccess on_success)
      : client_(client)
      , only_supergroup_(only_supergroup)
      , access_rights_(access_rights)
      , query_(std::move(query))
      , on_success_(std::move(on_success)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result), "chat not found");
    }

    CHECK(result->get_id() == td_api::chat::ID);
    auto chat = move_object_as<td_api::chat>(result);
    auto chat_info = client_->get_chat(chat->id_);
    CHECK(chat_info != nullptr);  // it must have already been got through updates
    CHECK(chat_info->title == chat->title_);
    if (only_supergroup_ && chat_info->type != ChatInfo::Type::Supergroup) {
      return fail_query(400, "Bad Request: chat not found", std::move(query_));
    }

    return client_->check_chat_access(chat->id_, access_rights_, chat_info, std::move(query_), std::move(on_success_));
  }

 private:
  const Client *client_;
  bool only_supergroup_;
  AccessRights access_rights_;
  PromisedQueryPtr query_;
  OnSuccess on_success_;
};

template <class OnSuccess>
class Client::TdOnSearchStickerSetCallback : public TdQueryCallback {
 public:
  TdOnSearchStickerSetCallback(PromisedQueryPtr query, OnSuccess on_success)
      : query_(std::move(query)), on_success_(std::move(on_success)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result), "sticker set not found");
    }

    CHECK(result->get_id() == td_api::stickerSet::ID);
    auto sticker_set = move_object_as<td_api::stickerSet>(result);
    on_success_(sticker_set->id_, std::move(query_));
  }

 private:
  PromisedQueryPtr query_;
  OnSuccess on_success_;
};

class Client::TdOnResolveBotUsernameCallback : public TdQueryCallback {
 public:
  TdOnResolveBotUsernameCallback(Client *client, td::string username)
      : client_(client), username_(std::move(username)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return client_->on_resolve_bot_username(username_, 0);
    }

    CHECK(result->get_id() == td_api::chat::ID);
    auto chat = move_object_as<td_api::chat>(result);
    auto chat_info = client_->get_chat(chat->id_);
    CHECK(chat_info != nullptr);  // it must have already been got through updates
    if (chat_info->type != ChatInfo::Type::Private) {
      return client_->on_resolve_bot_username(username_, 0);
    }
    auto user_info = client_->get_user_info(chat_info->user_id);
    CHECK(user_info != nullptr);
    if (user_info->type != UserInfo::Type::Bot) {
      return client_->on_resolve_bot_username(username_, 0);
    }

    client_->on_resolve_bot_username(username_, chat_info->user_id);
  }

 private:
  Client *client_;
  td::string username_;
};

template <class OnSuccess>
class Client::TdOnCheckMessageCallback : public TdQueryCallback {
 public:
  TdOnCheckMessageCallback(Client *client, int64 chat_id, bool allow_empty, Slice message_type, PromisedQueryPtr query,
                           OnSuccess on_success)
      : client_(client)
      , chat_id_(chat_id)
      , allow_empty_(allow_empty)
      , message_type_(message_type)
      , query_(std::move(query))
      , on_success_(std::move(on_success)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      auto error = move_object_as<td_api::error>(result);
      if (error->code_ == 429) {
        LOG(WARNING) << "Failed to get " << message_type_;
      }
      if (allow_empty_) {
        return on_success_(chat_id_, 0, std::move(query_));
      }
      return fail_query_with_error(std::move(query_), std::move(error), PSLICE() << message_type_ << " not found");
    }

    CHECK(result->get_id() == td_api::message::ID);
    auto full_message_id = client_->add_message(move_object_as<td_api::message>(result));
    CHECK(full_message_id.chat_id == chat_id_);
    on_success_(full_message_id.chat_id, full_message_id.message_id, std::move(query_));
  }

 private:
  Client *client_;
  int64 chat_id_;
  bool allow_empty_;
  Slice message_type_;
  PromisedQueryPtr query_;
  OnSuccess on_success_;
};

template <class OnSuccess>
class Client::TdOnCheckRemoteFileIdCallback : public TdQueryCallback {
 public:
  TdOnCheckRemoteFileIdCallback(PromisedQueryPtr query, OnSuccess on_success)
      : query_(std::move(query)), on_success_(std::move(on_success)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result), "invalid file_id");
    }

    CHECK(result->get_id() == td_api::file::ID);
    on_success_(move_object_as<td_api::file>(result), std::move(query_));
  }

 private:
  PromisedQueryPtr query_;
  OnSuccess on_success_;
};

template <class OnSuccess>
class Client::TdOnGetChatMemberCallback : public TdQueryCallback {
 public:
  TdOnGetChatMemberCallback(PromisedQueryPtr query, OnSuccess on_success)
      : query_(std::move(query)), on_success_(std::move(on_success)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result), "user not found");
    }

    CHECK(result->get_id() == td_api::chatMember::ID);
    on_success_(move_object_as<td_api::chatMember>(result), std::move(query_));
  }

 private:
  PromisedQueryPtr query_;
  OnSuccess on_success_;
};

class Client::TdOnDownloadFileCallback : public TdQueryCallback {
 public:
  TdOnDownloadFileCallback(Client *client, int32 file_id) : client_(client), file_id_(file_id) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      auto error = move_object_as<td_api::error>(result);
      return client_->on_file_download(file_id_, Status::Error(error->code_, error->message_));
    }
    CHECK(result->get_id() == td_api::file::ID);
    if (client_->is_file_being_downloaded(file_id_)) {  // if download is yet not finished
      client_->download_started_file_ids_.insert(file_id_);
    }
    client_->on_update_file(move_object_as<td_api::file>(result));
  }

 private:
  Client *client_;
  int32 file_id_;
};

class Client::TdOnCancelDownloadFileCallback : public TdQueryCallback {
 public:
  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      LOG(ERROR) << "Failed to cancel download file";
      return;
    }
    CHECK(result->get_id() == td_api::ok::ID);
  }
};

class Client::TdOnGetReplyMessageCallback : public TdQueryCallback {
 public:
  TdOnGetReplyMessageCallback(Client *client, int64 chat_id) : client_(client), chat_id_(chat_id) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return client_->on_get_reply_message(chat_id_, nullptr);
    }

    CHECK(result->get_id() == td_api::message::ID);
    client_->on_get_reply_message(chat_id_, move_object_as<td_api::message>(result));
  }

 private:
  Client *client_;
  int64 chat_id_;
};

class Client::TdOnGetEditedMessageCallback : public TdQueryCallback {
 public:
  explicit TdOnGetEditedMessageCallback(Client *client) : client_(client) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      auto error = move_object_as<td_api::error>(result);
      if (error->code_ == 429) {
        LOG(WARNING) << "Failed to get edited message";
      }
      return client_->on_get_edited_message(nullptr);
    }

    CHECK(result->get_id() == td_api::message::ID);
    client_->on_get_edited_message(move_object_as<td_api::message>(result));
  }

 private:
  Client *client_;
};

class Client::TdOnGetCallbackQueryMessageCallback : public TdQueryCallback {
 public:
  TdOnGetCallbackQueryMessageCallback(Client *client, int32 user_id, int state)
      : client_(client), user_id_(user_id), state_(state) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      auto error = move_object_as<td_api::error>(result);
      if (error->code_ == 429) {
        LOG(WARNING) << "Failed to get callback query message";
      }
      return client_->on_get_callback_query_message(nullptr, user_id_, state_);
    }

    CHECK(result->get_id() == td_api::message::ID);
    client_->on_get_callback_query_message(move_object_as<td_api::message>(result), user_id_, state_);
  }

 private:
  Client *client_;
  int32 user_id_;
  int state_;
};

class Client::TdOnGetStickerSetCallback : public TdQueryCallback {
 public:
  TdOnGetStickerSetCallback(Client *client, int64 set_id, int32 new_callback_query_user_id, int64 new_message_chat_id)
      : client_(client)
      , set_id_(set_id)
      , new_callback_query_user_id_(new_callback_query_user_id)
      , new_message_chat_id_(new_message_chat_id) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      auto error = move_object_as<td_api::error>(result);
      if (error->message_ != "STICKERSET_INVALID" && error->code_ != 401 && error->code_ != 500) {
        LOG(ERROR) << "Failed to get sticker set " << set_id_ << " from callback query by user "
                   << new_callback_query_user_id_ << "/new message in chat " << new_message_chat_id_ << ": "
                   << td::oneline(to_string(error));
      }
      return client_->on_get_sticker_set(set_id_, new_callback_query_user_id_, new_message_chat_id_, nullptr);
    }

    CHECK(result->get_id() == td_api::stickerSet::ID);
    client_->on_get_sticker_set(set_id_, new_callback_query_user_id_, new_message_chat_id_,
                                move_object_as<td_api::stickerSet>(result));
  }

 private:
  Client *client_;
  int64 set_id_;
  int32 new_callback_query_user_id_;
  int64 new_message_chat_id_;
};

class Client::TdOnGetChatStickerSetCallback : public TdQueryCallback {
 public:
  TdOnGetChatStickerSetCallback(Client *client, int64 chat_id, int64 pinned_message_id, PromisedQueryPtr query)
      : client_(client), chat_id_(chat_id), pinned_message_id_(pinned_message_id), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      auto chat_info = client_->get_chat(chat_id_);
      CHECK(chat_info != nullptr);
      CHECK(chat_info->type == ChatInfo::Type::Supergroup);
      client_->set_supergroup_sticker_set_id(chat_info->supergroup_id, 0);
    } else {
      CHECK(result->get_id() == td_api::stickerSet::ID);
      auto sticker_set = move_object_as<td_api::stickerSet>(result);
      client_->on_get_sticker_set_name(sticker_set->id_, sticker_set->name_);
    }

    answer_query(JsonChat(chat_id_, true, client_, pinned_message_id_), std::move(query_));
  }

 private:
  Client *client_;
  int64 chat_id_;
  int64 pinned_message_id_;
  PromisedQueryPtr query_;
};

class Client::TdOnGetChatPinnedMessageCallback : public TdQueryCallback {
 public:
  TdOnGetChatPinnedMessageCallback(Client *client, int64 chat_id, PromisedQueryPtr query)
      : client_(client), chat_id_(chat_id), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    int64 pinned_message_id = 0;
    if (result->get_id() == td_api::error::ID) {
      auto error = move_object_as<td_api::error>(result);
      if (error->code_ == 429) {
        return fail_query_with_error(std::move(query_), std::move(error));
      } else if (error->code_ != 404 && error->message_ != "CHANNEL_PRIVATE") {
        LOG(ERROR) << "Failed to get chat pinned message: " << to_string(error);
      }
    } else {
      CHECK(result->get_id() == td_api::message::ID);
      auto full_message_id = client_->add_message(move_object_as<td_api::message>(result));
      pinned_message_id = full_message_id.message_id;
      CHECK(full_message_id.chat_id == chat_id_);
      CHECK(pinned_message_id > 0);
    }

    auto chat_info = client_->get_chat(chat_id_);
    CHECK(chat_info != nullptr);
    if (chat_info->type == ChatInfo::Type::Supergroup) {
      auto supergroup_info = client_->get_supergroup_info(chat_info->supergroup_id);
      CHECK(supergroup_info != nullptr);

      auto sticker_set_id = supergroup_info->sticker_set_id;
      if (sticker_set_id != 0 && client_->get_sticker_set_name(sticker_set_id).empty()) {
        return client_->send_request(
            make_object<td_api::getStickerSet>(sticker_set_id),
            std::make_unique<TdOnGetChatStickerSetCallback>(client_, chat_id_, pinned_message_id, std::move(query_)));
      }
    }

    answer_query(JsonChat(chat_id_, true, client_, pinned_message_id), std::move(query_));
  }

 private:
  Client *client_;
  int64 chat_id_;
  PromisedQueryPtr query_;
};

class Client::TdOnGetChatPinnedMessageToUnpinCallback : public TdQueryCallback {
 public:
  TdOnGetChatPinnedMessageToUnpinCallback(Client *client, int64 chat_id, PromisedQueryPtr query)
      : client_(client), chat_id_(chat_id), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    int64 pinned_message_id = 0;
    if (result->get_id() == td_api::error::ID) {
      auto error = move_object_as<td_api::error>(result);
      if (error->code_ == 429) {
        return fail_query_with_error(std::move(query_), std::move(error));
      } else {
        return fail_query_with_error(std::move(query_), make_object<td_api::error>(400, "Message to unpin not found"));
      }
    }

    CHECK(result->get_id() == td_api::message::ID);
    auto full_message_id = client_->add_message(move_object_as<td_api::message>(result));
    pinned_message_id = full_message_id.message_id;
    CHECK(full_message_id.chat_id == chat_id_);
    CHECK(pinned_message_id > 0);

    client_->send_request(make_object<td_api::unpinChatMessage>(chat_id_, pinned_message_id),
                          std::make_unique<TdOnOkQueryCallback>(std::move(query_)));
  }

 private:
  Client *client_;
  int64 chat_id_;
  PromisedQueryPtr query_;
};

class Client::TdOnGetMyCommandsCallback : public TdQueryCallback {
 public:
  explicit TdOnGetMyCommandsCallback(PromisedQueryPtr query) : query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::botCommands::ID);
    auto bot_commands = move_object_as<td_api::botCommands>(result);
    answer_query(td::json_array(bot_commands->commands_, [](auto &command) { return JsonBotCommand(command.get()); }),
                 std::move(query_));
  }

 private:
  PromisedQueryPtr query_;
};

class Client::TdOnGetChatFullInfoCallback : public TdQueryCallback {
 public:
  TdOnGetChatFullInfoCallback(Client *client, int64 chat_id, PromisedQueryPtr query)
      : client_(client), chat_id_(chat_id), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    // we don't need the result, everything is already received through updates

    client_->send_request(make_object<td_api::getChatPinnedMessage>(chat_id_),
                          std::make_unique<TdOnGetChatPinnedMessageCallback>(client_, chat_id_, std::move(query_)));
  }

 private:
  Client *client_;
  int64 chat_id_;
  PromisedQueryPtr query_;
};

class Client::TdOnGetGroupMembersCallback : public TdQueryCallback {
 public:
  TdOnGetGroupMembersCallback(const Client *client, bool administrators_only, PromisedQueryPtr query)
      : client_(client), administrators_only_(administrators_only), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::basicGroupFullInfo::ID);
    auto group_full_info = move_object_as<td_api::basicGroupFullInfo>(result);
    answer_query(JsonChatMembers(group_full_info->members_, Client::ChatType::Group, administrators_only_, client_),
                 std::move(query_));
  }

 private:
  const Client *client_;
  bool administrators_only_;
  PromisedQueryPtr query_;
};

class Client::TdOnGetSupergroupMembersCallback : public TdQueryCallback {
 public:
  TdOnGetSupergroupMembersCallback(const Client *client, Client::ChatType chat_type, PromisedQueryPtr query)
      : client_(client), chat_type_(chat_type), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::chatMembers::ID);
    auto chat_members = move_object_as<td_api::chatMembers>(result);
    answer_query(JsonChatMembers(chat_members->members_, chat_type_, false, client_), std::move(query_));
  }

 private:
  const Client *client_;
  Client::ChatType chat_type_;
  PromisedQueryPtr query_;
};

class Client::TdOnGetSupergroupMembersCountCallback : public TdQueryCallback {
 public:
  explicit TdOnGetSupergroupMembersCountCallback(PromisedQueryPtr query) : query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::supergroupFullInfo::ID);
    auto supergroup_full_info = move_object_as<td_api::supergroupFullInfo>(result);
    if (supergroup_full_info->member_count_ == 0) {
      return fail_query(400, "Bad Request: need administrator rights", std::move(query_));
    }
    return answer_query(td::VirtuallyJsonableInt(supergroup_full_info->member_count_), std::move(query_));
  }

 private:
  PromisedQueryPtr query_;
};

class Client::TdOnReplacePrimaryChatInviteLinkCallback : public TdQueryCallback {
 public:
  explicit TdOnReplacePrimaryChatInviteLinkCallback(PromisedQueryPtr query) : query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::chatInviteLink::ID);
    auto invite_link = move_object_as<td_api::chatInviteLink>(result);
    return answer_query(td::VirtuallyJsonableString(invite_link->invite_link_), std::move(query_));
  }

 private:
  PromisedQueryPtr query_;
};

class Client::TdOnGetChatInviteLinkCallback : public TdQueryCallback {
 public:
  TdOnGetChatInviteLinkCallback(const Client *client, PromisedQueryPtr query)
      : client_(client), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    if (result->get_id() == td_api::chatInviteLink::ID) {
      auto invite_link = move_object_as<td_api::chatInviteLink>(result);
      return answer_query(JsonChatInviteLink(invite_link.get(), client_), std::move(query_));
    } else {
      CHECK(result->get_id() == td_api::chatInviteLinks::ID);
      auto invite_links = move_object_as<td_api::chatInviteLinks>(result);
      CHECK(!invite_links->invite_links_.empty());
      return answer_query(JsonChatInviteLink(invite_links->invite_links_[0].get(), client_), std::move(query_));
    }
  }

 private:
  const Client *client_;
  PromisedQueryPtr query_;
};

class Client::TdOnGetGameHighScoresCallback : public TdQueryCallback {
 public:
  TdOnGetGameHighScoresCallback(const Client *client, PromisedQueryPtr query)
      : client_(client), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::gameHighScores::ID);
    auto game_high_scores = move_object_as<td_api::gameHighScores>(result);
    answer_query(td::json_array(game_high_scores->scores_,
                                [client = client_](auto &score) { return JsonGameHighScore(score.get(), client); }),
                 std::move(query_));
  }

 private:
  const Client *client_;
  PromisedQueryPtr query_;
};

class Client::TdOnReturnFileCallback : public TdQueryCallback {
 public:
  TdOnReturnFileCallback(const Client *client, PromisedQueryPtr query) : client_(client), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::file::ID);
    auto file = move_object_as<td_api::file>(result);
    answer_query(JsonFile(file.get(), client_), std::move(query_));
  }

 private:
  const Client *client_;
  PromisedQueryPtr query_;
};

class Client::TdOnReturnStickerSetCallback : public TdQueryCallback {
 public:
  TdOnReturnStickerSetCallback(Client *client, bool return_sticker_set, PromisedQueryPtr query)
      : client_(client), return_sticker_set_(return_sticker_set), query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::stickerSet::ID);
    auto sticker_set = move_object_as<td_api::stickerSet>(result);
    client_->on_get_sticker_set_name(sticker_set->id_, sticker_set->name_);
    if (return_sticker_set_) {
      answer_query(JsonStickerSet(sticker_set.get(), client_), std::move(query_));
    } else {
      answer_query(td::JsonTrue(), std::move(query_));
    }
  }

 private:
  Client *client_;
  bool return_sticker_set_;
  PromisedQueryPtr query_;
};

class Client::TdOnSendCustomRequestCallback : public TdQueryCallback {
 public:
  explicit TdOnSendCustomRequestCallback(PromisedQueryPtr query) : query_(std::move(query)) {
  }

  void on_result(object_ptr<td_api::Object> result) override {
    if (result->get_id() == td_api::error::ID) {
      return fail_query_with_error(std::move(query_), move_object_as<td_api::error>(result));
    }

    CHECK(result->get_id() == td_api::customRequestResult::ID);
    auto res = move_object_as<td_api::customRequestResult>(result);
    answer_query(JsonCustomJson(res->result_), std::move(query_));
  }

 private:
  PromisedQueryPtr query_;
};

void Client::close() {
  need_close_ = true;
  if (td_client_.empty()) {
    set_timeout_in(0);
  } else if (!closing_) {
    do_send_request(make_object<td_api::close>(), std::make_unique<TdOnOkCallback>());
  }
}

void Client::log_out(bool is_api_id_invalid) {
  is_api_id_invalid_ |= is_api_id_invalid;
  if (!td_client_.empty() && !logging_out_ && !closing_) {
    do_send_request(make_object<td_api::logOut>(), std::make_unique<TdOnOkCallback>());
  }
}

std::size_t Client::get_pending_update_count() const {
  return parameters_->shared_data_->tqueue_->get_size(tqueue_id_);
}

ServerBotInfo Client::get_bot_info() const {
  ServerBotInfo res;
  res.id_ = bot_token_id_;
  res.token_ = bot_token_;
  auto user_info = get_user_info(my_id_);
  if (user_info != nullptr) {
    res.username_ = user_info->username;
  } else if (!was_authorized_) {
    res.username_ = "<unauthorized>";
  } else {
    res.username_ = "<unknown>";
  }
  res.webhook_ = webhook_url_;
  res.has_webhook_certificate_ = has_webhook_certificate_;
  auto &tqueue = parameters_->shared_data_->tqueue_;
  res.head_update_id_ = tqueue->get_head(tqueue_id_).value();
  res.tail_update_id_ = tqueue->get_tail(tqueue_id_).value();
  res.webhook_max_connections_ = webhook_max_connections_;
  res.pending_update_count_ = tqueue->get_size(tqueue_id_);
  res.start_time_ = start_time_;
  return res;
}

void Client::start_up() {
  start_time_ = td::Time::now();
  next_bot_updates_warning_time_ = start_time_ + 600;
  schedule_next_delete_messages_lru();
  webhook_set_time_ = start_time_;
  next_allowed_set_webhook_time_ = start_time_;
  next_set_webhook_logging_time_ = start_time_;
  next_webhook_is_not_modified_warning_time_ = start_time_;
  previous_get_updates_start_time_ = start_time_ - 100;
  next_get_updates_conflict_time_ = start_time_ - 100;

  sticker_set_names_[GREAT_MINDS_SET_ID] = GREAT_MINDS_SET_NAME.str();

  auto colon_pos = bot_token_.find_first_of(':');
  if (colon_pos == td::string::npos) {
    LOG(WARNING) << "Wrong bot token " << bot_token_;
    logging_out_ = true;
    return finish_closing();
  }
  bot_token_id_ = bot_token_.substr(0, colon_pos);

  auto base64_bot_token = bot_token_.substr(colon_pos + 1);
  if (td::base64url_decode(base64_bot_token).is_error() || base64_bot_token.size() < 24) {
    LOG(WARNING) << "Wrong bot token " << bot_token_;
    logging_out_ = true;
    return finish_closing();
  }

  bot_token_with_dc_ = bot_token_ + (is_test_dc_ ? ":T" : "");

  auto context = std::make_shared<td::ActorContext>();
  set_context(context);
  set_tag(bot_token_id_);

  auto suff = bot_token_with_dc_ + TD_DIR_SLASH;
  if (!parameters_->allow_colon_in_filenames_) {
    for (auto &c : suff) {
      if (c == ':') {
        c = '~';
      }
    }
  }
  dir_ = parameters_->working_directory_ + suff;

  class TdCallback : public td::TdCallback {
   public:
    explicit TdCallback(td::ActorId<Client> client) : client_(std::move(client)) {
    }
    void on_result(td::uint64 id, object_ptr<td_api::Object> result) override {
      send_closure_later(client_, &Client::on_result, id, std::move(result));
    }
    void on_error(td::uint64 id, object_ptr<td_api::error> result) override {
      send_closure_later(client_, &Client::on_result, id, std::move(result));
    }

   private:
    td::ActorId<Client> client_;
  };
  td::ClientActor::Options options;
  options.net_query_stats = parameters_->net_query_stats_;
  td_client_ = td::create_actor<td::ClientActor>("TdClientActor", td::make_unique<TdCallback>(actor_id(this)),
                                                 std::move(options));
}

void Client::send(PromisedQueryPtr query) {
  if (!query->is_internal()) {
    query->set_stat_actor(stat_actor_);
  }
  cmd_queue_.emplace(std::move(query));
  loop();
}

void Client::raw_event(const td::Event::Raw &event) {
  long_poll_wakeup(true);
}

void Client::loop() {
  if (logging_out_ || closing_ || was_authorized_) {
    while (!cmd_queue_.empty()) {
      auto query = std::move(cmd_queue_.front());
      cmd_queue_.pop();
      on_cmd(std::move(query));
    }
  }
}

void Client::on_get_reply_message(int64 chat_id, object_ptr<td_api::message> reply_to_message) {
  auto &queue = new_message_queues_[chat_id];
  CHECK(queue.has_active_request_);
  queue.has_active_request_ = false;

  CHECK(!queue.queue_.empty());
  object_ptr<td_api::message> &message = queue.queue_.front().message;
  CHECK(chat_id == message->chat_id_);
  int64 &reply_to_message_id = get_reply_to_message_id(message);
  CHECK(reply_to_message_id > 0);
  if (reply_to_message == nullptr) {
    LOG(INFO) << "Can't find message " << reply_to_message_id << " in chat " << chat_id
              << ". It is already deleted or inaccessible because of the chosen privacy mode";
    reply_to_message_id = 0;
  } else {
    CHECK(chat_id == reply_to_message->chat_id_);
    CHECK(reply_to_message_id == reply_to_message->id_);
    LOG(INFO) << "Receive reply to message " << reply_to_message_id << " in chat " << chat_id;
    add_message(std::move(reply_to_message));
  }

  process_new_message_queue(chat_id);
}

void Client::on_get_edited_message(object_ptr<td_api::message> edited_message) {
  if (edited_message == nullptr) {
    LOG(INFO) << "Can't find just edited message. It is already deleted or inaccessible because of chosen privacy mode";
  } else {
    add_new_message(std::move(edited_message), true);
  }
}

void Client::on_get_callback_query_message(object_ptr<td_api::message> message, int32 user_id, int state) {
  CHECK(user_id != 0);
  auto &queue = new_callback_query_queues_[user_id];
  CHECK(queue.has_active_request_);
  queue.has_active_request_ = false;

  CHECK(!queue.queue_.empty());
  int64 chat_id = queue.queue_.front()->chat_id_;
  int64 message_id = queue.queue_.front()->message_id_;
  if (message == nullptr) {
    if (state == 0) {
      LOG(INFO) << "Can't find callback query message " << message_id << " in chat " << chat_id
                << ". It may be already deleted";
    } else {
      CHECK(state == 1);
      auto message_info = get_message_editable(chat_id, message_id);
      if (message_info == nullptr) {
        LOG(INFO) << "Can't find callback query message " << message_id << " in chat " << chat_id
                  << ". It may be already deleted, while searcing for its reply to message";
        process_new_callback_query_queue(user_id, state);
        return;
      }
      LOG(INFO) << "Can't find callback query reply to message " << message_info->reply_to_message_id << " in chat "
                << chat_id << ". It may be already deleted";
      message_info->is_reply_to_message_deleted = true;
    }
  } else {
    LOG(INFO) << "Receive callback query " << (state == 1 ? "reply to " : "") << "message " << message_id << " in chat "
              << chat_id;
    add_message(std::move(message));
  }
  process_new_callback_query_queue(user_id, state + 1);
}

void Client::on_get_sticker_set(int64 set_id, int32 new_callback_query_user_id, int64 new_message_chat_id,
                                object_ptr<td_api::stickerSet> sticker_set) {
  if (new_callback_query_user_id != 0) {
    auto &queue = new_callback_query_queues_[new_callback_query_user_id];
    CHECK(queue.has_active_request_);
    queue.has_active_request_ = false;

    CHECK(!queue.queue_.empty());
  }
  if (new_message_chat_id != 0) {
    auto &queue = new_message_queues_[new_message_chat_id];
    CHECK(queue.has_active_request_);
    queue.has_active_request_ = false;

    CHECK(!queue.queue_.empty());
  }

  CHECK(set_id != 0);
  if (set_id != GREAT_MINDS_SET_ID) {
    td::string &set_name = sticker_set_names_[set_id];
    if (sticker_set != nullptr) {
      set_name = std::move(sticker_set->name_);
    }
  }

  if (new_callback_query_user_id != 0) {
    process_new_callback_query_queue(new_callback_query_user_id, 2);
  }
  if (new_message_chat_id != 0) {
    process_new_message_queue(new_message_chat_id);
  }
}

void Client::on_get_sticker_set_name(int64 set_id, const td::string &name) {
  CHECK(set_id != 0);
  if (set_id != GREAT_MINDS_SET_ID) {
    sticker_set_names_[set_id] = name;
  }
}

template <class OnSuccess>
void Client::check_user_read_access(const UserInfo *user_info, PromisedQueryPtr query, OnSuccess on_success) {
  CHECK(user_info != nullptr);
  if (!user_info->have_access) {
    return fail_query(400, "Bad Request: have no access to the user", std::move(query));
  }
  on_success(std::move(query));
}

template <class OnSuccess>
void Client::check_user(int32 user_id, PromisedQueryPtr query, OnSuccess on_success) {
  const UserInfo *user_info = get_user_info(user_id);
  if (user_info != nullptr) {
    return check_user_read_access(user_info, std::move(query), std::move(on_success));
  }
  send_request(make_object<td_api::getUser>(user_id),
               std::make_unique<TdOnCheckUserCallback<OnSuccess>>(this, std::move(query), std::move(on_success)));
}

template <class OnSuccess>
void Client::check_user_no_fail(int32 user_id, PromisedQueryPtr query, OnSuccess on_success) {
  const UserInfo *user_info = get_user_info(user_id);
  if (user_info != nullptr) {
    on_success(std::move(query));
    return;
  }
  send_request(make_object<td_api::getUser>(user_id),
               std::make_unique<TdOnCheckUserNoFailCallback<OnSuccess>>(std::move(query), std::move(on_success)));
}

template <class OnSuccess>
void Client::check_chat_access(int64 chat_id, AccessRights access_rights, const ChatInfo *chat_info,
                               PromisedQueryPtr query, OnSuccess on_success) const {
  CHECK(chat_info != nullptr);
  bool need_write_access = access_rights == AccessRights::Write;
  bool need_edit_access = access_rights == AccessRights::Edit || need_write_access;
  bool need_read_access = true;
  switch (chat_info->type) {
    case ChatInfo::Type::Private: {
      auto user_info = get_user_info(chat_info->user_id);
      CHECK(user_info != nullptr);
      if (user_info->type == UserInfo::Type::Deleted && need_edit_access) {
        return fail_query(403, "Forbidden: user is deactivated", std::move(query));
      }
      if (user_info->type == UserInfo::Type::Unknown) {
        return fail_query(400, "Bad Request: private chat not found", std::move(query));
      }
      break;
    }
    case ChatInfo::Type::Group: {
      if (access_rights == AccessRights::ReadMembers) {  // member list is inaccessible in deactivated groups
        need_write_access = true;
        need_edit_access = true;
      }
      auto group_info = get_group_info(chat_info->group_id);
      CHECK(group_info != nullptr);
      if (!group_info->is_active && need_write_access) {
        if (group_info->upgraded_to_supergroup_id != 0) {
          std::unordered_map<td::string, std::unique_ptr<td::VirtuallyJsonable>> parameters;
          auto updagraded_to_chat_id = get_supergroup_chat_id(group_info->upgraded_to_supergroup_id);
          parameters.emplace("migrate_to_chat_id", std::make_unique<td::VirtuallyJsonableLong>(updagraded_to_chat_id));
          return fail_query(400, "Bad Request: group chat was upgraded to a supergroup chat", std::move(query),
                            std::move(parameters));
        } else {
          return fail_query(403, "Forbidden: the group chat was deleted", std::move(query));
        }
      }
      if (group_info->is_active && group_info->kicked && need_edit_access) {
        return fail_query(403, "Forbidden: bot was kicked from the group chat", std::move(query));
      }
      if (group_info->is_active && group_info->left && need_edit_access) {
        return fail_query(403, "Forbidden: bot is not a member of the group chat", std::move(query));
      }
      break;
    }
    case ChatInfo::Type::Supergroup: {
      auto supergroup_info = get_supergroup_info(chat_info->supergroup_id);
      CHECK(supergroup_info != nullptr);
      bool is_public = !supergroup_info->username.empty() || supergroup_info->has_location;
      if (supergroup_info->status->get_id() == td_api::chatMemberStatusBanned::ID) {
        if (supergroup_info->is_supergroup) {
          return fail_query(403, "Forbidden: bot was kicked from the supergroup chat", std::move(query));
        } else {
          return fail_query(403, "Forbidden: bot was kicked from the channel chat", std::move(query));
        }
      }
      bool need_more_access_rights = is_public ? need_edit_access : need_read_access;
      if (supergroup_info->status->get_id() == td_api::chatMemberStatusLeft::ID && need_more_access_rights) {
        if (supergroup_info->is_supergroup) {
          return fail_query(403, "Forbidden: bot is not a member of the supergroup chat", std::move(query));
        } else {
          return fail_query(403, "Forbidden: bot is not a member of the channel chat", std::move(query));
        }
      }
      break;
    }
    case ChatInfo::Type::Unknown:
    default:
      UNREACHABLE();
  }
  on_success(chat_id, std::move(query));
}

template <class OnSuccess>
void Client::check_chat(Slice chat_id_str, AccessRights access_rights, PromisedQueryPtr query, OnSuccess on_success) {
  if (chat_id_str.empty()) {
    return fail_query(400, "Bad Request: chat_id is empty", std::move(query));
  }

  if (chat_id_str[0] == '@') {
    return send_request(make_object<td_api::searchPublicChat>(chat_id_str.str()),
                        std::make_unique<TdOnCheckChatCallback<OnSuccess>>(this, true, access_rights, std::move(query),
                                                                           std::move(on_success)));
  }

  auto chat_id = td::to_integer<int64>(chat_id_str);
  auto chat_info = get_chat(chat_id);
  if (chat_info != nullptr) {
    return check_chat_access(chat_id, access_rights, chat_info, std::move(query), std::move(on_success));
  }
  send_request(make_object<td_api::getChat>(chat_id),
               std::make_unique<TdOnCheckChatCallback<OnSuccess>>(this, false, access_rights, std::move(query),
                                                                  std::move(on_success)));
}

template <class OnSuccess>
void Client::check_bot_command_scope(BotCommandScope &&scope, PromisedQueryPtr query, OnSuccess on_success) {
  CHECK(scope.scope_ != nullptr);
  if (scope.chat_id_.empty()) {
    on_success(std::move(scope.scope_), std::move(query));
    return;
  }
  check_chat(scope.chat_id_, AccessRights::ReadMembers, std::move(query),
             [this, user_id = scope.user_id_, scope_id = scope.scope_->get_id(), on_success = std::move(on_success)](
                 int64 chat_id, PromisedQueryPtr query) mutable {
               switch (scope_id) {
                 case td_api::botCommandScopeChat::ID:
                   on_success(make_object<td_api::botCommandScopeChat>(chat_id), std::move(query));
                   break;
                 case td_api::botCommandScopeChatAdministrators::ID:
                   on_success(make_object<td_api::botCommandScopeChatAdministrators>(chat_id), std::move(query));
                   break;
                 case td_api::botCommandScopeChatMember::ID:
                   check_user_no_fail(
                       user_id, std::move(query),
                       [chat_id, user_id, on_success = std::move(on_success)](PromisedQueryPtr query) mutable {
                         on_success(make_object<td_api::botCommandScopeChatMember>(chat_id, user_id), std::move(query));
                       });
                   break;
                 default:
                   UNREACHABLE();
               }
             });
}

template <class OnSuccess>
void Client::check_remote_file_id(td::string file_id, PromisedQueryPtr query, OnSuccess on_success) {
  if (file_id.empty()) {
    return fail_query(400, "Bad Request: file_id not specified", std::move(query));
  }

  send_request(make_object<td_api::getRemoteFile>(std::move(file_id), nullptr),
               std::make_unique<TdOnCheckRemoteFileIdCallback<OnSuccess>>(std::move(query), std::move(on_success)));
}

bool Client::is_chat_member(const object_ptr<td_api::ChatMemberStatus> &status) {
  switch (status->get_id()) {
    case td_api::chatMemberStatusBanned::ID:
    case td_api::chatMemberStatusLeft::ID:
      return false;
    case td_api::chatMemberStatusRestricted::ID:
      return static_cast<const td_api::chatMemberStatusRestricted *>(status.get())->is_member_;
    default:
      // ignore Creator.is_member_
      return true;
  }
}

bool Client::have_message_access(int64 chat_id) const {
  auto chat_info = get_chat(chat_id);
  CHECK(chat_info != nullptr);
  switch (chat_info->type) {
    case ChatInfo::Type::Private:
    case ChatInfo::Type::Group:
      return true;
    case ChatInfo::Type::Supergroup: {
      auto supergroup_info = get_supergroup_info(chat_info->supergroup_id);
      CHECK(supergroup_info != nullptr);
      return is_chat_member(supergroup_info->status);
    }
    case ChatInfo::Type::Unknown:
    default:
      UNREACHABLE();
      return false;
  }
}

template <class OnSuccess>
void Client::check_message(Slice chat_id_str, int64 message_id, bool allow_empty, AccessRights access_rights,
                           Slice message_type, PromisedQueryPtr query, OnSuccess on_success) {
  check_chat(chat_id_str, access_rights, std::move(query),
             [this, message_id, allow_empty, message_type, on_success = std::move(on_success)](
                 int64 chat_id, PromisedQueryPtr query) mutable {
               if ((message_id <= 0 && !allow_empty) || !have_message_access(chat_id)) {
                 return fail_query_with_error(std::move(query), 400, "MESSAGE_NOT_FOUND",
                                              PSLICE() << message_type << " not found");
               }

               if (message_id <= 0) {
                 CHECK(allow_empty);
                 return on_success(chat_id, 0, std::move(query));
               }

               send_request(make_object<td_api::getMessage>(chat_id, message_id),
                            std::make_unique<TdOnCheckMessageCallback<OnSuccess>>(
                                this, chat_id, allow_empty, message_type, std::move(query), std::move(on_success)));
             });
}

template <class OnSuccess>
void Client::resolve_sticker_set(const td::string &sticker_set_name, PromisedQueryPtr query, OnSuccess on_success) {
  if (sticker_set_name.empty()) {
    return fail_query(400, "Bad Request: sticker_set_name is empty", std::move(query));
  }

  send_request(make_object<td_api::searchStickerSet>(sticker_set_name),
               std::make_unique<TdOnSearchStickerSetCallback<OnSuccess>>(std::move(query), std::move(on_success)));
}

void Client::fix_reply_markup_bot_user_ids(object_ptr<td_api::ReplyMarkup> &reply_markup) const {
  if (reply_markup == nullptr || reply_markup->get_id() != td_api::replyMarkupInlineKeyboard::ID) {
    return;
  }
  auto inline_keyboard = static_cast<td_api::replyMarkupInlineKeyboard *>(reply_markup.get());
  for (auto &row : inline_keyboard->rows_) {
    for (auto &button : row) {
      CHECK(button != nullptr);
      CHECK(button->type_ != nullptr);
      if (button->type_->get_id() != td_api::inlineKeyboardButtonTypeLoginUrl::ID) {
        continue;
      }
      auto login_url_button = static_cast<td_api::inlineKeyboardButtonTypeLoginUrl *>(button->type_.get());
      if (login_url_button->id_ % 1000 != 0) {
        continue;
      }
      auto it = temp_to_real_bot_user_id_.find(std::abs(login_url_button->id_));
      CHECK(it != temp_to_real_bot_user_id_.end());
      auto bot_user_id = it->second;
      CHECK(bot_user_id != 0);
      if (login_url_button->id_ < 0) {
        login_url_button->id_ = -bot_user_id;
      } else {
        login_url_button->id_ = bot_user_id;
      }
    }
  }
}

void Client::fix_inline_query_results_bot_user_ids(
    td::vector<object_ptr<td_api::InputInlineQueryResult>> &results) const {
  for (auto &result : results) {
    td_api::downcast_call(
        *result, [this](auto &result_type) { this->fix_reply_markup_bot_user_ids(result_type.reply_markup_); });
  }
}

void Client::resolve_bot_usernames(PromisedQueryPtr query, td::Promise<PromisedQueryPtr> on_success) {
  CHECK(!unresolved_bot_usernames_.empty());
  auto query_id = current_bot_resolve_query_id_++;
  auto &pending_query = pending_bot_resolve_queries_[query_id];
  pending_query.pending_resolve_count = unresolved_bot_usernames_.size();
  pending_query.query = std::move(query);
  pending_query.on_success = std::move(on_success);
  for (auto &username : unresolved_bot_usernames_) {
    auto &query_ids = awaiting_bot_resolve_queries_[username];
    query_ids.push_back(query_id);
    if (query_ids.size() == 1) {
      send_request(make_object<td_api::searchPublicChat>(username),
                   std::make_unique<TdOnResolveBotUsernameCallback>(this, username));
    }
  }
  unresolved_bot_usernames_.clear();
}

template <class OnSuccess>
void Client::resolve_reply_markup_bot_usernames(object_ptr<td_api::ReplyMarkup> reply_markup, PromisedQueryPtr query,
                                                OnSuccess on_success) {
  if (!unresolved_bot_usernames_.empty()) {
    CHECK(reply_markup != nullptr);
    CHECK(reply_markup->get_id() == td_api::replyMarkupInlineKeyboard::ID);
    return resolve_bot_usernames(
        std::move(query),
        td::PromiseCreator::lambda([this, reply_markup = std::move(reply_markup),
                                    on_success = std::move(on_success)](td::Result<PromisedQueryPtr> result) mutable {
          if (result.is_ok()) {
            fix_reply_markup_bot_user_ids(reply_markup);
            on_success(std::move(reply_markup), result.move_as_ok());
          }
        }));
  }
  on_success(std::move(reply_markup), std::move(query));
}

template <class OnSuccess>
void Client::resolve_inline_query_results_bot_usernames(td::vector<object_ptr<td_api::InputInlineQueryResult>> results,
                                                        PromisedQueryPtr query, OnSuccess on_success) {
  if (!unresolved_bot_usernames_.empty()) {
    return resolve_bot_usernames(
        std::move(query),
        td::PromiseCreator::lambda([this, results = std::move(results),
                                    on_success = std::move(on_success)](td::Result<PromisedQueryPtr> result) mutable {
          if (result.is_ok()) {
            fix_inline_query_results_bot_user_ids(results);
            on_success(std::move(results), result.move_as_ok());
          }
        }));
  }
  on_success(std::move(results), std::move(query));
}

void Client::on_resolve_bot_username(const td::string &username, int32 user_id) {
  auto query_ids_it = awaiting_bot_resolve_queries_.find(username);
  CHECK(query_ids_it != awaiting_bot_resolve_queries_.end());
  CHECK(!query_ids_it->second.empty());
  auto query_ids = std::move(query_ids_it->second);
  awaiting_bot_resolve_queries_.erase(query_ids_it);

  if (user_id == 0) {
    bot_user_ids_.erase(username);
  } else {
    auto &temp_bot_user_id = bot_user_ids_[username];
    temp_to_real_bot_user_id_[temp_bot_user_id] = user_id;
    temp_bot_user_id = user_id;
  }

  for (auto query_id : query_ids) {
    auto it = pending_bot_resolve_queries_.find(query_id);
    if (it == pending_bot_resolve_queries_.end()) {
      // the query has already failed
      continue;
    }
    CHECK(it->second.pending_resolve_count > 0);
    it->second.pending_resolve_count--;
    if (it->second.pending_resolve_count == 0 || user_id == 0) {
      if (user_id == 0) {
        fail_query(400, PSTRING() << "Bad Request: bot \"" << username << "\" not found", std::move(it->second.query));
      } else {
        it->second.on_success.set_value(std::move(it->second.query));
      }
      pending_bot_resolve_queries_.erase(it);
    }
  }
}

template <class OnSuccess>
void Client::get_chat_member(int64 chat_id, int32 user_id, PromisedQueryPtr query, OnSuccess on_success) {
  check_user_no_fail(
      user_id, std::move(query),
      [this, chat_id, user_id, on_success = std::move(on_success)](PromisedQueryPtr query) mutable {
        send_request(
            make_object<td_api::getChatMember>(chat_id, td_api::make_object<td_api::messageSenderUser>(user_id)),
            std::make_unique<TdOnGetChatMemberCallback<OnSuccess>>(std::move(query), std::move(on_success)));
      });
}

void Client::send_request(object_ptr<td_api::Function> &&f, std::unique_ptr<TdQueryCallback> handler) {
  if (logging_out_) {
    return handler->on_result(
        make_object<td_api::error>(LOGGING_OUT_ERROR_CODE, get_logging_out_error_description().str()));
  }
  if (closing_) {
    return handler->on_result(make_object<td_api::error>(CLOSING_ERROR_CODE, CLOSING_ERROR_DESCRIPTION.str()));
  }

  do_send_request(std::move(f), std::move(handler));
}

void Client::do_send_request(object_ptr<td_api::Function> &&f, std::unique_ptr<TdQueryCallback> handler) {
  CHECK(!td_client_.empty());
  auto id = handlers_.create(std::move(handler));
  send_closure(td_client_, &td::ClientActor::request, id, std::move(f));
}

td_api::object_ptr<td_api::Object> Client::execute(object_ptr<td_api::Function> &&f) {
  return td::ClientActor::execute(std::move(f));
}

void Client::on_update_file(object_ptr<td_api::file> file) {
  auto file_id = file->id_;
  if (!is_file_being_downloaded(file_id)) {
    return;
  }
  if (!parameters_->local_mode_ && file->local_->downloaded_size_ > MAX_DOWNLOAD_FILE_SIZE) {
    if (file->local_->is_downloading_active_) {
      send_request(make_object<td_api::cancelDownloadFile>(file_id, false),
                   std::make_unique<TdOnCancelDownloadFileCallback>());
    }
    return on_file_download(file_id, Status::Error(400, "Bad Request: file is too big"));
  }
  if (file->local_->is_downloading_completed_) {
    return on_file_download(file_id, std::move(file));
  }
  if (!file->local_->is_downloading_active_ && download_started_file_ids_.count(file_id)) {
    // also includes all 5xx and 429 errors
    auto error = Status::Error(400, "Bad Request: wrong file_id or the file is temporarily unavailable");
    if (logging_out_) {
      error = Status::Error(LOGGING_OUT_ERROR_CODE, get_logging_out_error_description());
    }
    if (closing_) {
      error = Status::Error(CLOSING_ERROR_CODE, CLOSING_ERROR_DESCRIPTION);
    }
    return on_file_download(file_id, std::move(error));
  }
}

void Client::on_update_authorization_state() {
  CHECK(authorization_state_ != nullptr);
  switch (authorization_state_->get_id()) {
    case td_api::authorizationStateWaitTdlibParameters::ID: {
      send_request(
          make_object<td_api::setOption>("ignore_inline_thumbnails", make_object<td_api::optionValueBoolean>(true)),
          std::make_unique<TdOnOkCallback>());
      send_request(make_object<td_api::setOption>("reuse_uploaded_photos_by_hash",
                                                  make_object<td_api::optionValueBoolean>(true)),
                   std::make_unique<TdOnOkCallback>());
      send_request(make_object<td_api::setOption>("disable_persistent_network_statistics",
                                                  make_object<td_api::optionValueBoolean>(true)),
                   std::make_unique<TdOnOkCallback>());
      send_request(make_object<td_api::setOption>("disable_time_adjustment_protection",
                                                  make_object<td_api::optionValueBoolean>(true)),
                   std::make_unique<TdOnOkCallback>());

      auto parameters = make_object<td_api::tdlibParameters>();

      parameters->use_test_dc_ = is_test_dc_;
      parameters->database_directory_ = dir_;
      //parameters->use_file_database_ = false;
      //parameters->use_chat_info_database_ = false;
      //parameters->use_secret_chats_ = false;
      parameters->use_message_database_ = USE_MESSAGE_DATABASE;
      parameters->api_id_ = parameters_->api_id_;
      parameters->api_hash_ = parameters_->api_hash_;
      parameters->system_language_code_ = "en";
      parameters->device_model_ = "server";
      parameters->application_version_ = parameters_->version_;
      parameters->enable_storage_optimizer_ = true;
      parameters->ignore_file_names_ = true;

      return send_request(make_object<td_api::setTdlibParameters>(std::move(parameters)),
                          std::make_unique<TdOnInitCallback>(this));
    }
    case td_api::authorizationStateWaitEncryptionKey::ID:
      return send_request(make_object<td_api::checkDatabaseEncryptionKey>(), std::make_unique<TdOnInitCallback>(this));
    case td_api::authorizationStateWaitPhoneNumber::ID:
      send_request(make_object<td_api::setOption>("online", make_object<td_api::optionValueBoolean>(true)),
                   std::make_unique<TdOnOkCallback>());
      return send_request(make_object<td_api::checkAuthenticationBotToken>(bot_token_),
                          std::make_unique<TdOnAuthorizationCallback>(this));
    case td_api::authorizationStateReady::ID: {
      auto user_info = get_user_info(my_id_);
      if (my_id_ <= 0 || user_info == nullptr) {
        return send_request(make_object<td_api::getMe>(), std::make_unique<TdOnAuthorizationCallback>(this));
      }

      if (!was_authorized_) {
        LOG(WARNING) << "Logged in as @" << user_info->username;
        was_authorized_ = true;
        update_shared_unix_time_difference();
        if (!pending_updates_.empty()) {
          LOG(INFO) << "Process " << pending_updates_.size() << " pending updates";
          for (auto &update : pending_updates_) {
            on_update(std::move(update));
          }
          td::reset_to_empty(pending_updates_);
        }
      }
      return loop();
    }
    case td_api::authorizationStateLoggingOut::ID:
      if (!logging_out_) {
        LOG(WARNING) << "Logging out";
        logging_out_ = true;
      }
      break;
    case td_api::authorizationStateClosing::ID:
      if (!closing_) {
        LOG(WARNING) << "Closing";
        closing_ = true;
      }
      break;
    case td_api::authorizationStateClosed::ID:
      return on_closed();
    default:
      return log_out(false);  // just in case
  }
}

bool Client::allow_update_before_authorization(const td_api::Object *update) const {
  auto update_id = update->get_id();
  if (update_id == td_api::updateAuthorizationState::ID) {
    return true;
  }
  if (update_id == td_api::updateOption::ID) {
    const auto &name = static_cast<const td_api::updateOption *>(update)->name_;
    return name == "my_id" || name == "unix_time";
  }
  if (update_id == td_api::updateUser::ID) {
    return static_cast<const td_api::updateUser *>(update)->user_->id_ == my_id_;
  }
  return false;
}

void Client::update_shared_unix_time_difference() {
  CHECK(was_authorized_);
  LOG_IF(ERROR, local_unix_time_difference_ == 0) << "Unix time difference was not updated";
  auto data = parameters_->shared_data_.get();
  if (local_unix_time_difference_ > data->unix_time_difference_) {
    data->unix_time_difference_ = local_unix_time_difference_;
  }
}

void Client::on_update(object_ptr<td_api::Object> result) {
  if (!was_authorized_ && !allow_update_before_authorization(result.get())) {
    pending_updates_.push_back(std::move(result));
    return;
  }
  switch (result->get_id()) {
    case td_api::updateAuthorizationState::ID: {
      auto update = move_object_as<td_api::updateAuthorizationState>(result);
      authorization_state_ = std::move(update->authorization_state_);
      on_update_authorization_state();
      break;
    }
    case td_api::updateNewMessage::ID: {
      auto update = move_object_as<td_api::updateNewMessage>(result);
      add_new_message(std::move(update->message_), false);
      break;
    }
    case td_api::updateMessageSendSucceeded::ID: {
      auto update = move_object_as<td_api::updateMessageSendSucceeded>(result);
      on_message_send_succeeded(std::move(update->message_), update->old_message_id_);
      break;
    }
    case td_api::updateMessageSendFailed::ID: {
      auto update = move_object_as<td_api::updateMessageSendFailed>(result);
      on_message_send_failed(update->message_->chat_id_, update->old_message_id_, update->message_->id_,
                             Status::Error(update->error_code_, update->error_message_));
      break;
    }
    case td_api::updateMessageContent::ID: {
      auto update = move_object_as<td_api::updateMessageContent>(result);
      update_message_content(update->chat_id_, update->message_id_, std::move(update->new_content_));
      break;
    }
    case td_api::updateMessageEdited::ID: {
      auto update = move_object_as<td_api::updateMessageEdited>(result);
      auto chat_id = update->chat_id_;
      auto message_id = update->message_id_;
      on_update_message_edited(chat_id, message_id, update->edit_date_, std::move(update->reply_markup_));
      send_request(make_object<td_api::getMessage>(chat_id, message_id),
                   std::make_unique<TdOnGetEditedMessageCallback>(this));
      break;
    }
    case td_api::updateDeleteMessages::ID: {
      auto update = move_object_as<td_api::updateDeleteMessages>(result);
      for (auto message_id : update->message_ids_) {
        delete_message(update->chat_id_, message_id, update->from_cache_);
      }
      break;
    }
    case td_api::updateFile::ID: {
      auto update = move_object_as<td_api::updateFile>(result);
      on_update_file(std::move(update->file_));
      break;
    }
    case td_api::updateFileGenerationStart::ID: {
      auto update = move_object_as<td_api::updateFileGenerationStart>(result);
      auto generation_id = update->generation_id_;
      send_request(
          make_object<td_api::finishFileGeneration>(generation_id, make_object<td_api::error>(400, "Wrong file_id")),
          std::make_unique<TdOnOkCallback>());
      break;
    }
    case td_api::updateNewChat::ID: {
      auto update = move_object_as<td_api::updateNewChat>(result);
      auto chat = std::move(update->chat_);
      auto chat_info = add_chat(chat->id_);
      bool need_warning = false;
      switch (chat->type_->get_id()) {
        case td_api::chatTypePrivate::ID: {
          auto type = move_object_as<td_api::chatTypePrivate>(chat->type_);
          chat_info->type = ChatInfo::Type::Private;
          auto user_id = type->user_id_;
          chat_info->user_id = user_id;
          need_warning = get_user_info(user_id) == nullptr;
          break;
        }
        case td_api::chatTypeBasicGroup::ID: {
          auto type = move_object_as<td_api::chatTypeBasicGroup>(chat->type_);
          chat_info->type = ChatInfo::Type::Group;
          auto group_id = type->basic_group_id_;
          chat_info->group_id = group_id;
          need_warning = get_group_info(group_id) == nullptr;
          break;
        }
        case td_api::chatTypeSupergroup::ID: {
          auto type = move_object_as<td_api::chatTypeSupergroup>(chat->type_);
          chat_info->type = ChatInfo::Type::Supergroup;
          auto supergroup_id = type->supergroup_id_;
          chat_info->supergroup_id = supergroup_id;
          need_warning = get_supergroup_info(supergroup_id) == nullptr;
          break;
        }
        case td_api::chatTypeSecret::ID:
          // unsupported
          break;
        default:
          UNREACHABLE();
      }
      if (need_warning) {
        LOG(ERROR) << "Received updateNewChat about chat " << chat->id_ << ", but hadn't received corresponding info";
      }

      chat_info->title = std::move(chat->title_);
      chat_info->photo = std::move(chat->photo_);
      chat_info->permissions = std::move(chat->permissions_);
      chat_info->message_auto_delete_time = chat->message_ttl_setting_;
      break;
    }
    case td_api::updateChatTitle::ID: {
      auto update = move_object_as<td_api::updateChatTitle>(result);
      auto chat_info = add_chat(update->chat_id_);
      CHECK(chat_info->type != ChatInfo::Type::Unknown);
      chat_info->title = std::move(update->title_);
      break;
    }
    case td_api::updateChatPhoto::ID: {
      auto update = move_object_as<td_api::updateChatPhoto>(result);
      auto chat_info = add_chat(update->chat_id_);
      CHECK(chat_info->type != ChatInfo::Type::Unknown);
      chat_info->photo = std::move(update->photo_);
      break;
    }
    case td_api::updateChatPermissions::ID: {
      auto update = move_object_as<td_api::updateChatPermissions>(result);
      auto chat_info = add_chat(update->chat_id_);
      CHECK(chat_info->type != ChatInfo::Type::Unknown);
      chat_info->permissions = std::move(update->permissions_);
      break;
    }
    case td_api::updateChatMessageTtlSetting::ID: {
      auto update = move_object_as<td_api::updateChatMessageTtlSetting>(result);
      auto chat_info = add_chat(update->chat_id_);
      CHECK(chat_info->type != ChatInfo::Type::Unknown);
      chat_info->message_auto_delete_time = update->message_ttl_setting_;
      break;
    }
    case td_api::updateUser::ID: {
      auto update = move_object_as<td_api::updateUser>(result);
      add_user(users_, std::move(update->user_));
      break;
    }
    case td_api::updateUserFullInfo::ID: {
      auto update = move_object_as<td_api::updateUserFullInfo>(result);
      auto user_id = update->user_id_;
      set_user_bio(user_id, std::move(update->user_full_info_->bio_));
      break;
    }
    case td_api::updateBasicGroup::ID: {
      auto update = move_object_as<td_api::updateBasicGroup>(result);
      add_group(groups_, std::move(update->basic_group_));
      break;
    }
    case td_api::updateBasicGroupFullInfo::ID: {
      auto update = move_object_as<td_api::updateBasicGroupFullInfo>(result);
      auto group_id = update->basic_group_id_;
      auto full_info = std::move(update->basic_group_full_info_);
      set_group_description(group_id, std::move(full_info->description_));
      set_group_invite_link(group_id, full_info->invite_link_ != nullptr
                                          ? std::move(full_info->invite_link_->invite_link_)
                                          : td::string());
      break;
    }
    case td_api::updateSupergroup::ID: {
      auto update = move_object_as<td_api::updateSupergroup>(result);
      add_supergroup(supergroups_, std::move(update->supergroup_));
      break;
    }
    case td_api::updateSupergroupFullInfo::ID: {
      auto update = move_object_as<td_api::updateSupergroupFullInfo>(result);
      auto supergroup_id = update->supergroup_id_;
      auto full_info = std::move(update->supergroup_full_info_);
      set_supergroup_description(supergroup_id, std::move(full_info->description_));
      set_supergroup_invite_link(supergroup_id, full_info->invite_link_ != nullptr
                                                    ? std::move(full_info->invite_link_->invite_link_)
                                                    : td::string());
      set_supergroup_sticker_set_id(supergroup_id, full_info->sticker_set_id_);
      set_supergroup_can_set_sticker_set(supergroup_id, full_info->can_set_sticker_set_);
      set_supergroup_slow_mode_delay(supergroup_id, full_info->slow_mode_delay_);
      set_supergroup_linked_chat_id(supergroup_id, full_info->linked_chat_id_);
      set_supergroup_location(supergroup_id, std::move(full_info->location_));
      break;
    }
    case td_api::updateOption::ID: {
      auto update = move_object_as<td_api::updateOption>(result);
      const td::string &name = update->name_;
      if (name == "my_id") {
        if (update->value_->get_id() == td_api::optionValueEmpty::ID) {
          CHECK(logging_out_);
          my_id_ = -1;
        } else {
          CHECK(update->value_->get_id() == td_api::optionValueInteger::ID);
          my_id_ = static_cast<int32>(move_object_as<td_api::optionValueInteger>(update->value_)->value_);
        }
      }
      if (name == "group_anonymous_bot_user_id" && update->value_->get_id() == td_api::optionValueInteger::ID) {
        group_anonymous_bot_user_id_ =
            static_cast<int32>(move_object_as<td_api::optionValueInteger>(update->value_)->value_);
      }
      if (name == "telegram_service_notifications_chat_id" &&
          update->value_->get_id() == td_api::optionValueInteger::ID) {
        service_notifications_user_id_ =
            static_cast<int32>(move_object_as<td_api::optionValueInteger>(update->value_)->value_);
      }
      if (name == "authorization_date") {
        if (update->value_->get_id() == td_api::optionValueEmpty::ID) {
          authorization_date_ = -1;
        } else {
          CHECK(update->value_->get_id() == td_api::optionValueInteger::ID);
          authorization_date_ = static_cast<int32>(move_object_as<td_api::optionValueInteger>(update->value_)->value_);
        }
      }
      if (name == "xallowed_update_types") {
        if (update->value_->get_id() == td_api::optionValueEmpty::ID) {
          allowed_update_types_ = DEFAULT_ALLOWED_UPDATE_TYPES;
        } else {
          CHECK(update->value_->get_id() == td_api::optionValueInteger::ID);
          allowed_update_types_ =
              static_cast<td::uint32>(move_object_as<td_api::optionValueInteger>(update->value_)->value_);
        }
      }
      if (name == "unix_time" && update->value_->get_id() != td_api::optionValueEmpty::ID) {
        CHECK(update->value_->get_id() == td_api::optionValueInteger::ID);
        local_unix_time_difference_ =
            static_cast<double>(move_object_as<td_api::optionValueInteger>(update->value_)->value_) - td::Time::now();
        if (was_authorized_) {
          update_shared_unix_time_difference();
        }
      }
      break;
    }
    case td_api::updatePoll::ID:
      add_update_poll(move_object_as<td_api::updatePoll>(result));
      break;
    case td_api::updatePollAnswer::ID:
      add_update_poll_answer(move_object_as<td_api::updatePollAnswer>(result));
      break;
    case td_api::updateNewInlineQuery::ID: {
      auto update = move_object_as<td_api::updateNewInlineQuery>(result);
      add_new_inline_query(update->id_, update->sender_user_id_, std::move(update->user_location_),
                           std::move(update->chat_type_), update->query_, update->offset_);
      break;
    }
    case td_api::updateNewChosenInlineResult::ID: {
      auto update = move_object_as<td_api::updateNewChosenInlineResult>(result);
      add_new_chosen_inline_result(update->sender_user_id_, std::move(update->user_location_), update->query_,
                                   update->result_id_, update->inline_message_id_);
      break;
    }
    case td_api::updateNewCallbackQuery::ID:
      add_new_callback_query(move_object_as<td_api::updateNewCallbackQuery>(result));
      break;
    case td_api::updateNewInlineCallbackQuery::ID:
      add_new_inline_callback_query(move_object_as<td_api::updateNewInlineCallbackQuery>(result));
      break;
    case td_api::updateNewShippingQuery::ID:
      add_new_shipping_query(move_object_as<td_api::updateNewShippingQuery>(result));
      break;
    case td_api::updateNewPreCheckoutQuery::ID:
      add_new_pre_checkout_query(move_object_as<td_api::updateNewPreCheckoutQuery>(result));
      break;
    case td_api::updateNewCustomEvent::ID:
      add_new_custom_event(move_object_as<td_api::updateNewCustomEvent>(result));
      break;
    case td_api::updateNewCustomQuery::ID:
      add_new_custom_query(move_object_as<td_api::updateNewCustomQuery>(result));
      break;
    case td_api::updateChatMember::ID:
      add_update_chat_member(move_object_as<td_api::updateChatMember>(result));
      break;
    default:
      // we are not interested in this updates
      break;
  }
}

void Client::on_result(td::uint64 id, object_ptr<td_api::Object> result) {
  LOG(DEBUG) << "Receive from Td: " << id << " " << to_string(result);
  if (id == 0) {
    return on_update(std::move(result));
  }

  auto *handler_ptr = handlers_.get(id);
  CHECK(handler_ptr != nullptr);
  auto handler = std::move(*handler_ptr);
  handler->on_result(std::move(result));
  handlers_.erase(id);
}

td::Slice Client::get_logging_out_error_description() const {
  return is_api_id_invalid_ ? API_ID_INVALID_ERROR_DESCRIPTION : LOGGING_OUT_ERROR_DESCRIPTION;
}

void Client::on_closed() {
  LOG(WARNING) << "Closed";
  CHECK(logging_out_ || closing_);
  CHECK(!td_client_.empty());
  td_client_.reset();

  int http_status_code = logging_out_ ? LOGGING_OUT_ERROR_CODE : CLOSING_ERROR_CODE;
  Slice description = logging_out_ ? get_logging_out_error_description() : CLOSING_ERROR_DESCRIPTION;
  if (webhook_set_query_) {
    fail_query(http_status_code, description, std::move(webhook_set_query_));
  }
  if (!webhook_url_.empty()) {
    webhook_id_.reset();
  }
  if (long_poll_query_) {
    long_poll_wakeup(true);
    CHECK(!long_poll_query_);
  }

  while (!cmd_queue_.empty()) {
    auto query = std::move(cmd_queue_.front());
    cmd_queue_.pop();
    fail_query(http_status_code, description, std::move(query));
  }

  while (!yet_unsent_messages_.empty()) {
    auto it = yet_unsent_messages_.begin();
    auto chat_id = it->first.chat_id;
    auto message_id = it->first.message_id;
    if (!USE_MESSAGE_DATABASE) {
      LOG(ERROR) << "Doesn't receive updateMessageSendFailed for message " << message_id << " in chat " << chat_id;
    }
    on_message_send_failed(chat_id, message_id, 0, Status::Error(http_status_code, description));
  }

  while (!pending_bot_resolve_queries_.empty()) {
    auto it = pending_bot_resolve_queries_.begin();
    fail_query(http_status_code, description, std::move(it->second.query));
    pending_bot_resolve_queries_.erase(it);
  }

  while (!file_download_listeners_.empty()) {
    auto it = file_download_listeners_.begin();
    auto file_id = it->first;
    LOG(ERROR) << "Doesn't receive updateFile for file " << file_id;
    on_file_download(file_id, Status::Error(http_status_code, description));
  }

  if (logging_out_) {
    parameters_->shared_data_->webhook_db_->erase(bot_token_with_dc_);

    class RmWorker : public td::Actor {
     public:
      RmWorker(td::string dir, td::ActorId<Client> parent) : dir_(std::move(dir)), parent_(std::move(parent)) {
      }

     private:
      td::string dir_;
      td::ActorId<Client> parent_;

      void start_up() override {
        CHECK(dir_.size() >= 24);
        CHECK(dir_.back() == TD_DIR_SLASH);
        td::rmrf(dir_).ignore();
        stop();
      }
      void tear_down() override {
        send_closure(parent_, &Client::finish_closing);
      }
    };
    // NB: the same scheduler as for database in Td
    auto current_scheduler_id = td::Scheduler::instance()->sched_id();
    auto scheduler_count = td::Scheduler::instance()->sched_count();
    auto scheduler_id = td::min(current_scheduler_id + 1, scheduler_count - 1);
    td::create_actor_on_scheduler<RmWorker>("RmWorker", scheduler_id, dir_, actor_id(this)).release();
    return;
  }

  finish_closing();
}

void Client::finish_closing() {
  if (clear_tqueue_ && logging_out_) {
    clear_tqueue();
  }

  set_timeout_in(need_close_ ? 0 : 600);
}

void Client::timeout_expired() {
  LOG(WARNING) << "Stop client";
  stop();
}

void Client::clear_tqueue() {
  CHECK(webhook_id_.empty());
  auto &tqueue = parameters_->shared_data_->tqueue_;
  auto size = tqueue->get_size(tqueue_id_);
  if (size > 0) {
    LOG(INFO) << "Removing " << size << " tqueue events";
    td::MutableSpan<td::TQueue::Event> span;
    auto r_size = tqueue->get(tqueue_id_, tqueue->get_tail(tqueue_id_), true, 0, span);
    CHECK(r_size.is_ok());
    CHECK(r_size.ok() == 0);
    CHECK(tqueue->get_size(tqueue_id_) == 0);
  }
}

bool Client::to_bool(td::MutableSlice value) {
  td::to_lower_inplace(value);
  value = td::trim(value);
  return value == "true" || value == "yes" || value == "1";
}

td::Result<td_api::object_ptr<td_api::keyboardButton>> Client::get_keyboard_button(JsonValue &button) {
  if (button.type() == JsonValue::Type::Object) {
    auto &object = button.get_object();

    TRY_RESULT(text, get_json_object_string_field(object, "text", false));

    TRY_RESULT(request_phone_number, get_json_object_bool_field(object, "request_phone_number"));
    TRY_RESULT(request_contact, get_json_object_bool_field(object, "request_contact"));
    if (request_phone_number || request_contact) {
      return make_object<td_api::keyboardButton>(text, make_object<td_api::keyboardButtonTypeRequestPhoneNumber>());
    }

    TRY_RESULT(request_location, get_json_object_bool_field(object, "request_location"));
    if (request_location) {
      return make_object<td_api::keyboardButton>(text, make_object<td_api::keyboardButtonTypeRequestLocation>());
    }

    if (has_json_object_field(object, "request_poll")) {
      bool force_regular = false;
      bool force_quiz = false;
      TRY_RESULT(request_poll, get_json_object_field(object, "request_poll", JsonValue::Type::Object, false));
      auto &request_poll_object = request_poll.get_object();
      if (has_json_object_field(request_poll_object, "type")) {
        TRY_RESULT(type, get_json_object_string_field(request_poll_object, "type"));
        if (type == "quiz") {
          force_quiz = true;
        } else if (type == "regular") {
          force_regular = true;
        }
      }
      return make_object<td_api::keyboardButton>(
          text, make_object<td_api::keyboardButtonTypeRequestPoll>(force_regular, force_quiz));
    }

    return make_object<td_api::keyboardButton>(text, nullptr);
  }
  if (button.type() == JsonValue::Type::String) {
    return make_object<td_api::keyboardButton>(button.get_string().str(), nullptr);
  }

  return Status::Error(400, "KeyboardButton must be a String or an Object");
}

td::Result<td_api::object_ptr<td_api::inlineKeyboardButton>> Client::get_inline_keyboard_button(JsonValue &button) {
  if (button.type() != JsonValue::Type::Object) {
    return Status::Error(400, "InlineKeyboardButton must be an Object");
  }

  auto &object = button.get_object();

  TRY_RESULT(text, get_json_object_string_field(object, "text", false));
  {
    TRY_RESULT(url, get_json_object_string_field(object, "url"));
    if (!url.empty()) {
      return make_object<td_api::inlineKeyboardButton>(text, make_object<td_api::inlineKeyboardButtonTypeUrl>(url));
    }
  }

  {
    TRY_RESULT(callback_data, get_json_object_string_field(object, "callback_data"));
    if (!callback_data.empty()) {
      return make_object<td_api::inlineKeyboardButton>(
          text, make_object<td_api::inlineKeyboardButtonTypeCallback>(callback_data));
    }
  }

  if (has_json_object_field(object, "callback_game")) {
    return make_object<td_api::inlineKeyboardButton>(text, make_object<td_api::inlineKeyboardButtonTypeCallbackGame>());
  }

  if (has_json_object_field(object, "pay")) {
    return make_object<td_api::inlineKeyboardButton>(text, make_object<td_api::inlineKeyboardButtonTypeBuy>());
  }

  if (has_json_object_field(object, "switch_inline_query")) {
    TRY_RESULT(switch_inline_query, get_json_object_string_field(object, "switch_inline_query", false));
    return make_object<td_api::inlineKeyboardButton>(
        text, make_object<td_api::inlineKeyboardButtonTypeSwitchInline>(switch_inline_query, false));
  }

  if (has_json_object_field(object, "switch_inline_query_current_chat")) {
    TRY_RESULT(switch_inline_query, get_json_object_string_field(object, "switch_inline_query_current_chat", false));
    return make_object<td_api::inlineKeyboardButton>(
        text, make_object<td_api::inlineKeyboardButtonTypeSwitchInline>(switch_inline_query, true));
  }

  if (has_json_object_field(object, "login_url")) {
    TRY_RESULT(login_url, get_json_object_field(object, "login_url", JsonValue::Type::Object, false));
    CHECK(login_url.type() == JsonValue::Type::Object);
    auto &login_url_object = login_url.get_object();
    TRY_RESULT(url, get_json_object_string_field(login_url_object, "url", false));
    TRY_RESULT(bot_username, get_json_object_string_field(login_url_object, "bot_username"));
    TRY_RESULT(request_write_access, get_json_object_bool_field(login_url_object, "request_write_access"));
    TRY_RESULT(forward_text, get_json_object_string_field(login_url_object, "forward_text"));

    int32 bot_user_id = 0;
    if (bot_username.empty()) {
      bot_user_id = my_id_;
    } else {
      if (bot_username[0] == '@') {
        bot_username = bot_username.substr(1);
      }
      if (bot_username.empty()) {
        return Status::Error(400, "LoginUrl bot username is invalid");
      }
      for (auto c : bot_username) {
        if (c != '_' && !td::is_alnum(c)) {
          return Status::Error(400, "LoginUrl bot username is invalid");
        }
      }
      if (cur_temp_bot_user_id_ >= 100000) {
        return Status::Error(400, "Too much different LoginUrl bot usernames");
      }
      auto &user_id = bot_user_ids_[bot_username];
      if (user_id == 0) {
        user_id = cur_temp_bot_user_id_++;
        user_id *= 1000;
      }
      if (user_id % 1000 == 0) {
        unresolved_bot_usernames_.insert(bot_username);
      }
      bot_user_id = user_id;
    }
    if (!request_write_access) {
      bot_user_id *= -1;
    }
    return make_object<td_api::inlineKeyboardButton>(
        text, make_object<td_api::inlineKeyboardButtonTypeLoginUrl>(url, bot_user_id, forward_text));
  }

  return Status::Error(400, "Text buttons are unallowed in the inline keyboard");
}

td::Result<td_api::object_ptr<td_api::ReplyMarkup>> Client::get_reply_markup(const Query *query) {
  auto reply_markup = query->arg("reply_markup");
  if (reply_markup.empty()) {
    return nullptr;
  }

  LOG(INFO) << "Parsing JSON object: " << reply_markup;
  auto r_value = json_decode(reply_markup);
  if (r_value.is_error()) {
    LOG(INFO) << "Can't parse JSON object: " << r_value.error();
    return Status::Error(400, "Can't parse reply keyboard markup JSON object");
  }

  return get_reply_markup(r_value.move_as_ok());
}

td::Result<td_api::object_ptr<td_api::ReplyMarkup>> Client::get_reply_markup(JsonValue &&value) {
  td::vector<td::vector<object_ptr<td_api::keyboardButton>>> rows;
  td::vector<td::vector<object_ptr<td_api::inlineKeyboardButton>>> inline_rows;
  Slice input_field_placeholder;
  bool resize = false;
  bool one_time = false;
  bool remove = false;
  bool is_personal = false;
  bool force_reply = false;

  if (value.type() != JsonValue::Type::Object) {
    return Status::Error(400, "Object expected as reply markup");
  }
  for (auto &field_value : value.get_object()) {
    if (field_value.first == "keyboard") {
      auto keyboard = std::move(field_value.second);
      if (keyboard.type() != JsonValue::Type::Array) {
        return Status::Error(400, "Field \"keyboard\" of the ReplyKeyboardMarkup must be an Array");
      }
      for (auto &row : keyboard.get_array()) {
        td::vector<object_ptr<td_api::keyboardButton>> new_row;
        if (row.type() != JsonValue::Type::Array) {
          return Status::Error(400, "Field \"keyboard\" of the ReplyKeyboardMarkup must be an Array of Arrays");
        }
        for (auto &button : row.get_array()) {
          auto r_button = get_keyboard_button(button);
          if (r_button.is_error()) {
            return Status::Error(400, PSLICE() << "Can't parse keyboard button: " << r_button.error().message());
          }
          new_row.push_back(r_button.move_as_ok());
        }

        rows.push_back(std::move(new_row));
      }
    } else if (field_value.first == "inline_keyboard") {
      auto inline_keyboard = std::move(field_value.second);
      if (inline_keyboard.type() != JsonValue::Type::Array) {
        return Status::Error(400, "Field \"inline_keyboard\" of the InlineKeyboardMarkup must be an Array");
      }
      for (auto &inline_row : inline_keyboard.get_array()) {
        td::vector<object_ptr<td_api::inlineKeyboardButton>> new_inline_row;
        if (inline_row.type() != JsonValue::Type::Array) {
          return Status::Error(400, "Field \"inline_keyboard\" of the InlineKeyboardMarkup must be an Array of Arrays");
        }
        for (auto &button : inline_row.get_array()) {
          auto r_button = get_inline_keyboard_button(button);
          if (r_button.is_error()) {
            return Status::Error(400, PSLICE() << "Can't parse inline keyboard button: " << r_button.error().message());
          }
          new_inline_row.push_back(r_button.move_as_ok());
        }

        inline_rows.push_back(std::move(new_inline_row));
      }
    } else if (field_value.first == "resize_keyboard") {
      if (field_value.second.type() != JsonValue::Type::Boolean) {
        return Status::Error(400, "Field \"resize_keyboard\" of the ReplyKeyboardMarkup must be of the type Boolean");
      }
      resize = field_value.second.get_boolean();
    } else if (field_value.first == "one_time_keyboard") {
      if (field_value.second.type() != JsonValue::Type::Boolean) {
        return Status::Error(400, "Field \"one_time_keyboard\" of the ReplyKeyboardMarkup must be of the type Boolean");
      }
      one_time = field_value.second.get_boolean();
    } else if (field_value.first == "hide_keyboard" || field_value.first == "remove_keyboard") {
      if (field_value.second.type() != JsonValue::Type::Boolean) {
        return Status::Error(400, "Field \"remove_keyboard\" of the ReplyKeyboardRemove must be of the type Boolean");
      }
      remove = field_value.second.get_boolean();
    } else if (field_value.first == "personal_keyboard" || field_value.first == "selective") {
      if (field_value.second.type() != JsonValue::Type::Boolean) {
        return Status::Error(400, "Field \"selective\" of the reply markup must be of the type Boolean");
      }
      is_personal = field_value.second.get_boolean();
    } else if (field_value.first == "force_reply_keyboard" || field_value.first == "force_reply") {
      if (field_value.second.type() != JsonValue::Type::Boolean) {
        return Status::Error(400, "Field \"force_reply\" of the reply markup must be of the type Boolean");
      }
      force_reply = field_value.second.get_boolean();
    } else if (field_value.first == "input_field_placeholder") {
      if (field_value.second.type() != JsonValue::Type::String) {
        return Status::Error(400, "Field \"input_field_placeholder\" of the reply markup must be of the type String");
      }
      input_field_placeholder = field_value.second.get_string();
    }
  }

  object_ptr<td_api::ReplyMarkup> result;
  if (!rows.empty()) {
    result = make_object<td_api::replyMarkupShowKeyboard>(std::move(rows), resize, one_time, is_personal,
                                                          input_field_placeholder.str());
  } else if (!inline_rows.empty()) {
    result = make_object<td_api::replyMarkupInlineKeyboard>(std::move(inline_rows));
  } else if (remove) {
    result = make_object<td_api::replyMarkupRemoveKeyboard>(is_personal);
  } else if (force_reply) {
    result = make_object<td_api::replyMarkupForceReply>(is_personal, input_field_placeholder.str());
  }
  if (result == nullptr || result->get_id() != td_api::replyMarkupInlineKeyboard::ID) {
    unresolved_bot_usernames_.clear();
  }

  return std::move(result);
}

td::Result<td_api::object_ptr<td_api::labeledPricePart>> Client::get_labeled_price_part(JsonValue &value) {
  if (value.type() != JsonValue::Type::Object) {
    return Status::Error(400, "LabeledPrice must be an Object");
  }

  auto &object = value.get_object();

  TRY_RESULT(label, get_json_object_string_field(object, "label", false));
  if (label.empty()) {
    return Status::Error(400, "LabeledPrice label must be non-empty");
  }

  TRY_RESULT(amount, get_json_object_field(object, "amount", JsonValue::Type::Null, false));
  Slice number;
  if (amount.type() == JsonValue::Type::Number) {
    number = amount.get_number();
  } else if (amount.type() == JsonValue::Type::String) {
    number = amount.get_string();
  } else {
    return Status::Error(400, "Field \"amount\" must be of type Number or String");
  }
  auto parsed_amount = td::to_integer_safe<int64>(number);
  if (parsed_amount.is_error()) {
    return Status::Error(400, "Can't parse \"amount\" as Number");
  }
  return make_object<td_api::labeledPricePart>(label, parsed_amount.ok());
}

td::Result<td::vector<td_api::object_ptr<td_api::labeledPricePart>>> Client::get_labeled_price_parts(JsonValue &value) {
  if (value.type() != JsonValue::Type::Array) {
    return Status::Error(400, "Expected an Array of labeled prices");
  }

  td::vector<object_ptr<td_api::labeledPricePart>> prices;
  for (auto &price : value.get_array()) {
    auto r_labeled_price = get_labeled_price_part(price);
    if (r_labeled_price.is_error()) {
      return Status::Error(400, PSLICE() << "Can't parse labeled price: " << r_labeled_price.error().message());
    }
    prices.push_back(r_labeled_price.move_as_ok());
  }
  if (prices.empty()) {
    return Status::Error(400, "There must be at least one price");
  }

  return std::move(prices);
}

td::Result<td::vector<td::int64>> Client::get_suggested_tip_amounts(JsonValue &value) {
  if (value.type() != JsonValue::Type::Array) {
    return Status::Error(400, "Expected an Array of suggested tip amounts");
  }

  td::vector<int64> suggested_tip_amounts;
  for (auto &amount : value.get_array()) {
    Slice number;
    if (amount.type() == JsonValue::Type::Number) {
      number = amount.get_number();
    } else if (amount.type() == JsonValue::Type::String) {
      number = amount.get_string();
    } else {
      return Status::Error(400, "Suggested tip amount must be of type Number or String");
    }
    auto parsed_amount = td::to_integer_safe<int64>(number);
    if (parsed_amount.is_error()) {
      return Status::Error(400, "Can't parse suggested tip amount as Number");
    }
    suggested_tip_amounts.push_back(parsed_amount.ok());
  }
  return std::move(suggested_tip_amounts);
}

td::Result<td_api::object_ptr<td_api::shippingOption>> Client::get_shipping_option(JsonValue &option) {
  if (option.type() != JsonValue::Type::Object) {
    return Status::Error(400, "ShippingOption must be an Object");
  }

  auto &object = option.get_object();

  TRY_RESULT(id, get_json_object_string_field(object, "id", false));
  if (id.empty()) {
    return Status::Error(400, "ShippingOption identifier must be non-empty");
  }

  TRY_RESULT(title, get_json_object_string_field(object, "title", false));
  if (title.empty()) {
    return Status::Error(400, "ShippingOption title must be non-empty");
  }

  TRY_RESULT(prices_json, get_json_object_field(object, "prices", JsonValue::Type::Array, false));

  auto r_prices = get_labeled_price_parts(prices_json);
  if (r_prices.is_error()) {
    return Status::Error(400, PSLICE() << "Can't parse shipping option prices: " << r_prices.error().message());
  }

  return make_object<td_api::shippingOption>(id, title, r_prices.move_as_ok());
}

td::Result<td::vector<td_api::object_ptr<td_api::shippingOption>>> Client::get_shipping_options(const Query *query) {
  TRY_RESULT(shipping_options, get_required_string_arg(query, "shipping_options"));

  LOG(INFO) << "Parsing JSON object: " << shipping_options;
  auto r_value = json_decode(shipping_options);
  if (r_value.is_error()) {
    LOG(INFO) << "Can't parse JSON object: " << r_value.error();
    return Status::Error(400, "Can't parse shipping options JSON object");
  }

  return get_shipping_options(r_value.move_as_ok());
}

td::Result<td::vector<td_api::object_ptr<td_api::shippingOption>>> Client::get_shipping_options(JsonValue &&value) {
  if (value.type() != JsonValue::Type::Array) {
    return Status::Error(400, "Expected an Array of shipping options");
  }

  td::vector<object_ptr<td_api::shippingOption>> options;
  for (auto &option : value.get_array()) {
    auto r_shipping_option = get_shipping_option(option);
    if (r_shipping_option.is_error()) {
      return Status::Error(400, PSLICE() << "Can't parse shipping option: " << r_shipping_option.error().message());
    }
    options.push_back(r_shipping_option.move_as_ok());
  }
  if (options.empty()) {
    return Status::Error(400, "There must be at least one shipping option");
  }

  return std::move(options);
}

td_api::object_ptr<td_api::ChatAction> Client::get_chat_action(const Query *query) {
  auto action = query->arg("action");
  td::to_lower_inplace(action);
  if (action == "cancel") {
    return make_object<td_api::chatActionCancel>();
  }
  if (action == "typing") {
    return make_object<td_api::chatActionTyping>();
  }
  if (action == "record_video") {
    return make_object<td_api::chatActionRecordingVideo>();
  }
  if (action == "upload_video") {
    return make_object<td_api::chatActionUploadingVideo>();
  }
  if (action == "record_audio" || action == "record_voice") {
    return make_object<td_api::chatActionRecordingVoiceNote>();
  }
  if (action == "upload_audio" || action == "upload_voice") {
    return make_object<td_api::chatActionUploadingVoiceNote>();
  }
  if (action == "upload_photo") {
    return make_object<td_api::chatActionUploadingPhoto>();
  }
  if (action == "upload_document") {
    return make_object<td_api::chatActionUploadingDocument>();
  }
  if (action == "pick_up_location" || action == "find_location") {
    return make_object<td_api::chatActionChoosingLocation>();
  }
  if (action == "record_video_note") {
    return make_object<td_api::chatActionRecordingVideoNote>();
  }
  if (action == "upload_video_note") {
    return make_object<td_api::chatActionUploadingVideoNote>();
  }
  return nullptr;
}

td_api::object_ptr<td_api::InputFile> Client::get_input_file(const Query *query, Slice field_name,
                                                             bool force_file) const {
  return get_input_file(query, field_name, query->arg(field_name), force_file);
}

td::string Client::get_local_file_path(Slice file_uri) {
  if (td::begins_with(file_uri, "/")) {
    file_uri.remove_prefix(td::begins_with(file_uri, "/localhost") ? 10 : 1);
  }
#if TD_PORT_WINDOWS
  if (td::begins_with(file_uri, "/")) {
    file_uri.remove_prefix(1);
  }
#endif
  td::string result(file_uri.size(), '\0');
  auto result_len = url_decode(file_uri, result, false);
  result.resize(result_len);
  return result;
}

td_api::object_ptr<td_api::InputFile> Client::get_input_file(const Query *query, Slice field_name, Slice file_id,
                                                             bool force_file) const {
  if (!file_id.empty()) {
    if (parameters_->local_mode_) {
      Slice file_protocol{"file:/"};
      if (td::begins_with(file_id, file_protocol)) {
        return make_object<td_api::inputFileLocal>(get_local_file_path(file_id.substr(file_protocol.size())));
      }
    }
    Slice attach_protocol{"attach://"};
    if (td::begins_with(file_id, attach_protocol)) {
      field_name = file_id.substr(attach_protocol.size());
    } else {
      if (!force_file) {
        return make_object<td_api::inputFileRemote>(file_id.str());
      }
    }
  }
  auto file = query->file(field_name);
  if (file != nullptr) {
    return make_object<td_api::inputFileLocal>(file->temp_file_name);
  }

  return nullptr;
}

td_api::object_ptr<td_api::inputThumbnail> Client::get_input_thumbnail(const Query *query, Slice field_name) const {
  auto input_file = get_input_file(query, field_name, true);
  if (input_file == nullptr) {
    return nullptr;
  }
  return make_object<td_api::inputThumbnail>(std::move(input_file), 0, 0);
}

td::Result<td_api::object_ptr<td_api::InputMessageContent>> Client::get_input_message_content(
    JsonValue &input_message_content, bool is_input_message_content_required) {
  CHECK(input_message_content.type() == JsonValue::Type::Object);
  auto &object = input_message_content.get_object();

  TRY_RESULT(message_text, get_json_object_string_field(object, "message_text"));

  if (!message_text.empty()) {
    TRY_RESULT(disable_web_page_preview, get_json_object_bool_field(object, "disable_web_page_preview"));
    TRY_RESULT(parse_mode, get_json_object_string_field(object, "parse_mode"));
    auto entities = get_json_object_field_force(object, "entities");
    TRY_RESULT(input_message_text, get_input_message_text(std::move(message_text), disable_web_page_preview,
                                                          std::move(parse_mode), std::move(entities)));
    return std::move(input_message_text);
  }

  if (has_json_object_field(object, "latitude") && has_json_object_field(object, "longitude")) {
    TRY_RESULT(latitude, get_json_object_double_field(object, "latitude", false));
    TRY_RESULT(longitude, get_json_object_double_field(object, "longitude", false));
    TRY_RESULT(horizontal_accuracy, get_json_object_double_field(object, "horizontal_accuracy"));
    TRY_RESULT(live_period, get_json_object_int_field(object, "live_period"));
    TRY_RESULT(heading, get_json_object_int_field(object, "heading"));
    TRY_RESULT(proximity_alert_radius, get_json_object_int_field(object, "proximity_alert_radius"));
    auto location = make_object<td_api::location>(latitude, longitude, horizontal_accuracy);

    if (has_json_object_field(object, "title") && has_json_object_field(object, "address")) {
      TRY_RESULT(title, get_json_object_string_field(object, "title", false));
      TRY_RESULT(address, get_json_object_string_field(object, "address", false));
      td::string provider;
      td::string venue_id;
      td::string venue_type;

      TRY_RESULT(google_place_id, get_json_object_string_field(object, "google_place_id"));
      TRY_RESULT(google_place_type, get_json_object_string_field(object, "google_place_type"));
      if (!google_place_id.empty() || !google_place_type.empty()) {
        provider = "gplaces";
        venue_id = std::move(google_place_id);
        venue_type = std::move(google_place_type);
      }
      TRY_RESULT(foursquare_id, get_json_object_string_field(object, "foursquare_id"));
      TRY_RESULT(foursquare_type, get_json_object_string_field(object, "foursquare_type"));
      if (!foursquare_id.empty() || !foursquare_type.empty()) {
        provider = "foursquare";
        venue_id = std::move(foursquare_id);
        venue_type = std::move(foursquare_type);
      }

      return make_object<td_api::inputMessageVenue>(
          make_object<td_api::venue>(std::move(location), title, address, provider, venue_id, venue_type));
    }

    return make_object<td_api::inputMessageLocation>(std::move(location), live_period, heading, proximity_alert_radius);
  }

  if (has_json_object_field(object, "phone_number")) {
    TRY_RESULT(phone_number, get_json_object_string_field(object, "phone_number", false));
    TRY_RESULT(first_name, get_json_object_string_field(object, "first_name", false));
    TRY_RESULT(last_name, get_json_object_string_field(object, "last_name"));
    TRY_RESULT(vcard, get_json_object_string_field(object, "vcard"));

    return make_object<td_api::inputMessageContact>(
        make_object<td_api::contact>(phone_number, first_name, last_name, vcard, 0));
  }

  if (has_json_object_field(object, "payload")) {
    TRY_RESULT(title, get_json_object_string_field(object, "title", false));
    TRY_RESULT(description, get_json_object_string_field(object, "description", false));
    TRY_RESULT(payload, get_json_object_string_field(object, "payload", false));
    if (!td::check_utf8(payload)) {
      return Status::Error(400, "InputInvoiceMessageContent payload must be encoded in UTF-8");
    }
    TRY_RESULT(provider_token, get_json_object_string_field(object, "provider_token", false));
    TRY_RESULT(currency, get_json_object_string_field(object, "currency", false));
    TRY_RESULT(prices_object, get_json_object_field(object, "prices", JsonValue::Type::Array, false));
    TRY_RESULT(prices, get_labeled_price_parts(prices_object));
    TRY_RESULT(provider_data, get_json_object_string_field(object, "provider_data"));
    TRY_RESULT(max_tip_amount, get_json_object_long_field(object, "max_tip_amount"));
    td::vector<int64> suggested_tip_amounts;
    TRY_RESULT(suggested_tip_amounts_array,
               get_json_object_field(object, "suggested_tip_amounts", JsonValue::Type::Array));
    if (suggested_tip_amounts_array.type() == JsonValue::Type::Array) {
      TRY_RESULT_ASSIGN(suggested_tip_amounts, get_suggested_tip_amounts(suggested_tip_amounts_array));
    }
    TRY_RESULT(photo_url, get_json_object_string_field(object, "photo_url"));
    TRY_RESULT(photo_size, get_json_object_int_field(object, "photo_size"));
    TRY_RESULT(photo_width, get_json_object_int_field(object, "photo_width"));
    TRY_RESULT(photo_height, get_json_object_int_field(object, "photo_height"));
    TRY_RESULT(need_name, get_json_object_bool_field(object, "need_name"));
    TRY_RESULT(need_phone_number, get_json_object_bool_field(object, "need_phone_number"));
    TRY_RESULT(need_email_address, get_json_object_bool_field(object, "need_email"));
    TRY_RESULT(need_shipping_address, get_json_object_bool_field(object, "need_shipping_address"));
    TRY_RESULT(send_phone_number_to_provider, get_json_object_bool_field(object, "send_phone_number_to_provider"));
    TRY_RESULT(send_email_address_to_provider, get_json_object_bool_field(object, "send_email_to_provider"));
    TRY_RESULT(is_flexible, get_json_object_bool_field(object, "is_flexible"));

    return make_object<td_api::inputMessageInvoice>(
        make_object<td_api::invoice>(currency, std::move(prices), max_tip_amount, std::move(suggested_tip_amounts),
                                     false, need_name, need_phone_number, need_email_address, need_shipping_address,
                                     send_phone_number_to_provider, send_email_address_to_provider, is_flexible),
        title, description, photo_url, photo_size, photo_width, photo_height, payload, provider_token, provider_data,
        td::string());
  }

  if (is_input_message_content_required) {
    return Status::Error(400, "Input message content is not specified");
  }
  return nullptr;
}

td_api::object_ptr<td_api::messageSendOptions> Client::get_message_send_options(bool disable_notification) {
  return make_object<td_api::messageSendOptions>(disable_notification, false, nullptr);
}

td::Result<td::vector<td_api::object_ptr<td_api::InputInlineQueryResult>>> Client::get_inline_query_results(
    const Query *query) {
  auto results_encoded = query->arg("results");
  if (results_encoded.empty()) {
    return td::vector<object_ptr<td_api::InputInlineQueryResult>>();
  }

  LOG(INFO) << "Parsing JSON object: " << results_encoded;
  auto r_values = json_decode(results_encoded);
  if (r_values.is_error()) {
    return Status::Error(400,
                         PSLICE() << "Can't parse JSON encoded inline query results: " << r_values.error().message());
  }

  return get_inline_query_results(r_values.move_as_ok());
}

td::Result<td::vector<td_api::object_ptr<td_api::InputInlineQueryResult>>> Client::get_inline_query_results(
    td::JsonValue &&values) {
  if (values.type() == JsonValue::Type::Null) {
    return td::vector<object_ptr<td_api::InputInlineQueryResult>>();
  }
  if (values.type() != JsonValue::Type::Array) {
    return Status::Error(400, "Expected an Array of inline query results");
  }

  td::vector<object_ptr<td_api::InputInlineQueryResult>> inline_query_results;
  for (auto &value : values.get_array()) {
    auto r_inline_query_result = get_inline_query_result(std::move(value));
    if (r_inline_query_result.is_error()) {
      return Status::Error(400,
                           PSLICE() << "Can't parse inline query result: " << r_inline_query_result.error().message());
    }
    inline_query_results.push_back(r_inline_query_result.move_as_ok());
  }

  return std::move(inline_query_results);
}

td::Result<td_api::object_ptr<td_api::InputInlineQueryResult>> Client::get_inline_query_result(td::JsonValue &&value) {
  if (value.type() != JsonValue::Type::Object) {
    return Status::Error(400, "Inline query result must be an object");
  }

  auto &object = value.get_object();

  TRY_RESULT(type, get_json_object_string_field(object, "type", false));
  td::to_lower_inplace(type);

  TRY_RESULT(id, get_json_object_string_field(object, "id", false));

  bool is_input_message_content_required = (type == "article");
  object_ptr<td_api::InputMessageContent> input_message_content;

  TRY_RESULT(input_message_content_obj,
             get_json_object_field(object, "input_message_content", JsonValue::Type::Object));
  if (input_message_content_obj.type() == JsonValue::Type::Null) {
    TRY_RESULT(message_text, get_json_object_string_field(object, "message_text", !is_input_message_content_required));
    TRY_RESULT(disable_web_page_preview, get_json_object_bool_field(object, "disable_web_page_preview"));
    TRY_RESULT(parse_mode, get_json_object_string_field(object, "parse_mode"));
    auto entities = get_json_object_field_force(object, "entities");

    if (is_input_message_content_required || !message_text.empty()) {
      TRY_RESULT(input_message_text, get_input_message_text(std::move(message_text), disable_web_page_preview,
                                                            std::move(parse_mode), std::move(entities)));
      input_message_content = std::move(input_message_text);
    }
  } else {
    TRY_RESULT(input_message_content_result,
               get_input_message_content(input_message_content_obj, is_input_message_content_required));
    input_message_content = std::move(input_message_content_result);
  }
  TRY_RESULT(input_caption, get_json_object_string_field(object, "caption"));
  TRY_RESULT(parse_mode, get_json_object_string_field(object, "parse_mode"));
  auto entities = get_json_object_field_force(object, "caption_entities");
  TRY_RESULT(caption, get_formatted_text(std::move(input_caption), std::move(parse_mode), std::move(entities)));

  TRY_RESULT(reply_markup_object, get_json_object_field(object, "reply_markup", JsonValue::Type::Object));
  object_ptr<td_api::ReplyMarkup> reply_markup;
  if (reply_markup_object.type() != JsonValue::Type::Null) {
    TRY_RESULT_ASSIGN(reply_markup, get_reply_markup(std::move(reply_markup_object)));
  }

  object_ptr<td_api::InputInlineQueryResult> result;
  if (type == "article") {
    TRY_RESULT(url, get_json_object_string_field(object, "url"));
    TRY_RESULT(hide_url, get_json_object_bool_field(object, "hide_url"));
    TRY_RESULT(title, get_json_object_string_field(object, "title", false));
    TRY_RESULT(description, get_json_object_string_field(object, "description"));
    TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url"));
    TRY_RESULT(thumbnail_width, get_json_object_int_field(object, "thumb_width"));
    TRY_RESULT(thumbnail_height, get_json_object_int_field(object, "thumb_height"));

    CHECK(input_message_content != nullptr);
    return make_object<td_api::inputInlineQueryResultArticle>(
        id, url, hide_url, title, description, thumbnail_url, thumbnail_width, thumbnail_height,
        std::move(reply_markup), std::move(input_message_content));
  }
  if (type == "audio") {
    TRY_RESULT(audio_url, get_json_object_string_field(object, "audio_url"));
    TRY_RESULT(audio_duration, get_json_object_int_field(object, "audio_duration"));
    TRY_RESULT(title, get_json_object_string_field(object, "title", audio_url.empty()));
    TRY_RESULT(performer, get_json_object_string_field(object, "performer"));
    if (audio_url.empty()) {
      TRY_RESULT_ASSIGN(audio_url, get_json_object_string_field(object, "audio_file_id", false));
    }

    if (input_message_content == nullptr) {
      input_message_content = make_object<td_api::inputMessageAudio>(nullptr, nullptr, audio_duration, title, performer,
                                                                     std::move(caption));
    }
    return make_object<td_api::inputInlineQueryResultAudio>(id, title, performer, audio_url, audio_duration,
                                                            std::move(reply_markup), std::move(input_message_content));
  }
  if (type == "contact") {
    TRY_RESULT(phone_number, get_json_object_string_field(object, "phone_number", false));
    TRY_RESULT(first_name, get_json_object_string_field(object, "first_name", false));
    TRY_RESULT(last_name, get_json_object_string_field(object, "last_name"));
    TRY_RESULT(vcard, get_json_object_string_field(object, "vcard"));
    TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url"));
    TRY_RESULT(thumbnail_width, get_json_object_int_field(object, "thumb_width"));
    TRY_RESULT(thumbnail_height, get_json_object_int_field(object, "thumb_height"));

    if (input_message_content == nullptr) {
      input_message_content = make_object<td_api::inputMessageContact>(
          make_object<td_api::contact>(phone_number, first_name, last_name, vcard, 0));
    }
    return make_object<td_api::inputInlineQueryResultContact>(
        id, make_object<td_api::contact>(phone_number, first_name, last_name, vcard, 0), thumbnail_url, thumbnail_width,
        thumbnail_height, std::move(reply_markup), std::move(input_message_content));
  }
  if (type == "document") {
    TRY_RESULT(title, get_json_object_string_field(object, "title", false));
    TRY_RESULT(description, get_json_object_string_field(object, "description"));
    TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url"));
    TRY_RESULT(document_url, get_json_object_string_field(object, "document_url"));
    TRY_RESULT(mime_type, get_json_object_string_field(object, "mime_type", document_url.empty()));
    TRY_RESULT(thumbnail_width, get_json_object_int_field(object, "thumb_width"));
    TRY_RESULT(thumbnail_height, get_json_object_int_field(object, "thumb_height"));
    if (document_url.empty()) {
      TRY_RESULT_ASSIGN(document_url, get_json_object_string_field(object, "document_file_id", false));
    }

    if (input_message_content == nullptr) {
      input_message_content = make_object<td_api::inputMessageDocument>(nullptr, nullptr, false, std::move(caption));
    }
    return make_object<td_api::inputInlineQueryResultDocument>(
        id, title, description, document_url, mime_type, thumbnail_url, thumbnail_width, thumbnail_height,
        std::move(reply_markup), std::move(input_message_content));
  }
  if (type == "game") {
    TRY_RESULT(game_short_name, get_json_object_string_field(object, "game_short_name", false));
    return make_object<td_api::inputInlineQueryResultGame>(id, game_short_name, std::move(reply_markup));
  }
  if (type == "gif") {
    TRY_RESULT(title, get_json_object_string_field(object, "title"));
    TRY_RESULT(gif_url, get_json_object_string_field(object, "gif_url"));
    TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url", gif_url.empty()));
    TRY_RESULT(thumbnail_mime_type, get_json_object_string_field(object, "thumb_mime_type", true));
    TRY_RESULT(gif_duration, get_json_object_int_field(object, "gif_duration"));
    TRY_RESULT(gif_width, get_json_object_int_field(object, "gif_width"));
    TRY_RESULT(gif_height, get_json_object_int_field(object, "gif_height"));
    if (gif_url.empty()) {
      TRY_RESULT_ASSIGN(gif_url, get_json_object_string_field(object, "gif_file_id", false));
    }

    if (input_message_content == nullptr) {
      input_message_content = make_object<td_api::inputMessageAnimation>(
          nullptr, nullptr, td::vector<int32>(), gif_duration, gif_width, gif_height, std::move(caption));
    }
    return make_object<td_api::inputInlineQueryResultAnimation>(
        id, title, thumbnail_url, thumbnail_mime_type, gif_url, "image/gif", gif_duration, gif_width, gif_height,
        std::move(reply_markup), std::move(input_message_content));
  }
  if (type == "location") {
    TRY_RESULT(latitude, get_json_object_double_field(object, "latitude", false));
    TRY_RESULT(longitude, get_json_object_double_field(object, "longitude", false));
    TRY_RESULT(horizontal_accuracy, get_json_object_double_field(object, "horizontal_accuracy"));
    TRY_RESULT(live_period, get_json_object_int_field(object, "live_period"));
    TRY_RESULT(heading, get_json_object_int_field(object, "heading"));
    TRY_RESULT(proximity_alert_radius, get_json_object_int_field(object, "proximity_alert_radius"));
    TRY_RESULT(title, get_json_object_string_field(object, "title", false));
    TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url"));
    TRY_RESULT(thumbnail_width, get_json_object_int_field(object, "thumb_width"));
    TRY_RESULT(thumbnail_height, get_json_object_int_field(object, "thumb_height"));

    if (input_message_content == nullptr) {
      auto location = make_object<td_api::location>(latitude, longitude, horizontal_accuracy);
      input_message_content =
          make_object<td_api::inputMessageLocation>(std::move(location), live_period, heading, proximity_alert_radius);
    }
    return make_object<td_api::inputInlineQueryResultLocation>(
        id, make_object<td_api::location>(latitude, longitude, horizontal_accuracy), live_period, title, thumbnail_url,
        thumbnail_width, thumbnail_height, std::move(reply_markup), std::move(input_message_content));
  }
  if (type == "mpeg4_gif") {
    TRY_RESULT(title, get_json_object_string_field(object, "title"));
    TRY_RESULT(mpeg4_url, get_json_object_string_field(object, "mpeg4_url"));
    TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url", mpeg4_url.empty()));
    TRY_RESULT(thumbnail_mime_type, get_json_object_string_field(object, "thumb_mime_type", true));
    TRY_RESULT(mpeg4_duration, get_json_object_int_field(object, "mpeg4_duration"));
    TRY_RESULT(mpeg4_width, get_json_object_int_field(object, "mpeg4_width"));
    TRY_RESULT(mpeg4_height, get_json_object_int_field(object, "mpeg4_height"));
    if (mpeg4_url.empty()) {
      TRY_RESULT_ASSIGN(mpeg4_url, get_json_object_string_field(object, "mpeg4_file_id", false));
    }

    if (input_message_content == nullptr) {
      input_message_content = make_object<td_api::inputMessageAnimation>(
          nullptr, nullptr, td::vector<int32>(), mpeg4_duration, mpeg4_width, mpeg4_height, std::move(caption));
    }
    return make_object<td_api::inputInlineQueryResultAnimation>(
        id, title, thumbnail_url, thumbnail_mime_type, mpeg4_url, "video/mp4", mpeg4_duration, mpeg4_width,
        mpeg4_height, std::move(reply_markup), std::move(input_message_content));
  }
  if (type == "photo") {
    TRY_RESULT(title, get_json_object_string_field(object, "title"));
    TRY_RESULT(description, get_json_object_string_field(object, "description"));
    TRY_RESULT(photo_url, get_json_object_string_field(object, "photo_url"));
    TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url", photo_url.empty()));
    TRY_RESULT(photo_width, get_json_object_int_field(object, "photo_width"));
    TRY_RESULT(photo_height, get_json_object_int_field(object, "photo_height"));
    if (photo_url.empty()) {
      TRY_RESULT_ASSIGN(photo_url, get_json_object_string_field(object, "photo_file_id", false));
    }

    if (input_message_content == nullptr) {
      input_message_content =
          make_object<td_api::inputMessagePhoto>(nullptr, nullptr, td::vector<int32>(), 0, 0, std::move(caption), 0);
    }
    return make_object<td_api::inputInlineQueryResultPhoto>(id, title, description, thumbnail_url, photo_url,
                                                            photo_width, photo_height, std::move(reply_markup),
                                                            std::move(input_message_content));
  }
  if (type == "sticker") {
    TRY_RESULT(sticker_file_id, get_json_object_string_field(object, "sticker_file_id", false));

    if (input_message_content == nullptr) {
      input_message_content = make_object<td_api::inputMessageSticker>(nullptr, nullptr, 0, 0, td::string());
    }
    return make_object<td_api::inputInlineQueryResultSticker>(id, "", sticker_file_id, 0, 0, std::move(reply_markup),
                                                              std::move(input_message_content));
  }
  if (type == "venue") {
    TRY_RESULT(latitude, get_json_object_double_field(object, "latitude", false));
    TRY_RESULT(longitude, get_json_object_double_field(object, "longitude", false));
    TRY_RESULT(horizontal_accuracy, get_json_object_double_field(object, "horizontal_accuracy"));
    TRY_RESULT(title, get_json_object_string_field(object, "title", false));
    TRY_RESULT(address, get_json_object_string_field(object, "address", false));
    TRY_RESULT(foursquare_id, get_json_object_string_field(object, "foursquare_id"));
    TRY_RESULT(foursquare_type, get_json_object_string_field(object, "foursquare_type"));
    TRY_RESULT(google_place_id, get_json_object_string_field(object, "google_place_id"));
    TRY_RESULT(google_place_type, get_json_object_string_field(object, "google_place_type"));
    TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url"));
    TRY_RESULT(thumbnail_width, get_json_object_int_field(object, "thumb_width"));
    TRY_RESULT(thumbnail_height, get_json_object_int_field(object, "thumb_height"));

    td::string provider;
    td::string venue_id;
    td::string venue_type;
    if (!google_place_id.empty() || !google_place_type.empty()) {
      provider = "gplaces";
      venue_id = std::move(google_place_id);
      venue_type = std::move(google_place_type);
    }
    if (!foursquare_id.empty() || !foursquare_type.empty()) {
      provider = "foursquare";
      venue_id = std::move(foursquare_id);
      venue_type = std::move(foursquare_type);
    }

    if (input_message_content == nullptr) {
      input_message_content = make_object<td_api::inputMessageVenue>(
          make_object<td_api::venue>(make_object<td_api::location>(latitude, longitude, horizontal_accuracy), title,
                                     address, provider, venue_id, venue_type));
    }
    return make_object<td_api::inputInlineQueryResultVenue>(
        id,
        make_object<td_api::venue>(make_object<td_api::location>(latitude, longitude, horizontal_accuracy), title,
                                   address, provider, venue_id, venue_type),
        thumbnail_url, thumbnail_width, thumbnail_height, std::move(reply_markup), std::move(input_message_content));
  }
  if (type == "video") {
    TRY_RESULT(title, get_json_object_string_field(object, "title", false));
    TRY_RESULT(description, get_json_object_string_field(object, "description"));
    TRY_RESULT(video_url, get_json_object_string_field(object, "video_url"));
    TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url", video_url.empty()));
    TRY_RESULT(mime_type, get_json_object_string_field(object, "mime_type", video_url.empty()));
    TRY_RESULT(video_width, get_json_object_int_field(object, "video_width"));
    TRY_RESULT(video_height, get_json_object_int_field(object, "video_height"));
    TRY_RESULT(video_duration, get_json_object_int_field(object, "video_duration"));
    if (video_url.empty()) {
      TRY_RESULT_ASSIGN(video_url, get_json_object_string_field(object, "video_file_id", false));
    }

    if (input_message_content == nullptr) {
      input_message_content =
          make_object<td_api::inputMessageVideo>(nullptr, nullptr, td::vector<int32>(), video_duration, video_width,
                                                 video_height, false, std::move(caption), 0);
    }
    return make_object<td_api::inputInlineQueryResultVideo>(id, title, description, thumbnail_url, video_url, mime_type,
                                                            video_width, video_height, video_duration,
                                                            std::move(reply_markup), std::move(input_message_content));
  }
  if (type == "voice") {
    TRY_RESULT(title, get_json_object_string_field(object, "title", false));
    TRY_RESULT(voice_note_url, get_json_object_string_field(object, "voice_url"));
    TRY_RESULT(voice_note_duration, get_json_object_int_field(object, "voice_duration"));
    if (voice_note_url.empty()) {
      TRY_RESULT_ASSIGN(voice_note_url, get_json_object_string_field(object, "voice_file_id", false));
    }

    if (input_message_content == nullptr) {
      input_message_content = make_object<td_api::inputMessageVoiceNote>(nullptr, voice_note_duration,
                                                                         "" /* waveform */, std::move(caption));
    }
    return make_object<td_api::inputInlineQueryResultVoiceNote>(
        id, title, voice_note_url, voice_note_duration, std::move(reply_markup), std::move(input_message_content));
  }

  return Status::Error(400, PSLICE() << "type \"" << type << "\" is unsupported for the inline query result");
}

td::Result<Client::BotCommandScope> Client::get_bot_command_scope(JsonValue &&value) {
  if (value.type() != JsonValue::Type::Object) {
    return Status::Error(400, "BotCommandScope must be an Object");
  }

  auto &object = value.get_object();

  TRY_RESULT(type, get_json_object_string_field(object, "type", false));
  if (type == "default") {
    return BotCommandScope(make_object<td_api::botCommandScopeDefault>());
  }
  if (type == "all_private_chats") {
    return BotCommandScope(make_object<td_api::botCommandScopeAllPrivateChats>());
  }
  if (type == "all_group_chats") {
    return BotCommandScope(make_object<td_api::botCommandScopeAllGroupChats>());
  }
  if (type == "all_chat_administrators") {
    return BotCommandScope(make_object<td_api::botCommandScopeAllChatAdministrators>());
  }
  if (type != "chat" && type != "chat_administrators" && type != "chat_member") {
    return Status::Error(400, "Unsupported type specified");
  }

  TRY_RESULT(chat_id, get_json_object_string_field(object, "chat_id", false));
  if (chat_id.empty()) {
    return Status::Error(400, "Empty chat_id specified");
  }
  if (type == "chat") {
    return BotCommandScope(make_object<td_api::botCommandScopeChat>(0), std::move(chat_id));
  }
  if (type == "chat_administrators") {
    return BotCommandScope(make_object<td_api::botCommandScopeChatAdministrators>(0), std::move(chat_id));
  }

  TRY_RESULT(user_id, get_json_object_int_field(object, "user_id", false));
  if (user_id <= 0) {
    return Status::Error(400, "Invalid user_id specified");
  }
  CHECK(type == "chat_member");
  return BotCommandScope(make_object<td_api::botCommandScopeChatMember>(0, user_id), std::move(chat_id), user_id);
}

td::Result<Client::BotCommandScope> Client::get_bot_command_scope(const Query *query) {
  auto scope = query->arg("scope");
  if (scope.empty()) {
    return BotCommandScope(make_object<td_api::botCommandScopeDefault>());
  }

  LOG(INFO) << "Parsing JSON object: " << scope;
  auto r_value = json_decode(scope);
  if (r_value.is_error()) {
    LOG(INFO) << "Can't parse JSON object: " << r_value.error();
    return Status::Error(400, "Can't parse BotCommandScope JSON object");
  }

  auto r_scope = get_bot_command_scope(r_value.move_as_ok());
  if (r_scope.is_error()) {
    return Status::Error(400, PSLICE() << "Can't parse BotCommandScope: " << r_scope.error().message());
  }
  return r_scope.move_as_ok();
}

td::Result<td_api::object_ptr<td_api::botCommand>> Client::get_bot_command(JsonValue &&value) {
  if (value.type() != JsonValue::Type::Object) {
    return Status::Error(400, "expected an Object");
  }

  auto &object = value.get_object();

  TRY_RESULT(command, get_json_object_string_field(object, "command", false));
  TRY_RESULT(description, get_json_object_string_field(object, "description", false));

  return make_object<td_api::botCommand>(command, description);
}

td::Result<td::vector<td_api::object_ptr<td_api::botCommand>>> Client::get_bot_commands(const Query *query) {
  auto commands = query->arg("commands");
  if (commands.empty()) {
    return td::vector<object_ptr<td_api::botCommand>>();
  }
  LOG(INFO) << "Parsing JSON object: " << commands;
  auto r_value = json_decode(commands);
  if (r_value.is_error()) {
    LOG(INFO) << "Can't parse JSON object: " << r_value.error();
    return Status::Error(400, "Can't parse commands JSON object");
  }

  auto value = r_value.move_as_ok();
  if (value.type() != JsonValue::Type::Array) {
    return Status::Error(400, "Expected an Array of BotCommand");
  }

  td::vector<object_ptr<td_api::botCommand>> bot_commands;
  for (auto &command : value.get_array()) {
    auto r_bot_command = get_bot_command(std::move(command));
    if (r_bot_command.is_error()) {
      return Status::Error(400, PSLICE() << "Can't parse BotCommand: " << r_bot_command.error().message());
    }
    bot_commands.push_back(r_bot_command.move_as_ok());
  }
  return std::move(bot_commands);
}

td::Result<td_api::object_ptr<td_api::maskPosition>> Client::get_mask_position(JsonValue &&value) {
  if (value.type() != JsonValue::Type::Object) {
    return Status::Error(400, "MaskPosition must be an Object");
  }

  auto &object = value.get_object();

  TRY_RESULT(point_str, get_json_object_string_field(object, "point", false));
  point_str = td::trim(td::to_lower(point_str));
  int32 point;
  for (point = 0; point < MASK_POINTS_SIZE; point++) {
    if (MASK_POINTS[point] == point_str) {
      break;
    }
  }
  if (point == MASK_POINTS_SIZE) {
    return Status::Error(400, "Wrong point specified in MaskPosition");
  }

  TRY_RESULT(x_shift, get_json_object_double_field(object, "x_shift", false));
  TRY_RESULT(y_shift, get_json_object_double_field(object, "y_shift", false));
  TRY_RESULT(scale, get_json_object_double_field(object, "scale", false));

  return make_object<td_api::maskPosition>(mask_index_to_point(point), x_shift, y_shift, scale);
}

td::int32 Client::mask_point_to_index(const object_ptr<td_api::MaskPoint> &mask_point) {
  CHECK(mask_point != nullptr);
  switch (mask_point->get_id()) {
    case td_api::maskPointForehead::ID:
      return 0;
    case td_api::maskPointEyes::ID:
      return 1;
    case td_api::maskPointMouth::ID:
      return 2;
    case td_api::maskPointChin::ID:
      return 3;
    default:
      UNREACHABLE();
      return -1;
  }
}

td_api::object_ptr<td_api::MaskPoint> Client::mask_index_to_point(int32 index) {
  switch (index) {
    case 0:
      return make_object<td_api::maskPointForehead>();
    case 1:
      return make_object<td_api::maskPointEyes>();
    case 2:
      return make_object<td_api::maskPointMouth>();
    case 3:
      return make_object<td_api::maskPointChin>();
    default:
      UNREACHABLE();
      return nullptr;
  }
}

td::Result<td_api::object_ptr<td_api::maskPosition>> Client::get_mask_position(const Query *query, Slice field_name) {
  auto mask_position = query->arg(field_name);
  if (mask_position.empty()) {
    return nullptr;
  }

  LOG(INFO) << "Parsing JSON object: " << mask_position;
  auto r_value = json_decode(mask_position);
  if (r_value.is_error()) {
    LOG(INFO) << "Can't parse JSON object: " << r_value.error();
    return Status::Error(400, "Can't parse mask position JSON object");
  }

  auto r_mask_position = get_mask_position(r_value.move_as_ok());
  if (r_mask_position.is_error()) {
    return Status::Error(400, PSLICE() << "Can't parse mask position: " << r_mask_position.error().message());
  }
  return r_mask_position.move_as_ok();
}

td::Result<td::vector<td_api::object_ptr<td_api::InputSticker>>> Client::get_input_stickers(const Query *query) const {
  auto emojis = query->arg("emojis");

  td::vector<object_ptr<td_api::InputSticker>> stickers;
  auto sticker = get_input_file(query, "png_sticker");
  if (sticker != nullptr) {
    TRY_RESULT(mask_position, get_mask_position(query, "mask_position"));
    stickers.push_back(
        make_object<td_api::inputStickerStatic>(std::move(sticker), emojis.str(), std::move(mask_position)));
  } else {
    sticker = get_input_file(query, "tgs_sticker", true);
    if (sticker == nullptr) {
      if (query->arg("tgs_sticker").empty()) {
        return Status::Error(400, "Bad Request: there is no sticker file in the request");
      }
      return Status::Error(400, "Bad Request: TGS sticker must be uploaded as an InputFile");
    }

    stickers.push_back(make_object<td_api::inputStickerAnimated>(std::move(sticker), emojis.str()));
  }
  CHECK(stickers.size() == 1);

  return std::move(stickers);
}

td::Result<td::string> Client::get_passport_element_hash(Slice encoded_hash) {
  if (!td::is_base64(encoded_hash)) {
    return Status::Error(400, "hash isn't a valid base64-encoded string");
  }
  return td::base64_decode(encoded_hash).move_as_ok();
}

td::Result<td_api::object_ptr<td_api::InputPassportElementErrorSource>> Client::get_passport_element_error_source(
    td::JsonObject &object) {
  TRY_RESULT(source, get_json_object_string_field(object, "source"));

  if (source.empty() || source == "unspecified") {
    TRY_RESULT(element_hash, get_json_object_string_field(object, "element_hash", false));
    TRY_RESULT(hash, get_passport_element_hash(element_hash));

    return make_object<td_api::inputPassportElementErrorSourceUnspecified>(hash);
  }
  if (source == "data") {
    TRY_RESULT(data_hash, get_json_object_string_field(object, "data_hash", false));
    TRY_RESULT(hash, get_passport_element_hash(data_hash));

    TRY_RESULT(field_name, get_json_object_string_field(object, "field_name", false));
    return make_object<td_api::inputPassportElementErrorSourceDataField>(field_name, hash);
  }
  if (source == "file" || source == "selfie" || source == "translation_file" || source == "front_side" ||
      source == "reverse_side") {
    TRY_RESULT(file_hash, get_json_object_string_field(object, "file_hash", false));
    TRY_RESULT(hash, get_passport_element_hash(file_hash));

    if (source == "front_side") {
      return make_object<td_api::inputPassportElementErrorSourceFrontSide>(hash);
    }
    if (source == "reverse_side") {
      return make_object<td_api::inputPassportElementErrorSourceReverseSide>(hash);
    }
    if (source == "selfie") {
      return make_object<td_api::inputPassportElementErrorSourceSelfie>(hash);
    }
    if (source == "translation_file") {
      return make_object<td_api::inputPassportElementErrorSourceTranslationFile>(hash);
    }
    if (source == "file") {
      return make_object<td_api::inputPassportElementErrorSourceFile>(hash);
    }
    UNREACHABLE();
  }
  if (source == "files" || source == "translation_files") {
    td::vector<td::string> input_hashes;
    TRY_RESULT(file_hashes, get_json_object_field(object, "file_hashes", JsonValue::Type::Array, false));
    for (auto &input_hash : file_hashes.get_array()) {
      if (input_hash.type() != JsonValue::Type::String) {
        return Status::Error(400, "hash must be a string");
      }
      TRY_RESULT(hash, get_passport_element_hash(input_hash.get_string()));
      input_hashes.push_back(std::move(hash));
    }
    if (source == "files") {
      return make_object<td_api::inputPassportElementErrorSourceFiles>(std::move(input_hashes));
    }
    if (source == "translation_files") {
      return make_object<td_api::inputPassportElementErrorSourceTranslationFiles>(std::move(input_hashes));
    }
    UNREACHABLE();
  }
  return Status::Error(400, "wrong source specified");
}

td::Result<td_api::object_ptr<td_api::inputPassportElementError>> Client::get_passport_element_error(
    JsonValue &&value) {
  if (value.type() != JsonValue::Type::Object) {
    return Status::Error(400, "expected an Object");
  }

  auto &object = value.get_object();

  TRY_RESULT(input_type, get_json_object_string_field(object, "type", false));
  auto type = get_passport_element_type(input_type);
  if (type == nullptr) {
    return Status::Error(400, "wrong Telegram Passport element type specified");
  }
  TRY_RESULT(message, get_json_object_string_field(object, "message", false));
  TRY_RESULT(source, get_passport_element_error_source(object));

  return make_object<td_api::inputPassportElementError>(std::move(type), message, std::move(source));
}

td::Result<td::vector<td_api::object_ptr<td_api::inputPassportElementError>>> Client::get_passport_element_errors(
    const Query *query) {
  auto input_errors = query->arg("errors");
  LOG(INFO) << "Parsing JSON object: " << input_errors;
  auto r_value = json_decode(input_errors);
  if (r_value.is_error()) {
    LOG(INFO) << "Can't parse JSON object: " << r_value.error();
    return Status::Error(400, "Can't parse errors JSON object");
  }

  auto value = r_value.move_as_ok();
  if (value.type() != JsonValue::Type::Array) {
    return Status::Error(400, "Expected an Array of PassportElementError");
  }

  td::vector<object_ptr<td_api::inputPassportElementError>> errors;
  for (auto &input_error : value.get_array()) {
    auto r_error = get_passport_element_error(std::move(input_error));
    if (r_error.is_error()) {
      return Status::Error(400, PSLICE() << "Can't parse PassportElementError: " << r_error.error().message());
    }
    errors.push_back(r_error.move_as_ok());
  }
  return std::move(errors);
}

JsonValue Client::get_input_entities(const Query *query, Slice field_name) {
  auto entities = query->arg(field_name);
  if (!entities.empty()) {
    auto r_value = json_decode(entities);
    if (r_value.is_ok()) {
      return r_value.move_as_ok();
    }

    LOG(INFO) << "Can't parse entities JSON object: " << r_value.error();
  }

  return JsonValue();
}

td::Result<td_api::object_ptr<td_api::formattedText>> Client::get_caption(const Query *query) {
  return get_formatted_text(query->arg("caption").str(), query->arg("parse_mode").str(),
                            get_input_entities(query, "caption_entities"));
}

td::Result<td_api::object_ptr<td_api::TextEntityType>> Client::get_text_entity_type(td::JsonObject &object) {
  TRY_RESULT(type, get_json_object_string_field(object, "type", false));
  if (type.empty()) {
    return Status::Error("Type is not specified");
  }

  if (type == "bold") {
    return make_object<td_api::textEntityTypeBold>();
  }
  if (type == "italic") {
    return make_object<td_api::textEntityTypeItalic>();
  }
  if (type == "underline") {
    return make_object<td_api::textEntityTypeUnderline>();
  }
  if (type == "strikethrough") {
    return make_object<td_api::textEntityTypeStrikethrough>();
  }
  if (type == "code") {
    return make_object<td_api::textEntityTypeCode>();
  }
  if (type == "pre") {
    TRY_RESULT(language, get_json_object_string_field(object, "language"));
    if (language.empty()) {
      return make_object<td_api::textEntityTypePre>();
    }
    return make_object<td_api::textEntityTypePreCode>(language);
  }
  if (type == "text_link") {
    TRY_RESULT(url, get_json_object_string_field(object, "url", false));
    return make_object<td_api::textEntityTypeTextUrl>(url);
  }
  if (type == "text_mention") {
    TRY_RESULT(user, get_json_object_field(object, "user", JsonValue::Type::Object, false));
    CHECK(user.type() == JsonValue::Type::Object);
    TRY_RESULT(user_id, get_json_object_int_field(user.get_object(), "id", false));
    return make_object<td_api::textEntityTypeMentionName>(user_id);
  }
  if (type == "mention" || type == "hashtag" || type == "cashtag" || type == "bot_command" || type == "url" ||
      type == "email" || type == "phone_number" || type == "bank_card_number") {
    return nullptr;
  }

  return Status::Error("Unsupported type specified");
}

td::Result<td_api::object_ptr<td_api::textEntity>> Client::get_text_entity(JsonValue &&value) {
  if (value.type() != JsonValue::Type::Object) {
    return Status::Error(400, "expected an Object");
  }

  auto &object = value.get_object();
  TRY_RESULT(offset, get_json_object_int_field(object, "offset", false));
  TRY_RESULT(length, get_json_object_int_field(object, "length", false));
  TRY_RESULT(type, get_text_entity_type(object));

  if (type == nullptr) {
    return nullptr;
  }

  return make_object<td_api::textEntity>(offset, length, std::move(type));
}

td::Result<td_api::object_ptr<td_api::formattedText>> Client::get_formatted_text(td::string text, td::string parse_mode,
                                                                                 JsonValue &&input_entities) {
  td::to_lower_inplace(parse_mode);
  if (!text.empty() && !parse_mode.empty() && parse_mode != "none") {
    object_ptr<td_api::TextParseMode> text_parse_mode;
    if (parse_mode == "markdown") {
      text_parse_mode = make_object<td_api::textParseModeMarkdown>(1);
    } else if (parse_mode == "markdownv2") {
      text_parse_mode = make_object<td_api::textParseModeMarkdown>(2);
    } else if (parse_mode == "html") {
      text_parse_mode = make_object<td_api::textParseModeHTML>();
    } else {
      return Status::Error(400, "Unsupported parse_mode");
    }

    auto parsed_text = execute(make_object<td_api::parseTextEntities>(text, std::move(text_parse_mode)));
    if (parsed_text->get_id() == td_api::error::ID) {
      auto error = move_object_as<td_api::error>(parsed_text);
      return Status::Error(error->code_, error->message_);
    }

    CHECK(parsed_text->get_id() == td_api::formattedText::ID);
    return move_object_as<td_api::formattedText>(parsed_text);
  }

  td::vector<object_ptr<td_api::textEntity>> entities;
  if (input_entities.type() == JsonValue::Type::Array) {
    for (auto &input_entity : input_entities.get_array()) {
      auto r_entity = get_text_entity(std::move(input_entity));
      if (r_entity.is_error()) {
        return Status::Error(400, PSLICE() << "Can't parse MessageEntity: " << r_entity.error().message());
      }
      if (r_entity.ok() == nullptr) {
        continue;
      }
      entities.push_back(r_entity.move_as_ok());
    }
  }

  return make_object<td_api::formattedText>(text, std::move(entities));
}

td::Result<td_api::object_ptr<td_api::inputMessageText>> Client::get_input_message_text(const Query *query) {
  return get_input_message_text(query->arg("text").str(), to_bool(query->arg("disable_web_page_preview")),
                                query->arg("parse_mode").str(), get_input_entities(query, "entities"));
}

td::Result<td_api::object_ptr<td_api::inputMessageText>> Client::get_input_message_text(td::string text,
                                                                                        bool disable_web_page_preview,
                                                                                        td::string parse_mode,
                                                                                        JsonValue &&input_entities) {
  if (text.empty()) {
    return Status::Error(400, "Message text is empty");
  }

  TRY_RESULT(formatted_text, get_formatted_text(std::move(text), std::move(parse_mode), std::move(input_entities)));

  return make_object<td_api::inputMessageText>(std::move(formatted_text), disable_web_page_preview, false);
}

td::Result<td_api::object_ptr<td_api::location>> Client::get_location(const Query *query) {
  auto latitude = trim(query->arg("latitude"));
  if (latitude.empty()) {
    return Status::Error(400, "Bad Request: latitude is empty");
  }
  auto longitude = trim(query->arg("longitude"));
  if (longitude.empty()) {
    return Status::Error(400, "Bad Request: longitude is empty");
  }
  auto horizontal_accuracy = trim(query->arg("horizontal_accuracy"));

  return make_object<td_api::location>(td::to_double(latitude), td::to_double(longitude),
                                       td::to_double(horizontal_accuracy));
}

td::Result<td_api::object_ptr<td_api::chatPermissions>> Client::get_chat_permissions(const Query *query,
                                                                                     bool &allow_legacy) {
  auto can_send_messages = false;
  auto can_send_media_messages = false;
  auto can_send_polls = false;
  auto can_send_other_messages = false;
  auto can_add_web_page_previews = false;
  auto can_change_info = false;
  auto can_invite_users = false;
  auto can_pin_messages = false;

  if (query->has_arg("permissions")) {
    allow_legacy = false;

    auto r_value = json_decode(query->arg("permissions"));
    if (r_value.is_error()) {
      LOG(INFO) << "Can't parse JSON object: " << r_value.error();
      return Status::Error(400, "Can't parse permissions JSON object");
    }

    auto value = r_value.move_as_ok();
    if (value.type() != JsonValue::Type::Object) {
      return Status::Error(400, "Object expected as permissions");
    }
    auto &object = value.get_object();

    auto status = [&] {
      TRY_RESULT_ASSIGN(can_send_messages, get_json_object_bool_field(object, "can_send_messages"));
      TRY_RESULT_ASSIGN(can_send_media_messages, get_json_object_bool_field(object, "can_send_media_messages"));
      TRY_RESULT_ASSIGN(can_send_polls, get_json_object_bool_field(object, "can_send_polls"));
      TRY_RESULT_ASSIGN(can_send_other_messages, get_json_object_bool_field(object, "can_send_other_messages"));
      TRY_RESULT_ASSIGN(can_add_web_page_previews, get_json_object_bool_field(object, "can_add_web_page_previews"));
      TRY_RESULT_ASSIGN(can_change_info, get_json_object_bool_field(object, "can_change_info"));
      TRY_RESULT_ASSIGN(can_invite_users, get_json_object_bool_field(object, "can_invite_users"));
      TRY_RESULT_ASSIGN(can_pin_messages, get_json_object_bool_field(object, "can_pin_messages"));
      return Status::OK();
    }();

    if (status.is_error()) {
      return Status::Error(400, PSLICE() << "Can't parse chat permissions: " << status.error().message());
    }
  } else if (allow_legacy) {
    allow_legacy = false;

    can_send_messages = to_bool(query->arg("can_send_messages"));
    can_send_media_messages = to_bool(query->arg("can_send_media_messages"));
    can_send_other_messages = to_bool(query->arg("can_send_other_messages"));
    can_add_web_page_previews = to_bool(query->arg("can_add_web_page_previews"));

    if (can_send_messages && can_send_media_messages && can_send_other_messages && can_add_web_page_previews) {
      // legacy unrestrict
      can_send_polls = true;
      can_change_info = true;
      can_invite_users = true;
      can_pin_messages = true;
    } else if (query->has_arg("can_send_messages") || query->has_arg("can_send_media_messages") ||
               query->has_arg("can_send_other_messages") || query->has_arg("can_add_web_page_previews")) {
      allow_legacy = true;
    }
  }

  if (can_send_other_messages || can_add_web_page_previews) {
    can_send_media_messages = true;
  }
  return make_object<td_api::chatPermissions>(can_send_messages, can_send_media_messages, can_send_polls,
                                              can_send_other_messages, can_add_web_page_previews, can_change_info,
                                              can_invite_users, can_pin_messages);
}

td::Result<td_api::object_ptr<td_api::InputMessageContent>> Client::get_input_media(const Query *query,
                                                                                    JsonValue &&input_media,
                                                                                    bool for_album) const {
  if (input_media.type() != JsonValue::Type::Object) {
    return Status::Error(400, "expected an Object");
  }

  auto &object = input_media.get_object();

  TRY_RESULT(input_caption, get_json_object_string_field(object, "caption"));
  TRY_RESULT(parse_mode, get_json_object_string_field(object, "parse_mode"));
  auto entities = get_json_object_field_force(object, "caption_entities");
  TRY_RESULT(caption, get_formatted_text(std::move(input_caption), std::move(parse_mode), std::move(entities)));
  // TRY_RESULT(ttl, get_json_object_int_field(object, "ttl"));
  int32 ttl = 0;
  TRY_RESULT(media, get_json_object_string_field(object, "media", true));

  auto input_file = get_input_file(query, Slice(), media, false);
  if (input_file == nullptr) {
    return Status::Error(400, "media not found");
  }

  TRY_RESULT(thumbnail, get_json_object_string_field(object, "thumb"));
  object_ptr<td_api::inputThumbnail> input_thumbnail;
  auto thumbanil_input_file = get_input_file(query, thumbnail.empty() ? Slice("thumb") : Slice(), thumbnail, true);
  if (thumbanil_input_file != nullptr) {
    input_thumbnail = make_object<td_api::inputThumbnail>(std::move(thumbanil_input_file), 0, 0);
  }

  TRY_RESULT(type, get_json_object_string_field(object, "type", false));
  if (type == "photo") {
    return make_object<td_api::inputMessagePhoto>(std::move(input_file), std::move(input_thumbnail),
                                                  td::vector<int32>(), 0, 0, std::move(caption), ttl);
  }
  if (type == "video") {
    TRY_RESULT(width, get_json_object_int_field(object, "width"));
    TRY_RESULT(height, get_json_object_int_field(object, "height"));
    TRY_RESULT(duration, get_json_object_int_field(object, "duration"));
    TRY_RESULT(supports_streaming, get_json_object_bool_field(object, "supports_streaming"));
    width = td::clamp(width, 0, MAX_LENGTH);
    height = td::clamp(height, 0, MAX_LENGTH);
    duration = td::clamp(duration, 0, MAX_DURATION);

    return make_object<td_api::inputMessageVideo>(std::move(input_file), std::move(input_thumbnail),
                                                  td::vector<int32>(), duration, width, height, supports_streaming,
                                                  std::move(caption), ttl);
  }
  if (for_album && type == "animation") {
    return Status::Error(400, PSLICE() << "type \"" << type << "\" can't be used in sendMediaGroup");
  }
  if (type == "animation") {
    TRY_RESULT(width, get_json_object_int_field(object, "width"));
    TRY_RESULT(height, get_json_object_int_field(object, "height"));
    TRY_RESULT(duration, get_json_object_int_field(object, "duration"));
    width = td::clamp(width, 0, MAX_LENGTH);
    height = td::clamp(height, 0, MAX_LENGTH);
    duration = td::clamp(duration, 0, MAX_DURATION);
    return make_object<td_api::inputMessageAnimation>(std::move(input_file), std::move(input_thumbnail),
                                                      td::vector<int32>(), duration, width, height, std::move(caption));
  }
  if (type == "audio") {
    TRY_RESULT(duration, get_json_object_int_field(object, "duration"));
    TRY_RESULT(title, get_json_object_string_field(object, "title"));
    TRY_RESULT(performer, get_json_object_string_field(object, "performer"));
    duration = td::clamp(duration, 0, MAX_DURATION);
    return make_object<td_api::inputMessageAudio>(std::move(input_file), std::move(input_thumbnail), duration, title,
                                                  performer, std::move(caption));
  }
  if (type == "document") {
    TRY_RESULT(disable_content_type_detection, get_json_object_bool_field(object, "disable_content_type_detection"));
    return make_object<td_api::inputMessageDocument>(std::move(input_file), std::move(input_thumbnail),
                                                     disable_content_type_detection || for_album, std::move(caption));
  }

  return Status::Error(400, PSLICE() << "type \"" << type << "\" is unsupported");
}

td::Result<td_api::object_ptr<td_api::InputMessageContent>> Client::get_input_media(const Query *query,
                                                                                    Slice field_name,
                                                                                    bool for_album) const {
  TRY_RESULT(media, get_required_string_arg(query, field_name));

  LOG(INFO) << "Parsing JSON object: " << media;
  auto r_value = json_decode(media);
  if (r_value.is_error()) {
    LOG(INFO) << "Can't parse JSON object: " << r_value.error();
    return Status::Error(400, "Can't parse input media JSON object");
  }

  auto r_input_message_content = get_input_media(query, r_value.move_as_ok(), for_album);
  if (r_input_message_content.is_error()) {
    return Status::Error(400, PSLICE() << "Can't parse InputMedia: " << r_input_message_content.error().message());
  }
  return r_input_message_content.move_as_ok();
}

td::Result<td::vector<td_api::object_ptr<td_api::InputMessageContent>>> Client::get_input_message_contents(
    const Query *query, Slice field_name) const {
  TRY_RESULT(media, get_required_string_arg(query, field_name));

  LOG(INFO) << "Parsing JSON object: " << media;
  auto r_value = json_decode(media);
  if (r_value.is_error()) {
    LOG(INFO) << "Can't parse JSON object: " << r_value.error();
    return Status::Error(400, "Can't parse media JSON object");
  }

  return get_input_message_contents(query, r_value.move_as_ok());
}

td::Result<td::vector<td_api::object_ptr<td_api::InputMessageContent>>> Client::get_input_message_contents(
    const Query *query, JsonValue &&value) const {
  if (value.type() != JsonValue::Type::Array) {
    return Status::Error(400, "Expected an Array of InputMedia");
  }

  td::vector<object_ptr<td_api::InputMessageContent>> contents;
  for (auto &input_media : value.get_array()) {
    TRY_RESULT(input_message_content, get_input_media(query, std::move(input_media), true));
    contents.push_back(std::move(input_message_content));
  }
  return std::move(contents);
}

td::Result<td::vector<td::string>> Client::get_poll_options(const Query *query) {
  auto input_options = query->arg("options");
  LOG(INFO) << "Parsing JSON object: " << input_options;
  auto r_value = json_decode(input_options);
  if (r_value.is_error()) {
    LOG(INFO) << "Can't parse JSON object: " << r_value.error();
    return Status::Error(400, "Can't parse options JSON object");
  }

  auto value = r_value.move_as_ok();
  if (value.type() != JsonValue::Type::Array) {
    return Status::Error(400, "Expected an Array of String as options");
  }

  td::vector<td::string> options;
  for (auto &input_option : value.get_array()) {
    if (input_option.type() != JsonValue::Type::String) {
      return Status::Error(400, "Expected an option to be of type String");
    }
    options.push_back(input_option.get_string().str());
  }
  return std::move(options);
}

td::int32 Client::get_integer_arg(const Query *query, Slice field_name, int32 default_value, int32 min_value,
                                  int32 max_value) {
  auto s_arg = query->arg(field_name);
  auto value = s_arg.empty() ? default_value : td::to_integer<int32>(s_arg);
  return td::clamp(value, min_value, max_value);
}

td::Result<td::MutableSlice> Client::get_required_string_arg(const Query *query, Slice field_name) {
  auto s_arg = query->arg(field_name);
  if (s_arg.empty()) {
    return Status::Error(400, PSLICE() << "Parameter \"" << field_name << "\" is required");
  }
  return s_arg;
}

td::int64 Client::get_message_id(const Query *query, Slice field_name) {
  auto s_arg = query->arg(field_name);
  if (s_arg.empty()) {
    return 0;
  }

  int arg = td::to_integer<int32>(s_arg);
  if (arg < 0) {
    return 0;
  }

  return as_tdlib_message_id(arg);
}

td::Result<td::Slice> Client::get_inline_message_id(const Query *query, Slice field_name) {
  auto s_arg = query->arg(field_name);
  if (s_arg.empty()) {
    return Status::Error(400, "Message identifier is not specified");
  }
  return s_arg;
}

td::Result<td::int32> Client::get_user_id(const Query *query, Slice field_name) {
  int32 user_id = get_integer_arg(query, field_name, 0, 0);
  if (user_id == 0) {
    return Status::Error(400, PSLICE() << "Invalid " << field_name << " specified");
  }
  return user_id;
}

td::int64 Client::extract_yet_unsent_message_query_id(int64 chat_id, int64 message_id,
                                                      bool *is_reply_to_message_deleted) {
  auto yet_unsent_message_it = yet_unsent_messages_.find({chat_id, message_id});
  CHECK(yet_unsent_message_it != yet_unsent_messages_.end());

  auto reply_to_message_id = yet_unsent_message_it->second.reply_to_message_id;
  if (is_reply_to_message_deleted != nullptr && yet_unsent_message_it->second.is_reply_to_message_deleted) {
    *is_reply_to_message_deleted = true;
  }
  auto query_id = yet_unsent_message_it->second.send_message_query_id;

  yet_unsent_messages_.erase(yet_unsent_message_it);

  if (reply_to_message_id > 0) {
    auto it = yet_unsent_reply_message_ids_.find({chat_id, reply_to_message_id});
    CHECK(it != yet_unsent_reply_message_ids_.end());
    auto erased_count = it->second.erase(message_id);
    CHECK(erased_count > 0);
    if (it->second.empty()) {
      yet_unsent_reply_message_ids_.erase(it);
    }
  }

  return query_id;
}

void Client::on_message_send_succeeded(object_ptr<td_api::message> &&message, int64 old_message_id) {
  auto full_message_id = add_message(std::move(message), true);

  int64 chat_id = full_message_id.chat_id;
  int64 new_message_id = full_message_id.message_id;
  CHECK(new_message_id > 0);

  auto message_info = get_message(chat_id, new_message_id);
  CHECK(message_info != nullptr);
  message_info->is_content_changed = false;

  auto query_id =
      extract_yet_unsent_message_query_id(chat_id, old_message_id, &message_info->is_reply_to_message_deleted);
  auto &query = pending_send_message_queries_[query_id];
  if (query.is_multisend) {
    query.messages.push_back(td::json_encode<td::string>(JsonMessage(message_info, true, "sent message", this)));
    query.awaited_messages--;

    if (query.awaited_messages == 0) {
      if (query.error == nullptr) {
        answer_query(JsonMessages(query.messages), std::move(query.query));
      } else {
        fail_query_with_error(std::move(query.query), std::move(query.error));
      }
      pending_send_message_queries_.erase(query_id);
    }
  } else {
    CHECK(query.awaited_messages == 1);
    if (query.query->method() == "copymessage") {
      answer_query(JsonMessageId(new_message_id), std::move(query.query));
    } else {
      answer_query(JsonMessage(message_info, true, "sent message", this), std::move(query.query));
    }
    pending_send_message_queries_.erase(query_id);
  }
}

void Client::on_message_send_failed(int64 chat_id, int64 old_message_id, int64 new_message_id, Status result) {
  auto error = make_object<td_api::error>(result.code(), result.message().str());

  auto query_id = extract_yet_unsent_message_query_id(chat_id, old_message_id, nullptr);
  auto &query = pending_send_message_queries_[query_id];
  if (query.is_multisend) {
    if (query.error == nullptr) {
      query.error = std::move(error);
    }
    query.awaited_messages--;

    if (query.awaited_messages == 0) {
      fail_query_with_error(std::move(query.query), std::move(query.error));
      pending_send_message_queries_.erase(query_id);
    }
  } else {
    CHECK(query.awaited_messages == 1);
    fail_query_with_error(std::move(query.query), std::move(error));
    pending_send_message_queries_.erase(query_id);
  }

  if (new_message_id != 0 && !logging_out_ && !closing_) {
    send_request(make_object<td_api::deleteMessages>(chat_id, td::vector<int64>{new_message_id}, false),
                 std::make_unique<TdOnDeleteFailedToSendMessageCallback>(this, chat_id, new_message_id));
  }
}

void Client::on_cmd(PromisedQueryPtr query) {
  LOG(DEBUG) << "Process query " << *query;
  if (!td_client_.empty()) {
    if (query->method() == "close") {
      auto retry_after = static_cast<int>(10 * 60 - (td::Time::now() - start_time_));
      if (retry_after > 0 && start_time_ > parameters_->start_time_ + 10 * 60) {
        return query->set_retry_after_error(retry_after);
      }
      need_close_ = true;
      return do_send_request(make_object<td_api::close>(), std::make_unique<TdOnOkQueryCallback>(std::move(query)));
    }
    if (query->method() == "logout") {
      clear_tqueue_ = true;
      return do_send_request(make_object<td_api::logOut>(), std::make_unique<TdOnOkQueryCallback>(std::move(query)));
    }
  }

  if (logging_out_) {
    return fail_query(LOGGING_OUT_ERROR_CODE, get_logging_out_error_description(), std::move(query));
  }
  if (closing_) {
    return fail_query(CLOSING_ERROR_CODE, CLOSING_ERROR_DESCRIPTION, std::move(query));
  }
  CHECK(was_authorized_);

  unresolved_bot_usernames_.clear();

  auto method_it = methods_.find(query->method().str());
  if (method_it == methods_.end()) {
    return fail_query(404, "Not Found: method not found", std::move(query));
  }

  auto result = (this->*(method_it->second))(query);
  if (result.is_error()) {
    fail_query_with_error(std::move(query), result.code(), result.message());
  }
}

td::Status Client::process_get_me_query(PromisedQueryPtr &query) {
  answer_query(JsonUser(my_id_, this, true), std::move(query));
  return Status::OK();
}

td::Status Client::process_get_my_commands_query(PromisedQueryPtr &query) {
  TRY_RESULT(scope, get_bot_command_scope(query.get()));

  check_bot_command_scope(std::move(scope), std::move(query),
                          [this](object_ptr<td_api::BotCommandScope> &&scope, PromisedQueryPtr query) mutable {
                            auto language_code = query->arg("language_code").str();
                            send_request(make_object<td_api::getCommands>(std::move(scope), language_code),
                                         std::make_unique<TdOnGetMyCommandsCallback>(std::move(query)));
                          });
  return Status::OK();
}

td::Status Client::process_set_my_commands_query(PromisedQueryPtr &query) {
  TRY_RESULT(bot_commands, get_bot_commands(query.get()));
  TRY_RESULT(scope, get_bot_command_scope(query.get()));

  check_bot_command_scope(
      std::move(scope), std::move(query),
      [this, bot_commands = std::move(bot_commands)](object_ptr<td_api::BotCommandScope> &&scope,
                                                     PromisedQueryPtr query) mutable {
        auto language_code = query->arg("language_code").str();
        send_request(make_object<td_api::setCommands>(std::move(scope), language_code, std::move(bot_commands)),
                     std::make_unique<TdOnOkQueryCallback>(std::move(query)));
      });
  return Status::OK();
}

td::Status Client::process_delete_my_commands_query(PromisedQueryPtr &query) {
  TRY_RESULT(scope, get_bot_command_scope(query.get()));

  check_bot_command_scope(std::move(scope), std::move(query),
                          [this](object_ptr<td_api::BotCommandScope> &&scope, PromisedQueryPtr query) mutable {
                            auto language_code = query->arg("language_code").str();
                            send_request(make_object<td_api::deleteCommands>(std::move(scope), language_code),
                                         std::make_unique<TdOnOkQueryCallback>(std::move(query)));
                          });
  return Status::OK();
}

td::Status Client::process_get_user_profile_photos_query(PromisedQueryPtr &query) {
  TRY_RESULT(user_id, get_user_id(query.get()));
  int32 offset = get_integer_arg(query.get(), "offset", 0, 0);
  int32 limit = get_integer_arg(query.get(), "limit", 100, 1, 100);

  check_user(user_id, std::move(query), [this, user_id, offset, limit](PromisedQueryPtr query) {
    send_request(make_object<td_api::getUserProfilePhotos>(user_id, offset, limit),
                 std::make_unique<TdOnGetUserProfilePhotosCallback>(this, std::move(query)));
  });
  return Status::OK();
}

td::Status Client::process_send_message_query(PromisedQueryPtr &query) {
  TRY_RESULT(input_message_text, get_input_message_text(query.get()));
  do_send_message(std::move(input_message_text), std::move(query));
  return Status::OK();
}

td::Status Client::process_send_animation_query(PromisedQueryPtr &query) {
  auto animation = get_input_file(query.get(), "animation");
  if (animation == nullptr) {
    return Status::Error(400, "There is no animation in the request");
  }
  auto thumbnail = get_input_thumbnail(query.get(), "thumb");
  int32 duration = get_integer_arg(query.get(), "duration", 0, 0, MAX_DURATION);
  int32 width = get_integer_arg(query.get(), "width", 0, 0, MAX_LENGTH);
  int32 height = get_integer_arg(query.get(), "height", 0, 0, MAX_LENGTH);
  TRY_RESULT(caption, get_caption(query.get()));
  do_send_message(
      make_object<td_api::inputMessageAnimation>(std::move(animation), std::move(thumbnail), td::vector<int32>(),
                                                 duration, width, height, std::move(caption)),
      std::move(query));
  return Status::OK();
}

td::Status Client::process_send_audio_query(PromisedQueryPtr &query) {
  auto audio = get_input_file(query.get(), "audio");
  if (audio == nullptr) {
    return Status::Error(400, "There is no audio in the request");
  }
  auto thumbnail = get_input_thumbnail(query.get(), "thumb");
  int32 duration = get_integer_arg(query.get(), "duration", 0, 0, MAX_DURATION);
  auto title = query->arg("title").str();
  auto performer = query->arg("performer").str();
  TRY_RESULT(caption, get_caption(query.get()));
  do_send_message(make_object<td_api::inputMessageAudio>(std::move(audio), std::move(thumbnail), duration, title,
                                                         performer, std::move(caption)),
                  std::move(query));
  return Status::OK();
}

td::Status Client::process_send_dice_query(PromisedQueryPtr &query) {
  auto emoji = query->arg("emoji");
  do_send_message(make_object<td_api::inputMessageDice>(emoji.str(), false), std::move(query));
  return Status::OK();
}

td::Status Client::process_send_document_query(PromisedQueryPtr &query) {
  auto document = get_input_file(query.get(), "document");
  if (document == nullptr) {
    return Status::Error(400, "There is no document in the request");
  }
  auto thumbnail = get_input_thumbnail(query.get(), "thumb");
  TRY_RESULT(caption, get_caption(query.get()));
  bool disable_content_type_detection = to_bool(query->arg("disable_content_type_detection"));
  do_send_message(make_object<td_api::inputMessageDocument>(std::move(document), std::move(thumbnail),
                                                            disable_content_type_detection, std::move(caption)),
                  std::move(query));
  return Status::OK();
}

td::Status Client::process_send_photo_query(PromisedQueryPtr &query) {
  auto photo = get_input_file(query.get(), "photo");
  if (photo == nullptr) {
    return Status::Error(400, "There is no photo in the request");
  }
  TRY_RESULT(caption, get_caption(query.get()));
  auto ttl = 0;
  do_send_message(make_object<td_api::inputMessagePhoto>(std::move(photo), nullptr, td::vector<int32>(), 0, 0,
                                                         std::move(caption), ttl),
                  std::move(query));
  return Status::OK();
}

td::Status Client::process_send_sticker_query(PromisedQueryPtr &query) {
  auto sticker = get_input_file(query.get(), "sticker");
  if (sticker == nullptr) {
    return Status::Error(400, "There is no sticker in the request");
  }
  do_send_message(make_object<td_api::inputMessageSticker>(std::move(sticker), nullptr, 0, 0, td::string()),
                  std::move(query));
  return Status::OK();
}

td::Status Client::process_send_video_query(PromisedQueryPtr &query) {
  auto video = get_input_file(query.get(), "video");
  if (video == nullptr) {
    return Status::Error(400, "There is no video in the request");
  }
  auto thumbnail = get_input_thumbnail(query.get(), "thumb");
  int32 duration = get_integer_arg(query.get(), "duration", 0, 0, MAX_DURATION);
  int32 width = get_integer_arg(query.get(), "width", 0, 0, MAX_LENGTH);
  int32 height = get_integer_arg(query.get(), "height", 0, 0, MAX_LENGTH);
  bool supports_streaming = to_bool(query->arg("supports_streaming"));
  TRY_RESULT(caption, get_caption(query.get()));
  auto ttl = 0;
  do_send_message(
      make_object<td_api::inputMessageVideo>(std::move(video), std::move(thumbnail), td::vector<int32>(), duration,
                                             width, height, supports_streaming, std::move(caption), ttl),
      std::move(query));
  return Status::OK();
}

td::Status Client::process_send_video_note_query(PromisedQueryPtr &query) {
  auto video_note = get_input_file(query.get(), "video_note");
  if (video_note == nullptr) {
    return Status::Error(400, "There is no video note in the request");
  }
  auto thumbnail = get_input_thumbnail(query.get(), "thumb");
  int32 duration = get_integer_arg(query.get(), "duration", 0, 0, MAX_DURATION);
  int32 length = get_integer_arg(query.get(), "length", 0, 0, MAX_LENGTH);
  do_send_message(
      make_object<td_api::inputMessageVideoNote>(std::move(video_note), std::move(thumbnail), duration, length),
      std::move(query));
  return Status::OK();
}

td::Status Client::process_send_voice_query(PromisedQueryPtr &query) {
  auto voice_note = get_input_file(query.get(), "voice");
  if (voice_note == nullptr) {
    return Status::Error(400, "There is no voice in the request");
  }
  int32 duration = get_integer_arg(query.get(), "duration", 0, 0, MAX_DURATION);
  TRY_RESULT(caption, get_caption(query.get()));
  do_send_message(make_object<td_api::inputMessageVoiceNote>(std::move(voice_note), duration, "", std::move(caption)),
                  std::move(query));
  return Status::OK();
}

td::Status Client::process_send_game_query(PromisedQueryPtr &query) {
  TRY_RESULT(game_short_name, get_required_string_arg(query.get(), "game_short_name"));
  do_send_message(make_object<td_api::inputMessageGame>(my_id_, game_short_name.str()), std::move(query));
  return Status::OK();
}

td::Status Client::process_send_invoice_query(PromisedQueryPtr &query) {
  TRY_RESULT(title, get_required_string_arg(query.get(), "title"));
  TRY_RESULT(description, get_required_string_arg(query.get(), "description"));
  TRY_RESULT(payload, get_required_string_arg(query.get(), "payload"));
  if (!td::check_utf8(payload.str())) {
    return Status::Error(400, "The payload must be encoded in UTF-8");
  }
  TRY_RESULT(provider_token, get_required_string_arg(query.get(), "provider_token"));
  auto provider_data = query->arg("provider_data");
  auto start_parameter = query->arg("start_parameter");
  TRY_RESULT(currency, get_required_string_arg(query.get(), "currency"));

  TRY_RESULT(labeled_price_parts, get_required_string_arg(query.get(), "prices"));
  auto r_labeled_price_parts_value = json_decode(labeled_price_parts);
  if (r_labeled_price_parts_value.is_error()) {
    return Status::Error(400, "Can't parse prices JSON object");
  }

  TRY_RESULT(prices, get_labeled_price_parts(r_labeled_price_parts_value.ok_ref()));

  int64 max_tip_amount = 0;
  td::vector<int64> suggested_tip_amounts;
  {
    auto max_tip_amount_str = query->arg("max_tip_amount");
    if (!max_tip_amount_str.empty()) {
      auto r_max_tip_amount = td::to_integer_safe<int64>(max_tip_amount_str);
      if (r_max_tip_amount.is_error()) {
        return Status::Error(400, "Can't parse \"max_tip_amount\" as Number");
      }
      max_tip_amount = r_max_tip_amount.ok();
    }

    auto suggested_tip_amounts_str = query->arg("suggested_tip_amounts");
    if (!suggested_tip_amounts_str.empty()) {
      auto r_suggested_tip_amounts_value = json_decode(suggested_tip_amounts_str);
      if (r_suggested_tip_amounts_value.is_error()) {
        return Status::Error(400, "Can't parse suggested_tip_amounts JSON object");
      }

      TRY_RESULT_ASSIGN(suggested_tip_amounts, get_suggested_tip_amounts(r_suggested_tip_amounts_value.ok_ref()));
    }
  }

  auto photo_url = query->arg("photo_url");
  int32 photo_size = get_integer_arg(query.get(), "photo_size", 0, 0, 1000000000);
  int32 photo_width = get_integer_arg(query.get(), "photo_width", 0, 0, MAX_LENGTH);
  int32 photo_height = get_integer_arg(query.get(), "photo_height", 0, 0, MAX_LENGTH);

  auto need_name = to_bool(query->arg("need_name"));
  auto need_phone_number = to_bool(query->arg("need_phone_number"));
  auto need_email_address = to_bool(query->arg("need_email"));
  auto need_shipping_address = to_bool(query->arg("need_shipping_address"));
  auto send_phone_number_to_provider = to_bool(query->arg("send_phone_number_to_provider"));
  auto send_email_address_to_provider = to_bool(query->arg("send_email_to_provider"));
  auto is_flexible = to_bool(query->arg("is_flexible"));

  do_send_message(make_object<td_api::inputMessageInvoice>(
                      make_object<td_api::invoice>(
                          currency.str(), std::move(prices), max_tip_amount, std::move(suggested_tip_amounts), false,
                          need_name, need_phone_number, need_email_address, need_shipping_address,
                          send_phone_number_to_provider, send_email_address_to_provider, is_flexible),
                      title.str(), description.str(), photo_url.str(), photo_size, photo_width, photo_height,
                      payload.str(), provider_token.str(), provider_data.str(), start_parameter.str()),
                  std::move(query));
  return Status::OK();
}

td::Status Client::process_send_location_query(PromisedQueryPtr &query) {
  TRY_RESULT(location, get_location(query.get()));
  int32 live_period = get_integer_arg(query.get(), "live_period", 0);
  int32 heading = get_integer_arg(query.get(), "heading", 0);
  int32 proximity_alert_radius = get_integer_arg(query.get(), "proximity_alert_radius", 0);

  do_send_message(
      make_object<td_api::inputMessageLocation>(std::move(location), live_period, heading, proximity_alert_radius),
      std::move(query));
  return Status::OK();
}

td::Status Client::process_send_venue_query(PromisedQueryPtr &query) {
  TRY_RESULT(location, get_location(query.get()));

  auto title = query->arg("title");
  auto address = query->arg("address");
  td::string provider;
  td::string venue_id;
  td::string venue_type;

  auto google_place_id = query->arg("google_place_id");
  auto google_place_type = query->arg("google_place_type");
  if (!google_place_id.empty() || !google_place_type.empty()) {
    provider = "gplaces";
    venue_id = google_place_id.str();
    venue_type = google_place_type.str();
  }
  auto foursquare_id = query->arg("foursquare_id");
  auto foursquare_type = query->arg("foursquare_type");
  if (!foursquare_id.empty() || !foursquare_type.empty()) {
    provider = "foursquare";
    venue_id = foursquare_id.str();
    venue_type = foursquare_type.str();
  }

  do_send_message(make_object<td_api::inputMessageVenue>(make_object<td_api::venue>(
                      std::move(location), title.str(), address.str(), provider, venue_id, venue_type)),
                  std::move(query));
  return Status::OK();
}

td::Status Client::process_send_contact_query(PromisedQueryPtr &query) {
  TRY_RESULT(phone_number, get_required_string_arg(query.get(), "phone_number"));
  TRY_RESULT(first_name, get_required_string_arg(query.get(), "first_name"));
  auto last_name = query->arg("last_name");
  auto vcard = query->arg("vcard");
  do_send_message(make_object<td_api::inputMessageContact>(make_object<td_api::contact>(
                      phone_number.str(), first_name.str(), last_name.str(), vcard.str(), 0)),
                  std::move(query));
  return Status::OK();
}

td::Status Client::process_send_poll_query(PromisedQueryPtr &query) {
  auto question = query->arg("question");
  TRY_RESULT(options, get_poll_options(query.get()));
  bool is_anonymous = true;
  if (query->has_arg("is_anonymous")) {
    is_anonymous = to_bool(query->arg("is_anonymous"));
  }

  object_ptr<td_api::PollType> poll_type;
  auto type = query->arg("type");
  if (type == "quiz") {
    TRY_RESULT(explanation,
               get_formatted_text(query->arg("explanation").str(), query->arg("explanation_parse_mode").str(),
                                  get_input_entities(query.get(), "explanation_entities")));

    poll_type = make_object<td_api::pollTypeQuiz>(get_integer_arg(query.get(), "correct_option_id", -1),
                                                  std::move(explanation));
  } else if (type.empty() || type == "regular") {
    poll_type = make_object<td_api::pollTypeRegular>(to_bool(query->arg("allows_multiple_answers")));
  } else {
    return Status::Error(400, "Unsupported poll type specified");
  }
  int32 open_period = get_integer_arg(query.get(), "open_period", 0, 0, 10 * 60);
  int32 close_date = get_integer_arg(query.get(), "close_date", 0);
  auto is_closed = to_bool(query->arg("is_closed"));
  do_send_message(make_object<td_api::inputMessagePoll>(question.str(), std::move(options), is_anonymous,
                                                        std::move(poll_type), open_period, close_date, is_closed),
                  std::move(query));
  return Status::OK();
}

td::Status Client::process_stop_poll_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto message_id = get_message_id(query.get());
  TRY_RESULT(reply_markup, get_reply_markup(query.get()));

  resolve_reply_markup_bot_usernames(
      std::move(reply_markup), std::move(query),
      [this, chat_id = chat_id.str(), message_id](object_ptr<td_api::ReplyMarkup> reply_markup,
                                                  PromisedQueryPtr query) {
        check_message(chat_id, message_id, false, AccessRights::Edit, "message with poll to stop", std::move(query),
                      [this, reply_markup = std::move(reply_markup)](int64 chat_id, int64 message_id,
                                                                     PromisedQueryPtr query) mutable {
                        send_request(
                            make_object<td_api::stopPoll>(chat_id, message_id, std::move(reply_markup)),
                            std::make_unique<TdOnStopPollCallback>(this, chat_id, message_id, std::move(query)));
                      });
      });
  return Status::OK();
}

td::Status Client::process_copy_message_query(PromisedQueryPtr &query) {
  TRY_RESULT(from_chat_id, get_required_string_arg(query.get(), "from_chat_id"));
  auto message_id = get_message_id(query.get());
  bool replace_caption = query->has_arg("caption");
  object_ptr<td_api::formattedText> caption;
  if (replace_caption) {
    TRY_RESULT_ASSIGN(caption, get_caption(query.get()));
  }
  auto options = make_object<td_api::messageCopyOptions>(true, replace_caption, std::move(caption));

  check_message(
      from_chat_id, message_id, false, AccessRights::Read, "message to copy", std::move(query),
      [this, options = std::move(options)](int64 from_chat_id, int64 message_id, PromisedQueryPtr query) mutable {
        do_send_message(make_object<td_api::inputMessageForwarded>(from_chat_id, message_id, false, std::move(options)),
                        std::move(query));
      });
  return Status::OK();
}

td::Status Client::process_forward_message_query(PromisedQueryPtr &query) {
  TRY_RESULT(from_chat_id, get_required_string_arg(query.get(), "from_chat_id"));
  auto message_id = get_message_id(query.get());

  check_message(from_chat_id, message_id, false, AccessRights::Read, "message to forward", std::move(query),
                [this](int64 from_chat_id, int64 message_id, PromisedQueryPtr query) {
                  do_send_message(make_object<td_api::inputMessageForwarded>(from_chat_id, message_id, false, nullptr),
                                  std::move(query));
                });
  return Status::OK();
}

td::Status Client::process_send_media_group_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto reply_to_message_id = get_message_id(query.get(), "reply_to_message_id");
  auto allow_sending_without_reply = to_bool(query->arg("allow_sending_without_reply"));
  auto disable_notification = to_bool(query->arg("disable_notification"));
  // TRY_RESULT(reply_markup, get_reply_markup(query.get()));
  auto reply_markup = nullptr;
  TRY_RESULT(input_message_contents, get_input_message_contents(query.get(), "media"));

  resolve_reply_markup_bot_usernames(
      std::move(reply_markup), std::move(query),
      [this, chat_id = chat_id.str(), reply_to_message_id, allow_sending_without_reply, disable_notification,
       input_message_contents = std::move(input_message_contents)](object_ptr<td_api::ReplyMarkup> reply_markup,
                                                                   PromisedQueryPtr query) mutable {
        auto on_success = [this, disable_notification, input_message_contents = std::move(input_message_contents),
                           reply_markup = std::move(reply_markup)](int64 chat_id, int64 reply_to_message_id,
                                                                   PromisedQueryPtr query) mutable {
          send_request(make_object<td_api::sendMessageAlbum>(chat_id, 0, reply_to_message_id,
                                                             get_message_send_options(disable_notification),
                                                             std::move(input_message_contents)),
                       std::make_unique<TdOnSendMessageAlbumCallback>(this, std::move(query)));
        };
        check_message(chat_id, reply_to_message_id, reply_to_message_id <= 0 || allow_sending_without_reply,
                      AccessRights::Write, "replied message", std::move(query), std::move(on_success));
      });
  return Status::OK();
}

td::Status Client::process_send_chat_action_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  object_ptr<td_api::ChatAction> action = get_chat_action(query.get());
  if (action == nullptr) {
    return Status::Error(400, "Wrong parameter action in request");
  }

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, action = std::move(action)](int64 chat_id, PromisedQueryPtr query) mutable {
               send_request(make_object<td_api::sendChatAction>(chat_id, 0, std::move(action)),
                            std::make_unique<TdOnOkQueryCallback>(std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_edit_message_text_query(PromisedQueryPtr &query) {
  TRY_RESULT(input_message_text, get_input_message_text(query.get()));
  auto chat_id = query->arg("chat_id");
  auto message_id = get_message_id(query.get());
  TRY_RESULT(reply_markup, get_reply_markup(query.get()));

  if (chat_id.empty() && message_id == 0) {
    TRY_RESULT(inline_message_id, get_inline_message_id(query.get()));
    resolve_reply_markup_bot_usernames(
        std::move(reply_markup), std::move(query),
        [this, inline_message_id = inline_message_id.str(), input_message_text = std::move(input_message_text)](
            object_ptr<td_api::ReplyMarkup> reply_markup, PromisedQueryPtr query) mutable {
          send_request(make_object<td_api::editInlineMessageText>(inline_message_id, std::move(reply_markup),
                                                                  std::move(input_message_text)),
                       std::make_unique<TdOnEditInlineMessageCallback>(std::move(query)));
        });
  } else {
    resolve_reply_markup_bot_usernames(
        std::move(reply_markup), std::move(query),
        [this, chat_id = chat_id.str(), message_id, input_message_text = std::move(input_message_text)](
            object_ptr<td_api::ReplyMarkup> reply_markup, PromisedQueryPtr query) mutable {
          check_message(
              chat_id, message_id, false, AccessRights::Edit, "message to edit", std::move(query),
              [this, input_message_text = std::move(input_message_text), reply_markup = std::move(reply_markup)](
                  int64 chat_id, int64 message_id, PromisedQueryPtr query) mutable {
                send_request(make_object<td_api::editMessageText>(chat_id, message_id, std::move(reply_markup),
                                                                  std::move(input_message_text)),
                             std::make_unique<TdOnEditMessageCallback>(this, std::move(query)));
              });
        });
  }
  return Status::OK();
}

td::Status Client::process_edit_message_live_location_query(PromisedQueryPtr &query) {
  object_ptr<td_api::location> location = nullptr;
  int32 heading = get_integer_arg(query.get(), "heading", 0);
  int32 proximity_alert_radius = get_integer_arg(query.get(), "proximity_alert_radius", 0);
  if (query->method() == "editmessagelivelocation") {
    TRY_RESULT_ASSIGN(location, get_location(query.get()));
  }
  auto chat_id = query->arg("chat_id");
  auto message_id = get_message_id(query.get());
  TRY_RESULT(reply_markup, get_reply_markup(query.get()));

  if (chat_id.empty() && message_id == 0) {
    TRY_RESULT(inline_message_id, get_inline_message_id(query.get()));
    resolve_reply_markup_bot_usernames(
        std::move(reply_markup), std::move(query),
        [this, inline_message_id = inline_message_id.str(), location = std::move(location), heading,
         proximity_alert_radius](object_ptr<td_api::ReplyMarkup> reply_markup, PromisedQueryPtr query) mutable {
          send_request(
              make_object<td_api::editInlineMessageLiveLocation>(inline_message_id, std::move(reply_markup),
                                                                 std::move(location), heading, proximity_alert_radius),
              std::make_unique<TdOnEditInlineMessageCallback>(std::move(query)));
        });
  } else {
    resolve_reply_markup_bot_usernames(
        std::move(reply_markup), std::move(query),
        [this, chat_id = chat_id.str(), message_id, location = std::move(location), heading, proximity_alert_radius](
            object_ptr<td_api::ReplyMarkup> reply_markup, PromisedQueryPtr query) mutable {
          check_message(chat_id, message_id, false, AccessRights::Edit, "message to edit", std::move(query),
                        [this, location = std::move(location), heading, proximity_alert_radius,
                         reply_markup = std::move(reply_markup)](int64 chat_id, int64 message_id,
                                                                 PromisedQueryPtr query) mutable {
                          send_request(make_object<td_api::editMessageLiveLocation>(
                                           chat_id, message_id, std::move(reply_markup), std::move(location), heading,
                                           proximity_alert_radius),
                                       std::make_unique<TdOnEditMessageCallback>(this, std::move(query)));
                        });
        });
  }
  return Status::OK();
}

td::Status Client::process_edit_message_media_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto message_id = get_message_id(query.get());
  TRY_RESULT(reply_markup, get_reply_markup(query.get()));
  TRY_RESULT(input_media, get_input_media(query.get(), "media", false));

  if (chat_id.empty() && message_id == 0) {
    TRY_RESULT(inline_message_id, get_inline_message_id(query.get()));
    resolve_reply_markup_bot_usernames(
        std::move(reply_markup), std::move(query),
        [this, inline_message_id = inline_message_id.str(), input_message_content = std::move(input_media)](
            object_ptr<td_api::ReplyMarkup> reply_markup, PromisedQueryPtr query) mutable {
          send_request(make_object<td_api::editInlineMessageMedia>(inline_message_id, std::move(reply_markup),
                                                                   std::move(input_message_content)),
                       std::make_unique<TdOnEditInlineMessageCallback>(std::move(query)));
        });
  } else {
    resolve_reply_markup_bot_usernames(
        std::move(reply_markup), std::move(query),
        [this, chat_id = chat_id.str(), message_id, input_message_content = std::move(input_media)](
            object_ptr<td_api::ReplyMarkup> reply_markup, PromisedQueryPtr query) mutable {
          check_message(
              chat_id, message_id, false, AccessRights::Edit, "message to edit", std::move(query),
              [this, reply_markup = std::move(reply_markup), input_message_content = std::move(input_message_content)](
                  int64 chat_id, int64 message_id, PromisedQueryPtr query) mutable {
                send_request(make_object<td_api::editMessageMedia>(chat_id, message_id, std::move(reply_markup),
                                                                   std::move(input_message_content)),
                             std::make_unique<TdOnEditMessageCallback>(this, std::move(query)));
              });
        });
  }
  return Status::OK();
}

td::Status Client::process_edit_message_caption_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto message_id = get_message_id(query.get());
  TRY_RESULT(reply_markup, get_reply_markup(query.get()));
  TRY_RESULT(caption, get_caption(query.get()));

  if (chat_id.empty() && message_id == 0) {
    TRY_RESULT(inline_message_id, get_inline_message_id(query.get()));
    resolve_reply_markup_bot_usernames(
        std::move(reply_markup), std::move(query),
        [this, inline_message_id = inline_message_id.str(), caption = std::move(caption)](
            object_ptr<td_api::ReplyMarkup> reply_markup, PromisedQueryPtr query) mutable {
          send_request(make_object<td_api::editInlineMessageCaption>(inline_message_id, std::move(reply_markup),
                                                                     std::move(caption)),
                       std::make_unique<TdOnEditInlineMessageCallback>(std::move(query)));
        });
  } else {
    resolve_reply_markup_bot_usernames(
        std::move(reply_markup), std::move(query),
        [this, chat_id = chat_id.str(), message_id, caption = std::move(caption)](
            object_ptr<td_api::ReplyMarkup> reply_markup, PromisedQueryPtr query) mutable {
          check_message(chat_id, message_id, false, AccessRights::Edit, "message to edit", std::move(query),
                        [this, reply_markup = std::move(reply_markup), caption = std::move(caption)](
                            int64 chat_id, int64 message_id, PromisedQueryPtr query) mutable {
                          send_request(make_object<td_api::editMessageCaption>(
                                           chat_id, message_id, std::move(reply_markup), std::move(caption)),
                                       std::make_unique<TdOnEditMessageCallback>(this, std::move(query)));
                        });
        });
  }
  return Status::OK();
}

td::Status Client::process_edit_message_reply_markup_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto message_id = get_message_id(query.get());
  TRY_RESULT(reply_markup, get_reply_markup(query.get()));

  if (chat_id.empty() && message_id == 0) {
    TRY_RESULT(inline_message_id, get_inline_message_id(query.get()));
    resolve_reply_markup_bot_usernames(
        std::move(reply_markup), std::move(query),
        [this, inline_message_id = inline_message_id.str()](object_ptr<td_api::ReplyMarkup> reply_markup,
                                                            PromisedQueryPtr query) {
          send_request(make_object<td_api::editInlineMessageReplyMarkup>(inline_message_id, std::move(reply_markup)),
                       std::make_unique<TdOnEditInlineMessageCallback>(std::move(query)));
        });
  } else {
    resolve_reply_markup_bot_usernames(
        std::move(reply_markup), std::move(query),
        [this, chat_id = chat_id.str(), message_id](object_ptr<td_api::ReplyMarkup> reply_markup,
                                                    PromisedQueryPtr query) {
          check_message(chat_id, message_id, false, AccessRights::Edit, "message to edit", std::move(query),
                        [this, reply_markup = std::move(reply_markup)](int64 chat_id, int64 message_id,
                                                                       PromisedQueryPtr query) mutable {
                          send_request(
                              make_object<td_api::editMessageReplyMarkup>(chat_id, message_id, std::move(reply_markup)),
                              std::make_unique<TdOnEditMessageCallback>(this, std::move(query)));
                        });
        });
  }
  return Status::OK();
}

td::Status Client::process_delete_message_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto message_id = get_message_id(query.get());

  if (chat_id.empty()) {
    return Status::Error(400, "Chat identifier is not specified");
  }

  if (message_id == 0) {
    return Status::Error(400, "Message identifier is not specified");
  }

  check_message(chat_id, message_id, false, AccessRights::Write, "message to delete", std::move(query),
                [this](int64 chat_id, int64 message_id, PromisedQueryPtr query) {
                  delete_message(chat_id, message_id, false);
                  send_request(make_object<td_api::deleteMessages>(chat_id, td::vector<int64>{message_id}, true),
                               std::make_unique<TdOnOkQueryCallback>(std::move(query)));
                });
  return Status::OK();
}

td::Status Client::process_set_game_score_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto message_id = get_message_id(query.get());
  TRY_RESULT(user_id, get_user_id(query.get()));
  auto score = td::to_integer<int32>(query->arg("score"));
  auto force = to_bool(query->arg("force"));
  bool edit_message = true;
  if (query->has_arg("disable_edit_message")) {
    edit_message = !to_bool(query->arg("disable_edit_message"));
  } else if (query->has_arg("edit_message")) {
    edit_message = to_bool(query->arg("edit_message"));
  }

  if (chat_id.empty() && message_id == 0) {
    TRY_RESULT(inline_message_id, get_inline_message_id(query.get()));
    check_user_no_fail(
        user_id, std::move(query),
        [this, inline_message_id = inline_message_id.str(), edit_message, user_id, score,
         force](PromisedQueryPtr query) {
          send_request(make_object<td_api::setInlineGameScore>(inline_message_id, edit_message, user_id, score, force),
                       std::make_unique<TdOnEditInlineMessageCallback>(std::move(query)));
        });
  } else {
    check_message(chat_id, message_id, false, AccessRights::Edit, "message to set game score", std::move(query),
                  [this, user_id, score, force, edit_message](int64 chat_id, int64 message_id, PromisedQueryPtr query) {
                    check_user_no_fail(
                        user_id, std::move(query),
                        [this, chat_id, message_id, user_id, score, force, edit_message](PromisedQueryPtr query) {
                          send_request(make_object<td_api::setGameScore>(chat_id, message_id, edit_message, user_id,
                                                                         score, force),
                                       std::make_unique<TdOnEditMessageCallback>(this, std::move(query)));
                        });
                  });
  }
  return Status::OK();
}

td::Status Client::process_get_game_high_scores_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto message_id = get_message_id(query.get());
  TRY_RESULT(user_id, get_user_id(query.get()));

  if (chat_id.empty() && message_id == 0) {
    TRY_RESULT(inline_message_id, get_inline_message_id(query.get()));
    check_user_no_fail(user_id, std::move(query),
                       [this, inline_message_id = inline_message_id.str(), user_id](PromisedQueryPtr query) {
                         send_request(make_object<td_api::getInlineGameHighScores>(inline_message_id, user_id),
                                      std::make_unique<TdOnGetGameHighScoresCallback>(this, std::move(query)));
                       });
  } else {
    check_message(chat_id, message_id, false, AccessRights::Read, "message to get game high scores", std::move(query),
                  [this, user_id](int64 chat_id, int64 message_id, PromisedQueryPtr query) {
                    check_user_no_fail(
                        user_id, std::move(query), [this, chat_id, message_id, user_id](PromisedQueryPtr query) {
                          send_request(make_object<td_api::getGameHighScores>(chat_id, message_id, user_id),
                                       std::make_unique<TdOnGetGameHighScoresCallback>(this, std::move(query)));
                        });
                  });
  }
  return Status::OK();
}

td::Status Client::process_answer_inline_query_query(PromisedQueryPtr &query) {
  auto inline_query_id = td::to_integer<int64>(query->arg("inline_query_id"));
  auto is_personal = to_bool(query->arg("is_personal"));
  int32 cache_time = get_integer_arg(query.get(), "cache_time", 300, 0, 24 * 60 * 60);
  auto next_offset = query->arg("next_offset");
  auto switch_pm_text = query->arg("switch_pm_text");
  auto switch_pm_parameter = query->arg("switch_pm_parameter");

  TRY_RESULT(results, get_inline_query_results(query.get()));

  resolve_inline_query_results_bot_usernames(
      std::move(results), std::move(query),
      [this, inline_query_id, is_personal, cache_time, next_offset = next_offset.str(),
       switch_pm_text = switch_pm_text.str(), switch_pm_parameter = switch_pm_parameter.str()](
          td::vector<object_ptr<td_api::InputInlineQueryResult>> results, PromisedQueryPtr query) {
        send_request(
            make_object<td_api::answerInlineQuery>(inline_query_id, is_personal, std::move(results), cache_time,
                                                   next_offset, switch_pm_text, switch_pm_parameter),
            std::make_unique<TdOnOkQueryCallback>(std::move(query)));
      });
  return Status::OK();
}

td::Status Client::process_answer_callback_query_query(PromisedQueryPtr &query) {
  auto callback_query_id = td::to_integer<int64>(query->arg("callback_query_id"));
  td::string text = query->arg("text").str();
  bool show_alert = to_bool(query->arg("show_alert"));
  td::string url = query->arg("url").str();
  int32 cache_time = get_integer_arg(query.get(), "cache_time", 0, 0, 24 * 30 * 60 * 60);

  send_request(make_object<td_api::answerCallbackQuery>(callback_query_id, text, show_alert, url, cache_time),
               std::make_unique<TdOnOkQueryCallback>(std::move(query)));
  return Status::OK();
}

td::Status Client::process_answer_shipping_query_query(PromisedQueryPtr &query) {
  auto shipping_query_id = td::to_integer<int64>(query->arg("shipping_query_id"));
  auto ok = to_bool(query->arg("ok"));
  td::vector<object_ptr<td_api::shippingOption>> shipping_options;
  td::MutableSlice error_message;
  if (ok) {
    TRY_RESULT_ASSIGN(shipping_options, get_shipping_options(query.get()));
  } else {
    TRY_RESULT_ASSIGN(error_message, get_required_string_arg(query.get(), "error_message"));
  }
  send_request(
      make_object<td_api::answerShippingQuery>(shipping_query_id, std::move(shipping_options), error_message.str()),
      std::make_unique<TdOnOkQueryCallback>(std::move(query)));
  return Status::OK();
}

td::Status Client::process_answer_pre_checkout_query_query(PromisedQueryPtr &query) {
  auto pre_checkout_query_id = td::to_integer<int64>(query->arg("pre_checkout_query_id"));
  auto ok = to_bool(query->arg("ok"));
  td::MutableSlice error_message;
  if (!ok) {
    TRY_RESULT_ASSIGN(error_message, get_required_string_arg(query.get(), "error_message"));
  }

  send_request(make_object<td_api::answerPreCheckoutQuery>(pre_checkout_query_id, error_message.str()),
               std::make_unique<TdOnOkQueryCallback>(std::move(query)));
  return Status::OK();
}

td::Status Client::process_export_chat_invite_link_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");

  check_chat(chat_id, AccessRights::Write, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) {
    send_request(make_object<td_api::replacePrimaryChatInviteLink>(chat_id),
                 std::make_unique<TdOnReplacePrimaryChatInviteLinkCallback>(std::move(query)));
  });
  return Status::OK();
}

td::Status Client::process_create_chat_invite_link_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto expire_date = get_integer_arg(query.get(), "expire_date", 0, 0);
  auto member_limit = get_integer_arg(query.get(), "member_limit", 0, 0, 100000);

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, expire_date, member_limit](int64 chat_id, PromisedQueryPtr query) {
               send_request(make_object<td_api::createChatInviteLink>(chat_id, expire_date, member_limit),
                            std::make_unique<TdOnGetChatInviteLinkCallback>(this, std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_edit_chat_invite_link_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto invite_link = query->arg("invite_link");
  auto expire_date = get_integer_arg(query.get(), "expire_date", 0, 0);
  auto member_limit = get_integer_arg(query.get(), "member_limit", 0, 0, 100000);

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, invite_link = invite_link.str(), expire_date, member_limit](int64 chat_id, PromisedQueryPtr query) {
               send_request(make_object<td_api::editChatInviteLink>(chat_id, invite_link, expire_date, member_limit),
                            std::make_unique<TdOnGetChatInviteLinkCallback>(this, std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_revoke_chat_invite_link_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto invite_link = query->arg("invite_link");

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, invite_link = invite_link.str()](int64 chat_id, PromisedQueryPtr query) {
               send_request(make_object<td_api::revokeChatInviteLink>(chat_id, invite_link),
                            std::make_unique<TdOnGetChatInviteLinkCallback>(this, std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_get_chat_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");

  check_chat(chat_id, AccessRights::Read, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) {
    auto chat_info = get_chat(chat_id);
    CHECK(chat_info != nullptr);
    switch (chat_info->type) {
      case ChatInfo::Type::Private:
        return send_request(make_object<td_api::getUserFullInfo>(chat_info->user_id),
                            std::make_unique<TdOnGetChatFullInfoCallback>(this, chat_id, std::move(query)));
      case ChatInfo::Type::Group:
        return send_request(make_object<td_api::getBasicGroupFullInfo>(chat_info->group_id),
                            std::make_unique<TdOnGetChatFullInfoCallback>(this, chat_id, std::move(query)));
      case ChatInfo::Type::Supergroup:
        return send_request(make_object<td_api::getSupergroupFullInfo>(chat_info->supergroup_id),
                            std::make_unique<TdOnGetChatFullInfoCallback>(this, chat_id, std::move(query)));
      case ChatInfo::Type::Unknown:
      default:
        UNREACHABLE();
    }
  });
  return Status::OK();
}

td::Status Client::process_set_chat_photo_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto photo = get_input_file(query.get(), "photo", true);
  if (photo == nullptr) {
    if (query->arg("photo").empty()) {
      return Status::Error(400, "There is no photo in the request");
    }
    return Status::Error(400, "Photo must be uploaded as an InputFile");
  }

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, photo = std::move(photo)](int64 chat_id, PromisedQueryPtr query) mutable {
               send_request(make_object<td_api::setChatPhoto>(
                                chat_id, make_object<td_api::inputChatPhotoStatic>(std::move(photo))),
                            std::make_unique<TdOnOkQueryCallback>(std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_delete_chat_photo_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");

  check_chat(chat_id, AccessRights::Write, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) {
    send_request(make_object<td_api::setChatPhoto>(chat_id, nullptr),
                 std::make_unique<TdOnOkQueryCallback>(std::move(query)));
  });
  return Status::OK();
}

td::Status Client::process_set_chat_title_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto title = query->arg("title");

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, title = title.str()](int64 chat_id, PromisedQueryPtr query) {
               send_request(make_object<td_api::setChatTitle>(chat_id, title),
                            std::make_unique<TdOnOkQueryCallback>(std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_set_chat_permissions_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  bool allow_legacy = false;
  TRY_RESULT(permissions, get_chat_permissions(query.get(), allow_legacy));
  CHECK(!allow_legacy);

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, permissions = std::move(permissions)](int64 chat_id, PromisedQueryPtr query) mutable {
               send_request(make_object<td_api::setChatPermissions>(chat_id, std::move(permissions)),
                            std::make_unique<TdOnOkQueryCallback>(std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_set_chat_description_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto description = query->arg("description");

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, description = description.str()](int64 chat_id, PromisedQueryPtr query) {
               send_request(make_object<td_api::setChatDescription>(chat_id, description),
                            std::make_unique<TdOnOkQueryCallback>(std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_pin_chat_message_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto message_id = get_message_id(query.get());
  auto disable_notification = to_bool(query->arg("disable_notification"));

  check_message(chat_id, message_id, false, AccessRights::Write, "message to pin", std::move(query),
                [this, disable_notification](int64 chat_id, int64 message_id, PromisedQueryPtr query) {
                  send_request(make_object<td_api::pinChatMessage>(chat_id, message_id, disable_notification, false),
                               std::make_unique<TdOnOkQueryCallback>(std::move(query)));
                });
  return Status::OK();
}

td::Status Client::process_unpin_chat_message_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto message_id = get_message_id(query.get());

  if (message_id == 0) {
    check_chat(chat_id, AccessRights::Write, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) {
      send_request(make_object<td_api::getChatPinnedMessage>(chat_id),
                   std::make_unique<TdOnGetChatPinnedMessageToUnpinCallback>(this, chat_id, std::move(query)));
    });
  } else {
    check_message(chat_id, message_id, false, AccessRights::Write, "message to unpin", std::move(query),
                  [this](int64 chat_id, int64 message_id, PromisedQueryPtr query) {
                    send_request(make_object<td_api::unpinChatMessage>(chat_id, message_id),
                                 std::make_unique<TdOnOkQueryCallback>(std::move(query)));
                  });
  }
  return Status::OK();
}

td::Status Client::process_unpin_all_chat_messages_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");

  check_chat(chat_id, AccessRights::Write, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) {
    send_request(make_object<td_api::unpinAllChatMessages>(chat_id),
                 std::make_unique<TdOnOkQueryCallback>(std::move(query)));
  });
  return Status::OK();
}

td::Status Client::process_set_chat_sticker_set_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  auto sticker_set_name = query->arg("sticker_set_name");

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, sticker_set_name = sticker_set_name.str()](int64 chat_id, PromisedQueryPtr query) {
               if (get_chat_type(chat_id) != ChatType::Supergroup) {
                 return fail_query(400, "Bad Request: method is available only for supergroups", std::move(query));
               }

               resolve_sticker_set(
                   sticker_set_name, std::move(query), [this, chat_id](int64 sticker_set_id, PromisedQueryPtr query) {
                     auto chat_info = get_chat(chat_id);
                     CHECK(chat_info != nullptr);
                     CHECK(chat_info->type == ChatInfo::Type::Supergroup);
                     send_request(
                         make_object<td_api::setSupergroupStickerSet>(chat_info->supergroup_id, sticker_set_id),
                         std::make_unique<TdOnOkQueryCallback>(std::move(query)));
                   });
             });
  return Status::OK();
}

td::Status Client::process_delete_chat_sticker_set_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");

  check_chat(chat_id, AccessRights::Write, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) {
    if (get_chat_type(chat_id) != ChatType::Supergroup) {
      return fail_query(400, "Bad Request: method is available only for supergroups", std::move(query));
    }

    auto chat_info = get_chat(chat_id);
    CHECK(chat_info != nullptr);
    CHECK(chat_info->type == ChatInfo::Type::Supergroup);
    send_request(make_object<td_api::setSupergroupStickerSet>(chat_info->supergroup_id, 0),
                 std::make_unique<TdOnOkQueryCallback>(std::move(query)));
  });
  return Status::OK();
}

td::Status Client::process_get_chat_member_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  TRY_RESULT(user_id, get_user_id(query.get()));

  check_chat(chat_id, AccessRights::ReadMembers, std::move(query),
             [this, user_id](int64 chat_id, PromisedQueryPtr query) {
               get_chat_member(chat_id, user_id, std::move(query),
                               [this, chat_type = get_chat_type(chat_id)](object_ptr<td_api::chatMember> &&chat_member,
                                                                          PromisedQueryPtr query) {
                                 answer_query(JsonChatMember(chat_member.get(), chat_type, this), std::move(query));
                               });
             });
  return Status::OK();
}

td::Status Client::process_get_chat_administrators_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");

  check_chat(chat_id, AccessRights::ReadMembers, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) {
    auto chat_info = get_chat(chat_id);
    CHECK(chat_info != nullptr);
    switch (chat_info->type) {
      case ChatInfo::Type::Private:
        return fail_query(400, "Bad Request: there are no administrators in the private chat", std::move(query));
      case ChatInfo::Type::Group:
        return send_request(make_object<td_api::getBasicGroupFullInfo>(chat_info->group_id),
                            std::make_unique<TdOnGetGroupMembersCallback>(this, true, std::move(query)));
      case ChatInfo::Type::Supergroup:
        return send_request(
            make_object<td_api::getSupergroupMembers>(
                chat_info->supergroup_id, make_object<td_api::supergroupMembersFilterAdministrators>(), 0, 100),
            std::make_unique<TdOnGetSupergroupMembersCallback>(this, get_chat_type(chat_id), std::move(query)));
      case ChatInfo::Type::Unknown:
      default:
        UNREACHABLE();
    }
  });
  return Status::OK();
}

td::Status Client::process_get_chat_member_count_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");

  check_chat(chat_id, AccessRights::ReadMembers, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) {
    auto chat_info = get_chat(chat_id);
    CHECK(chat_info != nullptr);
    switch (chat_info->type) {
      case ChatInfo::Type::Private:
        return answer_query(td::VirtuallyJsonableInt(1 + (chat_info->user_id != my_id_)), std::move(query));
      case ChatInfo::Type::Group: {
        auto group_info = get_group_info(chat_info->group_id);
        CHECK(group_info != nullptr);
        return answer_query(td::VirtuallyJsonableInt(group_info->member_count), std::move(query));
      }
      case ChatInfo::Type::Supergroup:
        return send_request(make_object<td_api::getSupergroupFullInfo>(chat_info->supergroup_id),
                            std::make_unique<TdOnGetSupergroupMembersCountCallback>(std::move(query)));
      case ChatInfo::Type::Unknown:
      default:
        UNREACHABLE();
    }
  });
  return Status::OK();
}

td::Status Client::process_leave_chat_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");

  check_chat(chat_id, AccessRights::Read, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) {
    send_request(make_object<td_api::leaveChat>(chat_id), std::make_unique<TdOnOkQueryCallback>(std::move(query)));
  });
  return Status::OK();
}

td::Status Client::process_promote_chat_member_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  TRY_RESULT(user_id, get_user_id(query.get()));
  auto can_manage_chat = to_bool(query->arg("can_manage_chat"));
  auto can_change_info = to_bool(query->arg("can_change_info"));
  auto can_post_messages = to_bool(query->arg("can_post_messages"));
  auto can_edit_messages = to_bool(query->arg("can_edit_messages"));
  auto can_delete_messages = to_bool(query->arg("can_delete_messages"));
  auto can_invite_users = to_bool(query->arg("can_invite_users"));
  auto can_restrict_members = to_bool(query->arg("can_restrict_members"));
  auto can_pin_messages = to_bool(query->arg("can_pin_messages"));
  auto can_promote_members = to_bool(query->arg("can_promote_members"));
  auto can_manage_voice_chats = to_bool(query->arg("can_manage_voice_chats"));
  auto is_anonymous = to_bool(query->arg("is_anonymous"));
  auto status = make_object<td_api::chatMemberStatusAdministrator>(
      td::string(), true, can_manage_chat, can_change_info, can_post_messages, can_edit_messages, can_delete_messages,
      can_invite_users, can_restrict_members, can_pin_messages, can_promote_members, can_manage_voice_chats,
      is_anonymous);
  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, user_id, status = std::move(status)](int64 chat_id, PromisedQueryPtr query) mutable {
               auto chat_info = get_chat(chat_id);
               CHECK(chat_info != nullptr);
               if (chat_info->type != ChatInfo::Type::Supergroup) {
                 return fail_query(400, "Bad Request: method is available for supergroup and channel chats only",
                                   std::move(query));
               }

               get_chat_member(
                   chat_id, user_id, std::move(query),
                   [this, chat_id, user_id, status = std::move(status)](object_ptr<td_api::chatMember> &&chat_member,
                                                                        PromisedQueryPtr query) mutable {
                     if (chat_member->status_->get_id() == td_api::chatMemberStatusAdministrator::ID) {
                       auto administrator =
                           static_cast<const td_api::chatMemberStatusAdministrator *>(chat_member->status_.get());
                       status->custom_title_ = std::move(administrator->custom_title_);
                     }

                     send_request(
                         make_object<td_api::setChatMemberStatus>(
                             chat_id, td_api::make_object<td_api::messageSenderUser>(user_id), std::move(status)),
                         std::make_unique<TdOnOkQueryCallback>(std::move(query)));
                   });
             });
  return Status::OK();
}

td::Status Client::process_set_chat_administrator_custom_title_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  TRY_RESULT(user_id, get_user_id(query.get()));

  check_chat(chat_id, AccessRights::Write, std::move(query), [this, user_id](int64 chat_id, PromisedQueryPtr query) {
    if (get_chat_type(chat_id) != ChatType::Supergroup) {
      return fail_query(400, "Bad Request: method is available only for supergroups", std::move(query));
    }

    get_chat_member(
        chat_id, user_id, std::move(query),
        [this, chat_id, user_id](object_ptr<td_api::chatMember> &&chat_member, PromisedQueryPtr query) {
          if (chat_member->status_->get_id() == td_api::chatMemberStatusCreator::ID) {
            return fail_query(400, "Bad Request: only creator can edit their custom title", std::move(query));
          }
          if (chat_member->status_->get_id() != td_api::chatMemberStatusAdministrator::ID) {
            return fail_query(400, "Bad Request: user is not an administrator", std::move(query));
          }
          auto administrator = td_api::move_object_as<td_api::chatMemberStatusAdministrator>(chat_member->status_);
          if (!administrator->can_be_edited_) {
            return fail_query(400, "Bad Request: not enough rights to change custom title of the user",
                              std::move(query));
          }
          administrator->custom_title_ = query->arg("custom_title").str();

          send_request(make_object<td_api::setChatMemberStatus>(
                           chat_id, td_api::make_object<td_api::messageSenderUser>(user_id), std::move(administrator)),
                       std::make_unique<TdOnOkQueryCallback>(std::move(query)));
        });
  });
  return Status::OK();
}

td::Status Client::process_ban_chat_member_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  TRY_RESULT(user_id, get_user_id(query.get()));
  int32 until_date = get_integer_arg(query.get(), "until_date", 0);
  auto revoke_messages = to_bool(query->arg("revoke_messages"));

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, user_id, until_date, revoke_messages](int64 chat_id, PromisedQueryPtr query) {
               check_user_no_fail(user_id, std::move(query),
                                  [this, chat_id, user_id, until_date, revoke_messages](PromisedQueryPtr query) {
                                    send_request(make_object<td_api::banChatMember>(
                                                     chat_id, td_api::make_object<td_api::messageSenderUser>(user_id),
                                                     until_date, revoke_messages),
                                                 std::make_unique<TdOnOkQueryCallback>(std::move(query)));
                                  });
             });
  return Status::OK();
}

td::Status Client::process_restrict_chat_member_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  TRY_RESULT(user_id, get_user_id(query.get()));
  int32 until_date = get_integer_arg(query.get(), "until_date", 0);
  bool allow_legacy = true;
  TRY_RESULT(permissions, get_chat_permissions(query.get(), allow_legacy));

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, user_id, until_date, is_legacy = allow_legacy, permissions = std::move(permissions)](
                 int64 chat_id, PromisedQueryPtr query) mutable {
               if (get_chat_type(chat_id) != ChatType::Supergroup) {
                 return fail_query(400, "Bad Request: method is available only for supergroups", std::move(query));
               }

               get_chat_member(
                   chat_id, user_id, std::move(query),
                   [this, chat_id, user_id, until_date, is_legacy, permissions = std::move(permissions)](
                       object_ptr<td_api::chatMember> &&chat_member, PromisedQueryPtr query) mutable {
                     if (is_legacy && chat_member->status_->get_id() == td_api::chatMemberStatusRestricted::ID) {
                       auto restricted =
                           static_cast<const td_api::chatMemberStatusRestricted *>(chat_member->status_.get());
                       auto *old_permissions = restricted->permissions_.get();
                       permissions->can_send_polls_ = old_permissions->can_send_polls_;
                       permissions->can_change_info_ = old_permissions->can_change_info_;
                       permissions->can_invite_users_ = old_permissions->can_invite_users_;
                       permissions->can_pin_messages_ = old_permissions->can_pin_messages_;
                     }

                     send_request(make_object<td_api::setChatMemberStatus>(
                                      chat_id, td_api::make_object<td_api::messageSenderUser>(user_id),
                                      make_object<td_api::chatMemberStatusRestricted>(
                                          is_chat_member(chat_member->status_), until_date, std::move(permissions))),
                                  std::make_unique<TdOnOkQueryCallback>(std::move(query)));
                   });
             });
  return Status::OK();
}

td::Status Client::process_unban_chat_member_query(PromisedQueryPtr &query) {
  auto chat_id = query->arg("chat_id");
  TRY_RESULT(user_id, get_user_id(query.get()));
  auto only_if_banned = to_bool(query->arg("only_if_banned"));

  check_chat(chat_id, AccessRights::Write, std::move(query),
             [this, user_id, only_if_banned](int64 chat_id, PromisedQueryPtr query) {
               auto chat_info = get_chat(chat_id);
               CHECK(chat_info != nullptr);
               if (chat_info->type != ChatInfo::Type::Supergroup) {
                 return fail_query(400, "Bad Request: method is available for supergroup and channel chats only",
                                   std::move(query));
               }

               if (only_if_banned) {
                 get_chat_member(
                     chat_id, user_id, std::move(query),
                     [this, chat_id, user_id](object_ptr<td_api::chatMember> &&chat_member, PromisedQueryPtr query) {
                       if (chat_member->status_->get_id() != td_api::chatMemberStatusBanned::ID) {
                         return answer_query(td::JsonTrue(), std::move(query));
                       }

                       send_request(make_object<td_api::setChatMemberStatus>(
                                        chat_id, td_api::make_object<td_api::messageSenderUser>(user_id),
                                        make_object<td_api::chatMemberStatusLeft>()),
                                    std::make_unique<TdOnOkQueryCallback>(std::move(query)));
                     });
               } else {
                 check_user_no_fail(user_id, std::move(query), [this, chat_id, user_id](PromisedQueryPtr query) {
                   send_request(make_object<td_api::setChatMemberStatus>(
                                    chat_id, td_api::make_object<td_api::messageSenderUser>(user_id),
                                    make_object<td_api::chatMemberStatusLeft>()),
                                std::make_unique<TdOnOkQueryCallback>(std::move(query)));
                 });
               }
             });
  return Status::OK();
}

td::Status Client::process_get_sticker_set_query(PromisedQueryPtr &query) {
  auto name = query->arg("name");
  if (td::trim(to_lower(name)) == to_lower(GREAT_MINDS_SET_NAME)) {
    send_request(make_object<td_api::getStickerSet>(GREAT_MINDS_SET_ID),
                 std::make_unique<TdOnReturnStickerSetCallback>(this, true, std::move(query)));
  } else {
    send_request(make_object<td_api::searchStickerSet>(name.str()),
                 std::make_unique<TdOnReturnStickerSetCallback>(this, true, std::move(query)));
  }
  return Status::OK();
}

td::Status Client::process_upload_sticker_file_query(PromisedQueryPtr &query) {
  TRY_RESULT(user_id, get_user_id(query.get()));
  auto png_sticker = get_input_file(query.get(), "png_sticker");

  check_user(user_id, std::move(query),
             [this, user_id, png_sticker = std::move(png_sticker)](PromisedQueryPtr query) mutable {
               send_request(make_object<td_api::uploadStickerFile>(
                                user_id, make_object<td_api::inputStickerStatic>(std::move(png_sticker), "", nullptr)),
                            std::make_unique<TdOnReturnFileCallback>(this, std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_create_new_sticker_set_query(PromisedQueryPtr &query) {
  TRY_RESULT(user_id, get_user_id(query.get()));
  auto name = query->arg("name");
  auto title = query->arg("title");
  auto is_masks = to_bool(query->arg("contains_masks"));
  TRY_RESULT(stickers, get_input_stickers(query.get()));

  check_user(user_id, std::move(query),
             [this, user_id, title, name, is_masks, stickers = std::move(stickers)](PromisedQueryPtr query) mutable {
               send_request(make_object<td_api::createNewStickerSet>(user_id, title.str(), name.str(), is_masks,
                                                                     std::move(stickers), PSTRING() << "bot" << my_id_),
                            std::make_unique<TdOnReturnStickerSetCallback>(this, false, std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_add_sticker_to_set_query(PromisedQueryPtr &query) {
  TRY_RESULT(user_id, get_user_id(query.get()));
  auto name = query->arg("name");
  TRY_RESULT(stickers, get_input_stickers(query.get()));
  CHECK(!stickers.empty());

  check_user(user_id, std::move(query),
             [this, user_id, name, sticker = std::move(stickers[0])](PromisedQueryPtr query) mutable {
               send_request(make_object<td_api::addStickerToSet>(user_id, name.str(), std::move(sticker)),
                            std::make_unique<TdOnReturnStickerSetCallback>(this, false, std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_set_sticker_set_thumb_query(PromisedQueryPtr &query) {
  TRY_RESULT(user_id, get_user_id(query.get()));
  auto name = query->arg("name");
  auto thumbnail = get_input_file(query.get(), "thumb");
  check_user(user_id, std::move(query),
             [this, user_id, name, thumbnail = std::move(thumbnail)](PromisedQueryPtr query) mutable {
               send_request(make_object<td_api::setStickerSetThumbnail>(user_id, name.str(), std::move(thumbnail)),
                            std::make_unique<TdOnReturnStickerSetCallback>(this, false, std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_set_sticker_position_in_set_query(PromisedQueryPtr &query) {
  auto file_id = trim(query->arg("sticker"));
  if (file_id.empty()) {
    return Status::Error(400, "Sticker is not specified");
  }
  int32 position = get_integer_arg(query.get(), "position", -1);

  send_request(
      make_object<td_api::setStickerPositionInSet>(make_object<td_api::inputFileRemote>(file_id.str()), position),
      std::make_unique<TdOnOkQueryCallback>(std::move(query)));
  return Status::OK();
}

td::Status Client::process_delete_sticker_from_set_query(PromisedQueryPtr &query) {
  auto file_id = trim(query->arg("sticker"));
  if (file_id.empty()) {
    return Status::Error(400, "Sticker is not specified");
  }

  send_request(make_object<td_api::removeStickerFromSet>(make_object<td_api::inputFileRemote>(file_id.str())),
               std::make_unique<TdOnOkQueryCallback>(std::move(query)));
  return Status::OK();
}

td::Status Client::process_set_passport_data_errors_query(PromisedQueryPtr &query) {
  TRY_RESULT(user_id, get_user_id(query.get()));
  TRY_RESULT(passport_element_errors, get_passport_element_errors(query.get()));

  check_user(user_id, std::move(query),
             [this, user_id, errors = std::move(passport_element_errors)](PromisedQueryPtr query) mutable {
               send_request(make_object<td_api::setPassportElementErrors>(user_id, std::move(errors)),
                            std::make_unique<TdOnOkQueryCallback>(std::move(query)));
             });
  return Status::OK();
}

td::Status Client::process_send_custom_request_query(PromisedQueryPtr &query) {
  TRY_RESULT(method, get_required_string_arg(query.get(), "method"));
  auto parameters = query->arg("parameters");
  send_request(make_object<td_api::sendCustomRequest>(method.str(), parameters.str()),
               std::make_unique<TdOnSendCustomRequestCallback>(std::move(query)));
  return Status::OK();
}

td::Status Client::process_answer_custom_query_query(PromisedQueryPtr &query) {
  auto custom_query_id = td::to_integer<int64>(query->arg("custom_query_id"));
  auto data = query->arg("data");
  send_request(make_object<td_api::answerCustomQuery>(custom_query_id, data.str()),
               std::make_unique<TdOnOkQueryCallback>(std::move(query)));
  return Status::OK();
}

td::Status Client::process_get_updates_query(PromisedQueryPtr &query) {
  if (!webhook_url_.empty() || webhook_set_query_) {
    fail_query_conflict(
        "Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first",
        std::move(query));
    return Status::OK();
  }
  int32 offset = get_integer_arg(query.get(), "offset", 0);
  int32 limit = get_integer_arg(query.get(), "limit", 100, 1, 100);
  int32 timeout = get_integer_arg(query.get(), "timeout", 0, 0, LONG_POLL_MAX_TIMEOUT);

  update_allowed_update_types(query.get());

  auto now = td::Time::now_cached();
  if (offset == previous_get_updates_offset_ && timeout < 3 && now < previous_get_updates_start_time_ + 3.0) {
    timeout = 3;
  }
  previous_get_updates_offset_ = offset;
  previous_get_updates_start_time_ = now;
  do_get_updates(offset, limit, timeout, std::move(query));
  return Status::OK();
}

td::Status Client::process_set_webhook_query(PromisedQueryPtr &query) {
  Slice new_url;
  if (query->method() == "setwebhook") {
    new_url = query->arg("url");
  }

  auto now = td::Time::now_cached();
  if (!new_url.empty() && !query->is_internal()) {
    if (now < next_allowed_set_webhook_time_) {
      query->set_retry_after_error(1);
      return Status::OK();
    }
    next_allowed_set_webhook_time_ = now + 1;
  }

  // do not send warning just after webhook was deleted or set
  next_bot_updates_warning_time_ = td::max(next_bot_updates_warning_time_, now + BOT_UPDATES_WARNING_DELAY);

  int32 new_max_connections = new_url.empty() ? 0 : get_webhook_max_connections(query.get());
  Slice new_ip_address = new_url.empty() ? Slice() : query->arg("ip_address");
  bool new_fix_ip_address = new_url.empty() ? false : get_webhook_fix_ip_address(query.get());
  bool drop_pending_updates = to_bool(query->arg("drop_pending_updates"));
  if (webhook_set_query_) {
    // already updating webhook. Cancel previous request
    fail_query_conflict("Conflict: terminated by other setWebhook", std::move(webhook_set_query_));
  } else if (webhook_url_ == new_url && !has_webhook_certificate_ && query->file("certificate") == nullptr &&
             query->arg("certificate").empty() && new_max_connections == webhook_max_connections_ &&
             new_fix_ip_address == webhook_fix_ip_address_ &&
             (!new_fix_ip_address || new_ip_address == webhook_ip_address_) && !drop_pending_updates) {
    if (update_allowed_update_types(query.get())) {
      save_webhook();
    } else if (now > next_webhook_is_not_modified_warning_time_) {
      next_webhook_is_not_modified_warning_time_ = now + 300;
      LOG(WARNING) << "Webhook is not modified: \"" << new_url << '"';
    }
    answer_query(td::JsonTrue(), std::move(query),
                 new_url.empty() ? Slice("Webhook is already deleted") : Slice("Webhook is already set"));
    return Status::OK();
  }

  if (now > next_set_webhook_logging_time_ || webhook_url_ != new_url) {
    next_set_webhook_logging_time_ = now + 300;
    LOG(WARNING) << "Set webhook to " << new_url << ", max_connections = " << new_max_connections
                 << ", IP address = " << new_ip_address;
  }

  if (!new_url.empty()) {
    abort_long_poll(true);
  }

  webhook_generation_++;
  // need to close old webhook first
  if (!webhook_url_.empty()) {
    if (!webhook_id_.empty()) {
      send_closure_later(std::move(webhook_id_), &WebhookActor::close);
    }

    // wait for webhook_close callback
    webhook_query_type_ = WebhookQueryType::Cancel;
    webhook_set_query_ = std::move(query);
    return Status::OK();
  }
  do_set_webhook(std::move(query), false);
  return Status::OK();
}

td::Status Client::process_get_webhook_info_query(PromisedQueryPtr &query) {
  answer_query(JsonWebhookInfo(this), std::move(query));
  return Status::OK();
}

td::Status Client::process_get_file_query(PromisedQueryPtr &query) {
  td::string file_id = query->arg("file_id").str();
  check_remote_file_id(file_id, std::move(query), [this](object_ptr<td_api::file> file, PromisedQueryPtr query) {
    do_get_file(std::move(file), std::move(query));
  });
  return Status::OK();
}

void Client::do_get_file(object_ptr<td_api::file> file, PromisedQueryPtr query) {
  if (!parameters_->local_mode_ &&
      td::max(file->expected_size_, file->local_->downloaded_size_) > MAX_DOWNLOAD_FILE_SIZE) {  // speculative check
    return fail_query(400, "Bad Request: file is too big", std::move(query));
  }

  auto file_id = file->id_;
  file_download_listeners_[file_id].push_back(std::move(query));
  if (file->local_->is_downloading_completed_) {
    Slice relative_path = td::PathView::relative(file->local_->path_, dir_, true);
    if (!relative_path.empty()) {
      auto r_stat = td::stat(file->local_->path_);
      if (r_stat.is_ok() && r_stat.ok().is_reg_ && r_stat.ok().size_ == file->size_) {
        return on_file_download(file_id, std::move(file));
      }
    }
  }

  send_request(make_object<td_api::downloadFile>(file_id, 1, 0, 0, false),
               std::make_unique<TdOnDownloadFileCallback>(this, file_id));
}

bool Client::is_file_being_downloaded(int32 file_id) const {
  return file_download_listeners_.count(file_id) > 0;
}

void Client::on_file_download(int32 file_id, td::Result<object_ptr<td_api::file>> r_file) {
  auto it = file_download_listeners_.find(file_id);
  if (it == file_download_listeners_.end()) {
    return;
  }
  auto queries = std::move(it->second);
  file_download_listeners_.erase(it);
  download_started_file_ids_.erase(file_id);
  for (auto &query : queries) {
    if (r_file.is_error()) {
      const auto &error = r_file.error();
      fail_query_with_error(std::move(query), error.code(), error.public_message());
    } else {
      answer_query(JsonFile(r_file.ok().get(), this), std::move(query));
    }
  }
}

void Client::webhook_verified(td::string cached_ip_address) {
  if (get_link_token() != webhook_generation_) {
    return;
  }
  bool need_save = webhook_set_query_ || cached_ip_address != webhook_ip_address_;
  webhook_ip_address_ = cached_ip_address;
  if (webhook_set_query_) {
    LOG(WARNING) << "Webhook verified";
    answer_query(td::JsonTrue(), std::move(webhook_set_query_), "Webhook was set");
  }
  if (need_save) {
    save_webhook();
  }
}

void Client::save_webhook() const {
  td::string value;
  if (has_webhook_certificate_) {
    value += "cert/";
  }
  value += PSTRING() << "#maxc" << webhook_max_connections_ << '/';
  if (!webhook_ip_address_.empty()) {
    value += PSTRING() << "#ip" << webhook_ip_address_ << '/';
  }
  if (webhook_fix_ip_address_) {
    value += "#fix_ip/";
  }
  if (allowed_update_types_ != DEFAULT_ALLOWED_UPDATE_TYPES) {
    value += PSTRING() << "#allow" << allowed_update_types_ << '/';
  }
  value += webhook_url_;
  LOG(INFO) << "Save webhook " << value;
  parameters_->shared_data_->webhook_db_->set(bot_token_with_dc_, value);
}

void Client::webhook_success() {
  next_bot_updates_warning_time_ = td::Time::now() + BOT_UPDATES_WARNING_DELAY;
  if (was_bot_updates_warning_) {
    send_request(make_object<td_api::setBotUpdatesStatus>(0, ""), std::make_unique<TdOnOkCallback>());
    was_bot_updates_warning_ = false;
  }
}

void Client::webhook_error(Status status) {
  CHECK(status.is_error());
  last_webhook_error_date_ = get_unix_time();
  last_webhook_error_ = std::move(status);

  auto pending_update_count = get_pending_update_count();
  if (pending_update_count >= MIN_PENDING_UPDATES_WARNING && td::Time::now() > next_bot_updates_warning_time_) {
    send_request(make_object<td_api::setBotUpdatesStatus>(td::narrow_cast<int32>(pending_update_count),
                                                          "Webhook error. " + last_webhook_error_.message().str()),
                 std::make_unique<TdOnOkCallback>());
    next_bot_updates_warning_time_ = td::Time::now_cached() + BOT_UPDATES_WARNING_DELAY;
    was_bot_updates_warning_ = true;
  }
}

void Client::webhook_closed(Status status) {
  LOG(WARNING) << "Webhook closed: " << status
               << ", webhook_query_type = " << (webhook_query_type_ == WebhookQueryType::Verify ? "verify" : "change");
  webhook_id_.release();
  webhook_url_ = td::string();
  if (has_webhook_certificate_) {
    td::unlink(get_webhook_certificate_path()).ignore();
    has_webhook_certificate_ = false;
  }
  webhook_max_connections_ = 0;
  webhook_ip_address_ = td::string();
  webhook_fix_ip_address_ = false;
  webhook_set_time_ = td::Time::now();
  last_webhook_error_date_ = 0;
  last_webhook_error_ = Status::OK();
  parameters_->shared_data_->webhook_db_->erase(bot_token_with_dc_);

  if (webhook_set_query_) {
    if (webhook_query_type_ == WebhookQueryType::Verify) {
      fail_query(400, PSLICE() << "Bad Request: bad webhook: " << status.message(), std::move(webhook_set_query_));
    } else {
      do_set_webhook(std::move(webhook_set_query_), true);
    }
  }
}

void Client::hangup_shared() {
  webhook_closed(Status::Error("Unknown"));
}

td::string Client::get_webhook_certificate_path() const {
  return dir_ + "cert.pem";
}

td::int32 Client::get_webhook_max_connections(const Query *query) const {
  auto default_value = parameters_->default_max_webhook_connections_;
  auto max_value = parameters_->local_mode_ ? 100000 : 100;
  return get_integer_arg(query, "max_connections", default_value, 1, max_value);
}

bool Client::get_webhook_fix_ip_address(const Query *query) {
  if (query->is_internal()) {
    return query->has_arg("fix_ip_address");
  }
  return !query->arg("ip_address").empty();
}

void Client::do_set_webhook(PromisedQueryPtr query, bool was_deleted) {
  CHECK(webhook_url_.empty());
  if (to_bool(query->arg("drop_pending_updates"))) {
    clear_tqueue();
  }
  Slice new_url;
  if (query->method() == "setwebhook") {
    new_url = query->arg("url");
  }
  if (!new_url.empty()) {
    auto url = td::parse_url(new_url, td::HttpUrl::Protocol::Https);
    if (url.is_error()) {
      return fail_query(400, "Bad Request: invalid webhook URL specified", std::move(query));
    }
    auto *cert_file_ptr = query->file("certificate");
    has_webhook_certificate_ = false;
    if (cert_file_ptr != nullptr) {
      auto size = cert_file_ptr->size;
      if (size > MAX_CERTIFICATE_FILE_SIZE) {
        return fail_query(400, PSLICE() << "Bad Request: certificate size is too big (" << size << " bytes)",
                          std::move(query));
      }
      auto from_path = cert_file_ptr->temp_file_name;
      auto to_path = get_webhook_certificate_path();
      auto status = td::copy_file(from_path, to_path, size);
      if (status.is_error()) {
        return fail_query(500, "Internal Server Error: failed to save certificate", std::move(query));
      }
      has_webhook_certificate_ = true;
    }

    if (query->is_internal() && query->arg("certificate") == "previous") {
      has_webhook_certificate_ = true;
    }
    webhook_url_ = new_url.str();
    webhook_set_time_ = td::Time::now();
    webhook_max_connections_ = get_webhook_max_connections(query.get());
    webhook_ip_address_ = query->arg("ip_address").str();
    webhook_fix_ip_address_ = get_webhook_fix_ip_address(query.get());
    last_webhook_error_date_ = 0;
    last_webhook_error_ = Status::OK();

    update_allowed_update_types(query.get());

    LOG(WARNING) << "Create " << (has_webhook_certificate_ ? "" : "not ") << "self signed webhook: " << url.ok();
    auto webhook_actor_name = PSTRING() << "Webhook " << url.ok();
    webhook_id_ = td::create_actor<WebhookActor>(
        webhook_actor_name, actor_shared(this, webhook_generation_), tqueue_id_, url.move_as_ok(),
        has_webhook_certificate_ ? get_webhook_certificate_path() : "", webhook_max_connections_, query->is_internal(),
        webhook_ip_address_, webhook_fix_ip_address_, parameters_);
    // wait for webhook verified or webhook callback
    webhook_query_type_ = WebhookQueryType::Verify;
    webhook_set_query_ = std::move(query);
  } else {
    answer_query(td::JsonTrue(), std::move(query),
                 was_deleted ? Slice("Webhook was deleted") : Slice("Webhook is already deleted"));
  }
}

void Client::do_send_message(object_ptr<td_api::InputMessageContent> input_message_content, PromisedQueryPtr query) {
  auto chat_id = query->arg("chat_id");
  auto reply_to_message_id = get_message_id(query.get(), "reply_to_message_id");
  auto allow_sending_without_reply = to_bool(query->arg("allow_sending_without_reply"));
  auto disable_notification = to_bool(query->arg("disable_notification"));
  auto r_reply_markup = get_reply_markup(query.get());
  if (r_reply_markup.is_error()) {
    return fail_query_with_error(std::move(query), 400, r_reply_markup.error().message());
  }
  auto reply_markup = r_reply_markup.move_as_ok();

  resolve_reply_markup_bot_usernames(
      std::move(reply_markup), std::move(query),
      [this, chat_id = chat_id.str(), reply_to_message_id, allow_sending_without_reply, disable_notification,
       input_message_content = std::move(input_message_content)](object_ptr<td_api::ReplyMarkup> reply_markup,
                                                                 PromisedQueryPtr query) mutable {
        auto on_success = [this, disable_notification, input_message_content = std::move(input_message_content),
                           reply_markup = std::move(reply_markup)](int64 chat_id, int64 reply_to_message_id,
                                                                   PromisedQueryPtr query) mutable {
          send_request(make_object<td_api::sendMessage>(chat_id, 0, reply_to_message_id,
                                                        get_message_send_options(disable_notification),
                                                        std::move(reply_markup), std::move(input_message_content)),
                       std::make_unique<TdOnSendMessageCallback>(this, std::move(query)));
        };
        check_message(chat_id, reply_to_message_id, reply_to_message_id <= 0 || allow_sending_without_reply,
                      AccessRights::Write, "replied message", std::move(query), std::move(on_success));
      });
}

td::int64 Client::get_send_message_query_id(PromisedQueryPtr query, bool is_multisend) {
  auto query_id = current_send_message_query_id_++;
  auto &pending_query = pending_send_message_queries_[query_id];
  pending_query.query = std::move(query);
  pending_query.is_multisend = is_multisend;
  return query_id;
}

void Client::on_sent_message(object_ptr<td_api::message> &&message, int64 query_id) {
  CHECK(message != nullptr);
  int64 chat_id = message->chat_id_;
  int64 message_id = message->id_;
  int64 reply_to_message_id = message->reply_to_message_id_;
  if (reply_to_message_id > 0) {
    CHECK(message->reply_in_chat_id_ == chat_id);
    bool is_inserted = yet_unsent_reply_message_ids_[{chat_id, reply_to_message_id}].insert(message_id).second;
    CHECK(is_inserted);
  }

  FullMessageId yet_unsent_message_id{chat_id, message_id};
  YetUnsentMessage yet_unsent_message;
  yet_unsent_message.reply_to_message_id = reply_to_message_id;
  yet_unsent_message.send_message_query_id = query_id;
  auto emplace_result = yet_unsent_messages_.emplace(yet_unsent_message_id, yet_unsent_message);
  CHECK(emplace_result.second);
  pending_send_message_queries_[query_id].awaited_messages++;
}

void Client::abort_long_poll(bool from_set_webhook) {
  if (long_poll_query_) {
    Slice message;
    if (from_set_webhook) {
      message = Slice("Conflict: terminated by setWebhook request");
    } else {
      message =
          Slice("Conflict: terminated by other getUpdates request; make sure that only one bot instance is running");
    }
    fail_query_conflict(message, std::move(long_poll_query_));
  }
}

void Client::fail_query_conflict(Slice message, PromisedQueryPtr &&query) {
  auto now = td::Time::now_cached();
  if (now >= next_get_updates_conflict_time_) {
    fail_query(409, message, std::move(query));
    next_get_updates_conflict_time_ = now + 3.0;
  } else {
    td::create_actor<td::SleepActor>(
        "FailQueryConflictSleepActor", 3.0,
        td::PromiseCreator::lambda([message = message.str(), query = std::move(query)](td::Result<> result) mutable {
          fail_query(409, message, std::move(query));
        }))
        .release();
  }
}

class Client::JsonUpdates : public Jsonable {
 public:
  explicit JsonUpdates(td::Span<td::TQueue::Event> updates) : updates_(updates) {
  }
  void store(JsonValueScope *scope) const {
    auto array = scope->enter_array();
    int left_len = 1 << 22;
    for (auto &update : updates_) {
      left_len -= 50 + td::narrow_cast<int>(update.data.size());
      if (left_len <= 0) {
        break;
      }
      array << JsonUpdate(update.id.value(), update.data);
    }
  }

 private:
  td::Span<td::TQueue::Event> updates_;
};

void Client::do_get_updates(int32 offset, int32 limit, int32 timeout, PromisedQueryPtr query) {
  auto &tqueue = parameters_->shared_data_->tqueue_;
  LOG(DEBUG) << "Get updates with offset = " << offset << ", limit = " << limit << " and timeout = " << timeout;
  LOG(DEBUG) << "Queue head = " << tqueue->get_head(tqueue_id_) << ", queue tail = " << tqueue->get_tail(tqueue_id_);

  if (offset <= 0) {
    auto head = tqueue->get_head(tqueue_id_);
    if (!head.empty() && offset < 0) {
      // negative offset is counted from the end
      auto tail = tqueue->get_tail(tqueue_id_);
      CHECK(!tail.empty());
      offset += tail.value();
    }
    if (offset < head.value()) {
      offset = head.value();
    }
  }

  auto updates = mutable_span(parameters_->shared_data_->event_buffer_, SharedData::TQUEUE_EVENT_BUFFER_SIZE);
  updates.truncate(limit);
  td::TQueue::EventId from;
  size_t total_size = 0;
  if (offset <= 0) {
    // queue is not created yet
    updates = {};
  } else {
    bool is_ok = false;
    auto r_offset = td::TQueue::EventId::from_int32(offset);
    auto now = get_unix_time();
    if (r_offset.is_ok()) {
      from = r_offset.ok();
      auto r_total_size = tqueue->get(tqueue_id_, from, true, now, updates);
      if (r_total_size.is_ok()) {
        is_ok = true;
        total_size = r_total_size.move_as_ok();
      }
    }
    if (!is_ok) {
      from = tqueue->get_head(tqueue_id_);
      auto r_total_size = tqueue->get(tqueue_id_, from, true, now, updates);
      CHECK(r_total_size.is_ok());
      total_size = r_total_size.move_as_ok();
    }
  }
  CHECK(total_size >= updates.size());
  total_size -= updates.size();

  bool need_warning = false;
  if (total_size <= MIN_PENDING_UPDATES_WARNING / 2) {
    if (last_pending_update_count_ > MIN_PENDING_UPDATES_WARNING) {
      need_warning = true;
      last_pending_update_count_ = MIN_PENDING_UPDATES_WARNING;
    }
  } else if (total_size >= last_pending_update_count_) {
    need_warning = true;
    while (total_size >= last_pending_update_count_) {
      last_pending_update_count_ *= 2;
    }
  }
  if (need_warning) {
    LOG(WARNING) << "Found " << updates.size() << " updates out of " << (total_size + updates.size())
                 << " after last getUpdates call " << (query->start_timestamp() - previous_get_updates_finish_time_)
                 << " seconds ago in " << (td::Time::now() - query->start_timestamp()) << " seconds";
  } else {
    LOG(DEBUG) << "Found " << updates.size() << " updates out of " << total_size << " from " << from;
  }

  if (timeout != 0 && updates.size() == 0) {
    abort_long_poll(false);
    long_poll_offset_ = offset;
    long_poll_limit_ = limit;
    long_poll_query_ = std::move(query);
    long_poll_was_wakeup_ = false;
    long_poll_hard_timeout_ = td::Time::now_cached() + timeout;
    long_poll_slot_.set_event(td::EventCreator::raw(actor_id(), static_cast<td::uint64>(0)));
    long_poll_slot_.set_timeout_at(long_poll_hard_timeout_);
    return;
  }
  previous_get_updates_finish_time_ = td::Time::now();
  next_bot_updates_warning_time_ = td::Time::now() + BOT_UPDATES_WARNING_DELAY;
  if (total_size == updates.size() && was_bot_updates_warning_) {
    send_request(make_object<td_api::setBotUpdatesStatus>(0, ""), std::make_unique<TdOnOkCallback>());
    was_bot_updates_warning_ = false;
  }
  answer_query(JsonUpdates(updates), std::move(query));
}

void Client::long_poll_wakeup(bool force_flag) {
  if (!long_poll_query_) {
    auto pending_update_count = get_pending_update_count();
    if (pending_update_count >= MIN_PENDING_UPDATES_WARNING && td::Time::now() > next_bot_updates_warning_time_) {
      send_request(make_object<td_api::setBotUpdatesStatus>(td::narrow_cast<int32>(pending_update_count),
                                                            "The getUpdates method is not called for too long"),
                   std::make_unique<TdOnOkCallback>());
      next_bot_updates_warning_time_ =
          td::Time::now_cached() + BOT_UPDATES_WARNING_DELAY;  // do not send warnings too often
      was_bot_updates_warning_ = true;
    }
    return;
  }
  if (force_flag) {
    do_get_updates(long_poll_offset_, long_poll_limit_, 0, std::move(long_poll_query_));
  } else {
    double now = td::Time::now();
    if (!long_poll_was_wakeup_) {
      long_poll_hard_timeout_ = td::min(now + LONG_POLL_MAX_DELAY, long_poll_hard_timeout_);
      long_poll_was_wakeup_ = true;
    }
    double timeout = td::min(now + LONG_POLL_WAIT_AFTER, long_poll_hard_timeout_);
    long_poll_slot_.set_event(td::EventCreator::raw(actor_id(), static_cast<td::uint64>(0)));
    long_poll_slot_.set_timeout_at(timeout);
  }
}

void Client::add_user(std::unordered_map<int32, UserInfo> &users, object_ptr<td_api::user> &&user) {
  auto user_info = &users[user->id_];
  user_info->first_name = std::move(user->first_name_);
  user_info->last_name = std::move(user->last_name_);
  user_info->username = std::move(user->username_);
  user_info->language_code = std::move(user->language_code_);

  user_info->have_access = user->have_access_;

  switch (user->type_->get_id()) {
    case td_api::userTypeRegular::ID:
      user_info->type = UserInfo::Type::Regular;
      break;
    case td_api::userTypeBot::ID: {
      user_info->type = UserInfo::Type::Bot;
      auto *bot = static_cast<const td_api::userTypeBot *>(user->type_.get());
      user_info->can_join_groups = bot->can_join_groups_;
      user_info->can_read_all_group_messages = bot->can_read_all_group_messages_;
      user_info->is_inline_bot = bot->is_inline_;
      break;
    }
    case td_api::userTypeDeleted::ID:
      user_info->type = UserInfo::Type::Deleted;
      break;
    case td_api::userTypeUnknown::ID:
      user_info->type = UserInfo::Type::Unknown;
      break;
    default:
      UNREACHABLE();
      break;
  }
}

const Client::UserInfo *Client::get_user_info(int32 user_id) const {
  auto it = users_.find(user_id);
  return it == users_.end() ? nullptr : &it->second;
}

void Client::set_user_bio(int32 user_id, td::string &&bio) {
  auto user_info = &users_[user_id];
  user_info->bio = std::move(bio);
}

void Client::add_group(std::unordered_map<int32, GroupInfo> &groups, object_ptr<td_api::basicGroup> &&group) {
  auto group_info = &groups[group->id_];
  group_info->member_count = group->member_count_;
  group_info->left = group->status_->get_id() == td_api::chatMemberStatusLeft::ID;
  group_info->kicked = group->status_->get_id() == td_api::chatMemberStatusBanned::ID;
  group_info->is_active = group->is_active_;
  group_info->upgraded_to_supergroup_id = group->upgraded_to_supergroup_id_;
  if (!group_info->left && !group_info->kicked && group_info->member_count == 0) {
    group_info->member_count = 1;
  }
}

const Client::GroupInfo *Client::get_group_info(int32 group_id) const {
  auto it = groups_.find(group_id);
  return it == groups_.end() ? nullptr : &it->second;
}

void Client::set_group_description(int32 group_id, td::string &&descripton) {
  auto group_info = &groups_[group_id];
  group_info->description = std::move(descripton);
}

void Client::set_group_invite_link(int32 group_id, td::string &&invite_link) {
  auto group_info = &groups_[group_id];
  group_info->invite_link = std::move(invite_link);
}

void Client::add_supergroup(std::unordered_map<int32, SupergroupInfo> &supergroups,
                            object_ptr<td_api::supergroup> &&supergroup) {
  auto supergroup_info = &supergroups[supergroup->id_];
  supergroup_info->username = std::move(supergroup->username_);
  supergroup_info->date = supergroup->date_;
  supergroup_info->status = std::move(supergroup->status_);
  supergroup_info->is_supergroup = !supergroup->is_channel_;
  supergroup_info->has_location = supergroup->has_location_;
}

void Client::set_supergroup_description(int32 supergroup_id, td::string &&descripton) {
  auto supergroup_info = &supergroups_[supergroup_id];
  supergroup_info->description = std::move(descripton);
}

void Client::set_supergroup_invite_link(int32 supergroup_id, td::string &&invite_link) {
  auto supergroup_info = &supergroups_[supergroup_id];
  supergroup_info->invite_link = std::move(invite_link);
}

void Client::set_supergroup_sticker_set_id(int32 supergroup_id, int64 sticker_set_id) {
  auto supergroup_info = &supergroups_[supergroup_id];
  supergroup_info->sticker_set_id = sticker_set_id;
}

void Client::set_supergroup_can_set_sticker_set(int32 supergroup_id, bool can_set_sticker_set) {
  auto supergroup_info = &supergroups_[supergroup_id];
  supergroup_info->can_set_sticker_set = can_set_sticker_set;
}

void Client::set_supergroup_slow_mode_delay(int32 supergroup_id, int32 slow_mode_delay) {
  auto supergroup_info = &supergroups_[supergroup_id];
  supergroup_info->slow_mode_delay = slow_mode_delay;
}

void Client::set_supergroup_linked_chat_id(int32 supergroup_id, int64 linked_chat_id) {
  auto supergroup_info = &supergroups_[supergroup_id];
  supergroup_info->linked_chat_id = linked_chat_id;
}

void Client::set_supergroup_location(int32 supergroup_id, object_ptr<td_api::chatLocation> location) {
  auto supergroup_info = &supergroups_[supergroup_id];
  supergroup_info->location = std::move(location);
}

const Client::SupergroupInfo *Client::get_supergroup_info(int32 supergroup_id) const {
  auto it = supergroups_.find(supergroup_id);
  return it == supergroups_.end() ? nullptr : &it->second;
}

Client::ChatInfo *Client::add_chat(int64 chat_id) {
  LOG(DEBUG) << "Update chat " << chat_id;
  return &chats_[chat_id];
}

const Client::ChatInfo *Client::get_chat(int64 chat_id) const {
  auto it = chats_.find(chat_id);
  if (it == chats_.end()) {
    return nullptr;
  }
  return &it->second;
}

Client::ChatType Client::get_chat_type(int64 chat_id) const {
  auto chat_info = get_chat(chat_id);
  if (chat_info == nullptr) {
    return ChatType::Unknown;
  }
  switch (chat_info->type) {
    case ChatInfo::Type::Private:
      return ChatType::Private;
    case ChatInfo::Type::Group:
      return ChatType::Group;
    case ChatInfo::Type::Supergroup: {
      auto supergroup_info = get_supergroup_info(chat_info->supergroup_id);
      if (supergroup_info == nullptr) {
        return ChatType::Unknown;
      }
      if (supergroup_info->is_supergroup) {
        return ChatType::Supergroup;
      } else {
        return ChatType::Channel;
      }
    }
    case ChatInfo::Type::Unknown:
      return ChatType::Unknown;
    default:
      UNREACHABLE();
      return ChatType::Unknown;
  }
}

td::string Client::get_chat_description(int64 chat_id) const {
  auto chat_info = get_chat(chat_id);
  if (chat_info == nullptr) {
    return PSTRING() << "unknown chat " << chat_id;
  }
  switch (chat_info->type) {
    case ChatInfo::Type::Private: {
      auto user_info = get_user_info(chat_info->user_id);
      return PSTRING() << "private " << (user_info == nullptr || !user_info->have_access ? "un" : "")
                       << "accessible chat " << chat_id;
    }
    case ChatInfo::Type::Group: {
      auto group_info = get_group_info(chat_info->group_id);
      if (group_info == nullptr) {
        return PSTRING() << "unknown group chat " << chat_id;
      }
      return PSTRING() << (group_info->is_active ? "" : "in") << "active group chat " << chat_id << ", chat status = "
                       << (group_info->kicked ? "kicked" : (group_info->left ? "left" : "member"));
    }
    case ChatInfo::Type::Supergroup: {
      auto supergroup_info = get_supergroup_info(chat_info->supergroup_id);
      if (supergroup_info == nullptr) {
        return PSTRING() << "unknown supergroup chat " << chat_id;
      }
      return PSTRING() << (supergroup_info->is_supergroup ? "supergroup" : "channel") << " chat " << chat_id
                       << ", chat status = " << to_string(supergroup_info->status)
                       << ", username = " << supergroup_info->username;
    }
    case ChatInfo::Type::Unknown:
      return PSTRING() << "unknown chat " << chat_id;
    default:
      UNREACHABLE();
      return "";
  }
}

void Client::json_store_file(td::JsonObjectScope &object, const td_api::file *file, bool with_path) const {
  if (file->id_ == 0) {
    return;
  }

  LOG_IF(ERROR, file->remote_->id_.empty()) << "File remote identifier is empty: " << td::oneline(to_string(*file));

  object("file_id", file->remote_->id_);
  object("file_unique_id", file->remote_->unique_id_);
  if (file->size_) {
    object("file_size", file->size_);
  }
  if (with_path && file->local_->is_downloading_completed_) {
    if (parameters_->local_mode_) {
      if (td::check_utf8(file->local_->path_)) {
        object("file_path", file->local_->path_);
      } else {
        object("file_path", td::JsonRawString(file->local_->path_));
      }
    } else {
      Slice relative_path = td::PathView::relative(file->local_->path_, dir_, true);
      if (!relative_path.empty() && file->local_->downloaded_size_ <= MAX_DOWNLOAD_FILE_SIZE) {
        object("file_path", relative_path);
      }
    }
  }
}

void Client::json_store_thumbnail(td::JsonObjectScope &object, const td_api::thumbnail *thumbnail) const {
  if (thumbnail == nullptr || thumbnail->format_->get_id() == td_api::thumbnailFormatMpeg4::ID) {
    return;
  }

  CHECK(thumbnail->file_->id_ > 0);
  object("thumb", JsonThumbnail(thumbnail, this));
}

void Client::json_store_callback_query_payload(td::JsonObjectScope &object,
                                               const td_api::CallbackQueryPayload *payload) {
  CHECK(payload != nullptr);
  switch (payload->get_id()) {
    case td_api::callbackQueryPayloadData::ID: {
      auto data = static_cast<const td_api::callbackQueryPayloadData *>(payload);
      if (!td::check_utf8(data->data_)) {
        LOG(WARNING) << "Receive non-UTF-8 callback query data";
        object("data", td::JsonRawString(data->data_));
      } else {
        object("data", data->data_);
      }
      break;
    }
    case td_api::callbackQueryPayloadGame::ID:
      object("game_short_name", static_cast<const td_api::callbackQueryPayloadGame *>(payload)->game_short_name_);
      break;
    case td_api::callbackQueryPayloadDataWithPassword::ID:
      UNREACHABLE();
      break;
    default:
      UNREACHABLE();
  }
}

void Client::json_store_permissions(td::JsonObjectScope &object, const td_api::chatPermissions *permissions) {
  object("can_send_messages", td::JsonBool(permissions->can_send_messages_));
  object("can_send_media_messages", td::JsonBool(permissions->can_send_media_messages_));
  object("can_send_polls", td::JsonBool(permissions->can_send_polls_));
  object("can_send_other_messages", td::JsonBool(permissions->can_send_other_messages_));
  object("can_add_web_page_previews", td::JsonBool(permissions->can_add_web_page_previews_));
  object("can_change_info", td::JsonBool(permissions->can_change_info_));
  object("can_invite_users", td::JsonBool(permissions->can_invite_users_));
  object("can_pin_messages", td::JsonBool(permissions->can_pin_messages_));
}

Client::Slice Client::get_update_type_name(UpdateType update_type) {
  switch (update_type) {
    case UpdateType::Message:
      return Slice("message");
    case UpdateType::EditedMessage:
      return Slice("edited_message");
    case UpdateType::ChannelPost:
      return Slice("channel_post");
    case UpdateType::EditedChannelPost:
      return Slice("edited_channel_post");
    case UpdateType::InlineQuery:
      return Slice("inline_query");
    case UpdateType::ChosenInlineResult:
      return Slice("chosen_inline_result");
    case UpdateType::CallbackQuery:
      return Slice("callback_query");
    case UpdateType::CustomEvent:
      return Slice("custom_event");
    case UpdateType::CustomQuery:
      return Slice("custom_query");
    case UpdateType::ShippingQuery:
      return Slice("shipping_query");
    case UpdateType::PreCheckoutQuery:
      return Slice("pre_checkout_query");
    case UpdateType::Poll:
      return Slice("poll");
    case UpdateType::PollAnswer:
      return Slice("poll_answer");
    case UpdateType::MyChatMember:
      return Slice("my_chat_member");
    case UpdateType::ChatMember:
      return Slice("chat_member");
    default:
      UNREACHABLE();
      return Slice();
  }
}

td::uint32 Client::get_allowed_update_types(td::MutableSlice allowed_updates, bool is_internal) {
  if (allowed_updates.empty()) {
    return 0;
  }

  LOG(INFO) << "Parsing JSON object: " << allowed_updates;
  auto r_value = json_decode(allowed_updates);
  if (r_value.is_error()) {
    LOG(INFO) << "Can't parse JSON object: " << r_value.error();
    return 0;
  }

  td::uint32 result = 0;
  auto value = r_value.move_as_ok();
  if (value.type() != JsonValue::Type::Array) {
    if (value.type() == JsonValue::Type::Number && is_internal) {
      auto r_number = td::to_integer_safe<td::uint32>(value.get_number());
      if (r_number.is_ok() && r_number.ok() > 0) {
        return r_number.ok();
      }
    }
    return 0;
  }
  for (auto &update_type_name : value.get_array()) {
    if (update_type_name.type() != JsonValue::Type::String) {
      return 0;
    }
    auto type_name = update_type_name.get_string();
    to_lower_inplace(type_name);
    for (int32 i = 0; i < static_cast<int32>(UpdateType::Size); i++) {
      if (get_update_type_name(static_cast<UpdateType>(i)) == type_name) {
        result |= (1 << i);
      }
    }
  }

  if (result == 0) {
    return DEFAULT_ALLOWED_UPDATE_TYPES;
  }
  return result;
}

bool Client::update_allowed_update_types(const Query *query) {
  auto allowed_update_types = get_allowed_update_types(query->arg("allowed_updates"), query->is_internal());
  if (allowed_update_types != 0 && allowed_update_types != allowed_update_types_) {
    allowed_update_types_ = allowed_update_types;
    object_ptr<td_api::OptionValue> value;
    if (allowed_update_types == DEFAULT_ALLOWED_UPDATE_TYPES) {
      value = make_object<td_api::optionValueEmpty>();
    } else {
      value = make_object<td_api::optionValueInteger>(allowed_update_types);
    }
    send_request(make_object<td_api::setOption>("xallowed_update_types", std::move(value)),
                 std::make_unique<TdOnOkCallback>());
    return true;
  }
  return false;
}

template <class T>
class UpdateJsonable : public td::VirtuallyJsonable {
 public:
  explicit UpdateJsonable(const T &update) : update(update) {
  }
  void store(JsonValueScope *scope) const override {
    *scope << update;
  }

 private:
  const T &update;
};

template <class T>
void Client::add_update(UpdateType update_type, const T &update, int32 timeout, int64 webhook_queue_id) {
  add_update_impl(update_type, UpdateJsonable<T>(update), timeout, webhook_queue_id);
}

void Client::add_update_impl(UpdateType update_type, const td::VirtuallyJsonable &update, int32 timeout,
                             int64 webhook_queue_id) {
  if (((allowed_update_types_ >> static_cast<int32>(update_type)) & 1) == 0) {
    return;
  }

  send_closure(stat_actor_, &BotStatActor::add_event<ServerBotStat::Update>, ServerBotStat::Update{}, td::Time::now());

  const size_t BUF_SIZE = 1 << 16;
  auto buf = td::StackAllocator::alloc(BUF_SIZE);
  td::JsonBuilder jb(td::StringBuilder(buf.as_slice(), true));
  jb.enter_value() << get_update_type_name(update_type);
  jb.string_builder() << ":";
  jb.enter_value() << update;
  if (jb.string_builder().is_error()) {
    LOG(ERROR) << "JSON buffer overflow";
    return;
  }

  auto update_slice = jb.string_builder().as_cslice();
  auto r_id = parameters_->shared_data_->tqueue_->push(tqueue_id_, update_slice.str(), get_unix_time() + timeout,
                                                       webhook_queue_id, td::TQueue::EventId());
  if (r_id.is_ok()) {
    auto id = r_id.move_as_ok();
    LOG(DEBUG) << "Update " << id << " was added for " << timeout << " seconds: " << update_slice;
    if (webhook_url_.empty()) {
      long_poll_wakeup(false);
    } else {
      send_closure(webhook_id_, &WebhookActor::update);
    }
  } else {
    LOG(DEBUG) << "Update failed to be added with error " << r_id.error() << " for " << timeout
               << " seconds: " << update_slice;
  }
}

void Client::add_new_message(object_ptr<td_api::message> &&message, bool is_edited) {
  CHECK(message != nullptr);

  if (message->sending_state_ != nullptr) {
    return;
  }

  auto chat_id = message->chat_id_;
  if (chat_id == 0) {
    LOG(ERROR) << "Receive invalid chat in " << to_string(message);
    return;
  }
  new_message_queues_[chat_id].queue_.emplace(std::move(message), is_edited);
  process_new_message_queue(chat_id);
}

void Client::add_update_poll(object_ptr<td_api::updatePoll> &&update) {
  CHECK(update != nullptr);
  add_update(UpdateType::Poll, JsonPoll(update->poll_.get(), this), 86400, update->poll_->id_);
}

void Client::add_update_poll_answer(object_ptr<td_api::updatePollAnswer> &&update) {
  CHECK(update != nullptr);
  add_update(UpdateType::PollAnswer, JsonPollAnswer(update.get(), this), 86400, update->poll_id_);
}

void Client::add_new_inline_query(int64 inline_query_id, int32 sender_user_id, object_ptr<td_api::location> location,
                                  object_ptr<td_api::ChatType> chat_type, const td::string &query,
                                  const td::string &offset) {
  add_update(UpdateType::InlineQuery,
             JsonInlineQuery(inline_query_id, sender_user_id, location.get(), chat_type.get(), query, offset, this), 30,
             sender_user_id + (static_cast<int64>(1) << 33));
}

void Client::add_new_chosen_inline_result(int32 sender_user_id, object_ptr<td_api::location> location,
                                          const td::string &query, const td::string &result_id,
                                          const td::string &inline_message_id) {
  add_update(UpdateType::ChosenInlineResult,
             JsonChosenInlineResult(sender_user_id, location.get(), query, result_id, inline_message_id, this), 600,
             sender_user_id + (static_cast<int64>(2) << 33));
}

void Client::add_new_callback_query(object_ptr<td_api::updateNewCallbackQuery> &&query) {
  CHECK(query != nullptr);
  auto user_id = query->sender_user_id_;
  if (user_id == 0) {
    LOG(ERROR) << "Receive invalid sender in " << to_string(query);
    return;
  }
  new_callback_query_queues_[user_id].queue_.push(std::move(query));
  process_new_callback_query_queue(user_id, 0);
}

void Client::process_new_callback_query_queue(int32 user_id, int state) {
  auto &queue = new_callback_query_queues_[user_id];
  if (queue.has_active_request_) {
    CHECK(state == 0);
    return;
  }
  if (logging_out_ || closing_) {
    new_callback_query_queues_.erase(user_id);
    return;
  }
  while (!queue.queue_.empty()) {
    auto &query = queue.queue_.front();
    int64 chat_id = query->chat_id_;
    int64 message_id = query->message_id_;
    auto message_info = get_message(chat_id, message_id);
    // callback message can be already deleted in the bot outbox
    if (state == 0) {
      if (message_info == nullptr) {
        // get the message from the server
        queue.has_active_request_ = true;
        return send_request(make_object<td_api::getCallbackQueryMessage>(chat_id, message_id, query->id_),
                            std::make_unique<TdOnGetCallbackQueryMessageCallback>(this, user_id, state));
      }
      state = 1;
    }
    if (state == 1) {
      auto reply_to_message_id =
          message_info == nullptr || message_info->is_reply_to_message_deleted ? 0 : message_info->reply_to_message_id;
      if (reply_to_message_id > 0 && get_message(chat_id, reply_to_message_id) == nullptr) {
        queue.has_active_request_ = true;
        return send_request(make_object<td_api::getRepliedMessage>(chat_id, message_id),
                            std::make_unique<TdOnGetCallbackQueryMessageCallback>(this, user_id, state));
      }
      state = 2;
    }
    if (state == 2) {
      auto message_sticker_set_id = message_info == nullptr ? 0 : get_sticker_set_id(message_info->content);
      if (!have_sticker_set_name(message_sticker_set_id)) {
        queue.has_active_request_ = true;
        return send_request(make_object<td_api::getStickerSet>(message_sticker_set_id),
                            std::make_unique<TdOnGetStickerSetCallback>(this, message_sticker_set_id, user_id, 0));
      }
      auto reply_to_message_id =
          message_info == nullptr || message_info->is_reply_to_message_deleted ? 0 : message_info->reply_to_message_id;
      if (reply_to_message_id > 0) {
        auto reply_to_message_info = get_message(chat_id, reply_to_message_id);
        auto reply_sticker_set_id =
            reply_to_message_info == nullptr ? 0 : get_sticker_set_id(reply_to_message_info->content);
        if (!have_sticker_set_name(reply_sticker_set_id)) {
          queue.has_active_request_ = true;
          return send_request(make_object<td_api::getStickerSet>(reply_sticker_set_id),
                              std::make_unique<TdOnGetStickerSetCallback>(this, reply_sticker_set_id, user_id, 0));
        }
      }
    }
    CHECK(state == 2);

    CHECK(user_id == query->sender_user_id_);
    add_update(UpdateType::CallbackQuery,
               JsonCallbackQuery(query->id_, user_id, chat_id, message_id, message_info, query->chat_instance_,
                                 query->payload_.get(), this),
               150, user_id + (static_cast<int64>(3) << 33));

    queue.queue_.pop();
  }
  new_callback_query_queues_.erase(user_id);
}

void Client::add_new_inline_callback_query(object_ptr<td_api::updateNewInlineCallbackQuery> &&query) {
  CHECK(query != nullptr);
  add_update(UpdateType::CallbackQuery,
             JsonInlineCallbackQuery(query->id_, query->sender_user_id_, query->inline_message_id_,
                                     query->chat_instance_, query->payload_.get(), this),
             150, query->sender_user_id_ + (static_cast<int64>(3) << 33));
}

void Client::add_new_shipping_query(object_ptr<td_api::updateNewShippingQuery> &&query) {
  CHECK(query != nullptr);
  add_update(UpdateType::ShippingQuery, JsonShippingQuery(query.get(), this), 150,
             query->sender_user_id_ + (static_cast<int64>(4) << 33));
}

void Client::add_new_pre_checkout_query(object_ptr<td_api::updateNewPreCheckoutQuery> &&query) {
  CHECK(query != nullptr);
  add_update(UpdateType::PreCheckoutQuery, JsonPreCheckoutQuery(query.get(), this), 150,
             query->sender_user_id_ + (static_cast<int64>(4) << 33));
}

void Client::add_new_custom_event(object_ptr<td_api::updateNewCustomEvent> &&event) {
  CHECK(event != nullptr);
  add_update(UpdateType::CustomEvent, JsonCustomJson(event->event_), 600, 0);
}

void Client::add_new_custom_query(object_ptr<td_api::updateNewCustomQuery> &&query) {
  CHECK(query != nullptr);
  int32 timeout = query->timeout_ <= 0 ? 86400 : query->timeout_;
  add_update(UpdateType::CustomQuery, JsonCustomJson(query->data_), timeout, 0);
}

void Client::add_update_chat_member(object_ptr<td_api::updateChatMember> &&update) {
  CHECK(update != nullptr);
  auto left_time = update->date_ + 86400 - get_unix_time();
  if (left_time > 0) {
    CHECK(update->old_chat_member_->member_id_ != nullptr);
    if (update->old_chat_member_->member_id_->get_id() != td_api::messageSenderUser::ID ||
        update->new_chat_member_->member_id_->get_id() != td_api::messageSenderUser::ID) {
      return;
    }
    auto user_id = static_cast<const td_api::messageSenderUser *>(update->old_chat_member_->member_id_.get())->user_id_;
    bool is_my = (user_id == my_id_);
    auto webhook_queue_id = update->chat_id_ + (static_cast<int64>(is_my ? 5 : 6) << 33);
    auto update_type = is_my ? UpdateType::MyChatMember : UpdateType::ChatMember;
    add_update(update_type, JsonChatMemberUpdated(update.get(), this), left_time, webhook_queue_id);
  }
}

td::int32 Client::choose_added_member_id(const td_api::messageChatAddMembers *message_add_members) const {
  CHECK(message_add_members != nullptr);
  for (auto &member_user_id : message_add_members->member_user_ids_) {
    if (member_user_id == my_id_) {
      return my_id_;
    }
  }
  if (message_add_members->member_user_ids_.empty()) {
    return 0;
  }
  return message_add_members->member_user_ids_[0];
}

bool Client::need_skip_update_message(int64 chat_id, const object_ptr<td_api::message> &message, bool is_edited) const {
  auto chat = get_chat(chat_id);
  CHECK(chat != nullptr);
  if (message->is_outgoing_) {
    switch (message->content_->get_id()) {
      case td_api::messageChatChangeTitle::ID:
      case td_api::messageChatChangePhoto::ID:
      case td_api::messageChatDeletePhoto::ID:
      case td_api::messageChatDeleteMember::ID:
      case td_api::messagePinMessage::ID:
      case td_api::messageProximityAlertTriggered::ID:
      case td_api::messageVoiceChatScheduled::ID:
      case td_api::messageVoiceChatStarted::ID:
      case td_api::messageVoiceChatEnded::ID:
      case td_api::messageInviteVoiceChatParticipants::ID:
        // don't skip
        break;
      default:
        return true;
    }
  }

  int32 message_date = message->edit_date_ == 0 ? message->date_ : message->edit_date_;
  if (message_date <= get_unix_time() - 86400) {
    // don't send messages received/edited more than 1 day ago
    return true;
  }

  if (chat->type == ChatInfo::Type::Supergroup) {
    auto supergroup_info = get_supergroup_info(chat->supergroup_id);
    if (supergroup_info->status->get_id() == td_api::chatMemberStatusLeft::ID ||
        supergroup_info->status->get_id() == td_api::chatMemberStatusBanned::ID) {
      // if we have left the chat, send only update about leaving the supergroup
      if (message->content_->get_id() == td_api::messageChatDeleteMember::ID) {
        auto user_id = static_cast<const td_api::messageChatDeleteMember *>(message->content_.get())->user_id_;
        return user_id != my_id_;
      }
      return true;
    }

    if (supergroup_info->date > message->date_ || authorization_date_ > message->date_) {
      // don't send messages received before join or getting authorization
      return true;
    }
  }

  if (message->ttl_ > 0 && message->ttl_expires_in_ == 0) {
    return true;
  }

  if (message->forward_info_ != nullptr &&
      message->forward_info_->origin_->get_id() == td_api::messageForwardOriginMessageImport::ID) {
    return true;
  }

  switch (message->content_->get_id()) {
    case td_api::messagePhoto::ID: {
      auto message_photo = static_cast<const td_api::messagePhoto *>(message->content_.get());
      if (message_photo->photo_ == nullptr) {
        LOG(ERROR) << "Got empty messagePhoto";
        return true;
      }
      break;
    }
    case td_api::messageChatAddMembers::ID: {
      auto message_add_members = static_cast<const td_api::messageChatAddMembers *>(message->content_.get());
      if (message_add_members->member_user_ids_.empty()) {
        LOG(ERROR) << "Got empty messageChatAddMembers";
        return true;
      }
      break;
    }
    case td_api::messageChatChangePhoto::ID: {
      auto message_change_photo = static_cast<const td_api::messageChatChangePhoto *>(message->content_.get());
      if (message_change_photo->photo_ == nullptr) {
        LOG(ERROR) << "Got empty messageChatChangePhoto";
        return true;
      }
      break;
    }
    case td_api::messageSupergroupChatCreate::ID: {
      if (chat->type != ChatInfo::Type::Supergroup) {
        LOG(ERROR) << "Receive messageSupergroupChatCreate in the non-supergroup chat " << chat_id;
        return true;
      }
      break;
    }
    case td_api::messagePinMessage::ID: {
      auto message_pin_message = static_cast<const td_api::messagePinMessage *>(message->content_.get());
      auto pinned_message_id = message_pin_message->message_id_;
      if (pinned_message_id <= 0) {
        return true;
      }
      const MessageInfo *pinned_message = get_message(chat_id, pinned_message_id);
      if (pinned_message == nullptr) {
        LOG(WARNING) << "Pinned unknown, inaccessible or deleted message " << pinned_message_id;
        return true;
      }
      break;
    }
    case td_api::messageProximityAlertTriggered::ID: {
      auto proximity_alert_triggered =
          static_cast<const td_api::messageProximityAlertTriggered *>(message->content_.get());
      return proximity_alert_triggered->traveler_->get_id() != td_api::messageSenderUser::ID ||
             proximity_alert_triggered->watcher_->get_id() != td_api::messageSenderUser::ID;
    }
    case td_api::messageGameScore::ID:
      return true;
    case td_api::messagePaymentSuccessful::ID:
      return true;
    case td_api::messagePassportDataSent::ID:
      return true;
    case td_api::messageCall::ID:
      return true;
    case td_api::messageUnsupported::ID:
      return true;
    case td_api::messageContactRegistered::ID:
      return true;
    case td_api::messageExpiredPhoto::ID:
      return true;
    case td_api::messageExpiredVideo::ID:
      return true;
    case td_api::messageCustomServiceAction::ID:
      return true;
    default:
      break;
  }

  if (is_edited) {
    const MessageInfo *old_message = get_message(chat_id, message->id_);
    if (old_message != nullptr && !old_message->is_content_changed) {
      return true;
    }
  }

  return false;
}

td::int64 &Client::get_reply_to_message_id(object_ptr<td_api::message> &message) {
  if (message->content_->get_id() == td_api::messagePinMessage::ID) {
    CHECK(message->reply_to_message_id_ == 0);
    return static_cast<td_api::messagePinMessage *>(message->content_.get())->message_id_;
  }
  if (message->reply_in_chat_id_ != message->chat_id_ && message->reply_to_message_id_ != 0) {
    LOG(WARNING) << "Drop reply to message " << message->id_ << " in chat " << message->chat_id_
                 << " from another chat " << message->reply_in_chat_id_;
    message->reply_in_chat_id_ = 0;
    message->reply_to_message_id_ = 0;
  }
  return message->reply_to_message_id_;
}

void Client::set_message_reply_to_message_id(MessageInfo *message_info, int64 reply_to_message_id) {
  if (message_info->reply_to_message_id == reply_to_message_id) {
    return;
  }

  if (message_info->reply_to_message_id > 0) {
    LOG_IF(ERROR, reply_to_message_id > 0)
        << "Message " << message_info->id << " in chat " << message_info->chat_id
        << " has changed reply_to_message from " << message_info->reply_to_message_id << " to " << reply_to_message_id;
    auto it = reply_message_ids_.find({message_info->chat_id, message_info->reply_to_message_id});
    if (it != reply_message_ids_.end()) {
      it->second.erase(message_info->id);
      if (it->second.empty()) {
        reply_message_ids_.erase(it);
      }
    }
  }
  if (reply_to_message_id > 0) {
    reply_message_ids_[{message_info->chat_id, reply_to_message_id}].insert(message_info->id);
  }

  message_info->reply_to_message_id = reply_to_message_id;
}

td::CSlice Client::get_callback_data(const object_ptr<td_api::InlineKeyboardButtonType> &type) {
  CHECK(type != nullptr);
  switch (type->get_id()) {
    case td_api::inlineKeyboardButtonTypeCallback::ID:
      return static_cast<const td_api::inlineKeyboardButtonTypeCallback *>(type.get())->data_;
    case td_api::inlineKeyboardButtonTypeCallbackWithPassword::ID:
      return static_cast<const td_api::inlineKeyboardButtonTypeCallbackWithPassword *>(type.get())->data_;
    default:
      UNREACHABLE();
      return td::CSlice();
  }
}

bool Client::are_equal_inline_keyboard_buttons(const td_api::inlineKeyboardButton *lhs,
                                               const td_api::inlineKeyboardButton *rhs) {
  CHECK(lhs != nullptr);
  CHECK(rhs != nullptr);
  if (lhs->text_ != rhs->text_) {
    return false;
  }
  if (lhs->type_->get_id() != rhs->type_->get_id()) {
    return false;
  }
  switch (lhs->type_->get_id()) {
    case td_api::inlineKeyboardButtonTypeUrl::ID: {
      auto lhs_type = static_cast<const td_api::inlineKeyboardButtonTypeUrl *>(lhs->type_.get());
      auto rhs_type = static_cast<const td_api::inlineKeyboardButtonTypeUrl *>(rhs->type_.get());
      return lhs_type->url_ == rhs_type->url_;
    }
    case td_api::inlineKeyboardButtonTypeLoginUrl::ID: {
      auto lhs_type = static_cast<const td_api::inlineKeyboardButtonTypeLoginUrl *>(lhs->type_.get());
      auto rhs_type = static_cast<const td_api::inlineKeyboardButtonTypeLoginUrl *>(rhs->type_.get());
      return lhs_type->url_ == rhs_type->url_;  // don't compare id_ and forward_text_
    }
    case td_api::inlineKeyboardButtonTypeCallback::ID:
    case td_api::inlineKeyboardButtonTypeCallbackWithPassword::ID:
      return get_callback_data(lhs->type_) == get_callback_data(rhs->type_);
    case td_api::inlineKeyboardButtonTypeCallbackGame::ID:
      return true;
    case td_api::inlineKeyboardButtonTypeSwitchInline::ID: {
      auto lhs_type = static_cast<const td_api::inlineKeyboardButtonTypeSwitchInline *>(lhs->type_.get());
      auto rhs_type = static_cast<const td_api::inlineKeyboardButtonTypeSwitchInline *>(rhs->type_.get());
      return lhs_type->query_ == rhs_type->query_ && lhs_type->in_current_chat_ == rhs_type->in_current_chat_;
    }
    case td_api::inlineKeyboardButtonTypeBuy::ID:
      return true;
    default:
      UNREACHABLE();
      return false;
  }
}

bool Client::are_equal_inline_keyboards(const td_api::replyMarkupInlineKeyboard *lhs,
                                        const td_api::replyMarkupInlineKeyboard *rhs) {
  CHECK(lhs != nullptr);
  CHECK(rhs != nullptr);
  auto &old_rows = lhs->rows_;
  auto &new_rows = rhs->rows_;
  if (old_rows.size() != new_rows.size()) {
    return false;
  }
  for (size_t i = 0; i < old_rows.size(); i++) {
    if (old_rows[i].size() != new_rows[i].size()) {
      return false;
    }
    for (size_t j = 0; j < old_rows[i].size(); j++) {
      if (!are_equal_inline_keyboard_buttons(old_rows[i][j].get(), new_rows[i][j].get())) {
        return false;
      }
    }
  }
  return true;
}

void Client::set_message_reply_markup(MessageInfo *message_info, object_ptr<td_api::ReplyMarkup> &&reply_markup) {
  if (reply_markup != nullptr && reply_markup->get_id() != td_api::replyMarkupInlineKeyboard::ID) {
    reply_markup = nullptr;
  }
  if (reply_markup == nullptr && message_info->reply_markup == nullptr) {
    return;
  }
  if (reply_markup != nullptr && message_info->reply_markup != nullptr) {
    CHECK(message_info->reply_markup->get_id() == td_api::replyMarkupInlineKeyboard::ID);
    if (are_equal_inline_keyboards(
            static_cast<const td_api::replyMarkupInlineKeyboard *>(message_info->reply_markup.get()),
            static_cast<const td_api::replyMarkupInlineKeyboard *>(reply_markup.get()))) {
      return;
    }
  }
  message_info->reply_markup = std::move(reply_markup);
  message_info->is_content_changed = true;
}

td::int64 Client::get_sticker_set_id(const object_ptr<td_api::MessageContent> &content) {
  if (content->get_id() != td_api::messageSticker::ID) {
    return 0;
  }

  return static_cast<const td_api::messageSticker *>(content.get())->sticker_->set_id_;
}

bool Client::have_sticker_set_name(int64 sticker_set_id) const {
  return sticker_set_id == 0 || sticker_set_names_.count(sticker_set_id) > 0;
}

Client::Slice Client::get_sticker_set_name(int64 sticker_set_id) const {
  auto it = sticker_set_names_.find(sticker_set_id);
  if (it == sticker_set_names_.end()) {
    return Slice();
  }
  return it->second;
}

void Client::process_new_message_queue(int64 chat_id) {
  auto &queue = new_message_queues_[chat_id];
  if (queue.has_active_request_) {
    return;
  }
  if (logging_out_ || closing_) {
    new_message_queues_.erase(chat_id);
    return;
  }
  while (!queue.queue_.empty()) {
    auto &message_ref = queue.queue_.front().message;
    CHECK(chat_id == message_ref->chat_id_);
    int64 message_id = message_ref->id_;
    int64 reply_to_message_id = get_reply_to_message_id(message_ref);
    if (reply_to_message_id > 0 && get_message(chat_id, reply_to_message_id) == nullptr) {
      queue.has_active_request_ = true;
      return send_request(make_object<td_api::getRepliedMessage>(chat_id, message_id),
                          std::make_unique<TdOnGetReplyMessageCallback>(this, chat_id));
    }
    auto message_sticker_set_id = get_sticker_set_id(message_ref->content_);
    if (!have_sticker_set_name(message_sticker_set_id)) {
      queue.has_active_request_ = true;
      return send_request(make_object<td_api::getStickerSet>(message_sticker_set_id),
                          std::make_unique<TdOnGetStickerSetCallback>(this, message_sticker_set_id, 0, chat_id));
    }
    if (reply_to_message_id > 0) {
      auto reply_to_message_info = get_message(chat_id, reply_to_message_id);
      CHECK(reply_to_message_info != nullptr);
      auto reply_sticker_set_id = get_sticker_set_id(reply_to_message_info->content);
      if (!have_sticker_set_name(reply_sticker_set_id)) {
        queue.has_active_request_ = true;
        return send_request(make_object<td_api::getStickerSet>(reply_sticker_set_id),
                            std::make_unique<TdOnGetStickerSetCallback>(this, reply_sticker_set_id, 0, chat_id));
      }
    }

    auto message = std::move(message_ref);
    auto is_edited = queue.queue_.front().is_edited;
    queue.queue_.pop();
    if (need_skip_update_message(chat_id, message, is_edited)) {
      add_message(std::move(message));
      continue;
    }

    auto chat = get_chat(chat_id);
    CHECK(chat != nullptr);
    bool is_channel_post =
        (chat->type == ChatInfo::Type::Supergroup && !get_supergroup_info(chat->supergroup_id)->is_supergroup);

    UpdateType update_type;
    if (is_channel_post) {
      update_type = is_edited ? UpdateType::EditedChannelPost : UpdateType::ChannelPost;
    } else {
      update_type = is_edited ? UpdateType::EditedMessage : UpdateType::Message;
    }

    int32 message_date = message->edit_date_ == 0 ? message->date_ : message->edit_date_;
    auto now = get_unix_time();
    auto update_delay_time = now - td::max(message_date, parameters_->shared_data_->get_unix_time(webhook_set_time_));
    const auto UPDATE_DELAY_WARNING_TIME = 10 * 60;
    LOG_IF(ERROR, update_delay_time > UPDATE_DELAY_WARNING_TIME)
        << "Receive very old update " << get_update_type_name(update_type) << " sent at " << message_date << " to chat "
        << chat_id << " with a delay of " << update_delay_time << " seconds: " << to_string(message);
    auto left_time = message_date + 86400 - now;
    add_message(std::move(message));

    auto message_info = get_message(chat_id, message_id);
    CHECK(message_info != nullptr);

    message_info->is_content_changed = false;
    add_update(update_type, JsonMessage(message_info, true, get_update_type_name(update_type).str(), this), left_time,
               chat_id);
  }
  new_message_queues_.erase(chat_id);
}

void Client::remove_replies_to_message(int64 chat_id, int64 reply_to_message_id, bool only_from_cache) {
  if (!only_from_cache) {
    auto yet_unsent_it = yet_unsent_reply_message_ids_.find({chat_id, reply_to_message_id});
    if (yet_unsent_it != yet_unsent_reply_message_ids_.end()) {
      for (auto message_id : yet_unsent_it->second) {
        auto &message = yet_unsent_messages_[{chat_id, message_id}];
        CHECK(message.reply_to_message_id == reply_to_message_id);
        message.is_reply_to_message_deleted = true;
      }
    }
  }

  auto it = reply_message_ids_.find({chat_id, reply_to_message_id});
  if (it == reply_message_ids_.end()) {
    return;
  }

  if (!only_from_cache) {
    for (auto message_id : it->second) {
      auto message_info = get_message_editable(chat_id, message_id);
      CHECK(message_info != nullptr);
      CHECK(message_info->reply_to_message_id == reply_to_message_id);
      message_info->reply_to_message_id = 0;
    }
  }
  reply_message_ids_.erase(it);
}

void Client::delete_message(int64 chat_id, int64 message_id, bool only_from_cache) {
  remove_replies_to_message(chat_id, message_id, only_from_cache);

  auto it = messages_.find({chat_id, message_id});
  if (it == messages_.end()) {
    if (yet_unsent_messages_.count({chat_id, message_id}) > 0) {
      // yet unsent message is deleted, possible only if we are trying to write to inaccessible supergroup or
      // sent message was deleted before added to the chat
      auto chat_info = get_chat(chat_id);
      CHECK(chat_info != nullptr);

      Status error =
          Status::Error(500, "Internal Server Error: sent message was immediately deleted and can't be returned");
      if (chat_info->type == ChatInfo::Type::Supergroup) {
        auto supergroup_info = get_supergroup_info(chat_info->supergroup_id);
        CHECK(supergroup_info != nullptr);
        if (supergroup_info->status->get_id() == td_api::chatMemberStatusBanned::ID ||
            supergroup_info->status->get_id() == td_api::chatMemberStatusLeft::ID) {
          if (supergroup_info->is_supergroup) {
            error = Status::Error(403, "Forbidden: bot is not a member of the supergroup chat");
          } else {
            error = Status::Error(403, "Forbidden: bot is not a member of the channel chat");
          }
        }
      }

      on_message_send_failed(chat_id, message_id, 0, std::move(error));
    }
    return;
  }

  auto message_info = it->second.get();
  CHECK(message_info->lru_next != nullptr);
  message_info->lru_next->lru_prev = message_info->lru_prev;
  message_info->lru_prev->lru_next = message_info->lru_next;

  set_message_reply_to_message_id(message_info, 0);

  messages_.erase(it);
}

void Client::schedule_next_delete_messages_lru() {
  CHECK(!next_delete_messages_lru_timeout_.has_timeout());
  next_delete_messages_lru_timeout_.set_callback(Client::delete_messages_lru);
  next_delete_messages_lru_timeout_.set_callback_data(static_cast<void *>(this));
  next_delete_messages_lru_timeout_.set_timeout_in(td::Random::fast(MESSAGES_CACHE_TIME, 2 * MESSAGES_CACHE_TIME));
}

void Client::delete_messages_lru(void *client_void) {
  CHECK(client_void != nullptr);
  auto client = static_cast<Client *>(client_void);

  auto now = td::Time::now();
  int32 deleted_message_count = 0;
  while (client->messages_lru_root_.lru_next->access_time < now - MESSAGES_CACHE_TIME) {
    auto message = client->messages_lru_root_.lru_next;
    if (client->yet_unsent_reply_message_ids_.count({message->chat_id, message->id})) {
      LOG(DEBUG) << "Force usage of message " << message->id << " in " << message->chat_id;
      client->update_message_lru(message);
    } else {
      client->delete_message(message->chat_id, message->id, true);
      deleted_message_count++;
    }
  }

  if (deleted_message_count != 0) {
    LOG(DEBUG) << "Delete " << deleted_message_count << " messages from cache";
  }
  client->schedule_next_delete_messages_lru();
}

void Client::update_message_lru(const MessageInfo *message_info) const {
  message_info->access_time = td::Time::now();
  if (message_info->lru_next != nullptr) {
    message_info->lru_next->lru_prev = message_info->lru_prev;
    message_info->lru_prev->lru_next = message_info->lru_next;
  }
  auto prev = messages_lru_root_.lru_prev;
  message_info->lru_prev = prev;
  prev->lru_next = message_info;
  message_info->lru_next = &messages_lru_root_;
  messages_lru_root_.lru_prev = message_info;
}

Client::FullMessageId Client::add_message(object_ptr<td_api::message> &&message, bool force_update_content) {
  CHECK(message != nullptr);
  CHECK(message->sending_state_ == nullptr);

  int64 chat_id = message->chat_id_;
  int64 message_id = message->id_;

  LOG(DEBUG) << "Add message " << message_id << " to chat " << chat_id;
  std::unique_ptr<MessageInfo> message_info;
  auto it = messages_.find({chat_id, message_id});
  if (it == messages_.end()) {
    message_info = std::make_unique<MessageInfo>();
  } else {
    message_info = std::move(it->second);
  }

  update_message_lru(message_info.get());

  message_info->id = message_id;
  message_info->chat_id = chat_id;
  message_info->date = message->date_;
  message_info->edit_date = message->edit_date_;
  message_info->media_album_id = message->media_album_id_;
  message_info->via_bot_user_id = message->via_bot_user_id_;

  CHECK(message->sender_ != nullptr);
  switch (message->sender_->get_id()) {
    case td_api::messageSenderUser::ID: {
      auto sender = move_object_as<td_api::messageSenderUser>(message->sender_);
      message_info->sender_user_id = sender->user_id_;
      CHECK(message_info->sender_user_id > 0);
      break;
    }
    case td_api::messageSenderChat::ID: {
      auto sender = move_object_as<td_api::messageSenderChat>(message->sender_);
      message_info->sender_chat_id = sender->chat_id_;

      auto chat_type = get_chat_type(chat_id);
      if (chat_type != ChatType::Channel) {
        if (message_info->sender_chat_id == chat_id) {
          message_info->sender_user_id = group_anonymous_bot_user_id_;
        } else {
          message_info->sender_user_id = service_notifications_user_id_;
        }
        CHECK(message_info->sender_user_id > 0);
      }
      break;
    }
    default:
      UNREACHABLE();
  }

  message_info->initial_chat_id = 0;
  message_info->initial_sender_user_id = 0;
  message_info->initial_sender_chat_id = 0;
  message_info->initial_send_date = 0;
  message_info->initial_message_id = 0;
  message_info->initial_author_signature = td::string();
  message_info->initial_sender_name = td::string();
  if (message->forward_info_ != nullptr) {
    message_info->initial_send_date = message->forward_info_->date_;
    auto origin = std::move(message->forward_info_->origin_);
    switch (origin->get_id()) {
      case td_api::messageForwardOriginUser::ID: {
        auto forward_info = move_object_as<td_api::messageForwardOriginUser>(origin);
        message_info->initial_sender_user_id = forward_info->sender_user_id_;
        break;
      }
      case td_api::messageForwardOriginChat::ID: {
        auto forward_info = move_object_as<td_api::messageForwardOriginChat>(origin);
        message_info->initial_sender_chat_id = forward_info->sender_chat_id_;
        message_info->initial_author_signature = std::move(forward_info->author_signature_);
        break;
      }
      case td_api::messageForwardOriginHiddenUser::ID: {
        auto forward_info = move_object_as<td_api::messageForwardOriginHiddenUser>(origin);
        message_info->initial_sender_name = std::move(forward_info->sender_name_);
        break;
      }
      case td_api::messageForwardOriginChannel::ID: {
        auto forward_info = move_object_as<td_api::messageForwardOriginChannel>(origin);
        message_info->initial_chat_id = forward_info->chat_id_;
        message_info->initial_message_id = forward_info->message_id_;
        message_info->initial_author_signature = std::move(forward_info->author_signature_);
        break;
      }
      case td_api::messageForwardOriginMessageImport::ID: {
        auto forward_info = move_object_as<td_api::messageForwardOriginMessageImport>(origin);
        message_info->initial_sender_name = std::move(forward_info->sender_name_);
        break;
      }
      default:
        UNREACHABLE();
    }
  }

  message_info->author_signature = std::move(message->author_signature_);

  if (message->reply_in_chat_id_ != chat_id && message->reply_to_message_id_ != 0) {
    LOG(WARNING) << "Drop reply to message " << message_id << " in chat " << chat_id << " from another chat "
                 << message->reply_in_chat_id_;
    message->reply_in_chat_id_ = 0;
    message->reply_to_message_id_ = 0;
  }
  set_message_reply_to_message_id(message_info.get(), message->reply_to_message_id_);
  if (message_info->content == nullptr || force_update_content) {
    message_info->content = std::move(message->content_);
    message_info->is_content_changed = true;

    auto sticker_set_id = get_sticker_set_id(message_info->content);
    if (!have_sticker_set_name(sticker_set_id)) {
      send_request(make_object<td_api::getStickerSet>(sticker_set_id),
                   std::make_unique<TdOnGetStickerSetCallback>(this, sticker_set_id, 0, 0));
    }
  }
  set_message_reply_markup(message_info.get(), std::move(message->reply_markup_));

  messages_[{chat_id, message_id}] = std::move(message_info);
  message = nullptr;

  return {chat_id, message_id};
}

void Client::update_message_content(int64 chat_id, int64 message_id, object_ptr<td_api::MessageContent> &&content) {
  auto message_info = get_message_editable(chat_id, message_id);
  if (message_info == nullptr) {
    return;
  }
  LOG(DEBUG) << "Update content of the message " << message_id << " from chat " << chat_id;

  message_info->content = std::move(content);
  message_info->is_content_changed = true;
}

void Client::on_update_message_edited(int64 chat_id, int64 message_id, int32 edit_date,
                                      object_ptr<td_api::ReplyMarkup> &&reply_markup) {
  auto message_info = get_message_editable(chat_id, message_id);
  if (message_info == nullptr) {
    return;
  }
  message_info->edit_date = edit_date;
  set_message_reply_markup(message_info, std::move(reply_markup));
}

const Client::MessageInfo *Client::get_message(int64 chat_id, int64 message_id) const {
  auto it = messages_.find({chat_id, message_id});
  if (it == messages_.end()) {
    LOG(DEBUG) << "Not found message " << message_id << " from chat " << chat_id;
    return nullptr;
  }
  LOG(DEBUG) << "Found message " << message_id << " from chat " << chat_id;

  auto result = it->second.get();
  update_message_lru(result);
  return result;
}

Client::MessageInfo *Client::get_message_editable(int64 chat_id, int64 message_id) {
  auto it = messages_.find({chat_id, message_id});
  if (it == messages_.end()) {
    LOG(DEBUG) << "Not found message " << message_id << " from chat " << chat_id;
    return nullptr;
  }
  LOG(DEBUG) << "Found message " << message_id << " from chat " << chat_id;

  auto result = it->second.get();
  update_message_lru(result);
  return result;
}

td::string Client::get_chat_member_status(const object_ptr<td_api::ChatMemberStatus> &status) {
  CHECK(status != nullptr);
  switch (status->get_id()) {
    case td_api::chatMemberStatusCreator::ID:
      return "creator";
    case td_api::chatMemberStatusAdministrator::ID:
      return "administrator";
    case td_api::chatMemberStatusMember::ID:
      return "member";
    case td_api::chatMemberStatusRestricted::ID:
      return "restricted";
    case td_api::chatMemberStatusLeft::ID:
      return "left";
    case td_api::chatMemberStatusBanned::ID:
      return "kicked";
    default:
      UNREACHABLE();
      return "";
  }
}

td::string Client::get_passport_element_type(int32 id) {
  switch (id) {
    case td_api::passportElementTypePersonalDetails::ID:
      return "personal_details";
    case td_api::passportElementTypePassport::ID:
      return "passport";
    case td_api::passportElementTypeDriverLicense::ID:
      return "driver_license";
    case td_api::passportElementTypeIdentityCard::ID:
      return "identity_card";
    case td_api::passportElementTypeInternalPassport::ID:
      return "internal_passport";
    case td_api::passportElementTypeAddress::ID:
      return "address";
    case td_api::passportElementTypeUtilityBill::ID:
      return "utility_bill";
    case td_api::passportElementTypeBankStatement::ID:
      return "bank_statement";
    case td_api::passportElementTypeRentalAgreement::ID:
      return "rental_agreement";
    case td_api::passportElementTypePassportRegistration::ID:
      return "passport_registration";
    case td_api::passportElementTypeTemporaryRegistration::ID:
      return "temporary_registration";
    case td_api::passportElementTypePhoneNumber::ID:
      return "phone_number";
    case td_api::passportElementTypeEmailAddress::ID:
      return "email";
    default:
      UNREACHABLE();
      return "None";
  }
}

td_api::object_ptr<td_api::PassportElementType> Client::get_passport_element_type(Slice type) {
  if (type == "personal_details") {
    return make_object<td_api::passportElementTypePersonalDetails>();
  }
  if (type == "passport") {
    return make_object<td_api::passportElementTypePassport>();
  }
  if (type == "driver_license") {
    return make_object<td_api::passportElementTypeDriverLicense>();
  }
  if (type == "identity_card") {
    return make_object<td_api::passportElementTypeIdentityCard>();
  }
  if (type == "internal_passport") {
    return make_object<td_api::passportElementTypeInternalPassport>();
  }
  if (type == "address") {
    return make_object<td_api::passportElementTypeAddress>();
  }
  if (type == "utility_bill") {
    return make_object<td_api::passportElementTypeUtilityBill>();
  }
  if (type == "bank_statement") {
    return make_object<td_api::passportElementTypeBankStatement>();
  }
  if (type == "rental_agreement") {
    return make_object<td_api::passportElementTypeRentalAgreement>();
  }
  if (type == "passport_registration") {
    return make_object<td_api::passportElementTypePassportRegistration>();
  }
  if (type == "temporary_registration") {
    return make_object<td_api::passportElementTypeTemporaryRegistration>();
  }
  if (type == "phone_number") {
    return make_object<td_api::passportElementTypePhoneNumber>();
  }
  if (type == "email") {
    return make_object<td_api::passportElementTypeEmailAddress>();
  }
  return nullptr;
}

td::int32 Client::get_unix_time() const {
  CHECK(was_authorized_);
  return parameters_->shared_data_->get_unix_time(td::Time::now());
}

td::int64 Client::as_tdlib_message_id(int32 message_id) {
  return static_cast<int64>(message_id) << 20;
}

td::int32 Client::as_client_message_id(int64 message_id) {
  int32 result = static_cast<int32>(message_id >> 20);
  CHECK(as_tdlib_message_id(result) == message_id);
  return result;
}

td::int64 Client::get_supergroup_chat_id(int32 supergroup_id) {
  return static_cast<td::int64>(-1000000000000ll) - static_cast<int64>(supergroup_id);
}

td::int64 Client::get_basic_group_chat_id(int32 basic_group_id) {
  return -static_cast<int64>(basic_group_id);
}

constexpr Client::int64 Client::GREAT_MINDS_SET_ID;
constexpr Client::Slice Client::GREAT_MINDS_SET_NAME;

constexpr Client::Slice Client::MASK_POINTS[MASK_POINTS_SIZE];

constexpr int Client::LOGGING_OUT_ERROR_CODE;
constexpr Client::Slice Client::LOGGING_OUT_ERROR_DESCRIPTION;
constexpr Client::Slice Client::API_ID_INVALID_ERROR_DESCRIPTION;

constexpr int Client::CLOSING_ERROR_CODE;
constexpr Client::Slice Client::CLOSING_ERROR_DESCRIPTION;

std::unordered_map<td::string, td::Status (Client::*)(PromisedQueryPtr &query)> Client::methods_;

}  // namespace telegram_bot_api