//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018
//
// 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 "td/telegram/MessagesManager.h"

#include "td/telegram/AuthManager.h"
#include "td/telegram/ChatId.h"
#include "td/telegram/ConfigShared.h"
#include "td/telegram/ContactsManager.h"
#include "td/telegram/DialogDb.h"
#include "td/telegram/DraftMessage.h"
#include "td/telegram/DraftMessage.hpp"
#include "td/telegram/files/FileId.hpp"
#include "td/telegram/files/FileManager.h"
#include "td/telegram/Global.h"
#include "td/telegram/HashtagHints.h"
#include "td/telegram/InlineQueriesManager.h"
#include "td/telegram/InputMessageText.h"
#include "td/telegram/logevent/LogEvent.h"
#include "td/telegram/logevent/LogEventHelper.h"
#include "td/telegram/MessageContent.h"
#include "td/telegram/MessageEntity.hpp"
#include "td/telegram/MessagesDb.h"
#include "td/telegram/misc.h"
#include "td/telegram/net/DcId.h"
#include "td/telegram/net/NetActor.h"
#include "td/telegram/net/NetQuery.h"
#include "td/telegram/NotificationGroupType.h"
#include "td/telegram/NotificationManager.h"
#include "td/telegram/NotificationSettings.hpp"
#include "td/telegram/NotificationType.h"
#include "td/telegram/Payments.h"
#include "td/telegram/ReplyMarkup.h"
#include "td/telegram/ReplyMarkup.hpp"
#include "td/telegram/SecretChatActor.h"
#include "td/telegram/SecretChatsManager.h"
#include "td/telegram/SequenceDispatcher.h"
#include "td/telegram/Td.h"
#include "td/telegram/TopDialogManager.h"
#include "td/telegram/UpdatesManager.h"
#include "td/telegram/WebPageId.h"
#include "td/telegram/WebPagesManager.h"

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

#include "td/db/binlog/BinlogHelper.h"
#include "td/db/SqliteKeyValueAsync.h"

#include "td/utils/format.h"
#include "td/utils/misc.h"
#include "td/utils/Random.h"
#include "td/utils/Slice.h"
#include "td/utils/Time.h"
#include "td/utils/tl_helpers.h"
#include "td/utils/tl_storers.h"
#include "td/utils/utf8.h"

#include <algorithm>
#include <cstring>
#include <iterator>
#include <limits>
#include <set>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>

namespace td {

void dummyUpdate::store(TlStorerToString &s, const char *field_name) const {
  s.store_class_begin(field_name, "dummyUpdate");
  s.store_class_end();
}

class GetDialogQuery : public Td::ResultHandler {
  DialogId dialog_id_;

 public:
  void send(DialogId dialog_id) {
    dialog_id_ = dialog_id;
    send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_getPeerDialogs(
        td->messages_manager_->get_input_dialog_peers({dialog_id}, AccessRights::Read)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getPeerDialogs>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto result = result_ptr.move_as_ok();
    LOG(INFO) << "Receive chat: " << to_string(result);

    td->contacts_manager_->on_get_chats(std::move(result->chats_));
    td->contacts_manager_->on_get_users(std::move(result->users_));
    td->messages_manager_->on_get_dialogs(
        std::move(result->dialogs_), -1, std::move(result->messages_),
        PromiseCreator::lambda([td = td, dialog_id = dialog_id_](Result<> result) {
          if (result.is_ok()) {
            td->messages_manager_->on_get_dialog_query_finished(dialog_id, Status::OK());
          } else {
            if (G()->close_flag()) {
              return;
            }
            td->messages_manager_->on_get_dialog_error(dialog_id, result.error(), "OnGetDialogs");
            td->messages_manager_->on_get_dialog_query_finished(dialog_id, result.move_as_error());
          }
        }));
  }

  void on_error(uint64 id, Status status) override {
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetDialogQuery");
    td->messages_manager_->on_get_dialog_query_finished(dialog_id_, std::move(status));
  }
};

class GetPinnedDialogsQuery : public NetActorOnce {
  Promise<Unit> promise_;

 public:
  explicit GetPinnedDialogsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  NetQueryRef send(uint64 sequence_id) {
    auto query = G()->net_query_creator().create(create_storer(telegram_api::messages_getPinnedDialogs()));
    auto result = query.get_weak();
    send_closure(td->messages_manager_->sequence_dispatcher_, &MultiSequenceDispatcher::send_with_callback,
                 std::move(query), actor_shared(this), sequence_id);
    return result;
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getPinnedDialogs>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto result = result_ptr.move_as_ok();
    LOG(INFO) << "Receive pinned chats: " << to_string(result);

    td->contacts_manager_->on_get_chats(std::move(result->chats_));
    td->contacts_manager_->on_get_users(std::move(result->users_));
    std::reverse(result->dialogs_.begin(), result->dialogs_.end());
    td->messages_manager_->on_get_dialogs(std::move(result->dialogs_), -2, std::move(result->messages_),
                                          std::move(promise_));
  }

  void on_error(uint64 id, Status status) override {
    promise_.set_error(std::move(status));
  }
};

class GetDialogUnreadMarksQuery : public Td::ResultHandler {
 public:
  void send() {
    send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_getDialogUnreadMarks())));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getDialogUnreadMarks>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto results = result_ptr.move_as_ok();
    for (auto &result : results) {
      td->messages_manager_->on_update_dialog_is_marked_as_unread(DialogId(result), true);
    }

    G()->td_db()->get_binlog_pmc()->set("fetched_marks_as_unread", "1");
  }

  void on_error(uint64 id, Status status) override {
    LOG(ERROR) << "Receive error for GetDialogUnreadMarksQuery: " << status;
    status.ignore();
  }
};

class GetMessagesQuery : public Td::ResultHandler {
  Promise<Unit> promise_;

 public:
  explicit GetMessagesQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(vector<tl_object_ptr<telegram_api::InputMessage>> &&message_ids) {
    send_query(
        G()->net_query_creator().create(create_storer(telegram_api::messages_getMessages(std::move(message_ids)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    // LOG(INFO) << "Receive result for GetMessagesQuery " << to_string(ptr);
    int32 constructor_id = ptr->get_id();
    switch (constructor_id) {
      case telegram_api::messages_messages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_messages(std::move(messages->messages_), false, "get messages");
        break;
      }
      case telegram_api::messages_messagesSlice::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messagesSlice>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_messages(std::move(messages->messages_), false, "get messages slice");
        break;
      }
      case telegram_api::messages_channelMessages::ID: {
        LOG(ERROR) << "Receive channel messages in GetMessagesQuery";
        auto messages = move_tl_object_as<telegram_api::messages_channelMessages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_messages(std::move(messages->messages_), false, "get channel messages");
        break;
      }
      default:
        UNREACHABLE();
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (status.message() == "MESSAGE_IDS_EMPTY") {
      promise_.set_value(Unit());
      return;
    }
    promise_.set_error(std::move(status));
  }
};

class GetChannelMessagesQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  ChannelId channel_id_;

 public:
  explicit GetChannelMessagesQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(ChannelId channel_id, tl_object_ptr<telegram_api::InputChannel> &&input_channel,
            vector<tl_object_ptr<telegram_api::InputMessage>> &&message_ids) {
    channel_id_ = channel_id;
    CHECK(input_channel != nullptr);
    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::channels_getMessages(std::move(input_channel), std::move(message_ids)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::channels_getMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(DEBUG) << "Receive result for GetChannelMessagesQuery " << to_string(ptr);
    int32 constructor_id = ptr->get_id();
    switch (constructor_id) {
      case telegram_api::messages_messages::ID: {
        LOG(ERROR) << "Receive ordinary messages in GetChannelMessagesQuery";
        auto messages = move_tl_object_as<telegram_api::messages_messages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_messages(std::move(messages->messages_), true, "get messages");
        break;
      }
      case telegram_api::messages_messagesSlice::ID: {
        LOG(ERROR) << "Receive ordinary messages in GetChannelMessagesQuery";
        auto messages = move_tl_object_as<telegram_api::messages_messagesSlice>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_messages(std::move(messages->messages_), true, "get messages slice");
        break;
      }
      case telegram_api::messages_channelMessages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_channelMessages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_messages(std::move(messages->messages_), true, "get channel messages");
        break;
      }
      default:
        UNREACHABLE();
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (status.message() == "MESSAGE_IDS_EMPTY") {
      promise_.set_value(Unit());
      return;
    }
    td->contacts_manager_->on_get_channel_error(channel_id_, status, "GetChannelMessagesQuery");
    promise_.set_error(std::move(status));
  }
};

class GetChannelPinnedMessageQuery : public Td::ResultHandler {
  Promise<MessageId> promise_;
  ChannelId channel_id_;

 public:
  explicit GetChannelPinnedMessageQuery(Promise<MessageId> &&promise) : promise_(std::move(promise)) {
  }

  void send(ChannelId channel_id) {
    auto input_channel = td->contacts_manager_->get_input_channel(channel_id);
    if (input_channel == nullptr) {
      return promise_.set_error(Status::Error(6, "Can't access the chat"));
    }

    channel_id_ = channel_id;
    vector<tl_object_ptr<telegram_api::InputMessage>> input_messages;
    input_messages.push_back(make_tl_object<telegram_api::inputMessagePinned>());
    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::channels_getMessages(std::move(input_channel), std::move(input_messages)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::channels_getMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(DEBUG) << "Receive result for GetChannelPinnedMessageQuery " << to_string(ptr);
    int32 constructor_id = ptr->get_id();
    switch (constructor_id) {
      case telegram_api::messages_messages::ID:
      case telegram_api::messages_messagesSlice::ID:
        LOG(ERROR) << "Receive ordinary messages in GetChannelPinnedMessageQuery " << to_string(ptr);
        return promise_.set_error(Status::Error(500, "Receive wrong request result"));
      case telegram_api::messages_channelMessages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_channelMessages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        if (messages->messages_.empty()) {
          return promise_.set_value(MessageId());
        }
        if (messages->messages_.size() >= 2) {
          LOG(ERROR) << to_string(ptr);
          return promise_.set_error(Status::Error(500, "More than 1 pinned message received"));
        }
        auto full_message_id = td->messages_manager_->on_get_message(std::move(messages->messages_[0]), false, true,
                                                                     false, false, "get channel pinned messages");
        if (full_message_id.get_dialog_id().is_valid() && full_message_id.get_dialog_id() != DialogId(channel_id_)) {
          LOG(ERROR) << full_message_id << " " << to_string(ptr);
          return promise_.set_error(Status::Error(500, "Receive pinned message in a wrong chat"));
        }
        return promise_.set_value(full_message_id.get_message_id());
      }
      default:
        UNREACHABLE();
    }
  }

  void on_error(uint64 id, Status status) override {
    if (status.message() == "MESSAGE_IDS_EMPTY") {
      promise_.set_value(MessageId());
      return;
    }
    td->contacts_manager_->on_get_channel_error(channel_id_, status, "GetChannelPinnedMessageQuery");
    promise_.set_error(std::move(status));
  }
};

class ExportChannelMessageLinkQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  ChannelId channel_id_;
  MessageId message_id_;
  bool for_group_;

 public:
  explicit ExportChannelMessageLinkQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(ChannelId channel_id, MessageId message_id, bool for_group) {
    channel_id_ = channel_id;
    message_id_ = message_id;
    for_group_ = for_group;
    auto input_channel = td->contacts_manager_->get_input_channel(channel_id);
    CHECK(input_channel != nullptr);
    send_query(G()->net_query_creator().create(create_storer(telegram_api::channels_exportMessageLink(
        std::move(input_channel), message_id.get_server_message_id().get(), for_group))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::channels_exportMessageLink>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(DEBUG) << "Receive result for ExportChannelMessageLinkQuery " << to_string(ptr);
    td->messages_manager_->on_get_public_message_link({DialogId(channel_id_), message_id_}, for_group_,
                                                      std::move(ptr->link_), std::move(ptr->html_));

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    td->contacts_manager_->on_get_channel_error(channel_id_, status, "ExportChannelMessageLinkQuery");
    promise_.set_error(std::move(status));
  }
};

class GetDialogListQuery : public NetActorOnce {
  Promise<Unit> promise_;

 public:
  explicit GetDialogListQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(int32 offset_date, ServerMessageId offset_message_id, DialogId offset_dialog_id, int32 limit,
            uint64 sequence_id) {
    auto input_peer = td->messages_manager_->get_input_peer(offset_dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      input_peer = make_tl_object<telegram_api::inputPeerEmpty>();
    }

    int32 flags = telegram_api::messages_getDialogs::EXCLUDE_PINNED_MASK;
    auto query = G()->net_query_creator().create(create_storer(telegram_api::messages_getDialogs(
        flags, false /*ignored*/, offset_date, offset_message_id.get(), std::move(input_peer), limit, 0)));
    send_closure(td->messages_manager_->sequence_dispatcher_, &MultiSequenceDispatcher::send_with_callback,
                 std::move(query), actor_shared(this), sequence_id);
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getDialogs>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for GetDialogListQuery " << to_string(ptr);
    switch (ptr->get_id()) {
      case telegram_api::messages_dialogs::ID: {
        auto dialogs = move_tl_object_as<telegram_api::messages_dialogs>(ptr);
        td->contacts_manager_->on_get_chats(std::move(dialogs->chats_));
        td->contacts_manager_->on_get_users(std::move(dialogs->users_));
        td->messages_manager_->on_get_dialogs(std::move(dialogs->dialogs_),
                                              narrow_cast<int32>(dialogs->dialogs_.size()),
                                              std::move(dialogs->messages_), std::move(promise_));
        break;
      }
      case telegram_api::messages_dialogsSlice::ID: {
        auto dialogs = move_tl_object_as<telegram_api::messages_dialogsSlice>(ptr);
        td->contacts_manager_->on_get_chats(std::move(dialogs->chats_));
        td->contacts_manager_->on_get_users(std::move(dialogs->users_));
        td->messages_manager_->on_get_dialogs(std::move(dialogs->dialogs_), max(dialogs->count_, 0),
                                              std::move(dialogs->messages_), std::move(promise_));
        break;
      }
      case telegram_api::messages_dialogsNotModified::ID:
        LOG(ERROR) << "Receive " << to_string(ptr);
        return on_error(id, Status::Error(500, "Receive wrong server response messages.dialogsNotModified"));
      default:
        UNREACHABLE();
    }
  }

  void on_error(uint64 id, Status status) override {
    promise_.set_error(std::move(status));
  }
};

class SearchPublicDialogsQuery : public Td::ResultHandler {
  string query_;

 public:
  void send(const string &query) {
    query_ = query;
    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::contacts_search(query, 3 /* ignored server-side */))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::contacts_search>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto dialogs = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SearchPublicDialogsQuery " << to_string(dialogs);
    td->contacts_manager_->on_get_chats(std::move(dialogs->chats_));
    td->contacts_manager_->on_get_users(std::move(dialogs->users_));
    td->messages_manager_->on_get_public_dialogs_search_result(query_, std::move(dialogs->my_results_),
                                                               std::move(dialogs->results_));
  }

  void on_error(uint64 id, Status status) override {
    LOG(ERROR) << "Receive error for SearchPublicDialogsQuery: " << status;
    td->messages_manager_->on_failed_public_dialogs_search(query_, std::move(status));
  }
};

class GetCommonDialogsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  UserId user_id_;

 public:
  explicit GetCommonDialogsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(UserId user_id, int32 offset_chat_id, int32 limit) {
    user_id_ = user_id;
    LOG(INFO) << "Get common dialogs with " << user_id << " from " << offset_chat_id << " with limit " << limit;

    auto input_user = td->contacts_manager_->get_input_user(user_id);
    CHECK(input_user != nullptr);

    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::messages_getCommonChats(std::move(input_user), offset_chat_id, limit))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getCommonChats>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto chats_ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for GetCommonDialogsQuery " << to_string(chats_ptr);
    int32 constructor_id = chats_ptr->get_id();
    switch (constructor_id) {
      case telegram_api::messages_chats::ID: {
        auto chats = move_tl_object_as<telegram_api::messages_chats>(chats_ptr);
        td->messages_manager_->on_get_common_dialogs(user_id_, std::move(chats->chats_),
                                                     narrow_cast<int32>(chats->chats_.size()));
        break;
      }
      case telegram_api::messages_chatsSlice::ID: {
        auto chats = move_tl_object_as<telegram_api::messages_chatsSlice>(chats_ptr);
        td->messages_manager_->on_get_common_dialogs(user_id_, std::move(chats->chats_), chats->count_);
        break;
      }
      default:
        UNREACHABLE();
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    promise_.set_error(std::move(status));
  }
};

class CreateChatQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  int64 random_id_;

 public:
  explicit CreateChatQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(vector<tl_object_ptr<telegram_api::InputUser>> &&input_users, const string &title, int64 random_id) {
    random_id_ = random_id;
    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::messages_createChat(std::move(input_users), title))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_createChat>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for createChat " << to_string(ptr);
    td->messages_manager_->on_create_new_dialog_success(random_id_, std::move(ptr), DialogType::Chat,
                                                        std::move(promise_));
  }

  void on_error(uint64 id, Status status) override {
    td->messages_manager_->on_create_new_dialog_fail(random_id_, std::move(status), std::move(promise_));
  }
};

class CreateChannelQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  int64 random_id_;

 public:
  explicit CreateChannelQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(const string &title, bool is_megagroup, const string &about, int64 random_id) {
    int32 flags = 0;
    if (is_megagroup) {
      flags |= telegram_api::channels_createChannel::MEGAGROUP_MASK;
    } else {
      flags |= telegram_api::channels_createChannel::BROADCAST_MASK;
    }

    random_id_ = random_id;
    send_query(G()->net_query_creator().create(create_storer(
        telegram_api::channels_createChannel(flags, false /*ignored*/, false /*ignored*/, title, about))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::channels_createChannel>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for createChannel " << to_string(ptr);
    td->messages_manager_->on_create_new_dialog_success(random_id_, std::move(ptr), DialogType::Channel,
                                                        std::move(promise_));
  }

  void on_error(uint64 id, Status status) override {
    td->messages_manager_->on_create_new_dialog_fail(random_id_, std::move(status), std::move(promise_));
  }
};

class EditDialogPhotoQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  FileId file_id_;
  DialogId dialog_id_;

 public:
  explicit EditDialogPhotoQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(FileId file_id, DialogId dialog_id, tl_object_ptr<telegram_api::InputChatPhoto> &&input_chat_photo) {
    CHECK(input_chat_photo != nullptr);
    file_id_ = file_id;
    dialog_id_ = dialog_id;

    switch (dialog_id.get_type()) {
      case DialogType::Chat:
        send_query(G()->net_query_creator().create(create_storer(
            telegram_api::messages_editChatPhoto(dialog_id.get_chat_id().get(), std::move(input_chat_photo)))));
        break;
      case DialogType::Channel: {
        auto channel_id = dialog_id.get_channel_id();
        auto input_channel = td->contacts_manager_->get_input_channel(channel_id);
        CHECK(input_channel != nullptr);
        send_query(G()->net_query_creator().create(
            create_storer(telegram_api::channels_editPhoto(std::move(input_channel), std::move(input_chat_photo)))));
        break;
      }
      default:
        UNREACHABLE();
    }
  }

  void on_result(uint64 id, BufferSlice packet) override {
    static_assert(std::is_same<telegram_api::messages_editChatPhoto::ReturnType,
                               telegram_api::channels_editPhoto::ReturnType>::value,
                  "");
    auto result_ptr = fetch_result<telegram_api::messages_editChatPhoto>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for editDialogPhoto: " << to_string(ptr);
    td->updates_manager_->on_get_updates(std::move(ptr));

    if (file_id_.is_valid()) {
      td->file_manager_->delete_partial_remote_location(file_id_);
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (file_id_.is_valid()) {
      td->file_manager_->delete_partial_remote_location(file_id_);
    }
    td->updates_manager_->get_difference("EditDialogPhotoQuery");

    if (status.message() == "CHAT_NOT_MODIFIED") {
      if (!td->auth_manager_->is_bot()) {
        promise_.set_value(Unit());
        return;
      }
    } else {
      td->messages_manager_->on_get_dialog_error(dialog_id_, status, "EditDialogPhotoQuery");
    }
    promise_.set_error(std::move(status));
  }
};

class EditDialogTitleQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit EditDialogTitleQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, const string &title) {
    dialog_id_ = dialog_id;
    switch (dialog_id.get_type()) {
      case DialogType::Chat:
        send_query(G()->net_query_creator().create(
            create_storer(telegram_api::messages_editChatTitle(dialog_id.get_chat_id().get(), title))));
        break;
      case DialogType::Channel: {
        auto channel_id = dialog_id.get_channel_id();
        auto input_channel = td->contacts_manager_->get_input_channel(channel_id);
        CHECK(input_channel != nullptr);
        send_query(G()->net_query_creator().create(
            create_storer(telegram_api::channels_editTitle(std::move(input_channel), title))));
        break;
      }
      default:
        UNREACHABLE();
    }
  }

  void on_result(uint64 id, BufferSlice packet) override {
    static_assert(std::is_same<telegram_api::messages_editChatTitle::ReturnType,
                               telegram_api::channels_editTitle::ReturnType>::value,
                  "");
    auto result_ptr = fetch_result<telegram_api::messages_editChatTitle>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for editDialogTitle " << to_string(ptr);
    td->updates_manager_->on_get_updates(std::move(ptr));

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    td->updates_manager_->get_difference("EditDialogTitleQuery");

    if (status.message() == "CHAT_NOT_MODIFIED") {
      if (!td->auth_manager_->is_bot()) {
        promise_.set_value(Unit());
        return;
      }
    } else {
      td->messages_manager_->on_get_dialog_error(dialog_id_, status, "EditDialogTitleQuery");
    }
    promise_.set_error(std::move(status));
  }
};

class SaveDraftMessageQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit SaveDraftMessageQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, const unique_ptr<DraftMessage> &draft_message) {
    LOG(INFO) << "Save draft in " << dialog_id;
    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      LOG(INFO) << "Can't update draft message because have no write access to " << dialog_id;
      on_error(0, Status::Error(500, "Can't save draft message"));
      return;
    }

    int32 flags = 0;
    ServerMessageId reply_to_message_id;
    if (draft_message != nullptr) {
      if (draft_message->reply_to_message_id.is_valid() && draft_message->reply_to_message_id.is_server()) {
        reply_to_message_id = draft_message->reply_to_message_id.get_server_message_id();
        flags |= MessagesManager::SEND_MESSAGE_FLAG_IS_REPLY;
      }
      if (draft_message->input_message_text.disable_web_page_preview) {
        flags |= MessagesManager::SEND_MESSAGE_FLAG_DISABLE_WEB_PAGE_PREVIEW;
      }
      if (draft_message->input_message_text.text.entities.size()) {
        flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_ENTITIES;
      }
    }

    dialog_id_ = dialog_id;
    send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_saveDraft(
        flags, false /*ignored*/, reply_to_message_id.get(), std::move(input_peer),
        draft_message == nullptr ? "" : draft_message->input_message_text.text.text,
        draft_message == nullptr
            ? vector<tl_object_ptr<telegram_api::MessageEntity>>()
            : get_input_message_entities(td->contacts_manager_.get(), draft_message->input_message_text.text.entities,
                                         "SaveDraftMessageQuery")))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_saveDraft>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    bool result = result_ptr.ok();
    if (!result) {
      on_error(id, Status::Error(400, "Save draft failed"));
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "SaveDraftMessageQuery")) {
      LOG(ERROR) << "Receive error for SaveDraftMessageQuery: " << status;
    }
    promise_.set_error(std::move(status));
  }
};

class ClearAllDraftsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;

 public:
  explicit ClearAllDraftsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send() {
    send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_clearAllDrafts())));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_clearAllDrafts>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    bool result = result_ptr.move_as_ok();
    if (!result) {
      LOG(INFO) << "Receive false for clearAllDrafts";
    } else {
      LOG(INFO) << "All draft messages has been cleared";
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(ERROR) << "Receive error for ClearAllDraftsQuery: " << status;
    promise_.set_error(std::move(status));
  }
};

class ToggleDialogPinQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  bool is_pinned_;

 public:
  explicit ToggleDialogPinQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, bool is_pinned) {
    dialog_id_ = dialog_id;
    is_pinned_ = is_pinned;
    auto input_peer = td->messages_manager_->get_input_dialog_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      on_error(0, Status::Error(500, "Can't update dialog is_pinned"));
      return;
    }

    int32 flags = 0;
    if (is_pinned) {
      flags |= telegram_api::messages_toggleDialogPin::PINNED_MASK;
    }
    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::messages_toggleDialogPin(flags, false /*ignored*/, std::move(input_peer)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_toggleDialogPin>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    bool result = result_ptr.ok();
    if (!result) {
      on_error(id, Status::Error(400, "Toggle dialog pin failed"));
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "ToggleDialogPinQuery")) {
      LOG(ERROR) << "Receive error for ToggleDialogPinQuery: " << status;
    }
    td->messages_manager_->on_update_dialog_is_pinned(dialog_id_, !is_pinned_);
    promise_.set_error(std::move(status));
  }
};

class ReorderPinnedDialogsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;

 public:
  explicit ReorderPinnedDialogsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(const vector<DialogId> &dialog_ids) {
    int32 flags = telegram_api::messages_reorderPinnedDialogs::FORCE_MASK;
    send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_reorderPinnedDialogs(
        flags, true /*ignored*/, td->messages_manager_->get_input_dialog_peers(dialog_ids, AccessRights::Read)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_reorderPinnedDialogs>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    bool result = result_ptr.move_as_ok();
    if (!result) {
      return on_error(id, Status::Error(400, "Result is false"));
    }
    LOG(INFO) << "Pinned chats reordered";

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(ERROR) << "Receive error for ReorderPinnedDialogsQuery: " << status;
    td->messages_manager_->on_update_pinned_dialogs();
    promise_.set_error(std::move(status));
  }
};

class ToggleDialogUnreadMarkQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  bool is_marked_as_unread_;

 public:
  explicit ToggleDialogUnreadMarkQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, bool is_marked_as_unread) {
    dialog_id_ = dialog_id;
    is_marked_as_unread_ = is_marked_as_unread;
    auto input_peer = td->messages_manager_->get_input_dialog_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      on_error(0, Status::Error(500, "Can't update dialog is_marked_as_unread"));
      return;
    }

    int32 flags = 0;
    if (is_marked_as_unread) {
      flags |= telegram_api::messages_markDialogUnread::UNREAD_MASK;
    }
    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::messages_markDialogUnread(flags, false /*ignored*/, std::move(input_peer)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_markDialogUnread>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    bool result = result_ptr.ok();
    if (!result) {
      on_error(id, Status::Error(400, "Toggle dialog mark failed"));
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "ToggleDialogUnreadMarkQuery")) {
      LOG(ERROR) << "Receive error for ToggleDialogUnreadMarkQuery: " << status;
    }
    td->messages_manager_->on_update_dialog_is_marked_as_unread(dialog_id_, !is_marked_as_unread_);
    promise_.set_error(std::move(status));
  }
};

class GetMessagesViewsQuery : public Td::ResultHandler {
  DialogId dialog_id_;
  vector<MessageId> message_ids_;

 public:
  void send(DialogId dialog_id, vector<MessageId> &&message_ids, bool increment_view_counter) {
    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      LOG(ERROR) << "Can't update message views because doesn't have info about the " << dialog_id;
      on_error(0, Status::Error(500, "Can't update message views"));
      return;
    }

    LOG(INFO) << "View " << message_ids.size() << " messages in " << dialog_id
              << ", increment = " << increment_view_counter;
    dialog_id_ = dialog_id;
    message_ids_ = std::move(message_ids);
    send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_getMessagesViews(
        std::move(input_peer), MessagesManager::get_server_message_ids(message_ids_), increment_view_counter))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getMessagesViews>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    vector<int32> views = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for GetMessagesViewsQuery " << format::as_array(views);
    if (message_ids_.size() != views.size()) {
      return on_error(id, Status::Error(500, "Wrong number of message views returned"));
    }

    for (size_t i = 0; i < message_ids_.size(); i++) {
      td->messages_manager_->on_update_message_views({dialog_id_, message_ids_[i]}, views[i]);
    }
  }

  void on_error(uint64 id, Status status) override {
    if (!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetMessagesViewsQuery")) {
      LOG(ERROR) << "Receive error for GetMessagesViewsQuery: " << status;
    }
    status.ignore();
  }
};

class ReadMessagesContentsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;

 public:
  explicit ReadMessagesContentsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(vector<MessageId> &&message_ids) {
    LOG(INFO) << "Receive ReadMessagesContentsQuery for messages " << format::as_array(message_ids);

    send_query(G()->net_query_creator().create(create_storer(
        telegram_api::messages_readMessageContents(MessagesManager::get_server_message_ids(message_ids)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_readMessageContents>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto affected_messages = result_ptr.move_as_ok();
    CHECK(affected_messages->get_id() == telegram_api::messages_affectedMessages::ID);

    if (affected_messages->pts_count_ > 0) {
      td->messages_manager_->add_pending_update(make_tl_object<dummyUpdate>(), affected_messages->pts_,
                                                affected_messages->pts_count_, false, "read messages content query");
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(ERROR) << "Receive error for read message contents: " << status;
    promise_.set_error(std::move(status));
  }
};

class ReadChannelMessagesContentsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  ChannelId channel_id_;

 public:
  explicit ReadChannelMessagesContentsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(ChannelId channel_id, vector<MessageId> &&message_ids) {
    channel_id_ = channel_id;

    auto input_channel = td->contacts_manager_->get_input_channel(channel_id);
    if (input_channel == nullptr) {
      LOG(ERROR) << "Have no input channel for " << channel_id;
      on_error(0, Status::Error(500, "Can't read channel message contents"));
      return;
    }

    LOG(INFO) << "Receive ReadChannelMessagesContentsQuery for messages " << format::as_array(message_ids) << " in "
              << channel_id;

    send_query(G()->net_query_creator().create(create_storer(telegram_api::channels_readMessageContents(
        std::move(input_channel), MessagesManager::get_server_message_ids(message_ids)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::channels_readMessageContents>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    bool result = result_ptr.ok();
    LOG_IF(ERROR, !result) << "Read channel messages contents failed";

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (!td->contacts_manager_->on_get_channel_error(channel_id_, status, "ReadChannelMessagesContentsQuery")) {
      LOG(ERROR) << "Receive error for read messages contents in " << channel_id_ << ": " << status;
    }
    promise_.set_error(std::move(status));
  }
};

class GetDialogMessageByDateQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  int32 date_;
  int64 random_id_;

 public:
  explicit GetDialogMessageByDateQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, int32 date, int64 random_id) {
    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(500, "Have no info about the chat"));
    }

    dialog_id_ = dialog_id;
    date_ = date;
    random_id_ = random_id;

    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::messages_getHistory(std::move(input_peer), 0, date, -3, 5, 0, 0, 0))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getHistory>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    int32 constructor_id = ptr->get_id();
    switch (constructor_id) {
      case telegram_api::messages_messages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messages>(ptr);
        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_dialog_message_by_date_success(dialog_id_, date_, random_id_,
                                                                     std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_messagesSlice::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messagesSlice>(ptr);
        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_dialog_message_by_date_success(dialog_id_, date_, random_id_,
                                                                     std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_channelMessages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_channelMessages>(ptr);
        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_dialog_message_by_date_success(dialog_id_, date_, random_id_,
                                                                     std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_messagesNotModified::ID:
        return on_error(id, Status::Error(500, "Server returned messagesNotModified"));
      default:
        UNREACHABLE();
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetDialogMessageByDateQuery")) {
      LOG(ERROR) << "Receive error for GetDialogMessageByDateQuery in " << dialog_id_ << ": " << status;
    }
    promise_.set_error(std::move(status));
    td->messages_manager_->on_get_dialog_message_by_date_fail(random_id_);
  }
};

class GetHistoryQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  MessageId from_message_id_;
  int32 offset_;
  int32 limit_;
  bool from_the_end_;

 public:
  explicit GetHistoryQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId from_message_id, int32 offset, int32 limit) {
    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      LOG(ERROR) << "Can't get chat history in " << dialog_id << " because doesn't have info about the chat";
      return promise_.set_error(Status::Error(500, "Have no info about the chat"));
    }
    CHECK(offset < 0);

    dialog_id_ = dialog_id;
    from_message_id_ = from_message_id;
    offset_ = offset;
    limit_ = limit;
    from_the_end_ = false;
    send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_getHistory(
        std::move(input_peer), from_message_id.get_server_message_id().get(), 0, offset, limit, 0, 0, 0))));
  }

  void send_get_from_the_end(DialogId dialog_id, int32 limit) {
    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      LOG(ERROR) << "Can't get chat history because doesn't have info about the chat";
      return promise_.set_error(Status::Error(500, "Have no info about the chat"));
    }

    dialog_id_ = dialog_id;
    offset_ = 0;
    limit_ = limit;
    from_the_end_ = true;
    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::messages_getHistory(std::move(input_peer), 0, 0, 0, limit, 0, 0, 0))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getHistory>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    // LOG(INFO) << "Receive result for GetHistoryQuery " << to_string(ptr);
    int32 constructor_id = ptr->get_id();
    switch (constructor_id) {
      case telegram_api::messages_messages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_history(dialog_id_, from_message_id_, offset_, limit_, from_the_end_,
                                              std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_messagesSlice::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messagesSlice>(ptr);
        // TODO use messages->count_

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_history(dialog_id_, from_message_id_, offset_, limit_, from_the_end_,
                                              std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_channelMessages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_channelMessages>(ptr);
        // TODO use messages->count_, messages->pts_

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_history(dialog_id_, from_message_id_, offset_, limit_, from_the_end_,
                                              std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_messagesNotModified::ID:
        LOG(ERROR) << "Receive messagesNotModified";
        break;
      default:
        UNREACHABLE();
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetHistoryQuery")) {
      LOG(ERROR) << "Receive error for getHistoryQuery in " << dialog_id_ << ": " << status;
    }
    promise_.set_error(std::move(status));
  }
};

class ReadHistoryQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit ReadHistoryQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId max_message_id) {
    dialog_id_ = dialog_id;
    send_query(G()->net_query_creator().create(create_storer(
        telegram_api::messages_readHistory(td->messages_manager_->get_input_peer(dialog_id, AccessRights::Read),
                                           max_message_id.get_server_message_id().get()))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_readHistory>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto affected_messages = result_ptr.move_as_ok();
    CHECK(affected_messages->get_id() == telegram_api::messages_affectedMessages::ID);
    LOG(INFO) << "Receive result for readHistory: " << to_string(affected_messages);

    if (affected_messages->pts_count_ > 0) {
      td->messages_manager_->add_pending_update(make_tl_object<dummyUpdate>(), affected_messages->pts_,
                                                affected_messages->pts_count_, false, "read history query");
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "ReadHistoryQuery")) {
      LOG(ERROR) << "Receive error for readHistory: " << status;
    }
    promise_.set_error(std::move(status));
  }
};

class ReadChannelHistoryQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  ChannelId channel_id_;

 public:
  explicit ReadChannelHistoryQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(ChannelId channel_id, MessageId max_message_id) {
    channel_id_ = channel_id;
    auto input_channel = td->contacts_manager_->get_input_channel(channel_id);
    CHECK(input_channel != nullptr);

    send_query(G()->net_query_creator().create(create_storer(
        telegram_api::channels_readHistory(std::move(input_channel), max_message_id.get_server_message_id().get()))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::channels_readHistory>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (!td->contacts_manager_->on_get_channel_error(channel_id_, status, "ReadChannelHistoryQuery")) {
      LOG(ERROR) << "Receive error for readChannelHistory: " << status;
    }
    promise_.set_error(std::move(status));
  }
};

class SearchMessagesQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  string query_;
  UserId sender_user_id_;
  MessageId from_message_id_;
  int32 offset_;
  int32 limit_;
  SearchMessagesFilter filter_;
  int64 random_id_;

 public:
  explicit SearchMessagesQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, const string &query, UserId sender_user_id,
            telegram_api::object_ptr<telegram_api::InputUser> &&sender_input_user, MessageId from_message_id,
            int32 offset, int32 limit, SearchMessagesFilter filter, int64 random_id) {
    auto input_peer = dialog_id.is_valid() ? td->messages_manager_->get_input_peer(dialog_id, AccessRights::Read)
                                           : make_tl_object<telegram_api::inputPeerEmpty>();
    if (input_peer == nullptr) {
      LOG(ERROR) << "Can't search messages because doesn't have info about the chat";
      return promise_.set_error(Status::Error(500, "Have no info about the chat"));
    }

    dialog_id_ = dialog_id;
    query_ = query;
    sender_user_id_ = sender_user_id;
    from_message_id_ = from_message_id;
    offset_ = offset;
    limit_ = limit;
    filter_ = filter;
    random_id_ = random_id;

    if (filter == SearchMessagesFilter::UnreadMention) {
      send_query(G()->net_query_creator().create(create_storer(
          telegram_api::messages_getUnreadMentions(std::move(input_peer), from_message_id.get_server_message_id().get(),
                                                   offset, limit, std::numeric_limits<int32>::max(), 0))));
    } else {
      int32 flags = 0;
      if (sender_input_user != nullptr) {
        flags |= telegram_api::messages_search::FROM_ID_MASK;
      }

      send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_search(
          flags, std::move(input_peer), query, std::move(sender_input_user),
          MessagesManager::get_input_messages_filter(filter), 0, std::numeric_limits<int32>::max(),
          from_message_id.get_server_message_id().get(), offset, limit, std::numeric_limits<int32>::max(), 0, 0))));
    }
  }

  void on_result(uint64 id, BufferSlice packet) override {
    static_assert(std::is_same<telegram_api::messages_getUnreadMentions::ReturnType,
                               telegram_api::messages_search::ReturnType>::value,
                  "");
    auto result_ptr = fetch_result<telegram_api::messages_search>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SearchMessagesQuery: " << to_string(ptr);
    int32 constructor_id = ptr->get_id();
    switch (constructor_id) {
      case telegram_api::messages_messages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_dialog_messages_search_result(
            dialog_id_, query_, sender_user_id_, from_message_id_, offset_, limit_, filter_, random_id_,
            narrow_cast<int32>(messages->messages_.size()), std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_messagesSlice::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messagesSlice>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_dialog_messages_search_result(
            dialog_id_, query_, sender_user_id_, from_message_id_, offset_, limit_, filter_, random_id_,
            messages->count_, std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_channelMessages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_channelMessages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_dialog_messages_search_result(
            dialog_id_, query_, sender_user_id_, from_message_id_, offset_, limit_, filter_, random_id_,
            messages->count_, std::move(messages->messages_));
        break;
      }
      default:
        UNREACHABLE();
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "SearchMessagesQuery");
    td->messages_manager_->on_failed_dialog_messages_search(dialog_id_, random_id_);
    promise_.set_error(std::move(status));
  }
};

class SearchMessagesGlobalQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  string query_;
  int32 offset_date_;
  DialogId offset_dialog_id_;
  MessageId offset_message_id_;
  int32 limit_;
  int64 random_id_;

 public:
  explicit SearchMessagesGlobalQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(const string &query, int32 offset_date, DialogId offset_dialog_id, MessageId offset_message_id, int32 limit,
            int64 random_id) {
    query_ = query;
    offset_date_ = offset_date;
    offset_dialog_id_ = offset_dialog_id;
    offset_message_id_ = offset_message_id;
    limit_ = limit;
    random_id_ = random_id;

    auto input_peer = td->messages_manager_->get_input_peer(offset_dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      input_peer = make_tl_object<telegram_api::inputPeerEmpty>();
    }

    send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_searchGlobal(
        query, offset_date_, std::move(input_peer), offset_message_id.get_server_message_id().get(), limit))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_searchGlobal>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SearchMessagesGlobalQuery: " << to_string(ptr);
    int32 constructor_id = ptr->get_id();
    switch (constructor_id) {
      case telegram_api::messages_messages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_messages_search_result(
            query_, offset_date_, offset_dialog_id_, offset_message_id_, limit_, random_id_,
            narrow_cast<int32>(messages->messages_.size()), std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_messagesSlice::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messagesSlice>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_messages_search_result(query_, offset_date_, offset_dialog_id_,
                                                             offset_message_id_, limit_, random_id_, messages->count_,
                                                             std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_channelMessages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_channelMessages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_messages_search_result(query_, offset_date_, offset_dialog_id_,
                                                             offset_message_id_, limit_, random_id_, messages->count_,
                                                             std::move(messages->messages_));
        break;
      }
      default:
        UNREACHABLE();
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    td->messages_manager_->on_failed_messages_search(random_id_);
    promise_.set_error(std::move(status));
  }
};

class GetRecentLocationsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  int32 limit_;
  int64 random_id_;

 public:
  explicit GetRecentLocationsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, int32 limit, int64 random_id) {
    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      LOG(ERROR) << "Can't get recent locations because doesn't have info about the chat";
      return promise_.set_error(Status::Error(500, "Have no info about the chat"));
    }

    dialog_id_ = dialog_id;
    limit_ = limit;
    random_id_ = random_id;

    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::messages_getRecentLocations(std::move(input_peer), limit, 0))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getRecentLocations>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for GetRecentLocationsQuery: " << to_string(ptr);
    int32 constructor_id = ptr->get_id();
    switch (constructor_id) {
      case telegram_api::messages_messages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_recent_locations(dialog_id_, limit_, random_id_,
                                                       narrow_cast<int32>(messages->messages_.size()),
                                                       std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_messagesSlice::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messagesSlice>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_recent_locations(dialog_id_, limit_, random_id_, messages->count_,
                                                       std::move(messages->messages_));
        break;
      }
      case telegram_api::messages_channelMessages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_channelMessages>(ptr);

        td->contacts_manager_->on_get_chats(std::move(messages->chats_));
        td->contacts_manager_->on_get_users(std::move(messages->users_));
        td->messages_manager_->on_get_recent_locations(dialog_id_, limit_, random_id_, messages->count_,
                                                       std::move(messages->messages_));
        break;
      }
      default:
        UNREACHABLE();
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetRecentLocationsQuery");
    td->messages_manager_->on_get_recent_locations_failed(random_id_);
    promise_.set_error(std::move(status));
  }
};

class DeleteHistoryQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  MessageId max_message_id_;
  bool remove_from_dialog_list_;

  void send_request() {
    auto input_peer = td->messages_manager_->get_input_peer(dialog_id_, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(3, "Chat is not accessible"));
    }

    int32 flags = 0;
    if (!remove_from_dialog_list_) {
      flags |= telegram_api::messages_deleteHistory::JUST_CLEAR_MASK;
    }
    LOG(INFO) << "Delete " << dialog_id_ << " history up to " << max_message_id_ << " with flags " << flags;

    send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_deleteHistory(
        flags, false /*ignored*/, std::move(input_peer), max_message_id_.get_server_message_id().get()))));
  }

 public:
  explicit DeleteHistoryQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId max_message_id, bool remove_from_dialog_list) {
    dialog_id_ = dialog_id;
    max_message_id_ = max_message_id;
    remove_from_dialog_list_ = remove_from_dialog_list;

    send_request();
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_deleteHistory>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto affected_history = result_ptr.move_as_ok();
    CHECK(affected_history->get_id() == telegram_api::messages_affectedHistory::ID);

    if (affected_history->pts_count_ > 0) {
      td->messages_manager_->add_pending_update(make_tl_object<dummyUpdate>(), affected_history->pts_,
                                                affected_history->pts_count_, false, "delete history query");
    }

    if (affected_history->offset_ > 0) {
      send_request();
      return;
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "DeleteHistoryQuery");
    promise_.set_error(std::move(status));
  }
};

class DeleteChannelHistoryQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  ChannelId channel_id_;
  MessageId max_message_id_;
  bool allow_error_;

 public:
  explicit DeleteChannelHistoryQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(ChannelId channel_id, MessageId max_message_id, bool allow_error) {
    channel_id_ = channel_id;
    max_message_id_ = max_message_id;
    allow_error_ = allow_error;
    auto input_channel = td->contacts_manager_->get_input_channel(channel_id);
    CHECK(input_channel != nullptr);

    send_query(G()->net_query_creator().create(create_storer(
        telegram_api::channels_deleteHistory(std::move(input_channel), max_message_id.get_server_message_id().get()))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::channels_deleteHistory>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    bool result = result_ptr.ok();
    LOG_IF(ERROR, !allow_error_ && !result)
        << "Delete history in " << channel_id_ << " up to " << max_message_id_ << " failed";

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (!td->contacts_manager_->on_get_channel_error(channel_id_, status, "DeleteChannelHistoryQuery")) {
      LOG(ERROR) << "Receive error for deleteChannelHistory: " << status;
    }
    promise_.set_error(std::move(status));
  }
};

class DeleteUserHistoryQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  ChannelId channel_id_;
  UserId user_id_;

  void send_request() {
    auto input_channel = td->contacts_manager_->get_input_channel(channel_id_);
    if (input_channel == nullptr) {
      return promise_.set_error(Status::Error(3, "Chat is not accessible"));
    }
    auto input_user = td->contacts_manager_->get_input_user(user_id_);
    if (input_user == nullptr) {
      return promise_.set_error(Status::Error(3, "Usat is not accessible"));
    }

    LOG(INFO) << "Delete all messages from " << user_id_ << " in " << channel_id_;

    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::channels_deleteUserHistory(std::move(input_channel), std::move(input_user)))));
  }

 public:
  explicit DeleteUserHistoryQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(ChannelId channel_id, UserId user_id) {
    channel_id_ = channel_id;
    user_id_ = user_id;

    send_request();
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::channels_deleteUserHistory>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto affected_history = result_ptr.move_as_ok();
    CHECK(affected_history->get_id() == telegram_api::messages_affectedHistory::ID);

    if (affected_history->pts_count_ > 0) {
      td->messages_manager_->add_pending_channel_update(DialogId(channel_id_), make_tl_object<dummyUpdate>(),
                                                        affected_history->pts_, affected_history->pts_count_,
                                                        "delete user history query");
    }

    if (affected_history->offset_ > 0) {
      send_request();
      return;
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    td->contacts_manager_->on_get_channel_error(channel_id_, status, "DeleteUserHistoryQuery");
    promise_.set_error(std::move(status));
  }
};

class ReadAllMentionsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

  void send_request() {
    auto input_peer = td->messages_manager_->get_input_peer(dialog_id_, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(3, "Chat is not accessible"));
    }

    LOG(INFO) << "Read all mentions in " << dialog_id_;

    send_query(
        G()->net_query_creator().create(create_storer(telegram_api::messages_readMentions(std::move(input_peer)))));
  }

 public:
  explicit ReadAllMentionsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id) {
    dialog_id_ = dialog_id;

    send_request();
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_readMentions>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto affected_history = result_ptr.move_as_ok();
    CHECK(affected_history->get_id() == telegram_api::messages_affectedHistory::ID);

    if (affected_history->pts_count_ > 0) {
      if (dialog_id_.get_type() == DialogType::Channel) {
        LOG(ERROR) << "Receive pts_count " << affected_history->pts_count_ << " in result of ReadAllMentionsQuery in "
                   << dialog_id_;
        td->updates_manager_->get_difference("Wrong messages_readMentions result");
      } else {
        td->messages_manager_->add_pending_update(make_tl_object<dummyUpdate>(), affected_history->pts_,
                                                  affected_history->pts_count_, false, "read all mentions query");
      }
    }

    if (affected_history->offset_ > 0) {
      send_request();
      return;
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "ReadAllMentionsQuery");
    promise_.set_error(std::move(status));
  }
};

class SendSecretMessageActor : public NetActor {
  int64 random_id_;

 public:
  void send(DialogId dialog_id, int64 reply_to_random_id, int32 ttl, const string &message, SecretInputMedia media,
            vector<tl_object_ptr<secret_api::MessageEntity>> &&entities, UserId via_bot_user_id, int64 media_album_id,
            int64 random_id) {
    if (false && !media.empty()) {
      td->messages_manager_->on_send_secret_message_error(random_id, Status::Error(400, "FILE_PART_1_MISSING"), Auto());
      stop();
      return;
    }

    CHECK(dialog_id.get_type() == DialogType::SecretChat);
    random_id_ = random_id;

    int32 flags = 0;
    if (reply_to_random_id != 0) {
      flags |= secret_api::decryptedMessage::REPLY_TO_RANDOM_ID_MASK;
    }
    if (via_bot_user_id.is_valid()) {
      flags |= secret_api::decryptedMessage::VIA_BOT_NAME_MASK;
    }
    if (!media.empty()) {
      flags |= secret_api::decryptedMessage::MEDIA_MASK;
    }
    if (!entities.empty()) {
      flags |= secret_api::decryptedMessage::ENTITIES_MASK;
    }
    if (media_album_id != 0) {
      CHECK(media_album_id < 0);
      flags |= secret_api::decryptedMessage::GROUPED_ID_MASK;
    }

    send_closure(G()->secret_chats_manager(), &SecretChatsManager::send_message, dialog_id.get_secret_chat_id(),
                 make_tl_object<secret_api::decryptedMessage>(
                     flags, random_id, ttl, message, std::move(media.decrypted_media_), std::move(entities),
                     td->contacts_manager_->get_user_username(via_bot_user_id), reply_to_random_id, -media_album_id),
                 std::move(media.input_file_),
                 PromiseCreator::event(self_closure(this, &SendSecretMessageActor::done)));
  }

  void done() {
    stop();
  }
};

class SendMessageActor : public NetActorOnce {
  int64 random_id_;
  DialogId dialog_id_;

 public:
  void send(int32 flags, DialogId dialog_id, MessageId reply_to_message_id,
            tl_object_ptr<telegram_api::ReplyMarkup> &&reply_markup,
            vector<tl_object_ptr<telegram_api::MessageEntity>> &&entities, const string &message, int64 random_id,
            NetQueryRef *send_query_ref, uint64 sequence_dispatcher_id) {
    random_id_ = random_id;
    dialog_id_ = dialog_id;

    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      on_error(0, Status::Error(400, "Have no write access to the chat"));
      stop();
      return;
    }

    if (!entities.empty()) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_ENTITIES;
    }

    auto query = G()->net_query_creator().create(create_storer(telegram_api::messages_sendMessage(
        flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, std::move(input_peer),
        reply_to_message_id.get_server_message_id().get(), message, random_id, std::move(reply_markup),
        std::move(entities))));
    if (G()->shared_config().get_option_boolean("use_quick_ack")) {
      query->quick_ack_promise_ = PromiseCreator::lambda(
          [random_id](Unit) {
            send_closure(G()->messages_manager(), &MessagesManager::on_send_message_get_quick_ack, random_id);
          },
          PromiseCreator::Ignore());
    }
    *send_query_ref = query.get_weak();
    query->debug("send to MessagesManager::MultiSequenceDispatcher");
    send_closure(td->messages_manager_->sequence_dispatcher_, &MultiSequenceDispatcher::send_with_callback,
                 std::move(query), actor_shared(this), sequence_dispatcher_id);
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_sendMessage>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for sendMessage for " << random_id_ << ": " << to_string(ptr);

    auto constructor_id = ptr->get_id();
    if (constructor_id != telegram_api::updateShortSentMessage::ID) {
      td->messages_manager_->check_send_message_result(random_id_, dialog_id_, ptr.get(), "SendMessage");
      return td->updates_manager_->on_get_updates(std::move(ptr));
    }
    auto sent_message = move_tl_object_as<telegram_api::updateShortSentMessage>(ptr);
    td->messages_manager_->on_update_sent_text_message(random_id_, std::move(sent_message->media_),
                                                       std::move(sent_message->entities_));

    auto message_id = MessageId(ServerMessageId(sent_message->id_));
    if (dialog_id_.get_type() == DialogType::Channel) {
      td->messages_manager_->add_pending_channel_update(
          dialog_id_, make_tl_object<updateSentMessage>(random_id_, message_id, sent_message->date_),
          sent_message->pts_, sent_message->pts_count_, "send message actor");
      return;
    }

    td->messages_manager_->add_pending_update(
        make_tl_object<updateSentMessage>(random_id_, message_id, sent_message->date_), sent_message->pts_,
        sent_message->pts_count_, false, "send message actor");
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for sendMessage: " << status;
    if (G()->close_flag() && G()->parameters().use_message_db) {
      // do not send error, message will be re-sent
      return;
    }
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "SendMessageActor");
    td->messages_manager_->on_send_message_fail(random_id_, std::move(status));
  }
};

class StartBotQuery : public Td::ResultHandler {
  int64 random_id_;
  DialogId dialog_id_;

 public:
  NetQueryRef send(tl_object_ptr<telegram_api::InputUser> bot_input_user, DialogId dialog_id,
                   tl_object_ptr<telegram_api::InputPeer> input_peer, const string &parameter, int64 random_id) {
    CHECK(bot_input_user != nullptr);
    CHECK(input_peer != nullptr);
    random_id_ = random_id;
    dialog_id_ = dialog_id;

    auto query = G()->net_query_creator().create(create_storer(
        telegram_api::messages_startBot(std::move(bot_input_user), std::move(input_peer), random_id, parameter)));
    if (G()->shared_config().get_option_boolean("use_quick_ack")) {
      query->quick_ack_promise_ = PromiseCreator::lambda(
          [random_id](Unit) {
            send_closure(G()->messages_manager(), &MessagesManager::on_send_message_get_quick_ack, random_id);
          },
          PromiseCreator::Ignore());
    }
    auto send_query_ref = query.get_weak();
    send_query(std::move(query));
    return send_query_ref;
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_startBot>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for startBot for " << random_id_ << ": " << to_string(ptr);
    // Result may contain messageActionChatAddUser
    // td->messages_manager_->check_send_message_result(random_id_, dialog_id_, ptr.get(), "StartBot");
    td->updates_manager_->on_get_updates(std::move(ptr));
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for startBot: " << status;
    if (G()->close_flag() && G()->parameters().use_message_db) {
      // do not send error, message should be re-sent
      return;
    }
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "StartBotQuery");
    td->messages_manager_->on_send_message_fail(random_id_, std::move(status));
  }
};

class SendInlineBotResultQuery : public Td::ResultHandler {
  int64 random_id_;
  DialogId dialog_id_;

 public:
  NetQueryRef send(int32 flags, DialogId dialog_id, MessageId reply_to_message_id, int64 random_id, int64 query_id,
                   const string &result_id) {
    random_id_ = random_id;
    dialog_id_ = dialog_id;

    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Write);
    CHECK(input_peer != nullptr);

    auto query = G()->net_query_creator().create(create_storer(telegram_api::messages_sendInlineBotResult(
        flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, std::move(input_peer),
        reply_to_message_id.get_server_message_id().get(), random_id, query_id, result_id)));
    auto send_query_ref = query.get_weak();
    send_query(std::move(query));
    return send_query_ref;
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_sendInlineBotResult>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for sendInlineBotResult for " << random_id_ << ": " << to_string(ptr);
    td->messages_manager_->check_send_message_result(random_id_, dialog_id_, ptr.get(), "SendInlineBotResult");
    td->updates_manager_->on_get_updates(std::move(ptr));
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for sendInlineBotResult: " << status;
    if (G()->close_flag() && G()->parameters().use_message_db) {
      // do not send error, message will be re-sent
      return;
    }
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "SendInlineBotResultQuery");
    td->messages_manager_->on_send_message_fail(random_id_, std::move(status));
  }
};

class SendMultiMediaActor : public NetActorOnce {
  vector<int64> random_ids_;
  DialogId dialog_id_;

 public:
  void send(int32 flags, DialogId dialog_id, MessageId reply_to_message_id,
            vector<tl_object_ptr<telegram_api::inputSingleMedia>> &&input_single_media, uint64 sequence_dispatcher_id) {
    for (auto &single_media : input_single_media) {
      random_ids_.push_back(single_media->random_id_);
    }
    dialog_id_ = dialog_id;

    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      on_error(0, Status::Error(400, "Have no write access to the chat"));
      stop();
      return;
    }

    auto query = G()->net_query_creator().create(create_storer(telegram_api::messages_sendMultiMedia(
        flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, std::move(input_peer),
        reply_to_message_id.get_server_message_id().get(), std::move(input_single_media))));
    if (G()->shared_config().get_option_boolean("use_quick_ack")) {
      query->quick_ack_promise_ = PromiseCreator::lambda(
          [random_ids = random_ids_](Unit) {
            for (auto random_id : random_ids) {
              send_closure(G()->messages_manager(), &MessagesManager::on_send_message_get_quick_ack, random_id);
            }
          },
          PromiseCreator::Ignore());
    }
    query->debug("send to MessagesManager::MultiSequenceDispatcher");
    send_closure(td->messages_manager_->sequence_dispatcher_, &MultiSequenceDispatcher::send_with_callback,
                 std::move(query), actor_shared(this), sequence_dispatcher_id);
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_sendMultiMedia>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for sendMultiMedia for " << format::as_array(random_ids_) << ": " << to_string(ptr);

    auto sent_random_ids = td->updates_manager_->get_sent_messages_random_ids(ptr.get());
    bool is_result_wrong = false;
    auto sent_random_ids_size = sent_random_ids.size();
    for (auto &random_id : random_ids_) {
      auto it = sent_random_ids.find(random_id);
      if (it == sent_random_ids.end()) {
        if (random_ids_.size() == 1) {
          is_result_wrong = true;
        }
        td->messages_manager_->on_send_message_fail(random_id, Status::Error(400, "Message was not sent"));
      } else {
        sent_random_ids.erase(it);
      }
    }
    if (!sent_random_ids.empty()) {
      is_result_wrong = true;
    }
    if (!is_result_wrong) {
      auto sent_messages = td->updates_manager_->get_new_messages(ptr.get());
      if (sent_random_ids_size != sent_messages.size()) {
        is_result_wrong = true;
      }
      for (auto &sent_message : sent_messages) {
        if (td->messages_manager_->get_message_dialog_id(*sent_message) != dialog_id_) {
          is_result_wrong = true;
        }
      }
    }
    if (is_result_wrong) {
      LOG(ERROR) << "Receive wrong result for sendMultiMedia with random_ids " << format::as_array(random_ids_)
                 << " to " << dialog_id_ << ": " << oneline(to_string(ptr));
      td->updates_manager_->schedule_get_difference("Wrong sendMultiMedia result");
    }

    td->updates_manager_->on_get_updates(std::move(ptr));
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for sendMultiMedia: " << status;
    if (G()->close_flag() && G()->parameters().use_message_db) {
      // do not send error, message will be re-sent
      return;
    }
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "SendMultiMediaActor");
    for (auto &random_id : random_ids_) {
      td->messages_manager_->on_send_message_fail(random_id, status.clone());
    }
  }
};

class SendMediaActor : public NetActorOnce {
  int64 random_id_;
  FileId file_id_;
  FileId thumbnail_file_id_;
  DialogId dialog_id_;

 public:
  void send(FileId file_id, FileId thumbnail_file_id, int32 flags, DialogId dialog_id, MessageId reply_to_message_id,
            tl_object_ptr<telegram_api::ReplyMarkup> &&reply_markup,
            vector<tl_object_ptr<telegram_api::MessageEntity>> &&entities, const string &message,
            tl_object_ptr<telegram_api::InputMedia> &&input_media, int64 random_id, NetQueryRef *send_query_ref,
            uint64 sequence_dispatcher_id) {
    random_id_ = random_id;
    file_id_ = file_id;
    thumbnail_file_id_ = thumbnail_file_id;
    dialog_id_ = dialog_id;

    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      on_error(0, Status::Error(400, "Have no write access to the chat"));
      stop();
      return;
    }
    if (!entities.empty()) {
      flags |= telegram_api::messages_sendMedia::ENTITIES_MASK;
    }

    telegram_api::messages_sendMedia request(flags, false /*ignored*/, false /*ignored*/, false /*ignored*/,
                                             std::move(input_peer), reply_to_message_id.get_server_message_id().get(),
                                             std::move(input_media), message, random_id, std::move(reply_markup),
                                             std::move(entities));
    LOG(INFO) << "Send media: " << to_string(request);
    auto query = G()->net_query_creator().create(create_storer(request));
    if (G()->shared_config().get_option_boolean("use_quick_ack")) {
      query->quick_ack_promise_ = PromiseCreator::lambda(
          [random_id](Unit) {
            send_closure(G()->messages_manager(), &MessagesManager::on_send_message_get_quick_ack, random_id);
          },
          PromiseCreator::Ignore());
    }
    *send_query_ref = query.get_weak();
    query->debug("send to MessagesManager::MultiSequenceDispatcher");
    send_closure(td->messages_manager_->sequence_dispatcher_, &MultiSequenceDispatcher::send_with_callback,
                 std::move(query), actor_shared(this), sequence_dispatcher_id);
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_sendMedia>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    if (thumbnail_file_id_.is_valid()) {
      // always delete partial remote location for the thumbnail, because it can't be reused anyway
      // TODO delete it only in the case it can't be merged with file thumbnail
      td->file_manager_->delete_partial_remote_location(thumbnail_file_id_);
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for sendMedia for " << random_id_ << ": " << to_string(ptr);
    td->messages_manager_->check_send_message_result(random_id_, dialog_id_, ptr.get(), "SendMedia");
    td->updates_manager_->on_get_updates(std::move(ptr));
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for sendMedia: " << status;
    if (G()->close_flag() && G()->parameters().use_message_db) {
      // do not send error, message will be re-sent
      return;
    }
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "SendMediaActor");
    if (thumbnail_file_id_.is_valid()) {
      // always delete partial remote location for the thumbnail, because it can't be reused anyway
      td->file_manager_->delete_partial_remote_location(thumbnail_file_id_);
    }
    if (file_id_.is_valid()) {
      if (begins_with(status.message(), "FILE_PART_") && ends_with(status.message(), "_MISSING")) {
        td->messages_manager_->on_send_message_file_part_missing(random_id_,
                                                                 to_integer<int32>(status.message().substr(10)));
        return;
      } else {
        if (status.code() != 429 && status.code() < 500 && !G()->close_flag()) {
          td->file_manager_->delete_partial_remote_location(file_id_);
        }
      }
    }
    td->messages_manager_->on_send_message_fail(random_id_, std::move(status));
  }
};

class UploadMediaQuery : public Td::ResultHandler {
  DialogId dialog_id_;
  MessageId message_id_;
  FileId file_id_;
  FileId thumbnail_file_id_;

 public:
  void send(DialogId dialog_id, MessageId message_id, FileId file_id, FileId thumbnail_file_id,
            tl_object_ptr<telegram_api::InputMedia> &&input_media) {
    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      on_error(0, Status::Error(400, "Have no write access to the chat"));
      return;
    }
    CHECK(input_media != nullptr);

    dialog_id_ = dialog_id;
    message_id_ = message_id;
    file_id_ = file_id;
    thumbnail_file_id_ = thumbnail_file_id;

    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::messages_uploadMedia(std::move(input_peer), std::move(input_media)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_uploadMedia>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    if (thumbnail_file_id_.is_valid()) {
      // always delete partial remote location for the thumbnail, because it can't be reused anyway
      td->file_manager_->delete_partial_remote_location(thumbnail_file_id_);
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for uploadMedia: " << to_string(ptr);
    td->messages_manager_->on_upload_message_media_success(dialog_id_, message_id_, std::move(ptr));
  }

  void on_error(uint64 id, Status status) override {
    LOG(WARNING) << "Receive error for uploadMedia: " << status;
    if (G()->close_flag() && G()->parameters().use_message_db) {
      // do not send error, message will be re-sent
      return;
    }
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "UploadMediaQuery");
    if (thumbnail_file_id_.is_valid()) {
      // always delete partial remote location for the thumbnail, because it can't be reused anyway
      td->file_manager_->delete_partial_remote_location(thumbnail_file_id_);
    }
    if (file_id_.is_valid()) {
      if (begins_with(status.message(), "FILE_PART_") && ends_with(status.message(), "_MISSING")) {
        td->messages_manager_->on_upload_message_media_file_part_missing(
            dialog_id_, message_id_, to_integer<int32>(status.message().substr(10)));
        return;
      } else {
        if (status.code() != 429 && status.code() < 500 && !G()->close_flag()) {
          td->file_manager_->delete_partial_remote_location(file_id_);
        }
      }
    }
    td->messages_manager_->on_upload_message_media_fail(dialog_id_, message_id_, std::move(status));
  }
};

class EditMessageActor : public NetActorOnce {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit EditMessageActor(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(int32 flags, DialogId dialog_id, MessageId message_id, const string &message,
            vector<tl_object_ptr<telegram_api::MessageEntity>> &&entities,
            tl_object_ptr<telegram_api::InputMedia> &&input_media,
            tl_object_ptr<telegram_api::InputGeoPoint> &&input_geo_point,
            tl_object_ptr<telegram_api::ReplyMarkup> &&reply_markup, uint64 sequence_dispatcher_id) {
    if (false && input_media != nullptr) {
      on_error(0, Status::Error(400, "FILE_PART_1_MISSING"));
      stop();
      return;
    }

    dialog_id_ = dialog_id;

    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Edit);
    if (input_peer == nullptr) {
      on_error(0, Status::Error(400, "Can't access the chat"));
      stop();
      return;
    }

    if (reply_markup != nullptr) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_REPLY_MARKUP;
    }
    if (!entities.empty()) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_ENTITIES;
    }
    if (!message.empty()) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_MESSAGE;
    }
    if (input_media != nullptr) {
      flags |= telegram_api::messages_editMessage::MEDIA_MASK;
    }
    if (input_geo_point != nullptr) {
      flags |= telegram_api::messages_editMessage::GEO_POINT_MASK;
    }
    LOG(DEBUG) << "Edit message with flags " << flags;

    auto query = G()->net_query_creator().create(create_storer(telegram_api::messages_editMessage(
        flags, false /*ignored*/, false /*ignored*/, std::move(input_peer), message_id.get_server_message_id().get(),
        message, std::move(input_media), std::move(reply_markup), std::move(entities), std::move(input_geo_point))));

    query->debug("send to MessagesManager::MultiSequenceDispatcher");
    send_closure(td->messages_manager_->sequence_dispatcher_, &MultiSequenceDispatcher::send_with_callback,
                 std::move(query), actor_shared(this), sequence_dispatcher_id);
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_editMessage>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for editMessage: " << to_string(ptr);
    if (ptr->get_id() == telegram_api::updateShortSentMessage::ID) {
      LOG(ERROR) << "Receive updateShortSentMessage in edit message";
      return on_error(id, Status::Error(500, "Unsupported result was returned from the server"));
    }

    td->updates_manager_->on_get_updates(std::move(ptr));

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for editMessage: " << status;
    if (!td->auth_manager_->is_bot() && status.message() == "MESSAGE_NOT_MODIFIED") {
      return promise_.set_value(Unit());
    }
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "EditMessageActor");
    promise_.set_error(std::move(status));
  }
};

class EditInlineMessageQuery : public Td::ResultHandler {
  Promise<Unit> promise_;

 public:
  explicit EditInlineMessageQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(int32 flags, tl_object_ptr<telegram_api::inputBotInlineMessageID> input_bot_inline_message_id,
            const string &message, vector<tl_object_ptr<telegram_api::MessageEntity>> &&entities,
            tl_object_ptr<telegram_api::InputMedia> &&input_media,
            tl_object_ptr<telegram_api::InputGeoPoint> &&input_geo_point,
            tl_object_ptr<telegram_api::ReplyMarkup> &&reply_markup) {
    CHECK(input_bot_inline_message_id != nullptr);

    if (reply_markup != nullptr) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_REPLY_MARKUP;
    }
    if (!entities.empty()) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_ENTITIES;
    }
    if (!message.empty()) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_MESSAGE;
    }
    if (input_geo_point != nullptr) {
      flags |= telegram_api::messages_editInlineBotMessage::GEO_POINT_MASK;
    }
    if (input_media != nullptr) {
      flags |= telegram_api::messages_editInlineBotMessage::MEDIA_MASK;
    }
    LOG(DEBUG) << "Edit inline message with flags " << flags;

    auto dc_id = DcId::internal(input_bot_inline_message_id->dc_id_);
    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::messages_editInlineBotMessage(
            flags, false /*ignored*/, false /*ignored*/, std::move(input_bot_inline_message_id), message,
            std::move(input_media), std::move(reply_markup), std::move(entities), std::move(input_geo_point))),
        dc_id));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_editInlineBotMessage>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    LOG_IF(ERROR, !result_ptr.ok()) << "Receive false in result of editInlineMessage";

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for editInlineMessage: " << status;
    promise_.set_error(std::move(status));
  }
};

class SetGameScoreActor : public NetActorOnce {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit SetGameScoreActor(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId message_id, bool edit_message,
            tl_object_ptr<telegram_api::InputUser> input_user, int32 score, bool force, uint64 sequence_dispatcher_id) {
    int32 flags = 0;
    if (edit_message) {
      flags |= telegram_api::messages_setGameScore::EDIT_MESSAGE_MASK;
    }
    if (force) {
      flags |= telegram_api::messages_setGameScore::FORCE_MASK;
    }

    dialog_id_ = dialog_id;

    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Edit);
    if (input_peer == nullptr) {
      on_error(0, Status::Error(400, "Can't access the chat"));
      stop();
      return;
    }

    CHECK(input_user != nullptr);
    auto query = G()->net_query_creator().create(create_storer(
        telegram_api::messages_setGameScore(flags, false /*ignored*/, false /*ignored*/, std::move(input_peer),
                                            message_id.get_server_message_id().get(), std::move(input_user), score)));

    LOG(INFO) << "Set game score to " << score;

    query->debug("send to MessagesManager::MultiSequenceDispatcher");
    send_closure(td->messages_manager_->sequence_dispatcher_, &MultiSequenceDispatcher::send_with_callback,
                 std::move(query), actor_shared(this), sequence_dispatcher_id);
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_setGameScore>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for setGameScore: " << to_string(ptr);
    td->updates_manager_->on_get_updates(std::move(ptr));

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for setGameScore: " << status;
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "SetGameScoreActor");
    promise_.set_error(std::move(status));
  }
};

class SetInlineGameScoreQuery : public Td::ResultHandler {
  Promise<Unit> promise_;

 public:
  explicit SetInlineGameScoreQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(tl_object_ptr<telegram_api::inputBotInlineMessageID> input_bot_inline_message_id, bool edit_message,
            tl_object_ptr<telegram_api::InputUser> input_user, int32 score, bool force) {
    CHECK(input_bot_inline_message_id != nullptr);
    CHECK(input_user != nullptr);

    int32 flags = 0;
    if (edit_message) {
      flags |= telegram_api::messages_setInlineGameScore::EDIT_MESSAGE_MASK;
    }
    if (force) {
      flags |= telegram_api::messages_setInlineGameScore::FORCE_MASK;
    }

    LOG(INFO) << "Set inline game score to " << score;
    auto dc_id = DcId::internal(input_bot_inline_message_id->dc_id_);
    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::messages_setInlineGameScore(flags, false /*ignored*/, false /*ignored*/,
                                                                std::move(input_bot_inline_message_id),
                                                                std::move(input_user), score)),
        dc_id));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_setInlineGameScore>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    LOG_IF(ERROR, !result_ptr.ok()) << "Receive false in result of setInlineGameScore";

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for setInlineGameScore: " << status;
    promise_.set_error(std::move(status));
  }
};

class GetGameHighScoresQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  int64 random_id_;

 public:
  explicit GetGameHighScoresQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId message_id, tl_object_ptr<telegram_api::InputUser> input_user,
            int64 random_id) {
    dialog_id_ = dialog_id;
    random_id_ = random_id;

    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);

    CHECK(input_user != nullptr);
    send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_getGameHighScores(
        std::move(input_peer), message_id.get_server_message_id().get(), std::move(input_user)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getGameHighScores>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    td->messages_manager_->on_get_game_high_scores(random_id_, result_ptr.move_as_ok());
    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for getGameHighScores: " << status;
    td->messages_manager_->on_get_game_high_scores(random_id_, nullptr);
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetGameHighScoresQuery");
    promise_.set_error(std::move(status));
  }
};

class GetInlineGameHighScoresQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  int64 random_id_;

 public:
  explicit GetInlineGameHighScoresQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(tl_object_ptr<telegram_api::inputBotInlineMessageID> input_bot_inline_message_id,
            tl_object_ptr<telegram_api::InputUser> input_user, int64 random_id) {
    CHECK(input_bot_inline_message_id != nullptr);
    CHECK(input_user != nullptr);

    random_id_ = random_id;

    auto dc_id = DcId::internal(input_bot_inline_message_id->dc_id_);
    send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_getInlineGameHighScores(
                                                   std::move(input_bot_inline_message_id), std::move(input_user))),
                                               dc_id));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getInlineGameHighScores>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    td->messages_manager_->on_get_game_high_scores(random_id_, result_ptr.move_as_ok());
    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for getInlineGameHighScores: " << status;
    td->messages_manager_->on_get_game_high_scores(random_id_, nullptr);
    promise_.set_error(std::move(status));
  }
};

class ForwardMessagesActor : public NetActorOnce {
  Promise<Unit> promise_;
  vector<int64> random_ids_;
  DialogId to_dialog_id_;

 public:
  explicit ForwardMessagesActor(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(int32 flags, DialogId to_dialog_id, DialogId from_dialog_id, const vector<MessageId> &message_ids,
            vector<int64> &&random_ids, uint64 sequence_dispatcher_id) {
    LOG(INFO) << "Forward " << format::as_array(message_ids) << " from " << from_dialog_id << " to " << to_dialog_id;

    random_ids_ = random_ids;
    to_dialog_id_ = to_dialog_id;

    auto to_input_peer = td->messages_manager_->get_input_peer(to_dialog_id, AccessRights::Write);
    if (to_input_peer == nullptr) {
      on_error(0, Status::Error(400, "Have no write access to the chat"));
      stop();
      return;
    }

    auto from_input_peer = td->messages_manager_->get_input_peer(from_dialog_id, AccessRights::Read);
    if (from_input_peer == nullptr) {
      on_error(0, Status::Error(400, "Can't access the chat to forward messages from"));
      stop();
      return;
    }

    auto query = G()->net_query_creator().create(create_storer(telegram_api::messages_forwardMessages(
        flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, std::move(from_input_peer),
        MessagesManager::get_server_message_ids(message_ids), std::move(random_ids), std::move(to_input_peer))));
    if (G()->shared_config().get_option_boolean("use_quick_ack")) {
      query->quick_ack_promise_ = PromiseCreator::lambda(
          [random_ids = random_ids_](Unit) {
            for (auto random_id : random_ids) {
              send_closure(G()->messages_manager(), &MessagesManager::on_send_message_get_quick_ack, random_id);
            }
          },
          PromiseCreator::Ignore());
    }
    send_closure(td->messages_manager_->sequence_dispatcher_, &MultiSequenceDispatcher::send_with_callback,
                 std::move(query), actor_shared(this), sequence_dispatcher_id);
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_forwardMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for forwardMessages for " << format::as_array(random_ids_) << ": " << to_string(ptr);
    auto sent_random_ids = td->updates_manager_->get_sent_messages_random_ids(ptr.get());
    bool is_result_wrong = false;
    auto sent_random_ids_size = sent_random_ids.size();
    for (auto &random_id : random_ids_) {
      auto it = sent_random_ids.find(random_id);
      if (it == sent_random_ids.end()) {
        if (random_ids_.size() == 1) {
          is_result_wrong = true;
        }
        td->messages_manager_->on_send_message_fail(random_id, Status::Error(400, "Message was not forwarded"));
      } else {
        sent_random_ids.erase(it);
      }
    }
    if (!sent_random_ids.empty()) {
      is_result_wrong = true;
    }
    if (!is_result_wrong) {
      auto sent_messages = td->updates_manager_->get_new_messages(ptr.get());
      if (sent_random_ids_size != sent_messages.size()) {
        is_result_wrong = true;
      }
      for (auto &sent_message : sent_messages) {
        if (td->messages_manager_->get_message_dialog_id(*sent_message) != to_dialog_id_) {
          is_result_wrong = true;
        }
      }
    }
    if (is_result_wrong) {
      LOG(ERROR) << "Receive wrong result for forwarding messages with random_ids " << format::as_array(random_ids_)
                 << " to " << to_dialog_id_ << ": " << oneline(to_string(ptr));
      td->updates_manager_->schedule_get_difference("Wrong forwardMessages result");
    }

    td->updates_manager_->on_get_updates(std::move(ptr));
    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for forward messages: " << status;
    if (G()->close_flag() && G()->parameters().use_message_db) {
      // do not send error, messages should be re-sent
      return;
    }
    // no on_get_dialog_error call, because two dialogs are involved
    for (auto &random_id : random_ids_) {
      td->messages_manager_->on_send_message_fail(random_id, status.clone());
    }
    promise_.set_error(std::move(status));
  }
};

class SendScreenshotNotificationQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  int64 random_id_;
  DialogId dialog_id_;

 public:
  explicit SendScreenshotNotificationQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, int64 random_id) {
    random_id_ = random_id;
    dialog_id_ = dialog_id;

    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Write);
    CHECK(input_peer != nullptr);

    auto query = G()->net_query_creator().create(
        create_storer(telegram_api::messages_sendScreenshotNotification(std::move(input_peer), 0, random_id)));
    send_query(std::move(query));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_sendScreenshotNotification>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SendScreenshotNotificationQuery for " << random_id_ << ": " << to_string(ptr);
    td->messages_manager_->check_send_message_result(random_id_, dialog_id_, ptr.get(),
                                                     "SendScreenshotNotificationQuery");
    td->updates_manager_->on_get_updates(std::move(ptr));
    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for SendScreenshotNotificationQuery: " << status;
    if (G()->close_flag() && G()->parameters().use_message_db) {
      // do not send error, messages should be re-sent
      return;
    }
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "SendScreenshotNotificationQuery");
    td->messages_manager_->on_send_message_fail(random_id_, status.clone());
    promise_.set_error(std::move(status));
  }
};

class SetTypingQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit SetTypingQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  NetQueryRef send(DialogId dialog_id, tl_object_ptr<telegram_api::SendMessageAction> &&action) {
    dialog_id_ = dialog_id;
    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Write);
    CHECK(input_peer != nullptr);

    auto net_query = G()->net_query_creator().create(
        create_storer(telegram_api::messages_setTyping(std::move(input_peer), std::move(action))));
    auto result = net_query.get_weak();
    send_query(std::move(net_query));
    return result;
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_setTyping>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    // ignore result

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (status.code() == NetQuery::Cancelled) {
      return promise_.set_value(Unit());
    }

    if (!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "SetTypingQuery")) {
      LOG(INFO) << "Receive error for set typing: " << status;
    }
    promise_.set_error(std::move(status));
  }
};

class DeleteMessagesQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  int32 query_count_;

 public:
  explicit DeleteMessagesQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(vector<MessageId> &&message_ids, bool revoke) {
    LOG(INFO) << "Send deleteMessagesQuery to delete " << format::as_array(message_ids);
    int32 flags = 0;
    if (revoke) {
      flags |= telegram_api::messages_deleteMessages::REVOKE_MASK;
    }

    query_count_ = 0;
    auto server_message_ids = MessagesManager::get_server_message_ids(message_ids);
    const size_t MAX_SLICE_SIZE = 100;
    for (size_t i = 0; i < server_message_ids.size(); i += MAX_SLICE_SIZE) {
      auto end_i = i + MAX_SLICE_SIZE;
      auto end = end_i < server_message_ids.size() ? server_message_ids.begin() + end_i : server_message_ids.end();
      vector<int32> slice(server_message_ids.begin() + i, end);

      query_count_++;
      send_query(G()->net_query_creator().create(
          create_storer(telegram_api::messages_deleteMessages(flags, false /*ignored*/, std::move(slice)))));
    }
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_deleteMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto affected_messages = result_ptr.move_as_ok();
    CHECK(affected_messages->get_id() == telegram_api::messages_affectedMessages::ID);

    if (affected_messages->pts_count_ > 0) {
      td->messages_manager_->add_pending_update(make_tl_object<dummyUpdate>(), affected_messages->pts_,
                                                affected_messages->pts_count_, false, "delete messages query");
    }
    if (--query_count_ == 0) {
      promise_.set_value(Unit());
    }
  }

  void on_error(uint64 id, Status status) override {
    LOG(ERROR) << "Receive error for delete messages: " << status;
    promise_.set_error(std::move(status));
  }
};

class DeleteChannelMessagesQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  int32 query_count_;
  ChannelId channel_id_;

 public:
  explicit DeleteChannelMessagesQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(ChannelId channel_id, vector<MessageId> &&message_ids) {
    channel_id_ = channel_id;
    LOG(INFO) << "Send deleteChannelMessagesQuery to delete " << format::as_array(message_ids) << " in the "
              << channel_id;

    query_count_ = 0;
    auto server_message_ids = MessagesManager::get_server_message_ids(message_ids);
    const size_t MAX_SLICE_SIZE = 100;
    for (size_t i = 0; i < server_message_ids.size(); i += MAX_SLICE_SIZE) {
      auto end_i = i + MAX_SLICE_SIZE;
      auto end = end_i < server_message_ids.size() ? server_message_ids.begin() + end_i : server_message_ids.end();
      vector<int32> slice(server_message_ids.begin() + i, end);

      query_count_++;
      auto input_channel = td->contacts_manager_->get_input_channel(channel_id);
      CHECK(input_channel != nullptr);
      send_query(G()->net_query_creator().create(
          create_storer(telegram_api::channels_deleteMessages(std::move(input_channel), std::move(slice)))));
    }
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::channels_deleteMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto affected_messages = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for deleteChannelMessages: " << to_string(affected_messages);
    CHECK(affected_messages->get_id() == telegram_api::messages_affectedMessages::ID);

    if (affected_messages->pts_count_ > 0) {
      td->messages_manager_->add_pending_channel_update(DialogId(channel_id_), make_tl_object<dummyUpdate>(),
                                                        affected_messages->pts_, affected_messages->pts_count_,
                                                        "DeleteChannelMessagesQuery");
    }
    if (--query_count_ == 0) {
      promise_.set_value(Unit());
    }
  }

  void on_error(uint64 id, Status status) override {
    if (!td->contacts_manager_->on_get_channel_error(channel_id_, status, "DeleteChannelMessagesQuery")) {
      LOG(ERROR) << "Receive error for delete channel messages: " << status;
    }
    promise_.set_error(std::move(status));
  }
};

class GetDialogNotifySettingsQuery : public Td::ResultHandler {
  DialogId dialog_id_;

 public:
  void send(DialogId dialog_id) {
    dialog_id_ = dialog_id;
    auto input_notify_peer = td->messages_manager_->get_input_notify_peer(dialog_id);
    CHECK(input_notify_peer != nullptr);
    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::account_getNotifySettings(std::move(input_notify_peer)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::account_getNotifySettings>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    td->messages_manager_->on_update_dialog_notify_settings(dialog_id_, std::move(ptr));
    td->messages_manager_->on_get_dialog_notification_settings_query_finished(dialog_id_, Status::OK());
  }

  void on_error(uint64 id, Status status) override {
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetDialogNotifySettingsQuery");
    td->messages_manager_->on_get_dialog_notification_settings_query_finished(dialog_id_, std::move(status));
  }
};

class GetScopeNotifySettingsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  NotificationSettingsScope scope_;

 public:
  explicit GetScopeNotifySettingsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(NotificationSettingsScope scope) {
    scope_ = scope;
    auto input_notify_peer = get_input_notify_peer(scope);
    CHECK(input_notify_peer != nullptr);
    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::account_getNotifySettings(std::move(input_notify_peer)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::account_getNotifySettings>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    td->messages_manager_->on_update_scope_notify_settings(scope_, std::move(ptr));

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    promise_.set_error(std::move(status));
  }
};

class UpdateDialogNotifySettingsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit UpdateDialogNotifySettingsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, const DialogNotificationSettings &new_settings) {
    auto input_notify_peer = td->messages_manager_->get_input_notify_peer(dialog_id);
    if (input_notify_peer == nullptr) {
      on_error(0, Status::Error(500, "Can't update chat notification settings"));
      return;
    }
    int32 flags = 0;
    if (!new_settings.use_default_mute_until) {
      flags |= telegram_api::inputPeerNotifySettings::MUTE_UNTIL_MASK;
    }
    if (!new_settings.use_default_sound) {
      flags |= telegram_api::inputPeerNotifySettings::SOUND_MASK;
    }
    if (!new_settings.use_default_show_preview) {
      flags |= telegram_api::inputPeerNotifySettings::SHOW_PREVIEWS_MASK;
    }
    if (new_settings.silent_send_message) {
      flags |= telegram_api::inputPeerNotifySettings::SILENT_MASK;
    }
    send_query(G()->net_query_creator().create(create_storer(telegram_api::account_updateNotifySettings(
        std::move(input_notify_peer), make_tl_object<telegram_api::inputPeerNotifySettings>(
                                          flags, new_settings.show_preview, new_settings.silent_send_message,
                                          new_settings.mute_until, new_settings.sound)))));
    dialog_id_ = dialog_id;
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::account_updateNotifySettings>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    bool result = result_ptr.ok();
    if (!result) {
      return on_error(id, Status::Error(400, "Receive false as result"));
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "UpdateDialogNotifySettingsQuery")) {
      LOG(INFO) << "Receive error for set chat notification settings: " << status;
    }

    if (!td->auth_manager_->is_bot() && td->messages_manager_->get_input_notify_peer(dialog_id_) != nullptr) {
      // trying to repair notification settings for this dialog
      td->messages_manager_->send_get_dialog_notification_settings_query(dialog_id_, Promise<>());
    }

    promise_.set_error(std::move(status));
  }
};

class UpdateScopeNotifySettingsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  NotificationSettingsScope scope_;

 public:
  explicit UpdateScopeNotifySettingsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(NotificationSettingsScope scope, const ScopeNotificationSettings &new_settings) {
    auto input_notify_peer = get_input_notify_peer(scope);
    CHECK(input_notify_peer != nullptr);
    int32 flags = telegram_api::inputPeerNotifySettings::MUTE_UNTIL_MASK |
                  telegram_api::inputPeerNotifySettings::SOUND_MASK |
                  telegram_api::inputPeerNotifySettings::SHOW_PREVIEWS_MASK;
    send_query(G()->net_query_creator().create(create_storer(telegram_api::account_updateNotifySettings(
        std::move(input_notify_peer),
        make_tl_object<telegram_api::inputPeerNotifySettings>(flags, new_settings.show_preview, false,
                                                              new_settings.mute_until, new_settings.sound)))));
    scope_ = scope;
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::account_updateNotifySettings>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    bool result = result_ptr.ok();
    if (!result) {
      return on_error(id, Status::Error(400, "Receive false as result"));
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for set notification settings: " << status;

    if (!td->auth_manager_->is_bot()) {
      // trying to repair notification settings for this scope
      td->messages_manager_->send_get_scope_notification_settings_query(scope_, Promise<>());
    }

    promise_.set_error(std::move(status));
  }
};

class ResetNotifySettingsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;

 public:
  explicit ResetNotifySettingsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send() {
    send_query(G()->net_query_creator().create(create_storer(telegram_api::account_resetNotifySettings())));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::account_resetNotifySettings>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    bool result = result_ptr.ok();
    if (!result) {
      return on_error(id, Status::Error(400, "Receive false as result"));
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(ERROR) << "Receive error for reset notification settings: " << status;
    promise_.set_error(std::move(status));
  }
};

class GetPeerSettingsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit GetPeerSettingsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id) {
    dialog_id_ = dialog_id;

    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);

    send_query(
        G()->net_query_creator().create(create_storer(telegram_api::messages_getPeerSettings(std::move(input_peer)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_getPeerSettings>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    td->messages_manager_->on_get_peer_settings(dialog_id_, result_ptr.move_as_ok());

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for get peer settings: " << status;
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetPeerSettingsQuery");
    promise_.set_error(std::move(status));
  }
};

class UpdatePeerSettingsQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit UpdatePeerSettingsQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, bool is_spam_dialog) {
    dialog_id_ = dialog_id;

    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);

    if (is_spam_dialog) {
      send_query(
          G()->net_query_creator().create(create_storer(telegram_api::messages_reportSpam(std::move(input_peer)))));
    } else {
      send_query(
          G()->net_query_creator().create(create_storer(telegram_api::messages_hideReportSpam(std::move(input_peer)))));
    }
  }

  void on_result(uint64 id, BufferSlice packet) override {
    static_assert(std::is_same<telegram_api::messages_reportSpam::ReturnType,
                               telegram_api::messages_hideReportSpam::ReturnType>::value,
                  "");
    auto result_ptr = fetch_result<telegram_api::messages_reportSpam>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    td->messages_manager_->on_get_peer_settings(dialog_id_,
                                                make_tl_object<telegram_api::peerSettings>(0, false /*ignored*/));

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for update peer settings: " << status;
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "UpdatePeerSettingsQuery");
    promise_.set_error(std::move(status));
  }
};

class ReportEncryptedSpamQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit ReportEncryptedSpamQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id) {
    dialog_id_ = dialog_id;

    auto input_peer = td->messages_manager_->get_input_encrypted_chat(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);
    LOG(INFO) << "Report spam in " << to_string(input_peer);

    send_query(G()->net_query_creator().create(
        create_storer(telegram_api::messages_reportEncryptedSpam(std::move(input_peer)))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::messages_reportEncryptedSpam>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    td->messages_manager_->on_get_peer_settings(dialog_id_,
                                                make_tl_object<telegram_api::peerSettings>(0, false /*ignored*/));

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for report encrypted spam: " << status;
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "ReportEncryptedSpamQuery");
    promise_.set_error(std::move(status));
  }
};

class ReportPeerQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit ReportPeerQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, tl_object_ptr<telegram_api::ReportReason> &&report_reason,
            const vector<MessageId> &message_ids) {
    dialog_id_ = dialog_id;

    auto input_peer = td->messages_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);

    if (message_ids.empty()) {
      send_query(G()->net_query_creator().create(
          create_storer(telegram_api::account_reportPeer(std::move(input_peer), std::move(report_reason)))));
    } else {
      send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_report(
          std::move(input_peer), MessagesManager::get_server_message_ids(message_ids), std::move(report_reason)))));
    }
  }

  void on_result(uint64 id, BufferSlice packet) override {
    static_assert(
        std::is_same<telegram_api::account_reportPeer::ReturnType, telegram_api::messages_report::ReturnType>::value,
        "");
    auto result_ptr = fetch_result<telegram_api::account_reportPeer>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    bool result = result_ptr.ok();
    if (!result) {
      return on_error(id, Status::Error(400, "Receive false as result"));
    }

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    LOG(INFO) << "Receive error for report peer: " << status;
    td->messages_manager_->on_get_dialog_error(dialog_id_, status, "ReportPeerQuery");
    promise_.set_error(std::move(status));
  }
};

class GetChannelDifferenceQuery : public Td::ResultHandler {
  DialogId dialog_id_;
  int32 pts_;
  int32 limit_;

 public:
  void send(DialogId dialog_id, tl_object_ptr<telegram_api::InputChannel> &&input_channel, int32 pts, int32 limit,
            bool force) {
    CHECK(pts >= 0);
    dialog_id_ = dialog_id;
    pts_ = pts;
    limit_ = limit;
    CHECK(input_channel != nullptr);

    int32 flags = 0;
    if (force) {
      flags |= telegram_api::updates_getChannelDifference::FORCE_MASK;
    }
    send_query(G()->net_query_creator().create(create_storer(telegram_api::updates_getChannelDifference(
        flags, false /*ignored*/, std::move(input_channel), make_tl_object<telegram_api::channelMessagesFilterEmpty>(),
        pts, limit))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::updates_getChannelDifference>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    td->messages_manager_->on_get_channel_difference(dialog_id_, pts_, limit_, result_ptr.move_as_ok());
  }

  void on_error(uint64 id, Status status) override {
    if (!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetChannelDifferenceQuery")) {
      LOG(ERROR) << "updates.getChannelDifference error for " << dialog_id_ << ": " << status;
    }
    td->messages_manager_->on_get_channel_difference(dialog_id_, pts_, limit_, nullptr);
    status.ignore();
  }
};

class ResolveUsernameQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  string username_;

 public:
  explicit ResolveUsernameQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(const string &username) {
    username_ = username;

    LOG(INFO) << "Send ResolveUsernameQuery with username = " << username;
    send_query(G()->net_query_creator().create(create_storer(telegram_api::contacts_resolveUsername(username))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::contacts_resolveUsername>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(DEBUG) << "Receive result for resolveUsername " << to_string(ptr);
    td->contacts_manager_->on_get_users(std::move(ptr->users_));
    td->contacts_manager_->on_get_chats(std::move(ptr->chats_));

    td->messages_manager_->on_resolved_username(username_, DialogId(ptr->peer_));

    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    if (status.message() == Slice("USERNAME_NOT_OCCUPIED")) {
      td->messages_manager_->drop_username(username_);
    }
    promise_.set_error(std::move(status));
  }
};

class GetChannelAdminLogQuery : public Td::ResultHandler {
  Promise<Unit> promise_;
  ChannelId channel_id_;
  int64 random_id_;

 public:
  explicit GetChannelAdminLogQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(ChannelId channel_id, const string &query, int64 from_event_id, int32 limit,
            tl_object_ptr<telegram_api::channelAdminLogEventsFilter> filter,
            vector<tl_object_ptr<telegram_api::InputUser>> input_users, int64 random_id) {
    channel_id_ = channel_id;
    random_id_ = random_id;

    auto input_channel = td->contacts_manager_->get_input_channel(channel_id);
    CHECK(input_channel != nullptr);

    int32 flags = 0;
    if (filter != nullptr) {
      flags |= telegram_api::channels_getAdminLog::EVENTS_FILTER_MASK;
    }
    if (!input_users.empty()) {
      flags |= telegram_api::channels_getAdminLog::ADMINS_MASK;
    }

    send_query(G()->net_query_creator().create(create_storer(telegram_api::channels_getAdminLog(
        flags, std::move(input_channel), query, std::move(filter), std::move(input_users), from_event_id, 0, limit))));
  }

  void on_result(uint64 id, BufferSlice packet) override {
    auto result_ptr = fetch_result<telegram_api::channels_getAdminLog>(packet);
    if (result_ptr.is_error()) {
      return on_error(id, result_ptr.move_as_error());
    }

    td->messages_manager_->on_get_event_log(random_id_, result_ptr.move_as_ok());
    promise_.set_value(Unit());
  }

  void on_error(uint64 id, Status status) override {
    td->contacts_manager_->on_get_channel_error(channel_id_, status, "GetChannelAdminLogQuery");
    td->messages_manager_->on_get_event_log(random_id_, nullptr);
    promise_.set_error(std::move(status));
  }
};

class MessagesManager::UploadMediaCallback : public FileManager::UploadCallback {
 public:
  void on_progress(FileId file_id) override {
  }
  void on_upload_ok(FileId file_id, tl_object_ptr<telegram_api::InputFile> input_file) override {
    send_closure_later(G()->messages_manager(), &MessagesManager::on_upload_media, file_id, std::move(input_file),
                       nullptr);
  }
  void on_upload_encrypted_ok(FileId file_id, tl_object_ptr<telegram_api::InputEncryptedFile> input_file) override {
    send_closure_later(G()->messages_manager(), &MessagesManager::on_upload_media, file_id, nullptr,
                       std::move(input_file));
  }
  void on_upload_secure_ok(FileId file_id, tl_object_ptr<telegram_api::InputSecureFile> input_file) override {
    UNREACHABLE();
  }
  void on_upload_error(FileId file_id, Status error) override {
    send_closure_later(G()->messages_manager(), &MessagesManager::on_upload_media_error, file_id, std::move(error));
  }
};

class MessagesManager::UploadThumbnailCallback : public FileManager::UploadCallback {
 public:
  void on_upload_ok(FileId file_id, tl_object_ptr<telegram_api::InputFile> input_file) override {
    send_closure_later(G()->messages_manager(), &MessagesManager::on_upload_thumbnail, file_id, std::move(input_file));
  }
  void on_upload_encrypted_ok(FileId file_id, tl_object_ptr<telegram_api::InputEncryptedFile> input_file) override {
    UNREACHABLE();
  }
  void on_upload_secure_ok(FileId file_id, tl_object_ptr<telegram_api::InputSecureFile> input_file) override {
    UNREACHABLE();
  }
  void on_upload_error(FileId file_id, Status error) override {
    send_closure_later(G()->messages_manager(), &MessagesManager::on_upload_thumbnail, file_id, nullptr);
  }
};

class MessagesManager::UploadDialogPhotoCallback : public FileManager::UploadCallback {
 public:
  void on_upload_ok(FileId file_id, tl_object_ptr<telegram_api::InputFile> input_file) override {
    send_closure_later(G()->messages_manager(), &MessagesManager::on_upload_dialog_photo, file_id,
                       std::move(input_file));
  }
  void on_upload_encrypted_ok(FileId file_id, tl_object_ptr<telegram_api::InputEncryptedFile> input_file) override {
    UNREACHABLE();
  }
  void on_upload_secure_ok(FileId file_id, tl_object_ptr<telegram_api::InputSecureFile> input_file) override {
    UNREACHABLE();
  }
  void on_upload_error(FileId file_id, Status error) override {
    send_closure_later(G()->messages_manager(), &MessagesManager::on_upload_dialog_photo_error, file_id,
                       std::move(error));
  }
};

template <class StorerT>
void MessagesManager::Message::store(StorerT &storer) const {
  using td::store;
  bool has_sender = sender_user_id.is_valid();
  bool has_edit_date = edit_date > 0;
  bool has_random_id = random_id != 0;
  bool is_forwarded = forward_info != nullptr;
  bool is_reply = reply_to_message_id.is_valid();
  bool is_reply_to_random_id = reply_to_random_id != 0;
  bool is_via_bot = via_bot_user_id.is_valid();
  bool has_views = views > 0;
  bool has_reply_markup = reply_markup != nullptr;
  bool has_ttl = ttl != 0;
  bool has_author_signature = !author_signature.empty();
  bool has_forward_author_signature = is_forwarded && !forward_info->author_signature.empty();
  bool has_media_album_id = media_album_id != 0;
  bool has_forward_from =
      is_forwarded && (forward_info->from_dialog_id.is_valid() || forward_info->from_message_id.is_valid());
  bool has_send_date = message_id.is_yet_unsent() && send_date != 0;
  bool has_flags2 = true;
  bool has_notification_id = notification_id.is_valid();
  BEGIN_STORE_FLAGS();
  STORE_FLAG(is_channel_post);
  STORE_FLAG(is_outgoing);
  STORE_FLAG(is_failed_to_send);
  STORE_FLAG(disable_notification);
  STORE_FLAG(contains_mention);
  STORE_FLAG(from_background);
  STORE_FLAG(disable_web_page_preview);
  STORE_FLAG(clear_draft);
  STORE_FLAG(have_previous);
  STORE_FLAG(have_next);
  STORE_FLAG(has_sender);
  STORE_FLAG(has_edit_date);
  STORE_FLAG(has_random_id);
  STORE_FLAG(is_forwarded);
  STORE_FLAG(is_reply);
  STORE_FLAG(is_reply_to_random_id);
  STORE_FLAG(is_via_bot);
  STORE_FLAG(has_views);
  STORE_FLAG(has_reply_markup);
  STORE_FLAG(has_ttl);
  STORE_FLAG(has_author_signature);
  STORE_FLAG(has_forward_author_signature);
  STORE_FLAG(had_reply_markup);
  STORE_FLAG(contains_unread_mention);
  STORE_FLAG(has_media_album_id);
  STORE_FLAG(has_forward_from);
  STORE_FLAG(in_game_share);
  STORE_FLAG(is_content_secret);
  STORE_FLAG(has_send_date);
  STORE_FLAG(has_flags2);
  END_STORE_FLAGS();
  if (has_flags2) {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(has_notification_id);
    END_STORE_FLAGS();
  }

  store(message_id, storer);
  if (has_sender) {
    store(sender_user_id, storer);
  }
  store(date, storer);
  if (has_edit_date) {
    store(edit_date, storer);
  }
  if (has_send_date) {
    store(send_date, storer);
  }
  if (has_random_id) {
    store(random_id, storer);
  }
  if (is_forwarded) {
    store(forward_info->sender_user_id, storer);
    store(forward_info->date, storer);
    store(forward_info->dialog_id, storer);
    store(forward_info->message_id, storer);
    if (has_forward_author_signature) {
      store(forward_info->author_signature, storer);
    }
    if (has_forward_from) {
      store(forward_info->from_dialog_id, storer);
      store(forward_info->from_message_id, storer);
    }
  }
  if (is_reply) {
    store(reply_to_message_id, storer);
  }
  if (is_reply_to_random_id) {
    store(reply_to_random_id, storer);
  }
  if (is_via_bot) {
    store(via_bot_user_id, storer);
  }
  if (has_views) {
    store(views, storer);
  }
  if (has_ttl) {
    store(ttl, storer);
    double server_time = storer.context()->server_time();
    if (ttl_expires_at == 0) {
      store(-1.0, storer);
    } else {
      double ttl_left = max(ttl_expires_at - Time::now_cached(), 0.0);
      store(ttl_left, storer);
      store(server_time, storer);
    }
  }
  if (has_author_signature) {
    store(author_signature, storer);
  }
  if (has_media_album_id) {
    store(media_album_id, storer);
  }
  if (has_notification_id) {
    store(notification_id, storer);
  }
  store_message_content(content.get(), storer);
  if (has_reply_markup) {
    store(*reply_markup, storer);
  }
}

// do not forget to resolve message dependencies
template <class ParserT>
void MessagesManager::Message::parse(ParserT &parser) {
  using td::parse;
  bool has_sender;
  bool has_edit_date;
  bool has_random_id;
  bool is_forwarded;
  bool is_reply;
  bool is_reply_to_random_id;
  bool is_via_bot;
  bool has_views;
  bool has_reply_markup;
  bool has_ttl;
  bool has_author_signature;
  bool has_forward_author_signature;
  bool has_media_album_id;
  bool has_forward_from;
  bool has_send_date;
  bool has_flags2;
  bool has_notification_id = false;
  BEGIN_PARSE_FLAGS();
  PARSE_FLAG(is_channel_post);
  PARSE_FLAG(is_outgoing);
  PARSE_FLAG(is_failed_to_send);
  PARSE_FLAG(disable_notification);
  PARSE_FLAG(contains_mention);
  PARSE_FLAG(from_background);
  PARSE_FLAG(disable_web_page_preview);
  PARSE_FLAG(clear_draft);
  PARSE_FLAG(have_previous);
  PARSE_FLAG(have_next);
  PARSE_FLAG(has_sender);
  PARSE_FLAG(has_edit_date);
  PARSE_FLAG(has_random_id);
  PARSE_FLAG(is_forwarded);
  PARSE_FLAG(is_reply);
  PARSE_FLAG(is_reply_to_random_id);
  PARSE_FLAG(is_via_bot);
  PARSE_FLAG(has_views);
  PARSE_FLAG(has_reply_markup);
  PARSE_FLAG(has_ttl);
  PARSE_FLAG(has_author_signature);
  PARSE_FLAG(has_forward_author_signature);
  PARSE_FLAG(had_reply_markup);
  PARSE_FLAG(contains_unread_mention);
  PARSE_FLAG(has_media_album_id);
  PARSE_FLAG(has_forward_from);
  PARSE_FLAG(in_game_share);
  PARSE_FLAG(is_content_secret);
  PARSE_FLAG(has_send_date);
  PARSE_FLAG(has_flags2);
  END_PARSE_FLAGS();
  if (has_flags2) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(has_notification_id);
    END_PARSE_FLAGS();
  }

  parse(message_id, parser);
  random_y = get_random_y(message_id);
  if (has_sender) {
    parse(sender_user_id, parser);
  }
  parse(date, parser);
  if (has_edit_date) {
    parse(edit_date, parser);
  }
  if (has_send_date) {
    CHECK(message_id.is_yet_unsent());
    parse(send_date, parser);
  } else if (message_id.is_valid() && message_id.is_yet_unsent()) {
    send_date = date;  // for backward compatibility
  }
  if (has_random_id) {
    parse(random_id, parser);
  }
  if (is_forwarded) {
    forward_info = make_unique<MessageForwardInfo>();
    parse(forward_info->sender_user_id, parser);
    parse(forward_info->date, parser);
    parse(forward_info->dialog_id, parser);
    parse(forward_info->message_id, parser);
    if (has_forward_author_signature) {
      parse(forward_info->author_signature, parser);
    }
    if (has_forward_from) {
      parse(forward_info->from_dialog_id, parser);
      parse(forward_info->from_message_id, parser);
    }
  }
  if (is_reply) {
    parse(reply_to_message_id, parser);
  }
  if (is_reply_to_random_id) {
    parse(reply_to_random_id, parser);
  }
  if (is_via_bot) {
    parse(via_bot_user_id, parser);
  }
  if (has_views) {
    parse(views, parser);
  }
  if (has_ttl) {
    parse(ttl, parser);
    double ttl_left;
    parse(ttl_left, parser);
    if (ttl_left < -0.1) {
      ttl_expires_at = 0;
    } else {
      double old_server_time;
      parse(old_server_time, parser);
      double passed_server_time = max(parser.context()->server_time() - old_server_time, 0.0);
      ttl_left = max(ttl_left - passed_server_time, 0.0);
      ttl_expires_at = Time::now_cached() + ttl_left;
    }
  }
  if (has_author_signature) {
    parse(author_signature, parser);
  }
  if (has_media_album_id) {
    parse(media_album_id, parser);
  }
  if (has_notification_id) {
    parse(notification_id, parser);
  }
  parse_message_content(content, parser);
  if (has_reply_markup) {
    reply_markup = make_unique<ReplyMarkup>();
    parse(*reply_markup, parser);
  }
  is_content_secret |=
      is_secret_message_content(ttl, content->get_type());  // repair is_content_secret for old messages
}

template <class StorerT>
void MessagesManager::NotificationGroupInfo::store(StorerT &storer) const {
  using td::store;
  store(group_id, storer);
  store(last_notification_date, storer);
  store(last_notification_id, storer);
  store(max_removed_notification_id, storer);
}

template <class ParserT>
void MessagesManager::NotificationGroupInfo::parse(ParserT &parser) {
  using td::parse;
  parse(group_id, parser);
  parse(last_notification_date, parser);
  parse(last_notification_id, parser);
  parse(max_removed_notification_id, parser);
}

template <class StorerT>
void MessagesManager::Dialog::store(StorerT &storer) const {
  using td::store;
  const Message *last_database_message = nullptr;
  if (last_database_message_id.is_valid()) {
    last_database_message = get_message(this, last_database_message_id);
  }

  bool has_draft_message = draft_message != nullptr;
  bool has_last_database_message = last_database_message != nullptr;
  bool has_first_database_message_id = first_database_message_id.is_valid();
  bool is_pinned = pinned_order != DEFAULT_ORDER;
  bool has_first_database_message_id_by_index = true;
  bool has_message_count_by_index = true;
  bool has_client_data = !client_data.empty();
  bool has_last_read_all_mentions_message_id = last_read_all_mentions_message_id.is_valid();
  bool has_max_unavailable_message_id = max_unavailable_message_id.is_valid();
  bool has_local_unread_count = local_unread_count != 0;
  bool has_deleted_last_message = delete_last_message_date > 0;
  bool has_last_clear_history_message_id = last_clear_history_message_id.is_valid();
  bool has_last_database_message_id = !has_last_database_message && last_database_message_id.is_valid();
  bool has_message_notification_group = message_notification_group.group_id.is_valid();
  bool has_mention_notification_group = mention_notification_group.group_id.is_valid();
  bool has_new_secret_chat_notification_id = new_secret_chat_notification_id.is_valid();
  bool has_pinned_message_notification = pinned_message_notification_message_id.is_valid();
  BEGIN_STORE_FLAGS();
  STORE_FLAG(has_draft_message);
  STORE_FLAG(has_last_database_message);
  STORE_FLAG(know_can_report_spam);
  STORE_FLAG(can_report_spam);
  STORE_FLAG(has_first_database_message_id);
  STORE_FLAG(is_pinned);
  STORE_FLAG(has_first_database_message_id_by_index);
  STORE_FLAG(has_message_count_by_index);
  STORE_FLAG(has_client_data);
  STORE_FLAG(need_restore_reply_markup);
  STORE_FLAG(have_full_history);
  STORE_FLAG(has_last_read_all_mentions_message_id);
  STORE_FLAG(has_max_unavailable_message_id);
  STORE_FLAG(is_last_read_inbox_message_id_inited);
  STORE_FLAG(is_last_read_outbox_message_id_inited);
  STORE_FLAG(has_local_unread_count);
  STORE_FLAG(has_deleted_last_message);
  STORE_FLAG(has_last_clear_history_message_id);
  STORE_FLAG(is_last_message_deleted_locally);
  STORE_FLAG(has_contact_registered_message);
  STORE_FLAG(has_last_database_message_id);
  STORE_FLAG(need_repair_server_unread_count);
  STORE_FLAG(is_marked_as_unread);
  STORE_FLAG(has_message_notification_group);
  STORE_FLAG(has_mention_notification_group);
  STORE_FLAG(has_new_secret_chat_notification_id);
  STORE_FLAG(has_pinned_message_notification);  // 26
  //
  //
  //STORE_FLAG(has_flags2);
  END_STORE_FLAGS();

  store(dialog_id, storer);  // must be stored at offset 4
  store(last_new_message_id, storer);
  store(server_unread_count, storer);
  if (has_local_unread_count) {
    store(local_unread_count, storer);
  }
  store(last_read_inbox_message_id, storer);
  store(last_read_outbox_message_id, storer);
  store(reply_markup_message_id, storer);
  store(notification_settings, storer);
  if (has_draft_message) {
    store(*draft_message, storer);
  }
  store(last_clear_history_date, storer);
  store(order, storer);
  if (has_last_database_message) {
    store(*last_database_message, storer);
  }
  if (has_first_database_message_id) {
    store(first_database_message_id, storer);
  }
  if (is_pinned) {
    store(pinned_order, storer);
  }
  if (has_deleted_last_message) {
    store(delete_last_message_date, storer);
    store(deleted_last_message_id, storer);
  }
  if (has_last_clear_history_message_id) {
    store(last_clear_history_message_id, storer);
  }

  if (has_first_database_message_id_by_index) {
    store(static_cast<int32>(first_database_message_id_by_index.size()), storer);
    for (auto first_message_id : first_database_message_id_by_index) {
      store(first_message_id, storer);
    }
  }
  if (has_message_count_by_index) {
    store(static_cast<int32>(message_count_by_index.size()), storer);
    for (auto message_count : message_count_by_index) {
      store(message_count, storer);
    }
  }
  if (has_client_data) {
    store(client_data, storer);
  }
  if (has_last_read_all_mentions_message_id) {
    store(last_read_all_mentions_message_id, storer);
  }
  if (has_max_unavailable_message_id) {
    store(max_unavailable_message_id, storer);
  }
  if (has_last_database_message_id) {
    store(last_database_message_id, storer);
  }
  if (has_message_notification_group) {
    store(message_notification_group, storer);
  }
  if (has_mention_notification_group) {
    store(mention_notification_group, storer);
  }
  if (has_new_secret_chat_notification_id) {
    store(new_secret_chat_notification_id, storer);
  }
  if (has_pinned_message_notification) {
    store(pinned_message_notification_message_id, storer);
  }
}

// do not forget to resolve dialog dependencies including dependencies of last_message
template <class ParserT>
void MessagesManager::Dialog::parse(ParserT &parser) {
  using td::parse;
  bool has_draft_message;
  bool has_last_database_message;
  bool has_first_database_message_id;
  bool is_pinned;
  bool has_first_database_message_id_by_index;
  bool has_message_count_by_index;
  bool has_client_data;
  bool has_last_read_all_mentions_message_id;
  bool has_max_unavailable_message_id;
  bool has_local_unread_count;
  bool has_deleted_last_message;
  bool has_last_clear_history_message_id;
  bool has_last_database_message_id;
  bool has_message_notification_group;
  bool has_mention_notification_group;
  bool has_new_secret_chat_notification_id;
  bool has_pinned_message_notification;
  BEGIN_PARSE_FLAGS();
  PARSE_FLAG(has_draft_message);
  PARSE_FLAG(has_last_database_message);
  PARSE_FLAG(know_can_report_spam);
  PARSE_FLAG(can_report_spam);
  PARSE_FLAG(has_first_database_message_id);
  PARSE_FLAG(is_pinned);
  PARSE_FLAG(has_first_database_message_id_by_index);
  PARSE_FLAG(has_message_count_by_index);
  PARSE_FLAG(has_client_data);
  PARSE_FLAG(need_restore_reply_markup);
  PARSE_FLAG(have_full_history);
  PARSE_FLAG(has_last_read_all_mentions_message_id);
  PARSE_FLAG(has_max_unavailable_message_id);
  PARSE_FLAG(is_last_read_inbox_message_id_inited);
  PARSE_FLAG(is_last_read_outbox_message_id_inited);
  PARSE_FLAG(has_local_unread_count);
  PARSE_FLAG(has_deleted_last_message);
  PARSE_FLAG(has_last_clear_history_message_id);
  PARSE_FLAG(is_last_message_deleted_locally);
  PARSE_FLAG(has_contact_registered_message);
  PARSE_FLAG(has_last_database_message_id);
  PARSE_FLAG(need_repair_server_unread_count);
  PARSE_FLAG(is_marked_as_unread);
  PARSE_FLAG(has_message_notification_group);
  PARSE_FLAG(has_mention_notification_group);
  PARSE_FLAG(has_new_secret_chat_notification_id);
  PARSE_FLAG(has_pinned_message_notification);
  END_PARSE_FLAGS();

  parse(dialog_id, parser);  // must be stored at offset 4
  parse(last_new_message_id, parser);
  parse(server_unread_count, parser);
  if (has_local_unread_count) {
    parse(local_unread_count, parser);
  }
  parse(last_read_inbox_message_id, parser);
  if (last_read_inbox_message_id.is_valid()) {
    is_last_read_inbox_message_id_inited = true;
  }
  parse(last_read_outbox_message_id, parser);
  if (last_read_outbox_message_id.is_valid()) {
    is_last_read_outbox_message_id_inited = true;
  }
  parse(reply_markup_message_id, parser);
  parse(notification_settings, parser);
  if (has_draft_message) {
    draft_message = make_unique<DraftMessage>();
    parse(*draft_message, parser);
  }
  parse(last_clear_history_date, parser);
  parse(order, parser);
  if (has_last_database_message) {
    messages = make_unique<Message>();
    parse(*messages, parser);
  }
  if (has_first_database_message_id) {
    parse(first_database_message_id, parser);
  }
  if (is_pinned) {
    parse(pinned_order, parser);
  }
  if (has_deleted_last_message) {
    parse(delete_last_message_date, parser);
    parse(deleted_last_message_id, parser);
  }
  if (has_last_clear_history_message_id) {
    parse(last_clear_history_message_id, parser);
  }

  if (has_first_database_message_id_by_index) {
    int32 size;
    parse(size, parser);
    if (size < 0) {
      // the logevent is broken
      // it should be impossible, but has happenned at least once
      parser.set_error("Wrong first_database_message_id_by_index table size");
      return;
    }
    CHECK(static_cast<size_t>(size) <= first_database_message_id_by_index.size())
        << size << " " << first_database_message_id_by_index.size();
    for (int32 i = 0; i < size; i++) {
      parse(first_database_message_id_by_index[i], parser);
    }
  }
  if (has_message_count_by_index) {
    int32 size;
    parse(size, parser);
    CHECK(static_cast<size_t>(size) <= message_count_by_index.size());
    for (int32 i = 0; i < size; i++) {
      parse(message_count_by_index[i], parser);
    }
  }
  unread_mention_count = message_count_by_index[search_messages_filter_index(SearchMessagesFilter::UnreadMention)];
  LOG(INFO) << "Set unread mention message count in " << dialog_id << " to " << unread_mention_count;
  if (unread_mention_count < 0) {
    unread_mention_count = 0;
  }
  if (has_client_data) {
    parse(client_data, parser);
  }
  if (has_last_read_all_mentions_message_id) {
    parse(last_read_all_mentions_message_id, parser);
  }
  if (has_max_unavailable_message_id) {
    parse(max_unavailable_message_id, parser);
  }
  if (has_last_database_message_id) {
    parse(last_database_message_id, parser);
  }
  if (has_message_notification_group) {
    parse(message_notification_group, parser);
  }
  if (has_mention_notification_group) {
    parse(mention_notification_group, parser);
  }
  if (has_new_secret_chat_notification_id) {
    parse(new_secret_chat_notification_id, parser);
  }
  if (has_pinned_message_notification) {
    parse(pinned_message_notification_message_id, parser);
  }
}

template <class StorerT>
void MessagesManager::CallsDbState::store(StorerT &storer) const {
  using td::store;
  store(static_cast<int32>(first_calls_database_message_id_by_index.size()), storer);
  for (auto first_message_id : first_calls_database_message_id_by_index) {
    store(first_message_id, storer);
  }
  store(static_cast<int32>(message_count_by_index.size()), storer);
  for (auto message_count : message_count_by_index) {
    store(message_count, storer);
  }
}

template <class ParserT>
void MessagesManager::CallsDbState::parse(ParserT &parser) {
  using td::parse;
  int32 size;
  parse(size, parser);
  CHECK(static_cast<size_t>(size) <= first_calls_database_message_id_by_index.size())
      << size << " " << first_calls_database_message_id_by_index.size();
  for (int32 i = 0; i < size; i++) {
    parse(first_calls_database_message_id_by_index[i], parser);
  }
  parse(size, parser);
  CHECK(static_cast<size_t>(size) <= message_count_by_index.size());
  for (int32 i = 0; i < size; i++) {
    parse(message_count_by_index[i], parser);
  }
}

void MessagesManager::load_calls_db_state() {
  if (!G()->parameters().use_message_db) {
    return;
  }
  std::fill(calls_db_state_.message_count_by_index.begin(), calls_db_state_.message_count_by_index.end(), -1);
  auto value = G()->td_db()->get_sqlite_sync_pmc()->get("calls_db_state");
  if (value.empty()) {
    return;
  }
  log_event_parse(calls_db_state_, value).ensure();
  LOG(INFO) << "Save calls database state " << calls_db_state_.first_calls_database_message_id_by_index[0] << " ("
            << calls_db_state_.message_count_by_index[0] << ") "
            << calls_db_state_.first_calls_database_message_id_by_index[1] << " ("
            << calls_db_state_.message_count_by_index[1] << ")";
}

void MessagesManager::save_calls_db_state() {
  if (!G()->parameters().use_message_db) {
    return;
  }

  LOG(INFO) << "Save calls database state " << calls_db_state_.first_calls_database_message_id_by_index[0] << " ("
            << calls_db_state_.message_count_by_index[0] << ") "
            << calls_db_state_.first_calls_database_message_id_by_index[1] << " ("
            << calls_db_state_.message_count_by_index[1] << ")";
  G()->td_db()->get_sqlite_pmc()->set("calls_db_state", log_event_store(calls_db_state_).as_slice().str(), Auto());
}

MessagesManager::Dialog::~Dialog() {
  if (!G()->close_flag()) {
    LOG(ERROR) << "Destroy " << dialog_id;
  }
}

MessagesManager::MessagesManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) {
  upload_media_callback_ = std::make_shared<UploadMediaCallback>();
  upload_thumbnail_callback_ = std::make_shared<UploadThumbnailCallback>();
  upload_dialog_photo_callback_ = std::make_shared<UploadDialogPhotoCallback>();

  channel_get_difference_timeout_.set_callback(on_channel_get_difference_timeout_callback);
  channel_get_difference_timeout_.set_callback_data(static_cast<void *>(this));

  channel_get_difference_retry_timeout_.set_callback(on_channel_get_difference_timeout_callback);
  channel_get_difference_retry_timeout_.set_callback_data(static_cast<void *>(this));

  pending_message_views_timeout_.set_callback(on_pending_message_views_timeout_callback);
  pending_message_views_timeout_.set_callback_data(static_cast<void *>(this));

  pending_draft_message_timeout_.set_callback(on_pending_draft_message_timeout_callback);
  pending_draft_message_timeout_.set_callback_data(static_cast<void *>(this));

  pending_read_history_timeout_.set_callback(on_pending_read_history_timeout_callback);
  pending_read_history_timeout_.set_callback_data(static_cast<void *>(this));

  pending_updated_dialog_timeout_.set_callback(on_pending_updated_dialog_timeout_callback);
  pending_updated_dialog_timeout_.set_callback_data(static_cast<void *>(this));

  pending_unload_dialog_timeout_.set_callback(on_pending_unload_dialog_timeout_callback);
  pending_unload_dialog_timeout_.set_callback_data(static_cast<void *>(this));

  dialog_unmute_timeout_.set_callback(on_dialog_unmute_timeout_callback);
  dialog_unmute_timeout_.set_callback_data(static_cast<void *>(this));

  pending_send_dialog_action_timeout_.set_callback(on_pending_send_dialog_action_timeout_callback);
  pending_send_dialog_action_timeout_.set_callback_data(static_cast<void *>(this));

  active_dialog_action_timeout_.set_callback(on_active_dialog_action_timeout_callback);
  active_dialog_action_timeout_.set_callback_data(static_cast<void *>(this));

  sequence_dispatcher_ = create_actor<MultiSequenceDispatcher>("multi sequence dispatcher");
}

MessagesManager::~MessagesManager() = default;

void MessagesManager::on_channel_get_difference_timeout_callback(void *messages_manager_ptr, int64 dialog_id_int) {
  if (G()->close_flag()) {
    return;
  }

  auto messages_manager = static_cast<MessagesManager *>(messages_manager_ptr);
  DialogId dialog_id(dialog_id_int);
  CHECK(dialog_id.get_type() == DialogType::Channel);
  auto d = messages_manager->get_dialog(dialog_id);
  CHECK(d != nullptr);
  messages_manager->get_channel_difference(dialog_id, d->pts, true, "on_channel_get_difference_timeout_callback");
}

void MessagesManager::on_pending_message_views_timeout_callback(void *messages_manager_ptr, int64 dialog_id_int) {
  if (G()->close_flag()) {
    return;
  }

  auto messages_manager = static_cast<MessagesManager *>(messages_manager_ptr);
  DialogId dialog_id(dialog_id_int);
  auto d = messages_manager->get_dialog(dialog_id);
  CHECK(d != nullptr);
  CHECK(!d->pending_viewed_message_ids.empty());

  const size_t MAX_MESSAGE_VIEWS = 100;  // server side limit
  vector<MessageId> message_ids;
  message_ids.reserve(min(d->pending_viewed_message_ids.size(), MAX_MESSAGE_VIEWS));
  for (auto message_id : d->pending_viewed_message_ids) {
    message_ids.push_back(message_id);
    if (message_ids.size() >= MAX_MESSAGE_VIEWS) {
      messages_manager->td_->create_handler<GetMessagesViewsQuery>()->send(dialog_id, std::move(message_ids),
                                                                           d->increment_view_counter);
      message_ids.clear();
    }
  }
  if (!message_ids.empty()) {
    messages_manager->td_->create_handler<GetMessagesViewsQuery>()->send(dialog_id, std::move(message_ids),
                                                                         d->increment_view_counter);
  }
  d->pending_viewed_message_ids.clear();
  d->increment_view_counter = false;
}

void MessagesManager::on_pending_draft_message_timeout_callback(void *messages_manager_ptr, int64 dialog_id_int) {
  if (G()->close_flag()) {
    return;
  }

  auto messages_manager = static_cast<MessagesManager *>(messages_manager_ptr);
  DialogId dialog_id(dialog_id_int);
  messages_manager->save_dialog_draft_message_on_server(dialog_id);
}

void MessagesManager::on_pending_read_history_timeout_callback(void *messages_manager_ptr, int64 dialog_id_int) {
  if (G()->close_flag()) {
    return;
  }

  auto messages_manager = static_cast<MessagesManager *>(messages_manager_ptr);
  DialogId dialog_id(dialog_id_int);
  messages_manager->read_history_on_server_impl(dialog_id, MessageId());
}

void MessagesManager::on_pending_updated_dialog_timeout_callback(void *messages_manager_ptr, int64 dialog_id_int) {
  auto messages_manager = static_cast<MessagesManager *>(messages_manager_ptr);
  // TODO it is unsafe to save dialog to database before binlog is flushed
  messages_manager->save_dialog_to_database(DialogId(dialog_id_int));
}

void MessagesManager::on_pending_unload_dialog_timeout_callback(void *messages_manager_ptr, int64 dialog_id_int) {
  if (G()->close_flag()) {
    return;
  }

  auto messages_manager = static_cast<MessagesManager *>(messages_manager_ptr);
  messages_manager->unload_dialog(DialogId(dialog_id_int));
}

void MessagesManager::on_dialog_unmute_timeout_callback(void *messages_manager_ptr, int64 dialog_id_int) {
  if (G()->close_flag()) {
    return;
  }

  auto messages_manager = static_cast<MessagesManager *>(messages_manager_ptr);
  if (1 <= dialog_id_int && dialog_id_int <= 2) {
    send_closure_later(messages_manager->actor_id(messages_manager), &MessagesManager::on_scope_unmute,
                       static_cast<NotificationSettingsScope>(dialog_id_int - 1));
  } else {
    send_closure_later(messages_manager->actor_id(messages_manager), &MessagesManager::on_dialog_unmute,
                       DialogId(dialog_id_int));
  }
}

void MessagesManager::on_pending_send_dialog_action_timeout_callback(void *messages_manager_ptr, int64 dialog_id_int) {
  if (G()->close_flag()) {
    return;
  }

  auto messages_manager = static_cast<MessagesManager *>(messages_manager_ptr);
  send_closure_later(messages_manager->actor_id(messages_manager), &MessagesManager::on_send_dialog_action_timeout,
                     DialogId(dialog_id_int));
}

void MessagesManager::on_active_dialog_action_timeout_callback(void *messages_manager_ptr, int64 dialog_id_int) {
  if (G()->close_flag()) {
    return;
  }

  auto messages_manager = static_cast<MessagesManager *>(messages_manager_ptr);
  send_closure_later(messages_manager->actor_id(messages_manager), &MessagesManager::on_active_dialog_action_timeout,
                     DialogId(dialog_id_int));
}

BufferSlice MessagesManager::get_dialog_database_value(const Dialog *d) {
  // can't use log_event_store, because it tries to parse stored Dialog
  LogEventStorerCalcLength storer_calc_length;
  store(*d, storer_calc_length);

  BufferSlice value_buffer{storer_calc_length.get_length()};
  auto value = value_buffer.as_slice();

  LogEventStorerUnsafe storer_unsafe(value.ubegin());
  store(*d, storer_unsafe);
  return value_buffer;
}

void MessagesManager::save_dialog_to_database(DialogId dialog_id) {
  CHECK(G()->parameters().use_message_db);
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  LOG(INFO) << "Save " << dialog_id << " to database";
  vector<NotificationGroupKey> changed_group_keys;
  if (d->message_notification_group.is_changed) {
    changed_group_keys.emplace_back(d->message_notification_group.group_id, dialog_id,
                                    d->message_notification_group.last_notification_date);
    d->message_notification_group.is_changed = false;
  }
  if (d->mention_notification_group.is_changed) {
    changed_group_keys.emplace_back(d->mention_notification_group.group_id, dialog_id,
                                    d->mention_notification_group.last_notification_date);
    d->mention_notification_group.is_changed = false;
  }
  G()->td_db()->get_dialog_db_async()->add_dialog(
      dialog_id, d->order, get_dialog_database_value(d), std::move(changed_group_keys),
      PromiseCreator::lambda([dialog_id](Result<> result) {
        send_closure(G()->messages_manager(), &MessagesManager::on_save_dialog_to_database, dialog_id, result.is_ok());
      }));
}

void MessagesManager::on_save_dialog_to_database(DialogId dialog_id, bool success) {
  LOG(INFO) << "Successfully saved " << dialog_id << " to database";

  if (success) {
    auto d = get_dialog(dialog_id);
    CHECK(d != nullptr);
    try_reuse_notification_group(d->message_notification_group);
    try_reuse_notification_group(d->mention_notification_group);
  }

  // TODO erase some events from binlog
}

void MessagesManager::try_reuse_notification_group(NotificationGroupInfo &group_info) {
  if (!group_info.try_reuse) {
    return;
  }
  group_info.try_reuse = false;
  if (group_info.is_changed) {
    LOG(ERROR) << "Failed to reuse changed " << group_info.group_id;
    return;
  }
  if (!group_info.group_id.is_valid()) {
    LOG(ERROR) << "Failed to reuse invalid " << group_info.group_id;
    return;
  }
  CHECK(group_info.last_notification_id == NotificationId());
  CHECK(group_info.last_notification_date == 0);
  send_closure_later(G()->notification_manager(), &NotificationManager::try_reuse_notification_group_id,
                     group_info.group_id);
  notification_group_id_to_dialog_id_.erase(group_info.group_id);
  group_info.group_id = NotificationGroupId();
  group_info.max_removed_notification_id = NotificationId();
}

void MessagesManager::update_message_count_by_index(Dialog *d, int diff, const Message *m) {
  auto index_mask = get_message_index_mask(d->dialog_id, m);
  index_mask &= ~search_messages_filter_index_mask(
      SearchMessagesFilter::UnreadMention);  // unread mention count has been already manually updated

  update_message_count_by_index(d, diff, index_mask);
}

void MessagesManager::update_message_count_by_index(Dialog *d, int diff, int32 index_mask) {
  if (index_mask == 0) {
    return;
  }

  int i = 0;
  for (auto &message_count : d->message_count_by_index) {
    if (((index_mask >> i) & 1) != 0 && message_count != -1) {
      message_count += diff;
      if (message_count < 0) {
        if (d->dialog_id.get_type() == DialogType::SecretChat) {
          message_count = 0;
        } else {
          message_count = -1;
        }
      }
      on_dialog_updated(d->dialog_id, "update_message_count_by_index");
    }
    i++;
  }

  i = static_cast<int>(SearchMessagesFilter::Call) - 1;
  for (auto &message_count : calls_db_state_.message_count_by_index) {
    if (((index_mask >> i) & 1) != 0 && message_count != -1) {
      message_count += diff;
      if (message_count < 0) {
        if (d->dialog_id.get_type() == DialogType::SecretChat) {
          message_count = 0;
        } else {
          message_count = -1;
        }
      }
      save_calls_db_state();
    }
    i++;
  }
}

int32 MessagesManager::get_message_index_mask(DialogId dialog_id, const Message *m) const {
  CHECK(m != nullptr);
  if (m->message_id.is_yet_unsent() || m->is_failed_to_send) {
    return 0;
  }
  bool is_secret = dialog_id.get_type() == DialogType::SecretChat;
  if (!m->message_id.is_server() && !is_secret) {
    return 0;
  }
  // retain second condition just in case
  if (m->is_content_secret || (m->ttl > 0 && !is_secret)) {
    return 0;
  }
  int32 mentions_mask = get_message_content_index_mask(m->content.get(), td_, is_secret, m->is_outgoing);
  if (m->contains_mention) {
    mentions_mask |= search_messages_filter_index_mask(SearchMessagesFilter::Mention);
    if (m->contains_unread_mention) {
      mentions_mask |= search_messages_filter_index_mask(SearchMessagesFilter::UnreadMention);
    }
  }
  return mentions_mask;
}

vector<MessageId> MessagesManager::get_message_ids(const vector<int64> &input_message_ids) {
  vector<MessageId> message_ids;
  message_ids.reserve(input_message_ids.size());
  for (auto &input_message_id : input_message_ids) {
    message_ids.push_back(MessageId(input_message_id));
  }
  return message_ids;
}

vector<int32> MessagesManager::get_server_message_ids(const vector<MessageId> &message_ids) {
  return transform(message_ids, [](MessageId message_id) { return message_id.get_server_message_id().get(); });
}

tl_object_ptr<telegram_api::InputMessage> MessagesManager::get_input_message(MessageId message_id) {
  return make_tl_object<telegram_api::inputMessageID>(message_id.get_server_message_id().get());
}

tl_object_ptr<telegram_api::InputPeer> MessagesManager::get_input_peer(DialogId dialog_id,
                                                                       AccessRights access_rights) const {
  switch (dialog_id.get_type()) {
    case DialogType::User: {
      UserId user_id = dialog_id.get_user_id();
      return td_->contacts_manager_->get_input_peer_user(user_id, access_rights);
    }
    case DialogType::Chat: {
      ChatId chat_id = dialog_id.get_chat_id();
      return td_->contacts_manager_->get_input_peer_chat(chat_id, access_rights);
    }
    case DialogType::Channel: {
      ChannelId channel_id = dialog_id.get_channel_id();
      return td_->contacts_manager_->get_input_peer_channel(channel_id, access_rights);
    }
    case DialogType::SecretChat:
      return nullptr;
    case DialogType::None:
      return make_tl_object<telegram_api::inputPeerEmpty>();
    default:
      UNREACHABLE();
      return nullptr;
  }
}

vector<tl_object_ptr<telegram_api::InputPeer>> MessagesManager::get_input_peers(const vector<DialogId> &dialog_ids,
                                                                                AccessRights access_rights) const {
  vector<tl_object_ptr<telegram_api::InputPeer>> input_peers;
  input_peers.reserve(dialog_ids.size());
  for (auto &dialog_id : dialog_ids) {
    auto input_peer = get_input_peer(dialog_id, access_rights);
    if (input_peer == nullptr) {
      LOG(ERROR) << "Have no access to " << dialog_id;
      continue;
    }
    input_peers.push_back(std::move(input_peer));
  }
  return input_peers;
}

tl_object_ptr<telegram_api::inputDialogPeer> MessagesManager::get_input_dialog_peer(DialogId dialog_id,
                                                                                    AccessRights access_rights) const {
  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
    case DialogType::None:
      return make_tl_object<telegram_api::inputDialogPeer>(get_input_peer(dialog_id, access_rights));
    case DialogType::SecretChat:
      return nullptr;
    default:
      UNREACHABLE();
      return nullptr;
  }
}

vector<tl_object_ptr<telegram_api::inputDialogPeer>> MessagesManager::get_input_dialog_peers(
    const vector<DialogId> &dialog_ids, AccessRights access_rights) const {
  vector<tl_object_ptr<telegram_api::inputDialogPeer>> input_dialog_peers;
  input_dialog_peers.reserve(dialog_ids.size());
  for (auto &dialog_id : dialog_ids) {
    auto input_dialog_peer = get_input_dialog_peer(dialog_id, access_rights);
    if (input_dialog_peer == nullptr) {
      LOG(ERROR) << "Have no access to " << dialog_id;
      continue;
    }
    input_dialog_peers.push_back(std::move(input_dialog_peer));
  }
  return input_dialog_peers;
}

bool MessagesManager::have_input_peer(DialogId dialog_id, AccessRights access_rights) const {
  switch (dialog_id.get_type()) {
    case DialogType::User: {
      UserId user_id = dialog_id.get_user_id();
      return td_->contacts_manager_->have_input_peer_user(user_id, access_rights);
    }
    case DialogType::Chat: {
      ChatId chat_id = dialog_id.get_chat_id();
      return td_->contacts_manager_->have_input_peer_chat(chat_id, access_rights);
    }
    case DialogType::Channel: {
      ChannelId channel_id = dialog_id.get_channel_id();
      return td_->contacts_manager_->have_input_peer_channel(channel_id, access_rights);
    }
    case DialogType::SecretChat: {
      SecretChatId secret_chat_id = dialog_id.get_secret_chat_id();
      return td_->contacts_manager_->have_input_encrypted_peer(secret_chat_id, access_rights);
    }
    case DialogType::None:
      return false;
    default:
      UNREACHABLE();
      return false;
  }
}

bool MessagesManager::have_dialog_info(DialogId dialog_id) const {
  switch (dialog_id.get_type()) {
    case DialogType::User: {
      UserId user_id = dialog_id.get_user_id();
      return td_->contacts_manager_->have_user(user_id);
    }
    case DialogType::Chat: {
      ChatId chat_id = dialog_id.get_chat_id();
      return td_->contacts_manager_->have_chat(chat_id);
    }
    case DialogType::Channel: {
      ChannelId channel_id = dialog_id.get_channel_id();
      return td_->contacts_manager_->have_channel(channel_id);
    }
    case DialogType::SecretChat: {
      SecretChatId secret_chat_id = dialog_id.get_secret_chat_id();
      return td_->contacts_manager_->have_secret_chat(secret_chat_id);
    }
    case DialogType::None:
    default:
      return false;
  }
}

bool MessagesManager::have_dialog_info_force(DialogId dialog_id) const {
  switch (dialog_id.get_type()) {
    case DialogType::User: {
      UserId user_id = dialog_id.get_user_id();
      return td_->contacts_manager_->have_user_force(user_id);
    }
    case DialogType::Chat: {
      ChatId chat_id = dialog_id.get_chat_id();
      return td_->contacts_manager_->have_chat_force(chat_id);
    }
    case DialogType::Channel: {
      ChannelId channel_id = dialog_id.get_channel_id();
      return td_->contacts_manager_->have_channel_force(channel_id);
    }
    case DialogType::SecretChat: {
      SecretChatId secret_chat_id = dialog_id.get_secret_chat_id();
      return td_->contacts_manager_->have_secret_chat_force(secret_chat_id);
    }
    case DialogType::None:
    default:
      return false;
  }
}

tl_object_ptr<telegram_api::inputEncryptedChat> MessagesManager::get_input_encrypted_chat(
    DialogId dialog_id, AccessRights access_rights) const {
  switch (dialog_id.get_type()) {
    case DialogType::SecretChat: {
      SecretChatId secret_chat_id = dialog_id.get_secret_chat_id();
      return td_->contacts_manager_->get_input_encrypted_chat(secret_chat_id, access_rights);
    }
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
    case DialogType::None:
    default:
      UNREACHABLE();
      return nullptr;
  }
}

bool MessagesManager::is_allowed_useless_update(const tl_object_ptr<telegram_api::Update> &update) const {
  auto constructor_id = update->get_id();
  if (constructor_id == dummyUpdate::ID) {
    // allow dummyUpdate just in case
    return true;
  }
  if (constructor_id == telegram_api::updateNewMessage::ID) {
    auto message = static_cast<const telegram_api::updateNewMessage *>(update.get())->message_.get();
    if (message->get_id() == telegram_api::message::ID) {
      auto m = static_cast<const telegram_api::message *>(message);
      bool is_outgoing =
          (m->flags_ & MESSAGE_FLAG_IS_OUT) != 0 || UserId(m->from_id_) == td_->contacts_manager_->get_my_id();
      if (is_outgoing && m->media_ != nullptr && m->media_->get_id() != telegram_api::messageMediaEmpty::ID) {
        // allow outgoing media, because they are returned if random_id coincided
        return true;
      }
    }
    if (message->get_id() == telegram_api::messageService::ID) {
      auto m = static_cast<const telegram_api::messageService *>(message);
      bool is_outgoing =
          (m->flags_ & MESSAGE_FLAG_IS_OUT) != 0 || UserId(m->from_id_) == td_->contacts_manager_->get_my_id();
      if (is_outgoing && m->action_->get_id() == telegram_api::messageActionScreenshotTaken::ID) {
        // allow outgoing messageActionScreenshotTaken, because they are returned if random_id coincided
        return true;
      }
    }
  }

  return false;
}

bool MessagesManager::check_update_dialog_id(const tl_object_ptr<telegram_api::Update> &update, DialogId dialog_id) {
  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
      return true;
    case DialogType::Channel:
    case DialogType::SecretChat:
    case DialogType::None:
      LOG(ERROR) << "Receive update in wrong " << dialog_id << ": " << oneline(to_string(update));
      return false;
    default:
      UNREACHABLE();
      return false;
  }
}

void MessagesManager::skip_old_pending_update(tl_object_ptr<telegram_api::Update> &&update, int32 new_pts,
                                              int32 old_pts, int32 pts_count, const char *source) {
  if (update->get_id() == telegram_api::updateNewMessage::ID) {
    auto update_new_message = static_cast<telegram_api::updateNewMessage *>(update.get());
    auto full_message_id = get_full_message_id(update_new_message->message_);
    if (update_message_ids_.find(full_message_id) != update_message_ids_.end()) {
      if (new_pts == old_pts) {  // otherwise message can be already deleted
        // apply sent message anyway
        on_get_message(std::move(update_new_message->message_), true, false, true, true,
                       "updateNewMessage with an awaited message");
        return;
      } else {
        LOG(ERROR) << "Receive awaited sent " << full_message_id << " from " << source << " with pts " << new_pts
                   << " and pts_count " << pts_count << ", but current pts is " << old_pts;
        dump_debug_message_op(get_dialog(full_message_id.get_dialog_id()), 3);
      }
    }
  }
  if (update->get_id() == updateSentMessage::ID) {
    auto update_sent_message = static_cast<updateSentMessage *>(update.get());
    if (being_sent_messages_.count(update_sent_message->random_id_) > 0) {
      if (new_pts == old_pts) {  // otherwise message can be already deleted
        // apply sent message anyway
        on_send_message_success(update_sent_message->random_id_, update_sent_message->message_id_,
                                update_sent_message->date_, FileId(), "process old updateSentMessage");
        return;
      } else {
        LOG(ERROR) << "Receive awaited sent " << update_sent_message->message_id_ << " from " << source << " with pts "
                   << new_pts << " and pts_count " << pts_count << ", but current pts is " << old_pts;
        dump_debug_message_op(get_dialog(being_sent_messages_[update_sent_message->random_id_].get_dialog_id()), 3);
      }
    }
    return;
  }

  // very old or unuseful update
  LOG_IF(WARNING, new_pts == old_pts && pts_count == 0 && !is_allowed_useless_update(update))
      << "Receive useless update " << oneline(to_string(update)) << " from " << source;
}

void MessagesManager::add_pending_update(tl_object_ptr<telegram_api::Update> &&update, int32 new_pts, int32 pts_count,
                                         bool force_apply, const char *source) {
  // do not try to run getDifference from this function
  CHECK(update != nullptr);
  CHECK(source != nullptr);
  LOG(INFO) << "Receive from " << source << " pending " << to_string(update) << "new_pts = " << new_pts
            << ", pts_count = " << pts_count << ", force_apply = " << force_apply;
  if (pts_count < 0 || new_pts <= pts_count) {
    LOG(ERROR) << "Receive update with wrong pts = " << new_pts << " or pts_count = " << pts_count << " from " << source
               << ": " << oneline(to_string(update));
    return;
  }

  // TODO need to save all updates that can change result of running queries not associated with pts (for example
  // getHistory) and apply them to result of this queries

  switch (update->get_id()) {
    case dummyUpdate::ID:
    case updateSentMessage::ID:
    case telegram_api::updateReadMessagesContents::ID:
    case telegram_api::updateDeleteMessages::ID:
      // nothing to check
      break;
    case telegram_api::updateNewMessage::ID: {
      auto update_new_message = static_cast<const telegram_api::updateNewMessage *>(update.get());
      DialogId dialog_id = get_message_dialog_id(update_new_message->message_);
      if (!check_update_dialog_id(update, dialog_id)) {
        return;
      }
      break;
    }
    case telegram_api::updateReadHistoryInbox::ID: {
      auto update_read_history_inbox = static_cast<const telegram_api::updateReadHistoryInbox *>(update.get());
      auto dialog_id = DialogId(update_read_history_inbox->peer_);
      if (!check_update_dialog_id(update, dialog_id)) {
        return;
      }
      break;
    }
    case telegram_api::updateReadHistoryOutbox::ID: {
      auto update_read_history_outbox = static_cast<const telegram_api::updateReadHistoryOutbox *>(update.get());
      auto dialog_id = DialogId(update_read_history_outbox->peer_);
      if (!check_update_dialog_id(update, dialog_id)) {
        return;
      }
      break;
    }
    case telegram_api::updateEditMessage::ID: {
      auto update_edit_message = static_cast<const telegram_api::updateEditMessage *>(update.get());
      DialogId dialog_id = get_message_dialog_id(update_edit_message->message_);
      if (!check_update_dialog_id(update, dialog_id)) {
        return;
      }
      break;
    }
    default:
      LOG(ERROR) << "Receive unexpected update " << oneline(to_string(update)) << "from " << source;
      return;
  }

  if (force_apply) {
    CHECK(pending_updates_.empty());
    CHECK(accumulated_pts_ == -1);
    if (pts_count != 0) {
      LOG(ERROR) << "Receive forced update with pts_count = " << pts_count << " from " << source;
    }

    process_update(std::move(update));
    return;
  }
  if (DROP_UPDATES) {
    return set_get_difference_timeout(1.0);
  }

  int32 old_pts = td_->updates_manager_->get_pts();
  if (new_pts < old_pts - 19999) {
    // restore pts after delete_first_messages
    LOG(ERROR) << "Restore pts after delete_first_messages from " << old_pts << " to " << new_pts
               << " is temporarily disabled, pts_count = " << pts_count << ", update is from " << source << ": "
               << oneline(to_string(update));
    if (old_pts < 10000000 && update->get_id() == telegram_api::updateNewMessage::ID) {
      auto update_new_message = static_cast<telegram_api::updateNewMessage *>(update.get());
      auto dialog_id = get_message_dialog_id(update_new_message->message_);
      dump_debug_message_op(get_dialog(dialog_id), 6);
    }
    set_get_difference_timeout(0.001);

    /*
    LOG(WARNING) << "Restore pts after delete_first_messages";
    td_->updates_manager_->set_pts(new_pts - 1, "restore pts after delete_first_messages");
    old_pts = td_->updates_manager_->get_pts();
    CHECK(old_pts == new_pts - 1);
    */
  }

  if (new_pts <= old_pts) {
    skip_old_pending_update(std::move(update), new_pts, old_pts, pts_count, source);
    return;
  }

  if (td_->updates_manager_->running_get_difference()) {
    LOG(INFO) << "Save pending update got while running getDifference from " << source;
    CHECK(update->get_id() == dummyUpdate::ID || update->get_id() == updateSentMessage::ID);
    if (pts_count > 0) {
      postponed_pts_updates_.emplace(new_pts, PendingPtsUpdate(std::move(update), new_pts, pts_count));
    }
    return;
  }

  if (old_pts + pts_count > new_pts) {
    LOG(WARNING) << "old_pts (= " << old_pts << ") + pts_count (= " << pts_count << ") > new_pts (= " << new_pts
                 << "). Logged in " << G()->shared_config().get_option_integer("authorization_date") << ". Update from "
                 << source << " = " << oneline(to_string(update));
    set_get_difference_timeout(0.001);
    return;
  }

  accumulated_pts_count_ += pts_count;
  if (new_pts > accumulated_pts_) {
    accumulated_pts_ = new_pts;
  }

  if (old_pts + accumulated_pts_count_ > accumulated_pts_) {
    LOG(WARNING) << "old_pts (= " << old_pts << ") + accumulated_pts_count (= " << accumulated_pts_count_
                 << ") > accumulated_pts (= " << accumulated_pts_ << "). new_pts = " << new_pts
                 << ", pts_count = " << pts_count << ". Logged in "
                 << G()->shared_config().get_option_integer("authorization_date") << ". Update from " << source << " = "
                 << oneline(to_string(update));
    set_get_difference_timeout(0.001);
    return;
  }

  LOG_IF(INFO, pts_count == 0 && update->get_id() != dummyUpdate::ID) << "Skip useless update " << to_string(update);

  if (pending_updates_.empty() && old_pts + accumulated_pts_count_ == accumulated_pts_ &&
      !pts_gap_timeout_.has_timeout()) {
    if (pts_count > 0) {
      process_update(std::move(update));

      td_->updates_manager_->set_pts(accumulated_pts_, "process pending updates fast path")
          .set_value(Unit());  // TODO can't set until get messages really stored on persistent storage
      accumulated_pts_count_ = 0;
      accumulated_pts_ = -1;
    }
    return;
  }

  if (pts_count > 0) {
    pending_updates_.emplace(new_pts, PendingPtsUpdate(std::move(update), new_pts, pts_count));
  }

  if (old_pts + accumulated_pts_count_ < accumulated_pts_) {
    set_get_difference_timeout(UpdatesManager::MAX_UNFILLED_GAP_TIME);
    return;
  }

  CHECK(old_pts + accumulated_pts_count_ == accumulated_pts_);
  if (!pending_updates_.empty()) {
    process_pending_updates();
  }
}

MessagesManager::Dialog *MessagesManager::get_service_notifications_dialog() {
  UserId service_notifications_user_id = td_->contacts_manager_->get_service_notifications_user_id();
  DialogId service_notifications_dialog_id(service_notifications_user_id);
  force_create_dialog(service_notifications_dialog_id, "get_service_notifications_dialog");
  return get_dialog(service_notifications_dialog_id);
}

void MessagesManager::on_update_service_notification(tl_object_ptr<telegram_api::updateServiceNotification> &&update) {
  int32 ttl = 0;
  auto content = get_message_content(
      td_,
      get_message_text(td_->contacts_manager_.get(), std::move(update->message_), std::move(update->entities_),
                       update->inbox_date_, "on_update_service_notification"),
      std::move(update->media_),
      td_->auth_manager_->is_bot() ? DialogId() : get_service_notifications_dialog()->dialog_id, false, UserId(), &ttl);
  bool is_content_secret = is_secret_message_content(ttl, content->get_type());
  if ((update->flags_ & telegram_api::updateServiceNotification::POPUP_MASK) != 0) {
    send_closure(
        G()->td(), &Td::send_update,
        make_tl_object<td_api::updateServiceNotification>(
            update->type_, get_message_content_object(content.get(), td_, update->inbox_date_, is_content_secret)));
  }
  if ((update->flags_ & telegram_api::updateServiceNotification::INBOX_DATE_MASK) != 0 &&
      !td_->auth_manager_->is_bot()) {
    Dialog *d = get_service_notifications_dialog();
    CHECK(d != nullptr);
    auto dialog_id = d->dialog_id;
    CHECK(dialog_id.get_type() == DialogType::User);
    auto new_message = make_unique<Message>();
    new_message->message_id = get_next_local_message_id(d);
    new_message->random_y = get_random_y(new_message->message_id);
    new_message->sender_user_id = dialog_id.get_user_id();
    new_message->date = update->inbox_date_;
    new_message->ttl = ttl;
    new_message->is_content_secret = is_content_secret;
    new_message->content = std::move(content);
    new_message->have_previous = true;
    new_message->have_next = true;

    bool need_update = true;
    bool need_update_dialog_pos = false;

    const Message *m = add_message_to_dialog(d, std::move(new_message), true, &need_update, &need_update_dialog_pos,
                                             "on_update_service_notification");
    if (m != nullptr && need_update) {
      send_update_new_message(d, m);
    }

    if (need_update_dialog_pos) {
      send_update_chat_last_message(d, "on_update_service_notification");
    }
  }
}

void MessagesManager::on_update_contact_registered(tl_object_ptr<telegram_api::updateContactRegistered> &&update) {
  if (update->date_ <= 0) {
    LOG(ERROR) << "Receive wrong date " << update->date_ << " in updateContactRegistered";
    return;
  }
  UserId user_id(update->user_id_);
  if (!user_id.is_valid()) {
    LOG(ERROR) << "Receive invalid " << user_id << " in updateContactRegistered";
    return;
  }

  if (!td_->auth_manager_->is_bot() &&
      !G()->shared_config().get_option_boolean("disable_contact_registered_notifications")) {
    DialogId dialog_id(user_id);
    force_create_dialog(dialog_id, "on_update_contact_registered");
    Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr);

    if (d->has_contact_registered_message) {
      LOG(INFO) << "Ignore duplicate updateContactRegistered about " << user_id;
      return;
    }
    if (d->last_message_id.is_valid()) {
      auto m = get_message(d, d->last_message_id);
      CHECK(m != nullptr);
      if (m->content->get_type() == MessageContentType::ContactRegistered) {
        LOG(INFO) << "Ignore duplicate updateContactRegistered about " << user_id;
        return;
      }
    }

    auto new_message = make_unique<Message>();
    new_message->message_id = get_next_local_message_id(d);
    new_message->random_y = get_random_y(new_message->message_id);
    new_message->sender_user_id = user_id;
    new_message->date = update->date_;
    new_message->content = create_contact_registered_message_content();
    new_message->have_previous = true;
    new_message->have_next = true;

    bool need_update = true;
    bool need_update_dialog_pos = false;

    const Message *m = add_message_to_dialog(d, std::move(new_message), true, &need_update, &need_update_dialog_pos,
                                             "on_update_contact_registered");
    if (m != nullptr && need_update) {
      send_update_new_message(d, m);
    }

    if (need_update_dialog_pos) {
      send_update_chat_last_message(d, "on_update_contact_registered");
    }
  }
}

void MessagesManager::on_update_new_channel_message(tl_object_ptr<telegram_api::updateNewChannelMessage> &&update) {
  int new_pts = update->pts_;
  int pts_count = update->pts_count_;
  DialogId dialog_id = get_message_dialog_id(update->message_);
  switch (dialog_id.get_type()) {
    case DialogType::None:
      return;
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::SecretChat:
      LOG(ERROR) << "Receive updateNewChannelMessage in wrong " << dialog_id;
      return;
    case DialogType::Channel: {
      auto channel_id = dialog_id.get_channel_id();
      if (!td_->contacts_manager_->have_channel(channel_id)) {
        // if min channel was received
        if (td_->contacts_manager_->have_min_channel(channel_id)) {
          td_->updates_manager_->schedule_get_difference("on_update_new_channel_message");
          return;
        }
      }
      // Ok
      break;
    }
    default:
      UNREACHABLE();
      return;
  }

  if (pts_count < 0 || new_pts <= pts_count) {
    LOG(ERROR) << "Receive new channel message with wrong pts = " << new_pts << " or pts_count = " << pts_count << ": "
               << oneline(to_string(update));
    return;
  }

  add_pending_channel_update(dialog_id, std::move(update), new_pts, pts_count, "on_update_new_channel_message");
}

void MessagesManager::on_update_edit_channel_message(tl_object_ptr<telegram_api::updateEditChannelMessage> &&update) {
  int new_pts = update->pts_;
  int pts_count = update->pts_count_;
  DialogId dialog_id = get_message_dialog_id(update->message_);
  switch (dialog_id.get_type()) {
    case DialogType::None:
      return;
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::SecretChat:
      LOG(ERROR) << "Receive updateNewChannelMessage in wrong " << dialog_id;
      return;
    case DialogType::Channel: {
      auto channel_id = dialog_id.get_channel_id();
      if (!td_->contacts_manager_->have_channel(channel_id)) {
        // if min channel was received
        if (td_->contacts_manager_->have_min_channel(channel_id)) {
          td_->updates_manager_->schedule_get_difference("on_update_edit_channel_message");
          return;
        }
      }
      // Ok
      break;
    }
    default:
      UNREACHABLE();
      return;
  }

  if (pts_count < 0 || new_pts <= pts_count) {
    LOG(ERROR) << "Receive edited channel message with wrong pts = " << new_pts << " or pts_count = " << pts_count
               << ": " << oneline(to_string(update));
    return;
  }

  add_pending_channel_update(dialog_id, std::move(update), new_pts, pts_count, "on_update_edit_channel_message");
}

void MessagesManager::on_update_read_channel_inbox(tl_object_ptr<telegram_api::updateReadChannelInbox> &&update) {
  ChannelId channel_id(update->channel_id_);
  if (!channel_id.is_valid()) {
    LOG(ERROR) << "Receive invalid " << channel_id << " in updateReadChannelInbox";
    return;
  }

  DialogId dialog_id = DialogId(channel_id);
  read_history_inbox(dialog_id, MessageId(ServerMessageId(update->max_id_)), -1, "updateReadChannelInbox");
}

void MessagesManager::on_update_read_channel_outbox(tl_object_ptr<telegram_api::updateReadChannelOutbox> &&update) {
  ChannelId channel_id(update->channel_id_);
  if (!channel_id.is_valid()) {
    LOG(ERROR) << "Receive invalid " << channel_id << " in updateReadChannelOutbox";
    return;
  }

  DialogId dialog_id = DialogId(channel_id);
  read_history_outbox(dialog_id, MessageId(ServerMessageId(update->max_id_)));
}

void MessagesManager::on_update_read_channel_messages_contents(
    tl_object_ptr<telegram_api::updateChannelReadMessagesContents> &&update) {
  ChannelId channel_id(update->channel_id_);
  if (!channel_id.is_valid()) {
    LOG(ERROR) << "Receive invalid " << channel_id << " in updateChannelReadMessagesContents";
    return;
  }

  DialogId dialog_id = DialogId(channel_id);

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(INFO) << "Receive read channel messages contents update in unknown " << dialog_id;
    return;
  }

  for (auto &server_message_id : update->messages_) {
    read_channel_message_content_from_updates(d, MessageId(ServerMessageId(server_message_id)));
  }
}

void MessagesManager::on_update_channel_too_long(tl_object_ptr<telegram_api::updateChannelTooLong> &&update,
                                                 bool force_apply) {
  ChannelId channel_id(update->channel_id_);
  if (!channel_id.is_valid()) {
    LOG(ERROR) << "Receive invalid " << channel_id << " in updateChannelTooLong";
    return;
  }

  DialogId dialog_id = DialogId(channel_id);
  auto d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    auto pts = load_channel_pts(dialog_id);
    if (pts > 0) {
      d = add_dialog(dialog_id);
      CHECK(d != nullptr);
      CHECK(d->pts == pts);
      update_dialog_pos(d, false, "on_update_channel_too_long");
    }
  }

  int32 update_pts = (update->flags_ & UPDATE_CHANNEL_TO_LONG_FLAG_HAS_PTS) ? update->pts_ : 0;

  if (d != nullptr) {
    if (update_pts == 0 || update_pts > d->pts) {
      get_channel_difference(dialog_id, d->pts, true, "on_update_channel_too_long 1");
    }
  } else {
    if (force_apply) {
      get_channel_difference(dialog_id, -1, true, "on_update_channel_too_long 2");
    } else {
      td_->updates_manager_->schedule_get_difference("on_update_channel_too_long 3");
    }
  }
}

void MessagesManager::on_update_message_views(FullMessageId full_message_id, int32 views) {
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(INFO) << "Ignore updateMessageViews in unknown " << dialog_id;
    return;
  }
  auto message_id = full_message_id.get_message_id();
  Message *m = get_message_force(d, message_id);
  if (m == nullptr) {
    LOG(INFO) << "Ignore updateMessageViews about unknown " << full_message_id;
    if (message_id.get() > d->last_new_message_id.get() && dialog_id.get_type() == DialogType::Channel) {
      get_channel_difference(dialog_id, d->pts, true, "on_update_message_views");
    }
    return;
  }

  if (update_message_views(full_message_id.get_dialog_id(), m, views)) {
    on_message_changed(d, m, true, "on_update_message_views");
  }
}

bool MessagesManager::update_message_views(DialogId dialog_id, Message *m, int32 views) {
  CHECK(m != nullptr);
  if (views > m->views) {
    m->views = views;
    send_closure(G()->td(), &Td::send_update,
                 make_tl_object<td_api::updateMessageViews>(dialog_id.get(), m->message_id.get(), m->views));
    return true;
  }
  return false;
}

bool MessagesManager::update_message_contains_unread_mention(Dialog *d, Message *m, bool contains_unread_mention,
                                                             const char *source) {
  CHECK(m != nullptr) << source;
  if (!contains_unread_mention && m->contains_unread_mention) {
    remove_message_notification_id(d, m, true);  // should be called before contains_unread_mention is updated

    m->contains_unread_mention = false;
    if (d->unread_mention_count == 0) {
      if (d->message_count_by_index[search_messages_filter_index(SearchMessagesFilter::UnreadMention)] != -1) {
        LOG(ERROR) << "Unread mention count of " << d->dialog_id << " became negative from " << source;
      }
    } else {
      d->unread_mention_count--;
      d->message_count_by_index[search_messages_filter_index(SearchMessagesFilter::UnreadMention)] =
          d->unread_mention_count;
      on_dialog_updated(d->dialog_id, "update_message_contains_unread_mention");
    }
    LOG(INFO) << "Update unread mention message count in " << d->dialog_id << " to " << d->unread_mention_count
              << " by reading " << m->message_id << " from " << source;

    send_closure(G()->td(), &Td::send_update,
                 make_tl_object<td_api::updateMessageMentionRead>(d->dialog_id.get(), m->message_id.get(),
                                                                  d->unread_mention_count));
    return true;
  }
  return false;
}

void MessagesManager::on_read_channel_inbox(ChannelId channel_id, MessageId max_message_id, int32 server_unread_count) {
  DialogId dialog_id(channel_id);
  if (max_message_id.is_valid() || server_unread_count > 0) {
    /*
    // dropping unread count can make things worse, so don't drop it
    if (server_unread_count > 0 && G()->parameters().use_message_db) {
      const Dialog *d = get_dialog_force(dialog_id);
      if (d == nullptr) {
        return;
      }

      if (d->is_last_read_inbox_message_id_inited) {
        server_unread_count = -1;
      }
    }
    */
    read_history_inbox(dialog_id, max_message_id, server_unread_count, "on_read_channel_inbox");
  }
}

void MessagesManager::on_read_channel_outbox(ChannelId channel_id, MessageId max_message_id) {
  DialogId dialog_id(channel_id);
  if (max_message_id.is_valid()) {
    read_history_outbox(dialog_id, max_message_id);
  }
}

void MessagesManager::on_update_channel_max_unavailable_message_id(ChannelId channel_id,
                                                                   MessageId max_unavailable_message_id) {
  if (!channel_id.is_valid()) {
    LOG(ERROR) << "Receive max_unavailable_message_id in invalid " << channel_id;
    return;
  }

  DialogId dialog_id(channel_id);
  if (!max_unavailable_message_id.is_valid() && max_unavailable_message_id != MessageId()) {
    LOG(ERROR) << "Receive wrong max_unavailable_message_id: " << max_unavailable_message_id;
    max_unavailable_message_id = MessageId();
  }
  set_dialog_max_unavailable_message_id(dialog_id, max_unavailable_message_id, true,
                                        "on_update_channel_max_unavailable_message_id");
}

void MessagesManager::on_update_include_sponsored_dialog_to_unread_count(bool include_sponsored_dialog) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }
  if (include_sponsored_dialog_to_unread_count_ == include_sponsored_dialog) {
    return;
  }
  if (sponsored_dialog_id_.is_valid()) {
    // preload sponsored dialog
    get_dialog_force(sponsored_dialog_id_);
  }

  include_sponsored_dialog_to_unread_count_ = include_sponsored_dialog;

  if (!sponsored_dialog_id_.is_valid()) {
    // nothing has changed
    return;
  }
  if (!G()->parameters().use_message_db) {
    // there is no support for unread count updates without message database
    return;
  }

  const Dialog *d = get_dialog(sponsored_dialog_id_);
  CHECK(d != nullptr);
  auto unread_count = d->server_unread_count + d->local_unread_count;
  if (unread_count != 0 && is_message_unread_count_inited_) {
    send_update_unread_message_count(d->dialog_id, true, "on_update_include_sponsored_dialog_to_unread_count");
  }
  if ((unread_count != 0 || d->is_marked_as_unread) && is_dialog_unread_count_inited_) {
    send_update_unread_chat_count(d->dialog_id, true, "on_update_include_sponsored_dialog_to_unread_count");
  }
}

bool MessagesManager::need_cancel_user_dialog_action(int32 action_id, MessageContentType message_content_type) {
  if (message_content_type == MessageContentType::None) {
    return true;
  }

  if (action_id == td_api::chatActionTyping::ID) {
    return message_content_type == MessageContentType::Text || message_content_type == MessageContentType::Game ||
           can_have_message_content_caption(message_content_type);
  }

  switch (message_content_type) {
    case MessageContentType::Animation:
    case MessageContentType::Audio:
    case MessageContentType::Document:
      return action_id == td_api::chatActionUploadingDocument::ID;
    case MessageContentType::ExpiredPhoto:
    case MessageContentType::Photo:
      return action_id == td_api::chatActionUploadingPhoto::ID;
    case MessageContentType::ExpiredVideo:
    case MessageContentType::Video:
      return action_id == td_api::chatActionRecordingVideo::ID || action_id == td_api::chatActionUploadingVideo::ID;
    case MessageContentType::VideoNote:
      return action_id == td_api::chatActionRecordingVideoNote::ID ||
             action_id == td_api::chatActionUploadingVideoNote::ID;
    case MessageContentType::VoiceNote:
      return action_id == td_api::chatActionRecordingVoiceNote::ID ||
             action_id == td_api::chatActionUploadingVoiceNote::ID;
    case MessageContentType::Contact:
      return action_id == td_api::chatActionChoosingContact::ID;
    case MessageContentType::LiveLocation:
    case MessageContentType::Location:
    case MessageContentType::Venue:
      return action_id == td_api::chatActionChoosingLocation::ID;
    case MessageContentType::Game:
    case MessageContentType::Invoice:
    case MessageContentType::Sticker:
    case MessageContentType::Text:
    case MessageContentType::Unsupported:
    case MessageContentType::ChatCreate:
    case MessageContentType::ChatChangeTitle:
    case MessageContentType::ChatChangePhoto:
    case MessageContentType::ChatDeletePhoto:
    case MessageContentType::ChatDeleteHistory:
    case MessageContentType::ChatAddUsers:
    case MessageContentType::ChatJoinedByLink:
    case MessageContentType::ChatDeleteUser:
    case MessageContentType::ChatMigrateTo:
    case MessageContentType::ChannelCreate:
    case MessageContentType::ChannelMigrateFrom:
    case MessageContentType::PinMessage:
    case MessageContentType::GameScore:
    case MessageContentType::ScreenshotTaken:
    case MessageContentType::ChatSetTtl:
    case MessageContentType::Call:
    case MessageContentType::PaymentSuccessful:
    case MessageContentType::ContactRegistered:
    case MessageContentType::CustomServiceAction:
    case MessageContentType::WebsiteConnected:
    case MessageContentType::PassportDataSent:
    case MessageContentType::PassportDataReceived:
      return false;
    default:
      UNREACHABLE();
      return false;
  }
}

void MessagesManager::on_user_dialog_action(DialogId dialog_id, UserId user_id,
                                            tl_object_ptr<td_api::ChatAction> &&action,
                                            MessageContentType message_content_type) {
  if (td_->auth_manager_->is_bot() || !user_id.is_valid() || is_broadcast_channel(dialog_id)) {
    return;
  }

  bool is_canceled = action == nullptr || action->get_id() == td_api::chatActionCancel::ID;
  if (is_canceled) {
    auto actions_it = active_dialog_actions_.find(dialog_id);
    if (actions_it == active_dialog_actions_.end()) {
      return;
    }

    auto &active_actions = actions_it->second;
    auto it = std::find_if(active_actions.begin(), active_actions.end(),
                           [user_id](const ActiveDialogAction &action) { return action.user_id == user_id; });
    if (it == active_actions.end()) {
      return;
    }

    if (!td_->contacts_manager_->is_user_bot(user_id) &&
        !need_cancel_user_dialog_action(it->action_id, message_content_type)) {
      return;
    }

    LOG(DEBUG) << "Cancel action of " << user_id << " in " << dialog_id;
    active_actions.erase(it);
    if (active_actions.empty()) {
      active_dialog_actions_.erase(dialog_id);
      LOG(DEBUG) << "Cancel action timeout in " << dialog_id;
      active_dialog_action_timeout_.cancel_timeout(dialog_id.get());
    }
    if (action == nullptr) {
      action = make_tl_object<td_api::chatActionCancel>();
    }
  } else {
    auto &active_actions = active_dialog_actions_[dialog_id];
    auto it = std::find_if(active_actions.begin(), active_actions.end(),
                           [user_id](const ActiveDialogAction &action) { return action.user_id == user_id; });
    int32 prev_action_id = 0;
    int32 prev_progress = 0;
    if (it != active_actions.end()) {
      LOG(DEBUG) << "Re-add action of " << user_id << " in " << dialog_id;
      prev_action_id = it->action_id;
      prev_progress = it->progress;
      active_actions.erase(it);
    } else {
      LOG(DEBUG) << "Add action of " << user_id << " in " << dialog_id;
    }

    auto action_id = action->get_id();
    auto progress = [&] {
      switch (action_id) {
        case td_api::chatActionUploadingVideo::ID:
          return static_cast<td_api::chatActionUploadingVideo &>(*action).progress_;
        case td_api::chatActionUploadingVoiceNote::ID:
          return static_cast<td_api::chatActionUploadingVoiceNote &>(*action).progress_;
        case td_api::chatActionUploadingPhoto::ID:
          return static_cast<td_api::chatActionUploadingPhoto &>(*action).progress_;
        case td_api::chatActionUploadingDocument::ID:
          return static_cast<td_api::chatActionUploadingDocument &>(*action).progress_;
        case td_api::chatActionUploadingVideoNote::ID:
          return static_cast<td_api::chatActionUploadingVideoNote &>(*action).progress_;
        default:
          return 0;
      }
    }();
    active_actions.emplace_back(user_id, action_id, Time::now());
    if (action_id == prev_action_id && progress <= prev_progress) {
      return;
    }
    if (active_actions.size() == 1u) {
      LOG(DEBUG) << "Set action timeout in " << dialog_id;
      active_dialog_action_timeout_.set_timeout_in(dialog_id.get(), DIALOG_ACTION_TIMEOUT);
    }
  }

  LOG(DEBUG) << "Send action of " << user_id << " in " << dialog_id << ": " << to_string(action);
  send_closure(G()->td(), &Td::send_update,
               make_tl_object<td_api::updateUserChatAction>(
                   dialog_id.get(), td_->contacts_manager_->get_user_id_object(user_id, "on_user_dialog_action"),
                   std::move(action)));
}

void MessagesManager::cancel_user_dialog_action(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  if (m->forward_info != nullptr || m->via_bot_user_id.is_valid() || m->is_channel_post) {
    return;
  }

  on_user_dialog_action(dialog_id, m->sender_user_id, nullptr, m->content->get_type());
}

void MessagesManager::add_pending_channel_update(DialogId dialog_id, tl_object_ptr<telegram_api::Update> &&update,
                                                 int32 new_pts, int32 pts_count, const char *source,
                                                 bool is_postponed_udpate) {
  LOG(INFO) << "Receive from " << source << " pending " << to_string(update);
  CHECK(update != nullptr);
  CHECK(dialog_id.get_type() == DialogType::Channel);
  if (pts_count < 0 || new_pts <= pts_count) {
    LOG(ERROR) << "Receive channel update from " << source << " with wrong pts = " << new_pts
               << " or pts_count = " << pts_count << ": " << oneline(to_string(update));
    return;
  }

  // TODO need to save all updates that can change result of running queries not associated with pts (for example
  // getHistory) and apply them to result of this queries

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    auto pts = load_channel_pts(dialog_id);
    if (pts > 0) {
      auto channel_id = dialog_id.get_channel_id();
      if (!td_->contacts_manager_->have_channel(channel_id)) {
        // do not create dialog if there is no info about the channel
        return;
      }

      d = add_dialog(dialog_id);
      CHECK(d != nullptr);
      CHECK(d->pts == pts);
      update_dialog_pos(d, false, "add_pending_channel_update");
    }
  }
  if (d == nullptr) {
    // if there is no dialog, it can be created by the update
    LOG(INFO) << "Receive pending update from " << source << " about unknown " << dialog_id;
    if (running_get_channel_difference(dialog_id)) {
      return;
    }
  } else {
    int32 old_pts = d->pts;
    if (new_pts <= old_pts) {  // very old or unuseful update
      if (new_pts < old_pts - 19999 && !is_postponed_udpate) {
        // restore channel pts after delete_first_messages
        LOG(ERROR) << "Restore pts in " << d->dialog_id << " from " << source << " after delete_first_messages from "
                   << old_pts << " to " << new_pts << " is temporarily disabled, pts_count = " << pts_count
                   << ", update is from " << source << ": " << oneline(to_string(update));
        if (old_pts < 10000000) {
          dump_debug_message_op(d, 6);
        }
        get_channel_difference(dialog_id, old_pts, true, "add_pending_channel_update old");
      }

      if (update->get_id() == telegram_api::updateNewChannelMessage::ID) {
        auto update_new_channel_message = static_cast<telegram_api::updateNewChannelMessage *>(update.get());
        auto message_id = get_message_id(update_new_channel_message->message_);
        FullMessageId full_message_id(dialog_id, message_id);
        if (update_message_ids_.find(full_message_id) != update_message_ids_.end()) {
          // apply sent channel message
          on_get_message(std::move(update_new_channel_message->message_), true, true, true, true,
                         "updateNewChannelMessage with an awaited message");
          return;
        }
      }
      if (update->get_id() == updateSentMessage::ID) {
        auto update_sent_message = static_cast<updateSentMessage *>(update.get());
        if (being_sent_messages_.count(update_sent_message->random_id_) > 0) {
          // apply sent channel message
          on_send_message_success(update_sent_message->random_id_, update_sent_message->message_id_,
                                  update_sent_message->date_, FileId(), "process old updateSentChannelMessage");
          return;
        }
      }

      LOG_IF(WARNING, new_pts == old_pts && pts_count == 0)
          << "Receive from " << source << " useless channel update " << oneline(to_string(update));
      return;
    }

    if (running_get_channel_difference(dialog_id)) {
      if (pts_count > 0) {
        d->postponed_channel_updates.emplace(new_pts, PendingPtsUpdate(std::move(update), new_pts, pts_count));
      }
      return;
    }

    if (old_pts + pts_count != new_pts) {
      LOG(INFO) << "Found a gap in the " << dialog_id << " with pts = " << old_pts << ". new_pts = " << new_pts
                << ", pts_count = " << pts_count << " in update from " << source;

      if (pts_count > 0) {
        d->postponed_channel_updates.emplace(new_pts, PendingPtsUpdate(std::move(update), new_pts, pts_count));
      }

      get_channel_difference(dialog_id, old_pts, true, "add_pending_channel_update pts mismatch");
      return;
    }
  }

  if (d == nullptr || pts_count > 0) {
    process_channel_update(std::move(update));
    CHECK(!running_get_channel_difference(dialog_id)) << '"' << active_get_channel_differencies_[dialog_id] << '"';
  } else {
    LOG_IF(INFO, update->get_id() != dummyUpdate::ID)
        << "Skip useless channel update from " << source << ": " << to_string(update);
  }

  if (d == nullptr) {
    d = get_dialog(dialog_id);
    if (d == nullptr) {
      // dialog was not created by the update
      return;
    }
  }

  CHECK(new_pts > d->pts);
  set_channel_pts(d, new_pts, source);
}

void MessagesManager::set_get_difference_timeout(double timeout) {
  if (!pts_gap_timeout_.has_timeout()) {
    LOG(INFO) << "Gap in pts has found, current pts is " << td_->updates_manager_->get_pts();
    pts_gap_timeout_.set_callback(std::move(UpdatesManager::fill_pts_gap));
    pts_gap_timeout_.set_callback_data(static_cast<void *>(td_));
    pts_gap_timeout_.set_timeout_in(timeout);
  }
}

void MessagesManager::process_update(tl_object_ptr<telegram_api::Update> &&update) {
  switch (update->get_id()) {
    case dummyUpdate::ID:
      LOG(INFO) << "Process dummyUpdate";
      break;
    case telegram_api::updateNewMessage::ID:
      LOG(INFO) << "Process updateNewMessage";
      on_get_message(std::move(move_tl_object_as<telegram_api::updateNewMessage>(update)->message_), true, false, true,
                     true, "updateNewMessage");
      break;
    case updateSentMessage::ID: {
      auto send_message_success_update = move_tl_object_as<updateSentMessage>(update);
      LOG(INFO) << "Process updateSentMessage " << send_message_success_update->random_id_;
      on_send_message_success(send_message_success_update->random_id_, send_message_success_update->message_id_,
                              send_message_success_update->date_, FileId(), "process updateSentMessage");
      break;
    }
    case telegram_api::updateReadMessagesContents::ID: {
      auto read_contents_update = move_tl_object_as<telegram_api::updateReadMessagesContents>(update);
      LOG(INFO) << "Process updateReadMessageContents";
      for (auto &message_id : read_contents_update->messages_) {
        read_message_content_from_updates(MessageId(ServerMessageId(message_id)));
      }
      break;
    }
    case telegram_api::updateEditMessage::ID: {
      auto full_message_id =
          on_get_message(std::move(move_tl_object_as<telegram_api::updateEditMessage>(update)->message_), false, false,
                         false, false, "updateEditMessage");
      LOG(INFO) << "Process updateEditMessage";
      on_message_edited(full_message_id);
      break;
    }
    case telegram_api::updateDeleteMessages::ID: {
      auto delete_update = move_tl_object_as<telegram_api::updateDeleteMessages>(update);
      LOG(INFO) << "Process updateDeleteMessages";
      vector<MessageId> message_ids;
      for (auto &message : delete_update->messages_) {
        message_ids.push_back(MessageId(ServerMessageId(message)));
      }
      delete_messages_from_updates(message_ids);
      break;
    }
    case telegram_api::updateReadHistoryInbox::ID: {
      auto read_update = move_tl_object_as<telegram_api::updateReadHistoryInbox>(update);
      LOG(INFO) << "Process updateReadHistoryInbox";
      read_history_inbox(DialogId(read_update->peer_), MessageId(ServerMessageId(read_update->max_id_)), -1,
                         "updateReadHistoryInbox");
      break;
    }
    case telegram_api::updateReadHistoryOutbox::ID: {
      auto read_update = move_tl_object_as<telegram_api::updateReadHistoryOutbox>(update);
      LOG(INFO) << "Process updateReadHistoryOutbox";
      read_history_outbox(DialogId(read_update->peer_), MessageId(ServerMessageId(read_update->max_id_)));
      break;
    }
    default:
      UNREACHABLE();
  }
  CHECK(!td_->updates_manager_->running_get_difference());
}

void MessagesManager::process_channel_update(tl_object_ptr<telegram_api::Update> &&update) {
  switch (update->get_id()) {
    case dummyUpdate::ID:
      LOG(INFO) << "Process dummyUpdate";
      break;
    case updateSentMessage::ID: {
      auto send_message_success_update = move_tl_object_as<updateSentMessage>(update);
      LOG(INFO) << "Process updateSentMessage " << send_message_success_update->random_id_;
      on_send_message_success(send_message_success_update->random_id_, send_message_success_update->message_id_,
                              send_message_success_update->date_, FileId(), "process updateSentChannelMessage");
      break;
    }
    case telegram_api::updateNewChannelMessage::ID:
      LOG(INFO) << "Process updateNewChannelMessage";
      on_get_message(std::move(move_tl_object_as<telegram_api::updateNewChannelMessage>(update)->message_), true, true,
                     true, true, "updateNewChannelMessage");
      break;
    case telegram_api::updateDeleteChannelMessages::ID: {
      auto delete_channel_messages_update = move_tl_object_as<telegram_api::updateDeleteChannelMessages>(update);
      LOG(INFO) << "Process updateDeleteChannelMessages";
      ChannelId channel_id(delete_channel_messages_update->channel_id_);
      if (!channel_id.is_valid()) {
        LOG(ERROR) << "Receive invalid " << channel_id;
        break;
      }

      vector<MessageId> message_ids;
      for (auto &message : delete_channel_messages_update->messages_) {
        message_ids.push_back(MessageId(ServerMessageId(message)));
      }

      auto dialog_id = DialogId(channel_id);
      delete_dialog_messages_from_updates(dialog_id, message_ids);
      break;
    }
    case telegram_api::updateEditChannelMessage::ID: {
      auto full_message_id =
          on_get_message(std::move(move_tl_object_as<telegram_api::updateEditChannelMessage>(update)->message_), false,
                         true, false, false, "updateEditChannelMessage");
      LOG(INFO) << "Process updateEditChannelMessage";
      on_message_edited(full_message_id);
      break;
    }
    default:
      UNREACHABLE();
  }
}

void MessagesManager::on_message_edited(FullMessageId full_message_id) {
  if (full_message_id == FullMessageId()) {
    return;
  }

  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog(dialog_id);
  const Message *m = get_message(d, full_message_id.get_message_id());
  CHECK(m != nullptr);
  if (td_->auth_manager_->is_bot()) {
    send_update_message_edited(dialog_id, m);
  } else {
    if (m->forward_info == nullptr && (m->is_outgoing || dialog_id == get_my_dialog_id())) {
      update_used_hashtags(dialog_id, m);
    }

    if (m->notification_id.is_valid() && is_message_notification_active(d, m)) {
      auto &group_info = get_notification_group_info(d, m);
      if (group_info.group_id.is_valid()) {
        send_closure_later(G()->notification_manager(), &NotificationManager::edit_notification, group_info.group_id,
                           m->notification_id, create_new_message_notification(m->message_id));
      }
    }
  }
}

void MessagesManager::process_pending_updates() {
  for (auto &update : pending_updates_) {
    process_update(std::move(update.second.update));
  }

  td_->updates_manager_->set_pts(accumulated_pts_, "process pending updates")
      .set_value(Unit());  // TODO can't set until get messages really stored on persistent storage
  drop_pending_updates();
}

void MessagesManager::drop_pending_updates() {
  accumulated_pts_count_ = 0;
  accumulated_pts_ = -1;
  pts_gap_timeout_.cancel_timeout();
  pending_updates_.clear();
}

string MessagesManager::get_notification_settings_scope_database_key(NotificationSettingsScope scope) {
  switch (scope) {
    case NotificationSettingsScope::Private:
      return "nsfpc";
    case NotificationSettingsScope::Group:
      return "nsfgc";
    default:
      UNREACHABLE();
      return "";
  }
}

void MessagesManager::save_scope_notification_settings(NotificationSettingsScope scope,
                                                       const ScopeNotificationSettings &new_settings) {
  string key = get_notification_settings_scope_database_key(scope);
  G()->td_db()->get_binlog_pmc()->set(key, log_event_store(new_settings).as_slice().str());
}

bool MessagesManager::update_dialog_notification_settings(DialogId dialog_id,
                                                          DialogNotificationSettings *current_settings,
                                                          const DialogNotificationSettings &new_settings) {
  bool need_update = current_settings->mute_until != new_settings.mute_until ||
                     current_settings->sound != new_settings.sound ||
                     current_settings->show_preview != new_settings.show_preview ||
                     current_settings->use_default_mute_until != new_settings.use_default_mute_until ||
                     current_settings->use_default_sound != new_settings.use_default_sound ||
                     current_settings->use_default_show_preview != new_settings.use_default_show_preview;
  bool need_update_default_disable_notification =
      current_settings->silent_send_message != new_settings.silent_send_message;
  bool is_changed = need_update || need_update_default_disable_notification ||
                    current_settings->is_synchronized != new_settings.is_synchronized ||
                    current_settings->is_use_default_fixed != new_settings.is_use_default_fixed;

  if (is_changed) {
    Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr) << "Wrong " << dialog_id << " in update_dialog_notification_settings";
    bool was_muted = is_dialog_muted(d);
    update_dialog_unmute_timeout(d, current_settings->use_default_mute_until, current_settings->mute_until,
                                 new_settings.use_default_mute_until, new_settings.mute_until);
    on_dialog_updated(dialog_id, "update_dialog_notification_settings");

    LOG(INFO) << "Update notification settings in " << dialog_id << " from " << *current_settings << " to "
              << new_settings;
    *current_settings = new_settings;

    if (!was_muted && is_dialog_muted(d)) {
      remove_all_dialog_notifications(dialog_id, d->message_notification_group, "save_scope_notification_settings");
    }

    if (need_update) {
      send_closure(G()->td(), &Td::send_update,
                   make_tl_object<td_api::updateChatNotificationSettings>(
                       dialog_id.get(), get_chat_notification_settings_object(current_settings)));
    }
    if (need_update_default_disable_notification) {
      send_closure(G()->td(), &Td::send_update,
                   make_tl_object<td_api::updateChatDefaultDisableNotification>(dialog_id.get(),
                                                                                current_settings->silent_send_message));
    }
  }
  return is_changed;
}

bool MessagesManager::update_scope_notification_settings(NotificationSettingsScope scope,
                                                         ScopeNotificationSettings *current_settings,
                                                         const ScopeNotificationSettings &new_settings) {
  bool need_update = current_settings->mute_until != new_settings.mute_until ||
                     current_settings->sound != new_settings.sound ||
                     current_settings->show_preview != new_settings.show_preview;
  bool is_changed = need_update || current_settings->is_synchronized != new_settings.is_synchronized;
  if (is_changed) {
    save_scope_notification_settings(scope, new_settings);

    update_scope_unmute_timeout(scope, current_settings->mute_until, new_settings.mute_until);

    LOG(INFO) << "Update notification settings in " << scope << " from " << *current_settings << " to " << new_settings;
    *current_settings = new_settings;

    if (need_update) {
      send_closure(G()->td(), &Td::send_update, get_update_scope_notification_settings_object(scope));
    }
  }
  return is_changed;
}

void MessagesManager::update_dialog_unmute_timeout(Dialog *d, bool old_use_default, int32 old_mute_until,
                                                   bool new_use_default, int32 new_mute_until) {
  if (old_use_default == new_use_default && old_mute_until == new_mute_until) {
    return;
  }
  CHECK(d != nullptr);

  auto now = G()->unix_time_cached();
  if (!new_use_default && new_mute_until >= now && new_mute_until < now + 366 * 86400) {
    dialog_unmute_timeout_.set_timeout_in(d->dialog_id.get(), new_mute_until - now + 1);
  } else {
    dialog_unmute_timeout_.cancel_timeout(d->dialog_id.get());
  }

  if (old_mute_until != -1 && need_unread_counter(d->order) &&
      (is_message_unread_count_inited_ || is_dialog_unread_count_inited_)) {
    auto unread_count = d->server_unread_count + d->local_unread_count;
    if (unread_count != 0 || d->is_marked_as_unread) {
      if (old_use_default || new_use_default) {
        auto scope_mute_until = get_scope_mute_until(d->dialog_id);
        if (old_use_default) {
          old_mute_until = scope_mute_until;
        }
        if (new_use_default) {
          new_mute_until = scope_mute_until;
        }
      }
      if ((old_mute_until != 0) != (new_mute_until != 0)) {
        if (unread_count != 0 && is_message_unread_count_inited_) {
          int32 delta = old_mute_until != 0 ? -unread_count : unread_count;
          unread_message_muted_count_ += delta;
          send_update_unread_message_count(d->dialog_id, true, "update_dialog_unmute_timeout");
        }
        if (is_dialog_unread_count_inited_) {
          int32 delta = old_mute_until != 0 ? -1 : 1;
          unread_dialog_muted_count_ += delta;
          if (unread_count == 0 && d->is_marked_as_unread) {
            unread_dialog_muted_marked_count_ += delta;
          }
          send_update_unread_chat_count(d->dialog_id, true, "update_dialog_unmute_timeout");
        }
      }
    }
  }
}

void MessagesManager::update_scope_unmute_timeout(NotificationSettingsScope scope, int32 old_mute_until,
                                                  int32 new_mute_until) {
  LOG(INFO) << "Update " << scope << " unmute timeout from " << old_mute_until << " to " << new_mute_until;
  if (old_mute_until == new_mute_until) {
    return;
  }

  auto now = G()->unix_time_cached();
  if (new_mute_until >= now && new_mute_until < now + 366 * 86400) {
    dialog_unmute_timeout_.set_timeout_in(static_cast<int64>(scope) + 1, new_mute_until - now + 1);
  } else {
    dialog_unmute_timeout_.cancel_timeout(static_cast<int64>(scope) + 1);
  }

  if (old_mute_until != -1 && (is_message_unread_count_inited_ || is_dialog_unread_count_inited_)) {
    auto was_muted = old_mute_until != 0;
    auto is_muted = new_mute_until != 0;
    if (was_muted != is_muted) {
      int32 delta = 0;
      int32 total_count = 0;
      int32 marked_count = 0;
      for (auto &dialog : dialogs_) {
        Dialog *d = dialog.second.get();
        if (need_unread_counter(d->order) && d->notification_settings.use_default_mute_until &&
            get_dialog_notification_setting_scope(d->dialog_id) == scope) {
          int32 unread_count = d->server_unread_count + d->local_unread_count;
          if (unread_count != 0) {
            delta += unread_count;
            total_count++;
          } else if (d->is_marked_as_unread) {
            total_count++;
            marked_count++;
          }
        }
      }
      if (delta != 0 && is_message_unread_count_inited_) {
        if (was_muted) {
          unread_message_muted_count_ -= delta;
        } else {
          unread_message_muted_count_ += delta;
        }
        send_update_unread_message_count(DialogId(), true, "update_scope_unmute_timeout");
      }
      if (total_count != 0 && is_dialog_unread_count_inited_) {
        if (was_muted) {
          unread_dialog_muted_count_ -= total_count;
          unread_dialog_muted_marked_count_ -= marked_count;
        } else {
          unread_dialog_muted_count_ += total_count;
          unread_dialog_muted_marked_count_ += marked_count;
        }
        send_update_unread_chat_count(DialogId(), true, "update_scope_unmute_timeout");
      }
    }
  }
}

void MessagesManager::on_dialog_unmute(DialogId dialog_id) {
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  if (d->notification_settings.use_default_mute_until) {
    return;
  }
  if (d->notification_settings.mute_until == 0) {
    return;
  }

  auto now = G()->unix_time();
  if (d->notification_settings.mute_until > now) {
    LOG(ERROR) << "Failed to unmute " << dialog_id << " in " << now << ", will be unmuted in "
               << d->notification_settings.mute_until;
    update_dialog_unmute_timeout(d, false, -1, false, d->notification_settings.mute_until);
    return;
  }

  LOG(INFO) << "Unmute " << dialog_id;
  update_dialog_unmute_timeout(d, false, d->notification_settings.mute_until, false, 0);
  d->notification_settings.mute_until = 0;
  send_closure(G()->td(), &Td::send_update,
               make_tl_object<td_api::updateChatNotificationSettings>(
                   dialog_id.get(), get_chat_notification_settings_object(&d->notification_settings)));
  on_dialog_updated(dialog_id, "on_dialog_unmute");
}

void MessagesManager::on_scope_unmute(NotificationSettingsScope scope) {
  auto notification_settings = get_scope_notification_settings(scope);
  CHECK(notification_settings != nullptr);

  if (notification_settings->mute_until == 0) {
    return;
  }

  auto now = G()->unix_time();
  if (notification_settings->mute_until > now) {
    LOG(ERROR) << "Failed to unmute " << scope << " in " << now << ", will be unmuted in "
               << notification_settings->mute_until;
    update_scope_unmute_timeout(scope, -1, notification_settings->mute_until);
    return;
  }

  LOG(INFO) << "Unmute " << scope;
  update_scope_unmute_timeout(scope, notification_settings->mute_until, 0);
  notification_settings->mute_until = 0;
  send_closure(G()->td(), &Td::send_update, get_update_scope_notification_settings_object(scope));
  save_scope_notification_settings(scope, *notification_settings);
}

void MessagesManager::on_update_dialog_notify_settings(
    DialogId dialog_id, tl_object_ptr<telegram_api::peerNotifySettings> &&peer_notify_settings) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  LOG(INFO) << "Receive notification settings for " << dialog_id << ": " << to_string(peer_notify_settings);

  const DialogNotificationSettings notification_settings =
      td::get_dialog_notification_settings(std::move(peer_notify_settings));
  if (!notification_settings.is_synchronized) {
    return;
  }

  DialogNotificationSettings *current_settings = get_dialog_notification_settings(dialog_id, true);
  if (current_settings == nullptr) {
    return;
  }
  update_dialog_notification_settings(dialog_id, current_settings, notification_settings);
}

void MessagesManager::on_update_scope_notify_settings(
    NotificationSettingsScope scope, tl_object_ptr<telegram_api::peerNotifySettings> &&peer_notify_settings) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  const ScopeNotificationSettings notification_settings =
      td::get_scope_notification_settings(std::move(peer_notify_settings));
  if (!notification_settings.is_synchronized) {
    return;
  }

  update_scope_notification_settings(scope, get_scope_notification_settings(scope), notification_settings);
}

bool MessagesManager::update_dialog_silent_send_message(Dialog *d, bool silent_send_message) {
  CHECK(d != nullptr);
  LOG_IF(WARNING, !d->notification_settings.is_synchronized)
      << "Have unknown notification settings in " << d->dialog_id;
  if (d->notification_settings.silent_send_message == silent_send_message) {
    return false;
  }

  LOG(INFO) << "Update silent send message in " << d->dialog_id << " to " << silent_send_message;
  d->notification_settings.silent_send_message = silent_send_message;

  on_dialog_updated(d->dialog_id, "update_dialog_silent_send_message");

  send_closure(G()->td(), &Td::send_update,
               make_tl_object<td_api::updateChatDefaultDisableNotification>(d->dialog_id.get(), silent_send_message));
  return true;
}

bool MessagesManager::get_dialog_report_spam_state(DialogId dialog_id, Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    promise.set_error(Status::Error(3, "Chat not found"));
    return false;
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(3, "Can't access the chat"));
    return false;
  }

  if (d->know_can_report_spam) {
    promise.set_value(Unit());
    return d->can_report_spam;
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      td_->create_handler<GetPeerSettingsQuery>(std::move(promise))->send(dialog_id);
      return false;
    case DialogType::SecretChat:
      promise.set_value(Unit());
      return false;
    case DialogType::None:
    default:
      UNREACHABLE();
      return false;
  }
}

void MessagesManager::change_dialog_report_spam_state(DialogId dialog_id, bool is_spam_dialog,
                                                      Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(3, "Chat not found"));
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(3, "Can't access the chat"));
  }

  if (!d->know_can_report_spam || !d->can_report_spam) {
    return promise.set_error(Status::Error(3, "Can't update chat report spam state"));
  }

  d->can_report_spam = false;
  on_dialog_updated(dialog_id, "change_dialog_report_spam_state");

  change_dialog_report_spam_state_on_server(dialog_id, is_spam_dialog, 0, std::move(promise));
}

class MessagesManager::ChangeDialogReportSpamStateOnServerLogEvent {
 public:
  DialogId dialog_id_;
  bool is_spam_dialog_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_id_, storer);
    td::store(is_spam_dialog_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_id_, parser);
    td::parse(is_spam_dialog_, parser);
  }
};

uint64 MessagesManager::save_change_dialog_report_spam_state_on_server_logevent(DialogId dialog_id,
                                                                                bool is_spam_dialog) {
  ChangeDialogReportSpamStateOnServerLogEvent logevent{dialog_id, is_spam_dialog};
  auto storer = LogEventStorerImpl<ChangeDialogReportSpamStateOnServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ChangeDialogReportSpamStateOnServer, storer);
}

void MessagesManager::change_dialog_report_spam_state_on_server(DialogId dialog_id, bool is_spam_dialog,
                                                                uint64 logevent_id, Promise<Unit> &&promise) {
  if (logevent_id == 0 && G()->parameters().use_message_db) {
    logevent_id = save_change_dialog_report_spam_state_on_server_logevent(dialog_id, is_spam_dialog);
  }

  auto new_promise = get_erase_logevent_promise(logevent_id, std::move(promise));
  promise = std::move(new_promise);  // to prevent self-move

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      return td_->create_handler<UpdatePeerSettingsQuery>(std::move(promise))->send(dialog_id, is_spam_dialog);
    case DialogType::SecretChat:
      if (is_spam_dialog) {
        return td_->create_handler<ReportEncryptedSpamQuery>(std::move(promise))->send(dialog_id);
      } else {
        promise.set_value(Unit());
        return;
      }
    case DialogType::None:
    default:
      UNREACHABLE();
      return;
  }
}

bool MessagesManager::can_report_dialog(DialogId dialog_id) const {
  switch (dialog_id.get_type()) {
    case DialogType::User:
      return td_->contacts_manager_->is_user_bot(dialog_id.get_user_id());
    case DialogType::Chat:
      return false;
    case DialogType::Channel:
      return !td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id()).is_creator();
    case DialogType::SecretChat:
      return false;
    case DialogType::None:
    default:
      UNREACHABLE();
      return false;
  }
}

void MessagesManager::report_dialog(DialogId dialog_id, const tl_object_ptr<td_api::ChatReportReason> &reason,
                                    const vector<MessageId> &message_ids, Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(3, "Chat not found"));
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(3, "Can't access the chat"));
  }

  if (reason == nullptr) {
    return promise.set_error(Status::Error(3, "Reason shouldn't be empty"));
  }

  if (!can_report_dialog(dialog_id)) {
    return promise.set_error(Status::Error(3, "Chat can't be reported"));
  }

  vector<MessageId> server_message_ids;
  for (auto message_id : message_ids) {
    if (message_id.is_valid() && message_id.is_server()) {
      server_message_ids.push_back(message_id);
    }
  }

  tl_object_ptr<telegram_api::ReportReason> report_reason;
  switch (reason->get_id()) {
    case td_api::chatReportReasonSpam::ID:
      report_reason = make_tl_object<telegram_api::inputReportReasonSpam>();
      break;
    case td_api::chatReportReasonViolence::ID:
      report_reason = make_tl_object<telegram_api::inputReportReasonViolence>();
      break;
    case td_api::chatReportReasonPornography::ID:
      report_reason = make_tl_object<telegram_api::inputReportReasonPornography>();
      break;
    case td_api::chatReportReasonChildAbuse::ID:
      report_reason = make_tl_object<telegram_api::inputReportReasonChildAbuse>();
      break;
    case td_api::chatReportReasonCopyright::ID:
      report_reason = make_tl_object<telegram_api::inputReportReasonCopyright>();
      break;
    case td_api::chatReportReasonCustom::ID: {
      auto other_reason = static_cast<const td_api::chatReportReasonCustom *>(reason.get());
      auto text = other_reason->text_;
      if (!clean_input_string(text)) {
        return promise.set_error(Status::Error(400, "Text must be encoded in UTF-8"));
      }

      report_reason = make_tl_object<telegram_api::inputReportReasonOther>(text);
      break;
    }
    default:
      UNREACHABLE();
  }
  CHECK(report_reason != nullptr);

  td_->create_handler<ReportPeerQuery>(std::move(promise))
      ->send(dialog_id, std::move(report_reason), server_message_ids);
}

void MessagesManager::on_get_peer_settings(DialogId dialog_id,
                                           tl_object_ptr<telegram_api::peerSettings> &&peer_settings) {
  CHECK(peer_settings != nullptr);
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return;
  }

  d->know_can_report_spam = true;
  d->can_report_spam = (peer_settings->flags_ & telegram_api::peerSettings::REPORT_SPAM_MASK) != 0;
  on_dialog_updated(dialog_id, "can_report_spam");
}

void MessagesManager::load_secret_thumbnail(FileId thumbnail_file_id) {
  class Callback : public FileManager::DownloadCallback {
   public:
    explicit Callback(Promise<> download_promise) : download_promise_(std::move(download_promise)) {
    }

    void on_download_ok(FileId file_id) override {
      download_promise_.set_value(Unit());
    }
    void on_download_error(FileId file_id, Status error) override {
      download_promise_.set_error(std::move(error));
    }

   private:
    Promise<> download_promise_;
  };

  auto thumbnail_promise = PromiseCreator::lambda([actor_id = actor_id(this),
                                                   thumbnail_file_id](Result<BufferSlice> r_thumbnail) mutable {
    BufferSlice thumbnail_slice;
    if (r_thumbnail.is_ok()) {
      thumbnail_slice = r_thumbnail.move_as_ok();
    }
    send_closure(actor_id, &MessagesManager::on_load_secret_thumbnail, thumbnail_file_id, std::move(thumbnail_slice));
  });

  auto download_promise = PromiseCreator::lambda(
      [thumbnail_file_id, thumbnail_promise = std::move(thumbnail_promise)](Result<Unit> r_download) mutable {
        if (r_download.is_error()) {
          thumbnail_promise.set_error(r_download.move_as_error());
          return;
        }
        send_closure(G()->file_manager(), &FileManager::get_content, thumbnail_file_id, std::move(thumbnail_promise));
      });

  send_closure(G()->file_manager(), &FileManager::download, thumbnail_file_id,
               std::make_shared<Callback>(std::move(download_promise)), 1, -1);
}

void MessagesManager::on_upload_media(FileId file_id, tl_object_ptr<telegram_api::InputFile> input_file,
                                      tl_object_ptr<telegram_api::InputEncryptedFile> input_encrypted_file) {
  LOG(INFO) << "File " << file_id << " has been uploaded";

  auto it = being_uploaded_files_.find(file_id);
  if (it == being_uploaded_files_.end()) {
    // callback may be called just before the file upload was cancelled
    return;
  }

  auto full_message_id = it->second.first;
  auto thumbnail_file_id = it->second.second;

  being_uploaded_files_.erase(it);

  Message *m = get_message(full_message_id);
  if (m == nullptr) {
    // message has already been deleted by the user or sent to inaccessible channel, do not need to send or edit it
    // file upload should be already cancelled in cancel_send_message_query, it shouldn't happen
    LOG(ERROR) << "Message with a media has already been deleted";
    return;
  }

  bool is_edit = m->message_id.is_server();
  auto dialog_id = full_message_id.get_dialog_id();
  auto can_send_status = can_send_message(dialog_id);
  if (!is_edit && can_send_status.is_error()) {
    // user has left the chat during upload of the file or lost his privileges
    LOG(INFO) << "Can't send a message to " << dialog_id << ": " << can_send_status.error();

    fail_send_message(full_message_id, can_send_status.move_as_error());
    return;
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      if (input_file && thumbnail_file_id.is_valid()) {
        // TODO: download thumbnail if needed (like in secret chats)
        being_uploaded_thumbnails_[thumbnail_file_id] = {full_message_id, file_id, std::move(input_file)};
        LOG(INFO) << "Ask to upload thumbnail " << thumbnail_file_id;
        td_->file_manager_->upload(thumbnail_file_id, upload_thumbnail_callback_, 1, m->message_id.get());
      } else {
        do_send_media(dialog_id, m, file_id, thumbnail_file_id, std::move(input_file), nullptr);
      }
      break;
    case DialogType::SecretChat:
      if (thumbnail_file_id.is_valid()) {
        being_loaded_secret_thumbnails_[thumbnail_file_id] = {full_message_id, file_id,
                                                              std::move(input_encrypted_file)};
        LOG(INFO) << "Ask to load thumbnail " << thumbnail_file_id;

        load_secret_thumbnail(thumbnail_file_id);
      } else {
        do_send_secret_media(dialog_id, m, file_id, thumbnail_file_id, std::move(input_encrypted_file), BufferSlice());
      }
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
      break;
  }
}

void MessagesManager::do_send_media(DialogId dialog_id, Message *m, FileId file_id, FileId thumbnail_file_id,
                                    tl_object_ptr<telegram_api::InputFile> input_file,
                                    tl_object_ptr<telegram_api::InputFile> input_thumbnail) {
  if (input_file == nullptr) {
    CHECK(input_thumbnail == nullptr);
    file_id = FileId();
    thumbnail_file_id = FileId();
  }
  CHECK(m != nullptr);

  MessageContent *content = nullptr;
  if (m->message_id.is_server()) {
    content = m->edited_content.get();
    if (content == nullptr) {
      LOG(ERROR) << "Message has no edited content";
      return;
    }
  } else {
    content = m->content.get();
  }

  on_message_media_uploaded(dialog_id, m,
                            get_input_media(content, td_, std::move(input_file), std::move(input_thumbnail), m->ttl),
                            file_id, thumbnail_file_id);
}

void MessagesManager::do_send_secret_media(DialogId dialog_id, Message *m, FileId file_id, FileId thumbnail_file_id,
                                           tl_object_ptr<telegram_api::InputEncryptedFile> input_encrypted_file,
                                           BufferSlice thumbnail) {
  if (input_encrypted_file == nullptr) {
    file_id = FileId();
    thumbnail_file_id = FileId();
  }

  CHECK(dialog_id.get_type() == DialogType::SecretChat);
  CHECK(m != nullptr);
  CHECK(m->message_id.is_yet_unsent());
  auto layer = td_->contacts_manager_->get_secret_chat_layer(dialog_id.get_secret_chat_id());
  on_secret_message_media_uploaded(
      dialog_id, m,
      get_secret_input_media(m->content.get(), td_, std::move(input_encrypted_file), std::move(thumbnail), layer),
      file_id, thumbnail_file_id);
}

void MessagesManager::on_upload_media_error(FileId file_id, Status status) {
  if (G()->close_flag()) {
    // do not fail upload if closing
    return;
  }

  LOG(WARNING) << "File " << file_id << " has upload error " << status;
  CHECK(status.is_error());

  auto it = being_uploaded_files_.find(file_id);
  if (it == being_uploaded_files_.end()) {
    // callback may be called just before the file upload was cancelled
    return;
  }

  auto full_message_id = it->second.first;

  being_uploaded_files_.erase(it);

  bool is_edit = full_message_id.get_message_id().is_server();
  if (is_edit) {
    fail_edit_message_media(full_message_id, Status::Error(status.code() > 0 ? status.code() : 500, status.message()));
  } else {
    fail_send_message(full_message_id, std::move(status));
  }
}

void MessagesManager::on_load_secret_thumbnail(FileId thumbnail_file_id, BufferSlice thumbnail) {
  if (G()->close_flag()) {
    // do not send secret media if closing, thumbnail may be wrong
    return;
  }

  LOG(INFO) << "SecretThumbnail " << thumbnail_file_id << " has been loaded with size " << thumbnail.size();

  auto it = being_loaded_secret_thumbnails_.find(thumbnail_file_id);
  if (it == being_loaded_secret_thumbnails_.end()) {
    // just in case, as in on_upload_thumbnail
    return;
  }

  auto full_message_id = it->second.full_message_id;
  auto file_id = it->second.file_id;
  auto input_file = std::move(it->second.input_file);

  being_loaded_secret_thumbnails_.erase(it);

  Message *m = get_message(full_message_id);
  if (m == nullptr) {
    // message has already been deleted by the user, do not need to send it
    // cancel file upload of the main file to allow next upload with the same file to succeed
    td_->file_manager_->upload(file_id, nullptr, 0, 0);
    LOG(INFO) << "Message with a media has already been deleted";
    return;
  }
  CHECK(m->message_id.is_yet_unsent());

  if (thumbnail.empty()) {
    delete_message_content_thumbnail(m->content.get(), td_);
  }

  auto dialog_id = full_message_id.get_dialog_id();
  auto can_send_status = can_send_message(dialog_id);
  if (can_send_status.is_error()) {
    // secret chat was closed during load of the file
    LOG(INFO) << "Can't send a message to " << dialog_id << ": " << can_send_status.error();

    fail_send_message(full_message_id, can_send_status.move_as_error());
    return;
  }

  do_send_secret_media(dialog_id, m, file_id, thumbnail_file_id, std::move(input_file), std::move(thumbnail));
}

void MessagesManager::on_upload_thumbnail(FileId thumbnail_file_id,
                                          tl_object_ptr<telegram_api::InputFile> thumbnail_input_file) {
  if (G()->close_flag()) {
    // do not fail upload if closing
    return;
  }

  LOG(INFO) << "Thumbnail " << thumbnail_file_id << " has been uploaded as " << to_string(thumbnail_input_file);

  auto it = being_uploaded_thumbnails_.find(thumbnail_file_id);
  if (it == being_uploaded_thumbnails_.end()) {
    // callback may be called just before the thumbnail upload was cancelled
    return;
  }

  auto full_message_id = it->second.full_message_id;
  auto file_id = it->second.file_id;
  auto input_file = std::move(it->second.input_file);

  being_uploaded_thumbnails_.erase(it);

  Message *m = get_message(full_message_id);
  if (m == nullptr) {
    // message has already been deleted by the user or sent to inaccessible channel, do not need to send or edit it
    // thumbnail file upload should be already cancelled in cancel_send_message_query
    LOG(ERROR) << "Message with a media has already been deleted";
    return;
  }

  bool is_edit = m->message_id.is_server();

  if (thumbnail_input_file == nullptr) {
    delete_message_content_thumbnail(is_edit ? m->edited_content.get() : m->content.get(), td_);
  }

  auto dialog_id = full_message_id.get_dialog_id();
  auto can_send_status = can_send_message(dialog_id);
  if (!is_edit && can_send_status.is_error()) {
    // user has left the chat during upload of the thumbnail or lost his privileges
    LOG(INFO) << "Can't send a message to " << dialog_id << ": " << can_send_status.error();

    fail_send_message(full_message_id, can_send_status.move_as_error());
    return;
  }

  do_send_media(dialog_id, m, file_id, thumbnail_file_id, std::move(input_file), std::move(thumbnail_input_file));
}

void MessagesManager::on_upload_dialog_photo(FileId file_id, tl_object_ptr<telegram_api::InputFile> input_file) {
  LOG(INFO) << "File " << file_id << " has been uploaded";

  auto it = uploaded_dialog_photos_.find(file_id);
  if (it == uploaded_dialog_photos_.end()) {
    // just in case, as in on_upload_media
    return;
  }

  Promise<Unit> promise = std::move(it->second.promise);
  DialogId dialog_id = it->second.dialog_id;

  uploaded_dialog_photos_.erase(it);

  tl_object_ptr<telegram_api::InputChatPhoto> input_chat_photo;
  FileView file_view = td_->file_manager_->get_file_view(file_id);
  CHECK(!file_view.is_encrypted());
  if (file_view.has_remote_location()) {
    if (file_view.remote_location().is_web()) {
      // TODO reupload
      promise.set_error(Status::Error(400, "Can't use web photo as profile photo"));
      return;
    }
    input_chat_photo = make_tl_object<telegram_api::inputChatPhoto>(file_view.remote_location().as_input_photo());
    file_id = FileId();
  } else {
    CHECK(input_file != nullptr);
    input_chat_photo = make_tl_object<telegram_api::inputChatUploadedPhoto>(std::move(input_file));
  }

  // TODO invoke after
  td_->create_handler<EditDialogPhotoQuery>(std::move(promise))->send(file_id, dialog_id, std::move(input_chat_photo));
}

void MessagesManager::on_upload_dialog_photo_error(FileId file_id, Status status) {
  if (G()->close_flag()) {
    // do not fail upload if closing
    return;
  }

  LOG(INFO) << "File " << file_id << " has upload error " << status;
  CHECK(status.is_error());

  auto it = uploaded_dialog_photos_.find(file_id);
  if (it == uploaded_dialog_photos_.end()) {
    // just in case, as in on_upload_media_error
    return;
  }

  Promise<Unit> promise = std::move(it->second.promise);

  uploaded_dialog_photos_.erase(it);

  promise.set_error(std::move(status));
}

void MessagesManager::before_get_difference() {
  running_get_difference_ = true;

  postponed_pts_updates_.insert(std::make_move_iterator(pending_updates_.begin()),
                                std::make_move_iterator(pending_updates_.end()));

  drop_pending_updates();
}

void MessagesManager::after_get_difference() {
  CHECK(!td_->updates_manager_->running_get_difference());

  if (postponed_pts_updates_.size()) {
    LOG(INFO) << "Begin to apply postponed pts updates";
    auto old_pts = td_->updates_manager_->get_pts();
    for (auto &update : postponed_pts_updates_) {
      auto new_pts = update.second.pts;
      if (new_pts <= old_pts) {
        skip_old_pending_update(std::move(update.second.update), new_pts, old_pts, update.second.pts_count,
                                "after get difference");
      } else {
        add_pending_update(std::move(update.second.update), update.second.pts, update.second.pts_count, false,
                           "after get difference");
      }
      CHECK(!td_->updates_manager_->running_get_difference());
    }
    postponed_pts_updates_.clear();
    LOG(INFO) << "Finish to apply postponed pts updates";
  }

  running_get_difference_ = false;

  if (!pending_on_get_dialogs_.empty()) {
    LOG(INFO) << "Apply postponed results of getDialogs";
    for (auto &res : pending_on_get_dialogs_) {
      on_get_dialogs(std::move(res.dialogs), res.total_count, std::move(res.messages), std::move(res.promise));
    }
    pending_on_get_dialogs_.clear();
  }

  if (!postponed_chat_read_inbox_updates_.empty()) {
    LOG(INFO) << "Send postponed chat read inbox updates";
    auto dialog_ids = std::move(postponed_chat_read_inbox_updates_);
    for (auto dialog_id : dialog_ids) {
      send_update_chat_read_inbox(get_dialog(dialog_id), false, "after_get_difference");
    }
  }
  if (have_postponed_unread_message_count_update_) {
    LOG(INFO) << "Send postponed unread message count update";
    send_update_unread_message_count(DialogId(), false, "after_get_difference");
  }
  if (have_postponed_unread_chat_count_update_) {
    LOG(INFO) << "Send postponed unread chat count update";
    send_update_unread_chat_count(DialogId(), false, "after_get_difference");
  }

  for (auto &it : update_message_ids_) {
    // this is impossible for ordinary chats because updates coming during getDifference have already been applied
    auto dialog_id = it.first.get_dialog_id();
    switch (dialog_id.get_type()) {
      case DialogType::Channel:
        // get channel difference may prevent updates from being applied
        if (running_get_channel_difference(dialog_id)) {
          break;
        }
      // fallthrough
      case DialogType::User:
      case DialogType::Chat:
        LOG(ERROR) << "Receive updateMessageId from " << it.second << " to " << it.first
                   << " but not receive corresponding message. " << td_->updates_manager_->get_state();
        if (dialog_id.get_type() != DialogType::Channel) {
          dump_debug_message_op(get_dialog(dialog_id));
        }
        get_messages_from_server(
            {it.first}, PromiseCreator::lambda([this, full_message_id = it.first](Result<Unit> result) {
              if (result.is_error()) {
                LOG(WARNING) << "Failed to get missing " << full_message_id << ": " << result.error();
              } else {
                LOG(WARNING) << "Successfully get missing " << full_message_id << ": "
                             << to_string(get_message_object(full_message_id));
              }
            }));
        break;
      case DialogType::SecretChat:
        break;
      case DialogType::None:
      default:
        UNREACHABLE();
        break;
    }
  }

  if (!G()->td_db()->get_binlog_pmc()->isset("fetched_marks_as_unread") && !td_->auth_manager_->is_bot()) {
    td_->create_handler<GetDialogUnreadMarksQuery>()->send();
  }

  load_notification_settings();

  // TODO resend some messages
}

void MessagesManager::on_get_messages(vector<tl_object_ptr<telegram_api::Message>> &&messages, bool is_channel_message,
                                      const char *source) {
  LOG(DEBUG) << "Receive " << messages.size() << " messages";
  for (auto &message : messages) {
    on_get_message(std::move(message), false, is_channel_message, false, false, source);
  }
}

void MessagesManager::on_get_history(DialogId dialog_id, MessageId from_message_id, int32 offset, int32 limit,
                                     bool from_the_end, vector<tl_object_ptr<telegram_api::Message>> &&messages) {
  LOG(INFO) << "Receive " << messages.size() << " history messages " << (from_the_end ? "from the end " : "") << "in "
            << dialog_id << " from " << from_message_id << " with offset " << offset << " and limit " << limit;
  CHECK(-limit < offset && offset <= 0);
  CHECK(offset < 0 || from_the_end);

  // it is likely that there is no more history messages on the server
  bool have_full_history = from_the_end && narrow_cast<int32>(messages.size()) < limit;
  Dialog *d = get_dialog(dialog_id);

  if (messages.empty()) {
    if (d != nullptr) {
      if (have_full_history) {
        d->have_full_history = true;
        on_dialog_updated(dialog_id, "set have_full_history");
      }

      if (from_the_end && d->have_full_history) {
        set_dialog_is_empty(d, "on_get_history empty");
      }
    }

    // be aware that in some cases an empty answer may be returned, because of the race of getHistory and deleteMessages
    // and not because there is no more messages
    return;
  }

  // TODO check that messages are received in decreasing message_id order

  // be aware that dialog may not yet exist
  // be aware that returned messages are guaranteed to be consecutive messages, but if !from_the_end they
  // may be older (if some messages was deleted) or newer (if some messages were added) than an expected answer
  // be aware that any subset of the returned messages may be already deleted and returned as MessageEmpty
  bool is_channel_message = dialog_id.get_type() == DialogType::Channel;
  MessageId first_added_message_id;
  MessageId last_received_message_id = get_message_id(messages[0]);
  MessageId last_added_message_id;
  bool have_next = false;

  if (narrow_cast<int32>(messages.size()) < limit + offset && d != nullptr) {
    MessageId first_received_message_id = get_message_id(messages.back());
    if (first_received_message_id.get() >= from_message_id.get() && d->first_database_message_id.is_valid() &&
        first_received_message_id.get() >= d->first_database_message_id.get()) {
      // it is likely that there is no more history messages on the server
      have_full_history = true;
    }
  }

  bool prev_have_full_history = false;
  MessageId prev_last_new_message_id;
  MessageId prev_first_database_message_id;
  MessageId prev_last_database_message_id;
  MessageId prev_last_message_id;
  if (d != nullptr) {
    prev_last_new_message_id = d->last_new_message_id;
    prev_first_database_message_id = d->first_database_message_id;
    prev_last_database_message_id = d->last_database_message_id;
    prev_last_message_id = d->last_message_id;
    prev_have_full_history = d->have_full_history;
  }

  for (auto &message : messages) {
    if (!have_next && from_the_end && d != nullptr && get_message_id(message).get() < d->last_message_id.get()) {
      // last message in the dialog should be attached to the next message if there is some
      have_next = true;
    }

    auto message_dialog_id = get_message_dialog_id(message);
    if (message_dialog_id != dialog_id) {
      LOG(ERROR) << "Receive " << get_message_id(message) << " in wrong " << message_dialog_id << " instead of "
                 << dialog_id << ": " << oneline(to_string(message));
      continue;
    }

    auto full_message_id =
        on_get_message(std::move(message), false, is_channel_message, false, have_next, "get history");
    auto message_id = full_message_id.get_message_id();
    if (message_id.is_valid()) {
      if (!last_added_message_id.is_valid()) {
        last_added_message_id = message_id;
      }

      if (!have_next) {
        if (d == nullptr) {
          d = get_dialog(dialog_id);
          CHECK(d != nullptr);
        }
        have_next = true;
      } else if (first_added_message_id.is_valid()) {
        Message *next_message = get_message(d, first_added_message_id);
        CHECK(next_message != nullptr);
        if (!next_message->have_previous) {
          LOG(INFO) << "Fix have_previous for " << first_added_message_id;
          next_message->have_previous = true;
          attach_message_to_previous(d, first_added_message_id, "on_get_history");
        }
      }
      first_added_message_id = message_id;
      if (!message_id.is_yet_unsent()) {
        // message should be already saved to database in on_get_message
        // add_message_to_database(d, get_message(d, message_id), "on_get_history");
      }
    }
  }

  if (d == nullptr) {
    return;
  }

  if (have_full_history) {
    d->have_full_history = true;
    on_dialog_updated(dialog_id, "set have_full_history 2");
  }

  //  LOG_IF(ERROR, d->first_message_id.is_valid() && d->first_message_id.get() > first_received_message_id.get())
  //      << "Receive message " << first_received_message_id << ", but first chat message is " << d->first_message_id;

  bool need_update_database_message_ids =
      last_added_message_id.is_valid() &&
      (from_the_end || (last_added_message_id.get() >= d->first_database_message_id.get() &&
                        d->last_database_message_id.get() >= first_added_message_id.get()));
  if (from_the_end) {
    if (!d->last_new_message_id.is_valid()) {
      set_dialog_last_new_message_id(
          d, last_added_message_id.is_valid() ? last_added_message_id : last_received_message_id, "on_get_history");
    }
    if (last_added_message_id.is_valid()) {
      if (last_added_message_id.get() > d->last_message_id.get()) {
        CHECK(d->last_new_message_id.is_valid());
        set_dialog_last_message_id(d, last_added_message_id, "on_get_history");
        send_update_chat_last_message(d, "on_get_history");
      }
    }
  }

  if (need_update_database_message_ids) {
    bool is_dialog_updated = false;
    if (!d->last_database_message_id.is_valid()) {
      CHECK(d->last_message_id.is_valid());
      MessagesConstIterator it(d, d->last_message_id);
      while (*it != nullptr) {
        auto message_id = (*it)->message_id;
        if (message_id.is_server() || message_id.is_local()) {
          if (!d->last_database_message_id.is_valid()) {
            set_dialog_last_database_message_id(d, message_id, "on_get_history");
          }
          set_dialog_first_database_message_id(d, message_id, "on_get_history");
          try_restore_dialog_reply_markup(d, *it);
        }
        --it;
      }
      is_dialog_updated = true;
    } else {
      CHECK(d->last_new_message_id.is_valid())
          << dialog_id << " " << from_the_end << " " << d->first_database_message_id << " "
          << d->last_database_message_id << " " << first_added_message_id << " " << last_added_message_id << " "
          << d->last_message_id << " " << d->last_new_message_id << " " << d->have_full_history << " "
          << prev_last_new_message_id << " " << prev_first_database_message_id << " " << prev_last_database_message_id
          << " " << prev_last_message_id << " " << prev_have_full_history << " " << d->debug_last_new_message_id << " "
          << d->debug_first_database_message_id << " " << d->debug_last_database_message_id << " " << from_message_id
          << " " << offset << " " << limit << " " << messages.size() << " " << last_received_message_id << " "
          << d->debug_set_dialog_last_database_message_id;
      CHECK(d->first_database_message_id.is_valid());
      {
        MessagesConstIterator it(d, d->first_database_message_id);
        if (*it != nullptr && ((*it)->message_id == d->first_database_message_id || (*it)->have_next)) {
          while (*it != nullptr) {
            auto message_id = (*it)->message_id;
            if ((message_id.is_server() || message_id.is_local()) &&
                message_id.get() < d->first_database_message_id.get()) {
              set_dialog_first_database_message_id(d, message_id, "on_get_history 2");
              try_restore_dialog_reply_markup(d, *it);
              is_dialog_updated = true;
            }
            --it;
          }
        }
      }
      {
        MessagesConstIterator it(d, d->last_database_message_id);
        if (*it != nullptr && ((*it)->message_id == d->last_database_message_id || (*it)->have_next)) {
          while (*it != nullptr) {
            auto message_id = (*it)->message_id;
            if ((message_id.is_server() || message_id.is_local()) &&
                message_id.get() > d->last_database_message_id.get()) {
              set_dialog_last_database_message_id(d, message_id, "on_get_history 2");
              is_dialog_updated = true;
            }
            ++it;
          }
        }
      }
    }
    CHECK(d->first_database_message_id.is_valid())
        << dialog_id << " " << from_the_end << " " << d->first_database_message_id << " " << d->last_database_message_id
        << " " << first_added_message_id << " " << last_added_message_id << " " << d->last_message_id << " "
        << d->last_new_message_id << " " << d->have_full_history << " " << prev_last_new_message_id << " "
        << prev_first_database_message_id << " " << prev_last_database_message_id << " " << prev_last_message_id << " "
        << prev_have_full_history << " " << d->debug_last_new_message_id << " " << d->debug_first_database_message_id
        << " " << d->debug_last_database_message_id << " " << from_message_id << " " << offset << " " << limit << " "
        << messages.size() << " " << last_received_message_id << " " << d->debug_set_dialog_last_database_message_id;

    CHECK(d->last_database_message_id.is_valid());

    for (auto &first_message_id : d->first_database_message_id_by_index) {
      if (first_added_message_id.get() < first_message_id.get() &&
          first_message_id.get() <= last_added_message_id.get()) {
        first_message_id = first_added_message_id;
      }
    }

    if (is_dialog_updated) {
      on_dialog_updated(dialog_id, "on_get_history");
    }
  }
}

vector<DialogId> MessagesManager::get_peers_dialog_ids(vector<tl_object_ptr<telegram_api::Peer>> &&peers) {
  vector<DialogId> result;
  result.reserve(peers.size());
  for (auto &peer : peers) {
    DialogId dialog_id(peer);
    if (dialog_id.is_valid()) {
      force_create_dialog(dialog_id, "get_peers_dialog_ids");
      result.push_back(dialog_id);
    }
  }
  return result;
}

void MessagesManager::on_get_public_dialogs_search_result(const string &query,
                                                          vector<tl_object_ptr<telegram_api::Peer>> &&my_peers,
                                                          vector<tl_object_ptr<telegram_api::Peer>> &&peers) {
  auto it = search_public_dialogs_queries_.find(query);
  CHECK(it != search_public_dialogs_queries_.end());
  CHECK(!it->second.empty());
  auto promises = std::move(it->second);
  search_public_dialogs_queries_.erase(it);

  found_public_dialogs_[query] = get_peers_dialog_ids(std::move(peers));
  found_on_server_dialogs_[query] = get_peers_dialog_ids(std::move(my_peers));

  for (auto &promise : promises) {
    promise.set_value(Unit());
  }
}

void MessagesManager::on_failed_public_dialogs_search(const string &query, Status &&error) {
  auto it = search_public_dialogs_queries_.find(query);
  CHECK(it != search_public_dialogs_queries_.end());
  CHECK(!it->second.empty());
  auto promises = std::move(it->second);
  search_public_dialogs_queries_.erase(it);

  found_public_dialogs_[query];     // negative cache
  found_on_server_dialogs_[query];  // negative cache

  for (auto &promise : promises) {
    promise.set_error(error.clone());
  }
}

void MessagesManager::on_get_dialog_messages_search_result(DialogId dialog_id, const string &query,
                                                           UserId sender_user_id, MessageId from_message_id,
                                                           int32 offset, int32 limit, SearchMessagesFilter filter,
                                                           int64 random_id, int32 total_count,
                                                           vector<tl_object_ptr<telegram_api::Message>> &&messages) {
  LOG(INFO) << "Receive " << messages.size() << " found messages in " << dialog_id;
  if (!dialog_id.is_valid()) {
    CHECK(query.empty());
    CHECK(!sender_user_id.is_valid());
    auto it = found_call_messages_.find(random_id);
    CHECK(it != found_call_messages_.end());

    MessageId first_added_message_id;
    if (messages.empty()) {
      // messages may be empty because there is no more messages or they can't be found due to global limit
      // anyway pretend that there is no more messages
      first_added_message_id = MessageId::min();
    }

    auto &result = it->second.second;
    CHECK(result.empty());
    for (auto &message : messages) {
      auto new_message = on_get_message(std::move(message), false, false, false, false, "search call messages");
      if (new_message != FullMessageId()) {
        result.push_back(new_message);
      }

      auto message_id = new_message.get_message_id();
      if (message_id.get() < first_added_message_id.get() || !first_added_message_id.is_valid()) {
        first_added_message_id = message_id;
      }
    }
    if (G()->parameters().use_message_db) {
      bool update_state = false;

      auto &old_message_count = calls_db_state_.message_count_by_index[search_calls_filter_index(filter)];
      if (old_message_count != total_count) {
        LOG(INFO) << "Update calls database message count to " << total_count;
        old_message_count = total_count;
        update_state = true;
      }

      auto &old_first_db_message_id =
          calls_db_state_.first_calls_database_message_id_by_index[search_calls_filter_index(filter)];
      bool from_the_end = !from_message_id.is_valid() || from_message_id.get() >= MessageId::max().get();
      LOG(INFO) << "from_the_end = " << from_the_end << ", old_first_db_message_id = " << old_first_db_message_id.get()
                << ", first_added_message_id = " << first_added_message_id.get()
                << ", from_message_id = " << from_message_id.get();
      if ((from_the_end ||
           (old_first_db_message_id.is_valid() && old_first_db_message_id.get() <= from_message_id.get())) &&
          (!old_first_db_message_id.is_valid() || first_added_message_id.get() < old_first_db_message_id.get())) {
        LOG(INFO) << "Update calls database first message id to " << first_added_message_id;
        old_first_db_message_id = first_added_message_id;
        update_state = true;
      }
      if (update_state) {
        save_calls_db_state();
      }
    }
    it->second.first = total_count;
    return;
  }

  auto it = found_dialog_messages_.find(random_id);
  CHECK(it != found_dialog_messages_.end());

  auto &result = it->second.second;
  CHECK(result.empty());
  MessageId first_added_message_id;
  if (messages.empty()) {
    // messages may be empty because there is no more messages or they can't be found due to global limit
    // anyway pretend that there is no more messages
    first_added_message_id = MessageId::min();
  }
  for (auto &message : messages) {
    auto new_message = on_get_message(std::move(message), false, dialog_id.get_type() == DialogType::Channel, false,
                                      false, "search chat messages");
    if (new_message != FullMessageId()) {
      if (new_message.get_dialog_id() != dialog_id) {
        LOG(ERROR) << "Receive " << new_message << " instead of a message in " << dialog_id;
        continue;
      }

      // TODO check that messages are returned in decreasing message_id order
      auto message_id = new_message.get_message_id();
      if (message_id.get() < first_added_message_id.get() || !first_added_message_id.is_valid()) {
        first_added_message_id = message_id;
      }
      result.push_back(message_id);
    } else {
      total_count--;
    }
  }
  if (total_count < static_cast<int32>(result.size())) {
    LOG(ERROR) << "Receive " << result.size() << " valid messages out of " << total_count << " in " << messages.size()
               << " messages";
    total_count = static_cast<int32>(result.size());
  }
  if (query.empty() && !sender_user_id.is_valid() && filter != SearchMessagesFilter::Empty &&
      G()->parameters().use_message_db) {
    Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr);
    bool update_dialog = false;

    auto &old_message_count = d->message_count_by_index[search_messages_filter_index(filter)];
    if (old_message_count != total_count) {
      old_message_count = total_count;
      if (filter == SearchMessagesFilter::UnreadMention) {
        d->unread_mention_count = old_message_count;
        update_dialog_mention_notification_count(d);
        send_update_chat_unread_mention_count(d);
      }
      update_dialog = true;
    }

    auto &old_first_db_message_id = d->first_database_message_id_by_index[search_messages_filter_index(filter)];
    bool from_the_end = !from_message_id.is_valid() ||
                        (d->last_message_id != MessageId() && from_message_id.get() > d->last_message_id.get()) ||
                        from_message_id.get() >= MessageId::max().get();
    if ((from_the_end ||
         (old_first_db_message_id.is_valid() && old_first_db_message_id.get() <= from_message_id.get())) &&
        (!old_first_db_message_id.is_valid() || first_added_message_id.get() < old_first_db_message_id.get())) {
      old_first_db_message_id = first_added_message_id;
      update_dialog = true;
    }
    if (update_dialog) {
      on_dialog_updated(dialog_id, "search results");
    }
  }
  it->second.first = total_count;
}

void MessagesManager::on_failed_dialog_messages_search(DialogId dialog_id, int64 random_id) {
  if (!dialog_id.is_valid()) {
    auto it = found_call_messages_.find(random_id);
    CHECK(it != found_call_messages_.end());
    found_call_messages_.erase(it);
    return;
  }

  auto it = found_dialog_messages_.find(random_id);
  CHECK(it != found_dialog_messages_.end());
  found_dialog_messages_.erase(it);
}

void MessagesManager::on_get_messages_search_result(const string &query, int32 offset_date, DialogId offset_dialog_id,
                                                    MessageId offset_message_id, int32 limit, int64 random_id,
                                                    int32 total_count,
                                                    vector<tl_object_ptr<telegram_api::Message>> &&messages) {
  LOG(INFO) << "Receive " << messages.size() << " found messages";
  auto it = found_messages_.find(random_id);
  CHECK(it != found_messages_.end());

  auto &result = it->second.second;
  CHECK(result.empty());
  for (auto &message : messages) {
    auto dialog_id = get_message_dialog_id(message);
    auto new_message = on_get_message(std::move(message), false, dialog_id.get_type() == DialogType::Channel, false,
                                      false, "search messages");
    if (new_message != FullMessageId()) {
      CHECK(dialog_id == new_message.get_dialog_id());
      result.push_back(new_message);
    } else {
      total_count--;
    }
  }
  if (total_count < static_cast<int32>(result.size())) {
    LOG(ERROR) << "Receive " << result.size() << " valid messages out of " << total_count << " in " << messages.size()
               << " messages";
    total_count = static_cast<int32>(result.size());
  }
  it->second.first = total_count;
}

void MessagesManager::on_failed_messages_search(int64 random_id) {
  auto it = found_messages_.find(random_id);
  CHECK(it != found_messages_.end());
  found_messages_.erase(it);
}

void MessagesManager::on_get_recent_locations(DialogId dialog_id, int32 limit, int64 random_id, int32 total_count,
                                              vector<tl_object_ptr<telegram_api::Message>> &&messages) {
  LOG(INFO) << "Receive " << messages.size() << " recent locations in " << dialog_id;
  auto it = found_dialog_recent_location_messages_.find(random_id);
  CHECK(it != found_dialog_recent_location_messages_.end());

  auto &result = it->second.second;
  CHECK(result.empty());
  for (auto &message : messages) {
    auto new_message = on_get_message(std::move(message), false, dialog_id.get_type() == DialogType::Channel, false,
                                      false, "get recent locations");
    if (new_message != FullMessageId()) {
      if (new_message.get_dialog_id() != dialog_id) {
        LOG(ERROR) << "Receive " << new_message << " instead of a message in " << dialog_id;
        continue;
      }
      auto m = get_message(new_message);
      if (m->content->get_type() != MessageContentType::LiveLocation) {
        LOG(ERROR) << "Receive a message of wrong type " << m->content->get_type() << " in on_get_recent_locations in "
                   << dialog_id;
        continue;
      }

      result.push_back(new_message.get_message_id());
    } else {
      total_count--;
    }
  }
  if (total_count < static_cast<int32>(result.size())) {
    LOG(ERROR) << "Receive " << result.size() << " valid messages out of " << total_count << " in " << messages.size()
               << " messages";
    total_count = static_cast<int32>(result.size());
  }
  it->second.first = total_count;
}

void MessagesManager::on_get_recent_locations_failed(int64 random_id) {
  auto it = found_dialog_recent_location_messages_.find(random_id);
  CHECK(it != found_dialog_recent_location_messages_.end());
  found_dialog_recent_location_messages_.erase(it);
}

void MessagesManager::delete_messages_from_updates(const vector<MessageId> &message_ids) {
  std::unordered_map<DialogId, vector<int64>, DialogIdHash> deleted_message_ids;
  std::unordered_map<DialogId, bool, DialogIdHash> need_update_dialog_pos;
  for (auto message_id : message_ids) {
    if (!message_id.is_valid() || !message_id.is_server()) {
      LOG(ERROR) << "Incoming update tries to delete " << message_id;
      continue;
    }

    Dialog *d = get_dialog_by_message_id(message_id);
    if (d != nullptr) {
      auto m = delete_message(d, message_id, true, &need_update_dialog_pos[d->dialog_id], "updates");
      CHECK(m != nullptr);
      CHECK(m->message_id == message_id);
      deleted_message_ids[d->dialog_id].push_back(message_id.get());
    }
    if (last_clear_history_message_id_to_dialog_id_.count(message_id)) {
      d = get_dialog(last_clear_history_message_id_to_dialog_id_[message_id]);
      CHECK(d != nullptr);
      auto m = delete_message(d, message_id, true, &need_update_dialog_pos[d->dialog_id], "updates");
      CHECK(m == nullptr);
    }
  }
  for (auto &it : need_update_dialog_pos) {
    if (it.second) {
      auto dialog_id = it.first;
      Dialog *d = get_dialog(dialog_id);
      CHECK(d != nullptr);
      send_update_chat_last_message(d, "delete_messages_from_updates");
    }
  }
  for (auto &it : deleted_message_ids) {
    auto dialog_id = it.first;
    send_update_delete_messages(dialog_id, std::move(it.second), true, false);
  }
}

void MessagesManager::delete_dialog_messages_from_updates(DialogId dialog_id, const vector<MessageId> &message_ids) {
  CHECK(dialog_id.get_type() == DialogType::Channel || dialog_id.get_type() == DialogType::SecretChat);
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(INFO) << "Ignore deleteChannelMessages for unknown " << dialog_id;
    CHECK(dialog_id.get_type() == DialogType::Channel);
    return;
  }

  vector<int64> deleted_message_ids;
  bool need_update_dialog_pos = false;
  for (auto message_id : message_ids) {
    if (!message_id.is_valid() || (!message_id.is_server() && dialog_id.get_type() != DialogType::SecretChat)) {
      LOG(ERROR) << "Incoming update tries to delete " << message_id;
      continue;
    }

    if (delete_message(d, message_id, true, &need_update_dialog_pos, "updates") != nullptr) {
      deleted_message_ids.push_back(message_id.get());
    }
  }
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "delete_dialog_messages_from_updates");
  }
  send_update_delete_messages(dialog_id, std::move(deleted_message_ids), true, false);
}

string MessagesManager::get_search_text(const Message *m) const {
  if (m->is_content_secret) {
    return string();
  }
  return get_message_content_search_text(td_, m->content.get());
}

bool MessagesManager::can_forward_message(DialogId from_dialog_id, const Message *m) {
  if (m == nullptr) {
    return false;
  }
  if (m->ttl > 0) {
    return false;
  }
  switch (from_dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      // ok
      break;
    case DialogType::SecretChat:
      return false;
    case DialogType::None:
    default:
      UNREACHABLE();
      return false;
  }

  return can_forward_message_content(m->content.get());
}

bool MessagesManager::can_delete_channel_message(DialogParticipantStatus status, const Message *m, bool is_bot) {
  if (m == nullptr) {
    return true;
  }
  if (m->message_id.is_local() || m->message_id.is_yet_unsent()) {
    return true;
  }

  if (is_bot && G()->unix_time_cached() >= m->date + 2 * 86400) {
    // bots can't delete messages older than 2 days
    return false;
  }

  CHECK(m->message_id.is_server());
  if (m->message_id.get_server_message_id().get() == 1) {
    return false;
  }
  auto content_type = m->content->get_type();
  if (content_type == MessageContentType::ChannelMigrateFrom || content_type == MessageContentType::ChannelCreate) {
    return false;
  }

  if (status.can_delete_messages()) {
    return true;
  }

  if (!m->is_outgoing) {
    return false;
  }

  if (m->is_channel_post || is_service_message_content(content_type)) {
    return status.can_post_messages();
  }

  return true;
}

bool MessagesManager::can_revoke_message(DialogId dialog_id, const Message *m) const {
  if (m == nullptr) {
    return false;
  }
  if (m->message_id.is_local()) {
    return false;
  }
  if (dialog_id == get_my_dialog_id()) {
    return false;
  }
  if (m->message_id.is_yet_unsent()) {
    return true;
  }
  CHECK(m->message_id.is_server());

  bool is_appointed_administrator = false;
  bool can_revoke_incoming = false;
  const int32 DEFAULT_REVOKE_TIME_LIMIT = 2 * 86400;
  int32 revoke_time_limit = G()->shared_config().get_option_integer("revoke_time_limit", DEFAULT_REVOKE_TIME_LIMIT);
  switch (dialog_id.get_type()) {
    case DialogType::User:
      can_revoke_incoming = G()->shared_config().get_option_boolean("revoke_pm_inbox");
      revoke_time_limit = G()->shared_config().get_option_integer("revoke_pm_time_limit", DEFAULT_REVOKE_TIME_LIMIT);
      break;
    case DialogType::Chat:
      is_appointed_administrator = td_->contacts_manager_->is_appointed_chat_administrator(dialog_id.get_chat_id());
      break;
    case DialogType::Channel:
      return true;  // any server message that can be deleted will be deleted for all participants
    case DialogType::SecretChat:
      // all non-service messages will be deleted for everyone if secret chat is active
      return td_->contacts_manager_->get_secret_chat_state(dialog_id.get_secret_chat_id()) == SecretChatState::Active &&
             !is_service_message_content(m->content->get_type());
    case DialogType::None:
    default:
      UNREACHABLE();
      return false;
  }

  return (((m->is_outgoing || can_revoke_incoming) && !is_service_message_content(m->content->get_type())) ||
          is_appointed_administrator) &&
         G()->unix_time_cached() - m->date <= revoke_time_limit;
}

void MessagesManager::delete_messages(DialogId dialog_id, const vector<MessageId> &input_message_ids, bool revoke,
                                      Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(6, "Chat is not found"));
  }

  if (input_message_ids.empty()) {
    return promise.set_value(Unit());
  }

  auto dialog_type = dialog_id.get_type();
  bool is_secret = dialog_type == DialogType::SecretChat;

  vector<MessageId> message_ids;
  message_ids.reserve(input_message_ids.size());
  vector<MessageId> deleted_server_message_ids;
  for (auto message_id : input_message_ids) {
    if (!message_id.is_valid()) {
      return promise.set_error(Status::Error(6, "Invalid message identifier"));
    }
    message_id = get_persistent_message_id(d, message_id);
    message_ids.push_back(message_id);
    if (get_message_force(d, message_id) != nullptr && (message_id.is_server() || is_secret)) {
      deleted_server_message_ids.push_back(message_id);
    }
  }

  bool is_bot = td_->auth_manager_->is_bot();
  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
      if (is_bot) {
        for (auto message_id : message_ids) {
          if (message_id.is_server() && !can_revoke_message(dialog_id, get_message(d, message_id))) {
            return promise.set_error(Status::Error(6, "Message can't be deleted"));
          }
        }
      }
      break;
    case DialogType::Channel: {
      auto dialog_status = td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id());
      for (auto message_id : message_ids) {
        if (!can_delete_channel_message(dialog_status, get_message(d, message_id), is_bot)) {
          return promise.set_error(Status::Error(6, "Message can't be deleted"));
        }
      }
      break;
    }
    case DialogType::SecretChat:
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  delete_messages_from_server(dialog_id, std::move(deleted_server_message_ids), revoke, 0, std::move(promise));

  bool need_update_dialog_pos = false;
  vector<int64> deleted_message_ids;
  for (auto message_id : message_ids) {
    auto m = delete_message(d, message_id, true, &need_update_dialog_pos, DELETE_MESSAGE_USER_REQUEST_SOURCE);
    if (m == nullptr) {
      LOG(INFO) << "Can't delete " << message_id << " because it is not found";
    } else {
      deleted_message_ids.push_back(m->message_id.get());
    }
  }

  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "delete_messages");
  }
  send_update_delete_messages(dialog_id, std::move(deleted_message_ids), true, false);
}

class MessagesManager::DeleteMessagesFromServerLogEvent {
 public:
  DialogId dialog_id_;
  vector<MessageId> message_ids_;
  bool revoke_;

  template <class StorerT>
  void store(StorerT &storer) const {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(revoke_);
    END_STORE_FLAGS();

    td::store(dialog_id_, storer);
    td::store(message_ids_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(revoke_);
    END_PARSE_FLAGS();

    td::parse(dialog_id_, parser);
    td::parse(message_ids_, parser);
  }
};

uint64 MessagesManager::save_delete_messages_from_server_logevent(DialogId dialog_id,
                                                                  const vector<MessageId> &message_ids, bool revoke) {
  DeleteMessagesFromServerLogEvent logevent{dialog_id, message_ids, revoke};
  auto storer = LogEventStorerImpl<DeleteMessagesFromServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteMessagesFromServer, storer);
}

void MessagesManager::delete_messages_from_server(DialogId dialog_id, vector<MessageId> message_ids, bool revoke,
                                                  uint64 logevent_id, Promise<Unit> &&promise) {
  if (message_ids.empty()) {
    promise.set_value(Unit());
    return;
  }
  LOG(INFO) << (revoke ? "Revoke " : "Delete ") << format::as_array(message_ids) << " in " << dialog_id
            << " from server";

  if (logevent_id == 0 && G()->parameters().use_message_db) {
    logevent_id = save_delete_messages_from_server_logevent(dialog_id, message_ids, revoke);
  }

  auto new_promise = get_erase_logevent_promise(logevent_id, std::move(promise));
  promise = std::move(new_promise);  // to prevent self-move

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
      td_->create_handler<DeleteMessagesQuery>(std::move(promise))->send(std::move(message_ids), revoke);
      break;
    case DialogType::Channel:
      td_->create_handler<DeleteChannelMessagesQuery>(std::move(promise))
          ->send(dialog_id.get_channel_id(), std::move(message_ids));
      break;
    case DialogType::SecretChat: {
      vector<int64> random_ids;
      auto d = get_dialog_force(dialog_id);
      CHECK(d != nullptr);
      for (auto &message_id : message_ids) {
        auto *message = get_message(d, message_id);
        if (message != nullptr) {
          random_ids.push_back(message->random_id);
        }
      }
      if (!random_ids.empty()) {
        send_closure(G()->secret_chats_manager(), &SecretChatsManager::delete_messages, dialog_id.get_secret_chat_id(),
                     std::move(random_ids), std::move(promise));
      } else {
        promise.set_value(Unit());
      }
      break;
    }
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

void MessagesManager::delete_dialog_history(DialogId dialog_id, bool remove_from_dialog_list, Promise<Unit> &&promise) {
  LOG(INFO) << "Receive deleteChatHistory request to delete all messages in " << dialog_id
            << ", remove_from_chat_list is " << remove_from_dialog_list;

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(3, "Chat not found"));
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(3, "Chat info not found"));
  }

  auto dialog_type = dialog_id.get_type();
  bool is_secret = false;
  switch (dialog_type) {
    case DialogType::User:
    case DialogType::Chat:
      // ok
      break;
    case DialogType::Channel:
      if (is_broadcast_channel(dialog_id)) {
        return promise.set_error(Status::Error(3, "Can't delete chat history in a channel"));
      }
      if (!get_dialog_username(dialog_id).empty()) {
        return promise.set_error(Status::Error(3, "Can't delete chat history in a public supergroup"));
      }
      break;
    case DialogType::SecretChat:
      is_secret = true;
      // ok
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
      break;
  }

  auto last_new_message_id = d->last_new_message_id;
  if (!is_secret && last_new_message_id == MessageId()) {
    // TODO get dialog from the server and delete history from last message id
  }

  bool allow_error = d->messages == nullptr;

  delete_all_dialog_messages(d, remove_from_dialog_list, true);

  if (last_new_message_id.is_valid() && last_new_message_id == d->max_unavailable_message_id) {
    // history has already been cleared, nothing to do
    promise.set_value(Unit());
    return;
  }

  set_dialog_max_unavailable_message_id(dialog_id, last_new_message_id, false, "delete_dialog_history");

  delete_dialog_history_from_server(dialog_id, last_new_message_id, remove_from_dialog_list, allow_error, 0,
                                    std::move(promise));
}

class MessagesManager::DeleteDialogHistoryFromServerLogEvent {
 public:
  DialogId dialog_id_;
  MessageId max_message_id_;
  bool remove_from_dialog_list_;

  template <class StorerT>
  void store(StorerT &storer) const {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(remove_from_dialog_list_);
    END_STORE_FLAGS();

    td::store(dialog_id_, storer);
    td::store(max_message_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(remove_from_dialog_list_);
    END_PARSE_FLAGS();

    td::parse(dialog_id_, parser);
    td::parse(max_message_id_, parser);
  }
};

uint64 MessagesManager::save_delete_dialog_history_from_server_logevent(DialogId dialog_id, MessageId max_message_id,
                                                                        bool remove_from_dialog_list) {
  DeleteDialogHistoryFromServerLogEvent logevent{dialog_id, max_message_id, remove_from_dialog_list};
  auto storer = LogEventStorerImpl<DeleteDialogHistoryFromServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteDialogHistoryFromServer, storer);
}

void MessagesManager::delete_dialog_history_from_server(DialogId dialog_id, MessageId max_message_id,
                                                        bool remove_from_dialog_list, bool allow_error,
                                                        uint64 logevent_id, Promise<Unit> &&promise) {
  LOG(INFO) << "Delete history in " << dialog_id << " up to " << max_message_id << " from server";

  if (logevent_id == 0 && G()->parameters().use_message_db) {
    logevent_id = save_delete_dialog_history_from_server_logevent(dialog_id, max_message_id, remove_from_dialog_list);
  }

  auto new_promise = get_erase_logevent_promise(logevent_id, std::move(promise));
  promise = std::move(new_promise);  // to prevent self-move

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
      td_->create_handler<DeleteHistoryQuery>(std::move(promise))
          ->send(dialog_id, max_message_id, remove_from_dialog_list);
      break;
    case DialogType::Channel:
      td_->create_handler<DeleteChannelHistoryQuery>(std::move(promise))
          ->send(dialog_id.get_channel_id(), max_message_id, allow_error);
      break;
    case DialogType::SecretChat:
      send_closure(G()->secret_chats_manager(), &SecretChatsManager::delete_all_messages,
                   dialog_id.get_secret_chat_id(), std::move(promise));
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
      break;
  }
}

void MessagesManager::find_messages_from_user(const unique_ptr<Message> &m, UserId user_id,
                                              vector<MessageId> &message_ids) {
  if (m == nullptr) {
    return;
  }

  find_messages_from_user(m->left, user_id, message_ids);

  if (m->sender_user_id == user_id) {
    message_ids.push_back(m->message_id);
  }

  find_messages_from_user(m->right, user_id, message_ids);
}

void MessagesManager::find_unread_mentions(const unique_ptr<Message> &m, vector<MessageId> &message_ids) {
  if (m == nullptr) {
    return;
  }

  find_unread_mentions(m->left, message_ids);

  if (m->contains_unread_mention) {
    message_ids.push_back(m->message_id);
  }

  find_unread_mentions(m->right, message_ids);
}

void MessagesManager::find_old_messages(const unique_ptr<Message> &m, MessageId max_message_id,
                                        vector<MessageId> &message_ids) {
  if (m == nullptr) {
    return;
  }

  find_old_messages(m->left, max_message_id, message_ids);

  if (m->message_id.get() <= max_message_id.get()) {
    message_ids.push_back(m->message_id);

    find_old_messages(m->right, max_message_id, message_ids);
  }
}

void MessagesManager::find_unloadable_messages(const Dialog *d, int32 unload_before_date, const unique_ptr<Message> &m,
                                               vector<MessageId> &message_ids, int32 &left_to_unload) const {
  if (m == nullptr) {
    return;
  }

  find_unloadable_messages(d, unload_before_date, m->left, message_ids, left_to_unload);

  if (can_unload_message(d, m.get())) {
    if (m->last_access_date <= unload_before_date) {
      message_ids.push_back(m->message_id);
    } else {
      left_to_unload++;
    }
  }

  find_unloadable_messages(d, unload_before_date, m->right, message_ids, left_to_unload);
}

void MessagesManager::delete_dialog_messages_from_user(DialogId dialog_id, UserId user_id, Promise<Unit> &&promise) {
  bool is_bot = td_->auth_manager_->is_bot();
  if (is_bot) {
    return promise.set_error(Status::Error(3, "Method is not available for bots"));
  }

  LOG(INFO) << "Receive deleteChatMessagesFromUser request to delete all messages in " << dialog_id << " from the user "
            << user_id;
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(3, "Chat not found"));
  }

  if (!have_input_peer(dialog_id, AccessRights::Write)) {
    return promise.set_error(Status::Error(3, "Not enough rights"));
  }

  if (!td_->contacts_manager_->have_input_user(user_id)) {
    return promise.set_error(Status::Error(3, "User not found"));
  }

  ChannelId channel_id;
  DialogParticipantStatus channel_status = DialogParticipantStatus::Left();
  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::SecretChat:
      return promise.set_error(Status::Error(3, "All messages from a user can be deleted only in supergroup chats"));
    case DialogType::Channel: {
      channel_id = dialog_id.get_channel_id();
      auto channel_type = td_->contacts_manager_->get_channel_type(channel_id);
      if (channel_type != ChannelType::Megagroup) {
        return promise.set_error(Status::Error(3, "The method is available only for supergroup chats"));
      }
      channel_status = td_->contacts_manager_->get_channel_status(channel_id);
      if (!channel_status.can_delete_messages()) {
        return promise.set_error(Status::Error(5, "Need delete messages administator right in the supergroup chat"));
      }
      channel_id = dialog_id.get_channel_id();
      break;
    }
    case DialogType::None:
    default:
      UNREACHABLE();
      break;
  }
  CHECK(channel_id.is_valid());

  if (G()->parameters().use_message_db) {
    LOG(INFO) << "Delete all messages from " << user_id << " in " << dialog_id << " from database";
    G()->td_db()->get_messages_db_async()->delete_dialog_messages_from_user(dialog_id, user_id,
                                                                            Auto());  // TODO Promise
  }

  vector<MessageId> message_ids;
  find_messages_from_user(d->messages, user_id, message_ids);

  vector<int64> deleted_message_ids;
  bool need_update_dialog_pos = false;
  for (auto message_id : message_ids) {
    auto m = get_message(d, message_id);
    CHECK(m != nullptr);
    CHECK(m->sender_user_id == user_id);
    CHECK(m->message_id == message_id);
    if (can_delete_channel_message(channel_status, m, is_bot)) {
      deleted_message_ids.push_back(message_id.get());
      auto p = delete_message(d, message_id, true, &need_update_dialog_pos, "delete messages from user");
      CHECK(p.get() == m);
    }
  }

  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "delete_messages_from_user");
  }

  send_update_delete_messages(dialog_id, std::move(deleted_message_ids), true, false);

  delete_all_channel_messages_from_user_on_server(channel_id, user_id, 0, std::move(promise));
}

class MessagesManager::DeleteAllChannelMessagesFromUserOnServerLogEvent {
 public:
  ChannelId channel_id_;
  UserId user_id_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(channel_id_, storer);
    td::store(user_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(channel_id_, parser);
    td::parse(user_id_, parser);
  }
};

uint64 MessagesManager::save_delete_all_channel_messages_from_user_on_server_logevent(ChannelId channel_id,
                                                                                      UserId user_id) {
  DeleteAllChannelMessagesFromUserOnServerLogEvent logevent{channel_id, user_id};
  auto storer = LogEventStorerImpl<DeleteAllChannelMessagesFromUserOnServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteAllChannelMessagesFromUserOnServer,
                    storer);
}

void MessagesManager::delete_all_channel_messages_from_user_on_server(ChannelId channel_id, UserId user_id,
                                                                      uint64 logevent_id, Promise<Unit> &&promise) {
  if (logevent_id == 0 && G()->parameters().use_chat_info_db) {
    logevent_id = save_delete_all_channel_messages_from_user_on_server_logevent(channel_id, user_id);
  }

  td_->create_handler<DeleteUserHistoryQuery>(get_erase_logevent_promise(logevent_id, std::move(promise)))
      ->send(channel_id, user_id);
}

void MessagesManager::unload_dialog(DialogId dialog_id) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  vector<MessageId> to_unload_message_ids;
  int32 left_to_unload = 0;
  find_unloadable_messages(d, G()->unix_time_cached() - DIALOG_UNLOAD_DELAY + 2, d->messages, to_unload_message_ids,
                           left_to_unload);

  vector<int64> unloaded_message_ids;
  for (auto message_id : to_unload_message_ids) {
    unload_message(d, message_id);
    unloaded_message_ids.push_back(message_id.get());
  }

  if (!unloaded_message_ids.empty()) {
    if (!G()->parameters().use_message_db) {
      d->have_full_history = false;
    }

    send_closure_later(
        G()->td(), &Td::send_update,
        make_tl_object<td_api::updateDeleteMessages>(dialog_id.get(), std::move(unloaded_message_ids), false, true));
  }

  if (left_to_unload > 0) {
    LOG(DEBUG) << "Need to unload " << left_to_unload << " messages more in " << dialog_id;
    pending_unload_dialog_timeout_.add_timeout_in(d->dialog_id.get(), DIALOG_UNLOAD_DELAY);
  }
}

void MessagesManager::delete_all_dialog_messages(Dialog *d, bool remove_from_dialog_list, bool is_permanent) {
  CHECK(d != nullptr);
  if (is_debug_message_op_enabled()) {
    d->debug_message_op.emplace_back(Dialog::MessageOp::DeleteAll, MessageId(), MessageContentType::None,
                                     remove_from_dialog_list, false, false, "");
  }

  if (d->server_unread_count + d->local_unread_count > 0) {
    MessageId max_message_id =
        d->last_database_message_id.is_valid() ? d->last_database_message_id : d->last_new_message_id;
    if (max_message_id.is_valid()) {
      read_history_inbox(d->dialog_id, max_message_id, -1, "delete_all_dialog_messages");
    }
    if (d->server_unread_count != 0 || d->local_unread_count != 0) {
      set_dialog_last_read_inbox_message_id(d, MessageId::min(), 0, 0, true, "delete_all_dialog_messages");
    }
  }

  if (d->unread_mention_count > 0) {
    d->unread_mention_count = 0;
    d->message_count_by_index[search_messages_filter_index(SearchMessagesFilter::UnreadMention)] = 0;
    send_update_chat_unread_mention_count(d);
  }

  bool has_last_message_id = d->last_message_id != MessageId();
  int32 last_message_date = 0;
  MessageId last_clear_history_message_id;
  if (!remove_from_dialog_list) {
    if (has_last_message_id) {
      auto m = get_message(d, d->last_message_id);
      CHECK(m != nullptr);
      last_message_date = m->date;
      last_clear_history_message_id = d->last_message_id;
    } else {
      last_message_date = d->last_clear_history_date;
      last_clear_history_message_id = d->last_clear_history_message_id;
    }
  }

  vector<int64> deleted_message_ids;
  do_delete_all_dialog_messages(d, d->messages, deleted_message_ids);
  delete_all_dialog_messages_from_database(d, MessageId::max(), "delete_all_dialog_messages");
  if (is_permanent) {
    for (auto id : deleted_message_ids) {
      d->deleted_message_ids.insert(MessageId{id});
    }
  }

  if (d->reply_markup_message_id != MessageId()) {
    set_dialog_reply_markup(d, MessageId());
  }

  set_dialog_first_database_message_id(d, MessageId(), "delete_all_dialog_messages");
  set_dialog_last_database_message_id(d, MessageId(), "delete_all_dialog_messages");
  set_dialog_last_clear_history_date(d, last_message_date, last_clear_history_message_id, "delete_all_dialog_messages");
  d->last_read_all_mentions_message_id = MessageId();                            // it is not needed anymore
  d->message_notification_group.max_removed_notification_id = NotificationId();  // it is not needed anymore
  d->mention_notification_group.max_removed_notification_id = NotificationId();  // it is not needed anymore
  std::fill(d->message_count_by_index.begin(), d->message_count_by_index.end(), 0);
  d->notification_id_to_message_id.clear();

  if (has_last_message_id) {
    set_dialog_last_message_id(d, MessageId(), "delete_all_dialog_messages");
    send_update_chat_last_message(d, "delete_all_dialog_messages");
  }
  if (remove_from_dialog_list && d->pinned_order != DEFAULT_ORDER) {
    set_dialog_is_pinned(d, false);
  }
  update_dialog_pos(d, remove_from_dialog_list, "delete_all_dialog_messages");

  on_dialog_updated(d->dialog_id, "delete_all_dialog_messages");

  send_update_delete_messages(d->dialog_id, std::move(deleted_message_ids), is_permanent, false);
}

void MessagesManager::delete_dialog(DialogId dialog_id) {
  LOG(INFO) << "Delete " << dialog_id;
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return;
  }

  delete_all_dialog_messages(d, true, false);
  if (dialog_id.get_type() != DialogType::SecretChat) {
    d->have_full_history = false;
    d->need_restore_reply_markup = true;
  }
  if (remove_recently_found_dialog_internal(dialog_id)) {
    save_recently_found_dialogs();
  }

  close_dialog(d);
}

void MessagesManager::read_all_dialog_mentions(DialogId dialog_id, Promise<Unit> &&promise) {
  bool is_bot = td_->auth_manager_->is_bot();
  if (is_bot) {
    return promise.set_error(Status::Error(3, "Method is not available for bots"));
  }

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(3, "Chat not found"));
  }

  LOG(INFO) << "Receive readAllChatMentions request in " << dialog_id << " with " << d->unread_mention_count
            << " unread mentions";
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(3, "Chat is not accessible"));
  }
  if (dialog_id.get_type() == DialogType::SecretChat) {
    CHECK(d->unread_mention_count == 0);
    return promise.set_value(Unit());
  }

  if (d->last_new_message_id.get() > d->last_read_all_mentions_message_id.get()) {
    d->last_read_all_mentions_message_id = d->last_new_message_id;
    on_dialog_updated(dialog_id, "read_all_mentions");
  }

  vector<MessageId> message_ids;
  find_unread_mentions(d->messages, message_ids);

  LOG(INFO) << "Found " << message_ids.size() << " messages with unread mentions in memory";
  bool is_update_sent = false;
  for (auto message_id : message_ids) {
    auto m = get_message(d, message_id);
    CHECK(m != nullptr);
    CHECK(m->contains_unread_mention);
    CHECK(m->message_id == message_id);
    remove_message_notification_id(d, m, true);  // should be called before contains_unread_mention is updated
    m->contains_unread_mention = false;

    send_closure(G()->td(), &Td::send_update,
                 make_tl_object<td_api::updateMessageMentionRead>(dialog_id.get(), m->message_id.get(), 0));
    is_update_sent = true;
    on_message_changed(d, m, true, "read_all_mentions");
  }

  if (d->unread_mention_count != 0) {
    d->unread_mention_count = 0;
    d->message_count_by_index[search_messages_filter_index(SearchMessagesFilter::UnreadMention)] = 0;
    if (!is_update_sent) {
      send_update_chat_unread_mention_count(d);
    } else {
      LOG(INFO) << "Update unread mention message count in " << dialog_id << " to " << d->unread_mention_count;
      on_dialog_updated(dialog_id, "read_all_mentions");
    }
  }
  remove_all_dialog_notifications(d, MessageId::max(), d->mention_notification_group, "read_all_dialog_mentions");

  read_all_dialog_mentions_on_server(dialog_id, 0, std::move(promise));
}

class MessagesManager::ReadAllDialogMentionsOnServerLogEvent {
 public:
  DialogId dialog_id_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_id_, parser);
  }
};

uint64 MessagesManager::save_read_all_dialog_mentions_on_server_logevent(DialogId dialog_id) {
  ReadAllDialogMentionsOnServerLogEvent logevent{dialog_id};
  auto storer = LogEventStorerImpl<ReadAllDialogMentionsOnServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ReadAllDialogMentionsOnServer, storer);
}

void MessagesManager::read_all_dialog_mentions_on_server(DialogId dialog_id, uint64 logevent_id,
                                                         Promise<Unit> &&promise) {
  if (logevent_id == 0 && G()->parameters().use_message_db) {
    logevent_id = save_read_all_dialog_mentions_on_server_logevent(dialog_id);
  }

  LOG(INFO) << "Read all mentions on server in " << dialog_id;
  td_->create_handler<ReadAllMentionsQuery>(get_erase_logevent_promise(logevent_id, std::move(promise)))
      ->send(dialog_id);
}

void MessagesManager::read_message_content_from_updates(MessageId message_id) {
  if (!message_id.is_valid() || !message_id.is_server()) {
    LOG(ERROR) << "Incoming update tries to read content of " << message_id;
    return;
  }

  Dialog *d = get_dialog_by_message_id(message_id);
  if (d != nullptr) {
    Message *m = get_message(d, message_id);
    CHECK(m != nullptr);
    read_message_content(d, m, false, "read_message_content_from_updates");
  }
}

void MessagesManager::read_channel_message_content_from_updates(Dialog *d, MessageId message_id) {
  CHECK(d != nullptr);
  if (!message_id.is_valid() || !message_id.is_server()) {
    LOG(ERROR) << "Incoming update tries to read content of " << message_id << " in " << d->dialog_id;
    return;
  }

  Message *m = get_message_force(d, message_id);
  if (m != nullptr) {
    read_message_content(d, m, false, "read_channel_message_content_from_updates");
  } else if (message_id.get() > d->last_new_message_id.get()) {
    get_channel_difference(d->dialog_id, d->pts, true, "read_channel_message_content_from_updates");
  }
}

bool MessagesManager::read_message_content(Dialog *d, Message *m, bool is_local_read, const char *source) {
  CHECK(m != nullptr) << source;
  bool is_mention_read = update_message_contains_unread_mention(d, m, false, "read_message_content");
  bool is_content_read =
      update_opened_message_content(m->content.get()) | ttl_on_open(d, m, Time::now(), is_local_read);

  LOG(INFO) << "Read message content of " << m->message_id << " in " << d->dialog_id
            << ": is_mention_read = " << is_mention_read << ", is_content_read = " << is_content_read;
  if (is_mention_read || is_content_read) {
    on_message_changed(d, m, true, "read_message_content");
    if (is_content_read) {
      send_closure(G()->td(), &Td::send_update,
                   make_tl_object<td_api::updateMessageContentOpened>(d->dialog_id.get(), m->message_id.get()));
    }
    return true;
  }
  return false;
}

int32 MessagesManager::calc_new_unread_count_from_last_unread(Dialog *d, MessageId max_message_id, MessageType type) {
  MessagesConstIterator it(d, max_message_id);
  if (*it == nullptr || (*it)->message_id != max_message_id) {
    return -1;
  }

  int32 unread_count = type == MessageType::Server ? d->server_unread_count : d->local_unread_count;
  while (*it != nullptr && (*it)->message_id.get() > d->last_read_inbox_message_id.get()) {
    if (!(*it)->is_outgoing && (*it)->message_id.get_type() == type) {
      unread_count--;
    }
    --it;
  }
  if (*it == nullptr || (*it)->message_id != d->last_read_inbox_message_id) {
    return -1;
  }

  LOG(INFO) << "Found " << unread_count << " unread messages in " << d->dialog_id << " from last unread message";
  return unread_count;
}

int32 MessagesManager::calc_new_unread_count_from_the_end(Dialog *d, MessageId max_message_id, MessageType type,
                                                          int32 hint_unread_count) {
  int32 unread_count = 0;
  MessagesConstIterator it(d, MessageId::max());
  while (*it != nullptr && (*it)->message_id.get() > max_message_id.get()) {
    if (!(*it)->is_outgoing && (*it)->message_id.get_type() == type) {
      unread_count++;
    }
    --it;
  }

  bool is_count_exact = d->last_message_id.is_valid() && *it != nullptr;
  if (hint_unread_count >= 0) {
    if (is_count_exact) {
      if (hint_unread_count == unread_count) {
        return hint_unread_count;
      }
    } else {
      if (hint_unread_count >= unread_count) {
        return hint_unread_count;
      }
    }

    // hint_unread_count is definitely wrong, ignore it
    LOG(ERROR) << "Receive hint_unread_count = " << hint_unread_count << ", but found " << unread_count
               << " unread messages in " << d->dialog_id;
  }

  if (!is_count_exact) {
    // unread count is likely to be calculated wrong, so ignore it
    return -1;
  }

  LOG(INFO) << "Found " << unread_count << " unread messages in " << d->dialog_id << " from the end";
  return unread_count;
}

int32 MessagesManager::calc_new_unread_count(Dialog *d, MessageId max_message_id, MessageType type,
                                             int32 hint_unread_count) {
  if (d->is_empty) {
    return 0;
  }

  if (!d->last_read_inbox_message_id.is_valid()) {
    return calc_new_unread_count_from_the_end(d, max_message_id, type, hint_unread_count);
  }

  if (!d->last_message_id.is_valid() ||
      (d->last_message_id.get() - max_message_id.get() > max_message_id.get() - d->last_read_inbox_message_id.get())) {
    int32 unread_count = calc_new_unread_count_from_last_unread(d, max_message_id, type);
    return unread_count >= 0 ? unread_count
                             : calc_new_unread_count_from_the_end(d, max_message_id, type, hint_unread_count);
  } else {
    int32 unread_count = calc_new_unread_count_from_the_end(d, max_message_id, type, hint_unread_count);
    return unread_count >= 0 ? unread_count : calc_new_unread_count_from_last_unread(d, max_message_id, type);
  }
}

void MessagesManager::repair_server_unread_count(DialogId dialog_id, int32 unread_count) {
  LOG(INFO) << "Repair server unread count in " << dialog_id << " from " << unread_count;
  create_actor<SleepActor>("RepairServerUnreadCountSleepActor", 0.2,
                           PromiseCreator::lambda([actor_id = actor_id(this), dialog_id](Result<Unit> result) {
                             send_closure(actor_id, &MessagesManager::send_get_dialog_query, dialog_id, Promise<Unit>(),
                                          0);
                           }))
      .release();
}

void MessagesManager::repair_channel_server_unread_count(Dialog *d) {
  CHECK(d != nullptr);
  CHECK(d->dialog_id.get_type() == DialogType::Channel);

  if (d->last_read_inbox_message_id.get() >= d->last_new_message_id.get()) {
    // all messages are already read
    return;
  }

  LOG(INFO) << "Reload ChannelFull for " << d->dialog_id << " to repair unread message counts";
  td_->contacts_manager_->get_channel_full(d->dialog_id.get_channel_id(), Auto());
}

void MessagesManager::read_history_inbox(DialogId dialog_id, MessageId max_message_id, int32 unread_count,
                                         const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  Dialog *d = get_dialog_force(dialog_id);
  if (d != nullptr) {
    if (unread_count < 0) {
      if (!max_message_id.is_valid()) {
        LOG(ERROR) << "Receive read inbox update in " << dialog_id << " up to " << max_message_id << " from " << source;
        return;
      }
    } else {
      if (!max_message_id.is_valid() && max_message_id != MessageId()) {
        LOG(ERROR) << "Receive read inbox history in " << dialog_id << " up to " << max_message_id << " from "
                   << source;
        return;
      }
    }
    if (d->is_last_read_inbox_message_id_inited && max_message_id.get() <= d->last_read_inbox_message_id.get()) {
      LOG(INFO) << "Receive read inbox update in " << dialog_id << " up to " << max_message_id << " from " << source
                << ", but all messages have already been read up to " << d->last_read_inbox_message_id;
      return;
    }

    if (max_message_id != MessageId() && max_message_id.is_yet_unsent()) {
      LOG(ERROR) << "Try to update last read inbox message in " << dialog_id << " with " << max_message_id << " from "
                 << source;
      return;
    }

    if (unread_count > 0 && max_message_id.get() >= d->last_new_message_id.get() &&
        max_message_id.get() >= d->last_message_id.get() && max_message_id.get() >= d->last_database_message_id.get()) {
      LOG(INFO) << "Have unknown " << unread_count << " unread messages in " << dialog_id;
      unread_count = 0;
    }

    LOG_IF(INFO, d->last_new_message_id.is_valid() && max_message_id.get() > d->last_new_message_id.get() &&
                     max_message_id.is_server() && dialog_id.get_type() != DialogType::Channel &&
                     !running_get_difference_)
        << "Receive read inbox update up to unknown " << max_message_id << " in " << dialog_id << " from " << source
        << ". Last new is " << d->last_new_message_id << ", unread_count = " << unread_count
        << ". Possible only for deleted incoming message. " << td_->updates_manager_->get_state();

    if (dialog_id.get_type() == DialogType::SecretChat) {
      ttl_read_history(d, false, max_message_id, d->last_read_inbox_message_id, Time::now());
    }

    bool is_saved_messages = dialog_id == get_my_dialog_id();
    int32 server_unread_count =
        is_saved_messages ? 0 : calc_new_unread_count(d, max_message_id, MessageType::Server, unread_count);
    int32 local_unread_count = d->local_unread_count == 0 || is_saved_messages
                                   ? 0
                                   : calc_new_unread_count(d, max_message_id, MessageType::Local, -1);

    if (server_unread_count < 0) {
      server_unread_count = unread_count >= 0 ? unread_count : d->server_unread_count;
      if (dialog_id.get_type() != DialogType::SecretChat && have_input_peer(dialog_id, AccessRights::Read)) {
        d->need_repair_server_unread_count = true;
        repair_server_unread_count(dialog_id, server_unread_count);
      }
    }
    if (local_unread_count < 0) {
      // TODO repair local unread count
      local_unread_count = d->local_unread_count;
    }

    set_dialog_last_read_inbox_message_id(d, max_message_id, server_unread_count, local_unread_count, true, source);

    if (d->is_marked_as_unread) {
      set_dialog_is_marked_as_unread(d, false);
    }
  } else {
    LOG(INFO) << "Receive read inbox about unknown " << dialog_id << " from " << source;
  }
}

void MessagesManager::read_history_outbox(DialogId dialog_id, MessageId max_message_id, int32 read_date) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  Dialog *d = get_dialog_force(dialog_id);
  if (d != nullptr) {
    if (!max_message_id.is_valid()) {
      LOG(ERROR) << "Receive read outbox update in " << dialog_id << " with " << max_message_id;
      return;
    }
    if (max_message_id.get() <= d->last_read_outbox_message_id.get()) {
      LOG(INFO) << "Receive read outbox update up to " << max_message_id
                << ", but all messages have already been read up to " << d->last_read_outbox_message_id;
      return;
    }

    if (max_message_id.is_yet_unsent()) {
      LOG(ERROR) << "Try to update last read outbox message with " << max_message_id;
      return;
    }

    // it is impossible for just sent outgoing messages because updates are ordered by pts
    LOG_IF(INFO, d->last_new_message_id.is_valid() && max_message_id.get() > d->last_new_message_id.get() &&
                     dialog_id.get_type() != DialogType::Channel)
        << "Receive read outbox update about unknown " << max_message_id << " in " << dialog_id << " with last new "
        << d->last_new_message_id << ". Possible only for deleted outgoing message. "
        << td_->updates_manager_->get_state();

    if (dialog_id.get_type() == DialogType::SecretChat) {
      double server_time = Time::now();
      double read_time = server_time;
      if (read_date <= 0) {
        LOG(ERROR) << "Receive wrong read date " << read_date << " in " << dialog_id;
      } else if (read_date < server_time) {
        read_time = read_date;
      }
      ttl_read_history(d, true, max_message_id, d->last_read_outbox_message_id, read_time);
    }

    set_dialog_last_read_outbox_message_id(d, max_message_id);
  } else {
    LOG(INFO) << "Receive read outbox update about unknown " << dialog_id;
  }
}

bool MessagesManager::need_unread_counter(int64 dialog_order) {
  return dialog_order != DEFAULT_ORDER;
}

void MessagesManager::recalc_unread_count() {
  if (td_->auth_manager_->is_bot() || !need_unread_count_recalc_) {
    return;
  }
  LOG(INFO) << "Recalculate unread counts";
  need_unread_count_recalc_ = false;
  is_message_unread_count_inited_ = true;
  is_dialog_unread_count_inited_ = true;

  int32 total_count = 0;
  int32 muted_count = 0;
  int32 dialog_total_count = 0;
  int32 dialog_muted_count = 0;
  int32 dialog_marked_count = 0;
  int32 dialog_muted_marked_count = 0;
  for (auto &dialog_date : ordered_server_dialogs_) {
    auto dialog_id = dialog_date.get_dialog_id();
    Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr);
    int unread_count = d->server_unread_count + d->local_unread_count;
    if (need_unread_counter(d->order) && (unread_count > 0 || d->is_marked_as_unread)) {
      total_count += unread_count;
      dialog_total_count++;
      if (unread_count == 0 && d->is_marked_as_unread) {
        dialog_marked_count++;
      }

      LOG(DEBUG) << "Have " << unread_count << " messages in " << dialog_id;
      if (is_dialog_muted(d)) {
        muted_count += unread_count;
        dialog_muted_count++;
        if (unread_count == 0 && d->is_marked_as_unread) {
          dialog_muted_marked_count++;
        }
      }
    }
  }

  if (unread_message_total_count_ != total_count || unread_message_muted_count_ != muted_count) {
    unread_message_total_count_ = total_count;
    unread_message_muted_count_ = muted_count;
    send_update_unread_message_count(DialogId(), true, "recalc_unread_count");
  }
  if (unread_dialog_total_count_ != dialog_total_count || unread_dialog_muted_count_ != dialog_muted_count ||
      unread_dialog_marked_count_ != dialog_marked_count ||
      unread_dialog_muted_marked_count_ != dialog_muted_marked_count) {
    unread_dialog_total_count_ = dialog_total_count;
    unread_dialog_muted_count_ = dialog_muted_count;
    unread_dialog_marked_count_ = dialog_marked_count;
    unread_dialog_muted_marked_count_ = dialog_muted_marked_count;
    send_update_unread_chat_count(DialogId(), true, "recalc_unread_count");
  }
}

void MessagesManager::set_dialog_last_read_inbox_message_id(Dialog *d, MessageId message_id, int32 server_unread_count,
                                                            int32 local_unread_count, bool force_update,
                                                            const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  LOG(INFO) << "Update last read inbox message in " << d->dialog_id << " from " << d->last_read_inbox_message_id
            << " to " << message_id << " and update unread message count from " << d->server_unread_count << " + "
            << d->local_unread_count << " to " << server_unread_count << " + " << local_unread_count << " from "
            << source;
  if (message_id != MessageId::min()) {
    d->last_read_inbox_message_id = message_id;
    d->is_last_read_inbox_message_id_inited = true;
  }
  int32 old_unread_count = d->server_unread_count + d->local_unread_count;
  d->server_unread_count = server_unread_count;
  d->local_unread_count = local_unread_count;
  int32 new_unread_count = d->server_unread_count + d->local_unread_count;
  int32 delta = new_unread_count - old_unread_count;
  if (delta != 0 && need_unread_counter(d->order) && is_message_unread_count_inited_) {
    unread_message_total_count_ += delta;
    if (is_dialog_muted(d)) {
      unread_message_muted_count_ += delta;
    }
    send_update_unread_message_count(d->dialog_id, force_update, source);
  }
  delta = static_cast<int32>(new_unread_count != 0) - static_cast<int32>(old_unread_count != 0);
  if (delta != 0 && need_unread_counter(d->order) && is_dialog_unread_count_inited_) {
    if (d->is_marked_as_unread) {
      unread_dialog_marked_count_ -= delta;
    } else {
      unread_dialog_total_count_ += delta;
    }
    if (is_dialog_muted(d)) {
      if (d->is_marked_as_unread) {
        unread_dialog_muted_marked_count_ -= delta;
      } else {
        unread_dialog_muted_count_ += delta;
      }
    }
    send_update_unread_chat_count(d->dialog_id, force_update, source);
  }
  if (message_id != MessageId::min() && d->last_read_inbox_message_id.is_valid() && d->order != DEFAULT_ORDER &&
      d->order != SPONSORED_DIALOG_ORDER) {
    if (d->message_notification_group.group_id.is_valid()) {
      auto total_count = get_dialog_pending_notification_count(d, false);
      if (total_count == 0) {
        set_dialog_last_notification(d->dialog_id, d->message_notification_group, 0, NotificationId(),
                                     "set_dialog_last_read_inbox_message_id");
      }
      total_count -= static_cast<int32>(d->pending_new_message_notifications.size());
      if (total_count < 0) {
        LOG(ERROR) << "Total message notification count is negative in " << d->dialog_id;
        total_count = 0;
      }
      send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification_group,
                         d->message_notification_group.group_id, NotificationId(), d->last_read_inbox_message_id,
                         total_count, Promise<Unit>());
    }

    if (d->mention_notification_group.group_id.is_valid() && d->pinned_message_notification_message_id.is_valid() &&
        d->pinned_message_notification_message_id.get() <= d->last_read_inbox_message_id.get()) {
      // remove pinned message notification when it is read
      set_dialog_pinned_message_notification(d, MessageId());
    }
  }

  send_update_chat_read_inbox(d, force_update, source);
}

void MessagesManager::set_dialog_last_read_outbox_message_id(Dialog *d, MessageId message_id) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  LOG(INFO) << "Update last read outbox message in " << d->dialog_id << " from " << d->last_read_outbox_message_id
            << " to " << message_id;
  d->last_read_outbox_message_id = message_id;
  d->is_last_read_outbox_message_id_inited = true;
  send_update_chat_read_outbox(d);
}

void MessagesManager::set_dialog_max_unavailable_message_id(DialogId dialog_id, MessageId max_unavailable_message_id,
                                                            bool from_update, const char *source) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d != nullptr) {
    if (d->last_new_message_id.is_valid() && max_unavailable_message_id.get() > d->last_new_message_id.get()) {
      LOG(ERROR) << "Tried to set " << dialog_id << " max unavailable message id to " << max_unavailable_message_id
                 << " from " << source << ", but last new message id is " << d->last_new_message_id;
      max_unavailable_message_id = d->last_new_message_id;
    }

    if (d->max_unavailable_message_id == max_unavailable_message_id) {
      return;
    }

    if (max_unavailable_message_id.is_valid() && max_unavailable_message_id.is_yet_unsent()) {
      LOG(ERROR) << "Try to update " << dialog_id << " last read outbox message with " << max_unavailable_message_id
                 << " from " << source;
      return;
    }
    LOG(INFO) << "Set max unavailable message id to " << max_unavailable_message_id << " in " << dialog_id << " from "
              << source;

    on_dialog_updated(dialog_id, "set_dialog_max_unavailable_message_id");

    if (d->max_unavailable_message_id.get() > max_unavailable_message_id.get()) {
      d->max_unavailable_message_id = max_unavailable_message_id;
      return;
    }

    d->max_unavailable_message_id = max_unavailable_message_id;

    vector<MessageId> message_ids;
    find_old_messages(d->messages, max_unavailable_message_id, message_ids);

    vector<int64> deleted_message_ids;
    bool need_update_dialog_pos = false;
    for (auto message_id : message_ids) {
      if (message_id.is_yet_unsent()) {
        continue;
      }

      auto m = get_message(d, message_id);
      CHECK(m != nullptr);
      CHECK(m->message_id.get() <= max_unavailable_message_id.get());
      CHECK(m->message_id == message_id);
      deleted_message_ids.push_back(message_id.get());
      auto p =
          delete_message(d, message_id, !from_update, &need_update_dialog_pos, "set_dialog_max_unavailable_message_id");
      CHECK(p.get() == m);
    }

    if (need_update_dialog_pos) {
      send_update_chat_last_message(d, "set_dialog_max_unavailable_message_id");
    }

    send_update_delete_messages(dialog_id, std::move(deleted_message_ids), !from_update, false);

    if (d->server_unread_count + d->local_unread_count > 0) {
      read_history_inbox(dialog_id, max_unavailable_message_id, -1, "set_dialog_max_unavailable_message_id");
    }
  } else {
    LOG(INFO) << "Receive max unavailable message identifier in unknown " << dialog_id << " from " << source;
  }
}

MessageId MessagesManager::get_message_id(const tl_object_ptr<telegram_api::Message> &message_ptr) {
  int32 constructor_id = message_ptr->get_id();
  switch (constructor_id) {
    case telegram_api::messageEmpty::ID: {
      auto message = static_cast<const telegram_api::messageEmpty *>(message_ptr.get());
      return MessageId(ServerMessageId(message->id_));
    }
    case telegram_api::message::ID: {
      auto message = static_cast<const telegram_api::message *>(message_ptr.get());
      return MessageId(ServerMessageId(message->id_));
    }
    case telegram_api::messageService::ID: {
      auto message = static_cast<const telegram_api::messageService *>(message_ptr.get());
      return MessageId(ServerMessageId(message->id_));
    }
    default:
      UNREACHABLE();
      return MessageId();
  }
}

DialogId MessagesManager::get_message_dialog_id(const tl_object_ptr<telegram_api::Message> &message_ptr) const {
  return get_full_message_id(message_ptr).get_dialog_id();
}

FullMessageId MessagesManager::get_full_message_id(const tl_object_ptr<telegram_api::Message> &message_ptr) const {
  int32 constructor_id = message_ptr->get_id();
  DialogId dialog_id;
  MessageId message_id;
  UserId sender_user_id;
  switch (constructor_id) {
    case telegram_api::messageEmpty::ID: {
      auto message = static_cast<const telegram_api::messageEmpty *>(message_ptr.get());
      LOG(INFO) << "Receive MessageEmpty";
      message_id = MessageId(ServerMessageId(message->id_));
      break;
    }
    case telegram_api::message::ID: {
      auto message = static_cast<const telegram_api::message *>(message_ptr.get());
      dialog_id = DialogId(message->to_id_);
      message_id = MessageId(ServerMessageId(message->id_));
      if (message->flags_ & MESSAGE_FLAG_HAS_FROM_ID) {
        sender_user_id = UserId(message->from_id_);
      }
      break;
    }
    case telegram_api::messageService::ID: {
      auto message = static_cast<const telegram_api::messageService *>(message_ptr.get());
      dialog_id = DialogId(message->to_id_);
      message_id = MessageId(ServerMessageId(message->id_));
      if (message->flags_ & MESSAGE_FLAG_HAS_FROM_ID) {
        sender_user_id = UserId(message->from_id_);
      }
      break;
    }
    default:
      UNREACHABLE();
      break;
  }

  if (dialog_id == get_my_dialog_id()) {
    LOG_IF(ERROR, !sender_user_id.is_valid()) << "Receive invalid " << sender_user_id;
    dialog_id = DialogId(sender_user_id);
  }
  return {dialog_id, message_id};
}

int32 MessagesManager::get_message_date(const tl_object_ptr<telegram_api::Message> &message_ptr) {
  int32 constructor_id = message_ptr->get_id();
  switch (constructor_id) {
    case telegram_api::messageEmpty::ID:
      return 0;
    case telegram_api::message::ID: {
      auto message = static_cast<const telegram_api::message *>(message_ptr.get());
      return message->date_;
    }
    case telegram_api::messageService::ID: {
      auto message = static_cast<const telegram_api::messageService *>(message_ptr.get());
      return message->date_;
    }
    default:
      UNREACHABLE();
      return 0;
  }
}

void MessagesManager::ttl_read_history(Dialog *d, bool is_outgoing, MessageId from_message_id,
                                       MessageId till_message_id, double view_date) {
  // TODO: protect with logevent
  suffix_load_till_message_id(d, till_message_id,
                              PromiseCreator::lambda([actor_id = actor_id(this), dialog_id = d->dialog_id, is_outgoing,
                                                      from_message_id, till_message_id, view_date](Result<Unit>) {
                                send_closure(actor_id, &MessagesManager::ttl_read_history_impl, dialog_id, is_outgoing,
                                             from_message_id, till_message_id, view_date);
                              }));
}

void MessagesManager::ttl_read_history_impl(DialogId dialog_id, bool is_outgoing, MessageId from_message_id,
                                            MessageId till_message_id, double view_date) {
  auto *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  auto now = Time::now();
  for (auto it = MessagesIterator(d, from_message_id); *it && (*it)->message_id.get() >= till_message_id.get(); --it) {
    auto *message = *it;
    if (message->is_outgoing == is_outgoing) {
      ttl_on_view(d, message, view_date, now);
    }
  }
}

void MessagesManager::ttl_on_view(const Dialog *d, Message *message, double view_date, double now) {
  if (message->ttl > 0 && message->ttl_expires_at == 0 && !message->message_id.is_yet_unsent() &&
      !message->is_failed_to_send && !message->is_content_secret) {
    message->ttl_expires_at = message->ttl + view_date;
    ttl_register_message(d->dialog_id, message, now);
    on_message_changed(d, message, true, "ttl_on_view");
  }
}

bool MessagesManager::ttl_on_open(Dialog *d, Message *message, double now, bool is_local_read) {
  if (message->ttl > 0 && message->ttl_expires_at == 0) {
    if (!is_local_read && d->dialog_id.get_type() != DialogType::SecretChat) {
      on_message_ttl_expired(d, message);
    } else {
      message->ttl_expires_at = message->ttl + now;
      ttl_register_message(d->dialog_id, message, now);
    }
    return true;
  }
  return false;
}

void MessagesManager::ttl_register_message(DialogId dialog_id, const Message *message, double now) {
  if (message->ttl_expires_at == 0) {
    return;
  }
  auto it_flag = ttl_nodes_.insert(TtlNode(dialog_id, message->message_id));
  CHECK(it_flag.second);
  auto it = it_flag.first;

  ttl_heap_.insert(message->ttl_expires_at, it->as_heap_node());
  ttl_update_timeout(now);
}

void MessagesManager::ttl_unregister_message(DialogId dialog_id, const Message *message, double now,
                                             const char *source) {
  if (message->ttl_expires_at == 0) {
    return;
  }

  TtlNode ttl_node(dialog_id, message->message_id);
  auto it = ttl_nodes_.find(ttl_node);
  CHECK(it != ttl_nodes_.end()) << dialog_id << " " << message->message_id << " " << source << " " << G()->close_flag();
  auto *heap_node = it->as_heap_node();
  if (heap_node->in_heap()) {
    ttl_heap_.erase(heap_node);
  }
  ttl_nodes_.erase(it);
  ttl_update_timeout(now);
}

void MessagesManager::ttl_loop(double now) {
  std::unordered_map<DialogId, std::vector<MessageId>, DialogIdHash> to_delete;
  while (!ttl_heap_.empty() && ttl_heap_.top_key() < now) {
    auto full_message_id = TtlNode::from_heap_node(ttl_heap_.pop())->full_message_id;
    auto dialog_id = full_message_id.get_dialog_id();
    if (dialog_id.get_type() == DialogType::SecretChat) {
      to_delete[dialog_id].push_back(full_message_id.get_message_id());
    } else {
      auto d = get_dialog(dialog_id);
      CHECK(d != nullptr);
      auto m = get_message(d, full_message_id.get_message_id());
      CHECK(m != nullptr);
      on_message_ttl_expired(d, m);
      on_message_changed(d, m, true, "ttl_loop");
    }
  }
  for (auto &it : to_delete) {
    delete_dialog_messages_from_updates(it.first, it.second);
  }
  ttl_update_timeout(now);
}

void MessagesManager::ttl_update_timeout(double now) {
  if (ttl_heap_.empty()) {
    if (!ttl_slot_.empty()) {
      ttl_slot_.cancel_timeout();
    }
    return;
  }
  ttl_slot_.set_event(EventCreator::yield(actor_id()));
  ttl_slot_.set_timeout_in(ttl_heap_.top_key() - now);
}

void MessagesManager::on_message_ttl_expired(Dialog *d, Message *message) {
  CHECK(d != nullptr);
  CHECK(message != nullptr);
  CHECK(message->ttl > 0);
  CHECK(d->dialog_id.get_type() != DialogType::SecretChat);
  ttl_unregister_message(d->dialog_id, message, Time::now(), "on_message_ttl_expired");
  on_message_ttl_expired_impl(d, message);
  send_update_message_content(d->dialog_id, message->message_id, message->content.get(), message->date,
                              message->is_content_secret, "on_message_ttl_expired");
}

void MessagesManager::on_message_ttl_expired_impl(Dialog *d, Message *message) {
  CHECK(d != nullptr);
  CHECK(message != nullptr);
  CHECK(message->ttl > 0);
  CHECK(d->dialog_id.get_type() != DialogType::SecretChat);
  delete_message_files(message);
  update_expired_message_content(message->content);
  message->ttl = 0;
  message->ttl_expires_at = 0;
  if (message->reply_markup != nullptr) {
    if (message->reply_markup->type != ReplyMarkup::Type::InlineKeyboard) {
      if (!td_->auth_manager_->is_bot()) {
        if (d->reply_markup_message_id == message->message_id) {
          set_dialog_reply_markup(d, MessageId());
        }
      }
      message->had_reply_markup = true;
    }
    message->reply_markup = nullptr;
  }
  remove_message_notification_id(d, message, true);
  update_message_contains_unread_mention(d, message, false, "on_message_ttl_expired_impl");
  message->contains_mention = false;
  message->reply_to_message_id = MessageId();
  message->is_content_secret = false;
}

void MessagesManager::loop() {
  auto token = get_link_token();
  if (token == YieldType::TtlDb) {
    ttl_db_loop(G()->server_time());
  } else {
    ttl_loop(Time::now());
  }
}

void MessagesManager::tear_down() {
  parent_.reset();
}

void MessagesManager::start_up() {
  always_wait_for_mailbox();

  start_time_ = Time::now();

  include_sponsored_dialog_to_unread_count_ =
      G()->shared_config().get_option_boolean("include_sponsored_chat_to_unread_count");

  if (G()->parameters().use_message_db) {
    auto last_database_server_dialog_date_string = G()->td_db()->get_binlog_pmc()->get("last_server_dialog_date");
    if (!last_database_server_dialog_date_string.empty()) {
      string order_str;
      string dialog_id_str;
      std::tie(order_str, dialog_id_str) = split(last_database_server_dialog_date_string);

      auto r_order = to_integer_safe<int64>(order_str);
      auto r_dialog_id = to_integer_safe<int64>(dialog_id_str);
      if (r_order.is_error() || r_dialog_id.is_error()) {
        LOG(ERROR) << "Can't parse " << last_database_server_dialog_date_string;
      } else {
        last_database_server_dialog_date_ = DialogDate(r_order.ok(), DialogId(r_dialog_id.ok()));
      }
    }
    LOG(INFO) << "Load last_database_server_dialog_date_ = " << last_database_server_dialog_date_;

    auto sponsored_dialog_id_string = G()->td_db()->get_binlog_pmc()->get("sponsored_dialog_id");
    if (sponsored_dialog_id_string.empty()) {
      sponsored_dialog_id_string = G()->td_db()->get_binlog_pmc()->get("promoted_dialog_id");
      if (!sponsored_dialog_id_string.empty()) {
        G()->td_db()->get_binlog_pmc()->erase("promoted_dialog_id");
        G()->td_db()->get_binlog_pmc()->set("sponsored_dialog_id", sponsored_dialog_id_string);
      }
    }
    if (!sponsored_dialog_id_string.empty()) {
      auto r_dialog_id = to_integer_safe<int64>(sponsored_dialog_id_string);
      if (r_dialog_id.is_error()) {
        LOG(ERROR) << "Can't parse " << sponsored_dialog_id_string;
      } else {
        sponsored_dialog_id_ = DialogId(r_dialog_id.ok());
        if (!sponsored_dialog_id_.is_valid()) {
          LOG(ERROR) << "Have invalid chat ID " << sponsored_dialog_id_string;
          sponsored_dialog_id_ = DialogId();
        } else {
          Dialog *d = get_dialog_force(sponsored_dialog_id_);
          if (d == nullptr) {
            LOG(ERROR) << "Can't load " << sponsored_dialog_id_;
            sponsored_dialog_id_ = DialogId();
          }
        }
      }
    }

    auto unread_message_count_string = G()->td_db()->get_binlog_pmc()->get("unread_message_count");
    if (!unread_message_count_string.empty()) {
      string total_count;
      string muted_count;
      std::tie(total_count, muted_count) = split(unread_message_count_string);

      auto r_total_count = to_integer_safe<int32>(total_count);
      auto r_muted_count = to_integer_safe<int32>(muted_count);
      if (r_total_count.is_error() || r_muted_count.is_error()) {
        LOG(ERROR) << "Can't parse " << unread_message_count_string;
      } else {
        unread_message_total_count_ = r_total_count.ok();
        unread_message_muted_count_ = r_muted_count.ok();
        is_message_unread_count_inited_ = true;
        send_update_unread_message_count(DialogId(), true, "load unread_message_count");
      }
    }

    auto unread_dialog_count_string = G()->td_db()->get_binlog_pmc()->get("unread_dialog_count");
    if (!unread_dialog_count_string.empty()) {
      auto counts =
          transform(full_split(unread_dialog_count_string), [](Slice str) { return to_integer_safe<int32>(str); });
      if (counts.size() != 4 || std::any_of(counts.begin(), counts.end(), [](auto &c) { return c.is_error(); })) {
        LOG(ERROR) << "Can't parse " << unread_dialog_count_string;
      } else {
        unread_dialog_total_count_ = counts[0].ok();
        unread_dialog_muted_count_ = counts[1].ok();
        unread_dialog_marked_count_ = counts[2].ok();
        unread_dialog_muted_marked_count_ = counts[3].ok();
        is_dialog_unread_count_inited_ = true;
        send_update_unread_chat_count(DialogId(), true, "load unread_dialog_count");
      }
    }

    ttl_db_loop_start(G()->server_time());
  } else {
    G()->td_db()->get_binlog_pmc()->erase("last_server_dialog_date");
    G()->td_db()->get_binlog_pmc()->erase("unread_message_count");
    G()->td_db()->get_binlog_pmc()->erase("unread_dialog_count");
    G()->td_db()->get_binlog_pmc()->erase("promoted_dialog_id");
    G()->td_db()->get_binlog_pmc()->erase("sponsored_dialog_id");
  }

  load_calls_db_state();

  vector<NotificationSettingsScope> scopes{NotificationSettingsScope::Private, NotificationSettingsScope::Group};
  for (auto scope : scopes) {
    auto notification_settings_string =
        G()->td_db()->get_binlog_pmc()->get(get_notification_settings_scope_database_key(scope));
    if (!notification_settings_string.empty()) {
      ScopeNotificationSettings notification_settings;
      log_event_parse(notification_settings, notification_settings_string).ensure();
      if (!notification_settings.is_synchronized) {
        continue;
      }

      auto current_settings = get_scope_notification_settings(scope);
      CHECK(current_settings != nullptr);
      current_settings->mute_until = -1;
      update_scope_notification_settings(scope, current_settings, notification_settings);
    }
  }
  G()->td_db()->get_binlog_pmc()->erase("nsfac");

  /*
  FI LE *f = std::f open("error.txt", "r");
  if (f != nullptr) {
    DialogId dialog_id(ChannelId(123456));
    force_create_dialog(dialog_id, "test");
    Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr);

    delete_all_dialog_messages(d, true, false);

    d->last_new_message_id = MessageId();
    d->last_read_inbox_message_id = MessageId();
    d->last_read_outbox_message_id = MessageId();
    d->is_last_read_inbox_message_id_inited = false;
    d->is_last_read_outbox_message_id_inited = false;

    struct MessageBasicInfo {
      MessageId message_id;
      bool have_previous;
      bool have_next;
    };
    vector<MessageBasicInfo> messages_info;
    std::function<void(Message *m)> get_messages_info = [&](Message *m) {
      if (m == nullptr) {
        return;
      }
      get_messages_info(m->left.get());
      messages_info.push_back(MessageBasicInfo{m->message_id, m->have_previous, m->have_next});
      get_messages_info(m->right.get());
    };

    char buf[1280];
    while (std::f gets(buf, sizeof(buf), f) != nullptr) {
      Slice log_string(buf, std::strlen(buf));
      Slice op = log_string.substr(0, log_string.find(' '));
      if (op != "MessageOpAdd" && op != "MessageOpDelete") {
        LOG(ERROR) << "Unsupported op " << op;
        continue;
      }
      log_string.remove_prefix(log_string.find(' ') + 1);

      if (!begins_with(log_string, "at ")) {
        LOG(ERROR) << "Date expected, found " << log_string;
        continue;
      }
      log_string.remove_prefix(3);
      auto date_slice = log_string.substr(0, log_string.find(' '));
      log_string.remove_prefix(date_slice.size());

      bool is_server = false;
      if (begins_with(log_string, " server message ")) {
        log_string.remove_prefix(16);
        is_server = true;
      } else if (begins_with(log_string, " yet unsent message ")) {
        log_string.remove_prefix(20);
      } else if (begins_with(log_string, " local message ")) {
        log_string.remove_prefix(15);
      } else {
        LOG(ERROR) << "Message id expected, found " << log_string;
        continue;
      }

      auto server_message_id = to_integer<int32>(log_string);
      auto add = 0;
      if (!is_server) {
        log_string.remove_prefix(log_string.find('.') + 1);
        add = to_integer<int32>(log_string);
      }
      log_string.remove_prefix(log_string.find(' ') + 1);

      auto message_id = MessageId((static_cast<int64>(server_message_id) << MessageId::SERVER_ID_SHIFT) + add);

      auto content_type = log_string.substr(0, log_string.find(' '));
      log_string.remove_prefix(log_string.find(' ') + 1);

      auto read_bool = [](Slice &str) {
        if (begins_with(str, "true ")) {
          str.remove_prefix(5);
          return true;
        }
        if (begins_with(str, "false ")) {
          str.remove_prefix(6);
          return false;
        }
        LOG(ERROR) << "Bool expected, found " << str;
        return false;
      };

      bool from_update = read_bool(log_string);
      bool have_previous = read_bool(log_string);
      bool have_next = read_bool(log_string);

      if (op == "MessageOpAdd") {
        auto m = make_unique<Message>();
        m->random_y = get_random_y(message_id);
        m->message_id = message_id;
        m->date = G()->unix_time();
        m->content = create_text_message_content("text", {}, {});

        m->have_previous = have_previous;
        m->have_next = have_next;

        bool need_update = false;
        bool need_update_dialog_pos = false;
        if (add_message_to_dialog(dialog_id, std::move(m), from_update, &need_update, &need_update_dialog_pos,
                                  "Unknown source") == nullptr) {
          LOG(ERROR) << "Can't add message " << message_id;
        }
      } else {
        bool need_update_dialog_pos = false;
        auto m = delete_message(d, message_id, true, &need_update_dialog_pos, "Unknown source");
        CHECK(m != nullptr);
      }

      messages_info.clear();
      get_messages_info(d->messages.get());

      for (size_t i = 0; i + 1 < messages_info.size(); i++) {
        if (messages_info[i].have_next != messages_info[i + 1].have_previous) {
          LOG(ERROR) << messages_info[i].message_id << " has have_next = " << messages_info[i].have_next << ", but "
                     << messages_info[i + 1].message_id
                     << " has have_previous = " << messages_info[i + 1].have_previous;
        }
      }
      if (!messages_info.empty()) {
        if (messages_info.back().have_next != false) {
          LOG(ERROR) << messages_info.back().message_id << " has have_next = true, but there is no next message";
        }
        if (messages_info[0].have_previous != false) {
          LOG(ERROR) << messages_info[0].message_id << " has have_previous = true, but there is no previous message";
        }
      }
    }

    messages_info.clear();
    get_messages_info(d->messages.get());
    for (auto &info : messages_info) {
      bool need_update_dialog_pos = false;
      auto m = delete_message(d, info.message_id, true, &need_update_dialog_pos, "Unknown source");
      CHECK(m != nullptr);
    }

    std::f close(f);
  }
  */
}

void MessagesManager::ttl_db_loop_start(double server_now) {
  ttl_db_expire_from_ = 0;
  ttl_db_expire_till_ = static_cast<int32>(server_now) + 15 /* 15 seconds */;
  ttl_db_has_query_ = false;

  ttl_db_loop(server_now);
}

void MessagesManager::ttl_db_loop(double server_now) {
  LOG(INFO) << "ttl_db: loop " << tag("expire_from", ttl_db_expire_from_) << tag("expire_till", ttl_db_expire_till_)
            << tag("has_query", ttl_db_has_query_);
  if (ttl_db_has_query_) {
    return;
  }

  auto now = static_cast<int32>(server_now);

  if (ttl_db_expire_till_ < 0) {
    LOG(INFO) << "ttl_db: finished";
    return;
  }

  if (now < ttl_db_expire_from_) {
    ttl_db_slot_.set_event(EventCreator::yield(actor_shared(this, YieldType::TtlDb)));
    auto wakeup_in = ttl_db_expire_from_ - server_now;
    ttl_db_slot_.set_timeout_in(wakeup_in);
    LOG(INFO) << "ttl_db: " << tag("wakeup in", wakeup_in);
    return;
  }

  ttl_db_has_query_ = true;
  int32 limit = 50;
  LOG(INFO) << "ttl_db: send query " << tag("expire_from", ttl_db_expire_from_)
            << tag("expire_till", ttl_db_expire_till_) << tag("limit", limit);
  G()->td_db()->get_messages_db_async()->get_expiring_messages(
      ttl_db_expire_from_, ttl_db_expire_till_, limit,
      PromiseCreator::lambda(
          [actor_id = actor_id(this)](Result<std::pair<std::vector<std::pair<DialogId, BufferSlice>>, int32>> result) {
            send_closure(actor_id, &MessagesManager::ttl_db_on_result, std::move(result), false);
          }));
}

void MessagesManager::ttl_db_on_result(Result<std::pair<std::vector<std::pair<DialogId, BufferSlice>>, int32>> r_result,
                                       bool dummy) {
  auto result = r_result.move_as_ok();
  ttl_db_has_query_ = false;
  ttl_db_expire_from_ = ttl_db_expire_till_;
  ttl_db_expire_till_ = result.second;

  LOG(INFO) << "ttl_db: query result " << tag("new expire_till", ttl_db_expire_till_)
            << tag("got messages", result.first.size());
  for (auto &dialog_message : result.first) {
    on_get_message_from_database(dialog_message.first, get_dialog_force(dialog_message.first), dialog_message.second);
  }
  ttl_db_loop(G()->server_time());
}

void MessagesManager::on_send_secret_message_error(int64 random_id, Status error, Promise<> promise) {
  promise.set_value(Unit());  // TODO: set after error is saved
  LOG(INFO) << "Receive error for SecretChatsManager::send_message: " << error;

  auto it = being_sent_messages_.find(random_id);
  if (it != being_sent_messages_.end()) {
    auto full_message_id = it->second;
    auto *message = get_message(full_message_id);
    if (message != nullptr) {
      auto file_id = get_message_content_file_id(message->content.get());
      if (file_id.is_valid()) {
        if (G()->close_flag() && G()->parameters().use_message_db) {
          // do not send error, message will be re-sent
          return;
        }
        if (begins_with(error.message(), "FILE_PART_") && ends_with(error.message(), "_MISSING")) {
          on_send_message_file_part_missing(random_id, to_integer<int32>(error.message().substr(10)));
          return;
        }

        if (error.code() != 429 && error.code() < 500 && !G()->close_flag()) {
          td_->file_manager_->delete_partial_remote_location(file_id);
        }
      }
    }
  }

  on_send_message_fail(random_id, std::move(error));
}

void MessagesManager::on_send_secret_message_success(int64 random_id, MessageId message_id, int32 date,
                                                     tl_object_ptr<telegram_api::EncryptedFile> file_ptr,
                                                     Promise<> promise) {
  promise.set_value(Unit());  // TODO: set after message is saved

  FileId new_file_id;
  if (file_ptr != nullptr && file_ptr->get_id() == telegram_api::encryptedFile::ID) {
    auto file = move_tl_object_as<telegram_api::encryptedFile>(file_ptr);
    if (!DcId::is_valid(file->dc_id_)) {
      LOG(ERROR) << "Wrong dc_id = " << file->dc_id_ << " in file " << to_string(file);
    } else {
      DialogId owner_dialog_id;
      auto it = being_sent_messages_.find(random_id);
      if (it != being_sent_messages_.end()) {
        owner_dialog_id = it->second.get_dialog_id();
      }

      new_file_id = td_->file_manager_->register_remote(
          FullRemoteFileLocation(FileType::Encrypted, file->id_, file->access_hash_, DcId::internal(file->dc_id_)),
          FileLocationSource::FromServer, owner_dialog_id, 0, 0, "");
    }
  }

  on_send_message_success(random_id, message_id, date, new_file_id, "process send_secret_message_success");
}

void MessagesManager::delete_secret_messages(SecretChatId secret_chat_id, std::vector<int64> random_ids,
                                             Promise<> promise) {
  promise.set_value(Unit());  // TODO: set after event is saved
  DialogId dialog_id(secret_chat_id);
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(ERROR) << "Ignore delete secret messages in unknown " << dialog_id;
    return;
  }

  vector<MessageId> to_delete_message_ids;
  for (auto &random_id : random_ids) {
    auto message_id = get_message_id_by_random_id(d, random_id, "delete_secret_messages");
    if (!message_id.is_valid()) {
      continue;
    }
    const Message *m = get_message(d, message_id);
    CHECK(m != nullptr);
    if (!is_service_message_content(m->content->get_type())) {
      to_delete_message_ids.push_back(message_id);
    }
  }
  delete_dialog_messages_from_updates(dialog_id, to_delete_message_ids);
}

void MessagesManager::delete_secret_chat_history(SecretChatId secret_chat_id, MessageId last_message_id,
                                                 Promise<> promise) {
  promise.set_value(Unit());  // TODO: set after event is saved
  auto dialog_id = DialogId(secret_chat_id);
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(ERROR) << "Ignore delete secret chat history in unknown " << dialog_id;
    return;
  }

  // TODO: probably last_message_id is not needed
  delete_all_dialog_messages(d, false, true);
}

void MessagesManager::read_secret_chat_outbox(SecretChatId secret_chat_id, int32 up_to_date, int32 read_date) {
  if (!secret_chat_id.is_valid()) {
    LOG(ERROR) << "Receive read secret chat outbox in the invalid " << secret_chat_id;
    return;
  }
  auto dialog_id = DialogId(secret_chat_id);
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return;
  }
  // TODO: protect with logevent
  suffix_load_till_date(
      d, up_to_date,
      PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, up_to_date, read_date](Result<Unit> result) {
        send_closure(actor_id, &MessagesManager::read_secret_chat_outbox_inner, dialog_id, up_to_date, read_date);
      }));
}

void MessagesManager::read_secret_chat_outbox_inner(DialogId dialog_id, int32 up_to_date, int32 read_date) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  auto end = MessagesConstIterator(d, MessageId::max());
  while (*end && (*end)->date > up_to_date) {
    --end;
  }
  if (!*end) {
    LOG(INFO) << "Ignore read_secret_chat_outbox in " << dialog_id << " at " << up_to_date
              << ": no messages with such date are known";
    return;
  }
  auto max_message_id = (*end)->message_id;
  read_history_outbox(dialog_id, max_message_id, read_date);
}

void MessagesManager::open_secret_message(SecretChatId secret_chat_id, int64 random_id, Promise<> promise) {
  promise.set_value(Unit());  // TODO: set after event is saved
  DialogId dialog_id(secret_chat_id);
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(ERROR) << "Ignore opening secret chat message in unknown " << dialog_id;
    return;
  }

  auto message_id = get_message_id_by_random_id(d, random_id, "open_secret_message");
  if (!message_id.is_valid()) {
    return;
  }
  Message *m = get_message(d, message_id);
  CHECK(m != nullptr);
  if (message_id.is_yet_unsent() || m->is_failed_to_send || !m->is_outgoing) {
    LOG(ERROR) << "Peer has opened wrong " << message_id << " in " << dialog_id;
    return;
  }

  read_message_content(d, m, false, "open_secret_message");
}

void MessagesManager::on_update_secret_chat_state(SecretChatId secret_chat_id, SecretChatState state) {
  if (state == SecretChatState::Closed) {
    DialogId dialog_id(secret_chat_id);
    Dialog *d = get_dialog_force(dialog_id);
    if (d != nullptr) {
      if (d->new_secret_chat_notification_id.is_valid()) {
        remove_new_secret_chat_notification(d, true);
      }
      if (d->message_notification_group.group_id.is_valid() && get_dialog_pending_notification_count(d, false) == 0) {
        d->message_notification_group.last_notification_id = NotificationId();
        d->message_notification_group.last_notification_date = 0;
        d->message_notification_group.try_reuse = true;
        d->message_notification_group.is_changed = true;
        on_dialog_updated(d->dialog_id, "on_update_secret_chat_state");
      }
      CHECK(!d->mention_notification_group.group_id.is_valid());  // there can't be unread mentions in secret chats
    }
  }
}

void MessagesManager::on_get_secret_message(SecretChatId secret_chat_id, UserId user_id, MessageId message_id,
                                            int32 date, tl_object_ptr<telegram_api::encryptedFile> file,
                                            tl_object_ptr<secret_api::decryptedMessage> message, Promise<> promise) {
  LOG(DEBUG) << "On get " << to_string(message);
  CHECK(message != nullptr);
  CHECK(secret_chat_id.is_valid());
  CHECK(user_id.is_valid());
  CHECK(message_id.is_valid());
  CHECK(date > 0);

  auto pending_secret_message = make_unique<PendingSecretMessage>();
  pending_secret_message->success_promise = std::move(promise);
  MessageInfo &message_info = pending_secret_message->message_info;
  message_info.dialog_id = DialogId(secret_chat_id);
  message_info.message_id = message_id;
  message_info.sender_user_id = user_id;
  message_info.date = date;
  message_info.random_id = message->random_id_;
  message_info.ttl = message->ttl_;

  Dialog *d = get_dialog_force(message_info.dialog_id);
  if (d == nullptr) {
    LOG(ERROR) << "Ignore secret message in unknown " << message_info.dialog_id;
    pending_secret_message->success_promise.set_error(Status::Error(500, "Chat not found"));
    return;
  }

  pending_secret_message->load_data_multipromise.add_promise(Auto());
  auto lock_promise = pending_secret_message->load_data_multipromise.get_promise();

  int32 flags = MESSAGE_FLAG_HAS_UNREAD_CONTENT | MESSAGE_FLAG_HAS_FROM_ID;
  if ((message->flags_ & secret_api::decryptedMessage::REPLY_TO_RANDOM_ID_MASK) != 0) {
    message_info.reply_to_message_id = get_message_id_by_random_id(
        get_dialog(message_info.dialog_id), message->reply_to_random_id_, "on_get_secret_message");
    if (message_info.reply_to_message_id.is_valid()) {
      flags |= MESSAGE_FLAG_IS_REPLY;
    }
  }
  if ((message->flags_ & secret_api::decryptedMessage::ENTITIES_MASK) != 0) {
    flags |= MESSAGE_FLAG_HAS_ENTITIES;
  }
  if ((message->flags_ & secret_api::decryptedMessage::MEDIA_MASK) != 0) {
    flags |= MESSAGE_FLAG_HAS_MEDIA;
  }

  if (!clean_input_string(message->via_bot_name_)) {
    LOG(WARNING) << "Receive invalid bot username " << message->via_bot_name_;
    message->via_bot_name_.clear();
  }
  if ((message->flags_ & secret_api::decryptedMessage::VIA_BOT_NAME_MASK) != 0 && !message->via_bot_name_.empty()) {
    pending_secret_message->load_data_multipromise.add_promise(
        PromiseCreator::lambda([this, via_bot_name = message->via_bot_name_, &flags = message_info.flags,
                                &via_bot_user_id = message_info.via_bot_user_id](Unit) mutable {
          auto dialog_id = resolve_dialog_username(via_bot_name);
          if (dialog_id.is_valid() && dialog_id.get_type() == DialogType::User) {
            flags |= MESSAGE_FLAG_IS_SENT_VIA_BOT;
            via_bot_user_id = dialog_id.get_user_id();
          }
        }));
    search_public_dialog(message->via_bot_name_, false, pending_secret_message->load_data_multipromise.get_promise());
  }
  if ((message->flags_ & secret_api::decryptedMessage::GROUPED_ID_MASK) != 0 && message->grouped_id_ != 0) {
    message_info.media_album_id = message->grouped_id_;
    flags |= MESSAGE_FLAG_HAS_MEDIA_ALBUM_ID;
  }

  message_info.flags = flags;
  message_info.content = get_secret_message_content(
      td_, std::move(message->message_), std::move(file), std::move(message->media_), std::move(message->entities_),
      message_info.dialog_id, pending_secret_message->load_data_multipromise);

  add_secret_message(std::move(pending_secret_message), std::move(lock_promise));
}

void MessagesManager::on_secret_chat_screenshot_taken(SecretChatId secret_chat_id, UserId user_id, MessageId message_id,
                                                      int32 date, int64 random_id, Promise<> promise) {
  LOG(DEBUG) << "On screenshot taken in " << secret_chat_id;
  CHECK(secret_chat_id.is_valid());
  CHECK(user_id.is_valid());
  CHECK(message_id.is_valid());
  CHECK(date > 0);

  auto pending_secret_message = make_unique<PendingSecretMessage>();
  pending_secret_message->success_promise = std::move(promise);
  MessageInfo &message_info = pending_secret_message->message_info;
  message_info.dialog_id = DialogId(secret_chat_id);
  message_info.message_id = message_id;
  message_info.sender_user_id = user_id;
  message_info.date = date;
  message_info.random_id = random_id;
  message_info.flags = MESSAGE_FLAG_HAS_FROM_ID;
  message_info.content = create_screenshot_taken_message_content();

  Dialog *d = get_dialog_force(message_info.dialog_id);
  if (d == nullptr) {
    LOG(ERROR) << "Ignore secret message in unknown " << message_info.dialog_id;
    pending_secret_message->success_promise.set_error(Status::Error(500, "Chat not found"));
    return;
  }

  add_secret_message(std::move(pending_secret_message));
}

void MessagesManager::on_secret_chat_ttl_changed(SecretChatId secret_chat_id, UserId user_id, MessageId message_id,
                                                 int32 date, int32 ttl, int64 random_id, Promise<> promise) {
  LOG(DEBUG) << "On ttl set in " << secret_chat_id << " to " << ttl;
  CHECK(secret_chat_id.is_valid());
  CHECK(user_id.is_valid());
  CHECK(message_id.is_valid());
  CHECK(date > 0);
  if (ttl < 0) {
    LOG(WARNING) << "Receive wrong ttl = " << ttl;
    promise.set_value(Unit());
    return;
  }

  auto pending_secret_message = make_unique<PendingSecretMessage>();
  pending_secret_message->success_promise = std::move(promise);
  MessageInfo &message_info = pending_secret_message->message_info;
  message_info.dialog_id = DialogId(secret_chat_id);
  message_info.message_id = message_id;
  message_info.sender_user_id = user_id;
  message_info.date = date;
  message_info.random_id = random_id;
  message_info.flags = MESSAGE_FLAG_HAS_FROM_ID;
  message_info.content = create_chat_set_ttl_message_content(ttl);

  Dialog *d = get_dialog_force(message_info.dialog_id);
  if (d == nullptr) {
    LOG(ERROR) << "Ignore secret message in unknown " << message_info.dialog_id;
    pending_secret_message->success_promise.set_error(Status::Error(500, "Chat not found"));
    return;
  }

  add_secret_message(std::move(pending_secret_message));
}

void MessagesManager::add_secret_message(unique_ptr<PendingSecretMessage> pending_secret_message,
                                         Promise<Unit> lock_promise) {
  auto &multipromise = pending_secret_message->load_data_multipromise;
  multipromise.set_ignore_errors(true);
  int64 token = pending_secret_messages_.add(std::move(pending_secret_message));

  multipromise.add_promise(PromiseCreator::lambda([token, actor_id = actor_id(this),
                                                   this](Result<Unit> result) mutable {
    if (result.is_ok()) {  // if we aren't closing
      this->pending_secret_messages_.finish(token, [actor_id](unique_ptr<PendingSecretMessage> pending_secret_message) {
        send_closure_later(actor_id, &MessagesManager::finish_add_secret_message, std::move(pending_secret_message));
      });
    }
  }));

  if (!lock_promise) {
    lock_promise = multipromise.get_promise();
  }
  lock_promise.set_value(Unit());
}

void MessagesManager::finish_add_secret_message(unique_ptr<PendingSecretMessage> pending_secret_message) {
  auto d = get_dialog(pending_secret_message->message_info.dialog_id);
  CHECK(d != nullptr);
  auto random_id = pending_secret_message->message_info.random_id;
  auto message_id = get_message_id_by_random_id(d, random_id, "finish_add_secret_message");
  if (message_id.is_valid()) {
    if (message_id != pending_secret_message->message_info.message_id) {
      LOG(WARNING) << "Ignore duplicate " << pending_secret_message->message_info.message_id
                   << " received earlier with " << message_id << " and random_id " << random_id;
    }
  } else {
    on_get_message(std::move(pending_secret_message->message_info), true, false, true, true,
                   "finish add secret message");
  }
  pending_secret_message->success_promise.set_value(Unit());  // TODO: set after message is saved
}

void MessagesManager::fix_message_info_dialog_id(MessageInfo &message_info) const {
  if (message_info.dialog_id != get_my_dialog_id()) {
    return;
  }

  UserId sender_user_id = message_info.sender_user_id;
  if (!sender_user_id.is_valid()) {
    LOG(ERROR) << "Receive invalid sender user id in private chat";
    return;
  }

  message_info.dialog_id = DialogId(sender_user_id);
  LOG_IF(ERROR, (message_info.flags & MESSAGE_FLAG_IS_OUT) != 0)
      << "Receive message out flag for incoming " << message_info.message_id << " in " << message_info.dialog_id;
}

MessagesManager::MessageInfo MessagesManager::parse_telegram_api_message(
    tl_object_ptr<telegram_api::Message> message_ptr, const char *source) const {
  LOG(DEBUG) << "Receive from " << source << " " << to_string(message_ptr);
  CHECK(message_ptr != nullptr) << source;
  int32 constructor_id = message_ptr->get_id();

  MessageInfo message_info;
  switch (constructor_id) {
    case telegram_api::messageEmpty::ID:
      break;
    case telegram_api::message::ID: {
      auto message = move_tl_object_as<telegram_api::message>(message_ptr);

      message_info.dialog_id = DialogId(message->to_id_);
      message_info.message_id = MessageId(ServerMessageId(message->id_));
      if (message->flags_ & MESSAGE_FLAG_HAS_FROM_ID) {
        message_info.sender_user_id = UserId(message->from_id_);
      }
      message_info.date = message->date_;
      message_info.forward_header = std::move(message->fwd_from_);
      message_info.reply_to_message_id = MessageId(ServerMessageId(
          message->flags_ & MESSAGE_FLAG_IS_REPLY ? message->reply_to_msg_id_ : 0));  // TODO zero init in fetch
      if (message->flags_ & MESSAGE_FLAG_IS_SENT_VIA_BOT) {
        message_info.via_bot_user_id = UserId(message->via_bot_id_);
        if (!message_info.via_bot_user_id.is_valid()) {
          LOG(ERROR) << "Receive invalid " << message_info.via_bot_user_id << " from " << source;
          message_info.via_bot_user_id = UserId();
        }
      }
      if (message->flags_ & MESSAGE_FLAG_HAS_VIEWS) {
        message_info.views = message->views_;
      }
      if (message->flags_ & MESSAGE_FLAG_HAS_EDIT_DATE) {
        message_info.edit_date = message->edit_date_;
      }
      if (message->flags_ & MESSAGE_FLAG_HAS_MEDIA_ALBUM_ID) {
        message_info.media_album_id = message->grouped_id_;
      }
      message_info.flags = message->flags_;
      fix_message_info_dialog_id(message_info);
      bool is_content_read = (message->flags_ & MESSAGE_FLAG_HAS_UNREAD_CONTENT) == 0;
      if (is_message_auto_read(message_info.dialog_id, (message->flags_ & MESSAGE_FLAG_IS_OUT) != 0)) {
        is_content_read = true;
      }
      message_info.content = get_message_content(
          td_,
          get_message_text(td_->contacts_manager_.get(), std::move(message->message_), std::move(message->entities_),
                           message_info.forward_header ? message_info.forward_header->date_ : message_info.date,
                           "parse_telegram_api_message"),
          std::move(message->media_), message_info.dialog_id, is_content_read, message_info.via_bot_user_id,
          &message_info.ttl);
      message_info.reply_markup =
          message->flags_ & MESSAGE_FLAG_HAS_REPLY_MARKUP ? std::move(message->reply_markup_) : nullptr;
      message_info.author_signature = std::move(message->post_author_);
      break;
    }
    case telegram_api::messageService::ID: {
      auto message = move_tl_object_as<telegram_api::messageService>(message_ptr);

      message_info.dialog_id = DialogId(message->to_id_);
      message_info.message_id = MessageId(ServerMessageId(message->id_));
      if (message->flags_ & MESSAGE_FLAG_HAS_FROM_ID) {
        message_info.sender_user_id = UserId(message->from_id_);
      }
      message_info.date = message->date_;
      message_info.flags = message->flags_;
      fix_message_info_dialog_id(message_info);
      MessageId reply_to_message_id = MessageId(ServerMessageId(
          message->flags_ & MESSAGE_FLAG_IS_REPLY ? message->reply_to_msg_id_ : 0));  // TODO zero init in fetch
      message_info.content =
          get_action_message_content(td_, std::move(message->action_), message_info.dialog_id, reply_to_message_id);
      break;
    }
    default:
      UNREACHABLE();
      break;
  }
  return message_info;
}

std::pair<DialogId, unique_ptr<MessagesManager::Message>> MessagesManager::create_message(MessageInfo &&message_info,
                                                                                          bool is_channel_message) {
  DialogId dialog_id = message_info.dialog_id;
  MessageId message_id = message_info.message_id;
  if (!message_id.is_valid() || !dialog_id.is_valid()) {
    if (message_id != MessageId() || dialog_id != DialogId()) {
      LOG(ERROR) << "Receive " << message_id << " in " << dialog_id;
    }
    return {DialogId(), nullptr};
  }
  if (message_id.is_yet_unsent()) {
    LOG(ERROR) << "Receive " << message_id;
    return {DialogId(), nullptr};
  }

  CHECK(message_info.content != nullptr);

  UserId sender_user_id = message_info.sender_user_id;
  if (!sender_user_id.is_valid()) {
    if (!is_broadcast_channel(dialog_id)) {
      LOG(ERROR) << "Invalid " << sender_user_id << " specified to be a sender of the " << message_id << " in "
                 << dialog_id;
      return {DialogId(), nullptr};
    }

    if (sender_user_id != UserId()) {
      LOG(ERROR) << "Receive invalid " << sender_user_id;
      sender_user_id = UserId();
    }
  }

  auto dialog_type = dialog_id.get_type();
  LOG_IF(ERROR, is_channel_message && dialog_type != DialogType::Channel)
      << "is_channel_message is true for message received in the " << dialog_id;

  int32 flags = message_info.flags;
  if (flags & ~(MESSAGE_FLAG_IS_OUT | MESSAGE_FLAG_IS_FORWARDED | MESSAGE_FLAG_IS_REPLY | MESSAGE_FLAG_HAS_MENTION |
                MESSAGE_FLAG_HAS_UNREAD_CONTENT | MESSAGE_FLAG_HAS_REPLY_MARKUP | MESSAGE_FLAG_HAS_ENTITIES |
                MESSAGE_FLAG_HAS_FROM_ID | MESSAGE_FLAG_HAS_MEDIA | MESSAGE_FLAG_HAS_VIEWS |
                MESSAGE_FLAG_IS_SENT_VIA_BOT | MESSAGE_FLAG_IS_SILENT | MESSAGE_FLAG_IS_POST |
                MESSAGE_FLAG_HAS_EDIT_DATE | MESSAGE_FLAG_HAS_AUTHOR_SIGNATURE | MESSAGE_FLAG_HAS_MEDIA_ALBUM_ID)) {
    LOG(ERROR) << "Unsupported message flags = " << flags << " received";
  }

  bool is_outgoing = (flags & MESSAGE_FLAG_IS_OUT) != 0;
  bool is_silent = (flags & MESSAGE_FLAG_IS_SILENT) != 0;
  bool is_channel_post = (flags & MESSAGE_FLAG_IS_POST) != 0;

  UserId my_id = td_->contacts_manager_->get_my_id();
  DialogId my_dialog_id = DialogId(my_id);
  if (dialog_id == my_dialog_id) {
    // dialog_id should be already fixed
    CHECK(sender_user_id == my_id);
  }

  if (sender_user_id.is_valid() && (sender_user_id == my_id && dialog_id != my_dialog_id) != is_outgoing) {
    //    if (content->get_type() != MessageContentType::ChatAddUser) {  // TODO: we have wrong flags for invites via links
    LOG(ERROR) << "Receive wrong message out flag: me is " << my_id << ", message is from " << sender_user_id
               << ", flags = " << flags << " for " << message_id << " in " << dialog_id;
    //    }
    is_outgoing = !is_outgoing;

    if (dialog_type == DialogType::Channel && !running_get_difference_ && !running_get_channel_difference(dialog_id) &&
        get_channel_difference_to_logevent_id_.count(dialog_id) == 0) {
      // it is safer to completely ignore the message and re-get it through getChannelsDifference
      Dialog *d = get_dialog(dialog_id);
      if (d != nullptr) {
        get_channel_difference(dialog_id, d->pts, true, "create_message");
        return {DialogId(), nullptr};
      }
    }
  }

  MessageId reply_to_message_id = message_info.reply_to_message_id;
  if (reply_to_message_id != MessageId() &&
      (!reply_to_message_id.is_valid() || reply_to_message_id.get() >= message_id.get())) {
    LOG(ERROR) << "Receive reply to wrong " << reply_to_message_id << " in " << message_id;
    reply_to_message_id = MessageId();
  }

  UserId via_bot_user_id = message_info.via_bot_user_id;
  if (!via_bot_user_id.is_valid()) {
    via_bot_user_id = UserId();
  }

  int32 date = message_info.date;
  if (date <= 0) {
    LOG(ERROR) << "Wrong date = " << date << " received in " << message_id << " in " << dialog_id;
    date = 1;
  }

  int32 edit_date = message_info.edit_date;
  if (edit_date < 0) {
    LOG(ERROR) << "Wrong edit_date = " << edit_date << " received in " << message_id << " in " << dialog_id;
    edit_date = 0;
  }

  int32 ttl = message_info.ttl;
  bool is_content_secret =
      is_secret_message_content(ttl, message_info.content->get_type());  // should be calculated before TTL is adjusted
  if (ttl < 0) {
    LOG(ERROR) << "Wrong ttl = " << ttl << " received in " << message_id << " in " << dialog_id;
    ttl = 0;
  } else if (ttl > 0) {
    ttl = max(ttl, get_message_content_duration(message_info.content.get(), td_) + 1);
  }

  int32 views = message_info.views;
  if (views < 0) {
    LOG(ERROR) << "Wrong views = " << views << " received in " << message_id << " in " << dialog_id;
    views = 0;
  }

  LOG(INFO) << "Receive " << message_id << " in " << dialog_id << " from " << sender_user_id;

  auto message = make_unique<Message>();
  message->random_y = get_random_y(message_id);
  message->message_id = message_id;
  message->sender_user_id = sender_user_id;
  message->date = date;
  message->ttl = ttl;
  message->edit_date = edit_date;
  message->random_id = message_info.random_id;
  message->forward_info = get_message_forward_info(std::move(message_info.forward_header));
  message->reply_to_message_id = reply_to_message_id;
  message->via_bot_user_id = via_bot_user_id;
  message->author_signature = std::move(message_info.author_signature);
  message->is_outgoing = is_outgoing;
  message->is_channel_post = is_channel_post;
  message->contains_mention =
      !is_outgoing && dialog_type != DialogType::User && (flags & MESSAGE_FLAG_HAS_MENTION) != 0;
  message->contains_unread_mention =
      message_id.is_server() && message->contains_mention && (flags & MESSAGE_FLAG_HAS_UNREAD_CONTENT) != 0 &&
      (dialog_type == DialogType::Chat || (dialog_type == DialogType::Channel && !is_broadcast_channel(dialog_id)));
  message->disable_notification = is_silent;
  message->is_content_secret = is_content_secret;
  message->views = views;
  message->content = std::move(message_info.content);
  message->reply_markup = get_reply_markup(std::move(message_info.reply_markup), td_->auth_manager_->is_bot(), false,
                                           message->contains_mention || dialog_id.get_type() == DialogType::User);

  auto content_type = message->content->get_type();
  if (content_type == MessageContentType::ExpiredPhoto || content_type == MessageContentType::ExpiredVideo) {
    CHECK(message->ttl == 0);  // ttl is ignored/set to 0 if the message has already been expired
    if (message->reply_markup != nullptr) {
      if (message->reply_markup->type != ReplyMarkup::Type::InlineKeyboard) {
        message->had_reply_markup = true;
      }
      message->reply_markup = nullptr;
    }
    message->reply_to_message_id = MessageId();
  }

  if (message_info.media_album_id != 0) {
    if (!is_allowed_media_group_content(content_type)) {
      LOG(ERROR) << "Receive media group id " << message_info.media_album_id << " in " << message_id << " from "
                 << dialog_id << " with content "
                 << oneline(to_string(
                        get_message_content_object(message->content.get(), td_, message->date, is_content_secret)));
    } else {
      message->media_album_id = message_info.media_album_id;
    }
  }

  return {dialog_id, std::move(message)};
}

FullMessageId MessagesManager::on_get_message(tl_object_ptr<telegram_api::Message> message_ptr, bool from_update,
                                              bool is_channel_message, bool have_previous, bool have_next,
                                              const char *source) {
  return on_get_message(parse_telegram_api_message(std::move(message_ptr), source), from_update, is_channel_message,
                        have_previous, have_next, source);
}

FullMessageId MessagesManager::on_get_message(MessageInfo &&message_info, bool from_update, bool is_channel_message,
                                              bool have_previous, bool have_next, const char *source) {
  DialogId dialog_id;
  unique_ptr<Message> new_message;
  std::tie(dialog_id, new_message) = create_message(std::move(message_info), is_channel_message);
  if (new_message == nullptr) {
    return FullMessageId();
  }
  MessageId message_id = new_message->message_id;

  new_message->have_previous = have_previous;
  new_message->have_next = have_next;

  bool need_update = from_update;
  bool need_update_dialog_pos = false;

  FullMessageId full_message_id(dialog_id, message_id);
  auto it = update_message_ids_.find(full_message_id);
  if (it != update_message_ids_.end()) {
    Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr);

    if (!from_update) {
      LOG_IF(ERROR, message_id.get() <= d->last_new_message_id.get())
          << "New " << message_id << " has id less than last_new_message_id = " << d->last_new_message_id;
      LOG(ERROR) << "Ignore " << it->second << " received not through update from " << source << ": "
                 << oneline(to_string(get_message_object(dialog_id, new_message.get())));  // TODO move to INFO
      dump_debug_message_op(d, 3);                                                         // TODO remove
      if (dialog_id.get_type() == DialogType::Channel && have_input_peer(dialog_id, AccessRights::Read)) {
        channel_get_difference_retry_timeout_.add_timeout_in(dialog_id.get(), 0.001);
      }
      return FullMessageId();
    }

    MessageId old_message_id = it->second;

    update_message_ids_.erase(it);

    if (!new_message->is_outgoing && dialog_id != get_my_dialog_id()) {
      // sent message is not from me
      LOG(ERROR) << "Sent in " << dialog_id << " " << message_id << " is sent by " << new_message->sender_user_id;
      return FullMessageId();
    }

    unique_ptr<Message> old_message =
        delete_message(d, old_message_id, false, &need_update_dialog_pos, "add sent message");
    if (old_message == nullptr) {
      // message has already been deleted by the user or sent to inaccessible channel
      // don't need to send update to the user, because the message has already been deleted
      LOG(INFO) << "Delete already deleted sent " << new_message->message_id << " from server";
      delete_messages_from_server(dialog_id, {new_message->message_id}, true, 0, Auto());
      return FullMessageId();
    }

    need_update = false;

    new_message->message_id = old_message_id;
    new_message->random_y = get_random_y(new_message->message_id);
    new_message->have_previous = false;
    new_message->have_next = false;
    update_message(d, old_message, std::move(new_message), true, &need_update_dialog_pos);
    new_message = std::move(old_message);

    new_message->message_id = message_id;
    new_message->random_y = get_random_y(new_message->message_id);
    send_update_message_send_succeeded(d, old_message_id, new_message.get());

    try_add_active_live_location(dialog_id, new_message.get());

    new_message->have_previous = true;
    new_message->have_next = true;
  }

  const Message *m = add_message_to_dialog(dialog_id, std::move(new_message), from_update, &need_update,
                                           &need_update_dialog_pos, source);
  Dialog *d = get_dialog(dialog_id);
  if (m == nullptr) {
    if (need_update_dialog_pos && d != nullptr) {
      send_update_chat_last_message(d, "on_get_message");
    }

    return FullMessageId();
  }

  CHECK(d != nullptr);

  auto pcc_it = pending_created_dialogs_.find(dialog_id);
  if (from_update && pcc_it != pending_created_dialogs_.end()) {
    pcc_it->second.set_value(Unit());

    pending_created_dialogs_.erase(pcc_it);
  }

  if (need_update) {
    send_update_new_message(d, m);
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    auto p = delete_message(d, message_id, false, &need_update_dialog_pos, "get a message in inaccessible chat");
    CHECK(p.get() == m);
    // CHECK(d->messages == nullptr);
    send_update_delete_messages(dialog_id, {message_id.get()}, false, false);
    // don't need to update dialog pos
    return FullMessageId();
  }

  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "on_get_message");
  }

  if (need_update && m->reply_markup != nullptr && m->reply_markup->type != ReplyMarkup::Type::InlineKeyboard &&
      m->reply_markup->is_personal && !td_->auth_manager_->is_bot()) {
    set_dialog_reply_markup(d, message_id);
  }

  return FullMessageId(dialog_id, message_id);
}

void MessagesManager::set_dialog_last_message_id(Dialog *d, MessageId last_message_id, const char *source) {
  LOG(INFO) << "Set " << d->dialog_id << " last message to " << last_message_id << " from " << source;
  d->last_message_id = last_message_id;

  if (!last_message_id.is_valid()) {
    d->suffix_load_first_message_id_ = MessageId();
    d->suffix_load_done_ = false;
  }
  if (last_message_id.is_valid() && d->delete_last_message_date != 0) {
    d->delete_last_message_date = 0;
    d->deleted_last_message_id = MessageId();
    d->is_last_message_deleted_locally = false;
    on_dialog_updated(d->dialog_id, "update_delete_last_message_date");
  }
  if (d->pending_last_message_date != 0) {
    d->pending_last_message_date = 0;
    d->pending_last_message_id = MessageId();
  }
}

void MessagesManager::set_dialog_first_database_message_id(Dialog *d, MessageId first_database_message_id,
                                                           const char *source) {
  LOG(INFO) << "Set " << d->dialog_id << " first database message to " << first_database_message_id << " from "
            << source;
  d->first_database_message_id = first_database_message_id;
  on_dialog_updated(d->dialog_id, "set_dialog_first_database_message_id");
}

void MessagesManager::set_dialog_last_database_message_id(Dialog *d, MessageId last_database_message_id,
                                                          const char *source, bool is_loaded_from_database) {
  LOG(INFO) << "Set " << d->dialog_id << " last database message to " << last_database_message_id << " from " << source;
  d->debug_set_dialog_last_database_message_id = source;
  d->last_database_message_id = last_database_message_id;
  if (!is_loaded_from_database) {
    on_dialog_updated(d->dialog_id, "set_dialog_last_database_message_id");
  }
}

void MessagesManager::set_dialog_last_new_message_id(Dialog *d, MessageId last_new_message_id, const char *source) {
  CHECK(last_new_message_id.get() > d->last_new_message_id.get())
      << last_new_message_id << " " << d->last_new_message_id << " " << source;
  CHECK(d->dialog_id.get_type() == DialogType::SecretChat || last_new_message_id.is_server());
  if (!d->last_new_message_id.is_valid()) {
    delete_all_dialog_messages_from_database(d, MessageId::max(), "set_dialog_last_new_message_id");
    set_dialog_first_database_message_id(d, MessageId(), "set_dialog_last_new_message_id");
    set_dialog_last_database_message_id(d, MessageId(), source);
    if (d->dialog_id.get_type() != DialogType::SecretChat) {
      d->have_full_history = false;
    }
    for (auto &first_message_id : d->first_database_message_id_by_index) {
      first_message_id = last_new_message_id;
    }

    MessagesConstIterator it(d, MessageId::max());
    vector<MessageId> to_delete_message_ids;
    while (*it != nullptr) {
      auto message_id = (*it)->message_id;
      if (message_id.get() <= last_new_message_id.get()) {
        break;
      }
      if (!message_id.is_yet_unsent()) {
        to_delete_message_ids.push_back(message_id);
      }
      --it;
    }
    if (!to_delete_message_ids.empty()) {
      LOG(ERROR) << "Delete " << format::as_array(to_delete_message_ids) << " because of received last new "
                 << last_new_message_id << " in " << d->dialog_id;

      vector<int64> deleted_message_ids;
      bool need_update_dialog_pos = false;
      for (auto message_id : to_delete_message_ids) {
        if (delete_message(d, message_id, false, &need_update_dialog_pos, "set_dialog_last_new_message_id") !=
            nullptr) {
          deleted_message_ids.push_back(message_id.get());
        }
      }
      if (need_update_dialog_pos) {
        send_update_chat_last_message(d, "set_dialog_last_new_message_id");
      }
      send_update_delete_messages(d->dialog_id, std::move(deleted_message_ids), false, false);
    }

    auto last_new_message = get_message(d, last_new_message_id);
    if (last_new_message != nullptr) {
      add_message_to_database(d, last_new_message, "set_dialog_last_new_message_id");
      set_dialog_first_database_message_id(d, last_new_message_id, "set_dialog_last_new_message_id");
      set_dialog_last_database_message_id(d, last_new_message_id, "set_dialog_last_new_message_id");
      try_restore_dialog_reply_markup(d, last_new_message);
    }
  }

  LOG(INFO) << "Set " << d->dialog_id << " last new message to " << last_new_message_id << " from " << source;
  d->last_new_message_id = last_new_message_id;
  on_dialog_updated(d->dialog_id, source);
}

void MessagesManager::set_dialog_last_clear_history_date(Dialog *d, int32 date, MessageId last_clear_history_message_id,
                                                         const char *source, bool is_loaded_from_database) {
  LOG(INFO) << "Set " << d->dialog_id << " last clear history date to " << date << " of "
            << last_clear_history_message_id << " from " << source;
  if (d->last_clear_history_message_id.is_valid()) {
    switch (d->dialog_id.get_type()) {
      case DialogType::User:
      case DialogType::Chat:
        last_clear_history_message_id_to_dialog_id_.erase(d->last_clear_history_message_id);
        break;
      case DialogType::Channel:
      case DialogType::SecretChat:
        // nothing to do
        break;
      case DialogType::None:
      default:
        UNREACHABLE();
    }
  }

  d->last_clear_history_date = date;
  d->last_clear_history_message_id = last_clear_history_message_id;
  if (!is_loaded_from_database) {
    on_dialog_updated(d->dialog_id, "set_dialog_last_clear_history_date");
  }

  if (d->last_clear_history_message_id.is_valid()) {
    switch (d->dialog_id.get_type()) {
      case DialogType::User:
      case DialogType::Chat:
        last_clear_history_message_id_to_dialog_id_[d->last_clear_history_message_id] = d->dialog_id;
        break;
      case DialogType::Channel:
      case DialogType::SecretChat:
        // nothing to do
        break;
      case DialogType::None:
      default:
        UNREACHABLE();
    }
  }
}

void MessagesManager::set_dialog_is_empty(Dialog *d, const char *source) {
  LOG(INFO) << "Set " << d->dialog_id << " is_empty to true from " << source;
  d->is_empty = true;

  if (d->delete_last_message_date != 0) {
    if (d->is_last_message_deleted_locally && d->last_clear_history_date == 0) {
      set_dialog_last_clear_history_date(d, d->delete_last_message_date, d->deleted_last_message_id,
                                         "set_dialog_is_empty");
    }
    d->delete_last_message_date = 0;
    d->deleted_last_message_id = MessageId();
    d->is_last_message_deleted_locally = false;

    on_dialog_updated(d->dialog_id, "set_dialog_is_empty");
  }
  if (d->pending_last_message_date != 0) {
    d->pending_last_message_date = 0;
    d->pending_last_message_id = MessageId();
  }

  update_dialog_pos(d, false, source);
}

void MessagesManager::set_dialog_is_pinned(DialogId dialog_id, bool is_pinned) {
  Dialog *d = get_dialog(dialog_id);
  set_dialog_is_pinned(d, is_pinned);
  update_dialog_pos(d, false, "set_dialog_is_pinned");
}

void MessagesManager::set_dialog_is_pinned(Dialog *d, bool is_pinned) {
  CHECK(d != nullptr);
  bool was_pinned = d->pinned_order != DEFAULT_ORDER;
  d->pinned_order = is_pinned ? get_next_pinned_dialog_order() : DEFAULT_ORDER;
  on_dialog_updated(d->dialog_id, "set_dialog_is_pinned");

  if (is_pinned != was_pinned) {
    LOG(INFO) << "Set " << d->dialog_id << " is pinned to " << is_pinned;
    CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in set_dialog_is_pinned";
    update_dialog_pos(d, false, "set_dialog_is_pinned", false);
    DialogDate dialog_date(d->order, d->dialog_id);
    send_closure(G()->td(), &Td::send_update,
                 make_tl_object<td_api::updateChatIsPinned>(d->dialog_id.get(), is_pinned,
                                                            dialog_date <= last_dialog_date_ ? d->order : 0));
  }
}

void MessagesManager::set_dialog_reply_markup(Dialog *d, MessageId message_id) {
  if (d->reply_markup_message_id != message_id) {
    on_dialog_updated(d->dialog_id, "set_dialog_reply_markup");
  }

  d->need_restore_reply_markup = false;

  if (d->reply_markup_message_id.is_valid() || message_id.is_valid()) {
    CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in set_dialog_reply_markup";
    d->reply_markup_message_id = message_id;
    send_closure(G()->td(), &Td::send_update,
                 make_tl_object<td_api::updateChatReplyMarkup>(d->dialog_id.get(), message_id.get()));
  }
}

void MessagesManager::try_restore_dialog_reply_markup(Dialog *d, const Message *m) {
  if (!d->need_restore_reply_markup || td_->auth_manager_->is_bot()) {
    return;
  }

  if (m->had_reply_markup) {
    LOG(INFO) << "Restore deleted reply markup in " << d->dialog_id;
    set_dialog_reply_markup(d, MessageId());
  } else if (m->reply_markup != nullptr && m->reply_markup->type != ReplyMarkup::Type::InlineKeyboard &&
             m->reply_markup->is_personal) {
    LOG(INFO) << "Restore reply markup in " << d->dialog_id << " to " << m->message_id;
    set_dialog_reply_markup(d, m->message_id);
  }
}

void MessagesManager::set_dialog_pinned_message_notification(Dialog *d, MessageId message_id) {
  auto old_message_id = d->pinned_message_notification_message_id;
  if (!old_message_id.is_valid() && !message_id.is_valid()) {
    return;
  }
  CHECK(old_message_id != message_id);
  VLOG(notifications) << "Change pinned message notification in " << d->dialog_id << " from " << old_message_id
                      << " to " << message_id;
  if (old_message_id.is_valid()) {
    auto m = get_message_force(d, old_message_id);
    if (m != nullptr && m->notification_id.is_valid() && is_message_notification_active(d, m)) {
      remove_message_notification_id(d, m, true);
      on_message_changed(d, m, false, "remove_message_notification_id");
    }
  }
  d->pinned_message_notification_message_id = message_id;
  on_dialog_updated(d->dialog_id, "set_dialog_pinned_message_notification");
}

bool MessagesManager::set_dialog_last_notification(DialogId dialog_id, NotificationGroupInfo &group_info,
                                                   int32 last_notification_date, NotificationId last_notification_id,
                                                   const char *source) {
  if (group_info.last_notification_date != last_notification_date ||
      group_info.last_notification_id != last_notification_id) {
    VLOG(notifications) << "Set " << group_info.group_id << '/' << dialog_id << " last notification to "
                        << last_notification_id << " sent at " << last_notification_date << " from " << source;
    group_info.last_notification_date = last_notification_date;
    group_info.last_notification_id = last_notification_id;
    group_info.is_changed = true;
    on_dialog_updated(dialog_id, "set_dialog_last_notification");
    return true;
  }
  return false;
}

void MessagesManager::on_update_sent_text_message(int64 random_id,
                                                  tl_object_ptr<telegram_api::MessageMedia> message_media,
                                                  vector<tl_object_ptr<telegram_api::MessageEntity>> &&entities) {
  int32 message_media_id = message_media == nullptr ? telegram_api::messageMediaEmpty::ID : message_media->get_id();
  LOG_IF(ERROR, message_media_id != telegram_api::messageMediaWebPage::ID &&
                    message_media_id != telegram_api::messageMediaEmpty::ID)
      << "Receive non web-page media for text message: " << oneline(to_string(message_media));

  auto it = being_sent_messages_.find(random_id);
  if (it == being_sent_messages_.end()) {
    // result of sending message has already been received through getDifference
    return;
  }

  auto dialog_id = it->second.get_dialog_id();
  auto m = get_message_force(it->second);
  if (m == nullptr) {
    // message has already been deleted
    return;
  }

  if (m->content->get_type() != MessageContentType::Text) {
    LOG(ERROR) << "Text message content has been already changed to " << m->content->get_type();
    return;
  }

  const FormattedText *old_message_text = get_message_content_text(m->content.get());
  CHECK(old_message_text != nullptr);
  FormattedText new_message_text =
      get_message_text(td_->contacts_manager_.get(), old_message_text->text, std::move(entities),
                       m->forward_info ? m->forward_info->date : m->date, "on_update_sent_text_message");
  auto new_content = get_message_content(td_, std::move(new_message_text), std::move(message_media), dialog_id,
                                         true /*likely ignored*/, UserId() /*likely ignored*/, nullptr /*ignored*/);
  if (new_content->get_type() != MessageContentType::Text) {
    LOG(ERROR) << "Text message content has changed to " << new_content->get_type();
    return;
  }

  bool need_update = false;
  bool is_content_changed = false;
  merge_message_contents(td_, m->content.get(), new_content.get(), need_message_changed_warning(m), dialog_id, false,
                         is_content_changed, need_update);

  if (is_content_changed || need_update) {
    m->content = std::move(new_content);
    m->is_content_secret = is_secret_message_content(m->ttl, MessageContentType::Text);
  }
  if (need_update) {
    send_update_message_content(dialog_id, m->message_id, m->content.get(), m->date, m->is_content_secret,
                                "on_update_sent_text_message");
  }
}

void MessagesManager::on_update_message_web_page(FullMessageId full_message_id, bool have_web_page) {
  waiting_for_web_page_messages_.erase(full_message_id);
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog(dialog_id);
  if (d == nullptr) {
    LOG(INFO) << "Can't find " << dialog_id;
    // dialog can be not yet added
    return;
  }
  auto message_id = full_message_id.get_message_id();
  Message *message = get_message(d, message_id);
  if (message == nullptr) {
    // message can be already deleted
    return;
  }
  CHECK(message->date > 0);

  MessageContent *content = message->content.get();
  auto old_web_page_id = get_message_content_web_page_id(content);
  if (!old_web_page_id.is_valid()) {
    // webpage has already been received as empty
    LOG_IF(ERROR, have_web_page) << "Receive earlier not received web page";
    return;
  }

  if (!have_web_page) {
    set_message_content_web_page_id(content, WebPageId());
    // don't need to send an update

    on_message_changed(d, message, true, "on_update_message_web_page");
    return;
  }

  send_update_message_content(dialog_id, message_id, content, message->date, message->is_content_secret,
                              "on_update_message_web_page");
}

void MessagesManager::on_get_dialogs(vector<tl_object_ptr<telegram_api::dialog>> &&dialogs, int32 total_count,
                                     vector<tl_object_ptr<telegram_api::Message>> &&messages, Promise<Unit> &&promise) {
  if (td_->updates_manager_->running_get_difference()) {
    LOG(INFO) << "Postpone result of getDialogs";
    pending_on_get_dialogs_.push_back(
        PendingOnGetDialogs{std::move(dialogs), total_count, std::move(messages), std::move(promise)});
    return;
  }
  bool from_dialog_list = total_count >= 0;
  bool from_get_dialog = total_count == -1;
  bool from_pinned_dialog_list = total_count == -2;

  if (from_get_dialog && dialogs.size() == 1) {
    DialogId dialog_id(dialogs[0]->peer_);
    if (running_get_channel_difference(dialog_id)) {
      LOG(INFO) << "Postpone result of channels getDialogs for " << dialog_id;
      pending_channel_on_get_dialogs_.emplace(
          dialog_id, PendingOnGetDialogs{std::move(dialogs), total_count, std::move(messages), std::move(promise)});
      return;
    }
  }

  LOG(INFO) << "Receive " << dialogs.size() << " dialogs out of " << total_count << " in result of GetDialogsQuery";
  std::unordered_map<FullMessageId, DialogDate, FullMessageIdHash> full_message_id_to_dialog_date;
  std::unordered_map<FullMessageId, tl_object_ptr<telegram_api::Message>, FullMessageIdHash> full_message_id_to_message;
  for (auto &message : messages) {
    auto full_message_id = get_full_message_id(message);
    if (from_dialog_list) {
      auto message_date = get_message_date(message);
      int64 order = get_dialog_order(full_message_id.get_message_id(), message_date);
      full_message_id_to_dialog_date.emplace(full_message_id, DialogDate(order, full_message_id.get_dialog_id()));
    }
    full_message_id_to_message[full_message_id] = std::move(message);
  }

  DialogDate max_dialog_date = MIN_DIALOG_DATE;
  for (auto &dialog : dialogs) {
    //    LOG(INFO) << to_string(dialog);
    DialogId dialog_id(dialog->peer_);
    bool has_pts = (dialog->flags_ & DIALOG_FLAG_HAS_PTS) != 0;

    if (!dialog_id.is_valid()) {
      LOG(ERROR) << "Receive wrong " << dialog_id;
      return promise.set_error(Status::Error(500, "Wrong query result returned: receive wrong chat identifier"));
    }
    switch (dialog_id.get_type()) {
      case DialogType::User:
      case DialogType::Chat:
        if (has_pts) {
          LOG(ERROR) << "Receive user or group " << dialog_id << " with pts";
          return promise.set_error(
              Status::Error(500, "Wrong query result returned: receive user or basic group chat with pts"));
        }
        break;
      case DialogType::Channel:
        if (!has_pts) {
          LOG(ERROR) << "Receive channel " << dialog_id << "without pts";
          return promise.set_error(
              Status::Error(500, "Wrong query result returned: receive supergroup chat without pts"));
        }
        break;
      case DialogType::SecretChat:
      case DialogType::None:
      default:
        UNREACHABLE();
        return promise.set_error(Status::Error(500, "UNREACHABLE"));
    }

    if (from_dialog_list) {
      MessageId last_message_id(ServerMessageId(dialog->top_message_));
      if (last_message_id.is_valid()) {
        FullMessageId full_message_id(dialog_id, last_message_id);
        auto it = full_message_id_to_dialog_date.find(full_message_id);
        if (it == full_message_id_to_dialog_date.end()) {
          LOG(ERROR) << "Last " << last_message_id << " in " << dialog_id << " not found";
          return promise.set_error(Status::Error(500, "Wrong query result returned: last message not found"));
        }
        DialogDate dialog_date = it->second;
        CHECK(dialog_date.get_dialog_id() == dialog_id);

        if (max_dialog_date < dialog_date) {
          max_dialog_date = dialog_date;
        }
      } else {
        LOG(ERROR) << "Receive " << last_message_id << " as last chat message";
        continue;
      }
    }
  }

  if (from_dialog_list) {
    if (dialogs.empty()) {
      // if there is no more dialogs on the server
      max_dialog_date = MAX_DIALOG_DATE;
    }
    if (last_server_dialog_date_ < max_dialog_date) {
      last_server_dialog_date_ = max_dialog_date;
      update_last_dialog_date();
    } else {
      LOG(ERROR) << "Last server dialog date didn't increased";
    }
  }
  if (from_pinned_dialog_list) {
    max_dialog_date = DialogDate(get_dialog_order(MessageId(), MIN_PINNED_DIALOG_DATE - 1), DialogId());
    if (last_server_dialog_date_ < max_dialog_date) {
      last_server_dialog_date_ = max_dialog_date;
      update_last_dialog_date();
    }
  }

  vector<DialogId> added_dialog_ids;
  for (auto &dialog : dialogs) {
    MessageId last_message_id(ServerMessageId(dialog->top_message_));
    if (!last_message_id.is_valid() && from_dialog_list) {
      // skip dialogs without messages
      continue;
    }

    DialogId dialog_id(dialog->peer_);
    if (std::find(added_dialog_ids.begin(), added_dialog_ids.end(), dialog_id) != added_dialog_ids.end()) {
      LOG(ERROR) << "Receive " << dialog_id << " twice in result of getChats with total_count = " << total_count;
      continue;
    }
    added_dialog_ids.push_back(dialog_id);
    Dialog *d = get_dialog_force(dialog_id);
    bool need_update_dialog_pos = false;
    being_added_dialog_id_ = dialog_id;
    if (d == nullptr) {
      d = add_dialog(dialog_id);
      need_update_dialog_pos = true;
    } else {
      LOG(INFO) << "Receive already created " << dialog_id;
      CHECK(d->dialog_id == dialog_id);
    }
    bool is_new = d->last_new_message_id == MessageId();

    on_update_dialog_notify_settings(dialog_id, std::move(dialog->notify_settings_));
    if (!d->notification_settings.is_synchronized) {
      LOG(ERROR) << "Failed to synchronize settings in " << dialog_id;
      d->notification_settings.is_synchronized = true;
      on_dialog_updated(dialog_id, "set notification_settings.is_synchronized");
    }

    if (dialog->unread_count_ < 0) {
      LOG(ERROR) << "Receive " << dialog->unread_count_ << " as number of unread messages in " << dialog_id;
      dialog->unread_count_ = 0;
    }
    MessageId read_inbox_max_message_id = MessageId(ServerMessageId(dialog->read_inbox_max_id_));
    if (!read_inbox_max_message_id.is_valid() && read_inbox_max_message_id != MessageId()) {
      LOG(ERROR) << "Receive " << read_inbox_max_message_id << " as last read inbox message in " << dialog_id;
      read_inbox_max_message_id = MessageId();
    }
    MessageId read_outbox_max_message_id = MessageId(ServerMessageId(dialog->read_outbox_max_id_));
    if (!read_outbox_max_message_id.is_valid() && read_outbox_max_message_id != MessageId()) {
      LOG(ERROR) << "Receive " << read_outbox_max_message_id << " as last read outbox message in " << dialog_id;
      read_outbox_max_message_id = MessageId();
    }
    if (dialog->unread_mentions_count_ < 0) {
      LOG(ERROR) << "Receive " << dialog->unread_mentions_count_ << " as number of unread mention messages in "
                 << dialog_id;
      dialog->unread_mentions_count_ = 0;
    }

    need_update_dialog_pos |= update_dialog_draft_message(
        d, get_draft_message(td_->contacts_manager_.get(), std::move(dialog->draft_)), true, false);
    if (is_new) {
      bool has_pts = (dialog->flags_ & DIALOG_FLAG_HAS_PTS) != 0;
      if (last_message_id.is_valid()) {
        FullMessageId full_message_id(dialog_id, last_message_id);
        auto last_message = std::move(full_message_id_to_message[full_message_id]);
        if (last_message == nullptr) {
          LOG(ERROR) << "Last " << full_message_id << " not found";
        } else {
          auto added_full_message_id =
              on_get_message(std::move(last_message), false, has_pts, false, false, "get chats");
          CHECK(d->last_new_message_id == MessageId());
          set_dialog_last_new_message_id(d, last_message_id, "on_get_dialogs");
          if (d->last_new_message_id.get() > d->last_message_id.get() &&
              added_full_message_id.get_message_id().is_valid()) {
            CHECK(added_full_message_id.get_message_id() == d->last_new_message_id);
            set_dialog_last_message_id(d, d->last_new_message_id, "on_get_dialogs");
            send_update_chat_last_message(d, "on_get_dialogs");
          }
        }
      }

      if (has_pts && !running_get_channel_difference(dialog_id)) {
        int32 channel_pts = dialog->pts_;
        LOG_IF(ERROR, channel_pts < d->pts) << "In new " << dialog_id << " pts = " << d->pts
                                            << ", but pts = " << channel_pts << " received in GetChats";
        set_channel_pts(d, channel_pts, "get channel");
      }
    }
    bool is_pinned = (dialog->flags_ & DIALOG_FLAG_IS_PINNED) != 0;
    bool was_pinned = d->pinned_order != DEFAULT_ORDER;
    if (is_pinned != was_pinned) {
      set_dialog_is_pinned(d, is_pinned);
      need_update_dialog_pos = false;
    }
    bool is_marked_as_unread = (dialog->flags_ & telegram_api::dialog::UNREAD_MARK_MASK) != 0;
    if (is_marked_as_unread != d->is_marked_as_unread) {
      set_dialog_is_marked_as_unread(d, is_marked_as_unread);
    }

    if (need_update_dialog_pos) {
      update_dialog_pos(d, false, "on_get_dialogs");
    }

    if (!G()->parameters().use_message_db || is_new || !d->is_last_read_inbox_message_id_inited ||
        d->need_repair_server_unread_count) {
      if (d->need_repair_server_unread_count) {
        LOG(INFO) << "Repaired server unread count in " << dialog_id << " from " << d->last_read_inbox_message_id << "/"
                  << d->server_unread_count << " to " << read_inbox_max_message_id << "/" << dialog->unread_count_;
        d->need_repair_server_unread_count = false;
        on_dialog_updated(dialog_id, "repair dialog server unread count");
      }
      if (d->server_unread_count != dialog->unread_count_ ||
          d->last_read_inbox_message_id.get() < read_inbox_max_message_id.get()) {
        set_dialog_last_read_inbox_message_id(d, read_inbox_max_message_id, dialog->unread_count_,
                                              d->local_unread_count, true, "on_get_dialogs");
      }
      if (!d->is_last_read_inbox_message_id_inited) {
        d->is_last_read_inbox_message_id_inited = true;
        on_dialog_updated(dialog_id, "set is_last_read_inbox_message_id_inited");
      }
    }

    if (!G()->parameters().use_message_db || is_new || !d->is_last_read_outbox_message_id_inited) {
      if (d->last_read_outbox_message_id.get() < read_outbox_max_message_id.get()) {
        set_dialog_last_read_outbox_message_id(d, read_outbox_max_message_id);
      }
      if (!d->is_last_read_outbox_message_id_inited) {
        d->is_last_read_outbox_message_id_inited = true;
        on_dialog_updated(dialog_id, "set is_last_read_outbox_message_id_inited");
      }
    }

    if (!G()->parameters().use_message_db || is_new) {
      if (d->unread_mention_count != dialog->unread_mentions_count_) {
        d->unread_mention_count = dialog->unread_mentions_count_;
        update_dialog_mention_notification_count(d);
        d->message_count_by_index[search_messages_filter_index(SearchMessagesFilter::UnreadMention)] =
            d->unread_mention_count;
        send_update_chat_unread_mention_count(d);
      }
    }
    being_added_dialog_id_ = DialogId();
  }
  if (from_pinned_dialog_list) {
    auto pinned_dialog_ids = remove_secret_chat_dialog_ids(get_pinned_dialogs());
    std::reverse(pinned_dialog_ids.begin(), pinned_dialog_ids.end());
    if (pinned_dialog_ids != added_dialog_ids) {
      LOG(INFO) << "Repair pinned dialogs order from " << format::as_array(pinned_dialog_ids) << " to "
                << format::as_array(added_dialog_ids);

      std::unordered_set<DialogId, DialogIdHash> old_pinned_dialog_ids(pinned_dialog_ids.begin(),
                                                                       pinned_dialog_ids.end());

      auto old_it = pinned_dialog_ids.begin();
      for (auto dialog_id : added_dialog_ids) {
        old_pinned_dialog_ids.erase(dialog_id);
        while (old_it < pinned_dialog_ids.end()) {
          if (*old_it == dialog_id) {
            break;
          }
          old_it++;
        }
        if (old_it < pinned_dialog_ids.end()) {
          // leave dialog where it is
          continue;
        }
        set_dialog_is_pinned(dialog_id, true);
      }
      for (auto dialog_id : old_pinned_dialog_ids) {
        set_dialog_is_pinned(dialog_id, false);
      }
    }
  }
  promise.set_value(Unit());
}

void MessagesManager::dump_debug_message_op(const Dialog *d, int priority) {
  if (!is_debug_message_op_enabled()) {
    return;
  }
  if (d == nullptr) {
    LOG(ERROR) << "Chat not found";
    return;
  }
  static int last_dumped_priority = -1;
  if (priority <= last_dumped_priority) {
    LOG(ERROR) << "Skip dump " << d->dialog_id;
    return;
  }
  last_dumped_priority = priority;

  for (auto &op : d->debug_message_op) {
    switch (op.type) {
      case Dialog::MessageOp::Add: {
        LOG(ERROR) << "MessageOpAdd at " << op.date << " " << op.message_id << " " << op.content_type << " "
                   << op.from_update << " " << op.have_previous << " " << op.have_next << " " << op.source;
        break;
      }
      case Dialog::MessageOp::SetPts: {
        LOG(ERROR) << "MessageOpSetPts at " << op.date << " " << op.pts << " " << op.source;
        break;
      }
      case Dialog::MessageOp::Delete: {
        LOG(ERROR) << "MessageOpDelete at " << op.date << " " << op.message_id << " " << op.content_type << " "
                   << op.from_update << " " << op.have_previous << " " << op.have_next << " " << op.source;
        break;
      }
      case Dialog::MessageOp::DeleteAll: {
        LOG(ERROR) << "MessageOpDeleteAll at " << op.date << " " << op.from_update;
        break;
      }
      default:
        UNREACHABLE();
    }
  }
}

bool MessagesManager::is_message_unload_enabled() const {
  return G()->parameters().use_message_db || td_->auth_manager_->is_bot();
}

bool MessagesManager::can_unload_message(const Dialog *d, const Message *m) const {
  // don't want to unload messages from opened dialogs
  // don't want to unload messages to which there are replies in yet unsent messages
  // don't want to unload messages with pending web pages
  // can't unload from memory last dialog, last database messages, yet unsent messages, being edited media messages and active live locations
  // can't unload messages in dialog with active suffix load query
  FullMessageId full_message_id{d->dialog_id, m->message_id};
  return !d->is_opened && m->message_id != d->last_message_id && m->message_id != d->last_database_message_id &&
         !m->message_id.is_yet_unsent() && active_live_location_full_message_ids_.count(full_message_id) == 0 &&
         replied_by_yet_unsent_messages_.count(full_message_id) == 0 && m->edited_content == nullptr &&
         waiting_for_web_page_messages_.count(full_message_id) == 0 && d->suffix_load_queries_.empty();
}

void MessagesManager::unload_message(Dialog *d, MessageId message_id) {
  bool need_update_dialog_pos = false;
  auto m = do_delete_message(d, message_id, false, true, &need_update_dialog_pos, "unload_message");
  CHECK(!need_update_dialog_pos);
}

unique_ptr<MessagesManager::Message> MessagesManager::delete_message(Dialog *d, MessageId message_id,
                                                                     bool is_permanently_deleted,
                                                                     bool *need_update_dialog_pos, const char *source) {
  return do_delete_message(d, message_id, is_permanently_deleted, false, need_update_dialog_pos, source);
}

void MessagesManager::add_random_id_to_message_id_correspondence(Dialog *d, int64 random_id, MessageId message_id) {
  CHECK(d != nullptr);
  CHECK(d->dialog_id.get_type() == DialogType::SecretChat);
  auto it = d->random_id_to_message_id.find(random_id);
  if (it == d->random_id_to_message_id.end() || it->second.get() < message_id.get()) {
    LOG(INFO) << "Add correspondence from random_id " << random_id << " to " << message_id << " in " << d->dialog_id;
    d->random_id_to_message_id[random_id] = message_id;
  }
}

void MessagesManager::delete_random_id_to_message_id_correspondence(Dialog *d, int64 random_id, MessageId message_id) {
  CHECK(d != nullptr);
  CHECK(d->dialog_id.get_type() == DialogType::SecretChat);
  auto it = d->random_id_to_message_id.find(random_id);
  if (it != d->random_id_to_message_id.end() && it->second == message_id) {
    LOG(INFO) << "Delete correspondence from random_id " << random_id << " to " << message_id << " in " << d->dialog_id;
    d->random_id_to_message_id.erase(it);
  }
}

void MessagesManager::add_notification_id_to_message_id_correspondence(Dialog *d, NotificationId notification_id,
                                                                       MessageId message_id) {
  CHECK(d != nullptr);
  auto it = d->notification_id_to_message_id.find(notification_id);
  if (it == d->notification_id_to_message_id.end()) {
    VLOG(notifications) << "Add correspondence from " << notification_id << " to " << message_id << " in "
                        << d->dialog_id;
    d->notification_id_to_message_id.emplace(notification_id, message_id);
  } else if (it->second.get() != message_id.get()) {
    LOG(ERROR) << "Have duplicated " << notification_id << " in " << d->dialog_id << " in " << message_id << " and "
               << it->second;
    if (it->second.get() < message_id.get()) {
      it->second = message_id;
    }
  }
}

void MessagesManager::delete_notification_id_to_message_id_correspondence(Dialog *d, NotificationId notification_id,
                                                                          MessageId message_id) {
  CHECK(d != nullptr);
  auto it = d->notification_id_to_message_id.find(notification_id);
  if (it != d->notification_id_to_message_id.end() && it->second == message_id) {
    VLOG(notifications) << "Delete correspondence from " << notification_id << " to " << message_id << " in "
                        << d->dialog_id;
    d->notification_id_to_message_id.erase(it);
  } else {
    LOG(ERROR) << "Can't find " << notification_id << " in " << d->dialog_id << " with " << message_id;
  }
}

void MessagesManager::remove_message_notification_id(Dialog *d, Message *m, bool is_permanent) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  if (!m->notification_id.is_valid()) {
    return;
  }

  auto from_mentions = is_from_mention_notification_group(d, m);
  auto &group_info = get_notification_group_info(d, m);
  if (!group_info.group_id.is_valid()) {
    return;
  }

  bool had_active_notification = is_message_notification_active(d, m);

  auto notification_id = m->notification_id;
  VLOG(notifications) << "Remove " << notification_id << " from " << m->message_id << " in " << group_info.group_id
                      << '/' << d->dialog_id << " from database, was_active = " << had_active_notification
                      << ", is_permanent = " << is_permanent;
  delete_notification_id_to_message_id_correspondence(d, notification_id, m->message_id);
  m->notification_id = NotificationId();
  if (group_info.last_notification_id == notification_id) {
    // last notification is deleted, need to find new last notification
    fix_dialog_last_notification_id(d, from_mentions, m->message_id);
  }
  if (is_permanent) {
    if (had_active_notification) {
      send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification, group_info.group_id,
                         notification_id, true, Promise<Unit>());
    }
    // on_message_changed will be called by the caller
    // don't need to call there to not save twice/or to save just deleted message
  } else {
    on_message_changed(d, m, false, "remove_message_notification_id");
  }
}

void MessagesManager::remove_new_secret_chat_notification(Dialog *d, bool is_permanent) {
  CHECK(d != nullptr);
  auto notification_id = d->new_secret_chat_notification_id;
  CHECK(notification_id.is_valid());
  VLOG(notifications) << "Remove " << notification_id << " about new secret " << d->dialog_id;
  d->new_secret_chat_notification_id = NotificationId();
  bool is_fixed = set_dialog_last_notification(d->dialog_id, d->message_notification_group, 0, NotificationId(),
                                               "remove_new_secret_chat_notification");
  CHECK(is_fixed);
  if (is_permanent) {
    CHECK(d->message_notification_group.group_id.is_valid());
    send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification,
                       d->message_notification_group.group_id, notification_id, true, Promise<Unit>());
  }
}

void MessagesManager::fix_dialog_last_notification_id(Dialog *d, bool from_mentions, MessageId message_id) {
  MessagesConstIterator it(d, message_id);
  auto &group_info = from_mentions ? d->mention_notification_group : d->message_notification_group;
  if (*it != nullptr && ((*it)->message_id == message_id || (*it)->have_next)) {
    while (*it != nullptr) {
      const Message *m = *it;
      if (is_from_mention_notification_group(d, m) == from_mentions && m->notification_id.is_valid() &&
          is_message_notification_active(d, m) && m->message_id != message_id) {
        bool is_fixed = set_dialog_last_notification(d->dialog_id, group_info, m->date, m->notification_id,
                                                     "fix_dialog_last_notification_id");
        CHECK(is_fixed);
        return;
      }
      --it;
    }
  }
  if (G()->parameters().use_message_db) {
    do_get_message_notifications_from_database(
        d, from_mentions, group_info.last_notification_id, group_info.last_notification_id, message_id, 1,
        PromiseCreator::lambda(
            [actor_id = actor_id(this), dialog_id = d->dialog_id, from_mentions,
             prev_last_notification_id = group_info.last_notification_id](Result<vector<Notification>> result) {
              send_closure(actor_id, &MessagesManager::do_fix_dialog_last_notification_id, dialog_id, from_mentions,
                           prev_last_notification_id, std::move(result));
            }));
  }
}

void MessagesManager::do_fix_dialog_last_notification_id(DialogId dialog_id, bool from_mentions,
                                                         NotificationId prev_last_notification_id,
                                                         Result<vector<Notification>> result) {
  if (result.is_error()) {
    return;
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  auto &group_info = from_mentions ? d->mention_notification_group : d->message_notification_group;
  VLOG(notifications) << "Receive " << result.ok().size() << " message notifications in " << group_info.group_id << '/'
                      << dialog_id;
  if (group_info.last_notification_id != prev_last_notification_id) {
    // last_notification_id was changed
    return;
  }

  auto notifications = result.move_as_ok();
  CHECK(notifications.size() <= 1);

  int32 last_notification_date = 0;
  NotificationId last_notification_id;
  if (!notifications.empty()) {
    last_notification_date = notifications[0].date;
    last_notification_id = notifications[0].notification_id;
  }

  bool is_fixed = set_dialog_last_notification(dialog_id, group_info, last_notification_date, last_notification_id,
                                               "do_fix_dialog_last_notification_id");
  CHECK(is_fixed);
}

// DO NOT FORGET TO ADD ALL CHANGES OF THIS FUNCTION AS WELL TO do_delete_all_dialog_messages
unique_ptr<MessagesManager::Message> MessagesManager::do_delete_message(Dialog *d, MessageId message_id,
                                                                        bool is_permanently_deleted,
                                                                        bool only_from_memory,
                                                                        bool *need_update_dialog_pos,
                                                                        const char *source) {
  if (!message_id.is_valid()) {
    LOG(ERROR) << "Trying to delete " << message_id << " in " << d->dialog_id << " from " << source;
    return nullptr;
  }

  FullMessageId full_message_id(d->dialog_id, message_id);
  unique_ptr<Message> *v = find_message(&d->messages, message_id);
  if (*v == nullptr) {
    LOG(INFO) << message_id << " is not found in " << d->dialog_id << " to be deleted from " << source;
    if (only_from_memory) {
      return nullptr;
    }

    if (get_message_force(d, message_id) == nullptr) {
      // currently there may be a race between add_message_to_database and get_message_force,
      // so delete a message from database just in case
      delete_message_from_database(d, message_id, nullptr, is_permanently_deleted);

      if (is_permanently_deleted && d->last_clear_history_message_id == message_id) {
        set_dialog_last_clear_history_date(d, 0, MessageId(), "do_delete_message");
        *need_update_dialog_pos = true;
      }

      /*
      can't do this because the message may be never received in the dialog, unread count will became negative
      // if last_read_inbox_message_id is not known, we can't be sure whether unread_count should be decreased or not
      if (message_id.is_valid() && !message_id.is_yet_unsent() && d->is_last_read_inbox_message_id_inited &&
          message_id.get() > d->last_read_inbox_message_id.get() && !td_->auth_manager_->is_bot()) {
        int32 server_unread_count = d->server_unread_count;
        int32 local_unread_count = d->local_unread_count;
        int32 &unread_count = message_id.is_server() ? server_unread_count : local_unread_count;
        if (unread_count == 0) {
          LOG(ERROR) << "Unread count became negative in " << d->dialog_id << " after deletion of " << message_id
                     << ". Last read is " << d->last_read_inbox_message_id;
          dump_debug_message_op(d, 3);
        } else {
          unread_count--;
          set_dialog_last_read_inbox_message_id(d, MessageId::min(), server_unread_count, local_unread_count, false,
                                                source);
        }
      }
      */
      return nullptr;
    }
    v = find_message(&d->messages, message_id);
    CHECK(*v != nullptr);
  }

  const Message *m = v->get();
  if (only_from_memory && !can_unload_message(d, m)) {
    return nullptr;
  }

  if (is_debug_message_op_enabled()) {
    d->debug_message_op.emplace_back(Dialog::MessageOp::Delete, m->message_id, m->content->get_type(), false,
                                     m->have_previous, m->have_next, source);
  }

  bool need_get_history = false;
  if (!only_from_memory) {
    LOG(INFO) << "Deleting " << full_message_id << " with have_previous = " << m->have_previous
              << " and have_next = " << m->have_next << " from " << source;

    delete_message_from_database(d, message_id, m, is_permanently_deleted);

    delete_active_live_location(d->dialog_id, m);

    if (message_id == d->last_message_id) {
      MessagesConstIterator it(d, message_id);
      CHECK(*it == m);
      if ((*it)->have_previous) {
        --it;
        if (*it != nullptr) {
          set_dialog_last_message_id(d, (*it)->message_id, "do_delete_message");
        } else {
          LOG(ERROR) << "have_previous is true, but there is no previous for " << full_message_id << " from " << source;
          dump_debug_message_op(d);
          set_dialog_last_message_id(d, MessageId(), "do_delete_message");
        }
      } else {
        need_get_history = true;
        set_dialog_last_message_id(d, MessageId(), "do_delete_message");
        d->delete_last_message_date = m->date;
        d->deleted_last_message_id = message_id;
        d->is_last_message_deleted_locally = Slice(source) == Slice(DELETE_MESSAGE_USER_REQUEST_SOURCE);
        on_dialog_updated(d->dialog_id, "do delete last message");
      }
      *need_update_dialog_pos = true;
    }

    if (message_id == d->last_database_message_id) {
      MessagesConstIterator it(d, message_id);
      CHECK(*it == m);
      while ((*it)->have_previous) {
        --it;
        if (*it == nullptr || !(*it)->message_id.is_yet_unsent()) {
          break;
        }
      }

      if (*it != nullptr) {
        if (!(*it)->message_id.is_yet_unsent() && (*it)->message_id != d->last_database_message_id) {
          set_dialog_last_database_message_id(d, (*it)->message_id, "do_delete_message");
          if (d->last_database_message_id.get() < d->first_database_message_id.get()) {
            LOG(ERROR) << "Last database " << d->last_database_message_id << " became less than first database "
                       << d->first_database_message_id << " in " << d->dialog_id;
            set_dialog_first_database_message_id(d, d->last_database_message_id, "do_delete_message 2");
          }
        } else {
          need_get_history = true;
        }
      } else {
        LOG(ERROR) << "have_previous is true, but there is no previous";
        dump_debug_message_op(d);
      }
    }
    if (d->last_database_message_id.is_valid()) {
      CHECK(d->first_database_message_id.is_valid());
    } else {
      set_dialog_first_database_message_id(d, MessageId(), "do_delete_message");
    }

    if (message_id == d->suffix_load_first_message_id_) {
      MessagesConstIterator it(d, message_id);
      CHECK(*it == m);
      if ((*it)->have_previous) {
        --it;
        if (*it != nullptr) {
          d->suffix_load_first_message_id_ = (*it)->message_id;
        } else {
          LOG(ERROR) << "have_previous is true, but there is no previous for " << full_message_id << " from " << source;
          dump_debug_message_op(d);
          d->suffix_load_first_message_id_ = MessageId();
          d->suffix_load_done_ = false;
        }
      } else {
        d->suffix_load_first_message_id_ = MessageId();
        d->suffix_load_done_ = false;
      }
    }
  }
  if (only_from_memory && message_id.get() >= d->suffix_load_first_message_id_.get()) {
    d->suffix_load_first_message_id_ = MessageId();
    d->suffix_load_done_ = false;
  }

  if (m->have_previous && (only_from_memory || !m->have_next)) {
    MessagesIterator it(d, message_id);
    CHECK(*it == m);
    --it;
    Message *prev_m = *it;
    if (prev_m != nullptr) {
      prev_m->have_next = false;
    } else {
      LOG(ERROR) << "have_previous is true, but there is no previous for " << full_message_id << " from " << source;
      dump_debug_message_op(d);
    }
  }
  if ((*v)->have_next && (only_from_memory || !(*v)->have_previous)) {
    MessagesIterator it(d, message_id);
    CHECK(*it == m);
    ++it;
    Message *next_m = *it;
    if (next_m != nullptr) {
      next_m->have_previous = false;
    } else {
      LOG(ERROR) << "have_next is true, but there is no next for " << full_message_id << " from " << source;
      dump_debug_message_op(d);
    }
  }

  unique_ptr<Message> result = std::move(*v);
  unique_ptr<Message> left = std::move(result->left);
  unique_ptr<Message> right = std::move(result->right);

  while (left != nullptr || right != nullptr) {
    if (left == nullptr || (right != nullptr && right->random_y > left->random_y)) {
      *v = std::move(right);
      v = &((*v)->left);
      right = std::move(*v);
    } else {
      *v = std::move(left);
      v = &((*v)->right);
      left = std::move(*v);
    }
  }
  CHECK(*v == nullptr);

  if (!only_from_memory) {
    if (message_id.is_yet_unsent()) {
      cancel_send_message_query(d->dialog_id, result);
    } else {
      cancel_edit_message_media(d->dialog_id, result.get());
    }

    if (need_get_history && !td_->auth_manager_->is_bot() && have_input_peer(d->dialog_id, AccessRights::Read)) {
      get_history_from_the_end(d->dialog_id, true, false, Auto());
    }

    if (d->reply_markup_message_id == message_id) {
      set_dialog_reply_markup(d, MessageId());
    }
    // if last_read_inbox_message_id is not known, we can't be sure whether unread_count should be decreased or not
    if (!result->is_outgoing && message_id.get() > d->last_read_inbox_message_id.get() &&
        d->dialog_id != get_my_dialog_id() && d->is_last_read_inbox_message_id_inited &&
        !td_->auth_manager_->is_bot()) {
      int32 server_unread_count = d->server_unread_count;
      int32 local_unread_count = d->local_unread_count;
      int32 &unread_count = message_id.is_server() ? server_unread_count : local_unread_count;
      if (unread_count == 0) {
        LOG(ERROR) << "Unread count became negative in " << d->dialog_id << " after deletion of " << message_id
                   << ". Last read is " << d->last_read_inbox_message_id;
        dump_debug_message_op(d, 3);
      } else {
        unread_count--;
        set_dialog_last_read_inbox_message_id(d, MessageId::min(), server_unread_count, local_unread_count, false,
                                              source);
      }
    }
    if (result->contains_unread_mention) {
      if (d->unread_mention_count == 0) {
        LOG_IF(ERROR,
               d->message_count_by_index[search_messages_filter_index(SearchMessagesFilter::UnreadMention)] != -1)
            << "Unread mention count became negative in " << d->dialog_id << " after deletion of " << message_id;
      } else {
        d->unread_mention_count--;
        d->message_count_by_index[search_messages_filter_index(SearchMessagesFilter::UnreadMention)] =
            d->unread_mention_count;
        send_update_chat_unread_mention_count(d);
      }
    }

    update_message_count_by_index(d, -1, result.get());
  }

  switch (d->dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
      message_id_to_dialog_id_.erase(message_id);
      break;
    case DialogType::Channel:
      // nothing to do
      break;
    case DialogType::SecretChat:
      delete_random_id_to_message_id_correspondence(d, result->random_id, message_id);
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }
  ttl_unregister_message(d->dialog_id, result.get(), Time::now(), "do_delete_message");
  if (result->notification_id.is_valid()) {
    delete_notification_id_to_message_id_correspondence(d, result->notification_id, message_id);
  }

  return result;
}

void MessagesManager::do_delete_all_dialog_messages(Dialog *d, unique_ptr<Message> &m,
                                                    vector<int64> &deleted_message_ids) {
  if (m == nullptr) {
    return;
  }
  MessageId message_id = m->message_id;

  if (is_debug_message_op_enabled()) {
    d->debug_message_op.emplace_back(Dialog::MessageOp::Delete, m->message_id, m->content->get_type(), false,
                                     m->have_previous, m->have_next, "delete all messages");
  }

  LOG(INFO) << "Delete " << message_id;
  deleted_message_ids.push_back(message_id.get());

  do_delete_all_dialog_messages(d, m->right, deleted_message_ids);
  do_delete_all_dialog_messages(d, m->left, deleted_message_ids);

  delete_active_live_location(d->dialog_id, m.get());

  if (message_id.is_yet_unsent()) {
    cancel_send_message_query(d->dialog_id, m);
  } else {
    cancel_edit_message_media(d->dialog_id, m.get());
  }

  switch (d->dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
      message_id_to_dialog_id_.erase(message_id);
      break;
    case DialogType::Channel:
      // nothing to do
      break;
    case DialogType::SecretChat:
      delete_random_id_to_message_id_correspondence(d, m->random_id, message_id);
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }
  ttl_unregister_message(d->dialog_id, m.get(), Time::now(), "do_delete_all_dialog_messages");

  m = nullptr;
}

bool MessagesManager::have_dialog(DialogId dialog_id) const {
  return dialogs_.count(dialog_id) > 0;
}

void MessagesManager::load_dialogs(vector<DialogId> dialog_ids, Promise<Unit> &&promise) {
  LOG(INFO) << "Load dialogs " << format::as_array(dialog_ids);

  Dependencies dependencies;
  for (auto dialog_id : dialog_ids) {
    if (dialog_id.is_valid() && !have_dialog(dialog_id)) {
      add_dialog_dependencies(dependencies, dialog_id);
    }
  }
  resolve_dependencies_force(dependencies);

  for (auto dialog_id : dialog_ids) {
    if (dialog_id.is_valid()) {
      force_create_dialog(dialog_id, "load_dialogs");
    }
  }

  promise.set_value(Unit());
}

bool MessagesManager::load_dialog(DialogId dialog_id, int left_tries, Promise<Unit> &&promise) {
  if (!dialog_id.is_valid()) {
    promise.set_error(Status::Error(6, "Invalid chat identifier"));
    return false;
  }

  if (!have_dialog_force(dialog_id)) {  // TODO remove _force
    if (G()->parameters().use_message_db) {
      //      TODO load dialog from database, DialogLoader
      //      send_closure_later(actor_id(this), &MessagesManager::load_dialog_from_database, dialog_id,
      //      std::move(promise));
      //      return false;
    }
    if (td_->auth_manager_->is_bot()) {
      switch (dialog_id.get_type()) {
        case DialogType::User: {
          auto user_id = dialog_id.get_user_id();
          auto have_user = td_->contacts_manager_->get_user(user_id, left_tries, std::move(promise));
          if (!have_user) {
            return false;
          }
          break;
        }
        case DialogType::Chat: {
          auto have_chat = td_->contacts_manager_->get_chat(dialog_id.get_chat_id(), left_tries, std::move(promise));
          if (!have_chat) {
            return false;
          }
          break;
        }
        case DialogType::Channel: {
          auto have_channel =
              td_->contacts_manager_->get_channel(dialog_id.get_channel_id(), left_tries, std::move(promise));
          if (!have_channel) {
            return false;
          }
          break;
        }
        case DialogType::SecretChat:
          promise.set_error(Status::Error(6, "Chat not found"));
          return false;
        case DialogType::None:
        default:
          UNREACHABLE();
      }
      if (!have_input_peer(dialog_id, AccessRights::Read)) {
        return false;
      }

      add_dialog(dialog_id);
      return true;
    }

    promise.set_error(Status::Error(6, "Chat not found"));
    return false;
  }

  promise.set_value(Unit());
  return true;
}

vector<DialogId> MessagesManager::get_dialogs(DialogDate offset, int32 limit, bool force, Promise<Unit> &&promise) {
  LOG(INFO) << "Get chats with offset " << offset << " and limit " << limit << ". Know about order of "
            << ordered_dialogs_.size() << " chat(s). last_dialog_date_ = " << last_dialog_date_
            << ", last_server_dialog_date_ = " << last_server_dialog_date_
            << ", last_loaded_database_dialog_date_ = " << last_loaded_database_dialog_date_;

  vector<DialogId> result;
  if (limit <= 0) {
    promise.set_error(Status::Error(3, "Parameter limit in getChats must be positive"));
    return result;
  }

  if (limit > MAX_GET_DIALOGS) {
    limit = MAX_GET_DIALOGS;
  }

  auto it = ordered_dialogs_.upper_bound(offset);
  auto end = ordered_dialogs_.end();
  while (it != end && limit-- > 0) {
    result.push_back(it->get_dialog_id());
    ++it;
  }

  if (limit <= 0 || last_dialog_date_ == MAX_DIALOG_DATE || force) {
    promise.set_value(Unit());
    return result;
  }

  load_dialog_list(limit, std::move(promise));
  return result;
}

void MessagesManager::load_dialog_list(int32 limit, Promise<Unit> &&promise) {
  LOG(INFO) << "Load dialog list with limit " << limit;
  auto &multipromise = load_dialog_list_multipromise_;
  multipromise.add_promise(std::move(promise));
  bool use_database =
      G()->parameters().use_message_db && last_loaded_database_dialog_date_ < last_database_server_dialog_date_;
  if (multipromise.promise_count() != 1) {
    // queries have already been sent, just wait for the result
    if (use_database) {
      load_dialog_list_limit_max_ = std::max(load_dialog_list_limit_max_, limit);
    }
    return;
  }

  bool is_query_sent = false;
  if (use_database) {
    load_dialog_list_from_database(limit, multipromise.get_promise());
    is_query_sent = true;
  } else {
    LOG(INFO) << "Get dialogs from " << last_server_dialog_date_;
    auto sequence_id = get_sequence_dispatcher_id(DialogId(), MessageContentType::None);
    send_closure(td_->create_net_actor<GetPinnedDialogsQuery>(multipromise.get_promise()), &GetPinnedDialogsQuery::send,
                 sequence_id);
    if (last_dialog_date_ == last_server_dialog_date_) {
      send_closure(td_->create_net_actor<GetDialogListQuery>(multipromise.get_promise()), &GetDialogListQuery::send,
                   last_server_dialog_date_.get_date(),
                   last_server_dialog_date_.get_message_id().get_next_server_message_id().get_server_message_id(),
                   last_server_dialog_date_.get_dialog_id(), int32{MAX_GET_DIALOGS}, sequence_id);
      is_query_sent = true;
    }
  }
  CHECK(is_query_sent);
}

void MessagesManager::load_dialog_list_from_database(int32 limit, Promise<Unit> &&promise) {
  LOG(INFO) << "Load " << limit << " dialogs from database from " << last_loaded_database_dialog_date_
            << ", last database server dialog date = " << last_database_server_dialog_date_;

  CHECK(load_dialog_list_limit_max_ == 0);
  load_dialog_list_limit_max_ = limit;
  G()->td_db()->get_dialog_db_async()->get_dialogs(
      last_loaded_database_dialog_date_.get_order(), last_loaded_database_dialog_date_.get_dialog_id(), limit,
      PromiseCreator::lambda(
          [actor_id = actor_id(this), limit, promise = std::move(promise)](vector<BufferSlice> result) mutable {
            send_closure(actor_id, &MessagesManager::on_get_dialogs_from_database, limit, std::move(result),
                         std::move(promise));
          }));
}

void MessagesManager::on_get_dialogs_from_database(int32 limit, vector<BufferSlice> &&dialogs,
                                                   Promise<Unit> &&promise) {
  LOG(INFO) << "Receive " << dialogs.size() << " from expected " << limit
            << " dialogs in result of GetDialogsFromDatabase";
  int32 new_get_dialogs_limit = 0;
  int32 have_more_dialogs_in_database = (limit == static_cast<int32>(dialogs.size()));
  if (have_more_dialogs_in_database && limit < load_dialog_list_limit_max_) {
    new_get_dialogs_limit = load_dialog_list_limit_max_ - limit;
  }
  load_dialog_list_limit_max_ = 0;

  DialogDate max_dialog_date = MIN_DIALOG_DATE;
  for (auto &dialog : dialogs) {
    Dialog *d = on_load_dialog_from_database(DialogId(), std::move(dialog));
    if (d == nullptr) {
      continue;
    }

    DialogDate dialog_date(d->order, d->dialog_id);
    if (max_dialog_date < dialog_date) {
      max_dialog_date = dialog_date;
    }
    LOG(INFO) << "Chat " << dialog_date << " is loaded from database";
  }

  if (!have_more_dialogs_in_database) {
    last_loaded_database_dialog_date_ = MAX_DIALOG_DATE;
    LOG(INFO) << "Set last loaded database dialog date to " << last_loaded_database_dialog_date_;
    last_server_dialog_date_ = max(last_server_dialog_date_, last_database_server_dialog_date_);
    LOG(INFO) << "Set last server dialog date to " << last_server_dialog_date_;
    update_last_dialog_date();
  } else if (last_loaded_database_dialog_date_ < max_dialog_date) {
    last_loaded_database_dialog_date_ = min(max_dialog_date, last_database_server_dialog_date_);
    LOG(INFO) << "Set last loaded database dialog date to " << last_loaded_database_dialog_date_;
    last_server_dialog_date_ = max(last_server_dialog_date_, last_loaded_database_dialog_date_);
    LOG(INFO) << "Set last server dialog date to " << last_server_dialog_date_;
    update_last_dialog_date();
  } else {
    LOG(ERROR) << "Last loaded database dialog date didn't increased";
  }

  if (!(last_loaded_database_dialog_date_ < last_database_server_dialog_date_)) {
    have_more_dialogs_in_database = false;
    new_get_dialogs_limit = 0;
  }

  if (new_get_dialogs_limit == 0) {
    if (!preload_dialog_list_timeout_.has_timeout()) {
      LOG(INFO) << "Schedule chat list preload";
      preload_dialog_list_timeout_.set_callback(std::move(MessagesManager::preload_dialog_list));
      preload_dialog_list_timeout_.set_callback_data(static_cast<void *>(this));
    }
    preload_dialog_list_timeout_.set_timeout_in(0.2);

    promise.set_value(Unit());
  } else {
    load_dialog_list_from_database(new_get_dialogs_limit, std::move(promise));
  }
}

void MessagesManager::preload_dialog_list(void *messages_manager_void) {
  if (G()->close_flag()) {
    LOG(INFO) << "Skip chat list preload, because of closing";
    return;
  }

  CHECK(messages_manager_void != nullptr);
  auto messages_manager = static_cast<MessagesManager *>(messages_manager_void);

  CHECK(G()->parameters().use_message_db);
  if (messages_manager->load_dialog_list_multipromise_.promise_count() != 0) {
    LOG(INFO) << "Skip chat list preload, because there is a pending load chat list request";
    return;
  }

  if (messages_manager->ordered_dialogs_.size() > MAX_PRELOADED_DIALOGS) {
    // do nothing if there are more than MAX_PRELOADED_DIALOGS dialogs already loaded
    messages_manager->recalc_unread_count();
    return;
  }

  if (messages_manager->last_loaded_database_dialog_date_ < messages_manager->last_database_server_dialog_date_) {
    // if there are some dialogs in database, preload some of them
    messages_manager->load_dialog_list(20, Auto());
  } else if (messages_manager->last_dialog_date_ != MAX_DIALOG_DATE) {
    // otherwise load more dialogs from the server
    messages_manager->load_dialog_list(MAX_GET_DIALOGS, PromiseCreator::lambda([messages_manager](Result<Unit> result) {
                                         if (result.is_ok()) {
                                           messages_manager->recalc_unread_count();
                                         }
                                       }));
  } else {
    messages_manager->recalc_unread_count();
  }
}

vector<DialogId> MessagesManager::get_pinned_dialogs() const {
  vector<DialogId> result;

  auto it = ordered_dialogs_.begin();
  auto end = ordered_dialogs_.end();
  while (it != end && it->get_date() >= MIN_PINNED_DIALOG_DATE) {
    if (it->get_order() != SPONSORED_DIALOG_ORDER) {
      result.push_back(it->get_dialog_id());
    }
    ++it;
  }

  return result;
}

vector<DialogId> MessagesManager::search_public_dialogs(const string &query, Promise<Unit> &&promise) {
  LOG(INFO) << "Search public chats with query = \"" << query << '"';

  if (utf8_length(query) < MIN_SEARCH_PUBLIC_DIALOG_PREFIX_LEN) {
    string username = clean_username(query);
    if (username[0] == '@') {
      username = username.substr(1);
    }

    for (auto &short_username : get_valid_short_usernames()) {
      if (2 * username.size() > short_username.size() && begins_with(short_username, username)) {
        username = short_username.str();
        auto it = resolved_usernames_.find(username);
        if (it == resolved_usernames_.end()) {
          td_->create_handler<ResolveUsernameQuery>(std::move(promise))->send(username);
          return {};
        }

        if (it->second.expires_at < Time::now()) {
          td_->create_handler<ResolveUsernameQuery>(Promise<>())->send(username);
        }

        auto dialog_id = it->second.dialog_id;
        force_create_dialog(dialog_id, "public dialogs search");
        promise.set_value(Unit());
        return {dialog_id};
      }
    }
    promise.set_value(Unit());
    return {};
  }

  auto it = found_public_dialogs_.find(query);
  if (it != found_public_dialogs_.end()) {
    promise.set_value(Unit());
    return it->second;
  }

  send_search_public_dialogs_query(query, std::move(promise));
  return vector<DialogId>();
}

void MessagesManager::send_search_public_dialogs_query(const string &query, Promise<Unit> &&promise) {
  auto &promises = search_public_dialogs_queries_[query];
  promises.push_back(std::move(promise));
  if (promises.size() != 1) {
    // query has already been sent, just wait for the result
    return;
  }

  td_->create_handler<SearchPublicDialogsQuery>()->send(query);
}

std::pair<size_t, vector<DialogId>> MessagesManager::search_dialogs(const string &query, int32 limit,
                                                                    Promise<Unit> &&promise) {
  LOG(INFO) << "Search chats with query \"" << query << "\" and limit " << limit;

  if (limit < 0) {
    promise.set_error(Status::Error(400, "Limit must be non-negative"));
    return {};
  }
  if (query.empty()) {
    if (!load_recently_found_dialogs(promise)) {
      return {};
    }

    promise.set_value(Unit());
    size_t result_size = min(static_cast<size_t>(limit), recently_found_dialog_ids_.size());
    return {recently_found_dialog_ids_.size(),
            vector<DialogId>(recently_found_dialog_ids_.begin(), recently_found_dialog_ids_.begin() + result_size)};
  }

  auto result = dialogs_hints_.search(query, limit);
  vector<DialogId> dialog_ids;
  dialog_ids.reserve(result.second.size());
  for (auto key : result.second) {
    dialog_ids.push_back(DialogId(-key));
  }

  promise.set_value(Unit());
  return {result.first, std::move(dialog_ids)};
}

vector<DialogId> MessagesManager::sort_dialogs_by_order(const vector<DialogId> &dialog_ids, int32 limit) const {
  auto dialog_dates = transform(dialog_ids, [this](auto dialog_id) {
    const Dialog *d = this->get_dialog(dialog_id);
    CHECK(d != nullptr);
    return DialogDate(d->order, dialog_id);
  });
  if (static_cast<size_t>(limit) >= dialog_dates.size()) {
    std::sort(dialog_dates.begin(), dialog_dates.end());
  } else {
    std::partial_sort(dialog_dates.begin(), dialog_dates.begin() + limit, dialog_dates.end());
    dialog_dates.resize(limit, MAX_DIALOG_DATE);
  }
  while (!dialog_dates.empty() && dialog_dates.back().get_order() == DEFAULT_ORDER) {
    dialog_dates.pop_back();
  }
  return transform(dialog_dates, [](auto dialog_date) { return dialog_date.get_dialog_id(); });
}

vector<DialogId> MessagesManager::search_dialogs_on_server(const string &query, int32 limit, Promise<Unit> &&promise) {
  LOG(INFO) << "Search chats on server with query \"" << query << "\" and limit " << limit;

  if (limit < 0) {
    promise.set_error(Status::Error(400, "Limit must be non-negative"));
    return {};
  }
  if (limit > MAX_GET_DIALOGS) {
    limit = MAX_GET_DIALOGS;
  }

  if (query.empty()) {
    promise.set_value(Unit());
    return {};
  }

  auto it = found_on_server_dialogs_.find(query);
  if (it != found_on_server_dialogs_.end()) {
    promise.set_value(Unit());
    return sort_dialogs_by_order(it->second, limit);
  }

  send_search_public_dialogs_query(query, std::move(promise));
  return vector<DialogId>();
}

vector<DialogId> MessagesManager::get_common_dialogs(UserId user_id, DialogId offset_dialog_id, int32 limit, bool force,
                                                     Promise<Unit> &&promise) {
  if (!td_->contacts_manager_->have_input_user(user_id)) {
    promise.set_error(Status::Error(6, "Have no access to the user"));
    return vector<DialogId>();
  }

  if (user_id == td_->contacts_manager_->get_my_id()) {
    promise.set_error(Status::Error(6, "Can't get common chats with self"));
    return vector<DialogId>();
  }
  if (limit <= 0) {
    promise.set_error(Status::Error(3, "Parameter limit must be positive"));
    return vector<DialogId>();
  }
  if (limit > MAX_GET_DIALOGS) {
    limit = MAX_GET_DIALOGS;
  }

  int32 offset_chat_id = 0;
  switch (offset_dialog_id.get_type()) {
    case DialogType::Chat:
      offset_chat_id = offset_dialog_id.get_chat_id().get();
      break;
    case DialogType::Channel:
      offset_chat_id = offset_dialog_id.get_channel_id().get();
      break;
    case DialogType::None:
      if (offset_dialog_id == DialogId()) {
        break;
      }
    // fallthrough
    case DialogType::User:
    case DialogType::SecretChat:
      promise.set_error(Status::Error(6, "Wrong offset_chat_id"));
      return vector<DialogId>();
    default:
      UNREACHABLE();
      break;
  }

  auto it = found_common_dialogs_.find(user_id);
  if (it != found_common_dialogs_.end() && !it->second.empty()) {
    vector<DialogId> &common_dialog_ids = it->second;
    auto offset_it = common_dialog_ids.begin();
    if (offset_dialog_id != DialogId()) {
      offset_it = std::find(common_dialog_ids.begin(), common_dialog_ids.end(), offset_dialog_id);
      if (offset_it == common_dialog_ids.end()) {
        promise.set_error(Status::Error(6, "Wrong offset_chat_id"));
        return vector<DialogId>();
      }
      ++offset_it;
    }
    vector<DialogId> result;
    while (result.size() < static_cast<size_t>(limit)) {
      if (offset_it == common_dialog_ids.end()) {
        break;
      }
      auto dialog_id = *offset_it++;
      if (dialog_id == DialogId()) {  // end of the list
        promise.set_value(Unit());
        return result;
      }
      result.push_back(dialog_id);
    }
    if (result.size() == static_cast<size_t>(limit) || force) {
      promise.set_value(Unit());
      return result;
    }
  }

  td_->create_handler<GetCommonDialogsQuery>(std::move(promise))->send(user_id, offset_chat_id, limit);
  return vector<DialogId>();
}

void MessagesManager::on_get_common_dialogs(UserId user_id, vector<tl_object_ptr<telegram_api::Chat>> &&chats,
                                            int32 total_count) {
  auto &result = found_common_dialogs_[user_id];
  if (!result.empty() && result.back() == DialogId()) {
    return;
  }
  for (auto &chat : chats) {
    DialogId dialog_id;
    switch (chat->get_id()) {
      case telegram_api::chatEmpty::ID: {
        auto c = static_cast<const telegram_api::chatEmpty *>(chat.get());
        ChatId chat_id(c->id_);
        if (!chat_id.is_valid()) {
          LOG(ERROR) << "Receive invalid " << chat_id;
          continue;
        }
        dialog_id = DialogId(chat_id);
        break;
      }
      case telegram_api::chat::ID: {
        auto c = static_cast<const telegram_api::chat *>(chat.get());
        ChatId chat_id(c->id_);
        if (!chat_id.is_valid()) {
          LOG(ERROR) << "Receive invalid " << chat_id;
          continue;
        }
        dialog_id = DialogId(chat_id);
        break;
      }
      case telegram_api::chatForbidden::ID: {
        auto c = static_cast<const telegram_api::chatForbidden *>(chat.get());
        ChatId chat_id(c->id_);
        if (!chat_id.is_valid()) {
          LOG(ERROR) << "Receive invalid " << chat_id;
          continue;
        }
        dialog_id = DialogId(chat_id);
        break;
      }
      case telegram_api::channel::ID: {
        auto c = static_cast<const telegram_api::channel *>(chat.get());
        ChannelId channel_id(c->id_);
        if (!channel_id.is_valid()) {
          LOG(ERROR) << "Receive invalid " << channel_id;
          continue;
        }
        dialog_id = DialogId(channel_id);
        break;
      }
      case telegram_api::channelForbidden::ID: {
        auto c = static_cast<const telegram_api::channelForbidden *>(chat.get());
        ChannelId channel_id(c->id_);
        if (!channel_id.is_valid()) {
          LOG(ERROR) << "Receive invalid " << channel_id;
          continue;
        }
        dialog_id = DialogId(channel_id);
        break;
      }
      default:
        UNREACHABLE();
    }
    CHECK(dialog_id.is_valid());
    td_->contacts_manager_->on_get_chat(std::move(chat));

    if (std::find(result.begin(), result.end(), dialog_id) == result.end()) {
      force_create_dialog(dialog_id, "get common dialogs");
      result.push_back(dialog_id);
    }
  }
  if (result.size() >= static_cast<size_t>(total_count)) {
    result.push_back(DialogId());
  }
}

bool MessagesManager::have_message(FullMessageId full_message_id) {
  return get_message_force(full_message_id) != nullptr;
}

MessagesManager::Message *MessagesManager::get_message(FullMessageId full_message_id) {
  Dialog *d = get_dialog(full_message_id.get_dialog_id());
  if (d == nullptr) {
    return nullptr;
  }

  return get_message(d, full_message_id.get_message_id());
}

const MessagesManager::Message *MessagesManager::get_message(FullMessageId full_message_id) const {
  const Dialog *d = get_dialog(full_message_id.get_dialog_id());
  if (d == nullptr) {
    return nullptr;
  }

  return get_message(d, full_message_id.get_message_id());
}

MessagesManager::Message *MessagesManager::get_message_force(FullMessageId full_message_id) {
  Dialog *d = get_dialog_force(full_message_id.get_dialog_id());
  if (d == nullptr) {
    return nullptr;
  }

  return get_message_force(d, full_message_id.get_message_id());
}

MessageId MessagesManager::get_replied_message_id(const Message *m) {
  auto message_id = get_message_content_replied_message_id(m->content.get());
  if (message_id.is_valid()) {
    CHECK(!m->reply_to_message_id.is_valid());
    return message_id;
  }
  return m->reply_to_message_id;
}

void MessagesManager::get_message_force_from_server(Dialog *d, MessageId message_id, Promise<Unit> &&promise,
                                                    tl_object_ptr<telegram_api::InputMessage> input_message) {
  auto m = get_message_force(d, message_id);
  if (m == nullptr && message_id.is_valid() && message_id.is_server()) {
    auto dialog_type = d->dialog_id.get_type();
    if (d->last_new_message_id != MessageId() && message_id.get() > d->last_new_message_id.get()) {
      // message will not be added to the dialog anyway
      if (dialog_type == DialogType::Channel) {
        // so we try to force channel difference first
        CHECK(input_message == nullptr);  // replied message can't be older than already added original message
        postponed_get_message_requests_[d->dialog_id].emplace_back(message_id, std::move(promise));
        get_channel_difference(d->dialog_id, d->pts, true, "get_message");
      } else {
        promise.set_value(Unit());
      }
      return;
    }

    if (d->deleted_message_ids.count(message_id) == 0 && dialog_type != DialogType::SecretChat) {
      return get_messages_from_server({FullMessageId(d->dialog_id, message_id)}, std::move(promise),
                                      std::move(input_message));
    }
  }

  promise.set_value(Unit());
}

void MessagesManager::get_message(FullMessageId full_message_id, Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(full_message_id.get_dialog_id());
  if (d == nullptr) {
    return promise.set_error(Status::Error(6, "Chat not found"));
  }

  get_message_force_from_server(d, full_message_id.get_message_id(), std::move(promise));
}

MessageId MessagesManager::get_replied_message(DialogId dialog_id, MessageId message_id, bool force,
                                               Promise<Unit> &&promise) {
  LOG(INFO) << "Get replied message to " << message_id << " in " << dialog_id;
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    promise.set_error(Status::Error(6, "Chat not found"));
    return MessageId();
  }

  auto m = get_message_force(d, message_id);
  if (m == nullptr) {
    if (force) {
      promise.set_value(Unit());
    } else {
      get_message_force_from_server(d, message_id, std::move(promise));
    }
    return MessageId();
  }

  tl_object_ptr<telegram_api::InputMessage> input_message;
  if (message_id.is_server()) {
    input_message = make_tl_object<telegram_api::inputMessageReplyTo>(message_id.get_server_message_id().get());
  }
  auto replied_message_id = get_replied_message_id(m);
  get_message_force_from_server(d, replied_message_id, std::move(promise), std::move(input_message));

  return replied_message_id;
}

void MessagesManager::get_dialog_pinned_message(DialogId dialog_id, Promise<MessageId> &&promise) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(6, "Chat not found"));
  }

  if (dialog_id.get_type() != DialogType::Channel) {
    return promise.set_value(MessageId());
  }

  auto channel_id = dialog_id.get_channel_id();
  auto message_id = td_->contacts_manager_->get_channel_pinned_message_id(channel_id);
  if (get_message_force(d, message_id) == nullptr) {
    return td_->create_handler<GetChannelPinnedMessageQuery>(std::move(promise))->send(channel_id);
  }

  promise.set_value(std::move(message_id));
}

bool MessagesManager::get_messages(DialogId dialog_id, const vector<MessageId> &message_ids, Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    promise.set_error(Status::Error(6, "Chat not found"));
    return false;
  }

  bool is_secret = dialog_id.get_type() == DialogType::SecretChat;
  vector<FullMessageId> missed_message_ids;
  for (auto message_id : message_ids) {
    if (!message_id.is_valid()) {
      promise.set_error(Status::Error(6, "Invalid message identifier"));
      return false;
    }

    auto message = get_message_force(d, message_id);
    if (message == nullptr && message_id.is_server() && !is_secret) {
      missed_message_ids.emplace_back(dialog_id, message_id);
      continue;
    }
  }

  if (!missed_message_ids.empty()) {
    get_messages_from_server(std::move(missed_message_ids), std::move(promise));
    return false;
  }

  promise.set_value(Unit());
  return true;
}

void MessagesManager::get_messages_from_server(vector<FullMessageId> &&message_ids, Promise<Unit> &&promise,
                                               tl_object_ptr<telegram_api::InputMessage> input_message) {
  if (message_ids.empty()) {
    LOG(ERROR) << "Empty message_ids";
    return;
  }

  if (input_message != nullptr) {
    CHECK(message_ids.size() == 1);
  }

  vector<tl_object_ptr<telegram_api::InputMessage>> ordinary_message_ids;
  std::unordered_map<ChannelId, vector<tl_object_ptr<telegram_api::InputMessage>>, ChannelIdHash> channel_message_ids;
  for (auto &full_message_id : message_ids) {
    auto dialog_id = full_message_id.get_dialog_id();
    auto message_id = full_message_id.get_message_id();
    if (!message_id.is_valid() || !message_id.is_server()) {
      continue;
    }

    switch (dialog_id.get_type()) {
      case DialogType::User:
      case DialogType::Chat:
        ordinary_message_ids.push_back(input_message == nullptr ? get_input_message(message_id)
                                                                : std::move(input_message));
        break;
      case DialogType::Channel:
        channel_message_ids[dialog_id.get_channel_id()].push_back(
            input_message == nullptr ? get_input_message(message_id) : std::move(input_message));
        break;
      case DialogType::SecretChat:
        LOG(ERROR) << "Can't get secret chat message from server";
        break;
      case DialogType::None:
      default:
        UNREACHABLE();
        break;
    }
  }

  // TODO MultiPromise
  size_t query_count = !ordinary_message_ids.empty() + channel_message_ids.size();
  LOG_IF(ERROR, query_count > 1 && promise) << "Promise will be called after first query returns";

  if (!ordinary_message_ids.empty()) {
    td_->create_handler<GetMessagesQuery>(std::move(promise))->send(std::move(ordinary_message_ids));
  }

  for (auto &it : channel_message_ids) {
    auto input_channel = td_->contacts_manager_->get_input_channel(it.first);
    if (input_channel == nullptr) {
      LOG(ERROR) << "Can't find info about " << it.first << " to get a message from it";
      promise.set_error(Status::Error(6, "Can't access the chat"));
      continue;
    }
    td_->create_handler<GetChannelMessagesQuery>(std::move(promise))
        ->send(it.first, std::move(input_channel), std::move(it.second));
  }
}

bool MessagesManager::is_message_edited_recently(FullMessageId full_message_id, int32 seconds) {
  if (seconds < 0) {
    return false;
  }

  auto m = get_message_force(full_message_id);
  if (m == nullptr) {
    return true;
  }

  return m->edit_date >= G()->unix_time() - seconds;
}

std::pair<string, string> MessagesManager::get_public_message_link(FullMessageId full_message_id, bool for_group,
                                                                   Promise<Unit> &&promise) {
  auto dialog_id = full_message_id.get_dialog_id();
  auto d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    promise.set_error(Status::Error(6, "Chat not found"));
    return {};
  }
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(6, "Can't access the chat"));
    return {};
  }
  if (dialog_id.get_type() != DialogType::Channel ||
      td_->contacts_manager_->get_channel_username(dialog_id.get_channel_id()).empty()) {
    promise.set_error(Status::Error(
        6, "Public message links are available only for messages in public supergroups and channel chats"));
    return {};
  }

  auto message_id = full_message_id.get_message_id();
  auto message = get_message_force(d, message_id);
  if (message == nullptr) {
    promise.set_error(Status::Error(6, "Message not found"));
    return {};
  }
  if (!message_id.is_server()) {
    promise.set_error(Status::Error(6, "Message is local"));
    return {};
  }

  auto it = public_message_links_[for_group].find(full_message_id);
  if (it == public_message_links_[for_group].end()) {
    td_->create_handler<ExportChannelMessageLinkQuery>(std::move(promise))
        ->send(dialog_id.get_channel_id(), message_id, for_group);
    return {};
  }

  promise.set_value(Unit());
  return it->second;
}

void MessagesManager::on_get_public_message_link(FullMessageId full_message_id, bool for_group, string url,
                                                 string html) {
  LOG_IF(ERROR, url.empty() && html.empty()) << "Receive empty public link for " << full_message_id;
  public_message_links_[for_group][full_message_id] = {std::move(url), std::move(html)};
}

Status MessagesManager::delete_dialog_reply_markup(DialogId dialog_id, MessageId message_id) {
  if (td_->auth_manager_->is_bot()) {
    return Status::Error(6, "Bots can't delete chat reply markup");
  }
  if (!message_id.is_valid()) {
    return Status::Error(6, "Invalid message id specified");
  }

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(6, "Chat not found");
  }
  if (d->reply_markup_message_id != message_id) {
    return Status::OK();
  }

  Message *message = get_message_force(d, message_id);
  CHECK(message != nullptr);
  CHECK(message->reply_markup != nullptr);

  if (message->reply_markup->type == ReplyMarkup::Type::ForceReply) {
    set_dialog_reply_markup(d, MessageId());
  } else if (message->reply_markup->type == ReplyMarkup::Type::ShowKeyboard) {
    if (!message->reply_markup->is_one_time_keyboard) {
      return Status::Error(6, "Do not need to delete non one-time keyboard");
    }
    if (message->reply_markup->is_personal) {
      message->reply_markup->is_personal = false;
      set_dialog_reply_markup(d, message_id);

      on_message_changed(d, message, true, "delete_dialog_reply_markup");
    }
  } else {
    // non-bots can't have messages with RemoveKeyboard
    UNREACHABLE();
  }
  return Status::OK();
}

class MessagesManager::SaveDialogDraftMessageOnServerLogEvent {
 public:
  DialogId dialog_id_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_id_, parser);
  }
};

Status MessagesManager::set_dialog_draft_message(DialogId dialog_id,
                                                 tl_object_ptr<td_api::draftMessage> &&draft_message) {
  if (td_->auth_manager_->is_bot()) {
    return Status::Error(6, "Bots can't change chat draft message");
  }

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(6, "Chat not found");
  }
  TRY_STATUS(can_send_message(dialog_id));

  unique_ptr<DraftMessage> new_draft_message;
  if (draft_message != nullptr) {
    new_draft_message = make_unique<DraftMessage>();
    new_draft_message->date = G()->unix_time();
    new_draft_message->reply_to_message_id = get_reply_to_message_id(d, MessageId(draft_message->reply_to_message_id_));

    auto input_message_content = std::move(draft_message->input_message_text_);
    if (input_message_content != nullptr) {
      int32 draft_message_content_type = input_message_content->get_id();
      if (draft_message_content_type != td_api::inputMessageText::ID) {
        return Status::Error(5, "Input message content type must be InputMessageText");
      }
      TRY_RESULT(message_content, process_input_message_text(td_->contacts_manager_.get(), dialog_id,
                                                             std::move(input_message_content), false, true));
      new_draft_message->input_message_text = std::move(message_content);
    }

    if (!new_draft_message->reply_to_message_id.is_valid() && new_draft_message->input_message_text.text.text.empty()) {
      new_draft_message = nullptr;
    }
  }

  if (update_dialog_draft_message(d, std::move(new_draft_message), false, true)) {
    if (dialog_id.get_type() != DialogType::SecretChat) {
      if (G()->parameters().use_message_db) {
        LOG(INFO) << "Save draft of " << dialog_id << " to binlog";
        SaveDialogDraftMessageOnServerLogEvent logevent;
        logevent.dialog_id_ = dialog_id;
        auto storer = LogEventStorerImpl<SaveDialogDraftMessageOnServerLogEvent>(logevent);
        if (d->save_draft_message_logevent_id == 0) {
          d->save_draft_message_logevent_id =
              binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::SaveDialogDraftMessageOnServer, storer);
          LOG(INFO) << "Add draft logevent " << d->save_draft_message_logevent_id;
        } else {
          auto new_logevent_id = binlog_rewrite(G()->td_db()->get_binlog(), d->save_draft_message_logevent_id,
                                                LogEvent::HandlerType::SaveDialogDraftMessageOnServer, storer);
          LOG(INFO) << "Rewrite draft logevent " << d->save_draft_message_logevent_id << " with " << new_logevent_id;
        }
        d->save_draft_message_logevent_id_generation++;
      }

      pending_draft_message_timeout_.set_timeout_in(dialog_id.get(), d->is_opened ? MIN_SAVE_DRAFT_DELAY : 0);
    }
  }
  return Status::OK();
}

void MessagesManager::save_dialog_draft_message_on_server(DialogId dialog_id) {
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  Promise<> promise;
  if (d->save_draft_message_logevent_id != 0) {
    promise = PromiseCreator::lambda(
        [actor_id = actor_id(this), dialog_id,
         generation = d->save_draft_message_logevent_id_generation](Result<Unit> result) mutable {
          if (!G()->close_flag()) {
            send_closure(actor_id, &MessagesManager::on_saved_dialog_draft_message, dialog_id, generation);
          }
        });
  }

  // TODO do not send two queries simultaneously or use SequenceDispatcher
  td_->create_handler<SaveDraftMessageQuery>(std::move(promise))->send(dialog_id, d->draft_message);
}

void MessagesManager::on_saved_dialog_draft_message(DialogId dialog_id, uint64 generation) {
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  LOG(INFO) << "Saved draft in " << dialog_id << " with logevent " << d->save_draft_message_logevent_id;
  if (d->save_draft_message_logevent_id_generation == generation) {
    CHECK(d->save_draft_message_logevent_id != 0);
    LOG(INFO) << "Delete draft logevent " << d->save_draft_message_logevent_id;
    binlog_erase(G()->td_db()->get_binlog(), d->save_draft_message_logevent_id);
    d->save_draft_message_logevent_id = 0;
  }
}

void MessagesManager::clear_all_draft_messages(bool exclude_secret_chats, Promise<Unit> &&promise) {
  if (!exclude_secret_chats) {
    for (auto &dialog : dialogs_) {
      Dialog *d = dialog.second.get();
      if (d->dialog_id.get_type() == DialogType::SecretChat) {
        update_dialog_draft_message(d, nullptr, false, true);
      }
    }
  }
  td_->create_handler<ClearAllDraftsQuery>(std::move(promise))->send();
}

int32 MessagesManager::get_pinned_dialogs_limit() {
  int32 limit = G()->shared_config().get_option_integer("pinned_chat_count_max");
  if (limit <= 0) {
    const int32 DEFAULT_PINNED_DIALOGS_LIMIT = 5;
    return DEFAULT_PINNED_DIALOGS_LIMIT;
  }
  return limit;
}

vector<DialogId> MessagesManager::remove_secret_chat_dialog_ids(vector<DialogId> dialog_ids) {
  dialog_ids.erase(std::remove_if(dialog_ids.begin(), dialog_ids.end(),
                                  [](DialogId dialog_id) { return dialog_id.get_type() == DialogType::SecretChat; }),
                   dialog_ids.end());
  return dialog_ids;
}

Status MessagesManager::toggle_dialog_is_pinned(DialogId dialog_id, bool is_pinned) {
  if (td_->auth_manager_->is_bot()) {
    return Status::Error(6, "Bots can't change chat pin state");
  }

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(6, "Chat not found");
  }
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(6, "Can't access the chat");
  }

  bool was_pinned = d->pinned_order != DEFAULT_ORDER;
  if (is_pinned == was_pinned) {
    return Status::OK();
  }

  if (is_pinned) {
    auto pinned_dialog_ids = get_pinned_dialogs();
    auto pinned_dialog_count = pinned_dialog_ids.size();
    auto secret_pinned_dialog_count =
        std::count_if(pinned_dialog_ids.begin(), pinned_dialog_ids.end(),
                      [](DialogId dialog_id) { return dialog_id.get_type() == DialogType::SecretChat; });
    size_t dialog_count = dialog_id.get_type() == DialogType::SecretChat
                              ? secret_pinned_dialog_count
                              : pinned_dialog_count - secret_pinned_dialog_count;

    if (dialog_count >= static_cast<size_t>(get_pinned_dialogs_limit())) {
      return Status::Error(400, "Maximum number of pinned chats exceeded");
    }
  }

  set_dialog_is_pinned(d, is_pinned);
  update_dialog_pos(d, false, "toggle_dialog_is_pinned");

  toggle_dialog_is_pinned_on_server(dialog_id, is_pinned, 0);
  return Status::OK();
}

class MessagesManager::ToggleDialogIsPinnedOnServerLogEvent {
 public:
  DialogId dialog_id_;
  bool is_pinned_;

  template <class StorerT>
  void store(StorerT &storer) const {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(is_pinned_);
    END_STORE_FLAGS();

    td::store(dialog_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(is_pinned_);
    END_PARSE_FLAGS();

    td::parse(dialog_id_, parser);
  }
};

uint64 MessagesManager::save_toggle_dialog_is_pinned_on_server_logevent(DialogId dialog_id, bool is_pinned) {
  ToggleDialogIsPinnedOnServerLogEvent logevent{dialog_id, is_pinned};
  auto storer = LogEventStorerImpl<ToggleDialogIsPinnedOnServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ToggleDialogIsPinnedOnServer, storer);
}

void MessagesManager::toggle_dialog_is_pinned_on_server(DialogId dialog_id, bool is_pinned, uint64 logevent_id) {
  if (logevent_id == 0 && dialog_id.get_type() == DialogType::SecretChat) {
    // don't even create new binlog events
    return;
  }

  if (logevent_id == 0 && G()->parameters().use_message_db) {
    logevent_id = save_toggle_dialog_is_pinned_on_server_logevent(dialog_id, is_pinned);
  }

  td_->create_handler<ToggleDialogPinQuery>(get_erase_logevent_promise(logevent_id))->send(dialog_id, is_pinned);
}

Status MessagesManager::set_pinned_dialogs(vector<DialogId> dialog_ids) {
  if (td_->auth_manager_->is_bot()) {
    return Status::Error(6, "Bots can't reorder pinned chats");
  }

  int32 dialog_count = 0;
  int32 secret_dialog_count = 0;
  auto dialog_count_limit = get_pinned_dialogs_limit();
  for (auto dialog_id : dialog_ids) {
    Dialog *d = get_dialog_force(dialog_id);
    if (d == nullptr) {
      return Status::Error(6, "Chat not found");
    }
    if (!have_input_peer(dialog_id, AccessRights::Read)) {
      return Status::Error(6, "Can't access the chat");
    }
    if (dialog_id.get_type() == DialogType::SecretChat) {
      secret_dialog_count++;
    } else {
      dialog_count++;
    }

    if (dialog_count > dialog_count_limit || secret_dialog_count > dialog_count_limit) {
      return Status::Error(400, "Maximum number of pinned chats exceeded");
    }
  }
  std::unordered_set<DialogId, DialogIdHash> new_pinned_dialog_ids(dialog_ids.begin(), dialog_ids.end());
  if (new_pinned_dialog_ids.size() != dialog_ids.size()) {
    return Status::Error(400, "Duplicate chats in the list of pinned chats");
  }

  auto pinned_dialog_ids = get_pinned_dialogs();
  if (pinned_dialog_ids == dialog_ids) {
    return Status::OK();
  }
  LOG(INFO) << "Reorder pinned chats order from " << format::as_array(pinned_dialog_ids) << " to "
            << format::as_array(dialog_ids);

  auto server_old_dialog_ids = remove_secret_chat_dialog_ids(pinned_dialog_ids);
  auto server_new_dialog_ids = remove_secret_chat_dialog_ids(dialog_ids);

  std::reverse(pinned_dialog_ids.begin(), pinned_dialog_ids.end());
  std::reverse(dialog_ids.begin(), dialog_ids.end());

  std::unordered_set<DialogId, DialogIdHash> old_pinned_dialog_ids(pinned_dialog_ids.begin(), pinned_dialog_ids.end());
  auto old_it = pinned_dialog_ids.begin();
  for (auto dialog_id : dialog_ids) {
    old_pinned_dialog_ids.erase(dialog_id);
    while (old_it < pinned_dialog_ids.end()) {
      if (*old_it == dialog_id) {
        break;
      }
      old_it++;
    }
    if (old_it < pinned_dialog_ids.end()) {
      // leave dialog where it is
      continue;
    }
    set_dialog_is_pinned(dialog_id, true);
  }
  for (auto dialog_id : old_pinned_dialog_ids) {
    set_dialog_is_pinned(dialog_id, false);
  }

  if (server_old_dialog_ids != server_new_dialog_ids) {
    reorder_pinned_dialogs_on_server(server_new_dialog_ids, 0);
  }
  return Status::OK();
}

class MessagesManager::ReorderPinnedDialogsOnServerLogEvent {
 public:
  vector<DialogId> dialog_ids_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_ids_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_ids_, parser);
  }
};

uint64 MessagesManager::save_reorder_pinned_dialogs_on_server_logevent(const vector<DialogId> &dialog_ids) {
  ReorderPinnedDialogsOnServerLogEvent logevent{dialog_ids};
  auto storer = LogEventStorerImpl<ReorderPinnedDialogsOnServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ReorderPinnedDialogsOnServer, storer);
}

void MessagesManager::reorder_pinned_dialogs_on_server(const vector<DialogId> &dialog_ids, uint64 logevent_id) {
  if (logevent_id == 0 && G()->parameters().use_message_db) {
    logevent_id = save_reorder_pinned_dialogs_on_server_logevent(dialog_ids);
  }

  td_->create_handler<ReorderPinnedDialogsQuery>(get_erase_logevent_promise(logevent_id))->send(dialog_ids);
}

Status MessagesManager::toggle_dialog_is_marked_as_unread(DialogId dialog_id, bool is_marked_as_unread) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(6, "Chat not found");
  }
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(6, "Can't access the chat");
  }

  if (is_marked_as_unread == d->is_marked_as_unread) {
    return Status::OK();
  }

  set_dialog_is_marked_as_unread(d, is_marked_as_unread);

  toggle_dialog_is_marked_as_unread_on_server(dialog_id, is_marked_as_unread, 0);
  return Status::OK();
}

class MessagesManager::ToggleDialogIsMarkedAsUnreadOnServerLogEvent {
 public:
  DialogId dialog_id_;
  bool is_marked_as_unread_;

  template <class StorerT>
  void store(StorerT &storer) const {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(is_marked_as_unread_);
    END_STORE_FLAGS();

    td::store(dialog_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(is_marked_as_unread_);
    END_PARSE_FLAGS();

    td::parse(dialog_id_, parser);
  }
};

uint64 MessagesManager::save_toggle_dialog_is_marked_as_unread_on_server_logevent(DialogId dialog_id,
                                                                                  bool is_marked_as_unread) {
  ToggleDialogIsMarkedAsUnreadOnServerLogEvent logevent{dialog_id, is_marked_as_unread};
  auto storer = LogEventStorerImpl<ToggleDialogIsMarkedAsUnreadOnServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ToggleDialogIsMarkedAsUnreadOnServer, storer);
}

void MessagesManager::toggle_dialog_is_marked_as_unread_on_server(DialogId dialog_id, bool is_marked_as_unread,
                                                                  uint64 logevent_id) {
  if (logevent_id == 0 && dialog_id.get_type() == DialogType::SecretChat) {
    // don't even create new binlog events
    return;
  }

  if (logevent_id == 0 && G()->parameters().use_message_db) {
    logevent_id = save_toggle_dialog_is_marked_as_unread_on_server_logevent(dialog_id, is_marked_as_unread);
  }

  td_->create_handler<ToggleDialogUnreadMarkQuery>(get_erase_logevent_promise(logevent_id))
      ->send(dialog_id, is_marked_as_unread);
}

Status MessagesManager::toggle_dialog_silent_send_message(DialogId dialog_id, bool silent_send_message) {
  CHECK(!td_->auth_manager_->is_bot());

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(6, "Chat not found");
  }
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(6, "Can't access the chat");
  }

  if (update_dialog_silent_send_message(d, silent_send_message)) {
    update_dialog_notification_settings_on_server(dialog_id, false);
  }

  return Status::OK();
}

class MessagesManager::UpdateDialogNotificationSettingsOnServerLogEvent {
 public:
  DialogId dialog_id_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_id_, parser);
  }
};

void MessagesManager::update_dialog_notification_settings_on_server(DialogId dialog_id, bool from_binlog) {
  if (!from_binlog && get_input_notify_peer(dialog_id) == nullptr) {
    // don't even create new binlog events
    return;
  }

  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  if (!from_binlog && G()->parameters().use_message_db) {
    LOG(INFO) << "Save notification settings of " << dialog_id << " to binlog";
    UpdateDialogNotificationSettingsOnServerLogEvent logevent;
    logevent.dialog_id_ = dialog_id;
    auto storer = LogEventStorerImpl<UpdateDialogNotificationSettingsOnServerLogEvent>(logevent);
    if (d->save_notification_settings_logevent_id == 0) {
      d->save_notification_settings_logevent_id = binlog_add(
          G()->td_db()->get_binlog(), LogEvent::HandlerType::UpdateDialogNotificationSettingsOnServer, storer);
      LOG(INFO) << "Add notification settings logevent " << d->save_notification_settings_logevent_id;
    } else {
      auto new_logevent_id = binlog_rewrite(G()->td_db()->get_binlog(), d->save_notification_settings_logevent_id,
                                            LogEvent::HandlerType::UpdateDialogNotificationSettingsOnServer, storer);
      LOG(INFO) << "Rewrite notification settings logevent " << d->save_notification_settings_logevent_id << " with "
                << new_logevent_id;
    }
    d->save_notification_settings_logevent_id_generation++;
  }

  Promise<> promise;
  if (d->save_notification_settings_logevent_id != 0) {
    promise = PromiseCreator::lambda(
        [actor_id = actor_id(this), dialog_id,
         generation = d->save_notification_settings_logevent_id_generation](Result<Unit> result) mutable {
          if (!G()->close_flag()) {
            send_closure(actor_id, &MessagesManager::on_updated_dialog_notification_settings, dialog_id, generation);
          }
        });
  }

  send_update_dialog_notification_settings_query(dialog_id, std::move(promise));
}

void MessagesManager::send_update_dialog_notification_settings_query(DialogId dialog_id, Promise<Unit> &&promise) {
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  // TODO do not send two queries simultaneously or use SequenceDispatcher
  td_->create_handler<UpdateDialogNotifySettingsQuery>(std::move(promise))->send(dialog_id, d->notification_settings);
}

void MessagesManager::on_updated_dialog_notification_settings(DialogId dialog_id, uint64 generation) {
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  LOG(INFO) << "Saved notification settings in " << dialog_id << " with logevent "
            << d->save_notification_settings_logevent_id;
  if (d->save_notification_settings_logevent_id_generation == generation) {
    CHECK(d->save_notification_settings_logevent_id != 0);
    LOG(INFO) << "Delete notification settings logevent " << d->save_notification_settings_logevent_id;
    binlog_erase(G()->td_db()->get_binlog(), d->save_notification_settings_logevent_id);
    d->save_notification_settings_logevent_id = 0;
  }
}

Status MessagesManager::set_dialog_client_data(DialogId dialog_id, string &&client_data) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(6, "Chat not found");
  }

  d->client_data = std::move(client_data);
  on_dialog_updated(d->dialog_id, "set_dialog_client_data");
  return Status::OK();
}

bool MessagesManager::is_dialog_inited(const Dialog *d) {
  return d != nullptr && d->notification_settings.is_synchronized && d->is_last_read_inbox_message_id_inited &&
         d->is_last_read_outbox_message_id_inited;
}

int32 MessagesManager::get_dialog_mute_until(const Dialog *d) const {
  CHECK(d != nullptr);
  return d->notification_settings.use_default_mute_until ? get_scope_mute_until(d->dialog_id)
                                                         : d->notification_settings.mute_until;
}

bool MessagesManager::is_dialog_muted(const Dialog *d) const {
  return get_dialog_mute_until(d) != 0;
}

void MessagesManager::create_dialog(DialogId dialog_id, bool force, Promise<Unit> &&promise) {
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    if (!have_dialog_info_force(dialog_id)) {
      return promise.set_error(Status::Error(6, "Chat info not found"));
    }
    if (!have_input_peer(dialog_id, AccessRights::Read)) {
      return promise.set_error(Status::Error(6, "Can't access the chat"));
    }
  }

  if (force || td_->auth_manager_->is_bot() || dialog_id.get_type() == DialogType::SecretChat) {
    force_create_dialog(dialog_id, "create dialog");
  } else {
    const Dialog *d = get_dialog_force(dialog_id);
    if (!is_dialog_inited(d)) {
      return send_get_dialog_query(dialog_id, std::move(promise));
    }
  }

  promise.set_value(Unit());
}

DialogId MessagesManager::create_new_group_chat(const vector<UserId> &user_ids, const string &title, int64 &random_id,
                                                Promise<Unit> &&promise) {
  LOG(INFO) << "Trying to create group chat \"" << title << "\" with members " << format::as_array(user_ids);

  if (random_id != 0) {
    // request has already been sent before
    auto it = created_dialogs_.find(random_id);
    CHECK(it != created_dialogs_.end());
    auto dialog_id = it->second;
    CHECK(dialog_id.get_type() == DialogType::Chat);
    CHECK(have_dialog(dialog_id));

    created_dialogs_.erase(it);

    // set default notification settings to newly created chat
    on_update_dialog_notify_settings(dialog_id,
                                     make_tl_object<telegram_api::peerNotifySettings>(0, false, false, 0, ""));

    promise.set_value(Unit());
    return dialog_id;
  }

  if (user_ids.empty()) {
    promise.set_error(Status::Error(3, "Too few users to create basic group chat"));
    return DialogId();
  }

  auto new_title = clean_name(title, MAX_TITLE_LENGTH);
  if (new_title.empty()) {
    promise.set_error(Status::Error(3, "Title can't be empty"));
    return DialogId();
  }

  vector<tl_object_ptr<telegram_api::InputUser>> input_users;
  for (auto user_id : user_ids) {
    auto input_user = td_->contacts_manager_->get_input_user(user_id);
    if (input_user == nullptr) {
      promise.set_error(Status::Error(3, "User not found"));
      return DialogId();
    }
    input_users.push_back(std::move(input_user));
  }

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || created_dialogs_.find(random_id) != created_dialogs_.end());
  created_dialogs_[random_id];  // reserve place for result

  td_->create_handler<CreateChatQuery>(std::move(promise))->send(std::move(input_users), new_title, random_id);
  return DialogId();
}

DialogId MessagesManager::create_new_channel_chat(const string &title, bool is_megagroup, const string &description,
                                                  int64 &random_id, Promise<Unit> &&promise) {
  LOG(INFO) << "Trying to create " << (is_megagroup ? "supergroup" : "broadcast") << " with title \"" << title
            << "\" and description \"" << description << "\"";

  if (random_id != 0) {
    // request has already been sent before
    auto it = created_dialogs_.find(random_id);
    CHECK(it != created_dialogs_.end());
    auto dialog_id = it->second;
    CHECK(dialog_id.get_type() == DialogType::Channel);
    CHECK(have_dialog(dialog_id));

    created_dialogs_.erase(it);

    // set default notification settings to newly created chat
    on_update_dialog_notify_settings(dialog_id,
                                     make_tl_object<telegram_api::peerNotifySettings>(0, false, false, 0, ""));

    promise.set_value(Unit());
    return dialog_id;
  }

  auto new_title = clean_name(title, MAX_TITLE_LENGTH);
  if (new_title.empty()) {
    promise.set_error(Status::Error(3, "Title can't be empty"));
    return DialogId();
  }

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || created_dialogs_.find(random_id) != created_dialogs_.end());
  created_dialogs_[random_id];  // reserve place for result

  td_->create_handler<CreateChannelQuery>(std::move(promise))
      ->send(new_title, is_megagroup, strip_empty_characters(description, MAX_DESCRIPTION_LENGTH), random_id);
  return DialogId();
}

void MessagesManager::create_new_secret_chat(UserId user_id, Promise<SecretChatId> &&promise) {
  auto user_base = td_->contacts_manager_->get_input_user(user_id);
  if (user_base == nullptr || user_base->get_id() != telegram_api::inputUser::ID) {
    return promise.set_error(Status::Error(6, "User not found"));
  }
  auto user = move_tl_object_as<telegram_api::inputUser>(user_base);

  send_closure(G()->secret_chats_manager(), &SecretChatsManager::create_chat, user->user_id_, user->access_hash_,
               std::move(promise));
}

DialogId MessagesManager::migrate_dialog_to_megagroup(DialogId dialog_id, Promise<Unit> &&promise) {
  LOG(INFO) << "Trying to convert " << dialog_id << " to supergroup";

  if (dialog_id.get_type() != DialogType::Chat) {
    promise.set_error(Status::Error(3, "Only basic group chats can be converted to supergroup"));
    return DialogId();
  }

  auto channel_id = td_->contacts_manager_->migrate_chat_to_megagroup(dialog_id.get_chat_id(), promise);
  if (!channel_id.is_valid()) {
    return DialogId();
  }

  if (!td_->contacts_manager_->have_channel(channel_id)) {
    LOG(ERROR) << "Can't find info about supergroup to which the group has migrated";
    promise.set_error(Status::Error(6, "Supergroup is not found"));
    return DialogId();
  }

  auto new_dialog_id = DialogId(channel_id);
  Dialog *d = get_dialog_force(new_dialog_id);
  if (d == nullptr) {
    d = add_dialog(new_dialog_id);
    if (d->pts == 0) {
      d->pts = 1;
      if (is_debug_message_op_enabled()) {
        d->debug_message_op.emplace_back(Dialog::MessageOp::SetPts, d->pts, "migrate");
      }
    }
    update_dialog_pos(d, false, "migrate_dialog_to_megagroup");
  }

  promise.set_value(Unit());
  return new_dialog_id;
}

Status MessagesManager::open_dialog(DialogId dialog_id) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(3, "Chat not found");
  }

  open_dialog(d);
  return Status::OK();
}

Status MessagesManager::close_dialog(DialogId dialog_id) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(3, "Chat not found");
  }

  close_dialog(d);
  return Status::OK();
}

DialogId MessagesManager::get_my_dialog_id() const {
  return DialogId(td_->contacts_manager_->get_my_id());
}

Status MessagesManager::view_messages(DialogId dialog_id, const vector<MessageId> &message_ids, bool force_read) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(3, "Chat not found");
  }
  for (auto message_id : message_ids) {
    if (!message_id.is_valid()) {
      return Status::Error(3, "Invalid message identifier");
    }
  }
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(5, "Can't access the chat");
  }

  bool need_read = force_read || d->is_opened;
  MessageId max_message_id;  // max server or local viewed message_id
  vector<MessageId> read_content_message_ids;
  for (auto message_id : message_ids) {
    auto message = get_message_force(d, message_id);
    if (message != nullptr) {
      if (message_id.is_server() && message->views > 0) {
        d->pending_viewed_message_ids.insert(message_id);
      }

      if (!message_id.is_yet_unsent() && message_id.get() > max_message_id.get()) {
        max_message_id = message_id;
      }

      if (need_read) {
        auto message_content_type = message->content->get_type();
        if (message_content_type != MessageContentType::VoiceNote &&
            message_content_type != MessageContentType::VideoNote &&
            update_message_contains_unread_mention(d, message, false, "view_messages")) {
          CHECK(message_id.is_server());
          read_content_message_ids.push_back(message_id);
          on_message_changed(d, message, true, "view_messages");
        }
      }
    }
  }
  if (!d->pending_viewed_message_ids.empty()) {
    pending_message_views_timeout_.add_timeout_in(dialog_id.get(), MAX_MESSAGE_VIEW_DELAY);
    d->increment_view_counter |= d->is_opened;
  }
  if (!read_content_message_ids.empty()) {
    read_message_contents_on_server(dialog_id, std::move(read_content_message_ids), 0);
  }

  if (need_read && max_message_id.get() > d->last_read_inbox_message_id.get()) {
    MessageId last_read_message_id = max_message_id;
    MessageId prev_last_read_inbox_message_id = d->last_read_inbox_message_id;
    read_history_inbox(d->dialog_id, last_read_message_id, -1, "view_messages");

    if (dialog_id.get_type() != DialogType::SecretChat) {
      if (last_read_message_id.get_prev_server_message_id().get() >
          prev_last_read_inbox_message_id.get_prev_server_message_id().get()) {
        read_history_on_server(d, last_read_message_id.get_prev_server_message_id());
      }
    } else {
      if (last_read_message_id.get() > prev_last_read_inbox_message_id.get()) {
        read_history_on_server(d, last_read_message_id);
      }
    }
  }
  if (need_read && d->is_marked_as_unread) {
    set_dialog_is_marked_as_unread(d, false);
  }

  return Status::OK();
}

Status MessagesManager::open_message_content(FullMessageId full_message_id) {
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(3, "Chat not found");
  }

  auto message_id = full_message_id.get_message_id();
  auto message = get_message_force(d, message_id);
  if (message == nullptr) {
    return Status::Error(4, "Message not found");
  }

  if (message_id.is_yet_unsent() || message->is_outgoing) {
    return Status::OK();
  }

  if (read_message_content(d, message, true, "open_message_content") &&
      (message_id.is_server() || dialog_id.get_type() == DialogType::SecretChat)) {
    read_message_contents_on_server(dialog_id, {message_id}, 0);
  }

  return Status::OK();
}

class MessagesManager::ReadMessageContentsOnServerLogEvent {
 public:
  DialogId dialog_id_;
  vector<MessageId> message_ids_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_id_, storer);
    td::store(message_ids_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_id_, parser);
    td::parse(message_ids_, parser);
  }
};

uint64 MessagesManager::save_read_message_contents_on_server_logevent(DialogId dialog_id,
                                                                      const vector<MessageId> &message_ids) {
  ReadMessageContentsOnServerLogEvent logevent{dialog_id, message_ids};
  auto storer = LogEventStorerImpl<ReadMessageContentsOnServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ReadMessageContentsOnServer, storer);
}

void MessagesManager::read_message_contents_on_server(DialogId dialog_id, vector<MessageId> message_ids,
                                                      uint64 logevent_id) {
  CHECK(!message_ids.empty());

  LOG(INFO) << "Read contents of " << format::as_array(message_ids) << " in " << dialog_id << " on server";

  if (logevent_id == 0 && G()->parameters().use_message_db) {
    logevent_id = save_read_message_contents_on_server_logevent(dialog_id, message_ids);
  }

  auto promise = get_erase_logevent_promise(logevent_id);
  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
      td_->create_handler<ReadMessagesContentsQuery>(std::move(promise))->send(std::move(message_ids));
      break;
    case DialogType::Channel:
      td_->create_handler<ReadChannelMessagesContentsQuery>(std::move(promise))
          ->send(dialog_id.get_channel_id(), std::move(message_ids));
      break;
    case DialogType::SecretChat:
      CHECK(message_ids.size() == 1);
      for (auto message_id : message_ids) {
        auto m = get_message_force({dialog_id, message_id});
        if (m != nullptr) {
          send_closure(G()->secret_chats_manager(), &SecretChatsManager::send_open_message,
                       dialog_id.get_secret_chat_id(), m->random_id, std::move(promise));
        }
      }
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

void MessagesManager::open_dialog(Dialog *d) {
  if (d->is_opened || !have_input_peer(d->dialog_id, AccessRights::Read)) {
    return;
  }
  d->is_opened = true;

  auto min_message_id = MessageId(ServerMessageId(1)).get();
  if (d->last_message_id == MessageId() && d->last_read_outbox_message_id.get() < min_message_id &&
      d->messages != nullptr && d->messages->message_id.get() < min_message_id) {
    Message *m = d->messages.get();
    while (m->right != nullptr) {
      m = m->right.get();
    }
    if (m->message_id.get() < min_message_id) {
      read_history_inbox(d->dialog_id, m->message_id, -1, "open_dialog");
    }
  }

  LOG(INFO) << "Cancel unload timeout for " << d->dialog_id;
  pending_unload_dialog_timeout_.cancel_timeout(d->dialog_id.get());

  switch (d->dialog_id.get_type()) {
    case DialogType::User:
      break;
    case DialogType::Chat: {
      auto chat_id = d->dialog_id.get_chat_id();
      td_->contacts_manager_->get_chat_full(chat_id, Promise<Unit>());
      break;
    }
    case DialogType::Channel:
      get_channel_difference(d->dialog_id, d->pts, true, "open_dialog");
      break;
    case DialogType::SecretChat:
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

void MessagesManager::close_dialog(Dialog *d) {
  if (!d->is_opened) {
    return;
  }
  d->is_opened = false;

  if (have_input_peer(d->dialog_id, AccessRights::Write)) {
    if (pending_draft_message_timeout_.has_timeout(d->dialog_id.get())) {
      pending_draft_message_timeout_.set_timeout_in(d->dialog_id.get(), 0.0);
    }
  } else {
    pending_draft_message_timeout_.cancel_timeout(d->dialog_id.get());
  }

  if (have_input_peer(d->dialog_id, AccessRights::Read)) {
    if (pending_message_views_timeout_.has_timeout(d->dialog_id.get())) {
      pending_message_views_timeout_.set_timeout_in(d->dialog_id.get(), 0.0);
    }
    if (pending_read_history_timeout_.has_timeout(d->dialog_id.get())) {
      pending_read_history_timeout_.set_timeout_in(d->dialog_id.get(), 0.0);
    }
  } else {
    pending_message_views_timeout_.cancel_timeout(d->dialog_id.get());
    d->pending_viewed_message_ids.clear();
    d->increment_view_counter = false;

    pending_read_history_timeout_.cancel_timeout(d->dialog_id.get());
  }

  if (is_message_unload_enabled()) {
    LOG(INFO) << "Schedule unload of " << d->dialog_id;
    pending_unload_dialog_timeout_.set_timeout_in(d->dialog_id.get(), DIALOG_UNLOAD_DELAY);
  }

  switch (d->dialog_id.get_type()) {
    case DialogType::User:
      break;
    case DialogType::Chat:
      break;
    case DialogType::Channel:
      channel_get_difference_timeout_.cancel_timeout(d->dialog_id.get());
      break;
    case DialogType::SecretChat:
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

tl_object_ptr<td_api::ChatType> MessagesManager::get_chat_type_object(DialogId dialog_id) const {
  switch (dialog_id.get_type()) {
    case DialogType::User:
      return make_tl_object<td_api::chatTypePrivate>(
          td_->contacts_manager_->get_user_id_object(dialog_id.get_user_id(), "chatTypePrivate"));
    case DialogType::Chat:
      return make_tl_object<td_api::chatTypeBasicGroup>(
          td_->contacts_manager_->get_basic_group_id_object(dialog_id.get_chat_id(), "chatTypeBasicGroup"));
    case DialogType::Channel: {
      auto channel_id = dialog_id.get_channel_id();
      auto channel_type = td_->contacts_manager_->get_channel_type(channel_id);
      return make_tl_object<td_api::chatTypeSupergroup>(
          td_->contacts_manager_->get_supergroup_id_object(channel_id, "chatTypeSupergroup"),
          channel_type != ChannelType::Megagroup);
    }
    case DialogType::SecretChat: {
      auto secret_chat_id = dialog_id.get_secret_chat_id();
      auto user_id = td_->contacts_manager_->get_secret_chat_user_id(secret_chat_id);
      return make_tl_object<td_api::chatTypeSecret>(
          td_->contacts_manager_->get_secret_chat_id_object(secret_chat_id, "chatTypeSecret"),
          td_->contacts_manager_->get_user_id_object(user_id, "chatTypeSecret"));
    }
    case DialogType::None:
    default:
      UNREACHABLE();
      return nullptr;
  }
}

tl_object_ptr<td_api::chat> MessagesManager::get_chat_object(const Dialog *d) const {
  CHECK(d != nullptr);
  return make_tl_object<td_api::chat>(
      d->dialog_id.get(), get_chat_type_object(d->dialog_id), get_dialog_title(d->dialog_id),
      get_chat_photo_object(td_->file_manager_.get(), get_dialog_photo(d->dialog_id)),
      get_message_object(d->dialog_id, get_message(d, d->last_message_id)),
      DialogDate(d->order, d->dialog_id) <= last_dialog_date_ ? d->order : 0, d->pinned_order != DEFAULT_ORDER,
      d->is_marked_as_unread, d->order == SPONSORED_DIALOG_ORDER, can_report_dialog(d->dialog_id),
      d->notification_settings.silent_send_message, d->server_unread_count + d->local_unread_count,
      d->last_read_inbox_message_id.get(), d->last_read_outbox_message_id.get(), d->unread_mention_count,
      get_chat_notification_settings_object(&d->notification_settings), d->reply_markup_message_id.get(),
      get_draft_message_object(d->draft_message), d->client_data);
}

tl_object_ptr<td_api::chat> MessagesManager::get_chat_object(DialogId dialog_id) const {
  return get_chat_object(get_dialog(dialog_id));
}

tl_object_ptr<td_api::chats> MessagesManager::get_chats_object(const vector<DialogId> &dialogs) {
  return td_api::make_object<td_api::chats>(transform(dialogs, [](DialogId dialog_id) { return dialog_id.get(); }));
}

td_api::object_ptr<td_api::updateScopeNotificationSettings>
MessagesManager::get_update_scope_notification_settings_object(NotificationSettingsScope scope) const {
  auto notification_settings = get_scope_notification_settings(scope);
  CHECK(notification_settings != nullptr);
  return td_api::make_object<td_api::updateScopeNotificationSettings>(
      get_notification_settings_scope_object(scope), get_scope_notification_settings_object(notification_settings));
}

std::pair<bool, int32> MessagesManager::get_dialog_mute_until(DialogId dialog_id, const Dialog *d) const {
  if (d == nullptr || !d->notification_settings.is_synchronized) {
    return {false, get_scope_mute_until(dialog_id)};
  }

  return {d->notification_settings.is_use_default_fixed, get_dialog_mute_until(d)};
}

NotificationSettingsScope MessagesManager::get_dialog_notification_setting_scope(DialogId dialog_id) {
  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::SecretChat:
      return NotificationSettingsScope::Private;
    case DialogType::Chat:
    case DialogType::Channel:
      return NotificationSettingsScope::Group;
    case DialogType::None:
    default:
      UNREACHABLE();
      return NotificationSettingsScope::Private;
  }
}

int32 MessagesManager::get_scope_mute_until(DialogId dialog_id) const {
  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::SecretChat:
      return users_notification_settings_.mute_until;
    case DialogType::Chat:
    case DialogType::Channel:
      return chats_notification_settings_.mute_until;
    case DialogType::None:
    default:
      UNREACHABLE();
      return 0;
  }
}

const ScopeNotificationSettings *MessagesManager::get_scope_notification_settings(NotificationSettingsScope scope,
                                                                                  Promise<Unit> &&promise) {
  const ScopeNotificationSettings *notification_settings = get_scope_notification_settings(scope);
  CHECK(notification_settings != nullptr);
  if (!notification_settings->is_synchronized && !td_->auth_manager_->is_bot()) {
    send_get_scope_notification_settings_query(scope, std::move(promise));
    return nullptr;
  }

  promise.set_value(Unit());
  return notification_settings;
}

DialogNotificationSettings *MessagesManager::get_dialog_notification_settings(DialogId dialog_id, bool force) {
  auto dialog = get_dialog_force(dialog_id);
  if (dialog == nullptr) {
    return nullptr;
  }
  if (!force && !have_input_peer(dialog_id, AccessRights::Read)) {
    return nullptr;
  }
  return &dialog->notification_settings;
}

ScopeNotificationSettings *MessagesManager::get_scope_notification_settings(NotificationSettingsScope scope) {
  switch (scope) {
    case NotificationSettingsScope::Private:
      return &users_notification_settings_;
    case NotificationSettingsScope::Group:
      return &chats_notification_settings_;
    default:
      UNREACHABLE();
      return nullptr;
  }
}

const ScopeNotificationSettings *MessagesManager::get_scope_notification_settings(
    NotificationSettingsScope scope) const {
  switch (scope) {
    case NotificationSettingsScope::Private:
      return &users_notification_settings_;
    case NotificationSettingsScope::Group:
      return &chats_notification_settings_;
    default:
      UNREACHABLE();
      return nullptr;
  }
}

tl_object_ptr<telegram_api::InputNotifyPeer> MessagesManager::get_input_notify_peer(DialogId dialog_id) const {
  if (get_dialog(dialog_id) == nullptr) {
    return nullptr;
  }
  auto input_peer = get_input_peer(dialog_id, AccessRights::Read);
  if (input_peer == nullptr) {
    return nullptr;
  }
  return make_tl_object<telegram_api::inputNotifyPeer>(std::move(input_peer));
}

Status MessagesManager::set_dialog_notification_settings(
    DialogId dialog_id, tl_object_ptr<td_api::chatNotificationSettings> &&notification_settings) {
  if (notification_settings == nullptr) {
    return Status::Error(400, "New notification settings must not be empty");
  }
  if (!clean_input_string(notification_settings->sound_)) {
    return Status::Error(400, "Notification settings sound must be encoded in UTF-8");
  }

  auto current_settings = get_dialog_notification_settings(dialog_id, false);
  if (current_settings == nullptr) {
    return Status::Error(6, "Wrong chat identifier specified");
  }

  int32 current_time = G()->unix_time();
  if (notification_settings->mute_for_ > std::numeric_limits<int32>::max() - current_time) {
    notification_settings->mute_for_ = std::numeric_limits<int32>::max() - current_time;
  }

  int32 mute_until;
  if (notification_settings->use_default_mute_for_ || notification_settings->mute_for_ <= 0) {
    mute_until = 0;
  } else {
    mute_until = notification_settings->mute_for_ + current_time;
  }

  DialogNotificationSettings new_settings(
      notification_settings->use_default_mute_for_, mute_until, notification_settings->use_default_sound_,
      std::move(notification_settings->sound_), notification_settings->use_default_show_preview_,
      notification_settings->show_preview_, current_settings->silent_send_message);
  if (update_dialog_notification_settings(dialog_id, current_settings, new_settings)) {
    update_dialog_notification_settings_on_server(dialog_id, false);
  }
  return Status::OK();
}

Status MessagesManager::set_scope_notification_settings(
    NotificationSettingsScope scope, tl_object_ptr<td_api::scopeNotificationSettings> &&notification_settings) {
  if (notification_settings == nullptr) {
    return Status::Error(400, "New notification settings must not be empty");
  }
  if (!clean_input_string(notification_settings->sound_)) {
    return Status::Error(400, "Notification settings sound must be encoded in UTF-8");
  }

  int32 current_time = G()->unix_time();
  if (notification_settings->mute_for_ > std::numeric_limits<int32>::max() - current_time) {
    notification_settings->mute_for_ = std::numeric_limits<int32>::max() - current_time;
  }

  int32 mute_until;
  if (notification_settings->mute_for_ <= 0) {
    mute_until = 0;
  } else {
    mute_until = notification_settings->mute_for_ + current_time;
  }

  ScopeNotificationSettings new_settings(mute_until, std::move(notification_settings->sound_),
                                         notification_settings->show_preview_);

  if (update_scope_notification_settings(scope, get_scope_notification_settings(scope), new_settings)) {
    update_scope_notification_settings_on_server(scope, 0);
  }
  return Status::OK();
}

class MessagesManager::UpdateScopeNotificationSettingsOnServerLogEvent {
 public:
  NotificationSettingsScope scope_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(scope_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(scope_, parser);
  }
};

uint64 MessagesManager::save_update_scope_notification_settings_on_server_logevent(NotificationSettingsScope scope) {
  UpdateScopeNotificationSettingsOnServerLogEvent logevent{scope};
  auto storer = LogEventStorerImpl<UpdateScopeNotificationSettingsOnServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::UpdateScopeNotificationSettingsOnServer, storer);
}

void MessagesManager::update_scope_notification_settings_on_server(NotificationSettingsScope scope,
                                                                   uint64 logevent_id) {
  if (logevent_id == 0) {
    logevent_id = save_update_scope_notification_settings_on_server_logevent(scope);
  }

  LOG(INFO) << "Update " << scope << " notification settings on server with logevent " << logevent_id;
  td_->create_handler<UpdateScopeNotifySettingsQuery>(get_erase_logevent_promise(logevent_id))
      ->send(scope, *get_scope_notification_settings(scope));
}

void MessagesManager::reset_all_notification_settings() {
  DialogNotificationSettings new_dialog_settings;
  ScopeNotificationSettings new_scope_settings;
  new_dialog_settings.is_synchronized = true;
  new_scope_settings.is_synchronized = true;

  update_scope_notification_settings(NotificationSettingsScope::Private, &users_notification_settings_,
                                     new_scope_settings);
  update_scope_notification_settings(NotificationSettingsScope::Group, &chats_notification_settings_,
                                     new_scope_settings);

  for (auto &dialog : dialogs_) {
    Dialog *d = dialog.second.get();
    update_dialog_notification_settings(d->dialog_id, &d->notification_settings, new_dialog_settings);
  }
  reset_all_notification_settings_on_server(0);
}

class MessagesManager::ResetAllNotificationSettingsOnServerLogEvent {
 public:
  template <class StorerT>
  void store(StorerT &storer) const {
  }

  template <class ParserT>
  void parse(ParserT &parser) {
  }
};

uint64 MessagesManager::save_reset_all_notification_settings_on_server_logevent() {
  ResetAllNotificationSettingsOnServerLogEvent logevent;
  auto storer = LogEventStorerImpl<ResetAllNotificationSettingsOnServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ResetAllNotificationSettingsOnServer, storer);
}

void MessagesManager::reset_all_notification_settings_on_server(uint64 logevent_id) {
  if (logevent_id == 0) {
    logevent_id = save_reset_all_notification_settings_on_server_logevent();
  }

  LOG(INFO) << "Reset all notification settings";
  td_->create_handler<ResetNotifySettingsQuery>(get_erase_logevent_promise(logevent_id))->send();
}

tl_object_ptr<td_api::messages> MessagesManager::get_dialog_history(DialogId dialog_id, MessageId from_message_id,
                                                                    int32 offset, int32 limit, int left_tries,
                                                                    bool only_local, Promise<Unit> &&promise) {
  if (limit <= 0) {
    promise.set_error(Status::Error(3, "Parameter limit must be positive"));
    return nullptr;
  }
  if (limit > MAX_GET_HISTORY) {
    limit = MAX_GET_HISTORY;
  }
  if (offset > 0) {
    promise.set_error(Status::Error(5, "Parameter offset must be non-positive"));
    return nullptr;
  }
  if (offset <= -MAX_GET_HISTORY) {
    promise.set_error(Status::Error(5, "Parameter offset must be greater than -100"));
    return nullptr;
  }
  if (offset < -limit) {
    promise.set_error(Status::Error(5, "Parameter offset must not be less than -limit"));
    return nullptr;
  }
  bool is_limit_increased = false;
  if (limit == -offset) {
    limit++;
    is_limit_increased = true;
  }
  CHECK(0 < limit && limit <= MAX_GET_HISTORY);
  CHECK(-limit < offset && offset <= 0);

  if (from_message_id == MessageId() || from_message_id.get() > MessageId::max().get()) {
    from_message_id = MessageId::max();
  }
  if (!from_message_id.is_valid()) {
    promise.set_error(Status::Error(3, "Invalid value of parameter from_message_id specified"));
    return nullptr;
  }

  const Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    promise.set_error(Status::Error(6, "Chat not found"));
    return nullptr;
  }
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(5, "Can't access the chat"));
    return nullptr;
  }

  LOG(INFO) << "Get " << (only_local ? "local " : "") << "history in " << dialog_id << " from " << from_message_id
            << " with offset " << offset << " and limit " << limit << ", " << left_tries
            << " tries left. Last read inbox message is " << d->last_read_inbox_message_id
            << ", last read outbox message is " << d->last_read_outbox_message_id
            << ", have_full_history = " << d->have_full_history;

  MessagesConstIterator p(d, from_message_id);
  LOG(DEBUG) << "Iterator points to " << (*p ? (*p)->message_id : MessageId());
  bool from_the_end = (d->last_message_id != MessageId() && from_message_id.get() > d->last_message_id.get()) ||
                      from_message_id.get() >= MessageId::max().get();

  if (from_the_end) {
    limit += offset;
    offset = 0;
    if (d->last_message_id == MessageId()) {
      p = MessagesConstIterator();
    }
  } else {
    bool have_a_gap = false;
    if (*p == nullptr) {
      // there is no gap if from_message_id is less than first message in the dialog
      if (left_tries == 0 && d->messages != nullptr && offset < 0) {
        const Message *cur = d->messages.get();
        while (cur->left != nullptr) {
          cur = cur->left.get();
        }
        CHECK(cur->message_id.get() > from_message_id.get());
        from_message_id = cur->message_id;
        p = MessagesConstIterator(d, from_message_id);
      } else {
        have_a_gap = true;
      }
    } else if ((*p)->message_id != from_message_id) {
      CHECK((*p)->message_id.get() < from_message_id.get());
      if (!(*p)->have_next &&
          (d->last_message_id == MessageId() || (*p)->message_id.get() < d->last_message_id.get())) {
        have_a_gap = true;
      }
    }

    if (have_a_gap) {
      LOG(INFO) << "Have a gap near message to get chat history from";
      p = MessagesConstIterator();
    }
    if (*p != nullptr && (*p)->message_id == from_message_id) {
      if (offset < 0) {
        offset++;
      } else {
        --p;
      }
    }

    while (*p != nullptr && offset < 0) {
      ++p;
      if (*p) {
        ++offset;
        from_message_id = (*p)->message_id;
      }
    }

    if (offset < 0 && ((d->last_message_id != MessageId() && from_message_id.get() >= d->last_message_id.get()) ||
                       (!have_a_gap && left_tries == 0))) {
      CHECK(!have_a_gap);
      limit += offset;
      offset = 0;
      p = MessagesConstIterator(d, from_message_id);
    }

    if (!have_a_gap && offset < 0) {
      offset--;
    }
  }

  LOG(INFO) << "Iterator after applying offset points to " << (*p ? (*p)->message_id : MessageId())
            << ", offset = " << offset << ", limit = " << limit << ", from_the_end = " << from_the_end;
  vector<tl_object_ptr<td_api::message>> messages;
  if (*p != nullptr && offset == 0) {
    while (*p != nullptr && messages.size() < static_cast<size_t>(limit)) {
      messages.push_back(get_message_object(dialog_id, *p));
      from_message_id = (*p)->message_id;
      --p;
    }
  }

  if (messages.size()) {
    // maybe need some messages
    CHECK(offset == 0);
    preload_newer_messages(d, MessageId(messages[0]->id_));
    preload_older_messages(d, MessageId(messages.back()->id_));
  } else if (messages.size() < static_cast<size_t>(limit) && left_tries != 0) {
    // there can be more messages on the server, need to load them
    if (from_the_end) {
      from_message_id = MessageId();
    }
    send_closure_later(actor_id(this), &MessagesManager::load_messages, d->dialog_id, from_message_id, offset,
                       limit - static_cast<int32>(messages.size()), left_tries, only_local, std::move(promise));
    return nullptr;
  }

  LOG(INFO) << "Have " << messages.size() << " messages out of requested "
            << (is_limit_increased ? "increased " : "exact ") << limit;
  if (is_limit_increased && static_cast<size_t>(limit) == messages.size()) {
    messages.pop_back();
  }

  LOG(INFO) << "Return " << messages.size() << " messages in result to getChatHistory";
  promise.set_value(Unit());                            // can send some messages
  return get_messages_object(-1, std::move(messages));  // TODO return real total_count of messages in the dialog
}

class MessagesManager::ReadHistoryOnServerLogEvent {
 public:
  DialogId dialog_id_;
  MessageId max_message_id_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_id_, storer);
    td::store(max_message_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_id_, parser);
    td::parse(max_message_id_, parser);
  }
};

class MessagesManager::ReadHistoryInSecretChatLogEvent {
 public:
  DialogId dialog_id_;
  int32 max_date_ = 0;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_id_, storer);
    td::store(max_date_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_id_, parser);
    td::parse(max_date_, parser);
  }
};

void MessagesManager::read_history_on_server(Dialog *d, MessageId max_message_id) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto dialog_id = d->dialog_id;
  LOG(INFO) << "Read history in " << dialog_id << " on server up to " << max_message_id;

  bool is_secret = dialog_id.get_type() == DialogType::SecretChat;
  if (is_secret) {
    auto *m = get_message_force(d, max_message_id);
    if (m == nullptr) {
      LOG(ERROR) << "Failed to read history in " << dialog_id << " up to " << max_message_id;
      return;
    }

    ReadHistoryInSecretChatLogEvent logevent;
    logevent.dialog_id_ = dialog_id;
    logevent.max_date_ = m->date;

    d->last_read_inbox_message_date = m->date;

    auto storer = LogEventStorerImpl<ReadHistoryInSecretChatLogEvent>(logevent);
    if (d->read_history_logevent_id == 0) {
      d->read_history_logevent_id =
          binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ReadHistoryInSecretChat, storer);
      LOG(INFO) << "Add read history logevent " << d->read_history_logevent_id;
    } else {
      auto new_logevent_id = binlog_rewrite(G()->td_db()->get_binlog(), d->read_history_logevent_id,
                                            LogEvent::HandlerType::ReadHistoryInSecretChat, storer);
      LOG(INFO) << "Rewrite read history logevent " << d->read_history_logevent_id << " with " << new_logevent_id;
    }
    d->read_history_logevent_id_generation++;
  } else if (G()->parameters().use_message_db) {
    ReadHistoryOnServerLogEvent logevent;
    logevent.dialog_id_ = dialog_id;
    logevent.max_message_id_ = max_message_id;

    auto storer = LogEventStorerImpl<ReadHistoryOnServerLogEvent>(logevent);
    if (d->read_history_logevent_id == 0) {
      d->read_history_logevent_id =
          binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ReadHistoryOnServer, storer);
      LOG(INFO) << "Add read history logevent " << d->read_history_logevent_id;
    } else {
      auto new_logevent_id = binlog_rewrite(G()->td_db()->get_binlog(), d->read_history_logevent_id,
                                            LogEvent::HandlerType::ReadHistoryOnServer, storer);
      LOG(INFO) << "Rewrite read history logevent " << d->read_history_logevent_id << " with " << new_logevent_id;
    }
    d->read_history_logevent_id_generation++;
  }

  bool need_delay = d->is_opened && !is_secret && d->server_unread_count > 0;
  pending_read_history_timeout_.set_timeout_in(dialog_id.get(), need_delay ? MIN_READ_HISTORY_DELAY : 0);
}

void MessagesManager::read_history_on_server_impl(DialogId dialog_id, MessageId max_message_id) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  auto message_id = d->last_read_inbox_message_id;
  if (dialog_id.get_type() != DialogType::SecretChat) {
    message_id = message_id.get_prev_server_message_id();
  }
  if (message_id.get() > max_message_id.get()) {
    max_message_id = message_id;
  }

  Promise<> promise;
  if (d->read_history_logevent_id != 0) {
    promise =
        PromiseCreator::lambda([actor_id = actor_id(this), dialog_id,
                                generation = d->read_history_logevent_id_generation](Result<Unit> result) mutable {
          if (!G()->close_flag()) {
            send_closure(actor_id, &MessagesManager::on_read_history_finished, dialog_id, generation);
          }
        });
  }

  LOG(INFO) << "Send read history request in " << dialog_id << " up to " << max_message_id;
  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
      td_->create_handler<ReadHistoryQuery>(std::move(promise))->send(dialog_id, max_message_id);
      break;
    case DialogType::Channel: {
      auto channel_id = dialog_id.get_channel_id();
      td_->create_handler<ReadChannelHistoryQuery>(std::move(promise))->send(channel_id, max_message_id);
      break;
    }
    case DialogType::SecretChat: {
      auto secret_chat_id = dialog_id.get_secret_chat_id();
      auto date = d->last_read_inbox_message_date;
      auto *message = get_message_force(d, max_message_id);
      if (message != nullptr && message->date > date) {
        date = message->date;
      }
      if (date == 0) {
        LOG(ERROR) << "Don't know last read inbox message date in " << dialog_id;
        break;
      }
      send_closure(G()->secret_chats_manager(), &SecretChatsManager::send_read_history, secret_chat_id, date,
                   std::move(promise));
      break;
    }
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

void MessagesManager::on_read_history_finished(DialogId dialog_id, uint64 generation) {
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  LOG(INFO) << "Finished reading history in " << dialog_id << " with logevent " << d->read_history_logevent_id;
  if (d->read_history_logevent_id_generation == generation) {
    CHECK(d->read_history_logevent_id != 0);
    LOG(INFO) << "Delete read history logevent " << d->read_history_logevent_id;
    binlog_erase(G()->td_db()->get_binlog(), d->read_history_logevent_id);
    d->read_history_logevent_id = 0;
  }
}

std::pair<int32, vector<MessageId>> MessagesManager::search_dialog_messages(
    DialogId dialog_id, const string &query, UserId sender_user_id, MessageId from_message_id, int32 offset,
    int32 limit, const tl_object_ptr<td_api::SearchMessagesFilter> &filter, int64 &random_id, bool use_db,
    Promise<Unit> &&promise) {
  if (random_id != 0) {
    // request has already been sent before
    auto it = found_dialog_messages_.find(random_id);
    if (it != found_dialog_messages_.end()) {
      auto result = std::move(it->second);
      found_dialog_messages_.erase(it);
      promise.set_value(Unit());
      return result;
    }
    random_id = 0;
  }
  LOG(INFO) << "Search messages with query \"" << query << "\" in " << dialog_id << " sent by " << sender_user_id
            << " filtered by " << to_string(filter) << " from " << from_message_id << " with offset " << offset
            << " and limit " << limit;

  std::pair<int32, vector<MessageId>> result;
  if (limit <= 0) {
    promise.set_error(Status::Error(3, "Parameter limit must be positive"));
    return result;
  }
  if (limit > MAX_SEARCH_MESSAGES) {
    limit = MAX_SEARCH_MESSAGES;
  }
  if (limit <= -offset) {
    promise.set_error(Status::Error(5, "Parameter limit must be greater than -offset"));
    return result;
  }
  if (offset > 0) {
    promise.set_error(Status::Error(5, "Parameter offset must be non-positive"));
    return result;
  }

  if (from_message_id.get() > MessageId::max().get()) {
    from_message_id = MessageId::max();
  }

  if (!from_message_id.is_valid() && from_message_id != MessageId()) {
    promise.set_error(Status::Error(3, "Parameter from_message_id must be identifier of the chat message or 0"));
    return result;
  }
  from_message_id = from_message_id.get_next_server_message_id();

  const Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    promise.set_error(Status::Error(6, "Chat not found"));
    return result;
  }

  auto input_user = td_->contacts_manager_->get_input_user(sender_user_id);
  if (sender_user_id.is_valid() && input_user == nullptr) {
    promise.set_error(Status::Error(6, "Wrong sender user identifier specified"));
    return result;
  }

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || found_dialog_messages_.find(random_id) != found_dialog_messages_.end());
  found_dialog_messages_[random_id];  // reserve place for result

  auto filter_type = get_search_messages_filter(filter);
  if (filter_type == SearchMessagesFilter::UnreadMention) {
    if (!query.empty()) {
      promise.set_error(Status::Error(6, "Non-empty query is unsupported with the specified filter"));
      return result;
    }
    if (input_user != nullptr) {
      promise.set_error(Status::Error(6, "Non-empty sender user is unsupported with the specified filter"));
      return result;
    }
  }

  // Trying to use database
  if (use_db && query.empty() && G()->parameters().use_message_db && filter_type != SearchMessagesFilter::Empty &&
      input_user == nullptr) {  // TODO support filter by users in the database
    MessageId first_db_message_id = get_first_database_message_id_by_index(d, filter_type);
    int32 message_count = d->message_count_by_index[search_messages_filter_index(filter_type)];
    auto fixed_from_message_id = from_message_id;
    if (fixed_from_message_id == MessageId()) {
      fixed_from_message_id = MessageId::max();
    }
    LOG(INFO) << "Search messages in " << dialog_id << " from " << fixed_from_message_id << ", have up to "
              << first_db_message_id << ", message_count = " << message_count;
    if ((first_db_message_id.get() < fixed_from_message_id.get() ||
         (first_db_message_id.get() == fixed_from_message_id.get() && offset < 0)) &&
        message_count != -1) {
      LOG(INFO) << "Search messages in database in " << dialog_id << " from " << fixed_from_message_id
                << " and with limit " << limit;
      auto new_promise = PromiseCreator::lambda(
          [random_id, dialog_id, fixed_from_message_id, first_db_message_id, filter_type, offset, limit,
           promise = std::move(promise)](Result<std::vector<BufferSlice>> r_messages) mutable {
            send_closure(G()->messages_manager(), &MessagesManager::on_search_dialog_messages_db_result, random_id,
                         dialog_id, fixed_from_message_id, first_db_message_id, filter_type, offset, limit,
                         std::move(r_messages), std::move(promise));
          });
      MessagesDbMessagesQuery db_query;
      db_query.dialog_id = dialog_id;
      db_query.index_mask = search_messages_filter_index_mask(filter_type);
      db_query.from_message_id = fixed_from_message_id;
      db_query.offset = offset;
      db_query.limit = limit;
      G()->td_db()->get_messages_db_async()->get_messages(db_query, std::move(new_promise));
      return result;
    }
  }

  LOG(DEBUG) << "Search messages on server in " << dialog_id << " with query \"" << query << "\" from user "
             << sender_user_id << " from " << from_message_id << " and with limit " << limit;

  switch (dialog_id.get_type()) {
    case DialogType::None:
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      td_->create_handler<SearchMessagesQuery>(std::move(promise))
          ->send(dialog_id, query, sender_user_id, std::move(input_user), from_message_id, offset, limit, filter_type,
                 random_id);
      break;
    case DialogType::SecretChat:
      if (filter_type == SearchMessagesFilter::UnreadMention) {
        promise.set_value(Unit());
      } else {
        promise.set_error(Status::Error(500, "Search messages in secret chats is not supported"));
      }
      break;
    default:
      UNREACHABLE();
      promise.set_error(Status::Error(500, "Search messages is not supported"));
  }
  return result;
}

std::pair<int32, vector<FullMessageId>> MessagesManager::search_call_messages(MessageId from_message_id, int32 limit,
                                                                              bool only_missed, int64 &random_id,
                                                                              bool use_db, Promise<Unit> &&promise) {
  if (random_id != 0) {
    // request has already been sent before
    auto it = found_call_messages_.find(random_id);
    if (it != found_call_messages_.end()) {
      auto result = std::move(it->second);
      found_call_messages_.erase(it);
      promise.set_value(Unit());
      return result;
    }
    random_id = 0;
  }
  LOG(INFO) << "Search call messages from " << from_message_id << " with limit " << limit;

  std::pair<int32, vector<FullMessageId>> result;
  if (limit <= 0) {
    promise.set_error(Status::Error(3, "Parameter limit must be positive"));
    return result;
  }
  if (limit > MAX_SEARCH_MESSAGES) {
    limit = MAX_SEARCH_MESSAGES;
  }

  if (from_message_id.get() > MessageId::max().get()) {
    from_message_id = MessageId::max();
  }

  if (!from_message_id.is_valid() && from_message_id != MessageId()) {
    promise.set_error(Status::Error(3, "Parameter from_message_id must be identifier of the chat message or 0"));
    return result;
  }
  from_message_id = from_message_id.get_next_server_message_id();

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || found_call_messages_.find(random_id) != found_call_messages_.end());
  found_call_messages_[random_id];  // reserve place for result

  auto filter_type = only_missed ? SearchMessagesFilter::MissedCall : SearchMessagesFilter::Call;

  if (use_db && G()->parameters().use_message_db) {
    // Try to use database
    MessageId first_db_message_id =
        calls_db_state_.first_calls_database_message_id_by_index[search_calls_filter_index(filter_type)];
    int32 message_count = calls_db_state_.message_count_by_index[search_calls_filter_index(filter_type)];
    auto fixed_from_message_id = from_message_id;
    if (fixed_from_message_id == MessageId()) {
      fixed_from_message_id = MessageId::max();
    }
    CHECK(fixed_from_message_id.is_valid() && fixed_from_message_id.is_server());
    LOG(INFO) << "Search call messages from " << fixed_from_message_id << ", have up to " << first_db_message_id
              << ", message_count = " << message_count;
    if (first_db_message_id.get() < fixed_from_message_id.get() && message_count != -1) {
      LOG(INFO) << "Search messages in database from " << fixed_from_message_id << " and with limit " << limit;

      MessagesDbCallsQuery db_query;
      db_query.index_mask = search_messages_filter_index_mask(filter_type);
      db_query.from_unique_message_id = fixed_from_message_id.get_server_message_id().get();
      db_query.limit = limit;
      G()->td_db()->get_messages_db_async()->get_calls(
          db_query, PromiseCreator::lambda([random_id, first_db_message_id, filter_type, promise = std::move(promise)](
                                               Result<MessagesDbCallsResult> calls_result) mutable {
            send_closure(G()->messages_manager(), &MessagesManager::on_messages_db_calls_result,
                         std::move(calls_result), random_id, first_db_message_id, filter_type, std::move(promise));
          }));
      return result;
    }
  }

  LOG(DEBUG) << "Search call messages on server from " << from_message_id << " and with limit " << limit;
  td_->create_handler<SearchMessagesQuery>(std::move(promise))
      ->send(DialogId(), "", UserId(), nullptr, from_message_id, 0, limit, filter_type, random_id);
  return result;
}

std::pair<int32, vector<MessageId>> MessagesManager::search_dialog_recent_location_messages(DialogId dialog_id,
                                                                                            int32 limit,
                                                                                            int64 &random_id,
                                                                                            Promise<Unit> &&promise) {
  if (random_id != 0) {
    // request has already been sent before
    auto it = found_dialog_recent_location_messages_.find(random_id);
    CHECK(it != found_dialog_recent_location_messages_.end());
    auto result = std::move(it->second);
    found_dialog_recent_location_messages_.erase(it);
    promise.set_value(Unit());
    return result;
  }
  LOG(INFO) << "Search recent location messages in " << dialog_id << " with limit " << limit;

  std::pair<int32, vector<MessageId>> result;
  if (limit <= 0) {
    promise.set_error(Status::Error(3, "Parameter limit must be positive"));
    return result;
  }
  if (limit > MAX_SEARCH_MESSAGES) {
    limit = MAX_SEARCH_MESSAGES;
  }

  const Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    promise.set_error(Status::Error(6, "Chat not found"));
    return result;
  }

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 ||
           found_dialog_recent_location_messages_.find(random_id) != found_dialog_recent_location_messages_.end());
  found_dialog_recent_location_messages_[random_id];  // reserve place for result

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      td_->create_handler<GetRecentLocationsQuery>(std::move(promise))->send(dialog_id, limit, random_id);
      break;
    case DialogType::SecretChat:
      promise.set_value(Unit());
      break;
    default:
      UNREACHABLE();
      promise.set_error(Status::Error(500, "Search messages is not supported"));
  }
  return result;
}

vector<FullMessageId> MessagesManager::get_active_live_location_messages(Promise<Unit> &&promise) {
  if (!G()->parameters().use_message_db) {
    are_active_live_location_messages_loaded_ = true;
  }

  if (!are_active_live_location_messages_loaded_) {
    load_active_live_location_messages_queries_.push_back(std::move(promise));
    if (load_active_live_location_messages_queries_.size() == 1u) {
      LOG(INFO) << "Trying to load active live location messages from database";
      G()->td_db()->get_sqlite_pmc()->get(
          "di_active_live_location_messages", PromiseCreator::lambda([](string value) {
            send_closure(G()->messages_manager(),
                         &MessagesManager::on_load_active_live_location_full_message_ids_from_database,
                         std::move(value));
          }));
    }
    return {};
  }

  promise.set_value(Unit());
  vector<FullMessageId> result;
  for (auto &full_message_id : active_live_location_full_message_ids_) {
    auto m = get_message(full_message_id);
    CHECK(m != nullptr);
    CHECK(m->content->get_type() == MessageContentType::LiveLocation);

    auto live_period = get_message_content_live_location_period(m->content.get());
    if (live_period <= G()->unix_time() - m->date) {  // bool is_expired flag?
      // live location is expired
      continue;
    }
    result.push_back(full_message_id);
  }

  return result;
}

void MessagesManager::on_load_active_live_location_full_message_ids_from_database(string value) {
  if (value.empty()) {
    LOG(INFO) << "Active live location messages aren't found in the database";
    on_load_active_live_location_messages_finished();
    return;
  }

  LOG(INFO) << "Successfully loaded active live location messages list of size " << value.size() << " from database";

  auto new_full_message_ids = std::move(active_live_location_full_message_ids_);
  vector<FullMessageId> old_full_message_ids;
  log_event_parse(old_full_message_ids, value).ensure();

  // TODO asynchronously load messages from database
  active_live_location_full_message_ids_.clear();
  for (auto full_message_id : old_full_message_ids) {
    Message *m = get_message_force(full_message_id);
    if (m != nullptr) {
      try_add_active_live_location(full_message_id.get_dialog_id(), m);
    }
  }

  for (auto full_message_id : new_full_message_ids) {
    add_active_live_location(full_message_id);
  }

  on_load_active_live_location_messages_finished();

  if (!new_full_message_ids.empty()) {
    save_active_live_locations();
  }
}

void MessagesManager::on_load_active_live_location_messages_finished() {
  are_active_live_location_messages_loaded_ = true;
  auto promises = std::move(load_active_live_location_messages_queries_);
  load_active_live_location_messages_queries_.clear();
  for (auto &promise : promises) {
    promise.set_value(Unit());
  }
}

void MessagesManager::try_add_active_live_location(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);

  if (m->content->get_type() != MessageContentType::LiveLocation) {
    return;
  }

  auto live_period = get_message_content_live_location_period(m->content.get());
  if (live_period <= G()->unix_time() - m->date + 1) {  // bool is_expired flag?
    // live location is expired
    return;
  }
  add_active_live_location({dialog_id, m->message_id});
}

void MessagesManager::add_active_live_location(FullMessageId full_message_id) {
  if (!active_live_location_full_message_ids_.insert(full_message_id).second) {
    return;
  }

  // TODO add timer for live location expiration

  if (are_active_live_location_messages_loaded_) {
    save_active_live_locations();
  }
}

bool MessagesManager::delete_active_live_location(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  return active_live_location_full_message_ids_.erase(FullMessageId{dialog_id, m->message_id}) != 0;
}

void MessagesManager::save_active_live_locations() {
  CHECK(are_active_live_location_messages_loaded_);
  LOG(INFO) << "Save active live locations of size " << active_live_location_full_message_ids_.size() << " to database";
  if (G()->parameters().use_message_db) {
    G()->td_db()->get_sqlite_pmc()->set("di_active_live_location_messages",
                                        log_event_store(active_live_location_full_message_ids_).as_slice().str(),
                                        Auto());
  }
}

MessageId MessagesManager::get_first_database_message_id_by_index(const Dialog *d, SearchMessagesFilter filter) {
  CHECK(d != nullptr);
  auto message_id = filter == SearchMessagesFilter::Empty
                        ? d->first_database_message_id
                        : d->first_database_message_id_by_index[search_messages_filter_index(filter)];
  if (!message_id.is_valid()) {
    if (d->dialog_id.get_type() == DialogType::SecretChat) {
      LOG(ERROR) << "Invalid first_database_message_id_by_index in " << d->dialog_id;
      return MessageId::min();
    }
    return MessageId::max();
  }
  return message_id;
}

void MessagesManager::on_search_dialog_messages_db_result(int64 random_id, DialogId dialog_id,
                                                          MessageId from_message_id, MessageId first_db_message_id,
                                                          SearchMessagesFilter filter_type, int32 offset, int32 limit,
                                                          Result<std::vector<BufferSlice>> r_messages,
                                                          Promise<> promise) {
  if (r_messages.is_error()) {
    LOG(ERROR) << r_messages.error();
    if (first_db_message_id != MessageId::min() && dialog_id.get_type() != DialogType::SecretChat) {
      found_dialog_messages_.erase(random_id);
    }
    return promise.set_value(Unit());
  }

  auto messages = r_messages.move_as_ok();

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  auto it = found_dialog_messages_.find(random_id);
  CHECK(it != found_dialog_messages_.end());
  auto &res = it->second.second;

  res.reserve(messages.size());
  for (auto &message : messages) {
    auto m = on_get_message_from_database(dialog_id, d, message);
    if (m != nullptr && first_db_message_id.get() <= m->message_id.get()) {
      res.push_back(m->message_id);
    }
  }

  auto &message_count = d->message_count_by_index[search_messages_filter_index(filter_type)];
  int32 result_size = narrow_cast<int32>(res.size());
  if ((message_count < result_size) ||
      (from_message_id == MessageId::max() && first_db_message_id == MessageId::min() && message_count > result_size &&
       result_size < limit + offset)) {
    LOG(INFO) << "Fix found message count in " << dialog_id << " from " << message_count << " to " << result_size;
    message_count = result_size;
    if (filter_type == SearchMessagesFilter::UnreadMention) {
      d->unread_mention_count = message_count;
      update_dialog_mention_notification_count(d);
      send_update_chat_unread_mention_count(d);
    }
    on_dialog_updated(dialog_id, "on_search_dialog_messages_db_result");
  }
  it->second.first = message_count;
  if (res.empty() && first_db_message_id != MessageId::min() && dialog_id.get_type() != DialogType::SecretChat) {
    LOG(INFO) << "No messages in database found";
    found_dialog_messages_.erase(it);
  } else {
    LOG(INFO) << "Found " << res.size() << " messages out of " << message_count << " in database";
  }
  promise.set_value(Unit());
}

std::pair<int64, vector<FullMessageId>> MessagesManager::offline_search_messages(
    DialogId dialog_id, const string &query, int64 from_search_id, int32 limit,
    const tl_object_ptr<td_api::SearchMessagesFilter> &filter, int64 &random_id, Promise<> &&promise) {
  if (!G()->parameters().use_message_db) {
    promise.set_error(Status::Error(400, "Message database is required to search messages in secret chats"));
    return {};
  }

  if (random_id != 0) {
    // request has already been sent before
    auto it = found_fts_messages_.find(random_id);
    CHECK(it != found_fts_messages_.end());
    auto result = std::move(it->second);
    found_fts_messages_.erase(it);
    promise.set_value(Unit());
    return result;
  }

  if (query.empty()) {
    promise.set_value(Unit());
    return {};
  }
  if (dialog_id != DialogId() && !have_dialog_force(dialog_id)) {
    promise.set_error(Status::Error(400, "Chat not found"));
    return {};
  }
  if (limit <= 0) {
    promise.set_error(Status::Error(400, "Limit must be positive"));
    return {};
  }
  if (limit > MAX_SEARCH_MESSAGES) {
    limit = MAX_SEARCH_MESSAGES;
  }

  MessagesDbFtsQuery fts_query;
  fts_query.query = query;
  fts_query.dialog_id = dialog_id;
  fts_query.index_mask = search_messages_filter_index_mask(get_search_messages_filter(filter));
  fts_query.from_search_id = from_search_id;
  fts_query.limit = limit;

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || found_fts_messages_.find(random_id) != found_fts_messages_.end());
  found_fts_messages_[random_id];  // reserve place for result

  G()->td_db()->get_messages_db_async()->get_messages_fts(
      std::move(fts_query),
      PromiseCreator::lambda([random_id, promise = std::move(promise)](Result<MessagesDbFtsResult> fts_result) mutable {
        send_closure(G()->messages_manager(), &MessagesManager::on_messages_db_fts_result, std::move(fts_result),
                     random_id, std::move(promise));
      }));

  return {};
}

void MessagesManager::on_messages_db_fts_result(Result<MessagesDbFtsResult> result, int64 random_id,
                                                Promise<> &&promise) {
  if (result.is_error()) {
    found_fts_messages_.erase(random_id);
    return promise.set_error(result.move_as_error());
  }
  auto fts_result = result.move_as_ok();

  auto it = found_fts_messages_.find(random_id);
  CHECK(it != found_fts_messages_.end());
  auto &res = it->second.second;

  res.reserve(fts_result.messages.size());
  for (auto &message : fts_result.messages) {
    auto m = on_get_message_from_database(message.dialog_id, get_dialog_force(message.dialog_id), message.data);
    if (m != nullptr) {
      res.push_back(FullMessageId(message.dialog_id, m->message_id));
    }
  }

  it->second.first = fts_result.next_search_id;

  promise.set_value(Unit());
}

void MessagesManager::on_messages_db_calls_result(Result<MessagesDbCallsResult> result, int64 random_id,
                                                  MessageId first_db_message_id, SearchMessagesFilter filter,
                                                  Promise<> &&promise) {
  if (result.is_error()) {
    found_call_messages_.erase(random_id);
    return promise.set_error(result.move_as_error());
  }
  auto calls_result = result.move_as_ok();

  auto it = found_call_messages_.find(random_id);
  CHECK(it != found_call_messages_.end());
  auto &res = it->second.second;

  res.reserve(calls_result.messages.size());
  for (auto &message : calls_result.messages) {
    auto m = on_get_message_from_database(message.dialog_id, get_dialog_force(message.dialog_id), message.data);

    if (m != nullptr && first_db_message_id.get() <= m->message_id.get()) {
      res.push_back(FullMessageId(message.dialog_id, m->message_id));
    }
  }
  it->second.first = calls_db_state_.message_count_by_index[search_calls_filter_index(filter)];

  if (res.empty() && first_db_message_id != MessageId::min()) {
    LOG(INFO) << "No messages in database found";
    found_call_messages_.erase(it);
  }

  promise.set_value(Unit());
}

std::pair<int32, vector<FullMessageId>> MessagesManager::search_messages(const string &query, int32 offset_date,
                                                                         DialogId offset_dialog_id,
                                                                         MessageId offset_message_id, int32 limit,
                                                                         int64 &random_id, Promise<Unit> &&promise) {
  if (random_id != 0) {
    // request has already been sent before
    auto it = found_messages_.find(random_id);
    CHECK(it != found_messages_.end());
    auto result = std::move(it->second);
    found_messages_.erase(it);
    promise.set_value(Unit());
    return result;
  }

  std::pair<int32, vector<FullMessageId>> result;
  if (limit <= 0) {
    promise.set_error(Status::Error(3, "Parameter limit must be positive"));
    return result;
  }
  if (limit > MAX_SEARCH_MESSAGES) {
    limit = MAX_SEARCH_MESSAGES;
  }

  if (offset_date <= 0) {
    offset_date = std::numeric_limits<int32>::max();
  }
  if (!offset_message_id.is_valid()) {
    offset_message_id = MessageId();
  }
  if (offset_message_id != MessageId() && !offset_message_id.is_server()) {
    promise.set_error(
        Status::Error(3, "Parameter offset_message_id must be identifier of the last found message or 0"));
    return result;
  }

  if (query.empty()) {
    promise.set_value(Unit());
    return result;
  }

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || found_messages_.find(random_id) != found_messages_.end());
  found_messages_[random_id];  // reserve place for result

  LOG(DEBUG) << "Search messages globally with query = \"" << query << "\" from date " << offset_date << ", "
             << offset_dialog_id << ", " << offset_message_id << " and with limit " << limit;

  td_->create_handler<SearchMessagesGlobalQuery>(std::move(promise))
      ->send(query, offset_date, offset_dialog_id, offset_message_id, limit, random_id);
  return result;
}

int64 MessagesManager::get_dialog_message_by_date(DialogId dialog_id, int32 date, Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    promise.set_error(Status::Error(5, "Chat not found"));
    return 0;
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(5, "Can't access the chat"));
    return 0;
  }

  if (date <= 0) {
    date = 1;
  }

  int64 random_id = 0;
  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 ||
           get_dialog_message_by_date_results_.find(random_id) != get_dialog_message_by_date_results_.end());
  get_dialog_message_by_date_results_[random_id];  // reserve place for result

  auto message_id = find_message_by_date(d->messages, date);
  if (message_id.is_valid() && (message_id == d->last_message_id || get_message(d, message_id)->have_next)) {
    get_dialog_message_by_date_results_[random_id] = {dialog_id, message_id};
    promise.set_value(Unit());
    return random_id;
  }

  if (G()->parameters().use_message_db && d->last_database_message_id != MessageId()) {
    CHECK(d->first_database_message_id != MessageId());
    G()->td_db()->get_messages_db_async()->get_dialog_message_by_date(
        dialog_id, d->first_database_message_id, d->last_database_message_id, date,
        PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, date, random_id,
                                promise = std::move(promise)](Result<BufferSlice> result) mutable {
          send_closure(actor_id, &MessagesManager::on_get_dialog_message_by_date_from_database, dialog_id, date,
                       random_id, std::move(result), std::move(promise));
        }));
  } else {
    get_dialog_message_by_date_from_server(d, date, random_id, false, std::move(promise));
  }
  return random_id;
}

MessageId MessagesManager::find_message_by_date(const unique_ptr<Message> &m, int32 date) {
  if (m == nullptr) {
    return MessageId();
  }

  if (m->date > date) {
    return find_message_by_date(m->left, date);
  }

  auto message_id = find_message_by_date(m->right, date);
  if (message_id.is_valid()) {
    return message_id;
  }

  return m->message_id;
}

void MessagesManager::on_get_dialog_message_by_date_from_database(DialogId dialog_id, int32 date, int64 random_id,
                                                                  Result<BufferSlice> result, Promise<Unit> promise) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (result.is_ok()) {
    Message *m = on_get_message_from_database(dialog_id, d, result.ok());
    if (m != nullptr) {
      auto message_id = find_message_by_date(d->messages, date);
      if (!message_id.is_valid()) {
        LOG(ERROR) << "Failed to find " << m->message_id << " in " << dialog_id << " by date " << date;
        message_id = m->message_id;
      }
      get_dialog_message_by_date_results_[random_id] = {dialog_id, message_id};
      promise.set_value(Unit());
      return;
    }
    // TODO if m == nullptr, we need to just adjust it to the next non-nullptr message, not get from server
  }

  return get_dialog_message_by_date_from_server(d, date, random_id, true, std::move(promise));
}

void MessagesManager::get_dialog_message_by_date_from_server(const Dialog *d, int32 date, int64 random_id,
                                                             bool after_database_search, Promise<Unit> &&promise) {
  CHECK(d != nullptr);
  if (d->have_full_history) {
    // request can be always done locally/in memory. There is no need to send request to the server
    if (after_database_search) {
      return promise.set_value(Unit());
    }

    auto message_id = find_message_by_date(d->messages, date);
    if (message_id.is_valid()) {
      get_dialog_message_by_date_results_[random_id] = {d->dialog_id, message_id};
    }
    promise.set_value(Unit());
    return;
  }
  if (d->dialog_id.get_type() == DialogType::SecretChat) {
    // there is no way to send request to the server
    return promise.set_value(Unit());
  }

  td_->create_handler<GetDialogMessageByDateQuery>(std::move(promise))->send(d->dialog_id, date, random_id);
}

void MessagesManager::on_get_dialog_message_by_date_success(DialogId dialog_id, int32 date, int64 random_id,
                                                            vector<tl_object_ptr<telegram_api::Message>> &&messages) {
  auto it = get_dialog_message_by_date_results_.find(random_id);
  CHECK(it != get_dialog_message_by_date_results_.end());
  auto &result = it->second;
  CHECK(result == FullMessageId());

  for (auto &message : messages) {
    auto message_date = get_message_date(message);
    auto message_dialog_id = get_message_dialog_id(message);
    if (message_dialog_id != dialog_id) {
      LOG(ERROR) << "Receive message in wrong " << message_dialog_id << " instead of " << dialog_id;
      continue;
    }
    if (message_date != 0 && message_date <= date) {
      result = on_get_message(std::move(message), false, dialog_id.get_type() == DialogType::Channel, false, false,
                              "on_get_dialog_message_by_date_success");
      if (result != FullMessageId()) {
        const Dialog *d = get_dialog(dialog_id);
        CHECK(d != nullptr);
        auto message_id = find_message_by_date(d->messages, date);
        if (!message_id.is_valid()) {
          LOG(ERROR) << "Failed to find " << result.get_message_id() << " in " << dialog_id << " by date " << date;
          message_id = result.get_message_id();
        }
        get_dialog_message_by_date_results_[random_id] = {dialog_id, message_id};
        // TODO result must be adjusted by local messages
        return;
      }
    }
  }
}

void MessagesManager::on_get_dialog_message_by_date_fail(int64 random_id) {
  auto erased = get_dialog_message_by_date_results_.erase(random_id);
  CHECK(erased > 0);
}

tl_object_ptr<td_api::message> MessagesManager::get_dialog_message_by_date_object(int64 random_id) {
  auto it = get_dialog_message_by_date_results_.find(random_id);
  CHECK(it != get_dialog_message_by_date_results_.end());
  auto full_message_id = std::move(it->second);
  get_dialog_message_by_date_results_.erase(it);
  return get_message_object(full_message_id);
}

int32 MessagesManager::get_dialog_message_count(DialogId dialog_id,
                                                const tl_object_ptr<td_api::SearchMessagesFilter> &filter,
                                                bool return_local, int64 &random_id, Promise<Unit> &&promise) {
  if (random_id != 0) {
    // request has already been sent before
    auto it = found_dialog_messages_.find(random_id);
    CHECK(it != found_dialog_messages_.end());
    auto result = std::move(it->second);
    found_dialog_messages_.erase(it);
    promise.set_value(Unit());
    return result.first;
  }

  LOG(INFO) << "Get " << (return_local ? "local " : "") << "number of messages in " << dialog_id << " filtered by "
            << to_string(filter);

  const Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    promise.set_error(Status::Error(6, "Chat not found"));
    return -1;
  }

  auto filter_type = get_search_messages_filter(filter);
  if (filter_type == SearchMessagesFilter::Empty) {
    promise.set_error(Status::Error(6, "SearchMessagesFilterEmpty is not supported"));
    return -1;
  }

  auto dialog_type = dialog_id.get_type();
  int32 message_count = d->message_count_by_index[search_messages_filter_index(filter_type)];
  if (message_count == -1) {
    if (filter_type == SearchMessagesFilter::UnreadMention) {
      message_count = d->unread_mention_count;
    }
  }
  if (message_count != -1 || return_local || dialog_type == DialogType::SecretChat) {
    promise.set_value(Unit());
    return message_count;
  }

  LOG(INFO) << "Get number of messages in " << dialog_id << " filtered by " << to_string(filter) << " from the server";

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || found_dialog_messages_.find(random_id) != found_dialog_messages_.end());
  found_dialog_messages_[random_id];  // reserve place for result

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      td_->create_handler<SearchMessagesQuery>(std::move(promise))
          ->send(dialog_id, "", UserId(), nullptr, MessageId(), 0, 1, filter_type, random_id);
      break;
    case DialogType::None:
    case DialogType::SecretChat:
    default:
      UNREACHABLE();
  }
  return -1;
}

void MessagesManager::preload_newer_messages(const Dialog *d, MessageId max_message_id) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  MessagesConstIterator p(d, max_message_id);
  int32 limit = MAX_GET_HISTORY * 3 / 10;
  while (*p != nullptr && limit-- > 0) {
    ++p;
    if (*p) {
      max_message_id = (*p)->message_id;
    }
  }
  if (limit > 0 && (d->last_message_id == MessageId() || max_message_id.get() < d->last_message_id.get())) {
    // need to preload some new messages
    LOG(INFO) << "Preloading newer after " << max_message_id;
    load_messages(d->dialog_id, max_message_id, -MAX_GET_HISTORY + 1, MAX_GET_HISTORY, 3, false, Promise<Unit>());
  }
}

void MessagesManager::preload_older_messages(const Dialog *d, MessageId min_message_id) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  /*
    if (d->first_remote_message_id == -1) {
      // nothing left to preload from server
      return;
    }
  */
  MessagesConstIterator p(d, min_message_id);
  int32 limit = MAX_GET_HISTORY * 3 / 10 + 1;
  while (*p != nullptr && limit-- > 0) {
    min_message_id = (*p)->message_id;
    --p;
  }
  if (limit > 0) {
    // need to preload some old messages
    LOG(INFO) << "Preloading older before " << min_message_id;
    load_messages(d->dialog_id, min_message_id, 0, MAX_GET_HISTORY / 2, 3, false, Promise<Unit>());
  }
}

unique_ptr<MessagesManager::Message> MessagesManager::parse_message(DialogId dialog_id, const BufferSlice &value) {
  LOG(INFO) << "Loaded message of size " << value.size() << " from database";
  auto m = make_unique<Message>();

  auto status = log_event_parse(*m, value.as_slice());
  if (status.is_error() || !m->message_id.is_valid()) {
    // can't happen unless database is broken, but has been seen in the wild
    LOG(ERROR) << "Receive invalid message from database: " << m->message_id << ' ' << status << ' '
               << format::as_hex_dump<4>(value.as_slice());
    if (dialog_id.get_type() != DialogType::SecretChat && m->message_id.is_valid() && m->message_id.is_server()) {
      // trying to repair the message
      get_messages_from_server({FullMessageId{dialog_id, m->message_id}}, Auto());
    }
    return nullptr;
  }

  return m;
}

void MessagesManager::on_get_history_from_database(DialogId dialog_id, MessageId from_message_id, int32 offset,
                                                   int32 limit, bool from_the_end, bool only_local,
                                                   vector<BufferSlice> &&messages, Promise<Unit> &&promise) {
  CHECK(-limit < offset && offset <= 0);
  CHECK(offset < 0 || from_the_end);

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    LOG(WARNING) << "Ignore result of get_history_from_database in " << dialog_id;
    promise.set_value(Unit());
    return;
  }

  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  LOG(INFO) << "Receive " << messages.size() << " history messages from database "
            << (from_the_end ? "from the end " : "") << "in " << dialog_id << " from " << from_message_id
            << " with offset " << offset << " and limit " << limit << ". First database message is "
            << d->first_database_message_id << ", have_full_history = " << d->have_full_history;

  if (messages.empty() && from_the_end && d->have_full_history) {
    set_dialog_is_empty(d, "on_get_history_from_database empty");
  }

  bool have_next = false;
  bool need_update = false;
  bool need_update_dialog_pos = false;
  bool added_new_message = false;
  MessageId last_added_message_id;
  Message *next_message = nullptr;
  Dependencies dependencies;
  bool is_first = true;
  bool had_full_history = d->have_full_history;
  auto debug_first_message_id = d->first_database_message_id;
  for (auto &message_slice : messages) {
    if (!d->first_database_message_id.is_valid() && !d->have_full_history) {
      break;
    }
    auto message = parse_message(dialog_id, std::move(message_slice));
    if (message == nullptr) {
      if (d->have_full_history) {
        d->have_full_history = false;
        on_dialog_updated(dialog_id, "drop have_full_history in on_get_history_from_database");
      }
      break;
    }
    if (message->message_id.get() < d->first_database_message_id.get()) {
      if (d->have_full_history) {
        LOG(ERROR) << "Have full history in the " << dialog_id << " and receive " << message->message_id
                   << " from database, but first database message is " << d->first_database_message_id;
      } else {
        break;
      }
    }
    if (!have_next &&
        (from_the_end || (is_first && offset < -1 && message->message_id.get() <= from_message_id.get())) &&
        message->message_id.get() < d->last_message_id.get()) {
      // last message in the dialog must be attached to the next local message
      have_next = true;
    }

    message->have_previous = false;
    message->have_next = have_next;
    message->from_database = true;

    auto old_message = get_message(d, message->message_id);
    if (old_message == nullptr) {
      auto web_page_id = get_message_content_web_page_id(message->content.get());
      if (web_page_id.is_valid()) {
        td_->web_pages_manager_->have_web_page_force(web_page_id);
      }
    }
    Message *m = old_message ? old_message
                             : add_message_to_dialog(d, std::move(message), false, &need_update,
                                                     &need_update_dialog_pos, "on_get_history_from_database");
    if (m != nullptr) {
      if (!have_next) {
        last_added_message_id = m->message_id;
      }
      if (old_message == nullptr) {
        add_message_dependencies(dependencies, dialog_id, m);
        added_new_message = true;
      } else if (m->message_id != from_message_id) {
        added_new_message = true;
      }
      if (next_message != nullptr && !next_message->have_previous) {
        CHECK(m->message_id.get() < next_message->message_id.get()) << m->message_id << ' ' << next_message->message_id;
        LOG(INFO) << "Fix have_previous for " << next_message->message_id;
        next_message->have_previous = true;
        attach_message_to_previous(
            d, next_message->message_id,
            (PSLICE() << "on_get_history_from_database 1 " << m->message_id << ' ' << from_message_id << ' ' << offset
                      << ' ' << limit << ' ' << d->first_database_message_id << ' ' << d->have_full_history)
                .c_str());
      }

      have_next = true;
      next_message = m;
    }
    is_first = false;
  }
  resolve_dependencies_force(dependencies);

  if (!added_new_message && !only_local && dialog_id.get_type() != DialogType::SecretChat) {
    if (from_the_end) {
      from_message_id = MessageId();
    }
    load_messages(dialog_id, from_message_id, offset, limit, 1, false, std::move(promise));
    return;
  }

  if (from_the_end && last_added_message_id.is_valid()) {
    // CHECK(d->first_database_message_id.is_valid());
    // CHECK(last_added_message_id.get() >= d->first_database_message_id.get());
    if ((had_full_history || d->have_full_history) && !d->last_new_message_id.is_valid() &&
        (last_added_message_id.is_server() || d->dialog_id.get_type() == DialogType::SecretChat)) {
      LOG(ERROR) << "Trying to hard fix " << d->dialog_id << " last new message to " << last_added_message_id
                 << " from on_get_history_from_database 2";
      d->last_new_message_id = last_added_message_id;
      on_dialog_updated(d->dialog_id, "on_get_history_from_database 3");
    }
    if (last_added_message_id.get() > d->last_message_id.get() && d->last_new_message_id.is_valid()) {
      set_dialog_last_message_id(d, last_added_message_id, "on_get_history_from_database 4");
      need_update_dialog_pos = true;
    }
    if (last_added_message_id.get() != d->last_database_message_id.get()) {
      auto debug_last_database_message_id = d->last_database_message_id;
      set_dialog_last_database_message_id(d, last_added_message_id, "on_get_history_from_database 5");
      if (last_added_message_id.get() < d->first_database_message_id.get() ||
          !d->first_database_message_id.is_valid()) {
        CHECK(next_message != nullptr);
        CHECK(had_full_history || d->have_full_history)
            << had_full_history << ' ' << d->have_full_history << ' ' << next_message->message_id << ' '
            << last_added_message_id << ' ' << d->first_database_message_id << ' ' << debug_first_message_id << ' '
            << d->last_database_message_id << ' ' << debug_last_database_message_id;
        CHECK(next_message->message_id.get() <= d->last_database_message_id.get());
        LOG(ERROR) << "Fix first database message id in " << dialog_id << " from " << d->first_database_message_id
                   << " to " << next_message->message_id;
        set_dialog_first_database_message_id(d, next_message->message_id, "on_get_history_from_database 6");
      }
    }
  }

  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "on_get_history_from_database 7");
  }

  promise.set_value(Unit());
}

void MessagesManager::get_history_from_the_end(DialogId dialog_id, bool from_database, bool only_local,
                                               Promise<Unit> &&promise) {
  CHECK(dialog_id.is_valid());
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    // can't get history in dialogs without read access
    return promise.set_value(Unit());
  }
  const int32 limit = MAX_GET_HISTORY;
  if (from_database && G()->parameters().use_message_db) {
    LOG(INFO) << "Get history from the end of " << dialog_id << " from database";
    MessagesDbMessagesQuery db_query;
    db_query.dialog_id = dialog_id;
    db_query.from_message_id = MessageId::max();
    db_query.limit = limit;
    G()->td_db()->get_messages_db_async()->get_messages(
        db_query, PromiseCreator::lambda([dialog_id, only_local, limit, actor_id = actor_id(this),
                                          promise = std::move(promise)](std::vector<BufferSlice> messages) mutable {
          send_closure(actor_id, &MessagesManager::on_get_history_from_database, dialog_id, MessageId::max(), 0, limit,
                       true, only_local, std::move(messages), std::move(promise));
        }));
  } else {
    if (only_local || dialog_id.get_type() == DialogType::SecretChat) {
      promise.set_value(Unit());
      return;
    }

    LOG(INFO) << "Get history from the end of " << dialog_id << " from server";
    td_->create_handler<GetHistoryQuery>(std::move(promise))->send_get_from_the_end(dialog_id, limit);
  }
}

void MessagesManager::get_history(DialogId dialog_id, MessageId from_message_id, int32 offset, int32 limit,
                                  bool from_database, bool only_local, Promise<Unit> &&promise) {
  CHECK(dialog_id.is_valid());
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    // can't get history in dialogs without read access
    return promise.set_value(Unit());
  }
  if (from_database && G()->parameters().use_message_db) {
    LOG(INFO) << "Get history in " << dialog_id << " from " << from_message_id << " with offset " << offset
              << " and limit " << limit << " from database";
    MessagesDbMessagesQuery db_query;
    db_query.dialog_id = dialog_id;
    db_query.from_message_id = from_message_id;
    db_query.offset = offset;
    db_query.limit = limit;
    G()->td_db()->get_messages_db_async()->get_messages(
        db_query,
        PromiseCreator::lambda([dialog_id, from_message_id, offset, limit, only_local, actor_id = actor_id(this),
                                promise = std::move(promise)](std::vector<BufferSlice> messages) mutable {
          send_closure(actor_id, &MessagesManager::on_get_history_from_database, dialog_id, from_message_id, offset,
                       limit, false, only_local, std::move(messages), std::move(promise));
        }));
  } else {
    if (only_local || dialog_id.get_type() == DialogType::SecretChat) {
      return promise.set_value(Unit());
    }

    LOG(INFO) << "Get history in " << dialog_id << " from " << from_message_id << " with offset " << offset
              << " and limit " << limit << " from server";
    td_->create_handler<GetHistoryQuery>(std::move(promise))
        ->send(dialog_id, from_message_id.get_next_server_message_id(), offset, limit);
  }
}

void MessagesManager::load_messages(DialogId dialog_id, MessageId from_message_id, int32 offset, int32 limit,
                                    int left_tries, bool only_local, Promise<Unit> &&promise) {
  LOG(INFO) << "Load " << (only_local ? "local " : "") << "messages in " << dialog_id << " from " << from_message_id
            << " with offset = " << offset << " and limit = " << limit << ". " << left_tries << " tries left";
  CHECK(offset <= 0);
  CHECK(left_tries > 0);
  only_local |= dialog_id.get_type() == DialogType::SecretChat;
  if (!only_local) {
    Dialog *d = get_dialog(dialog_id);
    if (d != nullptr && d->have_full_history) {
      LOG(INFO) << "Have full history in " << dialog_id << ", so don't need to get chat history from server";
      only_local = true;
    }
  }
  bool from_database = (left_tries > 2 || only_local) && G()->parameters().use_message_db;
  // TODO do not send requests to database if (from_message_id < d->first_database_message_id ||
  // !d->first_database_message_id.is_valid()) && !d->have_full_history

  if (from_message_id == MessageId()) {
    get_history_from_the_end(dialog_id, from_database, only_local, std::move(promise));
    return;
  }
  if (offset >= -1) {
    // get history before some server or local message
    limit = min(max(limit + offset + 1, MAX_GET_HISTORY / 2), MAX_GET_HISTORY);
    offset = -1;
  } else {
    // get history around some server or local message
    int32 messages_to_load = max(MAX_GET_HISTORY, limit);
    int32 max_add = max(messages_to_load - limit - 2, 0);
    offset -= max_add;
    limit = MAX_GET_HISTORY;
  }
  get_history(dialog_id, from_message_id, offset, limit, from_database, only_local, std::move(promise));
}

tl_object_ptr<td_api::message> MessagesManager::get_message_object(FullMessageId full_message_id) {
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog(dialog_id);
  return get_message_object(dialog_id, get_message_force(d, full_message_id.get_message_id()));
}

tl_object_ptr<td_api::message> MessagesManager::get_message_object(DialogId dialog_id, const Message *message) const {
  if (message == nullptr) {
    return nullptr;
  }

  // TODO get_message_sending_state_object
  tl_object_ptr<td_api::MessageSendingState> sending_state;
  if (message->is_failed_to_send) {
    sending_state = make_tl_object<td_api::messageSendingStateFailed>();
  } else if (message->message_id.is_yet_unsent()) {
    sending_state = make_tl_object<td_api::messageSendingStatePending>();
  }

  bool can_delete = true;
  auto dialog_type = dialog_id.get_type();
  auto is_bot = td_->auth_manager_->is_bot();
  if (dialog_type == DialogType::Channel) {
    auto dialog_status = td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id());
    can_delete = can_delete_channel_message(dialog_status, message, is_bot);
  }

  DialogId my_dialog_id = get_my_dialog_id();
  bool can_delete_for_self = false;
  bool can_delete_for_all_users = can_delete && can_revoke_message(dialog_id, message);
  if (can_delete) {
    switch (dialog_type) {
      case DialogType::User:
      case DialogType::Chat:
        // TODO allow to delete yet unsent message just for self
        can_delete_for_self = !message->message_id.is_yet_unsent() || dialog_id == my_dialog_id;
        break;
      case DialogType::Channel:
      case DialogType::SecretChat:
        can_delete_for_self = !can_delete_for_all_users;
        break;
      case DialogType::None:
      default:
        UNREACHABLE();
    }
  }

  bool is_outgoing = message->is_outgoing || sending_state != nullptr;
  if (dialog_id == my_dialog_id) {
    // in Saved Messages all messages without forward_info->from_dialog_id should be outgoing
    if (message->forward_info == nullptr || !message->forward_info->from_dialog_id.is_valid()) {
      is_outgoing = true;
    }
  }
  return make_tl_object<td_api::message>(
      message->message_id.get(), td_->contacts_manager_->get_user_id_object(message->sender_user_id, "sender_user_id"),
      dialog_id.get(), std::move(sending_state), is_outgoing, can_edit_message(dialog_id, message, false, is_bot),
      can_forward_message(dialog_id, message), can_delete_for_self, can_delete_for_all_users, message->is_channel_post,
      message->contains_unread_mention, message->date, message->edit_date,
      get_message_forward_info_object(message->forward_info), message->reply_to_message_id.get(), message->ttl,
      message->ttl_expires_at != 0 ? max(message->ttl_expires_at - Time::now(), 1e-3) : message->ttl,
      td_->contacts_manager_->get_user_id_object(message->via_bot_user_id, "via_bot_user_id"),
      message->author_signature, message->views, message->media_album_id,
      get_message_content_object(message->content.get(), td_, message->date, message->is_content_secret),
      get_reply_markup_object(message->reply_markup));
}

tl_object_ptr<td_api::messages> MessagesManager::get_messages_object(int32 total_count, DialogId dialog_id,
                                                                     const vector<MessageId> &message_ids) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  return get_messages_object(total_count, transform(message_ids, [this, dialog_id, d](MessageId message_id) {
                               return get_message_object(dialog_id, get_message_force(d, message_id));
                             }));
}

tl_object_ptr<td_api::messages> MessagesManager::get_messages_object(int32 total_count,
                                                                     const vector<FullMessageId> &full_message_ids) {
  return get_messages_object(total_count, transform(full_message_ids, [this](FullMessageId full_message_id) {
                               return get_message_object(full_message_id);
                             }));
}

tl_object_ptr<td_api::messages> MessagesManager::get_messages_object(
    int32 total_count, vector<tl_object_ptr<td_api::message>> &&messages) {
  if (total_count == -1) {
    total_count = narrow_cast<int32>(messages.size());
  }
  return td_api::make_object<td_api::messages>(total_count, std::move(messages));
}

MessagesManager::Message *MessagesManager::get_message_to_send(Dialog *d, MessageId reply_to_message_id,
                                                               bool disable_notification, bool from_background,
                                                               unique_ptr<MessageContent> &&content,
                                                               bool *need_update_dialog_pos,
                                                               unique_ptr<MessageForwardInfo> forward_info) {
  CHECK(d != nullptr);
  MessageId message_id = get_next_yet_unsent_message_id(d);
  DialogId dialog_id = d->dialog_id;
  LOG(INFO) << "Create " << message_id << " in " << dialog_id;

  auto dialog_type = dialog_id.get_type();
  auto my_id = td_->contacts_manager_->get_my_id();

  auto m = make_unique<Message>();
  m->random_y = get_random_y(message_id);
  m->message_id = message_id;
  bool is_channel_post = is_broadcast_channel(dialog_id);
  if (is_channel_post) {
    // sender of the post can be hidden
    if (td_->contacts_manager_->get_channel_sign_messages(dialog_id.get_channel_id())) {
      m->author_signature = td_->contacts_manager_->get_user_title(my_id);
    }
  } else {
    m->sender_user_id = my_id;
  }
  m->date = G()->unix_time();
  m->send_date = m->date;
  m->reply_to_message_id = reply_to_message_id;
  m->is_channel_post = is_channel_post;
  m->is_outgoing = dialog_id != DialogId(my_id);
  m->from_background = from_background;
  m->views = is_channel_post ? 1 : 0;
  m->content = std::move(content);
  m->forward_info = std::move(forward_info);

  if (td_->auth_manager_->is_bot() || disable_notification) {
    m->disable_notification = disable_notification;
  } else {
    auto notification_settings = get_dialog_notification_settings(dialog_id, true);
    CHECK(notification_settings != nullptr);
    m->disable_notification = notification_settings->silent_send_message;
  }

  if (dialog_type == DialogType::SecretChat) {
    m->ttl = td_->contacts_manager_->get_secret_chat_ttl(dialog_id.get_secret_chat_id());
    if (is_service_message_content(m->content->get_type())) {
      m->ttl = 0;
    }
    m->is_content_secret = is_secret_message_content(m->ttl, m->content->get_type());
    if (reply_to_message_id.is_valid()) {
      auto *reply_to_message = get_message_force(d, reply_to_message_id);
      if (reply_to_message != nullptr) {
        m->reply_to_random_id = reply_to_message->random_id;
      } else {
        m->reply_to_message_id = MessageId();
      }
    }
  }

  m->have_previous = true;
  m->have_next = true;

  do {
    m->random_id = Random::secure_int64();
  } while (m->random_id == 0 || message_random_ids_.find(m->random_id) != message_random_ids_.end());
  message_random_ids_.insert(m->random_id);

  bool need_update = false;
  CHECK(have_input_peer(dialog_id, AccessRights::Read));
  auto result = add_message_to_dialog(d, std::move(m), true, &need_update, need_update_dialog_pos, "send message");
  CHECK(result != nullptr);
  return result;
}

int64 MessagesManager::begin_send_message(DialogId dialog_id, const Message *m) {
  LOG(INFO) << "Begin to send " << FullMessageId(dialog_id, m->message_id) << " with random_id = " << m->random_id;
  CHECK(m->random_id != 0 && being_sent_messages_.find(m->random_id) == being_sent_messages_.end());
  CHECK(m->message_id.is_yet_unsent());
  being_sent_messages_[m->random_id] = FullMessageId(dialog_id, m->message_id);
  debug_being_sent_messages_[m->random_id] = dialog_id;
  return m->random_id;
}

Status MessagesManager::can_send_message(DialogId dialog_id) const {
  if (!have_input_peer(dialog_id, AccessRights::Write)) {
    return Status::Error(400, "Have no write access to the chat");
  }

  if (dialog_id.get_type() == DialogType::Channel) {
    auto channel_id = dialog_id.get_channel_id();
    auto channel_type = td_->contacts_manager_->get_channel_type(channel_id);
    auto channel_status = td_->contacts_manager_->get_channel_status(channel_id);

    switch (channel_type) {
      case ChannelType::Unknown:
      case ChannelType::Megagroup:
        if (!channel_status.can_send_messages()) {
          return Status::Error(400, "Have no rights to send a message");
        }
        break;
      case ChannelType::Broadcast: {
        if (!channel_status.can_post_messages()) {
          return Status::Error(400, "Need administrator rights in the channel chat");
        }
        break;
      }
      default:
        UNREACHABLE();
    }
  }
  return Status::OK();
}

Status MessagesManager::can_send_message_content(DialogId dialog_id, const MessageContent *content,
                                                 bool is_forward) const {
  auto dialog_type = dialog_id.get_type();
  int32 secret_chat_layer = std::numeric_limits<int32>::max();
  if (dialog_type == DialogType::SecretChat) {
    auto secret_chat_id = dialog_id.get_secret_chat_id();
    secret_chat_layer = td_->contacts_manager_->get_secret_chat_layer(secret_chat_id);
  }

  bool can_send_messages = true;
  bool can_send_media = true;
  bool can_send_stickers = true;
  bool can_send_animations = true;
  bool can_send_games = true;

  switch (dialog_type) {
    case DialogType::User:
    case DialogType::Chat:
      // ok
      break;
    case DialogType::Channel: {
      auto channel_status = td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id());
      can_send_messages = channel_status.can_send_messages();
      can_send_media = channel_status.can_send_media();
      can_send_stickers = channel_status.can_send_stickers();
      can_send_animations = channel_status.can_send_animations();
      can_send_games = channel_status.can_send_games();
      break;
    }
    case DialogType::SecretChat:
      can_send_games = false;
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  switch (content->get_type()) {
    case MessageContentType::Animation:
      if (!can_send_animations) {
        return Status::Error(400, "Not enough rights to send animations to the chat");
      }
      break;
    case MessageContentType::Audio:
      if (!can_send_media) {
        return Status::Error(400, "Not enough rights to send audios to the chat");
      }
      break;
    case MessageContentType::Contact:
      if (!can_send_messages) {
        return Status::Error(400, "Not enough rights to send contacts to the chat");
      }
      break;
    case MessageContentType::Document:
      if (!can_send_media) {
        return Status::Error(400, "Not enough rights to send documents to the chat");
      }
      break;
    case MessageContentType::Game:
      switch (dialog_id.get_type()) {
        case DialogType::User:
        case DialogType::Chat:
          // ok
          break;
        case DialogType::Channel: {
          auto channel_type = td_->contacts_manager_->get_channel_type(dialog_id.get_channel_id());
          if (channel_type == ChannelType::Broadcast) {
            return Status::Error(400, "Games can't be sent to channel chats");
          }
          break;
        }
        case DialogType::SecretChat:
          return Status::Error(400, "Games can't be sent to secret chats");
        case DialogType::None:
        default:
          UNREACHABLE();
      }

      if (!can_send_games) {
        return Status::Error(400, "Not enough rights to send games to the chat");
      }
      break;
    case MessageContentType::Invoice:
      if (!is_forward) {
        switch (dialog_type) {
          case DialogType::User:
            // ok
            break;
          case DialogType::Chat:
          case DialogType::Channel:
          case DialogType::SecretChat:
            return Status::Error(400, "Invoices can be sent only to private chats");
          case DialogType::None:
          default:
            UNREACHABLE();
        }
      }
      break;
    case MessageContentType::LiveLocation:
      if (!can_send_messages) {
        return Status::Error(400, "Not enough rights to send live locations to the chat");
      }
      break;
    case MessageContentType::Location:
      if (!can_send_messages) {
        return Status::Error(400, "Not enough rights to send locations to the chat");
      }
      break;
    case MessageContentType::Photo:
      if (!can_send_media) {
        return Status::Error(400, "Not enough rights to send photos to the chat");
      }
      break;
    case MessageContentType::Sticker:
      if (!can_send_stickers) {
        return Status::Error(400, "Not enough rights to send stickers to the chat");
      }
      break;
    case MessageContentType::Text:
      if (!can_send_messages) {
        return Status::Error(400, "Not enough rights to send text messages to the chat");
      }
      break;
    case MessageContentType::Venue:
      if (!can_send_messages) {
        return Status::Error(400, "Not enough rights to send venues to the chat");
      }
      break;
    case MessageContentType::Video:
      if (!can_send_media) {
        return Status::Error(400, "Not enough rights to send videos to the chat");
      }
      break;
    case MessageContentType::VideoNote:
      if (!can_send_media) {
        return Status::Error(400, "Not enough rights to send video notes to the chat");
      }
      if (secret_chat_layer < SecretChatActor::VIDEO_NOTES_LAYER) {
        return Status::Error(400, PSLICE()
                                      << "Video notes can't be sent to secret chat with layer " << secret_chat_layer);
      }
      break;
    case MessageContentType::VoiceNote:
      if (!can_send_media) {
        return Status::Error(400, "Not enough rights to send voice notes to the chat");
      }
      break;
    case MessageContentType::None:
    case MessageContentType::ChatCreate:
    case MessageContentType::ChatChangeTitle:
    case MessageContentType::ChatChangePhoto:
    case MessageContentType::ChatDeletePhoto:
    case MessageContentType::ChatDeleteHistory:
    case MessageContentType::ChatAddUsers:
    case MessageContentType::ChatJoinedByLink:
    case MessageContentType::ChatDeleteUser:
    case MessageContentType::ChatMigrateTo:
    case MessageContentType::ChannelCreate:
    case MessageContentType::ChannelMigrateFrom:
    case MessageContentType::PinMessage:
    case MessageContentType::GameScore:
    case MessageContentType::ScreenshotTaken:
    case MessageContentType::ChatSetTtl:
    case MessageContentType::Unsupported:
    case MessageContentType::Call:
    case MessageContentType::PaymentSuccessful:
    case MessageContentType::ContactRegistered:
    case MessageContentType::ExpiredPhoto:
    case MessageContentType::ExpiredVideo:
    case MessageContentType::CustomServiceAction:
    case MessageContentType::WebsiteConnected:
    case MessageContentType::PassportDataSent:
    case MessageContentType::PassportDataReceived:
      UNREACHABLE();
  }
  return Status::OK();
}

MessageId MessagesManager::get_persistent_message_id(const Dialog *d, MessageId message_id) const {
  if (!message_id.is_valid()) {
    return MessageId();
  }
  if (message_id.is_yet_unsent()) {
    // it is possible that user tries to do something with an already sent message by its temporary id
    // we need to use real message in this case and transparently replace message_id
    auto it = d->yet_unsent_message_id_to_persistent_message_id.find(message_id);
    if (it != d->yet_unsent_message_id_to_persistent_message_id.end()) {
      return it->second;
    }
  }

  return message_id;
}

MessageId MessagesManager::get_reply_to_message_id(Dialog *d, MessageId message_id) {
  CHECK(d != nullptr);
  message_id = get_persistent_message_id(d, message_id);
  const Message *reply_to_message = get_message_force(d, message_id);
  if (reply_to_message == nullptr || message_id.is_yet_unsent() ||
      (message_id.is_local() && d->dialog_id.get_type() != DialogType::SecretChat)) {
    // TODO local replies to local messages can be allowed
    // TODO replies to yet unsent messages can be allowed with special handling of them on application restart
    return MessageId();
  }
  return message_id;
}

vector<FileId> MessagesManager::get_message_file_ids(const Message *message) const {
  return get_message_content_file_ids(message->content.get(), td_);
}

void MessagesManager::cancel_upload_message_content_files(const MessageContent *content) {
  auto file_id = get_message_content_file_id(content);
  if (being_uploaded_files_.erase(file_id)) {
    LOG(INFO) << "Cancel upload file " << file_id;
    td_->file_manager_->upload(file_id, nullptr, 0, 0);
  }
  file_id = get_message_content_thumbnail_file_id(content, td_);
  if (being_uploaded_thumbnails_.erase(file_id)) {
    LOG(INFO) << "Cancel upload thumbnail file " << file_id;
    td_->file_manager_->upload(file_id, nullptr, 0, 0);
  }
}

void MessagesManager::cancel_send_message_query(DialogId dialog_id, unique_ptr<Message> &m) {
  CHECK(m != nullptr);
  CHECK(m->content != nullptr);
  CHECK(m->message_id.is_yet_unsent());
  LOG(INFO) << "Cancel send message query for " << m->message_id;

  cancel_upload_message_content_files(m->content.get());

  CHECK(m->edited_content == nullptr);

  if (!m->send_query_ref.empty()) {
    LOG(INFO) << "Cancel send query for " << m->message_id;
    cancel_query(m->send_query_ref);
    m->send_query_ref = NetQueryRef();
  }

  if (m->send_message_logevent_id != 0) {
    LOG(INFO) << "Delete send message log event for " << m->message_id;
    binlog_erase(G()->td_db()->get_binlog(), m->send_message_logevent_id);
    m->send_message_logevent_id = 0;
  }

  if (m->reply_to_message_id.is_valid() && !m->reply_to_message_id.is_yet_unsent()) {
    auto it = replied_by_yet_unsent_messages_.find({dialog_id, m->reply_to_message_id});
    CHECK(it != replied_by_yet_unsent_messages_.end());
    it->second--;
    CHECK(it->second >= 0);
    if (it->second == 0) {
      replied_by_yet_unsent_messages_.erase(it);
    }
  }

  if (m->media_album_id != 0) {
    send_closure_later(actor_id(this), &MessagesManager::on_upload_message_media_finished, m->media_album_id, dialog_id,
                       m->message_id, Status::OK());
  }

  if (G()->parameters().use_file_db) {  // ResourceManager::Mode::Baseline
    auto queue_id = get_sequence_dispatcher_id(dialog_id, m->content->get_type());
    if (queue_id & 1) {
      auto queue_it = yet_unsent_media_queues_.find(queue_id);
      if (queue_it != yet_unsent_media_queues_.end()) {
        auto &queue = queue_it->second;
        LOG(INFO) << "Delete " << m->message_id << " from queue " << queue_id;
        queue.erase(m->message_id.get());
        if (queue.empty()) {
          yet_unsent_media_queues_.erase(queue_it);
        } else {
          on_yet_unsent_media_queue_updated(dialog_id);
        }
      }
    }
  }
}

bool MessagesManager::is_message_auto_read(DialogId dialog_id, bool is_outgoing) const {
  switch (dialog_id.get_type()) {
    case DialogType::User: {
      auto user_id = dialog_id.get_user_id();
      if (user_id == td_->contacts_manager_->get_my_id()) {
        return true;
      }
      if (is_outgoing && td_->contacts_manager_->is_user_bot(user_id)) {
        return true;
      }
      return false;
    }
    case DialogType::Chat:
      // TODO auto_read message content and messages sent to group with bots only
      return false;
    case DialogType::Channel:
      return is_outgoing && is_broadcast_channel(dialog_id);
    case DialogType::SecretChat:
      return false;
    case DialogType::None:
      return false;
    default:
      UNREACHABLE();
      return false;
  }
}

void MessagesManager::add_message_dependencies(Dependencies &dependencies, DialogId dialog_id, const Message *m) {
  dependencies.user_ids.insert(m->sender_user_id);
  dependencies.user_ids.insert(m->via_bot_user_id);
  if (m->forward_info != nullptr) {
    dependencies.user_ids.insert(m->forward_info->sender_user_id);
    if (m->forward_info->dialog_id.is_valid() && dependencies.dialog_ids.insert(m->forward_info->dialog_id).second) {
      add_dialog_dependencies(dependencies, m->forward_info->dialog_id);
    }
    if (m->forward_info->from_dialog_id.is_valid() &&
        dependencies.dialog_ids.insert(m->forward_info->from_dialog_id).second) {
      add_dialog_dependencies(dependencies, m->forward_info->from_dialog_id);
    }
  }
  add_message_content_dependencies(dependencies, m->content.get());
}

void MessagesManager::add_dialog_dependencies(Dependencies &dependencies, DialogId dialog_id) {
  switch (dialog_id.get_type()) {
    case DialogType::User:
      dependencies.user_ids.insert(dialog_id.get_user_id());
      break;
    case DialogType::Chat:
      dependencies.chat_ids.insert(dialog_id.get_chat_id());
      break;
    case DialogType::Channel:
      dependencies.channel_ids.insert(dialog_id.get_channel_id());
      break;
    case DialogType::SecretChat:
      dependencies.secret_chat_ids.insert(dialog_id.get_secret_chat_id());
      break;
    case DialogType::None:
      break;
    default:
      UNREACHABLE();
  }
}

void MessagesManager::resolve_dependencies_force(const Dependencies &dependencies) {
  for (auto user_id : dependencies.user_ids) {
    if (user_id.is_valid() && !td_->contacts_manager_->have_user_force(user_id)) {
      LOG(ERROR) << "Can't find " << user_id;
    }
  }
  for (auto chat_id : dependencies.chat_ids) {
    if (chat_id.is_valid() && !td_->contacts_manager_->have_chat_force(chat_id)) {
      LOG(ERROR) << "Can't find " << chat_id;
    }
  }
  for (auto channel_id : dependencies.channel_ids) {
    if (channel_id.is_valid() && !td_->contacts_manager_->have_channel_force(channel_id)) {
      LOG(ERROR) << "Can't find " << channel_id;
    }
  }
  for (auto secret_chat_id : dependencies.secret_chat_ids) {
    if (secret_chat_id.is_valid() && !td_->contacts_manager_->have_secret_chat_force(secret_chat_id)) {
      LOG(ERROR) << "Can't find " << secret_chat_id;
    }
  }
  for (auto dialog_id : dependencies.dialog_ids) {
    if (dialog_id.is_valid() && !have_dialog_force(dialog_id)) {
      LOG(ERROR) << "Can't find " << dialog_id;
      force_create_dialog(dialog_id, "resolve_dependencies_force");
    }
  }
  for (auto web_page_id : dependencies.web_page_ids) {
    if (web_page_id.is_valid()) {
      td_->web_pages_manager_->have_web_page_force(web_page_id);
    }
  }
}

class MessagesManager::SendMessageLogEvent {
 public:
  DialogId dialog_id;
  const Message *m_in;
  unique_ptr<Message> m_out;

  SendMessageLogEvent() : dialog_id(), m_in(nullptr) {
  }

  SendMessageLogEvent(DialogId dialog_id, const Message *m) : dialog_id(dialog_id), m_in(m) {
  }

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_id, storer);
    td::store(*m_in, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_id, parser);
    CHECK(m_out == nullptr);
    m_out = make_unique<Message>();
    td::parse(*m_out, parser);
  }
};

Result<MessageId> MessagesManager::send_message(DialogId dialog_id, MessageId reply_to_message_id,
                                                bool disable_notification, bool from_background,
                                                tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                                tl_object_ptr<td_api::InputMessageContent> &&input_message_content) {
  if (input_message_content == nullptr) {
    return Status::Error(5, "Can't send message without content");
  }

  LOG(INFO) << "Begin to send message to " << dialog_id << " in reply to " << reply_to_message_id;
  if (input_message_content->get_id() == td_api::inputMessageForwarded::ID) {
    auto input_message = static_cast<const td_api::inputMessageForwarded *>(input_message_content.get());
    return forward_message(dialog_id, DialogId(input_message->from_chat_id_), MessageId(input_message->message_id_),
                           disable_notification, from_background, input_message->in_game_share_);
  }

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(5, "Chat not found");
  }

  TRY_STATUS(can_send_message(dialog_id));
  TRY_RESULT(message_reply_markup, get_dialog_reply_markup(dialog_id, std::move(reply_markup)));
  TRY_RESULT(message_content, process_input_message_content(dialog_id, std::move(input_message_content)));

  // there must be no errors after get_message_to_send call

  bool need_update_dialog_pos = false;
  Message *m = get_message_to_send(
      d, get_reply_to_message_id(d, reply_to_message_id), disable_notification, from_background,
      dup_message_content(td_, dialog_id, message_content.content.get(), false), &need_update_dialog_pos);
  m->reply_markup = std::move(message_reply_markup);
  m->via_bot_user_id = message_content.via_bot_user_id;
  m->disable_web_page_preview = message_content.disable_web_page_preview;
  m->clear_draft = message_content.clear_draft;
  if (message_content.ttl > 0) {
    m->ttl = message_content.ttl;
    m->is_content_secret = is_secret_message_content(m->ttl, m->content->get_type());
  }

  if (message_content.clear_draft) {
    update_dialog_draft_message(d, nullptr, false, !need_update_dialog_pos);
  }

  send_update_new_message(d, m);
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "send_message");
  }

  auto message_id = m->message_id;
  save_send_message_logevent(dialog_id, m);
  do_send_message(dialog_id, m);
  return message_id;
}

Result<InputMessageContent> MessagesManager::process_input_message_content(
    DialogId dialog_id, tl_object_ptr<td_api::InputMessageContent> &&input_message_content) const {
  if (input_message_content == nullptr) {
    return Status::Error(5, "Can't send message without content");
  }

  TRY_RESULT(content, get_input_message_content(dialog_id, std::move(input_message_content), td_));

  if (content.ttl < 0 || content.ttl > MAX_PRIVATE_MESSAGE_TTL) {
    return Status::Error(10, "Wrong message TTL specified");
  }
  if (content.ttl > 0 && dialog_id.get_type() != DialogType::User) {
    return Status::Error(10, "Message TTL can be specified only in private chats");
  }

  if (dialog_id != DialogId()) {
    TRY_STATUS(can_send_message_content(dialog_id, content.content.get(), false));
  }

  return std::move(content);
}

Result<vector<MessageId>> MessagesManager::send_message_group(
    DialogId dialog_id, MessageId reply_to_message_id, bool disable_notification, bool from_background,
    vector<tl_object_ptr<td_api::InputMessageContent>> &&input_message_contents) {
  if (input_message_contents.size() > MAX_GROUPED_MESSAGES) {
    return Status::Error(4, "Too much messages to send as an album");
  }
  if (input_message_contents.empty()) {
    return Status::Error(4, "There is no messages to send");
  }

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(5, "Chat not found");
  }

  TRY_STATUS(can_send_message(dialog_id));

  vector<std::pair<unique_ptr<MessageContent>, int32>> message_contents;
  for (auto &input_message_content : input_message_contents) {
    TRY_RESULT(message_content, process_input_message_content(dialog_id, std::move(input_message_content)));
    if (!is_allowed_media_group_content(message_content.content->get_type())) {
      return Status::Error(5, "Wrong message content type");
    }

    message_contents.emplace_back(std::move(message_content.content), message_content.ttl);
  }

  reply_to_message_id = get_reply_to_message_id(d, reply_to_message_id);

  int64 media_album_id = 0;
  if (message_contents.size() > 1) {
    do {
      media_album_id = Random::secure_int64();
    } while (media_album_id >= 0 || pending_message_group_sends_.count(media_album_id) != 0);
  }

  // there must be no errors after get_message_to_send calls

  vector<MessageId> result;
  bool need_update_dialog_pos = false;
  for (auto &message_content : message_contents) {
    Message *m = get_message_to_send(d, reply_to_message_id, disable_notification, from_background,
                                     dup_message_content(td_, dialog_id, message_content.first.get(), false),
                                     &need_update_dialog_pos);
    result.push_back(m->message_id);
    auto ttl = message_content.second;
    if (ttl > 0) {
      m->ttl = ttl;
      m->is_content_secret = is_secret_message_content(m->ttl, m->content->get_type());
    }
    m->media_album_id = media_album_id;

    save_send_message_logevent(dialog_id, m);
    do_send_message(dialog_id, m);

    send_update_new_message(d, m);
  }

  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "send_message_group");
  }

  return result;
}

void MessagesManager::save_send_message_logevent(DialogId dialog_id, Message *m) {
  if (!G()->parameters().use_message_db) {
    return;
  }

  CHECK(m != nullptr);
  LOG(INFO) << "Save " << FullMessageId(dialog_id, m->message_id) << " to binlog";
  auto logevent = SendMessageLogEvent(dialog_id, m);
  auto storer = LogEventStorerImpl<SendMessageLogEvent>(logevent);
  CHECK(m->send_message_logevent_id == 0);
  m->send_message_logevent_id = binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::SendMessage, storer);
}

void MessagesManager::do_send_message(DialogId dialog_id, Message *m, vector<int> bad_parts) {
  bool is_edit = m->message_id.is_server();
  LOG(INFO) << "Do " << (is_edit ? "edit" : "send") << ' ' << FullMessageId(dialog_id, m->message_id);
  bool is_secret = dialog_id.get_type() == DialogType::SecretChat;

  if (m->media_album_id != 0 && bad_parts.empty() && !is_secret && !is_edit) {
    auto &request = pending_message_group_sends_[m->media_album_id];
    request.dialog_id = dialog_id;
    request.message_ids.push_back(m->message_id);
    request.is_finished.push_back(false);

    request.results.push_back(Status::OK());
  }

  auto content = is_edit ? m->edited_content.get() : m->content.get();
  CHECK(content != nullptr);
  auto content_type = content->get_type();
  if (content_type == MessageContentType::Text) {
    const FormattedText *message_text = get_message_content_text(content);
    CHECK(message_text != nullptr);

    int64 random_id = begin_send_message(dialog_id, m);
    if (is_secret) {
      auto layer = td_->contacts_manager_->get_secret_chat_layer(dialog_id.get_secret_chat_id());
      send_closure(td_->create_net_actor<SendSecretMessageActor>(), &SendSecretMessageActor::send, dialog_id,
                   m->reply_to_random_id, m->ttl, message_text->text,
                   get_secret_input_media(content, td_, nullptr, BufferSlice(), layer),
                   get_input_secret_message_entities(message_text->entities), m->via_bot_user_id, m->media_album_id,
                   random_id);
    } else {
      send_closure(td_->create_net_actor<SendMessageActor>(), &SendMessageActor::send, get_message_flags(m), dialog_id,
                   m->reply_to_message_id, get_input_reply_markup(m->reply_markup),
                   get_input_message_entities(td_->contacts_manager_.get(), message_text->entities, "do_send_message"),
                   message_text->text, random_id, &m->send_query_ref,
                   get_sequence_dispatcher_id(dialog_id, content_type));
    }
    return;
  }

  FileId file_id = get_message_content_file_id(content);
  FileView file_view = td_->file_manager_->get_file_view(file_id);
  FileId thumbnail_file_id = get_message_content_thumbnail_file_id(content, td_);
  if (is_secret) {
    auto layer = td_->contacts_manager_->get_secret_chat_layer(dialog_id.get_secret_chat_id());
    auto secret_input_media = get_secret_input_media(content, td_, nullptr, BufferSlice(), layer);
    if (secret_input_media.empty()) {
      CHECK(file_view.is_encrypted_secret());
      CHECK(file_id.is_valid());
      being_uploaded_files_[file_id] = {FullMessageId(dialog_id, m->message_id), thumbnail_file_id};
      LOG(INFO) << "Ask to upload encrypted file " << file_id;
      // need to call resume_upload synchronously to make upload process consistent with being_uploaded_files_
      td_->file_manager_->resume_upload(file_id, std::move(bad_parts), upload_media_callback_, 1, m->message_id.get());
    } else {
      on_secret_message_media_uploaded(dialog_id, m, std::move(secret_input_media), FileId(), FileId());
    }
  } else {
    auto input_media = get_input_media(content, td_, nullptr, nullptr, m->ttl);
    if (input_media == nullptr) {
      if (content_type == MessageContentType::Photo) {
        thumbnail_file_id = FileId();
      }

      CHECK(file_id.is_valid());
      being_uploaded_files_[file_id] = {FullMessageId(dialog_id, m->message_id), thumbnail_file_id};
      LOG(INFO) << "Ask to upload file " << file_id;
      // need to call resume_upload synchronously to make upload process consistent with being_uploaded_files_
      td_->file_manager_->resume_upload(file_id, std::move(bad_parts), upload_media_callback_, 1, m->message_id.get());
    } else {
      on_message_media_uploaded(dialog_id, m, std::move(input_media), FileId(), FileId());
    }
  }
}

void MessagesManager::on_message_media_uploaded(DialogId dialog_id, Message *m,
                                                tl_object_ptr<telegram_api::InputMedia> &&input_media, FileId file_id,
                                                FileId thumbnail_file_id) {
  CHECK(m != nullptr);
  CHECK(input_media != nullptr);

  auto message_id = m->message_id;
  if (message_id.is_server()) {
    const FormattedText *caption = get_message_content_caption(m->edited_content.get());
    auto input_reply_markup = get_input_reply_markup(m->edited_reply_markup);

    LOG(INFO) << "Edit media from " << message_id << " in " << dialog_id;
    auto promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, message_id, file_id, thumbnail_file_id,
                                           generation = m->edit_generation](Result<Unit> result) {
      send_closure(actor_id, &MessagesManager::on_message_media_edited, dialog_id, message_id, file_id,
                   thumbnail_file_id, generation, std::move(result));
    });
    send_closure(td_->create_net_actor<EditMessageActor>(std::move(promise)), &EditMessageActor::send, 1 << 11,
                 dialog_id, message_id, caption == nullptr ? "" : caption->text,
                 get_input_message_entities(td_->contacts_manager_.get(), caption, "edit_message_media"),
                 std::move(input_media), nullptr, std::move(input_reply_markup),
                 get_sequence_dispatcher_id(dialog_id, MessageContentType::None));
    return;
  }

  if (m->media_album_id == 0) {
    on_media_message_ready_to_send(
        dialog_id, message_id,
        PromiseCreator::lambda([this, dialog_id, input_media = std::move(input_media), file_id,
                                thumbnail_file_id](Result<Message *> result) mutable {
          if (result.is_error() || G()->close_flag()) {
            return;
          }

          auto m = result.move_as_ok();
          CHECK(m != nullptr);
          CHECK(input_media != nullptr);

          const FormattedText *caption = get_message_content_caption(m->content.get());
          LOG(INFO) << "Send media from " << m->message_id << " in " << dialog_id << " in reply to "
                    << m->reply_to_message_id;
          int64 random_id = begin_send_message(dialog_id, m);
          send_closure(td_->create_net_actor<SendMediaActor>(), &SendMediaActor::send, file_id, thumbnail_file_id,
                       get_message_flags(m), dialog_id, m->reply_to_message_id, get_input_reply_markup(m->reply_markup),
                       get_input_message_entities(td_->contacts_manager_.get(), caption, "on_message_media_uploaded"),
                       caption == nullptr ? "" : caption->text, std::move(input_media), random_id, &m->send_query_ref,
                       get_sequence_dispatcher_id(dialog_id, m->content->get_type()));
        }));
  } else {
    switch (input_media->get_id()) {
      case telegram_api::inputMediaUploadedDocument::ID:
        static_cast<telegram_api::inputMediaUploadedDocument *>(input_media.get())->flags_ |=
            telegram_api::inputMediaUploadedDocument::NOSOUND_VIDEO_MASK;
      // fallthrough
      case telegram_api::inputMediaUploadedPhoto::ID:
      case telegram_api::inputMediaDocumentExternal::ID:
      case telegram_api::inputMediaPhotoExternal::ID:
        LOG(INFO) << "Upload media from " << message_id << " in " << dialog_id;
        td_->create_handler<UploadMediaQuery>()->send(dialog_id, message_id, file_id, thumbnail_file_id,
                                                      std::move(input_media));
        break;
      case telegram_api::inputMediaDocument::ID:
      case telegram_api::inputMediaPhoto::ID:
        send_closure_later(actor_id(this), &MessagesManager::on_upload_message_media_finished, m->media_album_id,
                           dialog_id, message_id, Status::OK());
        break;
      default:
        LOG(ERROR) << "Have wrong input media " << to_string(input_media);
        send_closure_later(actor_id(this), &MessagesManager::on_upload_message_media_finished, m->media_album_id,
                           dialog_id, message_id, Status::Error(400, "Wrong input media"));
    }
  }
}

void MessagesManager::on_secret_message_media_uploaded(DialogId dialog_id, Message *m,
                                                       SecretInputMedia &&secret_input_media, FileId file_id,
                                                       FileId thumbnail_file_id) {
  CHECK(m != nullptr);
  CHECK(!secret_input_media.empty());
  /*
  if (m->media_album_id != 0) {
    switch (secret_input_media->input_file_->get_id()) {
      case telegram_api::inputEncryptedFileUploaded::ID:
      case telegram_api::inputEncryptedFileBigUploaded::ID:
        LOG(INFO) << "Upload media from " << m->message_id << " in " << dialog_id;
        return td_->create_handler<UploadEncryptedMediaQuery>()->send(dialog_id, m->message_id, std::move(secret_input_media));
      case telegram_api::inputEncryptedFile::ID:
        return send_closure_later(actor_id(this), &MessagesManager::on_upload_message_media_finished, m->media_album_id,
                           dialog_id, m->message_id, Status::OK());
      default:
        LOG(ERROR) << "Have wrong secret input media " << to_string(secret_input_media->input_file_);
        return send_closure_later(actor_id(this), &MessagesManager::on_upload_message_media_finished, m->media_album_id,
                           dialog_id, m->message_id, Status::Error(400, "Wrong input media"));
  }
  */
  // TODO use file_id, thumbnail_file_id, invalidate partial remote location for file_id in case of failed upload
  // even message has already been deleted
  on_media_message_ready_to_send(
      dialog_id, m->message_id,
      PromiseCreator::lambda(
          [this, dialog_id, secret_input_media = std::move(secret_input_media)](Result<Message *> result) mutable {
            if (result.is_error() || G()->close_flag()) {
              return;
            }

            auto m = result.move_as_ok();
            CHECK(m != nullptr);
            CHECK(!secret_input_media.empty());
            LOG(INFO) << "Send secret media from " << m->message_id << " in " << dialog_id << " in reply to "
                      << m->reply_to_message_id;
            int64 random_id = begin_send_message(dialog_id, m);
            send_closure(td_->create_net_actor<SendSecretMessageActor>(), &SendSecretMessageActor::send, dialog_id,
                         m->reply_to_random_id, m->ttl, "", std::move(secret_input_media),
                         vector<tl_object_ptr<secret_api::MessageEntity>>(), m->via_bot_user_id, m->media_album_id,
                         random_id);
          }));
}

void MessagesManager::on_upload_message_media_success(DialogId dialog_id, MessageId message_id,
                                                      tl_object_ptr<telegram_api::MessageMedia> &&media) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  CHECK(message_id.is_yet_unsent());
  Message *m = get_message(d, message_id);
  if (m == nullptr) {
    // message has already been deleted by the user or sent to inaccessible channel
    // don't need to send error to the user, because the message has already been deleted
    // and there is nothing to be deleted from the server
    LOG(INFO) << "Fail to send already deleted by the user or sent to inaccessible chat "
              << FullMessageId{dialog_id, message_id};
    return;
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    return;  // the message should be deleted soon
  }

  auto caption = get_message_content_caption(m->content.get());
  auto content = get_message_content(td_, caption == nullptr ? FormattedText() : *caption, std::move(media), dialog_id,
                                     false, UserId(), nullptr);

  update_message_content(dialog_id, m, std::move(content), true, true);

  auto input_media = get_input_media(m->content.get(), td_, nullptr, nullptr, m->ttl);
  Status result;
  if (input_media == nullptr) {
    result = Status::Error(400, "Failed to upload file");
  }

  send_closure_later(actor_id(this), &MessagesManager::on_upload_message_media_finished, m->media_album_id, dialog_id,
                     message_id, std::move(result));
}

void MessagesManager::on_upload_message_media_file_part_missing(DialogId dialog_id, MessageId message_id,
                                                                int bad_part) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  Message *m = get_message(d, message_id);
  if (m == nullptr) {
    // message has already been deleted by the user or sent to inaccessible channel
    // don't need to send error to the user, because the message has already been deleted
    // and there is nothing to be deleted from the server
    LOG(INFO) << "Fail to send already deleted by the user or sent to inaccessible chat "
              << FullMessageId{dialog_id, message_id};
    return;
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    // LOG(ERROR) << "Found " << m->message_id << " in inaccessible " << dialog_id;
    // dump_debug_message_op(get_dialog(dialog_id), 5);
    return;  // the message should be deleted soon
  }

  CHECK(dialog_id.get_type() != DialogType::SecretChat);

  do_send_message(dialog_id, m, {bad_part});
}

void MessagesManager::on_upload_message_media_fail(DialogId dialog_id, MessageId message_id, Status error) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  Message *m = get_message(d, message_id);
  if (m == nullptr) {
    // message has already been deleted by the user or sent to inaccessible channel
    // don't need to send error to the user, because the message has already been deleted
    // and there is nothing to be deleted from the server
    LOG(INFO) << "Fail to send already deleted by the user or sent to inaccessible chat "
              << FullMessageId{dialog_id, message_id};
    return;
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    // LOG(ERROR) << "Found " << m->message_id << " in inaccessible " << dialog_id;
    // dump_debug_message_op(get_dialog(dialog_id), 5);
    return;  // the message should be deleted soon
  }

  CHECK(dialog_id.get_type() != DialogType::SecretChat);

  send_closure_later(actor_id(this), &MessagesManager::on_upload_message_media_finished, m->media_album_id, dialog_id,
                     message_id, std::move(error));
}

void MessagesManager::on_upload_message_media_finished(int64 media_album_id, DialogId dialog_id, MessageId message_id,
                                                       Status result) {
  CHECK(media_album_id < 0);
  auto it = pending_message_group_sends_.find(media_album_id);
  if (it == pending_message_group_sends_.end()) {
    // the group may be already sent or failed to be sent
    return;
  }
  auto &request = it->second;
  CHECK(request.dialog_id == dialog_id);
  auto message_it = std::find(request.message_ids.begin(), request.message_ids.end(), message_id);
  CHECK(message_it != request.message_ids.end());
  auto pos = static_cast<size_t>(message_it - request.message_ids.begin());

  if (request.is_finished[pos]) {
    return;
  }
  LOG(INFO) << "Finish to upload media of " << message_id << " in " << dialog_id << " from group " << media_album_id;

  request.results[pos] = std::move(result);
  request.is_finished[pos] = true;
  request.finished_count++;

  if (request.finished_count == request.message_ids.size() || request.results[pos].is_error()) {
    // send later, because some messages may be being deleted now
    for (auto request_message_id : request.message_ids) {
      LOG(INFO) << "Send on_media_message_ready_to_send for " << request_message_id << " in " << dialog_id;
      send_closure_later(
          actor_id(this), &MessagesManager::on_media_message_ready_to_send, dialog_id, request_message_id,
          PromiseCreator::lambda([actor_id = actor_id(this), media_album_id](Result<Message *> result) mutable {
            if (result.is_error() || G()->close_flag()) {
              return;
            }

            auto m = result.move_as_ok();
            CHECK(m != nullptr);
            CHECK(m->media_album_id == media_album_id);
            send_closure_later(actor_id, &MessagesManager::do_send_message_group, media_album_id);
          }));
    }
  }
}

void MessagesManager::do_send_message_group(int64 media_album_id) {
  CHECK(media_album_id < 0);
  auto it = pending_message_group_sends_.find(media_album_id);
  if (it == pending_message_group_sends_.end()) {
    // the group may be already sent or failed to be sent
    return;
  }
  auto &request = it->second;

  auto dialog_id = request.dialog_id;
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  auto default_status = can_send_message(dialog_id);
  bool success = default_status.is_ok();
  vector<int64> random_ids;
  vector<tl_object_ptr<telegram_api::inputSingleMedia>> input_single_media;
  MessageId reply_to_message_id;
  int32 flags = 0;
  for (size_t i = 0; i < request.message_ids.size(); i++) {
    auto *m = get_message(d, request.message_ids[i]);
    if (m == nullptr) {
      // skip deleted messages
      random_ids.push_back(0);
      continue;
    }

    reply_to_message_id = m->reply_to_message_id;
    flags = get_message_flags(m);

    random_ids.push_back(begin_send_message(dialog_id, m));
    const FormattedText *caption = get_message_content_caption(m->content.get());
    auto input_media = get_input_media(m->content.get(), td_, nullptr, nullptr, m->ttl);
    auto entities = get_input_message_entities(td_->contacts_manager_.get(), caption, "do_send_message_group");
    int32 input_single_media_flags = 0;
    if (!entities.empty()) {
      input_single_media_flags |= telegram_api::inputSingleMedia::ENTITIES_MASK;
    }

    input_single_media.push_back(make_tl_object<telegram_api::inputSingleMedia>(
        input_single_media_flags, std::move(input_media), random_ids.back(), caption == nullptr ? "" : caption->text,
        std::move(entities)));
    if (request.results[i].is_error()) {
      success = false;
    }
  }

  if (!success) {
    if (default_status.is_ok()) {
      default_status = Status::Error(400, "Group send failed");
    }
    for (size_t i = 0; i < random_ids.size(); i++) {
      if (random_ids[i] != 0) {
        on_send_message_fail(random_ids[i],
                             request.results[i].is_error() ? std::move(request.results[i]) : default_status.clone());
      }
    }
    pending_message_group_sends_.erase(it);
    return;
  }
  CHECK(request.finished_count == request.message_ids.size())
      << request.finished_count << " " << request.message_ids.size();
  pending_message_group_sends_.erase(it);

  LOG(INFO) << "Begin to send media group " << media_album_id << " to " << dialog_id;

  if (input_single_media.empty()) {
    LOG(INFO) << "Media group " << media_album_id << " from " << dialog_id << " is empty";
  }
  send_closure(td_->create_net_actor<SendMultiMediaActor>(), &SendMultiMediaActor::send, flags, dialog_id,
               reply_to_message_id, std::move(input_single_media),
               get_sequence_dispatcher_id(dialog_id, MessageContentType::Photo));
}

void MessagesManager::on_media_message_ready_to_send(DialogId dialog_id, MessageId message_id,
                                                     Promise<Message *> &&promise) {
  LOG(INFO) << "Ready to send " << message_id << " to " << dialog_id;
  CHECK(promise);
  if (!G()->parameters().use_file_db) {  // ResourceManager::Mode::Greedy
    auto m = get_message({dialog_id, message_id});
    if (m != nullptr) {
      promise.set_value(std::move(m));
    }
    return;
  }

  auto queue_id = get_sequence_dispatcher_id(dialog_id, MessageContentType::Photo);
  CHECK(queue_id & 1);
  auto &queue = yet_unsent_media_queues_[queue_id];
  auto it = queue.find(message_id.get());
  if (it == queue.end()) {
    if (queue.empty()) {
      yet_unsent_media_queues_.erase(queue_id);
    }

    LOG(INFO) << "Can't find " << message_id << " in the queue of " << dialog_id;
    auto m = get_message({dialog_id, message_id});
    if (m != nullptr) {
      promise.set_value(std::move(m));
    }
    return;
  }
  if (it->second) {
    promise.set_error(Status::Error(500, "Duplicate promise"));
    return;
  }
  it->second = std::move(promise);

  on_yet_unsent_media_queue_updated(dialog_id);
}

void MessagesManager::on_yet_unsent_media_queue_updated(DialogId dialog_id) {
  auto queue_id = get_sequence_dispatcher_id(dialog_id, MessageContentType::Photo);
  CHECK(queue_id & 1);
  auto &queue = yet_unsent_media_queues_[queue_id];
  LOG(INFO) << "Queue for " << dialog_id << " is updated to size of " << queue.size();
  while (!queue.empty()) {
    auto first_it = queue.begin();
    if (!first_it->second) {
      break;
    }

    auto m = get_message({dialog_id, MessageId(first_it->first)});
    if (m != nullptr) {
      LOG(INFO) << "Can send " << FullMessageId{dialog_id, m->message_id};
      first_it->second.set_value(std::move(m));
    }
    queue.erase(first_it);
  }
  LOG(INFO) << "Queue for " << dialog_id << " now has size " << queue.size();
  if (queue.empty()) {
    yet_unsent_media_queues_.erase(queue_id);
  }
}

Result<MessageId> MessagesManager::send_bot_start_message(UserId bot_user_id, DialogId dialog_id,
                                                          const string &parameter) {
  LOG(INFO) << "Begin to send bot start message to " << dialog_id;
  if (td_->auth_manager_->is_bot()) {
    return Status::Error(5, "Bot can't send start message to another bot");
  }

  TRY_RESULT(bot_data, td_->contacts_manager_->get_bot_data(bot_user_id));

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(5, "Chat not found");
  }

  bool is_chat_with_bot = false;
  switch (dialog_id.get_type()) {
    case DialogType::User:
      if (dialog_id.get_user_id() != bot_user_id) {
        return Status::Error(5, "Can't send start message to a private chat other than chat with the bot");
      }
      is_chat_with_bot = true;
      break;
    case DialogType::Chat: {
      if (!bot_data.can_join_groups) {
        return Status::Error(5, "Bot can't join groups");
      }

      auto chat_id = dialog_id.get_chat_id();
      if (!td_->contacts_manager_->have_input_peer_chat(chat_id, AccessRights::Write)) {
        return Status::Error(3, "Can't access the chat");
      }
      auto status = td_->contacts_manager_->get_chat_status(chat_id);
      if (!status.can_invite_users()) {
        return Status::Error(3, "Need administrator rights to invite a bot to the group chat");
      }
      break;
    }
    case DialogType::Channel: {
      auto channel_id = dialog_id.get_channel_id();
      if (!td_->contacts_manager_->have_input_peer_channel(channel_id, AccessRights::Write)) {
        return Status::Error(3, "Can't access the chat");
      }
      switch (td_->contacts_manager_->get_channel_type(channel_id)) {
        case ChannelType::Megagroup:
          if (!bot_data.can_join_groups) {
            return Status::Error(5, "The bot can't join groups");
          }
          break;
        case ChannelType::Broadcast:
          return Status::Error(3, "Bots can't be invited to channel chats. Add them as administrators instead");
        case ChannelType::Unknown:
        default:
          UNREACHABLE();
      }
      auto status = td_->contacts_manager_->get_channel_status(channel_id);
      if (!status.can_invite_users()) {
        return Status::Error(3, "Need administrator rights to invite a bot to the supergroup chat");
      }
      break;
    }
    case DialogType::SecretChat:
      return Status::Error(5, "Can't send bot start message to a secret chat");
    case DialogType::None:
    default:
      UNREACHABLE();
  }
  string text = "/start";
  if (!is_chat_with_bot) {
    text += '@';
    text += bot_data.username;
  }

  vector<MessageEntity> text_entities;
  text_entities.emplace_back(MessageEntity::Type::BotCommand, 0, narrow_cast<int32>(text.size()));
  bool need_update_dialog_pos = false;
  Message *m = get_message_to_send(d, MessageId(), false, false,
                                   create_text_message_content(text, std::move(text_entities), WebPageId()),
                                   &need_update_dialog_pos);

  send_update_new_message(d, m);
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "send_bot_start_message");
  }

  save_send_bot_start_message_logevent(bot_user_id, dialog_id, parameter, m);
  do_send_bot_start_message(bot_user_id, dialog_id, parameter, m);
  return m->message_id;
}

class MessagesManager::SendBotStartMessageLogEvent {
 public:
  UserId bot_user_id;
  DialogId dialog_id;
  string parameter;
  const Message *m_in = nullptr;
  unique_ptr<Message> m_out;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(bot_user_id, storer);
    td::store(dialog_id, storer);
    td::store(parameter, storer);
    td::store(*m_in, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(bot_user_id, parser);
    td::parse(dialog_id, parser);
    td::parse(parameter, parser);
    CHECK(m_out == nullptr);
    m_out = make_unique<Message>();
    td::parse(*m_out, parser);
  }
};

void MessagesManager::save_send_bot_start_message_logevent(UserId bot_user_id, DialogId dialog_id,
                                                           const string &parameter, Message *m) {
  if (!G()->parameters().use_message_db) {
    return;
  }

  CHECK(m != nullptr);
  LOG(INFO) << "Save " << FullMessageId(dialog_id, m->message_id) << " to binlog";
  SendBotStartMessageLogEvent logevent;
  logevent.bot_user_id = bot_user_id;
  logevent.dialog_id = dialog_id;
  logevent.parameter = parameter;
  logevent.m_in = m;
  auto storer = LogEventStorerImpl<SendBotStartMessageLogEvent>(logevent);
  CHECK(m->send_message_logevent_id == 0);
  m->send_message_logevent_id =
      binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::SendBotStartMessage, storer);
}

void MessagesManager::do_send_bot_start_message(UserId bot_user_id, DialogId dialog_id, const string &parameter,
                                                Message *m) {
  LOG(INFO) << "Do send bot start " << FullMessageId(dialog_id, m->message_id) << " to bot " << bot_user_id;

  int64 random_id = begin_send_message(dialog_id, m);
  telegram_api::object_ptr<telegram_api::InputPeer> input_peer = dialog_id.get_type() == DialogType::User
                                                                     ? make_tl_object<telegram_api::inputPeerEmpty>()
                                                                     : get_input_peer(dialog_id, AccessRights::Write);
  if (input_peer == nullptr) {
    return on_send_message_fail(random_id, Status::Error(400, "Have no info about the chat"));
  }
  auto bot_input_user = td_->contacts_manager_->get_input_user(bot_user_id);
  if (bot_input_user == nullptr) {
    return on_send_message_fail(random_id, Status::Error(400, "Have no info about the bot"));
  }

  m->send_query_ref = td_->create_handler<StartBotQuery>()->send(std::move(bot_input_user), dialog_id,
                                                                 std::move(input_peer), parameter, random_id);
}

Result<MessageId> MessagesManager::send_inline_query_result_message(DialogId dialog_id, MessageId reply_to_message_id,
                                                                    bool disable_notification, bool from_background,
                                                                    int64 query_id, const string &result_id) {
  LOG(INFO) << "Begin to send inline query result message to " << dialog_id << " in reply to " << reply_to_message_id;

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(5, "Chat not found");
  }

  TRY_STATUS(can_send_message(dialog_id));
  bool to_secret = false;
  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
      // ok
      break;
    case DialogType::Channel: {
      auto channel_status = td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id());
      if (!channel_status.can_use_inline_bots()) {
        return Status::Error(400, "Can't use inline bots in the chat");
      }
      break;
    }
    case DialogType::SecretChat:
      to_secret = true;
      // ok
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  const InlineMessageContent *content = td_->inline_queries_manager_->get_inline_message_content(query_id, result_id);
  if (content == nullptr) {
    return Status::Error(5, "Inline query result not found");
  }

  TRY_STATUS(can_send_message_content(dialog_id, content->message_content.get(), false));

  bool need_update_dialog_pos = false;
  Message *m = get_message_to_send(
      d, get_reply_to_message_id(d, reply_to_message_id), disable_notification, from_background,
      dup_message_content(td_, dialog_id, content->message_content.get(), false), &need_update_dialog_pos);
  m->via_bot_user_id = td_->inline_queries_manager_->get_inline_bot_user_id(query_id);
  if (content->message_reply_markup != nullptr && !to_secret) {
    m->reply_markup = make_unique<ReplyMarkup>(*content->message_reply_markup);
  }
  m->disable_web_page_preview = content->disable_web_page_preview;
  m->clear_draft = true;

  update_dialog_draft_message(d, nullptr, false, !need_update_dialog_pos);

  send_update_new_message(d, m);
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "send_inline_query_result_message");
  }

  if (to_secret) {
    save_send_message_logevent(dialog_id, m);
    auto message_id = m->message_id;
    do_send_message(dialog_id, m);
    return message_id;
  }

  save_send_inline_query_result_message_logevent(dialog_id, m, query_id, result_id);
  do_send_inline_query_result_message(dialog_id, m, query_id, result_id);
  return m->message_id;
}

class MessagesManager::SendInlineQueryResultMessageLogEvent {
 public:
  DialogId dialog_id;
  int64 query_id;
  string result_id;
  const Message *m_in = nullptr;
  unique_ptr<Message> m_out;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_id, storer);
    td::store(query_id, storer);
    td::store(result_id, storer);
    td::store(*m_in, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_id, parser);
    td::parse(query_id, parser);
    td::parse(result_id, parser);
    CHECK(m_out == nullptr);
    m_out = make_unique<Message>();
    td::parse(*m_out, parser);
  }
};

void MessagesManager::save_send_inline_query_result_message_logevent(DialogId dialog_id, Message *m, int64 query_id,
                                                                     const string &result_id) {
  if (!G()->parameters().use_message_db) {
    return;
  }

  CHECK(m != nullptr);
  LOG(INFO) << "Save " << FullMessageId(dialog_id, m->message_id) << " to binlog";
  SendInlineQueryResultMessageLogEvent logevent;
  logevent.dialog_id = dialog_id;
  logevent.query_id = query_id;
  logevent.result_id = result_id;
  logevent.m_in = m;
  auto storer = LogEventStorerImpl<SendInlineQueryResultMessageLogEvent>(logevent);
  CHECK(m->send_message_logevent_id == 0);
  m->send_message_logevent_id =
      binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::SendInlineQueryResultMessage, storer);
}

void MessagesManager::do_send_inline_query_result_message(DialogId dialog_id, Message *m, int64 query_id,
                                                          const string &result_id) {
  LOG(INFO) << "Do send inline query result " << FullMessageId(dialog_id, m->message_id);

  int64 random_id = begin_send_message(dialog_id, m);
  m->send_query_ref = td_->create_handler<SendInlineBotResultQuery>()->send(
      get_message_flags(m), dialog_id, m->reply_to_message_id, random_id, query_id, result_id);
}

bool MessagesManager::can_edit_message(DialogId dialog_id, const Message *m, bool is_editing,
                                       bool only_reply_markup) const {
  if (m == nullptr) {
    return false;
  }
  if (m->message_id.is_yet_unsent()) {
    return false;
  }
  if (m->message_id.is_local()) {
    return false;
  }
  if (m->forward_info != nullptr) {
    return false;
  }

  bool is_bot = td_->auth_manager_->is_bot();
  if (m->had_reply_markup) {
    return false;
  }
  if (m->reply_markup != nullptr && m->reply_markup->type != ReplyMarkup::Type::InlineKeyboard) {
    return false;
  }

  auto my_id = td_->contacts_manager_->get_my_id();
  if (m->via_bot_user_id.is_valid() && m->via_bot_user_id != my_id) {
    return false;
  }

  DialogId my_dialog_id(my_id);
  bool has_edit_time_limit = !(is_bot && m->is_outgoing) && dialog_id != my_dialog_id;

  switch (dialog_id.get_type()) {
    case DialogType::User:
      if (!m->is_outgoing && dialog_id != my_dialog_id) {
        return false;
      }
      break;
    case DialogType::Chat:
      if (!m->is_outgoing) {
        return false;
      }
      break;
    case DialogType::Channel: {
      if (m->via_bot_user_id.is_valid()) {
        // outgoing via_bot messages can always be edited
        break;
      }

      auto channel_id = dialog_id.get_channel_id();
      auto channel_status = td_->contacts_manager_->get_channel_status(channel_id);
      if (m->is_channel_post) {
        if (!channel_status.can_edit_messages() && !(channel_status.can_post_messages() && m->is_outgoing)) {
          return false;
        }
      } else {
        if (!m->is_outgoing) {
          return false;
        }
        if (channel_status.can_pin_messages()) {
          has_edit_time_limit = false;
        }
      }
      break;
    }
    case DialogType::SecretChat:
      return false;
    case DialogType::None:
    default:
      UNREACHABLE();
      return false;
  }

  const int32 DEFAULT_EDIT_TIME_LIMIT = 2 * 86400;
  int32 edit_time_limit = G()->shared_config().get_option_integer("edit_time_limit", DEFAULT_EDIT_TIME_LIMIT);
  if (has_edit_time_limit && G()->unix_time_cached() - m->date >= edit_time_limit + (is_editing ? 300 : 0)) {
    return false;
  }

  switch (m->content->get_type()) {
    case MessageContentType::Animation:
    case MessageContentType::Audio:
    case MessageContentType::Document:
    case MessageContentType::Game:
    case MessageContentType::Photo:
    case MessageContentType::Text:
    case MessageContentType::Video:
    case MessageContentType::VoiceNote:
      return true;
    case MessageContentType::LiveLocation: {
      if (is_bot && only_reply_markup) {
        // there is no caption to edit, but bot can edit inline reply_markup
        return true;
      }
      return G()->unix_time_cached() - m->date < get_message_content_live_location_period(m->content.get());
    }
    case MessageContentType::Contact:
    case MessageContentType::Location:
    case MessageContentType::Sticker:
    case MessageContentType::Venue:
    case MessageContentType::VideoNote:
      // there is no caption to edit, but bot can edit inline reply_markup
      return is_bot && only_reply_markup;
    case MessageContentType::Invoice:
    case MessageContentType::Unsupported:
    case MessageContentType::ChatCreate:
    case MessageContentType::ChatChangeTitle:
    case MessageContentType::ChatChangePhoto:
    case MessageContentType::ChatDeletePhoto:
    case MessageContentType::ChatDeleteHistory:
    case MessageContentType::ChatAddUsers:
    case MessageContentType::ChatJoinedByLink:
    case MessageContentType::ChatDeleteUser:
    case MessageContentType::ChatMigrateTo:
    case MessageContentType::ChannelCreate:
    case MessageContentType::ChannelMigrateFrom:
    case MessageContentType::PinMessage:
    case MessageContentType::GameScore:
    case MessageContentType::ScreenshotTaken:
    case MessageContentType::ChatSetTtl:
    case MessageContentType::Call:
    case MessageContentType::PaymentSuccessful:
    case MessageContentType::ContactRegistered:
    case MessageContentType::ExpiredPhoto:
    case MessageContentType::ExpiredVideo:
    case MessageContentType::CustomServiceAction:
    case MessageContentType::WebsiteConnected:
    case MessageContentType::PassportDataSent:
    case MessageContentType::PassportDataReceived:
      return false;
    default:
      UNREACHABLE();
  }

  return false;
}

bool MessagesManager::is_broadcast_channel(DialogId dialog_id) const {
  if (dialog_id.get_type() != DialogType::Channel) {
    return false;
  }

  return td_->contacts_manager_->get_channel_type(dialog_id.get_channel_id()) == ChannelType::Broadcast;
}

void MessagesManager::edit_message_text(FullMessageId full_message_id,
                                        tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                        tl_object_ptr<td_api::InputMessageContent> &&input_message_content,
                                        Promise<Unit> &&promise) {
  if (input_message_content == nullptr) {
    return promise.set_error(Status::Error(5, "Can't edit message without new content"));
  }
  int32 new_message_content_type = input_message_content->get_id();
  if (new_message_content_type != td_api::inputMessageText::ID) {
    return promise.set_error(Status::Error(5, "Input message content type must be InputMessageText"));
  }

  LOG(INFO) << "Begin to edit text of " << full_message_id;
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(5, "Chat not found"));
  }

  if (!have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(5, "Can't access the chat"));
  }

  auto message_id = full_message_id.get_message_id();
  const Message *message = get_message_force(d, message_id);
  if (message == nullptr) {
    return promise.set_error(Status::Error(5, "Message not found"));
  }

  if (!can_edit_message(dialog_id, message, true)) {
    return promise.set_error(Status::Error(5, "Message can't be edited"));
  }

  MessageContentType old_message_content_type = message->content->get_type();
  if (old_message_content_type != MessageContentType::Text && old_message_content_type != MessageContentType::Game) {
    return promise.set_error(Status::Error(5, "There is no text in the message to edit"));
  }

  auto r_input_message_text = process_input_message_text(
      td_->contacts_manager_.get(), dialog_id, std::move(input_message_content), td_->auth_manager_->is_bot());
  if (r_input_message_text.is_error()) {
    return promise.set_error(r_input_message_text.move_as_error());
  }
  InputMessageText input_message_text = r_input_message_text.move_as_ok();

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false,
                                             !is_broadcast_channel(dialog_id));
  if (r_new_reply_markup.is_error()) {
    return promise.set_error(r_new_reply_markup.move_as_error());
  }
  auto input_reply_markup = get_input_reply_markup(r_new_reply_markup.ok());
  int32 flags = 0;
  if (input_message_text.disable_web_page_preview) {
    flags |= SEND_MESSAGE_FLAG_DISABLE_WEB_PAGE_PREVIEW;
  }

  send_closure(
      td_->create_net_actor<EditMessageActor>(std::move(promise)), &EditMessageActor::send, flags, dialog_id,
      message_id, input_message_text.text.text,
      get_input_message_entities(td_->contacts_manager_.get(), input_message_text.text.entities, "edit_message_text"),
      nullptr, nullptr, std::move(input_reply_markup), get_sequence_dispatcher_id(dialog_id, MessageContentType::None));
}

void MessagesManager::edit_message_live_location(FullMessageId full_message_id,
                                                 tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                                 tl_object_ptr<td_api::location> &&input_location,
                                                 Promise<Unit> &&promise) {
  LOG(INFO) << "Begin to edit live location of " << full_message_id;
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(5, "Chat not found"));
  }

  if (!have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(5, "Can't access the chat"));
  }

  auto message_id = full_message_id.get_message_id();
  const Message *message = get_message_force(d, message_id);
  if (message == nullptr) {
    return promise.set_error(Status::Error(5, "Message not found"));
  }

  if (!can_edit_message(dialog_id, message, true)) {
    return promise.set_error(Status::Error(5, "Message can't be edited"));
  }

  MessageContentType old_message_content_type = message->content->get_type();
  if (old_message_content_type != MessageContentType::LiveLocation) {
    return promise.set_error(Status::Error(5, "There is no live location in the message to edit"));
  }

  Location location(input_location);
  if (location.empty() && input_location != nullptr) {
    return promise.set_error(Status::Error(400, "Wrong location specified"));
  }

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false,
                                             !is_broadcast_channel(dialog_id));
  if (r_new_reply_markup.is_error()) {
    return promise.set_error(r_new_reply_markup.move_as_error());
  }
  auto input_reply_markup = get_input_reply_markup(r_new_reply_markup.ok());

  int32 flags = 0;
  if (location.empty()) {
    flags |= telegram_api::messages_editMessage::STOP_GEO_LIVE_MASK;
  }
  send_closure(td_->create_net_actor<EditMessageActor>(std::move(promise)), &EditMessageActor::send, flags, dialog_id,
               message_id, string(), vector<tl_object_ptr<telegram_api::MessageEntity>>(), nullptr,
               location.empty() ? nullptr : location.get_input_geo_point(), std::move(input_reply_markup),
               get_sequence_dispatcher_id(dialog_id, MessageContentType::None));
}

void MessagesManager::cancel_edit_message_media(DialogId dialog_id, Message *m) {
  if (m->edited_content == nullptr) {
    return;
  }

  cancel_upload_message_content_files(m->edited_content.get());

  m->edited_content = nullptr;
  m->edited_reply_markup = nullptr;
  m->edit_generation = 0;
  m->edit_promise.set_error(Status::Error(400, "Cancelled"));
}

void MessagesManager::on_message_media_edited(DialogId dialog_id, MessageId message_id, FileId file_id,
                                              FileId thumbnail_file_id, uint64 generation, Result<Unit> &&result) {
  CHECK(message_id.is_server());
  auto m = get_message({dialog_id, message_id});
  if (m == nullptr || m->edit_generation != generation) {
    // message is already deleted or was edited again
    return;
  }

  CHECK(m->edited_content != nullptr);
  if (result.is_ok()) {
    std::swap(m->content, m->edited_content);
    bool need_send_update_message_content = m->edited_content->get_type() == MessageContentType::Photo &&
                                            m->content->get_type() == MessageContentType::Photo;
    update_message_content(dialog_id, m, std::move(m->edited_content), need_send_update_message_content, true);
  } else {
    auto error_message = result.error().message();
    if (thumbnail_file_id.is_valid()) {
      // always delete partial remote location for the thumbnail, because it can't be reused anyway
      td_->file_manager_->delete_partial_remote_location(thumbnail_file_id);
    }
    if (file_id.is_valid()) {
      if (begins_with(error_message, "FILE_PART_") && ends_with(error_message, "_MISSING")) {
        do_send_message(dialog_id, m, {to_integer<int32>(error_message.substr(10))});
        return;
      }

      if (result.error().code() != 429 && result.error().code() < 500 && !G()->close_flag()) {
        td_->file_manager_->delete_partial_remote_location(file_id);
      }
    }

    cancel_upload_message_content_files(m->edited_content.get());
  }

  m->edited_content = nullptr;
  m->edited_reply_markup = nullptr;
  m->edit_generation = 0;
  if (result.is_ok()) {
    m->edit_promise.set_value(Unit());
  } else {
    m->edit_promise.set_error(result.move_as_error());
  }
}

void MessagesManager::edit_message_media(FullMessageId full_message_id,
                                         tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                         tl_object_ptr<td_api::InputMessageContent> &&input_message_content,
                                         Promise<Unit> &&promise) {
  if (input_message_content == nullptr) {
    return promise.set_error(Status::Error(5, "Can't edit message without new content"));
  }
  int32 new_message_content_type = input_message_content->get_id();
  if (new_message_content_type != td_api::inputMessageAnimation::ID &&
      new_message_content_type != td_api::inputMessageAudio::ID &&
      new_message_content_type != td_api::inputMessageDocument::ID &&
      new_message_content_type != td_api::inputMessagePhoto::ID &&
      new_message_content_type != td_api::inputMessageVideo::ID) {
    return promise.set_error(Status::Error(5, "Unsupported input message content type"));
  }

  LOG(INFO) << "Begin to edit media of " << full_message_id;
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(5, "Chat not found"));
  }

  if (!have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(5, "Can't access the chat"));
  }

  auto message_id = full_message_id.get_message_id();
  Message *m = get_message_force(d, message_id);
  if (m == nullptr) {
    return promise.set_error(Status::Error(5, "Message not found"));
  }

  if (!can_edit_message(dialog_id, m, true)) {
    return promise.set_error(Status::Error(5, "Message can't be edited"));
  }
  CHECK(message_id.is_server());

  MessageContentType old_message_content_type = m->content->get_type();
  if (old_message_content_type != MessageContentType::Animation &&
      old_message_content_type != MessageContentType::Audio &&
      old_message_content_type != MessageContentType::Document &&
      old_message_content_type != MessageContentType::Photo && old_message_content_type != MessageContentType::Video) {
    return promise.set_error(Status::Error(5, "There is no media in the message to edit"));
  }
  if (m->media_album_id != 0 && new_message_content_type != td_api::inputMessagePhoto::ID &&
      new_message_content_type != td_api::inputMessageVideo::ID) {
    return promise.set_error(Status::Error(5, "Message can be edit only to Photo or Video"));
  }
  if (m->ttl > 0) {
    return promise.set_error(Status::Error(5, "Can't edit media in self-destructing message"));
  }

  auto r_input_message_content = process_input_message_content(dialog_id, std::move(input_message_content));
  if (r_input_message_content.is_error()) {
    return promise.set_error(r_input_message_content.move_as_error());
  }
  InputMessageContent content = r_input_message_content.move_as_ok();
  if (content.ttl > 0) {
    return promise.set_error(Status::Error(5, "Can't enable self-destruction for media"));
  }

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false,
                                             !is_broadcast_channel(dialog_id));
  if (r_new_reply_markup.is_error()) {
    return promise.set_error(r_new_reply_markup.move_as_error());
  }

  cancel_edit_message_media(dialog_id, m);

  m->edited_content = dup_message_content(td_, dialog_id, content.content.get(), false);
  m->edited_reply_markup = r_new_reply_markup.move_as_ok();
  m->edit_generation = ++current_message_edit_generation_;
  m->edit_promise = std::move(promise);

  do_send_message(dialog_id, m);
}

void MessagesManager::edit_message_caption(FullMessageId full_message_id,
                                           tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                           tl_object_ptr<td_api::formattedText> &&input_caption,
                                           Promise<Unit> &&promise) {
  LOG(INFO) << "Begin to edit caption of " << full_message_id;

  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(5, "Chat not found"));
  }

  if (!have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(5, "Can't access the chat"));
  }

  auto message_id = full_message_id.get_message_id();
  const Message *message = get_message_force(d, message_id);
  if (message == nullptr) {
    return promise.set_error(Status::Error(5, "Message not found"));
  }

  if (!can_edit_message(dialog_id, message, true)) {
    return promise.set_error(Status::Error(5, "Message can't be edited"));
  }

  if (!can_have_message_content_caption(message->content->get_type())) {
    return promise.set_error(Status::Error(400, "There is no caption in the message to edit"));
  }

  auto r_caption = process_input_caption(td_->contacts_manager_.get(), dialog_id, std::move(input_caption),
                                         td_->auth_manager_->is_bot());
  if (r_caption.is_error()) {
    return promise.set_error(r_caption.move_as_error());
  }
  auto caption = r_caption.move_as_ok();

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false,
                                             !is_broadcast_channel(dialog_id));
  if (r_new_reply_markup.is_error()) {
    return promise.set_error(r_new_reply_markup.move_as_error());
  }
  auto input_reply_markup = get_input_reply_markup(r_new_reply_markup.ok());

  send_closure(td_->create_net_actor<EditMessageActor>(std::move(promise)), &EditMessageActor::send, 1 << 11, dialog_id,
               message_id, caption.text,
               get_input_message_entities(td_->contacts_manager_.get(), caption.entities, "edit_message_caption"),
               nullptr, nullptr, std::move(input_reply_markup),
               get_sequence_dispatcher_id(dialog_id, MessageContentType::None));
}

void MessagesManager::edit_message_reply_markup(FullMessageId full_message_id,
                                                tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                                Promise<Unit> &&promise) {
  if (!td_->auth_manager_->is_bot()) {
    return promise.set_error(Status::Error(3, "Method is available only for bots"));
  }

  LOG(INFO) << "Begin to edit reply markup of " << full_message_id;
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(5, "Chat not found"));
  }

  if (!have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(5, "Can't access the chat"));
  }

  auto message_id = full_message_id.get_message_id();
  const Message *message = get_message_force(d, message_id);
  if (message == nullptr) {
    return promise.set_error(Status::Error(5, "Message not found"));
  }

  if (!can_edit_message(dialog_id, message, true, true)) {
    return promise.set_error(Status::Error(5, "Message can't be edited"));
  }

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false,
                                             !is_broadcast_channel(dialog_id));
  if (r_new_reply_markup.is_error()) {
    return promise.set_error(r_new_reply_markup.move_as_error());
  }
  auto input_reply_markup = get_input_reply_markup(r_new_reply_markup.ok());
  send_closure(td_->create_net_actor<EditMessageActor>(std::move(promise)), &EditMessageActor::send, 0, dialog_id,
               message_id, string(), vector<tl_object_ptr<telegram_api::MessageEntity>>(), nullptr, nullptr,
               std::move(input_reply_markup), get_sequence_dispatcher_id(dialog_id, MessageContentType::None));
}

void MessagesManager::edit_inline_message_text(const string &inline_message_id,
                                               tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                               tl_object_ptr<td_api::InputMessageContent> &&input_message_content,
                                               Promise<Unit> &&promise) {
  if (!td_->auth_manager_->is_bot()) {
    return promise.set_error(Status::Error(3, "Method is available only for bots"));
  }

  if (input_message_content == nullptr) {
    return promise.set_error(Status::Error(5, "Can't edit message without new content"));
  }
  int32 new_message_content_type = input_message_content->get_id();
  if (new_message_content_type != td_api::inputMessageText::ID) {
    return promise.set_error(Status::Error(5, "Input message content type must be InputMessageText"));
  }

  auto r_input_message_text = process_input_message_text(
      td_->contacts_manager_.get(), DialogId(), std::move(input_message_content), td_->auth_manager_->is_bot());
  if (r_input_message_text.is_error()) {
    return promise.set_error(r_input_message_text.move_as_error());
  }
  InputMessageText input_message_text = r_input_message_text.move_as_ok();

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false, true);
  if (r_new_reply_markup.is_error()) {
    return promise.set_error(r_new_reply_markup.move_as_error());
  }

  auto input_bot_inline_message_id = td_->inline_queries_manager_->get_input_bot_inline_message_id(inline_message_id);
  if (input_bot_inline_message_id == nullptr) {
    return promise.set_error(Status::Error(400, "Wrong inline message identifier specified"));
  }

  int32 flags = 0;
  if (input_message_text.disable_web_page_preview) {
    flags |= SEND_MESSAGE_FLAG_DISABLE_WEB_PAGE_PREVIEW;
  }
  td_->create_handler<EditInlineMessageQuery>(std::move(promise))
      ->send(flags, std::move(input_bot_inline_message_id), input_message_text.text.text,
             get_input_message_entities(td_->contacts_manager_.get(), input_message_text.text.entities,
                                        "edit_inline_message_text"),
             nullptr, nullptr, get_input_reply_markup(r_new_reply_markup.ok()));
}

void MessagesManager::edit_inline_message_live_location(const string &inline_message_id,
                                                        tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                                        tl_object_ptr<td_api::location> &&input_location,
                                                        Promise<Unit> &&promise) {
  if (!td_->auth_manager_->is_bot()) {
    return promise.set_error(Status::Error(3, "Method is available only for bots"));
  }

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false, true);
  if (r_new_reply_markup.is_error()) {
    return promise.set_error(r_new_reply_markup.move_as_error());
  }

  auto input_bot_inline_message_id = td_->inline_queries_manager_->get_input_bot_inline_message_id(inline_message_id);
  if (input_bot_inline_message_id == nullptr) {
    return promise.set_error(Status::Error(400, "Wrong inline message identifier specified"));
  }

  Location location(input_location);
  if (location.empty() && input_location != nullptr) {
    return promise.set_error(Status::Error(400, "Wrong location specified"));
  }

  int32 flags = 0;
  if (location.empty()) {
    flags |= telegram_api::messages_editMessage::STOP_GEO_LIVE_MASK;
  }
  td_->create_handler<EditInlineMessageQuery>(std::move(promise))
      ->send(flags, std::move(input_bot_inline_message_id), "", vector<tl_object_ptr<telegram_api::MessageEntity>>(),
             nullptr, location.empty() ? nullptr : location.get_input_geo_point(),
             get_input_reply_markup(r_new_reply_markup.ok()));
}

void MessagesManager::edit_inline_message_media(const string &inline_message_id,
                                                tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                                tl_object_ptr<td_api::InputMessageContent> &&input_message_content,
                                                Promise<Unit> &&promise) {
  if (!td_->auth_manager_->is_bot()) {
    return promise.set_error(Status::Error(3, "Method is available only for bots"));
  }

  if (input_message_content == nullptr) {
    return promise.set_error(Status::Error(5, "Can't edit message without new content"));
  }
  int32 new_message_content_type = input_message_content->get_id();
  if (new_message_content_type != td_api::inputMessageAnimation::ID &&
      new_message_content_type != td_api::inputMessageAudio::ID &&
      new_message_content_type != td_api::inputMessageDocument::ID &&
      new_message_content_type != td_api::inputMessagePhoto::ID &&
      new_message_content_type != td_api::inputMessageVideo::ID) {
    return promise.set_error(Status::Error(5, "Unsupported input message content type"));
  }

  auto r_input_message_content = process_input_message_content(DialogId(), std::move(input_message_content));
  if (r_input_message_content.is_error()) {
    return promise.set_error(r_input_message_content.move_as_error());
  }
  InputMessageContent content = r_input_message_content.move_as_ok();
  if (content.ttl > 0) {
    LOG(ERROR) << "Have message content with ttl " << content.ttl;
    return promise.set_error(Status::Error(5, "Can't enable self-destruction for media"));
  }

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false, true);
  if (r_new_reply_markup.is_error()) {
    return promise.set_error(r_new_reply_markup.move_as_error());
  }

  auto input_bot_inline_message_id = td_->inline_queries_manager_->get_input_bot_inline_message_id(inline_message_id);
  if (input_bot_inline_message_id == nullptr) {
    return promise.set_error(Status::Error(400, "Wrong inline message identifier specified"));
  }

  auto input_media = get_input_media(content.content.get(), td_, nullptr, nullptr, 0);
  if (input_media == nullptr) {
    return promise.set_error(Status::Error(400, "Wrong message content specified"));
  }

  const FormattedText *caption = get_message_content_caption(content.content.get());
  td_->create_handler<EditInlineMessageQuery>(std::move(promise))
      ->send(1 << 11, std::move(input_bot_inline_message_id), caption == nullptr ? "" : caption->text,
             get_input_message_entities(td_->contacts_manager_.get(), caption, "edit_inline_message_media"),
             std::move(input_media), nullptr, get_input_reply_markup(r_new_reply_markup.ok()));
}

void MessagesManager::edit_inline_message_caption(const string &inline_message_id,
                                                  tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                                  tl_object_ptr<td_api::formattedText> &&input_caption,
                                                  Promise<Unit> &&promise) {
  if (!td_->auth_manager_->is_bot()) {
    return promise.set_error(Status::Error(3, "Method is available only for bots"));
  }

  auto r_caption = process_input_caption(td_->contacts_manager_.get(), DialogId(), std::move(input_caption),
                                         td_->auth_manager_->is_bot());
  if (r_caption.is_error()) {
    return promise.set_error(r_caption.move_as_error());
  }
  auto caption = r_caption.move_as_ok();

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false, true);
  if (r_new_reply_markup.is_error()) {
    return promise.set_error(r_new_reply_markup.move_as_error());
  }

  auto input_bot_inline_message_id = td_->inline_queries_manager_->get_input_bot_inline_message_id(inline_message_id);
  if (input_bot_inline_message_id == nullptr) {
    return promise.set_error(Status::Error(400, "Wrong inline message identifier specified"));
  }

  td_->create_handler<EditInlineMessageQuery>(std::move(promise))
      ->send(1 << 11, std::move(input_bot_inline_message_id), caption.text,
             get_input_message_entities(td_->contacts_manager_.get(), caption.entities, "edit_inline_message_caption"),
             nullptr, nullptr, get_input_reply_markup(r_new_reply_markup.ok()));
}

void MessagesManager::edit_inline_message_reply_markup(const string &inline_message_id,
                                                       tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                                       Promise<Unit> &&promise) {
  if (!td_->auth_manager_->is_bot()) {
    return promise.set_error(Status::Error(3, "Method is available only for bots"));
  }

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false, true);
  if (r_new_reply_markup.is_error()) {
    return promise.set_error(r_new_reply_markup.move_as_error());
  }

  auto input_bot_inline_message_id = td_->inline_queries_manager_->get_input_bot_inline_message_id(inline_message_id);
  if (input_bot_inline_message_id == nullptr) {
    return promise.set_error(Status::Error(400, "Wrong inline message identifier specified"));
  }

  td_->create_handler<EditInlineMessageQuery>(std::move(promise))
      ->send(0, std::move(input_bot_inline_message_id), string(), vector<tl_object_ptr<telegram_api::MessageEntity>>(),
             nullptr, nullptr, get_input_reply_markup(r_new_reply_markup.ok()));
}

int32 MessagesManager::get_message_flags(const Message *m) {
  int32 flags = 0;
  if (m->reply_to_message_id.is_valid()) {
    flags |= SEND_MESSAGE_FLAG_IS_REPLY;
  }
  if (m->disable_web_page_preview) {
    flags |= SEND_MESSAGE_FLAG_DISABLE_WEB_PAGE_PREVIEW;
  }
  if (m->reply_markup != nullptr) {
    flags |= SEND_MESSAGE_FLAG_HAS_REPLY_MARKUP;
  }
  if (m->disable_notification) {
    flags |= SEND_MESSAGE_FLAG_DISABLE_NOTIFICATION;
  }
  if (m->from_background) {
    flags |= SEND_MESSAGE_FLAG_FROM_BACKGROUND;
  }
  if (m->clear_draft) {
    flags |= SEND_MESSAGE_FLAG_CLEAR_DRAFT;
  }
  return flags;
}

bool MessagesManager::can_set_game_score(DialogId dialog_id, const Message *m) const {
  if (m == nullptr) {
    return false;
  }
  if (m->message_id.is_yet_unsent()) {
    return false;
  }
  if (m->message_id.is_local()) {
    return false;
  }
  if (m->via_bot_user_id.is_valid() && !m->is_outgoing) {
    return false;
  }

  if (!td_->auth_manager_->is_bot()) {
    return false;
  }
  if (m->reply_markup == nullptr || m->reply_markup->type != ReplyMarkup::Type::InlineKeyboard ||
      m->reply_markup->inline_keyboard.empty()) {
    return false;
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      if (!m->is_outgoing && dialog_id != get_my_dialog_id()) {
        return false;
      }
      break;
    case DialogType::Chat:
      if (!m->is_outgoing) {
        return false;
      }
      break;
    case DialogType::Channel: {
      if (m->via_bot_user_id.is_valid()) {
        // outgoing via_bot messages can always be edited
        break;
      }
      auto channel_id = dialog_id.get_channel_id();
      auto channel_status = td_->contacts_manager_->get_channel_status(channel_id);
      if (m->is_channel_post) {
        if (!channel_status.can_edit_messages() && (!channel_status.can_post_messages() || !m->is_outgoing)) {
          return false;
        }
      } else {
        if (!m->is_outgoing) {
          return false;
        }
      }
      break;
    }
    case DialogType::SecretChat:
      return false;
    case DialogType::None:
    default:
      UNREACHABLE();
      return false;
  }

  return m->content->get_type() == MessageContentType::Game;
}

void MessagesManager::set_game_score(FullMessageId full_message_id, bool edit_message, UserId user_id, int32 score,
                                     bool force, Promise<Unit> &&promise) {
  if (!td_->auth_manager_->is_bot()) {
    return promise.set_error(Status::Error(3, "Method is available only for bots"));
  }

  LOG(INFO) << "Begin to set game score of " << user_id << " in " << full_message_id;
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return promise.set_error(Status::Error(5, "Chat not found"));
  }

  if (!have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(5, "Can't access the chat"));
  }

  auto message_id = full_message_id.get_message_id();
  const Message *message = get_message_force(d, message_id);
  if (message == nullptr) {
    return promise.set_error(Status::Error(5, "Message not found"));
  }

  auto input_user = td_->contacts_manager_->get_input_user(user_id);
  if (input_user == nullptr) {
    return promise.set_error(Status::Error(400, "Wrong user identifier specified"));
  }

  if (!can_set_game_score(dialog_id, message)) {
    return promise.set_error(Status::Error(5, "Game score can't be set"));
  }

  send_closure(td_->create_net_actor<SetGameScoreActor>(std::move(promise)), &SetGameScoreActor::send, dialog_id,
               message_id, edit_message, std::move(input_user), score, force,
               get_sequence_dispatcher_id(dialog_id, MessageContentType::None));
}

void MessagesManager::set_inline_game_score(const string &inline_message_id, bool edit_message, UserId user_id,
                                            int32 score, bool force, Promise<Unit> &&promise) {
  if (!td_->auth_manager_->is_bot()) {
    return promise.set_error(Status::Error(3, "Method is available only for bots"));
  }

  auto input_bot_inline_message_id = td_->inline_queries_manager_->get_input_bot_inline_message_id(inline_message_id);
  if (input_bot_inline_message_id == nullptr) {
    return promise.set_error(Status::Error(400, "Wrong inline message identifier specified"));
  }

  auto input_user = td_->contacts_manager_->get_input_user(user_id);
  if (input_user == nullptr) {
    return promise.set_error(Status::Error(400, "Wrong user identifier specified"));
  }

  td_->create_handler<SetInlineGameScoreQuery>(std::move(promise))
      ->send(std::move(input_bot_inline_message_id), edit_message, std::move(input_user), score, force);
}

int64 MessagesManager::get_game_high_scores(FullMessageId full_message_id, UserId user_id, Promise<Unit> &&promise) {
  if (!td_->auth_manager_->is_bot()) {
    promise.set_error(Status::Error(3, "Method is available only for bots"));
    return 0;
  }

  LOG(INFO) << "Begin to get game high scores of " << user_id << " in " << full_message_id;
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    promise.set_error(Status::Error(5, "Chat not found"));
    return 0;
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(5, "Can't access the chat"));
    return 0;
  }

  auto message_id = full_message_id.get_message_id();
  const Message *message = get_message_force(d, message_id);
  if (message == nullptr) {
    promise.set_error(Status::Error(5, "Message not found"));
    return 0;
  }
  if (!message_id.is_server()) {
    promise.set_error(Status::Error(5, "Wrong message identifier specified"));
    return 0;
  }

  auto input_user = td_->contacts_manager_->get_input_user(user_id);
  if (input_user == nullptr) {
    promise.set_error(Status::Error(400, "Wrong user identifier specified"));
    return 0;
  }

  int64 random_id = 0;
  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || game_high_scores_.find(random_id) != game_high_scores_.end());
  game_high_scores_[random_id];  // reserve place for result

  td_->create_handler<GetGameHighScoresQuery>(std::move(promise))
      ->send(dialog_id, message_id, std::move(input_user), random_id);
  return random_id;
}

int64 MessagesManager::get_inline_game_high_scores(const string &inline_message_id, UserId user_id,
                                                   Promise<Unit> &&promise) {
  if (!td_->auth_manager_->is_bot()) {
    promise.set_error(Status::Error(3, "Method is available only for bots"));
    return 0;
  }

  auto input_bot_inline_message_id = td_->inline_queries_manager_->get_input_bot_inline_message_id(inline_message_id);
  if (input_bot_inline_message_id == nullptr) {
    promise.set_error(Status::Error(400, "Wrong inline message identifier specified"));
    return 0;
  }

  auto input_user = td_->contacts_manager_->get_input_user(user_id);
  if (input_user == nullptr) {
    promise.set_error(Status::Error(400, "Wrong user identifier specified"));
    return 0;
  }

  int64 random_id = 0;
  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || game_high_scores_.find(random_id) != game_high_scores_.end());
  game_high_scores_[random_id];  // reserve place for result

  td_->create_handler<GetInlineGameHighScoresQuery>(std::move(promise))
      ->send(std::move(input_bot_inline_message_id), std::move(input_user), random_id);
  return random_id;
}

void MessagesManager::on_get_game_high_scores(int64 random_id,
                                              tl_object_ptr<telegram_api::messages_highScores> &&high_scores) {
  auto it = game_high_scores_.find(random_id);
  CHECK(it != game_high_scores_.end());
  auto &result = it->second;
  CHECK(result == nullptr);

  if (high_scores == nullptr) {
    game_high_scores_.erase(it);
    return;
  }

  td_->contacts_manager_->on_get_users(std::move(high_scores->users_));

  result = make_tl_object<td_api::gameHighScores>();

  for (auto &high_score : high_scores->scores_) {
    int32 position = high_score->pos_;
    if (position <= 0) {
      LOG(ERROR) << "Receive wrong position = " << position;
      continue;
    }
    UserId user_id(high_score->user_id_);
    LOG_IF(ERROR, !td_->contacts_manager_->have_user(user_id)) << "Have no info about " << user_id;
    int32 score = high_score->score_;
    if (score < 0) {
      LOG(ERROR) << "Receive wrong score = " << score;
      continue;
    }
    result->scores_.push_back(make_tl_object<td_api::gameHighScore>(
        position, td_->contacts_manager_->get_user_id_object(user_id, "gameHighScore"), score));
  }
}

tl_object_ptr<td_api::gameHighScores> MessagesManager::get_game_high_scores_object(int64 random_id) {
  auto it = game_high_scores_.find(random_id);
  CHECK(it != game_high_scores_.end());
  auto result = std::move(it->second);
  game_high_scores_.erase(it);
  return result;
}

unique_ptr<MessagesManager::MessageForwardInfo> MessagesManager::get_message_forward_info(
    tl_object_ptr<telegram_api::messageFwdHeader> &&forward_header) {
  if (forward_header == nullptr) {
    return nullptr;
  }

  if (forward_header->date_ <= 0) {
    LOG(ERROR) << "Wrong date in message forward header: " << oneline(to_string(forward_header));
    return nullptr;
  }

  auto flags = forward_header->flags_;
  UserId sender_user_id;
  ChannelId channel_id;
  MessageId message_id;
  string author_signature;
  DialogId from_dialog_id;
  MessageId from_message_id;
  if ((flags & MESSAGE_FORWARD_HEADER_FLAG_HAS_AUTHOR_ID) != 0) {
    sender_user_id = UserId(forward_header->from_id_);
    if (!sender_user_id.is_valid()) {
      LOG(ERROR) << "Receive invalid sender id in message forward header: " << oneline(to_string(forward_header));
      sender_user_id = UserId();
    }
  }
  if ((flags & MESSAGE_FORWARD_HEADER_FLAG_HAS_CHANNEL_ID) != 0) {
    channel_id = ChannelId(forward_header->channel_id_);
    if (!channel_id.is_valid()) {
      LOG(ERROR) << "Receive invalid channel id in message forward header: " << oneline(to_string(forward_header));
    }
  }
  if ((flags & MESSAGE_FORWARD_HEADER_FLAG_HAS_MESSAGE_ID) != 0) {
    message_id = MessageId(ServerMessageId(forward_header->channel_post_));
    if (!message_id.is_valid()) {
      LOG(ERROR) << "Receive " << message_id << " in message forward header: " << oneline(to_string(forward_header));
      message_id = MessageId();
    }
  }
  if ((flags & MESSAGE_FORWARD_HEADER_FLAG_HAS_AUTHOR_SIGNATURE) != 0) {
    author_signature = std::move(forward_header->post_author_);
  }
  if ((flags & MESSAGE_FORWARD_HEADER_FLAG_HAS_SAVED_FROM) != 0) {
    from_dialog_id = DialogId(forward_header->saved_from_peer_);
    from_message_id = MessageId(ServerMessageId(forward_header->saved_from_msg_id_));
    if (!from_dialog_id.is_valid() || !from_message_id.is_valid()) {
      LOG(ERROR) << "Receive " << from_message_id << " in " << from_dialog_id
                 << " in message forward header: " << oneline(to_string(forward_header));
      from_dialog_id = DialogId();
      from_message_id = MessageId();
    }
  }

  DialogId dialog_id;
  if (!channel_id.is_valid()) {
    if (sender_user_id.is_valid()) {
      if (message_id.is_valid()) {
        LOG(ERROR) << "Receive non-empty message id in message forward header: " << oneline(to_string(forward_header));
        message_id = MessageId();
      }
    } else {
      LOG(ERROR) << "Receive wrong message forward header: " << oneline(to_string(forward_header));
      return nullptr;
    }
  } else {
    LOG_IF(ERROR, td_->contacts_manager_->have_min_channel(channel_id)) << "Receive forward from min channel";
    dialog_id = DialogId(channel_id);
    force_create_dialog(dialog_id, "message forward info");
    if (sender_user_id.is_valid()) {
      LOG(ERROR) << "Receive valid sender user id in message forward header: " << oneline(to_string(forward_header));
      sender_user_id = UserId();
    }
  }
  if (from_dialog_id.is_valid()) {
    force_create_dialog(from_dialog_id, "message forward from info");
  }

  return td::make_unique<MessageForwardInfo>(sender_user_id, forward_header->date_, dialog_id, message_id,
                                             author_signature, from_dialog_id, from_message_id);
}

tl_object_ptr<td_api::MessageForwardInfo> MessagesManager::get_message_forward_info_object(
    const unique_ptr<MessageForwardInfo> &forward_info) const {
  if (forward_info == nullptr) {
    return nullptr;
  }

  if (forward_info->dialog_id.is_valid()) {
    return make_tl_object<td_api::messageForwardedPost>(
        forward_info->dialog_id.get(), forward_info->author_signature, forward_info->date,
        forward_info->message_id.get(), forward_info->from_dialog_id.get(), forward_info->from_message_id.get());
  }
  return make_tl_object<td_api::messageForwardedFromUser>(
      td_->contacts_manager_->get_user_id_object(forward_info->sender_user_id, "messageForwardedFromUser"),
      forward_info->date, forward_info->from_dialog_id.get(), forward_info->from_message_id.get());
}

Result<unique_ptr<ReplyMarkup>> MessagesManager::get_dialog_reply_markup(
    DialogId dialog_id, tl_object_ptr<td_api::ReplyMarkup> &&reply_markup_ptr) const {
  if (reply_markup_ptr == nullptr) {
    return nullptr;
  }

  auto dialog_type = dialog_id.get_type();
  bool is_broadcast = is_broadcast_channel(dialog_id);

  bool only_inline_keyboard = is_broadcast;
  bool request_buttons_allowed = dialog_type == DialogType::User;
  bool switch_inline_buttons_allowed = !is_broadcast;

  TRY_RESULT(reply_markup,
             get_reply_markup(std::move(reply_markup_ptr), td_->auth_manager_->is_bot(), only_inline_keyboard,
                              request_buttons_allowed, switch_inline_buttons_allowed));
  if (reply_markup == nullptr) {
    return nullptr;
  }

  switch (dialog_type) {
    case DialogType::User:
      if (reply_markup->type != ReplyMarkup::Type::InlineKeyboard) {
        reply_markup->is_personal = false;
      }
      break;
    case DialogType::Channel:
    case DialogType::Chat:
    case DialogType::SecretChat:
    case DialogType::None:
      // nothing special
      break;
    default:
      UNREACHABLE();
  }

  return std::move(reply_markup);
}

class MessagesManager::ForwardMessagesLogEvent {
 public:
  DialogId to_dialog_id;
  DialogId from_dialog_id;
  vector<MessageId> message_ids;
  vector<Message *> messages_in;
  vector<unique_ptr<Message>> messages_out;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(to_dialog_id, storer);
    td::store(from_dialog_id, storer);
    td::store(message_ids, storer);

    td::store(narrow_cast<int32>(messages_in.size()), storer);
    for (auto m : messages_in) {
      td::store(*m, storer);
    }
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(to_dialog_id, parser);
    td::parse(from_dialog_id, parser);
    td::parse(message_ids, parser);

    CHECK(messages_out.empty());
    int32 size = parser.fetch_int();
    messages_out.resize(size);
    for (auto &m_out : messages_out) {
      m_out = make_unique<Message>();
      td::parse(*m_out, parser);
    }
  }
};

uint64 MessagesManager::save_forward_messages_logevent(DialogId to_dialog_id, DialogId from_dialog_id,
                                                       const vector<Message *> &messages,
                                                       const vector<MessageId> &message_ids) {
  ForwardMessagesLogEvent logevent{to_dialog_id, from_dialog_id, message_ids, messages, Auto()};
  auto storer = LogEventStorerImpl<ForwardMessagesLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ForwardMessages, storer);
}

void MessagesManager::do_forward_messages(DialogId to_dialog_id, DialogId from_dialog_id,
                                          const vector<Message *> &messages, const vector<MessageId> &message_ids,
                                          int64 logevent_id) {
  CHECK(messages.size() == message_ids.size());
  if (messages.empty()) {
    return;
  }

  if (logevent_id == 0 && G()->parameters().use_message_db) {
    logevent_id = save_forward_messages_logevent(to_dialog_id, from_dialog_id, messages, message_ids);
  }

  int32 flags = 0;
  if (messages[0]->disable_notification) {
    flags |= SEND_MESSAGE_FLAG_DISABLE_NOTIFICATION;
  }
  if (messages[0]->from_background) {
    flags |= SEND_MESSAGE_FLAG_FROM_BACKGROUND;
  }
  if (messages[0]->media_album_id != 0) {
    flags |= SEND_MESSAGE_FLAG_GROUP_MEDIA;
  }
  if (messages[0]->in_game_share) {
    flags |= SEND_MESSAGE_FLAG_WITH_MY_SCORE;
  }

  vector<int64> random_ids =
      transform(messages, [this, to_dialog_id](const Message *m) { return begin_send_message(to_dialog_id, m); });
  send_closure(td_->create_net_actor<ForwardMessagesActor>(get_erase_logevent_promise(logevent_id)),
               &ForwardMessagesActor::send, flags, to_dialog_id, from_dialog_id, message_ids, std::move(random_ids),
               get_sequence_dispatcher_id(to_dialog_id, MessageContentType::None));
}

Result<MessageId> MessagesManager::forward_message(DialogId to_dialog_id, DialogId from_dialog_id, MessageId message_id,
                                                   bool disable_notification, bool from_background,
                                                   bool in_game_share) {
  TRY_RESULT(result, forward_messages(to_dialog_id, from_dialog_id, {message_id}, disable_notification, from_background,
                                      in_game_share, false));
  CHECK(result.size() == 1);
  auto sent_message_id = result[0];
  if (sent_message_id == MessageId()) {
    return Status::Error(11, "Message can't be forwarded");
  }
  return sent_message_id;
}

Result<vector<MessageId>> MessagesManager::forward_messages(DialogId to_dialog_id, DialogId from_dialog_id,
                                                            vector<MessageId> message_ids, bool disable_notification,
                                                            bool from_background, bool in_game_share, bool as_album) {
  if (message_ids.size() > 100) {  // TODO replace with const from config or implement mass-forward
    return Status::Error(4, "Too much messages to forward");
  }
  if (message_ids.empty()) {
    return Status::Error(4, "There is no messages to forward");
  }

  Dialog *from_dialog = get_dialog_force(from_dialog_id);
  if (from_dialog == nullptr) {
    return Status::Error(5, "Chat to forward messages from not found");
  }
  if (!have_input_peer(from_dialog_id, AccessRights::Read)) {
    return Status::Error(5, "Can't access the chat to forward messages from");
  }
  if (from_dialog_id.get_type() == DialogType::SecretChat) {
    return Status::Error(7, "Can't forward messages from secret chats");
  }

  Dialog *to_dialog = get_dialog_force(to_dialog_id);
  if (to_dialog == nullptr) {
    return Status::Error(5, "Chat to forward messages to not found");
  }

  TRY_STATUS(can_send_message(to_dialog_id));

  for (auto message_id : message_ids) {
    if (!message_id.is_valid()) {
      return Status::Error(5, "Invalid message identifier");
    }
  }

  int64 media_album_id = 0;
  if (as_album && message_ids.size() > 1 && message_ids.size() <= MAX_GROUPED_MESSAGES) {
    do {
      media_album_id = Random::secure_int64();
    } while (media_album_id >= 0 || pending_message_group_sends_.count(media_album_id) != 0);
  }

  bool to_secret = to_dialog_id.get_type() == DialogType::SecretChat;

  vector<MessageId> result(message_ids.size());
  vector<Message *> forwarded_messages;
  vector<MessageId> forwarded_message_ids;
  vector<unique_ptr<MessageContent>> unforwarded_message_contents(message_ids.size());
  vector<bool> unforwarded_message_disable_web_page_previews(message_ids.size());
  auto my_id = td_->contacts_manager_->get_my_id();
  bool need_update_dialog_pos = false;
  for (size_t i = 0; i < message_ids.size(); i++) {
    MessageId message_id = get_persistent_message_id(from_dialog, message_ids[i]);

    const Message *forwarded_message = get_message_force(from_dialog, message_id);
    if (forwarded_message == nullptr) {
      LOG(INFO) << "Can't find " << message_id << " to forward";
      continue;
    }

    if (!can_forward_message(from_dialog_id, forwarded_message)) {
      LOG(INFO) << "Can't forward " << message_id;
      continue;
    }

    unique_ptr<MessageContent> content = dup_message_content(td_, to_dialog_id, forwarded_message->content.get(), true);
    if (content == nullptr) {
      LOG(INFO) << "Can't forward " << message_id;
      continue;
    }

    auto can_send_status = can_send_message_content(to_dialog_id, content.get(), true);
    if (can_send_status.is_error()) {
      LOG(INFO) << "Can't forward " << message_id << ": " << can_send_status.message();
      continue;
    }

    if (!message_id.is_server() || to_secret) {
      unforwarded_message_contents[i] = std::move(content);
      unforwarded_message_disable_web_page_previews[i] = forwarded_message->disable_web_page_preview;
      continue;
    }

    auto content_type = content->get_type();
    if (media_album_id != 0 && !is_allowed_media_group_content(content_type)) {
      media_album_id = 0;
      for (auto m : forwarded_messages) {
        m->media_album_id = 0;
      }
    }

    bool is_game = content_type == MessageContentType::Game;
    unique_ptr<MessageForwardInfo> forward_info;
    if (!is_game && content_type != MessageContentType::Audio) {
      DialogId saved_from_dialog_id;
      MessageId saved_from_message_id;
      if (to_dialog_id == DialogId(my_id)) {
        saved_from_dialog_id = from_dialog_id;
        saved_from_message_id = message_id;
      }

      if (forwarded_message->forward_info != nullptr) {
        forward_info = make_unique<MessageForwardInfo>(*forwarded_message->forward_info);
        forward_info->from_dialog_id = saved_from_dialog_id;
        forward_info->from_message_id = saved_from_message_id;
      } else {
        if (from_dialog_id != DialogId(my_id)) {
          if (!forwarded_message->is_channel_post) {
            forward_info =
                make_unique<MessageForwardInfo>(forwarded_message->sender_user_id, forwarded_message->date, DialogId(),
                                                MessageId(), "", saved_from_dialog_id, saved_from_message_id);
          } else {
            CHECK(from_dialog_id.get_type() == DialogType::Channel);
            MessageId forwarded_message_id = forwarded_message->message_id;
            auto author_signature = forwarded_message->sender_user_id.is_valid()
                                        ? td_->contacts_manager_->get_user_title(forwarded_message->sender_user_id)
                                        : forwarded_message->author_signature;
            forward_info = td::make_unique<MessageForwardInfo>(UserId(), forwarded_message->date, from_dialog_id,
                                                               forwarded_message_id, std::move(author_signature),
                                                               saved_from_dialog_id, saved_from_message_id);
          }
        }
      }
    }

    Message *m = get_message_to_send(to_dialog, MessageId(), disable_notification, from_background, std::move(content),
                                     &need_update_dialog_pos, std::move(forward_info));
    m->debug_forward_from = from_dialog_id;
    m->via_bot_user_id = forwarded_message->via_bot_user_id;
    m->media_album_id = media_album_id;
    m->in_game_share = in_game_share;
    if (forwarded_message->views > 0 && m->forward_info != nullptr) {
      m->views = forwarded_message->views;
    }

    if (is_game) {
      if (m->via_bot_user_id == my_id) {
        m->via_bot_user_id = UserId();
      } else if (m->via_bot_user_id == UserId()) {
        m->via_bot_user_id = forwarded_message->sender_user_id;
      }
    }

    result[i] = m->message_id;
    forwarded_messages.push_back(m);
    forwarded_message_ids.push_back(message_id);

    send_update_new_message(to_dialog, m);
  }

  if (!forwarded_messages.empty()) {
    do_forward_messages(to_dialog_id, from_dialog_id, forwarded_messages, forwarded_message_ids, 0);
  }

  for (size_t i = 0; i < unforwarded_message_contents.size(); i++) {
    if (unforwarded_message_contents[i] != nullptr) {
      Message *m = get_message_to_send(to_dialog, MessageId(), disable_notification, from_background,
                                       std::move(unforwarded_message_contents[i]), &need_update_dialog_pos);
      m->disable_web_page_preview = unforwarded_message_disable_web_page_previews[i];
      if (to_secret) {
        m->media_album_id = media_album_id;
      }

      save_send_message_logevent(to_dialog_id, m);
      do_send_message(to_dialog_id, m);
      result[i] = m->message_id;

      send_update_new_message(to_dialog, m);
    }
  }

  if (need_update_dialog_pos) {
    send_update_chat_last_message(to_dialog, "forward_messages");
  }

  return result;
}

Result<MessageId> MessagesManager::send_dialog_set_ttl_message(DialogId dialog_id, int32 ttl) {
  if (dialog_id.get_type() != DialogType::SecretChat) {
    return Status::Error(5, "Can't set chat ttl in non-secret chat");
  }

  if (ttl < 0) {
    return Status::Error(5, "Message ttl can't be negative");
  }

  LOG(INFO) << "Begin to set ttl in " << dialog_id << " to " << ttl;

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(5, "Chat not found");
  }

  TRY_STATUS(can_send_message(dialog_id));
  bool need_update_dialog_pos = false;
  Message *m = get_message_to_send(d, MessageId(), false, false, create_chat_set_ttl_message_content(ttl),
                                   &need_update_dialog_pos);

  send_update_new_message(d, m);
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "send_dialog_set_ttl_message");
  }

  int64 random_id = begin_send_message(dialog_id, m);

  send_closure(td_->secret_chats_manager_, &SecretChatsManager::send_set_ttl_message, dialog_id.get_secret_chat_id(),
               ttl, random_id, Promise<>());  // TODO Promise

  return m->message_id;
}

Status MessagesManager::send_screenshot_taken_notification_message(DialogId dialog_id) {
  auto dialog_type = dialog_id.get_type();
  if (dialog_type != DialogType::User && dialog_type != DialogType::SecretChat) {
    return Status::Error(5, "Notification about taken screenshot can be sent only in private and secret chats");
  }

  LOG(INFO) << "Begin to send notification about taken screenshot in " << dialog_id;

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(5, "Chat not found");
  }

  TRY_STATUS(can_send_message(dialog_id));

  if (dialog_type == DialogType::User) {
    bool need_update_dialog_pos = false;
    const Message *m = get_message_to_send(d, MessageId(), false, false, create_screenshot_taken_message_content(),
                                           &need_update_dialog_pos);

    do_send_screenshot_taken_notification_message(dialog_id, m, 0);

    send_update_new_message(d, m);
    if (need_update_dialog_pos) {
      send_update_chat_last_message(d, "send_screenshot_taken_notification_message");
    }
  } else {
    send_closure(td_->secret_chats_manager_, &SecretChatsManager::notify_screenshot_taken,
                 dialog_id.get_secret_chat_id(),
                 Promise<>());  // TODO Promise
  }

  return Status::OK();
}

class MessagesManager::SendScreenshotTakenNotificationMessageLogEvent {
 public:
  DialogId dialog_id;
  const Message *m_in = nullptr;
  unique_ptr<Message> m_out;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_id, storer);
    td::store(*m_in, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_id, parser);
    CHECK(m_out == nullptr);
    m_out = make_unique<Message>();
    td::parse(*m_out, parser);
  }
};

uint64 MessagesManager::save_send_screenshot_taken_notification_message_logevent(DialogId dialog_id, const Message *m) {
  if (!G()->parameters().use_message_db) {
    return 0;
  }

  CHECK(m != nullptr);
  LOG(INFO) << "Save " << FullMessageId(dialog_id, m->message_id) << " to binlog";
  SendScreenshotTakenNotificationMessageLogEvent logevent;
  logevent.dialog_id = dialog_id;
  logevent.m_in = m;
  auto storer = LogEventStorerImpl<SendScreenshotTakenNotificationMessageLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::SendScreenshotTakenNotificationMessage, storer);
}

void MessagesManager::do_send_screenshot_taken_notification_message(DialogId dialog_id, const Message *m,
                                                                    uint64 logevent_id) {
  LOG(INFO) << "Do send screenshot taken notification " << FullMessageId(dialog_id, m->message_id);
  CHECK(dialog_id.get_type() == DialogType::User);

  if (logevent_id == 0) {
    logevent_id = save_send_screenshot_taken_notification_message_logevent(dialog_id, m);
  }

  int64 random_id = begin_send_message(dialog_id, m);
  td_->create_handler<SendScreenshotNotificationQuery>(get_erase_logevent_promise(logevent_id))
      ->send(dialog_id, random_id);
}

Result<MessageId> MessagesManager::add_local_message(
    DialogId dialog_id, UserId sender_user_id, MessageId reply_to_message_id, bool disable_notification,
    tl_object_ptr<td_api::InputMessageContent> &&input_message_content) {
  if (input_message_content == nullptr) {
    return Status::Error(5, "Can't add local message without content");
  }

  LOG(INFO) << "Begin to add local message to " << dialog_id << " in reply to " << reply_to_message_id;
  if (input_message_content->get_id() == td_api::inputMessageForwarded::ID) {
    return Status::Error(5, "Can't add forwarded local message");
  }
  if (input_message_content->get_id() == td_api::inputMessageGame::ID) {
    return Status::Error(5, "Can't add local game message");
  }

  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    return Status::Error(5, "Chat not found");
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(400, "Can't access the chat");
  }
  TRY_RESULT(message_content, process_input_message_content(dialog_id, std::move(input_message_content)));

  bool is_channel_post = is_broadcast_channel(dialog_id);
  if (!td_->contacts_manager_->have_user_force(sender_user_id) && !(is_channel_post && sender_user_id == UserId())) {
    return Status::Error(400, "User not found");
  }

  auto dialog_type = dialog_id.get_type();
  auto my_id = td_->contacts_manager_->get_my_id();
  if (sender_user_id != my_id) {
    if (dialog_type == DialogType::User && DialogId(sender_user_id) != dialog_id) {
      return Status::Error(400, "Wrong sender user");
    }
    if (dialog_type == DialogType::SecretChat &&
        sender_user_id != td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id())) {
      return Status::Error(400, "Wrong sender user");
    }
  }

  MessageId message_id = get_next_local_message_id(d);

  auto m = make_unique<Message>();
  m->random_y = get_random_y(message_id);
  m->message_id = message_id;
  if (is_channel_post) {
    // sender of the post can be hidden
    if (td_->contacts_manager_->get_channel_sign_messages(dialog_id.get_channel_id())) {
      m->author_signature = td_->contacts_manager_->get_user_title(sender_user_id);
    }
  } else {
    m->sender_user_id = sender_user_id;
  }
  m->date = G()->unix_time();
  m->reply_to_message_id = get_reply_to_message_id(d, reply_to_message_id);
  m->is_channel_post = is_channel_post;
  m->is_outgoing = dialog_id != DialogId(my_id) && sender_user_id == my_id;
  m->disable_notification = disable_notification;
  m->from_background = false;
  m->views = 0;
  m->content = std::move(message_content.content);
  m->disable_web_page_preview = message_content.disable_web_page_preview;
  m->clear_draft = message_content.clear_draft;
  if (dialog_type == DialogType::SecretChat) {
    m->ttl = td_->contacts_manager_->get_secret_chat_ttl(dialog_id.get_secret_chat_id());
    if (is_service_message_content(m->content->get_type())) {
      m->ttl = 0;
    }
  } else if (message_content.ttl > 0) {
    m->ttl = message_content.ttl;
  }
  m->is_content_secret = is_secret_message_content(m->ttl, m->content->get_type());

  m->have_previous = true;
  m->have_next = true;

  bool need_update = true;
  bool need_update_dialog_pos = false;
  auto result =
      add_message_to_dialog(d, std::move(m), true, &need_update, &need_update_dialog_pos, "add local message");
  CHECK(result != nullptr) << message_id << " " << debug_add_message_to_dialog_fail_reason_;

  if (is_message_auto_read(dialog_id, result->is_outgoing)) {
    if (result->is_outgoing) {
      read_history_outbox(dialog_id, message_id);
    } else {
      read_history_inbox(dialog_id, message_id, 0, "add_local_message");
    }
  }

  if (message_content.clear_draft) {
    update_dialog_draft_message(d, nullptr, false, !need_update_dialog_pos);
  }

  send_update_new_message(d, result);
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "add_local_message");
  }

  return message_id;
}

bool MessagesManager::on_update_message_id(int64 random_id, MessageId new_message_id, const string &source) {
  if (!new_message_id.is_valid()) {
    LOG(ERROR) << "Receive " << new_message_id << " in update message id with random_id " << random_id << " from "
               << source;
    auto it = debug_being_sent_messages_.find(random_id);
    if (it == debug_being_sent_messages_.end()) {
      LOG(ERROR) << "Message with random_id " << random_id << " was not sent";
      return false;
    }
    auto dialog_id = it->second;
    if (!dialog_id.is_valid()) {
      LOG(ERROR) << "Sent message is in invalid " << dialog_id;
      return false;
    }
    if (!have_dialog(dialog_id)) {
      LOG(ERROR) << "Sent message is in not found " << dialog_id;
      return false;
    }
    LOG(ERROR) << "Receive " << new_message_id << " in update message id with random_id " << random_id << " in "
               << dialog_id;
    return false;
  }

  auto it = being_sent_messages_.find(random_id);
  if (it == being_sent_messages_.end()) {
    // update about new message sent from other device or service message
    LOG(INFO) << "Receive not send outgoing " << new_message_id << " with random_id = " << random_id;
    return true;
  }

  auto dialog_id = it->second.get_dialog_id();
  auto old_message_id = it->second.get_message_id();

  being_sent_messages_.erase(it);

  update_message_ids_[FullMessageId(dialog_id, new_message_id)] = old_message_id;
  return true;
}

bool MessagesManager::on_get_dialog_error(DialogId dialog_id, const Status &status, const string &source) {
  if (status.code() == 401 || status.message() == CSlice("SESSION_REVOKED") ||
      status.message() == CSlice("USER_DEACTIVATED")) {
    // authorization is lost
    return true;
  }
  if (status.code() == 420 || status.code() == 429) {
    // flood wait
    return true;
  }
  if (status.message() == CSlice("BOT_METHOD_INVALID")) {
    LOG(ERROR) << "Receive BOT_METHOD_INVALID from " << source;
    return true;
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::SecretChat:
      // to be implemented if necessary
      break;
    case DialogType::Channel:
      return td_->contacts_manager_->on_get_channel_error(dialog_id.get_channel_id(), status, source);
    case DialogType::None:
      // to be implemented if necessary
      break;
    default:
      UNREACHABLE();
  }
  return false;
}

void MessagesManager::on_dialog_updated(DialogId dialog_id, const char *source) {
  if (G()->parameters().use_message_db) {
    LOG(INFO) << "Update " << dialog_id << " from " << source;
    pending_updated_dialog_timeout_.add_timeout_in(dialog_id.get(), MAX_SAVE_DIALOG_DELAY);
  }
}

void MessagesManager::send_update_new_message(const Dialog *d, const Message *m) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);

  LOG(INFO) << "Send updateNewMessage for " << m->message_id << " in " << d->dialog_id;
  send_closure(G()->td(), &Td::send_update,
               make_tl_object<td_api::updateNewMessage>(get_message_object(d->dialog_id, m)));
}

MessagesManager::NotificationGroupInfo &MessagesManager::get_notification_group_info(Dialog *d, const Message *m) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  return is_from_mention_notification_group(d, m) ? d->mention_notification_group : d->message_notification_group;
}

NotificationGroupId MessagesManager::get_dialog_notification_group_id(DialogId dialog_id,
                                                                      NotificationGroupInfo &group_info) {
  if (!group_info.group_id.is_valid()) {
    NotificationGroupId next_notification_group_id;
    do {
      next_notification_group_id = td_->notification_manager_->get_next_notification_group_id();
      if (!next_notification_group_id.is_valid()) {
        return next_notification_group_id;
      }
    } while (get_message_notification_group_force(next_notification_group_id).dialog_id.is_valid());
    group_info.group_id = next_notification_group_id;
    group_info.is_changed = true;
    VLOG(notifications) << "Assign " << next_notification_group_id << " to " << group_info.group_id << '/' << dialog_id;
    on_dialog_updated(dialog_id, "get_dialog_notification_group_id");

    notification_group_id_to_dialog_id_.emplace(next_notification_group_id, dialog_id);

    if (running_get_channel_difference(dialog_id) || get_channel_difference_to_logevent_id_.count(dialog_id) != 0) {
      send_closure_later(td_->notification_manager_actor_, &NotificationManager::before_get_chat_difference,
                         next_notification_group_id);
    }
  }

  CHECK(group_info.group_id.is_valid());

  // notification group must be preloaded to guarantee that there is no race between
  // get_message_notifications_from_database_force and new notifications added right now
  td_->notification_manager_->load_group_force(group_info.group_id);

  return group_info.group_id;
}

NotificationId MessagesManager::get_next_notification_id(Dialog *d, NotificationGroupId notification_group_id,
                                                         MessageId message_id) {
  NotificationId notification_id;
  do {
    notification_id = td_->notification_manager_->get_next_notification_id();
    if (!notification_id.is_valid()) {
      return notification_id;
    }
  } while (d->notification_id_to_message_id.count(notification_id) != 0 ||
           d->new_secret_chat_notification_id == notification_id);  // just in case
  if (message_id.is_valid()) {
    add_notification_id_to_message_id_correspondence(d, notification_id, message_id);
  }
  return notification_id;
}

MessagesManager::MessageNotificationGroup MessagesManager::get_message_notification_group_force(
    NotificationGroupId group_id) {
  CHECK(group_id.is_valid());
  Dialog *d = nullptr;
  auto it = notification_group_id_to_dialog_id_.find(group_id);
  if (it != notification_group_id_to_dialog_id_.end()) {
    d = get_dialog(it->second);
    CHECK(d != nullptr);
  } else if (G()->parameters().use_message_db) {
    auto r_value = G()->td_db()->get_dialog_db_sync()->get_notification_group(group_id);
    if (r_value.is_ok()) {
      VLOG(notifications) << "Loaded " << r_value.ok() << " from database";
      d = get_dialog_force(r_value.ok().dialog_id);
    } else {
      VLOG(notifications) << "Failed to load " << group_id << " from database";
    }
  }

  if (d == nullptr) {
    return MessageNotificationGroup();
  }
  CHECK(d->message_notification_group.group_id == group_id || d->mention_notification_group.group_id == group_id);

  bool from_mentions = d->mention_notification_group.group_id == group_id;
  auto &group_info = from_mentions ? d->mention_notification_group : d->message_notification_group;

  MessageNotificationGroup result;
  VLOG(notifications) << "Found " << group_info.group_id << '/' << d->dialog_id << " by " << group_id;
  result.dialog_id = d->dialog_id;
  result.type = from_mentions ? NotificationGroupType::Mentions : NotificationGroupType::Messages;
  result.total_count = get_dialog_pending_notification_count(d, from_mentions);
  result.total_count -= static_cast<int32>(from_mentions ? d->pending_new_mention_notifications.size()
                                                         : d->pending_new_message_notifications.size());
  if (result.total_count < 0) {
    LOG(ERROR) << "Total notification count is negative in " << d->dialog_id;
    result.total_count = 0;
  }
  if (d->new_secret_chat_notification_id.is_valid()) {
    CHECK(d->dialog_id.get_type() == DialogType::SecretChat);
    result.notifications.emplace_back(d->new_secret_chat_notification_id,
                                      td_->contacts_manager_->get_secret_chat_date(d->dialog_id.get_secret_chat_id()),
                                      create_new_secret_chat_notification());
  } else {
    result.notifications = get_message_notifications_from_database_force(
        d, from_mentions, static_cast<int32>(td_->notification_manager_->get_max_notification_group_size()));
  }

  int32 last_notification_date = 0;
  NotificationId last_notification_id;
  if (!result.notifications.empty()) {
    last_notification_date = result.notifications[0].date;
    last_notification_id = result.notifications[0].notification_id;
  }
  if (last_notification_date != group_info.last_notification_date ||
      last_notification_id != group_info.last_notification_id) {
    LOG(ERROR) << "Fix last notification date in " << d->dialog_id << " from " << group_info.last_notification_date
               << " to " << last_notification_date << " and last notification id from "
               << group_info.last_notification_id << " to " << last_notification_id;
    set_dialog_last_notification(d->dialog_id, group_info, last_notification_date, last_notification_id,
                                 "get_message_notification_group_force");
  }

  std::reverse(result.notifications.begin(), result.notifications.end());

  return result;
}

bool MessagesManager::is_from_mention_notification_group(const Dialog *d, const Message *m) {
  return m->contains_mention;
}

bool MessagesManager::is_message_notification_active(const Dialog *d, const Message *m) {
  if (is_from_mention_notification_group(d, m)) {
    return m->notification_id.get() > d->mention_notification_group.max_removed_notification_id.get() &&
           (m->contains_unread_mention || m->message_id == d->pinned_message_notification_message_id);
  } else {
    return m->notification_id.get() > d->message_notification_group.max_removed_notification_id.get() &&
           m->message_id.get() > d->last_read_inbox_message_id.get();
  }
}

void MessagesManager::try_add_pinned_message_notification(Dialog *d, vector<Notification> &res,
                                                          NotificationId max_notification_id, int32 limit) {
  CHECK(d != nullptr);
  auto message_id = d->pinned_message_notification_message_id;
  if (!message_id.is_valid()) {
    return;
  }

  auto m = get_message_force(d, message_id);
  if (m->notification_id.get() > d->mention_notification_group.max_removed_notification_id.get()) {
    if (m->notification_id.get() < max_notification_id.get()) {
      VLOG(notifications) << "Add " << m->notification_id << " about pinned " << message_id << " in " << d->dialog_id;
      auto pos = res.size();
      res.emplace_back(m->notification_id, m->date, create_new_message_notification(message_id));
      while (pos > 0 && res[pos - 1].type->get_message_id().get() < message_id.get()) {
        std::swap(res[pos - 1], res[pos]);
        pos--;
      }
      if (pos > 0 && res[pos - 1].type->get_message_id().get() == message_id.get()) {
        res.erase(res.begin() + pos);  // notification was already there
      }
      if (res.size() > static_cast<size_t>(limit)) {
        res.pop_back();
        CHECK(res.size() == static_cast<size_t>(limit));
      }
    }
  } else {
    set_dialog_pinned_message_notification(d, MessageId());
  }
}

vector<Notification> MessagesManager::get_message_notifications_from_database_force(Dialog *d, bool from_mentions,
                                                                                    int32 limit) {
  CHECK(d != nullptr);
  if (!G()->parameters().use_message_db) {
    return {};
  }

  auto &group_info = from_mentions ? d->mention_notification_group : d->message_notification_group;
  auto from_notification_id = NotificationId::max();
  auto from_message_id = MessageId::max();
  vector<Notification> res;
  while (true) {
    auto result = do_get_message_notifications_from_database_force(d, from_mentions, from_notification_id,
                                                                   from_message_id, limit);
    if (result.is_error()) {
      break;
    }
    auto messages = result.move_as_ok();
    if (messages.empty()) {
      break;
    }

    bool is_found = false;
    VLOG(notifications) << "Loaded " << messages.size() << (from_mentions ? " mention" : "")
                        << " messages with notifications from database in " << group_info.group_id << '/'
                        << d->dialog_id;
    for (auto &message : messages) {
      auto m = on_get_message_from_database(d->dialog_id, d, std::move(message));
      if (m == nullptr || !m->notification_id.is_valid()) {
        // notification_id can be empty if it is deleted in memory, but not in the database
        continue;
      }

      if (m->notification_id.get() <= group_info.max_removed_notification_id.get() ||
          (!from_mentions && m->message_id.get() <= d->last_read_inbox_message_id.get())) {
        // if message still has notification_id, but it was removed via max_removed_notification_id
        // or last_read_inbox_message_id, then there will be no more messages with active notifications
        is_found = false;
        break;
      }

      CHECK(is_message_notification_active(d, m));

      CHECK(m->notification_id.get() < from_notification_id.get());
      from_notification_id = m->notification_id;
      from_message_id = m->message_id;
      is_found = true;

      if (is_from_mention_notification_group(d, m) == from_mentions) {
        // skip mention messages returned among unread messages
        res.emplace_back(m->notification_id, m->date, create_new_message_notification(m->message_id));
      }
    }
    if (!res.empty() || !is_found) {
      break;
    }
  }
  if (from_mentions) {
    try_add_pinned_message_notification(d, res, NotificationId::max(), limit);
  }
  return res;
}

Result<vector<BufferSlice>> MessagesManager::do_get_message_notifications_from_database_force(
    Dialog *d, bool from_mentions, NotificationId from_notification_id, MessageId from_message_id, int32 limit) {
  CHECK(G()->parameters().use_message_db);

  auto *db = G()->td_db()->get_messages_db_sync();
  if (!from_mentions) {
    CHECK(from_message_id.get() > d->last_read_inbox_message_id.get());
    VLOG(notifications) << "Trying to load " << limit << " messages with notifications in "
                        << d->message_notification_group.group_id << '/' << d->dialog_id << " from "
                        << from_notification_id;
    return db->get_messages_from_notification_id(d->dialog_id, from_notification_id, limit);
  } else {
    VLOG(notifications) << "Trying to load " << limit << " messages with unread mentions in "
                        << d->mention_notification_group.group_id << '/' << d->dialog_id << " from " << from_message_id;

    // ignore first_db_message_id, notifications can be nonconsecutive
    MessagesDbMessagesQuery db_query;
    db_query.dialog_id = d->dialog_id;
    db_query.index_mask = search_messages_filter_index_mask(SearchMessagesFilter::UnreadMention);
    db_query.from_message_id = from_message_id;
    db_query.offset = 0;
    db_query.limit = limit;
    return db->get_messages(db_query);
  }
}

vector<NotificationGroupKey> MessagesManager::get_message_notification_group_keys_from_database(
    NotificationGroupKey from_group_key, int32 limit) {
  if (!G()->parameters().use_message_db) {
    return {};
  }
  CHECK(start_time_ != 0);  // ensure that MessagesManager was already inited in start_up

  VLOG(notifications) << "Trying to load " << limit << " message notification groups from database from "
                      << from_group_key;

  Result<vector<NotificationGroupKey>> r_notification_group_keys =
      G()->td_db()->get_dialog_db_sync()->get_notification_groups_by_last_notification_date(from_group_key, limit);
  r_notification_group_keys.ensure();
  auto group_keys = r_notification_group_keys.move_as_ok();

  for (auto &group_key : group_keys) {
    CHECK(group_key.dialog_id.is_valid());
    Dialog *d = get_dialog_force(group_key.dialog_id);
    if (d == nullptr) {
      continue;
    }

    CHECK(d->dialog_id == group_key.dialog_id);
    CHECK(notification_group_id_to_dialog_id_[group_key.group_id] == d->dialog_id);

    VLOG(notifications) << "Loaded " << group_key << " from database";
  }
  return group_keys;
}

void MessagesManager::get_message_notifications_from_database(DialogId dialog_id, NotificationGroupId group_id,
                                                              NotificationId from_notification_id,
                                                              MessageId from_message_id, int32 limit,
                                                              Promise<vector<Notification>> promise) {
  if (!G()->parameters().use_message_db) {
    return promise.set_error(Status::Error(500, "There is no message database"));
  }

  CHECK(dialog_id.is_valid());
  CHECK(group_id.is_valid());
  CHECK(limit > 0);

  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (d->message_notification_group.group_id != group_id && d->mention_notification_group.group_id != group_id) {
    return promise.set_value(vector<Notification>());
  }
  bool from_mentions = d->mention_notification_group.group_id == group_id;
  if (d->new_secret_chat_notification_id.is_valid()) {
    CHECK(d->dialog_id.get_type() == DialogType::SecretChat);
    vector<Notification> notifications;
    if (!from_mentions && d->new_secret_chat_notification_id.get() < from_notification_id.get()) {
      notifications.emplace_back(d->new_secret_chat_notification_id,
                                 td_->contacts_manager_->get_secret_chat_date(d->dialog_id.get_secret_chat_id()),
                                 create_new_secret_chat_notification());
    }
    return promise.set_value(std::move(notifications));
  }

  do_get_message_notifications_from_database(d, from_mentions, from_notification_id, from_notification_id,
                                             from_message_id, limit, std::move(promise));
}

void MessagesManager::do_get_message_notifications_from_database(Dialog *d, bool from_mentions,
                                                                 NotificationId initial_from_notification_id,
                                                                 NotificationId from_notification_id,
                                                                 MessageId from_message_id, int32 limit,
                                                                 Promise<vector<Notification>> promise) {
  CHECK(G()->parameters().use_message_db);

  auto &group_info = from_mentions ? d->mention_notification_group : d->message_notification_group;
  if (from_notification_id.get() <= group_info.max_removed_notification_id.get() ||
      (!from_mentions && from_message_id.get() <= d->last_read_inbox_message_id.get())) {
    return promise.set_value(vector<Notification>());
  }

  auto dialog_id = d->dialog_id;
  auto new_promise =
      PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, from_mentions, initial_from_notification_id, limit,
                              promise = std::move(promise)](Result<vector<BufferSlice>> result) mutable {
        send_closure(actor_id, &MessagesManager::on_get_message_notifications_from_database, dialog_id, from_mentions,
                     initial_from_notification_id, limit, std::move(result), std::move(promise));
      });

  auto *db = G()->td_db()->get_messages_db_async();
  if (!from_mentions) {
    VLOG(notifications) << "Trying to load " << limit << " messages with notifications in " << group_info.group_id
                        << '/' << dialog_id << " from " << from_notification_id;
    return db->get_messages_from_notification_id(d->dialog_id, from_notification_id, limit, std::move(new_promise));
  } else {
    VLOG(notifications) << "Trying to load " << limit << " messages with unread mentions in " << group_info.group_id
                        << '/' << dialog_id << " from " << from_message_id;

    // ignore first_db_message_id, notifications can be nonconsecutive
    MessagesDbMessagesQuery db_query;
    db_query.dialog_id = dialog_id;
    db_query.index_mask = search_messages_filter_index_mask(SearchMessagesFilter::UnreadMention);
    db_query.from_message_id = from_message_id;
    db_query.offset = 0;
    db_query.limit = limit;
    return db->get_messages(db_query, std::move(new_promise));
  }
}

void MessagesManager::on_get_message_notifications_from_database(DialogId dialog_id, bool from_mentions,
                                                                 NotificationId initial_from_notification_id,
                                                                 int32 limit, Result<vector<BufferSlice>> result,
                                                                 Promise<vector<Notification>> promise) {
  if (result.is_error()) {
    return promise.set_error(result.move_as_error());
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  auto &group_info = from_mentions ? d->mention_notification_group : d->message_notification_group;
  if (!group_info.group_id.is_valid()) {
    return promise.set_error(Status::Error("Notification group was deleted"));
  }

  auto messages = result.move_as_ok();
  vector<Notification> res;
  res.reserve(messages.size());
  NotificationId from_notification_id;
  MessageId from_message_id;
  VLOG(notifications) << "Loaded " << messages.size() << " messages with notifications in " << group_info.group_id
                      << '/' << dialog_id << " from database";
  for (auto &message : messages) {
    auto m = on_get_message_from_database(dialog_id, d, std::move(message));
    if (m == nullptr || !m->notification_id.is_valid()) {
      continue;
    }

    if (m->notification_id.get() <= group_info.max_removed_notification_id.get() ||
        (!from_mentions && m->message_id.get() <= d->last_read_inbox_message_id.get())) {
      // if message still has notification_id, but it was removed via max_removed_notification_id
      // or last_read_inbox_message_id, then there will be no more messages with active notifications
      from_notification_id = NotificationId();  // stop requesting database
      break;
    }

    CHECK(is_message_notification_active(d, m));

    CHECK(!from_notification_id.is_valid() || m->notification_id.get() < from_notification_id.get());
    from_notification_id = m->notification_id;
    CHECK(!from_message_id.is_valid() || m->message_id.get() < from_message_id.get());
    from_message_id = m->message_id;

    if (is_from_mention_notification_group(d, m) == from_mentions) {
      // skip mention messages returned among unread messages
      res.emplace_back(m->notification_id, m->date, create_new_message_notification(m->message_id));
    }
  }
  if (!res.empty() || !from_notification_id.is_valid() || static_cast<size_t>(limit) > messages.size()) {
    if (from_mentions) {
      try_add_pinned_message_notification(d, res, initial_from_notification_id, limit);
    }

    std::reverse(res.begin(), res.end());
    return promise.set_value(std::move(res));
  }

  // try again from adjusted from_notification_id and from_message_id
  do_get_message_notifications_from_database(d, from_mentions, initial_from_notification_id, from_notification_id,
                                             from_message_id, limit, std::move(promise));
}

void MessagesManager::remove_message_notification(DialogId dialog_id, NotificationGroupId group_id,
                                                  NotificationId notification_id) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(ERROR) << "Can't find " << dialog_id;
    return;
  }
  if (d->message_notification_group.group_id != group_id && d->mention_notification_group.group_id != group_id) {
    LOG(ERROR) << "There is no " << group_id << " in " << dialog_id;
    return;
  }
  if (notification_id == NotificationId::max() || !notification_id.is_valid()) {
    return;  // there can be no notification with this ID
  }

  bool from_mentions = d->mention_notification_group.group_id == group_id;
  if (d->new_secret_chat_notification_id.is_valid()) {
    if (!from_mentions && d->new_secret_chat_notification_id == notification_id) {
      return remove_new_secret_chat_notification(d, false);
    }
    return;
  }

  auto it = d->notification_id_to_message_id.find(notification_id);
  if (it != d->notification_id_to_message_id.end()) {
    auto m = get_message(d, it->second);
    CHECK(m != nullptr);
    CHECK(m->notification_id == notification_id);
    if (is_from_mention_notification_group(d, m) == from_mentions && is_message_notification_active(d, m)) {
      remove_message_notification_id(d, m, false);
    }
    return;
  }

  if (G()->parameters().use_message_db) {
    G()->td_db()->get_messages_db_async()->get_messages_from_notification_id(
        dialog_id, NotificationId(notification_id.get() + 1), 1,
        PromiseCreator::lambda(
            [dialog_id, from_mentions, notification_id, actor_id = actor_id(this)](vector<BufferSlice> result) {
              send_closure(actor_id, &MessagesManager::do_remove_message_notification, dialog_id, from_mentions,
                           notification_id, std::move(result));
            }));
  }
}

void MessagesManager::do_remove_message_notification(DialogId dialog_id, bool from_mentions,
                                                     NotificationId notification_id, vector<BufferSlice> result) {
  if (result.empty()) {
    return;
  }
  CHECK(result.size() == 1);

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  auto m = on_get_message_from_database(dialog_id, d, std::move(result[0]));
  if (m != nullptr && m->notification_id == notification_id &&
      is_from_mention_notification_group(d, m) == from_mentions && is_message_notification_active(d, m)) {
    remove_message_notification_id(d, m, false);
  }
}

void MessagesManager::remove_message_notifications(DialogId dialog_id, NotificationGroupId group_id,
                                                   NotificationId max_notification_id) {
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(ERROR) << "Can't find " << dialog_id;
    return;
  }
  if (d->message_notification_group.group_id != group_id && d->mention_notification_group.group_id != group_id) {
    LOG(ERROR) << "There is no " << group_id << " in " << dialog_id;
    return;
  }
  if (!max_notification_id.is_valid()) {
    return;
  }

  bool from_mentions = d->mention_notification_group.group_id == group_id;
  if (d->new_secret_chat_notification_id.is_valid()) {
    if (!from_mentions && d->new_secret_chat_notification_id.get() <= max_notification_id.get()) {
      return remove_new_secret_chat_notification(d, false);
    }
    return;
  }
  auto &group_info = from_mentions ? d->mention_notification_group : d->message_notification_group;
  if (max_notification_id.get() <= group_info.max_removed_notification_id.get()) {
    return;
  }

  VLOG(notifications) << "Set max_removed_notification_id in " << group_info.group_id << '/' << dialog_id << " to "
                      << max_notification_id;
  group_info.max_removed_notification_id = max_notification_id;
  on_dialog_updated(dialog_id, "remove_message_notifications");
}

int32 MessagesManager::get_dialog_pending_notification_count(const Dialog *d, bool from_mentions) const {
  CHECK(d != nullptr);
  if (from_mentions) {
    return d->unread_mention_count + (d->pinned_message_notification_message_id.is_valid() ? 1 : 0);
  } else {
    if (d->new_secret_chat_notification_id.is_valid()) {
      return 1;
    }
    if (is_dialog_muted(d)) {
      return 0;
    }

    return d->server_unread_count + d->local_unread_count;
  }
}

void MessagesManager::update_dialog_mention_notification_count(const Dialog *d) {
  CHECK(d != nullptr);
  if (!d->mention_notification_group.group_id.is_valid()) {
    return;
  }
  auto total_count =
      get_dialog_pending_notification_count(d, true) - static_cast<int32>(d->pending_new_mention_notifications.size());
  if (total_count < 0) {
    LOG(ERROR) << "Total mention notification count is negative in " << d->dialog_id;
    total_count = 0;
  }
  send_closure_later(G()->notification_manager(), &NotificationManager::set_notification_total_count,
                     d->mention_notification_group.group_id, total_count);
}

bool MessagesManager::add_new_message_notification(Dialog *d, Message *m, bool force) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);

  CHECK(!m->notification_id.is_valid());
  if (m->is_outgoing || d->dialog_id == get_my_dialog_id() || td_->auth_manager_->is_bot()) {
    return false;
  }
  auto from_mentions = is_from_mention_notification_group(d, m);
  bool is_pinned = m->content->get_type() == MessageContentType::PinMessage;
  bool is_active = from_mentions ? m->contains_unread_mention || is_pinned
                                 : m->message_id.get() > d->last_read_inbox_message_id.get();
  if (!is_active) {
    VLOG(notifications) << "Disable inactive notification for " << m->message_id << " in " << d->dialog_id;
    return false;
  }

  if (d->dialog_id.get_type() == DialogType::Channel) {
    if (!td_->contacts_manager_->get_channel_status(d->dialog_id.get_channel_id()).is_member() ||
        m->date < td_->contacts_manager_->get_channel_date(d->dialog_id.get_channel_id())) {
      return false;
    }
  }
  if (d->dialog_id.get_type() == DialogType::SecretChat) {
    if (td_->contacts_manager_->get_secret_chat_state(d->dialog_id.get_secret_chat_id()) == SecretChatState::Closed) {
      return false;
    }
  }

  VLOG(notifications) << "Trying to " << (force ? "forcely " : "") << "add new message notification for "
                      << m->message_id << " in " << d->dialog_id
                      << (m->disable_notification ? " silently" : " with sound");

  DialogId settings_dialog_id = d->dialog_id;
  Dialog *settings_dialog = d;
  if (m->contains_mention && m->sender_user_id.is_valid()) {
    // have a mention, so use notification settings from the dialog with the sender
    settings_dialog_id = DialogId(m->sender_user_id);
    settings_dialog = get_dialog_force(settings_dialog_id);
  }

  bool have_settings;
  int32 mute_until;
  std::tie(have_settings, mute_until) = get_dialog_mute_until(settings_dialog_id, settings_dialog);
  if (mute_until > G()->unix_time()) {
    VLOG(notifications) << "Disable notification, because " << settings_dialog_id << " is muted";
    if (is_pinned) {
      set_dialog_pinned_message_notification(d, MessageId());
    }
    return false;
  }

  auto &pending_notifications =
      from_mentions ? d->pending_new_mention_notifications : d->pending_new_message_notifications;
  if (!force && (!have_settings || !pending_notifications.empty())) {
    VLOG(notifications) << "Delay new message notification for " << m->message_id << " in " << d->dialog_id << " with "
                        << pending_notifications.size() << " already waiting messages";
    if (pending_notifications.empty()) {
      create_actor<SleepActor>("FlushPendingNewMessageNotificationsSleepActor", 5.0,
                               PromiseCreator::lambda([actor_id = actor_id(this), dialog_id = d->dialog_id,
                                                       from_mentions](Result<Unit> result) {
                                 send_closure(actor_id, &MessagesManager::flush_pending_new_message_notifications,
                                              dialog_id, from_mentions, DialogId());
                               }))
          .release();
    }
    auto last_settings_dialog_id = (pending_notifications.empty() ? DialogId() : pending_notifications.back().first);
    pending_notifications.emplace_back((have_settings ? DialogId() : settings_dialog_id), m->message_id);
    if (!have_settings && last_settings_dialog_id != settings_dialog_id) {
      VLOG(notifications) << "Fetch notification settings for " << settings_dialog_id;
      auto promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_id = d->dialog_id, from_mentions,
                                             settings_dialog_id](Result<Unit> result) {
        send_closure(actor_id, &MessagesManager::flush_pending_new_message_notifications, dialog_id, from_mentions,
                     settings_dialog_id);
      });
      if (settings_dialog == nullptr && have_input_peer(settings_dialog_id, AccessRights::Read)) {
        force_create_dialog(settings_dialog_id, "add_new_message_notification");
        settings_dialog = get_dialog(settings_dialog_id);
      }
      if (settings_dialog != nullptr) {
        send_get_dialog_notification_settings_query(settings_dialog_id, std::move(promise));
      } else {
        send_get_dialog_query(settings_dialog_id, std::move(promise));
      }
    }
    return false;
  }

  LOG_IF(WARNING, !have_settings) << "Have no notification settings for " << settings_dialog_id
                                  << ", but forced to send notification about " << m->message_id << " in "
                                  << d->dialog_id;
  auto &group_info = get_notification_group_info(d, m);
  auto notification_group_id = get_dialog_notification_group_id(d->dialog_id, group_info);
  if (!notification_group_id.is_valid()) {
    return false;
  }
  // if !force, then add_message_to_dialog will add the correspondence
  m->notification_id = get_next_notification_id(d, notification_group_id, force ? m->message_id : MessageId());
  if (!m->notification_id.is_valid()) {
    return false;
  }
  bool is_changed = set_dialog_last_notification(d->dialog_id, group_info, m->date, m->notification_id,
                                                 "add_new_message_notification");
  CHECK(is_changed);
  if (is_pinned) {
    set_dialog_pinned_message_notification(d, from_mentions ? m->message_id : MessageId());
  }
  VLOG(notifications) << "Create " << m->notification_id << " with " << m->message_id << " in " << group_info.group_id
                      << '/' << d->dialog_id;
  send_closure_later(G()->notification_manager(), &NotificationManager::add_notification, notification_group_id,
                     from_mentions ? NotificationGroupType::Mentions : NotificationGroupType::Messages, d->dialog_id,
                     m->date, settings_dialog_id, m->disable_notification, m->notification_id,
                     create_new_message_notification(m->message_id));
  return true;
}

void MessagesManager::flush_pending_new_message_notifications(DialogId dialog_id, bool from_mentions,
                                                              DialogId settings_dialog_id) {
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  auto &pending_notifications =
      from_mentions ? d->pending_new_mention_notifications : d->pending_new_message_notifications;
  if (pending_notifications.empty()) {
    return;
  }
  for (auto &it : pending_notifications) {
    if (it.first == settings_dialog_id || !settings_dialog_id.is_valid()) {
      it.first = DialogId();
    }
  }

  VLOG(notifications) << "Flush pending notifications in " << dialog_id
                      << " because of received notification settings in " << settings_dialog_id;
  auto it = pending_notifications.begin();
  while (it != pending_notifications.end() && it->first == DialogId()) {
    auto m = get_message(d, it->second);
    if (m != nullptr) {
      if (add_new_message_notification(d, m, true)) {
        on_message_changed(d, m, false, "flush_pending_new_message_notifications");
      }
    }
    ++it;
  }

  if (it == pending_notifications.end()) {
    reset_to_empty(pending_notifications);
  } else {
    pending_notifications.erase(pending_notifications.begin(), it);
  }
}

void MessagesManager::remove_all_dialog_notifications(DialogId dialog_id, NotificationGroupInfo &group_info,
                                                      const char *source) {
  // removes up to group_info.last_notification_id
  if (group_info.group_id.is_valid() && group_info.last_notification_id.is_valid() &&
      group_info.max_removed_notification_id != group_info.last_notification_id) {
    VLOG(notifications) << "Set max_removed_notification_id in " << group_info.group_id << '/' << dialog_id << " to "
                        << group_info.last_notification_id << " from " << source;
    group_info.max_removed_notification_id = group_info.last_notification_id;
    send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification_group,
                       group_info.group_id, group_info.last_notification_id, MessageId(), 0, Promise<Unit>());
    bool is_changed = set_dialog_last_notification(dialog_id, group_info, 0, NotificationId(), source);
    CHECK(is_changed);
  }
}

void MessagesManager::remove_all_dialog_notifications(Dialog *d, MessageId max_message_id,
                                                      NotificationGroupInfo &group_info, const char *source) {
  // removes up to max_message_id
  if (group_info.group_id.is_valid()) {
    VLOG(notifications) << "Remove all dialog notifications in " << group_info.group_id << '/' << d->dialog_id
                        << " up to " << max_message_id << " from " << source;

    auto max_notification_message_id = max_message_id;
    if (d->last_message_id.is_valid() && max_notification_message_id.get() >= d->last_message_id.get()) {
      max_notification_message_id = d->last_message_id;
      set_dialog_last_notification(d->dialog_id, group_info, 0, NotificationId(), "remove_all_dialog_notifications 1");
    } else if (max_notification_message_id == MessageId::max()) {
      max_notification_message_id = get_next_local_message_id(d);
      set_dialog_last_notification(d->dialog_id, group_info, 0, NotificationId(), "remove_all_dialog_notifications 2");
    } else {
      LOG(FATAL) << "TODO support deleting up to " << max_message_id << " if ever will be needed";
    }

    send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification_group,
                       group_info.group_id, NotificationId(), max_notification_message_id, 0, Promise<Unit>());
  }
}

void MessagesManager::send_update_message_send_succeeded(Dialog *d, MessageId old_message_id, const Message *m) const {
  CHECK(m != nullptr);
  d->yet_unsent_message_id_to_persistent_message_id.emplace(old_message_id, m->message_id);
  send_closure(
      G()->td(), &Td::send_update,
      make_tl_object<td_api::updateMessageSendSucceeded>(get_message_object(d->dialog_id, m), old_message_id.get()));
}

void MessagesManager::send_update_message_content(DialogId dialog_id, MessageId message_id,
                                                  const MessageContent *content, int32 message_date,
                                                  bool is_content_secret, const char *source) const {
  LOG(INFO) << "Send updateMessageContent for " << message_id << " in " << dialog_id << " from " << source;
  CHECK(have_dialog(dialog_id)) << "Send updateMessageContent in unknown " << dialog_id << " from " << source
                                << " with load count " << loaded_dialogs_.count(dialog_id);
  send_closure(G()->td(), &Td::send_update,
               make_tl_object<td_api::updateMessageContent>(
                   dialog_id.get(), message_id.get(),
                   get_message_content_object(content, td_, message_date, is_content_secret)));
}

void MessagesManager::send_update_message_edited(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  cancel_user_dialog_action(dialog_id, m);
  send_closure(G()->td(), &Td::send_update,
               make_tl_object<td_api::updateMessageEdited>(dialog_id.get(), m->message_id.get(), m->edit_date,
                                                           get_reply_markup_object(m->reply_markup)));
}

void MessagesManager::send_update_delete_messages(DialogId dialog_id, vector<int64> &&message_ids, bool is_permanent,
                                                  bool from_cache) const {
  if (!message_ids.empty()) {
    CHECK(have_dialog(dialog_id)) << "Wrong " << dialog_id << " in send_update_delete_messages";
    send_closure(G()->td(), &Td::send_update,
                 make_tl_object<td_api::updateDeleteMessages>(dialog_id.get(), std::move(message_ids), is_permanent,
                                                              from_cache));
  }
}

void MessagesManager::send_update_new_chat(Dialog *d) {
  CHECK(d != nullptr);
  CHECK(d->messages == nullptr);
  send_closure(G()->td(), &Td::send_update, make_tl_object<td_api::updateNewChat>(get_chat_object(d)));
  d->is_update_new_chat_sent = true;
}

void MessagesManager::send_update_chat_draft_message(const Dialog *d) {
  CHECK(d != nullptr);
  CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_draft_message";
  on_dialog_updated(d->dialog_id, "send_update_chat_draft_message");
  send_closure(G()->td(), &Td::send_update,
               make_tl_object<td_api::updateChatDraftMessage>(
                   d->dialog_id.get(), get_draft_message_object(d->draft_message),
                   DialogDate(d->order, d->dialog_id) <= last_dialog_date_ ? d->order : 0));
}

void MessagesManager::send_update_chat_last_message(Dialog *d, const char *source) {
  update_dialog_pos(d, false, source, false);
  send_update_chat_last_message_impl(d, source);
}

void MessagesManager::send_update_chat_last_message_impl(const Dialog *d, const char *source) const {
  CHECK(d != nullptr);
  CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_last_message from " << source;
  LOG(INFO) << "Send updateChatLastMessage in " << d->dialog_id << " to " << d->last_message_id << " from " << source;
  auto update = make_tl_object<td_api::updateChatLastMessage>(
      d->dialog_id.get(), get_message_object(d->dialog_id, get_message(d, d->last_message_id)),
      DialogDate(d->order, d->dialog_id) <= last_dialog_date_ ? d->order : 0);
  send_closure(G()->td(), &Td::send_update, std::move(update));
}

void MessagesManager::send_update_unread_message_count(DialogId dialog_id, bool force, const char *source) {
  if (!td_->auth_manager_->is_bot() && G()->parameters().use_message_db) {
    CHECK(is_message_unread_count_inited_);
    if (unread_message_muted_count_ < 0 || unread_message_muted_count_ > unread_message_total_count_) {
      LOG(ERROR) << "Unread message count became invalid: " << unread_message_total_count_ << '/'
                 << unread_message_total_count_ - unread_message_muted_count_ << " from " << source << " and "
                 << dialog_id;
      if (unread_message_muted_count_ < 0) {
        unread_message_muted_count_ = 0;
      }
      if (unread_message_muted_count_ > unread_message_total_count_) {
        unread_message_total_count_ = unread_message_muted_count_;
      }
    }
    G()->td_db()->get_binlog_pmc()->set("unread_message_count",
                                        PSTRING() << unread_message_total_count_ << ' ' << unread_message_muted_count_);
    int32 unread_unmuted_count = unread_message_total_count_ - unread_message_muted_count_;
    if (!force && running_get_difference_) {
      LOG(INFO) << "Postpone updateUnreadMessageCount to " << unread_message_total_count_ << '/' << unread_unmuted_count
                << " from " << source << " and " << dialog_id;
      have_postponed_unread_message_count_update_ = true;
    } else {
      have_postponed_unread_message_count_update_ = false;
      LOG(INFO) << "Send updateUnreadMessageCount to " << unread_message_total_count_ << '/' << unread_unmuted_count
                << " from " << source << " and " << dialog_id;
      send_closure(G()->td(), &Td::send_update, get_update_unread_message_count_object());
    }
  }
}

void MessagesManager::send_update_unread_chat_count(DialogId dialog_id, bool force, const char *source) {
  if (!td_->auth_manager_->is_bot() && G()->parameters().use_message_db) {
    CHECK(is_dialog_unread_count_inited_);
    if (unread_dialog_muted_marked_count_ < 0 || unread_dialog_marked_count_ < unread_dialog_muted_marked_count_ ||
        unread_dialog_muted_count_ < unread_dialog_muted_marked_count_ ||
        unread_dialog_total_count_ + unread_dialog_muted_marked_count_ <
            unread_dialog_muted_count_ + unread_dialog_marked_count_) {
      LOG(ERROR) << "Unread chat count became invalid: " << unread_dialog_total_count_ << '/'
                 << unread_dialog_total_count_ - unread_dialog_muted_count_ << '/' << unread_dialog_marked_count_ << '/'
                 << unread_dialog_marked_count_ - unread_dialog_muted_marked_count_ << " from " << source << " and "
                 << dialog_id;
      if (unread_dialog_muted_marked_count_ < 0) {
        unread_dialog_muted_marked_count_ = 0;
      }
      if (unread_dialog_marked_count_ < unread_dialog_muted_marked_count_) {
        unread_dialog_marked_count_ = unread_dialog_muted_marked_count_;
      }
      if (unread_dialog_muted_count_ < unread_dialog_muted_marked_count_) {
        unread_dialog_muted_count_ = unread_dialog_muted_marked_count_;
      }
      if (unread_dialog_total_count_ + unread_dialog_muted_marked_count_ <
          unread_dialog_muted_count_ + unread_dialog_marked_count_) {
        unread_dialog_total_count_ =
            unread_dialog_muted_count_ + unread_dialog_marked_count_ - unread_dialog_muted_marked_count_;
      }
    }
    G()->td_db()->get_binlog_pmc()->set(
        "unread_dialog_count", PSTRING() << unread_dialog_total_count_ << ' ' << unread_dialog_muted_count_ << ' '
                                         << unread_dialog_marked_count_ << ' ' << unread_dialog_muted_marked_count_);
    bool need_postpone = !force && running_get_difference_;
    int32 unread_unmuted_count = unread_dialog_total_count_ - unread_dialog_muted_count_;
    int32 unread_unmuted_marked_count = unread_dialog_marked_count_ - unread_dialog_muted_marked_count_;
    LOG(INFO) << (need_postpone ? "Postpone" : "Send") << " updateUnreadChatCount to " << unread_dialog_total_count_
              << '/' << unread_unmuted_count << '/' << unread_dialog_marked_count_ << '/' << unread_unmuted_marked_count
              << " from " << source << " and " << dialog_id;
    if (need_postpone) {
      have_postponed_unread_chat_count_update_ = true;
    } else {
      have_postponed_unread_chat_count_update_ = false;
      send_closure(G()->td(), &Td::send_update, get_update_unread_chat_count_object());
    }
  }
}

void MessagesManager::send_update_chat_read_inbox(const Dialog *d, bool force, const char *source) {
  CHECK(d != nullptr);
  if (!td_->auth_manager_->is_bot()) {
    CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_read_inbox from " << source;
    on_dialog_updated(d->dialog_id, source);
    if (!force && (running_get_difference_ || running_get_channel_difference(d->dialog_id) ||
                   get_channel_difference_to_logevent_id_.count(d->dialog_id) != 0)) {
      LOG(INFO) << "Postpone updateChatReadInbox in " << d->dialog_id << "(" << get_dialog_title(d->dialog_id)
                << ") to " << d->server_unread_count << " + " << d->local_unread_count << " from " << source;
      postponed_chat_read_inbox_updates_.insert(d->dialog_id);
    } else {
      postponed_chat_read_inbox_updates_.erase(d->dialog_id);
      LOG(INFO) << "Send updateChatReadInbox in " << d->dialog_id << "(" << get_dialog_title(d->dialog_id) << ") to "
                << d->server_unread_count << " + " << d->local_unread_count << " from " << source;
      send_closure(G()->td(), &Td::send_update,
                   make_tl_object<td_api::updateChatReadInbox>(d->dialog_id.get(), d->last_read_inbox_message_id.get(),
                                                               d->server_unread_count + d->local_unread_count));
    }
  }
}

void MessagesManager::send_update_chat_read_outbox(const Dialog *d) {
  CHECK(d != nullptr);
  if (!td_->auth_manager_->is_bot()) {
    CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_read_outbox";
    on_dialog_updated(d->dialog_id, "send_update_chat_read_outbox");
    send_closure(
        G()->td(), &Td::send_update,
        make_tl_object<td_api::updateChatReadOutbox>(d->dialog_id.get(), d->last_read_outbox_message_id.get()));
  }
}

void MessagesManager::send_update_chat_unread_mention_count(const Dialog *d) {
  CHECK(d != nullptr);
  if (!td_->auth_manager_->is_bot()) {
    CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_unread_mention_count";
    LOG(INFO) << "Update unread mention message count in " << d->dialog_id << " to " << d->unread_mention_count;
    on_dialog_updated(d->dialog_id, "send_update_chat_unread_mention_count");
    send_closure(G()->td(), &Td::send_update,
                 make_tl_object<td_api::updateChatUnreadMentionCount>(d->dialog_id.get(), d->unread_mention_count));
  }
}

void MessagesManager::send_update_chat_is_sponsored(const Dialog *d) const {
  if (!td_->auth_manager_->is_bot()) {
    bool is_sponsored = d->order == SPONSORED_DIALOG_ORDER;
    LOG(INFO) << "Update chat is sponsored for " << d->dialog_id;
    auto order = DialogDate(d->order, d->dialog_id) <= last_dialog_date_ ? d->order : 0;
    send_closure(G()->td(), &Td::send_update,
                 make_tl_object<td_api::updateChatIsSponsored>(d->dialog_id.get(), is_sponsored, order));
  }
}

void MessagesManager::on_send_message_get_quick_ack(int64 random_id) {
  auto it = being_sent_messages_.find(random_id);
  if (it == being_sent_messages_.end()) {
    LOG(ERROR) << "Receive quick ack about unknown message with random_id = " << random_id;
    return;
  }

  auto dialog_id = it->second.get_dialog_id();
  auto message_id = it->second.get_message_id();

  send_closure(G()->td(), &Td::send_update,
               make_tl_object<td_api::updateMessageSendAcknowledged>(dialog_id.get(), message_id.get()));
}

void MessagesManager::check_send_message_result(int64 random_id, DialogId dialog_id,
                                                const telegram_api::Updates *updates_ptr, const char *source) {
  CHECK(updates_ptr != nullptr);
  CHECK(source != nullptr);
  auto sent_messages = td_->updates_manager_->get_new_messages(updates_ptr);
  auto sent_messages_random_ids = td_->updates_manager_->get_sent_messages_random_ids(updates_ptr);
  if (sent_messages.size() != 1u || sent_messages_random_ids.size() != 1u ||
      *sent_messages_random_ids.begin() != random_id || get_message_dialog_id(*sent_messages[0]) != dialog_id) {
    LOG(ERROR) << "Receive wrong result for sending message with random_id " << random_id << " from " << source
               << " to " << dialog_id << ": " << oneline(to_string(*updates_ptr));
    if (dialog_id.get_type() == DialogType::Channel) {
      Dialog *d = get_dialog(dialog_id);
      CHECK(d != nullptr);
      get_channel_difference(dialog_id, d->pts, true, "check_send_message_result");
    } else {
      td_->updates_manager_->schedule_get_difference("check_send_message_result");
    }
  }
}

FullMessageId MessagesManager::on_send_message_success(int64 random_id, MessageId new_message_id, int32 date,
                                                       FileId new_file_id, const char *source) {
  CHECK(source != nullptr);
  // do not try to run getDifference from this function
  if (DROP_UPDATES) {
    return {};
  }
  if (!new_message_id.is_valid()) {
    LOG(ERROR) << "Receive " << new_message_id << " as sent message from " << source;
    on_send_message_fail(random_id,
                         Status::Error(500, "Internal server error: receive invalid message id as sent message id"));
    return {};
  }
  if (new_message_id.is_yet_unsent()) {
    LOG(ERROR) << "Receive " << new_message_id << " as sent message from " << source;
    on_send_message_fail(random_id,
                         Status::Error(500, "Internal server error: receive yet unsent message as sent message"));
    return {};
  }

  auto it = being_sent_messages_.find(random_id);
  if (it == being_sent_messages_.end()) {
    LOG(ERROR) << "Result from sendMessage for " << new_message_id << " with random_id " << random_id << " sent at "
               << date << " comes from " << source << " after updateNewMessageId, but was not discarded by pts. "
               << td_->updates_manager_->get_state();
    if (debug_being_sent_messages_.count(random_id) == 0) {
      LOG(ERROR) << "Message with random_id " << random_id << " was mot sent";
      return {};
    }
    auto dialog_id = debug_being_sent_messages_[random_id];
    if (!dialog_id.is_valid()) {
      LOG(ERROR) << "Sent message is in invalid " << dialog_id;
      return {};
    }
    auto d = get_dialog(dialog_id);
    if (d == nullptr) {
      LOG(ERROR) << "Sent message is in not found " << dialog_id;
      return {};
    }
    dump_debug_message_op(d, 7);
    auto m = get_message_force(d, new_message_id);
    if (m == nullptr) {
      LOG(ERROR) << new_message_id << " in " << dialog_id << " not found";
      return {};
    }
    LOG(ERROR) << "Result from sent " << (m->is_outgoing ? "outgoing" : "incoming")
               << (m->forward_info == nullptr ? " not" : "") << " forwarded " << new_message_id
               << " with content of the type " << m->content->get_type() << " in " << dialog_id
               << " comes after updateNewMessageId, current last new is " << d->last_new_message_id << ", last is "
               << d->last_message_id << ". " << td_->updates_manager_->get_state();
    return {};
  }

  auto dialog_id = it->second.get_dialog_id();
  auto old_message_id = it->second.get_message_id();

  being_sent_messages_.erase(it);

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  bool need_update_dialog_pos = false;
  unique_ptr<Message> sent_message = delete_message(d, old_message_id, false, &need_update_dialog_pos, source);
  if (sent_message == nullptr) {
    // message has already been deleted by the user or sent to inaccessible channel
    // don't need to send update to the user, because the message has already been deleted
    LOG(INFO) << "Delete already deleted sent " << new_message_id << " from server";
    delete_messages_from_server(dialog_id, {new_message_id}, true, 0, Auto());
    return {};
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    // LOG(ERROR) << "Found " << old_message_id << " in inaccessible " << dialog_id;
    // dump_debug_message_op(d, 5);
  }

  // imitation of update_message(d, sent_message, std::move(new_message), true, &need_update_dialog_pos);
  if (date <= 0) {
    LOG(ERROR) << "Receive " << new_message_id << " in " << dialog_id << " with wrong date " << date;
  } else {
    CHECK(sent_message->date > 0) << old_message_id << ' ' << sent_message->message_id << ' ' << sent_message->date
                                  << ' ' << date;
    sent_message->date = date;
    CHECK(d->last_message_id != old_message_id);
  }

  // reply_to message may be already deleted
  // but can't use get_message for check, because the message can be already unloaded from the memory
  // if (get_message_force(d, sent_message->reply_to_message_id) == nullptr) {
  //   sent_message->reply_to_message_id = MessageId();
  // }

  if (merge_message_content_file_id(td_, sent_message->content.get(), new_file_id)) {
    send_update_message_content(dialog_id, old_message_id, sent_message->content.get(), sent_message->date,
                                sent_message->is_content_secret, source);
  }

  sent_message->message_id = new_message_id;
  sent_message->random_y = get_random_y(sent_message->message_id);

  sent_message->have_previous = true;
  sent_message->have_next = true;

  bool need_update = true;
  Message *m = add_message_to_dialog(d, std::move(sent_message), true, &need_update, &need_update_dialog_pos, source);
  CHECK(m != nullptr) << dialog_id << " " << have_input_peer(dialog_id, AccessRights::Read) << " "
                      << debug_add_message_to_dialog_fail_reason_ << " " << source;

  send_update_message_send_succeeded(d, old_message_id, m);
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "on_send_message_success");
  }
  try_add_active_live_location(dialog_id, m);
  return {dialog_id, new_message_id};
}

void MessagesManager::on_send_message_file_part_missing(int64 random_id, int bad_part) {
  auto it = being_sent_messages_.find(random_id);
  if (it == being_sent_messages_.end()) {
    // we can't receive fail more than once
    // but message can be successfully sent before
    LOG(WARNING) << "Receive FILE_PART_" << bad_part
                 << "_MISSING about successfully sent message with random_id = " << random_id;
    return;
  }

  auto full_message_id = it->second;

  being_sent_messages_.erase(it);

  Message *m = get_message(full_message_id);
  if (m == nullptr) {
    // message has already been deleted by the user or sent to inaccessible channel
    // don't need to send error to the user, because the message has already been deleted
    // and there is nothing to be deleted from the server
    LOG(INFO) << "Fail to send already deleted by the user or sent to inaccessible chat " << full_message_id;
    return;
  }

  auto dialog_id = full_message_id.get_dialog_id();
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    // LOG(ERROR) << "Found " << m->message_id << " in inaccessible " << dialog_id;
    // dump_debug_message_op(get_dialog(dialog_id), 5);
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (dialog_id.get_type() == DialogType::SecretChat) {
    // need to change message random_id before resending
    do {
      m->random_id = Random::secure_int64();
    } while (m->random_id == 0 || message_random_ids_.find(m->random_id) != message_random_ids_.end());
    message_random_ids_.insert(m->random_id);

    delete_random_id_to_message_id_correspondence(d, random_id, m->message_id);
    add_random_id_to_message_id_correspondence(d, m->random_id, m->message_id);

    auto logevent = SendMessageLogEvent(dialog_id, m);
    auto storer = LogEventStorerImpl<SendMessageLogEvent>(logevent);
    CHECK(m->send_message_logevent_id != 0);
    binlog_rewrite(G()->td_db()->get_binlog(), m->send_message_logevent_id, LogEvent::HandlerType::SendMessage, storer);
  }

  do_send_message(dialog_id, m, {bad_part});
}

void MessagesManager::on_send_message_fail(int64 random_id, Status error) {
  CHECK(error.is_error());

  auto it = being_sent_messages_.find(random_id);
  if (it == being_sent_messages_.end()) {
    // we can't receive fail more than once
    // but message can be successfully sent before
    if (error.code() != NetQuery::Cancelled) {
      auto debug_it = debug_being_sent_messages_.find(random_id);
      if (debug_it == debug_being_sent_messages_.end()) {
        LOG(ERROR) << "Message with random_id " << random_id << " was not sent";
        return;
      }
      auto dialog_id = debug_it->second;
      if (!dialog_id.is_valid()) {
        LOG(ERROR) << "Sent message is in invalid " << dialog_id;
        return;
      }
      if (!have_dialog(dialog_id)) {
        LOG(ERROR) << "Sent message is in not found " << dialog_id;
        return;
      }
      LOG(ERROR) << "Receive error " << error << " about successfully sent message with random_id = " << random_id
                 << " in " << dialog_id;
      dump_debug_message_op(get_dialog(dialog_id), 7);
    }
    return;
  }

  auto full_message_id = it->second;

  being_sent_messages_.erase(it);

  Message *m = get_message(full_message_id);
  if (m == nullptr) {
    // message has already been deleted by the user or sent to inaccessible channel
    // don't need to send error to the user, because the message has already been deleted
    // and there is nothing to be deleted from the server
    LOG(INFO) << "Fail to send already deleted by the user or sent to inaccessible chat " << full_message_id;
    return;
  }
  LOG_IF(ERROR, error.code() == NetQuery::Cancelled)
      << "Receive error " << error << " about sent message with random_id = " << random_id;

  auto dialog_id = full_message_id.get_dialog_id();
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    // LOG(ERROR) << "Found " << m->message_id << " in inaccessible " << dialog_id;
    // dump_debug_message_op(get_dialog(dialog_id), 5);
  }

  int error_code = error.code();
  string error_message = error.message().str();
  switch (error_code) {
    case 420:
      error_code = 429;
      LOG(ERROR) << "Receive error 420: " << error_message;
      break;
    case 429:
      // nothing special, error description has already been changed
      LOG_IF(ERROR, !begins_with(error_message, "Too Many Requests: retry after "))
          << "Wrong error message: " << error_message;
      break;
    case 400:
      if (error.message() == "MESSAGE_TOO_LONG") {
        error_message = "Message is too long";
        // TODO move check to send_message
      } else if (error.message() == "INPUT_USER_DEACTIVATED") {
        error_code = 403;
        error_message = "User is deactivated";
      } else if (error.message() == "USER_IS_BLOCKED") {
        error_code = 403;
        if (td_->auth_manager_->is_bot()) {
          switch (dialog_id.get_type()) {
            case DialogType::User:
              error_message = "Bot was blocked by the user";
              break;
            case DialogType::Chat:
            case DialogType::Channel:
              error_message = "Bot was kicked from the chat";
              break;
            case DialogType::SecretChat:
              break;
            case DialogType::None:
            default:
              UNREACHABLE();
          }
        } else {
          switch (dialog_id.get_type()) {
            case DialogType::User:
              error_message = "User was blocked by the other user";
              break;
            case DialogType::Chat:
            case DialogType::Channel:
              error_message = "User is not in the chat";
              break;
            case DialogType::SecretChat:
              break;
            case DialogType::None:
            default:
              UNREACHABLE();
          }
        }
        // TODO add check to send_message
      } else if (error.message() == "USER_IS_BOT") {
        if (td_->auth_manager_->is_bot() && dialog_id.get_type() == DialogType::User) {
          error_code = 403;
          if (td_->contacts_manager_->is_user_bot(dialog_id.get_user_id())) {
            error_message = "Bot can't send messages to bots";
          } else {
            error_message = "Bot can't send messages to the user";
          }
          // TODO move check to send_message
        }
      } else if (error.message() == "PEER_ID_INVALID") {
        error_code = 403;
        if (td_->auth_manager_->is_bot()) {
          error_message = "Bot can't initiate conversation with a user";
        }
      } 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_EMPTY") {
        auto content_type = m->content->get_type();
        if (content_type == MessageContentType::Game) {
          error_message = "Wrong game short name specified";
        } else if (content_type == MessageContentType::Invoice) {
          error_message = "Wrong invoice information specified";
        } else {
          error_message = "Wrong file identifier/HTTP URL specified";
        }
      } else if (error.message() == "PHOTO_EXT_INVALID") {
        error_message = "Photo has unsupported extension. Use one of .jpg, .jpeg, .gif, .png, .tif or .bmp";
      }
      break;
    case 403:
      if (error.message() == "MESSAGE_DELETE_FORBIDDEN") {
        error_code = 400;
        error_message = "Message can't be deleted";
      } else if (error.message() != "CHANNEL_PUBLIC_GROUP_NA" && error.message() != "USER_IS_BLOCKED" &&
                 error.message() != "USER_BOT_INVALID" && error.message() != "USER_DELETED") {
        error_code = 400;
      }
      break;
    // TODO other codes
    default:
      break;
  }
  if (error.message() == "REPLY_MARKUP_INVALID") {
    if (m->reply_markup == nullptr) {
      LOG(ERROR) << "Receive " << error.message() << " for " << oneline(to_string(get_message_object(dialog_id, m)));
    } else {
      LOG(ERROR) << "Receive " << error.message() << " for " << full_message_id << " with keyboard "
                 << *m->reply_markup;
    }
  }
  LOG_IF(WARNING, error_code != 403) << "Fail to send " << full_message_id << " with the error " << error;
  if (error_code <= 0) {
    error_code = 500;
  }
  fail_send_message(full_message_id, error_code, error_message);
}

MessageId MessagesManager::get_next_message_id(Dialog *d, int32 type) {
  CHECK(d != nullptr);
  int64 last =
      std::max({d->last_message_id.get(), d->last_new_message_id.get(), d->last_database_message_id.get(),
                d->last_assigned_message_id.get(), d->last_clear_history_message_id.get(),
                d->deleted_last_message_id.get(), d->max_unavailable_message_id.get(), d->max_added_message_id.get()});
  if (last < d->last_read_inbox_message_id.get() &&
      d->last_read_inbox_message_id.get() < d->last_new_message_id.get() + MessageId::FULL_TYPE_MASK) {
    last = d->last_read_inbox_message_id.get();
  }
  if (last < d->last_read_outbox_message_id.get() &&
      d->last_read_outbox_message_id.get() < d->last_new_message_id.get() + MessageId::FULL_TYPE_MASK) {
    last = d->last_read_outbox_message_id.get();
  }

  int64 base = (last + MessageId::TYPE_MASK + 1) & ~MessageId::TYPE_MASK;
  d->last_assigned_message_id = MessageId(base + type);
  CHECK(d->last_assigned_message_id.is_valid()) << d->last_assigned_message_id;
  return d->last_assigned_message_id;
}

MessageId MessagesManager::get_next_yet_unsent_message_id(Dialog *d) {
  return get_next_message_id(d, MessageId::TYPE_YET_UNSENT);
}

MessageId MessagesManager::get_next_local_message_id(Dialog *d) {
  return get_next_message_id(d, MessageId::TYPE_LOCAL);
}

void MessagesManager::fail_send_message(FullMessageId full_message_id, int error_code, const string &error_message) {
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  MessageId old_message_id = full_message_id.get_message_id();
  CHECK(old_message_id.is_valid());
  CHECK(old_message_id.is_yet_unsent());

  bool need_update_dialog_pos = false;
  unique_ptr<Message> message = delete_message(d, old_message_id, false, &need_update_dialog_pos, "fail send message");
  if (message == nullptr) {
    // message has already been deleted by the user or sent to inaccessible channel
    // don't need to send update to the user, because the message has already been deleted
    // and there is nothing to be deleted from the server
    return;
  }

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    // LOG(ERROR) << "Found " << old_message_id << " in inaccessible " << dialog_id;
    // dump_debug_message_op(d, 5);
  }

  auto new_message_id = MessageId(old_message_id.get() - MessageId::TYPE_YET_UNSENT + MessageId::TYPE_LOCAL);
  if (get_message_force(d, new_message_id) != nullptr || d->deleted_message_ids.count(new_message_id) ||
      new_message_id.get() <= d->last_clear_history_message_id.get()) {
    new_message_id = get_next_local_message_id(d);
  }

  message->message_id = new_message_id;
  CHECK(message->message_id.is_valid());
  message->random_y = get_random_y(message->message_id);
  message->is_failed_to_send = true;

  message->have_previous = true;
  message->have_next = true;

  bool need_update = false;
  Message *m = add_message_to_dialog(dialog_id, std::move(message), false, &need_update, &need_update_dialog_pos,
                                     "fail_send_message");
  CHECK(m != nullptr) << "Failed to add failed to send " << new_message_id << " to " << dialog_id << " due to "
                      << debug_add_message_to_dialog_fail_reason_;

  LOG(INFO) << "Send updateMessageSendFailed for " << full_message_id;
  d->yet_unsent_message_id_to_persistent_message_id.emplace(old_message_id, m->message_id);
  send_closure(G()->td(), &Td::send_update,
               make_tl_object<td_api::updateMessageSendFailed>(get_message_object(dialog_id, m), old_message_id.get(),
                                                               error_code, error_message));
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "fail_send_message");
  }
}

void MessagesManager::fail_send_message(FullMessageId full_message_id, Status error) {
  fail_send_message(full_message_id, error.code() > 0 ? error.code() : 500,
                    error.message().str());  // TODO CHECK that status has always a code
}

void MessagesManager::fail_edit_message_media(FullMessageId full_message_id, Status &&error) {
  auto dialog_id = full_message_id.get_dialog_id();
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  MessageId message_id = full_message_id.get_message_id();
  CHECK(message_id.is_server());

  auto m = get_message(d, message_id);
  if (m == nullptr) {
    // message has already been deleted by the user or sent to inaccessible channel
    return;
  }
  CHECK(m->edited_content != nullptr);
  m->edit_promise.set_error(std::move(error));
  cancel_edit_message_media(dialog_id, m);
}

void MessagesManager::on_update_dialog_draft_message(DialogId dialog_id,
                                                     tl_object_ptr<telegram_api::DraftMessage> &&draft_message) {
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive update chat draft in invalid " << dialog_id;
    return;
  }
  auto d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(INFO) << "Ignore update chat draft in unknown " << dialog_id;
    return;
  }
  update_dialog_draft_message(d, get_draft_message(td_->contacts_manager_.get(), std::move(draft_message)), true, true);
}

bool MessagesManager::update_dialog_draft_message(Dialog *d, unique_ptr<DraftMessage> &&draft_message, bool from_update,
                                                  bool need_update_dialog_pos) {
  CHECK(d != nullptr);
  if (from_update && d->is_opened && d->draft_message != nullptr) {
    // send the update anyway, despite it shouldn't be applied client-side
    // return false;
  }
  if (draft_message == nullptr) {
    if (d->draft_message != nullptr) {
      d->draft_message = nullptr;
      if (need_update_dialog_pos) {
        update_dialog_pos(d, false, "update_dialog_draft_message", false);
      }
      send_update_chat_draft_message(d);
      return true;
    }
  } else {
    if (d->draft_message != nullptr && d->draft_message->reply_to_message_id == draft_message->reply_to_message_id &&
        d->draft_message->input_message_text == draft_message->input_message_text) {
      if (d->draft_message->date < draft_message->date) {
        if (need_update_dialog_pos) {
          update_dialog_pos(d, false, "update_dialog_draft_message 2");
        }
        d->draft_message->date = draft_message->date;
        return true;
      }
    } else {
      if (!from_update || d->draft_message == nullptr || d->draft_message->date <= draft_message->date) {
        d->draft_message = std::move(draft_message);
        if (need_update_dialog_pos) {
          update_dialog_pos(d, false, "update_dialog_draft_message 3", false);
        }
        send_update_chat_draft_message(d);
        return true;
      }
    }
  }
  return false;
}

void MessagesManager::on_update_dialog_is_pinned(DialogId dialog_id, bool is_pinned) {
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive pin of invalid " << dialog_id;
    return;
  }

  auto d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(WARNING) << "Can't apply updateDialogPinned with " << dialog_id;
    // TODO logevent + promise
    send_closure(td_->create_net_actor<GetPinnedDialogsQuery>(Promise<>()), &GetPinnedDialogsQuery::send,
                 get_sequence_dispatcher_id(DialogId(), MessageContentType::None));
    return;
  }
  if (!is_pinned && d->pinned_order == DEFAULT_ORDER) {
    return;
  }
  set_dialog_is_pinned(d, is_pinned);
  update_dialog_pos(d, false, "on_update_dialog_is_pinned");
}

void MessagesManager::on_update_pinned_dialogs() {
  // TODO logevent + promise
  send_closure(td_->create_net_actor<GetPinnedDialogsQuery>(Promise<>()), &GetPinnedDialogsQuery::send,
               get_sequence_dispatcher_id(DialogId(), MessageContentType::None));
}

void MessagesManager::on_update_dialog_is_marked_as_unread(DialogId dialog_id, bool is_marked_as_unread) {
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive marking as unread of invalid " << dialog_id;
    return;
  }

  auto d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    // nothing to do
    return;
  }

  if (is_marked_as_unread == d->is_marked_as_unread) {
    return;
  }

  set_dialog_is_marked_as_unread(d, is_marked_as_unread);
}

void MessagesManager::set_dialog_is_marked_as_unread(Dialog *d, bool is_marked_as_unread) {
  CHECK(d != nullptr);
  CHECK(d->is_marked_as_unread != is_marked_as_unread);
  d->is_marked_as_unread = is_marked_as_unread;
  on_dialog_updated(d->dialog_id, "set_dialog_is_marked_as_unread");

  LOG(INFO) << "Set " << d->dialog_id << " is marked as unread to " << is_marked_as_unread;
  CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in set_dialog_is_marked_as_unread";
  send_closure(G()->td(), &Td::send_update,
               make_tl_object<td_api::updateChatIsMarkedAsUnread>(d->dialog_id.get(), is_marked_as_unread));

  if (d->server_unread_count + d->local_unread_count == 0 && need_unread_counter(d->order) &&
      is_dialog_unread_count_inited_) {
    int32 delta = d->is_marked_as_unread ? 1 : -1;
    unread_dialog_total_count_ += delta;
    unread_dialog_marked_count_ += delta;
    if (is_dialog_muted(d)) {
      unread_dialog_muted_count_ += delta;
      unread_dialog_muted_marked_count_ += delta;
    }
    send_update_unread_chat_count(d->dialog_id, true, "set_dialog_is_marked_as_unread");
  }
}

void MessagesManager::on_create_new_dialog_success(int64 random_id, tl_object_ptr<telegram_api::Updates> &&updates,
                                                   DialogType expected_type, Promise<Unit> &&promise) {
  auto sent_messages = td_->updates_manager_->get_new_messages(updates.get());
  auto sent_messages_random_ids = td_->updates_manager_->get_sent_messages_random_ids(updates.get());
  if (sent_messages.size() != 1u || sent_messages_random_ids.size() != 1u) {
    LOG(ERROR) << "Receive wrong result for create group or channel chat " << oneline(to_string(updates));
    return on_create_new_dialog_fail(random_id, Status::Error(500, "Unsupported server response"), std::move(promise));
  }

  auto message = *sent_messages.begin();
  // int64 message_random_id = *sent_messages_random_ids.begin();
  // TODO check that message_random_id equals random_id after messages_createChat will be updated

  auto dialog_id = get_message_dialog_id(*message);
  if (dialog_id.get_type() != expected_type) {
    return on_create_new_dialog_fail(random_id, Status::Error(500, "Chat of wrong type has been created"),
                                     std::move(promise));
  }

  auto it = created_dialogs_.find(random_id);
  CHECK(it != created_dialogs_.end());
  CHECK(it->second == DialogId());

  it->second = dialog_id;

  const Dialog *d = get_dialog(dialog_id);
  if (d != nullptr && d->last_new_message_id.is_valid()) {
    // dialog have been already created and at least one non-temporary message was added,
    // i.e. we are not interested in the creation of dialog by searchMessages
    // then messages have already been added, so just set promise
    return promise.set_value(Unit());
  }

  if (pending_created_dialogs_.find(dialog_id) == pending_created_dialogs_.end()) {
    pending_created_dialogs_.emplace(dialog_id, std::move(promise));
  } else {
    LOG(ERROR) << dialog_id << " returned twice as result of chat creation";
    return on_create_new_dialog_fail(random_id, Status::Error(500, "Chat was created earlier"), std::move(promise));
  }

  td_->updates_manager_->on_get_updates(std::move(updates));
}

void MessagesManager::on_create_new_dialog_fail(int64 random_id, Status error, Promise<Unit> &&promise) {
  LOG(INFO) << "Clean up creation of group or channel chat";
  auto it = created_dialogs_.find(random_id);
  CHECK(it != created_dialogs_.end());
  CHECK(it->second == DialogId());
  created_dialogs_.erase(it);

  CHECK(error.is_error());
  promise.set_error(std::move(error));

  // repairing state by running get difference
  td_->updates_manager_->get_difference("on_create_new_dialog_fail");
}

void MessagesManager::on_dialog_photo_updated(DialogId dialog_id) {
  auto d = get_dialog(dialog_id);
  if (d != nullptr && d->is_update_new_chat_sent) {
    send_closure(G()->td(), &Td::send_update,
                 make_tl_object<td_api::updateChatPhoto>(
                     dialog_id.get(), get_chat_photo_object(td_->file_manager_.get(), get_dialog_photo(dialog_id))));
  }
}

void MessagesManager::on_dialog_title_updated(DialogId dialog_id) {
  auto d = get_dialog(dialog_id);
  if (d != nullptr) {
    update_dialogs_hints(d);
    if (d->is_update_new_chat_sent) {
      send_closure(G()->td(), &Td::send_update,
                   make_tl_object<td_api::updateChatTitle>(dialog_id.get(), get_dialog_title(dialog_id)));
    }
  }
}

DialogId MessagesManager::resolve_dialog_username(const string &username) {
  auto it = resolved_usernames_.find(clean_username(username));
  if (it != resolved_usernames_.end()) {
    return it->second.dialog_id;
  }

  auto it2 = unaccessible_resolved_usernames_.find(clean_username(username));
  if (it2 != unaccessible_resolved_usernames_.end()) {
    return it2->second;
  }

  return DialogId();
}

DialogId MessagesManager::search_public_dialog(const string &username_to_search, bool force, Promise<Unit> &&promise) {
  string username = clean_username(username_to_search);
  if (username[0] == '@') {
    username = username.substr(1);
  }
  if (username.empty()) {
    promise.set_error(Status::Error(200, "Username is invalid"));
    return DialogId();
  }

  DialogId dialog_id;
  auto it = resolved_usernames_.find(username);
  if (it != resolved_usernames_.end()) {
    if (it->second.expires_at < Time::now()) {
      td_->create_handler<ResolveUsernameQuery>(Promise<>())->send(username);
    }
    dialog_id = it->second.dialog_id;
  } else {
    auto it2 = unaccessible_resolved_usernames_.find(username);
    if (it2 != unaccessible_resolved_usernames_.end()) {
      dialog_id = it2->second;
    }
  }

  if (dialog_id.is_valid()) {
    if (have_input_peer(dialog_id, AccessRights::Read)) {
      if (td_->auth_manager_->is_bot()) {
        force_create_dialog(dialog_id, "search public dialog");
      } else {
        const Dialog *d = get_dialog_force(dialog_id);
        if (!is_dialog_inited(d)) {
          send_get_dialog_query(dialog_id, std::move(promise));
          return DialogId();
        }
      }

      promise.set_value(Unit());
      return dialog_id;
    } else {
      // bot username maybe known despite there is no access_hash
      if (force || dialog_id.get_type() != DialogType::User) {
        force_create_dialog(dialog_id, "search public dialog");
        promise.set_value(Unit());
        return dialog_id;
      }
    }
  }

  td_->create_handler<ResolveUsernameQuery>(std::move(promise))->send(username);
  return DialogId();
}

void MessagesManager::send_get_dialog_notification_settings_query(DialogId dialog_id, Promise<Unit> &&promise) {
  if (td_->auth_manager_->is_bot() || dialog_id.get_type() == DialogType::SecretChat) {
    LOG(WARNING) << "Can't get notification settings for " << dialog_id;
    return promise.set_error(Status::Error(500, "Wrong getDialogNotificationSettings query"));
  }
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    LOG(WARNING) << "Have no access to " << dialog_id << " to get notification settings";
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  LOG(INFO) << "Send GetDialogNotifySettingsQuery for " << dialog_id;
  auto &promises = get_dialog_notification_settings_queries_[dialog_id];
  promises.push_back(std::move(promise));
  if (promises.size() != 1) {
    // query has already been sent, just wait for the result
    return;
  }

  td_->create_handler<GetDialogNotifySettingsQuery>()->send(dialog_id);
}

void MessagesManager::send_get_scope_notification_settings_query(NotificationSettingsScope scope,
                                                                 Promise<Unit> &&promise) {
  td_->create_handler<GetScopeNotifySettingsQuery>(std::move(promise))->send(scope);
}

void MessagesManager::on_get_dialog_notification_settings_query_finished(DialogId dialog_id, Status &&status) {
  auto it = get_dialog_notification_settings_queries_.find(dialog_id);
  CHECK(it != get_dialog_notification_settings_queries_.end());
  CHECK(!it->second.empty());
  auto promises = std::move(it->second);
  get_dialog_notification_settings_queries_.erase(it);

  for (auto &promise : promises) {
    if (status.is_ok()) {
      promise.set_value(Unit());
    } else {
      promise.set_error(status.clone());
    }
  }
}

class MessagesManager::GetDialogFromServerLogEvent {
 public:
  DialogId dialog_id_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(dialog_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(dialog_id_, parser);
  }
};

uint64 MessagesManager::save_get_dialog_from_server_logevent(DialogId dialog_id) {
  GetDialogFromServerLogEvent logevent{dialog_id};
  auto storer = LogEventStorerImpl<GetDialogFromServerLogEvent>(logevent);
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::GetDialogFromServer, storer);
}

void MessagesManager::send_get_dialog_query(DialogId dialog_id, Promise<Unit> &&promise, uint64 logevent_id) {
  if (td_->auth_manager_->is_bot() || dialog_id.get_type() == DialogType::SecretChat) {
    if (logevent_id != 0) {
      binlog_erase(G()->td_db()->get_binlog(), logevent_id);
    }
    return promise.set_error(Status::Error(500, "Wrong getDialog query"));
  }
  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    if (logevent_id != 0) {
      binlog_erase(G()->td_db()->get_binlog(), logevent_id);
    }
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  auto &promises = get_dialog_queries_[dialog_id];
  promises.push_back(std::move(promise));
  if (promises.size() != 1) {
    if (logevent_id != 0) {
      LOG(INFO) << "Duplicate getDialog query for " << dialog_id;
      binlog_erase(G()->td_db()->get_binlog(), logevent_id);
    }
    // query has already been sent, just wait for the result
    return;
  }

  if (logevent_id == 0 && G()->parameters().use_message_db) {
    logevent_id = save_get_dialog_from_server_logevent(dialog_id);
  }
  if (logevent_id != 0) {
    auto result = get_dialog_query_logevent_id_.emplace(dialog_id, logevent_id);
    CHECK(result.second);
  }

  LOG(INFO) << "Send get " << dialog_id << " query";
  td_->create_handler<GetDialogQuery>()->send(dialog_id);
}

void MessagesManager::on_get_dialog_query_finished(DialogId dialog_id, Status &&status) {
  auto it = get_dialog_queries_.find(dialog_id);
  CHECK(it != get_dialog_queries_.end());
  CHECK(!it->second.empty());
  auto promises = std::move(it->second);
  get_dialog_queries_.erase(it);

  auto logevent_it = get_dialog_query_logevent_id_.find(dialog_id);
  if (logevent_it != get_dialog_query_logevent_id_.end()) {
    binlog_erase(G()->td_db()->get_binlog(), logevent_it->second);
    get_dialog_query_logevent_id_.erase(logevent_it);
  }

  for (auto &promise : promises) {
    if (status.is_ok()) {
      promise.set_value(Unit());
    } else {
      promise.set_error(status.clone());
    }
  }
}

bool MessagesManager::is_update_about_username_change_received(DialogId dialog_id) const {
  switch (dialog_id.get_type()) {
    case DialogType::User:
      return td_->contacts_manager_->is_update_about_username_change_received(dialog_id.get_user_id());
    case DialogType::Chat:
      return true;
    case DialogType::Channel: {
      auto status = td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id());
      return status.is_member();
    }
    case DialogType::SecretChat:
      return true;
    case DialogType::None:
    default:
      UNREACHABLE();
      return false;
  }
}

void MessagesManager::on_dialog_username_updated(DialogId dialog_id, const string &old_username,
                                                 const string &new_username) {
  auto d = get_dialog(dialog_id);
  if (d != nullptr) {
    update_dialogs_hints(d);
  }
  if (!old_username.empty() && old_username != new_username) {
    resolved_usernames_.erase(clean_username(old_username));
    unaccessible_resolved_usernames_.erase(clean_username(old_username));
  }
  if (!new_username.empty()) {
    auto cache_time = is_update_about_username_change_received(dialog_id) ? USERNAME_CACHE_EXPIRE_TIME
                                                                          : USERNAME_CACHE_EXPIRE_TIME_SHORT;
    resolved_usernames_[clean_username(new_username)] = ResolvedUsername{dialog_id, Time::now() + cache_time};
  }
}

void MessagesManager::on_resolved_username(const string &username, DialogId dialog_id) {
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Resolve username \"" << username << "\" to invalid " << dialog_id;
    return;
  }

  auto it = resolved_usernames_.find(clean_username(username));
  if (it != resolved_usernames_.end()) {
    LOG_IF(ERROR, it->second.dialog_id != dialog_id)
        << "Resolve username \"" << username << "\" to " << dialog_id << ", but have it in " << it->second.dialog_id;
    return;
  }

  unaccessible_resolved_usernames_[clean_username(username)] = dialog_id;
}

void MessagesManager::drop_username(const string &username) {
  unaccessible_resolved_usernames_.erase(clean_username(username));

  auto it = resolved_usernames_.find(clean_username(username));
  if (it == resolved_usernames_.end()) {
    return;
  }

  auto dialog_id = it->second.dialog_id;
  if (have_input_peer(dialog_id, AccessRights::Read)) {
    CHECK(dialog_id.get_type() != DialogType::SecretChat);
    send_get_dialog_query(dialog_id, Auto());
  }

  resolved_usernames_.erase(it);
}

const DialogPhoto *MessagesManager::get_dialog_photo(DialogId dialog_id) const {
  switch (dialog_id.get_type()) {
    case DialogType::User:
      return td_->contacts_manager_->get_user_dialog_photo(dialog_id.get_user_id());
    case DialogType::Chat:
      return td_->contacts_manager_->get_chat_dialog_photo(dialog_id.get_chat_id());
    case DialogType::Channel:
      return td_->contacts_manager_->get_channel_dialog_photo(dialog_id.get_channel_id());
    case DialogType::SecretChat:
      return td_->contacts_manager_->get_secret_chat_dialog_photo(dialog_id.get_secret_chat_id());
    case DialogType::None:
    default:
      UNREACHABLE();
      return nullptr;
  }
}

string MessagesManager::get_dialog_title(DialogId dialog_id) const {
  switch (dialog_id.get_type()) {
    case DialogType::User:
      return td_->contacts_manager_->get_user_title(dialog_id.get_user_id());
    case DialogType::Chat:
      return td_->contacts_manager_->get_chat_title(dialog_id.get_chat_id());
    case DialogType::Channel:
      return td_->contacts_manager_->get_channel_title(dialog_id.get_channel_id());
    case DialogType::SecretChat:
      return td_->contacts_manager_->get_secret_chat_title(dialog_id.get_secret_chat_id());
    case DialogType::None:
    default:
      UNREACHABLE();
      return string();
  }
}

string MessagesManager::get_dialog_username(DialogId dialog_id) const {
  switch (dialog_id.get_type()) {
    case DialogType::User:
      return td_->contacts_manager_->get_user_username(dialog_id.get_user_id());
    case DialogType::Chat:
      return string();
    case DialogType::Channel:
      return td_->contacts_manager_->get_channel_username(dialog_id.get_channel_id());
    case DialogType::SecretChat:
      return td_->contacts_manager_->get_secret_chat_username(dialog_id.get_secret_chat_id());
    case DialogType::None:
    default:
      UNREACHABLE();
      return string();
  }
}

void MessagesManager::send_dialog_action(DialogId dialog_id, const tl_object_ptr<td_api::ChatAction> &action,
                                         Promise<Unit> &&promise) {
  if (action == nullptr) {
    return promise.set_error(Status::Error(5, "Action must not be empty"));
  }

  if (!have_dialog_force(dialog_id)) {
    return promise.set_error(Status::Error(5, "Chat not found"));
  }

  auto can_send_status = can_send_message(dialog_id);
  if (can_send_status.is_error()) {
    return promise.set_error(can_send_status.move_as_error());
  }

  if (dialog_id.get_type() == DialogType::SecretChat) {
    tl_object_ptr<secret_api::SendMessageAction> send_action;
    switch (action->get_id()) {
      case td_api::chatActionCancel::ID:
        send_action = make_tl_object<secret_api::sendMessageCancelAction>();
        break;
      case td_api::chatActionTyping::ID:
        send_action = make_tl_object<secret_api::sendMessageTypingAction>();
        break;
      case td_api::chatActionRecordingVideo::ID:
        send_action = make_tl_object<secret_api::sendMessageRecordVideoAction>();
        break;
      case td_api::chatActionUploadingVideo::ID:
        send_action = make_tl_object<secret_api::sendMessageUploadVideoAction>();
        break;
      case td_api::chatActionRecordingVoiceNote::ID:
        send_action = make_tl_object<secret_api::sendMessageRecordAudioAction>();
        break;
      case td_api::chatActionUploadingVoiceNote::ID:
        send_action = make_tl_object<secret_api::sendMessageUploadAudioAction>();
        break;
      case td_api::chatActionUploadingPhoto::ID:
        send_action = make_tl_object<secret_api::sendMessageUploadPhotoAction>();
        break;
      case td_api::chatActionUploadingDocument::ID:
        send_action = make_tl_object<secret_api::sendMessageUploadDocumentAction>();
        break;
      case td_api::chatActionChoosingLocation::ID:
        send_action = make_tl_object<secret_api::sendMessageGeoLocationAction>();
        break;
      case td_api::chatActionChoosingContact::ID:
        send_action = make_tl_object<secret_api::sendMessageChooseContactAction>();
        break;
      case td_api::chatActionRecordingVideoNote::ID:
        send_action = make_tl_object<secret_api::sendMessageRecordRoundAction>();
        break;
      case td_api::chatActionUploadingVideoNote::ID:
        send_action = make_tl_object<secret_api::sendMessageUploadRoundAction>();
        break;
      case td_api::chatActionStartPlayingGame::ID:
        return promise.set_error(Status::Error(5, "Games are unsupported in secret chats"));
      default:
        UNREACHABLE();
    }
    send_closure(G()->secret_chats_manager(), &SecretChatsManager::send_message_action, dialog_id.get_secret_chat_id(),
                 std::move(send_action));
    promise.set_value(Unit());
    return;
  }

  tl_object_ptr<telegram_api::SendMessageAction> send_action;
  switch (action->get_id()) {
    case td_api::chatActionCancel::ID:
      send_action = make_tl_object<telegram_api::sendMessageCancelAction>();
      break;
    case td_api::chatActionTyping::ID:
      send_action = make_tl_object<telegram_api::sendMessageTypingAction>();
      break;
    case td_api::chatActionRecordingVideo::ID:
      send_action = make_tl_object<telegram_api::sendMessageRecordVideoAction>();
      break;
    case td_api::chatActionUploadingVideo::ID: {
      auto progress = static_cast<const td_api::chatActionUploadingVideo &>(*action).progress_;
      send_action = make_tl_object<telegram_api::sendMessageUploadVideoAction>(progress);
      break;
    }
    case td_api::chatActionRecordingVoiceNote::ID:
      send_action = make_tl_object<telegram_api::sendMessageRecordAudioAction>();
      break;
    case td_api::chatActionUploadingVoiceNote::ID: {
      auto progress = static_cast<const td_api::chatActionUploadingVoiceNote &>(*action).progress_;
      send_action = make_tl_object<telegram_api::sendMessageUploadAudioAction>(progress);
      break;
    }
    case td_api::chatActionUploadingPhoto::ID: {
      auto progress = static_cast<const td_api::chatActionUploadingPhoto &>(*action).progress_;
      send_action = make_tl_object<telegram_api::sendMessageUploadPhotoAction>(progress);
      break;
    }
    case td_api::chatActionUploadingDocument::ID: {
      auto progress = static_cast<const td_api::chatActionUploadingDocument &>(*action).progress_;
      send_action = make_tl_object<telegram_api::sendMessageUploadDocumentAction>(progress);
      break;
    }
    case td_api::chatActionChoosingLocation::ID:
      send_action = make_tl_object<telegram_api::sendMessageGeoLocationAction>();
      break;
    case td_api::chatActionChoosingContact::ID:
      send_action = make_tl_object<telegram_api::sendMessageChooseContactAction>();
      break;
    case td_api::chatActionStartPlayingGame::ID:
      send_action = make_tl_object<telegram_api::sendMessageGamePlayAction>();
      break;
    case td_api::chatActionRecordingVideoNote::ID:
      send_action = make_tl_object<telegram_api::sendMessageRecordRoundAction>();
      break;
    case td_api::chatActionUploadingVideoNote::ID: {
      auto progress = static_cast<const td_api::chatActionUploadingVideoNote &>(*action).progress_;
      send_action = make_tl_object<telegram_api::sendMessageUploadRoundAction>(progress);
      break;
    }
    default:
      UNREACHABLE();
  }

  auto &query_ref = set_typing_query_[dialog_id];
  if (!query_ref.empty() && !td_->auth_manager_->is_bot()) {
    LOG(INFO) << "Cancel previous set typing query";
    cancel_query(query_ref);
  }
  query_ref = td_->create_handler<SetTypingQuery>(std::move(promise))->send(dialog_id, std::move(send_action));
}

void MessagesManager::on_send_dialog_action_timeout(DialogId dialog_id) {
  LOG(INFO) << "Receive send_dialog_action timeout in " << dialog_id;
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  if (can_send_message(dialog_id).is_error()) {
    return;
  }

  auto queue_id = get_sequence_dispatcher_id(dialog_id, MessageContentType::Photo);
  CHECK(queue_id & 1);

  auto queue_it = yet_unsent_media_queues_.find(queue_id);
  if (queue_it == yet_unsent_media_queues_.end()) {
    return;
  }

  pending_send_dialog_action_timeout_.add_timeout_in(dialog_id.get(), 4.0);

  CHECK(!queue_it->second.empty());
  MessageId message_id(queue_it->second.begin()->first);
  const Message *m = get_message(d, message_id);
  if (m == nullptr) {
    return;
  }
  if (m->forward_info != nullptr) {
    return;
  }

  auto file_id = get_message_content_file_id(m->content.get());
  if (!file_id.is_valid()) {
    LOG(ERROR) << "Have no file in "
               << to_string(get_message_content_object(m->content.get(), td_, m->date, m->is_content_secret));
    return;
  }
  auto file_view = td_->file_manager_->get_file_view(file_id);
  if (!file_view.is_uploading()) {
    return;
  }
  int64 total_size = file_view.expected_size();
  int64 uploaded_size = file_view.remote_size();
  int32 progress = 0;
  if (total_size > 0 && uploaded_size > 0) {
    if (uploaded_size > total_size) {
      uploaded_size = total_size;  // just in case
    }
    progress = static_cast<int32>(100 * uploaded_size / total_size);
  }

  td_api::object_ptr<td_api::ChatAction> action;
  switch (m->content->get_type()) {
    case MessageContentType::Animation:
    case MessageContentType::Audio:
    case MessageContentType::Document:
      action = td_api::make_object<td_api::chatActionUploadingDocument>(progress);
      break;
    case MessageContentType::Photo:
      action = td_api::make_object<td_api::chatActionUploadingPhoto>(progress);
      break;
    case MessageContentType::Video:
      action = td_api::make_object<td_api::chatActionUploadingVideo>(progress);
      break;
    case MessageContentType::VideoNote:
      action = td_api::make_object<td_api::chatActionUploadingVideoNote>(progress);
      break;
    case MessageContentType::VoiceNote:
      action = td_api::make_object<td_api::chatActionUploadingVoiceNote>(progress);
      break;
    default:
      return;
  }
  CHECK(action != nullptr);
  LOG(INFO) << "Send action in " << dialog_id << ": " << to_string(action);
  send_dialog_action(dialog_id, std::move(action), Auto());
}

void MessagesManager::on_active_dialog_action_timeout(DialogId dialog_id) {
  LOG(DEBUG) << "Receive active dialog action timeout in " << dialog_id;
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  auto actions_it = active_dialog_actions_.find(dialog_id);
  if (actions_it == active_dialog_actions_.end()) {
    return;
  }
  CHECK(!actions_it->second.empty());

  auto now = Time::now();
  while (actions_it->second[0].start_time + DIALOG_ACTION_TIMEOUT < now + 0.1) {
    on_user_dialog_action(dialog_id, actions_it->second[0].user_id, nullptr);

    actions_it = active_dialog_actions_.find(dialog_id);
    if (actions_it == active_dialog_actions_.end()) {
      return;
    }
    CHECK(!actions_it->second.empty());
  }

  LOG(DEBUG) << "Schedule next action timeout in " << dialog_id;
  active_dialog_action_timeout_.add_timeout_in(dialog_id.get(),
                                               actions_it->second[0].start_time + DIALOG_ACTION_TIMEOUT - now);
}

tl_object_ptr<telegram_api::InputChatPhoto> MessagesManager::get_input_chat_photo(FileId file_id) const {
  if (!file_id.is_valid()) {
    return make_tl_object<telegram_api::inputChatPhotoEmpty>();
  }

  FileView file_view = td_->file_manager_->get_file_view(file_id);
  CHECK(!file_view.is_encrypted());
  if (file_view.has_remote_location()) {
    if (file_view.remote_location().is_web()) {
      return nullptr;
    }
    return make_tl_object<telegram_api::inputChatPhoto>(file_view.remote_location().as_input_photo());
  }

  return nullptr;
}

tl_object_ptr<telegram_api::MessagesFilter> MessagesManager::get_input_messages_filter(SearchMessagesFilter filter) {
  switch (filter) {
    case SearchMessagesFilter::Empty:
      return make_tl_object<telegram_api::inputMessagesFilterEmpty>();
    case SearchMessagesFilter::Animation:
      return make_tl_object<telegram_api::inputMessagesFilterGif>();
    case SearchMessagesFilter::Audio:
      return make_tl_object<telegram_api::inputMessagesFilterMusic>();
    case SearchMessagesFilter::Document:
      return make_tl_object<telegram_api::inputMessagesFilterDocument>();
    case SearchMessagesFilter::Photo:
      return make_tl_object<telegram_api::inputMessagesFilterPhotos>();
    case SearchMessagesFilter::Video:
      return make_tl_object<telegram_api::inputMessagesFilterVideo>();
    case SearchMessagesFilter::VoiceNote:
      return make_tl_object<telegram_api::inputMessagesFilterVoice>();
    case SearchMessagesFilter::PhotoAndVideo:
      return make_tl_object<telegram_api::inputMessagesFilterPhotoVideo>();
    case SearchMessagesFilter::Url:
      return make_tl_object<telegram_api::inputMessagesFilterUrl>();
    case SearchMessagesFilter::ChatPhoto:
      return make_tl_object<telegram_api::inputMessagesFilterChatPhotos>();
    case SearchMessagesFilter::Call:
      return make_tl_object<telegram_api::inputMessagesFilterPhoneCalls>(0, false /*ignored*/);
    case SearchMessagesFilter::MissedCall:
      return make_tl_object<telegram_api::inputMessagesFilterPhoneCalls>(
          telegram_api::inputMessagesFilterPhoneCalls::MISSED_MASK, false /*ignored*/);
    case SearchMessagesFilter::VideoNote:
      return make_tl_object<telegram_api::inputMessagesFilterRoundVideo>();
    case SearchMessagesFilter::VoiceAndVideoNote:
      return make_tl_object<telegram_api::inputMessagesFilterRoundVoice>();
    case SearchMessagesFilter::Mention:
      return make_tl_object<telegram_api::inputMessagesFilterMyMentions>();
    default:
      UNREACHABLE();
      return nullptr;
  }
}

SearchMessagesFilter MessagesManager::get_search_messages_filter(
    const tl_object_ptr<td_api::SearchMessagesFilter> &filter) {
  if (filter == nullptr) {
    return SearchMessagesFilter::Empty;
  }
  switch (filter->get_id()) {
    case td_api::searchMessagesFilterEmpty::ID:
      return SearchMessagesFilter::Empty;
    case td_api::searchMessagesFilterAnimation::ID:
      return SearchMessagesFilter::Animation;
    case td_api::searchMessagesFilterAudio::ID:
      return SearchMessagesFilter::Audio;
    case td_api::searchMessagesFilterDocument::ID:
      return SearchMessagesFilter::Document;
    case td_api::searchMessagesFilterPhoto::ID:
      return SearchMessagesFilter::Photo;
    case td_api::searchMessagesFilterVideo::ID:
      return SearchMessagesFilter::Video;
    case td_api::searchMessagesFilterVoiceNote::ID:
      return SearchMessagesFilter::VoiceNote;
    case td_api::searchMessagesFilterPhotoAndVideo::ID:
      return SearchMessagesFilter::PhotoAndVideo;
    case td_api::searchMessagesFilterUrl::ID:
      return SearchMessagesFilter::Url;
    case td_api::searchMessagesFilterChatPhoto::ID:
      return SearchMessagesFilter::ChatPhoto;
    case td_api::searchMessagesFilterCall::ID:
      return SearchMessagesFilter::Call;
    case td_api::searchMessagesFilterMissedCall::ID:
      return SearchMessagesFilter::MissedCall;
    case td_api::searchMessagesFilterVideoNote::ID:
      return SearchMessagesFilter::VideoNote;
    case td_api::searchMessagesFilterVoiceAndVideoNote::ID:
      return SearchMessagesFilter::VoiceAndVideoNote;
    case td_api::searchMessagesFilterMention::ID:
      return SearchMessagesFilter::Mention;
    case td_api::searchMessagesFilterUnreadMention::ID:
      return SearchMessagesFilter::UnreadMention;
    default:
      UNREACHABLE();
      return SearchMessagesFilter::Empty;
  }
}

void MessagesManager::set_dialog_photo(DialogId dialog_id, const tl_object_ptr<td_api::InputFile> &photo,
                                       Promise<Unit> &&promise) {
  LOG(INFO) << "Receive SetChatPhoto request to change photo of " << dialog_id;

  if (!have_dialog_force(dialog_id)) {
    return promise.set_error(Status::Error(3, "Chat not found"));
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      return promise.set_error(Status::Error(3, "Can't change private chat photo"));
    case DialogType::Chat: {
      auto chat_id = dialog_id.get_chat_id();
      auto status = td_->contacts_manager_->get_chat_status(chat_id);
      if (!status.can_change_info_and_settings() ||
          (td_->auth_manager_->is_bot() && !td_->contacts_manager_->is_appointed_chat_administrator(chat_id))) {
        return promise.set_error(Status::Error(3, "Not enough rights to change chat photo"));
      }
      break;
    }
    case DialogType::Channel: {
      auto status = td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id());
      if (!status.can_change_info_and_settings()) {
        return promise.set_error(Status::Error(3, "Not enough rights to change chat photo"));
      }
      break;
    }
    case DialogType::SecretChat:
      return promise.set_error(Status::Error(3, "Can't change secret chat photo"));
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  auto r_file_id = td_->file_manager_->get_input_file_id(FileType::Photo, photo, dialog_id, true, false);
  if (r_file_id.is_error()) {
    return promise.set_error(Status::Error(7, r_file_id.error().message()));
  }
  FileId file_id = r_file_id.ok();

  auto input_chat_photo = get_input_chat_photo(file_id);
  if (input_chat_photo != nullptr) {
    // file has already been uploaded, just send change photo request
    // TODO invoke after
    td_->create_handler<EditDialogPhotoQuery>(std::move(promise))
        ->send(FileId(), dialog_id, std::move(input_chat_photo));
    return;
  }

  // need to upload file first
  auto upload_file_id = td_->file_manager_->dup_file_id(file_id);
  CHECK(upload_file_id.is_valid());
  CHECK(uploaded_dialog_photos_.find(upload_file_id) == uploaded_dialog_photos_.end());
  uploaded_dialog_photos_[upload_file_id] = {std::move(promise), dialog_id};
  LOG(INFO) << "Ask to upload chat photo " << upload_file_id;
  td_->file_manager_->upload(upload_file_id, upload_dialog_photo_callback_, 1, 0);
}

void MessagesManager::set_dialog_title(DialogId dialog_id, const string &title, Promise<Unit> &&promise) {
  LOG(INFO) << "Receive SetChatTitle request to change title of " << dialog_id << " to \"" << title << '"';

  if (!have_dialog_force(dialog_id)) {
    return promise.set_error(Status::Error(3, "Chat not found"));
  }

  auto new_title = clean_name(title, MAX_TITLE_LENGTH);
  if (new_title.empty()) {
    return promise.set_error(Status::Error(3, "Title can't be empty"));
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      return promise.set_error(Status::Error(3, "Can't change private chat title"));
    case DialogType::Chat: {
      auto chat_id = dialog_id.get_chat_id();
      auto status = td_->contacts_manager_->get_chat_status(chat_id);
      if (!status.can_change_info_and_settings() ||
          (td_->auth_manager_->is_bot() && !td_->contacts_manager_->is_appointed_chat_administrator(chat_id))) {
        return promise.set_error(Status::Error(3, "Not enough rights to change chat title"));
      }
      break;
    }
    case DialogType::Channel: {
      auto status = td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id());
      if (!status.can_change_info_and_settings()) {
        return promise.set_error(Status::Error(3, "Not enough rights to change chat title"));
      }
      break;
    }
    case DialogType::SecretChat:
      return promise.set_error(Status::Error(3, "Can't change secret chat title"));
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  // TODO this can be wrong if there was previous change title requests
  if (get_dialog_title(dialog_id) == new_title) {
    return promise.set_value(Unit());
  }

  // TODO invoke after
  td_->create_handler<EditDialogTitleQuery>(std::move(promise))->send(dialog_id, new_title);
}

void MessagesManager::add_dialog_participant(DialogId dialog_id, UserId user_id, int32 forward_limit,
                                             Promise<Unit> &&promise) {
  LOG(INFO) << "Receive AddChatParticipant request to add " << user_id << " to " << dialog_id;
  if (!have_dialog_force(dialog_id)) {
    return promise.set_error(Status::Error(3, "Chat not found"));
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      return promise.set_error(Status::Error(3, "Can't add members to a private chat"));
    case DialogType::Chat:
      return td_->contacts_manager_->add_chat_participant(dialog_id.get_chat_id(), user_id, forward_limit,
                                                          std::move(promise));
    case DialogType::Channel:
      return td_->contacts_manager_->add_channel_participant(dialog_id.get_channel_id(), user_id, std::move(promise));
    case DialogType::SecretChat:
      return promise.set_error(Status::Error(3, "Can't add members to a secret chat"));
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

void MessagesManager::add_dialog_participants(DialogId dialog_id, const vector<UserId> &user_ids,
                                              Promise<Unit> &&promise) {
  LOG(INFO) << "Receive AddChatParticipants request to add " << format::as_array(user_ids) << " to " << dialog_id;
  if (td_->auth_manager_->is_bot()) {
    return promise.set_error(Status::Error(3, "Method is not available for bots"));
  }

  if (!have_dialog_force(dialog_id)) {
    return promise.set_error(Status::Error(3, "Chat not found"));
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      return promise.set_error(Status::Error(3, "Can't add members to a private chat"));
    case DialogType::Chat:
      return promise.set_error(Status::Error(3, "Can't add many members at once to a basic group chat"));
    case DialogType::Channel:
      return td_->contacts_manager_->add_channel_participants(dialog_id.get_channel_id(), user_ids, std::move(promise));
    case DialogType::SecretChat:
      return promise.set_error(Status::Error(3, "Can't add members to a secret chat"));
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

void MessagesManager::set_dialog_participant_status(DialogId dialog_id, UserId user_id,
                                                    const tl_object_ptr<td_api::ChatMemberStatus> &chat_member_status,
                                                    Promise<Unit> &&promise) {
  auto status = get_dialog_participant_status(chat_member_status);
  LOG(INFO) << "Receive SetChatMemberStatus request with " << user_id << " and " << dialog_id << " to " << status;
  if (!have_dialog_force(dialog_id)) {
    return promise.set_error(Status::Error(3, "Chat not found"));
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      return promise.set_error(Status::Error(3, "Chat member status can't be changed in private chats"));
    case DialogType::Chat:
      return td_->contacts_manager_->change_chat_participant_status(dialog_id.get_chat_id(), user_id, status,
                                                                    std::move(promise));
    case DialogType::Channel:
      return td_->contacts_manager_->change_channel_participant_status(dialog_id.get_channel_id(), user_id, status,
                                                                       std::move(promise));
    case DialogType::SecretChat:
      return promise.set_error(Status::Error(3, "Chat member status can't be changed in secret chats"));
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

DialogParticipant MessagesManager::get_dialog_participant(DialogId dialog_id, UserId user_id, int64 &random_id,
                                                          bool force, Promise<Unit> &&promise) {
  LOG(INFO) << "Receive GetChatMember request to get " << user_id << " in " << dialog_id;
  if (!have_dialog_force(dialog_id)) {
    promise.set_error(Status::Error(3, "Chat not found"));
    return DialogParticipant();
  }

  switch (dialog_id.get_type()) {
    case DialogType::User: {
      auto peer_user_id = dialog_id.get_user_id();
      if (user_id == td_->contacts_manager_->get_my_id()) {
        promise.set_value(Unit());
        return {user_id, peer_user_id, 0, DialogParticipantStatus::Member()};
      }
      if (user_id == peer_user_id) {
        promise.set_value(Unit());
        return {peer_user_id, user_id, 0, DialogParticipantStatus::Member()};
      }

      promise.set_error(Status::Error(3, "User is not a member of the private chat"));
      break;
    }
    case DialogType::Chat:
      return td_->contacts_manager_->get_chat_participant(dialog_id.get_chat_id(), user_id, force, std::move(promise));
    case DialogType::Channel:
      return td_->contacts_manager_->get_channel_participant(dialog_id.get_channel_id(), user_id, random_id, force,
                                                             std::move(promise));
    case DialogType::SecretChat: {
      auto peer_user_id = td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id());
      if (user_id == td_->contacts_manager_->get_my_id()) {
        promise.set_value(Unit());
        return {user_id, peer_user_id, 0, DialogParticipantStatus::Member()};
      }
      if (user_id == peer_user_id) {
        promise.set_value(Unit());
        return {peer_user_id, user_id, 0, DialogParticipantStatus::Member()};
      }

      promise.set_error(Status::Error(3, "User is not a member of the secret chat"));
      break;
    }
    case DialogType::None:
    default:
      UNREACHABLE();
      promise.set_error(Status::Error(500, "Wrong chat type"));
  }
  return DialogParticipant();
}

std::pair<int32, vector<DialogParticipant>> MessagesManager::search_private_chat_participants(
    UserId my_user_id, UserId peer_user_id, const string &query, int32 limit, DialogParticipantsFilter filter) const {
  vector<UserId> user_ids;
  switch (filter) {
    case DialogParticipantsFilter::Administrators:
      break;
    case DialogParticipantsFilter::Members:
      user_ids.push_back(my_user_id);
      user_ids.push_back(peer_user_id);
      break;
    case DialogParticipantsFilter::Restricted:
      break;
    case DialogParticipantsFilter::Banned:
      break;
    case DialogParticipantsFilter::Bots:
      if (td_->auth_manager_->is_bot()) {
        user_ids.push_back(my_user_id);
      }
      if (td_->contacts_manager_->is_user_bot(peer_user_id)) {
        user_ids.push_back(peer_user_id);
      }
      break;
    default:
      UNREACHABLE();
  }

  auto result = td_->contacts_manager_->search_among_users(user_ids, query, limit);
  return {result.first, transform(result.second, [&](UserId user_id) {
            return DialogParticipant(user_id, user_id == my_user_id ? peer_user_id : my_user_id, 0,
                                     DialogParticipantStatus::Member());
          })};
}

std::pair<int32, vector<DialogParticipant>> MessagesManager::search_dialog_participants(
    DialogId dialog_id, const string &query, int32 limit, DialogParticipantsFilter filter, int64 &random_id, bool force,
    Promise<Unit> &&promise) {
  LOG(INFO) << "Receive SearchChatMembers request to search for " << query << " in " << dialog_id;
  if (!have_dialog_force(dialog_id)) {
    promise.set_error(Status::Error(3, "Chat not found"));
    return {};
  }
  if (limit < 0) {
    promise.set_error(Status::Error(3, "Parameter limit must be non-negative"));
    return {};
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      promise.set_value(Unit());
      return search_private_chat_participants(td_->contacts_manager_->get_my_id(), dialog_id.get_user_id(), query,
                                              limit, filter);
    case DialogType::Chat:
      return td_->contacts_manager_->search_chat_participants(dialog_id.get_chat_id(), query, limit, filter, force,
                                                              std::move(promise));
    case DialogType::Channel: {
      tl_object_ptr<td_api::SupergroupMembersFilter> request_filter;
      string additional_query;
      int32 additional_limit = 0;
      switch (filter) {
        case DialogParticipantsFilter::Administrators:
          request_filter = td_api::make_object<td_api::supergroupMembersFilterAdministrators>();
          additional_query = query;
          additional_limit = limit;
          limit = 100;
          break;
        case DialogParticipantsFilter::Members:
          request_filter = td_api::make_object<td_api::supergroupMembersFilterSearch>(query);
          break;
        case DialogParticipantsFilter::Restricted:
          request_filter = td_api::make_object<td_api::supergroupMembersFilterRestricted>(query);
          break;
        case DialogParticipantsFilter::Banned:
          request_filter = td_api::make_object<td_api::supergroupMembersFilterBanned>(query);
          break;
        case DialogParticipantsFilter::Bots:
          request_filter = td_api::make_object<td_api::supergroupMembersFilterBots>();
          additional_query = query;
          additional_limit = limit;
          limit = 100;
          break;
        default:
          UNREACHABLE();
      }

      return td_->contacts_manager_->get_channel_participants(dialog_id.get_channel_id(), request_filter,
                                                              additional_query, 0, limit, additional_limit, random_id,
                                                              force, std::move(promise));
    }
    case DialogType::SecretChat: {
      promise.set_value(Unit());
      auto peer_user_id = td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id());
      return search_private_chat_participants(td_->contacts_manager_->get_my_id(), peer_user_id, query, limit, filter);
    }
    case DialogType::None:
    default:
      UNREACHABLE();
      promise.set_error(Status::Error(500, "Wrong chat type"));
  }
  return {};
}

vector<UserId> MessagesManager::get_dialog_administrators(DialogId dialog_id, int left_tries, Promise<Unit> &&promise) {
  LOG(INFO) << "Receive GetChatAdministrators request in " << dialog_id;
  if (!have_dialog_force(dialog_id)) {
    promise.set_error(Status::Error(3, "Chat not found"));
    return {};
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::SecretChat:
      promise.set_value(Unit());
      break;
    case DialogType::Chat:
    case DialogType::Channel:
      return td_->contacts_manager_->get_dialog_administrators(dialog_id, left_tries, std::move(promise));
    case DialogType::None:
    default:
      UNREACHABLE();
      promise.set_error(Status::Error(500, "Wrong chat type"));
  }
  return {};
}

void MessagesManager::export_dialog_invite_link(DialogId dialog_id, Promise<Unit> &&promise) {
  LOG(INFO) << "Receive ExportDialogInviteLink request for " << dialog_id;
  if (!have_dialog_force(dialog_id)) {
    return promise.set_error(Status::Error(3, "Chat not found"));
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      return promise.set_error(Status::Error(3, "Can't invite members to a private chat"));
    case DialogType::Chat:
      return td_->contacts_manager_->export_chat_invite_link(dialog_id.get_chat_id(), std::move(promise));
    case DialogType::Channel:
      return td_->contacts_manager_->export_channel_invite_link(dialog_id.get_channel_id(), std::move(promise));
    case DialogType::SecretChat:
      return promise.set_error(Status::Error(3, "Can't invite members to a secret chat"));
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

string MessagesManager::get_dialog_invite_link(DialogId dialog_id) {
  switch (dialog_id.get_type()) {
    case DialogType::Chat:
      return td_->contacts_manager_->get_chat_invite_link(dialog_id.get_chat_id());
    case DialogType::Channel:
      return td_->contacts_manager_->get_channel_invite_link(dialog_id.get_channel_id());
    case DialogType::User:
    case DialogType::SecretChat:
    case DialogType::None:
      return string();
    default:
      UNREACHABLE();
      return string();
  }
}

tl_object_ptr<telegram_api::channelAdminLogEventsFilter> MessagesManager::get_channel_admin_log_events_filter(
    const tl_object_ptr<td_api::chatEventLogFilters> &filters) {
  if (filters == nullptr) {
    return nullptr;
  }

  int32 flags = 0;
  if (filters->message_edits_) {
    flags |= telegram_api::channelAdminLogEventsFilter::EDIT_MASK;
  }
  if (filters->message_deletions_) {
    flags |= telegram_api::channelAdminLogEventsFilter::DELETE_MASK;
  }
  if (filters->message_pins_) {
    flags |= telegram_api::channelAdminLogEventsFilter::PINNED_MASK;
  }
  if (filters->member_joins_) {
    flags |= telegram_api::channelAdminLogEventsFilter::JOIN_MASK;
  }
  if (filters->member_leaves_) {
    flags |= telegram_api::channelAdminLogEventsFilter::LEAVE_MASK;
  }
  if (filters->member_invites_) {
    flags |= telegram_api::channelAdminLogEventsFilter::INVITE_MASK;
  }
  if (filters->member_promotions_) {
    flags |= telegram_api::channelAdminLogEventsFilter::PROMOTE_MASK;
    flags |= telegram_api::channelAdminLogEventsFilter::DEMOTE_MASK;
  }
  if (filters->member_restrictions_) {
    flags |= telegram_api::channelAdminLogEventsFilter::BAN_MASK;
    flags |= telegram_api::channelAdminLogEventsFilter::UNBAN_MASK;
    flags |= telegram_api::channelAdminLogEventsFilter::KICK_MASK;
    flags |= telegram_api::channelAdminLogEventsFilter::UNKICK_MASK;
  }
  if (filters->info_changes_) {
    flags |= telegram_api::channelAdminLogEventsFilter::INFO_MASK;
  }
  if (filters->setting_changes_) {
    flags |= telegram_api::channelAdminLogEventsFilter::SETTINGS_MASK;
  }

  return make_tl_object<telegram_api::channelAdminLogEventsFilter>(
      flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/,
      false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/,
      false /*ignored*/, false /*ignored*/, false /*ignored*/);
}

int64 MessagesManager::get_dialog_event_log(DialogId dialog_id, const string &query, int64 from_event_id, int32 limit,
                                            const tl_object_ptr<td_api::chatEventLogFilters> &filters,
                                            const vector<UserId> &user_ids, Promise<Unit> &&promise) {
  if (td_->auth_manager_->is_bot()) {
    promise.set_error(Status::Error(3, "Method is not available for bots"));
    return 0;
  }

  if (!have_dialog_force(dialog_id)) {
    promise.set_error(Status::Error(3, "Chat not found"));
    return 0;
  }

  if (dialog_id.get_type() != DialogType::Channel) {
    promise.set_error(Status::Error(3, "Chat is not a supergroup chat"));
    return 0;
  }

  auto channel_id = dialog_id.get_channel_id();
  if (!td_->contacts_manager_->have_channel(channel_id)) {
    promise.set_error(Status::Error(3, "Chat info not found"));
    return 0;
  }

  if (!td_->contacts_manager_->get_channel_status(channel_id).is_administrator()) {
    promise.set_error(Status::Error(3, "Not enough rights to get event log"));
    return 0;
  }

  vector<tl_object_ptr<telegram_api::InputUser>> input_users;
  for (auto user_id : user_ids) {
    auto input_user = td_->contacts_manager_->get_input_user(user_id);
    if (input_user == nullptr) {
      promise.set_error(Status::Error(3, "User not found"));
      return 0;
    }
    input_users.push_back(std::move(input_user));
  }

  int64 random_id = 0;
  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || chat_events_.find(random_id) != chat_events_.end());
  chat_events_[random_id];  // reserve place for result

  td_->create_handler<GetChannelAdminLogQuery>(std::move(promise))
      ->send(channel_id, query, from_event_id, limit, get_channel_admin_log_events_filter(filters),
             std::move(input_users), random_id);

  return random_id;
}

tl_object_ptr<td_api::ChatEventAction> MessagesManager::get_chat_event_action_object(
    tl_object_ptr<telegram_api::ChannelAdminLogEventAction> &&action_ptr) {
  CHECK(action_ptr != nullptr);
  switch (action_ptr->get_id()) {
    case telegram_api::channelAdminLogEventActionParticipantJoin::ID:
      return make_tl_object<td_api::chatEventMemberJoined>();
    case telegram_api::channelAdminLogEventActionParticipantLeave::ID:
      return make_tl_object<td_api::chatEventMemberLeft>();
    case telegram_api::channelAdminLogEventActionParticipantInvite::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionParticipantInvite>(action_ptr);
      auto member = td_->contacts_manager_->get_dialog_participant(ChannelId(), std::move(action->participant_));
      return make_tl_object<td_api::chatEventMemberInvited>(
          td_->contacts_manager_->get_user_id_object(member.user_id, "chatEventMemberInvited"),
          member.status.get_chat_member_status_object());
    }
    case telegram_api::channelAdminLogEventActionParticipantToggleBan::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionParticipantToggleBan>(action_ptr);
      auto old_member =
          td_->contacts_manager_->get_dialog_participant(ChannelId(), std::move(action->prev_participant_));
      auto new_member =
          td_->contacts_manager_->get_dialog_participant(ChannelId(), std::move(action->new_participant_));
      if (old_member.user_id != new_member.user_id) {
        LOG(ERROR) << old_member.user_id << " VS " << new_member.user_id;
        return nullptr;
      }
      return make_tl_object<td_api::chatEventMemberRestricted>(
          td_->contacts_manager_->get_user_id_object(old_member.user_id, "chatEventMemberRestricted"),
          old_member.status.get_chat_member_status_object(), new_member.status.get_chat_member_status_object());
    }
    case telegram_api::channelAdminLogEventActionParticipantToggleAdmin::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionParticipantToggleAdmin>(action_ptr);
      auto old_member =
          td_->contacts_manager_->get_dialog_participant(ChannelId(), std::move(action->prev_participant_));
      auto new_member =
          td_->contacts_manager_->get_dialog_participant(ChannelId(), std::move(action->new_participant_));
      if (old_member.user_id != new_member.user_id) {
        LOG(ERROR) << old_member.user_id << " VS " << new_member.user_id;
        return nullptr;
      }
      return make_tl_object<td_api::chatEventMemberPromoted>(
          td_->contacts_manager_->get_user_id_object(old_member.user_id, "chatEventMemberPromoted"),
          old_member.status.get_chat_member_status_object(), new_member.status.get_chat_member_status_object());
    }
    case telegram_api::channelAdminLogEventActionChangeTitle::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionChangeTitle>(action_ptr);
      return make_tl_object<td_api::chatEventTitleChanged>(std::move(action->prev_value_),
                                                           std::move(action->new_value_));
    }
    case telegram_api::channelAdminLogEventActionChangeAbout::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionChangeAbout>(action_ptr);
      return make_tl_object<td_api::chatEventDescriptionChanged>(std::move(action->prev_value_),
                                                                 std::move(action->new_value_));
    }
    case telegram_api::channelAdminLogEventActionChangeUsername::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionChangeUsername>(action_ptr);
      return make_tl_object<td_api::chatEventUsernameChanged>(std::move(action->prev_value_),
                                                              std::move(action->new_value_));
    }
    case telegram_api::channelAdminLogEventActionChangePhoto::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionChangePhoto>(action_ptr);
      auto file_manager = td_->file_manager_.get();
      auto old_photo = td::get_dialog_photo(file_manager, std::move(action->prev_photo_));
      auto new_photo = td::get_dialog_photo(file_manager, std::move(action->new_photo_));
      return make_tl_object<td_api::chatEventPhotoChanged>(get_chat_photo_object(file_manager, &old_photo),
                                                           get_chat_photo_object(file_manager, &new_photo));
    }
    case telegram_api::channelAdminLogEventActionToggleInvites::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionToggleInvites>(action_ptr);
      return make_tl_object<td_api::chatEventInvitesToggled>(action->new_value_);
    }
    case telegram_api::channelAdminLogEventActionToggleSignatures::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionToggleSignatures>(action_ptr);
      return make_tl_object<td_api::chatEventSignMessagesToggled>(action->new_value_);
    }
    case telegram_api::channelAdminLogEventActionUpdatePinned::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionUpdatePinned>(action_ptr);
      auto message = create_message(
          parse_telegram_api_message(std::move(action->message_), "channelAdminLogEventActionUpdatePinned"), true);
      if (message.second == nullptr) {
        return make_tl_object<td_api::chatEventMessageUnpinned>();
      }
      return make_tl_object<td_api::chatEventMessagePinned>(get_message_object(message.first, message.second.get()));
    }
    case telegram_api::channelAdminLogEventActionEditMessage::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionEditMessage>(action_ptr);
      auto old_message = create_message(
          parse_telegram_api_message(std::move(action->prev_message_), "prev channelAdminLogEventActionEditMessage"),
          true);
      auto new_message = create_message(
          parse_telegram_api_message(std::move(action->new_message_), "new channelAdminLogEventActionEditMessage"),
          true);
      if (old_message.second == nullptr || new_message.second == nullptr || old_message.first != new_message.first) {
        LOG(ERROR) << "Failed to get edited message";
        return nullptr;
      }
      return make_tl_object<td_api::chatEventMessageEdited>(
          get_message_object(old_message.first, old_message.second.get()),
          get_message_object(new_message.first, new_message.second.get()));
    }
    case telegram_api::channelAdminLogEventActionDeleteMessage::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionDeleteMessage>(action_ptr);
      auto message = create_message(
          parse_telegram_api_message(std::move(action->message_), "channelAdminLogEventActionDeleteMessage"), true);
      if (message.second == nullptr) {
        LOG(ERROR) << "Failed to get deleted message";
        return nullptr;
      }
      return make_tl_object<td_api::chatEventMessageDeleted>(get_message_object(message.first, message.second.get()));
    }
    case telegram_api::channelAdminLogEventActionChangeStickerSet::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionChangeStickerSet>(action_ptr);
      auto old_sticker_set_id = add_sticker_set(td_, std::move(action->prev_stickerset_));
      auto new_sticker_set_id = add_sticker_set(td_, std::move(action->new_stickerset_));
      return make_tl_object<td_api::chatEventStickerSetChanged>(old_sticker_set_id, new_sticker_set_id);
    }
    case telegram_api::channelAdminLogEventActionTogglePreHistoryHidden::ID: {
      auto action = move_tl_object_as<telegram_api::channelAdminLogEventActionTogglePreHistoryHidden>(action_ptr);
      return make_tl_object<td_api::chatEventIsAllHistoryAvailableToggled>(!action->new_value_);
    }
    default:
      UNREACHABLE();
      return nullptr;
  }
}

void MessagesManager::on_get_event_log(int64 random_id,
                                       tl_object_ptr<telegram_api::channels_adminLogResults> &&events) {
  auto it = chat_events_.find(random_id);
  CHECK(it != chat_events_.end());
  auto &result = it->second;
  CHECK(result == nullptr);

  if (events == nullptr) {
    chat_events_.erase(it);
    return;
  }

  LOG(INFO) << "Receive " << to_string(events);

  td_->contacts_manager_->on_get_users(std::move(events->users_));
  td_->contacts_manager_->on_get_chats(std::move(events->chats_));

  result = make_tl_object<td_api::chatEvents>();
  result->events_.reserve(events->events_.size());
  for (auto &event : events->events_) {
    if (event->date_ <= 0) {
      LOG(ERROR) << "Receive wrong event date = " << event->date_;
      event->date_ = 0;
    }

    UserId user_id(event->user_id_);
    if (!user_id.is_valid()) {
      LOG(ERROR) << "Receive invalid " << user_id;
      continue;
    }
    LOG_IF(ERROR, !td_->contacts_manager_->have_user(user_id)) << "Have no info about " << user_id;

    auto action = get_chat_event_action_object(std::move(event->action_));
    if (action == nullptr) {
      continue;
    }
    result->events_.push_back(make_tl_object<td_api::chatEvent>(
        event->id_, event->date_, td_->contacts_manager_->get_user_id_object(user_id, "chatEvent"), std::move(action)));
  }
}

tl_object_ptr<td_api::chatEvents> MessagesManager::get_chat_events_object(int64 random_id) {
  auto it = chat_events_.find(random_id);
  CHECK(it != chat_events_.end());
  auto result = std::move(it->second);
  chat_events_.erase(it);
  return result;
}

unique_ptr<MessagesManager::Message> *MessagesManager::find_message(unique_ptr<Message> *v, MessageId message_id) {
  return const_cast<unique_ptr<Message> *>(find_message(static_cast<const unique_ptr<Message> *>(v), message_id));
}

const unique_ptr<MessagesManager::Message> *MessagesManager::find_message(const unique_ptr<Message> *v,
                                                                          MessageId message_id) {
  LOG(DEBUG) << "Searching for " << message_id << " in " << static_cast<const void *>(v->get());
  while (*v != nullptr) {
    //    LOG(DEBUG) << "Pass " << (*v)->message_id;
    if ((*v)->message_id.get() < message_id.get()) {
      //      LOG(DEBUG) << "Go right";
      v = &(*v)->right;
    } else if ((*v)->message_id.get() > message_id.get()) {
      //      LOG(DEBUG) << "Go left";
      v = &(*v)->left;
    } else {
      LOG(DEBUG) << "Message found";
      break;
    }
  }
  return v;
}

MessagesManager::Message *MessagesManager::insert_message(unique_ptr<Message> *v, unique_ptr<Message> message) {
  auto message_id = message->message_id;
  while (*v != nullptr && (*v)->random_y >= message->random_y) {
    if ((*v)->message_id.get() < message_id.get()) {
      v = &(*v)->right;
    } else if ((*v)->message_id == message_id) {
      UNREACHABLE();
    } else {
      v = &(*v)->left;
    }
  }

  unique_ptr<Message> *left = &message->left;
  unique_ptr<Message> *right = &message->right;

  unique_ptr<Message> cur = std::move(*v);
  while (cur != nullptr) {
    if (cur->message_id.get() < message_id.get()) {
      *left = std::move(cur);
      left = &((*left)->right);
      cur = std::move(*left);
    } else {
      *right = std::move(cur);
      right = &((*right)->left);
      cur = std::move(*right);
    }
  }
  CHECK(*left == nullptr);
  CHECK(*right == nullptr);
  *v = std::move(message);
  return v->get();
}

MessagesManager::Message *MessagesManager::get_message(Dialog *d, MessageId message_id) {
  return const_cast<Message *>(get_message(static_cast<const Dialog *>(d), message_id));
}

const MessagesManager::Message *MessagesManager::get_message(const Dialog *d, MessageId message_id) {
  if (!message_id.is_valid()) {
    return nullptr;
  }

  CHECK(d != nullptr);
  LOG(DEBUG) << "Search for " << message_id << " in " << d->dialog_id;
  auto result = find_message(&d->messages, message_id)->get();
  if (result != nullptr) {
    result->last_access_date = G()->unix_time_cached();
  }
  return result;
}

MessagesManager::Message *MessagesManager::get_message_force(Dialog *d, MessageId message_id) {
  if (!message_id.is_valid()) {
    return nullptr;
  }

  auto result = get_message(d, message_id);
  if (result != nullptr) {
    return result;
  }

  if (!G()->parameters().use_message_db || message_id.is_yet_unsent()) {
    return nullptr;
  }

  if (d->deleted_message_ids.count(message_id)) {
    return nullptr;
  }

  LOG(INFO) << "Trying to load " << FullMessageId{d->dialog_id, message_id} << " from database";
  auto r_value = G()->td_db()->get_messages_db_sync()->get_message({d->dialog_id, message_id});
  if (r_value.is_error()) {
    return nullptr;
  }
  return on_get_message_from_database(d->dialog_id, d, r_value.ok());
}

MessagesManager::Message *MessagesManager::on_get_message_from_database(DialogId dialog_id, Dialog *d,
                                                                        const BufferSlice &value) {
  if (value.empty()) {
    return nullptr;
  }

  auto m = parse_message(dialog_id, std::move(value));
  if (m == nullptr) {
    return nullptr;
  }

  if (d == nullptr) {
    LOG(ERROR) << "Can't find " << dialog_id << ", but have a message from it";
    if (!dialog_id.is_valid()) {
      LOG(ERROR) << "Got message in invalid " << dialog_id;
      return nullptr;
    }

    // can succeed in private chats
    get_messages_from_server({FullMessageId{dialog_id, m->message_id}}, Auto());

    force_create_dialog(dialog_id, "on_get_message_from_database");
    d = get_dialog_force(dialog_id);
    CHECK(d != nullptr);
  }

  if (!have_input_peer(d->dialog_id, AccessRights::Read)) {
    return nullptr;
  }

  auto old_message = get_message(d, m->message_id);
  if (old_message != nullptr) {
    CHECK(m->message_id == old_message->message_id);
    // data in the database is always outdated, so return a message from the memory
    if (dialog_id.get_type() == DialogType::SecretChat) {
      // just in case restore random_id to message_id corespondence
      // can be needed if there was newer unloaded message with the same random_id
      add_random_id_to_message_id_correspondence(d, old_message->random_id, m->message_id);
    }

    if (old_message->notification_id.is_valid()) {
      add_notification_id_to_message_id_correspondence(d, old_message->notification_id, m->message_id);
    }

    return old_message;
  }

  Dependencies dependencies;
  add_message_dependencies(dependencies, d->dialog_id, m.get());
  resolve_dependencies_force(dependencies);

  m->have_next = false;
  m->have_previous = false;
  m->from_database = true;
  bool need_update = false;
  bool need_update_dialog_pos = false;
  auto result = add_message_to_dialog(d, std::move(m), false, &need_update, &need_update_dialog_pos,
                                      "on_get_message_from_database");
  if (need_update_dialog_pos) {
    LOG(ERROR) << "Need update dialog pos after load " << (result == nullptr ? MessageId() : result->message_id)
               << " in " << d->dialog_id << " from database";
    send_update_chat_last_message(d, "on_get_message_from_database");
  }
  return result;
}

int32 MessagesManager::get_random_y(MessageId message_id) {
  return static_cast<int32>(static_cast<uint32>(message_id.get() * 2101234567u));
}

MessagesManager::Message *MessagesManager::add_message_to_dialog(DialogId dialog_id, unique_ptr<Message> message,
                                                                 bool from_update, bool *need_update,
                                                                 bool *need_update_dialog_pos, const char *source) {
  CHECK(message != nullptr);
  CHECK(dialog_id.get_type() != DialogType::None);
  CHECK(need_update_dialog_pos != nullptr);

  MessageId message_id = message->message_id;
  if (!message_id.is_valid()) {
    LOG(ERROR) << "Receive " << message_id << " in " << dialog_id << " from " << source;
    debug_add_message_to_dialog_fail_reason_ = "invalid message id";
    return nullptr;
  }

  // TODO remove creation of dialog from this function, use cgc or cpc or something else
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    d = add_dialog(dialog_id);
    *need_update_dialog_pos = true;
  } else {
    CHECK(d->dialog_id == dialog_id);
  }
  return add_message_to_dialog(d, std::move(message), from_update, need_update, need_update_dialog_pos, source);
}

MessagesManager::Message *MessagesManager::add_message_to_dialog(Dialog *d, unique_ptr<Message> message,
                                                                 bool from_update, bool *need_update,
                                                                 bool *need_update_dialog_pos, const char *source) {
  CHECK(message != nullptr);
  CHECK(d != nullptr);
  CHECK(need_update != nullptr);
  CHECK(need_update_dialog_pos != nullptr);
  CHECK(source != nullptr);
  debug_add_message_to_dialog_fail_reason_ = "success";

  DialogId dialog_id = d->dialog_id;
  MessageId message_id = message->message_id;

  if (d->deleted_message_ids.count(message_id)) {
    LOG(INFO) << "Skip adding deleted " << message_id << " to " << dialog_id << " from " << source;
    debug_add_message_to_dialog_fail_reason_ = "adding deleted message";
    return nullptr;
  }
  if (message_id.get() <= d->last_clear_history_message_id.get()) {
    LOG(INFO) << "Skip adding cleared " << message_id << " to " << dialog_id << " from " << source;
    debug_add_message_to_dialog_fail_reason_ = "cleared full history";
    return nullptr;
  }
  if (d->deleted_message_ids.count(message->reply_to_message_id)) {
    // LOG(INFO) << "Remove reply to deleted " << message->reply_to_message_id << " in " << message_id << " from " << dialog_id << " from " << source;
    // we don't want to lose information that the message was a reply for now
    // message->reply_to_message_id = MessageId();
  }

  LOG(INFO) << "Adding " << message_id << " of type " << message->content->get_type() << " to " << dialog_id << " from "
            << source << ". Last new is " << d->last_new_message_id << ", last is " << d->last_message_id
            << ", from_update = " << from_update << ", have_previous = " << message->have_previous
            << ", have_next = " << message->have_next;

  if (!message_id.is_valid()) {
    LOG(ERROR) << "Receive " << message_id << " in " << dialog_id << " from " << source;
    CHECK(!message->from_database);
    debug_add_message_to_dialog_fail_reason_ = "invalid message id";
    return nullptr;
  }

  auto message_content_type = message->content->get_type();
  if (is_debug_message_op_enabled()) {
    d->debug_message_op.emplace_back(Dialog::MessageOp::Add, message_id, message_content_type, from_update,
                                     message->have_previous, message->have_next, source);
  }

  message->last_access_date = G()->unix_time_cached();

  if (*need_update) {
    CHECK(from_update);
  }
  if (from_update) {
    CHECK(message->have_next);
    CHECK(message->have_previous);
    if (message_id.get() <= d->last_new_message_id.get() && dialog_id.get_type() != DialogType::Channel) {
      if (!G()->parameters().use_message_db) {
        if (td_->auth_manager_->is_bot() && Time::now() > start_time_ + 300 &&
            MessageId(ServerMessageId(100)).get() <= message_id.get() &&
            message_id.get() <= MessageId(ServerMessageId(1000)).get() &&
            d->last_new_message_id.get() >= MessageId(ServerMessageId(2147483000)).get()) {
          LOG(FATAL) << "Force restart because of message_id overflow in " << dialog_id << " from "
                     << d->last_new_message_id << " to " << message_id;
        }
        LOG(ERROR) << "New " << message_id << " in " << dialog_id << " from " << source
                   << " has id less than last_new_message_id = " << d->last_new_message_id;
        dump_debug_message_op(d);
      }
    }
  }

  if (!from_update && ((message_id.is_server() && d->last_new_message_id != MessageId() &&
                        message_id.get() > d->last_new_message_id.get()) ||
                       (message_id.is_local() && d->last_database_message_id != MessageId() &&
                        message_id.get() > d->last_database_message_id.get() && !message->is_failed_to_send))) {
    if (!message->from_database) {
      LOG(ERROR) << "Ignore " << message_id << " in " << dialog_id << " received not through update from " << source
                 << ". Last new is " << d->last_new_message_id << ", "
                 << to_string(get_message_object(dialog_id, message.get()));
      dump_debug_message_op(d, 3);
      if (dialog_id.get_type() == DialogType::Channel && have_input_peer(dialog_id, AccessRights::Read)) {
        channel_get_difference_retry_timeout_.add_timeout_in(dialog_id.get(), 0.001);
      }
    } else {
      LOG(INFO) << "Ignore " << message_id << " in " << dialog_id << " received not through update from " << source;
    }
    debug_add_message_to_dialog_fail_reason_ = "too new message not from update";
    return nullptr;
  }
  if ((message_id.is_server() || (message_id.is_local() && dialog_id.get_type() == DialogType::SecretChat)) &&
      message_id.get() <= d->max_unavailable_message_id.get()) {
    LOG(INFO) << "Can't add an unavailable " << message_id << " to " << dialog_id << " from " << source;
    if (message->from_database) {
      delete_message_from_database(d, message_id, message.get(), true);
    }
    debug_add_message_to_dialog_fail_reason_ = "ignore unavailable message";
    return nullptr;
  }

  auto web_page_id = get_message_content_web_page_id(message->content.get());
  if (web_page_id.is_valid() && !td_->web_pages_manager_->have_web_page(web_page_id)) {
    waiting_for_web_page_messages_.emplace(dialog_id, message_id);
    send_closure(G()->web_pages_manager(), &WebPagesManager::wait_for_pending_web_page, dialog_id, message_id,
                 web_page_id);
  }

  if (*need_update && message_id.get() <= d->last_new_message_id.get()) {
    *need_update = false;
  }

  bool auto_attach = message->have_previous && message->have_next &&
                     (from_update || message_id.is_local() || message_id.is_yet_unsent());
  if (message_content_type == MessageContentType::ChatDeleteHistory) {
    auto m = delete_message(d, message_id, true, need_update_dialog_pos, "message chat delete history");
    if (m != nullptr) {
      send_update_delete_messages(dialog_id, {m->message_id.get()}, true, false);
    }
    int32 last_message_date = 0;
    if (d->last_message_id != MessageId()) {
      auto last_message = get_message(d, d->last_message_id);
      CHECK(last_message != nullptr);
      last_message_date = last_message->date - 1;
    } else {
      last_message_date = d->last_clear_history_date;
    }
    if (message->date > last_message_date) {
      set_dialog_last_clear_history_date(d, message->date, message_id, "update_last_clear_history_date");
      *need_update_dialog_pos = true;
    }
    LOG(INFO) << "Process MessageChatDeleteHistory in " << message_id << " in " << dialog_id << " with date "
              << message->date << " from " << source;
    CHECK(!message->from_database);
    debug_add_message_to_dialog_fail_reason_ = "skip adding MessageChatDeleteHistory";
    return nullptr;
  }

  if (message->reply_markup != nullptr &&
      (message->reply_markup->type == ReplyMarkup::Type::RemoveKeyboard ||
       (message->reply_markup->type == ReplyMarkup::Type::ForceReply && !message->reply_markup->is_personal)) &&
      !td_->auth_manager_->is_bot()) {
    if (*need_update && message->reply_markup->is_personal) {  // if this keyboard is for us
      if (d->reply_markup_message_id != MessageId()) {
        const Message *old_message = get_message_force(d, d->reply_markup_message_id);
        if (old_message == nullptr || old_message->sender_user_id == message->sender_user_id) {
          set_dialog_reply_markup(d, MessageId());
        }
      }
    }
    message->had_reply_markup = message->reply_markup->is_personal;
    message->reply_markup = nullptr;
  }

  if (from_update) {
    cancel_user_dialog_action(dialog_id, message.get());
  }

  {
    unique_ptr<Message> *v = find_message(&d->messages, message_id);
    if (*v == nullptr && !message->from_database) {
      // load message from database before updating it
      if (G()->parameters().use_message_db && get_message_force(d, message_id) != nullptr) {
        v = find_message(&d->messages, message_id);
        CHECK(*v != nullptr);
      }
    }

    if (*v != nullptr) {
      LOG(INFO) << "Adding already existing " << message_id << " in " << dialog_id << " from " << source;
      if (*need_update) {
        *need_update = false;
        if (!G()->parameters().use_message_db) {
          LOG(ERROR) << "Receive again " << (message->is_outgoing ? "outgoing" : "incoming")
                     << (message->forward_info == nullptr ? " not" : "") << " forwarded " << message_id
                     << " with content of type " << message_content_type << " in " << dialog_id << " from " << source
                     << ", current last new is " << d->last_new_message_id << ", last is " << d->last_message_id << ". "
                     << td_->updates_manager_->get_state();
          dump_debug_message_op(d, 1);
        }
      }
      if (auto_attach) {
        CHECK(message->have_previous);
        CHECK(message->have_next);
        message->have_previous = false;
        message->have_next = false;
      }
      if (!message->from_database) {
        const int32 INDEX_MASK_MASK = ~search_messages_filter_index_mask(SearchMessagesFilter::UnreadMention);
        auto old_index_mask = get_message_index_mask(dialog_id, v->get()) & INDEX_MASK_MASK;
        bool was_deleted = delete_active_live_location(dialog_id, v->get());
        update_message(d, *v, std::move(message), true, need_update_dialog_pos);
        auto new_index_mask = get_message_index_mask(dialog_id, v->get()) & INDEX_MASK_MASK;
        if (was_deleted) {
          try_add_active_live_location(dialog_id, v->get());
        }
        if (old_index_mask != new_index_mask) {
          update_message_count_by_index(d, -1, old_index_mask & ~new_index_mask);
          update_message_count_by_index(d, +1, new_index_mask & ~old_index_mask);
        }
      }
      return v->get();
    }
  }

  if (d->new_secret_chat_notification_id.is_valid()) {
    remove_new_secret_chat_notification(d, true);
  }
  if (message->message_id.get() > d->max_added_message_id.get()) {
    d->max_added_message_id = message->message_id;
  }

  if (d->have_full_history && !message->from_database && !from_update && !message_id.is_local() &&
      !message_id.is_yet_unsent()) {
    LOG(ERROR) << "Have full history in " << dialog_id << ", but receive unknown " << message_id
               << " with content of type " << message_content_type << " from " << source << ". Last new is "
               << d->last_new_message_id << ", last is " << d->last_message_id << ", first database is "
               << d->first_database_message_id << ", last database is " << d->last_database_message_id
               << ", last read inbox is " << d->last_read_inbox_message_id << ", last read outbox is "
               << d->last_read_inbox_message_id << ", last read all mentions is "
               << d->last_read_all_mentions_message_id << ", max unavailable is " << d->max_unavailable_message_id
               << ", last assigned is " << d->last_assigned_message_id;
    d->have_full_history = false;
    on_dialog_updated(dialog_id, "drop have_full_history");
  }

  if (!d->is_opened && d->messages != nullptr && is_message_unload_enabled()) {
    LOG(INFO) << "Schedule unload of " << dialog_id;
    pending_unload_dialog_timeout_.add_timeout_in(dialog_id.get(), DIALOG_UNLOAD_DELAY);
  }

  if (message->ttl > 0 && message->ttl_expires_at != 0) {
    auto now = Time::now();
    if (message->ttl_expires_at <= now) {
      if (dialog_id.get_type() == DialogType::SecretChat) {
        LOG(INFO) << "Can't add " << message_id << " with expired TTL to " << dialog_id << " from " << source;
        delete_message_from_database(d, message_id, message.get(), true);
        debug_add_message_to_dialog_fail_reason_ = "delete expired by TTL message";
        return nullptr;
      } else {
        on_message_ttl_expired_impl(d, message.get());
        message_content_type = message->content->get_type();
        if (message->from_database) {
          add_message_to_database(d, message.get(), "add expired message to dialog");
        }
      }
    } else {
      ttl_register_message(dialog_id, message.get(), now);
    }
  }

  LOG(INFO) << "Adding not found " << message_id << " to " << dialog_id << " from " << source;
  if (d->is_empty) {
    d->is_empty = false;
    *need_update_dialog_pos = true;
  }

  if (dialog_id.get_type() == DialogType::Channel && !message->contains_unread_mention &&
      message->date <
          G()->unix_time_cached() - G()->shared_config().get_option_integer("channels_read_media_period",
                                                                            (G()->is_test_dc() ? 300 : 7 * 86400))) {
    update_opened_message_content(message->content.get());
  }

  if (message->contains_unread_mention && message_id.get() <= d->last_read_all_mentions_message_id.get()) {
    message->contains_unread_mention = false;
  }

  if (message_id.is_yet_unsent() && message->reply_to_message_id.is_valid() &&
      !message->reply_to_message_id.is_yet_unsent()) {
    replied_by_yet_unsent_messages_[FullMessageId{dialog_id, message->reply_to_message_id}]++;
  }

  if (G()->parameters().use_file_db && message_id.is_yet_unsent() && !message->via_bot_user_id.is_valid()) {
    auto queue_id = get_sequence_dispatcher_id(dialog_id, message_content_type);
    if (queue_id & 1) {
      LOG(INFO) << "Add " << message_id << " from " << source << " to queue " << queue_id;
      yet_unsent_media_queues_[queue_id][message_id.get()];  // reserve place for promise
      if (!td_->auth_manager_->is_bot() && !is_broadcast_channel(dialog_id)) {
        pending_send_dialog_action_timeout_.add_timeout_in(dialog_id.get(), 1.0);
      }
    }
  }

  if (!(d->have_full_history && auto_attach) && d->last_message_id.is_valid() &&
      d->last_message_id.get() < MessageId(ServerMessageId(1)).get() &&
      message_id.get() >= MessageId(ServerMessageId(1)).get()) {
    set_dialog_last_message_id(d, MessageId(), "add_message_to_dialog");

    set_dialog_first_database_message_id(d, MessageId(), "add_message_to_dialog");
    set_dialog_last_database_message_id(d, MessageId(), source);
    d->have_full_history = false;
    for (auto &first_message_id : d->first_database_message_id_by_index) {
      first_message_id = MessageId();
    }
    std::fill(d->message_count_by_index.begin(), d->message_count_by_index.end(), -1);
    d->local_unread_count = 0;  // read all local messages. They will not be reachable anymore

    on_dialog_updated(dialog_id, "add gap to dialog");

    send_update_chat_last_message(d, "add gap to dialog");
    *need_update_dialog_pos = false;
  }

  if (from_update && message_id.get() > d->last_new_message_id.get() && !message_id.is_yet_unsent()) {
    if (dialog_id.get_type() == DialogType::SecretChat || message_id.is_server()) {
      // can delete messages, therefore must be called before message attaching/adding
      set_dialog_last_new_message_id(d, message_id, "add_message_to_dialog");
    }
  }

  bool is_attached = false;
  if (auto_attach) {
    LOG(INFO) << "Trying to auto attach " << message_id;
    auto it = MessagesIterator(d, message_id);
    Message *previous_message = *it;
    if (previous_message != nullptr) {
      auto previous_message_id = previous_message->message_id;
      CHECK(previous_message_id.get() < message_id.get());
      if (previous_message->have_next ||
          (d->last_message_id.is_valid() && previous_message_id.get() >= d->last_message_id.get())) {
        if (message_id.is_server() && previous_message_id.is_server() && previous_message->have_next) {
          ++it;
          auto next_message = *it;
          if (next_message != nullptr) {
            if (next_message->message_id.is_server()) {
              LOG(ERROR) << "Attach " << message_id << " from " << source << " before " << next_message->message_id
                         << " and after " << previous_message_id << " in " << dialog_id;
              dump_debug_message_op(d);
            }
          } else {
            LOG(ERROR) << "Have_next is true, but there is no next message after " << previous_message_id << " from "
                       << source << " in " << dialog_id;
            dump_debug_message_op(d);
          }
        }

        LOG(INFO) << "Attach " << message_id << " to the previous " << previous_message_id;
        message->have_previous = true;
        message->have_next = previous_message->have_next;
        previous_message->have_next = true;
        is_attached = true;
      }
    }
    if (!is_attached && !message_id.is_yet_unsent()) {
      // message may be attached to the next message if there is no previous message
      Message *cur = d->messages.get();
      Message *next_message = nullptr;
      while (cur != nullptr) {
        if (cur->message_id.get() < message_id.get()) {
          cur = cur->right.get();
        } else {
          next_message = cur;
          cur = cur->left.get();
        }
      }
      if (next_message != nullptr) {
        CHECK(!next_message->have_previous);
        LOG(INFO) << "Attach " << message_id << " to the next " << next_message->message_id;
        LOG_IF(ERROR, from_update) << "Attach " << message_id << " from " << source << " to the next "
                                   << next_message->message_id << " in " << dialog_id;
        message->have_next = true;
        message->have_previous = next_message->have_previous;
        next_message->have_previous = true;
        is_attached = true;
      }
    }
    if (!is_attached) {
      LOG(INFO) << "Can't attach " << message_id;
      message->have_previous = false;
      message->have_next = false;
    }
  }

  if (*need_update) {
    if (message_content_type == MessageContentType::PinMessage &&
        !get_message_content_pinned_message_id(message->content.get()).is_valid()) {
      // treat message pin without pinned message as ordinary message
      message->contains_mention = false;
    }

    // notification must be added before updating unread_count to have correct total notification count
    // in get_message_notification_group_force
    add_new_message_notification(d, message.get(), false);
  }

  UserId my_user_id(td_->contacts_manager_->get_my_id());
  DialogId my_dialog_id(my_user_id);
  if (*need_update && message_id.get() > d->last_read_inbox_message_id.get() && !td_->auth_manager_->is_bot()) {
    if (!message->is_outgoing && dialog_id != my_dialog_id) {
      int32 server_unread_count = d->server_unread_count;
      int32 local_unread_count = d->local_unread_count;
      if (message_id.is_server()) {
        server_unread_count++;
      } else {
        local_unread_count++;
      }
      set_dialog_last_read_inbox_message_id(d, MessageId::min(), server_unread_count, local_unread_count, false,
                                            source);
    } else {
      // if outgoing message has id one greater than last_read_inbox_message_id than definitely there is no
      // unread incoming message before it
      if (message_id.is_server() && d->last_read_inbox_message_id.is_valid() &&
          d->last_read_inbox_message_id.is_server() &&
          message_id.get_server_message_id().get() == d->last_read_inbox_message_id.get_server_message_id().get() + 1) {
        read_history_inbox(dialog_id, message_id, 0, "add_message_to_dialog");
      }
    }
  }
  if (*need_update && message->contains_unread_mention) {
    d->unread_mention_count++;
    d->message_count_by_index[search_messages_filter_index(SearchMessagesFilter::UnreadMention)] =
        d->unread_mention_count;
    send_update_chat_unread_mention_count(d);
  }
  if (*need_update) {
    update_message_count_by_index(d, +1, message.get());
  }
  if (auto_attach && message_id.get() > d->last_message_id.get() && message_id.get() >= d->last_new_message_id.get()) {
    set_dialog_last_message_id(d, message_id, "add_message_to_dialog");
    *need_update_dialog_pos = true;
  }
  if (auto_attach && !message_id.is_yet_unsent() && message_id.get() >= d->last_new_message_id.get() &&
      (d->last_new_message_id.is_valid() ||
       (message_id.is_local() && d->last_message_id.is_valid() &&
        (message_id.get() >= d->last_message_id.get() ||
         (d->last_database_message_id.is_valid() && message_id.get() > d->last_database_message_id.get()))))) {
    CHECK(message_id.get() <= d->last_message_id.get());
    if (message_id.get() > d->last_database_message_id.get()) {
      set_dialog_last_database_message_id(d, message_id, "add_message_to_dialog");
      if (!d->first_database_message_id.is_valid()) {
        set_dialog_first_database_message_id(d, message_id, "add_message_to_dialog");
        try_restore_dialog_reply_markup(d, message.get());
      }
    }
  }
  if (*need_update && dialog_id.get_type() == DialogType::Channel &&
      message->date < G()->unix_time_cached() - 2 * 86400 && Slice(source) == Slice("updateNewChannelMessage")) {
    // if the message is pretty old, we might have missed the update that the message has already been read
    repair_channel_server_unread_count(d);
  }

  const Message *m = message.get();
  if (!m->from_database && !message_id.is_yet_unsent()) {
    add_message_to_database(d, m, "add_message_to_dialog");
  }

  if (!is_attached && !m->have_next && !m->have_previous) {
    MessagesIterator it(d, message_id);
    if (*it != nullptr && (*it)->have_next) {
      // need to drop a connection between messages
      auto previous_message = *it;
      ++it;
      auto next_message = *it;
      if (next_message != nullptr) {
        if (next_message->message_id.is_server() &&
            !(td_->auth_manager_->is_bot() && Slice(source) == Slice("get channel messages"))) {
          LOG(ERROR) << "Can't attach " << message_id << " from " << source << " before " << next_message->message_id
                     << " and after " << previous_message->message_id << " in " << dialog_id;
          dump_debug_message_op(d);
        }

        next_message->have_previous = false;
        previous_message->have_next = false;
      } else {
        LOG(ERROR) << "Have_next is true, but there is no next message after " << previous_message->message_id
                   << " from " << source << " in " << dialog_id;
        dump_debug_message_op(d);
      }
    }
  }

  if (!td_->auth_manager_->is_bot() && from_update && d->reply_markup_message_id != MessageId()) {
    auto deleted_user_id = get_message_content_deleted_user_id(m->content.get());
    if (deleted_user_id.is_valid() && td_->contacts_manager_->is_user_bot(deleted_user_id)) {
      const Message *old_message = get_message_force(d, d->reply_markup_message_id);
      if (old_message == nullptr || old_message->sender_user_id == deleted_user_id) {
        set_dialog_reply_markup(d, MessageId());
      }
    }
  }

  if (message_content_type == MessageContentType::ContactRegistered && !d->has_contact_registered_message) {
    d->has_contact_registered_message = true;
    on_dialog_updated(dialog_id, "update_has_contact_registered_message");
  }

  if (message_id.is_server() && dialog_id.get_type() != DialogType::SecretChat &&
      need_reget_message_content(m->content.get())) {
    FullMessageId full_message_id{dialog_id, message_id};
    LOG(INFO) << "Reget from server " << full_message_id;
    get_messages_from_server({full_message_id}, Auto());
  }

  if (from_update && message_id.is_server() && dialog_id.get_type() == DialogType::Channel) {
    int32 new_participant_count = get_message_content_new_participant_count(m->content.get());
    if (new_participant_count != 0) {
      td_->contacts_manager_->speculative_add_channel_participants(dialog_id.get_channel_id(), new_participant_count,
                                                                   m->sender_user_id == my_user_id);
    }
  }
  if (from_update && message_id.is_server()) {
    auto pinned_message_id = get_message_content_pinned_message_id(m->content.get());
    if (pinned_message_id.is_valid()) {
      switch (dialog_id.get_type()) {
        case DialogType::User:
        case DialogType::Chat:
          // nothing to do yet
          break;
        case DialogType::SecretChat:
          break;
        case DialogType::Channel:
          td_->contacts_manager_->on_update_channel_pinned_message(dialog_id.get_channel_id(), pinned_message_id);
          break;
        case DialogType::None:
        default:
          UNREACHABLE();
      }
    }
  }
  if (!td_->auth_manager_->is_bot() && from_update && m->forward_info == nullptr &&
      (m->is_outgoing || dialog_id == my_dialog_id)) {
    if (dialog_id.get_type() != DialogType::SecretChat && !message_id.is_local()) {
      on_sent_message_content(td_, m->content.get());
    }
    update_used_hashtags(dialog_id, m);
  }
  if (!td_->auth_manager_->is_bot() && from_update && message_id.is_server() &&
      (m->is_outgoing || dialog_id == my_dialog_id) && dialog_id.get_type() != DialogType::SecretChat) {
    if (m->via_bot_user_id.is_valid() && m->forward_info == nullptr) {
      // forwarded game messages can't be distinguished from sent via bot game messages, so increase rating anyway
      send_closure(G()->top_dialog_manager(), &TopDialogManager::on_dialog_used, TopDialogCategory::BotInline,
                   DialogId(m->via_bot_user_id), m->date);
    }

    TopDialogCategory category = TopDialogCategory::Size;
    switch (dialog_id.get_type()) {
      case DialogType::User: {
        if (td_->contacts_manager_->is_user_bot(dialog_id.get_user_id())) {
          category = TopDialogCategory::BotPM;
        } else {
          category = TopDialogCategory::Correspondent;
        }
        break;
      }
      case DialogType::Chat:
        category = TopDialogCategory::Group;
        break;
      case DialogType::Channel:
        switch (td_->contacts_manager_->get_channel_type(dialog_id.get_channel_id())) {
          case ChannelType::Broadcast:
            category = TopDialogCategory::Channel;
            break;
          case ChannelType::Megagroup:
            category = TopDialogCategory::Group;
            break;
          case ChannelType::Unknown:
            break;
          default:
            UNREACHABLE();
            break;
        }
        break;
      case DialogType::SecretChat:
      case DialogType::None:
      default:
        UNREACHABLE();
    }
    if (category != TopDialogCategory::Size) {
      send_closure(G()->top_dialog_manager(), &TopDialogManager::on_dialog_used, category, dialog_id, m->date);
    }
  }

  Message *result_message = insert_message(&d->messages, std::move(message));
  CHECK(result_message != nullptr);
  CHECK(d->messages != nullptr);

  if (!is_attached) {
    if (m->have_next) {
      CHECK(!m->have_previous);
      attach_message_to_next(d, message_id, source);
    } else if (m->have_previous) {
      attach_message_to_previous(d, message_id, source);
    }
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
      message_id_to_dialog_id_[message_id] = dialog_id;
      break;
    case DialogType::Channel:
      // nothing to do
      break;
    case DialogType::SecretChat:
      add_random_id_to_message_id_correspondence(d, m->random_id, message_id);
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  if (m->notification_id.is_valid()) {
    add_notification_id_to_message_id_correspondence(d, m->notification_id, message_id);
  }

  return result_message;
}

void MessagesManager::on_message_changed(const Dialog *d, const Message *m, bool need_send_update, const char *source) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  if (need_send_update && m->message_id == d->last_message_id) {
    send_update_chat_last_message_impl(d, source);
  }

  if (m->message_id == d->last_database_message_id) {
    on_dialog_updated(d->dialog_id, source);
  }

  if (!m->message_id.is_yet_unsent()) {
    add_message_to_database(d, m, source);
  }
}

void MessagesManager::add_message_to_database(const Dialog *d, const Message *m, const char *source) {
  if (!G()->parameters().use_message_db) {
    return;
  }

  CHECK(d != nullptr);
  CHECK(m != nullptr);
  MessageId message_id = m->message_id;
  CHECK(message_id.is_server() || message_id.is_local()) << source;

  LOG(INFO) << "Add " << FullMessageId(d->dialog_id, message_id) << " to database from " << source;

  ServerMessageId unique_message_id;
  int64 random_id = 0;
  int64 search_id = 0;
  string text;
  switch (d->dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
      if (message_id.is_server()) {
        unique_message_id = message_id.get_server_message_id();
      }
      //FOR DEBUG
      //text = get_search_text(m);
      //if (!text.empty()) {
      //search_id = (static_cast<int64>(m->date) << 32) | static_cast<uint32>(Random::secure_int32());
      //}
      break;
    case DialogType::Channel:
      break;
    case DialogType::SecretChat:
      random_id = m->random_id;
      text = get_search_text(m);
      if (!text.empty()) {
        search_id = (static_cast<int64>(m->date) << 32) | static_cast<uint32>(m->random_id);
      }
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  int32 ttl_expires_at = 0;
  if (m->ttl_expires_at != 0) {
    ttl_expires_at = static_cast<int32>(m->ttl_expires_at - Time::now() + G()->server_time());
  }
  G()->td_db()->get_messages_db_async()->add_message({d->dialog_id, message_id}, unique_message_id, m->sender_user_id,
                                                     random_id, ttl_expires_at, get_message_index_mask(d->dialog_id, m),
                                                     search_id, text, m->notification_id, log_event_store(*m),
                                                     Auto());  // TODO Promise
}

void MessagesManager::delete_all_dialog_messages_from_database(Dialog *d, MessageId max_message_id,
                                                               const char *source) {
  CHECK(d != nullptr);
  remove_all_dialog_notifications(d, max_message_id, d->message_notification_group, source);
  remove_all_dialog_notifications(d, max_message_id, d->mention_notification_group, source);

  if (!G()->parameters().use_message_db) {
    return;
  }

  auto dialog_id = d->dialog_id;
  if (!max_message_id.is_valid()) {
    return;
  }

  LOG(INFO) << "Delete all messages in " << dialog_id << " from database up to " << max_message_id << " from "
            << source;
  /*
  if (dialog_id.get_type() == DialogType::User && max_message_id.is_server()) {
    bool need_save = false;
    int i = 0;
    for (auto &first_message_id : calls_db_state_.first_calls_database_message_id_by_index) {
      if (first_message_id.get() <= max_message_id.get()) {
        first_message_id = max_message_id.get_next_server_message_id();
        calls_db_state_.message_count_by_index[i] = -1;
        need_save = true;
      }
      i++;
    }
    if (need_save) {
      save_calls_db_state();
    }
  }
  */
  G()->td_db()->get_messages_db_async()->delete_all_dialog_messages(dialog_id, max_message_id, Auto());  // TODO Promise
}

class MessagesManager::DeleteMessageLogEvent {
 public:
  LogEvent::Id id_{0};
  FullMessageId full_message_id_;
  std::vector<FileId> file_ids_;

  template <class StorerT>
  void store(StorerT &storer) const {
    bool has_file_ids = !file_ids_.empty();
    BEGIN_STORE_FLAGS();
    STORE_FLAG(has_file_ids);
    END_STORE_FLAGS();

    td::store(full_message_id_, storer);
    if (has_file_ids) {
      td::store(file_ids_, storer);
    }
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    bool has_file_ids;
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(has_file_ids);
    END_PARSE_FLAGS();

    td::parse(full_message_id_, parser);
    if (has_file_ids) {
      td::parse(file_ids_, parser);
    }
  }
};

void MessagesManager::delete_message_files(const Message *m) const {
  for (auto file_id : get_message_file_ids(m)) {
    send_closure(G()->file_manager(), &FileManager::delete_file, file_id, Promise<>(),
                 "delete_message_files");  // TODO add log event
  }
}

void MessagesManager::delete_message_from_database(Dialog *d, MessageId message_id, const Message *m,
                                                   bool is_permanently_deleted) {
  if (!message_id.is_valid()) {
    return;
  }

  if (is_permanently_deleted) {
    d->deleted_message_ids.insert(message_id);
  }

  if (message_id.is_yet_unsent()) {
    return;
  }

  if (m != nullptr && m->notification_id.is_valid()) {
    auto from_mentions = is_from_mention_notification_group(d, m);
    auto &group_info = from_mentions ? d->mention_notification_group : d->message_notification_group;

    if (group_info.group_id.is_valid()) {
      if (group_info.last_notification_id == m->notification_id) {
        // last notification is deleted, need to find new last notification
        fix_dialog_last_notification_id(d, from_mentions, m->message_id);
      }
      send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification, group_info.group_id,
                         m->notification_id, true, Promise<Unit>());
    }
  }

  auto is_secret = d->dialog_id.get_type() == DialogType::SecretChat;
  if (m != nullptr && m->ttl > 0) {
    delete_message_files(m);
  }

  if (!G()->parameters().use_message_db) {
    // TODO message files should be deleted anyway
    return;
  }

  DeleteMessageLogEvent logevent;

  logevent.full_message_id_ = {d->dialog_id, message_id};

  if (is_secret && m != nullptr) {
    logevent.file_ids_ = get_message_file_ids(m);
  }

  do_delete_message_logevent(logevent);
}

void MessagesManager::do_delete_message_logevent(const DeleteMessageLogEvent &logevent) const {
  CHECK(G()->parameters().use_message_db);

  Promise<Unit> db_promise;
  if (!logevent.file_ids_.empty()) {
    auto logevent_id = logevent.id_;
    if (logevent_id == 0) {
      auto storer = LogEventStorerImpl<DeleteMessageLogEvent>(logevent);
      logevent_id = binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteMessage, storer);
    }

    MultiPromiseActorSafe mpas{"DeleteMessageMultiPromiseActor"};
    mpas.add_promise(PromiseCreator::lambda([logevent_id](Result<Unit> result) {
      if (result.is_error()) {
        return;
      }
      binlog_erase(G()->td_db()->get_binlog(), logevent_id);
    }));

    auto lock = mpas.get_promise();
    for (auto file_id : logevent.file_ids_) {
      send_closure(G()->file_manager(), &FileManager::delete_file, file_id, mpas.get_promise(),
                   "do_delete_message_logevent");
    }
    db_promise = mpas.get_promise();
    lock.set_value(Unit());
  }

  // message may not exist in the dialog
  LOG(INFO) << "Delete " << logevent.full_message_id_ << " from database";
  G()->td_db()->get_messages_db_async()->delete_message(logevent.full_message_id_, std::move(db_promise));
}

void MessagesManager::attach_message_to_previous(Dialog *d, MessageId message_id, const char *source) {
  CHECK(d != nullptr);
  MessagesIterator it(d, message_id);
  Message *message = *it;
  CHECK(message != nullptr);
  CHECK(message->message_id == message_id);
  CHECK(message->have_previous) << d->dialog_id << " " << message_id << " " << source;
  --it;
  CHECK(*it != nullptr) << d->dialog_id << " " << message_id << " " << source;
  LOG(INFO) << "Attach " << message_id << " to the previous " << (*it)->message_id;
  if ((*it)->have_next) {
    message->have_next = true;
  } else {
    (*it)->have_next = true;
  }
}

void MessagesManager::attach_message_to_next(Dialog *d, MessageId message_id, const char *source) {
  CHECK(d != nullptr);
  MessagesIterator it(d, message_id);
  Message *message = *it;
  CHECK(message != nullptr);
  CHECK(message->message_id == message_id);
  CHECK(message->have_next) << d->dialog_id << " " << message_id << " " << source;
  ++it;
  CHECK(*it != nullptr) << d->dialog_id << " " << message_id << " " << source;
  LOG(INFO) << "Attach " << message_id << " to the next " << (*it)->message_id;
  if ((*it)->have_previous) {
    message->have_previous = true;
  } else {
    (*it)->have_previous = true;
  }
}

void MessagesManager::update_message(Dialog *d, unique_ptr<Message> &old_message, unique_ptr<Message> new_message,
                                     bool need_send_update_message_content, bool *need_update_dialog_pos) {
  CHECK(d != nullptr);
  CHECK(old_message != nullptr);
  CHECK(new_message != nullptr);
  CHECK(old_message->message_id == new_message->message_id);
  CHECK(old_message->random_y == new_message->random_y);
  CHECK(need_update_dialog_pos != nullptr);

  DialogId dialog_id = d->dialog_id;
  MessageId message_id = old_message->message_id;
  bool is_changed = false;
  if (old_message->date != new_message->date) {
    if (new_message->date > 0) {
      LOG_IF(ERROR, !new_message->is_outgoing && dialog_id != get_my_dialog_id())
          << "Date has changed for incoming " << message_id << " in " << dialog_id << " from " << old_message->date
          << " to " << new_message->date;
      CHECK(old_message->date > 0);
      old_message->date = new_message->date;
      if (d->last_message_id == message_id) {
        *need_update_dialog_pos = true;
      }
      is_changed = true;
    } else {
      LOG(ERROR) << "Receive " << message_id << " in " << dialog_id << " with wrong date " << new_message->date;
    }
  }
  bool is_edited = false;
  if (old_message->edit_date != new_message->edit_date) {
    if (new_message->edit_date > 0) {
      if (new_message->edit_date > old_message->edit_date) {
        old_message->edit_date = new_message->edit_date;
        is_edited = true;
        is_changed = true;
      }
    } else {
      LOG(ERROR) << "Receive " << message_id << " in " << dialog_id << " with wrong edit date "
                 << new_message->edit_date << ", old edit date = " << old_message->edit_date;
    }
  }

  if (old_message->author_signature != new_message->author_signature) {
    LOG_IF(INFO, !old_message->sender_user_id.is_valid() || new_message->sender_user_id.is_valid())
        << "Author signature has changed for " << message_id << " in " << dialog_id << " sent by "
        << old_message->sender_user_id << "/" << new_message->sender_user_id << " from "
        << old_message->author_signature << " to " << new_message->author_signature;
    old_message->author_signature = std::move(new_message->author_signature);
    is_changed = true;
  }
  if (old_message->sender_user_id != new_message->sender_user_id) {
    // there can be race for sent signed posts
    LOG_IF(ERROR, old_message->sender_user_id != UserId() && new_message->sender_user_id != UserId())
        << message_id << " in " << dialog_id << " has changed sender from " << old_message->sender_user_id << " to "
        << new_message->sender_user_id;

    LOG_IF(WARNING, new_message->sender_user_id.is_valid() || old_message->author_signature.empty())
        << "Update message sender from " << old_message->sender_user_id << " to " << new_message->sender_user_id
        << " in " << dialog_id;
    old_message->sender_user_id = new_message->sender_user_id;
    is_changed = true;
  }
  if (old_message->forward_info == nullptr) {
    if (new_message->forward_info != nullptr) {
      LOG(ERROR) << message_id << " in " << dialog_id << " has received forward info " << *new_message->forward_info
                 << ", really forwarded from " << old_message->debug_forward_from << ", message content type is "
                 << old_message->content->get_type() << '/' << new_message->content->get_type();
    }
  } else {
    if (new_message->forward_info != nullptr) {
      if (*old_message->forward_info != *new_message->forward_info &&
          (!old_message->forward_info->sender_user_id.is_valid() ||
           new_message->forward_info->sender_user_id.is_valid())) {
        old_message->forward_info->author_signature = new_message->forward_info->author_signature;
        LOG_IF(ERROR, *old_message->forward_info != *new_message->forward_info)
            << message_id << " in " << dialog_id << " has changed forward info from " << *old_message->forward_info
            << " to " << *new_message->forward_info << ", really forwarded from " << old_message->debug_forward_from;
      }
      old_message->forward_info = std::move(new_message->forward_info);
      is_changed = true;
    } else {
      LOG(ERROR) << message_id << " in " << dialog_id << " sent by " << old_message->sender_user_id
                 << " has lost forward info " << *old_message->forward_info << ", really forwarded from "
                 << old_message->debug_forward_from;
      old_message->forward_info = nullptr;
      is_changed = true;
    }
  }
  if (old_message->notification_id != new_message->notification_id) {
    if (old_message->notification_id.is_valid()) {
      if (new_message->notification_id.is_valid()) {
        LOG(ERROR) << "Notification id for " << message_id << " in " << dialog_id << " has tried to change from "
                   << old_message->notification_id << " to " << new_message->notification_id;
      }
    } else {
      CHECK(new_message->notification_id.is_valid());
      add_notification_id_to_message_id_correspondence(d, new_message->notification_id, message_id);
      old_message->notification_id = new_message->notification_id;
    }
  }

  if (old_message->reply_to_message_id != new_message->reply_to_message_id) {
    // Can't check "&& get_message_force(d, old_message->reply_to_message_id) == nullptr", because it
    // can change message tree and invalidate reference to old_message
    if (new_message->reply_to_message_id == MessageId()) {
      old_message->reply_to_message_id = MessageId();
      is_changed = true;
    } else {
      LOG(ERROR) << message_id << " in " << dialog_id << " has changed message it is reply to from "
                 << old_message->reply_to_message_id << " to " << new_message->reply_to_message_id;
      dump_debug_message_op(d);
    }
  }
  if (old_message->via_bot_user_id != new_message->via_bot_user_id) {
    LOG(ERROR) << message_id << " in " << dialog_id << " has changed bot via it is sent from "
               << old_message->via_bot_user_id << " to " << new_message->via_bot_user_id;
    old_message->via_bot_user_id = new_message->via_bot_user_id;
    is_changed = true;
  }
  if (old_message->is_outgoing != new_message->is_outgoing) {
    LOG(ERROR) << message_id << " in " << dialog_id << " has changed is_outgoing from " << old_message->is_outgoing
               << " to " << new_message->is_outgoing;
    old_message->is_outgoing = new_message->is_outgoing;
    is_changed = true;
  }
  LOG_IF(ERROR, old_message->is_channel_post != new_message->is_channel_post)
      << message_id << " in " << dialog_id << " has changed is_channel_post from " << old_message->is_channel_post
      << " to " << new_message->is_channel_post;
  if (old_message->contains_mention != new_message->contains_mention) {
    LOG_IF(ERROR, old_message->edit_date == 0)
        << message_id << " in " << dialog_id << " has changed contains_mention from " << old_message->contains_mention
        << " to " << new_message->contains_mention;
    // contains_mention flag shouldn't be changed, because the message will not be added to unread mention list
    // and we are unable to show/hide message notification
    // old_message->contains_mention = new_message->contains_mention;
    // is_changed = true;
  }
  if (old_message->disable_notification != new_message->disable_notification) {
    LOG_IF(ERROR, old_message->edit_date == 0)
        << "Disable_notification has changed from " << old_message->disable_notification << " to "
        << new_message->disable_notification
        << ". Old message: " << to_string(get_message_object(dialog_id, old_message.get()))
        << ". New message: " << to_string(get_message_object(dialog_id, new_message.get()));
    // disable_notification flag shouldn't be changed, because we are unable to show/hide message notification
    // old_message->disable_notification = new_message->disable_notification;
    // is_changed = true;
  }

  if (update_message_contains_unread_mention(d, old_message.get(), new_message->contains_unread_mention,
                                             "update_message")) {
    is_changed = true;
  }
  if (update_message_views(dialog_id, old_message.get(), new_message->views)) {
    is_changed = true;
  }
  if ((old_message->media_album_id == 0 || td_->auth_manager_->is_bot()) && new_message->media_album_id != 0) {
    old_message->media_album_id = new_message->media_album_id;
    is_changed = true;
  }

  if (old_message->edit_date > 0) {
    // inline keyboard can be edited
    bool reply_markup_changed =
        ((old_message->reply_markup == nullptr) != (new_message->reply_markup == nullptr)) ||
        (old_message->reply_markup != nullptr && *old_message->reply_markup != *new_message->reply_markup);
    if (reply_markup_changed) {
      if (d->reply_markup_message_id == message_id && !td_->auth_manager_->is_bot() &&
          new_message->reply_markup == nullptr) {
        set_dialog_reply_markup(d, MessageId());
      }
      old_message->reply_markup = std::move(new_message->reply_markup);
      is_edited = true;
      is_changed = true;
    }
    old_message->had_reply_markup = false;
  } else {
    if (old_message->reply_markup == nullptr) {
      if (new_message->reply_markup != nullptr) {
        auto content_type = old_message->content->get_type();
        // MessageGame and MessageInvoice reply markup can be generated server side
        LOG_IF(ERROR, content_type != MessageContentType::Game && content_type != MessageContentType::Invoice)
            << message_id << " in " << dialog_id << " has received reply markup " << *new_message->reply_markup;

        old_message->had_reply_markup = false;
        old_message->reply_markup = std::move(new_message->reply_markup);
        is_changed = true;
      }
    } else {
      if (new_message->reply_markup != nullptr) {
        LOG_IF(WARNING, *old_message->reply_markup != *new_message->reply_markup)
            << message_id << " in " << dialog_id << " has changed reply_markup from " << *old_message->reply_markup
            << " to " << *new_message->reply_markup;
      } else {
        LOG(ERROR) << message_id << " in " << dialog_id << " sent by " << old_message->sender_user_id
                   << " has lost reply markup " << *old_message->reply_markup
                   << ". Old message: " << to_string(get_message_object(dialog_id, old_message.get()))
                   << ". New message: " << to_string(get_message_object(dialog_id, new_message.get()));
      }
    }
  }

  if (old_message->last_access_date < new_message->last_access_date) {
    old_message->last_access_date = new_message->last_access_date;
  }

  CHECK(!new_message->have_previous || !new_message->have_next);
  if (new_message->have_previous && !old_message->have_previous) {
    old_message->have_previous = true;
    attach_message_to_previous(d, message_id, "update_message");
  } else if (new_message->have_next && !old_message->have_next) {
    old_message->have_next = true;
    attach_message_to_next(d, message_id, "update_message");
  }

  if (update_message_content(dialog_id, old_message.get(), std::move(new_message->content),
                             need_send_update_message_content,
                             message_id.is_yet_unsent() && new_message->edit_date == 0)) {
    is_changed = true;
  }
  // TODO update can be send only if the message has already been returned to the user
  if (is_edited && !td_->auth_manager_->is_bot()) {
    send_update_message_edited(dialog_id, old_message.get());
  }

  on_message_changed(d, old_message.get(), is_changed, "update_message");
}

bool MessagesManager::need_message_changed_warning(const Message *old_message) {
  if (old_message->edit_date > 0) {
    // message was edited
    return false;
  }
  if (old_message->message_id.is_yet_unsent() && old_message->forward_info != nullptr) {
    // original message may be edited
    return false;
  }
  return true;
}

bool MessagesManager::update_message_content(DialogId dialog_id, Message *old_message,
                                             unique_ptr<MessageContent> new_content,
                                             bool need_send_update_message_content, bool need_merge_files) {
  bool is_content_changed = false;
  bool need_update = false;
  unique_ptr<MessageContent> &old_content = old_message->content;
  MessageContentType old_content_type = old_content->get_type();
  MessageContentType new_content_type = new_content->get_type();

  auto old_file_id = get_message_content_file_id(old_content.get());
  bool need_finish_upload = old_file_id.is_valid() && need_merge_files;
  if (old_content_type != new_content_type) {
    need_update = true;
    LOG(INFO) << "Message content has changed its type from " << old_content_type << " to " << new_content_type;

    old_message->is_content_secret = is_secret_message_content(old_message->ttl, new_content->get_type());

    if (need_merge_files && old_file_id.is_valid()) {
      auto new_file_id = get_message_content_file_id(new_content.get());
      if (new_file_id.is_valid()) {
        FileView old_file_view = td_->file_manager_->get_file_view(old_file_id);
        FileView new_file_view = td_->file_manager_->get_file_view(new_file_id);
        // if file type has changed, but file size remains the same, we are trying to update local location of the new
        // file with the old local location
        if (old_file_view.has_local_location() && !new_file_view.has_local_location() && old_file_view.size() != 0 &&
            old_file_view.size() == new_file_view.size()) {
          auto old_file_type = old_file_view.get_type();
          auto new_file_type = new_file_view.get_type();
          auto is_document_file_type = [](FileType file_type) {
            switch (file_type) {
              case FileType::Video:
              case FileType::VoiceNote:
              case FileType::Document:
              case FileType::Sticker:
              case FileType::Audio:
              case FileType::Animation:
              case FileType::VideoNote:
                return true;
              default:
                return false;
            }
          };

          if (is_document_file_type(old_file_type) && is_document_file_type(new_file_type)) {
            auto &old_location = old_file_view.local_location();
            auto r_file_id = td_->file_manager_->register_local(
                FullLocalFileLocation(new_file_type, old_location.path_, old_location.mtime_nsec_), dialog_id,
                old_file_view.size());
            if (r_file_id.is_ok()) {
              LOG_STATUS(td_->file_manager_->merge(new_file_id, r_file_id.ok()));
            }
          }
        }
      }
    }
  } else {
    merge_message_contents(td_, old_content.get(), new_content.get(), need_message_changed_warning(old_message),
                           dialog_id, need_merge_files, is_content_changed, need_update);
  }
  if (need_finish_upload) {
    // the file is likely to be already merged with a server file, but if not we need to
    // cancel file upload of the main file to allow next upload with the same file to succeed
    td_->file_manager_->upload(old_file_id, nullptr, 0, 0);
  }

  if (is_content_changed || need_update) {
    old_content = std::move(new_content);
    update_message_content_file_id_remote(old_content.get(), old_file_id);
  } else {
    update_message_content_file_id_remote(old_content.get(), get_message_content_file_id(new_content.get()));
  }
  if (is_content_changed && !need_update) {
    LOG(INFO) << "Content of " << old_message->message_id << " in " << dialog_id << " has changed";
  }

  if (need_update && need_send_update_message_content) {
    send_update_message_content(dialog_id, old_message->message_id, old_content.get(), old_message->date,
                                old_message->is_content_secret, "update_message_content");
  }
  return is_content_changed || need_update;
}

MessagesManager::Dialog *MessagesManager::get_dialog_by_message_id(MessageId message_id) {
  CHECK(message_id.is_valid() && message_id.is_server());
  auto it = message_id_to_dialog_id_.find(message_id);
  if (it == message_id_to_dialog_id_.end()) {
    if (G()->parameters().use_message_db) {
      auto r_value =
          G()->td_db()->get_messages_db_sync()->get_message_by_unique_message_id(message_id.get_server_message_id());
      if (r_value.is_ok()) {
        DialogId dialog_id(r_value.ok().first);
        Message *m = on_get_message_from_database(dialog_id, get_dialog_force(dialog_id), r_value.ok().second);
        if (m != nullptr) {
          CHECK(m->message_id == message_id);
          CHECK(message_id_to_dialog_id_[message_id] == dialog_id);
          Dialog *d = get_dialog(dialog_id);
          CHECK(d != nullptr);
          return d;
        }
      }
    }

    return nullptr;
  }

  return get_dialog(it->second);
}

MessageId MessagesManager::get_message_id_by_random_id(Dialog *d, int64 random_id, const char *source) {
  CHECK(d != nullptr);
  CHECK(d->dialog_id.get_type() == DialogType::SecretChat);
  if (random_id == 0) {
    return MessageId();
  }
  auto it = d->random_id_to_message_id.find(random_id);
  if (it == d->random_id_to_message_id.end()) {
    if (G()->parameters().use_message_db) {
      auto r_value = G()->td_db()->get_messages_db_sync()->get_message_by_random_id(d->dialog_id, random_id);
      if (r_value.is_ok()) {
        debug_add_message_to_dialog_fail_reason_ = "not called";
        Message *m = on_get_message_from_database(d->dialog_id, d, r_value.ok());
        if (m != nullptr) {
          CHECK(m->random_id == random_id)
              << random_id << " " << m->random_id << " " << d->random_id_to_message_id[random_id] << " "
              << d->random_id_to_message_id[m->random_id] << " " << m->message_id << " " << source << " "
              << m->from_database << get_message(d, m->message_id) << " " << m << " "
              << debug_add_message_to_dialog_fail_reason_;
          CHECK(d->random_id_to_message_id.count(random_id))
              << source << " " << random_id << " " << m->message_id << " " << m->is_failed_to_send << " "
              << m->is_outgoing << " " << m->from_database << " " << get_message(d, m->message_id) << " " << m << " "
              << debug_add_message_to_dialog_fail_reason_;
          CHECK(d->random_id_to_message_id[random_id] == m->message_id)
              << source << " " << random_id << " " << d->random_id_to_message_id[random_id] << " " << m->message_id
              << " " << m->is_failed_to_send << " " << m->is_outgoing << " " << m->from_database << " "
              << get_message(d, m->message_id) << " " << m << " " << debug_add_message_to_dialog_fail_reason_;
          return m->message_id;
        }
      }
    }

    return MessageId();
  }

  return it->second;
}

void MessagesManager::force_create_dialog(DialogId dialog_id, const char *source, bool force_update_dialog_pos) {
  CHECK(dialog_id.is_valid());
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(INFO) << "Force create " << dialog_id << " from " << source;
    if (loaded_dialogs_.count(dialog_id) > 0) {
      return;
    }

    d = add_dialog(dialog_id);
    update_dialog_pos(d, false, "force_create_dialog");

    if (dialog_id.get_type() == DialogType::SecretChat) {
      // secret chat is being created
      // let's copy notification settings from main chat if available
      auto secret_chat_id = dialog_id.get_secret_chat_id();
      auto user_id = td_->contacts_manager_->get_secret_chat_user_id(secret_chat_id);
      Dialog *user_d = get_dialog_force(DialogId(user_id));
      if (user_d != nullptr && user_d->notification_settings.is_synchronized) {
        LOG(INFO) << "Copy notification settings from " << user_d->dialog_id << " to " << dialog_id;
        update_dialog_notification_settings(dialog_id, &d->notification_settings, user_d->notification_settings);
      }

      if (G()->parameters().use_message_db && !td_->auth_manager_->is_bot() &&
          !td_->contacts_manager_->get_secret_chat_is_outbound(secret_chat_id)) {
        auto notification_group_id = get_dialog_notification_group_id(dialog_id, d->message_notification_group);
        if (notification_group_id.is_valid()) {
          d->new_secret_chat_notification_id = get_next_notification_id(d, notification_group_id, MessageId());
          if (d->new_secret_chat_notification_id.is_valid()) {
            auto date = td_->contacts_manager_->get_secret_chat_date(secret_chat_id);
            bool is_changed = set_dialog_last_notification(dialog_id, d->message_notification_group, date,
                                                           d->new_secret_chat_notification_id, "add_new_secret_chat");
            CHECK(is_changed);
            VLOG(notifications) << "Create " << d->new_secret_chat_notification_id << " with " << secret_chat_id;
            send_closure_later(G()->notification_manager(), &NotificationManager::add_notification,
                               notification_group_id, NotificationGroupType::SecretChat, dialog_id, date, dialog_id,
                               false, d->new_secret_chat_notification_id, create_new_secret_chat_notification());
          }
        }
      }
    }
    if (!have_input_peer(dialog_id, AccessRights::Read)) {
      if (!have_dialog_info(dialog_id)) {
        LOG(ERROR) << "Have no info about " << dialog_id << " received from " << source << ", but forced to create it";
      } else {
        LOG_IF(ERROR, Slice(source) != Slice("message forward info") &&
                          Slice(source) != Slice("on_new_callback_query") &&
                          Slice(source) != Slice("search public dialog") &&
                          Slice(source) != Slice("create new secret chat") && !force_update_dialog_pos)
            << "Have no access to " << dialog_id << " received from " << source << ", but forced to create it";
      }
    }
  } else if (force_update_dialog_pos) {
    update_dialog_pos(d, false, "force update dialog pos");
  }
}

MessagesManager::Dialog *MessagesManager::add_dialog(DialogId dialog_id) {
  LOG(DEBUG) << "Creating " << dialog_id;
  CHECK(!have_dialog(dialog_id));

  if (G()->parameters().use_message_db) {
    // TODO preload dialog asynchronously, remove loading from this function
    auto r_value = G()->td_db()->get_dialog_db_sync()->get_dialog(dialog_id);
    if (r_value.is_ok()) {
      LOG(INFO) << "Synchronously loaded " << dialog_id << " from database";
      return add_new_dialog(parse_dialog(dialog_id, r_value.ok()), true);
    }
  }

  auto d = make_unique<Dialog>();
  std::fill(d->message_count_by_index.begin(), d->message_count_by_index.end(), -1);
  d->dialog_id = dialog_id;

  return add_new_dialog(std::move(d), false);
}

MessagesManager::Dialog *MessagesManager::add_new_dialog(unique_ptr<Dialog> &&d, bool is_loaded_from_database) {
  auto dialog_id = d->dialog_id;
  switch (dialog_id.get_type()) {
    case DialogType::User:
      if (dialog_id == get_my_dialog_id()) {
        d->last_read_inbox_message_id = MessageId::max();
        d->is_last_read_inbox_message_id_inited = true;
        d->last_read_outbox_message_id = MessageId::max();
        d->is_last_read_outbox_message_id_inited = true;
      }
      break;
    case DialogType::Chat:
      break;
    case DialogType::Channel: {
      auto channel_type = td_->contacts_manager_->get_channel_type(dialog_id.get_channel_id());
      if (channel_type == ChannelType::Broadcast) {
        d->last_read_outbox_message_id = MessageId::max();
        d->is_last_read_outbox_message_id_inited = true;
      }

      auto pts = load_channel_pts(dialog_id);
      if (pts > 0) {
        d->pts = pts;
        if (is_debug_message_op_enabled()) {
          d->debug_message_op.emplace_back(Dialog::MessageOp::SetPts, pts, "add_new_dialog");
        }
      }
      break;
    }
    case DialogType::SecretChat:
      if (!d->last_new_message_id.is_valid()) {
        LOG(INFO) << "Set " << d->dialog_id << " last new message id in add_new_dialog";
        // TODO use date << MessageId::SERVER_ID_SHIFT;
        d->last_new_message_id = MessageId::min();
      }
      for (auto &first_message_id : d->first_database_message_id_by_index) {
        first_message_id = MessageId::min();
      }
      for (auto &message_count : d->message_count_by_index) {
        if (message_count == -1) {
          message_count = 0;
        }
      }

      d->have_full_history = true;
      d->need_restore_reply_markup = false;
      d->is_last_read_inbox_message_id_inited = true;
      d->is_last_read_outbox_message_id_inited = true;
      d->notification_settings.is_synchronized = true;
      d->know_can_report_spam = true;
      if (!is_loaded_from_database) {
        d->can_report_spam =
            td_->contacts_manager_->default_can_report_spam_in_secret_chat(dialog_id.get_secret_chat_id());
      }
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }
  if (!is_loaded_from_database) {
    on_dialog_updated(dialog_id, "add_new_dialog");
  }

  unique_ptr<Message> last_database_message = std::move(d->messages);
  MessageId last_database_message_id = d->last_database_message_id;
  d->last_database_message_id = MessageId();
  int64 order = d->order;
  d->order = DEFAULT_ORDER;
  int32 last_clear_history_date = d->last_clear_history_date;
  MessageId last_clear_history_message_id = d->last_clear_history_message_id;
  d->last_clear_history_date = 0;
  d->last_clear_history_message_id = MessageId();

  if (d->message_notification_group.group_id.is_valid()) {
    notification_group_id_to_dialog_id_.emplace(d->message_notification_group.group_id, d->dialog_id);
  }
  if (d->mention_notification_group.group_id.is_valid()) {
    notification_group_id_to_dialog_id_.emplace(d->mention_notification_group.group_id, d->dialog_id);
  }

  if (!is_loaded_from_database) {
    CHECK(order == DEFAULT_ORDER);
    CHECK(last_database_message == nullptr);
  }

  auto dialog_it = dialogs_.emplace(dialog_id, std::move(d)).first;

  loaded_dialogs_.erase(dialog_id);

  Dialog *dialog = dialog_it->second.get();
  send_update_new_chat(dialog);

  fix_new_dialog(dialog, std::move(last_database_message), last_database_message_id, order, last_clear_history_date,
                 last_clear_history_message_id, is_loaded_from_database);

  return dialog;
}

void MessagesManager::fix_new_dialog(Dialog *d, unique_ptr<Message> &&last_database_message,
                                     MessageId last_database_message_id, int64 order, int32 last_clear_history_date,
                                     MessageId last_clear_history_message_id, bool is_loaded_from_database) {
  CHECK(d != nullptr);
  auto dialog_id = d->dialog_id;

  if (being_added_dialog_id_ != dialog_id && !td_->auth_manager_->is_bot() && !is_dialog_inited(d) &&
      dialog_id.get_type() != DialogType::SecretChat && have_input_peer(dialog_id, AccessRights::Read)) {
    // asynchronously get dialog from the server
    send_get_dialog_query(dialog_id, Auto());
  }

  if (d->notification_settings.is_synchronized && !d->notification_settings.is_use_default_fixed &&
      have_input_peer(dialog_id, AccessRights::Read)) {
    LOG(INFO) << "Reget notification settings of " << dialog_id;
    if (d->dialog_id.get_type() == DialogType::SecretChat) {
      if (d->notification_settings.mute_until == 0 && users_notification_settings_.mute_until == 0) {
        d->notification_settings.use_default_mute_until = true;
      }
      if (d->notification_settings.sound == "default" && users_notification_settings_.sound == "default") {
        d->notification_settings.use_default_sound = true;
      }
      if (d->notification_settings.show_preview && users_notification_settings_.show_preview) {
        d->notification_settings.use_default_show_preview = true;
      }
      d->notification_settings.is_use_default_fixed = true;
      on_dialog_updated(d->dialog_id, "reget notification settings");
    } else {
      send_get_dialog_notification_settings_query(dialog_id, Promise<>());
    }
  }
  if (d->notification_settings.use_default_mute_until || d->notification_settings.mute_until <= G()->unix_time()) {
    d->notification_settings.mute_until = 0;
  } else {
    update_dialog_unmute_timeout(d, false, -1, false, d->notification_settings.mute_until);
  }

  auto pending_it = pending_add_dialog_last_database_message_dependent_dialogs_.find(dialog_id);
  if (pending_it != pending_add_dialog_last_database_message_dependent_dialogs_.end()) {
    auto pending_dialogs = std::move(pending_it->second);
    pending_add_dialog_last_database_message_dependent_dialogs_.erase(pending_it);

    for (auto &pending_dialog_id : pending_dialogs) {
      auto &counter_message = pending_add_dialog_last_database_message_[pending_dialog_id];
      CHECK(counter_message.first > 0);
      counter_message.first--;
      if (counter_message.first == 0) {
        add_dialog_last_database_message(get_dialog(pending_dialog_id), std::move(counter_message.second));
        pending_add_dialog_last_database_message_.erase(pending_dialog_id);
      }
    }
  }

  set_dialog_last_clear_history_date(d, last_clear_history_date, last_clear_history_message_id, "fix_new_dialog 8",
                                     is_loaded_from_database);

  set_dialog_order(d, order, false, is_loaded_from_database, "fix_new_dialog 9");

  if (dialog_id.get_type() != DialogType::SecretChat && d->last_new_message_id.is_valid() &&
      !d->last_new_message_id.is_server()) {
    // fix wrong last_new_message_id
    d->last_new_message_id = MessageId(d->last_new_message_id.get() & ~MessageId::FULL_TYPE_MASK);
    on_dialog_updated(dialog_id, "fix_new_dialog 10");
  }

  bool need_get_history = true;

  // add last database message to dialog
  MessageId last_message_id;
  if (last_database_message != nullptr) {
    need_get_history = false;
    last_message_id = last_database_message->message_id;
  } else if (last_database_message_id.is_valid()) {
    last_message_id = last_database_message_id;
  }

  if (last_message_id.is_valid()) {
    if ((last_message_id.is_server() || dialog_id.get_type() == DialogType::SecretChat) &&
        !d->last_new_message_id.is_valid()) {
      LOG(ERROR) << "Bugfixing wrong last_new_message_id to " << last_message_id << " in " << dialog_id;
      // must be called before set_dialog_first_database_message_id and set_dialog_last_database_message_id
      set_dialog_last_new_message_id(d, last_message_id, "fix_new_dialog 1");
    }
    if (!d->first_database_message_id.is_valid() || d->first_database_message_id.get() > last_message_id.get()) {
      LOG(ERROR) << "Bugfixing wrong first_database_message_id from " << d->first_database_message_id << " to "
                 << last_message_id << " in " << dialog_id;
      set_dialog_first_database_message_id(d, last_message_id, "fix_new_dialog 2");
    }
    set_dialog_last_database_message_id(d, last_message_id, "fix_new_dialog 3", is_loaded_from_database);
  } else if (d->first_database_message_id.is_valid()) {
    // ensure that first_database_message_id <= last_database_message_id
    if (d->first_database_message_id.get() <= d->last_new_message_id.get()) {
      set_dialog_last_database_message_id(d, d->last_new_message_id, "fix_new_dialog 4");
    } else {
      // can't fix last_database_message_id, drop first_database_message_id; it shouldn't happen anyway
      set_dialog_first_database_message_id(d, MessageId(), "fix_new_dialog 5");
    }
  }
  d->debug_first_database_message_id = d->first_database_message_id;
  d->debug_last_database_message_id = d->last_database_message_id;
  d->debug_last_new_message_id = d->last_new_message_id;

  if (last_database_message != nullptr) {
    int32 dependent_dialog_count = 0;
    if (last_database_message->forward_info != nullptr) {
      auto other_dialog_id = last_database_message->forward_info->dialog_id;
      if (other_dialog_id.is_valid() && !have_dialog(other_dialog_id)) {
        LOG(INFO) << "Postpone adding of last message in " << dialog_id << " because of cyclic dependency with "
                  << other_dialog_id;
        pending_add_dialog_last_database_message_dependent_dialogs_[other_dialog_id].push_back(dialog_id);
        dependent_dialog_count++;
      }
      other_dialog_id = last_database_message->forward_info->from_dialog_id;
      if (other_dialog_id.is_valid() && !have_dialog(other_dialog_id)) {
        LOG(INFO) << "Postpone adding of last message in " << dialog_id << " because of cyclic dependency with "
                  << other_dialog_id;
        pending_add_dialog_last_database_message_dependent_dialogs_[other_dialog_id].push_back(dialog_id);
        dependent_dialog_count++;
      }
    }

    if (dependent_dialog_count == 0) {
      add_dialog_last_database_message(d, std::move(last_database_message));
    } else {
      // can't add message immediately, because need to notify first about adding of dependent dialogs
      d->pending_last_message_date = last_database_message->date;
      d->pending_last_message_id = last_database_message->message_id;
      pending_add_dialog_last_database_message_[dialog_id] = {dependent_dialog_count, std::move(last_database_message)};
    }
  } else if (last_database_message_id.is_valid()) {
    d->pending_last_message_date = DialogDate(order, dialog_id).get_date();
    d->pending_last_message_id = last_database_message_id;
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      break;
    case DialogType::Chat:
      if (d->last_read_inbox_message_id.get() < d->last_read_outbox_message_id.get()) {
        LOG(INFO) << "Have last read outbox message " << d->last_read_outbox_message_id << " in " << dialog_id
                  << ", but last read inbox message is " << d->last_read_inbox_message_id;
        // can't fix last_read_inbox_message_id by last_read_outbox_message_id because last_read_outbox_message_id is
        // just a message id not less than last read outgoing message and less than first unread outgoing message, so
        // it may not point to the outgoing message
        // read_history_inbox(dialog_id, d->last_read_outbox_message_id, d->server_unread_count, "fix_new_dialog 6");
      }
      break;
    case DialogType::Channel:
      break;
    case DialogType::SecretChat:
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  update_dialogs_hints(d);

  if (d->delete_last_message_date != 0) {
    if (d->last_message_id.is_valid()) {
      LOG(ERROR) << "Last " << d->deleted_last_message_id << " in " << dialog_id << " was deleted at "
                 << d->delete_last_message_date << ", but have last " << d->last_message_id;
      d->delete_last_message_date = 0;
      d->deleted_last_message_id = MessageId();
      d->is_last_message_deleted_locally = false;
      on_dialog_updated(dialog_id, "update_delete_last_message_date");
    } else {
      need_get_history = true;
    }
  }

  if (need_get_history && !td_->auth_manager_->is_bot() && have_input_peer(dialog_id, AccessRights::Read) &&
      d->order != DEFAULT_ORDER) {
    get_history_from_the_end(dialog_id, true, false, Auto());
  }

  if (d->need_repair_server_unread_count && have_input_peer(dialog_id, AccessRights::Read)) {
    CHECK(dialog_id.get_type() != DialogType::SecretChat);
    repair_server_unread_count(dialog_id, d->server_unread_count);
  }

  update_dialog_pos(d, false, "fix_new_dialog 7", true, is_loaded_from_database);

  LOG(INFO) << "Loaded " << dialog_id << " with last new " << d->last_new_message_id << ", first database "
            << d->first_database_message_id << ", last database " << d->last_database_message_id << ", last "
            << d->last_message_id;
}

void MessagesManager::add_dialog_last_database_message(Dialog *d, unique_ptr<Message> &&last_database_message) {
  CHECK(d != nullptr);
  CHECK(last_database_message != nullptr);
  CHECK(last_database_message->left == nullptr);
  CHECK(last_database_message->right == nullptr);

  auto message_id = last_database_message->message_id;
  CHECK(d->last_database_message_id == message_id)
      << message_id << " " << d->last_database_message_id << " " << d->debug_set_dialog_last_database_message_id;

  if (!have_input_peer(d->dialog_id, AccessRights::Read)) {
    // do not add last message to inaccessible dialog
    if (d->pending_last_message_date != 0) {
      d->pending_last_message_date = 0;
      d->pending_last_message_id = MessageId();
      update_dialog_pos(d, false, "add_dialog_last_database_message 1");
    }
    return;
  }

  bool need_update = false;
  bool need_update_dialog_pos = false;
  last_database_message->have_previous = false;
  last_database_message->have_next = false;
  last_database_message->from_database = true;
  Message *m = add_message_to_dialog(d, std::move(last_database_message), false, &need_update, &need_update_dialog_pos,
                                     "add_dialog_last_database_message 2");
  if (need_update_dialog_pos) {
    LOG(ERROR) << "Need to update pos in " << d->dialog_id;
  }
  if (m != nullptr) {
    set_dialog_last_message_id(d, message_id, "add_dialog_last_database_message 3");
    send_update_chat_last_message(d, "add_dialog_last_database_message 4");
  } else if (d->pending_last_message_date != 0) {
    d->pending_last_message_date = 0;
    d->pending_last_message_id = MessageId();
    need_update_dialog_pos = true;
  }

  if (need_update_dialog_pos) {
    update_dialog_pos(d, false, "add_dialog_last_database_message 5");
  }
}

void MessagesManager::update_dialogs_hints(const Dialog *d) {
  if (!td_->auth_manager_->is_bot() && d->order != DEFAULT_ORDER) {
    dialogs_hints_.add(-d->dialog_id.get(), get_dialog_title(d->dialog_id) + ' ' + get_dialog_username(d->dialog_id));
  }
}

void MessagesManager::update_dialogs_hints_rating(const Dialog *d) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  if (d->order == DEFAULT_ORDER) {
    LOG(INFO) << "Remove " << d->dialog_id << " from chats search";
    dialogs_hints_.remove(-d->dialog_id.get());
  } else {
    LOG(INFO) << "Change position of " << d->dialog_id << " in chats search";
    dialogs_hints_.set_rating(-d->dialog_id.get(), -d->order);
  }
}

int64 MessagesManager::get_dialog_order(MessageId message_id, int32 message_date) {
  return (static_cast<int64>(message_date) << 32) + narrow_cast<int32>(message_id.get() >> MessageId::SERVER_ID_SHIFT);
}

int64 MessagesManager::get_next_pinned_dialog_order() {
  if (current_pinned_dialog_order_ == DEFAULT_ORDER) {
    string res_str = G()->td_db()->get_binlog_pmc()->get("dialog_pinned_current_order");
    if (res_str.empty()) {
      current_pinned_dialog_order_ = static_cast<int64>(MIN_PINNED_DIALOG_DATE) << 32;
    } else {
      current_pinned_dialog_order_ = to_integer<int64>(res_str);
    }
  }
  CHECK(current_pinned_dialog_order_ != DEFAULT_ORDER);

  current_pinned_dialog_order_++;
  G()->td_db()->get_binlog_pmc()->set("dialog_pinned_current_order", to_string(current_pinned_dialog_order_));
  LOG(INFO) << "Assign pinned_order = " << current_pinned_dialog_order_;
  return current_pinned_dialog_order_;
}

void MessagesManager::update_dialog_pos(Dialog *d, bool remove_from_dialog_list, const char *source,
                                        bool need_send_update_chat_order, bool is_loaded_from_database) {
  CHECK(d != nullptr);
  LOG(INFO) << "Trying to update " << d->dialog_id << " order from " << source;
  auto dialog_type = d->dialog_id.get_type();

  switch (dialog_type) {
    case DialogType::User:
      break;
    case DialogType::Chat: {
      auto chat_id = d->dialog_id.get_chat_id();
      if (!td_->contacts_manager_->get_chat_is_active(chat_id)) {
        remove_from_dialog_list = true;
      }
      break;
    }
    case DialogType::Channel: {
      auto channel_id = d->dialog_id.get_channel_id();
      auto status = td_->contacts_manager_->get_channel_status(channel_id);
      if (!status.is_member()) {
        remove_from_dialog_list = true;
      }
      break;
    }
    case DialogType::SecretChat:
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
      return;
  }

  int64 new_order = DEFAULT_ORDER;
  if (!remove_from_dialog_list) {
    if (d->pinned_order != DEFAULT_ORDER) {
      LOG(INFO) << "Pin at " << d->pinned_order << " found";
      new_order = d->pinned_order;
    }
    if (d->last_message_id != MessageId()) {
      auto m = get_message(d, d->last_message_id);
      CHECK(m != nullptr);
      LOG(INFO) << "Last message at " << m->date << " found";
      int64 last_message_order = get_dialog_order(m->message_id, m->date);
      if (last_message_order > new_order) {
        new_order = last_message_order;
      }
    } else if (d->delete_last_message_date > 0) {
      LOG(INFO) << "Deleted last " << d->deleted_last_message_id << " at " << d->delete_last_message_date << " found";
      int64 delete_order = get_dialog_order(d->deleted_last_message_id, d->delete_last_message_date);
      if (delete_order > new_order) {
        new_order = delete_order;
      }
    } else if (d->last_clear_history_date > 0) {
      LOG(INFO) << "Clear history at " << d->last_clear_history_date << " found";
      int64 clear_order = get_dialog_order(d->last_clear_history_message_id, d->last_clear_history_date);
      if (clear_order > new_order) {
        new_order = clear_order;
      }
    }
    if (d->pending_last_message_date > 0) {
      LOG(INFO) << "Pending last " << d->pending_last_message_id << " at " << d->pending_last_message_date << " found";
      int64 pending_order = get_dialog_order(d->pending_last_message_id, d->pending_last_message_date);
      if (pending_order > new_order) {
        new_order = pending_order;
      }
    }
    if (d->draft_message != nullptr) {
      LOG(INFO) << "Draft message at " << d->draft_message->date << " found";
      int64 draft_order = get_dialog_order(MessageId(), d->draft_message->date);
      if (draft_order > new_order) {
        new_order = draft_order;
      }
    }
    if (dialog_type == DialogType::Channel) {
      auto date = td_->contacts_manager_->get_channel_date(d->dialog_id.get_channel_id());
      LOG(INFO) << "Join of channel at " << date << " found";
      int64 join_order = get_dialog_order(MessageId(), date);
      if (join_order > new_order) {
        new_order = join_order;
      }
    }
    if (dialog_type == DialogType::SecretChat) {
      auto secret_chat_id = d->dialog_id.get_secret_chat_id();
      auto date = td_->contacts_manager_->get_secret_chat_date(secret_chat_id);
      auto state = td_->contacts_manager_->get_secret_chat_state(secret_chat_id);
      // do not return removed from the chat list closed secret chats
      if (date != 0 && (d->order != DEFAULT_ORDER || state != SecretChatState::Closed)) {
        LOG(INFO) << "Creation of secret chat at " << date << " found";
        int64 creation_order = get_dialog_order(MessageId(), date);
        if (creation_order > new_order) {
          new_order = creation_order;
        }
      }
    }
    if (d->dialog_id != sponsored_dialog_id_ && new_order == DEFAULT_ORDER && !d->is_empty) {
      // if there is no known messages in the dialog, just leave it where it is
      LOG(INFO) << "There is no known messages in the dialog";
      return;
    }
  }
  if (new_order == DEFAULT_ORDER && d->dialog_id == sponsored_dialog_id_) {
    new_order = SPONSORED_DIALOG_ORDER;
  }

  if (set_dialog_order(d, new_order, need_send_update_chat_order, is_loaded_from_database, source)) {
    on_dialog_updated(d->dialog_id, "update_dialog_pos");
  }
}

bool MessagesManager::set_dialog_order(Dialog *d, int64 new_order, bool need_send_update_chat_order,
                                       bool is_loaded_from_database, const char *source) {
  CHECK(d != nullptr);
  DialogId dialog_id = d->dialog_id;
  DialogDate old_date(d->order, dialog_id);
  DialogDate new_date(new_order, dialog_id);

  std::set<DialogDate> *ordered_dialogs_set = nullptr;
  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
    case DialogType::SecretChat:
      ordered_dialogs_set = &ordered_server_dialogs_;
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
      return false;
  }

  if (old_date == new_date) {
    if (new_order == DEFAULT_ORDER && ordered_dialogs_set->count(old_date) == 0) {
      ordered_dialogs_set->insert(new_date);
    }
    LOG(INFO) << "Dialog order is not changed: " << new_order << " from " << source;
    return false;
  }
  LOG(INFO) << "Update order of " << dialog_id << " from " << d->order << " to " << new_order << " from " << source;

  bool need_update = false;
  if (old_date <= last_dialog_date_) {
    if (ordered_dialogs_.erase(old_date) == 0) {
      UNREACHABLE();
    }
    need_update = true;
  }
  if (ordered_dialogs_set->erase(old_date) == 0) {
    LOG_IF(ERROR, d->order != DEFAULT_ORDER) << dialog_id << " not found in the chat list from " << source;
  }
  LOG_IF(ERROR, is_loaded_from_database && d->order != DEFAULT_ORDER)
      << dialog_id << " has non-default order after loading from database from " << source;

  int64 updated_to = 0;
  if (new_date <= last_dialog_date_) {
    ordered_dialogs_.insert(new_date);
    need_update = true;
    updated_to = new_order;
  }
  ordered_dialogs_set->insert(new_date);

  bool add_to_hints = (d->order == DEFAULT_ORDER);
  bool was_sponsored = (d->order == SPONSORED_DIALOG_ORDER);
  bool is_sponsored = (new_order == SPONSORED_DIALOG_ORDER);
  bool had_unread_counter = need_unread_counter(d->order);
  bool has_unread_counter = need_unread_counter(new_order);

  if (!is_loaded_from_database && had_unread_counter != has_unread_counter) {
    auto unread_count = d->server_unread_count + d->local_unread_count;
    const char *change_source = had_unread_counter ? "on_dialog_leave" : "on_dialog_join";
    if (unread_count != 0 && is_message_unread_count_inited_) {
      if (had_unread_counter) {
        unread_count = -unread_count;
      } else {
        CHECK(has_unread_counter);
      }

      unread_message_total_count_ += unread_count;
      if (is_dialog_muted(d)) {
        unread_message_muted_count_ += unread_count;
      }
      send_update_unread_message_count(dialog_id, true, change_source);
    }
    if ((unread_count != 0 || d->is_marked_as_unread) && is_dialog_unread_count_inited_) {
      int delta = 1;
      if (had_unread_counter) {
        delta = -1;
      } else {
        CHECK(has_unread_counter);
      }

      unread_dialog_total_count_ += delta;
      if (unread_count == 0 && d->is_marked_as_unread) {
        unread_dialog_marked_count_ += delta;
      }
      if (is_dialog_muted(d)) {
        unread_dialog_muted_count_ += delta;
        if (unread_count == 0 && d->is_marked_as_unread) {
          unread_dialog_muted_marked_count_ += delta;
        }
      }
      send_update_unread_chat_count(dialog_id, true, change_source);
    }

    auto dialog_type = dialog_id.get_type();
    if (dialog_type == DialogType::Channel && has_unread_counter && being_added_dialog_id_ != dialog_id) {
      repair_channel_server_unread_count(d);
      channel_get_difference_retry_timeout_.add_timeout_in(dialog_id.get(), 0.001);
    }
    if (dialog_type == DialogType::Channel && !has_unread_counter) {
      remove_all_dialog_notifications(dialog_id, d->message_notification_group, "set_dialog_order 1");
      remove_all_dialog_notifications(dialog_id, d->mention_notification_group, "set_dialog_order 2");
    }
  }

  d->order = new_order;

  if (add_to_hints) {
    update_dialogs_hints(d);
  }
  update_dialogs_hints_rating(d);

  if (was_sponsored != is_sponsored) {
    send_update_chat_is_sponsored(d);
    if (!is_loaded_from_database && is_sponsored) {
      // channel is sponsored only if user isn't a channel member
      remove_all_dialog_notifications(dialog_id, d->message_notification_group, "set_dialog_order 3");
      remove_all_dialog_notifications(dialog_id, d->mention_notification_group, "set_dialog_order 4");
    }
    need_update = false;
  }
  if (need_update && need_send_update_chat_order) {
    send_closure(G()->td(), &Td::send_update, make_tl_object<td_api::updateChatOrder>(dialog_id.get(), updated_to));
  }
  return true;
}

void MessagesManager::update_last_dialog_date() {
  auto old_last_dialog_date = last_dialog_date_;
  last_dialog_date_ = last_server_dialog_date_;  // std::min
  CHECK(old_last_dialog_date <= last_dialog_date_);

  LOG(INFO) << "Update last dialog date from " << old_last_dialog_date << " to " << last_dialog_date_;
  LOG(INFO) << "Know about " << ordered_server_dialogs_.size() << " chats";

  if (old_last_dialog_date != last_dialog_date_) {
    for (auto it = ordered_server_dialogs_.upper_bound(old_last_dialog_date);
         it != ordered_server_dialogs_.end() && *it <= last_dialog_date_; ++it) {
      auto dialog_id = it->get_dialog_id();
      auto d = get_dialog(dialog_id);
      CHECK(d != nullptr);
      ordered_dialogs_.insert(DialogDate(d->order, d->dialog_id));
      send_closure(G()->td(), &Td::send_update, make_tl_object<td_api::updateChatOrder>(d->dialog_id.get(), d->order));
    }

    if (last_dialog_date_ == MAX_DIALOG_DATE) {
      recalc_unread_count();
    }
  }

  if (G()->parameters().use_message_db && last_database_server_dialog_date_ < last_server_dialog_date_) {
    auto last_server_dialog_date_string = PSTRING() << last_server_dialog_date_.get_order() << ' '
                                                    << last_server_dialog_date_.get_dialog_id().get();
    G()->td_db()->get_binlog_pmc()->set("last_server_dialog_date", last_server_dialog_date_string);
    LOG(INFO) << "Save last server dialog date " << last_server_dialog_date_string;
    last_database_server_dialog_date_ = last_server_dialog_date_;
    last_loaded_database_dialog_date_ = last_server_dialog_date_;
  }
}

uint64 MessagesManager::get_sequence_dispatcher_id(DialogId dialog_id, MessageContentType message_content_type) {
  switch (message_content_type) {
    case MessageContentType::Animation:
    case MessageContentType::Audio:
    case MessageContentType::Document:
    case MessageContentType::Photo:
    case MessageContentType::Sticker:
    case MessageContentType::Video:
    case MessageContentType::VideoNote:
    case MessageContentType::VoiceNote:
      return static_cast<uint64>(dialog_id.get() * 2 + 1);
    default:
      return static_cast<uint64>(dialog_id.get() * 2 + 2);
  }
}

MessagesManager::Dialog *MessagesManager::get_dialog(DialogId dialog_id) {
  auto it = dialogs_.find(dialog_id);
  return it == dialogs_.end() ? nullptr : it->second.get();
}

const MessagesManager::Dialog *MessagesManager::get_dialog(DialogId dialog_id) const {
  auto it = dialogs_.find(dialog_id);
  return it == dialogs_.end() ? nullptr : it->second.get();
}

bool MessagesManager::have_dialog_force(DialogId dialog_id) {
  return loaded_dialogs_.count(dialog_id) > 0 || get_dialog_force(dialog_id) != nullptr;
}

MessagesManager::Dialog *MessagesManager::get_dialog_force(DialogId dialog_id) {
  // TODO remove most usages of that function, preload dialog asynchronously if possible
  auto it = dialogs_.find(dialog_id);
  if (it != dialogs_.end()) {
    return it->second.get();
  }

  if (!dialog_id.is_valid() || !G()->parameters().use_message_db || loaded_dialogs_.count(dialog_id) > 0) {
    return nullptr;
  }

  auto r_value = G()->td_db()->get_dialog_db_sync()->get_dialog(dialog_id);
  if (r_value.is_ok()) {
    LOG(INFO) << "Loaded " << dialog_id << " from database";
    auto d = on_load_dialog_from_database(dialog_id, r_value.move_as_ok());
    CHECK(d == nullptr || d->dialog_id == dialog_id) << d->dialog_id << " " << dialog_id;
    return d;
  } else {
    LOG(INFO) << "Failed to load " << dialog_id << " from database: " << r_value.error().message();
    return nullptr;
  }
}

unique_ptr<MessagesManager::Dialog> MessagesManager::parse_dialog(DialogId dialog_id, const BufferSlice &value) {
  LOG(INFO) << "Loaded " << dialog_id << " of size " << value.size() << " from database";
  auto d = make_unique<Dialog>();
  std::fill(d->message_count_by_index.begin(), d->message_count_by_index.end(), -1);

  loaded_dialogs_.insert(dialog_id);

  auto status = log_event_parse(*d, value.as_slice());
  if (status.is_error() || !d->dialog_id.is_valid() || d->dialog_id != dialog_id) {
    // can't happen unless database is broken, but has been seen in the wild
    // if dialog_id is invalid, we can't repair the dialog
    CHECK(dialog_id.is_valid()) << "Can't repair " << dialog_id << ' ' << d->dialog_id << ' ' << status << ' '
                                << format::as_hex_dump<4>(value.as_slice());

    LOG(ERROR) << "Repair broken " << dialog_id << ' ' << format::as_hex_dump<4>(value.as_slice());

    // just clean all known data about the dialog
    d = make_unique<Dialog>();
    std::fill(d->message_count_by_index.begin(), d->message_count_by_index.end(), -1);
    d->dialog_id = dialog_id;

    // and try to reget it from the server if possible
    have_dialog_info_force(dialog_id);
    if (have_input_peer(dialog_id, AccessRights::Read)) {
      if (dialog_id.get_type() != DialogType::SecretChat) {
        send_get_dialog_query(dialog_id, Auto());
      }
    } else {
      LOG(ERROR) << "Have no info about " << dialog_id << " to repair it";
    }
  }
  CHECK(dialog_id == d->dialog_id);

  Dependencies dependencies;
  add_dialog_dependencies(dependencies, dialog_id);
  if (d->messages != nullptr) {
    add_message_dependencies(dependencies, dialog_id, d->messages.get());
  }
  if (d->draft_message != nullptr) {
    add_formatted_text_dependencies(dependencies, &d->draft_message->input_message_text.text);
  }
  resolve_dependencies_force(dependencies);

  return d;
}

MessagesManager::Dialog *MessagesManager::on_load_dialog_from_database(DialogId dialog_id, const BufferSlice &value) {
  CHECK(G()->parameters().use_message_db);

  if (!dialog_id.is_valid()) {
    // hack
    LogEventParser dialog_id_parser(value.as_slice());
    int32 flags;
    parse(flags, dialog_id_parser);
    parse(dialog_id, dialog_id_parser);

    if (!dialog_id.is_valid()) {
      LOG(ERROR) << "Failed to parse dialog_id from blob. Database is broken";
      return nullptr;
    }
  }

  auto old_d = get_dialog(dialog_id);
  if (old_d != nullptr) {
    return old_d;
  }

  return add_new_dialog(parse_dialog(dialog_id, value), true);
}

void MessagesManager::load_notification_settings() {
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  if (!users_notification_settings_.is_synchronized) {
    send_get_scope_notification_settings_query(NotificationSettingsScope::Private, Promise<>());
  }
  if (!chats_notification_settings_.is_synchronized) {
    send_get_scope_notification_settings_query(NotificationSettingsScope::Group, Promise<>());
  }
}

string MessagesManager::get_channel_pts_key(DialogId dialog_id) {
  CHECK(dialog_id.get_type() == DialogType::Channel);
  auto channel_id = dialog_id.get_channel_id();
  return PSTRING() << "ch.p" << channel_id.get();
}

int32 MessagesManager::load_channel_pts(DialogId dialog_id) const {
  auto pts = to_integer<int32>(G()->td_db()->get_binlog_pmc()->get(get_channel_pts_key(dialog_id)));
  LOG(INFO) << "Load " << dialog_id << " pts = " << pts;
  return pts;
}

void MessagesManager::set_channel_pts(Dialog *d, int32 new_pts, const char *source) const {
  CHECK(d != nullptr);
  CHECK(d->dialog_id.get_type() == DialogType::Channel);

  LOG_IF(ERROR, running_get_channel_difference(d->dialog_id))
      << "Set pts of " << d->dialog_id << " to " << new_pts << " while running getChannelDifference";

  if (is_debug_message_op_enabled()) {
    d->debug_message_op.emplace_back(Dialog::MessageOp::SetPts, new_pts, source);
  }

  // TODO delete_first_messages support in channels
  if (new_pts == std::numeric_limits<int32>::max()) {
    LOG(ERROR) << "Update " << d->dialog_id << " pts to -1";
    G()->td_db()->get_binlog_pmc()->erase(get_channel_pts_key(d->dialog_id));
    d->pts = std::numeric_limits<int32>::max();
    return;
  }
  if (new_pts > d->pts || (0 < new_pts && new_pts < d->pts - 99999)) {  // pts can only go up or drop cardinally
    if (new_pts < d->pts - 99999) {
      LOG(WARNING) << "Pts of " << d->dialog_id << " decreases from " << d->pts << " to " << new_pts;
    } else {
      LOG(INFO) << "Update " << d->dialog_id << " pts to " << new_pts;
    }

    d->pts = new_pts;
    G()->td_db()->get_binlog_pmc()->set(get_channel_pts_key(d->dialog_id), to_string(new_pts));
  } else if (new_pts < d->pts) {
    LOG(ERROR) << "Receive wrong pts " << new_pts << " in " << d->dialog_id << " . Current pts is " << d->pts;
  }
}

bool MessagesManager::running_get_channel_difference(DialogId dialog_id) const {
  return active_get_channel_differencies_.count(dialog_id) > 0;
}

class MessagesManager::GetChannelDifferenceLogEvent {
 public:
  ChannelId channel_id;
  int64 access_hash;

  GetChannelDifferenceLogEvent() : channel_id(), access_hash() {
  }

  GetChannelDifferenceLogEvent(ChannelId channel_id, int64 access_hash)
      : channel_id(channel_id), access_hash(access_hash) {
  }

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(channel_id, storer);
    td::store(access_hash, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(channel_id, parser);
    td::parse(access_hash, parser);
  }
};

void MessagesManager::get_channel_difference(DialogId dialog_id, int32 pts, bool force, const char *source) {
  if (channel_get_difference_retry_timeout_.has_timeout(dialog_id.get())) {
    LOG(INFO) << "Skip running channels.getDifference for " << dialog_id << " from " << source
              << " because it is scheduled for later time";
    return;
  }

  auto input_channel = td_->contacts_manager_->get_input_channel(dialog_id.get_channel_id());
  if (input_channel == nullptr) {
    LOG(ERROR) << "Skip running channels.getDifference for " << dialog_id << " from " << source
               << " because have no info about the chat";
    after_get_channel_difference(dialog_id, false);
    return;
  }

  if (force && get_channel_difference_to_logevent_id_.count(dialog_id) == 0) {
    auto channel_id = dialog_id.get_channel_id();
    CHECK(input_channel->get_id() == telegram_api::inputChannel::ID);
    auto access_hash = static_cast<const telegram_api::inputChannel &>(*input_channel).access_hash_;
    auto logevent = GetChannelDifferenceLogEvent(channel_id, access_hash);
    auto storer = LogEventStorerImpl<GetChannelDifferenceLogEvent>(logevent);
    auto logevent_id = binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::GetChannelDifference, storer);

    get_channel_difference_to_logevent_id_.emplace(dialog_id, logevent_id);
  }

  return do_get_channel_difference(dialog_id, pts, force, std::move(input_channel), source);
}

void MessagesManager::do_get_channel_difference(DialogId dialog_id, int32 pts, bool force,
                                                tl_object_ptr<telegram_api::InputChannel> &&input_channel,
                                                const char *source) {
  auto inserted = active_get_channel_differencies_.emplace(dialog_id, source);
  if (!inserted.second) {
    LOG(INFO) << "Skip running channels.getDifference for " << dialog_id << " from " << source
              << " because it has already been run";
    return;
  }

  const Dialog *d = get_dialog(dialog_id);
  if (d != nullptr) {
    if (d->message_notification_group.group_id.is_valid()) {
      send_closure_later(td_->notification_manager_actor_, &NotificationManager::before_get_chat_difference,
                         d->message_notification_group.group_id);
    }
    if (d->mention_notification_group.group_id.is_valid()) {
      send_closure_later(td_->notification_manager_actor_, &NotificationManager::before_get_chat_difference,
                         d->mention_notification_group.group_id);
    }
  }

  int32 limit = td_->auth_manager_->is_bot() ? MAX_BOT_CHANNEL_DIFFERENCE : MAX_CHANNEL_DIFFERENCE;
  if (pts <= 0) {
    pts = 1;
    limit = MIN_CHANNEL_DIFFERENCE;
  }

  LOG(INFO) << "-----BEGIN GET CHANNEL DIFFERENCE----- for " << dialog_id << " with pts " << pts << " and limit "
            << limit << " from " << source;

  td_->create_handler<GetChannelDifferenceQuery>()->send(dialog_id, std::move(input_channel), pts, limit, force);
}

void MessagesManager::process_get_channel_difference_updates(
    DialogId dialog_id, vector<tl_object_ptr<telegram_api::Message>> &&new_messages,
    vector<tl_object_ptr<telegram_api::Update>> &&other_updates) {
  LOG(INFO) << "In get channel difference for " << dialog_id << " receive " << new_messages.size() << " messages and "
            << other_updates.size() << " other updates";
  for (auto &update : other_updates) {
    if (update->get_id() == telegram_api::updateMessageID::ID) {
      auto sent_message_update = move_tl_object_as<telegram_api::updateMessageID>(update);
      on_update_message_id(sent_message_update->random_id_, MessageId(ServerMessageId(sent_message_update->id_)),
                           "get_channel_difference");
      update = nullptr;
    }
  }

  // if last message is pretty old, we might have missed the update
  bool need_repair_unread_count =
      !new_messages.empty() && get_message_date(new_messages[0]) < G()->unix_time() - 2 * 86400;

  for (auto &message : new_messages) {
    on_get_message(std::move(message), true, true, true, true, "get channel difference");
  }

  for (auto &update : other_updates) {
    if (update != nullptr) {
      switch (update->get_id()) {
        case telegram_api::updateDeleteChannelMessages::ID:
          process_channel_update(std::move(update));
          break;
        case telegram_api::updateEditChannelMessage::ID:
          process_channel_update(std::move(update));
          break;
        default:
          LOG(ERROR) << "Unsupported update received in getChannelDifference: " << oneline(to_string(update));
          break;
      }
    }
  }
  CHECK(!running_get_channel_difference(dialog_id)) << '"' << active_get_channel_differencies_[dialog_id] << '"';

  if (need_repair_unread_count) {
    repair_channel_server_unread_count(get_dialog(dialog_id));
  }
}

void MessagesManager::on_get_channel_dialog(DialogId dialog_id, MessageId last_message_id,
                                            MessageId read_inbox_max_message_id, int32 server_unread_count,
                                            int32 unread_mention_count, MessageId read_outbox_max_message_id,
                                            vector<tl_object_ptr<telegram_api::Message>> &&messages) {
  std::unordered_map<FullMessageId, tl_object_ptr<telegram_api::Message>, FullMessageIdHash> full_message_id_to_message;
  for (auto &message : messages) {
    auto message_id = get_message_id(message);
    auto message_dialog_id = get_message_dialog_id(message);
    if (!message_dialog_id.is_valid()) {
      message_dialog_id = dialog_id;
    }
    auto full_message_id = FullMessageId(message_dialog_id, message_id);
    full_message_id_to_message[full_message_id] = std::move(message);
  }

  FullMessageId last_full_message_id(dialog_id, last_message_id);
  if (last_message_id.is_valid()) {
    if (full_message_id_to_message.count(last_full_message_id) == 0) {
      LOG(ERROR) << "Last " << last_message_id << " in " << dialog_id << " not found. Have:";
      for (auto &message : full_message_id_to_message) {
        LOG(ERROR) << to_string(message.second);
      }
      return;
    }
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  // TODO gaps support
  // There are many ways of handling a gap in a channel:
  // 1) Delete all known messages in the chat, begin from scratch. It is easy to implement, but suddenly disappearing
  //    messages looks awful for the user.
  // 2) Save all messages loaded in the memory until application restart, but delete all messages from database.
  //    Messages left in the memory must be lazily updated using calls to getHistory. It looks much smoothly for the
  //    user, he will need to redownload messages only after client restart. Unsynchronized messages left in the
  //    memory shouldn't be saved to database, results of getHistory and getMessage must be used to update state of
  //    deleted and edited messages left in the memory.
  // 3) Save all messages loaded in the memory and stored in the database without saving that some messages form
  //    continuous ranges. Messages in the database will be excluded from results of getChatHistory and
  //    searchChatMessages after application restart and will be available only through getMessage.
  //    Every message should still be checked using getHistory. It has more disadvantages over 2) than advantages.
  // 4) Save all messages with saving all data about continuous message ranges. Messages from the database may be used
  //    as results of getChatHistory and (if implemented continuous ranges support for searching shared media)
  //    searchChatMessages. The messages should still be lazily checked using getHistory, but they are still available
  //    offline. It is the best way for gaps support, but it is pretty hard to implement correctly.
  // It should be also noted that some messages like live location messages shouldn't be deleted.

  if (last_message_id.get() > d->last_new_message_id.get()) {
    // TODO properly support last_message_id.get() <= d->last_new_message_id.get()
    set_dialog_first_database_message_id(d, MessageId(), "on_get_channel_dialog");
    set_dialog_last_database_message_id(d, MessageId(), "on_get_channel_dialog");
    d->have_full_history = false;
    for (auto &first_message_id : d->first_database_message_id_by_index) {
      first_message_id = MessageId();
    }
  }
  std::fill(d->message_count_by_index.begin(), d->message_count_by_index.end(), -1);

  on_dialog_updated(dialog_id, "on_get_channel_dialog 10");

  // TODO properly support last_message_id.get() <= d->last_new_message_id.get()
  if (last_message_id.get() > d->last_new_message_id.get()) {  // if last message is really a new message
    d->last_new_message_id = MessageId();
    set_dialog_last_message_id(d, MessageId(), "on_get_channel_dialog 20");
    send_update_chat_last_message(d, "on_get_channel_dialog 30");
    auto added_full_message_id = on_get_message(std::move(full_message_id_to_message[last_full_message_id]), true, true,
                                                true, true, "channel difference too long");
    if (added_full_message_id.get_message_id().is_valid()) {
      if (added_full_message_id.get_message_id() == d->last_new_message_id) {
        CHECK(last_full_message_id == added_full_message_id);
        CHECK(d->last_message_id == d->last_new_message_id);
      } else {
        LOG(ERROR) << added_full_message_id << " doesn't became last new message";
        dump_debug_message_op(d, 2);
      }
    } else {
      set_dialog_last_new_message_id(d, last_message_id,
                                     "on_get_channel_dialog 40");  // skip updates about some messages
    }
  }

  if (d->server_unread_count != server_unread_count || d->last_read_inbox_message_id != read_inbox_max_message_id) {
    set_dialog_last_read_inbox_message_id(d, read_inbox_max_message_id, server_unread_count, d->local_unread_count,
                                          false, "on_get_channel_dialog 50");
  }
  if (d->unread_mention_count != unread_mention_count) {
    d->unread_mention_count = unread_mention_count;
    d->message_count_by_index[search_messages_filter_index(SearchMessagesFilter::UnreadMention)] =
        d->unread_mention_count;
    update_dialog_mention_notification_count(d);
    send_update_chat_unread_mention_count(d);
  }

  if (d->last_read_outbox_message_id != read_outbox_max_message_id) {
    set_dialog_last_read_outbox_message_id(d, read_outbox_max_message_id);
  }
}

void MessagesManager::on_get_channel_difference(
    DialogId dialog_id, int32 request_pts, int32 request_limit,
    tl_object_ptr<telegram_api::updates_ChannelDifference> &&difference_ptr) {
  LOG(INFO) << "----- END  GET CHANNEL DIFFERENCE----- for " << dialog_id;
  CHECK(active_get_channel_differencies_.count(dialog_id) == 1);
  active_get_channel_differencies_.erase(dialog_id);
  auto d = get_dialog_force(dialog_id);

  if (difference_ptr == nullptr) {
    bool have_access = have_input_peer(dialog_id, AccessRights::Read);
    if (have_access) {
      CHECK(d != nullptr);
      channel_get_difference_retry_timeout_.add_timeout_in(dialog_id.get(), d->retry_get_difference_timeout);
      d->retry_get_difference_timeout *= 2;
      if (d->retry_get_difference_timeout > 60) {
        d->retry_get_difference_timeout = Random::fast(60, 80);
      }
    } else {
      after_get_channel_difference(dialog_id, false);
    }
    return;
  }

  bool need_update_dialog_pos = false;
  if (d == nullptr) {
    d = add_dialog(dialog_id);
    need_update_dialog_pos = true;
  }

  int32 cur_pts = d->pts <= 0 ? 1 : d->pts;
  LOG_IF(ERROR, cur_pts != request_pts) << "Channel pts has changed from " << request_pts << " to " << d->pts << " in "
                                        << dialog_id << " during getChannelDifference";

  LOG(INFO) << "Receive result of getChannelDifference for " << dialog_id << " with pts = " << request_pts
            << " and limit = " << request_limit << ": " << to_string(difference_ptr);

  d->retry_get_difference_timeout = 1;

  bool is_final = true;
  int32 timeout = 0;
  switch (difference_ptr->get_id()) {
    case telegram_api::updates_channelDifferenceEmpty::ID: {
      auto difference = move_tl_object_as<telegram_api::updates_channelDifferenceEmpty>(difference_ptr);
      int32 flags = difference->flags_;
      is_final = (flags & CHANNEL_DIFFERENCE_FLAG_IS_FINAL) != 0;
      LOG_IF(ERROR, !is_final) << "Receive channelDifferenceEmpty as result of getChannelDifference with pts = "
                               << request_pts << " and limit = " << request_limit << " in " << dialog_id
                               << ", but it is not final";
      if (flags & CHANNEL_DIFFERENCE_FLAG_HAS_TIMEOUT) {
        timeout = difference->timeout_;
      }

      // bots can receive channelDifferenceEmpty with pts bigger than known pts
      LOG_IF(ERROR, request_pts != difference->pts_ && !td_->auth_manager_->is_bot())
          << "Receive channelDifferenceEmpty as result of getChannelDifference with pts = " << request_pts
          << " and limit = " << request_limit << " in " << dialog_id << ", but pts has changed from " << request_pts
          << " to " << difference->pts_;
      set_channel_pts(d, difference->pts_, "channel difference empty");
      break;
    }
    case telegram_api::updates_channelDifference::ID: {
      auto difference = move_tl_object_as<telegram_api::updates_channelDifference>(difference_ptr);

      int32 flags = difference->flags_;
      is_final = (flags & CHANNEL_DIFFERENCE_FLAG_IS_FINAL) != 0;
      if (flags & CHANNEL_DIFFERENCE_FLAG_HAS_TIMEOUT) {
        timeout = difference->timeout_;
      }

      auto new_pts = difference->pts_;
      if (request_pts >= new_pts) {
        LOG(ERROR) << "Receive channelDifference as result of getChannelDifference with pts = " << request_pts
                   << " and limit = " << request_limit << " in " << dialog_id << ", but pts has changed from " << d->pts
                   << " to " << new_pts << ". Difference: " << oneline(to_string(difference));
        new_pts = request_pts + 1;
      }

      td_->contacts_manager_->on_get_users(std::move(difference->users_));
      td_->contacts_manager_->on_get_chats(std::move(difference->chats_));

      process_get_channel_difference_updates(dialog_id, std::move(difference->new_messages_),
                                             std::move(difference->other_updates_));

      set_channel_pts(d, new_pts, "channel difference");
      break;
    }
    case telegram_api::updates_channelDifferenceTooLong::ID: {
      auto difference = move_tl_object_as<telegram_api::updates_channelDifferenceTooLong>(difference_ptr);

      int32 flags = difference->flags_;
      is_final = (flags & CHANNEL_DIFFERENCE_FLAG_IS_FINAL) != 0;
      if (flags & CHANNEL_DIFFERENCE_FLAG_HAS_TIMEOUT) {
        timeout = difference->timeout_;
      }

      auto new_pts = difference->pts_;
      if (request_pts + request_limit > new_pts) {
        LOG(ERROR) << "Receive channelDifferenceTooLong as result of getChannelDifference with pts = " << request_pts
                   << " and limit = " << request_limit << " in " << dialog_id << ", but pts has changed from " << d->pts
                   << " to " << new_pts << ". Difference: " << oneline(to_string(difference));
        if (request_pts >= new_pts) {
          new_pts = request_pts + 1;
        }
      }

      td_->contacts_manager_->on_get_users(std::move(difference->users_));
      td_->contacts_manager_->on_get_chats(std::move(difference->chats_));

      on_get_channel_dialog(dialog_id, MessageId(ServerMessageId(difference->top_message_)),
                            MessageId(ServerMessageId(difference->read_inbox_max_id_)), difference->unread_count_,
                            difference->unread_mentions_count_,
                            MessageId(ServerMessageId(difference->read_outbox_max_id_)),
                            std::move(difference->messages_));
      need_update_dialog_pos = true;

      set_channel_pts(d, new_pts, "channel difference too long");
      break;
    }
    default:
      UNREACHABLE();
  }

  if (need_update_dialog_pos) {
    update_dialog_pos(d, false, "on_get_channel_difference");
  }

  if (!is_final) {
    LOG_IF(ERROR, timeout > 0) << "Have timeout in not final ChannelDifference in " << dialog_id;
    get_channel_difference(dialog_id, d->pts, true, "on_get_channel_difference");
    return;
  }

  LOG_IF(ERROR, timeout == 0) << "Have no timeout in final ChannelDifference in " << dialog_id;
  if (timeout > 0 && d->is_opened) {
    channel_get_difference_timeout_.add_timeout_in(dialog_id.get(), timeout);
  }
  after_get_channel_difference(dialog_id, true);
}

void MessagesManager::after_get_channel_difference(DialogId dialog_id, bool success) {
  LOG(INFO) << "After " << (success ? "un" : "") << "successful get channel difference in " << dialog_id;
  CHECK(!running_get_channel_difference(dialog_id)) << '"' << active_get_channel_differencies_[dialog_id] << '"';

  auto logevent_it = get_channel_difference_to_logevent_id_.find(dialog_id);
  if (logevent_it != get_channel_difference_to_logevent_id_.end()) {
    binlog_erase(G()->td_db()->get_binlog(), logevent_it->second);
    get_channel_difference_to_logevent_id_.erase(logevent_it);
  }

  auto d = get_dialog(dialog_id);
  bool have_access = have_input_peer(dialog_id, AccessRights::Read);
  if (!have_access) {
    if (d != nullptr) {
      d->postponed_channel_updates.clear();
    }
  } else if (d->postponed_channel_updates.size()) {
    LOG(INFO) << "Begin to apply postponed channel updates";
    while (!d->postponed_channel_updates.empty()) {
      auto it = d->postponed_channel_updates.begin();
      auto update = std::move(it->second.update);
      auto update_pts = it->second.pts;
      auto update_pts_count = it->second.pts_count;
      d->postponed_channel_updates.erase(it);
      auto old_size = d->postponed_channel_updates.size();
      auto update_id = update->get_id();
      add_pending_channel_update(dialog_id, std::move(update), update_pts, update_pts_count,
                                 "apply postponed channel updates", true);
      if (d->postponed_channel_updates.size() != old_size || running_get_channel_difference(dialog_id)) {
        if (success && update_pts < d->pts + 10000 && update_pts_count == 1) {
          // if getChannelDifference was successful and update pts is near channel pts,
          // we hope that the update eventually can be applied
          LOG(INFO) << "Can't apply postponed channel updates";
        } else {
          // otherwise we protecting from getChannelDifference repeating calls by dropping pending updates
          LOG(ERROR) << "Failed to apply postponed updates of type " << update_id << " in " << dialog_id << " with pts "
                     << d->pts << ", update pts is " << update_pts << ", update pts count is " << update_pts_count;
          d->postponed_channel_updates.clear();
        }
        break;
      }
    }
    LOG(INFO) << "Finish to apply postponed channel updates";
  }

  if (d != nullptr) {
    if (d->message_notification_group.group_id.is_valid()) {
      send_closure_later(td_->notification_manager_actor_, &NotificationManager::after_get_chat_difference,
                         d->message_notification_group.group_id);
    }
    if (d->mention_notification_group.group_id.is_valid()) {
      send_closure_later(td_->notification_manager_actor_, &NotificationManager::after_get_chat_difference,
                         d->mention_notification_group.group_id);
    }
  }

  if (postponed_chat_read_inbox_updates_.erase(dialog_id) > 0) {
    send_update_chat_read_inbox(d, true, "after_get_channel_difference");
  }

  auto it_get_message_requests = postponed_get_message_requests_.find(dialog_id);
  if (it_get_message_requests != postponed_get_message_requests_.end()) {
    CHECK(d != nullptr);
    for (auto &request : it_get_message_requests->second) {
      auto message_id = request.first;
      LOG(INFO) << "Run postponed getMessage request for " << message_id << " in " << dialog_id;
      if (d->last_new_message_id != MessageId() && message_id.get() > d->last_new_message_id.get()) {
        // message will not be added to the dialog anyway, get channel difference didn't help
        request.second.set_value(Unit());
      } else {
        get_messages_from_server({FullMessageId{dialog_id, message_id}}, std::move(request.second));
      }
    }
    postponed_get_message_requests_.erase(it_get_message_requests);
  }

  auto it = pending_channel_on_get_dialogs_.find(dialog_id);
  if (it != pending_channel_on_get_dialogs_.end()) {
    LOG(INFO) << "Apply postponed results of channel getDialogs for " << dialog_id;
    PendingOnGetDialogs res = std::move(it->second);
    pending_channel_on_get_dialogs_.erase(it);

    on_get_dialogs(std::move(res.dialogs), res.total_count, std::move(res.messages), std::move(res.promise));
  }

  // TODO resend some messages
}

void MessagesManager::update_used_hashtags(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  if (m->via_bot_user_id.is_valid()) {
    return;
  }
  const FormattedText *text = get_message_content_text(m->content.get());
  if (text == nullptr || text->text.empty()) {
    return;
  }
  const unsigned char *ptr = Slice(text->text).ubegin();
  const unsigned char *end = Slice(text->text).uend();
  int32 utf16_pos = 0;
  for (auto &entity : text->entities) {
    if (entity.type != MessageEntity::Type::Hashtag) {
      continue;
    }
    while (utf16_pos < entity.offset && ptr < end) {
      utf16_pos += 1 + (ptr[0] >= 0xf0);
      ptr = next_utf8_unsafe(ptr, nullptr, "update_used_hashtags");
    }
    CHECK(utf16_pos == entity.offset);
    auto from = ptr;

    while (utf16_pos < entity.offset + entity.length && ptr < end) {
      utf16_pos += 1 + (ptr[0] >= 0xf0);
      ptr = next_utf8_unsafe(ptr, nullptr, "update_used_hashtags 2");
    }
    CHECK(utf16_pos == entity.offset + entity.length);
    auto to = ptr;

    send_closure(td_->hashtag_hints_, &HashtagHints::hashtag_used, Slice(from + 1, to).str());
  }
}

MessagesManager::Message *MessagesManager::continue_send_message(DialogId dialog_id, unique_ptr<Message> &&m,
                                                                 uint64 logevent_id) {
  CHECK(logevent_id != 0);
  Dialog *d = get_dialog_force(dialog_id);
  if (d == nullptr) {
    LOG(ERROR) << "Can't find " << dialog_id << " to resend a message";
    binlog_erase(G()->td_db()->get_binlog(), logevent_id);
    return nullptr;
  }

  auto now = G()->unix_time();

  m->message_id = get_next_yet_unsent_message_id(d);
  m->random_y = get_random_y(m->message_id);
  m->date = now;
  m->have_previous = true;
  m->have_next = true;

  LOG(INFO) << "Continue to send " << m->message_id << " to " << dialog_id << " initially sent at " << m->send_date
            << " from binlog";

  if (!have_input_peer(dialog_id, AccessRights::Read)) {
    binlog_erase(G()->td_db()->get_binlog(), logevent_id);
    return nullptr;
  }

  message_random_ids_.insert(m->random_id);

  bool need_update = false;
  bool need_update_dialog_pos = false;
  auto result_message =
      add_message_to_dialog(d, std::move(m), true, &need_update, &need_update_dialog_pos, "resend message");
  CHECK(result_message != nullptr);
  // CHECK(need_update_dialog_pos == true);

  send_update_new_message(d, result_message);
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "on_resend_message");
  }

  auto can_send_status = can_send_message(dialog_id);
  if (can_send_status.is_ok() && result_message->send_date < now - MAX_RESEND_DELAY &&
      dialog_id != get_my_dialog_id()) {
    can_send_status = Status::Error(400, "Message is too old to be re-sent automatically");
  }
  if (can_send_status.is_error()) {
    LOG(INFO) << "Can't resend a message to " << dialog_id << ": " << can_send_status.error();

    fail_send_message({dialog_id, result_message->message_id}, can_send_status.move_as_error());
    return nullptr;
  }

  return result_message;
}

void MessagesManager::on_binlog_events(vector<BinlogEvent> &&events) {
  for (auto &event : events) {
    switch (event.type_) {
      case LogEvent::HandlerType::SendMessage: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SendMessageLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id;
        auto m = std::move(log_event.m_out);
        m->send_message_logevent_id = event.id_;

        if (m->content->get_type() == MessageContentType::Unsupported) {
          LOG(ERROR) << "Message content is invalid: " << format::as_hex_dump<4>(Slice(event.data_));
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          continue;
        }

        Dependencies dependencies;
        add_dialog_dependencies(dependencies, dialog_id);
        add_message_dependencies(dependencies, dialog_id, m.get());
        resolve_dependencies_force(dependencies);

        m->content = dup_message_content(td_, dialog_id, m->content.get(), false);

        auto result_message = continue_send_message(dialog_id, std::move(m), event.id_);
        if (result_message != nullptr) {
          do_send_message(dialog_id, result_message);
        }
        break;
      }
      case LogEvent::HandlerType::SendBotStartMessage: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SendBotStartMessageLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id;
        auto m = std::move(log_event.m_out);
        m->send_message_logevent_id = event.id_;

        CHECK(m->content->get_type() == MessageContentType::Text);

        Dependencies dependencies;
        add_dialog_dependencies(dependencies, dialog_id);
        add_message_dependencies(dependencies, dialog_id, m.get());
        resolve_dependencies_force(dependencies);

        auto bot_user_id = log_event.bot_user_id;
        if (!td_->contacts_manager_->have_user_force(bot_user_id)) {
          LOG(ERROR) << "Can't find bot " << bot_user_id;
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          continue;
        }

        auto result_message = continue_send_message(dialog_id, std::move(m), event.id_);
        if (result_message != nullptr) {
          do_send_bot_start_message(bot_user_id, dialog_id, log_event.parameter, result_message);
        }
        break;
      }
      case LogEvent::HandlerType::SendInlineQueryResultMessage: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SendInlineQueryResultMessageLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id;
        auto m = std::move(log_event.m_out);
        m->send_message_logevent_id = event.id_;

        if (m->content->get_type() == MessageContentType::Unsupported) {
          LOG(ERROR) << "Message content is invalid: " << format::as_hex_dump<4>(Slice(event.data_));
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          continue;
        }

        Dependencies dependencies;
        add_dialog_dependencies(dependencies, dialog_id);
        add_message_dependencies(dependencies, dialog_id, m.get());
        resolve_dependencies_force(dependencies);

        m->content = dup_message_content(td_, dialog_id, m->content.get(), false);

        auto result_message = continue_send_message(dialog_id, std::move(m), event.id_);
        if (result_message != nullptr) {
          do_send_inline_query_result_message(dialog_id, result_message, log_event.query_id, log_event.result_id);
        }
        break;
      }
      case LogEvent::HandlerType::SendScreenshotTakenNotificationMessage: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SendScreenshotTakenNotificationMessageLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id;
        auto m = std::move(log_event.m_out);
        m->send_message_logevent_id = 0;  // to not allow event deletion by message deletion

        CHECK(m->content->get_type() == MessageContentType::ScreenshotTaken);

        Dependencies dependencies;
        add_dialog_dependencies(dependencies, dialog_id);
        add_message_dependencies(dependencies, dialog_id, m.get());
        resolve_dependencies_force(dependencies);

        auto result_message = continue_send_message(dialog_id, std::move(m), event.id_);
        if (result_message != nullptr) {
          do_send_screenshot_taken_notification_message(dialog_id, result_message, event.id_);
        }
        break;
      }
      case LogEvent::HandlerType::ForwardMessages: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ForwardMessagesLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto to_dialog_id = log_event.to_dialog_id;
        auto from_dialog_id = log_event.from_dialog_id;
        auto messages = std::move(log_event.messages_out);

        Dependencies dependencies;
        add_dialog_dependencies(dependencies, to_dialog_id);
        add_dialog_dependencies(dependencies, from_dialog_id);
        for (auto &m : messages) {
          add_message_dependencies(dependencies, to_dialog_id, m.get());
        }
        resolve_dependencies_force(dependencies);

        Dialog *to_dialog = get_dialog_force(to_dialog_id);
        if (to_dialog == nullptr) {
          LOG(ERROR) << "Can't find " << to_dialog_id << " to forward messages to";
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          continue;
        }
        Dialog *from_dialog = get_dialog_force(from_dialog_id);
        if (from_dialog == nullptr) {
          LOG(ERROR) << "Can't find " << from_dialog_id << " to forward messages from";
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          continue;
        }
        auto now = G()->unix_time();
        for (auto &m : messages) {
          m->message_id = get_next_yet_unsent_message_id(to_dialog);
          m->random_y = get_random_y(m->message_id);
          m->date = now;
          m->content = dup_message_content(td_, to_dialog_id, m->content.get(), true);
          m->have_previous = true;
          m->have_next = true;
        }

        if (!have_input_peer(from_dialog_id, AccessRights::Read) || can_send_message(to_dialog_id).is_error() ||
            messages.empty() ||
            (messages[0]->send_date < now - MAX_RESEND_DELAY && to_dialog_id != get_my_dialog_id())) {
          LOG(WARNING) << "Can't continue forwarding " << messages.size() << " message(s) to " << to_dialog_id;
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        LOG(INFO) << "Continue to forward " << messages.size() << " message(s) to " << to_dialog_id << " from binlog";

        bool need_update = false;
        bool need_update_dialog_pos = false;
        vector<Message *> forwarded_messages;
        for (auto &m : messages) {
          message_random_ids_.insert(m->random_id);
          forwarded_messages.push_back(add_message_to_dialog(to_dialog, std::move(m), true, &need_update,
                                                             &need_update_dialog_pos, "forward message again"));
          send_update_new_message(to_dialog, forwarded_messages.back());
        }
        if (need_update_dialog_pos) {
          send_update_chat_last_message(to_dialog, "on_reforward_message");
        }

        do_forward_messages(to_dialog_id, from_dialog_id, forwarded_messages, log_event.message_ids, event.id_);
        break;
      }
      case LogEvent::HandlerType::DeleteMessage: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        DeleteMessageLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();
        log_event.id_ = event.id_;

        Dialog *d = get_dialog_force(log_event.full_message_id_.get_dialog_id());
        if (d != nullptr) {
          d->deleted_message_ids.insert(log_event.full_message_id_.get_message_id());
        }

        do_delete_message_logevent(log_event);
        break;
      }
      case LogEvent::HandlerType::DeleteMessagesFromServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        DeleteMessagesFromServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id);
        if (d == nullptr || !have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        d->deleted_message_ids.insert(log_event.message_ids_.begin(), log_event.message_ids_.end());

        delete_messages_from_server(dialog_id, std::move(log_event.message_ids_), log_event.revoke_, event.id_, Auto());
        break;
      }
      case LogEvent::HandlerType::DeleteDialogHistoryFromServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        DeleteDialogHistoryFromServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id);
        if (d == nullptr || !have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        delete_dialog_history_from_server(dialog_id, log_event.max_message_id_, log_event.remove_from_dialog_list_,
                                          true, event.id_, Auto());
        break;
      }
      case LogEvent::HandlerType::DeleteAllChannelMessagesFromUserOnServer: {
        if (!G()->parameters().use_chat_info_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        DeleteAllChannelMessagesFromUserOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto channel_id = log_event.channel_id_;
        if (!td_->contacts_manager_->have_channel_force(channel_id)) {
          LOG(ERROR) << "Can't find " << channel_id;
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        auto user_id = log_event.user_id_;
        if (!td_->contacts_manager_->have_user_force(user_id)) {
          LOG(ERROR) << "Can't find user " << user_id;
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        delete_all_channel_messages_from_user_on_server(channel_id, user_id, event.id_, Auto());
        break;
      }
      case LogEvent::HandlerType::ReadHistoryOnServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ReadHistoryOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id);
        if (d == nullptr || !have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }
        if (d->read_history_logevent_id != 0) {
          // we need only latest read history event
          binlog_erase(G()->td_db()->get_binlog(), d->read_history_logevent_id);
        }
        d->read_history_logevent_id = event.id_;
        d->read_history_logevent_id_generation++;

        read_history_on_server_impl(dialog_id, log_event.max_message_id_);
        break;
      }
      case LogEvent::HandlerType::ReadHistoryInSecretChat: {
        ReadHistoryInSecretChatLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        CHECK(dialog_id.get_type() == DialogType::SecretChat);
        if (!td_->contacts_manager_->have_secret_chat_force(dialog_id.get_secret_chat_id())) {
          LOG(ERROR) << "Have no info about " << dialog_id;
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }
        force_create_dialog(dialog_id, "ReadHistoryInSecretChatLogEvent");
        Dialog *d = get_dialog(dialog_id);
        if (d == nullptr || !have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }
        if (d->read_history_logevent_id != 0) {
          // we need only latest read history event
          binlog_erase(G()->td_db()->get_binlog(), d->read_history_logevent_id);
        }
        d->read_history_logevent_id = event.id_;
        d->read_history_logevent_id_generation++;
        d->last_read_inbox_message_date = log_event.max_date_;

        read_history_on_server_impl(dialog_id, MessageId());
        break;
      }
      case LogEvent::HandlerType::ReadMessageContentsOnServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ReadMessageContentsOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id);
        if (d == nullptr || !have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        read_message_contents_on_server(dialog_id, std::move(log_event.message_ids_), event.id_);
        break;
      }
      case LogEvent::HandlerType::ReadAllDialogMentionsOnServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ReadAllDialogMentionsOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id);
        if (d == nullptr || !have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        read_all_dialog_mentions_on_server(dialog_id, event.id_, Promise<Unit>());
        break;
      }
      case LogEvent::HandlerType::ToggleDialogIsPinnedOnServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ToggleDialogIsPinnedOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id);
        if (d == nullptr || !have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        toggle_dialog_is_pinned_on_server(dialog_id, log_event.is_pinned_, event.id_);
        break;
      }
      case LogEvent::HandlerType::ReorderPinnedDialogsOnServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ReorderPinnedDialogsOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        vector<DialogId> dialog_ids;
        for (auto &dialog_id : log_event.dialog_ids_) {
          Dialog *d = get_dialog_force(dialog_id);
          if (d != nullptr && have_input_peer(dialog_id, AccessRights::Read)) {
            dialog_ids.push_back(dialog_id);
          }
        }
        if (dialog_ids.empty()) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        reorder_pinned_dialogs_on_server(dialog_ids, event.id_);
        break;
      }
      case LogEvent::HandlerType::ToggleDialogIsMarkedAsUnreadOnServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ToggleDialogIsMarkedAsUnreadOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id);
        if (d == nullptr || !have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        toggle_dialog_is_marked_as_unread_on_server(dialog_id, log_event.is_marked_as_unread_, event.id_);
        break;
      }
      case LogEvent::HandlerType::SaveDialogDraftMessageOnServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SaveDialogDraftMessageOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id);
        if (d == nullptr || !have_input_peer(dialog_id, AccessRights::Write)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }
        d->save_draft_message_logevent_id = event.id_;
        d->save_draft_message_logevent_id_generation++;

        save_dialog_draft_message_on_server(dialog_id);
        break;
      }
      case LogEvent::HandlerType::UpdateDialogNotificationSettingsOnServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        UpdateDialogNotificationSettingsOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id);
        if (d == nullptr || !have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }
        d->save_notification_settings_logevent_id = event.id_;
        d->save_notification_settings_logevent_id_generation++;

        update_dialog_notification_settings_on_server(dialog_id, true);
        break;
      }
      case LogEvent::HandlerType::UpdateScopeNotificationSettingsOnServer: {
        UpdateScopeNotificationSettingsOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        update_scope_notification_settings_on_server(log_event.scope_, event.id_);
        break;
      }
      case LogEvent::HandlerType::ResetAllNotificationSettingsOnServer: {
        ResetAllNotificationSettingsOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        reset_all_notification_settings_on_server(event.id_);
        break;
      }
      case LogEvent::HandlerType::ChangeDialogReportSpamStateOnServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ChangeDialogReportSpamStateOnServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id);
        if (d == nullptr || !have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        change_dialog_report_spam_state_on_server(dialog_id, log_event.is_spam_dialog_, event.id_, Promise<Unit>());
        break;
      }
      case LogEvent::HandlerType::GetDialogFromServer: {
        if (!G()->parameters().use_message_db) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        GetDialogFromServerLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dependencies dependencies;
        add_dialog_dependencies(dependencies, dialog_id);
        resolve_dependencies_force(dependencies);

        get_dialog_force(dialog_id);  // load it if exists

        if (!have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        send_get_dialog_query(dialog_id, Promise<Unit>(), event.id_);
        break;
      }
      case LogEvent::HandlerType::GetChannelDifference: {
        GetChannelDifferenceLogEvent log_event;
        log_event_parse(log_event, event.data_).ensure();

        DialogId dialog_id(log_event.channel_id);
        LOG(INFO) << "Continue to run getChannelDifference in " << dialog_id;
        get_channel_difference_to_logevent_id_.emplace(dialog_id, event.id_);
        do_get_channel_difference(
            dialog_id, load_channel_pts(dialog_id), true,
            telegram_api::make_object<telegram_api::inputChannel>(log_event.channel_id.get(), log_event.access_hash),
            "LogEvent::HandlerType::GetChannelDifference");
        break;
      }
      default:
        LOG(FATAL) << "Unsupported logevent type " << event.type_;
    }
  }
}

void MessagesManager::save_recently_found_dialogs() {
  if (recently_found_dialogs_loaded_ < 2) {
    return;
  }

  string value;
  for (auto &dialog_id : recently_found_dialog_ids_) {
    if (!value.empty()) {
      value += ',';
    }
    if (!G()->parameters().use_message_db) {
      // if there is no dialog database, prefer to save dialogs by username
      auto username = get_dialog_username(dialog_id);
      if (dialog_id.get_type() != DialogType::SecretChat && !username.empty()) {
        value += '@';
        value += username;
        continue;
      }
    }
    value += to_string(dialog_id.get());
  }
  G()->td_db()->get_binlog_pmc()->set("recently_found_dialog_usernames_and_ids", value);
}

bool MessagesManager::load_recently_found_dialogs(Promise<Unit> &promise) {
  if (recently_found_dialogs_loaded_ >= 2) {
    return true;
  }

  string found_dialogs_str = G()->td_db()->get_binlog_pmc()->get("recently_found_dialog_usernames_and_ids");
  if (found_dialogs_str.empty()) {
    recently_found_dialogs_loaded_ = 2;
    if (!recently_found_dialog_ids_.empty()) {
      save_recently_found_dialogs();
    }
    return true;
  }

  auto found_dialogs = full_split(found_dialogs_str, ',');
  if (recently_found_dialogs_loaded_ == 1 && resolve_recently_found_dialogs_multipromise_.promise_count() == 0) {
    // queries was sent and have already been finished
    auto newly_found_dialogs = std::move(recently_found_dialog_ids_);
    recently_found_dialog_ids_.clear();

    for (auto it = found_dialogs.rbegin(); it != found_dialogs.rend(); ++it) {
      if ((*it)[0] == '@') {
        auto dialog_id = resolve_dialog_username(it->substr(1));
        if (dialog_id.is_valid() && have_input_peer(dialog_id, AccessRights::Read)) {
          force_create_dialog(dialog_id, "recently found resolved dialog");
          add_recently_found_dialog_internal(dialog_id);
        }
      } else {
        auto dialog_id = DialogId(to_integer<int64>(*it));
        CHECK(dialog_id.is_valid());
        if (have_input_peer(dialog_id, AccessRights::Read)) {
          force_create_dialog(dialog_id, "recently found dialog");
          add_recently_found_dialog_internal(dialog_id);
        }
      }
    }
    for (auto it = newly_found_dialogs.rbegin(); it != newly_found_dialogs.rend(); ++it) {
      add_recently_found_dialog_internal(*it);
    }
    recently_found_dialogs_loaded_ = 2;
    if (!newly_found_dialogs.empty()) {
      save_recently_found_dialogs();
    }
    return true;
  }

  resolve_recently_found_dialogs_multipromise_.add_promise(std::move(promise));
  if (recently_found_dialogs_loaded_ == 0) {
    recently_found_dialogs_loaded_ = 1;

    resolve_recently_found_dialogs_multipromise_.set_ignore_errors(true);

    for (auto &found_dialog : found_dialogs) {
      if (found_dialog[0] == '@') {
        search_public_dialog(found_dialog, false, resolve_recently_found_dialogs_multipromise_.get_promise());
      }
    }
    if (G()->parameters().use_message_db) {
      for (auto &found_dialog : found_dialogs) {
        if (found_dialog[0] != '@') {
          auto dialog_id = DialogId(to_integer<int64>(found_dialog));
          CHECK(dialog_id.is_valid());
          // TODO use asynchronous load
          // get_dialog(dialog_id, resolve_recently_found_dialogs_multipromise_.get_promise());
          get_dialog_force(dialog_id);
        }
      }
      resolve_recently_found_dialogs_multipromise_.get_promise().set_value(Unit());
    } else {
      get_dialogs(MIN_DIALOG_DATE, MAX_GET_DIALOGS, false, resolve_recently_found_dialogs_multipromise_.get_promise());
      td_->contacts_manager_->search_contacts("", 1, resolve_recently_found_dialogs_multipromise_.get_promise());
    }
  }
  return false;
}

Status MessagesManager::add_recently_found_dialog(DialogId dialog_id) {
  if (!have_dialog_force(dialog_id)) {
    return Status::Error(5, "Chat not found");
  }
  if (add_recently_found_dialog_internal(dialog_id)) {
    save_recently_found_dialogs();
  }

  return Status::OK();
}

Status MessagesManager::remove_recently_found_dialog(DialogId dialog_id) {
  if (!have_dialog_force(dialog_id)) {
    return Status::Error(5, "Chat not found");
  }
  if (remove_recently_found_dialog_internal(dialog_id)) {
    save_recently_found_dialogs();
  }

  return Status::OK();
}

void MessagesManager::clear_recently_found_dialogs() {
  recently_found_dialogs_loaded_ = 2;
  if (recently_found_dialog_ids_.empty()) {
    return;
  }

  recently_found_dialog_ids_.clear();
  save_recently_found_dialogs();
}

bool MessagesManager::add_recently_found_dialog_internal(DialogId dialog_id) {
  CHECK(have_dialog(dialog_id));

  if (!recently_found_dialog_ids_.empty() && recently_found_dialog_ids_[0] == dialog_id) {
    return false;
  }

  // TODO create function
  auto it = std::find(recently_found_dialog_ids_.begin(), recently_found_dialog_ids_.end(), dialog_id);
  if (it == recently_found_dialog_ids_.end()) {
    if (narrow_cast<int32>(recently_found_dialog_ids_.size()) == MAX_RECENT_FOUND_DIALOGS) {
      CHECK(!recently_found_dialog_ids_.empty());
      recently_found_dialog_ids_.back() = dialog_id;
    } else {
      recently_found_dialog_ids_.push_back(dialog_id);
    }
    it = recently_found_dialog_ids_.end() - 1;
  }
  std::rotate(recently_found_dialog_ids_.begin(), it, it + 1);
  return true;
}

bool MessagesManager::remove_recently_found_dialog_internal(DialogId dialog_id) {
  CHECK(have_dialog(dialog_id));

  auto it = std::find(recently_found_dialog_ids_.begin(), recently_found_dialog_ids_.end(), dialog_id);
  if (it == recently_found_dialog_ids_.end()) {
    return false;
  }
  recently_found_dialog_ids_.erase(it);
  return true;
}

void MessagesManager::suffix_load_loop(Dialog *d) {
  if (d->suffix_load_has_query_) {
    return;
  }

  if (d->suffix_load_queries_.empty()) {
    return;
  }
  CHECK(!d->suffix_load_done_);

  auto dialog_id = d->dialog_id;
  auto from_message_id = d->suffix_load_first_message_id_;
  LOG(INFO) << "Send suffix load query in " << dialog_id << " from " << from_message_id;
  auto promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_id](Result<Unit> result) {
    send_closure(actor_id, &MessagesManager::suffix_load_query_ready, dialog_id);
  });
  d->suffix_load_has_query_ = true;
  d->suffix_load_query_message_id_ = from_message_id;
  if (from_message_id.is_valid()) {
    get_history(dialog_id, from_message_id, -1, 100, true, true, std::move(promise));
  } else {
    get_history_from_the_end(dialog_id, true, true, std::move(promise));
  }
}

void MessagesManager::suffix_load_update_first_message_id(Dialog *d) {
  if (!d->suffix_load_first_message_id_.is_valid()) {
    if (!d->last_message_id.is_valid()) {
      return;
    }

    d->suffix_load_first_message_id_ = d->last_message_id;
  }
  auto it = MessagesConstIterator(d, d->suffix_load_first_message_id_);
  CHECK(*it != nullptr);
  CHECK((*it)->message_id == d->suffix_load_first_message_id_);
  while ((*it)->have_previous) {
    --it;
  }
  d->suffix_load_first_message_id_ = (*it)->message_id;
}

void MessagesManager::suffix_load_query_ready(DialogId dialog_id) {
  LOG(INFO) << "Finished suffix load query in " << dialog_id;
  auto *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  bool is_unchanged = d->suffix_load_first_message_id_ == d->suffix_load_query_message_id_;
  suffix_load_update_first_message_id(d);
  if (is_unchanged && d->suffix_load_first_message_id_ == d->suffix_load_query_message_id_) {
    LOG(INFO) << "suffix_load done " << dialog_id;
    d->suffix_load_done_ = true;
  }
  d->suffix_load_has_query_ = false;

  // Remove ready queries
  auto *m = get_message_force(d, d->suffix_load_first_message_id_);
  auto ready_it = std::partition(d->suffix_load_queries_.begin(), d->suffix_load_queries_.end(),
                                 [&](auto &value) { return !(d->suffix_load_done_ || value.second(m)); });
  for (auto it = ready_it; it != d->suffix_load_queries_.end(); ++it) {
    it->first.set_value(Unit());
  }
  d->suffix_load_queries_.erase(ready_it, d->suffix_load_queries_.end());

  suffix_load_loop(d);
}

void MessagesManager::suffix_load_add_query(Dialog *d,
                                            std::pair<Promise<>, std::function<bool(const Message *)>> query) {
  suffix_load_update_first_message_id(d);
  auto *m = get_message_force(d, d->suffix_load_first_message_id_);
  if (d->suffix_load_done_ || query.second(m)) {
    query.first.set_value(Unit());
  } else {
    d->suffix_load_queries_.emplace_back(std::move(query));
    suffix_load_loop(d);
  }
}

void MessagesManager::suffix_load_till_date(Dialog *d, int32 date, Promise<> promise) {
  LOG(INFO) << "Load suffix of " << d->dialog_id << " till date " << date;
  auto condition = [date](const Message *m) { return m != nullptr && m->date < date; };
  suffix_load_add_query(d, std::make_pair(std::move(promise), std::move(condition)));
}

void MessagesManager::suffix_load_till_message_id(Dialog *d, MessageId message_id, Promise<> promise) {
  LOG(INFO) << "Load suffix of " << d->dialog_id << " till " << message_id;
  auto condition = [message_id](const Message *m) { return m != nullptr && m->message_id.get() < message_id.get(); };
  suffix_load_add_query(d, std::make_pair(std::move(promise), std::move(condition)));
}

Result<ServerMessageId> MessagesManager::get_invoice_message_id(FullMessageId full_message_id) {
  auto message = get_message_force(full_message_id);
  if (message == nullptr) {
    return Status::Error(5, "Message not found");
  }
  if (message->content->get_type() != MessageContentType::Invoice) {
    return Status::Error(5, "Message has no invoice");
  }
  auto message_id = full_message_id.get_message_id();
  if (!message_id.is_server()) {
    return Status::Error(5, "Wrong message identifier");
  }
  // TODO need to check that message is not forwarded

  return message_id.get_server_message_id();
}

void MessagesManager::get_payment_form(FullMessageId full_message_id,
                                       Promise<tl_object_ptr<td_api::paymentForm>> &&promise) {
  auto r_message_id = get_invoice_message_id(full_message_id);
  if (r_message_id.is_error()) {
    return promise.set_error(r_message_id.move_as_error());
  }

  td::get_payment_form(r_message_id.ok(), std::move(promise));
}

void MessagesManager::validate_order_info(FullMessageId full_message_id, tl_object_ptr<td_api::orderInfo> order_info,
                                          bool allow_save,
                                          Promise<tl_object_ptr<td_api::validatedOrderInfo>> &&promise) {
  auto r_message_id = get_invoice_message_id(full_message_id);
  if (r_message_id.is_error()) {
    return promise.set_error(r_message_id.move_as_error());
  }

  td::validate_order_info(r_message_id.ok(), std::move(order_info), allow_save, std::move(promise));
}

void MessagesManager::send_payment_form(FullMessageId full_message_id, const string &order_info_id,
                                        const string &shipping_option_id,
                                        const tl_object_ptr<td_api::InputCredentials> &credentials,
                                        Promise<tl_object_ptr<td_api::paymentResult>> &&promise) {
  auto r_message_id = get_invoice_message_id(full_message_id);
  if (r_message_id.is_error()) {
    return promise.set_error(r_message_id.move_as_error());
  }

  td::send_payment_form(r_message_id.ok(), order_info_id, shipping_option_id, credentials, std::move(promise));
}

void MessagesManager::get_payment_receipt(FullMessageId full_message_id,
                                          Promise<tl_object_ptr<td_api::paymentReceipt>> &&promise) {
  auto message = get_message_force(full_message_id);
  if (message == nullptr) {
    return promise.set_error(Status::Error(5, "Message not found"));
  }
  if (message->content->get_type() != MessageContentType::PaymentSuccessful) {
    return promise.set_error(Status::Error(5, "Message has wrong type"));
  }
  auto message_id = full_message_id.get_message_id();
  if (!message_id.is_server()) {
    return promise.set_error(Status::Error(5, "Wrong message identifier"));
  }

  td::get_payment_receipt(message_id.get_server_message_id(), std::move(promise));
}

void MessagesManager::on_get_sponsored_dialog_id(tl_object_ptr<telegram_api::Peer> peer,
                                                 vector<tl_object_ptr<telegram_api::User>> users,
                                                 vector<tl_object_ptr<telegram_api::Chat>> chats) {
  if (peer == nullptr) {
    set_sponsored_dialog_id(DialogId());
    return;
  }

  td_->contacts_manager_->on_get_users(std::move(users));
  td_->contacts_manager_->on_get_chats(std::move(chats));

  set_sponsored_dialog_id(DialogId(peer));
}

void MessagesManager::set_sponsored_dialog_id(DialogId dialog_id) {
  if (sponsored_dialog_id_ == dialog_id) {
    return;
  }

  if (sponsored_dialog_id_.is_valid()) {
    Dialog *d = get_dialog(sponsored_dialog_id_);
    CHECK(d != nullptr);
    sponsored_dialog_id_ = DialogId();
    update_dialog_pos(d, false, "delete_sponsored_dialog_id");
  }

  if (dialog_id.is_valid()) {
    force_create_dialog(dialog_id, "set_sponsored_dialog_id");

    Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr);
    sponsored_dialog_id_ = dialog_id;
    update_dialog_pos(d, false, "set_sponsored_dialog_id");
  }

  if (G()->parameters().use_message_db) {
    if (sponsored_dialog_id_.is_valid()) {
      G()->td_db()->get_binlog_pmc()->set("sponsored_dialog_id", to_string(sponsored_dialog_id_.get()));
    } else {
      G()->td_db()->get_binlog_pmc()->erase("sponsored_dialog_id");
    }
    LOG(INFO) << "Save sponsored " << sponsored_dialog_id_;
  }
}

td_api::object_ptr<td_api::updateUnreadMessageCount> MessagesManager::get_update_unread_message_count_object() const {
  CHECK(is_message_unread_count_inited_);
  int32 unread_count = unread_message_total_count_;
  int32 unread_unmuted_count = unread_message_total_count_ - unread_message_muted_count_;

  if (!include_sponsored_dialog_to_unread_count_ && sponsored_dialog_id_.is_valid()) {
    const Dialog *d = get_dialog(sponsored_dialog_id_);
    CHECK(d != nullptr);
    auto sponsored_unread_count = d->server_unread_count + d->local_unread_count;
    if (sponsored_unread_count != 0) {
      unread_count -= sponsored_unread_count;
      if (unread_count < 0) {
        unread_count = 0;
      }
      if (!is_dialog_muted(d)) {
        unread_unmuted_count -= sponsored_unread_count;
        if (unread_unmuted_count < 0) {
          unread_unmuted_count = 0;
        }
      }
    }
  }

  CHECK(unread_count >= 0);
  CHECK(unread_unmuted_count >= 0);
  return td_api::make_object<td_api::updateUnreadMessageCount>(unread_count, unread_unmuted_count);
}

td_api::object_ptr<td_api::updateUnreadChatCount> MessagesManager::get_update_unread_chat_count_object() const {
  CHECK(is_dialog_unread_count_inited_);
  int32 unread_count = unread_dialog_total_count_;
  int32 unread_unmuted_count = unread_count - unread_dialog_muted_count_;
  int32 unread_marked_count = unread_dialog_marked_count_;
  int32 unread_unmuted_marked_count = unread_marked_count - unread_dialog_muted_marked_count_;
  CHECK(unread_count >= 0);
  CHECK(unread_unmuted_count >= 0);
  CHECK(unread_marked_count >= 0);
  CHECK(unread_unmuted_marked_count >= 0);

  if (!include_sponsored_dialog_to_unread_count_ && sponsored_dialog_id_.is_valid()) {
    const Dialog *d = get_dialog(sponsored_dialog_id_);
    CHECK(d != nullptr);
    auto sponsored_unread_count = d->server_unread_count + d->local_unread_count;
    if (sponsored_unread_count != 0 || d->is_marked_as_unread) {
      unread_count = td::max(unread_count - 1, 0);
      if (sponsored_unread_count == 0 && d->is_marked_as_unread) {
        unread_marked_count = td::max(unread_marked_count - 1, 0);
      }
      if (!is_dialog_muted(d)) {
        unread_unmuted_count = td::max(unread_unmuted_count - 1, 0);
        if (sponsored_unread_count == 0 && d->is_marked_as_unread) {
          unread_unmuted_marked_count = td::max(unread_unmuted_marked_count - 1, 0);
        }
      }
    }
  }

  return td_api::make_object<td_api::updateUnreadChatCount>(unread_count, unread_unmuted_count, unread_marked_count,
                                                            unread_unmuted_marked_count);
}

void MessagesManager::get_current_state(vector<td_api::object_ptr<td_api::Update>> &updates) const {
  if (!td_->auth_manager_->is_bot()) {
    if (G()->parameters().use_message_db) {
      if (is_message_unread_count_inited_) {
        updates.push_back(get_update_unread_message_count_object());
      }
      if (is_dialog_unread_count_inited_) {
        updates.push_back(get_update_unread_chat_count_object());
      }
    }

    vector<NotificationSettingsScope> scopes{NotificationSettingsScope::Private, NotificationSettingsScope::Group};
    for (auto scope : scopes) {
      auto current_settings = get_scope_notification_settings(scope);
      CHECK(current_settings != nullptr);
      if (current_settings->is_synchronized) {
        updates.push_back(get_update_scope_notification_settings_object(scope));
      }
    }
  }

  vector<td_api::object_ptr<td_api::Update>> last_message_updates;
  for (auto &it : dialogs_) {
    const Dialog *d = it.second.get();
    auto update = td_api::make_object<td_api::updateNewChat>(get_chat_object(d));
    if (update->chat_->last_message_ != nullptr && update->chat_->last_message_->forward_info_ != nullptr) {
      DialogDate dialog_date(d->order, d->dialog_id);
      last_message_updates.push_back(td_api::make_object<td_api::updateChatLastMessage>(
          d->dialog_id.get(), std::move(update->chat_->last_message_),
          dialog_date <= last_dialog_date_ ? d->order : 0));
    }
    updates.push_back(std::move(update));
  }
  append(updates, std::move(last_message_updates));
}

}  // namespace td
