Skip to content
Snippets Groups Projects
Utils.cpp 62.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • // 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 <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.begin());
            if (!segments.empty() && segments.first().isEmpty())
                segments.erase(segments.begin());
            body = segments.join('\n');
        }
    
        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);
    
        formatted_body.remove(QRegularExpression(QStringLiteral("<mx-reply>.*</mx-reply>"),
    
                                                 QRegularExpression::DotMatchesEverythingOption));
    
        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);
    
        doc.replace(
          QRegularExpression(QStringLiteral("\\b(?<![\"'])(?>(matrix:[\\S]{5,}))(?![\"'])\\b")),
          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.");
    
        }
        case MsgType::Audio: {
    
            return QStringLiteral("sent an audio file.");