File indexing completed on 2024-12-08 07:34:31

0001 /*
0002    SPDX-FileCopyrightText: 2017-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "utils.h"
0008 #include "colorsandmessageviewstyle.h"
0009 #include "ruqola_debug.h"
0010 #include <KLocalizedString>
0011 
0012 #include <KColorScheme>
0013 #include <QCryptographicHash>
0014 #include <QDateTime>
0015 #include <QJsonDocument>
0016 #include <QRegularExpression>
0017 
0018 #include <TextEmoticonsCore/EmoticonUnicodeUtils>
0019 
0020 QUrl Utils::generateServerUrl(const QString &url)
0021 {
0022     if (url.isEmpty()) {
0023         return {};
0024     }
0025     QString serverUrl = url;
0026     if (serverUrl.startsWith(QLatin1String("https://"))) {
0027         serverUrl.replace(QLatin1String("https://"), QLatin1String("wss://"));
0028     } else if (serverUrl.startsWith(QLatin1String("http://"))) {
0029         serverUrl.replace(QLatin1String("http://"), QLatin1String("ws://"));
0030     } else {
0031         serverUrl = QLatin1String("wss://") + serverUrl;
0032     }
0033     return QUrl(serverUrl + QStringLiteral("/websocket"));
0034 }
0035 
0036 QString Utils::extractRoomUserFromUrl(QString url)
0037 {
0038     url.remove(QStringLiteral("ruqola:/user/"));
0039     url.remove(QStringLiteral("ruqola:/room/"));
0040     return url;
0041 }
0042 
0043 QString Utils::formatQuotedRichText(const QuotedRichTextInfo &info)
0044 {
0045     // Qt's support for borders is limited to tables, so we have to jump through some hoops...
0046     const auto backgroundColor = ColorsAndMessageViewStyle::self().schemeView().background(KColorScheme::AlternateBackground).color().name();
0047     const auto borderColor = ColorsAndMessageViewStyle::self().schemeView().foreground(KColorScheme::LinkText).color().name();
0048     QString dateTimeInfo;
0049     if (!info.displayTime.isEmpty()) {
0050         if (!info.url.isEmpty()) {
0051             dateTimeInfo = QLatin1Char('\n') + QStringLiteral("<a href='%1'>%2</a>").arg(info.url, info.displayTime);
0052         } else {
0053             dateTimeInfo = QLatin1Char('\n') + info.displayTime;
0054         }
0055     }
0056     return QStringLiteral("<table><tr><td style='background-color:%1; padding-left: 5px; border-left: 5px solid %2'>").arg(backgroundColor, borderColor)
0057         + info.richText + dateTimeInfo + QStringLiteral("</td></tr></table>");
0058 }
0059 
0060 QString Utils::presenceStatusToString(User::PresenceStatus status)
0061 {
0062     switch (status) {
0063     case User::PresenceStatus::PresenceOnline:
0064         return QStringLiteral("online");
0065     case User::PresenceStatus::PresenceBusy:
0066         return QStringLiteral("busy");
0067     case User::PresenceStatus::PresenceAway:
0068         return QStringLiteral("away");
0069     case User::PresenceStatus::PresenceOffline:
0070         return QStringLiteral("offline");
0071     case User::PresenceStatus::Unknown:
0072         return {};
0073     }
0074 
0075     return {};
0076 }
0077 
0078 QString Utils::iconFromPresenceStatus(User::PresenceStatus status)
0079 {
0080     switch (status) {
0081     case User::PresenceStatus::PresenceOnline:
0082         return QStringLiteral("user-online");
0083     case User::PresenceStatus::PresenceBusy:
0084         return QStringLiteral("user-busy");
0085     case User::PresenceStatus::PresenceAway:
0086         return QStringLiteral("user-away");
0087     case User::PresenceStatus::PresenceOffline:
0088         return QStringLiteral("user-offline");
0089     case User::PresenceStatus::Unknown:
0090         return QStringLiteral("unknown");
0091     }
0092     qCWarning(RUQOLA_LOG) << "Unknown status" << status;
0093     return QStringLiteral("unknown");
0094 }
0095 
0096 QString Utils::iconFromStatus(const QString &status)
0097 {
0098     if (status == QLatin1String("online")) {
0099         return QStringLiteral("user-online");
0100     } else if (status == QLatin1String("busy")) {
0101         return QStringLiteral("user-busy");
0102     } else if (status == QLatin1String("away")) {
0103         return QStringLiteral("user-away");
0104     } else if (status == QLatin1String("offline")) {
0105         return QStringLiteral("user-offline");
0106     } else {
0107         qCWarning(RUQOLA_LOG) << "Unknown status" << status;
0108         return QStringLiteral("unknown");
0109     }
0110 }
0111 
0112 User::PresenceStatus Utils::presenceStatusFromString(const QString &status)
0113 {
0114     if (status == QLatin1String("online")) {
0115         return User::PresenceStatus::PresenceOnline;
0116     } else if (status == QLatin1String("busy")) {
0117         return User::PresenceStatus::PresenceBusy;
0118     } else if (status == QLatin1String("away")) {
0119         return User::PresenceStatus::PresenceAway;
0120     } else if (status == QLatin1String("offline")) {
0121         return User::PresenceStatus::PresenceOffline;
0122     } else {
0123         qCDebug(RUQOLA_LOG) << "Problem with status " << status;
0124         return User::PresenceStatus::Unknown;
0125     }
0126 }
0127 
0128 QString Utils::userIdFromDirectChannel(const QString &rid, const QString &userId)
0129 {
0130     QString newUserId = rid;
0131     newUserId.remove(userId);
0132     return newUserId;
0133 }
0134 
0135 qint64 Utils::parseDate(const QString &key, const QJsonObject &o)
0136 {
0137     return o.value(key).toObject().value(QLatin1String("$date")).toDouble(-1);
0138 }
0139 
0140 qint64 Utils::parseIsoDate(const QString &key, const QJsonObject &o)
0141 {
0142     if (o.contains(key)) {
0143         return QDateTime::fromString(o.value(key).toString(), Qt::ISODate).toMSecsSinceEpoch();
0144     } else {
0145         return -1;
0146     }
0147 }
0148 
0149 QString Utils::convertTextHeaders(const QString &str)
0150 {
0151     static const QRegularExpression useHeaders(QStringLiteral("(^|\\n)#.*\\s"));
0152     if (str.contains(useHeaders)) {
0153         static const QRegularExpression regularHeader1(QStringLiteral("(^|\\n)(#)\\s(.*)($|\\n)"));
0154         QString newStr = str;
0155         newStr = newStr.replace(regularHeader1, QStringLiteral("\\1<h1>\\3</h1>"));
0156         static const QRegularExpression regularHeader2(QStringLiteral("(^|\\n|</h\\d>)(##)\\s(.*)($|\\n)"));
0157         newStr = newStr.replace(regularHeader2, QStringLiteral("\\1<h2>\\3</h2>"));
0158         static const QRegularExpression regularHeader3(QStringLiteral("(^|\\n|</h\\d>)(###)\\s(.*)($|\\n)"));
0159         newStr = newStr.replace(regularHeader3, QStringLiteral("\\1<h3>\\3</h3>"));
0160         static const QRegularExpression regularHeader4(QStringLiteral("(^|\\n|</h\\d>)(####)\\s(.*)($|\\n)"));
0161         newStr = newStr.replace(regularHeader4, QStringLiteral("\\1<h4>\\3</h4>"));
0162         static const QRegularExpression regularHeader5(QStringLiteral("(^|\\n|</h\\d>)(#####)\\s(.*)($|\\n)"));
0163         newStr = newStr.replace(regularHeader5, QStringLiteral("\\1<h5>\\3</h5>"));
0164         static const QRegularExpression regularHeader6(QStringLiteral("(^|\\n|</h\\d>)(######)\\s(.*)($|\\n)"));
0165         newStr = newStr.replace(regularHeader6, QStringLiteral("\\1<h6>\\3</h6>"));
0166         return newStr;
0167     }
0168     return str;
0169 }
0170 
0171 QString Utils::convertTextWithCheckMark(const QString &str)
0172 {
0173     static const QRegularExpression regularUnCheckMark(QStringLiteral("(^|\\n)-\\s\\[\\s\\]\\s"));
0174     static const QRegularExpression regularCheckMark(QStringLiteral("(^|\\n)-\\s\\[x]\\s"));
0175     QString newStr = str;
0176     newStr = newStr.replace(regularUnCheckMark, QStringLiteral("\\1:white_medium_square: "));
0177     newStr = newStr.replace(regularCheckMark, QStringLiteral("\\1:ballot_box_with_check: "));
0178     return newStr;
0179 }
0180 
0181 QString Utils::convertTextWithUrl(const QString &str)
0182 {
0183     static const QRegularExpression regularExpressionAHref(QStringLiteral("<a href=\"(.*)\">(.*)</a>"));
0184     static const QRegularExpression regularExpressionCustomAHref(QStringLiteral("<a href=\"(.*)\\|(.*)\">(.*)</a>"));
0185     QString newStr;
0186     bool isRef = false;
0187     bool isUrl = false;
0188     bool isHasNewRef = false;
0189     QString url;
0190     QString references;
0191     for (int i = 0; i < str.length(); ++i) {
0192         const QChar ref = str.at(i);
0193         if (ref == QLatin1Char('[')) {
0194             if (isRef) {
0195                 isRef = false;
0196                 newStr += QLatin1Char('[') + references + QLatin1Char('[');
0197                 references.clear();
0198             } else {
0199                 isRef = true;
0200             }
0201 #if 0
0202         } else if (isUrl && ref == QLatin1Char(']') && isHasNewRef) {
0203             isUrl = false;
0204             isRef = false;
0205             newStr += QStringLiteral("<a href=\'%1'>%2</a>").arg(url, references);
0206             references.clear();
0207             url.clear();
0208 #endif
0209         } else if (isRef && ref == QLatin1Char(']')) {
0210             isRef = false;
0211             if ((i == str.length() - 1) || (str.at(i + 1) != QLatin1Char('('))) {
0212                 if (references.startsWith(QLatin1Char('<'))) {
0213                     newStr += references.replace(regularExpressionCustomAHref, QStringLiteral("<a href=\"\\2\">\\1</a>"));
0214                 } else {
0215                     newStr += QLatin1Char('[') + references + QLatin1Char(']');
0216                 }
0217                 references.clear();
0218             }
0219         } else if (ref == QLatin1Char('(') && !references.isEmpty()) {
0220             isUrl = true;
0221         } else if (isUrl && ref == QLatin1Char(')') && !references.isEmpty()) {
0222             isUrl = false;
0223             // detect whether the string already contains HTML <a/> tags
0224             if (url.startsWith(QLatin1Char('<'))) {
0225                 newStr += url.replace(regularExpressionAHref, QStringLiteral("<a href=\"\\1\">%1</a>").arg(references));
0226             } else {
0227                 newStr += QStringLiteral("<a href=\'%1'>%2</a>").arg(url, references);
0228             }
0229             references.clear();
0230             url.clear();
0231 #if 0
0232         } else if (ref == QLatin1Char('|') && !references.isEmpty()) {
0233             isUrl = true;
0234             isRef = false;
0235             isHasNewRef = true;
0236 #endif
0237         } else {
0238             if (isRef) {
0239                 references += ref;
0240             } else if (isUrl) {
0241                 url += ref;
0242             } else {
0243                 newStr += ref;
0244             }
0245         }
0246     }
0247     if (isRef) {
0248         newStr += QLatin1Char('[') + references;
0249     } else if (isUrl) {
0250         newStr += QLatin1Char('[') + references + QLatin1String("](") + url;
0251     } else if (isHasNewRef) {
0252         if (!url.isEmpty() && !references.isEmpty()) {
0253             newStr += QStringLiteral("<a href=\'%1'>%2</a>").arg(url, references);
0254         }
0255     }
0256     return newStr;
0257 }
0258 
0259 QJsonObject Utils::strToJsonObject(const QString &jsonString)
0260 {
0261     QJsonParseError jsonParseError;
0262     const auto doc = QJsonDocument::fromJson(jsonString.toLatin1(), &jsonParseError);
0263 
0264     if (jsonParseError.error != QJsonParseError::NoError) {
0265         qCWarning(RUQOLA_LOG).nospace() << Q_FUNC_INFO << " Couldn't parse a valid JSON from argument: " << jsonString
0266                                         << "\n JSON parse error: " << jsonParseError.errorString();
0267         return {};
0268     }
0269 
0270     if (!doc.isObject()) {
0271         qCWarning(RUQOLA_LOG) << Q_FUNC_INFO << "The JSON string argument is not a JSON object." << jsonString;
0272         return {};
0273     }
0274 
0275     return doc.object();
0276 }
0277 
0278 QJsonArray Utils::strToJsonArray(const QString &jsonString)
0279 {
0280     QJsonParseError jsonParseError;
0281     const auto doc = QJsonDocument::fromJson(jsonString.toLatin1(), &jsonParseError);
0282 
0283     if (jsonParseError.error != QJsonParseError::NoError) {
0284         qCWarning(RUQOLA_LOG).nospace() << Q_FUNC_INFO << " Couldn't parse a valid JSON from argument: " << jsonString
0285                                         << "\n JSON parse error: " << jsonParseError.errorString();
0286         return {};
0287     }
0288 
0289     if (!doc.isArray()) {
0290         qCWarning(RUQOLA_LOG) << Q_FUNC_INFO << "The JSON string argument is not a JSON array." << jsonString;
0291         return {};
0292     }
0293 
0294     return doc.array();
0295 }
0296 
0297 QByteArray Utils::convertSha256Password(const QString &pwd)
0298 {
0299     const QByteArray sha256pw = QCryptographicHash::hash(pwd.toUtf8(), QCryptographicHash::Sha256).toHex();
0300     return sha256pw;
0301 }
0302 
0303 QUrl Utils::avatarUrl(const QString &serverRcUrl, const AvatarInfo &avatarInfo)
0304 {
0305     if (serverRcUrl.isEmpty()) {
0306         return {};
0307     }
0308     QString serverUrl = serverRcUrl;
0309     QString subFolder;
0310     switch (avatarInfo.avatarType) {
0311     case AvatarType::Room:
0312         subFolder = QStringLiteral("/room");
0313         break;
0314     case AvatarType::Unknown:
0315     case AvatarType::User:
0316     case AvatarType::UserAndRoom:
0317         break;
0318     }
0319     subFolder += QLatin1Char('/') + avatarInfo.identifier;
0320     subFolder += QStringLiteral("?format=png");
0321     if (!avatarInfo.etag.isEmpty()) {
0322         subFolder += QStringLiteral("&etag=%1").arg(avatarInfo.etag);
0323     }
0324     subFolder += QStringLiteral("&size=22");
0325     if (!serverUrl.startsWith(QStringView(u"https://")) && !serverUrl.startsWith(QStringView(u"http://"))) {
0326         serverUrl.prepend(QStringLiteral("https://"));
0327     }
0328     return QUrl(serverUrl + QStringLiteral("/avatar") + subFolder);
0329 }
0330 
0331 QDebug operator<<(QDebug d, const Utils::AvatarInfo &t)
0332 {
0333     d << "etag " << t.etag;
0334     d << "identifier " << t.identifier;
0335     d << "avatarType " << static_cast<int>(t.avatarType);
0336     return d;
0337 }
0338 
0339 QString Utils::emojiFontName()
0340 {
0341     return TextEmoticonsCore::EmoticonUnicodeUtils::emojiFontName();
0342 }
0343 
0344 QString Utils::displaytextFromPresenceStatus(User::PresenceStatus status)
0345 {
0346     switch (status) {
0347     case User::PresenceStatus::PresenceOnline:
0348         return i18n("Online");
0349     case User::PresenceStatus::PresenceBusy:
0350         return i18n("Busy");
0351     case User::PresenceStatus::PresenceAway:
0352         return i18n("Away");
0353     case User::PresenceStatus::PresenceOffline:
0354         return i18n("Offline");
0355     case User::PresenceStatus::Unknown:
0356         return {};
0357     }
0358     return {};
0359 }
0360 
0361 QString Utils::AvatarInfo::generateAvatarIdentifier() const
0362 {
0363     if (etag.isEmpty()) {
0364         return identifier;
0365     } else {
0366         return identifier + QLatin1Char('-') + etag;
0367     }
0368 }