Add ability to highlight messages based on user badges (#1704)

Co-authored-by: Paweł <zneix@zneix.eu>
Co-authored-by: 23rd <23rd@vivaldi.net>
Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
Daniel
2021-05-02 18:08:08 -04:00
committed by GitHub
parent 6ab5b13017
commit f6d9fb2aac
34 changed files with 956 additions and 115 deletions
+179 -31
View File
@@ -7,60 +7,95 @@
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "common/QLogging.hpp"
#include "messages/Emote.hpp"
namespace chatterino {
TwitchBadges::TwitchBadges()
{
this->loadTwitchBadges();
}
void TwitchBadges::loadTwitchBadges()
{
assert(this->loaded_ == false);
static QString url(
"https://badges.twitch.tv/v1/badges/global/display?language=en");
NetworkRequest(url)
.onSuccess([this](auto result) -> Outcome {
auto root = result.parseJson();
auto badgeSets = this->badgeSets_.access();
auto jsonSets = root.value("badge_sets").toObject();
for (auto sIt = jsonSets.begin(); sIt != jsonSets.end(); ++sIt)
{
auto key = sIt.key();
auto versions =
sIt.value().toObject().value("versions").toObject();
auto root = result.parseJson();
auto badgeSets = this->badgeSets_.access();
for (auto vIt = versions.begin(); vIt != versions.end(); ++vIt)
auto jsonSets = root.value("badge_sets").toObject();
for (auto sIt = jsonSets.begin(); sIt != jsonSets.end(); ++sIt)
{
auto versionObj = vIt.value().toObject();
auto key = sIt.key();
auto versions =
sIt.value().toObject().value("versions").toObject();
auto emote = Emote{
{""},
ImageSet{
Image::fromUrl(
{versionObj.value("image_url_1x").toString()},
1),
Image::fromUrl(
{versionObj.value("image_url_2x").toString()},
.5),
Image::fromUrl(
{versionObj.value("image_url_4x").toString()},
.25),
},
Tooltip{versionObj.value("title").toString()},
Url{versionObj.value("click_url").toString()}};
// "title"
// "clickAction"
for (auto vIt = versions.begin(); vIt != versions.end();
++vIt)
{
auto versionObj = vIt.value().toObject();
(*badgeSets)[key][vIt.key()] =
std::make_shared<Emote>(emote);
auto emote = Emote{
{""},
ImageSet{
Image::fromUrl({versionObj.value("image_url_1x")
.toString()},
1),
Image::fromUrl({versionObj.value("image_url_2x")
.toString()},
.5),
Image::fromUrl({versionObj.value("image_url_4x")
.toString()},
.25),
},
Tooltip{versionObj.value("title").toString()},
Url{versionObj.value("click_url").toString()}};
// "title"
// "clickAction"
(*badgeSets)[key][vIt.key()] =
std::make_shared<Emote>(emote);
}
}
}
this->loaded();
return Success;
})
.onError([this](auto res) {
qCDebug(chatterinoTwitch)
<< "Error loading Twitch Badges:" << res.status();
// Despite erroring out, we still want to reach the same point
// Loaded should still be set to true to not build up an endless queue, and the quuee should still be flushed.
this->loaded();
})
.execute();
}
void TwitchBadges::loaded()
{
std::unique_lock loadedLock(this->loadedMutex_);
assert(this->loaded_ == false);
this->loaded_ = true;
// Flush callback queue
std::unique_lock queueLock(this->queueMutex_);
while (!this->callbackQueue_.empty())
{
auto callback = this->callbackQueue_.front();
this->callbackQueue_.pop();
this->getBadgeIcon(callback.first, callback.second);
}
}
boost::optional<EmotePtr> TwitchBadges::badge(const QString &set,
const QString &version) const
{
@@ -77,4 +112,117 @@ boost::optional<EmotePtr> TwitchBadges::badge(const QString &set,
return boost::none;
}
boost::optional<EmotePtr> TwitchBadges::badge(const QString &set) const
{
auto badgeSets = this->badgeSets_.access();
auto it = badgeSets->find(set);
if (it != badgeSets->end())
{
if (it->second.size() > 0)
{
return it->second.begin()->second;
}
}
return boost::none;
}
void TwitchBadges::getBadgeIcon(const QString &name, BadgeIconCallback callback)
{
{
std::shared_lock loadedLock(this->loadedMutex_);
if (!this->loaded_)
{
// Badges have not been loaded yet, store callback in a queue
std::unique_lock queueLock(this->queueMutex_);
this->callbackQueue_.push({name, std::move(callback)});
return;
}
}
{
std::shared_lock badgeLock(this->badgesMutex_);
if (this->badgesMap_.contains(name))
{
callback(name, this->badgesMap_[name]);
return;
}
}
// Split string in format "name1/version1,name2/version2" to "name1", "version1"
// If not in list+version form, name will remain the same
auto targetBadge = name.split(",").at(0).split("/");
const auto badge = targetBadge.size() == 2
? this->badge(targetBadge.at(0), targetBadge.at(1))
: this->badge(targetBadge.at(0));
if (badge)
{
this->loadEmoteImage(name, (*badge)->images.getImage3(),
std::move(callback));
}
}
void TwitchBadges::getBadgeIcon(const DisplayBadge &badge,
BadgeIconCallback callback)
{
this->getBadgeIcon(badge.badgeName(), std::move(callback));
}
void TwitchBadges::getBadgeIcons(const QList<DisplayBadge> &badges,
BadgeIconCallback callback)
{
for (const auto &item : badges)
{
this->getBadgeIcon(item, callback);
}
}
void TwitchBadges::loadEmoteImage(const QString &name, ImagePtr image,
BadgeIconCallback &&callback)
{
NetworkRequest(image->url().string)
.concurrent()
.cache()
.onSuccess([this, name, callback](auto result) -> Outcome {
auto data = result.getData();
// const cast since we are only reading from it
QBuffer buffer(const_cast<QByteArray *>(&data));
buffer.open(QIODevice::ReadOnly);
QImageReader reader(&buffer);
QImage image;
if (reader.imageCount() == 0 || !reader.read(&image))
{
return Failure;
}
auto icon = std::make_shared<QIcon>(QPixmap::fromImage(image));
{
std::unique_lock lock(this->badgesMutex_);
this->badgesMap_[name] = icon;
}
callback(name, icon);
return Success;
})
.execute();
}
TwitchBadges *TwitchBadges::instance_;
TwitchBadges *TwitchBadges::instance()
{
if (TwitchBadges::instance_ == nullptr)
{
TwitchBadges::instance_ = new TwitchBadges();
}
return TwitchBadges::instance_;
}
} // namespace chatterino
+37 -2
View File
@@ -5,8 +5,14 @@
#include <unordered_map>
#include "common/UniqueAccess.hpp"
#include "messages/Image.hpp"
#include "util/DisplayBadge.hpp"
#include "util/QStringHash.hpp"
#include "pajlada/signals/signal.hpp"
#include <shared_mutex>
namespace chatterino {
struct Emote;
@@ -17,13 +23,42 @@ class Paths;
class TwitchBadges
{
public:
void loadTwitchBadges();
using QIconPtr = std::shared_ptr<QIcon>;
using ImagePtr = std::shared_ptr<Image>;
using BadgeIconCallback = std::function<void(QString, const QIconPtr)>;
public:
static TwitchBadges *instance();
// Get badge from name and version
boost::optional<EmotePtr> badge(const QString &set,
const QString &version) const;
// Get first matching badge with name, regardless of version
boost::optional<EmotePtr> badge(const QString &set) const;
void getBadgeIcon(const QString &name, BadgeIconCallback callback);
void getBadgeIcon(const DisplayBadge &badge, BadgeIconCallback callback);
void getBadgeIcons(const QList<DisplayBadge> &badges,
BadgeIconCallback callback);
private:
static TwitchBadges *instance_;
TwitchBadges();
void loadTwitchBadges();
void loaded();
void loadEmoteImage(const QString &name, ImagePtr image,
BadgeIconCallback &&callback);
std::shared_mutex badgesMutex_;
QMap<QString, QIconPtr> badgesMap_;
std::mutex queueMutex_;
std::queue<QPair<QString, BadgeIconCallback>> callbackQueue_;
std::shared_mutex loadedMutex_;
bool loaded_ = false;
UniqueAccess<
std::unordered_map<QString, std::unordered_map<QString, EmotePtr>>>
badgeSets_; // "bits": { "100": ... "500": ...
+1 -8
View File
@@ -143,8 +143,7 @@ namespace {
}
} // namespace
TwitchChannel::TwitchChannel(const QString &name,
TwitchBadges &globalTwitchBadges, BttvEmotes &bttv,
TwitchChannel::TwitchChannel(const QString &name, BttvEmotes &bttv,
FfzEmotes &ffz)
: Channel(name, Channel::Type::Twitch)
, ChannelChatters(*static_cast<Channel *>(this))
@@ -153,7 +152,6 @@ TwitchChannel::TwitchChannel(const QString &name,
, channelUrl_("https://twitch.tv/" + name)
, popoutPlayerUrl_("https://player.twitch.tv/?parent=twitch.tv&channel=" +
name)
, globalTwitchBadges_(globalTwitchBadges)
, globalBttv_(bttv)
, globalFfz_(ffz)
, bttvEmotes_(std::make_shared<EmoteMap>())
@@ -478,11 +476,6 @@ SharedAccessGuard<const TwitchChannel::StreamStatus>
return this->streamStatus_.accessConst();
}
const TwitchBadges &TwitchChannel::globalTwitchBadges() const
{
return this->globalTwitchBadges_;
}
const BttvEmotes &TwitchChannel::globalBttv() const
{
return this->globalBttv_;
+2 -7
View File
@@ -87,7 +87,6 @@ public:
SharedAccessGuard<const StreamStatus> accessStreamStatus() const;
// Emotes
const TwitchBadges &globalTwitchBadges() const;
const BttvEmotes &globalBttv() const;
const FfzEmotes &globalFfz() const;
boost::optional<EmotePtr> bttvEmote(const EmoteName &name) const;
@@ -128,9 +127,8 @@ private:
} nameOptions;
protected:
explicit TwitchChannel(const QString &channelName,
TwitchBadges &globalTwitchBadges,
BttvEmotes &globalBttv, FfzEmotes &globalFfz);
explicit TwitchChannel(const QString &channelName, BttvEmotes &globalBttv,
FfzEmotes &globalFfz);
private:
// Methods
@@ -163,9 +161,6 @@ private:
UniqueAccess<StreamStatus> streamStatus_;
UniqueAccess<RoomModes> roomModes_;
// Emotes
TwitchBadges &globalTwitchBadges_;
protected:
BttvEmotes &globalBttv_;
FfzEmotes &globalFfz_;
+2 -3
View File
@@ -45,7 +45,6 @@ void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
});
});
this->twitchBadges.loadTwitchBadges();
this->bttv.loadEmotes();
this->ffz.loadEmotes();
}
@@ -88,8 +87,8 @@ void TwitchIrcServer::initializeConnection(IrcConnection *connection,
std::shared_ptr<Channel> TwitchIrcServer::createChannel(
const QString &channelName)
{
auto channel = std::shared_ptr<TwitchChannel>(new TwitchChannel(
channelName, this->twitchBadges, this->bttv, this->ffz));
auto channel = std::shared_ptr<TwitchChannel>(
new TwitchChannel(channelName, this->bttv, this->ffz));
channel->initialize();
channel->sendMessageSignal.connect(
-2
View File
@@ -7,7 +7,6 @@
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/ffz/FfzEmotes.hpp"
#include "providers/irc/AbstractIrcServer.hpp"
#include "providers/twitch/TwitchBadges.hpp"
#include <chrono>
#include <memory>
@@ -75,7 +74,6 @@ private:
std::chrono::steady_clock::time_point lastErrorTimeSpeed_;
std::chrono::steady_clock::time_point lastErrorTimeAmount_;
TwitchBadges twitchBadges;
BttvEmotes bttv;
FfzEmotes ffz;
@@ -1083,8 +1083,8 @@ boost::optional<EmotePtr> TwitchMessageBuilder::getTwitchBadge(
return channelBadge;
}
if (auto globalBadge = this->twitchChannel->globalTwitchBadges().badge(
badge.key_, badge.value_))
if (auto globalBadge =
TwitchBadges::instance()->badge(badge.key_, badge.value_))
{
return globalBadge;
}