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:
@@ -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
|
||||
|
||||
@@ -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": ...
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user