mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-02-20 23:27:23 +00:00
Add stories to data export.
This commit is contained in:
parent
08c4f1f67a
commit
2a1631247d
@ -111,6 +111,11 @@ pre {
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.story {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.userpic .initials {
|
||||
display: block;
|
||||
color: #fff;
|
||||
@ -194,6 +199,10 @@ a.block_link:hover {
|
||||
text-decoration: none !important;
|
||||
background-color: #f5f7f8;
|
||||
}
|
||||
a.expanded {
|
||||
padding: 2px 8px;
|
||||
margin: -2px -8px;
|
||||
}
|
||||
.sections {
|
||||
padding: 11px 0;
|
||||
}
|
||||
@ -428,6 +437,9 @@ div.toast_shown {
|
||||
.section.sessions {
|
||||
background-image: url(../images/section_sessions.png);
|
||||
}
|
||||
.section.stories {
|
||||
background-image: url(../images/section_stories.png);
|
||||
}
|
||||
.section.web {
|
||||
background-image: url(../images/section_web.png);
|
||||
}
|
||||
@ -489,6 +501,9 @@ div.toast_shown {
|
||||
.section.sessions {
|
||||
background-image: url(../images/section_sessions@2x.png);
|
||||
}
|
||||
.section.stories {
|
||||
background-image: url(../images/section_stories@2x.png);
|
||||
}
|
||||
.section.web {
|
||||
background-image: url(../images/section_web@2x.png);
|
||||
}
|
||||
|
BIN
Telegram/Resources/export_html/images/section_stories.png
Normal file
BIN
Telegram/Resources/export_html/images/section_stories.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 605 B |
BIN
Telegram/Resources/export_html/images/section_stories@2x.png
Normal file
BIN
Telegram/Resources/export_html/images/section_stories@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
@ -3409,6 +3409,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_export_option_info_about" = "Your chosen screen name, username, phone number and profile pictures.";
|
||||
"lng_export_option_contacts" = "Contacts list";
|
||||
"lng_export_option_contacts_about" = "If you allow access, contacts are continuously synced with Telegram. You can adjust this in Settings > Privacy & Security on mobile devices.";
|
||||
"lng_export_option_stories" = "Stories archive";
|
||||
"lng_export_option_stories_about" = "All stories you posted from Telegram mobile apps.";
|
||||
"lng_export_option_sessions" = "Active sessions";
|
||||
"lng_export_option_sessions_about" = "We store this to display your connected devices in Settings > Privacy & Security > Active Sessions.";
|
||||
"lng_export_header_other" = "Other";
|
||||
|
@ -37,6 +37,8 @@
|
||||
<file alias="images/section_photos@2x.png">../../export_html/images/section_photos@2x.png</file>
|
||||
<file alias="images/section_sessions.png">../../export_html/images/section_sessions.png</file>
|
||||
<file alias="images/section_sessions@2x.png">../../export_html/images/section_sessions@2x.png</file>
|
||||
<file alias="images/section_stories.png">../../export_html/images/section_stories.png</file>
|
||||
<file alias="images/section_stories@2x.png">../../export_html/images/section_stories@2x.png</file>
|
||||
<file alias="images/section_web.png">../../export_html/images/section_web.png</file>
|
||||
<file alias="images/section_web@2x.png">../../export_html/images/section_web@2x.png</file>
|
||||
<file alias="js/script.js">../../export_html/js/script.js</file>
|
||||
|
@ -42,6 +42,16 @@ QString PreparePhotoFileName(int index, TimeId date) {
|
||||
+ ".jpg";
|
||||
}
|
||||
|
||||
QString PrepareStoryFileName(
|
||||
int index,
|
||||
TimeId date,
|
||||
const Utf8String &extension) {
|
||||
return "story_"
|
||||
+ QString::number(index)
|
||||
+ PrepareFileNameDatePart(date)
|
||||
+ extension;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int PeerColorIndex(BareId bareId) {
|
||||
@ -584,6 +594,99 @@ UserpicsSlice ParseUserpicsSlice(
|
||||
return result;
|
||||
}
|
||||
|
||||
File &Story::file() {
|
||||
return media.file();
|
||||
}
|
||||
|
||||
const File &Story::file() const {
|
||||
return media.file();
|
||||
}
|
||||
|
||||
Image &Story::thumb() {
|
||||
return media.thumb();
|
||||
}
|
||||
|
||||
const Image &Story::thumb() const {
|
||||
return media.thumb();
|
||||
}
|
||||
|
||||
StoriesSlice ParseStoriesSlice(
|
||||
const MTPVector<MTPStoryItem> &data,
|
||||
int baseIndex) {
|
||||
const auto &list = data.v;
|
||||
auto result = StoriesSlice();
|
||||
result.list.reserve(list.size());
|
||||
for (const auto &story : list) {
|
||||
result.lastId = story.match([](const auto &data) {
|
||||
return data.vid().v;
|
||||
});
|
||||
++result.skipped;
|
||||
story.match([&](const MTPDstoryItem &data) {
|
||||
const auto date = data.vdate().v;
|
||||
const auto expires = data.vexpire_date().v;
|
||||
auto media = Media();
|
||||
data.vmedia().match([&](const MTPDmessageMediaPhoto &data) {
|
||||
const auto suggestedPath = "stories/"
|
||||
+ PrepareStoryFileName(
|
||||
++baseIndex,
|
||||
date,
|
||||
".jpg"_q);
|
||||
const auto photo = data.vphoto();
|
||||
auto content = photo
|
||||
? ParsePhoto(*photo, suggestedPath)
|
||||
: Photo();
|
||||
media.content = content;
|
||||
}, [&](const MTPDmessageMediaDocument &data) {
|
||||
const auto document = data.vdocument();
|
||||
auto fake = ParseMediaContext();
|
||||
auto content = document
|
||||
? ParseDocument(fake, *document, "stories", date)
|
||||
: Document();
|
||||
const auto extension = (content.mime == "image/jpeg")
|
||||
? ".jpg"_q
|
||||
: (content.mime == "image/png")
|
||||
? ".png"_q
|
||||
: [&] {
|
||||
const auto mimeType = Core::MimeTypeForName(
|
||||
content.mime);
|
||||
QStringList patterns = mimeType.globPatterns();
|
||||
if (!patterns.isEmpty()) {
|
||||
return patterns.front().replace(
|
||||
'*',
|
||||
QString()).toUtf8();
|
||||
}
|
||||
return QByteArray();
|
||||
}();
|
||||
const auto path = content.file.suggestedPath = "stories/"
|
||||
+ PrepareStoryFileName(
|
||||
++baseIndex,
|
||||
date,
|
||||
extension);
|
||||
content.thumb.file.suggestedPath = path + "_thumb.jpg";
|
||||
media.content = content;
|
||||
}, [&](const auto &data) {
|
||||
media.content = UnsupportedMedia();
|
||||
});
|
||||
if (!v::is<UnsupportedMedia>(media.content)) {
|
||||
result.list.push_back(Story{
|
||||
.id = data.vid().v,
|
||||
.date = date,
|
||||
.expires = data.vexpire_date().v,
|
||||
.media = std::move(media),
|
||||
.pinned = data.is_pinned(),
|
||||
.caption = (data.vcaption()
|
||||
? ParseText(
|
||||
*data.vcaption(),
|
||||
data.ventities().value_or_empty())
|
||||
: std::vector<TextPart>()),
|
||||
});
|
||||
--result.skipped;
|
||||
}
|
||||
}, [](const auto &) {});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::pair<QString, QSize> WriteImageThumb(
|
||||
const QString &basePath,
|
||||
const QString &largePath,
|
||||
|
@ -48,6 +48,10 @@ struct UserpicsInfo {
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
struct StoriesInfo {
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
struct FileLocation {
|
||||
int dcId = 0;
|
||||
MTPInputFileLocation data;
|
||||
@ -663,9 +667,34 @@ struct FileOrigin {
|
||||
int split = 0;
|
||||
MTPInputPeer peer;
|
||||
int32 messageId = 0;
|
||||
int32 storyId = 0;
|
||||
uint64 customEmojiId = 0;
|
||||
};
|
||||
|
||||
struct Story {
|
||||
int32 id = 0;
|
||||
TimeId date = 0;
|
||||
TimeId expires = 0;
|
||||
Media media;
|
||||
bool pinned = false;
|
||||
std::vector<TextPart> caption;
|
||||
|
||||
File &file();
|
||||
const File &file() const;
|
||||
Image &thumb();
|
||||
const Image &thumb() const;
|
||||
};
|
||||
|
||||
struct StoriesSlice {
|
||||
std::vector<Story> list;
|
||||
int32 lastId = 0;
|
||||
int skipped = 0;
|
||||
};
|
||||
|
||||
StoriesSlice ParseStoriesSlice(
|
||||
const MTPVector<MTPStoryItem> &data,
|
||||
int baseIndex);
|
||||
|
||||
Message ParseMessage(
|
||||
ParseMediaContext &context,
|
||||
const MTPMessage &data,
|
||||
|
@ -30,6 +30,7 @@ constexpr auto kTopPeerSliceLimit = 100;
|
||||
constexpr auto kFileMaxSize = 4000 * int64(1024 * 1024);
|
||||
constexpr auto kLocationCacheSize = 100'000;
|
||||
constexpr auto kMaxEmojiPerRequest = 100;
|
||||
constexpr auto kStoriesSliceLimit = 100;
|
||||
|
||||
struct LocationKey {
|
||||
uint64 type;
|
||||
@ -109,6 +110,7 @@ struct ApiWrap::StartProcess {
|
||||
|
||||
enum class Step {
|
||||
UserpicsCount,
|
||||
StoriesCount,
|
||||
SplitRanges,
|
||||
DialogsCount,
|
||||
LeftChannelsCount,
|
||||
@ -139,6 +141,19 @@ struct ApiWrap::UserpicsProcess {
|
||||
int fileIndex = 0;
|
||||
};
|
||||
|
||||
struct ApiWrap::StoriesProcess {
|
||||
FnMut<bool(Data::StoriesInfo&&)> start;
|
||||
Fn<bool(DownloadProgress)> fileProgress;
|
||||
Fn<bool(Data::StoriesSlice&&)> handleSlice;
|
||||
FnMut<void()> finish;
|
||||
|
||||
int processed = 0;
|
||||
std::optional<Data::StoriesSlice> slice;
|
||||
int offsetId = 0;
|
||||
bool lastSlice = false;
|
||||
int fileIndex = 0;
|
||||
};
|
||||
|
||||
struct ApiWrap::OtherDataProcess {
|
||||
Data::File file;
|
||||
FnMut<void(Data::File&&)> done;
|
||||
@ -417,6 +432,9 @@ void ApiWrap::startExport(
|
||||
if (_settings->types & Settings::Type::Userpics) {
|
||||
_startProcess->steps.push_back(Step::UserpicsCount);
|
||||
}
|
||||
if (_settings->types & Settings::Type::Stories) {
|
||||
_startProcess->steps.push_back(Step::StoriesCount);
|
||||
}
|
||||
if (_settings->types & Settings::Type::AnyChatsMask) {
|
||||
_startProcess->steps.push_back(Step::SplitRanges);
|
||||
}
|
||||
@ -447,6 +465,8 @@ void ApiWrap::sendNextStartRequest() {
|
||||
switch (step) {
|
||||
case Step::UserpicsCount:
|
||||
return requestUserpicsCount();
|
||||
case Step::StoriesCount:
|
||||
return requestStoriesCount();
|
||||
case Step::SplitRanges:
|
||||
return requestSplitRanges();
|
||||
case Step::DialogsCount:
|
||||
@ -480,6 +500,22 @@ void ApiWrap::requestUserpicsCount() {
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ApiWrap::requestStoriesCount() {
|
||||
Expects(_startProcess != nullptr);
|
||||
|
||||
mainRequest(MTPstories_GetStoriesArchive(
|
||||
MTP_int(0), // offset_id
|
||||
MTP_int(0) // limit
|
||||
)).done([=](const MTPstories_Stories &result) {
|
||||
Expects(_settings != nullptr);
|
||||
Expects(_startProcess != nullptr);
|
||||
|
||||
_startProcess->info.storiesCount = result.data().vcount().v;
|
||||
|
||||
sendNextStartRequest();
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ApiWrap::requestSplitRanges() {
|
||||
Expects(_startProcess != nullptr);
|
||||
|
||||
@ -616,7 +652,8 @@ void ApiWrap::startMainSession(FnMut<void()> done) {
|
||||
using Type = Settings::Type;
|
||||
const auto sizeLimit = _settings->media.sizeLimit;
|
||||
const auto hasFiles = ((_settings->media.types != 0) && (sizeLimit > 0))
|
||||
|| (_settings->types & Type::Userpics);
|
||||
|| (_settings->types & Type::Userpics)
|
||||
|| (_settings->types & Type::Stories);
|
||||
|
||||
using Flag = MTPaccount_InitTakeoutSession::Flag;
|
||||
const auto flags = Flag(0)
|
||||
@ -856,6 +893,171 @@ void ApiWrap::finishUserpics() {
|
||||
base::take(_userpicsProcess)->finish();
|
||||
}
|
||||
|
||||
void ApiWrap::requestStories(
|
||||
FnMut<bool(Data::StoriesInfo&&)> start,
|
||||
Fn<bool(DownloadProgress)> progress,
|
||||
Fn<bool(Data::StoriesSlice&&)> slice,
|
||||
FnMut<void()> finish) {
|
||||
Expects(_storiesProcess == nullptr);
|
||||
|
||||
_storiesProcess = std::make_unique<StoriesProcess>();
|
||||
_storiesProcess->start = std::move(start);
|
||||
_storiesProcess->fileProgress = std::move(progress);
|
||||
_storiesProcess->handleSlice = std::move(slice);
|
||||
_storiesProcess->finish = std::move(finish);
|
||||
|
||||
mainRequest(MTPstories_GetStoriesArchive(
|
||||
MTP_int(_storiesProcess->offsetId),
|
||||
MTP_int(kStoriesSliceLimit)
|
||||
)).done([=](const MTPstories_Stories &result) mutable {
|
||||
Expects(_storiesProcess != nullptr);
|
||||
|
||||
auto startInfo = Data::StoriesInfo{ result.data().vcount().v };
|
||||
if (!_storiesProcess->start(std::move(startInfo))) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleStoriesSlice(result);
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ApiWrap::handleStoriesSlice(const MTPstories_Stories &result) {
|
||||
Expects(_storiesProcess != nullptr);
|
||||
|
||||
loadStoriesFiles(Data::ParseStoriesSlice(
|
||||
result.data().vstories(),
|
||||
_storiesProcess->processed));
|
||||
}
|
||||
|
||||
void ApiWrap::loadStoriesFiles(Data::StoriesSlice &&slice) {
|
||||
Expects(_storiesProcess != nullptr);
|
||||
Expects(!_storiesProcess->slice.has_value());
|
||||
|
||||
if (!slice.lastId) {
|
||||
_storiesProcess->lastSlice = true;
|
||||
}
|
||||
_storiesProcess->slice = std::move(slice);
|
||||
_storiesProcess->fileIndex = 0;
|
||||
loadNextStory();
|
||||
}
|
||||
|
||||
void ApiWrap::loadNextStory() {
|
||||
Expects(_storiesProcess != nullptr);
|
||||
Expects(_storiesProcess->slice.has_value());
|
||||
|
||||
for (auto &list = _storiesProcess->slice->list
|
||||
; _storiesProcess->fileIndex < list.size()
|
||||
; ++_storiesProcess->fileIndex) {
|
||||
auto &story = list[_storiesProcess->fileIndex];
|
||||
const auto origin = Data::FileOrigin{ .storyId = story.id };
|
||||
const auto ready = processFileLoad(
|
||||
story.file(),
|
||||
origin,
|
||||
[=](FileProgress value) { return loadStoryProgress(value); },
|
||||
[=](const QString &path) { loadStoryDone(path); });
|
||||
if (!ready) {
|
||||
return;
|
||||
}
|
||||
const auto thumbProgress = [=](FileProgress value) {
|
||||
return loadStoryThumbProgress(value);
|
||||
};
|
||||
const auto thumbReady = processFileLoad(
|
||||
story.thumb().file,
|
||||
origin,
|
||||
thumbProgress,
|
||||
[=](const QString &path) { loadStoryThumbDone(path); },
|
||||
nullptr,
|
||||
&story);
|
||||
if (!thumbReady) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
finishStoriesSlice();
|
||||
}
|
||||
|
||||
void ApiWrap::finishStoriesSlice() {
|
||||
Expects(_storiesProcess != nullptr);
|
||||
Expects(_storiesProcess->slice.has_value());
|
||||
|
||||
auto slice = *base::take(_storiesProcess->slice);
|
||||
if (slice.lastId) {
|
||||
_storiesProcess->processed += slice.list.size();
|
||||
_storiesProcess->offsetId = slice.lastId;
|
||||
if (!_storiesProcess->handleSlice(std::move(slice))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_storiesProcess->lastSlice) {
|
||||
finishStories();
|
||||
return;
|
||||
}
|
||||
|
||||
mainRequest(MTPstories_GetStoriesArchive(
|
||||
MTP_int(_storiesProcess->offsetId),
|
||||
MTP_int(kStoriesSliceLimit)
|
||||
)).done([=](const MTPstories_Stories &result) {
|
||||
handleStoriesSlice(result);
|
||||
}).send();
|
||||
}
|
||||
|
||||
bool ApiWrap::loadStoryProgress(FileProgress progress) {
|
||||
Expects(_fileProcess != nullptr);
|
||||
Expects(_storiesProcess != nullptr);
|
||||
Expects(_storiesProcess->slice.has_value());
|
||||
Expects((_storiesProcess->fileIndex >= 0)
|
||||
&& (_storiesProcess->fileIndex
|
||||
< _storiesProcess->slice->list.size()));
|
||||
|
||||
return _storiesProcess->fileProgress(DownloadProgress{
|
||||
_fileProcess->randomId,
|
||||
_fileProcess->relativePath,
|
||||
_storiesProcess->fileIndex,
|
||||
progress.ready,
|
||||
progress.total });
|
||||
}
|
||||
|
||||
void ApiWrap::loadStoryDone(const QString &relativePath) {
|
||||
Expects(_storiesProcess != nullptr);
|
||||
Expects(_storiesProcess->slice.has_value());
|
||||
Expects((_storiesProcess->fileIndex >= 0)
|
||||
&& (_storiesProcess->fileIndex
|
||||
< _storiesProcess->slice->list.size()));
|
||||
|
||||
const auto index = _storiesProcess->fileIndex;
|
||||
auto &file = _storiesProcess->slice->list[index].file();
|
||||
file.relativePath = relativePath;
|
||||
if (relativePath.isEmpty()) {
|
||||
file.skipReason = Data::File::SkipReason::Unavailable;
|
||||
}
|
||||
loadNextStory();
|
||||
}
|
||||
|
||||
bool ApiWrap::loadStoryThumbProgress(FileProgress progress) {
|
||||
return loadStoryProgress(progress);
|
||||
}
|
||||
|
||||
void ApiWrap::loadStoryThumbDone(const QString &relativePath) {
|
||||
Expects(_storiesProcess != nullptr);
|
||||
Expects(_storiesProcess->slice.has_value());
|
||||
Expects((_storiesProcess->fileIndex >= 0)
|
||||
&& (_storiesProcess->fileIndex
|
||||
< _storiesProcess->slice->list.size()));
|
||||
|
||||
const auto index = _storiesProcess->fileIndex;
|
||||
auto &file = _storiesProcess->slice->list[index].thumb().file;
|
||||
file.relativePath = relativePath;
|
||||
if (relativePath.isEmpty()) {
|
||||
file.skipReason = Data::File::SkipReason::Unavailable;
|
||||
}
|
||||
loadNextStory();
|
||||
}
|
||||
|
||||
void ApiWrap::finishStories() {
|
||||
Expects(_storiesProcess != nullptr);
|
||||
|
||||
base::take(_storiesProcess)->finish();
|
||||
}
|
||||
|
||||
void ApiWrap::requestContacts(FnMut<void(Data::ContactsList&&)> done) {
|
||||
Expects(_contactsProcess == nullptr);
|
||||
|
||||
@ -1753,7 +1955,8 @@ bool ApiWrap::processFileLoad(
|
||||
const Data::FileOrigin &origin,
|
||||
Fn<bool(FileProgress)> progress,
|
||||
FnMut<void(QString)> done,
|
||||
Data::Message *message) {
|
||||
Data::Message *message,
|
||||
Data::Story *story) {
|
||||
using SkipReason = Data::File::SkipReason;
|
||||
|
||||
if (!file.relativePath.isEmpty()
|
||||
@ -1767,7 +1970,12 @@ bool ApiWrap::processFileLoad(
|
||||
}
|
||||
|
||||
using Type = MediaSettings::Type;
|
||||
const auto type = message ? v::match(message->media.content, [&](
|
||||
const auto media = message
|
||||
? &message->media
|
||||
: story
|
||||
? &story->media
|
||||
: nullptr;
|
||||
const auto type = media ? v::match(media->content, [&](
|
||||
const Data::Document &data) {
|
||||
if (data.isSticker) {
|
||||
return Type::Sticker;
|
||||
@ -1786,14 +1994,18 @@ bool ApiWrap::processFileLoad(
|
||||
return Type::Photo;
|
||||
}) : Type(0);
|
||||
|
||||
const auto limit = _settings->media.sizeLimit;
|
||||
const auto fullSize = message
|
||||
? message->file().size
|
||||
: story
|
||||
? story->file().size
|
||||
: file.size;
|
||||
if (message && Data::SkipMessageByDate(*message, *_settings)) {
|
||||
file.skipReason = SkipReason::DateLimits;
|
||||
return true;
|
||||
} else if ((_settings->media.types & type) != type) {
|
||||
} else if (!story && (_settings->media.types & type) != type) {
|
||||
file.skipReason = SkipReason::FileType;
|
||||
return true;
|
||||
} else if ((message ? message->file().size : file.size) >= limit) {
|
||||
} else if (!story && fullSize >= _settings->media.sizeLimit) {
|
||||
// Don't load thumbs for large files that we skip.
|
||||
file.skipReason = SkipReason::FileSize;
|
||||
return true;
|
||||
@ -1972,7 +2184,20 @@ void ApiWrap::filePartRefreshReference(int64 offset) {
|
||||
Expects(_fileProcess->requestId == 0);
|
||||
|
||||
const auto &origin = _fileProcess->origin;
|
||||
if (!origin.messageId) {
|
||||
if (origin.storyId) {
|
||||
_fileProcess->requestId = mainRequest(MTPstories_GetStoriesByID(
|
||||
MTP_inputUserSelf(),
|
||||
MTP_vector<MTPint>(1, MTP_int(origin.storyId))
|
||||
)).fail([=](const MTP::Error &error) {
|
||||
_fileProcess->requestId = 0;
|
||||
filePartUnavailable();
|
||||
return true;
|
||||
}).done([=](const MTPstories_Stories &result) {
|
||||
_fileProcess->requestId = 0;
|
||||
filePartExtractReference(offset, result);
|
||||
}).send();
|
||||
return;
|
||||
} else if (!origin.messageId) {
|
||||
error("FILE_REFERENCE error for non-message file.");
|
||||
return;
|
||||
}
|
||||
@ -2061,6 +2286,38 @@ void ApiWrap::filePartExtractReference(
|
||||
});
|
||||
}
|
||||
|
||||
void ApiWrap::filePartExtractReference(
|
||||
int64 offset,
|
||||
const MTPstories_Stories &result) {
|
||||
Expects(_fileProcess != nullptr);
|
||||
Expects(_fileProcess->requestId == 0);
|
||||
|
||||
const auto stories = Data::ParseStoriesSlice(
|
||||
result.data().vstories(),
|
||||
0);
|
||||
for (const auto &story : stories.list) {
|
||||
if (story.id == _fileProcess->origin.storyId) {
|
||||
const auto refresh1 = Data::RefreshFileReference(
|
||||
_fileProcess->location,
|
||||
story.file().location);
|
||||
const auto refresh2 = Data::RefreshFileReference(
|
||||
_fileProcess->location,
|
||||
story.thumb().file.location);
|
||||
if (refresh1 || refresh2) {
|
||||
_fileProcess->requestId = fileRequest(
|
||||
_fileProcess->location,
|
||||
offset
|
||||
).done([=](const MTPupload_File &result) {
|
||||
_fileProcess->requestId = 0;
|
||||
filePartDone(offset, result);
|
||||
}).send();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
filePartUnavailable();
|
||||
}
|
||||
|
||||
void ApiWrap::filePartUnavailable() {
|
||||
Expects(_fileProcess != nullptr);
|
||||
Expects(!_fileProcess->requests.empty());
|
||||
|
@ -19,12 +19,15 @@ struct FileLocation;
|
||||
struct PersonalInfo;
|
||||
struct UserpicsInfo;
|
||||
struct UserpicsSlice;
|
||||
struct StoriesInfo;
|
||||
struct StoriesSlice;
|
||||
struct ContactsList;
|
||||
struct SessionsList;
|
||||
struct DialogsInfo;
|
||||
struct DialogInfo;
|
||||
struct MessagesSlice;
|
||||
struct Message;
|
||||
struct Story;
|
||||
struct FileOrigin;
|
||||
} // namespace Data
|
||||
|
||||
@ -44,6 +47,7 @@ public:
|
||||
|
||||
struct StartInfo {
|
||||
int userpicsCount = 0;
|
||||
int storiesCount = 0;
|
||||
int dialogsCount = 0;
|
||||
};
|
||||
void startExport(
|
||||
@ -74,6 +78,12 @@ public:
|
||||
Fn<bool(Data::UserpicsSlice&&)> slice,
|
||||
FnMut<void()> finish);
|
||||
|
||||
void requestStories(
|
||||
FnMut<bool(Data::StoriesInfo&&)> start,
|
||||
Fn<bool(DownloadProgress)> progress,
|
||||
Fn<bool(Data::StoriesSlice&&)> slice,
|
||||
FnMut<void()> finish);
|
||||
|
||||
void requestContacts(FnMut<void(Data::ContactsList&&)> done);
|
||||
|
||||
void requestSessions(FnMut<void(Data::SessionsList&&)> done);
|
||||
@ -96,6 +106,7 @@ private:
|
||||
struct StartProcess;
|
||||
struct ContactsProcess;
|
||||
struct UserpicsProcess;
|
||||
struct StoriesProcess;
|
||||
struct OtherDataProcess;
|
||||
struct FileProcess;
|
||||
struct FileProgress;
|
||||
@ -107,6 +118,7 @@ private:
|
||||
void startMainSession(FnMut<void()> done);
|
||||
void sendNextStartRequest();
|
||||
void requestUserpicsCount();
|
||||
void requestStoriesCount();
|
||||
void requestSplitRanges();
|
||||
void requestDialogsCount();
|
||||
void requestLeftChannelsCount();
|
||||
@ -122,6 +134,16 @@ private:
|
||||
void finishUserpicsSlice();
|
||||
void finishUserpics();
|
||||
|
||||
void handleStoriesSlice(const MTPstories_Stories &result);
|
||||
void loadStoriesFiles(Data::StoriesSlice &&slice);
|
||||
void loadNextStory();
|
||||
bool loadStoryProgress(FileProgress value);
|
||||
void loadStoryDone(const QString &relativePath);
|
||||
bool loadStoryThumbProgress(FileProgress value);
|
||||
void loadStoryThumbDone(const QString &relativePath);
|
||||
void finishStoriesSlice();
|
||||
void finishStories();
|
||||
|
||||
void otherDataDone(const QString &relativePath);
|
||||
|
||||
bool useOnlyLastSplit() const;
|
||||
@ -179,7 +201,8 @@ private:
|
||||
const Data::FileOrigin &origin,
|
||||
Fn<bool(FileProgress)> progress,
|
||||
FnMut<void(QString)> done,
|
||||
Data::Message *message = nullptr);
|
||||
Data::Message *message = nullptr,
|
||||
Data::Story *story = nullptr);
|
||||
std::unique_ptr<FileProcess> prepareFileProcess(
|
||||
const Data::File &file,
|
||||
const Data::FileOrigin &origin) const;
|
||||
@ -198,6 +221,9 @@ private:
|
||||
void filePartExtractReference(
|
||||
int64 offset,
|
||||
const MTPmessages_Messages &result);
|
||||
void filePartExtractReference(
|
||||
int64 offset,
|
||||
const MTPstories_Stories &result);
|
||||
|
||||
template <typename Request>
|
||||
class RequestBuilder;
|
||||
@ -228,6 +254,7 @@ private:
|
||||
std::unique_ptr<LoadedFileCache> _fileCache;
|
||||
std::unique_ptr<ContactsProcess> _contactsProcess;
|
||||
std::unique_ptr<UserpicsProcess> _userpicsProcess;
|
||||
std::unique_ptr<StoriesProcess> _storiesProcess;
|
||||
std::unique_ptr<OtherDataProcess> _otherDataProcess;
|
||||
std::unique_ptr<FileProcess> _fileProcess;
|
||||
std::unique_ptr<LeftChannelsProcess> _leftChannelsProcess;
|
||||
|
@ -75,6 +75,7 @@ private:
|
||||
void collectDialogsList();
|
||||
void exportPersonalInfo();
|
||||
void exportUserpics();
|
||||
void exportStories();
|
||||
void exportContacts();
|
||||
void exportSessions();
|
||||
void exportOtherData();
|
||||
@ -89,6 +90,7 @@ private:
|
||||
ProcessingState stateDialogsList(int processed) const;
|
||||
ProcessingState statePersonalInfo() const;
|
||||
ProcessingState stateUserpics(const DownloadProgress &progress) const;
|
||||
ProcessingState stateStories(const DownloadProgress &progress) const;
|
||||
ProcessingState stateContacts() const;
|
||||
ProcessingState stateSessions() const;
|
||||
ProcessingState stateOtherData() const;
|
||||
@ -114,6 +116,9 @@ private:
|
||||
int _userpicsWritten = 0;
|
||||
int _userpicsCount = 0;
|
||||
|
||||
int _storiesWritten = 0;
|
||||
int _storiesCount = 0;
|
||||
|
||||
// rpl::variable<State> fails to compile in MSVC :(
|
||||
State _state;
|
||||
rpl::event_stream<State> _stateChanges;
|
||||
@ -273,6 +278,9 @@ void ControllerObject::fillExportSteps() {
|
||||
if (_settings.types & Type::Userpics) {
|
||||
_steps.push_back(Step::Userpics);
|
||||
}
|
||||
if (_settings.types & Type::Stories) {
|
||||
_steps.push_back(Step::Stories);
|
||||
}
|
||||
if (_settings.types & Type::Contacts) {
|
||||
_steps.push_back(Step::Contacts);
|
||||
}
|
||||
@ -306,6 +314,9 @@ void ControllerObject::fillSubstepsInSteps(const ApiWrap::StartInfo &info) {
|
||||
if (_settings.types & Settings::Type::Userpics) {
|
||||
push(Step::Userpics, 1);
|
||||
}
|
||||
if (_settings.types & Settings::Type::Stories) {
|
||||
push(Step::Stories, 1);
|
||||
}
|
||||
if (_settings.types & Settings::Type::Contacts) {
|
||||
push(Step::Contacts, 1);
|
||||
}
|
||||
@ -344,6 +355,7 @@ void ControllerObject::exportNext() {
|
||||
case Step::DialogsList: return collectDialogsList();
|
||||
case Step::PersonalInfo: return exportPersonalInfo();
|
||||
case Step::Userpics: return exportUserpics();
|
||||
case Step::Stories: return exportStories();
|
||||
case Step::Contacts: return exportContacts();
|
||||
case Step::Sessions: return exportSessions();
|
||||
case Step::OtherData: return exportOtherData();
|
||||
@ -416,6 +428,32 @@ void ControllerObject::exportUserpics() {
|
||||
});
|
||||
}
|
||||
|
||||
void ControllerObject::exportStories() {
|
||||
_api.requestStories([=](Data::StoriesInfo &&start) {
|
||||
if (ioCatchError(_writer->writeStoriesStart(start))) {
|
||||
return false;
|
||||
}
|
||||
_storiesWritten = 0;
|
||||
_storiesCount = start.count;
|
||||
return true;
|
||||
}, [=](DownloadProgress progress) {
|
||||
setState(stateStories(progress));
|
||||
return true;
|
||||
}, [=](Data::StoriesSlice &&slice) {
|
||||
if (ioCatchError(_writer->writeStoriesSlice(slice))) {
|
||||
return false;
|
||||
}
|
||||
_storiesWritten += slice.list.size();
|
||||
setState(stateStories(DownloadProgress()));
|
||||
return true;
|
||||
}, [=] {
|
||||
if (ioCatchError(_writer->writeStoriesEnd())) {
|
||||
return;
|
||||
}
|
||||
exportNext();
|
||||
});
|
||||
}
|
||||
|
||||
void ControllerObject::exportContacts() {
|
||||
setState(stateContacts());
|
||||
_api.requestContacts([=](Data::ContactsList &&result) {
|
||||
@ -533,7 +571,21 @@ ProcessingState ControllerObject::stateUserpics(
|
||||
return prepareState(Step::Userpics, [&](ProcessingState &result) {
|
||||
result.entityIndex = _userpicsWritten + progress.itemIndex;
|
||||
result.entityCount = std::max(_userpicsCount, result.entityIndex);
|
||||
result.bytesType = ProcessingState::FileType::Photo;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
ProcessingState ControllerObject::stateStories(
|
||||
const DownloadProgress &progress) const {
|
||||
return prepareState(Step::Stories, [&](ProcessingState &result) {
|
||||
result.entityIndex = _storiesWritten + progress.itemIndex;
|
||||
result.entityCount = std::max(_storiesCount, result.entityIndex);
|
||||
result.bytesRandomId = progress.randomId;
|
||||
if (!progress.path.isEmpty()) {
|
||||
const auto last = progress.path.lastIndexOf('/');
|
||||
@ -586,7 +638,6 @@ void ControllerObject::fillMessagesState(
|
||||
: ProcessingState::EntityType::Chat;
|
||||
result.itemIndex = _messagesWritten + progress.itemIndex;
|
||||
result.itemCount = std::max(_messagesCount, result.itemIndex);
|
||||
result.bytesType = ProcessingState::FileType::File; // TODO
|
||||
result.bytesRandomId = progress.randomId;
|
||||
if (!progress.path.isEmpty()) {
|
||||
const auto last = progress.path.lastIndexOf('/');
|
||||
|
@ -38,21 +38,12 @@ struct ProcessingState {
|
||||
DialogsList,
|
||||
PersonalInfo,
|
||||
Userpics,
|
||||
Stories,
|
||||
Contacts,
|
||||
Sessions,
|
||||
OtherData,
|
||||
Dialogs,
|
||||
};
|
||||
enum class FileType {
|
||||
None,
|
||||
Photo,
|
||||
Video,
|
||||
VoiceMessage,
|
||||
VideoMessage,
|
||||
Sticker,
|
||||
GIF,
|
||||
File,
|
||||
};
|
||||
enum class EntityType {
|
||||
Chat,
|
||||
SavedMessages,
|
||||
@ -75,7 +66,6 @@ struct ProcessingState {
|
||||
int itemCount = 0;
|
||||
|
||||
uint64 bytesRandomId = 0;
|
||||
FileType bytesType = FileType::None;
|
||||
QString bytesName;
|
||||
int64 bytesLoaded = 0;
|
||||
int64 bytesCount = 0;
|
||||
|
@ -57,13 +57,18 @@ struct Settings {
|
||||
PublicGroups = 0x100,
|
||||
PrivateChannels = 0x200,
|
||||
PublicChannels = 0x400,
|
||||
Stories = 0x800,
|
||||
|
||||
GroupsMask = PrivateGroups | PublicGroups,
|
||||
ChannelsMask = PrivateChannels | PublicChannels,
|
||||
GroupsChannelsMask = GroupsMask | ChannelsMask,
|
||||
NonChannelChatsMask = PersonalChats | BotChats | PrivateGroups,
|
||||
AnyChatsMask = PersonalChats | BotChats | GroupsChannelsMask,
|
||||
NonChatsMask = PersonalInfo | Userpics | Contacts | Sessions,
|
||||
NonChatsMask = (PersonalInfo
|
||||
| Userpics
|
||||
| Contacts
|
||||
| Stories
|
||||
| Sessions),
|
||||
AllMask = NonChatsMask | OtherData | AnyChatsMask,
|
||||
};
|
||||
using Types = base::flags<Type>;
|
||||
@ -91,6 +96,7 @@ struct Settings {
|
||||
return Type::PersonalInfo
|
||||
| Type::Userpics
|
||||
| Type::Contacts
|
||||
| Type::Stories
|
||||
| Type::PersonalChats
|
||||
| Type::PrivateGroups;
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ namespace Data {
|
||||
struct PersonalInfo;
|
||||
struct UserpicsInfo;
|
||||
struct UserpicsSlice;
|
||||
struct StoriesInfo;
|
||||
struct StoriesSlice;
|
||||
struct ContactsList;
|
||||
struct SessionsList;
|
||||
struct DialogsInfo;
|
||||
@ -55,6 +57,12 @@ public:
|
||||
const Data::UserpicsSlice &data) = 0;
|
||||
[[nodiscard]] virtual Result writeUserpicsEnd() = 0;
|
||||
|
||||
[[nodiscard]] virtual Result writeStoriesStart(
|
||||
const Data::StoriesInfo &data) = 0;
|
||||
[[nodiscard]] virtual Result writeStoriesSlice(
|
||||
const Data::StoriesSlice &data) = 0;
|
||||
[[nodiscard]] virtual Result writeStoriesEnd() = 0;
|
||||
|
||||
[[nodiscard]] virtual Result writeContactsList(
|
||||
const Data::ContactsList &data) = 0;
|
||||
|
||||
|
@ -35,11 +35,23 @@ constexpr auto kStickerMaxWidth = 384;
|
||||
constexpr auto kStickerMaxHeight = 384;
|
||||
constexpr auto kStickerMinWidth = 80;
|
||||
constexpr auto kStickerMinHeight = 80;
|
||||
constexpr auto kStoryThumbWidth = 45;
|
||||
constexpr auto kStoryThumbHeight = 80;
|
||||
|
||||
constexpr auto kChatsPriority = 0;
|
||||
constexpr auto kContactsPriority = 2;
|
||||
constexpr auto kFrequentContactsPriority = 3;
|
||||
constexpr auto kUserpicsPriority = 4;
|
||||
constexpr auto kStoriesPriority = 5;
|
||||
constexpr auto kSessionsPriority = 6;
|
||||
constexpr auto kWebSessionsPriority = 7;
|
||||
constexpr auto kOtherPriority = 8;
|
||||
|
||||
const auto kLineBreak = QByteArrayLiteral("<br>");
|
||||
|
||||
using Context = details::HtmlContext;
|
||||
using UserpicData = details::UserpicData;
|
||||
using StoryData = details::StoryData;
|
||||
using PeersMap = details::PeersMap;
|
||||
using MediaData = details::MediaData;
|
||||
|
||||
@ -347,6 +359,11 @@ struct UserpicData {
|
||||
QByteArray lastName;
|
||||
};
|
||||
|
||||
struct StoryData {
|
||||
QString imageLink;
|
||||
QString largeLink;
|
||||
};
|
||||
|
||||
class PeersMap {
|
||||
public:
|
||||
using Peer = Data::Peer;
|
||||
@ -503,6 +520,14 @@ public:
|
||||
const QByteArray &details,
|
||||
const QByteArray &info,
|
||||
const QString &link = QString());
|
||||
[[nodiscard]] QByteArray pushStoriesListEntry(
|
||||
const StoryData &story,
|
||||
const QByteArray &name,
|
||||
const QByteArrayList &details,
|
||||
const QByteArray &info,
|
||||
const std::vector<Data::TextPart> &caption,
|
||||
const QString &internalLinksDomain,
|
||||
const QString &link = QString());
|
||||
[[nodiscard]] QByteArray pushSessionListEntry(
|
||||
int apiId,
|
||||
const QByteArray &name,
|
||||
@ -750,6 +775,75 @@ QByteArray HtmlWriter::Wrap::pushListEntry(
|
||||
info);
|
||||
}
|
||||
|
||||
QByteArray HtmlWriter::Wrap::pushStoriesListEntry(
|
||||
const StoryData &story,
|
||||
const QByteArray &name,
|
||||
const QByteArrayList &details,
|
||||
const QByteArray &info,
|
||||
const std::vector<Data::TextPart> &caption,
|
||||
const QString &internalLinksDomain,
|
||||
const QString &link) {
|
||||
auto result = pushDiv("entry clearfix");
|
||||
if (!link.isEmpty()) {
|
||||
result.append(pushTag("a", {
|
||||
{ "class", "pull_left userpic_wrap" },
|
||||
{ "href", relativePath(link).toUtf8() + "#allow_back" },
|
||||
}));
|
||||
} else {
|
||||
result.append(pushDiv("pull_left userpic_wrap"));
|
||||
}
|
||||
if (!story.imageLink.isEmpty()) {
|
||||
const auto sizeStyle = "width: "
|
||||
+ Data::NumberToString(kStoryThumbWidth)
|
||||
+ "px; height: "
|
||||
+ Data::NumberToString(kStoryThumbHeight)
|
||||
+ "px";
|
||||
result.append(pushTag("img", {
|
||||
{ "class", "story" },
|
||||
{ "style", sizeStyle },
|
||||
{ "src", relativePath(story.imageLink).toUtf8() },
|
||||
{ "empty", "" }
|
||||
}));
|
||||
}
|
||||
result.append(popTag());
|
||||
result.append(pushDiv("body"));
|
||||
if (!info.isEmpty()) {
|
||||
result.append(pushDiv("pull_right info details"));
|
||||
result.append(SerializeString(info));
|
||||
result.append(popTag());
|
||||
}
|
||||
if (!name.isEmpty()) {
|
||||
if (!link.isEmpty()) {
|
||||
result.append(pushTag("a", {
|
||||
{ "class", "block_link expanded" },
|
||||
{ "href", relativePath(link).toUtf8() + "#allow_back" },
|
||||
}));
|
||||
}
|
||||
result.append(pushDiv("name bold"));
|
||||
result.append(SerializeString(name));
|
||||
result.append(popTag());
|
||||
if (!link.isEmpty()) {
|
||||
result.append(popTag());
|
||||
}
|
||||
}
|
||||
const auto text = caption.empty()
|
||||
? QByteArray()
|
||||
: FormatText(caption, internalLinksDomain, _base);
|
||||
if (!text.isEmpty()) {
|
||||
result.append(pushDiv("text"));
|
||||
result.append(text);
|
||||
result.append(popTag());
|
||||
}
|
||||
for (const auto &detail : details) {
|
||||
result.append(pushDiv("details_entry details"));
|
||||
result.append(SerializeString(detail));
|
||||
result.append(popTag());
|
||||
}
|
||||
result.append(popTag());
|
||||
result.append(popTag());
|
||||
return result;
|
||||
}
|
||||
|
||||
QByteArray HtmlWriter::Wrap::pushSessionListEntry(
|
||||
int apiId,
|
||||
const QByteArray &name,
|
||||
@ -1980,6 +2074,7 @@ Result HtmlWriter::start(
|
||||
"images/section_other.png",
|
||||
"images/section_photos.png",
|
||||
"images/section_sessions.png",
|
||||
"images/section_stories.png",
|
||||
"images/section_web.png",
|
||||
"js/script.js",
|
||||
};
|
||||
@ -2176,13 +2271,114 @@ QString HtmlWriter::userpicsFilePath() const {
|
||||
|
||||
void HtmlWriter::pushUserpicsSection() {
|
||||
pushSection(
|
||||
4,
|
||||
kUserpicsPriority,
|
||||
"Profile pictures",
|
||||
"photos",
|
||||
_userpicsCount,
|
||||
userpicsFilePath());
|
||||
}
|
||||
|
||||
Result HtmlWriter::writeStoriesStart(const Data::StoriesInfo &data) {
|
||||
Expects(_summary != nullptr);
|
||||
Expects(_stories == nullptr);
|
||||
|
||||
_storiesCount = data.count;
|
||||
if (!_storiesCount) {
|
||||
return Result::Success();
|
||||
}
|
||||
_stories = fileWithRelativePath(storiesFilePath());
|
||||
|
||||
auto block = _stories->pushHeader(
|
||||
"Stories archive",
|
||||
mainFileRelativePath());
|
||||
block.append(_stories->pushDiv("page_body list_page"));
|
||||
block.append(_stories->pushDiv("entry_list"));
|
||||
if (const auto result = _stories->writeBlock(block); !result) {
|
||||
return result;
|
||||
}
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result HtmlWriter::writeStoriesSlice(const Data::StoriesSlice &data) {
|
||||
Expects(_stories != nullptr);
|
||||
|
||||
_storiesCount -= data.skipped;
|
||||
if (data.list.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
auto block = QByteArray();
|
||||
for (const auto &story : data.list) {
|
||||
auto data = StoryData{};
|
||||
using SkipReason = Data::File::SkipReason;
|
||||
const auto &file = story.file();
|
||||
Assert(!file.relativePath.isEmpty()
|
||||
|| file.skipReason != SkipReason::None);
|
||||
auto status = QByteArrayList();
|
||||
if (story.pinned) {
|
||||
status.append("Saved to Profile");
|
||||
}
|
||||
if (story.expires > 0) {
|
||||
status.append("Expiring: " + Data::FormatDateTime(story.expires));
|
||||
}
|
||||
status.append([&]() -> Data::Utf8String {
|
||||
switch (file.skipReason) {
|
||||
case SkipReason::Unavailable:
|
||||
return "(Story unavailable, please try again later)";
|
||||
case SkipReason::FileSize:
|
||||
return "(Story exceeds maximum size. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::FileType:
|
||||
return "(Story not included. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::None: return Data::FormatFileSize(file.size);
|
||||
}
|
||||
Unexpected("Skip reason while writing story path.");
|
||||
}());
|
||||
const auto &path = story.file().relativePath;
|
||||
const auto &image = story.thumb().file.relativePath.isEmpty()
|
||||
? story.file().relativePath
|
||||
: story.thumb().file.relativePath;
|
||||
data.imageLink = Data::WriteImageThumb(
|
||||
_settings.path,
|
||||
image,
|
||||
kStoryThumbWidth * 2,
|
||||
kStoryThumbHeight * 2);
|
||||
const auto info = (story.date > 0)
|
||||
? Data::FormatDateTime(story.date)
|
||||
: QByteArray();
|
||||
block.append(_stories->pushStoriesListEntry(
|
||||
data,
|
||||
(path.isEmpty() ? QString("Story unavailable") : path).toUtf8(),
|
||||
status,
|
||||
info,
|
||||
story.caption,
|
||||
_environment.internalLinksDomain,
|
||||
path));
|
||||
}
|
||||
return _stories->writeBlock(block);
|
||||
}
|
||||
|
||||
Result HtmlWriter::writeStoriesEnd() {
|
||||
pushStoriesSection();
|
||||
if (_stories) {
|
||||
return base::take(_stories)->close();
|
||||
}
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
QString HtmlWriter::storiesFilePath() const {
|
||||
return "lists/stories.html";
|
||||
}
|
||||
|
||||
void HtmlWriter::pushStoriesSection() {
|
||||
pushSection(
|
||||
kStoriesPriority,
|
||||
"Stories archive",
|
||||
"stories",
|
||||
_storiesCount,
|
||||
storiesFilePath());
|
||||
}
|
||||
|
||||
Result HtmlWriter::writeContactsList(const Data::ContactsList &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
@ -2228,7 +2424,7 @@ Result HtmlWriter::writeSavedContacts(const Data::ContactsList &data) {
|
||||
}
|
||||
|
||||
pushSection(
|
||||
2,
|
||||
kContactsPriority,
|
||||
"Contacts",
|
||||
"contacts",
|
||||
data.list.size(),
|
||||
@ -2294,7 +2490,7 @@ Result HtmlWriter::writeFrequentContacts(const Data::ContactsList &data) {
|
||||
}
|
||||
|
||||
pushSection(
|
||||
3,
|
||||
kFrequentContactsPriority,
|
||||
"Frequent contacts",
|
||||
"frequent",
|
||||
size,
|
||||
@ -2360,7 +2556,7 @@ Result HtmlWriter::writeSessions(const Data::SessionsList &data) {
|
||||
}
|
||||
|
||||
pushSection(
|
||||
5,
|
||||
kSessionsPriority,
|
||||
"Sessions",
|
||||
"sessions",
|
||||
data.list.size(),
|
||||
@ -2406,7 +2602,7 @@ Result HtmlWriter::writeWebSessions(const Data::SessionsList &data) {
|
||||
}
|
||||
|
||||
pushSection(
|
||||
6,
|
||||
kWebSessionsPriority,
|
||||
"Web sessions",
|
||||
"web",
|
||||
data.webList.size(),
|
||||
@ -2418,7 +2614,7 @@ Result HtmlWriter::writeOtherData(const Data::File &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
pushSection(
|
||||
7,
|
||||
kOtherPriority,
|
||||
"Other data",
|
||||
"other",
|
||||
1,
|
||||
@ -2447,7 +2643,7 @@ Result HtmlWriter::writeDialogsStart(const Data::DialogsInfo &data) {
|
||||
}
|
||||
|
||||
pushSection(
|
||||
0,
|
||||
kChatsPriority,
|
||||
"Chats",
|
||||
"chats",
|
||||
data.chats.size() + data.left.size(),
|
||||
|
@ -35,6 +35,7 @@ private:
|
||||
};
|
||||
|
||||
struct UserpicData;
|
||||
struct StoryData;
|
||||
class PeersMap;
|
||||
struct MediaData;
|
||||
|
||||
@ -59,6 +60,10 @@ public:
|
||||
Result writeUserpicsSlice(const Data::UserpicsSlice &data) override;
|
||||
Result writeUserpicsEnd() override;
|
||||
|
||||
Result writeStoriesStart(const Data::StoriesInfo &data) override;
|
||||
Result writeStoriesSlice(const Data::StoriesSlice &data) override;
|
||||
Result writeStoriesEnd() override;
|
||||
|
||||
Result writeContactsList(const Data::ContactsList &data) override;
|
||||
|
||||
Result writeSessionsList(const Data::SessionsList &data) override;
|
||||
@ -125,8 +130,10 @@ private:
|
||||
const Data::PersonalInfo &data,
|
||||
const QString &userpicPath);
|
||||
void pushUserpicsSection();
|
||||
void pushStoriesSection();
|
||||
|
||||
[[nodiscard]] QString userpicsFilePath() const;
|
||||
[[nodiscard]] QString storiesFilePath() const;
|
||||
|
||||
[[nodiscard]] QByteArray wrapMessageLink(
|
||||
int messageId,
|
||||
@ -149,6 +156,9 @@ private:
|
||||
int _userpicsCount = 0;
|
||||
std::unique_ptr<Wrap> _userpics;
|
||||
|
||||
int _storiesCount = 0;
|
||||
std::unique_ptr<Wrap> _stories;
|
||||
|
||||
QString _dialogsRelativePath;
|
||||
Data::DialogInfo _dialog;
|
||||
DialogsMode _dialogsMode = DialogsMode::None;
|
||||
|
@ -887,6 +887,77 @@ Result JsonWriter::writeUserpicsEnd() {
|
||||
return _output->writeBlock(popNesting());
|
||||
}
|
||||
|
||||
Result JsonWriter::writeStoriesStart(const Data::StoriesInfo &data) {
|
||||
Expects(_output != nullptr);
|
||||
|
||||
auto block = prepareObjectItemStart("stories");
|
||||
return _output->writeBlock(block + pushNesting(Context::kArray));
|
||||
}
|
||||
|
||||
Result JsonWriter::writeStoriesSlice(const Data::StoriesSlice &data) {
|
||||
Expects(_output != nullptr);
|
||||
|
||||
if (data.list.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
auto block = QByteArray();
|
||||
for (const auto &story : data.list) {
|
||||
using SkipReason = Data::File::SkipReason;
|
||||
const auto &file = story.file();
|
||||
Assert(!file.relativePath.isEmpty()
|
||||
|| file.skipReason != SkipReason::None);
|
||||
const auto path = [&]() -> Data::Utf8String {
|
||||
switch (file.skipReason) {
|
||||
case SkipReason::Unavailable:
|
||||
return "(Photo unavailable, please try again later)";
|
||||
case SkipReason::FileSize:
|
||||
return "(Photo exceeds maximum size. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::FileType:
|
||||
return "(Photo not included. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::None: return FormatFilePath(file);
|
||||
}
|
||||
Unexpected("Skip reason while writing story path.");
|
||||
}();
|
||||
block.append(prepareArrayItemStart());
|
||||
block.append(SerializeObject(_context, {
|
||||
{
|
||||
"date",
|
||||
story.date ? SerializeDate(story.date) : QByteArray()
|
||||
},
|
||||
{
|
||||
"date_unixtime",
|
||||
story.date ? SerializeDateRaw(story.date) : QByteArray()
|
||||
},
|
||||
{
|
||||
"expires",
|
||||
story.expires ? SerializeDate(story.expires) : QByteArray()
|
||||
},
|
||||
{
|
||||
"expires_unixtime",
|
||||
story.expires ? SerializeDateRaw(story.expires) : QByteArray()
|
||||
},
|
||||
{
|
||||
"pinned",
|
||||
story.pinned ? "true" : "false"
|
||||
},
|
||||
{
|
||||
"media",
|
||||
SerializeString(path)
|
||||
},
|
||||
}));
|
||||
}
|
||||
return _output->writeBlock(block);
|
||||
}
|
||||
|
||||
Result JsonWriter::writeStoriesEnd() {
|
||||
Expects(_output != nullptr);
|
||||
|
||||
return _output->writeBlock(popNesting());
|
||||
}
|
||||
|
||||
Result JsonWriter::writeContactsList(const Data::ContactsList &data) {
|
||||
Expects(_output != nullptr);
|
||||
|
||||
|
@ -44,6 +44,10 @@ public:
|
||||
Result writeUserpicsSlice(const Data::UserpicsSlice &data) override;
|
||||
Result writeUserpicsEnd() override;
|
||||
|
||||
Result writeStoriesStart(const Data::StoriesInfo &data) override;
|
||||
Result writeStoriesSlice(const Data::StoriesSlice &data) override;
|
||||
Result writeStoriesEnd() override;
|
||||
|
||||
Result writeContactsList(const Data::ContactsList &data) override;
|
||||
|
||||
Result writeSessionsList(const Data::SessionsList &data) override;
|
||||
|
@ -89,6 +89,13 @@ Content ContentFromState(
|
||||
case Step::Contacts:
|
||||
pushMain(tr::lng_export_option_contacts(tr::now));
|
||||
break;
|
||||
case Step::Stories:
|
||||
pushMain(tr::lng_export_option_stories(tr::now));
|
||||
pushBytes(
|
||||
"story" + QString::number(state.entityIndex),
|
||||
state.bytesName,
|
||||
state.bytesRandomId);
|
||||
break;
|
||||
case Step::Sessions:
|
||||
pushMain(tr::lng_export_option_sessions(tr::now));
|
||||
break;
|
||||
|
@ -173,6 +173,11 @@ void SettingsWidget::setupFullExportOptions(
|
||||
tr::lng_export_option_contacts(tr::now),
|
||||
Type::Contacts,
|
||||
tr::lng_export_option_contacts_about(tr::now));
|
||||
addOptionWithAbout(
|
||||
container,
|
||||
tr::lng_export_option_stories(tr::now),
|
||||
Type::Stories,
|
||||
tr::lng_export_option_stories_about(tr::now));
|
||||
addHeader(container, tr::lng_export_header_chats(tr::now));
|
||||
addOption(
|
||||
container,
|
||||
|
Loading…
Reference in New Issue
Block a user