Add basic HTML export.

This commit is contained in:
John Preston 2018-06-23 21:27:41 +01:00
parent e708065446
commit 9d66f9cc03
22 changed files with 1904 additions and 66 deletions

View file

@ -0,0 +1,3 @@
.page_wrap {
background-color: #fff;
}

View file

@ -1681,7 +1681,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_export_option_size_limit" = "Size limit: {size}";
"lng_export_header_format" = "Location and format";
"lng_export_option_location" = "Download path: {path}";
"lng_export_option_text" = "Human-readable text";
"lng_export_option_html" = "Human-readable HTML";
"lng_export_option_json" = "Machine-readable JSON";
"lng_export_start" = "Export";
"lng_export_state_initializing" = "Initializing...";

View file

@ -1,4 +1,7 @@
<RCC>
<qresource prefix="/export">
<file alias="css/style.css">../css/export_style.css</file>
</qresource>
<qresource prefix="/gui">
<file alias="fonts/OpenSans-Regular.ttf">../fonts/OpenSans-Regular.ttf</file>
<file alias="fonts/OpenSans-Bold.ttf">../fonts/OpenSans-Bold.ttf</file>

View file

@ -458,6 +458,9 @@ User ParseUser(const MTPUser &data) {
if (data.has_bot_info_version()) {
result.isBot = true;
}
if (data.is_self()) {
result.isSelf = true;
}
const auto access_hash = data.has_access_hash()
? data.vaccess_hash
: MTP_long(0);
@ -492,7 +495,8 @@ Chat ParseChat(const MTPChat &data) {
result.input = MTP_inputPeerChat(MTP_int(result.id));
}, [&](const MTPDchannel &data) {
result.id = data.vid.v;
result.broadcast = data.is_broadcast();
result.isBroadcast = data.is_broadcast();
result.isSupergroup = data.is_megagroup();
result.title = ParseString(data.vtitle);
if (data.has_username()) {
result.username = ParseString(data.vusername);
@ -502,7 +506,8 @@ Chat ParseChat(const MTPChat &data) {
data.vaccess_hash);
}, [&](const MTPDchannelForbidden &data) {
result.id = data.vid.v;
result.broadcast = data.is_broadcast();
result.isBroadcast = data.is_broadcast();
result.isSupergroup = data.is_megagroup();
result.title = ParseString(data.vtitle);
result.input = MTP_inputPeerChannel(
MTP_int(result.id),
@ -1102,16 +1107,22 @@ SessionsList ParseWebSessionsList(
DialogInfo::Type DialogTypeFromChat(const Chat &chat) {
using Type = DialogInfo::Type;
return chat.username.isEmpty()
? (chat.broadcast
? (chat.isBroadcast
? Type::PrivateChannel
: chat.isSupergroup
? Type::PrivateSupergroup
: Type::PrivateGroup)
: (chat.broadcast
: (chat.isBroadcast
? Type::PublicChannel
: Type::PublicGroup);
: Type::PublicSupergroup);
}
DialogInfo::Type DialogTypeFromUser(const User &user) {
return user.isBot ? DialogInfo::Type::Bot : DialogInfo::Type::Personal;
return user.isSelf
? DialogInfo::Type::Self
: user.isBot
? DialogInfo::Type::Bot
: DialogInfo::Type::Personal;
}
DialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data) {
@ -1185,11 +1196,13 @@ void FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings) {
using Type = Settings::Type;
const auto setting = [&] {
switch (dialog.type) {
case DialogType::Self:
case DialogType::Personal: return Type::PersonalChats;
case DialogType::Bot: return Type::BotChats;
case DialogType::PrivateGroup: return Type::PrivateGroups;
case DialogType::PrivateGroup:
case DialogType::PrivateSupergroup: return Type::PrivateGroups;
case DialogType::PrivateChannel: return Type::PrivateChannels;
case DialogType::PublicGroup: return Type::PublicGroups;
case DialogType::PublicSupergroup: return Type::PublicGroups;
case DialogType::PublicChannel: return Type::PublicChannels;
}
Unexpected("Type in ApiWrap::onlyMyMessages.");

View file

@ -162,6 +162,7 @@ struct User {
ContactInfo info;
Utf8String username;
bool isBot = false;
bool isSelf = false;
MTPInputUser input = MTP_inputUserEmpty();
@ -175,7 +176,8 @@ struct Chat {
int32 id = 0;
Utf8String title;
Utf8String username;
bool broadcast = false;
bool isBroadcast = false;
bool isSupergroup = false;
MTPInputPeer input = MTP_inputPeerEmpty();
};
@ -466,10 +468,12 @@ std::map<uint64, Message> ParseMessagesList(
struct DialogInfo {
enum class Type {
Unknown,
Self,
Personal,
Bot,
PrivateGroup,
PublicGroup,
PrivateSupergroup,
PublicSupergroup,
PrivateChannel,
PublicChannel,
};
@ -479,7 +483,7 @@ struct DialogInfo {
MTPInputPeer input = MTP_inputPeerEmpty();
int32 topMessageId = 0;
TimeId topMessageDate = 0;
PeerId peerId = 0;
PeerId peerId;
// User messages splits which contained that dialog.
std::vector<int> splits;

View file

@ -69,13 +69,15 @@ LocationKey ComputeLocationKey(const Data::FileLocation &value) {
Settings::Type SettingsFromDialogsType(Data::DialogInfo::Type type) {
using DialogType = Data::DialogInfo::Type;
switch (type) {
case DialogType::Self:
case DialogType::Personal:
return Settings::Type::PersonalChats;
case DialogType::Bot:
return Settings::Type::BotChats;
case DialogType::PrivateGroup:
case DialogType::PrivateSupergroup:
return Settings::Type::PrivateGroups;
case DialogType::PublicGroup:
case DialogType::PublicSupergroup:
return Settings::Type::PublicGroups;
case DialogType::PrivateChannel:
return Settings::Type::PrivateChannels;

View file

@ -223,43 +223,12 @@ void Controller::startExport(const Settings &settings) {
}
_settings = base::duplicate(settings);
if (!normalizePath()) {
ioError(_settings.path);
return;
}
_settings.path = Output::NormalizePath(_settings.path);
_writer = Output::CreateWriter(_settings.format);
fillExportSteps();
exportNext();
}
bool Controller::normalizePath() {
QDir folder(_settings.path);
const auto path = folder.absolutePath();
_settings.path = path.endsWith('/') ? path : (path + '/');
if (!folder.exists()) {
return true;
}
const auto mode = QDir::AllEntries | QDir::NoDotAndDotDot;
const auto list = folder.entryInfoList(mode);
if (list.isEmpty()) {
return true;
}
const auto date = QDate::currentDate();
const auto base = QString("DataExport_%1_%2_%3"
).arg(date.day(), 2, 10, QChar('0')
).arg(date.month(), 2, 10, QChar('0')
).arg(date.year());
const auto add = [&](int i) {
return base + (i ? " (" + QString::number(i) + ')' : QString());
};
auto index = 0;
while (QDir(_settings.path + add(index)).exists()) {
++index;
}
_settings.path += add(index) + '/';
return true;
}
void Controller::fillExportSteps() {
using Type = Settings::Type;
_steps.push_back(Step::Initializing);

View file

@ -8,18 +8,476 @@ 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_html.h"
#include "export/output/export_output_json.h"
#include "export/output/export_output_stats.h"
#include "export/output/export_output_result.h"
#include <QtCore/QDir>
#include <QtCore/QDate>
namespace Export {
namespace Output {
QString NormalizePath(const QString &source) {
QDir folder(source);
const auto path = folder.absolutePath();
auto result = path.endsWith('/') ? path : (path + '/');
if (!folder.exists()) {
return result;
}
const auto mode = QDir::AllEntries | QDir::NoDotAndDotDot;
const auto list = folder.entryInfoList(mode);
if (list.isEmpty()) {
return result;
}
const auto date = QDate::currentDate();
const auto base = QString("DataExport_%1_%2_%3"
).arg(date.day(), 2, 10, QChar('0')
).arg(date.month(), 2, 10, QChar('0')
).arg(date.year());
const auto add = [&](int i) {
return base + (i ? " (" + QString::number(i) + ')' : QString());
};
auto index = 0;
while (QDir(result + add(index)).exists()) {
++index;
}
result += add(index) + '/';
return result;
}
std::unique_ptr<AbstractWriter> CreateWriter(Format format) {
switch (format) {
case Format::Html: return std::make_unique<HtmlWriter>();
case Format::Text: return std::make_unique<TextWriter>();
case Format::Json: return std::make_unique<JsonWriter>();
}
Unexpected("Format in Export::Output::CreateWriter.");
}
Stats AbstractWriter::produceTestExample(const QString &path) {
auto result = Stats();
const auto folder = QDir(path).absolutePath();
auto settings = Settings();
settings.format = format();
settings.path = (folder.endsWith('/') ? folder : (folder + '/'))
+ "ExportExample/";
settings.internalLinksDomain = "https://t.me/";
settings.types = Settings::Type::AllMask;
settings.fullChats = Settings::Type::AllMask
& ~(Settings::Type::PublicChannels | Settings::Type::PublicGroups);
settings.media.types = MediaSettings::Type::AllMask;
settings.media.sizeLimit = 1024 * 1024;
const auto check = [](Result result) {
Assert(result.isSuccess());
};
check(start(settings, &result));
const auto counter = [&] {
static auto GlobalCounter = 0;
return ++GlobalCounter;
};
const auto date = [&] {
return time(nullptr) - 86400 + counter();
};
const auto prevdate = [&] {
return date() - 86400;
};
auto personal = Data::PersonalInfo();
personal.bio = "Nice text about me.";
personal.user.info.firstName = "John";
personal.user.info.lastName = "Preston";
personal.user.info.phoneNumber = "447400000000";
personal.user.info.date = date();
personal.user.username = "preston";
personal.user.info.userId = counter();
personal.user.isBot = false;
personal.user.isSelf = true;
check(writePersonal(personal));
const auto generatePhoto = [&] {
static auto index = 0;
auto result = Data::Photo();
result.date = date();
result.id = counter();
result.image.width = 512;
result.image.height = 512;
result.image.file.relativePath = "Files/Photo_"
+ QString::number(++index)
+ ".jpg";
return result;
};
auto userpics = Data::UserpicsInfo();
userpics.count = 3;
auto userpicsSlice1 = Data::UserpicsSlice();
userpicsSlice1.list.push_back(generatePhoto());
userpicsSlice1.list.push_back(generatePhoto());
auto userpicsSlice2 = Data::UserpicsSlice();
userpicsSlice2.list.push_back(generatePhoto());
check(writeUserpicsStart(userpics));
check(writeUserpicsSlice(userpicsSlice1));
check(writeUserpicsSlice(userpicsSlice2));
check(writeUserpicsEnd());
auto contacts = Data::ContactsList();
auto topUser = Data::TopPeer();
auto user = personal.user;
auto peerUser = Data::Peer{ user };
topUser.peer = peerUser;
topUser.rating = 0.5;
auto topChat = Data::TopPeer();
auto chat = Data::Chat();
chat.id = counter();
chat.title = "Group chat";
auto peerChat = Data::Peer{ chat };
topChat.peer = peerChat;
topChat.rating = 0.25;
auto topBot = Data::TopPeer();
auto bot = Data::User();
bot.info.date = date();
bot.isBot = true;
bot.info.firstName = "Bot";
bot.info.lastName = "Father";
bot.info.userId = counter();
bot.username = "botfather";
auto peerBot = Data::Peer{ bot };
topBot.peer = peerBot;
topBot.rating = 0.125;
auto peers = std::map<Data::PeerId, Data::Peer>();
peers.emplace(peerUser.id(), peerUser);
peers.emplace(peerBot.id(), peerBot);
peers.emplace(peerChat.id(), peerChat);
contacts.correspondents.push_back(topUser);
contacts.correspondents.push_back(topChat);
contacts.inlineBots.push_back(topBot);
contacts.inlineBots.push_back(topBot);
contacts.phoneCalls.push_back(topUser);
contacts.list.push_back(user.info);
contacts.list.push_back(bot.info);
check(writeContactsList(contacts));
auto sessions = Data::SessionsList();
auto session = Data::Session();
session.applicationName = "Telegram Desktop";
session.applicationVersion = "1.3.8";
session.country = "GB";
session.created = date();
session.deviceModel = "PC";
session.ip = "127.0.0.1";
session.lastActive = date();
session.platform = "Windows";
session.region = "London";
session.systemVersion = "10";
sessions.list.push_back(session);
sessions.list.push_back(session);
auto webSession = Data::WebSession();
webSession.botUsername = "botfather";
webSession.browser = "Google Chrome";
webSession.created = date();
webSession.domain = "telegram.org";
webSession.ip = "127.0.0.1";
webSession.lastActive = date();
webSession.platform = "Windows";
webSession.region = "London, GB";
sessions.webList.push_back(webSession);
sessions.webList.push_back(webSession);
check(writeSessionsList(sessions));
auto sampleMessage = [&] {
auto message = Data::Message();
message.id = counter();
message.date = prevdate();
message.edited = date();
message.forwardedFromId = user.info.userId;
message.fromId = user.info.userId;
message.replyToMsgId = counter();
message.viaBotId = bot.info.userId;
message.text.push_back(Data::TextPart{
Data::TextPart::Type::Text,
("Text message " + QString::number(counter())).toUtf8()
});
return message;
};
auto sliceBot1 = Data::MessagesSlice();
sliceBot1.peers = peers;
sliceBot1.list.push_back(sampleMessage());
sliceBot1.list.push_back([&] {
auto message = sampleMessage();
message.media.content = generatePhoto();
message.media.ttl = counter();
return message;
}());
sliceBot1.list.push_back([&] {
auto message = sampleMessage();
auto document = Data::Document();
document.date = prevdate();
document.duration = counter();
auto photo = generatePhoto();
document.file = photo.image.file;
document.width = photo.image.width;
document.height = photo.image.height;
document.id = counter();
message.media.content = document;
return message;
}());
sliceBot1.list.push_back([&] {
auto message = sampleMessage();
message.media.content = user.info;
return message;
}());
auto sliceBot2 = Data::MessagesSlice();
sliceBot2.peers = peers;
sliceBot2.list.push_back([&] {
auto message = sampleMessage();
auto point = Data::GeoPoint();
point.latitude = 1.5;
point.longitude = 2.8;
point.valid = true;
message.media.content = point;
message.media.ttl = counter();
return message;
}());
sliceBot2.list.push_back([&] {
auto message = sampleMessage();
message.replyToMsgId = sliceBot1.list.back().id;
auto venue = Data::Venue();
venue.point.latitude = 1.5;
venue.point.longitude = 2.8;
venue.point.valid = true;
venue.address = "Test address";
venue.title = "Test venue";
message.media.content = venue;
return message;
}());
sliceBot2.list.push_back([&] {
auto message = sampleMessage();
auto game = Data::Game();
game.botId = bot.info.userId;
game.title = "Test game";
game.description = "Test game description";
game.id = counter();
game.shortName = "testgame";
message.media.content = game;
return message;
}());
sliceBot2.list.push_back([&] {
auto message = sampleMessage();
auto invoice = Data::Invoice();
invoice.amount = counter();
invoice.currency = "GBP";
invoice.title = "Huge invoice.";
invoice.description = "So money.";
invoice.receiptMsgId = sliceBot2.list.front().id;
message.media.content = invoice;
return message;
}());
auto serviceMessage = [&] {
auto message = Data::Message();
message.id = counter();
message.date = prevdate();
message.fromId = user.info.userId;
return message;
};
auto sliceChat1 = Data::MessagesSlice();
sliceChat1.peers = peers;
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatCreate();
action.title = "Test chat";
action.userIds.push_back(user.info.userId);
action.userIds.push_back(bot.info.userId);
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatEditTitle();
action.title = "New title";
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatEditPhoto();
action.photo = generatePhoto();
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatDeletePhoto();
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatAddUser();
action.userIds.push_back(user.info.userId);
action.userIds.push_back(bot.info.userId);
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatDeleteUser();
action.userId = bot.info.userId;
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatJoinedByLink();
action.inviterId = bot.info.userId;
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChannelCreate();
action.title = "Channel name";
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatMigrateTo();
action.channelId = chat.id;
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChannelMigrateFrom();
action.chatId = chat.id;
action.title = "Supergroup now";
message.action.content = action;
return message;
}());
auto sliceChat2 = Data::MessagesSlice();
sliceChat2.peers = peers;
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionPinMessage();
message.replyToMsgId = sliceChat1.list.back().id;
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionHistoryClear();
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionGameScore();
action.score = counter();
action.gameId = counter();
message.replyToMsgId = sliceChat2.list.back().id;
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionPaymentSent();
action.amount = counter();
action.currency = "GBP";
message.replyToMsgId = sliceChat2.list.front().id;
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionPhoneCall();
action.duration = counter();
action.discardReason = Data::ActionPhoneCall::DiscardReason::Busy;
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionScreenshotTaken();
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionCustomAction();
action.message = "Custom chat action.";
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionBotAllowed();
action.domain = "telegram.org";
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionSecureValuesSent();
using Type = Data::ActionSecureValuesSent::Type;
action.types.push_back(Type::BankStatement);
action.types.push_back(Type::Phone);
message.action.content = action;
return message;
}());
auto dialogs = Data::DialogsInfo();
auto dialogBot = Data::DialogInfo();
dialogBot.messagesCountPerSplit.push_back(sliceBot1.list.size());
dialogBot.messagesCountPerSplit.push_back(sliceBot2.list.size());
dialogBot.type = Data::DialogInfo::Type::Bot;
dialogBot.name = peerBot.name();
dialogBot.onlyMyMessages = false;
dialogBot.peerId = peerBot.id();
dialogBot.relativePath = "Chats/C_" + QString::number(counter()) + '/';
dialogBot.splits.push_back(0);
dialogBot.splits.push_back(1);
dialogBot.topMessageDate = sliceBot2.list.back().date;
dialogBot.topMessageId = sliceBot2.list.back().id;
auto dialogChat = Data::DialogInfo();
dialogChat.messagesCountPerSplit.push_back(sliceChat1.list.size());
dialogChat.messagesCountPerSplit.push_back(sliceChat2.list.size());
dialogChat.type = Data::DialogInfo::Type::PrivateGroup;
dialogChat.name = peerChat.name();
dialogChat.onlyMyMessages = true;
dialogChat.peerId = peerChat.id();
dialogChat.relativePath = "Chats/C_" + QString::number(counter()) + '/';
dialogChat.splits.push_back(0);
dialogChat.splits.push_back(1);
dialogChat.topMessageDate = sliceChat2.list.back().date;
dialogChat.topMessageId = sliceChat2.list.back().id;
dialogs.list.push_back(dialogBot);
dialogs.list.push_back(dialogChat);
check(writeDialogsStart(dialogs));
check(writeDialogStart(dialogBot));
check(writeDialogSlice(sliceBot1));
check(writeDialogSlice(sliceBot2));
check(writeDialogEnd());
check(writeDialogStart(dialogChat));
check(writeDialogSlice(sliceChat1));
check(writeDialogSlice(sliceChat2));
check(writeDialogEnd());
check(writeDialogsEnd());
check(writeLeftChannelsStart(Data::DialogsInfo()));
check(writeLeftChannelsEnd());
check(finish());
return result;
}
} // namespace Output
} // namespace Export

View file

@ -25,18 +25,22 @@ struct Settings;
namespace Output {
QString NormalizePath(const QString &source);
struct Result;
class Stats;
enum class Format {
Text,
Json,
Yaml,
Html,
Json,
Text,
Yaml,
};
class AbstractWriter {
public:
[[nodiscard]] virtual Format format() = 0;
[[nodiscard]] virtual Result start(
const Settings &settings,
Stats *stats) = 0;
@ -80,6 +84,8 @@ public:
virtual ~AbstractWriter() = default;
Stats produceTestExample(const QString &path);
};
std::unique_ptr<AbstractWriter> CreateWriter(Format format);

View file

@ -120,5 +120,20 @@ QString File::PrepareRelativePath(
}
}
Result File::Copy(
const QString &source,
const QString &path,
Stats *stats) {
QFile f(source);
if (!f.exists() || !f.open(QIODevice::ReadOnly)) {
return Result(Result::Type::FatalError, source);
}
const auto bytes = f.readAll();
if (bytes.size() != f.size()) {
return Result(Result::Type::FatalError, source);
}
return File(path, stats).writeBlock(bytes);
}
} // namespace Output
} // namespace File

View file

@ -32,6 +32,11 @@ public:
const QString &folder,
const QString &suggested);
[[nodiscard]] static Result Copy(
const QString &source,
const QString &path,
Stats *stats);
private:
[[nodiscard]] Result reopen();
[[nodiscard]] Result writeBlockAttempt(const QByteArray &block);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,101 @@
/*
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 {
class HtmlWriter : public AbstractWriter {
public:
HtmlWriter();
Format format() override {
return Format::Html;
}
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;
~HtmlWriter();
private:
class Wrap;
Result copyFile(
const QString &source,
const QString &relativePath) const;
QString mainFileRelativePath() const;
QString pathWithRelativePath(const QString &path) const;
std::unique_ptr<Wrap> fileWithRelativePath(const QString &path) const;
Result writeSavedContacts(const Data::ContactsList &data);
Result writeFrequentContacts(const Data::ContactsList &data);
Result writeSessions(const Data::SessionsList &data);
Result writeWebSessions(const Data::SessionsList &data);
Result writeChatsStart(
const Data::DialogsInfo &data,
const QByteArray &listName,
const QString &fileName);
Result writeChatStart(const Data::DialogInfo &data);
Result writeChatSlice(const Data::MessagesSlice &data);
Result writeChatEnd();
Result writeChatsEnd();
Settings _settings;
Stats *_stats = nullptr;
std::unique_ptr<Wrap> _summary;
int _userpicsCount = 0;
std::unique_ptr<Wrap> _userpics;
int _dialogsCount = 0;
int _dialogIndex = 0;
Data::DialogInfo _dialog;
int _messagesCount = 0;
std::unique_ptr<Wrap> _chats;
std::unique_ptr<Wrap> _chat;
};
} // namespace Output
} // namespace Export

View file

@ -733,12 +733,14 @@ Result JsonWriter::writeFrequentContacts(const Data::ContactsList &data) {
const auto type = [&] {
if (const auto chat = top.peer.chat()) {
return chat->username.isEmpty()
? (chat->broadcast
? (chat->isBroadcast
? "private_channel"
: "private_group")
: (chat->broadcast
: (chat->isSupergroup
? "private_supergroup"
: "private_group"))
: (chat->isBroadcast
? "public_channel"
: "public_group");
: "public_supergroup");
}
return "user";
}();
@ -879,10 +881,12 @@ Result JsonWriter::writeChatStart(const Data::DialogInfo &data) {
const auto TypeString = [](Type type) {
switch (type) {
case Type::Unknown: return "";
case Type::Self: return "saved_messages";
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::PrivateSupergroup: return "private_supergroup";
case Type::PublicSupergroup: return "public_supergroup";
case Type::PrivateChannel: return "private_channel";
case Type::PublicChannel: return "public_channel";
}
@ -891,8 +895,10 @@ Result JsonWriter::writeChatStart(const Data::DialogInfo &data) {
auto block = prepareArrayItemStart();
block.append(pushNesting(Context::kObject));
if (data.type != Type::Self) {
block.append(prepareObjectItemStart("name")
+ StringAllowNull(data.name));
}
block.append(prepareObjectItemStart("type")
+ StringAllowNull(TypeString(data.type)));
block.append(prepareObjectItemStart("messages"));

View file

@ -29,6 +29,10 @@ struct JsonContext {
class JsonWriter : public AbstractWriter {
public:
Format format() override {
return Format::Json;
}
Result start(const Settings &settings, Stats *stats) override;
Result writePersonal(const Data::PersonalInfo &data) override;

View file

@ -10,6 +10,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Export {
namespace Output {
Stats::Stats(const Stats &other)
: _files(other._files.load())
, _bytes(other._bytes.load()) {
}
void Stats::incrementFiles() {
++_files;
}

View file

@ -14,6 +14,9 @@ namespace Output {
class Stats {
public:
Stats() = default;
Stats(const Stats &other);
void incrementFiles();
void incrementBytes(int count);

View file

@ -580,7 +580,7 @@ Result TextWriter::writeFrequentContacts(const Data::ContactsList &data) {
Data::Utf8String category) {
for (const auto &top : peers) {
const auto user = [&]() -> Data::Utf8String {
if (!top.peer.user()) {
if (!top.peer.user() || top.peer.user()->isSelf) {
return Data::Utf8String();
} else if (top.peer.name().isEmpty()) {
return "(deleted user)";
@ -590,12 +590,14 @@ Result TextWriter::writeFrequentContacts(const Data::ContactsList &data) {
const auto chatType = [&] {
if (const auto chat = top.peer.chat()) {
return chat->username.isEmpty()
? (chat->broadcast
? (chat->isBroadcast
? "Private channel"
: "Private group")
: (chat->broadcast
: (chat->isSupergroup
? "Private supergroup"
: "Private group"))
: (chat->isBroadcast
? "Public channel"
: "Public group");
: "Public supergroup");
}
return "";
}();
@ -607,11 +609,18 @@ Result TextWriter::writeFrequentContacts(const Data::ContactsList &data) {
}
return top.peer.name();
}();
const auto saved = [&]() -> Data::Utf8String {
if (!top.peer.user() || !top.peer.user()->isSelf) {
return Data::Utf8String();
}
return "Saved messages";
}();
list.push_back(SerializeKeyValue({
{ "Category", category },
{ "User", top.peer.user() ? user : QByteArray() },
{ "Chat", saved },
{ chatType, chat },
{ "Rating", QString::number(top.rating).toUtf8() }
{ "Rating", Data::NumberToString(top.rating) }
}));
}
};
@ -834,18 +843,24 @@ Result TextWriter::writeChatEnd() {
const auto TypeString = [](Type type) {
switch (type) {
case Type::Unknown: return "(unknown)";
case Type::Self:
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::PrivateSupergroup: return "Private supergroup";
case Type::PublicSupergroup: return "Public supergroup";
case Type::PrivateChannel: return "Private channel";
case Type::PublicChannel: return "Public channel";
}
Unexpected("Dialog type in TypeString.");
};
const auto NameString = [](
const Data::Utf8String &name,
const Data::DialogInfo &dialog,
Type type) -> QByteArray {
if (dialog.type == Type::Self) {
return "Saved messages";
}
const auto name = dialog.name;
if (!name.isEmpty()) {
return name;
}
@ -854,14 +869,15 @@ Result TextWriter::writeChatEnd() {
case Type::Personal: return "(deleted user)";
case Type::Bot: return "(deleted bot)";
case Type::PrivateGroup:
case Type::PublicGroup: return "(deleted group)";
case Type::PrivateSupergroup:
case Type::PublicSupergroup: return "(deleted group)";
case Type::PrivateChannel:
case Type::PublicChannel: return "(deleted channel)";
}
Unexpected("Dialog type in TypeString.");
};
return _chats->writeBlock(SerializeKeyValue({
{ "Name", NameString(_dialog.name, _dialog.type) },
{ "Name", NameString(_dialog, _dialog.type) },
{ "Type", TypeString(_dialog.type) },
{
(_dialog.onlyMyMessages

View file

@ -17,6 +17,10 @@ namespace Output {
class TextWriter : public AbstractWriter {
public:
Format format() override {
return Format::Text;
}
Result start(const Settings &settings, Stats *stats) override;
Result writePersonal(const Data::PersonalInfo &data) override;

View file

@ -192,7 +192,7 @@ void SettingsWidget::setupPathAndFormat(
};
addHeader(container, lng_export_header_format);
addLocationLabel(container);
addFormatOption(lng_export_option_text, Format::Text);
addFormatOption(lng_export_option_html, Format::Html);
addFormatOption(lng_export_option_json, Format::Json);
}

View file

@ -109,6 +109,7 @@
'<@(style_files)',
'<!@(<(list_sources_command) <(qt_moc_list_sources_arg))',
'telegram_sources.txt',
'<(res_loc)/css/export_style.css',
],
'sources!': [
'<!@(<(list_sources_command) <(qt_moc_list_sources_arg) --exclude_for <(build_os))',

View file

@ -63,8 +63,11 @@
'<(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_html.cpp',
'<(src_loc)/export/output/export_output_html.h',
'<(src_loc)/export/output/export_output_json.cpp',
'<(src_loc)/export/output/export_output_json.h',
'<(src_loc)/export/output/export_output_result.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',