This repository has been archived on 2020-05-25. You can view files and clone it, but cannot push or open issues or pull requests.
tdlib-fork/td/telegram/SecretChatActor.h
levlam 86666a8418 Cancel previous setEncryptedTyping query.
GitOrigin-RevId: dcebab6bae15c25def5f7c6bc15f03fffea66ef4
2018-03-13 11:08:56 +03:00

677 lines
25 KiB
C++

//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#pragma once
#include "td/telegram/secret_api.h"
#include "td/telegram/telegram_api.h"
#include "td/actor/actor.h"
#include "td/actor/PromiseFuture.h"
#include "td/mtproto/AuthKey.h"
#include "td/mtproto/crypto.h"
#include "td/mtproto/Transport.h"
#include "td/telegram/DhConfig.h"
#include "td/telegram/logevent/SecretChatEvent.h"
#include "td/telegram/MessageId.h"
#include "td/telegram/PtsManager.h"
#include "td/telegram/SecretChatDb.h"
#include "td/telegram/SecretChatId.h"
#include "td/telegram/UserId.h"
#include "td/utils/buffer.h"
#include "td/utils/ChangesProcessor.h"
#include "td/utils/Container.h"
#include "td/utils/format.h"
#include "td/utils/port/Clocks.h"
#include "td/utils/Slice.h"
#include "td/utils/Status.h"
#include "td/utils/StringBuilder.h"
#include "td/utils/Time.h"
#include "td/utils/tl_helpers.h"
#include <functional>
#include <map>
#include <memory>
#include <tuple>
#include <utility>
namespace td {
class BinlogInterface;
class DhCache;
class NetQueryCreator;
class SequenceDispatcher;
class SecretChatActor : public NetQueryCallback {
public:
// do not change DEFAULT_LAYER, unless all it's usages are fixed
enum : int32 { DEFAULT_LAYER = 46, VOICE_NOTES_LAYER = 66, MTPROTO_2_LAYER = 73, MY_LAYER = MTPROTO_2_LAYER };
class Context {
public:
Context() = default;
Context(const Context &) = delete;
Context &operator=(const Context &) = delete;
virtual ~Context() = default;
virtual DhCallback *dh_callback() = 0;
virtual BinlogInterface *binlog() = 0;
virtual SecretChatDb *secret_chat_db() = 0;
virtual NetQueryCreator &net_query_creator() = 0;
virtual std::shared_ptr<DhConfig> dh_config() = 0;
virtual void set_dh_config(std::shared_ptr<DhConfig> dh_config) = 0;
virtual bool get_config_option_boolean(const string &name) const = 0;
virtual int32 unix_time() = 0;
virtual bool close_flag() = 0;
// We don't want to expose the whole NetQueryDispatcher, MessagesManager and ContactsManager.
// So it is more clear which parts of MessagesManager is really used. And it is much easier to create tests.
virtual void send_net_query(NetQueryPtr query, ActorShared<NetQueryCallback> callback, bool ordered) = 0;
virtual void on_update_secret_chat(int64 access_hash, UserId user_id, SecretChatState state, bool is_outbound,
int32 ttl, int32 date, string key_hash, int32 layer) = 0;
// Promise must be set only after the update is processed.
//
// For example, one may set promise, after update was sent to binlog. It is ok, because SecretChatsActor will delete
// this update thought binlog too. So it wouldn't be deleted before update is saved.
// inbound messages
virtual void on_inbound_message(UserId user_id, MessageId message_id, int32 date,
tl_object_ptr<telegram_api::encryptedFile> file,
tl_object_ptr<secret_api::decryptedMessage> message, Promise<> promise) = 0;
virtual void on_delete_messages(std::vector<int64> random_id, Promise<> promise) = 0;
virtual void on_flush_history(MessageId message_id, Promise<> promise) = 0;
virtual void on_read_message(int64 random_id, Promise<> promise) = 0;
virtual void on_screenshot_taken(UserId user_id, MessageId message_id, int32 date, int64 random_id,
Promise<> promise) = 0;
virtual void on_set_ttl(UserId user_id, MessageId message_id, int32 date, int32 ttl, int64 random_id,
Promise<> promise) = 0;
// outbound messages
virtual void on_send_message_ack(int64 random_id) = 0;
virtual void on_send_message_ok(int64 random_id, MessageId message_id, int32 date,
tl_object_ptr<telegram_api::EncryptedFile> file, Promise<> promise) = 0;
virtual void on_send_message_error(int64 random_id, Status error, Promise<> promise) = 0;
};
SecretChatActor(int32 id, std::unique_ptr<Context> context, bool can_be_empty);
// First query to new chat must be on of this two
void update_chat(telegram_api::object_ptr<telegram_api::EncryptedChat> chat);
void create_chat(int32 user_id, int64 user_access_hash, int32 random_id, Promise<SecretChatId> promise);
void cancel_chat(Promise<> promise);
// Inbound messages
// Logevent is created by SecretChatsManager, because it must contain qts
void add_inbound_message(std::unique_ptr<logevent::InboundSecretMessage> message);
// Outbound messages
// Promise will be set just after correspoiding logevent will be SENT to binlog.
void send_message(tl_object_ptr<secret_api::DecryptedMessage> message,
tl_object_ptr<telegram_api::InputEncryptedFile> file, Promise<> promise);
void send_message_action(tl_object_ptr<secret_api::SendMessageAction> action);
void send_read_history(int32 date,
Promise<>); // no binlog event. TODO: Promise will be set after the net query is sent
void send_open_message(int64 random_id, Promise<>);
void delete_message(int64 random_id, Promise<> promise);
void delete_messages(std::vector<int64> random_ids, Promise<> promise);
void delete_all_messages(Promise<> promise);
void notify_screenshot_taken(Promise<> promise);
void send_set_ttl_message(int32 ttl, int64 random_id, Promise<> promise);
// Binlog replay interface
void replay_inbound_message(std::unique_ptr<logevent::InboundSecretMessage> message);
void replay_outbound_message(std::unique_ptr<logevent::OutboundSecretMessage> message);
void replay_close_chat(std::unique_ptr<logevent::CloseSecretChat> event);
void replay_create_chat(std::unique_ptr<logevent::CreateSecretChat> event);
void binlog_replay_finish();
private:
enum class State { Empty, SendRequest, SendAccept, WaitRequestResponse, WaitAcceptResponse, Ready, Closed };
enum { MAX_RESEND_COUNT = 1000 };
// We have git state that should be shynchronized with db.
// It is splitted into several parts because:
// 1. Some parts are BIG (auth_key, for example) and are rarely updated.
// 2. Other are frequently updated, so probably should be as small as possible.
// 3. Some parts must be updated atomically.
struct SeqNoState {
int32 message_id = 0;
int32 my_in_seq_no = 0;
int32 my_out_seq_no = 0;
int32 his_in_seq_no = 0;
int32 resend_end_seq_no = -1;
static Slice key() {
return Slice("state");
}
template <class StorerT>
void store(StorerT &storer) const {
storer.store_int(message_id);
storer.store_int(my_in_seq_no);
storer.store_int(my_out_seq_no);
storer.store_int(his_in_seq_no);
storer.store_int(resend_end_seq_no);
}
template <class ParserT>
void parse(ParserT &parser) {
message_id = parser.fetch_int();
my_in_seq_no = parser.fetch_int();
my_out_seq_no = parser.fetch_int();
his_in_seq_no = parser.fetch_int();
resend_end_seq_no = parser.fetch_int();
}
};
struct ConfigState {
int32 his_layer = 8;
int32 my_layer = 8;
int32 ttl = 0;
static Slice key() {
return Slice("config");
}
template <class StorerT>
void store(StorerT &storer) const {
storer.store_int(his_layer | HAS_FLAGS);
storer.store_int(ttl);
storer.store_int(my_layer);
//for future usage
BEGIN_STORE_FLAGS();
END_STORE_FLAGS();
}
template <class ParserT>
void parse(ParserT &parser) {
his_layer = parser.fetch_int();
ttl = parser.fetch_int();
bool has_flags = (his_layer & HAS_FLAGS) != 0;
if (has_flags) {
his_layer &= ~HAS_FLAGS;
my_layer = parser.fetch_int();
// for future usage
BEGIN_PARSE_FLAGS();
END_PARSE_FLAGS();
}
}
enum { HAS_FLAGS = 1 << 31 };
};
// PfsAction
struct PfsState {
enum State : int32 {
Empty,
WaitSendRequest,
SendRequest,
WaitRequestResponse,
WaitSendAccept,
SendAccept,
WaitAcceptResponse,
WaitSendCommit,
SendCommit
} state = Empty;
enum Flags : int32 { CanForgetOtherKey = 1 };
mtproto::AuthKey auth_key;
mtproto::AuthKey other_auth_key;
bool can_forget_other_key = true;
int32 message_id = 0; // to skip old actions
int32 wait_message_id = 0;
int64 exchange_id = 0;
int32 last_message_id = 0;
double last_timestamp = 0;
int32 last_out_seq_no = 0;
DhHandshake handshake;
static Slice key() {
return Slice("pfs_state");
}
template <class StorerT>
void store(StorerT &storer) const {
int32 flags = 0;
if (can_forget_other_key) {
flags |= CanForgetOtherKey;
}
storer.store_int(flags);
storer.store_int(state);
auth_key.store(storer);
other_auth_key.store(storer);
storer.store_int(message_id);
storer.store_long(exchange_id);
storer.store_int(last_message_id);
storer.store_long(static_cast<int64>((last_timestamp - Time::now() + Clocks::system()) * 1000000));
storer.store_int(last_out_seq_no);
handshake.store(storer);
}
template <class ParserT>
void parse(ParserT &parser) {
int32 flags = parser.fetch_int();
can_forget_other_key = (flags & CanForgetOtherKey) != 0;
state = static_cast<State>(parser.fetch_int());
auth_key.parse(parser);
other_auth_key.parse(parser);
message_id = parser.fetch_int();
exchange_id = parser.fetch_long();
last_message_id = parser.fetch_int();
last_timestamp = static_cast<double>(parser.fetch_long()) / 1000000 - Clocks::system() + Time::now();
if (last_timestamp > Time::now_cached()) {
last_timestamp = Time::now_cached();
}
last_out_seq_no = parser.fetch_int();
handshake.parse(parser);
}
};
friend StringBuilder &operator<<(StringBuilder &sb, const PfsState &state) {
return sb << "PfsState["
<< tag("state",
[&] {
switch (state.state) {
case PfsState::Empty:
return "Empty";
case PfsState::WaitSendRequest:
return "WaitSendRequest";
case PfsState::SendRequest:
return "SendRequest";
case PfsState::WaitRequestResponse:
return "WaitRequestResponse";
case PfsState::WaitSendAccept:
return "WaitSendAccept";
case PfsState::SendAccept:
return "SendAccept";
case PfsState::WaitAcceptResponse:
return "WaitAcceptResponse";
case PfsState::WaitSendCommit:
return "WaitSendCommit";
case PfsState::SendCommit:
return "SendCommit";
}
return "UNKNOWN";
}())
<< tag("message_id", state.message_id) << tag("auth_key", format::as_hex(state.auth_key.id()))
<< tag("last_message_id", state.last_message_id)
<< tag("other_auth_key", format::as_hex(state.other_auth_key.id()))
<< tag("can_forget", state.can_forget_other_key) << "]";
}
PfsState pfs_state_;
bool pfs_state_changed_ = false;
void on_outbound_action(secret_api::decryptedMessageActionSetMessageTTL &set_ttl);
void on_outbound_action(secret_api::decryptedMessageActionReadMessages &read_messages);
void on_outbound_action(secret_api::decryptedMessageActionDeleteMessages &delete_messages);
void on_outbound_action(secret_api::decryptedMessageActionScreenshotMessages &screenshot);
void on_outbound_action(secret_api::decryptedMessageActionFlushHistory &flush_history);
void on_outbound_action(secret_api::decryptedMessageActionResend &resend);
void on_outbound_action(secret_api::decryptedMessageActionNotifyLayer &notify_layer);
void on_outbound_action(secret_api::decryptedMessageActionTyping &typing);
Status on_inbound_action(secret_api::decryptedMessageActionSetMessageTTL &set_ttl);
Status on_inbound_action(secret_api::decryptedMessageActionReadMessages &read_messages);
Status on_inbound_action(secret_api::decryptedMessageActionDeleteMessages &delete_messages);
Status on_inbound_action(secret_api::decryptedMessageActionScreenshotMessages &screenshot);
Status on_inbound_action(secret_api::decryptedMessageActionFlushHistory &screenshot);
Status on_inbound_action(secret_api::decryptedMessageActionResend &resend);
Status on_inbound_action(secret_api::decryptedMessageActionNotifyLayer &notify_layer);
Status on_inbound_action(secret_api::decryptedMessageActionTyping &typing);
// Perfect Forward Secrecy
void on_outbound_action(secret_api::decryptedMessageActionRequestKey &request_key);
void on_outbound_action(secret_api::decryptedMessageActionAcceptKey &accept_key);
void on_outbound_action(secret_api::decryptedMessageActionAbortKey &abort_key);
void on_outbound_action(secret_api::decryptedMessageActionCommitKey &commit_key);
void on_outbound_action(secret_api::decryptedMessageActionNoop &noop);
Status on_inbound_action(secret_api::decryptedMessageActionRequestKey &request_key);
Status on_inbound_action(secret_api::decryptedMessageActionAcceptKey &accept_key);
Status on_inbound_action(secret_api::decryptedMessageActionAbortKey &abort_key);
Status on_inbound_action(secret_api::decryptedMessageActionCommitKey &commit_key);
Status on_inbound_action(secret_api::decryptedMessageActionNoop &noop);
Status on_inbound_action(secret_api::DecryptedMessageAction &action, int32 message_id);
void on_outbound_action(secret_api::DecryptedMessageAction &action, int32 message_id);
void request_new_key();
struct AuthState {
State state = State::Empty;
int x = -1;
string key_hash;
int32 id = 0;
int64 access_hash = 0;
int32 user_id = 0;
int64 user_access_hash = 0;
int32 random_id = 0;
int32 date = 0;
DhConfig dh_config;
DhHandshake handshake;
static Slice key() {
return Slice("auth_state");
}
template <class StorerT>
void store(StorerT &storer) const {
uint32 flags = 0;
bool date_flag = date != 0;
bool key_hash_flag = true;
if (date_flag) {
flags |= 1;
}
if (key_hash_flag) {
flags |= 2;
}
storer.store_int((flags << 8) | static_cast<int32>(state));
storer.store_int(x);
storer.store_int(id);
storer.store_long(access_hash);
storer.store_int(user_id);
storer.store_long(user_access_hash);
storer.store_int(random_id);
if (date_flag) {
storer.store_int(date);
}
if (key_hash_flag) {
storer.store_string(key_hash);
}
dh_config.store(storer);
if (state == State::SendRequest || state == State::WaitRequestResponse) {
handshake.store(storer);
}
}
template <class ParserT>
void parse(ParserT &parser) {
uint32 tmp = parser.fetch_int();
state = static_cast<State>(tmp & 255);
uint32 flags = tmp >> 8;
bool date_flag = (flags & 1) != 0;
bool key_hash_flag = (flags & 2) != 0;
x = parser.fetch_int();
id = parser.fetch_int();
access_hash = parser.fetch_long();
user_id = parser.fetch_int();
user_access_hash = parser.fetch_long();
random_id = parser.fetch_int();
if (date_flag) {
date = parser.fetch_int();
}
if (key_hash_flag) {
key_hash = parser.template fetch_string<std::string>();
}
dh_config.parse(parser);
if (state == State::SendRequest || state == State::WaitRequestResponse) {
handshake.parse(parser);
}
}
};
std::shared_ptr<SecretChatDb> db_;
std::unique_ptr<Context> context_;
bool binlog_replay_finish_flag_ = false;
bool close_flag_ = false;
LogEvent::Id close_logevent_id_ = 0;
LogEvent::Id create_logevent_id_ = 0;
enum class QueryType : uint8 { DhConfig, EncryptedChat, Message, Ignore, DiscardEncryption };
bool can_be_empty_;
AuthState auth_state_;
ConfigState config_state_;
// Turns out, that all changes should be made made through StateChange.
//
// The problem is the time between the moment we made decision about change and
// the moment we actually apply the change to memory.
// We may accept some other change during that time, and there goes our problem
// The reason for the change may already be invalid. So we should somehow recheck change, that
// is already written to binlog, and apply it only if necessary.
// This is completly flawed.
// (A-start_save_to_binlog ----> B-start_save_to_binlog+change_memory ----> A-finish_save_to_binlog+surprise)
//
// Instead I suggest general solution that is already used with SeqNoState and Qts
// 1. We APPLY CHANGE to memory immidiately AFTER correspoding EVENT is SENT to the binlog.
// 2. We SEND CHANGE to db only after correspoiding EVENT is SAVED to the binlog.
// 3. The we are able to ERASE EVENT just AFTER the CHANGE is SAVED to the binlog.
//
// Actually the change will be saved do binlog too.
// So we can do it immidiatelly after EVENT is SENT to the binlog, because SEND CHANGE and ERASE EVENT will be
// ordered automatically.
//
// We will use common ChangeProcessor for all changes (inside one SecretChatActor).
// So all changes will be saved in exactly the same order as they are applied
template <class StateT>
class Change {
public:
Change() = default;
explicit operator bool() const {
return !data.empty();
}
explicit Change(const StateT &state) {
data = serialize(state);
message_id = state.message_id;
}
template <class StorerT>
void store(StorerT &storer) const {
// NB: rely that storer will the same as in serialize
storer.store_slice(data);
}
static Slice key() {
return StateT::key();
}
friend StringBuilder &operator<<(StringBuilder &sb, const Change<StateT> &change) {
if (change) {
StateT state;
unserialize(state, change.data).ensure();
return sb << state;
}
return sb;
}
int32 message_id;
private:
std::string data;
};
// SeqNoState
using SeqNoStateChange = Change<SeqNoState>;
using PfsStateChange = Change<PfsState>;
struct StateChange {
// TODO(perf): Less allocations, please? May be BufferSlice instead of std::string?
SeqNoStateChange seq_no_state_change;
PfsStateChange pfs_state_change;
Promise<Unit> save_changes_finish;
};
ChangesProcessor<StateChange> changes_processor_;
int32 saved_pfs_state_message_id_;
SeqNoState seq_no_state_;
bool seq_no_state_changed_ = false;
int32 last_binlog_message_id_ = -1;
Status check_seq_no(int in_seq_no, int out_seq_no) TD_WARN_UNUSED_RESULT;
void on_his_in_seq_no_updated();
void on_seq_no_state_changed();
template <class T>
void update_seq_no_state(const T &new_seq_no_state);
void on_pfs_state_changed();
Promise<> add_changes(Promise<> save_changes_finish = Promise<>());
// called only via Promise
void on_save_changes_start(ChangesProcessor<StateChange>::Id save_changes_token);
// InboundMessage
struct InboundMessageState {
bool save_changes_finish = false;
bool save_message_finish = false;
LogEvent::Id logevent_id = 0;
int32 message_id;
};
Container<InboundMessageState> inbound_message_states_;
std::map<int32, std::unique_ptr<logevent::InboundSecretMessage>> pending_inbound_messages_;
Result<std::tuple<uint64, BufferSlice, int32>> decrypt(BufferSlice &encrypted_message);
Status do_inbound_message_encrypted(std::unique_ptr<logevent::InboundSecretMessage> message);
Status do_inbound_message_decrypted_unchecked(std::unique_ptr<logevent::InboundSecretMessage> message);
Status do_inbound_message_decrypted(std::unique_ptr<logevent::InboundSecretMessage> message);
Status do_inbound_message_decrypted_pending(std::unique_ptr<logevent::InboundSecretMessage> message);
void on_inbound_save_message_finish(uint64 state_id);
void on_inbound_save_changes_finish(uint64 state_id);
void inbound_loop(InboundMessageState *state, uint64 state_id);
// OutboundMessage
struct OutboundMessageState {
std::unique_ptr<logevent::OutboundSecretMessage> message;
Promise<> outer_send_message_finish;
Promise<> send_message_finish;
bool save_changes_finish_flag = false;
bool send_message_finish_flag = false;
bool ack_flag = false;
uint64 net_query_id = 0;
NetQueryRef net_query_ref;
bool net_query_may_fail = false;
std::function<void(Promise<>)> send_result_;
};
std::map<uint64, uint64> random_id_to_outbound_message_state_token_;
std::map<int32, uint64> out_seq_no_to_outbound_message_state_token_;
Container<OutboundMessageState> outbound_message_states_;
NetQueryRef set_typing_query_;
enum SendFlag {
None = 0,
External = 1,
Push = 2,
};
void send_action(tl_object_ptr<secret_api::DecryptedMessageAction> action, int32 flags, Promise<> promise);
void send_message_impl(tl_object_ptr<secret_api::DecryptedMessage> message,
tl_object_ptr<telegram_api::InputEncryptedFile> file, int32 flags, Promise<> promise);
void do_outbound_message_impl(std::unique_ptr<logevent::OutboundSecretMessage>, Promise<> promise);
Result<BufferSlice> create_encrypted_message(int32 layer, int32 my_in_seq_no, int32 my_out_seq_no,
tl_object_ptr<secret_api::DecryptedMessage> &message);
NetQueryPtr create_net_query(const logevent::OutboundSecretMessage &message);
void outbound_resend(uint64 state_id);
Status outbound_rewrite_with_empty(uint64 state_id);
void on_outbound_send_message_start(uint64 state_id);
void on_outbound_send_message_result(NetQueryPtr query, Promise<NetQueryPtr> resend_promise);
void on_outbound_send_message_error(uint64 state_id, Status error, Promise<NetQueryPtr> resend_promise);
void on_outbound_send_message_finish(uint64 state_id);
void on_outbound_save_changes_finish(uint64 state_id);
void on_outbound_ack(uint64 state_id);
void on_outbound_outer_send_message_promise(uint64 state_id, Promise<> promise);
void outbound_loop(OutboundMessageState *state, uint64 state_id);
// DiscardEncryption
void on_fatal_error(Status status);
void do_close_chat_impl(std::unique_ptr<logevent::CloseSecretChat> event);
void on_discard_encryption_result(NetQueryPtr result);
// Other
template <class T>
Status save_common_info(T &update);
int32 current_layer() const {
int32 layer = MY_LAYER;
if (config_state_.his_layer < layer) {
layer = config_state_.his_layer;
}
if (layer < DEFAULT_LAYER) {
layer = DEFAULT_LAYER;
}
return layer;
}
void ask_on_binlog_replay_finish();
void check_status(Status status);
void start_up() override;
void loop() override;
Status do_loop();
void tear_down() override;
void on_result_resendable(NetQueryPtr net_query, Promise<NetQueryPtr> promise) override;
Status run_auth();
void run_pfs();
void run_fill_gaps();
void on_send_message_ack(int64 random_id);
Status on_delete_messages(const vector<int64> &random_ids);
Status on_flush_history(int32 last_message_id);
telegram_api::object_ptr<telegram_api::inputUser> get_input_user();
telegram_api::object_ptr<telegram_api::inputEncryptedChat> get_input_chat();
Status on_update_chat(telegram_api::encryptedChatRequested &update) TD_WARN_UNUSED_RESULT;
Status on_update_chat(telegram_api::encryptedChatEmpty &update) TD_WARN_UNUSED_RESULT;
Status on_update_chat(telegram_api::encryptedChatWaiting &update) TD_WARN_UNUSED_RESULT;
Status on_update_chat(telegram_api::encryptedChat &update) TD_WARN_UNUSED_RESULT;
Status on_update_chat(telegram_api::encryptedChatDiscarded &update) TD_WARN_UNUSED_RESULT;
Status on_update_chat(NetQueryPtr query) TD_WARN_UNUSED_RESULT;
Status on_update_chat(telegram_api::object_ptr<telegram_api::EncryptedChat> chat) TD_WARN_UNUSED_RESULT;
void on_promise_error(Status error, string desc);
void get_dh_config();
Status on_dh_config(NetQueryPtr query) TD_WARN_UNUSED_RESULT;
void on_dh_config(telegram_api::messages_dhConfigNotModified &dh_not_modified);
void on_dh_config(telegram_api::messages_dhConfig &dh);
void do_create_chat_impl(std::unique_ptr<logevent::CreateSecretChat> event);
SecretChatId get_secret_chat_id() {
return SecretChatId(auth_state_.id);
}
UserId get_user_id() {
return UserId(auth_state_.user_id);
}
void send_update_ttl(int32 ttl);
void send_update_secret_chat();
void calc_key_hash();
friend inline StringBuilder &operator<<(StringBuilder &sb, const SecretChatActor::SeqNoState &state) {
return sb << "[" << tag("my_in_seq_no", state.my_in_seq_no) << tag("my_out_seq_no", state.my_out_seq_no)
<< tag("his_in_seq_no", state.his_in_seq_no) << "]";
}
};
} // namespace td