Skip to content
Snippets Groups Projects
Utils.cpp 73.3 KiB
Newer Older
// SPDX-FileCopyrightText: Nheko Contributors
Nicolas Werner's avatar
Nicolas Werner committed
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include <QApplication>
#include <QBuffer>
#include <QComboBox>
#include <QCryptographicHash>
#include <QGuiApplication>
#include <QImageReader>
#include <QProcessEnvironment>
#include <QScreen>
#include <QStringBuilder>
Joe Donofry's avatar
Joe Donofry committed
#include <QTextBoundaryFinder>
Nicolas Werner's avatar
Nicolas Werner committed
#include <QWindow>
#include <QXmlStreamReader>

#include <array>
LordMZTE's avatar
LordMZTE committed
#include <cmath>
#include <mtx/responses/messages.hpp>
#include <unordered_set>
Nicolas Werner's avatar
Nicolas Werner committed
#include <variant>
#include <cmark.h>
#include "Cache_p.h"
#include "Config.h"
#include "Logging.h"
#include "UserSettingsPage.h"
#include "timeline/Permissions.h"
Nicolas Werner's avatar
Nicolas Werner committed
template<class T, class Event>
static DescInfo
createDescriptionInfo(const Event &event, const QString &localUser, const QString &displayName)
    const auto msg    = std::get<T>(event);
    const auto sender = QString::fromStdString(msg.sender);
    const auto username = displayName;
    const auto ts       = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
    auto body           = mtx::accessors::body(event);
    if (mtx::accessors::relations(event).reply_to())
        body = utils::stripReplyFromBody(body);

    return DescInfo{
      QString::fromStdString(msg.event_id),
      sender,
      utils::messageDescription<T>(username, QString::fromStdString(body), sender == localUser),
      utils::descriptiveTime(ts),
      msg.origin_server_ts,
      ts};
