tdesktop/Telegram/SourceFiles/iv/iv_prepare.cpp

1271 lines
37 KiB
C++

/*
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 "iv/iv_prepare.h"
#include "base/openssl_help.h"
#include "base/unixtime.h"
#include "iv/iv_data.h"
#include "lang/lang_keys.h"
#include "ui/image/image_prepare.h"
#include "ui/grouped_layout.h"
#include "styles/palette.h"
#include "styles/style_chat.h"
#include <QtCore/QSize>
namespace Iv {
namespace {
struct Attribute {
QByteArray name;
std::optional<QByteArray> value;
};
using Attributes = std::vector<Attribute>;
struct Photo {
uint64 id = 0;
int width = 0;
int height = 0;
QByteArray minithumbnail;
};
struct Document {
uint64 id = 0;
int width = 0;
int height = 0;
QByteArray minithumbnail;
};
template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
[[nodiscard]] QByteArray Number(T value) {
return QByteArray::number(value);
}
template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
[[nodiscard]] QByteArray Percent(T value) {
return Number(base::SafeRound(value * 10000.) / 100.);
};
[[nodiscard]] QByteArray Escape(QByteArray value) {
auto result = QByteArray();
result.reserve(value.size());
for (const auto &ch : value) {
switch (ch) {
case '&': result.append("&amp;"); break;
case '<': result.append("&lt;"); break;
case '>': result.append("&gt;"); break;
case '"': result.append("&quot;"); break;
case '\'': result.append("&apos;"); break;
default: result.append(ch); break;
}
}
return result;
}
[[nodiscard]] QByteArray Date(TimeId date) {
return Escape(langDateTimeFull(base::unixtime::parse(date)).toUtf8());
}
class Parser final {
public:
Parser(const Source &source, const Options &options);
[[nodiscard]] Prepared result();
private:
void process(const Source &source);
void process(const MTPPhoto &photo);
void process(const MTPDocument &document);
template <typename Inner>
[[nodiscard]] QByteArray list(const MTPVector<Inner> &data);
[[nodiscard]] QByteArray collage(
const QVector<MTPPageBlock> &list,
const std::vector<QSize> &dimensions,
int offset = 0);
[[nodiscard]] QByteArray slideshow(
const QVector<MTPPageBlock> &list,
QSize dimensions);
[[nodiscard]] QByteArray block(const MTPDpageBlockUnsupported &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockTitle &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockSubtitle &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockAuthorDate &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockHeader &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockSubheader &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockParagraph &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockPreformatted &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockFooter &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockDivider &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockAnchor &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockList &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockBlockquote &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockPullquote &data);
[[nodiscard]] QByteArray block(
const MTPDpageBlockPhoto &data,
const Ui::GroupMediaLayout &layout = {},
QSize outer = {});
[[nodiscard]] QByteArray block(
const MTPDpageBlockVideo &data,
const Ui::GroupMediaLayout &layout = {},
QSize outer = {});
[[nodiscard]] QByteArray block(const MTPDpageBlockCover &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockEmbed &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockEmbedPost &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockCollage &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockSlideshow &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockChannel &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockAudio &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockKicker &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockTable &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockOrderedList &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockDetails &data);
[[nodiscard]] QByteArray block(
const MTPDpageBlockRelatedArticles &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockMap &data);
[[nodiscard]] QByteArray block(const MTPDpageRelatedArticle &data);
[[nodiscard]] QByteArray block(const MTPDpageTableRow &data);
[[nodiscard]] QByteArray block(const MTPDpageTableCell &data);
[[nodiscard]] QByteArray block(const MTPDpageListItemText &data);
[[nodiscard]] QByteArray block(const MTPDpageListItemBlocks &data);
[[nodiscard]] QByteArray block(const MTPDpageListOrderedItemText &data);
[[nodiscard]] QByteArray block(
const MTPDpageListOrderedItemBlocks &data);
[[nodiscard]] QByteArray wrap(const QByteArray &content, int views);
[[nodiscard]] QByteArray tag(
const QByteArray &name,
const QByteArray &body = {});
[[nodiscard]] QByteArray tag(
const QByteArray &name,
const Attributes &attributes,
const QByteArray &body = {});
[[nodiscard]] QByteArray utf(const MTPstring &text);
[[nodiscard]] QByteArray utf(const tl::conditional<MTPstring> &text);
[[nodiscard]] QByteArray rich(const MTPRichText &text);
[[nodiscard]] QByteArray caption(const MTPPageCaption &caption);
[[nodiscard]] Photo parse(const MTPPhoto &photo);
[[nodiscard]] Document parse(const MTPDocument &document);
[[nodiscard]] Geo parse(const MTPGeoPoint &geo);
[[nodiscard]] Photo photoById(uint64 id);
[[nodiscard]] Document documentById(uint64 id);
[[nodiscard]] QByteArray photoFullUrl(const Photo &photo);
[[nodiscard]] QByteArray documentFullUrl(const Document &document);
[[nodiscard]] QByteArray embedUrl(const QByteArray &html);
[[nodiscard]] QByteArray mapUrl(
const Geo &geo,
int width,
int height,
int zoom);
[[nodiscard]] QByteArray resource(QByteArray id);
[[nodiscard]] std::vector<QSize> computeCollageDimensions(
const QVector<MTPPageBlock> &items);
[[nodiscard]] QSize computeSlideshowDimensions(
const QVector<MTPPageBlock> &items);
//const Options _options;
const QByteArray _fileOriginPostfix;
base::flat_set<QByteArray> _resources;
Prepared _result;
base::flat_map<uint64, Photo> _photosById;
base::flat_map<uint64, Document> _documentsById;
};
[[nodiscard]] bool IsVoidElement(const QByteArray &name) {
// Thanks https://developer.mozilla.org/en-US/docs/Glossary/Void_element
static const auto voids = base::flat_set<QByteArray>{
"area"_q,
"base"_q,
"br"_q,
"col"_q,
"embed"_q,
"hr"_q,
"img"_q,
"input"_q,
"link"_q,
"meta"_q,
"source"_q,
"track"_q,
"wbr"_q,
};
return voids.contains(name);
}
[[nodiscard]] QByteArray ArrowSvg(bool left) {
const auto rotate = QByteArray(left ? "180" : "0");
return R"(
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M14.9972363,18 L9.13865768,12.1414214 C9.06055283,12.0633165 9.06055283,11.9366835 9.13865768,11.8585786 L14.9972363,6 L14.9972363,6"
transform="translate(11.997236, 12) scale(-1, -1) rotate()" + rotate + ") translate(-11.997236, -12)" + R"(">
</path>
</svg>)";
}
Parser::Parser(const Source &source, const Options &options)
: /*_options(options)
, */_fileOriginPostfix('/' + Number(source.pageId)) {
process(source);
_result.pageId = source.pageId;
_result.name = source.name;
_result.rtl = source.page.data().is_rtl();
const auto views = source.page.data().vviews().value_or_empty();
const auto content = list(source.page.data().vblocks());
_result.content = wrap(content, views);
}
Prepared Parser::result() {
return _result;
}
void Parser::process(const Source &source) {
const auto &data = source.page.data();
for (const auto &photo : data.vphotos().v) {
process(photo);
}
for (const auto &document : data.vdocuments().v) {
process(document);
}
if (source.webpagePhoto) {
process(*source.webpagePhoto);
}
if (source.webpageDocument) {
process(*source.webpageDocument);
}
}
void Parser::process(const MTPPhoto &photo) {
_photosById.emplace(
photo.match([](const auto &data) { return data.vid().v; }),
parse(photo));
}
void Parser::process(const MTPDocument &document) {
_documentsById.emplace(
document.match([](const auto &data) { return data.vid().v; }),
parse(document));
}
template <typename Inner>
QByteArray Parser::list(const MTPVector<Inner> &data) {
auto result = QByteArrayList();
result.reserve(data.v.size());
for (const auto &item : data.v) {
result.append(item.match([&](const auto &data) {
return block(data);
}));
}
return result.join(QByteArray());
}
QByteArray Parser::collage(
const QVector<MTPPageBlock> &list,
const std::vector<QSize> &dimensions,
int offset) {
Expects(list.size() == dimensions.size());
constexpr auto kPerCollage = 10;
const auto last = (offset + kPerCollage >= int(dimensions.size()));
auto result = QByteArray();
auto slice = ((offset > 0) || (dimensions.size() > kPerCollage))
? (dimensions
| ranges::views::drop(offset)
| ranges::views::take(kPerCollage)
| ranges::to_vector)
: dimensions;
const auto layout = Ui::LayoutMediaGroup(
slice,
st::historyGroupWidthMax,
st::historyGroupWidthMin,
st::historyGroupSkip);
auto size = QSize();
for (const auto &part : layout) {
const auto &rect = part.geometry;
size = QSize(
std::max(size.width(), rect.x() + rect.width()),
std::max(size.height(), rect.y() + rect.height()));
}
for (auto i = 0, count = int(layout.size()); i != count; ++i) {
const auto &part = layout[i];
list[offset + i].match([&](const MTPDpageBlockPhoto &data) {
result += block(data, part, size);
}, [&](const MTPDpageBlockVideo &data) {
result += block(data, part, size);
}, [](const auto &) {
Unexpected("Block type in collage layout.");
});
}
const auto aspectHeight = size.height() / float64(size.width());
const auto aspectSkip = st::historyGroupSkip / float64(size.width());
auto wrapped = tag("figure", {
{ "class", "collage" },
{
"style",
("padding-top: " + Percent(aspectHeight) + "%; "
+ "margin-bottom: " + Percent(last ? 0 : aspectSkip) + "%;")
},
}, result);
if (offset + kPerCollage < int(dimensions.size())) {
wrapped += collage(list, dimensions, offset + kPerCollage);
}
return wrapped;
}
QByteArray Parser::slideshow(
const QVector<MTPPageBlock> &list,
QSize dimensions) {
auto result = QByteArray();
for (auto i = 0, count = int(list.size()); i != count; ++i) {
list[i].match([&](const MTPDpageBlockPhoto &data) {
result += block(data, {}, dimensions);
}, [&](const MTPDpageBlockVideo &data) {
result += block(data, {}, dimensions);
}, [](const auto &) {
Unexpected("Block type in collage layout.");
});
}
auto inputs = QByteArrayList();
for (auto i = 0; i != int(list.size()); ++i) {
auto attributes = Attributes{
{ "type", "radio" },
{ "name", "s" },
{ "value", Number(i) },
{ "onchange", "return IV.slideshowSlide(this);" },
};
if (!i) {
attributes.push_back({ "checked", std::nullopt });
}
inputs.append(tag("label", tag("input", attributes, tag("i"))));
}
const auto form = tag(
"form",
{ { "class", "slideshow-buttons" } },
tag("fieldset", inputs.join(QByteArray())));
const auto navigation = tag("a", {
{ "class", "slideshow-prev" },
{ "onclick", "IV.slideshowSlide(this, -1);" },
}, ArrowSvg(true)) + tag("a", {
{ "class", "slideshow-next" },
{ "onclick", "IV.slideshowSlide(this, 1);" },
}, ArrowSvg(false));
auto wrapStyle = "padding-top: calc(min("
+ Percent(dimensions.height() / float64(dimensions.width()))
+ "%, 480px));";
result = form + tag("figure", {
{ "class", "slideshow" },
}, result) + navigation;
return tag("figure", {
{ "class", "slideshow-wrap" },
{ "style", wrapStyle },
}, result);
}
QByteArray Parser::block(const MTPDpageBlockUnsupported &data) {
return QByteArray();
}
QByteArray Parser::block(const MTPDpageBlockTitle &data) {
return tag("h1", { { "class", "title" } }, rich(data.vtext()));
}
QByteArray Parser::block(const MTPDpageBlockSubtitle &data) {
return tag("h2", { { "class", "subtitle" } }, rich(data.vtext()));
}
QByteArray Parser::block(const MTPDpageBlockAuthorDate &data) {
auto inner = rich(data.vauthor());
if (const auto date = data.vpublished_date().v) {
inner += " \xE2\x80\xA2 " + tag("time", Date(date));
}
return tag("address", inner);
}
QByteArray Parser::block(const MTPDpageBlockHeader &data) {
return tag("h3", { { "class", "header" } }, rich(data.vtext()));
}
QByteArray Parser::block(const MTPDpageBlockSubheader &data) {
return tag("h4", { { "class", "subheader" } }, rich(data.vtext()));
}
QByteArray Parser::block(const MTPDpageBlockParagraph &data) {
return tag("p", rich(data.vtext()));
}
QByteArray Parser::block(const MTPDpageBlockPreformatted &data) {
auto list = Attributes();
const auto language = utf(data.vlanguage());
if (!language.isEmpty()) {
list.push_back({ "data-language", language });
list.push_back({ "class", "lang-" + language });
_result.hasCode = true;
}
return tag("pre", list, rich(data.vtext()));
}
QByteArray Parser::block(const MTPDpageBlockFooter &data) {
return tag("footer", { { "class", "footer" } }, rich(data.vtext()));
}
QByteArray Parser::block(const MTPDpageBlockDivider &data) {
return tag("hr", { { "class", "divider" } });
}
QByteArray Parser::block(const MTPDpageBlockAnchor &data) {
return tag("a", { { "name", utf(data.vname()) } });
}
QByteArray Parser::block(const MTPDpageBlockList &data) {
return tag("ul", list(data.vitems()));
}
QByteArray Parser::block(const MTPDpageBlockBlockquote &data) {
const auto caption = rich(data.vcaption());
const auto cite = caption.isEmpty()
? QByteArray()
: tag("cite", caption);
return tag("blockquote", rich(data.vtext()) + cite);
}
QByteArray Parser::block(const MTPDpageBlockPullquote &data) {
const auto caption = rich(data.vcaption());
const auto cite = caption.isEmpty()
? QByteArray()
: tag("cite", caption);
return tag(
"div",
{ { "class", "pullquote" } },
rich(data.vtext()) + cite);
}
QByteArray Parser::block(
const MTPDpageBlockPhoto &data,
const Ui::GroupMediaLayout &layout,
QSize outer) {
const auto collage = !layout.geometry.isEmpty();
const auto slideshow = !collage && !outer.isEmpty();
const auto photo = photoById(data.vphoto_id().v);
if (!photo.id) {
return "Photo not found.";
}
const auto src = photoFullUrl(photo);
auto wrapStyle = QByteArray();
if (collage) {
const auto wcoef = 1. / outer.width();
const auto hcoef = 1. / outer.height();
wrapStyle += "left: " + Percent(layout.geometry.x() * wcoef) + "%; "
+ "top: " + Percent(layout.geometry.y() * hcoef) + "%; "
+ "width: " + Percent(layout.geometry.width() * wcoef) + "%; "
+ "height: " + Percent(layout.geometry.height() * hcoef) + "%";
} else if (!slideshow && photo.width) {
wrapStyle += "max-width:" + Number(photo.width) + "px";
}
const auto dimension = collage
? (layout.geometry.height() / float64(layout.geometry.width()))
: (photo.width && photo.height)
? (photo.height / float64(photo.width))
: (3 / 4.);
const auto paddingTop = collage
? Percent(dimension) + "%"
: "calc(min(480px, " + Percent(dimension) + "%))";
const auto style = "background-image:url('" + src + "');"
"padding-top: " + paddingTop + ";";
auto inner = tag("div", {
{ "class", "photo" },
{ "style", style } });
const auto minithumb = Images::ExpandInlineBytes(photo.minithumbnail);
if (!minithumb.isEmpty()) {
const auto image = Images::Read({ .content = minithumb });
inner = tag("div", {
{ "class", "photo-bg" },
{ "style", "background-image:url('data:image/jpeg;base64,"
+ minithumb.toBase64()
+ "');" },
}) + inner;
}
auto attributes = Attributes{
{ "class", "photo-wrap" },
{ "style", wrapStyle }
};
auto result = tag("div", attributes, inner);
const auto href = data.vurl() ? utf(*data.vurl()) : photoFullUrl(photo);
const auto id = Number(photo.id);
result = tag("a", {
{ "href", href },
{ "oncontextmenu", data.vurl() ? QByteArray() : "return false;" },
{ "data-context", data.vurl() ? QByteArray() : "viewer-photo" + id },
}, result);
if (!slideshow) {
result += caption(data.vcaption());
if (!collage) {
result = tag("div", { { "class", "media-outer" } }, result);
}
}
return result;
}
QByteArray Parser::block(
const MTPDpageBlockVideo &data,
const Ui::GroupMediaLayout &layout,
QSize outer) {
const auto collage = !layout.geometry.isEmpty();
const auto slideshow = !collage && !outer.isEmpty();
const auto collageSmall = collage
&& (layout.geometry.width() < outer.width());
const auto video = documentById(data.vvideo_id().v);
if (!video.id) {
return "Video not found.";
}
auto inner = tag("div", {
{ "class", "video" },
{ "data-src", documentFullUrl(video) },
{ "data-autoplay", data.is_autoplay() ? "1" : "0" },
{ "data-loop", data.is_loop() ? "1" : "0" },
{ "data-small", collageSmall ? "1" : "0" },
});
const auto minithumb = Images::ExpandInlineBytes(video.minithumbnail);
if (!minithumb.isEmpty()) {
const auto image = Images::Read({ .content = minithumb });
inner = tag("div", {
{ "class", "video-bg" },
{ "style", "background-image:url('data:image/jpeg;base64,"
+ minithumb.toBase64()
+ "');" },
}) + inner;
}
auto wrapStyle = QByteArray();
if (collage) {
const auto wcoef = 1. / outer.width();
const auto hcoef = 1. / outer.height();
wrapStyle += "left: " + Percent(layout.geometry.x() * wcoef) + "%; "
+ "top: " + Percent(layout.geometry.y() * hcoef) + "%; "
+ "width: " + Percent(layout.geometry.width() * wcoef) + "%; "
+ "height: " + Percent(layout.geometry.height() * hcoef) + "%; ";
} else {
const auto dimension = (video.width && video.height)
? (video.height / float64(video.width))
: (3 / 4.);
wrapStyle += "padding-top: calc(min(480px, "
+ Percent(dimension)
+ "%));";
}
auto attributes = Attributes{
{ "class", "video-wrap" },
{ "style", wrapStyle },
};
auto result = tag("div", attributes, inner);
if (data.is_autoplay() || collageSmall) {
const auto id = Number(video.id);
const auto href = resource("video" + id);
result = tag("a", {
{ "href", href },
{ "oncontextmenu", "return false;" },
{ "data-context", "viewer-video" + id },
}, result);
}
if (!slideshow) {
result += caption(data.vcaption());
if (!collage) {
result = tag("div", { { "class", "media-outer" } }, result);
}
}
return result;
}
QByteArray Parser::block(const MTPDpageBlockCover &data) {
return tag("figure", data.vcover().match([&](const auto &data) {
return block(data);
}));
}
QByteArray Parser::block(const MTPDpageBlockEmbed &data) {
_result.hasEmbeds = true;
auto eclass = data.is_full_width() ? QByteArray() : "nowide";
auto width = QByteArray();
auto height = QByteArray();
auto iframeWidth = QByteArray();
auto iframeHeight = QByteArray();
const auto autosize = !data.vw();
if (autosize) {
iframeWidth = "100%";
eclass = "nowide";
} else if (data.is_full_width() || !data.vw()->v) {
width = "100%";
height = Number(data.vh()->v) + "px";
iframeWidth = "100%";
iframeHeight = height;
} else {
width = Number(data.vw()->v) + "px";
height = Percent(data.vh()->v / float64(data.vw()->v)) + "%";
}
auto attributes = Attributes();
if (autosize) {
attributes.push_back({ "class", "autosize" });
}
attributes.push_back({ "width", iframeWidth });
attributes.push_back({ "height", iframeHeight });
if (const auto url = data.vurl()) {
if (!autosize) {
attributes.push_back({ "src", utf(url) });
} else {
attributes.push_back({ "srcdoc", utf(url) });
}
} else if (const auto html = data.vhtml()) {
attributes.push_back({ "src", embedUrl(html->v) });
}
if (!data.is_allow_scrolling()) {
attributes.push_back({ "scrolling", "no" });
}
attributes.push_back({ "frameborder", "0" });
attributes.push_back({ "allowtransparency", "true" });
attributes.push_back({ "allowfullscreen", "true" });
auto result = tag("iframe", attributes);
if (!autosize) {
result = tag("div", {
{ "class", "iframe-wrap" },
{ "style", "width:" + width },
}, tag("div", {
{ "style", "padding-bottom: " + height },
}, result));
}
result += caption(data.vcaption());
return tag("figure", { { "class", eclass } }, result);
}
QByteArray Parser::block(const MTPDpageBlockEmbedPost &data) {
auto result = QByteArray();
if (!data.vblocks().v.isEmpty()) {
auto address = QByteArray();
const auto photo = photoById(data.vauthor_photo_id().v);
if (photo.id) {
const auto src = photoFullUrl(photo);
address += tag(
"figure",
{ { "style", "background-image:url('" + src + "')" } });
}
address += tag(
"a",
{ { "rel", "author" }, { "onclick", "return false;" } },
utf(data.vauthor()));
if (const auto date = data.vdate().v) {
const auto parsed = base::unixtime::parse(date);
address += tag("time", Date(date));
}
const auto inner = tag("address", address) + list(data.vblocks());
result = tag("blockquote", { { "class", "embed-post" } }, inner);
} else {
const auto url = utf(data.vurl());
const auto inner = tag("strong", utf(data.vauthor()))
+ tag(
"small",
tag("a", { { "href", url } }, url));
result = tag("section", { { "class", "embed-post" } }, inner);
}
result += caption(data.vcaption());
return tag("figure", result);
}
QByteArray Parser::block(const MTPDpageBlockCollage &data) {
const auto &items = data.vitems().v;
const auto dimensions = computeCollageDimensions(items);
if (dimensions.empty()) {
return tag(
"figure",
tag("figure", list(data.vitems())) + caption(data.vcaption()));
}
return tag(
"figure",
{ { "class", "collage-wrap" } },
collage(items, dimensions) + caption(data.vcaption()));
}
QByteArray Parser::block(const MTPDpageBlockSlideshow &data) {
const auto &items = data.vitems().v;
const auto dimensions = computeSlideshowDimensions(items);
if (dimensions.isEmpty()) {
return list(data.vitems());
}
const auto result = slideshow(items, dimensions);
return tag("figure", result + caption(data.vcaption()));
}
QByteArray Parser::block(const MTPDpageBlockChannel &data) {
auto name = QByteArray();
auto username = QByteArray();
auto id = data.vchannel().match([](const auto &data) {
return Number(data.vid().v);
});
data.vchannel().match([&](const MTPDchannel &data) {
if (const auto has = data.vusername()) {
username = utf(*has);
}
name = utf(data.vtitle());
}, [&](const MTPDchat &data) {
name = utf(data.vtitle());
}, [](const auto &) {
});
auto result = tag(
"div",
{ { "class", "join" }, { "data-context", "join_link" + id } },
tag("span")
) + tag("h4", name);
const auto link = username.isEmpty()
? "javascript:alert('Channel Link');"
: "https://t.me/" + username;
result = tag(
"a",
{ { "href", link }, { "data-context", "channel" + id } },
result);
_result.channelIds.emplace(id);
return tag("section", {
{ "class", "channel joined" },
{ "data-context", "channel" + id },
}, result);
}
QByteArray Parser::block(const MTPDpageBlockAudio &data) {
const auto audio = documentById(data.vaudio_id().v);
if (!audio.id) {
return "Audio not found.";
}
const auto src = documentFullUrl(audio);
return tag("figure", tag("audio", {
{ "src", src },
{ "oncontextmenu", "return false;" },
{ "controls", std::nullopt },
}) + caption(data.vcaption()));
}
QByteArray Parser::block(const MTPDpageBlockKicker &data) {
return tag("h5", { { "class", "kicker" } }, rich(data.vtext()));
}
QByteArray Parser::block(const MTPDpageBlockTable &data) {
auto classes = QByteArrayList();
if (data.is_bordered()) {
classes.push_back("bordered");
}
if (data.is_striped()) {
classes.push_back("striped");
}
auto attibutes = Attributes();
if (!classes.isEmpty()) {
attibutes.push_back({ "class", classes.join(" ") });
}
auto title = rich(data.vtitle());
if (!title.isEmpty()) {
title = tag("caption", title);
}
auto result = tag("table", attibutes, title + list(data.vrows()));
result = tag("figure", { { "class", "table" } }, result);
result = tag("figure", { { "class", "table-wrap" } }, result);
return tag("figure", result);
}
QByteArray Parser::block(const MTPDpageBlockOrderedList &data) {
return tag("ol", list(data.vitems()));
}
QByteArray Parser::block(const MTPDpageBlockDetails &data) {
auto attributes = Attributes();
if (data.is_open()) {
attributes.push_back({ "open", std::nullopt });
}
return tag(
"details",
attributes,
tag("summary", rich(data.vtitle())) + list(data.vblocks()));
}
QByteArray Parser::block(const MTPDpageBlockRelatedArticles &data) {
const auto result = list(data.varticles());
if (result.isEmpty()) {
return QByteArray();
}
auto title = rich(data.vtitle());
if (!title.isEmpty()) {
title = tag("h4", { { "class", "related-title" } }, title);
}
return tag("section", { { "class", "related" } }, title + result);
}
QByteArray Parser::block(const MTPDpageBlockMap &data) {
const auto geo = parse(data.vgeo());
if (!geo.access) {
return "Map not found.";
}
const auto width = 650;
const auto height = std::min(450, (data.vh().v * width / data.vw().v));
return tag("figure", tag("img", {
{ "src", mapUrl(geo, width, height, data.vzoom().v) },
}) + caption(data.vcaption()));
}
QByteArray Parser::block(const MTPDpageRelatedArticle &data) {
auto result = QByteArray();
const auto photo = photoById(data.vphoto_id().value_or_empty());
if (photo.id) {
const auto src = photoFullUrl(photo);
result += tag("i", {
{ "class", "related-link-thumb" },
{ "style", "background-image:url('" + src + "')" },
});
}
const auto title = data.vtitle();
const auto description = data.vdescription();
const auto author = data.vauthor();
const auto published = data.vpublished_date();
if (title || description || author || published) {
auto inner = QByteArray();
if (title) {
inner += tag(
"span",
{ { "class", "related-link-title" } },
utf(*title));
}
if (description) {
inner += tag(
"span",
{ { "class", "related-link-desc" } },
utf(*description));
}
if (author || published) {
inner += tag(
"span",
{ { "class", "related-link-source" } },
((author ? utf(*author) : QByteArray())
+ ((author && published) ? ", " : QByteArray())
+ (published ? Date(published->v) : QByteArray())));
}
result += tag("span", {
{ "class", "related-link-content" },
}, inner);
}
const auto webpageId = data.vwebpage_id().v;
const auto context = webpageId
? ("webpage" + Number(webpageId))
: QByteArray();
return tag("a", {
{ "class", "related-link" },
{ "href", utf(data.vurl()) },
{ "data-context", context },
}, result);
}
QByteArray Parser::block(const MTPDpageTableRow &data) {
return tag("tr", list(data.vcells()));
}
QByteArray Parser::block(const MTPDpageTableCell &data) {
const auto text = data.vtext() ? rich(*data.vtext()) : QByteArray();
auto style = QByteArray();
if (data.is_align_right()) {
style += "text-align:right;";
} else if (data.is_align_center()) {
style += "text-align:center;";
} else {
style += "text-align:left;";
}
if (data.is_valign_bottom()) {
style += "vertical-align:bottom;";
} else if (data.is_valign_middle()) {
style += "vertical-align:middle;";
} else {
style += "vertical-align:top;";
}
auto attributes = Attributes{ { "style", style } };
if (const auto cs = data.vcolspan()) {
attributes.push_back({ "colspan", Number(cs->v) });
}
if (const auto rs = data.vrowspan()) {
attributes.push_back({ "rowspan", Number(rs->v) });
}
return tag(data.is_header() ? "th" : "td", attributes, text);
}
QByteArray Parser::block(const MTPDpageListItemText &data) {
return tag("li", rich(data.vtext()));
}
QByteArray Parser::block(const MTPDpageListItemBlocks &data) {
return tag("li", list(data.vblocks()));
}
QByteArray Parser::block(const MTPDpageListOrderedItemText &data) {
return tag(
"li",
{ { "value", utf(data.vnum()) } },
rich(data.vtext()));
}
QByteArray Parser::block(const MTPDpageListOrderedItemBlocks &data) {
return tag(
"li",
{ { "value", utf(data.vnum()) } },
list(data.vblocks()));
}
QByteArray Parser::utf(const MTPstring &text) {
return Escape(text.v);
}
QByteArray Parser::utf(const tl::conditional<MTPstring> &text) {
return text ? utf(*text) : QByteArray();
}
QByteArray Parser::wrap(const QByteArray &content, int views) {
const auto sep = " \xE2\x80\xA2 ";
const auto viewsText = views
? (tr::lng_stories_views(tr::now, lt_count_decimal, views) + sep)
: QString();
return R"(
<div class="page-slide">
<article>)"_q + content + R"(</article>
</div>
<div class="page-footer">
<div class="content">
)"_q
+ viewsText.toUtf8()
+ R"(<a class="wrong" data-context="report-iv">)"_q
+ tr::lng_iv_wrong_layout(tr::now).toUtf8()
+ R"(</a>
</div>
</div>)"_q;
}
QByteArray Parser::tag(
const QByteArray &name,
const QByteArray &body) {
return tag(name, {}, body);
}
QByteArray Parser::tag(
const QByteArray &name,
const Attributes &attributes,
const QByteArray &body) {
auto list = QByteArrayList();
list.reserve(attributes.size());
for (auto &[name, value] : attributes) {
list.push_back(' ' + name + (value ? "=\"" + *value + "\"" : ""));
}
const auto serialized = list.join(QByteArray());
return (IsVoidElement(name) && body.isEmpty())
? ('<' + name + serialized + " />")
: ('<' + name + serialized + '>' + body + "</" + name + '>');
}
QByteArray Parser::rich(const MTPRichText &text) {
return text.match([&](const MTPDtextEmpty &data) {
return QByteArray();
}, [&](const MTPDtextPlain &data) {
struct Replacement {
QByteArray from;
QByteArray to;
};
const auto replacements = std::vector<Replacement>{
{ "\xE2\x81\xA6", "<span dir=\"ltr\">" },
{ "\xE2\x81\xA7", "<span dir=\"rtl\">" },
{ "\xE2\x81\xA8", "<span dir=\"auto\">" },
{ "\xE2\x81\xA9", "</span>" },
};
auto text = utf(data.vtext());
for (const auto &[from, to] : replacements) {
text.replace(from, to);
}
return text;
}, [&](const MTPDtextConcat &data) {
const auto &list = data.vtexts().v;
auto result = QByteArrayList();
result.reserve(list.size());
for (const auto &item : list) {
result.append(rich(item));
}
return result.join(QByteArray());
}, [&](const MTPDtextImage &data) {
const auto image = documentById(data.vdocument_id().v);
if (!image.id) {
return "Image not found."_q;
}
auto attributes = Attributes{
{ "class", "pic" },
{ "src", documentFullUrl(image) },
};
if (const auto width = data.vw().v) {
attributes.push_back({ "width", Number(width) });
}
if (const auto height = data.vh().v) {
attributes.push_back({ "height", Number(height) });
}
return tag("img", attributes);
}, [&](const MTPDtextBold &data) {
return tag("b", rich(data.vtext()));
}, [&](const MTPDtextItalic &data) {
return tag("i", rich(data.vtext()));
}, [&](const MTPDtextUnderline &data) {
return tag("u", rich(data.vtext()));
}, [&](const MTPDtextStrike &data) {
return tag("s", rich(data.vtext()));
}, [&](const MTPDtextFixed &data) {
return tag("code", rich(data.vtext()));
}, [&](const MTPDtextUrl &data) {
const auto webpageId = data.vwebpage_id().v;
const auto context = webpageId
? ("webpage" + Number(webpageId))
: QByteArray();
return tag("a", {
{ "href", utf(data.vurl()) },
{ "class", webpageId ? "internal-iv-link" : "" },
{ "data-context", context },
}, rich(data.vtext()));
}, [&](const MTPDtextEmail &data) {
return tag("a", {
{ "href", "mailto:" + utf(data.vemail()) },
}, rich(data.vtext()));
}, [&](const MTPDtextSubscript &data) {
return tag("sub", rich(data.vtext()));
}, [&](const MTPDtextSuperscript &data) {
return tag("sup", rich(data.vtext()));
}, [&](const MTPDtextMarked &data) {
return tag("mark", rich(data.vtext()));
}, [&](const MTPDtextPhone &data) {
return tag("a", {
{ "href", "tel:" + utf(data.vphone()) },
}, rich(data.vtext()));
}, [&](const MTPDtextAnchor &data) {
const auto inner = rich(data.vtext());
const auto name = utf(data.vname());
return inner.isEmpty()
? tag("a", { { "name", name } })
: tag(
"span",
{ { "class", "reference" } },
tag("a", { { "name", name } }) + inner);
});
}
QByteArray Parser::caption(const MTPPageCaption &caption) {
auto text = rich(caption.data().vtext());
const auto credit = rich(caption.data().vcredit());
if (!credit.isEmpty()) {
text += tag("cite", credit);
} else if (text.isEmpty()) {
return QByteArray();
}
return tag("figcaption", text);
}
Photo Parser::parse(const MTPPhoto &photo) {
auto result = Photo{
.id = photo.match([&](const auto &d) { return d.vid().v; }),
};
auto sizes = base::flat_map<QByteArray, QSize>();
photo.match([](const MTPDphotoEmpty &) {
}, [&](const MTPDphoto &data) {
for (const auto &size : data.vsizes().v) {
size.match([&](const MTPDphotoSizeEmpty &data) {
}, [&](const MTPDphotoSize &data) {
sizes.emplace(
data.vtype().v,
QSize(data.vw().v, data.vh().v));
}, [&](const MTPDphotoCachedSize &data) {
sizes.emplace(
data.vtype().v,
QSize(data.vw().v, data.vh().v));
}, [&](const MTPDphotoStrippedSize &data) {
result.minithumbnail = data.vbytes().v;
}, [&](const MTPDphotoSizeProgressive &data) {
sizes.emplace(
data.vtype().v,
QSize(data.vw().v, data.vh().v));
}, [&](const MTPDphotoPathSize &data) {
});
}
});
for (const auto attempt : { "y", "x", "w" }) {
const auto i = sizes.find(QByteArray(attempt));
if (i != end(sizes)) {
result.width = i->second.width();
result.height = i->second.height();
break;
}
}
return result;
}
Document Parser::parse(const MTPDocument &document) {
auto result = Document{
.id = document.match([&](const auto &d) { return d.vid().v; }),
};
document.match([](const MTPDdocumentEmpty &) {
}, [&](const MTPDdocument &data) {
for (const auto &attribute : data.vattributes().v) {
attribute.match([&](const MTPDdocumentAttributeImageSize &data) {
result.width = data.vw().v;
result.height = data.vh().v;
}, [&](const MTPDdocumentAttributeVideo &data) {
result.width = data.vw().v;
result.height = data.vh().v;
}, [](const auto &) {});
}
if (const auto sizes = data.vthumbs()) {
for (const auto &size : sizes->v) {
size.match([&](const MTPDphotoStrippedSize &data) {
result.minithumbnail = data.vbytes().v;
}, [&](const auto &data) {
});
}
}
});
return result;
}
Geo Parser::parse(const MTPGeoPoint &geo) {
return geo.match([](const MTPDgeoPointEmpty &) {
return Geo();
}, [&](const MTPDgeoPoint &data) {
return Geo{
.lat = data.vlat().v,
.lon = data.vlong().v,
.access = data.vaccess_hash().v,
};
});
}
Photo Parser::photoById(uint64 id) {
const auto i = _photosById.find(id);
return (i != end(_photosById)) ? i->second : Photo();
}
Document Parser::documentById(uint64 id) {
const auto i = _documentsById.find(id);
return (i != end(_documentsById)) ? i->second : Document();
}
QByteArray Parser::photoFullUrl(const Photo &photo) {
return resource("photo/" + Number(photo.id) + _fileOriginPostfix);
}
QByteArray Parser::documentFullUrl(const Document &document) {
return resource("document/" + Number(document.id) + _fileOriginPostfix);
}
QByteArray Parser::embedUrl(const QByteArray &html) {
auto binary = std::array<uchar, SHA256_DIGEST_LENGTH>{};
SHA256(
reinterpret_cast<const unsigned char*>(html.data()),
html.size(),
binary.data());
const auto hex = [](uchar value) -> char {
return (value >= 10) ? ('a' + (value - 10)) : ('0' + value);
};
auto result = QByteArray();
result.reserve(binary.size() * 2);
for (const auto byte : binary) {
result.push_back(hex(byte / 16));
result.push_back(hex(byte % 16));
}
result += ".html";
_result.embeds.emplace(result, html);
return resource("html/" + result);
}
QByteArray Parser::mapUrl(const Geo &geo, int width, int height, int zoom) {
return resource("map/"
+ GeoPointId(geo) + "&"
+ Number(width) + ","
+ Number(height) + "&"
+ Number(zoom));
}
QByteArray Parser::resource(QByteArray id) {
return '/' + id;
}
std::vector<QSize> Parser::computeCollageDimensions(
const QVector<MTPPageBlock> &items) {
if (items.size() < 2) {
return {};
}
auto result = std::vector<QSize>(items.size());
for (auto i = 0, count = int(items.size()); i != count; ++i) {
items[i].match([&](const MTPDpageBlockPhoto &data) {
const auto photo = photoById(data.vphoto_id().v);
if (photo.id && photo.width > 0 && photo.height > 0) {
result[i] = QSize(photo.width, photo.height);
}
}, [&](const MTPDpageBlockVideo &data) {
const auto document = documentById(data.vvideo_id().v);
if (document.id && document.width > 0 && document.height > 0) {
result[i] = QSize(document.width, document.height);
}
}, [](const auto &) {});
if (result[i].isEmpty()) {
return {};
}
}
return result;
}
QSize Parser::computeSlideshowDimensions(
const QVector<MTPPageBlock> &items) {
if (items.size() < 2) {
return {};
}
auto result = QSize();
for (const auto &item : items) {
auto size = QSize();
item.match([&](const MTPDpageBlockPhoto &data) {
const auto photo = photoById(data.vphoto_id().v);
if (photo.id && photo.width > 0 && photo.height > 0) {
size = QSize(photo.width, photo.height);
}
}, [&](const MTPDpageBlockVideo &data) {
const auto document = documentById(data.vvideo_id().v);
if (document.id && document.width > 0 && document.height > 0) {
size = QSize(document.width, document.height);
}
}, [](const auto &) {});
if (size.isEmpty()) {
return {};
} else if (result.height() * size.width()
< result.width() * size.height()) {
result = size;
}
}
return result;
}
} // namespace
Prepared Prepare(const Source &source, const Options &options) {
auto parser = Parser(source, options);
return parser.result();
}
} // namespace Iv