Respect follower emotes context, making them only available in their owner channels (#2951)

Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
Paweł 2021-07-11 11:12:49 +02:00 committed by GitHub
parent 2844c8e7e0
commit d5add46730
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 180 additions and 28 deletions

View file

@ -9,6 +9,7 @@
- Bugfix: Fixed large timeout durations in moderation buttons overlapping with usernames or other buttons. (#2865, #2921) - 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: 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: 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: Fixed founder badge not being respected by `author.subbed` filter. (#2971)
- Bugfix: Usercards on IRC will now only show user's messages. (#1780, #2979) - Bugfix: Usercards on IRC will now only show user's messages. (#1780, #2979)

View file

@ -10,6 +10,7 @@
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "util/QStringHash.hpp"
#include <QtAlgorithms> #include <QtAlgorithms>
#include <utility> #include <utility>
@ -96,14 +97,24 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
if (auto channel = dynamic_cast<TwitchChannel *>(&this->channel_)) if (auto channel = dynamic_cast<TwitchChannel *>(&this->channel_))
{ {
// account emotes
if (auto account = getApp()->accounts->twitch.getCurrent()) 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 addString(emote.first.string, TaggedString::TwitchGlobalEmote);
// emote right now }
addString(emote.string, TaggedString::Type::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);
}
} }
} }

View file

@ -23,6 +23,7 @@ class CompletionModel : public QAbstractListModel
BTTVGlobalEmote, BTTVGlobalEmote,
BTTVChannelEmote, BTTVChannelEmote,
TwitchGlobalEmote, TwitchGlobalEmote,
TwitchLocalEmote,
TwitchSubscriberEmote, TwitchSubscriberEmote,
Emoji, Emoji,
EmoteEnd, EmoteEnd,

View file

