File indexing completed on 2024-03-03 15:46:56

0001 /*
0002    SPDX-FileCopyrightText: 2018-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "emoticons/emojimanager.h"
0008 #include "rocketchataccount.h"
0009 #include "ruqola_debug.h"
0010 #include <TextEmoticonsCore/UnicodeEmoticonManager>
0011 
0012 #include <QJsonArray>
0013 #include <QJsonObject>
0014 #include <QRegularExpressionMatch>
0015 #include <QTextStream>
0016 
0017 EmojiManager::EmojiManager(RocketChatAccount *account, QObject *parent)
0018     : QObject(parent)
0019     , mRocketChatAccount(account)
0020 {
0021 }
0022 
0023 EmojiManager::~EmojiManager() = default;
0024 
0025 QList<TextEmoticonsCore::UnicodeEmoticon> EmojiManager::unicodeEmojiList() const
0026 {
0027     return TextEmoticonsCore::UnicodeEmoticonManager::self()->unicodeEmojiList();
0028 }
0029 
0030 QList<TextEmoticonsCore::EmoticonCategory> EmojiManager::categories() const
0031 {
0032     return TextEmoticonsCore::UnicodeEmoticonManager::self()->categories();
0033 }
0034 
0035 QList<TextEmoticonsCore::UnicodeEmoticon> EmojiManager::emojisForCategory(const QString &category) const
0036 {
0037     return TextEmoticonsCore::UnicodeEmoticonManager::self()->emojisForCategory(category);
0038 }
0039 
0040 void EmojiManager::addUpdateEmojiCustomList(const QJsonArray &arrayEmojiCustomArray)
0041 {
0042     bool newEmoji = true;
0043     for (int i = 0; i < arrayEmojiCustomArray.count(); ++i) {
0044         const QJsonObject obj = arrayEmojiCustomArray.at(i).toObject();
0045         const QJsonObject customEmojiObj = obj.value(QLatin1String("emojiData")).toObject();
0046         if (!customEmojiObj.isEmpty()) {
0047             if (customEmojiObj.contains(QLatin1String("_id"))) {
0048                 const QString identifier = customEmojiObj.value(QLatin1String("_id")).toString();
0049                 for (auto emoji : std::as_const(mCustomEmojiList)) {
0050                     if (emoji.identifier() == identifier) {
0051                         mCustomEmojiList.removeAll(emoji);
0052                         emoji.parseEmoji(customEmojiObj, true);
0053                         mCustomEmojiList.append(emoji);
0054                         newEmoji = false;
0055                         break;
0056                     }
0057                 }
0058             }
0059             //            if (!found) {
0060             //                // Parse
0061             //                CustomEmoji newEmoji;
0062             //                newEmoji.parseEmoji(customEmojiObj, true);
0063             //                if (newEmoji.isValid()) {
0064             //                    mCustomEmojiList.append(newEmoji);
0065             //                }
0066             //            }
0067         } else {
0068             qCWarning(RUQOLA_LOG) << "addUpdateEmojiCustomList invalid QJsonObject" << customEmojiObj;
0069         }
0070     }
0071 
0072     // New QJsonArray([{"emojiData":{"_id":"HdN28k4PQ6J9xLkZ8","_updatedAt":{"$date":1631885946222},"aliases":["roo"],"extension":"png","name":"ruqola"}}])
0073     // Update
0074     // QJsonArray([{"emojiData":{"_id":"vxE6eG5FrZCvbgM3t","aliases":["rooss"],"extension":"png","name":"xxx","newFile":true,"previousExtension":"png","previousName":"ruqolas"}}
0075     Q_EMIT customEmojiChanged(newEmoji);
0076 }
0077 
0078 void EmojiManager::deleteEmojiCustom(const QJsonArray &arrayEmojiCustomArray)
0079 {
0080     // ([{"emojiData":{"_id":"PpawhZMaseBcEuGCG","_updatedAt":{"$date":1631858916014},"aliases":[],"extension":"png","name":"ruqolaff"}}])
0081     const auto count{arrayEmojiCustomArray.count()};
0082     for (auto i = 0; i < count; ++i) {
0083         const QJsonObject obj = arrayEmojiCustomArray.at(i).toObject();
0084         const QJsonObject emojiData = obj.value(QStringLiteral("emojiData")).toObject();
0085         const QString identifier = emojiData.value(QStringLiteral("_id")).toString();
0086         if (!identifier.isEmpty()) {
0087             auto it = std::find_if(mCustomEmojiList.cbegin(), mCustomEmojiList.cend(), [identifier](const auto &emoji) {
0088                 return emoji.identifier() == identifier;
0089             });
0090             if (it != mCustomEmojiList.cend()) {
0091                 mCustomEmojiList.removeAll(*it);
0092             }
0093         }
0094     }
0095     Q_EMIT customEmojiChanged(false);
0096 }
0097 
0098 void EmojiManager::loadCustomEmoji(const QJsonObject &obj)
0099 {
0100     mCustomEmojiList.clear();
0101     const QJsonObject result = obj.value(QLatin1String("emojis")).toObject();
0102     const QJsonArray array = result.value(QLatin1String("update")).toArray();
0103     // TODO add support for remove when we store it in local
0104     for (int i = 0, total = array.size(); i < total; ++i) {
0105         const QJsonObject emojiJson = array.at(i).toObject();
0106         CustomEmoji emoji;
0107         emoji.parseEmoji(emojiJson);
0108         if (emoji.isValid()) {
0109             mCustomEmojiList.append(std::move(emoji));
0110         }
0111     }
0112 
0113     // clear cache
0114     mReplacePatternDirty = true;
0115 }
0116 
0117 int EmojiManager::count() const
0118 {
0119     return mCustomEmojiList.count() + TextEmoticonsCore::UnicodeEmoticonManager::self()->count();
0120 }
0121 
0122 bool EmojiManager::isAnimatedImage(const QString &emojiIdentifier) const
0123 {
0124     if (emojiIdentifier.startsWith(QLatin1Char(':')) && emojiIdentifier.endsWith(QLatin1Char(':'))) {
0125         for (int i = 0, total = mCustomEmojiList.size(); i < total; ++i) {
0126             const CustomEmoji emoji = mCustomEmojiList.at(i);
0127             if (emoji.hasEmoji(emojiIdentifier)) {
0128                 return emoji.isAnimatedImage();
0129             }
0130         }
0131     }
0132     return false;
0133 }
0134 
0135 TextEmoticonsCore::UnicodeEmoticon EmojiManager::unicodeEmoticonForEmoji(const QString &emojiIdentifier) const
0136 {
0137     return TextEmoticonsCore::UnicodeEmoticonManager::self()->unicodeEmoticonForEmoji(emojiIdentifier);
0138 }
0139 
0140 QString EmojiManager::customEmojiFileNameFromIdentifier(const QString &emojiIdentifier) const
0141 {
0142     for (const CustomEmoji &customEmoji : mCustomEmojiList) {
0143         if (customEmoji.identifier() == emojiIdentifier) {
0144             return customEmoji.emojiFileName();
0145         }
0146     }
0147     return {};
0148 }
0149 
0150 QString EmojiManager::customEmojiFileName(const QString &emojiIdentifier) const
0151 {
0152     for (const CustomEmoji &customEmoji : mCustomEmojiList) {
0153         if (customEmoji.hasEmoji(emojiIdentifier)) {
0154             return customEmoji.emojiFileName();
0155         }
0156     }
0157     return {};
0158 }
0159 
0160 QString EmojiManager::normalizedReactionEmoji(const QString &emojiIdentifier) const
0161 {
0162     for (const auto &customEmoji : mCustomEmojiList) {
0163         if (customEmoji.hasEmoji(emojiIdentifier)) {
0164             return customEmoji.emojiIdentifier();
0165         }
0166     }
0167     const auto unicodeEmojis = unicodeEmojiList();
0168     for (const auto &unicodeEmoji : unicodeEmojis) {
0169         if (unicodeEmoji.hasEmoji(emojiIdentifier)) {
0170             return unicodeEmoji.identifier();
0171         }
0172     }
0173     return emojiIdentifier;
0174 }
0175 
0176 QString EmojiManager::replaceEmojiIdentifier(const QString &emojiIdentifier, bool isReaction)
0177 {
0178     if (mServerUrl.isEmpty()) {
0179         qCWarning(RUQOLA_LOG) << "Server Url not defined";
0180         return emojiIdentifier;
0181     }
0182     if (mRocketChatAccount && !mRocketChatAccount->ownUserPreferences().convertAsciiEmoji()) {
0183         return emojiIdentifier;
0184     }
0185     if (emojiIdentifier.startsWith(QLatin1Char(':')) && emojiIdentifier.endsWith(QLatin1Char(':'))) {
0186         for (const CustomEmoji &emoji : std::as_const(mCustomEmojiList)) {
0187             if (emoji.hasEmoji(emojiIdentifier)) {
0188                 QString cachedHtml = emoji.cachedHtml();
0189                 if (cachedHtml.isEmpty()) {
0190                     // For the moment we can't support animated image as emoticon in text. Only as Reaction.
0191                     if (emoji.isAnimatedImage() && isReaction) {
0192                         cachedHtml = emoji.generateAnimatedUrlFromCustomEmoji(mServerUrl);
0193                     } else {
0194                         const QString fileName = customEmojiFileName(emojiIdentifier);
0195                         if (!fileName.isEmpty() && mRocketChatAccount) {
0196                             const QUrl emojiUrl = mRocketChatAccount->attachmentUrlFromLocalCache(fileName);
0197                             if (emojiUrl.isEmpty()) {
0198                                 // The download is happening, this will all be updated again later
0199                             } else {
0200                                 cachedHtml = emoji.generateHtmlFromCustomEmojiLocalPath(emojiUrl.path());
0201                             }
0202                         } else {
0203                             qCDebug(RUQOLA_LOG) << " Impossible to find custom emoji " << emojiIdentifier;
0204                         }
0205                     }
0206                 }
0207                 return cachedHtml;
0208             }
0209         }
0210     }
0211 
0212     const TextEmoticonsCore::UnicodeEmoticon unicodeEmoticon = unicodeEmoticonForEmoji(emojiIdentifier);
0213     if (unicodeEmoticon.isValid()) {
0214         return unicodeEmoticon.unicodeDisplay();
0215     }
0216 
0217     return emojiIdentifier;
0218 }
0219 
0220 void EmojiManager::replaceEmojis(QString *str)
0221 {
0222     Q_ASSERT(str);
0223     if (mReplacePatternDirty) {
0224         // build a regexp pattern for all the possible emoticons we want to replace
0225         // i.e. this is going to build a pattern like this:
0226         // \:smiley\:|\:\-\)|...
0227         // to optimize it a bit, we use a common pattern that matches most
0228         // emojis and then we only need to add the other special (ascii) ones
0229         // otherwise the pattern could become extremely long
0230         //
0231         // furthermore, we don't want to replace emojis (esp. non-colon escaped ones) in the
0232         // middle of another string, such as within a URL or such. at the same time, multiple
0233         // smileys may come after another...
0234         const auto commonPattern = QLatin1String(":[\\w\\-]+:");
0235         // TODO: use QRegularExpression::anchoredPattern once ruqola depends on Qt 5.15
0236         static const QRegularExpression common(QLatin1Char('^') + commonPattern + QLatin1Char('$'));
0237 
0238         QString pattern;
0239         QTextStream stream(&pattern);
0240         // prevent replacements within other strings, use a negative-lookbehind to rule out
0241         // that we are within some word or link or such
0242         stream << "(?<![\\w\\-:])";
0243         // put all other patterns in a non-capturing group
0244         stream << "(?:";
0245         stream << commonPattern;
0246 
0247         auto addEmoji = [&](const QString &string) {
0248             if (common.match(string).hasMatch()) {
0249                 return;
0250             }
0251             stream << '|';
0252             stream << QRegularExpression::escape(string);
0253         };
0254         auto addEmojis = [&](const auto &emojis) {
0255             for (const auto &emoji : emojis) {
0256                 addEmoji(emoji.identifier());
0257                 const auto aliases = emoji.aliases();
0258                 for (const auto &alias : aliases) {
0259                     addEmoji(alias);
0260                 }
0261             }
0262         };
0263 
0264         addEmojis(mCustomEmojiList);
0265         addEmojis(unicodeEmojiList());
0266         // close non-capturing group
0267         stream << ")";
0268         stream.flush();
0269 
0270         mReplacePattern.setPattern(pattern);
0271         mReplacePattern.optimize();
0272         mReplacePatternDirty = false;
0273     }
0274 
0275     if (mReplacePattern.pattern().isEmpty() || !mReplacePattern.isValid()) {
0276         qCWarning(RUQOLA_LOG) << "invalid emoji replace pattern" << mReplacePattern.pattern() << mReplacePattern.errorString();
0277         return;
0278     }
0279 
0280     int offset = 0;
0281     while (offset < str->size()) {
0282 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0283         const auto match = mReplacePattern.match(str, offset);
0284 #else
0285         const auto match = mReplacePattern.matchView(QStringView(*str), offset);
0286 #endif
0287         if (!match.hasMatch()) {
0288             break;
0289         }
0290         const auto word = match.captured();
0291         const auto replaceWord = replaceEmojiIdentifier(word);
0292         str->replace(match.capturedStart(), word.size(), replaceWord);
0293         offset = match.capturedStart() + replaceWord.size();
0294     }
0295 }
0296 
0297 QString EmojiManager::serverUrl() const
0298 {
0299     return mServerUrl;
0300 }
0301 
0302 void EmojiManager::setServerUrl(const QString &serverUrl)
0303 {
0304     if (mServerUrl != serverUrl) {
0305         mServerUrl = serverUrl;
0306         clearCustomEmojiCachedHtml();
0307     }
0308 }
0309 
0310 void EmojiManager::clearCustomEmojiCachedHtml()
0311 {
0312     for (int i = 0, total = mCustomEmojiList.size(); i < total; ++i) {
0313         mCustomEmojiList[i].clearCachedHtml();
0314     }
0315 }
0316 
0317 const QVector<CustomEmoji> &EmojiManager::customEmojiList() const
0318 {
0319     return mCustomEmojiList;
0320 }
0321 
0322 #include "moc_emojimanager.cpp"