//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2024
//
// 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/AccessRights.h"
#include "td/telegram/AccountManager.h"
#include "td/telegram/AuthManager.h"
#include "td/telegram/BackgroundInfo.hpp"
#include "td/telegram/BlockListId.h"
#include "td/telegram/ChainId.h"
#include "td/telegram/ChannelType.h"
#include "td/telegram/ChatReactions.hpp"
#include "td/telegram/ContactsManager.h"
#include "td/telegram/Dependencies.h"
#include "td/telegram/DialogAction.h"
#include "td/telegram/DialogActionBar.h"
#include "td/telegram/DialogActionManager.h"
#include "td/telegram/DialogDb.h"
#include "td/telegram/DialogFilter.h"
#include "td/telegram/DialogFilterManager.h"
#include "td/telegram/DialogManager.h"
#include "td/telegram/DialogNotificationSettings.hpp"
#include "td/telegram/DialogParticipantManager.h"
#include "td/telegram/DownloadManager.h"
#include "td/telegram/DraftMessage.h"
#include "td/telegram/DraftMessage.hpp"
#include "td/telegram/FileReferenceManager.h"
#include "td/telegram/files/FileId.hpp"
#include "td/telegram/files/FileLocation.h"
#include "td/telegram/files/FileManager.h"
#include "td/telegram/files/FileType.h"
#include "td/telegram/ForumTopicManager.h"
#include "td/telegram/Global.h"
#include "td/telegram/GroupCallManager.h"
#include "td/telegram/InlineQueriesManager.h"
#include "td/telegram/InputDialogId.h"
#include "td/telegram/InputMessageText.h"
#include "td/telegram/LinkManager.h"
#include "td/telegram/Location.h"
#include "td/telegram/logevent/LogEvent.h"
#include "td/telegram/MessageContent.h"
#include "td/telegram/MessageDb.h"
#include "td/telegram/MessageEntity.h"
#include "td/telegram/MessageEntity.hpp"
#include "td/telegram/MessageForwardInfo.h"
#include "td/telegram/MessageForwardInfo.hpp"
#include "td/telegram/MessageOrigin.hpp"
#include "td/telegram/MessageReaction.h"
#include "td/telegram/MessageReaction.hpp"
#include "td/telegram/MessageReplyInfo.hpp"
#include "td/telegram/MessageSender.h"
#include "td/telegram/misc.h"
#include "td/telegram/net/DcId.h"
#include "td/telegram/NotificationGroupInfo.hpp"
#include "td/telegram/NotificationGroupType.h"
#include "td/telegram/NotificationManager.h"
#include "td/telegram/NotificationObjectId.h"
#include "td/telegram/NotificationSettingsManager.h"
#include "td/telegram/NotificationSound.h"
#include "td/telegram/NotificationType.h"
#include "td/telegram/OptionManager.h"
#include "td/telegram/Photo.h"
#include "td/telegram/PollId.h"
#include "td/telegram/PublicDialogType.h"
#include "td/telegram/QuickReplyManager.h"
#include "td/telegram/ReactionManager.h"
#include "td/telegram/RepliedMessageInfo.hpp"
#include "td/telegram/ReplyMarkup.h"
#include "td/telegram/ReplyMarkup.hpp"
#include "td/telegram/SavedMessagesManager.h"
#include "td/telegram/SecretChatsManager.h"
#include "td/telegram/SponsoredMessageManager.h"
#include "td/telegram/StickerType.h"
#include "td/telegram/StoryId.h"
#include "td/telegram/StoryManager.h"
#include "td/telegram/Td.h"
#include "td/telegram/TdDb.h"
#include "td/telegram/telegram_api.h"
#include "td/telegram/TopDialogCategory.h"
#include "td/telegram/TopDialogManager.h"
#include "td/telegram/TranslationManager.h"
#include "td/telegram/UpdatesManager.h"
#include "td/telegram/Usernames.h"
#include "td/telegram/Version.h"
#include "td/telegram/WebPageId.h"

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

#include "td/actor/SleepActor.h"

#include "td/utils/algorithm.h"
#include "td/utils/format.h"
#include "td/utils/logging.h"
#include "td/utils/misc.h"
#include "td/utils/Random.h"
#include "td/utils/Slice.h"
#include "td/utils/SliceBuilder.h"
#include "td/utils/Time.h"
#include "td/utils/tl_helpers.h"
#include "td/utils/utf8.h"

#include <algorithm>
#include <limits>
#include <tuple>
#include <type_traits>
#include <unordered_set>
#include <utility>

namespace td {

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

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

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

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

    td_->contacts_manager_->on_get_users(std::move(result->users_), "GetDialogQuery");
    td_->contacts_manager_->on_get_chats(std::move(result->chats_), "GetDialogQuery");
    td_->messages_manager_->on_get_dialogs(FolderId(), std::move(result->dialogs_), -1, std::move(result->messages_),
                                           PromiseCreator::lambda([actor_id = td_->messages_manager_actor_.get(),
                                                                   dialog_id = dialog_id_](Result<> result) {
                                             send_closure(actor_id, &MessagesManager::on_get_dialog_query_finished,
                                                          dialog_id,
                                                          result.is_error() ? result.move_as_error() : Status::OK());
                                           }));
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetDialogQuery");
    td_->messages_manager_->on_get_dialog_query_finished(dialog_id_, std::move(status));
  }
};

class GetPinnedDialogsQuery final : public Td::ResultHandler {
  FolderId folder_id_;
  Promise<Unit> promise_;

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

  NetQueryRef send(FolderId folder_id) {
    folder_id_ = folder_id;
    auto query =
        G()->net_query_creator().create(telegram_api::messages_getPinnedDialogs(folder_id.get()), {{folder_id}});
    auto result = query.get_weak();
    send_query(std::move(query));
    return result;
  }

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

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

    td_->contacts_manager_->on_get_users(std::move(result->users_), "GetPinnedDialogsQuery");
    td_->contacts_manager_->on_get_chats(std::move(result->chats_), "GetPinnedDialogsQuery");
    td_->messages_manager_->on_get_dialogs(folder_id_, std::move(result->dialogs_), -2, std::move(result->messages_),
                                           std::move(promise_));
  }

  void on_error(Status status) final {
    promise_.set_error(std::move(status));
  }
};

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

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getDialogUnreadMarks>(packet);
    if (result_ptr.is_error()) {
      return on_error(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(Status status) final {
    if (!G()->is_expected_error(status)) {
      LOG(ERROR) << "Receive error for GetDialogUnreadMarksQuery: " << status;
    }
    status.ignore();
  }
};

class GetDiscussionMessageQuery final : public Td::ResultHandler {
  Promise<MessageThreadInfo> promise_;
  DialogId dialog_id_;
  MessageId message_id_;
  DialogId expected_dialog_id_;
  MessageId expected_message_id_;

 public:
  explicit GetDiscussionMessageQuery(Promise<MessageThreadInfo> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId message_id, DialogId expected_dialog_id, MessageId expected_message_id) {
    dialog_id_ = dialog_id;
    message_id_ = message_id;
    expected_dialog_id_ = expected_dialog_id;
    expected_message_id_ = expected_message_id;
    CHECK(expected_dialog_id_.is_valid());
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);
    send_query(G()->net_query_creator().create(
        telegram_api::messages_getDiscussionMessage(std::move(input_peer), message_id.get_server_message_id().get())));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getDiscussionMessage>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    td_->messages_manager_->process_discussion_message(result_ptr.move_as_ok(), dialog_id_, message_id_,
                                                       expected_dialog_id_, expected_message_id_, std::move(promise_));
  }

  void on_error(Status status) final {
    td_->messages_manager_->on_get_message_error(dialog_id_, message_id_, status, "GetDiscussionMessageQuery");
    promise_.set_error(std::move(status));
  }
};

class GetMessagesQuery final : 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(telegram_api::messages_getMessages(std::move(message_ids))));
  }

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

    auto info = get_messages_info(td_, DialogId(), result_ptr.move_as_ok(), "GetMessagesQuery");
    LOG_IF(ERROR, info.is_channel_messages) << "Receive channel messages in GetMessagesQuery";
    td_->messages_manager_->on_get_messages(std::move(info.messages), info.is_channel_messages, false,
                                            std::move(promise_), "GetMessagesQuery");
  }

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

class GetChannelMessagesQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  ChannelId channel_id_;
  MessageId last_new_message_id_;
  bool can_be_inaccessible_ = false;

 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, MessageId last_new_message_id) {
    channel_id_ = channel_id;
    last_new_message_id_ = last_new_message_id;
    can_be_inaccessible_ = message_ids.size() == 1 && message_ids[0]->get_id() != telegram_api::inputMessageID::ID;
    CHECK(input_channel != nullptr);
    send_query(G()->net_query_creator().create(
        telegram_api::channels_getMessages(std::move(input_channel), std::move(message_ids))));
  }

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

    auto info = get_messages_info(td_, DialogId(channel_id_), result_ptr.move_as_ok(), "GetChannelMessagesQuery");
    LOG_IF(ERROR, !info.is_channel_messages) << "Receive ordinary messages in GetChannelMessagesQuery";
    // messages with invalid big identifiers can be received as messageEmpty
    // bots can receive messageEmpty because of their privacy mode
    if (last_new_message_id_.is_valid() && !td_->auth_manager_->is_bot()) {
      vector<MessageId> empty_message_ids;
      for (auto &message : info.messages) {
        if (message->get_id() == telegram_api::messageEmpty::ID) {
          auto message_id = MessageId::get_message_id(message, false);
          if (message_id.is_valid() && message_id <= last_new_message_id_) {
            empty_message_ids.push_back(message_id);
          }
        }
      }
      td_->messages_manager_->on_get_empty_messages(DialogId(channel_id_), empty_message_ids);
    }
    const char *source = can_be_inaccessible_ ? "GetRepliedChannelMessageQuery" : "GetChannelMessagesQuery";
    td_->messages_manager_->get_channel_difference_if_needed(
        DialogId(channel_id_), std::move(info),
        PromiseCreator::lambda([actor_id = td_->messages_manager_actor_.get(), source,
                                promise = std::move(promise_)](Result<MessagesInfo> &&result) mutable {
          if (result.is_error()) {
            promise.set_error(result.move_as_error());
          } else {
            auto info = result.move_as_ok();
            send_closure(actor_id, &MessagesManager::on_get_messages, std::move(info.messages),
                         info.is_channel_messages, false, std::move(promise), source);
          }
        }),
        source);
  }

  void on_error(Status status) final {
    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 GetScheduledMessagesQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

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

  void send(DialogId dialog_id, tl_object_ptr<telegram_api::InputPeer> &&input_peer, vector<int32> &&message_ids) {
    dialog_id_ = dialog_id;
    CHECK(input_peer != nullptr);
    send_query(G()->net_query_creator().create(
        telegram_api::messages_getScheduledMessages(std::move(input_peer), std::move(message_ids))));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getScheduledMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto info = get_messages_info(td_, dialog_id_, result_ptr.move_as_ok(), "GetScheduledMessagesQuery");
    LOG_IF(ERROR, info.is_channel_messages != (dialog_id_.get_type() == DialogType::Channel))
        << "Receive wrong messages constructor in GetScheduledMessagesQuery";
    td_->messages_manager_->on_get_messages(std::move(info.messages), info.is_channel_messages, true,
                                            std::move(promise_), "GetScheduledMessagesQuery");
  }

  void on_error(Status status) final {
    if (status.message() == "MESSAGE_IDS_EMPTY") {
      promise_.set_value(Unit());
      return;
    }
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetScheduledMessagesQuery");
    promise_.set_error(std::move(status));
  }
};

class UpdateDialogPinnedMessageQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  MessageId message_id_;

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

  void send(DialogId dialog_id, MessageId message_id, bool is_unpin, bool disable_notification, bool only_for_self) {
    dialog_id_ = dialog_id;
    message_id_ = message_id;
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      LOG(INFO) << "Can't update pinned message in " << dialog_id;
      return on_error(Status::Error(400, "Can't update pinned message"));
    }

    int32 flags = 0;
    if (disable_notification) {
      flags |= telegram_api::messages_updatePinnedMessage::SILENT_MASK;
    }
    if (is_unpin) {
      flags |= telegram_api::messages_updatePinnedMessage::UNPIN_MASK;
    }
    if (only_for_self) {
      flags |= telegram_api::messages_updatePinnedMessage::PM_ONESIDE_MASK;
    }
    send_query(G()->net_query_creator().create(
        telegram_api::messages_updatePinnedMessage(flags, false /*ignored*/, false /*ignored*/, false /*ignored*/,
                                                   std::move(input_peer), message_id.get_server_message_id().get())));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_updatePinnedMessage>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for UpdateDialogPinnedMessageQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

  void on_error(Status status) final {
    td_->messages_manager_->on_get_message_error(dialog_id_, message_id_, status, "UpdateDialogPinnedMessageQuery");
    promise_.set_error(std::move(status));
  }
};

class UnpinAllMessagesQuery final : public Td::ResultHandler {
  Promise<AffectedHistory> promise_;
  DialogId dialog_id_;
  MessageId top_thread_message_id_;

 public:
  explicit UnpinAllMessagesQuery(Promise<AffectedHistory> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId top_thread_message_id) {
    dialog_id_ = dialog_id;
    top_thread_message_id_ = top_thread_message_id;

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Write);
    if (input_peer == nullptr) {
      LOG(INFO) << "Can't unpin all messages in " << dialog_id_;
      return on_error(Status::Error(400, "Can't unpin all messages"));
    }

    int32 flags = 0;
    if (top_thread_message_id.is_valid()) {
      flags |= telegram_api::messages_unpinAllMessages::TOP_MSG_ID_MASK;
    }
    send_query(G()->net_query_creator().create(telegram_api::messages_unpinAllMessages(
        flags, std::move(input_peer), top_thread_message_id.get_server_message_id().get())));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_unpinAllMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    promise_.set_value(AffectedHistory(result_ptr.move_as_ok()));
  }

  void on_error(Status status) final {
    td_->messages_manager_->on_get_message_error(dialog_id_, top_thread_message_id_, status, "UnpinAllMessagesQuery");
    promise_.set_error(std::move(status));
  }
};

class GetOutboxReadDateQuery final : public Td::ResultHandler {
  Promise<td_api::object_ptr<td_api::MessageReadDate>> promise_;
  DialogId dialog_id_;
  MessageId message_id_;

 public:
  explicit GetOutboxReadDateQuery(Promise<td_api::object_ptr<td_api::MessageReadDate>> &&promise)
      : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId message_id) {
    dialog_id_ = dialog_id;
    message_id_ = message_id;
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);
    send_query(G()->net_query_creator().create(
        telegram_api::messages_getOutboxReadDate(std::move(input_peer), message_id.get_server_message_id().get())));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getOutboxReadDate>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    promise_.set_value(td_api::make_object<td_api::messageReadDateRead>(ptr->date_));
  }

  void on_error(Status status) final {
    if (status.message() == "USER_PRIVACY_RESTRICTED") {
      return promise_.set_value(td_api::make_object<td_api::messageReadDateUserPrivacyRestricted>());
    }
    if (status.message() == "YOUR_PRIVACY_RESTRICTED") {
      return promise_.set_value(td_api::make_object<td_api::messageReadDateMyPrivacyRestricted>());
    }
    if (status.message() == "MESSAGE_TOO_OLD") {
      return promise_.set_value(td_api::make_object<td_api::messageReadDateTooOld>());
    }

    td_->messages_manager_->on_get_message_error(dialog_id_, message_id_, status, "GetOutboxReadDateQuery");
    promise_.set_error(std::move(status));
  }
};

class GetMessageReadParticipantsQuery final : public Td::ResultHandler {
  Promise<MessageViewers> promise_;
  DialogId dialog_id_;
  MessageId message_id_;

 public:
  explicit GetMessageReadParticipantsQuery(Promise<MessageViewers> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId message_id) {
    dialog_id_ = dialog_id;
    message_id_ = message_id;
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);
    send_query(G()->net_query_creator().create(telegram_api::messages_getMessageReadParticipants(
        std::move(input_peer), message_id.get_server_message_id().get())));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getMessageReadParticipants>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    promise_.set_value(MessageViewers(result_ptr.move_as_ok()));
  }

  void on_error(Status status) final {
    td_->messages_manager_->on_get_message_error(dialog_id_, message_id_, status, "GetMessageReadParticipantsQuery");
    promise_.set_error(std::move(status));
  }
};

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

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

  void send(ChannelId channel_id, MessageId message_id, bool for_group, bool ignore_result) {
    channel_id_ = channel_id;
    message_id_ = message_id;
    for_group_ = for_group;
    ignore_result_ = ignore_result;
    auto input_channel = td_->contacts_manager_->get_input_channel(channel_id);
    if (input_channel == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }
    int32 flags = 0;
    if (for_group) {
      flags |= telegram_api::channels_exportMessageLink::GROUPED_MASK;
    }
    send_query(G()->net_query_creator().create(
        telegram_api::channels_exportMessageLink(flags, false /*ignored*/, false /*ignored*/, std::move(input_channel),
                                                 message_id.get_server_message_id().get())));
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(DEBUG) << "Receive result for ExportChannelMessageLinkQuery: " << to_string(ptr);
    if (!ignore_result_) {
      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(Status status) final {
    if (!ignore_result_) {
      td_->messages_manager_->on_get_message_error(DialogId(channel_id_), message_id_, status,
                                                   "ExportChannelMessageLinkQuery");
    }
    promise_.set_error(std::move(status));
  }
};

class GetDialogListQuery final : public Td::ResultHandler {
  FolderId folder_id_;
  Promise<Unit> promise_;

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

  void send(FolderId folder_id, int32 offset_date, ServerMessageId offset_message_id, DialogId offset_dialog_id,
            int32 limit) {
    folder_id_ = folder_id;
    auto input_peer = DialogManager::get_input_peer_force(offset_dialog_id);
    CHECK(input_peer != nullptr);

    int32 flags =
        telegram_api::messages_getDialogs::EXCLUDE_PINNED_MASK | telegram_api::messages_getDialogs::FOLDER_ID_MASK;
    send_query(G()->net_query_creator().create(
        telegram_api::messages_getDialogs(flags, false /*ignored*/, folder_id.get(), offset_date,
                                          offset_message_id.get(), std::move(input_peer), limit, 0),
        {{folder_id}}));
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive chats from chat list of " << folder_id_ << ": " << 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_users(std::move(dialogs->users_), "GetDialogListQuery");
        td_->contacts_manager_->on_get_chats(std::move(dialogs->chats_), "GetDialogListQuery");
        td_->messages_manager_->on_get_dialogs(folder_id_, 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_users(std::move(dialogs->users_), "GetDialogListQuery slice");
        td_->contacts_manager_->on_get_chats(std::move(dialogs->chats_), "GetDialogListQuery slice");
        td_->messages_manager_->on_get_dialogs(folder_id_, 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(Status::Error(500, "Receive wrong server response messages.dialogsNotModified"));
      default:
        UNREACHABLE();
    }
  }

  void on_error(Status status) final {
    promise_.set_error(std::move(status));
  }
};

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

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

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::contacts_search>(packet);
    if (result_ptr.is_error()) {
      return on_error(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_users(std::move(dialogs->users_), "SearchPublicDialogsQuery");
    td_->contacts_manager_->on_get_chats(std::move(dialogs->chats_), "SearchPublicDialogsQuery");
    td_->messages_manager_->on_get_public_dialogs_search_result(query_, std::move(dialogs->my_results_),
                                                                std::move(dialogs->results_));
  }

  void on_error(Status status) final {
    if (!G()->is_expected_error(status)) {
      if (status.message() == "QUERY_TOO_SHORT") {
        return td_->messages_manager_->on_get_public_dialogs_search_result(query_, {}, {});
      }
      LOG(ERROR) << "Receive error for SearchPublicDialogsQuery: " << status;
    }
    td_->messages_manager_->on_failed_public_dialogs_search(query_, std::move(status));
  }
};

class GetBlockedDialogsQuery final : public Td::ResultHandler {
  Promise<td_api::object_ptr<td_api::messageSenders>> promise_;
  int32 offset_;
  int32 limit_;

 public:
  explicit GetBlockedDialogsQuery(Promise<td_api::object_ptr<td_api::messageSenders>> &&promise)
      : promise_(std::move(promise)) {
  }

  void send(BlockListId block_list_id, int32 offset, int32 limit) {
    offset_ = offset;
    limit_ = limit;

    int32 flags = 0;
    if (block_list_id == BlockListId::stories()) {
      flags |= telegram_api::contacts_getBlocked::MY_STORIES_FROM_MASK;
    }

    send_query(
        G()->net_query_creator().create(telegram_api::contacts_getBlocked(flags, false /*ignored*/, offset, limit)));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::contacts_getBlocked>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

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

    switch (ptr->get_id()) {
      case telegram_api::contacts_blocked::ID: {
        auto blocked_peers = move_tl_object_as<telegram_api::contacts_blocked>(ptr);

        td_->contacts_manager_->on_get_users(std::move(blocked_peers->users_), "GetBlockedDialogsQuery");
        td_->contacts_manager_->on_get_chats(std::move(blocked_peers->chats_), "GetBlockedDialogsQuery");
        td_->messages_manager_->on_get_blocked_dialogs(offset_, limit_,
                                                       narrow_cast<int32>(blocked_peers->blocked_.size()),
                                                       std::move(blocked_peers->blocked_), std::move(promise_));
        break;
      }
      case telegram_api::contacts_blockedSlice::ID: {
        auto blocked_peers = move_tl_object_as<telegram_api::contacts_blockedSlice>(ptr);

        td_->contacts_manager_->on_get_users(std::move(blocked_peers->users_), "GetBlockedDialogsQuery slice");
        td_->contacts_manager_->on_get_chats(std::move(blocked_peers->chats_), "GetBlockedDialogsQuery slice");
        td_->messages_manager_->on_get_blocked_dialogs(offset_, limit_, blocked_peers->count_,
                                                       std::move(blocked_peers->blocked_), std::move(promise_));
        break;
      }
      default:
        UNREACHABLE();
    }
  }

  void on_error(Status status) final {
    promise_.set_error(std::move(status));
  }
};

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

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

  void send(DialogId dialog_id, const ChatReactions &available_reactions) {
    dialog_id_ = dialog_id;
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }
    send_query(G()->net_query_creator().create(telegram_api::messages_setChatAvailableReactions(
        std::move(input_peer), available_reactions.get_input_chat_reactions())));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_setChatAvailableReactions>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SetChatAvailableReactionsQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

  void on_error(Status status) final {
    if (status.message() == "CHAT_NOT_MODIFIED") {
      if (!td_->auth_manager_->is_bot()) {
        promise_.set_value(Unit());
        return;
      }
    } else {
      td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "SetChatAvailableReactionsQuery");
      td_->dialog_manager_->reload_dialog_info_full(dialog_id_, "SetChatAvailableReactionsQuery");
    }
    promise_.set_error(std::move(status));
  }
};

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

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

  void send(DialogId dialog_id, const string &theme_name) {
    dialog_id_ = dialog_id;
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write);
    CHECK(input_peer != nullptr);
    send_query(G()->net_query_creator().create(telegram_api::messages_setChatTheme(std::move(input_peer), theme_name)));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_setChatTheme>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SetChatThemeQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

  void on_error(Status status) final {
    if (status.message() == "CHAT_NOT_MODIFIED") {
      if (!td_->auth_manager_->is_bot()) {
        promise_.set_value(Unit());
        return;
      }
    } else {
      td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "SetChatThemeQuery");
    }
    promise_.set_error(std::move(status));
  }
};

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

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

  void send(DialogId dialog_id, int32 period) {
    dialog_id_ = dialog_id;

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

    send_query(G()->net_query_creator().create(telegram_api::messages_setHistoryTTL(std::move(input_peer), period)));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_setHistoryTTL>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SetHistoryTtlQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

  void on_error(Status status) final {
    if (status.message() == "CHAT_NOT_MODIFIED") {
      if (!td_->auth_manager_->is_bot()) {
        promise_.set_value(Unit());
        return;
      }
    } else {
      td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "SetHistoryTtlQuery");
    }
    promise_.set_error(std::move(status));
  }
};

class ToggleDialogPinQuery final : 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_->dialog_manager_->get_input_dialog_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

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

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

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

    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ToggleDialogPinQuery")) {
      LOG(ERROR) << "Receive error for ToggleDialogPinQuery: " << status;
    }
    td_->messages_manager_->on_update_pinned_dialogs(FolderId::main());
    td_->messages_manager_->on_update_pinned_dialogs(FolderId::archive());
    promise_.set_error(std::move(status));
  }
};

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

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

  void send(FolderId folder_id, const vector<DialogId> &dialog_ids) {
    folder_id_ = folder_id;
    int32 flags = telegram_api::messages_reorderPinnedDialogs::FORCE_MASK;
    send_query(G()->net_query_creator().create(telegram_api::messages_reorderPinnedDialogs(
        flags, true /*ignored*/, folder_id.get(),
        td_->dialog_manager_->get_input_dialog_peers(dialog_ids, AccessRights::Read))));
  }

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

    bool result = result_ptr.move_as_ok();
    if (!result) {
      return on_error(Status::Error(400, "Result is false"));
    }
    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    if (!G()->is_expected_error(status)) {
      LOG(ERROR) << "Receive error for ReorderPinnedDialogsQuery: " << status;
    }
    td_->messages_manager_->on_update_pinned_dialogs(folder_id_);
    promise_.set_error(std::move(status));
  }
};

class ToggleViewForumAsMessagesQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  bool view_as_messages_;

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

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

    CHECK(dialog_id.get_type() == DialogType::Channel);
    auto input_channel = td_->contacts_manager_->get_input_channel(dialog_id.get_channel_id());
    CHECK(input_channel != nullptr);
    send_query(G()->net_query_creator().create(
        telegram_api::channels_toggleViewForumAsMessages(std::move(input_channel), view_as_messages), {{dialog_id}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::channels_toggleViewForumAsMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for ToggleViewForumAsMessagesQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ToggleViewForumAsMessagesQuery")) {
      LOG(ERROR) << "Receive error for ToggleViewForumAsMessagesQuery: " << status;
    }
    if (!G()->close_flag()) {
      td_->messages_manager_->on_update_dialog_view_as_messages(dialog_id_, !view_as_messages_);
    }
    promise_.set_error(std::move(status));
  }
};

class ToggleDialogUnreadMarkQuery final : 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_->dialog_manager_->get_input_dialog_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

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

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

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

    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ToggleDialogUnreadMarkQuery")) {
      LOG(ERROR) << "Receive error for ToggleDialogUnreadMarkQuery: " << status;
    }
    if (!G()->close_flag()) {
      td_->messages_manager_->on_update_dialog_is_marked_as_unread(dialog_id_, !is_marked_as_unread_);
    }
    promise_.set_error(std::move(status));
  }
};

class ToggleDialogTranslationsQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  bool is_translatable_;

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

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

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    int32 flags = 0;
    if (!is_translatable) {
      flags |= telegram_api::messages_togglePeerTranslations::DISABLED_MASK;
    }
    send_query(G()->net_query_creator().create(
        telegram_api::messages_togglePeerTranslations(flags, false /*ignored*/, std::move(input_peer)), {{dialog_id}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_togglePeerTranslations>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

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

    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ToggleDialogTranslationsQuery")) {
      LOG(ERROR) << "Receive error for ToggleDialogTranslationsQuery: " << status;
    }
    if (!G()->close_flag()) {
      td_->messages_manager_->on_update_dialog_is_translatable(dialog_id_, !is_translatable_);
    }
    promise_.set_error(std::move(status));
  }
};

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

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

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

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Know);
    CHECK(input_peer != nullptr && input_peer->get_id() != telegram_api::inputPeerEmpty::ID);

    int32 flags = 0;
    if (is_blocked_for_stories) {
      flags |= telegram_api::contacts_block::MY_STORIES_FROM_MASK;
    }
    vector<ChainId> chain_ids{{dialog_id, MessageContentType::Photo}, {dialog_id, MessageContentType::Text}};
    auto query =
        is_blocked || is_blocked_for_stories
            ? G()->net_query_creator().create(
                  telegram_api::contacts_block(flags, false /*ignored*/, std::move(input_peer)), std::move(chain_ids))
            : G()->net_query_creator().create(
                  telegram_api::contacts_unblock(flags, false /*ignored*/, std::move(input_peer)),
                  std::move(chain_ids));
    send_query(std::move(query));
  }

  void on_result(BufferSlice packet) final {
    static_assert(
        std::is_same<telegram_api::contacts_block::ReturnType, telegram_api::contacts_unblock::ReturnType>::value, "");
    auto result_ptr = fetch_result<telegram_api::contacts_block>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    bool result = result_ptr.ok();
    LOG_IF(WARNING, !result) << "Block/Unblock " << dialog_id_ << " has failed";

    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ToggleDialogIsBlockedQuery")) {
      LOG(ERROR) << "Receive error for ToggleDialogIsBlockedQuery: " << status;
    }
    if (!G()->close_flag()) {
      td_->dialog_manager_->get_dialog_info_full(dialog_id_, Auto(), "ToggleDialogIsBlockedQuery");
      td_->messages_manager_->reget_dialog_action_bar(dialog_id_, "ToggleDialogIsBlockedQuery");
    }
    promise_.set_error(std::move(status));
  }
};

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

 public:
  void send(DialogId dialog_id, vector<MessageId> &&message_ids, bool increment_view_counter) {
    dialog_id_ = dialog_id;
    message_ids_ = std::move(message_ids);

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    send_query(G()->net_query_creator().create(telegram_api::messages_getMessagesViews(
        std::move(input_peer), MessageId::get_server_message_ids(message_ids_), increment_view_counter)));
  }

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

    auto result = result_ptr.move_as_ok();
    auto interaction_infos = std::move(result->views_);
    if (message_ids_.size() != interaction_infos.size()) {
      return on_error(Status::Error(500, "Wrong number of message views returned"));
    }
    td_->contacts_manager_->on_get_users(std::move(result->users_), "GetMessagesViewsQuery");
    td_->contacts_manager_->on_get_chats(std::move(result->chats_), "GetMessagesViewsQuery");
    for (size_t i = 0; i < message_ids_.size(); i++) {
      MessageFullId message_full_id{dialog_id_, message_ids_[i]};

      auto *info = interaction_infos[i].get();
      auto flags = info->flags_;
      auto view_count = (flags & telegram_api::messageViews::VIEWS_MASK) != 0 ? info->views_ : 0;
      auto forward_count = (flags & telegram_api::messageViews::FORWARDS_MASK) != 0 ? info->forwards_ : 0;
      td_->messages_manager_->on_update_message_interaction_info(message_full_id, view_count, forward_count, true,
                                                                 std::move(info->replies_));
    }
    td_->messages_manager_->finish_get_message_views(dialog_id_, message_ids_);
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetMessagesViewsQuery")) {
      LOG(ERROR) << "Receive error for GetMessagesViewsQuery: " << status;
    }
    td_->messages_manager_->finish_get_message_views(dialog_id_, message_ids_);
  }
};

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

 public:
  void send(DialogId dialog_id, vector<MessageId> &&message_ids) {
    dialog_id_ = dialog_id;
    message_ids_ = std::move(message_ids);

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    send_query(G()->net_query_creator().create(telegram_api::messages_getExtendedMedia(
        std::move(input_peer), MessageId::get_server_message_ids(message_ids_))));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getExtendedMedia>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for GetExtendedMediaQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), Promise<Unit>());
    td_->messages_manager_->finish_get_message_extended_media(dialog_id_, message_ids_);
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetExtendedMediaQuery");
    td_->messages_manager_->finish_get_message_extended_media(dialog_id_, message_ids_);
  }
};

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

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

  void send(vector<MessageId> &&message_ids) {
    send_query(G()->net_query_creator().create(
        telegram_api::messages_readMessageContents(MessageId::get_server_message_ids(message_ids))));
  }

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

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

    if (affected_messages->pts_count_ > 0) {
      td_->updates_manager_->add_pending_pts_update(make_tl_object<dummyUpdate>(), affected_messages->pts_,
                                                    affected_messages->pts_count_, Time::now(), Promise<Unit>(),
                                                    "read messages content query");
    }

    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    if (!G()->is_expected_error(status)) {
      LOG(ERROR) << "Receive error for read message contents: " << status;
    }
    promise_.set_error(std::move(status));
  }
};

class ReadChannelMessagesContentsQuery final : 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;
      return on_error(Status::Error(400, "Can't access the chat"));
    }

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

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::channels_readMessageContents>(packet);
    if (result_ptr.is_error()) {
      return on_error(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(Status status) final {
    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 final : 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_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(400, "Can't access the chat"));
    }

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

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

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

    auto info = get_messages_info(td_, dialog_id_, result_ptr.move_as_ok(), "GetDialogMessageByDateQuery");
    td_->messages_manager_->get_channel_difference_if_needed(
        dialog_id_, std::move(info),
        PromiseCreator::lambda([actor_id = td_->messages_manager_actor_.get(), dialog_id = dialog_id_, date = date_,
                                random_id = random_id_,
                                promise = std::move(promise_)](Result<MessagesInfo> &&result) mutable {
          if (result.is_error()) {
            promise.set_error(result.move_as_error());
          } else {
            auto info = result.move_as_ok();
            send_closure(actor_id, &MessagesManager::on_get_dialog_message_by_date_success, dialog_id, date, random_id,
                         std::move(info.messages), std::move(promise));
          }
        }),
        "GetDialogMessageByDateQuery");
  }

  void on_error(Status status) final {
    if (!td_->dialog_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 final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  MessageId from_message_id_;
  MessageId old_last_new_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, MessageId old_last_new_message_id, int32 offset,
            int32 limit) {
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(400, "Can't access the chat"));
    }
    CHECK(offset < 0);

    dialog_id_ = dialog_id;
    from_message_id_ = from_message_id;
    old_last_new_message_id_ = old_last_new_message_id;
    offset_ = offset;
    limit_ = limit;
    from_the_end_ = false;
    send_query(G()->net_query_creator().create(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, MessageId old_last_new_message_id, int32 limit) {
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(400, "Can't access the chat"));
    }

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

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

    auto info = get_messages_info(td_, dialog_id_, result_ptr.move_as_ok(), "GetHistoryQuery");
    td_->messages_manager_->get_channel_difference_if_needed(
        dialog_id_, std::move(info),
        PromiseCreator::lambda([actor_id = td_->messages_manager_actor_.get(), dialog_id = dialog_id_,
                                from_message_id = from_message_id_, old_last_new_message_id = old_last_new_message_id_,
                                offset = offset_, limit = limit_, from_the_end = from_the_end_,
                                promise = std::move(promise_)](Result<MessagesInfo> &&result) mutable {
          if (result.is_error()) {
            promise.set_error(result.move_as_error());
          } else {
            auto info = result.move_as_ok();
            // TODO use info.total_count, info.pts
            send_closure(actor_id, &MessagesManager::on_get_history, dialog_id, from_message_id,
                         old_last_new_message_id, offset, limit, from_the_end, std::move(info.messages),
                         std::move(promise));
          }
        }),
        "GetHistoryQuery");
  }

  void on_error(Status status) final {
    if (!td_->dialog_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 final : 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;
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);
    send_query(G()->net_query_creator().create(
        telegram_api::messages_readHistory(std::move(input_peer), max_message_id.get_server_message_id().get()),
        {{dialog_id}}));
  }

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

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

    if (affected_messages->pts_count_ > 0) {
      td_->updates_manager_->add_pending_pts_update(make_tl_object<dummyUpdate>(), affected_messages->pts_,
                                                    affected_messages->pts_count_, Time::now(), Promise<Unit>(),
                                                    "read history query");
    }

    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ReadHistoryQuery")) {
      LOG(ERROR) << "Receive error for ReadHistoryQuery: " << status;
    }
    promise_.set_error(std::move(status));
  }
};

class ReadChannelHistoryQuery final : 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);
    if (input_channel == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

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

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

    promise_.set_value(Unit());
  }

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

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

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

  void send(DialogId dialog_id, MessageId top_thread_message_id, MessageId max_message_id) {
    dialog_id_ = dialog_id;
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);
    send_query(G()->net_query_creator().create(
        telegram_api::messages_readDiscussion(std::move(input_peer),
                                              top_thread_message_id.get_server_message_id().get(),
                                              max_message_id.get_server_message_id().get()),
        {{dialog_id}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_readDiscussion>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ReadDiscussionQuery");
    promise_.set_error(std::move(status));
  }
};

class GetSearchResultCalendarQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  SavedMessagesTopicId saved_messages_topic_id_;
  MessageId from_message_id_;
  MessageSearchFilter filter_;
  int64 random_id_;

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

  void send(DialogId dialog_id, SavedMessagesTopicId saved_messages_topic_id, MessageId from_message_id,
            MessageSearchFilter filter, int64 random_id) {
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);

    dialog_id_ = dialog_id;
    saved_messages_topic_id_ = saved_messages_topic_id;
    from_message_id_ = from_message_id;
    filter_ = filter;
    random_id_ = random_id;

    int32 flags = 0;
    telegram_api::object_ptr<telegram_api::InputPeer> saved_input_peer;
    if (saved_messages_topic_id.is_valid()) {
      flags |= telegram_api::messages_getSearchResultsCalendar::SAVED_PEER_ID_MASK;
      saved_input_peer = saved_messages_topic_id.get_input_peer(td_);
      CHECK(saved_input_peer != nullptr);
    }
    send_query(G()->net_query_creator().create(telegram_api::messages_getSearchResultsCalendar(
        flags, std::move(input_peer), std::move(saved_input_peer), get_input_messages_filter(filter),
        from_message_id.get_server_message_id().get(), 0)));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getSearchResultsCalendar>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto result = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for GetSearchResultCalendarQuery: " << to_string(result);
    td_->contacts_manager_->on_get_users(std::move(result->users_), "GetSearchResultCalendarQuery");
    td_->contacts_manager_->on_get_chats(std::move(result->chats_), "GetSearchResultCalendarQuery");

    // unused: inexact:flags.0?true min_date:int min_msg_id:int offset_id_offset:flags.1?int

    MessagesInfo info;
    info.messages = std::move(result->messages_);
    info.total_count = result->count_;
    info.is_channel_messages = dialog_id_.get_type() == DialogType::Channel;

    td_->messages_manager_->get_channel_difference_if_needed(
        dialog_id_, std::move(info),
        PromiseCreator::lambda([actor_id = td_->messages_manager_actor_.get(), dialog_id = dialog_id_,
                                saved_messages_topic_id = saved_messages_topic_id_, from_message_id = from_message_id_,
                                filter = filter_, random_id = random_id_, periods = std::move(result->periods_),
                                promise = std::move(promise_)](Result<MessagesInfo> &&result) mutable {
          if (result.is_error()) {
            promise.set_error(result.move_as_error());
          } else {
            auto info = result.move_as_ok();
            send_closure(actor_id, &MessagesManager::on_get_message_search_result_calendar, dialog_id,
                         saved_messages_topic_id, from_message_id, filter, random_id, info.total_count,
                         std::move(info.messages), std::move(periods), std::move(promise));
          }
        }),
        "GetSearchResultCalendarQuery");
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetSearchResultCalendarQuery");
    td_->messages_manager_->on_failed_get_message_search_result_calendar(random_id_);
    promise_.set_error(std::move(status));
  }
};

class GetMessagePositionQuery final : public Td::ResultHandler {
  Promise<int32> promise_;
  DialogId dialog_id_;
  MessageId message_id_;
  MessageId top_thread_message_id_;
  SavedMessagesTopicId saved_messages_topic_id_;
  MessageSearchFilter filter_;

 public:
  explicit GetMessagePositionQuery(Promise<int32> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId message_id, MessageSearchFilter filter, MessageId top_thread_message_id,
            SavedMessagesTopicId saved_messages_topic_id) {
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);

    dialog_id_ = dialog_id;
    message_id_ = message_id;
    top_thread_message_id_ = top_thread_message_id;
    saved_messages_topic_id_ = saved_messages_topic_id;
    filter_ = filter;

    if (filter == MessageSearchFilter::Empty && !top_thread_message_id.is_valid()) {
      if (saved_messages_topic_id.is_valid()) {
        send_query(G()->net_query_creator().create(telegram_api::messages_getSavedHistory(
            saved_messages_topic_id.get_input_peer(td_), message_id.get_server_message_id().get(), 0, -1, 1, 0, 0, 0)));
      } else {
        send_query(G()->net_query_creator().create(telegram_api::messages_getHistory(
            std::move(input_peer), message_id.get_server_message_id().get(), 0, -1, 1, 0, 0, 0)));
      }
    } else {
      int32 flags = 0;
      tl_object_ptr<telegram_api::InputPeer> saved_input_peer;
      if (saved_messages_topic_id.is_valid()) {
        flags |= telegram_api::messages_search::SAVED_PEER_ID_MASK;
        saved_input_peer = saved_messages_topic_id.get_input_peer(td_);
        CHECK(saved_input_peer != nullptr);
      }
      if (top_thread_message_id.is_valid()) {
        flags |= telegram_api::messages_search::TOP_MSG_ID_MASK;
      }
      send_query(G()->net_query_creator().create(telegram_api::messages_search(
          flags, std::move(input_peer), string(), nullptr, std::move(saved_input_peer), Auto(),
          top_thread_message_id.get_server_message_id().get(), get_input_messages_filter(filter), 0,
          std::numeric_limits<int32>::max(), message_id.get_server_message_id().get(), -1, 1,
          std::numeric_limits<int32>::max(), 0, 0)));
    }
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_search>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto messages_ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for GetMessagePositionQuery: " << to_string(messages_ptr);
    switch (messages_ptr->get_id()) {
      case telegram_api::messages_messages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messages>(messages_ptr);
        if (messages->messages_.size() != 1 ||
            MessageId::get_message_id(messages->messages_[0], false) != message_id_) {
          return promise_.set_error(Status::Error(400, "Message not found by the filter"));
        }
        return promise_.set_value(narrow_cast<int32>(messages->messages_.size()));
      }
      case telegram_api::messages_messagesSlice::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_messagesSlice>(messages_ptr);
        if (messages->messages_.size() != 1 ||
            MessageId::get_message_id(messages->messages_[0], false) != message_id_) {
          return promise_.set_error(Status::Error(400, "Message not found by the filter"));
        }
        if (messages->offset_id_offset_ <= 0) {
          LOG(ERROR) << "Failed to receive position for " << message_id_ << " in thread of " << top_thread_message_id_
                     << " and in " << saved_messages_topic_id_ << " in " << dialog_id_ << " by " << filter_;
          return promise_.set_error(Status::Error(400, "Message position is unknown"));
        }
        return promise_.set_value(std::move(messages->offset_id_offset_));
      }
      case telegram_api::messages_channelMessages::ID: {
        auto messages = move_tl_object_as<telegram_api::messages_channelMessages>(messages_ptr);
        if (messages->messages_.size() != 1 ||
            MessageId::get_message_id(messages->messages_[0], false) != message_id_) {
          return promise_.set_error(Status::Error(400, "Message not found by the filter"));
        }
        if (messages->offset_id_offset_ <= 0) {
          LOG(ERROR) << "Failed to receive position for " << message_id_ << " in " << dialog_id_ << " by " << filter_;
          return promise_.set_error(Status::Error(500, "Message position is unknown"));
        }
        return promise_.set_value(std::move(messages->offset_id_offset_));
      }
      case telegram_api::messages_messagesNotModified::ID:
        LOG(ERROR) << "Server returned messagesNotModified in response to GetMessagePositionQuery";
        return promise_.set_error(Status::Error(500, "Receive invalid response"));
      default:
        UNREACHABLE();
        break;
    }
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetMessagePositionQuery");
    promise_.set_error(std::move(status));
  }
};

class SearchMessagesQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  SavedMessagesTopicId saved_messages_topic_id_;
  string query_;
  DialogId sender_dialog_id_;
  MessageId from_message_id_;
  int32 offset_;
  int32 limit_;
  MessageSearchFilter filter_;
  MessageId top_thread_message_id_;
  ReactionType tag_;
  int64 random_id_;
  bool handle_errors_ = true;

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

  void send(DialogId dialog_id, SavedMessagesTopicId saved_messages_topic_id, const string &query,
            DialogId sender_dialog_id, MessageId from_message_id, int32 offset, int32 limit, MessageSearchFilter filter,
            MessageId top_thread_message_id, const ReactionType &tag, int64 random_id) {
    auto input_peer = dialog_id.is_valid() ? td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read)
                                           : make_tl_object<telegram_api::inputPeerEmpty>();
    CHECK(input_peer != nullptr);

    dialog_id_ = dialog_id;
    saved_messages_topic_id_ = saved_messages_topic_id;
    query_ = query;
    sender_dialog_id_ = sender_dialog_id;
    from_message_id_ = from_message_id;
    offset_ = offset;
    limit_ = limit;
    filter_ = filter;
    top_thread_message_id_ = top_thread_message_id;
    tag_ = tag;
    random_id_ = random_id;

    auto top_msg_id = top_thread_message_id.get_server_message_id().get();
    auto offset_id = from_message_id.get_server_message_id().get();
    if (filter == MessageSearchFilter::UnreadMention) {
      CHECK(!saved_messages_topic_id.is_valid());
      CHECK(tag_.is_empty());
      int32 flags = 0;
      if (top_thread_message_id.is_valid()) {
        flags |= telegram_api::messages_getUnreadMentions::TOP_MSG_ID_MASK;
      }
      send_query(G()->net_query_creator().create(telegram_api::messages_getUnreadMentions(
          flags, std::move(input_peer), top_msg_id, offset_id, offset, limit, std::numeric_limits<int32>::max(), 0)));
    } else if (filter == MessageSearchFilter::UnreadReaction) {
      CHECK(!saved_messages_topic_id.is_valid());
      CHECK(tag_.is_empty());
      int32 flags = 0;
      if (top_thread_message_id.is_valid()) {
        flags |= telegram_api::messages_getUnreadReactions::TOP_MSG_ID_MASK;
      }
      send_query(G()->net_query_creator().create(telegram_api::messages_getUnreadReactions(
          flags, std::move(input_peer), top_msg_id, offset_id, offset, limit, std::numeric_limits<int32>::max(), 0)));
    } else if (top_thread_message_id.is_valid() && query.empty() && !sender_dialog_id.is_valid() &&
               filter == MessageSearchFilter::Empty) {
      CHECK(!saved_messages_topic_id.is_valid());
      CHECK(tag_.is_empty());
      handle_errors_ = dialog_id.get_type() != DialogType::Channel ||
                       !td_->contacts_manager_->is_broadcast_channel(dialog_id.get_channel_id());
      send_query(G()->net_query_creator().create(telegram_api::messages_getReplies(
          std::move(input_peer), top_msg_id, offset_id, 0, offset, limit, std::numeric_limits<int32>::max(), 0, 0)));
    } else {
      int32 flags = 0;
      tl_object_ptr<telegram_api::InputPeer> sender_input_peer;
      if (sender_dialog_id.is_valid()) {
        flags |= telegram_api::messages_search::FROM_ID_MASK;
        sender_input_peer = td_->dialog_manager_->get_input_peer(sender_dialog_id, AccessRights::Know);
        CHECK(sender_input_peer != nullptr);
      }
      tl_object_ptr<telegram_api::InputPeer> saved_input_peer;
      if (saved_messages_topic_id.is_valid()) {
        flags |= telegram_api::messages_search::SAVED_PEER_ID_MASK;
        saved_input_peer = saved_messages_topic_id.get_input_peer(td_);
        CHECK(saved_input_peer != nullptr);
      }
      vector<telegram_api::object_ptr<telegram_api::Reaction>> saved_reactions;
      if (!tag.is_empty()) {
        flags |= telegram_api::messages_search::SAVED_REACTION_MASK;
        saved_reactions.push_back(tag.get_input_reaction());
      }
      if (top_thread_message_id.is_valid()) {
        flags |= telegram_api::messages_search::TOP_MSG_ID_MASK;
      }

      send_query(G()->net_query_creator().create(telegram_api::messages_search(
          flags, std::move(input_peer), query, std::move(sender_input_peer), std::move(saved_input_peer),
          std::move(saved_reactions), top_msg_id, get_input_messages_filter(filter), 0,
          std::numeric_limits<int32>::max(), offset_id, offset, limit, std::numeric_limits<int32>::max(), 0, 0)));
    }
  }

  void on_result(BufferSlice packet) final {
    static_assert(std::is_same<telegram_api::messages_getUnreadMentions::ReturnType,
                               telegram_api::messages_search::ReturnType>::value,
                  "");
    static_assert(std::is_same<telegram_api::messages_getUnreadReactions::ReturnType,
                               telegram_api::messages_search::ReturnType>::value,
                  "");
    static_assert(
        std::is_same<telegram_api::messages_getReplies::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(result_ptr.move_as_error());
    }

    auto info = get_messages_info(td_, dialog_id_, result_ptr.move_as_ok(), "SearchMessagesQuery");
    td_->messages_manager_->get_channel_difference_if_needed(
        dialog_id_, std::move(info),
        PromiseCreator::lambda(
            [actor_id = td_->messages_manager_actor_.get(), dialog_id = dialog_id_,
             saved_messages_topic_id = saved_messages_topic_id_, query = std::move(query_),
             sender_dialog_id = sender_dialog_id_, from_message_id = from_message_id_, offset = offset_, limit = limit_,
             filter = filter_, top_thread_message_id = top_thread_message_id_, tag = std::move(tag_),
             random_id = random_id_, promise = std::move(promise_)](Result<MessagesInfo> &&result) mutable {
              if (result.is_error()) {
                promise.set_error(result.move_as_error());
              } else {
                auto info = result.move_as_ok();
                send_closure(actor_id, &MessagesManager::on_get_dialog_messages_search_result, dialog_id,
                             saved_messages_topic_id, query, sender_dialog_id, from_message_id, offset, limit, filter,
                             top_thread_message_id, tag, random_id, info.total_count, std::move(info.messages),
                             std::move(promise));
              }
            }),
        "SearchMessagesQuery");
  }

  void on_error(Status status) final {
    if (handle_errors_) {
      td_->dialog_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 GetSearchResultPositionsQuery final : public Td::ResultHandler {
  Promise<td_api::object_ptr<td_api::messagePositions>> promise_;
  DialogId dialog_id_;
  SavedMessagesTopicId saved_messages_topic_id_;
  MessageSearchFilter filter_;

 public:
  explicit GetSearchResultPositionsQuery(Promise<td_api::object_ptr<td_api::messagePositions>> &&promise)
      : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, SavedMessagesTopicId saved_messages_topic_id, MessageSearchFilter filter,
            MessageId from_message_id, int32 limit) {
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(400, "Can't access the chat"));
    }
    dialog_id_ = dialog_id;
    saved_messages_topic_id_ = saved_messages_topic_id;
    filter_ = filter;

    int32 flags = 0;
    telegram_api::object_ptr<telegram_api::InputPeer> saved_input_peer;
    if (saved_messages_topic_id.is_valid()) {
      flags |= telegram_api::messages_getSearchResultsPositions::SAVED_PEER_ID_MASK;
      saved_input_peer = saved_messages_topic_id.get_input_peer(td_);
      CHECK(saved_input_peer != nullptr);
    }
    send_query(G()->net_query_creator().create(telegram_api::messages_getSearchResultsPositions(
        flags, std::move(input_peer), std::move(saved_input_peer), get_input_messages_filter(filter),
        from_message_id.get_server_message_id().get(), limit)));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getSearchResultsPositions>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    td_->messages_manager_->on_get_dialog_sparse_message_positions(dialog_id_, saved_messages_topic_id_, filter_,
                                                                   result_ptr.move_as_ok(), std::move(promise_));
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetSearchResultPositionsQuery");
    promise_.set_error(std::move(status));
  }
};

class GetSearchCountersQuery final : public Td::ResultHandler {
  Promise<int32> promise_;
  DialogId dialog_id_;
  SavedMessagesTopicId saved_messages_topic_id_;
  MessageSearchFilter filter_;

 public:
  explicit GetSearchCountersQuery(Promise<int32> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, SavedMessagesTopicId saved_messages_topic_id, MessageSearchFilter filter) {
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(400, "Can't access the chat"));
    }

    dialog_id_ = dialog_id;
    saved_messages_topic_id_ = saved_messages_topic_id;
    filter_ = filter;

    CHECK(filter != MessageSearchFilter::Empty);
    CHECK(filter != MessageSearchFilter::UnreadMention);
    CHECK(filter != MessageSearchFilter::FailedToSend);
    CHECK(filter != MessageSearchFilter::UnreadReaction);
    vector<telegram_api::object_ptr<telegram_api::MessagesFilter>> filters;
    filters.push_back(get_input_messages_filter(filter));

    int32 flags = 0;
    telegram_api::object_ptr<telegram_api::InputPeer> saved_input_peer;
    if (saved_messages_topic_id.is_valid()) {
      flags |= telegram_api::messages_getSearchCounters::SAVED_PEER_ID_MASK;
      saved_input_peer = saved_messages_topic_id.get_input_peer(td_);
      CHECK(saved_input_peer != nullptr);
    }
    send_query(G()->net_query_creator().create(telegram_api::messages_getSearchCounters(
        flags, std::move(input_peer), std::move(saved_input_peer), 0, std::move(filters))));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getSearchCounters>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto result = result_ptr.move_as_ok();
    if (result.size() != 1 || result[0]->filter_->get_id() != get_input_messages_filter(filter_)->get_id()) {
      LOG(ERROR) << "Receive unexpected response for get message count in " << dialog_id_ << " with filter " << filter_
                 << ": " << to_string(result);
      return on_error(Status::Error(500, "Receive wrong response"));
    }

    td_->messages_manager_->on_get_dialog_message_count(dialog_id_, saved_messages_topic_id_, filter_,
                                                        result[0]->count_, std::move(promise_));
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetSearchCountersQuery");
    promise_.set_error(std::move(status));
  }
};

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

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

  void send(FolderId folder_id, bool ignore_folder_id, const string &query, int32 offset_date,
            DialogId offset_dialog_id, MessageId offset_message_id, int32 limit, MessageSearchFilter filter,
            int32 min_date, int32 max_date, 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;
    filter_ = filter;
    min_date_ = min_date;
    max_date_ = max_date;

    auto input_peer = DialogManager::get_input_peer_force(offset_dialog_id);
    CHECK(input_peer != nullptr);

    int32 flags = 0;
    if (!ignore_folder_id) {
      flags |= telegram_api::messages_searchGlobal::FOLDER_ID_MASK;
    }
    send_query(G()->net_query_creator().create(telegram_api::messages_searchGlobal(
        flags, folder_id.get(), query, get_input_messages_filter(filter), min_date_, max_date_, offset_date_,
        std::move(input_peer), offset_message_id.get_server_message_id().get(), limit)));
  }

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

    auto info = get_messages_info(td_, DialogId(), result_ptr.move_as_ok(), "SearchMessagesGlobalQuery");
    td_->messages_manager_->get_channel_differences_if_needed(
        std::move(info),
        PromiseCreator::lambda([actor_id = td_->messages_manager_actor_.get(), query = std::move(query_),
                                offset_date = offset_date_, offset_dialog_id = offset_dialog_id_,
                                offset_message_id = offset_message_id_, limit = limit_, filter = std::move(filter_),
                                min_date = min_date_, max_date = max_date_, random_id = random_id_,
                                promise = std::move(promise_)](Result<MessagesInfo> &&result) mutable {
          if (result.is_error()) {
            promise.set_error(result.move_as_error());
          } else {
            auto info = result.move_as_ok();
            send_closure(actor_id, &MessagesManager::on_get_messages_search_result, query, offset_date,
                         offset_dialog_id, offset_message_id, limit, filter, min_date, max_date, random_id,
                         info.total_count, std::move(info.messages), info.next_rate, std::move(promise));
          }
        }),
        "SearchMessagesGlobalQuery");
  }

  void on_error(Status status) final {
    td_->messages_manager_->on_failed_messages_search(random_id_);
    promise_.set_error(std::move(status));
  }
};

class GetAllScheduledMessagesQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  uint32 generation_;

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

  void send(DialogId dialog_id, int64 hash, uint32 generation) {
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);

    dialog_id_ = dialog_id;
    generation_ = generation;

    send_query(
        G()->net_query_creator().create(telegram_api::messages_getScheduledHistory(std::move(input_peer), hash)));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getScheduledHistory>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    if (result_ptr.ok()->get_id() == telegram_api::messages_messagesNotModified::ID) {
      td_->messages_manager_->on_get_scheduled_server_messages(dialog_id_, generation_, Auto(), true);
    } else {
      auto info = get_messages_info(td_, dialog_id_, result_ptr.move_as_ok(), "GetAllScheduledMessagesQuery");
      td_->messages_manager_->on_get_scheduled_server_messages(dialog_id_, generation_, std::move(info.messages),
                                                               false);
    }

    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetAllScheduledMessagesQuery");
    promise_.set_error(std::move(status));
  }
};

class SearchSentMediaQuery final : public Td::ResultHandler {
  Promise<td_api::object_ptr<td_api::foundMessages>> promise_;

 public:
  explicit SearchSentMediaQuery(Promise<td_api::object_ptr<td_api::foundMessages>> &&promise)
      : promise_(std::move(promise)) {
  }

  void send(const string &query, int32 limit) {
    send_query(G()->net_query_creator().create(telegram_api::messages_searchSentMedia(
        query, telegram_api::make_object<telegram_api::inputMessagesFilterDocument>(), limit)));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_searchSentMedia>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto info = get_messages_info(td_, DialogId(), result_ptr.move_as_ok(), "SearchSentMediaQuery");
    td_->messages_manager_->get_channel_differences_if_needed(
        std::move(info),
        PromiseCreator::lambda([actor_id = td_->messages_manager_actor_.get(),
                                promise = std::move(promise_)](Result<MessagesInfo> &&result) mutable {
          if (result.is_error()) {
            promise.set_error(result.move_as_error());
          } else {
            auto info = result.move_as_ok();
            send_closure(actor_id, &MessagesManager::on_get_outgoing_document_messages, std::move(info.messages),
                         std::move(promise));
          }
        }),
        "SearchSentMediaQuery");
  }

  void on_error(Status status) final {
    promise_.set_error(std::move(status));
  }
};

class GetRecentLocationsQuery final : public Td::ResultHandler {
  Promise<td_api::object_ptr<td_api::messages>> promise_;
  DialogId dialog_id_;
  int32 limit_;

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

  void send(DialogId dialog_id, int32 limit) {
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Chat is not accessible"));
    }

    dialog_id_ = dialog_id;
    limit_ = limit;

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

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

    auto info = get_messages_info(td_, dialog_id_, result_ptr.move_as_ok(), "GetRecentLocationsQuery");
    td_->messages_manager_->get_channel_difference_if_needed(
        dialog_id_, std::move(info),
        PromiseCreator::lambda([actor_id = td_->messages_manager_actor_.get(), dialog_id = dialog_id_, limit = limit_,
                                promise = std::move(promise_)](Result<MessagesInfo> &&result) mutable {
          if (result.is_error()) {
            promise.set_error(result.move_as_error());
          } else {
            auto info = result.move_as_ok();
            send_closure(actor_id, &MessagesManager::on_get_recent_locations, dialog_id, limit, info.total_count,
                         std::move(info.messages), std::move(promise));
          }
        }),
        "GetRecentLocationsQuery");
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetRecentLocationsQuery");
    promise_.set_error(std::move(status));
  }
};

class HidePromoDataQuery final : public Td::ResultHandler {
  DialogId dialog_id_;

 public:
  void send(DialogId dialog_id) {
    dialog_id_ = dialog_id;
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);
    send_query(G()->net_query_creator().create(telegram_api::help_hidePromoData(std::move(input_peer))));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::help_hidePromoData>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    // we are not interested in the result
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "HidePromoDataQuery")) {
      LOG(ERROR) << "Receive error for sponsored chat hiding: " << status;
    }
  }
};

class DeleteHistoryQuery final : public Td::ResultHandler {
  Promise<AffectedHistory> promise_;
  DialogId dialog_id_;

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

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

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(400, "Chat is not accessible"));
    }

    int32 flags = 0;
    if (!remove_from_dialog_list) {
      flags |= telegram_api::messages_deleteHistory::JUST_CLEAR_MASK;
    }
    if (revoke) {
      flags |= telegram_api::messages_deleteHistory::REVOKE_MASK;
    }

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

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

    promise_.set_value(AffectedHistory(result_ptr.move_as_ok()));
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "DeleteHistoryQuery");
    promise_.set_error(std::move(status));
  }
};

class DeleteTopicHistoryQuery final : public Td::ResultHandler {
  Promise<AffectedHistory> promise_;
  ChannelId channel_id_;
  MessageId top_thread_message_id_;

 public:
  explicit DeleteTopicHistoryQuery(Promise<AffectedHistory> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId top_thread_message_id) {
    CHECK(dialog_id.get_type() == DialogType::Channel);
    channel_id_ = dialog_id.get_channel_id();
    top_thread_message_id_ = top_thread_message_id;

    auto input_channel = td_->contacts_manager_->get_input_channel(channel_id_);
    if (input_channel == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    send_query(G()->net_query_creator().create(telegram_api::channels_deleteTopicHistory(
        std::move(input_channel), top_thread_message_id.get_server_message_id().get())));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::channels_deleteTopicHistory>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    promise_.set_value(AffectedHistory(result_ptr.move_as_ok()));
  }

  void on_error(Status status) final {
    td_->messages_manager_->on_get_message_error(DialogId(channel_id_), top_thread_message_id_, status,
                                                 "DeleteTopicHistoryQuery");
    promise_.set_error(std::move(status));
  }
};

class DeleteChannelHistoryQuery final : 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, bool revoke) {
    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);
    if (input_channel == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    int32 flags = 0;
    if (revoke) {
      flags |= telegram_api::channels_deleteHistory::FOR_EVERYONE_MASK;
    }

    send_query(G()->net_query_creator().create(telegram_api::channels_deleteHistory(
        flags, false /*ignored*/, std::move(input_channel), max_message_id.get_server_message_id().get())));
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for DeleteChannelHistoryQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

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

class DeleteMessagesByDateQuery final : public Td::ResultHandler {
  Promise<AffectedHistory> promise_;
  DialogId dialog_id_;

 public:
  explicit DeleteMessagesByDateQuery(Promise<AffectedHistory> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, int32 min_date, int32 max_date, bool revoke) {
    dialog_id_ = dialog_id;

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(400, "Chat is not accessible"));
    }

    int32 flags = telegram_api::messages_deleteHistory::JUST_CLEAR_MASK |
                  telegram_api::messages_deleteHistory::MIN_DATE_MASK |
                  telegram_api::messages_deleteHistory::MAX_DATE_MASK;
    if (revoke) {
      flags |= telegram_api::messages_deleteHistory::REVOKE_MASK;
    }

    send_query(G()->net_query_creator().create(telegram_api::messages_deleteHistory(
        flags, false /*ignored*/, false /*ignored*/, std::move(input_peer), 0, min_date, max_date)));
  }

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

    promise_.set_value(AffectedHistory(result_ptr.move_as_ok()));
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "DeleteMessagesByDateQuery");
    promise_.set_error(std::move(status));
  }
};

class DeletePhoneCallHistoryQuery final : public Td::ResultHandler {
  Promise<AffectedHistory> promise_;

 public:
  explicit DeletePhoneCallHistoryQuery(Promise<AffectedHistory> &&promise) : promise_(std::move(promise)) {
  }

  void send(bool revoke) {
    int32 flags = 0;
    if (revoke) {
      flags |= telegram_api::messages_deletePhoneCallHistory::REVOKE_MASK;
    }
    send_query(
        G()->net_query_creator().create(telegram_api::messages_deletePhoneCallHistory(flags, false /*ignored*/)));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_deletePhoneCallHistory>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto affected_messages = result_ptr.move_as_ok();
    if (!affected_messages->messages_.empty()) {
      td_->messages_manager_->process_pts_update(
          make_tl_object<telegram_api::updateDeleteMessages>(std::move(affected_messages->messages_), 0, 0));
    }
    promise_.set_value(AffectedHistory(std::move(affected_messages)));
  }

  void on_error(Status status) final {
    promise_.set_error(std::move(status));
  }
};

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

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

  void send(MessageId message_id, bool need_delete_message, bool need_delete_all_messages, bool report_spam) {
    int32 flags = 0;
    if (need_delete_message) {
      flags |= telegram_api::contacts_blockFromReplies::DELETE_MESSAGE_MASK;
    }
    if (need_delete_all_messages) {
      flags |= telegram_api::contacts_blockFromReplies::DELETE_HISTORY_MASK;
    }
    if (report_spam) {
      flags |= telegram_api::contacts_blockFromReplies::REPORT_SPAM_MASK;
    }
    send_query(G()->net_query_creator().create(telegram_api::contacts_blockFromReplies(
        flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, message_id.get_server_message_id().get())));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::contacts_blockFromReplies>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for BlockFromRepliesQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

  void on_error(Status status) final {
    promise_.set_error(std::move(status));
  }
};

class DeleteParticipantHistoryQuery final : public Td::ResultHandler {
  Promise<AffectedHistory> promise_;
  ChannelId channel_id_;
  DialogId sender_dialog_id_;

 public:
  explicit DeleteParticipantHistoryQuery(Promise<AffectedHistory> &&promise) : promise_(std::move(promise)) {
  }

  void send(ChannelId channel_id, DialogId sender_dialog_id) {
    channel_id_ = channel_id;
    sender_dialog_id_ = sender_dialog_id;

    auto input_channel = td_->contacts_manager_->get_input_channel(channel_id);
    if (input_channel == nullptr) {
      return promise_.set_error(Status::Error(400, "Chat is not accessible"));
    }
    auto input_peer = td_->dialog_manager_->get_input_peer(sender_dialog_id, AccessRights::Know);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(400, "Message sender is not accessible"));
    }

    send_query(G()->net_query_creator().create(
        telegram_api::channels_deleteParticipantHistory(std::move(input_channel), std::move(input_peer))));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::channels_deleteParticipantHistory>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    promise_.set_value(AffectedHistory(result_ptr.move_as_ok()));
  }

  void on_error(Status status) final {
    if (sender_dialog_id_.get_type() != DialogType::Channel) {
      td_->contacts_manager_->on_get_channel_error(channel_id_, status, "DeleteParticipantHistoryQuery");
    }
    promise_.set_error(std::move(status));
  }
};

class ReadMentionsQuery final : public Td::ResultHandler {
  Promise<AffectedHistory> promise_;
  DialogId dialog_id_;

 public:
  explicit ReadMentionsQuery(Promise<AffectedHistory> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId top_thread_message_id) {
    dialog_id_ = dialog_id;

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(400, "Chat is not accessible"));
    }

    int32 flags = 0;
    if (top_thread_message_id.is_valid()) {
      flags |= telegram_api::messages_readMentions::TOP_MSG_ID_MASK;
    }
    send_query(G()->net_query_creator().create(
        telegram_api::messages_readMentions(flags, std::move(input_peer),
                                            top_thread_message_id.get_server_message_id().get()),
        {{dialog_id}}));
  }

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

    promise_.set_value(AffectedHistory(result_ptr.move_as_ok()));
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ReadMentionsQuery");
    promise_.set_error(std::move(status));
  }
};

class ReadReactionsQuery final : public Td::ResultHandler {
  Promise<AffectedHistory> promise_;
  DialogId dialog_id_;

 public:
  explicit ReadReactionsQuery(Promise<AffectedHistory> &&promise) : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, MessageId top_thread_message_id) {
    dialog_id_ = dialog_id;

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_error(Status::Error(400, "Chat is not accessible"));
    }

    int32 flags = 0;
    if (top_thread_message_id.is_valid()) {
      flags |= telegram_api::messages_readReactions::TOP_MSG_ID_MASK;
    }
    send_query(G()->net_query_creator().create(
        telegram_api::messages_readReactions(flags, std::move(input_peer),
                                             top_thread_message_id.get_server_message_id().get()),
        {{dialog_id}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_readReactions>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    promise_.set_value(AffectedHistory(result_ptr.move_as_ok()));
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ReadReactionsQuery");
    promise_.set_error(std::move(status));
  }
};

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

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

  void send(DialogId dialog_id, DialogId send_as_dialog_id) {
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);

    auto send_as_input_peer = td_->dialog_manager_->get_input_peer(send_as_dialog_id, AccessRights::Read);
    CHECK(send_as_input_peer != nullptr);

    send_query(G()->net_query_creator().create(
        telegram_api::messages_saveDefaultSendAs(std::move(input_peer), std::move(send_as_input_peer)),
        {{dialog_id, MessageContentType::Photo}, {dialog_id, MessageContentType::Text}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_saveDefaultSendAs>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto success = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SaveDefaultSendAsQuery: " << success;

    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    // td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "SaveDefaultSendAsQuery");
    promise_.set_error(std::move(status));
  }
};

class SendMessageQuery final : public Td::ResultHandler {
  int64 random_id_;
  DialogId dialog_id_;

 public:
  void send(int32 flags, DialogId dialog_id, tl_object_ptr<telegram_api::InputPeer> as_input_peer,
            const MessageInputReplyTo &input_reply_to, MessageId top_thread_message_id, int32 schedule_date,
            tl_object_ptr<telegram_api::ReplyMarkup> &&reply_markup,
            vector<tl_object_ptr<telegram_api::MessageEntity>> &&entities, const string &text, bool is_copy,
            int64 random_id, NetQueryRef *send_query_ref) {
    random_id_ = random_id;
    dialog_id_ = dialog_id;

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

    auto reply_to = input_reply_to.get_input_reply_to(td_, top_thread_message_id);

    if (reply_to != nullptr) {
      flags |= telegram_api::messages_sendMessage::REPLY_TO_MASK;
    }
    if (!entities.empty()) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_ENTITIES;
    }
    if (as_input_peer != nullptr) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_SEND_AS;
    }

    auto query = G()->net_query_creator().create(
        telegram_api::messages_sendMessage(
            flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/,
            false /*ignored*/, false /*ignored*/, std::move(input_peer), std::move(reply_to), text, random_id,
            std::move(reply_markup), std::move(entities), schedule_date, std::move(as_input_peer), nullptr),
        {{dialog_id, MessageContentType::Text},
         {dialog_id, is_copy ? MessageContentType::Photo : MessageContentType::Text}});
    if (td_->option_manager_->get_option_boolean("use_quick_ack")) {
      query->quick_ack_promise_ = PromiseCreator::lambda([random_id](Result<Unit> result) {
        if (result.is_ok()) {
          send_closure(G()->messages_manager(), &MessagesManager::on_send_message_get_quick_ack, random_id);
        }
      });
    }
    *send_query_ref = query.get_weak();
    send_query(std::move(query));
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SendMessageQuery 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), Promise<Unit>());
    }
    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_));
    auto ttl_period = sent_message->ttl_period_;
    auto update = make_tl_object<updateSentMessage>(random_id_, message_id, sent_message->date_, ttl_period);
    if (dialog_id_.get_type() == DialogType::Channel) {
      td_->messages_manager_->add_pending_channel_update(dialog_id_, std::move(update), sent_message->pts_,
                                                         sent_message->pts_count_, Promise<Unit>(),
                                                         "send message actor");
      return;
    }

    td_->updates_manager_->add_pending_pts_update(std::move(update), sent_message->pts_, sent_message->pts_count_,
                                                  Time::now(), Promise<Unit>(), "send message actor");
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for SendMessage: " << status;
    if (G()->close_flag() && G()->use_message_database()) {
      // do not send error, message will be re-sent after restart
      return;
    }
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "SendMessageQuery");
    td_->messages_manager_->on_send_message_fail(random_id_, std::move(status));
  }
};

class StartBotQuery final : 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(
        telegram_api::messages_startBot(std::move(bot_input_user), std::move(input_peer), random_id, parameter),
        {{dialog_id, MessageContentType::Text}, {dialog_id, MessageContentType::Photo}});
    if (td_->option_manager_->get_option_boolean("use_quick_ack")) {
      query->quick_ack_promise_ = PromiseCreator::lambda([random_id](Result<Unit> result) {
        if (result.is_ok()) {
          send_closure(G()->messages_manager(), &MessagesManager::on_send_message_get_quick_ack, random_id);
        }
      });
    }
    auto send_query_ref = query.get_weak();
    send_query(std::move(query));
    return send_query_ref;
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for StartBotQuery 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), Promise<Unit>());
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for StartBotQuery: " << status;
    if (G()->close_flag() && G()->use_message_database()) {
      // do not send error, message should be re-sent after restart
      return;
    }
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "StartBotQuery");
    td_->messages_manager_->on_send_message_fail(random_id_, std::move(status));
  }
};

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

 public:
  NetQueryRef send(int32 flags, DialogId dialog_id, tl_object_ptr<telegram_api::InputPeer> as_input_peer,
                   const MessageInputReplyTo &input_reply_to, MessageId top_thread_message_id, int32 schedule_date,
                   int64 random_id, int64 query_id, const string &result_id) {
    random_id_ = random_id;
    dialog_id_ = dialog_id;

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

    auto reply_to = input_reply_to.get_input_reply_to(td_, top_thread_message_id);

    if (reply_to != nullptr) {
      flags |= telegram_api::messages_sendInlineBotResult::REPLY_TO_MASK;
    }
    if (as_input_peer != nullptr) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_SEND_AS;
    }

    auto query = G()->net_query_creator().create(
        telegram_api::messages_sendInlineBotResult(
            flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, std::move(input_peer),
            std::move(reply_to), random_id, query_id, result_id, schedule_date, std::move(as_input_peer), nullptr),
        {{dialog_id, MessageContentType::Text}, {dialog_id, MessageContentType::Photo}});
    auto send_query_ref = query.get_weak();
    send_query(std::move(query));
    return send_query_ref;
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SendInlineBotResultQuery 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), Promise<Unit>());
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for SendInlineBotResultQuery: " << status;
    if (G()->close_flag() && G()->use_message_database()) {
      // do not send error, message will be re-sent after restart
      return;
    }
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "SendInlineBotResultQuery");
    td_->messages_manager_->on_send_message_fail(random_id_, std::move(status));
  }
};

class SendMultiMediaQuery final : public Td::ResultHandler {
  vector<FileId> file_ids_;
  vector<string> file_references_;
  vector<int64> random_ids_;
  DialogId dialog_id_;

 public:
  void send(int32 flags, DialogId dialog_id, tl_object_ptr<telegram_api::InputPeer> as_input_peer,
            const MessageInputReplyTo &input_reply_to, MessageId top_thread_message_id, int32 schedule_date,
            vector<FileId> &&file_ids, vector<tl_object_ptr<telegram_api::inputSingleMedia>> &&input_single_media,
            bool is_copy) {
    for (auto &single_media : input_single_media) {
      random_ids_.push_back(single_media->random_id_);
      CHECK(FileManager::extract_was_uploaded(single_media->media_) == false);
      file_references_.push_back(FileManager::extract_file_reference(single_media->media_));
    }
    dialog_id_ = dialog_id;
    file_ids_ = std::move(file_ids);
    CHECK(file_ids_.size() == random_ids_.size());

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

    auto reply_to = input_reply_to.get_input_reply_to(td_, top_thread_message_id);

    if (reply_to != nullptr) {
      flags |= telegram_api::messages_sendMultiMedia::REPLY_TO_MASK;
    }
    if (as_input_peer != nullptr) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_SEND_AS;
    }

    // no quick ack, because file reference errors are very likely to happen
    send_query(G()->net_query_creator().create(
        telegram_api::messages_sendMultiMedia(flags, false /*ignored*/, false /*ignored*/, false /*ignored*/,
                                              false /*ignored*/, false /*ignored*/, false /*ignored*/,
                                              std::move(input_peer), std::move(reply_to), std::move(input_single_media),
                                              schedule_date, std::move(as_input_peer), nullptr),
        {{dialog_id, is_copy ? MessageContentType::Text : MessageContentType::Photo},
         {dialog_id, MessageContentType::Photo}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_sendMultiMedia>(packet);
    if (result_ptr.is_error()) {
      return on_error(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 = UpdatesManager::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 = UpdatesManager::get_new_messages(ptr.get());
      if (sent_random_ids_size != sent_messages.size()) {
        is_result_wrong = true;
      }
      for (auto &sent_message : sent_messages) {
        if (DialogId::get_message_dialog_id(sent_message.first) != dialog_id_) {
          is_result_wrong = true;
        }
      }
    }
    if (is_result_wrong) {
      LOG(ERROR) << "Receive wrong result for SendMultiMediaQuery 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), Promise<Unit>());
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for SendMultiMedia: " << status;
    if (G()->close_flag() && G()->use_message_database()) {
      // do not send error, message will be re-sent after restart
      return;
    }
    if (!td_->auth_manager_->is_bot() && FileReferenceManager::is_file_reference_error(status)) {
      auto pos = FileReferenceManager::get_file_reference_error_pos(status);
      if (1 <= pos && pos <= file_ids_.size() && file_ids_[pos - 1].is_valid()) {
        VLOG(file_references) << "Receive " << status << " for " << file_ids_[pos - 1];
        td_->file_manager_->delete_file_reference(file_ids_[pos - 1], file_references_[pos - 1]);
        td_->messages_manager_->on_send_media_group_file_reference_error(dialog_id_, std::move(random_ids_));
        return;
      } else {
        LOG(ERROR) << "Receive file reference error " << status << ", but file_ids = " << file_ids_
                   << ", message_count = " << file_ids_.size();
      }
    }
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "SendMultiMediaQuery");
    for (auto &random_id : random_ids_) {
      td_->messages_manager_->on_send_message_fail(random_id, status.clone());
    }
  }
};

class SendMediaQuery final : public Td::ResultHandler {
  int64 random_id_ = 0;
  FileId file_id_;
  FileId thumbnail_file_id_;
  DialogId dialog_id_;
  string file_reference_;
  bool was_uploaded_ = false;
  bool was_thumbnail_uploaded_ = false;

 public:
  void send(FileId file_id, FileId thumbnail_file_id, int32 flags, DialogId dialog_id,
            tl_object_ptr<telegram_api::InputPeer> as_input_peer, const MessageInputReplyTo &input_reply_to,
            MessageId top_thread_message_id, int32 schedule_date,
            tl_object_ptr<telegram_api::ReplyMarkup> &&reply_markup,
            vector<tl_object_ptr<telegram_api::MessageEntity>> &&entities, const string &text,
            tl_object_ptr<telegram_api::InputMedia> &&input_media, MessageContentType content_type, bool is_copy,
            int64 random_id, NetQueryRef *send_query_ref) {
    random_id_ = random_id;
    file_id_ = file_id;
    thumbnail_file_id_ = thumbnail_file_id;
    dialog_id_ = dialog_id;
    file_reference_ = FileManager::extract_file_reference(input_media);
    was_uploaded_ = FileManager::extract_was_uploaded(input_media);
    was_thumbnail_uploaded_ = FileManager::extract_was_thumbnail_uploaded(input_media);

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

    auto reply_to = input_reply_to.get_input_reply_to(td_, top_thread_message_id);

    if (reply_to != nullptr) {
      flags |= telegram_api::messages_sendMedia::REPLY_TO_MASK;
    }
    if (!entities.empty()) {
      flags |= telegram_api::messages_sendMedia::ENTITIES_MASK;
    }
    if (as_input_peer != nullptr) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_SEND_AS;
    }

    auto query = G()->net_query_creator().create(
        telegram_api::messages_sendMedia(
            flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/,
            false /*ignored*/, std::move(input_peer), std::move(reply_to), std::move(input_media), text, random_id,
            std::move(reply_markup), std::move(entities), schedule_date, std::move(as_input_peer), nullptr),
        {{dialog_id, content_type}, {dialog_id, is_copy ? MessageContentType::Text : content_type}});
    if (td_->option_manager_->get_option_boolean("use_quick_ack") && was_uploaded_) {
      query->quick_ack_promise_ = PromiseCreator::lambda([random_id](Result<Unit> result) {
        if (result.is_ok()) {
          send_closure(G()->messages_manager(), &MessagesManager::on_send_message_get_quick_ack, random_id);
        }
      });
    }
    *send_query_ref = query.get_weak();
    send_query(std::move(query));
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SendMediaQuery 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), Promise<Unit>());

    if (was_thumbnail_uploaded_) {
      CHECK(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_);
    }
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for SendMedia: " << status;
    if (G()->close_flag() && G()->use_message_database()) {
      // do not send error, message will be re-sent after restart
      return;
    }
    if (was_uploaded_) {
      if (was_thumbnail_uploaded_) {
        CHECK(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_);
      }

      CHECK(file_id_.is_valid());
      auto bad_parts = FileManager::get_missing_file_parts(status);
      if (!bad_parts.empty()) {
        td_->messages_manager_->on_send_message_file_parts_missing(random_id_, std::move(bad_parts));
        return;
      } else {
        if (status.code() != 429 && status.code() < 500 && !G()->close_flag()) {
          td_->file_manager_->delete_partial_remote_location(file_id_);
        }
      }
    } else if (!td_->auth_manager_->is_bot() && FileReferenceManager::is_file_reference_error(status)) {
      if (file_id_.is_valid() && !was_uploaded_) {
        VLOG(file_references) << "Receive " << status << " for " << file_id_;
        td_->file_manager_->delete_file_reference(file_id_, file_reference_);
        td_->messages_manager_->on_send_message_file_reference_error(random_id_);
        return;
      } else {
        LOG(ERROR) << "Receive file reference error, but file_id = " << file_id_
                   << ", was_uploaded = " << was_uploaded_;
      }
    }

    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "SendMediaQuery");
    td_->messages_manager_->on_send_message_fail(random_id_, std::move(status));
  }
};

class UploadMediaQuery final : public Td::ResultHandler {
  DialogId dialog_id_;
  MessageId message_id_;
  FileId file_id_;
  FileId thumbnail_file_id_;
  string file_reference_;
  bool was_uploaded_ = false;
  bool was_thumbnail_uploaded_ = false;

 public:
  void send(DialogId dialog_id, MessageId message_id, FileId file_id, FileId thumbnail_file_id,
            tl_object_ptr<telegram_api::InputMedia> &&input_media) {
    CHECK(input_media != nullptr);
    dialog_id_ = dialog_id;
    message_id_ = message_id;
    file_id_ = file_id;
    thumbnail_file_id_ = thumbnail_file_id;
    file_reference_ = FileManager::extract_file_reference(input_media);
    was_uploaded_ = FileManager::extract_was_uploaded(input_media);
    was_thumbnail_uploaded_ = FileManager::extract_was_thumbnail_uploaded(input_media);

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

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

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

    if (was_thumbnail_uploaded_) {
      CHECK(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 UploadMediaQuery for " << message_id_ << " in " << dialog_id_ << ": "
              << to_string(ptr);
    td_->messages_manager_->on_upload_message_media_success(dialog_id_, message_id_, std::move(ptr));
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for UploadMediaQuery for " << message_id_ << " in " << dialog_id_ << ": " << status;
    if (G()->close_flag() && G()->use_message_database()) {
      // do not send error, message will be re-sent after restart
      return;
    }
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "UploadMediaQuery");
    if (was_uploaded_) {
      if (was_thumbnail_uploaded_) {
        CHECK(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_);
      }

      CHECK(file_id_.is_valid());
      auto bad_parts = FileManager::get_missing_file_parts(status);
      if (!bad_parts.empty()) {
        td_->messages_manager_->on_upload_message_media_file_parts_missing(dialog_id_, message_id_,
                                                                           std::move(bad_parts));
        return;
      } else {
        if (status.code() != 429 && status.code() < 500 && !G()->close_flag()) {
          td_->file_manager_->delete_partial_remote_location(file_id_);
        }
      }
    } else if (FileReferenceManager::is_file_reference_error(status)) {
      LOG(ERROR) << "Receive file reference error for UploadMediaQuery";
    }
    td_->messages_manager_->on_upload_message_media_fail(dialog_id_, message_id_, std::move(status));
  }
};

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

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

  void send(DialogId dialog_id, MessageId message_id) {
    dialog_id_ = dialog_id;

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

    int32 server_message_id = message_id.get_scheduled_server_message_id().get();
    send_query(G()->net_query_creator().create(
        telegram_api::messages_sendScheduledMessages(std::move(input_peer), {server_message_id}),
        {{dialog_id, MessageContentType::Text}, {dialog_id, MessageContentType::Photo}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_sendScheduledMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SendScheduledMessageQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for SendScheduledMessageQuery: " << status;
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "SendScheduledMessageQuery");
    promise_.set_error(std::move(status));
  }
};

class EditMessageQuery final : public Td::ResultHandler {
  Promise<int32> promise_;
  DialogId dialog_id_;
  MessageId message_id_;

 public:
  explicit EditMessageQuery(Promise<Unit> &&promise) {
    promise_ = PromiseCreator::lambda([promise = std::move(promise)](Result<int32> result) mutable {
      if (result.is_error()) {
        promise.set_error(result.move_as_error());
      } else {
        promise.set_value(Unit());
      }
    });
  }
  explicit EditMessageQuery(Promise<int32> &&promise) : promise_(std::move(promise)) {
  }

  void send(int32 flags, DialogId dialog_id, MessageId message_id, const string &text,
            vector<tl_object_ptr<telegram_api::MessageEntity>> &&entities,
            tl_object_ptr<telegram_api::InputMedia> &&input_media, bool invert_media,
            tl_object_ptr<telegram_api::ReplyMarkup> &&reply_markup, int32 schedule_date) {
    dialog_id_ = dialog_id;
    message_id_ = message_id;

    if (input_media != nullptr && false) {
      return on_error(Status::Error(400, "FILE_PART_1_MISSING"));
    }

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

    if (reply_markup != nullptr) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_REPLY_MARKUP;
    }
    if (!entities.empty()) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_ENTITIES;
    }
    if (!text.empty()) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_MESSAGE;
    }
    if (input_media != nullptr) {
      flags |= telegram_api::messages_editMessage::MEDIA_MASK;
    }
    if (invert_media) {
      flags |= telegram_api::messages_editMessage::INVERT_MEDIA_MASK;
    }
    if (schedule_date != 0) {
      flags |= telegram_api::messages_editMessage::SCHEDULE_DATE_MASK;
    }

    int32 server_message_id = schedule_date != 0 ? message_id.get_scheduled_server_message_id().get()
                                                 : message_id.get_server_message_id().get();
    send_query(G()->net_query_creator().create(
        telegram_api::messages_editMessage(flags, false /*ignored*/, false /*ignored*/, std::move(input_peer),
                                           server_message_id, text, std::move(input_media), std::move(reply_markup),
                                           std::move(entities), schedule_date, 0),
        {{dialog_id}}));
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for EditMessageQuery: " << to_string(ptr);
    auto pts = td_->updates_manager_->get_update_edit_message_pts(ptr.get(), {dialog_id_, message_id_});
    auto promise = PromiseCreator::lambda(
        [promise = std::move(promise_), pts](Result<Unit> result) mutable { promise.set_value(std::move(pts)); });
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise));
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for EditMessageQuery: " << status;
    if (!td_->auth_manager_->is_bot() && status.message() == "MESSAGE_NOT_MODIFIED") {
      return promise_.set_value(0);
    }
    td_->messages_manager_->on_get_message_error(dialog_id_, message_id_, status, "EditMessageQuery");
    promise_.set_error(std::move(status));
  }
};

class EditInlineMessageQuery final : 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 &text, vector<tl_object_ptr<telegram_api::MessageEntity>> &&entities,
            tl_object_ptr<telegram_api::InputMedia> &&input_media, bool invert_media,
            tl_object_ptr<telegram_api::ReplyMarkup> &&reply_markup) {
    CHECK(input_bot_inline_message_id != nullptr);

    // file in an inline message can't be uploaded to another datacenter,
    // so only previously uploaded files or URLs can be used in the InputMedia
    CHECK(!FileManager::extract_was_uploaded(input_media));

    if (reply_markup != nullptr) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_REPLY_MARKUP;
    }
    if (!entities.empty()) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_ENTITIES;
    }
    if (!text.empty()) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_MESSAGE;
    }
    if (input_media != nullptr) {
      flags |= telegram_api::messages_editInlineBotMessage::MEDIA_MASK;
    }
    if (invert_media) {
      flags |= telegram_api::messages_editInlineBotMessage::INVERT_MEDIA_MASK;
    }

    auto dc_id = DcId::internal(InlineQueriesManager::get_inline_message_dc_id(input_bot_inline_message_id));
    send_query(G()->net_query_creator().create(
        telegram_api::messages_editInlineBotMessage(
            flags, false /*ignored*/, false /*ignored*/, std::move(input_bot_inline_message_id), text,
            std::move(input_media), std::move(reply_markup), std::move(entities)),
        {}, dc_id));
  }

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

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

    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for EditInlineMessageQuery: " << status;
    promise_.set_error(std::move(status));
  }
};

class ForwardMessagesQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  vector<int64> random_ids_;
  DialogId from_dialog_id_;
  DialogId to_dialog_id_;
  MessageId message_id_;

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

  void send(int32 flags, DialogId to_dialog_id, MessageId top_thread_message_id, DialogId from_dialog_id,
            tl_object_ptr<telegram_api::InputPeer> as_input_peer, const vector<MessageId> &message_ids,
            vector<int64> &&random_ids, int32 schedule_date) {
    random_ids_ = random_ids;
    from_dialog_id_ = from_dialog_id;
    to_dialog_id_ = to_dialog_id;
    if (message_ids.size() == 1) {
      message_id_ = message_ids[0];
    }

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

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

    if (as_input_peer != nullptr) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_HAS_SEND_AS;
    }
    if (top_thread_message_id.is_valid()) {
      flags |= MessagesManager::SEND_MESSAGE_FLAG_IS_FROM_THREAD;
    }

    auto query = G()->net_query_creator().create(
        telegram_api::messages_forwardMessages(
            flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/,
            false /*ignored*/, std::move(from_input_peer), MessageId::get_server_message_ids(message_ids),
            std::move(random_ids), std::move(to_input_peer), top_thread_message_id.get_server_message_id().get(),
            schedule_date, std::move(as_input_peer), nullptr),
        {{to_dialog_id, MessageContentType::Text}, {to_dialog_id, MessageContentType::Photo}});
    if (td_->option_manager_->get_option_boolean("use_quick_ack")) {
      query->quick_ack_promise_ = PromiseCreator::lambda([random_ids = random_ids_](Result<Unit> result) {
        if (result.is_ok()) {
          for (auto random_id : random_ids) {
            send_closure(G()->messages_manager(), &MessagesManager::on_send_message_get_quick_ack, random_id);
          }
        }
      });
    }
    send_query(std::move(query));
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for ForwardMessagesQuery for " << format::as_array(random_ids_) << ": "
              << to_string(ptr);
    auto sent_random_ids = UpdatesManager::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 = UpdatesManager::get_new_messages(ptr.get());
      if (sent_random_ids_size != sent_messages.size()) {
        is_result_wrong = true;
      }
      for (auto &sent_message : sent_messages) {
        if (DialogId::get_message_dialog_id(sent_message.first) != 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), std::move(promise_));
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for forward messages: " << status;
    if (G()->close_flag() && G()->use_message_database()) {
      // do not send error, messages will be re-sent after restart
      return;
    }
    // no on_get_dialog_error call, because two dialogs are involved
    if (status.code() == 400 && status.message() == CSlice("CHAT_FORWARDS_RESTRICTED")) {
      td_->dialog_manager_->reload_dialog_info(from_dialog_id_, Promise<Unit>());
    }
    if (status.code() == 400 && status.message() == CSlice("SEND_AS_PEER_INVALID")) {
      td_->dialog_manager_->reload_dialog_info_full(to_dialog_id_, "SEND_AS_PEER_INVALID");
    }
    if (message_id_.is_valid() && status.message() == CSlice("MESSAGE_ID_INVALID")) {
      td_->messages_manager_->get_message_from_server({from_dialog_id_, message_id_}, Promise<Unit>(),
                                                      "ForwardMessagesQuery");
    }
    for (auto &random_id : random_ids_) {
      td_->messages_manager_->on_send_message_fail(random_id, status.clone());
    }
    promise_.set_error(std::move(status));
  }
};

class SendQuickReplyMessagesQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  vector<int64> random_ids_;
  DialogId dialog_id_;

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

  void send(DialogId dialog_id, QuickReplyShortcutId shortcut_id, const vector<MessageId> &message_ids,
            vector<int64> &&random_ids) {
    random_ids_ = std::move(random_ids);
    dialog_id_ = dialog_id;

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Write);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Have no write access to the chat"));
    }

    auto query = G()->net_query_creator().create(
        telegram_api::messages_sendQuickReplyMessages(std::move(input_peer), shortcut_id.get()),
        {{dialog_id, MessageContentType::Text}, {dialog_id, MessageContentType::Photo}});
    if (td_->option_manager_->get_option_boolean("use_quick_ack")) {
      query->quick_ack_promise_ = PromiseCreator::lambda([random_ids = random_ids_](Result<Unit> result) {
        if (result.is_ok()) {
          for (auto random_id : random_ids) {
            send_closure(G()->messages_manager(), &MessagesManager::on_send_message_get_quick_ack, random_id);
          }
        }
      });
    }
    send_query(std::move(query));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_sendQuickReplyMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SendQuickReplyMessagesQuery for " << format::as_array(random_ids_) << ": "
              << to_string(ptr);
    auto sent_messages = UpdatesManager::get_new_messages(ptr.get());
    bool is_result_wrong = false;
    if (random_ids_.size() != sent_messages.size()) {
      is_result_wrong = true;
    }
    for (auto &sent_message : sent_messages) {
      if (DialogId::get_message_dialog_id(sent_message.first) != dialog_id_) {
        is_result_wrong = true;
      }
    }
    if (is_result_wrong) {
      LOG(ERROR) << "Receive wrong result for sending quick reply messages with random_ids "
                 << format::as_array(random_ids_) << " to " << dialog_id_ << ": " << oneline(to_string(ptr));
      td_->updates_manager_->schedule_get_difference("Wrong sendQuickReplyMessages result");
      for (auto &random_id : random_ids_) {
        td_->messages_manager_->on_send_message_fail(random_id, Status::Error(500, "Receive invalid response"));
      }
    } else {
      // generate fake updates
      for (size_t i = 0; i < random_ids_.size(); i++) {
        td_->messages_manager_->on_update_message_id(
            random_ids_[i], MessageId::get_message_id(sent_messages[i].first, false), "SendQuickReplyMessagesQuery");
      }
    }
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for SendQuickReplyMessagesQuery: " << status;
    if (G()->close_flag() && G()->use_message_database()) {
      // do not send error, messages will be re-sent after restart
      return;
    }
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "SendQuickReplyMessagesQuery");
    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 final : 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_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write);
    CHECK(input_peer != nullptr);

    send_query(G()->net_query_creator().create(
        telegram_api::messages_sendScreenshotNotification(
            std::move(input_peer),
            telegram_api::make_object<telegram_api::inputReplyToMessage>(0, 0, 0, nullptr, string(), Auto(), 0),
            random_id),
        {{dialog_id, MessageContentType::Text}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_sendScreenshotNotification>(packet);
    if (result_ptr.is_error()) {
      return on_error(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), std::move(promise_));
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for SendScreenshotNotificationQuery: " << status;
    if (G()->close_flag() && G()->use_message_database()) {
      // do not send error, messages will be re-sent after restart
      return;
    }
    td_->dialog_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 SendBotRequestedPeerQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;

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

  void send(MessageFullId message_full_id, int32 button_id, vector<DialogId> &&requested_dialog_ids) {
    auto dialog_id = message_full_id.get_dialog_id();
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }
    vector<telegram_api::object_ptr<telegram_api::InputPeer>> requested_peers;
    for (auto requested_dialog_id : requested_dialog_ids) {
      auto requested_peer = td_->dialog_manager_->get_input_peer(requested_dialog_id, AccessRights::Read);
      if (requested_peer == nullptr) {
        return on_error(Status::Error(400, "Can't access the chosen chat"));
      }
      requested_peers.push_back(std::move(requested_peer));
    }

    send_query(G()->net_query_creator().create(
        telegram_api::messages_sendBotRequestedPeer(std::move(input_peer),
                                                    message_full_id.get_message_id().get_server_message_id().get(),
                                                    button_id, std::move(requested_peers)),
        {{dialog_id, MessageContentType::Text}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_sendBotRequestedPeer>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SendBotRequestedPeerQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

  void on_error(Status status) final {
    promise_.set_error(std::move(status));
  }
};

class DeleteMessagesQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  vector<int32> server_message_ids_;

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

  void send(DialogId dialog_id, vector<int32> &&server_message_ids, bool revoke) {
    dialog_id_ = dialog_id;
    server_message_ids_ = server_message_ids;

    int32 flags = 0;
    if (revoke) {
      flags |= telegram_api::messages_deleteMessages::REVOKE_MASK;
    }

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

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

    auto affected_messages = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for DeleteMessagesQuery: " << to_string(affected_messages);
    if (affected_messages->pts_count_ > 0) {
      td_->updates_manager_->add_pending_pts_update(make_tl_object<dummyUpdate>(), affected_messages->pts_,
                                                    affected_messages->pts_count_, Time::now(), std::move(promise_),
                                                    "delete messages query");
    } else {
      promise_.set_value(Unit());
    }
  }

  void on_error(Status status) final {
    if (!G()->is_expected_error(status)) {
      // MESSAGE_DELETE_FORBIDDEN can be returned in group chats when administrator rights was removed
      // MESSAGE_DELETE_FORBIDDEN can be returned in private chats for bots when revoke time limit exceeded
      if (status.message() != "MESSAGE_DELETE_FORBIDDEN" ||
          (dialog_id_.get_type() == DialogType::User && !td_->auth_manager_->is_bot())) {
        LOG(ERROR) << "Receive error for delete messages: " << status;
      }
    }
    td_->messages_manager_->on_failed_message_deletion(dialog_id_, server_message_ids_);
    promise_.set_error(std::move(status));
  }
};

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

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

  void send(ChannelId channel_id, vector<int32> &&server_message_ids) {
    channel_id_ = channel_id;
    server_message_ids_ = server_message_ids;

    auto input_channel = td_->contacts_manager_->get_input_channel(channel_id);
    if (input_channel == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }
    send_query(G()->net_query_creator().create(
        telegram_api::channels_deleteMessages(std::move(input_channel), std::move(server_message_ids))));
  }

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

    auto affected_messages = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for DeleteChannelMessagesQuery: " << to_string(affected_messages);
    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_,
                                                         std::move(promise_), "DeleteChannelMessagesQuery");
    } else {
      promise_.set_value(Unit());
    }
  }

  void on_error(Status status) final {
    if (!td_->contacts_manager_->on_get_channel_error(channel_id_, status, "DeleteChannelMessagesQuery")) {
      if (status.message() != "MESSAGE_DELETE_FORBIDDEN") {
        LOG(ERROR) << "Receive error for delete channel messages: " << status;
      }
    }
    td_->messages_manager_->on_failed_message_deletion(DialogId(channel_id_), server_message_ids_);
    promise_.set_error(std::move(status));
  }
};

class DeleteScheduledMessagesQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  vector<MessageId> message_ids_;

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

  void send(DialogId dialog_id, vector<MessageId> &&message_ids) {
    dialog_id_ = dialog_id;
    message_ids_ = std::move(message_ids);

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }
    send_query(G()->net_query_creator().create(telegram_api::messages_deleteScheduledMessages(
        std::move(input_peer), MessageId::get_scheduled_server_message_ids(message_ids_))));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_deleteScheduledMessages>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for DeleteScheduledMessagesQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "DeleteScheduledMessagesQuery")) {
      LOG(ERROR) << "Receive error for delete scheduled messages: " << status;
    }
    td_->messages_manager_->on_failed_scheduled_message_deletion(dialog_id_, message_ids_);
    promise_.set_error(std::move(status));
  }
};

class GetPeerSettingsQuery final : public Td::ResultHandler {
  DialogId dialog_id_;

 public:
  void send(DialogId dialog_id) {
    dialog_id_ = dialog_id;

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

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

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

    auto ptr = result_ptr.move_as_ok();
    td_->contacts_manager_->on_get_users(std::move(ptr->users_), "GetPeerSettingsQuery");
    td_->contacts_manager_->on_get_chats(std::move(ptr->chats_), "GetPeerSettingsQuery");
    td_->messages_manager_->on_get_peer_settings(dialog_id_, std::move(ptr->settings_));
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for get peer settings: " << status;
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetPeerSettingsQuery");
  }
};

class UpdatePeerSettingsQuery final : 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_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      return promise_.set_value(Unit());
    }

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

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

    td_->messages_manager_->on_get_peer_settings(dialog_id_, make_tl_object<telegram_api::peerSettings>(), true);

    promise_.set_value(Unit());
  }

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

class ReportEncryptedSpamQuery final : 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_->dialog_manager_->get_input_encrypted_chat(dialog_id, AccessRights::Read);
    CHECK(input_peer != nullptr);

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

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

    td_->messages_manager_->on_get_peer_settings(dialog_id_, make_tl_object<telegram_api::peerSettings>(), true);

    promise_.set_value(Unit());
  }

  void on_error(Status status) final {
    LOG(INFO) << "Receive error for report encrypted spam: " << status;
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ReportEncryptedSpamQuery");
    td_->messages_manager_->reget_dialog_action_bar(
        DialogId(td_->contacts_manager_->get_secret_chat_user_id(dialog_id_.get_secret_chat_id())),
        "ReportEncryptedSpamQuery");
    promise_.set_error(std::move(status));
  }
};

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

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

  void send(DialogId dialog_id, FolderId folder_id) {
    dialog_id_ = dialog_id;

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

    vector<telegram_api::object_ptr<telegram_api::inputFolderPeer>> input_folder_peers;
    input_folder_peers.push_back(
        telegram_api::make_object<telegram_api::inputFolderPeer>(std::move(input_peer), folder_id.get()));
    send_query(G()->net_query_creator().create(telegram_api::folders_editPeerFolders(std::move(input_folder_peers))));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::folders_editPeerFolders>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for EditPeerFoldersQuery: " << to_string(ptr);
    td_->updates_manager_->on_get_updates(std::move(ptr), std::move(promise_));
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "EditPeerFoldersQuery")) {
      LOG(INFO) << "Receive error for EditPeerFoldersQuery: " << status;
    }

    // trying to repair folder ID for this dialog
    td_->dialog_manager_->get_dialog_info_full(dialog_id_, Auto(), "EditPeerFoldersQuery");

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

class GetChannelDifferenceQuery final : 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(telegram_api::updates_getChannelDifference(
        flags, false /*ignored*/, std::move(input_channel), make_tl_object<telegram_api::channelMessagesFilterEmpty>(),
        pts, limit)));
  }

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

    td_->messages_manager_->on_get_channel_difference(dialog_id_, pts_, limit_, result_ptr.move_as_ok(), Status::OK());
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetChannelDifferenceQuery") &&
        status.message() != "PERSISTENT_TIMESTAMP_INVALID") {
      LOG(ERROR) << "Receive error for GetChannelDifferenceQuery for " << dialog_id_ << " with PTS " << pts_
                 << " and limit " << limit_ << ": " << status;
    }
    td_->messages_manager_->on_get_channel_difference(dialog_id_, pts_, limit_, nullptr, std::move(status));
  }
};

class MessagesManager::UploadMediaCallback final : public FileManager::UploadCallback {
 public:
  void on_upload_ok(FileId file_id, tl_object_ptr<telegram_api::InputFile> input_file) final {
    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) final {
    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) final {
    UNREACHABLE();
  }
  void on_upload_error(FileId file_id, Status error) final {
    send_closure_later(G()->messages_manager(), &MessagesManager::on_upload_media_error, file_id, std::move(error));
  }
};

class MessagesManager::UploadThumbnailCallback final : public FileManager::UploadCallback {
 public:
  void on_upload_ok(FileId file_id, tl_object_ptr<telegram_api::InputFile> input_file) final {
    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) final {
    UNREACHABLE();
  }
  void on_upload_secure_ok(FileId file_id, tl_object_ptr<telegram_api::InputSecureFile> input_file) final {
    UNREACHABLE();
  }
  void on_upload_error(FileId file_id, Status error) final {
    send_closure_later(G()->messages_manager(), &MessagesManager::on_upload_thumbnail, file_id, nullptr);
  }
};

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_reply_to_random_id = reply_to_random_id != 0;
  bool is_via_bot = via_bot_user_id.is_valid();
  bool has_view_count = view_count > 0;
  bool has_reply_markup = reply_markup != nullptr;
  bool has_ttl = ttl.is_valid();
  bool has_author_signature = !author_signature.empty();
  bool has_media_album_id = media_album_id != 0;
  bool has_send_date = message_id.is_yet_unsent() && send_date != 0;
  bool has_flags2 = true;
  bool has_notification_id = notification_id.is_valid();
  bool has_send_error_code = send_error_code != 0;
  bool has_real_forward_from = real_forward_from_dialog_id.is_valid() && real_forward_from_message_id.is_valid();
  bool has_legacy_layer = legacy_layer != 0;
  bool has_restriction_reasons = !restriction_reasons.empty();
  bool has_forward_count = forward_count > 0;
  bool has_reply_info = !reply_info.is_empty();
  bool has_sender_dialog_id = sender_dialog_id.is_valid();
  bool has_top_thread_message_id = top_thread_message_id.is_valid();
  bool has_thread_draft_message = thread_draft_message != nullptr;
  bool has_local_thread_message_ids = !local_thread_message_ids.empty();
  bool has_linked_top_thread_message_id = linked_top_thread_message_id.is_valid();
  bool has_interaction_info_update_date = interaction_info_update_date != 0;
  bool has_send_emoji = !send_emoji.empty();
  bool has_ttl_period = ttl_period != 0;
  bool has_max_reply_media_timestamp = max_reply_media_timestamp >= 0;
  bool are_message_media_timestamp_entities_found = true;
  bool has_flags3 = true;
  bool has_reactions = reactions != nullptr;
  bool has_available_reactions_generation = available_reactions_generation != 0;
  bool has_history_generation = history_generation != 0;
  bool is_reply_to_story = reply_to_story_full_id != StoryFullId();
  bool has_input_reply_to = !message_id.is_any_server() && input_reply_to.is_valid();
  bool has_replied_message_info = !replied_message_info.is_empty();
  bool has_forward_info = forward_info != nullptr;
  bool has_saved_messages_topic_id = saved_messages_topic_id.is_valid();
  bool has_initial_top_thread_message_id = !message_id.is_any_server() && initial_top_thread_message_id.is_valid();
  bool has_sender_boost_count = sender_boost_count != 0;
  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(false);
  STORE_FLAG(false);
  STORE_FLAG(has_sender);
  STORE_FLAG(has_edit_date);
  STORE_FLAG(has_random_id);
  STORE_FLAG(false);
  STORE_FLAG(false);
  STORE_FLAG(is_reply_to_random_id);
  STORE_FLAG(is_via_bot);
  STORE_FLAG(has_view_count);
  STORE_FLAG(has_reply_markup);
  STORE_FLAG(has_ttl);
  STORE_FLAG(has_author_signature);
  STORE_FLAG(false);
  STORE_FLAG(had_reply_markup);
  STORE_FLAG(contains_unread_mention);
  STORE_FLAG(has_media_album_id);
  STORE_FLAG(false);
  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);
    STORE_FLAG(is_mention_notification_disabled);
    STORE_FLAG(had_forward_info);
    STORE_FLAG(false);
    STORE_FLAG(has_send_error_code);
    STORE_FLAG(hide_via_bot);
    STORE_FLAG(is_bot_start_message);
    STORE_FLAG(has_real_forward_from);
    STORE_FLAG(has_legacy_layer);
    STORE_FLAG(hide_edit_date);
    STORE_FLAG(has_restriction_reasons);
    STORE_FLAG(is_from_scheduled);
    STORE_FLAG(is_copy);
    STORE_FLAG(false);
    STORE_FLAG(has_forward_count);
    STORE_FLAG(has_reply_info);
    STORE_FLAG(has_sender_dialog_id);
    STORE_FLAG(false);
    STORE_FLAG(has_top_thread_message_id);
    STORE_FLAG(has_thread_draft_message);
    STORE_FLAG(has_local_thread_message_ids);
    STORE_FLAG(has_linked_top_thread_message_id);
    STORE_FLAG(is_pinned);
    STORE_FLAG(has_interaction_info_update_date);
    STORE_FLAG(has_send_emoji);
    STORE_FLAG(false);
    STORE_FLAG(has_ttl_period);
    STORE_FLAG(has_max_reply_media_timestamp);
    STORE_FLAG(are_message_media_timestamp_entities_found);
    STORE_FLAG(has_flags3);
    END_STORE_FLAGS();
  }
  if (has_flags3) {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(noforwards);
    STORE_FLAG(has_explicit_sender);
    STORE_FLAG(has_reactions);
    STORE_FLAG(has_available_reactions_generation);
    STORE_FLAG(update_stickersets_order);
    STORE_FLAG(is_topic_message);
    STORE_FLAG(has_history_generation);
    STORE_FLAG(is_reply_to_story);
    STORE_FLAG(false);
    STORE_FLAG(invert_media);
    STORE_FLAG(has_input_reply_to);
    STORE_FLAG(has_replied_message_info);
    STORE_FLAG(has_forward_info);
    STORE_FLAG(has_saved_messages_topic_id);
    STORE_FLAG(has_initial_top_thread_message_id);
    STORE_FLAG(has_sender_boost_count);
    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 (has_forward_info) {
    store(forward_info, storer);
  }
  if (has_real_forward_from) {
    store(real_forward_from_dialog_id, storer);
    store(real_forward_from_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_view_count) {
    store(view_count, storer);
  }
  if (has_forward_count) {
    store(forward_count, storer);
  }
  if (has_reply_info) {
    store(reply_info, storer);
  }
  if (has_ttl) {
    store(ttl, storer);
    store_time(ttl_expires_at, storer);
  }
  if (has_send_error_code) {
    store(send_error_code, storer);
    store(send_error_message, storer);
    if (send_error_code == 429) {
      store_time(try_resend_at, 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);
  }
  if (has_legacy_layer) {
    store(legacy_layer, storer);
  }
  if (has_restriction_reasons) {
    store(restriction_reasons, storer);
  }
  if (has_sender_dialog_id) {
    store(sender_dialog_id, storer);
  }
  if (has_top_thread_message_id) {
    store(top_thread_message_id, storer);
  }
  if (has_thread_draft_message) {
    store(thread_draft_message, storer);
  }
  if (has_local_thread_message_ids) {
    store(local_thread_message_ids, storer);
  }
  if (has_linked_top_thread_message_id) {
    store(linked_top_thread_message_id, storer);
  }
  if (has_interaction_info_update_date) {
    store(interaction_info_update_date, storer);
  }
  if (has_send_emoji) {
    store(send_emoji, storer);
  }
  store_message_content(content.get(), storer);
  if (has_reply_markup) {
    store(reply_markup, storer);
  }
  if (has_ttl_period) {
    store(ttl_period, storer);
  }
  if (has_max_reply_media_timestamp) {
    store(max_reply_media_timestamp, storer);
  }
  if (has_reactions) {
    store(reactions, storer);
  }
  if (has_available_reactions_generation) {
    store(available_reactions_generation, storer);
  }
  if (has_history_generation) {
    store(history_generation, storer);
  }
  if (is_reply_to_story) {
    store(reply_to_story_full_id, storer);
  }
  if (has_input_reply_to) {
    store(input_reply_to, storer);
  }
  if (has_replied_message_info) {
    store(replied_message_info, storer);
  }
  if (has_saved_messages_topic_id) {
    store(saved_messages_topic_id, storer);
  }
  if (has_initial_top_thread_message_id) {
    store(initial_top_thread_message_id, storer);
  }
  if (has_sender_boost_count) {
    store(sender_boost_count, storer);
  }
}

// do not forget to resolve message dependencies
template <class ParserT>
void MessagesManager::Message::parse(ParserT &parser) {
  using td::parse;
  bool legacy_have_previous;
  bool legacy_have_next;
  bool has_sender;
  bool has_edit_date;
  bool has_random_id;
  bool legacy_is_forwarded;
  bool legacy_is_reply;
  bool is_reply_to_random_id;
  bool is_via_bot;
  bool has_view_count;
  bool has_reply_markup;
  bool has_ttl;
  bool has_author_signature;
  bool legacy_has_forward_author_signature;
  bool has_media_album_id;
  bool legacy_has_forward_from;
  bool has_send_date;
  bool has_flags2;
  bool has_notification_id = false;
  bool legacy_has_forward_sender_name = false;
  bool has_send_error_code = false;
  bool has_real_forward_from = false;
  bool has_legacy_layer = false;
  bool has_restriction_reasons = false;
  bool legacy_has_forward_psa_type = false;
  bool has_forward_count = false;
  bool has_reply_info = false;
  bool has_sender_dialog_id = false;
  bool legacy_has_reply_in_dialog_id = false;
  bool has_top_thread_message_id = false;
  bool has_thread_draft_message = false;
  bool has_local_thread_message_ids = false;
  bool has_linked_top_thread_message_id = false;
  bool has_interaction_info_update_date = false;
  bool has_send_emoji = false;
  bool legacy_is_imported = false;
  bool has_ttl_period = false;
  bool has_max_reply_media_timestamp = false;
  bool has_flags3 = false;
  bool has_reactions = false;
  bool has_available_reactions_generation = false;
  bool has_history_generation = false;
  bool is_reply_to_story = false;
  bool legacy_has_forward_origin = false;
  bool has_input_reply_to = false;
  bool has_replied_message_info = false;
  bool has_forward_info = false;
  bool has_saved_messages_topic_id = false;
  bool has_initial_top_thread_message_id = false;
  bool has_sender_boost_count = 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(legacy_have_previous);
  PARSE_FLAG(legacy_have_next);
  PARSE_FLAG(has_sender);
  PARSE_FLAG(has_edit_date);
  PARSE_FLAG(has_random_id);
  PARSE_FLAG(legacy_is_forwarded);
  PARSE_FLAG(legacy_is_reply);
  PARSE_FLAG(is_reply_to_random_id);
  PARSE_FLAG(is_via_bot);
  PARSE_FLAG(has_view_count);
  PARSE_FLAG(has_reply_markup);
  PARSE_FLAG(has_ttl);
  PARSE_FLAG(has_author_signature);
  PARSE_FLAG(legacy_has_forward_author_signature);
  PARSE_FLAG(had_reply_markup);
  PARSE_FLAG(contains_unread_mention);
  PARSE_FLAG(has_media_album_id);
  PARSE_FLAG(legacy_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);
    PARSE_FLAG(is_mention_notification_disabled);
    PARSE_FLAG(had_forward_info);
    PARSE_FLAG(legacy_has_forward_sender_name);
    PARSE_FLAG(has_send_error_code);
    PARSE_FLAG(hide_via_bot);
    PARSE_FLAG(is_bot_start_message);
    PARSE_FLAG(has_real_forward_from);
    PARSE_FLAG(has_legacy_layer);
    PARSE_FLAG(hide_edit_date);
    PARSE_FLAG(has_restriction_reasons);
    PARSE_FLAG(is_from_scheduled);
    PARSE_FLAG(is_copy);
    PARSE_FLAG(legacy_has_forward_psa_type);
    PARSE_FLAG(has_forward_count);
    PARSE_FLAG(has_reply_info);
    PARSE_FLAG(has_sender_dialog_id);
    PARSE_FLAG(legacy_has_reply_in_dialog_id);
    PARSE_FLAG(has_top_thread_message_id);
    PARSE_FLAG(has_thread_draft_message);
    PARSE_FLAG(has_local_thread_message_ids);
    PARSE_FLAG(has_linked_top_thread_message_id);
    PARSE_FLAG(is_pinned);
    PARSE_FLAG(has_interaction_info_update_date);
    PARSE_FLAG(has_send_emoji);
    PARSE_FLAG(legacy_is_imported);
    PARSE_FLAG(has_ttl_period);
    PARSE_FLAG(has_max_reply_media_timestamp);
    PARSE_FLAG(are_media_timestamp_entities_found);
    PARSE_FLAG(has_flags3);
    END_PARSE_FLAGS();
  }
  if (has_flags3) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(noforwards);
    PARSE_FLAG(has_explicit_sender);
    PARSE_FLAG(has_reactions);
    PARSE_FLAG(has_available_reactions_generation);
    PARSE_FLAG(update_stickersets_order);
    PARSE_FLAG(is_topic_message);
    PARSE_FLAG(has_history_generation);
    PARSE_FLAG(is_reply_to_story);
    PARSE_FLAG(legacy_has_forward_origin);
    PARSE_FLAG(invert_media);
    PARSE_FLAG(has_input_reply_to);
    PARSE_FLAG(has_replied_message_info);
    PARSE_FLAG(has_forward_info);
    PARSE_FLAG(has_saved_messages_topic_id);
    PARSE_FLAG(has_initial_top_thread_message_id);
    PARSE_FLAG(has_sender_boost_count);
    END_PARSE_FLAGS();
  }

  parse(message_id, parser);
  if (!message_id.is_valid() && !message_id.is_valid_scheduled()) {
    return parser.set_error("Invalid message identifier");
  }
  if (has_sender) {
    parse(sender_user_id, parser);
  }
  parse(date, parser);
  if (has_edit_date) {
    parse(edit_date, parser);
  }
  if (has_send_date) {
    if (!message_id.is_yet_unsent()) {
      return parser.set_error("Unexpected send date");
    }
    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 (has_forward_info) {
    parse(forward_info, parser);
  } else if (legacy_is_forwarded) {
    MessageOrigin forward_origin;
    int32 forward_date;
    if (legacy_has_forward_origin) {
      parse(forward_origin, parser);
      parse(forward_date, parser);
    } else {
      UserId forward_sender_user_id;
      DialogId forward_sender_dialog_id;
      MessageId forward_message_id;
      string forward_author_signature;
      string forward_sender_name;
      parse(forward_sender_user_id, parser);
      parse(forward_date, parser);
      parse(forward_sender_dialog_id, parser);
      parse(forward_message_id, parser);
      if (legacy_has_forward_author_signature) {
        parse(forward_author_signature, parser);
      }
      if (legacy_has_forward_sender_name) {
        parse(forward_sender_name, parser);
      }
      forward_origin = MessageOrigin(forward_sender_user_id, forward_sender_dialog_id, forward_message_id,
                                     std::move(forward_author_signature), std::move(forward_sender_name));
    }
    LastForwardedMessageInfo last_message_info;
    if (legacy_has_forward_from) {
      DialogId forward_from_dialog_id;
      MessageId forward_from_message_id;
      parse(forward_from_dialog_id, parser);
      parse(forward_from_message_id, parser);
      last_message_info =
          LastForwardedMessageInfo(forward_from_dialog_id, forward_from_message_id, DialogId(), string(), 0, false);
    }
    string psa_type;
    if (legacy_has_forward_psa_type) {
      parse(psa_type, parser);
    }
    forward_info = td::make_unique<MessageForwardInfo>(
        std::move(forward_origin), forward_date, std::move(last_message_info), std::move(psa_type), legacy_is_imported);
  }
  if (has_real_forward_from) {
    parse(real_forward_from_dialog_id, parser);
    parse(real_forward_from_message_id, parser);
  }
  MessageId legacy_reply_to_message_id;
  if (legacy_is_reply) {
    parse(legacy_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_view_count) {
    parse(view_count, parser);
  }
  if (has_forward_count) {
    parse(forward_count, parser);
  }
  if (has_reply_info) {
    parse(reply_info, parser);
  }
  if (has_ttl) {
    parse(ttl, parser);
    parse_time(ttl_expires_at, parser);
  }
  if (has_send_error_code) {
    parse(send_error_code, parser);
    parse(send_error_message, parser);
    if (send_error_code == 429) {
      parse_time(try_resend_at, parser);
    }
  }
  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);
  }
  if (has_legacy_layer) {
    parse(legacy_layer, parser);
  }
  if (has_restriction_reasons) {
    parse(restriction_reasons, parser);
  }
  if (has_sender_dialog_id) {
    parse(sender_dialog_id, parser);
  }
  DialogId legacy_reply_in_dialog_id;
  if (legacy_has_reply_in_dialog_id) {
    parse(legacy_reply_in_dialog_id, parser);
  }
  if (has_top_thread_message_id) {
    parse(top_thread_message_id, parser);
  }
  if (has_thread_draft_message) {
    parse(thread_draft_message, parser);
  }
  if (has_local_thread_message_ids) {
    parse(local_thread_message_ids, parser);
  }
  if (has_linked_top_thread_message_id) {
    parse(linked_top_thread_message_id, parser);
  }
  if (has_interaction_info_update_date) {
    parse(interaction_info_update_date, parser);
  }
  if (has_send_emoji) {
    parse(send_emoji, parser);
  }
  parse_message_content(content, parser);
  if (has_reply_markup) {
    parse(reply_markup, parser);
  }
  if (has_ttl_period) {
    parse(ttl_period, parser);
  }
  if (has_max_reply_media_timestamp) {
    parse(max_reply_media_timestamp, parser);
  }
  if (has_reactions) {
    parse(reactions, parser);
  }
  if (has_available_reactions_generation) {
    parse(available_reactions_generation, parser);
  }
  if (has_history_generation) {
    parse(history_generation, parser);
  }
  if (is_reply_to_story) {
    parse(reply_to_story_full_id, parser);
  }
  if (has_input_reply_to) {
    parse(input_reply_to, parser);
  } else if (!message_id.is_any_server()) {
    if (reply_to_story_full_id.is_valid()) {
      input_reply_to = MessageInputReplyTo(reply_to_story_full_id);
    } else if (legacy_reply_to_message_id.is_valid()) {
      input_reply_to = MessageInputReplyTo{legacy_reply_to_message_id, DialogId(), FormattedText(), 0};
    }
  }
  if (has_replied_message_info) {
    parse(replied_message_info, parser);
  } else {
    replied_message_info = RepliedMessageInfo::legacy(legacy_reply_to_message_id, legacy_reply_in_dialog_id);
  }
  if (has_saved_messages_topic_id) {
    parse(saved_messages_topic_id, parser);
  }
  if (has_initial_top_thread_message_id) {
    parse(initial_top_thread_message_id, parser);
  }
  if (has_sender_boost_count) {
    parse(sender_boost_count, parser);
  }

  CHECK(content != nullptr);
  is_content_secret |= ttl.is_secret_message_content(content->get_type());  // repair is_content_secret for old messages
  if (hide_edit_date && content->get_type() == MessageContentType::LiveLocation) {
    hide_edit_date = false;
  }
}

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

  auto dialog_type = dialog_id.get_type();
  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 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 =
      notification_info != nullptr && notification_info->message_notification_group_.is_active();
  bool has_mention_notification_group =
      notification_info != nullptr && notification_info->mention_notification_group_.is_active();
  bool has_new_secret_chat_notification_id =
      notification_info != nullptr && notification_info->new_secret_chat_notification_id_.is_valid();
  bool has_pinned_message_notification =
      notification_info != nullptr && notification_info->pinned_message_notification_message_id_.is_valid();
  bool has_last_pinned_message_id = last_pinned_message_id.is_valid();
  bool has_flags2 = true;
  bool has_max_push_notification_message_id =
      notification_info != nullptr && notification_info->max_push_notification_message_id_.is_valid() &&
      notification_info->max_push_notification_message_id_ > last_new_message_id;
  bool has_folder_id = folder_id != FolderId();
  bool has_pending_read_channel_inbox = pending_read_channel_inbox_pts != 0;
  bool has_last_yet_unsent_message = last_message_id.is_valid() && last_message_id.is_yet_unsent();
  bool has_active_group_call_id = active_group_call_id.is_valid();
  bool has_message_ttl = !message_ttl.is_empty();
  bool has_default_join_group_call_as_dialog_id = default_join_group_call_as_dialog_id.is_valid();
  bool store_has_bots = dialog_type == DialogType::Chat || dialog_type == DialogType::Channel;
  bool has_theme_name = !theme_name.empty();
  bool has_flags3 = true;
  bool has_pending_join_requests = pending_join_request_count != 0;
  bool has_action_bar = action_bar != nullptr;
  bool has_default_send_message_as_dialog_id = default_send_message_as_dialog_id.is_valid();
  bool has_legacy_available_reactions = false;
  bool has_available_reactions_generation = available_reactions_generation != 0;
  bool has_have_full_history_source = have_full_history && have_full_history_source != 0;
  bool has_available_reactions = !available_reactions.empty();
  bool has_history_generation = history_generation != 0;
  bool has_background = background_info.is_valid();
  BEGIN_STORE_FLAGS();
  STORE_FLAG(has_draft_message);
  STORE_FLAG(has_last_database_message);
  STORE_FLAG(false);  // legacy_know_can_report_spam
  STORE_FLAG(false);  // action_bar->can_report_spam
  STORE_FLAG(has_first_database_message_id);
  STORE_FLAG(false);  // legacy_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);
  STORE_FLAG(has_last_pinned_message_id);
  STORE_FLAG(is_last_pinned_message_id_inited);
  STORE_FLAG(has_flags2);
  END_STORE_FLAGS();

  store(dialog_id, storer);  // must be stored at offset 4

  if (has_flags2) {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(has_max_push_notification_message_id);
    STORE_FLAG(has_folder_id);
    STORE_FLAG(is_folder_id_inited);
    STORE_FLAG(has_pending_read_channel_inbox);
    STORE_FLAG(know_action_bar);
    STORE_FLAG(false);  // action_bar->can_add_contact
    STORE_FLAG(false);  // action_bar->can_block_user
    STORE_FLAG(false);  // action_bar->can_share_phone_number
    STORE_FLAG(false);  // action_bar->can_report_location
    STORE_FLAG(has_scheduled_server_messages);
    STORE_FLAG(has_scheduled_database_messages);
    STORE_FLAG(need_repair_channel_server_unread_count);
    STORE_FLAG(false);  // action_bar->can_unarchive
    STORE_FLAG(false);  // action_bar_has_distance
    STORE_FLAG(has_outgoing_messages);
    STORE_FLAG(has_last_yet_unsent_message);
    STORE_FLAG(is_blocked);
    STORE_FLAG(is_is_blocked_inited);
    STORE_FLAG(has_active_group_call);
    STORE_FLAG(is_group_call_empty);
    STORE_FLAG(has_active_group_call_id);
    STORE_FLAG(false);  // action_bar->can_invite_members
    STORE_FLAG(has_message_ttl);
    STORE_FLAG(is_message_ttl_inited);
    STORE_FLAG(has_default_join_group_call_as_dialog_id);
    STORE_FLAG(store_has_bots ? has_bots : false);
    STORE_FLAG(store_has_bots ? is_has_bots_inited : false);
    STORE_FLAG(is_theme_name_inited);
    STORE_FLAG(has_theme_name);
    STORE_FLAG(has_flags3);
    END_STORE_FLAGS();
  }
  if (has_flags3) {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(has_pending_join_requests);
    STORE_FLAG(need_repair_action_bar);
    STORE_FLAG(has_action_bar);
    STORE_FLAG(has_default_send_message_as_dialog_id);
    STORE_FLAG(need_drop_default_send_message_as_dialog_id);
    STORE_FLAG(has_legacy_available_reactions);
    STORE_FLAG(is_available_reactions_inited);
    STORE_FLAG(has_available_reactions_generation);
    STORE_FLAG(has_have_full_history_source);
    STORE_FLAG(has_available_reactions);
    STORE_FLAG(has_history_generation);
    STORE_FLAG(need_repair_unread_reaction_count);
    STORE_FLAG(is_translatable);
    STORE_FLAG(need_repair_unread_mention_count);
    STORE_FLAG(is_background_inited);
    STORE_FLAG(has_background);
    STORE_FLAG(is_blocked_for_stories);
    STORE_FLAG(is_is_blocked_for_stories_inited);
    STORE_FLAG(view_as_messages);
    STORE_FLAG(is_view_as_messages_inited);
    STORE_FLAG(is_forum);
    STORE_FLAG(is_saved_messages_view_as_messages_inited);
    END_STORE_FLAGS();
  }

  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 (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(notification_info->message_notification_group_, storer);
  }
  if (has_mention_notification_group) {
    store(notification_info->mention_notification_group_, storer);
  }
  if (has_new_secret_chat_notification_id) {
    store(notification_info->new_secret_chat_notification_id_, storer);
  }
  if (has_pinned_message_notification) {
    store(notification_info->pinned_message_notification_message_id_, storer);
  }
  if (has_last_pinned_message_id) {
    store(last_pinned_message_id, storer);
  }
  if (has_max_push_notification_message_id) {
    store(notification_info->max_push_notification_message_id_, storer);
  }
  if (has_folder_id) {
    store(folder_id, storer);
  }
  if (has_pending_read_channel_inbox) {
    store(pending_read_channel_inbox_pts, storer);
    store(pending_read_channel_inbox_max_message_id, storer);
    store(pending_read_channel_inbox_server_unread_count, storer);
  }
  if (has_active_group_call_id) {
    store(active_group_call_id, storer);
  }
  if (has_message_ttl) {
    store(message_ttl, storer);
  }
  if (has_default_join_group_call_as_dialog_id) {
    store(default_join_group_call_as_dialog_id, storer);
  }
  if (has_theme_name) {
    store(theme_name, storer);
  }
  if (has_pending_join_requests) {
    store(pending_join_request_count, storer);
    store(pending_join_request_user_ids, storer);
  }
  if (has_action_bar) {
    store(action_bar, storer);
  }
  if (has_default_send_message_as_dialog_id) {
    store(default_send_message_as_dialog_id, storer);
  }
  if (has_available_reactions) {
    store(available_reactions, storer);
  }
  if (has_available_reactions_generation) {
    store(available_reactions_generation, storer);
  }
  if (has_have_full_history_source) {
    store(have_full_history_source, storer);
  }
  if (has_history_generation) {
    store(history_generation, storer);
  }
  if (has_background) {
    store(background_info, 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 legacy_know_can_report_spam;
  bool has_first_database_message_id;
  bool legacy_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;
  bool has_last_pinned_message_id;
  bool has_flags2;
  bool has_max_push_notification_message_id = false;
  bool has_folder_id = false;
  bool has_pending_read_channel_inbox = false;
  bool has_active_group_call_id = false;
  bool has_message_ttl = false;
  bool has_default_join_group_call_as_dialog_id = false;
  bool has_theme_name = false;
  bool has_flags3 = false;
  bool has_pending_join_requests = false;
  bool action_bar_can_report_spam = false;
  bool action_bar_can_add_contact = false;
  bool action_bar_can_block_user = false;
  bool action_bar_can_share_phone_number = false;
  bool action_bar_can_report_location = false;
  bool action_bar_can_unarchive = false;
  bool action_bar_has_distance = false;
  bool action_bar_can_invite_members = false;
  bool has_action_bar = false;
  bool has_default_send_message_as_dialog_id = false;
  bool has_legacy_available_reactions = false;
  bool has_available_reactions_generation = false;
  bool has_have_full_history_source = false;
  bool has_available_reactions = false;
  bool has_history_generation = false;
  bool has_background = false;
  BEGIN_PARSE_FLAGS();
  PARSE_FLAG(has_draft_message);
  PARSE_FLAG(has_last_database_message);
  PARSE_FLAG(legacy_know_can_report_spam);
  PARSE_FLAG(action_bar_can_report_spam);
  PARSE_FLAG(has_first_database_message_id);
  PARSE_FLAG(legacy_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);
  PARSE_FLAG(has_last_pinned_message_id);
  PARSE_FLAG(is_last_pinned_message_id_inited);
  PARSE_FLAG(has_flags2);
  END_PARSE_FLAGS();

  parse(dialog_id, parser);  // must be stored at offset 4

  if (has_flags2) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(has_max_push_notification_message_id);
    PARSE_FLAG(has_folder_id);
    PARSE_FLAG(is_folder_id_inited);
    PARSE_FLAG(has_pending_read_channel_inbox);
    PARSE_FLAG(know_action_bar);
    PARSE_FLAG(action_bar_can_add_contact);
    PARSE_FLAG(action_bar_can_block_user);
    PARSE_FLAG(action_bar_can_share_phone_number);
    PARSE_FLAG(action_bar_can_report_location);
    PARSE_FLAG(has_scheduled_server_messages);
    PARSE_FLAG(has_scheduled_database_messages);
    PARSE_FLAG(need_repair_channel_server_unread_count);
    PARSE_FLAG(action_bar_can_unarchive);
    PARSE_FLAG(action_bar_has_distance);
    PARSE_FLAG(has_outgoing_messages);
    PARSE_FLAG(had_last_yet_unsent_message);
    PARSE_FLAG(is_blocked);
    PARSE_FLAG(is_is_blocked_inited);
    PARSE_FLAG(has_active_group_call);
    PARSE_FLAG(is_group_call_empty);
    PARSE_FLAG(has_active_group_call_id);
    PARSE_FLAG(action_bar_can_invite_members);
    PARSE_FLAG(has_message_ttl);
    PARSE_FLAG(is_message_ttl_inited);
    PARSE_FLAG(has_default_join_group_call_as_dialog_id);
    PARSE_FLAG(has_bots);
    PARSE_FLAG(is_has_bots_inited);
    PARSE_FLAG(is_theme_name_inited);
    PARSE_FLAG(has_theme_name);
    PARSE_FLAG(has_flags3);
    END_PARSE_FLAGS();
  } else {
    is_folder_id_inited = false;
    has_scheduled_server_messages = false;
    has_scheduled_database_messages = false;
    need_repair_channel_server_unread_count = false;
    has_outgoing_messages = false;
    had_last_yet_unsent_message = false;
    is_blocked = false;
    is_is_blocked_inited = false;
    has_active_group_call = false;
    is_group_call_empty = false;
    is_message_ttl_inited = false;
    has_bots = false;
    is_has_bots_inited = false;
    is_theme_name_inited = false;
  }
  if (has_flags3) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(has_pending_join_requests);
    PARSE_FLAG(need_repair_action_bar);
    PARSE_FLAG(has_action_bar);
    PARSE_FLAG(has_default_send_message_as_dialog_id);
    PARSE_FLAG(need_drop_default_send_message_as_dialog_id);
    PARSE_FLAG(has_legacy_available_reactions);
    PARSE_FLAG(is_available_reactions_inited);
    PARSE_FLAG(has_available_reactions_generation);
    PARSE_FLAG(has_have_full_history_source);
    PARSE_FLAG(has_available_reactions);
    PARSE_FLAG(has_history_generation);
    PARSE_FLAG(need_repair_unread_reaction_count);
    PARSE_FLAG(is_translatable);
    PARSE_FLAG(need_repair_unread_mention_count);
    PARSE_FLAG(is_background_inited);
    PARSE_FLAG(has_background);
    PARSE_FLAG(is_blocked_for_stories);
    PARSE_FLAG(is_is_blocked_for_stories_inited);
    PARSE_FLAG(view_as_messages);
    PARSE_FLAG(is_view_as_messages_inited);
    PARSE_FLAG(is_forum);
    PARSE_FLAG(is_saved_messages_view_as_messages_inited);
    END_PARSE_FLAGS();
  } else {
    need_repair_action_bar = false;
    is_available_reactions_inited = false;
    is_background_inited = false;
    is_blocked_for_stories = false;
    is_is_blocked_for_stories_inited = false;
    view_as_messages = false;
    is_view_as_messages_inited = false;
    is_forum = false;
    is_saved_messages_view_as_messages_inited = false;
  }

  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) {
    parse(draft_message, parser);
  }
  parse(last_clear_history_date, parser);
  parse(order, parser);
  if (has_last_database_message) {
    unique_ptr<Message> last_database_message;
    parse(last_database_message, parser);
    auto loaded_last_database_message_id = last_database_message->message_id;
    if (loaded_last_database_message_id.is_valid()) {
      messages.set(loaded_last_database_message_id, std::move(last_database_message));
    }
  }
  if (has_first_database_message_id) {
    parse(first_database_message_id, parser);
  }
  if (legacy_is_pinned) {
    int64 legacy_pinned_order;
    parse(legacy_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 (static_cast<size_t>(size) > first_database_message_id_by_index.size()) {
      // the log event is broken
      return parser.set_error("Wrong first_database_message_id_by_index table 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);
    if (static_cast<size_t>(size) > message_count_by_index.size()) {
      // the log event is broken
      return parser.set_error("Wrong message_count_by_index table size");
    }
    for (int32 i = 0; i < size; i++) {
      parse(message_count_by_index[i], parser);
    }
  }
  unread_mention_count = message_count_by_index[message_search_filter_index(MessageSearchFilter::UnreadMention)];
  LOG(INFO) << "Set unread mention message count in " << dialog_id << " to " << unread_mention_count;
  if (unread_mention_count < 0) {
    unread_mention_count = 0;
  }
  unread_reaction_count = message_count_by_index[message_search_filter_index(MessageSearchFilter::UnreadReaction)];
  LOG(INFO) << "Set unread reaction count in " << dialog_id << " to " << unread_reaction_count;
  if (unread_reaction_count < 0) {
    unread_reaction_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(add_dialog_notification_info(this)->message_notification_group_, parser);
  }
  if (has_mention_notification_group) {
    parse(add_dialog_notification_info(this)->mention_notification_group_, parser);
  }
  if (has_new_secret_chat_notification_id) {
    parse(add_dialog_notification_info(this)->new_secret_chat_notification_id_, parser);
  }
  if (has_pinned_message_notification) {
    parse(add_dialog_notification_info(this)->pinned_message_notification_message_id_, parser);
  }
  if (has_last_pinned_message_id) {
    parse(last_pinned_message_id, parser);
  }
  if (has_max_push_notification_message_id) {
    parse(add_dialog_notification_info(this)->max_push_notification_message_id_, parser);
  }
  if (has_folder_id) {
    parse(folder_id, parser);
  }
  if (has_pending_read_channel_inbox) {
    parse(pending_read_channel_inbox_pts, parser);
    parse(pending_read_channel_inbox_max_message_id, parser);
    parse(pending_read_channel_inbox_server_unread_count, parser);
  }
  int32 action_bar_distance = -1;
  if (action_bar_has_distance) {
    parse(action_bar_distance, parser);
  }
  if (has_active_group_call_id) {
    parse(active_group_call_id, parser);
  }
  if (has_message_ttl) {
    parse(message_ttl, parser);
  }
  if (has_default_join_group_call_as_dialog_id) {
    parse(default_join_group_call_as_dialog_id, parser);
  }
  if (has_theme_name) {
    parse(theme_name, parser);
  }
  if (has_pending_join_requests) {
    parse(pending_join_request_count, parser);
    parse(pending_join_request_user_ids, parser);
  }
  if (has_action_bar) {
    parse(action_bar, parser);
  }
  if (has_default_send_message_as_dialog_id) {
    parse(default_send_message_as_dialog_id, parser);
  }
  if (has_available_reactions) {
    parse(available_reactions, parser);
  } else if (has_legacy_available_reactions) {
    vector<ReactionType> legacy_available_reaction_types;
    parse(legacy_available_reaction_types, parser);
    available_reactions = ChatReactions(std::move(legacy_available_reaction_types));
  }
  if (has_available_reactions_generation) {
    parse(available_reactions_generation, parser);
  }
  if (has_have_full_history_source) {
    parse(have_full_history_source, parser);
  }
  if (has_history_generation) {
    parse(history_generation, parser);
  }
  if (has_background) {
    parse(background_info, parser);
  }

  (void)legacy_know_can_report_spam;
  if (know_action_bar && !has_action_bar) {
    action_bar = DialogActionBar::create(
        action_bar_can_report_spam, action_bar_can_add_contact, action_bar_can_block_user,
        action_bar_can_share_phone_number, action_bar_can_report_location, action_bar_can_unarchive,
        has_outgoing_messages ? -1 : action_bar_distance, action_bar_can_invite_members, string(), false, 0);
  }
}

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);
  if (static_cast<size_t>(size) > first_calls_database_message_id_by_index.size()) {
    return parser.set_error("Wrong first_calls_database_message_id_by_index table size");
  }
  for (int32 i = 0; i < size; i++) {
    parse(first_calls_database_message_id_by_index[i], parser);
  }
  parse(size, parser);
  if (static_cast<size_t>(size) > message_count_by_index.size()) {
    return parser.set_error("Wrong message_count_by_index table size");
  }
  for (int32 i = 0; i < size; i++) {
    parse(message_count_by_index[i], parser);
  }
}

void MessagesManager::load_calls_db_state() {
  if (!G()->use_message_database()) {
    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()->use_message_database()) {
    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::MessagesManager(Td *td, ActorShared<> parent)
    : recently_found_dialogs_{td, "recently_found", MAX_RECENT_DIALOGS}
    , recently_opened_dialogs_{td, "recently_opened", MAX_RECENT_DIALOGS}
    , td_(td)
    , parent_(std::move(parent)) {
  upload_media_callback_ = std::make_shared<UploadMediaCallback>();
  upload_thumbnail_callback_ = std::make_shared<UploadThumbnailCallback>();

  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_message_live_location_view_timeout_.set_callback(on_pending_message_live_location_view_timeout_callback);
  pending_message_live_location_view_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));

  preload_folder_dialog_list_timeout_.set_callback(on_preload_folder_dialog_list_timeout_callback);
  preload_folder_dialog_list_timeout_.set_callback_data(static_cast<void *>(this));

  update_viewed_messages_timeout_.set_callback(on_update_viewed_messages_timeout_callback);
  update_viewed_messages_timeout_.set_callback_data(static_cast<void *>(this));

  send_update_chat_read_inbox_timeout_.set_callback(on_send_update_chat_read_inbox_timeout_callback);
  send_update_chat_read_inbox_timeout_.set_callback_data(static_cast<void *>(this));
}

MessagesManager::~MessagesManager() {
  Scheduler::instance()->destroy_on_scheduler(
      G()->get_gc_scheduler_id(), ttl_nodes_, ttl_heap_, being_sent_messages_, update_message_ids_,
      update_scheduled_message_ids_, message_id_to_dialog_id_, last_clear_history_message_id_to_dialog_id_, dialogs_,
      postponed_chat_read_inbox_updates_, found_public_dialogs_, found_on_server_dialogs_, message_embedding_codes_[0],
      message_embedding_codes_[1], message_to_replied_media_timestamp_messages_,
      story_to_replied_media_timestamp_messages_, notification_group_id_to_dialog_id_, pending_get_channel_differences_,
      active_get_channel_differences_, get_channel_difference_to_log_event_id_, channel_get_difference_retry_timeouts_,
      is_channel_difference_finished_, expected_channel_pts_, expected_channel_max_message_id_,
      dialog_bot_command_message_ids_, message_full_id_to_file_source_id_, last_outgoing_forwarded_message_date_,
      dialog_viewed_messages_, previous_repaired_read_inbox_max_message_id_, failed_to_load_dialogs_);
}

MessagesManager::AddDialogData::AddDialogData(int32 dependent_dialog_count, unique_ptr<Message> &&last_message,
                                              unique_ptr<DraftMessage> &&draft_message)
    : dependent_dialog_count_(dependent_dialog_count)
    , last_message_(std::move(last_message))
    , draft_message_(std::move(draft_message)) {
}

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);
  send_closure_later(messages_manager->actor_id(messages_manager), &MessagesManager::on_channel_get_difference_timeout,
                     DialogId(dialog_id_int));
}

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);
  send_closure_later(messages_manager->actor_id(messages_manager), &MessagesManager::on_pending_message_views_timeout,
                     DialogId(dialog_id_int));
}

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

  auto messages_manager = static_cast<MessagesManager *>(messages_manager_ptr);
  send_closure_later(messages_manager->actor_id(messages_manager),
                     &MessagesManager::view_message_live_location_on_server, task_id);
}

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);
  send_closure_later(messages_manager->actor_id(messages_manager),
                     &MessagesManager::save_dialog_draft_message_on_server, DialogId(dialog_id_int));
}

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);
  send_closure_later(messages_manager->actor_id(messages_manager), &MessagesManager::do_read_history_on_server,
                     DialogId(dialog_id_int));
}

void MessagesManager::on_pending_updated_dialog_timeout_callback(void *messages_manager_ptr, int64 dialog_id_int) {
  // no check for G()->close_flag() to save dialogs even while closing

  auto messages_manager = static_cast<MessagesManager *>(messages_manager_ptr);
  // TODO it is unsafe to save dialog to database before binlog is flushed

  // no send_closure_later, because messages_manager can be not an actor while closing
  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);
  send_closure_later(messages_manager->actor_id(messages_manager), &MessagesManager::unload_dialog,
                     DialogId(dialog_id_int), -1);
}

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);
  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_preload_folder_dialog_list_timeout_callback(void *messages_manager_ptr, int64 folder_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::preload_folder_dialog_list,
                     FolderId(narrow_cast<int32>(folder_id_int)));
}

void MessagesManager::on_update_viewed_messages_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_update_viewed_messages_timeout,
                     DialogId(dialog_id_int));
}

void MessagesManager::on_send_update_chat_read_inbox_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_update_chat_read_inbox_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_mutable_slice();

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

void MessagesManager::save_dialog_to_database(DialogId dialog_id) {
  CHECK(G()->use_message_database());
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  LOG(INFO) << "Save " << dialog_id << " to database";
  vector<NotificationGroupKey> changed_group_keys;
  if (d->notification_info != nullptr) {
    d->notification_info->message_notification_group_.add_group_key_if_changed(changed_group_keys, dialog_id);
    d->notification_info->mention_notification_group_.add_group_key_if_changed(changed_group_keys, dialog_id);
  }
  bool can_reuse_notification_group = false;
  for (auto &group_key : changed_group_keys) {
    if (group_key.dialog_id == DialogId()) {
      can_reuse_notification_group = true;
    }
  }
  G()->td_db()->get_dialog_db_async()->add_dialog(
      dialog_id, d->folder_id, d->order, get_dialog_database_value(d), std::move(changed_group_keys),
      PromiseCreator::lambda([dialog_id, can_reuse_notification_group](Result<> result) {
        send_closure(G()->messages_manager(), &MessagesManager::on_save_dialog_to_database, dialog_id,
                     can_reuse_notification_group, result.is_ok());
      }));
}

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

  if (success && can_reuse_notification_group && !G()->close_flag()) {
    auto d = get_dialog(dialog_id);
    CHECK(d != nullptr);
    if (d->notification_info != nullptr) {
      try_reuse_notification_group(d->notification_info->message_notification_group_);
      try_reuse_notification_group(d->notification_info->mention_notification_group_);
    }
  }

  // TODO erase some events from binlog
}

void MessagesManager::try_reuse_notification_group(NotificationGroupInfo &group_info) {
  auto group_id = group_info.get_reused_group_id();
  if (group_id.is_valid()) {
    send_closure_later(G()->notification_manager(), &NotificationManager::try_reuse_notification_group_id, group_id);
    notification_group_id_to_dialog_id_.erase(group_id);
  }
}

void MessagesManager::invalidate_message_indexes(Dialog *d) {
  CHECK(d != nullptr);
  bool is_secret = d->dialog_id.get_type() == DialogType::SecretChat;
  for (size_t i = 0; i < d->message_count_by_index.size(); i++) {
    if (is_secret || i == static_cast<size_t>(message_search_filter_index(MessageSearchFilter::FailedToSend))) {
      // always know all messages
      d->first_database_message_id_by_index[i] = MessageId::min();
      // keep the count
    } else {
      // some messages are unknown; drop first_database_message_id and count
      d->first_database_message_id_by_index[i] = MessageId();
      d->message_count_by_index[i] = -1;
    }
  }
}

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 &= ~message_search_filter_index_mask(
      MessageSearchFilter::UnreadMention);  // unread mention count has been already manually updated
  index_mask &= ~message_search_filter_index_mask(
      MessageSearchFilter::UnreadReaction);  // unread reaction 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;
  }

  LOG(INFO) << "Update message count by " << diff << " in index mask " << index_mask;
  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 ||
            i == message_search_filter_index(MessageSearchFilter::FailedToSend)) {
          message_count = 0;
        } else {
          message_count = -1;
        }
      }
      on_dialog_updated(d->dialog_id, "update_message_count_by_index");
    }
    i++;
  }

  i = static_cast<int>(MessageSearchFilter::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 (td_->auth_manager_->is_bot() || m->message_id.is_scheduled() || m->message_id.is_yet_unsent()) {
    return 0;
  }
  if (m->is_failed_to_send) {
    return message_search_filter_index_mask(MessageSearchFilter::FailedToSend);
  }
  bool is_secret = dialog_id.get_type() == DialogType::SecretChat;
  if (!m->message_id.is_server() && !is_secret) {
    return 0;
  }

  int32 index_mask = 0;
  if (m->is_pinned) {
    index_mask |= message_search_filter_index_mask(MessageSearchFilter::Pinned);
  }
  // retain second condition just in case
  if (m->is_content_secret || (!m->ttl.is_empty() && !is_secret)) {
    return index_mask;
  }
  index_mask |= get_message_content_index_mask(m->content.get(), td_, m->is_outgoing);
  if (m->contains_mention) {
    index_mask |= message_search_filter_index_mask(MessageSearchFilter::Mention);
    if (m->contains_unread_mention) {
      index_mask |= message_search_filter_index_mask(MessageSearchFilter::UnreadMention);
    }
  }
  if (has_unread_message_reactions(dialog_id, m)) {
    index_mask |= message_search_filter_index_mask(MessageSearchFilter::UnreadReaction);
  }
  LOG(INFO) << "Have index mask " << index_mask << " for " << m->message_id << " in " << dialog_id;
  return index_mask;
}

void MessagesManager::update_reply_count_by_message(Dialog *d, int diff, const Message *m) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  if (td_->auth_manager_->is_bot() || !m->top_thread_message_id.is_valid() ||
      m->top_thread_message_id == m->message_id || !m->message_id.is_valid() || !m->message_id.is_server()) {
    return;
  }

  update_message_reply_count(d, m->top_thread_message_id, get_message_sender(m), m->message_id,
                             diff < 0 ? G()->unix_time() : m->date, diff);
}

void MessagesManager::update_message_reply_count(Dialog *d, MessageId message_id, DialogId replier_dialog_id,
                                                 MessageId reply_message_id, int32 update_date, int diff,
                                                 bool is_recursive) {
  if (d == nullptr) {
    return;
  }

  Message *m = get_message(d, message_id);
  if (m == nullptr || !is_active_message_reply_info(d->dialog_id, m->reply_info)) {
    return;
  }
  LOG(INFO) << "Update reply count to " << message_id << " in " << d->dialog_id << " by " << diff << " from "
            << reply_message_id << " sent by " << replier_dialog_id;
  if (m->interaction_info_update_date < update_date &&
      m->reply_info.add_reply(replier_dialog_id, reply_message_id, diff)) {
    on_message_reply_info_changed(d->dialog_id, m);
    on_message_changed(d, m, true, "update_message_reply_count_by_message");
  }

  if (!is_recursive && is_discussion_message(d->dialog_id, m)) {
    auto linked_message_full_id = m->forward_info->get_last_message_full_id();
    update_message_reply_count(get_dialog(linked_message_full_id.get_dialog_id()),
                               linked_message_full_id.get_message_id(), replier_dialog_id, reply_message_id,
                               update_date, diff, true);
  }
}

bool MessagesManager::have_dialog_scheduled_messages_in_memory(const Dialog *d) {
  return d->scheduled_messages != nullptr && !d->scheduled_messages->scheduled_messages_.empty();
}

bool MessagesManager::is_allowed_useless_update(const tl_object_ptr<telegram_api::Update> &update) {
  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 ||
      constructor_id == telegram_api::updateNewChannelMessage::ID) {
    // new outgoing messages are received again if random_id coincide
    return true;
  }

  return false;
}

void MessagesManager::skip_old_pending_pts_update(tl_object_ptr<telegram_api::Update> &&update, int32 new_pts,
                                                  int32 old_pts, int32 pts_count, const char *source) {
  LOG(DEBUG) << "Skip old update with PTS = " << new_pts << ", current PTS = " << old_pts;
  if (update->get_id() == telegram_api::updateNewMessage::ID) {
    auto update_new_message = static_cast<telegram_api::updateNewMessage *>(update.get());
    auto message_full_id = MessageFullId::get_message_full_id(update_new_message->message_, false);
    if (update_message_ids_.count(message_full_id) > 0) {
      // apply the sent message anyway, even it could have been deleted or edited already

      CHECK(message_full_id.get_dialog_id().get_type() == DialogType::User ||
            message_full_id.get_dialog_id().get_type() == DialogType::Chat);  // checked in check_pts_update
      delete_messages_from_updates({message_full_id.get_message_id()}, false);

      auto added_message_full_id = on_get_message(std::move(update_new_message->message_), true, false, false,
                                                  "updateNewMessage with an awaited message");
      if (added_message_full_id != message_full_id) {
        LOG(ERROR) << "Failed to add an awaited " << message_full_id << " from " << source;
      }
      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 the sent message anyway, even it could have been deleted or edited already
      delete_messages_from_updates({update_sent_message->message_id_}, false);
      on_send_message_success(update_sent_message->random_id_, update_sent_message->message_id_,
                              update_sent_message->date_, update_sent_message->ttl_period_, FileId(),
                              "process old updateSentMessage");
      return;
    }
    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;
}

MessagesManager::Dialog *MessagesManager::get_service_notifications_dialog() {
  UserId service_notifications_user_id = td_->contacts_manager_->add_service_notifications_user();
  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::extract_authentication_codes(DialogId dialog_id, const Message *m,
                                                   vector<string> &authentication_codes) {
  CHECK(m != nullptr);
  if (dialog_id != DialogId(ContactsManager::get_service_notifications_user_id()) || !m->message_id.is_valid() ||
      !m->message_id.is_server() || m->content->get_type() != MessageContentType::Text || m->is_outgoing) {
    return;
  }
  auto *formatted_text = get_message_content_text(m->content.get());
  CHECK(formatted_text != nullptr);
  const string &text = formatted_text->text;
  for (size_t i = 0; i < text.size(); i++) {
    if (is_digit(text[i])) {
      string code;
      do {
        if (is_digit(text[i])) {
          code += text[i++];
          continue;
        }
        if (text[i] == '-') {
          i++;
          continue;
        }
        break;
      } while (true);
      if (5 <= code.size() && code.size() <= 7) {
        authentication_codes.push_back(code);
      }
    }
  }
}

void MessagesManager::save_auth_notification_ids() {
  auto min_date = G()->unix_time() - AUTH_NOTIFICATION_ID_CACHE_TIME;
  vector<string> stored_ids;
  for (const auto &it : auth_notification_id_date_) {
    auto date = it.second;
    if (date < min_date) {
      continue;
    }
    stored_ids.push_back(it.first);
    stored_ids.push_back(to_string(date));
  }

  if (stored_ids.empty()) {
    G()->td_db()->get_binlog_pmc()->erase("auth_notification_ids");
    return;
  }

  G()->td_db()->get_binlog_pmc()->set("auth_notification_ids", implode(stored_ids, ','));
}

void MessagesManager::on_update_service_notification(tl_object_ptr<telegram_api::updateServiceNotification> &&update,
                                                     bool skip_new_entities, Promise<Unit> &&promise) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  bool has_date = (update->flags_ & telegram_api::updateServiceNotification::INBOX_DATE_MASK) != 0;
  auto date = has_date ? update->inbox_date_ : G()->unix_time();
  if (date <= 0) {
    LOG(ERROR) << "Receive message date " << date << " in " << to_string(update);
    return;
  }
  bool is_auth_notification = update->type_.size() >= 5 && begins_with(update->type_, "auth");
  if (is_auth_notification) {
    auto &old_date = auth_notification_id_date_[update->type_.substr(4)];
    if (date <= old_date) {
      LOG(INFO) << "Skip already applied " << to_string(update);
      return;
    }
    old_date = date;

    if (auth_notification_id_date_.size() > MAX_SAVED_AUTH_NOTIFICATION_IDS) {
      auto min_date = date + 1;
      const string *min_key = nullptr;
      for (const auto &it : auth_notification_id_date_) {
        if (it.second < min_date) {
          min_date = it.second;
          min_key = &it.first;
        }
      }
      CHECK(min_key != nullptr);
      auth_notification_id_date_.erase(*min_key);
    }
  }

  bool is_authorized = td_->auth_manager_->is_authorized();
  bool is_user = is_authorized && !td_->auth_manager_->is_bot();
  auto contacts_manager = is_authorized ? td_->contacts_manager_.get() : nullptr;
  auto message_text = get_message_text(contacts_manager, std::move(update->message_), std::move(update->entities_),
                                       skip_new_entities, !is_user, date, false, "on_update_service_notification");
  DialogId owner_dialog_id = is_user ? get_service_notifications_dialog()->dialog_id : DialogId();
  MessageSelfDestructType ttl;
  bool disable_web_page_preview = false;
  auto content = get_message_content(td_, std::move(message_text), std::move(update->media_), owner_dialog_id, date,
                                     false, UserId(), &ttl, &disable_web_page_preview, "updateServiceNotification");
  bool is_content_secret = ttl.is_secret_message_content(content->get_type());

  if (update->popup_) {
    send_closure(
        G()->td(), &Td::send_update,
        td_api::make_object<td_api::updateServiceNotification>(
            update->type_, get_message_content_object(content.get(), td_, owner_dialog_id, date, is_content_secret,
                                                      true, -1, update->invert_media_, disable_web_page_preview)));
  }
  if (has_date && is_user) {
    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->sender_user_id = dialog_id.get_user_id();
    new_message->date = date;
    new_message->ttl = ttl;
    new_message->disable_web_page_preview = disable_web_page_preview;
    new_message->is_content_secret = is_content_secret;
    new_message->invert_media = update->invert_media_;
    new_message->content = std::move(content);

    bool need_update = true;
    bool need_update_dialog_pos = false;

    Dependencies dependencies;
    add_message_dependencies(dependencies, new_message.get());
    for (auto dependent_dialog_id : dependencies.get_dialog_ids()) {
      force_create_dialog(dependent_dialog_id, "on_update_service_notification", true);
    }

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

    if (need_update_dialog_pos) {
      send_update_chat_last_message(d, "on_update_service_notification");
    }
  }
  promise.set_value(Unit());

  if (is_auth_notification) {
    save_auth_notification_ids();
  }
}

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;
  }
  on_update_dialog_folder_id(DialogId(channel_id), FolderId(update->folder_id_));
  on_read_channel_inbox(channel_id, MessageId(ServerMessageId(update->max_id_)), update->still_unread_count_,
                        update->pts_, "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;
  }
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  DialogId dialog_id = DialogId(channel_id);

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

  if ((update->flags_ & telegram_api::updateChannelReadMessagesContents::TOP_MSG_ID_MASK) != 0) {
    // TODO
    return;
  }

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

void MessagesManager::on_update_read_message_comments(DialogId dialog_id, MessageId message_id,
                                                      MessageId max_message_id, MessageId last_read_inbox_message_id,
                                                      MessageId last_read_outbox_message_id, int32 unread_count) {
  Dialog *d = get_dialog_force(dialog_id, "on_update_read_message_comments");
  if (d == nullptr) {
    LOG(INFO) << "Ignore update of read message comments in unknown " << dialog_id << " in updateReadDiscussion";
    return;
  }

  auto m = get_message_force(d, message_id, "on_update_read_message_comments");
  if (m == nullptr || !m->message_id.is_server() || m->top_thread_message_id != m->message_id) {
    return;
  }
  if (m->is_topic_message) {
    td_->forum_topic_manager_->on_update_forum_topic_unread(
        dialog_id, message_id, max_message_id, last_read_inbox_message_id, last_read_outbox_message_id, unread_count);
  }
  if (!is_active_message_reply_info(dialog_id, m->reply_info)) {
    return;
  }
  if (m->reply_info.update_max_message_ids(max_message_id, last_read_inbox_message_id, last_read_outbox_message_id)) {
    on_message_reply_info_changed(dialog_id, m);
    on_message_changed(d, m, true, "on_update_read_message_comments");
  }
}

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;
  }
  if (!td_->contacts_manager_->have_channel_force(channel_id, "on_update_channel_too_long")) {
    LOG(INFO) << "Skip updateChannelTooLong about unknown " << channel_id;
    return;
  }

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

  if (d != nullptr) {
    if (update->pts_ == 0 || update->pts_ > d->pts) {
      get_channel_difference(dialog_id, d->pts, update->pts_, MessageId(), true, "on_update_channel_too_long 1");
    }
  } else {
    if (force_apply) {
      get_channel_difference(dialog_id, -1, update->pts_, MessageId(), 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_view_count(MessageFullId message_full_id, int32 view_count) {
  if (view_count < 0) {
    LOG(ERROR) << "Receive " << view_count << " views in updateChannelMessageViews for " << message_full_id;
    return;
  }
  update_message_interaction_info(message_full_id, view_count, -1, false, nullptr, false, nullptr);
}

void MessagesManager::on_update_message_forward_count(MessageFullId message_full_id, int32 forward_count) {
  if (forward_count < 0) {
    LOG(ERROR) << "Receive " << forward_count << " forwards in updateChannelMessageForwards for " << message_full_id;
    return;
  }
  update_message_interaction_info(message_full_id, -1, forward_count, false, nullptr, false, nullptr);
}

void MessagesManager::on_update_message_reactions(MessageFullId message_full_id,
                                                  tl_object_ptr<telegram_api::messageReactions> &&reactions,
                                                  Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  auto new_reactions = MessageReactions::get_message_reactions(td_, std::move(reactions), td_->auth_manager_->is_bot());
  if (!have_message_force(message_full_id, "on_update_message_reactions")) {
    auto dialog_id = message_full_id.get_dialog_id();
    if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
      LOG(INFO) << "Ignore updateMessageReaction in inaccessible " << message_full_id;
      promise.set_value(Unit());
      return;
    }
    Dialog *d = get_dialog(dialog_id);
    if (d == nullptr) {
      LOG(INFO) << "Ignore updateMessageReaction in unknown " << dialog_id;
      promise.set_value(Unit());
      return;
    }

    // there is no message, so the update can be ignored
    if ((new_reactions != nullptr && !new_reactions->unread_reactions_.empty()) || d->unread_reaction_count > 0) {
      // but if there are unread reactions or the chat has unread reactions,
      // then number of unread reactions could have been changed, so reload the number of unread reactions
      repair_dialog_unread_reaction_count(d, std::move(promise), "on_update_message_reactions");
    } else {
      promise.set_value(Unit());
    }
    return;
  }

  update_message_interaction_info(message_full_id, -1, -1, false, nullptr, true, std::move(new_reactions));
  promise.set_value(Unit());
}

void MessagesManager::update_message_reactions(MessageFullId message_full_id,
                                               unique_ptr<MessageReactions> &&reactions) {
  update_message_interaction_info(message_full_id, -1, -1, false, nullptr, true, std::move(reactions));
}

void MessagesManager::on_get_message_reaction_list(
    MessageFullId message_full_id, const ReactionType &reaction_type,
    FlatHashMap<ReactionType, vector<DialogId>, ReactionTypeHash> reaction_types, int32 total_count) {
  const Message *m = get_message_force(message_full_id, "on_get_message_reaction_list");
  if (m == nullptr || m->reactions == nullptr) {
    return;
  }

  // it's impossible to use received reactions to update message reactions, because there is no way to find,
  // which reactions are chosen by the current user, so just reload message reactions for consistency
  if (m->reactions->are_consistent_with_list(reaction_type, std::move(reaction_types), total_count)) {
    return;
  }

  LOG(INFO) << "Need reload reactions in " << message_full_id << " for consistency";

  auto it = pending_reactions_.find(message_full_id);
  if (it != pending_reactions_.end()) {
    it->second.was_updated = true;
  } else {
    queue_message_reactions_reload(message_full_id);
  }
}

void MessagesManager::on_update_message_interaction_info(MessageFullId message_full_id, int32 view_count,
                                                         int32 forward_count, bool has_reply_info,
                                                         tl_object_ptr<telegram_api::messageReplies> &&reply_info) {
  if (view_count < 0 || forward_count < 0) {
    LOG(ERROR) << "Receive " << view_count << "/" << forward_count << " interaction counters for " << message_full_id;
    return;
  }
  update_message_interaction_info(message_full_id, view_count, forward_count, has_reply_info, std::move(reply_info),
                                  false, nullptr);
}

void MessagesManager::on_pending_message_views_timeout(DialogId dialog_id) {
  if (G()->close_flag()) {
    return;
  }

  auto it = pending_message_views_.find(dialog_id);
  if (it == pending_message_views_.end()) {
    return;
  }
  auto &pending_viewed_message_ids = it->second.message_ids_;
  bool increment_view_counter = it->second.increment_view_counter_;

  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  const size_t MAX_MESSAGE_VIEWS = 100;  // server side limit
  vector<MessageId> message_ids;
  message_ids.reserve(min(pending_viewed_message_ids.size(), MAX_MESSAGE_VIEWS));
  for (auto message_id : pending_viewed_message_ids) {
    auto *m = get_message(d, message_id);
    if (m == nullptr) {
      continue;
    }
    if (m->has_get_message_views_query) {
      if (!increment_view_counter || m->need_view_counter_increment) {
        continue;
      }
      m->need_view_counter_increment = true;
    } else {
      m->has_get_message_views_query = true;
      m->need_view_counter_increment = increment_view_counter;
    }
    message_ids.push_back(message_id);
    if (message_ids.size() >= MAX_MESSAGE_VIEWS) {
      td_->create_handler<GetMessagesViewsQuery>()->send(dialog_id, std::move(message_ids), increment_view_counter);
      message_ids.clear();
    }
  }
  if (!message_ids.empty()) {
    td_->create_handler<GetMessagesViewsQuery>()->send(dialog_id, std::move(message_ids), increment_view_counter);
  }
  pending_message_views_.erase(it);
}

void MessagesManager::update_message_interaction_info(MessageFullId message_full_id, int32 view_count,
                                                      int32 forward_count, bool has_reply_info,
                                                      tl_object_ptr<telegram_api::messageReplies> &&reply_info,
                                                      bool has_reactions, unique_ptr<MessageReactions> &&reactions) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "update_message_interaction_info");
  if (d == nullptr) {
    return;
  }
  auto message_id = message_full_id.get_message_id();
  Message *m = get_message_force(d, message_id, "update_message_interaction_info");
  if (m == nullptr) {
    LOG(INFO) << "Ignore message interaction info about unknown " << message_full_id;
    if (!message_id.is_scheduled() && d->last_new_message_id.is_valid() && message_id > d->last_new_message_id &&
        dialog_id.get_type() == DialogType::Channel) {
      get_channel_difference(dialog_id, d->pts, 0, message_id, true, "update_message_interaction_info");
    }
    return;
  }

  if (view_count < 0) {
    view_count = m->view_count;
  }
  if (forward_count < 0) {
    forward_count = m->forward_count;
  }
  bool is_empty_reply_info = reply_info == nullptr;
  MessageReplyInfo new_reply_info(td_, std::move(reply_info), td_->auth_manager_->is_bot());
  if (new_reply_info.is_empty() && !is_empty_reply_info) {
    has_reply_info = false;
  }

  if (update_message_interaction_info(d, m, view_count, forward_count, has_reply_info, std::move(new_reply_info),
                                      has_reactions, std::move(reactions), "update_message_interaction_info")) {
    on_message_changed(d, m, true, "update_message_interaction_info");
  }
}

bool MessagesManager::is_thread_message(DialogId dialog_id, const Message *m) const {
  CHECK(m != nullptr);
  return is_thread_message(dialog_id, m->message_id, m->reply_info, m->content->get_type());
}

bool MessagesManager::is_thread_message(DialogId dialog_id, MessageId message_id, const MessageReplyInfo &reply_info,
                                        MessageContentType content_type) const {
  if (dialog_id.get_type() != DialogType::Channel || td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
    return false;
  }
  if (!message_id.is_valid() || !message_id.is_server()) {
    return false;
  }
  return !reply_info.is_empty() || reply_info.was_dropped() || content_type == MessageContentType::TopicCreate;
}

bool MessagesManager::is_active_message_reply_info(DialogId dialog_id, const MessageReplyInfo &reply_info) const {
  if (reply_info.is_empty()) {
    return false;
  }
  if (dialog_id.get_type() != DialogType::Channel) {
    return false;
  }

  if (!reply_info.is_comment_) {
    return true;
  }
  if (!td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
    return true;
  }

  auto channel_id = dialog_id.get_channel_id();
  if (!td_->contacts_manager_->get_channel_has_linked_channel(channel_id)) {
    return false;
  }

  auto linked_channel_id =
      td_->contacts_manager_->get_channel_linked_channel_id(channel_id, "is_active_message_reply_info");
  if (!linked_channel_id.is_valid()) {
    // keep the comment button while linked channel is unknown
    send_closure_later(G()->contacts_manager(), &ContactsManager::load_channel_full, channel_id, false, Promise<Unit>(),
                       "is_active_message_reply_info");
    return true;
  }

  if (linked_channel_id != reply_info.channel_id_) {
    return false;
  }

  return true;
}

bool MessagesManager::is_visible_message_reply_info(DialogId dialog_id, const Message *m) const {
  CHECK(m != nullptr);
  if (!m->message_id.is_valid()) {
    return false;
  }
  bool is_broadcast = td_->dialog_manager_->is_broadcast_channel(dialog_id);
  if (!m->message_id.is_server() && !(is_broadcast && m->message_id.is_yet_unsent())) {
    return false;
  }
  if (is_broadcast && (m->had_reply_markup || m->reply_markup != nullptr)) {
    return false;
  }
  if (!is_active_message_reply_info(dialog_id, m->reply_info)) {
    return false;
  }
  if (m->reply_info.is_comment_ && is_broadcast &&
      td_->contacts_manager_->have_channel_force(m->reply_info.channel_id_, "is_visible_message_reply_info") &&
      !td_->contacts_manager_->have_input_peer_channel(m->reply_info.channel_id_, AccessRights::Read)) {
    // keep the comment button while have no information about the linked channel
    return false;
  }
  return true;
}

bool MessagesManager::is_visible_message_reactions(DialogId dialog_id, const Message *m) const {
  if (m == nullptr) {
    return false;
  }

  const Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (get_message_active_reactions(d, m).empty()) {
    return false;
  }
  if (m->available_reactions_generation != d->available_reactions_generation) {
    return false;
  }
  return true;
}

bool MessagesManager::has_unread_message_reactions(DialogId dialog_id, const Message *m) const {
  if (td_->auth_manager_->is_bot()) {
    return false;
  }
  CHECK(m != nullptr);
  return m->reactions != nullptr && !m->reactions->unread_reactions_.empty() &&
         is_visible_message_reactions(dialog_id, m);
}

void MessagesManager::on_message_reply_info_changed(DialogId dialog_id, const Message *m) const {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  if (is_visible_message_reply_info(dialog_id, m)) {
    send_update_message_interaction_info(dialog_id, m);
  }
}

td_api::object_ptr<td_api::messageInteractionInfo> MessagesManager::get_message_interaction_info_object(
    DialogId dialog_id, const Message *m) const {
  bool is_visible_reply_info = is_visible_message_reply_info(dialog_id, m);
  bool has_reactions =
      m->reactions != nullptr && !m->reactions->reactions_.empty() && is_visible_message_reactions(dialog_id, m);
  if (m->view_count == 0 && m->forward_count == 0 && !is_visible_reply_info && !has_reactions) {
    return nullptr;
  }
  if (m->message_id.is_scheduled() &&
      (m->forward_info == nullptr || td_->dialog_manager_->is_broadcast_channel(dialog_id))) {
    return nullptr;
  }
  if (m->message_id.is_local() && m->forward_info == nullptr) {
    return nullptr;
  }

  td_api::object_ptr<td_api::messageReplyInfo> reply_info;
  if (is_visible_reply_info) {
    auto expected_dialog_id = m->reply_info.is_comment_ ? DialogId(m->reply_info.channel_id_) : dialog_id;
    const Dialog *d = get_dialog(expected_dialog_id);
    reply_info =
        m->reply_info.get_message_reply_info_object(td_, d != nullptr ? d->last_read_inbox_message_id : MessageId());
    CHECK(reply_info != nullptr);
  }

  td_api::object_ptr<td_api::messageReactions> reactions;
  if (has_reactions) {
    UserId my_user_id;
    UserId peer_user_id;
    if (dialog_id.get_type() == DialogType::User) {
      my_user_id = td_->contacts_manager_->get_my_id();
      peer_user_id = dialog_id.get_user_id();
    }
    reactions = m->reactions->get_message_reactions_object(td_, my_user_id, peer_user_id);
  }

  return td_api::make_object<td_api::messageInteractionInfo>(m->view_count, m->forward_count, std::move(reply_info),
                                                             std::move(reactions));
}

vector<td_api::object_ptr<td_api::unreadReaction>> MessagesManager::get_unread_reactions_object(
    DialogId dialog_id, const Message *m) const {
  if (!has_unread_message_reactions(dialog_id, m)) {
    return {};
  }

  vector<td_api::object_ptr<td_api::unreadReaction>> unread_reactions;
  for (const auto &unread_reaction : m->reactions->unread_reactions_) {
    auto unread_reaction_object = unread_reaction.get_unread_reaction_object(td_);
    if (unread_reaction_object != nullptr) {
      unread_reactions.push_back(std::move(unread_reaction_object));
    }
  }
  return unread_reactions;
}

bool MessagesManager::update_message_interaction_info(Dialog *d, Message *m, int32 view_count, int32 forward_count,
                                                      bool has_reply_info, MessageReplyInfo &&reply_info,
                                                      bool has_reactions, unique_ptr<MessageReactions> &&reactions,
                                                      const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return false;
  }

  CHECK(d != nullptr);
  CHECK(m != nullptr);
  auto dialog_id = d->dialog_id;
  m->interaction_info_update_date = G()->unix_time();  // doesn't force message saving
  if (m->message_id.is_valid_scheduled()) {
    has_reply_info = false;
    has_reactions = false;
  }
  bool need_update_reply_info = has_reply_info && m->reply_info.need_update_to(reply_info);
  if (has_reply_info && m->reply_info.channel_id_ == reply_info.channel_id_) {
    if (need_update_reply_info) {
      reply_info.update_max_message_ids(m->reply_info);
    } else {
      if (m->reply_info.update_max_message_ids(reply_info) && view_count <= m->view_count &&
          forward_count <= m->forward_count) {
        on_message_reply_info_changed(dialog_id, m);
        on_message_changed(d, m, true, "update_message_interaction_info");
      }
    }
  }
  MessageFullId message_full_id{dialog_id, m->message_id};
  if (has_reactions) {
    auto it = pending_reactions_.find(message_full_id);
    if (it != pending_reactions_.end()) {
      LOG(INFO) << "Ignore reactions for " << message_full_id << ", because they are being changed";
      has_reactions = false;
      it->second.was_updated = true;
    }
    if (has_reactions && pending_read_reactions_.count(message_full_id) > 0) {
      LOG(INFO) << "Ignore reactions for " << message_full_id << ", because they are being read";
      has_reactions = false;
    }
  }
  if (has_reactions && reactions != nullptr) {
    if (m->reactions != nullptr) {
      reactions->update_from(*m->reactions);
    }
    reactions->sort_reactions(active_reaction_pos_);
    reactions->fix_chosen_reaction();
    reactions->fix_my_recent_chooser_dialog_id(td_->dialog_manager_->get_my_dialog_id());
  }
  bool need_update_reactions =
      has_reactions && MessageReactions::need_update_message_reactions(m->reactions.get(), reactions.get());
  bool need_update_unread_reactions =
      has_reactions && MessageReactions::need_update_unread_reactions(m->reactions.get(), reactions.get());
  bool need_update_chosen_reaction_order = has_reactions && reactions != nullptr && m->reactions != nullptr &&
                                           m->reactions->chosen_reaction_order_ != reactions->chosen_reaction_order_;
  if (view_count > m->view_count || forward_count > m->forward_count || need_update_reply_info ||
      need_update_reactions || need_update_unread_reactions || need_update_chosen_reaction_order) {
    LOG(DEBUG) << "Update interaction info of " << message_full_id << " from " << m->view_count << '/'
               << m->forward_count << '/' << m->reply_info << '/' << m->reactions << " to " << view_count << '/'
               << forward_count << '/' << reply_info << '/' << reactions
               << ", need_update_reply_info = " << need_update_reply_info
               << ", need_update_reactions = " << need_update_reactions
               << ", need_update_unread_reactions = " << need_update_unread_reactions
               << ", need_update_chosen_reaction_order = " << need_update_chosen_reaction_order;
    bool need_update = false;
    if (view_count > m->view_count) {
      m->view_count = view_count;
      need_update = true;
    }
    if (forward_count > m->forward_count) {
      m->forward_count = forward_count;
      need_update = true;
    }
    if (need_update_reply_info) {
      if (m->reply_info.channel_id_ != reply_info.channel_id_) {
        if (m->reply_info.channel_id_.is_valid() && reply_info.channel_id_.is_valid() && m->message_id.is_server()) {
          LOG(ERROR) << "Reply info of " << message_full_id << " changed from " << m->reply_info << " to " << reply_info
                     << " from " << source;
        }
      }
      m->reply_info = std::move(reply_info);
      if (!m->top_thread_message_id.is_valid() && is_thread_message(dialog_id, m)) {
        m->top_thread_message_id = m->message_id;
      }
      need_update |= is_visible_message_reply_info(dialog_id, m);
    }
    int32 new_dialog_unread_reaction_count = -1;
    if (need_update_reactions || need_update_unread_reactions) {
      CHECK(m->message_id.is_valid());

      auto old_chosen_tags = get_chosen_tags(m->reactions);
      int32 unread_reaction_diff = 0;
      unread_reaction_diff -= (has_unread_message_reactions(dialog_id, m) ? 1 : 0);
      m->reactions = std::move(reactions);
      m->available_reactions_generation = d->available_reactions_generation;
      unread_reaction_diff += (has_unread_message_reactions(dialog_id, m) ? 1 : 0);
      auto new_chosen_tags = get_chosen_tags(m->reactions);

      td_->reaction_manager_->update_saved_messages_tags(m->saved_messages_topic_id, old_chosen_tags, new_chosen_tags);

      if (is_visible_message_reactions(dialog_id, m)) {
        need_update |= need_update_reactions;
        if (need_update_unread_reactions) {
          if (unread_reaction_diff != 0) {
            // remove_message_notification_id(d, m, true, true);

            if (d->unread_reaction_count + unread_reaction_diff < 0) {
              if (is_dialog_inited(d)) {
                LOG(ERROR) << "Unread reaction count of " << dialog_id << " became negative from " << source;
              }
            } else {
              set_dialog_unread_reaction_count(d, d->unread_reaction_count + unread_reaction_diff);
              on_dialog_updated(dialog_id, "update_message_interaction_info");
            }
          }

          if (unread_reaction_diff < 0) {
            send_update_message_unread_reactions(dialog_id, m, d->unread_reaction_count);
          } else {
            new_dialog_unread_reaction_count = d->unread_reaction_count;
          }
        }
      }
    } else if (has_reactions) {
      bool is_changed = false;
      if (m->available_reactions_generation != d->available_reactions_generation) {
        m->available_reactions_generation = d->available_reactions_generation;
        is_changed = true;
      }
      if (need_update_chosen_reaction_order) {
        m->reactions->chosen_reaction_order_ = std::move(reactions->chosen_reaction_order_);
        is_changed = true;
      }
      if (is_changed) {
        on_message_changed(d, m, false, "update_message_interaction_info");
      }
    }
    if (need_update) {
      send_update_message_interaction_info(dialog_id, m);
    }
    if (new_dialog_unread_reaction_count >= 0) {
      send_update_message_unread_reactions(dialog_id, m, new_dialog_unread_reaction_count);
    }
    return true;
  } else if (has_reactions && m->available_reactions_generation != d->available_reactions_generation) {
    m->available_reactions_generation = d->available_reactions_generation;
    on_message_changed(d, m, false, "update_message_interaction_info");
  }
  return false;
}

void MessagesManager::on_update_live_location_viewed(MessageFullId message_full_id) {
  LOG(DEBUG) << "Live location was viewed in " << message_full_id;
  if (!are_active_live_location_messages_loaded_) {
    get_active_live_location_messages(PromiseCreator::lambda([actor_id = actor_id(this), message_full_id](Unit result) {
      send_closure(actor_id, &MessagesManager::on_update_live_location_viewed, message_full_id);
    }));
    return;
  }

  auto active_live_location_message_ids = get_active_live_location_messages(Auto());
  if (!td::contains(active_live_location_message_ids, message_full_id)) {
    LOG(DEBUG) << "Can't find " << message_full_id << " in " << active_live_location_message_ids;
    return;
  }

  send_update_message_live_location_viewed(message_full_id);
}

void MessagesManager::on_update_some_live_location_viewed(Promise<Unit> &&promise) {
  LOG(DEBUG) << "Some live location was viewed";
  if (!are_active_live_location_messages_loaded_) {
    get_active_live_location_messages(
        PromiseCreator::lambda([actor_id = actor_id(this), promise = std::move(promise)](Unit result) mutable {
          send_closure(actor_id, &MessagesManager::on_update_some_live_location_viewed, std::move(promise));
        }));
    return;
  }

  // update all live locations, because it is unknown, which exactly was viewed
  auto active_live_location_message_ids = get_active_live_location_messages(Auto());
  for (const auto &message_full_id : active_live_location_message_ids) {
    send_update_message_live_location_viewed(message_full_id);
  }

  promise.set_value(Unit());
}

void MessagesManager::on_update_message_extended_media(
    MessageFullId message_full_id, telegram_api::object_ptr<telegram_api::MessageExtendedMedia> extended_media) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "on_update_message_extended_media");
  if (d == nullptr) {
    LOG(INFO) << "Ignore update of message extended media in unknown " << dialog_id;
    return;
  }

  auto m = get_message_force(d, message_full_id.get_message_id(), "on_update_message_extended_media");
  if (m == nullptr) {
    LOG(INFO) << "Ignore update of message extended media in unknown " << message_full_id;
    return;
  }

  auto content = m->content.get();
  auto content_type = content->get_type();
  if (content_type != MessageContentType::Invoice) {
    if (content_type != MessageContentType::Unsupported) {
      LOG(ERROR) << "Receive updateMessageExtendedMedia for " << message_full_id << " of type " << content_type;
    }
    return;
  }
  if (update_message_content_extended_media(content, std::move(extended_media), dialog_id, td_)) {
    send_update_message_content(d, m, true, "on_update_message_extended_media");
    on_message_changed(d, m, true, "on_update_message_extended_media");
    on_message_notification_changed(d, m, "on_update_message_extended_media");  // usually a no-op
  }
}

bool MessagesManager::need_skip_bot_commands(DialogId dialog_id, const Message *m) const {
  if (td_->auth_manager_->is_bot()) {
    return false;
  }

  if (m != nullptr && m->message_id.is_scheduled()) {
    return true;
  }

  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  return (d->is_has_bots_inited && !d->has_bots) || td_->dialog_manager_->is_broadcast_channel(dialog_id);
}

void MessagesManager::on_external_update_message_content(MessageFullId message_full_id) {
  Dialog *d = get_dialog(message_full_id.get_dialog_id());
  CHECK(d != nullptr);
  Message *m = get_message(d, message_full_id.get_message_id());
  CHECK(m != nullptr);
  send_update_message_content(d, m, true, "on_external_update_message_content");
  // must not call on_message_changed, because the message itself wasn't changed
  send_update_last_message_if_needed(d, m, "on_external_update_message_content");
  on_message_notification_changed(d, m, "on_external_update_message_content");
}

void MessagesManager::on_update_message_content(MessageFullId message_full_id) {
  Dialog *d = get_dialog(message_full_id.get_dialog_id());
  CHECK(d != nullptr);
  Message *m = get_message(d, message_full_id.get_message_id());
  CHECK(m != nullptr);
  send_update_message_content(d, m, true, "on_update_message_content");
  on_message_changed(d, m, true, "on_update_message_content");
  on_message_notification_changed(d, m, "on_update_message_content");
}

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

    m->contains_unread_mention = false;
    if (d->unread_mention_count == 0) {
      if (is_dialog_inited(d)) {
        LOG(ERROR) << "Unread mention count of " << d->dialog_id << " became negative from " << source;
      }
    } else {
      set_dialog_unread_mention_count(d, d->unread_mention_count - 1);
      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,
                 td_api::make_object<td_api::updateMessageMentionRead>(
                     get_chat_id_object(d->dialog_id, "updateMessageMentionRead"), m->message_id.get(),
                     d->unread_mention_count));
    return true;
  }
  return false;
}

bool MessagesManager::remove_message_unread_reactions(Dialog *d, Message *m, const char *source) {
  CHECK(m != nullptr);
  CHECK(!m->message_id.is_scheduled());
  if (!has_unread_message_reactions(d->dialog_id, m)) {
    return false;
  }
  // remove_message_notification_id(d, m, true, true);

  m->reactions->unread_reactions_.clear();
  if (d->unread_reaction_count == 0) {
    if (is_dialog_inited(d)) {
      LOG(ERROR) << "Unread reaction count of " << d->dialog_id << " became negative from " << source;
    }
  } else {
    set_dialog_unread_reaction_count(d, d->unread_reaction_count - 1);
    on_dialog_updated(d->dialog_id, "remove_message_unread_reactions");
  }
  LOG(INFO) << "Update unread reaction count in " << d->dialog_id << " to " << d->unread_reaction_count
            << " by reading " << m->message_id << " from " << source;

  send_update_message_unread_reactions(d->dialog_id, m, d->unread_reaction_count);

  return true;
}

void MessagesManager::on_read_channel_inbox(ChannelId channel_id, MessageId max_message_id, int32 server_unread_count,
                                            int32 pts, const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(!max_message_id.is_scheduled());
  if (!max_message_id.is_valid() && server_unread_count <= 0) {
    return;
  }

  DialogId dialog_id(channel_id);
  Dialog *d = get_dialog_force(dialog_id, source);
  if (d == nullptr) {
    LOG(INFO) << "Receive read inbox in unknown " << dialog_id << " from " << source;
    return;
  }

  /*
  // dropping unread count can make things worse, so don't drop it
  if (server_unread_count > 0 && G()->use_message_database() && d->is_last_read_inbox_message_id_inited) {
    server_unread_count = -1;
  }
  */

  if (d->pts == pts) {
    read_history_inbox(d, max_message_id, server_unread_count, source);
  } else if (d->pts > pts) {
    // outdated update, need to repair server_unread_count from the server
    repair_channel_server_unread_count(d);
  } else {
    // update from the future, keep it until it can be applied
    if (pts >= d->pending_read_channel_inbox_pts) {
      if (d->pending_read_channel_inbox_pts == 0) {
        schedule_get_channel_difference(dialog_id, pts, MessageId(), 0.001, "on_read_channel_inbox");
      }
      d->pending_read_channel_inbox_pts = pts;
      d->pending_read_channel_inbox_max_message_id = max_message_id;
      d->pending_read_channel_inbox_server_unread_count = server_unread_count;
      on_dialog_updated(dialog_id, "on_read_channel_inbox");
    }
  }
}

void MessagesManager::on_read_channel_outbox(ChannelId channel_id, MessageId max_message_id) {
  DialogId dialog_id(channel_id);
  CHECK(!max_message_id.is_scheduled());
  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,
                                                                   const char *source) {
  if (!channel_id.is_valid()) {
    LOG(ERROR) << "Receive max_unavailable_message_id in invalid " << channel_id << " from " << source;
    return;
  }

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

void MessagesManager::on_update_delete_scheduled_messages(DialogId dialog_id,
                                                          vector<ScheduledServerMessageId> &&server_message_ids) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive deleted scheduled messages in invalid " << dialog_id;
    return;
  }

  Dialog *d = get_dialog_force(dialog_id, "on_update_delete_scheduled_messages");
  if (d == nullptr) {
    LOG(INFO) << "Skip updateDeleteScheduledMessages in unknown " << dialog_id;
    return;
  }

  vector<int64> deleted_message_ids;
  for (auto server_message_id : server_message_ids) {
    if (!server_message_id.is_valid()) {
      LOG(ERROR) << "Incoming update tries to delete scheduled message " << server_message_id.get();
      continue;
    }

    auto message = do_delete_scheduled_message(d, MessageId(server_message_id, std::numeric_limits<int32>::max()), true,
                                               "on_update_delete_scheduled_messages");
    if (message != nullptr) {
      deleted_message_ids.push_back(message->message_id.get());
    }
  }

  send_update_delete_messages(dialog_id, std::move(deleted_message_ids), true);

  send_update_chat_has_scheduled_messages(d, true);
}

void MessagesManager::on_update_created_public_broadcasts(vector<ChannelId> channel_ids) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  if (created_public_broadcasts_inited_ && created_public_broadcasts_ == channel_ids) {
    return;
  }

  LOG(INFO) << "Update create public channels to " << channel_ids;
  for (auto channel_id : channel_ids) {
    force_create_dialog(DialogId(channel_id), "on_update_created_public_broadcasts");
  }

  created_public_broadcasts_inited_ = true;
  created_public_broadcasts_ = std::move(channel_ids);
}

void MessagesManager::on_dialog_speaking_action(DialogId dialog_id, DialogId speaking_dialog_id, int32 date) {
  const Dialog *d = get_dialog_force(dialog_id, "on_dialog_speaking_action");
  if (d != nullptr && d->active_group_call_id.is_valid()) {
    auto group_call_id = td_->group_call_manager_->get_group_call_id(d->active_group_call_id, dialog_id);
    td_->group_call_manager_->on_user_speaking_in_group_call(group_call_id, speaking_dialog_id, false, date);
  }
}

void MessagesManager::on_message_animated_emoji_clicked(MessageFullId message_full_id, string &&emoji, string &&data) {
  const auto *m = get_message_force(message_full_id, "on_message_animated_emoji_clicked");
  if (m != nullptr) {
    on_message_content_animated_emoji_clicked(m->content.get(), message_full_id, td_, std::move(emoji),
                                              std::move(data));
  }
}

void MessagesManager::cancel_dialog_action(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  if (td_->auth_manager_->is_bot() || m->forward_info != nullptr || m->had_forward_info ||
      m->via_bot_user_id.is_valid() || m->hide_via_bot || m->is_channel_post || m->message_id.is_scheduled()) {
    return;
  }

  td_->dialog_action_manager_->on_dialog_action(dialog_id, MessageId() /*ignored*/, get_message_sender(m),
                                                DialogAction(), m->date, m->content->get_type());
}

void MessagesManager::add_postponed_channel_update(DialogId dialog_id, tl_object_ptr<telegram_api::Update> &&update,
                                                   int32 new_pts, int32 pts_count, Promise<Unit> &&promise) {
  postponed_channel_updates_[dialog_id].emplace(
      new_pts, PendingPtsUpdate(std::move(update), new_pts, pts_count, std::move(promise)));
}

void MessagesManager::add_pending_channel_update(DialogId dialog_id, tl_object_ptr<telegram_api::Update> &&update,
                                                 int32 new_pts, int32 pts_count, Promise<Unit> &&promise,
                                                 const char *source, bool is_postponed_update) {
  LOG(INFO) << "Receive from " << source << " pending " << to_string(update);
  CHECK(update != nullptr);
  if (dialog_id.get_type() != DialogType::Channel) {
    LOG(ERROR) << "Receive channel update in invalid " << dialog_id << " from " << source << ": "
               << oneline(to_string(update));
    promise.set_value(Unit());
    return;
  }
  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));
    promise.set_value(Unit());
    return;
  }

  auto channel_id = dialog_id.get_channel_id();
  if (!td_->contacts_manager_->have_channel(channel_id) && td_->contacts_manager_->have_min_channel(channel_id)) {
    td_->updates_manager_->schedule_get_difference("add_pending_channel_update 1");
    promise.set_value(Unit());
    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 these queries

  Dialog *d = get_dialog_force(dialog_id, "add_pending_channel_update 2");
  if (d == nullptr) {
    auto pts = load_channel_pts(dialog_id);
    if (pts > 0) {
      if (!td_->contacts_manager_->have_channel(channel_id)) {
        // do not create dialog if there is no info about the channel
        LOG(INFO) << "There is no info about " << channel_id << ", so ignore " << oneline(to_string(update));
        promise.set_value(Unit());
        return;
      }

      d = add_dialog(dialog_id, "add_pending_channel_update 4");
      CHECK(d != nullptr);
      CHECK(d->pts == pts);
      update_dialog_pos(d, "add_pending_channel_update 5");
    }
  }
  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)) {
      add_postponed_channel_update(dialog_id, std::move(update), new_pts, pts_count, std::move(promise));
      return;
    }
  } else {
    int32 old_pts = d->pts;
    if (new_pts <= old_pts) {  // very old or unuseful update
      if (update->get_id() == telegram_api::updateNewChannelMessage::ID) {
        auto update_new_channel_message = static_cast<telegram_api::updateNewChannelMessage *>(update.get());
        auto message_id = MessageId::get_message_id(update_new_channel_message->message_, false);
        MessageFullId message_full_id(dialog_id, message_id);
        if (update_message_ids_.count(message_full_id) > 0) {
          // apply sent channel message
          auto added_message_full_id = on_get_message(std::move(update_new_channel_message->message_), true, true,
                                                      false, "updateNewChannelMessage with an awaited message");
          if (added_message_full_id != message_full_id) {
            LOG(ERROR) << "Failed to add an awaited " << message_full_id << " from " << source;
          }
          promise.set_value(Unit());
          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_, update_sent_message->ttl_period_, FileId(),
                                  "process old updateSentChannelMessage");
          promise.set_value(Unit());
          return;
        }
      }

      LOG_IF(WARNING, new_pts == old_pts && pts_count == 0 && !is_allowed_useless_update(update))
          << "Receive from " << source << " useless channel update " << oneline(to_string(update));
      LOG(INFO) << "Skip already applied channel update";
      promise.set_value(Unit());
      return;
    }

    if (running_get_channel_difference(dialog_id)) {
      LOG(INFO) << "Postpone channel update, because getChannelDifference is run";
      add_postponed_channel_update(dialog_id, std::move(update), new_pts, pts_count, std::move(promise));
      return;
    }

    if (old_pts == 0) {
      old_pts = new_pts - pts_count;
      LOG(INFO) << "Receive first update in " << dialog_id << " with PTS = " << new_pts << " from " << source;
    } else if (old_pts != new_pts - pts_count) {
      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 (d->was_opened || td_->contacts_manager_->get_channel_status(channel_id).is_member() ||
          is_dialog_sponsored(d)) {
        add_postponed_channel_update(dialog_id, std::move(update), new_pts, pts_count, std::move(promise));
        get_channel_difference(dialog_id, old_pts, new_pts, MessageId(), true,
                               "add_pending_channel_update PTS mismatch");
      } else {
        promise.set_value(Unit());
      }
      return;
    }
    CHECK(old_pts + pts_count == new_pts);  // the update can be applied
  }

  if (d == nullptr || pts_count > 0) {
    if (!process_channel_update(std::move(update)) &&
        channel_get_difference_retry_timeout_.has_timeout(dialog_id.get())) {
      promise.set_value(Unit());
      return;
    }
    LOG_CHECK(!running_get_channel_difference(dialog_id)) << '"' << active_get_channel_differences_[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) {
      LOG(INFO) << "Update didn't created " << dialog_id;
      promise.set_value(Unit());
      return;
    }
  }

  CHECK(new_pts > d->pts);
  set_channel_pts(d, new_pts, source);
  promise.set_value(Unit());
}

bool MessagesManager::is_old_channel_update(DialogId dialog_id, int32 new_pts) {
  CHECK(dialog_id.get_type() == DialogType::Channel);

  const Dialog *d = get_dialog_force(dialog_id, "is_old_channel_update");
  return new_pts <= (d == nullptr ? load_channel_pts(dialog_id) : d->pts);
}

void MessagesManager::process_pts_update(tl_object_ptr<telegram_api::Update> &&update_ptr) {
  switch (update_ptr->get_id()) {
    case dummyUpdate::ID:
      LOG(INFO) << "Process dummyUpdate";
      break;
    case telegram_api::updateNewMessage::ID: {
      auto update = move_tl_object_as<telegram_api::updateNewMessage>(update_ptr);
      LOG(INFO) << "Process updateNewMessage";
      on_get_message(std::move(update->message_), true, false, false, "updateNewMessage");
      break;
    }
    case updateSentMessage::ID: {
      auto update = move_tl_object_as<updateSentMessage>(update_ptr);
      LOG(INFO) << "Process updateSentMessage " << update->random_id_;
      on_send_message_success(update->random_id_, update->message_id_, update->date_, update->ttl_period_, FileId(),
                              "process updateSentMessage");
      break;
    }
    case telegram_api::updateReadMessagesContents::ID: {
      auto update = move_tl_object_as<telegram_api::updateReadMessagesContents>(update_ptr);
      LOG(INFO) << "Process updateReadMessageContents";
      for (auto &message_id : update->messages_) {
        read_message_content_from_updates(MessageId(ServerMessageId(message_id)), update->date_);
      }
      break;
    }
    case telegram_api::updateEditMessage::ID: {
      auto update = move_tl_object_as<telegram_api::updateEditMessage>(update_ptr);
      LOG(INFO) << "Process updateEditMessage";
      bool had_message =
          have_message_force(MessageFullId::get_message_full_id(update->message_, false), "updateEditMessage");
      auto message_full_id = on_get_message(std::move(update->message_), false, false, false, "updateEditMessage");
      on_message_edited(message_full_id, update->pts_, had_message);
      break;
    }
    case telegram_api::updateDeleteMessages::ID: {
      auto update = move_tl_object_as<telegram_api::updateDeleteMessages>(update_ptr);
      LOG(INFO) << "Process updateDeleteMessages";
      vector<MessageId> message_ids;
      for (auto message : update->messages_) {
        message_ids.push_back(MessageId(ServerMessageId(message)));
      }
      delete_messages_from_updates(message_ids, true);
      break;
    }
    case telegram_api::updateReadHistoryInbox::ID: {
      auto update = move_tl_object_as<telegram_api::updateReadHistoryInbox>(update_ptr);
      LOG(INFO) << "Process updateReadHistoryInbox";
      DialogId dialog_id(update->peer_);
      on_update_dialog_folder_id(dialog_id, FolderId(update->folder_id_));
      read_history_inbox(dialog_id, MessageId(ServerMessageId(update->max_id_)), -1 /*update->still_unread_count*/,
                         "updateReadHistoryInbox");
      break;
    }
    case telegram_api::updateReadHistoryOutbox::ID: {
      auto update = move_tl_object_as<telegram_api::updateReadHistoryOutbox>(update_ptr);
      LOG(INFO) << "Process updateReadHistoryOutbox";
      read_history_outbox(DialogId(update->peer_), MessageId(ServerMessageId(update->max_id_)));
      break;
    }
    case telegram_api::updatePinnedMessages::ID: {
      auto update = move_tl_object_as<telegram_api::updatePinnedMessages>(update_ptr);
      LOG(INFO) << "Process updatePinnedMessages";
      vector<MessageId> message_ids;
      for (auto message : update->messages_) {
        message_ids.push_back(MessageId(ServerMessageId(message)));
      }
      update_dialog_pinned_messages_from_updates(DialogId(update->peer_), message_ids, update->pinned_);
      break;
    }
    default:
      UNREACHABLE();
  }
  update_ptr = nullptr;
  CHECK(!td_->updates_manager_->running_get_difference());
}

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

      vector<MessageId> message_ids;
      for (auto &message : update->messages_) {
        auto message_id = MessageId(ServerMessageId(message));
        if (message_id.is_valid()) {
          message_ids.push_back(message_id);
        } else {
          LOG(ERROR) << "Receive updateDeleteChannelMessages with message " << message << " in " << channel_id;
        }
      }

      delete_dialog_messages(DialogId(channel_id), message_ids, true, "updateDeleteChannelMessages");
      break;
    }
    case telegram_api::updateEditChannelMessage::ID: {
      auto update = move_tl_object_as<telegram_api::updateEditChannelMessage>(update_ptr);
      LOG(INFO) << "Process updateEditChannelMessage";
      bool had_message =
          have_message_force(MessageFullId::get_message_full_id(update->message_, false), "updateEditChannelMessage");
      auto message_full_id =
          on_get_message(std::move(update->message_), false, true, false, "updateEditChannelMessage");
      if (message_full_id == MessageFullId()) {
        return false;
      }
      on_message_edited(message_full_id, update->pts_, had_message);
      break;
    }
    case telegram_api::updatePinnedChannelMessages::ID: {
      auto update = move_tl_object_as<telegram_api::updatePinnedChannelMessages>(update_ptr);
      LOG(INFO) << "Process updatePinnedChannelMessages";
      ChannelId channel_id(update->channel_id_);
      if (!channel_id.is_valid()) {
        LOG(ERROR) << "Receive invalid " << channel_id;
        break;
      }

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

      update_dialog_pinned_messages_from_updates(DialogId(channel_id), message_ids, update->pinned_);
      break;
    }
    default:
      UNREACHABLE();
  }
  return true;
}

void MessagesManager::on_message_edited(MessageFullId message_full_id, int32 pts, bool had_message) {
  if (message_full_id == MessageFullId()) {
    return;
  }

  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog(dialog_id);
  Message *m = get_message(d, message_full_id.get_message_id());
  CHECK(m != nullptr);
  m->last_edit_pts = pts;
  d->last_edited_message_id = m->message_id;
  if (td_->auth_manager_->is_bot()) {
    send_update_message_edited(dialog_id, m);
  }
  update_used_hashtags(dialog_id, m);

  if (!had_message &&
      ((m->reactions != nullptr && !m->reactions->unread_reactions_.empty()) || d->unread_reaction_count > 0)) {
    // if new message with unread reactions was added or the chat has unread reactions,
    // then number of unread reactions could have been changed, so reload the number of unread reactions
    repair_dialog_unread_reaction_count(d, Promise<Unit>(), "on_message_edited");
  }
}

bool MessagesManager::update_dialog_notification_settings(DialogId dialog_id,
                                                          DialogNotificationSettings *current_settings,
                                                          DialogNotificationSettings &&new_settings) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return false;
  }

  auto need_update = need_update_dialog_notification_settings(current_settings, new_settings);
  if (need_update.are_changed) {
    Dialog *d = get_dialog(dialog_id);
    LOG_CHECK(d != nullptr) << "Wrong " << dialog_id << " in update_dialog_notification_settings";
    bool was_dialog_mentions_disabled = is_dialog_mention_notifications_disabled(d);

    VLOG(notifications) << "Update notification settings in " << dialog_id << " from " << *current_settings << " to "
                        << new_settings;

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

    *current_settings = std::move(new_settings);
    on_dialog_updated(dialog_id, "update_dialog_notification_settings");

    if (is_dialog_muted(d)) {
      // no check for was_muted to clean pending message notifications in chats with unsynchronized settings
      remove_all_dialog_notifications(d, false, "update_dialog_notification_settings 2");
    }
    if (is_dialog_pinned_message_notifications_disabled(d) && d->notification_info != nullptr &&
        d->notification_info->mention_notification_group_.is_valid() &&
        d->notification_info->pinned_message_notification_message_id_.is_valid()) {
      remove_dialog_pinned_message_notification(d, "update_dialog_notification_settings 3");
    }
    if (was_dialog_mentions_disabled != is_dialog_mention_notifications_disabled(d)) {
      if (was_dialog_mentions_disabled) {
        update_dialog_mention_notification_count(d);
      } else {
        remove_dialog_mention_notifications(d);
      }
    }

    if (need_update.need_update_server || need_update.need_update_local) {
      send_closure(G()->td(), &Td::send_update,
                   td_api::make_object<td_api::updateChatNotificationSettings>(
                       get_chat_id_object(dialog_id, "updateChatNotificationSettings"),
                       get_chat_notification_settings_object(current_settings)));
    }
  }
  return need_update.need_update_server;
}

void MessagesManager::schedule_dialog_unmute(DialogId dialog_id, bool use_default, int32 mute_until, int32 unix_time) {
  if (!use_default && mute_until >= unix_time && mute_until < unix_time + 366 * 86400) {
    dialog_unmute_timeout_.set_timeout_in(dialog_id.get(), mute_until - unix_time + 1);
  } else {
    dialog_unmute_timeout_.cancel_timeout(dialog_id.get());
  }
}

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 (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  if (old_use_default == new_use_default && old_mute_until == new_mute_until) {
    return;
  }
  CHECK(d != nullptr);
  CHECK(old_mute_until >= 0);

  schedule_dialog_unmute(d->dialog_id, new_use_default, new_mute_until, G()->unix_time());

  auto scope = td_->dialog_manager_->get_dialog_notification_setting_scope(d->dialog_id);
  auto scope_mute_until = td_->notification_settings_manager_->get_scope_mute_until(scope);
  bool was_muted = (old_use_default ? scope_mute_until : old_mute_until) != 0;
  bool is_muted = (new_use_default ? scope_mute_until : new_mute_until) != 0;
  if (was_muted != is_muted && need_unread_counter(d->order)) {
    auto unread_count = d->server_unread_count + d->local_unread_count;
    if (unread_count != 0 || d->is_marked_as_unread) {
      for (auto &list : get_dialog_lists(d)) {
        if (unread_count != 0 && list.is_message_unread_count_inited_) {
          int32 delta = was_muted ? -unread_count : unread_count;
          list.unread_message_muted_count_ += delta;
          send_update_unread_message_count(list, d->dialog_id, true, "update_dialog_unmute_timeout");
        }
        if (list.is_dialog_unread_count_inited_) {
          int32 delta = was_muted ? -1 : 1;
          list.unread_dialog_muted_count_ += delta;
          if (unread_count == 0 && d->is_marked_as_unread) {
            list.unread_dialog_muted_marked_count_ += delta;
          }
          send_update_unread_chat_count(list, d->dialog_id, true, "update_dialog_unmute_timeout");
        }
      }
    }
  }

  old_use_default = new_use_default;
  old_mute_until = new_mute_until;

  if (was_muted != is_muted && td_->dialog_filter_manager_->have_dialog_filters()) {
    update_dialog_lists(d, get_dialog_positions(d), true, false, "update_dialog_unmute_timeout");
  }
}

void MessagesManager::on_update_notification_scope_is_muted(NotificationSettingsScope scope, bool is_muted) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }
  if (G()->use_message_database()) {
    std::unordered_map<DialogListId, int32, DialogListIdHash> delta;
    std::unordered_map<DialogListId, int32, DialogListIdHash> total_count;
    std::unordered_map<DialogListId, int32, DialogListIdHash> marked_count;
    std::unordered_set<DialogListId, DialogListIdHash> dialog_list_ids;
    dialogs_.foreach([&](const DialogId &dialog_id, unique_ptr<Dialog> &dialog) {
      Dialog *d = dialog.get();
      if (need_unread_counter(d->order) && d->notification_settings.use_default_mute_until &&
          td_->dialog_manager_->get_dialog_notification_setting_scope(d->dialog_id) == scope) {
        int32 unread_count = d->server_unread_count + d->local_unread_count;
        if (unread_count != 0) {
          for (auto dialog_list_id : get_dialog_list_ids(d)) {
            delta[dialog_list_id] += unread_count;
            total_count[dialog_list_id]++;
            dialog_list_ids.insert(dialog_list_id);
          }
        } else if (d->is_marked_as_unread) {
          for (auto dialog_list_id : get_dialog_list_ids(d)) {
            total_count[dialog_list_id]++;
            marked_count[dialog_list_id]++;
            dialog_list_ids.insert(dialog_list_id);
          }
        }
      }
    });
    for (auto dialog_list_id : dialog_list_ids) {
      auto *list = get_dialog_list(dialog_list_id);
      CHECK(list != nullptr);
      if (delta[dialog_list_id] != 0 && list->is_message_unread_count_inited_) {
        if (is_muted) {
          list->unread_message_muted_count_ += delta[dialog_list_id];
        } else {
          list->unread_message_muted_count_ -= delta[dialog_list_id];
        }
        send_update_unread_message_count(*list, DialogId(), true, "on_update_notification_scope_is_muted");
      }
      if (total_count[dialog_list_id] != 0 && list->is_dialog_unread_count_inited_) {
        if (is_muted) {
          list->unread_dialog_muted_count_ += total_count[dialog_list_id];
          list->unread_dialog_muted_marked_count_ += marked_count[dialog_list_id];
        } else {
          list->unread_dialog_muted_count_ -= total_count[dialog_list_id];
          list->unread_dialog_muted_marked_count_ -= marked_count[dialog_list_id];
        }
        send_update_unread_chat_count(*list, DialogId(), true, "on_update_notification_scope_is_muted");
      }
    }
  }

  if (td_->dialog_filter_manager_->have_dialog_filters()) {
    dialogs_.foreach([&](const DialogId &dialog_id, unique_ptr<Dialog> &dialog) {
      Dialog *d = dialog.get();
      if (need_unread_counter(d->order) && d->notification_settings.use_default_mute_until &&
          td_->dialog_manager_->get_dialog_notification_setting_scope(d->dialog_id) == scope) {
        update_dialog_lists(d, get_dialog_positions(d), true, false, "on_update_notification_scope_is_muted");
      }
    });
  }

  if (is_muted) {
    dialogs_.foreach([&](const DialogId &dialog_id, unique_ptr<Dialog> &dialog) {
      Dialog *d = dialog.get();
      if (need_unread_counter(d->order) && d->notification_settings.use_default_mute_until &&
          td_->dialog_manager_->get_dialog_notification_setting_scope(d->dialog_id) == scope) {
        remove_all_dialog_notifications(d, false, "on_update_notification_scope_is_muted");
      }
    });
  }
}

void MessagesManager::on_dialog_unmute(DialogId dialog_id) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  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 unix_time = G()->unix_time();
  if (d->notification_settings.mute_until > unix_time) {
    LOG(INFO) << "Failed to unmute " << dialog_id << " in " << unix_time << ", will be unmuted in "
              << d->notification_settings.mute_until;
    schedule_dialog_unmute(dialog_id, false, d->notification_settings.mute_until, unix_time);
    return;
  }

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

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

  VLOG(notifications) << "Receive notification settings for " << dialog_id << " from " << source << ": "
                      << to_string(peer_notify_settings);

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

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

  update_dialog_notification_settings(dialog_id, current_settings, std::move(notification_settings));
}

void MessagesManager::on_update_dialog_available_reactions(
    DialogId dialog_id, telegram_api::object_ptr<telegram_api::ChatReactions> &&available_reactions) {
  Dialog *d = get_dialog_force(dialog_id, "on_update_dialog_available_reactions");
  if (d == nullptr) {
    return;
  }

  set_dialog_available_reactions(d, ChatReactions(std::move(available_reactions)));
}

void MessagesManager::set_dialog_available_reactions(Dialog *d, ChatReactions &&available_reactions) {
  CHECK(d != nullptr);
  switch (d->dialog_id.get_type()) {
    case DialogType::Chat:
    case DialogType::Channel:
      // ok
      break;
    case DialogType::User:
    case DialogType::SecretChat:
    default:
      UNREACHABLE();
      break;
  }
  if (d->available_reactions == available_reactions) {
    if (!d->is_available_reactions_inited) {
      d->is_available_reactions_inited = true;
      on_dialog_updated(d->dialog_id, "set_dialog_available_reactions");
    }
    return;
  }

  LOG(INFO) << "Update available reactions in " << d->dialog_id << " to " << available_reactions;

  auto old_active_reactions = get_active_reactions(d->available_reactions);
  auto new_active_reactions = get_active_reactions(available_reactions);
  bool need_update = old_active_reactions != new_active_reactions;
  bool need_update_message_reactions_visibility =
      old_active_reactions.empty() != new_active_reactions.empty() && !td_->auth_manager_->is_bot();

  d->available_reactions = std::move(available_reactions);
  d->is_available_reactions_inited = true;
  if (need_update_message_reactions_visibility) {
    if (!old_active_reactions.empty()) {
      hide_dialog_message_reactions(d);
    }

    set_dialog_next_available_reactions_generation(d, d->available_reactions_generation);
  }
  on_dialog_updated(d->dialog_id, "set_dialog_available_reactions");

  if (need_update) {
    send_update_chat_available_reactions(d);
  }
}

void MessagesManager::set_dialog_next_available_reactions_generation(Dialog *d, uint32 generation) {
  CHECK(d != nullptr);
  switch (d->dialog_id.get_type()) {
    case DialogType::Chat:
    case DialogType::Channel:
      // ok
      break;
    case DialogType::User:
    case DialogType::SecretChat:
    default:
      UNREACHABLE();
      break;
  }
  if (get_active_reactions(d->available_reactions).empty()) {
    // 0 -> 1
    // 1 -> 3
    generation = generation + (generation & 1) + 1;
  } else {
    // 0 -> 2
    // 1 -> 2
    generation = generation - (generation & 1) + 2;
  }
  LOG(INFO) << "Change available reactions generation from " << d->available_reactions_generation << " to "
            << generation << " in " << d->dialog_id;
  d->available_reactions_generation = generation;
}

void MessagesManager::hide_dialog_message_reactions(Dialog *d) {
  CHECK(!td_->auth_manager_->is_bot());
  auto dialog_type = d->dialog_id.get_type();
  CHECK(dialog_type == DialogType::Chat || dialog_type == DialogType::Channel);
  auto message_ids = find_dialog_messages(
      d, [](const Message *m) { return m->reactions != nullptr && !m->reactions->reactions_.empty(); });
  for (auto message_id : message_ids) {
    Message *m = get_message(d, message_id);
    CHECK(m != nullptr);
    CHECK(m->reactions != nullptr);
    bool need_update_unread_reactions = !m->reactions->unread_reactions_.empty();
    m->reactions = nullptr;
    // don't resave all messages to the database
    if (need_update_unread_reactions) {
      send_update_message_unread_reactions(d->dialog_id, m, d->unread_reaction_count);
    }
    send_update_message_interaction_info(d->dialog_id, m);
  }
  if (d->unread_reaction_count != 0) {
    set_dialog_unread_reaction_count(d, 0);
  }
}

void MessagesManager::set_active_reactions(vector<ReactionType> active_reaction_types) {
  if (active_reaction_types == active_reaction_types_) {
    return;
  }

  LOG(INFO) << "Set active reactions to " << active_reaction_types;
  bool is_changed = active_reaction_types != active_reaction_types_;
  active_reaction_types_ = std::move(active_reaction_types);

  auto old_active_reaction_pos_ = std::move(active_reaction_pos_);
  active_reaction_pos_.clear();
  for (size_t i = 0; i < active_reaction_types_.size(); i++) {
    CHECK(!active_reaction_types_[i].is_empty());
    active_reaction_pos_[active_reaction_types_[i]] = i;
  }

  if (td_->auth_manager_->is_bot()) {
    return;
  }

  dialogs_.foreach([&](const DialogId &dialog_id, unique_ptr<Dialog> &dialog) {
    Dialog *d = dialog.get();
    switch (dialog_id.get_type()) {
      case DialogType::User:
        if (is_changed) {
          send_update_chat_available_reactions(d);
        }
        break;
      case DialogType::Chat:
      case DialogType::Channel: {
        auto old_reactions = d->available_reactions.get_active_reactions(old_active_reaction_pos_);
        auto new_reactions = d->available_reactions.get_active_reactions(active_reaction_pos_);
        if (old_reactions != new_reactions) {
          if (old_reactions.empty() != new_reactions.empty()) {
            if (!old_reactions.empty()) {
              hide_dialog_message_reactions(d);
            }
            set_dialog_next_available_reactions_generation(d, d->available_reactions_generation);
            on_dialog_updated(d->dialog_id, "set_active_reactions");
          }
          send_update_chat_available_reactions(d);
        }
        break;
      }
      case DialogType::SecretChat:
        break;
      default:
        UNREACHABLE();
        break;
    }
  });
}

ChatReactions MessagesManager::get_active_reactions(const ChatReactions &available_reactions) const {
  if (td_->auth_manager_->is_bot()) {
    return available_reactions;
  }
  return available_reactions.get_active_reactions(active_reaction_pos_);
}

ChatReactions MessagesManager::get_dialog_active_reactions(const Dialog *d) const {
  CHECK(d != nullptr);
  switch (d->dialog_id.get_type()) {
    case DialogType::User:
      return ChatReactions(true, true);
    case DialogType::Chat:
    case DialogType::Channel:
      return get_active_reactions(d->available_reactions);
    case DialogType::SecretChat:
      return {};
    default:
      UNREACHABLE();
      return {};
  }
}

// affects all users
ChatReactions MessagesManager::get_message_active_reactions(const Dialog *d, const Message *m) const {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  if (is_service_message_content(m->content->get_type()) || !m->ttl.is_empty() || !m->message_id.is_valid() ||
      !m->message_id.is_server()) {
    return ChatReactions();
  }
  auto dialog_id = d->dialog_id;
  if (is_discussion_message(dialog_id, m)) {
    auto linked_dialog_id = m->forward_info->get_last_dialog_id();
    d = get_dialog(linked_dialog_id);
    if (d == nullptr) {
      LOG(ERROR) << "Failed to find linked " << linked_dialog_id << " to determine correct active reactions";
      return ChatReactions();
    }
  }
  return get_dialog_active_reactions(d);
}

bool MessagesManager::need_poll_dialog_message_reactions(const Dialog *d) {
  CHECK(d != nullptr);
  switch (d->dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::SecretChat:
      return false;
    case DialogType::Chat:
    case DialogType::Channel:
      return (d->available_reactions_generation & 1) == 0;
    default:
      UNREACHABLE();
      return {};
  }
}

bool MessagesManager::need_poll_message_reactions(const Dialog *d, const Message *m) {
  CHECK(m != nullptr);
  if (!m->message_id.is_valid() || !m->message_id.is_server()) {
    return false;
  }
  if (!need_poll_dialog_message_reactions(d)) {
    return false;
  }
  if (m->reactions != nullptr) {
    return true;
  }
  if (m->available_reactions_generation == d->available_reactions_generation) {
    return false;
  }
  if (is_service_message_content(m->content->get_type())) {
    return false;
  }
  return true;
}

void MessagesManager::queue_message_reactions_reload(MessageFullId message_full_id) {
  auto dialog_id = message_full_id.get_dialog_id();
  CHECK(dialog_id.is_valid());
  auto message_id = message_full_id.get_message_id();
  CHECK(message_id.is_valid());
  being_reloaded_reactions_[dialog_id].message_ids.insert(message_id);
  try_reload_message_reactions(dialog_id, false);
}

void MessagesManager::queue_message_reactions_reload(DialogId dialog_id, const vector<MessageId> &message_ids) {
  LOG(INFO) << "Queue reload of reactions in " << message_ids << " in " << dialog_id;
  auto &message_ids_to_reload = being_reloaded_reactions_[dialog_id].message_ids;
  for (auto &message_id : message_ids) {
    CHECK(message_id.is_valid());
    message_ids_to_reload.insert(message_id);
  }
  try_reload_message_reactions(dialog_id, false);
}

void MessagesManager::try_reload_message_reactions(DialogId dialog_id, bool is_finished) {
  if (G()->close_flag()) {
    return;
  }

  auto it = being_reloaded_reactions_.find(dialog_id);
  if (it == being_reloaded_reactions_.end()) {
    return;
  }
  if (is_finished) {
    CHECK(it->second.is_request_sent);
    it->second.is_request_sent = false;

    if (it->second.message_ids.empty()) {
      being_reloaded_reactions_.erase(it);
      return;
    }
  } else if (it->second.is_request_sent) {
    return;
  }

  CHECK(!it->second.message_ids.empty());
  CHECK(!it->second.is_request_sent);

  it->second.is_request_sent = true;

  static constexpr size_t MAX_MESSAGE_IDS = 100;  // server-side limit
  vector<MessageId> message_ids;
  for (auto message_id_it = it->second.message_ids.begin();
       message_id_it != it->second.message_ids.end() && message_ids.size() < MAX_MESSAGE_IDS; ++message_id_it) {
    auto message_id = *message_id_it;
    if (pending_read_reactions_.count({dialog_id, message_id}) == 0) {
      message_ids.push_back(message_id);
    }
  }
  for (auto message_id : message_ids) {
    it->second.message_ids.erase(message_id);
  }
  reload_message_reactions(td_, dialog_id, std::move(message_ids));
}

bool MessagesManager::update_dialog_silent_send_message(Dialog *d, bool silent_send_message) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return false;
  }

  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,
               td_api::make_object<td_api::updateChatDefaultDisableNotification>(
                   get_chat_id_object(d->dialog_id, "updateChatDefaultDisableNotification"), silent_send_message));
  return true;
}

void MessagesManager::reget_dialog_action_bar(DialogId dialog_id, const char *source, bool is_repair) {
  if (G()->close_flag() || !dialog_id.is_valid() || td_->auth_manager_->is_bot()) {
    return;
  }

  Dialog *d = get_dialog_force(dialog_id, source);
  if (d == nullptr) {
    return;
  }

  if (is_repair && !d->need_repair_action_bar) {
    d->need_repair_action_bar = true;
    on_dialog_updated(dialog_id, source);
  }

  LOG(INFO) << "Reget action bar in " << dialog_id << " from " << source;
  switch (dialog_id.get_type()) {
    case DialogType::User:
      td_->contacts_manager_->reload_user_full(dialog_id.get_user_id(), Auto(), source);
      return;
    case DialogType::Chat:
    case DialogType::Channel:
      if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
        return;
      }

      return td_->create_handler<GetPeerSettingsQuery>()->send(dialog_id);
    case DialogType::SecretChat:
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

void MessagesManager::repair_dialog_action_bar(Dialog *d, const char *source) {
  CHECK(d != nullptr);
  auto dialog_id = d->dialog_id;
  d->need_repair_action_bar = true;
  if (td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    create_actor<SleepActor>(
        "RepairChatActionBarActor", 1.0,
        PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, source](Result<Unit> result) {
          send_closure(actor_id, &MessagesManager::reget_dialog_action_bar, dialog_id, source, true);
        }))
        .release();
  }
  // there is no need to change action bar
  on_dialog_updated(dialog_id, source);
}

void MessagesManager::hide_dialog_action_bar(DialogId dialog_id) {
  Dialog *d = get_dialog_force(dialog_id, "hide_dialog_action_bar");
  if (d == nullptr) {
    return;
  }
  hide_dialog_action_bar(d);
}

void MessagesManager::hide_dialog_action_bar(Dialog *d) {
  CHECK(d->dialog_id.get_type() != DialogType::SecretChat);
  if (!d->know_action_bar) {
    return;
  }
  if (d->need_repair_action_bar) {
    d->need_repair_action_bar = false;
    on_dialog_updated(d->dialog_id, "hide_dialog_action_bar");
  }
  if (d->action_bar == nullptr) {
    return;
  }

  d->action_bar = nullptr;
  send_update_chat_action_bar(d);
}

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

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

  if (dialog_id.get_type() == DialogType::SecretChat) {
    dialog_id = DialogId(td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id()));
    d = get_dialog_force(dialog_id, "remove_dialog_action_bar 2");
    if (d == nullptr) {
      return promise.set_error(Status::Error(400, "Chat with the user not found"));
    }
    if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
      return promise.set_error(Status::Error(400, "Can't access the chat"));
    }
  }

  if (!d->know_action_bar) {
    return promise.set_error(Status::Error(400, "Can't update chat action bar"));
  }
  if (d->need_repair_action_bar) {
    d->need_repair_action_bar = false;
    on_dialog_updated(dialog_id, "remove_dialog_action_bar");
  }
  if (d->action_bar == nullptr) {
    return promise.set_value(Unit());
  }

  d->action_bar = nullptr;
  send_update_chat_action_bar(d);

  toggle_dialog_report_spam_state_on_server(dialog_id, false, 0, std::move(promise));
}

void MessagesManager::repair_dialog_active_group_call_id(DialogId dialog_id) {
  if (td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    LOG(INFO) << "Repair active voice chat ID in " << dialog_id;
    create_actor<SleepActor>("RepairChatActiveVoiceChatId", 1.0,
                             PromiseCreator::lambda([actor_id = actor_id(this), dialog_id](Result<Unit> result) {
                               send_closure(actor_id, &MessagesManager::do_repair_dialog_active_group_call_id,
                                            dialog_id);
                             }))
        .release();
  }
}

void MessagesManager::do_repair_dialog_active_group_call_id(DialogId dialog_id) {
  if (G()->close_flag()) {
    return;
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  bool need_repair_active_group_call_id = d->has_active_group_call && !d->active_group_call_id.is_valid();
  bool need_repair_expected_group_call_id =
      d->has_expected_active_group_call_id && d->active_group_call_id != d->expected_active_group_call_id;
  d->has_expected_active_group_call_id = false;
  if (!need_repair_active_group_call_id && !need_repair_expected_group_call_id) {
    return;
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return;
  }

  td_->dialog_manager_->reload_dialog_info_full(dialog_id, "do_repair_dialog_active_group_call_id");
}

class MessagesManager::ToggleDialogReportSpamStateOnServerLogEvent {
 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_toggle_dialog_report_spam_state_on_server_log_event(DialogId dialog_id,
                                                                                 bool is_spam_dialog) {
  ToggleDialogReportSpamStateOnServerLogEvent log_event{dialog_id, is_spam_dialog};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ToggleDialogReportSpamStateOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::toggle_dialog_report_spam_state_on_server(DialogId dialog_id, bool is_spam_dialog,
                                                                uint64 log_event_id, Promise<Unit> &&promise) {
  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_toggle_dialog_report_spam_state_on_server_log_event(dialog_id, is_spam_dialog);
  }

  auto new_promise = get_erase_log_event_promise(log_event_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 {
        auto user_id = td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id());
        if (!user_id.is_valid()) {
          return promise.set_error(Status::Error(400, "Peer user not found"));
        }
        return td_->create_handler<UpdatePeerSettingsQuery>(std::move(promise))->send(DialogId(user_id), false);
      }
    case DialogType::None:
    default:
      UNREACHABLE();
      return;
  }
}

void MessagesManager::on_get_peer_settings(DialogId dialog_id,
                                           tl_object_ptr<telegram_api::peerSettings> &&peer_settings,
                                           bool ignore_privacy_exception) {
  CHECK(peer_settings != nullptr);
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  if (dialog_id.get_type() == DialogType::User && !ignore_privacy_exception) {
    td_->contacts_manager_->on_update_user_need_phone_number_privacy_exception(dialog_id.get_user_id(),
                                                                               peer_settings->need_contacts_exception_);
  }

  Dialog *d = get_dialog_force(dialog_id, "on_get_peer_settings");
  if (d == nullptr) {
    return;
  }

  auto distance =
      (peer_settings->flags_ & telegram_api::peerSettings::GEO_DISTANCE_MASK) != 0 ? peer_settings->geo_distance_ : -1;
  if (distance < -1 || d->has_outgoing_messages) {
    distance = -1;
  }
  auto action_bar =
      DialogActionBar::create(peer_settings->report_spam_, peer_settings->add_contact_, peer_settings->block_contact_,
                              peer_settings->share_contact_, peer_settings->report_geo_, peer_settings->autoarchived_,
                              distance, peer_settings->invite_members_, peer_settings->request_chat_title_,
                              peer_settings->request_chat_broadcast_, peer_settings->request_chat_date_);

  fix_dialog_action_bar(d, action_bar.get());

  if (d->action_bar == action_bar) {
    if (!d->know_action_bar || d->need_repair_action_bar) {
      d->know_action_bar = true;
      d->need_repair_action_bar = false;
      on_dialog_updated(d->dialog_id, "on_get_peer_settings");
    }
    return;
  }

  d->know_action_bar = true;
  d->need_repair_action_bar = false;
  d->action_bar = std::move(action_bar);

  send_update_chat_action_bar(d);
}

void MessagesManager::fix_dialog_action_bar(const Dialog *d, DialogActionBar *action_bar) {
  if (action_bar == nullptr) {
    return;
  }

  CHECK(d != nullptr);
  action_bar->fix(td_, d->dialog_id, d->is_blocked, d->folder_id);
}

Result<string> MessagesManager::get_login_button_url(MessageFullId message_full_id, int64 button_id) {
  Dialog *d = get_dialog_force(message_full_id.get_dialog_id(), "get_login_button_url");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }
  if (!td_->dialog_manager_->have_input_peer(d->dialog_id, AccessRights::Read)) {
    return Status::Error(400, "Can't access the chat");
  }

  auto m = get_message_force(d, message_full_id.get_message_id(), "get_login_button_url");
  if (m == nullptr) {
    return Status::Error(400, "Message not found");
  }
  if (m->reply_markup == nullptr || m->reply_markup->type != ReplyMarkup::Type::InlineKeyboard) {
    return Status::Error(400, "Message has no inline keyboard");
  }
  if (m->message_id.is_scheduled()) {
    return Status::Error(400, "Can't use login buttons from scheduled messages");
  }
  if (!m->message_id.is_server()) {
    // it shouldn't have UrlAuth buttons anyway
    return Status::Error(400, "Message is not server");
  }
  if (d->dialog_id.get_type() == DialogType::SecretChat) {
    // secret chat messages can't have reply markup, so this shouldn't happen now
    return Status::Error(400, "Message is in a secret chat");
  }
  if (button_id < std::numeric_limits<int32>::min() || button_id > std::numeric_limits<int32>::max()) {
    return Status::Error(400, "Invalid button identifier specified");
  }

  for (auto &row : m->reply_markup->inline_keyboard) {
    for (auto &button : row) {
      if (button.type == InlineKeyboardButton::Type::UrlAuth && button.id == button_id) {
        return button.data;
      }
    }
  }

  return Status::Error(400, "Button not found");
}

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

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

   private:
    Promise<Unit> download_promise_;
  };

  auto thumbnail_promise = PromiseCreator::lambda([actor_id = actor_id(this),
                                                   thumbnail_file_id](Result<BufferSlice> r_thumbnail) {
    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, -1,
               Promise<td_api::object_ptr<td_api::file>>());
}

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 canceled
    return;
  }

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

  being_uploaded_files_.erase(it);

  Message *m = get_message(message_full_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 canceled 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_any_server();
  auto dialog_id = message_full_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 their privileges
    LOG(INFO) << "Can't send a message to " << dialog_id << ": " << can_send_status;

    fail_send_message(message_full_id, std::move(can_send_status));
    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)
        LOG(INFO) << "Ask to upload thumbnail " << thumbnail_file_id;
        bool is_inserted =
            being_uploaded_thumbnails_
                .emplace(thumbnail_file_id, UploadedThumbnailInfo{message_full_id, file_id, std::move(input_file)})
                .second;
        CHECK(is_inserted);
        td_->file_manager_->upload(thumbnail_file_id, upload_thumbnail_callback_, 32, 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()) {
        LOG(INFO) << "Ask to load thumbnail " << thumbnail_file_id;
        bool is_inserted = being_loaded_secret_thumbnails_
                               .emplace(thumbnail_file_id, UploadedSecretThumbnailInfo{message_full_id, file_id,
                                                                                       std::move(input_encrypted_file)})
                               .second;
        CHECK(is_inserted);

        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) {
  CHECK(m != nullptr);

  bool have_input_file = input_file != nullptr;
  bool have_input_thumbnail = input_thumbnail != nullptr;
  LOG(INFO) << "Do send media file " << file_id << " with thumbnail " << thumbnail_file_id
            << ", have_input_file = " << have_input_file << ", have_input_thumbnail = " << have_input_thumbnail
            << ", self-destruct time = " << m->ttl;

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

  auto input_media = get_input_media(content, td_, std::move(input_file), std::move(input_thumbnail), file_id,
                                     thumbnail_file_id, m->ttl, m->send_emoji, true);
  LOG_CHECK(input_media != nullptr) << to_string(get_message_object(dialog_id, m, "do_send_media")) << ' '
                                    << have_input_file << ' ' << have_input_thumbnail << ' ' << file_id << ' '
                                    << thumbnail_file_id << ' ' << m->ttl;

  on_message_media_uploaded(dialog_id, m, std::move(input_media), 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) {
  CHECK(dialog_id.get_type() == DialogType::SecretChat);
  CHECK(m != nullptr);
  CHECK(m->message_id.is_valid());
  CHECK(m->message_id.is_yet_unsent());

  bool have_input_file = input_encrypted_file != nullptr;
  LOG(INFO) << "Do send secret media file " << file_id << " with thumbnail " << thumbnail_file_id
            << ", have_input_file = " << have_input_file;

  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 canceled
    return;
  }

  auto message_full_id = it->second.first;

  being_uploaded_files_.erase(it);

  bool is_edit = message_full_id.get_message_id().is_any_server();
  if (is_edit) {
    fail_edit_message_media(message_full_id, std::move(status));
  } else {
    fail_send_message(message_full_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 message_full_id = it->second.message_full_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(message_full_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
    LOG(INFO) << "Message with a media has already been deleted";
    cancel_upload_file(file_id, "on_load_secret_thumbnail");
    return;
  }
  CHECK(m->message_id.is_yet_unsent());

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

  auto dialog_id = message_full_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;

    fail_send_message(message_full_id, std::move(can_send_status));
    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 canceled
    return;
  }

  auto message_full_id = it->second.message_full_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(message_full_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 canceled in cancel_send_message_query
    LOG(ERROR) << "Message with a media has already been deleted";
    return;
  }

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

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

  auto dialog_id = message_full_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 their privileges
    LOG(INFO) << "Can't send a message to " << dialog_id << ": " << can_send_status;

    fail_send_message(message_full_id, std::move(can_send_status));
    return;
  }

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

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

  // scheduled messages are not returned in getDifference, so we must always reget them after it
  scheduled_messages_sync_generation_++;
}

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

  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(res.folder_id, 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");
    }
  }
  while (!postponed_unread_message_count_updates_.empty()) {
    auto *list = get_dialog_list(*postponed_unread_message_count_updates_.begin());
    CHECK(list != nullptr);
    send_update_unread_message_count(*list, DialogId(), true, "after_get_difference");
  }
  while (!postponed_unread_chat_count_updates_.empty()) {
    auto *list = get_dialog_list(*postponed_unread_chat_count_updates_.begin());
    CHECK(list != nullptr);
    send_update_unread_chat_count(*list, DialogId(), true, "after_get_difference");
  }

  vector<MessageFullId> update_message_ids_to_delete;
  for (auto &it : update_message_ids_) {
    // there can be unhandled updateMessageId updates after getDifference even for ordinary chats,
    // because despite updates coming during getDifference have already been applied,
    // some of them could be postponed because of PTS gap
    auto message_full_id = it.first;
    auto dialog_id = message_full_id.get_dialog_id();
    auto message_id = message_full_id.get_message_id();
    auto old_message_id = it.second;
    CHECK(message_id.is_valid());
    CHECK(message_id.is_server());
    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: {
        if (!have_message_force({dialog_id, old_message_id}, "after get difference")) {
          // The sent message has already been deleted by the user or sent to inaccessible channel.
          // The sent message may never be received, but we will need updateMessageId in case the message is received
          // to delete it from the server and not add to the chat.
          // But if the chat is inaccessible or the message is in an inaccessible chat part, then we will not be able to
          // add the message or delete it from the server. In this case we forget updateMessageId for such messages in
          // order to not check them over and over.
          const Dialog *d = get_dialog(dialog_id);
          if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read) ||
              (d != nullptr &&
               message_id <= td::max(d->last_clear_history_message_id, d->max_unavailable_message_id))) {
            update_message_ids_to_delete.push_back(message_full_id);
          }
          break;
        }

        const Dialog *d = get_dialog(dialog_id);
        CHECK(d != nullptr);
        if (dialog_id.get_type() == DialogType::Channel || message_id <= d->last_new_message_id) {
          LOG(ERROR) << "Receive updateMessageId from " << old_message_id << " to " << message_full_id
                     << " but not receive corresponding message, last_new_message_id = " << d->last_new_message_id;
        }
        if (message_id <= d->last_new_message_id) {
          get_message_from_server(
              message_full_id,
              PromiseCreator::lambda([actor_id = actor_id(this), message_full_id, old_message_id](Result<Unit> result) {
                send_closure(actor_id, &MessagesManager::on_restore_missing_message_after_get_difference,
                             message_full_id, old_message_id, std::move(result));
              }),
              "get missing");
        } else if (dialog_id.get_type() == DialogType::Channel) {
          schedule_get_channel_difference(dialog_id, 0, message_id, 0.001, "after_get_difference");
        }
        break;
      }
      case DialogType::SecretChat:
        break;
      case DialogType::None:
      default:
        UNREACHABLE();
        break;
    }
  }
  for (const auto &message_full_id : update_message_ids_to_delete) {
    update_message_ids_.erase(message_full_id);
  }

  if (!td_->auth_manager_->is_bot()) {
    if (!G()->td_db()->get_binlog_pmc()->isset("fetched_marks_as_unread")) {
      td_->create_handler<GetDialogUnreadMarksQuery>()->send();
    }

    auto dialog_list_id = DialogListId(FolderId::archive());
    auto *list = get_dialog_list(dialog_list_id);
    CHECK(list != nullptr);
    if (!list->is_dialog_unread_count_inited_) {
      int32 limit = list->are_pinned_dialogs_inited_ ? static_cast<int32>(list->pinned_dialogs_.size())
                                                     : get_pinned_dialogs_limit(dialog_list_id);
      LOG(INFO) << "Loading chat list in " << dialog_list_id << " to init total unread count";
      get_dialogs_from_list(dialog_list_id, limit + 2, Auto());
    }
  }
}

void MessagesManager::on_restore_missing_message_after_get_difference(MessageFullId message_full_id,
                                                                      MessageId old_message_id, Result<Unit> result) {
  if (result.is_error()) {
    LOG(WARNING) << "Failed to get missing " << message_full_id << " for " << old_message_id << ": " << result.error();
  } else {
    LOG(WARNING) << "Successfully get missing " << message_full_id << " for " << old_message_id;

    bool have_message = have_message_force(message_full_id, "on_restore_missing_message_after_get_difference");
    if (!have_message && update_message_ids_.count(message_full_id)) {
      LOG(ERROR) << "Receive messageEmpty instead of missing " << message_full_id << " for " << old_message_id;

      delete_dialog_messages(message_full_id.get_dialog_id(), {old_message_id}, false,
                             "on_restore_missing_message_after_get_difference");

      update_message_ids_.erase(message_full_id);
    }
  }
}

void MessagesManager::on_get_empty_messages(DialogId dialog_id, const vector<MessageId> &empty_message_ids) {
  if (!empty_message_ids.empty()) {
    delete_dialog_messages(dialog_id, empty_message_ids, false, "on_get_empty_messages");
  }
}

void MessagesManager::get_channel_difference_if_needed(DialogId dialog_id, MessagesInfo &&messages_info,
                                                       Promise<MessagesInfo> &&promise, const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return promise.set_value(std::move(messages_info));
  }
  if (!dialog_id.is_valid()) {
    return get_channel_differences_if_needed(std::move(messages_info), std::move(promise), source);
  }
  for (auto &message : messages_info.messages) {
    if (need_channel_difference_to_add_message(dialog_id, message)) {
      auto max_message_id = MessageId::get_max_message_id(messages_info.messages);
      return run_after_channel_difference(
          dialog_id, max_message_id,
          PromiseCreator::lambda([messages_info = std::move(messages_info), promise = std::move(promise)](
                                     Unit ignored) mutable { promise.set_value(std::move(messages_info)); }),
          source);
    }
  }
  promise.set_value(std::move(messages_info));
}

void MessagesManager::get_channel_differences_if_needed(MessagesInfo &&messages_info, Promise<MessagesInfo> &&promise,
                                                        const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return promise.set_value(std::move(messages_info));
  }
  MultiPromiseActorSafe mpas{"GetChannelDifferencesIfNeededMultiPromiseActor"};
  mpas.add_promise(Promise<Unit>());
  mpas.set_ignore_errors(true);
  auto lock = mpas.get_promise();
  for (auto &message : messages_info.messages) {
    if (message == nullptr) {
      continue;
    }

    auto dialog_id = DialogId::get_message_dialog_id(message);
    if (need_channel_difference_to_add_message(dialog_id, message)) {
      run_after_channel_difference(dialog_id, MessageId::get_message_id(message, false), mpas.get_promise(), source);
    }
  }
  // must be added after messages_info is checked
  mpas.add_promise(PromiseCreator::lambda([messages_info = std::move(messages_info), promise = std::move(promise)](
                                              Unit ignored) mutable { promise.set_value(std::move(messages_info)); }));
  lock.set_value(Unit());
}

void MessagesManager::get_channel_differences_if_needed(
    const vector<const telegram_api::object_ptr<telegram_api::Message> *> &messages, Promise<Unit> &&promise,
    const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return promise.set_value(Unit());
  }
  MultiPromiseActorSafe mpas{"GetChannelDifferencesIfNeededGenericMultiPromiseActor"};
  mpas.add_promise(std::move(promise));
  mpas.set_ignore_errors(true);
  auto lock = mpas.get_promise();
  for (const auto &message : messages) {
    if (message == nullptr) {
      continue;
    }
    auto dialog_id = DialogId::get_message_dialog_id(*message);
    if (need_channel_difference_to_add_message(dialog_id, *message)) {
      run_after_channel_difference(dialog_id, MessageId::get_message_id(*message, false), mpas.get_promise(), source);
    }
  }
  lock.set_value(Unit());
}

void MessagesManager::on_get_messages(vector<tl_object_ptr<telegram_api::Message>> &&messages, bool is_channel_message,
                                      bool is_scheduled, Promise<Unit> &&promise, const char *source) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  for (auto &message : messages) {
    LOG(INFO) << "Receive " << to_string(message);
    on_get_message(std::move(message), false, is_channel_message, is_scheduled, source);
  }
  promise.set_value(Unit());
}

bool MessagesManager::delete_newer_server_messages_at_the_end(Dialog *d, MessageId max_message_id) {
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(!max_message_id.is_scheduled());
  auto message_ids = d->ordered_messages.find_newer_messages(max_message_id);
  if (message_ids.empty()) {
    return false;
  }

  vector<MessageId> server_message_ids;
  vector<MessageId> kept_message_ids;
  for (auto message_id : message_ids) {
    CHECK(message_id > max_message_id);
    if (message_id.is_server()) {
      server_message_ids.push_back(message_id);
    } else {
      kept_message_ids.push_back(message_id);
    }
  }

  delete_dialog_messages(d, server_message_ids, false, "delete_newer_server_messages_at_the_end");

  // connect all messages with ID > max_message_id
  for (size_t i = 0; i + 1 < kept_message_ids.size(); i++) {
    d->ordered_messages.attach_message_to_next(kept_message_ids[i], "delete_newer_server_messages_at_the_end");
  }

  return !kept_message_ids.empty();
}

void MessagesManager::on_get_history(DialogId dialog_id, MessageId from_message_id, MessageId old_last_new_message_id,
                                     int32 offset, int32 limit, bool from_the_end,
                                     vector<tl_object_ptr<telegram_api::Message>> &&messages, Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  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);
  if (from_the_end) {
    CHECK(from_message_id == MessageId());
  } else {
    CHECK(!from_message_id.is_scheduled());
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  MessageId last_received_message_id = messages.empty() ? MessageId() : MessageId::get_message_id(messages[0], false);
  if (old_last_new_message_id < d->last_new_message_id && (from_the_end || old_last_new_message_id < from_message_id) &&
      last_received_message_id < d->last_new_message_id) {
    // new server messages were added to the dialog since the request was sent, but weren't received
    // they should have been received, so we must repeat the request to get them
    get_history_impl(d, from_message_id, offset, limit, false, false, std::move(promise), "on_get_history");
    return;
  }

  // the server can return less messages than requested if some of messages are deleted during request
  // but if it happens, it is likely that there are no more messages on the server
  bool have_full_history = from_the_end && narrow_cast<int32>(messages.size()) < limit && messages.size() <= 1;

  if (messages.empty()) {
    if (have_full_history) {
      d->have_full_history = true;
      d->have_full_history_source = 1;
      on_dialog_updated(dialog_id, "set have_full_history");
    }

    if (from_the_end && d->have_full_history && d->ordered_messages.empty()) {
      if (!d->last_database_message_id.is_valid()) {
        set_dialog_is_empty(d, "on_get_history empty");
      } else {
        LOG(INFO) << "Skip marking " << dialog_id << " as empty, because it probably has messages from "
                  << d->first_database_message_id << " to " << d->last_database_message_id << " in the database";
      }
    }

    // be aware that in some cases an empty answer may be returned, because of the race of getHistory and deleteMessages
    // and not because there are no more messages
    promise.set_value(Unit());
    return;
  }

  if (messages.size() > 1) {
    // check that messages are received in decreasing message_id order
    MessageId cur_message_id = MessageId::max();
    for (const auto &message : messages) {
      MessageId message_id = MessageId::get_message_id(message, false);
      if (message_id >= cur_message_id) {
        string error = PSTRING() << "Receive messages in the wrong order in history of " << dialog_id << " from "
                                 << from_message_id << " with offset " << offset << ", limit " << limit
                                 << ", from_the_end = " << from_the_end << ": ";
        for (const auto &debug_message : messages) {
          error += to_string(debug_message);
        }
        LOG(ERROR) << error;
        promise.set_value(Unit());
        return;
      }
      cur_message_id = message_id;
    }
  }

  // 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_added_message_id;
  bool have_next = false;

  if (narrow_cast<int32>(messages.size()) < limit + offset && messages.size() <= 1) {
    MessageId first_received_message_id = MessageId::get_message_id(messages.back(), false);
    if (first_received_message_id >= from_message_id && d->first_database_message_id.is_valid() &&
        first_received_message_id >= d->first_database_message_id) {
      // it is likely that there are no more history messages on the server
      have_full_history = true;
    }
  }

  if (d->last_new_message_id.is_valid()) {
    // remove too new messages from response
    while (!messages.empty()) {
      if (DialogId::get_message_dialog_id(messages[0]) == dialog_id &&
          MessageId::get_message_id(messages[0], false) <= d->last_new_message_id) {
        // the message is old enough
        break;
      }

      LOG(INFO) << "Ignore too new " << MessageId::get_message_id(messages[0], false);
      messages.erase(messages.begin());
      if (messages.empty()) {
        // received no suitable messages; try again
        return promise.set_value(Unit());
      } else {
        last_received_message_id = MessageId::get_message_id(messages[0], false);
      }
    }
  }

  bool prev_have_full_history = d->have_full_history;
  MessageId prev_last_new_message_id = d->last_new_message_id;
  MessageId prev_first_database_message_id = d->first_database_message_id;
  MessageId prev_last_database_message_id = d->last_database_message_id;
  MessageId prev_last_message_id = d->last_message_id;
  if (from_the_end) {
    // delete all server messages with ID > last_received_message_id
    // there were no new messages received after the getHistory request was sent, so they are already deleted message
    if (last_received_message_id.is_valid() && delete_newer_server_messages_at_the_end(d, last_received_message_id)) {
      have_next = true;
    }
  }

  for (auto &message : messages) {
    auto expected_message_id = MessageId::get_message_id(message, false);
    if (!have_next && from_the_end && expected_message_id < d->last_message_id) {
      // last message in the dialog should be attached to the next message if there is some
      have_next = true;
    }

    auto message_dialog_id = DialogId::get_message_dialog_id(message);
    if (message_dialog_id != dialog_id) {
      LOG(ERROR) << "Receive " << expected_message_id << " in wrong " << message_dialog_id << " instead of "
                 << dialog_id << ": " << oneline(to_string(message));
      continue;
    }

    auto message_full_id = on_get_message(std::move(message), false, is_channel_message, false, "get history");
    auto message_id = message_full_id.get_message_id();
    if (message_id.is_valid()) {
      CHECK(message_id == expected_message_id);
      if (have_next) {
        d->ordered_messages.attach_message_to_next(message_id, "on_get_history");
      }
      if (!last_added_message_id.is_valid()) {
        last_added_message_id = message_id;
      }

      if (!have_next) {
        have_next = true;
      } else if (first_added_message_id.is_valid()) {
        d->ordered_messages.attach_message_to_previous(first_added_message_id, "on_get_history");
      }
      first_added_message_id = message_id;
    }
  }

  if (from_the_end && last_added_message_id.is_valid() && last_added_message_id != last_received_message_id) {
    CHECK(last_added_message_id < last_received_message_id);
    delete_newer_server_messages_at_the_end(d, last_added_message_id);
  }

  if (have_full_history) {
    d->have_full_history = true;
    d->have_full_history_source = 2;
    on_dialog_updated(dialog_id, "set have_full_history 2");
  }

  if (from_the_end && !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");
  }
  bool intersect_last_database_message_ids =
      last_added_message_id >= d->first_database_message_id && d->last_database_message_id >= first_added_message_id;
  bool need_update_database_message_ids =
      last_added_message_id.is_valid() && (from_the_end || intersect_last_database_message_ids);
  if (from_the_end && last_added_message_id.is_valid() && last_added_message_id > d->last_message_id) {
    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) {
    if (from_the_end && !intersect_last_database_message_ids && d->last_database_message_id.is_valid()) {
      if (d->last_database_message_id < first_added_message_id || last_added_message_id == d->last_message_id) {
        set_dialog_first_database_message_id(d, MessageId(), "on_get_history 1");
        set_dialog_last_database_message_id(d, MessageId(), "on_get_history 1");
      } else {
        auto min_message_id = td::min(d->first_database_message_id, d->last_message_id);
        LOG_CHECK(last_added_message_id < min_message_id)
            << need_update_database_message_ids << ' ' << first_added_message_id << ' ' << last_added_message_id << ' '
            << d->first_database_message_id << ' ' << d->last_database_message_id << ' ' << d->last_new_message_id
            << ' ' << d->last_message_id << ' ' << prev_first_database_message_id << ' '
            << prev_last_database_message_id << ' ' << prev_last_new_message_id << ' ' << prev_last_message_id;
        if (min_message_id <= last_added_message_id.get_next_message_id(MessageType::Server)) {
          // connect local messages with last received server message
          set_dialog_first_database_message_id(d, last_added_message_id, "on_get_history 2");
        } else {
          LOG(WARNING) << "Have last " << d->last_message_id << " and first database " << d->first_database_message_id
                       << " in " << dialog_id << ", but received history from the end only up to "
                       << last_added_message_id;
          // can't connect messages, because there can be unknown server messages after last_added_message_id
        }
      }
    }
    if (!d->last_database_message_id.is_valid()) {
      CHECK(d->last_message_id.is_valid());
      auto it = d->ordered_messages.get_const_iterator(d->last_message_id);
      MessageId new_first_database_message_id;
      while (*it != nullptr) {
        auto message_id = (*it)->get_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");
          }
          new_first_database_message_id = message_id;
          try_restore_dialog_reply_markup(d, get_message(d, message_id));
        }
        --it;
      }
      if (new_first_database_message_id.is_valid()) {
        set_dialog_first_database_message_id(d, new_first_database_message_id, "on_get_history");
      }
    } else {
      LOG_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());
      {
        auto it = d->ordered_messages.get_const_iterator(d->first_database_message_id);
        if (*it != nullptr && ((*it)->get_message_id() == d->first_database_message_id || (*it)->have_next())) {
          MessageId new_first_database_message_id = d->first_database_message_id;
          while (*it != nullptr) {
            auto message_id = (*it)->get_message_id();
            if ((message_id.is_server() || message_id.is_local()) && message_id < new_first_database_message_id) {
              new_first_database_message_id = message_id;
              try_restore_dialog_reply_markup(d, get_message(d, message_id));
            }
            --it;
          }
          if (new_first_database_message_id != d->first_database_message_id) {
            set_dialog_first_database_message_id(d, new_first_database_message_id, "on_get_history 2");
          }
        }
      }
      {
        auto it = d->ordered_messages.get_const_iterator(d->last_database_message_id);
        if (*it != nullptr && ((*it)->get_message_id() == d->last_database_message_id || (*it)->have_next())) {
          MessageId new_last_database_message_id = d->last_database_message_id;
          while (*it != nullptr) {
            auto message_id = (*it)->get_message_id();
            if ((message_id.is_server() || message_id.is_local()) && message_id > new_last_database_message_id) {
              new_last_database_message_id = message_id;
            }
            ++it;
          }
          if (new_last_database_message_id != d->last_database_message_id) {
            set_dialog_last_database_message_id(d, new_last_database_message_id, "on_get_history 2");
          }
        }
      }
    }
    LOG_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 < first_message_id && first_message_id <= last_added_message_id) {
        first_message_id = first_added_message_id;
      }
    }
  }
  promise.set_value(Unit());
}

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

  CHECK(!query.empty());
  found_public_dialogs_[query] = td_->dialog_manager_->get_peers_dialog_ids(std::move(peers));
  found_on_server_dialogs_[query] = td_->dialog_manager_->get_peers_dialog_ids(std::move(my_peers));

  set_promises(promises);
}

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

  fail_promises(promises, std::move(error));
}

void MessagesManager::on_get_message_search_result_calendar(
    DialogId dialog_id, SavedMessagesTopicId saved_messages_topic_id, MessageId from_message_id,
    MessageSearchFilter filter, int64 random_id, int32 total_count,
    vector<tl_object_ptr<telegram_api::Message>> &&messages,
    vector<tl_object_ptr<telegram_api::searchResultsCalendarPeriod>> &&periods, Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  auto it = found_dialog_message_calendars_.find(random_id);
  CHECK(it != found_dialog_message_calendars_.end());

  int32 received_message_count = 0;
  for (auto &message : messages) {
    auto new_message_full_id = on_get_message(std::move(message), false, dialog_id.get_type() == DialogType::Channel,
                                              false, "on_get_message_search_result_calendar");
    if (new_message_full_id == MessageFullId()) {
      total_count--;
      continue;
    }

    if (new_message_full_id.get_dialog_id() != dialog_id) {
      LOG(ERROR) << "Receive " << new_message_full_id << " instead of a message in " << dialog_id;
      total_count--;
      continue;
    }

    received_message_count++;
  }
  if (total_count < received_message_count) {
    LOG(ERROR) << "Receive " << received_message_count << " valid messages out of " << total_count << " in "
               << messages.size() << " messages";
    total_count = received_message_count;
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (!saved_messages_topic_id.is_valid()) {
    auto &old_message_count = d->message_count_by_index[message_search_filter_index(filter)];
    if (old_message_count != total_count) {
      old_message_count = total_count;
      on_dialog_updated(dialog_id, "on_get_message_search_result_calendar");
    }
  }

  vector<td_api::object_ptr<td_api::messageCalendarDay>> days;
  for (auto &period : periods) {
    auto message_id = MessageId(ServerMessageId(period->min_msg_id_));
    const auto *m = get_message(d, message_id);
    if (m == nullptr) {
      LOG(ERROR) << "Failed to find " << message_id;
      continue;
    }
    if (period->count_ <= 0) {
      LOG(ERROR) << "Receive " << to_string(period);
      continue;
    }
    days.push_back(td_api::make_object<td_api::messageCalendarDay>(
        period->count_, get_message_object(dialog_id, m, "on_get_message_search_result_calendar")));
  }
  it->second = td_api::make_object<td_api::messageCalendar>(total_count, std::move(days));
  promise.set_value(Unit());
}

void MessagesManager::on_failed_get_message_search_result_calendar(int64 random_id) {
  auto it = found_dialog_message_calendars_.find(random_id);
  CHECK(it != found_dialog_message_calendars_.end());
  found_dialog_message_calendars_.erase(it);
}

void MessagesManager::on_get_dialog_messages_search_result(
    DialogId dialog_id, SavedMessagesTopicId saved_messages_topic_id, const string &query, DialogId sender_dialog_id,
    MessageId from_message_id, int32 offset, int32 limit, MessageSearchFilter filter, MessageId top_thread_message_id,
    const ReactionType &tag, int64 random_id, int32 total_count,
    vector<tl_object_ptr<telegram_api::Message>> &&messages, Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  LOG(INFO) << "Receive " << messages.size() << " found messages in " << dialog_id;
  if (!dialog_id.is_valid()) {
    CHECK(query.empty());
    CHECK(!sender_dialog_id.is_valid());
    CHECK(!top_thread_message_id.is_valid());
    CHECK(!saved_messages_topic_id.is_valid());
    CHECK(tag.is_empty());
    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 are no more messages or they can't be found due to global limit
      // anyway pretend that there are no more messages
      first_added_message_id = MessageId::min();
    }

    auto &result = it->second.message_full_ids;
    CHECK(result.empty());
    int32 added_message_count = 0;
    MessageId next_offset_message_id;
    for (auto &message : messages) {
      auto message_id = MessageId::get_message_id(message, false);
      if (message_id.is_valid() && (!next_offset_message_id.is_valid() || message_id < next_offset_message_id)) {
        next_offset_message_id = message_id;
      }
      auto new_message_full_id = on_get_message(std::move(message), false, false, false, "search call messages");
      if (new_message_full_id == MessageFullId()) {
        continue;
      }

      result.push_back(new_message_full_id);
      added_message_count++;

      CHECK(message_id == new_message_full_id.get_message_id());
      CHECK(message_id.is_valid());
      if (message_id < first_added_message_id || !first_added_message_id.is_valid()) {
        first_added_message_id = message_id;
      }
    }
    if (total_count < added_message_count) {
      LOG(ERROR) << "Receive total_count = " << total_count << ", but added " << added_message_count
                 << " messages out of " << messages.size();
      total_count = added_message_count;
    }
    if (G()->use_message_database()) {
      bool update_state = false;

      auto &old_message_count = calls_db_state_.message_count_by_index[call_message_search_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[call_message_search_filter_index(filter)];
      bool from_the_end = !from_message_id.is_valid() || from_message_id >= MessageId::max();
      LOG(INFO) << "Have from_the_end = " << from_the_end << ", old_first_db_message_id = " << old_first_db_message_id
                << ", first_added_message_id = " << first_added_message_id << ", from_message_id = " << from_message_id;
      if ((from_the_end || (old_first_db_message_id.is_valid() && old_first_db_message_id <= from_message_id)) &&
          (!old_first_db_message_id.is_valid() || first_added_message_id < old_first_db_message_id)) {
        LOG(INFO) << "Update calls database first message 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.total_count = total_count;
    if (next_offset_message_id.is_valid()) {
      it->second.next_offset = PSTRING() << next_offset_message_id.get_server_message_id().get();
    }
    promise.set_value(Unit());
    return;
  }

  auto it = found_dialog_messages_.find(random_id);
  CHECK(it != found_dialog_messages_.end());

  auto &result = it->second.message_ids;
  CHECK(result.empty());
  MessageId first_added_message_id;
  if (messages.empty()) {
    // messages may be empty because there are no more messages or they can't be found due to global limit
    // anyway pretend that there are no more messages
    first_added_message_id = MessageId::min();
  }
  bool can_be_in_different_dialog =
      top_thread_message_id.is_valid() && td_->dialog_manager_->is_broadcast_channel(dialog_id);
  DialogId real_dialog_id;
  MessageId next_from_message_id;
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  for (auto &message : messages) {
    auto message_id = MessageId::get_message_id(message, false);
    if (message_id.is_valid() && (!next_from_message_id.is_valid() || message_id < next_from_message_id)) {
      next_from_message_id = message_id;
    }
    auto new_message_full_id = on_get_message(std::move(message), false, dialog_id.get_type() == DialogType::Channel,
                                              false, "on_get_dialog_messages_search_result");
    if (new_message_full_id == MessageFullId()) {
      total_count--;
      continue;
    }

    if (new_message_full_id.get_dialog_id() != dialog_id) {
      if (!can_be_in_different_dialog) {
        LOG(ERROR) << "Receive " << new_message_full_id << " instead of a message in " << dialog_id;
        total_count--;
        continue;
      } else {
        if (!real_dialog_id.is_valid()) {
          real_dialog_id = new_message_full_id.get_dialog_id();
          found_dialog_messages_dialog_id_[random_id] = real_dialog_id;
        } else if (new_message_full_id.get_dialog_id() != real_dialog_id) {
          LOG(ERROR) << "Receive " << new_message_full_id << " instead of a message in " << real_dialog_id << " or "
                     << dialog_id;
          total_count--;
          continue;
        }
      }
    }

    CHECK(message_id == new_message_full_id.get_message_id());
    if (filter == MessageSearchFilter::UnreadMention && message_id <= d->last_read_all_mentions_message_id &&
        !real_dialog_id.is_valid()) {
      total_count--;
      continue;
    }

    if (filter != MessageSearchFilter::Empty) {
      const Message *m = get_message(new_message_full_id);
      CHECK(m != nullptr);
      auto index_mask = get_message_index_mask(new_message_full_id.get_dialog_id(), m);
      if ((message_search_filter_index_mask(filter) & index_mask) == 0) {
        LOG(INFO) << "Skip " << new_message_full_id << " of unexpected type";
        total_count--;
        continue;
      }
    }

    // TODO check that messages are returned in decreasing message_id order
    if (message_id < first_added_message_id || !first_added_message_id.is_valid()) {
      first_added_message_id = message_id;
    }
    result.push_back(message_id);
  }
  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_dialog_id.is_valid() && filter != MessageSearchFilter::Empty &&
      !top_thread_message_id.is_valid() && !saved_messages_topic_id.is_valid() && tag.is_empty()) {
    bool from_the_end = !from_message_id.is_valid() ||
                        (d->last_message_id != MessageId() && from_message_id > d->last_message_id) ||
                        from_message_id >= MessageId::max();
    bool update_dialog = false;

    auto &old_message_count = d->message_count_by_index[message_search_filter_index(filter)];
    if (old_message_count != total_count) {
      old_message_count = total_count;
      if (filter == MessageSearchFilter::UnreadMention) {
        d->unread_mention_count = old_message_count;
        update_dialog_mention_notification_count(d);
        send_update_chat_unread_mention_count(d);
      }
      if (filter == MessageSearchFilter::UnreadReaction) {
        d->unread_reaction_count = old_message_count;
        // update_dialog_mention_notification_count(d);
        send_update_chat_unread_reaction_count(d, "on_get_dialog_messages_search_result");
      }
      update_dialog = true;
    }

    auto &old_first_database_message_id = d->first_database_message_id_by_index[message_search_filter_index(filter)];
    if ((from_the_end ||
         (old_first_database_message_id.is_valid() && old_first_database_message_id <= from_message_id)) &&
        (!old_first_database_message_id.is_valid() || first_added_message_id < old_first_database_message_id)) {
      old_first_database_message_id = first_added_message_id;
      update_dialog = true;
    }
    if (update_dialog) {
      on_dialog_updated(dialog_id, "search results");
    }

    if (from_the_end && filter == MessageSearchFilter::Pinned) {
      set_dialog_last_pinned_message_id(d, result.empty() ? MessageId() : result[0]);
    }
  }

  it->second.total_count = total_count;
  it->second.next_from_message_id = next_from_message_id;
  promise.set_value(Unit());
}

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_dialog_message_count(DialogId dialog_id, SavedMessagesTopicId saved_messages_topic_id,
                                                  MessageSearchFilter filter, int32 total_count,
                                                  Promise<int32> &&promise) {
  LOG(INFO) << "Receive " << total_count << " message count in " << dialog_id << " with filter " << filter;
  if (total_count < 0) {
    LOG(ERROR) << "Receive total message count = " << total_count << " in " << dialog_id << " with "
               << saved_messages_topic_id << " and filter " << filter;
    total_count = 0;
  }

  if (saved_messages_topic_id.is_valid()) {
    return promise.set_value(std::move(total_count));
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  CHECK(filter != MessageSearchFilter::Empty);
  CHECK(filter != MessageSearchFilter::UnreadMention);
  CHECK(filter != MessageSearchFilter::UnreadReaction);
  CHECK(filter != MessageSearchFilter::FailedToSend);

  auto &old_message_count = d->message_count_by_index[message_search_filter_index(filter)];
  if (old_message_count != total_count) {
    old_message_count = total_count;
    on_dialog_updated(dialog_id, "on_get_dialog_message_count");
  }

  if (total_count == 0) {
    auto &old_first_database_message_id = d->first_database_message_id_by_index[message_search_filter_index(filter)];
    if (old_first_database_message_id != MessageId::min()) {
      old_first_database_message_id = MessageId::min();
      on_dialog_updated(dialog_id, "on_get_dialog_message_count");
    }
    if (filter == MessageSearchFilter::Pinned) {
      set_dialog_last_pinned_message_id(d, MessageId());
    }
  }
  promise.set_value(std::move(total_count));
}

void MessagesManager::on_get_messages_search_result(const string &query, int32 offset_date, DialogId offset_dialog_id,
                                                    MessageId offset_message_id, int32 limit,
                                                    MessageSearchFilter filter, int32 min_date, int32 max_date,
                                                    int64 random_id, int32 total_count,
                                                    vector<tl_object_ptr<telegram_api::Message>> &&messages,
                                                    int32 next_rate, Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  LOG(INFO) << "Receive " << messages.size() << " found messages";
  auto it = found_messages_.find(random_id);
  CHECK(it != found_messages_.end());

  auto &result = it->second.message_full_ids;
  CHECK(result.empty());
  int32 last_message_date = 0;
  MessageId last_message_id;
  DialogId last_dialog_id;
  for (auto &message : messages) {
    auto message_date = get_message_date(message);
    auto message_id = MessageId::get_message_id(message, false);
    auto dialog_id = DialogId::get_message_dialog_id(message);
    if (message_date > 0 && message_id.is_valid() && dialog_id.is_valid()) {
      last_message_date = message_date;
      last_message_id = message_id;
      last_dialog_id = dialog_id;
    }

    auto new_message_full_id = on_get_message(std::move(message), false, dialog_id.get_type() == DialogType::Channel,
                                              false, "search messages");
    if (new_message_full_id != MessageFullId()) {
      CHECK(dialog_id == new_message_full_id.get_dialog_id());
      result.push_back(new_message_full_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.total_count = total_count;
  if (!result.empty()) {
    if (next_rate > 0) {
      last_message_date = next_rate;
    }
    it->second.next_offset = PSTRING() << last_message_date << ',' << last_dialog_id.get() << ','
                                       << last_message_id.get_server_message_id().get();
  }
  promise.set_value(Unit());
}

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_outgoing_document_messages(vector<tl_object_ptr<telegram_api::Message>> &&messages,
                                                        Promise<td_api::object_ptr<td_api::foundMessages>> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  FoundMessages found_messages;
  for (auto &message : messages) {
    auto dialog_id = DialogId::get_message_dialog_id(message);
    auto message_full_id = on_get_message(std::move(message), false, dialog_id.get_type() == DialogType::Channel, false,
                                          "on_get_outgoing_document_messages");
    if (message_full_id != MessageFullId()) {
      CHECK(dialog_id == message_full_id.get_dialog_id());
      found_messages.message_full_ids.push_back(message_full_id);
    }
  }
  auto result = get_found_messages_object(found_messages, "on_get_outgoing_document_messages");
  td::remove_if(result->messages_,
                [](const auto &message) { return message->content_->get_id() != td_api::messageDocument::ID; });
  result->total_count_ = narrow_cast<int32>(result->messages_.size());
  promise.set_value(std::move(result));
}

void MessagesManager::on_get_scheduled_server_messages(DialogId dialog_id, uint32 generation,
                                                       vector<tl_object_ptr<telegram_api::Message>> &&messages,
                                                       bool is_not_modified) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (generation < d->scheduled_messages_sync_generation) {
    LOG(INFO) << "Ignore scheduled messages with old generation " << generation << " instead of "
              << d->scheduled_messages_sync_generation << " in " << dialog_id;
    return;
  }
  d->scheduled_messages_sync_generation = generation;

  if (is_not_modified) {
    LOG(INFO) << "Scheduled messages are mot modified in " << dialog_id;
    return;
  }

  vector<MessageId> old_message_ids;
  if (d->scheduled_messages != nullptr) {
    for (const auto &it : d->scheduled_messages->scheduled_messages_) {
      old_message_ids.push_back(it.first);
    };
  }
  FlatHashMap<ScheduledServerMessageId, MessageId, ScheduledServerMessageIdHash> old_server_message_ids;
  for (auto &message_id : old_message_ids) {
    if (message_id.is_scheduled_server()) {
      old_server_message_ids[message_id.get_scheduled_server_message_id()] = message_id;
    }
  }

  bool is_channel_message = dialog_id.get_type() == DialogType::Channel;
  bool has_scheduled_server_messages = false;
  for (auto &message : messages) {
    auto message_dialog_id = DialogId::get_message_dialog_id(message);
    if (message_dialog_id != dialog_id) {
      // server can send messageEmpty for deleted scheduled messages
      auto message_id = MessageId::get_message_id(message, true);
      if (message_id.is_valid() || message_dialog_id.is_valid()) {
        LOG(ERROR) << "Receive " << message_id << " in wrong " << message_dialog_id << " instead of " << dialog_id
                   << ": " << oneline(to_string(message));
      }
      continue;
    }

    auto message_full_id = on_get_message(std::move(message), d->sent_scheduled_messages, is_channel_message, true,
                                          "on_get_scheduled_server_messages");
    auto message_id = message_full_id.get_message_id();
    if (message_id.is_valid_scheduled()) {
      CHECK(message_id.is_scheduled_server());
      old_server_message_ids.erase(message_id.get_scheduled_server_message_id());
      has_scheduled_server_messages = true;
    }
  }
  on_update_dialog_has_scheduled_server_messages(dialog_id, has_scheduled_server_messages);

  for (const auto &it : old_server_message_ids) {
    auto message_id = it.second;
    auto message = do_delete_scheduled_message(d, message_id, true, "on_get_scheduled_server_messages");
    CHECK(message != nullptr);
    send_update_delete_messages(dialog_id, {message->message_id.get()}, true);
  }

  send_update_chat_has_scheduled_messages(d, false);
}

void MessagesManager::on_get_recent_locations(DialogId dialog_id, int32 limit, int32 total_count,
                                              vector<tl_object_ptr<telegram_api::Message>> &&messages,
                                              Promise<td_api::object_ptr<td_api::messages>> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  LOG(INFO) << "Receive " << messages.size() << " recent locations in " << dialog_id;
  vector<MessageId> result;
  for (auto &message : messages) {
    auto new_message_full_id = on_get_message(std::move(message), false, dialog_id.get_type() == DialogType::Channel,
                                              false, "get recent locations");
    if (new_message_full_id != MessageFullId()) {
      if (new_message_full_id.get_dialog_id() != dialog_id) {
        LOG(ERROR) << "Receive " << new_message_full_id << " instead of a message in " << dialog_id;
        total_count--;
        continue;
      }
      auto m = get_message(new_message_full_id);
      CHECK(m != nullptr);
      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;
        total_count--;
        continue;
      }

      result.push_back(m->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());
  }
  promise.set_value(get_messages_object(total_count, dialog_id, result, true, "on_get_recent_locations"));
}

void MessagesManager::delete_messages_from_updates(const vector<MessageId> &message_ids, bool is_permanent) {
  FlatHashMap<DialogId, vector<int64>, DialogIdHash> deleted_message_ids;
  FlatHashMap<DialogId, bool, DialogIdHash> need_update_dialog_pos;
  vector<unique_ptr<Message>> deleted_messages;
  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 message = delete_message(d, message_id, is_permanent, &need_update_dialog_pos[d->dialog_id],
                                    "delete_messages_from_updates");
      CHECK(message != nullptr);
      LOG_CHECK(message->message_id == message_id) << message_id << " " << message->message_id << " " << d->dialog_id;
      deleted_message_ids[d->dialog_id].push_back(message->message_id.get());
      deleted_messages.push_back(std::move(message));
    }
    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 message = delete_message(d, message_id, is_permanent, &need_update_dialog_pos[d->dialog_id],
                                    "delete_messages_from_updates");
      CHECK(message == nullptr);
    }
  }
  if (deleted_messages.size() >= MIN_DELETED_ASYNCHRONOUSLY_MESSAGES) {
    Scheduler::instance()->destroy_on_scheduler(G()->get_gc_scheduler_id(), deleted_messages);
  }
  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), is_permanent);
  }
}

void MessagesManager::delete_dialog_messages(DialogId dialog_id, const vector<MessageId> &message_ids,
                                             bool force_update_for_not_found_messages, const char *source) {
  Dialog *d = get_dialog_force(dialog_id, "delete_dialog_messages");
  if (d == nullptr) {
    LOG(INFO) << "Ignore deleteChannelMessages for unknown " << dialog_id << " from " << source;
    CHECK(dialog_id.get_type() == DialogType::Channel);
    return;
  }

  delete_dialog_messages(d, message_ids, force_update_for_not_found_messages, source);
}

void MessagesManager::delete_dialog_messages(Dialog *d, const vector<MessageId> &message_ids,
                                             bool force_update_for_not_found_messages, const char *source) {
  vector<unique_ptr<Message>> deleted_messages;
  vector<int64> deleted_message_ids;
  bool need_update_dialog_pos = false;
  bool need_update_chat_has_scheduled_messages = false;
  for (auto message_id : message_ids) {
    CHECK(message_id.is_valid() || message_id.is_valid_scheduled());

    bool force_update = force_update_for_not_found_messages && !is_deleted_message(d, message_id);
    auto message = delete_message(d, message_id, true, &need_update_dialog_pos, source);
    if (message == nullptr) {
      if (force_update) {
        deleted_message_ids.push_back(message_id.get());
      }
    } else {
      need_update_chat_has_scheduled_messages |= message->message_id.is_scheduled();
      deleted_message_ids.push_back(message->message_id.get());
      deleted_messages.push_back(std::move(message));
    }
  }
  if (deleted_messages.size() >= MIN_DELETED_ASYNCHRONOUSLY_MESSAGES) {
    Scheduler::instance()->destroy_on_scheduler(G()->get_gc_scheduler_id(), deleted_messages);
  }
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, source);
  }
  send_update_delete_messages(d->dialog_id, std::move(deleted_message_ids), true);

  if (need_update_chat_has_scheduled_messages) {
    send_update_chat_has_scheduled_messages(d, true);
  }
}

void MessagesManager::update_dialog_pinned_messages_from_updates(DialogId dialog_id,
                                                                 const vector<MessageId> &message_ids, bool is_pin) {
  Dialog *d = get_dialog_force(dialog_id, "update_dialog_pinned_messages_from_updates");
  if (d == nullptr) {
    LOG(INFO) << "Ignore updatePinnedMessages for unknown " << dialog_id;
    return;
  }

  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 pin/unpin " << message_id << " in " << dialog_id;
      continue;
    }

    Message *m = get_message_force(d, message_id, "update_dialog_pinned_messages_from_updates");
    if (m != nullptr && update_message_is_pinned(d, m, is_pin, "update_dialog_pinned_messages_from_updates")) {
      on_message_changed(d, m, true, "update_dialog_pinned_messages_from_updates");
    }
  }
}

bool MessagesManager::update_message_is_pinned(Dialog *d, Message *m, bool is_pinned, const char *source) {
  CHECK(m != nullptr);
  CHECK(!m->message_id.is_scheduled());
  if (m->is_pinned == is_pinned) {
    return false;
  }

  LOG(INFO) << "Update message is_pinned of " << m->message_id << " in " << d->dialog_id << " to " << is_pinned
            << " from " << source;
  auto old_index_mask = get_message_index_mask(d->dialog_id, m);
  m->is_pinned = is_pinned;
  auto new_index_mask = get_message_index_mask(d->dialog_id, m);
  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);

  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateMessageIsPinned>(
                   get_chat_id_object(d->dialog_id, "updateMessageIsPinned"), m->message_id.get(), is_pinned));
  if (is_pinned) {
    if (d->is_last_pinned_message_id_inited && m->message_id > d->last_pinned_message_id) {
      set_dialog_last_pinned_message_id(d, m->message_id);
    }
  } else {
    if (d->is_last_pinned_message_id_inited && m->message_id == d->last_pinned_message_id) {
      if (!td_->auth_manager_->is_bot() &&
          d->message_count_by_index[message_search_filter_index(MessageSearchFilter::Pinned)] == 0) {
        set_dialog_last_pinned_message_id(d, MessageId());
      } else {
        drop_dialog_last_pinned_message_id(d);
      }
    }
  }
  return true;
}

string MessagesManager::get_message_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.is_empty()) {
    return false;
  }
  if (m->message_id.is_scheduled()) {
    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_save_message(DialogId dialog_id, const Message *m) const {
  if (m == nullptr || m->noforwards || m->is_content_secret) {
    return false;
  }
  return !td_->dialog_manager_->get_dialog_has_protected_content(dialog_id);
}

bool MessagesManager::can_get_message_statistics(MessageFullId message_full_id) {
  return can_get_message_statistics(message_full_id.get_dialog_id(),
                                    get_message_force(message_full_id, "can_get_message_statistics"));
}

bool MessagesManager::can_get_message_statistics(DialogId dialog_id, const Message *m) const {
  if (td_->auth_manager_->is_bot()) {
    return false;
  }
  if (m == nullptr || m->message_id.is_scheduled() || !m->message_id.is_server() || m->view_count == 0 ||
      m->had_forward_info || (m->forward_info != nullptr && m->forward_info->get_origin().is_channel_post())) {
    return false;
  }
  return td_->contacts_manager_->can_get_channel_message_statistics(dialog_id);
}

bool MessagesManager::can_delete_channel_message(const 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 (m->message_id.is_scheduled()) {
    if (m->is_channel_post) {
      return status.can_post_messages();
    }
    return true;
  }

  if (is_bot && G()->unix_time() >= 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 ||
      content_type == MessageContentType::TopicCreate) {
    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_delete_message(DialogId dialog_id, const Message *m) const {
  if (m == nullptr) {
    return true;
  }
  if (m->message_id.is_local() || m->message_id.is_yet_unsent()) {
    return true;
  }
  switch (dialog_id.get_type()) {
    case DialogType::User:
      return true;
    case DialogType::Chat:
      return true;
    case DialogType::Channel: {
      auto dialog_status = td_->contacts_manager_->get_channel_permissions(dialog_id.get_channel_id());
      return can_delete_channel_message(dialog_status, m, td_->auth_manager_->is_bot());
    }
    case DialogType::SecretChat:
      return true;
    case DialogType::None:
    default:
      UNREACHABLE();
      return false;
  }
}

bool MessagesManager::can_revoke_message(DialogId dialog_id, const Message *m) const {
  if (m == nullptr) {
    return true;
  }
  if (m->message_id.is_local()) {
    return false;
  }
  if (dialog_id == td_->dialog_manager_->get_my_dialog_id()) {
    return false;
  }
  if (m->message_id.is_scheduled()) {
    return false;
  }
  if (m->message_id.is_yet_unsent()) {
    return true;
  }
  CHECK(m->message_id.is_server());

  const int32 DEFAULT_REVOKE_TIME_LIMIT = td_->auth_manager_->is_bot() ? 2 * 86400 : std::numeric_limits<int32>::max();
  auto content_type = m->content->get_type();
  switch (dialog_id.get_type()) {
    case DialogType::User: {
      bool can_revoke_incoming = td_->option_manager_->get_option_boolean("revoke_pm_inbox", true);
      int64 revoke_time_limit =
          td_->option_manager_->get_option_integer("revoke_pm_time_limit", DEFAULT_REVOKE_TIME_LIMIT);

      if (G()->unix_time() - m->date < 86400 && content_type == MessageContentType::Dice) {
        return false;
      }
      return ((m->is_outgoing && !is_service_message_content(content_type)) ||
              (can_revoke_incoming && content_type != MessageContentType::ScreenshotTaken)) &&
             G()->unix_time() - m->date <= revoke_time_limit;
    }
    case DialogType::Chat: {
      bool is_appointed_administrator =
          td_->contacts_manager_->is_appointed_chat_administrator(dialog_id.get_chat_id());
      int64 revoke_time_limit =
          td_->option_manager_->get_option_integer("revoke_time_limit", DEFAULT_REVOKE_TIME_LIMIT);

      return ((m->is_outgoing && !is_service_message_content(content_type)) || is_appointed_administrator) &&
             G()->unix_time() - m->date <= revoke_time_limit;
    }
    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(content_type);
    case DialogType::None:
    default:
      UNREACHABLE();
      return false;
  }
}

void MessagesManager::delete_messages(DialogId dialog_id, const vector<MessageId> &input_message_ids, bool revoke,
                                      Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());
  Dialog *d = get_dialog_force(dialog_id, "delete_messages");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "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;

  vector<MessageId> deleted_scheduled_server_message_ids;
  for (auto message_id : input_message_ids) {
    if (!message_id.is_valid() && !message_id.is_valid_scheduled()) {
      return promise.set_error(Status::Error(400, "Invalid message identifier"));
    }

    message_id = get_persistent_message_id(d, message_id);
    message_ids.push_back(message_id);
    auto m = get_message_force(d, message_id, "delete_messages");
    if (m != nullptr) {
      if (m->message_id.is_scheduled()) {
        if (m->message_id.is_scheduled_server()) {
          deleted_scheduled_server_message_ids.push_back(m->message_id);
        }
      } else {
        if (m->message_id.is_server() || is_secret) {
          deleted_server_message_ids.push_back(m->message_id);
        }
      }
    }
  }

  bool is_bot = td_->auth_manager_->is_bot();
  for (auto message_id : message_ids) {
    auto m = get_message(d, message_id);
    if (!can_delete_message(dialog_id, m)) {
      return promise.set_error(Status::Error(400, "Message can't be deleted"));
    }
    if (is_bot && !message_id.is_scheduled() && message_id.is_server() && !can_revoke_message(dialog_id, m)) {
      return promise.set_error(Status::Error(400, "Message can't be deleted for everyone"));
    }
  }

  MultiPromiseActorSafe mpas{"DeleteMessagesMultiPromiseActor"};
  mpas.add_promise(std::move(promise));

  auto lock = mpas.get_promise();
  delete_messages_on_server(dialog_id, std::move(deleted_server_message_ids), revoke, 0, mpas.get_promise());
  delete_scheduled_messages_on_server(dialog_id, std::move(deleted_scheduled_server_message_ids), 0,
                                      mpas.get_promise());
  lock.set_value(Unit());

  delete_dialog_messages(d, message_ids, false, DELETE_MESSAGE_USER_REQUEST_SOURCE);
}

void MessagesManager::erase_delete_messages_log_event(uint64 log_event_id) {
  if (!G()->close_flag()) {
    binlog_erase(G()->td_db()->get_binlog(), log_event_id);
  }
}

void MessagesManager::delete_sent_message_on_server(DialogId dialog_id, MessageId message_id,
                                                    MessageId old_message_id) {
  // this would be a no-op, because replies have already been removed in cancel_send_message_query
  // update_reply_to_message_id(dialog_id, old_message_id, message_id, false, "delete_sent_message_on_server");

  // being sent message was deleted by the user or is in an inaccessible channel
  // don't need to send an update to the user, because the message has already been deleted
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    LOG(INFO) << "Ignore sent " << message_id << " in inaccessible " << dialog_id;
    return;
  }

  LOG(INFO) << "Delete already deleted sent " << message_id << " in " << dialog_id << " from server";
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (get_message_force(d, message_id, "delete_sent_message_on_server") != nullptr) {
    delete_messages(dialog_id, {message_id}, true, Auto());
  } else {
    if (message_id.is_valid()) {
      CHECK(message_id.is_server());
      delete_messages_on_server(dialog_id, {message_id}, true, 0, Auto());
    } else {
      CHECK(message_id.is_scheduled_server());
      delete_scheduled_messages_on_server(dialog_id, {message_id}, 0, Auto());
    }

    bool need_update_dialog_pos = false;
    auto message = delete_message(d, message_id, true, &need_update_dialog_pos, "delete_sent_message_on_server");
    CHECK(message == nullptr);
    if (need_update_dialog_pos) {  // last_clear_history_message_id might be removed
      update_dialog_pos(d, "delete_sent_message_on_server");
    }
  }
}

class MessagesManager::DeleteMessagesOnServerLogEvent {
 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_on_server_log_event(DialogId dialog_id,
                                                                 const vector<MessageId> &message_ids, bool revoke) {
  DeleteMessagesOnServerLogEvent log_event{dialog_id, message_ids, revoke};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteMessagesOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::delete_messages_on_server(DialogId dialog_id, vector<MessageId> message_ids, bool revoke,
                                                uint64 log_event_id, Promise<Unit> &&promise) {
  if (message_ids.empty()) {
    return promise.set_value(Unit());
  }
  LOG(INFO) << (revoke ? "Revoke " : "Delete ") << format::as_array(message_ids) << " in " << dialog_id
            << " from server";

  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_delete_messages_on_server_log_event(dialog_id, message_ids, revoke);
  }

  MultiPromiseActorSafe mpas{"DeleteMessagesOnServerMultiPromiseActor"};
  mpas.add_promise(std::move(promise));
  if (log_event_id != 0) {
    mpas.add_promise(PromiseCreator::lambda([actor_id = actor_id(this), log_event_id](Unit) {
      send_closure(actor_id, &MessagesManager::erase_delete_messages_log_event, log_event_id);
    }));
  }
  auto lock = mpas.get_promise();
  auto dialog_type = dialog_id.get_type();
  switch (dialog_type) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel: {
      auto server_message_ids = MessageId::get_server_message_ids(message_ids);
      const size_t MAX_SLICE_SIZE = 100;  // server side limit
      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();
        if (dialog_type != DialogType::Channel) {
          td_->create_handler<DeleteMessagesQuery>(mpas.get_promise())
              ->send(dialog_id, {server_message_ids.begin() + i, end}, revoke);
        } else {
          td_->create_handler<DeleteChannelMessagesQuery>(mpas.get_promise())
              ->send(dialog_id.get_channel_id(), {server_message_ids.begin() + i, end});
        }
      }
      break;
    }
    case DialogType::SecretChat: {
      vector<int64> random_ids;
      auto d = get_dialog_force(dialog_id, "delete_messages_on_server");
      CHECK(d != nullptr);
      for (auto &message_id : message_ids) {
        auto *m = get_message(d, message_id);
        if (m != nullptr) {
          random_ids.push_back(m->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), mpas.get_promise());
      }
      break;
    }
    case DialogType::None:
    default:
      UNREACHABLE();
  }
  lock.set_value(Unit());
}

class MessagesManager::DeleteScheduledMessagesOnServerLogEvent {
 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_delete_scheduled_messages_on_server_log_event(DialogId dialog_id,
                                                                           const vector<MessageId> &message_ids) {
  DeleteScheduledMessagesOnServerLogEvent log_event{dialog_id, message_ids};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteScheduledMessagesOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::delete_scheduled_messages_on_server(DialogId dialog_id, vector<MessageId> message_ids,
                                                          uint64 log_event_id, Promise<Unit> &&promise) {
  if (message_ids.empty()) {
    return promise.set_value(Unit());
  }
  LOG(INFO) << "Delete " << format::as_array(message_ids) << " in " << dialog_id << " from server";

  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_delete_scheduled_messages_on_server_log_event(dialog_id, message_ids);
  }

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

  td_->create_handler<DeleteScheduledMessagesQuery>(std::move(promise))->send(dialog_id, std::move(message_ids));
}

void MessagesManager::on_failed_message_deletion(DialogId dialog_id, const vector<int32> &server_message_ids) {
  if (G()->close_flag()) {
    return;
  }
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  vector<MessageFullId> message_full_ids;
  for (auto &server_message_id : server_message_ids) {
    auto message_id = MessageId(ServerMessageId(server_message_id));
    d->deleted_message_ids.erase(message_id);
    message_full_ids.emplace_back(dialog_id, message_id);
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return;
  }
  get_messages_from_server(std::move(message_full_ids), Promise<Unit>(), "on_failed_message_deletion");
}

void MessagesManager::on_failed_scheduled_message_deletion(DialogId dialog_id, const vector<MessageId> &message_ids) {
  if (G()->close_flag()) {
    return;
  }
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (d->scheduled_messages != nullptr) {
    for (auto &message_id : message_ids) {
      d->scheduled_messages->deleted_scheduled_server_message_ids_.erase(message_id.get_scheduled_server_message_id());
    }
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read) ||
      dialog_id.get_type() == DialogType::SecretChat) {
    return;
  }
  if (td_->dialog_manager_->is_broadcast_channel(dialog_id) &&
      !td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id()).can_post_messages()) {
    return;
  }
  load_dialog_scheduled_messages(dialog_id, false, 0, Promise<Unit>());
}

MessagesManager::CanDeleteDialog MessagesManager::can_delete_dialog(const Dialog *d) const {
  if (is_dialog_sponsored(d)) {
    auto chat_source = sponsored_dialog_source_.get_chat_source_object();
    if (chat_source != nullptr) {
      switch (chat_source->get_id()) {
        case td_api::chatSourcePublicServiceAnnouncement::ID:
          // can delete for self (but only while removing from dialog list)
          return {true, false};
        default:
          return {false, false};
      }
    }
  }
  if (td_->auth_manager_->is_bot() || !td_->dialog_manager_->have_input_peer(d->dialog_id, AccessRights::Read)) {
    return {false, false};
  }

  switch (d->dialog_id.get_type()) {
    case DialogType::User:
      if (d->dialog_id == td_->dialog_manager_->get_my_dialog_id() ||
          td_->contacts_manager_->is_user_deleted(d->dialog_id.get_user_id()) ||
          td_->contacts_manager_->is_user_bot(d->dialog_id.get_user_id())) {
        return {true, false};
      }
      return {true, td_->option_manager_->get_option_boolean("revoke_pm_inbox", true)};
    case DialogType::Chat:
      // chats can be deleted only for self and can be deleted for everyone by their creator
      return {true, td_->contacts_manager_->get_chat_status(d->dialog_id.get_chat_id()).is_creator()};
    case DialogType::Channel: {
      // private non-forum joined supergroups can be deleted for self
      auto channel_id = d->dialog_id.get_channel_id();
      return {!td_->contacts_manager_->is_broadcast_channel(channel_id) &&
                  !td_->contacts_manager_->is_channel_public(channel_id) &&
                  !td_->contacts_manager_->is_forum_channel(channel_id) &&
                  td_->contacts_manager_->get_channel_status(channel_id).is_member(),
              td_->contacts_manager_->get_channel_can_be_deleted(channel_id)};
    }
    case DialogType::SecretChat:
      if (td_->contacts_manager_->get_secret_chat_state(d->dialog_id.get_secret_chat_id()) == SecretChatState::Closed) {
        // in a closed secret chats there is no way to delete messages for both users
        return {true, false};
      }
      // active secret chats can be deleted only for both users
      return {false, true};
    case DialogType::None:
    default:
      UNREACHABLE();
      return {false, false};
  }
}

void MessagesManager::delete_dialog_history(DialogId dialog_id, bool remove_from_dialog_list, bool revoke,
                                            Promise<Unit> &&promise) {
  LOG(INFO) << "Receive deleteChatHistory request to delete all messages in " << dialog_id
            << ", remove_from_chat_list is " << remove_from_dialog_list << ", revoke is " << revoke;

  Dialog *d = get_dialog_force(dialog_id, "delete_dialog_history");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Chat info not found"));
  }

  if (is_dialog_sponsored(d)) {
    auto chat_source = sponsored_dialog_source_.get_chat_source_object();
    if (chat_source == nullptr || chat_source->get_id() != td_api::chatSourcePublicServiceAnnouncement::ID) {
      return promise.set_error(Status::Error(400, "Can't delete the chat"));
    }
    if (!remove_from_dialog_list) {
      return promise.set_error(Status::Error(400, "Can't delete chat history without removing the chat"));
    }

    removed_sponsored_dialog_id_ = dialog_id;
    remove_sponsored_dialog();

    if (dialog_id.get_type() != DialogType::SecretChat) {
      td_->create_handler<HidePromoDataQuery>()->send(dialog_id);
    }
    promise.set_value(Unit());
    return;
  }

  auto can_delete = can_delete_dialog(d);
  if (revoke) {
    if (!can_delete.for_all_users_) {
      if (!can_delete.for_self_) {
        return promise.set_error(Status::Error(400, "Chat history can't be deleted"));
      }

      LOG(INFO) << "Can't delete history of " << dialog_id << " for everyone; delete it only for self";
      revoke = false;
    }
  } else {
    if (!can_delete.for_self_) {
      return promise.set_error(
          Status::Error(400, PSLICE() << "Can't delete history of " << dialog_id << " only for self"));
    }
  }

  auto last_new_message_id = d->last_new_message_id;
  if (dialog_id.get_type() != DialogType::SecretChat && last_new_message_id == MessageId()) {
    // TODO get dialog from the server and delete history from last message identifier
  }

  bool allow_error = d->messages.empty();
  auto old_order = d->order;

  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 && !revoke &&
      !(old_order != DEFAULT_ORDER && remove_from_dialog_list)) {
    // 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_on_server(dialog_id, last_new_message_id, remove_from_dialog_list, revoke, allow_error, 0,
                                  std::move(promise));
}

class MessagesManager::DeleteDialogHistoryOnServerLogEvent {
 public:
  DialogId dialog_id_;
  MessageId max_message_id_;
  bool remove_from_dialog_list_;
  bool revoke_;

  template <class StorerT>
  void store(StorerT &storer) const {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(remove_from_dialog_list_);
    STORE_FLAG(revoke_);
    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_);
    PARSE_FLAG(revoke_);
    END_PARSE_FLAGS();

    td::parse(dialog_id_, parser);
    td::parse(max_message_id_, parser);
  }
};

uint64 MessagesManager::save_delete_dialog_history_on_server_log_event(DialogId dialog_id, MessageId max_message_id,
                                                                       bool remove_from_dialog_list, bool revoke) {
  DeleteDialogHistoryOnServerLogEvent log_event{dialog_id, max_message_id, remove_from_dialog_list, revoke};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteDialogHistoryOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::delete_dialog_history_on_server(DialogId dialog_id, MessageId max_message_id,
                                                      bool remove_from_dialog_list, bool revoke, bool allow_error,
                                                      uint64 log_event_id, Promise<Unit> &&promise) {
  LOG(INFO) << "Delete history in " << dialog_id << " up to " << max_message_id << " from server";

  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id =
        save_delete_dialog_history_on_server_log_event(dialog_id, max_message_id, remove_from_dialog_list, revoke);
  }

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

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat: {
      AffectedHistoryQuery query = [td = td_, max_message_id, remove_from_dialog_list, revoke](
                                       DialogId dialog_id, Promise<AffectedHistory> &&query_promise) {
        td->create_handler<DeleteHistoryQuery>(std::move(query_promise))
            ->send(dialog_id, max_message_id, remove_from_dialog_list, revoke);
      };
      run_affected_history_query_until_complete(dialog_id, std::move(query), false, std::move(promise));
      break;
    }
    case DialogType::Channel:
      td_->create_handler<DeleteChannelHistoryQuery>(std::move(promise))
          ->send(dialog_id.get_channel_id(), max_message_id, allow_error, revoke);
      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::delete_topic_history(DialogId dialog_id, MessageId top_thread_message_id,
                                           Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(dialog_id, "delete_dialog_history");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Chat info not found"));
  }

  // auto old_order = d->order;
  // delete_all_dialog_topic_messages(d, top_thread_message_id);

  delete_topic_history_on_server(dialog_id, top_thread_message_id, 0, std::move(promise));
}

class MessagesManager::DeleteTopicHistoryOnServerLogEvent {
 public:
  DialogId dialog_id_;
  MessageId top_thread_message_id_;

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

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

uint64 MessagesManager::save_delete_topic_history_on_server_log_event(DialogId dialog_id,
                                                                      MessageId top_thread_message_id) {
  DeleteTopicHistoryOnServerLogEvent log_event{dialog_id, top_thread_message_id};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteTopicHistoryOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::delete_topic_history_on_server(DialogId dialog_id, MessageId top_thread_message_id,
                                                     uint64 log_event_id, Promise<Unit> &&promise) {
  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_delete_topic_history_on_server_log_event(dialog_id, top_thread_message_id);
  }

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

  AffectedHistoryQuery query = [td = td_, top_thread_message_id](DialogId dialog_id,
                                                                 Promise<AffectedHistory> &&query_promise) {
    td->create_handler<DeleteTopicHistoryQuery>(std::move(query_promise))->send(dialog_id, top_thread_message_id);
  };
  run_affected_history_query_until_complete(dialog_id, std::move(query), true, std::move(promise));
}

void MessagesManager::delete_all_call_messages(bool revoke, Promise<Unit> &&promise) {
  delete_all_call_messages_on_server(revoke, 0, std::move(promise));
}

class MessagesManager::DeleteAllCallMessagesOnServerLogEvent {
 public:
  bool revoke_;

  template <class StorerT>
  void store(StorerT &storer) const {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(revoke_);
    END_STORE_FLAGS();
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(revoke_);
    END_PARSE_FLAGS();
  }
};

uint64 MessagesManager::save_delete_all_call_messages_on_server_log_event(bool revoke) {
  DeleteAllCallMessagesOnServerLogEvent log_event{revoke};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteAllCallMessagesOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::delete_all_call_messages_on_server(bool revoke, uint64 log_event_id, Promise<Unit> &&promise) {
  if (log_event_id == 0) {
    log_event_id = save_delete_all_call_messages_on_server_log_event(revoke);
  }

  AffectedHistoryQuery query = [td = td_, revoke](DialogId /*dialog_id*/, Promise<AffectedHistory> &&query_promise) {
    td->create_handler<DeletePhoneCallHistoryQuery>(std::move(query_promise))->send(revoke);
  };
  run_affected_history_query_until_complete(DialogId(), std::move(query), false,
                                            get_erase_log_event_promise(log_event_id, std::move(promise)));
}

vector<MessageId> MessagesManager::find_dialog_messages(const Dialog *d,
                                                        const std::function<bool(const Message *)> &condition) {
  vector<MessageId> message_ids;
  d->messages.foreach([&](const MessageId &message_id, const unique_ptr<Message> &message) {
    CHECK(message_id == message->message_id);
    if (condition(message.get())) {
      message_ids.push_back(message_id);
    }
  });
  return message_ids;
}

vector<MessageId> MessagesManager::find_unloadable_messages(const Dialog *d, int32 unload_before_date,
                                                            bool &has_left_to_unload_messages) const {
  vector<MessageId> message_ids;
  for (auto it = d->message_lru_list.next; it != &d->message_lru_list; it = it->next) {
    if (message_ids.size() >= MAX_UNLOADED_MESSAGES) {
      has_left_to_unload_messages = true;
      break;
    }
    const auto *m = static_cast<const Message *>(it);
    if (can_unload_message(d, m)) {
      if (m->last_access_date <= unload_before_date) {
        message_ids.push_back(m->message_id);
      } else {
        has_left_to_unload_messages = true;
      }
    }
    if (has_left_to_unload_messages && m->date > unload_before_date) {
      // we aren't interested in unloading too new messages
      break;
    }
  }
  return message_ids;
}

void MessagesManager::delete_dialog_messages_by_sender(DialogId dialog_id, DialogId sender_dialog_id,
                                                       Promise<Unit> &&promise) {
  bool is_bot = td_->auth_manager_->is_bot();
  CHECK(!is_bot);

  Dialog *d = get_dialog_force(dialog_id, "delete_dialog_messages_by_sender");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Write)) {
    return promise.set_error(Status::Error(400, "Not enough rights"));
  }

  if (!td_->dialog_manager_->have_input_peer(sender_dialog_id, AccessRights::Know)) {
    return promise.set_error(Status::Error(400, "Message sender 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(400, "All messages from a sender can be deleted only in supergroup chats"));
    case DialogType::Channel: {
      channel_id = dialog_id.get_channel_id();
      if (!td_->contacts_manager_->is_megagroup_channel(channel_id)) {
        return promise.set_error(Status::Error(400, "The method is available only in supergroup chats"));
      }
      channel_status = td_->contacts_manager_->get_channel_permissions(channel_id);
      if (!channel_status.can_delete_messages()) {
        return promise.set_error(Status::Error(400, "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 (sender_dialog_id.get_type() == DialogType::SecretChat) {
    return promise.set_value(Unit());
  }

  if (G()->use_message_database()) {
    LOG(INFO) << "Delete all messages from " << sender_dialog_id << " in " << dialog_id << " from database";
    G()->td_db()->get_message_db_async()->delete_dialog_messages_by_sender(dialog_id, sender_dialog_id,
                                                                           Auto());  // TODO Promise
  }

  vector<MessageId> message_ids = find_dialog_messages(d, [sender_dialog_id, channel_status, is_bot](const Message *m) {
    return sender_dialog_id == get_message_sender(m) && can_delete_channel_message(channel_status, m, is_bot);
  });

  delete_dialog_messages(d, message_ids, false, DELETE_MESSAGE_USER_REQUEST_SOURCE);

  delete_all_channel_messages_by_sender_on_server(channel_id, sender_dialog_id, 0, std::move(promise));
}

class MessagesManager::DeleteAllChannelMessagesFromSenderOnServerLogEvent {
 public:
  ChannelId channel_id_;
  DialogId sender_dialog_id_;

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

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(channel_id_, parser);
    if (parser.version() >= static_cast<int32>(Version::AddKeyboardButtonFlags)) {
      td::parse(sender_dialog_id_, parser);
    } else {
      UserId user_id;
      td::parse(user_id, parser);
      sender_dialog_id_ = DialogId(user_id);
    }
  }
};

uint64 MessagesManager::save_delete_all_channel_messages_by_sender_on_server_log_event(ChannelId channel_id,
                                                                                       DialogId sender_dialog_id) {
  DeleteAllChannelMessagesFromSenderOnServerLogEvent log_event{channel_id, sender_dialog_id};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteAllChannelMessagesFromSenderOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::delete_all_channel_messages_by_sender_on_server(ChannelId channel_id, DialogId sender_dialog_id,
                                                                      uint64 log_event_id, Promise<Unit> &&promise) {
  if (log_event_id == 0 && G()->use_chat_info_database()) {
    log_event_id = save_delete_all_channel_messages_by_sender_on_server_log_event(channel_id, sender_dialog_id);
  }

  AffectedHistoryQuery query = [td = td_, sender_dialog_id](DialogId dialog_id,
                                                            Promise<AffectedHistory> &&query_promise) {
    td->create_handler<DeleteParticipantHistoryQuery>(std::move(query_promise))
        ->send(dialog_id.get_channel_id(), sender_dialog_id);
  };
  run_affected_history_query_until_complete(DialogId(channel_id), std::move(query),
                                            sender_dialog_id.get_type() != DialogType::User,
                                            get_erase_log_event_promise(log_event_id, std::move(promise)));
}

Status MessagesManager::fix_delete_message_min_max_dates(int32 &min_date, int32 &max_date) {
  if (min_date > max_date) {
    return Status::Error(400, "Wrong date interval specified");
  }

  const int32 telegram_launch_date = 1376438400;
  if (max_date < telegram_launch_date) {
    max_date = 0;
    min_date = 0;
    return Status::OK();
  }
  if (min_date < telegram_launch_date) {
    min_date = telegram_launch_date;
  }

  auto current_date = max(G()->unix_time(), 1635000000);
  if (min_date >= current_date - 30) {
    max_date = 0;
    min_date = 0;
    return Status::OK();
  }
  if (max_date >= current_date - 30) {
    max_date = current_date - 31;
  }
  CHECK(min_date <= max_date);
  return Status::OK();
}

void MessagesManager::delete_dialog_messages_by_date(DialogId dialog_id, int32 min_date, int32 max_date, bool revoke,
                                                     Promise<Unit> &&promise) {
  CHECK(!td_->auth_manager_->is_bot());

  Dialog *d = get_dialog_force(dialog_id, "delete_dialog_messages_by_date");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

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

  TRY_STATUS_PROMISE(promise, fix_delete_message_min_max_dates(min_date, max_date));
  if (max_date == 0) {
    return promise.set_value(Unit());
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      break;
    case DialogType::Chat:
      if (revoke) {
        return promise.set_error(Status::Error(400, "Bulk message revocation is unsupported in basic group chats"));
      }
      break;
    case DialogType::Channel:
      return promise.set_error(Status::Error(400, "Bulk message deletion is unsupported in supergroup chats"));
    case DialogType::SecretChat:
      return promise.set_error(Status::Error(400, "Bulk message deletion is unsupported in secret chats"));
    case DialogType::None:
    default:
      UNREACHABLE();
      break;
  }

  // TODO delete in database by dates

  auto message_ids = d->ordered_messages.find_messages_by_date(min_date, max_date, get_get_message_date(d));

  delete_dialog_messages(d, message_ids, false, DELETE_MESSAGE_USER_REQUEST_SOURCE);

  delete_dialog_messages_by_date_on_server(dialog_id, min_date, max_date, revoke, 0, std::move(promise));
}

class MessagesManager::DeleteDialogMessagesByDateOnServerLogEvent {
 public:
  DialogId dialog_id_;
  int32 min_date_;
  int32 max_date_;
  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(min_date_, storer);
    td::store(max_date_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(revoke_);
    END_PARSE_FLAGS();
    td::parse(dialog_id_, parser);
    td::parse(min_date_, parser);
    td::parse(max_date_, parser);
  }
};

uint64 MessagesManager::save_delete_dialog_messages_by_date_on_server_log_event(DialogId dialog_id, int32 min_date,
                                                                                int32 max_date, bool revoke) {
  DeleteDialogMessagesByDateOnServerLogEvent log_event{dialog_id, min_date, max_date, revoke};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteDialogMessagesByDateOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::delete_dialog_messages_by_date_on_server(DialogId dialog_id, int32 min_date, int32 max_date,
                                                               bool revoke, uint64 log_event_id,
                                                               Promise<Unit> &&promise) {
  if (log_event_id == 0 && G()->use_chat_info_database()) {
    log_event_id = save_delete_dialog_messages_by_date_on_server_log_event(dialog_id, min_date, max_date, revoke);
  }

  AffectedHistoryQuery query = [td = td_, min_date, max_date, revoke](DialogId dialog_id,
                                                                      Promise<AffectedHistory> &&query_promise) {
    td->create_handler<DeleteMessagesByDateQuery>(std::move(query_promise))
        ->send(dialog_id, min_date, max_date, revoke);
  };
  run_affected_history_query_until_complete(dialog_id, std::move(query), true,
                                            get_erase_log_event_promise(log_event_id, std::move(promise)));
}

int32 MessagesManager::get_unload_dialog_delay() const {
  constexpr int32 DIALOG_UNLOAD_DELAY = 60;        // seconds
  constexpr int32 DIALOG_UNLOAD_BOT_DELAY = 1800;  // seconds

  CHECK(is_message_unload_enabled());
  auto default_unload_delay = td_->auth_manager_->is_bot() ? DIALOG_UNLOAD_BOT_DELAY : DIALOG_UNLOAD_DELAY;
  return narrow_cast<int32>(td_->option_manager_->get_option_integer("message_unload_delay", default_unload_delay));
}

double MessagesManager::get_next_unload_dialog_delay(Dialog *d) const {
  if (d->unload_dialog_delay_seed == 0) {
    d->unload_dialog_delay_seed = Random::fast(1, 1000000000);
  }
  auto delay = get_unload_dialog_delay() / 4;
  return delay + delay * 1e-9 * d->unload_dialog_delay_seed;
}

void MessagesManager::unload_dialog(DialogId dialog_id, int32 delay) {
  if (G()->close_flag()) {
    return;
  }
  if (delay < 0) {
    delay = get_unload_dialog_delay() - 2;
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  if (!d->has_unload_timeout) {
    LOG(INFO) << "Don't need to unload " << dialog_id;
    // possible right after the dialog was opened
    return;
  }

  if (!is_message_unload_enabled()) {
    // just in case
    LOG(INFO) << "Message unload is disabled in " << dialog_id;
    d->has_unload_timeout = false;
    return;
  }

  bool has_left_to_unload_messages = false;
  auto to_unload_message_ids = find_unloadable_messages(d, G()->unix_time() - delay, has_left_to_unload_messages);

  vector<int64> unloaded_message_ids;
  vector<unique_ptr<Message>> unloaded_messages;
  for (auto message_id : to_unload_message_ids) {
    auto message = unload_message(d, message_id);
    CHECK(message != nullptr);
    if (message->is_update_sent) {
      unloaded_message_ids.push_back(message->message_id.get());
    }
    unloaded_messages.push_back(std::move(message));
  }
  if (unloaded_messages.size() >= MIN_DELETED_ASYNCHRONOUSLY_MESSAGES) {
    Scheduler::instance()->destroy_on_scheduler(G()->get_gc_scheduler_id(), unloaded_messages);
  }

  if (!to_unload_message_ids.empty() && !G()->use_message_database() && !d->is_empty) {
    d->have_full_history = false;
    d->have_full_history_source = 0;
  }

  if (!unloaded_message_ids.empty()) {
    send_closure_later(
        G()->td(), &Td::send_update,
        td_api::make_object<td_api::updateDeleteMessages>(get_chat_id_object(dialog_id, "updateDeleteMessages"),
                                                          std::move(unloaded_message_ids), false, true));
  }

  if (has_left_to_unload_messages) {
    LOG(DEBUG) << "Need to unload more messages in " << dialog_id;
    pending_unload_dialog_timeout_.add_timeout_in(
        d->dialog_id.get(),
        to_unload_message_ids.size() >= MAX_UNLOADED_MESSAGES ? 1.0 : get_next_unload_dialog_delay(d));
  } else {
    d->has_unload_timeout = false;
  }
}

void MessagesManager::clear_dialog_message_list(Dialog *d, bool remove_from_dialog_list, int32 last_message_date) {
  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, max_message_id, -1, "delete_all_dialog_messages 1");
    }
    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 2");
    }
  }

  if (d->unread_mention_count > 0) {
    set_dialog_unread_mention_count(d, 0);
    send_update_chat_unread_mention_count(d);
  }
  if (d->unread_reaction_count > 0) {
    set_dialog_unread_reaction_count(d, 0);
    send_update_chat_unread_reaction_count(d, "delete_all_dialog_messages");
  }

  bool has_last_message_id = d->last_message_id != MessageId();
  MessageId last_clear_history_message_id;
  if (!remove_from_dialog_list) {
    if (has_last_message_id) {
      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;
    }
  }

  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 4");
  set_dialog_last_database_message_id(d, MessageId(), "delete_all_dialog_messages 5");
  set_dialog_last_clear_history_date(d, last_message_date, last_clear_history_message_id,
                                     "delete_all_dialog_messages 6");
  d->last_read_all_mentions_message_id = MessageId();  // it is not needed anymore
  std::fill(d->message_count_by_index.begin(), d->message_count_by_index.end(), 0);

  if (has_last_message_id) {
    set_dialog_last_message_id(d, MessageId(), "delete_all_dialog_messages 7");
    send_update_chat_last_message(d, "delete_all_dialog_messages 8");
  }
  if (remove_from_dialog_list) {
    set_dialog_order(d, DEFAULT_ORDER, true, false, "delete_all_dialog_messages 9");
  } else {
    update_dialog_pos(d, "delete_all_dialog_messages 10");
  }
}

void MessagesManager::delete_all_dialog_messages(Dialog *d, bool remove_from_dialog_list, bool is_permanently_deleted) {
  CHECK(d != nullptr);
  LOG(INFO) << "Delete all messages in " << d->dialog_id
            << " with remove_from_dialog_list = " << remove_from_dialog_list
            << " and is_permanently_deleted = " << is_permanently_deleted;
  if (!td_->auth_manager_->is_bot()) {
    int32 last_message_date = 0;
    if (!remove_from_dialog_list && d->last_message_id.is_valid()) {
      auto m = get_message(d, d->last_message_id);
      CHECK(m != nullptr);
      last_message_date = m->date;
    }
    clear_dialog_message_list(d, remove_from_dialog_list, last_message_date);
  }

  vector<int64> deleted_message_ids;
  d->messages.foreach([&](const MessageId &message_id, unique_ptr<Message> &message) {
    CHECK(message_id == message->message_id);
    Message *m = message.get();

    static_cast<ListNode *>(m)->remove();

    LOG(INFO) << "Delete " << message_id;
    deleted_message_ids.push_back(message_id.get());

    delete_active_live_location(d->dialog_id, m);
    remove_message_file_sources(d->dialog_id, m);

    on_message_deleted(d, m, is_permanently_deleted, "do_delete_all_dialog_messages");

    if (is_permanently_deleted) {
      d->deleted_message_ids.insert(m->message_id);
    }
  });
  Scheduler::instance()->destroy_on_scheduler(G()->get_gc_scheduler_id(), d->messages, d->ordered_messages);

  delete_all_dialog_messages_from_database(d, MessageId::max(), "delete_all_dialog_messages 3");

  if (d->notification_info != nullptr) {
    // they aren't needed anymore
    delete_all_dialog_notifications(d, MessageId::max(), "delete_all_dialog_messages 4");

    d->notification_info->message_notification_group_.drop_max_removed_notification_id();
    d->notification_info->mention_notification_group_.drop_max_removed_notification_id();
    d->notification_info->notification_id_to_message_id_.clear();
  }

  on_dialog_updated(d->dialog_id, "delete_all_dialog_messages 11");

  send_update_delete_messages(d->dialog_id, std::move(deleted_message_ids), is_permanently_deleted);
}

void MessagesManager::on_dialog_deleted(DialogId dialog_id, Promise<Unit> &&promise) {
  LOG(INFO) << "Delete " << dialog_id;
  Dialog *d = get_dialog_force(dialog_id, "on_dialog_deleted");
  if (d == nullptr) {
    return promise.set_value(Unit());
  }

  delete_all_dialog_messages(d, true, false);
  if (dialog_id.get_type() != DialogType::SecretChat) {
    d->have_full_history = false;
    d->have_full_history_source = 0;
    d->is_empty = false;
    d->need_restore_reply_markup = true;
    on_dialog_updated(dialog_id, "on_dialog_deleted");
  }
  if (!td_->auth_manager_->is_bot()) {
    recently_found_dialogs_.remove_dialog(dialog_id);
    recently_opened_dialogs_.remove_dialog(dialog_id);
  }
  if (dialog_id.get_type() == DialogType::Channel) {
    G()->td_db()->get_binlog_pmc()->erase(get_channel_pts_key(dialog_id));
  }

  close_dialog(d);

  td_->forum_topic_manager_->delete_all_dialog_topics(dialog_id);

  promise.set_value(Unit());
}

void MessagesManager::on_update_dialog_group_call_rights(DialogId dialog_id) {
  auto d = get_dialog(dialog_id);
  if (d == nullptr) {
    // nothing to do
    return;
  }

  if (d->active_group_call_id.is_valid()) {
    td_->group_call_manager_->on_update_group_call_rights(d->active_group_call_id);
  }
}

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

  TRY_STATUS_PROMISE(promise, can_use_top_thread_message_id(d, top_thread_message_id, MessageInputReplyTo()));

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Chat is not accessible"));
  }

  if (top_thread_message_id.is_valid()) {
    LOG(INFO) << "Receive readAllChatMentions request in thread of " << top_thread_message_id << " in " << dialog_id;
    AffectedHistoryQuery query = [td = td_, top_thread_message_id](DialogId dialog_id,
                                                                   Promise<AffectedHistory> &&query_promise) {
      td->create_handler<ReadMentionsQuery>(std::move(query_promise))->send(dialog_id, top_thread_message_id);
    };
    run_affected_history_query_until_complete(dialog_id, std::move(query), true, std::move(promise));
    return;
  } else {
    LOG(INFO) << "Receive readAllChatMentions request in " << dialog_id << " with " << d->unread_mention_count
              << " unread mentions";
  }
  if (dialog_id.get_type() == DialogType::SecretChat) {
    CHECK(d->unread_mention_count == 0);
    return promise.set_value(Unit());
  }

  if (d->last_new_message_id > d->last_read_all_mentions_message_id) {
    d->last_read_all_mentions_message_id = d->last_new_message_id;
    on_dialog_updated(dialog_id, "read_all_dialog_mentions");
  }

  auto message_ids = find_dialog_messages(d, [](const Message *m) { return m->contains_unread_mention; });

  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);
    CHECK(m->message_id.is_valid());
    remove_message_notification_id(d, m, true, false);  // must be called before contains_unread_mention is updated
    m->contains_unread_mention = false;

    send_closure(G()->td(), &Td::send_update,
                 td_api::make_object<td_api::updateMessageMentionRead>(
                     get_chat_id_object(dialog_id, "updateMessageMentionRead"), m->message_id.get(), 0));
    is_update_sent = true;
    on_message_changed(d, m, true, "read_all_dialog_mentions");
  }

  if (d->unread_mention_count != 0) {
    set_dialog_unread_mention_count(d, 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_dialog_mentions");
    }
  }
  remove_message_dialog_notifications(d, MessageId::max(), true, "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_log_event(DialogId dialog_id) {
  ReadAllDialogMentionsOnServerLogEvent log_event{dialog_id};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ReadAllDialogMentionsOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::read_all_dialog_mentions_on_server(DialogId dialog_id, uint64 log_event_id,
                                                         Promise<Unit> &&promise) {
  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_read_all_dialog_mentions_on_server_log_event(dialog_id);
  }

  AffectedHistoryQuery query = [td = td_](DialogId dialog_id, Promise<AffectedHistory> &&query_promise) {
    td->create_handler<ReadMentionsQuery>(std::move(query_promise))->send(dialog_id, MessageId());
  };
  run_affected_history_query_until_complete(dialog_id, std::move(query), false,
                                            get_erase_log_event_promise(log_event_id, std::move(promise)));
}

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

  TRY_STATUS_PROMISE(promise, can_use_top_thread_message_id(d, top_thread_message_id, MessageInputReplyTo()));

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Chat is not accessible"));
  }

  if (top_thread_message_id.is_valid()) {
    LOG(INFO) << "Receive readAllChatReactions request in thread of " << top_thread_message_id << " in " << dialog_id;
    AffectedHistoryQuery query = [td = td_, top_thread_message_id](DialogId dialog_id,
                                                                   Promise<AffectedHistory> &&query_promise) {
      td->create_handler<ReadReactionsQuery>(std::move(query_promise))->send(dialog_id, top_thread_message_id);
    };
    run_affected_history_query_until_complete(dialog_id, std::move(query), true, std::move(promise));
    return;
  } else {
    LOG(INFO) << "Receive readAllChatReactions request in " << dialog_id << " with " << d->unread_reaction_count
              << " unread reactions";
  }

  if (dialog_id.get_type() == DialogType::SecretChat) {
    CHECK(d->unread_reaction_count == 0);
    return promise.set_value(Unit());
  }

  auto message_ids = find_dialog_messages(
      d, [this, dialog_id](const Message *m) { return has_unread_message_reactions(dialog_id, m); });

  LOG(INFO) << "Found " << message_ids.size() << " messages with unread reactions in memory";
  bool is_update_sent = false;
  for (auto message_id : message_ids) {
    auto m = get_message(d, message_id);
    CHECK(m != nullptr);
    CHECK(has_unread_message_reactions(dialog_id, m));
    CHECK(m->message_id == message_id);
    CHECK(m->message_id.is_valid());
    // remove_message_notification_id(d, m, true, false);  // must be called before unread_reactions are cleared
    m->reactions->unread_reactions_.clear();

    send_update_message_unread_reactions(dialog_id, m, 0);
    is_update_sent = true;
    on_message_changed(d, m, true, "read_all_dialog_reactions");
  }

  if (d->unread_reaction_count != 0) {
    set_dialog_unread_reaction_count(d, 0);
    if (!is_update_sent) {
      send_update_chat_unread_reaction_count(d, "read_all_dialog_reactions");
    } else {
      LOG(INFO) << "Update unread reaction message count in " << dialog_id << " to " << d->unread_reaction_count;
      on_dialog_updated(dialog_id, "read_all_dialog_reactions");
    }
  }
  // remove_message_dialog_notifications(d, MessageId::max(), true, "read_all_dialog_reactions");

  read_all_dialog_reactions_on_server(dialog_id, 0, std::move(promise));
}

class MessagesManager::ReadAllDialogReactionsOnServerLogEvent {
 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_reactions_on_server_log_event(DialogId dialog_id) {
  ReadAllDialogReactionsOnServerLogEvent log_event{dialog_id};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ReadAllDialogReactionsOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::read_all_dialog_reactions_on_server(DialogId dialog_id, uint64 log_event_id,
                                                          Promise<Unit> &&promise) {
  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_read_all_dialog_reactions_on_server_log_event(dialog_id);
  }

  AffectedHistoryQuery query = [td = td_](DialogId dialog_id, Promise<AffectedHistory> &&query_promise) {
    td->create_handler<ReadReactionsQuery>(std::move(query_promise))->send(dialog_id, MessageId());
  };
  run_affected_history_query_until_complete(dialog_id, std::move(query), false,
                                            get_erase_log_event_promise(log_event_id, std::move(promise)));
}

void MessagesManager::read_message_content_from_updates(MessageId message_id, int32 read_date) {
  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_date, "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;
  }
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  Message *m = get_message_force(d, message_id, "read_channel_message_content_from_updates");
  if (m != nullptr) {
    read_message_content(d, m, false, 0, "read_channel_message_content_from_updates");
  } else {
    if (!td_->dialog_manager_->have_input_peer(d->dialog_id, AccessRights::Read)) {
      LOG(INFO) << "Ignore updateChannelReadMessagesContents in inaccessible " << d->dialog_id;
      if (d->unread_mention_count != 0) {
        set_dialog_unread_mention_count(d, 0);
      }
      return;
    }
    if (message_id > d->last_new_message_id && d->last_new_message_id.is_valid()) {
      get_channel_difference(d->dialog_id, d->pts, 0, message_id, true, "read_channel_message_content_from_updates");
    } else {
      // there is no message, so the update can be ignored
      if (d->unread_mention_count > 0) {
        // but if the chat has unread mentions, then number of unread mentions could have been changed
        repair_dialog_unread_mention_count(d, "read_channel_message_content_from_updates");
      }
    }
  }
}

bool MessagesManager::read_message_content(Dialog *d, Message *m, bool is_local_read, int32 read_date,
                                           const char *source) {
  LOG_CHECK(m != nullptr) << source;
  CHECK(!m->message_id.is_scheduled());
  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());
  if (ttl_on_open(d, m, Time::now(), is_local_read, read_date)) {
    is_content_read = true;
  }

  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,
                   td_api::make_object<td_api::updateMessageContentOpened>(
                       get_chat_id_object(d->dialog_id, "updateMessageContentOpened"), m->message_id.get()));
    }
    return true;
  }
  return false;
}

bool MessagesManager::has_incoming_notification(DialogId dialog_id, const Message *m) const {
  CHECK(m != nullptr);
  if (m->is_from_scheduled) {
    return true;
  }
  return !m->message_id.is_scheduled() && !m->is_outgoing && dialog_id != td_->dialog_manager_->get_my_dialog_id();
}

int32 MessagesManager::calc_new_unread_count_from_last_unread(Dialog *d, MessageId max_message_id,
                                                              MessageType type) const {
  CHECK(!max_message_id.is_scheduled());
  auto it = d->ordered_messages.get_const_iterator(max_message_id);
  if (*it == nullptr || (*it)->get_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)->get_message_id() > d->last_read_inbox_message_id) {
    auto message_id = (*it)->get_message_id();
    if (message_id.get_type() == type && has_incoming_notification(d->dialog_id, get_message(d, message_id))) {
      unread_count--;
    }
    --it;
  }
  if (*it == nullptr || (*it)->get_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) const {
  CHECK(!max_message_id.is_scheduled());
  int32 unread_count = 0;
  auto it = d->ordered_messages.get_const_iterator(MessageId::max());
  while (*it != nullptr && (*it)->get_message_id() > max_message_id) {
    auto message_id = (*it)->get_message_id();
    if (message_id.get_type() == type && has_incoming_notification(d->dialog_id, get_message(d, message_id))) {
      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

    if (need_unread_counter(d->order)) {
      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) const {
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(!max_message_id.is_scheduled());
  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, const char *source) {
  if (td_->auth_manager_->is_bot() || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return;
  }
  if (pending_read_history_timeout_.has_timeout(dialog_id.get())) {
    return;  // postpone until read history request is sent
  }

  LOG(INFO) << "Repair server unread count in " << dialog_id << " from " << unread_count << " from " << source;
  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, "repair_server_unread_count");
                           }))
      .release();
}

void MessagesManager::repair_channel_server_unread_count(Dialog *d) {
  CHECK(d != nullptr);
  CHECK(d->dialog_id.get_type() == DialogType::Channel);

  if (td_->auth_manager_->is_bot()) {
    return;
  }
  if (d->last_read_inbox_message_id >= d->last_new_message_id) {
    // all messages are already read
    return;
  }
  if (!need_unread_counter(d->order)) {
    // there are no unread counters in left channels
    return;
  }
  if (!d->need_repair_channel_server_unread_count) {
    d->need_repair_channel_server_unread_count = true;
    on_dialog_updated(d->dialog_id, "repair_channel_server_unread_count");
  }

  LOG(INFO) << "Reload ChannelFull for " << d->dialog_id << " to repair unread message counts";
  td_->dialog_manager_->get_dialog_info_full(d->dialog_id, Auto(), "repair_channel_server_unread_count");
}

void MessagesManager::repair_dialog_unread_reaction_count(Dialog *d, Promise<Unit> &&promise, const char *source) {
  CHECK(d != nullptr);

  if (td_->auth_manager_->is_bot()) {
    return;
  }
  if (!d->need_repair_unread_reaction_count) {
    d->need_repair_unread_reaction_count = true;
    on_dialog_updated(d->dialog_id, "repair_dialog_unread_reaction_count");
  }

  send_get_dialog_query(d->dialog_id, std::move(promise), 0, source);
}

void MessagesManager::repair_dialog_unread_mention_count(Dialog *d, const char *source) {
  CHECK(d != nullptr);

  if (td_->auth_manager_->is_bot()) {
    return;
  }
  if (!d->need_repair_unread_mention_count) {
    d->need_repair_unread_mention_count = true;
    on_dialog_updated(d->dialog_id, "repair_dialog_unread_mention_count");
  }

  send_get_dialog_query(d->dialog_id, Promise<Unit>(), 0, source);
}

void MessagesManager::read_history_inbox(DialogId dialog_id, MessageId max_message_id, int32 unread_count,
                                         const char *source) {
  CHECK(!max_message_id.is_scheduled());

  if (td_->auth_manager_->is_bot()) {
    return;
  }

  Dialog *d = get_dialog_force(dialog_id, "read_history_inbox");
  if (d != nullptr) {
    read_history_inbox(d, max_message_id, unread_count, source);
  } else {
    LOG(INFO) << "Receive read inbox about unknown " << dialog_id << " from " << source;
  }
}

void MessagesManager::read_history_inbox(Dialog *d, MessageId max_message_id, int32 unread_count, const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto dialog_id = d->dialog_id;
  if (d->need_repair_channel_server_unread_count) {
    d->need_repair_channel_server_unread_count = false;
    on_dialog_updated(dialog_id, "read_history_inbox");
  }

  // there can be updateReadHistoryInbox up to message 0, if messages where read and then all messages where deleted
  if (!max_message_id.is_valid() && max_message_id != MessageId()) {
    LOG(ERROR) << "Receive read inbox update in " << dialog_id << " up to " << max_message_id << " from " << source;
    return;
  }
  if (d->is_last_read_inbox_message_id_inited && max_message_id <= d->last_read_inbox_message_id) {
    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;
    if (max_message_id == d->last_read_inbox_message_id && unread_count >= 0 &&
        unread_count != d->server_unread_count) {
      set_dialog_last_read_inbox_message_id(d, MessageId::min(), unread_count, d->local_unread_count, true, source);
    }
    return;
  }

  if (max_message_id != MessageId() && max_message_id.is_yet_unsent()) {
    LOG(ERROR) << "Tried to update last read inbox message in " << dialog_id << " with " << max_message_id << " from "
               << source;
    return;
  }

  if (max_message_id != MessageId() && unread_count > 0 && max_message_id >= d->last_new_message_id &&
      max_message_id >= d->last_message_id && max_message_id >= d->last_database_message_id) {
    if (d->last_new_message_id.is_valid()) {
      LOG(ERROR) << "Have unknown " << unread_count << " unread messages up to " << max_message_id << " in "
                 << dialog_id << " with last_new_message_id = " << d->last_new_message_id
                 << ", last_message_id = " << d->last_message_id
                 << ", last_database_message_id = " << d->last_database_message_id << ", and " << d->server_unread_count
                 << " unread messages up to " << d->last_read_inbox_message_id << " from " << source;
      unread_count = d->server_unread_count;
    } else {
      unread_count = 0;
    }
  }

  LOG_IF(INFO, d->last_new_message_id.is_valid() && max_message_id > d->last_new_message_id &&
                   (d->notification_info != nullptr &&
                    max_message_id > d->notification_info->max_push_notification_message_id_) &&
                   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";

  if (dialog_id.get_type() == DialogType::SecretChat) {
    ttl_read_history(d, false, max_message_id, d->last_read_inbox_message_id, Time::now());
  }

  if (max_message_id > d->last_new_message_id && dialog_id.get_type() == DialogType::Channel) {
    schedule_get_channel_difference(dialog_id, 0, max_message_id, 0.001, "read_history_inbox");
  }

  int32 server_unread_count = calc_new_unread_count(d, max_message_id, MessageType::Server, unread_count);
  int32 local_unread_count =
      d->local_unread_count == 0 ? 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 &&
        td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read) && need_unread_counter(d->order)) {
      d->need_repair_server_unread_count = true;
      on_dialog_updated(dialog_id, "read_history_inbox");
      repair_server_unread_count(dialog_id, server_unread_count, "read_history_inbox");
    }
  }
  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 && max_message_id != MessageId()) {
    set_dialog_is_marked_as_unread(d, false);
  }
}

void MessagesManager::read_history_outbox(DialogId dialog_id, MessageId max_message_id, int32 read_date) {
  CHECK(!max_message_id.is_scheduled());

  if (td_->auth_manager_->is_bot()) {
    return;
  }

  Dialog *d = get_dialog_force(dialog_id, "read_history_outbox");
  if (d != nullptr) {
    read_history_outbox(d, max_message_id, read_date);
  } else {
    LOG(INFO) << "Receive read outbox update about unknown " << dialog_id;
  }
}

void MessagesManager::read_history_outbox(Dialog *d, MessageId max_message_id, int32 read_date) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto dialog_id = d->dialog_id;
  if (!max_message_id.is_valid()) {
    LOG(ERROR) << "Receive read outbox update in " << dialog_id << " with " << max_message_id;
    return;
  }
  if (max_message_id <= d->last_read_outbox_message_id) {
    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) << "Tried to update last read outbox message with " << max_message_id << " in " << dialog_id;
    return;
  }

  // it is impossible for just sent outgoing messages because updates are ordered by PTS
  if (d->last_new_message_id.is_valid() && max_message_id > d->last_new_message_id &&
      dialog_id.get_type() != DialogType::Channel) {
    LOG(INFO) << "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";
  }

  if (dialog_id.get_type() == DialogType::SecretChat) {
    double server_time = G()->server_time();
    double read_time = Time::now();
    if (read_date <= 0) {
      LOG(ERROR) << "Receive wrong read date " << read_date << " in " << dialog_id;
    } else if (read_date < server_time) {
      read_time -= (server_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);
}

bool MessagesManager::need_unread_counter(int64 dialog_order) {
  return dialog_order != DEFAULT_ORDER;
}

int32 MessagesManager::get_dialog_total_count(const DialogList &list) const {
  int32 sponsored_dialog_count = 0;
  if (sponsored_dialog_id_.is_valid() && list.dialog_list_id == DialogListId(FolderId::main())) {
    auto d = get_dialog(sponsored_dialog_id_);
    CHECK(d != nullptr);
    if (is_dialog_sponsored(d)) {
      sponsored_dialog_count = 1;
    }
  }
  if (list.server_dialog_total_count_ != -1 && list.secret_chat_total_count_ != -1) {
    return std::max(list.server_dialog_total_count_ + list.secret_chat_total_count_,
                    list.in_memory_dialog_total_count_) +
           sponsored_dialog_count;
  }
  if (list.list_last_dialog_date_ == MAX_DIALOG_DATE) {
    return list.in_memory_dialog_total_count_ + sponsored_dialog_count;
  }
  return list.in_memory_dialog_total_count_ + sponsored_dialog_count + 1;
}

void MessagesManager::repair_server_dialog_total_count(DialogListId dialog_list_id) {
  if (G()->close_flag()) {
    return;
  }
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  if (!dialog_list_id.is_folder()) {
    // can repair total count only in folders
    return;
  }

  LOG(INFO) << "Repair total chat count in " << dialog_list_id;
  td_->create_handler<GetDialogListQuery>(Promise<Unit>())
      ->send(dialog_list_id.get_folder_id(), 2147483647, ServerMessageId(), DialogId(), 1);
}

void MessagesManager::repair_secret_chat_total_count(DialogListId dialog_list_id) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  if (G()->use_message_database() && dialog_list_id.is_folder()) {
    // race-prone
    G()->td_db()->get_dialog_db_async()->get_secret_chat_count(
        dialog_list_id.get_folder_id(),
        PromiseCreator::lambda([actor_id = actor_id(this), dialog_list_id](Result<int32> result) {
          if (result.is_error()) {
            return;
          }
          send_closure(actor_id, &MessagesManager::on_get_secret_chat_total_count, dialog_list_id, result.move_as_ok());
        }));
  } else {
    int32 total_count = 0;
    auto *list = get_dialog_list(dialog_list_id);
    CHECK(list != nullptr);
    for (auto &folder_id : get_dialog_list_folder_ids(*list)) {
      const auto *folder_list = get_dialog_list(DialogListId(folder_id));
      CHECK(folder_list != nullptr);
      if (folder_list->need_unread_count_recalc_) {
        // can't repair total secret chat count yet
        return;
      }

      const auto *folder = get_dialog_folder(folder_id);
      CHECK(folder != nullptr);
      for (const auto &dialog_date : folder->ordered_dialogs_) {
        auto dialog_id = dialog_date.get_dialog_id();
        if (dialog_id.get_type() == DialogType::SecretChat && dialog_date.get_order() != DEFAULT_ORDER) {
          total_count++;
        }
      }
    }
    on_get_secret_chat_total_count(dialog_list_id, total_count);
  }
}

void MessagesManager::on_get_secret_chat_total_count(DialogListId dialog_list_id, int32 total_count) {
  if (G()->close_flag()) {
    return;
  }

  CHECK(!td_->auth_manager_->is_bot());
  auto *list = get_dialog_list(dialog_list_id);
  if (list == nullptr) {
    // just in case
    return;
  }
  CHECK(total_count >= 0);
  if (list->secret_chat_total_count_ != total_count) {
    auto old_dialog_total_count = get_dialog_total_count(*list);
    list->secret_chat_total_count_ = total_count;
    if (list->is_dialog_unread_count_inited_) {
      if (old_dialog_total_count != get_dialog_total_count(*list)) {
        send_update_unread_chat_count(*list, DialogId(), true, "on_get_secret_chat_total_count");
      } else {
        save_unread_chat_count(*list);
      }
    }
  }
}

void MessagesManager::recalc_unread_count(DialogListId dialog_list_id, int32 old_dialog_total_count, bool force) {
  if (G()->close_flag() || td_->auth_manager_->is_bot() || !G()->use_message_database()) {
    return;
  }

  auto *list_ptr = get_dialog_list(dialog_list_id);
  CHECK(list_ptr != nullptr);
  auto &list = *list_ptr;
  if (!list.need_unread_count_recalc_ && !force) {
    return;
  }
  LOG(INFO) << "Recalculate unread counts in " << dialog_list_id;
  list.need_unread_count_recalc_ = false;
  list.is_message_unread_count_inited_ = true;
  list.is_dialog_unread_count_inited_ = true;

  int32 message_total_count = 0;
  int32 message_muted_count = 0;
  int32 dialog_total_count = 0;
  int32 dialog_muted_count = 0;
  int32 dialog_marked_count = 0;
  int32 dialog_muted_marked_count = 0;
  int32 server_dialog_total_count = 0;
  int32 secret_chat_total_count = 0;
  for (auto folder_id : get_dialog_list_folder_ids(list)) {
    const auto &folder = *get_dialog_folder(folder_id);
    for (const auto &dialog_date : folder.ordered_dialogs_) {
      if (dialog_date.get_order() == DEFAULT_ORDER) {
        break;
      }

      auto dialog_id = dialog_date.get_dialog_id();
      Dialog *d = get_dialog(dialog_id);
      CHECK(d != nullptr);
      if (!is_dialog_in_list(d, dialog_list_id)) {
        continue;
      }

      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)) {
        message_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)) {
          message_muted_count += unread_count;
          dialog_muted_count++;
          if (unread_count == 0 && d->is_marked_as_unread) {
            dialog_muted_marked_count++;
          }
        }
      }
      if (d->order != DEFAULT_ORDER) {  // must not count sponsored dialog, which is added independently
        if (dialog_id.get_type() == DialogType::SecretChat) {
          secret_chat_total_count++;
        } else {
          server_dialog_total_count++;
        }
      }
    }
  }

  if (list.unread_message_total_count_ != message_total_count ||
      list.unread_message_muted_count_ != message_muted_count) {
    list.unread_message_total_count_ = message_total_count;
    list.unread_message_muted_count_ = message_muted_count;
    send_update_unread_message_count(list, DialogId(), true, "recalc_unread_count");
  }

  if (old_dialog_total_count == -1) {
    old_dialog_total_count = get_dialog_total_count(list);
  }
  bool need_save = false;
  if (list.list_last_dialog_date_ == MAX_DIALOG_DATE) {
    if (server_dialog_total_count != list.server_dialog_total_count_ ||
        secret_chat_total_count != list.secret_chat_total_count_) {
      list.server_dialog_total_count_ = server_dialog_total_count;
      list.secret_chat_total_count_ = secret_chat_total_count;
      need_save = true;
    }
  } else {
    if (list.server_dialog_total_count_ == -1) {
      // recalc_unread_count is called only after getDialogs request; it is unneeded to call getDialogs again
      repair_server_dialog_total_count(dialog_list_id);
    }

    if (list.secret_chat_total_count_ == -1) {
      repair_secret_chat_total_count(dialog_list_id);
    }
  }
  if (list.unread_dialog_total_count_ != dialog_total_count || list.unread_dialog_muted_count_ != dialog_muted_count ||
      list.unread_dialog_marked_count_ != dialog_marked_count ||
      list.unread_dialog_muted_marked_count_ != dialog_muted_marked_count ||
      old_dialog_total_count != get_dialog_total_count(list)) {
    list.unread_dialog_total_count_ = dialog_total_count;
    list.unread_dialog_muted_count_ = dialog_muted_count;
    list.unread_dialog_marked_count_ = dialog_marked_count;
    list.unread_dialog_muted_marked_count_ = dialog_muted_marked_count;
    send_update_unread_chat_count(list, DialogId(), true, "recalc_unread_count");
  } else if (need_save) {
    save_unread_chat_count(list);
  }
}

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) {
  CHECK(!message_id.is_scheduled());

  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;

  if (need_unread_counter(d->order)) {
    const int32 new_unread_count = d->server_unread_count + d->local_unread_count;
    for (auto &list : get_dialog_lists(d)) {
      int32 delta = new_unread_count - old_unread_count;
      if (delta != 0 && list.is_message_unread_count_inited_) {
        list.unread_message_total_count_ += delta;
        if (is_dialog_muted(d)) {
          list.unread_message_muted_count_ += delta;
        }
        send_update_unread_message_count(list, d->dialog_id, force_update, source);
      }
      delta = static_cast<int32>(new_unread_count != 0) - static_cast<int32>(old_unread_count != 0);
      if (delta != 0 && list.is_dialog_unread_count_inited_) {
        if (d->is_marked_as_unread) {
          list.unread_dialog_marked_count_ -= delta;
        } else {
          list.unread_dialog_total_count_ += delta;
        }
        if (is_dialog_muted(d)) {
          if (d->is_marked_as_unread) {
            list.unread_dialog_muted_marked_count_ -= delta;
          } else {
            list.unread_dialog_muted_count_ += delta;
          }
        }
        send_update_unread_chat_count(list, d->dialog_id, force_update, source);
      }
    }

    bool was_unread = old_unread_count != 0 || d->is_marked_as_unread;
    bool is_unread = new_unread_count != 0 || d->is_marked_as_unread;
    if (td_->dialog_filter_manager_->have_dialog_filters() && was_unread != is_unread) {
      update_dialog_lists(d, get_dialog_positions(d), true, false, "set_dialog_last_read_inbox_message_id");
    }
  }

  if (message_id != MessageId::min() && d->last_read_inbox_message_id.is_valid() &&
      (d->order != DEFAULT_ORDER || is_dialog_sponsored(d))) {
    VLOG(notifications) << "Remove some notifications in " << d->dialog_id
                        << " after updating last read inbox message to " << message_id
                        << " and unread message count to " << server_unread_count << " + " << local_unread_count
                        << " from " << source;
    if (d->notification_info != nullptr && d->notification_info->message_notification_group_.is_valid()) {
      auto total_count = get_dialog_pending_notification_count(d, false);
      if (total_count == 0) {
        set_dialog_last_notification(d->dialog_id, d->notification_info->message_notification_group_, 0,
                                     NotificationId(), source);
      }
      if (!d->notification_info->pending_new_message_notifications_.empty()) {
        for (auto &it : d->notification_info->pending_new_message_notifications_) {
          if (it.second <= message_id) {
            it.first = DialogId();
          }
        }
        flush_pending_new_message_notifications(d->dialog_id, false, DialogId(UserId(static_cast<int64>(1))));
      }
      total_count -= static_cast<int32>(d->notification_info->pending_new_message_notifications_.size());
      if (total_count < 0) {
        LOG(ERROR) << "Total message notification count is " << total_count << " in " << d->dialog_id
                   << " with old unread_count = " << old_unread_count << " and "
                   << d->notification_info->pending_new_message_notifications_
                   << " pending new message notifications after reading history up to " << message_id;
        total_count = 0;
      }
      send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification_group,
                         d->notification_info->message_notification_group_.get_group_id(), NotificationId(),
                         d->last_read_inbox_message_id, total_count, Slice(source) == Slice("view_messages"),
                         Promise<Unit>());
    }

    if (d->notification_info != nullptr && d->notification_info->mention_notification_group_.is_valid() &&
        d->notification_info->pinned_message_notification_message_id_.is_valid() &&
        d->notification_info->pinned_message_notification_message_id_ <= d->last_read_inbox_message_id) {
      // remove pinned message notification when it is read
      remove_dialog_pinned_message_notification(d, source);
    }
  }

  on_dialog_updated(d->dialog_id, source);
  send_update_chat_read_inbox(d, force_update, source);
}

void MessagesManager::set_dialog_last_read_outbox_message_id(Dialog *d, MessageId message_id) {
  CHECK(!message_id.is_scheduled());

  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) {
  CHECK(!max_unavailable_message_id.is_scheduled());

  Dialog *d = get_dialog_force(dialog_id, source);
  if (d != nullptr) {
    if (d->last_new_message_id.is_valid() && max_unavailable_message_id > d->last_new_message_id && from_update) {
      // possible if the last unavailable message has already been deleted
      LOG(INFO) << "Tried to set " << dialog_id << " max unavailable message to " << max_unavailable_message_id
                << " from " << source << ", but last new message 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) << "Tried to update " << dialog_id << " max unavailable message with " << max_unavailable_message_id
                 << " from " << source;
      return;
    }
    LOG(INFO) << "Set max unavailable message 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 > max_unavailable_message_id) {
      d->max_unavailable_message_id = max_unavailable_message_id;
      return;
    }

    d->max_unavailable_message_id = max_unavailable_message_id;

    auto message_ids = d->ordered_messages.find_older_messages(max_unavailable_message_id);

    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 <= max_unavailable_message_id);
      CHECK(m->message_id == message_id);
      auto message =
          delete_message(d, message_id, !from_update, &need_update_dialog_pos, "set_dialog_max_unavailable_message_id");
      CHECK(message.get() == m);
      deleted_message_ids.push_back(m->message_id.get());
    }

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

    if (d->server_unread_count + d->local_unread_count > 0) {
      read_history_inbox(d, max_unavailable_message_id, -1, "set_dialog_max_unavailable_message_id");
    }
  } else {
    LOG(INFO) << "Receive max unavailable message in unknown " << dialog_id << " from " << source;
  }
}

void MessagesManager::on_update_viewed_messages_timeout(DialogId dialog_id) {
  if (G()->close_flag()) {
    return;
  }

  LOG(INFO) << "Expired timeout for updating of recently viewed messages in " << dialog_id;
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (d->open_count == 0) {
    return;
  }

  auto it = dialog_viewed_messages_.find(dialog_id);
  if (it == dialog_viewed_messages_.end() || !td_->is_online()) {
    return;
  }

  auto &info = it->second;
  CHECK(info != nullptr);
  vector<MessageId> reaction_message_ids;
  vector<MessageId> views_message_ids;
  vector<MessageId> extended_media_message_ids;
  int32 newest_message_date = 0;
  for (auto &message_it : info->message_id_to_view_id) {
    Message *m = get_message_force(d, message_it.first, "on_update_viewed_messages_timeout");
    CHECK(m != nullptr);
    CHECK(m->message_id.is_valid());
    CHECK(m->message_id.is_server());
    if (need_poll_message_reactions(d, m)) {
      reaction_message_ids.push_back(m->message_id);
    }
    if (m->view_count > 0 && !m->has_get_message_views_query) {
      m->has_get_message_views_query = true;
      views_message_ids.push_back(m->message_id);
    }
    if (need_poll_message_content_extended_media(m->content.get()) && !m->has_get_extended_media_query) {
      m->has_get_extended_media_query = true;
      extended_media_message_ids.push_back(m->message_id);
    }
    if (m->date > newest_message_date) {
      newest_message_date = m->date;
    }
  }

  if (!reaction_message_ids.empty()) {
    queue_message_reactions_reload(dialog_id, reaction_message_ids);
  }
  if (!views_message_ids.empty()) {
    td_->create_handler<GetMessagesViewsQuery>()->send(dialog_id, std::move(views_message_ids), false);
  }
  if (!extended_media_message_ids.empty()) {
    td_->create_handler<GetExtendedMediaQuery>()->send(dialog_id, std::move(extended_media_message_ids));
  }

  int divisor = 5 - min(max(G()->unix_time() - newest_message_date, 0) / UPDATE_VIEWED_MESSAGES_PERIOD, 4);
  update_viewed_messages_timeout_.add_timeout_in(dialog_id.get(), UPDATE_VIEWED_MESSAGES_PERIOD / divisor);
}

void MessagesManager::on_send_update_chat_read_inbox_timeout(DialogId dialog_id) {
  if (G()->close_flag()) {
    return;
  }

  if (postponed_chat_read_inbox_updates_.erase(dialog_id) > 0) {
    send_update_chat_read_inbox(get_dialog(dialog_id), true, "on_send_update_chat_read_inbox_timeout");
  }
}

int32 MessagesManager::get_message_date(const tl_object_ptr<telegram_api::Message> &message_ptr) {
  switch (message_ptr->get_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;
  }
}

vector<UserId> MessagesManager::get_message_user_ids(const Message *m) const {
  vector<UserId> user_ids;
  if (m->sender_user_id.is_valid()) {
    user_ids.push_back(m->sender_user_id);
  }
  if (m->via_bot_user_id.is_valid()) {
    user_ids.push_back(m->via_bot_user_id);
  }
  if (m->forward_info != nullptr) {
    m->forward_info->add_min_user_ids(user_ids);
  }
  append(user_ids, get_message_content_min_user_ids(td_, m->content.get()));
  if (!m->replied_message_info.is_empty()) {
    append(user_ids, m->replied_message_info.get_min_user_ids(td_));
  }
  return user_ids;
}

vector<ChannelId> MessagesManager::get_message_channel_ids(const Message *m) const {
  vector<ChannelId> channel_ids;
  if (m->sender_dialog_id.is_valid() && m->sender_dialog_id.get_type() == DialogType::Channel) {
    channel_ids.push_back(m->sender_dialog_id.get_channel_id());
  }
  if (m->forward_info != nullptr) {
    m->forward_info->add_min_channel_ids(channel_ids);
  }
  append(channel_ids, get_message_content_min_channel_ids(td_, m->content.get()));
  if (!m->replied_message_info.is_empty()) {
    append(channel_ids, m->replied_message_info.get_min_channel_ids(td_));
  }
  return channel_ids;
}

void MessagesManager::ttl_read_history(Dialog *d, bool is_outgoing, MessageId from_message_id,
                                       MessageId till_message_id, double view_date) {
  CHECK(!from_message_id.is_scheduled());
  CHECK(!till_message_id.is_scheduled());

  // TODO: protect with log event
  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) {
  CHECK(dialog_id.get_type() == DialogType::SecretChat);
  CHECK(!from_message_id.is_scheduled());
  CHECK(!till_message_id.is_scheduled());

  auto *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  auto now = Time::now();
  for (auto it = d->ordered_messages.get_const_iterator(from_message_id);
       *it && (*it)->get_message_id() >= till_message_id; --it) {
    auto *m = get_message(d, (*it)->get_message_id());
    CHECK(m != nullptr);
    if (m->is_outgoing == is_outgoing) {
      ttl_on_view(d, m, view_date, now);
    }
  }
}

void MessagesManager::ttl_on_view(const Dialog *d, Message *m, double view_date, double now) {
  if (!m->ttl.is_empty() && m->ttl_expires_at == 0 && !m->message_id.is_scheduled() && !m->message_id.is_yet_unsent() &&
      !m->is_failed_to_send && !m->is_content_secret) {
    m->ttl_expires_at = m->ttl.get_input_ttl() + view_date;
    ttl_register_message(d->dialog_id, m, now);
    on_message_changed(d, m, true, "ttl_on_view");
  }
}

bool MessagesManager::ttl_on_open(Dialog *d, Message *m, double now, bool is_local_read, int32 read_date) {
  CHECK(!m->message_id.is_scheduled());
  if (!m->ttl.is_empty() && m->ttl_expires_at == 0) {
    int32 passed_after_read_time = 0;
    auto can_destroy_immediately = [&] {
      if (m->ttl.is_immediate()) {
        return true;
      }
      if (is_local_read) {
        return false;
      }
      if (read_date <= 0) {
        return d->dialog_id.get_type() != DialogType::SecretChat;
      }
      passed_after_read_time = max(G()->unix_time() - read_date, 0);
      if (m->ttl.get_input_ttl() <= passed_after_read_time) {
        return true;
      }
      return false;
    }();
    if (can_destroy_immediately) {
      on_message_ttl_expired(d, m);
    } else {
      m->ttl_expires_at = m->ttl.get_input_ttl() + now - passed_after_read_time;
      ttl_register_message(d->dialog_id, m, now);
    }
    return true;
  }
  return false;
}

void MessagesManager::ttl_register_message(DialogId dialog_id, const Message *m, double now) {
  CHECK(m != nullptr);
  CHECK(m->ttl_expires_at != 0);
  CHECK(!m->message_id.is_scheduled());

  auto it_flag = ttl_nodes_.emplace(dialog_id, m->message_id, false);
  CHECK(it_flag.second);
  auto it = it_flag.first;

  ttl_heap_.insert(m->ttl_expires_at, it->as_heap_node());
  ttl_update_timeout(now);
}

void MessagesManager::ttl_period_register_message(DialogId dialog_id, const Message *m, double server_time) {
  CHECK(m != nullptr);
  CHECK(m->ttl_period != 0);
  CHECK(!m->message_id.is_scheduled());

  auto it_flag = ttl_nodes_.emplace(dialog_id, m->message_id, true);
  CHECK(it_flag.second);
  auto it = it_flag.first;

  auto now = Time::now();
  ttl_heap_.insert(now + (m->date + m->ttl_period - server_time), it->as_heap_node());
  ttl_update_timeout(now);
}

void MessagesManager::ttl_unregister_message(DialogId dialog_id, const Message *m, const char *source) {
  if (m->ttl_expires_at == 0) {
    return;
  }
  CHECK(!m->message_id.is_scheduled());

  auto it = ttl_nodes_.find(TtlNode(dialog_id, m->message_id, false));

  // expect m->ttl.is_empty(), but m->ttl_expires_at > 0 from binlog
  CHECK(it != ttl_nodes_.end());

  auto *heap_node = it->as_heap_node();
  if (heap_node->in_heap()) {
    ttl_heap_.erase(heap_node);
  }
  ttl_nodes_.erase(it);
  ttl_update_timeout(Time::now());
}

void MessagesManager::ttl_period_unregister_message(DialogId dialog_id, const Message *m) {
  if (m->ttl_period == 0) {
    return;
  }
  CHECK(!m->message_id.is_scheduled());

  auto it = ttl_nodes_.find(TtlNode(dialog_id, m->message_id, true));

  CHECK(it != ttl_nodes_.end());

  auto *heap_node = it->as_heap_node();
  if (heap_node->in_heap()) {
    ttl_heap_.erase(heap_node);
  }
  ttl_nodes_.erase(it);
  ttl_update_timeout(Time::now());
}

void MessagesManager::ttl_loop(double now) {
  FlatHashMap<DialogId, std::vector<MessageId>, DialogIdHash> to_delete;
  while (!ttl_heap_.empty() && ttl_heap_.top_key() < now) {
    TtlNode *ttl_node = TtlNode::from_heap_node(ttl_heap_.pop());
    auto message_full_id = ttl_node->message_full_id_;
    auto dialog_id = message_full_id.get_dialog_id();
    CHECK(dialog_id.is_valid());
    if (dialog_id.get_type() == DialogType::SecretChat || ttl_node->by_ttl_period_) {
      to_delete[dialog_id].push_back(message_full_id.get_message_id());
    } else {
      auto d = get_dialog(dialog_id);
      CHECK(d != nullptr);
      auto m = get_message(d, message_full_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(it.first, it.second, false, "ttl_loop");
  }
  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 *m) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  CHECK(m->ttl.is_valid());
  CHECK(d->dialog_id.get_type() != DialogType::SecretChat);
  ttl_unregister_message(d->dialog_id, m, "on_message_ttl_expired");
  unregister_message_content(td_, m->content.get(), {d->dialog_id, m->message_id}, "on_message_ttl_expired");
  remove_message_file_sources(d->dialog_id, m);
  on_message_ttl_expired_impl(d, m, true);
  register_message_content(td_, m->content.get(), {d->dialog_id, m->message_id}, "on_message_ttl_expired");
  send_update_message_content(d, m, true, "on_message_ttl_expired");
  // the caller must call on_message_changed
}

void MessagesManager::on_message_ttl_expired_impl(Dialog *d, Message *m, bool is_message_in_dialog) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  CHECK(m->message_id.is_valid());
  CHECK(!m->message_id.is_yet_unsent());
  CHECK(m->ttl.is_valid());
  CHECK(d->dialog_id.get_type() != DialogType::SecretChat);
  delete_message_files(d->dialog_id, m);
  update_expired_message_content(m->content);
  m->ttl = {};
  m->ttl_expires_at = 0;
  if (m->reply_markup != nullptr) {
    if (m->reply_markup->type != ReplyMarkup::Type::InlineKeyboard) {
      if (d->reply_markup_message_id == m->message_id) {
        set_dialog_reply_markup(d, MessageId());
      }
      m->had_reply_markup = true;
    }
    m->reply_markup = nullptr;
  }
  remove_message_notification_id(d, m, true, true);
  update_message_contains_unread_mention(d, m, false, "on_message_ttl_expired_impl");
  remove_message_unread_reactions(d, m, "on_message_ttl_expired_impl");
  set_message_reply(d, m, MessageInputReplyTo(), is_message_in_dialog);
  m->noforwards = false;
  m->contains_mention = false;
  m->linked_top_thread_message_id = MessageId();
  m->is_content_secret = false;
  m->invert_media = false;
}

void MessagesManager::loop() {
  auto token = get_link_token();
  if (token == YieldType::TtlDb) {
    ttl_db_loop();
  } else {
    ttl_loop(Time::now());
  }
}

void MessagesManager::tear_down() {
  parent_.reset();

  LOG(DEBUG) << "Have " << dialogs_.calc_size() << " chats with " << added_message_count_ << " messages to free";
}

void MessagesManager::hangup() {
  postponed_channel_updates_.clear();

  if (!G()->use_message_database()) {
    while (!being_uploaded_files_.empty()) {
      auto it = being_uploaded_files_.begin();
      auto message_full_id = it->second.first;
      being_uploaded_files_.erase(it);
      if (message_full_id.get_message_id().is_yet_unsent()) {
        fail_send_message(message_full_id, Global::request_aborted_error());
      }
    }
    while (!being_uploaded_thumbnails_.empty()) {
      auto it = being_uploaded_thumbnails_.begin();
      auto message_full_id = it->second.message_full_id;
      being_uploaded_thumbnails_.erase(it);
      if (message_full_id.get_message_id().is_yet_unsent()) {
        fail_send_message(message_full_id, Global::request_aborted_error());
      }
    }
    while (!being_loaded_secret_thumbnails_.empty()) {
      auto it = being_loaded_secret_thumbnails_.begin();
      auto message_full_id = it->second.message_full_id;
      being_loaded_secret_thumbnails_.erase(it);
      if (message_full_id.get_message_id().is_yet_unsent()) {
        fail_send_message(message_full_id, Global::request_aborted_error());
      }
    }
    while (!yet_unsent_media_queues_.empty()) {
      auto it = yet_unsent_media_queues_.begin();
      auto queue = std::move(it->second);
      yet_unsent_media_queues_.erase(it);
      for (auto &promise_it : queue.queue_) {
        auto message_id = promise_it.first;
        if (message_id.is_yet_unsent()) {
          fail_send_message({queue.dialog_id_, message_id}, Global::request_aborted_error());
        }
      }
    }
    while (!being_sent_messages_.empty()) {
      on_send_message_fail(being_sent_messages_.begin()->first, Global::request_aborted_error());
    }
    while (!update_message_ids_.empty()) {
      auto it = update_message_ids_.begin();
      auto message_full_id = MessageFullId(it->first.get_dialog_id(), it->second);
      update_message_ids_.erase(it);

      // the message was sent successfully, but can't be added yet
      fail_send_message(message_full_id, Global::request_aborted_error());
    }
    while (!update_scheduled_message_ids_.empty()) {
      auto it = update_scheduled_message_ids_.begin();
      auto dialog_id = it->first;
      auto message_ids = std::move(it->second);
      update_scheduled_message_ids_.erase(it);
      for (auto &message_id_it : message_ids) {
        // the message was sent successfully, but can't be added yet
        fail_send_message({dialog_id, message_id_it.second}, Global::request_aborted_error());
      }
    }
  }

  fail_promises(load_active_live_location_messages_queries_, Global::request_aborted_error());
  auto fail_promise_map = [](auto &queries) {
    while (!queries.empty()) {
      auto it = queries.begin();
      auto promises = std::move(it->second);
      queries.erase(it);
      fail_promises(promises, Global::request_aborted_error());
    }
  };
  fail_promise_map(get_dialog_queries_);
  fail_promise_map(load_scheduled_messages_from_database_queries_);
  fail_promise_map(run_after_get_channel_difference_);
  fail_promise_map(search_public_dialogs_queries_);
  fail_promise_map(get_history_queries_);
  while (!pending_channel_on_get_dialogs_.empty()) {
    auto it = pending_channel_on_get_dialogs_.begin();
    auto promise = std::move(it->second.promise);
    pending_channel_on_get_dialogs_.erase(it);
    promise.set_error(Global::request_aborted_error());
  }
  while (!get_dialogs_tasks_.empty()) {
    auto it = get_dialogs_tasks_.begin();
    auto promise = std::move(it->second.promise);
    get_dialogs_tasks_.erase(it);
    promise.set_error(Global::request_aborted_error());
  }

  stop();
}

void MessagesManager::start_up() {
  init();
}

void MessagesManager::create_folders(int source) {
  LOG(INFO) << "Create folders";
  create_folders_source_ = source;
  dialog_folders_[FolderId::main()].folder_id = FolderId::main();
  dialog_folders_[FolderId::archive()].folder_id = FolderId::archive();

  add_dialog_list(DialogListId(FolderId::main()));
  add_dialog_list(DialogListId(FolderId::archive()));
  create_folders_source_ = source + 1;
}

void MessagesManager::init() {
  if (is_inited_) {
    return;
  }
  is_inited_ = true;

  td_->notification_settings_manager_->init();  // load scope notification settings
  td_->reaction_manager_->init();               // load available reactions

  start_time_ = Time::now();
  last_channel_pts_jump_warning_time_ = start_time_ - 3600;

  bool was_authorized_user = td_->auth_manager_->was_authorized() && !td_->auth_manager_->is_bot();
  if (was_authorized_user) {
    create_folders(10);  // ensure that Main and Archive dialog lists are created
  }
  authorization_date_ = td_->option_manager_->get_option_integer("authorization_date");

  td_->dialog_filter_manager_->init();  // load dialog filters

  if (G()->use_message_database() && was_authorized_user) {
    // erase old keys
    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");

    auto last_database_server_dialog_dates = G()->td_db()->get_binlog_pmc()->prefix_get("last_server_dialog_date");
    for (auto &it : last_database_server_dialog_dates) {
      auto r_folder_id = to_integer_safe<int32>(it.first);
      if (r_folder_id.is_error()) {
        LOG(ERROR) << "Can't parse folder ID from " << it.first;
        continue;
      }

      string order_str;
      string dialog_id_str;
      std::tie(order_str, dialog_id_str) = split(it.second);

      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 " << it.second;
      } else {
        FolderId folder_id(r_folder_id.ok());
        auto *folder = get_dialog_folder(folder_id);
        CHECK(folder != nullptr);
        DialogDate dialog_date(r_order.ok(), DialogId(r_dialog_id.ok()));
        if (dialog_date.get_date() == 0 && dialog_date != MAX_DIALOG_DATE) {
          LOG(ERROR) << "Ignore incorrect last database server dialog date " << dialog_date << " in " << folder_id;
        } else {
          if (folder->last_database_server_dialog_date_ < dialog_date) {
            folder->last_database_server_dialog_date_ = dialog_date;
          }
          LOG(INFO) << "Loaded last_database_server_dialog_date_ " << folder->last_database_server_dialog_date_
                    << " in " << folder_id;
        }
      }
    }

    auto sponsored_dialog_id_string = G()->td_db()->get_binlog_pmc()->get("sponsored_dialog_id");
    if (!sponsored_dialog_id_string.empty()) {
      auto dialog_id_source = split(Slice(sponsored_dialog_id_string));
      auto r_dialog_id = to_integer_safe<int64>(dialog_id_source.first);
      auto r_source = DialogSource::unserialize(dialog_id_source.second);
      if (r_dialog_id.is_error() || r_source.is_error()) {
        LOG(ERROR) << "Can't parse " << sponsored_dialog_id_string;
      } else {
        DialogId dialog_id(r_dialog_id.ok());

        const Dialog *d = get_dialog_force(dialog_id, "init");
        if (d != nullptr) {
          LOG(INFO) << "Loaded sponsored " << dialog_id;
          add_sponsored_dialog(d, r_source.move_as_ok());
        } else {
          LOG(ERROR) << "Can't load " << dialog_id;
        }
      }
    }

    auto pinned_dialog_ids = G()->td_db()->get_binlog_pmc()->prefix_get("pinned_dialog_ids");
    for (auto &it : pinned_dialog_ids) {
      auto r_folder_id = to_integer_safe<int32>(it.first);
      if (r_folder_id.is_error()) {
        LOG(ERROR) << "Can't parse folder ID from " << it.first;
        continue;
      }
      FolderId folder_id(r_folder_id.ok());

      auto r_dialog_ids = transform(full_split(Slice(it.second), ','), [](Slice str) -> Result<DialogId> {
        TRY_RESULT(dialog_id_int, to_integer_safe<int64>(str));
        DialogId dialog_id(dialog_id_int);
        if (!dialog_id.is_valid()) {
          return Status::Error("Have invalid dialog ID");
        }
        return dialog_id;
      });
      if (any_of(r_dialog_ids, [](const auto &r_dialog_id) { return r_dialog_id.is_error(); })) {
        LOG(ERROR) << "Can't parse " << it.second;
        reload_pinned_dialogs(DialogListId(folder_id), Auto());
      } else {
        auto *list = get_dialog_list(DialogListId(folder_id));
        CHECK(list != nullptr);
        CHECK(list->pinned_dialogs_.empty());
        for (auto &r_dialog_id : reversed(r_dialog_ids)) {
          auto dialog_id = r_dialog_id.move_as_ok();
          if (!dialog_id.is_valid()) {
            LOG(ERROR) << "Loaded " << dialog_id << " as a pinned dialog";
            continue;
          }
          auto order = get_next_pinned_dialog_order();
          list->pinned_dialogs_.emplace_back(order, dialog_id);
          list->pinned_dialog_id_orders_.emplace(dialog_id, order);
        }
        std::reverse(list->pinned_dialogs_.begin(), list->pinned_dialogs_.end());
        list->are_pinned_dialogs_inited_ = true;
        update_list_last_pinned_dialog_date(*list);

        LOG(INFO) << "Loaded pinned chats " << list->pinned_dialogs_ << " in " << folder_id;
      }
    }

    auto unread_message_counts = G()->td_db()->get_binlog_pmc()->prefix_get("unread_message_count");
    for (auto &it : unread_message_counts) {
      auto r_dialog_list_id = to_integer_safe<int64>(it.first);
      if (r_dialog_list_id.is_error()) {
        LOG(ERROR) << "Can't parse dialog list ID from " << it.first;
        continue;
      }
      string total_count;
      string muted_count;
      std::tie(total_count, muted_count) = split(it.second);

      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 " << it.second;
      } else {
        DialogListId dialog_list_id(r_dialog_list_id.ok());
        auto *list = get_dialog_list(dialog_list_id);
        if (list != nullptr) {
          list->unread_message_total_count_ = r_total_count.ok();
          list->unread_message_muted_count_ = r_muted_count.ok();
          list->is_message_unread_count_inited_ = true;
          send_update_unread_message_count(*list, DialogId(), true, "load unread_message_count", true);
        } else {
          G()->td_db()->get_binlog_pmc()->erase("unread_message_count" + it.first);
        }
      }
    }

    auto unread_dialog_counts = G()->td_db()->get_binlog_pmc()->prefix_get("unread_dialog_count");
    for (auto &it : unread_dialog_counts) {
      auto r_dialog_list_id = to_integer_safe<int64>(it.first);
      if (r_dialog_list_id.is_error()) {
        LOG(ERROR) << "Can't parse dialog list ID from " << it.first;
        continue;
      }

      auto counts = transform(full_split(Slice(it.second)), [](Slice str) { return to_integer_safe<int32>(str); });
      if ((counts.size() != 4 && counts.size() != 6) || any_of(counts, [](const auto &c) { return c.is_error(); })) {
        LOG(ERROR) << "Can't parse " << it.second;
      } else {
        DialogListId dialog_list_id(r_dialog_list_id.ok());
        auto *list = get_dialog_list(dialog_list_id);
        if (list != nullptr) {
          list->unread_dialog_total_count_ = counts[0].ok();
          list->unread_dialog_muted_count_ = counts[1].ok();
          list->unread_dialog_marked_count_ = counts[2].ok();
          list->unread_dialog_muted_marked_count_ = counts[3].ok();
          if (counts.size() == 6) {
            list->server_dialog_total_count_ = counts[4].ok();
            list->secret_chat_total_count_ = counts[5].ok();
          }
          if (list->server_dialog_total_count_ == -1) {
            repair_server_dialog_total_count(dialog_list_id);
          }
          if (list->secret_chat_total_count_ == -1) {
            repair_secret_chat_total_count(dialog_list_id);
          }
          list->is_dialog_unread_count_inited_ = true;
          send_update_unread_chat_count(*list, DialogId(), true, "load unread_dialog_count", true);
        } else {
          G()->td_db()->get_binlog_pmc()->erase("unread_dialog_count" + it.first);
        }
      }
    }
  } else if (!td_->auth_manager_->is_bot()) {
    G()->td_db()->get_binlog_pmc()->erase_by_prefix("pinned_dialog_ids");
    G()->td_db()->get_binlog_pmc()->erase_by_prefix("last_server_dialog_date");
    G()->td_db()->get_binlog_pmc()->erase_by_prefix("unread_message_count");
    G()->td_db()->get_binlog_pmc()->erase_by_prefix("unread_dialog_count");
    G()->td_db()->get_binlog_pmc()->erase("sponsored_dialog_id");
    G()->td_db()->get_binlog_pmc()->erase("fetched_marks_as_unread");
  }
  G()->td_db()->get_binlog_pmc()->erase("dialog_pinned_current_order");

  if (G()->use_message_database()) {
    ttl_db_loop();
  }

  load_calls_db_state();

  auto auth_notification_ids_string = G()->td_db()->get_binlog_pmc()->get("auth_notification_ids");
  if (!auth_notification_ids_string.empty()) {
    VLOG(notifications) << "Loaded auth_notification_ids = " << auth_notification_ids_string;
    auto stored_ids = full_split(auth_notification_ids_string, ',');
    CHECK(stored_ids.size() % 2 == 0);
    bool is_changed = false;
    auto min_date = G()->unix_time() - AUTH_NOTIFICATION_ID_CACHE_TIME;
    for (size_t i = 0; i < stored_ids.size(); i += 2) {
      auto date = to_integer_safe<int32>(stored_ids[i + 1]).ok();
      if (date < min_date || stored_ids[i].empty()) {
        is_changed = true;
        continue;
      }
      if (auth_notification_id_date_.size() == MAX_SAVED_AUTH_NOTIFICATION_IDS) {
        is_changed = true;
        break;
      }
      auth_notification_id_date_.emplace(std::move(stored_ids[i]), date);
    }
    if (is_changed) {
      save_auth_notification_ids();
    }
  }
}

void MessagesManager::on_authorization_success() {
  CHECK(td_->auth_manager_->is_authorized());
  authorization_date_ = td_->option_manager_->get_option_integer("authorization_date");

  if (td_->auth_manager_->is_bot()) {
    return;
  }

  create_folders(20);
}

void MessagesManager::ttl_db_loop() {
  if (ttl_db_has_query_) {
    return;
  }

  auto now = Time::now();
  if (now < ttl_db_next_request_time_) {
    ttl_db_slot_.set_event(EventCreator::yield(actor_shared(this, YieldType::TtlDb)));
    auto wakeup_in = ttl_db_next_request_time_ - now;
    ttl_db_slot_.set_timeout_in(wakeup_in);
    LOG(INFO) << "Set ttl_db timeout in " << wakeup_in;
    return;
  }

  ttl_db_has_query_ = true;
  LOG(INFO) << "Send ttl_db query with limit " << ttl_db_next_limit_;
  G()->td_db()->get_message_db_async()->get_expiring_messages(
      G()->unix_time() - 1, ttl_db_next_limit_,
      PromiseCreator::lambda([actor_id = actor_id(this)](Result<std::vector<MessageDbMessage>> result) {
        send_closure(actor_id, &MessagesManager::ttl_db_on_result, std::move(result), false);
      }));
}

void MessagesManager::ttl_db_on_result(Result<std::vector<MessageDbMessage>> r_result, bool dummy) {
  if (G()->close_flag()) {
    return;
  }

  CHECK(r_result.is_ok());
  auto result = r_result.move_as_ok();
  ttl_db_has_query_ = false;

  int32 next_request_delay;
  if (result.size() == static_cast<size_t>(ttl_db_next_limit_)) {
    CHECK(ttl_db_next_limit_ < (1 << 30));
    ttl_db_next_limit_ *= 2;
    next_request_delay = 1;
  } else {
    ttl_db_next_limit_ = DEFAULT_LOADED_EXPIRED_MESSAGES;
    next_request_delay = Random::fast(3000, 4200);
  }
  ttl_db_next_request_time_ = Time::now() + next_request_delay;

  LOG(INFO) << "Receive " << result.size() << " expired messages from ttl_db with next request in "
            << next_request_delay << " seconds";
  for (auto &dialog_message : result) {
    on_get_message_from_database(dialog_message, false, "ttl_db_on_result");
  }
  ttl_db_loop();
}

void MessagesManager::on_send_secret_message_error(int64 random_id, Status error, Promise<Unit> promise) {
  promise.set_value(Unit());  // TODO: set after error is saved

  auto it = being_sent_messages_.find(random_id);
  if (it != being_sent_messages_.end()) {
    auto message_full_id = it->second;
    auto *m = get_message(message_full_id);
    if (m != nullptr) {
      auto file_id = get_message_content_upload_file_id(m->content.get());
      if (file_id.is_valid()) {
        if (G()->close_flag() && G()->use_message_database()) {
          // do not send error, message will be re-sent after restart
          return;
        }
        auto bad_parts = FileManager::get_missing_file_parts(error);
        if (!bad_parts.empty()) {
          on_send_message_file_parts_missing(random_id, std::move(bad_parts));
          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,
                                                     unique_ptr<EncryptedFile> file, Promise<Unit> promise) {
  promise.set_value(Unit());  // TODO: set after message is saved

  FileId new_file_id;
  if (file != nullptr) {
    if (!DcId::is_valid(file->dc_id_)) {
      LOG(ERROR) << "Wrong dc_id = " << file->dc_id_ << " in file " << *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, file->size_, to_string(static_cast<uint64>(file->id_)));
    }
  }

  on_send_message_success(random_id, message_id, date, 0, new_file_id, "on_send_secret_message_success");
}

void MessagesManager::delete_secret_messages(SecretChatId secret_chat_id, std::vector<int64> random_ids,
                                             Promise<Unit> promise) {
  LOG(DEBUG) << "On delete messages in " << secret_chat_id << " with random_ids " << random_ids;
  CHECK(secret_chat_id.is_valid());

  DialogId dialog_id(secret_chat_id);
  if (!have_dialog_force(dialog_id, "delete_secret_messages")) {
    LOG(ERROR) << "Ignore delete secret messages in unknown " << dialog_id;
    promise.set_value(Unit());
    return;
  }

  auto pending_secret_message = make_unique<PendingSecretMessage>();
  pending_secret_message->success_promise = std::move(promise);
  pending_secret_message->type = PendingSecretMessage::Type::DeleteMessages;
  pending_secret_message->dialog_id = dialog_id;
  pending_secret_message->random_ids = std::move(random_ids);

  add_secret_message(std::move(pending_secret_message));
}

void MessagesManager::finish_delete_secret_messages(DialogId dialog_id, std::vector<int64> random_ids,
                                                    Promise<Unit> promise) {
  LOG(INFO) << "Delete messages with random_ids " << random_ids << " in " << dialog_id;
  promise.set_value(Unit());  // TODO: set after event is saved

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  vector<MessageId> to_delete_message_ids;
  for (auto &random_id : random_ids) {
    auto message_id = get_message_id_by_random_id(d, random_id, "finish_delete_secret_messages");
    if (!message_id.is_valid()) {
      LOG(INFO) << "Can't find message with random_id " << random_id;
      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);
    } else {
      LOG(INFO) << "Skip deletion of service " << message_id;
    }
  }
  delete_dialog_messages(d, to_delete_message_ids, true, "finish_delete_secret_messages");
}

void MessagesManager::delete_secret_chat_history(SecretChatId secret_chat_id, bool remove_from_dialog_list,
                                                 MessageId last_message_id, Promise<Unit> promise) {
  LOG(DEBUG) << "Delete history in " << secret_chat_id << " up to " << last_message_id;
  CHECK(secret_chat_id.is_valid());
  CHECK(!last_message_id.is_scheduled());

  DialogId dialog_id(secret_chat_id);
  if (!have_dialog_force(dialog_id, "delete_secret_chat_history")) {
    LOG(ERROR) << "Ignore delete history in unknown " << dialog_id;
    promise.set_value(Unit());
    return;
  }

  auto pending_secret_message = make_unique<PendingSecretMessage>();
  pending_secret_message->success_promise = std::move(promise);
  pending_secret_message->type = PendingSecretMessage::Type::DeleteHistory;
  pending_secret_message->dialog_id = dialog_id;
  pending_secret_message->last_message_id = last_message_id;
  pending_secret_message->remove_from_dialog_list = remove_from_dialog_list;

  add_secret_message(std::move(pending_secret_message));
}

void MessagesManager::finish_delete_secret_chat_history(DialogId dialog_id, bool remove_from_dialog_list,
                                                        MessageId last_message_id, Promise<Unit> promise) {
  LOG(DEBUG) << "Delete history in " << dialog_id << " up to " << last_message_id;
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  // TODO: probably last_message_id is not needed
  delete_all_dialog_messages(d, remove_from_dialog_list, true);
  promise.set_value(Unit());  // TODO: set after event is saved
}

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;
  }
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  auto dialog_id = DialogId(secret_chat_id);
  Dialog *d = get_dialog_force(dialog_id, "read_secret_chat_outbox");
  if (d == nullptr) {
    return;
  }

  if (read_date > 0) {
    auto user_id = td_->contacts_manager_->get_secret_chat_user_id(secret_chat_id);
    if (user_id.is_valid()) {
      td_->contacts_manager_->on_update_user_local_was_online(user_id, read_date);
    }
  }

  // TODO: protect with log event
  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 = d->ordered_messages.get_const_iterator(MessageId::max());
  while (*end &&
         (get_message(d, (*end)->get_message_id())->date > up_to_date || (*end)->get_message_id().is_yet_unsent())) {
    --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)->get_message_id();
  read_history_outbox(d, max_message_id, read_date);
}

void MessagesManager::open_secret_message(SecretChatId secret_chat_id, int64 random_id, Promise<Unit> promise) {
  promise.set_value(Unit());  // TODO: set after event is saved
  DialogId dialog_id(secret_chat_id);
  Dialog *d = get_dialog_force(dialog_id, "open_secret_message");
  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 (m->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, 0, "open_secret_message");
}

void MessagesManager::on_update_secret_chat_state(SecretChatId secret_chat_id, SecretChatState state) {
  if (state == SecretChatState::Closed && !td_->auth_manager_->is_bot()) {
    DialogId dialog_id(secret_chat_id);
    Dialog *d = get_dialog_force(dialog_id, "on_update_secret_chat_state");
    if (d != nullptr && d->notification_info != nullptr) {
      if (d->notification_info->new_secret_chat_notification_id_.is_valid()) {
        remove_new_secret_chat_notification(d, true);
      }
      if (d->notification_info->message_notification_group_.is_valid() &&
          get_dialog_pending_notification_count(d, false) == 0 &&
          !d->notification_info->message_notification_group_.get_last_notification_id().is_valid()) {
        d->notification_info->message_notification_group_.try_reuse();
        on_dialog_updated(d->dialog_id, "on_update_secret_chat_state");
      }
      CHECK(!d->notification_info->mention_notification_group_
                 .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, unique_ptr<EncryptedFile> file,
                                            tl_object_ptr<secret_api::decryptedMessage> message,
                                            Promise<Unit> 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);

  if (message->random_id_ == 0) {
    LOG(ERROR) << "Ignore secret message with random_id == 0";
    promise.set_error(Status::Error(400, "Invalid random_id"));
    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 = message->random_id_;
  message_info.ttl = MessageSelfDestructType(message->ttl_, false);
  message_info.has_unread_content = true;
  message_info.is_silent = message->silent_;
  message_info.media_album_id = message->grouped_id_;

  Dialog *d = get_dialog_force(message_info.dialog_id, "on_get_secret_message");
  if (d == nullptr && td_->dialog_manager_->have_dialog_info_force(message_info.dialog_id, "on_get_secret_message")) {
    force_create_dialog(message_info.dialog_id, "on_get_secret_message", true, true);
    d = get_dialog(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_ids_[message_info.dialog_id][message_info.random_id] = message_id;

  pending_secret_message->load_data_multipromise.add_promise(Auto());
  auto lock_promise = pending_secret_message->load_data_multipromise.get_promise();

  if ((message->flags_ & secret_api::decryptedMessage::REPLY_TO_RANDOM_ID_MASK) != 0) {
    auto reply_to_message_id = get_message_id_by_random_id(d, message->reply_to_random_id_, "on_get_secret_message");
    if (!reply_to_message_id.is_valid()) {
      auto dialog_it = pending_secret_message_ids_.find(message_info.dialog_id);
      if (dialog_it != pending_secret_message_ids_.end()) {
        auto message_it = dialog_it->second.find(message->reply_to_random_id_);
        if (message_it != dialog_it->second.end()) {
          reply_to_message_id = message_it->second;
        }
      }
    }
    message_info.reply_header.replied_message_info_ = RepliedMessageInfo::legacy(reply_to_message_id);
  }

  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->via_bot_name_.empty()) {
    auto request_promise = PromiseCreator::lambda(
        [actor_id = actor_id(this), via_bot_username = message->via_bot_name_, message_info_ptr = &message_info,
         promise = pending_secret_message->load_data_multipromise.get_promise()](Unit) mutable {
          send_closure(actor_id, &MessagesManager::on_resolve_secret_chat_message_via_bot_username, via_bot_username,
                       message_info_ptr, std::move(promise));
        });
    td_->dialog_manager_->search_public_dialog(message->via_bot_name_, false, std::move(request_promise));
  }

  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,
      td_->contacts_manager_->is_user_premium(user_id));

  add_secret_message(std::move(pending_secret_message), std::move(lock_promise));
}

void MessagesManager::on_resolve_secret_chat_message_via_bot_username(const string &via_bot_username,
                                                                      MessageInfo *message_info_ptr,
                                                                      Promise<Unit> &&promise) {
  if (!G()->close_flag()) {
    auto dialog_id = td_->dialog_manager_->get_resolved_dialog_by_username(via_bot_username);
    if (dialog_id.is_valid() && dialog_id.get_type() == DialogType::User) {
      auto user_id = dialog_id.get_user_id();
      auto r_bot_data = td_->contacts_manager_->get_bot_data(user_id);
      if (r_bot_data.is_ok() && r_bot_data.ok().is_inline) {
        message_info_ptr->via_bot_user_id = user_id;
      }
    }
  }
  promise.set_value(Unit());
}

void MessagesManager::on_secret_chat_screenshot_taken(SecretChatId secret_chat_id, UserId user_id, MessageId message_id,
                                                      int32 date, int64 random_id, Promise<Unit> 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.content = create_screenshot_taken_message_content();

  Dialog *d = get_dialog_force(message_info.dialog_id, "on_secret_chat_screenshot_taken");
  if (d == nullptr &&
      td_->dialog_manager_->have_dialog_info_force(message_info.dialog_id, "on_secret_chat_screenshot_taken")) {
    force_create_dialog(message_info.dialog_id, "on_get_secret_message", true, true);
    d = get_dialog(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<Unit> promise) {
  LOG(DEBUG) << "On self-destruct timer 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 self-destruct time = " << 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.content = create_chat_set_ttl_message_content(ttl, UserId());

  Dialog *d = get_dialog_force(message_info.dialog_id, "on_secret_chat_ttl_changed");
  if (d == nullptr &&
      td_->dialog_manager_->have_dialog_info_force(message_info.dialog_id, "on_secret_chat_ttl_changed")) {
    force_create_dialog(message_info.dialog_id, "on_get_secret_message", true, true);
    d = get_dialog(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([actor_id = actor_id(this), token](Result<Unit> result) {
    if (result.is_ok()) {
      send_closure(actor_id, &MessagesManager::on_add_secret_message_ready, token);
    }
  }));

  if (!lock_promise) {
    lock_promise = multipromise.get_promise();
  }
  lock_promise.set_value(Unit());
}

void MessagesManager::on_add_secret_message_ready(int64 token) {
  if (G()->close_flag()) {
    return;
  }

  pending_secret_messages_.finish(
      token, [actor_id = actor_id(this)](unique_ptr<PendingSecretMessage> pending_secret_message) {
        send_closure_later(actor_id, &MessagesManager::finish_add_secret_message, std::move(pending_secret_message));
      });
}

void MessagesManager::finish_add_secret_message(unique_ptr<PendingSecretMessage> pending_secret_message) {
  if (G()->close_flag()) {
    return;
  }

  if (pending_secret_message->type == PendingSecretMessage::Type::DeleteMessages) {
    return finish_delete_secret_messages(pending_secret_message->dialog_id,
                                         std::move(pending_secret_message->random_ids),
                                         std::move(pending_secret_message->success_promise));
  }
  if (pending_secret_message->type == PendingSecretMessage::Type::DeleteHistory) {
    return finish_delete_secret_chat_history(
        pending_secret_message->dialog_id, pending_secret_message->remove_from_dialog_list,
        pending_secret_message->last_message_id, std::move(pending_secret_message->success_promise));
  }

  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 {
    if (!td_->contacts_manager_->is_user_premium(pending_secret_message->message_info.sender_user_id)) {
      auto message_text = get_message_content_text_mutable(pending_secret_message->message_info.content.get());
      if (message_text != nullptr) {
        remove_premium_custom_emoji_entities(td_, message_text->entities, true);
      }
    }

    on_get_message(std::move(pending_secret_message->message_info), true, false, "finish add secret message");
  }
  auto dialog_it = pending_secret_message_ids_.find(d->dialog_id);
  if (dialog_it != pending_secret_message_ids_.end()) {
    auto message_it = dialog_it->second.find(random_id);
    if (message_it != dialog_it->second.end() && message_it->second == message_id) {
      dialog_it->second.erase(message_it);
      if (dialog_it->second.empty()) {
        pending_secret_message_ids_.erase(dialog_it);
      }
    }
  }

  pending_secret_message->success_promise.set_value(Unit());  // TODO: set after message is saved
}

MessagesManager::MessageInfo MessagesManager::parse_telegram_api_message(
    tl_object_ptr<telegram_api::Message> message_ptr, bool is_scheduled, const char *source) const {
  LOG(DEBUG) << "Receive from " << source << " " << to_string(message_ptr);
  LOG_CHECK(message_ptr != nullptr) << source;

  MessageInfo message_info;
  message_info.message_id = MessageId::get_message_id(message_ptr, is_scheduled);
  switch (message_ptr->get_id()) {
    case telegram_api::messageEmpty::ID:
      message_info.message_id = MessageId();
      break;
    case telegram_api::message::ID: {
      auto message = move_tl_object_as<telegram_api::message>(message_ptr);
      if (message->quick_reply_shortcut_id_ != 0) {
        LOG(ERROR) << "Receive shortcut " << message->quick_reply_shortcut_id_ << " from " << source;
        message_info.message_id = MessageId();
        break;
      }

      message_info.dialog_id = DialogId(message->peer_id_);
      if (message->from_id_ != nullptr) {
        message_info.sender_dialog_id = DialogId(message->from_id_);
      } else {
        message_info.sender_dialog_id = message_info.dialog_id;
      }
      message_info.date = message->date_;
      message_info.forward_header = std::move(message->fwd_from_);
      bool can_have_thread = message_info.dialog_id.get_type() == DialogType::Channel &&
                             !td_->dialog_manager_->is_broadcast_channel(message_info.dialog_id);
      message_info.reply_header = MessageReplyHeader(td_, std::move(message->reply_to_), message_info.dialog_id,
                                                     message_info.message_id, message_info.date, can_have_thread);
      if (message->flags_ & telegram_api::message::VIA_BOT_ID_MASK) {
        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();
        }
      }
      message_info.view_count = message->views_;
      message_info.forward_count = message->forwards_;
      message_info.reply_info = std::move(message->replies_);
      message_info.reactions = std::move(message->reactions_);
      message_info.edit_date = message->edit_date_;
      message_info.media_album_id = message->grouped_id_;
      message_info.ttl_period = message->ttl_period_;
      message_info.is_outgoing = message->out_;
      message_info.is_silent = message->silent_;
      message_info.is_channel_post = message->post_;
      message_info.is_legacy = message->legacy_;
      message_info.hide_edit_date = message->edit_hide_;
      message_info.is_from_scheduled = message->from_scheduled_;
      message_info.is_pinned = message->pinned_;
      message_info.noforwards = message->noforwards_;
      message_info.has_mention = message->mentioned_;
      message_info.has_unread_content = message->media_unread_;
      message_info.invert_media = message->invert_media_;

      bool is_content_read = true;
      if (!td_->auth_manager_->is_bot()) {
        if (is_scheduled) {
          is_content_read = false;
        } else if (is_message_auto_read(message_info.dialog_id, message_info.is_outgoing)) {
          is_content_read = true;
        } else {
          is_content_read = !message_info.has_unread_content;
        }
      }
      auto new_source = PSTRING() << MessageFullId(message_info.dialog_id, message_info.message_id) << " sent by "
                                  << message_info.sender_dialog_id << " from " << source;
      message_info.content = get_message_content(
          td_,
          get_message_text(td_->contacts_manager_.get(), std::move(message->message_), std::move(message->entities_),
                           true, td_->auth_manager_->is_bot(),
                           message_info.forward_header ? message_info.forward_header->date_ : message_info.date,
                           message_info.media_album_id != 0, new_source.c_str()),
          std::move(message->media_), message_info.dialog_id, message_info.date, is_content_read,
          message_info.via_bot_user_id, &message_info.ttl, &message_info.disable_web_page_preview, new_source.c_str());
      message_info.reply_markup = std::move(message->reply_markup_);
      message_info.restriction_reasons = get_restriction_reasons(std::move(message->restriction_reason_));
      message_info.author_signature = std::move(message->post_author_);
      message_info.sender_boost_count = message->from_boosts_applied_;
      if (message->saved_peer_id_ != nullptr) {
        message_info.saved_messages_topic_id = SavedMessagesTopicId(DialogId(message->saved_peer_id_));
      }
      break;
    }
    case telegram_api::messageService::ID: {
      auto message = move_tl_object_as<telegram_api::messageService>(message_ptr);

      message_info.dialog_id = DialogId(message->peer_id_);
      if (message->from_id_ != nullptr) {
        message_info.sender_dialog_id = DialogId(message->from_id_);
      } else {
        message_info.sender_dialog_id = message_info.dialog_id;
      }
      message_info.date = message->date_;
      message_info.ttl_period = message->ttl_period_;
      message_info.is_outgoing = message->out_;
      message_info.is_silent = message->silent_;
      message_info.is_channel_post = message->post_;
      message_info.is_legacy = message->legacy_;
      message_info.has_mention = message->mentioned_;
      message_info.has_unread_content = message->media_unread_;
      bool can_have_thread = message_info.dialog_id.get_type() == DialogType::Channel &&
                             !td_->dialog_manager_->is_broadcast_channel(message_info.dialog_id);
      message_info.reply_header = MessageReplyHeader(td_, std::move(message->reply_to_), message_info.dialog_id,
                                                     message_info.message_id, message_info.date, can_have_thread);
      message_info.content =
          get_action_message_content(td_, std::move(message->action_), message_info.dialog_id, message_info.date,
                                     message_info.reply_header.replied_message_info_);
      message_info.reply_header.replied_message_info_ = RepliedMessageInfo();
      message_info.reply_header.story_full_id_ = StoryFullId();
      if (message_info.dialog_id == td_->dialog_manager_->get_my_dialog_id()) {
        message_info.saved_messages_topic_id = SavedMessagesTopicId(message_info.dialog_id);
      }
      break;
    }
    default:
      UNREACHABLE();
      break;
  }
  if (message_info.sender_dialog_id.is_valid() && message_info.sender_dialog_id.get_type() == DialogType::User) {
    message_info.sender_user_id = message_info.sender_dialog_id.get_user_id();
    message_info.sender_dialog_id = DialogId();
  }
  return message_info;
}

std::pair<DialogId, unique_ptr<MessagesManager::Message>> MessagesManager::create_message(MessageInfo &&message_info,
                                                                                          bool is_channel_message,
                                                                                          const char *source) {
  DialogId dialog_id = message_info.dialog_id;
  MessageId message_id = message_info.message_id;
  if ((!message_id.is_valid() && !message_id.is_valid_scheduled()) || !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() || message_id.is_local()) {
    LOG(ERROR) << "Receive " << message_id;
    return {DialogId(), nullptr};
  }

  CHECK(message_info.content != nullptr);

  auto dialog_type = dialog_id.get_type();
  UserId sender_user_id = message_info.sender_user_id;
  DialogId sender_dialog_id = message_info.sender_dialog_id;
  if (!sender_user_id.is_valid()) {
    if (sender_user_id != UserId()) {
      LOG(ERROR) << "Receive invalid " << sender_user_id;
      sender_user_id = UserId();
    }
    if (!td_->dialog_manager_->is_broadcast_channel(dialog_id) && td_->auth_manager_->is_bot()) {
      if (dialog_id == sender_dialog_id) {
        td_->contacts_manager_->add_anonymous_bot_user();
      } else {
        td_->contacts_manager_->add_service_notifications_user();
        td_->contacts_manager_->add_channel_bot_user();
      }
    }
  }
  if (sender_dialog_id.is_valid()) {
    if (dialog_type == DialogType::User || dialog_type == DialogType::SecretChat) {
      LOG(ERROR) << "Receive " << message_id << " sent by " << sender_dialog_id << " in " << dialog_id;
      return {DialogId(), nullptr};
    }
  } else if (sender_dialog_id != DialogId()) {
    LOG(ERROR) << "Receive invalid " << sender_dialog_id;
    sender_dialog_id = DialogId();
  }
  if (message_id.is_scheduled()) {
    is_channel_message = (dialog_type == DialogType::Channel);
  }

  LOG_IF(ERROR, is_channel_message != (dialog_type == DialogType::Channel))
      << "Receive wrong is_channel_message for " << message_id << " in " << dialog_id;

  bool is_channel_post = message_info.is_channel_post;
  if (is_channel_post && !td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
    LOG(ERROR) << "Receive is_channel_post for " << message_id << " in " << dialog_id;
    is_channel_post = false;
  }

  UserId my_id = td_->contacts_manager_->get_my_id();
  DialogId my_dialog_id = DialogId(my_id);
  if (dialog_id == my_dialog_id && (sender_user_id != my_id || sender_dialog_id.is_valid())) {
    LOG(ERROR) << "Receive " << sender_user_id << "/" << sender_dialog_id << " as a sender of " << message_id
               << " instead of self";
    sender_user_id = my_id;
    sender_dialog_id = DialogId();
  }

  bool is_outgoing = message_info.is_outgoing;
  bool supposed_to_be_outgoing = sender_user_id == my_id && !(dialog_id == my_dialog_id && !message_id.is_scheduled());
  if (sender_user_id.is_valid() && supposed_to_be_outgoing != is_outgoing) {
    LOG(ERROR) << "Receive wrong out flag for " << message_id << " in " << dialog_id << ": me is " << my_id
               << ", but message is from " << sender_user_id;
    is_outgoing = supposed_to_be_outgoing;

    /*
    // it is useless to call getChannelDifference, because the channel PTS will be increased already
    if (dialog_type == DialogType::Channel && !running_get_difference_ && !running_get_channel_difference(dialog_id) &&
        !message_id.is_scheduled() && get_channel_difference_to_log_event_id_.count(dialog_id) == 0) {
      // it is safer to completely ignore the message and re-get it through getChannelDifference
      Dialog *d = get_dialog(dialog_id);
      if (d != nullptr) {
        schedule_get_channel_difference(dialog_id, 0, message_id, 0.001, source);
        return {DialogId(), nullptr};
      }
    }
    */
  }

  int32 date = message_info.date;
  if (date <= 0) {
    LOG(ERROR) << "Wrong date = " << date << " received in " << message_id << " in " << dialog_id;
    date = 1;
  }

  MessageId top_thread_message_id = message_info.reply_header.top_thread_message_id_;
  bool is_topic_message = message_info.reply_header.is_topic_message_;
  auto reply_to_story_full_id = message_info.reply_header.story_full_id_;
  if (reply_to_story_full_id != StoryFullId() &&
      (reply_to_story_full_id.get_dialog_id() != my_dialog_id && reply_to_story_full_id.get_dialog_id() != dialog_id)) {
    LOG(ERROR) << "Receive reply to " << reply_to_story_full_id << " in " << dialog_id;
    reply_to_story_full_id = {};
  }

  UserId via_bot_user_id = message_info.via_bot_user_id;
  if (!via_bot_user_id.is_valid()) {
    via_bot_user_id = UserId();
  }

  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;
  }

  auto content_type = message_info.content->get_type();
  if (content_type == MessageContentType::Sticker &&
      get_message_content_sticker_type(td_, message_info.content.get()) == StickerType::CustomEmoji) {
    LOG(INFO) << "Replace emoji sticker with an empty message";
    message_info.content =
        create_text_message_content("Invalid sticker", {}, WebPageId(), false, false, false, string());
    message_info.invert_media = false;
    content_type = message_info.content->get_type();
  }

  bool hide_edit_date = message_info.hide_edit_date;
  if (hide_edit_date && td_->auth_manager_->is_bot()) {
    hide_edit_date = false;
  }
  if (hide_edit_date && content_type == MessageContentType::LiveLocation) {
    hide_edit_date = false;
  }

  int32 ttl_period = message_info.ttl_period;
  if (ttl_period < 0 || (message_id.is_scheduled() && ttl_period != 0)) {
    LOG(ERROR) << "Wrong auto-delete time " << ttl_period << " received in " << message_id << " in " << dialog_id;
    ttl_period = 0;
  }

  auto ttl = message_info.ttl;
  bool is_content_secret = ttl.is_secret_message_content(content_type);  // must be calculated before TTL is adjusted
  if (!ttl.is_empty()) {
    if (!ttl.is_valid() || message_id.is_scheduled()) {
      LOG(ERROR) << "Wrong " << ttl << " received in " << message_id << " in " << dialog_id;
      ttl = {};
    } else {
      ttl.ensure_at_least(get_message_content_duration(message_info.content.get(), td_) + 1);
    }
  }

  if (message_id.is_scheduled()) {
    if (message_info.reply_info != nullptr) {
      LOG(ERROR) << "Receive " << message_id << " in " << dialog_id << " with " << to_string(message_info.reply_info);
      message_info.reply_info = nullptr;
    }
    if (message_info.reactions != nullptr) {
      LOG(ERROR) << "Receive " << message_id << " in " << dialog_id << " with " << to_string(message_info.reactions);
      message_info.reactions = nullptr;
    }
  }
  int32 view_count = message_info.view_count;
  if (view_count < 0) {
    LOG(ERROR) << "Wrong view_count = " << view_count << " received in " << message_id << " in " << dialog_id;
    view_count = 0;
  }
  int32 forward_count = message_info.forward_count;
  if (forward_count < 0) {
    LOG(ERROR) << "Wrong forward_count = " << forward_count << " received in " << message_id << " in " << dialog_id;
    forward_count = 0;
  }
  MessageReplyInfo reply_info(td_, std::move(message_info.reply_info), td_->auth_manager_->is_bot());
  if (!top_thread_message_id.is_valid() && is_thread_message(dialog_id, message_id, reply_info, content_type)) {
    top_thread_message_id = message_id;
    is_topic_message = (content_type == MessageContentType::TopicCreate);
  }
  if (top_thread_message_id.is_valid() && dialog_type != DialogType::Channel) {
    // just in case
    top_thread_message_id = MessageId();
  }
  if (!top_thread_message_id.is_valid()) {
    // just in case
    is_topic_message = false;
  }
  auto reactions =
      MessageReactions::get_message_reactions(td_, std::move(message_info.reactions), td_->auth_manager_->is_bot());
  if (reactions != nullptr) {
    reactions->sort_reactions(active_reaction_pos_);
    reactions->fix_chosen_reaction();
    reactions->fix_my_recent_chooser_dialog_id(td_->dialog_manager_->get_my_dialog_id());
  }

  bool has_forward_info = message_info.forward_header != nullptr;

  if (sender_dialog_id.is_valid() && sender_dialog_id != dialog_id) {
    CHECK(sender_dialog_id.get_type() != DialogType::User);
    force_create_dialog(sender_dialog_id, "create_message", true);
  }

  bool noforwards = message_info.noforwards;
  bool is_expired = is_expired_message_content(content_type);
  if (is_expired) {
    CHECK(ttl.is_empty());  // self-destruct time is ignored/set to 0 if the message has already been expired
    message_info.reply_header.replied_message_info_ = {};
    reply_to_story_full_id = StoryFullId();
    noforwards = false;
    is_content_secret = false;
    message_info.invert_media = false;
  }

  bool is_pinned = message_info.is_pinned;
  if (is_pinned && message_id.is_scheduled()) {
    LOG(ERROR) << "Receive pinned " << message_id << " in " << dialog_id;
    is_pinned = false;
  }

  bool has_mention =
      message_info.has_mention || (content_type == MessageContentType::PinMessage &&
                                   td_->option_manager_->get_option_boolean("process_pinned_messages_as_mentions"));

  LOG(INFO) << "Receive " << message_id << " in " << dialog_id << " from " << sender_user_id << "/" << sender_dialog_id;

  auto message = make_unique<Message>();
  message->message_id = message_id;
  message->sender_user_id = sender_user_id;
  message->sender_dialog_id = sender_dialog_id;
  message->date = date;
  message->ttl_period = ttl_period;
  message->ttl = ttl;
  message->disable_web_page_preview = message_info.disable_web_page_preview;
  message->edit_date = edit_date;
  message->random_id = message_info.random_id;
  message->forward_info = MessageForwardInfo::get_message_forward_info(td_, std::move(message_info.forward_header));
  message->replied_message_info = std::move(message_info.reply_header.replied_message_info_);
  message->top_thread_message_id = top_thread_message_id;
  message->is_topic_message = is_topic_message;
  message->via_bot_user_id = via_bot_user_id;
  message->reply_to_story_full_id = reply_to_story_full_id;
  message->restriction_reasons = std::move(message_info.restriction_reasons);
  message->author_signature = std::move(message_info.author_signature);
  message->sender_boost_count = message_info.sender_boost_count;
  message->saved_messages_topic_id = message_info.saved_messages_topic_id;
  message->is_outgoing = is_outgoing;
  message->is_channel_post = is_channel_post;
  message->contains_mention =
      !is_outgoing && dialog_type != DialogType::User && !is_expired && has_mention && !td_->auth_manager_->is_bot();
  message->contains_unread_mention =
      !message_id.is_scheduled() && message_id.is_server() && message->contains_mention &&
      message_info.has_unread_content &&
      (dialog_type == DialogType::Chat ||
       (dialog_type == DialogType::Channel && !td_->dialog_manager_->is_broadcast_channel(dialog_id)));
  message->disable_notification = message_info.is_silent;
  message->is_content_secret = is_content_secret;
  message->hide_edit_date = hide_edit_date;
  message->is_from_scheduled = message_info.is_from_scheduled;
  message->is_pinned = is_pinned;
  message->noforwards = noforwards;
  message->interaction_info_update_date = G()->unix_time();
  message->view_count = view_count;
  message->forward_count = forward_count;
  message->reply_info = std::move(reply_info);
  message->reactions = std::move(reactions);
  message->legacy_layer = (message_info.is_legacy ? MTPROTO_LAYER : 0);
  message->invert_media = message_info.invert_media;
  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_type == DialogType::User);
  if (message->reply_markup != nullptr && is_expired) {
    // just in case
    if (message->reply_markup->type != ReplyMarkup::Type::InlineKeyboard) {
      message->had_reply_markup = true;
    }
    message->reply_markup = nullptr;
  }

  if (message_info.media_album_id != 0) {
    if (!is_allowed_media_group_content(content_type)) {
      if (content_type != MessageContentType::Unsupported) {
        LOG(ERROR) << "Receive media group identifier " << message_info.media_album_id << " in " << message_id
                   << " from " << dialog_id << " with content "
                   << oneline(to_string(get_message_message_content_object(dialog_id, message.get())));
      }
    } else {
      message->media_album_id = message_info.media_album_id;
    }
  }

  if (message->forward_info == nullptr && has_forward_info) {
    message->had_forward_info = true;
  }

  if (dialog_id == my_dialog_id) {
    if (!message->saved_messages_topic_id.is_valid()) {
      LOG(ERROR) << "Receive no Saved Messages topic for " << message_id << " in " << dialog_id;
      message->saved_messages_topic_id = SavedMessagesTopicId(my_dialog_id, message->forward_info.get(), DialogId());
    }
  } else {
    if (message->saved_messages_topic_id.is_valid()) {
      LOG(ERROR) << "Receive Saved Messages " << message_info.saved_messages_topic_id << " for " << message_id << " in "
                 << dialog_id;
      message->saved_messages_topic_id = SavedMessagesTopicId();
    }
  }

  Dependencies dependencies;
  add_message_dependencies(dependencies, message.get());
  for (auto dependent_dialog_id : dependencies.get_dialog_ids()) {
    force_create_dialog(dependent_dialog_id, source, true);
  }

  return {dialog_id, std::move(message)};
}

MessageId MessagesManager::find_old_message_id(DialogId dialog_id, MessageId message_id) const {
  if (message_id.is_scheduled()) {
    CHECK(message_id.is_scheduled_server());
    auto dialog_it = update_scheduled_message_ids_.find(dialog_id);
    if (dialog_it != update_scheduled_message_ids_.end()) {
      auto it = dialog_it->second.find(message_id.get_scheduled_server_message_id());
      if (it != dialog_it->second.end()) {
        return it->second;
      }
    }
  } else {
    CHECK(message_id.is_server());
    auto it = update_message_ids_.find(MessageFullId(dialog_id, message_id));
    if (it != update_message_ids_.end()) {
      return it->second;
    }
  }
  return MessageId();
}

void MessagesManager::delete_update_message_id(DialogId dialog_id, MessageId message_id) {
  if (message_id.is_scheduled()) {
    CHECK(message_id.is_scheduled_server());
    auto dialog_it = update_scheduled_message_ids_.find(dialog_id);
    CHECK(dialog_it != update_scheduled_message_ids_.end());
    auto erased_count = dialog_it->second.erase(message_id.get_scheduled_server_message_id());
    CHECK(erased_count > 0);
    if (dialog_it->second.empty()) {
      update_scheduled_message_ids_.erase(dialog_it);
    }
  } else {
    CHECK(message_id.is_server());
    auto erased_count = update_message_ids_.erase(MessageFullId(dialog_id, message_id));
    CHECK(erased_count > 0);
  }
}

MessageFullId MessagesManager::on_get_message(tl_object_ptr<telegram_api::Message> message_ptr, bool from_update,
                                              bool is_channel_message, bool is_scheduled, const char *source) {
  return on_get_message(parse_telegram_api_message(std::move(message_ptr), is_scheduled, source), from_update,
                        is_channel_message, source);
}

MessageFullId MessagesManager::on_get_message(MessageInfo &&message_info, const bool from_update,
                                              const bool is_channel_message, 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, source);
  if (new_message == nullptr) {
    return MessageFullId();
  }
  MessageId message_id = new_message->message_id;

  bool need_update = from_update;
  bool need_update_dialog_pos = false;

  Dialog *d = get_dialog_force(dialog_id, source);

  MessageId old_message_id = find_old_message_id(dialog_id, message_id);
  bool is_sent_message = false;
  if (old_message_id.is_valid() || old_message_id.is_valid_scheduled()) {
    LOG(INFO) << "Found temporary " << old_message_id << " for " << MessageFullId{dialog_id, message_id};
    CHECK(d != nullptr);

    if (!from_update && !message_id.is_scheduled()) {
      if (message_id <= d->last_new_message_id) {
        if (get_message_force(d, message_id, "receive missed unsent message not from update") != nullptr) {
          LOG(ERROR) << "New " << old_message_id << "/" << message_id << " in " << dialog_id << " from " << source
                     << " has identifier less than last_new_message_id = " << d->last_new_message_id;
          return MessageFullId();
        }
        // if there is no message yet, then it is likely was missed because of a server bug and is being repaired via
        // get_message_from_server from after_get_difference
        // TODO move to INFO
        LOG(ERROR) << "Receive " << old_message_id << "/" << message_id << " in " << dialog_id << " from " << source
                   << " with identifier less than last_new_message_id = " << d->last_new_message_id
                   << " and trying to add it anyway";
      } else {
        LOG(INFO) << "Ignore " << old_message_id << "/" << message_id << " received not through update from " << source
                  << ": " << oneline(to_string(get_message_object(dialog_id, new_message.get(), "on_get_message")));
        if (dialog_id.get_type() == DialogType::Channel &&
            td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          schedule_get_channel_difference(dialog_id, 0, message_id, 0.001, "on_get_message");
        }
        return MessageFullId();
      }
    }

    delete_update_message_id(dialog_id, message_id);

    if (!new_message->is_outgoing && dialog_id != td_->dialog_manager_->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 << "/"
                 << new_message->sender_dialog_id;
      return MessageFullId();
    }

    // must be called before delete_message
    update_reply_to_message_id(dialog_id, old_message_id, message_id, true, "on_get_message");

    being_readded_message_id_ = {dialog_id, old_message_id};
    unique_ptr<Message> old_message =
        delete_message(d, old_message_id, false, &need_update_dialog_pos, "add sent message");
    if (old_message == nullptr) {
      delete_sent_message_on_server(dialog_id, message_id, old_message_id);
      being_readded_message_id_ = MessageFullId();
      return MessageFullId();
    }
    old_message_id = old_message->message_id;

    need_update = false;

    new_message->message_id = old_message_id;
    update_message(d, old_message.get(), std::move(new_message), false);
    new_message = std::move(old_message);

    auto reply_message_full_id = new_message->replied_message_info.get_reply_message_full_id(dialog_id, false);
    auto reply_message_id = reply_message_full_id.get_message_id();
    if (reply_message_id.is_valid() && reply_message_id.is_yet_unsent()) {
      set_message_reply(d, new_message.get(), MessageInputReplyTo(), false);
    }

    new_message->message_id = message_id;
    send_update_message_send_succeeded(d, old_message_id, new_message.get(), &need_update_dialog_pos);

    if (!message_id.is_scheduled()) {
      is_sent_message = true;
    }
  }
  if (d == nullptr) {
    d = add_dialog_for_new_message(dialog_id, from_update, &need_update_dialog_pos, source);
  }

  const Message *m = add_message_to_dialog(d, std::move(new_message), false, from_update, &need_update,
                                           &need_update_dialog_pos, source);
  being_readded_message_id_ = MessageFullId();
  if (m == nullptr) {
    if (need_update_dialog_pos) {
      send_update_chat_last_message(d, "on_get_message");
    }
    if (old_message_id.is_valid() || old_message_id.is_valid_scheduled()) {
      if (!old_message_id.is_valid() || !message_id.is_valid() || old_message_id <= message_id) {
        LOG(ERROR) << "Failed to add just sent " << old_message_id << " to " << dialog_id << " as " << message_id
                   << " from " << source << ": " << debug_add_message_to_dialog_fail_reason_;
      }
      send_update_delete_messages(dialog_id, {message_id.get()}, true);
    }

    return MessageFullId();
  }

  if (need_update) {
    send_update_new_message(d, m);
  }

  if (is_sent_message) {
    try_add_active_live_location(dialog_id, m);

    // add_message_to_dialog will not update counts, because need_update == false
    update_message_count_by_index(d, +1, m);
  }

  if (is_sent_message || (need_update && !message_id.is_scheduled())) {
    update_reply_count_by_message(d, +1, m);
    update_forward_count(dialog_id, m);
  }

  if (dialog_id.get_type() == DialogType::Channel &&
      !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    auto message = delete_message(d, message_id, false, &need_update_dialog_pos, "get a message in inaccessible chat");
    CHECK(message.get() == m);
    send_update_delete_messages(dialog_id, {m->message_id.get()}, false);
    // don't need to update chat position
    return MessageFullId();
  }

  if (m->message_id.is_scheduled()) {
    send_update_chat_has_scheduled_messages(d, false);
  }

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

  // set dialog reply markup only after updateNewMessage and updateChatLastMessage are sent
  if (need_update && m->reply_markup != nullptr && !m->message_id.is_scheduled() &&
      m->reply_markup->type != ReplyMarkup::Type::InlineKeyboard && m->reply_markup->is_personal &&
      !td_->auth_manager_->is_bot()) {
    set_dialog_reply_markup(d, message_id);
  }

  if (from_update) {
    auto it = pending_created_dialogs_.find(dialog_id);
    if (it != pending_created_dialogs_.end()) {
      auto pending_created_dialog = std::move(it->second);
      pending_created_dialogs_.erase(it);

      pending_created_dialog.promise_.set_value(get_chat_object(d));
      if (!pending_created_dialog.group_invite_privacy_forbidden_user_ids_.empty()) {
        send_closure(G()->dialog_participant_manager(),
                     &DialogParticipantManager::send_update_add_chat_members_privacy_forbidden, dialog_id,
                     std::move(pending_created_dialog.group_invite_privacy_forbidden_user_ids_), "on_get_message");
      }
    }
  }

  return MessageFullId(dialog_id, message_id);
}

void MessagesManager::set_dialog_last_message_id(Dialog *d, MessageId last_message_id, const char *source,
                                                 const Message *m) {
  CHECK(!last_message_id.is_scheduled());
  CHECK(!td_->auth_manager_->is_bot());

  LOG(INFO) << "Set " << d->dialog_id << " last message to " << last_message_id << " from " << source;
  d->last_message_id = last_message_id;

  if (m != nullptr) {
    d->last_media_album_id = m->media_album_id;
  } else if (!last_message_id.is_valid()) {
    d->last_media_album_id = 0;
  } else {
    m = get_message(d, last_message_id);
    if (m == nullptr) {
      LOG(ERROR) << "Failed to find last " << last_message_id << " in " << d->dialog_id;
      d->last_media_album_id = 0;
    } else {
      d->last_media_album_id = m->media_album_id;
    }
  }
  if (!last_message_id.is_valid()) {
    auto it = dialog_suffix_load_queries_.find(d->dialog_id);
    if (it != dialog_suffix_load_queries_.end()) {
      it->second->suffix_load_first_message_id_ = MessageId();
      it->second->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");
  }
  d->pending_order = DEFAULT_ORDER;
}

void MessagesManager::set_dialog_first_database_message_id(Dialog *d, MessageId first_database_message_id,
                                                           const char *source) {
  CHECK(!first_database_message_id.is_scheduled());
  CHECK(!td_->auth_manager_->is_bot());
  if (first_database_message_id == d->first_database_message_id) {
    return;
  }

  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) {
  CHECK(!last_database_message_id.is_scheduled());
  CHECK(!td_->auth_manager_->is_bot());
  if (last_database_message_id == d->last_database_message_id) {
    return;
  }

  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::remove_dialog_newer_messages(Dialog *d, MessageId from_message_id, const char *source) {
  LOG(INFO) << "Remove messages in " << d->dialog_id << " newer than " << from_message_id << " from " << source;
  CHECK(!d->last_new_message_id.is_valid());
  CHECK(!td_->auth_manager_->is_bot());

  // the function is called to handle channel gaps, and hence must always delete all database messages,
  // even first_database_message_id and last_database_message_id are empty
  delete_all_dialog_messages_from_database(d, MessageId::max(), "remove_dialog_newer_messages");
  set_dialog_first_database_message_id(d, MessageId(), "remove_dialog_newer_messages");
  set_dialog_last_database_message_id(d, MessageId(), source);
  if (d->dialog_id.get_type() != DialogType::SecretChat && !d->is_empty) {
    d->have_full_history = false;
    d->have_full_history_source = 0;
  }
  invalidate_message_indexes(d);

  auto to_delete_message_ids = d->ordered_messages.find_newer_messages(from_message_id);
  td::remove_if(to_delete_message_ids, [](MessageId message_id) { return message_id.is_yet_unsent(); });
  if (!to_delete_message_ids.empty()) {
    LOG(INFO) << "Delete " << format::as_array(to_delete_message_ids) << " newer than " << from_message_id << " in "
              << d->dialog_id << " from " << source;

    vector<int64> deleted_message_ids;
    bool need_update_dialog_pos = false;
    for (auto message_id : to_delete_message_ids) {
      auto message = delete_message(d, message_id, false, &need_update_dialog_pos, "remove_dialog_newer_messages");
      if (message != nullptr) {
        deleted_message_ids.push_back(message->message_id.get());
      }
    }
    if (need_update_dialog_pos) {
      send_update_chat_last_message(d, "remove_dialog_newer_messages");
    }
    send_update_delete_messages(d->dialog_id, std::move(deleted_message_ids), false);
  }
}

void MessagesManager::set_dialog_last_new_message_id(Dialog *d, MessageId last_new_message_id, const char *source) {
  CHECK(!last_new_message_id.is_scheduled());
  CHECK(!td_->auth_manager_->is_bot());

  LOG_CHECK(last_new_message_id > d->last_new_message_id)
      << 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()) {
    remove_dialog_newer_messages(d, last_new_message_id, source);

    auto last_new_message = get_message(d, last_new_message_id);
    if (last_new_message != nullptr) {
      add_message_to_database(d, last_new_message, source);
      set_dialog_first_database_message_id(d, last_new_message_id, source);
      set_dialog_last_database_message_id(d, last_new_message_id, source);
      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) {
  CHECK(!last_clear_history_message_id.is_scheduled());

  if (d->last_clear_history_message_id == last_clear_history_message_id && d->last_clear_history_date == date) {
    return;
  }

  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_unread_mention_count(Dialog *d, int32 unread_mention_count) {
  CHECK(d->unread_mention_count != unread_mention_count);
  CHECK(unread_mention_count >= 0);

  d->unread_mention_count = unread_mention_count;
  d->message_count_by_index[message_search_filter_index(MessageSearchFilter::UnreadMention)] = unread_mention_count;
}

void MessagesManager::set_dialog_unread_reaction_count(Dialog *d, int32 unread_reaction_count) {
  CHECK(d->unread_reaction_count != unread_reaction_count);
  CHECK(unread_reaction_count >= 0);

  d->unread_reaction_count = unread_reaction_count;
  d->message_count_by_index[message_search_filter_index(MessageSearchFilter::UnreadReaction)] = unread_reaction_count;
}

void MessagesManager::set_dialog_is_empty(Dialog *d, const char *source) {
  LOG(INFO) << "Set " << d->dialog_id << " is_empty to true from " << source;
  CHECK(d->have_full_history);
  if (!d->is_empty && d->order != DEFAULT_ORDER) {
    td_->dialog_manager_->reload_dialog_info_full(d->dialog_id, "set_dialog_is_empty");
  }
  d->is_empty = true;

  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, max_message_id, -1, "set_dialog_is_empty");
    }
    if (d->server_unread_count != 0 || d->local_unread_count != 0) {
      set_dialog_last_read_inbox_message_id(d, MessageId::min(), 0, 0, true, "set_dialog_is_empty");
    }
  }
  if (d->unread_mention_count > 0) {
    set_dialog_unread_mention_count(d, 0);
    send_update_chat_unread_mention_count(d);
  }
  if (d->unread_reaction_count > 0) {
    set_dialog_unread_reaction_count(d, 0);
    send_update_chat_unread_reaction_count(d, "set_dialog_is_empty");
  }
  if (d->reply_markup_message_id != MessageId()) {
    set_dialog_reply_markup(d, MessageId());
  }
  std::fill(d->message_count_by_index.begin(), d->message_count_by_index.end(), 0);
  if (d->notification_info != nullptr) {
    d->notification_info->notification_id_to_message_id_.clear();
  }

  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");
  }
  d->pending_order = DEFAULT_ORDER;
  if (d->last_database_message_id.is_valid()) {
    set_dialog_first_database_message_id(d, MessageId(), "set_dialog_is_empty");
    set_dialog_last_database_message_id(d, MessageId(), "set_dialog_is_empty");
  }

  update_dialog_pos(d, source);
}

bool MessagesManager::is_dialog_pinned(DialogListId dialog_list_id, DialogId dialog_id) const {
  if (get_dialog_pinned_order(dialog_list_id, dialog_id) != DEFAULT_ORDER) {
    return true;
  }
  if (dialog_list_id.is_filter()) {
    return td_->dialog_filter_manager_->is_dialog_pinned(dialog_list_id.get_filter_id(), dialog_id);
  }
  return false;
}

int64 MessagesManager::get_dialog_pinned_order(DialogListId dialog_list_id, DialogId dialog_id) const {
  return get_dialog_pinned_order(get_dialog_list(dialog_list_id), dialog_id);
}

int64 MessagesManager::get_dialog_pinned_order(const DialogList *list, DialogId dialog_id) {
  if (list != nullptr && !list->pinned_dialog_id_orders_.empty()) {
    auto it = list->pinned_dialog_id_orders_.find(dialog_id);
    if (it != list->pinned_dialog_id_orders_.end()) {
      return it->second;
    }
  }
  return DEFAULT_ORDER;
}

bool MessagesManager::set_dialog_is_pinned(DialogId dialog_id, bool is_pinned) {
  if (td_->auth_manager_->is_bot()) {
    return false;
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  return set_dialog_is_pinned(DialogListId(d->folder_id), d, is_pinned);
}

// only removes the Dialog from the dialog list, but changes nothing in the corresponding DialogFilter
bool MessagesManager::set_dialog_is_pinned(DialogListId dialog_list_id, Dialog *d, bool is_pinned,
                                           bool need_update_dialog_lists) {
  if (td_->auth_manager_->is_bot()) {
    return false;
  }

  CHECK(d != nullptr);
  if (d->order == DEFAULT_ORDER && is_pinned) {
    // the chat can't be pinned
    return false;
  }

  auto positions = get_dialog_positions(d);
  auto *list = get_dialog_list(dialog_list_id);
  if (list == nullptr) {
    return false;
  }
  if (!list->are_pinned_dialogs_inited_) {
    return false;
  }
  auto dialog_id = d->dialog_id;
  auto is_changed_dialog = [dialog_id](const DialogDate &dialog_date) {
    return dialog_date.get_dialog_id() == dialog_id;
  };
  if (is_pinned) {
    if (!list->pinned_dialogs_.empty() && is_changed_dialog(list->pinned_dialogs_[0])) {
      return false;
    }
    auto order = get_next_pinned_dialog_order();
    DialogDate dialog_date(order, dialog_id);
    add_to_top_if(list->pinned_dialogs_, list->pinned_dialogs_.size() + 1, dialog_date, is_changed_dialog);
    auto it = list->pinned_dialog_id_orders_.find(dialog_id);
    if (it != list->pinned_dialog_id_orders_.end()) {
      CHECK(list->pinned_dialogs_[0] != dialog_date);
      list->pinned_dialogs_[0] = dialog_date;
      it->second = order;
    } else {
      CHECK(list->pinned_dialogs_[0] == dialog_date);
      list->pinned_dialog_id_orders_.emplace(dialog_id, order);
    }
  } else {
    if (!td::remove_if(list->pinned_dialogs_, is_changed_dialog)) {
      return false;
    }
    list->pinned_dialog_id_orders_.erase(dialog_id);
  }

  LOG(INFO) << "Set " << d->dialog_id << " is pinned in " << dialog_list_id << " to " << is_pinned;

  save_pinned_folder_dialog_ids(*list);

  if (need_update_dialog_lists) {
    update_dialog_lists(d, std::move(positions), true, false, "set_dialog_is_pinned");
  }
  return true;
}

void MessagesManager::save_pinned_folder_dialog_ids(const DialogList &list) const {
  if (!list.dialog_list_id.is_folder() || !G()->use_message_database()) {
    return;
  }
  G()->td_db()->get_binlog_pmc()->set(
      PSTRING() << "pinned_dialog_ids" << list.dialog_list_id.get_folder_id().get(),
      implode(transform(list.pinned_dialogs_,
                        [](auto &pinned_dialog) { return PSTRING() << pinned_dialog.get_dialog_id().get(); }),
              ','));
}

void MessagesManager::set_dialog_reply_markup(Dialog *d, MessageId message_id) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(!message_id.is_scheduled());

  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()) {
    LOG_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,
                 td_api::make_object<td_api::updateChatReplyMarkup>(
                     get_chat_id_object(d->dialog_id, "updateChatReplyMarkup"), 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;
  }

  CHECK(!m->message_id.is_scheduled());
  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, const char *source) {
  CHECK(d != nullptr);
  CHECK(!message_id.is_scheduled());
  if (d->notification_info == nullptr && message_id == MessageId()) {
    return;
  }
  CHECK(!td_->auth_manager_->is_bot());
  auto notification_info = add_dialog_notification_info(d);
  auto old_message_id = notification_info->pinned_message_notification_message_id_;
  if (old_message_id == message_id) {
    return;
  }
  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, source);
    if (m != nullptr && m->notification_id.is_valid() && is_message_notification_active(d, m)) {
      // Can't remove pinned_message_notification_message_id before the call,
      // because the notification needs to be still active inside remove_message_notification_id
      remove_message_notification_id(d, m, true, false, true);
      on_message_changed(d, m, false, source);
    } else {
      send_closure_later(G()->notification_manager(), &NotificationManager::remove_temporary_notification_by_object_id,
                         notification_info->mention_notification_group_.get_group_id(), old_message_id, false, source);
    }
  }
  notification_info->pinned_message_notification_message_id_ = message_id;
  on_dialog_updated(d->dialog_id, source);
}

void MessagesManager::remove_dialog_pinned_message_notification(Dialog *d, const char *source) {
  set_dialog_pinned_message_notification(d, MessageId(), source);
}

void MessagesManager::remove_scope_pinned_message_notifications(NotificationSettingsScope scope) {
  VLOG(notifications) << "Remove pinned message notifications in " << scope;
  dialogs_.foreach([&](const DialogId &dialog_id, unique_ptr<Dialog> &dialog) {
    Dialog *d = dialog.get();
    if (d->notification_settings.use_default_disable_pinned_message_notifications && d->notification_info != nullptr &&
        d->notification_info->mention_notification_group_.is_valid() &&
        d->notification_info->pinned_message_notification_message_id_.is_valid() &&
        td_->dialog_manager_->get_dialog_notification_setting_scope(dialog_id) == scope) {
      remove_dialog_pinned_message_notification(d, "remove_scope_pinned_message_notifications");
    }
  });
}

void MessagesManager::on_update_scope_mention_notifications(NotificationSettingsScope scope,
                                                            bool disable_mention_notifications) {
  VLOG(notifications) << "Remove mention notifications in " << scope;
  dialogs_.foreach([&](const DialogId &dialog_id, unique_ptr<Dialog> &dialog) {
    Dialog *d = dialog.get();
    if (d->notification_settings.use_default_disable_mention_notifications &&
        td_->dialog_manager_->get_dialog_notification_setting_scope(dialog_id) == scope) {
      if (!disable_mention_notifications) {
        update_dialog_mention_notification_count(d);
      } else {
        remove_dialog_mention_notifications(d);
      }
    }
  });
}

void MessagesManager::remove_dialog_mention_notifications(Dialog *d) {
  if (d->notification_info == nullptr || !d->notification_info->mention_notification_group_.is_valid() ||
      d->unread_mention_count == 0) {
    return;
  }
  CHECK(!d->being_added_message_id.is_valid());

  VLOG(notifications) << "Remove mention notifications in " << d->dialog_id;

  auto message_ids = find_dialog_messages(d, [](const Message *m) { return m->contains_unread_mention; });
  VLOG(notifications) << "Found unread mentions in " << message_ids;
  FlatHashSet<NotificationId, NotificationIdHash> removed_notification_ids_set;
  for (auto &message_id : message_ids) {
    auto m = get_message(d, message_id);
    CHECK(m != nullptr);
    CHECK(m->message_id.is_valid());
    if (m->notification_id.is_valid() && is_message_notification_active(d, m) &&
        is_from_mention_notification_group(m)) {
      removed_notification_ids_set.insert(m->notification_id);
    }
  }

  auto notification_group_id = d->notification_info->mention_notification_group_.get_group_id();
  CHECK(notification_group_id.is_valid());
  message_ids = td_->notification_manager_->get_notification_group_message_ids(notification_group_id);
  VLOG(notifications) << "Found active mention notifications in " << message_ids;
  for (auto &message_id : message_ids) {
    CHECK(!message_id.is_scheduled());
    if (message_id != d->notification_info->pinned_message_notification_message_id_) {
      auto m = get_message_force(d, message_id, "remove_dialog_mention_notifications");
      if (m != nullptr && m->notification_id.is_valid()) {
        CHECK(is_from_mention_notification_group(m));
        if (is_message_notification_active(d, m)) {
          removed_notification_ids_set.insert(m->notification_id);
        }
      }
    }
  }

  vector<NotificationId> removed_notification_ids;
  for (auto notification_id : removed_notification_ids_set) {
    removed_notification_ids.push_back(notification_id);
  }
  for (size_t i = 0; i < removed_notification_ids.size(); i++) {
    send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification, notification_group_id,
                       removed_notification_ids[i], false, i + 1 == removed_notification_ids.size(), Promise<Unit>(),
                       "remove_dialog_mention_notifications");
  }
}

void 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.set_last_notification(last_notification_date, last_notification_id, source)) {
    on_dialog_updated(dialog_id, "set_dialog_last_notification");
  }
}

void MessagesManager::set_dialog_last_notification_checked(DialogId dialog_id, NotificationGroupInfo &group_info,
                                                           int32 last_notification_date,
                                                           NotificationId last_notification_id, const char *source) {
  bool is_changed = group_info.set_last_notification(last_notification_date, last_notification_id, source);
  CHECK(is_changed);
  on_dialog_updated(dialog_id, "set_dialog_last_notification_checked");
}

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 message_full_id = it->second;
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog(dialog_id);
  auto m = get_message_force(d, message_full_id.get_message_id(), "on_update_sent_text_message");
  if (m == nullptr) {
    // message has already been deleted
    return;
  }
  CHECK(m->message_id.is_yet_unsent());
  message_full_id = MessageFullId(dialog_id, m->message_id);

  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), true, td_->auth_manager_->is_bot(),
      get_message_original_date(m), m->media_album_id != 0, "on_update_sent_text_message");
  auto new_content = get_message_content(td_, std::move(new_message_text), std::move(message_media), dialog_id, m->date,
                                         true /*likely ignored*/, UserId() /*likely ignored*/, nullptr /*ignored*/,
                                         nullptr, "on_update_sent_text_message");
  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);
  compare_message_contents(td_, m->content.get(), new_content.get(), is_content_changed, need_update);

  if (is_content_changed || need_update) {
    reregister_message_content(td_, m->content.get(), new_content.get(), message_full_id,
                               "on_update_sent_text_message");
    m->content = std::move(new_content);
    m->is_content_secret = m->ttl.is_secret_message_content(MessageContentType::Text);

    if (need_update) {
      send_update_message_content(d, m, true, "on_update_sent_text_message");
    }
    on_message_changed(d, m, need_update, "on_update_sent_text_message");
  }
}

void MessagesManager::delete_pending_message_web_page(MessageFullId message_full_id) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  Message *m = get_message(d, message_full_id.get_message_id());
  CHECK(m != nullptr);

  MessageContent *content = m->content.get();
  CHECK(has_message_content_web_page(content));
  unregister_message_content(td_, content, message_full_id, "delete_pending_message_web_page");
  remove_message_content_web_page(content);
  register_message_content(td_, content, message_full_id, "delete_pending_message_web_page");

  // don't need to send an updateMessageContent, because the web page was pending

  on_message_changed(d, m, false, "delete_pending_message_web_page");
}

void MessagesManager::on_get_dialogs(FolderId folder_id, vector<tl_object_ptr<telegram_api::Dialog>> &&dialog_folders,
                                     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{folder_id, std::move(dialog_folders), 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 && dialog_folders.size() == 1 && dialog_folders[0]->get_id() == telegram_api::dialog::ID) {
    DialogId dialog_id(static_cast<const telegram_api::dialog *>(dialog_folders[0].get())->peer_);
    if (dialog_id.is_valid() && 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{folder_id, std::move(dialog_folders), total_count, std::move(messages),
                                         std::move(promise)});
      return;
    }
  }

  vector<tl_object_ptr<telegram_api::dialog>> dialogs;
  for (auto &dialog_folder : dialog_folders) {
    switch (dialog_folder->get_id()) {
      case telegram_api::dialog::ID:
        dialogs.push_back(telegram_api::move_object_as<telegram_api::dialog>(dialog_folder));
        break;
      case telegram_api::dialogFolder::ID: {
        auto folder = telegram_api::move_object_as<telegram_api::dialogFolder>(dialog_folder);
        if (from_pinned_dialog_list) {
          // TODO update unread_muted_peers_count:int unread_unmuted_peers_count:int
          // unread_muted_messages_count:int unread_unmuted_messages_count:int
          FolderId folder_folder_id(folder->folder_->id_);
          if (folder_folder_id == FolderId::archive()) {
            // archive is expected in pinned dialogs list
            break;
          }
        }
        LOG(ERROR) << "Receive unexpected " << to_string(folder);
        break;
      }
      default:
        UNREACHABLE();
    }
  }

  const char *source = nullptr;
  if (from_get_dialog) {
    LOG(INFO) << "Process " << dialogs.size() << " chats";
    source = "get chat";
  } else if (from_pinned_dialog_list) {
    LOG(INFO) << "Process " << dialogs.size() << " pinned chats in " << folder_id;
    source = "get pinned chats";
  } else {
    LOG(INFO) << "Process " << dialogs.size() << " chats out of " << total_count << " in " << folder_id;
    source = "get chat list";
  }
  FlatHashMap<MessageFullId, DialogDate, MessageFullIdHash> message_full_id_to_dialog_date;
  FlatHashMap<MessageFullId, tl_object_ptr<telegram_api::Message>, MessageFullIdHash> message_full_id_to_message;
  for (auto &message : messages) {
    auto message_full_id = MessageFullId::get_message_full_id(message, false);
    if (!message_full_id.get_message_id().is_valid()) {  // must not check dialog_id because of messageEmpty
      continue;
    }
    if (from_dialog_list) {
      auto message_date = get_message_date(message);
      int64 order = get_dialog_order(message_full_id.get_message_id(), message_date);
      message_full_id_to_dialog_date.emplace(message_full_id, DialogDate(order, message_full_id.get_dialog_id()));
    }
    message_full_id_to_message[message_full_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()) {
        MessageFullId message_full_id(dialog_id, last_message_id);
        auto it = message_full_id_to_dialog_date.find(message_full_id);
        if (it == message_full_id_to_dialog_date.end() && dialog_id.get_type() != DialogType::Channel) {
          it = message_full_id_to_dialog_date.find({DialogId(), last_message_id});
        }
        if (it == message_full_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"));
        }
        FolderId dialog_folder_id(dialog->folder_id_);
        if (dialog_folder_id != folder_id) {
          LOG(ERROR) << "Receive " << dialog_id << " in " << dialog_folder_id << " instead of " << folder_id;
          continue;
        }

        DialogDate dialog_date = it->second;
        if (dialog_date.get_date() > 0 && dialog_date.get_dialog_id() == dialog_id && 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 && total_count < narrow_cast<int32>(dialogs.size())) {
    LOG(ERROR) << "Receive chat total_count = " << total_count << ", but " << dialogs.size() << " chats";
    total_count = narrow_cast<int32>(dialogs.size());
  }

  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
      total_count--;
      continue;
    }

    DialogId dialog_id(dialog->peer_);
    if (td::contains(added_dialog_ids, dialog_id)) {
      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, source);
    bool need_update_dialog_pos = false;
    CHECK(!being_added_dialog_id_.is_valid());
    being_added_dialog_id_ = dialog_id;
    if (d == nullptr) {
      d = add_dialog(dialog_id, source);
      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();
    auto positions = get_dialog_positions(d);

    set_dialog_folder_id(d, FolderId(dialog->folder_id_));
    if (dialog_id.get_type() == DialogType::Channel) {
      set_dialog_view_as_messages(d, dialog->view_forum_as_messages_, "on_get_dialogs");
    }

    on_update_dialog_notify_settings(dialog_id, std::move(dialog->notify_settings_), source);
    if (!d->notification_settings.is_synchronized && !td_->auth_manager_->is_bot()) {
      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;
    }
    if (dialog->unread_reactions_count_ < 0) {
      LOG(ERROR) << "Receive " << dialog->unread_reactions_count_ << " as number of messages with unread reactions in "
                 << dialog_id;
      dialog->unread_reactions_count_ = 0;
    }
    if (dialog->ttl_period_ < 0) {
      LOG(ERROR) << "Receive " << dialog->ttl_period_ << " as message auto-delete time in " << dialog_id;
      dialog->ttl_period_ = 0;
    }
    if (!td_->auth_manager_->is_bot()) {
      const char *reload_source = nullptr;
      if (!d->is_is_blocked_for_stories_inited) {
        reload_source = "on_get_dialogs init is_blocked_for_stories";
      } else if (!d->is_has_bots_inited) {
        reload_source = "on_get_dialogs init has_bots";
      } else if (!d->is_background_inited) {
        reload_source = "on_get_dialogs init background";
      } else if (!d->is_theme_name_inited) {
        reload_source = "on_get_dialogs init theme_name";
      } else if (!d->is_available_reactions_inited) {
        reload_source = "on_get_dialogs init available_reactions";
      } else if (!d->is_last_pinned_message_id_inited) {
        get_dialog_pinned_message(dialog_id, Auto());
      }
      if (reload_source != nullptr) {
        // asynchronously get dialog info from the server
        // TODO add is_blocked/is_blocked_for_stories/has_bots/background/theme_name/available_reactions
        // to telegram_api::dialog
        td_->dialog_manager_->reload_dialog_info_full(dialog_id, reload_source);
      }
    }

    need_update_dialog_pos |=
        update_dialog_draft_message(d, get_draft_message(td_, 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() && !td_->auth_manager_->is_bot()) {
        MessageFullId message_full_id(dialog_id, last_message_id);
        auto it = message_full_id_to_message.find(message_full_id);
        if (it == message_full_id_to_message.end() && dialog_id.get_type() != DialogType::Channel) {
          it = message_full_id_to_message.find({DialogId(), last_message_id});
        }
        if (it == message_full_id_to_message.end()) {
          LOG(ERROR) << "Last " << message_full_id << " not found";
        } else if (!has_pts || d->pts == 0 || dialog->pts_ <= d->pts || d->is_channel_difference_finished) {
          auto added_message_full_id = on_get_message(std::move(it->second), false, has_pts, false, source);
          message_full_id_to_message.erase(it);
          CHECK(d->last_new_message_id == MessageId());
          set_dialog_last_new_message_id(d, last_message_id, source);
          if (d->last_new_message_id > d->last_message_id && added_message_full_id.get_message_id().is_valid()) {
            CHECK(added_message_full_id.get_message_id() == d->last_new_message_id);
            set_dialog_last_message_id(d, d->last_new_message_id, source);
            send_update_chat_last_message(d, source);
          }
        } else if (dialog_id.get_type() == DialogType::Channel) {
          get_channel_difference(dialog_id, d->pts, dialog->pts_, last_message_id, true, source);
        }
      }

      if (has_pts && !running_get_channel_difference(dialog_id)) {
        set_channel_pts(d, dialog->pts_, source);
      }
    }
    bool is_marked_as_unread = dialog->unread_mark_;
    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, source);
    }

    if (!td_->auth_manager_->is_bot() && !from_pinned_dialog_list) {
      // set is_pinned only after updating chat position to ensure that order is initialized
      bool is_pinned = (dialog->flags_ & DIALOG_FLAG_IS_PINNED) != 0;
      bool was_pinned = is_dialog_pinned(DialogListId(d->folder_id), dialog_id);
      if (is_pinned != was_pinned) {
        set_dialog_is_pinned(DialogListId(d->folder_id), d, is_pinned);
      }
    }

    if (!G()->use_message_database() || is_new || !d->is_last_read_inbox_message_id_inited ||
        d->need_repair_server_unread_count) {
      if (d->last_read_inbox_message_id.is_valid() && !d->last_read_inbox_message_id.is_server() &&
          read_inbox_max_message_id == d->last_read_inbox_message_id.get_prev_server_message_id()) {
        read_inbox_max_message_id = d->last_read_inbox_message_id;
      }
      if (d->need_repair_server_unread_count &&
          (d->last_read_inbox_message_id <= read_inbox_max_message_id || !need_unread_counter(d->order) ||
           !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read))) {
        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, "repaired dialog server unread count");
      }
      if (d->need_repair_server_unread_count) {
        auto &previous_message_id = previous_repaired_read_inbox_max_message_id_[dialog_id];
        if (previous_message_id >= read_inbox_max_message_id) {
          // protect from sending the request in a loop
          if (d->server_unread_count != 0) {
            LOG(ERROR) << "Failed to repair server unread count in " << dialog_id
                       << ", because receive read_inbox_max_message_id = " << read_inbox_max_message_id << " after "
                       << previous_message_id << ", but messages are read up to " << d->last_read_inbox_message_id;
          } else {
            LOG(INFO) << "Failed to repair server unread count in " << dialog_id
                      << ", because receive read_inbox_max_message_id = " << read_inbox_max_message_id
                      << ", but messages are read up to " << d->last_read_inbox_message_id
                      << ". Likely all messages after " << read_inbox_max_message_id << " are outgoing";
          }
          d->need_repair_server_unread_count = false;
          on_dialog_updated(dialog_id, "failed to repair dialog server unread count");
        } else {
          LOG(INFO) << "Have last_read_inbox_message_id = " << d->last_read_inbox_message_id << ", but received only "
                    << read_inbox_max_message_id << " from the server, trying to repair server unread count again";
          previous_message_id = read_inbox_max_message_id;
          repair_server_unread_count(dialog_id, d->server_unread_count, source);
        }
      }
      if (!d->need_repair_server_unread_count) {
        previous_repaired_read_inbox_max_message_id_.erase(dialog_id);
      }
      if ((d->server_unread_count != dialog->unread_count_ &&
           d->last_read_inbox_message_id == read_inbox_max_message_id) ||
          d->last_read_inbox_message_id < read_inbox_max_message_id) {
        set_dialog_last_read_inbox_message_id(d, read_inbox_max_message_id, dialog->unread_count_,
                                              d->local_unread_count, true, source);
      }
      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()->use_message_database() || is_new || !d->is_last_read_outbox_message_id_inited) {
      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);
      }
      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()->use_message_database() || is_new || d->need_repair_unread_mention_count) {
      if (d->need_repair_unread_mention_count) {
        if (d->unread_mention_count != dialog->unread_mentions_count_) {
          LOG(INFO) << "Repaired unread mention count in " << dialog_id << " from " << d->unread_mention_count << " to "
                    << dialog->unread_mentions_count_;
        }
        d->need_repair_unread_mention_count = false;
        on_dialog_updated(dialog_id, "repaired dialog unread mention count");
      }
      if (d->unread_mention_count != dialog->unread_mentions_count_ && !td_->auth_manager_->is_bot()) {
        set_dialog_unread_mention_count(d, dialog->unread_mentions_count_);
        update_dialog_mention_notification_count(d);
        send_update_chat_unread_mention_count(d);
      }
    }
    if (!G()->use_message_database() || is_new || d->need_repair_unread_reaction_count) {
      if (d->need_repair_unread_reaction_count) {
        if (d->unread_reaction_count != dialog->unread_reactions_count_) {
          LOG(INFO) << "Repaired unread reaction count in " << dialog_id << " from " << d->unread_reaction_count
                    << " to " << dialog->unread_reactions_count_;
        }
        d->need_repair_unread_reaction_count = false;
        on_dialog_updated(dialog_id, "repaired dialog unread reaction count");
      }
      if (d->unread_reaction_count != dialog->unread_reactions_count_ && !td_->auth_manager_->is_bot()) {
        set_dialog_unread_reaction_count(d, dialog->unread_reactions_count_);
        // update_dialog_reaction_notification_count(d);
        send_update_chat_unread_reaction_count(d, source);
      }
    }

    set_dialog_message_ttl(d, MessageTtl(dialog->ttl_period_));

    being_added_dialog_id_ = DialogId();

    update_dialog_lists(d, std::move(positions), true, false, source);

    if ((from_dialog_list || from_pinned_dialog_list) && d->order == DEFAULT_ORDER) {
      load_last_dialog_message(d, "on_get_dialog");
    }
  }

  if (from_dialog_list) {
    CHECK(!td_->auth_manager_->is_bot());
    CHECK(total_count >= 0);

    auto &folder_list = add_dialog_list(DialogListId(folder_id));
    if (folder_list.server_dialog_total_count_ != total_count) {
      auto old_dialog_total_count = get_dialog_total_count(folder_list);
      folder_list.server_dialog_total_count_ = total_count;
      if (folder_list.is_dialog_unread_count_inited_) {
        if (old_dialog_total_count != get_dialog_total_count(folder_list)) {
          send_update_unread_chat_count(folder_list, DialogId(), true, source);
        } else {
          save_unread_chat_count(folder_list);
        }
      }
    }

    auto *folder = get_dialog_folder(folder_id);
    CHECK(folder != nullptr);
    if (dialogs.empty()) {
      // if there are no more dialogs on the server
      max_dialog_date = MAX_DIALOG_DATE;
    }
    if (folder->last_server_dialog_date_ < max_dialog_date) {
      folder->last_server_dialog_date_ = max_dialog_date;
      update_last_dialog_date(folder_id);
    } else if (promise) {
      LOG(ERROR) << "Last server dialog date didn't increased from " << folder->last_server_dialog_date_ << " to "
                 << max_dialog_date << " after receiving " << dialogs.size() << " chats " << added_dialog_ids
                 << " from " << total_count << " in " << folder_id
                 << ". last_dialog_date = " << folder->folder_last_dialog_date_
                 << ", last_loaded_database_dialog_date = " << folder->last_loaded_database_dialog_date_;
    }
  }
  if (from_pinned_dialog_list) {
    CHECK(!td_->auth_manager_->is_bot());
    auto *folder_list = get_dialog_list(DialogListId(folder_id));
    CHECK(folder_list != nullptr);
    auto pinned_dialog_ids = DialogId::remove_secret_chat_dialog_ids(get_pinned_dialog_ids(DialogListId(folder_id)));
    bool are_pinned_dialogs_saved = folder_list->are_pinned_dialogs_inited_;
    folder_list->are_pinned_dialogs_inited_ = true;
    if (pinned_dialog_ids != added_dialog_ids) {
      if (set_folder_pinned_dialogs(folder_id, std::move(pinned_dialog_ids), std::move(added_dialog_ids))) {
        are_pinned_dialogs_saved = true;
      }
    } else {
      LOG(INFO) << "Pinned chats are not changed";
    }
    update_list_last_pinned_dialog_date(*folder_list);

    if (!are_pinned_dialogs_saved && G()->use_message_database()) {
      LOG(INFO) << "Save empty pinned chat list in " << folder_id;
      G()->td_db()->get_binlog_pmc()->set(PSTRING() << "pinned_dialog_ids" << folder_id.get(), "");
    }
  }
  promise.set_value(Unit());
}

bool MessagesManager::is_message_unload_enabled() const {
  return G()->use_message_database() || td_->auth_manager_->is_bot();
}

bool MessagesManager::can_unload_message(const Dialog *d, const Message *m) const {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  CHECK(m->message_id.is_valid());
  MessageFullId message_full_id{d->dialog_id, m->message_id};
  if (td_->auth_manager_->is_bot() && !G()->use_message_database()) {
    return !m->message_id.is_yet_unsent() && replied_by_yet_unsent_messages_.count(message_full_id) == 0 &&
           m->edited_content == nullptr && m->message_id != d->last_pinned_message_id &&
           m->message_id != d->last_edited_message_id;
  }
  // 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 message with active reply markup
  // don't want to unload the newest pinned message
  // don't want to unload last edited message, because server can send updateEditChannelMessage again
  // don't want to unload messages from the last album
  // 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
  {
    auto it = dialog_suffix_load_queries_.find(d->dialog_id);
    if (it != dialog_suffix_load_queries_.end() && !it->second->suffix_load_queries_.empty()) {
      return false;
    }
  }
  return d->open_count == 0 && m->message_id != d->last_message_id && m->message_id != d->last_database_message_id &&
         !m->message_id.is_yet_unsent() && active_live_location_message_full_ids_.count(message_full_id) == 0 &&
         replied_by_yet_unsent_messages_.count(message_full_id) == 0 && m->edited_content == nullptr &&
         m->message_id != d->reply_markup_message_id && m->message_id != d->last_pinned_message_id &&
         m->message_id != d->last_edited_message_id &&
         (m->media_album_id != d->last_media_album_id || m->media_album_id == 0);
}

unique_ptr<MessagesManager::Message> MessagesManager::unload_message(Dialog *d, MessageId message_id) {
  CHECK(d != nullptr);
  CHECK(message_id.is_valid());
  bool need_update_dialog_pos = false;
  auto message = do_delete_message(d, message_id, false, true, &need_update_dialog_pos, "unload_message");
  CHECK(!need_update_dialog_pos);
  return message;
}

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 || message_id.is_yet_unsent());
  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;
    if (random_id != 0) {
      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 || message_id.is_yet_unsent());
  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(NotificationInfo *notification_info,
                                                                       NotificationId notification_id,
                                                                       MessageId message_id) {
  CHECK(notification_info != nullptr);
  CHECK(notification_id.is_valid());
  CHECK(message_id.is_valid());
  auto it = notification_info->notification_id_to_message_id_.find(notification_id);
  if (it == notification_info->notification_id_to_message_id_.end()) {
    VLOG(notifications) << "Add correspondence from " << notification_id << " to " << message_id;
    notification_info->notification_id_to_message_id_.emplace(notification_id, message_id);
  } else if (it->second != message_id) {
    LOG(ERROR) << "Have the same " << notification_id << " for " << message_id << " and " << it->second;
    if (it->second < message_id) {
      it->second = message_id;
    }
  }
}

void MessagesManager::delete_notification_id_to_message_id_correspondence(NotificationInfo *notification_info,
                                                                          NotificationId notification_id,
                                                                          MessageId message_id) {
  CHECK(notification_info != nullptr);
  CHECK(notification_id.is_valid());
  CHECK(message_id.is_valid());
  auto it = notification_info->notification_id_to_message_id_.find(notification_id);
  if (it != notification_info->notification_id_to_message_id_.end() && it->second == message_id) {
    VLOG(notifications) << "Delete correspondence from " << notification_id << " to " << message_id;
    notification_info->notification_id_to_message_id_.erase(it);
  } else {
    LOG(ERROR) << "Can't find " << notification_id << " from " << message_id;
  }
}

bool MessagesManager::is_notification_info_group_id(const NotificationInfo *notification_info,
                                                    NotificationGroupId group_id) {
  if (!group_id.is_valid() || notification_info == nullptr) {
    return false;
  }
  return notification_info->message_notification_group_.has_group_id(group_id) ||
         notification_info->mention_notification_group_.has_group_id(group_id);
}

bool MessagesManager::is_dialog_notification_group_id(const Dialog *d, NotificationGroupId group_id) {
  if (d == nullptr) {
    return false;
  }
  return is_notification_info_group_id(d->notification_info.get(), group_id);
}

void MessagesManager::remove_message_notification_id(Dialog *d, Message *m, bool is_permanent, bool force_update,
                                                     bool ignore_pinned_message_notification_removal) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  CHECK(m->message_id.is_valid());
  if (!m->notification_id.is_valid()) {
    return;
  }

  auto from_mentions = is_from_mention_notification_group(m);
  auto &group_info = get_notification_group_info(d, m);
  if (!group_info.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.get_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_info.get(), notification_id, m->message_id);
  m->removed_notification_id = m->notification_id;
  m->notification_id = NotificationId();
  if (d->notification_info->pinned_message_notification_message_id_ == m->message_id && is_permanent &&
      !ignore_pinned_message_notification_removal) {
    remove_dialog_pinned_message_notification(
        d, "remove_message_notification_id");  // must be called after notification_id is removed
  }
  if (group_info.get_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.get_group_id(), notification_id, is_permanent, force_update, Promise<Unit>(),
                         "remove_message_notification_id");
    }

    // 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);
  CHECK(d->notification_info != nullptr);
  auto notification_id = d->notification_info->new_secret_chat_notification_id_;
  CHECK(notification_id.is_valid());
  VLOG(notifications) << "Remove " << notification_id << " about new secret " << d->dialog_id << " from "
                      << d->notification_info->message_notification_group_.get_group_id();
  d->notification_info->new_secret_chat_notification_id_ = NotificationId();
  set_dialog_last_notification_checked(d->dialog_id, d->notification_info->message_notification_group_, 0,
                                       NotificationId(), "remove_new_secret_chat_notification");
  if (is_permanent) {
    CHECK(d->notification_info->message_notification_group_.is_valid());
    send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification,
                       d->notification_info->message_notification_group_.get_group_id(), notification_id, true, true,
                       Promise<Unit>(), "remove_new_secret_chat_notification");
  }
}

void MessagesManager::fix_dialog_last_notification_id(Dialog *d, bool from_mentions, MessageId message_id) {
  CHECK(d != nullptr);
  CHECK(!message_id.is_scheduled());
  if (d->notification_info == nullptr) {
    return;
  }
  CHECK(!td_->auth_manager_->is_bot());
  auto &group_info = get_notification_group_info(d, from_mentions);
  CHECK(group_info.is_valid());
  auto it = d->ordered_messages.get_const_iterator(message_id);
  VLOG(notifications) << "Trying to fix last notification identifier in " << group_info.get_group_id() << " from "
                      << d->dialog_id << " from " << message_id << '/' << group_info.get_last_notification_id();
  if (*it != nullptr && ((*it)->get_message_id() == message_id || (*it)->have_next())) {
    while (*it != nullptr) {
      const Message *m = get_message(d, (*it)->get_message_id());
      if (is_from_mention_notification_group(m) == from_mentions && m->notification_id.is_valid() &&
          is_message_notification_active(d, m) && m->message_id != message_id) {
        set_dialog_last_notification(d->dialog_id, group_info, m->date, m->notification_id,
                                     "fix_dialog_last_notification_id");
        return;
      }
      --it;
    }
  }
  if (G()->use_message_database()) {
    get_message_notifications_from_database(
        d->dialog_id, group_info.get_group_id(), group_info.get_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.get_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);
  if (d->notification_info == nullptr) {
    return;
  }
  auto &group_info = get_notification_group_info(d, from_mentions);
  if (!group_info.is_valid()) {
    return;
  }
  VLOG(notifications) << "Receive " << result.ok().size() << " message notifications in " << group_info.get_group_id()
                      << '/' << dialog_id << " from " << prev_last_notification_id;
  if (group_info.get_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;
  }

  set_dialog_last_notification(dialog_id, group_info, last_notification_date, last_notification_id,
                               "do_fix_dialog_last_notification_id");
}

// DO NOT FORGET TO ADD ALL CHANGES OF THIS FUNCTION AS WELL TO 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) {
  CHECK(d != nullptr);
  if (!message_id.is_valid()) {
    if (message_id.is_valid_scheduled()) {
      return do_delete_scheduled_message(d, message_id, is_permanently_deleted, source);
    }

    LOG(ERROR) << "Trying to delete " << message_id << " in " << d->dialog_id << " from " << source;
    return nullptr;
  }

  MessageFullId message_full_id(d->dialog_id, message_id);
  const Message *m = get_message(d, message_id);
  if (m == nullptr) {
    if (only_from_memory) {
      return nullptr;
    }

    LOG(INFO) << message_id << " is not found in " << d->dialog_id << " to be deleted from " << source;
    m = get_message_force(d, message_id, "do_delete_message");
    if (m == 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, source);

      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 > d->last_read_inbox_message_id && !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;
        } else {
          unread_count--;
          set_dialog_last_read_inbox_message_id(d, MessageId::min(), server_unread_count, local_unread_count, false,
                                                source);
        }
      }
      */
      return nullptr;
    }
  }

  if (only_from_memory && !can_unload_message(d, m)) {
    return nullptr;
  }

  LOG_CHECK(!d->being_deleted_message_id.is_valid())
      << d->being_deleted_message_id << " " << message_id << " " << source;
  d->being_deleted_message_id = message_id;

  bool need_get_history = false;
  if (!only_from_memory) {
    LOG(INFO) << "Deleting " << message_full_id << " from " << source;

    delete_message_from_database(d, message_id, m, is_permanently_deleted, source);

    delete_active_live_location(d->dialog_id, m);
    remove_message_file_sources(d->dialog_id, m);

    if (message_id == d->last_message_id) {
      auto it = d->ordered_messages.get_const_iterator(message_id);
      CHECK(*it != nullptr);
      CHECK((*it)->get_message_id() == message_id);
      --it;
      if (*it != nullptr) {
        set_dialog_last_message_id(d, (*it)->get_message_id(), "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) {
      auto it = d->ordered_messages.get_const_iterator(message_id);
      CHECK(*it != nullptr);
      CHECK((*it)->get_message_id() == message_id);
      do {
        --it;
      } while (*it != nullptr && (*it)->get_message_id().is_yet_unsent());

      if (*it != nullptr) {
        if ((*it)->get_message_id() < d->first_database_message_id && d->dialog_id.get_type() == DialogType::Channel) {
          // possible if messages was deleted from database, but not from memory after updateChannelTooLong
          set_dialog_last_database_message_id(d, MessageId(), "do_delete_message 1");
        } else {
          set_dialog_last_database_message_id(d, (*it)->get_message_id(), "do_delete_message 2");
          if (d->last_database_message_id < d->first_database_message_id) {
            LOG(ERROR) << "Last database " << d->last_database_message_id << " became less than first database "
                       << d->first_database_message_id << " after deletion of " << message_full_id;
            set_dialog_first_database_message_id(d, d->last_database_message_id, "do_delete_message 2");
          }
        }
      } else {
        if (d->first_database_message_id == d->last_database_message_id) {
          // database definitely has no more messages
          set_dialog_last_database_message_id(d, MessageId(), "do_delete_message 3");
        } else {
          LOG(INFO) << "Need to get history to repair last_database_message_id in " << d->dialog_id;
          need_get_history = true;
        }
      }
    }
    if (d->last_database_message_id.is_valid()) {
      CHECK(d->first_database_message_id.is_valid());
    } else if (d->first_database_message_id.is_valid()) {
      set_dialog_first_database_message_id(d, MessageId(), "do_delete_message");
    }

    auto suffix_load_queries_it = dialog_suffix_load_queries_.find(d->dialog_id);
    if (suffix_load_queries_it != dialog_suffix_load_queries_.end() &&
        message_id == suffix_load_queries_it->second->suffix_load_first_message_id_) {
      auto it = d->ordered_messages.get_const_iterator(message_id);
      CHECK(*it != nullptr);
      CHECK((*it)->get_message_id() == message_id);
      --it;
      if (*it != nullptr) {
        suffix_load_queries_it->second->suffix_load_first_message_id_ = (*it)->get_message_id();
      } else {
        suffix_load_queries_it->second->suffix_load_first_message_id_ = MessageId();
        suffix_load_queries_it->second->suffix_load_done_ = false;
      }
    }
  } else {
    auto suffix_load_queries_it = dialog_suffix_load_queries_.find(d->dialog_id);
    if (suffix_load_queries_it != dialog_suffix_load_queries_.end() &&
        message_id >= suffix_load_queries_it->second->suffix_load_first_message_id_) {
      suffix_load_queries_it->second->suffix_load_first_message_id_ = MessageId();
      suffix_load_queries_it->second->suffix_load_done_ = false;
    }
  }

  auto result = std::move(d->messages[message_id]);
  CHECK(m == result.get());
  d->messages.erase(message_id);

  static_cast<ListNode *>(result.get())->remove();

  if (!td_->auth_manager_->is_bot()) {
    d->ordered_messages.erase(message_id, only_from_memory);
  }

  d->being_deleted_message_id = MessageId();

  if (need_get_history) {
    send_closure_later(actor_id(this), &MessagesManager::load_last_dialog_message_later, d->dialog_id);
  }

  on_message_deleted(d, result.get(), is_permanently_deleted, source);

  return result;
}

void MessagesManager::on_message_deleted_from_database(Dialog *d, const Message *m, const char *source) {
  CHECK(d != nullptr);
  if (m == nullptr || m->message_id.is_scheduled() || td_->auth_manager_->is_bot()) {
    return;
  }

  auto message_id = m->message_id;
  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 (has_incoming_notification(d->dialog_id, m) && message_id > d->last_read_inbox_message_id &&
      d->is_last_read_inbox_message_id_inited) {
    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) {
      if (need_unread_counter(d->order)) {
        LOG(ERROR) << "Unread count became negative in " << d->dialog_id << " after deletion of " << message_id
                   << ". Last read is " << d->last_read_inbox_message_id;
      }
    } else {
      unread_count--;
      set_dialog_last_read_inbox_message_id(d, MessageId::min(), server_unread_count, local_unread_count, false,
                                            source);
    }
  }
  if (m->contains_unread_mention) {
    if (d->unread_mention_count == 0) {
      if (is_dialog_inited(d)) {
        LOG(ERROR) << "Unread mention count became negative in " << d->dialog_id << " after deletion of " << message_id;
      }
    } else {
      set_dialog_unread_mention_count(d, d->unread_mention_count - 1);
      send_update_chat_unread_mention_count(d);
    }
  }
  if (has_unread_message_reactions(d->dialog_id, m)) {
    if (d->unread_reaction_count == 0) {
      if (is_dialog_inited(d)) {
        LOG(ERROR) << "Unread reaction count became negative in " << d->dialog_id << " after deletion of "
                   << message_id;
      }
    } else {
      set_dialog_unread_reaction_count(d, d->unread_reaction_count - 1);
      send_update_chat_unread_reaction_count(d, source);
    }
  }

  update_message_count_by_index(d, -1, m);
  update_reply_count_by_message(d, -1, m);

  td_->reaction_manager_->update_saved_messages_tags(m->saved_messages_topic_id, get_chosen_tags(m->reactions), {});
}

void MessagesManager::on_message_deleted(Dialog *d, Message *m, bool is_permanently_deleted, const char *source) {
  // also called for unloaded messages, but not for scheduled messages
  CHECK(m->message_id.is_valid());

  if (m->message_id.is_yet_unsent() && !m->message_id.is_scheduled() && m->top_thread_message_id.is_valid() &&
      !td_->auth_manager_->is_bot()) {
    auto it = yet_unsent_thread_message_ids_.find({d->dialog_id, m->top_thread_message_id});
    CHECK(it != yet_unsent_thread_message_ids_.end());
    auto is_deleted = it->second.erase(m->message_id) > 0;
    CHECK(is_deleted);
    if (it->second.empty()) {
      yet_unsent_thread_message_ids_.erase(it);
    }
  }
  if (d->open_count > 0) {
    auto it = dialog_viewed_messages_.find(d->dialog_id);
    if (it != dialog_viewed_messages_.end()) {
      auto &info = it->second;
      CHECK(info != nullptr);
      auto message_it = info->message_id_to_view_id.find(m->message_id);
      if (message_it != info->message_id_to_view_id.end()) {
        info->recently_viewed_messages.erase(message_it->second);
        info->message_id_to_view_id.erase(message_it);
      }
    }
  }

  cancel_send_deleted_message(d->dialog_id, m, is_permanently_deleted);

  auto dialog_type = d->dialog_id.get_type();
  switch (dialog_type) {
    case DialogType::User:
    case DialogType::Chat:
      if (m->message_id.is_server()) {
        message_id_to_dialog_id_.erase(m->message_id);
      }
      break;
    case DialogType::Channel:
      if (m->message_id.is_server() && !td_->auth_manager_->is_bot()) {
        td_->contacts_manager_->unregister_message_users({d->dialog_id, m->message_id}, get_message_user_ids(m));
        td_->contacts_manager_->unregister_message_channels({d->dialog_id, m->message_id}, get_message_channel_ids(m));
      }
      break;
    case DialogType::SecretChat:
      // nothing to do
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }
  ttl_unregister_message(d->dialog_id, m, source);
  ttl_period_unregister_message(d->dialog_id, m);
  delete_bot_command_message_id(d->dialog_id, m->message_id);
  unregister_message_content(td_, m->content.get(), {d->dialog_id, m->message_id}, "on_message_deleted");
  unregister_message_reply(d->dialog_id, m);
  if (m->notification_id.is_valid()) {
    delete_notification_id_to_message_id_correspondence(d->notification_info.get(), m->notification_id, m->message_id);
  }
  if (m->message_id.is_yet_unsent() || dialog_type == DialogType::SecretChat) {
    delete_random_id_to_message_id_correspondence(d, m->random_id, m->message_id);
  }
  if (m->is_topic_message) {
    td_->forum_topic_manager_->on_topic_message_count_changed(d->dialog_id, m->top_thread_message_id, -1);
  }
  if (is_permanently_deleted && !td_->auth_manager_->is_bot() && m->saved_messages_topic_id.is_valid()) {
    CHECK(d->dialog_id == td_->dialog_manager_->get_my_dialog_id());
    td_->saved_messages_manager_->on_topic_message_deleted(m->saved_messages_topic_id, m->message_id);
  }

  added_message_count_--;
}

bool MessagesManager::is_deleted_message(const Dialog *d, MessageId message_id) {
  if (message_id.is_scheduled() && message_id.is_valid_scheduled() && message_id.is_scheduled_server()) {
    return d->scheduled_messages != nullptr && d->scheduled_messages->deleted_scheduled_server_message_ids_.count(
                                                   message_id.get_scheduled_server_message_id()) > 0;
  } else {
    return d->deleted_message_ids.count(message_id) > 0;
  }
}

unique_ptr<MessagesManager::Message> MessagesManager::do_delete_scheduled_message(Dialog *d, MessageId message_id,
                                                                                  bool is_permanently_deleted,
                                                                                  const char *source) {
  CHECK(d != nullptr);
  LOG_CHECK(message_id.is_valid_scheduled()) << d->dialog_id << ' ' << message_id << ' ' << source;

  if (d->scheduled_messages == nullptr) {
    auto message = get_message_force(d, message_id, "do_delete_scheduled_message");
    if (message == 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, source);
      return nullptr;
    }
    CHECK(d->scheduled_messages != nullptr);
  }
  auto it = d->scheduled_messages->scheduled_messages_.find(message_id);
  if (it == d->scheduled_messages->scheduled_messages_.end()) {
    LOG(INFO) << message_id << " is not found in " << d->dialog_id << " to be deleted from " << source;
    auto message = get_message_force(d, message_id, "do_delete_scheduled_message");
    if (message == 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, source);
      return nullptr;
    }

    message_id = message->message_id;
    it = d->scheduled_messages->scheduled_messages_.find(message_id);
    CHECK(it != d->scheduled_messages->scheduled_messages_.end());
  }

  const Message *m = it->second.get();
  CHECK(m->message_id == message_id);

  LOG(INFO) << "Deleting " << MessageFullId{d->dialog_id, message_id} << " from " << source;

  delete_message_from_database(d, message_id, m, is_permanently_deleted, source);

  remove_message_file_sources(d->dialog_id, m);

  it = d->scheduled_messages->scheduled_messages_.find(message_id);
  auto result = std::move(it->second);
  d->scheduled_messages->scheduled_messages_.erase(it);
  CHECK(m == result.get());

  if (message_id.is_scheduled_server()) {
    size_t erased_count =
        d->scheduled_messages->scheduled_message_date_.erase(message_id.get_scheduled_server_message_id());
    CHECK(erased_count != 0);
  }

  cancel_send_deleted_message(d->dialog_id, result.get(), is_permanently_deleted);

  unregister_message_content(td_, m->content.get(), {d->dialog_id, message_id}, "do_delete_scheduled_message");
  unregister_message_reply(d->dialog_id, m);
  if (message_id.is_yet_unsent()) {
    delete_random_id_to_message_id_correspondence(d, m->random_id, m->message_id);
  }
  if (m->is_topic_message) {
    td_->forum_topic_manager_->on_topic_message_count_changed(d->dialog_id, m->top_thread_message_id, -1);
  }

  return result;
}

bool MessagesManager::have_dialog(DialogId dialog_id) const {
  return dialogs_.count(dialog_id) > 0;
}

void MessagesManager::load_dialogs(vector<DialogId> dialog_ids, Promise<vector<DialogId>> &&promise) {
  LOG(INFO) << "Load chats " << format::as_array(dialog_ids);

  Dependencies dependencies;
  for (auto dialog_id : dialog_ids) {
    if (!have_dialog(dialog_id)) {
      dependencies.add_dialog_dependencies(dialog_id);
    }
  }
  dependencies.resolve_force(td_, "load_dialogs", true);

  td::remove_if(dialog_ids, [this](DialogId dialog_id) { return !td_->dialog_manager_->have_dialog_info(dialog_id); });

  for (auto dialog_id : dialog_ids) {
    force_create_dialog(dialog_id, "load_dialogs");
  }

  LOG(INFO) << "Loaded chats " << format::as_array(dialog_ids);
  promise.set_value(std::move(dialog_ids));
}

bool MessagesManager::load_dialog(DialogId dialog_id, int left_tries, Promise<Unit> &&promise) {
  if (!dialog_id.is_valid()) {
    promise.set_error(Status::Error(400, "Invalid chat identifier specified"));
    return false;
  }

  if (!have_dialog_force(dialog_id, "load_dialog")) {  // TODO remove _force
    if (G()->use_message_database()) {
      //      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(400, "Chat not found"));
          return false;
        case DialogType::None:
        default:
          UNREACHABLE();
      }
      if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
        return false;
      }

      add_dialog(dialog_id, "load_dialog");
      return true;
    }

    promise.set_error(Status::Error(400, "Chat not found"));
    return false;
  }

  promise.set_value(Unit());
  return true;
}

Result<DialogDate> MessagesManager::get_dialog_list_last_date(DialogListId dialog_list_id) {
  CHECK(!td_->auth_manager_->is_bot());

  auto *list_ptr = get_dialog_list(dialog_list_id);
  if (list_ptr == nullptr) {
    return Status::Error(400, "Chat list not found");
  }
  return list_ptr->list_last_dialog_date_;
}

vector<DialogId> MessagesManager::get_dialogs(DialogListId dialog_list_id, DialogDate offset, int32 limit,
                                              bool exact_limit, bool force, Promise<Unit> &&promise) {
  CHECK(!td_->auth_manager_->is_bot());

  auto *list_ptr = get_dialog_list(dialog_list_id);
  if (list_ptr == nullptr) {
    promise.set_error(Status::Error(400, "Chat list not found"));
    return {};
  }
  auto &list = *list_ptr;

  LOG(INFO) << "Get chats in " << dialog_list_id << " with offset " << offset << " and limit " << limit
            << ". last_dialog_date = " << list.list_last_dialog_date_
            << ", last_pinned_dialog_date_ = " << list.last_pinned_dialog_date_
            << ", are_pinned_dialogs_inited_ = " << list.are_pinned_dialogs_inited_;

  if (limit <= 0) {
    promise.set_error(Status::Error(400, "Parameter limit must be positive"));
    return {};
  }

  vector<DialogId> result;
  if (dialog_list_id == DialogListId(FolderId::main()) && sponsored_dialog_id_.is_valid()) {
    auto d = get_dialog(sponsored_dialog_id_);
    CHECK(d != nullptr);
    if (is_dialog_sponsored(d)) {
      DialogDate date(get_dialog_private_order(&list, d), d->dialog_id);
      if (offset < date) {
        result.push_back(sponsored_dialog_id_);
        offset = date;
        limit--;
      }
    }
  }

  if (!list.are_pinned_dialogs_inited_) {
    if (limit == 0 || force) {
      promise.set_value(Unit());
      return result;
    } else {
      if (dialog_list_id.is_folder()) {
        auto &folder = *get_dialog_folder(dialog_list_id.get_folder_id());
        if (folder.last_loaded_database_dialog_date_ == folder.last_database_server_dialog_date_ &&
            folder.folder_last_dialog_date_ != MAX_DIALOG_DATE) {
          load_dialog_list(list, limit, std::move(promise));
          return {};
        }
      }
      reload_pinned_dialogs(dialog_list_id, std::move(promise));
      return {};
    }
  }
  if (dialog_list_id.is_filter()) {
    auto dialog_filter_id = dialog_list_id.get_filter_id();
    vector<InputDialogId> input_dialog_ids;
    for (const auto &input_dialog_id : td_->dialog_filter_manager_->get_pinned_input_dialog_ids(dialog_filter_id)) {
      auto dialog_id = input_dialog_id.get_dialog_id();
      if (!have_dialog_force(dialog_id, "get_dialogs")) {
        if (dialog_id.get_type() == DialogType::SecretChat) {
          if (td_->dialog_manager_->have_dialog_info_force(dialog_id, "get_dialogs")) {
            force_create_dialog(dialog_id, "get_dialogs");
          }
        } else {
          input_dialog_ids.push_back(input_dialog_id);
        }
      }
    }

    if (!input_dialog_ids.empty()) {
      if (limit == 0 || force) {
        promise.set_value(Unit());
        return result;
      } else {
        td_->dialog_filter_manager_->load_dialog_filter_dialogs(dialog_filter_id, std::move(input_dialog_ids),
                                                                std::move(promise));
        return {};
      }
    }
  }

  if (!list.pinned_dialogs_.empty() && offset < list.pinned_dialogs_.back() && limit > 0) {
    bool need_reload_pinned_dialogs = false;
    bool need_remove_unknown_secret_chats = false;
    for (auto &pinned_dialog : list.pinned_dialogs_) {
      if (offset < pinned_dialog) {
        auto dialog_id = pinned_dialog.get_dialog_id();
        auto d = get_dialog_force(dialog_id, "get_dialogs");
        if (d == nullptr) {
          LOG(ERROR) << "Failed to load pinned " << dialog_id << " from " << dialog_list_id;
          if (dialog_id.get_type() != DialogType::SecretChat) {
            need_reload_pinned_dialogs = true;
          } else {
            need_remove_unknown_secret_chats = true;
          }
          continue;
        }
        if (d->order == DEFAULT_ORDER) {
          LOG(INFO) << "Loaded pinned " << dialog_id << " with default order in " << dialog_list_id;
          continue;
        }
        result.push_back(dialog_id);
        offset = pinned_dialog;
        limit--;
        if (limit == 0) {
          break;
        }
      }
    }
    if (need_reload_pinned_dialogs) {
      reload_pinned_dialogs(dialog_list_id, Auto());
    }
    if (need_remove_unknown_secret_chats) {
      td::remove_if(list.pinned_dialogs_, [this, &list](const DialogDate &dialog_date) {
        auto dialog_id = dialog_date.get_dialog_id();
        if (dialog_id.get_type() == DialogType::SecretChat && !have_dialog_force(dialog_id, "get_dialogs 2")) {
          list.pinned_dialog_id_orders_.erase(dialog_id);
          return true;
        }
        return false;
      });
      save_pinned_folder_dialog_ids(list);
    }
  }
  update_list_last_pinned_dialog_date(list);

  vector<const DialogFolder *> folders;
  vector<std::set<DialogDate>::const_iterator> folder_iterators;
  for (auto folder_id : get_dialog_list_folder_ids(list)) {
    folders.push_back(get_dialog_folder(folder_id));
    folder_iterators.push_back(folders.back()->ordered_dialogs_.upper_bound(offset));
  }
  while (limit > 0) {
    size_t best_pos = 0;
    DialogDate best_dialog_date = MAX_DIALOG_DATE;
    for (size_t i = 0; i < folders.size(); i++) {
      while (folder_iterators[i] != folders[i]->ordered_dialogs_.end() &&
             *folder_iterators[i] <= list.list_last_dialog_date_ &&
             (!is_dialog_in_list(get_dialog(folder_iterators[i]->get_dialog_id()), dialog_list_id) ||
              get_dialog_pinned_order(&list, folder_iterators[i]->get_dialog_id()) != DEFAULT_ORDER)) {
        ++folder_iterators[i];
      }
      if (folder_iterators[i] != folders[i]->ordered_dialogs_.end() &&
          *folder_iterators[i] <= list.list_last_dialog_date_ && *folder_iterators[i] < best_dialog_date) {
        best_pos = i;
        best_dialog_date = *folder_iterators[i];
      }
    }
    if (best_dialog_date == MAX_DIALOG_DATE || best_dialog_date.get_order() == DEFAULT_ORDER) {
      break;
    }

    limit--;
    result.push_back(folder_iterators[best_pos]->get_dialog_id());
    ++folder_iterators[best_pos];
  }

  if ((!result.empty() && (!exact_limit || limit == 0)) || force || list.list_last_dialog_date_ == MAX_DIALOG_DATE) {
    if (limit > 0 && list.list_last_dialog_date_ != MAX_DIALOG_DATE) {
      LOG(INFO) << "Preload next " << limit << " chats in " << dialog_list_id;
      load_dialog_list(list, limit, Promise<Unit>());
    }

    promise.set_value(Unit());
    return result;
  } else {
    if (!result.empty()) {
      LOG(INFO) << "Have only " << result.size() << " chats, but " << limit << " chats more are needed";
    }
    load_dialog_list(list, limit, std::move(promise));
    return {};
  }
}

void MessagesManager::load_dialog_list(DialogList &list, int32 limit, Promise<Unit> &&promise) {
  CHECK(!td_->auth_manager_->is_bot());
  if (limit > MAX_GET_DIALOGS + 2) {
    limit = MAX_GET_DIALOGS + 2;
  }
  bool is_request_sent = false;
  for (auto folder_id : get_dialog_list_folder_ids(list)) {
    const auto &folder = *get_dialog_folder(folder_id);
    if (folder.folder_last_dialog_date_ != MAX_DIALOG_DATE) {
      load_folder_dialog_list(folder_id, limit, false);
      is_request_sent = true;
    }
  }
  if (is_request_sent) {
    LOG(INFO) << "Wait for loading of " << limit << " chats in " << list.dialog_list_id;
    list.load_list_queries_.push_back(std::move(promise));
  } else {
    LOG(ERROR) << "There is nothing to load for " << list.dialog_list_id << " with folders "
               << get_dialog_list_folder_ids(list);
    promise.set_error(Status::Error(404, "Not Found"));
  }
}

void MessagesManager::load_folder_dialog_list(FolderId folder_id, int32 limit, bool only_local) {
  if (G()->close_flag()) {
    return;
  }

  CHECK(!td_->auth_manager_->is_bot());
  auto &folder = *get_dialog_folder(folder_id);
  if (folder.folder_last_dialog_date_ == MAX_DIALOG_DATE) {
    return;
  }

  bool use_database = G()->use_message_database() &&
                      folder.last_loaded_database_dialog_date_ < folder.last_database_server_dialog_date_;
  if (only_local && !use_database) {
    return;
  }

  auto &multipromise = folder.load_folder_dialog_list_multipromise_;
  if (multipromise.promise_count() != 0) {
    // queries have already been sent, just wait for the result
    LOG(INFO) << "Skip loading of dialog list in " << folder_id << " with limit " << limit
              << ", because it is already being loaded";
    if (use_database && folder.load_dialog_list_limit_max_ != 0) {
      folder.load_dialog_list_limit_max_ = max(folder.load_dialog_list_limit_max_, limit);
    }
    return;
  }
  LOG(INFO) << "Load chat list in " << folder_id << " with limit " << limit;
  multipromise.add_promise(PromiseCreator::lambda([actor_id = actor_id(this), folder_id](Result<Unit> result) {
    send_closure_later(actor_id, &MessagesManager::on_load_folder_dialog_list, folder_id, std::move(result));
  }));

  bool is_query_sent = false;
  if (use_database) {
    load_folder_dialog_list_from_database(folder_id, limit, multipromise.get_promise());
    is_query_sent = true;
  } else {
    LOG(INFO) << "Get chats from " << folder.last_server_dialog_date_;
    multipromise.add_promise(PromiseCreator::lambda([actor_id = actor_id(this), folder_id](Result<Unit> result) {
      if (result.is_ok()) {
        send_closure(actor_id, &MessagesManager::recalc_unread_count, DialogListId(folder_id), -1, true);
      }
    }));
    auto lock = multipromise.get_promise();
    reload_pinned_dialogs(DialogListId(folder_id), multipromise.get_promise());
    if (folder.folder_last_dialog_date_ == folder.last_server_dialog_date_) {
      td_->create_handler<GetDialogListQuery>(multipromise.get_promise())
          ->send(folder_id, folder.last_server_dialog_date_.get_date(),
                 folder.last_server_dialog_date_.get_message_id().get_next_server_message_id().get_server_message_id(),
                 folder.last_server_dialog_date_.get_dialog_id(), int32{MAX_GET_DIALOGS});
      is_query_sent = true;
    }
    if (folder_id == FolderId::main() && folder.last_server_dialog_date_ == MIN_DIALOG_DATE) {
      // do not pass promise to not wait for drafts before showing the chat list
      load_all_draft_messages(td_);
    }
    lock.set_value(Unit());
  }
  CHECK(is_query_sent);
}

void MessagesManager::on_load_folder_dialog_list(FolderId folder_id, Result<Unit> &&result) {
  if (G()->close_flag()) {
    return;
  }
  CHECK(!td_->auth_manager_->is_bot());

  const auto &folder = *get_dialog_folder(folder_id);
  if (result.is_ok()) {
    LOG(INFO) << "Successfully loaded chats in " << folder_id;
    if (folder.last_server_dialog_date_ == MAX_DIALOG_DATE) {
      return;
    }

    bool need_new_get_dialog_list = false;
    for (const auto &list_it : dialog_lists_) {
      auto &list = list_it.second;
      if (!list.load_list_queries_.empty() && has_dialogs_from_folder(list, folder)) {
        LOG(INFO) << "Need to load more chats in " << folder_id << " for " << list_it.first;
        need_new_get_dialog_list = true;
      }
    }
    if (need_new_get_dialog_list) {
      load_folder_dialog_list(folder_id, int32{MAX_GET_DIALOGS}, false);
    }
    return;
  }

  LOG(WARNING) << "Failed to load chats in " << folder_id << ": " << result.error();
  vector<Promise<Unit>> promises;
  for (auto &list_it : dialog_lists_) {
    auto &list = list_it.second;
    if (!list.load_list_queries_.empty() && has_dialogs_from_folder(list, folder)) {
      append(promises, std::move(list.load_list_queries_));
      list.load_list_queries_.clear();
    }
  }

  fail_promises(promises, result.move_as_error());
}

void MessagesManager::load_folder_dialog_list_from_database(FolderId folder_id, int32 limit, Promise<Unit> &&promise) {
  CHECK(!td_->auth_manager_->is_bot());
  auto &folder = *get_dialog_folder(folder_id);
  LOG(INFO) << "Load " << limit << " chats in " << folder_id << " from database from "
            << folder.last_loaded_database_dialog_date_
            << ", last database server dialog date = " << folder.last_database_server_dialog_date_;

  CHECK(folder.load_dialog_list_limit_max_ == 0);
  folder.load_dialog_list_limit_max_ = limit;
  G()->td_db()->get_dialog_db_async()->get_dialogs(
      folder_id, folder.last_loaded_database_dialog_date_.get_order(),
      folder.last_loaded_database_dialog_date_.get_dialog_id(), limit,
      PromiseCreator::lambda([actor_id = actor_id(this), folder_id, limit,
                              promise = std::move(promise)](DialogDbGetDialogsResult result) mutable {
        send_closure(actor_id, &MessagesManager::on_get_dialogs_from_database, folder_id, limit, std::move(result),
                     std::move(promise));
      }));
}

void MessagesManager::on_get_dialogs_from_database(FolderId folder_id, int32 limit, DialogDbGetDialogsResult &&dialogs,
                                                   Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());
  CHECK(!td_->auth_manager_->is_bot());
  auto &folder = *get_dialog_folder(folder_id);
  LOG(INFO) << "Receive " << dialogs.dialogs.size() << " from expected " << limit << " chats in " << folder_id
            << " in from database with next order " << dialogs.next_order << " and next " << dialogs.next_dialog_id;
  int32 new_get_dialogs_limit = 0;
  int32 have_more_dialogs_in_database = (limit == static_cast<int32>(dialogs.dialogs.size()));
  if (have_more_dialogs_in_database && limit < folder.load_dialog_list_limit_max_) {
    new_get_dialogs_limit = folder.load_dialog_list_limit_max_ - limit;
  }
  folder.load_dialog_list_limit_max_ = 0;

  size_t dialogs_skipped = 0;
  for (auto &dialog : dialogs.dialogs) {
    Dialog *d = on_load_dialog_from_database(DialogId(), std::move(dialog), "on_get_dialogs_from_database");
    if (d == nullptr) {
      dialogs_skipped++;
      continue;
    }
    if (d->folder_id != folder_id) {
      LOG(INFO) << "Skip " << d->dialog_id << " received from database, because it is in " << d->folder_id
                << " instead of " << folder_id;
      dialogs_skipped++;
      continue;
    }

    LOG(INFO) << "Loaded from database " << d->dialog_id << " with order " << d->order;
  }

  DialogDate max_dialog_date(dialogs.next_order, dialogs.next_dialog_id);
  if (!have_more_dialogs_in_database) {
    folder.last_loaded_database_dialog_date_ = MAX_DIALOG_DATE;
    LOG(INFO) << "Set last loaded database dialog date to " << folder.last_loaded_database_dialog_date_;
    folder.last_server_dialog_date_ = max(folder.last_server_dialog_date_, folder.last_database_server_dialog_date_);
    LOG(INFO) << "Set last server dialog date to " << folder.last_server_dialog_date_;
    update_last_dialog_date(folder_id);
  } else if (folder.last_loaded_database_dialog_date_ < max_dialog_date) {
    folder.last_loaded_database_dialog_date_ = min(max_dialog_date, folder.last_database_server_dialog_date_);
    LOG(INFO) << "Set last loaded database dialog date to " << folder.last_loaded_database_dialog_date_;
    folder.last_server_dialog_date_ = max(folder.last_server_dialog_date_, folder.last_loaded_database_dialog_date_);
    LOG(INFO) << "Set last server dialog date to " << folder.last_server_dialog_date_;
    update_last_dialog_date(folder_id);

    for (const auto &list_it : dialog_lists_) {
      auto &list = list_it.second;
      if (!list.load_list_queries_.empty() && has_dialogs_from_folder(list, folder) && new_get_dialogs_limit < limit) {
        new_get_dialogs_limit = limit;
      }
    }
  } else {
    LOG(ERROR) << "Last loaded database dialog date didn't increased, skipped " << dialogs_skipped << " chats out of "
               << dialogs.dialogs.size();
  }

  if (!(folder.last_loaded_database_dialog_date_ < folder.last_database_server_dialog_date_)) {
    // have_more_dialogs_in_database = false;
    new_get_dialogs_limit = 0;
  }

  if (new_get_dialogs_limit == 0) {
    preload_folder_dialog_list_timeout_.add_timeout_in(folder_id.get(), 0.2);
    promise.set_value(Unit());
  } else {
    load_folder_dialog_list_from_database(folder_id, new_get_dialogs_limit, std::move(promise));
  }
}

void MessagesManager::preload_folder_dialog_list(FolderId folder_id) {
  if (G()->close_flag()) {
    LOG(INFO) << "Skip chat list preload in " << folder_id << " because of closing";
    return;
  }
  CHECK(!td_->auth_manager_->is_bot());

  auto &folder = *get_dialog_folder(folder_id);
  CHECK(G()->use_message_database());
  if (folder.load_folder_dialog_list_multipromise_.promise_count() != 0) {
    LOG(INFO) << "Skip chat list preload in " << folder_id << ", because there is a pending load chat list request";
    return;
  }

  if (folder.last_loaded_database_dialog_date_ < folder.last_database_server_dialog_date_) {
    // if there are some dialogs in database, preload some of them
    load_folder_dialog_list(folder_id, 20, true);
  } else if (folder.folder_last_dialog_date_ != MAX_DIALOG_DATE) {
    // otherwise load more dialogs from the server
    load_folder_dialog_list(folder_id, MAX_GET_DIALOGS, false);
  } else {
    recalc_unread_count(DialogListId(folder_id), -1, false);
  }
}

void MessagesManager::get_dialogs_from_list(DialogListId dialog_list_id, int32 limit,
                                            Promise<td_api::object_ptr<td_api::chats>> &&promise) {
  CHECK(!td_->auth_manager_->is_bot());

  if (get_dialog_list(dialog_list_id) == nullptr) {
    return promise.set_error(Status::Error(400, "Chat list not found"));
  }

  if (limit <= 0) {
    return promise.set_error(Status::Error(400, "Parameter limit must be positive"));
  }

  auto task_id = ++current_get_dialogs_task_id_;
  auto &task = get_dialogs_tasks_[task_id];
  task.dialog_list_id = dialog_list_id;
  task.retry_count = 5;
  task.limit = limit;
  task.promise = std::move(promise);
  get_dialogs_from_list_impl(task_id);
}

void MessagesManager::get_dialogs_from_list_impl(int64 task_id) {
  auto task_it = get_dialogs_tasks_.find(task_id);
  CHECK(task_it != get_dialogs_tasks_.end());
  auto &task = task_it->second;
  auto promise = PromiseCreator::lambda([actor_id = actor_id(this), task_id](Result<Unit> &&result) {
    // on_get_dialogs_from_list can delete get_dialogs_tasks_[task_id], so it must be called later
    send_closure_later(actor_id, &MessagesManager::on_get_dialogs_from_list, task_id, std::move(result));
  });
  auto dialog_ids = get_dialogs(task.dialog_list_id, MIN_DIALOG_DATE, task.limit, true, false, std::move(promise));
  auto &list = *get_dialog_list(task.dialog_list_id);
  auto total_count = get_dialog_total_count(list);
  LOG(INFO) << "Receive " << dialog_ids.size() << " chats instead of " << task.limit << " out of " << total_count
            << " in " << task.dialog_list_id;
  CHECK(dialog_ids.size() <= static_cast<size_t>(total_count));
  CHECK(dialog_ids.size() <= static_cast<size_t>(task.limit));
  if (dialog_ids.size() == static_cast<size_t>(min(total_count, task.limit)) ||
      list.list_last_dialog_date_ == MAX_DIALOG_DATE || task.retry_count == 0) {
    auto task_promise = std::move(task.promise);
    get_dialogs_tasks_.erase(task_it);
    if (!task_promise) {
      dialog_ids.clear();
    }
    return task_promise.set_value(
        td_->dialog_manager_->get_chats_object(total_count, dialog_ids, "get_dialogs_from_list_impl"));
  }
  // nor the limit, nor the end of the list were reached; wait for the promise
}

void MessagesManager::on_get_dialogs_from_list(int64 task_id, Result<Unit> &&result) {
  auto task_it = get_dialogs_tasks_.find(task_id);
  if (task_it == get_dialogs_tasks_.end()) {
    // the task has already been completed
    LOG(INFO) << "Chat list load task " << task_id << " has already been completed";
    return;
  }
  auto &task = task_it->second;
  if (result.is_error()) {
    LOG(INFO) << "Chat list load task " << task_id << " failed with the error " << result.error();
    auto task_promise = std::move(task.promise);
    get_dialogs_tasks_.erase(task_it);
    return task_promise.set_error(result.move_as_error());
  }

  auto list_ptr = get_dialog_list(task.dialog_list_id);
  CHECK(list_ptr != nullptr);
  auto &list = *list_ptr;
  if (task.last_dialog_date == list.list_last_dialog_date_) {
    // no new chats were loaded
    task.retry_count--;
  } else {
    CHECK(task.last_dialog_date < list.list_last_dialog_date_);
    task.last_dialog_date = list.list_last_dialog_date_;
    task.retry_count = 5;
  }
  get_dialogs_from_list_impl(task_id);
}

void MessagesManager::mark_dialog_as_read(Dialog *d) {
  if (td_->dialog_manager_->is_forum_channel(d->dialog_id)) {
    // TODO read forum topics
  }
  if (d->server_unread_count + d->local_unread_count > 0 && d->last_message_id.is_valid()) {
    auto it = d->ordered_messages.get_const_iterator(d->last_message_id);
    while (*it != nullptr) {
      auto message_id = (*it)->get_message_id();
      if (message_id.is_server() || message_id.is_local()) {
        read_dialog_inbox(d, message_id);
        break;
      }
      --it;
    }
    if (*it == nullptr) {
      // if can't find the last message, then d->last_new_message_id is the best guess
      read_dialog_inbox(d, d->last_new_message_id);
    }
  }
  if (d->is_marked_as_unread) {
    set_dialog_is_marked_as_unread(d, false);
  }
}

void MessagesManager::read_all_dialogs_from_list(DialogListId dialog_list_id, Promise<Unit> &&promise,
                                                 bool is_recursive) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  if (get_dialog_list(dialog_list_id) == nullptr) {
    return promise.set_error(Status::Error(400, "Chat list not found"));
  }

  dialogs_.foreach([&](const DialogId &dialog_id, unique_ptr<Dialog> &dialog) {
    Dialog *d = dialog.get();
    if (is_dialog_in_list(d, dialog_list_id)) {
      mark_dialog_as_read(d);
    }
  });

  if (is_recursive) {
    return promise.set_value(Unit());
  }

  auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_list_id, promise = std::move(promise)](
                                                  Result<td_api::object_ptr<td_api::chats>> &&) mutable {
    // it is expected that loading of so many chats will fail, so just ignore the error and result itself
    send_closure(actor_id, &MessagesManager::read_all_dialogs_from_list, dialog_list_id, std::move(promise), true);
  });
  get_dialogs_from_list(dialog_list_id, 10000, std::move(query_promise));
}

vector<DialogId> MessagesManager::get_pinned_dialog_ids(DialogListId dialog_list_id) const {
  CHECK(!td_->auth_manager_->is_bot());
  if (dialog_list_id.is_filter()) {
    return td_->dialog_filter_manager_->get_pinned_dialog_ids(dialog_list_id.get_filter_id());
  }

  auto *list = get_dialog_list(dialog_list_id);
  if (list == nullptr || !list->are_pinned_dialogs_inited_) {
    return {};
  }
  return transform(list->pinned_dialogs_, [](auto &pinned_dialog) { return pinned_dialog.get_dialog_id(); });
}

void MessagesManager::reload_pinned_dialogs(DialogListId dialog_list_id, Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());
  CHECK(!td_->auth_manager_->is_bot());

  if (dialog_list_id.is_folder()) {
    td_->create_handler<GetPinnedDialogsQuery>(std::move(promise))->send(dialog_list_id.get_folder_id());
  } else if (dialog_list_id.is_filter()) {
    td_->dialog_filter_manager_->schedule_reload_dialog_filters(std::move(promise));
  }
}

vector<DialogId> MessagesManager::search_public_dialogs(const string &query, Promise<Unit> &&promise) {
  LOG(INFO) << "Search public chats with query = \"" << query << '"';

  auto query_length = utf8_length(query);
  if (query_length < MIN_SEARCH_PUBLIC_DIALOG_PREFIX_LEN ||
      (query_length == MIN_SEARCH_PUBLIC_DIALOG_PREFIX_LEN && query[0] == '@')) {
    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 dialog_id = td_->dialog_manager_->resolve_dialog_username(username, promise);
        if (!dialog_id.is_valid()) {
          return {};
        }

        force_create_dialog(dialog_id, "search_public_dialogs");

        auto d = get_dialog(dialog_id);
        if (d == nullptr || d->order != DEFAULT_ORDER ||
            (dialog_id.get_type() == DialogType::User &&
             td_->contacts_manager_->is_user_contact(dialog_id.get_user_id()))) {
          continue;
        }

        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 {};
}

void MessagesManager::send_search_public_dialogs_query(const string &query, Promise<Unit> &&promise) {
  CHECK(!query.empty());
  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<int32, vector<DialogId>> MessagesManager::search_dialogs(const string &query, int32 limit,
                                                                   Promise<Unit> &&promise) {
  LOG(INFO) << "Search chats with query \"" << query << "\" and limit " << limit;
  CHECK(!td_->auth_manager_->is_bot());

  if (limit < 0) {
    promise.set_error(Status::Error(400, "Limit must be non-negative"));
    return {};
  }
  if (query.empty()) {
    return recently_found_dialogs_.get_dialogs(limit, std::move(promise));
  }

  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 {narrow_cast<int32>(result.first), std::move(dialog_ids)};
}

std::pair<int32, vector<DialogId>> MessagesManager::search_recently_found_dialogs(const string &query, int32 limit,
                                                                                  Promise<Unit> &&promise) {
  auto result = recently_found_dialogs_.get_dialogs(query.empty() ? limit : 50, std::move(promise));
  if (result.first == 0 || query.empty()) {
    return result;
  }

  Hints hints;
  int rating = 1;
  for (auto dialog_id : result.second) {
    hints.add(dialog_id.get(), td_->dialog_manager_->get_dialog_search_text(dialog_id));
    hints.set_rating(dialog_id.get(), ++rating);
  }

  auto hints_result = hints.search(query, limit, false);
  return {narrow_cast<int32>(hints_result.first),
          transform(hints_result.second, [](int64 key) { return DialogId(key); })};
}

std::pair<int32, vector<DialogId>> MessagesManager::get_recently_opened_dialogs(int32 limit, Promise<Unit> &&promise) {
  CHECK(!td_->auth_manager_->is_bot());
  return recently_opened_dialogs_.get_dialogs(limit, std::move(promise));
}

vector<DialogId> MessagesManager::sort_dialogs_by_order(const vector<DialogId> &dialog_ids, int32 limit) const {
  CHECK(!td_->auth_manager_->is_bot());
  auto fake_order = static_cast<int64>(dialog_ids.size()) + 1;
  auto dialog_dates = transform(dialog_ids, [this, &fake_order](DialogId dialog_id) {
    const Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr);
    auto order = get_dialog_base_order(d);
    if (is_dialog_inited(d) || order != DEFAULT_ORDER) {
      return DialogDate(order, dialog_id);
    }
    // if the dialog is not inited yet, we need to assume that server knows better and the dialog needs to be returned
    return DialogDate(fake_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 {};
}

void MessagesManager::block_message_sender_from_replies(MessageId message_id, bool need_delete_message,
                                                        bool need_delete_all_messages, bool report_spam,
                                                        Promise<Unit> &&promise) {
  auto dialog_id = DialogId(ContactsManager::get_replies_bot_user_id());
  Dialog *d = get_dialog_force(dialog_id, "block_message_sender_from_replies");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Not enough rights"));
  }

  auto *m = get_message_force(d, message_id, "block_message_sender_from_replies");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }
  if (m->is_outgoing || m->message_id.is_scheduled() || !m->message_id.is_server()) {
    return promise.set_error(Status::Error(400, "Wrong message specified"));
  }

  DialogId sender_dialog_id;
  if (m->forward_info != nullptr) {
    sender_dialog_id = m->forward_info->get_origin().get_sender();
  }
  vector<MessageId> message_ids;
  if (need_delete_all_messages && sender_dialog_id.is_valid()) {
    message_ids = find_dialog_messages(d, [sender_dialog_id](const Message *m) {
      return !m->is_outgoing && m->forward_info != nullptr &&
             m->forward_info->get_origin().get_sender() == sender_dialog_id;
    });
    CHECK(td::contains(message_ids, message_id));
  } else if (need_delete_message) {
    message_ids.push_back(message_id);
  }

  delete_dialog_messages(d, message_ids, false, DELETE_MESSAGE_USER_REQUEST_SOURCE);

  block_message_sender_from_replies_on_server(message_id, need_delete_message, need_delete_all_messages, report_spam, 0,
                                              std::move(promise));
}

class MessagesManager::BlockMessageSenderFromRepliesOnServerLogEvent {
 public:
  MessageId message_id_;
  bool delete_message_;
  bool delete_all_messages_;
  bool report_spam_;

  template <class StorerT>
  void store(StorerT &storer) const {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(delete_message_);
    STORE_FLAG(delete_all_messages_);
    STORE_FLAG(report_spam_);
    END_STORE_FLAGS();

    td::store(message_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(delete_message_);
    PARSE_FLAG(delete_all_messages_);
    PARSE_FLAG(report_spam_);
    END_PARSE_FLAGS();

    td::parse(message_id_, parser);
  }
};

uint64 MessagesManager::save_block_message_sender_from_replies_on_server_log_event(MessageId message_id,
                                                                                   bool need_delete_message,
                                                                                   bool need_delete_all_messages,
                                                                                   bool report_spam) {
  BlockMessageSenderFromRepliesOnServerLogEvent log_event{message_id, need_delete_message, need_delete_all_messages,
                                                          report_spam};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::BlockMessageSenderFromRepliesOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::block_message_sender_from_replies_on_server(MessageId message_id, bool need_delete_message,
                                                                  bool need_delete_all_messages, bool report_spam,
                                                                  uint64 log_event_id, Promise<Unit> &&promise) {
  if (log_event_id == 0) {
    log_event_id = save_block_message_sender_from_replies_on_server_log_event(message_id, need_delete_message,
                                                                              need_delete_all_messages, report_spam);
  }

  td_->create_handler<BlockFromRepliesQuery>(get_erase_log_event_promise(log_event_id, std::move(promise)))
      ->send(message_id, need_delete_message, need_delete_all_messages, report_spam);
}

bool MessagesManager::is_dialog_blocked(DialogId dialog_id) const {
  const Dialog *d = get_dialog(dialog_id);
  return d != nullptr && d->is_blocked;
}

void MessagesManager::get_blocked_dialogs(const td_api::object_ptr<td_api::BlockList> &block_list, int32 offset,
                                          int32 limit, Promise<td_api::object_ptr<td_api::messageSenders>> &&promise) {
  if (offset < 0) {
    return promise.set_error(Status::Error(400, "Parameter offset must be non-negative"));
  }

  if (limit <= 0) {
    return promise.set_error(Status::Error(400, "Parameter limit must be positive"));
  }

  auto block_list_id = BlockListId(block_list);
  if (!block_list_id.is_valid()) {
    return promise.set_error(Status::Error(400, "Block list must be non-empty"));
  }

  td_->create_handler<GetBlockedDialogsQuery>(std::move(promise))->send(block_list_id, offset, limit);
}

void MessagesManager::on_get_blocked_dialogs(int32 offset, int32 limit, int32 total_count,
                                             vector<tl_object_ptr<telegram_api::peerBlocked>> &&blocked_peers,
                                             Promise<td_api::object_ptr<td_api::messageSenders>> &&promise) {
  LOG(INFO) << "Receive " << blocked_peers.size() << " blocked chats from offset " << offset << " out of "
            << total_count;
  auto peers = transform(std::move(blocked_peers), [](tl_object_ptr<telegram_api::peerBlocked> &&blocked_peer) {
    return std::move(blocked_peer->peer_id_);
  });
  auto dialog_ids = get_message_sender_dialog_ids(td_, std::move(peers));
  if (!dialog_ids.empty() && offset + dialog_ids.size() > static_cast<size_t>(total_count)) {
    LOG(ERROR) << "Fix total count of blocked chats from " << total_count << " to " << offset + dialog_ids.size();
    total_count = offset + narrow_cast<int32>(dialog_ids.size());
  }

  auto senders = transform(dialog_ids, [td = td_](DialogId dialog_id) {
    return get_message_sender_object(td, dialog_id, "on_get_blocked_dialogs");
  });
  promise.set_value(td_api::make_object<td_api::messageSenders>(total_count, std::move(senders)));
}

DialogId MessagesManager::get_dialog_message_sender(MessageFullId message_full_id) {
  const auto *m = get_message_force(message_full_id, "get_dialog_message_sender");
  if (m == nullptr) {
    return DialogId();
  }
  return get_message_sender(m);
}

bool MessagesManager::have_message_force(MessageFullId message_full_id, const char *source) {
  return get_message_force(message_full_id, source) != nullptr;
}

bool MessagesManager::have_message_force(Dialog *d, MessageId message_id, const char *source) {
  return get_message_force(d, message_id, source) != nullptr;
}

MessagesManager::Message *MessagesManager::get_message(MessageFullId message_full_id) {
  Dialog *d = get_dialog(message_full_id.get_dialog_id());
  if (d == nullptr) {
    return nullptr;
  }

  return get_message(d, message_full_id.get_message_id());
}

const MessagesManager::Message *MessagesManager::get_message(MessageFullId message_full_id) const {
  const Dialog *d = get_dialog(message_full_id.get_dialog_id());
  if (d == nullptr) {
    return nullptr;
  }

  return get_message(d, message_full_id.get_message_id());
}

MessagesManager::Message *MessagesManager::get_message_force(MessageFullId message_full_id, const char *source) {
  Dialog *d = get_dialog_force(message_full_id.get_dialog_id(), source);
  if (d == nullptr) {
    return nullptr;
  }

  return get_message_force(d, message_full_id.get_message_id(), source);
}

MessageFullId MessagesManager::get_replied_message_id(DialogId dialog_id, const Message *m) {
  if (m->reply_to_story_full_id.is_valid()) {
    return {};
  }
  auto message_full_id = get_message_content_replied_message_id(dialog_id, m->content.get());
  if (message_full_id.get_message_id().is_valid()) {
    CHECK(m->replied_message_info.is_empty());
    return message_full_id;
  }
  auto reply_message_full_id = m->replied_message_info.get_reply_message_full_id(dialog_id, true);
  if (reply_message_full_id.get_message_id() != MessageId()) {
    return reply_message_full_id;
  }
  if (m->top_thread_message_id.is_valid() && m->top_thread_message_id != m->message_id) {
    return {dialog_id, m->top_thread_message_id};
  }
  return {};
}

void MessagesManager::get_message_force_from_server(Dialog *d, MessageId message_id, Promise<Unit> &&promise,
                                                    tl_object_ptr<telegram_api::InputMessage> input_message) {
  LOG(INFO) << "Get " << message_id << " in " << d->dialog_id << " using " << to_string(input_message);
  auto dialog_type = d->dialog_id.get_type();
  auto m = get_message_force(d, message_id, "get_message_force_from_server");
  if (m == nullptr && !is_deleted_message(d, message_id) && dialog_type != DialogType::SecretChat) {
    if (message_id.is_valid() && message_id.is_server()) {
      if (d->last_new_message_id != MessageId() && message_id > d->last_new_message_id &&
          dialog_type != DialogType::Channel && !td_->auth_manager_->is_bot()) {
        // message will not be added to the dialog anyway
        return promise.set_value(Unit());
      }

      return get_message_from_server({d->dialog_id, message_id}, std::move(promise), "get_message_force_from_server",
                                     std::move(input_message));
    }
    if (message_id.is_valid_scheduled() && message_id.is_scheduled_server() && input_message == nullptr) {
      return get_message_from_server({d->dialog_id, message_id}, std::move(promise), "get_message_force_from_server");
    }
  }

  promise.set_value(Unit());
}

void MessagesManager::get_message(MessageFullId message_full_id, Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(message_full_id.get_dialog_id(), "get_message");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  get_message_force_from_server(d, message_full_id.get_message_id(), std::move(promise));
}

MessageFullId 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, "get_replied_message");
  if (d == nullptr) {
    promise.set_error(Status::Error(400, "Chat not found"));
    return MessageFullId();
  }

  message_id = get_persistent_message_id(d, message_id);
  auto m = get_message_force(d, message_id, "get_replied_message");
  if (m == nullptr) {
    if (force) {
      promise.set_value(Unit());
    } else {
      get_message_force_from_server(d, message_id, std::move(promise));
    }
    return MessageFullId();
  }

  tl_object_ptr<telegram_api::InputMessage> input_message;
  auto replied_message_id = get_replied_message_id(dialog_id, m);
  if (replied_message_id.get_dialog_id() != dialog_id) {
    dialog_id = replied_message_id.get_dialog_id();
    if (!td_->dialog_manager_->have_dialog_info_force(dialog_id, "get_replied_message")) {
      promise.set_value(Unit());
      return {};
    }
    if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
      promise.set_value(Unit());
      return {};
    }

    force_create_dialog(dialog_id, "get_replied_message");
    d = get_dialog_force(dialog_id, "get_replied_message");
    if (d == nullptr) {
      promise.set_error(Status::Error(500, "Chat with replied message not found"));
      return {};
    }
  } else if (m->message_id.is_valid() && m->message_id.is_server()) {
    input_message = make_tl_object<telegram_api::inputMessageReplyTo>(m->message_id.get_server_message_id().get());
  }
  get_message_force_from_server(d, replied_message_id.get_message_id(), std::move(promise), std::move(input_message));

  return replied_message_id;
}

Result<MessageFullId> MessagesManager::get_top_thread_message_full_id(DialogId dialog_id, const Message *m,
                                                                      bool allow_non_root) const {
  CHECK(m != nullptr);
  if (m->message_id.is_scheduled()) {
    return Status::Error(400, "Message is scheduled");
  }
  if (dialog_id.get_type() != DialogType::Channel) {
    return Status::Error(400, "Chat can't have message threads");
  }
  if (!m->reply_info.is_empty() && m->reply_info.is_comment_) {
    if (!is_visible_message_reply_info(dialog_id, m)) {
      return Status::Error(400, "Message has no comments");
    }
    if (m->message_id.is_yet_unsent()) {
      return Status::Error(400, "Message is not sent yet");
    }
    return MessageFullId{DialogId(m->reply_info.channel_id_), m->linked_top_thread_message_id};
  } else {
    if (!m->top_thread_message_id.is_valid()) {
      return Status::Error(400, "Message has no thread");
    }
    if (!allow_non_root && m->top_thread_message_id != m->message_id &&
        !td_->contacts_manager_->get_channel_has_linked_channel(dialog_id.get_channel_id())) {
      return Status::Error(400, "Root message must be used to get the message thread");
    }
    return MessageFullId{dialog_id, m->top_thread_message_id};
  }
}

void MessagesManager::get_message_thread(DialogId dialog_id, MessageId message_id,
                                         Promise<MessageThreadInfo> &&promise) {
  LOG(INFO) << "Get message thread from " << message_id << " in " << dialog_id;
  Dialog *d = get_dialog_force(dialog_id, "get_message_thread");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }
  if (dialog_id.get_type() != DialogType::Channel) {
    return promise.set_error(Status::Error(400, "Chat is not a supergroup or a channel"));
  }
  if (message_id.is_scheduled()) {
    return promise.set_error(Status::Error(400, "Scheduled messages can't have message threads"));
  }

  MessageFullId top_thread_message_full_id;
  if (message_id == MessageId(ServerMessageId(1)) && td_->dialog_manager_->is_forum_channel(dialog_id)) {
    top_thread_message_full_id = MessageFullId{dialog_id, message_id};
  } else {
    message_id = get_persistent_message_id(d, message_id);
    auto m = get_message_force(d, message_id, "get_message_thread");
    if (m == nullptr) {
      return promise.set_error(Status::Error(400, "Message not found"));
    }

    TRY_RESULT_PROMISE_ASSIGN(promise, top_thread_message_full_id, get_top_thread_message_full_id(dialog_id, m, true));
    if ((m->reply_info.is_empty() || !m->reply_info.is_comment_) &&
        top_thread_message_full_id.get_message_id() != m->message_id) {
      CHECK(dialog_id == top_thread_message_full_id.get_dialog_id());
      // get information about the thread from the top message
      message_id = top_thread_message_full_id.get_message_id();
      CHECK(message_id.is_valid());
    }
  }

  auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, message_id,
                                               promise = std::move(promise)](Result<MessageThreadInfo> result) mutable {
    if (result.is_error()) {
      return promise.set_error(result.move_as_error());
    }
    send_closure(actor_id, &MessagesManager::on_get_discussion_message, dialog_id, message_id, result.move_as_ok(),
                 std::move(promise));
  });

  td_->create_handler<GetDiscussionMessageQuery>(std::move(query_promise))
      ->send(dialog_id, message_id, top_thread_message_full_id.get_dialog_id(),
             top_thread_message_full_id.get_message_id());
}

void MessagesManager::process_discussion_message(
    telegram_api::object_ptr<telegram_api::messages_discussionMessage> &&result, DialogId dialog_id,
    MessageId message_id, DialogId expected_dialog_id, MessageId expected_message_id,
    Promise<MessageThreadInfo> promise) {
  LOG(INFO) << "Receive discussion message for " << message_id << " in " << dialog_id << " with expected "
            << expected_message_id << " in " << expected_dialog_id << ": " << to_string(result);
  td_->contacts_manager_->on_get_users(std::move(result->users_), "process_discussion_message");
  td_->contacts_manager_->on_get_chats(std::move(result->chats_), "process_discussion_message");

  for (auto &message : result->messages_) {
    auto message_dialog_id = DialogId::get_message_dialog_id(message);
    if (message_dialog_id != expected_dialog_id) {
      return promise.set_error(Status::Error(500, "Expected messages in a different chat"));
    }
  }

  for (auto &message : result->messages_) {
    if (need_channel_difference_to_add_message(expected_dialog_id, message)) {
      auto max_message_id = MessageId::get_max_message_id(result->messages_);
      return run_after_channel_difference(
          expected_dialog_id, max_message_id,
          PromiseCreator::lambda([actor_id = actor_id(this), result = std::move(result), dialog_id, message_id,
                                  expected_dialog_id, expected_message_id,
                                  promise = std::move(promise)](Unit ignored) mutable {
            send_closure(actor_id, &MessagesManager::process_discussion_message_impl, std::move(result), dialog_id,
                         message_id, expected_dialog_id, expected_message_id, std::move(promise));
          }),
          "process_discussion_message");
    }
  }

  process_discussion_message_impl(std::move(result), dialog_id, message_id, expected_dialog_id, expected_message_id,
                                  std::move(promise));
}

void MessagesManager::process_discussion_message_impl(
    telegram_api::object_ptr<telegram_api::messages_discussionMessage> &&result, DialogId dialog_id,
    MessageId message_id, DialogId expected_dialog_id, MessageId expected_message_id,
    Promise<MessageThreadInfo> promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  MessageThreadInfo message_thread_info;
  message_thread_info.dialog_id = expected_dialog_id;
  message_thread_info.unread_message_count = max(0, result->unread_count_);
  MessageId top_message_id;
  for (auto &message : result->messages_) {
    auto message_full_id = on_get_message(std::move(message), false, true, false, "process_discussion_message_impl");
    if (message_full_id.get_message_id().is_valid()) {
      CHECK(message_full_id.get_dialog_id() == expected_dialog_id);
      message_thread_info.message_ids.push_back(message_full_id.get_message_id());
      if (message_full_id.get_message_id() == expected_message_id) {
        top_message_id = expected_message_id;
      }
    }
  }
  if (!message_thread_info.message_ids.empty() && !top_message_id.is_valid()) {
    top_message_id = message_thread_info.message_ids.back();
  }
  auto max_message_id = MessageId(ServerMessageId(result->max_id_));
  auto last_read_inbox_message_id = MessageId(ServerMessageId(result->read_inbox_max_id_));
  auto last_read_outbox_message_id = MessageId(ServerMessageId(result->read_outbox_max_id_));
  if (top_message_id.is_valid()) {
    on_update_read_message_comments(expected_dialog_id, top_message_id, max_message_id, last_read_inbox_message_id,
                                    last_read_outbox_message_id, message_thread_info.unread_message_count);
  }
  if (expected_dialog_id != dialog_id) {
    on_update_read_message_comments(dialog_id, message_id, max_message_id, last_read_inbox_message_id,
                                    last_read_outbox_message_id, message_thread_info.unread_message_count);
  }
  promise.set_value(std::move(message_thread_info));
}

void MessagesManager::on_get_discussion_message(DialogId dialog_id, MessageId message_id,
                                                MessageThreadInfo &&message_thread_info,
                                                Promise<MessageThreadInfo> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  Dialog *d = get_dialog_force(dialog_id, "on_get_discussion_message");
  CHECK(d != nullptr);

  CHECK(message_id.is_valid());
  auto m = get_message_force(d, message_id, "on_get_discussion_message");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }
  if (message_thread_info.message_ids.empty()) {
    if (message_thread_info.dialog_id != dialog_id &&
        !td_->dialog_manager_->have_input_peer(message_thread_info.dialog_id, AccessRights::Read)) {
      return promise.set_error(Status::Error(400, "Can't access message comments"));
    }
    return promise.set_error(Status::Error(400, "Message has no thread"));
  }

  DialogId expected_dialog_id;
  if (m->reply_info.is_comment_) {
    if (!is_active_message_reply_info(dialog_id, m->reply_info)) {
      return promise.set_error(Status::Error(400, "Message has no comments"));
    }
    expected_dialog_id = DialogId(m->reply_info.channel_id_);
  } else if (message_id == MessageId(ServerMessageId(1)) && td_->dialog_manager_->is_forum_channel(dialog_id)) {
    // General forum topic
    expected_dialog_id = dialog_id;
  } else {
    if (!m->top_thread_message_id.is_valid()) {
      return promise.set_error(Status::Error(400, "Message has no thread"));
    }
    expected_dialog_id = dialog_id;
  }

  if (expected_dialog_id != dialog_id && m->reply_info.is_comment_ &&
      m->linked_top_thread_message_id != message_thread_info.message_ids.back()) {
    auto linked_d = get_dialog_force(expected_dialog_id, "on_get_discussion_message 2");
    CHECK(linked_d != nullptr);
    td::remove_if(message_thread_info.message_ids, [&](MessageId linked_message_id) {
      return !have_message_force(linked_d, linked_message_id, "on_get_discussion_message 4");
    });
    if (message_thread_info.message_ids.empty()) {
      return promise.set_error(Status::Error(400, "Message has no thread"));
    }
    auto linked_message_id = message_thread_info.message_ids.back();
    Message *linked_m = get_message_force(linked_d, linked_message_id, "on_get_discussion_message 3");
    CHECK(linked_m != nullptr && linked_m->message_id.is_server());
    if (linked_m->top_thread_message_id == linked_m->message_id &&
        is_active_message_reply_info(expected_dialog_id, linked_m->reply_info)) {
      if (m->linked_top_thread_message_id.is_valid()) {
        LOG(ERROR) << "Comment message identifier for " << message_id << " in " << dialog_id << " changed from "
                   << m->linked_top_thread_message_id << " to " << linked_message_id;
      }
      m->linked_top_thread_message_id = linked_message_id;
      on_dialog_updated(dialog_id, "on_get_discussion_message");
    }
  }
  promise.set_value(std::move(message_thread_info));
}

td_api::object_ptr<td_api::messageThreadInfo> MessagesManager::get_message_thread_info_object(
    const MessageThreadInfo &info) {
  if (info.message_ids.empty()) {
    return nullptr;
  }

  Dialog *d = get_dialog(info.dialog_id);
  CHECK(d != nullptr);
  td_api::object_ptr<td_api::messageReplyInfo> reply_info;
  vector<td_api::object_ptr<td_api::message>> messages;
  messages.reserve(info.message_ids.size());
  bool is_forum_topic = false;
  for (auto message_id : info.message_ids) {
    const Message *m = get_message_force(d, message_id, "get_message_thread_info_object");
    auto message = get_message_object(d->dialog_id, m, "get_message_thread_info_object");
    if (message != nullptr) {
      if (message->interaction_info_ != nullptr && message->interaction_info_->reply_info_ != nullptr) {
        reply_info = m->reply_info.get_message_reply_info_object(td_, d->last_read_inbox_message_id);
        CHECK(reply_info != nullptr);
      }
      is_forum_topic = message->is_topic_message_;
      messages.push_back(std::move(message));
    }
  }
  if (messages.size() != 1) {
    is_forum_topic = false;
  } else if (info.message_ids[0] == MessageId(ServerMessageId(1)) &&
             td_->dialog_manager_->is_forum_channel(info.dialog_id)) {
    // General forum topic
    is_forum_topic = true;
  }
  if (reply_info == nullptr && !is_forum_topic) {
    return nullptr;
  }

  MessageId top_thread_message_id;
  td_api::object_ptr<td_api::draftMessage> draft_message;
  if (!info.message_ids.empty()) {
    top_thread_message_id = info.message_ids.back();
    if (can_send_message(d->dialog_id).is_ok()) {
      const Message *m = get_message_force(d, top_thread_message_id, "get_message_thread_info_object 2");
      if (m != nullptr && !m->reply_info.is_comment_ && is_active_message_reply_info(d->dialog_id, m->reply_info)) {
        draft_message = get_draft_message_object(td_, m->thread_draft_message);
      }
    }
  }
  return td_api::make_object<td_api::messageThreadInfo>(
      get_chat_id_object(d->dialog_id, "messageThreadInfo"), top_thread_message_id.get(), std::move(reply_info),
      info.unread_message_count, std::move(messages), std::move(draft_message));
}

Status MessagesManager::can_get_message_read_date(DialogId dialog_id, const Message *m) const {
  if (td_->auth_manager_->is_bot()) {
    return Status::Error(400, "User is bot");
  }
  CHECK(m != nullptr);
  if (!m->is_outgoing) {
    return Status::Error(400, "Can't get read date of incoming messages");
  }
  if (G()->unix_time() - m->date > td_->option_manager_->get_option_integer("pm_read_date_expire_period")) {
    return Status::Error(400, "Message is too old");
  }

  if (dialog_id.get_type() != DialogType::User) {
    return Status::Error(400, "Read date can be received only in private chats");
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(400, "Can't access the chat");
  }
  auto user_id = dialog_id.get_user_id();
  if (td_->contacts_manager_->is_user_bot(user_id)) {
    return Status::Error(400, "The user is a bot");
  }
  if (td_->contacts_manager_->is_user_support(user_id)) {
    return Status::Error(400, "The user is a Telegram support account");
  }

  if (m->message_id.is_scheduled()) {
    return Status::Error(400, "Scheduled messages can't be read");
  }
  if (m->message_id.is_yet_unsent()) {
    return Status::Error(400, "Yet unsent messages can't be read");
  }
  if (m->message_id.is_local()) {
    return Status::Error(400, "Local messages can't be read");
  }
  CHECK(m->message_id.is_server());

  return Status::OK();
}

void MessagesManager::get_message_read_date(MessageFullId message_full_id,
                                            Promise<td_api::object_ptr<td_api::MessageReadDate>> &&promise) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "get_message_read_date");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  auto m = get_message_force(d, message_full_id.get_message_id(), "get_message_read_date");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }

  TRY_STATUS_PROMISE(promise, can_get_message_read_date(dialog_id, m));

  if (d->last_read_outbox_message_id < m->message_id) {
    return promise.set_value(td_api::make_object<td_api::messageReadDateUnread>());
  }
  if (td_->contacts_manager_->get_user_read_dates_private(dialog_id.get_user_id())) {
    return promise.set_value(td_api::make_object<td_api::messageReadDateUserPrivacyRestricted>());
  }

  td_->create_handler<GetOutboxReadDateQuery>(std::move(promise))
      ->send(message_full_id.get_dialog_id(), message_full_id.get_message_id());
}

Status MessagesManager::can_get_message_viewers(MessageFullId message_full_id) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "can_get_message_viewers");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }

  auto m = get_message_force(d, message_full_id.get_message_id(), "can_get_message_viewers");
  if (m == nullptr) {
    return Status::Error(400, "Message not found");
  }

  return can_get_message_viewers(dialog_id, m);
}

Status MessagesManager::can_get_message_viewers(DialogId dialog_id, const Message *m) const {
  if (td_->auth_manager_->is_bot()) {
    return Status::Error(400, "User is bot");
  }
  CHECK(m != nullptr);
  if (!m->is_outgoing) {
    return Status::Error(400, "Can't get viewers of incoming messages");
  }
  if (G()->unix_time() - m->date >
      td_->option_manager_->get_option_integer("chat_read_mark_expire_period", 7 * 86400)) {
    return Status::Error(400, "Message is too old");
  }

  int32 participant_count = 0;
  switch (dialog_id.get_type()) {
    case DialogType::User:
      return Status::Error(400, "Can't get message viewers in private chats");
    case DialogType::Chat:
      if (!td_->contacts_manager_->get_chat_is_active(dialog_id.get_chat_id())) {
        return Status::Error(400, "Chat is deactivated");
      }
      participant_count = td_->contacts_manager_->get_chat_participant_count(dialog_id.get_chat_id());
      break;
    case DialogType::Channel:
      if (td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
        return Status::Error(400, "Can't get message viewers in channel chats");
      }
      if (td_->contacts_manager_->get_channel_effective_has_hidden_participants(dialog_id.get_channel_id(),
                                                                                "can_get_message_viewers")) {
        return Status::Error(400, "Participant list is hidden in the chat");
      }
      participant_count = td_->contacts_manager_->get_channel_participant_count(dialog_id.get_channel_id());
      break;
    case DialogType::SecretChat:
      return Status::Error(400, "Can't get message viewers in secret chats");
    case DialogType::None:
    default:
      UNREACHABLE();
      return Status::OK();
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(400, "Can't access the chat");
  }
  if (participant_count == 0) {
    return Status::Error(400, "Chat is empty or have unknown number of members");
  }
  if (participant_count > td_->option_manager_->get_option_integer("chat_read_mark_size_threshold", 100)) {
    return Status::Error(400, "Chat is too big");
  }

  if (m->message_id.is_scheduled()) {
    return Status::Error(400, "Scheduled messages can't have viewers");
  }
  if (m->message_id.is_yet_unsent()) {
    return Status::Error(400, "Yet unsent messages can't have viewers");
  }
  if (m->message_id.is_local()) {
    return Status::Error(400, "Local messages can't have viewers");
  }
  CHECK(m->message_id.is_server());

  if (m->content->get_type() == MessageContentType::Poll &&
      get_message_content_poll_is_anonymous(td_, m->content.get())) {
    return Status::Error(400, "Anonymous poll viewers are unavailable");
  }

  return Status::OK();
}

void MessagesManager::get_message_viewers(MessageFullId message_full_id,
                                          Promise<td_api::object_ptr<td_api::messageViewers>> &&promise) {
  TRY_STATUS_PROMISE(promise, can_get_message_viewers(message_full_id));

  auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_id = message_full_id.get_dialog_id(),
                                               promise = std::move(promise)](Result<MessageViewers> result) mutable {
    if (result.is_error()) {
      return promise.set_error(result.move_as_error());
    }
    send_closure(actor_id, &MessagesManager::on_get_message_viewers, dialog_id, result.move_as_ok(), false,
                 std::move(promise));
  });

  td_->create_handler<GetMessageReadParticipantsQuery>(std::move(query_promise))
      ->send(message_full_id.get_dialog_id(), message_full_id.get_message_id());
}

void MessagesManager::on_get_message_viewers(DialogId dialog_id, MessageViewers message_viewers, bool is_recursive,
                                             Promise<td_api::object_ptr<td_api::messageViewers>> &&promise) {
  if (!is_recursive) {
    bool need_participant_list = false;
    for (auto user_id : message_viewers.get_user_ids()) {
      if (!td_->contacts_manager_->have_user_force(user_id, "on_get_message_viewers")) {
        need_participant_list = true;
      }
    }
    if (need_participant_list) {
      auto query_promise =
          PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, message_viewers = std::move(message_viewers),
                                  promise = std::move(promise)](Unit result) mutable {
            send_closure(actor_id, &MessagesManager::on_get_message_viewers, dialog_id, std::move(message_viewers),
                         true, std::move(promise));
          });

      switch (dialog_id.get_type()) {
        case DialogType::Chat:
          return td_->contacts_manager_->reload_chat_full(dialog_id.get_chat_id(), std::move(query_promise),
                                                          "on_get_message_viewers");
        case DialogType::Channel:
          return td_->dialog_participant_manager_->get_channel_participants(
              dialog_id.get_channel_id(), td_api::make_object<td_api::supergroupMembersFilterRecent>(), string(), 0,
              200, 200, PromiseCreator::lambda([query_promise = std::move(query_promise)](DialogParticipants) mutable {
                query_promise.set_value(Unit());
              }));
        default:
          UNREACHABLE();
          return;
      }
    }
  }
  promise.set_value(message_viewers.get_message_viewers_object(td_->contacts_manager_.get()));
}

void MessagesManager::translate_message_text(MessageFullId message_full_id, const string &to_language_code,
                                             Promise<td_api::object_ptr<td_api::formattedText>> &&promise) {
  auto m = get_message_force(message_full_id, "translate_message_text");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }

  auto text = get_message_content_text(m->content.get());
  if (text == nullptr) {
    return promise.set_value(td_api::make_object<td_api::formattedText>());
  }

  auto skip_bot_commands = need_skip_bot_commands(message_full_id.get_dialog_id(), m);
  auto max_media_timestamp = get_message_max_media_timestamp(m);
  td_->translation_manager_->translate_text(*text, skip_bot_commands, max_media_timestamp, to_language_code,
                                            std::move(promise));
}

void MessagesManager::reload_dialog_notification_settings(DialogId dialog_id, Promise<Unit> &&promise,
                                                          const char *source) {
  LOG(INFO) << "Reload notification settings for " << dialog_id << " from " << source;
  const Dialog *d = get_dialog(dialog_id);
  if (d != nullptr) {
    td_->notification_settings_manager_->send_get_dialog_notification_settings_query(dialog_id, MessageId(),
                                                                                     std::move(promise));
  } else {
    send_get_dialog_query(dialog_id, std::move(promise), 0, source);
  }
}

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

  LOG(INFO) << "Get pinned message in " << dialog_id << " with "
            << (d->is_last_pinned_message_id_inited ? "inited" : "unknown") << " pinned " << d->last_pinned_message_id;

  if (!d->is_last_pinned_message_id_inited) {
    // must call get_dialog_info_full as expected in fix_new_dialog
    td_->dialog_manager_->get_dialog_info_full(dialog_id, std::move(promise), "get_dialog_pinned_message 1");
    return MessageId();
  }

  td_->dialog_manager_->get_dialog_info_full(dialog_id, Auto(), "get_dialog_pinned_message 2");

  if (d->last_pinned_message_id.is_valid()) {
    tl_object_ptr<telegram_api::InputMessage> input_message;
    if (dialog_id.get_type() == DialogType::Channel) {
      input_message = make_tl_object<telegram_api::inputMessagePinned>();
    }
    get_message_force_from_server(d, d->last_pinned_message_id, std::move(promise), std::move(input_message));
  } else {
    promise.set_value(Unit());
  }

  return d->last_pinned_message_id;
}

void MessagesManager::get_callback_query_message(DialogId dialog_id, MessageId message_id, int64 callback_query_id,
                                                 Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(dialog_id, "get_callback_query_message");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  if (!message_id.is_valid() || !message_id.is_server()) {
    return promise.set_error(Status::Error(400, "Invalid message identifier specified"));
  }

  LOG(INFO) << "Get callback query " << message_id << " in " << dialog_id << " for query " << callback_query_id;

  auto input_message = make_tl_object<telegram_api::inputMessageCallbackQuery>(message_id.get_server_message_id().get(),
                                                                               callback_query_id);
  get_message_force_from_server(d, message_id, std::move(promise), std::move(input_message));
}

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

  bool is_secret = dialog_id.get_type() == DialogType::SecretChat;
  vector<MessageFullId> missed_message_ids;
  for (auto message_id : message_ids) {
    if (!message_id.is_valid() && !message_id.is_valid_scheduled()) {
      promise.set_error(Status::Error(400, "Invalid message identifier"));
      return false;
    }

    auto *m = get_message_force(d, message_id, "get_messages");
    if (m == nullptr && message_id.is_any_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), "get_messages");
    return false;
  }

  promise.set_value(Unit());
  return true;
}

void MessagesManager::get_message_from_server(MessageFullId message_full_id, Promise<Unit> &&promise,
                                              const char *source,
                                              tl_object_ptr<telegram_api::InputMessage> input_message) {
  get_messages_from_server({message_full_id}, std::move(promise), source, std::move(input_message));
}

void MessagesManager::get_messages_from_server(vector<MessageFullId> &&message_ids, Promise<Unit> &&promise,
                                               const char *source,
                                               tl_object_ptr<telegram_api::InputMessage> input_message) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  if (message_ids.empty()) {
    LOG(ERROR) << "Empty message_ids from " << source;
    return promise.set_error(Status::Error(500, "There are no messages specified to fetch"));
  }

  if (input_message != nullptr) {
    CHECK(message_ids.size() == 1);
  }

  vector<tl_object_ptr<telegram_api::InputMessage>> ordinary_message_ids;
  FlatHashMap<ChannelId, vector<tl_object_ptr<telegram_api::InputMessage>>, ChannelIdHash> channel_message_ids;
  FlatHashMap<DialogId, vector<int32>, DialogIdHash> scheduled_message_ids;
  for (auto &message_full_id : message_ids) {
    auto dialog_id = message_full_id.get_dialog_id();
    auto message_id = message_full_id.get_message_id();
    if (!message_id.is_valid() || !message_id.is_server()) {
      if (message_id.is_valid_scheduled() && message_id.is_scheduled_server() && dialog_id.is_valid()) {
        scheduled_message_ids[dialog_id].push_back(message_id.get_scheduled_server_message_id().get());
      }
      continue;
    }

    if (input_message == nullptr) {
      input_message = make_tl_object<telegram_api::inputMessageID>(message_id.get_server_message_id().get());
    }

    switch (dialog_id.get_type()) {
      case DialogType::User:
      case DialogType::Chat:
        ordinary_message_ids.push_back(std::move(input_message));
        break;
      case DialogType::Channel:
        channel_message_ids[dialog_id.get_channel_id()].push_back(std::move(input_message));
        break;
      case DialogType::SecretChat:
        LOG(ERROR) << "Can't get " << message_full_id << " from server from " << source;
        break;
      case DialogType::None:
      default:
        UNREACHABLE();
        break;
    }
  }

  MultiPromiseActorSafe mpas{"GetMessagesOnServerMultiPromiseActor"};
  mpas.add_promise(std::move(promise));
  auto lock = mpas.get_promise();

  if (!ordinary_message_ids.empty()) {
    td_->create_handler<GetMessagesQuery>(mpas.get_promise())->send(std::move(ordinary_message_ids));
  }

  for (auto &it : scheduled_message_ids) {
    auto dialog_id = it.first;
    have_dialog_force(dialog_id, "get_messages_from_server");
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read);
    if (input_peer == nullptr) {
      LOG(ERROR) << "Can't find info about " << dialog_id << " to get a message from it from " << source;
      mpas.get_promise().set_error(Status::Error(400, "Can't access the chat"));
      continue;
    }
    td_->create_handler<GetScheduledMessagesQuery>(mpas.get_promise())
        ->send(dialog_id, std::move(input_peer), std::move(it.second));
  }

  for (auto &it : channel_message_ids) {
    td_->contacts_manager_->have_channel_force(it.first, "get_messages_from_server");
    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 from " << source;
      mpas.get_promise().set_error(Status::Error(400, "Can't access the chat"));
      continue;
    }
    const auto *d = get_dialog_force(DialogId(it.first));
    td_->create_handler<GetChannelMessagesQuery>(mpas.get_promise())
        ->send(it.first, std::move(input_channel), std::move(it.second),
               d == nullptr ? MessageId() : d->last_new_message_id);
  }
  lock.set_value(Unit());
}

bool MessagesManager::is_message_edited_recently(MessageFullId message_full_id, int32 seconds) {
  if (seconds < 0) {
    return false;
  }
  if (!message_full_id.get_message_id().is_valid()) {
    return false;
  }

  auto m = get_message_force(message_full_id, "is_message_edited_recently");
  if (m == nullptr) {
    return true;
  }

  return m->edit_date >= G()->unix_time() - seconds;
}

MessagesManager::ReportDialogFromActionBar MessagesManager::report_dialog_from_action_bar(DialogId dialog_id,
                                                                                          Promise<Unit> &promise) {
  ReportDialogFromActionBar result;
  Dialog *d = nullptr;
  if (dialog_id.get_type() == DialogType::SecretChat) {
    auto user_dialog_id = DialogId(td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id()));
    d = get_dialog_force(user_dialog_id, "report_dialog_from_action_bar");
    if (d == nullptr) {
      promise.set_error(Status::Error(400, "Chat with the user not found"));
      result.is_reported_ = true;
      return result;
    }
  } else {
    d = get_dialog(dialog_id);
    CHECK(d != nullptr);
  }
  result.know_action_bar_ = d->know_action_bar;

  if (d->know_action_bar && d->action_bar != nullptr && d->action_bar->can_report_spam()) {
    result.is_reported_ = true;
    hide_dialog_action_bar(d);
    toggle_dialog_report_spam_state_on_server(dialog_id, true, 0, std::move(promise));
  }
  return result;
}

Status MessagesManager::can_get_media_timestamp_link(DialogId dialog_id, const Message *m) {
  if (m == nullptr) {
    return Status::Error(400, "Message not found");
  }

  if (dialog_id.get_type() != DialogType::Channel) {
    if (!can_message_content_have_media_timestamp(m->content.get()) || m->forward_info == nullptr ||
        m->forward_info->is_imported()) {
      return Status::Error(400, "Message links are available only for messages in supergroups and channel chats");
    }
    auto origin_message_full_id = m->forward_info->get_origin_message_full_id();
    auto origin_message_id = origin_message_full_id.get_message_id();
    if (!origin_message_id.is_valid() || !origin_message_id.is_server()) {
      return Status::Error(400, "Message links are available only for messages in supergroups and channel chats");
    }
    return Status::OK();
  }

  if (m->message_id.is_yet_unsent()) {
    return Status::Error(400, "Message is not sent yet");
  }
  if (m->message_id.is_scheduled()) {
    return Status::Error(400, "Message is scheduled");
  }
  if (!m->message_id.is_server()) {
    return Status::Error(400, "Message is local");
  }
  return Status::OK();
}

bool MessagesManager::can_report_message_reactions(DialogId dialog_id, const Message *m) const {
  CHECK(m != nullptr);
  if (dialog_id.get_type() != DialogType::Channel || td_->dialog_manager_->is_broadcast_channel(dialog_id) ||
      !td_->contacts_manager_->is_channel_public(dialog_id.get_channel_id())) {
    return false;
  }
  if (m->message_id.is_scheduled() || !m->message_id.is_server()) {
    return false;
  }
  if (is_discussion_message(dialog_id, m)) {
    return false;
  }

  return true;
}

Result<std::pair<string, bool>> MessagesManager::get_message_link(MessageFullId message_full_id, int32 media_timestamp,
                                                                  bool for_group, bool in_message_thread) {
  auto dialog_id = message_full_id.get_dialog_id();
  auto d = get_dialog_force(dialog_id, "get_message_link");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(400, "Can't access the chat");
  }

  auto *m = get_message_force(d, message_full_id.get_message_id(), "get_message_link");
  TRY_STATUS(can_get_media_timestamp_link(dialog_id, m));

  if (media_timestamp <= 0 || !can_message_content_have_media_timestamp(m->content.get())) {
    media_timestamp = 0;
  }
  if (media_timestamp != 0) {
    for_group = false;
    auto duration = get_message_content_media_duration(m->content.get(), td_);
    if (duration != 0 && media_timestamp > duration) {
      media_timestamp = 0;
    }
  }

  auto message_id = m->message_id;
  if (dialog_id.get_type() != DialogType::Channel) {
    CHECK(m->forward_info != nullptr);

    auto origin_message_full_id = m->forward_info->get_origin_message_full_id();
    dialog_id = origin_message_full_id.get_dialog_id();
    message_id = origin_message_full_id.get_message_id();
    CHECK(dialog_id.get_type() == DialogType::Channel);
    for_group = false;
    in_message_thread = false;
    auto channel_message = get_message(origin_message_full_id);
    if (channel_message != nullptr && channel_message->media_album_id == 0) {
      for_group = true;  // default is true
    }
  } else {
    if (m->media_album_id == 0) {
      for_group = true;  // default is true
    }
  }

  bool is_forum = td_->dialog_manager_->is_forum_channel(dialog_id);
  if (!is_forum &&
      (!m->top_thread_message_id.is_valid() || !m->top_thread_message_id.is_server() ||
       is_deleted_message(d, m->top_thread_message_id) || td_->dialog_manager_->is_broadcast_channel(dialog_id))) {
    in_message_thread = false;
  }

  if (!td_->auth_manager_->is_bot()) {
    td_->create_handler<ExportChannelMessageLinkQuery>(Promise<Unit>())
        ->send(dialog_id.get_channel_id(), message_id, for_group, true);
  }

  SliceBuilder sb;
  sb << LinkManager::get_t_me_url();

  if (in_message_thread && !is_forum) {
    // try to generate a comment link
    auto *top_m = get_message_force(d, m->top_thread_message_id, "get_public_message_link");
    if (is_discussion_message(dialog_id, top_m) && is_active_message_reply_info(dialog_id, top_m->reply_info)) {
      auto linked_message_full_id = top_m->forward_info->get_last_message_full_id();
      auto linked_dialog_id = linked_message_full_id.get_dialog_id();
      auto linked_message_id = linked_message_full_id.get_message_id();
      auto linked_d = get_dialog(linked_dialog_id);
      CHECK(linked_d != nullptr);
      CHECK(linked_dialog_id.get_type() == DialogType::Channel);
      auto *linked_m = get_message_force(linked_d, linked_message_id, "get_public_message_link");
      auto channel_username = td_->contacts_manager_->get_channel_first_username(linked_dialog_id.get_channel_id());
      if (linked_m != nullptr && is_active_message_reply_info(linked_dialog_id, linked_m->reply_info) &&
          linked_message_id.is_server() &&
          td_->dialog_manager_->have_input_peer(linked_dialog_id, AccessRights::Read) && !channel_username.empty()) {
        sb << channel_username << '/' << linked_message_id.get_server_message_id().get()
           << "?comment=" << message_id.get_server_message_id().get();
        if (!for_group) {
          sb << "&single";
        }
        if (media_timestamp > 0) {
          sb << "&t=" << media_timestamp;
        }
        return std::make_pair(sb.as_cslice().str(), true);
      }
    }
  }

  auto dialog_username = td_->contacts_manager_->get_channel_first_username(dialog_id.get_channel_id());
  bool is_public = !dialog_username.empty();
  if (m->content->get_type() == MessageContentType::VideoNote &&
      td_->dialog_manager_->is_broadcast_channel(dialog_id) && is_public) {
    return std::make_pair(
        PSTRING() << "https://telesco.pe/" << dialog_username << '/' << message_id.get_server_message_id().get(), true);
  }

  if (is_public) {
    sb << dialog_username;
  } else {
    sb << "c/" << dialog_id.get_channel_id().get();
  }
  if (in_message_thread && is_forum) {
    auto top_thread_message_id = m->is_topic_message ? m->top_thread_message_id : MessageId(ServerMessageId(1));
    if (top_thread_message_id != message_id) {
      sb << '/' << top_thread_message_id.get_server_message_id().get();
    }
    in_message_thread = false;
  }
  sb << '/' << message_id.get_server_message_id().get();

  char separator = '?';
  if (in_message_thread) {
    sb << separator << "thread=" << m->top_thread_message_id.get_server_message_id().get();
    separator = '&';
  }
  if (!for_group) {
    sb << separator << "single";
    separator = '&';
  }
  if (media_timestamp > 0) {
    sb << separator << "t=" << media_timestamp;
    separator = '&';
  }
  CHECK(separator == '?' || separator == '&');

  return std::make_pair(sb.as_cslice().str(), is_public);
}

string MessagesManager::get_message_embedding_code(MessageFullId message_full_id, bool for_group,
                                                   Promise<Unit> &&promise) {
  auto dialog_id = message_full_id.get_dialog_id();
  auto d = get_dialog_force(dialog_id, "get_message_embedding_code");
  if (d == nullptr) {
    promise.set_error(Status::Error(400, "Chat not found"));
    return {};
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(400, "Can't access the chat"));
    return {};
  }
  if (dialog_id.get_type() != DialogType::Channel ||
      td_->contacts_manager_->get_channel_first_username(dialog_id.get_channel_id()).empty()) {
    promise.set_error(Status::Error(
        400, "Message embedding code is available only for messages in public supergroups and channel chats"));
    return {};
  }

  auto *m = get_message_force(d, message_full_id.get_message_id(), "get_message_embedding_code");
  if (m == nullptr) {
    promise.set_error(Status::Error(400, "Message not found"));
    return {};
  }
  if (m->message_id.is_yet_unsent()) {
    promise.set_error(Status::Error(400, "Message is not sent yet"));
    return {};
  }
  if (m->message_id.is_scheduled()) {
    promise.set_error(Status::Error(400, "Message is scheduled"));
    return {};
  }
  if (!m->message_id.is_server()) {
    promise.set_error(Status::Error(400, "Message is local"));
    return {};
  }

  if (m->media_album_id == 0) {
    for_group = true;  // default is true
  }

  auto &links = message_embedding_codes_[for_group][dialog_id].embedding_codes_;
  auto it = links.find(m->message_id);
  if (it == links.end()) {
    td_->create_handler<ExportChannelMessageLinkQuery>(std::move(promise))
        ->send(dialog_id.get_channel_id(), m->message_id, for_group, false);
    return {};
  }

  promise.set_value(Unit());
  return it->second;
}

void MessagesManager::on_get_public_message_link(MessageFullId message_full_id, bool for_group, string url,
                                                 string html) {
  LOG_IF(ERROR, url.empty() && html.empty()) << "Receive empty public link for " << message_full_id;
  auto dialog_id = message_full_id.get_dialog_id();
  auto message_id = message_full_id.get_message_id();
  message_embedding_codes_[for_group][dialog_id].embedding_codes_[message_id] = std::move(html);
}

void MessagesManager::get_message_link_info(Slice url, Promise<MessageLinkInfo> &&promise) {
  auto r_message_link_info = LinkManager::get_message_link_info(url);
  if (r_message_link_info.is_error()) {
    return promise.set_error(Status::Error(400, r_message_link_info.error().message()));
  }

  auto info = r_message_link_info.move_as_ok();
  auto query_promise = PromiseCreator::lambda(
      [actor_id = actor_id(this), info, promise = std::move(promise)](Result<DialogId> &&result) mutable {
        if (result.is_error()) {
          return promise.set_value(std::move(info));
        }
        send_closure(actor_id, &MessagesManager::on_get_message_link_dialog, std::move(info), result.ok(),
                     std::move(promise));
      });
  td_->dialog_manager_->resolve_dialog(info.username, info.channel_id, std::move(query_promise));
}

void MessagesManager::on_get_message_link_dialog(MessageLinkInfo &&info, DialogId dialog_id,
                                                 Promise<MessageLinkInfo> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  Dialog *d = get_dialog_force(dialog_id, "on_get_message_link_dialog");
  CHECK(d != nullptr);
  get_message_force_from_server(d, info.message_id,
                                PromiseCreator::lambda([actor_id = actor_id(this), info = std::move(info), dialog_id,
                                                        promise = std::move(promise)](Result<Unit> &&result) mutable {
                                  if (result.is_error()) {
                                    return promise.set_value(std::move(info));
                                  }
                                  send_closure(actor_id, &MessagesManager::on_get_message_link_message, std::move(info),
                                               dialog_id, std::move(promise));
                                }));
}

void MessagesManager::on_get_message_link_message(MessageLinkInfo &&info, DialogId dialog_id,
                                                  Promise<MessageLinkInfo> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  auto message_id = info.message_id;
  Message *m = get_message_force({dialog_id, message_id}, "on_get_message_link_message");
  if (info.comment_message_id == MessageId() || m == nullptr ||
      !td_->dialog_manager_->is_broadcast_channel(dialog_id) || !m->reply_info.is_comment_ ||
      !is_active_message_reply_info(dialog_id, m->reply_info)) {
    return promise.set_value(std::move(info));
  }

  if (td_->contacts_manager_->have_channel_force(m->reply_info.channel_id_, "on_get_message_link_message")) {
    force_create_dialog(DialogId(m->reply_info.channel_id_), "on_get_message_link_message");
    on_get_message_link_discussion_message(std::move(info), DialogId(m->reply_info.channel_id_), std::move(promise));
    return;
  }

  auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), info = std::move(info),
                                               promise = std::move(promise)](Result<MessageThreadInfo> result) mutable {
    if (result.is_error() || result.ok().message_ids.empty()) {
      return promise.set_value(std::move(info));
    }
    send_closure(actor_id, &MessagesManager::on_get_message_link_discussion_message, std::move(info),
                 result.ok().dialog_id, std::move(promise));
  });

  td_->create_handler<GetDiscussionMessageQuery>(std::move(query_promise))
      ->send(dialog_id, message_id, DialogId(m->reply_info.channel_id_), MessageId());
}

void MessagesManager::on_get_message_link_discussion_message(MessageLinkInfo &&info, DialogId comment_dialog_id,
                                                             Promise<MessageLinkInfo> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  CHECK(comment_dialog_id.is_valid());
  info.comment_dialog_id = comment_dialog_id;

  Dialog *d = get_dialog_force(comment_dialog_id, "on_get_message_link_discussion_message");
  if (d == nullptr) {
    return promise.set_error(Status::Error(500, "Chat not found"));
  }

  auto comment_message_id = info.comment_message_id;
  get_message_force_from_server(
      d, comment_message_id,
      PromiseCreator::lambda([info = std::move(info), promise = std::move(promise)](Result<Unit> &&result) mutable {
        return promise.set_value(std::move(info));
      }));
}

td_api::object_ptr<td_api::messageLinkInfo> MessagesManager::get_message_link_info_object(
    const MessageLinkInfo &info) const {
  CHECK(info.username.empty() == info.channel_id.is_valid());

  bool is_public = !info.username.empty();
  DialogId dialog_id = info.comment_dialog_id.is_valid()
                           ? info.comment_dialog_id
                           : (is_public ? td_->dialog_manager_->get_resolved_dialog_by_username(info.username)
                                        : DialogId(info.channel_id));
  MessageId top_thread_message_id;
  MessageId message_id = info.comment_dialog_id.is_valid() ? info.comment_message_id : info.message_id;
  td_api::object_ptr<td_api::message> message;
  int32 media_timestamp = 0;
  bool for_album = false;

  const Dialog *d = get_dialog(dialog_id);
  if (d == nullptr) {
    dialog_id = DialogId();
    top_thread_message_id = MessageId();
  } else {
    const Message *m = get_message(d, message_id);
    if (m != nullptr) {
      message = get_message_object(dialog_id, m, "get_message_link_info_object");
      for_album = !info.is_single && m->media_album_id != 0;
      if (info.comment_dialog_id.is_valid() || info.for_comment || m->is_topic_message) {
        top_thread_message_id = m->top_thread_message_id;
      } else if (td_->dialog_manager_->is_forum_channel(dialog_id) &&
                 info.top_thread_message_id == MessageId(ServerMessageId(1))) {
        // General topic
        top_thread_message_id = info.top_thread_message_id;
      } else {
        top_thread_message_id = MessageId();
      }
      if (can_message_content_have_media_timestamp(m->content.get())) {
        auto duration = get_message_content_media_duration(m->content.get(), td_);
        if (duration == 0 || info.media_timestamp <= duration) {
          media_timestamp = info.media_timestamp;
        }
      }
      if (m->content->get_type() == MessageContentType::TopicCreate && top_thread_message_id.is_valid()) {
        message = nullptr;
        CHECK(!for_album);
        CHECK(media_timestamp == 0);
      }
    } else if (!info.comment_dialog_id.is_valid() && dialog_id.get_type() == DialogType::Channel &&
               !td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
      top_thread_message_id = info.top_thread_message_id;
    }
  }

  return td_api::make_object<td_api::messageLinkInfo>(is_public, get_chat_id_object(dialog_id, "messageLinkInfo"),
                                                      top_thread_message_id.get(), std::move(message), media_timestamp,
                                                      for_album);
}

Status MessagesManager::can_add_dialog_to_filter(DialogId dialog_id) {
  if (!dialog_id.is_valid()) {
    return Status::Error(400, "Invalid chat identifier specified");
  }
  const Dialog *d = get_dialog_force(dialog_id, "can_add_dialog_to_filter");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(400, "Can't access the chat");
  }
  if (d->order == DEFAULT_ORDER) {
    return Status::Error(400, "Chat is not in the chat list");
  }
  return Status::OK();
}

Status MessagesManager::delete_dialog_reply_markup(DialogId dialog_id, MessageId message_id) {
  if (td_->auth_manager_->is_bot()) {
    return Status::Error(400, "Bots can't delete chat reply markup");
  }
  if (message_id.is_scheduled()) {
    return Status::Error(400, "Wrong message identifier specified");
  }
  if (!message_id.is_valid()) {
    return Status::Error(400, "Invalid message identifier specified");
  }

  Dialog *d = get_dialog_force(dialog_id, "delete_dialog_reply_markup");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }
  if (d->reply_markup_message_id != message_id) {
    return Status::OK();
  }

  Message *m = get_message_force(d, message_id, "delete_dialog_reply_markup");
  CHECK(m != nullptr);
  CHECK(m->reply_markup != nullptr);

  if (m->reply_markup->type == ReplyMarkup::Type::ForceReply) {
    set_dialog_reply_markup(d, MessageId());
  } else if (m->reply_markup->type == ReplyMarkup::Type::ShowKeyboard) {
    if (!m->reply_markup->is_one_time_keyboard) {
      return Status::Error(400, "Do not need to delete non one-time keyboard");
    }
    if (m->reply_markup->is_personal) {
      m->reply_markup->is_personal = false;
      set_dialog_reply_markup(d, message_id);

      on_message_changed(d, m, 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, MessageId top_thread_message_id,
                                                 tl_object_ptr<td_api::draftMessage> &&draft_message) {
  if (td_->auth_manager_->is_bot()) {
    return Status::Error(400, "Bots can't change chat draft message");
  }

  Dialog *d = get_dialog_force(dialog_id, "set_dialog_draft_message");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }
  TRY_STATUS(can_send_message(dialog_id));

  TRY_STATUS(can_use_top_thread_message_id(d, top_thread_message_id, MessageInputReplyTo()));

  TRY_RESULT(new_draft_message,
             DraftMessage::get_draft_message(td_, dialog_id, top_thread_message_id, std::move(draft_message)));

  if (top_thread_message_id != MessageId()) {
    CHECK(top_thread_message_id.is_valid());
    CHECK(top_thread_message_id.is_server());
    auto m = get_message_force(d, top_thread_message_id, "set_dialog_draft_message");
    if (m == nullptr || m->reply_info.is_comment_ || !is_active_message_reply_info(dialog_id, m->reply_info)) {
      return Status::OK();
    }

    if (need_update_draft_message(m->thread_draft_message, new_draft_message, false)) {
      m->thread_draft_message = std::move(new_draft_message);
      on_message_changed(d, m, false, "set_dialog_draft_message");
    }

    return Status::OK();
  }

  if (update_dialog_draft_message(d, std::move(new_draft_message), false, true)) {
    if (dialog_id.get_type() != DialogType::SecretChat && !is_local_draft_message(d->draft_message)) {
      if (G()->use_message_database()) {
        SaveDialogDraftMessageOnServerLogEvent log_event;
        log_event.dialog_id_ = dialog_id;
        add_log_event(d->save_draft_message_log_event_id, get_log_event_storer(log_event),
                      LogEvent::HandlerType::SaveDialogDraftMessageOnServer, "draft");
      }

      pending_draft_message_timeout_.set_timeout_in(dialog_id.get(), d->open_count > 0 ? MIN_SAVE_DRAFT_DELAY : 0);
    }
  }
  return Status::OK();
}

void MessagesManager::save_dialog_draft_message_on_server(DialogId dialog_id) {
  if (G()->close_flag()) {
    return;
  }

  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  Promise<Unit> promise;
  if (d->save_draft_message_log_event_id.log_event_id != 0) {
    d->save_draft_message_log_event_id.generation++;
    promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_id,
                                      generation = d->save_draft_message_log_event_id.generation](Result<Unit> result) {
      if (!G()->close_flag()) {
        send_closure(actor_id, &MessagesManager::on_saved_dialog_draft_message, dialog_id, generation);
      }
    });
  }

  save_draft_message(td_, dialog_id, d->draft_message, std::move(promise));
}

void MessagesManager::on_saved_dialog_draft_message(DialogId dialog_id, uint64 generation) {
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  delete_log_event(d->save_draft_message_log_event_id, generation, "draft");
}

void MessagesManager::clear_all_draft_messages(bool exclude_secret_chats, Promise<Unit> &&promise) {
  if (!exclude_secret_chats) {
    dialogs_.foreach([&](const DialogId &dialog_id, unique_ptr<Dialog> &dialog) {
      Dialog *d = dialog.get();
      if (dialog_id.get_type() == DialogType::SecretChat) {
        update_dialog_draft_message(d, nullptr, false, true);
      }
    });
  }
  ::td::clear_all_draft_messages(td_, std::move(promise));
}

int32 MessagesManager::get_pinned_dialogs_limit(DialogListId dialog_list_id) const {
  if (dialog_list_id.is_filter()) {
    return DialogFilter::get_max_filter_dialogs();
  }

  Slice key{"pinned_chat_count_max"};
  int32 default_limit = 5;
  if (!dialog_list_id.is_folder() || dialog_list_id.get_folder_id() != FolderId::main()) {
    key = Slice("pinned_archived_chat_count_max");
    default_limit = 100;
  }
  int32 limit = clamp(narrow_cast<int32>(td_->option_manager_->get_option_integer(key)), 0, 1000);
  if (limit <= 0) {
    if (td_->option_manager_->get_option_boolean("is_premium")) {
      default_limit *= 2;
    }
    return default_limit;
  }
  return limit;
}

Status MessagesManager::toggle_dialog_is_pinned(DialogListId dialog_list_id, DialogId dialog_id, bool is_pinned) {
  if (td_->auth_manager_->is_bot()) {
    return Status::Error(400, "Bots can't change chat pin state");
  }

  Dialog *d = get_dialog_force(dialog_id, "toggle_dialog_is_pinned");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(400, "Can't access the chat");
  }
  if (d->order == DEFAULT_ORDER && is_pinned) {
    return Status::Error(400, "The chat can't be pinned");
  }

  auto *list = get_dialog_list(dialog_list_id);
  if (list == nullptr) {
    return Status::Error(400, "Chat list not found");
  }
  if (!list->are_pinned_dialogs_inited_) {
    return Status::Error(400, "Pinned chats must be loaded first");
  }

  bool was_pinned = is_dialog_pinned(dialog_list_id, dialog_id);
  if (is_pinned == was_pinned) {
    return Status::OK();
  }

  if (dialog_list_id.is_filter()) {
    return td_->dialog_filter_manager_->set_dialog_is_pinned(
        dialog_list_id.get_filter_id(), td_->dialog_manager_->get_input_dialog_id(dialog_id), is_pinned);
  }

  CHECK(dialog_list_id.is_folder());
  auto folder_id = dialog_list_id.get_folder_id();
  if (is_pinned) {
    if (d->folder_id != folder_id) {
      return Status::Error(400, "Chat not in the list");
    }

    auto pinned_dialog_ids = get_pinned_dialog_ids(dialog_list_id);
    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(dialog_list_id))) {
      return Status::Error(400, "The maximum number of pinned chats exceeded");
    }
  }

  if (set_dialog_is_pinned(dialog_list_id, d, 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_log_event(DialogId dialog_id, bool is_pinned) {
  ToggleDialogIsPinnedOnServerLogEvent log_event{dialog_id, is_pinned};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ToggleDialogIsPinnedOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::toggle_dialog_is_pinned_on_server(DialogId dialog_id, bool is_pinned, uint64 log_event_id) {
  CHECK(!td_->auth_manager_->is_bot());
  if (log_event_id == 0 && dialog_id.get_type() == DialogType::SecretChat) {
    // don't even create new binlog events
    return;
  }

  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_toggle_dialog_is_pinned_on_server_log_event(dialog_id, is_pinned);
  }

  td_->create_handler<ToggleDialogPinQuery>(get_erase_log_event_promise(log_event_id))->send(dialog_id, is_pinned);
}

bool MessagesManager::set_folder_pinned_dialogs(FolderId folder_id, vector<DialogId> old_dialog_ids,
                                                vector<DialogId> new_dialog_ids) {
  LOG(INFO) << "Reorder pinned chats in " << folder_id << " from " << old_dialog_ids << " to " << new_dialog_ids;

  std::reverse(old_dialog_ids.begin(), old_dialog_ids.end());
  std::reverse(new_dialog_ids.begin(), new_dialog_ids.end());

  FlatHashSet<DialogId, DialogIdHash> all_old_pinned_dialog_ids;
  for (auto dialog_id : old_dialog_ids) {
    CHECK(dialog_id.is_valid());
    all_old_pinned_dialog_ids.insert(dialog_id);
  }

  bool are_pinned_dialogs_saved = false;
  auto old_it = old_dialog_ids.begin();
  for (auto dialog_id : new_dialog_ids) {
    all_old_pinned_dialog_ids.erase(dialog_id);
    while (old_it < old_dialog_ids.end()) {
      if (*old_it == dialog_id) {
        break;
      }
      ++old_it;
    }
    if (old_it < old_dialog_ids.end()) {
      // leave dialog where it is
      ++old_it;
      continue;
    }
    if (set_dialog_is_pinned(dialog_id, true)) {
      are_pinned_dialogs_saved = true;
    }
  }
  for (auto dialog_id : all_old_pinned_dialog_ids) {
    Dialog *d = get_dialog_force(dialog_id, "set_folder_pinned_dialogs 1");
    if (d == nullptr) {
      LOG(ERROR) << "Failed to find " << dialog_id << " to unpin in " << folder_id;
      force_create_dialog(dialog_id, "set_folder_pinned_dialogs 2", true);
      d = get_dialog_force(dialog_id, "set_folder_pinned_dialogs 3");
    }
    if (d != nullptr && set_dialog_is_pinned(DialogListId(folder_id), d, false)) {
      are_pinned_dialogs_saved = true;
    }
  }
  return are_pinned_dialogs_saved;
}

Status MessagesManager::set_pinned_dialogs(DialogListId dialog_list_id, vector<DialogId> dialog_ids) {
  if (td_->auth_manager_->is_bot()) {
    return Status::Error(400, "Bots can't reorder pinned chats");
  }

  int32 dialog_count = 0;
  int32 secret_dialog_count = 0;
  auto dialog_count_limit = get_pinned_dialogs_limit(dialog_list_id);
  FlatHashSet<DialogId, DialogIdHash> new_pinned_dialog_ids;
  for (auto dialog_id : dialog_ids) {
    Dialog *d = get_dialog_force(dialog_id, "set_pinned_dialogs");
    if (d == nullptr) {
      return Status::Error(400, "Chat not found");
    }
    if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
      return Status::Error(400, "Can't access the chat");
    }
    if (d->order == DEFAULT_ORDER) {
      return Status::Error(400, "The chat can't be pinned");
    }
    if (dialog_list_id.is_folder() && d->folder_id != dialog_list_id.get_folder_id()) {
      return Status::Error(400, "Chat not in the list");
    }
    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, "The maximum number of pinned chats exceeded");
    }

    new_pinned_dialog_ids.insert(dialog_id);
  }
  if (new_pinned_dialog_ids.size() != dialog_ids.size()) {
    return Status::Error(400, "Duplicate chats in the list of pinned chats");
  }

  auto *list = get_dialog_list(dialog_list_id);
  if (list == nullptr) {
    return Status::Error(400, "Chat list not found");
  }
  if (!list->are_pinned_dialogs_inited_) {
    return Status::Error(400, "Pinned chats must be loaded first");
  }

  auto pinned_dialog_ids = get_pinned_dialog_ids(dialog_list_id);
  if (pinned_dialog_ids == dialog_ids) {
    return Status::OK();
  }

  auto server_old_dialog_ids = DialogId::remove_secret_chat_dialog_ids(pinned_dialog_ids);
  auto server_new_dialog_ids = DialogId::remove_secret_chat_dialog_ids(dialog_ids);

  if (dialog_list_id.is_filter()) {
    return td_->dialog_filter_manager_->set_pinned_dialog_ids(
        dialog_list_id.get_filter_id(),
        transform(dialog_ids,
                  [this](DialogId dialog_id) { return td_->dialog_manager_->get_input_dialog_id(dialog_id); }),
        server_old_dialog_ids != server_new_dialog_ids);
  }

  auto folder_id = dialog_list_id.get_folder_id();
  set_folder_pinned_dialogs(folder_id, std::move(pinned_dialog_ids), std::move(dialog_ids));

  if (server_old_dialog_ids != server_new_dialog_ids) {
    reorder_pinned_dialogs_on_server(folder_id, server_new_dialog_ids, 0);
  }
  return Status::OK();
}

class MessagesManager::ReorderPinnedDialogsOnServerLogEvent {
 public:
  FolderId folder_id_;
  vector<DialogId> dialog_ids_;

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

  template <class ParserT>
  void parse(ParserT &parser) {
    if (parser.version() >= static_cast<int32>(Version::AddFolders)) {
      td::parse(folder_id_, parser);
    } else {
      folder_id_ = FolderId();
    }
    td::parse(dialog_ids_, parser);
  }
};

uint64 MessagesManager::save_reorder_pinned_dialogs_on_server_log_event(FolderId folder_id,
                                                                        const vector<DialogId> &dialog_ids) {
  ReorderPinnedDialogsOnServerLogEvent log_event{folder_id, dialog_ids};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ReorderPinnedDialogsOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::reorder_pinned_dialogs_on_server(FolderId folder_id, const vector<DialogId> &dialog_ids,
                                                       uint64 log_event_id) {
  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_reorder_pinned_dialogs_on_server_log_event(folder_id, dialog_ids);
  }

  td_->create_handler<ReorderPinnedDialogsQuery>(get_erase_log_event_promise(log_event_id))
      ->send(folder_id, dialog_ids);
}

Status MessagesManager::toggle_dialog_view_as_messages(DialogId dialog_id, bool view_as_messages) {
  Dialog *d = get_dialog_force(dialog_id, "toggle_dialog_view_as_messages");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(400, "Can't access the chat");
  }
  bool is_saved_messages = dialog_id == td_->dialog_manager_->get_my_dialog_id();
  if (!is_saved_messages && !td_->dialog_manager_->is_forum_channel(dialog_id)) {
    return Status::Error(400, "The method is available only in forum channels");
  }

  if (view_as_messages == d->view_as_messages) {
    return Status::OK();
  }

  set_dialog_view_as_messages(d, view_as_messages, "toggle_dialog_view_as_messages");

  if (!is_saved_messages) {
    toggle_dialog_view_as_messages_on_server(dialog_id, view_as_messages, 0);
  }
  return Status::OK();
}

class MessagesManager::ToggleDialogViewAsMessagesOnServerLogEvent {
 public:
  DialogId dialog_id_;
  bool view_as_messages_;

  template <class StorerT>
  void store(StorerT &storer) const {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(view_as_messages_);
    END_STORE_FLAGS();

    td::store(dialog_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(view_as_messages_);
    END_PARSE_FLAGS();

    td::parse(dialog_id_, parser);
  }
};

uint64 MessagesManager::save_toggle_dialog_view_as_messages_on_server_log_event(DialogId dialog_id,
                                                                                bool view_as_messages) {
  ToggleDialogViewAsMessagesOnServerLogEvent log_event{dialog_id, view_as_messages};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ToggleDialogViewAsMessagesOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::toggle_dialog_view_as_messages_on_server(DialogId dialog_id, bool view_as_messages,
                                                               uint64 log_event_id) {
  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_toggle_dialog_view_as_messages_on_server_log_event(dialog_id, view_as_messages);
  }

  td_->create_handler<ToggleViewForumAsMessagesQuery>(get_erase_log_event_promise(log_event_id))
      ->send(dialog_id, view_as_messages);
}

Status MessagesManager::toggle_dialog_is_marked_as_unread(DialogId dialog_id, bool is_marked_as_unread) {
  Dialog *d = get_dialog_force(dialog_id, "toggle_dialog_is_marked_as_unread");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(400, "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_log_event(DialogId dialog_id,
                                                                                   bool is_marked_as_unread) {
  ToggleDialogIsMarkedAsUnreadOnServerLogEvent log_event{dialog_id, is_marked_as_unread};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ToggleDialogIsMarkedAsUnreadOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::toggle_dialog_is_marked_as_unread_on_server(DialogId dialog_id, bool is_marked_as_unread,
                                                                  uint64 log_event_id) {
  if (log_event_id == 0 && dialog_id.get_type() == DialogType::SecretChat) {
    // don't even create new binlog events
    return;
  }

  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_toggle_dialog_is_marked_as_unread_on_server_log_event(dialog_id, is_marked_as_unread);
  }

  td_->create_handler<ToggleDialogUnreadMarkQuery>(get_erase_log_event_promise(log_event_id))
      ->send(dialog_id, is_marked_as_unread);
}

Status MessagesManager::toggle_dialog_is_translatable(DialogId dialog_id, bool is_translatable) {
  Dialog *d = get_dialog_force(dialog_id, "toggle_dialog_is_translatable");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(400, "Can't access the chat");
  }

  if (is_translatable == d->is_translatable) {
    return Status::OK();
  }

  set_dialog_is_translatable(d, is_translatable);

  toggle_dialog_is_translatable_on_server(dialog_id, is_translatable, 0);
  return Status::OK();
}

class MessagesManager::ToggleDialogIsTranslatableOnServerLogEvent {
 public:
  DialogId dialog_id_;
  bool is_translatable_;

  template <class StorerT>
  void store(StorerT &storer) const {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(is_translatable_);
    END_STORE_FLAGS();

    td::store(dialog_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(is_translatable_);
    END_PARSE_FLAGS();

    td::parse(dialog_id_, parser);
  }
};

uint64 MessagesManager::save_toggle_dialog_is_translatable_on_server_log_event(DialogId dialog_id,
                                                                               bool is_translatable) {
  ToggleDialogIsTranslatableOnServerLogEvent log_event{dialog_id, is_translatable};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ToggleDialogIsTranslatableOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::toggle_dialog_is_translatable_on_server(DialogId dialog_id, bool is_translatable,
                                                              uint64 log_event_id) {
  if (log_event_id == 0 && dialog_id.get_type() == DialogType::SecretChat) {
    // don't even create new binlog events
    return;
  }

  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_toggle_dialog_is_translatable_on_server_log_event(dialog_id, is_translatable);
  }

  td_->create_handler<ToggleDialogTranslationsQuery>(get_erase_log_event_promise(log_event_id))
      ->send(dialog_id, is_translatable);
}

Status MessagesManager::set_message_sender_block_list(const td_api::object_ptr<td_api::MessageSender> &sender,
                                                      const td_api::object_ptr<td_api::BlockList> &block_list) {
  TRY_RESULT(dialog_id, get_message_sender_dialog_id(td_, sender, true, false));
  BlockListId block_list_id(block_list);
  bool is_blocked = block_list_id == BlockListId::main();
  bool is_blocked_for_stories = block_list_id == BlockListId::stories();
  switch (dialog_id.get_type()) {
    case DialogType::User:
      if (dialog_id == td_->dialog_manager_->get_my_dialog_id()) {
        return Status::Error(
            400, is_blocked || is_blocked_for_stories ? Slice("Can't block self") : Slice("Can't unblock self"));
      }
      break;
    case DialogType::Chat:
      return Status::Error(400, "Basic group chats can't be blocked");
    case DialogType::Channel:
      // ok
      break;
    case DialogType::SecretChat: {
      auto user_id = td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id());
      if (!user_id.is_valid() || !td_->contacts_manager_->have_user_force(user_id, "set_message_sender_block_list")) {
        return Status::Error(400, "The secret chat can't be blocked");
      }
      dialog_id = DialogId(user_id);
      break;
    }
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  Dialog *d = get_dialog_force(dialog_id, "set_message_sender_block_list");
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Know)) {
    return Status::Error(400, "Message sender isn't accessible");
  }
  if (d != nullptr) {
    if (is_blocked == d->is_blocked && is_blocked_for_stories == d->is_blocked_for_stories) {
      return Status::OK();
    }
    set_dialog_is_blocked(d, is_blocked, is_blocked_for_stories);
  } else {
    CHECK(dialog_id.get_type() == DialogType::User);
    td_->contacts_manager_->on_update_user_is_blocked(dialog_id.get_user_id(), is_blocked, is_blocked_for_stories);
  }

  toggle_dialog_is_blocked_on_server(dialog_id, is_blocked, is_blocked_for_stories, 0);
  return Status::OK();
}

class MessagesManager::ToggleDialogIsBlockedOnServerLogEvent {
 public:
  DialogId dialog_id_;
  bool is_blocked_;
  bool is_blocked_for_stories_;

  template <class StorerT>
  void store(StorerT &storer) const {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(is_blocked_);
    STORE_FLAG(is_blocked_for_stories_);
    END_STORE_FLAGS();

    td::store(dialog_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    BEGIN_PARSE_FLAGS();
    PARSE_FLAG(is_blocked_);
    PARSE_FLAG(is_blocked_for_stories_);
    END_PARSE_FLAGS();

    td::parse(dialog_id_, parser);
  }
};

uint64 MessagesManager::save_toggle_dialog_is_blocked_on_server_log_event(DialogId dialog_id, bool is_blocked,
                                                                          bool is_blocked_for_stories) {
  ToggleDialogIsBlockedOnServerLogEvent log_event{dialog_id, is_blocked, is_blocked_for_stories};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ToggleDialogIsBlockedOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::toggle_dialog_is_blocked_on_server(DialogId dialog_id, bool is_blocked,
                                                         bool is_blocked_for_stories, uint64 log_event_id) {
  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_toggle_dialog_is_blocked_on_server_log_event(dialog_id, is_blocked, is_blocked_for_stories);
  }

  td_->create_handler<ToggleDialogIsBlockedQuery>(get_erase_log_event_promise(log_event_id))
      ->send(dialog_id, is_blocked, is_blocked_for_stories);
}

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, "toggle_dialog_silent_send_message");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(400, "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 (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  if (!from_binlog && td_->notification_settings_manager_->get_input_notify_peer(dialog_id, MessageId()) == nullptr) {
    // don't even create new binlog events
    return;
  }

  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  if (!from_binlog && G()->use_message_database()) {
    UpdateDialogNotificationSettingsOnServerLogEvent log_event;
    log_event.dialog_id_ = dialog_id;
    add_log_event(d->save_notification_settings_log_event_id, get_log_event_storer(log_event),
                  LogEvent::HandlerType::UpdateDialogNotificationSettingsOnServer, "notification settings");
  }

  Promise<Unit> promise;
  if (d->save_notification_settings_log_event_id.log_event_id != 0) {
    d->save_notification_settings_log_event_id.generation++;
    promise = PromiseCreator::lambda(
        [actor_id = actor_id(this), dialog_id,
         generation = d->save_notification_settings_log_event_id.generation](Result<Unit> result) {
          if (!G()->close_flag()) {
            send_closure(actor_id, &MessagesManager::on_updated_dialog_notification_settings, dialog_id, generation);
          }
        });
  }

  send_update_dialog_notification_settings_query(d, std::move(promise));
}

void MessagesManager::send_update_dialog_notification_settings_query(const Dialog *d, Promise<Unit> &&promise) {
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(d != nullptr);
  // TODO do not send two queries simultaneously or use InvokeAfter
  td_->notification_settings_manager_->update_dialog_notify_settings(d->dialog_id, MessageId(),
                                                                     d->notification_settings, std::move(promise));
}

void MessagesManager::on_updated_dialog_notification_settings(DialogId dialog_id, uint64 generation) {
  CHECK(!td_->auth_manager_->is_bot());
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  delete_log_event(d->save_notification_settings_log_event_id, generation, "notification settings");
}

Status MessagesManager::set_dialog_client_data(DialogId dialog_id, string &&client_data) {
  Dialog *d = get_dialog_force(dialog_id, "set_dialog_client_data");
  if (d == nullptr) {
    return Status::Error(400, "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(!td_->auth_manager_->is_bot());
  CHECK(d != nullptr);
  if (d->notification_settings.use_default_mute_until) {
    auto scope = td_->dialog_manager_->get_dialog_notification_setting_scope(d->dialog_id);
    return td_->notification_settings_manager_->get_scope_mute_until(scope);
  } else {
    return d->notification_settings.mute_until;
  }
}

bool MessagesManager::is_dialog_muted(const Dialog *d) const {
  return get_dialog_mute_until(d) != 0;
}

bool MessagesManager::is_dialog_pinned_message_notifications_disabled(const Dialog *d) const {
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(d != nullptr);
  if (d->notification_settings.use_default_disable_pinned_message_notifications) {
    auto scope = td_->dialog_manager_->get_dialog_notification_setting_scope(d->dialog_id);
    return td_->notification_settings_manager_->get_scope_disable_pinned_message_notifications(scope);
  }

  return d->notification_settings.disable_pinned_message_notifications;
}

bool MessagesManager::is_dialog_mention_notifications_disabled(const Dialog *d) const {
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(d != nullptr);
  if (d->notification_settings.use_default_disable_mention_notifications) {
    auto scope = td_->dialog_manager_->get_dialog_notification_setting_scope(d->dialog_id);
    return td_->notification_settings_manager_->get_scope_disable_mention_notifications(scope);
  }

  return d->notification_settings.disable_mention_notifications;
}

void MessagesManager::create_dialog(DialogId dialog_id, bool force, Promise<Unit> &&promise) {
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    if (!td_->dialog_manager_->have_dialog_info_force(dialog_id, "create dialog")) {
      return promise.set_error(Status::Error(400, "Chat info not found"));
    }
    if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
      return promise.set_error(Status::Error(400, "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, "create_dialog");
    if (!is_dialog_inited(d)) {
      return send_get_dialog_query(dialog_id, std::move(promise), 0, "create_dialog");
    }
  }

  promise.set_value(Unit());
}

bool MessagesManager::is_dialog_opened(DialogId dialog_id) const {
  const Dialog *d = get_dialog(dialog_id);
  return d != nullptr && d->open_count > 0;
}

Status MessagesManager::open_dialog(DialogId dialog_id) {
  Dialog *d = get_dialog_force(dialog_id, "open_dialog");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }

  open_dialog(d);
  return Status::OK();
}

Status MessagesManager::close_dialog(DialogId dialog_id) {
  Dialog *d = get_dialog_force(dialog_id, "close_dialog");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }

  close_dialog(d);
  return Status::OK();
}

Status MessagesManager::view_messages(DialogId dialog_id, vector<MessageId> message_ids, MessageSource source,
                                      bool force_read) {
  CHECK(!td_->auth_manager_->is_bot());

  Dialog *d = get_dialog_force(dialog_id, "view_messages");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return Status::Error(400, "Can't access the chat");
  }

  if (source == MessageSource::Auto) {
    if (d->open_count > 0) {
      source = MessageSource::DialogHistory;
    } else {
      source = MessageSource::Other;
    }
  }
  bool is_dialog_history = source == MessageSource::DialogHistory || source == MessageSource::MessageThreadHistory ||
                           source == MessageSource::ForumTopicHistory;
  bool need_read = force_read || is_dialog_history;
  bool need_update_view_count = is_dialog_history || source == MessageSource::HistoryPreview ||
                                source == MessageSource::DialogList || source == MessageSource::Other;
  bool need_mark_download_as_viewed = is_dialog_history || source == MessageSource::HistoryPreview ||
                                      source == MessageSource::Search || source == MessageSource::Other;
  bool need_invalidate_authentication_code =
      dialog_id == DialogId(ContactsManager::get_service_notifications_user_id()) &&
      source == MessageSource::Screenshot;
  auto dialog_type = dialog_id.get_type();
  bool need_screenshot_notification = source == MessageSource::Screenshot &&
                                      (dialog_type == DialogType::User || dialog_type == DialogType::SecretChat) &&
                                      can_send_message(dialog_id).is_ok();

  if (source == MessageSource::DialogList && dialog_type == DialogType::User) {
    td_->story_manager_->on_view_dialog_active_stories({dialog_id});
  }

  // keep only valid message identifiers
  size_t pos = 0;
  for (auto message_id : message_ids) {
    if (!message_id.is_valid()) {
      if (message_id.is_valid_scheduled()) {
        // nothing to do for scheduled messages
        continue;
      }
      if (message_id.is_valid_sponsored()) {
        if (is_dialog_history) {
          td_->sponsored_message_manager_->view_sponsored_message(dialog_id, message_id);
          continue;
        } else if (source == MessageSource::HistoryPreview || source == MessageSource::Other) {
          continue;
        } else {
          return Status::Error(400, "Can't view the message from the specified source");
        }
      }
      return Status::Error(400, "Invalid message identifier");
    }
    message_ids[pos++] = message_id;
  }
  message_ids.resize(pos);
  if (message_ids.empty()) {
    // nothing to do
    return Status::OK();
  }

  if (source == MessageSource::DialogEventLog) {
    // nothing more to do
    return Status::OK();
  }

  for (auto message_id : message_ids) {
    auto *m = get_message_force(d, message_id, "view_messages 20");
    if (m != nullptr) {
      auto file_ids = get_message_file_ids(m);
      for (auto file_id : file_ids) {
        td_->file_manager_->check_local_location_async(file_id, true);
      }
    }
  }

  // get information about thread of the messages
  MessageId top_thread_message_id;
  if (source == MessageSource::MessageThreadHistory) {
    if (dialog_type != DialogType::Channel || td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
      return Status::Error(400, "There are no message threads in the chat");
    }

    std::sort(message_ids.begin(), message_ids.end());

    vector<MessageId> top_thread_message_ids;
    vector<int64> media_album_ids;
    for (auto message_id : message_ids) {
      auto *m = get_message_force(d, message_id, "view_messages 1");
      if (m != nullptr) {
        top_thread_message_ids.push_back(m->top_thread_message_id);
        media_album_ids.push_back(m->media_album_id);
      }
    }

    if (!top_thread_message_ids.empty()) {
      // first we expect a root album, then messages from the thread
      // the root album can have all messages from their own threads,
      // or all messages except the first one without thread for automatic forwards
      size_t thread_start = 0;
      if (media_album_ids[0] != 0) {
        thread_start++;
        while (thread_start < media_album_ids.size() && media_album_ids[thread_start] == media_album_ids[0]) {
          thread_start++;
        }
      }
      if (thread_start < media_album_ids.size()) {
        top_thread_message_id = top_thread_message_ids[thread_start];
        for (size_t i = thread_start; i < top_thread_message_ids.size(); i++) {
          if (!top_thread_message_ids[i].is_valid()) {
            return Status::Error(400, "Messages must be from a message thread");
          }
          if (top_thread_message_ids[i] != top_thread_message_id) {
            return Status::Error(400, "All messages must be from the same message thread");
          }
        }
        // do not check album messages to belong to the thread top_thread_message_id
      } else {
        // all messages are from the same album; thread of the first message is always the best guess
        top_thread_message_id = top_thread_message_ids[0];
      }
    }
  }

  MessageId max_thread_message_id;
  if (top_thread_message_id.is_valid()) {
    const auto *top_m = get_message_force(d, top_thread_message_id, "view_messages 2");
    if (top_m != nullptr && !top_m->reply_info.is_comment_) {
      max_thread_message_id = top_m->reply_info.max_message_id_;
    }
  }

  // get forum topic identifier for the messages
  MessageId forum_topic_id;
  if (source == MessageSource::ForumTopicHistory) {
    if (!td_->dialog_manager_->is_forum_channel(dialog_id)) {
      return Status::Error(400, "Chat has no topics");
    }

    for (auto message_id : message_ids) {
      auto *m = get_message_force(d, message_id, "view_messages 3");
      if (m != nullptr) {
        auto message_forum_topic_id = m->is_topic_message ? m->top_thread_message_id : MessageId(ServerMessageId(1));
        if (forum_topic_id.is_valid()) {
          if (message_forum_topic_id != forum_topic_id) {
            return Status::Error(400, "All messages must be from the same forum topic");
          }
        } else {
          forum_topic_id = message_forum_topic_id;
        }
      }
    }
  }

  MessageId max_message_id;  // max server or local viewed message_id
  vector<MessageId> read_content_message_ids;
  vector<MessageId> read_reaction_message_ids;
  vector<MessageId> new_viewed_message_ids;
  vector<MessageId> viewed_reaction_message_ids;
  vector<string> authentication_codes;
  vector<MessageId> screenshotted_secret_message_ids;
  for (auto message_id : message_ids) {
    auto *m = get_message_force(d, message_id, "view_messages 4");
    if (m != nullptr) {
      if (m->message_id.is_server() && m->view_count > 0 && need_update_view_count) {
        pending_message_views_[dialog_id].message_ids_.insert(m->message_id);
      }

      if (!m->message_id.is_yet_unsent() && m->message_id > max_message_id) {
        max_message_id = m->message_id;
      }

      auto message_content_type = m->content->get_type();
      if (message_content_type == MessageContentType::LiveLocation) {
        on_message_live_location_viewed(d, m);
      }

      if (need_read && message_content_type != MessageContentType::VoiceNote &&
          message_content_type != MessageContentType::VideoNote &&
          update_message_contains_unread_mention(d, m, false, "view_messages 5")) {
        CHECK(m->message_id.is_server());
        read_content_message_ids.push_back(m->message_id);
        on_message_changed(d, m, true, "view_messages 6");
      }

      if (need_read && remove_message_unread_reactions(d, m, "view_messages 7")) {
        CHECK(m->message_id.is_server());
        read_reaction_message_ids.push_back(m->message_id);
        on_message_changed(d, m, true, "view_messages 8");
      }

      auto file_source_id = message_full_id_to_file_source_id_.get({dialog_id, m->message_id});
      if (file_source_id.is_valid() && need_mark_download_as_viewed) {
        LOG(INFO) << "Have " << file_source_id << " for " << m->message_id;
        CHECK(file_source_id.is_valid());
        for (auto file_id : get_message_file_ids(m)) {
          auto file_view = td_->file_manager_->get_file_view(file_id);
          CHECK(!file_view.empty());
          send_closure(td_->download_manager_actor_, &DownloadManager::update_file_viewed, file_view.get_main_file_id(),
                       file_source_id);
        }
      }

      auto story_full_id = get_message_content_story_full_id(td_, m->content.get());
      if (story_full_id.is_valid()) {
        td_->story_manager_->view_story_message(story_full_id);
      }

      if (m->message_id.is_server() && d->open_count > 0) {
        auto &info = dialog_viewed_messages_[dialog_id];
        if (info == nullptr) {
          info = make_unique<ViewedMessagesInfo>();
        }
        auto &view_id = info->message_id_to_view_id[message_id];
        if (view_id == 0) {
          new_viewed_message_ids.push_back(message_id);
          if (need_poll_message_reactions(d, m)) {
            viewed_reaction_message_ids.push_back(message_id);
          }
        } else {
          info->recently_viewed_messages.erase(view_id);
        }
        view_id = ++info->current_view_id;
        info->recently_viewed_messages[view_id] = message_id;
      }

      if (need_invalidate_authentication_code) {
        extract_authentication_codes(dialog_id, m, authentication_codes);
      }
      if (need_screenshot_notification && !m->is_outgoing) {
        if ((dialog_type == DialogType::User && m->is_content_secret) || dialog_type == DialogType::SecretChat) {
          screenshotted_secret_message_ids.push_back(m->message_id);
        }
      }
    } else if (!message_id.is_yet_unsent() && message_id > max_message_id) {
      if ((d->notification_info != nullptr && message_id <= d->notification_info->max_push_notification_message_id_) ||
          message_id <= d->last_new_message_id || message_id <= max_thread_message_id) {
        max_message_id = message_id;
      }
    }
  }
  if (pending_message_views_.count(dialog_id) != 0) {
    pending_message_views_timeout_.add_timeout_in(dialog_id.get(), MAX_MESSAGE_VIEW_DELAY);
    if (is_dialog_history) {
      pending_message_views_[dialog_id].increment_view_counter_ = true;
    }
  }
  if (!read_content_message_ids.empty()) {
    read_message_contents_on_server(dialog_id, std::move(read_content_message_ids), 0, Auto());
  }
  if (!read_reaction_message_ids.empty()) {
    for (auto message_id : read_reaction_message_ids) {
      pending_read_reactions_[{dialog_id, message_id}]++;
    }
    auto promise = PromiseCreator::lambda(
        [actor_id = actor_id(this), dialog_id, read_reaction_message_ids](Result<Unit> &&result) mutable {
          send_closure(actor_id, &MessagesManager::on_read_message_reactions, dialog_id,
                       std::move(read_reaction_message_ids), std::move(result));
        });
    read_message_contents_on_server(dialog_id, std::move(read_reaction_message_ids), 0, std::move(promise));
  }
  if (!new_viewed_message_ids.empty()) {
    LOG(INFO) << "Have new viewed " << new_viewed_message_ids;
    auto &info = dialog_viewed_messages_[dialog_id];
    CHECK(info != nullptr);
    CHECK(info->message_id_to_view_id.size() == info->recently_viewed_messages.size());
    constexpr size_t MAX_RECENTLY_VIEWED_MESSAGES = 25;
    while (info->recently_viewed_messages.size() > MAX_RECENTLY_VIEWED_MESSAGES) {
      auto it = info->recently_viewed_messages.begin();
      info->message_id_to_view_id.erase(it->second);
      info->recently_viewed_messages.erase(it);
    }
    if (!viewed_reaction_message_ids.empty()) {
      queue_message_reactions_reload(dialog_id, viewed_reaction_message_ids);
    }
  }
  if (td_->is_online() && dialog_viewed_messages_.count(dialog_id) != 0) {
    update_viewed_messages_timeout_.add_timeout_in(dialog_id.get(), UPDATE_VIEWED_MESSAGES_PERIOD);
  }
  if (!authentication_codes.empty()) {
    td_->account_manager_->invalidate_authentication_codes(std::move(authentication_codes));
  }
  if (!screenshotted_secret_message_ids.empty()) {
    send_screenshot_taken_notification_message(d);
  }

  if (!need_read) {
    return Status::OK();
  }

  if (source == MessageSource::MessageThreadHistory) {
    if (!top_thread_message_id.is_valid() || !max_message_id.is_valid()) {
      return Status::OK();
    }
    MessageId prev_last_read_inbox_message_id;
    max_thread_message_id = MessageId();
    Message *top_m = get_message_force(d, top_thread_message_id, "view_messages 9");
    if (top_m != nullptr && is_active_message_reply_info(dialog_id, top_m->reply_info)) {
      prev_last_read_inbox_message_id = top_m->reply_info.last_read_inbox_message_id_;
      if (top_m->reply_info.update_max_message_ids(MessageId(), max_message_id, MessageId())) {
        on_message_reply_info_changed(dialog_id, top_m);
        on_message_changed(d, top_m, true, "view_messages 10");
      }
      max_thread_message_id = top_m->reply_info.max_message_id_;

      if (is_discussion_message(dialog_id, top_m)) {
        auto linked_message_full_id = top_m->forward_info->get_last_message_full_id();
        auto linked_dialog_id = linked_message_full_id.get_dialog_id();
        auto linked_d = get_dialog(linked_dialog_id);
        CHECK(linked_d != nullptr);
        CHECK(linked_dialog_id.get_type() == DialogType::Channel);
        auto *linked_m = get_message_force(linked_d, linked_message_full_id.get_message_id(), "view_messages 11");
        if (linked_m != nullptr && is_active_message_reply_info(linked_dialog_id, linked_m->reply_info)) {
          if (linked_m->reply_info.last_read_inbox_message_id_ < prev_last_read_inbox_message_id) {
            prev_last_read_inbox_message_id = linked_m->reply_info.last_read_inbox_message_id_;
          }
          if (linked_m->reply_info.update_max_message_ids(MessageId(), max_message_id, MessageId())) {
            on_message_reply_info_changed(linked_dialog_id, linked_m);
            on_message_changed(linked_d, linked_m, true, "view_messages 12");
          }
          if (linked_m->reply_info.max_message_id_ > max_thread_message_id) {
            max_thread_message_id = linked_m->reply_info.max_message_id_;
          }
        }
      }
    }

    if (max_message_id.get_prev_server_message_id().get() >
        prev_last_read_inbox_message_id.get_prev_server_message_id().get()) {
      read_message_thread_history_on_server(d, top_thread_message_id, max_message_id.get_prev_server_message_id(),
                                            max_thread_message_id.get_prev_server_message_id());
    }

    return Status::OK();
  }
  if (source == MessageSource::ForumTopicHistory) {
    if (!forum_topic_id.is_valid() || !max_message_id.is_valid()) {
      return Status::OK();
    }

    td_->forum_topic_manager_->read_forum_topic_messages(dialog_id, forum_topic_id, max_message_id);
    return Status::OK();
  }

  CHECK(source == MessageSource::DialogHistory || force_read);
  read_dialog_inbox(d, max_message_id);
  if (d->is_marked_as_unread) {
    set_dialog_is_marked_as_unread(d, false);
  }
  return Status::OK();
}

void MessagesManager::read_dialog_inbox(Dialog *d, MessageId max_message_id) {
  if (max_message_id == MessageId()) {
    return;
  }
  CHECK(d != nullptr);
  CHECK(max_message_id.is_valid());
  CHECK(max_message_id.is_server() || max_message_id.is_local());
  if (max_message_id <= d->last_read_inbox_message_id) {
    return;
  }

  const MessageId last_read_message_id = max_message_id;
  const MessageId prev_last_read_inbox_message_id = d->last_read_inbox_message_id;
  MessageId read_history_on_server_message_id;
  if (d->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_message_id = last_read_message_id.get_prev_server_message_id();
    }
  } else {
    if (last_read_message_id > prev_last_read_inbox_message_id) {
      read_history_on_server_message_id = last_read_message_id;
    }
  }

  if (read_history_on_server_message_id.is_valid()) {
    // add dummy timeout to not try to repair unread_count in read_history_inbox before server request succeeds
    // the timeout will be overwritten in the read_history_on_server call
    pending_read_history_timeout_.add_timeout_in(d->dialog_id.get(), 0);
  }
  read_history_inbox(d, last_read_message_id, -1, "read_dialog_inbox");
  if (read_history_on_server_message_id.is_valid()) {
    // call read_history_on_server after read_history_inbox to not have delay before request if all messages are read
    read_history_on_server(d, read_history_on_server_message_id);
  }
}

void MessagesManager::finish_get_message_views(DialogId dialog_id, const vector<MessageId> &message_ids) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  for (auto message_id : message_ids) {
    auto *m = get_message(d, message_id);
    if (m != nullptr) {
      m->has_get_message_views_query = false;
      m->need_view_counter_increment = false;
    }
  }
}

void MessagesManager::finish_get_message_extended_media(DialogId dialog_id, const vector<MessageId> &message_ids) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  for (auto message_id : message_ids) {
    auto *m = get_message(d, message_id);
    if (m != nullptr) {
      m->has_get_extended_media_query = false;
    }
  }
}

Status MessagesManager::open_message_content(MessageFullId message_full_id) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "open_message_content");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }

  auto *m = get_message_force(d, message_full_id.get_message_id(), "open_message_content");
  if (m == nullptr) {
    return Status::Error(400, "Message not found");
  }

  if (m->message_id.is_scheduled() || m->message_id.is_yet_unsent() || m->is_outgoing) {
    return Status::OK();
  }

  if (read_message_content(d, m, true, 0, "open_message_content") &&
      (m->message_id.is_server() || dialog_id.get_type() == DialogType::SecretChat)) {
    read_message_contents_on_server(dialog_id, {m->message_id}, 0, Auto());
  }

  if (m->content->get_type() == MessageContentType::LiveLocation) {
    on_message_live_location_viewed(d, m);
  }

  auto file_ids = get_message_file_ids(m);
  for (auto file_id : file_ids) {
    td_->file_manager_->check_local_location_async(file_id, true);
  }

  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_log_event(DialogId dialog_id,
                                                                       const vector<MessageId> &message_ids) {
  ReadMessageContentsOnServerLogEvent log_event{dialog_id, message_ids};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ReadMessageContentsOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::read_message_contents_on_server(DialogId dialog_id, vector<MessageId> message_ids,
                                                      uint64 log_event_id, Promise<Unit> &&promise,
                                                      bool skip_log_event) {
  CHECK(!message_ids.empty());

  LOG(INFO) << "Read contents of " << format::as_array(message_ids) << " in " << dialog_id << " on server";

  if (log_event_id == 0 && G()->use_message_database() && !skip_log_event) {
    log_event_id = save_read_message_contents_on_server_log_event(dialog_id, message_ids);
  }

  auto new_promise = get_erase_log_event_promise(log_event_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<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);
      auto m = get_message_force({dialog_id, message_ids[0]}, "read_message_contents_on_server");
      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));
      } else {
        promise.set_error(Status::Error(400, "Message not found"));
      }
      break;
    }
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

void MessagesManager::click_animated_emoji_message(MessageFullId message_full_id,
                                                   Promise<td_api::object_ptr<td_api::sticker>> &&promise) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "click_animated_emoji_message");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  auto message_id = get_persistent_message_id(d, message_full_id.get_message_id());
  auto *m = get_message_force(d, message_id, "click_animated_emoji_message");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }

  if (m->message_id.is_scheduled() || dialog_id.get_type() != DialogType::User || !m->message_id.is_server()) {
    return promise.set_value(nullptr);
  }

  get_message_content_animated_emoji_click_sticker(m->content.get(), message_full_id, td_, std::move(promise));
}

void MessagesManager::open_dialog(Dialog *d) {
  CHECK(!td_->auth_manager_->is_bot());
  DialogId dialog_id = d->dialog_id;
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return;
  }
  recently_opened_dialogs_.add_dialog(dialog_id);
  if (d->open_count == std::numeric_limits<uint32>::max()) {
    return;
  }
  d->open_count++;
  if (d->open_count != 1) {
    return;
  }
  d->was_opened = true;

  auto min_message_id = MessageId(ServerMessageId(1));
  if (d->last_message_id == MessageId() && d->last_read_outbox_message_id < min_message_id) {
    auto it = d->ordered_messages.get_const_iterator(MessageId::max());
    if (*it != nullptr && (*it)->get_message_id() < min_message_id) {
      read_history_inbox(d, (*it)->get_message_id(), -1, "open_dialog");
    }
  }

  if (d->has_unload_timeout) {
    LOG(INFO) << "Cancel unload timeout for " << dialog_id;
    pending_unload_dialog_timeout_.cancel_timeout(dialog_id.get());
    d->has_unload_timeout = false;
  }

  if (d->notification_info != nullptr && d->notification_info->new_secret_chat_notification_id_.is_valid()) {
    remove_new_secret_chat_notification(d, true);
  }

  get_dialog_pinned_message(dialog_id, Auto());

  if (d->active_group_call_id.is_valid()) {
    td_->group_call_manager_->reload_group_call(d->active_group_call_id, Auto());
  }
  if (d->need_drop_default_send_message_as_dialog_id) {
    CHECK(d->default_send_message_as_dialog_id.is_valid());
    d->need_drop_default_send_message_as_dialog_id = false;
    d->default_send_message_as_dialog_id = DialogId();
    LOG(INFO) << "Set message sender in " << d->dialog_id << " to " << d->default_send_message_as_dialog_id;
    on_dialog_updated(dialog_id, "open_dialog");
    send_update_chat_message_sender(d);
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      td_->story_manager_->on_view_dialog_active_stories({dialog_id});
      break;
    case DialogType::Chat:
      td_->contacts_manager_->repair_chat_participants(dialog_id.get_chat_id());
      reget_dialog_action_bar(dialog_id, "open_dialog", false);
      break;
    case DialogType::Channel: {
      auto channel_id = dialog_id.get_channel_id();
      if (!td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
        auto participant_count = td_->contacts_manager_->get_channel_participant_count(channel_id);
        auto has_hidden_participants = td_->contacts_manager_->get_channel_effective_has_hidden_participants(
            dialog_id.get_channel_id(), "open_dialog");
        if (participant_count < 195 && !has_hidden_participants) {  // include unknown participant_count
          td_->dialog_participant_manager_->get_channel_participants(
              channel_id, td_api::make_object<td_api::supergroupMembersFilterRecent>(), string(), 0, 200, 200, Auto());
        }
      } else {
        td_->story_manager_->on_view_dialog_active_stories({dialog_id});
      }
      get_channel_difference(dialog_id, d->pts, 0, MessageId(), true, "open_dialog");
      reget_dialog_action_bar(dialog_id, "open_dialog", false);

      if (td_->contacts_manager_->get_channel_has_linked_channel(channel_id)) {
        auto linked_channel_id = td_->contacts_manager_->get_channel_linked_channel_id(channel_id, "open_dialog");
        if (!linked_channel_id.is_valid()) {
          // load linked_channel_id
          send_closure_later(G()->contacts_manager(), &ContactsManager::load_channel_full, channel_id, false,
                             Promise<Unit>(), "open_dialog");
        } else {
          td_->dialog_manager_->get_dialog_info_full(DialogId(linked_channel_id), Auto(), "open_dialog");
        }
      }
      break;
    }
    case DialogType::SecretChat: {
      // to repair dialog action bar
      auto user_id = td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id());
      if (user_id.is_valid()) {
        td_->contacts_manager_->reload_user_full(user_id, Promise<Unit>(), "open_dialog");
      }
      break;
    }
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  if (!td_->auth_manager_->is_bot()) {
    td_->dialog_participant_manager_->on_dialog_opened(dialog_id);

    if (d->has_scheduled_database_messages && !d->is_has_scheduled_database_messages_checked) {
      CHECK(G()->use_message_database());

      LOG(INFO) << "Send check has_scheduled_database_messages request";
      d->is_has_scheduled_database_messages_checked = true;
      G()->td_db()->get_message_db_async()->get_scheduled_messages(
          dialog_id, 1,
          PromiseCreator::lambda([actor_id = actor_id(this), dialog_id](vector<MessageDbDialogMessage> messages) {
            if (messages.empty()) {
              send_closure(actor_id, &MessagesManager::set_dialog_has_scheduled_database_messages, dialog_id, false);
            }
          }));
    }
  }
}

void MessagesManager::close_dialog(Dialog *d) {
  if (d->open_count == 0) {
    return;
  }
  d->open_count--;
  if (d->open_count > 0) {
    return;
  }

  auto dialog_id = d->dialog_id;
  if (td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Write)) {
    if (pending_draft_message_timeout_.has_timeout(dialog_id.get())) {
      pending_draft_message_timeout_.set_timeout_in(dialog_id.get(), 0.0);
    }
  } else {
    pending_draft_message_timeout_.cancel_timeout(dialog_id.get());
  }

  if (td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    if (pending_message_views_timeout_.has_timeout(dialog_id.get())) {
      pending_message_views_timeout_.set_timeout_in(dialog_id.get(), 0.0);
    }
    if (pending_read_history_timeout_.has_timeout(dialog_id.get())) {
      pending_read_history_timeout_.set_timeout_in(dialog_id.get(), 0.0);
    }
  } else {
    pending_message_views_timeout_.cancel_timeout(dialog_id.get());
    pending_message_views_.erase(dialog_id);
    pending_read_history_timeout_.cancel_timeout(dialog_id.get());
  }

  if (is_message_unload_enabled()) {
    CHECK(!d->has_unload_timeout);
    pending_unload_dialog_timeout_.set_timeout_in(dialog_id.get(), get_next_unload_dialog_delay(d));
    d->has_unload_timeout = true;

    if (d->need_unload_on_close) {
      unload_dialog(dialog_id, 0);
      d->need_unload_on_close = false;
    }
  }

  dialog_viewed_messages_.erase(dialog_id);
  update_viewed_messages_timeout_.cancel_timeout(dialog_id.get());

  auto live_locations_it = pending_viewed_live_locations_.find(dialog_id);
  if (live_locations_it != pending_viewed_live_locations_.end()) {
    for (auto &it : live_locations_it->second) {
      auto live_location_task_id = it.second;
      auto erased_count = viewed_live_location_tasks_.erase(live_location_task_id);
      CHECK(erased_count > 0);
    }
    pending_viewed_live_locations_.erase(live_locations_it);
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      break;
    case DialogType::Chat:
      break;
    case DialogType::Channel:
      channel_get_difference_timeout_.cancel_timeout(dialog_id.get());
      break;
    case DialogType::SecretChat:
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  if (!td_->auth_manager_->is_bot()) {
    if (postponed_chat_read_inbox_updates_.erase(dialog_id) > 0) {
      send_update_chat_read_inbox(d, false, "close_dialog");
    }

    td_->dialog_participant_manager_->on_dialog_closed(dialog_id);
  }
}

int64 MessagesManager::get_chat_id_object(DialogId dialog_id, const char *source) const {
  const Dialog *d = get_dialog(dialog_id);
  if (d == nullptr) {
    if (dialog_id != DialogId()) {
      LOG(ERROR) << "Can't find " << dialog_id << ", needed from " << source;
    }
  } else if (!d->is_update_new_chat_sent && !d->is_update_new_chat_being_sent) {
    LOG(ERROR) << "Didn't send updateNewChat for " << dialog_id << ", needed from " << source;
  }
  return dialog_id.get();
}

td_api::object_ptr<td_api::ChatActionBar> MessagesManager::get_chat_action_bar_object(const Dialog *d) const {
  CHECK(d != nullptr);
  auto dialog_type = d->dialog_id.get_type();
  if (dialog_type == DialogType::SecretChat) {
    auto user_id = td_->contacts_manager_->get_secret_chat_user_id(d->dialog_id.get_secret_chat_id());
    if (!user_id.is_valid()) {
      return nullptr;
    }
    const Dialog *user_d = get_dialog(DialogId(user_id));
    if (user_d == nullptr || user_d->action_bar == nullptr) {
      return nullptr;
    }
    return user_d->action_bar->get_chat_action_bar_object(DialogType::User, d->folder_id != FolderId::archive());
  }

  if (d->action_bar == nullptr) {
    return nullptr;
  }
  return d->action_bar->get_chat_action_bar_object(dialog_type, false);
}

td_api::object_ptr<td_api::chatBackground> MessagesManager::get_chat_background_object(const Dialog *d) const {
  CHECK(d != nullptr);
  if (d->dialog_id.get_type() == DialogType::SecretChat) {
    auto user_id = td_->contacts_manager_->get_secret_chat_user_id(d->dialog_id.get_secret_chat_id());
    if (!user_id.is_valid()) {
      return nullptr;
    }
    d = get_dialog(DialogId(user_id));
    if (d == nullptr) {
      return nullptr;
    }
  }
  return d->background_info.get_chat_background_object(td_);
}

string MessagesManager::get_dialog_theme_name(const Dialog *d) const {
  CHECK(d != nullptr);
  if (d->dialog_id.get_type() == DialogType::SecretChat) {
    auto user_id = td_->contacts_manager_->get_secret_chat_user_id(d->dialog_id.get_secret_chat_id());
    if (!user_id.is_valid()) {
      return string();
    }
    d = get_dialog(DialogId(user_id));
    if (d == nullptr) {
      return string();
    }
  }
  return d->theme_name;
}

td_api::object_ptr<td_api::chatJoinRequestsInfo> MessagesManager::get_chat_join_requests_info_object(
    const Dialog *d) const {
  if (d->pending_join_request_count == 0) {
    return nullptr;
  }
  return td_api::make_object<td_api::chatJoinRequestsInfo>(
      d->pending_join_request_count, td_->contacts_manager_->get_user_ids_object(d->pending_join_request_user_ids,
                                                                                 "get_chat_join_requests_info_object"));
}

td_api::object_ptr<td_api::videoChat> MessagesManager::get_video_chat_object(const Dialog *d) const {
  auto active_group_call_id = td_->group_call_manager_->get_group_call_id(d->active_group_call_id, d->dialog_id);
  auto default_participant_alias =
      d->default_join_group_call_as_dialog_id.is_valid()
          ? get_message_sender_object_const(td_, d->default_join_group_call_as_dialog_id, "get_video_chat_object")
          : nullptr;
  return make_tl_object<td_api::videoChat>(active_group_call_id.get(),
                                           active_group_call_id.is_valid() ? !d->is_group_call_empty : false,
                                           std::move(default_participant_alias));
}

td_api::object_ptr<td_api::MessageSender> MessagesManager::get_default_message_sender_object(const Dialog *d) const {
  auto as_dialog_id = d->default_send_message_as_dialog_id;
  return as_dialog_id.is_valid()
             ? get_message_sender_object_const(td_, as_dialog_id, "get_default_message_sender_object")
             : nullptr;
}

td_api::object_ptr<td_api::chat> MessagesManager::get_chat_object(const Dialog *d) const {
  CHECK(d != nullptr);

  bool is_premium = td_->option_manager_->get_option_boolean("is_premium");
  auto chat_source = is_dialog_sponsored(d) ? sponsored_dialog_source_.get_chat_source_object() : nullptr;
  auto can_delete = can_delete_dialog(d);
  // TODO hide/show draft message when need_hide_dialog_draft_message changes
  auto draft_message = !need_hide_dialog_draft_message(d) ? get_draft_message_object(td_, d->draft_message) : nullptr;
  auto available_reactions = get_dialog_active_reactions(d).get_chat_available_reactions_object();
  auto is_translatable = d->is_translatable && is_premium;
  auto block_list_id = BlockListId(d->is_blocked, d->is_blocked_for_stories);
  auto chat_lists = transform(get_dialog_list_ids(d),
                              [](DialogListId dialog_list_id) { return dialog_list_id.get_chat_list_object(); });
  return make_tl_object<td_api::chat>(
      d->dialog_id.get(), td_->dialog_manager_->get_chat_type_object(d->dialog_id),
      td_->dialog_manager_->get_dialog_title(d->dialog_id),
      get_chat_photo_info_object(td_->file_manager_.get(), td_->dialog_manager_->get_dialog_photo(d->dialog_id)),
      td_->dialog_manager_->get_dialog_accent_color_id_object(d->dialog_id),
      td_->dialog_manager_->get_dialog_background_custom_emoji_id(d->dialog_id).get(),
      td_->dialog_manager_->get_dialog_profile_accent_color_id_object(d->dialog_id),
      td_->dialog_manager_->get_dialog_profile_background_custom_emoji_id(d->dialog_id).get(),
      td_->dialog_manager_->get_dialog_default_permissions(d->dialog_id).get_chat_permissions_object(),
      get_message_object(d->dialog_id, get_message(d, d->last_message_id), "get_chat_object"),
      get_chat_positions_object(d), std::move(chat_lists), get_default_message_sender_object(d),
      block_list_id.get_block_list_object(), td_->dialog_manager_->get_dialog_has_protected_content(d->dialog_id),
      is_translatable, d->is_marked_as_unread, get_dialog_view_as_topics(d), get_dialog_has_scheduled_messages(d),
      can_delete.for_self_, can_delete.for_all_users_, td_->dialog_manager_->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,
      d->unread_reaction_count, get_chat_notification_settings_object(&d->notification_settings),
      std::move(available_reactions), d->message_ttl.get_message_auto_delete_time_object(),
      td_->dialog_manager_->get_dialog_emoji_status_object(d->dialog_id), get_chat_background_object(d),
      get_dialog_theme_name(d), get_chat_action_bar_object(d), get_video_chat_object(d),
      get_chat_join_requests_info_object(d), d->reply_markup_message_id.get(), std::move(draft_message),
      d->client_data);
}

td_api::object_ptr<td_api::chat> MessagesManager::get_chat_object(DialogId dialog_id) {
  const Dialog *d = get_dialog(dialog_id);
  if (postponed_chat_read_inbox_updates_.erase(dialog_id) > 0) {
    send_update_chat_read_inbox(d, true, "get_chat_object");
  }
  return get_chat_object(d);
}

td_api::object_ptr<td_api::draftMessage> MessagesManager::get_my_dialog_draft_message_object() const {
  const Dialog *d = get_dialog(td_->dialog_manager_->get_my_dialog_id());
  if (d == nullptr) {
    return nullptr;
  }
  return get_draft_message_object(td_, d->draft_message);
}

std::pair<bool, int32> MessagesManager::get_dialog_mute_until(DialogId dialog_id, const Dialog *d) const {
  CHECK(!td_->auth_manager_->is_bot());
  if (d == nullptr || !d->notification_settings.is_synchronized) {
    auto scope = td_->dialog_manager_->get_dialog_notification_setting_scope(dialog_id);
    return {false, td_->notification_settings_manager_->get_scope_mute_until(scope)};
  }

  return {d->notification_settings.is_use_default_fixed, get_dialog_mute_until(d)};
}

int64 MessagesManager::get_dialog_notification_ringtone_id(DialogId dialog_id, const Dialog *d) const {
  CHECK(!td_->auth_manager_->is_bot());
  if (d == nullptr || !d->notification_settings.is_synchronized ||
      is_notification_sound_default(d->notification_settings.sound)) {
    auto scope = td_->dialog_manager_->get_dialog_notification_setting_scope(dialog_id);
    return get_notification_sound_ringtone_id(td_->notification_settings_manager_->get_scope_notification_sound(scope));
  }

  return get_notification_sound_ringtone_id(d->notification_settings.sound);
}

StoryNotificationSettings MessagesManager::get_story_notification_settings(DialogId dialog_id) {
  bool need_dialog_settings = false;
  bool need_top_dialogs = false;
  bool are_muted = false;
  bool hide_sender = false;
  int64 ringtone_id = 0;

  const Dialog *d = get_dialog_force(dialog_id, "get_story_notification_settings");
  if (d == nullptr || !d->notification_settings.is_synchronized) {
    need_dialog_settings = true;
  }
  auto scope = td_->dialog_manager_->get_dialog_notification_setting_scope(dialog_id);
  if (need_dialog_settings || d->notification_settings.use_default_mute_stories) {
    bool use_default;
    std::tie(use_default, are_muted) = td_->notification_settings_manager_->get_scope_mute_stories(scope);
    if (use_default) {
      auto is_top_dialog = td_->top_dialog_manager_->is_top_dialog(TopDialogCategory::Correspondent, 5, dialog_id);
      if (is_top_dialog == -1) {
        need_top_dialogs = true;
      } else {
        are_muted = is_top_dialog != 0;
      }
    }
  } else {
    are_muted = d->notification_settings.mute_stories;
  }
  if (need_dialog_settings || d->notification_settings.use_default_hide_story_sender) {
    hide_sender = td_->notification_settings_manager_->get_scope_hide_story_sender(scope);
  } else {
    hide_sender = d->notification_settings.hide_story_sender;
  }
  if (need_dialog_settings || is_notification_sound_default(d->notification_settings.story_sound)) {
    ringtone_id = get_notification_sound_ringtone_id(
        td_->notification_settings_manager_->get_scope_story_notification_sound(scope));
  } else {
    ringtone_id = get_notification_sound_ringtone_id(d->notification_settings.story_sound);
  }

  return {need_dialog_settings, need_top_dialogs, are_muted, hide_sender, ringtone_id};
}

vector<DialogId> MessagesManager::get_dialog_notification_settings_exceptions(NotificationSettingsScope scope,
                                                                              bool filter_scope, bool compare_sound,
                                                                              bool force, Promise<Unit> &&promise) {
  CHECK(!td_->auth_manager_->is_bot());
  bool have_all_dialogs = true;
  for (const auto &list : dialog_folders_) {
    if (list.second.folder_last_dialog_date_ != MAX_DIALOG_DATE) {
      have_all_dialogs = false;
    }
  }

  if (have_all_dialogs || force) {
    vector<DialogDate> ordered_dialogs;
    auto my_dialog_id = td_->dialog_manager_->get_my_dialog_id();
    for (const auto &list : dialog_folders_) {
      for (const auto &dialog_date : list.second.ordered_dialogs_) {
        auto dialog_id = dialog_date.get_dialog_id();
        if (filter_scope && td_->dialog_manager_->get_dialog_notification_setting_scope(dialog_id) != scope) {
          continue;
        }
        if (dialog_id == my_dialog_id) {
          continue;
        }

        const Dialog *d = get_dialog(dialog_id);
        CHECK(d != nullptr);
        LOG_CHECK(d->folder_id == list.first)
            << list.first << ' ' << dialog_id << ' ' << d->folder_id << ' ' << d->order;
        if (d->order == DEFAULT_ORDER) {
          break;
        }
        if (are_default_dialog_notification_settings(d->notification_settings, compare_sound)) {
          continue;
        }
        if (is_dialog_message_notification_disabled(dialog_id, std::numeric_limits<int32>::max())) {
          continue;
        }
        ordered_dialogs.push_back(DialogDate(get_dialog_base_order(d), dialog_id));
      }
    }
    std::sort(ordered_dialogs.begin(), ordered_dialogs.end());

    vector<DialogId> result;
    for (auto &dialog_date : ordered_dialogs) {
      CHECK(result.empty() || result.back() != dialog_date.get_dialog_id());
      result.push_back(dialog_date.get_dialog_id());
    }
    promise.set_value(Unit());
    return result;
  }

  for (const auto &folder : dialog_folders_) {
    load_folder_dialog_list(folder.first, MAX_GET_DIALOGS, true);
  }

  td_->notification_settings_manager_->get_notify_settings_exceptions(scope, filter_scope, compare_sound,
                                                                      std::move(promise));
  return {};
}

DialogNotificationSettings *MessagesManager::get_dialog_notification_settings(DialogId dialog_id, bool force) {
  Dialog *d = get_dialog_force(dialog_id, "get_dialog_notification_settings");
  if (d == nullptr) {
    return nullptr;
  }
  if (!force && !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return nullptr;
  }
  return &d->notification_settings;
}

Status MessagesManager::set_dialog_notification_settings(
    DialogId dialog_id, tl_object_ptr<td_api::chatNotificationSettings> &&notification_settings) {
  CHECK(!td_->auth_manager_->is_bot());
  auto current_settings = get_dialog_notification_settings(dialog_id, false);
  if (current_settings == nullptr) {
    return Status::Error(400, "Wrong chat identifier specified");
  }
  if (dialog_id == td_->dialog_manager_->get_my_dialog_id()) {
    return Status::Error(400, "Notification settings of the Saved Messages chat can't be changed");
  }

  TRY_RESULT(new_settings, ::td::get_dialog_notification_settings(std::move(notification_settings), current_settings));
  if (update_dialog_notification_settings(dialog_id, current_settings, std::move(new_settings))) {
    update_dialog_notification_settings_on_server(dialog_id, false);
  }
  return Status::OK();
}

void MessagesManager::reset_all_notification_settings() {
  CHECK(!td_->auth_manager_->is_bot());

  dialogs_.foreach([&](const DialogId &dialog_id, unique_ptr<Dialog> &dialog) {
    DialogNotificationSettings new_dialog_settings;
    new_dialog_settings.is_synchronized = true;
    Dialog *d = dialog.get();
    update_dialog_notification_settings(dialog_id, &d->notification_settings, std::move(new_dialog_settings));
  });

  td_->notification_settings_manager_->reset_scope_notification_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_log_event() {
  ResetAllNotificationSettingsOnServerLogEvent log_event;
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ResetAllNotificationSettingsOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::reset_all_notification_settings_on_server(uint64 log_event_id) {
  CHECK(!td_->auth_manager_->is_bot());
  if (log_event_id == 0) {
    log_event_id = save_reset_all_notification_settings_on_server_log_event();
  }

  LOG(INFO) << "Reset all notification settings";
  td_->notification_settings_manager_->reset_notify_settings(get_erase_log_event_promise(log_event_id));
}

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(400, "Parameter limit must be positive"));
    return nullptr;
  }
  if (limit > MAX_GET_HISTORY) {
    limit = MAX_GET_HISTORY;
  }
  if (offset > 0) {
    promise.set_error(Status::Error(400, "Parameter offset must be non-positive"));
    return nullptr;
  }
  if (offset <= -MAX_GET_HISTORY) {
    promise.set_error(Status::Error(400, "Parameter offset must be greater than -100"));
    return nullptr;
  }
  if (offset < -limit) {
    promise.set_error(Status::Error(400, "Parameter offset must be greater than or equal to -limit"));
    return nullptr;
  }

  const Dialog *d = get_dialog_force(dialog_id, "get_dialog_history");
  if (d == nullptr) {
    promise.set_error(Status::Error(400, "Chat not found"));
    return nullptr;
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(400, "Can't access the chat"));
    return nullptr;
  }

  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(400, "Invalid value of parameter from_message_id specified"));
    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, is_empty = " << d->is_empty << ", have_full_history = " << d->have_full_history
            << ", have_full_history_source = " << d->have_full_history_source;

  auto message_ids = d->ordered_messages.get_history(d->last_message_id, from_message_id, offset, limit,
                                                     left_tries == 0 && !only_local);
  if (!message_ids.empty()) {
    // maybe need some messages
    CHECK(offset == 0);
    preload_newer_messages(d, message_ids[0]);
    preload_older_messages(d, message_ids.back());
  } else if (limit > 0 && left_tries != 0 && !(d->is_empty && d->have_full_history && left_tries < 3)) {
    // there can be more messages in the database or on the server, need to load them
    send_closure_later(actor_id(this), &MessagesManager::load_messages, dialog_id, from_message_id, offset, limit,
                       left_tries, only_local, std::move(promise));
    return nullptr;
  }

  LOG(INFO) << "Return " << message_ids << " in result to getChatHistory";
  promise.set_value(Unit());  // can return some messages
  return get_messages_object(-1, dialog_id, message_ids, true,
                             "get_dialog_history");  // 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);
  }
};

class MessagesManager::ReadMessageThreadHistoryOnServerLogEvent {
 public:
  DialogId dialog_id_;
  MessageId top_thread_message_id_;
  MessageId max_message_id_;

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

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

void MessagesManager::read_history_on_server(Dialog *d, MessageId max_message_id) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  CHECK(!max_message_id.is_scheduled());

  auto dialog_id = d->dialog_id;
  bool is_secret = dialog_id.get_type() == DialogType::SecretChat;
  bool need_delay = d->open_count > 0 && !is_secret &&
                    (d->server_unread_count > 0 || (!need_unread_counter(d->order) && d->last_message_id.is_valid() &&
                                                    max_message_id < d->last_message_id));
  LOG(INFO) << "Read history in " << dialog_id << " on server up to " << max_message_id << " with"
            << (need_delay ? "" : "out") << " delay";

  if (is_secret) {
    auto *m = get_message_force(d, max_message_id, "read_history_on_server");
    if (m == nullptr) {
      LOG(ERROR) << "Failed to read history in " << dialog_id << " up to " << max_message_id;
      return;
    }

    ReadHistoryInSecretChatLogEvent log_event;
    log_event.dialog_id_ = dialog_id;
    log_event.max_date_ = m->date;
    add_log_event(read_history_log_event_ids_[dialog_id][0], get_log_event_storer(log_event),
                  LogEvent::HandlerType::ReadHistoryInSecretChat, "read history");

    d->last_read_inbox_message_date = m->date;
  } else if (G()->use_message_database()) {
    ReadHistoryOnServerLogEvent log_event;
    log_event.dialog_id_ = dialog_id;
    log_event.max_message_id_ = max_message_id;
    add_log_event(read_history_log_event_ids_[dialog_id][0], get_log_event_storer(log_event),
                  LogEvent::HandlerType::ReadHistoryOnServer, "read history");
  }

  updated_read_history_message_ids_[dialog_id].insert(MessageId());

  pending_read_history_timeout_.set_timeout_in(dialog_id.get(), need_delay ? MIN_READ_HISTORY_DELAY : 0);
}

void MessagesManager::read_message_thread_history_on_server(Dialog *d, MessageId top_thread_message_id,
                                                            MessageId max_message_id, MessageId last_message_id) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  CHECK(top_thread_message_id.is_valid());
  CHECK(top_thread_message_id.is_server());
  CHECK(max_message_id.is_server());

  auto dialog_id = d->dialog_id;
  LOG(INFO) << "Read history in thread of " << top_thread_message_id << " in " << dialog_id << " on server up to "
            << max_message_id;

  if (G()->use_message_database()) {
    ReadMessageThreadHistoryOnServerLogEvent log_event;
    log_event.dialog_id_ = dialog_id;
    log_event.top_thread_message_id_ = top_thread_message_id;
    log_event.max_message_id_ = max_message_id;
    add_log_event(read_history_log_event_ids_[dialog_id][top_thread_message_id.get()], get_log_event_storer(log_event),
                  LogEvent::HandlerType::ReadMessageThreadHistoryOnServer, "read history");
  }

  updated_read_history_message_ids_[dialog_id].insert(top_thread_message_id);

  bool need_delay = d->open_count > 0 && last_message_id.is_valid() && max_message_id < last_message_id;
  pending_read_history_timeout_.set_timeout_in(dialog_id.get(), need_delay ? MIN_READ_HISTORY_DELAY : 0);
}

void MessagesManager::do_read_history_on_server(DialogId dialog_id) {
  if (G()->close_flag()) {
    return;
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  auto it = updated_read_history_message_ids_.find(dialog_id);
  if (it != updated_read_history_message_ids_.end()) {
    auto top_thread_message_ids = std::move(it->second);
    updated_read_history_message_ids_.erase(it);
    for (auto top_thread_message_id : top_thread_message_ids) {
      if (!top_thread_message_id.is_valid()) {
        read_history_on_server_impl(d, MessageId());
      } else {
        read_message_thread_history_on_server_impl(d, top_thread_message_id, MessageId());
      }
    }
  }
}

void MessagesManager::read_history_on_server_impl(Dialog *d, MessageId max_message_id) {
  CHECK(d != nullptr);
  CHECK(max_message_id == MessageId() || max_message_id.is_valid());
  auto dialog_id = d->dialog_id;

  {
    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 > max_message_id) {
      max_message_id = message_id;
    }
  }

  Promise<Unit> promise;
  auto &log_event_id = read_history_log_event_ids_[dialog_id][0];
  if (log_event_id.log_event_id != 0) {
    log_event_id.generation++;
    promise = PromiseCreator::lambda(
        [actor_id = actor_id(this), dialog_id, generation = log_event_id.generation](Result<Unit> result) {
          if (!G()->close_flag()) {
            send_closure(actor_id, &MessagesManager::on_read_history_finished, dialog_id, MessageId(), generation);
          }
        });
  }
  if (d->need_repair_server_unread_count && need_unread_counter(d->order)) {
    repair_server_unread_count(dialog_id, d->server_unread_count, "read_history_on_server_impl");
  }

  if (!max_message_id.is_valid() || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_value(Unit());
  }

  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 *m = get_message_force(d, max_message_id, "read_history_on_server_impl");
      if (m != nullptr && m->date > date) {
        date = m->date;
      }
      if (date == 0) {
        LOG(ERROR) << "Don't know last read inbox message date in " << dialog_id;
        return promise.set_value(Unit());
      }
      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::read_message_thread_history_on_server_impl(Dialog *d, MessageId top_thread_message_id,
                                                                 MessageId max_message_id) {
  CHECK(d != nullptr);
  CHECK(max_message_id == MessageId() || max_message_id.is_valid());
  auto dialog_id = d->dialog_id;
  CHECK(dialog_id.get_type() == DialogType::Channel);

  const Message *m = get_message_force(d, top_thread_message_id, "read_message_thread_history_on_server_impl");
  if (m != nullptr) {
    auto message_id = m->reply_info.last_read_inbox_message_id_.get_prev_server_message_id();
    if (message_id > max_message_id) {
      max_message_id = message_id;
    }
  }

  Promise<Unit> promise;
  auto &log_event_id = read_history_log_event_ids_[dialog_id][top_thread_message_id.get()];
  if (log_event_id.log_event_id != 0) {
    log_event_id.generation++;
    promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, top_thread_message_id,
                                      generation = log_event_id.generation](Result<Unit> result) {
      if (!G()->close_flag()) {
        send_closure(actor_id, &MessagesManager::on_read_history_finished, dialog_id, top_thread_message_id,
                     generation);
      }
    });
  }

  if (!max_message_id.is_valid() || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_value(Unit());
  }

  LOG(INFO) << "Send read history request in thread of " << top_thread_message_id << " in " << dialog_id << " up to "
            << max_message_id;
  td_->create_handler<ReadDiscussionQuery>(std::move(promise))->send(dialog_id, top_thread_message_id, max_message_id);
}

void MessagesManager::on_read_history_finished(DialogId dialog_id, MessageId top_thread_message_id, uint64 generation) {
  auto dialog_it = read_history_log_event_ids_.find(dialog_id);
  if (dialog_it == read_history_log_event_ids_.end()) {
    return;
  }
  auto it = dialog_it->second.find(top_thread_message_id.get());
  if (it == dialog_it->second.end()) {
    return;
  }
  delete_log_event(it->second, generation, "read history");
  if (it->second.log_event_id == 0) {
    dialog_it->second.erase(it);
    if (dialog_it->second.empty()) {
      read_history_log_event_ids_.erase(dialog_it);
    }
  }
}

template <class T, class It>
vector<MessageId> MessagesManager::get_message_history_slice(const T &begin, It it, const T &end,
                                                             MessageId from_message_id, int32 offset, int32 limit) {
  int32 left_offset = -offset;
  int32 left_limit = limit + offset;
  while (left_offset > 0 && it != end) {
    ++it;
    left_offset--;
    left_limit++;
  }

  vector<MessageId> message_ids;
  while (left_limit > 0 && it != begin) {
    --it;
    left_limit--;
    message_ids.push_back(*it);
  }
  return message_ids;
}

std::pair<DialogId, vector<MessageId>> MessagesManager::get_message_thread_history(
    DialogId dialog_id, MessageId message_id, MessageId from_message_id, int32 offset, int32 limit, int64 &random_id,
    Promise<Unit> &&promise) {
  if (limit <= 0) {
    promise.set_error(Status::Error(400, "Parameter limit must be positive"));
    return {};
  }
  if (limit > MAX_GET_HISTORY) {
    limit = MAX_GET_HISTORY;
  }
  if (offset > 0) {
    promise.set_error(Status::Error(400, "Parameter offset must be non-positive"));
    return {};
  }
  if (offset <= -MAX_GET_HISTORY) {
    promise.set_error(Status::Error(400, "Parameter offset must be greater than -100"));
    return {};
  }
  if (offset < -limit) {
    promise.set_error(Status::Error(400, "Parameter offset must be greater than or equal to -limit"));
    return {};
  }
  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);

  Dialog *d = get_dialog_force(dialog_id, "get_message_thread_history");
  if (d == nullptr) {
    promise.set_error(Status::Error(400, "Chat not found"));
    return {};
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(400, "Can't access the chat"));
    return {};
  }
  if (dialog_id.get_type() != DialogType::Channel) {
    promise.set_error(Status::Error(400, "Can't get message thread history in the chat"));
    return {};
  }

  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(400, "Parameter from_message_id must be identifier of a chat message or 0"));
    return {};
  }

  MessageFullId top_thread_message_full_id;
  if (message_id == MessageId(ServerMessageId(1)) && td_->dialog_manager_->is_forum_channel(dialog_id)) {
    top_thread_message_full_id = MessageFullId{dialog_id, message_id};
  } else {
    message_id = get_persistent_message_id(d, message_id);
    Message *m = get_message_force(d, message_id, "get_message_thread_history 1");
    if (m == nullptr) {
      promise.set_error(Status::Error(400, "Message not found"));
      return {};
    }

    auto r_top_thread_message_full_id = get_top_thread_message_full_id(dialog_id, m, true);
    if (r_top_thread_message_full_id.is_error()) {
      promise.set_error(r_top_thread_message_full_id.move_as_error());
      return {};
    }
    top_thread_message_full_id = r_top_thread_message_full_id.move_as_ok();
    if ((m->reply_info.is_empty() || !m->reply_info.is_comment_) &&
        top_thread_message_full_id.get_message_id() != m->message_id) {
      CHECK(dialog_id == top_thread_message_full_id.get_dialog_id());
      // get information about the thread from the top message
      message_id = top_thread_message_full_id.get_message_id();
      CHECK(message_id.is_valid());
    }

    if (!top_thread_message_full_id.get_message_id().is_valid()) {
      CHECK(m->reply_info.is_comment_);
      get_message_thread(
          dialog_id, message_id,
          PromiseCreator::lambda([promise = std::move(promise)](Result<MessageThreadInfo> &&result) mutable {
            if (result.is_error()) {
              promise.set_error(result.move_as_error());
            } else {
              promise.set_value(Unit());
            }
          }));
      return {};
    }
  }

  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.message_ids);
    found_dialog_messages_.erase(it);

    auto dialog_id_it = found_dialog_messages_dialog_id_.find(random_id);
    if (dialog_id_it != found_dialog_messages_dialog_id_.end()) {
      dialog_id = dialog_id_it->second;
      found_dialog_messages_dialog_id_.erase(dialog_id_it);

      d = get_dialog(dialog_id);
      CHECK(d != nullptr);
    }
    if (dialog_id != top_thread_message_full_id.get_dialog_id()) {
      promise.set_error(Status::Error(500, "Receive messages in an unexpected chat"));
      return {};
    }

    auto yet_unsent_it = yet_unsent_thread_message_ids_.find(top_thread_message_full_id);
    if (yet_unsent_it != yet_unsent_thread_message_ids_.end()) {
      const std::set<MessageId> &message_ids = yet_unsent_it->second;
      auto merge_message_ids = get_message_history_slice(message_ids.begin(), message_ids.lower_bound(from_message_id),
                                                         message_ids.end(), from_message_id, offset, limit);
      vector<MessageId> new_result(result.size() + merge_message_ids.size());
      std::merge(result.begin(), result.end(), merge_message_ids.begin(), merge_message_ids.end(), new_result.begin(),
                 std::greater<>());
      result = std::move(new_result);
    }

    Message *top_m = get_message_force(d, top_thread_message_full_id.get_message_id(), "get_message_thread_history 2");
    if (top_m != nullptr && !top_m->local_thread_message_ids.empty()) {
      vector<MessageId> &message_ids = top_m->local_thread_message_ids;
      vector<MessageId> merge_message_ids;
      while (true) {
        merge_message_ids = get_message_history_slice(
            message_ids.begin(), std::lower_bound(message_ids.begin(), message_ids.end(), from_message_id),
            message_ids.end(), from_message_id, offset, limit);
        bool found_deleted = false;
        for (auto local_message_id : merge_message_ids) {
          Message *local_m = get_message_force(d, local_message_id, "get_message_thread_history 3");
          if (local_m == nullptr) {
            auto local_it = std::lower_bound(message_ids.begin(), message_ids.end(), local_message_id);
            CHECK(local_it != message_ids.end() && *local_it == local_message_id);
            message_ids.erase(local_it);
            found_deleted = true;
          }
        }
        if (!found_deleted) {
          break;
        }
        on_message_changed(d, top_m, false, "get_message_thread_history");
      }
      vector<MessageId> new_result(result.size() + merge_message_ids.size());
      std::merge(result.begin(), result.end(), merge_message_ids.begin(), merge_message_ids.end(), new_result.begin(),
                 std::greater<>());
      result = std::move(new_result);
    }

    if (is_limit_increased) {
      limit--;
    }

    std::reverse(result.begin(), result.end());
    result = get_message_history_slice(result.begin(), std::lower_bound(result.begin(), result.end(), from_message_id),
                                       result.end(), from_message_id, offset, limit);

    LOG(INFO) << "Return " << result.size() << " messages in result to getMessageThreadHistory";

    promise.set_value(Unit());
    return {dialog_id, std::move(result)};
  }

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || found_dialog_messages_.count(random_id) > 0);
  found_dialog_messages_[random_id];  // reserve place for result

  td_->create_handler<SearchMessagesQuery>(std::move(promise))
      ->send(dialog_id, SavedMessagesTopicId(), string(), DialogId(), from_message_id.get_next_server_message_id(),
             offset, limit, MessageSearchFilter::Empty, message_id, ReactionType(), random_id);
  return {};
}

td_api::object_ptr<td_api::messageCalendar> MessagesManager::get_dialog_message_calendar(
    DialogId dialog_id, SavedMessagesTopicId saved_messages_topic_id, MessageId from_message_id,
    MessageSearchFilter filter, int64 &random_id, bool use_db, Promise<Unit> &&promise) {
  if (random_id != 0) {
    // request has already been sent before
    auto it = found_dialog_message_calendars_.find(random_id);
    if (it != found_dialog_message_calendars_.end()) {
      auto result = std::move(it->second);
      found_dialog_message_calendars_.erase(it);
      promise.set_value(Unit());
      return result;
    }
    random_id = 0;
  }
  LOG(INFO) << "Get message calendar in " << dialog_id << " with " << saved_messages_topic_id << " filtered by "
            << filter << " from " << from_message_id;

  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(400, "Parameter from_message_id must be identifier of a chat message or 0"));
    return {};
  }
  from_message_id = from_message_id.get_next_server_message_id();

  const Dialog *d = get_dialog_force(dialog_id, "get_dialog_message_calendar");
  if (d == nullptr) {
    promise.set_error(Status::Error(400, "Chat not found"));
    return {};
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(400, "Can't access the chat"));
    return {};
  }
  {
    auto status = saved_messages_topic_id.is_valid_in(td_, dialog_id);
    if (status.is_error()) {
      promise.set_error(std::move(status));
      return {};
    }
  }

  CHECK(filter != MessageSearchFilter::Call && filter != MessageSearchFilter::MissedCall);
  if (filter == MessageSearchFilter::Empty || filter == MessageSearchFilter::Mention ||
      filter == MessageSearchFilter::UnreadMention || filter == MessageSearchFilter::UnreadReaction) {
    if (filter != MessageSearchFilter::Empty && saved_messages_topic_id.is_valid()) {
      return td_api::make_object<td_api::messageCalendar>();
    }
    promise.set_error(Status::Error(400, "The filter is not supported"));
    return {};
  }

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || found_dialog_message_calendars_.count(random_id) > 0);
  found_dialog_message_calendars_[random_id];  // reserve place for result

  // Trying to use database
  if (use_db && G()->use_message_database() && !saved_messages_topic_id.is_valid()) {
    MessageId first_db_message_id = get_first_database_message_id_by_index(d, filter);
    int32 message_count = d->message_count_by_index[message_search_filter_index(filter)];
    auto fixed_from_message_id = from_message_id;
    if (fixed_from_message_id == MessageId()) {
      fixed_from_message_id = MessageId::max();
    }
    LOG(INFO) << "Get message calendar in " << dialog_id << " from " << fixed_from_message_id << ", have up to "
              << first_db_message_id << ", message_count = " << message_count;
    if (first_db_message_id < fixed_from_message_id && message_count != -1) {
      LOG(INFO) << "Get message calendar from database in " << dialog_id << " from " << fixed_from_message_id;
      auto new_promise =
          PromiseCreator::lambda([random_id, dialog_id, fixed_from_message_id, first_db_message_id, filter,
                                  promise = std::move(promise)](Result<MessageDbCalendar> r_calendar) mutable {
            send_closure(G()->messages_manager(), &MessagesManager::on_get_message_calendar_from_database, random_id,
                         dialog_id, fixed_from_message_id, first_db_message_id, filter, std::move(r_calendar),
                         std::move(promise));
          });
      MessageDbDialogCalendarQuery db_query;
      db_query.dialog_id = dialog_id;
      db_query.filter = filter;
      db_query.from_message_id = fixed_from_message_id;
      db_query.tz_offset = static_cast<int32>(td_->option_manager_->get_option_integer("utc_time_offset"));
      G()->td_db()->get_message_db_async()->get_dialog_message_calendar(db_query, std::move(new_promise));
      return {};
    }
  }
  if (filter == MessageSearchFilter::FailedToSend) {
    found_dialog_message_calendars_.erase(random_id);
    promise.set_value(Unit());
    return td_api::make_object<td_api::messageCalendar>();
  }

  switch (dialog_id.get_type()) {
    case DialogType::None:
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      td_->create_handler<GetSearchResultCalendarQuery>(std::move(promise))
          ->send(dialog_id, saved_messages_topic_id, from_message_id, filter, random_id);
      break;
    case DialogType::SecretChat:
      promise.set_value(Unit());
      break;
    default:
      UNREACHABLE();
      promise.set_error(Status::Error(500, "Message search is not supported"));
  }
  return {};
}

void MessagesManager::on_get_message_calendar_from_database(int64 random_id, DialogId dialog_id,
                                                            MessageId from_message_id, MessageId first_db_message_id,
                                                            MessageSearchFilter filter,
                                                            Result<MessageDbCalendar> r_calendar,
                                                            Promise<Unit> promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  if (r_calendar.is_error()) {
    LOG(ERROR) << "Failed to get message calendar from the database: " << r_calendar.error();
    if (first_db_message_id != MessageId::min() && dialog_id.get_type() != DialogType::SecretChat &&
        filter != MessageSearchFilter::FailedToSend) {
      found_dialog_message_calendars_.erase(random_id);
    }
    return promise.set_value(Unit());
  }
  CHECK(!from_message_id.is_scheduled());
  CHECK(!first_db_message_id.is_scheduled());

  auto calendar = r_calendar.move_as_ok();

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  auto it = found_dialog_message_calendars_.find(random_id);
  CHECK(it != found_dialog_message_calendars_.end());
  CHECK(it->second == nullptr);

  vector<std::pair<MessageId, int32>> periods;
  periods.reserve(calendar.messages.size());
  for (size_t i = 0; i < calendar.messages.size(); i++) {
    auto m = on_get_message_from_database(d, calendar.messages[i], false, "on_get_message_calendar_from_database");
    if (m != nullptr && first_db_message_id <= m->message_id) {
      CHECK(!m->message_id.is_scheduled());
      periods.emplace_back(m->message_id, calendar.total_counts[i]);
    }
  }

  if (periods.empty() && first_db_message_id != MessageId::min() && dialog_id.get_type() != DialogType::SecretChat) {
    LOG(INFO) << "No messages found in database";
    found_dialog_message_calendars_.erase(it);
  } else {
    auto total_count = d->message_count_by_index[message_search_filter_index(filter)];
    vector<td_api::object_ptr<td_api::messageCalendarDay>> days;
    for (auto &period : periods) {
      const auto *m = get_message(d, period.first);
      CHECK(m != nullptr);
      days.push_back(td_api::make_object<td_api::messageCalendarDay>(
          period.second, get_message_object(dialog_id, m, "on_get_message_calendar_from_database")));
    }
    it->second = td_api::make_object<td_api::messageCalendar>(total_count, std::move(days));
  }
  promise.set_value(Unit());
}

MessagesManager::FoundDialogMessages MessagesManager::search_dialog_messages(
    DialogId dialog_id, const string &query, const td_api::object_ptr<td_api::MessageSender> &sender,
    MessageId from_message_id, int32 offset, int32 limit, MessageSearchFilter filter, MessageId top_thread_message_id,
    SavedMessagesTopicId saved_messages_topic_id, const ReactionType &tag, 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()) {
      CHECK(found_dialog_messages_dialog_id_.count(random_id) == 0);
      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 "
            << oneline(to_string(sender)) << " in thread of " << top_thread_message_id << " and in "
            << saved_messages_topic_id << " filtered by " << filter << " from " << from_message_id << " with offset "
            << offset << " and limit " << limit;

  FoundDialogMessages result;
  if (limit <= 0) {
    promise.set_error(Status::Error(400, "Parameter limit must be positive"));
    return result;
  }
  if (limit > MAX_SEARCH_MESSAGES) {
    limit = MAX_SEARCH_MESSAGES;
  }
  if (limit <= -offset) {
    promise.set_error(Status::Error(400, "Parameter limit must be greater than -offset"));
    return result;
  }
  if (offset > 0) {
    promise.set_error(Status::Error(400, "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(400, "Parameter from_message_id must be identifier of a 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, "search_dialog_messages");
  if (d == nullptr) {
    promise.set_error(Status::Error(400, "Chat not found"));
    return result;
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(400, "Can't access the chat"));
    return result;
  }

  auto r_sender_dialog_id = get_message_sender_dialog_id(td_, sender, true, true);
  if (r_sender_dialog_id.is_error()) {
    promise.set_error(r_sender_dialog_id.move_as_error());
    return result;
  }
  auto sender_dialog_id = r_sender_dialog_id.move_as_ok();
  if (sender_dialog_id != DialogId() && !td_->dialog_manager_->have_input_peer(sender_dialog_id, AccessRights::Know)) {
    promise.set_error(Status::Error(400, "Invalid message sender specified"));
    return result;
  }
  if (sender_dialog_id == dialog_id && td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
    sender_dialog_id = DialogId();
  }

  if (filter == MessageSearchFilter::FailedToSend && sender_dialog_id.is_valid()) {
    if (sender_dialog_id != td_->dialog_manager_->get_my_dialog_id()) {
      promise.set_value(Unit());
      return result;
    }
    sender_dialog_id = DialogId();
  }

  if (top_thread_message_id != MessageId()) {
    if (!top_thread_message_id.is_valid() || !top_thread_message_id.is_server()) {
      promise.set_error(Status::Error(400, "Invalid message thread specified"));
      return result;
    }
    if (dialog_id.get_type() != DialogType::Channel || td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
      promise.set_error(Status::Error(400, "Can't filter by message thread in the chat"));
      return result;
    }
  }
  {
    auto status = saved_messages_topic_id.is_valid_in(td_, dialog_id);
    if (status.is_error()) {
      promise.set_error(std::move(status));
      return result;
    }
  }

  if (sender_dialog_id.get_type() == DialogType::SecretChat) {
    promise.set_value(Unit());
    return result;
  }

  if (filter == MessageSearchFilter::UnreadMention || filter == MessageSearchFilter::UnreadReaction) {
    if (!query.empty()) {
      promise.set_error(Status::Error(400, "Non-empty query is unsupported with the specified filter"));
      return result;
    }
    if (sender_dialog_id.is_valid()) {
      promise.set_error(Status::Error(400, "Filtering by sender is unsupported with the specified filter"));
      return result;
    }
    if (saved_messages_topic_id.is_valid()) {
      promise.set_value(Unit());
      return result;
    }
  }

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || found_dialog_messages_.count(random_id) > 0);
  found_dialog_messages_[random_id];  // reserve place for result

  // Trying to use database
  if (use_db && query.empty() && G()->use_message_database() && filter != MessageSearchFilter::Empty &&
      !sender_dialog_id.is_valid() && top_thread_message_id == MessageId() &&
      saved_messages_topic_id == SavedMessagesTopicId() && tag.is_empty()) {
    MessageId first_db_message_id = get_first_database_message_id_by_index(d, filter);
    int32 message_count = d->message_count_by_index[message_search_filter_index(filter)];
    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 < fixed_from_message_id || (first_db_message_id == fixed_from_message_id && 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, offset, limit,
           promise = std::move(promise)](Result<vector<MessageDbDialogMessage>> r_messages) mutable {
            send_closure(G()->messages_manager(), &MessagesManager::on_search_dialog_message_db_result, random_id,
                         dialog_id, fixed_from_message_id, first_db_message_id, filter, offset, limit,
                         std::move(r_messages), std::move(promise));
          });
      MessageDbMessagesQuery db_query;
      db_query.dialog_id = dialog_id;
      db_query.filter = filter;
      db_query.from_message_id = fixed_from_message_id;
      db_query.offset = offset;
      db_query.limit = limit;
      G()->td_db()->get_message_db_async()->get_messages(db_query, std::move(new_promise));
      return result;
    }
  }
  if (filter == MessageSearchFilter::FailedToSend) {
    found_dialog_messages_.erase(random_id);
    promise.set_value(Unit());
    return result;
  }

  LOG(DEBUG) << "Search messages on server in " << dialog_id << " with query \"" << query << "\" from "
             << sender_dialog_id << " in thread of " << top_thread_message_id << " from " << from_message_id
             << " and with limit " << limit;

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      td_->create_handler<SearchMessagesQuery>(std::move(promise))
          ->send(dialog_id, saved_messages_topic_id, query, sender_dialog_id, from_message_id, offset, limit, filter,
                 top_thread_message_id, tag, random_id);
      break;
    case DialogType::SecretChat:
      if (filter == MessageSearchFilter::UnreadMention || filter == MessageSearchFilter::Pinned ||
          filter == MessageSearchFilter::UnreadReaction) {
        promise.set_value(Unit());
      } else {
        promise.set_error(Status::Error(500, "Message search is not supported in secret chats"));
      }
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
      promise.set_error(Status::Error(500, "Message search is not supported"));
  }
  return result;
}

MessagesManager::FoundMessages MessagesManager::search_call_messages(const string &offset, 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 " << offset << " with limit " << limit;

  FoundMessages result;
  if (limit <= 0) {
    promise.set_error(Status::Error(400, "Parameter limit must be positive"));
    return result;
  }
  if (limit > MAX_SEARCH_MESSAGES) {
    limit = MAX_SEARCH_MESSAGES;
  }

  MessageId offset_message_id;
  if (!offset.empty()) {
    auto r_offset_server_message_id = to_integer_safe<int32>(offset);
    if (r_offset_server_message_id.is_error()) {
      promise.set_error(Status::Error(400, "Invalid offset specified"));
      return result;
    }

    offset_message_id = MessageId(ServerMessageId(r_offset_server_message_id.ok()));
  }

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || found_call_messages_.count(random_id) > 0);
  found_call_messages_[random_id];  // reserve place for result

  auto filter = only_missed ? MessageSearchFilter::MissedCall : MessageSearchFilter::Call;

  if (use_db && G()->use_message_database()) {
    // try to use database
    MessageId first_db_message_id =
        calls_db_state_.first_calls_database_message_id_by_index[call_message_search_filter_index(filter)];
    int32 message_count = calls_db_state_.message_count_by_index[call_message_search_filter_index(filter)];
    auto fixed_from_message_id = offset_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 < fixed_from_message_id && message_count != -1) {
      LOG(INFO) << "Search messages in database from " << fixed_from_message_id << " and with limit " << limit;

      MessageDbCallsQuery db_query;
      db_query.filter = filter;
      db_query.from_unique_message_id = fixed_from_message_id.get_server_message_id().get();
      db_query.limit = limit;
      G()->td_db()->get_message_db_async()->get_calls(
          db_query, PromiseCreator::lambda([random_id, first_db_message_id, filter, promise = std::move(promise)](
                                               Result<MessageDbCallsResult> calls_result) mutable {
            send_closure(G()->messages_manager(), &MessagesManager::on_message_db_calls_result, std::move(calls_result),
                         random_id, first_db_message_id, filter, std::move(promise));
          }));
      return result;
    }
  }

  td_->create_handler<SearchMessagesQuery>(std::move(promise))
      ->send(DialogId(), SavedMessagesTopicId(), string(), DialogId(), offset_message_id, 0, limit, filter, MessageId(),
             ReactionType(), random_id);
  return result;
}

void MessagesManager::search_outgoing_document_messages(const string &query, int32 limit,
                                                        Promise<td_api::object_ptr<td_api::foundMessages>> &&promise) {
  if (limit <= 0) {
    return promise.set_error(Status::Error(400, "Parameter limit must be positive"));
  }
  if (limit > MAX_SEARCH_MESSAGES) {
    limit = MAX_SEARCH_MESSAGES;
  }

  td_->create_handler<SearchSentMediaQuery>(std::move(promise))->send(query, limit);
}

void MessagesManager::search_dialog_recent_location_messages(DialogId dialog_id, int32 limit,
                                                             Promise<td_api::object_ptr<td_api::messages>> &&promise) {
  LOG(INFO) << "Search recent location messages in " << dialog_id << " with limit " << limit;

  if (limit <= 0) {
    return promise.set_error(Status::Error(400, "Parameter limit must be positive"));
  }
  if (limit > MAX_SEARCH_MESSAGES) {
    limit = MAX_SEARCH_MESSAGES;
  }

  const Dialog *d = get_dialog_force(dialog_id, "search_dialog_recent_location_messages");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      return td_->create_handler<GetRecentLocationsQuery>(std::move(promise))->send(dialog_id, limit);
    case DialogType::SecretChat:
      return promise.set_value(get_messages_object(0, vector<td_api::object_ptr<td_api::message>>(), false));
    default:
      UNREACHABLE();
      promise.set_error(Status::Error(500, "Message search is not supported"));
  }
}

vector<MessageFullId> MessagesManager::get_active_live_location_messages(Promise<Unit> &&promise) {
  if (!G()->use_message_database()) {
    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_message_full_ids_from_database,
                         std::move(value));
          }));
    }
    return {};
  }

  promise.set_value(Unit());
  vector<MessageFullId> result;
  for (auto &message_full_id : active_live_location_message_full_ids_) {
    auto m = get_message(message_full_id);
    CHECK(m != nullptr);
    CHECK(m->content->get_type() == MessageContentType::LiveLocation);
    CHECK(!m->message_id.is_scheduled());

    if (m->is_failed_to_send) {
      continue;
    }

    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(message_full_id);
  }

  return result;
}

void MessagesManager::on_load_active_live_location_message_full_ids_from_database(string value) {
  if (G()->close_flag()) {
    return;
  }
  if (value.empty()) {
    LOG(INFO) << "Active live location messages aren't found in the database";
    on_load_active_live_location_messages_finished();

    if (!active_live_location_message_full_ids_.empty()) {
      save_active_live_locations();
    }
    return;
  }

  LOG(INFO) << "Successfully loaded active live location messages list of size " << value.size() << " from database";

  auto new_message_full_ids = std::move(active_live_location_message_full_ids_);
  vector<MessageFullId> old_message_full_ids;
  log_event_parse(old_message_full_ids, value).ensure();

  // TODO asynchronously load messages from database
  active_live_location_message_full_ids_.clear();
  for (const auto &message_full_id : old_message_full_ids) {
    Message *m = get_message_force(message_full_id, "on_load_active_live_location_message_full_ids_from_database");
    if (m != nullptr) {
      try_add_active_live_location(message_full_id.get_dialog_id(), m);
    }
  }

  for (const auto &message_full_id : new_message_full_ids) {
    add_active_live_location(message_full_id);
  }

  on_load_active_live_location_messages_finished();

  if (!new_message_full_ids.empty() || old_message_full_ids.size() != active_live_location_message_full_ids_.size()) {
    save_active_live_locations();
  }
}

void MessagesManager::on_load_active_live_location_messages_finished() {
  are_active_live_location_messages_loaded_ = true;
  set_promises(load_active_live_location_messages_queries_);
}

void MessagesManager::try_add_active_live_location(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);

  if (td_->auth_manager_->is_bot()) {
    return;
  }
  if (m->content->get_type() != MessageContentType::LiveLocation || m->message_id.is_scheduled() ||
      m->message_id.is_local() || m->via_bot_user_id.is_valid() || m->forward_info != nullptr) {
    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(MessageFullId message_full_id) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  CHECK(message_full_id.get_message_id().is_valid());
  if (!active_live_location_message_full_ids_.insert(message_full_id).second) {
    return;
  }

  // TODO add timer for live location expiration

  if (!G()->use_message_database()) {
    return;
  }

  if (are_active_live_location_messages_loaded_) {
    save_active_live_locations();
  } else if (load_active_live_location_messages_queries_.empty()) {
    // load active live locations and save after that
    get_active_live_location_messages(Auto());
  }
}

bool MessagesManager::delete_active_live_location(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  return active_live_location_message_full_ids_.erase(MessageFullId{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_message_full_ids_.size() << " to database";
  if (G()->use_message_database()) {
    G()->td_db()->get_sqlite_pmc()->set("di_active_live_location_messages",
                                        log_event_store(active_live_location_message_full_ids_).as_slice().str(),
                                        Auto());
  }
}

void MessagesManager::on_message_live_location_viewed(Dialog *d, const Message *m) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  CHECK(m->content->get_type() == MessageContentType::LiveLocation);
  CHECK(!m->message_id.is_scheduled());

  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  switch (d->dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      // ok
      break;
    case DialogType::SecretChat:
      return;
    default:
      UNREACHABLE();
      return;
  }
  if (d->open_count == 0) {
    return;
  }

  if (m->is_outgoing || !m->message_id.is_server() || m->via_bot_user_id.is_valid() || !m->sender_user_id.is_valid() ||
      td_->contacts_manager_->is_user_bot(m->sender_user_id) || m->forward_info != nullptr) {
    return;
  }

  auto live_period = get_message_content_live_location_period(m->content.get());
  if (live_period <= G()->unix_time() - m->date + 1) {
    // live location is expired
    return;
  }

  auto &live_location_task_id = pending_viewed_live_locations_[d->dialog_id][m->message_id];
  if (live_location_task_id != 0) {
    return;
  }

  live_location_task_id = ++viewed_live_location_task_id_;
  auto &message_full_id = viewed_live_location_tasks_[live_location_task_id];
  message_full_id = MessageFullId(d->dialog_id, m->message_id);
  view_message_live_location_on_server_impl(live_location_task_id, message_full_id);
}

void MessagesManager::view_message_live_location_on_server(int64 task_id) {
  if (G()->close_flag()) {
    return;
  }

  auto it = viewed_live_location_tasks_.find(task_id);
  if (it == viewed_live_location_tasks_.end()) {
    return;
  }

  auto message_full_id = it->second;
  Dialog *d = get_dialog(message_full_id.get_dialog_id());
  const Message *m = get_message_force(d, message_full_id.get_message_id(), "view_message_live_location_on_server");
  if (m == nullptr || get_message_content_live_location_period(m->content.get()) <= G()->unix_time() - m->date + 1) {
    // the message was deleted or live location is expired
    viewed_live_location_tasks_.erase(it);
    auto live_locations_it = pending_viewed_live_locations_.find(d->dialog_id);
    CHECK(live_locations_it != pending_viewed_live_locations_.end());
    auto erased_count = live_locations_it->second.erase(message_full_id.get_message_id());
    CHECK(erased_count > 0);
    if (live_locations_it->second.empty()) {
      pending_viewed_live_locations_.erase(live_locations_it);
    }
    return;
  }

  view_message_live_location_on_server_impl(task_id, message_full_id);
}

void MessagesManager::view_message_live_location_on_server_impl(int64 task_id, MessageFullId message_full_id) {
  auto promise = PromiseCreator::lambda([actor_id = actor_id(this), task_id](Unit result) {
    send_closure(actor_id, &MessagesManager::on_message_live_location_viewed_on_server, task_id);
  });
  read_message_contents_on_server(message_full_id.get_dialog_id(), {message_full_id.get_message_id()}, 0,
                                  std::move(promise), true);
}

void MessagesManager::on_message_live_location_viewed_on_server(int64 task_id) {
  if (G()->close_flag()) {
    return;
  }

  auto it = viewed_live_location_tasks_.find(task_id);
  if (it == viewed_live_location_tasks_.end()) {
    return;
  }

  pending_message_live_location_view_timeout_.add_timeout_in(task_id, LIVE_LOCATION_VIEW_PERIOD);
}

void MessagesManager::try_add_bot_command_message_id(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  if (td_->auth_manager_->is_bot() || !td_->dialog_manager_->is_group_dialog(dialog_id) ||
      m->message_id.is_scheduled() || !has_bot_commands(get_message_content_text(m->content.get()))) {
    return;
  }

  dialog_bot_command_message_ids_[dialog_id].message_ids.insert(m->message_id);
}

void MessagesManager::delete_bot_command_message_id(DialogId dialog_id, MessageId message_id) {
  if (message_id.is_scheduled()) {
    return;
  }
  auto it = dialog_bot_command_message_ids_.find(dialog_id);
  if (it == dialog_bot_command_message_ids_.end()) {
    return;
  }
  if (it->second.message_ids.erase(message_id) && it->second.message_ids.empty()) {
    dialog_bot_command_message_ids_.erase(it);
  }
}

FileSourceId MessagesManager::get_message_file_source_id(MessageFullId message_full_id, bool force) {
  if (!force) {
    if (td_->auth_manager_->is_bot()) {
      return FileSourceId();
    }

    auto dialog_id = message_full_id.get_dialog_id();
    auto message_id = message_full_id.get_message_id();
    if (!dialog_id.is_valid() || !(message_id.is_valid() || message_id.is_valid_scheduled()) ||
        dialog_id.get_type() == DialogType::SecretChat || !message_id.is_any_server()) {
      return FileSourceId();
    }
  }

  auto &file_source_id = message_full_id_to_file_source_id_[message_full_id];
  if (!file_source_id.is_valid()) {
    file_source_id = td_->file_reference_manager_->create_message_file_source(message_full_id);
  }
  return file_source_id;
}

void MessagesManager::add_message_file_sources(DialogId dialog_id, const Message *m) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  if (dialog_id.get_type() != DialogType::SecretChat && m->is_content_secret) {
    // return;
  }

  auto file_ids = get_message_file_ids(m);
  if (file_ids.empty()) {
    return;
  }

  // do not create file_source_id for messages without file_ids
  auto file_source_id = get_message_file_source_id(MessageFullId(dialog_id, m->message_id));
  if (file_source_id.is_valid()) {
    for (auto file_id : file_ids) {
      td_->file_manager_->add_file_source(file_id, file_source_id);
    }
  }
}

void MessagesManager::remove_message_file_sources(DialogId dialog_id, const Message *m) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto file_ids = get_message_file_ids(m);
  if (file_ids.empty()) {
    return;
  }

  // do not create file_source_id for messages without file_ids
  auto file_source_id = get_message_file_source_id(MessageFullId(dialog_id, m->message_id));
  if (file_source_id.is_valid()) {
    for (auto file_id : file_ids) {
      auto file_view = td_->file_manager_->get_file_view(file_id);
      send_closure(td_->download_manager_actor_, &DownloadManager::remove_file, file_view.get_main_file_id(),
                   file_source_id, false, Promise<Unit>());
      td_->file_manager_->remove_file_source(file_id, file_source_id);
    }
  }
}

void MessagesManager::change_message_files(DialogId dialog_id, const Message *m, const vector<FileId> &old_file_ids) {
  if (dialog_id.get_type() != DialogType::SecretChat && m->is_content_secret) {
    // return;
  }

  auto new_file_ids = get_message_file_ids(m);
  if (new_file_ids == old_file_ids) {
    return;
  }

  MessageFullId message_full_id{dialog_id, m->message_id};
  bool need_delete_files = need_delete_message_files(dialog_id, m);
  auto file_source_id = get_message_file_source_id(message_full_id);
  for (auto file_id : old_file_ids) {
    if (!td::contains(new_file_ids, file_id)) {
      if (need_delete_files && need_delete_file(message_full_id, file_id)) {
        send_closure(G()->file_manager(), &FileManager::delete_file, file_id, Promise<Unit>(), "change_message_files");
      }
      if (file_source_id.is_valid()) {
        auto file_view = td_->file_manager_->get_file_view(file_id);
        send_closure(td_->download_manager_actor_, &DownloadManager::remove_file, file_view.get_main_file_id(),
                     file_source_id, false, Promise<Unit>());
      }
    }
  }

  if (file_source_id.is_valid()) {
    td_->file_manager_->change_files_source(file_source_id, old_file_ids, new_file_ids);
  }
}

MessageId MessagesManager::get_first_database_message_id_by_index(const Dialog *d, MessageSearchFilter filter) {
  CHECK(d != nullptr);
  auto message_id = filter == MessageSearchFilter::Empty
                        ? d->first_database_message_id
                        : d->first_database_message_id_by_index[message_search_filter_index(filter)];
  CHECK(!message_id.is_scheduled());
  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_message_db_result(int64 random_id, DialogId dialog_id, MessageId from_message_id,
                                                         MessageId first_db_message_id, MessageSearchFilter filter,
                                                         int32 offset, int32 limit,
                                                         Result<vector<MessageDbDialogMessage>> r_messages,
                                                         Promise<Unit> promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  if (r_messages.is_error()) {
    LOG(ERROR) << "Failed to get messages from the database: " << r_messages.error();
    if (first_db_message_id != MessageId::min() && dialog_id.get_type() != DialogType::SecretChat &&
        filter != MessageSearchFilter::FailedToSend) {
      found_dialog_messages_.erase(random_id);
    }
    return promise.set_value(Unit());
  }
  CHECK(!from_message_id.is_scheduled());
  CHECK(!first_db_message_id.is_scheduled());

  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.message_ids;

  MessageId next_from_message_id;
  res.reserve(messages.size());
  for (auto &message : messages) {
    auto m = on_get_message_from_database(d, message, false, "on_search_dialog_message_db_result");
    if (m != nullptr && first_db_message_id <= m->message_id) {
      if (!next_from_message_id.is_valid() || m->message_id < next_from_message_id) {
        next_from_message_id = m->message_id;
      }
      if (filter == MessageSearchFilter::UnreadMention && !m->contains_unread_mention) {
        // skip already read by d->last_read_all_mentions_message_id mentions
      } else {
        CHECK(!m->message_id.is_scheduled());
        res.push_back(m->message_id);
      }
    }
  }

  auto &message_count = d->message_count_by_index[message_search_filter_index(filter)];
  auto result_size = narrow_cast<int32>(res.size());
  bool from_the_end =
      from_message_id == MessageId::max() || (offset < 0 && (result_size == 0 || res[0] < from_message_id));
  if ((message_count != -1 && message_count < result_size) ||
      (message_count > result_size && from_the_end && first_db_message_id == MessageId::min() &&
       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 == MessageSearchFilter::UnreadMention) {
      d->unread_mention_count = message_count;
      update_dialog_mention_notification_count(d);
      send_update_chat_unread_mention_count(d);
    }
    if (filter == MessageSearchFilter::UnreadReaction) {
      d->unread_reaction_count = message_count;
      // update_dialog_mention_notification_count(d);
      send_update_chat_unread_reaction_count(d, "on_search_dialog_message_db_result");
    }
    on_dialog_updated(dialog_id, "on_search_dialog_message_db_result");
  }
  it->second.total_count = message_count;
  it->second.next_from_message_id = next_from_message_id;
  if (res.empty() && first_db_message_id != MessageId::min() && dialog_id.get_type() != DialogType::SecretChat) {
    LOG(INFO) << "No messages found in database";
    found_dialog_messages_.erase(it);
  } else {
    LOG(INFO) << "Found " << res.size() << " messages out of " << message_count << " in database";
    if (from_the_end && filter == MessageSearchFilter::Pinned) {
      set_dialog_last_pinned_message_id(d, res.empty() ? MessageId() : res[0]);
    }
  }
  promise.set_value(Unit());
}

td_api::object_ptr<td_api::foundChatMessages> MessagesManager::get_found_chat_messages_object(
    DialogId dialog_id, const FoundDialogMessages &found_dialog_messages, const char *source) {
  auto *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  vector<tl_object_ptr<td_api::message>> result;
  result.reserve(found_dialog_messages.message_ids.size());
  for (const auto &message_id : found_dialog_messages.message_ids) {
    auto message = get_message_object(dialog_id, get_message_force(d, message_id, source), source);
    if (message != nullptr) {
      result.push_back(std::move(message));
    }
  }

  return td_api::make_object<td_api::foundChatMessages>(found_dialog_messages.total_count, std::move(result),
                                                        found_dialog_messages.next_from_message_id.get());
}

td_api::object_ptr<td_api::foundMessages> MessagesManager::get_found_messages_object(
    const FoundMessages &found_messages, const char *source) {
  vector<tl_object_ptr<td_api::message>> result;
  result.reserve(found_messages.message_full_ids.size());
  for (const auto &message_full_id : found_messages.message_full_ids) {
    auto message = get_message_object(message_full_id, source);
    if (message != nullptr) {
      result.push_back(std::move(message));
    }
  }

  return td_api::make_object<td_api::foundMessages>(found_messages.total_count, std::move(result),
                                                    found_messages.next_offset);
}

MessagesManager::FoundMessages MessagesManager::offline_search_messages(DialogId dialog_id, const string &query,
                                                                        string offset, int32 limit,
                                                                        MessageSearchFilter filter, int64 &random_id,
                                                                        Promise<Unit> &&promise) {
  if (!G()->use_message_database()) {
    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, "offline_search_messages")) {
    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;
  }

  MessageDbFtsQuery fts_query;
  fts_query.query = query;
  fts_query.dialog_id = dialog_id;
  fts_query.filter = filter;
  if (!offset.empty()) {
    auto r_from_search_id = to_integer_safe<int64>(offset);
    if (r_from_search_id.is_error()) {
      promise.set_error(Status::Error(400, "Invalid offset specified"));
      return {};
    }
    fts_query.from_search_id = r_from_search_id.ok();
  }
  fts_query.limit = limit;

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || found_fts_messages_.count(random_id) > 0);
  found_fts_messages_[random_id];  // reserve place for result

  G()->td_db()->get_message_db_async()->get_messages_fts(
      std::move(fts_query),
      PromiseCreator::lambda([random_id, offset = std::move(offset), limit,
                              promise = std::move(promise)](Result<MessageDbFtsResult> fts_result) mutable {
        send_closure(G()->messages_manager(), &MessagesManager::on_message_db_fts_result, std::move(fts_result),
                     std::move(offset), limit, random_id, std::move(promise));
      }));

  return {};
}

void MessagesManager::on_message_db_fts_result(Result<MessageDbFtsResult> result, string offset, int32 limit,
                                               int64 random_id, Promise<Unit> &&promise) {
  G()->ignore_result_if_closing(result);
  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.message_full_ids;

  res.reserve(fts_result.messages.size());
  for (auto &message : fts_result.messages) {
    auto m = on_get_message_from_database(message, false, "on_message_db_fts_result");
    if (m != nullptr) {
      res.emplace_back(message.dialog_id, m->message_id);
    }
  }

  it->second.next_offset = fts_result.next_search_id <= 1 ? string() : to_string(fts_result.next_search_id);
  it->second.total_count = offset.empty() && fts_result.messages.size() < static_cast<size_t>(limit)
                               ? static_cast<int32>(fts_result.messages.size())
                               : -1;

  promise.set_value(Unit());
}

void MessagesManager::on_message_db_calls_result(Result<MessageDbCallsResult> result, int64 random_id,
                                                 MessageId first_db_message_id, MessageSearchFilter filter,
                                                 Promise<Unit> &&promise) {
  G()->ignore_result_if_closing(result);
  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.message_full_ids;

  CHECK(!first_db_message_id.is_scheduled());
  res.reserve(calls_result.messages.size());
  MessageId next_offset_message_id;
  for (auto &message : calls_result.messages) {
    auto m = on_get_message_from_database(message, false, "on_message_db_calls_result");
    if (m != nullptr && first_db_message_id <= m->message_id) {
      if (!next_offset_message_id.is_valid() || m->message_id < next_offset_message_id) {
        next_offset_message_id = m->message_id;
      }
      res.emplace_back(message.dialog_id, m->message_id);
    }
  }
  it->second.total_count = calls_db_state_.message_count_by_index[call_message_search_filter_index(filter)];
  if (next_offset_message_id.is_valid()) {
    it->second.next_offset = PSTRING() << next_offset_message_id.get_server_message_id().get();
  }

  if (res.empty() && first_db_message_id != MessageId::min()) {
    LOG(INFO) << "No messages found in database";
    found_call_messages_.erase(it);
  }

  promise.set_value(Unit());
}

MessagesManager::FoundMessages MessagesManager::search_messages(FolderId folder_id, bool ignore_folder_id,
                                                                const string &query, const string &offset, int32 limit,
                                                                MessageSearchFilter filter, int32 min_date,
                                                                int32 max_date, 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;
  }

  if (limit <= 0) {
    promise.set_error(Status::Error(400, "Parameter limit must be positive"));
    return {};
  }
  if (limit > MAX_SEARCH_MESSAGES) {
    limit = MAX_SEARCH_MESSAGES;
  }

  int32 offset_date = std::numeric_limits<int32>::max();
  DialogId offset_dialog_id;
  MessageId offset_message_id;
  bool is_offset_valid = [&] {
    if (offset.empty()) {
      return true;
    }

    auto parts = full_split(offset, ',');
    if (parts.size() != 3) {
      return false;
    }
    auto r_offset_date = to_integer_safe<int32>(parts[0]);
    auto r_offset_dialog_id = to_integer_safe<int64>(parts[1]);
    auto r_offset_message_id = to_integer_safe<int32>(parts[2]);
    if (r_offset_date.is_error() || r_offset_date.ok() <= 0 || r_offset_message_id.is_error() ||
        r_offset_dialog_id.is_error()) {
      return false;
    }
    offset_date = r_offset_date.ok();
    offset_message_id = MessageId(ServerMessageId(r_offset_message_id.ok()));
    offset_dialog_id = DialogId(r_offset_dialog_id.ok());
    if (!offset_message_id.is_valid() || !offset_dialog_id.is_valid() ||
        DialogManager::get_input_peer_force(offset_dialog_id)->get_id() == telegram_api::inputPeerEmpty::ID) {
      return false;
    }
    return true;
  }();
  if (!is_offset_valid) {
    promise.set_error(Status::Error(400, "Invalid offset specified"));
    return {};
  }

  CHECK(filter != MessageSearchFilter::Call && filter != MessageSearchFilter::MissedCall);
  if (filter == MessageSearchFilter::Mention || filter == MessageSearchFilter::UnreadMention ||
      filter == MessageSearchFilter::UnreadReaction || filter == MessageSearchFilter::FailedToSend ||
      filter == MessageSearchFilter::Pinned) {
    promise.set_error(Status::Error(400, "The filter is not supported"));
    return {};
  }

  if (query.empty() && filter == MessageSearchFilter::Empty) {
    promise.set_value(Unit());
    return {};
  }

  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || found_messages_.count(random_id) > 0);
  found_messages_[random_id];  // reserve place for result

  LOG(DEBUG) << "Search all messages filtered by " << filter << " with query = \"" << query << "\" from offset "
             << offset << " and limit " << limit;

  td_->create_handler<SearchMessagesGlobalQuery>(std::move(promise))
      ->send(folder_id, ignore_folder_id, query, offset_date, offset_dialog_id, offset_message_id, limit, filter,
             min_date, max_date, random_id);
  return {};
}

int64 MessagesManager::get_dialog_message_by_date(DialogId dialog_id, int32 date, Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(dialog_id, "get_dialog_message_by_date");
  if (d == nullptr) {
    promise.set_error(Status::Error(400, "Chat not found"));
    return 0;
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(400, "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_.count(random_id) > 0);
  get_dialog_message_by_date_results_[random_id];  // reserve place for result

  auto message_id = d->ordered_messages.find_message_by_date(date, get_get_message_date(d));
  if (message_id.is_valid() &&
      (message_id == d->last_message_id || (*d->ordered_messages.get_const_iterator(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()->use_message_database() && d->last_database_message_id != MessageId()) {
    CHECK(d->first_database_message_id != MessageId());
    G()->td_db()->get_message_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<MessageDbDialogMessage> 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;
}

void MessagesManager::run_affected_history_query_until_complete(DialogId dialog_id, AffectedHistoryQuery query,
                                                                bool get_affected_messages, Promise<Unit> &&promise) {
  CHECK(!G()->close_flag());
  auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, query, get_affected_messages,
                                               promise = std::move(promise)](Result<AffectedHistory> &&result) mutable {
    if (result.is_error()) {
      return promise.set_error(result.move_as_error());
    }

    send_closure(actor_id, &MessagesManager::on_get_affected_history, dialog_id, query, get_affected_messages,
                 result.move_as_ok(), std::move(promise));
  });
  query(dialog_id, std::move(query_promise));
}

void MessagesManager::on_get_affected_history(DialogId dialog_id, AffectedHistoryQuery query,
                                              bool get_affected_messages, AffectedHistory affected_history,
                                              Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());
  LOG(INFO) << "Receive " << (affected_history.is_final_ ? "final " : "partial ")
            << "affected history with PTS = " << affected_history.pts_
            << " and pts_count = " << affected_history.pts_count_;

  if (affected_history.pts_count_ > 0) {
    if (get_affected_messages) {
      affected_history.pts_count_ = 0;
    }
    auto update_promise = affected_history.is_final_ ? std::move(promise) : Promise<Unit>();
    if (dialog_id.get_type() == DialogType::Channel) {
      add_pending_channel_update(dialog_id, make_tl_object<dummyUpdate>(), affected_history.pts_,
                                 affected_history.pts_count_, std::move(update_promise), "on_get_affected_history");
    } else {
      td_->updates_manager_->add_pending_pts_update(
          make_tl_object<dummyUpdate>(), affected_history.pts_, affected_history.pts_count_,
          Time::now() - (get_affected_messages ? 10.0 : 0.0), std::move(update_promise), "on_get_affected_history");
    }
  } else if (affected_history.is_final_) {
    promise.set_value(Unit());
  }

  if (!affected_history.is_final_) {
    run_affected_history_query_until_complete(dialog_id, std::move(query), get_affected_messages, std::move(promise));
  }
}

std::function<int32(MessageId)> MessagesManager::get_get_message_date(const Dialog *d) const {
  return [d](MessageId message_id) {
    const auto *m = get_message_static(d, message_id);
    CHECK(m != nullptr);
    return m->date;
  };
}

void MessagesManager::on_get_dialog_message_by_date_from_database(DialogId dialog_id, int32 date, int64 random_id,
                                                                  Result<MessageDbDialogMessage> result,
                                                                  Promise<Unit> promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (result.is_ok()) {
    Message *m = on_get_message_from_database(d, result.ok(), false, "on_get_dialog_message_by_date_from_database");
    if (m != nullptr) {
      auto message_id = d->ordered_messages.find_message_by_date(date, get_get_message_date(d));
      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 always be 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 = d->ordered_messages.find_message_by_date(date, get_get_message_date(d));
    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,
                                                            Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  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 == MessageFullId());

  for (auto &message : messages) {
    auto message_date = get_message_date(message);
    auto message_dialog_id = DialogId::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,
                              "on_get_dialog_message_by_date_success");
      if (result != MessageFullId()) {
        const Dialog *d = get_dialog(dialog_id);
        CHECK(d != nullptr);
        auto message_id = d->ordered_messages.find_message_by_date(date, get_get_message_date(d));
        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
        promise.set_value(Unit());
        return;
      }
    }
  }
  promise.set_value(Unit());
}

void MessagesManager::on_get_dialog_message_by_date_fail(int64 random_id) {
  auto erased_count = get_dialog_message_by_date_results_.erase(random_id);
  CHECK(erased_count > 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 message_full_id = std::move(it->second);
  get_dialog_message_by_date_results_.erase(it);
  return get_message_object(message_full_id, "get_dialog_message_by_date_object");
}

void MessagesManager::get_dialog_sparse_message_positions(
    DialogId dialog_id, SavedMessagesTopicId saved_messages_topic_id, MessageSearchFilter filter,
    MessageId from_message_id, int32 limit, Promise<td_api::object_ptr<td_api::messagePositions>> &&promise) {
  const Dialog *d = get_dialog_force(dialog_id, "get_dialog_sparse_message_positions");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  if (limit < 50 || limit > 2000) {  // server-side limits
    return promise.set_error(Status::Error(400, "Invalid limit specified"));
  }

  CHECK(filter != MessageSearchFilter::Call && filter != MessageSearchFilter::MissedCall);
  if (filter == MessageSearchFilter::Empty || filter == MessageSearchFilter::Mention ||
      filter == MessageSearchFilter::UnreadMention || filter == MessageSearchFilter::UnreadReaction ||
      filter == MessageSearchFilter::Pinned) {
    return promise.set_error(Status::Error(400, "The filter is not supported"));
  }

  if (from_message_id.is_scheduled()) {
    return promise.set_error(Status::Error(400, "Invalid from_message_id specified"));
  }
  if (!from_message_id.is_valid() || from_message_id > d->last_new_message_id) {
    if (d->last_new_message_id.is_valid()) {
      from_message_id = d->last_new_message_id.get_next_message_id(MessageType::Server);
    } else {
      from_message_id = MessageId::max();
    }
  } else {
    from_message_id = from_message_id.get_next_server_message_id();
  }
  TRY_STATUS_PROMISE(promise, saved_messages_topic_id.is_valid_in(td_, dialog_id));

  if (filter == MessageSearchFilter::FailedToSend || dialog_id.get_type() == DialogType::SecretChat) {
    if (saved_messages_topic_id.is_valid()) {
      return promise.set_value(td_api::make_object<td_api::messagePositions>());
    }
    if (!G()->use_message_database()) {
      return promise.set_error(Status::Error(400, "Unsupported without message database"));
    }

    LOG(INFO) << "Get sparse message positions from database";
    auto new_promise =
        PromiseCreator::lambda([promise = std::move(promise)](Result<MessageDbMessagePositions> result) mutable {
          TRY_STATUS_PROMISE(promise, G()->close_status());
          if (result.is_error()) {
            return promise.set_error(result.move_as_error());
          }

          auto positions = result.move_as_ok();
          promise.set_value(td_api::make_object<td_api::messagePositions>(
              positions.total_count, transform(positions.positions, [](const MessageDbMessagePosition &position) {
                return td_api::make_object<td_api::messagePosition>(position.position, position.message_id.get(),
                                                                    position.date);
              })));
        });
    MessageDbGetDialogSparseMessagePositionsQuery db_query;
    db_query.dialog_id = dialog_id;
    db_query.filter = filter;
    db_query.from_message_id = from_message_id;
    db_query.limit = limit;
    G()->td_db()->get_message_db_async()->get_dialog_sparse_message_positions(db_query, std::move(new_promise));
    return;
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      td_->create_handler<GetSearchResultPositionsQuery>(std::move(promise))
          ->send(dialog_id, saved_messages_topic_id, filter, from_message_id, limit);
      break;
    case DialogType::SecretChat:
    case DialogType::None:
    default:
      UNREACHABLE();
  }
}

void MessagesManager::on_get_dialog_sparse_message_positions(
    DialogId dialog_id, SavedMessagesTopicId saved_messages_topic_id, MessageSearchFilter filter,
    telegram_api::object_ptr<telegram_api::messages_searchResultsPositions> positions,
    Promise<td_api::object_ptr<td_api::messagePositions>> &&promise) {
  auto message_positions = transform(
      positions->positions_, [](const telegram_api::object_ptr<telegram_api::searchResultPosition> &position) {
        return td_api::make_object<td_api::messagePosition>(
            position->offset_, MessageId(ServerMessageId(position->msg_id_)).get(), position->date_);
      });
  promise.set_value(td_api::make_object<td_api::messagePositions>(positions->count_, std::move(message_positions)));
}

void MessagesManager::get_dialog_message_count(DialogId dialog_id, SavedMessagesTopicId saved_messages_topic_id,
                                               MessageSearchFilter filter, bool return_local,
                                               Promise<int32> &&promise) {
  LOG(INFO) << "Get " << (return_local ? "local " : "") << "number of messages in " << dialog_id << " filtered by "
            << filter;

  const Dialog *d = get_dialog_force(dialog_id, "get_dialog_message_count");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  if (filter == MessageSearchFilter::Empty) {
    return promise.set_error(Status::Error(400, "Can't use searchMessagesFilterEmpty"));
  }

  TRY_STATUS_PROMISE(promise, saved_messages_topic_id.is_valid_in(td_, dialog_id));
  if (saved_messages_topic_id.is_valid()) {
    if (filter == MessageSearchFilter::UnreadMention || filter == MessageSearchFilter::UnreadReaction ||
        filter == MessageSearchFilter::FailedToSend) {
      return promise.set_value(static_cast<int32>(0));
    }
    if (return_local) {
      return promise.set_value(static_cast<int32>(-1));
    }
  } else {
    auto dialog_type = dialog_id.get_type();
    int32 message_count = d->message_count_by_index[message_search_filter_index(filter)];
    if (message_count == -1 && filter == MessageSearchFilter::UnreadMention) {
      message_count = d->unread_mention_count;
    }
    if (message_count == -1 && filter == MessageSearchFilter::UnreadReaction) {
      message_count = d->unread_reaction_count;
    }
    if (message_count != -1 || return_local || dialog_type == DialogType::SecretChat ||
        filter == MessageSearchFilter::FailedToSend) {
      return promise.set_value(std::move(message_count));
    }
  }

  get_dialog_message_count_from_server(dialog_id, saved_messages_topic_id, filter, std::move(promise));
}

void MessagesManager::get_dialog_message_count_from_server(DialogId dialog_id,
                                                           SavedMessagesTopicId saved_messages_topic_id,
                                                           MessageSearchFilter filter, Promise<int32> &&promise) {
  LOG(INFO) << "Get number of messages in " << dialog_id << " with " << saved_messages_topic_id << " filtered by "
            << filter << " from the server";

  switch (dialog_id.get_type()) {
    case DialogType::User:
    case DialogType::Chat:
    case DialogType::Channel:
      td_->create_handler<GetSearchCountersQuery>(std::move(promise))->send(dialog_id, saved_messages_topic_id, filter);
      break;
    case DialogType::None:
    case DialogType::SecretChat:
    default:
      UNREACHABLE();
  }
}

void MessagesManager::get_dialog_message_position(MessageFullId message_full_id, MessageSearchFilter filter,
                                                  MessageId top_thread_message_id,
                                                  SavedMessagesTopicId saved_messages_topic_id,
                                                  Promise<int32> &&promise) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "get_dialog_message_position");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  auto message_id = message_full_id.get_message_id();
  const Message *m = get_message_force(d, message_id, "get_dialog_message_position");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }
  if (!m->message_id.is_valid() || !m->message_id.is_server() ||
      (filter != MessageSearchFilter::Empty &&
       (get_message_index_mask(d->dialog_id, m) & message_search_filter_index_mask(filter)) == 0)) {
    return promise.set_error(Status::Error(400, "Message can't be found in the filter"));
  }

  if (top_thread_message_id != MessageId()) {
    if (!top_thread_message_id.is_valid() || !top_thread_message_id.is_server()) {
      return promise.set_error(Status::Error(400, "Invalid message thread identifier specified"));
    }
    if (dialog_id.get_type() != DialogType::Channel || td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
      return promise.set_error(Status::Error(400, "Can't filter by message thread identifier in the chat"));
    }
    if (m->top_thread_message_id != top_thread_message_id ||
        (m->message_id == top_thread_message_id && !m->is_topic_message)) {
      return promise.set_error(Status::Error(400, "Message doesn't belong to the message thread"));
    }
  }
  TRY_STATUS_PROMISE(promise, saved_messages_topic_id.is_valid_in(td_, dialog_id));

  if (dialog_id.get_type() == DialogType::SecretChat) {
    return promise.set_error(Status::Error(400, "The method can't be used in secret chats"));
  }

  if (filter == MessageSearchFilter::UnreadMention || filter == MessageSearchFilter::UnreadReaction ||
      filter == MessageSearchFilter::FailedToSend) {
    return promise.set_error(Status::Error(400, "The filter is not supported"));
  }

  td_->create_handler<GetMessagePositionQuery>(std::move(promise))
      ->send(dialog_id, message_id, filter, top_thread_message_id, saved_messages_topic_id);
}

void MessagesManager::preload_newer_messages(const Dialog *d, MessageId max_message_id) {
  CHECK(d != nullptr);
  CHECK(max_message_id.is_valid());
  CHECK(!td_->auth_manager_->is_bot());

  auto it = d->ordered_messages.get_const_iterator(max_message_id);
  int32 limit = MAX_GET_HISTORY * 3 / 10;
  while (*it != nullptr && limit-- > 0) {
    ++it;
    if (*it) {
      max_message_id = (*it)->get_message_id();
    }
  }
  if (limit > 0 && (d->last_message_id == MessageId() || max_message_id < d->last_message_id)) {
    // need to preload some new messages
    LOG(INFO) << "Preloading newer after " << max_message_id;
    load_messages_impl(d, 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) {
  CHECK(d != nullptr);
  CHECK(min_message_id.is_valid());
  CHECK(!td_->auth_manager_->is_bot());

  /*
    if (d->first_remote_message_id == -1) {
      // nothing left to preload from server
      return;
    }
  */
  auto it = d->ordered_messages.get_const_iterator(min_message_id);
  int32 limit = MAX_GET_HISTORY * 3 / 10 + 1;
  while (*it != nullptr && limit-- > 0) {
    min_message_id = (*it)->get_message_id();
    --it;
  }
  if (limit > 0) {
    // need to preload some old messages
    LOG(INFO) << "Preloading older before " << min_message_id;
    load_messages_impl(d, min_message_id, 0, MAX_GET_HISTORY / 2, 3, false, Promise<Unit>());
  }
}

unique_ptr<MessagesManager::Message> MessagesManager::parse_message(Dialog *d, MessageId expected_message_id,
                                                                    const BufferSlice &value, bool is_scheduled) {
  CHECK(d != nullptr);
  auto dialog_id = d->dialog_id;
  auto message = make_unique<Message>();
  auto *m = message.get();

  auto status = log_event_parse(*m, value.as_slice());
  bool is_message_id_valid = [&] {
    if (is_scheduled) {
      if (!expected_message_id.is_valid_scheduled()) {
        return false;
      }
      if (m->message_id == expected_message_id) {
        return true;
      }
      return m->message_id.is_valid_scheduled() && expected_message_id.is_scheduled_server() &&
             m->message_id.is_scheduled_server() &&
             m->message_id.get_scheduled_server_message_id() == expected_message_id.get_scheduled_server_message_id();
    } else {
      if (!expected_message_id.is_valid()) {
        return false;
      }
      return m->message_id == expected_message_id;
    }
  }();
  if (status.is_error() || !is_message_id_valid) {
    // can't happen unless the database is broken, but has been seen in the wild
    LOG(ERROR) << "Receive invalid message from database: " << expected_message_id << ' ' << m->message_id << ' '
               << status << ' ' << format::as_hex_dump<4>(value.as_slice());
    if (!is_scheduled && dialog_id.get_type() != DialogType::SecretChat) {
      // trying to repair the message
      if (expected_message_id.is_valid() && expected_message_id.is_server()) {
        get_message_from_server({dialog_id, expected_message_id}, Auto(), "parse_message");
      }
      if (m->message_id.is_valid() && m->message_id.is_server()) {
        get_message_from_server({dialog_id, m->message_id}, Auto(), "parse_message");
      }
    }
    return nullptr;
  }
  if (m->reactions != nullptr) {
    if (td_->auth_manager_->is_bot() || m->available_reactions_generation < d->available_reactions_generation) {
      m->reactions = nullptr;
      m->available_reactions_generation = 0;
    } else if (m->available_reactions_generation > d->available_reactions_generation &&
               m->available_reactions_generation - d->available_reactions_generation < 1000000000) {
      switch (dialog_id.get_type()) {
        case DialogType::Chat:
        case DialogType::Channel:
          LOG(ERROR) << "Fix available_reactions_generation in " << dialog_id << " from "
                     << d->available_reactions_generation << " to " << m->available_reactions_generation;
          hide_dialog_message_reactions(d);
          set_dialog_next_available_reactions_generation(d, m->available_reactions_generation);
          on_dialog_updated(dialog_id, "parse_message");
          break;
        case DialogType::User:
        case DialogType::SecretChat:
        default:
          LOG(ERROR) << "Receive available_reactions_generation = " << m->available_reactions_generation << " in "
                     << m->message_id << " in " << dialog_id;
          break;
      }
    }
    if (m->reactions != nullptr) {
      m->reactions->fix_my_recent_chooser_dialog_id(td_->dialog_manager_->get_my_dialog_id());
    }
  }
  if (m->contains_mention && td_->auth_manager_->is_bot()) {
    m->contains_mention = false;
    m->contains_unread_mention = false;
  }
  if (m->history_generation > d->history_generation && m->history_generation - d->history_generation < 1000000000) {
    switch (dialog_id.get_type()) {
      case DialogType::Channel:
        LOG(ERROR) << "Fix history_generation in " << dialog_id << " from " << d->history_generation << " to "
                   << m->history_generation;
        d->history_generation = m->history_generation + 1;
        on_dialog_updated(dialog_id, "parse_message");
        break;
      case DialogType::Chat:
      case DialogType::User:
      case DialogType::SecretChat:
      default:
        LOG(ERROR) << "Receive history_generation = " << m->history_generation << " in " << m->message_id << " in "
                   << dialog_id;
        break;
    }
  }
  if (m->is_pinned && is_scheduled) {
    m->is_pinned = false;
  }
  if (dialog_id == td_->dialog_manager_->get_my_dialog_id() && !m->saved_messages_topic_id.is_valid()) {
    m->saved_messages_topic_id = SavedMessagesTopicId(dialog_id, m->forward_info.get(), m->real_forward_from_dialog_id);
  }

  LOG(INFO) << "Loaded " << m->message_id << " in " << dialog_id << " of size " << value.size() << " from database";
  return message;
}

void MessagesManager::on_get_history_from_database(DialogId dialog_id, MessageId from_message_id,
                                                   MessageId old_last_database_message_id, int32 offset, int32 limit,
                                                   bool only_local, vector<MessageDbDialogMessage> &&messages,
                                                   Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());
  bool from_the_end = from_message_id == MessageId::max();
  CHECK(-limit < offset && offset <= 0);
  CHECK(offset < 0 || from_the_end);
  CHECK(!from_message_id.is_scheduled());

  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  if (old_last_database_message_id < d->last_database_message_id && old_last_database_message_id < from_message_id) {
    // new messages where added to the database since the request was sent
    // they should have been received from the database, so we must repeat the request to get them
    LOG(INFO) << "Reget chat history from database in " << dialog_id
              << ", because last database message was changed from " << old_last_database_message_id << " to "
              << d->last_database_message_id;
    get_history_impl(d, from_message_id, offset, limit, true, only_local, std::move(promise),
                     "on_get_history_from_database 20");
    return;
  }

  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 << ", last database message is " << d->last_database_message_id
            << ", have_full_history = " << d->have_full_history
            << ", have_full_history_source = " << d->have_full_history_source;

  bool had_full_history = d->have_full_history;
  auto debug_first_database_message_id = d->first_database_message_id;
  auto debug_last_message_id = d->last_message_id;
  auto debug_last_new_message_id = d->last_new_message_id;

  bool have_error = false;
  auto message_ids = on_get_messages_from_database(
      d, std::move(messages), d->have_full_history ? MessageId::min() : d->first_database_message_id, have_error,
      "on_get_history_from_database");
  if (have_error) {
    // database is broken
    message_ids.clear();  // ignore received messages just in case
    if (d->have_full_history) {
      d->have_full_history = false;
      d->have_full_history_source = 0;
      d->is_empty = false;  // just in case
      on_dialog_updated(dialog_id, "drop have_full_history in on_get_history_from_database");
    }
  }

  vector<bool> is_added_to_dialog;
  for (auto &message_id : message_ids) {
    Message *m = get_message(d, message_id);
    is_added_to_dialog.push_back(m != nullptr);
  }

  bool have_next = false;
  MessageId first_added_message_id;
  MessageId last_added_message_id;
  MessageId next_message_id;
  auto first_received_message_id = MessageId::max();
  MessageId last_received_message_id;
  for (size_t pos = 0; pos < message_ids.size(); pos++) {
    auto message_id = message_ids[pos];
    bool is_added = is_added_to_dialog[pos];
    first_received_message_id = message_id;
    if (!last_received_message_id.is_valid()) {
      last_received_message_id = message_id;
    }

    if (!have_next && (from_the_end || (pos == 0 && offset < -1 && message_id <= from_message_id)) &&
        message_id < d->last_message_id) {
      // last message in the dialog must be attached to the next local message
      have_next = true;
    }

    if (is_added) {
      if (have_next) {
        d->ordered_messages.attach_message_to_next(message_id, "on_get_history");
      }
      first_added_message_id = message_id;
      if (!last_added_message_id.is_valid()) {
        last_added_message_id = message_id;
      }
      if (next_message_id.is_valid()) {
        CHECK(message_id < next_message_id);
        d->ordered_messages.attach_message_to_previous(
            next_message_id, (PSLICE() << "on_get_history_from_database 1 " << message_id << ' ' << from_message_id
                                       << ' ' << offset << ' ' << limit << ' ' << d->first_database_message_id << ' '
                                       << d->have_full_history << ' ' << pos)
                                 .c_str());
      }

      have_next = true;
      next_message_id = message_id;
    }
  }

  if (from_the_end && messages.empty() && d->ordered_messages.empty()) {
    if (d->have_full_history) {
      set_dialog_is_empty(d, "on_get_history_from_database empty");
    } else if (d->last_database_message_id.is_valid()) {
      set_dialog_first_database_message_id(d, MessageId(), "on_get_history_from_database empty");
      set_dialog_last_database_message_id(d, MessageId(), "on_get_history_from_database empty");
    }
  }

  if (from_the_end && !last_added_message_id.is_valid() && d->first_database_message_id.is_valid() &&
      !d->have_full_history) {
    if (first_received_message_id <= d->first_database_message_id) {
      // database definitely has no messages from first_database_message_id to last_database_message_id; drop them
      set_dialog_first_database_message_id(d, MessageId(), "on_get_history_from_database 8");
      set_dialog_last_database_message_id(d, MessageId(), "on_get_history_from_database 9");
    } else {
      CHECK(first_received_message_id.is_valid());
      // if a message was received, but wasn't added, then it is likely to be already deleted
      // if it is less than d->last_database_message_id, then we can adjust d->last_database_message_id and
      // try again database search without chance to loop
      if (first_received_message_id < d->last_database_message_id) {
        set_dialog_last_database_message_id(d, first_received_message_id, "on_get_history_from_database 12");

        get_history_impl(d, MessageId::max(), 0, -1, true, only_local, std::move(promise),
                         "on_get_history_from_database 21");
        return;
      }

      if (limit > 1) {
        // we expected to have messages [first_database_message_id, last_database_message_id] in the database, but
        // received messages [first_received_message_id, last_received_message_id], none of which can be added
        // first_database_message_id and last_database_message_id are very wrong, so it is better to drop them,
        // pretending that the database has no usable messages
        if (!is_deleted_message(d, d->first_database_message_id) ||
            !is_deleted_message(d, d->last_database_message_id)) {
          if (first_received_message_id == MessageId::max()) {
            CHECK(last_received_message_id == MessageId());
            LOG(ERROR) << "Receive no usable messages in " << dialog_id
                       << " from database from the end, but expected messages from " << d->first_database_message_id
                       << " up to " << d->last_database_message_id
                       << ". Have old last_database_message_id = " << old_last_database_message_id << " and "
                       << messages.size() << " received messages";
          } else {
            LOG(ERROR) << "Receive " << messages.size() << " unusable messages [" << first_received_message_id
                       << " ... " << last_received_message_id << "] in " << dialog_id
                       << " from database from the end, but expected messages from " << d->first_database_message_id
                       << " up to " << d->last_database_message_id;
          }
        }
        set_dialog_first_database_message_id(d, MessageId(), "on_get_history_from_database 13");
        set_dialog_last_database_message_id(d, MessageId(), "on_get_history_from_database 14");
      }
    }
  }

  if (!first_added_message_id.is_valid() && !only_local && dialog_id.get_type() != DialogType::SecretChat) {
    load_messages_impl(d, from_message_id, offset, limit, 1, false, std::move(promise));
    return;
  }

  bool need_update_dialog_pos = false;
  if (from_the_end && last_added_message_id.is_valid()) {
    CHECK(next_message_id.is_valid());
    // CHECK(d->first_database_message_id.is_valid());
    // CHECK(last_added_message_id >= d->first_database_message_id);
    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 > d->last_message_id && 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 != d->last_database_message_id && d->last_new_message_id.is_valid()) {
      auto debug_last_database_message_id = d->last_database_message_id;
      auto debug_set_dialog_last_database_message_id = d->debug_set_dialog_last_database_message_id;
      if (!d->first_database_message_id.is_valid() && !d->last_database_message_id.is_valid()) {
        set_dialog_first_database_message_id(d, next_message_id, "on_get_history_from_database 5");
      }
      set_dialog_last_database_message_id(d, last_added_message_id, "on_get_history_from_database 5");
      if (last_added_message_id < d->first_database_message_id || !d->first_database_message_id.is_valid()) {
        LOG_CHECK(had_full_history || d->have_full_history)
            << had_full_history << ' ' << d->have_full_history << ' ' << next_message_id << ' ' << last_added_message_id
            << ' ' << d->first_database_message_id << ' ' << debug_first_database_message_id << ' '
            << d->last_database_message_id << ' ' << debug_last_database_message_id << ' ' << dialog_id << ' '
            << d->last_new_message_id << ' ' << debug_last_new_message_id << ' ' << d->last_message_id << ' '
            << debug_last_message_id << ' ' << debug_set_dialog_last_database_message_id << ' '
            << d->debug_set_dialog_last_database_message_id << ' ' << first_received_message_id << ' '
            << last_received_message_id << ' ' << d->debug_first_database_message_id << ' '
            << d->debug_last_database_message_id << ' ' << d->debug_last_new_message_id << ' '
            << d->have_full_history_source;
        CHECK(next_message_id <= d->last_database_message_id);
        LOG(ERROR) << "Fix first database message in " << dialog_id << " from " << d->first_database_message_id
                   << " to " << next_message_id;
        set_dialog_first_database_message_id(d, next_message_id, "on_get_history_from_database 6");
      }
    }
  }
  if (first_added_message_id.is_valid() && first_added_message_id != d->first_database_message_id &&
      first_received_message_id < d->first_database_message_id && d->last_new_message_id.is_valid() &&
      !d->have_full_history) {
    CHECK(first_added_message_id > d->first_database_message_id);
    set_dialog_first_database_message_id(d, first_added_message_id, "on_get_history_from_database 10");
    if (d->last_database_message_id < d->first_database_message_id) {
      set_dialog_last_database_message_id(d, d->first_database_message_id, "on_get_history_from_database 11");
    }
  }

  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "on_get_history_from_database 7");
  }

  promise.set_value(Unit());
}

void MessagesManager::load_last_dialog_message_later(DialogId dialog_id) {
  if (G()->close_flag()) {
    return;
  }
  load_last_dialog_message(get_dialog(dialog_id), "load_last_dialog_message");
}

void MessagesManager::load_last_dialog_message(const Dialog *d, const char *source) {
  get_history_impl(d, MessageId::max(), 0, -1, true, false, Promise<Unit>(), source);
}

void MessagesManager::on_get_history_finished(const PendingGetHistoryQuery &query, Result<Unit> &&result) {
  G()->ignore_result_if_closing(result);

  auto it = get_history_queries_.find(query);
  if (it == get_history_queries_.end()) {
    return;
  }
  auto promises = std::move(it->second);
  CHECK(!promises.empty());
  get_history_queries_.erase(it);

  if (result.is_ok()) {
    set_promises(promises);
  } else {
    fail_promises(promises, result.move_as_error());
  }
}

void MessagesManager::get_history_impl(const Dialog *d, MessageId from_message_id, int32 offset, int32 limit,
                                       bool from_database, bool only_local, Promise<Unit> &&promise,
                                       const char *source) {
  CHECK(d != nullptr);
  bool from_the_end = from_message_id == MessageId() || from_message_id == MessageId::max();
  if (!from_the_end) {
    CHECK(from_message_id.is_valid());
  } else {
    from_message_id = MessageId::max();
  }

  auto dialog_id = d->dialog_id;
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    // can't get history in dialogs without read access
    return promise.set_value(Unit());
  }
  if ((!d->first_database_message_id.is_valid() || from_message_id <= d->first_database_message_id) &&
      !d->have_full_history) {
    from_database = false;
  }
  if (!G()->use_message_database()) {
    from_database = false;
  }

  if (from_the_end) {
    // load only 10 messages when repairing the last message and can't save the result to the database
    limit = !promise && (from_database || !G()->use_message_database()) ? max(limit, 10) : MAX_GET_HISTORY;
    offset = 0;
  } else if (offset >= -1) {
    // get history before some server or local message
    limit = clamp(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;
  }

  PendingGetHistoryQuery query;
  query.dialog_id_ = dialog_id;
  query.from_message_id_ = from_message_id;
  query.offset_ = offset;
  query.limit_ = limit;
  query.from_database_ = from_database;
  query.only_local_ = only_local;

  if (from_database) {
    LOG(INFO) << "Get history in " << dialog_id << " from " << from_message_id << " with offset " << offset
              << " and limit " << limit << " from database from " << source;

    query.old_last_message_id_ = d->last_database_message_id;

    auto &promises = get_history_queries_[query];
    promises.push_back(std::move(promise));
    if (promises.size() != 1) {
      // query has already been sent, just wait for the result
      return;
    }

    auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), query](Result<Unit> &&result) {
      send_closure(actor_id, &MessagesManager::on_get_history_finished, query, std::move(result));
    });

    MessageDbMessagesQuery 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_message_db_async()->get_messages(
        db_query,
        PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, from_message_id,
                                old_last_database_message_id = d->last_database_message_id, offset, limit, only_local,
                                promise = std::move(query_promise)](vector<MessageDbDialogMessage> messages) mutable {
          send_closure(actor_id, &MessagesManager::on_get_history_from_database, dialog_id, from_message_id,
                       old_last_database_message_id, offset, limit, only_local, std::move(messages),
                       std::move(promise));
        }));
  } else {
    if (only_local || dialog_id.get_type() == DialogType::SecretChat ||
        (from_the_end && d->last_message_id.is_valid())) {
      // if the last message is known, there are no reasons to get message history from server from the end
      return promise.set_value(Unit());
    }

    query.old_last_message_id_ = d->last_new_message_id;

    auto &promises = get_history_queries_[query];
    promises.push_back(std::move(promise));
    if (promises.size() != 1) {
      // query has already been sent, just wait for the result
      return;
    }

    auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), query](Result<Unit> &&result) {
      send_closure(actor_id, &MessagesManager::on_get_history_finished, query, std::move(result));
    });

    if (from_the_end) {
      LOG(INFO) << "Get history from the end of " << dialog_id << " from server from " << source;
      td_->create_handler<GetHistoryQuery>(std::move(query_promise))
          ->send_get_from_the_end(dialog_id, d->last_new_message_id, limit);
    } else {
      LOG(INFO) << "Get history in " << dialog_id << " from " << from_message_id << " with offset " << offset
                << " and limit " << limit << " from server from " << source;
      td_->create_handler<GetHistoryQuery>(std::move(query_promise))
          ->send(dialog_id, from_message_id.get_next_server_message_id(), d->last_new_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) {
  load_messages_impl(get_dialog(dialog_id), from_message_id, offset, limit, left_tries, only_local, std::move(promise));
}

void MessagesManager::load_messages_impl(const Dialog *d, MessageId from_message_id, int32 offset, int32 limit,
                                         int left_tries, bool only_local, Promise<Unit> &&promise) {
  CHECK(d != nullptr);
  CHECK(offset <= 0);
  CHECK(left_tries > 0);
  auto dialog_id = d->dialog_id;
  LOG(INFO) << "Load " << (only_local ? "local " : "") << "messages in " << dialog_id << " from " << from_message_id
            << " with offset = " << offset << " and limit = " << limit << ". " << left_tries << " tries left";
  only_local |= dialog_id.get_type() == DialogType::SecretChat;
  if (!only_local && 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()->use_message_database();
  get_history_impl(d, from_message_id, offset, limit, from_database, only_local, std::move(promise),
                   "load_messages_impl");
}

vector<MessageId> MessagesManager::get_dialog_scheduled_messages(DialogId dialog_id, bool force, bool ignore_result,
                                                                 Promise<Unit> &&promise) {
  if (G()->close_flag()) {
    promise.set_error(Global::request_aborted_error());
    return {};
  }

  LOG(INFO) << "Get scheduled messages in " << dialog_id;
  Dialog *d = get_dialog_force(dialog_id, "get_dialog_scheduled_messages");
  if (d == nullptr) {
    promise.set_error(Status::Error(400, "Chat not found"));
    return {};
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    promise.set_error(Status::Error(400, "Can't access the chat"));
    return {};
  }
  if (td_->dialog_manager_->is_broadcast_channel(dialog_id) &&
      !td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id()).can_post_messages()) {
    promise.set_error(Status::Error(400, "Not enough rights to get scheduled messages"));
    return {};
  }
  if (d->dialog_id.get_type() == DialogType::SecretChat) {
    promise.set_value(Unit());
    return {};
  }

  if (!d->has_loaded_scheduled_messages_from_database) {
    load_dialog_scheduled_messages(dialog_id, true, 0, std::move(promise));
    return {};
  }

  vector<MessageId> message_ids;
  if (d->scheduled_messages != nullptr) {
    for (const auto &it : d->scheduled_messages->scheduled_messages_) {
      message_ids.push_back(it.first);
    };
    std::sort(message_ids.begin(), message_ids.end(), std::greater<>());
  }

  if (G()->use_message_database()) {
    bool has_scheduled_database_messages = false;
    for (auto &message_id : message_ids) {
      CHECK(message_id.is_valid_scheduled());
      if (!message_id.is_yet_unsent()) {
        has_scheduled_database_messages = true;
        break;
      }
    }
    set_dialog_has_scheduled_database_messages(d->dialog_id, has_scheduled_database_messages);
  }

  if (d->scheduled_messages_sync_generation != scheduled_messages_sync_generation_) {
    vector<uint64> numbers;
    for (auto &message_id : message_ids) {
      if (!message_id.is_scheduled_server()) {
        continue;
      }

      numbers.push_back(message_id.get_scheduled_server_message_id().get());
      const Message *m = get_message(d, message_id);
      CHECK(m != nullptr);
      CHECK(m->message_id.get_scheduled_server_message_id() == message_id.get_scheduled_server_message_id());
      numbers.push_back(m->edit_date);
      numbers.push_back(m->date);
    }
    auto hash = get_vector_hash(numbers);

    if (!force && (d->has_scheduled_server_messages ||
                   (d->scheduled_messages_sync_generation == 0 && !G()->use_message_database()))) {
      load_dialog_scheduled_messages(dialog_id, false, hash, std::move(promise));
      return {};
    }
    load_dialog_scheduled_messages(dialog_id, false, hash, Promise<Unit>());
  }

  if (!ignore_result) {
    d->sent_scheduled_messages = true;
  }

  promise.set_value(Unit());
  return message_ids;
}

void MessagesManager::load_dialog_scheduled_messages(DialogId dialog_id, bool from_database, int64 hash,
                                                     Promise<Unit> &&promise) {
  CHECK(dialog_id.get_type() != DialogType::SecretChat);
  if (G()->use_message_database() && from_database) {
    LOG(INFO) << "Load scheduled messages from database in " << dialog_id;
    auto &queries = load_scheduled_messages_from_database_queries_[dialog_id];
    queries.push_back(std::move(promise));
    if (queries.size() == 1) {
      G()->td_db()->get_message_db_async()->get_scheduled_messages(
          dialog_id, 1000,
          PromiseCreator::lambda([actor_id = actor_id(this), dialog_id](vector<MessageDbDialogMessage> messages) {
            send_closure(actor_id, &MessagesManager::on_get_scheduled_messages_from_database, dialog_id,
                         std::move(messages));
          }));
    }
  } else {
    td_->create_handler<GetAllScheduledMessagesQuery>(std::move(promise))
        ->send(dialog_id, hash, scheduled_messages_sync_generation_);
  }
}

void MessagesManager::on_get_scheduled_messages_from_database(DialogId dialog_id,
                                                              vector<MessageDbDialogMessage> &&messages) {
  if (G()->close_flag()) {
    auto it = load_scheduled_messages_from_database_queries_.find(dialog_id);
    CHECK(it != load_scheduled_messages_from_database_queries_.end());
    CHECK(!it->second.empty());
    auto promises = std::move(it->second);
    load_scheduled_messages_from_database_queries_.erase(it);

    fail_promises(promises, Global::request_aborted_error());
    return;
  }
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  d->has_loaded_scheduled_messages_from_database = true;

  LOG(INFO) << "Receive " << messages.size() << " scheduled messages from database in " << dialog_id;

  Dependencies dependencies;
  vector<MessageId> added_message_ids;
  for (auto &message_slice : messages) {
    auto message = parse_message(d, message_slice.message_id, message_slice.data, true);
    if (message == nullptr) {
      continue;
    }

    if (get_message(d, message->message_id) != nullptr) {
      continue;
    }

    bool need_update = false;
    Message *m = add_scheduled_message_to_dialog(d, std::move(message), true, false, &need_update,
                                                 "on_get_scheduled_messages_from_database");
    if (m != nullptr) {
      add_message_dependencies(dependencies, m);
      added_message_ids.push_back(m->message_id);
    }
  }
  dependencies.resolve_force(td_, "on_get_scheduled_messages_from_database");

  // for (auto message_id : added_message_ids) {
  //   send_update_new_message(d, get_message(d, message_id));
  // }
  send_update_chat_has_scheduled_messages(d, false);

  auto it = load_scheduled_messages_from_database_queries_.find(dialog_id);
  CHECK(it != load_scheduled_messages_from_database_queries_.end());
  CHECK(!it->second.empty());
  auto promises = std::move(it->second);
  load_scheduled_messages_from_database_queries_.erase(it);

  set_promises(promises);
}

bool MessagesManager::can_add_message_tag(DialogId dialog_id, const MessageReactions *reactions) const {
  return dialog_id == td_->dialog_manager_->get_my_dialog_id() &&
         (reactions == nullptr || reactions->reactions_.empty() || reactions->are_tags_);
}

Result<td_api::object_ptr<td_api::availableReactions>> MessagesManager::get_message_available_reactions(
    MessageFullId message_full_id, int32 row_size) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "get_message_available_reactions");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }

  const Message *m = get_message_force(d, message_full_id.get_message_id(), "get_message_available_reactions");
  if (m == nullptr) {
    return Status::Error(400, "Message not found");
  }

  bool is_tag = can_add_message_tag(dialog_id, m->reactions.get());
  ReactionUnavailabilityReason unavailability_reason = ReactionUnavailabilityReason::None;
  auto available_reactions = get_message_available_reactions(d, m, false, &unavailability_reason);
  return td_->reaction_manager_->get_sorted_available_reactions(
      std::move(available_reactions), get_message_active_reactions(d, m), row_size, is_tag, unavailability_reason);
}

ChatReactions MessagesManager::get_message_available_reactions(const Dialog *d, const Message *m,
                                                               bool disallow_custom_for_non_premium,
                                                               ReactionUnavailabilityReason *unavailability_reason) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  auto active_reactions = get_message_active_reactions(d, m);
  if (active_reactions.empty()) {
    return {};
  }

  bool can_use_reactions = true;
  if (d->dialog_id.get_type() == DialogType::Channel) {
    auto channel_id = d->dialog_id.get_channel_id();
    if (td_->contacts_manager_->is_megagroup_channel(channel_id) &&
        !td_->contacts_manager_->get_channel_status(channel_id).is_member() &&
        can_send_message(d->dialog_id).is_error()) {
      // can't use reactions if can't send messages to the group without joining
      if (unavailability_reason != nullptr) {
        *unavailability_reason = ReactionUnavailabilityReason::Guest;
      }
      can_use_reactions = false;
    } else if (td_->dialog_manager_->is_anonymous_administrator(d->dialog_id, nullptr) &&
               !td_->dialog_manager_->is_broadcast_channel(d->dialog_id) &&
               !td_->contacts_manager_->get_channel_status(channel_id).is_creator()) {
      // only creator can react as the chat
      if (unavailability_reason != nullptr) {
        *unavailability_reason = ReactionUnavailabilityReason::AnonymousAdministrator;
      }
      can_use_reactions = false;
    }
  }

  int64 reactions_uniq_max = td_->option_manager_->get_option_integer("reactions_uniq_max", 11);
  bool can_add_new_reactions =
      m->reactions == nullptr || static_cast<int64>(m->reactions->reactions_.size()) < reactions_uniq_max;

  if (!can_use_reactions || !can_add_new_reactions) {
    active_reactions = ChatReactions();
  }

  if (active_reactions.allow_all_regular_) {
    if (can_add_message_tag(d->dialog_id, m->reactions.get())) {
      auto default_tag_reactions = td_->reaction_manager_->get_default_tag_reactions();
      active_reactions.reaction_types_ = default_tag_reactions;
      if (td_->option_manager_->get_option_boolean("is_premium")) {
        for (auto &reaction_type : active_reaction_types_) {
          if (!td::contains(default_tag_reactions, reaction_type)) {
            active_reactions.reaction_types_.push_back(reaction_type);
          }
        }
      } else {
        disallow_custom_for_non_premium = true;
      }
    } else {
      active_reactions.reaction_types_ = active_reaction_types_;
    }
    active_reactions.allow_all_regular_ = false;
  }
  if (can_use_reactions && m->reactions != nullptr) {
    for (const auto &reaction : m->reactions->reactions_) {
      // an already used reaction can be added if it is an active reaction
      const auto &reaction_type = reaction.get_reaction_type();
      if (reaction_type.is_active_reaction(active_reaction_pos_) &&
          !td::contains(active_reactions.reaction_types_, reaction_type)) {
        active_reactions.reaction_types_.push_back(reaction_type);
      }
    }
  }
  if (disallow_custom_for_non_premium && !td_->option_manager_->get_option_boolean("is_premium")) {
    active_reactions.allow_all_custom_ = false;
  }
  return active_reactions;
}

DialogId MessagesManager::get_my_reaction_dialog_id(const Dialog *d) const {
  auto my_dialog_id = td_->dialog_manager_->get_my_dialog_id();
  auto reaction_dialog_id =
      d->default_send_message_as_dialog_id.is_valid() ? d->default_send_message_as_dialog_id : my_dialog_id;
  if (reaction_dialog_id == my_dialog_id && td_->dialog_manager_->is_anonymous_administrator(d->dialog_id, nullptr) &&
      !td_->dialog_manager_->is_broadcast_channel(d->dialog_id)) {
    // react as the supergroup
    return d->dialog_id;
  }
  return reaction_dialog_id;
}

void MessagesManager::add_message_reaction(MessageFullId message_full_id, ReactionType reaction_type, bool is_big,
                                           bool add_to_recent, Promise<Unit> &&promise) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "add_message_reaction");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  Message *m = get_message_force(d, message_full_id.get_message_id(), "add_message_reaction");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }

  if (!get_message_available_reactions(d, m, true, nullptr).is_allowed_reaction_type(reaction_type)) {
    return promise.set_error(Status::Error(400, "The reaction isn't available for the message"));
  }

  bool have_recent_choosers =
      !td_->dialog_manager_->is_broadcast_channel(dialog_id) && !is_discussion_message(dialog_id, m);
  if (m->reactions == nullptr) {
    m->reactions = make_unique<MessageReactions>();
    m->reactions->can_get_added_reactions_ = have_recent_choosers && dialog_id.get_type() != DialogType::User;
    m->available_reactions_generation = d->available_reactions_generation;
  }

  LOG(INFO) << "Have message with " << *m->reactions;
  bool is_tag = can_add_message_tag(dialog_id, m->reactions.get());
  auto old_chosen_tags = get_chosen_tags(m->reactions);
  if (!m->reactions->add_my_reaction(reaction_type, is_big, get_my_reaction_dialog_id(d), have_recent_choosers,
                                     is_tag)) {
    return promise.set_value(Unit());
  }

  set_message_reactions(d, m, is_big, add_to_recent, std::move(promise));

  if (is_tag) {
    td_->reaction_manager_->update_saved_messages_tags(m->saved_messages_topic_id, old_chosen_tags,
                                                       get_chosen_tags(m->reactions));
  } else if (add_to_recent) {
    td_->reaction_manager_->add_recent_reaction(reaction_type);
  }
}

void MessagesManager::remove_message_reaction(MessageFullId message_full_id, ReactionType reaction_type,
                                              Promise<Unit> &&promise) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "remove_message_reaction");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  Message *m = get_message_force(d, message_full_id.get_message_id(), "remove_message_reaction");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }

  if (reaction_type.is_empty()) {
    return promise.set_error(Status::Error(400, "Invalid reaction specified"));
  }

  if (m->reactions == nullptr) {
    return promise.set_value(Unit());
  }

  LOG(INFO) << "Have message with " << *m->reactions;
  auto old_chosen_tags = get_chosen_tags(m->reactions);
  if (!m->reactions->remove_my_reaction(reaction_type, get_my_reaction_dialog_id(d))) {
    return promise.set_value(Unit());
  }

  set_message_reactions(d, m, false, false, std::move(promise));

  if (!old_chosen_tags.empty()) {
    td_->reaction_manager_->update_saved_messages_tags(m->saved_messages_topic_id, old_chosen_tags,
                                                       get_chosen_tags(m->reactions));
  }
}

void MessagesManager::set_message_reactions(Dialog *d, Message *m, bool is_big, bool add_to_recent,
                                            Promise<Unit> &&promise) {
  CHECK(m->reactions != nullptr);
  m->reactions->sort_reactions(active_reaction_pos_);

  LOG(INFO) << "Update message reactions to " << *m->reactions;

  MessageFullId message_full_id{d->dialog_id, m->message_id};
  pending_reactions_[message_full_id].query_count++;

  send_update_message_interaction_info(d->dialog_id, m);
  on_message_changed(d, m, true, "set_message_reactions");

  // TODO cancel previous queries, log event
  auto query_promise = PromiseCreator::lambda(
      [actor_id = actor_id(this), message_full_id, promise = std::move(promise)](Result<Unit> &&result) mutable {
        send_closure(actor_id, &MessagesManager::on_set_message_reactions, message_full_id, std::move(result),
                     std::move(promise));
      });
  send_message_reaction(td_, message_full_id, m->reactions->get_chosen_reaction_types(), is_big, add_to_recent,
                        std::move(query_promise));
}

void MessagesManager::on_set_message_reactions(MessageFullId message_full_id, Result<Unit> result,
                                               Promise<Unit> promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  bool need_reload = result.is_error();
  auto it = pending_reactions_.find(message_full_id);
  CHECK(it != pending_reactions_.end());
  if (--it->second.query_count == 0) {
    // need_reload |= it->second.was_updated;
    pending_reactions_.erase(it);
  }

  if (!have_message_force(message_full_id, "on_set_message_reactions")) {
    return promise.set_value(Unit());
  }

  if (need_reload) {
    queue_message_reactions_reload(message_full_id);
  }

  promise.set_result(std::move(result));
}

void MessagesManager::on_read_message_reactions(DialogId dialog_id, vector<MessageId> &&message_ids,
                                                Result<Unit> &&result) {
  for (auto message_id : message_ids) {
    MessageFullId message_full_id{dialog_id, message_id};
    auto it = pending_read_reactions_.find(message_full_id);
    CHECK(it != pending_read_reactions_.end());
    if (--it->second == 0) {
      pending_read_reactions_.erase(it);
    }

    if (!have_message_force(message_full_id, "on_read_message_reactions")) {
      continue;
    }

    if (result.is_error()) {
      queue_message_reactions_reload(message_full_id);
    }
  }
}

Result<int32> MessagesManager::get_message_schedule_date(
    td_api::object_ptr<td_api::MessageSchedulingState> &&scheduling_state) {
  if (scheduling_state == nullptr) {
    return 0;
  }

  switch (scheduling_state->get_id()) {
    case td_api::messageSchedulingStateSendWhenOnline::ID: {
      auto send_date = SCHEDULE_WHEN_ONLINE_DATE;
      return send_date;
    }
    case td_api::messageSchedulingStateSendAtDate::ID: {
      auto send_at_date = td_api::move_object_as<td_api::messageSchedulingStateSendAtDate>(scheduling_state);
      auto send_date = send_at_date->send_date_;
      if (send_date <= 0) {
        return Status::Error(400, "Invalid send date specified");
      }
      if (send_date <= G()->unix_time() + 10) {
        return 0;
      }
      if (send_date - G()->unix_time() > 367 * 86400) {
        return Status::Error(400, "Send date is too far in the future");
      }
      return send_date;
    }
    default:
      UNREACHABLE();
      return 0;
  }
}

tl_object_ptr<td_api::MessageSendingState> MessagesManager::get_message_sending_state_object(const Message *m) const {
  CHECK(m != nullptr);
  if (m->message_id.is_yet_unsent()) {
    return td_api::make_object<td_api::messageSendingStatePending>(m->sending_id);
  }
  if (m->is_failed_to_send) {
    auto can_retry = can_resend_message(m);
    auto error_code = m->send_error_code > 0 ? m->send_error_code : 400;
    auto need_another_sender =
        can_retry && error_code == 400 && m->send_error_message == CSlice("SEND_AS_PEER_INVALID");
    auto need_another_reply_quote =
        can_retry && error_code == 400 && m->send_error_message == CSlice("QUOTE_TEXT_INVALID");
    auto need_drop_reply =
        can_retry && error_code == 400 && m->send_error_message == CSlice("REPLY_MESSAGE_ID_INVALID");
    return td_api::make_object<td_api::messageSendingStateFailed>(
        td_api::make_object<td_api::error>(error_code, m->send_error_message), can_retry, need_another_sender,
        need_another_reply_quote, need_drop_reply, max(m->try_resend_at - Time::now(), 0.0));
  }
  return nullptr;
}

tl_object_ptr<td_api::MessageSchedulingState> MessagesManager::get_message_scheduling_state_object(int32 send_date) {
  if (send_date == SCHEDULE_WHEN_ONLINE_DATE) {
    return td_api::make_object<td_api::messageSchedulingStateSendWhenOnline>();
  }
  return td_api::make_object<td_api::messageSchedulingStateSendAtDate>(send_date);
}

td_api::object_ptr<td_api::MessageContent> MessagesManager::get_message_message_content_object(DialogId dialog_id,
                                                                                               const Message *m) const {
  auto live_location_date = m->is_failed_to_send ? 0 : m->date;
  return get_message_content_object(m->content.get(), td_, dialog_id, live_location_date, m->is_content_secret,
                                    need_skip_bot_commands(dialog_id, m), get_message_max_media_timestamp(m),
                                    m->invert_media, m->disable_web_page_preview);
}

td_api::object_ptr<td_api::message> MessagesManager::get_dialog_event_log_message_object(
    DialogId dialog_id, tl_object_ptr<telegram_api::Message> &&message, DialogId &sender_dialog_id) {
  auto dialog_message =
      create_message(parse_telegram_api_message(std::move(message), false, "get_dialog_event_log_message_object"),
                     dialog_id.get_type() == DialogType::Channel, "get_dialog_event_log_message_object");
  const Message *m = dialog_message.second.get();
  if (m == nullptr || dialog_message.first != dialog_id) {
    LOG(ERROR) << "Failed to create event log message in " << dialog_id;
    return nullptr;
  }
  sender_dialog_id = get_message_sender(m);

  auto sender = get_message_sender_object_const(td_, m->sender_user_id, m->sender_dialog_id,
                                                "get_dialog_event_log_message_object");
  auto forward_info =
      m->forward_info == nullptr ? nullptr : m->forward_info->get_message_forward_info_object(td_, false);
  auto import_info = m->forward_info == nullptr ? nullptr : m->forward_info->get_message_import_info_object();
  auto interaction_info = get_message_interaction_info_object(dialog_id, m);
  auto can_be_saved = can_save_message(dialog_id, m);
  auto via_bot_user_id = td_->contacts_manager_->get_user_id_object(m->via_bot_user_id, "via_bot_user_id");
  auto edit_date = m->hide_edit_date ? 0 : m->edit_date;
  auto reply_markup = get_reply_markup_object(td_->contacts_manager_.get(), m->reply_markup);
  auto content =
      get_message_content_object(m->content.get(), td_, dialog_id, 0, false, true,
                                 get_message_own_max_media_timestamp(m), m->invert_media, m->disable_web_page_preview);
  return td_api::make_object<td_api::message>(
      m->message_id.get(), std::move(sender), get_chat_id_object(dialog_id, "get_dialog_event_log_message_object"),
      nullptr, nullptr, m->is_outgoing, m->is_pinned, false, false, false, can_be_saved, false, false, false, false,
      false, false, false, false, false, true, m->is_channel_post, m->is_topic_message, false, m->date, edit_date,
      std::move(forward_info), std::move(import_info), std::move(interaction_info), Auto(), nullptr, 0, 0, nullptr, 0.0,
      0.0, via_bot_user_id, m->sender_boost_count, m->author_signature, 0,
      get_restriction_reason_description(m->restriction_reasons), std::move(content), std::move(reply_markup));
}

tl_object_ptr<td_api::message> MessagesManager::get_message_object(MessageFullId message_full_id, const char *source) {
  return get_message_object(message_full_id.get_dialog_id(), get_message_force(message_full_id, source), source);
}

tl_object_ptr<td_api::message> MessagesManager::get_message_object(DialogId dialog_id, const Message *m,
                                                                   const char *source) const {
  if (m == nullptr) {
    return nullptr;
  }
  LOG_CHECK(have_dialog(dialog_id)) << source;

  auto is_bot = td_->auth_manager_->is_bot();
  auto sending_state = get_message_sending_state_object(m);
  if (sending_state == nullptr || !is_bot) {
    m->is_update_sent = true;
  }
  bool can_delete = can_delete_message(dialog_id, m);
  bool is_scheduled = m->message_id.is_scheduled();
  DialogId my_dialog_id = td_->dialog_manager_->get_my_dialog_id();
  bool can_delete_for_self = false;
  bool can_delete_for_all_users = can_delete && can_revoke_message(dialog_id, m);
  if (can_delete) {
    switch (dialog_id.get_type()) {
      case DialogType::User:
      case DialogType::Chat:
        // TODO allow to delete yet unsent message just for self
        can_delete_for_self = !m->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();
    }
  }
  if (is_scheduled) {
    can_delete_for_self = (dialog_id == my_dialog_id);
    can_delete_for_all_users = !can_delete_for_self;
  }

  bool is_outgoing = m->is_outgoing;
  if (dialog_id == my_dialog_id) {
    // in Saved Messages all non-forwarded messages must be outgoing
    // a forwarded message is outgoing, only if it doesn't have from_dialog_id and its sender isn't hidden
    // i.e. a message is incoming only if it's a forwarded message with known from_dialog_id or with a hidden sender
    is_outgoing =
        is_scheduled || m->forward_info == nullptr ||
        (!m->forward_info->get_last_dialog_id().is_valid() && !m->forward_info->get_origin().is_sender_hidden());
  }

  double ttl_expires_in =
      m->ttl_expires_at != 0 ? clamp(m->ttl_expires_at - Time::now(), 1e-3, m->ttl.get_input_ttl() - 1e-3) : 0.0;
  double auto_delete_in =
      m->ttl_period == 0 ? 0.0 : clamp(m->date + m->ttl_period - G()->server_time(), 1e-3, m->ttl_period - 1e-3);
  auto sender = get_message_sender_object_const(td_, m->sender_user_id, m->sender_dialog_id, source);
  auto scheduling_state = is_scheduled ? get_message_scheduling_state_object(m->date) : nullptr;
  auto forward_info = m->forward_info == nullptr
                          ? nullptr
                          : m->forward_info->get_message_forward_info_object(td_, dialog_id == my_dialog_id);
  auto import_info = m->forward_info == nullptr ? nullptr : m->forward_info->get_message_import_info_object();
  auto interaction_info = is_bot ? nullptr : get_message_interaction_info_object(dialog_id, m);
  auto unread_reactions = get_unread_reactions_object(dialog_id, m);
  auto can_be_saved = can_save_message(dialog_id, m);
  auto can_be_edited = can_edit_message(dialog_id, m, false, is_bot);
  auto can_be_forwarded = can_be_saved && can_forward_message(dialog_id, m);
  auto can_be_replied_in_another_chat = can_be_forwarded && m->message_id.is_server();
  auto can_get_added_reactions = m->reactions != nullptr && m->reactions->can_get_added_reactions_;
  auto can_get_statistics = can_get_message_statistics(dialog_id, m);
  auto can_get_message_thread = get_top_thread_message_full_id(dialog_id, m, false).is_ok();
  auto can_get_read_date = can_get_message_read_date(dialog_id, m).is_ok();
  auto can_get_viewers = can_get_message_viewers(dialog_id, m).is_ok();
  auto can_get_media_timestamp_links = can_get_media_timestamp_link(dialog_id, m).is_ok();
  auto can_report_reactions = can_report_message_reactions(dialog_id, m);
  auto via_bot_user_id = td_->contacts_manager_->get_user_id_object(m->via_bot_user_id, "via_bot_user_id");
  auto reply_to = [&]() -> td_api::object_ptr<td_api::MessageReplyTo> {
    if (!m->replied_message_info.is_empty()) {
      if (!is_bot && m->is_topic_message &&
          m->replied_message_info.get_same_chat_reply_to_message_id(false) == m->top_thread_message_id) {
        return nullptr;
      }
      return m->replied_message_info.get_message_reply_to_message_object(td_, dialog_id);
    }
    if (m->reply_to_story_full_id.is_valid()) {
      return td_api::make_object<td_api::messageReplyToStory>(
          get_chat_id_object(m->reply_to_story_full_id.get_dialog_id(), "messageReplyToStory"),
          m->reply_to_story_full_id.get_story_id().get());
    }
    return nullptr;
  }();
  auto top_thread_message_id = m->top_thread_message_id.get();
  auto date = is_scheduled ? 0 : m->date;
  auto edit_date = m->hide_edit_date ? 0 : m->edit_date;
  auto has_timestamped_media = reply_to == nullptr || m->max_own_media_timestamp >= 0;
  auto reply_markup = get_reply_markup_object(td_->contacts_manager_.get(), m->reply_markup);
  auto content = get_message_message_content_object(dialog_id, m);
  auto self_destruct_type = m->ttl.get_message_self_destruct_type_object();

  return td_api::make_object<td_api::message>(
      m->message_id.get(), std::move(sender), get_chat_id_object(dialog_id, "get_message_object"),
      std::move(sending_state), std::move(scheduling_state), is_outgoing, m->is_pinned, can_be_edited, can_be_forwarded,
      can_be_replied_in_another_chat, can_be_saved, can_delete_for_self, can_delete_for_all_users,
      can_get_added_reactions, can_get_statistics, can_get_message_thread, can_get_read_date, can_get_viewers,
      can_get_media_timestamp_links, can_report_reactions, has_timestamped_media, m->is_channel_post,
      m->is_topic_message, m->contains_unread_mention, date, edit_date, std::move(forward_info), std::move(import_info),
      std::move(interaction_info), std::move(unread_reactions), std::move(reply_to), top_thread_message_id,
      td_->saved_messages_manager_->get_saved_messages_topic_id_object(m->saved_messages_topic_id),
      std::move(self_destruct_type), ttl_expires_in, auto_delete_in, via_bot_user_id, m->sender_boost_count,
      m->author_signature, m->media_album_id, get_restriction_reason_description(m->restriction_reasons),
      std::move(content), std::move(reply_markup));
}

tl_object_ptr<td_api::messages> MessagesManager::get_messages_object(int32 total_count, DialogId dialog_id,
                                                                     const vector<MessageId> &message_ids,
                                                                     bool skip_not_found, const char *source) {
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  auto message_objects = transform(message_ids, [this, dialog_id, d, source](MessageId message_id) {
    return get_message_object(dialog_id, get_message_force(d, message_id, source), source);
  });
  return get_messages_object(total_count, std::move(message_objects), skip_not_found);
}

tl_object_ptr<td_api::messages> MessagesManager::get_messages_object(int32 total_count,
                                                                     const vector<MessageFullId> &message_full_ids,
                                                                     bool skip_not_found, const char *source) {
  auto message_objects = transform(message_full_ids, [this, source](MessageFullId message_full_id) {
    return get_message_object(message_full_id, source);
  });
  return get_messages_object(total_count, std::move(message_objects), skip_not_found);
}

tl_object_ptr<td_api::messages> MessagesManager::get_messages_object(int32 total_count,
                                                                     vector<tl_object_ptr<td_api::message>> &&messages,
                                                                     bool skip_not_found) {
  auto message_count = narrow_cast<int32>(messages.size());
  if (total_count < message_count) {
    if (total_count != -1) {
      LOG(ERROR) << "Have wrong total_count = " << total_count << ", while having " << message_count << " messages";
    }
    total_count = message_count;
  }
  if (skip_not_found && td::remove(messages, nullptr)) {
    total_count -= message_count - static_cast<int32>(messages.size());
  }
  return td_api::make_object<td_api::messages>(total_count, std::move(messages));
}

bool MessagesManager::get_dialog_silent_send_message(DialogId dialog_id) const {
  auto *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  return d->notification_settings.silent_send_message;
}

bool MessagesManager::get_dialog_has_last_message(DialogId dialog_id) const {
  const auto *d = get_dialog(dialog_id);
  return d != nullptr && d->last_message_id.is_valid();
}

DialogId MessagesManager::get_dialog_default_send_message_as_dialog_id(DialogId dialog_id) const {
  auto *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  return d->default_send_message_as_dialog_id;
}

MessageInputReplyTo MessagesManager::get_message_input_reply_to(
    DialogId dialog_id, MessageId top_thread_message_id, td_api::object_ptr<td_api::InputMessageReplyTo> &&reply_to,
    bool for_draft) {
  return get_message_input_reply_to(get_dialog(dialog_id), top_thread_message_id, std::move(reply_to), for_draft);
}

int64 MessagesManager::generate_new_random_id(const Dialog *d) {
  int64 random_id;
  do {
    random_id = Random::secure_int64();
  } while (random_id == 0 || being_sent_messages_.count(random_id) > 0 ||
           d->random_id_to_message_id.count(random_id) > 0);
  return random_id;
}

unique_ptr<MessagesManager::Message> MessagesManager::create_message_to_send(
    Dialog *d, MessageId top_thread_message_id, MessageInputReplyTo &&input_reply_to, const MessageSendOptions &options,
    unique_ptr<MessageContent> &&content, bool invert_media, bool suppress_reply_info,
    unique_ptr<MessageForwardInfo> forward_info, DialogId real_forward_from_dialog_id, bool is_copy,
    DialogId send_as_dialog_id) const {
  CHECK(d != nullptr);
  CHECK(content != nullptr);

  bool is_scheduled = options.schedule_date != 0;
  DialogId dialog_id = d->dialog_id;

  auto dialog_type = dialog_id.get_type();
  auto my_id = td_->contacts_manager_->get_my_id();

  int64 reply_to_random_id = 0;
  bool is_topic_message = false;
  auto initial_top_thread_message_id = top_thread_message_id;
  auto same_chat_reply_to_message_id = input_reply_to.get_same_chat_reply_to_message_id();
  if (same_chat_reply_to_message_id.is_valid() || same_chat_reply_to_message_id.is_valid_scheduled()) {
    // the message was forcely preloaded in get_message_input_reply_to
    // it can be missing, only if it is unknown message from a push notification, or an unknown top thread message
    const Message *reply_m = get_message(d, same_chat_reply_to_message_id);
    if (reply_m != nullptr && !same_chat_reply_to_message_id.is_scheduled()) {
      if (reply_m->top_thread_message_id.is_valid()) {
        top_thread_message_id = reply_m->top_thread_message_id;
      }
      is_topic_message = reply_m->is_topic_message;
    }
    if (dialog_type == DialogType::SecretChat || same_chat_reply_to_message_id.is_yet_unsent()) {
      if (reply_m != nullptr) {
        reply_to_random_id = reply_m->random_id;
      } else {
        CHECK(dialog_type == DialogType::SecretChat);
        CHECK(top_thread_message_id == MessageId());
        input_reply_to = MessageInputReplyTo();
      }
    }
  }
  if (top_thread_message_id.is_valid()) {
    const Message *top_m = get_message(d, top_thread_message_id);
    if (top_m != nullptr) {
      is_topic_message = top_m->is_topic_message;
    }
  }

  auto message = make_unique<Message>();
  auto *m = message.get();
  bool is_channel_post = td_->dialog_manager_->is_broadcast_channel(dialog_id);
  if (is_channel_post) {
    // sender of the post can be hidden
    if (!is_scheduled && td_->contacts_manager_->get_channel_sign_messages(dialog_id.get_channel_id())) {
      m->author_signature = td_->contacts_manager_->get_user_title(my_id);
    }
    m->sender_dialog_id = dialog_id;
  } else {
    if (send_as_dialog_id.is_valid()) {
      if (send_as_dialog_id.get_type() == DialogType::User) {
        m->sender_user_id = send_as_dialog_id.get_user_id();
      } else {
        m->sender_dialog_id = send_as_dialog_id;
      }
    } else if (d->default_send_message_as_dialog_id.is_valid()) {
      if (d->default_send_message_as_dialog_id.get_type() == DialogType::User) {
        m->sender_user_id = my_id;
      } else {
        m->sender_dialog_id = d->default_send_message_as_dialog_id;
      }
      m->has_explicit_sender = true;
    } else {
      if (td_->dialog_manager_->is_anonymous_administrator(dialog_id, &m->author_signature)) {
        m->sender_dialog_id = dialog_id;
      } else {
        m->sender_user_id = my_id;
      }
    }
  }
  m->message_id = options.schedule_date != 0 ? get_next_yet_unsent_scheduled_message_id(d, options.schedule_date)
                                             : get_next_yet_unsent_message_id(d);
  m->send_date = G()->unix_time();
  m->date = is_scheduled ? options.schedule_date : m->send_date;
  m->replied_message_info = RepliedMessageInfo(td_, input_reply_to);
  m->reply_to_story_full_id = input_reply_to.get_story_full_id();
  m->input_reply_to = std::move(input_reply_to);
  m->reply_to_random_id = reply_to_random_id;
  m->top_thread_message_id = top_thread_message_id;
  m->initial_top_thread_message_id = initial_top_thread_message_id;
  m->is_topic_message = is_topic_message;
  m->is_channel_post = is_channel_post;
  m->is_outgoing = is_scheduled || dialog_id != DialogId(my_id);
  m->from_background = options.from_background;
  m->update_stickersets_order = options.update_stickersets_order;
  m->noforwards = options.protect_content;
  m->view_count = is_channel_post && !is_scheduled ? 1 : 0;
  m->forward_count = 0;
  if ([&] {
        if (suppress_reply_info) {
          return false;
        }
        if (is_scheduled) {
          return false;
        }
        if (dialog_type != DialogType::Channel) {
          return false;
        }
        if (td_->auth_manager_->is_bot()) {
          return false;
        }
        if (is_channel_post) {
          return td_->contacts_manager_->get_channel_has_linked_channel(dialog_id.get_channel_id());
        }
        return !m->input_reply_to.is_valid();
      }()) {
    m->reply_info.reply_count_ = 0;
    if (is_channel_post) {
      auto linked_channel_id =
          td_->contacts_manager_->get_channel_linked_channel_id(dialog_id.get_channel_id(), "create_message_to_send");
      if (linked_channel_id.is_valid()) {
        m->reply_info.is_comment_ = true;
        m->reply_info.channel_id_ = linked_channel_id;
      }
    }
  }
  if (m->sender_user_id == my_id && dialog_type == DialogType::Channel) {
    m->sender_boost_count = td_->contacts_manager_->get_channel_my_boost_count(dialog_id.get_channel_id());
  }
  m->content = std::move(content);
  m->invert_media = invert_media;
  m->forward_info = std::move(forward_info);
  m->real_forward_from_dialog_id = real_forward_from_dialog_id;
  m->is_copy = is_copy || m->forward_info != nullptr;
  m->sending_id = options.sending_id;

  if (td_->auth_manager_->is_bot() || options.disable_notification ||
      td_->option_manager_->get_option_boolean("ignore_default_disable_notification")) {
    m->disable_notification = options.disable_notification;
  } else {
    m->disable_notification = d->notification_settings.silent_send_message;
  }

  if (dialog_type == DialogType::SecretChat) {
    CHECK(!is_scheduled);
    if (is_service_message_content(m->content->get_type())) {
      m->ttl = {};
    } else {
      m->ttl =
          MessageSelfDestructType(td_->contacts_manager_->get_secret_chat_ttl(dialog_id.get_secret_chat_id()), false);
    }
    m->is_content_secret = m->ttl.is_secret_message_content(m->content->get_type());
  }
  if (dialog_id == DialogId(my_id)) {
    m->saved_messages_topic_id = SavedMessagesTopicId(dialog_id, m->forward_info.get(), m->real_forward_from_dialog_id);
  }

  return message;
}

MessagesManager::Message *MessagesManager::get_message_to_send(
    Dialog *d, MessageId top_thread_message_id, MessageInputReplyTo &&input_reply_to, const MessageSendOptions &options,
    unique_ptr<MessageContent> &&content, bool invert_media, bool *need_update_dialog_pos, bool suppress_reply_info,
    unique_ptr<MessageForwardInfo> forward_info, DialogId real_forward_from_dialog_id, bool is_copy,
    DialogId send_as_dialog_id) {
  d->was_opened = true;

  auto message = create_message_to_send(d, top_thread_message_id, std::move(input_reply_to), options,
                                        std::move(content), invert_media, suppress_reply_info, std::move(forward_info),
                                        real_forward_from_dialog_id, is_copy, send_as_dialog_id);

  auto message_id = message->message_id;
  message->random_id = generate_new_random_id(d);

  bool need_update = false;
  CHECK(td_->dialog_manager_->have_input_peer(d->dialog_id, AccessRights::Read));
  auto result =
      add_message_to_dialog(d, std::move(message), false, true, &need_update, need_update_dialog_pos, "send message");
  LOG_CHECK(result != nullptr) << message_id << " " << debug_add_message_to_dialog_fail_reason_;
  if (result->message_id.is_scheduled()) {
    send_update_chat_has_scheduled_messages(d, false);
  }
  if (options.update_stickersets_order && !td_->auth_manager_->is_bot()) {
    move_message_content_sticker_set_to_top(td_, result->content.get());
  }
  return result;
}

int64 MessagesManager::begin_send_message(DialogId dialog_id, const Message *m) {
  LOG(INFO) << "Begin to send " << MessageFullId(dialog_id, m->message_id) << " with random_id = " << m->random_id;
  CHECK(m->random_id != 0);
  CHECK(m->message_id.is_yet_unsent());
  bool is_inserted = being_sent_messages_.emplace(m->random_id, MessageFullId(dialog_id, m->message_id)).second;
  CHECK(is_inserted);
  return m->random_id;
}

Status MessagesManager::can_send_message(DialogId dialog_id) const {
  if (!td_->dialog_manager_->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_permissions(channel_id);

    switch (channel_type) {
      case ChannelType::Unknown:
      case ChannelType::Megagroup:
        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();
}

MessageId MessagesManager::get_persistent_message_id(const Dialog *d, MessageId message_id) const {
  if (!message_id.is_valid() && !message_id.is_valid_scheduled()) {
    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 identifier
    // we need to use real message in this case and transparently replace message_id
    auto it = yet_unsent_message_full_id_to_persistent_message_id_.find({d->dialog_id, message_id});
    if (it != yet_unsent_message_full_id_to_persistent_message_id_.end()) {
      return it->second;
    }
  }

  return message_id;
}

MessageInputReplyTo MessagesManager::get_message_input_reply_to(
    Dialog *d, MessageId top_thread_message_id, td_api::object_ptr<td_api::InputMessageReplyTo> &&reply_to,
    bool for_draft) {
  CHECK(d != nullptr);
  if (top_thread_message_id.is_valid() &&
      !have_message_force(d, top_thread_message_id, "get_message_input_reply_to 1")) {
    LOG(INFO) << "Have reply in the thread of unknown " << top_thread_message_id;
  }
  if (reply_to == nullptr) {
    if (!for_draft && top_thread_message_id.is_valid() && top_thread_message_id.is_server()) {
      return MessageInputReplyTo{top_thread_message_id, DialogId(), FormattedText(), 0};
    }
    return {};
  }
  switch (reply_to->get_id()) {
    case td_api::inputMessageReplyToStory::ID: {
      if (for_draft) {
        return {};
      }
      auto reply_to_story = td_api::move_object_as<td_api::inputMessageReplyToStory>(reply_to);
      auto story_id = StoryId(reply_to_story->story_id_);
      auto sender_dialog_id = DialogId(reply_to_story->story_sender_chat_id_);
      if (d->dialog_id != sender_dialog_id || td_->dialog_manager_->is_broadcast_channel(sender_dialog_id)) {
        LOG(INFO) << "Ignore reply to story from " << sender_dialog_id << " in a wrong " << d->dialog_id;
        return {};
      }
      if (!story_id.is_server()) {
        LOG(INFO) << "Ignore reply to invalid " << story_id;
        return {};
      }
      return MessageInputReplyTo{StoryFullId(sender_dialog_id, story_id)};
    }
    case td_api::inputMessageReplyToMessage::ID: {
      auto reply_to_message = td_api::move_object_as<td_api::inputMessageReplyToMessage>(reply_to);
      auto message_id = MessageId(reply_to_message->message_id_);
      if (!message_id.is_valid()) {
        if (!for_draft && message_id == MessageId() && top_thread_message_id.is_valid() &&
            top_thread_message_id.is_server()) {
          return MessageInputReplyTo{top_thread_message_id, DialogId(), FormattedText(), 0};
        }
        return {};
      }
      auto *reply_d = d;
      auto reply_dialog_id = DialogId(reply_to_message->chat_id_);
      if (reply_dialog_id != DialogId()) {
        reply_d = get_dialog_force(reply_dialog_id, "get_message_input_reply_to");
        if (reply_d == nullptr) {
          return {};
        }
        if (d->dialog_id.get_type() == DialogType::SecretChat) {
          return {};
        }
      }
      message_id = get_persistent_message_id(reply_d, message_id);
      if (message_id == MessageId(ServerMessageId(1)) && reply_d->dialog_id.get_type() == DialogType::Channel) {
        return {};
      }
      FormattedText quote;
      int32 quote_position = 0;
      if (reply_to_message->quote_ != nullptr) {
        int32 ltrim_count = 0;
        auto r_quote = get_formatted_text(td_, td_->dialog_manager_->get_my_dialog_id(),
                                          std::move(reply_to_message->quote_->text_), td_->auth_manager_->is_bot(),
                                          true, true, false, &ltrim_count);
        if (r_quote.is_ok() && d->dialog_id.get_type() != DialogType::SecretChat && !r_quote.ok().text.empty()) {
          quote = r_quote.move_as_ok();
          quote_position = reply_to_message->quote_->position_;
          if (0 <= quote_position && quote_position <= 1000000) {  // some unreasonably big bound
            quote_position += ltrim_count;
          } else {
            quote_position = 0;
          }
        }
      }
      const Message *m = get_message_force(reply_d, message_id, "get_message_input_reply_to 2");
      if (m == nullptr || m->message_id.is_yet_unsent() ||
          (m->message_id.is_local() && reply_d->dialog_id.get_type() != DialogType::SecretChat)) {
        if (message_id.is_server() && reply_d->dialog_id.get_type() != DialogType::SecretChat &&
            reply_dialog_id == DialogId() && message_id > reply_d->last_new_message_id &&
            (reply_d->notification_info != nullptr &&
             message_id <= reply_d->notification_info->max_push_notification_message_id_)) {
          // allow to reply yet unreceived server message in the same chat
          return MessageInputReplyTo{message_id, reply_dialog_id, std::move(quote), quote_position};
        }
        if (!for_draft && top_thread_message_id.is_valid() && top_thread_message_id.is_server()) {
          return MessageInputReplyTo{top_thread_message_id, DialogId(), FormattedText(), 0};
        }
        LOG(INFO) << "Can't find " << message_id << " in " << reply_d->dialog_id;

        // 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 {};
      }
      if (reply_dialog_id != DialogId() && (!can_forward_message(reply_dialog_id, m) || !m->message_id.is_server())) {
        LOG(INFO) << "Can't reply in another chat " << m->message_id << " in " << reply_d->dialog_id;
        return {};
      }
      return MessageInputReplyTo{m->message_id, reply_dialog_id, std::move(quote), quote_position};
    }
    default:
      UNREACHABLE();
      return {};
  }
}

MessagesManager::ForwardedMessageInfo MessagesManager::get_forwarded_message_info(MessageFullId message_full_id) {
  ForwardedMessageInfo result;
  auto *m = get_message_force(message_full_id, "get_forwarded_message_info");
  if (m == nullptr || m->message_id.is_scheduled()) {
    return result;
  }
  auto dialog_id = message_full_id.get_dialog_id();
  result.origin_date_ = get_message_original_date(m);
  result.origin_ = get_forwarded_message_origin(dialog_id, m);
  result.content_ = dup_message_content(td_, td_->dialog_manager_->get_my_dialog_id(), m->content.get(),
                                        MessageContentDupType::Forward, MessageCopyOptions());
  return result;
}

const MessageInputReplyTo *MessagesManager::get_message_input_reply_to(const Message *m) {
  CHECK(m != nullptr);
  CHECK(!m->message_id.is_any_server());
  return &m->input_reply_to;
}

vector<FileId> MessagesManager::get_message_file_ids(const Message *m) const {
  CHECK(m != nullptr);
  auto file_ids = get_message_content_file_ids(m->content.get(), td_);
  if (!m->replied_message_info.is_empty()) {
    append(file_ids, m->replied_message_info.get_file_ids(td_));
  }
  return file_ids;
}

void MessagesManager::cancel_upload_message_content_files(const MessageContent *content) {
  auto file_id = get_message_content_upload_file_id(content);
  // always cancel file upload, it should be a no-op in the worst case
  if (being_uploaded_files_.erase(file_id) || file_id.is_valid()) {
    cancel_upload_file(file_id, "cancel_upload_message_content_files");
  }
  file_id = get_message_content_thumbnail_file_id(content, td_);
  if (being_uploaded_thumbnails_.erase(file_id) || file_id.is_valid()) {
    cancel_upload_file(file_id, "cancel_upload_message_content_files");
  }
}

void MessagesManager::cancel_upload_file(FileId file_id, const char *source) {
  // send the request later so they doesn't interfere with other actions
  // for example merge, supposed to happen soon, can auto-cancel the upload
  LOG(INFO) << "Cancel upload of file " << file_id << " from " << source;
  send_closure_later(G()->file_manager(), &FileManager::cancel_upload, file_id);
}

void MessagesManager::cancel_send_message_query(DialogId dialog_id, Message *m) {
  CHECK(m != nullptr);
  CHECK(m->content != nullptr);
  CHECK(m->message_id.is_valid() || m->message_id.is_valid_scheduled());
  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_log_event_id != 0) {
    LOG(INFO) << "Delete send message log event for " << m->message_id;
    binlog_erase(G()->td_db()->get_binlog(), m->send_message_log_event_id);
    m->send_message_log_event_id = 0;
  }

  update_replied_by_message_count(dialog_id, m, false);
  {
    auto it = replied_yet_unsent_messages_.find({dialog_id, m->message_id});
    if (it != replied_yet_unsent_messages_.end()) {
      for (auto message_full_id : it->second) {
        auto reply_d = get_dialog(message_full_id.get_dialog_id());
        CHECK(reply_d != nullptr);
        auto replied_m = get_message(reply_d, message_full_id.get_message_id());
        CHECK(replied_m != nullptr);
        const auto *input_reply_to = get_message_input_reply_to(replied_m);
        CHECK(input_reply_to != nullptr);
        CHECK(input_reply_to->get_reply_message_full_id(reply_d->dialog_id) == MessageFullId(dialog_id, m->message_id));
        set_message_reply(reply_d, replied_m,
                          MessageInputReplyTo{replied_m->top_thread_message_id, DialogId(), FormattedText(), 0}, true);
      }
      replied_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 (!m->message_id.is_scheduled() && G()->keep_media_order() && !m->is_copy) {
    auto queue_id = ChainId(dialog_id, m->content->get_type()).get();
    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.queue_;
        LOG(INFO) << "Delete " << m->message_id << " from queue " << queue_id;
        if (queue.erase(m->message_id) != 0) {
          if (queue.empty()) {
            yet_unsent_media_queues_.erase(queue_it);
          } else {
            // send later, because delete_all_dialog_messages can be called right now
            send_closure_later(actor_id(this), &MessagesManager::on_yet_unsent_media_queue_updated, dialog_id);
          }
        }
      }
    }
  }
}

void MessagesManager::cancel_send_deleted_message(DialogId dialog_id, Message *m, bool is_permanently_deleted) {
  CHECK(m != nullptr);
  if (m->message_id.is_yet_unsent()) {
    cancel_send_message_query(dialog_id, m);
  } else if (is_permanently_deleted || !m->message_id.is_scheduled()) {
    cancel_edit_message_media(dialog_id, m, "Message was deleted");
  }
}

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) &&
          !td_->contacts_manager_->is_user_support(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 && td_->dialog_manager_->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, const Message *m) {
  auto is_bot = td_->auth_manager_->is_bot();
  dependencies.add(m->sender_user_id);
  dependencies.add_dialog_and_dependencies(m->sender_dialog_id);
  m->saved_messages_topic_id.add_dependencies(dependencies);
  m->replied_message_info.add_dependencies(dependencies, is_bot);
  dependencies.add_dialog_and_dependencies(m->reply_to_story_full_id.get_dialog_id());
  dependencies.add_dialog_and_dependencies(m->real_forward_from_dialog_id);
  dependencies.add(m->via_bot_user_id);
  if (m->forward_info != nullptr) {
    m->forward_info->add_dependencies(dependencies);
  }
  for (const auto &replier_min_channel : m->reply_info.replier_min_channels_) {
    LOG(INFO) << "Add min replied " << replier_min_channel.first;
    td_->contacts_manager_->add_min_channel(replier_min_channel.first, replier_min_channel.second);
  }
  for (auto recent_replier_dialog_id : m->reply_info.recent_replier_dialog_ids_) {
    dependencies.add_message_sender_dependencies(recent_replier_dialog_id);
  }
  if (m->reactions != nullptr) {
    m->reactions->add_min_channels(td_);
    m->reactions->add_dependencies(dependencies);
  }
  add_message_content_dependencies(dependencies, m->content.get(), is_bot);
  add_reply_markup_dependencies(dependencies, m->reply_markup.get());
  add_draft_message_dependencies(dependencies, m->thread_draft_message);
}

void MessagesManager::get_dialog_send_message_as_dialog_ids(
    DialogId dialog_id, Promise<td_api::object_ptr<td_api::chatMessageSenders>> &&promise, bool is_recursive) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  const Dialog *d = get_dialog_force(dialog_id, "get_group_call_join_as");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Can't access chat"));
  }
  if (!d->default_send_message_as_dialog_id.is_valid()) {
    return promise.set_value(td_api::make_object<td_api::chatMessageSenders>());
  }
  CHECK(dialog_id.get_type() == DialogType::Channel);

  if (created_public_broadcasts_inited_) {
    auto senders = td_api::make_object<td_api::chatMessageSenders>();
    if (!created_public_broadcasts_.empty()) {
      auto add_sender = [&senders, td = td_](DialogId dialog_id, bool needs_premium) {
        auto sender = get_message_sender_object(td, dialog_id, "add_sender");
        senders->senders_.push_back(td_api::make_object<td_api::chatMessageSender>(std::move(sender), needs_premium));
      };
      if (td_->dialog_manager_->is_anonymous_administrator(dialog_id, nullptr)) {
        add_sender(dialog_id, false);
      } else {
        add_sender(td_->dialog_manager_->get_my_dialog_id(), false);
      }

      struct Sender {
        ChannelId channel_id;
        bool needs_premium;
      };
      std::multimap<int64, Sender> sorted_senders;

      bool is_premium = td_->option_manager_->get_option_boolean("is_premium");
      auto linked_channel_id = td_->contacts_manager_->get_channel_linked_channel_id(
          dialog_id.get_channel_id(), "get_dialog_send_message_as_dialog_ids");
      for (auto channel_id : created_public_broadcasts_) {
        int64 score = td_->contacts_manager_->get_channel_participant_count(channel_id);
        bool needs_premium = !is_premium && channel_id != linked_channel_id &&
                             !td_->contacts_manager_->get_channel_is_verified(channel_id);
        if (needs_premium) {
          score -= static_cast<int64>(1) << 40;
        }
        if (channel_id == linked_channel_id) {
          score += static_cast<int64>(1) << 32;
        }
        sorted_senders.emplace(-score, Sender{channel_id, needs_premium});
      };

      for (auto &sender : sorted_senders) {
        add_sender(DialogId(sender.second.channel_id), sender.second.needs_premium);
      }
    }
    return promise.set_value(std::move(senders));
  }

  CHECK(!is_recursive);
  auto new_promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, promise = std::move(promise)](
                                                Result<td_api::object_ptr<td_api::chats>> &&result) mutable {
    if (result.is_error()) {
      promise.set_error(result.move_as_error());
    } else {
      send_closure_later(actor_id, &MessagesManager::get_dialog_send_message_as_dialog_ids, dialog_id,
                         std::move(promise), true);
    }
  });
  td_->contacts_manager_->get_created_public_dialogs(PublicDialogType::HasUsername, std::move(new_promise), true);
}

void MessagesManager::set_dialog_default_send_message_as_dialog_id(DialogId dialog_id,
                                                                   DialogId message_sender_dialog_id,
                                                                   Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(dialog_id, "set_dialog_default_send_message_as_dialog_id");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  if (!d->default_send_message_as_dialog_id.is_valid()) {
    return promise.set_error(Status::Error(400, "Can't change message sender in the chat"));
  }
  // checked in on_update_dialog_default_send_message_as_dialog_id
  CHECK(dialog_id.get_type() == DialogType::Channel && !td_->dialog_manager_->is_broadcast_channel(dialog_id));
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  bool is_anonymous = td_->dialog_manager_->is_anonymous_administrator(dialog_id, nullptr);
  switch (message_sender_dialog_id.get_type()) {
    case DialogType::User:
      if (message_sender_dialog_id != td_->dialog_manager_->get_my_dialog_id()) {
        return promise.set_error(Status::Error(400, "Can't send messages as another user"));
      }
      if (is_anonymous) {
        return promise.set_error(Status::Error(400, "Can't send messages as self"));
      }
      break;
    case DialogType::Chat:
    case DialogType::Channel:
    case DialogType::SecretChat:
      if (is_anonymous && dialog_id == message_sender_dialog_id) {
        break;
      }
      if (!td_->dialog_manager_->is_broadcast_channel(message_sender_dialog_id) ||
          td_->contacts_manager_->get_channel_first_username(message_sender_dialog_id.get_channel_id()).empty()) {
        return promise.set_error(Status::Error(400, "Message sender chat must be a public channel"));
      }
      break;
    default:
      return promise.set_error(Status::Error(400, "Invalid message sender specified"));
  }
  if (!td_->dialog_manager_->have_input_peer(message_sender_dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Can't access specified message sender chat"));
  }

  td_->dialog_action_manager_->cancel_send_dialog_action_queries(dialog_id);

  td_->create_handler<SaveDefaultSendAsQuery>(std::move(promise))->send(dialog_id, message_sender_dialog_id);

  on_update_dialog_default_send_message_as_dialog_id(dialog_id, message_sender_dialog_id, true);
}

class MessagesManager::SendMessageLogEvent {
 public:
  DialogId dialog_id;
  const Message *m_in;
  unique_ptr<Message> message_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);
    td::parse(message_out, parser);
  }
};

Result<td_api::object_ptr<td_api::message>> MessagesManager::send_message(
    DialogId dialog_id, const MessageId top_thread_message_id,
    td_api::object_ptr<td_api::InputMessageReplyTo> &&reply_to, tl_object_ptr<td_api::messageSendOptions> &&options,
    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(400, "Can't send message without content");
  }

  Dialog *d = get_dialog_force(dialog_id, "send_message");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }

  auto input_reply_to = get_message_input_reply_to(d, top_thread_message_id, std::move(reply_to), false);

  if (input_message_content->get_id() == td_api::inputMessageForwarded::ID) {
    auto input_message = td_api::move_object_as<td_api::inputMessageForwarded>(input_message_content);
    TRY_RESULT(copy_options, process_message_copy_options(dialog_id, std::move(input_message->copy_options_)));
    copy_options.input_reply_to = std::move(input_reply_to);
    TRY_RESULT_ASSIGN(copy_options.reply_markup, get_dialog_reply_markup(dialog_id, std::move(reply_markup)));
    return forward_message(dialog_id, top_thread_message_id, DialogId(input_message->from_chat_id_),
                           MessageId(input_message->message_id_), std::move(options), input_message->in_game_share_,
                           std::move(copy_options));
  }

  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)));
  TRY_RESULT(message_send_options, process_message_send_options(dialog_id, std::move(options), true));
  TRY_STATUS(can_use_message_send_options(message_send_options, message_content));
  TRY_STATUS(can_use_top_thread_message_id(d, top_thread_message_id, input_reply_to));

  // there must be no errors after get_message_to_send call

  auto content = dup_message_content(td_, dialog_id, message_content.content.get(), MessageContentDupType::Send,
                                     MessageCopyOptions());
  bool need_update_dialog_pos = false;
  unique_ptr<Message> message;
  Message *m;
  if (message_send_options.only_preview) {
    message = create_message_to_send(d, top_thread_message_id, std::move(input_reply_to), message_send_options,
                                     std::move(content), message_content.invert_media, false, nullptr, DialogId(),
                                     message_content.via_bot_user_id.is_valid(), DialogId());
    m = message.get();
  } else {
    m = get_message_to_send(d, top_thread_message_id, std::move(input_reply_to), message_send_options,
                            std::move(content), message_content.invert_media, &need_update_dialog_pos, false, nullptr,
                            DialogId(), message_content.via_bot_user_id.is_valid());
  }
  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.is_valid()) {
    m->ttl = message_content.ttl;
    m->is_content_secret = m->ttl.is_secret_message_content(m->content->get_type());
  }
  m->send_emoji = std::move(message_content.emoji);

  if (message_send_options.only_preview) {
    return get_message_object(dialog_id, m, "send_message");
  }

  clear_dialog_draft_by_sent_message(d, m, !need_update_dialog_pos);

  save_send_message_log_event(dialog_id, m);
  do_send_message(dialog_id, m);

  if (!td_->auth_manager_->is_bot()) {
    send_update_new_message(d, m);
    if (need_update_dialog_pos) {
      send_update_chat_last_message(d, "send_message");
    }
  }

  return get_message_object(dialog_id, m, "send_message");
}

Result<InputMessageContent> MessagesManager::process_input_message_content(
    DialogId dialog_id, tl_object_ptr<td_api::InputMessageContent> &&input_message_content, bool check_permissions) {
  if (input_message_content != nullptr && input_message_content->get_id() == td_api::inputMessageForwarded::ID) {
    // for sendMessageAlbum/editMessageMedia/addLocalMessage
    auto input_message = td_api::move_object_as<td_api::inputMessageForwarded>(input_message_content);
    TRY_RESULT(copy_options, process_message_copy_options(dialog_id, std::move(input_message->copy_options_)));
    if (!copy_options.send_copy) {
      return Status::Error(400, "Can't use forwarded message");
    }

    DialogId from_dialog_id(input_message->from_chat_id_);
    Dialog *from_dialog = get_dialog_force(from_dialog_id, "send_message copy");
    if (from_dialog == nullptr) {
      return Status::Error(400, "Chat to copy message from not found");
    }
    if (!td_->dialog_manager_->have_input_peer(from_dialog_id, AccessRights::Read)) {
      return Status::Error(400, "Can't access the chat to copy message from");
    }
    if (from_dialog_id.get_type() == DialogType::SecretChat) {
      return Status::Error(400, "Can't copy message from secret chats");
    }
    MessageId message_id = get_persistent_message_id(from_dialog, MessageId(input_message->message_id_));

    const Message *copied_message = get_message_force(from_dialog, message_id, "process_input_message_content");
    if (copied_message == nullptr) {
      return Status::Error(400, "Can't find message to copy");
    }
    if (!can_forward_message(from_dialog_id, copied_message)) {
      return Status::Error(400, "Can't copy message");
    }
    if (!can_save_message(from_dialog_id, copied_message) && !td_->auth_manager_->is_bot()) {
      return Status::Error(400, "Message copying is restricted");
    }

    unique_ptr<MessageContent> content = dup_message_content(td_, dialog_id, copied_message->content.get(),
                                                             MessageContentDupType::Copy, std::move(copy_options));
    if (content == nullptr) {
      return Status::Error(400, "Can't copy message content");
    }

    return InputMessageContent(std::move(content), get_message_disable_web_page_preview(copied_message),
                               copied_message->invert_media, false, MessageSelfDestructType(), UserId(),
                               copied_message->send_emoji);
  }

  bool is_premium = td_->option_manager_->get_option_boolean("is_premium");
  TRY_RESULT(content, get_input_message_content(dialog_id, std::move(input_message_content), td_, is_premium));

  if (dialog_id != DialogId()) {
    TRY_STATUS(can_send_message_content(dialog_id, content.content.get(), false, check_permissions, td_));
  }

  return std::move(content);
}

Result<MessageCopyOptions> MessagesManager::process_message_copy_options(
    DialogId dialog_id, tl_object_ptr<td_api::messageCopyOptions> &&options) const {
  if (options == nullptr || !options->send_copy_) {
    return MessageCopyOptions();
  }
  MessageCopyOptions result;
  result.send_copy = true;
  result.replace_caption = options->replace_caption_;
  if (result.replace_caption) {
    TRY_RESULT_ASSIGN(result.new_caption, get_formatted_text(td_, dialog_id, std::move(options->new_caption_),
                                                             td_->auth_manager_->is_bot(), true, false, false));
  }
  return std::move(result);
}

Result<MessagesManager::MessageSendOptions> MessagesManager::process_message_send_options(
    DialogId dialog_id, tl_object_ptr<td_api::messageSendOptions> &&options,
    bool allow_update_stickersets_order) const {
  MessageSendOptions result;
  if (options != nullptr) {
    result.disable_notification = options->disable_notification_;
    result.from_background = options->from_background_;
    if (allow_update_stickersets_order) {
      result.update_stickersets_order = options->update_order_of_installed_sticker_sets_;
    }
    result.protect_content = options->protect_content_;
    result.only_preview = options->only_preview_;
    TRY_RESULT_ASSIGN(result.schedule_date, get_message_schedule_date(std::move(options->scheduling_state_)));
    result.sending_id = options->sending_id_;
  }

  auto dialog_type = dialog_id.get_type();
  if (result.schedule_date != 0) {
    if (dialog_type == DialogType::SecretChat) {
      return Status::Error(400, "Can't schedule messages in secret chats");
    }
    if (td_->auth_manager_->is_bot()) {
      return Status::Error(400, "Bots can't send scheduled messages");
    }
  }
  if (result.schedule_date == SCHEDULE_WHEN_ONLINE_DATE) {
    if (dialog_type != DialogType::User) {
      return Status::Error(400, "Messages can be scheduled till online only in private chats");
    }
    if (dialog_id == td_->dialog_manager_->get_my_dialog_id()) {
      return Status::Error(400, "Can't scheduled till online messages in chat with self");
    }
  }

  if (result.protect_content && !td_->auth_manager_->is_bot()) {
    result.protect_content = false;
  }

  return result;
}

Status MessagesManager::can_use_message_send_options(const MessageSendOptions &options,
                                                     const unique_ptr<MessageContent> &content,
                                                     MessageSelfDestructType ttl) {
  if (options.schedule_date != 0) {
    if (ttl.is_valid()) {
      return Status::Error(400, "Can't send scheduled self-destructing messages");
    }
    if (content->get_type() == MessageContentType::LiveLocation) {
      return Status::Error(400, "Can't send scheduled live location messages");
    }
  }

  return Status::OK();
}

Status MessagesManager::can_use_message_send_options(const MessageSendOptions &options,
                                                     const InputMessageContent &content) {
  return can_use_message_send_options(options, content.content, content.ttl);
}

Status MessagesManager::can_use_top_thread_message_id(Dialog *d, MessageId top_thread_message_id,
                                                      const MessageInputReplyTo &input_reply_to) {
  if (top_thread_message_id == MessageId()) {
    return Status::OK();
  }

  if (!top_thread_message_id.is_valid() || !top_thread_message_id.is_server()) {
    return Status::Error(400, "Invalid message thread identifier specified");
  }
  if (d->dialog_id.get_type() != DialogType::Channel || td_->dialog_manager_->is_broadcast_channel(d->dialog_id)) {
    return Status::Error(400, "Chat doesn't have threads");
  }
  if (input_reply_to.get_story_full_id().is_valid()) {
    return Status::Error(400, "Can't send story replies to the thread");
  }
  auto same_chat_reply_to_message_id = input_reply_to.get_same_chat_reply_to_message_id();
  if (same_chat_reply_to_message_id.is_valid()) {
    const Message *reply_m = get_message_force(d, same_chat_reply_to_message_id, "can_use_top_thread_message_id 1");
    if (reply_m != nullptr && top_thread_message_id != reply_m->top_thread_message_id) {
      if (reply_m->top_thread_message_id.is_valid() || reply_m->media_album_id == 0) {
        return Status::Error(400, "The message to be replied is not in the specified message thread");
      }

      // if the message is in an album and not in the thread, it can be in the album of top_thread_message_id
      const Message *top_m = get_message_force(d, top_thread_message_id, "can_use_top_thread_message_id 2");
      if (top_m != nullptr &&
          (top_m->media_album_id != reply_m->media_album_id || top_m->top_thread_message_id != top_m->message_id)) {
        return Status::Error(400, "The message to be replied is not in the specified message thread root album");
      }
    }
  }

  return Status::OK();
}

int64 MessagesManager::generate_new_media_album_id() {
  int64 media_album_id = 0;
  do {
    media_album_id = Random::secure_int64();
  } while (media_album_id >= 0 || pending_message_group_sends_.count(media_album_id) != 0);
  return media_album_id;
}

Result<td_api::object_ptr<td_api::messages>> MessagesManager::send_message_group(
    DialogId dialog_id, const MessageId top_thread_message_id,
    td_api::object_ptr<td_api::InputMessageReplyTo> &&reply_to, tl_object_ptr<td_api::messageSendOptions> &&options,
    vector<tl_object_ptr<td_api::InputMessageContent>> &&input_message_contents) {
  if (input_message_contents.size() > MAX_GROUPED_MESSAGES) {
    return Status::Error(400, "Too many messages to send as an album");
  }
  if (input_message_contents.empty()) {
    return Status::Error(400, "There are no messages to send");
  }

  Dialog *d = get_dialog_force(dialog_id, "send_message_group");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }

  TRY_STATUS(can_send_message(dialog_id));
  TRY_RESULT(message_send_options, process_message_send_options(dialog_id, std::move(options), true));

  vector<InputMessageContent> message_contents;
  std::unordered_set<MessageContentType, MessageContentTypeHash> message_content_types;
  for (auto &input_message_content : input_message_contents) {
    TRY_RESULT(message_content, process_input_message_content(dialog_id, std::move(input_message_content)));
    TRY_STATUS(can_use_message_send_options(message_send_options, message_content));
    auto message_content_type = message_content.content->get_type();
    if (!is_allowed_media_group_content(message_content_type)) {
      return Status::Error(400, "Invalid message content type");
    }
    message_content_types.insert(message_content_type);

    message_contents.push_back(std::move(message_content));
  }
  if (message_content_types.size() > 1) {
    for (auto message_content_type : message_content_types) {
      if (is_homogenous_media_group_content(message_content_type)) {
        return Status::Error(400, PSLICE() << message_content_type << " can't be mixed with other media types");
      }
    }
  }

  auto input_reply_to = get_message_input_reply_to(d, top_thread_message_id, std::move(reply_to), false);
  TRY_STATUS(can_use_top_thread_message_id(d, top_thread_message_id, input_reply_to));

  int64 media_album_id = 0;
  if (message_contents.size() > 1) {
    media_album_id = generate_new_media_album_id();
  }

  // there must be no errors after get_message_to_send calls

  vector<tl_object_ptr<td_api::message>> result;
  bool need_update_dialog_pos = false;
  for (size_t i = 0; i < message_contents.size(); i++) {
    auto &message_content = message_contents[i];
    unique_ptr<Message> message;
    Message *m;
    if (message_send_options.only_preview) {
      message = create_message_to_send(d, top_thread_message_id, std::move(input_reply_to), message_send_options,
                                       std::move(message_content.content), message_content.invert_media, i != 0,
                                       nullptr, DialogId(), false, DialogId());
      m = message.get();
    } else {
      m = get_message_to_send(d, top_thread_message_id, std::move(input_reply_to), message_send_options,
                              dup_message_content(td_, dialog_id, message_content.content.get(),
                                                  MessageContentDupType::Send, MessageCopyOptions()),
                              message_content.invert_media, &need_update_dialog_pos, i != 0);
    }

    auto ttl = message_content.ttl;
    if (ttl.is_valid()) {
      m->ttl = ttl;
      m->is_content_secret = m->ttl.is_secret_message_content(m->content->get_type());
    }
    m->media_album_id = media_album_id;

    result.push_back(get_message_object(dialog_id, m, "send_message_group"));

    if (!message_send_options.only_preview) {
      save_send_message_log_event(dialog_id, m);
      do_send_message(dialog_id, m);

      if (!td_->auth_manager_->is_bot()) {
        send_update_new_message(d, m);
      }
    }
  }

  if (need_update_dialog_pos) {
    CHECK(!message_send_options.only_preview);
    send_update_chat_last_message(d, "send_message_group");
  }

  return get_messages_object(-1, std::move(result), false);
}

void MessagesManager::save_send_message_log_event(DialogId dialog_id, const Message *m) {
  if (!G()->use_message_database()) {
    return;
  }

  CHECK(m != nullptr);
  LOG(INFO) << "Save " << MessageFullId(dialog_id, m->message_id) << " to binlog";
  auto log_event = SendMessageLogEvent(dialog_id, m);
  CHECK(m->send_message_log_event_id == 0);
  m->send_message_log_event_id =
      binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::SendMessage, get_log_event_storer(log_event));
}

void MessagesManager::do_send_message(DialogId dialog_id, const Message *m, vector<int> bad_parts) {
  bool is_edit = m->message_id.is_any_server();
  LOG(INFO) << "Do " << (is_edit ? "edit" : "send") << ' ' << MessageFullId(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) {
    CHECK(!is_edit);
    send_closure_later(actor_id(this), &MessagesManager::on_text_message_ready_to_send, dialog_id, m->message_id);
    return;
  }

  FileId file_id = get_message_content_any_file_id(content);  // any_file_id, because it could be a photo sent by ID
  FileView file_view = td_->file_manager_->get_file_view(file_id);
  FileId thumbnail_file_id = get_message_content_thumbnail_file_id(content, td_);
  LOG(DEBUG) << "Need to send file " << file_id << " with thumbnail " << thumbnail_file_id;
  if (is_secret) {
    CHECK(!is_edit);
    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()) {
      LOG(INFO) << "Ask to upload encrypted file " << file_id;
      CHECK(file_view.is_encrypted_secret());
      CHECK(file_id.is_valid());
      bool is_inserted =
          being_uploaded_files_
              .emplace(file_id, std::make_pair(MessageFullId(dialog_id, m->message_id), thumbnail_file_id))
              .second;
      CHECK(is_inserted);
      // 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), file_id, thumbnail_file_id);
    }
  } else {
    auto input_media =
        get_input_media(content, td_, m->ttl, m->send_emoji, td_->auth_manager_->is_bot() && bad_parts.empty());
    if (input_media == nullptr) {
      if (content_type == MessageContentType::Game || content_type == MessageContentType::Poll ||
          content_type == MessageContentType::Story) {
        return;
      }
      if (get_main_file_type(file_view.get_type()) == FileType::Photo) {
        thumbnail_file_id = FileId();
      }

      LOG(INFO) << "Ask to upload file " << file_id << " with bad parts " << bad_parts;
      CHECK(file_id.is_valid());
      bool is_inserted =
          being_uploaded_files_
              .emplace(file_id, std::make_pair(MessageFullId(dialog_id, m->message_id), thumbnail_file_id))
              .second;
      CHECK(is_inserted);
      // need to call resume_upload synchronously to make upload process consistent with being_uploaded_files_
      // and to send is_uploading_active == true in the updates
      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), file_id, thumbnail_file_id);
    }
  }
}

void MessagesManager::on_message_media_uploaded(DialogId dialog_id, const Message *m,
                                                tl_object_ptr<telegram_api::InputMedia> &&input_media, FileId file_id,
                                                FileId thumbnail_file_id) {
  if (G()->close_flag()) {
    return;
  }

  CHECK(m != nullptr);
  CHECK(input_media != nullptr);
  auto message_id = m->message_id;
  if (message_id.is_any_server()) {
    const FormattedText *caption = get_message_content_caption(m->edited_content.get());
    auto input_reply_markup = get_input_reply_markup(td_->contacts_manager_.get(), m->edited_reply_markup);
    bool was_uploaded = FileManager::extract_was_uploaded(input_media);
    bool was_thumbnail_uploaded = FileManager::extract_was_thumbnail_uploaded(input_media);

    LOG(INFO) << "Edit media from " << message_id << " in " << dialog_id;
    auto schedule_date = get_message_schedule_date(m);
    auto promise = PromiseCreator::lambda(
        [actor_id = actor_id(this), dialog_id, message_id, file_id, thumbnail_file_id, schedule_date,
         generation = m->edit_generation, was_uploaded, was_thumbnail_uploaded,
         file_reference = FileManager::extract_file_reference(input_media)](Result<int32> result) mutable {
          send_closure(actor_id, &MessagesManager::on_message_media_edited, dialog_id, message_id, file_id,
                       thumbnail_file_id, was_uploaded, was_thumbnail_uploaded, std::move(file_reference),
                       schedule_date, generation, std::move(result));
        });
    td_->create_handler<EditMessageQuery>(std::move(promise))
        ->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), m->edited_invert_media, std::move(input_reply_markup), schedule_date);
    return;
  }

  if (m->media_album_id == 0) {
    send_closure_later(
        actor_id(this), &MessagesManager::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 (G()->close_flag() || result.is_error()) {
            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;
          int64 random_id = begin_send_message(dialog_id, m);
          td_->create_handler<SendMediaQuery>()->send(
              file_id, thumbnail_file_id, get_message_flags(m), dialog_id, get_send_message_as_input_peer(m),
              *get_message_input_reply_to(m), m->initial_top_thread_message_id, get_message_schedule_date(m),
              get_input_reply_markup(td_->contacts_manager_.get(), m->reply_markup),
              get_input_message_entities(td_->contacts_manager_.get(), caption, "on_message_media_uploaded"),
              caption == nullptr ? "" : caption->text, std::move(input_media), m->content->get_type(), m->is_copy,
              random_id, &m->send_query_ref);
        }));
  } 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, "Invalid input media"));
    }
  }
}

void MessagesManager::on_secret_message_media_uploaded(DialogId dialog_id, const Message *m,
                                                       SecretInputMedia &&secret_input_media, FileId file_id,
                                                       FileId thumbnail_file_id) {
  if (G()->close_flag()) {
    return;
  }

  CHECK(m != nullptr);
  CHECK(m->message_id.is_valid());
  if (secret_input_media.empty()) {
    // the media can't be sent to the chat
    LOG(INFO) << "Can't send a media message to " << dialog_id;

    fail_send_message({dialog_id, m->message_id}, Status::Error(400, "The file can't be sent to the secret chat"));
    return;
  }

  /*
  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, "Invalid input media"));
  }
  */
  // TODO use file_id, thumbnail_file_id, was_uploaded, was_thumbnail_uploaded,
  // invalidate partial remote location for file_id in case of failed upload even message has already been deleted
  send_closure_later(actor_id(this), &MessagesManager::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 (G()->close_flag() || result.is_error()) {
                         return;
                       }

                       auto m = result.move_as_ok();
                       CHECK(m != nullptr);
                       CHECK(!secret_input_media.empty());
                       send_secret_message(dialog_id, m, std::move(secret_input_media));
                     }));
}

void MessagesManager::send_secret_message(DialogId dialog_id, const Message *m, SecretInputMedia media) {
  CHECK(dialog_id.get_type() == DialogType::SecretChat);
  int64 random_id = begin_send_message(dialog_id, m);

  auto text = get_message_content_text(m->content.get());
  vector<tl_object_ptr<secret_api::MessageEntity>> entities;
  if (text != nullptr && !text->entities.empty()) {
    auto layer = td_->contacts_manager_->get_secret_chat_layer(dialog_id.get_secret_chat_id());
    entities = get_input_secret_message_entities(text->entities, layer);
  }

  int32 flags = 0;
  if (m->reply_to_random_id != 0) {
    flags |= secret_api::decryptedMessage::REPLY_TO_RANDOM_ID_MASK;
  }
  if (m->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 (m->media_album_id != 0) {
    CHECK(m->media_album_id < 0);
    flags |= secret_api::decryptedMessage::GROUPED_ID_MASK;
  }
  if (m->disable_notification) {
    flags |= secret_api::decryptedMessage::SILENT_MASK;
  }

  send_closure(
      td_->secret_chats_manager_, &SecretChatsManager::send_message, dialog_id.get_secret_chat_id(),
      make_tl_object<secret_api::decryptedMessage>(
          flags, false /*ignored*/, random_id, m->ttl.get_input_ttl(),
          m->content->get_type() == MessageContentType::Text ? text->text : string(), std::move(media.decrypted_media_),
          std::move(entities), td_->contacts_manager_->get_user_first_username(m->via_bot_user_id),
          m->reply_to_random_id, -m->media_album_id),
      std::move(media.input_file_), Promise<Unit>());
}

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_valid() || message_id.is_valid_scheduled());
  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) << "Don't need to send already deleted by the user or sent to an inaccessible chat "
              << MessageFullId{dialog_id, message_id};
    return;
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return;  // the message should be deleted soon
  }

  auto caption = get_message_content_caption(m->content.get());
  auto has_spoiler = get_message_content_has_spoiler(m->content.get());
  auto content = get_message_content(td_, caption == nullptr ? FormattedText() : *caption, std::move(media), dialog_id,
                                     m->date, false, UserId(), nullptr, nullptr, "on_upload_message_media_success");
  set_message_content_has_spoiler(content.get(), has_spoiler);

  bool is_content_changed = false;
  bool need_update = update_message_content(dialog_id, m, std::move(content), true, true, is_content_changed);
  if (need_update) {
    send_update_message_content(d, m, true, "on_upload_message_media_success");
  }
  if (is_content_changed || need_update) {
    on_message_changed(d, m, need_update, "on_upload_message_media_success");
  }

  auto input_media = get_input_media(m->content.get(), td_, m->ttl, m->send_emoji, true);
  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,
                     m->message_id, std::move(result));
}

void MessagesManager::on_upload_message_media_file_parts_missing(DialogId dialog_id, MessageId message_id,
                                                                 vector<int> &&bad_parts) {
  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) << "Don't need to send already deleted by the user or sent to an inaccessible chat "
              << MessageFullId{dialog_id, message_id};
    return;
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return;  // the message should be deleted soon
  }

  CHECK(dialog_id.get_type() != DialogType::SecretChat);

  do_send_message(dialog_id, m, std::move(bad_parts));
}

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) << "Don't need to send already deleted by the user or sent to an inaccessible chat "
              << MessageFullId{dialog_id, message_id};
    return;
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    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,
                     m->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);
  if (message_it == request.message_ids.end()) {
    // the message may be already deleted and the album is recreated without it
    CHECK(message_id.is_yet_unsent());
    LOG_CHECK(get_message({dialog_id, message_id}) == nullptr)
        << dialog_id << ' ' << request.message_ids << ' ' << message_id << ' ' << request.finished_count << ' '
        << request.is_finished << ' ' << request.results;
    return;
  }
  auto pos = static_cast<size_t>(message_it - request.message_ids.begin());

  if (request.is_finished[pos]) {
    LOG(INFO) << "Upload media of " << message_id << " in " << dialog_id << " from group " << media_album_id
              << " at pos " << pos << " was already finished";
    return;
  }
  LOG(INFO) << "Finish to upload media of " << message_id << " in " << dialog_id << " from group " << media_album_id
            << " at pos " << pos << " with result " << result
            << " and previous finished_count = " << request.finished_count;

  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()) {
    // must use send_closure_later if some messages may be being deleted now
    // but this function is called only through send_closure_later, so there should be no being deleted messages
    // we must use synchronous calls to keep the correct message order during copying of multiple messages
    // but "request" iterator can be invalidated by do_send_message_group, so it must not be used below
    auto message_ids = request.message_ids;
    for (auto request_message_id : message_ids) {
      LOG(INFO) << "Send on_media_message_ready_to_send for " << request_message_id << " in " << dialog_id;
      auto promise = PromiseCreator::lambda([this, media_album_id](Result<Message *> result) {
        if (G()->close_flag() || result.is_error()) {
          return;
        }

        auto m = result.move_as_ok();
        CHECK(m != nullptr);
        CHECK(m->media_album_id == media_album_id);
        do_send_message_group(media_album_id);
        // send_closure_later(actor_id, &MessagesManager::do_send_message_group, media_album_id);
      });
      // send_closure_later(actor_id(this), &MessagesManager::on_media_message_ready_to_send, dialog_id,
      //                   request_message_id, std::move(promise));
      on_media_message_ready_to_send(dialog_id, request_message_id, std::move(promise));
    }
  }
}

void MessagesManager::do_send_message_group(int64 media_album_id) {
  if (G()->close_flag()) {
    return;
  }

  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<FileId> file_ids;
  vector<int64> random_ids;
  vector<tl_object_ptr<telegram_api::inputSingleMedia>> input_single_media;
  tl_object_ptr<telegram_api::InputPeer> as_input_peer;
  const MessageInputReplyTo *input_reply_to = nullptr;
  MessageId top_thread_message_id;
  int32 flags = 0;
  int32 schedule_date = 0;
  bool is_copy = false;
  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;
    }

    input_reply_to = get_message_input_reply_to(m);
    top_thread_message_id = m->initial_top_thread_message_id;
    flags = get_message_flags(m);
    schedule_date = get_message_schedule_date(m);
    is_copy = m->is_copy;
    as_input_peer = get_send_message_as_input_peer(m);

    file_ids.push_back(get_message_content_any_file_id(m->content.get()));
    random_ids.push_back(begin_send_message(dialog_id, m));

    LOG(INFO) << "Have file " << file_ids.back() << " in " << m->message_id << " with result " << request.results[i]
              << " and is_finished = " << static_cast<bool>(request.is_finished[i]);

    if (request.results[i].is_error() || !request.is_finished[i]) {
      success = false;
      continue;
    }

    const FormattedText *caption = get_message_content_caption(m->content.get());
    auto input_media = get_input_media(m->content.get(), td_, m->ttl, m->send_emoji, true);
    if (input_media == nullptr) {
      // TODO return CHECK
      auto file_id = get_message_content_any_file_id(m->content.get());
      auto file_view = td_->file_manager_->get_file_view(file_id);
      bool has_remote = file_view.has_remote_location();
      bool is_web = has_remote ? file_view.remote_location().is_web() : false;
      LOG(FATAL) << request.dialog_id << " " << request.finished_count << " " << i << " " << request.message_ids << " "
                 << request.is_finished << " " << request.results << " " << m->ttl << " " << has_remote << " "
                 << file_view.has_alive_remote_location() << " " << file_view.has_active_upload_remote_location() << " "
                 << file_view.has_active_download_remote_location() << " " << file_view.is_encrypted() << " " << is_web
                 << " " << file_view.has_url() << " " << to_string(get_message_message_content_object(dialog_id, m));
    }
    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 (!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;
  }
  LOG_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";
  }
  CHECK(input_reply_to != nullptr);
  td_->create_handler<SendMultiMediaQuery>()->send(flags, dialog_id, std::move(as_input_peer), *input_reply_to,
                                                   top_thread_message_id, schedule_date, std::move(file_ids),
                                                   std::move(input_single_media), is_copy);
}

void MessagesManager::on_text_message_ready_to_send(DialogId dialog_id, MessageId message_id) {
  if (G()->close_flag()) {
    return;
  }

  LOG(INFO) << "Ready to send " << message_id << " to " << dialog_id;

  auto m = get_message({dialog_id, message_id});
  if (m == nullptr) {
    return;
  }

  CHECK(message_id.is_yet_unsent());

  auto content = m->content.get();
  CHECK(content != nullptr);

  if (dialog_id.get_type() == DialogType::SecretChat) {
    CHECK(!message_id.is_scheduled());
    auto layer = td_->contacts_manager_->get_secret_chat_layer(dialog_id.get_secret_chat_id());
    send_secret_message(dialog_id, m, get_secret_input_media(content, td_, nullptr, BufferSlice(), layer));
  } else {
    const FormattedText *message_text = get_message_content_text(content);
    CHECK(message_text != nullptr);
    int64 random_id = begin_send_message(dialog_id, m);
    auto input_media = get_message_content_input_media_web_page(td_, content);
    if (input_media == nullptr) {
      td_->create_handler<SendMessageQuery>()->send(
          get_message_flags(m), dialog_id, get_send_message_as_input_peer(m), *get_message_input_reply_to(m),
          m->initial_top_thread_message_id, get_message_schedule_date(m),
          get_input_reply_markup(td_->contacts_manager_.get(), m->reply_markup),
          get_input_message_entities(td_->contacts_manager_.get(), message_text, "do_send_message"), message_text->text,
          m->is_copy, random_id, &m->send_query_ref);
    } else {
      td_->create_handler<SendMediaQuery>()->send(
          FileId(), FileId(), get_message_flags(m), dialog_id, get_send_message_as_input_peer(m),
          *get_message_input_reply_to(m), m->initial_top_thread_message_id, get_message_schedule_date(m),
          get_input_reply_markup(td_->contacts_manager_.get(), m->reply_markup),
          get_input_message_entities(td_->contacts_manager_.get(), message_text, "do_send_message"), message_text->text,
          std::move(input_media), MessageContentType::Text, m->is_copy, random_id, &m->send_query_ref);
    }
  }
}

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()->keep_media_order() || message_id.is_scheduled()) {
    auto m = get_message({dialog_id, message_id});
    if (m != nullptr) {
      promise.set_value(std::move(m));
    }
    return;
  }

  auto queue_id = ChainId(dialog_id, MessageContentType::Photo).get();
  CHECK(queue_id & 1);
  auto &queue = yet_unsent_media_queues_[queue_id];
  queue.dialog_id_ = dialog_id;
  auto it = queue.queue_.find(message_id);
  if (it == queue.queue_.end()) {
    if (queue.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 = ChainId(dialog_id, MessageContentType::Photo).get();
  CHECK(queue_id & 1);
  while (true) {
    auto it = yet_unsent_media_queues_.find(queue_id);
    if (it == yet_unsent_media_queues_.end()) {
      return;
    }
    auto &queue = it->second.queue_;
    if (queue.empty()) {
      yet_unsent_media_queues_.erase(it);
      return;
    }
    auto first_it = queue.begin();
    if (!first_it->second) {
      return;
    }

    auto m = get_message({dialog_id, first_it->first});
    auto promise = std::move(first_it->second);
    queue.erase(first_it);
    LOG(INFO) << "Queue for " << dialog_id << " now has size " << queue.size();

    // don't use it/queue/first_it after promise is called
    if (m != nullptr) {
      LOG(INFO) << "Can send " << MessageFullId{dialog_id, m->message_id};
      promise.set_value(std::move(m));
    } else {
      promise.set_error(Status::Error(400, "Message not found"));
    }
  }
}

Result<MessageId> MessagesManager::send_bot_start_message(UserId bot_user_id, DialogId dialog_id,
                                                          const string &parameter) {
  CHECK(!td_->auth_manager_->is_bot());

  TRY_RESULT(bot_data, td_->contacts_manager_->get_bot_data(bot_user_id));

  Dialog *d = get_dialog_force(dialog_id, "send_bot_start_message");
  if (d == nullptr) {
    return Status::Error(400, "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(400, "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(400, "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(400, "Can't access the chat");
      }
      auto status = td_->contacts_manager_->get_chat_permissions(chat_id);
      if (!status.can_invite_users()) {
        return Status::Error(400, "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(400, "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(400, "The bot can't join groups");
          }
          break;
        case ChannelType::Broadcast:
          return Status::Error(400, "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_permissions(channel_id);
      if (!status.can_invite_users()) {
        return Status::Error(400, "Need administrator rights to invite a bot to the supergroup chat");
      }
      break;
    }
    case DialogType::SecretChat:
      return Status::Error(400, "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(), MessageInputReplyTo(), MessageSendOptions(),
      create_text_message_content(text, std::move(text_entities), WebPageId(), false, false, false, string()), false,
      &need_update_dialog_pos);
  m->is_bot_start_message = true;

  send_update_new_message(d, m);
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "send_bot_start_message");
  }

  if (parameter.empty() && is_chat_with_bot) {
    save_send_message_log_event(dialog_id, m);
    do_send_message(dialog_id, m);
  } else {
    save_send_bot_start_message_log_event(bot_user_id, dialog_id, parameter, m);
    send_closure_later(actor_id(this), &MessagesManager::do_send_bot_start_message, bot_user_id, dialog_id,
                       m->message_id, parameter);
  }
  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> message_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);
    td::parse(message_out, parser);
  }
};

void MessagesManager::save_send_bot_start_message_log_event(UserId bot_user_id, DialogId dialog_id,
                                                            const string &parameter, const Message *m) {
  if (!G()->use_message_database()) {
    return;
  }

  CHECK(m != nullptr);
  LOG(INFO) << "Save " << MessageFullId(dialog_id, m->message_id) << " to binlog";
  SendBotStartMessageLogEvent log_event;
  log_event.bot_user_id = bot_user_id;
  log_event.dialog_id = dialog_id;
  log_event.parameter = parameter;
  log_event.m_in = m;
  CHECK(m->send_message_log_event_id == 0);
  m->send_message_log_event_id = binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::SendBotStartMessage,
                                            get_log_event_storer(log_event));
}

void MessagesManager::do_send_bot_start_message(UserId bot_user_id, DialogId dialog_id, MessageId message_id,
                                                const string &parameter) {
  if (G()->close_flag()) {
    return;
  }

  LOG(INFO) << "Do send bot start " << MessageFullId(dialog_id, message_id) << " to bot " << bot_user_id;

  auto m = get_message({dialog_id, message_id});
  if (m == nullptr) {
    return;
  }

  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>()
                                               : td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write);
  if (input_peer == nullptr) {
    return on_send_message_fail(random_id, Status::Error(400, "Chat is not accessible"));
  }
  auto r_bot_input_user = td_->contacts_manager_->get_input_user(bot_user_id);
  if (r_bot_input_user.is_error()) {
    return on_send_message_fail(random_id, r_bot_input_user.move_as_error());
  }

  m->send_query_ref = td_->create_handler<StartBotQuery>()->send(r_bot_input_user.move_as_ok(), dialog_id,
                                                                 std::move(input_peer), parameter, random_id);
}

Result<td_api::object_ptr<td_api::message>> MessagesManager::send_inline_query_result_message(
    DialogId dialog_id, const MessageId top_thread_message_id,
    td_api::object_ptr<td_api::InputMessageReplyTo> &&reply_to, tl_object_ptr<td_api::messageSendOptions> &&options,
    int64 query_id, const string &result_id, bool hide_via_bot) {
  Dialog *d = get_dialog_force(dialog_id, "send_inline_query_result_message");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }

  TRY_STATUS(can_send_message(dialog_id));
  TRY_RESULT(message_send_options, process_message_send_options(dialog_id, std::move(options), false));
  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_permissions(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(400, "Inline query result not found");
  }

  auto input_reply_to = get_message_input_reply_to(d, top_thread_message_id, std::move(reply_to), false);
  TRY_STATUS(can_use_message_send_options(message_send_options, content->message_content, MessageSelfDestructType()));
  TRY_STATUS(can_send_message_content(dialog_id, content->message_content.get(), false, true, td_));
  TRY_STATUS(can_use_top_thread_message_id(d, top_thread_message_id, input_reply_to));

  auto message_content = dup_message_content(td_, dialog_id, content->message_content.get(),
                                             MessageContentDupType::SendViaBot, MessageCopyOptions());
  bool need_update_dialog_pos = false;
  unique_ptr<Message> message;
  Message *m;
  if (message_send_options.only_preview) {
    message = create_message_to_send(d, top_thread_message_id, std::move(input_reply_to), message_send_options,
                                     std::move(message_content), content->invert_media, false, nullptr, DialogId(),
                                     true, DialogId());
    m = message.get();
  } else {
    m = get_message_to_send(d, top_thread_message_id, std::move(input_reply_to), message_send_options,
                            std::move(message_content), content->invert_media, &need_update_dialog_pos, false, nullptr,
                            DialogId(), true);
  }

  m->hide_via_bot = hide_via_bot;
  if (!hide_via_bot) {
    m->via_bot_user_id = td_->inline_queries_manager_->get_inline_bot_user_id(query_id);
  }
  if (!to_secret) {
    m->reply_markup = dup_reply_markup(content->message_reply_markup);
  }
  m->disable_web_page_preview = content->disable_web_page_preview;
  m->clear_draft = !hide_via_bot;

  if (message_send_options.only_preview) {
    return get_message_object(dialog_id, m, "send_inline_query_result_message");
  }

  clear_dialog_draft_by_sent_message(d, m, !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_log_event(dialog_id, m);
    do_send_message(dialog_id, m);
  } else {
    save_send_inline_query_result_message_log_event(dialog_id, m, query_id, result_id);
    send_closure_later(actor_id(this), &MessagesManager::do_send_inline_query_result_message, dialog_id, m->message_id,
                       query_id, result_id);
  }
  return get_message_object(dialog_id, m, "send_inline_query_result_message");
}

class MessagesManager::SendInlineQueryResultMessageLogEvent {
 public:
  DialogId dialog_id;
  int64 query_id;
  string result_id;
  const Message *m_in = nullptr;
  unique_ptr<Message> message_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);
    td::parse(message_out, parser);
  }
};

void MessagesManager::save_send_inline_query_result_message_log_event(DialogId dialog_id, const Message *m,
                                                                      int64 query_id, const string &result_id) {
  if (!G()->use_message_database()) {
    return;
  }

  CHECK(m != nullptr);
  LOG(INFO) << "Save " << MessageFullId(dialog_id, m->message_id) << " to binlog";
  SendInlineQueryResultMessageLogEvent log_event;
  log_event.dialog_id = dialog_id;
  log_event.query_id = query_id;
  log_event.result_id = result_id;
  log_event.m_in = m;
  CHECK(m->send_message_log_event_id == 0);
  m->send_message_log_event_id = binlog_add(
      G()->td_db()->get_binlog(), LogEvent::HandlerType::SendInlineQueryResultMessage, get_log_event_storer(log_event));
}

void MessagesManager::do_send_inline_query_result_message(DialogId dialog_id, MessageId message_id, int64 query_id,
                                                          const string &result_id) {
  if (G()->close_flag()) {
    return;
  }

  LOG(INFO) << "Do send inline query result " << MessageFullId(dialog_id, message_id);

  auto m = get_message({dialog_id, message_id});
  if (m == nullptr) {
    return;
  }

  int64 random_id = begin_send_message(dialog_id, m);
  auto flags = get_message_flags(m);
  if (!m->via_bot_user_id.is_valid() || m->hide_via_bot) {
    flags |= telegram_api::messages_sendInlineBotResult::HIDE_VIA_MASK;
  }
  m->send_query_ref = td_->create_handler<SendInlineBotResultQuery>()->send(
      flags, dialog_id, get_send_message_as_input_peer(m), *get_message_input_reply_to(m),
      m->initial_top_thread_message_id, get_message_schedule_date(m), 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 || m->had_forward_info) {
    return false;
  }

  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 || m->message_id.is_scheduled())) {
    return false;
  }

  bool is_bot = td_->auth_manager_->is_bot();
  auto content_type = m->content->get_type();
  DialogId my_dialog_id(my_id);
  bool has_edit_time_limit = !(is_bot && m->is_outgoing) && dialog_id != my_dialog_id &&
                             content_type != MessageContentType::Poll &&
                             content_type != MessageContentType::LiveLocation && !m->message_id.is_scheduled();
  switch (dialog_id.get_type()) {
    case DialogType::User:
      if (!m->is_outgoing && dialog_id != my_dialog_id && !m->via_bot_user_id.is_valid()) {
        return false;
      }
      break;
    case DialogType::Chat:
      if (!m->is_outgoing && !m->via_bot_user_id.is_valid()) {
        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_permissions(channel_id);
      if (m->is_channel_post) {
        if (m->message_id.is_scheduled()) {
          if (!channel_status.can_post_messages()) {
            return false;
          }
        } else {
          if (!channel_status.can_edit_messages() && !(channel_status.can_post_messages() && m->is_outgoing)) {
            return false;
          }
          if (channel_status.can_edit_messages()) {
            has_edit_time_limit = false;
          }
        }
        if (is_bot && only_reply_markup) {
          has_edit_time_limit = 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;
  }

  if (has_edit_time_limit) {
    const int32 DEFAULT_EDIT_TIME_LIMIT = 2 * 86400;
    int64 edit_time_limit = td_->option_manager_->get_option_integer("edit_time_limit", DEFAULT_EDIT_TIME_LIMIT);
    if (G()->unix_time() - m->date - (is_editing ? 300 : 0) >= edit_time_limit) {
      return false;
    }
  }

  if (is_editable_message_content(content_type)) {
    return true;
  }
  if (is_bot && only_reply_markup && !is_service_message_content(content_type) &&
      !is_expired_message_content(content_type)) {
    return true;
  }
  switch (content_type) {
    case MessageContentType::LiveLocation:
      // live location can be edited only during the period
      return G()->unix_time() - m->date < get_message_content_live_location_period(m->content.get());
    case MessageContentType::Poll:
      // non-closed non-scheduled polls can be closed
      return !m->message_id.is_scheduled() && !get_message_content_poll_is_closed(td_, m->content.get());
    default:
      return false;
  }
}

bool MessagesManager::can_resend_message(const Message *m) const {
  if (m->send_error_code != 429 && m->send_error_message != "Message is too old to be re-sent automatically" &&
      m->send_error_message != "SCHEDULE_TOO_MUCH" && m->send_error_message != "SEND_AS_PEER_INVALID" &&
      m->send_error_message != "QUOTE_TEXT_INVALID" && m->send_error_message != "REPLY_MESSAGE_ID_INVALID") {
    return false;
  }
  if (m->is_bot_start_message) {
    return false;
  }
  if (m->forward_info != nullptr || m->real_forward_from_dialog_id.is_valid()) {
    // TODO implement resending of forwarded messages
    return false;
  }
  auto content_type = m->content->get_type();
  if (m->via_bot_user_id.is_valid() || m->hide_via_bot) {
    // via bot message
    if (!can_have_input_media(td_, m->content.get(), false)) {
      return false;
    }

    // resend via_bot message as an ordinary message if error code is 429
    // TODO support other error codes
  }

  if (content_type == MessageContentType::ChatSetTtl || content_type == MessageContentType::ScreenshotTaken) {
    // TODO implement resending of ChatSetTtl and ScreenshotTaken messages
    return false;
  }
  return true;
}

bool MessagesManager::is_deleted_secret_chat(DialogId dialog_id) const {
  return is_deleted_secret_chat(get_dialog(dialog_id));
}

bool MessagesManager::is_deleted_secret_chat(const Dialog *d) const {
  if (d == nullptr) {
    return true;
  }
  if (d->dialog_id.get_type() != DialogType::SecretChat) {
    return false;
  }

  if (d->order != DEFAULT_ORDER || !d->messages.empty()) {
    return false;
  }

  auto state = td_->contacts_manager_->get_secret_chat_state(d->dialog_id.get_secret_chat_id());
  if (state != SecretChatState::Closed) {
    return false;
  }

  return true;
}

int32 MessagesManager::get_message_schedule_date(const Message *m) {
  CHECK(m != nullptr);
  if (!m->message_id.is_scheduled()) {
    return 0;
  }
  if (m->edited_schedule_date != 0) {
    return m->edited_schedule_date;
  }
  return m->date;
}

int32 MessagesManager::get_message_original_date(const Message *m) {
  CHECK(m != nullptr);
  if (m->forward_info != nullptr) {
    return m->forward_info->get_origin_date();
  }
  return m->date;
}

DialogId MessagesManager::get_message_original_sender(const Message *m) {
  CHECK(m != nullptr);
  if (m->forward_info != nullptr) {
    if (m->forward_info->is_imported()) {
      return DialogId();
    }
    return m->forward_info->get_origin().get_sender();
  }
  return get_message_sender(m);
}

DialogId MessagesManager::get_message_sender(const Message *m) {
  CHECK(m != nullptr);
  return m->sender_dialog_id.is_valid() ? m->sender_dialog_id : DialogId(m->sender_user_id);
}

void MessagesManager::edit_message_text(MessageFullId message_full_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(400, "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(400, "Input message content type must be InputMessageText"));
  }

  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "edit_message_text");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  const Message *m = get_message_force(d, message_full_id.get_message_id(), "edit_message_text");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }

  if (!can_edit_message(dialog_id, m, true)) {
    return promise.set_error(Status::Error(400, "Message can't be edited"));
  }

  MessageContentType old_message_content_type = m->content->get_type();
  if (old_message_content_type != MessageContentType::Text && old_message_content_type != MessageContentType::Game) {
    return promise.set_error(Status::Error(400, "There is no text in the message to edit"));
  }

  auto r_input_message_text =
      process_input_message_text(td_, 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());
  }
  const 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,
                                             has_message_sender_user_id(dialog_id, m));
  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(td_->contacts_manager_.get(), r_new_reply_markup.ok());
  int32 flags = 0;
  if (input_message_text.disable_web_page_preview) {
    flags |= SEND_MESSAGE_FLAG_DISABLE_WEB_PAGE_PREVIEW;
  }

  td_->create_handler<EditMessageQuery>(std::move(promise))
      ->send(flags, dialog_id, m->message_id, input_message_text.text.text,
             get_input_message_entities(td_->contacts_manager_.get(), input_message_text.text.entities,
                                        "edit_message_text"),
             input_message_text.get_input_media_web_page(), input_message_text.show_above_text,
             std::move(input_reply_markup), get_message_schedule_date(m));
}

void MessagesManager::edit_message_live_location(MessageFullId message_full_id,
                                                 tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                                 tl_object_ptr<td_api::location> &&input_location, int32 heading,
                                                 int32 proximity_alert_radius, Promise<Unit> &&promise) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "edit_message_live_location");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  const Message *m = get_message_force(d, message_full_id.get_message_id(), "edit_message_live_location");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }

  if (!can_edit_message(dialog_id, m, true)) {
    return promise.set_error(Status::Error(400, "Message can't be edited"));
  }

  MessageContentType old_message_content_type = m->content->get_type();
  if (old_message_content_type != MessageContentType::LiveLocation) {
    return promise.set_error(Status::Error(400, "There is no live location in the message to edit"));
  }
  if (m->message_id.is_scheduled()) {
    LOG(ERROR) << "Have " << message_full_id << " with live location";
    return promise.set_error(Status::Error(400, "Can't edit live location in scheduled message"));
  }

  Location location(input_location);
  if (location.empty() && input_location != nullptr) {
    return promise.set_error(Status::Error(400, "Invalid location specified"));
  }

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false,
                                             has_message_sender_user_id(dialog_id, m));
  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(td_->contacts_manager_.get(), r_new_reply_markup.ok());

  int32 flags = 0;
  if (location.empty()) {
    flags |= telegram_api::inputMediaGeoLive::STOPPED_MASK;
  }
  if (heading != 0) {
    flags |= telegram_api::inputMediaGeoLive::HEADING_MASK;
  }
  flags |= telegram_api::inputMediaGeoLive::PROXIMITY_NOTIFICATION_RADIUS_MASK;
  auto input_media = telegram_api::make_object<telegram_api::inputMediaGeoLive>(
      flags, false /*ignored*/, location.get_input_geo_point(), heading, 0, proximity_alert_radius);
  td_->create_handler<EditMessageQuery>(std::move(promise))
      ->send(0, dialog_id, m->message_id, string(), vector<tl_object_ptr<telegram_api::MessageEntity>>(),
             std::move(input_media), false, std::move(input_reply_markup), get_message_schedule_date(m));
}

void MessagesManager::cancel_edit_message_media(DialogId dialog_id, Message *m, Slice error_message) {
  if (m->edited_content == nullptr) {
    return;
  }

  cancel_upload_message_content_files(m->edited_content.get());

  m->edited_content = nullptr;
  m->edited_invert_media = false;
  m->edited_reply_markup = nullptr;
  m->edit_generation = 0;
  m->edit_promise.set_error(Status::Error(400, error_message));
}

void MessagesManager::on_message_media_edited(DialogId dialog_id, MessageId message_id, FileId file_id,
                                              FileId thumbnail_file_id, bool was_uploaded, bool was_thumbnail_uploaded,
                                              string file_reference, int32 schedule_date, uint64 generation,
                                              Result<int32> &&result) {
  // must not run getDifference

  CHECK(message_id.is_any_server());
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  auto m = get_message(d, 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()) {
    // message content has already been replaced from updateEdit{Channel,}Message
    // need only merge files from edited_content with their uploaded counterparts
    // updateMessageContent was already sent and needs to be sent again,
    // only if 'i' and 't' sizes from edited_content were added to the photo
    auto pts = result.ok();
    LOG(INFO) << "Successfully edited " << message_id << " in " << dialog_id << " with PTS = " << pts
              << " and last edit PTS = " << m->last_edit_pts;
    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;
    bool need_merge_files = pts != 0 && pts == m->last_edit_pts;
    bool is_content_changed = false;
    bool need_update =
        update_message_content(dialog_id, m, std::move(m->edited_content), need_merge_files, true, is_content_changed);
    if (need_send_update_message_content) {
      if (need_update) {
        send_update_message_content(d, m, true, "on_message_media_edited");
      }
      if (is_content_changed || need_update) {
        on_message_changed(d, m, need_update, "on_message_media_edited");
      }
    }
  } else {
    LOG(INFO) << "Failed to edit " << message_id << " in " << dialog_id << ": " << result.error();
    if (was_uploaded) {
      if (was_thumbnail_uploaded) {
        CHECK(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);
      }
      CHECK(file_id.is_valid());
      auto bad_parts = FileManager::get_missing_file_parts(result.error());
      if (!bad_parts.empty()) {
        do_send_message(dialog_id, m, std::move(bad_parts));
        return;
      }

      if (result.error().code() != 429 && result.error().code() < 500 && !G()->close_flag()) {
        td_->file_manager_->delete_partial_remote_location(file_id);
      }
    } else if (!td_->auth_manager_->is_bot() && FileReferenceManager::is_file_reference_error(result.error())) {
      if (file_id.is_valid()) {
        VLOG(file_references) << "Receive " << result.error() << " for " << file_id;
        td_->file_manager_->delete_file_reference(file_id, file_reference);
        do_send_message(dialog_id, m, {-1});
        return;
      } else {
        LOG(ERROR) << "Receive file reference error, but have no file_id";
      }
    }

    cancel_upload_message_content_files(m->edited_content.get());

    if (dialog_id.get_type() != DialogType::SecretChat) {
      get_message_from_server({dialog_id, m->message_id}, Auto(), "on_message_media_edited");
    }
  }

  if (m->edited_schedule_date == schedule_date) {
    m->edited_schedule_date = 0;
  }
  m->edited_content = nullptr;
  m->edited_invert_media = false;
  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(MessageFullId message_full_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(400, "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(400, "Unsupported input message content type"));
  }

  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "edit_message_media");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  Message *m = get_message_force(d, message_full_id.get_message_id(), "edit_message_media");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }

  if (!can_edit_message(dialog_id, m, true)) {
    return promise.set_error(Status::Error(400, "Message can't be edited"));
  }
  CHECK(m->message_id.is_any_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(400, "There is no media in the message to edit"));
  }
  if (!m->ttl.is_empty()) {
    return promise.set_error(Status::Error(400, "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.is_empty()) {
    return promise.set_error(Status::Error(400, "Can't enable self-destruction for media"));
  }

  if (m->media_album_id != 0) {
    auto new_content_type = content.content->get_type();
    if (old_message_content_type != new_content_type) {
      if (!is_allowed_media_group_content(new_content_type)) {
        return promise.set_error(Status::Error(400, "Message content type can't be used in an album"));
      }
      if (is_homogenous_media_group_content(old_message_content_type) ||
          is_homogenous_media_group_content(new_content_type)) {
        return promise.set_error(Status::Error(400, "Can't change media type in the album"));
      }
    }
  }

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false,
                                             has_message_sender_user_id(dialog_id, m));
  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, "Canceled by new editMessageMedia request");

  m->edited_content =
      dup_message_content(td_, dialog_id, content.content.get(), MessageContentDupType::Send, MessageCopyOptions());
  CHECK(m->edited_content != nullptr);
  m->edited_invert_media = content.invert_media;
  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(MessageFullId message_full_id,
                                           tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                           tl_object_ptr<td_api::formattedText> &&input_caption,
                                           Promise<Unit> &&promise) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "edit_message_caption");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  const Message *m = get_message_force(d, message_full_id.get_message_id(), "edit_message_caption");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }

  if (!can_edit_message(dialog_id, m, true)) {
    return promise.set_error(Status::Error(400, "Message can't be edited"));
  }

  if (!can_have_message_content_caption(m->content->get_type())) {
    return promise.set_error(Status::Error(400, "There is no caption in the message to edit"));
  }

  auto r_caption =
      get_formatted_text(td_, dialog_id, std::move(input_caption), td_->auth_manager_->is_bot(), true, false, false);
  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,
                                             has_message_sender_user_id(dialog_id, m));
  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(td_->contacts_manager_.get(), r_new_reply_markup.ok());

  td_->create_handler<EditMessageQuery>(std::move(promise))
      ->send(1 << 11, dialog_id, m->message_id, caption.text,
             get_input_message_entities(td_->contacts_manager_.get(), caption.entities, "edit_message_caption"),
             nullptr, m->invert_media, std::move(input_reply_markup), get_message_schedule_date(m));
}

void MessagesManager::edit_message_reply_markup(MessageFullId message_full_id,
                                                tl_object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                                Promise<Unit> &&promise) {
  CHECK(td_->auth_manager_->is_bot());

  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "edit_message_reply_markup");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  const Message *m = get_message_force(d, message_full_id.get_message_id(), "edit_message_reply_markup");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }

  if (!can_edit_message(dialog_id, m, true, true)) {
    return promise.set_error(Status::Error(400, "Message can't be edited"));
  }

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false,
                                             has_message_sender_user_id(dialog_id, m));
  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(td_->contacts_manager_.get(), r_new_reply_markup.ok());
  td_->create_handler<EditMessageQuery>(std::move(promise))
      ->send(0, dialog_id, m->message_id, string(), vector<tl_object_ptr<telegram_api::MessageEntity>>(), nullptr,
             false, std::move(input_reply_markup), get_message_schedule_date(m));
}

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) {
  CHECK(td_->auth_manager_->is_bot());

  if (input_message_content == nullptr) {
    return promise.set_error(Status::Error(400, "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(400, "Input message content type must be InputMessageText"));
  }

  auto r_input_message_text =
      process_input_message_text(td_, 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());
  }
  const 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, "Invalid 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"),
             input_message_text.get_input_media_web_page(), input_message_text.show_above_text,
             get_input_reply_markup(td_->contacts_manager_.get(), 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, int32 heading,
                                                        int32 proximity_alert_radius, Promise<Unit> &&promise) {
  CHECK(td_->auth_manager_->is_bot());

  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, "Invalid inline message identifier specified"));
  }

  Location location(input_location);
  if (location.empty() && input_location != nullptr) {
    return promise.set_error(Status::Error(400, "Invalid location specified"));
  }

  int32 flags = 0;
  if (location.empty()) {
    flags |= telegram_api::inputMediaGeoLive::STOPPED_MASK;
  }
  if (heading != 0) {
    flags |= telegram_api::inputMediaGeoLive::HEADING_MASK;
  }
  flags |= telegram_api::inputMediaGeoLive::PROXIMITY_NOTIFICATION_RADIUS_MASK;
  auto input_media = telegram_api::make_object<telegram_api::inputMediaGeoLive>(
      flags, false /*ignored*/, location.get_input_geo_point(), heading, 0, proximity_alert_radius);
  td_->create_handler<EditInlineMessageQuery>(std::move(promise))
      ->send(0, std::move(input_bot_inline_message_id), "", vector<tl_object_ptr<telegram_api::MessageEntity>>(),
             std::move(input_media), false,
             get_input_reply_markup(td_->contacts_manager_.get(), 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) {
  CHECK(td_->auth_manager_->is_bot());

  if (input_message_content == nullptr) {
    return promise.set_error(Status::Error(400, "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(400, "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.is_empty()) {
    return promise.set_error(Status::Error(400, "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, "Invalid inline message identifier specified"));
  }

  auto input_media = get_input_media(content.content.get(), td_, MessageSelfDestructType(), string(), true);
  if (input_media == nullptr) {
    return promise.set_error(Status::Error(400, "Invalid 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), content.invert_media,
             get_input_reply_markup(td_->contacts_manager_.get(), 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) {
  CHECK(td_->auth_manager_->is_bot());

  auto r_caption =
      get_formatted_text(td_, DialogId(), std::move(input_caption), td_->auth_manager_->is_bot(), true, false, false);
  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, "Invalid 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, false, get_input_reply_markup(td_->contacts_manager_.get(), 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) {
  CHECK(td_->auth_manager_->is_bot());

  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, "Invalid 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, false, get_input_reply_markup(td_->contacts_manager_.get(), r_new_reply_markup.ok()));
}

void MessagesManager::edit_message_scheduling_state(
    MessageFullId message_full_id, td_api::object_ptr<td_api::MessageSchedulingState> &&scheduling_state,
    Promise<Unit> &&promise) {
  auto r_schedule_date = get_message_schedule_date(std::move(scheduling_state));
  if (r_schedule_date.is_error()) {
    return promise.set_error(r_schedule_date.move_as_error());
  }
  auto schedule_date = r_schedule_date.move_as_ok();

  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog_force(dialog_id, "edit_message_scheduling_state");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Edit)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  Message *m = get_message_force(d, message_full_id.get_message_id(), "edit_message_scheduling_state");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }

  if (!m->message_id.is_scheduled()) {
    return promise.set_error(Status::Error(400, "Message is not scheduled"));
  }
  if (!m->message_id.is_scheduled_server()) {
    return promise.set_error(Status::Error(400, "Can't reschedule the message"));
  }

  if (get_message_schedule_date(m) == schedule_date) {
    return promise.set_value(Unit());
  }
  m->edited_schedule_date = schedule_date;

  if (schedule_date > 0) {
    td_->create_handler<EditMessageQuery>(std::move(promise))
        ->send(0, dialog_id, m->message_id, string(), vector<tl_object_ptr<telegram_api::MessageEntity>>(), nullptr,
               false, nullptr, schedule_date);
  } else {
    td_->create_handler<SendScheduledMessageQuery>(std::move(promise))->send(dialog_id, m->message_id);
  }
}

bool MessagesManager::is_discussion_message(DialogId dialog_id, const Message *m) const {
  if (m == nullptr || m->forward_info == nullptr) {
    return false;
  }
  if (m->sender_user_id.is_valid()) {
    if (!td_->auth_manager_->is_bot() || m->sender_user_id != ContactsManager::get_service_notifications_user_id()) {
      return false;
    }
  }
  auto linked_dialog_id = m->forward_info->get_last_dialog_id();
  if (linked_dialog_id.get_type() != DialogType::Channel) {
    return false;
  }
  if (dialog_id.get_type() != DialogType::Channel || td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
    return false;
  }
  if (linked_dialog_id == dialog_id) {
    return false;
  }
  return true;
}

bool MessagesManager::has_message_sender_user_id(DialogId dialog_id, const Message *m) const {
  if (!m->sender_user_id.is_valid()) {
    return false;
  }
  if (td_->auth_manager_->is_bot() && is_discussion_message(dialog_id, m)) {
    return false;
  }
  return true;
}

int32 MessagesManager::get_message_own_max_media_timestamp(const Message *m) const {
  auto duration = get_message_content_media_duration(m->content.get(), td_);
  return duration == 0 ? std::numeric_limits<int32>::max() : duration;
}

int32 MessagesManager::get_message_max_media_timestamp(const Message *m) {
  return m->max_own_media_timestamp >= 0 ? m->max_own_media_timestamp : m->max_reply_media_timestamp;
}

void MessagesManager::update_message_max_reply_media_timestamp(const Dialog *d, Message *m,
                                                               bool need_send_update_message_content) const {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto new_max_reply_media_timestamp = -1;
  auto reply_message_full_id = m->replied_message_info.get_reply_message_full_id(d->dialog_id, false);
  auto reply_message_id = reply_message_full_id.get_message_id();
  if (reply_message_id.is_valid() && !reply_message_id.is_yet_unsent()) {
    const auto *reply_d =
        reply_message_full_id.get_dialog_id() != d->dialog_id ? get_dialog(reply_message_full_id.get_dialog_id()) : d;
    if (reply_d == nullptr) {
      // replied message isn't loaded yet
      return;
    }
    auto replied_m = get_message(reply_d, reply_message_id);
    if (replied_m != nullptr) {
      new_max_reply_media_timestamp = get_message_own_max_media_timestamp(replied_m);
    } else if (!is_deleted_message(reply_d, reply_message_id) &&
               reply_message_id > reply_d->last_clear_history_message_id &&
               reply_message_id > reply_d->max_unavailable_message_id) {
      // replied message isn't deleted and isn't loaded yet
      return;
    }
  } else if (m->reply_to_story_full_id != StoryFullId()) {
    if (!td_->story_manager_->have_story(m->reply_to_story_full_id)) {
      if (!td_->story_manager_->is_inaccessible_story(m->reply_to_story_full_id)) {
        // replied story isn't loaded yet
        return;
      }
    } else {
      new_max_reply_media_timestamp = td_->story_manager_->get_story_duration(m->reply_to_story_full_id);
    }
  }

  if (m->max_reply_media_timestamp == new_max_reply_media_timestamp) {
    return;
  }

  LOG(INFO) << "Set max_reply_media_timestamp in " << m->message_id << " in " << d->dialog_id << " to "
            << new_max_reply_media_timestamp;
  auto old_max_media_timestamp = get_message_max_media_timestamp(m);
  m->max_reply_media_timestamp = new_max_reply_media_timestamp;
  auto new_max_media_timestamp = get_message_max_media_timestamp(m);
  if (need_send_update_message_content && old_max_media_timestamp != new_max_media_timestamp) {
    if (old_max_media_timestamp > new_max_media_timestamp) {
      std::swap(old_max_media_timestamp, new_max_media_timestamp);
    }

    if (has_media_timestamps(get_message_content_text(m->content.get()), old_max_media_timestamp + 1,
                             new_max_media_timestamp)) {
      send_update_message_content_impl(d->dialog_id, m, "update_message_max_reply_media_timestamp");
    }
  }
}

void MessagesManager::update_message_max_own_media_timestamp(const Dialog *d, Message *m) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto new_max_own_media_timestamp = get_message_own_max_media_timestamp(m);
  if (m->max_own_media_timestamp == new_max_own_media_timestamp) {
    return;
  }

  LOG(INFO) << "Set max_own_media_timestamp in " << m->message_id << " in " << d->dialog_id << " to "
            << new_max_own_media_timestamp;
  m->max_own_media_timestamp = new_max_own_media_timestamp;

  update_message_max_reply_media_timestamp_in_replied_messages(d->dialog_id, m->message_id);
}

void MessagesManager::update_message_max_reply_media_timestamp_in_replied_messages(DialogId dialog_id,
                                                                                   MessageId reply_to_message_id) {
  if (reply_to_message_id.is_scheduled()) {
    return;
  }
  CHECK(reply_to_message_id.is_valid());
  if (reply_to_message_id.is_yet_unsent()) {
    return;
  }

  MessageFullId message_full_id{dialog_id, reply_to_message_id};
  auto it = message_to_replied_media_timestamp_messages_.find(message_full_id);
  if (it == message_to_replied_media_timestamp_messages_.end()) {
    return;
  }

  LOG(INFO) << "Update max_reply_media_timestamp for replies of " << message_full_id;

  for (auto replied_message_full_id : it->second) {
    auto replied_dialog_id = replied_message_full_id.get_dialog_id();
    Dialog *d = get_dialog(replied_dialog_id);
    auto m = get_message(d, replied_message_full_id.get_message_id());
    CHECK(m != nullptr);
    CHECK(m->replied_message_info.get_reply_message_full_id(replied_dialog_id, false) == message_full_id);
    update_message_max_reply_media_timestamp(d, m, true);
  }
}

void MessagesManager::update_story_max_reply_media_timestamp_in_replied_messages(StoryFullId story_full_id) {
  auto it = story_to_replied_media_timestamp_messages_.find(story_full_id);
  if (it == story_to_replied_media_timestamp_messages_.end()) {
    return;
  }

  LOG(INFO) << "Update max_reply_media_timestamp for replies of " << story_full_id;

  for (auto replied_message_full_id : it->second) {
    auto replied_dialog_id = replied_message_full_id.get_dialog_id();
    Dialog *d = get_dialog(replied_dialog_id);
    auto m = get_message(d, replied_message_full_id.get_message_id());
    CHECK(m != nullptr);
    CHECK(m->reply_to_story_full_id == story_full_id);
    update_message_max_reply_media_timestamp(d, m, true);
  }
}

bool MessagesManager::can_register_message_reply(const Message *m) const {
  if (td_->auth_manager_->is_bot()) {
    return false;
  }
  auto reply_message_full_id = m->replied_message_info.get_reply_message_full_id(DialogId(), false);
  auto reply_message_id = reply_message_full_id.get_message_id();
  if (reply_message_id.is_valid() && !reply_message_id.is_yet_unsent()) {
    return true;
  }
  if (m->reply_to_story_full_id.is_valid()) {
    return true;
  }
  return false;
}

void MessagesManager::register_message_reply(DialogId dialog_id, const Message *m) {
  m->replied_message_info.register_content(td_);

  if (!can_register_message_reply(m)) {
    return;
  }

  if (has_media_timestamps(get_message_content_text(m->content.get()), 0, std::numeric_limits<int32>::max())) {
    if (m->reply_to_story_full_id.is_valid()) {
      LOG(INFO) << "Register " << m->message_id << " in " << dialog_id << " as reply to " << m->reply_to_story_full_id;
      bool is_inserted = story_to_replied_media_timestamp_messages_[m->reply_to_story_full_id]
                             .insert({dialog_id, m->message_id})
                             .second;
      CHECK(is_inserted);
    } else {
      auto message_full_id = m->replied_message_info.get_reply_message_full_id(dialog_id, false);
      LOG(INFO) << "Register " << m->message_id << " in " << dialog_id << " as reply to " << message_full_id;
      bool is_inserted =
          message_to_replied_media_timestamp_messages_[message_full_id].insert({dialog_id, m->message_id}).second;
      CHECK(is_inserted);
    }
  }
}

void MessagesManager::reregister_message_reply(DialogId dialog_id, const Message *m) {
  // reply itself wan't changed, so there is nothing to reregister
  if (!can_register_message_reply(m)) {
    return;
  }

  bool was_registered = false;
  if (m->reply_to_story_full_id.is_valid()) {
    auto it = story_to_replied_media_timestamp_messages_.find(m->reply_to_story_full_id);
    was_registered =
        it != story_to_replied_media_timestamp_messages_.end() && it->second.count({dialog_id, m->message_id}) > 0;
  } else {
    auto message_full_id = m->replied_message_info.get_reply_message_full_id(dialog_id, false);
    auto it = message_to_replied_media_timestamp_messages_.find(message_full_id);
    was_registered =
        it != message_to_replied_media_timestamp_messages_.end() && it->second.count({dialog_id, m->message_id}) > 0;
  }
  bool need_register =
      has_media_timestamps(get_message_content_text(m->content.get()), 0, std::numeric_limits<int32>::max());
  if (was_registered == need_register) {
    return;
  }
  if (was_registered) {
    unregister_message_reply(dialog_id, m);
  } else {
    register_message_reply(dialog_id, m);
  }
}

void MessagesManager::unregister_message_reply(DialogId dialog_id, const Message *m) {
  m->replied_message_info.unregister_content(td_);

  if (!can_register_message_reply(m)) {
    return;
  }

  if (m->reply_to_story_full_id.is_valid()) {
    auto it = story_to_replied_media_timestamp_messages_.find(m->reply_to_story_full_id);
    if (it == story_to_replied_media_timestamp_messages_.end()) {
      return;
    }

    auto is_deleted = it->second.erase({dialog_id, m->message_id}) > 0;
    if (is_deleted) {
      LOG(INFO) << "Unregister " << m->message_id << " in " << dialog_id << " as reply to "
                << m->reply_to_story_full_id;
      if (it->second.empty()) {
        story_to_replied_media_timestamp_messages_.erase(it);
      }
    }
  } else {
    auto message_full_id = m->replied_message_info.get_reply_message_full_id(dialog_id, false);
    auto it = message_to_replied_media_timestamp_messages_.find(message_full_id);
    if (it == message_to_replied_media_timestamp_messages_.end()) {
      return;
    }

    auto is_deleted = it->second.erase({dialog_id, m->message_id}) > 0;
    if (is_deleted) {
      LOG(INFO) << "Unregister " << m->message_id << " in " << dialog_id << " as reply to " << message_full_id;
      if (it->second.empty()) {
        message_to_replied_media_timestamp_messages_.erase(it);
      }
    }
  }
}

bool MessagesManager::get_message_disable_web_page_preview(const Message *m) {
  if (m->content->get_type() != MessageContentType::Text) {
    return false;
  }
  if (has_message_content_web_page(m->content.get())) {
    return false;
  }
  return m->disable_web_page_preview;
}

int32 MessagesManager::get_message_flags(const Message *m) {
  int32 flags = 0;
  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;
  }
  if (m->message_id.is_scheduled()) {
    flags |= SEND_MESSAGE_FLAG_HAS_SCHEDULE_DATE;
  }
  if (m->noforwards) {
    flags |= SEND_MESSAGE_FLAG_NOFORWARDS;
  }
  if (m->update_stickersets_order) {
    flags |= SEND_MESSAGE_FLAG_UPDATE_STICKER_SETS_ORDER;
  }
  if (m->invert_media) {
    flags |= SEND_MESSAGE_FLAG_INVERT_MEDIA;
  }
  return flags;
}

tl_object_ptr<telegram_api::InputPeer> MessagesManager::get_send_message_as_input_peer(const Message *m) const {
  if (!m->has_explicit_sender) {
    return nullptr;
  }
  return td_->dialog_manager_->get_input_peer(get_message_sender(m), AccessRights::Write);
}

bool MessagesManager::can_set_game_score(MessageFullId message_full_id) const {
  return can_set_game_score(message_full_id.get_dialog_id(), get_message(message_full_id));
}

bool MessagesManager::can_set_game_score(DialogId dialog_id, const Message *m) const {
  if (m == nullptr) {
    return false;
  }
  if (m->content->get_type() != MessageContentType::Game) {
    return false;
  }
  if (m->message_id.is_scheduled()) {
    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->via_bot_user_id != td_->contacts_manager_->get_my_id()) {
    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 != td_->dialog_manager_->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_permissions(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 true;
}

Result<unique_ptr<ReplyMarkup>> MessagesManager::get_dialog_reply_markup(
    DialogId dialog_id, tl_object_ptr<td_api::ReplyMarkup> &&reply_markup) const {
  return get_reply_markup(std::move(reply_markup), dialog_id, td_->auth_manager_->is_bot(),
                          td_->dialog_manager_->is_anonymous_administrator(dialog_id, nullptr));
}

class MessagesManager::ForwardMessagesLogEvent {
 public:
  DialogId to_dialog_id;
  DialogId from_dialog_id;
  vector<MessageId> message_ids;
  vector<Message *> messages_in;
  bool drop_author;
  bool drop_media_captions;
  vector<unique_ptr<Message>> messages_out;

  template <class StorerT>
  void store(StorerT &storer) const {
    BEGIN_STORE_FLAGS();
    STORE_FLAG(drop_author);
    STORE_FLAG(drop_media_captions);
    END_STORE_FLAGS();
    td::store(to_dialog_id, storer);
    td::store(from_dialog_id, storer);
    td::store(message_ids, storer);
    td::store(messages_in, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    if (parser.version() >= static_cast<int32>(Version::UseServerForwardAsCopy)) {
      BEGIN_PARSE_FLAGS();
      PARSE_FLAG(drop_author);
      PARSE_FLAG(drop_media_captions);
      END_PARSE_FLAGS();
    } else {
      drop_author = false;
      drop_media_captions = false;
    }
    td::parse(to_dialog_id, parser);
    td::parse(from_dialog_id, parser);
    td::parse(message_ids, parser);
    td::parse(messages_out, parser);
  }
};

uint64 MessagesManager::save_forward_messages_log_event(DialogId to_dialog_id, DialogId from_dialog_id,
                                                        const vector<Message *> &messages,
                                                        const vector<MessageId> &message_ids, bool drop_author,
                                                        bool drop_media_captions) {
  ForwardMessagesLogEvent log_event{to_dialog_id, from_dialog_id,      message_ids, messages,
                                    drop_author,  drop_media_captions, Auto()};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::ForwardMessages,
                    get_log_event_storer(log_event));
}

void MessagesManager::do_forward_messages(DialogId to_dialog_id, DialogId from_dialog_id,
                                          const vector<Message *> &messages, const vector<MessageId> &message_ids,
                                          bool drop_author, bool drop_media_captions, uint64 log_event_id) {
  if (G()->close_flag()) {
    return;
  }

  CHECK(messages.size() == message_ids.size());
  if (messages.empty()) {
    return;
  }

  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_forward_messages_log_event(to_dialog_id, from_dialog_id, messages, message_ids, drop_author,
                                                   drop_media_captions);
  }

  auto schedule_date = get_message_schedule_date(messages[0]);
  auto as_input_peer = get_send_message_as_input_peer(messages[0]);

  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]->in_game_share) {
    flags |= SEND_MESSAGE_FLAG_WITH_MY_SCORE;
  }
  if (schedule_date != 0) {
    flags |= SEND_MESSAGE_FLAG_HAS_SCHEDULE_DATE;
  }
  if (as_input_peer != nullptr) {
    flags |= SEND_MESSAGE_FLAG_HAS_SEND_AS;
  }
  if (messages[0]->noforwards) {
    flags |= SEND_MESSAGE_FLAG_NOFORWARDS;
  }
  if (drop_author) {
    flags |= telegram_api::messages_forwardMessages::DROP_AUTHOR_MASK;
  }
  if (drop_media_captions) {
    flags |= telegram_api::messages_forwardMessages::DROP_MEDIA_CAPTIONS_MASK;
  }

  vector<int64> random_ids =
      transform(messages, [this, to_dialog_id](const Message *m) { return begin_send_message(to_dialog_id, m); });
  send_closure_later(actor_id(this), &MessagesManager::send_forward_message_query, flags, to_dialog_id,
                     messages[0]->initial_top_thread_message_id, from_dialog_id, std::move(as_input_peer), message_ids,
                     std::move(random_ids), schedule_date, get_erase_log_event_promise(log_event_id));
}

void MessagesManager::send_forward_message_query(int32 flags, DialogId to_dialog_id,
                                                 const MessageId top_thread_message_id, DialogId from_dialog_id,
                                                 tl_object_ptr<telegram_api::InputPeer> as_input_peer,
                                                 vector<MessageId> message_ids, vector<int64> random_ids,
                                                 int32 schedule_date, Promise<Unit> promise) {
  td_->create_handler<ForwardMessagesQuery>(std::move(promise))
      ->send(flags, to_dialog_id, top_thread_message_id, from_dialog_id, std::move(as_input_peer), message_ids,
             std::move(random_ids), schedule_date);
}

Result<td_api::object_ptr<td_api::message>> MessagesManager::forward_message(
    DialogId to_dialog_id, MessageId top_thread_message_id, DialogId from_dialog_id, MessageId message_id,
    tl_object_ptr<td_api::messageSendOptions> &&options, bool in_game_share, MessageCopyOptions &&copy_options) {
  bool need_copy = copy_options.send_copy;
  vector<MessageCopyOptions> all_copy_options;
  all_copy_options.push_back(std::move(copy_options));
  TRY_RESULT(result, forward_messages(to_dialog_id, top_thread_message_id, from_dialog_id, {message_id},
                                      std::move(options), in_game_share, std::move(all_copy_options)));
  CHECK(result->messages_.size() == 1);
  if (result->messages_[0] == nullptr) {
    return Status::Error(400,
                         need_copy ? Slice("The message can't be copied") : Slice("The message can't be forwarded"));
  }
  return std::move(result->messages_[0]);
}

MessageOrigin MessagesManager::get_forwarded_message_origin(DialogId dialog_id, const Message *m) const {
  CHECK(m != nullptr);
  MessageOrigin origin;
  if (m->forward_info != nullptr) {
    origin = m->forward_info->get_origin();
  } else if (m->is_channel_post) {
    if (td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
      auto author_signature = m->sender_user_id.is_valid() ? td_->contacts_manager_->get_user_title(m->sender_user_id)
                                                           : m->author_signature;
      origin = MessageOrigin{UserId(), dialog_id, m->message_id, std::move(author_signature), string()};
    } else {
      LOG(ERROR) << "Don't know how to forward a channel post not from a channel";
    }
  } else if (m->sender_user_id.is_valid() || m->sender_dialog_id.is_valid()) {
    auto author_signature = m->author_signature;
    origin = MessageOrigin{m->sender_user_id, m->sender_dialog_id, MessageId(), string(), std::move(author_signature)};
  } else {
    LOG(ERROR) << "Don't know how to forward a non-channel post message without forward info and sender";
  }
  origin.hide_sender_if_needed(td_);
  return origin;
}

unique_ptr<MessageForwardInfo> MessagesManager::create_message_forward_info(DialogId from_dialog_id,
                                                                            DialogId to_dialog_id,
                                                                            const Message *m) const {
  auto content_type = m->content->get_type();
  if (content_type == MessageContentType::Game) {
    return nullptr;
  }

  auto my_dialog_id = td_->dialog_manager_->get_my_dialog_id();

  LastForwardedMessageInfo last_message_info;
  if (to_dialog_id == my_dialog_id) {
    last_message_info = LastForwardedMessageInfo(from_dialog_id, m->message_id, get_message_sender(m), string(),
                                                 m->date, m->is_outgoing);
  } else if (content_type == MessageContentType::Audio || content_type == MessageContentType::Story) {
    return nullptr;
  }

  if (m->forward_info != nullptr) {
    return MessageForwardInfo::copy_message_forward_info(td_, *m->forward_info, std::move(last_message_info));
  }

  if (from_dialog_id != my_dialog_id || content_type == MessageContentType::Dice) {
    auto origin = get_forwarded_message_origin(from_dialog_id, m);
    if (!origin.is_empty()) {
      last_message_info.hide_sender_if_needed(td_);
      if (last_message_info.get_dialog_id() != DialogId()) {
        last_message_info = LastForwardedMessageInfo(from_dialog_id, m->message_id, DialogId(), string(), 0,
                                                     m->is_outgoing && origin.get_sender() != my_dialog_id);
      }
      return td::make_unique<MessageForwardInfo>(std::move(origin), m->date, std::move(last_message_info), "", false);
    }
  }
  return nullptr;
}

void MessagesManager::fix_forwarded_message(Message *m, DialogId to_dialog_id, const Message *forwarded_message,
                                            int64 media_album_id, bool drop_author) const {
  if (m->content->get_type() == MessageContentType::Audio) {
    drop_author = true;
  }
  bool is_game = m->content->get_type() == MessageContentType::Game;
  if (!drop_author || is_game) {
    m->via_bot_user_id = forwarded_message->via_bot_user_id;
  }
  m->media_album_id = media_album_id;
  if (!drop_author && forwarded_message->view_count > 0 && m->forward_info != nullptr && m->view_count == 0 &&
      !(m->message_id.is_scheduled() && td_->dialog_manager_->is_broadcast_channel(to_dialog_id))) {
    m->view_count = forwarded_message->view_count;
    m->forward_count = forwarded_message->forward_count;
    m->interaction_info_update_date = G()->unix_time();
  }

  if (m->content->get_type() == MessageContentType::Game) {
    // via_bot_user_id in games is present unless the message is sent by the bot
    if (m->via_bot_user_id == UserId()) {
      // if there is no via_bot_user_id, then the original message was sent by the game owner
      m->via_bot_user_id = forwarded_message->sender_user_id;
    }
    if (m->via_bot_user_id == td_->contacts_manager_->get_my_id()) {
      // if via_bot_user_id is the current bot user, then there should be
      m->via_bot_user_id = UserId();
    }
  }
  if (forwarded_message->reply_markup != nullptr &&
      forwarded_message->reply_markup->type == ReplyMarkup::Type::InlineKeyboard &&
      to_dialog_id.get_type() != DialogType::SecretChat) {
    bool need_reply_markup = true;
    for (auto &row : forwarded_message->reply_markup->inline_keyboard) {
      for (auto &button : row) {
        if (button.type == InlineKeyboardButton::Type::Url || button.type == InlineKeyboardButton::Type::UrlAuth) {
          // ok
          continue;
        }
        if (m->via_bot_user_id.is_valid() && (button.type == InlineKeyboardButton::Type::SwitchInline ||
                                              button.type == InlineKeyboardButton::Type::SwitchInlineCurrentDialog)) {
          // ok
          continue;
        }

        need_reply_markup = false;
      }
    }
    if (need_reply_markup) {
      m->reply_markup = dup_reply_markup(forwarded_message->reply_markup);
      for (auto &row : m->reply_markup->inline_keyboard) {
        for (auto &button : row) {
          if (button.type == InlineKeyboardButton::Type::SwitchInlineCurrentDialog) {
            button.type = InlineKeyboardButton::Type::SwitchInline;
          }
          if (!button.forward_text.empty()) {
            button.text = std::move(button.forward_text);
            button.forward_text.clear();
          }
        }
      }
    }
  }
}

Result<MessagesManager::ForwardedMessages> MessagesManager::get_forwarded_messages(
    DialogId to_dialog_id, MessageId top_thread_message_id, DialogId from_dialog_id,
    const vector<MessageId> &message_ids, tl_object_ptr<td_api::messageSendOptions> &&options, bool in_game_share,
    vector<MessageCopyOptions> &&copy_options) {
  CHECK(copy_options.size() == message_ids.size());
  if (message_ids.size() > 100) {  // TODO replace with const from config or implement mass-forward
    return Status::Error(400, "Too many messages to forward");
  }
  if (message_ids.empty()) {
    return Status::Error(400, "There are no messages to forward");
  }

  Dialog *from_dialog = get_dialog_force(from_dialog_id, "forward_messages from");
  if (from_dialog == nullptr) {
    return Status::Error(400, "Chat to forward messages from not found");
  }
  if (!td_->dialog_manager_->have_input_peer(from_dialog_id, AccessRights::Read)) {
    return Status::Error(400, "Can't access the chat to forward messages from");
  }
  if (from_dialog_id.get_type() == DialogType::SecretChat) {
    return Status::Error(400, "Can't forward messages from secret chats");
  }
  if (td_->dialog_manager_->get_dialog_has_protected_content(from_dialog_id)) {
    for (const auto &copy_option : copy_options) {
      if (!copy_option.send_copy || !td_->auth_manager_->is_bot()) {
        return Status::Error(400, "Message has protected content and can't be forwarded");
      }
    }
  }

  Dialog *to_dialog = get_dialog_force(to_dialog_id, "forward_messages to");
  if (to_dialog == nullptr) {
    return Status::Error(400, "Chat to forward messages to not found");
  }

  TRY_STATUS(can_send_message(to_dialog_id));
  TRY_RESULT(message_send_options, process_message_send_options(to_dialog_id, std::move(options), false));
  TRY_STATUS(can_use_top_thread_message_id(to_dialog, top_thread_message_id, MessageInputReplyTo()));

  {
    MessageId last_message_id;
    for (auto message_id : message_ids) {
      if (message_id.is_valid_scheduled()) {
        return Status::Error(400, "Can't forward scheduled messages");
      }
      if (message_id.is_scheduled() || !message_id.is_valid()) {
        return Status::Error(400, "Invalid message identifier");
      }

      if (message_id <= last_message_id) {
        return Status::Error(400, "Message identifiers must be in a strictly increasing order");
      }
      last_message_id = message_id;
    }
  }

  bool to_secret = to_dialog_id.get_type() == DialogType::SecretChat;

  bool can_use_server_forward = true;
  for (auto &copy_option : copy_options) {
    if (!copy_option.is_supported_server_side()) {
      can_use_server_forward = false;
      break;
    }
  }
  CHECK(can_use_server_forward || copy_options.size() == 1);
  if (to_secret) {
    can_use_server_forward = false;
  }

  ForwardedMessages result;
  result.to_dialog = to_dialog;
  result.from_dialog = from_dialog;
  result.message_send_options = message_send_options;
  auto &copied_messages = result.copied_messages;
  auto &forwarded_message_contents = result.forwarded_message_contents;
  result.drop_author = can_use_server_forward && copy_options[0].send_copy;
  result.drop_media_captions = can_use_server_forward && copy_options[0].replace_caption;

  std::unordered_map<int64, std::pair<int64, int32>, Hash<int64>> new_copied_media_album_ids;
  std::unordered_map<int64, std::pair<int64, int32>, Hash<int64>> new_forwarded_media_album_ids;

  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, "get_forwarded_messages");
    if (forwarded_message == nullptr) {
      LOG(INFO) << "Can't find " << message_id << " to forward";
      continue;
    }
    CHECK(message_id.is_valid());
    CHECK(message_id == forwarded_message->message_id);

    if (!can_forward_message(from_dialog_id, forwarded_message)) {
      LOG(INFO) << "Can't forward " << message_id;
      continue;
    }

    bool is_broken_server_copy = [&] {
      switch (forwarded_message->content->get_type()) {
        case MessageContentType::Dice:
          return true;
        default:
          return false;
      }
    }();

    bool need_copy = !message_id.is_server() || to_secret || copy_options[i].send_copy;
    bool is_local_copy = need_copy && !(message_id.is_server() && can_use_server_forward && !is_broken_server_copy);
    if (!(need_copy && td_->auth_manager_->is_bot()) && !can_save_message(from_dialog_id, forwarded_message)) {
      LOG(INFO) << "Forward of " << message_id << " is restricted";
      continue;
    }

    auto type = need_copy ? (is_local_copy ? MessageContentDupType::Copy : MessageContentDupType::ServerCopy)
                          : MessageContentDupType::Forward;
    auto input_reply_to = std::move(copy_options[i].input_reply_to);
    auto reply_markup = std::move(copy_options[i].reply_markup);
    unique_ptr<MessageContent> content =
        dup_message_content(td_, to_dialog_id, forwarded_message->content.get(), type, std::move(copy_options[i]));
    if (content == nullptr) {
      LOG(INFO) << "Can't forward content of " << message_id;
      continue;
    }

    auto can_send_status = can_send_message_content(to_dialog_id, content.get(), !is_local_copy, true, td_);
    if (can_send_status.is_error()) {
      LOG(INFO) << "Can't forward " << message_id << ": " << can_send_status.message();
      continue;
    }

    auto can_use_options_status =
        can_use_message_send_options(message_send_options, content, MessageSelfDestructType());
    if (can_use_options_status.is_error()) {
      LOG(INFO) << "Can't forward " << message_id << ": " << can_send_status.message();
      continue;
    }

    if (can_use_top_thread_message_id(to_dialog, top_thread_message_id, input_reply_to).is_error()) {
      LOG(INFO) << "Ignore invalid message thread ID " << top_thread_message_id;
      top_thread_message_id = MessageId();
    }

    if (forwarded_message->media_album_id != 0) {
      auto &new_media_album_id = is_local_copy ? new_copied_media_album_ids[forwarded_message->media_album_id]
                                               : new_forwarded_media_album_ids[forwarded_message->media_album_id];
      new_media_album_id.second++;
      if (new_media_album_id.second == 2) {  // have at least 2 messages in the new album
        CHECK(new_media_album_id.first == 0);
        new_media_album_id.first = generate_new_media_album_id();
      }
      if (new_media_album_id.second == MAX_GROUPED_MESSAGES + 1) {
        CHECK(new_media_album_id.first != 0);
        new_media_album_id.first = 0;  // just in case
      }
    }

    if (is_local_copy) {
      auto original_reply_to_message_id =
          forwarded_message->replied_message_info.get_same_chat_reply_to_message_id(true);
      copied_messages.push_back(
          {std::move(content), std::move(input_reply_to), forwarded_message->message_id, original_reply_to_message_id,
           std::move(reply_markup), forwarded_message->media_album_id,
           get_message_disable_web_page_preview(forwarded_message), forwarded_message->invert_media, i});
    } else {
      forwarded_message_contents.push_back(
          {std::move(content), forwarded_message->invert_media, forwarded_message->media_album_id, i});
    }
  }
  result.top_thread_message_id = top_thread_message_id;

  if (2 <= forwarded_message_contents.size() && forwarded_message_contents.size() <= MAX_GROUPED_MESSAGES) {
    std::unordered_set<MessageContentType, MessageContentTypeHash> message_content_types;
    std::unordered_set<DialogId, DialogIdHash> sender_dialog_ids;
    for (auto &message_content : forwarded_message_contents) {
      message_content_types.insert(message_content.content->get_type());

      MessageId message_id = get_persistent_message_id(from_dialog, message_ids[message_content.index]);
      sender_dialog_ids.insert(get_message_original_sender(get_message(from_dialog, message_id)));
    }
    if (message_content_types.size() == 1 && is_homogenous_media_group_content(*message_content_types.begin()) &&
        sender_dialog_ids.size() == 1 && *sender_dialog_ids.begin() != DialogId()) {
      new_forwarded_media_album_ids[0].first = generate_new_media_album_id();
      for (auto &message : forwarded_message_contents) {
        message.media_album_id = 0;
      }
    }
  }
  for (auto &message : forwarded_message_contents) {
    message.media_album_id = new_forwarded_media_album_ids[message.media_album_id].first;
  }

  if (2 <= copied_messages.size() && copied_messages.size() <= MAX_GROUPED_MESSAGES) {
    std::unordered_set<MessageContentType, MessageContentTypeHash> message_content_types;
    for (auto &copied_message : copied_messages) {
      message_content_types.insert(copied_message.content->get_type());
    }
    if (message_content_types.size() == 1 && is_homogenous_media_group_content(*message_content_types.begin())) {
      new_copied_media_album_ids[0].first = generate_new_media_album_id();
      for (auto &message : copied_messages) {
        message.media_album_id = 0;
      }
    }
  }
  for (auto &message : copied_messages) {
    message.media_album_id = new_copied_media_album_ids[message.media_album_id].first;
  }
  return std::move(result);
}

Result<td_api::object_ptr<td_api::messages>> MessagesManager::forward_messages(
    DialogId to_dialog_id, MessageId top_thread_message_id, DialogId from_dialog_id, vector<MessageId> message_ids,
    tl_object_ptr<td_api::messageSendOptions> &&options, bool in_game_share,
    vector<MessageCopyOptions> &&copy_options) {
  TRY_RESULT(forwarded_messages_info,
             get_forwarded_messages(to_dialog_id, top_thread_message_id, from_dialog_id, message_ids,
                                    std::move(options), in_game_share, std::move(copy_options)));
  auto from_dialog = forwarded_messages_info.from_dialog;
  auto to_dialog = forwarded_messages_info.to_dialog;
  const auto message_send_options = forwarded_messages_info.message_send_options;
  auto &copied_messages = forwarded_messages_info.copied_messages;
  auto &forwarded_message_contents = forwarded_messages_info.forwarded_message_contents;
  auto drop_author = forwarded_messages_info.drop_author;
  auto drop_media_captions = forwarded_messages_info.drop_media_captions;
  top_thread_message_id = forwarded_messages_info.top_thread_message_id;

  FlatHashMap<MessageId, MessageId, MessageIdHash> forwarded_message_id_to_new_message_id;
  vector<td_api::object_ptr<td_api::message>> result(message_ids.size());
  vector<Message *> forwarded_messages;
  vector<MessageId> forwarded_message_ids;
  bool need_update_dialog_pos = false;
  for (size_t j = 0; j < forwarded_message_contents.size(); j++) {
    MessageId message_id = get_persistent_message_id(from_dialog, message_ids[forwarded_message_contents[j].index]);
    const Message *forwarded_message = get_message(from_dialog, message_id);
    CHECK(forwarded_message != nullptr);

    auto content = std::move(forwarded_message_contents[j].content);
    auto forward_info =
        drop_author ? nullptr : create_message_forward_info(from_dialog_id, to_dialog_id, forwarded_message);
    MessageInputReplyTo input_reply_to;
    auto original_reply_to_message_id = forwarded_message->replied_message_info.get_same_chat_reply_to_message_id(true);
    if (original_reply_to_message_id.is_valid()) {
      auto it = forwarded_message_id_to_new_message_id.find(original_reply_to_message_id);
      if (it != forwarded_message_id_to_new_message_id.end()) {
        input_reply_to = forwarded_message->replied_message_info.get_input_reply_to();
        input_reply_to.set_message_id(it->second);
      }
    }

    unique_ptr<Message> message;
    Message *m;
    if (message_send_options.only_preview) {
      message = create_message_to_send(
          to_dialog, top_thread_message_id, std::move(input_reply_to), message_send_options, std::move(content),
          forwarded_message_contents[j].invert_media, j + 1 != forwarded_message_contents.size(),
          std::move(forward_info), from_dialog_id, false, DialogId());
      m = message.get();
    } else {
      m = get_message_to_send(to_dialog, top_thread_message_id, std::move(input_reply_to), message_send_options,
                              std::move(content), forwarded_message_contents[j].invert_media, &need_update_dialog_pos,
                              j + 1 != forwarded_message_contents.size(), std::move(forward_info), from_dialog_id);
    }
    fix_forwarded_message(m, to_dialog_id, forwarded_message, forwarded_message_contents[j].media_album_id,
                          drop_author);
    m->in_game_share = in_game_share;
    m->real_forward_from_message_id = message_id;
    forwarded_message_id_to_new_message_id.emplace(message_id, m->message_id);
    if (forwarded_message->replied_message_info.is_external()) {
      if (!message_send_options.only_preview) {
        unregister_message_reply(to_dialog_id, m);
      }
      m->replied_message_info = forwarded_message->replied_message_info.clone(td_);
      if (!message_send_options.only_preview) {
        register_message_reply(to_dialog_id, m);
      }
    }

    if (!message_send_options.only_preview) {
      if (!td_->auth_manager_->is_bot()) {
        send_update_new_message(to_dialog, m);
      }
      forwarded_messages.push_back(m);
      forwarded_message_ids.push_back(message_id);
    }

    result[forwarded_message_contents[j].index] = get_message_object(to_dialog_id, m, "forward_messages");
  }

  if (!forwarded_messages.empty()) {
    CHECK(!message_send_options.only_preview);
    do_forward_messages(to_dialog_id, from_dialog_id, forwarded_messages, forwarded_message_ids, drop_author,
                        drop_media_captions, 0);
  }

  bool is_secret = to_dialog_id.get_type() == DialogType::SecretChat;
  bool is_copy = !is_secret;
  bool need_invalidate_authentication_code =
      from_dialog_id == DialogId(ContactsManager::get_service_notifications_user_id()) && !td_->auth_manager_->is_bot();
  vector<string> authentication_codes;
  for (const auto &copied_message : copied_messages) {
    if (forwarded_message_id_to_new_message_id.count(copied_message.original_reply_to_message_id) > 0) {
      is_copy = true;
      break;
    }
    forwarded_message_id_to_new_message_id.emplace(copied_message.original_message_id, MessageId());
  }
  for (auto &copied_message : copied_messages) {
    auto input_reply_to = std::move(copied_message.input_reply_to);
    if (!input_reply_to.is_valid() && copied_message.original_reply_to_message_id.is_valid() && is_secret) {
      auto it = forwarded_message_id_to_new_message_id.find(copied_message.original_reply_to_message_id);
      if (it != forwarded_message_id_to_new_message_id.end()) {
        input_reply_to = MessageInputReplyTo{it->second, DialogId(), FormattedText(), 0};
      }
    }

    unique_ptr<Message> message;
    Message *m;
    if (message_send_options.only_preview) {
      message = create_message_to_send(to_dialog, top_thread_message_id, std::move(input_reply_to),
                                       message_send_options, std::move(copied_message.content),
                                       copied_message.invert_media, false, nullptr, DialogId(), is_copy, DialogId());
      m = message.get();
    } else {
      if (need_invalidate_authentication_code) {
        const Message *forwarded_message = get_message(from_dialog, copied_message.original_message_id);
        CHECK(forwarded_message != nullptr);
        extract_authentication_codes(from_dialog_id, forwarded_message, authentication_codes);
      }

      m = get_message_to_send(to_dialog, top_thread_message_id, std::move(input_reply_to), message_send_options,
                              std::move(copied_message.content), copied_message.invert_media, &need_update_dialog_pos,
                              false, nullptr, DialogId(), is_copy);
    }
    m->disable_web_page_preview = copied_message.disable_web_page_preview;
    m->media_album_id = copied_message.media_album_id;
    m->reply_markup = std::move(copied_message.reply_markup);
    forwarded_message_id_to_new_message_id[copied_message.original_message_id] = m->message_id;

    if (!message_send_options.only_preview) {
      save_send_message_log_event(to_dialog_id, m);
      do_send_message(to_dialog_id, m);
      if (!td_->auth_manager_->is_bot()) {
        send_update_new_message(to_dialog, m);
      }
    }

    result[copied_message.index] = get_message_object(to_dialog_id, m, "forward_messages");
  }

  if (need_update_dialog_pos) {
    CHECK(!message_send_options.only_preview);
    send_update_chat_last_message(to_dialog, "forward_messages");
  }

  if (!authentication_codes.empty()) {
    td_->account_manager_->invalidate_authentication_codes(std::move(authentication_codes));
  }

  return get_messages_object(-1, std::move(result), false);
}

Result<td_api::object_ptr<td_api::messages>> MessagesManager::send_quick_reply_shortcut_messages(
    DialogId dialog_id, QuickReplyShortcutId shortcut_id, int32 sending_id) {
  TRY_RESULT(message_contents, td_->quick_reply_manager_->get_quick_reply_message_contents(dialog_id, shortcut_id));

  if (message_contents.empty()) {
    return td_api::object_ptr<td_api::messages>();
  }

  std::unordered_map<int64, std::pair<int64, int32>, Hash<int64>> new_media_album_ids;
  for (auto &content : message_contents) {
    if (content.media_album_id_ == 0) {
      continue;
    }
    auto &new_media_album_id = new_media_album_ids[content.media_album_id_];
    new_media_album_id.second++;
    if (new_media_album_id.second == 2) {  // have at least 2 messages in the new album
      CHECK(new_media_album_id.first == 0);
      new_media_album_id.first = generate_new_media_album_id();
    }
    if (new_media_album_id.second == MAX_GROUPED_MESSAGES + 1) {
      CHECK(new_media_album_id.first != 0);
      new_media_album_id.first = 0;  // just in case
    }
  }
  for (auto &content : message_contents) {
    content.media_album_id_ = new_media_album_ids[content.media_album_id_].first;
  }

  auto *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  MessageSendOptions message_send_options(false, false, false, false, false, 0, sending_id);
  FlatHashMap<MessageId, MessageId, MessageIdHash> original_message_id_to_new_message_id;
  vector<td_api::object_ptr<td_api::message>> result;
  vector<Message *> sent_messages;
  vector<MessageId> sent_message_ids;
  bool need_update_dialog_pos = false;
  for (auto &content : message_contents) {
    MessageInputReplyTo input_reply_to;
    if (content.original_reply_to_message_id_.is_valid()) {
      auto it = original_message_id_to_new_message_id.find(content.original_reply_to_message_id_);
      if (it != original_message_id_to_new_message_id.end()) {
        input_reply_to = MessageInputReplyTo{it->second, DialogId(), FormattedText(), 0};
      }
    }

    Message *m = get_message_to_send(d, MessageId(), std::move(input_reply_to), message_send_options,
                                     std::move(content.content_), content.invert_media_, &need_update_dialog_pos, false,
                                     nullptr, DialogId(), true);
    m->disable_web_page_preview = content.disable_web_page_preview_;
    m->media_album_id = content.media_album_id_;
    original_message_id_to_new_message_id.emplace(content.original_message_id_, m->message_id);

    if (!td_->auth_manager_->is_bot()) {
      send_update_new_message(d, m);
    }
    sent_messages.push_back(m);
    sent_message_ids.push_back(m->message_id);

    result.push_back(get_message_object(dialog_id, m, "send_quick_reply_shortcut_messages"));
  }

  do_send_quick_reply_shortcut_messages(dialog_id, shortcut_id, sent_messages, sent_message_ids, 0);

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

  return get_messages_object(-1, std::move(result), false);
}

void MessagesManager::do_send_quick_reply_shortcut_messages(DialogId dialog_id, QuickReplyShortcutId shortcut_id,
                                                            const vector<Message *> &messages,
                                                            const vector<MessageId> &message_ids, uint64 log_event_id) {
  if (G()->close_flag()) {
    return;
  }

  CHECK(messages.size() == message_ids.size());
  if (messages.empty()) {
    return;
  }

  if (log_event_id == 0 && G()->use_message_database()) {
    // log_event_id = save_send_quick_reply_shortcut_messages_log_event(dialog_id, shprtcut_id, messages, message_ids);
  }

  vector<int64> random_ids =
      transform(messages, [this, dialog_id](const Message *m) { return begin_send_message(dialog_id, m); });
  send_closure_later(actor_id(this), &MessagesManager::send_send_quick_reply_messages_query, dialog_id, shortcut_id,
                     message_ids, std::move(random_ids), get_erase_log_event_promise(log_event_id));
}

void MessagesManager::send_send_quick_reply_messages_query(DialogId dialog_id, QuickReplyShortcutId shortcut_id,
                                                           vector<MessageId> message_ids, vector<int64> random_ids,
                                                           Promise<Unit> promise) {
  td_->create_handler<SendQuickReplyMessagesQuery>(std::move(promise))
      ->send(dialog_id, shortcut_id, std::move(message_ids), std::move(random_ids));
}

Result<vector<MessageId>> MessagesManager::resend_messages(DialogId dialog_id, vector<MessageId> message_ids,
                                                           td_api::object_ptr<td_api::inputTextQuote> &&quote) {
  if (message_ids.empty()) {
    return Status::Error(400, "There are no messages to resend");
  }

  Dialog *d = get_dialog_force(dialog_id, "resend_messages");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }

  TRY_STATUS(can_send_message(dialog_id));

  MessageId last_message_id;
  for (auto &message_id : message_ids) {
    message_id = get_persistent_message_id(d, message_id);
    const Message *m = get_message_force(d, message_id, "resend_messages");
    if (m == nullptr) {
      return Status::Error(400, "Message not found");
    }
    if (!m->is_failed_to_send) {
      return Status::Error(400, "Message is not failed to send");
    }
    if (!can_resend_message(m)) {
      return Status::Error(400, "Message can't be re-sent");
    }
    if (m->try_resend_at > Time::now()) {
      return Status::Error(400, "Message can't be re-sent yet");
    }
    if (last_message_id != MessageId()) {
      if (m->message_id.is_scheduled() != last_message_id.is_scheduled()) {
        return Status::Error(400, "Messages must be all scheduled or ordinary");
      }
      if (m->message_id <= last_message_id) {
        return Status::Error(400, "Message identifiers must be in a strictly increasing order");
      }
    }
    last_message_id = m->message_id;
  }

  vector<unique_ptr<MessageContent>> new_contents(message_ids.size());
  std::unordered_map<int64, std::pair<int64, int32>, Hash<int64>> new_media_album_ids;
  for (size_t i = 0; i < message_ids.size(); i++) {
    MessageId message_id = message_ids[i];
    const Message *m = get_message(d, message_id);
    CHECK(m != nullptr);

    unique_ptr<MessageContent> content =
        dup_message_content(td_, dialog_id, m->content.get(), MessageContentDupType::Send, MessageCopyOptions());
    if (content == nullptr) {
      LOG(INFO) << "Can't resend " << m->message_id;
      continue;
    }

    auto can_send_status = can_send_message_content(dialog_id, content.get(), false, true, td_);
    if (can_send_status.is_error()) {
      LOG(INFO) << "Can't resend " << m->message_id << ": " << can_send_status.message();
      continue;
    }

    new_contents[i] = std::move(content);

    if (m->media_album_id != 0) {
      auto &new_media_album_id = new_media_album_ids[m->media_album_id];
      new_media_album_id.second++;
      if (new_media_album_id.second == 2) {  // have at least 2 messages in the new album
        CHECK(new_media_album_id.first == 0);
        new_media_album_id.first = generate_new_media_album_id();
      }
      if (new_media_album_id.second == MAX_GROUPED_MESSAGES + 1) {
        CHECK(new_media_album_id.first != 0);
        new_media_album_id.first = 0;  // just in case
      }
    }
  }

  vector<MessageId> result(message_ids.size());
  bool need_update_dialog_pos = false;
  for (size_t i = 0; i < message_ids.size(); i++) {
    if (new_contents[i] == nullptr) {
      continue;
    }

    being_readded_message_id_ = {dialog_id, message_ids[i]};
    auto message = delete_message(d, message_ids[i], true, &need_update_dialog_pos, "resend_messages");
    CHECK(message != nullptr);
    send_update_delete_messages(dialog_id, {message->message_id.get()}, true);

    auto need_another_sender =
        message->send_error_code == 400 && message->send_error_message == CSlice("SEND_AS_PEER_INVALID");
    auto need_another_reply_quote =
        message->send_error_code == 400 && message->send_error_message == CSlice("QUOTE_TEXT_INVALID");
    auto need_drop_reply =
        message->send_error_code == 400 && message->send_error_message == CSlice("REPLY_MESSAGE_ID_INVALID");
    if (need_another_reply_quote && message_ids.size() == 1 && quote != nullptr) {
      CHECK(message->input_reply_to.is_valid());
      CHECK(message->input_reply_to.has_quote());  // checked in on_send_message_fail
      auto r_quote = get_formatted_text(td_, td_->dialog_manager_->get_my_dialog_id(), std::move(quote->text_),
                                        td_->auth_manager_->is_bot(), true, true, true);
      if (r_quote.is_ok()) {
        message->input_reply_to.set_quote(r_quote.move_as_ok(), quote->position_);
      }
    } else if (need_drop_reply) {
      message->input_reply_to = {};
    }
    MessageSendOptions options(message->disable_notification, message->from_background,
                               message->update_stickersets_order, message->noforwards, false,
                               get_message_schedule_date(message.get()), message->sending_id);
    Message *m = get_message_to_send(d, message->top_thread_message_id, std::move(message->input_reply_to), options,
                                     std::move(new_contents[i]), message->invert_media, &need_update_dialog_pos, false,
                                     nullptr, DialogId(), message->is_copy,
                                     need_another_sender ? DialogId() : get_message_sender(message.get()));
    m->reply_markup = std::move(message->reply_markup);
    // m->via_bot_user_id = message->via_bot_user_id;
    m->disable_web_page_preview = message->disable_web_page_preview;
    m->clear_draft = false;  // never clear draft in resend
    m->ttl = message->ttl;
    m->is_content_secret = message->is_content_secret;
    m->media_album_id = new_media_album_ids[message->media_album_id].first;
    m->send_emoji = message->send_emoji;
    m->has_explicit_sender |= message->has_explicit_sender;

    save_send_message_log_event(dialog_id, m);
    do_send_message(dialog_id, m);

    send_update_new_message(d, m);

    result[i] = m->message_id;
    being_readded_message_id_ = MessageFullId();
  }

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

  return result;
}

void MessagesManager::send_screenshot_taken_notification_message(Dialog *d) {
  LOG(INFO) << "Begin to send notification about taken screenshot in " << d->dialog_id;
  auto dialog_type = d->dialog_id.get_type();
  if (dialog_type == DialogType::User) {
    bool need_update_dialog_pos = false;
    const Message *m = get_message_to_send(d, MessageId(), MessageInputReplyTo(), MessageSendOptions(),
                                           create_screenshot_taken_message_content(), false, &need_update_dialog_pos);

    do_send_screenshot_taken_notification_message(d->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 {
    CHECK(dialog_type == DialogType::SecretChat);
    send_closure(td_->secret_chats_manager_, &SecretChatsManager::notify_screenshot_taken,
                 d->dialog_id.get_secret_chat_id(), Promise<Unit>());
  }
}

class MessagesManager::SendScreenshotTakenNotificationMessageLogEvent {
 public:
  DialogId dialog_id;
  const Message *m_in = nullptr;
  unique_ptr<Message> message_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);
    td::parse(message_out, parser);
  }
};

uint64 MessagesManager::save_send_screenshot_taken_notification_message_log_event(DialogId dialog_id,
                                                                                  const Message *m) {
  if (!G()->use_message_database()) {
    return 0;
  }

  CHECK(m != nullptr);
  LOG(INFO) << "Save " << MessageFullId(dialog_id, m->message_id) << " to binlog";
  SendScreenshotTakenNotificationMessageLogEvent log_event;
  log_event.dialog_id = dialog_id;
  log_event.m_in = m;
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::SendScreenshotTakenNotificationMessage,
                    get_log_event_storer(log_event));
}

void MessagesManager::do_send_screenshot_taken_notification_message(DialogId dialog_id, const Message *m,
                                                                    uint64 log_event_id) {
  LOG(INFO) << "Do send screenshot taken notification " << MessageFullId(dialog_id, m->message_id);
  CHECK(dialog_id.get_type() == DialogType::User);

  if (log_event_id == 0) {
    log_event_id = save_send_screenshot_taken_notification_message_log_event(dialog_id, m);
  }

  int64 random_id = begin_send_message(dialog_id, m);
  td_->create_handler<SendScreenshotNotificationQuery>(get_erase_log_event_promise(log_event_id))
      ->send(dialog_id, random_id);
}

void MessagesManager::share_dialogs_with_bot(MessageFullId message_full_id, int32 button_id,
                                             vector<DialogId> shared_dialog_ids, bool expect_user, bool only_check,
                                             Promise<Unit> &&promise) {
  const Message *m = get_message_force(message_full_id, "share_dialog_with_bot");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }
  if (m->reply_markup == nullptr) {
    return promise.set_error(Status::Error(400, "Message has no buttons"));
  }
  CHECK(m->message_id.is_valid() && m->message_id.is_server());
  TRY_STATUS_PROMISE(promise, m->reply_markup->check_shared_dialog_count(button_id, shared_dialog_ids.size()));
  for (auto shared_dialog_id : shared_dialog_ids) {
    if (shared_dialog_id.get_type() != DialogType::User) {
      if (!have_dialog_force(shared_dialog_id, "share_dialogs_with_bot")) {
        return promise.set_error(Status::Error(400, "Shared chat not found"));
      }
    } else {
      if (!expect_user) {
        return promise.set_error(Status::Error(400, "Wrong chat type"));
      }
      if (!td_->contacts_manager_->have_user(shared_dialog_id.get_user_id())) {
        return promise.set_error(Status::Error(400, "Shared user not found"));
      }
    }
    TRY_STATUS_PROMISE(promise, m->reply_markup->check_shared_dialog(td_, button_id, shared_dialog_id));
  }

  if (only_check) {
    return promise.set_value(Unit());
  }

  td_->create_handler<SendBotRequestedPeerQuery>(std::move(promise))
      ->send(message_full_id, button_id, std::move(shared_dialog_ids));
}

Result<MessageId> MessagesManager::add_local_message(
    DialogId dialog_id, td_api::object_ptr<td_api::MessageSender> &&sender,
    td_api::object_ptr<td_api::InputMessageReplyTo> &&reply_to, bool disable_notification,
    tl_object_ptr<td_api::InputMessageContent> &&input_message_content) {
  if (input_message_content == nullptr) {
    return Status::Error(400, "Can't add local message without content");
  }

  Dialog *d = get_dialog_force(dialog_id, "add_local_message");
  if (d == nullptr) {
    return Status::Error(400, "Chat not found");
  }

  if (!td_->dialog_manager_->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), false));
  if (message_content.content->get_type() == MessageContentType::Poll) {
    return Status::Error(400, "Can't add local poll message");
  }
  if (message_content.content->get_type() == MessageContentType::Game) {
    return Status::Error(400, "Can't add local game message");
  }
  if (message_content.content->get_type() == MessageContentType::Dice) {
    return Status::Error(400, "Can't add local dice message");
  }

  bool is_channel_post = td_->dialog_manager_->is_broadcast_channel(dialog_id);
  UserId sender_user_id;
  DialogId sender_dialog_id;
  if (sender != nullptr) {
    TRY_RESULT_ASSIGN(sender_dialog_id, get_message_sender_dialog_id(td_, sender, true, false));
    auto sender_dialog_type = sender_dialog_id.get_type();
    if (sender_dialog_type == DialogType::User) {
      sender_user_id = sender_dialog_id.get_user_id();
      sender_dialog_id = DialogId();
    } else if (sender_dialog_type != DialogType::Channel) {
      return Status::Error(400, "Sender chat must be a supergroup or channel");
    }
  } else if (is_channel_post) {
    sender_dialog_id = dialog_id;
  } else {
    return Status::Error(400, "The message must have a sender");
  }
  if (is_channel_post && sender_user_id.is_valid()) {
    return Status::Error(400, "Channel post can't have user as a sender");
  }
  if (is_channel_post && sender_dialog_id != dialog_id) {
    return Status::Error(400, "Channel post must have the channel as a sender");
  }

  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) {
      auto peer_user_id = td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id());
      if (!peer_user_id.is_valid() || sender_user_id != peer_user_id) {
        return Status::Error(400, "Wrong sender user");
      }
    }
  }

  auto input_reply_to = get_message_input_reply_to(d, MessageId(), std::move(reply_to), false);

  MessageId message_id = get_next_local_message_id(d);

  auto message = make_unique<Message>();
  auto *m = message.get();
  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);
    }
    m->sender_dialog_id = sender_dialog_id;
  } else {
    m->sender_user_id = sender_user_id;
    m->sender_dialog_id = sender_dialog_id;
  }
  m->date = G()->unix_time();
  m->replied_message_info = RepliedMessageInfo(td_, input_reply_to);
  m->reply_to_story_full_id = input_reply_to.get_story_full_id();
  if (!message_id.is_scheduled()) {
    auto reply_to_message_id = input_reply_to.get_same_chat_reply_to_message_id();
    const Message *reply_m = get_message(d, reply_to_message_id);
    if (reply_m != nullptr) {
      m->top_thread_message_id = reply_m->top_thread_message_id;
      if (m->top_thread_message_id.is_valid()) {
        m->is_topic_message = reply_m->is_topic_message;
      }
    }
  }
  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->update_stickersets_order = false;
  m->view_count = 0;
  m->forward_count = 0;
  if (m->sender_user_id == my_id && dialog_type == DialogType::Channel) {
    m->sender_boost_count = td_->contacts_manager_->get_channel_my_boost_count(dialog_id.get_channel_id());
  }
  m->content = std::move(message_content.content);
  m->invert_media = message_content.invert_media;
  m->disable_web_page_preview = message_content.disable_web_page_preview;
  m->clear_draft = message_content.clear_draft;
  if (dialog_type == DialogType::SecretChat) {
    if (!is_service_message_content(m->content->get_type())) {
      m->ttl =
          MessageSelfDestructType(td_->contacts_manager_->get_secret_chat_ttl(dialog_id.get_secret_chat_id()), false);
    }
  } else if (message_content.ttl.is_valid()) {
    m->ttl = message_content.ttl;
  }
  m->is_content_secret = m->ttl.is_secret_message_content(m->content->get_type());
  m->send_emoji = std::move(message_content.emoji);
  if (dialog_id == DialogId(my_id)) {
    m->saved_messages_topic_id = SavedMessagesTopicId(dialog_id, m->forward_info.get(), DialogId());
  }

  bool need_update = true;
  bool need_update_dialog_pos = false;
  auto result = add_message_to_dialog(d, std::move(message), false, true, &need_update, &need_update_dialog_pos,
                                      "add local message");
  LOG_CHECK(result != nullptr) << message_id << " " << debug_add_message_to_dialog_fail_reason_;
  register_new_local_message_id(d, result);

  if (is_message_auto_read(dialog_id, result->is_outgoing)) {
    if (result->is_outgoing) {
      read_history_outbox(d, message_id);
    } else {
      read_history_inbox(d, message_id, 0, "add_local_message");
    }
  }

  clear_dialog_draft_by_sent_message(d, result, !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 char *source) {
  if (!new_message_id.is_valid() && !new_message_id.is_valid_scheduled()) {
    LOG(ERROR) << "Receive " << new_message_id << " in updateMessageId with random_id " << random_id << " from "
               << source;
    return false;
  }
  CHECK(new_message_id.is_any_server());

  auto it = being_sent_messages_.find(random_id);
  if (it == being_sent_messages_.end()) {
    // update about a new message sent from other device or a service message
    LOG(INFO) << "Receive not sent 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);

  if (!have_message_force({dialog_id, old_message_id}, "on_update_message_id")) {
    delete_sent_message_on_server(dialog_id, new_message_id, old_message_id);
    return true;
  }

  LOG(INFO) << "Save correspondence from " << new_message_id << " in " << dialog_id << " to " << old_message_id;
  CHECK(old_message_id.is_yet_unsent());
  if (new_message_id.is_scheduled()) {
    update_scheduled_message_ids_[dialog_id][new_message_id.get_scheduled_server_message_id()] = old_message_id;
  } else {
    update_message_ids_[MessageFullId(dialog_id, new_message_id)] = old_message_id;
  }
  return true;
}

bool MessagesManager::on_get_message_error(DialogId dialog_id, MessageId message_id, const Status &status,
                                           const char *source) {
  if (status.message() == "MSG_ID_INVALID" || status.message() == "MESSAGE_ID_INVALID" ||
      status.message() == "DATA_INVALID") {
    get_message_from_server({dialog_id, message_id}, Promise<Unit>(), source);
    return true;
  }
  return td_->dialog_manager_->on_get_dialog_error(dialog_id, status, source);
}

void MessagesManager::on_dialog_updated(DialogId dialog_id, const char *source) {
  if (G()->use_message_database()) {
    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);
  CHECK(d->is_update_new_chat_sent);
  send_closure(
      G()->td(), &Td::send_update,
      td_api::make_object<td_api::updateNewMessage>(get_message_object(d->dialog_id, m, "send_update_new_message")));
}

NotificationGroupInfo &MessagesManager::get_notification_group_info(Dialog *d, bool from_mentions) {
  CHECK(d != nullptr);
  auto notification_info = add_dialog_notification_info(d);
  return from_mentions ? notification_info->mention_notification_group_
                       : notification_info->message_notification_group_;
}

NotificationGroupInfo &MessagesManager::get_notification_group_info(Dialog *d, const Message *m) {
  return get_notification_group_info(d, is_from_mention_notification_group(m));
}

NotificationGroupId MessagesManager::get_dialog_notification_group_id(DialogId dialog_id,
                                                                      NotificationGroupInfo &group_info) {
  CHECK(!td_->auth_manager_->is_bot());
  if (!group_info.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 NotificationGroupId();
      }
    } while (td_->notification_manager_->have_group_force(next_notification_group_id));
    group_info = NotificationGroupInfo(next_notification_group_id);
    VLOG(notifications) << "Assign " << next_notification_group_id << " to " << 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_log_event_id_.count(dialog_id) != 0) {
      send_closure_later(G()->notification_manager(), &NotificationManager::before_get_chat_difference,
                         next_notification_group_id);
    }
  }
  CHECK(group_info.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.get_group_id());

  return group_info.get_group_id();
}

Result<MessagesManager::MessagePushNotificationInfo> MessagesManager::get_message_push_notification_info(
    DialogId dialog_id, MessageId message_id, int64 random_id, UserId sender_user_id, DialogId sender_dialog_id,
    int32 date, bool is_from_scheduled, bool contains_mention, bool is_pinned, bool is_from_binlog) {
  if (!is_from_scheduled && dialog_id == td_->dialog_manager_->get_my_dialog_id()) {
    return Status::Error("Ignore notification in chat with self");
  }
  if (td_->auth_manager_->is_bot()) {
    return Status::Error("Ignore notification sent to bot");
  }

  Dialog *d = get_dialog_force(dialog_id, "get_message_push_notification_info");
  if (d == nullptr) {
    return Status::Error(406, "Ignore notification in unknown chat");
  }
  if (sender_dialog_id.is_valid() && !have_dialog_force(sender_dialog_id, "get_message_push_notification_info")) {
    return Status::Error(406, "Ignore notification sent by unknown chat");
  }

  if (is_from_scheduled && dialog_id != td_->dialog_manager_->get_my_dialog_id() &&
      td_->option_manager_->get_option_boolean("disable_sent_scheduled_message_notifications")) {
    return Status::Error("Ignore notification about sent scheduled message");
  }

  bool is_new_pinned =
      is_pinned && message_id.is_valid() &&
      (d->notification_info == nullptr || message_id > d->notification_info->max_push_notification_message_id_);
  CHECK(!message_id.is_scheduled());
  if (message_id.is_valid()) {
    if (message_id <= d->last_new_message_id) {
      return Status::Error("Ignore notification about known message");
    }
    if (!is_from_binlog && d->notification_info != nullptr) {
      if (message_id == d->notification_info->max_push_notification_message_id_) {
        return Status::Error("Ignore previously added message push notification");
      }
      if (message_id < d->notification_info->max_push_notification_message_id_) {
        return Status::Error("Ignore out of order message push notification");
      }
    }
    if (message_id <= d->last_read_inbox_message_id) {
      return Status::Error("Ignore notification about read message");
    }
    if (message_id <= d->last_clear_history_message_id) {
      return Status::Error("Ignore notification about message from cleared chat history");
    }
    if (is_deleted_message(d, message_id)) {
      return Status::Error("Ignore notification about deleted message");
    }
    if (message_id <= d->max_unavailable_message_id) {
      return Status::Error("Ignore notification about unavailable message");
    }
  }
  if (random_id != 0) {
    CHECK(dialog_id.get_type() == DialogType::SecretChat);
    if (get_message_id_by_random_id(d, random_id, "get_message_push_notification_info").is_valid()) {
      return Status::Error(406, "Ignore notification about known secret message");
    }
  }

  if (is_pinned) {
    contains_mention = !is_dialog_pinned_message_notifications_disabled(d);
  } else if (contains_mention && is_dialog_mention_notifications_disabled(d)) {
    contains_mention = false;
  }
  if (dialog_id.get_type() == DialogType::User) {
    contains_mention = false;
  }

  DialogId settings_dialog_id = dialog_id;
  Dialog *settings_dialog = d;
  if (contains_mention) {
    auto real_sender_dialog_id = sender_dialog_id.is_valid() ? sender_dialog_id : DialogId(sender_user_id);
    if (real_sender_dialog_id.is_valid()) {
      settings_dialog_id = real_sender_dialog_id;
      settings_dialog = get_dialog_force(settings_dialog_id, "get_message_push_notification_info");
    }
  }

  bool have_settings;
  int32 mute_until;
  std::tie(have_settings, mute_until) = get_dialog_mute_until(settings_dialog_id, settings_dialog);
  if (have_settings && mute_until > date) {
    if (is_new_pinned) {
      remove_dialog_pinned_message_notification(d, "get_message_push_notification_info");
    }
    return Status::Error("Ignore notification in muted chat");
  }

  if (is_dialog_message_notification_disabled(settings_dialog_id, date)) {
    if (is_new_pinned) {
      remove_dialog_pinned_message_notification(d, "get_message_push_notification_info");
    }
    return Status::Error("Ignore notification in chat, because notifications are disabled in the chat");
  }

  auto group_id = get_dialog_notification_group_id(dialog_id, get_notification_group_info(d, contains_mention));
  if (!group_id.is_valid()) {
    return Status::Error("Can't assign notification group ID");
  }

  if (message_id.is_valid()) {
    auto notification_info = add_dialog_notification_info(d);
    if (message_id > notification_info->max_push_notification_message_id_) {
      if (is_new_pinned && contains_mention) {
        set_dialog_pinned_message_notification(d, message_id, "get_message_push_notification_info");
      }
      notification_info->max_push_notification_message_id_ = message_id;
      on_dialog_updated(dialog_id, "set_max_push_notification_message_id");
    }
  }

  MessagePushNotificationInfo result;
  result.group_id = group_id;
  result.group_type = contains_mention ? NotificationGroupType::Mentions : NotificationGroupType::Messages;
  result.settings_dialog_id = settings_dialog_id;
  return result;
}

NotificationId MessagesManager::get_next_notification_id(NotificationInfo *notification_info,
                                                         NotificationGroupId notification_group_id,
                                                         MessageId message_id) {
  CHECK(notification_info != nullptr);
  CHECK(!message_id.is_scheduled());
  NotificationId notification_id;
  do {
    notification_id = td_->notification_manager_->get_next_notification_id();
    if (!notification_id.is_valid()) {
      return NotificationId();
    }
  } while (notification_info->notification_id_to_message_id_.count(notification_id) != 0 ||
           notification_info->new_secret_chat_notification_id_ == notification_id ||
           notification_info->message_notification_group_.is_used_notification_id(notification_id) ||
           notification_info->mention_notification_group_.is_used_notification_id(notification_id));  // just in case
  if (message_id.is_valid()) {
    add_notification_id_to_message_id_correspondence(notification_info, notification_id, message_id);
  }
  return notification_id;
}

NotificationGroupFromDatabase MessagesManager::get_message_notification_group_force(NotificationGroupId group_id) {
  CHECK(!td_->auth_manager_->is_bot());
  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()->use_message_database()) {
    auto *dialog_db = G()->td_db()->get_dialog_db_sync();
    dialog_db->begin_read_transaction().ensure();
    auto r_value = dialog_db->get_notification_group(group_id);
    if (r_value.is_ok()) {
      VLOG(notifications) << "Loaded " << r_value.ok() << " from database by " << group_id;
      d = get_dialog_force(r_value.ok().dialog_id, "get_message_notification_group_force");
    } else {
      LOG_CHECK(r_value.error().message() == "Not found") << r_value.error();
      VLOG(notifications) << "Failed to load " << group_id << " from database";
    }
    dialog_db->commit_transaction().ensure();
  }

  if (d == nullptr || d->notification_info == nullptr) {
    return NotificationGroupFromDatabase();
  }
  if (!is_dialog_notification_group_id(d, group_id)) {
    if (d->dialog_id.get_type() == DialogType::SecretChat &&
        !d->notification_info->message_notification_group_.is_valid() &&
        !d->notification_info->mention_notification_group_.is_valid()) {
      // the group was reused, but wasn't deleted from the database, trying to resave it
      auto &group_info = d->notification_info->message_notification_group_;
      group_info = NotificationGroupInfo(group_id);
      group_info.try_reuse();
      save_dialog_to_database(d->dialog_id);
      group_info = NotificationGroupInfo();
    }
    CHECK(is_dialog_notification_group_id(d, group_id));
  }

  bool from_mentions = d->notification_info->mention_notification_group_.has_group_id(group_id);
  auto &group_info = get_notification_group_info(d, from_mentions);

  NotificationGroupFromDatabase result;
  VLOG(notifications) << "Found " << (from_mentions ? "Mentions " : "Messages ") << group_info.get_group_id() << '/'
                      << d->dialog_id << " by " << group_id << " with " << d->unread_mention_count
                      << " unread mentions, " << d->unread_reaction_count << " unread reactions, pinned "
                      << d->notification_info->pinned_message_notification_message_id_ << ", new secret chat "
                      << d->notification_info->new_secret_chat_notification_id_ << " and "
                      << d->server_unread_count + d->local_unread_count << " unread messages";
  result.dialog_id = d->dialog_id;
  result.total_count = get_dialog_pending_notification_count(d, from_mentions);
  auto pending_notification_count = from_mentions ? d->notification_info->pending_new_mention_notifications_.size()
                                                  : d->notification_info->pending_new_message_notifications_.size();
  result.total_count -= static_cast<int32>(pending_notification_count);
  if (result.total_count < 0) {
    LOG(ERROR) << "Total notification count is " << result.total_count << " in " << d->dialog_id << " with "
               << pending_notification_count << " pending new notifications";
    result.total_count = 0;
  }
  if (d->notification_info->new_secret_chat_notification_id_.is_valid()) {
    CHECK(d->dialog_id.get_type() == DialogType::SecretChat);
    result.type = NotificationGroupType::SecretChat;
    result.notifications.emplace_back(d->notification_info->new_secret_chat_notification_id_,
                                      td_->contacts_manager_->get_secret_chat_date(d->dialog_id.get_secret_chat_id()),
                                      false, create_new_secret_chat_notification());
  } else {
    result.type = from_mentions ? NotificationGroupType::Mentions : NotificationGroupType::Messages;
    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;
  }
  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::get_dialog_show_preview(const Dialog *d) const {
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(d != nullptr);
  if (d->notification_settings.use_default_show_preview) {
    auto scope = td_->dialog_manager_->get_dialog_notification_setting_scope(d->dialog_id);
    return td_->notification_settings_manager_->get_scope_show_preview(scope);
  } else {
    return d->notification_settings.show_preview;
  }
}

bool MessagesManager::is_message_preview_enabled(const Dialog *d, const Message *m, bool from_mentions) {
  if (!get_dialog_show_preview(d)) {
    return false;
  }
  if (!from_mentions) {
    return true;
  }
  auto sender_dialog_id = get_message_sender(m);
  if (!sender_dialog_id.is_valid()) {
    return true;
  }
  d = get_dialog_force(sender_dialog_id, "is_message_preview_enabled");
  if (d == nullptr) {
    auto scope = td_->dialog_manager_->get_dialog_notification_setting_scope(sender_dialog_id);
    return td_->notification_settings_manager_->get_scope_show_preview(scope);
  }
  return get_dialog_show_preview(d);
}

bool MessagesManager::is_from_mention_notification_group(const Message *m) {
  CHECK(m != nullptr);
  return m->contains_mention && !m->is_mention_notification_disabled;
}

bool MessagesManager::is_message_notification_active(const Dialog *d, const Message *m) {
  CHECK(!m->message_id.is_scheduled());
  if (d->notification_info == nullptr) {
    if (is_from_mention_notification_group(m)) {
      return m->contains_unread_mention;
    } else {
      return m->message_id > d->last_read_inbox_message_id;
    }
  }
  if (is_from_mention_notification_group(m)) {
    return !d->notification_info->mention_notification_group_.is_removed_notification(m->notification_id,
                                                                                      m->message_id) &&
           (m->contains_unread_mention ||
            m->message_id == d->notification_info->pinned_message_notification_message_id_);
  } else {
    return !d->notification_info->message_notification_group_.is_removed_notification(m->notification_id,
                                                                                      m->message_id) &&
           m->message_id > d->last_read_inbox_message_id;
  }
}

void MessagesManager::try_add_pinned_message_notification(Dialog *d, vector<Notification> &res,
                                                          NotificationId max_notification_id, int32 limit) {
  CHECK(d != nullptr);
  if (d->notification_info == nullptr) {
    return;
  }
  auto message_id = d->notification_info->pinned_message_notification_message_id_;
  if (!message_id.is_valid() || message_id > d->last_new_message_id) {
    CHECK(!message_id.is_scheduled());
    return;
  }

  auto m = get_message_force(d, message_id, "try_add_pinned_message_notification");
  if (m != nullptr &&
      !d->notification_info->mention_notification_group_.is_removed_notification(m->notification_id, m->message_id) &&
      m->message_id > d->last_read_inbox_message_id && !is_dialog_pinned_message_notifications_disabled(d)) {
    if (m->notification_id.get() < max_notification_id.get()) {
      VLOG(notifications) << "Add " << m->notification_id << " about pinned " << message_id << " in " << d->dialog_id;
      auto pinned_message_id = get_message_content_pinned_message_id(m->content.get());
      if (pinned_message_id.is_valid()) {
        get_message_force(d, pinned_message_id, "try_add_pinned_message_notification 2");  // preload pinned message
      }

      auto pos = res.size();
      res.emplace_back(m->notification_id, m->date, m->disable_notification,
                       create_new_message_notification(message_id, is_message_preview_enabled(d, m, true)));
      NotificationObjectId object_id(message_id);
      while (pos > 0 && res[pos - 1].type->get_object_id() < object_id) {
        std::swap(res[pos - 1], res[pos]);
        pos--;
      }
      if (pos > 0 && res[pos - 1].type->get_object_id() == object_id) {
        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 {
    remove_dialog_pinned_message_notification(d, "try_add_pinned_message_notification");
  }
}

vector<Notification> MessagesManager::get_message_notifications_from_database_force(Dialog *d, bool from_mentions,
                                                                                    int32 limit) {
  CHECK(d != nullptr);
  if (!G()->use_message_database() || td_->auth_manager_->is_bot()) {
    return {};
  }

  auto &group_info = get_notification_group_info(d, from_mentions);
  auto from_notification_id = NotificationId::max();
  auto from_message_id = MessageId::max();
  vector<Notification> res;
  if (!from_mentions && from_message_id <= d->last_read_inbox_message_id) {
    return res;
  }
  while (true) {
    auto messages = do_get_message_notifications_from_database_force(d, from_mentions, from_notification_id,
                                                                     from_message_id, limit);
    if (messages.empty()) {
      break;
    }

    bool is_found = false;
    VLOG(notifications) << "Loaded " << messages.size() << (from_mentions ? " mention" : "")
                        << " messages with notifications from database in " << group_info.get_group_id() << '/'
                        << d->dialog_id;
    for (auto &message : messages) {
      auto m = on_get_message_from_database(d, message, false, "get_message_notifications_from_database_force");
      if (m == nullptr) {
        VLOG(notifications) << "Receive from database a broken message";
        continue;
      }

      auto notification_id = m->notification_id.is_valid() ? m->notification_id : m->removed_notification_id;
      if (!notification_id.is_valid()) {
        if (from_mentions) {
          VLOG(notifications) << "Receive " << m->message_id << " with unread mention, but without notification";
          is_found = false;
        } else {
          LOG(ERROR) << "Can't find notification identifier for " << m->message_id << " in " << d->dialog_id
                     << " with from_mentions = " << from_mentions;
        }
        continue;
      }
      CHECK(m->message_id.is_valid());

      bool is_correct = true;
      if (notification_id.get() >= from_notification_id.get()) {
        // possible if two messages have the same notification_id
        LOG(ERROR) << "Have nonmonotonic notification identifiers: " << d->dialog_id << " " << m->message_id << " "
                   << notification_id << " " << from_message_id << " " << from_notification_id;
        is_correct = false;
      } else {
        from_notification_id = notification_id;
        is_found = true;
      }
      if (m->message_id >= from_message_id) {
        LOG(ERROR) << "Have nonmonotonic message identifiers: " << d->dialog_id << " " << m->message_id << " "
                   << notification_id << " " << from_message_id << " " << from_notification_id;
        is_correct = false;
      } else {
        from_message_id = m->message_id;
        is_found = true;
      }

      if (group_info.is_removed_notification(notification_id, m->message_id) ||
          (!from_mentions && m->message_id <= d->last_read_inbox_message_id)) {
        // if message still has notification_id, but it was removed,
        // then there will be no more messages with active notifications
        is_found = false;
        break;
      }

      if (!m->notification_id.is_valid()) {
        // notification_id can be empty if it is deleted in memory, but not in the database
        VLOG(notifications) << "Receive from database " << m->message_id << " with removed "
                            << m->removed_notification_id;
        continue;
      }

      if (is_from_mention_notification_group(m) != from_mentions) {
        VLOG(notifications) << "Receive from database " << m->message_id << " with " << m->notification_id
                            << " from another group";
        continue;
      }

      if (!is_message_notification_active(d, m)) {
        CHECK(from_mentions);
        CHECK(!m->contains_unread_mention);
        // skip read mentions
        continue;
      }

      if (is_correct) {
        // skip mention messages returned among unread messages
        res.emplace_back(
            m->notification_id, m->date, m->disable_notification,
            create_new_message_notification(m->message_id, is_message_preview_enabled(d, m, from_mentions)));
      } else {
        remove_message_notification_id(d, m, true, false);
        on_message_changed(d, m, false, "get_message_notifications_from_database_force");
      }
    }
    if (!res.empty() || !is_found) {
      break;
    }
  }
  if (from_mentions) {
    try_add_pinned_message_notification(d, res, NotificationId::max(), limit);
  }
  return res;
}

vector<MessageDbDialogMessage> 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()->use_message_database());
  CHECK(!from_message_id.is_scheduled());

  auto *db = G()->td_db()->get_message_db_sync();
  if (!from_mentions) {
    CHECK(from_message_id > d->last_read_inbox_message_id);
    VLOG(notifications) << "Trying to load " << limit << " messages with notifications in " << 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->dialog_id
                        << " from " << from_message_id;

    // ignore first_db_message_id, notifications can be nonconsecutive
    MessageDbMessagesQuery db_query;
    db_query.dialog_id = d->dialog_id;
    db_query.filter = MessageSearchFilter::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()->use_message_database()) {
    return {};
  }

  VLOG(notifications) << "Trying to load " << limit << " message notification groups from database from "
                      << from_group_key;

  auto *dialog_db = G()->td_db()->get_dialog_db_sync();
  dialog_db->begin_read_transaction().ensure();
  auto group_keys = dialog_db->get_notification_groups_by_last_notification_date(from_group_key, limit);
  vector<NotificationGroupKey> result;
  for (auto &group_key : group_keys) {
    CHECK(group_key.group_id.is_valid());
    CHECK(group_key.dialog_id.is_valid());
    const Dialog *d = get_dialog_force(group_key.dialog_id, "get_message_notification_group_keys_from_database");
    if (!is_dialog_notification_group_id(d, group_key.group_id)) {
      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";
    result.push_back(group_key);
  }
  dialog_db->commit_transaction().ensure();
  return result;
}

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()->use_message_database()) {
    return promise.set_error(Status::Error(500, "There is no message database"));
  }
  if (td_->auth_manager_->is_bot()) {
    return promise.set_error(Status::Error(500, "Bots have no notifications"));
  }

  CHECK(dialog_id.is_valid());
  CHECK(group_id.is_valid());
  CHECK(!from_message_id.is_scheduled());
  CHECK(limit > 0);

  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (!is_dialog_notification_group_id(d, group_id)) {
    return promise.set_value(vector<Notification>());
  }

  VLOG(notifications) << "Get " << limit << " message notifications from database in " << group_id << " from "
                      << dialog_id << " from " << from_notification_id << "/" << from_message_id;
  bool from_mentions = d->notification_info->mention_notification_group_.has_group_id(group_id);
  if (d->notification_info->new_secret_chat_notification_id_.is_valid()) {
    CHECK(dialog_id.get_type() == DialogType::SecretChat);
    vector<Notification> notifications;
    if (!from_mentions && d->notification_info->new_secret_chat_notification_id_.get() < from_notification_id.get()) {
      auto date = td_->contacts_manager_->get_secret_chat_date(dialog_id.get_secret_chat_id());
      if (date <= 0) {
        remove_new_secret_chat_notification(d, true);
      } else {
        notifications.emplace_back(d->notification_info->new_secret_chat_notification_id_, date, false,
                                   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()->use_message_database());
  CHECK(!from_message_id.is_scheduled());

  auto &group_info = get_notification_group_info(d, from_mentions);
  if (!group_info.is_valid() || group_info.is_removed_notification(from_notification_id, from_message_id) ||
      (!from_mentions && from_message_id <= d->last_read_inbox_message_id)) {
    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<MessageDbDialogMessage>> 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_message_db_async();
  if (!from_mentions) {
    VLOG(notifications) << "Trying to load " << limit << " messages with notifications in " << group_info.get_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.get_group_id() << '/' << dialog_id << " from " << from_message_id;

    // ignore first_db_message_id, notifications can be nonconsecutive
    MessageDbMessagesQuery db_query;
    db_query.dialog_id = dialog_id;
    db_query.filter = MessageSearchFilter::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<MessageDbDialogMessage>> result,
                                                                 Promise<vector<Notification>> promise) {
  G()->ignore_result_if_closing(result);
  if (result.is_error()) {
    return promise.set_error(result.move_as_error());
  }

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  auto &group_info = get_notification_group_info(d, from_mentions);
  if (!group_info.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.get_group_id()
                      << '/' << dialog_id << " from database";
  for (auto &message : messages) {
    auto m = on_get_message_from_database(d, message, false, "on_get_message_notifications_from_database");
    if (m == nullptr) {
      VLOG(notifications) << "Receive from database a broken message";
      continue;
    }

    auto notification_id = m->notification_id.is_valid() ? m->notification_id : m->removed_notification_id;
    if (!notification_id.is_valid()) {
      if (from_mentions) {
        VLOG(notifications) << "Receive " << m->message_id << " with unread mention, but without notification";
        from_notification_id = NotificationId();  // stop requesting database
      } else {
        LOG(ERROR) << "Can't find notification identifier for " << m->message_id << " in " << d->dialog_id
                   << " with from_mentions = " << from_mentions;
      }
      continue;
    }
    CHECK(m->message_id.is_valid());

    bool is_correct = true;
    if (from_notification_id.is_valid() && notification_id.get() >= from_notification_id.get()) {
      LOG(ERROR) << "Receive " << m->message_id << "/" << notification_id << " after " << from_message_id << "/"
                 << from_notification_id;
      is_correct = false;
    } else {
      from_notification_id = notification_id;
    }
    if (from_message_id.is_valid() && m->message_id >= from_message_id) {
      LOG(ERROR) << "Receive " << m->message_id << "/" << notification_id << " after " << from_message_id << "/"
                 << from_notification_id;
      is_correct = false;
    } else {
      from_message_id = m->message_id;
    }

    if (group_info.is_removed_notification(notification_id, m->message_id) ||
        (!from_mentions && m->message_id <= d->last_read_inbox_message_id)) {
      // if message still has notification_id, but it was removed via max_removed_notification_id,
      // or max_removed_message_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;
    }

    if (!m->notification_id.is_valid()) {
      // notification_id can be empty if it is deleted in memory, but not in the database
      VLOG(notifications) << "Receive from database " << m->message_id << " with removed "
                          << m->removed_notification_id;
      continue;
    }

    if (is_from_mention_notification_group(m) != from_mentions) {
      VLOG(notifications) << "Receive from database " << m->message_id << " with " << m->notification_id
                          << " from another category";
      continue;
    }

    if (!is_message_notification_active(d, m)) {
      CHECK(from_mentions);
      CHECK(!m->contains_unread_mention);
      // skip read mentions
      continue;
    }

    if (is_correct) {
      // skip mention messages returned among unread messages
      CHECK(m->date > 0);
      res.emplace_back(m->notification_id, m->date, m->disable_notification,
                       create_new_message_notification(m->message_id, is_message_preview_enabled(d, m, from_mentions)));
    } else {
      remove_message_notification_id(d, m, true, false);
      on_message_changed(d, m, false, "on_get_message_notifications_from_database");
    }
  }
  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, "remove_message_notification");
  if (!is_dialog_notification_group_id(d, 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->notification_info->mention_notification_group_.has_group_id(group_id);
  if (d->notification_info->new_secret_chat_notification_id_.is_valid()) {
    if (!from_mentions && d->notification_info->new_secret_chat_notification_id_ == notification_id) {
      return remove_new_secret_chat_notification(d, false);
    }
    return;
  }

  auto it = d->notification_info->notification_id_to_message_id_.find(notification_id);
  if (it != d->notification_info->notification_id_to_message_id_.end()) {
    auto m = get_message(d, it->second);
    CHECK(m != nullptr);
    CHECK(m->notification_id == notification_id);
    CHECK(!m->message_id.is_scheduled());
    if (is_from_mention_notification_group(m) == from_mentions && is_message_notification_active(d, m)) {
      remove_message_notification_id(d, m, false, false);
    }
    return;
  }

  if (G()->use_message_database()) {
    G()->td_db()->get_message_db_async()->get_messages_from_notification_id(
        dialog_id, NotificationId(notification_id.get() + 1), 1,
        PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, from_mentions,
                                notification_id](vector<MessageDbDialogMessage> result) {
          send_closure(actor_id, &MessagesManager::do_remove_message_notification, dialog_id, from_mentions,
                       notification_id, std::move(result));
        }));
  }
}

void MessagesManager::remove_message_notifications_by_message_ids(DialogId dialog_id,
                                                                  const vector<MessageId> &message_ids) {
  VLOG(notifications) << "Trying to remove notification about " << message_ids << " in " << dialog_id;
  Dialog *d = get_dialog_force(dialog_id, "remove_message_notifications_by_message_ids");
  if (d == nullptr || d->notification_info == nullptr) {
    return;
  }

  bool need_update_dialog_pos = false;
  vector<int64> deleted_message_ids;
  for (auto message_id : message_ids) {
    CHECK(!message_id.is_scheduled());
    // can't remove just notification_id, because total_count will stay wrong after restart
    // delete whole message
    auto message =
        delete_message(d, message_id, true, &need_update_dialog_pos, "remove_message_notifications_by_message_ids");
    if (message == nullptr) {
      LOG(INFO) << "Can't delete " << message_id << " because it is not found";
      // call synchronously to remove them before ProcessPush returns
      td_->notification_manager_->remove_temporary_notification_by_object_id(
          d->notification_info->message_notification_group_.get_group_id(), message_id, true,
          "remove_message_notifications_by_message_ids");
      td_->notification_manager_->remove_temporary_notification_by_object_id(
          d->notification_info->mention_notification_group_.get_group_id(), message_id, true,
          "remove_message_notifications_by_message_ids");
      continue;
    }
    deleted_message_ids.push_back(message->message_id.get());
  }

  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "remove_message_notifications_by_message_ids");
  }
  send_update_delete_messages(dialog_id, std::move(deleted_message_ids), true);
}

void MessagesManager::do_remove_message_notification(DialogId dialog_id, bool from_mentions,
                                                     NotificationId notification_id,
                                                     vector<MessageDbDialogMessage> result) {
  if (G()->close_flag() || result.empty()) {
    return;
  }
  CHECK(result.size() == 1);

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  auto m = on_get_message_from_database(d, result[0], false, "do_remove_message_notification");
  if (m != nullptr && m->notification_id == notification_id && is_from_mention_notification_group(m) == from_mentions &&
      is_message_notification_active(d, m)) {
    remove_message_notification_id(d, m, false, false);
  }
}

void MessagesManager::remove_message_notifications(DialogId dialog_id, NotificationGroupId group_id,
                                                   NotificationId max_notification_id, MessageId max_message_id) {
  Dialog *d = get_dialog_force(dialog_id, "remove_message_notifications");
  if (!is_dialog_notification_group_id(d, group_id)) {
    LOG(ERROR) << "There is no " << group_id << " in " << dialog_id;
    return;
  }
  if (!max_notification_id.is_valid()) {
    return;
  }
  CHECK(!max_message_id.is_scheduled());

  bool from_mentions = d->notification_info->mention_notification_group_.has_group_id(group_id);
  if (d->notification_info->new_secret_chat_notification_id_.is_valid()) {
    if (!from_mentions && d->notification_info->new_secret_chat_notification_id_.get() <= max_notification_id.get()) {
      return remove_new_secret_chat_notification(d, false);
    }
    return;
  }
  auto &group_info = get_notification_group_info(d, from_mentions);
  if (group_info.set_max_removed_notification_id(max_notification_id, max_message_id.get_prev_server_message_id(),
                                                 "remove_message_notifications")) {
    on_dialog_updated(dialog_id, "remove_message_notifications");
  }
}

int32 MessagesManager::get_dialog_pending_notification_count(const Dialog *d, bool from_mentions) const {
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(d != nullptr);
  auto notification_info = d->notification_info.get();
  CHECK(notification_info != nullptr);
  if (from_mentions) {
    bool has_pinned_message = notification_info->pinned_message_notification_message_id_.is_valid() &&
                              notification_info->pinned_message_notification_message_id_ <= d->last_new_message_id;
    return d->unread_mention_count + static_cast<int32>(has_pinned_message);
  } else {
    if (notification_info->new_secret_chat_notification_id_.is_valid()) {
      return 1;
    }
    if (is_dialog_muted(d)) {
      return narrow_cast<int32>(notification_info->pending_new_message_notifications_.size());  // usually 0
    }

    return d->server_unread_count + d->local_unread_count;
  }
}

void MessagesManager::update_dialog_mention_notification_count(const Dialog *d) {
  CHECK(d != nullptr);
  if (td_->auth_manager_->is_bot() || d->notification_info == nullptr ||
      !d->notification_info->mention_notification_group_.is_valid()) {
    return;
  }
  auto total_count = get_dialog_pending_notification_count(d, true) -
                     static_cast<int32>(d->notification_info->pending_new_mention_notifications_.size());
  if (total_count < 0) {
    LOG(ERROR) << "Total mention notification count is " << total_count << " in " << d->dialog_id << " with "
               << d->notification_info->pending_new_mention_notifications_ << " pending new mention notifications";
    total_count = 0;
  }
  send_closure_later(G()->notification_manager(), &NotificationManager::set_notification_total_count,
                     d->notification_info->mention_notification_group_.get_group_id(), total_count);
}

bool MessagesManager::is_message_notification_disabled(const Dialog *d, const Message *m) const {
  CHECK(d != nullptr);
  CHECK(m != nullptr);

  if (!has_incoming_notification(d->dialog_id, m) || td_->auth_manager_->is_bot()) {
    return true;
  }
  if (m->is_from_scheduled && d->dialog_id != td_->dialog_manager_->get_my_dialog_id() &&
      td_->option_manager_->get_option_boolean("disable_sent_scheduled_message_notifications")) {
    return true;
  }
  if (m->forward_info != nullptr && m->forward_info->is_imported()) {
    return true;
  }

  switch (m->content->get_type()) {
    case MessageContentType::ChatDeleteHistory:
    case MessageContentType::ChatMigrateTo:
    case MessageContentType::Unsupported:
    case MessageContentType::ExpiredPhoto:
    case MessageContentType::ExpiredVideo:
    case MessageContentType::ExpiredVideoNote:
    case MessageContentType::ExpiredVoiceNote:
    case MessageContentType::PassportDataSent:
    case MessageContentType::PassportDataReceived:
    case MessageContentType::WebViewDataSent:
    case MessageContentType::WebViewDataReceived:
    case MessageContentType::GiveawayLaunch:
      VLOG(notifications) << "Disable notification for " << m->message_id << " in " << d->dialog_id
                          << " with content of type " << m->content->get_type();
      return true;
    case MessageContentType::ContactRegistered:
      if (m->disable_notification) {
        return true;
      }
      break;
    default:
      break;
  }

  return is_dialog_message_notification_disabled(d->dialog_id, m->date);
}

bool MessagesManager::is_dialog_message_notification_disabled(DialogId dialog_id, int32 message_date) const {
  switch (dialog_id.get_type()) {
    case DialogType::User:
      break;
    case DialogType::Chat:
      if (!td_->contacts_manager_->get_chat_is_active(dialog_id.get_chat_id())) {
        return true;
      }
      break;
    case DialogType::Channel:
      if (!td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id()).is_member() ||
          message_date < td_->contacts_manager_->get_channel_date(dialog_id.get_channel_id())) {
        return true;
      }
      break;
    case DialogType::SecretChat:
      if (td_->contacts_manager_->get_secret_chat_state(dialog_id.get_secret_chat_id()) == SecretChatState::Closed) {
        return true;
      }
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }
  if (message_date < authorization_date_) {
    return true;
  }

  return false;
}

bool MessagesManager::may_need_message_notification(const Dialog *d, const Message *m) const {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  CHECK(m->message_id.is_valid());

  if (is_message_notification_disabled(d, m)) {
    return false;
  }

  if (is_from_mention_notification_group(m)) {
    return true;
  }

  bool have_settings;
  int32 mute_until;
  std::tie(have_settings, mute_until) = get_dialog_mute_until(d->dialog_id, d);
  return !have_settings || mute_until <= m->date;
}

bool MessagesManager::add_new_message_notification(Dialog *d, Message *m, bool force) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  CHECK(m->message_id.is_valid());

  if (!force && d->notification_info != nullptr) {
    if (d->notification_info->message_notification_group_.is_valid()) {
      send_closure_later(G()->notification_manager(), &NotificationManager::remove_temporary_notifications,
                         d->notification_info->message_notification_group_.get_group_id(),
                         "add_new_message_notification 1");
    }
    if (d->notification_info->mention_notification_group_.is_valid()) {
      send_closure_later(G()->notification_manager(), &NotificationManager::remove_temporary_notifications,
                         d->notification_info->mention_notification_group_.get_group_id(),
                         "add_new_message_notification 2");
    }
  }

  CHECK(!m->notification_id.is_valid());
  if (is_message_notification_disabled(d, m)) {
    return false;
  }

  auto from_mentions = is_from_mention_notification_group(m);
  bool is_pinned = m->content->get_type() == MessageContentType::PinMessage;
  bool is_active =
      from_mentions ? m->contains_unread_mention || is_pinned : m->message_id > d->last_read_inbox_message_id;
  if (is_active) {
    auto &group_info = get_notification_group_info(d, from_mentions);
    if (group_info.is_removed_object_id(m->message_id)) {
      is_active = false;
    }
  }
  if (!is_active) {
    VLOG(notifications) << "Disable inactive notification for " << m->message_id << " in " << d->dialog_id;
    if (is_pinned && from_mentions) {
      remove_dialog_pinned_message_notification(d, "add_new_message_notification");
    }
    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 (is_from_mention_notification_group(m)) {
    // have a mention, so use notification settings from the dialog with the sender
    auto sender_dialog_id = get_message_sender(m);
    if (sender_dialog_id.is_valid()) {
      settings_dialog_id = sender_dialog_id;
      settings_dialog = get_dialog_force(settings_dialog_id, "add_new_message_notification");
    }
  }

  bool have_settings;
  int32 mute_until;
  std::tie(have_settings, mute_until) = get_dialog_mute_until(settings_dialog_id, settings_dialog);
  if (mute_until > m->date && (have_settings || force)) {
    VLOG(notifications) << "Disable notification, because " << settings_dialog_id << " is muted";
    if (is_pinned && from_mentions) {
      remove_dialog_pinned_message_notification(d, "add_new_message_notification");
    }
    return false;
  }

  MessageId missing_pinned_message_id;
  if (is_pinned) {
    auto message_id = get_message_content_pinned_message_id(m->content.get());
    if (message_id.is_valid() &&
        !have_message_force(d, message_id,
                            force ? "add_new_message_notification force" : "add_new_message_notification not force")) {
      missing_pinned_message_id = message_id;
    }
  }

  auto notification_info = add_dialog_notification_info(d);
  auto &pending_notifications = from_mentions ? notification_info->pending_new_mention_notifications_
                                              : notification_info->pending_new_message_notifications_;
  if (!force && (!have_settings || !pending_notifications.empty() || missing_pinned_message_id.is_valid())) {
    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()) {
      VLOG(notifications) << "Create FlushPendingNewMessageNotificationsSleepActor for " << d->dialog_id;
      create_actor<SleepActor>("FlushPendingNewMessageNotificationsSleepActor", 5.0,
                               PromiseCreator::lambda([actor_id = actor_id(this), dialog_id = d->dialog_id,
                                                       from_mentions](Result<Unit> result) {
                                 VLOG(notifications)
                                     << "Pending notifications timeout in " << dialog_id << " has expired";
                                 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);
      });
      reload_dialog_notification_settings(settings_dialog_id, std::move(promise), "add_new_message_notification");
    }
    if (missing_pinned_message_id.is_valid()) {
      VLOG(notifications) << "Fetch pinned " << missing_pinned_message_id;
      auto promise = 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,
                         dialog_id);
          });
      get_message_from_server({d->dialog_id, missing_pinned_message_id}, std::move(promise),
                              "add_new_message_notification");
    }
    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(notification_info, notification_group_id, force ? m->message_id : MessageId());
  if (!m->notification_id.is_valid()) {
    return false;
  }
  set_dialog_last_notification_checked(d->dialog_id, group_info, m->date, m->notification_id,
                                       "add_new_message_notification 3");
  if (is_pinned && from_mentions) {
    set_dialog_pinned_message_notification(d, m->message_id, "add_new_message_notification");
  }
  if (!m->notification_id.is_valid()) {
    // protection from accidental notification_id removal in set_dialog_pinned_message_notification
    return false;
  }
  VLOG(notifications) << "Create " << m->notification_id << " with " << m->message_id << " in "
                      << group_info.get_group_id() << '/' << d->dialog_id;
  int32 min_delay_ms = 0;
  if (need_delay_message_content_notification(m->content.get(), td_->contacts_manager_->get_my_id())) {
    min_delay_ms = 3000;  // 3 seconds
  } else if (td_->is_online() && d->open_count > 0) {
    min_delay_ms = 1000;  // 1 second
  }
  auto ringtone_id = get_dialog_notification_ringtone_id(settings_dialog_id, settings_dialog);
  bool is_silent = m->disable_notification || m->message_id <= notification_info->max_push_notification_message_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, is_silent ? 0 : ringtone_id, min_delay_ms,
                     m->notification_id,
                     create_new_message_notification(m->message_id, is_message_preview_enabled(d, m, from_mentions)),
                     "add_new_message_notification");
  return true;
}

void MessagesManager::flush_pending_new_message_notifications(DialogId dialog_id, bool from_mentions,
                                                              DialogId settings_dialog_id) {
  // flush pending notifications even while closing
  CHECK(!td_->auth_manager_->is_bot());

  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  if (d->notification_info == nullptr) {
    return;
  }
  auto &pending_notifications = from_mentions ? d->notification_info->pending_new_mention_notifications_
                                              : d->notification_info->pending_new_message_notifications_;
  if (pending_notifications.empty()) {
    VLOG(notifications) << "Have no pending notifications in " << dialog_id << " to flush";
    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) {
      VLOG(notifications) << "Can't find " << it->second << " in " << dialog_id << " with pending notification";
    } else 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(Dialog *d, bool from_mentions, const char *source) {
  // removes up to group_info.get_last_notification_id()
  CHECK(!td_->auth_manager_->is_bot());
  if (d->notification_info == nullptr) {
    return;
  }
  NotificationGroupInfo &group_info = get_notification_group_info(d, from_mentions);
  if (group_info.is_valid() && group_info.get_last_notification_id().is_valid()) {
    auto last_notification_id = group_info.get_last_notification_id();
    group_info.set_max_removed_notification_id(last_notification_id,
                                               d->notification_info->max_push_notification_message_id_, source);
    on_dialog_updated(d->dialog_id, source);

    if (!d->notification_info->pending_new_message_notifications_.empty()) {
      for (auto &it : d->notification_info->pending_new_message_notifications_) {
        it.first = DialogId();
      }
      flush_pending_new_message_notifications(d->dialog_id, from_mentions, DialogId(UserId(static_cast<int64>(2))));
    }
    // remove_message_notifications will be called by NotificationManager
    send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification_group,
                       group_info.get_group_id(), last_notification_id, MessageId(), 0, true, Promise<Unit>());
    d->notification_info->new_secret_chat_notification_id_ = NotificationId();
    set_dialog_last_notification(d->dialog_id, group_info, 0, NotificationId(), source);  // just in case
  }
}

void MessagesManager::remove_message_dialog_notifications(Dialog *d, MessageId max_message_id, bool from_mentions,
                                                          const char *source) {
  // removes up to max_message_id
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(!max_message_id.is_scheduled());
  if (d->notification_info == nullptr) {
    return;
  }
  NotificationGroupInfo &group_info = get_notification_group_info(d, from_mentions);
  if (!group_info.is_valid()) {
    return;
  }

  VLOG(notifications) << "Remove message dialog notifications in " << group_info.get_group_id() << '/' << d->dialog_id
                      << " up to " << max_message_id << " from " << source;

  if (!d->notification_info->pending_new_message_notifications_.empty()) {
    for (auto &it : d->notification_info->pending_new_message_notifications_) {
      if (it.second <= max_message_id) {
        it.first = DialogId();
      }
    }
    flush_pending_new_message_notifications(d->dialog_id, from_mentions, DialogId(UserId(static_cast<int64>(3))));
  }

  auto max_notification_message_id = max_message_id;
  if (d->last_message_id.is_valid() && max_notification_message_id >= d->last_message_id) {
    max_notification_message_id = d->last_message_id;
    set_dialog_last_notification(d->dialog_id, group_info, 0, NotificationId(),
                                 "remove_message_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_message_dialog_notifications 2");
  } else {
    LOG(FATAL) << "TODO support notification deletion up to " << max_message_id << " if it would be ever needed from "
               << source;
  }

  send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification_group,
                     group_info.get_group_id(), NotificationId(), max_notification_message_id, 0, true,
                     Promise<Unit>());
}

void MessagesManager::send_update_message_send_succeeded(Dialog *d, MessageId old_message_id, const Message *m,
                                                         bool *need_update_dialog_pos) {
  CHECK(m != nullptr);
  CHECK(d != nullptr);
  CHECK(d->is_update_new_chat_sent);
  if (!td_->auth_manager_->is_bot()) {
    yet_unsent_message_full_id_to_persistent_message_id_.emplace({d->dialog_id, old_message_id}, m->message_id);

    auto message =
        delete_message(d, m->message_id, false, need_update_dialog_pos, "send_update_message_send_succeeded");
    if (message != nullptr) {
      send_update_delete_messages(d->dialog_id, {message->message_id.get()}, false);
    }
  }
  if (old_message_id.is_valid() && m->message_id.is_valid() && m->message_id < old_message_id &&
      !d->had_yet_unsent_message_id_overflow && m->message_id != MessageId(ServerMessageId(1))) {
    LOG(ERROR) << "Sent " << old_message_id << " to " << d->dialog_id << " as " << m->message_id;
  }
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateMessageSendSucceeded>(
                   get_message_object(d->dialog_id, m, "send_update_message_send_succeeded"), old_message_id.get()));
}

void MessagesManager::send_update_message_content(const Dialog *d, Message *m, bool is_message_in_dialog,
                                                  const char *source) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  if (is_message_in_dialog) {
    delete_bot_command_message_id(d->dialog_id, m->message_id);
    try_add_bot_command_message_id(d->dialog_id, m);
    reregister_message_reply(d->dialog_id, m);
    update_message_max_reply_media_timestamp(d, m, false);  // because the message reply can be just registered
    update_message_max_own_media_timestamp(d, m);
  }
  send_update_message_content_impl(d->dialog_id, m, source);
}

void MessagesManager::send_update_message_content_impl(DialogId dialog_id, const Message *m, const char *source) const {
  CHECK(m != nullptr);
  if (!m->is_update_sent) {
    LOG(INFO) << "Skip updateMessageContent for " << m->message_id << " in " << dialog_id << " from " << source;
    return;
  }
  LOG(INFO) << "Send updateMessageContent for " << m->message_id << " in " << dialog_id << " from " << source;
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateMessageContent>(get_chat_id_object(dialog_id, "updateMessageContent"),
                                                                 m->message_id.get(),
                                                                 get_message_message_content_object(dialog_id, m)));
}

void MessagesManager::send_update_message_edited(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  cancel_dialog_action(dialog_id, m);
  auto edit_date = m->hide_edit_date ? 0 : m->edit_date;
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateMessageEdited>(
                   get_chat_id_object(dialog_id, "updateMessageEdited"), m->message_id.get(), edit_date,
                   get_reply_markup_object(td_->contacts_manager_.get(), m->reply_markup)));
}

void MessagesManager::send_update_message_interaction_info(DialogId dialog_id, const Message *m) const {
  CHECK(m != nullptr);
  if (td_->auth_manager_->is_bot() || !m->is_update_sent) {
    return;
  }

  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateMessageInteractionInfo>(
                   get_chat_id_object(dialog_id, "updateMessageInteractionInfo"), m->message_id.get(),
                   get_message_interaction_info_object(dialog_id, m)));
}

void MessagesManager::send_update_message_unread_reactions(DialogId dialog_id, const Message *m,
                                                           int32 unread_reaction_count) const {
  CHECK(m != nullptr);
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  if (!m->is_update_sent) {
    LOG(INFO) << "Update unread reaction message count in " << dialog_id << " to " << unread_reaction_count;
    send_closure(G()->td(), &Td::send_update,
                 td_api::make_object<td_api::updateChatUnreadReactionCount>(
                     get_chat_id_object(dialog_id, "updateChatUnreadReactionCount"), unread_reaction_count));
    return;
  }

  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateMessageUnreadReactions>(
                   get_chat_id_object(dialog_id, "updateMessageUnreadReactions"), m->message_id.get(),
                   get_unread_reactions_object(dialog_id, m), unread_reaction_count));
}

void MessagesManager::send_update_message_live_location_viewed(MessageFullId message_full_id) {
  CHECK(get_message(message_full_id) != nullptr);
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateMessageLiveLocationViewed>(message_full_id.get_dialog_id().get(),
                                                                            message_full_id.get_message_id().get()));
}

void MessagesManager::send_update_delete_messages(DialogId dialog_id, vector<int64> &&message_ids,
                                                  bool is_permanent) const {
  if (message_ids.empty()) {
    return;
  }

  LOG_CHECK(have_dialog(dialog_id)) << "Wrong " << dialog_id << " in send_update_delete_messages";
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateDeleteMessages>(get_chat_id_object(dialog_id, "updateDeleteMessages"),
                                                                 std::move(message_ids), is_permanent, false));
}

void MessagesManager::send_update_new_chat(Dialog *d) {
  CHECK(d != nullptr);
  CHECK(d->messages.empty());
  if ((d->dialog_id.get_type() == DialogType::User || d->dialog_id.get_type() == DialogType::SecretChat) &&
      td_->auth_manager_->is_bot()) {
    (void)td_->dialog_manager_->get_dialog_photo(d->dialog_id);  // to apply pending user photo
  }
  d->is_update_new_chat_being_sent = true;
  auto chat_object = get_chat_object(d);
  bool has_action_bar = chat_object->action_bar_ != nullptr;
  bool has_background = chat_object->background_ != nullptr;
  bool has_theme = !chat_object->theme_name_.empty();
  d->last_sent_has_scheduled_messages = chat_object->has_scheduled_messages_;
  send_closure(G()->td(), &Td::send_update, td_api::make_object<td_api::updateNewChat>(std::move(chat_object)));
  d->is_update_new_chat_sent = true;
  d->is_update_new_chat_being_sent = false;

  if (has_action_bar) {
    send_update_secret_chats_with_user_action_bar(d);
  }
  if (has_background) {
    send_update_secret_chats_with_user_background(d);
  }
  if (has_theme) {
    send_update_secret_chats_with_user_theme(d);
  }
}

bool MessagesManager::need_hide_dialog_draft_message(const Dialog *d) const {
  if (d->dialog_id == td_->dialog_manager_->get_my_dialog_id()) {
    return false;
  }
  return get_dialog_view_as_topics(d) || can_send_message(d->dialog_id).is_error();
}

void MessagesManager::send_update_chat_draft_message(const Dialog *d) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  CHECK(d != nullptr);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_draft_message";
  if (d->draft_message == nullptr || !need_hide_dialog_draft_message(d)) {
    send_closure(G()->td(), &Td::send_update,
                 td_api::make_object<td_api::updateChatDraftMessage>(
                     get_chat_id_object(d->dialog_id, "updateChatDraftMessage"),
                     get_draft_message_object(td_, d->draft_message), get_chat_positions_object(d)));

    if (d->dialog_id == td_->dialog_manager_->get_my_dialog_id()) {
      td_->saved_messages_manager_->on_topic_draft_message_updated(
          SavedMessagesTopicId(d->dialog_id), d->draft_message == nullptr ? 0 : d->draft_message->get_date());
    }
  }
}

void MessagesManager::send_update_last_message_if_needed(const Dialog *d, const Message *m, const char *source) const {
  if (m->message_id == d->last_message_id) {
    send_update_chat_last_message_impl(d, source);
  }
  if (d->dialog_id == td_->dialog_manager_->get_my_dialog_id() && m->saved_messages_topic_id.is_valid()) {
    td_->saved_messages_manager_->on_topic_message_updated(m->saved_messages_topic_id, m->message_id);
  }
}

void MessagesManager::send_update_chat_last_message(Dialog *d, const char *source) {
  update_dialog_pos(d, 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 {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  LOG_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;
  const auto *m = get_message(d, d->last_message_id);
  auto message_object = get_message_object(d->dialog_id, m, "send_update_chat_last_message_impl");
  auto positions_object = get_chat_positions_object(d);
  auto update =
      td_api::make_object<td_api::updateChatLastMessage>(get_chat_id_object(d->dialog_id, "updateChatLastMessage"),
                                                         std::move(message_object), std::move(positions_object));
  send_closure(G()->td(), &Td::send_update, std::move(update));
}

void MessagesManager::send_update_unread_message_count(DialogList &list, DialogId dialog_id, bool force,
                                                       const char *source, bool from_database) {
  if (td_->auth_manager_->is_bot() || !G()->use_message_database()) {
    return;
  }

  auto dialog_list_id = list.dialog_list_id;
  CHECK(list.is_message_unread_count_inited_);
  if (list.unread_message_muted_count_ < 0 || list.unread_message_muted_count_ > list.unread_message_total_count_) {
    if (!list.need_unread_count_recalc_) {
      LOG(ERROR) << "Unread message count became invalid in " << dialog_list_id << ": "
                 << list.unread_message_total_count_ << '/'
                 << list.unread_message_total_count_ - list.unread_message_muted_count_ << " from " << source << " and "
                 << dialog_id;
    }
    if (list.unread_message_muted_count_ < 0) {
      list.unread_message_muted_count_ = 0;
    }
    if (list.unread_message_muted_count_ > list.unread_message_total_count_) {
      list.unread_message_total_count_ = list.unread_message_muted_count_;
    }
  }

  if (!from_database) {
    LOG(INFO) << "Save unread message count in " << dialog_list_id;
    G()->td_db()->get_binlog_pmc()->set(
        PSTRING() << "unread_message_count" << dialog_list_id.get(),
        PSTRING() << list.unread_message_total_count_ << ' ' << list.unread_message_muted_count_);
  }

  int32 unread_unmuted_count = list.unread_message_total_count_ - list.unread_message_muted_count_;
  if (!force && running_get_difference_) {
    LOG(INFO) << "Postpone updateUnreadMessageCount in " << dialog_list_id << " to " << list.unread_message_total_count_
              << '/' << unread_unmuted_count << " from " << source << " and " << dialog_id;
    postponed_unread_message_count_updates_.insert(dialog_list_id);
  } else {
    postponed_unread_message_count_updates_.erase(dialog_list_id);
    LOG(INFO) << "Send updateUnreadMessageCount in " << dialog_list_id << " to " << list.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(list));
  }
}

void MessagesManager::send_update_unread_chat_count(DialogList &list, DialogId dialog_id, bool force,
                                                    const char *source, bool from_database) {
  if (td_->auth_manager_->is_bot() || !G()->use_message_database()) {
    return;
  }

  auto dialog_list_id = list.dialog_list_id;
  CHECK(list.is_dialog_unread_count_inited_);
  if (list.unread_dialog_muted_marked_count_ < 0 ||
      list.unread_dialog_marked_count_ < list.unread_dialog_muted_marked_count_ ||
      list.unread_dialog_muted_count_ < list.unread_dialog_muted_marked_count_ ||
      list.unread_dialog_total_count_ + list.unread_dialog_muted_marked_count_ <
          list.unread_dialog_muted_count_ + list.unread_dialog_marked_count_) {
    if (!list.need_unread_count_recalc_) {
      LOG(ERROR) << "Unread chat count became invalid in " << dialog_list_id << ": " << list.unread_dialog_total_count_
                 << '/' << list.unread_dialog_total_count_ - list.unread_dialog_muted_count_ << '/'
                 << list.unread_dialog_marked_count_ << '/'
                 << list.unread_dialog_marked_count_ - list.unread_dialog_muted_marked_count_ << " from " << source
                 << " and " << dialog_id;
    }
    if (list.unread_dialog_muted_marked_count_ < 0) {
      list.unread_dialog_muted_marked_count_ = 0;
    }
    if (list.unread_dialog_marked_count_ < list.unread_dialog_muted_marked_count_) {
      list.unread_dialog_marked_count_ = list.unread_dialog_muted_marked_count_;
    }
    if (list.unread_dialog_muted_count_ < list.unread_dialog_muted_marked_count_) {
      list.unread_dialog_muted_count_ = list.unread_dialog_muted_marked_count_;
    }
    if (list.unread_dialog_total_count_ + list.unread_dialog_muted_marked_count_ <
        list.unread_dialog_muted_count_ + list.unread_dialog_marked_count_) {
      list.unread_dialog_total_count_ =
          list.unread_dialog_muted_count_ + list.unread_dialog_marked_count_ - list.unread_dialog_muted_marked_count_;
    }
  }

  if (!from_database) {
    save_unread_chat_count(list);
  }

  bool need_postpone = !force && running_get_difference_;
  int32 unread_unmuted_count = list.unread_dialog_total_count_ - list.unread_dialog_muted_count_;
  int32 unread_unmuted_marked_count = list.unread_dialog_marked_count_ - list.unread_dialog_muted_marked_count_;
  LOG(INFO) << (need_postpone ? "Postpone" : "Send") << " updateUnreadChatCount in " << dialog_list_id << " to "
            << list.in_memory_dialog_total_count_ << '/' << list.server_dialog_total_count_ << '+'
            << list.secret_chat_total_count_ << '/' << list.unread_dialog_total_count_ << '/' << unread_unmuted_count
            << '/' << list.unread_dialog_marked_count_ << '/' << unread_unmuted_marked_count << " from " << source
            << " and " << dialog_id;
  if (need_postpone) {
    postponed_unread_chat_count_updates_.insert(dialog_list_id);
  } else {
    postponed_unread_chat_count_updates_.erase(dialog_list_id);
    send_closure(G()->td(), &Td::send_update, get_update_unread_chat_count_object(list));
  }
}

void MessagesManager::save_unread_chat_count(const DialogList &list) {
  LOG(INFO) << "Save unread chat count in " << list.dialog_list_id;
  G()->td_db()->get_binlog_pmc()->set(
      PSTRING() << "unread_dialog_count" << list.dialog_list_id.get(),
      PSTRING() << list.unread_dialog_total_count_ << ' ' << list.unread_dialog_muted_count_ << ' '
                << list.unread_dialog_marked_count_ << ' ' << list.unread_dialog_muted_marked_count_ << ' '
                << list.server_dialog_total_count_ << ' ' << list.secret_chat_total_count_);
}

void MessagesManager::send_update_chat_read_inbox(const Dialog *d, bool force, const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_read_inbox from "
                                        << source;
  if (!force && (running_get_difference_ || running_get_channel_difference(d->dialog_id) ||
                 get_channel_difference_to_log_event_id_.count(d->dialog_id) != 0 ||
                 (d->open_count > 0 && d->server_unread_count + d->local_unread_count > 0))) {
    LOG(INFO) << "Postpone updateChatReadInbox in " << d->dialog_id << "("
              << td_->dialog_manager_->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);
    if (d->open_count > 0) {
      send_update_chat_read_inbox_timeout_.add_timeout_in(d->dialog_id.get(), 0.1);
    }
  } else {
    postponed_chat_read_inbox_updates_.erase(d->dialog_id);
    LOG(INFO) << "Send updateChatReadInbox in " << d->dialog_id << "("
              << td_->dialog_manager_->get_dialog_title(d->dialog_id) << ") to " << d->server_unread_count << " + "
              << d->local_unread_count << " from " << source;
    send_closure(G()->td(), &Td::send_update,
                 td_api::make_object<td_api::updateChatReadInbox>(
                     get_chat_id_object(d->dialog_id, "updateChatReadInbox"), 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) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  LOG_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,
               td_api::make_object<td_api::updateChatReadOutbox>(
                   get_chat_id_object(d->dialog_id, "updateChatReadOutbox"), d->last_read_outbox_message_id.get()));
}

void MessagesManager::send_update_chat_unread_mention_count(const Dialog *d) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  LOG_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,
               td_api::make_object<td_api::updateChatUnreadMentionCount>(
                   get_chat_id_object(d->dialog_id, "updateChatUnreadMentionCount"), d->unread_mention_count));
}

void MessagesManager::send_update_chat_unread_reaction_count(const Dialog *d, const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id
                                        << " in send_update_chat_unread_reaction_count from " << source;
  LOG(INFO) << "Update unread reaction message count in " << d->dialog_id << " to " << d->unread_reaction_count
            << " from " << source;
  on_dialog_updated(d->dialog_id, "send_update_chat_unread_reaction_count");
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatUnreadReactionCount>(
                   get_chat_id_object(d->dialog_id, "updateChatUnreadReactionCount"), d->unread_reaction_count));
}

void MessagesManager::send_update_chat_position(DialogListId dialog_list_id, const Dialog *d,
                                                const char *source) const {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_position";
  LOG(INFO) << "Send updateChatPosition for " << d->dialog_id << " in " << dialog_list_id << " from " << source;
  auto position = get_chat_position_object(dialog_list_id, d);
  if (position == nullptr) {
    position = td_api::make_object<td_api::chatPosition>(dialog_list_id.get_chat_list_object(), 0, false, nullptr);
  }
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatPosition>(get_chat_id_object(d->dialog_id, "updateChatPosition"),
                                                               std::move(position)));
}

void MessagesManager::send_update_secret_chats_with_user_action_bar(const Dialog *d) const {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  if (d->dialog_id.get_type() != DialogType::User) {
    return;
  }

  td_->contacts_manager_->for_each_secret_chat_with_user(
      d->dialog_id.get_user_id(), [this, user_d = d](SecretChatId secret_chat_id) {
        DialogId dialog_id(secret_chat_id);
        auto secret_chat_d = get_dialog(dialog_id);  // must not create the dialog
        if (secret_chat_d != nullptr && secret_chat_d->is_update_new_chat_sent) {
          send_closure(G()->td(), &Td::send_update,
                       td_api::make_object<td_api::updateChatActionBar>(
                           get_chat_id_object(dialog_id, "updateChatActionBar"), get_chat_action_bar_object(user_d)));
        }
      });
}

void MessagesManager::send_update_chat_action_bar(Dialog *d) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  if (d->action_bar != nullptr && d->action_bar->is_empty()) {
    d->action_bar = nullptr;
  }

  CHECK(d != nullptr);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_action_bar";
  on_dialog_updated(d->dialog_id, "send_update_chat_action_bar");
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatActionBar>(get_chat_id_object(d->dialog_id, "updateChatActionBar"),
                                                                get_chat_action_bar_object(d)));

  send_update_secret_chats_with_user_action_bar(d);
}

void MessagesManager::send_update_chat_available_reactions(const Dialog *d) {
  CHECK(d != nullptr);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_available_reactions";
  auto available_reactions = get_dialog_active_reactions(d).get_chat_available_reactions_object();
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatAvailableReactions>(
                   get_chat_id_object(d->dialog_id, "updateChatAvailableReactions"), std::move(available_reactions)));
}

void MessagesManager::send_update_secret_chats_with_user_background(const Dialog *d) const {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  if (d->dialog_id.get_type() != DialogType::User) {
    return;
  }

  td_->contacts_manager_->for_each_secret_chat_with_user(
      d->dialog_id.get_user_id(), [this, user_d = d](SecretChatId secret_chat_id) {
        DialogId dialog_id(secret_chat_id);
        auto secret_chat_d = get_dialog(dialog_id);  // must not create the dialog
        if (secret_chat_d != nullptr && secret_chat_d->is_update_new_chat_sent) {
          send_closure(G()->td(), &Td::send_update,
                       td_api::make_object<td_api::updateChatBackground>(
                           get_chat_id_object(dialog_id, "updateChatBackground"), get_chat_background_object(user_d)));
        }
      });
}

void MessagesManager::send_update_chat_background(const Dialog *d) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  CHECK(d->dialog_id.get_type() != DialogType::SecretChat);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_background";
  on_dialog_updated(d->dialog_id, "send_update_chat_background");
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatBackground>(
                   get_chat_id_object(d->dialog_id, "updateChatBackground"), get_chat_background_object(d)));

  send_update_secret_chats_with_user_background(d);
}

void MessagesManager::send_update_secret_chats_with_user_theme(const Dialog *d) const {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  if (d->dialog_id.get_type() != DialogType::User) {
    return;
  }

  td_->contacts_manager_->for_each_secret_chat_with_user(
      d->dialog_id.get_user_id(), [this, user_d = d](SecretChatId secret_chat_id) {
        DialogId dialog_id(secret_chat_id);
        auto secret_chat_d = get_dialog(dialog_id);  // must not create the dialog
        if (secret_chat_d != nullptr && secret_chat_d->is_update_new_chat_sent) {
          send_closure(G()->td(), &Td::send_update,
                       td_api::make_object<td_api::updateChatTheme>(get_chat_id_object(dialog_id, "updateChatTheme"),
                                                                    user_d->theme_name));
        }
      });
}

void MessagesManager::send_update_chat_theme(const Dialog *d) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  CHECK(d->dialog_id.get_type() != DialogType::SecretChat);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_theme";
  on_dialog_updated(d->dialog_id, "send_update_chat_theme");
  send_closure(
      G()->td(), &Td::send_update,
      td_api::make_object<td_api::updateChatTheme>(get_chat_id_object(d->dialog_id, "updateChatTheme"), d->theme_name));

  send_update_secret_chats_with_user_theme(d);
}

void MessagesManager::send_update_chat_pending_join_requests(const Dialog *d) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_pending_join_requests";
  on_dialog_updated(d->dialog_id, "send_update_chat_pending_join_requests");
  send_closure(
      G()->td(), &Td::send_update,
      td_api::make_object<td_api::updateChatPendingJoinRequests>(
          get_chat_id_object(d->dialog_id, "updateChatPendingJoinRequests"), get_chat_join_requests_info_object(d)));
}

void MessagesManager::send_update_chat_video_chat(const Dialog *d) {
  CHECK(d != nullptr);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_video_chat";
  on_dialog_updated(d->dialog_id, "send_update_chat_video_chat");
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatVideoChat>(get_chat_id_object(d->dialog_id, "updateChatVideoChat"),
                                                                get_video_chat_object(d)));
}

void MessagesManager::send_update_chat_message_sender(const Dialog *d) {
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(d != nullptr);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_message_sender";
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatMessageSender>(
                   get_chat_id_object(d->dialog_id, "updateChatMessageSender"), get_default_message_sender_object(d)));
}

void MessagesManager::send_update_chat_message_auto_delete_time(const Dialog *d) {
  CHECK(d != nullptr);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_message_auto_delete_time";
  on_dialog_updated(d->dialog_id, "send_update_chat_message_auto_delete_time");
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatMessageAutoDeleteTime>(
                   get_chat_id_object(d->dialog_id, "updateChatMessageAutoDeleteTime"),
                   d->message_ttl.get_message_auto_delete_time_object()));
}

void MessagesManager::send_update_chat_has_scheduled_messages(Dialog *d, bool from_deletion) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  if (!have_dialog_scheduled_messages_in_memory(d)) {
    if (d->has_scheduled_database_messages) {
      if (d->has_loaded_scheduled_messages_from_database) {
        set_dialog_has_scheduled_database_messages_impl(d, false);
      } else {
        CHECK(G()->use_message_database());
        repair_dialog_scheduled_messages(d);
      }
    }
    if (d->has_scheduled_server_messages) {
      if (from_deletion && d->scheduled_messages_sync_generation > 0) {
        set_dialog_has_scheduled_server_messages(d, false);
      } else {
        d->last_repair_scheduled_messages_generation = 0;
        repair_dialog_scheduled_messages(d);
      }
    }
  }

  LOG(INFO) << "In " << d->dialog_id << " have scheduled messages on server = " << d->has_scheduled_server_messages
            << ", in database = " << d->has_scheduled_database_messages
            << " and in memory = " << have_dialog_scheduled_messages_in_memory(d)
            << "; was loaded from database = " << d->has_loaded_scheduled_messages_from_database;
  bool has_scheduled_messages = get_dialog_has_scheduled_messages(d);
  if (has_scheduled_messages == d->last_sent_has_scheduled_messages) {
    return;
  }
  d->last_sent_has_scheduled_messages = has_scheduled_messages;

  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in send_update_chat_has_scheduled_messages";
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatHasScheduledMessages>(
                   get_chat_id_object(d->dialog_id, "updateChatHasScheduledMessages"), has_scheduled_messages));
}

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,
               td_api::make_object<td_api::updateMessageSendAcknowledged>(
                   get_chat_id_object(dialog_id, "updateMessageSendAcknowledged"), message_id.get()));
}

bool MessagesManager::is_invalid_poll_message(const telegram_api::Message *message) {
  CHECK(message != nullptr);
  auto constructor_id = message->get_id();
  if (constructor_id != telegram_api::message::ID) {
    return false;
  }

  auto media = static_cast<const telegram_api::message *>(message)->media_.get();
  if (media == nullptr || media->get_id() != telegram_api::messageMediaPoll::ID) {
    return false;
  }

  auto poll = static_cast<const telegram_api::messageMediaPoll *>(media)->poll_.get();
  return !PollId(poll->id_).is_valid();
}

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 = UpdatesManager::get_new_messages(updates_ptr);
  auto sent_messages_random_ids = UpdatesManager::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 ||
      DialogId::get_message_dialog_id(sent_messages[0].first) != dialog_id ||
      is_invalid_poll_message(sent_messages[0].first)) {
    LOG(ERROR) << "Receive wrong result for sending message with random_id " << random_id << " from " << source
               << " to " << dialog_id << ": " << oneline(to_string(*updates_ptr));
    Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr);
    if (dialog_id.get_type() == DialogType::Channel) {
      get_channel_difference(dialog_id, d->pts, 0, MessageId(), true, "check_send_message_result");
    } else {
      td_->updates_manager_->schedule_get_difference("check_send_message_result");
    }
    repair_dialog_scheduled_messages(d);
  }
}

void MessagesManager::update_reply_to_message_id(DialogId dialog_id, MessageId old_message_id, MessageId new_message_id,
                                                 bool have_new_message, const char *source) {
  LOG(INFO) << "Update replies of " << MessageFullId{dialog_id, old_message_id} << " to " << new_message_id << " from "
            << source;
  auto it = replied_yet_unsent_messages_.find({dialog_id, old_message_id});
  if (it == replied_yet_unsent_messages_.end()) {
    return;
  }
  CHECK(old_message_id.is_yet_unsent());

  for (auto message_full_id : it->second) {
    auto reply_d = get_dialog(message_full_id.get_dialog_id());
    CHECK(reply_d != nullptr);
    auto replied_m = get_message(reply_d, message_full_id.get_message_id());
    CHECK(replied_m != nullptr);
    const auto *input_reply_to = get_message_input_reply_to(replied_m);
    CHECK(input_reply_to != nullptr);
    CHECK(input_reply_to->get_reply_message_full_id(reply_d->dialog_id) == MessageFullId(dialog_id, old_message_id));
    if (new_message_id != MessageId()) {
      update_message_reply_to_message_id(reply_d, replied_m, new_message_id, true);
    } else {
      set_message_reply(reply_d, replied_m, MessageInputReplyTo(), true);
    }
  }
  if (have_new_message) {
    CHECK(!new_message_id.is_yet_unsent());
    replied_by_yet_unsent_messages_[MessageFullId{dialog_id, new_message_id}] = static_cast<int32>(it->second.size());
  } else {
    replied_by_yet_unsent_messages_.erase(MessageFullId{dialog_id, new_message_id});
  }
  replied_yet_unsent_messages_.erase(it);
}

MessageFullId MessagesManager::on_send_message_success(int64 random_id, MessageId new_message_id, int32 date,
                                                       int32 ttl_period, FileId new_file_id, const char *source) {
  CHECK(source != nullptr);
  // do not try to run getDifference from this function
  if (DROP_SEND_MESSAGE_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 identifier as sent message identifier"));
    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";
    return {};
  }

  auto dialog_id = it->second.get_dialog_id();
  auto old_message_id = it->second.get_message_id();

  if (new_message_id.is_local() && dialog_id.get_type() != DialogType::SecretChat) {
    LOG(ERROR) << "Receive " << new_message_id << " as sent message from " << source;
    on_send_message_fail(random_id, Status::Error(500, "Internal Server Error: receive local as sent message"));
    return {};
  }

  being_sent_messages_.erase(it);

  // must be called before delete_message
  update_reply_to_message_id(dialog_id, old_message_id, new_message_id, true, "on_send_message_success");

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  bool need_update_dialog_pos = false;
  being_readded_message_id_ = {dialog_id, old_message_id};
  auto sent_message = delete_message(d, old_message_id, false, &need_update_dialog_pos, source);
  if (sent_message == nullptr) {
    delete_sent_message_on_server(dialog_id, new_message_id, old_message_id);
    being_readded_message_id_ = MessageFullId();
    return {};
  }

  // imitation of update_message(d, sent_message.get(), std::move(new_message), false);
  if (date <= 0) {
    LOG(ERROR) << "Receive " << new_message_id << " in " << dialog_id << " with wrong date " << date << " from "
               << source;
  } else {
    LOG_CHECK(sent_message->date > 0) << old_message_id << ' ' << sent_message->message_id << ' ' << new_message_id
                                      << ' ' << sent_message->date << ' ' << date << ' ' << source;
    sent_message->date = date;
    CHECK(d->last_message_id != old_message_id);
  }

  sent_message->ttl_period = ttl_period;

  if (merge_message_content_file_id(td_, sent_message->content.get(), new_file_id)) {
    send_update_message_content(d, sent_message.get(), false, source);
  }

  const auto *input_reply_to = get_message_input_reply_to(sent_message.get());
  if (input_reply_to != nullptr && input_reply_to->is_valid() &&
      input_reply_to->get_reply_message_full_id(dialog_id).get_message_id().is_yet_unsent()) {
    set_message_reply(d, sent_message.get(), MessageInputReplyTo(), false);
  }

  sent_message->message_id = new_message_id;

  send_update_message_send_succeeded(d, old_message_id, sent_message.get(), &need_update_dialog_pos);

  bool need_update = true;
  Message *m =
      add_message_to_dialog(d, std::move(sent_message), false, true, &need_update, &need_update_dialog_pos, source);
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, source);
  }

  if (m == nullptr) {
    if (!(old_message_id.is_valid() && new_message_id < old_message_id) &&
        !(ttl_period > 0 && date + ttl_period <= G()->server_time())) {
      // if message ID has decreased, which could happen if some messages were lost,
      // or the message has already been deleted after auto-delete timer expired, then the error is expected
      LOG(ERROR) << "Failed to add just sent " << old_message_id << " to " << dialog_id << " as " << new_message_id
                 << " from " << source << ": " << debug_add_message_to_dialog_fail_reason_;
    }
    send_update_delete_messages(dialog_id, {new_message_id.get()}, true);
    being_readded_message_id_ = MessageFullId();
    return {};
  }

  try_add_active_live_location(dialog_id, m);
  update_reply_count_by_message(d, +1, m);
  update_forward_count(dialog_id, m);
  being_readded_message_id_ = MessageFullId();
  return {dialog_id, new_message_id};
}

void MessagesManager::on_send_message_file_parts_missing(int64 random_id, vector<int> &&bad_parts) {
  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(INFO) << "Receive error for successfully sent message with random_id = " << random_id;
    return;
  }

  auto message_full_id = it->second;

  being_sent_messages_.erase(it);

  Message *m = get_message(message_full_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) << "Don't need to send already deleted by the user or sent to an inaccessible chat " << message_full_id;
    return;
  }

  auto dialog_id = message_full_id.get_dialog_id();
  if (dialog_id.get_type() == DialogType::SecretChat) {
    CHECK(!m->message_id.is_scheduled());
    Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr);

    delete_random_id_to_message_id_correspondence(d, m->random_id, m->message_id);

    // need to change message random_id before resending
    m->random_id = generate_new_random_id(d);

    add_random_id_to_message_id_correspondence(d, m->random_id, m->message_id);

    auto log_event = SendMessageLogEvent(dialog_id, m);
    CHECK(m->send_message_log_event_id != 0);
    binlog_rewrite(G()->td_db()->get_binlog(), m->send_message_log_event_id, LogEvent::HandlerType::SendMessage,
                   get_log_event_storer(log_event));
  }

  do_send_message(dialog_id, m, std::move(bad_parts));
}

void MessagesManager::on_send_message_file_reference_error(int64 random_id) {
  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 reference invalid error about successfully sent message with random_id = "
                 << random_id;
    return;
  }

  auto message_full_id = it->second;

  being_sent_messages_.erase(it);

  Message *m = get_message(message_full_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) << "Don't need to send already deleted by the user or sent to an inaccessible chat " << message_full_id;
    return;
  }

  auto dialog_id = message_full_id.get_dialog_id();
  if (dialog_id.get_type() == DialogType::SecretChat) {
    CHECK(!m->message_id.is_scheduled());
    Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr);

    delete_random_id_to_message_id_correspondence(d, m->random_id, m->message_id);

    // need to change message random_id before resending
    m->random_id = generate_new_random_id(d);

    add_random_id_to_message_id_correspondence(d, m->random_id, m->message_id);

    auto log_event = SendMessageLogEvent(dialog_id, m);
    CHECK(m->send_message_log_event_id != 0);
    binlog_rewrite(G()->td_db()->get_binlog(), m->send_message_log_event_id, LogEvent::HandlerType::SendMessage,
                   get_log_event_storer(log_event));
  }

  do_send_message(dialog_id, m, {-1});
}

void MessagesManager::on_send_media_group_file_reference_error(DialogId dialog_id, vector<int64> random_ids) {
  int64 media_album_id = 0;
  vector<MessageId> message_ids;
  vector<Message *> messages;
  for (auto &random_id : random_ids) {
    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(ERROR) << "Receive file reference invalid error about successfully sent message with random_id = "
                 << random_id;
      continue;
    }

    auto message_full_id = it->second;

    being_sent_messages_.erase(it);

    Message *m = get_message(message_full_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) << "Don't need to send already deleted by the user or sent to an inaccessible chat " << message_full_id;
      continue;
    }

    CHECK(m->media_album_id != 0);
    CHECK(media_album_id == 0 || media_album_id == m->media_album_id);
    media_album_id = m->media_album_id;

    CHECK(dialog_id == message_full_id.get_dialog_id());
    message_ids.push_back(m->message_id);
    messages.push_back(m);
  }

  CHECK(dialog_id.get_type() != DialogType::SecretChat);

  if (message_ids.empty()) {
    // all messages was deleted, nothing to do
    return;
  }

  auto &request = pending_message_group_sends_[media_album_id];
  CHECK(!request.dialog_id.is_valid());
  CHECK(request.finished_count == 0);
  CHECK(request.results.empty());
  request.dialog_id = dialog_id;
  request.message_ids = std::move(message_ids);
  request.is_finished.resize(request.message_ids.size(), false);
  for (size_t i = 0; i < request.message_ids.size(); i++) {
    request.results.push_back(Status::OK());
  }

  for (auto m : messages) {
    do_send_message(dialog_id, m, {-1});
  }
}

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::Canceled) {
      LOG(ERROR) << "Receive error " << error << " about successfully sent message with random_id = " << random_id;
    }
    return;
  }

  auto message_full_id = it->second;

  being_sent_messages_.erase(it);

  Message *m = get_message(message_full_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) << "Don't need to send already deleted by the user or sent to an inaccessible chat " << message_full_id;
    return;
  }
  LOG_IF(ERROR, error.code() == NetQuery::Canceled)
      << "Receive error " << error << " about sent message with random_id = " << random_id;

  auto dialog_id = message_full_id.get_dialog_id();
  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";
      } else if (error.message() == "MEDIA_CAPTION_TOO_LONG") {
        error_message = "Message caption is too long";
      } else if (error.message() == "INPUT_USER_DEACTIVATED") {
        error_code = 403;
        error_message = "User is deactivated";
      } else if (error.message() == "USER_IS_BOT") {
        if (td_->auth_manager_->is_bot() &&
            (dialog_id.get_type() == DialogType::User || dialog_id.get_type() == DialogType::SecretChat)) {
          error_code = 403;
          error_message = "Bots can't send messages to bots";
        }
      } else if (error.message() == "PEER_ID_INVALID") {
        error_code = 403;
        if (td_->auth_manager_->is_bot() &&
            (dialog_id.get_type() == DialogType::User || dialog_id.get_type() == DialogType::SecretChat)) {
          error_message = "Bot can't initiate conversation with a user";
        }
      } else if (error.message() == "CHAT_FORWARDS_RESTRICTED") {
        error_message = "Message has protected content and can't be forwarded";
      } 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 if (content_type == MessageContentType::Poll) {
          error_message = "Wrong poll data specified";
        } else if (content_type == MessageContentType::Contact) {
          error_message = "Wrong phone number specified";
        } else if (content_type == MessageContentType::Story) {
          error_message = "Wrong story data 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";
      } else if (error.message() == "QUOTE_TEXT_INVALID") {
        auto *input_reply_to = get_message_input_reply_to(m);
        if (input_reply_to != nullptr && !input_reply_to->is_empty() && input_reply_to->has_quote()) {
          auto reply_message_full_id = input_reply_to->get_reply_message_full_id(dialog_id);
          if (reply_message_full_id.get_message_id().is_valid()) {
            get_message_from_server(reply_message_full_id, Auto(), "QUOTE_TEXT_INVALID");
          }
        } else {
          error_code = 500;
          error_message = "Unexpected QUOTE_TEXT_INVALID error";
        }
      } else if (error.message() == "REPLY_MESSAGE_ID_INVALID") {
        auto *input_reply_to = get_message_input_reply_to(m);
        if (input_reply_to != nullptr && !input_reply_to->is_empty()) {
          auto reply_message_full_id = input_reply_to->get_reply_message_full_id(dialog_id);
          if (reply_message_full_id.get_message_id().is_valid()) {
            get_message_from_server(reply_message_full_id, Auto(), "REPLY_MESSAGE_ID_INVALID");
          }
        } else {
          error_code = 500;
          error_message = "Unexpected REPLY_MESSAGE_ID_INVALID error";
        }
      }
      break;
    case 403:
      if (error.message() == "CHAT_GUEST_SEND_FORBIDDEN") {
        error_code = 400;
        if (dialog_id.get_type() == DialogType::Channel) {
          td_->contacts_manager_->reload_channel(dialog_id.get_channel_id(), Promise<Unit>(),
                                                 "CHAT_GUEST_SEND_FORBIDDEN");
        }
      } 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, "on_send_message_fail")));
    } else {
      LOG(ERROR) << "Receive " << error.message() << " for " << message_full_id << " with keyboard "
                 << *m->reply_markup;
    }
  }
  if (error_code != 403 && !(error_code == 500 && G()->close_flag())) {
    LOG(WARNING) << "Failed to send " << message_full_id << " with the error " << error;
  }
  fail_send_message(message_full_id, error_code, error_message);
}

MessageId MessagesManager::get_next_message_id(Dialog *d, MessageType type) const {
  CHECK(d != nullptr);
  MessageId last_message_id;
  if (td_->auth_manager_->is_bot()) {
    last_message_id = max(d->last_assigned_message_id, d->max_unavailable_message_id);
  } else {
    last_message_id = std::max({d->last_message_id, d->last_new_message_id, d->last_database_message_id,
                                d->last_assigned_message_id, d->last_clear_history_message_id,
                                d->deleted_last_message_id, d->max_unavailable_message_id, d->max_added_message_id});
    if (last_message_id < d->last_read_inbox_message_id &&
        d->last_read_inbox_message_id < d->last_new_message_id.get_next_server_message_id()) {
      last_message_id = d->last_read_inbox_message_id;
    }
    if (last_message_id < d->last_read_outbox_message_id &&
        d->last_read_outbox_message_id < d->last_new_message_id.get_next_server_message_id()) {
      last_message_id = d->last_read_outbox_message_id;
    }
  }

  d->last_assigned_message_id = last_message_id.get_next_message_id(type);
  if (d->last_assigned_message_id > MessageId::max()) {
    LOG(FATAL) << "Force restart because of message_id overflow: " << d->last_assigned_message_id;
  }
  CHECK(d->last_assigned_message_id.is_valid());
  if (d->last_assigned_message_id.get_prev_server_message_id() != last_message_id.get_prev_server_message_id()) {
    d->had_yet_unsent_message_id_overflow = true;
  }
  return d->last_assigned_message_id;
}

MessageId MessagesManager::get_next_yet_unsent_message_id(Dialog *d) const {
  return get_next_message_id(d, MessageType::YetUnsent);
}

MessageId MessagesManager::get_next_local_message_id(Dialog *d) const {
  return get_next_message_id(d, MessageType::Local);
}

MessageId MessagesManager::get_next_yet_unsent_scheduled_message_id(Dialog *d, int32 date) {
  CHECK(date > 0);

  auto *scheduled_messages = add_dialog_scheduled_messages(d);
  MessageId message_id(ScheduledServerMessageId(1), date);
  for (const auto &it : d->scheduled_messages->scheduled_messages_) {
    if (it.first.get_scheduled_message_date() == date && it.first > message_id) {
      message_id = it.first;
    }
  };

  auto &last_assigned_message_id = scheduled_messages->last_assigned_scheduled_message_id_[date];
  if (last_assigned_message_id != MessageId() && last_assigned_message_id > message_id) {
    message_id = last_assigned_message_id;
  }

  last_assigned_message_id = message_id.get_next_message_id(MessageType::YetUnsent);
  return last_assigned_message_id;
}

void MessagesManager::fail_send_message(MessageFullId message_full_id, int32 error_code, const string &error_message) {
  if (error_code <= 0) {
    error_code = 500;
  }

  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  MessageId old_message_id = message_full_id.get_message_id();
  CHECK(old_message_id.is_valid() || old_message_id.is_valid_scheduled());
  CHECK(old_message_id.is_yet_unsent());

  // must be called before delete_message
  update_reply_to_message_id(dialog_id, old_message_id, MessageId(), false, "fail_send_message");

  bool need_update_dialog_pos = false;
  being_readded_message_id_ = message_full_id;
  auto 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
    being_readded_message_id_ = MessageFullId();
    return;
  }

  MessageId new_message_id = old_message_id.get_next_message_id(MessageType::Local);  // trying to keep message position
  if (!old_message_id.is_scheduled()) {
    if (get_message_force(d, new_message_id, "fail_send_message") != nullptr || is_deleted_message(d, new_message_id) ||
        new_message_id <= d->last_clear_history_message_id) {
      new_message_id = get_next_local_message_id(d);
    } else if (new_message_id > d->last_assigned_message_id) {
      d->last_assigned_message_id = new_message_id;
    }
  } else {
    while (get_message_force(d, new_message_id, "fail_send_message") != nullptr ||
           is_deleted_message(d, new_message_id)) {
      new_message_id = new_message_id.get_next_message_id(MessageType::Local);
    }
  }

  message->message_id = new_message_id;
  if (old_message_id.is_scheduled()) {
    CHECK(message->message_id.is_valid_scheduled());
  } else {
    CHECK(message->message_id.is_valid());
  }
  if (message->forward_info == nullptr && message->view_count == 1) {
    message->view_count = 0;
  }
  message->is_failed_to_send = true;
  message->send_error_code = error_code;
  message->send_error_message = error_message;
  message->try_resend_at = 0.0;
  auto retry_after = Global::get_retry_after(error_code, error_message);
  if (retry_after > 0) {
    message->try_resend_at = Time::now() + retry_after;
  }
  update_failed_to_send_message_content(td_, message->content);

  bool need_update = false;
  Message *m = add_message_to_dialog(d, std::move(message), false, true, &need_update, &need_update_dialog_pos,
                                     "fail_send_message");
  LOG_CHECK(m != nullptr) << "Failed to add failed to send " << new_message_id << " to " << dialog_id << " due to "
                          << debug_add_message_to_dialog_fail_reason_;
  if (!m->message_id.is_scheduled()) {
    // add_message_to_dialog will not update counts, because need_update == false
    update_message_count_by_index(d, +1, m);
    update_reply_count_by_message(d, +1, m);  // no-op because the message isn't server
  }
  register_new_local_message_id(d, m);

  LOG(INFO) << "Send updateMessageSendFailed for " << message_full_id;
  if (!td_->auth_manager_->is_bot()) {
    yet_unsent_message_full_id_to_persistent_message_id_.emplace({dialog_id, old_message_id}, m->message_id);
  }
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateMessageSendFailed>(
                   get_message_object(dialog_id, m, "fail_send_message"), old_message_id.get(),
                   td_api::make_object<td_api::error>(error_code, error_message)));
  if (need_update_dialog_pos) {
    send_update_chat_last_message(d, "fail_send_message");
  }
  being_readded_message_id_ = MessageFullId();
}

void MessagesManager::fail_send_message(MessageFullId message_full_id, Status error) {
  fail_send_message(message_full_id, error.code(), error.message().str());
}

void MessagesManager::fail_edit_message_media(MessageFullId message_full_id, Status &&error) {
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  MessageId message_id = message_full_id.get_message_id();
  CHECK(message_id.is_any_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, "Failed to edit message. MUST BE IGNORED");
}

void MessagesManager::on_update_dialog_draft_message(DialogId dialog_id, MessageId top_thread_message_id,
                                                     tl_object_ptr<telegram_api::DraftMessage> &&draft_message,
                                                     bool force) {
  if (G()->close_flag()) {
    return;
  }
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive update of draft message in invalid " << dialog_id;
    return;
  }
  if (td_->auth_manager_->is_bot()) {
    if (draft_message != nullptr && draft_message->get_id() != telegram_api::draftMessageEmpty::ID) {
      LOG(ERROR) << "Receive update of draft message in " << dialog_id;
    }
    return;
  }
  auto d = get_dialog_force(dialog_id, "on_update_dialog_draft_message");
  if (d == nullptr) {
    LOG(INFO) << "Ignore update chat draft in unknown " << dialog_id;
    if (draft_message != nullptr && draft_message->get_id() != telegram_api::draftMessageEmpty::ID) {
      if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
        LOG(ERROR) << "Have no read access to " << dialog_id << " to repair chat draft message";
      } else {
        send_get_dialog_query(dialog_id, Auto(), 0, "on_update_dialog_draft_message");
      }
    }
    return;
  }
  if (top_thread_message_id.is_valid()) {
    // TODO update thread message draft
    return;
  }

  if (!force && draft_message != nullptr && draft_message->get_id() == telegram_api::draftMessage::ID) {
    auto *input_reply_to = static_cast<const telegram_api::draftMessage *>(draft_message.get())->reply_to_.get();
    if (input_reply_to != nullptr) {
      InputDialogId input_dialog_id;
      switch (input_reply_to->get_id()) {
        case telegram_api::inputReplyToStory::ID: {
          auto reply_to = static_cast<const telegram_api::inputReplyToStory *>(input_reply_to);
          input_dialog_id = InputDialogId(reply_to->peer_);
          break;
        }
        case telegram_api::inputReplyToMessage::ID: {
          auto reply_to = static_cast<const telegram_api::inputReplyToMessage *>(input_reply_to);
          if (reply_to->reply_to_peer_id_ != nullptr) {
            input_dialog_id = InputDialogId(reply_to->reply_to_peer_id_);
          }
          break;
        }
        default:
          UNREACHABLE();
      }
      auto reply_in_dialog_id = input_dialog_id.get_dialog_id();
      if (reply_in_dialog_id.is_valid() && !have_dialog_force(reply_in_dialog_id, "on_update_dialog_draft_message")) {
        td_->dialog_filter_manager_->load_input_dialog(
            input_dialog_id, [actor_id = actor_id(this), dialog_id, top_thread_message_id,
                              draft_message = std::move(draft_message)](Unit) mutable {
              send_closure(actor_id, &MessagesManager::on_update_dialog_draft_message, dialog_id, top_thread_message_id,
                           std::move(draft_message), true);
            });
        return;
      }
    }
  }
  update_dialog_draft_message(d, get_draft_message(td_, 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 (!td_->auth_manager_->is_bot() && need_update_draft_message(d->draft_message, draft_message, from_update)) {
    d->draft_message = std::move(draft_message);
    if (need_update_dialog_pos) {
      update_dialog_pos(d, "update_dialog_draft_message", false);
    }
    on_dialog_updated(d->dialog_id, "update_dialog_draft_message");
    send_update_chat_draft_message(d);
    return true;
  }
  return false;
}

void MessagesManager::clear_dialog_draft_by_sent_message(Dialog *d, const Message *m, bool need_update_dialog_pos) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  if (!m->clear_draft) {
    const DraftMessage *draft_message = nullptr;
    if (m->initial_top_thread_message_id.is_valid()) {
      auto top_m = get_message_force(d, m->initial_top_thread_message_id, "clear_dialog_draft_by_sent_message");
      if (top_m != nullptr) {
        draft_message = top_m->thread_draft_message.get();
      }
    } else {
      draft_message = d->draft_message.get();
    }
    if (draft_message == nullptr || !draft_message->need_clear_local(m->content->get_type())) {
      return;
    }
  }
  if (m->initial_top_thread_message_id.is_valid()) {
    set_dialog_draft_message(d->dialog_id, m->initial_top_thread_message_id, nullptr).ignore();
  } else {
    update_dialog_draft_message(d, nullptr, false, need_update_dialog_pos);
  }
}

void MessagesManager::on_update_dialog_is_pinned(FolderId folder_id, DialogId dialog_id, bool is_pinned) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive pin of invalid " << dialog_id;
    return;
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_is_pinned");
  if (d == nullptr) {
    LOG(INFO) << "Can't apply updateDialogPinned in " << folder_id << " with unknown " << dialog_id;
    on_update_pinned_dialogs(folder_id);
    return;
  }
  if (d->order == DEFAULT_ORDER) {
    // the chat can't be pinned or is already unpinned
    // don't change it's folder_id
    LOG(INFO) << "Can't apply updateDialogPinned in " << folder_id << " with " << dialog_id;
    return;
  }

  auto *list = get_dialog_list(DialogListId(folder_id));
  CHECK(list != nullptr);
  if (!list->are_pinned_dialogs_inited_) {
    return;
  }

  set_dialog_folder_id(d, folder_id);
  set_dialog_is_pinned(DialogListId(folder_id), d, is_pinned);
}

void MessagesManager::on_update_pinned_dialogs(FolderId folder_id) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  // TODO log event + delete_log_event_promise

  auto *list = get_dialog_list(DialogListId(folder_id));
  if (list == nullptr || !list->are_pinned_dialogs_inited_) {
    return;
  }
  // preload all pinned dialogs
  int32 limit = narrow_cast<int32>(list->pinned_dialogs_.size()) +
                (folder_id == FolderId::main() && sponsored_dialog_id_.is_valid() ? 1 : 0);
  get_dialogs_from_list(DialogListId(folder_id), limit, Auto());
  reload_pinned_dialogs(DialogListId(folder_id), Auto());
}

void MessagesManager::on_update_dialog_view_as_topics(const Dialog *d, bool old_view_as_topics) {
  auto new_view_as_topics = get_dialog_view_as_topics(d);
  if (old_view_as_topics == new_view_as_topics) {
    return;
  }

  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in on_update_dialog_view_as_topics";
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatViewAsTopics>(
                   get_chat_id_object(d->dialog_id, "updateChatViewAsTopics"), new_view_as_topics));

  if (d->dialog_id != td_->dialog_manager_->get_my_dialog_id() && d->draft_message != nullptr &&
      can_send_message(d->dialog_id).is_ok()) {
    // need_hide_dialog_draft_message has changed and there is draft message
    send_update_chat_draft_message(d);
  }

  // TODO update unread counters in chat lists
}

void MessagesManager::on_update_dialog_is_forum(DialogId dialog_id, bool is_forum) {
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive is_forum for invalid " << dialog_id;
    return;
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_is_forum");
  if (d == nullptr) {
    // nothing to do
    return;
  }
  set_dialog_is_forum(d, is_forum);
}

void MessagesManager::set_dialog_is_forum(Dialog *d, bool is_forum) {
  CHECK(d != nullptr);
  if (d->is_forum == is_forum) {
    return;
  }
  auto old_view_as_topics = get_dialog_view_as_topics(d);
  d->is_forum = is_forum;
  on_dialog_updated(d->dialog_id, "set_dialog_is_forum");

  LOG(INFO) << "Set " << d->dialog_id << " is_forum to " << is_forum;
  on_update_dialog_view_as_topics(d, old_view_as_topics);
}

void MessagesManager::on_update_dialog_view_as_messages(DialogId dialog_id, bool view_as_messages) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive view_as_messages for invalid " << dialog_id;
    return;
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_view_as_messages");
  if (d == nullptr) {
    // nothing to do
    return;
  }
  set_dialog_view_as_messages(d, view_as_messages, "on_update_dialog_view_as_messages");
}

void MessagesManager::set_dialog_view_as_messages(Dialog *d, bool view_as_messages, const char *source) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  CHECK(d != nullptr);
  if (d->view_as_messages == view_as_messages) {
    if (!d->is_view_as_messages_inited) {
      d->is_view_as_messages_inited = true;
      on_dialog_updated(d->dialog_id, source);
    }
    return;
  }
  auto old_view_as_topics = get_dialog_view_as_topics(d);
  d->view_as_messages = view_as_messages;
  d->is_view_as_messages_inited = true;
  on_dialog_updated(d->dialog_id, source);

  LOG(INFO) << "Set " << d->dialog_id << " view_as_messages to " << view_as_messages << " from " << source;
  on_update_dialog_view_as_topics(d, old_view_as_topics);
}

void MessagesManager::on_update_dialog_is_marked_as_unread(DialogId dialog_id, bool is_marked_as_unread) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive marking as unread of invalid " << dialog_id;
    return;
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_is_marked_as_unread");
  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) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  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;
  LOG_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,
               td_api::make_object<td_api::updateChatIsMarkedAsUnread>(
                   get_chat_id_object(d->dialog_id, "updateChatIsMarkedAsUnread"), is_marked_as_unread));

  if (d->server_unread_count + d->local_unread_count == 0 && need_unread_counter(d->order)) {
    int32 delta = d->is_marked_as_unread ? 1 : -1;
    for (auto &list : get_dialog_lists(d)) {
      if (list.is_dialog_unread_count_inited_) {
        list.unread_dialog_total_count_ += delta;
        list.unread_dialog_marked_count_ += delta;
        if (is_dialog_muted(d)) {
          list.unread_dialog_muted_count_ += delta;
          list.unread_dialog_muted_marked_count_ += delta;
        }
        send_update_unread_chat_count(list, d->dialog_id, true, "set_dialog_is_marked_as_unread");
      }
    }

    if (td_->dialog_filter_manager_->have_dialog_filters()) {
      update_dialog_lists(d, get_dialog_positions(d), true, false, "set_dialog_is_marked_as_unread");
    }
  }
}

void MessagesManager::on_update_dialog_is_translatable(DialogId dialog_id, bool is_translatable) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive marking as unread of invalid " << dialog_id;
    return;
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_is_translatable");
  if (d == nullptr) {
    // nothing to do
    return;
  }

  if (is_translatable == d->is_translatable) {
    return;
  }

  set_dialog_is_translatable(d, is_translatable);
}

void MessagesManager::update_is_translatable(bool new_is_premium) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  dialogs_.foreach([&](const DialogId &dialog_id, const unique_ptr<Dialog> &dialog) {
    if (dialog->is_translatable) {
      send_closure(G()->td(), &Td::send_update,
                   td_api::make_object<td_api::updateChatIsTranslatable>(
                       get_chat_id_object(dialog_id, "updateChatIsTranslatable"), new_is_premium));
    }
  });
}

void MessagesManager::set_dialog_is_translatable(Dialog *d, bool is_translatable) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }

  CHECK(d != nullptr);
  CHECK(d->is_translatable != is_translatable);
  d->is_translatable = is_translatable;
  on_dialog_updated(d->dialog_id, "set_dialog_is_translatable");

  LOG(INFO) << "Set " << d->dialog_id << " is translatable to " << is_translatable;
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in set_dialog_is_translatable";
  bool is_premium = td_->option_manager_->get_option_boolean("is_premium");
  if (is_premium) {
    send_closure(G()->td(), &Td::send_update,
                 td_api::make_object<td_api::updateChatIsTranslatable>(
                     get_chat_id_object(d->dialog_id, "updateChatIsTranslatable"), is_translatable));
  }
}

void MessagesManager::on_update_dialog_is_blocked(DialogId dialog_id, bool is_blocked, bool is_blocked_for_stories) {
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive pinned message in invalid " << dialog_id;
    return;
  }
  if (dialog_id.get_type() == DialogType::User) {
    td_->contacts_manager_->on_update_user_is_blocked(dialog_id.get_user_id(), is_blocked, is_blocked_for_stories);
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_is_blocked");
  if (d == nullptr) {
    // nothing to do
    return;
  }

  if (d->is_blocked == is_blocked && d->is_blocked_for_stories == is_blocked_for_stories) {
    if (!d->is_is_blocked_for_stories_inited) {
      CHECK(is_blocked_for_stories == false);
      d->is_is_blocked_inited = true;
      d->is_is_blocked_for_stories_inited = true;
      on_dialog_updated(dialog_id, "on_update_dialog_is_blocked");
    }
    return;
  }

  set_dialog_is_blocked(d, is_blocked, is_blocked_for_stories);
}

void MessagesManager::set_dialog_is_blocked(Dialog *d, bool is_blocked, bool is_blocked_for_stories) {
  CHECK(d != nullptr);
  CHECK(d->is_blocked != is_blocked || d->is_blocked_for_stories != is_blocked_for_stories);
  d->is_blocked = is_blocked;
  d->is_blocked_for_stories = is_blocked_for_stories;
  d->is_is_blocked_inited = true;
  d->is_is_blocked_for_stories_inited = true;
  on_dialog_updated(d->dialog_id, "set_dialog_is_blocked");

  LOG(INFO) << "Set " << d->dialog_id << " is_blocked to " << is_blocked << '/' << is_blocked_for_stories;
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in set_dialog_is_blocked";
  auto block_list_id = BlockListId(d->is_blocked, d->is_blocked_for_stories);
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatBlockList>(get_chat_id_object(d->dialog_id, "updateChatBlockList"),
                                                                block_list_id.get_block_list_object()));

  if (d->dialog_id.get_type() == DialogType::User) {
    td_->contacts_manager_->on_update_user_is_blocked(d->dialog_id.get_user_id(), is_blocked, is_blocked_for_stories);

    if (d->know_action_bar) {
      if (is_blocked) {
        if (d->action_bar != nullptr) {
          d->action_bar = nullptr;
          send_update_chat_action_bar(d);
        }
      } else {
        repair_dialog_action_bar(d, "on_dialog_user_is_blocked_updated");
      }
    }

    td_->contacts_manager_->for_each_secret_chat_with_user(
        d->dialog_id.get_user_id(), [this, is_blocked, is_blocked_for_stories](SecretChatId secret_chat_id) {
          DialogId dialog_id(secret_chat_id);
          auto d = get_dialog(dialog_id);  // must not create the dialog
          if (d != nullptr && d->is_update_new_chat_sent &&
              (d->is_blocked != is_blocked || d->is_blocked_for_stories != is_blocked_for_stories)) {
            set_dialog_is_blocked(d, is_blocked, is_blocked_for_stories);
          }
        });
  }
}

void MessagesManager::on_update_dialog_last_pinned_message_id(DialogId dialog_id, MessageId pinned_message_id) {
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive pinned message in invalid " << dialog_id;
    return;
  }
  if (!pinned_message_id.is_valid() && pinned_message_id != MessageId()) {
    LOG(ERROR) << "Receive as pinned message " << pinned_message_id;
    return;
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_last_pinned_message_id");
  if (d == nullptr) {
    // nothing to do
    return;
  }

  set_dialog_last_pinned_message_id(d, pinned_message_id);
}

void MessagesManager::set_dialog_last_pinned_message_id(Dialog *d, MessageId pinned_message_id) {
  CHECK(d != nullptr);
  Message *m = get_message_force(d, pinned_message_id, "set_dialog_last_pinned_message_id");
  if (m != nullptr && update_message_is_pinned(d, m, true, "set_dialog_last_pinned_message_id")) {
    on_message_changed(d, m, true, "set_dialog_last_pinned_message_id");
  }

  if (d->is_last_pinned_message_id_inited && d->last_pinned_message_id == pinned_message_id) {
    return;
  }
  d->last_pinned_message_id = pinned_message_id;
  d->is_last_pinned_message_id_inited = true;
  on_dialog_updated(d->dialog_id, "set_dialog_last_pinned_message_id");

  LOG(INFO) << "Set " << d->dialog_id << " pinned message to " << pinned_message_id;
}

void MessagesManager::drop_dialog_last_pinned_message_id(Dialog *d) {
  d->last_pinned_message_id = MessageId();
  d->is_last_pinned_message_id_inited = false;
  on_dialog_updated(d->dialog_id, "drop_dialog_last_pinned_message_id");

  LOG(INFO) << "Drop " << d->dialog_id << " pinned message";

  create_actor<SleepActor>(
      "ReloadDialogFullInfoActor", 1.0,
      PromiseCreator::lambda([actor_id = G()->dialog_manager(), dialog_id = d->dialog_id](Result<Unit> result) {
        send_closure(actor_id, &DialogManager::reload_dialog_info_full, dialog_id,
                     "drop_dialog_last_pinned_message_id");
      }))
      .release();
}

void MessagesManager::on_update_dialog_background(DialogId dialog_id,
                                                  telegram_api::object_ptr<telegram_api::WallPaper> &&wallpaper) {
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive background in invalid " << dialog_id;
    return;
  }
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_background");
  if (d == nullptr) {
    // nothing to do
    return;
  }

  set_dialog_background(d, BackgroundInfo(td_, std::move(wallpaper), true));
}

void MessagesManager::set_dialog_background(Dialog *d, BackgroundInfo &&background_info) {
  CHECK(d != nullptr);
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  bool is_changed = d->background_info != background_info;
  if (!is_changed && d->is_background_inited) {
    return;
  }
  d->background_info = std::move(background_info);
  d->is_background_inited = true;

  if (is_changed) {
    LOG(INFO) << "Set " << d->dialog_id << " background to " << d->background_info;
    send_update_chat_background(d);
  } else {
    on_dialog_updated(d->dialog_id, "set_dialog_background");
  }
}

void MessagesManager::on_update_dialog_theme_name(DialogId dialog_id, string theme_name) {
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive theme in invalid " << dialog_id;
    return;
  }
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_theme_name");
  if (d == nullptr) {
    // nothing to do
    return;
  }

  set_dialog_theme_name(d, std::move(theme_name));
}

void MessagesManager::set_dialog_theme_name(Dialog *d, string theme_name) {
  CHECK(d != nullptr);
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  bool is_changed = d->theme_name != theme_name;
  if (!is_changed && d->is_theme_name_inited) {
    return;
  }
  d->theme_name = std::move(theme_name);
  d->is_theme_name_inited = true;

  if (is_changed) {
    LOG(INFO) << "Set " << d->dialog_id << " theme to \"" << d->theme_name << '"';
    send_update_chat_theme(d);
  } else {
    on_dialog_updated(d->dialog_id, "set_dialog_theme_name");
  }
}

void MessagesManager::drop_dialog_pending_join_requests(DialogId dialog_id) {
  CHECK(dialog_id.is_valid());
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  auto d = get_dialog(dialog_id);  // called from update_chat/channel, must not create the dialog
  if (d != nullptr && d->is_update_new_chat_sent) {
    set_dialog_pending_join_requests(d, 0, {});
  }
}

void MessagesManager::on_update_dialog_pending_join_requests(DialogId dialog_id, int32 pending_join_request_count,
                                                             vector<int64> pending_requesters) {
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive pending join request count in invalid " << dialog_id;
    return;
  }
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_pending_join_request_count");
  if (d == nullptr) {
    // nothing to do
    return;
  }

  set_dialog_pending_join_requests(d, pending_join_request_count, UserId::get_user_ids(pending_requesters));
}

void MessagesManager::fix_pending_join_requests(DialogId dialog_id, int32 &pending_join_request_count,
                                                vector<UserId> &pending_join_request_user_ids) const {
  td::remove_if(pending_join_request_user_ids, [](UserId user_id) { return !user_id.is_valid(); });

  bool need_drop_pending_join_requests = [&] {
    if (pending_join_request_count < 0) {
      return true;
    }
    switch (dialog_id.get_type()) {
      case DialogType::User:
      case DialogType::SecretChat:
        return true;
      case DialogType::Chat: {
        auto chat_id = dialog_id.get_chat_id();
        auto status = td_->contacts_manager_->get_chat_status(chat_id);
        if (!status.can_manage_invite_links()) {
          return true;
        }
        break;
      }
      case DialogType::Channel: {
        auto channel_id = dialog_id.get_channel_id();
        auto status = td_->contacts_manager_->get_channel_permissions(channel_id);
        if (!status.can_manage_invite_links()) {
          return true;
        }
        break;
      }
      case DialogType::None:
      default:
        UNREACHABLE();
    }
    return false;
  }();
  if (need_drop_pending_join_requests) {
    pending_join_request_count = 0;
    pending_join_request_user_ids.clear();
  } else if (static_cast<size_t>(pending_join_request_count) < pending_join_request_user_ids.size()) {
    LOG(ERROR) << "Fix pending join request count from " << pending_join_request_count << " to "
               << pending_join_request_user_ids.size();
    pending_join_request_count = narrow_cast<int32>(pending_join_request_user_ids.size());
  }

  static constexpr size_t MAX_PENDING_JOIN_REQUESTS = 3;
  if (pending_join_request_user_ids.size() > MAX_PENDING_JOIN_REQUESTS) {
    pending_join_request_user_ids.resize(MAX_PENDING_JOIN_REQUESTS);
  }
}

void MessagesManager::set_dialog_pending_join_requests(Dialog *d, int32 pending_join_request_count,
                                                       vector<UserId> pending_join_request_user_ids) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  fix_pending_join_requests(d->dialog_id, pending_join_request_count, pending_join_request_user_ids);
  if (d->pending_join_request_count == pending_join_request_count &&
      d->pending_join_request_user_ids == pending_join_request_user_ids) {
    return;
  }
  d->pending_join_request_count = pending_join_request_count;
  d->pending_join_request_user_ids = std::move(pending_join_request_user_ids);
  send_update_chat_pending_join_requests(d);
}

void MessagesManager::repair_dialog_scheduled_messages(Dialog *d) {
  if (td_->auth_manager_->is_bot() || d->dialog_id.get_type() == DialogType::SecretChat) {
    return;
  }

  if (d->last_repair_scheduled_messages_generation == scheduled_messages_sync_generation_) {
    return;
  }
  d->last_repair_scheduled_messages_generation = scheduled_messages_sync_generation_;

  // TODO create log event
  auto dialog_id = d->dialog_id;
  LOG(INFO) << "Repair scheduled messages in " << dialog_id << " with generation "
            << d->last_repair_scheduled_messages_generation;
  get_dialog_scheduled_messages(dialog_id, false, true,
                                PromiseCreator::lambda([actor_id = actor_id(this), dialog_id](Unit) {
                                  send_closure(G()->messages_manager(), &MessagesManager::get_dialog_scheduled_messages,
                                               dialog_id, true, true, Promise<Unit>());
                                }));
}

void MessagesManager::on_update_dialog_has_scheduled_server_messages(DialogId dialog_id,
                                                                     bool has_scheduled_server_messages) {
  CHECK(dialog_id.is_valid());
  if (td_->auth_manager_->is_bot() || dialog_id.get_type() == DialogType::SecretChat) {
    return;
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_has_scheduled_server_messages");
  if (d == nullptr) {
    // nothing to do
    return;
  }

  LOG(INFO) << "Receive has_scheduled_server_messages = " << has_scheduled_server_messages << " in " << dialog_id;
  if (d->has_scheduled_server_messages != has_scheduled_server_messages) {
    set_dialog_has_scheduled_server_messages(d, has_scheduled_server_messages);
  } else if (has_scheduled_server_messages !=
             (d->has_scheduled_database_messages || have_dialog_scheduled_messages_in_memory(d))) {
    repair_dialog_scheduled_messages(d);
  }
}

void MessagesManager::set_dialog_has_scheduled_server_messages(Dialog *d, bool has_scheduled_server_messages) {
  CHECK(d != nullptr);
  CHECK(d->has_scheduled_server_messages != has_scheduled_server_messages);
  d->has_scheduled_server_messages = has_scheduled_server_messages;
  repair_dialog_scheduled_messages(d);
  on_dialog_updated(d->dialog_id, "set_dialog_has_scheduled_server_messages");

  LOG(INFO) << "Set " << d->dialog_id << " has_scheduled_server_messages to " << has_scheduled_server_messages;

  send_update_chat_has_scheduled_messages(d, false);
}

void MessagesManager::set_dialog_has_scheduled_database_messages(DialogId dialog_id,
                                                                 bool has_scheduled_database_messages) {
  if (G()->close_flag()) {
    return;
  }
  return set_dialog_has_scheduled_database_messages_impl(get_dialog(dialog_id), has_scheduled_database_messages);
}

void MessagesManager::set_dialog_has_scheduled_database_messages_impl(Dialog *d, bool has_scheduled_database_messages) {
  CHECK(d != nullptr);
  if (d->has_scheduled_database_messages == has_scheduled_database_messages) {
    return;
  }

  if (d->has_scheduled_database_messages && have_dialog_scheduled_messages_in_memory(d) &&
      !d->scheduled_messages->scheduled_messages_.begin()->first.is_yet_unsent()) {
    // to prevent race between add_message_to_database and check of has_scheduled_database_messages
    return;
  }

  CHECK(G()->use_message_database());

  d->has_scheduled_database_messages = has_scheduled_database_messages;
  on_dialog_updated(d->dialog_id, "set_dialog_has_scheduled_database_messages");
}

void MessagesManager::on_update_dialog_folder_id(DialogId dialog_id, FolderId folder_id) {
  auto d = get_dialog_force(dialog_id, "on_update_dialog_folder_id");
  if (d == nullptr) {
    // nothing to do
    return;
  }

  set_dialog_folder_id(d, folder_id);
}

void MessagesManager::set_dialog_folder_id(Dialog *d, FolderId folder_id) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);

  if (d->folder_id == folder_id) {
    if (!d->is_folder_id_inited) {
      LOG(INFO) << "Folder of " << d->dialog_id << " is still " << folder_id;
      do_set_dialog_folder_id(d, folder_id);
    }
    return;
  }

  LOG(INFO) << "Change " << d->dialog_id << " folder from " << d->folder_id << " to " << folder_id;

  auto dialog_positions = get_dialog_positions(d);

  if (get_dialog_pinned_order(DialogListId(d->folder_id), d->dialog_id) != DEFAULT_ORDER) {
    set_dialog_is_pinned(DialogListId(d->folder_id), d, false, false);
  }

  DialogDate dialog_date(d->order, d->dialog_id);
  if (get_dialog_folder(d->folder_id)->ordered_dialogs_.erase(dialog_date) == 0) {
    LOG_IF(ERROR, d->order != DEFAULT_ORDER) << d->dialog_id << " not found in the chat list";
  }

  do_set_dialog_folder_id(d, folder_id);

  get_dialog_folder(d->folder_id)->ordered_dialogs_.insert(dialog_date);

  update_dialog_lists(d, std::move(dialog_positions), true, false, "set_dialog_folder_id");
}

void MessagesManager::do_set_dialog_folder_id(Dialog *d, FolderId folder_id) {
  CHECK(!td_->auth_manager_->is_bot());
  bool is_changed = d->folder_id != folder_id;
  if (!is_changed && d->is_folder_id_inited) {
    return;
  }
  d->is_folder_id_inited = true;

  if (is_changed) {
    d->folder_id = folder_id;
    if (d->dialog_id.get_type() == DialogType::SecretChat) {
      // need to change action bar only for the secret chat and keep unarchive for the main chat
      auto user_id = td_->contacts_manager_->get_secret_chat_user_id(d->dialog_id.get_secret_chat_id());
      if (d->is_update_new_chat_sent && user_id.is_valid()) {
        const Dialog *user_d = get_dialog(DialogId(user_id));
        if (user_d != nullptr && user_d->action_bar != nullptr && user_d->action_bar->can_unarchive()) {
          send_closure(G()->td(), &Td::send_update,
                       td_api::make_object<td_api::updateChatActionBar>(
                           get_chat_id_object(d->dialog_id, "updateChatActionBar"), get_chat_action_bar_object(d)));
        }
      }
    } else if (folder_id != FolderId::archive() && d->action_bar != nullptr && d->action_bar->on_dialog_unarchived()) {
      send_update_chat_action_bar(d);
    }
  }

  on_dialog_updated(d->dialog_id, "do_set_dialog_folder_id");
}

void MessagesManager::on_update_dialog_group_call(DialogId dialog_id, bool has_active_group_call,
                                                  bool is_group_call_empty, const char *source, bool force) {
  LOG(INFO) << "Update voice chat in " << dialog_id << " with has_active_voice_chat = " << has_active_group_call
            << " and is_voice_chat_empty = " << is_group_call_empty << " from " << source;
  CHECK(dialog_id.is_valid());
  Dialog *d = get_dialog(dialog_id);  // must not create the Dialog, because it is called from on_get_chat
  if (d == nullptr) {
    LOG(INFO) << "Can't find " << dialog_id;
    pending_dialog_group_call_updates_[dialog_id] = {has_active_group_call, is_group_call_empty};
    return;
  }

  if (!has_active_group_call) {
    is_group_call_empty = false;
  }
  if (d->active_group_call_id.is_valid() && has_active_group_call && is_group_call_empty &&
      (td_->group_call_manager_->is_group_call_being_joined(d->active_group_call_id) ||
       td_->group_call_manager_->is_group_call_joined(d->active_group_call_id))) {
    LOG(INFO) << "Fix is_group_call_empty to false";
    is_group_call_empty = false;
  }
  if (d->has_active_group_call == has_active_group_call && d->is_group_call_empty == is_group_call_empty) {
    return;
  }
  if (!force && d->active_group_call_id.is_valid() && has_active_group_call &&
      td_->group_call_manager_->is_group_call_being_joined(d->active_group_call_id)) {
    LOG(INFO) << "Ignore update in a being joined group call";
    return;
  }

  if (d->has_active_group_call && !has_active_group_call && d->active_group_call_id.is_valid()) {
    d->active_group_call_id = InputGroupCallId();
    d->has_active_group_call = false;
    d->is_group_call_empty = false;
    send_update_chat_video_chat(d);
  } else if (d->has_active_group_call && has_active_group_call) {
    d->is_group_call_empty = is_group_call_empty;
    send_update_chat_video_chat(d);
  } else {
    d->has_active_group_call = has_active_group_call;
    d->is_group_call_empty = is_group_call_empty;
    on_dialog_updated(dialog_id, "on_update_dialog_group_call");

    if (has_active_group_call && !d->active_group_call_id.is_valid() && !td_->auth_manager_->is_bot()) {
      repair_dialog_active_group_call_id(dialog_id);
    }
  }
}

void MessagesManager::on_update_dialog_group_call_id(DialogId dialog_id, InputGroupCallId input_group_call_id) {
  auto d = get_dialog_force(dialog_id, "on_update_dialog_group_call_id");
  if (d == nullptr) {
    // nothing to do
    return;
  }

  if (d->active_group_call_id != input_group_call_id) {
    LOG(INFO) << "Update active group call in " << dialog_id << " to " << input_group_call_id;
    d->active_group_call_id = input_group_call_id;
    bool has_active_group_call = input_group_call_id.is_valid();
    if (has_active_group_call != d->has_active_group_call) {
      d->has_active_group_call = has_active_group_call;
      if (!has_active_group_call) {
        d->is_group_call_empty = false;
      }
    }
    send_update_chat_video_chat(d);
  }
}

void MessagesManager::on_update_dialog_default_join_group_call_as_dialog_id(DialogId dialog_id,
                                                                            DialogId default_join_as_dialog_id,
                                                                            bool force) {
  auto d = get_dialog_force(dialog_id, "on_update_dialog_default_join_group_call_as_dialog_id");
  if (d == nullptr) {
    // nothing to do
    return;
  }

  if (!force && d->active_group_call_id.is_valid() &&
      td_->group_call_manager_->is_group_call_being_joined(d->active_group_call_id)) {
    LOG(INFO) << "Ignore default_join_as_dialog_id update in a being joined group call";
    return;
  }

  if (default_join_as_dialog_id.is_valid()) {
    if (default_join_as_dialog_id.get_type() != DialogType::User) {
      force_create_dialog(default_join_as_dialog_id, "on_update_dialog_default_join_group_call_as_dialog_id");
    } else if (!td_->contacts_manager_->have_user_force(default_join_as_dialog_id.get_user_id(),
                                                        "on_update_dialog_default_join_group_call_as_dialog_id") ||
               default_join_as_dialog_id != td_->dialog_manager_->get_my_dialog_id()) {
      default_join_as_dialog_id = DialogId();
    }
  }

  if (d->default_join_group_call_as_dialog_id != default_join_as_dialog_id) {
    d->default_join_group_call_as_dialog_id = default_join_as_dialog_id;
    send_update_chat_video_chat(d);
  }
}

void MessagesManager::on_update_dialog_default_send_message_as_dialog_id(DialogId dialog_id,
                                                                         DialogId default_send_as_dialog_id,
                                                                         bool force) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return;
  }
  auto dialog_type = dialog_id.get_type();
  if (dialog_type != DialogType::Channel || td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
    if (default_send_as_dialog_id != DialogId()) {
      LOG(ERROR) << "Receive message sender " << default_send_as_dialog_id << " in " << dialog_id;
    }
    return;
  }

  auto d = get_dialog_force(dialog_id, "on_update_dialog_default_send_message_as_dialog_id");
  if (d == nullptr) {
    // nothing to do
    return;
  }

  if (!force) {
    // TODO ignore update if have being sent messages
  }

  if (default_send_as_dialog_id.is_valid()) {
    if (default_send_as_dialog_id.get_type() != DialogType::User) {
      force_create_dialog(default_send_as_dialog_id, "on_update_dialog_default_send_message_as_dialog_id");
    } else if (!td_->contacts_manager_->have_user_force(default_send_as_dialog_id.get_user_id(),
                                                        "on_update_dialog_default_send_message_as_dialog_id") ||
               default_send_as_dialog_id != td_->dialog_manager_->get_my_dialog_id()) {
      default_send_as_dialog_id = DialogId();
    }
  }

  if (d->default_send_message_as_dialog_id != default_send_as_dialog_id) {
    if (force || default_send_as_dialog_id.is_valid() ||
        (created_public_broadcasts_inited_ && created_public_broadcasts_.empty())) {
      LOG(INFO) << "Set message sender in " << dialog_id << " to " << default_send_as_dialog_id;
      d->need_drop_default_send_message_as_dialog_id = false;
      d->default_send_message_as_dialog_id = default_send_as_dialog_id;
      send_update_chat_message_sender(d);
    } else {
      LOG(INFO) << "Postpone removal of message sender in " << dialog_id;
      d->need_drop_default_send_message_as_dialog_id = true;
    }
    on_dialog_updated(d->dialog_id, "on_update_dialog_default_send_message_as_dialog_id");
  } else if (default_send_as_dialog_id.is_valid() && d->need_drop_default_send_message_as_dialog_id) {
    LOG(INFO) << "Don't remove message sender in " << dialog_id;
    d->need_drop_default_send_message_as_dialog_id = false;
    on_dialog_updated(d->dialog_id, "on_update_dialog_default_send_message_as_dialog_id");
  }
}

void MessagesManager::on_update_dialog_message_ttl(DialogId dialog_id, MessageTtl message_ttl) {
  auto d = get_dialog_force(dialog_id, "on_update_dialog_message_ttl");
  if (d == nullptr) {
    // nothing to do
    return;
  }
  set_dialog_message_ttl(d, std::move(message_ttl));
}

void MessagesManager::set_dialog_message_ttl(Dialog *d, MessageTtl message_ttl) {
  CHECK(d != nullptr);
  if (d->message_ttl != message_ttl) {
    d->message_ttl = message_ttl;
    d->is_message_ttl_inited = true;
    send_update_chat_message_auto_delete_time(d);
  }
  if (!d->is_message_ttl_inited) {
    d->is_message_ttl_inited = true;
    on_dialog_updated(d->dialog_id, "on_update_dialog_message_ttl");
  }
}

void MessagesManager::on_create_new_dialog(telegram_api::object_ptr<telegram_api::Updates> &&updates,
                                           DialogType expected_type,
                                           Promise<td_api::object_ptr<td_api::chat>> &&promise) {
  LOG(INFO) << "Receive result for creation of a chat: " << to_string(updates);

  auto sent_messages = UpdatesManager::get_new_messages(updates.get());
  auto sent_messages_random_ids = UpdatesManager::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 promise.set_error(Status::Error(500, "Unsupported server response"));
  }

  auto *message = sent_messages.begin()->first;
  // int64 message_random_id = *sent_messages_random_ids.begin();
  // TODO check that message_random_id equals random_id after messages_createChat will be updated

  if (sent_messages.begin()->second) {
    return promise.set_error(Status::Error(500, "Scheduled message received"));
  }

  auto dialog_id = DialogId::get_message_dialog_id(message);
  if (dialog_id.get_type() != expected_type) {
    return promise.set_error(Status::Error(500, "Chat of wrong type has been created"));
  }
  if (message->get_id() != telegram_api::messageService::ID) {
    return promise.set_error(Status::Error(500, "Invalid message received"));
  }
  auto action_id = static_cast<const telegram_api::messageService *>(message)->action_->get_id();
  if (action_id != telegram_api::messageActionChatCreate::ID &&
      action_id != telegram_api::messageActionChannelCreate::ID) {
    return promise.set_error(Status::Error(500, "Invalid service message received"));
  }

  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(get_chat_object(d));
  }

  if (pending_created_dialogs_.count(dialog_id) == 0) {
    PendingCreatedDialog pending_created_dialog;
    pending_created_dialog.promise_ = std::move(promise);
    pending_created_dialog.group_invite_privacy_forbidden_user_ids_ =
        td_->updates_manager_->extract_group_invite_privacy_forbidden_updates(updates);
    pending_created_dialogs_.emplace(dialog_id, std::move(pending_created_dialog));
  } else {
    LOG(ERROR) << "Receive twice " << dialog_id << " as result of chat creation";
    return promise.set_error(Status::Error(500, "Chat was created earlier"));
  }

  td_->updates_manager_->on_get_updates(std::move(updates), Promise<Unit>());
}

void MessagesManager::on_dialog_bots_updated(DialogId dialog_id, vector<UserId> bot_user_ids, bool from_database) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto d = from_database ? get_dialog(dialog_id) : get_dialog_force(dialog_id, "on_dialog_bots_updated");
  if (d == nullptr) {
    return;
  }

  bool has_bots = !bot_user_ids.empty();
  if (!d->is_has_bots_inited || d->has_bots != has_bots) {
    set_dialog_has_bots(d, has_bots);
    on_dialog_updated(dialog_id, "on_dialog_bots_updated");
  }

  if (d->reply_markup_message_id != MessageId()) {
    const Message *m = get_message_force(d, d->reply_markup_message_id, "on_dialog_bots_updated");
    if (m == nullptr || (m->sender_user_id.is_valid() && !td::contains(bot_user_ids, m->sender_user_id))) {
      LOG(INFO) << "Remove reply markup in " << dialog_id << ", because bot "
                << (m == nullptr ? UserId() : m->sender_user_id) << " isn't a member of the chat";
      set_dialog_reply_markup(d, MessageId());
    }
  }
}

void MessagesManager::set_dialog_has_bots(Dialog *d, bool has_bots) {
  CHECK(d != nullptr);
  LOG_CHECK(d->is_update_new_chat_sent) << "Wrong " << d->dialog_id << " in set_dialog_has_bots";

  LOG(INFO) << "Set " << d->dialog_id << " has_bots to " << has_bots;

  auto old_skip_bot_commands = need_skip_bot_commands(d->dialog_id, nullptr);
  d->has_bots = has_bots;
  d->is_has_bots_inited = true;
  auto new_skip_bot_commands = need_skip_bot_commands(d->dialog_id, nullptr);
  if (old_skip_bot_commands != new_skip_bot_commands) {
    auto it = dialog_bot_command_message_ids_.find(d->dialog_id);
    if (it != dialog_bot_command_message_ids_.end()) {
      for (auto message_id : it->second.message_ids) {
        auto m = get_message(d, message_id);
        LOG_CHECK(m != nullptr) << d->dialog_id << ' ' << message_id;
        send_update_message_content_impl(d->dialog_id, m, "set_dialog_has_bots");
      }
    }
  }
}

void MessagesManager::on_dialog_photo_updated(DialogId dialog_id) {
  auto d = get_dialog(dialog_id);  // called from update_user, must not create the dialog
  if (d != nullptr && d->is_update_new_chat_sent) {
    send_closure(
        G()->td(), &Td::send_update,
        td_api::make_object<td_api::updateChatPhoto>(
            get_chat_id_object(dialog_id, "updateChatPhoto"),
            get_chat_photo_info_object(td_->file_manager_.get(), td_->dialog_manager_->get_dialog_photo(dialog_id))));
  } else if (d != nullptr && d->is_update_new_chat_being_sent) {
    const auto *photo = td_->dialog_manager_->get_dialog_photo(dialog_id);
    if (photo == nullptr) {
      LOG(ERROR) << "Removed photo of " << dialog_id << " while the chat is being added";
    } else {
      LOG(ERROR) << "Changed photo of " << dialog_id << " while the chat is being added to " << *photo;
    }
  }
}

void MessagesManager::on_dialog_accent_colors_updated(DialogId dialog_id) {
  auto d = get_dialog(dialog_id);  // called from update_user, must not create the dialog
  if (d != nullptr && d->is_update_new_chat_sent) {
    send_closure(G()->td(), &Td::send_update,
                 td_api::make_object<td_api::updateChatAccentColors>(
                     get_chat_id_object(dialog_id, "updateChatAccentColors"),
                     td_->dialog_manager_->get_dialog_accent_color_id_object(dialog_id),
                     td_->dialog_manager_->get_dialog_background_custom_emoji_id(dialog_id).get(),
                     td_->dialog_manager_->get_dialog_profile_accent_color_id_object(dialog_id),
                     td_->dialog_manager_->get_dialog_profile_background_custom_emoji_id(dialog_id).get()));
  }
}

void MessagesManager::on_dialog_title_updated(DialogId dialog_id) {
  auto d = get_dialog(dialog_id);  // called from update_user, must not create the dialog
  if (d != nullptr) {
    update_dialogs_hints(d);
    if (d->is_update_new_chat_sent) {
      send_closure(G()->td(), &Td::send_update,
                   td_api::make_object<td_api::updateChatTitle>(dialog_id.get(),
                                                                td_->dialog_manager_->get_dialog_title(dialog_id)));
    }
  }
}

void MessagesManager::on_dialog_emoji_status_updated(DialogId dialog_id) {
  auto d = get_dialog(dialog_id);  // called from update_user, must not create the dialog
  if (d != nullptr && d->is_update_new_chat_sent) {
    send_closure(G()->td(), &Td::send_update,
                 td_api::make_object<td_api::updateChatEmojiStatus>(
                     get_chat_id_object(dialog_id, "updateChatEmojiStatus"),
                     td_->dialog_manager_->get_dialog_emoji_status_object(dialog_id)));
  }
}

void MessagesManager::on_dialog_default_permissions_updated(DialogId dialog_id) {
  auto d = get_dialog(dialog_id);  // called from update_user, must not create the dialog
  if (d != nullptr && d->is_update_new_chat_sent) {
    send_closure(G()->td(), &Td::send_update,
                 td_api::make_object<td_api::updateChatPermissions>(
                     dialog_id.get(),
                     td_->dialog_manager_->get_dialog_default_permissions(dialog_id).get_chat_permissions_object()));
  }
}

void MessagesManager::on_dialog_has_protected_content_updated(DialogId dialog_id) {
  auto d = get_dialog(dialog_id);  // called from update_chat, must not create the dialog
  if (d != nullptr && d->is_update_new_chat_sent) {
    send_closure(G()->td(), &Td::send_update,
                 td_api::make_object<td_api::updateChatHasProtectedContent>(
                     dialog_id.get(), td_->dialog_manager_->get_dialog_has_protected_content(dialog_id)));
  }
}

void MessagesManager::on_dialog_user_is_contact_updated(DialogId dialog_id, bool is_contact) {
  CHECK(dialog_id.get_type() == DialogType::User);
  auto d = get_dialog(dialog_id);  // called from update_user, must not create the dialog
  if (d != nullptr && d->is_update_new_chat_sent) {
    if (d->know_action_bar) {
      if (is_contact) {
        if (d->action_bar != nullptr && d->action_bar->on_user_contact_added()) {
          send_update_chat_action_bar(d);
        }
      } else {
        repair_dialog_action_bar(d, "on_dialog_user_is_contact_updated");
      }
    }

    if (td_->dialog_filter_manager_->have_dialog_filters() && d->order != DEFAULT_ORDER) {
      update_dialog_lists(d, get_dialog_positions(d), true, false, "on_dialog_user_is_contact_updated");
      td_->contacts_manager_->for_each_secret_chat_with_user(
          d->dialog_id.get_user_id(), [this](SecretChatId secret_chat_id) {
            DialogId dialog_id(secret_chat_id);
            auto d = get_dialog(dialog_id);  // must not create the dialog
            if (d != nullptr && d->is_update_new_chat_sent && d->order != DEFAULT_ORDER) {
              update_dialog_lists(d, get_dialog_positions(d), true, false, "on_dialog_user_is_contact_updated");
            }
          });
    }
  }
}

void MessagesManager::on_dialog_user_is_deleted_updated(DialogId dialog_id, bool is_deleted) {
  CHECK(dialog_id.get_type() == DialogType::User);
  auto d = get_dialog(dialog_id);  // called from update_user, must not create the dialog
  if (d != nullptr && d->is_update_new_chat_sent) {
    if (d->know_action_bar) {
      if (is_deleted) {
        if (d->action_bar != nullptr && d->action_bar->on_user_deleted()) {
          send_update_chat_action_bar(d);
        }
      } else {
        repair_dialog_action_bar(d, "on_dialog_user_is_deleted_updated");
      }
    }

    if (td_->dialog_filter_manager_->have_dialog_filters() && d->order != DEFAULT_ORDER) {
      update_dialog_lists(d, get_dialog_positions(d), true, false, "on_dialog_user_is_deleted_updated");
      td_->contacts_manager_->for_each_secret_chat_with_user(
          dialog_id.get_user_id(), [this](SecretChatId secret_chat_id) {
            DialogId dialog_id(secret_chat_id);
            auto d = get_dialog(dialog_id);  // must not create the dialog
            if (d != nullptr && d->is_update_new_chat_sent && d->order != DEFAULT_ORDER) {
              update_dialog_lists(d, get_dialog_positions(d), true, false, "on_dialog_user_is_deleted_updated");
            }
          });
    }

    if (is_deleted && d->has_bots) {
      set_dialog_has_bots(d, false);
      td_->contacts_manager_->for_each_secret_chat_with_user(
          dialog_id.get_user_id(), [this](SecretChatId secret_chat_id) {
            DialogId dialog_id(secret_chat_id);
            auto d = get_dialog(dialog_id);  // must not create the dialog
            if (d != nullptr && d->is_update_new_chat_sent && d->has_bots) {
              set_dialog_has_bots(d, false);
            }
          });
    }
  }
}

void MessagesManager::on_dialog_linked_channel_updated(DialogId dialog_id, ChannelId old_linked_channel_id,
                                                       ChannelId new_linked_channel_id) const {
  CHECK(dialog_id.get_type() == DialogType::Channel);
  if (td_->auth_manager_->is_bot() || !td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
    return;
  }
  auto d = get_dialog(dialog_id);  // no need to create the dialog
  if (d == nullptr || !d->is_update_new_chat_sent) {
    return;
  }

  auto message_ids = find_dialog_messages(d, [old_linked_channel_id, new_linked_channel_id](const Message *m) {
    return !m->reply_info.is_empty() && m->reply_info.channel_id_.is_valid() &&
           (m->reply_info.channel_id_ == old_linked_channel_id || m->reply_info.channel_id_ == new_linked_channel_id);
  });
  LOG(INFO) << "Found discussion messages " << message_ids;
  for (auto message_id : message_ids) {
    const Message *m = get_message(d, message_id);
    send_update_message_interaction_info(dialog_id, m);
    send_update_last_message_if_needed(d, m, "on_dialog_linked_channel_updated");
  }
}

class MessagesManager::RegetDialogLogEvent {
 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_reget_dialog_log_event(DialogId dialog_id) {
  RegetDialogLogEvent log_event{dialog_id};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::RegetDialog, get_log_event_storer(log_event));
}

void MessagesManager::send_get_dialog_query(DialogId dialog_id, Promise<Unit> &&promise, uint64 log_event_id,
                                            const char *source) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  if (td_->auth_manager_->is_bot() || dialog_id.get_type() == DialogType::SecretChat) {
    if (log_event_id != 0) {
      binlog_erase(G()->td_db()->get_binlog(), log_event_id);
    }
    return promise.set_error(Status::Error(500, "Wrong getDialog query"));
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    if (log_event_id != 0) {
      binlog_erase(G()->td_db()->get_binlog(), log_event_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 (log_event_id != 0) {
      LOG(INFO) << "Duplicate getDialog query for " << dialog_id << " from " << source;
      binlog_erase(G()->td_db()->get_binlog(), log_event_id);
    }
    // query has already been sent, just wait for the result
    return;
  }

  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_reget_dialog_log_event(dialog_id);
  }
  if (log_event_id != 0) {
    auto result = get_dialog_query_log_event_id_.emplace(dialog_id, log_event_id);
    CHECK(result.second);
  }

  if (!G()->close_flag()) {
    LOG(INFO) << "Send get " << dialog_id << " query from " << source;
    td_->create_handler<GetDialogQuery>()->send(dialog_id);
  } else {
    // request will be sent after restart
  }
}

void MessagesManager::on_get_dialog_query_finished(DialogId dialog_id, Status &&status) {
  if (G()->close_flag()) {
    return;
  }

  LOG(INFO) << "Finished getting " << dialog_id << " with result " << 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 log_event_it = get_dialog_query_log_event_id_.find(dialog_id);
  if (log_event_it != get_dialog_query_log_event_id_.end()) {
    if (!G()->close_flag()) {
      binlog_erase(G()->td_db()->get_binlog(), log_event_it->second);
    }
    get_dialog_query_log_event_id_.erase(log_event_it);
  }

  if (status.is_ok()) {
    set_promises(promises);
  } else {
    fail_promises(promises, std::move(status));
  }
}

void MessagesManager::on_dialog_usernames_updated(DialogId dialog_id, const Usernames &old_usernames,
                                                  const Usernames &new_usernames) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  LOG(INFO) << "Update usernames in " << dialog_id << " from " << old_usernames << " to " << new_usernames;

  message_embedding_codes_[0].erase(dialog_id);
  message_embedding_codes_[1].erase(dialog_id);

  const auto *d = get_dialog(dialog_id);
  if (d != nullptr) {
    update_dialogs_hints(d);
  }
}

bool MessagesManager::get_dialog_view_as_topics(const Dialog *d) const {
  return !d->view_as_messages && (d->is_forum || d->dialog_id == td_->dialog_manager_->get_my_dialog_id());
}

bool MessagesManager::get_dialog_has_scheduled_messages(const Dialog *d) const {
  if (!td_->dialog_manager_->have_input_peer(d->dialog_id, AccessRights::Read)) {
    return false;
  }
  if (td_->dialog_manager_->is_broadcast_channel(d->dialog_id) &&
      !td_->contacts_manager_->get_channel_status(d->dialog_id.get_channel_id()).can_post_messages()) {
    return false;
  }
  // TODO send updateChatHasScheduledMessage when can_post_messages changes

  return d->has_scheduled_server_messages || d->has_scheduled_database_messages ||
         have_dialog_scheduled_messages_in_memory(d);
}

MessagesManager::DialogScheduledMessages *MessagesManager::add_dialog_scheduled_messages(Dialog *d) {
  if (d->scheduled_messages == nullptr) {
    d->scheduled_messages = make_unique<DialogScheduledMessages>();
  }
  return d->scheduled_messages.get();
}

MessagesManager::NotificationInfo *MessagesManager::add_dialog_notification_info(Dialog *d) {
  if (d->notification_info == nullptr) {
    d->notification_info = make_unique<NotificationInfo>();
  }
  return d->notification_info.get();
}

void MessagesManager::on_send_dialog_action_timeout(DialogId dialog_id) {
  LOG(INFO) << "Receive send_chat_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 = ChainId(dialog_id, MessageContentType::Photo).get();
  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);

  auto &queue = queue_it->second.queue_;
  CHECK(!queue.empty());
  const Message *m = get_message(d, queue.begin()->first);
  if (m == nullptr) {
    return;
  }
  CHECK(m->message_id.is_yet_unsent());
  if (m->forward_info != nullptr || m->had_forward_info || m->is_copy || m->message_id.is_scheduled() ||
      m->sender_dialog_id.is_valid()) {
    return;
  }

  auto file_id = get_message_content_upload_file_id(m->content.get());
  if (!file_id.is_valid()) {
    LOG(ERROR) << "Have no file in " << to_string(get_message_message_content_object(dialog_id, m));
    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);
  }

  DialogAction action = DialogAction::get_uploading_action(m->content->get_type(), progress);
  if (action == DialogAction()) {
    return;
  }
  LOG(INFO) << "Send " << action << " in " << dialog_id;
  td_->dialog_action_manager_->send_dialog_action(dialog_id, m->top_thread_message_id, std::move(action),
                                                  Promise<Unit>());
}

void MessagesManager::get_dialog_filter_dialog_count(td_api::object_ptr<td_api::chatFolder> filter,
                                                     Promise<int32> &&promise) {
  TRY_RESULT_PROMISE(promise, dialog_filter,
                     DialogFilter::create_dialog_filter(td_, DialogFilterId(), std::move(filter)));

  int32 total_count = 0;
  for (auto folder_id : dialog_filter->get_folder_ids()) {
    const auto &folder = *get_dialog_folder(folder_id);
    for (const auto &dialog_date : folder.ordered_dialogs_) {
      if (dialog_date.get_order() == DEFAULT_ORDER) {
        break;
      }

      auto dialog_id = dialog_date.get_dialog_id();
      if (dialog_filter->need_dialog(td_, get_dialog_info_for_dialog_filter(get_dialog(dialog_id)))) {
        total_count++;
      }
    }
  }
  promise.set_value(std::move(total_count));
}

void MessagesManager::add_dialog_list_for_dialog_filter(DialogFilterId dialog_filter_id) {
  DialogListId dialog_list_id(dialog_filter_id);
  CHECK(dialog_lists_.count(dialog_list_id) == 0);

  auto &list = add_dialog_list(dialog_list_id);
  auto folder_ids = get_dialog_list_folder_ids(list);
  CHECK(!folder_ids.empty());

  for (auto folder_id : folder_ids) {
    auto *folder = get_dialog_folder(folder_id);
    CHECK(folder != nullptr);
    for (const auto &dialog_date : folder->ordered_dialogs_) {
      if (dialog_date.get_order() == DEFAULT_ORDER) {
        break;
      }

      auto dialog_id = dialog_date.get_dialog_id();
      Dialog *d = get_dialog(dialog_id);
      CHECK(d != nullptr);

      if (need_dialog_in_list(d, list)) {
        list.in_memory_dialog_total_count_++;

        add_dialog_to_list(d, dialog_list_id);
      }
    }
  }

  auto pinned_dialog_ids = td_->dialog_filter_manager_->get_pinned_dialog_ids(dialog_filter_id);
  for (const auto &dialog_id : reversed(pinned_dialog_ids)) {
    if (!dialog_id.is_valid()) {
      continue;
    }
    auto order = get_next_pinned_dialog_order();
    list.pinned_dialogs_.emplace_back(order, dialog_id);
    list.pinned_dialog_id_orders_.emplace(dialog_id, order);
  }
  std::reverse(list.pinned_dialogs_.begin(), list.pinned_dialogs_.end());
  list.are_pinned_dialogs_inited_ = true;

  update_list_last_pinned_dialog_date(list);
  update_list_last_dialog_date(list);
}

void MessagesManager::edit_dialog_list_for_dialog_filter(unique_ptr<DialogFilter> &old_dialog_filter,
                                                         unique_ptr<DialogFilter> new_dialog_filter,
                                                         bool &disable_get_dialog_filter, const char *source) {
  CHECK(old_dialog_filter != nullptr);
  CHECK(new_dialog_filter != nullptr);
  auto dialog_list_id = DialogListId(old_dialog_filter->get_dialog_filter_id());
  auto *old_list_ptr = get_dialog_list(dialog_list_id);
  CHECK(old_list_ptr != nullptr);
  auto &old_list = *old_list_ptr;

  auto folder_ids = old_dialog_filter->get_folder_ids();
  CHECK(!folder_ids.empty());
  for (auto folder_id : new_dialog_filter->get_folder_ids()) {
    if (!td::contains(folder_ids, folder_id)) {
      folder_ids.push_back(folder_id);
    }
  }

  DialogList new_list;
  new_list.dialog_list_id = dialog_list_id;

  auto old_it = old_list.pinned_dialogs_.rbegin();
  for (const auto &input_dialog_id : reversed(new_dialog_filter->get_pinned_input_dialog_ids())) {
    auto dialog_id = input_dialog_id.get_dialog_id();
    if (!dialog_id.is_valid()) {
      continue;
    }
    while (old_it < old_list.pinned_dialogs_.rend()) {
      if (old_it->get_dialog_id() == dialog_id) {
        break;
      }
      ++old_it;
    }

    int64 order;
    if (old_it < old_list.pinned_dialogs_.rend()) {
      order = old_it->get_order();
      ++old_it;
    } else {
      order = get_next_pinned_dialog_order();
    }
    new_list.pinned_dialogs_.emplace_back(order, dialog_id);
    new_list.pinned_dialog_id_orders_.emplace(dialog_id, order);
  }
  std::reverse(new_list.pinned_dialogs_.begin(), new_list.pinned_dialogs_.end());
  new_list.are_pinned_dialogs_inited_ = true;

  do_update_list_last_pinned_dialog_date(new_list);
  do_update_list_last_dialog_date(new_list, new_dialog_filter->get_folder_ids());

  new_list.server_dialog_total_count_ = 0;
  new_list.secret_chat_total_count_ = 0;

  std::map<DialogDate, const Dialog *> updated_position_dialogs;
  for (auto folder_id : folder_ids) {
    auto *folder = get_dialog_folder(folder_id);
    CHECK(folder != nullptr);
    for (const auto &dialog_date : folder->ordered_dialogs_) {
      if (dialog_date.get_order() == DEFAULT_ORDER) {
        break;
      }

      auto dialog_id = dialog_date.get_dialog_id();
      Dialog *d = get_dialog(dialog_id);
      CHECK(d != nullptr);

      const DialogPositionInList old_position = get_dialog_position_in_list(old_list_ptr, d);
      // can't use get_dialog_position_in_list, because need_dialog_in_list calls get_dialog_filter
      DialogPositionInList new_position;
      if (new_dialog_filter->need_dialog(td_, get_dialog_info_for_dialog_filter(d))) {
        new_position.private_order = get_dialog_private_order(&new_list, d);
        if (new_position.private_order != 0) {
          new_position.public_order =
              DialogDate(new_position.private_order, dialog_id) <= new_list.list_last_dialog_date_
                  ? new_position.private_order
                  : 0;
          new_position.is_pinned = get_dialog_pinned_order(&new_list, dialog_id) != DEFAULT_ORDER;
          new_position.is_sponsored = is_dialog_sponsored(d);
        }
      }

      if (need_send_update_chat_position(old_position, new_position)) {
        updated_position_dialogs.emplace(DialogDate(new_position.public_order, dialog_id), d);
      }

      bool was_in_list = old_position.private_order != 0;
      bool is_in_list = new_position.private_order != 0;
      if (is_in_list) {
        if (!was_in_list) {
          add_dialog_to_list(d, dialog_list_id);
        }

        new_list.in_memory_dialog_total_count_++;
        if (dialog_id.get_type() == DialogType::SecretChat) {
          new_list.secret_chat_total_count_++;
        } else {
          new_list.server_dialog_total_count_++;
        }

        auto unread_count = d->server_unread_count + d->local_unread_count;
        if (unread_count != 0) {
          new_list.unread_message_total_count_ += unread_count;
          if (is_dialog_muted(d)) {
            new_list.unread_message_muted_count_ += unread_count;
          }
        }
        if (unread_count != 0 || d->is_marked_as_unread) {
          new_list.unread_dialog_total_count_++;
          if (unread_count == 0 && d->is_marked_as_unread) {
            new_list.unread_dialog_marked_count_++;
          }
          if (is_dialog_muted(d)) {
            new_list.unread_dialog_muted_count_++;
            if (unread_count == 0 && d->is_marked_as_unread) {
              new_list.unread_dialog_muted_marked_count_++;
            }
          }
        }
      } else {
        if (was_in_list) {
          remove_dialog_from_list(d, dialog_list_id);
        }
      }
    }
  }

  if (new_list.list_last_dialog_date_ == MAX_DIALOG_DATE) {
    new_list.is_message_unread_count_inited_ = true;
    new_list.is_dialog_unread_count_inited_ = true;
    new_list.need_unread_count_recalc_ = false;
  } else {
    if (old_list.is_message_unread_count_inited_) {  // can't stop sending updates
      new_list.is_message_unread_count_inited_ = true;
    }
    if (old_list.is_dialog_unread_count_inited_) {  // can't stop sending updates
      new_list.is_dialog_unread_count_inited_ = true;
    }
    new_list.server_dialog_total_count_ = -1;
    new_list.secret_chat_total_count_ = -1;
  }

  bool need_update_unread_message_count =
      new_list.is_message_unread_count_inited_ &&
      (old_list.unread_message_total_count_ != new_list.unread_message_total_count_ ||
       old_list.unread_message_muted_count_ != new_list.unread_message_muted_count_ ||
       !old_list.is_message_unread_count_inited_);
  bool need_update_unread_chat_count =
      new_list.is_dialog_unread_count_inited_ &&
      (old_list.unread_dialog_total_count_ != new_list.unread_dialog_total_count_ ||
       old_list.unread_dialog_muted_count_ != new_list.unread_dialog_muted_count_ ||
       old_list.unread_dialog_marked_count_ != new_list.unread_dialog_marked_count_ ||
       old_list.unread_dialog_muted_marked_count_ != new_list.unread_dialog_muted_marked_count_ ||
       get_dialog_total_count(old_list) != get_dialog_total_count(new_list) ||
       !old_list.is_dialog_unread_count_inited_);
  bool need_save_unread_chat_count = new_list.is_dialog_unread_count_inited_ &&
                                     (old_list.server_dialog_total_count_ != new_list.server_dialog_total_count_ ||
                                      old_list.secret_chat_total_count_ != new_list.secret_chat_total_count_);

  auto load_list_promises = std::move(old_list.load_list_queries_);

  old_list = std::move(new_list);
  old_dialog_filter = std::move(new_dialog_filter);

  disable_get_dialog_filter = false;

  if (need_update_unread_message_count) {
    send_update_unread_message_count(old_list, DialogId(), true, source);
  }
  if (need_update_unread_chat_count) {
    send_update_unread_chat_count(old_list, DialogId(), true, source);
  } else if (need_save_unread_chat_count) {
    save_unread_chat_count(old_list);
  }

  for (const auto &it : updated_position_dialogs) {
    send_update_chat_position(dialog_list_id, it.second, source);
  }

  if (old_list.need_unread_count_recalc_) {
    // repair unread count
    get_dialogs_from_list(dialog_list_id, static_cast<int32>(old_list.pinned_dialogs_.size() + 2), Auto());
  }

  if (!load_list_promises.empty()) {
    LOG(INFO) << "Retry loading of chats in " << dialog_list_id;
    set_promises(load_list_promises);  // try again
  }
}

void MessagesManager::delete_dialog_list_for_dialog_filter(DialogFilterId dialog_filter_id, const char *source) {
  auto dialog_list_id = DialogListId(dialog_filter_id);
  auto *list = get_dialog_list(dialog_list_id);
  CHECK(list != nullptr);
  auto folder_ids = get_dialog_list_folder_ids(*list);
  CHECK(!folder_ids.empty());

  for (auto folder_id : folder_ids) {
    auto *folder = get_dialog_folder(folder_id);
    CHECK(folder != nullptr);
    for (const auto &dialog_date : folder->ordered_dialogs_) {
      if (dialog_date.get_order() == DEFAULT_ORDER) {
        break;
      }

      auto dialog_id = dialog_date.get_dialog_id();
      Dialog *d = get_dialog(dialog_id);
      CHECK(d != nullptr);

      const DialogPositionInList old_position = get_dialog_position_in_list(list, d);

      if (is_dialog_in_list(d, dialog_list_id)) {
        remove_dialog_from_list(d, dialog_list_id);

        if (old_position.public_order != 0) {
          send_update_chat_position(dialog_list_id, d, source);
        }
      }
    }
  }

  if (G()->use_message_database()) {
    postponed_unread_message_count_updates_.erase(dialog_list_id);
    postponed_unread_chat_count_updates_.erase(dialog_list_id);

    if (list->is_message_unread_count_inited_) {
      list->unread_message_total_count_ = 0;
      list->unread_message_muted_count_ = 0;
      send_update_unread_message_count(*list, DialogId(), true, source, true);
      G()->td_db()->get_binlog_pmc()->erase(PSTRING() << "unread_message_count" << dialog_list_id.get());
    }
    if (list->is_dialog_unread_count_inited_) {
      list->unread_dialog_total_count_ = 0;
      list->unread_dialog_muted_count_ = 0;
      list->unread_dialog_marked_count_ = 0;
      list->unread_dialog_muted_marked_count_ = 0;
      list->in_memory_dialog_total_count_ = 0;
      list->server_dialog_total_count_ = 0;
      list->secret_chat_total_count_ = 0;
      send_update_unread_chat_count(*list, DialogId(), true, source, true);
      G()->td_db()->get_binlog_pmc()->erase(PSTRING() << "unread_dialog_count" << dialog_list_id.get());
    }
  }

  fail_promises(list->load_list_queries_, Status::Error(400, "Chat list not found"));

  dialog_lists_.erase(dialog_list_id);
}

vector<DialogListId> MessagesManager::get_dialog_lists_to_add_dialog(DialogId dialog_id) {
  vector<DialogListId> result;
  if (can_add_dialog_to_filter(dialog_id).is_error()) {
    return result;
  }

  if (dialog_id != td_->dialog_manager_->get_my_dialog_id() &&
      dialog_id != DialogId(ContactsManager::get_service_notifications_user_id())) {
    result.push_back(
        DialogListId(get_dialog(dialog_id)->folder_id == FolderId::archive() ? FolderId::main() : FolderId::archive()));
  }

  for (auto dialog_filter_id : td_->dialog_filter_manager_->get_dialog_filters_to_add_dialog(dialog_id)) {
    result.push_back(DialogListId(dialog_filter_id));
  }
  return result;
}

void MessagesManager::add_dialog_to_list(DialogId dialog_id, DialogListId dialog_list_id, Promise<Unit> &&promise) {
  LOG(INFO) << "Receive addChatToList request to add " << dialog_id << " to " << dialog_list_id;
  CHECK(!td_->auth_manager_->is_bot());

  Dialog *d = get_dialog_force(dialog_id, "add_dialog_to_list");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  if (d->order == DEFAULT_ORDER) {
    return promise.set_error(Status::Error(400, "Chat is not in a chat list"));
  }

  if (get_dialog_list(dialog_list_id) == nullptr) {
    return promise.set_error(Status::Error(400, "Chat list not found"));
  }

  if (dialog_list_id.is_filter()) {
    return promise.set_result(td_->dialog_filter_manager_->add_dialog(
        dialog_list_id.get_filter_id(), td_->dialog_manager_->get_input_dialog_id(dialog_id)));
  }

  CHECK(dialog_list_id.is_folder());
  auto folder_id = dialog_list_id.get_folder_id();
  if (d->folder_id == folder_id) {
    return promise.set_value(Unit());
  }

  if (folder_id == FolderId::archive() &&
      (dialog_id == td_->dialog_manager_->get_my_dialog_id() ||
       dialog_id == DialogId(ContactsManager::get_service_notifications_user_id()))) {
    return promise.set_error(Status::Error(400, "Chat can't be archived"));
  }

  set_dialog_folder_id(d, folder_id);

  if (dialog_id.get_type() != DialogType::SecretChat) {
    set_dialog_folder_id_on_server(dialog_id, false);
  }
  promise.set_value(Unit());
}

class MessagesManager::SetDialogFolderIdOnServerLogEvent {
 public:
  DialogId dialog_id_;
  FolderId folder_id_;

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

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

void MessagesManager::set_dialog_folder_id_on_server(DialogId dialog_id, bool from_binlog) {
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  if (!from_binlog && G()->use_message_database()) {
    SetDialogFolderIdOnServerLogEvent log_event;
    log_event.dialog_id_ = dialog_id;
    log_event.folder_id_ = d->folder_id;
    add_log_event(d->set_folder_id_log_event_id, get_log_event_storer(log_event),
                  LogEvent::HandlerType::SetDialogFolderIdOnServer, "set chat folder");
  }

  Promise<Unit> promise;
  if (d->set_folder_id_log_event_id.log_event_id != 0) {
    d->set_folder_id_log_event_id.generation++;
    promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_id,
                                      generation = d->set_folder_id_log_event_id.generation](Result<Unit> result) {
      if (!G()->close_flag()) {
        send_closure(actor_id, &MessagesManager::on_updated_dialog_folder_id, dialog_id, generation);
      }
    });
  }

  // TODO do not send two queries simultaneously or use InvokeAfter
  td_->create_handler<EditPeerFoldersQuery>(std::move(promise))->send(dialog_id, d->folder_id);
}

void MessagesManager::on_updated_dialog_folder_id(DialogId dialog_id, uint64 generation) {
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  delete_log_event(d->set_folder_id_log_event_id, generation, "set chat folder");
}

void MessagesManager::set_dialog_available_reactions(
    DialogId dialog_id, td_api::object_ptr<td_api::ChatAvailableReactions> &&available_reactions_ptr,
    Promise<Unit> &&promise) {
  Dialog *d = get_dialog_force(dialog_id, "set_dialog_available_reactions");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }

  ChatReactions available_reactions(std::move(available_reactions_ptr),
                                    !td_->dialog_manager_->is_broadcast_channel(dialog_id));
  auto active_reactions = get_active_reactions(available_reactions);
  if (active_reactions.reaction_types_.size() != available_reactions.reaction_types_.size()) {
    return promise.set_error(Status::Error(400, "Invalid reactions specified"));
  }
  available_reactions = std::move(active_reactions);

  switch (dialog_id.get_type()) {
    case DialogType::User:
      return promise.set_error(Status::Error(400, "Can't change private chat available reactions"));
    case DialogType::Chat: {
      auto chat_id = dialog_id.get_chat_id();
      auto status = td_->contacts_manager_->get_chat_permissions(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(400, "Not enough rights to change chat available reactions"));
      }
      break;
    }
    case DialogType::Channel: {
      auto status = td_->contacts_manager_->get_channel_permissions(dialog_id.get_channel_id());
      if (!status.can_change_info_and_settings()) {
        return promise.set_error(Status::Error(400, "Not enough rights to change chat available reactions"));
      }
      break;
    }
    case DialogType::SecretChat:
      return promise.set_error(Status::Error(400, "Can't change secret chat available reactions"));
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  bool is_changed = d->available_reactions != available_reactions;

  set_dialog_available_reactions(d, ChatReactions(available_reactions));

  if (!is_changed) {
    return promise.set_value(Unit());
  }

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

void MessagesManager::set_dialog_message_ttl(DialogId dialog_id, int32 ttl, Promise<Unit> &&promise) {
  if (ttl < 0) {
    return promise.set_error(Status::Error(400, "Message auto-delete time can't be negative"));
  }

  Dialog *d = get_dialog_force(dialog_id, "set_dialog_message_ttl");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Write)) {
    return promise.set_error(Status::Error(400, "Have no write access to the chat"));
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      if (dialog_id == td_->dialog_manager_->get_my_dialog_id() ||
          dialog_id == DialogId(ContactsManager::get_service_notifications_user_id())) {
        return promise.set_error(Status::Error(400, "Message auto-delete time in the chat can't be changed"));
      }
      break;
    case DialogType::Chat: {
      auto chat_id = dialog_id.get_chat_id();
      auto status = td_->contacts_manager_->get_chat_permissions(chat_id);
      if (!status.can_change_info_and_settings()) {
        return promise.set_error(
            Status::Error(400, "Not enough rights to change message auto-delete time in the chat"));
      }
      break;
    }
    case DialogType::Channel: {
      auto status = td_->contacts_manager_->get_channel_permissions(dialog_id.get_channel_id());
      if (!status.can_change_info_and_settings()) {
        return promise.set_error(
            Status::Error(400, "Not enough rights to change message auto-delete time in the chat"));
      }
      break;
    }
    case DialogType::SecretChat:
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  if (dialog_id.get_type() != DialogType::SecretChat) {
    // TODO invoke after
    td_->create_handler<SetHistoryTtlQuery>(std::move(promise))->send(dialog_id, ttl);
  } else {
    bool need_update_dialog_pos = false;
    Message *m =
        get_message_to_send(d, MessageId(), MessageInputReplyTo(), MessageSendOptions(),
                            create_chat_set_ttl_message_content(ttl, UserId()), false, &need_update_dialog_pos);

    send_update_new_message(d, m);
    if (need_update_dialog_pos) {
      send_update_chat_last_message(d, "set_dialog_message_ttl");
    }

    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, std::move(promise));
  }
}

void MessagesManager::set_dialog_theme(DialogId dialog_id, const string &theme_name, Promise<Unit> &&promise) {
  auto d = get_dialog_force(dialog_id, "set_dialog_theme");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Write)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }

  switch (dialog_id.get_type()) {
    case DialogType::User:
      break;
    case DialogType::Chat:
    case DialogType::Channel:
      return promise.set_error(Status::Error(400, "Can't change theme in the chat"));
    case DialogType::SecretChat: {
      auto user_id = td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id());
      if (!user_id.is_valid()) {
        return promise.set_error(Status::Error(400, "Can't access the user"));
      }
      dialog_id = DialogId(user_id);
      break;
    }
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  // TODO this can be wrong if there were previous change theme requests
  if (get_dialog_theme_name(d) == theme_name) {
    return promise.set_value(Unit());
  }

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

void MessagesManager::pin_dialog_message(DialogId dialog_id, MessageId message_id, bool disable_notification,
                                         bool only_for_self, bool is_unpin, Promise<Unit> &&promise) {
  auto d = get_dialog_force(dialog_id, "pin_dialog_message");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  TRY_STATUS_PROMISE(promise, td_->dialog_manager_->can_pin_messages(dialog_id));

  const Message *m = get_message_force(d, message_id, "pin_dialog_message");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }
  if (message_id.is_scheduled()) {
    return promise.set_error(Status::Error(400, "Scheduled message can't be pinned"));
  }
  if (!message_id.is_server()) {
    return promise.set_error(Status::Error(400, "Message can't be pinned"));
  }

  if (is_service_message_content(m->content->get_type())) {
    return promise.set_error(Status::Error(400, "A service message can't be pinned"));
  }

  if (only_for_self && dialog_id.get_type() != DialogType::User) {
    return promise.set_error(Status::Error(400, "Messages can't be pinned only for self in the chat"));
  }

  // TODO log event
  td_->create_handler<UpdateDialogPinnedMessageQuery>(std::move(promise))
      ->send(dialog_id, message_id, is_unpin, disable_notification, only_for_self);
}

void MessagesManager::unpin_all_dialog_messages(DialogId dialog_id, MessageId top_thread_message_id,
                                                Promise<Unit> &&promise) {
  auto d = get_dialog_force(dialog_id, "unpin_all_dialog_messages");
  if (d == nullptr) {
    return promise.set_error(Status::Error(400, "Chat not found"));
  }
  TRY_STATUS_PROMISE(promise, td_->dialog_manager_->can_pin_messages(dialog_id));
  TRY_STATUS_PROMISE(promise, can_use_top_thread_message_id(d, top_thread_message_id, MessageInputReplyTo()));

  if (!td_->auth_manager_->is_bot()) {
    auto message_ids = find_dialog_messages(d, [top_thread_message_id](const Message *m) {
      return m->is_pinned && (!top_thread_message_id.is_valid() ||
                              (m->is_topic_message && m->top_thread_message_id == top_thread_message_id));
    });

    vector<int64> deleted_message_ids;
    for (auto message_id : message_ids) {
      auto m = get_message(d, message_id);
      CHECK(m != nullptr);

      m->is_pinned = false;
      send_closure(G()->td(), &Td::send_update,
                   td_api::make_object<td_api::updateMessageIsPinned>(
                       get_chat_id_object(d->dialog_id, "updateMessageIsPinned"), m->message_id.get(), m->is_pinned));
      on_message_changed(d, m, true, "unpin_all_dialog_messages");
    }
  }

  if (top_thread_message_id.is_valid()) {
    AffectedHistoryQuery query = [td = td_, top_thread_message_id](DialogId dialog_id,
                                                                   Promise<AffectedHistory> &&query_promise) {
      td->create_handler<UnpinAllMessagesQuery>(std::move(query_promise))->send(dialog_id, top_thread_message_id);
    };
    run_affected_history_query_until_complete(dialog_id, std::move(query), true, std::move(promise));
    return;
  }

  set_dialog_last_pinned_message_id(d, MessageId());
  if (!td_->auth_manager_->is_bot() &&
      d->message_count_by_index[message_search_filter_index(MessageSearchFilter::Pinned)] != 0) {
    d->message_count_by_index[message_search_filter_index(MessageSearchFilter::Pinned)] = 0;
    on_dialog_updated(dialog_id, "unpin_all_dialog_messages");
  }

  unpin_all_dialog_messages_on_server(dialog_id, 0, std::move(promise));
}

class MessagesManager::UnpinAllDialogMessagesOnServerLogEvent {
 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_unpin_all_dialog_messages_on_server_log_event(DialogId dialog_id) {
  UnpinAllDialogMessagesOnServerLogEvent log_event{dialog_id};
  return binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::UnpinAllDialogMessagesOnServer,
                    get_log_event_storer(log_event));
}

void MessagesManager::unpin_all_dialog_messages_on_server(DialogId dialog_id, uint64 log_event_id,
                                                          Promise<Unit> &&promise) {
  if (log_event_id == 0 && G()->use_message_database()) {
    log_event_id = save_unpin_all_dialog_messages_on_server_log_event(dialog_id);
  }

  AffectedHistoryQuery query = [td = td_](DialogId dialog_id, Promise<AffectedHistory> &&query_promise) {
    td->create_handler<UnpinAllMessagesQuery>(std::move(query_promise))->send(dialog_id, MessageId());
  };
  run_affected_history_query_until_complete(dialog_id, std::move(query), true,
                                            get_erase_log_event_promise(log_event_id, std::move(promise)));
}

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) {
  CHECK(d != nullptr);
  const Message *result = nullptr;
  if (message_id.is_scheduled()) {
    if (d->scheduled_messages != nullptr && message_id.is_valid_scheduled()) {
      if (message_id.is_scheduled_server()) {
        auto server_message_id = message_id.get_scheduled_server_message_id();
        auto it = d->scheduled_messages->scheduled_message_date_.find(server_message_id);
        if (it != d->scheduled_messages->scheduled_message_date_.end()) {
          int32 date = it->second;
          message_id = MessageId(server_message_id, date);
          CHECK(message_id.is_scheduled_server());
        }
      }
      auto it = d->scheduled_messages->scheduled_messages_.find(message_id);
      if (it != d->scheduled_messages->scheduled_messages_.end()) {
        result = it->second.get();
      }
    }
  } else {
    result = d->messages.get_pointer(message_id);
    if (result != nullptr) {
      auto unix_time = G()->unix_time();
      if (unix_time > result->last_access_date + 5) {
        result->last_access_date = unix_time;
        auto list_node = const_cast<ListNode *>(static_cast<const ListNode *>(result));
        list_node->remove();
        d->message_lru_list.put_back(list_node);
      }
    }
  }
  LOG(INFO) << "Search for " << message_id << " in " << d->dialog_id << " found " << result;
  return result;
}

const MessagesManager::Message *MessagesManager::get_message_static(const Dialog *d, MessageId message_id) {
  return get_message(d, message_id);
}

MessagesManager::Message *MessagesManager::get_message_force(Dialog *d, MessageId message_id, const char *source) {
  if (!message_id.is_valid() && !message_id.is_valid_scheduled()) {
    return nullptr;
  }

  auto result = get_message(d, message_id);
  if (result != nullptr) {
    return result;
  }

  if (!G()->use_message_database() || message_id.is_yet_unsent() || is_deleted_message(d, message_id)) {
    return nullptr;
  }

  if (message_id.is_scheduled() && d->has_loaded_scheduled_messages_from_database) {
    return nullptr;
  }

  LOG(INFO) << "Trying to load " << MessageFullId{d->dialog_id, message_id} << " from database from " << source;

  auto r_value = G()->td_db()->get_message_db_sync()->get_message({d->dialog_id, message_id});
  if (r_value.is_error()) {
    return nullptr;
  }
  return on_get_message_from_database(d, r_value.ok(), message_id.is_scheduled(), source);
}

MessagesManager::Message *MessagesManager::on_get_message_from_database(const MessageDbMessage &message,
                                                                        bool is_scheduled, const char *source) {
  if (message.data.empty()) {
    return nullptr;
  }

  auto dialog_id = message.dialog_id;
  Dialog *d = get_dialog_force(dialog_id, source);
  if (d == nullptr) {
    LOG(ERROR) << "Can't find " << dialog_id << ", but have a message from it from " << source;
    if (!dialog_id.is_valid()) {
      LOG(ERROR) << "Receive message in invalid " << dialog_id << " from " << source;
      return nullptr;
    }

    bool is_valid_server_message_id =
        (is_scheduled ? message.message_id.is_valid_scheduled() && message.message_id.is_scheduled_server()
                      : message.message_id.is_valid() && message.message_id.is_server());
    if (is_valid_server_message_id &&
        (dialog_id.get_type() == DialogType::User || dialog_id.get_type() == DialogType::Chat)) {
      get_message_from_server({dialog_id, message.message_id}, Auto(), "on_get_message_from_database 1");
    }

    force_create_dialog(dialog_id, source);
    d = get_dialog_force(dialog_id, source);
    CHECK(d != nullptr);
  }

  return on_get_message_from_database(d, message.message_id, message.data, is_scheduled, source);
}

MessagesManager::Message *MessagesManager::on_get_message_from_database(Dialog *d,
                                                                        const MessageDbDialogMessage &message,
                                                                        bool is_scheduled, const char *source) {
  return on_get_message_from_database(d, message.message_id, message.data, is_scheduled, source);
}

MessagesManager::Message *MessagesManager::on_get_message_from_database(Dialog *d, MessageId expected_message_id,
                                                                        const BufferSlice &value, bool is_scheduled,
                                                                        const char *source) {
  if (value.empty()) {
    return nullptr;
  }

  auto message = parse_message(d, expected_message_id, value, is_scheduled);
  if (message == nullptr) {
    return nullptr;
  }

  CHECK(d != nullptr);
  auto dialog_id = d->dialog_id;
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    return nullptr;
  }

  auto old_message = get_message(d, message->message_id);
  if (old_message != nullptr) {
    // data in the database is always outdated, so return a message from the memory
    if (dialog_id.get_type() == DialogType::SecretChat) {
      CHECK(!is_scheduled);
      // 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, old_message->message_id);
    }

    if (old_message->notification_id.is_valid() && !is_scheduled) {
      auto notification_info = add_dialog_notification_info(d);
      add_notification_id_to_message_id_correspondence(notification_info, old_message->notification_id,
                                                       old_message->message_id);
    }

    return old_message;
  }

  Dependencies dependencies;
  add_message_dependencies(dependencies, message.get());
  if (!dependencies.resolve_force(td_, "on_get_message_from_database") &&
      dialog_id.get_type() != DialogType::SecretChat) {
    get_message_from_server({dialog_id, message->message_id}, Auto(), "on_get_message_from_database 2");
  }

  bool need_update = false;
  bool need_update_dialog_pos = false;
  auto result =
      add_message_to_dialog(d, std::move(message), true, false, &need_update, &need_update_dialog_pos, source);
  if (need_update_dialog_pos) {
    LOG(ERROR) << "Need update chat position after loading of "
               << (result == nullptr ? MessageId() : result->message_id) << " in " << dialog_id << " from " << source;
    send_update_chat_last_message(d, source);
  }
  return result;
}

vector<MessageId> MessagesManager::on_get_messages_from_database(Dialog *d, vector<MessageDbDialogMessage> &&messages,
                                                                 MessageId first_message_id, bool &have_error,
                                                                 const char *source) {
  vector<MessageId> result;
  if (!first_message_id.is_valid() || !td_->dialog_manager_->have_input_peer(d->dialog_id, AccessRights::Read)) {
    return result;
  }
  bool need_update = false;
  bool need_update_dialog_pos = false;
  auto next_message_id = MessageId::max();
  Dependencies dependencies;
  for (auto &message_slice : messages) {
    auto message = parse_message(d, message_slice.message_id, message_slice.data, false);
    if (message == nullptr) {
      have_error = true;
      break;
    }
    if (message->message_id >= next_message_id) {
      LOG(ERROR) << "Receive " << message->message_id << " after " << next_message_id
                 << " from database in the history of " << d->dialog_id;
      have_error = true;
      break;
    }
    next_message_id = message->message_id;

    if (message->message_id < first_message_id) {
      break;
    }

    result.push_back(message->message_id);
    auto *m = get_message(d, message->message_id);
    if (m == nullptr) {
      m = add_message_to_dialog(d, std::move(message), true, false, &need_update, &need_update_dialog_pos, source);
      if (m != nullptr) {
        add_message_dependencies(dependencies, m);
      }
    }
  }
  dependencies.resolve_force(td_, source);
  if (need_update_dialog_pos) {
    LOG(ERROR) << "Need update chat position after loading of " << result << " in " << d->dialog_id << " from "
               << source;
    send_update_chat_last_message(d, source);
  }
  return result;
}

void MessagesManager::fix_new_message(const Dialog *d, Message *m, bool from_database) const {
  CHECK(d != nullptr);
  CHECK(m != nullptr);

  DialogId dialog_id = d->dialog_id;
  if (!has_message_sender_user_id(dialog_id, m) && !m->sender_dialog_id.is_valid()) {
    if (td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
      m->sender_dialog_id = dialog_id;
    } else {
      if (is_discussion_message(dialog_id, m)) {
        m->sender_dialog_id = m->forward_info->get_last_dialog_id();
      } else {
        LOG(ERROR) << "Failed to repair sender chat in " << m->message_id << " in " << dialog_id;
      }
    }
  }

  auto dialog_type = dialog_id.get_type();
  if (m->sender_user_id == ContactsManager::get_anonymous_bot_user_id() && !m->sender_dialog_id.is_valid() &&
      dialog_type == DialogType::Channel && !td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
    m->sender_user_id = UserId();
    m->sender_dialog_id = dialog_id;
  }

  if (!from_database && m->message_id.is_valid()) {
    switch (dialog_type) {
      case DialogType::Chat:
      case DialogType::Channel: {
        m->available_reactions_generation = d->available_reactions_generation;
        break;
      }
      case DialogType::User:
      case DialogType::SecretChat:
        break;
      default:
        UNREACHABLE();
        break;
    }
    m->history_generation = d->history_generation;
  }

  if (m->message_id.is_scheduled() && !m->message_id.is_yet_unsent()) {
    m->top_thread_message_id = MessageId();
  } else if (m->top_thread_message_id.is_valid()) {
    if (td_->dialog_manager_->is_broadcast_channel(dialog_id)) {
      m->top_thread_message_id = MessageId();
    }
  } else {
    if (is_thread_message(dialog_id, m)) {
      m->top_thread_message_id = m->message_id;
    }
  }

  m->last_access_date = G()->unix_time();

  if (!from_database && m->contains_mention) {
    CHECK(!td_->auth_manager_->is_bot());
    auto message_content_type = m->content->get_type();
    if (message_content_type == MessageContentType::PinMessage) {
      if (is_dialog_pinned_message_notifications_disabled(d) ||
          !get_message_content_pinned_message_id(m->content.get()).is_valid()) {
        // treat message pin without pinned message as an ordinary message
        m->contains_mention = false;
      }
    } else if (is_dialog_mention_notifications_disabled(d)) {
      // disable mention notification
      m->is_mention_notification_disabled = true;
    }
  }

  if (m->contains_unread_mention && m->message_id <= d->last_read_all_mentions_message_id) {
    m->contains_unread_mention = false;
  }

  if (dialog_type == DialogType::Channel && !m->contains_unread_mention) {
    auto channel_read_media_period =
        td_->option_manager_->get_option_integer("channels_read_media_period", (G()->is_test_dc() ? 300 : 7 * 86400));
    if (m->date < G()->unix_time() - channel_read_media_period) {
      update_opened_message_content(m->content.get());
    }
  }

  if (m->reply_markup != nullptr && !m->reply_markup->is_personal &&
      (m->reply_markup->type == ReplyMarkup::Type::ForceReply ||
       m->reply_markup->type == ReplyMarkup::Type::RemoveKeyboard) &&
      !td_->auth_manager_->is_bot()) {
    m->reply_markup = nullptr;
  }
}

void MessagesManager::remove_message_remove_keyboard_reply_markup(Message *m) const {
  CHECK(m != nullptr);
  if (m->reply_markup == nullptr || m->reply_markup->type != ReplyMarkup::Type::RemoveKeyboard ||
      td_->auth_manager_->is_bot()) {
    return;
  }
  CHECK(m->reply_markup->is_personal);  // otherwise it was removed in fix_new_message
  m->had_reply_markup = true;
  m->reply_markup = nullptr;
}

void MessagesManager::update_replied_by_message_count(DialogId dialog_id, const Message *m, bool is_add) {
  CHECK(m != nullptr);
  if (!m->message_id.is_yet_unsent()) {
    return;
  }
  const auto *input_reply_to = get_message_input_reply_to(m);
  if (input_reply_to == nullptr || input_reply_to->is_empty()) {
    return;
  }
  auto replied_message_full_id = input_reply_to->get_reply_message_full_id(dialog_id);
  auto replied_message_id = replied_message_full_id.get_message_id();
  if (!replied_message_id.is_valid() && !replied_message_id.is_valid_scheduled()) {
    return;
  }
  if (!replied_message_id.is_yet_unsent()) {
    if (!replied_message_id.is_scheduled()) {
      if (is_add) {
        replied_by_yet_unsent_messages_[replied_message_full_id]++;
      } else {
        auto it = replied_by_yet_unsent_messages_.find(replied_message_full_id);
        CHECK(it != replied_by_yet_unsent_messages_.end());
        it->second--;
        if (it->second == 0) {
          replied_by_yet_unsent_messages_.erase(it);
        } else {
          CHECK(it->second > 0);
        }
      }
    }
  } else {
    if (is_add) {
      replied_yet_unsent_messages_[replied_message_full_id].insert({dialog_id, m->message_id});
    } else {
      auto it = replied_yet_unsent_messages_.find(replied_message_full_id);
      CHECK(it != replied_yet_unsent_messages_.end());
      size_t erased_count = it->second.erase({dialog_id, m->message_id});
      CHECK(erased_count > 0);
      if (it->second.empty()) {
        replied_yet_unsent_messages_.erase(it);
      }
    }
  }
}

void MessagesManager::add_message_to_dialog_message_list(const Message *m, Dialog *d, const bool from_database,
                                                         const bool from_update, const bool need_update,
                                                         bool *need_update_dialog_pos, const char *source) {
  CHECK(!td_->auth_manager_->is_bot());
  auto dialog_id = d->dialog_id;
  auto dialog_type = dialog_id.get_type();
  auto message_id = m->message_id;
  if (d->have_full_history && !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 " << m->content->get_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_outbox_message_id << ", last read all mentions is "
               << d->last_read_all_mentions_message_id << ", last clear history date is " << d->last_clear_history_date
               << ", last clear history is " << d->last_clear_history_message_id << ", last delete is "
               << d->deleted_last_message_id << ", delete last message date is " << d->delete_last_message_date
               << ", have_full_history source = " << d->have_full_history_source;
    d->have_full_history = false;
    d->have_full_history_source = 0;
    on_dialog_updated(dialog_id, "drop have_full_history");
  }

  if (d->is_empty) {
    d->is_empty = false;
    *need_update_dialog_pos = true;
  }

  if (!(d->have_full_history && from_update) && d->last_message_id.is_valid() &&
      d->last_message_id < MessageId(ServerMessageId(1)) && message_id >= MessageId(ServerMessageId(1))) {
    set_dialog_last_message_id(d, MessageId(), "add_message_to_dialog_message_list");

    set_dialog_first_database_message_id(d, MessageId(), "add_message_to_dialog_message_list");
    set_dialog_last_database_message_id(d, MessageId(), source);
    d->have_full_history = false;
    d->have_full_history_source = 0;
    invalidate_message_indexes(d);
    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 && !m->is_failed_to_send && message_id > d->last_new_message_id && !message_id.is_yet_unsent()) {
    if (dialog_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_message_list");
    }
  }

  auto old_last_message_id = d->last_message_id;

  if (need_update && message_id > d->last_read_inbox_message_id) {
    if (has_incoming_notification(dialog_id, m)) {
      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 non-scheduled outgoing message has identifier one greater than last_read_inbox_message_id,
      // then definitely there are no unread incoming messages 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 == d->last_read_inbox_message_id.get_next_message_id(MessageType::Server)) {
        read_history_inbox(d, message_id, 0, "add_message_to_dialog_message_list");
      }
    }
  }
  if (need_update && m->contains_unread_mention) {
    set_dialog_unread_mention_count(d, d->unread_mention_count + 1);
    send_update_chat_unread_mention_count(d);
  }
  if (need_update && has_unread_message_reactions(dialog_id, m)) {
    set_dialog_unread_reaction_count(d, d->unread_reaction_count + 1);
    send_update_chat_unread_reaction_count(d, "add_message_to_dialog_message_list");
  }
  if (need_update) {
    update_message_count_by_index(d, +1, m);
  }
  if (from_update && message_id > d->last_message_id && message_id >= d->last_new_message_id) {
    set_dialog_last_message_id(d, message_id, "add_message_to_dialog_message_list", m);
    *need_update_dialog_pos = true;
  }
  if (from_update && !message_id.is_yet_unsent() && message_id >= d->last_new_message_id &&
      (d->last_new_message_id.is_valid() ||
       (message_id.is_local() && d->last_message_id.is_valid() &&
        (message_id >= d->last_message_id ||
         (d->last_database_message_id.is_valid() && message_id > d->last_database_message_id))))) {
    CHECK(message_id <= d->last_message_id);
    if (message_id > d->last_database_message_id) {
      set_dialog_last_database_message_id(d, message_id, "add_message_to_dialog_message_list");
      if (!d->first_database_message_id.is_valid()) {
        set_dialog_first_database_message_id(d, message_id, "add_message_to_dialog_message_list");
        try_restore_dialog_reply_markup(d, m);
      }
    }
  }

  if (!from_update && message_id.is_server() && d->last_message_id.is_valid() && message_id > d->last_message_id) {
    LOG(INFO) << "Receive " << message_id << ", which is newer than the last " << d->last_message_id
              << " not from update";
    set_dialog_last_message_id(d, MessageId(), source);
    if (message_id > d->deleted_last_message_id) {
      d->delete_last_message_date = m->date;
      d->deleted_last_message_id = message_id;
    }

    set_dialog_first_database_message_id(d, MessageId(), source);
    set_dialog_last_database_message_id(d, MessageId(), source);
    d->have_full_history = false;
    d->have_full_history_source = 0;
    invalidate_message_indexes(d);

    on_dialog_updated(dialog_id, source);

    send_update_chat_last_message(d, source);
    *need_update_dialog_pos = false;

    on_dialog_updated(dialog_id, "do delete last message");

    send_closure_later(actor_id(this), &MessagesManager::load_last_dialog_message_later, dialog_id);
  }

  d->ordered_messages.insert(message_id, from_update, old_last_message_id, source);
}

// keep synced with add_scheduled_message_to_dialog
MessagesManager::Message *MessagesManager::add_message_to_dialog(Dialog *d, unique_ptr<Message> message,
                                                                 const bool from_database, const bool from_update,
                                                                 bool *need_update, bool *need_update_dialog_pos,
                                                                 const char *source) {
  if (!message->message_id.is_valid()) {
    return add_scheduled_message_to_dialog(d, std::move(message), from_database, from_update, need_update, source);
  }

  CHECK(need_update != nullptr);
  CHECK(need_update_dialog_pos != nullptr);
  CHECK(source != nullptr);

  fix_new_message(d, message.get(), from_database);

  debug_add_message_to_dialog_fail_reason_ = "success";

  DialogId dialog_id = d->dialog_id;
  MessageId message_id = message->message_id;

  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;

  if (*need_update) {
    CHECK(from_update);
  }

  if (message_id <= d->last_clear_history_message_id) {
    LOG(INFO) << "Skip adding cleared " << message_id << " to " << dialog_id;
    if (from_database) {
      delete_message_from_database(d, message_id, message.get(), true, "cleared full history");
    }
    debug_add_message_to_dialog_fail_reason_ = "cleared full history";
    return nullptr;
  }

  if (is_deleted_message(d, message_id)) {
    LOG(INFO) << "Skip adding deleted " << message_id << " to " << dialog_id;
    debug_add_message_to_dialog_fail_reason_ = "adding deleted message";
    return nullptr;
  }

  auto dialog_type = dialog_id.get_type();
  if (from_update && !message->is_failed_to_send) {
    if (message_id <= d->last_new_message_id && dialog_type != DialogType::Channel &&
        (message_id.is_server() || !td_->auth_manager_->is_bot())) {
      LOG(ERROR) << "New " << message_id << " in " << dialog_id << " from " << source
                 << " has identifier less than last_new_message_id = " << d->last_new_message_id;
    }
  }

  if (!from_update && !message->is_failed_to_send) {
    MessageId max_message_id;
    if (message_id.is_server()) {
      if (d->being_added_message_id.is_valid()) {
        // if a too new message not from update has failed to preload before being_added_message_id was set,
        // then it should fail to load even after it is set and last_new_message_id has changed
        max_message_id = d->being_updated_last_new_message_id;
      } else {
        max_message_id = d->last_new_message_id;
      }
    } else if (message_id.is_local()) {
      if (d->being_added_message_id.is_valid()) {
        max_message_id = d->being_updated_last_database_message_id;
      } else {
        max_message_id = d->last_database_message_id;
      }
    }
    if (max_message_id != MessageId() && message_id > max_message_id) {
      if (!from_database) {
        LOG(ERROR) << "Ignore " << message_id << " in " << dialog_id << " received not through update from " << source
                   << ". The maximum allowed is " << max_message_id << ", last is " << d->last_message_id
                   << ", being added message is " << d->being_added_message_id << ", channel difference "
                   << debug_channel_difference_dialog_ << " "
                   << to_string(get_message_object(dialog_id, message.get(), "add_message_to_dialog"));

        if (need_channel_difference_to_add_message(dialog_id, nullptr)) {
          schedule_get_channel_difference(dialog_id, 0, MessageId(), 0.001, "add_message_to_dialog");
        }
      } 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_type == DialogType::SecretChat)) &&
      message_id <= d->max_unavailable_message_id) {
    LOG(INFO) << "Can't add an unavailable " << message_id << " to " << dialog_id;
    if (from_database) {
      delete_message_from_database(d, message_id, message.get(), true, "ignore unavailable message");
    }
    debug_add_message_to_dialog_fail_reason_ = "ignore unavailable message";
    return nullptr;
  }

  auto message_content_type = message->content->get_type();
  if (message_content_type == MessageContentType::ChatDeleteHistory) {
    {
      auto old_message = delete_message(d, message_id, true, need_update_dialog_pos, "message chat delete history");
      if (old_message != nullptr) {
        send_update_delete_messages(dialog_id, {old_message->message_id.get()}, true);
      }
    }
    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;
    if (message_id > d->max_unavailable_message_id) {
      set_dialog_max_unavailable_message_id(dialog_id, message_id, false, "message chat delete history");
    }
    CHECK(!from_database);
    debug_add_message_to_dialog_fail_reason_ = "skip adding MessageChatDeleteHistory";
    return nullptr;
  }

  if (*need_update && message_id <= d->last_new_message_id && !td_->auth_manager_->is_bot()) {
    *need_update = false;
  }

  if (*need_update && d->reply_markup_message_id != MessageId()) {
    UserId bot_user_id;
    if (message->reply_markup != nullptr && message->reply_markup->type == ReplyMarkup::Type::RemoveKeyboard &&
        message_id > d->reply_markup_message_id) {
      bot_user_id = message->sender_user_id;
    } else {
      bot_user_id = get_message_content_deleted_user_id(message->content.get());
      // do not check for is_user_bot to allow deleted bots
    }
    if (bot_user_id.is_valid()) {
      const Message *old_message = get_message_force(d, d->reply_markup_message_id, "add_message_to_dialog 1");
      if (old_message == nullptr || old_message->sender_user_id == bot_user_id) {
        set_dialog_reply_markup(d, MessageId());
      }
    }
  }

  // must be after set_dialog_reply_markup(d, MessageId()), but before try_restore_dialog_reply_markup
  remove_message_remove_keyboard_reply_markup(message.get());

  {
    Message *m =
        from_database ? get_message(d, message_id) : get_message_force(d, message_id, "add_message_to_dialog 2");
    if (m != nullptr) {
      CHECK(m->message_id == message_id);
      CHECK(message->message_id == message_id);
      LOG(INFO) << "Adding already existing " << message_id << " in " << dialog_id << " from " << source;
      if (*need_update && !td_->auth_manager_->is_bot()) {
        *need_update = false;
        if (!G()->use_message_database()) {
          // can happen if the message is received first through getMessage in an unknown chat without
          // last_new_message_id and only after that received through getDifference or getChannelDifference
          if (d->last_new_message_id.is_valid()) {
            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;
          }
        }
      }
      if (!from_database && (from_update || message->edit_date >= m->edit_date)) {
        const int32 INDEX_MASK_MASK = ~(message_search_filter_index_mask(MessageSearchFilter::UnreadMention) |
                                        message_search_filter_index_mask(MessageSearchFilter::UnreadReaction));
        auto old_index_mask = get_message_index_mask(dialog_id, m) & INDEX_MASK_MASK;
        bool was_deleted = delete_active_live_location(dialog_id, m);
        auto old_file_ids = get_message_file_ids(m);

        bool need_send_update = update_message(d, m, std::move(message), true);
        if (!need_send_update) {
          LOG(INFO) << message_id << " in " << dialog_id << " is not changed";
        }
        auto new_index_mask = get_message_index_mask(dialog_id, m) & INDEX_MASK_MASK;
        if (was_deleted) {
          try_add_active_live_location(dialog_id, m);
        }
        change_message_files(dialog_id, m, old_file_ids);
        if (need_send_update) {
          on_message_notification_changed(d, m, source);
        }
        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 m;
    }
  }

  if (*need_update && may_need_message_notification(d, message.get())) {
    // notification group must be created here because it may force adding new messages from database
    // in get_message_notification_group_force
    get_dialog_notification_group_id(d->dialog_id, get_notification_group_info(d, message.get()));
  }
  if (*need_update || (!d->last_new_message_id.is_valid() && !message_id.is_yet_unsent() && from_update &&
                       !message->is_failed_to_send)) {
    auto pinned_message_id = get_message_content_pinned_message_id(message->content.get());
    if (pinned_message_id.is_valid() && pinned_message_id < message_id &&
        have_message_force(d, pinned_message_id, "preload pinned message")) {
      LOG(INFO) << "Preloaded pinned " << pinned_message_id << " from database";
    }

    if (d->notification_info != nullptr && d->notification_info->pinned_message_notification_message_id_.is_valid() &&
        d->notification_info->pinned_message_notification_message_id_ != message_id &&
        have_message_force(d, d->notification_info->pinned_message_notification_message_id_,
                           "preload previously pinned message")) {
      LOG(INFO) << "Preloaded previously pinned " << d->notification_info->pinned_message_notification_message_id_
                << " from database";
    }
  }
  if (from_update && !message->is_failed_to_send && message->top_thread_message_id.is_valid() &&
      message->top_thread_message_id != message_id && message_id.is_server() &&
      have_message_force(d, message->top_thread_message_id, "preload top reply message")) {
    LOG(INFO) << "Preloaded top thread " << message->top_thread_message_id << " from database";

    Message *top_m = get_message(d, message->top_thread_message_id);
    CHECK(top_m != nullptr);
    if (is_active_message_reply_info(dialog_id, top_m->reply_info) && is_discussion_message(dialog_id, top_m)) {
      auto linked_message_full_id = top_m->forward_info->get_last_message_full_id();
      if (have_message_force(linked_message_full_id, "preload discussed message")) {
        LOG(INFO) << "Preloaded discussed " << linked_message_full_id << " from database";
      }
    }
  }

  // there must be no two recursive calls to add_message_to_dialog
  LOG_CHECK(!d->being_added_message_id.is_valid())
      << d->dialog_id << " " << d->being_added_message_id << " " << message_id << " " << *need_update << " "
      << d->last_new_message_id << " " << source;
  LOG_CHECK(!d->being_deleted_message_id.is_valid())
      << d->being_deleted_message_id << " " << message_id << " " << source;

  d->being_added_message_id = message_id;
  d->being_updated_last_new_message_id = d->last_new_message_id;
  d->being_updated_last_database_message_id = d->last_database_message_id;

  if (d->notification_info != nullptr && d->notification_info->new_secret_chat_notification_id_.is_valid()) {
    remove_new_secret_chat_notification(d, true);
  }

  if (message->message_id > d->max_added_message_id) {
    d->max_added_message_id = message->message_id;
  }

  if (d->open_count == 0 && !d->has_unload_timeout && is_message_unload_enabled() &&
      (!d->messages.empty() || !from_database)) {
    LOG(INFO) << "Schedule unload of " << dialog_id;
    pending_unload_dialog_timeout_.add_timeout_in(dialog_id.get(), get_next_unload_dialog_delay(d));
    d->has_unload_timeout = true;
  }

  if (message->ttl.is_valid() && message->ttl_expires_at != 0) {
    auto now = Time::now();
    if (message->ttl_expires_at <= now) {
      if (dialog_type == DialogType::SecretChat) {
        LOG(INFO) << "Can't add " << message_id << " with expired " << (now - message->ttl_expires_at)
                  << " seconds ago self-destruct timer to " << dialog_id << " from " << source;
        delete_message_from_database(d, message_id, message.get(), true, "delete self-destructed message");
        debug_add_message_to_dialog_fail_reason_ = "delete self-destructed message";
        d->being_added_message_id = MessageId();
        return nullptr;
      } else {
        on_message_ttl_expired_impl(d, message.get(), false);
        message_content_type = message->content->get_type();
        if (from_database) {
          on_message_changed(d, message.get(), false, "add expired message to dialog");
        }
      }
    } else {
      ttl_register_message(dialog_id, message.get(), now);
    }
  }
  if (message->ttl_period > 0) {
    CHECK(dialog_type != DialogType::SecretChat);
    auto server_time = G()->server_time();
    if (message->date + message->ttl_period <= server_time) {
      LOG(INFO) << "Can't add auto-deleted " << message_id << " sent at " << message->date << " with auto-delete time "
                << message->ttl_period << " to " << dialog_id << " from " << source;
      delete_message_from_database(d, message_id, message.get(), true, "delete auto-deleted message");
      debug_add_message_to_dialog_fail_reason_ = "delete auto-deleted message";
      d->being_added_message_id = MessageId();
      return nullptr;
    } else {
      ttl_period_register_message(dialog_id, message.get(), server_time);
    }
  }

  if (!message->are_media_timestamp_entities_found) {
    message->are_media_timestamp_entities_found = true;
    if (from_database && !td_->auth_manager_->is_bot()) {  // otherwise the entities were found in get_message_text
      auto text = get_message_content_text_mutable(message->content.get());
      if (text != nullptr) {
        fix_formatted_text(text->text, text->entities, true, true, true, false, false).ensure();
        // always call to save are_media_timestamp_entities_found flag
        on_message_changed(d, message.get(), false, "save media timestamp entities");
      }
    }
  }

  LOG(INFO) << "Adding not found " << message_id << " to " << dialog_id << " from " << source;
  if (G()->keep_media_order() && message_id.is_yet_unsent() && !message->via_bot_user_id.is_valid() &&
      !message->hide_via_bot && !message->is_copy) {
    auto queue_id = ChainId(dialog_id, message_content_type).get();
    if (queue_id & 1) {
      LOG(INFO) << "Add " << message_id << " from " << source << " to queue " << queue_id;
      auto &queue = yet_unsent_media_queues_[queue_id];
      queue.dialog_id_ = dialog_id;
      queue.queue_[message_id];  // reserve place for promise
      if (!td_->auth_manager_->is_bot()) {
        pending_send_dialog_action_timeout_.add_timeout_in(dialog_id.get(), 1.0);
      }
    }
  }

  if (!td_->auth_manager_->is_bot()) {
    if (*need_update) {
      // 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);
    } else {
      if (from_database && message->notification_id.is_valid() && is_from_mention_notification_group(message.get()) &&
          is_message_notification_active(d, message.get()) && is_dialog_mention_notifications_disabled(d) &&
          d->notification_info != nullptr &&
          message_id != d->notification_info->pinned_message_notification_message_id_) {
        auto notification_id = message->notification_id;
        VLOG(notifications) << "Remove mention " << notification_id << " in " << message_id << " in " << dialog_id;
        message->notification_id = NotificationId();
        if (d->notification_info->mention_notification_group_.get_last_notification_id() == notification_id) {
          // last notification is deleted, need to find new last notification
          fix_dialog_last_notification_id(d, true, message_id);
        }

        send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification,
                           d->notification_info->mention_notification_group_.get_group_id(), notification_id, false,
                           false, Promise<Unit>(), "remove disabled mention notification");

        on_message_changed(d, message.get(), false, "remove_mention_notification");
      }
    }
  }

  const Message *m = message.get();

  if (!td_->auth_manager_->is_bot()) {
    add_message_to_dialog_message_list(m, d, from_database, from_update, *need_update, need_update_dialog_pos, source);
  }

  update_replied_by_message_count(dialog_id, m, true);

  if (!from_database && !m->message_id.is_yet_unsent()) {
    add_message_to_database(d, m, "add_message_to_dialog");
  }

  if (from_update && !m->is_failed_to_send && dialog_type == DialogType::Channel) {
    auto now = max(G()->unix_time(), m->date);
    if (m->date < now - 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);
    }
    if (m->date + 3600 >= now && m->is_outgoing) {
      auto channel_id = dialog_id.get_channel_id();
      auto slow_mode_delay = td_->contacts_manager_->get_channel_slow_mode_delay(channel_id, "add_message_to_dialog");
      auto status = td_->contacts_manager_->get_channel_status(dialog_id.get_channel_id());
      if (m->date + slow_mode_delay > now && !status.is_administrator()) {
        td_->contacts_manager_->on_update_channel_slow_mode_next_send_date(channel_id, m->date + slow_mode_delay);
      }
    }
    if (m->date > now - 14 * 86400) {
      td_->contacts_manager_->remove_inactive_channel(dialog_id.get_channel_id());
    }
  }

  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 (m->sender_user_id.is_valid()) {
    auto story_full_id = get_message_content_story_full_id(td_, m->content.get());
    if (story_full_id.is_valid()) {
      td_->story_manager_->on_story_replied(story_full_id, m->sender_user_id);
    }
    if (m->reply_to_story_full_id.is_valid()) {
      td_->story_manager_->on_story_replied(m->reply_to_story_full_id, m->sender_user_id);
    }
  }

  reget_message_from_server_if_needed(dialog_id, m);

  add_message_file_sources(dialog_id, m);

  register_message_content(td_, m->content.get(), {dialog_id, m->message_id}, "add_message_to_dialog");

  register_message_reply(dialog_id, m);

  if (*need_update && m->message_id.is_server()) {
    switch (message_content_type) {
      case MessageContentType::PinMessage: {
        auto pinned_message_id = get_message_content_pinned_message_id(m->content.get());
        if (d->is_last_pinned_message_id_inited && pinned_message_id > d->last_pinned_message_id) {
          set_dialog_last_pinned_message_id(d, pinned_message_id);
        }
        break;
      }
      case MessageContentType::SetBackground:
        set_dialog_background(d, get_message_content_my_background_info(
                                     m->content.get(), m->is_outgoing || dialog_id.get_type() != DialogType::User));
        break;
      case MessageContentType::ChatSetTheme:
        set_dialog_theme_name(d, get_message_content_theme_name(m->content.get()));
        break;
      case MessageContentType::ChatCreate:
      case MessageContentType::ChannelCreate:
        if (m->ttl_period > 0) {
          set_dialog_message_ttl(d, MessageTtl(m->ttl_period));
        }
        break;
      default:
        // nothing to do
        break;
    }
  }

  if (from_update && !m->is_failed_to_send) {
    speculatively_update_active_group_call_id(d, m);
    speculatively_update_channel_participants(dialog_id, m);
    update_forum_topic_info_by_service_message_content(td_, m->content.get(), dialog_id, m->top_thread_message_id);
    update_sent_message_contents(dialog_id, m);
    update_used_hashtags(dialog_id, m);
    update_top_dialogs(dialog_id, m);
    cancel_dialog_action(dialog_id, m);
    update_has_outgoing_messages(dialog_id, m);

    if (!td_->auth_manager_->is_bot() && d->messages.empty() && !m->is_outgoing &&
        dialog_id != td_->dialog_manager_->get_my_dialog_id()) {
      switch (dialog_type) {
        case DialogType::User:
          td_->contacts_manager_->invalidate_user_full(dialog_id.get_user_id());
          td_->contacts_manager_->reload_user_full(dialog_id.get_user_id(), Promise<Unit>(), "add_message_to_dialog");
          break;
        case DialogType::Chat:
        case DialogType::Channel:
          // nothing to do
          break;
        case DialogType::SecretChat: {
          auto user_id = td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id());
          if (user_id.is_valid()) {
            td_->contacts_manager_->invalidate_user_full(user_id);
            td_->contacts_manager_->reload_user_full(user_id, Promise<Unit>(), "add_message_to_dialog");
          }
          break;
        }
        case DialogType::None:
        default:
          UNREACHABLE();
      }
    }
  }

  if (m->is_topic_message) {
    td_->forum_topic_manager_->on_topic_message_count_changed(dialog_id, m->top_thread_message_id, +1);
  }

  Message *result_message = message.get();
  d->messages.set(message_id, std::move(message));

  d->message_lru_list.put_back(result_message);

  if (m->message_id.is_yet_unsent() && !m->message_id.is_scheduled() && m->top_thread_message_id.is_valid() &&
      !td_->auth_manager_->is_bot()) {
    auto is_inserted =
        yet_unsent_thread_message_ids_[MessageFullId{dialog_id, m->top_thread_message_id}].insert(m->message_id).second;
    CHECK(is_inserted);
  }

  if (!td_->auth_manager_->is_bot() && dialog_type == DialogType::User && !m->is_outgoing) {
    td_->contacts_manager_->allow_send_message_to_user(dialog_id.get_user_id());
  }

  switch (dialog_type) {
    case DialogType::User:
    case DialogType::Chat:
      if (m->message_id.is_server()) {
        message_id_to_dialog_id_.set(m->message_id, dialog_id);
      }
      break;
    case DialogType::Channel:
      if (m->message_id.is_server() && !td_->auth_manager_->is_bot()) {
        td_->contacts_manager_->register_message_users({dialog_id, m->message_id}, get_message_user_ids(m));
        td_->contacts_manager_->register_message_channels({dialog_id, m->message_id}, get_message_channel_ids(m));
      }
      break;
    case DialogType::SecretChat:
      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }

  if (m->notification_id.is_valid()) {
    auto notification_info = add_dialog_notification_info(d);
    add_notification_id_to_message_id_correspondence(notification_info, m->notification_id, m->message_id);
  }
  if (m->message_id.is_yet_unsent() || dialog_type == DialogType::SecretChat) {
    add_random_id_to_message_id_correspondence(d, m->random_id, m->message_id);
  }

  try_add_bot_command_message_id(dialog_id, m);

  // must be called after the message is added to correctly update replies
  update_message_max_reply_media_timestamp(d, result_message, false);
  update_message_max_own_media_timestamp(d, result_message);

  if (!td_->auth_manager_->is_bot() && from_update && m->saved_messages_topic_id.is_valid()) {
    CHECK(dialog_id == td_->dialog_manager_->get_my_dialog_id());
    td_->saved_messages_manager_->set_topic_last_message_id(m->saved_messages_topic_id, m->message_id, m->date);
  }

  result_message->debug_source = source;
  d->being_added_message_id = MessageId();

  added_message_count_++;

  return result_message;
}

MessagesManager::Message *MessagesManager::add_scheduled_message_to_dialog(Dialog *d, unique_ptr<Message> message,
                                                                           bool from_database, bool from_update,
                                                                           bool *need_update, const char *source) {
  CHECK(message != nullptr);
  CHECK(d != nullptr);
  CHECK(need_update != nullptr);
  CHECK(source != nullptr);

  DialogId dialog_id = d->dialog_id;
  MessageId message_id = message->message_id;
  CHECK(message_id.is_valid_scheduled());
  CHECK(!message->notification_id.is_valid());
  CHECK(!message->removed_notification_id.is_valid());

  fix_new_message(d, message.get(), from_database);

  debug_add_message_to_dialog_fail_reason_ = "success";

  LOG(INFO) << "Adding " << message_id << " of type " << message->content->get_type() << " to " << dialog_id << " from "
            << source;

  if (is_deleted_message(d, message_id)) {
    LOG(INFO) << "Skip adding deleted " << message_id << " to " << dialog_id << " from " << source;
    debug_add_message_to_dialog_fail_reason_ = "adding deleted scheduled message";
    return nullptr;
  }

  if (dialog_id.get_type() == DialogType::SecretChat) {
    LOG(ERROR) << "Tried to add " << message_id << " to " << dialog_id << " from " << source;
    debug_add_message_to_dialog_fail_reason_ = "skip adding scheduled message to secret chat";
    return nullptr;
  }
  if (message->ttl != MessageSelfDestructType() || message->ttl_expires_at != 0) {
    LOG(ERROR) << "Tried to add " << message_id << " with " << message->ttl << '/' << message->ttl_expires_at << " to "
               << dialog_id << " from " << source;
    debug_add_message_to_dialog_fail_reason_ = "skip adding secret scheduled message";
    return nullptr;
  }
  if (message->ttl_period != 0) {
    LOG(ERROR) << "Tried to add " << message_id << " with auto-delete timer " << message->ttl_period << " to "
               << dialog_id << " from " << source;
    debug_add_message_to_dialog_fail_reason_ = "skip adding auto-deleting scheduled message";
    return nullptr;
  }
  if (td_->auth_manager_->is_bot()) {
    LOG(ERROR) << "Bot tried to add " << message_id << " to " << dialog_id << " from " << source;
    debug_add_message_to_dialog_fail_reason_ = "skip adding scheduled message by bot";
    return nullptr;
  }

  auto message_content_type = message->content->get_type();
  if (is_service_message_content(message_content_type) || message_content_type == MessageContentType::LiveLocation ||
      is_expired_message_content(message_content_type)) {
    LOG(ERROR) << "Tried to add " << message_id << " of type " << message_content_type << " to " << dialog_id
               << " from " << source;
    debug_add_message_to_dialog_fail_reason_ = "skip adding message of unexpected type";
    return nullptr;
  }

  {
    Message *m = from_database ? get_message(d, message_id)
                               : get_message_force(d, message_id, "add_scheduled_message_to_dialog");
    if (m != nullptr) {
      auto old_message_id = m->message_id;
      LOG(INFO) << "Adding already existing " << old_message_id << " in " << dialog_id << " from " << source;
      message->message_id = old_message_id;
      if (!from_database) {
        auto old_file_ids = get_message_file_ids(m);
        update_message(d, m, std::move(message), true);
        change_message_files(dialog_id, m, old_file_ids);
      }
      if (old_message_id != message_id) {
        being_readded_message_id_ = {dialog_id, old_message_id};
        message = do_delete_scheduled_message(d, old_message_id, false, "add_scheduled_message_to_dialog");
        CHECK(message != nullptr);
        send_update_delete_messages(dialog_id, {message->message_id.get()}, false);
        message->message_id = message_id;
        from_database = false;
      } else {
        *need_update = false;
        return m;
      }
    }
  }

  LOG(INFO) << "Adding not found " << message_id << " to " << dialog_id << " from " << source;

  const Message *m = message.get();

  update_replied_by_message_count(dialog_id, m, true);

  if (!from_database && !m->message_id.is_yet_unsent()) {
    add_message_to_database(d, m, "add_scheduled_message_to_dialog");
  }

  reget_message_from_server_if_needed(dialog_id, m);

  add_message_file_sources(dialog_id, m);

  register_message_content(td_, m->content.get(), {dialog_id, m->message_id}, "add_scheduled_message_to_dialog");

  if (m->message_id.is_yet_unsent()) {
    add_random_id_to_message_id_correspondence(d, m->random_id, m->message_id);
  }

  // must be called after register_message_content, which loads web page
  update_message_max_reply_media_timestamp(d, message.get(), false);
  update_message_max_own_media_timestamp(d, message.get());

  register_message_reply(dialog_id, m);

  if (from_update && !m->is_failed_to_send) {
    update_sent_message_contents(dialog_id, m);
    update_used_hashtags(dialog_id, m);
    update_has_outgoing_messages(dialog_id, m);
  }

  if (m->is_topic_message) {
    td_->forum_topic_manager_->on_topic_message_count_changed(dialog_id, m->top_thread_message_id, +1);
  }

  auto *scheduled_messages = add_dialog_scheduled_messages(d);
  if (m->message_id.is_scheduled_server()) {
    auto is_inserted =
        scheduled_messages->scheduled_message_date_.emplace(m->message_id.get_scheduled_server_message_id(), m->date)
            .second;
    CHECK(is_inserted);
  }

  auto result_message = message.get();
  auto is_inserted =
      scheduled_messages->scheduled_messages_.emplace(result_message->message_id, std::move(message)).second;
  CHECK(is_inserted);
  being_readded_message_id_ = MessageFullId();
  return result_message;
}

void MessagesManager::register_new_local_message_id(Dialog *d, const Message *m) {
  if (m == nullptr) {
    return;
  }
  if (m->message_id.is_scheduled()) {
    return;
  }
  CHECK(m->message_id.is_local());
  if (m->top_thread_message_id.is_valid() && m->top_thread_message_id != m->message_id) {
    Message *top_m = get_message_force(d, m->top_thread_message_id, "register_new_local_message_id");
    if (top_m != nullptr && top_m->top_thread_message_id == top_m->message_id) {
      auto it = std::lower_bound(top_m->local_thread_message_ids.begin(), top_m->local_thread_message_ids.end(),
                                 m->message_id);
      if (it == top_m->local_thread_message_ids.end() || *it != m->message_id) {
        top_m->local_thread_message_ids.insert(it, m->message_id);
        if (top_m->local_thread_message_ids.size() >= 1000) {
          top_m->local_thread_message_ids.erase(top_m->local_thread_message_ids.begin());
        }
        on_message_changed(d, top_m, false, "register_new_local_message_id");
      }
    }
  }
}

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) {
    send_update_last_message_if_needed(d, m, 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::on_message_notification_changed(Dialog *d, const Message *m, const char *source) {
  CHECK(d != nullptr);
  CHECK(m != nullptr);
  if (m->notification_id.is_valid() && is_message_notification_active(d, m)) {
    auto &group_info = get_notification_group_info(d, m);
    if (group_info.is_valid()) {
      send_closure_later(G()->notification_manager(), &NotificationManager::edit_notification,
                         group_info.get_group_id(), m->notification_id,
                         create_new_message_notification(
                             m->message_id, is_message_preview_enabled(d, m, is_from_mention_notification_group(m))));
    }
  }
  if (m->is_pinned && d->notification_info != nullptr &&
      d->notification_info->pinned_message_notification_message_id_.is_valid() &&
      d->notification_info->mention_notification_group_.is_valid()) {
    auto pinned_message =
        get_message_force(d, d->notification_info->pinned_message_notification_message_id_, "after update_message");
    if (pinned_message != nullptr && pinned_message->notification_id.is_valid() &&
        is_message_notification_active(d, pinned_message) &&
        get_message_content_pinned_message_id(pinned_message->content.get()) == m->message_id) {
      send_closure_later(
          G()->notification_manager(), &NotificationManager::edit_notification,
          d->notification_info->mention_notification_group_.get_group_id(), pinned_message->notification_id,
          create_new_message_notification(pinned_message->message_id, is_message_preview_enabled(d, m, true)));
    }
  }
}

void MessagesManager::add_message_to_database(const Dialog *d, const Message *m, const char *source) {
  if (!G()->use_message_database()) {
    return;
  }

  CHECK(d != nullptr);
  CHECK(m != nullptr);
  MessageId message_id = m->message_id;

  if (message_id.is_scheduled()) {
    LOG(INFO) << "Add " << MessageFullId(d->dialog_id, message_id) << " to database from " << source;

    set_dialog_has_scheduled_database_messages(d->dialog_id, true);
    G()->td_db()->get_message_db_async()->add_scheduled_message({d->dialog_id, message_id}, log_event_store(*m),
                                                                Auto());  // TODO Promise
    return;
  }
  LOG_CHECK(message_id.is_server() || message_id.is_local()) << source;

  LOG(INFO) << "Add " << MessageFullId(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_message_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_message_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()) + 1;
  }
  if (m->ttl_period != 0 && (ttl_expires_at == 0 || m->date + m->ttl_period < ttl_expires_at)) {
    ttl_expires_at = m->date + m->ttl_period;
  }
  G()->td_db()->get_message_db_async()->add_message({d->dialog_id, message_id}, unique_message_id,
                                                    get_message_sender(m), random_id, ttl_expires_at,
                                                    get_message_index_mask(d->dialog_id, m), search_id, text,
                                                    m->notification_id, m->top_thread_message_id, log_event_store(*m),
                                                    Auto());  // TODO Promise
}

void MessagesManager::delete_all_dialog_notifications(Dialog *d, MessageId max_message_id, const char *source) {
  CHECK(d != nullptr);
  if (d->notification_info == nullptr) {
    return;
  }
  if (d->notification_info->new_secret_chat_notification_id_.is_valid()) {
    remove_new_secret_chat_notification(d, true);
  }
  if (d->notification_info->pinned_message_notification_message_id_.is_valid() &&
      d->notification_info->pinned_message_notification_message_id_ <= max_message_id) {
    remove_dialog_pinned_message_notification(d, source);
  }
  remove_message_dialog_notifications(d, max_message_id, false, source);
  remove_message_dialog_notifications(d, max_message_id, true, source);
}

void MessagesManager::delete_all_dialog_messages_from_database(Dialog *d, MessageId max_message_id,
                                                               const char *source) {
  CHECK(d != nullptr);
  CHECK(max_message_id.is_valid());
  if (!G()->use_message_database()) {
    return;
  }

  auto dialog_id = d->dialog_id;
  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 <= max_message_id) {
        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_message_db_async()->delete_all_dialog_messages(dialog_id, max_message_id, Auto());  // TODO Promise
}

class MessagesManager::DeleteMessageLogEvent {
 public:
  LogEvent::Id id_{0};
  MessageFullId message_full_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(message_full_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(message_full_id_, parser);
    if (has_file_ids) {
      td::parse(file_ids_, parser);
    }
  }
};

void MessagesManager::delete_message_files(DialogId dialog_id, const Message *m) const {
  for (auto file_id : get_message_file_ids(m)) {
    if (need_delete_file({dialog_id, m->message_id}, file_id)) {
      send_closure(G()->file_manager(), &FileManager::delete_file, file_id, Promise<Unit>(), "delete_message_files");
    }
  }
}

bool MessagesManager::need_delete_file(MessageFullId message_full_id, FileId file_id) const {
  if (being_readded_message_id_ == message_full_id || td_->auth_manager_->is_bot()) {
    return false;
  }

  auto main_file_id = td_->file_manager_->get_file_view(file_id).get_main_file_id();
  auto message_full_ids = td_->file_reference_manager_->get_some_message_file_sources(main_file_id);
  LOG(INFO) << "Receive " << message_full_ids << " as sources for file " << main_file_id << "/" << file_id << " from "
            << message_full_id;
  for (const auto &other_full_messsage_id : message_full_ids) {
    if (other_full_messsage_id != message_full_id) {
      return false;
    }
  }

  return true;
}

bool MessagesManager::need_delete_message_files(DialogId dialog_id, const Message *m) const {
  if (m == nullptr || td_->auth_manager_->is_bot()) {
    return false;
  }

  auto dialog_type = dialog_id.get_type();
  if (!m->message_id.is_scheduled() && !m->message_id.is_server() && dialog_type != DialogType::SecretChat) {
    return false;
  }
  if (being_readded_message_id_ == MessageFullId{dialog_id, m->message_id}) {
    return false;
  }

  if (m->forward_info != nullptr && m->forward_info->get_last_dialog_id().is_valid()) {
    // this function must not try to load the message, because it can be called from
    // do_delete_message or add_scheduled_message_to_dialog
    const Message *old_m = get_message(m->forward_info->get_last_message_full_id());
    if (old_m != nullptr && get_message_file_ids(old_m) == get_message_file_ids(m)) {
      return false;
    }
  }

  return true;
}

void MessagesManager::delete_message_from_database(Dialog *d, MessageId message_id, const Message *m,
                                                   bool is_permanently_deleted, const char *source) {
  CHECK(d != nullptr);
  if (!message_id.is_valid() && !message_id.is_valid_scheduled()) {
    return;
  }

  if (message_id.is_yet_unsent()) {
    return;
  }

  if (m != nullptr && !m->message_id.is_scheduled() && m->message_id.is_local() &&
      m->top_thread_message_id.is_valid() && m->top_thread_message_id != m->message_id) {
    // must not load the message from the database
    Message *top_m = get_message(d, m->top_thread_message_id);
    if (top_m != nullptr && top_m->top_thread_message_id == top_m->message_id) {
      auto it = std::lower_bound(top_m->local_thread_message_ids.begin(), top_m->local_thread_message_ids.end(),
                                 m->message_id);
      if (it != top_m->local_thread_message_ids.end() && *it == m->message_id) {
        top_m->local_thread_message_ids.erase(it);
        on_message_changed(d, top_m, false, "delete_message_from_database");
      }
    }
  }

  if (is_permanently_deleted) {
    if (message_id.is_scheduled() && message_id.is_valid_scheduled() && message_id.is_scheduled_server()) {
      add_dialog_scheduled_messages(d)->deleted_scheduled_server_message_ids_.insert(
          message_id.get_scheduled_server_message_id());
    } else {
      // don't store failed to send message identifiers for bots to reduce memory usage
      if (m == nullptr || !td_->auth_manager_->is_bot() || !m->is_failed_to_send) {
        d->deleted_message_ids.insert(message_id);
        send_closure_later(actor_id(this),
                           &MessagesManager::update_message_max_reply_media_timestamp_in_replied_messages, d->dialog_id,
                           message_id);
      }
    }

    if (message_id.is_any_server()) {
      auto old_message_id = find_old_message_id(d->dialog_id, message_id);
      if (old_message_id.is_valid()) {
        bool have_old_message = get_message(d, old_message_id) != nullptr;
        LOG(WARNING) << "Sent " << MessageFullId{d->dialog_id, message_id}
                     << " was deleted before it was received. Have old " << old_message_id << " = " << have_old_message;
        send_closure_later(actor_id(this), &MessagesManager::delete_messages, d->dialog_id,
                           vector<MessageId>{old_message_id}, false, Promise<Unit>());
        delete_update_message_id(d->dialog_id, message_id);
      }
    }
  }

  if (m != nullptr && m->notification_id.is_valid()) {
    CHECK(!message_id.is_scheduled());
    auto from_mentions = is_from_mention_notification_group(m);
    auto &group_info = get_notification_group_info(d, from_mentions);

    if (group_info.is_valid()) {
      if (group_info.get_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);
      }
      if (is_message_notification_active(d, m)) {
        send_closure_later(G()->notification_manager(), &NotificationManager::remove_notification,
                           group_info.get_group_id(), m->notification_id, true, false, Promise<Unit>(),
                           "delete_message_from_database");
      }
    }
  } else if (!message_id.is_scheduled() && message_id > d->last_new_message_id && d->notification_info != nullptr) {
    send_closure_later(G()->notification_manager(), &NotificationManager::remove_temporary_notification_by_object_id,
                       d->notification_info->message_notification_group_.get_group_id(), message_id, false,
                       "delete_message_from_database");
    send_closure_later(G()->notification_manager(), &NotificationManager::remove_temporary_notification_by_object_id,
                       d->notification_info->mention_notification_group_.get_group_id(), message_id, false,
                       "delete_message_from_database");
  }

  auto need_delete_files = need_delete_message_files(d->dialog_id, m);
  if (need_delete_files) {
    delete_message_files(d->dialog_id, m);
  }

  if (G()->use_message_database()) {
    DeleteMessageLogEvent log_event;

    log_event.message_full_id_ = {d->dialog_id, message_id};

    if (need_delete_files) {
      log_event.file_ids_ = get_message_file_ids(m);
    }

    do_delete_message_log_event(log_event);
  }

  on_message_deleted_from_database(d, m, source);
}

void MessagesManager::do_delete_message_log_event(const DeleteMessageLogEvent &log_event) const {
  CHECK(G()->use_message_database());

  Promise<Unit> db_promise;
  if (!log_event.file_ids_.empty()) {
    auto log_event_id = log_event.id_;
    if (log_event_id == 0) {
      log_event_id =
          binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::DeleteMessage, get_log_event_storer(log_event));
    }

    MultiPromiseActorSafe mpas{"DeleteMessageMultiPromiseActor"};
    mpas.add_promise(
        PromiseCreator::lambda([log_event_id, context_weak_ptr = get_context_weak_ptr()](Result<Unit> result) {
          auto context = context_weak_ptr.lock();
          if (result.is_error() || context == nullptr) {
            return;
          }
          CHECK(context->get_id() == Global::ID);
          auto global = static_cast<Global *>(context.get());
          if (global->close_flag()) {
            return;
          }

          binlog_erase(global->td_db()->get_binlog(), log_event_id);
        }));

    auto lock = mpas.get_promise();
    for (auto file_id : log_event.file_ids_) {
      if (need_delete_file(log_event.message_full_id_, file_id)) {
        send_closure(G()->file_manager(), &FileManager::delete_file, file_id, mpas.get_promise(),
                     "do_delete_message_log_event");
      }
    }
    db_promise = mpas.get_promise();
    lock.set_value(Unit());
  }

  // message may not exist in the dialog
  LOG(INFO) << "Delete " << log_event.message_full_id_ << " from database";
  G()->td_db()->get_message_db_async()->delete_message(log_event.message_full_id_, std::move(db_promise));
}

int64 MessagesManager::get_message_reply_to_random_id(const Dialog *d, const Message *m) const {
  auto same_chat_reply_to_message_id = m->replied_message_info.get_same_chat_reply_to_message_id(false);
  if (same_chat_reply_to_message_id != MessageId() && m->message_id.is_yet_unsent() &&
      (d->dialog_id.get_type() == DialogType::SecretChat || same_chat_reply_to_message_id.is_yet_unsent())) {
    auto *replied_m = get_message(d, same_chat_reply_to_message_id);
    if (replied_m != nullptr) {
      return replied_m->random_id;
    }
  }
  return 0;
}

bool MessagesManager::update_message(Dialog *d, Message *old_message, unique_ptr<Message> new_message,
                                     bool is_message_in_dialog) {
  CHECK(d != nullptr);
  CHECK(old_message != nullptr);
  CHECK(new_message != nullptr);
  LOG_CHECK(old_message->message_id == new_message->message_id)
      << d->dialog_id << ' ' << old_message->message_id << ' ' << new_message->message_id << ' '
      << is_message_in_dialog;

  DialogId dialog_id = d->dialog_id;
  MessageId message_id = old_message->message_id;
  auto old_content_type = old_message->content->get_type();
  auto new_content_type = new_message->content->get_type();
  bool is_scheduled = message_id.is_scheduled();
  bool need_send_update = false;
  bool is_new_available =
      new_content_type != MessageContentType::ChatDeleteHistory && new_message->restriction_reasons.empty();
  bool replace_legacy = (old_message->legacy_layer != 0 &&
                         (new_message->legacy_layer == 0 || old_message->legacy_layer < new_message->legacy_layer)) ||
                        old_content_type == MessageContentType::Unsupported;
  bool was_visible_message_reply_info = is_visible_message_reply_info(dialog_id, old_message);
  if (old_message->date != new_message->date) {
    if (new_message->date > 0 && (is_scheduled || !is_message_in_dialog)) {
      if (!(is_scheduled || message_id.is_yet_unsent() ||
            (message_id.is_server() && message_id.get_server_message_id().get() == 1) ||
            old_content_type == MessageContentType::ChannelMigrateFrom ||
            old_content_type == MessageContentType::ChannelCreate)) {
        LOG(ERROR) << "Date has changed for " << message_id << " in " << dialog_id << " from " << old_message->date
                   << " to " << new_message->date << ", message content type is " << old_content_type << '/'
                   << new_content_type;
      }
      CHECK(old_message->date > 0);
      LOG(DEBUG) << "Message date has changed from " << old_message->date << " to " << new_message->date;
      old_message->date = new_message->date;

      if (is_scheduled && message_id.is_scheduled_server()) {
        CHECK(d->scheduled_messages != nullptr);
        CHECK(is_message_in_dialog);
        int32 &date = d->scheduled_messages->scheduled_message_date_[message_id.get_scheduled_server_message_id()];
        CHECK(date != 0);
        date = new_message->date;
      }

      need_send_update = true;
    } else {
      LOG(ERROR) << "Receive " << message_id << " in " << dialog_id << " with date changed from " << old_message->date
                 << " to " << new_message->date << ", message content type is " << old_content_type << '/'
                 << new_content_type;
    }
  }
  if (old_message->date == old_message->edited_schedule_date) {
    old_message->edited_schedule_date = 0;
  }
  bool is_edited = false;
  int32 old_shown_edit_date = old_message->hide_edit_date ? 0 : old_message->edit_date;
  if (old_message->edit_date != new_message->edit_date) {
    if (new_message->edit_date > 0) {
      if (new_message->edit_date > old_message->edit_date) {
        LOG(DEBUG) << "Message edit date has changed from " << old_message->edit_date << " to "
                   << new_message->edit_date;
        old_message->edit_date = new_message->edit_date;
      }
    } else {
      LOG(ERROR) << "Receive " << message_id << " in " << dialog_id << " of type " << old_content_type << "/"
                 << new_content_type << " 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(DEBUG) << "Author signature has changed for " << message_id << " in " << dialog_id << " sent by "
               << old_message->sender_user_id << "/" << new_message->sender_user_id << " or "
               << old_message->sender_dialog_id << "/" << new_message->sender_dialog_id << " from "
               << old_message->author_signature << " to " << new_message->author_signature;
    old_message->author_signature = std::move(new_message->author_signature);
    need_send_update = true;
  }
  if (old_message->sender_user_id != new_message->sender_user_id) {
    // there can be race for sent signed posts or changed anonymous flag
    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 << ", message content type is " << old_content_type << '/' << new_content_type;

    LOG_IF(WARNING, (new_message->sender_user_id.is_valid() || old_message->author_signature.empty()) &&
                        !old_message->sender_dialog_id.is_valid() && !new_message->sender_dialog_id.is_valid())
        << "Update message sender from " << old_message->sender_user_id << " to " << new_message->sender_user_id
        << " in " << dialog_id;
    LOG(DEBUG) << "Change message sender";
    old_message->sender_user_id = new_message->sender_user_id;
    need_send_update = true;
  }
  if (old_message->sender_dialog_id != new_message->sender_dialog_id) {
    // there can be race for changed anonymous flag
    LOG_IF(ERROR, old_message->sender_dialog_id != DialogId() && new_message->sender_dialog_id != DialogId())
        << message_id << " in " << dialog_id << " has changed sender from " << old_message->sender_dialog_id << " to "
        << new_message->sender_dialog_id << ", message content type is " << old_content_type << '/' << new_content_type;

    LOG(DEBUG) << "Change message sender";
    old_message->sender_dialog_id = new_message->sender_dialog_id;
    need_send_update = true;
  }
  if (old_message->sender_boost_count != new_message->sender_boost_count) {
    LOG(DEBUG) << "Change sender boost count";
    old_message->sender_boost_count = new_message->sender_boost_count;
    need_send_update = true;
  }
  if (old_message->forward_info != new_message->forward_info) {
    if (!replace_legacy && is_new_available &&
        MessageForwardInfo::need_change_warning(old_message->forward_info.get(), new_message->forward_info.get(),
                                                message_id)) {
      LOG(ERROR) << message_id << " in " << dialog_id << " sent by " << old_message->sender_user_id << "/"
                 << old_message->sender_dialog_id << " has changed forward info from " << old_message->forward_info
                 << " to " << new_message->forward_info << ", really forwarded from "
                 << old_message->real_forward_from_message_id << " in " << old_message->real_forward_from_dialog_id
                 << ", message content type is " << old_content_type << '/' << new_content_type;
    } else {
      LOG(DEBUG) << "Message forward info has changed from " << old_message->forward_info << " to "
                 << new_message->forward_info;
    }
    old_message->forward_info = std::move(new_message->forward_info);
    need_send_update = true;
  }
  if (old_message->had_forward_info != new_message->had_forward_info) {
    LOG(DEBUG) << "Message had_forward_info has changed from " << old_message->had_forward_info << " to "
               << new_message->had_forward_info;
    old_message->had_forward_info = new_message->had_forward_info;
  }
  if (old_message->saved_messages_topic_id != new_message->saved_messages_topic_id) {
    if (!(message_id.is_yet_unsent() && (old_message->saved_messages_topic_id.is_author_hidden() ||
                                         new_message->saved_messages_topic_id.is_author_hidden()))) {
      LOG(ERROR) << "Saved Messages topic for " << message_id << " in " << dialog_id << " changed from "
                 << old_message->saved_messages_topic_id << " to " << new_message->saved_messages_topic_id;
    }
    old_message->saved_messages_topic_id = new_message->saved_messages_topic_id;
    need_send_update = true;
  }
  if (old_message->notification_id != new_message->notification_id) {
    CHECK(!is_scheduled);
    if (old_message->notification_id.is_valid()) {
      if (new_message->notification_id.is_valid()) {
        LOG(ERROR) << "Notification identifier for " << message_id << " in " << dialog_id
                   << " has tried to change from " << old_message->notification_id << " to "
                   << new_message->notification_id << ", message content type is " << old_content_type << '/'
                   << new_content_type;
      }
    } else {
      CHECK(new_message->notification_id.is_valid());
      auto notification_info = add_dialog_notification_info(d);
      add_notification_id_to_message_id_correspondence(notification_info, new_message->notification_id, message_id);
      old_message->notification_id = new_message->notification_id;
    }
  }
  if (new_message->is_mention_notification_disabled) {
    // is_mention_notification_disabled must not be changed, because the message notification will not
    // be moved to the other group
    // old_message->is_mention_notification_disabled = true;
  }

  if (old_message->ttl_period != new_message->ttl_period) {
    if (old_message->ttl_period != 0 || !message_id.is_yet_unsent()) {
      LOG(ERROR) << message_id << " in " << dialog_id << " has changed auto-delete timer from "
                 << old_message->ttl_period << " to " << new_message->ttl_period;
    } else {
      LOG(DEBUG) << "Change message auto-delete timer";
      old_message->ttl_period = new_message->ttl_period;
      need_send_update = true;
    }
  }
  if (old_message->ttl != new_message->ttl && old_message->ttl_expires_at == 0) {
    if (message_id.is_yet_unsent() || replace_legacy) {
      LOG(INFO) << "Change message self-destruct time from " << old_message->ttl << " to " << new_message->ttl;
      old_message->ttl = new_message->ttl;
    } else {
      LOG(INFO) << "Ignore message self-destruct time change from " << old_message->ttl << " to " << new_message->ttl;
    }
  }

  const bool is_replied_message_info_changed = old_message->replied_message_info != new_message->replied_message_info;
  const bool is_top_thread_message_id_changed =
      old_message->top_thread_message_id != new_message->top_thread_message_id;
  const bool is_is_topic_message_changed = old_message->is_topic_message != new_message->is_topic_message;
  if (is_replied_message_info_changed || is_top_thread_message_id_changed || is_is_topic_message_changed ||
      old_message->reply_to_story_full_id != new_message->reply_to_story_full_id) {
    if (!replace_legacy && is_new_available) {
      if (is_replied_message_info_changed) {
        LOG(INFO) << "Update replied message of " << MessageFullId{dialog_id, message_id} << " from "
                  << old_message->replied_message_info << " to " << new_message->replied_message_info;
        auto is_reply_to_deleted_message = [&](const RepliedMessageInfo &replied_message_info) {
          auto reply_message_full_id = replied_message_info.get_reply_message_full_id(dialog_id, false);
          auto *d = get_dialog(reply_message_full_id.get_dialog_id());
          if (d == nullptr) {
            return false;
          }
          return is_deleted_message(d, reply_message_full_id.get_message_id());
        };
        if (RepliedMessageInfo::need_reply_changed_warning(
                td_, old_message->replied_message_info, new_message->replied_message_info,
                old_message->top_thread_message_id, message_id.is_yet_unsent() && !is_message_in_dialog,
                is_reply_to_deleted_message)) {
          const MessageInputReplyTo *message_input_reply_to = nullptr;
          if (!message_id.is_any_server()) {
            message_input_reply_to = get_message_input_reply_to(old_message);
          }
          LOG(ERROR) << message_id << " in " << dialog_id << " has changed replied message from "
                     << old_message->replied_message_info << '/' << message_input_reply_to << " to "
                     << new_message->replied_message_info << ", message content type is " << old_content_type << '/'
                     << new_content_type;
        }
      }
      if (is_top_thread_message_id_changed) {
        if ((new_message->top_thread_message_id != MessageId() && old_message->top_thread_message_id != MessageId()) ||
            is_message_in_dialog) {
          LOG(ERROR) << message_id << " in " << dialog_id << " has changed message thread from "
                     << old_message->top_thread_message_id << " to " << new_message->top_thread_message_id
                     << ", message content type is " << old_content_type << '/' << new_content_type;
        } else {
          LOG(INFO) << "Update message thread of " << MessageFullId{dialog_id, message_id} << " from "
                    << old_message->top_thread_message_id << " to " << new_message->top_thread_message_id;
        }
      }
      if (is_is_topic_message_changed) {
        if (!message_id.is_yet_unsent()) {
          LOG(ERROR) << message_id << " in " << dialog_id << " has changed is_topic_message to "
                     << new_message->is_topic_message;
        } else {
          LOG(INFO) << "Update is_topic_message of " << MessageFullId{dialog_id, message_id} << " from "
                    << old_message->is_topic_message << " to " << new_message->is_topic_message;
        }
      }
      if (old_message->reply_to_story_full_id != new_message->reply_to_story_full_id) {
        if (!message_id.is_yet_unsent() || new_message->reply_to_story_full_id.is_valid() || is_message_in_dialog) {
          LOG(ERROR) << message_id << " in " << dialog_id << " has changed replied story from "
                     << old_message->reply_to_story_full_id << " to " << new_message->reply_to_story_full_id
                     << ", message content type is " << old_content_type << '/' << new_content_type;
        } else {
          LOG(INFO) << "Update replied story of " << MessageFullId{dialog_id, message_id} << " from "
                    << old_message->reply_to_story_full_id << " to " << new_message->reply_to_story_full_id;
        }
      }
    }

    if ((is_top_thread_message_id_changed || is_is_topic_message_changed) && is_message_in_dialog &&
        old_message->is_topic_message && old_message->top_thread_message_id != MessageId()) {
      td_->forum_topic_manager_->on_topic_message_count_changed(dialog_id, old_message->top_thread_message_id, -1);
    }

    if (is_message_in_dialog) {
      unregister_message_reply(d->dialog_id, old_message);
    }
    old_message->replied_message_info = std::move(new_message->replied_message_info);
    old_message->reply_to_story_full_id = new_message->reply_to_story_full_id;
    old_message->top_thread_message_id = new_message->top_thread_message_id;
    old_message->is_topic_message = new_message->is_topic_message;
    old_message->reply_to_random_id = get_message_reply_to_random_id(d, old_message);

    if (is_message_in_dialog) {
      register_message_reply(d->dialog_id, old_message);
    }
    update_message_max_reply_media_timestamp(d, old_message, is_message_in_dialog);
    if ((is_top_thread_message_id_changed || is_is_topic_message_changed) && is_message_in_dialog &&
        old_message->is_topic_message && old_message->top_thread_message_id != MessageId()) {
      td_->forum_topic_manager_->on_topic_message_count_changed(dialog_id, old_message->top_thread_message_id, +1);
    }
    need_send_update = true;
  }
  if (old_message->via_bot_user_id != new_message->via_bot_user_id) {
    if ((!message_id.is_yet_unsent() || old_message->via_bot_user_id.is_valid()) && is_new_available &&
        !replace_legacy) {
      LOG(ERROR) << message_id << " in " << dialog_id << " has changed inline bot from " << old_message->via_bot_user_id
                 << " to " << new_message->via_bot_user_id << ", message content type is " << old_content_type << '/'
                 << new_content_type;
    }
    LOG(DEBUG) << "Change message via_bot 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;
    need_send_update = true;

    if (old_message->hide_via_bot && old_message->via_bot_user_id.is_valid()) {
      // wrongly set hide_via_bot
      old_message->hide_via_bot = false;
    }
  }
  if (old_message->is_outgoing != new_message->is_outgoing && is_new_available) {
    if (!replace_legacy && !(message_id.is_scheduled() && dialog_id == td_->dialog_manager_->get_my_dialog_id())) {
      LOG(ERROR) << message_id << " in " << dialog_id << " has changed is_outgoing from " << old_message->is_outgoing
                 << " to " << new_message->is_outgoing << ", message content type is " << old_content_type << '/'
                 << new_content_type;
      if (new_message->is_outgoing) {
        old_message->is_outgoing = new_message->is_outgoing;
        need_send_update = true;
      }
    } else {
      LOG(DEBUG) << "Message is_outgoing has changed from " << old_message->is_outgoing << " to "
                 << new_message->is_outgoing;
      old_message->is_outgoing = new_message->is_outgoing;
      need_send_update = 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 << ", message content type is " << old_content_type << '/'
      << new_content_type;
  if (old_message->contains_mention != new_message->contains_mention) {
    if (old_message->edit_date == 0 && is_new_available && new_content_type != MessageContentType::PinMessage &&
        !is_expired_message_content(new_content_type) && !replace_legacy) {
      LOG(ERROR) << message_id << " in " << dialog_id << " has changed contains_mention from "
                 << old_message->contains_mention << " to " << new_message->contains_mention
                 << ", is_outgoing = " << old_message->is_outgoing << ", message content type is " << old_content_type
                 << '/' << new_content_type;
    }
    // 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;
    // need_send_update = true;
  }
  if (old_message->disable_notification != new_message->disable_notification) {
    LOG_IF(ERROR, old_message->edit_date == 0 && is_new_available && !replace_legacy)
        << "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, "update_message"))
        << ". New message: " << to_string(get_message_object(dialog_id, new_message.get(), "update_message"));
    // disable_notification flag shouldn't be changed, because we are unable to show/hide message notification
    // old_message->disable_notification = new_message->disable_notification;
    // need_send_update = true;
  }

  if (old_message->noforwards != new_message->noforwards) {
    LOG(DEBUG) << "Message can_be_saved has changed from " << !old_message->noforwards << " to "
               << !old_message->noforwards;
    old_message->noforwards = new_message->noforwards;
    need_send_update = true;
  }
  if (old_message->restriction_reasons != new_message->restriction_reasons) {
    LOG(DEBUG) << "Message restriction_reasons have changed from " << old_message->restriction_reasons << " to "
               << old_message->restriction_reasons;
    old_message->restriction_reasons = std::move(new_message->restriction_reasons);
    need_send_update = true;
  }
  if (old_message->legacy_layer != new_message->legacy_layer) {
    old_message->legacy_layer = new_message->legacy_layer;
  }
  if (old_message->media_album_id != new_message->media_album_id &&
      (old_message->media_album_id == 0 || (new_message->media_album_id == 0 && message_id.is_yet_unsent()) ||
       td_->auth_manager_->is_bot())) {
    old_message->media_album_id = new_message->media_album_id;
    LOG(DEBUG) << "Update message media_album_id";
    need_send_update = true;
  }
  if (old_message->hide_edit_date != new_message->hide_edit_date) {
    old_message->hide_edit_date = new_message->hide_edit_date;
  }
  int32 new_shown_edit_date = old_message->hide_edit_date ? 0 : old_message->edit_date;
  if (new_shown_edit_date != old_shown_edit_date) {
    LOG(DEBUG) << "Message edit_date has changed";
    is_edited = true;
    need_send_update = true;
  }

  if (old_message->is_from_scheduled != new_message->is_from_scheduled) {
    // is_from_scheduled flag shouldn't be changed, because we are unable to show/hide message notification
    // old_message->is_from_scheduled = new_message->is_from_scheduled;
  }

  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());
      }
      LOG(DEBUG) << "Update message reply keyboard";
      old_message->reply_markup = std::move(new_message->reply_markup);
      is_edited = true;
      need_send_update = true;
    }
    old_message->had_reply_markup = false;
  } else {
    if (old_message->reply_markup == nullptr) {
      if (new_message->reply_markup != nullptr) {
        // MessageGame and MessageInvoice reply markup can be generated server side
        // some forwards retain their reply markup
        if (old_content_type != MessageContentType::Game && old_content_type != MessageContentType::Invoice &&
            need_message_changed_warning(old_message) && !replace_legacy) {
          LOG(ERROR) << message_id << " in " << dialog_id << " has received reply markup " << *new_message->reply_markup
                     << ", message content type is " << old_content_type << '/' << new_content_type;
        } else {
          LOG(DEBUG) << "Add message reply keyboard";
        }

        old_message->had_reply_markup = false;
        old_message->reply_markup = std::move(new_message->reply_markup);
        need_send_update = true;
      }
    } else {
      if (new_message->reply_markup != nullptr) {
        if (replace_legacy ||
            (message_id.is_yet_unsent() && old_message->reply_markup->type == ReplyMarkup::Type::InlineKeyboard &&
             new_message->reply_markup->type == ReplyMarkup::Type::InlineKeyboard)) {
          // allow the server to update inline keyboard for sent messages
          // this is needed to get correct button_id for UrlAuth buttons
          old_message->had_reply_markup = false;
          old_message->reply_markup = std::move(new_message->reply_markup);
          need_send_update = true;
        } else if (need_message_changed_warning(old_message) && is_new_available) {
          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 {
        // if the message is not accessible anymore, then we don't need a warning
        if (need_message_changed_warning(old_message) && is_new_available) {
          LOG(ERROR) << message_id << " in " << dialog_id << " sent by " << old_message->sender_user_id << "/"
                     << old_message->sender_dialog_id << " has lost reply markup " << *old_message->reply_markup
                     << ". Old message: " << to_string(get_message_object(dialog_id, old_message, "update_message"))
                     << ". New message: "
                     << to_string(get_message_object(dialog_id, new_message.get(), "update_message"));
        }
      }
    }
  }

  if (old_message->last_access_date < new_message->last_access_date) {
    old_message->last_access_date = new_message->last_access_date;
  }
  if (old_message->history_generation < new_message->history_generation) {
    old_message->history_generation = new_message->history_generation;
  }
  if (new_message->is_update_sent || is_edited) {
    old_message->is_update_sent = true;
  }

  if (!is_scheduled && update_message_is_pinned(d, old_message, new_message->is_pinned, "update_message")) {
    need_send_update = true;
  }
  if (!is_scheduled &&
      update_message_contains_unread_mention(d, old_message, new_message->contains_unread_mention, "update_message")) {
    need_send_update = true;
  }
  // update_message_interaction_info must be called after top_thread_message_id is updated
  if (update_message_interaction_info(d, old_message, new_message->view_count, new_message->forward_count, true,
                                      std::move(new_message->reply_info), true, std::move(new_message->reactions),
                                      "update_message")) {
    need_send_update = true;
  }

  bool is_preview_changed = false;
  if (old_message->invert_media != new_message->invert_media) {
    old_message->invert_media = new_message->invert_media;
    is_preview_changed = true;
  }
  if (old_message->disable_web_page_preview != new_message->disable_web_page_preview) {
    if (old_message->disable_web_page_preview) {
      is_preview_changed = has_message_content_web_page(new_message->content.get());
    } else {
      CHECK(new_message->disable_web_page_preview);
      is_preview_changed = has_message_content_web_page(old_message->content.get());
    }
    old_message->disable_web_page_preview = new_message->disable_web_page_preview;
  }

  bool is_content_changed = false;
  if (update_message_content(dialog_id, old_message, std::move(new_message->content),
                             message_id.is_yet_unsent() && new_message->edit_date == 0, is_message_in_dialog,
                             is_content_changed) ||
      is_preview_changed) {
    send_update_message_content(d, old_message, is_message_in_dialog, "update_message");
    need_send_update = true;
  }

  if (was_visible_message_reply_info && !is_visible_message_reply_info(dialog_id, old_message)) {
    send_update_message_interaction_info(dialog_id, old_message);
  }

  if (is_edited && !td_->auth_manager_->is_bot()) {
    send_update_message_edited(dialog_id, old_message);
  }

  if (!was_visible_message_reply_info && is_visible_message_reply_info(dialog_id, old_message)) {
    send_update_message_interaction_info(dialog_id, old_message);
  }

  on_message_changed(d, old_message, need_send_update, "update_message");
  return need_send_update;
}

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 || old_message->had_forward_info ||
       old_message->real_forward_from_dialog_id.is_valid())) {
    // original message may be edited
    return false;
  }
  if (old_message->ttl.is_valid()) {
    // message can expire
    return false;
  }
  if (!old_message->restriction_reasons.empty()) {
    return false;
  }
  return true;
}

bool MessagesManager::update_message_content(DialogId dialog_id, Message *old_message,
                                             unique_ptr<MessageContent> new_content, bool need_merge_files,
                                             bool is_message_in_dialog, bool &is_content_changed) {
  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_any_file_id(old_content.get());
  bool need_finish_upload = old_file_id.is_valid() && need_merge_files;
  if (old_content_type != new_content_type) {
    if (old_message->ttl.is_valid() && old_message->ttl_expires_at > 0 &&
        ((new_content_type == MessageContentType::ExpiredPhoto && old_content_type == MessageContentType::Photo) ||
         (new_content_type == MessageContentType::ExpiredVideo && old_content_type == MessageContentType::Video) ||
         (new_content_type == MessageContentType::ExpiredVideoNote &&
          old_content_type == MessageContentType::VideoNote) ||
         (new_content_type == MessageContentType::ExpiredVoiceNote &&
          old_content_type == MessageContentType::VoiceNote))) {
      LOG(INFO) << "Do not apply expired message content early";
    } else {
      need_update = true;
      LOG(INFO) << "Message content has changed type from " << old_content_type << " to " << new_content_type;

      old_message->is_content_secret = old_message->ttl.is_secret_message_content(new_content->get_type());
    }

    if (need_merge_files && old_file_id.is_valid()) {
      auto new_file_id = get_message_content_any_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();

          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);
    compare_message_contents(td_, old_content.get(), new_content.get(), 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
    cancel_upload_file(old_file_id, "update_message_content");
  }

  if (is_content_changed || need_update) {
    if (is_message_in_dialog) {
      reregister_message_content(td_, old_content.get(), new_content.get(), {dialog_id, old_message->message_id},
                                 "update_message_content");
    }
    old_content = std::move(new_content);
    old_message->last_edit_pts = 0;
    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_any_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) {
    auto file_ids = get_message_content_file_ids(old_content.get(), td_);
    if (!file_ids.empty()) {
      auto file_source_id = get_message_file_source_id(MessageFullId(dialog_id, old_message->message_id));
      if (file_source_id.is_valid()) {
        auto search_text = get_message_search_text(old_message);
        for (auto file_id : file_ids) {
          auto file_view = td_->file_manager_->get_file_view(file_id);
          send_closure(td_->download_manager_actor_, &DownloadManager::change_search_text, file_view.get_main_file_id(),
                       file_source_id, search_text);
        }
      }
    }
  }
  return need_update;
}

MessagesManager::Dialog *MessagesManager::get_dialog_by_message_id(MessageId message_id) {
  CHECK(message_id.is_valid() && message_id.is_server());
  auto dialog_id = message_id_to_dialog_id_.get(message_id);
  if (dialog_id == DialogId()) {
    if (G()->use_message_database()) {
      auto r_value =
          G()->td_db()->get_message_db_sync()->get_message_by_unique_message_id(message_id.get_server_message_id());
      if (r_value.is_ok()) {
        Message *m = on_get_message_from_database(r_value.ok(), false, "get_dialog_by_message_id");
        if (m != nullptr) {
          dialog_id = r_value.ok().dialog_id;
          CHECK(m->message_id == message_id);
          LOG_CHECK(message_id_to_dialog_id_.get(message_id) == dialog_id)
              << message_id << ' ' << dialog_id << ' ' << message_id_to_dialog_id_.get(message_id) << ' '
              << m->debug_source;
          Dialog *d = get_dialog(dialog_id);
          CHECK(d != nullptr);
          return d;
        }
      }
    }

    LOG(INFO) << "Can't find the chat by " << message_id;
    return nullptr;
  }

  return get_dialog(dialog_id);
}

MessageId MessagesManager::get_message_id_by_random_id(Dialog *d, int64 random_id, const char *source) {
  CHECK(d != nullptr);
  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()->use_message_database() && d->dialog_id.get_type() == DialogType::SecretChat && random_id != 0) {
      auto r_value = G()->td_db()->get_message_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, r_value.ok(), false, "get_message_id_by_random_id");
        if (m != nullptr) {
          LOG_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 << " "
              << get_message(d, m->message_id) << " " << m << " " << debug_add_message_to_dialog_fail_reason_;
          LOG_CHECK(d->random_id_to_message_id.count(random_id))
              << source << " " << random_id << " " << m->message_id << " " << m->is_failed_to_send << " "
              << m->is_outgoing << " " << get_message(d, m->message_id) << " " << m << " "
              << debug_add_message_to_dialog_fail_reason_;
          LOG_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 << " " << get_message(d, m->message_id) << " "
              << m << " " << debug_add_message_to_dialog_fail_reason_;
          LOG(INFO) << "Found " << MessageFullId{d->dialog_id, m->message_id} << " by random_id " << random_id
                    << " from " << source;
          return m->message_id;
        }
      }
    }

    LOG(INFO) << "Found no message by random_id " << random_id << " from " << source;
    return MessageId();
  }

  LOG(INFO) << "Found " << MessageFullId{d->dialog_id, it->second} << " by random_id " << random_id << " from "
            << source;
  return it->second;
}

void MessagesManager::try_update_dialog_pos(DialogId dialog_id) {
  Dialog *d = get_dialog(dialog_id);
  if (d != nullptr && d->is_update_new_chat_sent) {
    update_dialog_pos(d, "try_update_dialog_pos");
  }
}

void MessagesManager::force_create_dialog(DialogId dialog_id, const char *source, bool expect_no_access,
                                          bool force_update_dialog_pos) {
  LOG_CHECK(dialog_id.is_valid()) << source;
  LOG_CHECK(is_inited_) << dialog_id << ' ' << source << ' ' << expect_no_access << ' ' << force_update_dialog_pos;
  Dialog *d = get_dialog_force(dialog_id, source);
  if (d == nullptr) {
    LOG(INFO) << "Force create " << dialog_id << " from " << source;
    if (loaded_dialogs_.count(dialog_id) > 0) {
      LOG(INFO) << "Skip creation of " << dialog_id << ", because it is being loaded now";
      return;
    }

    d = add_dialog(dialog_id, source);
    update_dialog_pos(d, source);

    if (dialog_id.get_type() == DialogType::SecretChat && !d->notification_settings.is_synchronized &&
        td_->contacts_manager_->get_secret_chat_state(dialog_id.get_secret_chat_id()) != SecretChatState::Closed) {
      // secret chat is being created
      // let's copy notification settings from main chat if available
      VLOG(notifications) << "Create new secret " << dialog_id << " from " << source;
      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), source);
        if (user_d != nullptr && user_d->notification_settings.is_synchronized) {
          VLOG(notifications) << "Copy notification settings from " << user_d->dialog_id << " to " << dialog_id;
          auto user_settings = &user_d->notification_settings;
          auto new_notification_settings = DialogNotificationSettings(
              user_settings->use_default_mute_until, user_settings->mute_until,
              dup_notification_sound(user_settings->sound), true /*use_default_show_preview*/, false /*show_preview*/,
              user_settings->use_default_mute_stories, user_settings->mute_stories,
              dup_notification_sound(user_settings->story_sound), user_settings->use_default_hide_story_sender,
              user_settings->hide_story_sender, user_settings->silent_send_message, true, false, true, false);
          new_notification_settings.is_secret_chat_show_preview_fixed = true;
          update_dialog_notification_settings(dialog_id, &d->notification_settings,
                                              std::move(new_notification_settings));
        } else {
          d->notification_settings.is_synchronized = true;
        }
      }

      if (G()->use_message_database() && !td_->auth_manager_->is_bot() &&
          !td_->contacts_manager_->get_secret_chat_is_outbound(secret_chat_id)) {
        auto notification_info = add_dialog_notification_info(d);
        auto notification_group_id =
            get_dialog_notification_group_id(dialog_id, notification_info->message_notification_group_);
        if (notification_group_id.is_valid()) {
          if (notification_info->new_secret_chat_notification_id_.is_valid()) {
            LOG(ERROR) << "Found previously created " << notification_info->new_secret_chat_notification_id_ << " in "
                       << d->dialog_id << ", when creating it from " << source;
          } else {
            notification_info->new_secret_chat_notification_id_ =
                get_next_notification_id(notification_info, notification_group_id, MessageId());
            if (notification_info->new_secret_chat_notification_id_.is_valid()) {
              auto date = td_->contacts_manager_->get_secret_chat_date(secret_chat_id);
              set_dialog_last_notification_checked(dialog_id, notification_info->message_notification_group_, date,
                                                   notification_info->new_secret_chat_notification_id_,
                                                   "add_new_secret_chat");
              VLOG(notifications) << "Create " << notification_info->new_secret_chat_notification_id_ << " with "
                                  << secret_chat_id;
              auto ringtone_id = get_dialog_notification_ringtone_id(dialog_id, d);
              send_closure_later(G()->notification_manager(), &NotificationManager::add_notification,
                                 notification_group_id, NotificationGroupType::SecretChat, dialog_id, date, dialog_id,
                                 false, ringtone_id, 0, notification_info->new_secret_chat_notification_id_,
                                 create_new_secret_chat_notification(), "add_new_secret_chat_notification");
            }
          }
        }
      }
    }
    if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
      if (!td_->dialog_manager_->have_dialog_info(dialog_id)) {
        if (expect_no_access && dialog_id.get_type() == DialogType::Channel &&
            td_->contacts_manager_->have_min_channel(dialog_id.get_channel_id())) {
          LOG(INFO) << "Created " << dialog_id << " for min-channel from " << source;
        } else {
          LOG(ERROR) << "Forced to create unknown " << dialog_id << " from " << source;
        }
      } else if (!expect_no_access) {
        LOG(ERROR) << "Have no access to " << dialog_id << " received from " << source << ", but forced to create it";
      }
    }
  } else if (force_update_dialog_pos) {
    update_dialog_pos(d, "force update chat position");
  }
}

MessagesManager::Dialog *MessagesManager::add_dialog_for_new_message(DialogId dialog_id, bool have_last_message,
                                                                     bool *need_update_dialog_pos, const char *source) {
  if (have_last_message) {
    CHECK(!being_added_by_new_message_dialog_id_.is_valid());
    being_added_by_new_message_dialog_id_ = dialog_id;
  }
  auto *d = add_dialog(dialog_id, source);
  CHECK(d != nullptr);
  being_added_by_new_message_dialog_id_ = DialogId();
  *need_update_dialog_pos = true;
  return d;
}

MessagesManager::Dialog *MessagesManager::add_dialog(DialogId dialog_id, const char *source) {
  LOG(DEBUG) << "Creating " << dialog_id << " from " << source;
  CHECK(!have_dialog(dialog_id));
  LOG_CHECK(dialog_id.is_valid()) << source;

  if (G()->use_message_database() && failed_to_load_dialogs_.count(dialog_id) == 0) {
    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 from " << source;
      return add_new_dialog(parse_dialog(dialog_id, r_value.ok(), source), true, source);
    }
  }

  auto dialog = make_unique<Dialog>();
  dialog->dialog_id = dialog_id;
  invalidate_message_indexes(dialog.get());

  return add_new_dialog(std::move(dialog), false, source);
}

MessagesManager::Dialog *MessagesManager::add_new_dialog(unique_ptr<Dialog> &&dialog, bool is_loaded_from_database,
                                                         const char *source) {
  Dialog *d = dialog.get();
  auto dialog_id = d->dialog_id;
  LOG_CHECK(is_inited_) << dialog_id << ' ' << is_loaded_from_database << ' ' << source;
  LOG_CHECK(!have_dialog(dialog_id)) << dialog_id << ' ' << is_loaded_from_database << ' ' << source;
  switch (dialog_id.get_type()) {
    case DialogType::User:
      if (dialog_id == td_->dialog_manager_->get_my_dialog_id() && d->last_read_inbox_message_id == MessageId::max() &&
          d->last_read_outbox_message_id == MessageId::max()) {
        d->last_read_inbox_message_id = d->last_new_message_id;
        d->last_read_outbox_message_id = d->last_new_message_id;
      }
      d->has_bots = dialog_id.get_user_id() != ContactsManager::get_replies_bot_user_id() &&
                    td_->contacts_manager_->is_user_bot(dialog_id.get_user_id());
      d->is_has_bots_inited = true;
      d->is_available_reactions_inited = true;
      d->is_view_as_messages_inited = true;
      if (!d->is_saved_messages_view_as_messages_inited && dialog_id == td_->dialog_manager_->get_my_dialog_id()) {
        d->is_saved_messages_view_as_messages_inited = true;
        d->view_as_messages = true;
      }
      break;
    case DialogType::Chat:
      d->is_is_blocked_inited = true;
      d->is_is_blocked_for_stories_inited = true;
      d->is_background_inited = true;
      d->is_view_as_messages_inited = true;
      break;
    case DialogType::Channel: {
      if (td_->contacts_manager_->is_broadcast_channel(dialog_id.get_channel_id())) {
        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;
      }
      break;
    }
    case DialogType::SecretChat:
      if (d->last_new_message_id.get() <= MessageId::min().get()) {
        LOG(INFO) << "Set " << dialog_id << " last new message in add_new_dialog from " << source;
        d->last_new_message_id = MessageId::min().get_next_message_id(MessageType::Local);
      }

      if (!d->notification_settings.is_secret_chat_show_preview_fixed) {
        d->notification_settings.use_default_show_preview = true;
        d->notification_settings.show_preview = false;
        d->notification_settings.is_secret_chat_show_preview_fixed = true;
        on_dialog_updated(dialog_id, "fix secret chat show preview");
      }

      d->have_full_history = true;
      d->have_full_history_source = 4;
      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->is_last_pinned_message_id_inited = true;
      d->is_background_inited = true;
      d->is_theme_name_inited = true;
      d->is_is_blocked_inited = true;
      d->is_is_blocked_for_stories_inited = true;
      d->is_view_as_messages_inited = true;
      if (!d->is_folder_id_inited && !td_->auth_manager_->is_bot()) {
        do_set_dialog_folder_id(
            d, td_->contacts_manager_->get_secret_chat_initial_folder_id(dialog_id.get_secret_chat_id()));
      }
      d->message_ttl = MessageTtl(td_->contacts_manager_->get_secret_chat_ttl(dialog_id.get_secret_chat_id()));
      d->is_message_ttl_inited = true;
      d->has_bots = td_->contacts_manager_->is_user_bot(
          td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id()));
      d->is_has_bots_inited = true;
      d->is_available_reactions_inited = true;
      d->has_loaded_scheduled_messages_from_database = true;

      break;
    case DialogType::None:
    default:
      UNREACHABLE();
  }
  if (!is_loaded_from_database) {
    on_dialog_updated(dialog_id, "add_new_dialog");
  }
  if (td_->auth_manager_->is_bot()) {
    d->notification_settings.is_synchronized = true;
  }
  if (is_channel_difference_finished_.erase(dialog_id)) {
    d->is_channel_difference_finished = true;
  }

  auto draft_message = std::move(d->draft_message);
  d->draft_message = nullptr;
  unique_ptr<Message> last_database_message;
  if (!d->messages.empty()) {
    d->messages.foreach([&](const MessageId &message_id, unique_ptr<Message> &message) {
      CHECK(last_database_message == nullptr);
      last_database_message = std::move(message);
    });
    d->messages = {};
  }
  auto last_database_message_id = d->last_database_message_id;
  d->last_database_message_id = MessageId();
  if (td_->auth_manager_->is_bot()) {
    draft_message = nullptr;
    last_database_message = nullptr;
    d->first_database_message_id = MessageId();
    last_database_message_id = MessageId();

    d->reply_markup_message_id = MessageId();
    d->last_assigned_message_id = MessageId(ServerMessageId(1));
    d->last_new_message_id = MessageId();
    d->last_clear_history_date = 0;
    d->last_clear_history_message_id = MessageId();
    d->order = DEFAULT_ORDER;
  }
  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();
  DialogId default_join_group_call_as_dialog_id = d->default_join_group_call_as_dialog_id;
  if (default_join_group_call_as_dialog_id != dialog_id &&
      default_join_group_call_as_dialog_id.get_type() != DialogType::User &&
      !have_dialog(default_join_group_call_as_dialog_id)) {
    d->default_join_group_call_as_dialog_id = DialogId();
  }
  DialogId default_send_message_as_dialog_id = d->default_send_message_as_dialog_id;
  bool need_drop_default_send_message_as_dialog_id = d->need_drop_default_send_message_as_dialog_id;
  if (default_send_message_as_dialog_id != dialog_id &&
      default_send_message_as_dialog_id.get_type() != DialogType::User &&
      !have_dialog(default_send_message_as_dialog_id)) {
    d->need_drop_default_send_message_as_dialog_id = false;
    d->default_send_message_as_dialog_id = DialogId();
  }

  if (d->notification_info != nullptr) {
    if (d->notification_info->message_notification_group_.is_valid()) {
      notification_group_id_to_dialog_id_.emplace(d->notification_info->message_notification_group_.get_group_id(),
                                                  dialog_id);
    }
    if (d->notification_info->mention_notification_group_.is_valid()) {
      notification_group_id_to_dialog_id_.emplace(d->notification_info->mention_notification_group_.get_group_id(),
                                                  dialog_id);
    }
  }
  if (pending_dialog_group_call_updates_.count(dialog_id) > 0) {
    auto it = pending_dialog_group_call_updates_.find(dialog_id);
    bool has_active_group_call = it->second.first;
    bool is_group_call_empty = it->second.second;
    pending_dialog_group_call_updates_.erase(it);
    if (d->has_active_group_call != has_active_group_call || d->is_group_call_empty != is_group_call_empty) {
      if (!has_active_group_call) {
        d->active_group_call_id = InputGroupCallId();
      }
      d->has_active_group_call = has_active_group_call;
      d->is_group_call_empty = is_group_call_empty;
      on_dialog_updated(dialog_id, "pending update_dialog_group_call");
    }
  }
  fix_pending_join_requests(dialog_id, d->pending_join_request_count, d->pending_join_request_user_ids);

  if (!is_loaded_from_database) {
    CHECK(order == DEFAULT_ORDER);
    CHECK(draft_message == nullptr);
    CHECK(last_database_message == nullptr);
  }

  CHECK(!have_dialog(dialog_id));
  dialogs_.set(dialog_id, std::move(dialog));

  CHECK(!being_added_new_dialog_id_.is_valid());
  being_added_new_dialog_id_ = dialog_id;

  loaded_dialogs_.erase(dialog_id);
  failed_to_load_dialogs_.erase(dialog_id);

  fix_dialog_action_bar(d, d->action_bar.get());

  send_update_new_chat(d);

  being_added_new_dialog_id_ = DialogId();

  CHECK(d->messages.empty());

  fix_new_dialog(d, std::move(draft_message), std::move(last_database_message), last_database_message_id, order,
                 last_clear_history_date, last_clear_history_message_id, default_join_group_call_as_dialog_id,
                 default_send_message_as_dialog_id, need_drop_default_send_message_as_dialog_id,
                 is_loaded_from_database, source);

  return d;
}

void MessagesManager::fix_new_dialog(Dialog *d, unique_ptr<DraftMessage> &&draft_message,
                                     unique_ptr<Message> &&last_database_message, MessageId last_database_message_id,
                                     int64 order, int32 last_clear_history_date,
                                     MessageId last_clear_history_message_id,
                                     DialogId default_join_group_call_as_dialog_id,
                                     DialogId default_send_message_as_dialog_id,
                                     bool need_drop_default_send_message_as_dialog_id, bool is_loaded_from_database,
                                     const char *source) {
  CHECK(d != nullptr);
  auto dialog_id = d->dialog_id;
  auto dialog_type = dialog_id.get_type();

  if (!td_->auth_manager_->is_bot() && dialog_type == DialogType::SecretChat) {
    auto user_id = td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id());
    if (user_id.is_valid()) {
      force_create_dialog(DialogId(user_id), "add chat with user to load/store action_bar and is_blocked");

      Dialog *user_d = get_dialog_force(DialogId(user_id), "fix_new_dialog");
      if (user_d != nullptr &&
          (d->is_blocked != user_d->is_blocked || d->is_blocked_for_stories != user_d->is_blocked_for_stories)) {
        set_dialog_is_blocked(d, user_d->is_blocked, user_d->is_blocked_for_stories);
      }
    }
  }

  if (d->is_empty && !d->have_full_history) {
    LOG(ERROR) << "Drop invalid flag is_empty";
    d->is_empty = false;
  }

  if (being_added_dialog_id_ != dialog_id && !td_->auth_manager_->is_bot() && !is_dialog_inited(d) &&
      dialog_type != DialogType::SecretChat && td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    // asynchronously get dialog from the server
    send_get_dialog_query(dialog_id, Auto(), 0, "fix_new_dialog 20");
  }

  if (being_added_dialog_id_ != dialog_id && !td_->auth_manager_->is_bot()) {
    const char *reload_source = nullptr;
    if (!d->is_is_blocked_for_stories_inited) {
      reload_source = "fix_new_dialog init is_blocked_for_stories";
    } else if (!d->is_has_bots_inited) {
      reload_source = "fix_new_dialog init has_bots";
    } else if (!d->is_background_inited) {
      reload_source = "fix_new_dialog init background";
    } else if (!d->is_theme_name_inited) {
      reload_source = "fix_new_dialog init theme_name";
    } else if (!d->is_available_reactions_inited) {
      reload_source = "fix_new_dialog init available_reactions";
    } else if (!d->is_folder_id_inited && order != DEFAULT_ORDER) {
      reload_source = "fix_new_dialog init folder_id";
    } else if (!d->is_message_ttl_inited && td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Write)) {
      reload_source = "fix_new_dialog init message_auto_delete_time";
    } else if (!d->is_view_as_messages_inited && td_->dialog_manager_->is_forum_channel(dialog_id)) {
      reload_source = "fix_new_dialog init view_as_messages";
    } else if (!d->is_last_pinned_message_id_inited) {
      get_dialog_pinned_message(dialog_id, Auto());
    }
    if (reload_source != nullptr) {
      // asynchronously get dialog info from the server
      td_->dialog_manager_->reload_dialog_info_full(dialog_id, reload_source);
    }
  }
  if ((!d->know_action_bar || d->need_repair_action_bar) && !td_->auth_manager_->is_bot() &&
      dialog_type != DialogType::SecretChat && dialog_id != td_->dialog_manager_->get_my_dialog_id() &&
      td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    // asynchronously get action bar from the server
    reget_dialog_action_bar(dialog_id, "fix_new_dialog", false);
  }
  if (d->has_active_group_call && !d->active_group_call_id.is_valid() && !td_->auth_manager_->is_bot()) {
    repair_dialog_active_group_call_id(dialog_id);
  }
  set_dialog_is_forum(d, td_->dialog_manager_->is_forum_channel(dialog_id));

  if (d->notification_settings.is_synchronized && !d->notification_settings.is_use_default_fixed &&
      td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read) && !td_->auth_manager_->is_bot()) {
    LOG(INFO) << "Reget notification settings of " << dialog_id;
    if (dialog_type == DialogType::SecretChat) {
      d->notification_settings.is_use_default_fixed = true;
      on_dialog_updated(dialog_id, "reget notification settings");
    } else {
      td_->notification_settings_manager_->send_get_dialog_notification_settings_query(dialog_id, MessageId(),
                                                                                       Promise<Unit>());
    }
  }
  if (td_->auth_manager_->is_bot() || d->notification_settings.use_default_mute_until ||
      d->notification_settings.mute_until <= G()->unix_time()) {
    d->notification_settings.mute_until = 0;
  } else {
    schedule_dialog_unmute(dialog_id, false, d->notification_settings.mute_until, G()->unix_time());
  }
  if (d->notification_info != nullptr && d->notification_info->pinned_message_notification_message_id_.is_valid()) {
    auto pinned_message_id = d->notification_info->pinned_message_notification_message_id_;
    if (!d->notification_info->mention_notification_group_.is_valid()) {
      LOG(ERROR) << "Have pinned message notification in " << pinned_message_id << " in " << dialog_id
                 << ", but there is no mention notification group";
      d->notification_info->pinned_message_notification_message_id_ = MessageId();
      on_dialog_updated(dialog_id, "fix pinned message notification");
    } else if (is_dialog_pinned_message_notifications_disabled(d) ||
               pinned_message_id <= d->last_read_inbox_message_id ||
               d->notification_info->mention_notification_group_.is_removed_object_id(pinned_message_id)) {
      VLOG(notifications) << "Remove disabled pinned message notification in " << pinned_message_id << " in "
                          << dialog_id;
      send_closure_later(G()->notification_manager(), &NotificationManager::remove_temporary_notification_by_object_id,
                         d->notification_info->mention_notification_group_.get_group_id(), pinned_message_id, true,
                         "fix pinned message notification");
      d->notification_info->pinned_message_notification_message_id_ = MessageId();
      on_dialog_updated(dialog_id, "fix pinned message notification 2");
    }
  }

  {
    auto it = pending_add_dialog_dependent_dialogs_.find(dialog_id);
    if (it != pending_add_dialog_dependent_dialogs_.end()) {
      auto pending_dialog_ids = std::move(it->second);
      pending_add_dialog_dependent_dialogs_.erase(it);

      for (auto &pending_dialog_id : pending_dialog_ids) {
        auto &add_dialog_data = pending_add_dialog_data_[pending_dialog_id];
        CHECK(add_dialog_data.dependent_dialog_count_ > 0);
        add_dialog_data.dependent_dialog_count_--;
        if (add_dialog_data.dependent_dialog_count_ == 0) {
          LOG(INFO) << "Add postponed last database message in " << pending_dialog_id;
          add_pending_dialog_data(get_dialog(pending_dialog_id), std::move(add_dialog_data.last_message_),
                                  std::move(add_dialog_data.draft_message_));
          pending_add_dialog_data_.erase(pending_dialog_id);
        }
      }
    }
  }

  {
    auto it = pending_add_default_join_group_call_as_dialog_id_.find(dialog_id);
    if (it != pending_add_default_join_group_call_as_dialog_id_.end()) {
      auto pending_dialog_ids = std::move(it->second);
      pending_add_default_join_group_call_as_dialog_id_.erase(it);

      for (auto &pending_dialog_id : pending_dialog_ids) {
        on_update_dialog_default_join_group_call_as_dialog_id(pending_dialog_id, dialog_id, false);
      }
    }
  }

  {
    auto it = pending_add_default_send_message_as_dialog_id_.find(dialog_id);
    if (it != pending_add_default_send_message_as_dialog_id_.end()) {
      auto pending_dialog_ids = std::move(it->second);
      pending_add_default_send_message_as_dialog_id_.erase(it);

      for (auto &pending_dialog_id : pending_dialog_ids) {
        Dialog *pending_d = get_dialog(pending_dialog_id.first);
        CHECK(pending_d != nullptr);
        if (!pending_d->default_send_message_as_dialog_id.is_valid()) {
          LOG(INFO) << "Set postponed message sender in " << pending_dialog_id << " to " << dialog_id;
          pending_d->need_drop_default_send_message_as_dialog_id = pending_dialog_id.second;
          pending_d->default_send_message_as_dialog_id = dialog_id;
          send_update_chat_message_sender(pending_d);
        }
      }
    }
  }

  if (dialog_id != being_added_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_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 = d->last_new_message_id.get_prev_server_message_id();
    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 (!d->last_new_message_id.is_valid() && d->last_new_message_id != MessageId()) {
    LOG(ERROR) << "Drop invalid last_new_message_id " << d->last_new_message_id << " in " << dialog_id;
    d->last_new_message_id = MessageId();
  }
  if (last_message_id.is_valid()) {
    if ((last_message_id.is_server() || dialog_type == DialogType::SecretChat) && !last_message_id.is_yet_unsent() &&
        !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 > last_message_id) {
      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 <= d->last_new_message_id) {
      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 || draft_message != nullptr) {
    Dependencies dependencies;
    if (last_database_message != nullptr) {
      add_message_dependencies(dependencies, last_database_message.get());
    }
    if (draft_message != nullptr) {
      draft_message->add_dependencies(dependencies);
    }

    int32 dependent_dialog_count = 0;
    for (const auto &other_dialog_id : dependencies.get_dialog_ids()) {
      if (other_dialog_id.is_valid() && !have_dialog(other_dialog_id)) {
        LOG(INFO) << "Postpone adding of data in " << dialog_id << " because of cyclic dependency with "
                  << other_dialog_id;
        pending_add_dialog_dependent_dialogs_[other_dialog_id].push_back(dialog_id);
        dependent_dialog_count++;
      }
    };

    auto pending_order = DEFAULT_ORDER;
    if (last_database_message != nullptr) {
      auto last_message_order = get_dialog_order(last_message_id, last_database_message->date);
      if (last_message_order > pending_order) {
        pending_order = last_message_order;
      }
    }
    if (draft_message != nullptr && !need_hide_dialog_draft_message(d)) {
      int64 draft_order = get_dialog_order(MessageId(), draft_message->get_date());
      if (draft_order > pending_order) {
        pending_order = draft_order;
      }
    }
    if (dependent_dialog_count == 0) {
      if (!add_pending_dialog_data(d, std::move(last_database_message), std::move(draft_message))) {
        // failed to add last message; keep the current position and get history from the database
        d->pending_order = pending_order;
      }
    } else {
      // can't add data immediately, because need to notify first about adding of dependent dialogs
      d->pending_order = pending_order;
      pending_add_dialog_data_[dialog_id] = {dependent_dialog_count, std::move(last_database_message),
                                             std::move(draft_message)};
    }
  } else if (last_database_message_id.is_valid()) {
    auto date = DialogDate(order, dialog_id).get_date();
    if (date < MIN_PINNED_DIALOG_DATE) {
      d->pending_order = get_dialog_order(last_message_id, date);
    }
  }

  if (default_join_group_call_as_dialog_id != d->default_join_group_call_as_dialog_id) {
    CHECK(default_join_group_call_as_dialog_id.is_valid());
    CHECK(default_join_group_call_as_dialog_id.get_type() != DialogType::User);
    CHECK(!d->default_join_group_call_as_dialog_id.is_valid());
    if (!have_dialog(default_join_group_call_as_dialog_id)) {
      LOG(INFO) << "Postpone adding of default join voice chat as " << default_join_group_call_as_dialog_id << " in "
                << dialog_id;
      pending_add_default_join_group_call_as_dialog_id_[default_join_group_call_as_dialog_id].push_back(dialog_id);
    } else {
      on_update_dialog_default_join_group_call_as_dialog_id(dialog_id, default_join_group_call_as_dialog_id, false);
    }
  }

  if (default_send_message_as_dialog_id != d->default_send_message_as_dialog_id) {
    CHECK(default_send_message_as_dialog_id.is_valid());
    CHECK(default_send_message_as_dialog_id.get_type() != DialogType::User);
    CHECK(!d->default_send_message_as_dialog_id.is_valid());
    if (!have_dialog(default_send_message_as_dialog_id)) {
      LOG(INFO) << "Postpone setting of message sender " << default_send_message_as_dialog_id << " in " << dialog_id;
      pending_add_default_send_message_as_dialog_id_[default_send_message_as_dialog_id].emplace_back(
          dialog_id, need_drop_default_send_message_as_dialog_id);
    } else {
      LOG(INFO) << "Set message sender in " << dialog_id << " to " << default_send_message_as_dialog_id;
      d->need_drop_default_send_message_as_dialog_id = need_drop_default_send_message_as_dialog_id;
      d->default_send_message_as_dialog_id = default_send_message_as_dialog_id;
      send_update_chat_message_sender(d);
    }
  }

  switch (dialog_type) {
    case DialogType::User:
      break;
    case DialogType::Chat:
      if (d->last_read_inbox_message_id < d->last_read_outbox_message_id) {
        LOG(INFO) << "Last read outbox message is " << 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 identifier not less than an identifier of last read outgoing message and less than
        // an identifier of first unread outgoing message, so it may not point to the outgoing message
        // read_history_inbox(d, 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();
  }

  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 (!G()->use_message_database()) {
    d->has_loaded_scheduled_messages_from_database = true;
  }

  if (dialog_id != being_added_dialog_id_) {
    update_dialog_pos(d, "fix_new_dialog 7", true, is_loaded_from_database);
  }
  if (is_loaded_from_database && d->order != order && order < MAX_ORDINARY_DIALOG_ORDER &&
      !td_->dialog_manager_->is_dialog_info_received_from_server(dialog_id) && !d->had_last_yet_unsent_message &&
      !td_->auth_manager_->is_bot()) {
    LOG(ERROR) << dialog_id << " has order " << d->order << " instead of saved to database order " << order;
  }

  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 << " with order " << d->order;
  if (d->notification_info != nullptr) {
    VLOG(notifications) << "Have " << dialog_id << " with message " << d->notification_info->message_notification_group_
                        << " and new secret chat " << d->notification_info->new_secret_chat_notification_id_;
    VLOG(notifications) << "Have " << dialog_id << " with mention " << d->notification_info->mention_notification_group_
                        << " and pinned " << d->notification_info->pinned_message_notification_message_id_;
    VLOG(notifications) << "In " << dialog_id << " have last_read_inbox_message_id = " << d->last_read_inbox_message_id
                        << ", last_new_message_id = " << d->last_new_message_id
                        << ", max_push_notification_message_id = "
                        << d->notification_info->max_push_notification_message_id_;
  }

  LOG_CHECK(d->messages.calc_size() <= 1)
      << d->messages.calc_size() << ' ' << last_message_id << ' ' << d->last_message_id << ' '
      << d->last_database_message_id << ' ' << d->debug_set_dialog_last_database_message_id << ' '
      << is_loaded_from_database << ' ' << source << ' ' << being_added_dialog_id_ << ' ' << being_added_new_dialog_id_
      << ' ' << dialog_id << ' ' << d->is_channel_difference_finished << ' '
      << debug_last_get_channel_difference_dialog_id_ << ' ' << debug_last_get_channel_difference_source_ << ' '
      << G()->use_message_database();

  // must be after update_dialog_pos, because uses d->order
  // must be after checks that dialog has at most one message, because read_history_inbox can load
  // pinned message to remove notification about it
  if (d->pending_read_channel_inbox_pts != 0 && !td_->auth_manager_->is_bot() &&
      td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read) && need_unread_counter(d->order)) {
    if (d->pts == d->pending_read_channel_inbox_pts) {
      d->pending_read_channel_inbox_pts = 0;
      read_history_inbox(d, d->pending_read_channel_inbox_max_message_id,
                         d->pending_read_channel_inbox_server_unread_count, "fix_new_dialog 12");
      on_dialog_updated(dialog_id, "fix_new_dialog 13");
    } else if (d->pts > d->pending_read_channel_inbox_pts) {
      d->need_repair_channel_server_unread_count = true;
      d->pending_read_channel_inbox_pts = 0;
      on_dialog_updated(dialog_id, "fix_new_dialog 14");
    } else {
      schedule_get_channel_difference(dialog_id, d->pending_read_channel_inbox_pts, MessageId(), 0.001, source);
    }
  } else {
    d->pending_read_channel_inbox_pts = 0;
  }
  if (need_get_history && !td_->auth_manager_->is_bot() && dialog_id != being_added_dialog_id_ &&
      dialog_id != being_added_by_new_message_dialog_id_ && (d->order != DEFAULT_ORDER || is_dialog_sponsored(d))) {
    load_last_dialog_message(d, "fix_new_dialog");
  }
  if (d->need_repair_server_unread_count && need_unread_counter(d->order)) {
    CHECK(dialog_type != DialogType::SecretChat);
    repair_server_unread_count(dialog_id, d->server_unread_count, "fix_new_dialog");
  }
  if (dialog_type == DialogType::Channel && need_unread_counter(d->order) && d->server_unread_count > 0 &&
      !td_->auth_manager_->is_bot() && td_->option_manager_->get_option_integer("since_last_open") >= 2 * 86400) {
    d->need_repair_channel_server_unread_count = true;
  }
  if (d->need_repair_channel_server_unread_count) {
    repair_channel_server_unread_count(d);
  }
  if (d->need_repair_unread_reaction_count) {
    repair_dialog_unread_reaction_count(d, Promise<Unit>(), "fix_new_dialog");
  }
  if (d->need_repair_unread_mention_count) {
    repair_dialog_unread_mention_count(d, "fix_new_dialog");
  }
}

bool MessagesManager::add_pending_dialog_data(Dialog *d, unique_ptr<Message> &&last_database_message,
                                              unique_ptr<DraftMessage> &&draft_message) {
  CHECK(d != nullptr);
  CHECK(last_database_message != nullptr || draft_message != nullptr);
  CHECK(!td_->auth_manager_->is_bot());

  bool need_update_dialog_pos = false;
  bool was_added_last_message = false;
  if (last_database_message != nullptr) {
    auto dialog_id = d->dialog_id;
    auto message_id = last_database_message->message_id;
    CHECK(message_id.is_valid());
    LOG_CHECK(d->last_database_message_id == message_id)
        << message_id << " " << d->last_database_message_id << " " << d->debug_set_dialog_last_database_message_id;

    const Message *m = nullptr;
    if (td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
      bool need_update = false;
      m = add_message_to_dialog(d, std::move(last_database_message), true, false, &need_update, &need_update_dialog_pos,
                                "add_pending_dialog_data 1");
      if (need_update_dialog_pos) {
        LOG(ERROR) << "Need to update pos in " << dialog_id;
      }
    }
    if (m != nullptr) {
      set_dialog_last_message_id(d, m->message_id, "add_pending_dialog_data 2", m);
      send_update_chat_last_message(d, "add_pending_dialog_data 3");
      was_added_last_message = true;
    } else {
      on_dialog_updated(dialog_id, "add_pending_dialog_data 4");  // resave without last database message

      if (!td_->auth_manager_->is_bot() && dialog_id != being_added_dialog_id_ &&
          dialog_id != being_added_by_new_message_dialog_id_ && (d->order != DEFAULT_ORDER || is_dialog_sponsored(d))) {
        load_last_dialog_message(d, "add_pending_dialog_data 5");
      }
    }
  }
  if (draft_message != nullptr) {
    d->draft_message = std::move(draft_message);
    need_update_dialog_pos = true;
    send_update_chat_draft_message(d);
  }
  if (d->pending_order != DEFAULT_ORDER) {
    d->pending_order = DEFAULT_ORDER;
    need_update_dialog_pos = true;
  }

  if (need_update_dialog_pos) {
    update_dialog_pos(d, "add_pending_dialog_data 6");
  }
  return was_added_last_message;
}

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(), td_->dialog_manager_->get_dialog_search_text(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(), -get_dialog_base_order(d));
  }
}

int64 MessagesManager::get_dialog_order(MessageId message_id, int32 message_date) {
  CHECK(!message_id.is_scheduled());
  return (static_cast<int64>(message_date) << 32) +
         message_id.get_prev_server_message_id().get_server_message_id().get();
}

bool MessagesManager::is_dialog_sponsored(const Dialog *d) const {
  return d->order == DEFAULT_ORDER && d->dialog_id == sponsored_dialog_id_;
}

int64 MessagesManager::get_dialog_base_order(const Dialog *d) const {
  if (td_->auth_manager_->is_bot()) {
    return 0;  // to not call get_dialog_list
  }
  if (is_dialog_sponsored(d)) {
    return SPONSORED_DIALOG_ORDER;
  }
  if (d->order == DEFAULT_ORDER) {
    return 0;
  }
  auto pinned_order = get_dialog_pinned_order(DialogListId(FolderId::main()), d->dialog_id);
  if (pinned_order != DEFAULT_ORDER) {
    return pinned_order;
  }
  return d->order;
}

int64 MessagesManager::get_dialog_private_order(const DialogList *list, const Dialog *d) const {
  if (list == nullptr || td_->auth_manager_->is_bot()) {
    return 0;
  }

  if (is_dialog_sponsored(d) && list->dialog_list_id == DialogListId(FolderId::main())) {
    return SPONSORED_DIALOG_ORDER;
  }
  if (d->order == DEFAULT_ORDER) {
    return 0;
  }
  auto pinned_order = get_dialog_pinned_order(list, d->dialog_id);
  if (pinned_order != DEFAULT_ORDER) {
    return pinned_order;
  }
  return d->order;
}

td_api::object_ptr<td_api::chatPosition> MessagesManager::get_chat_position_object(DialogListId dialog_list_id,
                                                                                   const Dialog *d) const {
  if (td_->auth_manager_->is_bot()) {
    return nullptr;
  }

  auto *list = get_dialog_list(dialog_list_id);
  if (list == nullptr) {
    return nullptr;
  }

  auto position = get_dialog_position_in_list(list, d);
  if (position.public_order == 0) {
    return nullptr;
  }

  auto chat_source = position.is_sponsored ? sponsored_dialog_source_.get_chat_source_object() : nullptr;
  return td_api::make_object<td_api::chatPosition>(dialog_list_id.get_chat_list_object(), position.public_order,
                                                   position.is_pinned, std::move(chat_source));
}

vector<td_api::object_ptr<td_api::chatPosition>> MessagesManager::get_chat_positions_object(const Dialog *d) const {
  vector<td_api::object_ptr<td_api::chatPosition>> positions;
  if (!td_->auth_manager_->is_bot()) {
    for (auto dialog_list_id : get_dialog_list_ids(d)) {
      auto position = get_chat_position_object(dialog_list_id, d);
      if (position != nullptr) {
        positions.push_back(std::move(position));
      }
    }
    if (is_dialog_sponsored(d)) {
      CHECK(positions.empty());
      positions.push_back(get_chat_position_object(DialogListId(FolderId::main()), d));
    }
  }
  return positions;
}

int64 MessagesManager::get_next_pinned_dialog_order() {
  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, const char *source, bool need_send_update,
                                        bool is_loaded_from_database) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  LOG(INFO) << "Trying to update " << d->dialog_id << " order from " << source;

  int64 new_order = DEFAULT_ORDER;
  if (!td_->dialog_manager_->is_dialog_removed_from_dialog_list(d->dialog_id)) {
    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_order != DEFAULT_ORDER) {
      LOG(INFO) << "Pending order " << d->pending_order << " found";
      if (d->pending_order > new_order) {
        new_order = d->pending_order;
      }
    }
    if (d->draft_message != nullptr && !need_hide_dialog_draft_message(d)) {
      auto draft_message_date = d->draft_message->get_date();
      LOG(INFO) << "Draft message at " << draft_message_date << " found";
      int64 draft_order = get_dialog_order(MessageId(), draft_message_date);
      if (draft_order > new_order) {
        new_order = draft_order;
      }
    }
    switch (d->dialog_id.get_type()) {
      case DialogType::Chat: {
        auto chat_id = d->dialog_id.get_chat_id();
        auto date = td_->contacts_manager_->get_chat_date(chat_id);
        LOG(INFO) << "Creation at " << date << " found";
        int64 join_order = get_dialog_order(MessageId(), date);
        if (join_order > new_order && td_->contacts_manager_->get_chat_status(chat_id).is_member()) {
          new_order = join_order;
        }
        break;
      }
      case DialogType::Channel: {
        auto date = td_->contacts_manager_->get_channel_date(d->dialog_id.get_channel_id());
        LOG(INFO) << "Join at " << date << " found";
        int64 join_order = get_dialog_order(MessageId(), date);
        if (join_order > new_order) {
          new_order = join_order;
        }
        break;
      }
      case DialogType::SecretChat: {
        auto date = td_->contacts_manager_->get_secret_chat_date(d->dialog_id.get_secret_chat_id());
        if (date != 0 && !is_deleted_secret_chat(d)) {
          LOG(INFO) << "Creation at " << date << " found";
          int64 creation_order = get_dialog_order(MessageId(), date);
          if (creation_order > new_order) {
            new_order = creation_order;
          }
        }
        break;
      }
      default:
        break;
    }
    if (new_order == DEFAULT_ORDER && !d->is_empty) {
      LOG(INFO) << "There are no known messages in the chat, just leave it where it is";
      new_order = d->order;
      load_last_dialog_message(d, source);
    }
  }

  if (set_dialog_order(d, new_order, need_send_update, 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, bool is_loaded_from_database,
                                       const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return false;
  }

  CHECK(d != nullptr);
  DialogId dialog_id = d->dialog_id;
  DialogDate old_date(d->order, dialog_id);
  DialogDate new_date(new_order, dialog_id);

  if (old_date == new_date) {
    LOG(INFO) << "Order of " << d->dialog_id << " from " << d->folder_id << " is still " << new_order << " from "
              << source;
  } else {
    LOG(INFO) << "Update order of " << dialog_id << " from " << d->folder_id << " from " << d->order << " to "
              << new_order << " from " << source;
  }

  auto folder_ptr = get_dialog_folder(d->folder_id);
  LOG_CHECK(folder_ptr != nullptr) << is_inited_ << ' ' << create_folders_source_ << ' ' << G()->close_flag() << ' '
                                   << dialog_id << ' ' << d->folder_id << ' ' << is_loaded_from_database << ' '
                                   << td_->auth_manager_->is_authorized() << ' ' << td_->auth_manager_->was_authorized()
                                   << ' ' << source;
  auto &folder = *folder_ptr;
  if (old_date == new_date) {
    if (new_order == DEFAULT_ORDER) {
      // first addition of a new left dialog
      if (folder.ordered_dialogs_.insert(new_date).second) {
        for (const auto &dialog_list : dialog_lists_) {
          if (get_dialog_pinned_order(&dialog_list.second, d->dialog_id) != DEFAULT_ORDER) {
            set_dialog_is_pinned(dialog_list.first, d, false);
          }
        }
      }
    }

    return false;
  }

  auto dialog_positions = get_dialog_positions(d);

  if (folder.ordered_dialogs_.erase(old_date) == 0) {
    LOG_IF(ERROR, d->order != DEFAULT_ORDER) << dialog_id << " not found in the chat list from " << source;
  }

  folder.ordered_dialogs_.insert(new_date);

  bool is_added = (d->order == DEFAULT_ORDER);
  bool is_removed = (new_order == DEFAULT_ORDER);

  d->order = new_order;

  if (is_added) {
    update_dialogs_hints(d);
  }
  update_dialogs_hints_rating(d);

  update_dialog_lists(d, std::move(dialog_positions), need_send_update, is_loaded_from_database, source);

  if (!is_loaded_from_database) {
    auto dialog_type = dialog_id.get_type();
    if (dialog_type == DialogType::Channel && is_added && being_added_dialog_id_ != dialog_id) {
      repair_channel_server_unread_count(d);
      schedule_get_channel_difference(dialog_id, 0, MessageId(), 0.001, source);
    }
    if (dialog_type == DialogType::Channel && is_removed) {
      remove_all_dialog_notifications(d, false, source);
      remove_all_dialog_notifications(d, true, source);
      td_->dialog_action_manager_->clear_active_dialog_actions(dialog_id);
    }
  }

  return true;
}

void MessagesManager::update_dialog_lists(
    Dialog *d, std::unordered_map<DialogListId, DialogPositionInList, DialogListIdHash> &&old_positions,
    bool need_send_update, bool is_loaded_from_database, const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(d != nullptr);
  auto dialog_id = d->dialog_id;
  if (being_added_dialog_id_ == dialog_id) {
    // do not try to update dialog lists, while the dialog isn't inited
    return;
  }

  LOG(INFO) << "Update lists of " << dialog_id << " from " << source;

  if (d->order == DEFAULT_ORDER) {
    for (auto &old_position : old_positions) {
      if (old_position.second.is_pinned) {
        set_dialog_is_pinned(old_position.first, d, false, false);
      }
    }

    if (d->folder_id != FolderId::main()) {
      LOG(INFO) << "Change folder of " << dialog_id << " to " << FolderId::main();
      DialogDate dialog_date(d->order, dialog_id);
      get_dialog_folder(d->folder_id)->ordered_dialogs_.erase(dialog_date);
      do_set_dialog_folder_id(d, FolderId::main());
      get_dialog_folder(d->folder_id)->ordered_dialogs_.insert(dialog_date);
    }
  }

  for (auto &dialog_list : dialog_lists_) {
    auto dialog_list_id = dialog_list.first;
    auto &list = dialog_list.second;

    const DialogPositionInList &old_position = old_positions[dialog_list_id];
    const DialogPositionInList new_position = get_dialog_position_in_list(&list, d, true);

    // sponsored chat is never "in list"
    bool was_in_list = old_position.order != DEFAULT_ORDER && old_position.private_order != 0;
    bool is_in_list = new_position.order != DEFAULT_ORDER && new_position.private_order != 0;
    CHECK(was_in_list == is_dialog_in_list(d, dialog_list_id));

    LOG(DEBUG) << "Update position of " << dialog_id << " in " << dialog_list_id << " from " << old_position << " to "
               << new_position;

    bool need_update_unread_chat_count = false;
    if (was_in_list != is_in_list) {
      const int32 delta = was_in_list ? -1 : 1;
      list.in_memory_dialog_total_count_ += delta;
      if (!is_loaded_from_database) {
        int32 &total_count = dialog_id.get_type() == DialogType::SecretChat ? list.secret_chat_total_count_
                                                                            : list.server_dialog_total_count_;
        if (total_count != -1) {
          total_count += delta;
          if (total_count < 0) {
            LOG(ERROR) << "Total chat count in " << dialog_list_id << " became negative after removing " << dialog_id;
            total_count = 0;
          }
        }
      }

      if (!is_loaded_from_database) {
        need_update_unread_chat_count =
            list.is_dialog_unread_count_inited_ && old_position.total_dialog_count != get_dialog_total_count(list);
        auto unread_count = d->server_unread_count + d->local_unread_count;
        const char *change_source = was_in_list ? "on_dialog_remove" : "on_dialog_add";
        if (unread_count != 0 && list.is_message_unread_count_inited_) {
          unread_count *= delta;

          list.unread_message_total_count_ += unread_count;
          if (is_dialog_muted(d)) {
            list.unread_message_muted_count_ += unread_count;
          }
          send_update_unread_message_count(list, dialog_id, true, change_source);
        }
        if ((unread_count != 0 || d->is_marked_as_unread) && list.is_dialog_unread_count_inited_) {
          list.unread_dialog_total_count_ += delta;
          if (unread_count == 0 && d->is_marked_as_unread) {
            list.unread_dialog_marked_count_ += delta;
          }
          if (is_dialog_muted(d)) {
            list.unread_dialog_muted_count_ += delta;
            if (unread_count == 0 && d->is_marked_as_unread) {
              list.unread_dialog_muted_marked_count_ += delta;
            }
          }
          need_update_unread_chat_count = true;
        }
        if (need_update_unread_chat_count) {
          send_update_unread_chat_count(list, dialog_id, true, change_source);
        }
      }

      if (was_in_list) {
        remove_dialog_from_list(d, dialog_list_id);
      } else {
        add_dialog_to_list(d, dialog_list_id);
      }
    }
    if (!need_update_unread_chat_count && list.is_dialog_unread_count_inited_ &&
        old_position.total_dialog_count != get_dialog_total_count(list)) {
      send_update_unread_chat_count(list, dialog_id, true, "changed total count");
    }

    if (need_send_update && need_send_update_chat_position(old_position, new_position)) {
      send_update_chat_position(dialog_list_id, d, source);
    }

    if (!is_loaded_from_database && !old_position.is_sponsored && new_position.is_sponsored) {
      // a chat is sponsored only if user isn't a chat member
      remove_all_dialog_notifications(d, false, "update_dialog_lists 3");
      remove_all_dialog_notifications(d, true, "update_dialog_lists 4");
    }
  }
}

void MessagesManager::update_last_dialog_date(FolderId folder_id) {
  CHECK(!td_->auth_manager_->is_bot());
  auto *folder = get_dialog_folder(folder_id);
  CHECK(folder != nullptr);
  auto old_last_dialog_date = folder->folder_last_dialog_date_;
  folder->folder_last_dialog_date_ = folder->last_server_dialog_date_;
  CHECK(old_last_dialog_date <= folder->folder_last_dialog_date_);

  LOG(INFO) << "Update last dialog date in " << folder_id << " from " << old_last_dialog_date << " to "
            << folder->folder_last_dialog_date_;
  LOG(INFO) << "Know about " << folder->ordered_dialogs_.size() << " chats";

  if (old_last_dialog_date != folder->folder_last_dialog_date_) {
    for (auto &dialog_list : dialog_lists_) {
      update_list_last_pinned_dialog_date(dialog_list.second);
      update_list_last_dialog_date(dialog_list.second);
    }
  }

  if (G()->use_message_database() && folder->last_database_server_dialog_date_ < folder->last_server_dialog_date_) {
    auto last_server_dialog_date_string = PSTRING() << folder->last_server_dialog_date_.get_order() << ' '
                                                    << folder->last_server_dialog_date_.get_dialog_id().get();
    G()->td_db()->get_binlog_pmc()->set(PSTRING() << "last_server_dialog_date" << folder_id.get(),
                                        last_server_dialog_date_string);
    LOG(INFO) << "Save last server dialog date " << folder->last_server_dialog_date_;
    folder->last_database_server_dialog_date_ = folder->last_server_dialog_date_;
    folder->last_loaded_database_dialog_date_ = folder->last_server_dialog_date_;
  }
}

// must not call get_dialog_filter
bool MessagesManager::do_update_list_last_pinned_dialog_date(DialogList &list) const {
  CHECK(!td_->auth_manager_->is_bot());
  if (list.last_pinned_dialog_date_ == MAX_DIALOG_DATE) {
    return false;
  }
  if (!list.are_pinned_dialogs_inited_) {
    return false;
  }

  DialogDate max_dialog_date = MIN_DIALOG_DATE;
  for (const auto &pinned_dialog : list.pinned_dialogs_) {
    if (!have_dialog(pinned_dialog.get_dialog_id())) {
      break;
    }

    max_dialog_date = pinned_dialog;
  }
  if (list.pinned_dialogs_.empty() || max_dialog_date == list.pinned_dialogs_.back()) {
    max_dialog_date = MAX_DIALOG_DATE;
  }
  if (list.last_pinned_dialog_date_ < max_dialog_date) {
    LOG(INFO) << "Update last pinned dialog date in " << list.dialog_list_id << " from "
              << list.last_pinned_dialog_date_ << " to " << max_dialog_date;
    list.last_pinned_dialog_date_ = max_dialog_date;
    return true;
  }
  return false;
}

void MessagesManager::update_list_last_pinned_dialog_date(DialogList &list) {
  CHECK(!td_->auth_manager_->is_bot());
  if (do_update_list_last_pinned_dialog_date(list)) {
    update_list_last_dialog_date(list);
  }
}

// must not call get_dialog_filter
bool MessagesManager::do_update_list_last_dialog_date(DialogList &list, const vector<FolderId> &folder_ids) const {
  CHECK(!td_->auth_manager_->is_bot());
  auto new_last_dialog_date = list.last_pinned_dialog_date_;
  for (auto folder_id : folder_ids) {
    const auto &folder = *get_dialog_folder(folder_id);
    if (folder.folder_last_dialog_date_ < new_last_dialog_date) {
      new_last_dialog_date = folder.folder_last_dialog_date_;
    }
  }

  if (list.list_last_dialog_date_ != new_last_dialog_date) {
    auto old_last_dialog_date = list.list_last_dialog_date_;
    LOG(INFO) << "Update last dialog date in " << list.dialog_list_id << " from " << old_last_dialog_date << " to "
              << new_last_dialog_date;
    LOG_CHECK(old_last_dialog_date < new_last_dialog_date)
        << list.dialog_list_id << " " << old_last_dialog_date << " " << new_last_dialog_date << " "
        << get_dialog_list_folder_ids(list) << " " << list.last_pinned_dialog_date_ << " "
        << get_dialog_folder(FolderId::main())->folder_last_dialog_date_ << " "
        << get_dialog_folder(FolderId::archive())->folder_last_dialog_date_ << " " << list.load_list_queries_.size()
        << " " << list.pinned_dialogs_;
    list.list_last_dialog_date_ = new_last_dialog_date;
    return true;
  }
  return false;
}

void MessagesManager::update_list_last_dialog_date(DialogList &list) {
  CHECK(!td_->auth_manager_->is_bot());
  auto old_dialog_total_count = get_dialog_total_count(list);
  auto old_last_dialog_date = list.list_last_dialog_date_;
  if (!do_update_list_last_dialog_date(list, get_dialog_list_folder_ids(list))) {
    LOG(INFO) << "Don't need to update last dialog date in " << list.dialog_list_id;
    return;
  }

  for (auto it = std::upper_bound(list.pinned_dialogs_.begin(), list.pinned_dialogs_.end(), old_last_dialog_date);
       it != list.pinned_dialogs_.end() && *it <= list.list_last_dialog_date_; ++it) {
    auto dialog_id = it->get_dialog_id();
    auto d = get_dialog(dialog_id);
    CHECK(d != nullptr);
    send_update_chat_position(list.dialog_list_id, d, "update_list_last_dialog_date");
  }

  bool is_list_further_loaded = list.list_last_dialog_date_ == MAX_DIALOG_DATE;
  for (auto folder_id : get_dialog_list_folder_ids(list)) {
    const auto &folder = *get_dialog_folder(folder_id);
    for (auto it = folder.ordered_dialogs_.upper_bound(old_last_dialog_date);
         it != folder.ordered_dialogs_.end() && *it <= folder.folder_last_dialog_date_; ++it) {
      if (it->get_order() == DEFAULT_ORDER) {
        break;
      }
      auto dialog_id = it->get_dialog_id();
      if (get_dialog_pinned_order(&list, dialog_id) == DEFAULT_ORDER) {
        auto d = get_dialog(dialog_id);
        CHECK(d != nullptr);
        if (is_dialog_in_list(d, list.dialog_list_id)) {
          send_update_chat_position(list.dialog_list_id, d, "update_list_last_dialog_date 2");
          is_list_further_loaded = true;
        }
      }
    }
  }

  if (list.list_last_dialog_date_ == MAX_DIALOG_DATE) {
    recalc_unread_count(list.dialog_list_id, old_dialog_total_count, true);
  }

  LOG(INFO) << "After updating last dialog date in " << list.dialog_list_id << " to " << list.list_last_dialog_date_
            << " have is_list_further_loaded == " << is_list_further_loaded << " and " << list.load_list_queries_.size()
            << " pending load list queries";
  if (is_list_further_loaded && !list.load_list_queries_.empty()) {
    set_promises(list.load_list_queries_);
  }
}

MessagesManager::Dialog *MessagesManager::get_dialog(DialogId dialog_id) {
  return dialogs_.get_pointer(dialog_id);
}

const MessagesManager::Dialog *MessagesManager::get_dialog(DialogId dialog_id) const {
  return dialogs_.get_pointer(dialog_id);
}

bool MessagesManager::have_dialog_force(DialogId dialog_id, const char *source) {
  return loaded_dialogs_.count(dialog_id) > 0 || get_dialog_force(dialog_id, source) != nullptr;
}

MessagesManager::Dialog *MessagesManager::get_dialog_force(DialogId dialog_id, const char *source) {
  init();

  Dialog *d = get_dialog(dialog_id);
  if (d != nullptr) {
    LOG_CHECK(d->dialog_id == dialog_id) << d->dialog_id << ' ' << dialog_id;
    return d;
  }

  if (!dialog_id.is_valid() || !G()->use_message_database() || loaded_dialogs_.count(dialog_id) > 0 ||
      failed_to_load_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 from " << source;
    d = on_load_dialog_from_database(dialog_id, r_value.move_as_ok(), source);
    LOG_CHECK(d == nullptr || d->dialog_id == dialog_id) << d->dialog_id << ' ' << dialog_id;
  } else {
    failed_to_load_dialogs_.insert(dialog_id);
    LOG(INFO) << "Failed to load " << dialog_id << " from database from " << source << ": "
              << r_value.error().message();
  }
  return d;
}

unique_ptr<MessagesManager::Dialog> MessagesManager::parse_dialog(DialogId dialog_id, const BufferSlice &value,
                                                                  const char *source) {
  LOG(INFO) << "Loaded " << dialog_id << " of size " << value.size() << " from database from " << source;
  CHECK(dialog_id.is_valid());
  auto dialog = make_unique<Dialog>();
  Dialog *d = dialog.get();
  d->dialog_id = dialog_id;
  invalidate_message_indexes(d);  // must initialize indexes, because some of them could be not parsed

  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
    LOG_CHECK(dialog_id.is_valid()) << "Can't repair " << dialog_id << ' ' << d->dialog_id << ' ' << status << ' '
                                    << source << ' ' << format::as_hex_dump<4>(value.as_slice());

    LOG(ERROR) << "Repair broken " << dialog_id << ": " << status << ' ' << format::as_hex_dump<4>(value.as_slice());

    // just clean all known data about the dialog
    dialog = make_unique<Dialog>();
    d = dialog.get();
    d->dialog_id = dialog_id;
    invalidate_message_indexes(d);

    // and try to reget it from the server if possible
    td_->dialog_manager_->have_dialog_info_force(dialog_id, "parse_dialog");
    if (td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
      if (dialog_id.get_type() != DialogType::SecretChat) {
        send_get_dialog_query(dialog_id, Auto(), 0, source);
      }
    } else {
      LOG(ERROR) << "Can't repair unknown " << dialog_id << " from " << source;
    }
  }
  CHECK(dialog_id == d->dialog_id);

  Dependencies dependencies;
  dependencies.add_dialog_dependencies(dialog_id);
  if (d->default_join_group_call_as_dialog_id != dialog_id) {
    dependencies.add_message_sender_dependencies(d->default_join_group_call_as_dialog_id);
  }
  if (d->default_send_message_as_dialog_id != dialog_id) {
    dependencies.add_message_sender_dependencies(d->default_send_message_as_dialog_id);
  }
  d->messages.foreach([&](const MessageId &message_id, const unique_ptr<Message> &message) {
    add_message_dependencies(dependencies, message.get());
  });
  add_draft_message_dependencies(dependencies, d->draft_message);
  for (auto user_id : d->pending_join_request_user_ids) {
    dependencies.add(user_id);
  }
  if (!dependencies.resolve_force(td_, source)) {
    send_get_dialog_query(dialog_id, Auto(), 0, source);
  }

  if (td_->auth_manager_->is_bot()) {
    if (d->unread_mention_count > 0) {
      set_dialog_unread_mention_count(d, 0);
    }
    if (d->unread_reaction_count > 0) {
      set_dialog_unread_reaction_count(d, 0);
    }
  }

  auto dialog_type = d->dialog_id.get_type();
  switch (dialog_type) {
    case DialogType::Chat:
    case DialogType::Channel:
      if (get_active_reactions(d->available_reactions).empty() != ((d->available_reactions_generation & 1) == 1)) {
        set_dialog_next_available_reactions_generation(d, d->available_reactions_generation);
      }
      break;
    case DialogType::User:
    case DialogType::SecretChat:
    default:
      break;
  }
  if (!d->need_drop_default_send_message_as_dialog_id && d->default_send_message_as_dialog_id.is_valid() &&
      dialog_type == DialogType::Channel && !td_->contacts_manager_->is_channel_public(dialog_id.get_channel_id()) &&
      !td_->contacts_manager_->get_channel_has_linked_channel(dialog_id.get_channel_id())) {
    LOG(INFO) << "Drop message sender in " << dialog_id;
    d->need_drop_default_send_message_as_dialog_id = true;
  }

  return dialog;
}

MessagesManager::Dialog *MessagesManager::on_load_dialog_from_database(DialogId dialog_id, BufferSlice &&value,
                                                                       const char *source) {
  CHECK(G()->use_message_database());

  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;
  }

  LOG(INFO) << "Add new " << dialog_id << " from database from " << source;
  return add_new_dialog(parse_dialog(dialog_id, value, source), true, source);
}

vector<FolderId> MessagesManager::get_dialog_list_folder_ids(const DialogList &list) const {
  CHECK(!td_->auth_manager_->is_bot());
  if (list.dialog_list_id.is_folder()) {
    return {list.dialog_list_id.get_folder_id()};
  }
  if (list.dialog_list_id.is_filter()) {
    return td_->dialog_filter_manager_->get_dialog_filter_folder_ids(list.dialog_list_id.get_filter_id());
  }
  UNREACHABLE();
  return {};
}

bool MessagesManager::has_dialogs_from_folder(const DialogList &list, const DialogFolder &folder) const {
  CHECK(!td_->auth_manager_->is_bot());
  if (list.dialog_list_id.is_folder()) {
    return list.dialog_list_id.get_folder_id() == folder.folder_id;
  }
  if (list.dialog_list_id.is_filter()) {
    return td::contains(td_->dialog_filter_manager_->get_dialog_filter_folder_ids(list.dialog_list_id.get_filter_id()),
                        folder.folder_id);
  }
  UNREACHABLE();
  return false;
}

bool MessagesManager::is_dialog_in_list(const Dialog *d, DialogListId dialog_list_id) {
  return td::contains(d->dialog_list_ids, dialog_list_id);
}

void MessagesManager::add_dialog_to_list(Dialog *d, DialogListId dialog_list_id) {
  LOG(INFO) << "Add " << d->dialog_id << " to " << dialog_list_id;
  CHECK(!is_dialog_in_list(d, dialog_list_id));
  d->dialog_list_ids.push_back(dialog_list_id);
  CHECK(d->is_update_new_chat_sent);
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatAddedToList>(
                   get_chat_id_object(d->dialog_id, "add_dialog_to_list"), dialog_list_id.get_chat_list_object()));
}

void MessagesManager::remove_dialog_from_list(Dialog *d, DialogListId dialog_list_id) {
  LOG(INFO) << "Remove " << d->dialog_id << " from " << dialog_list_id;
  bool is_removed = td::remove(d->dialog_list_ids, dialog_list_id);
  CHECK(is_removed);
  CHECK(d->is_update_new_chat_sent);
  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updateChatRemovedFromList>(
                   get_chat_id_object(d->dialog_id, "remove_dialog_from_list"), dialog_list_id.get_chat_list_object()));
}

DialogFilterDialogInfo MessagesManager::get_dialog_info_for_dialog_filter(const Dialog *d) const {
  CHECK(d != nullptr);
  CHECK(d->order != DEFAULT_ORDER);
  DialogFilterDialogInfo dialog_info;
  dialog_info.dialog_id_ = d->dialog_id;
  dialog_info.folder_id_ = d->folder_id;
  dialog_info.has_unread_mentions_ = d->unread_mention_count != 0 && !is_dialog_mention_notifications_disabled(d);
  dialog_info.is_muted_ = is_dialog_muted(d);
  dialog_info.has_unread_messages_ = d->server_unread_count + d->local_unread_count != 0 || d->is_marked_as_unread;
  return dialog_info;
}

bool MessagesManager::is_dialog_in_dialog_list(DialogId dialog_id) const {
  CHECK(!td_->auth_manager_->is_bot());
  const Dialog *d = get_dialog(dialog_id);
  return d != nullptr && d->order != DEFAULT_ORDER;
}

bool MessagesManager::need_dialog_in_list(const Dialog *d, const DialogList &list) const {
  CHECK(!td_->auth_manager_->is_bot());
  if (d->order == DEFAULT_ORDER) {
    return false;
  }
  if (list.dialog_list_id.is_folder()) {
    return d->folder_id == list.dialog_list_id.get_folder_id();
  }
  if (list.dialog_list_id.is_filter()) {
    return td_->dialog_filter_manager_->need_dialog_in_filter(list.dialog_list_id.get_filter_id(),
                                                              get_dialog_info_for_dialog_filter(d));
  }
  UNREACHABLE();
  return false;
}

bool MessagesManager::need_send_update_chat_position(const DialogPositionInList &old_position,
                                                     const DialogPositionInList &new_position) {
  if (old_position.public_order != new_position.public_order) {
    return true;
  }
  if (old_position.public_order == 0) {
    return false;
  }
  return old_position.is_pinned != new_position.is_pinned || old_position.is_sponsored != new_position.is_sponsored;
}

MessagesManager::DialogPositionInList MessagesManager::get_dialog_position_in_list(const DialogList *list,
                                                                                   const Dialog *d, bool actual) const {
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(list != nullptr);
  CHECK(d != nullptr);

  DialogPositionInList position;
  position.order = d->order;
  if (is_dialog_sponsored(d) || (actual ? need_dialog_in_list(d, *list) : is_dialog_in_list(d, list->dialog_list_id))) {
    position.private_order = get_dialog_private_order(list, d);
  }
  if (position.private_order != 0) {
    position.public_order =
        DialogDate(position.private_order, d->dialog_id) <= list->list_last_dialog_date_ ? position.private_order : 0;
    position.is_pinned = get_dialog_pinned_order(list, d->dialog_id) != DEFAULT_ORDER;
    position.is_sponsored = is_dialog_sponsored(d);
  }
  position.total_dialog_count = get_dialog_total_count(*list);
  return position;
}

std::unordered_map<DialogListId, MessagesManager::DialogPositionInList, DialogListIdHash>
MessagesManager::get_dialog_positions(const Dialog *d) const {
  CHECK(d != nullptr);
  std::unordered_map<DialogListId, MessagesManager::DialogPositionInList, DialogListIdHash> positions;
  if (!td_->auth_manager_->is_bot()) {
    for (const auto &dialog_list : dialog_lists_) {
      positions.emplace(dialog_list.first, get_dialog_position_in_list(&dialog_list.second, d));
    }
  }
  return positions;
}

vector<DialogListId> MessagesManager::get_dialog_list_ids(const Dialog *d) {
  return d->dialog_list_ids;
}

MessagesManager::DialogListView MessagesManager::get_dialog_lists(const Dialog *d) {
  return DialogListView(this, get_dialog_list_ids(d));
}

MessagesManager::DialogList &MessagesManager::add_dialog_list(DialogListId dialog_list_id) {
  CHECK(!td_->auth_manager_->is_bot());
  if (dialog_lists_.count(dialog_list_id) == 0) {
    LOG(INFO) << "Create " << dialog_list_id;
  }
  auto &list = dialog_lists_[dialog_list_id];
  list.dialog_list_id = dialog_list_id;
  return list;
}

MessagesManager::DialogList *MessagesManager::get_dialog_list(DialogListId dialog_list_id) {
  CHECK(!td_->auth_manager_->is_bot());
  auto it = dialog_lists_.find(dialog_list_id);
  if (it == dialog_lists_.end()) {
    return nullptr;
  }
  return &it->second;
}

const MessagesManager::DialogList *MessagesManager::get_dialog_list(DialogListId dialog_list_id) const {
  CHECK(!td_->auth_manager_->is_bot());
  auto it = dialog_lists_.find(dialog_list_id);
  if (it == dialog_lists_.end()) {
    return nullptr;
  }
  return &it->second;
}

MessagesManager::DialogFolder *MessagesManager::get_dialog_folder(FolderId folder_id) {
  CHECK(!td_->auth_manager_->is_bot());
  auto it = dialog_folders_.find(folder_id);
  if (it == dialog_folders_.end()) {
    return nullptr;
  }
  return &it->second;
}

const MessagesManager::DialogFolder *MessagesManager::get_dialog_folder(FolderId folder_id) const {
  CHECK(!td_->auth_manager_->is_bot());
  auto it = dialog_folders_.find(folder_id);
  if (it == dialog_folders_.end()) {
    return nullptr;
  }
  return &it->second;
}

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 {
  if (td_->ignore_background_updates() || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    G()->td_db()->get_binlog_pmc()->erase(get_channel_pts_key(dialog_id));  // just in case
    return 0;
  }
  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) {
  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 << " from " << source
      << " while running getChannelDifference";

  // TODO delete_first_messages support in channels
  if (new_pts == std::numeric_limits<int32>::max()) {
    LOG(ERROR) << "Update " << d->dialog_id << " PTS to -1 from " << source;
    G()->td_db()->get_binlog_pmc()->erase(get_channel_pts_key(d->dialog_id));
    d->pts = std::numeric_limits<int32>::max();
    if (d->pending_read_channel_inbox_pts != 0) {
      d->pending_read_channel_inbox_pts = 0;
    }
    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 << " from "
                   << source;
    } else {
      LOG(INFO) << "Update " << d->dialog_id << " PTS to " << new_pts << " from " << source;
    }

    d->pts = new_pts;
    if (d->pending_read_channel_inbox_pts != 0 && d->pending_read_channel_inbox_pts <= d->pts) {
      auto pts = d->pending_read_channel_inbox_pts;
      d->pending_read_channel_inbox_pts = 0;
      on_dialog_updated(d->dialog_id, "set_channel_pts");
      if (d->pts == pts) {
        read_history_inbox(d, d->pending_read_channel_inbox_max_message_id,
                           d->pending_read_channel_inbox_server_unread_count, "set_channel_pts");
      } else if (d->pts > pts) {
        repair_channel_server_unread_count(d);
      }
    }
    if (!td_->ignore_background_updates() && td_->dialog_manager_->have_input_peer(d->dialog_id, AccessRights::Read)) {
      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 << " from " << source << ". Current PTS is "
               << d->pts;
  }
}

bool MessagesManager::need_channel_difference_to_add_message(DialogId dialog_id,
                                                             const tl_object_ptr<telegram_api::Message> &message_ptr) {
  if (dialog_id.get_type() != DialogType::Channel ||
      !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read) ||
      dialog_id == debug_channel_difference_dialog_ || td_->auth_manager_->is_bot()) {
    return false;
  }
  if (message_ptr == nullptr) {
    return true;
  }
  if (DialogId::get_message_dialog_id(message_ptr) != dialog_id) {
    return false;
  }

  Dialog *d = get_dialog_force(dialog_id, "need_channel_difference_to_add_message");
  if (d == nullptr) {
    LOG(DEBUG) << "Can't find " << dialog_id;
    return load_channel_pts(dialog_id) > 0 && !is_channel_difference_finished_.count(dialog_id);
  }
  if (d->last_new_message_id == MessageId()) {
    LOG(DEBUG) << "Can't find last message in " << dialog_id;
    return d->pts > 0 && !d->is_channel_difference_finished;
  }

  auto message_id = MessageId::get_message_id(message_ptr, false);
  LOG(DEBUG) << "Check ability to add " << message_id << " to " << dialog_id;
  return message_id > d->last_new_message_id;
}

void MessagesManager::run_after_channel_difference(DialogId dialog_id, MessageId expected_max_message_id,
                                                   Promise<Unit> &&promise, const char *source) {
  CHECK(dialog_id.get_type() == DialogType::Channel);
  CHECK(td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read));

  run_after_get_channel_difference_[dialog_id].push_back(std::move(promise));

  const Dialog *d = get_dialog(dialog_id);
  get_channel_difference(dialog_id, d == nullptr ? load_channel_pts(dialog_id) : d->pts, 0, expected_max_message_id,
                         true, source);
}

bool MessagesManager::running_get_channel_difference(DialogId dialog_id) const {
  return active_get_channel_differences_.count(dialog_id) > 0;
}

void MessagesManager::on_channel_get_difference_timeout(DialogId dialog_id) {
  if (G()->close_flag()) {
    return;
  }

  CHECK(dialog_id.get_type() == DialogType::Channel);
  auto d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  get_channel_difference(dialog_id, d->pts, 0, MessageId(), true, "on_channel_get_difference_timeout");
}

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::update_expected_channel_pts(DialogId dialog_id, int32 expected_pts) {
  if (expected_pts <= 0) {
    return;
  }
  CHECK(dialog_id.is_valid());
  auto &old_pts = expected_channel_pts_[dialog_id];
  if (old_pts < expected_pts) {
    old_pts = expected_pts;
  }
}

void MessagesManager::update_expected_channel_max_message_id(DialogId dialog_id, MessageId expected_max_message_id) {
  if (expected_max_message_id == MessageId() || td_->auth_manager_->is_bot()) {
    return;
  }
  CHECK(dialog_id.is_valid());
  auto &old_max_message_id = expected_channel_max_message_id_[dialog_id];
  if (old_max_message_id < expected_max_message_id) {
    old_max_message_id = expected_max_message_id;
  }
}

void MessagesManager::schedule_get_channel_difference(DialogId dialog_id, int32 expected_pts,
                                                      MessageId expected_max_message_id, double delay,
                                                      const char *source) {
  LOG(INFO) << "Schedule getDifference in " << dialog_id << " from " << source;
  update_expected_channel_pts(dialog_id, expected_pts);
  update_expected_channel_max_message_id(dialog_id, expected_max_message_id);
  channel_get_difference_retry_timeout_.add_timeout_in(dialog_id.get(), delay);
}

void MessagesManager::get_channel_difference(DialogId dialog_id, int32 pts, int32 expected_pts,
                                             MessageId expected_max_message_id, bool force, const char *source,
                                             bool is_old) {
  update_expected_channel_pts(dialog_id, expected_pts);
  update_expected_channel_max_message_id(dialog_id, expected_max_message_id);

  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;
  }
  LOG_CHECK(dialog_id.get_type() == DialogType::Channel) << dialog_id << " " << source;

  if (active_get_channel_differences_.count(dialog_id)) {
    LOG(INFO) << "Skip running channels.getDifference for " << dialog_id << " from " << source
              << " because it has already been run";
    return;
  }

  debug_last_get_channel_difference_dialog_id_ = dialog_id;
  debug_last_get_channel_difference_source_ = source;

  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 the channel is unknown";
    after_get_channel_difference(dialog_id, false);
    return;
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    LOG(INFO) << "Skip running channels.getDifference for " << dialog_id << " from " << source
              << " because have no read access to it";
    after_get_channel_difference(dialog_id, false);
    return;
  }

  if (force && get_channel_difference_to_log_event_id_.count(dialog_id) == 0 && !td_->ignore_background_updates() &&
      input_channel->get_id() == telegram_api::inputChannel::ID) {
    auto channel_id = dialog_id.get_channel_id();
    auto access_hash = static_cast<const telegram_api::inputChannel &>(*input_channel).access_hash_;
    auto log_event = GetChannelDifferenceLogEvent(channel_id, access_hash);
    auto log_event_id = binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::GetChannelDifference,
                                   get_log_event_storer(log_event));

    get_channel_difference_to_log_event_id_.emplace(dialog_id, log_event_id);
  }

  return do_get_channel_difference(dialog_id, pts, force, std::move(input_channel), is_old, source);
}

void MessagesManager::do_get_channel_difference(DialogId dialog_id, int32 pts, bool force,
                                                tl_object_ptr<telegram_api::InputChannel> &&input_channel, bool is_old,
                                                const char *source) {
  auto inserted = active_get_channel_differences_.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;
  }
  // must work even we know nothing about the dialog

  // can be called multiple times before after_get_channel_difference
  const Dialog *d = get_dialog(dialog_id);
  if (d != nullptr && d->notification_info != nullptr) {
    if (d->notification_info->message_notification_group_.is_valid()) {
      send_closure_later(G()->notification_manager(), &NotificationManager::before_get_chat_difference,
                         d->notification_info->message_notification_group_.get_group_id());
    }
    if (d->notification_info->mention_notification_group_.is_valid()) {
      send_closure_later(G()->notification_manager(), &NotificationManager::before_get_chat_difference,
                         d->notification_info->mention_notification_group_.get_group_id());
    }
  }

  int32 limit = td_->auth_manager_->is_bot() && !is_old ? MAX_BOT_CHANNEL_DIFFERENCE : MAX_CHANNEL_DIFFERENCE;
  if (pts <= 0) {
    pts = 1;
    limit = MIN_CHANNEL_DIFFERENCE;
  }

  pending_get_channel_differences_.push(
      td::make_unique<PendingGetChannelDifference>(dialog_id, pts, limit, force, std::move(input_channel), source));
  process_pending_get_channel_differences();
}

void MessagesManager::process_pending_get_channel_differences() {
  static constexpr int32 MAX_CONCURRENT_GET_CHANNEL_DIFFERENCES = 10;

  if (pending_get_channel_differences_.empty() ||
      get_channel_difference_count_ >= MAX_CONCURRENT_GET_CHANNEL_DIFFERENCES) {
    return;
  }

  get_channel_difference_count_++;

  auto query = std::move(pending_get_channel_differences_.front());
  pending_get_channel_differences_.pop();

  LOG(INFO) << "-----BEGIN GET CHANNEL DIFFERENCE----- for " << query->dialog_id_ << " with PTS " << query->pts_
            << " and limit " << query->limit_ << " from " << query->source_;

  td_->create_handler<GetChannelDifferenceQuery>()->send(query->dialog_id_, std::move(query->input_channel_),
                                                         query->pts_, query->limit_, query->force_);
}

void MessagesManager::process_get_channel_difference_updates(
    DialogId dialog_id, int32 new_pts, 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";
  CHECK(!debug_channel_difference_dialog_.is_valid());
  debug_channel_difference_dialog_ = dialog_id;

  // identifiers of edited and deleted messages
  FlatHashSet<MessageId, MessageIdHash> changed_message_ids;
  for (auto &update_ptr : other_updates) {
    bool is_good_update = true;
    switch (update_ptr->get_id()) {
      case telegram_api::updateMessageID::ID: {
        // in channels.getDifference updateMessageID can't be received for scheduled messages
        auto sent_message_update = move_tl_object_as<telegram_api::updateMessageID>(update_ptr);
        on_update_message_id(sent_message_update->random_id_, MessageId(ServerMessageId(sent_message_update->id_)),
                             "get_channel_difference");
        update_ptr = nullptr;
        break;
      }
      case telegram_api::updateDeleteChannelMessages::ID: {
        auto *update = static_cast<const telegram_api::updateDeleteChannelMessages *>(update_ptr.get());
        if (DialogId(ChannelId(update->channel_id_)) != dialog_id) {
          is_good_update = false;
        } else {
          for (auto &message : update->messages_) {
            changed_message_ids.insert(MessageId(ServerMessageId(message)));
          }
        }
        break;
      }
      case telegram_api::updateEditChannelMessage::ID: {
        auto *update = static_cast<const telegram_api::updateEditChannelMessage *>(update_ptr.get());
        auto message_full_id = MessageFullId::get_message_full_id(update->message_, false);
        if (message_full_id.get_dialog_id() != dialog_id) {
          is_good_update = false;
        } else {
          changed_message_ids.insert(message_full_id.get_message_id());
        }
        break;
      }
      case telegram_api::updatePinnedChannelMessages::ID: {
        auto *update = static_cast<const telegram_api::updatePinnedChannelMessages *>(update_ptr.get());
        if (DialogId(ChannelId(update->channel_id_)) != dialog_id) {
          is_good_update = false;
        }
        break;
      }
      default:
        is_good_update = false;
        break;
    }
    if (!is_good_update) {
      LOG(ERROR) << "Receive wrong update in channelDifference of " << dialog_id << ": " << to_string(update_ptr);
      update_ptr = nullptr;
    }
  }

  auto is_edited_message = [](const tl_object_ptr<telegram_api::Message> &message) {
    if (message->get_id() != telegram_api::message::ID) {
      return false;
    }
    return static_cast<const telegram_api::message *>(message.get())->edit_date_ > 0;
  };

  for (auto &message : new_messages) {
    if (is_edited_message(message)) {
      auto message_id = MessageId::get_message_id(message, false);
      if (message_id.is_valid()) {
        changed_message_ids.insert(message_id);
      }
    }
  }

  // extract awaited sent messages, which were edited or deleted after that
  auto postponed_updates_it = postponed_channel_updates_.find(dialog_id);
  struct AwaitedMessage {
    tl_object_ptr<telegram_api::Message> message;
    Promise<Unit> promise;
  };
  std::map<MessageId, AwaitedMessage> awaited_messages;
  if (postponed_updates_it != postponed_channel_updates_.end()) {
    auto &updates = postponed_updates_it->second;
    while (!updates.empty()) {
      auto it = updates.begin();
      auto update_pts = it->second.pts;
      if (update_pts > new_pts) {
        break;
      }

      auto update = std::move(it->second.update);
      auto promise = std::move(it->second.promise);
      updates.erase(it);

      if (update->get_id() == telegram_api::updateNewChannelMessage::ID) {
        auto update_new_channel_message = static_cast<telegram_api::updateNewChannelMessage *>(update.get());
        auto message_id = MessageId::get_message_id(update_new_channel_message->message_, false);
        MessageFullId message_full_id(dialog_id, message_id);
        if (update_message_ids_.count(message_full_id) > 0 && changed_message_ids.count(message_id) > 0) {
          changed_message_ids.erase(message_id);
          AwaitedMessage awaited_message;
          awaited_message.message = std::move(update_new_channel_message->message_);
          awaited_message.promise = std::move(promise);
          awaited_messages.emplace(message_id, std::move(awaited_message));
          continue;
        }
      }

      LOG(INFO) << "Skip to be applied from getChannelDifference " << to_string(update);
      promise.set_value(Unit());
    }
    if (updates.empty()) {
      postponed_channel_updates_.erase(postponed_updates_it);
    }
  }

  // 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;

  auto it = awaited_messages.begin();
  for (auto &message : new_messages) {
    auto message_id = MessageId::get_message_id(message, false);
    while (it != awaited_messages.end() && it->first < message_id) {
      on_get_message(std::move(it->second.message), true, true, false, "postponed channel update");
      it->second.promise.set_value(Unit());
      ++it;
    }
    Promise<Unit> promise;
    if (it != awaited_messages.end() && it->first == message_id) {
      if (is_edited_message(message)) {
        // the new message is edited, apply postponed one and move this to updateEditChannelMessage
        other_updates.push_back(make_tl_object<telegram_api::updateEditChannelMessage>(std::move(message), new_pts, 0));
        message = std::move(it->second.message);
        promise = std::move(it->second.promise);
      } else {
        it->second.promise.set_value(Unit());
      }
      ++it;
    }
    on_get_message(std::move(message), true, true, false, "get channel difference");
    promise.set_value(Unit());
  }
  while (it != awaited_messages.end()) {
    on_get_message(std::move(it->second.message), true, true, false, "postponed channel update 2");
    it->second.promise.set_value(Unit());
    ++it;
  }

  for (auto &update : other_updates) {
    if (update != nullptr) {
      process_channel_update(std::move(update));
    }
  }
  LOG_CHECK(!running_get_channel_difference(dialog_id)) << '"' << active_get_channel_differences_[dialog_id] << '"';

  if (need_repair_unread_count) {
    repair_channel_server_unread_count(get_dialog(dialog_id));
  }

  CHECK(debug_channel_difference_dialog_ == dialog_id);
  debug_channel_difference_dialog_ = DialogId();
}

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, int32 unread_reaction_count,
                                            MessageId read_outbox_max_message_id,
                                            vector<tl_object_ptr<telegram_api::Message>> &&messages) {
  FlatHashMap<MessageFullId, tl_object_ptr<telegram_api::Message>, MessageFullIdHash> message_full_id_to_message;
  for (auto &message : messages) {
    auto message_id = MessageId::get_message_id(message, false);
    if (!message_id.is_valid()) {
      continue;
    }
    auto message_dialog_id = DialogId::get_message_dialog_id(message);
    if (!message_dialog_id.is_valid()) {
      message_dialog_id = dialog_id;
    }
    auto message_full_id = MessageFullId(message_dialog_id, message_id);
    message_full_id_to_message[message_full_id] = std::move(message);
  }

  MessageFullId last_message_full_id(dialog_id, last_message_id);
  if (last_message_id.is_valid() && message_full_id_to_message.count(last_message_full_id) == 0) {
    LOG(ERROR) << "Last " << last_message_id << " in " << dialog_id << " not found. Have:";
    for (auto &message : message_full_id_to_message) {
      LOG(ERROR) << to_string(message.second);
    }
    return;
  }
  CHECK(!last_message_id.is_scheduled());

  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);

  // 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 the 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 outgoing live location messages shouldn't be deleted.

  if (is_message_unload_enabled()) {
    if (d->open_count == 0) {
      unload_dialog(dialog_id, 0);
    } else {
      d->need_unload_on_close = true;
    }
  }

  if (last_message_id > d->last_new_message_id && !td_->auth_manager_->is_bot()) {
    // TODO properly support last_message_id <= d->last_new_message_id
    set_dialog_first_database_message_id(d, MessageId(), "on_get_channel_dialog 6");
    set_dialog_last_database_message_id(d, MessageId(), "on_get_channel_dialog 7");
    d->have_full_history = false;
    d->have_full_history_source = 0;
    d->is_empty = false;
  }
  invalidate_message_indexes(d);
  // d->history_generation++;

  on_dialog_updated(dialog_id, "on_get_channel_dialog 10");

  // TODO properly support last_message_id <= d->last_new_message_id
  if (last_message_id > d->last_new_message_id &&
      !td_->auth_manager_->is_bot()) {  // if last message is really a new message
    if (!d->last_new_message_id.is_valid() && last_message_id <= d->max_added_message_id) {
      auto prev_message_id = MessageId(ServerMessageId(last_message_id.get_server_message_id().get() - 1));
      remove_dialog_newer_messages(d, prev_message_id, "on_get_channel_dialog 15");
    }
    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");
    MessageFullId added_message_full_id;
    if (last_message_id.is_valid()) {
      added_message_full_id = on_get_message(std::move(message_full_id_to_message[last_message_full_id]), true, true,
                                             false, "channel difference too long");
    }
    if (added_message_full_id.get_message_id().is_valid()) {
      if (added_message_full_id.get_message_id() == d->last_new_message_id) {
        CHECK(last_message_full_id == added_message_full_id);
        if (!td_->auth_manager_->is_bot()) {
          CHECK(d->last_message_id == d->last_new_message_id);
        }
      } else {
        LOG(ERROR) << added_message_full_id << " doesn't became last new message, which is " << d->last_new_message_id;
      }
    } else if (last_message_id > d->last_new_message_id) {
      set_dialog_last_new_message_id(d, last_message_id,
                                     "on_get_channel_dialog 40");  // skip updates about some messages
    }
  }

  if (d->last_read_inbox_message_id.is_valid() && !d->last_read_inbox_message_id.is_server() &&
      read_inbox_max_message_id == d->last_read_inbox_message_id.get_prev_server_message_id()) {
    read_inbox_max_message_id = d->last_read_inbox_message_id;
  }
  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 && !td_->auth_manager_->is_bot()) {
    set_dialog_unread_mention_count(d, unread_mention_count);
    update_dialog_mention_notification_count(d);
    send_update_chat_unread_mention_count(d);
  }
  if (d->unread_reaction_count != unread_reaction_count && !td_->auth_manager_->is_bot()) {
    set_dialog_unread_reaction_count(d, unread_reaction_count);
    // update_dialog_mention_notification_count(d);
    send_update_chat_unread_reaction_count(d, "on_get_channel_dialog 60");
  }

  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::retry_get_channel_difference_later(DialogId dialog_id) {
  auto &delay = channel_get_difference_retry_timeouts_[dialog_id];
  if (delay == 0) {
    delay = 1;
  }
  schedule_get_channel_difference(dialog_id, 0, MessageId(), Random::fast(delay * 800, delay * 1200) * 1e-3,
                                  "retry_get_channel_difference_later");
  delay *= 2;
  if (delay > 60) {
    delay = Random::fast(60, 80);
  }
}

void MessagesManager::on_get_channel_difference(DialogId dialog_id, int32 request_pts, int32 request_limit,
                                                tl_object_ptr<telegram_api::updates_ChannelDifference> &&difference_ptr,
                                                Status &&status) {
  get_channel_difference_count_--;
  CHECK(get_channel_difference_count_ >= 0);
  process_pending_get_channel_differences();
  LOG(INFO) << "----- END  GET CHANNEL DIFFERENCE----- for " << dialog_id;
  auto it = active_get_channel_differences_.find(dialog_id);
  CHECK(it != active_get_channel_differences_.end());
  string source = std::move(it->second);
  active_get_channel_differences_.erase(it);
  auto d = get_dialog_force(dialog_id, "on_get_channel_difference");

  if (difference_ptr == nullptr) {
    CHECK(status.is_error());
    bool have_access =
        td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read) && status.message() != "CHANNEL_INVALID";
    if (have_access) {
      if (d == nullptr) {
        force_create_dialog(dialog_id, "on_get_channel_difference failed");
        d = get_dialog(dialog_id);
        if (d == nullptr) {
          return after_get_channel_difference(dialog_id, false);
        }
      }
      retry_get_channel_difference_later(dialog_id);
    } else {
      after_get_channel_difference(dialog_id, false);
    }
    return;
  }
  CHECK(status.is_ok());

  LOG(INFO) << "Receive result of getChannelDifference for " << dialog_id << " with PTS = " << request_pts
            << " and limit = " << request_limit << " from " << source << ": " << to_string(difference_ptr);

  bool have_new_messages = false;
  switch (difference_ptr->get_id()) {
    case telegram_api::updates_channelDifferenceEmpty::ID:
      if (d == nullptr) {
        // no need to create the dialog
        channel_get_difference_retry_timeouts_.erase(dialog_id);
        after_get_channel_difference(dialog_id, true);
        return;
      }
      break;
    case telegram_api::updates_channelDifference::ID: {
      auto difference = static_cast<telegram_api::updates_channelDifference *>(difference_ptr.get());
      have_new_messages = !difference->new_messages_.empty();
      td_->contacts_manager_->on_get_users(std::move(difference->users_), "updates.channelDifference");
      td_->contacts_manager_->on_get_chats(std::move(difference->chats_), "updates.channelDifference");
      for (const auto &message : difference->new_messages_) {
        if (is_invalid_poll_message(message.get())) {
          LOG(ERROR) << "Receive invalid poll message in updates.channelDifference: " << oneline(to_string(message));
          if (channel_get_difference_retry_timeouts_[dialog_id] <= 2) {
            return retry_get_channel_difference_later(dialog_id);
          }
        }
      }
      break;
    }
    case telegram_api::updates_channelDifferenceTooLong::ID: {
      auto difference = static_cast<telegram_api::updates_channelDifferenceTooLong *>(difference_ptr.get());
      have_new_messages = difference->dialog_->get_id() == telegram_api::dialog::ID && !difference->messages_.empty();
      td_->contacts_manager_->on_get_users(std::move(difference->users_), "updates.channelDifferenceTooLong");
      td_->contacts_manager_->on_get_chats(std::move(difference->chats_), "updates.channelDifferenceTooLong");
      break;
    }
    default:
      UNREACHABLE();
  }

  channel_get_difference_retry_timeouts_.erase(dialog_id);

  bool need_update_dialog_pos = false;
  if (d == nullptr) {
    d = add_dialog_for_new_message(dialog_id, have_new_messages, &need_update_dialog_pos, "on_get_channel_difference");
  }

  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 from " << source;

  bool is_final = true;
  bool is_old = false;
  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 from " << source
                               << " 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
      // also, this can happen for deleted channels
      if (request_pts != difference->pts_ && !td_->auth_manager_->is_bot() &&
          td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
        LOG(ERROR) << "Receive channelDifferenceEmpty as result of getChannelDifference from " << source
                   << " with PTS = " << request_pts << " and limit = " << request_limit << " in " << dialog_id
                   << ", but PTS has changed 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 && request_pts > 1 && (request_pts > new_pts || !td_->auth_manager_->is_bot())) {
        LOG(ERROR) << "Receive channelDifference as result of getChannelDifference from " << source
                   << " 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;
      }

      if (difference->new_messages_.size() > 1) {
        // check that new messages are received in increasing message_id order
        MessageId cur_message_id;
        for (const auto &message : difference->new_messages_) {
          auto message_id = MessageId::get_message_id(message, false);
          if (message_id <= cur_message_id) {
            LOG(ERROR) << "Receive " << cur_message_id << " after " << message_id << " in channelDifference of "
                       << dialog_id << " from " << source << " with PTS " << request_pts << " and limit "
                       << request_limit << ": " << to_string(difference);
            after_get_channel_difference(dialog_id, false);
            return;
          }
          cur_message_id = message_id;
        }
      }
      if (!is_final && !difference->new_messages_.empty() && td_->auth_manager_->is_bot()) {
        auto date = get_message_date(difference->new_messages_.back());
        if (0 < date && date < G()->unix_time() - 2 * 86400) {
          is_old = true;
        }
      }

      process_get_channel_difference_updates(dialog_id, new_pts, 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);

      telegram_api::dialog *dialog = nullptr;
      switch (difference->dialog_->get_id()) {
        case telegram_api::dialog::ID:
          dialog = static_cast<telegram_api::dialog *>(difference->dialog_.get());
          break;
        case telegram_api::dialogFolder::ID:
          return after_get_channel_difference(dialog_id, false);
        default:
          UNREACHABLE();
          return;
      }

      CHECK(dialog != nullptr);
      if ((dialog->flags_ & telegram_api::dialog::PTS_MASK) == 0) {
        LOG(ERROR) << "Receive " << dialog_id << " without PTS";
        return after_get_channel_difference(dialog_id, false);
      }

      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 = dialog->pts_;
      if (request_pts > new_pts - request_limit) {
        LOG(ERROR) << "Receive channelDifferenceTooLong as result of getChannelDifference from " << source
                   << " 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;
        }
      }

      set_dialog_folder_id(d, FolderId(dialog->folder_id_));
      set_dialog_view_as_messages(d, dialog->view_forum_as_messages_, "updates.channelDifferenceTooLong");

      on_update_dialog_notify_settings(dialog_id, std::move(dialog->notify_settings_),
                                       "updates.channelDifferenceTooLong");

      bool is_marked_as_unread = dialog->unread_mark_;
      if (is_marked_as_unread != d->is_marked_as_unread) {
        set_dialog_is_marked_as_unread(d, is_marked_as_unread);
      }

      update_dialog_draft_message(d, get_draft_message(td_, std::move(dialog->draft_)), true, false);

      on_get_channel_dialog(dialog_id, MessageId(ServerMessageId(dialog->top_message_)),
                            MessageId(ServerMessageId(dialog->read_inbox_max_id_)), dialog->unread_count_,
                            dialog->unread_mentions_count_, dialog->unread_reactions_count_,
                            MessageId(ServerMessageId(dialog->read_outbox_max_id_)), std::move(difference->messages_));
      update_dialog_pos(d, "updates.channelDifferenceTooLong");

      if (!td_->auth_manager_->is_bot()) {
        // set is_pinned only after updating chat position to ensure that order is initialized
        bool is_pinned = (dialog->flags_ & DIALOG_FLAG_IS_PINNED) != 0;
        bool was_pinned = is_dialog_pinned(DialogListId(d->folder_id), dialog_id);
        if (is_pinned != was_pinned) {
          set_dialog_is_pinned(DialogListId(d->folder_id), d, is_pinned);
        }
      }

      set_channel_pts(d, new_pts, "channel difference too long");
      break;
    }
    default:
      UNREACHABLE();
  }

  if (need_update_dialog_pos) {
    update_dialog_pos(d, "on_get_channel_difference");
  }

  if (!is_final) {
    LOG_IF(ERROR, timeout > 0) << "Have timeout in non-final ChannelDifference in " << dialog_id;
    get_channel_difference(dialog_id, d->pts, 0, MessageId(), true, "on_get_channel_difference", is_old);
    return;
  }

  LOG_IF(ERROR, timeout == 0) << "Have no timeout in final ChannelDifference in " << dialog_id;
  if (timeout > 0 && d->open_count > 0) {
    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;
  LOG_CHECK(!running_get_channel_difference(dialog_id)) << '"' << active_get_channel_differences_[dialog_id] << '"';

  auto log_event_it = get_channel_difference_to_log_event_id_.find(dialog_id);
  if (log_event_it != get_channel_difference_to_log_event_id_.end()) {
    if (!G()->close_flag()) {
      binlog_erase(G()->td_db()->get_binlog(), log_event_it->second);
    }
    get_channel_difference_to_log_event_id_.erase(log_event_it);
  }

  auto d = get_dialog(dialog_id);
  bool have_access = td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read);
  auto pts = d != nullptr ? d->pts : load_channel_pts(dialog_id);
  auto postponed_updates_it = postponed_channel_updates_.find(dialog_id);
  if (postponed_updates_it != postponed_channel_updates_.end()) {
    auto &updates = postponed_updates_it->second;
    LOG(INFO) << "Begin to apply " << updates.size() << " postponed channel updates";
    while (!updates.empty()) {
      auto it = updates.begin();
      auto update = std::move(it->second.update);
      auto update_pts = it->second.pts;
      auto update_pts_count = it->second.pts_count;
      auto promise = std::move(it->second.promise);
      updates.erase(it);

      auto old_size = updates.size();
      auto update_id = update->get_id();
      if (have_access) {
        add_pending_channel_update(dialog_id, std::move(update), update_pts, update_pts_count, std::move(promise),
                                   "apply postponed channel updates", true);
      } else {
        promise.set_value(Unit());
      }
      if (updates.size() != old_size || running_get_channel_difference(dialog_id)) {
        if (success && update_pts - 10000 < pts && 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 protect from getChannelDifference repeating calls by dropping postponed updates
          LOG(WARNING) << "Failed to apply postponed updates of type " << update_id << " in " << dialog_id
                       << " with PTS " << pts << ", update PTS is " << update_pts << ", update PTS count is "
                       << update_pts_count;
          vector<Promise<Unit>> update_promises;
          for (auto &postponed_update : updates) {
            update_promises.push_back(std::move(postponed_update.second.promise));
          }
          updates.clear();
          for (auto &update_promise : update_promises) {
            update_promise.set_value(Unit());
          }
        }
        break;
      }
    }
    if (updates.empty()) {
      postponed_channel_updates_.erase(postponed_updates_it);
    }
    LOG(INFO) << "Finish to apply postponed channel updates";
  }

  if (d != nullptr) {
    d->is_channel_difference_finished = true;

    if (d->notification_info != nullptr) {
      if (d->notification_info->message_notification_group_.is_valid()) {
        send_closure_later(G()->notification_manager(), &NotificationManager::after_get_chat_difference,
                           d->notification_info->message_notification_group_.get_group_id());
      }
      if (d->notification_info->mention_notification_group_.is_valid()) {
        send_closure_later(G()->notification_manager(), &NotificationManager::after_get_chat_difference,
                           d->notification_info->mention_notification_group_.get_group_id());
      }
    }
  } else {
    is_channel_difference_finished_.insert(dialog_id);
  }

  if (postponed_chat_read_inbox_updates_.erase(dialog_id) > 0) {
    send_update_chat_read_inbox(d, true, "after_get_channel_difference");
  }

  auto promise_it = run_after_get_channel_difference_.find(dialog_id);
  if (promise_it != run_after_get_channel_difference_.end()) {
    auto promises = std::move(promise_it->second);
    run_after_get_channel_difference_.erase(promise_it);

    set_promises(promises);
  }

  auto on_get_dialogs_it = pending_channel_on_get_dialogs_.find(dialog_id);
  if (on_get_dialogs_it != pending_channel_on_get_dialogs_.end()) {
    LOG(INFO) << "Apply postponed results of channel getDialogs for " << dialog_id;
    PendingOnGetDialogs res = std::move(on_get_dialogs_it->second);
    pending_channel_on_get_dialogs_.erase(on_get_dialogs_it);

    on_get_dialogs(res.folder_id, std::move(res.dialogs), res.total_count, std::move(res.messages),
                   std::move(res.promise));
  }

  if (d != nullptr && !td_->auth_manager_->is_bot() && have_access && !d->last_message_id.is_valid() && !d->is_empty &&
      (d->order != DEFAULT_ORDER || is_dialog_sponsored(d))) {
    load_last_dialog_message(d, "after_get_channel_difference");
  }

  auto expected_channel_pts_it = expected_channel_pts_.find(dialog_id);
  if (expected_channel_pts_it != expected_channel_pts_.end()) {
    if (success && d != nullptr && expected_channel_pts_it->second > pts) {
      schedule_get_channel_difference(dialog_id, 0, MessageId(), 1.0, "after_get_channel_difference");
    }
    expected_channel_pts_.erase(expected_channel_pts_it);
  }

  auto expected_channel_max_message_id_it = expected_channel_max_message_id_.find(dialog_id);
  if (expected_channel_max_message_id_it != expected_channel_max_message_id_.end()) {
    if (success && d != nullptr && expected_channel_max_message_id_it->second > d->last_new_message_id) {
      schedule_get_channel_difference(dialog_id, 0, MessageId(), 1.0, "after_get_channel_difference 2");
    }
    expected_channel_max_message_id_.erase(expected_channel_max_message_id_it);
  }
}

void MessagesManager::reget_message_from_server_if_needed(DialogId dialog_id, const Message *m) {
  if (!m->message_id.is_any_server() || dialog_id.get_type() == DialogType::SecretChat) {
    return;
  }

  if (need_reget_message_content(m->content.get()) || (m->legacy_layer != 0 && m->legacy_layer < MTPROTO_LAYER) ||
      m->reply_info.need_reget(td_) || m->replied_message_info.need_reget()) {
    MessageFullId message_full_id{dialog_id, m->message_id};
    LOG(INFO) << "Reget from server " << message_full_id;
    get_message_from_server(message_full_id, Auto(), "reget_message_from_server_if_needed");
  }
}

void MessagesManager::speculatively_update_active_group_call_id(Dialog *d, const Message *m) {
  CHECK(m != nullptr);
  if (!m->message_id.is_any_server() || m->content->get_type() != MessageContentType::GroupCall) {
    return;
  }

  InputGroupCallId input_group_call_id;
  bool is_ended;
  std::tie(input_group_call_id, is_ended) = get_message_content_group_call_info(m->content.get());
  d->has_expected_active_group_call_id = true;
  if (is_ended) {
    d->expected_active_group_call_id = InputGroupCallId();
    if (d->active_group_call_id == input_group_call_id) {
      on_update_dialog_group_call_id(d->dialog_id, InputGroupCallId());
    }
  } else {
    d->expected_active_group_call_id = input_group_call_id;
    if (d->active_group_call_id != input_group_call_id && !td_->auth_manager_->is_bot()) {
      repair_dialog_active_group_call_id(d->dialog_id);
    }
  }
}

void MessagesManager::speculatively_update_channel_participants(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  if (!m->message_id.is_any_server() || dialog_id.get_type() != DialogType::Channel || !m->sender_user_id.is_valid()) {
    return;
  }

  auto channel_id = dialog_id.get_channel_id();
  UserId my_user_id(td_->contacts_manager_->get_my_id());
  bool by_me = m->sender_user_id == my_user_id;
  switch (m->content->get_type()) {
    case MessageContentType::ChatAddUsers:
      send_closure_later(G()->contacts_manager(), &ContactsManager::speculative_add_channel_participants, channel_id,
                         get_message_content_added_user_ids(m->content.get()), m->sender_user_id, m->date, by_me);
      break;
    case MessageContentType::ChatJoinedByLink:
      send_closure_later(G()->contacts_manager(), &ContactsManager::speculative_add_channel_participants, channel_id,
                         vector<UserId>{m->sender_user_id}, m->sender_user_id, m->date, by_me);
      break;
    case MessageContentType::ChatDeleteUser:
      send_closure_later(G()->contacts_manager(), &ContactsManager::speculative_delete_channel_participant, channel_id,
                         get_message_content_deleted_user_id(m->content.get()), by_me);
      break;
    default:
      break;
  }
}

void MessagesManager::update_sent_message_contents(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  if (td_->auth_manager_->is_bot() || (!m->is_outgoing && dialog_id != td_->dialog_manager_->get_my_dialog_id()) ||
      dialog_id.get_type() == DialogType::SecretChat || m->message_id.is_local() || m->forward_info != nullptr ||
      m->had_forward_info) {
    return;
  }

  on_sent_message_content(td_, m->content.get());
}

void MessagesManager::update_used_hashtags(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  if (td_->auth_manager_->is_bot() || (!m->is_outgoing && dialog_id != td_->dialog_manager_->get_my_dialog_id()) ||
      m->via_bot_user_id.is_valid() || m->hide_via_bot || m->forward_info != nullptr || m->had_forward_info) {
    return;
  }

  ::td::update_used_hashtags(td_, m->content.get());
}

void MessagesManager::update_top_dialogs(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  auto dialog_type = dialog_id.get_type();
  if (td_->auth_manager_->is_bot() || (!m->is_outgoing && dialog_id != td_->dialog_manager_->get_my_dialog_id()) ||
      dialog_type == DialogType::SecretChat || !m->message_id.is_any_server()) {
    return;
  }

  bool is_forward = m->forward_info != nullptr || m->had_forward_info;
  if (m->via_bot_user_id.is_valid() && !is_forward) {
    // forwarded game messages can't be distinguished from sent via bot game messages, so increase rating anyway
    on_dialog_used(TopDialogCategory::BotInline, DialogId(m->via_bot_user_id), m->date);
  }

  if (is_forward) {
    auto &last_forward_date = last_outgoing_forwarded_message_date_[dialog_id];
    if (last_forward_date < m->date) {
      TopDialogCategory category =
          dialog_type == DialogType::User ? TopDialogCategory::ForwardUsers : TopDialogCategory::ForwardChats;
      on_dialog_used(category, dialog_id, m->date);
      last_forward_date = m->date;
    }
  }

  TopDialogCategory category = TopDialogCategory::Size;
  switch (dialog_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) {
    on_dialog_used(category, dialog_id, m->date);
  }
}

void MessagesManager::update_forward_count(DialogId dialog_id, const Message *m) {
  if (td_->auth_manager_->is_bot() || m->forward_info == nullptr) {
    return;
  }
  auto origin_message_full_id = m->forward_info->get_origin_message_full_id();
  if (!origin_message_full_id.get_message_id().is_valid()) {
    return;
  }
  if (is_discussion_message(dialog_id, m) && origin_message_full_id == m->forward_info->get_last_message_full_id()) {
    return;
  }
  update_forward_count(origin_message_full_id, m->date);
}

void MessagesManager::update_forward_count(MessageFullId message_full_id, int32 update_date) {
  CHECK(!td_->auth_manager_->is_bot());
  auto dialog_id = message_full_id.get_dialog_id();
  Dialog *d = get_dialog(dialog_id);
  CHECK(d != nullptr);
  Message *m = get_message_force(d, message_full_id.get_message_id(), "update_forward_count");
  if (m != nullptr && !m->message_id.is_scheduled() && m->message_id.is_server() && m->view_count > 0 &&
      m->interaction_info_update_date < update_date) {
    if (m->forward_count == 0) {
      m->forward_count++;
      send_update_message_interaction_info(dialog_id, m);
      on_message_changed(d, m, true, "update_forward_count");
    }
    if (pending_message_views_[dialog_id].message_ids_.insert(m->message_id).second) {
      pending_message_views_timeout_.add_timeout_in(dialog_id.get(), 0.0);
    }
  }
}

void MessagesManager::update_has_outgoing_messages(DialogId dialog_id, const Message *m) {
  CHECK(m != nullptr);
  if (td_->auth_manager_->is_bot() || (!m->is_outgoing && dialog_id != td_->dialog_manager_->get_my_dialog_id())) {
    return;
  }

  Dialog *d = nullptr;
  switch (dialog_id.get_type()) {
    case DialogType::User:
      d = get_dialog(dialog_id);
      break;
    case DialogType::Chat:
    case DialogType::Channel:
      break;
    case DialogType::SecretChat: {
      auto user_id = td_->contacts_manager_->get_secret_chat_user_id(dialog_id.get_secret_chat_id());
      if (user_id.is_valid()) {
        d = get_dialog_force(DialogId(user_id), "update_has_outgoing_messages");
      }
      break;
    }
    default:
      UNREACHABLE();
  }
  if (d == nullptr || d->has_outgoing_messages) {
    return;
  }

  d->has_outgoing_messages = true;
  on_dialog_updated(dialog_id, "update_has_outgoing_messages");

  if (d->action_bar != nullptr && d->action_bar->on_outgoing_message()) {
    send_update_chat_action_bar(d);
  }
}

void MessagesManager::set_message_reply(const Dialog *d, Message *m, MessageInputReplyTo input_reply_to,
                                        bool is_message_in_dialog) {
  LOG(INFO) << "Update replied message of " << MessageFullId{d->dialog_id, m->message_id} << " from "
            << m->replied_message_info << " to " << input_reply_to;
  if (is_message_in_dialog) {
    unregister_message_reply(d->dialog_id, m);
  }
  m->replied_message_info = RepliedMessageInfo(td_, input_reply_to);
  m->reply_to_story_full_id = StoryFullId();
  m->reply_to_random_id = get_message_reply_to_random_id(d, m);
  if (!m->message_id.is_any_server()) {
    m->input_reply_to = std::move(input_reply_to);
  }
  if (is_message_in_dialog) {
    register_message_reply(d->dialog_id, m);
  }
  update_message_max_reply_media_timestamp(d, m, is_message_in_dialog);
}

void MessagesManager::update_message_reply_to_message_id(const Dialog *d, Message *m, MessageId reply_to_message_id,
                                                         bool is_message_in_dialog) {
  LOG(INFO) << "Update identifier of replied message of " << MessageFullId{d->dialog_id, m->message_id} << " from "
            << m->replied_message_info << " to " << reply_to_message_id;
  if (is_message_in_dialog) {
    unregister_message_reply(d->dialog_id, m);
  }
  m->replied_message_info.set_message_id(reply_to_message_id);
  if (!m->message_id.is_any_server()) {
    m->input_reply_to.set_message_id(reply_to_message_id);
  }
  if (is_message_in_dialog) {
    register_message_reply(d->dialog_id, m);
  }
}

void MessagesManager::restore_message_reply_to_message_id(Dialog *d, Message *m) {
  const auto *input_reply_to = get_message_input_reply_to(m);
  CHECK(input_reply_to != nullptr);
  if (input_reply_to->is_empty()) {
    return;
  }
  auto replied_message_full_id = input_reply_to->get_reply_message_full_id(d->dialog_id);
  auto replied_message_id = replied_message_full_id.get_message_id();
  if (replied_message_id == MessageId() || !replied_message_id.is_yet_unsent()) {
    return;
  }
  CHECK(replied_message_full_id.get_dialog_id() == d->dialog_id);

  auto message_id = get_message_id_by_random_id(d, m->reply_to_random_id, "restore_message_reply_to_message_id");
  if (message_id.is_valid() || message_id.is_valid_scheduled()) {
    update_message_reply_to_message_id(d, m, message_id, false);
  } else {
    set_message_reply(d, m, MessageInputReplyTo{m->top_thread_message_id, DialogId(), FormattedText(), 0}, false);
  }
}

MessagesManager::Message *MessagesManager::continue_send_message(DialogId dialog_id, unique_ptr<Message> &&message,
                                                                 bool *need_update_dialog_pos, uint64 log_event_id) {
  CHECK(log_event_id != 0);
  CHECK(message != nullptr);
  CHECK(message->content != nullptr);

  Dialog *d = get_dialog_force(dialog_id, "continue_send_message");
  if (d == nullptr) {
    LOG(ERROR) << "Can't find " << dialog_id << " to continue send a message";
    binlog_erase(G()->td_db()->get_binlog(), log_event_id);
    return nullptr;
  }
  if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
    binlog_erase(G()->td_db()->get_binlog(), log_event_id);
    return nullptr;
  }

  LOG(INFO) << "Continue to send " << message->message_id << " to " << dialog_id << " initially sent at "
            << message->send_date << " from binlog";

  d->was_opened = true;

  auto now = G()->unix_time();
  if (message->message_id.is_scheduled()) {
    message->message_id = get_next_yet_unsent_scheduled_message_id(d, message->date);
  } else {
    message->message_id = get_next_yet_unsent_message_id(d);
    message->date = now;
  }

  restore_message_reply_to_message_id(d, message.get());

  bool need_update = false;
  auto result_message = add_message_to_dialog(d, std::move(message), false, true, &need_update, need_update_dialog_pos,
                                              "continue_send_message");
  CHECK(result_message != nullptr);

  if (result_message->message_id.is_scheduled()) {
    send_update_chat_has_scheduled_messages(d, false);
  }

  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 != td_->dialog_manager_->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 continue to send a message to " << dialog_id << ": " << can_send_status;

    send_update_new_message(d, result_message);
    if (*need_update_dialog_pos) {
      send_update_chat_last_message(d, "continue_send_message");
    }

    fail_send_message({dialog_id, result_message->message_id}, std::move(can_send_status));
    return nullptr;
  }

  return result_message;
}

void MessagesManager::on_binlog_events(vector<BinlogEvent> &&events) {
  if (G()->close_flag()) {
    return;
  }
  bool have_old_message_database = G()->use_message_database() && !G()->td_db()->was_dialog_db_created();
  for (auto &event : events) {
    CHECK(event.id_ != 0);
    switch (event.type_) {
      case LogEvent::HandlerType::SendMessage: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SendMessageLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id;
        auto message = std::move(log_event.message_out);
        message->send_message_log_event_id = event.id_;

        if (message->content->get_type() == MessageContentType::Unsupported) {
          LOG(ERROR) << "Message content is invalid: " << format::as_hex_dump<4>(event.get_data());
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          continue;
        }

        Dependencies dependencies;
        dependencies.add_dialog_dependencies(dialog_id);
        add_message_dependencies(dependencies, message.get());
        dependencies.resolve_force(td_, "SendMessageLogEvent");

        message->content = dup_message_content(td_, dialog_id, message->content.get(), MessageContentDupType::Send,
                                               MessageCopyOptions());

        bool need_update_dialog_pos = false;
        auto result_message = continue_send_message(dialog_id, std::move(message), &need_update_dialog_pos, event.id_);
        if (result_message != nullptr) {
          // uses send_closure_later internally
          do_send_message(dialog_id, result_message);
          Dialog *d = get_dialog(dialog_id);
          send_update_new_message(d, result_message);
          if (need_update_dialog_pos) {
            send_update_chat_last_message(d, "SendMessageLogEvent");
          }
        }

        break;
      }
      case LogEvent::HandlerType::SendBotStartMessage: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SendBotStartMessageLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id;
        auto message = std::move(log_event.message_out);
        message->send_message_log_event_id = event.id_;

        CHECK(message->content->get_type() == MessageContentType::Text);

        auto bot_user_id = log_event.bot_user_id;
        Dependencies dependencies;
        dependencies.add(bot_user_id);
        dependencies.add_dialog_and_dependencies(dialog_id);
        add_message_dependencies(dependencies, message.get());
        if (!dependencies.resolve_force(td_, "SendBotStartMessageLogEvent")) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          continue;
        }

        bool need_update_dialog_pos = false;
        auto result_message = continue_send_message(dialog_id, std::move(message), &need_update_dialog_pos, event.id_);
        if (result_message != nullptr) {
          send_closure_later(actor_id(this), &MessagesManager::do_send_bot_start_message, bot_user_id, dialog_id,
                             result_message->message_id, log_event.parameter);
          Dialog *d = get_dialog(dialog_id);
          send_update_new_message(d, result_message);
          if (need_update_dialog_pos) {
            send_update_chat_last_message(d, "SendBotStartMessageLogEvent");
          }
        }
        break;
      }
      case LogEvent::HandlerType::SendInlineQueryResultMessage: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SendInlineQueryResultMessageLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id;
        auto message = std::move(log_event.message_out);
        message->send_message_log_event_id = event.id_;

        if (message->content->get_type() == MessageContentType::Unsupported) {
          LOG(ERROR) << "Message content is invalid: " << format::as_hex_dump<4>(event.get_data());
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          continue;
        }

        Dependencies dependencies;
        dependencies.add_dialog_dependencies(dialog_id);
        add_message_dependencies(dependencies, message.get());
        dependencies.resolve_force(td_, "SendInlineQueryResultMessageLogEvent");

        message->content = dup_message_content(td_, dialog_id, message->content.get(),
                                               MessageContentDupType::SendViaBot, MessageCopyOptions());

        bool need_update_dialog_pos = false;
        auto result_message = continue_send_message(dialog_id, std::move(message), &need_update_dialog_pos, event.id_);
        if (result_message != nullptr) {
          send_closure_later(actor_id(this), &MessagesManager::do_send_inline_query_result_message, dialog_id,
                             result_message->message_id, log_event.query_id, log_event.result_id);
          Dialog *d = get_dialog(dialog_id);
          send_update_new_message(d, result_message);
          if (need_update_dialog_pos) {
            send_update_chat_last_message(d, "SendInlineQueryResultMessageLogEvent");
          }
        }
        break;
      }
      case LogEvent::HandlerType::SendScreenshotTakenNotificationMessage: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SendScreenshotTakenNotificationMessageLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id;
        auto message = std::move(log_event.message_out);
        message->send_message_log_event_id = 0;  // to not allow event deletion by message deletion

        CHECK(message->content->get_type() == MessageContentType::ScreenshotTaken);

        Dependencies dependencies;
        dependencies.add_dialog_dependencies(dialog_id);
        add_message_dependencies(dependencies, message.get());
        dependencies.resolve_force(td_, "SendScreenshotTakenNotificationMessageLogEvent");

        bool need_update_dialog_pos = false;
        auto result_message = continue_send_message(dialog_id, std::move(message), &need_update_dialog_pos, event.id_);
        if (result_message != nullptr) {
          // order with other messages isn't kept
          do_send_screenshot_taken_notification_message(dialog_id, result_message, event.id_);
          Dialog *d = get_dialog(dialog_id);
          send_update_new_message(d, result_message);
          if (need_update_dialog_pos) {
            send_update_chat_last_message(d, "SendScreenshotTakenNotificationMessageLogEvent");
          }
        }
        break;
      }
      case LogEvent::HandlerType::ForwardMessages: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          continue;
        }

        ForwardMessagesLogEvent log_event;
        log_event_parse(log_event, event.get_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;
        dependencies.add_dialog_and_dependencies(to_dialog_id);
        dependencies.add_dialog_and_dependencies(from_dialog_id);
        for (auto &message : messages) {
          add_message_dependencies(dependencies, message.get());
        }
        if (!dependencies.resolve_force(td_, "ForwardMessagesLogEvent")) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          continue;
        }

        Dialog *to_dialog = get_dialog(to_dialog_id);
        CHECK(to_dialog != nullptr);
        to_dialog->was_opened = true;

        auto now = G()->unix_time();
        if (!td_->dialog_manager_->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 != td_->dialog_manager_->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_);
          continue;
        }

        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 &message : messages) {
          if (message->message_id.is_scheduled()) {
            message->message_id = get_next_yet_unsent_scheduled_message_id(to_dialog, message->date);
          } else {
            message->message_id = get_next_yet_unsent_message_id(to_dialog);
            message->date = now;
          }
          message->content = dup_message_content(td_, to_dialog_id, message->content.get(),
                                                 MessageContentDupType::Forward, MessageCopyOptions());
          CHECK(message->content != nullptr);

          restore_message_reply_to_message_id(to_dialog, message.get());

          forwarded_messages.push_back(add_message_to_dialog(to_dialog, std::move(message), false, true, &need_update,
                                                             &need_update_dialog_pos, "forward message again"));
          send_update_new_message(to_dialog, forwarded_messages.back());
        }

        send_update_chat_has_scheduled_messages(to_dialog, false);

        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,
                            log_event.drop_author, log_event.drop_media_captions, event.id_);
        break;
      }
      case LogEvent::HandlerType::DeleteMessage: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        DeleteMessageLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();
        log_event.id_ = event.id_;

        Dialog *d = get_dialog_force(log_event.message_full_id_.get_dialog_id(), "DeleteMessageLogEvent");
        if (d != nullptr) {
          auto message_id = log_event.message_full_id_.get_message_id();
          if (message_id.is_valid_scheduled() && message_id.is_scheduled_server()) {
            add_dialog_scheduled_messages(d)->deleted_scheduled_server_message_ids_.insert(
                message_id.get_scheduled_server_message_id());
          } else if (message_id != MessageId()) {
            d->deleted_message_ids.insert(message_id);
          }
        }

        do_delete_message_log_event(log_event);
        break;
      }
      case LogEvent::HandlerType::DeleteMessagesOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        DeleteMessagesOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "DeleteMessagesOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        for (auto message_id : log_event.message_ids_) {
          CHECK(message_id.is_valid());
          d->deleted_message_ids.insert(message_id);
        }

        delete_messages_on_server(dialog_id, std::move(log_event.message_ids_), log_event.revoke_, event.id_, Auto());
        break;
      }
      case LogEvent::HandlerType::DeleteScheduledMessagesOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        DeleteScheduledMessagesOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "DeleteScheduledMessagesOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        for (auto message_id : log_event.message_ids_) {
          CHECK(message_id.is_scheduled_server());
          add_dialog_scheduled_messages(d)->deleted_scheduled_server_message_ids_.insert(
              message_id.get_scheduled_server_message_id());
        }

        delete_scheduled_messages_on_server(dialog_id, std::move(log_event.message_ids_), event.id_, Auto());
        break;
      }
      case LogEvent::HandlerType::DeleteDialogHistoryOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        DeleteDialogHistoryOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "DeleteDialogHistoryOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        delete_dialog_history_on_server(dialog_id, log_event.max_message_id_, log_event.remove_from_dialog_list_,
                                        log_event.revoke_, true, event.id_, Auto());
        break;
      }
      case LogEvent::HandlerType::DeleteTopicHistoryOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        DeleteTopicHistoryOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "DeleteTopicHistoryOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        delete_topic_history_on_server(dialog_id, log_event.top_thread_message_id_, event.id_, Auto());
        break;
      }
      case LogEvent::HandlerType::DeleteAllCallMessagesOnServer: {
        DeleteAllCallMessagesOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        delete_all_call_messages_on_server(log_event.revoke_, event.id_, Auto());
        break;
      }
      case LogEvent::HandlerType::BlockMessageSenderFromRepliesOnServer: {
        BlockMessageSenderFromRepliesOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        block_message_sender_from_replies_on_server(log_event.message_id_, log_event.delete_message_,
                                                    log_event.delete_all_messages_, log_event.report_spam_, event.id_,
                                                    Auto());
        break;
      }
      case LogEvent::HandlerType::DeleteAllChannelMessagesFromSenderOnServer: {
        if (!G()->use_chat_info_database()) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        DeleteAllChannelMessagesFromSenderOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto channel_id = log_event.channel_id_;
        auto sender_dialog_id = log_event.sender_dialog_id_;
        Dependencies dependencies;
        dependencies.add(channel_id);
        dependencies.add_dialog_dependencies(sender_dialog_id);
        if (!dependencies.resolve_force(td_, "DeleteAllChannelMessagesFromSenderOnServer") ||
            !td_->dialog_manager_->have_input_peer(sender_dialog_id, AccessRights::Know)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          continue;
        }

        delete_all_channel_messages_by_sender_on_server(channel_id, sender_dialog_id, event.id_, Auto());
        break;
      }
      case LogEvent::HandlerType::DeleteDialogMessagesByDateOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        DeleteDialogMessagesByDateOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "DeleteDialogMessagesByDateOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        delete_dialog_messages_by_date_on_server(dialog_id, log_event.min_date_, log_event.max_date_, log_event.revoke_,
                                                 event.id_, Auto());
        break;
      }
      case LogEvent::HandlerType::ReadHistoryOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ReadHistoryOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "ReadHistoryOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }
        auto &log_event_id = read_history_log_event_ids_[dialog_id][0];
        if (log_event_id.log_event_id != 0) {
          // we need only the latest read history event
          binlog_erase(G()->td_db()->get_binlog(), log_event_id.log_event_id);
        }
        log_event_id.log_event_id = event.id_;

        read_history_on_server_impl(d, log_event.max_message_id_);
        break;
      }
      case LogEvent::HandlerType::ReadHistoryInSecretChat: {
        ReadHistoryInSecretChatLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        CHECK(dialog_id.get_type() == DialogType::SecretChat);
        Dependencies dependencies;
        dependencies.add_dialog_and_dependencies(dialog_id);
        if (!dependencies.resolve_force(td_, "ReadHistoryInSecretChat") ||
            !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        auto &log_event_id = read_history_log_event_ids_[dialog_id][0];
        if (log_event_id.log_event_id != 0) {
          // we need only the latest read history event
          binlog_erase(G()->td_db()->get_binlog(), log_event_id.log_event_id);
        }
        log_event_id.log_event_id = event.id_;

        Dialog *d = get_dialog(dialog_id);
        CHECK(d != nullptr);
        d->last_read_inbox_message_date = log_event.max_date_;

        read_history_on_server_impl(d, MessageId());
        break;
      }
      case LogEvent::HandlerType::ReadMessageThreadHistoryOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ReadMessageThreadHistoryOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "ReadMessageThreadHistoryOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }
        auto top_thread_message_id = log_event.top_thread_message_id_;
        auto &log_event_id = read_history_log_event_ids_[dialog_id][top_thread_message_id.get()];
        if (log_event_id.log_event_id != 0) {
          // we need only the latest read history event
          binlog_erase(G()->td_db()->get_binlog(), log_event_id.log_event_id);
        }
        log_event_id.log_event_id = event.id_;

        read_message_thread_history_on_server_impl(d, top_thread_message_id, log_event.max_message_id_);
        break;
      }
      case LogEvent::HandlerType::ReadMessageContentsOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ReadMessageContentsOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "ReadMessageContentsOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->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_, Auto());
        break;
      }
      case LogEvent::HandlerType::ReadAllDialogMentionsOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ReadAllDialogMentionsOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "ReadAllDialogMentionsOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->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::ReadAllDialogReactionsOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ReadAllDialogReactionsOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "ReadAllDialogReactionsOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        read_all_dialog_reactions_on_server(dialog_id, event.id_, Promise<Unit>());
        break;
      }
      case LogEvent::HandlerType::ToggleDialogIsPinnedOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ToggleDialogIsPinnedOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "ToggleDialogIsPinnedOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->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 (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ReorderPinnedDialogsOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        vector<DialogId> dialog_ids;
        for (auto &dialog_id : log_event.dialog_ids_) {
          Dialog *d = get_dialog_force(dialog_id, "ReorderPinnedDialogsOnServerLogEvent");
          if (d != nullptr && td_->dialog_manager_->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(log_event.folder_id_, dialog_ids, event.id_);
        break;
      }
      case LogEvent::HandlerType::ToggleDialogViewAsMessagesOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ToggleDialogViewAsMessagesOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        if (!have_dialog_force(dialog_id, "ToggleDialogViewAsMessagesOnServerLogEvent") ||
            !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        toggle_dialog_view_as_messages_on_server(dialog_id, log_event.view_as_messages_, event.id_);
        break;
      }
      case LogEvent::HandlerType::ToggleDialogIsTranslatableOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ToggleDialogIsTranslatableOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        if (!have_dialog_force(dialog_id, "ToggleDialogIsTranslatableOnServerLogEvent") ||
            !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        toggle_dialog_is_translatable_on_server(dialog_id, log_event.is_translatable_, event.id_);
        break;
      }
      case LogEvent::HandlerType::ToggleDialogIsMarkedAsUnreadOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ToggleDialogIsMarkedAsUnreadOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        if (!have_dialog_force(dialog_id, "ToggleDialogIsMarkedAsUnreadOnServerLogEvent") ||
            !td_->dialog_manager_->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::ToggleDialogIsBlockedOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ToggleDialogIsBlockedOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        if (dialog_id.get_type() == DialogType::SecretChat ||
            !td_->dialog_manager_->have_dialog_info_force(dialog_id, "ToggleDialogIsBlockedOnServerLogEvent") ||
            !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Know)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        toggle_dialog_is_blocked_on_server(dialog_id, log_event.is_blocked_, log_event.is_blocked_for_stories_,
                                           event.id_);
        break;
      }
      case LogEvent::HandlerType::SaveDialogDraftMessageOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SaveDialogDraftMessageOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "SaveDialogDraftMessageOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Write)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }
        d->save_draft_message_log_event_id.log_event_id = event.id_;

        save_dialog_draft_message_on_server(dialog_id);
        break;
      }
      case LogEvent::HandlerType::UpdateDialogNotificationSettingsOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        UpdateDialogNotificationSettingsOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "UpdateDialogNotificationSettingsOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }
        d->save_notification_settings_log_event_id.log_event_id = event.id_;

        update_dialog_notification_settings_on_server(dialog_id, true);
        break;
      }
      case LogEvent::HandlerType::ResetAllNotificationSettingsOnServer: {
        ResetAllNotificationSettingsOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        reset_all_notification_settings_on_server(event.id_);
        break;
      }
      case LogEvent::HandlerType::ToggleDialogReportSpamStateOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        ToggleDialogReportSpamStateOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "ToggleDialogReportSpamStateOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        toggle_dialog_report_spam_state_on_server(dialog_id, log_event.is_spam_dialog_, event.id_, Promise<Unit>());
        break;
      }
      case LogEvent::HandlerType::SetDialogFolderIdOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SetDialogFolderIdOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dialog *d = get_dialog_force(dialog_id, "SetDialogFolderIdOnServerLogEvent");
        if (d == nullptr || !td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }
        d->set_folder_id_log_event_id.log_event_id = event.id_;

        set_dialog_folder_id(d, log_event.folder_id_);

        set_dialog_folder_id_on_server(dialog_id, true);
        break;
      }
      case LogEvent::HandlerType::RegetDialog: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        RegetDialogLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.dialog_id_;
        Dependencies dependencies;
        dependencies.add_dialog_and_dependencies(dialog_id);
        dependencies.resolve_force(td_, "RegetDialogLogEvent", true);  // dialog itself may not exist

        if (!td_->dialog_manager_->have_input_peer(dialog_id, AccessRights::Read)) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        send_get_dialog_query(dialog_id, Auto(), event.id_, "RegetDialogLogEvent");
        break;
      }
      case LogEvent::HandlerType::UnpinAllDialogMessagesOnServer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        UnpinAllDialogMessagesOnServerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        unpin_all_dialog_messages_on_server(log_event.dialog_id_, event.id_, Auto());
        break;
      }
      case LogEvent::HandlerType::GetChannelDifference: {
        if (td_->ignore_background_updates()) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        GetChannelDifferenceLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        DialogId dialog_id(log_event.channel_id);
        if (dialog_id.get_type() != DialogType::Channel) {
          LOG(ERROR) << "Trying to run GetChannelDifference in " << dialog_id;
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }
        LOG(INFO) << "Continue to run getChannelDifference in " << dialog_id;
        get_channel_difference_to_log_event_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),
            false, "LogEvent::HandlerType::GetChannelDifference");
        break;
      }
      default:
        LOG(FATAL) << "Unsupported log event type " << event.type_;
    }
  }
}

Status MessagesManager::add_recently_found_dialog(DialogId dialog_id) {
  if (!have_dialog_force(dialog_id, "add_recently_found_dialog")) {
    return Status::Error(400, "Chat not found");
  }
  recently_found_dialogs_.add_dialog(dialog_id);
  return Status::OK();
}

Status MessagesManager::remove_recently_found_dialog(DialogId dialog_id) {
  if (!have_dialog_force(dialog_id, "remove_recently_found_dialog")) {
    return Status::Error(400, "Chat not found");
  }
  recently_found_dialogs_.remove_dialog(dialog_id);
  return Status::OK();
}

void MessagesManager::clear_recently_found_dialogs() {
  recently_found_dialogs_.clear_dialogs();
}

void MessagesManager::suffix_load_loop(const Dialog *d, SuffixLoadQueries *queries) {
  CHECK(queries != nullptr);
  if (queries->suffix_load_has_query_) {
    return;
  }
  if (queries->suffix_load_queries_.empty()) {
    return;
  }
  CHECK(!queries->suffix_load_done_);

  CHECK(d != nullptr);
  auto dialog_id = d->dialog_id;
  auto from_message_id = queries->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);
  });
  queries->suffix_load_has_query_ = true;
  queries->suffix_load_query_message_id_ = from_message_id;
  get_history_impl(d, from_message_id, -1, MAX_GET_HISTORY, true, true, std::move(promise), "suffix_load_loop");
}

void MessagesManager::suffix_load_update_first_message_id(const Dialog *d, SuffixLoadQueries *queries) {
  CHECK(d != nullptr);
  CHECK(queries != nullptr);
  if (!queries->suffix_load_first_message_id_.is_valid()) {
    if (!d->last_message_id.is_valid()) {
      return;
    }

    queries->suffix_load_first_message_id_ = d->last_message_id;
  }
  auto it = d->ordered_messages.get_const_iterator(queries->suffix_load_first_message_id_);
  CHECK(*it != nullptr);
  CHECK((*it)->get_message_id() == queries->suffix_load_first_message_id_);
  while (true) {
    --it;
    if (*it == nullptr) {
      break;
    }
    queries->suffix_load_first_message_id_ = (*it)->get_message_id();
  }
}

void MessagesManager::suffix_load_query_ready(DialogId dialog_id) {
  auto *queries = dialog_suffix_load_queries_[dialog_id].get();
  CHECK(queries != nullptr);
  CHECK(queries->suffix_load_has_query_);

  LOG(INFO) << "Finished suffix load query in " << dialog_id;
  auto *d = get_dialog(dialog_id);
  bool is_unchanged = queries->suffix_load_first_message_id_ == queries->suffix_load_query_message_id_;
  suffix_load_update_first_message_id(d, queries);
  if (is_unchanged && queries->suffix_load_first_message_id_ == queries->suffix_load_query_message_id_) {
    LOG(INFO) << "Finished suffix load in " << dialog_id;
    queries->suffix_load_done_ = true;
  }
  queries->suffix_load_has_query_ = false;

  // Remove ready queries
  auto *m = get_message_force(d, queries->suffix_load_first_message_id_, "suffix_load_query_ready");
  auto ready_it = std::partition(queries->suffix_load_queries_.begin(), queries->suffix_load_queries_.end(),
                                 [&](auto &value) { return !(queries->suffix_load_done_ || value.second(m)); });
  for (auto it = ready_it; it != queries->suffix_load_queries_.end(); ++it) {
    it->first.set_value(Unit());
  }
  queries->suffix_load_queries_.erase(ready_it, queries->suffix_load_queries_.end());

  suffix_load_loop(d, queries);
}

void MessagesManager::suffix_load_add_query(Dialog *d,
                                            std::pair<Promise<Unit>, std::function<bool(const Message *)>> query) {
  CHECK(!td_->auth_manager_->is_bot());
  auto &queries = dialog_suffix_load_queries_[d->dialog_id];
  if (queries == nullptr) {
    queries = make_unique<SuffixLoadQueries>();
  }
  suffix_load_update_first_message_id(d, queries.get());
  auto *m = get_message_force(d, queries->suffix_load_first_message_id_, "suffix_load_add_query");
  if (queries->suffix_load_done_ || query.second(m)) {
    query.first.set_value(Unit());
  } else {
    queries->suffix_load_queries_.emplace_back(std::move(query));
    suffix_load_loop(d, queries.get());
  }
}

void MessagesManager::suffix_load_till_date(Dialog *d, int32 date, Promise<Unit> 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<Unit> 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 < message_id;
  };
  suffix_load_add_query(d, std::make_pair(std::move(promise), std::move(condition)));
}

void MessagesManager::set_poll_answer(MessageFullId message_full_id, vector<int32> &&option_ids,
                                      Promise<Unit> &&promise) {
  auto m = get_message_force(message_full_id, "set_poll_answer");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }
  if (!td_->dialog_manager_->have_input_peer(message_full_id.get_dialog_id(), AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }
  if (m->content->get_type() != MessageContentType::Poll) {
    return promise.set_error(Status::Error(400, "Message is not a poll"));
  }
  if (m->message_id.is_scheduled()) {
    return promise.set_error(Status::Error(400, "Can't answer polls from scheduled messages"));
  }
  if (!m->message_id.is_server()) {
    return promise.set_error(Status::Error(400, "Poll can't be answered"));
  }

  set_message_content_poll_answer(td_, m->content.get(), message_full_id, std::move(option_ids), std::move(promise));
}

void MessagesManager::get_poll_voters(MessageFullId message_full_id, int32 option_id, int32 offset, int32 limit,
                                      Promise<td_api::object_ptr<td_api::messageSenders>> &&promise) {
  auto m = get_message_force(message_full_id, "get_poll_voters");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }
  if (!td_->dialog_manager_->have_input_peer(message_full_id.get_dialog_id(), AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }
  if (m->content->get_type() != MessageContentType::Poll) {
    return promise.set_error(Status::Error(400, "Message is not a poll"));
  }
  if (m->message_id.is_scheduled()) {
    return promise.set_error(Status::Error(400, "Can't get poll results from scheduled messages"));
  }
  if (!m->message_id.is_server()) {
    return promise.set_error(Status::Error(400, "Poll results can't be received"));
  }

  get_message_content_poll_voters(td_, m->content.get(), message_full_id, option_id, offset, limit, std::move(promise));
}

void MessagesManager::stop_poll(MessageFullId message_full_id, td_api::object_ptr<td_api::ReplyMarkup> &&reply_markup,
                                Promise<Unit> &&promise) {
  auto m = get_message_force(message_full_id, "stop_poll");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }
  if (!td_->dialog_manager_->have_input_peer(message_full_id.get_dialog_id(), AccessRights::Read)) {
    return promise.set_error(Status::Error(400, "Can't access the chat"));
  }
  if (m->content->get_type() != MessageContentType::Poll) {
    return promise.set_error(Status::Error(400, "Message is not a poll"));
  }
  if (get_message_content_poll_is_closed(td_, m->content.get())) {
    return promise.set_error(Status::Error(400, "Poll has already been closed"));
  }
  if (!can_edit_message(message_full_id.get_dialog_id(), m, true)) {
    return promise.set_error(Status::Error(400, "Poll can't be stopped"));
  }
  if (m->message_id.is_scheduled()) {
    return promise.set_error(Status::Error(400, "Can't stop polls from scheduled messages"));
  }
  if (!m->message_id.is_server()) {
    return promise.set_error(Status::Error(400, "Poll can't be stopped"));
  }

  auto r_new_reply_markup = get_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(), true, false,
                                             has_message_sender_user_id(message_full_id.get_dialog_id(), m));
  if (r_new_reply_markup.is_error()) {
    return promise.set_error(r_new_reply_markup.move_as_error());
  }

  stop_message_content_poll(td_, m->content.get(), message_full_id, r_new_reply_markup.move_as_ok(),
                            std::move(promise));
}

Result<ServerMessageId> MessagesManager::get_invoice_message_id(MessageFullId message_full_id) {
  auto m = get_message_force(message_full_id, "get_invoice_message_id");
  if (m == nullptr) {
    return Status::Error(400, "Message not found");
  }
  if (m->content->get_type() != MessageContentType::Invoice) {
    return Status::Error(400, "Message has no invoice");
  }
  if (m->message_id.is_scheduled()) {
    return Status::Error(400, "Wrong scheduled message identifier");
  }
  if (!m->message_id.is_server()) {
    return Status::Error(400, "Wrong message identifier");
  }
  if (m->reply_markup == nullptr || m->reply_markup->inline_keyboard.empty() ||
      m->reply_markup->inline_keyboard[0].empty() ||
      m->reply_markup->inline_keyboard[0][0].type != InlineKeyboardButton::Type::Buy) {
    return Status::Error(400, "Message has no Pay button");
  }

  return m->message_id.get_server_message_id();
}

Result<ServerMessageId> MessagesManager::get_payment_successful_message_id(MessageFullId message_full_id) {
  auto m = get_message_force(message_full_id, "get_payment_successful_message_id");
  if (m == nullptr) {
    return Status::Error(400, "Message not found");
  }
  if (m->content->get_type() != MessageContentType::PaymentSuccessful) {
    return Status::Error(400, "Message has wrong type");
  }
  if (m->message_id.is_scheduled()) {
    return Status::Error(400, "Wrong scheduled message identifier");
  }
  if (!m->message_id.is_server()) {
    return Status::Error(400, "Wrong message identifier");
  }

  return m->message_id.get_server_message_id();
}

Result<ServerMessageId> MessagesManager::get_giveaway_message_id(MessageFullId message_full_id) {
  auto m = get_message_force(message_full_id, "get_giveaway_message_id");
  if (m == nullptr) {
    return Status::Error(400, "Message not found");
  }
  switch (m->content->get_type()) {
    case MessageContentType::Giveaway:
    case MessageContentType::GiveawayWinners:
      // ok
      break;
    default:
      return Status::Error(400, "Message has wrong type");
  }
  if (m->message_id.is_scheduled()) {
    return Status::Error(400, "Wrong scheduled message identifier");
  }
  if (!m->message_id.is_server()) {
    return Status::Error(400, "Wrong message identifier");
  }

  return m->message_id.get_server_message_id();
}

void MessagesManager::remove_sponsored_dialog() {
  set_sponsored_dialog(DialogId(), DialogSource());
}

void MessagesManager::on_get_sponsored_dialog(tl_object_ptr<telegram_api::Peer> peer, DialogSource source,
                                              vector<tl_object_ptr<telegram_api::User>> users,
                                              vector<tl_object_ptr<telegram_api::Chat>> chats) {
  CHECK(peer != nullptr);

  td_->contacts_manager_->on_get_users(std::move(users), "on_get_sponsored_dialog");
  td_->contacts_manager_->on_get_chats(std::move(chats), "on_get_sponsored_dialog");

  set_sponsored_dialog(DialogId(peer), std::move(source));
}

void MessagesManager::add_sponsored_dialog(const Dialog *d, DialogSource source) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  CHECK(!sponsored_dialog_id_.is_valid());
  sponsored_dialog_id_ = d->dialog_id;
  sponsored_dialog_source_ = std::move(source);

  // update last_pinned_dialog_date in any case, because all chats before SPONSORED_DIALOG_ORDER are known
  auto dialog_list_id = DialogListId(FolderId::main());
  auto *list = get_dialog_list(dialog_list_id);
  CHECK(list != nullptr);
  DialogDate max_dialog_date(SPONSORED_DIALOG_ORDER, d->dialog_id);
  if (list->last_pinned_dialog_date_ < max_dialog_date) {
    list->last_pinned_dialog_date_ = max_dialog_date;
    update_list_last_dialog_date(*list);
  }

  if (is_dialog_sponsored(d)) {
    send_update_chat_position(dialog_list_id, d, "add_sponsored_dialog");
    // the sponsored dialog must not be saved there
  }
}

void MessagesManager::save_sponsored_dialog() {
  if (!G()->use_message_database()) {
    return;
  }

  LOG(INFO) << "Save sponsored " << sponsored_dialog_id_ << " with source " << sponsored_dialog_source_;
  if (sponsored_dialog_id_.is_valid()) {
    G()->td_db()->get_binlog_pmc()->set(
        "sponsored_dialog_id",
        PSTRING() << sponsored_dialog_id_.get() << ' ' << sponsored_dialog_source_.DialogSource::serialize());
  } else {
    G()->td_db()->get_binlog_pmc()->erase("sponsored_dialog_id");
  }
}

void MessagesManager::set_sponsored_dialog(DialogId dialog_id, DialogSource source) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  LOG(INFO) << "Change sponsored chat from " << sponsored_dialog_id_ << " to " << dialog_id;
  if (removed_sponsored_dialog_id_.is_valid() && dialog_id == removed_sponsored_dialog_id_) {
    return;
  }

  if (sponsored_dialog_id_ == dialog_id) {
    if (sponsored_dialog_source_ != source) {
      CHECK(sponsored_dialog_id_.is_valid());
      sponsored_dialog_source_ = std::move(source);
      const Dialog *d = get_dialog(sponsored_dialog_id_);
      CHECK(d != nullptr);
      send_update_chat_position(DialogListId(FolderId::main()), d, "set_sponsored_dialog");
      save_sponsored_dialog();
    }
    return;
  }

  bool need_update_total_chat_count = false;
  if (sponsored_dialog_id_.is_valid()) {
    const Dialog *d = get_dialog(sponsored_dialog_id_);
    CHECK(d != nullptr);
    bool was_sponsored = is_dialog_sponsored(d);
    sponsored_dialog_id_ = DialogId();
    sponsored_dialog_source_ = DialogSource();
    if (was_sponsored) {
      send_update_chat_position(DialogListId(FolderId::main()), d, "set_sponsored_dialog 2");
      need_update_total_chat_count = true;
    }
  }

  if (dialog_id.is_valid()) {
    force_create_dialog(dialog_id, "set_sponsored_dialog_id");
    const Dialog *d = get_dialog(dialog_id);
    CHECK(d != nullptr);
    add_sponsored_dialog(d, std::move(source));
    if (is_dialog_sponsored(d)) {
      need_update_total_chat_count = !need_update_total_chat_count;
    }
  }

  if (need_update_total_chat_count) {
    auto dialog_list_id = DialogListId(FolderId::main());
    auto *list = get_dialog_list(dialog_list_id);
    CHECK(list != nullptr);
    if (list->is_dialog_unread_count_inited_) {
      send_update_unread_chat_count(*list, DialogId(), true, "set_sponsored_dialog_id");
    }
  }

  save_sponsored_dialog();
}

td_api::object_ptr<td_api::updateUnreadMessageCount> MessagesManager::get_update_unread_message_count_object(
    const DialogList &list) const {
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(list.is_message_unread_count_inited_);
  int32 unread_count = list.unread_message_total_count_;
  int32 unread_unmuted_count = list.unread_message_total_count_ - list.unread_message_muted_count_;
  CHECK(unread_count >= 0);
  CHECK(unread_unmuted_count >= 0);
  return td_api::make_object<td_api::updateUnreadMessageCount>(list.dialog_list_id.get_chat_list_object(), unread_count,
                                                               unread_unmuted_count);
}

td_api::object_ptr<td_api::updateUnreadChatCount> MessagesManager::get_update_unread_chat_count_object(
    const DialogList &list) const {
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(list.is_dialog_unread_count_inited_);
  int32 unread_count = list.unread_dialog_total_count_;
  int32 unread_unmuted_count = unread_count - list.unread_dialog_muted_count_;
  int32 unread_marked_count = list.unread_dialog_marked_count_;
  int32 unread_unmuted_marked_count = unread_marked_count - list.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);
  return td_api::make_object<td_api::updateUnreadChatCount>(
      list.dialog_list_id.get_chat_list_object(), get_dialog_total_count(list), 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()->use_message_database()) {
      for (const auto &it : dialog_lists_) {
        auto &list = it.second;
        if (list.is_message_unread_count_inited_) {
          updates.push_back(get_update_unread_message_count_object(list));
        }
        if (list.is_dialog_unread_count_inited_) {
          updates.push_back(get_update_unread_chat_count_object(list));
        }
      }
    }
  }

  vector<td_api::object_ptr<td_api::Update>> last_message_updates;
  dialogs_.foreach([&](const DialogId &dialog_id, const unique_ptr<Dialog> &dialog) {
    const Dialog *d = dialog.get();
    auto update = td_api::make_object<td_api::updateNewChat>(get_chat_object(d));
    if (update->chat_->last_message_ != nullptr) {
      last_message_updates.push_back(td_api::make_object<td_api::updateChatLastMessage>(
          get_chat_id_object(dialog_id, "updateChatLastMessage"), std::move(update->chat_->last_message_),
          get_chat_positions_object(d)));
    }
    updates.push_back(std::move(update));
  });
  append(updates, std::move(last_message_updates));
}

void MessagesManager::add_message_file_to_downloads(MessageFullId message_full_id, FileId file_id, int32 priority,
                                                    Promise<td_api::object_ptr<td_api::file>> promise) {
  auto m = get_message_force(message_full_id, "add_message_file_to_downloads");
  if (m == nullptr) {
    return promise.set_error(Status::Error(400, "Message not found"));
  }
  auto file_view = td_->file_manager_->get_file_view(file_id);
  if (file_view.empty()) {
    return promise.set_error(Status::Error(400, "File not found"));
  }
  file_id = file_view.get_main_file_id();
  bool is_found = false;
  for (auto message_file_id : get_message_file_ids(m)) {
    auto message_file_view = td_->file_manager_->get_file_view(message_file_id);
    CHECK(!message_file_view.empty());
    if (message_file_view.get_main_file_id() == file_id) {
      is_found = true;
    }
  }
  if (!is_found) {
    return promise.set_error(Status::Error(400, "Message has no specified file"));
  }
  if (m->message_id.is_yet_unsent()) {
    return promise.set_error(Status::Error(400, "Yet unsent messages can't be added to Downloads"));
  }
  auto search_text = get_message_search_text(m);
  auto file_source_id = get_message_file_source_id(message_full_id, true);
  CHECK(file_source_id.is_valid());
  send_closure(td_->download_manager_actor_, &DownloadManager::add_file, file_id, file_source_id,
               std::move(search_text), static_cast<int8>(priority), std::move(promise));
}

void MessagesManager::get_message_file_search_text(MessageFullId message_full_id, string unique_file_id,
                                                   Promise<string> promise) {
  auto m = get_message_force(message_full_id, "get_message_file_search_text");
  if (m == nullptr) {
    return promise.set_error(Status::Error(200, "Message not found"));
  }
  for (auto file_id : get_message_file_ids(m)) {
    auto file_view = td_->file_manager_->get_file_view(file_id);
    CHECK(!file_view.empty());
    if (file_view.get_unique_file_id() == unique_file_id) {
      return promise.set_value(get_message_search_text(m));
    }
  }
  return promise.set_error(Status::Error(200, "File not found"));
}

}  // namespace td
