Fixed newly uploaded subscriber emotes not being available (#2992)

Co-authored-by: Felanbird <41973452+Felanbird@users.noreply.github.com>
Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
Paweł 2021-07-17 17:18:17 +02:00 committed by GitHub
parent 91ab8b90a0
commit 7e13564c24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 214 additions and 145 deletions

View file

@ -2,6 +2,7 @@
## Unversioned
- Major: Newly uploaded Twitch emotes are once again present in emote picker and can be autocompleted with Tab as well. (#2992)
- Minor: Added autocompletion in /whispers for Twitch emotes, Global Bttv/Ffz emotes and emojis. (#2999)
- Bugfix: Fixed "smiley" emotes being unable to be "Tabbed" with autocompletion, introduced in v2.3.3. (#3010)
- Dev: Ubuntu packages are now available (#2936)

View file

@ -35,7 +35,8 @@ void IvrApi::getSubage(QString userName, QString channelName,
void IvrApi::getBulkEmoteSets(QString emoteSetList,
ResultCallback<QJsonArray> successCallback,
IvrFailureCallback failureCallback)
IvrFailureCallback failureCallback,
std::function<void()> finallyCallback)
{
QUrlQuery urlQuery;
urlQuery.addQueryItem("set_id", emoteSetList);
@ -54,6 +55,7 @@ void IvrApi::getBulkEmoteSets(QString emoteSetList,
<< QString(result.getData());
failureCallback();
})
.finally(std::move(finallyCallback))
.execute();
}

View file

@ -81,10 +81,11 @@ public:
ResultCallback<IvrSubage> resultCallback,
IvrFailureCallback failureCallback);
// https://api.ivr.fi/docs#tag/Twitch/paths/~1twitch~1emoteset/get
// https://api.ivr.fi/v2/docs/static/index.html#/Twitch/get_twitch_emotes_sets
void getBulkEmoteSets(QString emoteSetList,
ResultCallback<QJsonArray> successCallback,
IvrFailureCallback failureCallback);
IvrFailureCallback failureCallback,
std::function<void()> finallyCallback);
static void initialize();

View file

@ -497,7 +497,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
if (emoteSetsChanged)
{
currentUser->loadUserstateEmotes();
currentUser->loadUserstateEmotes([] {});
}
QString channelName;
@ -512,6 +512,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
return;
}
// Checking if currentUser is a VIP or staff member
QVariant _badges = message->tag("badges");
if (_badges.isValid())
{
@ -524,6 +525,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
}
}
// Checking if currentUser is a moderator
QVariant _mod = message->tag("mod");
if (_mod.isValid())
{
@ -535,6 +537,24 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
}
}
// This will emit only once and right after user logs in to IRC - reset emote data and reload emotes
void IrcMessageHandler::handleGlobalUserStateMessage(
Communi::IrcMessage *message)
{
auto currentUser = getApp()->accounts->twitch.getCurrent();
// set received emote-sets, this time used to initially load emotes
// NOTE: this should always return true unless we reconnect
auto emoteSetsChanged = currentUser->setUserstateEmoteSets(
message->tag("emote-sets").toString().split(","));
// We should always attempt to reload emotes even on reconnections where
// emoteSetsChanged, since we want to trigger emote reloads when
// "currentUserChanged" signal is emitted
qCDebug(chatterinoTwitch) << emoteSetsChanged << message->toData();
currentUser->loadEmotes();
}
void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
{
MessageParseArgs args;

View file

@ -30,6 +30,7 @@ public:
void handleClearChatMessage(Communi::IrcMessage *message);
void handleClearMessageMessage(Communi::IrcMessage *message);
void handleUserStateMessage(Communi::IrcMessage *message);
void handleGlobalUserStateMessage(Communi::IrcMessage *message);
void handleWhisperMessage(Communi::IrcMessage *message);
// parseUserNoticeMessage parses a single IRC USERNOTICE message into 0+

View file

@ -21,6 +21,35 @@
#include "util/QStringHash.hpp"
#include "util/RapidjsonHelpers.hpp"
namespace {
std::vector<QStringList> getEmoteSetBatches(QStringList emoteSetKeys)
{
// splitting emoteSetKeys to batches of 100, because Ivr API endpoint accepts a maximum of 100 emotesets at once
constexpr int batchSize = 100;
int batchCount = (emoteSetKeys.size() / batchSize) + 1;
std::vector<QStringList> batches;
batches.reserve(batchCount);
for (int i = 0; i < batchCount; i++)
{
QStringList batch;
int last = std::min(batchSize, emoteSetKeys.size() - batchSize * i);
for (int j = 0; j < last; j++)
{
batch.push_back(emoteSetKeys.at(j + (batchSize * i)));
}
batches.emplace_back(batch);
}
return batches;
}
} // namespace
namespace chatterino {
TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken,
const QString &oauthClient, const QString &userID)
@ -116,7 +145,7 @@ void TwitchAccount::loadBlocks()
}
},
[] {
qDebug() << "Fetching blocks failed!";
qCWarning(chatterinoTwitch) << "Fetching blocks failed!";
});
}
@ -196,76 +225,24 @@ void TwitchAccount::loadEmotes()
if (this->getOAuthClient().isEmpty() || this->getOAuthToken().isEmpty())
{
qCDebug(chatterinoTwitch) << "Missing Client ID and/or OAuth token";
return;
}
// Getting subscription emotes from kraken
getKraken()->getUserEmotes(
this,
[this](KrakenEmoteSets data) {
// no emotes available
if (data.emoteSets.isEmpty())
{
qCWarning(chatterinoTwitch)
<< "\"emoticon_sets\" either empty or not present in "
"Kraken::getUserEmotes response";
qCDebug(chatterinoTwitch)
<< "Aborted loadEmotes due to missing Client ID and/or OAuth token";
return;
}
{
// Clearing emote data
auto emoteData = this->emotes_.access();
emoteData->emoteSets.clear();
emoteData->emotes.clear();
for (auto emoteSetIt = data.emoteSets.begin();
emoteSetIt != data.emoteSets.end(); ++emoteSetIt)
{
auto emoteSet = std::make_shared<EmoteSet>();
emoteSet->key = emoteSetIt.key();
this->loadEmoteSetData(emoteSet);
for (const auto emoteArrObj : emoteSetIt.value().toArray())
{
if (!emoteArrObj.isObject())
{
qCWarning(chatterinoTwitch)
<< QString(
"Emote value from set %1 was invalid")
.arg(emoteSet->key);
continue;
}
KrakenEmote krakenEmote(emoteArrObj.toObject());
auto id = EmoteId{krakenEmote.id};
auto code = EmoteName{
TwitchEmotes::cleanUpEmoteCode(krakenEmote.code)};
emoteSet->emotes.emplace_back(TwitchEmote{id, code});
if (!emoteSet->local)
{
auto emote =
getApp()->emotes->twitch.getOrCreateEmote(id,
code);
emoteData->emotes.emplace(code, emote);
}
qCDebug(chatterinoTwitch) << "Cleared emotes!";
}
std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(),
[](const TwitchEmote &l, const TwitchEmote &r) {
return l.name.string < r.name.string;
});
emoteData->emoteSets.emplace_back(emoteSet);
}
}
// Getting userstate emotes from Ivr
this->loadUserstateEmotes();
},
[] {
// kraken request failed
// TODO(zneix): Once Helix adds Get User Emotes we could remove this hacky solution
// For now, this is necessary as Kraken's equivalent doesn't return all emotes
// See: https://twitch.uservoice.com/forums/310213-developers/suggestions/43599900
this->loadUserstateEmotes([=] {
// Fill up emoteData with emote sets that were returned in a Kraken call, but aren't present in emoteData.
this->loadKrakenEmotes();
});
}
@ -284,10 +261,11 @@ bool TwitchAccount::setUserstateEmoteSets(QStringList newEmoteSets)
return true;
}
void TwitchAccount::loadUserstateEmotes()
void TwitchAccount::loadUserstateEmotes(std::function<void()> callback)
{
if (this->userstateEmoteSets_.isEmpty())
{
callback();
return;
}
@ -303,7 +281,7 @@ void TwitchAccount::loadUserstateEmotes()
}
// filter out emote sets from userstate message, which are not in fetched emote set list
for (const auto &emoteSetKey : this->userstateEmoteSets_)
for (const auto &emoteSetKey : qAsConst(this->userstateEmoteSets_))
{
if (!krakenEmoteSetKeys.contains(emoteSetKey))
{
@ -314,54 +292,50 @@ void TwitchAccount::loadUserstateEmotes()
// return if there are no new emote sets
if (newEmoteSetKeys.isEmpty())
{
callback();
return;
}
qCDebug(chatterinoTwitch) << QString("Loading %1 emotesets from IVR: %2")
.arg(newEmoteSetKeys.size())
.arg(newEmoteSetKeys.join(", "));
// splitting newEmoteSetKeys to batches of 100, because Ivr API endpoint accepts a maximum of 100 emotesets at once
constexpr int batchSize = 100;
std::vector<QStringList> batches;
int batchCount = (newEmoteSetKeys.size() / batchSize) + 1;
batches.reserve(batchCount);
for (int i = 0; i < batchCount; i++)
{
QStringList batch;
int last = std::min(batchSize, newEmoteSetKeys.size() - batchSize * i);
for (int j = batchSize * i; j < last; j++)
{
batch.push_back(newEmoteSetKeys.at(j));
}
batches.emplace_back(batch);
}
// requesting emotes
for (const auto &batch : batches)
auto batches = getEmoteSetBatches(newEmoteSetKeys);
for (int i = 0; i < batches.size(); i++)
{
qCDebug(chatterinoTwitch)
<< QString(
"Loading %1 emotesets from IVR; batch %2/%3 (%4 sets): %5")
.arg(newEmoteSetKeys.size())
.arg(i + 1)
.arg(batches.size())
.arg(batches.at(i).size())
.arg(batches.at(i).join(","));
getIvr()->getBulkEmoteSets(
batch.join(","),
batches.at(i).join(","),
[this](QJsonArray emoteSetArray) {
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 emoteSet = std::make_shared<EmoteSet>();
IvrEmoteSet ivrEmoteSet(emoteSet.toObject());
IvrEmoteSet ivrEmoteSet(emoteSet_.toObject());
newUserEmoteSet->key = ivrEmoteSet.setId;
QString setKey = ivrEmoteSet.setId;
emoteSet->key = setKey;
auto name = ivrEmoteSet.login;
name.detach();
name[0] = name[0].toUpper();
// check if the emoteset is already in emoteData
auto isAlreadyFetched =
std::find_if(emoteData->emoteSets.begin(),
emoteData->emoteSets.end(),
[setKey](std::shared_ptr<EmoteSet> set) {
return (set->key == setKey);
});
if (isAlreadyFetched != emoteData->emoteSets.end())
{
continue;
}
newUserEmoteSet->text = name;
newUserEmoteSet->channelName = ivrEmoteSet.login;
emoteSet->channelName = ivrEmoteSet.login;
emoteSet->text = ivrEmoteSet.displayName;
for (const auto &emoteObj : ivrEmoteSet.emotes)
{
@ -371,8 +345,7 @@ void TwitchAccount::loadUserstateEmotes()
auto code = EmoteName{
TwitchEmotes::cleanUpEmoteCode(ivrEmote.code)};
newUserEmoteSet->emotes.push_back(
TwitchEmote{id, code});
emoteSet->emotes.push_back(TwitchEmote{id, code});
auto emote =
getApp()->emotes->twitch.getOrCreateEmote(id, code);
@ -380,7 +353,7 @@ void TwitchAccount::loadUserstateEmotes()
// Follower emotes can be only used in their origin channel
if (ivrEmote.emoteType == "FOLLOWER")
{
newUserEmoteSet->local = true;
emoteSet->local = true;
// EmoteMap for target channel wasn't initialized yet, doing it now
if (localEmoteData->find(ivrEmoteSet.channelId) ==
@ -398,16 +371,25 @@ void TwitchAccount::loadUserstateEmotes()
emoteData->emotes.emplace(code, emote);
}
}
std::sort(newUserEmoteSet->emotes.begin(),
newUserEmoteSet->emotes.end(),
std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(),
[](const TwitchEmote &l, const TwitchEmote &r) {
return l.name.string < r.name.string;
});
emoteData->emoteSets.emplace_back(newUserEmoteSet);
emoteData->emoteSets.emplace_back(emoteSet);
}
},
[] {
// fetching emotes failed, ivr API might be down
},
[=] {
// XXX(zneix): We check if this is the last iteration and if so, call the callback
if (i + 1 == batches.size())
{
qCDebug(chatterinoTwitch)
<< "Finished loading emotes from IVR, attempting to "
"load Kraken emotes now";
callback();
}
});
};
}
@ -517,6 +499,79 @@ void TwitchAccount::autoModDeny(const QString msgID, ChannelPtr channel)
});
}
void TwitchAccount::loadKrakenEmotes()
{
getKraken()->getUserEmotes(
this,
[this](KrakenEmoteSets data) {
// no emotes available
if (data.emoteSets.isEmpty())
{
qCWarning(chatterinoTwitch)
<< "\"emoticon_sets\" either empty or not present in "
"Kraken::getUserEmotes response";
return;
}
auto emoteData = this->emotes_.access();
for (auto emoteSetIt = data.emoteSets.begin();
emoteSetIt != data.emoteSets.end(); ++emoteSetIt)
{
auto emoteSet = std::make_shared<EmoteSet>();
QString setKey = emoteSetIt.key();
emoteSet->key = setKey;
this->loadEmoteSetData(emoteSet);
// check if the emoteset is already in emoteData
auto isAlreadyFetched = std::find_if(
emoteData->emoteSets.begin(), emoteData->emoteSets.end(),
[setKey](std::shared_ptr<EmoteSet> set) {
return (set->key == setKey);
});
if (isAlreadyFetched != emoteData->emoteSets.end())
{
continue;
}
for (const auto emoteArrObj : emoteSetIt->toArray())
{
if (!emoteArrObj.isObject())
{
qCWarning(chatterinoTwitch)
<< QString("Emote value from set %1 was invalid")
.arg(emoteSet->key);
continue;
}
KrakenEmote krakenEmote(emoteArrObj.toObject());
auto id = EmoteId{krakenEmote.id};
auto code = EmoteName{
TwitchEmotes::cleanUpEmoteCode(krakenEmote.code)};
emoteSet->emotes.emplace_back(TwitchEmote{id, code});
if (!emoteSet->local)
{
auto emote =
getApp()->emotes->twitch.getOrCreateEmote(id, code);
emoteData->emotes.emplace(code, emote);
}
}
std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(),
[](const TwitchEmote &l, const TwitchEmote &r) {
return l.name.string < r.name.string;
});
emoteData->emoteSets.emplace_back(emoteSet);
}
},
[] {
// kraken request failed
});
}
void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)
{
if (!emoteSet)
@ -546,7 +601,7 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)
if (emoteSetData.ownerId.isEmpty() ||
emoteSetData.setId != emoteSet->key)
{
qCWarning(chatterinoTwitch)
qCDebug(chatterinoTwitch)
<< QString("Failed to fetch emoteSetData for %1, assuming "
"Twitch is the owner")
.arg(emoteSet->key);

View file

@ -115,7 +115,7 @@ public:
void loadEmotes();
// loadUserstateEmotes loads emote sets that are part of the USERSTATE emote-sets key
// this function makes sure not to load emote sets that have already been loaded
void loadUserstateEmotes();
void loadUserstateEmotes(std::function<void()> callback);
// setUserStateEmoteSets sets the emote sets that were parsed from the USERSTATE emote-sets key
// Returns true if the newly inserted emote sets differ from the ones previously saved
[[nodiscard]] bool setUserstateEmoteSets(QStringList newEmoteSets);
@ -128,6 +128,7 @@ public:
void autoModDeny(const QString msgID, ChannelPtr channel);
private:
void loadKrakenEmotes();
void loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet);
QString oauthClient_;

