mirror of
https://github.com/vale981/tdesktop
synced 2025-03-06 02:01:40 -05:00
Add basic HTML export.
This commit is contained in:
parent
e708065446
commit
9d66f9cc03
22 changed files with 1904 additions and 66 deletions
3
Telegram/Resources/css/export_style.css
Normal file
3
Telegram/Resources/css/export_style.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.page_wrap {
|
||||
background-color: #fff;
|
||||
}
|
|
@ -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...";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
1217
Telegram/SourceFiles/export/output/export_output_html.cpp
Normal file
1217
Telegram/SourceFiles/export/output/export_output_html.cpp
Normal file
File diff suppressed because it is too large
Load diff
101
Telegram/SourceFiles/export/output/export_output_html.h
Normal file
101
Telegram/SourceFiles/export/output/export_output_html.h
Normal 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
|
|
@ -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));
|
||||
block.append(prepareObjectItemStart("name")
|
||||
+ StringAllowNull(data.name));
|
||||
if (data.type != Type::Self) {
|
||||
block.append(prepareObjectItemStart("name")
|
||||
+ StringAllowNull(data.name));
|
||||
}
|
||||
block.append(prepareObjectItemStart("type")
|
||||
+ StringAllowNull(TypeString(data.type)));
|
||||
block.append(prepareObjectItemStart("messages"));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ namespace Output {
|
|||
|
||||
class Stats {
|
||||
public:
|
||||
Stats() = default;
|
||||
Stats(const Stats &other);
|
||||
|
||||
void incrementFiles();
|
||||
void incrementBytes(int count);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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))',
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Reference in a new issue