diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css
index 456d59f46..79b680cc2 100644
--- a/Telegram/Resources/export_html/css/style.css
+++ b/Telegram/Resources/export_html/css/style.css
@@ -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);
}
diff --git a/Telegram/Resources/export_html/images/section_stories.png b/Telegram/Resources/export_html/images/section_stories.png
new file mode 100644
index 000000000..650c69c91
Binary files /dev/null and b/Telegram/Resources/export_html/images/section_stories.png differ
diff --git a/Telegram/Resources/export_html/images/section_stories@2x.png b/Telegram/Resources/export_html/images/section_stories@2x.png
new file mode 100644
index 000000000..429245138
Binary files /dev/null and b/Telegram/Resources/export_html/images/section_stories@2x.png differ
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index ad8e9037d..bbd952df0 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -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";
diff --git a/Telegram/Resources/qrc/telegram/export.qrc b/Telegram/Resources/qrc/telegram/export.qrc
index 06ecc7eb7..290d24a30 100644
--- a/Telegram/Resources/qrc/telegram/export.qrc
+++ b/Telegram/Resources/qrc/telegram/export.qrc
@@ -37,6 +37,8 @@
../../export_html/images/section_photos@2x.png
../../export_html/images/section_sessions.png
../../export_html/images/section_sessions@2x.png
+ ../../export_html/images/section_stories.png
+ ../../export_html/images/section_stories@2x.png
../../export_html/images/section_web.png
../../export_html/images/section_web@2x.png
../../export_html/js/script.js
diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp
index e2840acc4..e2bc9fde2 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.cpp
+++ b/Telegram/SourceFiles/export/data/export_data_types.cpp
@@ -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 &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(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()),
+ });
+ --result.skipped;
+ }
+ }, [](const auto &) {});
+ }
+ return result;
+}
+
std::pair WriteImageThumb(
const QString &basePath,
const QString &largePath,
diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h
index d383fdb94..27fb14ea1 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.h
+++ b/Telegram/SourceFiles/export/data/export_data_types.h
@@ -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 caption;
+
+ File &file();
+ const File &file() const;
+ Image &thumb();
+ const Image &thumb() const;
+};
+
+struct StoriesSlice {
+ std::vector list;
+ int32 lastId = 0;
+ int skipped = 0;
+};
+
+StoriesSlice ParseStoriesSlice(
+ const MTPVector &data,
+ int baseIndex);
+
Message ParseMessage(
ParseMediaContext &context,
const MTPMessage &data,
diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp
index 3e31914d4..1ff8d6774 100644
--- a/Telegram/SourceFiles/export/export_api_wrap.cpp
+++ b/Telegram/SourceFiles/export/export_api_wrap.cpp
@@ -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 start;
+ Fn fileProgress;
+ Fn handleSlice;
+ FnMut finish;
+
+ int processed = 0;
+ std::optional slice;
+ int offsetId = 0;
+ bool lastSlice = false;
+ int fileIndex = 0;
+};
+
struct ApiWrap::OtherDataProcess {
Data::File file;
FnMut 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 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 start,
+ Fn progress,
+ Fn slice,
+ FnMut finish) {
+ Expects(_storiesProcess == nullptr);
+
+ _storiesProcess = std::make_unique();
+ _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 done) {
Expects(_contactsProcess == nullptr);
@@ -1753,7 +1955,8 @@ bool ApiWrap::processFileLoad(
const Data::FileOrigin &origin,
Fn progress,
FnMut 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(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());
diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h
index 6384457d7..4723cd882 100644
--- a/Telegram/SourceFiles/export/export_api_wrap.h
+++ b/Telegram/SourceFiles/export/export_api_wrap.h
@@ -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 slice,
FnMut finish);
+ void requestStories(
+ FnMut start,
+ Fn progress,
+ Fn slice,
+ FnMut finish);
+
void requestContacts(FnMut done);
void requestSessions(FnMut 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 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 progress,
FnMut done,
- Data::Message *message = nullptr);
+ Data::Message *message = nullptr,
+ Data::Story *story = nullptr);
std::unique_ptr 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
class RequestBuilder;
@@ -228,6 +254,7 @@ private:
std::unique_ptr _fileCache;
std::unique_ptr _contactsProcess;
std::unique_ptr _userpicsProcess;
+ std::unique_ptr _storiesProcess;
std::unique_ptr _otherDataProcess;
std::unique_ptr _fileProcess;
std::unique_ptr _leftChannelsProcess;
diff --git a/Telegram/SourceFiles/export/export_controller.cpp b/Telegram/SourceFiles/export/export_controller.cpp
index 516c75cf7..b3d543f4c 100644
--- a/Telegram/SourceFiles/export/export_controller.cpp
+++ b/Telegram/SourceFiles/export/export_controller.cpp
@@ -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 fails to compile in MSVC :(
State _state;
rpl::event_stream _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('/');
diff --git a/Telegram/SourceFiles/export/export_controller.h b/Telegram/SourceFiles/export/export_controller.h
index e9b08acdc..fae4b54a1 100644
--- a/Telegram/SourceFiles/export/export_controller.h
+++ b/Telegram/SourceFiles/export/export_controller.h
@@ -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;
diff --git a/Telegram/SourceFiles/export/export_settings.h b/Telegram/SourceFiles/export/export_settings.h
index beac47760..be2e1b782 100644
--- a/Telegram/SourceFiles/export/export_settings.h
+++ b/Telegram/SourceFiles/export/export_settings.h
@@ -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;
@@ -91,6 +96,7 @@ struct Settings {
return Type::PersonalInfo
| Type::Userpics
| Type::Contacts
+ | Type::Stories
| Type::PersonalChats
| Type::PrivateGroups;
}
diff --git a/Telegram/SourceFiles/export/output/export_output_abstract.h b/Telegram/SourceFiles/export/output/export_output_abstract.h
index b98f65422..7a2bb65ea 100644
--- a/Telegram/SourceFiles/export/output/export_output_abstract.h
+++ b/Telegram/SourceFiles/export/output/export_output_abstract.h
@@ -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;
diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp
index 00c4155ad..efa4a7599 100644
--- a/Telegram/SourceFiles/export/output/export_output_html.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_html.cpp
@@ -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("
");
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 &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 &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(),
diff --git a/Telegram/SourceFiles/export/output/export_output_html.h b/Telegram/SourceFiles/export/output/export_output_html.h
index a2235d130..c1dee2ffd 100644
--- a/Telegram/SourceFiles/export/output/export_output_html.h
+++ b/Telegram/SourceFiles/export/output/export_output_html.h
@@ -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 _userpics;
+ int _storiesCount = 0;
+ std::unique_ptr _stories;
+
QString _dialogsRelativePath;
Data::DialogInfo _dialog;
DialogsMode _dialogsMode = DialogsMode::None;
diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp
index 1c6ba2c87..9dfd21620 100644
--- a/Telegram/SourceFiles/export/output/export_output_json.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_json.cpp
@@ -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);
diff --git a/Telegram/SourceFiles/export/output/export_output_json.h b/Telegram/SourceFiles/export/output/export_output_json.h
index 49f7035b0..879918fce 100644
--- a/Telegram/SourceFiles/export/output/export_output_json.h
+++ b/Telegram/SourceFiles/export/output/export_output_json.h
@@ -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;
diff --git a/Telegram/SourceFiles/export/view/export_view_content.cpp b/Telegram/SourceFiles/export/view/export_view_content.cpp
index fac8b3d76..9813321ef 100644
--- a/Telegram/SourceFiles/export/view/export_view_content.cpp
+++ b/Telegram/SourceFiles/export/view/export_view_content.cpp
@@ -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;
diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp
index d01d4eb53..94e00b43a 100644
--- a/Telegram/SourceFiles/export/view/export_view_settings.cpp
+++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp
@@ -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,