View file

@ -60,6 +60,19 @@ void TwitchIrcServer::initializeConnection(IrcConnection *connection,
qCDebug(chatterinoTwitch) << "logging in as" << account->getUserName();
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags
// twitch.tv/commands enables a bunch of miscellaneous command capabilities. See https://dev.twitch.tv/docs/irc/commands
// twitch.tv/membership enables the JOIN/PART/NAMES commands. See https://dev.twitch.tv/docs/irc/membership
// This is enabled so we receive USERSTATE messages when joining channels / typing messages, along with the other command capabilities
QStringList caps{"twitch.tv/tags", "twitch.tv/commands"};
if (type != ConnectionType::Write)
{
caps.push_back("twitch.tv/membership");
}
connection->network()->setSkipCapabilityValidation(true);
connection->network()->setRequestedCapabilities(caps);
QString username = account->getUserName();
QString oauthToken = account->getOAuthToken();
@ -169,6 +182,10 @@ void TwitchIrcServer::readConnectionMessageReceived(
"Twitch Servers requested us to reconnect, reconnecting");
this->connect();
}
else if (command == "GLOBALUSERSTATE")
{
handler.handleGlobalUserStateMessage(message);
}
}
void TwitchIrcServer::writeConnectionMessageReceived(
@ -219,28 +236,6 @@ void TwitchIrcServer::writeConnectionMessageReceived(
}
}
void TwitchIrcServer::onReadConnected(IrcConnection *connection)
{
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
// twitch.tv/membership enables the JOIN/PART/MODE/NAMES commands. See https://dev.twitch.tv/docs/irc/membership/
// twitch.tv/commands enables a bunch of miscellaneous command capabilities. See https://dev.twitch.tv/docs/irc/commands/
// This is enabled here so we receive USERSTATE messages when joining channels
connection->sendRaw(
"CAP REQ :twitch.tv/tags twitch.tv/membership twitch.tv/commands");
AbstractIrcServer::onReadConnected(connection);
}
void TwitchIrcServer::onWriteConnected(IrcConnection *connection)
{
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
// twitch.tv/commands enables a bunch of miscellaneous command capabilities. See https://dev.twitch.tv/docs/irc/commands/
// This is enabled here so we receive USERSTATE messages when typing messages, along with the other command capabilities
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/commands");
AbstractIrcServer::onWriteConnected(connection);
}
std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
const QString &channelName)
{

View file

@ -56,9 +56,6 @@ protected:
virtual void writeConnectionMessageReceived(
Communi::IrcMessage *message) override;
virtual void onReadConnected(IrcConnection *connection) override;
virtual void onWriteConnected(IrcConnection *connection) override;
virtual std::shared_ptr<Channel> getCustomChannel(
const QString &channelname) override;

View file

@ -11,10 +11,6 @@ Emotes::Emotes()
void Emotes::initialize(Settings &settings, Paths &paths)
{
getApp()->accounts->twitch.currentUserChanged.connect([] {
getApp()->accounts->twitch.getCurrent()->loadEmotes();
});
this->emojis.load();
this->gifTimer.initialize();