@ -40,7 +40,7 @@ void IvrApi::getBulkEmoteSets(QString emoteSetList,
QUrlQuery urlQuery; QUrlQuery urlQuery;
urlQuery.addQueryItem("set_id", emoteSetList); urlQuery.addQueryItem("set_id", emoteSetList);
this->makeRequest("twitch/emoteset", urlQuery) this->makeRequest("v2/twitch/emotes/sets", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome { .onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJsonArray(); auto root = result.parseJsonArray();

View file

@ -2,6 +2,7 @@
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "messages/Link.hpp" #include "messages/Link.hpp"
#include "providers/twitch/TwitchEmotes.hpp"
#include <boost/noncopyable.hpp> #include <boost/noncopyable.hpp>
@ -35,7 +36,7 @@ struct IvrEmoteSet {
const QString setId; const QString setId;
const QString displayName; const QString displayName;
const QString login; const QString login;
const QString id; const QString channelId;
const QString tier; const QString tier;
const QJsonArray emotes; const QJsonArray emotes;
@ -43,9 +44,9 @@ struct IvrEmoteSet {
: setId(root.value("setID").toString()) : setId(root.value("setID").toString())
, displayName(root.value("channelName").toString()) , displayName(root.value("channelName").toString())
, login(root.value("channelLogin").toString()) , login(root.value("channelLogin").toString())
, id(root.value("channelID").toString()) , channelId(root.value("channelID").toString())
, tier(root.value("tier").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 id;
const QString setId; const QString setId;
const QString url; const QString url;
const QString emoteType;
const QString imageType;
IvrEmote(QJsonObject root) explicit IvrEmote(QJsonObject root)
: code(root.value("token").toString()) : code(root.value("code").toString())
, id(root.value("id").toString()) , id(root.value("id").toString())
, setId(root.value("setID").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())
{ {
} }
}; };

View file

@ -18,6 +18,7 @@
#include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/api/Kraken.hpp" #include "providers/twitch/api/Kraken.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "util/QStringHash.hpp"
#include "util/RapidjsonHelpers.hpp" #include "util/RapidjsonHelpers.hpp"
namespace chatterino { namespace chatterino {
@ -216,7 +217,7 @@ void TwitchAccount::loadEmotes()
// Clearing emote data // Clearing emote data
auto emoteData = this->emotes_.access(); auto emoteData = this->emotes_.access();
emoteData->emoteSets.clear(); emoteData->emoteSets.clear();
emoteData->allEmoteNames.clear(); emoteData->emotes.clear();
for (auto emoteSetIt = data.emoteSets.begin(); for (auto emoteSetIt = data.emoteSets.begin();
emoteSetIt != data.emoteSets.end(); ++emoteSetIt) emoteSetIt != data.emoteSets.end(); ++emoteSetIt)
@ -245,12 +246,15 @@ void TwitchAccount::loadEmotes()
EmoteName{TwitchEmotes::cleanUpEmoteCode(code)}; EmoteName{TwitchEmotes::cleanUpEmoteCode(code)};
emoteSet->emotes.emplace_back( emoteSet->emotes.emplace_back(
TwitchEmote{id, cleanCode}); TwitchEmote{id, cleanCode});
emoteData->allEmoteNames.push_back(cleanCode);
if (!emoteSet->local)
{
auto emote = auto emote =
getApp()->emotes->twitch.getOrCreateEmote(id, code); getApp()->emotes->twitch.getOrCreateEmote(id,
code);
emoteData->emotes.emplace(code, emote); emoteData->emotes.emplace(code, emote);
} }
}
std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(), std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(),
[](const TwitchEmote &l, const TwitchEmote &r) { [](const TwitchEmote &l, const TwitchEmote &r) {
@ -345,6 +349,7 @@ void TwitchAccount::loadUserstateEmotes()
batch.join(","), batch.join(","),
[this](QJsonArray emoteSetArray) { [this](QJsonArray emoteSetArray) {
auto emoteData = this->emotes_.access(); auto emoteData = this->emotes_.access();
auto localEmoteData = this->localEmotes_.access();
for (auto emoteSet : emoteSetArray) for (auto emoteSet : emoteSetArray)
{ {
auto newUserEmoteSet = std::make_shared<EmoteSet>(); auto newUserEmoteSet = std::make_shared<EmoteSet>();
@ -360,9 +365,9 @@ void TwitchAccount::loadUserstateEmotes()
newUserEmoteSet->text = name; newUserEmoteSet->text = name;
newUserEmoteSet->channelName = ivrEmoteSet.login; 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 id = EmoteId{ivrEmote.id};
auto code = EmoteName{ivrEmote.code}; auto code = EmoteName{ivrEmote.code};
@ -371,11 +376,29 @@ void TwitchAccount::loadUserstateEmotes()
newUserEmoteSet->emotes.push_back( newUserEmoteSet->emotes.push_back(
TwitchEmote{id, cleanCode}); TwitchEmote{id, cleanCode});
emoteData->allEmoteNames.push_back(cleanCode); auto emote =
auto twitchEmote =
getApp()->emotes->twitch.getOrCreateEmote(id, code); 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(), std::sort(newUserEmoteSet->emotes.begin(),
newUserEmoteSet->emotes.end(), newUserEmoteSet->emotes.end(),
@ -397,6 +420,12 @@ SharedAccessGuard<const TwitchAccount::TwitchAccountEmoteData>
return this->emotes_.accessConst(); return this->emotes_.accessConst();
} }
SharedAccessGuard<const std::unordered_map<QString, EmoteMap>>
TwitchAccount::accessLocalEmotes() const
{
return this->localEmotes_.accessConst();
}
// AutoModActions // AutoModActions
void TwitchAccount::autoModAllow(const QString msgID, ChannelPtr channel) void TwitchAccount::autoModAllow(const QString msgID, ChannelPtr channel)
{ {
@ -510,6 +539,12 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)
getHelix()->getEmoteSetData( getHelix()->getEmoteSetData(
emoteSet->key, emoteSet->key,
[emoteSet](HelixEmoteSetData emoteSetData) { [emoteSet](HelixEmoteSetData emoteSetData) {
// Follower emotes can be only used in their origin channel
if (emoteSetData.emoteType == "follower")
{
emoteSet->local = true;
}
if (emoteSetData.ownerId.isEmpty() || if (emoteSetData.ownerId.isEmpty() ||
emoteSetData.setId != emoteSet->key) emoteSetData.setId != emoteSet->key)
{ {

View file

@ -7,6 +7,7 @@
#include "controllers/accounts/Account.hpp" #include "controllers/accounts/Account.hpp"
#include "messages/Emote.hpp" #include "messages/Emote.hpp"
#include "providers/twitch/TwitchUser.hpp" #include "providers/twitch/TwitchUser.hpp"
#include "util/QStringHash.hpp"
#include <rapidjson/document.h> #include <rapidjson/document.h>
#include <QColor> #include <QColor>
@ -62,6 +63,7 @@ public:
QString key; QString key;
QString channelName; QString channelName;
QString text; QString text;
bool local{false};
std::vector<TwitchEmote> emotes; std::vector<TwitchEmote> emotes;
}; };
@ -70,8 +72,8 @@ public:
struct TwitchAccountEmoteData { struct TwitchAccountEmoteData {
std::vector<std::shared_ptr<EmoteSet>> emoteSets; std::vector<std::shared_ptr<EmoteSet>> emoteSets;
std::vector<EmoteName> allEmoteNames; // this EmoteMap should contain all emotes available globally
// excluding locally available emotes, such as follower ones
EmoteMap emotes; EmoteMap emotes;
}; };
@ -118,6 +120,8 @@ public:
// Returns true if the newly inserted emote sets differ from the ones previously saved // Returns true if the newly inserted emote sets differ from the ones previously saved
[[nodiscard]] bool setUserstateEmoteSets(QStringList newEmoteSets); [[nodiscard]] bool setUserstateEmoteSets(QStringList newEmoteSets);
SharedAccessGuard<const TwitchAccountEmoteData> accessEmotes() const; SharedAccessGuard<const TwitchAccountEmoteData> accessEmotes() const;
SharedAccessGuard<const std::unordered_map<QString, EmoteMap>>
accessLocalEmotes() const;
// Automod actions // Automod actions
void autoModAllow(const QString msgID, ChannelPtr channel); void autoModAllow(const QString msgID, ChannelPtr channel);
@ -140,6 +144,7 @@ private:
// std::map<UserId, TwitchAccountEmoteData> emotes; // std::map<UserId, TwitchAccountEmoteData> emotes;
UniqueAccess<TwitchAccountEmoteData> emotes_; UniqueAccess<TwitchAccountEmoteData> emotes_;
UniqueAccess<std::unordered_map<QString, EmoteMap>> localEmotes_;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -4,6 +4,7 @@
#include "common/Common.hpp" #include "common/Common.hpp"
#include "common/Env.hpp" #include "common/Env.hpp"
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "common/QLogging.hpp"
#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/AccountController.hpp"
#include "controllers/notifications/NotificationController.hpp" #include "controllers/notifications/NotificationController.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
@ -21,6 +22,7 @@
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include "util/FormatTime.hpp" #include "util/FormatTime.hpp"
#include "util/PostToThread.hpp" #include "util/PostToThread.hpp"
#include "util/QStringHash.hpp"
#include "widgets/Window.hpp" #include "widgets/Window.hpp"
#include <rapidjson/document.h> #include <rapidjson/document.h>
@ -30,7 +32,6 @@
#include <QJsonValue> #include <QJsonValue>
#include <QThread> #include <QThread>
#include <QTimer> #include <QTimer>
#include "common/QLogging.hpp"
namespace chatterino { namespace chatterino {
namespace { namespace {

View file

@ -10,6 +10,7 @@
#include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/ChannelPointReward.hpp"
#include "providers/twitch/TwitchEmotes.hpp" #include "providers/twitch/TwitchEmotes.hpp"
#include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Helix.hpp"
#include "util/QStringHash.hpp"
#include <QColor> #include <QColor>
#include <QElapsedTimer> #include <QElapsedTimer>

View file

@ -793,6 +793,43 @@ void Helix::getEmoteSetData(QString emoteSetId,
.execute(); .execute();
} }
void Helix::getChannelEmotes(
QString broadcasterId,
ResultCallback<std::vector<HelixChannelEmote>> 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<HelixChannelEmote> 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) NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery)
{ {
assert(!url.startsWith("/")); assert(!url.startsWith("/"));

View file

@ -2,6 +2,7 @@
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "providers/twitch/TwitchEmotes.hpp"
#include <QJsonArray> #include <QJsonArray>
#include <QString> #include <QString>
@ -267,10 +268,31 @@ struct HelixCheermoteSet {
struct HelixEmoteSetData { struct HelixEmoteSetData {
QString setId; QString setId;
QString ownerId; QString ownerId;
QString emoteType;
explicit HelixEmoteSetData(QJsonObject jsonObject) explicit HelixEmoteSetData(QJsonObject jsonObject)
: setId(jsonObject.value("emote_set_id").toString()) : setId(jsonObject.value("emote_set_id").toString())
, ownerId(jsonObject.value("owner_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<HelixEmoteSetData> successCallback, ResultCallback<HelixEmoteSetData> successCallback,
HelixFailureCallback failureCallback); HelixFailureCallback failureCallback);
// https://dev.twitch.tv/docs/api/reference#get-channel-emotes
void getChannelEmotes(
QString broadcasterId,
ResultCallback<std::vector<HelixChannelEmote>> successCallback,
HelixFailureCallback failureCallback);
void update(QString clientId, QString oauthToken); void update(QString clientId, QString oauthToken);
static void initialize(); static void initialize();

View file

@ -165,6 +165,13 @@ URL: https://dev.twitch.tv/docs/api/reference#get-emote-sets
Used in: Used in:
- `providers/twitch/TwitchAccount.cpp` to set emoteset owner data upon loading subscriber emotes from Kraken - `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 ## TMI
The TMI api is undocumented. The TMI api is undocumented.

View file

@ -69,6 +69,12 @@ namespace {
for (const auto &set : sets) 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 // TITLE
auto channelName = set->channelName; auto channelName = set->channelName;
auto text = set->text.isEmpty() ? "Twitch" : set->text; auto text = set->text.isEmpty() ? "Twitch" : set->text;

View file

@ -80,8 +80,20 @@ void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel)
{ {
if (auto user = getApp()->accounts->twitch.getCurrent()) if (auto user = getApp()->accounts->twitch.getCurrent())
{ {
auto twitch = user->accessEmotes(); // Twitch Emotes available globally
addEmotes(emotes, twitch->emotes, text, "Twitch Emote"); 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) if (tc)