targetakhil's avatar
targetakhil committed
std::string
utils::stripReplyFromBody(const std::string &bodyi)
    QString body = QString::fromStdString(bodyi);
    if (body.startsWith(QLatin1String("> <"))) {
        auto segments = body.split('\n');
        while (!segments.isEmpty() && segments.begin()->startsWith('>'))
            segments.erase(segments.cbegin());
        if (!segments.empty() && segments.first().isEmpty())
            segments.erase(segments.cbegin());
    body.replace(QLatin1String("@room"), QString::fromUtf8("@\u2060room"));
    return body.toStdString();
targetakhil's avatar
targetakhil committed
std::string
utils::stripReplyFromFormattedBody(const std::string &formatted_bodyi)
    QString formatted_body = QString::fromStdString(formatted_bodyi);
    static QRegularExpression replyRegex(QStringLiteral("<mx-reply>.*</mx-reply>"),
                                         QRegularExpression::DotMatchesEverythingOption);
    formatted_body.remove(replyRegex);
    formatted_body.replace(QLatin1String("@room"), QString::fromUtf8("@\u2060room"));
    return formatted_body.toStdString();
RelatedInfo
utils::stripReplyFallbacks(const mtx::events::collections::TimelineEvents &event,
                           std::string id,
                           QString room_id_)
    RelatedInfo related   = {};
    related.quoted_user   = QString::fromStdString(mtx::accessors::sender(event));
    related.related_event = std::move(id);
    related.type          = mtx::accessors::msg_type(event);
    // get body, strip reply fallback, then transform the event to text, if it is a media event
    // etc
    related.quoted_body = QString::fromStdString(mtx::accessors::body(event));
    related.quoted_body =
      QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString()));
    related.quoted_body = utils::getQuoteBody(related);
    // get quoted body and strip reply fallback
    related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
    related.quoted_formatted_body = QString::fromStdString(
      stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString()));
    related.room = room_id_;
    return related;
    return QString::fromStdString(http::client()->user_id().to_string());
    // TODO: Be more precise here.
    return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) ||
           (code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f;
QString
utils::replaceEmoji(const QString &body)
{
    QString fmtBody;
    fmtBody.reserve(body.size());

    QVector<uint> utf32_string = body.toUcs4();

    bool insideFontBlock = false;
    bool insideTag       = false;
    for (auto &code : utf32_string) {
        if (code == U'<')
            insideTag = true;
        else if (code == U'>')
            insideTag = false;

        if (!insideTag && utils::codepointIsEmoji(code)) {
            if (!insideFontBlock) {
                fmtBody += QStringLiteral("<font face=\"") % UserSettings::instance()->emojiFont() %
                           (UserSettings::instance()->enlargeEmojiOnlyMessages()
Loren Burkholder's avatar
Loren Burkholder committed
                              ? QStringLiteral("\" size=\"4\">")
                              : QStringLiteral("\">"));
                insideFontBlock = true;
            } else if (code == 0xfe0f) {
                // BUG(Nico):
                // Workaround https://bugreports.qt.io/browse/QTBUG-97401
                // See also https://github.com/matrix-org/matrix-react-sdk/pull/1458/files
                // Nheko bug: https://github.com/Nheko-Reborn/nheko/issues/439
                continue;
            }
        } else {
            if (insideFontBlock) {
                fmtBody += QStringLiteral("</font>");
                insideFontBlock = false;
            }
        if (QChar::requiresSurrogates(code)) {
            QChar emoji[] = {static_cast<ushort>(QChar::highSurrogate(code)),
                             static_cast<ushort>(QChar::lowSurrogate(code))};
            fmtBody.append(emoji, 2);
        } else {
            fmtBody.append(QChar(static_cast<ushort>(code)));
        }
    }
    if (insideFontBlock) {
        fmtBody += QStringLiteral("</font>");
    }
    return fmtBody;
void
utils::setScaleFactor(float factor)
{
    if (factor < 1 || factor > 3)
        return;
    QSettings settings;
    settings.setValue(QStringLiteral("settings/scale_factor"), factor);
    QSettings settings;
    return settings.value(QStringLiteral("settings/scale_factor"), -1).toFloat();
QString
utils::descriptiveTime(const QDateTime &then)
{
    const auto now  = QDateTime::currentDateTime();
    const auto days = then.daysTo(now);
    if (days == 0)
        return QLocale::system().toString(then.time(), QLocale::ShortFormat);
    else if (days < 2)
        return QString(QCoreApplication::translate("descriptiveTime", "Yesterday"));
    else if (days < 7)
        return then.toString(QStringLiteral("dddd"));
    return QLocale::system().toString(then.date(), QLocale::ShortFormat);
utils::getMessageDescription(const mtx::events::collections::TimelineEvents &event,
Konstantinos Sideris's avatar
Konstantinos Sideris committed
                             const QString &localUser,
                             const QString &displayName)
    using Audio         = mtx::events::RoomEvent<mtx::events::msg::Audio>;
    using Emote         = mtx::events::RoomEvent<mtx::events::msg::Emote>;
    using File          = mtx::events::RoomEvent<mtx::events::msg::File>;
    using Image         = mtx::events::RoomEvent<mtx::events::msg::Image>;
    using Notice        = mtx::events::RoomEvent<mtx::events::msg::Notice>;
    using Text          = mtx::events::RoomEvent<mtx::events::msg::Text>;
    using Unknown       = mtx::events::RoomEvent<mtx::events::msg::Unknown>;
    using Video         = mtx::events::RoomEvent<mtx::events::msg::Video>;
    using ElementEffect = mtx::events::RoomEvent<mtx::events::msg::ElementEffect>;
    using CallInvite    = mtx::events::RoomEvent<mtx::events::voip::CallInvite>;
    using CallAnswer    = mtx::events::RoomEvent<mtx::events::voip::CallAnswer>;
    using CallHangUp    = mtx::events::RoomEvent<mtx::events::voip::CallHangUp>;
    using CallReject    = mtx::events::RoomEvent<mtx::events::voip::CallReject>;
    using Encrypted     = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;

    if (std::holds_alternative<Audio>(event)) {
        return createDescriptionInfo<Audio>(event, localUser, displayName);
    } else if (std::holds_alternative<Emote>(event)) {
        return createDescriptionInfo<Emote>(event, localUser, displayName);
    } else if (std::holds_alternative<File>(event)) {
        return createDescriptionInfo<File>(event, localUser, displayName);
    } else if (std::holds_alternative<Image>(event)) {
        return createDescriptionInfo<Image>(event, localUser, displayName);
    } else if (std::holds_alternative<Notice>(event)) {
        return createDescriptionInfo<Notice>(event, localUser, displayName);
    } else if (std::holds_alternative<Text>(event)) {
        return createDescriptionInfo<Text>(event, localUser, displayName);
    } else if (std::holds_alternative<Unknown>(event)) {
        return createDescriptionInfo<Unknown>(event, localUser, displayName);
    } else if (std::holds_alternative<Video>(event)) {
        return createDescriptionInfo<Video>(event, localUser, displayName);
    } else if (std::holds_alternative<ElementEffect>(event)) {
        return createDescriptionInfo<ElementEffect>(event, localUser, displayName);
    } else if (std::holds_alternative<CallInvite>(event)) {
        return createDescriptionInfo<CallInvite>(event, localUser, displayName);
    } else if (std::holds_alternative<CallAnswer>(event)) {
        return createDescriptionInfo<CallAnswer>(event, localUser, displayName);
    } else if (std::holds_alternative<CallHangUp>(event)) {
        return createDescriptionInfo<CallHangUp>(event, localUser, displayName);
    } else if (std::holds_alternative<CallReject>(event)) {
        return createDescriptionInfo<CallReject>(event, localUser, displayName);
    } else if (std::holds_alternative<mtx::events::Sticker>(event)) {
        return createDescriptionInfo<mtx::events::Sticker>(event, localUser, displayName);
    } else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) {
        const auto sender = QString::fromStdString(msg->sender);

        const auto username = displayName;
        const auto ts       = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts);

        DescInfo info;
        info.userid = sender;
        info.body   = QStringLiteral(" %1").arg(
          messageDescription<Encrypted>(username, QLatin1String(""), sender == localUser));
        info.timestamp       = msg->origin_server_ts;
        info.descriptiveTime = utils::descriptiveTime(ts);
        info.event_id        = QString::fromStdString(msg->event_id);
        info.datetime        = ts;

        return info;
    }
    return DescInfo{};

QString
utils::firstChar(const QString &input)
{
    if (input.isEmpty())
        return input;
    for (auto const &c : input.toStdU32String()) {
        if (QString::fromUcs4(&c, 1) != QStringLiteral("#"))
            return QString::fromUcs4(&c, 1).toUpper();
    }
    return QString::fromUcs4(&input.toStdU32String().at(0), 1).toUpper();
Konstantinos Sideris's avatar
Konstantinos Sideris committed
utils::humanReadableFileSize(uint64_t bytes)
    constexpr static const char *units[] = {"B", "KiB", "MiB", "GiB", "TiB"};
    constexpr static const int length    = sizeof(units) / sizeof(units[0]);
    int u       = 0;
    double size = static_cast<double>(bytes);
    while (size >= 1024.0 && u < length) {
        ++u;
        size /= 1024.0;
    }
    return QString::number(size, 'g', 4) + ' ' + units[u];

int
utils::levenshtein_distance(const std::string &s1, const std::string &s2)
{
    const auto nlen = s1.size();
    const auto hlen = s2.size();
    if (hlen == 0)
        return -1;
    if (nlen == 1)
        return (int)s2.find(s1);
    std::vector<int> row1(hlen + 1, 0);
    for (size_t i = 0; i < nlen; ++i) {
        std::vector<int> row2(1, (int)i + 1);
        for (size_t j = 0; j < hlen; ++j) {
            const int cost = s1[i] != s2[j];
            row2.push_back(std::min(row1[j + 1] + 1, std::min(row2[j] + 1, row1[j] + cost)));
        row1.swap(row2);
    }

    return *std::min_element(row1.begin(), row1.end());
QPixmap
utils::scaleImageToPixmap(const QImage &img, int size)
{
    if (img.isNull())
        return QPixmap();
    // Deprecated in 5.13: const double sz =
    //  std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size);
Nicolas Werner's avatar
Nicolas Werner committed
    const int sz = static_cast<int>(
      std::ceil(QGuiApplication::primaryScreen()->devicePixelRatio() * (double)size));
    return QPixmap::fromImage(img.scaled(sz, sz, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
QPixmap
utils::scaleDown(uint64_t maxWidth, uint64_t maxHeight, const QPixmap &source)
{
    if (source.isNull())
        return QPixmap();
    const double widthRatio     = (double)maxWidth / (double)source.width();
    const double heightRatio    = (double)maxHeight / (double)source.height();
    const double minAspectRatio = std::min(widthRatio, heightRatio);
    // Size of the output image.
    int w, h = 0;
    if (minAspectRatio > 1) {
        w = source.width();
        h = source.height();
    } else {
Nicolas Werner's avatar
Nicolas Werner committed
        w = static_cast<int>(static_cast<double>(source.width()) * minAspectRatio);
        h = static_cast<int>(static_cast<double>(source.height()) * minAspectRatio);
    return source.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
QString
utils::mxcToHttp(const QUrl &url, const QString &server, int port)
{
    auto mxcParts = mtx::client::utils::parse_mxc_url(url.toString().toStdString());
    return QStringLiteral("https://%1:%2/_matrix/media/r0/download/%3/%4")
      .arg(server)
      .arg(port)
      .arg(QString::fromStdString(mxcParts.server), QString::fromStdString(mxcParts.media_id));

QString
utils::humanReadableFingerprint(const std::string &ed25519)
{
    return humanReadableFingerprint(QString::fromStdString(ed25519));
}
QString
utils::humanReadableFingerprint(const QString &ed25519)
{
    QString fingerprint;
    for (int i = 0; i < ed25519.length(); i = i + 4) {
Nicolas Werner's avatar
Nicolas Werner committed
        fingerprint.append(QStringView(ed25519).mid(i, 4));
        if (i > 0 && i == 20)
            fingerprint.append('\n');
        else if (i < ed25519.length())
            fingerprint.append(' ');
    }
    return fingerprint;

QString
utils::linkifyMessage(const QString &body)
{
    // Convert to valid XML.
    auto doc = body;
    doc.replace(conf::strings::url_regex, conf::strings::url_html);

    static QRegularExpression matrixURIRegex(
      QStringLiteral("\\b(?<![\"'])(?>(matrix:[\\S]{5,}))(?![\"'])\\b"));
    doc.replace(matrixURIRegex, conf::strings::url_html);
    return doc;
QString
utils::escapeBlacklistedHtml(const QString &rawStr)
    static const std::set<QByteArray> allowedTags = {
Nicolas Werner's avatar
Nicolas Werner committed
      "font",       "/font",       "del",     "/del",    "h1",    "/h1",    "h2",     "/h2",
      "h3",         "/h3",         "h4",      "/h4",     "h5",    "/h5",    "h6",     "/h6",
      "blockquote", "/blockquote", "p",       "/p",      "a",     "/a",     "ul",     "/ul",
      "ol",         "/ol",         "sup",     "/sup",    "sub",   "/sub",   "li",     "/li",
      "b",          "/b",          "i",       "/i",      "u",     "/u",     "strong", "/strong",
      "em",         "/em",         "strike",  "/strike", "code",  "/code",  "hr",     "/hr",
      "br",         "br/",         "div",     "/div",    "table", "/table", "thead",  "/thead",
      "tbody",      "/tbody",      "tr",      "/tr",     "th",    "/th",    "td",     "/td",
      "caption",    "/caption",    "pre",     "/pre",    "span",  "/span",  "img",    "/img",
      "details",    "/details",    "summary", "/summary"};
    constexpr static const std::array tagNameEnds   = {' ', '>'};
    constexpr static const std::array attrNameEnds  = {' ', '>', '=', '\t', '\r', '\n', '/', '\f'};
    constexpr static const std::array attrValueEnds = {' ', '\t', '\r', '\n', '\f', '>'};
    constexpr static const std::array spaceChars    = {' ', '\t', '\r', '\n', '\f'};
    QByteArray data = rawStr.toUtf8();
    QByteArray buffer;
    const int length = data.size();
    buffer.reserve(length);
    const auto end = data.cend();
    for (auto pos = data.cbegin(); pos < end;) {
        auto tagStart = std::find(pos, end, '<');
Nicolas Werner's avatar
Nicolas Werner committed
        buffer.append(pos, static_cast<int>(tagStart - pos));
        if (tagStart == end)
            break;

        const auto tagNameStart = tagStart + 1;
        const auto tagNameEnd =
          std::find_first_of(tagNameStart, end, tagNameEnds.begin(), tagNameEnds.end());

Nicolas Werner's avatar
Nicolas Werner committed
        if (allowedTags.find(
              QByteArray(tagNameStart, static_cast<int>(tagNameEnd - tagNameStart)).toLower()) ==
            allowedTags.end()) {
            // not allowed -> escape
            buffer.append("&lt;");
            pos = tagNameStart;
            continue;
        } else {
Nicolas Werner's avatar
Nicolas Werner committed
            buffer.append(tagStart, static_cast<int>(tagNameEnd - tagStart));

            pos = tagNameEnd;

            if (tagNameEnd != end) {
                auto attrStart = tagNameEnd;
                auto attrsEnd  = std::find(attrStart, end, '>');
Nicolas Werner's avatar
Nicolas Werner committed
                // we don't want to consume the slash of self closing tags as part of an attribute.
                // However, obviously we don't want to move backwards, if there are no attributes.
                if (*(attrsEnd - 1) == '/' && attrStart < attrsEnd)
                    attrsEnd -= 1;

                pos = attrsEnd;

                auto consumeSpaces = [attrsEnd](auto p) {
                    while (p < attrsEnd &&
                           std::find(spaceChars.begin(), spaceChars.end(), *p) != spaceChars.end())
                        p++;
                    return p;
                };

                attrStart = consumeSpaces(attrStart);

                while (attrStart < attrsEnd) {
                    auto attrEnd = std::find_first_of(
                      attrStart, attrsEnd, attrNameEnds.begin(), attrNameEnds.end());

Nicolas Werner's avatar
Nicolas Werner committed
                    auto attrName =
                      QByteArray(attrStart, static_cast<int>(attrEnd - attrStart)).toLower();

                    auto sanitizeValue = [&attrName](QByteArray val) {
                        if (attrName == QByteArrayLiteral("src") && !val.startsWith("mxc://"))
                            return QByteArray();
                        else
                            return val;
                    };

                    attrStart = consumeSpaces(attrEnd);

                    if (attrName.isEmpty()) {
                        buffer.append(QUrl::toPercentEncoding(QString(QByteArray(attrStart, 1))));
                        attrStart++;
                        continue;
                    } else if (attrStart < attrsEnd) {
                        if (*attrStart == '=') {
                            attrStart = consumeSpaces(attrStart + 1);

                            if (attrStart < attrsEnd) {
                                // we fall through here if the value is empty to transform attr=""
                                // into attr, because otherwise we can't style it
                                if (*attrStart == '"') {
                                    attrStart += 1;
                                    auto valueEnd = std::find(attrStart, attrsEnd, '"');
                                    if (valueEnd == attrsEnd)
                                        break;

Nicolas Werner's avatar
Nicolas Werner committed
                                    auto val  = sanitizeValue(QByteArray(
                                      attrStart, static_cast<int>(valueEnd - attrStart)));
                                    attrStart = consumeSpaces(valueEnd + 1);
                                    if (!val.isEmpty()) {
                                        buffer.append(' ');
                                        buffer.append(attrName);
                                        buffer.append("=\"");
                                        buffer.append(val);
                                        buffer.append('"');
                                        continue;
                                    }
                                } else if (*attrStart == '\'') {
                                    attrStart += 1;
                                    auto valueEnd = std::find(attrStart, attrsEnd, '\'');
                                    if (valueEnd == attrsEnd)
                                        break;

Nicolas Werner's avatar
Nicolas Werner committed
                                    auto val  = sanitizeValue(QByteArray(
                                      attrStart, static_cast<int>(valueEnd - attrStart)));
                                    attrStart = consumeSpaces(valueEnd + 1);
                                    if (!val.isEmpty()) {
                                        buffer.append(' ');
                                        buffer.append(attrName);
                                        buffer.append("=\'");
                                        buffer.append(val);
                                        buffer.append('\'');
                                        continue;
                                    }
                                } else {
                                    auto valueEnd = std::find_first_of(attrStart,
                                                                       attrsEnd,
                                                                       attrValueEnds.begin(),
                                                                       attrValueEnds.end());
Nicolas Werner's avatar
Nicolas Werner committed
                                    auto val      = sanitizeValue(QByteArray(
                                      attrStart, static_cast<int>(valueEnd - attrStart)));
Nicolas Werner's avatar
Nicolas Werner committed
                                    attrStart     = consumeSpaces(valueEnd);

                                    if (val.contains('"'))
                                        continue;

                                    buffer.append("=\"");
                                    buffer.append(val);
                                    buffer.append('"');
    return QString::fromUtf8(buffer);
static void
rainbowify(cmark_node *node)
    // create iterator over node
    cmark_iter *iter = cmark_iter_new(node);

    // First loop to get total text length
    int textLen = 0;
    while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
        cmark_node *cur = cmark_iter_get_node(iter);
        // only text nodes (no code or semilar)
        if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
            continue;
        // count up by length of current node's text
        QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme,
                                QString(cmark_node_get_literal(cur)));
        while (tbf.toNextBoundary() != -1)
            textLen++;
    }
    // create new iter to start over
    cmark_iter_free(iter);
    iter = cmark_iter_new(node);
    // Second loop to rainbowify
    int charIdx = 0;
    while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
        cmark_node *cur = cmark_iter_get_node(iter);
        // only text nodes (no code or similar)
        if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
            continue;

        // get text in current node
        QString nodeText(cmark_node_get_literal(cur));
        // create buffer to append rainbow text to
        QString buf;
        int boundaryStart = 0;
        int boundaryEnd   = 0;
        // use QTextBoundaryFinder to iterate over graphemes
        QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, nodeText);
        while ((boundaryEnd = tbf.toNextBoundary()) != -1) {
            charIdx++;
            // Split text to get current char
            auto curChar  = QStringView(nodeText).mid(boundaryStart, boundaryEnd - boundaryStart);
            boundaryStart = boundaryEnd;
            // Don't rainbowify whitespaces
            if (curChar.trimmed().isEmpty() || utils::codepointIsEmoji(curChar.toUcs4().at(0))) {
                buf.append(curChar);
                continue;
            }

            // get correct color for char index
            // Use colors as described here:
            // https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html
            auto color = QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5);
            // format color for HTML
            auto colorString = color.name(QColor::NameFormat::HexRgb);
            // create HTML element for current char
            auto curCharColored =
              QStringLiteral("<font color=\"%0\">%1</font>").arg(colorString).arg(curChar);
            // append colored HTML element to buffer
            buf.append(curCharColored);
LordMZTE's avatar
LordMZTE committed

        // create HTML_INLINE node to prevent HTML from being escaped
        auto htmlNode = cmark_node_new(CMARK_NODE_HTML_INLINE);
        // set content of HTML node to buffer contents
        cmark_node_set_literal(htmlNode, buf.toUtf8().data());
        // replace current node with HTML node
        cmark_node_replace(cur, htmlNode);
        // free memory of old node
        cmark_node_free(cur);
    }
    cmark_iter_free(iter);
}
static std::string
extract_spoiler_warning(std::string &inside_spoiler)
{
    std::string spoiler_text;
    if (auto spoilerTextEnd = inside_spoiler.find("|"); spoilerTextEnd != std::string::npos) {
        spoiler_text   = inside_spoiler.substr(0, spoilerTextEnd);
        inside_spoiler = inside_spoiler.substr(spoilerTextEnd + 1);
    }
    return QString::fromStdString(spoiler_text).replace('"', "&quot;").toStdString();
}
LordMZTE's avatar
LordMZTE committed

// TODO(Nico): Add tests :D
static void
process_spoilers(cmark_node *node)
{
    auto iter = cmark_iter_new(node);

    while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
        cmark_node *cur = cmark_iter_get_node(iter);
        // only text nodes (no code or similar)
        if (cmark_node_get_type(cur) != CMARK_NODE_TEXT) {
            continue;
LordMZTE's avatar
LordMZTE committed
        }
        std::string_view content = cmark_node_get_literal(cur);

        if (auto posStart = content.find("||"); posStart != std::string::npos) {
            // we have the start of the spoiler
            if (auto posEnd = content.find("||", posStart + 2); posEnd != std::string::npos) {
                // we have the end of the spoiler in the same node

                std::string before_spoiler = std::string(content.substr(0, posStart));
                std::string inside_spoiler =
                  std::string(content.substr(posStart + 2, posEnd - 2 - posStart));
                std::string after_spoiler = std::string(content.substr(posEnd + 2));

                std::string spoiler_text = extract_spoiler_warning(inside_spoiler);

                // create the new nodes
                auto before_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                cmark_node_set_literal(before_node, before_spoiler.c_str());
                auto after_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                cmark_node_set_literal(after_node, after_spoiler.c_str());

                auto block = cmark_node_new(cmark_node_type::CMARK_NODE_CUSTOM_INLINE);
                cmark_node_set_on_enter(
                  block, ("<span data-mx-spoiler=\"" + spoiler_text + "\">").c_str());
                cmark_node_set_on_exit(block, "</span>");
                auto child_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                cmark_node_set_literal(child_node, inside_spoiler.c_str());
                cmark_node_append_child(block, child_node);

                // insert the new nodes into the tree
                cmark_node_replace(cur, block);
                cmark_node_insert_before(block, before_node);
                cmark_node_insert_after(block, after_node);

                // cleanup the replaced node
                cmark_node_free(cur);

                // fixup the iterator
                cmark_iter_reset(iter, block, CMARK_EVENT_EXIT);

            } else {
                // no end found, but lets try sibling nodes
                for (auto next = cmark_node_next(cur); next != nullptr;
                     next      = cmark_node_next(next)) {
                    // only text nodes again
                    if (cmark_node_get_type(next) != CMARK_NODE_TEXT)
                        continue;

                    std::string_view next_content = cmark_node_get_literal(next);
                    if (auto posEndNext = next_content.find("||");
                        posEndNext != std::string_view::npos) {
                        // We found the end of the spoiler
                        std::string before_spoiler = std::string(content.substr(0, posStart));
                        std::string after_spoiler =
                          std::string(next_content.substr(posEndNext + 2));

                        std::string inside_spoiler_start =
                          std::string(content.substr(posStart + 2));
                        std::string inside_spoiler_end =
                          std::string(next_content.substr(0, posEndNext));

                        std::string spoiler_text = extract_spoiler_warning(inside_spoiler_start);

                        // save all the nodes inside the spoiler for later
                        std::vector<cmark_node *> child_nodes;
                        for (auto kid = cmark_node_next(cur); kid != nullptr && kid != next;
                             kid      = cmark_node_next(kid)) {
                            child_nodes.push_back(kid);
                        }

                        // create the new nodes
                        auto before_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                        cmark_node_set_literal(before_node, before_spoiler.c_str());
                        auto after_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                        cmark_node_set_literal(after_node, after_spoiler.c_str());

                        auto block = cmark_node_new(cmark_node_type::CMARK_NODE_CUSTOM_INLINE);
                        cmark_node_set_on_enter(
                          block, ("<span data-mx-spoiler=\"" + spoiler_text + "\">").c_str());
                        cmark_node_set_on_exit(block, "</span>");

                        // create the content inside the spoiler by adding the old text at the start
                        // and the end as well as all the existing children
                        auto child_node_start = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                        cmark_node_set_literal(child_node_start, inside_spoiler_start.c_str());
                        cmark_node_append_child(block, child_node_start);
                        for (auto &child : child_nodes)
                            cmark_node_append_child(block, child);
                        auto child_node_end = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                        cmark_node_set_literal(child_node_end, inside_spoiler_end.c_str());
                        cmark_node_append_child(block, child_node_end);

                        // insert the new nodes into the tree
                        cmark_node_replace(cur, block);
                        cmark_node_insert_before(block, before_node);
                        cmark_node_insert_after(block, after_node);

                        // cleanup removed nodes
                        cmark_node_free(cur);
                        cmark_node_free(next);

                        // fixup the iterator
                        cmark_iter_reset(iter, block, CMARK_EVENT_EXIT);

                        break;
                    }
                }
            }
        }
    }

    cmark_iter_free(iter);
}

static void
process_strikethrough(cmark_node *node)
{
    auto iter = cmark_iter_new(node);

    while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
        cmark_node *cur = cmark_iter_get_node(iter);

        // only text nodes (no code or similar)
        if (cmark_node_get_type(cur) != CMARK_NODE_TEXT) {
            continue;
        }

        std::string_view content = cmark_node_get_literal(cur);

        if (auto posStart = content.find("~~"); posStart != std::string::npos) {
            // we have the start of the strikethrough
            if (auto posEnd = content.find("~~", posStart + 2); posEnd != std::string::npos) {
                // we have the end of the strikethrough in the same node

                std::string before_strike = std::string(content.substr(0, posStart));
                std::string inside_strike =
                  std::string(content.substr(posStart + 2, posEnd - 2 - posStart));
                std::string after_strike = std::string(content.substr(posEnd + 2));

                // create the new nodes
                auto before_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                cmark_node_set_literal(before_node, before_strike.c_str());
                auto after_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                cmark_node_set_literal(after_node, after_strike.c_str());

                auto block = cmark_node_new(cmark_node_type::CMARK_NODE_CUSTOM_INLINE);
                cmark_node_set_on_enter(block, "<del>");
                cmark_node_set_on_exit(block, "</del>");
                auto child_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                cmark_node_set_literal(child_node, inside_strike.c_str());
                cmark_node_append_child(block, child_node);

                // insert the new nodes into the tree
                cmark_node_replace(cur, block);
                cmark_node_insert_before(block, before_node);
                cmark_node_insert_after(block, after_node);

                // cleanup the replaced node
                cmark_node_free(cur);

                // fixup the iterator
                cmark_iter_reset(iter, block, CMARK_EVENT_EXIT);

            } else {
                // no end found, but lets try sibling nodes
                for (auto next = cmark_node_next(cur); next != nullptr;
                     next      = cmark_node_next(next)) {
                    // only text nodes again
                    if (cmark_node_get_type(next) != CMARK_NODE_TEXT)
                        continue;

                    std::string_view next_content = cmark_node_get_literal(next);
                    if (auto posEndNext = next_content.find("~~");
                        posEndNext != std::string_view::npos) {
                        // We found the end of the strikethrough
                        std::string before_strike = std::string(content.substr(0, posStart));
                        std::string after_strike = std::string(next_content.substr(posEndNext + 2));

                        std::string inside_strike_start = std::string(content.substr(posStart + 2));
                        std::string inside_strike_end =
                          std::string(next_content.substr(0, posEndNext));

                        // save all the nodes inside the strikethrough for later
                        std::vector<cmark_node *> child_nodes;
                        for (auto kid = cmark_node_next(cur); kid != nullptr && kid != next;
                             kid      = cmark_node_next(kid)) {
                            child_nodes.push_back(kid);
                        }

                        // create the new nodes
                        auto before_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                        cmark_node_set_literal(before_node, before_strike.c_str());
                        auto after_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                        cmark_node_set_literal(after_node, after_strike.c_str());

                        auto block = cmark_node_new(cmark_node_type::CMARK_NODE_CUSTOM_INLINE);
                        cmark_node_set_on_enter(block, "<del>");
                        cmark_node_set_on_exit(block, "</del>");

                        // create the content inside the strikethrough by adding the old text at the
                        // start and the end as well as all the existing children
                        auto child_node_start = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                        cmark_node_set_literal(child_node_start, inside_strike_start.c_str());
                        cmark_node_append_child(block, child_node_start);
                        for (auto &child : child_nodes)
                            cmark_node_append_child(block, child);
                        auto child_node_end = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
                        cmark_node_set_literal(child_node_end, inside_strike_end.c_str());
                        cmark_node_append_child(block, child_node_end);

                        // insert the new nodes into the tree
                        cmark_node_replace(cur, block);
                        cmark_node_insert_before(block, before_node);
                        cmark_node_insert_after(block, after_node);

                        // cleanup removed nodes
                        cmark_node_free(cur);
                        cmark_node_free(next);

                        // fixup the iterator
                        cmark_iter_reset(iter, block, CMARK_EVENT_EXIT);

                        break;
                    }
                }
            }
        }
    }

    cmark_iter_free(iter);
}
utils::markdownToHtml(const QString &text, bool rainbowify_, bool noExtensions)
{
    const auto str         = text.toUtf8();
    cmark_node *const node = cmark_parse_document(str.constData(), str.size(), CMARK_OPT_UNSAFE);

    if (!noExtensions) {
        process_strikethrough(node);
        process_spoilers(node);
        if (rainbowify_) {
            rainbowify(node);
        }
    const char *tmp_buf = cmark_render_html(
      node,
      // by default make single linebreaks <br> tags
      noExtensions ? CMARK_OPT_UNSAFE : (CMARK_OPT_UNSAFE | CMARK_OPT_HARDBREAKS));
    // Copy the null terminated output buffer.
    std::string html(tmp_buf);
    // The buffer is no longer needed.
    free((char *)tmp_buf);
    auto result = escapeBlacklistedHtml(QString::fromStdString(html)).trimmed();

    if (!noExtensions) {
        result = linkifyMessage(std::move(result)).trimmed();
    }
    if (result.count(QStringLiteral("<p>")) == 1 && result.startsWith(QLatin1String("<p>")) &&
        result.endsWith(QLatin1String("</p>"))) {
        result = result.mid(3, result.size() - 3 - 4);
    }
    return result;
utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
    auto getFormattedBody = [related]() -> QString {
        using MsgType = mtx::events::MessageType;

        switch (related.type) {
        case MsgType::File: {
            return QStringLiteral("sent a file.");
        }
        case MsgType::Image: {
            return QStringLiteral("sent an image.");
        }
        case MsgType::Audio: {
            return QStringLiteral("sent an audio file.");
        }
        case MsgType::Video: {
            return QStringLiteral("sent a video");
            return related.quoted_formatted_body;
    };
    return QString("<mx-reply><blockquote><a "
                   "href=\"https://matrix.to/#/%1/%2\">In reply "
                   "to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br"
                   "/>%5</blockquote></mx-reply>")
             .arg(related.room,
                  QString::fromStdString(related.related_event),
                  related.quoted_user,
                  related.quoted_user,
                  getFormattedBody()) +
           html;
}

QString
utils::getQuoteBody(const RelatedInfo &related)
{
    using MsgType = mtx::events::MessageType;

    switch (related.type) {
    case MsgType::File: {
        return QStringLiteral("sent a file.");
    }
    case MsgType::Image: {
        return QStringLiteral("sent an image.");