From d5add4673083ba8830850ef5a4e73d8d454503b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Sun, 11 Jul 2021 11:12:49 +0200 Subject: [PATCH] Respect follower emotes context, making them only available in their owner channels (#2951) Co-authored-by: pajlada --- CHANGELOG.md | 1 + src/common/CompletionModel.cpp | 21 ++++++-- src/common/CompletionModel.hpp | 1 + src/providers/IvrApi.cpp | 2 +- src/providers/IvrApi.hpp | 19 ++++--- src/providers/twitch/TwitchAccount.cpp | 57 +++++++++++++++++---- src/providers/twitch/TwitchAccount.hpp | 9 +++- src/providers/twitch/TwitchChannel.cpp | 3 +- src/providers/twitch/TwitchChannel.hpp | 1 + src/providers/twitch/api/Helix.cpp | 37 +++++++++++++ src/providers/twitch/api/Helix.hpp | 28 ++++++++++ src/providers/twitch/api/README.md | 7 +++ src/widgets/dialogs/EmotePopup.cpp | 6 +++ src/widgets/splits/InputCompletionPopup.cpp | 16 +++++- 14 files changed, 180 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ed219c..ecffd568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Bugfix: Fixed large timeout durations in moderation buttons overlapping with usernames or other buttons. (#2865, #2921) - Bugfix: Middle mouse click no longer scrolls in not fully populated usercards and splits. (#2933) - Bugfix: Fix bad behavior of the HTML color picker edit when user input is being entered. (#2942) +- Bugfix: Made follower emotes suggested (in emote popup menu, tab completion, emote input menu) only in their origin channel, not globally. (#2951) - Bugfix: Fixed founder badge not being respected by `author.subbed` filter. (#2971) - Bugfix: Usercards on IRC will now only show user's messages. (#1780, #2979) diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index 61e61d35..1f16e6a2 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -10,6 +10,7 @@ #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" +#include "util/QStringHash.hpp" #include #include @@ -96,14 +97,24 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) if (auto channel = dynamic_cast(&this->channel_)) { - // account emotes if (auto account = getApp()->accounts->twitch.getCurrent()) { - for (const auto &emote : account->accessEmotes()->allEmoteNames) + // Twitch Emotes available globally + for (const auto &emote : account->accessEmotes()->emotes) { - // XXX: No way to discern between a twitch global emote and sub - // emote right now - addString(emote.string, TaggedString::Type::TwitchGlobalEmote); + addString(emote.first.string, TaggedString::TwitchGlobalEmote); + } + + // Twitch Emotes available locally + auto localEmoteData = account->accessLocalEmotes(); + if (localEmoteData->find(channel->roomId()) != + localEmoteData->end()) + { + for (const auto &emote : localEmoteData->at(channel->roomId())) + { + addString(emote.first.string, + TaggedString::Type::TwitchLocalEmote); + } } } diff --git a/src/common/CompletionModel.hpp b/src/common/CompletionModel.hpp index 2ce37439..fee84b60 100644 --- a/src/common/CompletionModel.hpp +++ b/src/common/CompletionModel.hpp @@ -23,6 +23,7 @@ class CompletionModel : public QAbstractListModel BTTVGlobalEmote, BTTVChannelEmote, TwitchGlobalEmote, + TwitchLocalEmote, TwitchSubscriberEmote, Emoji, EmoteEnd, diff --git a/src/providers/IvrApi.cpp b/src/providers/IvrApi.cpp index 3e5ac4dd..dff138ef 100644 --- a/src/providers/IvrApi.cpp +++ b/src/providers/IvrApi.cpp @@ -40,7 +40,7 @@ void IvrApi::getBulkEmoteSets(QString emoteSetList, QUrlQuery urlQuery; urlQuery.addQueryItem("set_id", emoteSetList); - this->makeRequest("twitch/emoteset", urlQuery) + this->makeRequest("v2/twitch/emotes/sets", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto root = result.parseJsonArray(); diff --git a/src/providers/IvrApi.hpp b/src/providers/IvrApi.hpp index 857a4869..095d86d1 100644 --- a/src/providers/IvrApi.hpp +++ b/src/providers/IvrApi.hpp @@ -2,6 +2,7 @@ #include "common/NetworkRequest.hpp" #include "messages/Link.hpp" +#include "providers/twitch/TwitchEmotes.hpp" #include @@ -35,7 +36,7 @@ struct IvrEmoteSet { const QString setId; const QString displayName; const QString login; - const QString id; + const QString channelId; const QString tier; const QJsonArray emotes; @@ -43,9 +44,9 @@ struct IvrEmoteSet { : setId(root.value("setID").toString()) , displayName(root.value("channelName").toString()) , login(root.value("channelLogin").toString()) - , id(root.value("channelID").toString()) + , channelId(root.value("channelID").toString()) , tier(root.value("tier").toString()) - , emotes(root.value("emotes").toArray()) + , emotes(root.value("emoteList").toArray()) { } @@ -56,12 +57,18 @@ struct IvrEmote { const QString id; const QString setId; const QString url; + const QString emoteType; + const QString imageType; - IvrEmote(QJsonObject root) - : code(root.value("token").toString()) + explicit IvrEmote(QJsonObject root) + : code(root.value("code").toString()) , id(root.value("id").toString()) , setId(root.value("setID").toString()) - , url(root.value("url_3x").toString()) + , url(QString(TWITCH_EMOTE_TEMPLATE) + .replace("{id}", this->id) + .replace("{scale}", "3.0")) + , emoteType(root.value("type").toString()) + , imageType(root.value("assetType").toString()) { } }; diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 580ee89b..d4862d22 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -18,6 +18,7 @@ #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Kraken.hpp" #include "singletons/Emotes.hpp" +#include "util/QStringHash.hpp" #include "util/RapidjsonHelpers.hpp" namespace chatterino { @@ -216,7 +217,7 @@ void TwitchAccount::loadEmotes() // Clearing emote data auto emoteData = this->emotes_.access(); emoteData->emoteSets.clear(); - emoteData->allEmoteNames.clear(); + emoteData->emotes.clear(); for (auto emoteSetIt = data.emoteSets.begin(); emoteSetIt != data.emoteSets.end(); ++emoteSetIt) @@ -245,11 +246,14 @@ void TwitchAccount::loadEmotes() EmoteName{TwitchEmotes::cleanUpEmoteCode(code)}; emoteSet->emotes.emplace_back( TwitchEmote{id, cleanCode}); - emoteData->allEmoteNames.push_back(cleanCode); - auto emote = - getApp()->emotes->twitch.getOrCreateEmote(id, code); - emoteData->emotes.emplace(code, emote); + if (!emoteSet->local) + { + auto emote = + getApp()->emotes->twitch.getOrCreateEmote(id, + code); + emoteData->emotes.emplace(code, emote); + } } std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(), @@ -345,6 +349,7 @@ void TwitchAccount::loadUserstateEmotes() batch.join(","), [this](QJsonArray emoteSetArray) { auto emoteData = this->emotes_.access(); + auto localEmoteData = this->localEmotes_.access(); for (auto emoteSet : emoteSetArray) { auto newUserEmoteSet = std::make_shared(); @@ -360,9 +365,9 @@ void TwitchAccount::loadUserstateEmotes() newUserEmoteSet->text = name; newUserEmoteSet->channelName = ivrEmoteSet.login; - for (const auto &emote : ivrEmoteSet.emotes) + for (const auto &emoteObj : ivrEmoteSet.emotes) { - IvrEmote ivrEmote(emote.toObject()); + IvrEmote ivrEmote(emoteObj.toObject()); auto id = EmoteId{ivrEmote.id}; auto code = EmoteName{ivrEmote.code}; @@ -371,11 +376,29 @@ void TwitchAccount::loadUserstateEmotes() newUserEmoteSet->emotes.push_back( TwitchEmote{id, cleanCode}); - emoteData->allEmoteNames.push_back(cleanCode); - - auto twitchEmote = + auto emote = getApp()->emotes->twitch.getOrCreateEmote(id, code); - emoteData->emotes.emplace(code, twitchEmote); + + // Follower emotes can be only used in their origin channel + if (ivrEmote.emoteType == "FOLLOWER") + { + newUserEmoteSet->local = true; + + // EmoteMap for target channel wasn't initialized yet, doing it now + if (localEmoteData->find(ivrEmoteSet.channelId) == + localEmoteData->end()) + { + localEmoteData->emplace(ivrEmoteSet.channelId, + EmoteMap()); + } + + localEmoteData->at(ivrEmoteSet.channelId) + .emplace(code, emote); + } + else + { + emoteData->emotes.emplace(code, emote); + } } std::sort(newUserEmoteSet->emotes.begin(), newUserEmoteSet->emotes.end(), @@ -397,6 +420,12 @@ SharedAccessGuard return this->emotes_.accessConst(); } +SharedAccessGuard> + TwitchAccount::accessLocalEmotes() const +{ + return this->localEmotes_.accessConst(); +} + // AutoModActions void TwitchAccount::autoModAllow(const QString msgID, ChannelPtr channel) { @@ -510,6 +539,12 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr emoteSet) getHelix()->getEmoteSetData( emoteSet->key, [emoteSet](HelixEmoteSetData emoteSetData) { + // Follower emotes can be only used in their origin channel + if (emoteSetData.emoteType == "follower") + { + emoteSet->local = true; + } + if (emoteSetData.ownerId.isEmpty() || emoteSetData.setId != emoteSet->key) { diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index fc1918b1..89df716f 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -7,6 +7,7 @@ #include "controllers/accounts/Account.hpp" #include "messages/Emote.hpp" #include "providers/twitch/TwitchUser.hpp" +#include "util/QStringHash.hpp" #include #include @@ -62,6 +63,7 @@ public: QString key; QString channelName; QString text; + bool local{false}; std::vector emotes; }; @@ -70,8 +72,8 @@ public: struct TwitchAccountEmoteData { std::vector> emoteSets; - std::vector allEmoteNames; - + // this EmoteMap should contain all emotes available globally + // excluding locally available emotes, such as follower ones EmoteMap emotes; }; @@ -118,6 +120,8 @@ public: // Returns true if the newly inserted emote sets differ from the ones previously saved [[nodiscard]] bool setUserstateEmoteSets(QStringList newEmoteSets); SharedAccessGuard accessEmotes() const; + SharedAccessGuard> + accessLocalEmotes() const; // Automod actions void autoModAllow(const QString msgID, ChannelPtr channel); @@ -140,6 +144,7 @@ private: // std::map emotes; UniqueAccess emotes_; + UniqueAccess> localEmotes_; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 2e49f5d2..4a3fe0d8 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -4,6 +4,7 @@ #include "common/Common.hpp" #include "common/Env.hpp" #include "common/NetworkRequest.hpp" +#include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/notifications/NotificationController.hpp" #include "messages/Message.hpp" @@ -21,6 +22,7 @@ #include "singletons/WindowManager.hpp" #include "util/FormatTime.hpp" #include "util/PostToThread.hpp" +#include "util/QStringHash.hpp" #include "widgets/Window.hpp" #include @@ -30,7 +32,6 @@ #include #include #include -#include "common/QLogging.hpp" namespace chatterino { namespace { diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index af115799..6b6daa3c 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -10,6 +10,7 @@ #include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/TwitchEmotes.hpp" #include "providers/twitch/api/Helix.hpp" +#include "util/QStringHash.hpp" #include #include diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index c430b59c..2fe734a5 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -793,6 +793,43 @@ void Helix::getEmoteSetData(QString emoteSetId, .execute(); } +void Helix::getChannelEmotes( + QString broadcasterId, + ResultCallback> successCallback, + HelixFailureCallback failureCallback) +{ + QUrlQuery urlQuery; + urlQuery.addQueryItem("broadcaster_id", broadcasterId); + + this->makeRequest("chat/emotes", urlQuery) + .onSuccess([successCallback, + failureCallback](NetworkResult result) -> Outcome { + QJsonObject root = result.parseJson(); + auto data = root.value("data"); + + if (!data.isArray()) + { + failureCallback(); + return Failure; + } + + std::vector channelEmotes; + + for (const auto &jsonStream : data.toArray()) + { + channelEmotes.emplace_back(jsonStream.toObject()); + } + + successCallback(channelEmotes); + return Success; + }) + .onError([failureCallback](auto result) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) { assert(!url.startsWith("/")); diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index a4f745a7..0b437765 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -2,6 +2,7 @@ #include "common/Aliases.hpp" #include "common/NetworkRequest.hpp" +#include "providers/twitch/TwitchEmotes.hpp" #include #include @@ -267,10 +268,31 @@ struct HelixCheermoteSet { struct HelixEmoteSetData { QString setId; QString ownerId; + QString emoteType; explicit HelixEmoteSetData(QJsonObject jsonObject) : setId(jsonObject.value("emote_set_id").toString()) , ownerId(jsonObject.value("owner_id").toString()) + , emoteType(jsonObject.value("emote_type").toString()) + { + } +}; + +struct HelixChannelEmote { + const QString emoteId; + const QString name; + const QString type; + const QString setId; + const QString url; + + explicit HelixChannelEmote(QJsonObject jsonObject) + : emoteId(jsonObject.value("id").toString()) + , name(jsonObject.value("name").toString()) + , type(jsonObject.value("emote_type").toString()) + , setId(jsonObject.value("emote_set_id").toString()) + , url(QString(TWITCH_EMOTE_TEMPLATE) + .replace("{id}", this->emoteId) + .replace("{scale}", "3.0")) { } }; @@ -414,6 +436,12 @@ public: ResultCallback successCallback, HelixFailureCallback failureCallback); + // https://dev.twitch.tv/docs/api/reference#get-channel-emotes + void getChannelEmotes( + QString broadcasterId, + ResultCallback> successCallback, + HelixFailureCallback failureCallback); + void update(QString clientId, QString oauthToken); static void initialize(); diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index 2a4a2ded..15c52cca 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -165,6 +165,13 @@ URL: https://dev.twitch.tv/docs/api/reference#get-emote-sets Used in: - `providers/twitch/TwitchAccount.cpp` to set emoteset owner data upon loading subscriber emotes from Kraken +### Get Channel Emotes + +URL: https://dev.twitch.tv/docs/api/reference#get-channel-emotes + +- We implement this in `providers/twitch/api/Helix.cpp getChannelEmotes` + Not used anywhere at the moment. + ## TMI The TMI api is undocumented. diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index ce839b4b..3ca63ca1 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -69,6 +69,12 @@ namespace { for (const auto &set : sets) { + // Some emotes (e.g. follower ones) are only available in their origin channel + if (set->local && currentChannelName != set->channelName) + { + continue; + } + // TITLE auto channelName = set->channelName; auto text = set->text.isEmpty() ? "Twitch" : set->text; diff --git a/src/widgets/splits/InputCompletionPopup.cpp b/src/widgets/splits/InputCompletionPopup.cpp index 689aa1c8..e8781197 100644 --- a/src/widgets/splits/InputCompletionPopup.cpp +++ b/src/widgets/splits/InputCompletionPopup.cpp @@ -80,8 +80,20 @@ void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel) { if (auto user = getApp()->accounts->twitch.getCurrent()) { - auto twitch = user->accessEmotes(); - addEmotes(emotes, twitch->emotes, text, "Twitch Emote"); + // Twitch Emotes available globally + auto emoteData = user->accessEmotes(); + addEmotes(emotes, emoteData->emotes, text, "Twitch Emote"); + + // Twitch Emotes available locally + auto localEmoteData = user->accessLocalEmotes(); + if (localEmoteData->find(tc->roomId()) != localEmoteData->end()) + { + if (auto localEmotes = &localEmoteData->at(tc->roomId())) + { + addEmotes(emotes, *localEmotes, text, + "Local Twitch Emotes"); + } + } } if (tc)