/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "export/output/export_output_json.h" #include "export/output/export_output_result.h" #include "export/data/export_data_types.h" #include "core/utils.h" #include #include #include #include #include namespace Export { namespace Output { namespace { using Context = details::JsonContext; struct RichSerializeContext { Context &json; const Data::RichMessage &message; }; QByteArray SerializeString(const QByteArray &value) { const auto size = value.size(); const auto begin = value.data(); const auto end = begin + size; auto result = QByteArray(); result.reserve(2 + size * 4); result.append('"'); for (auto p = begin; p != end; ++p) { const auto ch = *p; if (ch == '\n') { result.append("\\n", 2); } else if (ch == '\r') { result.append("\\r", 2); } else if (ch == '\t') { result.append("\\t", 2); } else if (ch == '"') { result.append("\\\"", 2); } else if (ch == '\\') { result.append("\\\\", 2); } else if (ch >= 0 && ch < 32) { result.append("\\x", 2).append('0' + (ch >> 4)); const auto left = (ch & 0x0F); if (left >= 10) { result.append('A' + (left - 10)); } else { result.append('0' + left); } } else if (ch == char(0xE2) && (p + 2 < end) && *(p + 1) == char(0x80)) { if (*(p + 2) == char(0xA8)) { // Line separator. result.append("\\u2028", 6); } else if (*(p + 2) == char(0xA9)) { // Paragraph separator. result.append("\\u2029", 6); } else { result.append(ch); } } else { result.append(ch); } } result.append('"'); return result; } QByteArray SerializeDate(TimeId date) { return SerializeString( QDateTime::fromSecsSinceEpoch(date).toString(Qt::ISODate).toUtf8()); } QByteArray SerializeDateRaw(TimeId date) { return SerializeString(QString::number(date).toUtf8()); } QByteArray StringAllowEmpty(const Data::Utf8String &data) { return data.isEmpty() ? data : SerializeString(data); } QByteArray StringAllowNull(const Data::Utf8String &data) { return data.isEmpty() ? QByteArray("null") : SerializeString(data); } QByteArray Indentation(int size) { return QByteArray(size, ' '); } QByteArray Indentation(const Context &context) { return Indentation(context.nesting.size()); } QByteArray SerializeObject( Context &context, const std::vector> &values) { const auto indent = Indentation(context); context.nesting.push_back(Context::kObject); const auto guard = gsl::finally([&] { context.nesting.pop_back(); }); const auto next = '\n' + Indentation(context); auto first = true; auto result = QByteArray(); result.append('{'); for (const auto &[key, value] : values) { if (value.isEmpty()) { continue; } if (first) { first = false; } else { result.append(','); } result.append(next).append(SerializeString(key)).append(": ", 2); result.append(value); } result.append('\n').append(indent).append("}"); return result; } QByteArray SerializeArray( Context &context, const std::vector &values) { const auto indent = Indentation(context.nesting.size()); const auto next = '\n' + Indentation(context.nesting.size() + 1); auto first = true; auto result = QByteArray(); result.append('['); for (const auto &value : values) { if (first) { first = false; } else { result.append(','); } result.append(next).append(value); } result.append('\n').append(indent).append("]"); return result; } QByteArray SerializeText( Context &context, const std::vector &data, bool serializeToObjects = false) { using Type = Data::TextPart::Type; if (data.empty()) { return serializeToObjects ? QByteArray("[]") : SerializeString(""); } context.nesting.push_back(Context::kArray); const auto text = ranges::views::all( data ) | ranges::views::transform([&](const Data::TextPart &part) { if ((part.type == Type::Text) && !serializeToObjects) { return SerializeString(part.text); } const auto typeString = [&] { switch (part.type) { case Type::Unknown: return "unknown"; case Type::Mention: return "mention"; case Type::Hashtag: return "hashtag"; case Type::BotCommand: return "bot_command"; case Type::Url: return "link"; case Type::Email: return "email"; case Type::Bold: return "bold"; case Type::Italic: return "italic"; case Type::Code: return "code"; case Type::Pre: return "pre"; case Type::Text: return "plain"; case Type::TextUrl: return "text_link"; case Type::MentionName: return "mention_name"; case Type::Phone: return "phone"; case Type::Cashtag: return "cashtag"; case Type::Underline: return "underline"; case Type::Strike: return "strikethrough"; case Type::Blockquote: return "blockquote"; case Type::BankCard: return "bank_card"; case Type::Spoiler: return "spoiler"; case Type::CustomEmoji: return "custom_emoji"; } Unexpected("Type in SerializeText."); }(); const auto additionalName = (part.type == Type::MentionName) ? "user_id" : (part.type == Type::CustomEmoji) ? "document_id" : (part.type == Type::Pre) ? "language" : (part.type == Type::TextUrl) ? "href" : (part.type == Type::Blockquote) ? "collapsed" : "none"; const auto additionalValue = (part.type == Type::MentionName) ? part.additional : (part.type == Type::Pre || part.type == Type::TextUrl || part.type == Type::CustomEmoji) ? SerializeString(part.additional) : (part.type == Type::Blockquote) ? (part.additional.isEmpty() ? "false" : "true") : QByteArray(); return SerializeObject(context, { { "type", SerializeString(typeString) }, { "text", SerializeString(part.text) }, { additionalName, additionalValue }, }); }) | ranges::to_vector; context.nesting.pop_back(); if (!serializeToObjects) { if (data.size() == 1 && data[0].type == Data::TextPart::Type::Text) { return text[0]; } } return SerializeArray(context, text); } Data::Utf8String FormatUsername(const Data::Utf8String &username) { return username.isEmpty() ? username : ('@' + username); } QByteArray FormatFilePath(const Data::File &file) { return file.relativePath.toUtf8(); } QByteArray SerializeRichBool(bool value) { return value ? "true" : "false"; } template QByteArray SerializeRichNumberString(Value value) { return SerializeString(Data::NumberToString(value)); } const Data::Photo *FindRichPhoto( const RichSerializeContext &context, uint64 photoId) { const auto i = context.message.photos.find(photoId); return (i != end(context.message.photos)) ? &i->second : nullptr; } const Data::Document *FindRichDocument( const RichSerializeContext &context, uint64 documentId) { const auto i = context.message.documents.find(documentId); return (i != end(context.message.documents)) ? &i->second : nullptr; } void AppendRichFileAvailability( std::vector> &values, const Data::File *file, const QByteArray &pathKey, const QByteArray &reasonKey) { using SkipReason = Data::File::SkipReason; if (file) { switch (file->skipReason) { case SkipReason::Unavailable: values.emplace_back( reasonKey, SerializeString("unavailable")); return; case SkipReason::FileType: values.emplace_back(reasonKey, SerializeString("file_type")); return; case SkipReason::FileSize: values.emplace_back(reasonKey, SerializeString("file_size")); return; case SkipReason::DateLimits: values.emplace_back(reasonKey, SerializeString("date_limits")); return; case SkipReason::None: if (!file->relativePath.isEmpty()) { values.emplace_back( pathKey, SerializeString(file->relativePath.toUtf8())); return; } break; } } values.emplace_back(reasonKey, SerializeString("unavailable")); } void AppendRichPhotoMetadata( std::vector> &values, const Data::Photo *photo, const QByteArray &pathKey, const QByteArray &reasonKey, const QByteArray &sizeKey, const QByteArray &widthKey, const QByteArray &heightKey) { AppendRichFileAvailability( values, photo ? &photo->image.file : nullptr, pathKey, reasonKey); if (!photo) { return; } values.emplace_back( sizeKey, Data::NumberToString(photo->image.file.size)); if (photo->image.width && photo->image.height) { values.emplace_back( widthKey, Data::NumberToString(photo->image.width)); values.emplace_back( heightKey, Data::NumberToString(photo->image.height)); } } void AppendRichDocumentMetadata( std::vector> &values, const Data::Document *document, bool includeDimensions) { AppendRichFileAvailability( values, document ? &document->file : nullptr, "file", "file_skip_reason"); if (!document) { return; } if (!document->name.isEmpty()) { values.emplace_back("file_name", SerializeString(document->name)); } values.emplace_back( "file_size", Data::NumberToString(document->file.size)); if (document->thumb.width > 0) { AppendRichFileAvailability( values, &document->thumb.file, "thumbnail", "thumbnail_skip_reason"); values.emplace_back( "thumbnail_file_size", Data::NumberToString(document->thumb.file.size)); } if (document->isSticker) { values.emplace_back("media_type", SerializeString("sticker")); if (!document->stickerEmoji.isEmpty()) { values.emplace_back( "sticker_emoji", SerializeString(document->stickerEmoji)); } } else if (document->isVideoMessage) { values.emplace_back("media_type", SerializeString("video_message")); } else if (document->isVoiceMessage) { values.emplace_back("media_type", SerializeString("voice_message")); } else if (document->isAnimated) { values.emplace_back("media_type", SerializeString("animation")); } else if (document->isVideoFile) { values.emplace_back("media_type", SerializeString("video_file")); } else if (document->isAudioFile) { values.emplace_back("media_type", SerializeString("audio_file")); if (!document->songPerformer.isEmpty()) { values.emplace_back( "performer", SerializeString(document->songPerformer)); } if (!document->songTitle.isEmpty()) { values.emplace_back( "title", SerializeString(document->songTitle)); } } if (!document->mime.isEmpty()) { values.emplace_back("mime_type", SerializeString(document->mime)); } if (document->duration) { values.emplace_back( "duration_seconds", Data::NumberToString(document->duration)); } if (includeDimensions && document->width && document->height) { values.emplace_back("width", Data::NumberToString(document->width)); values.emplace_back("height", Data::NumberToString(document->height)); } } template QByteArray SerializeRichArray( Context &context, const std::vector &data, Serializer &&serializer) { if (data.empty()) { return "[]"; } auto values = std::vector(); values.reserve(data.size()); { context.nesting.push_back(Context::kArray); const auto guard = gsl::finally([&] { context.nesting.pop_back(); }); for (const auto &value : data) { values.push_back(serializer(value)); } } return SerializeArray(context, values); } QByteArray RichTextTypeToString(Data::RichText::Type type) { using Type = Data::RichText::Type; switch (type) { case Type::Empty: return "empty"; case Type::Plain: return "plain"; case Type::Concat: return "concat"; case Type::Bold: return "bold"; case Type::Italic: return "italic"; case Type::Underline: return "underline"; case Type::Strike: return "strikethrough"; case Type::Fixed: return "code"; case Type::Url: return "text_link"; case Type::Email: return "email"; case Type::Phone: return "phone"; case Type::Subscript: return "subscript"; case Type::Superscript: return "superscript"; case Type::Marked: return "marked"; case Type::Anchor: return "anchor"; case Type::Math: return "math"; case Type::CustomEmoji: return "custom_emoji"; case Type::Spoiler: return "spoiler"; case Type::Mention: return "mention"; case Type::Hashtag: return "hashtag"; case Type::BotCommand: return "bot_command"; case Type::Cashtag: return "cashtag"; case Type::AutoUrl: return "link"; case Type::AutoEmail: return "email"; case Type::AutoPhone: return "phone"; case Type::BankCard: return "bank_card"; case Type::MentionName: return "mention_name"; case Type::FormattedDate: return "formatted_date"; case Type::InlineImage: return "inline_image"; case Type::Diff: return "diff"; } Unexpected("Type in RichText::Type."); } QByteArray SerializeRichText( RichSerializeContext &context, const Data::RichText &data); QByteArray SerializeRichTexts( RichSerializeContext &context, const std::vector &data); QByteArray SerializeRichTextChild( RichSerializeContext &context, const std::vector &children) { Expects(children.size() == 1); return SerializeRichText(context, children.front()); } QByteArray SerializeRichText( RichSerializeContext &context, const Data::RichText &data) { using Type = Data::RichText::Type; auto values = std::vector>{ { "type", SerializeString(RichTextTypeToString(data.type)) }, }; { context.json.nesting.push_back(Context::kObject); const auto guard = gsl::finally([&] { context.json.nesting.pop_back(); }); switch (data.type) { case Type::Empty: break; case Type::Plain: values.emplace_back("text", SerializeString(data.text)); break; case Type::Concat: values.emplace_back( "text", SerializeRichTexts(context, data.children)); break; case Type::Bold: case Type::Italic: case Type::Underline: case Type::Strike: case Type::Fixed: case Type::Subscript: case Type::Superscript: case Type::Marked: case Type::Spoiler: case Type::Mention: case Type::Hashtag: case Type::BotCommand: case Type::Cashtag: case Type::AutoUrl: case Type::AutoEmail: case Type::AutoPhone: case Type::BankCard: values.emplace_back( "text", SerializeRichTextChild(context, data.children)); break; case Type::Url: values.emplace_back("href", SerializeString(data.data)); if (data.id) { values.emplace_back( "webpage_id", SerializeRichNumberString(data.id)); } values.emplace_back( "text", SerializeRichTextChild(context, data.children)); break; case Type::Email: values.emplace_back("email", SerializeString(data.data)); values.emplace_back( "text", SerializeRichTextChild(context, data.children)); break; case Type::Phone: values.emplace_back("phone", SerializeString(data.data)); values.emplace_back( "text", SerializeRichTextChild(context, data.children)); break; case Type::Anchor: values.emplace_back("name", SerializeString(data.data)); values.emplace_back( "text", SerializeRichTextChild(context, data.children)); break; case Type::Math: values.emplace_back("source", SerializeString(data.data)); break; case Type::CustomEmoji: values.emplace_back("text", SerializeString(data.text)); values.emplace_back( "document_id", SerializeString(data.customEmojiData)); break; case Type::MentionName: values.emplace_back( "user_id", SerializeRichNumberString(data.id)); values.emplace_back( "text", SerializeRichTextChild(context, data.children)); break; case Type::FormattedDate: values.emplace_back( "text", SerializeRichTextChild(context, data.children)); values.emplace_back("date", SerializeDate(data.date)); values.emplace_back( "date_unixtime", SerializeDateRaw(data.date)); values.emplace_back( "relative", SerializeRichBool(data.relative)); values.emplace_back( "short_time", SerializeRichBool(data.shortTime)); values.emplace_back( "long_time", SerializeRichBool(data.longTime)); values.emplace_back( "short_date", SerializeRichBool(data.shortDate)); values.emplace_back( "long_date", SerializeRichBool(data.longDate)); values.emplace_back( "day_of_week", SerializeRichBool(data.dayOfWeek)); break; case Type::InlineImage: values.emplace_back( "document_id", SerializeRichNumberString(data.id)); AppendRichDocumentMetadata( values, FindRichDocument(context, data.id), false); values.emplace_back( "width", Data::NumberToString(data.width)); values.emplace_back( "height", Data::NumberToString(data.height)); break; case Type::Diff: values.emplace_back("unsupported", "true"); values.emplace_back( "text", SerializeRichTextChild(context, data.children)); values.emplace_back( "old_text", SerializeRichTextChild(context, data.oldChildren)); break; } } return SerializeObject(context.json, values); } QByteArray SerializeRichTexts( RichSerializeContext &context, const std::vector &data) { return SerializeRichArray( context.json, data, [&](const Data::RichText &text) { return SerializeRichText(context, text); }); } QByteArray SerializeRichCaption( RichSerializeContext &context, const Data::RichCaption &data) { auto values = std::vector>(); { context.json.nesting.push_back(Context::kObject); const auto guard = gsl::finally([&] { context.json.nesting.pop_back(); }); values.emplace_back("text", SerializeRichText(context, data.text)); values.emplace_back( "credit", SerializeRichText(context, data.credit)); } return SerializeObject(context.json, values); } QByteArray RichTaskStateToString(Data::RichTaskState state) { using State = Data::RichTaskState; switch (state) { case State::None: return "none"; case State::Unchecked: return "unchecked"; case State::Checked: return "checked"; } Unexpected("State in RichTaskState."); } QByteArray RichListKindToString(Data::RichListKind kind) { using Kind = Data::RichListKind; switch (kind) { case Kind::Bullet: return "bullet"; case Kind::Ordered: return "ordered"; } Unexpected("Kind in RichListKind."); } QByteArray RichListItemContentToString(Data::RichListItemContent content) { using Content = Data::RichListItemContent; switch (content) { case Content::Text: return "text"; case Content::Blocks: return "blocks"; } Unexpected("Content in RichListItemContent."); } QByteArray RichQuoteContentToString(Data::RichQuoteContent content) { using Content = Data::RichQuoteContent; switch (content) { case Content::Text: return "text"; case Content::Blocks: return "blocks"; } Unexpected("Content in RichQuoteContent."); } QByteArray RichTableAlignmentToString(Data::RichTableAlignment alignment) { using Alignment = Data::RichTableAlignment; switch (alignment) { case Alignment::Left: return "left"; case Alignment::Center: return "center"; case Alignment::Right: return "right"; } Unexpected("Alignment in RichTableAlignment."); } QByteArray RichTableVerticalAlignmentToString( Data::RichTableVerticalAlignment alignment) { using Alignment = Data::RichTableVerticalAlignment; switch (alignment) { case Alignment::Top: return "top"; case Alignment::Middle: return "middle"; case Alignment::Bottom: return "bottom"; } Unexpected("Alignment in RichTableVerticalAlignment."); } QByteArray RichChannelSourceToString(Data::RichChannel::Source source) { using Source = Data::RichChannel::Source; switch (source) { case Source::ChatEmpty: return "chat_empty"; case Source::Chat: return "chat"; case Source::ChatForbidden: return "chat_forbidden"; case Source::Channel: return "channel"; case Source::ChannelForbidden: return "channel_forbidden"; case Source::Community: return "community"; case Source::CommunityForbidden: return "community_forbidden"; } Unexpected("Source in RichChannel::Source."); } QByteArray RichMapPointSourceToString(Data::RichMapPoint::Source source) { using Source = Data::RichMapPoint::Source; switch (source) { case Source::GeoPointEmpty: return "geo_point_empty"; case Source::GeoPoint: return "geo_point"; case Source::InputGeoPointEmpty: return "input_geo_point_empty"; case Source::InputGeoPoint: return "input_geo_point"; } Unexpected("Source in RichMapPoint::Source."); } QByteArray SerializeRichBlock( RichSerializeContext &context, const Data::RichBlock &data); QByteArray SerializeRichBlocks( RichSerializeContext &context, const std::vector &data); QByteArray SerializeRichListItem( RichSerializeContext &context, const Data::RichListItem &data) { using Content = Data::RichListItemContent; auto values = std::vector>{ { "task_state", SerializeString(RichTaskStateToString(data.taskState)), }, { "content", SerializeString(RichListItemContentToString(data.content)), }, }; { context.json.nesting.push_back(Context::kObject); const auto guard = gsl::finally([&] { context.json.nesting.pop_back(); }); switch (data.content) { case Content::Text: if (data.text) { values.emplace_back( "text", SerializeRichText(context, *data.text)); } else { values.emplace_back( "text", SerializeRichText(context, Data::RichText())); } break; case Content::Blocks: values.emplace_back( "blocks", SerializeRichBlocks(context, data.blocks)); break; } } if (data.num) { values.emplace_back("num", SerializeString(*data.num)); } if (data.value) { values.emplace_back("value", Data::NumberToString(*data.value)); } if (data.type) { values.emplace_back("item_type", SerializeString(*data.type)); } return SerializeObject(context.json, values); } QByteArray SerializeRichListItems( RichSerializeContext &context, const std::vector &data) { return SerializeRichArray( context.json, data, [&](const Data::RichListItem &item) { return SerializeRichListItem(context, item); }); } QByteArray SerializeRichTableCell( RichSerializeContext &context, const Data::RichTableCell &data) { auto values = std::vector>(); if (data.text) { context.json.nesting.push_back(Context::kObject); const auto guard = gsl::finally([&] { context.json.nesting.pop_back(); }); values.emplace_back( "text", SerializeRichText(context, *data.text)); } if (data.colspan) { values.emplace_back("colspan", Data::NumberToString(*data.colspan)); } if (data.rowspan) { values.emplace_back("rowspan", Data::NumberToString(*data.rowspan)); } values.emplace_back("header", SerializeRichBool(data.header)); values.emplace_back( "align", SerializeString(RichTableAlignmentToString(data.alignment))); values.emplace_back( "vertical_align", SerializeString(RichTableVerticalAlignmentToString( data.verticalAlignment))); return SerializeObject(context.json, values); } QByteArray SerializeRichTableCells( RichSerializeContext &context, const std::vector &data) { return SerializeRichArray( context.json, data, [&](const Data::RichTableCell &cell) { return SerializeRichTableCell(context, cell); }); } QByteArray SerializeRichTableRow( RichSerializeContext &context, const Data::RichTableRow &data) { auto cells = QByteArray(); { context.json.nesting.push_back(Context::kObject); const auto guard = gsl::finally([&] { context.json.nesting.pop_back(); }); cells = SerializeRichTableCells(context, data.cells); } return SerializeObject(context.json, { { "cells", cells }, }); } QByteArray SerializeRichTableRows( RichSerializeContext &context, const std::vector &data) { return SerializeRichArray( context.json, data, [&](const Data::RichTableRow &row) { return SerializeRichTableRow(context, row); }); } QByteArray SerializeRichRelatedArticle( RichSerializeContext &context, const Data::RichRelatedArticle &data) { auto values = std::vector>{ { "url", SerializeString(data.url) }, { "webpage_id", SerializeRichNumberString(data.webpageId) }, }; if (data.title) { values.emplace_back("title", SerializeString(*data.title)); } if (data.description) { values.emplace_back( "description", SerializeString(*data.description)); } if (data.photoId) { values.emplace_back( "photo_id", SerializeRichNumberString(*data.photoId)); AppendRichPhotoMetadata( values, FindRichPhoto(context, *data.photoId), "photo", "photo_skip_reason", "photo_file_size", "photo_width", "photo_height"); } if (data.author) { values.emplace_back("author", SerializeString(*data.author)); } if (data.publishedDate) { values.emplace_back( "published_date", SerializeDate(*data.publishedDate)); values.emplace_back( "published_date_unixtime", SerializeDateRaw(*data.publishedDate)); } return SerializeObject(context.json, values); } QByteArray SerializeRichRelatedArticles( RichSerializeContext &context, const std::vector &data) { return SerializeRichArray( context.json, data, [&](const Data::RichRelatedArticle &article) { return SerializeRichRelatedArticle(context, article); }); } QByteArray SerializeRichChannel( RichSerializeContext &context, const Data::RichChannel &data) { using Source = Data::RichChannel::Source; auto values = std::vector>{ { "source_type", SerializeString(RichChannelSourceToString(data.source)), }, { "id", SerializeRichNumberString(data.id) }, }; if (data.accessHash) { values.emplace_back( "access_hash", SerializeRichNumberString(*data.accessHash)); } if (data.title) { values.emplace_back("title", SerializeString(*data.title)); } if (data.username) { values.emplace_back("username", SerializeString(*data.username)); } if (data.source == Source::Channel || data.source == Source::ChannelForbidden) { values.emplace_back("broadcast", SerializeRichBool(data.broadcast)); values.emplace_back("megagroup", SerializeRichBool(data.megagroup)); values.emplace_back("monoforum", SerializeRichBool(data.monoforum)); } return SerializeObject(context.json, values); } QByteArray SerializeRichMapPoint( RichSerializeContext &context, const Data::RichMapPoint &data) { using Source = Data::RichMapPoint::Source; auto values = std::vector>{ { "source_type", SerializeString(RichMapPointSourceToString(data.source)), }, }; switch (data.source) { case Source::GeoPointEmpty: case Source::InputGeoPointEmpty: break; case Source::GeoPoint: case Source::InputGeoPoint: values.emplace_back( "latitude", Data::NumberToString(data.latitude)); values.emplace_back( "longitude", Data::NumberToString(data.longitude)); break; } if (data.accessHash) { values.emplace_back( "access_hash", SerializeRichNumberString(*data.accessHash)); } if (data.accuracyRadius) { values.emplace_back( "accuracy_radius", Data::NumberToString(*data.accuracyRadius)); } return SerializeObject(context.json, values); } QByteArray RichBlockKindToString(Data::RichBlock::Kind kind) { using Kind = Data::RichBlock::Kind; switch (kind) { case Kind::Unsupported: return "unsupported"; case Kind::Heading: return "heading"; case Kind::Paragraph: return "paragraph"; case Kind::Footer: return "footer"; case Kind::Thinking: return "thinking"; case Kind::AuthorDate: return "author_date"; case Kind::Code: return "code"; case Kind::Divider: return "divider"; case Kind::Anchor: return "anchor"; case Kind::List: return "list"; case Kind::Quote: return "quote"; case Kind::Photo: return "photo"; case Kind::Video: return "video"; case Kind::Cover: return "cover"; case Kind::Embed: return "embed"; case Kind::EmbedPost: return "embed_post"; case Kind::Collage: return "collage"; case Kind::Slideshow: return "slideshow"; case Kind::Channel: return "channel"; case Kind::Audio: return "audio"; case Kind::Math: return "math"; case Kind::Table: return "table"; case Kind::Details: return "details"; case Kind::RelatedArticles: return "related_articles"; case Kind::Map: return "map"; case Kind::InputMap: return "input_map"; case Kind::Unknown: return "unsupported"; } Unexpected("Kind in RichBlock::Kind."); } QByteArray SerializeRichBlock( RichSerializeContext &context, const Data::RichBlock &data) { using Kind = Data::RichBlock::Kind; using ListKind = Data::RichListKind; using QuoteContent = Data::RichQuoteContent; auto values = std::vector>{ { "type", SerializeString(RichBlockKindToString(data.kind)) }, }; { context.json.nesting.push_back(Context::kObject); const auto guard = gsl::finally([&] { context.json.nesting.pop_back(); }); switch (data.kind) { case Kind::Unsupported: values.emplace_back( "source_type", SerializeString("page_block_unsupported")); values.emplace_back("unsupported", "true"); break; case Kind::Heading: values.emplace_back( "level", Data::NumberToString(data.headingLevel)); values.emplace_back( "text", SerializeRichText(context, data.text)); break; case Kind::Paragraph: case Kind::Footer: case Kind::Thinking: values.emplace_back( "text", SerializeRichText(context, data.text)); break; case Kind::AuthorDate: values.emplace_back( "author", SerializeRichText(context, data.text)); values.emplace_back("date", SerializeDate(data.date)); values.emplace_back( "date_unixtime", SerializeDateRaw(data.date)); break; case Kind::Code: values.emplace_back( "text", SerializeRichText(context, data.text)); values.emplace_back( "language", SerializeString(data.language)); break; case Kind::Divider: break; case Kind::Anchor: values.emplace_back("name", SerializeString(data.name)); break; case Kind::List: values.emplace_back( "kind", SerializeString(RichListKindToString(data.listKind))); if (data.listKind == ListKind::Ordered) { values.emplace_back( "reversed", SerializeRichBool(data.orderedList.reversed)); if (data.orderedList.start) { values.emplace_back( "start", Data::NumberToString(*data.orderedList.start)); } if (data.orderedList.type) { values.emplace_back( "list_type", SerializeString(*data.orderedList.type)); } } values.emplace_back( "items", SerializeRichListItems(context, data.listItems)); break; case Kind::Quote: values.emplace_back( "content", SerializeString(RichQuoteContentToString( data.quoteContent))); values.emplace_back( "pullquote", SerializeRichBool(data.pullquote)); switch (data.quoteContent) { case QuoteContent::Text: values.emplace_back( "text", SerializeRichText(context, data.text)); break; case QuoteContent::Blocks: values.emplace_back( "blocks", SerializeRichBlocks(context, data.blocks)); break; } values.emplace_back( "caption", SerializeRichText(context, data.quoteCaption)); break; case Kind::Photo: values.emplace_back( "photo_id", SerializeRichNumberString(data.photoId)); AppendRichPhotoMetadata( values, FindRichPhoto(context, data.photoId), "photo", "photo_skip_reason", "photo_file_size", "width", "height"); values.emplace_back( "spoiler", SerializeRichBool(data.spoiler)); if (data.optionalUrl) { values.emplace_back( "url", SerializeString(*data.optionalUrl)); } if (data.optionalWebpageId) { values.emplace_back( "webpage_id", SerializeRichNumberString(*data.optionalWebpageId)); } values.emplace_back( "caption", SerializeRichCaption(context, data.caption)); break; case Kind::Video: values.emplace_back( "document_id", SerializeRichNumberString(data.documentId)); AppendRichDocumentMetadata( values, FindRichDocument(context, data.documentId), true); values.emplace_back( "autoplay", SerializeRichBool(data.autoplay)); values.emplace_back("loop", SerializeRichBool(data.loop)); values.emplace_back( "spoiler", SerializeRichBool(data.spoiler)); values.emplace_back( "caption", SerializeRichCaption(context, data.caption)); break; case Kind::Cover: Expects(data.blocks.size() == 1); values.emplace_back( "block", SerializeRichBlock(context, data.blocks.front())); break; case Kind::Embed: if (data.optionalUrl) { values.emplace_back( "url", SerializeString(*data.optionalUrl)); } if (data.html) { values.emplace_back("html", SerializeString(*data.html)); } if (data.posterPhotoId) { values.emplace_back( "poster_photo_id", SerializeRichNumberString(*data.posterPhotoId)); AppendRichPhotoMetadata( values, FindRichPhoto(context, *data.posterPhotoId), "poster_photo", "poster_photo_skip_reason", "poster_photo_file_size", "poster_photo_width", "poster_photo_height"); } if (data.width) { values.emplace_back( "width", Data::NumberToString(*data.width)); } if (data.height) { values.emplace_back( "height", Data::NumberToString(*data.height)); } values.emplace_back( "full_width", SerializeRichBool(data.fullWidth)); values.emplace_back( "allow_scrolling", SerializeRichBool(data.allowScrolling)); values.emplace_back( "caption", SerializeRichCaption(context, data.caption)); break; case Kind::EmbedPost: values.emplace_back("url", SerializeString(data.url)); values.emplace_back( "webpage_id", SerializeRichNumberString(data.webpageId)); values.emplace_back( "author_photo_id", SerializeRichNumberString(data.authorPhotoId)); AppendRichPhotoMetadata( values, FindRichPhoto(context, data.authorPhotoId), "author_photo", "author_photo_skip_reason", "author_photo_file_size", "author_photo_width", "author_photo_height"); values.emplace_back("author", SerializeString(data.author)); values.emplace_back("date", SerializeDate(data.date)); values.emplace_back( "date_unixtime", SerializeDateRaw(data.date)); values.emplace_back( "blocks", SerializeRichBlocks(context, data.blocks)); values.emplace_back( "caption", SerializeRichCaption(context, data.caption)); break; case Kind::Collage: case Kind::Slideshow: values.emplace_back( "items", SerializeRichBlocks(context, data.blocks)); values.emplace_back( "caption", SerializeRichCaption(context, data.caption)); break; case Kind::Channel: values.emplace_back( "channel", SerializeRichChannel(context, data.channel)); break; case Kind::Audio: values.emplace_back( "document_id", SerializeRichNumberString(data.documentId)); AppendRichDocumentMetadata( values, FindRichDocument(context, data.documentId), true); values.emplace_back( "caption", SerializeRichCaption(context, data.caption)); break; case Kind::Math: values.emplace_back("formula", SerializeString(data.formula)); break; case Kind::Table: values.emplace_back( "title", SerializeRichText(context, data.text)); values.emplace_back( "bordered", SerializeRichBool(data.bordered)); values.emplace_back( "striped", SerializeRichBool(data.striped)); values.emplace_back( "rows", SerializeRichTableRows(context, data.tableRows)); break; case Kind::Details: values.emplace_back( "title", SerializeRichText(context, data.text)); values.emplace_back("open", SerializeRichBool(data.open)); values.emplace_back( "blocks", SerializeRichBlocks(context, data.blocks)); break; case Kind::RelatedArticles: values.emplace_back( "title", SerializeRichText(context, data.text)); values.emplace_back( "articles", SerializeRichRelatedArticles( context, data.relatedArticles)); break; case Kind::Map: case Kind::InputMap: values.emplace_back( "geo", SerializeRichMapPoint(context, data.mapPoint)); values.emplace_back("zoom", Data::NumberToString(data.zoom)); values.emplace_back( "width", Data::NumberToString(data.mapWidth)); values.emplace_back( "height", Data::NumberToString(data.mapHeight)); values.emplace_back( "caption", SerializeRichCaption(context, data.caption)); break; case Kind::Unknown: values.emplace_back( "source_type", SerializeString("unknown")); values.emplace_back("unsupported", "true"); break; } } return SerializeObject(context.json, values); } QByteArray SerializeRichBlocks( RichSerializeContext &context, const std::vector &data) { return SerializeRichArray( context.json, data, [&](const Data::RichBlock &block) { return SerializeRichBlock(context, block); }); } QByteArray SerializeRichMessage( Context &context, const Data::RichMessage &data) { auto richContext = RichSerializeContext{ context, data }; auto values = std::vector>{ { "rtl", SerializeRichBool(data.rtl) }, { "part", SerializeRichBool(data.part) }, }; { context.nesting.push_back(Context::kObject); const auto guard = gsl::finally([&] { context.nesting.pop_back(); }); values.emplace_back( "blocks", SerializeRichBlocks(richContext, data.blocks)); } return SerializeObject(context, values); } QByteArray SerializeMessage( Context &context, const Data::Message &message, const std::map &peers, const QString &internalLinksDomain) { using namespace Data; if (v::is(message.media.content) && !message.richMessage) { return SerializeObject(context, { { "id", Data::NumberToString(message.id) }, { "type", SerializeString("unsupported") } }); } const auto peer = [&](PeerId peerId) -> const Peer& { if (const auto i = peers.find(peerId); i != end(peers)) { return i->second; } static auto empty = Peer{ User() }; return empty; }; const auto user = [&](UserId userId) -> const User& { if (const auto result = peer(userId).user()) { return *result; } static auto empty = User(); return empty; }; auto values = std::vector>{ { "id", NumberToString(message.id) }, { "type", SerializeString(!v::is_null(message.action.content) ? "service" : "message") }, { "date", SerializeDate(message.date) }, { "date_unixtime", SerializeDateRaw(message.date) }, }; context.nesting.push_back(Context::kObject); const auto serialized = [&] { context.nesting.pop_back(); return SerializeObject(context, values); }; const auto pushBare = [&]( const QByteArray &key, const QByteArray &value) { if (!value.isEmpty()) { values.emplace_back(key, value); } }; if (message.edited) { pushBare("edited", SerializeDate(message.edited)); pushBare("edited_unixtime", SerializeDateRaw(message.edited)); } const auto wrapPeerId = [&](PeerId peerId) { if (const auto chat = peerToChat(peerId)) { return SerializeString("chat" + Data::NumberToString(chat.bare)); } else if (const auto channel = peerToChannel(peerId)) { return SerializeString("channel" + Data::NumberToString(channel.bare)); } return SerializeString("user" + Data::NumberToString(peerToUser(peerId).bare)); }; const auto push = [&](const QByteArray &key, const auto &value) { using V = std::decay_t; if constexpr (std::is_same_v) { pushBare(key, value ? "true" : "false"); } else if constexpr (std::is_arithmetic_v) { pushBare(key, Data::NumberToString(value)); } else if constexpr (std::is_same_v) { pushBare(key, wrapPeerId(value)); } else { const auto wrapped = QByteArray(value); if (!wrapped.isEmpty()) { pushBare(key, SerializeString(wrapped)); } } }; const auto wrapPeerName = [&](PeerId peerId) { return StringAllowNull(peer(peerId).name()); }; const auto wrapUserName = [&](UserId userId) { return StringAllowNull(user(userId).name()); }; const auto pushFrom = [&](const QByteArray &label = "from") { if (message.fromId) { pushBare(label, wrapPeerName(message.fromId)); push(label + "_id", message.fromId); } }; const auto pushReplyToMsgId = [&]( const QByteArray &label = "reply_to_message_id") { if (message.replyToMsgId) { push(label, message.replyToMsgId); if (message.replyToPeerId) { push("reply_to_peer_id", message.replyToPeerId); } } }; const auto pushUserNames = [&]( const std::vector &data, const QByteArray &label = "members") { auto list = std::vector(); for (const auto &userId : data) { list.push_back(wrapUserName(userId)); } pushBare(label, SerializeArray(context, list)); }; const auto pushActor = [&] { pushFrom("actor"); }; const auto pushAction = [&](const QByteArray &action) { push("action", action); }; const auto pushTTL = [&]( const QByteArray &label = "self_destruct_period_seconds") { if (const auto ttl = message.media.ttl) { push(label, ttl); } }; using SkipReason = Data::File::SkipReason; const auto pushPath = [&]( const Data::File &file, const QByteArray &label, const QByteArray &name = QByteArray()) { Expects(!file.relativePath.isEmpty() || file.skipReason != SkipReason::None); push(label, [&]() -> QByteArray { const auto pre = name.isEmpty() ? QByteArray() : name + ' '; switch (file.skipReason) { case SkipReason::Unavailable: return pre + "(File unavailable, please try again later)"; case SkipReason::FileSize: return pre + "(File exceeds maximum size. " "Change data exporting settings to download.)"; case SkipReason::FileType: return pre + "(File 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"); push("photo_file_size", image.file.size); if (image.width && image.height) { push("width", image.width); push("height", image.height); } }; const auto pushSpoiler = [&](const auto &media) { if (media.spoilered) { push("media_spoiler", true); } }; 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("title", data.title); }, [&](const ActionChatEditPhoto &data) { pushActor(); pushAction("edit_group_photo"); pushPhoto(data.photo.image); pushSpoiler(data.photo); }, [&](const ActionChatDeletePhoto &data) { pushActor(); pushAction("delete_group_photo"); }, [&](const ActionChatAddUser &data) { pushActor(); pushAction("invite_members"); pushUserNames(data.userIds); }, [&](const ActionChatDeleteUser &data) { pushActor(); pushAction("remove_members"); pushUserNames({ data.userId }); }, [&](const ActionChatJoinedByLink &data) { pushActor(); pushAction("join_group_by_link"); pushBare("inviter", wrapUserName(data.inviterId)); }, [&](const ActionChannelCreate &data) { pushActor(); pushAction("create_channel"); push("title", data.title); }, [&](const ActionChatMigrateTo &data) { pushActor(); pushAction("migrate_to_supergroup"); }, [&](const ActionChannelMigrateFrom &data) { pushActor(); pushAction("migrate_from_group"); push("title", data.title); }, [&](const ActionPinMessage &data) { pushActor(); pushAction("pin_message"); pushReplyToMsgId("message_id"); }, [&](const ActionHistoryClear &data) { pushActor(); pushAction("clear_history"); }, [&](const ActionGameScore &data) { pushActor(); pushAction("score_in_game"); pushReplyToMsgId("game_message_id"); push("score", data.score); }, [&](const ActionPaymentSent &data) { pushAction("send_payment"); push("amount", data.amount); push("currency", data.currency); pushReplyToMsgId("invoice_message_id"); if (data.recurringUsed) { push("recurring", "used"); } else if (data.recurringInit) { push("recurring", "init"); } }, [&](const ActionPhoneCall &data) { pushActor(); pushAction(data.conferenceId ? "conference_call" : "phone_call"); if (data.duration) { push("duration_seconds", data.duration); } using State = ActionPhoneCall::State; if (data.conferenceId) { push("is_active", data.state == State::Active); push("is_missed", data.state == State::Missed); } else { push("discard_reason", [&] { switch (data.state) { case State::Busy: return "busy"; case State::Disconnect: return "disconnect"; case State::Hangup: return "hangup"; case State::Missed: return "missed"; case State::MigrateConferenceCall: return "migrate_conference_all"; } return ""; }()); } }, [&](const ActionScreenshotTaken &data) { pushActor(); pushAction("take_screenshot"); }, [&](const ActionCustomAction &data) { pushActor(); push("information_text", data.message); }, [&](const ActionBotAllowed &data) { if (data.attachMenu) { pushAction("attach_menu_bot_allowed"); } else if (data.fromRequest) { pushAction("web_app_bot_allowed"); } else if (data.appId) { pushAction("allow_sending_messages"); push("reason_app_id", data.appId); push("reason_app_name", data.app); } else { pushAction("allow_sending_messages"); push("reason_domain", data.domain); } }, [&](const ActionSecureValuesSent &data) { pushAction("send_passport_values"); auto list = std::vector(); for (const auto type : data.types) { list.push_back(SerializeString([&] { using Type = ActionSecureValuesSent::Type; switch (type) { case Type::PersonalDetails: return "personal_details"; case Type::Passport: return "passport"; case Type::DriverLicense: return "driver_license"; case Type::IdentityCard: return "identity_card"; case Type::InternalPassport: return "internal_passport"; case Type::Address: return "address_information"; case Type::UtilityBill: return "utility_bill"; case Type::BankStatement: return "bank_statement"; case Type::RentalAgreement: return "rental_agreement"; case Type::PassportRegistration: return "passport_registration"; case Type::TemporaryRegistration: return "temporary_registration"; case Type::Phone: return "phone_number"; case Type::Email: return "email"; } return ""; }())); } pushBare("values", SerializeArray(context, list)); }, [&](const ActionContactSignUp &data) { pushActor(); pushAction("joined_telegram"); }, [&](const ActionGeoProximityReached &data) { pushAction("proximity_reached"); if (data.fromId) { pushBare("from", wrapPeerName(data.fromId)); push("from_id", data.fromId); } if (data.toId) { pushBare("to", wrapPeerName(data.toId)); push("to_id", data.toId); } push("distance", data.distance); }, [&](const ActionPhoneNumberRequest &data) { pushActor(); pushAction("requested_phone_number"); }, [&](const ActionGroupCall &data) { pushActor(); pushAction("group_call"); if (data.duration) { push("duration", data.duration); } }, [&](const ActionInviteToGroupCall &data) { pushActor(); pushAction("invite_to_group_call"); pushUserNames(data.userIds); }, [&](const ActionSetMessagesTTL &data) { pushActor(); pushAction("set_messages_ttl"); push("period", data.period); }, [&](const ActionGroupCallScheduled &data) { pushActor(); pushAction("group_call_scheduled"); push("schedule_date", data.date); }, [&](const ActionSetChatTheme &data) { pushActor(); pushAction("edit_chat_theme"); if (!data.emoji.isEmpty()) { push("emoticon", data.emoji.toUtf8()); } }, [&](const ActionChatJoinedByRequest &data) { pushActor(); pushAction("join_group_by_request"); }, [&](const ActionWebViewDataSent &data) { pushAction("send_webview_data"); push("text", data.text); }, [&](const ActionGiftPremium &data) { pushActor(); pushAction("send_premium_gift"); if (!data.cost.isEmpty()) { push("cost", data.cost); } if (data.days) { push("days", data.days); } }, [&](const ActionTopicCreate &data) { pushActor(); pushAction("topic_created"); push("title", data.title); }, [&](const ActionTopicEdit &data) { pushActor(); pushAction("topic_edit"); if (!data.title.isEmpty()) { push("new_title", data.title); } if (data.iconEmojiId) { push("new_icon_emoji_id", *data.iconEmojiId); } }, [&](const ActionSuggestProfilePhoto &data) { pushActor(); pushAction("suggest_profile_photo"); pushPhoto(data.photo.image); pushSpoiler(data.photo); }, [&](const ActionRequestedPeer &data) { pushActor(); pushAction("requested_peer"); push("button_id", data.buttonId); auto values = std::vector(); for (const auto &one : data.peers) { values.push_back(Data::NumberToString(one.value)); } push("peers", SerializeArray(context, values)); }, [&](const ActionGiftCode &data) { pushAction("gift_code_prize"); push("gift_code", data.code); if (data.boostPeerId) { push("boost_peer_id", data.boostPeerId); } push("days", data.days); push("unclaimed", data.unclaimed); push("via_giveaway", data.viaGiveaway); }, [&](const ActionGiveawayLaunch &data) { pushAction("giveaway_launch"); }, [&](const ActionGiveawayResults &data) { pushAction("giveaway_results"); push("winners", data.winners); push("unclaimed", data.unclaimed); push("stars", data.credits); }, [&](const ActionSetChatWallPaper &data) { pushActor(); pushAction(data.same ? "set_same_chat_wallpaper" : "set_chat_wallpaper"); pushReplyToMsgId("message_id"); }, [&](const ActionBoostApply &data) { pushActor(); pushAction("boost_apply"); push("boosts", data.boosts); }, [&](const ActionPaymentRefunded &data) { pushAction("refunded_payment"); push("amount", data.amount); push("currency", data.currency); pushBare("peer_name", wrapPeerName(data.peerId)); push("peer_id", data.peerId); push("charge_id", data.transactionId); }, [&](const ActionGiftCredits &data) { pushActor(); pushAction(data.amount.ton() ? "send_ton_gift" : "send_stars_gift"); if (!data.cost.isEmpty()) { push("cost", data.cost); } if (data.amount) { push("amount_whole", data.amount.whole()); push("amount_nano", data.amount.nano()); } }, [&](const ActionPrizeStars &data) { pushActor(); pushAction("stars_prize"); push("boost_peer_id", data.peerId); pushBare("boost_peer_name", wrapPeerName(data.peerId)); push("stars", data.amount); push("is_unclaimed", data.isUnclaimed); push("giveaway_msg_id", data.giveawayMsgId); push("transaction_id", data.transactionId); }, [&](const ActionStarGift &data) { pushActor(); pushAction("send_star_gift"); push("gift_id", data.giftId); push("stars", data.stars); push("is_limited", data.limited); push("is_anonymous", data.anonymous); pushBare("gift_text", SerializeText(context, data.text)); }, [&](const ActionPaidMessagesRefunded &data) { pushActor(); pushAction("paid_messages_refund"); push("messages_count", data.messages); push("stars_count", data.stars); }, [&](const ActionPaidMessagesPrice &data) { pushActor(); pushAction("paid_messages_price_change"); push("price_stars", data.stars); push("is_broadcast_messages_allowed", data.broadcastAllowed); }, [&](const ActionTodoCompletions &data) { pushActor(); pushAction("todo_completions"); auto completed = QByteArrayList(); for (const auto index : data.completed) { completed.push_back(QByteArray::number(index)); } auto incompleted = QByteArrayList(); for (const auto index : data.incompleted) { incompleted.push_back(QByteArray::number(index)); } pushBare("completed", '[' + completed.join(',') + ']'); pushBare("incompleted", '[' + incompleted.join(',') + ']'); }, [&](const ActionTodoAppendTasks &data) { pushActor(); pushAction("todo_append_tasks"); const auto items = ranges::views::all( data.items ) | ranges::views::transform([&](const TodoListItem &item) { context.nesting.push_back(Context::kArray); auto result = SerializeObject(context, { { "text", SerializeText(context, item.text) }, { "id", NumberToString(item.id) }, }); context.nesting.pop_back(); return result; }) | ranges::to_vector; pushBare("items", SerializeArray(context, items)); }, [&](const ActionPollAppendAnswer &data) { pushActor(); pushAction("poll_append_answer"); push("option", data.option); }, [&](const ActionPollDeleteAnswer &data) { pushActor(); pushAction("poll_delete_answer"); push("option", data.option); }, [&](const ActionSuggestedPostApproval &data) { pushActor(); pushAction("process_suggested_post"); if (data.rejected) { pushBare("rejected", "true"); if (!data.rejectComment.isEmpty()) { push("comment", data.rejectComment); } } else { push("price_amount_whole", NumberToString(data.price.whole())); push("price_amount_nano", NumberToString(data.price.nano())); push("price_currency", data.price.ton() ? "TON" : "Stars"); push("scheduled_date", data.scheduleDate); } }, [&](const ActionSuggestedPostSuccess &data) { pushActor(); pushAction("suggested_post_success"); push("price_amount_whole", NumberToString(data.price.whole())); push("price_amount_nano", NumberToString(data.price.nano())); push("price_currency", data.price.ton() ? "TON" : "Stars"); }, [&](const ActionSuggestedPostRefund &data) { pushActor(); pushAction("suggested_post_refund"); push("user_initiated", data.payerInitiated); }, [&](const ActionSuggestBirthday &data) { pushActor(); pushAction("suggest_birthday"); push("day", data.birthday.day()); push("month", data.birthday.month()); if (const auto year = data.birthday.year()) { push("year", year); } }, [&](const ActionNoForwardsToggle &data) { pushActor(); pushAction("no_forwards_toggle"); push("new_value", data.newValue); }, [&](const ActionNoForwardsRequest &data) { pushActor(); pushAction("no_forwards_request"); push("expired", data.expired); push("new_value", data.newValue); }, [&](const ActionNewCreatorPending &data) { pushActor(); pushAction("new_creator_pending"); pushBare("new_creator", wrapUserName(data.newCreatorId)); }, [&](const ActionChangeCreator &data) { pushActor(); pushAction("change_creator"); pushBare("new_creator", wrapUserName(data.newCreatorId)); }, [&](const ActionManagedBotCreated &data) { pushActor(); pushAction("managed_bot_created"); pushBare("bot", wrapUserName(data.botId)); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { pushFrom(); push("author", message.signature); if (message.forwardedFromId) { pushBare( "forwarded_from", wrapPeerName(message.forwardedFromId)); push("forwarded_from_id", message.forwardedFromId); } else if (!message.forwardedFromName.isEmpty()) { pushBare( "forwarded_from", StringAllowNull(message.forwardedFromName)); } if (message.savedFromChatId) { pushBare("saved_from", wrapPeerName(message.savedFromChatId)); } pushReplyToMsgId(); if (message.viaBotId) { const auto username = FormatUsername( user(message.viaBotId).username); if (!username.isEmpty()) { push("via_bot", username); } } } v::match(message.media.content, [&](const Photo &photo) { pushPhoto(photo.image); pushSpoiler(photo); pushTTL(); }, [&](const Document &data) { pushPath(data.file, "file"); push("file_name", data.name); push("file_size", data.file.size); if (data.thumb.width > 0) { pushPath(data.thumb.file, "thumbnail"); push("thumbnail_file_size", data.thumb.file.size); } const auto pushType = [&](const QByteArray &value) { push("media_type", value); }; if (data.isSticker) { pushType("sticker"); push("sticker_emoji", data.stickerEmoji); } else if (data.isVideoMessage) { pushType("video_message"); } else if (data.isVoiceMessage) { pushType("voice_message"); } else if (data.isAnimated) { pushType("animation"); } else if (data.isVideoFile) { pushType("video_file"); } else if (data.isAudioFile) { pushType("audio_file"); push("performer", data.songPerformer); push("title", data.songTitle); } push("mime_type", data.mime); if (data.duration) { push("duration_seconds", data.duration); } if (data.width && data.height) { push("width", data.width); push("height", data.height); } pushSpoiler(data); pushTTL(); }, [&](const SharedContact &data) { pushBare("contact_information", SerializeObject(context, { { "first_name", SerializeString(data.info.firstName) }, { "last_name", SerializeString(data.info.lastName) }, { "phone_number", SerializeString(FormatPhoneNumber(data.info.phoneNumber)) } })); if (!data.vcard.content.isEmpty()) { pushPath(data.vcard, "contact_vcard"); push("contact_vcard_file_size", data.vcard.size); } }, [&](const GeoPoint &data) { pushBare( "location_information", data.valid ? SerializeObject(context, { { "latitude", NumberToString(data.latitude) }, { "longitude", NumberToString(data.longitude) }, }) : QByteArray("null")); pushTTL("live_location_period_seconds"); }, [&](const Venue &data) { push("place_name", data.title); push("address", data.address); if (data.point.valid) { pushBare("location_information", SerializeObject(context, { { "latitude", NumberToString(data.point.latitude) }, { "longitude", NumberToString(data.point.longitude) }, })); } }, [&](const Game &data) { push("game_title", data.title); push("game_description", data.description); if (data.botId != 0 && !data.shortName.isEmpty()) { const auto bot = user(data.botId); if (bot.isBot && !bot.username.isEmpty()) { push("game_link", internalLinksDomain.toUtf8() + bot.username + "?game=" + data.shortName); } } }, [&](const Invoice &data) { push("invoice_information", SerializeObject(context, { { "title", SerializeString(data.title) }, { "description", SerializeString(data.description) }, { "amount", NumberToString(data.amount) }, { "currency", SerializeString(data.currency) }, { "receipt_message_id", (data.receiptMsgId ? NumberToString(data.receiptMsgId) : QByteArray()) } })); }, [&](const Poll &data) { context.nesting.push_back(Context::kObject); const auto answers = ranges::views::all( data.answers ) | ranges::views::transform([&](const Poll::Answer &answer) { context.nesting.push_back(Context::kArray); auto result = SerializeObject(context, { { "text", SerializeText(context, answer.text) }, { "voters", NumberToString(answer.votes) }, { "chosen", answer.my ? "true" : "false" }, }); context.nesting.pop_back(); return result; }) | ranges::to_vector; const auto serialized = SerializeArray(context, answers); context.nesting.pop_back(); pushBare("poll", SerializeObject(context, { { "question", SerializeText(context, data.question) }, { "closed", data.closed ? "true" : "false" }, { "total_voters", NumberToString(data.totalVotes) }, { "answers", serialized } })); }, [&](const TodoList &data) { context.nesting.push_back(Context::kObject); const auto items = ranges::views::all( data.items ) | ranges::views::transform([&](const TodoListItem &item) { context.nesting.push_back(Context::kArray); auto result = SerializeObject(context, { { "text", SerializeText(context, item.text) }, { "id", NumberToString(item.id) }, }); context.nesting.pop_back(); return result; }) | ranges::to_vector; const auto serialized = SerializeArray(context, items); context.nesting.pop_back(); pushBare("todo_list", SerializeObject(context, { { "title", SerializeText(context, data.title) }, { "others_can_append", data.othersCanAppend ? "true" : "false" }, { "others_can_complete", data.othersCanComplete ? "true" : "false", }, { "answers", serialized } })); }, [&](const GiveawayStart &data) { context.nesting.push_back(Context::kArray); const auto channels = ranges::views::all( data.channels ) | ranges::views::transform([&](ChannelId id) { return NumberToString(id.bare); }) | ranges::to_vector; const auto serialized = SerializeArray(context, channels); context.nesting.pop_back(); context.nesting.push_back(Context::kArray); const auto countries = ranges::views::all( data.countries ) | ranges::views::transform([&](const QString &code) { return SerializeString(code.toUtf8()); }) | ranges::to_vector; const auto serializedCountries = SerializeArray(context, countries); context.nesting.pop_back(); const auto additionalPrize = data.additionalPrize.toUtf8(); pushBare("giveaway_information", SerializeObject(context, { { "quantity", NumberToString(data.quantity) }, { "months", NumberToString(data.months) }, { "until_date", SerializeDate(data.untilDate) }, { "channels", serialized }, { "countries", serializedCountries }, { "additional_prize", SerializeString(additionalPrize) }, { "stars", NumberToString(data.credits) }, { "is_only_new_subscribers", (!data.all) ? "true" : "false" }, })); }, [&](const GiveawayResults &data) { context.nesting.push_back(Context::kArray); const auto winners = ranges::views::all( data.winners ) | ranges::views::transform([&](PeerId id) { return NumberToString(id.value); }) | ranges::to_vector; const auto serialized = SerializeArray(context, winners); context.nesting.pop_back(); const auto additionalPrize = data.additionalPrize.toUtf8(); const auto peersCount = data.additionalPeersCount; pushBare("giveaway_results", SerializeObject(context, { { "channel", NumberToString(data.channel.bare) }, { "winners", serialized }, { "additional_prize", SerializeString(additionalPrize) }, { "until_date", SerializeDate(data.untilDate) }, { "launch_message_id", NumberToString(data.launchId) }, { "additional_peers_count", NumberToString(peersCount) }, { "winners_count", NumberToString(data.winnersCount) }, { "unclaimed_count", NumberToString(data.unclaimedCount) }, { "months", NumberToString(data.months) }, { "stars", NumberToString(data.credits) }, { "is_refunded", data.refunded ? "true" : "false" }, { "is_only_new_subscribers", (!data.all) ? "true" : "false" }, })); }, [&](const PaidMedia &data) { push("paid_stars_amount", data.stars); }, [&](const UnsupportedMedia &) { Expects(message.richMessage.has_value()); }, [](v::null_t) {}); if (message.richMessage) { pushBare( "rich_message", SerializeRichMessage(context, *message.richMessage)); } else { pushBare("text", SerializeText(context, message.text)); pushBare("text_entities", SerializeText(context, message.text, true)); } if (!message.inlineButtonRows.empty()) { const auto serializeRow = [&]( const std::vector &row) { context.nesting.push_back(Context::kArray); const auto buttons = ranges::views::all( row ) | ranges::views::transform([&]( const HistoryMessageMarkupButton &entry) { auto pairs = std::vector>(); pairs.push_back({ "type", SerializeString( HistoryMessageMarkupButton::TypeToString(entry)), }); if (!entry.text.isEmpty()) { pairs.push_back({ "text", SerializeString(entry.text.toUtf8()), }); } if (!entry.data.isEmpty()) { using Type = HistoryMessageMarkupButton::Type; const auto isCallback = (entry.type == Type::Callback) || (entry.type == Type::CallbackWithPassword); const auto data = isCallback ? entry.data.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) : entry.data; if (isCallback) { pairs.push_back({ "dataBase64", SerializeString(data), }); pairs.push_back({ "data", SerializeString({}) }); } else { pairs.push_back({ "data", SerializeString(data) }); } } if (!entry.forwardText.isEmpty()) { pairs.push_back({ "forward_text", SerializeString(entry.forwardText.toUtf8()), }); } if (entry.buttonId) { pairs.push_back({ "button_id", NumberToString(entry.buttonId), }); } return SerializeObject(context, pairs); }) | ranges::to_vector; context.nesting.pop_back(); return SerializeArray(context, buttons); }; context.nesting.push_back(Context::kArray); const auto rows = ranges::views::all( message.inlineButtonRows ) | ranges::views::transform(serializeRow) | ranges::to_vector; context.nesting.pop_back(); pushBare("inline_bot_buttons", SerializeArray(context, rows)); } if (!message.reactions.empty()) { const auto serializeReaction = [&](const Reaction &reaction) { context.nesting.push_back(Context::kObject); const auto guard = gsl::finally([&] { context.nesting.pop_back(); }); auto pairs = std::vector>(); pairs.push_back({ "type", SerializeString(Reaction::TypeToString(reaction)), }); pairs.push_back({ "count", NumberToString(reaction.count), }); switch (reaction.type) { case Reaction::Type::Emoji: pairs.push_back({ "emoji", SerializeString(reaction.emoji.toUtf8()), }); break; case Reaction::Type::CustomEmoji: pairs.push_back({ "document_id", SerializeString(reaction.documentId), }); break; } if (!reaction.recent.empty()) { context.nesting.push_back(Context::kArray); const auto recents = ranges::views::all( reaction.recent ) | ranges::views::transform([&]( const Reaction::Recent &recent) { context.nesting.push_back(Context::kArray); const auto guard = gsl::finally([&] { context.nesting.pop_back(); }); return SerializeObject(context, { { "from", wrapPeerName(recent.peerId) }, { "from_id", wrapPeerId(recent.peerId) }, { "date", SerializeDate(recent.date) }, }); }) | ranges::to_vector; pairs.push_back({"recent", SerializeArray(context, recents)}); context.nesting.pop_back(); } return SerializeObject(context, pairs); }; context.nesting.push_back(Context::kArray); const auto reactions = ranges::views::all( message.reactions ) | ranges::views::transform(serializeReaction) | ranges::to_vector; pushBare("reactions", SerializeArray(context, reactions)); context.nesting.pop_back(); } return serialized(); } } // namespace Result JsonWriter::start( const Settings &settings, const Environment &environment, Stats *stats) { Expects(_output == nullptr); Expects(settings.path.endsWith('/')); _settings = base::duplicate(settings); _environment = environment; _stats = stats; _output = fileWithRelativePath(mainFileRelativePath()); if (_settings.onlySinglePeer()) { return Result::Success(); } auto block = pushNesting(Context::kObject); block.append(prepareObjectItemStart("about")); block.append(SerializeString(_environment.aboutTelegram)); return _output->writeBlock(block); } QByteArray JsonWriter::pushNesting(Context::Type type) { Expects(_output != nullptr); _context.nesting.push_back(type); _currentNestingHadItem = false; return (type == Context::kObject ? "{" : "["); } QByteArray JsonWriter::prepareObjectItemStart(const QByteArray &key) { const auto guard = gsl::finally([&] { _currentNestingHadItem = true; }); return (_currentNestingHadItem ? ",\n" : "\n") + Indentation(_context) + SerializeString(key) + ": "; } QByteArray JsonWriter::prepareArrayItemStart() { const auto guard = gsl::finally([&] { _currentNestingHadItem = true; }); return (_currentNestingHadItem ? ",\n" : "\n") + Indentation(_context); } QByteArray JsonWriter::popNesting() { Expects(_output != nullptr); Expects(!_context.nesting.empty()); const auto type = Context::Type(_context.nesting.back()); _context.nesting.pop_back(); _currentNestingHadItem = true; return '\n' + Indentation(_context) + (type == Context::kObject ? '}' : ']'); } Result JsonWriter::writePersonal(const Data::PersonalInfo &data) { Expects(_output != nullptr); const auto &info = data.user.info; return _output->writeBlock( prepareObjectItemStart("personal_information") + SerializeObject(_context, { { "user_id", Data::NumberToString(data.user.bareId) }, { "first_name", SerializeString(info.firstName) }, { "last_name", SerializeString(info.lastName) }, { "phone_number", SerializeString(Data::FormatPhoneNumber(info.phoneNumber)) }, { "username", (!data.user.username.isEmpty() ? SerializeString(FormatUsername(data.user.username)) : QByteArray()) }, { "bio", (!data.bio.isEmpty() ? SerializeString(data.bio) : QByteArray()) }, })); } Result JsonWriter::writeUserpicsStart(const Data::UserpicsInfo &data) { Expects(_output != nullptr); auto block = prepareObjectItemStart("profile_pictures"); return _output->writeBlock(block + pushNesting(Context::kArray)); } Result JsonWriter::writeUserpicsSlice(const Data::UserpicsSlice &data) { Expects(_output != nullptr); Expects(!data.list.empty()); auto block = QByteArray(); for (const auto &userpic : data.list) { 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."); }(); block.append(prepareArrayItemStart()); block.append(SerializeObject(_context, { { "date", userpic.date ? SerializeDate(userpic.date) : QByteArray() }, { "date_unixtime", userpic.date ? SerializeDateRaw(userpic.date) : QByteArray() }, { "photo", SerializeString(path) }, })); } return _output->writeBlock(block); } Result JsonWriter::writeUserpicsEnd() { Expects(_output != nullptr); return _output->writeBlock(popNesting()); } Result JsonWriter::writeStoriesStart(const Data::StoriesInfo &data) { Expects(_output != nullptr); auto block = prepareObjectItemStart("stories"); return _output->writeBlock(block + pushNesting(Context::kArray)); } Result JsonWriter::writeStoriesSlice(const Data::StoriesSlice &data) { Expects(_output != nullptr); if (data.list.empty()) { return Result::Success(); } auto block = QByteArray(); for (const auto &story : data.list) { using SkipReason = Data::File::SkipReason; const auto &file = story.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 story path."); }(); block.append(prepareArrayItemStart()); block.append(SerializeObject(_context, { { "date", story.date ? SerializeDate(story.date) : QByteArray() }, { "date_unixtime", story.date ? SerializeDateRaw(story.date) : QByteArray() }, { "expires", story.expires ? SerializeDate(story.expires) : QByteArray() }, { "expires_unixtime", story.expires ? SerializeDateRaw(story.expires) : QByteArray() }, { "pinned", story.pinned ? "true" : "false" }, { "media", SerializeString(path) }, })); } return _output->writeBlock(block); } Result JsonWriter::writeStoriesEnd() { Expects(_output != nullptr); return _output->writeBlock(popNesting()); } Result JsonWriter::writeProfileMusicStart(const Data::ProfileMusicInfo &data) { Expects(_output != nullptr); auto block = prepareObjectItemStart("profile_music"); return _output->writeBlock(block + pushNesting(Context::kArray)); } Result JsonWriter::writeProfileMusicSlice(const Data::ProfileMusicSlice &data) { Expects(_output != nullptr); if (data.list.empty()) { return Result::Success(); } auto block = QByteArray(); for (const auto &message : data.list) { block.append(prepareArrayItemStart()); block.append(SerializeMessage(_context, message, {}, QString())); } return _output->writeBlock(block); } Result JsonWriter::writeProfileMusicEnd() { Expects(_output != nullptr); return _output->writeBlock(popNesting()); } Result JsonWriter::writeContactsList(const Data::ContactsList &data) { Expects(_output != nullptr); if (const auto result = writeSavedContacts(data); !result) { return result; } else if (const auto result = writeFrequentContacts(data); !result) { return result; } return Result::Success(); } Result JsonWriter::writeSavedContacts(const Data::ContactsList &data) { Expects(_output != nullptr); auto block = prepareObjectItemStart("contacts"); block.append(pushNesting(Context::kObject)); block.append(prepareObjectItemStart("about")); block.append(SerializeString(_environment.aboutContacts)); block.append(prepareObjectItemStart("list")); block.append(pushNesting(Context::kArray)); for (const auto index : Data::SortedContactsIndices(data)) { const auto &contact = data.list[index]; block.append(prepareArrayItemStart()); if (contact.firstName.isEmpty() && contact.lastName.isEmpty() && contact.phoneNumber.isEmpty()) { block.append(SerializeObject(_context, { { "date", SerializeDate(contact.date) }, { "date_unixtime", SerializeDateRaw(contact.date) }, })); } else { block.append(SerializeObject(_context, { { "user_id", (contact.userId ? Data::NumberToString(contact.userId.bare) : QByteArray()) }, { "first_name", SerializeString(contact.firstName) }, { "last_name", SerializeString(contact.lastName) }, { "phone_number", SerializeString( Data::FormatPhoneNumber(contact.phoneNumber)) }, { "date", SerializeDate(contact.date) }, { "date_unixtime", SerializeDateRaw(contact.date) }, })); } } block.append(popNesting()); return _output->writeBlock(block + popNesting()); } Result JsonWriter::writeFrequentContacts(const Data::ContactsList &data) { Expects(_output != nullptr); auto block = prepareObjectItemStart("frequent_contacts"); block.append(pushNesting(Context::kObject)); block.append(prepareObjectItemStart("about")); block.append(SerializeString(_environment.aboutFrequent)); block.append(prepareObjectItemStart("list")); block.append(pushNesting(Context::kArray)); const auto writeList = [&]( const std::vector &peers, Data::Utf8String category) { for (const auto &top : peers) { const auto type = [&] { if (const auto chat = top.peer.chat()) { return chat->username.isEmpty() ? (chat->isBroadcast ? "private_channel" : (chat->isSupergroup ? "private_supergroup" : "private_group")) : (chat->isBroadcast ? "public_channel" : "public_supergroup"); } return "user"; }(); block.append(prepareArrayItemStart()); block.append(SerializeObject(_context, { { "id", Data::NumberToString(Data::PeerToBareId(top.peer.id())) }, { "category", SerializeString(category) }, { "type", SerializeString(type) }, { "name", StringAllowNull(top.peer.name()) }, { "rating", Data::NumberToString(top.rating) }, })); } }; writeList(data.correspondents, "people"); writeList(data.inlineBots, "inline_bots"); writeList(data.phoneCalls, "calls"); block.append(popNesting()); return _output->writeBlock(block + popNesting()); } Result JsonWriter::writeSessionsList(const Data::SessionsList &data) { Expects(_output != nullptr); if (const auto result = writeSessions(data); !result) { return result; } else if (const auto result = writeWebSessions(data); !result) { return result; } return Result::Success(); } Result JsonWriter::writeOtherData(const Data::File &data) { Expects(_output != nullptr); Expects(data.skipReason == Data::File::SkipReason::None); Expects(!data.relativePath.isEmpty()); QFile f(pathWithRelativePath(data.relativePath)); if (!f.open(QIODevice::ReadOnly)) { return Result(Result::Type::FatalError, f.fileName()); } const auto content = f.readAll(); if (content.isEmpty()) { return Result::Success(); } auto error = QJsonParseError{ 0, QJsonParseError::NoError }; const auto document = QJsonDocument::fromJson(content, &error); if (error.error != QJsonParseError::NoError) { return Result(Result::Type::FatalError, f.fileName()); } auto block = prepareObjectItemStart("other_data"); Fn pushObject; Fn pushArray; Fn pushValue; pushObject = [&](const QJsonObject &data) { block.append(pushNesting(Context::kObject)); for (auto i = data.begin(); i != data.end(); ++i) { if ((*i).type() != QJsonValue::Undefined) { block.append(prepareObjectItemStart(i.key().toUtf8())); pushValue(*i); } } block.append(popNesting()); }; pushArray = [&](const QJsonArray &data) { block.append(pushNesting(Context::kArray)); for (auto i = data.begin(); i != data.end(); ++i) { if ((*i).type() != QJsonValue::Undefined) { block.append(prepareArrayItemStart()); pushValue(*i); } } block.append(popNesting()); }; pushValue = [&](const QJsonValue &data) { switch (data.type()) { case QJsonValue::Null: block.append("null"); return; case QJsonValue::Bool: block.append(data.toBool() ? "true" : "false"); return; case QJsonValue::Double: block.append(Data::NumberToString(data.toDouble())); return; case QJsonValue::String: block.append(SerializeString(data.toString().toUtf8())); return; case QJsonValue::Array: return pushArray(data.toArray()); case QJsonValue::Object: return pushObject(data.toObject()); } Unexpected("Type of json valuein JsonWriter::writeOtherData."); }; if (document.isObject()) { pushObject(document.object()); } else { pushArray(document.array()); } return _output->writeBlock(block); } Result JsonWriter::writeSessions(const Data::SessionsList &data) { Expects(_output != nullptr); auto block = prepareObjectItemStart("sessions"); block.append(pushNesting(Context::kObject)); block.append(prepareObjectItemStart("about")); block.append(SerializeString(_environment.aboutSessions)); block.append(prepareObjectItemStart("list")); block.append(pushNesting(Context::kArray)); for (const auto &session : data.list) { block.append(prepareArrayItemStart()); block.append(SerializeObject(_context, { { "last_active", SerializeDate(session.lastActive) }, { "last_active_unixtime", SerializeDateRaw(session.lastActive) }, { "last_ip", SerializeString(session.ip) }, { "last_country", SerializeString(session.country) }, { "last_region", SerializeString(session.region) }, { "application_name", StringAllowNull(session.applicationName) }, { "application_version", StringAllowEmpty(session.applicationVersion) }, { "device_model", SerializeString(session.deviceModel) }, { "platform", SerializeString(session.platform) }, { "system_version", SerializeString(session.systemVersion) }, { "created", SerializeDate(session.created) }, { "created_unixtime", SerializeDateRaw(session.created) }, })); } block.append(popNesting()); return _output->writeBlock(block + popNesting()); } Result JsonWriter::writeWebSessions(const Data::SessionsList &data) { Expects(_output != nullptr); auto block = prepareObjectItemStart("web_sessions"); block.append(pushNesting(Context::kObject)); block.append(prepareObjectItemStart("about")); block.append(SerializeString(_environment.aboutWebSessions)); block.append(prepareObjectItemStart("list")); block.append(pushNesting(Context::kArray)); for (const auto &session : data.webList) { block.append(prepareArrayItemStart()); block.append(SerializeObject(_context, { { "last_active", SerializeDate(session.lastActive) }, { "last_active_unixtime", SerializeDateRaw(session.lastActive) }, { "last_ip", SerializeString(session.ip) }, { "last_region", SerializeString(session.region) }, { "bot_username", StringAllowNull(session.botUsername) }, { "domain_name", StringAllowNull(session.domain) }, { "browser", SerializeString(session.browser) }, { "platform", SerializeString(session.platform) }, { "created", SerializeDate(session.created) }, { "created_unixtime", SerializeDateRaw(session.created) }, })); } block.append(popNesting()); return _output->writeBlock(block + popNesting()); } Result JsonWriter::writeDialogsStart(const Data::DialogsInfo &data) { return Result::Success(); } Result JsonWriter::writeDialogStart(const Data::DialogInfo &data) { Expects(_output != nullptr); if (!_settings.onlySinglePeer()) { const auto result = validateDialogsMode(data.isLeftChannel); if (!result) { return result; } } using Type = Data::DialogInfo::Type; const auto TypeString = [](Type type) { switch (type) { case Type::Unknown: return ""; case Type::Self: return "saved_messages"; case Type::Replies: return "replies"; case Type::VerifyCodes: return "verification_codes"; 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."); }; auto block = _settings.onlySinglePeer() ? QByteArray() : prepareArrayItemStart(); block.append(pushNesting(Context::kObject)); if (data.type != Type::Self && data.type != Type::Replies && data.type != Type::VerifyCodes) { block.append(prepareObjectItemStart("name") + StringAllowNull(data.name)); } block.append(prepareObjectItemStart("type") + StringAllowNull(TypeString(data.type))); block.append(prepareObjectItemStart("id") + Data::NumberToString(Data::PeerToBareId(data.peerId))); block.append(prepareObjectItemStart("messages")); block.append(pushNesting(Context::kArray)); return _output->writeBlock(block); } Result JsonWriter::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 ? "left_chats" : "chats", (isLeftChannel ? _environment.aboutLeftChats : _environment.aboutChats)); } Result JsonWriter::writeDialogSlice(const Data::MessagesSlice &data) { Expects(_output != nullptr); auto block = QByteArray(); for (const auto &message : data.list) { if (Data::SkipMessageByDate(message, _settings)) { continue; } block.append(prepareArrayItemStart() + SerializeMessage( _context, message, data.peers, _environment.internalLinksDomain)); } return block.isEmpty() ? Result::Success() : _output->writeBlock(block); } Result JsonWriter::writeDialogEnd() { Expects(_output != nullptr); auto block = popNesting(); return _output->writeBlock(block + popNesting()); } Result JsonWriter::writeDialogsEnd() { if (_settings.onlySinglePeer()) { return Result::Success(); } return writeChatsEnd(); } Result JsonWriter::writeChatsStart( const QByteArray &listName, const QByteArray &about) { Expects(_output != nullptr); auto block = prepareObjectItemStart(listName); block.append(pushNesting(Context::kObject)); block.append(prepareObjectItemStart("about")); block.append(SerializeString(about)); block.append(prepareObjectItemStart("list")); return _output->writeBlock(block + pushNesting(Context::kArray)); } Result JsonWriter::writeChatsEnd() { Expects(_output != nullptr); auto block = popNesting(); return _output->writeBlock(block + popNesting()); } Result JsonWriter::finish() { Expects(_output != nullptr); if (_settings.onlySinglePeer()) { Assert(_context.nesting.empty()); return Result::Success(); } auto block = popNesting(); Assert(_context.nesting.empty()); return _output->writeBlock(block); } QString JsonWriter::mainFilePath() { return pathWithRelativePath(mainFileRelativePath()); } QString JsonWriter::mainFileRelativePath() const { return "result.json"; } QString JsonWriter::pathWithRelativePath(const QString &path) const { return _settings.path + path; } std::unique_ptr JsonWriter::fileWithRelativePath( const QString &path) const { return std::make_unique(pathWithRelativePath(path), _stats); } } // namespace Output } // namespace Export