Add stories to data export.

This commit is contained in:
John Preston 2023-06-16 21:23:40 +04:00
parent 08c4f1f67a
commit 2a1631247d
19 changed files with 812 additions and 29 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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";

View File

@ -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>

View 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,

View File

@ -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,

View File

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

View File

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

View File

@ -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('/');

View File

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

View File

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

View File

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

View File

@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

@ -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,