Added initial ability to export topic history.
This commit is contained in:
@@ -1735,6 +1735,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_profile_block_user" = "Block user";
|
||||
"lng_profile_unblock_user" = "Unblock user";
|
||||
"lng_profile_export_chat" = "Export chat history";
|
||||
"lng_profile_export_topic" = "Export topic history";
|
||||
"lng_profile_gift_premium" = "Gift Premium";
|
||||
"lng_media_selected_photo#one" = "{count} Photo";
|
||||
"lng_media_selected_photo#other" = "{count} Photos";
|
||||
@@ -6312,6 +6313,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_export_option_other" = "Miscellaneous data";
|
||||
"lng_export_option_other_about" = "Other types of data not mentioned above (beta).";
|
||||
"lng_export_header_chats" = "Chat export settings";
|
||||
"lng_export_header_topic" = "Topic export settings";
|
||||
"lng_export_option_personal_chats" = "Personal chats";
|
||||
"lng_export_option_bot_chats" = "Bot chats";
|
||||
"lng_export_option_private_groups" = "Private groups";
|
||||
|
||||
@@ -225,25 +225,41 @@ struct ApiWrap::DialogsProcess : ChatsProcess {
|
||||
MTPInputPeer offsetPeer = MTP_inputPeerEmpty();
|
||||
};
|
||||
|
||||
struct ApiWrap::ChatProcess {
|
||||
Data::DialogInfo info;
|
||||
|
||||
FnMut<bool(const Data::DialogInfo &)> start;
|
||||
struct ApiWrap::AbstractMessagesProcess {
|
||||
Fn<bool(DownloadProgress)> fileProgress;
|
||||
Fn<bool(Data::MessagesSlice&&)> handleSlice;
|
||||
FnMut<void()> done;
|
||||
|
||||
FnMut<void(MTPmessages_Messages&&)> requestDone;
|
||||
|
||||
int localSplitIndex = 0;
|
||||
int32 largestIdPlusOne = 1;
|
||||
|
||||
Data::ParseMediaContext context;
|
||||
std::optional<Data::MessagesSlice> slice;
|
||||
bool lastSlice = false;
|
||||
int fileIndex = 0;
|
||||
};
|
||||
|
||||
struct ApiWrap::ChatProcess : AbstractMessagesProcess {
|
||||
Data::DialogInfo info;
|
||||
|
||||
FnMut<bool(const Data::DialogInfo &)> start;
|
||||
|
||||
int localSplitIndex = 0;
|
||||
int32 largestIdPlusOne = 1;
|
||||
};
|
||||
|
||||
struct ApiWrap::TopicProcess : AbstractMessagesProcess {
|
||||
PeerId peerId = 0;
|
||||
MTPInputPeer inputPeer;
|
||||
int32 topicRootId = 0;
|
||||
QString relativePath;
|
||||
|
||||
FnMut<bool(int count)> start;
|
||||
|
||||
int32 offsetId = 0;
|
||||
int totalCount = 0;
|
||||
int processedCount = 0;
|
||||
};
|
||||
|
||||
|
||||
template <typename Request>
|
||||
class ApiWrap::RequestBuilder {
|
||||
@@ -2080,13 +2096,18 @@ std::optional<QByteArray> ApiWrap::getCustomEmoji(QByteArray &data) {
|
||||
}
|
||||
auto &file = i->second.file;
|
||||
const auto fileProgress = [=](FileProgress value) {
|
||||
return loadMessageEmojiProgress(value);
|
||||
if (_chatProcess) {
|
||||
return loadMessageEmojiProgress(value);
|
||||
} else if (_topicProcess) {
|
||||
return loadTopicEmojiProgress(value);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auto ready = processFileLoad(
|
||||
file,
|
||||
{ .customEmojiId = id },
|
||||
fileProgress,
|
||||
[=](const QString &path) { loadMessageEmojiDone(id, path); });
|
||||
[=](const QString &path) { loadCustomEmojiDone(id, path); });
|
||||
if (!ready) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -2262,6 +2283,36 @@ void ApiWrap::loadMessageEmojiDone(uint64 id, const QString &relativePath) {
|
||||
loadNextMessageFile();
|
||||
}
|
||||
|
||||
bool ApiWrap::loadTopicEmojiProgress(FileProgress progress) {
|
||||
Expects(_fileProcess != nullptr);
|
||||
Expects(_topicProcess != nullptr);
|
||||
Expects(_topicProcess->slice.has_value());
|
||||
Expects((_topicProcess->fileIndex >= 0)
|
||||
&& (_topicProcess->fileIndex < _topicProcess->slice->list.size()));
|
||||
|
||||
return _topicProcess->fileProgress(DownloadProgress{
|
||||
.randomId = _fileProcess->randomId,
|
||||
.path = _fileProcess->relativePath,
|
||||
.itemIndex = _topicProcess->fileIndex,
|
||||
.ready = progress.ready,
|
||||
.total = progress.total });
|
||||
}
|
||||
|
||||
void ApiWrap::loadCustomEmojiDone(uint64 id, const QString &relativePath) {
|
||||
const auto i = _resolvedCustomEmoji.find(id);
|
||||
if (i != end(_resolvedCustomEmoji)) {
|
||||
i->second.file.relativePath = relativePath;
|
||||
if (relativePath.isEmpty()) {
|
||||
i->second.file.skipReason = Data::File::SkipReason::Unavailable;
|
||||
}
|
||||
}
|
||||
if (_chatProcess) {
|
||||
loadNextMessageFile();
|
||||
} else if (_topicProcess) {
|
||||
loadNextTopicMessageFile();
|
||||
}
|
||||
}
|
||||
|
||||
void ApiWrap::finishMessages() {
|
||||
Expects(_chatProcess != nullptr);
|
||||
Expects(!_chatProcess->slice.has_value());
|
||||
@@ -2270,6 +2321,316 @@ void ApiWrap::finishMessages() {
|
||||
process->done();
|
||||
}
|
||||
|
||||
void ApiWrap::requestTopicMessages(
|
||||
PeerId peerId,
|
||||
MTPInputPeer inputPeer,
|
||||
int32 topicRootId,
|
||||
FnMut<bool(int count)> start,
|
||||
Fn<bool(DownloadProgress)> progress,
|
||||
Fn<bool(Data::MessagesSlice&&)> slice,
|
||||
FnMut<void()> done) {
|
||||
Expects(_topicProcess == nullptr);
|
||||
Expects(_selfId.has_value());
|
||||
|
||||
_topicProcess = std::make_unique<TopicProcess>();
|
||||
_topicProcess->context.selfPeerId = peerFromUser(*_selfId);
|
||||
_topicProcess->peerId = peerId;
|
||||
_topicProcess->inputPeer = inputPeer;
|
||||
_topicProcess->topicRootId = topicRootId;
|
||||
_topicProcess->relativePath = "chats/chat_"
|
||||
+ QString::number(peerId.value)
|
||||
+ "/topic_"
|
||||
+ QString::number(topicRootId)
|
||||
+ "/";
|
||||
_topicProcess->start = std::move(start);
|
||||
_topicProcess->fileProgress = std::move(progress);
|
||||
_topicProcess->handleSlice = std::move(slice);
|
||||
_topicProcess->done = std::move(done);
|
||||
|
||||
mainRequest(MTPchannels_GetMessages(
|
||||
MTP_inputChannel(
|
||||
inputPeer.c_inputPeerChannel().vchannel_id(),
|
||||
inputPeer.c_inputPeerChannel().vaccess_hash()),
|
||||
MTP_vector<MTPInputMessage>(
|
||||
1,
|
||||
MTP_inputMessageID(MTP_int(topicRootId)))
|
||||
)).done([=](const MTPmessages_Messages &rootResult) {
|
||||
Expects(_topicProcess != nullptr);
|
||||
|
||||
auto rootSlice = rootResult.match([&](
|
||||
const MTPDmessages_messagesNotModified &) {
|
||||
return Data::MessagesSlice();
|
||||
}, [&](const auto &data) {
|
||||
return Data::ParseMessagesSlice(
|
||||
_topicProcess->context,
|
||||
data.vmessages(),
|
||||
data.vusers(),
|
||||
data.vchats(),
|
||||
_topicProcess->relativePath);
|
||||
});
|
||||
|
||||
auto rootSlicePtr = std::make_shared<Data::MessagesSlice>(
|
||||
std::move(rootSlice));
|
||||
|
||||
requestTopicReplies(
|
||||
0,
|
||||
0,
|
||||
kMessagesSliceLimit,
|
||||
[=](const MTPmessages_Messages &result) {
|
||||
Expects(_topicProcess != nullptr);
|
||||
|
||||
const auto count = result.match(
|
||||
[](const MTPDmessages_messages &data) {
|
||||
return int(data.vmessages().v.size());
|
||||
}, [](const MTPDmessages_messagesSlice &data) {
|
||||
return data.vcount().v;
|
||||
}, [](const MTPDmessages_channelMessages &data) {
|
||||
return data.vcount().v;
|
||||
}, [](const MTPDmessages_messagesNotModified &data) {
|
||||
return -1;
|
||||
});
|
||||
if (count < 0) {
|
||||
error("Unexpected messagesNotModified received.");
|
||||
return;
|
||||
}
|
||||
_topicProcess->totalCount = count;
|
||||
if (!_topicProcess->start(count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rootSlicePtr->list.empty()) {
|
||||
collectMessagesCustomEmoji(*rootSlicePtr);
|
||||
_topicProcess->slice = std::move(*rootSlicePtr);
|
||||
_topicProcess->fileIndex = 0;
|
||||
resolveTopicCustomEmoji();
|
||||
return;
|
||||
}
|
||||
|
||||
requestTopicMessagesSlice();
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ApiWrap::requestTopicMessagesSlice() {
|
||||
Expects(_topicProcess != nullptr);
|
||||
|
||||
const auto offsetId = (_topicProcess->offsetId == 0)
|
||||
? 1
|
||||
: (_topicProcess->offsetId + 1);
|
||||
requestTopicReplies(
|
||||
offsetId,
|
||||
-kMessagesSliceLimit,
|
||||
kMessagesSliceLimit,
|
||||
[=](const MTPmessages_Messages &result) {
|
||||
Expects(_topicProcess != nullptr);
|
||||
|
||||
result.match([&](const MTPDmessages_messagesNotModified &data) {
|
||||
error("Unexpected messagesNotModified received.");
|
||||
}, [&](const auto &data) {
|
||||
if constexpr (MTPDmessages_messages::Is<decltype(data)>()) {
|
||||
_topicProcess->lastSlice = true;
|
||||
}
|
||||
auto slice = Data::ParseMessagesSlice(
|
||||
_topicProcess->context,
|
||||
data.vmessages(),
|
||||
data.vusers(),
|
||||
data.vchats(),
|
||||
_topicProcess->relativePath);
|
||||
if (slice.list.empty()) {
|
||||
_topicProcess->lastSlice = true;
|
||||
}
|
||||
loadTopicMessagesFiles(std::move(slice));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void ApiWrap::requestTopicReplies(
|
||||
int offsetId,
|
||||
int addOffset,
|
||||
int limit,
|
||||
FnMut<void(MTPmessages_Messages&&)> done) {
|
||||
Expects(_topicProcess != nullptr);
|
||||
|
||||
_topicProcess->requestDone = std::move(done);
|
||||
const auto doneHandler = [=](MTPmessages_Messages &&result) {
|
||||
Expects(_topicProcess != nullptr);
|
||||
base::take(_topicProcess->requestDone)(std::move(result));
|
||||
};
|
||||
|
||||
mainRequest(MTPmessages_GetReplies(
|
||||
_topicProcess->inputPeer,
|
||||
MTP_int(_topicProcess->topicRootId),
|
||||
MTP_int(offsetId),
|
||||
MTP_int(0),
|
||||
MTP_int(addOffset),
|
||||
MTP_int(limit),
|
||||
MTP_int(0),
|
||||
MTP_int(0),
|
||||
MTP_long(0)
|
||||
)).done(doneHandler).send();
|
||||
}
|
||||
|
||||
void ApiWrap::loadTopicMessagesFiles(Data::MessagesSlice &&slice) {
|
||||
Expects(_topicProcess != nullptr);
|
||||
Expects(!_topicProcess->slice.has_value());
|
||||
|
||||
collectMessagesCustomEmoji(slice);
|
||||
|
||||
if (slice.list.empty()) {
|
||||
_topicProcess->lastSlice = true;
|
||||
}
|
||||
_topicProcess->slice = std::move(slice);
|
||||
_topicProcess->fileIndex = 0;
|
||||
|
||||
resolveTopicCustomEmoji();
|
||||
}
|
||||
|
||||
void ApiWrap::resolveTopicCustomEmoji() {
|
||||
if (_unresolvedCustomEmoji.empty()) {
|
||||
loadNextTopicMessageFile();
|
||||
return;
|
||||
}
|
||||
const auto count = std::min(
|
||||
int(_unresolvedCustomEmoji.size()),
|
||||
kMaxEmojiPerRequest);
|
||||
auto v = QVector<MTPlong>();
|
||||
v.reserve(count);
|
||||
const auto till = end(_unresolvedCustomEmoji);
|
||||
const auto from = end(_unresolvedCustomEmoji) - count;
|
||||
for (auto i = from; i != till; ++i) {
|
||||
v.push_back(MTP_long(*i));
|
||||
}
|
||||
_unresolvedCustomEmoji.erase(from, till);
|
||||
const auto finalize = [=] {
|
||||
for (const auto &id : v) {
|
||||
if (_resolvedCustomEmoji.contains(id.v)) {
|
||||
continue;
|
||||
}
|
||||
_resolvedCustomEmoji.emplace(
|
||||
id.v,
|
||||
Data::Document{
|
||||
.file = {
|
||||
.skipReason = Data::File::SkipReason::Unavailable,
|
||||
},
|
||||
});
|
||||
}
|
||||
resolveTopicCustomEmoji();
|
||||
};
|
||||
mainRequest(MTPmessages_GetCustomEmojiDocuments(
|
||||
MTP_vector<MTPlong>(v)
|
||||
)).fail([=](const MTP::Error &error) {
|
||||
LOG(("Export Error: Failed to get documents for emoji."));
|
||||
finalize();
|
||||
return true;
|
||||
}).done([=](const MTPVector<MTPDocument> &result) {
|
||||
for (const auto &entry : result.v) {
|
||||
auto document = Data::ParseDocument(
|
||||
_topicProcess->context,
|
||||
entry,
|
||||
_topicProcess->relativePath,
|
||||
TimeId());
|
||||
_resolvedCustomEmoji.emplace(document.id, std::move(document));
|
||||
}
|
||||
finalize();
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ApiWrap::loadNextTopicMessageFile() {
|
||||
Expects(_topicProcess != nullptr);
|
||||
Expects(_topicProcess->slice.has_value());
|
||||
|
||||
const auto makeProgress = [=](FileProgress progress) {
|
||||
return _topicProcess->fileProgress(DownloadProgress{
|
||||
.randomId = _fileProcess->randomId,
|
||||
.path = _fileProcess->relativePath,
|
||||
.itemIndex = _topicProcess->fileIndex,
|
||||
.ready = progress.ready,
|
||||
.total = progress.total,
|
||||
});
|
||||
};
|
||||
for (auto &list = _topicProcess->slice->list
|
||||
; _topicProcess->fileIndex < list.size()
|
||||
; ++_topicProcess->fileIndex) {
|
||||
auto &message = list[_topicProcess->fileIndex];
|
||||
if (!messageCustomEmojiReady(message)) {
|
||||
return;
|
||||
}
|
||||
const auto origin = Data::FileOrigin{
|
||||
.peer = _topicProcess->inputPeer,
|
||||
.messageId = message.id
|
||||
};
|
||||
const auto ready = processFileLoad(
|
||||
message.file(),
|
||||
origin,
|
||||
makeProgress,
|
||||
[=, &message](const QString &path) {
|
||||
loadTopicMessageFileOrThumbDone(message.file(), path);
|
||||
},
|
||||
&message);
|
||||
if (!ready) {
|
||||
return;
|
||||
}
|
||||
const auto thumbReady = processFileLoad(
|
||||
message.thumb().file,
|
||||
origin,
|
||||
makeProgress,
|
||||
[=, &message](const QString &path) {
|
||||
loadTopicMessageFileOrThumbDone(message.thumb().file, path);
|
||||
},
|
||||
&message);
|
||||
if (!thumbReady) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
finishTopicMessagesSlice();
|
||||
}
|
||||
|
||||
void ApiWrap::finishTopicMessagesSlice() {
|
||||
Expects(_topicProcess != nullptr);
|
||||
Expects(_topicProcess->slice.has_value());
|
||||
|
||||
auto slice = *base::take(_topicProcess->slice);
|
||||
if (!slice.list.empty()) {
|
||||
_topicProcess->offsetId = slice.list.back().id;
|
||||
_topicProcess->processedCount += slice.list.size();
|
||||
if (!_topicProcess->handleSlice(std::move(slice))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const auto reachedTotal = _topicProcess->totalCount > 0
|
||||
&& _topicProcess->processedCount >= _topicProcess->totalCount;
|
||||
|
||||
if (!_topicProcess->lastSlice && !reachedTotal) {
|
||||
requestTopicMessagesSlice();
|
||||
} else {
|
||||
finishTopicMessages();
|
||||
}
|
||||
}
|
||||
|
||||
void ApiWrap::loadTopicMessageFileOrThumbDone(
|
||||
Data::File &file,
|
||||
const QString &relativePath) {
|
||||
Expects(_topicProcess != nullptr);
|
||||
Expects(_topicProcess->slice.has_value());
|
||||
Expects((_topicProcess->fileIndex >= 0)
|
||||
&& (_topicProcess->fileIndex < _topicProcess->slice->list.size()));
|
||||
|
||||
file.relativePath = relativePath;
|
||||
if (relativePath.isEmpty()) {
|
||||
file.skipReason = Data::File::SkipReason::Unavailable;
|
||||
}
|
||||
loadNextTopicMessageFile();
|
||||
}
|
||||
|
||||
void ApiWrap::finishTopicMessages() {
|
||||
Expects(_topicProcess != nullptr);
|
||||
Expects(!_topicProcess->slice.has_value());
|
||||
|
||||
const auto process = base::take(_topicProcess);
|
||||
process->done();
|
||||
}
|
||||
|
||||
bool ApiWrap::processFileLoad(
|
||||
Data::File &file,
|
||||
const Data::FileOrigin &origin,
|
||||
|
||||
@@ -106,6 +106,15 @@ public:
|
||||
Fn<bool(Data::MessagesSlice&&)> slice,
|
||||
FnMut<void()> done);
|
||||
|
||||
void requestTopicMessages(
|
||||
PeerId peerId,
|
||||
MTPInputPeer inputPeer,
|
||||
int32 topicRootId,
|
||||
FnMut<bool(int count)> start,
|
||||
Fn<bool(DownloadProgress)> progress,
|
||||
Fn<bool(Data::MessagesSlice&&)> slice,
|
||||
FnMut<void()> done);
|
||||
|
||||
void finishExport(FnMut<void()> done);
|
||||
void skipFile(uint64 randomId);
|
||||
void cancelExportFast();
|
||||
@@ -125,7 +134,9 @@ private:
|
||||
struct ChatsProcess;
|
||||
struct LeftChannelsProcess;
|
||||
struct DialogsProcess;
|
||||
struct AbstractMessagesProcess;
|
||||
struct ChatProcess;
|
||||
struct TopicProcess;
|
||||
|
||||
void startMainSession(FnMut<void()> done);
|
||||
void sendNextStartRequest();
|
||||
@@ -202,6 +213,12 @@ private:
|
||||
int addOffset,
|
||||
int limit,
|
||||
FnMut<void(MTPmessages_Messages&&)> done);
|
||||
void requestTopicMessagesSlice();
|
||||
void requestTopicReplies(
|
||||
int offsetId,
|
||||
int addOffset,
|
||||
int limit,
|
||||
FnMut<void(MTPmessages_Messages&&)> done);
|
||||
void collectMessagesCustomEmoji(const Data::MessagesSlice &slice);
|
||||
void resolveCustomEmoji();
|
||||
void loadMessagesFiles(Data::MessagesSlice &&slice);
|
||||
@@ -217,6 +234,17 @@ private:
|
||||
void finishMessagesSlice();
|
||||
void finishMessages();
|
||||
|
||||
void loadTopicMessagesFiles(Data::MessagesSlice &&slice);
|
||||
void resolveTopicCustomEmoji();
|
||||
void loadNextTopicMessageFile();
|
||||
bool loadTopicEmojiProgress(FileProgress progress);
|
||||
void loadCustomEmojiDone(uint64 id, const QString &relativePath);
|
||||
void loadTopicMessageFileOrThumbDone(
|
||||
Data::File &file,
|
||||
const QString &relativePath);
|
||||
void finishTopicMessagesSlice();
|
||||
void finishTopicMessages();
|
||||
|
||||
[[nodiscard]] Data::Message *currentFileMessage() const;
|
||||
[[nodiscard]] Data::FileOrigin currentFileMessageOrigin() const;
|
||||
|
||||
@@ -285,6 +313,7 @@ private:
|
||||
std::unique_ptr<LeftChannelsProcess> _leftChannelsProcess;
|
||||
std::unique_ptr<DialogsProcess> _dialogsProcess;
|
||||
std::unique_ptr<ChatProcess> _chatProcess;
|
||||
std::unique_ptr<TopicProcess> _topicProcess;
|
||||
base::flat_set<uint64> _unresolvedCustomEmoji;
|
||||
base::flat_map<uint64, Data::Document> _resolvedCustomEmoji;
|
||||
QVector<MTPMessageRange> _splits;
|
||||
|
||||
@@ -37,6 +37,12 @@ public:
|
||||
crl::weak_on_queue<ControllerObject> weak,
|
||||
QPointer<MTP::Instance> mtproto,
|
||||
const MTPInputPeer &peer);
|
||||
ControllerObject(
|
||||
crl::weak_on_queue<ControllerObject> weak,
|
||||
QPointer<MTP::Instance> mtproto,
|
||||
const MTPInputPeer &peer,
|
||||
int32 topicRootId,
|
||||
uint64 peerId);
|
||||
|
||||
rpl::producer<State> state() const;
|
||||
|
||||
@@ -82,6 +88,7 @@ private:
|
||||
void exportOtherData();
|
||||
void exportDialogs();
|
||||
void exportNextDialog();
|
||||
void exportTopic();
|
||||
|
||||
template <typename Callback = const decltype(kNullStateCallback) &>
|
||||
ProcessingState prepareState(
|
||||
@@ -97,6 +104,7 @@ private:
|
||||
ProcessingState stateSessions() const;
|
||||
ProcessingState stateOtherData() const;
|
||||
ProcessingState stateDialogs(const DownloadProgress &progress) const;
|
||||
ProcessingState stateTopic(const DownloadProgress &progress) const;
|
||||
void fillMessagesState(
|
||||
ProcessingState &result,
|
||||
const Data::DialogsInfo &info,
|
||||
@@ -139,6 +147,9 @@ private:
|
||||
std::vector<Step> _steps;
|
||||
int _stepIndex = -1;
|
||||
|
||||
int32 _topicRootId = 0;
|
||||
uint64 _topicPeerId = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
@@ -167,6 +178,34 @@ ControllerObject::ControllerObject(
|
||||
setState(std::move(state));
|
||||
}
|
||||
|
||||
ControllerObject::ControllerObject(
|
||||
crl::weak_on_queue<ControllerObject> weak,
|
||||
QPointer<MTP::Instance> mtproto,
|
||||
const MTPInputPeer &peer,
|
||||
int32 topicRootId,
|
||||
uint64 peerId)
|
||||
: _api(mtproto, weak.runner())
|
||||
, _state(PasswordCheckState{})
|
||||
, _topicRootId(topicRootId)
|
||||
, _topicPeerId(peerId) {
|
||||
_api.errors(
|
||||
) | rpl::start_with_next([=](const MTP::Error &error) {
|
||||
setState(ApiErrorState{ error });
|
||||
}, _lifetime);
|
||||
|
||||
_api.ioErrors(
|
||||
) | rpl::start_with_next([=](const Output::Result &result) {
|
||||
ioCatchError(result);
|
||||
}, _lifetime);
|
||||
|
||||
//requestPasswordState();
|
||||
auto state = PasswordCheckState();
|
||||
state.checked = false;
|
||||
state.requesting = false;
|
||||
state.singlePeer = peer;
|
||||
setState(std::move(state));
|
||||
}
|
||||
|
||||
rpl::producer<State> ControllerObject::state() const {
|
||||
return rpl::single(
|
||||
_state
|
||||
@@ -257,6 +296,8 @@ void ControllerObject::startExport(
|
||||
}
|
||||
_settings = NormalizeSettings(settings);
|
||||
_environment = environment;
|
||||
_settings.singleTopicRootId = _topicRootId;
|
||||
_settings.singleTopicPeerId = _topicPeerId;
|
||||
|
||||
_settings.path = Output::NormalizePath(_settings);
|
||||
_writer = Output::CreateWriter(_settings.format);
|
||||
@@ -274,6 +315,10 @@ void ControllerObject::skipFile(uint64 randomId) {
|
||||
void ControllerObject::fillExportSteps() {
|
||||
using Type = Settings::Type;
|
||||
_steps.push_back(Step::Initializing);
|
||||
if (_settings.onlySingleTopic()) {
|
||||
_steps.push_back(Step::Topic);
|
||||
return;
|
||||
}
|
||||
if (_settings.types & Type::AnyChatsMask) {
|
||||
_steps.push_back(Step::DialogsList);
|
||||
}
|
||||
@@ -340,6 +385,9 @@ void ControllerObject::fillSubstepsInSteps(const ApiWrap::StartInfo &info) {
|
||||
if (_settings.types & Settings::Type::AnyChatsMask) {
|
||||
push(Step::Dialogs, info.dialogsCount);
|
||||
}
|
||||
if (_settings.onlySingleTopic()) {
|
||||
push(Step::Topic, 1);
|
||||
}
|
||||
_substepsInStep = std::move(result);
|
||||
_substepsTotal = ranges::accumulate(_substepsInStep, 0);
|
||||
}
|
||||
@@ -372,6 +420,7 @@ void ControllerObject::exportNext() {
|
||||
case Step::Sessions: return exportSessions();
|
||||
case Step::OtherData: return exportOtherData();
|
||||
case Step::Dialogs: return exportDialogs();
|
||||
case Step::Topic: return exportTopic();
|
||||
}
|
||||
Unexpected("Step in ControllerObject::exportNext.");
|
||||
}
|
||||
@@ -708,6 +757,70 @@ int ControllerObject::substepsInStep(Step step) const {
|
||||
return _substepsInStep[static_cast<int>(step)];
|
||||
}
|
||||
|
||||
void ControllerObject::exportTopic() {
|
||||
auto topicInfo = Data::DialogInfo();
|
||||
topicInfo.type = Data::DialogInfo::Type::PublicSupergroup;
|
||||
topicInfo.name = "Topic";
|
||||
topicInfo.peerId = PeerId(_topicPeerId);
|
||||
topicInfo.relativePath = QString();
|
||||
|
||||
if (ioCatchError(_writer->writeDialogStart(topicInfo))) {
|
||||
return;
|
||||
}
|
||||
|
||||
_api.requestTopicMessages(
|
||||
PeerId(_topicPeerId),
|
||||
_settings.singlePeer,
|
||||
_topicRootId,
|
||||
[=](int count) {
|
||||
_messagesWritten = 0;
|
||||
_messagesCount = count;
|
||||
setState(stateTopic(DownloadProgress()));
|
||||
return true;
|
||||
},
|
||||
[=](DownloadProgress progress) {
|
||||
setState(stateTopic(progress));
|
||||
return true;
|
||||
},
|
||||
[=](Data::MessagesSlice &&slice) {
|
||||
if (ioCatchError(_writer->writeDialogSlice(slice))) {
|
||||
return false;
|
||||
}
|
||||
_messagesWritten += slice.list.size();
|
||||
setState(stateTopic(DownloadProgress()));
|
||||
return true;
|
||||
},
|
||||
[=] {
|
||||
if (ioCatchError(_writer->writeDialogEnd())) {
|
||||
return;
|
||||
}
|
||||
if (ioCatchError(_writer->finish())) {
|
||||
return;
|
||||
}
|
||||
_api.finishExport([=] {
|
||||
setFinishedState();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ProcessingState ControllerObject::stateTopic(
|
||||
const DownloadProgress &progress) const {
|
||||
return prepareState(Step::Topic, [&](ProcessingState &result) {
|
||||
result.entityType = ProcessingState::EntityType::Topic;
|
||||
result.entityIndex = 0;
|
||||
result.entityCount = 1;
|
||||
result.itemIndex = _messagesWritten + progress.itemIndex;
|
||||
result.itemCount = std::max(_messagesCount, result.itemIndex);
|
||||
result.bytesRandomId = progress.randomId;
|
||||
if (!progress.path.isEmpty()) {
|
||||
const auto last = progress.path.lastIndexOf('/');
|
||||
result.bytesName = progress.path.mid(last + 1);
|
||||
}
|
||||
result.bytesLoaded = progress.ready;
|
||||
result.bytesCount = progress.total;
|
||||
});
|
||||
}
|
||||
|
||||
void ControllerObject::setFinishedState() {
|
||||
setState(FinishedState{
|
||||
_writer->mainFilePath(),
|
||||
@@ -721,6 +834,18 @@ Controller::Controller(
|
||||
: _wrapped(std::move(mtproto), peer) {
|
||||
}
|
||||
|
||||
Controller::Controller(
|
||||
QPointer<MTP::Instance> mtproto,
|
||||
const MTPInputPeer &peer,
|
||||
int32 topicRootId,
|
||||
uint64 peerId)
|
||||
: _wrapped(
|
||||
std::move(mtproto),
|
||||
peer,
|
||||
static_cast<int32>(topicRootId),
|
||||
static_cast<uint64>(peerId)) {
|
||||
}
|
||||
|
||||
rpl::producer<State> Controller::state() const {
|
||||
return _wrapped.producer_on_main([=](const Implementation &unwrapped) {
|
||||
return unwrapped.state();
|
||||
|
||||
@@ -44,12 +44,14 @@ struct ProcessingState {
|
||||
Sessions,
|
||||
OtherData,
|
||||
Dialogs,
|
||||
Topic,
|
||||
};
|
||||
enum class EntityType {
|
||||
Chat,
|
||||
SavedMessages,
|
||||
RepliesMessages,
|
||||
VerifyCodes,
|
||||
Topic,
|
||||
Other,
|
||||
};
|
||||
|
||||
@@ -115,6 +117,11 @@ public:
|
||||
Controller(
|
||||
QPointer<MTP::Instance> mtproto,
|
||||
const MTPInputPeer &peer);
|
||||
Controller(
|
||||
QPointer<MTP::Instance> mtproto,
|
||||
const MTPInputPeer &peer,
|
||||
int32 topicRootId,
|
||||
uint64 peerId);
|
||||
|
||||
rpl::producer<State> state() const;
|
||||
|
||||
|
||||
@@ -25,6 +25,21 @@ void Manager::start(not_null<PeerData*> peer) {
|
||||
start(&peer->session(), peer->input);
|
||||
}
|
||||
|
||||
void Manager::startTopic(
|
||||
not_null<PeerData*> peer,
|
||||
MsgId topicRootId) {
|
||||
if (_panel) {
|
||||
_panel->activatePanel();
|
||||
return;
|
||||
}
|
||||
_controller = std::make_unique<Controller>(
|
||||
&peer->session().mtp(),
|
||||
peer->input,
|
||||
int32(topicRootId.bare),
|
||||
uint64(peer->id.value));
|
||||
setupPanel(&peer->session());
|
||||
}
|
||||
|
||||
void Manager::start(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPInputPeer &singlePeer) {
|
||||
@@ -35,6 +50,10 @@ void Manager::start(
|
||||
_controller = std::make_unique<Controller>(
|
||||
&session->mtp(),
|
||||
singlePeer);
|
||||
setupPanel(session);
|
||||
}
|
||||
|
||||
void Manager::setupPanel(not_null<Main::Session*> session) {
|
||||
_panel = std::make_unique<View::PanelController>(
|
||||
session,
|
||||
_controller.get());
|
||||
|
||||
@@ -34,6 +34,9 @@ public:
|
||||
void start(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPInputPeer &singlePeer = MTP_inputPeerEmpty());
|
||||
void startTopic(
|
||||
not_null<PeerData*> peer,
|
||||
MsgId topicRootId);
|
||||
|
||||
[[nodiscard]] rpl::producer<View::PanelController*> currentView() const;
|
||||
[[nodiscard]] bool inProgress() const;
|
||||
@@ -42,6 +45,8 @@ public:
|
||||
void stop();
|
||||
|
||||
private:
|
||||
void setupPanel(not_null<Main::Session*> session);
|
||||
|
||||
std::unique_ptr<Controller> _controller;
|
||||
std::unique_ptr<View::PanelController> _panel;
|
||||
rpl::event_stream<View::PanelController*> _viewChanges;
|
||||
|
||||
@@ -88,12 +88,19 @@ struct Settings {
|
||||
TimeId singlePeerFrom = 0;
|
||||
TimeId singlePeerTill = 0;
|
||||
|
||||
int32 singleTopicRootId = 0;
|
||||
uint64 singleTopicPeerId = 0;
|
||||
|
||||
TimeId availableAt = 0;
|
||||
|
||||
bool onlySinglePeer() const {
|
||||
return singlePeer.type() != mtpc_inputPeerEmpty;
|
||||
}
|
||||
|
||||
bool onlySingleTopic() const {
|
||||
return onlySinglePeer() && singleTopicRootId != 0;
|
||||
}
|
||||
|
||||
static inline Types DefaultTypes() {
|
||||
return Type::PersonalInfo
|
||||
| Type::Userpics
|
||||
|
||||
@@ -138,6 +138,24 @@ Content ContentFromState(
|
||||
state.bytesName,
|
||||
state.bytesRandomId);
|
||||
break;
|
||||
case Step::Topic:
|
||||
pushMain(tr::lng_export_state_chats(tr::now));
|
||||
push(
|
||||
"topic",
|
||||
u"Topic"_q,
|
||||
(state.itemCount > 0
|
||||
? (QString::number(state.itemIndex)
|
||||
+ " / "
|
||||
+ QString::number(state.itemCount))
|
||||
: QString()),
|
||||
(state.itemCount > 0
|
||||
? (state.itemIndex / float64(state.itemCount))
|
||||
: 0.));
|
||||
pushBytes(
|
||||
"file_topic_" + QString::number(state.itemIndex),
|
||||
state.bytesName,
|
||||
state.bytesRandomId);
|
||||
break;
|
||||
default: Unexpected("Step in ContentFromState.");
|
||||
}
|
||||
const auto requiredRows = settings->onlySinglePeer() ? 2 : 3;
|
||||
|
||||
@@ -168,10 +168,13 @@ void PanelController::activatePanel() {
|
||||
|
||||
void PanelController::createPanel() {
|
||||
const auto singlePeer = _settings->onlySinglePeer();
|
||||
const auto singleTopic = _settings->onlySingleTopic();
|
||||
_panel = base::make_unique_q<Ui::SeparatePanel>(Ui::SeparatePanelArgs{
|
||||
.onAllSpaces = true,
|
||||
});
|
||||
_panel->setTitle((singlePeer
|
||||
_panel->setTitle((singleTopic
|
||||
? tr::lng_export_header_topic
|
||||
: singlePeer
|
||||
? tr::lng_export_header_chats
|
||||
: tr::lng_export_title)());
|
||||
_panel->setInnerSize(st::exportPanelSize);
|
||||
|
||||
@@ -906,11 +906,19 @@ void Filler::addDirectMessages() {
|
||||
}
|
||||
|
||||
void Filler::addExportChat() {
|
||||
if (_thread->asTopic() || !_peer->canExportChatHistory()) {
|
||||
if (!_peer->canExportChatHistory()) {
|
||||
return;
|
||||
}
|
||||
const auto peer = _peer;
|
||||
const auto navigation = _controller;
|
||||
if (const auto topic = _thread->asTopic()) {
|
||||
const auto topicRootId = topic->rootId();
|
||||
_addAction(
|
||||
tr::lng_profile_export_topic(tr::now),
|
||||
[=] { PeerMenuExportTopic(navigation, peer, topicRootId); },
|
||||
&st::menuIconExport);
|
||||
return;
|
||||
}
|
||||
_addAction(
|
||||
tr::lng_profile_export_chat(tr::now),
|
||||
[=] { PeerMenuExportChat(navigation, peer); },
|
||||
@@ -1746,6 +1754,15 @@ void PeerMenuExportChat(
|
||||
});
|
||||
}
|
||||
|
||||
void PeerMenuExportTopic(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<PeerData*> peer,
|
||||
MsgId topicRootId) {
|
||||
base::call_delayed(st::defaultPopupMenu.showDuration, [=] {
|
||||
Core::App().exportManager().startTopic(peer, topicRootId);
|
||||
});
|
||||
}
|
||||
|
||||
void PeerMenuDeleteContact(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<UserData*> user) {
|
||||
|
||||
@@ -98,6 +98,10 @@ void MenuAddMarkAsReadChatListAction(
|
||||
void PeerMenuExportChat(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<PeerData*> peer);
|
||||
void PeerMenuExportTopic(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<PeerData*> peer,
|
||||
MsgId topicRootId);
|
||||
void PeerMenuDeleteContact(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<UserData*> user);
|
||||
|
||||
Reference in New Issue
Block a user