mirror of
https://github.com/vale981/tdesktop
synced 2025-03-06 02:01:40 -05:00
Add export to JSON.
This commit is contained in:
parent
d056c00c67
commit
b5a65a4519
6 changed files with 952 additions and 14 deletions
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -8,12 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#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<AbstractWriter> CreateWriter(Format format) {
|
||||
return std::make_unique<TextWriter>();
|
||||
return std::make_unique<JsonWriter>();
|
||||
}
|
||||
|
||||
} // namespace Output
|
||||
|
|
847
Telegram/SourceFiles/export/output/export_output_json.cpp
Normal file
847
Telegram/SourceFiles/export/output/export_output_json.cpp
Normal file
|
@ -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 <QtCore/QDateTime>
|
||||
|
||||
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<std::pair<QByteArray, QByteArray>> &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<QByteArray> &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<Data::PeerId, Data::Peer> &peers,
|
||||
const QString &internalLinksDomain) {
|
||||
using namespace Data;
|
||||
|
||||
if (message.media.content.is<UnsupportedMedia>()) {
|
||||
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<std::pair<QByteArray, QByteArray>>{
|
||||
{ "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<std::decay_t<decltype(value)>>) {
|
||||
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<int32> &data,
|
||||
const QByteArray &label = "members") {
|
||||
auto list = std::vector<QByteArray>();
|
||||
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<QByteArray>();
|
||||
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<Data::TopPeer> &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<File> JsonWriter::fileWithRelativePath(
|
||||
const QString &path) const {
|
||||
return std::make_unique<File>(pathWithRelativePath(path), _stats);
|
||||
}
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
93
Telegram/SourceFiles/export/output/export_output_json.h
Normal file
93
Telegram/SourceFiles/export/output/export_output_json.h
Normal file
|
@ -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<bool>.
|
||||
std::vector<Type> 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<File> 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<File> _output;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
|
@ -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.");
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Reference in a new issue