From b5a65a4519dca89af12ff0b9356b9ce0a0478d58 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 21 Jun 2018 21:42:50 +0100 Subject: [PATCH] Add export to JSON. --- .../SourceFiles/export/export_controller.cpp | 17 +- .../export/output/export_output_abstract.cpp | 3 +- .../export/output/export_output_json.cpp | 847 ++++++++++++++++++ .../export/output/export_output_json.h | 93 ++ .../export/output/export_output_text.cpp | 4 +- Telegram/gyp/lib_export.gyp | 2 + 6 files changed, 952 insertions(+), 14 deletions(-) create mode 100644 Telegram/SourceFiles/export/output/export_output_json.cpp create mode 100644 Telegram/SourceFiles/export/output/export_output_json.h diff --git a/Telegram/SourceFiles/export/export_controller.cpp b/Telegram/SourceFiles/export/export_controller.cpp index be1190d7eb4bcc..e8a8f04aaf4e5e 100644 --- a/Telegram/SourceFiles/export/export_controller.cpp +++ b/Telegram/SourceFiles/export/export_controller.cpp @@ -233,14 +233,11 @@ void Controller::startExport(const Settings &settings) { } bool Controller::normalizePath() { - const auto check = [&] { - return QDir().mkpath(_settings.path); - }; QDir folder(_settings.path); const auto path = folder.absolutePath(); _settings.path = path.endsWith('/') ? path : (path + '/'); if (!folder.exists()) { - return check(); + return true; } const auto mode = QDir::AllEntries | QDir::NoDotAndDotDot; const auto list = folder.entryInfoList(mode); @@ -260,7 +257,7 @@ bool Controller::normalizePath() { ++index; } _settings.path += add(index) + '/'; - return check(); + return true; } void Controller::fillExportSteps() { @@ -336,12 +333,7 @@ void Controller::cancelExportFast() { } void Controller::exportNext() { - if (!++_stepIndex) { - if (ioCatchError(_writer->start(_settings, &_stats))) { - return; - } - } - if (_stepIndex >= _steps.size()) { + if (++_stepIndex >= _steps.size()) { if (ioCatchError(_writer->finish())) { return; } @@ -370,6 +362,9 @@ void Controller::initialize() { setState(stateInitializing()); _api.startExport(_settings, &_stats, [=](ApiWrap::StartInfo info) { + if (ioCatchError(_writer->start(_settings, &_stats))) { + return; + } fillSubstepsInSteps(info); exportNext(); }); diff --git a/Telegram/SourceFiles/export/output/export_output_abstract.cpp b/Telegram/SourceFiles/export/output/export_output_abstract.cpp index eddd9ff08e33c5..bf4044bc150319 100644 --- a/Telegram/SourceFiles/export/output/export_output_abstract.cpp +++ b/Telegram/SourceFiles/export/output/export_output_abstract.cpp @@ -8,12 +8,13 @@ For license and copyright information please follow this link: #include "export/output/export_output_abstract.h" #include "export/output/export_output_text.h" +#include "export/output/export_output_json.h" namespace Export { namespace Output { std::unique_ptr CreateWriter(Format format) { - return std::make_unique(); + return std::make_unique(); } } // namespace Output diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp new file mode 100644 index 00000000000000..b9561867c177e2 --- /dev/null +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -0,0 +1,847 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "export/output/export_output_json.h" + +#include "export/output/export_output_result.h" +#include "export/data/export_data_types.h" +#include "core/utils.h" + +#include + +namespace Export { +namespace Output { +namespace { + +using Context = details::JsonContext; + +QByteArray SerializeString(const QByteArray &value) { + const auto size = value.size(); + const auto begin = value.data(); + const auto end = begin + size; + + auto result = QByteArray(); + result.reserve(2 + size * 4); + result.append('"'); + for (auto p = begin; p != end; ++p) { + const auto ch = *p; + if (ch == '\n') { + result.append("\\n", 2); + } else if (ch == '\r') { + result.append("\\r", 2); + } else if (ch == '\t') { + result.append("\\t", 2); + } else if (ch == '"') { + result.append("\\\"", 2); + } else if (ch == '\\') { + result.append("\\\\", 2); + } else if (ch >= 0 && ch < 32) { + result.append("\\x", 2).append('0' + (ch >> 4)); + const auto left = (ch & 0x0F); + if (left >= 10) { + result.append('A' + (left - 10)); + } else { + result.append('0' + left); + } + } else if (ch == 0xE2 && (p + 2 < end) && *(p + 1) == 0x80) { + if (*(p + 2) == 0xA8) { // Line separator. + result.append("\\u2028", 6); + } else if (*(p + 2) == 0xA9) { // Paragraph separator. + result.append("\\u2029", 6); + } else { + result.append(ch); + } + } else { + result.append(ch); + } + } + result.append('"'); + return result; +} + +QByteArray SerializeDate(TimeId date) { + return SerializeString( + QDateTime::fromTime_t(date).toString(Qt::ISODate).toUtf8()); +} + +QByteArray StringAllowEmpty(const Data::Utf8String &data) { + return data.isEmpty() ? data : SerializeString(data); +} + +QByteArray StringAllowNull(const Data::Utf8String &data) { + return data.isEmpty() ? QByteArray("null") : SerializeString(data); +} + +QByteArray Indentation(int size) { + return QByteArray(size, ' '); +} + +QByteArray Indentation(const Context &context) { + return Indentation(context.nesting.size()); +} + +QByteArray SerializeObject( + Context &context, + const std::vector> &values) { + const auto indent = Indentation(context); + + context.nesting.push_back(Context::kObject); + const auto guard = gsl::finally([&] { context.nesting.pop_back(); }); + const auto next = '\n' + Indentation(context); + + auto first = true; + auto result = QByteArray(); + result.append('{'); + for (const auto &[key, value] : values) { + if (value.isEmpty()) { + continue; + } + if (first) { + first = false; + } else { + result.append(','); + } + result.append(next).append(SerializeString(key)).append(": ", 2); + result.append(value); + } + result.append('\n').append(indent).append("}"); + return result; +} + +QByteArray SerializeArray( + Context &context, + const std::vector &values) { + const auto indent = Indentation(context.nesting.size()); + const auto next = '\n' + Indentation(context.nesting.size() + 1); + + auto first = true; + auto result = QByteArray(); + result.append('['); + for (const auto &value : values) { + if (first) { + first = false; + } else { + result.append(','); + } + result.append(next).append(value); + } + result.append('\n').append(indent).append("]"); + return result; +} + +Data::Utf8String FormatUsername(const Data::Utf8String &username) { + return username.isEmpty() ? username : ('@' + username); +} + +QByteArray FormatFilePath(const Data::File &file) { + return file.relativePath.toUtf8(); +} + +QByteArray SerializeMessage( + Context &context, + const Data::Message &message, + const std::map &peers, + const QString &internalLinksDomain) { + using namespace Data; + + if (message.media.content.is()) { + return SerializeObject(context, { + { "id", NumberToString(message.id) }, + { "type", SerializeString("unsupported") } + }); + } + + const auto peer = [&](PeerId peerId) -> const Peer& { + if (const auto i = peers.find(peerId); i != end(peers)) { + return i->second; + } + static auto empty = Peer{ User() }; + return empty; + }; + const auto user = [&](int32 userId) -> const User& { + if (const auto result = peer(UserPeerId(userId)).user()) { + return *result; + } + static auto empty = User(); + return empty; + }; + const auto chat = [&](int32 chatId) -> const Chat& { + if (const auto result = peer(ChatPeerId(chatId)).chat()) { + return *result; + } + static auto empty = Chat(); + return empty; + }; + + auto values = std::vector>{ + { "id", NumberToString(message.id) }, + { + "type", + SerializeString(message.action.content ? "service" : "message") + }, + { "date", SerializeDate(message.date) }, + { "edited", SerializeDate(message.edited) }, + }; + + context.nesting.push_back(Context::kObject); + const auto serialized = [&] { + context.nesting.pop_back(); + return SerializeObject(context, values); + }; + + const auto pushBare = [&]( + const QByteArray &key, + const QByteArray &value) { + if (!value.isEmpty()) { + values.emplace_back(key, value); + } + }; + const auto push = [&](const QByteArray &key, const auto &value) { + if constexpr (std::is_arithmetic_v>) { + pushBare(key, NumberToString(value)); + } else { + const auto wrapped = QByteArray(value); + if (!wrapped.isEmpty()) { + pushBare(key, SerializeString(wrapped)); + } + } + }; + const auto wrapPeerName = [&](PeerId peerId) { + return StringAllowNull(peer(peerId).name()); + }; + const auto wrapUserName = [&](int32 userId) { + return StringAllowNull(user(userId).name()); + }; + const auto pushFrom = [&](const QByteArray &label = "from") { + if (message.fromId) { + pushBare(label, wrapUserName(message.fromId)); + } + }; + const auto pushReplyToMsgId = [&]( + const QByteArray &label = "reply_to_message_id") { + if (message.replyToMsgId) { + push(label, message.replyToMsgId); + } + }; + const auto pushUserNames = [&]( + const std::vector &data, + const QByteArray &label = "members") { + auto list = std::vector(); + for (const auto userId : data) { + list.push_back(wrapUserName(userId)); + } + pushBare(label, SerializeArray(context, list)); + }; + const auto pushActor = [&] { + pushFrom("actor"); + }; + const auto pushAction = [&](const QByteArray &action) { + push("action", action); + }; + const auto pushTTL = [&]( + const QByteArray &label = "self_destruct_period_seconds") { + if (const auto ttl = message.media.ttl) { + push(label, ttl); + } + }; + + using SkipReason = Data::File::SkipReason; + const auto pushPath = [&]( + const Data::File &file, + const QByteArray &label, + const QByteArray &name = QByteArray()) { + Expects(!file.relativePath.isEmpty() + || file.skipReason != SkipReason::None); + + push(label, [&]() -> QByteArray { + const auto pre = name.isEmpty() ? QByteArray() : name + ' '; + switch (file.skipReason) { + case SkipReason::Unavailable: return pre + "(file unavailable)"; + case SkipReason::FileSize: return pre + "(file too large)"; + case SkipReason::FileType: return pre + "(file skipped)"; + case SkipReason::None: return FormatFilePath(file); + } + Unexpected("Skip reason while writing file path."); + }()); + }; + const auto pushPhoto = [&](const Image &image) { + pushPath(image.file, "photo"); + if (image.width && image.height) { + push("width", image.width); + push("height", image.height); + } + }; + + message.action.content.match([&](const ActionChatCreate &data) { + pushActor(); + pushAction("create_group"); + push("title", data.title); + pushUserNames(data.userIds); + }, [&](const ActionChatEditTitle &data) { + pushActor(); + pushAction("edit_group_title"); + push("title", data.title); + }, [&](const ActionChatEditPhoto &data) { + pushActor(); + pushAction("edit_group_photo"); + pushPhoto(data.photo.image); + }, [&](const ActionChatDeletePhoto &data) { + pushActor(); + pushAction("delete_group_photo"); + }, [&](const ActionChatAddUser &data) { + pushActor(); + pushAction("invite_members"); + pushUserNames(data.userIds); + }, [&](const ActionChatDeleteUser &data) { + pushActor(); + pushAction("remove_members"); + pushUserNames({ data.userId }); + }, [&](const ActionChatJoinedByLink &data) { + pushActor(); + pushAction("join_group_by_link"); + pushBare("inviter", wrapUserName(data.inviterId)); + }, [&](const ActionChannelCreate &data) { + pushActor(); + pushAction("create_channel"); + push("title", data.title); + }, [&](const ActionChatMigrateTo &data) { + pushActor(); + pushAction("migrate_to_supergroup"); + }, [&](const ActionChannelMigrateFrom &data) { + pushActor(); + pushAction("migrate_from_group"); + push("title", data.title); + }, [&](const ActionPinMessage &data) { + pushActor(); + pushAction("pin_message"); + pushReplyToMsgId("message_id"); + }, [&](const ActionHistoryClear &data) { + pushActor(); + pushAction("clear_history"); + }, [&](const ActionGameScore &data) { + pushActor(); + pushAction("score_in_game"); + pushReplyToMsgId("game_message_id"); + push("score", data.score); + }, [&](const ActionPaymentSent &data) { + pushAction("send_payment"); + push("amount", data.amount); + push("currency", data.currency); + pushReplyToMsgId("invoice_message_id"); + }, [&](const ActionPhoneCall &data) { + pushActor(); + pushAction("phone_call"); + if (data.duration) { + push("duration_seconds", data.duration); + } + using Reason = ActionPhoneCall::DiscardReason; + push("discard_reason", [&] { + switch (data.discardReason) { + case Reason::Busy: return "busy"; + case Reason::Disconnect: return "disconnect"; + case Reason::Hangup: return "hangup"; + case Reason::Missed: return "missed"; + } + return ""; + }()); + }, [&](const ActionScreenshotTaken &data) { + pushActor(); + pushAction("take_screenshot"); + }, [&](const ActionCustomAction &data) { + pushActor(); + push("information_text", data.message); + }, [&](const ActionBotAllowed &data) { + pushAction("allow_sending_messages"); + push("reason_domain", data.domain); + }, [&](const ActionSecureValuesSent &data) { + pushAction("send_passport_values"); + auto list = std::vector(); + for (const auto type : data.types) { + list.push_back(SerializeString([&] { + using Type = ActionSecureValuesSent::Type; + switch (type) { + case Type::PersonalDetails: return "personal_details"; + case Type::Passport: return "passport"; + case Type::DriverLicense: return "driver_license"; + case Type::IdentityCard: return "identity_card"; + case Type::InternalPassport: return "internal_passport"; + case Type::Address: return "address_information"; + case Type::UtilityBill: return "utility_bill"; + case Type::BankStatement: return "bank_statement"; + case Type::RentalAgreement: return "rental_agreement"; + case Type::PassportRegistration: + return "passport_registration"; + case Type::TemporaryRegistration: + return "temporary_registration"; + case Type::Phone: return "phone_number"; + case Type::Email: return "email"; + } + return ""; + }())); + } + pushBare("values", SerializeArray(context, list)); + }, [](const base::none_type &) {}); + + if (!message.action.content) { + pushFrom(); + push("author", message.signature); + if (message.forwardedFromId) { + pushBare( + "forwarded_from", + wrapPeerName(message.forwardedFromId)); + } + pushReplyToMsgId(); + if (message.viaBotId) { + const auto username = FormatUsername( + user(message.viaBotId).username); + if (!username.isEmpty()) { + push("via_bot", username); + } + } + } + + message.media.content.match([&](const Photo &photo) { + pushPhoto(photo.image); + pushTTL(); + }, [&](const Document &data) { + pushPath(data.file, "file"); + const auto pushType = [&](const QByteArray &value) { + push("media_type", value); + }; + if (data.isSticker) { + pushType("sticker"); + push("sticker_emoji", data.stickerEmoji); + } else if (data.isVideoMessage) { + pushType("video_message"); + } else if (data.isVoiceMessage) { + pushType("voice_message"); + } else if (data.isAnimated) { + pushType("animation"); + } else if (data.isVideoFile) { + pushType("video_file"); + } else if (data.isAudioFile) { + pushType("audio_file"); + push("performer", data.songPerformer); + push("title", data.songTitle); + } + if (!data.isSticker) { + push("mime_type", data.mime); + } + if (data.duration) { + push("duration_seconds", data.duration); + } + if (data.width && data.height) { + push("width", data.width); + push("height", data.height); + } + pushTTL(); + }, [&](const ContactInfo &data) { + pushBare("contact_information", SerializeObject(context, { + { "first_name", SerializeString(data.firstName) }, + { "last_name", SerializeString(data.lastName) }, + { + "phone_number", + SerializeString(FormatPhoneNumber(data.phoneNumber)) + }, + })); + }, [&](const GeoPoint &data) { + pushBare( + "location_information", + data.valid ? SerializeObject(context, { + { "latitude", NumberToString(data.latitude) }, + { "longitude", NumberToString(data.longitude) }, + }) : QByteArray("null")); + pushTTL("live_location_period_seconds"); + }, [&](const Venue &data) { + push("place_name", data.title); + push("address", data.address); + if (data.point.valid) { + pushBare("location_information", SerializeObject(context, { + { "latitude", NumberToString(data.point.latitude) }, + { "longitude", NumberToString(data.point.longitude) }, + })); + } + }, [&](const Game &data) { + push("game_title", data.title); + push("game_description", data.description); + if (data.botId != 0 && !data.shortName.isEmpty()) { + const auto bot = user(data.botId); + if (bot.isBot && !bot.username.isEmpty()) { + push("game_link", internalLinksDomain.toUtf8() + + bot.username + + "?game=" + + data.shortName); + } + } + }, [&](const Invoice &data) { + push("invoice_information", SerializeObject(context, { + { "title", SerializeString(data.title) }, + { "description", SerializeString(data.description) }, + { "amount", NumberToString(data.amount) }, + { "currency", SerializeString(data.currency) }, + { "receipt_message_id", (data.receiptMsgId + ? NumberToString(data.receiptMsgId) + : QByteArray()) } + })); + }, [](const UnsupportedMedia &data) { + Unexpected("Unsupported message."); + }, [](const base::none_type &) {}); + + push("text", message.text); + + return serialized(); +} + +} // namespace + +Result JsonWriter::start(const Settings &settings, Stats *stats) { + Expects(_output == nullptr); + Expects(settings.path.endsWith('/')); + + _settings = base::duplicate(settings); + _stats = stats; + _output = fileWithRelativePath(mainFileRelativePath()); + + return _output->writeBlock(pushNesting(Context::kObject)); +} + +QByteArray JsonWriter::pushNesting(Context::Type type) { + Expects(_output != nullptr); + + _context.nesting.push_back(type); + _currentNestingHadItem = false; + return (type == Context::kObject ? "{" : "["); +} + +QByteArray JsonWriter::prepareObjectItemStart(const QByteArray &key) { + const auto guard = gsl::finally([&] { _currentNestingHadItem = true; }); + return (_currentNestingHadItem ? ",\n" : "\n") + + Indentation(_context) + + SerializeString(key) + + ": "; +} + +QByteArray JsonWriter::prepareArrayItemStart() { + const auto guard = gsl::finally([&] { _currentNestingHadItem = true; }); + return (_currentNestingHadItem ? ",\n" : "\n") + Indentation(_context); +} + +QByteArray JsonWriter::popNesting() { + Expects(_output != nullptr); + Expects(!_context.nesting.empty()); + + const auto type = Context::Type(_context.nesting.back()); + _context.nesting.pop_back(); + + _currentNestingHadItem = true; + return '\n' + + Indentation(_context) + + (type == Context::kObject ? '}' : ']'); +} + +Result JsonWriter::writePersonal(const Data::PersonalInfo &data) { + Expects(_output != nullptr); + + const auto &info = data.user.info; + return _output->writeBlock( + prepareObjectItemStart("personal_information") + + SerializeObject(_context, { + { "first_name", SerializeString(info.firstName) }, + { "last_name", SerializeString(info.lastName) }, + { + "phone_number", + SerializeString(Data::FormatPhoneNumber(info.phoneNumber)) + }, + { + "username", + (!data.user.username.isEmpty() + ? SerializeString(FormatUsername(data.user.username)) + : QByteArray()) + }, + { + "bio", + (!data.bio.isEmpty() + ? SerializeString(data.bio) + : QByteArray()) + }, + })); +} + +Result JsonWriter::writeUserpicsStart(const Data::UserpicsInfo &data) { + Expects(_output != nullptr); + + auto block = prepareObjectItemStart("personal_photos"); + return _output->writeBlock(block + pushNesting(Context::kArray)); +} + +Result JsonWriter::writeUserpicsSlice(const Data::UserpicsSlice &data) { + Expects(_output != nullptr); + Expects(!data.list.empty()); + + auto block = QByteArray(); + for (const auto &userpic : data.list) { + block.append(prepareArrayItemStart()); + block.append(SerializeObject(_context, { + { + "date", + userpic.date ? SerializeDate(userpic.date) : QByteArray() + }, + { + "photo", + SerializeString(userpic.image.file.relativePath.isEmpty() + ? QByteArray("(file unavailable)") + : FormatFilePath(userpic.image.file)) + }, + })); + } + return _output->writeBlock(block); +} + +Result JsonWriter::writeUserpicsEnd() { + Expects(_output != nullptr); + + return _output->writeBlock(popNesting()); +} + +Result JsonWriter::writeContactsList(const Data::ContactsList &data) { + Expects(_output != nullptr); + + if (const auto result = writeSavedContacts(data); !result) { + return result; + } else if (const auto result = writeFrequentContacts(data); !result) { + return result; + } + return Result::Success(); +} + +Result JsonWriter::writeSavedContacts(const Data::ContactsList &data) { + Expects(_output != nullptr); + + auto block = prepareObjectItemStart("contacts"); + block.append(pushNesting(Context::kArray)); + for (const auto index : Data::SortedContactsIndices(data)) { + const auto &contact = data.list[index]; + block.append(prepareArrayItemStart()); + + if (contact.firstName.isEmpty() + && contact.lastName.isEmpty() + && contact.phoneNumber.isEmpty()) { + block.append(SerializeObject(_context, { + { "date", SerializeDate(contact.date) } + })); + } else { + block.append(SerializeObject(_context, { + { "first_name", SerializeString(contact.firstName) }, + { "last_name", SerializeString(contact.lastName) }, + { + "phone_number", + SerializeString( + Data::FormatPhoneNumber(contact.phoneNumber)) + }, + { "date", SerializeDate(contact.date) } + })); + } + } + return _output->writeBlock(block + popNesting()); +} + +Result JsonWriter::writeFrequentContacts(const Data::ContactsList &data) { + Expects(_output != nullptr); + + auto block = prepareObjectItemStart("frequent_contacts"); + block.append(pushNesting(Context::kArray)); + const auto writeList = [&]( + const std::vector &peers, + Data::Utf8String category) { + for (const auto &top : peers) { + const auto type = [&] { + if (const auto chat = top.peer.chat()) { + return chat->username.isEmpty() + ? (chat->broadcast + ? "private_channel" + : "private_group") + : (chat->broadcast + ? "public_channel" + : "public_group"); + } + return "user"; + }(); + block.append(prepareArrayItemStart()); + block.append(SerializeObject(_context, { + { "category", SerializeString(category) }, + { "type", SerializeString(type) }, + { "name", StringAllowNull(top.peer.name()) }, + { "rating", Data::NumberToString(top.rating) }, + })); + } + }; + writeList(data.correspondents, "correspondents"); + writeList(data.inlineBots, "inline_bots"); + writeList(data.phoneCalls, "calls"); + return _output->writeBlock(block + popNesting()); +} + +Result JsonWriter::writeSessionsList(const Data::SessionsList &data) { + Expects(_output != nullptr); + + auto block = prepareObjectItemStart("sessions"); + block.append(pushNesting(Context::kArray)); + for (const auto &session : data.list) { + block.append(prepareArrayItemStart()); + block.append(SerializeObject(_context, { + { "last_active", SerializeDate(session.lastActive) }, + { "last_ip", SerializeString(session.ip) }, + { "last_country", SerializeString(session.country) }, + { "last_region", SerializeString(session.region) }, + { + "application_name", + StringAllowNull(session.applicationName) + }, + { + "application_version", + StringAllowEmpty(session.applicationVersion) + }, + { "device_model", SerializeString(session.deviceModel) }, + { "platform", SerializeString(session.platform) }, + { "system_version", SerializeString(session.systemVersion) }, + { "created", SerializeDate(session.created) }, + })); + } + return _output->writeBlock(block + popNesting()); +} + +Result JsonWriter::writeDialogsStart(const Data::DialogsInfo &data) { + return writeChatsStart(data, "chats"); +} + +Result JsonWriter::writeDialogStart(const Data::DialogInfo &data) { + return writeChatStart(data); +} + +Result JsonWriter::writeDialogSlice(const Data::MessagesSlice &data) { + return writeChatSlice(data); +} + +Result JsonWriter::writeDialogEnd() { + return writeChatEnd(); +} + +Result JsonWriter::writeDialogsEnd() { + return writeChatsEnd(); +} + +Result JsonWriter::writeLeftChannelsStart(const Data::DialogsInfo &data) { + return writeChatsStart(data, "left_chats"); +} + +Result JsonWriter::writeLeftChannelStart(const Data::DialogInfo &data) { + return writeChatStart(data); +} + +Result JsonWriter::writeLeftChannelSlice(const Data::MessagesSlice &data) { + return writeChatSlice(data); +} + +Result JsonWriter::writeLeftChannelEnd() { + return writeChatEnd(); +} + +Result JsonWriter::writeLeftChannelsEnd() { + return writeChatsEnd(); +} + +Result JsonWriter::writeChatsStart( + const Data::DialogsInfo &data, + const QByteArray &listName) { + Expects(_output != nullptr); + + auto block = prepareObjectItemStart(listName); + return _output->writeBlock(block + pushNesting(Context::kArray)); +} + +Result JsonWriter::writeChatStart(const Data::DialogInfo &data) { + Expects(_output != nullptr); + + using Type = Data::DialogInfo::Type; + const auto TypeString = [](Type type) { + switch (type) { + case Type::Unknown: return ""; + case Type::Personal: return "personal_chat"; + case Type::Bot: return "bot_chat"; + case Type::PrivateGroup: return "private_group"; + case Type::PublicGroup: return "public_group"; + case Type::PrivateChannel: return "private_channel"; + case Type::PublicChannel: return "public_channel"; + } + Unexpected("Dialog type in TypeString."); + }; + + auto block = prepareArrayItemStart(); + block.append(pushNesting(Context::kObject)); + block.append(prepareObjectItemStart("name") + + StringAllowNull(data.name)); + block.append(prepareObjectItemStart("type") + + StringAllowNull(TypeString(data.type))); + block.append(prepareObjectItemStart("messages")); + block.append(pushNesting(Context::kArray)); + return _output->writeBlock(block); +} + +Result JsonWriter::writeChatSlice(const Data::MessagesSlice &data) { + Expects(_output != nullptr); + + auto block = QByteArray(); + for (const auto &message : data.list) { + block.append(prepareArrayItemStart() + SerializeMessage( + _context, + message, + data.peers, + _settings.internalLinksDomain)); + } + return _output->writeBlock(block); +} + +Result JsonWriter::writeChatEnd() { + Expects(_output != nullptr); + + auto block = popNesting(); + return _output->writeBlock(block + popNesting()); +} + +Result JsonWriter::writeChatsEnd() { + Expects(_output != nullptr); + + return _output->writeBlock(popNesting()); +} + +Result JsonWriter::finish() { + Expects(_output != nullptr); + + auto block = popNesting(); + Assert(_context.nesting.empty()); + return _output->writeBlock(block); +} + +QString JsonWriter::mainFilePath() { + return pathWithRelativePath(mainFileRelativePath()); +} + +QString JsonWriter::mainFileRelativePath() const { + return "result.json"; +} + +QString JsonWriter::pathWithRelativePath(const QString &path) const { + return _settings.path + path; +} + +std::unique_ptr JsonWriter::fileWithRelativePath( + const QString &path) const { + return std::make_unique(pathWithRelativePath(path), _stats); +} + +} // namespace Output +} // namespace Export diff --git a/Telegram/SourceFiles/export/output/export_output_json.h b/Telegram/SourceFiles/export/output/export_output_json.h new file mode 100644 index 00000000000000..fe34afa7f33430 --- /dev/null +++ b/Telegram/SourceFiles/export/output/export_output_json.h @@ -0,0 +1,93 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "export/output/export_output_abstract.h" +#include "export/output/export_output_file.h" +#include "export/export_settings.h" +#include "export/data/export_data_types.h" + +namespace Export { +namespace Output { +namespace details { + +struct JsonContext { + using Type = bool; + static const auto kObject = Type(true); + static const auto kArray = Type(false); + + // Always fun to use std::vector. + std::vector nesting; +}; + +} // namespace details + +class JsonWriter : public AbstractWriter { +public: + Result start(const Settings &settings, Stats *stats) override; + + Result writePersonal(const Data::PersonalInfo &data) override; + + Result writeUserpicsStart(const Data::UserpicsInfo &data) override; + Result writeUserpicsSlice(const Data::UserpicsSlice &data) override; + Result writeUserpicsEnd() override; + + Result writeContactsList(const Data::ContactsList &data) override; + + Result writeSessionsList(const Data::SessionsList &data) override; + + Result writeDialogsStart(const Data::DialogsInfo &data) override; + Result writeDialogStart(const Data::DialogInfo &data) override; + Result writeDialogSlice(const Data::MessagesSlice &data) override; + Result writeDialogEnd() override; + Result writeDialogsEnd() override; + + Result writeLeftChannelsStart(const Data::DialogsInfo &data) override; + Result writeLeftChannelStart(const Data::DialogInfo &data) override; + Result writeLeftChannelSlice(const Data::MessagesSlice &data) override; + Result writeLeftChannelEnd() override; + Result writeLeftChannelsEnd() override; + + Result finish() override; + + QString mainFilePath() override; + +private: + using Context = details::JsonContext; + + [[nodiscard]] QByteArray pushNesting(Context::Type type); + [[nodiscard]] QByteArray prepareObjectItemStart(const QByteArray &key); + [[nodiscard]] QByteArray prepareArrayItemStart(); + [[nodiscard]] QByteArray popNesting(); + + QString mainFileRelativePath() const; + QString pathWithRelativePath(const QString &path) const; + std::unique_ptr fileWithRelativePath(const QString &path) const; + + Result writeSavedContacts(const Data::ContactsList &data); + Result writeFrequentContacts(const Data::ContactsList &data); + + Result writeChatsStart( + const Data::DialogsInfo &data, + const QByteArray &listName); + Result writeChatStart(const Data::DialogInfo &data); + Result writeChatSlice(const Data::MessagesSlice &data); + Result writeChatEnd(); + Result writeChatsEnd(); + + Settings _settings; + Stats *_stats = nullptr; + Context _context; + bool _currentNestingHadItem = false; + + std::unique_ptr _output; + +}; + +} // namespace Output +} // namespace Export diff --git a/Telegram/SourceFiles/export/output/export_output_text.cpp b/Telegram/SourceFiles/export/output/export_output_text.cpp index 4d10b941689a0e..3c8ee7e9e68217 100644 --- a/Telegram/SourceFiles/export/output/export_output_text.cpp +++ b/Telegram/SourceFiles/export/output/export_output_text.cpp @@ -696,7 +696,7 @@ Result TextWriter::writeLeftChannelEnd() { } Result TextWriter::writeLeftChannelsEnd() { - return Result::Success(); + return writeChatsEnd(); } Result TextWriter::writeChatsStart( @@ -768,7 +768,7 @@ Result TextWriter::writeChatEnd() { case Type::PrivateGroup: return "Private group"; case Type::PublicGroup: return "Public group"; case Type::PrivateChannel: return "Private channel"; - case Type::PublicChannel: return "Private channel"; + case Type::PublicChannel: return "Public channel"; } Unexpected("Dialog type in TypeString."); }; diff --git a/Telegram/gyp/lib_export.gyp b/Telegram/gyp/lib_export.gyp index 5a98a1c917a57d..d9912ac6fec8be 100644 --- a/Telegram/gyp/lib_export.gyp +++ b/Telegram/gyp/lib_export.gyp @@ -61,6 +61,8 @@ '<(src_loc)/export/output/export_output_abstract.h', '<(src_loc)/export/output/export_output_file.cpp', '<(src_loc)/export/output/export_output_file.h', + '<(src_loc)/export/output/export_output_json.cpp', + '<(src_loc)/export/output/export_output_json.h', '<(src_loc)/export/output/export_output_stats.cpp', '<(src_loc)/export/output/export_output_stats.h', '<(src_loc)/export/output/export_output_text.cpp',