Fix export of discussion messages.
This commit is contained in:
parent
81457693f1
commit
874e5e0a61
9 changed files with 45 additions and 1128 deletions
|
@ -1187,6 +1187,8 @@ Message ParseMessage(
|
||||||
});
|
});
|
||||||
result.forwarded = result.forwardedFromId
|
result.forwarded = result.forwardedFromId
|
||||||
|| !result.forwardedFromName.isEmpty();
|
|| !result.forwardedFromName.isEmpty();
|
||||||
|
result.showForwardedAsOriginal = result.forwarded
|
||||||
|
&& result.savedFromChatId;
|
||||||
}
|
}
|
||||||
if (const auto postAuthor = data.vpost_author()) {
|
if (const auto postAuthor = data.vpost_author()) {
|
||||||
result.signature = ParseString(*postAuthor);
|
result.signature = ParseString(*postAuthor);
|
||||||
|
|
|
@ -520,6 +520,7 @@ struct Message {
|
||||||
Utf8String forwardedFromName;
|
Utf8String forwardedFromName;
|
||||||
TimeId forwardedDate = 0;
|
TimeId forwardedDate = 0;
|
||||||
bool forwarded = false;
|
bool forwarded = false;
|
||||||
|
bool showForwardedAsOriginal = false;
|
||||||
PeerId savedFromChatId = 0;
|
PeerId savedFromChatId = 0;
|
||||||
Utf8String signature;
|
Utf8String signature;
|
||||||
int32 viaBotId = 0;
|
int32 viaBotId = 0;
|
||||||
|
|
|
@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#include "export/output/export_output_abstract.h"
|
#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_html.h"
|
||||||
#include "export/output/export_output_json.h"
|
#include "export/output/export_output_json.h"
|
||||||
#include "export/output/export_output_stats.h"
|
#include "export/output/export_output_stats.h"
|
||||||
|
@ -50,7 +49,6 @@ QString NormalizePath(const Settings &settings) {
|
||||||
std::unique_ptr<AbstractWriter> CreateWriter(Format format) {
|
std::unique_ptr<AbstractWriter> CreateWriter(Format format) {
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case Format::Html: return std::make_unique<HtmlWriter>();
|
case Format::Html: return std::make_unique<HtmlWriter>();
|
||||||
case Format::Text: return std::make_unique<TextWriter>();
|
|
||||||
case Format::Json: return std::make_unique<JsonWriter>();
|
case Format::Json: return std::make_unique<JsonWriter>();
|
||||||
}
|
}
|
||||||
Unexpected("Format in Export::Output::CreateWriter.");
|
Unexpected("Format in Export::Output::CreateWriter.");
|
||||||
|
|
|
@ -35,8 +35,6 @@ class Stats;
|
||||||
enum class Format {
|
enum class Format {
|
||||||
Html,
|
Html,
|
||||||
Json,
|
Json,
|
||||||
Text,
|
|
||||||
Yaml,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class AbstractWriter {
|
class AbstractWriter {
|
||||||
|
|
|
@ -516,11 +516,13 @@ struct HtmlWriter::MessageInfo {
|
||||||
};
|
};
|
||||||
int32 id = 0;
|
int32 id = 0;
|
||||||
Type type = Type::Service;
|
Type type = Type::Service;
|
||||||
int32 fromId = 0;
|
Data::PeerId fromId = 0;
|
||||||
|
int32 viaBotId = 0;
|
||||||
TimeId date = 0;
|
TimeId date = 0;
|
||||||
Data::PeerId forwardedFromId = 0;
|
Data::PeerId forwardedFromId = 0;
|
||||||
QString forwardedFromName;
|
QString forwardedFromName;
|
||||||
bool forwarded = false;
|
bool forwarded = false;
|
||||||
|
bool showForwardedAsOriginal = false;
|
||||||
TimeId forwardedDate = 0;
|
TimeId forwardedDate = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -968,11 +970,13 @@ auto HtmlWriter::Wrap::pushMessage(
|
||||||
auto info = MessageInfo();
|
auto info = MessageInfo();
|
||||||
info.id = message.id;
|
info.id = message.id;
|
||||||
info.fromId = message.fromId;
|
info.fromId = message.fromId;
|
||||||
|
info.viaBotId = message.viaBotId;
|
||||||
info.date = message.date;
|
info.date = message.date;
|
||||||
info.forwardedFromId = message.forwardedFromId;
|
info.forwardedFromId = message.forwardedFromId;
|
||||||
info.forwardedFromName = message.forwardedFromName;
|
info.forwardedFromName = message.forwardedFromName;
|
||||||
info.forwardedDate = message.forwardedDate;
|
info.forwardedDate = message.forwardedDate;
|
||||||
info.forwarded = message.forwarded;
|
info.forwarded = message.forwarded;
|
||||||
|
info.showForwardedAsOriginal = message.showForwardedAsOriginal;
|
||||||
if (v::is<UnsupportedMedia>(message.media.content)) {
|
if (v::is<UnsupportedMedia>(message.media.content)) {
|
||||||
return { info, pushServiceMessage(
|
return { info, pushServiceMessage(
|
||||||
message.id,
|
message.id,
|
||||||
|
@ -989,7 +993,7 @@ auto HtmlWriter::Wrap::pushMessage(
|
||||||
using DialogType = Data::DialogInfo::Type;
|
using DialogType = Data::DialogInfo::Type;
|
||||||
const auto isChannel = (dialog.type == DialogType::PrivateChannel)
|
const auto isChannel = (dialog.type == DialogType::PrivateChannel)
|
||||||
|| (dialog.type == DialogType::PublicChannel);
|
|| (dialog.type == DialogType::PublicChannel);
|
||||||
const auto serviceFrom = peers.wrapUserName(message.fromId);
|
const auto serviceFrom = peers.wrapPeerName(message.fromId);
|
||||||
const auto serviceText = v::match(message.action.content, [&](
|
const auto serviceText = v::match(message.action.content, [&](
|
||||||
const ActionChatCreate &data) {
|
const ActionChatCreate &data) {
|
||||||
return serviceFrom
|
return serviceFrom
|
||||||
|
@ -1107,13 +1111,31 @@ auto HtmlWriter::Wrap::pushMessage(
|
||||||
info.type = MessageInfo::Type::Default;
|
info.type = MessageInfo::Type::Default;
|
||||||
|
|
||||||
const auto wrap = messageNeedsWrap(message, previous);
|
const auto wrap = messageNeedsWrap(message, previous);
|
||||||
const auto fromPeerId = message.fromId
|
const auto fromPeerId = message.fromId;
|
||||||
? UserPeerId(message.fromId)
|
const auto showForwardedInfo = message.forwarded
|
||||||
: ChatPeerId(message.chatId);
|
&& !message.showForwardedAsOriginal;
|
||||||
|
auto forwardedUserpic = UserpicData();
|
||||||
|
if (message.forwarded) {
|
||||||
|
forwardedUserpic.colorIndex = message.forwardedFromId
|
||||||
|
? PeerColorIndex(BarePeerId(message.forwardedFromId))
|
||||||
|
: PeerColorIndex(message.id);
|
||||||
|
forwardedUserpic.pixelSize = kHistoryUserpicSize;
|
||||||
|
if (message.forwardedFromId) {
|
||||||
|
FillUserpicNames(
|
||||||
|
forwardedUserpic,
|
||||||
|
peers.peer(message.forwardedFromId));
|
||||||
|
} else {
|
||||||
|
FillUserpicNames(forwardedUserpic, message.forwardedFromName);
|
||||||
|
}
|
||||||
|
}
|
||||||
auto userpic = UserpicData();
|
auto userpic = UserpicData();
|
||||||
userpic.colorIndex = PeerColorIndex(BarePeerId(fromPeerId));
|
if (message.showForwardedAsOriginal) {
|
||||||
userpic.pixelSize = kHistoryUserpicSize;
|
userpic = forwardedUserpic;
|
||||||
FillUserpicNames(userpic, peers.peer(fromPeerId));
|
} else {
|
||||||
|
userpic.colorIndex = PeerColorIndex(BarePeerId(fromPeerId));
|
||||||
|
userpic.pixelSize = kHistoryUserpicSize;
|
||||||
|
FillUserpicNames(userpic, peers.peer(fromPeerId));
|
||||||
|
}
|
||||||
|
|
||||||
const auto via = [&] {
|
const auto via = [&] {
|
||||||
if (message.viaBotId) {
|
if (message.viaBotId) {
|
||||||
|
@ -1148,25 +1170,13 @@ auto HtmlWriter::Wrap::pushMessage(
|
||||||
block.append(pushDiv("from_name"));
|
block.append(pushDiv("from_name"));
|
||||||
block.append(SerializeString(
|
block.append(SerializeString(
|
||||||
ComposeName(userpic, "Deleted Account")));
|
ComposeName(userpic, "Deleted Account")));
|
||||||
if (!via.isEmpty() && !message.forwarded) {
|
if (!via.isEmpty()
|
||||||
|
&& (!message.forwarded || message.showForwardedAsOriginal)) {
|
||||||
block.append(" via @" + via);
|
block.append(" via @" + via);
|
||||||
}
|
}
|
||||||
block.append(popTag());
|
block.append(popTag());
|
||||||
}
|
}
|
||||||
if (message.forwarded) {
|
if (showForwardedInfo) {
|
||||||
auto forwardedUserpic = UserpicData();
|
|
||||||
forwardedUserpic.colorIndex = message.forwardedFromId
|
|
||||||
? PeerColorIndex(BarePeerId(message.forwardedFromId))
|
|
||||||
: PeerColorIndex(message.id);
|
|
||||||
forwardedUserpic.pixelSize = kHistoryUserpicSize;
|
|
||||||
if (message.forwardedFromId) {
|
|
||||||
FillUserpicNames(
|
|
||||||
forwardedUserpic,
|
|
||||||
peers.peer(message.forwardedFromId));
|
|
||||||
} else {
|
|
||||||
FillUserpicNames(forwardedUserpic, message.forwardedFromName);
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto forwardedWrap = forwardedNeedsWrap(message, previous);
|
const auto forwardedWrap = forwardedNeedsWrap(message, previous);
|
||||||
if (forwardedWrap) {
|
if (forwardedWrap) {
|
||||||
block.append(pushDiv("pull_left forwarded userpic_wrap"));
|
block.append(pushDiv("pull_left forwarded userpic_wrap"));
|
||||||
|
@ -1214,7 +1224,7 @@ auto HtmlWriter::Wrap::pushMessage(
|
||||||
block.append(SerializeString(message.signature));
|
block.append(SerializeString(message.signature));
|
||||||
block.append(popTag());
|
block.append(popTag());
|
||||||
}
|
}
|
||||||
if (message.forwarded) {
|
if (showForwardedInfo) {
|
||||||
block.append(popTag());
|
block.append(popTag());
|
||||||
}
|
}
|
||||||
block.append(popTag());
|
block.append(popTag());
|
||||||
|
@ -1232,10 +1242,15 @@ bool HtmlWriter::Wrap::messageNeedsWrap(
|
||||||
return true;
|
return true;
|
||||||
} else if (!message.fromId || previous->fromId != message.fromId) {
|
} else if (!message.fromId || previous->fromId != message.fromId) {
|
||||||
return true;
|
return true;
|
||||||
|
} else if (message.viaBotId != previous->viaBotId) {
|
||||||
|
return true;
|
||||||
} else if (QDateTime::fromTime_t(previous->date).date()
|
} else if (QDateTime::fromTime_t(previous->date).date()
|
||||||
!= QDateTime::fromTime_t(message.date).date()) {
|
!= QDateTime::fromTime_t(message.date).date()) {
|
||||||
return true;
|
return true;
|
||||||
} else if (message.forwarded != previous->forwarded) {
|
} else if (message.forwarded != previous->forwarded
|
||||||
|
|| message.showForwardedAsOriginal != previous->showForwardedAsOriginal
|
||||||
|
|| message.forwardedFromId != previous->forwardedFromId
|
||||||
|
|| message.forwardedFromName != previous->forwardedFromName) {
|
||||||
return true;
|
return true;
|
||||||
} else if (std::abs(message.date - previous->date)
|
} else if (std::abs(message.date - previous->date)
|
||||||
> ((message.forwardedFromId || !message.forwardedFromName.isEmpty())
|
> ((message.forwardedFromId || !message.forwardedFromName.isEmpty())
|
||||||
|
|
|
@ -295,8 +295,8 @@ QByteArray SerializeMessage(
|
||||||
};
|
};
|
||||||
const auto pushFrom = [&](const QByteArray &label = "from") {
|
const auto pushFrom = [&](const QByteArray &label = "from") {
|
||||||
if (message.fromId) {
|
if (message.fromId) {
|
||||||
pushBare(label, wrapUserName(message.fromId));
|
pushBare(label, wrapPeerName(message.fromId));
|
||||||
pushBare(label+"_id", Data::NumberToString(message.fromId));
|
push(label+"_id", message.fromId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const auto pushReplyToMsgId = [&](
|
const auto pushReplyToMsgId = [&](
|
||||||
|
|
|
@ -1,997 +0,0 @@
|
||||||
/*
|
|
||||||
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_text.h"
|
|
||||||
|
|
||||||
#include "export/output/export_output_result.h"
|
|
||||||
#include "export/data/export_data_types.h"
|
|
||||||
#include "core/utils.h"
|
|
||||||
|
|
||||||
#include <QtCore/QFile>
|
|
||||||
|
|
||||||
namespace Export {
|
|
||||||
namespace Output {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
|
||||||
const auto kLineBreak = QByteArrayLiteral("\r\n");
|
|
||||||
#else // Q_OS_WIN
|
|
||||||
const auto kLineBreak = QByteArrayLiteral("\n");
|
|
||||||
#endif // Q_OS_WIN
|
|
||||||
|
|
||||||
void SerializeMultiline(
|
|
||||||
QByteArray &appendTo,
|
|
||||||
const QByteArray &value,
|
|
||||||
int newline) {
|
|
||||||
const auto data = value.data();
|
|
||||||
auto offset = 0;
|
|
||||||
do {
|
|
||||||
appendTo.append("> ");
|
|
||||||
const auto win = (newline > 0 && *(data + newline - 1) == '\r');
|
|
||||||
if (win) --newline;
|
|
||||||
appendTo.append(data + offset, newline - offset).append(kLineBreak);
|
|
||||||
if (win) ++newline;
|
|
||||||
offset = newline + 1;
|
|
||||||
newline = value.indexOf('\n', offset);
|
|
||||||
} while (newline > 0);
|
|
||||||
if (const auto size = value.size(); size > offset) {
|
|
||||||
appendTo.append("> ");
|
|
||||||
appendTo.append(data + offset, size - offset).append(kLineBreak);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray JoinList(
|
|
||||||
const QByteArray &separator,
|
|
||||||
const std::vector<QByteArray> &list) {
|
|
||||||
if (list.empty()) {
|
|
||||||
return QByteArray();
|
|
||||||
} else if (list.size() == 1) {
|
|
||||||
return list[0];
|
|
||||||
}
|
|
||||||
auto size = (list.size() - 1) * separator.size();
|
|
||||||
for (const auto &value : list) {
|
|
||||||
size += value.size();
|
|
||||||
}
|
|
||||||
auto result = QByteArray();
|
|
||||||
result.reserve(size);
|
|
||||||
auto counter = 0;
|
|
||||||
while (true) {
|
|
||||||
result.append(list[counter]);
|
|
||||||
if (++counter == list.size()) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
result.append(separator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray SerializeKeyValue(
|
|
||||||
std::vector<std::pair<QByteArray, QByteArray>> &&values) {
|
|
||||||
auto result = QByteArray();
|
|
||||||
for (const auto &[key, value] : values) {
|
|
||||||
if (value.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
result.append(key);
|
|
||||||
if (const auto newline = value.indexOf('\n'); newline >= 0) {
|
|
||||||
result.append(':').append(kLineBreak);
|
|
||||||
SerializeMultiline(result, value, newline);
|
|
||||||
} else {
|
|
||||||
result.append(": ").append(value).append(kLineBreak);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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(
|
|
||||||
const Data::Message &message,
|
|
||||||
const std::map<Data::PeerId, Data::Peer> &peers,
|
|
||||||
const QString &internalLinksDomain) {
|
|
||||||
using namespace Data;
|
|
||||||
|
|
||||||
if (v::is<UnsupportedMedia>(message.media.content)) {
|
|
||||||
return "Error! This message is not supported "
|
|
||||||
"by this version of Telegram Desktop. "
|
|
||||||
"Please update the application.";
|
|
||||||
}
|
|
||||||
|
|
||||||
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) },
|
|
||||||
{ "Date", FormatDateTime(message.date) },
|
|
||||||
{ "Edited", FormatDateTime(message.edited) },
|
|
||||||
};
|
|
||||||
const auto push = [&](const QByteArray &key, const QByteArray &value) {
|
|
||||||
if (!value.isEmpty()) {
|
|
||||||
values.emplace_back(key, value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const auto wrapPeerName = [&](PeerId peerId) {
|
|
||||||
const auto result = peer(peerId).name();
|
|
||||||
return result.isEmpty() ? QByteArray("(deleted peer)") : result;
|
|
||||||
};
|
|
||||||
const auto wrapUserName = [&](int32 userId) {
|
|
||||||
const auto result = user(userId).name();
|
|
||||||
return result.isEmpty() ? QByteArray("(deleted user)") : result;
|
|
||||||
};
|
|
||||||
const auto pushFrom = [&](const QByteArray &label = "From") {
|
|
||||||
if (message.fromId) {
|
|
||||||
push(label, wrapUserName(message.fromId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const auto pushReplyToMsgId = [&](
|
|
||||||
const QByteArray &label = "Reply to message") {
|
|
||||||
if (message.replyToMsgId) {
|
|
||||||
push(label, "ID-" + NumberToString(message.replyToMsgId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const auto pushUserNames = [&](
|
|
||||||
const std::vector<int32> &data,
|
|
||||||
const QByteArray &labelOne = "Member",
|
|
||||||
const QByteArray &labelMany = "Members") {
|
|
||||||
auto list = std::vector<QByteArray>();
|
|
||||||
for (const auto userId : data) {
|
|
||||||
list.push_back(wrapUserName(userId));
|
|
||||||
}
|
|
||||||
if (list.size() == 1) {
|
|
||||||
push(labelOne, list[0]);
|
|
||||||
} else if (!list.empty()) {
|
|
||||||
push(labelMany, JoinList(", ", list));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const auto pushActor = [&] {
|
|
||||||
pushFrom("Actor");
|
|
||||||
};
|
|
||||||
const auto pushAction = [&](const QByteArray &action) {
|
|
||||||
push("Action", action);
|
|
||||||
};
|
|
||||||
const auto pushTTL = [&](
|
|
||||||
const QByteArray &label = "Self destruct period") {
|
|
||||||
if (const auto ttl = message.media.ttl) {
|
|
||||||
push(label, NumberToString(ttl) + " sec.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 + "(" + label + " unavailable, "
|
|
||||||
"please try again later)";
|
|
||||||
case SkipReason::FileSize:
|
|
||||||
return pre + "(" + label + " exceeds maximum size. "
|
|
||||||
"Change data exporting settings to download.)";
|
|
||||||
case SkipReason::FileType:
|
|
||||||
return pre + "(" + label + " not included. "
|
|
||||||
"Change data exporting settings to download.)";
|
|
||||||
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", NumberToString(image.width));
|
|
||||||
push("Height", NumberToString(image.height));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
v::match(message.action.content, [&](const ActionChatCreate &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Create group");
|
|
||||||
push("Title", data.title);
|
|
||||||
pushUserNames(data.userIds);
|
|
||||||
}, [&](const ActionChatEditTitle &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Edit group title");
|
|
||||||
push("New 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");
|
|
||||||
push("Member", wrapUserName(data.userId));
|
|
||||||
}, [&](const ActionChatJoinedByLink &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Join group by link");
|
|
||||||
push("Inviter", wrapUserName(data.inviterId));
|
|
||||||
}, [&](const ActionChannelCreate &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Create channel");
|
|
||||||
push("Title", data.title);
|
|
||||||
}, [&](const ActionChatMigrateTo &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Convert this group to supergroup");
|
|
||||||
}, [&](const ActionChannelMigrateFrom &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Basic group converted to supergroup");
|
|
||||||
push("Title", data.title);
|
|
||||||
}, [&](const ActionPinMessage &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Pin message");
|
|
||||||
pushReplyToMsgId("Message");
|
|
||||||
}, [&](const ActionHistoryClear &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Clear history");
|
|
||||||
}, [&](const ActionGameScore &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Score in a game");
|
|
||||||
pushReplyToMsgId("Game message");
|
|
||||||
push("Score", NumberToString(data.score));
|
|
||||||
}, [&](const ActionPaymentSent &data) {
|
|
||||||
pushAction("Send payment");
|
|
||||||
push(
|
|
||||||
"Amount",
|
|
||||||
Data::FormatMoneyAmount(data.amount, data.currency));
|
|
||||||
pushReplyToMsgId("Invoice message");
|
|
||||||
}, [&](const ActionPhoneCall &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Phone call");
|
|
||||||
if (data.duration) {
|
|
||||||
push("Duration", NumberToString(data.duration) + " sec.");
|
|
||||||
}
|
|
||||||
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", data.message);
|
|
||||||
}, [&](const ActionBotAllowed &data) {
|
|
||||||
pushAction("Allow sending messages");
|
|
||||||
push("Reason", "Login on \"" + data.domain + "\"");
|
|
||||||
}, [&](const ActionSecureValuesSent &data) {
|
|
||||||
pushAction("Send Telegram Passport values");
|
|
||||||
auto list = std::vector<QByteArray>();
|
|
||||||
for (const auto type : data.types) {
|
|
||||||
list.push_back([&] {
|
|
||||||
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 "";
|
|
||||||
}());
|
|
||||||
}
|
|
||||||
if (list.size() == 1) {
|
|
||||||
push("Value", list[0]);
|
|
||||||
} else if (!list.empty()) {
|
|
||||||
push("Values", JoinList(", ", list));
|
|
||||||
}
|
|
||||||
}, [&](const ActionContactSignUp &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Join Telegram");
|
|
||||||
}, [&](const ActionPhoneNumberRequest &data) {
|
|
||||||
pushActor();
|
|
||||||
pushAction("Request Phone Number");
|
|
||||||
}, [](v::null_t) {});
|
|
||||||
|
|
||||||
if (v::is_null(message.action.content)) {
|
|
||||||
pushFrom();
|
|
||||||
push("Author", message.signature);
|
|
||||||
if (message.forwardedFromId) {
|
|
||||||
push("Forwarded from", wrapPeerName(message.forwardedFromId));
|
|
||||||
} else if (!message.forwardedFromName.isEmpty()) {
|
|
||||||
push("Forwarded from", message.forwardedFromName);
|
|
||||||
}
|
|
||||||
if (message.savedFromChatId) {
|
|
||||||
push("Saved from", wrapPeerName(message.savedFromChatId));
|
|
||||||
}
|
|
||||||
pushReplyToMsgId();
|
|
||||||
if (message.viaBotId) {
|
|
||||||
push("Via", user(message.viaBotId).username);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v::match(message.media.content, [&](const Photo &photo) {
|
|
||||||
pushPhoto(photo.image);
|
|
||||||
pushTTL();
|
|
||||||
}, [&](const Document &data) {
|
|
||||||
const auto pushMyPath = [&](const QByteArray &label) {
|
|
||||||
return pushPath(data.file, label);
|
|
||||||
};
|
|
||||||
if (data.isSticker) {
|
|
||||||
pushMyPath("Sticker");
|
|
||||||
push("Emoji", data.stickerEmoji);
|
|
||||||
} else if (data.isVideoMessage) {
|
|
||||||
pushMyPath("Video message");
|
|
||||||
} else if (data.isVoiceMessage) {
|
|
||||||
pushMyPath("Voice message");
|
|
||||||
} else if (data.isAnimated) {
|
|
||||||
pushMyPath("Animation");
|
|
||||||
} else if (data.isVideoFile) {
|
|
||||||
pushMyPath("Video file");
|
|
||||||
} else if (data.isAudioFile) {
|
|
||||||
pushMyPath("Audio file");
|
|
||||||
push("Performer", data.songPerformer);
|
|
||||||
push("Title", data.songTitle);
|
|
||||||
} else {
|
|
||||||
pushMyPath("File");
|
|
||||||
}
|
|
||||||
if (!data.isSticker) {
|
|
||||||
push("Mime type", data.mime);
|
|
||||||
}
|
|
||||||
if (data.duration) {
|
|
||||||
push("Duration", NumberToString(data.duration) + " sec.");
|
|
||||||
}
|
|
||||||
if (data.width && data.height) {
|
|
||||||
push("Width", NumberToString(data.width));
|
|
||||||
push("Height", NumberToString(data.height));
|
|
||||||
}
|
|
||||||
pushTTL();
|
|
||||||
}, [&](const SharedContact &data) {
|
|
||||||
push("Contact information", SerializeKeyValue({
|
|
||||||
{ "First name", data.info.firstName },
|
|
||||||
{ "Last name", data.info.lastName },
|
|
||||||
{ "Phone number", FormatPhoneNumber(data.info.phoneNumber) },
|
|
||||||
}));
|
|
||||||
if (!data.vcard.content.isEmpty()) {
|
|
||||||
pushPath(data.vcard, "Contact vcard");
|
|
||||||
}
|
|
||||||
}, [&](const GeoPoint &data) {
|
|
||||||
push("Location", data.valid ? SerializeKeyValue({
|
|
||||||
{ "Latitude", NumberToString(data.latitude) },
|
|
||||||
{ "Longitude", NumberToString(data.longitude) },
|
|
||||||
}) : QByteArray("(empty value)"));
|
|
||||||
pushTTL("Live location period");
|
|
||||||
}, [&](const Venue &data) {
|
|
||||||
push("Place name", data.title);
|
|
||||||
push("Address", data.address);
|
|
||||||
if (data.point.valid) {
|
|
||||||
push("Location", SerializeKeyValue({
|
|
||||||
{ "Latitude", NumberToString(data.point.latitude) },
|
|
||||||
{ "Longitude", NumberToString(data.point.longitude) },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [&](const Game &data) {
|
|
||||||
push("Game", data.title);
|
|
||||||
push("Description", data.description);
|
|
||||||
if (data.botId != 0 && !data.shortName.isEmpty()) {
|
|
||||||
const auto bot = user(data.botId);
|
|
||||||
if (bot.isBot && !bot.username.isEmpty()) {
|
|
||||||
push("Link", internalLinksDomain.toUtf8()
|
|
||||||
+ bot.username
|
|
||||||
+ "?game="
|
|
||||||
+ data.shortName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [&](const Invoice &data) {
|
|
||||||
push("Invoice", SerializeKeyValue({
|
|
||||||
{ "Title", data.title },
|
|
||||||
{ "Description", data.description },
|
|
||||||
{
|
|
||||||
"Amount",
|
|
||||||
Data::FormatMoneyAmount(data.amount, data.currency)
|
|
||||||
},
|
|
||||||
{ "Receipt message", (data.receiptMsgId
|
|
||||||
? "ID-" + NumberToString(data.receiptMsgId)
|
|
||||||
: QByteArray()) }
|
|
||||||
}));
|
|
||||||
}, [&](const Poll &data) {
|
|
||||||
push("Poll", SerializeKeyValue({
|
|
||||||
{ "Question", data.question },
|
|
||||||
{ "Closed", data.closed ? QByteArray("Yes") : QByteArray() },
|
|
||||||
{ "Votes", NumberToString(data.totalVotes) },
|
|
||||||
}));
|
|
||||||
for (const auto &answer : data.answers) {
|
|
||||||
push("Answer", SerializeKeyValue({
|
|
||||||
{ "Text", answer.text },
|
|
||||||
{ "Votes", NumberToString(answer.votes) },
|
|
||||||
{ "Chosen", answer.my ? QByteArray("Yes") : QByteArray() }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [](const UnsupportedMedia &data) {
|
|
||||||
Unexpected("Unsupported message.");
|
|
||||||
}, [](v::null_t) {});
|
|
||||||
|
|
||||||
auto value = JoinList(QByteArray(), ranges::view::all(
|
|
||||||
message.text
|
|
||||||
) | ranges::view::transform([](const Data::TextPart &part) {
|
|
||||||
return part.text;
|
|
||||||
}) | ranges::to_vector);
|
|
||||||
push("Text", value);
|
|
||||||
|
|
||||||
return SerializeKeyValue(std::move(values));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Result TextWriter::start(
|
|
||||||
const Settings &settings,
|
|
||||||
const Environment &environment,
|
|
||||||
Stats *stats) {
|
|
||||||
Expects(settings.path.endsWith('/'));
|
|
||||||
|
|
||||||
_settings = base::duplicate(settings);
|
|
||||||
_environment = environment;
|
|
||||||
_stats = stats;
|
|
||||||
_summary = fileWithRelativePath(mainFileRelativePath());
|
|
||||||
return _summary->writeBlock(_environment.aboutTelegram
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writePersonal(const Data::PersonalInfo &data) {
|
|
||||||
Expects(_summary != nullptr);
|
|
||||||
|
|
||||||
const auto &info = data.user.info;
|
|
||||||
const auto serialized = SerializeKeyValue({
|
|
||||||
{ "First name", info.firstName },
|
|
||||||
{ "Last name", info.lastName },
|
|
||||||
{ "Phone number", Data::FormatPhoneNumber(info.phoneNumber) },
|
|
||||||
{ "Username", FormatUsername(data.user.username) },
|
|
||||||
{ "Bio", data.bio },
|
|
||||||
})
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak;
|
|
||||||
return _summary->writeBlock(serialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeUserpicsStart(const Data::UserpicsInfo &data) {
|
|
||||||
Expects(_summary != nullptr);
|
|
||||||
Expects(_userpics == nullptr);
|
|
||||||
|
|
||||||
_userpicsCount = data.count;
|
|
||||||
if (!_userpicsCount) {
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
const auto filename = "lists/profile_pictures.txt";
|
|
||||||
_userpics = fileWithRelativePath(filename);
|
|
||||||
|
|
||||||
const auto serialized = "Profile pictures"
|
|
||||||
"(" + Data::NumberToString(_userpicsCount) + ") - " + filename
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak;
|
|
||||||
return _summary->writeBlock(serialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeUserpicsSlice(const Data::UserpicsSlice &data) {
|
|
||||||
Expects(_userpics != nullptr);
|
|
||||||
Expects(!data.list.empty());
|
|
||||||
|
|
||||||
auto lines = std::vector<QByteArray>();
|
|
||||||
lines.reserve(data.list.size());
|
|
||||||
for (const auto &userpic : data.list) {
|
|
||||||
if (!userpic.date) {
|
|
||||||
lines.push_back("(deleted photo)");
|
|
||||||
} else {
|
|
||||||
using SkipReason = Data::File::SkipReason;
|
|
||||||
const auto &file = userpic.image.file;
|
|
||||||
Assert(!file.relativePath.isEmpty()
|
|
||||||
|| file.skipReason != SkipReason::None);
|
|
||||||
const auto path = [&]() -> Data::Utf8String {
|
|
||||||
switch (file.skipReason) {
|
|
||||||
case SkipReason::Unavailable:
|
|
||||||
return "(Photo unavailable, please try again later)";
|
|
||||||
case SkipReason::FileSize:
|
|
||||||
return "(Photo exceeds maximum size. "
|
|
||||||
"Change data exporting settings to download.)";
|
|
||||||
case SkipReason::FileType:
|
|
||||||
return "(Photo not included. "
|
|
||||||
"Change data exporting settings to download.)";
|
|
||||||
case SkipReason::None: return FormatFilePath(file);
|
|
||||||
}
|
|
||||||
Unexpected("Skip reason while writing photo path.");
|
|
||||||
}();
|
|
||||||
lines.push_back(SerializeKeyValue({
|
|
||||||
{ "Added", Data::FormatDateTime(userpic.date) },
|
|
||||||
{ "Photo", path },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _userpics->writeBlock(JoinList(kLineBreak, lines) + kLineBreak);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeUserpicsEnd() {
|
|
||||||
_userpics = nullptr;
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeContactsList(const Data::ContactsList &data) {
|
|
||||||
Expects(_summary != nullptr);
|
|
||||||
|
|
||||||
if (const auto result = writeSavedContacts(data); !result) {
|
|
||||||
return result;
|
|
||||||
} else if (const auto result = writeFrequentContacts(data); !result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeSavedContacts(const Data::ContactsList &data) {
|
|
||||||
if (data.list.empty()) {
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto filename = "lists/contacts.txt";
|
|
||||||
const auto file = fileWithRelativePath(filename);
|
|
||||||
auto list = std::vector<QByteArray>();
|
|
||||||
list.reserve(data.list.size());
|
|
||||||
for (const auto index : Data::SortedContactsIndices(data)) {
|
|
||||||
const auto &contact = data.list[index];
|
|
||||||
if (contact.firstName.isEmpty()
|
|
||||||
&& contact.lastName.isEmpty()
|
|
||||||
&& contact.phoneNumber.isEmpty()) {
|
|
||||||
list.push_back("(deleted user)" + kLineBreak);
|
|
||||||
} else {
|
|
||||||
list.push_back(SerializeKeyValue({
|
|
||||||
{ "First name", contact.firstName },
|
|
||||||
{ "Last name", contact.lastName },
|
|
||||||
{
|
|
||||||
"Phone number",
|
|
||||||
Data::FormatPhoneNumber(contact.phoneNumber)
|
|
||||||
},
|
|
||||||
{ "Added", Data::FormatDateTime(contact.date) }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const auto full = _environment.aboutContacts
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak
|
|
||||||
+ JoinList(kLineBreak, list);
|
|
||||||
if (const auto result = file->writeBlock(full); !result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto header = "Contacts "
|
|
||||||
"(" + Data::NumberToString(data.list.size()) + ") - " + filename
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak;
|
|
||||||
return _summary->writeBlock(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeFrequentContacts(const Data::ContactsList &data) {
|
|
||||||
const auto size = data.correspondents.size()
|
|
||||||
+ data.inlineBots.size()
|
|
||||||
+ data.phoneCalls.size();
|
|
||||||
if (!size) {
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto filename = "lists/frequent.txt";
|
|
||||||
const auto file = fileWithRelativePath(filename);
|
|
||||||
auto list = std::vector<QByteArray>();
|
|
||||||
list.reserve(size);
|
|
||||||
const auto writeList = [&](
|
|
||||||
const std::vector<Data::TopPeer> &peers,
|
|
||||||
Data::Utf8String category) {
|
|
||||||
for (const auto &top : peers) {
|
|
||||||
const auto user = [&]() -> Data::Utf8String {
|
|
||||||
if (!top.peer.user() || top.peer.user()->isSelf) {
|
|
||||||
return Data::Utf8String();
|
|
||||||
} else if (top.peer.name().isEmpty()) {
|
|
||||||
return "(deleted user)";
|
|
||||||
}
|
|
||||||
return top.peer.name();
|
|
||||||
}();
|
|
||||||
const auto chatType = [&] {
|
|
||||||
if (const auto chat = top.peer.chat()) {
|
|
||||||
return chat->username.isEmpty()
|
|
||||||
? (chat->isBroadcast
|
|
||||||
? "Private channel"
|
|
||||||
: (chat->isSupergroup
|
|
||||||
? "Private supergroup"
|
|
||||||
: "Private group"))
|
|
||||||
: (chat->isBroadcast
|
|
||||||
? "Public channel"
|
|
||||||
: "Public supergroup");
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}();
|
|
||||||
const auto chat = [&]() -> Data::Utf8String {
|
|
||||||
if (!top.peer.chat()) {
|
|
||||||
return Data::Utf8String();
|
|
||||||
} else if (top.peer.name().isEmpty()) {
|
|
||||||
return "(deleted chat)";
|
|
||||||
}
|
|
||||||
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", Data::NumberToString(top.rating) }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
writeList(data.correspondents, "People");
|
|
||||||
writeList(data.inlineBots, "Inline bots");
|
|
||||||
writeList(data.phoneCalls, "Calls");
|
|
||||||
const auto full = _environment.aboutFrequent
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak
|
|
||||||
+ JoinList(kLineBreak, list);
|
|
||||||
if (const auto result = file->writeBlock(full); !result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto header = "Frequent contacts "
|
|
||||||
"(" + Data::NumberToString(size) + ") - lists/frequent.txt"
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak;
|
|
||||||
return _summary->writeBlock(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeSessionsList(const Data::SessionsList &data) {
|
|
||||||
Expects(_summary != nullptr);
|
|
||||||
|
|
||||||
if (const auto result = writeSessions(data); !result) {
|
|
||||||
return result;
|
|
||||||
} else if (const auto result = writeWebSessions(data); !result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeSessions(const Data::SessionsList &data) {
|
|
||||||
Expects(_summary != nullptr);
|
|
||||||
|
|
||||||
if (data.list.empty()) {
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto filename = "lists/sessions.txt";
|
|
||||||
const auto file = fileWithRelativePath(filename);
|
|
||||||
auto list = std::vector<QByteArray>();
|
|
||||||
list.reserve(data.list.size());
|
|
||||||
for (const auto &session : data.list) {
|
|
||||||
list.push_back(SerializeKeyValue({
|
|
||||||
{ "Last active", Data::FormatDateTime(session.lastActive) },
|
|
||||||
{ "Last IP address", session.ip },
|
|
||||||
{ "Last country", session.country },
|
|
||||||
{ "Last region", session.region },
|
|
||||||
{
|
|
||||||
"Application name",
|
|
||||||
(session.applicationName.isEmpty()
|
|
||||||
? Data::Utf8String("(unknown)")
|
|
||||||
: session.applicationName)
|
|
||||||
},
|
|
||||||
{ "Application version", session.applicationVersion },
|
|
||||||
{ "Device model", session.deviceModel },
|
|
||||||
{ "Platform", session.platform },
|
|
||||||
{ "System version", session.systemVersion },
|
|
||||||
{ "Created", Data::FormatDateTime(session.created) },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
const auto full = _environment.aboutSessions
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak
|
|
||||||
+ JoinList(kLineBreak, list);
|
|
||||||
if (const auto result = file->writeBlock(full); !result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto header = "Sessions "
|
|
||||||
"(" + Data::NumberToString(data.list.size()) + ") - " + filename
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak;
|
|
||||||
return _summary->writeBlock(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeWebSessions(const Data::SessionsList &data) {
|
|
||||||
Expects(_summary != nullptr);
|
|
||||||
|
|
||||||
if (data.webList.empty()) {
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto filename = "lists/web_sessions.txt";
|
|
||||||
const auto file = fileWithRelativePath(filename);
|
|
||||||
auto list = std::vector<QByteArray>();
|
|
||||||
list.reserve(data.webList.size());
|
|
||||||
for (const auto &session : data.webList) {
|
|
||||||
list.push_back(SerializeKeyValue({
|
|
||||||
{ "Last active", Data::FormatDateTime(session.lastActive) },
|
|
||||||
{ "Last IP address", session.ip },
|
|
||||||
{ "Last region", session.region },
|
|
||||||
{
|
|
||||||
"Bot username",
|
|
||||||
(session.botUsername.isEmpty()
|
|
||||||
? Data::Utf8String("(unknown)")
|
|
||||||
: session.botUsername)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Domain name",
|
|
||||||
(session.domain.isEmpty()
|
|
||||||
? Data::Utf8String("(unknown)")
|
|
||||||
: session.domain)
|
|
||||||
},
|
|
||||||
{ "Browser", session.browser },
|
|
||||||
{ "Platform", session.platform },
|
|
||||||
{ "Created", Data::FormatDateTime(session.created) },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
const auto full = _environment.aboutWebSessions
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak
|
|
||||||
+ JoinList(kLineBreak, list);
|
|
||||||
if (const auto result = file->writeBlock(full); !result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto header = "Web sessions "
|
|
||||||
"(" + Data::NumberToString(data.webList.size()) + ") - " + filename
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak;
|
|
||||||
return _summary->writeBlock(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeOtherData(const Data::File &data) {
|
|
||||||
Expects(_summary != nullptr);
|
|
||||||
|
|
||||||
const auto header = "Other data - " + data.relativePath.toUtf8()
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak;
|
|
||||||
return _summary->writeBlock(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeDialogsStart(const Data::DialogsInfo &data) {
|
|
||||||
_dialogsCount = data.chats.size();
|
|
||||||
_leftChannelsCount = data.left.size();
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeDialogStart(const Data::DialogInfo &data) {
|
|
||||||
Expects(_chat == nullptr);
|
|
||||||
|
|
||||||
const auto result = validateDialogsMode(data.isLeftChannel);
|
|
||||||
if (!result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
_chat = fileWithRelativePath(data.relativePath + "messages.txt");
|
|
||||||
_messagesCount = 0;
|
|
||||||
_dialog = data;
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::validateDialogsMode(bool isLeftChannel) {
|
|
||||||
const auto mode = isLeftChannel
|
|
||||||
? DialogsMode::Left
|
|
||||||
: DialogsMode::Chats;
|
|
||||||
if (_dialogsMode == mode) {
|
|
||||||
return Result::Success();
|
|
||||||
} else if (_dialogsMode != DialogsMode::None) {
|
|
||||||
if (const auto result = writeChatsEnd(); !result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_dialogsMode = mode;
|
|
||||||
return writeChatsStart(
|
|
||||||
isLeftChannel ? _leftChannelsCount : _dialogsCount,
|
|
||||||
isLeftChannel ? "Left chats" : "Chats",
|
|
||||||
(isLeftChannel
|
|
||||||
? _environment.aboutLeftChats
|
|
||||||
: _environment.aboutChats),
|
|
||||||
isLeftChannel ? "lists/left_chats.txt" : "lists/chats.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeDialogSlice(const Data::MessagesSlice &data) {
|
|
||||||
Expects(_chat != nullptr);
|
|
||||||
Expects(!data.list.empty());
|
|
||||||
|
|
||||||
auto list = std::vector<QByteArray>();
|
|
||||||
list.reserve(data.list.size());
|
|
||||||
for (const auto &message : data.list) {
|
|
||||||
if (Data::SkipMessageByDate(message, _settings)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
list.push_back(SerializeMessage(
|
|
||||||
message,
|
|
||||||
data.peers,
|
|
||||||
_environment.internalLinksDomain));
|
|
||||||
++_messagesCount;
|
|
||||||
}
|
|
||||||
if (list.empty()) {
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
const auto full = _chat->empty()
|
|
||||||
? JoinList(kLineBreak, list)
|
|
||||||
: kLineBreak + JoinList(kLineBreak, list);
|
|
||||||
return _chat->writeBlock(full);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeDialogEnd() {
|
|
||||||
Expects(_chats != nullptr);
|
|
||||||
Expects(_chat != nullptr);
|
|
||||||
|
|
||||||
_chat = nullptr;
|
|
||||||
|
|
||||||
using Type = Data::DialogInfo::Type;
|
|
||||||
const auto TypeString = [](Type type) {
|
|
||||||
switch (type) {
|
|
||||||
case Type::Unknown: return "(unknown)";
|
|
||||||
case Type::Self:
|
|
||||||
case Type::Replies:
|
|
||||||
case Type::Personal: return "Personal chat";
|
|
||||||
case Type::Bot: return "Bot chat";
|
|
||||||
case Type::PrivateGroup: return "Private 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::DialogInfo &dialog,
|
|
||||||
Type type) -> QByteArray {
|
|
||||||
if (dialog.type == Type::Self) {
|
|
||||||
return "Saved messages";
|
|
||||||
} else if (dialog.type == Type::Replies) {
|
|
||||||
return "Replies";
|
|
||||||
}
|
|
||||||
const auto name = dialog.name;
|
|
||||||
if (!name.isEmpty()) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
switch (type) {
|
|
||||||
case Type::Unknown: return "(unknown)";
|
|
||||||
case Type::Personal: return "(deleted user)";
|
|
||||||
case Type::Bot: return "(deleted bot)";
|
|
||||||
case Type::PrivateGroup:
|
|
||||||
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(kLineBreak + SerializeKeyValue({
|
|
||||||
{ "Name", NameString(_dialog, _dialog.type) },
|
|
||||||
{ "Type", TypeString(_dialog.type) },
|
|
||||||
{
|
|
||||||
(_dialog.onlyMyMessages
|
|
||||||
? "Outgoing messages count"
|
|
||||||
: "Messages count"),
|
|
||||||
Data::NumberToString(_messagesCount)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Content",
|
|
||||||
(_messagesCount > 0
|
|
||||||
? (_dialog.relativePath + "messages.txt").toUtf8()
|
|
||||||
: QByteArray())
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeDialogsEnd() {
|
|
||||||
return writeChatsEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeChatsStart(
|
|
||||||
int count,
|
|
||||||
const QByteArray &listName,
|
|
||||||
const QByteArray &about,
|
|
||||||
const QString &fileName) {
|
|
||||||
Expects(_summary != nullptr);
|
|
||||||
Expects(_chats == nullptr);
|
|
||||||
|
|
||||||
if (!count) {
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
_chats = fileWithRelativePath(fileName);
|
|
||||||
|
|
||||||
const auto block = about + kLineBreak;
|
|
||||||
if (const auto result = _chats->writeBlock(block); !result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto header = listName + " "
|
|
||||||
"(" + Data::NumberToString(count) + ") - "
|
|
||||||
+ fileName.toUtf8()
|
|
||||||
+ kLineBreak
|
|
||||||
+ kLineBreak;
|
|
||||||
return _summary->writeBlock(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::writeChatsEnd() {
|
|
||||||
_chats = nullptr;
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result TextWriter::finish() {
|
|
||||||
return Result::Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString TextWriter::mainFilePath() {
|
|
||||||
return pathWithRelativePath(mainFileRelativePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString TextWriter::mainFileRelativePath() const {
|
|
||||||
return "export_results.txt";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString TextWriter::pathWithRelativePath(const QString &path) const {
|
|
||||||
return _settings.path + path;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<File> TextWriter::fileWithRelativePath(
|
|
||||||
const QString &path) const {
|
|
||||||
return std::make_unique<File>(pathWithRelativePath(path), _stats);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Output
|
|
||||||
} // namespace Export
|
|
|
@ -1,98 +0,0 @@
|
||||||
/*
|
|
||||||
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 TextWriter : public AbstractWriter {
|
|
||||||
public:
|
|
||||||
Format format() override {
|
|
||||||
return Format::Text;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result start(
|
|
||||||
const Settings &settings,
|
|
||||||
const Environment &environment,
|
|
||||||
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 writeOtherData(const Data::File &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 finish() override;
|
|
||||||
|
|
||||||
QString mainFilePath() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
enum class DialogsMode {
|
|
||||||
None,
|
|
||||||
Chats,
|
|
||||||
Left,
|
|
||||||
};
|
|
||||||
|
|
||||||
[[nodiscard]] QString mainFileRelativePath() const;
|
|
||||||
[[nodiscard]] QString pathWithRelativePath(const QString &path) const;
|
|
||||||
[[nodiscard]] std::unique_ptr<File> fileWithRelativePath(
|
|
||||||
const QString &path) const;
|
|
||||||
|
|
||||||
[[nodiscard]] Result writeSavedContacts(const Data::ContactsList &data);
|
|
||||||
[[nodiscard]] Result writeFrequentContacts(const Data::ContactsList &data);
|
|
||||||
|
|
||||||
[[nodiscard]] Result writeSessions(const Data::SessionsList &data);
|
|
||||||
[[nodiscard]] Result writeWebSessions(const Data::SessionsList &data);
|
|
||||||
|
|
||||||
[[nodiscard]] Result validateDialogsMode(bool isLeftChannel);
|
|
||||||
[[nodiscard]] Result writeChatsStart(
|
|
||||||
int count,
|
|
||||||
const QByteArray &listName,
|
|
||||||
const QByteArray &about,
|
|
||||||
const QString &fileName);
|
|
||||||
[[nodiscard]] Result writeChatsEnd();
|
|
||||||
|
|
||||||
Settings _settings;
|
|
||||||
Environment _environment;
|
|
||||||
Stats *_stats = nullptr;
|
|
||||||
|
|
||||||
std::unique_ptr<File> _summary;
|
|
||||||
|
|
||||||
int _userpicsCount = 0;
|
|
||||||
std::unique_ptr<File> _userpics;
|
|
||||||
|
|
||||||
int _dialogsCount = 0;
|
|
||||||
int _leftChannelsCount = 0;
|
|
||||||
Data::DialogInfo _dialog;
|
|
||||||
DialogsMode _dialogsMode = DialogsMode::None;
|
|
||||||
|
|
||||||
int _messagesCount = 0;
|
|
||||||
std::unique_ptr<File> _chats;
|
|
||||||
std::unique_ptr<File> _chat;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Output
|
|
||||||
} // namespace Export
|
|
|
@ -31,8 +31,6 @@ PRIVATE
|
||||||
export/output/export_output_result.h
|
export/output/export_output_result.h
|
||||||
export/output/export_output_stats.cpp
|
export/output/export_output_stats.cpp
|
||||||
export/output/export_output_stats.h
|
export/output/export_output_stats.h
|
||||||
export/output/export_output_text.cpp
|
|
||||||
export/output/export_output_text.h
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(td_export
|
target_include_directories(td_export
|
||||||
|
|
Loading…
Reference in a new issue