feat: Add 7TV Emotes and Badges (#4002)

Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix
2022-10-16 13:22:17 +02:00
committed by GitHub
parent e8fd49aadb
commit 3e41b84ed7
29 changed files with 780 additions and 7 deletions
+76
View File
@@ -0,0 +1,76 @@
#include "providers/seventv/SeventvBadges.hpp"
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "messages/Emote.hpp"
#include <QUrl>
#include <QUrlQuery>
#include <map>
namespace chatterino {
void SeventvBadges::initialize(Settings & /*settings*/, Paths & /*paths*/)
{
this->loadSeventvBadges();
}
boost::optional<EmotePtr> SeventvBadges::getBadge(const UserId &id)
{
std::shared_lock lock(this->mutex_);
auto it = this->badgeMap_.find(id.string);
if (it != this->badgeMap_.end())
{
return this->emotes_[it->second];
}
return boost::none;
}
void SeventvBadges::loadSeventvBadges()
{
// Cosmetics will work differently in v3, until this is ready
// we'll use this endpoint.
static QUrl url("https://7tv.io/v2/cosmetics");
static QUrlQuery urlQuery;
// valid user_identifier values: "object_id", "twitch_id", "login"
urlQuery.addQueryItem("user_identifier", "twitch_id");
url.setQuery(urlQuery);
NetworkRequest(url)
.onSuccess([this](const NetworkResult &result) -> Outcome {
auto root = result.parseJson();
std::shared_lock lock(this->mutex_);
int index = 0;
for (const auto &jsonBadge : root.value("badges").toArray())
{
auto badge = jsonBadge.toObject();
auto urls = badge.value("urls").toArray();
auto emote =
Emote{EmoteName{},
ImageSet{Url{urls.at(0).toArray().at(1).toString()},
Url{urls.at(1).toArray().at(1).toString()},
Url{urls.at(2).toArray().at(1).toString()}},
Tooltip{badge.value("tooltip").toString()}, Url{}};
this->emotes_.push_back(
std::make_shared<const Emote>(std::move(emote)));
for (const auto &user : badge.value("users").toArray())
{
this->badgeMap_[user.toString()] = index;
}
++index;
}
return Success;
})
.execute();
}
} // namespace chatterino
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "common/Aliases.hpp"
#include "util/QStringHash.hpp"
#include <boost/optional.hpp>
#include <common/Singleton.hpp>
#include <memory>
#include <shared_mutex>
#include <unordered_map>
namespace chatterino {
struct Emote;
using EmotePtr = std::shared_ptr<const Emote>;
class SeventvBadges : public Singleton
{
public:
void initialize(Settings &settings, Paths &paths) override;
boost::optional<EmotePtr> getBadge(const UserId &id);
private:
void loadSeventvBadges();
// Mutex for both `badgeMap_` and `emotes_`
std::shared_mutex mutex_;
std::unordered_map<QString, int> badgeMap_;
std::vector<EmotePtr> emotes_;
};
} // namespace chatterino
+365
View File
@@ -0,0 +1,365 @@
#include "providers/seventv/SeventvEmotes.hpp"
#include "common/Common.hpp"
#include "common/NetworkRequest.hpp"
#include "common/QLogging.hpp"
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
#include "messages/ImageSet.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Settings.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QThread>
#include <utility>
/**
* # References
*
* - EmoteSet: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L8-L18
* - ActiveEmote: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L20-L27
* - EmotePartial (emoteData): https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote.model.go#L24-L34
* - ImageHost: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/model.go#L36-L39
* - ImageFile: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/model.go#L41-L48
*/
namespace {
using namespace chatterino;
// These declarations won't throw an exception.
const QString CHANNEL_HAS_NO_EMOTES("This channel has no 7TV channel emotes.");
const QString EMOTE_LINK_FORMAT("https://7tv.app/emotes/%1");
// TODO(nerix): add links to documentation (7tv.io)
const QString API_URL_USER("https://7tv.io/v3/users/twitch/%1");
const QString API_URL_GLOBAL_EMOTE_SET("https://7tv.io/v3/emote-sets/global");
struct CreateEmoteResult {
Emote emote;
EmoteId id;
EmoteName name;
bool hasImages;
};
EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id)
{
static std::unordered_map<EmoteId, std::weak_ptr<const Emote>> cache;
static std::mutex mutex;
return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id);
}
/**
* This decides whether an emote should be displayed
* as zero-width
*/
bool isZeroWidthActive(const QJsonObject &activeEmote)
{
auto flags = SeventvActiveEmoteFlags(
SeventvActiveEmoteFlag(activeEmote.value("flags").toInt()));
return flags.has(SeventvActiveEmoteFlag::ZeroWidth);
}
/**
* This is only an indicator if an emote should be added
* as zero-width or not. The user can still overwrite this.
*/
bool isZeroWidthRecommended(const QJsonObject &emoteData)
{
auto flags =
SeventvEmoteFlags(SeventvEmoteFlag(emoteData.value("flags").toInt()));
return flags.has(SeventvEmoteFlag::ZeroWidth);
}
ImageSet makeImageSet(const QJsonObject &emoteData)
{
auto host = emoteData["host"].toObject();
// "//cdn.7tv[...]"
auto baseUrl = host["url"].toString();
auto files = host["files"].toArray();
// TODO: emit four images
std::array<ImagePtr, 3> sizes;
double baseWidth = 0.0;
int nextSize = 0;
for (auto fileItem : files)
{
if (nextSize >= sizes.size())
{
break;
}
auto file = fileItem.toObject();
if (file["format"].toString() != "WEBP")
{
continue; // We only use webp
}
double width = file["width"].toDouble();
double scale = 1.0; // in relation to first image
if (baseWidth > 0.0)
{
scale = baseWidth / width;
}
else
{
// => this is the first image
baseWidth = width;
}
auto image = Image::fromUrl(
{QString("https:%1/%2").arg(baseUrl, file["name"].toString())},
scale);
sizes.at(nextSize) = image;
nextSize++;
}
if (nextSize < sizes.size())
{
// this should be really rare
// this means we didn't get all sizes of an emote
if (nextSize == 0)
{
qCDebug(chatterinoSeventv)
<< "Got file list without any eligible files";
// When this emote is typed, chatterino will crash.
return ImageSet{};
}
for (; nextSize < sizes.size(); nextSize++)
{
sizes.at(nextSize) = Image::getEmpty();
}
}
return ImageSet{sizes[0], sizes[1], sizes[2]};
}
Tooltip createTooltip(const QString &name, const QString &author, bool isGlobal)
{
return Tooltip{QString("%1<br>%2 7TV Emote<br>By: %3")
.arg(name, isGlobal ? "Global" : "Channel",
author.isEmpty() ? "<deleted>" : author)};
}
Tooltip createAliasedTooltip(const QString &name, const QString &baseName,
const QString &author, bool isGlobal)
{
return Tooltip{QString("%1<br>Alias to %2<br>%3 7TV Emote<br>By: %4")
.arg(name, baseName, isGlobal ? "Global" : "Channel",
author.isEmpty() ? "<deleted>" : author)};
}
CreateEmoteResult createEmote(const QJsonObject &activeEmote,
const QJsonObject &emoteData, bool isGlobal)
{
auto emoteId = EmoteId{activeEmote["id"].toString()};
auto emoteName = EmoteName{activeEmote["name"].toString()};
auto author =
EmoteAuthor{emoteData["owner"].toObject()["display_name"].toString()};
auto baseEmoteName = emoteData["name"].toString();
bool zeroWidth = isZeroWidthActive(activeEmote);
bool aliasedName = emoteName.string != baseEmoteName;
auto tooltip =
aliasedName ? createAliasedTooltip(emoteName.string, baseEmoteName,
author.string, isGlobal)
: createTooltip(emoteName.string, author.string, isGlobal);
auto imageSet = makeImageSet(emoteData);
auto emote = Emote({emoteName, imageSet, tooltip,
Url{EMOTE_LINK_FORMAT.arg(emoteId.string)}, zeroWidth});
return {emote, emoteId, emoteName, !emote.images.getImage1()->isEmpty()};
}
bool checkEmoteVisibility(const QJsonObject &emoteData)
{
if (!emoteData["listed"].toBool() &&
!getSettings()->showUnlistedSevenTVEmotes)
{
return false;
}
auto flags =
SeventvEmoteFlags(SeventvEmoteFlag(emoteData["flags"].toInt()));
return !flags.has(SeventvEmoteFlag::ContentTwitchDisallowed);
}
EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal)
{
auto emotes = EmoteMap();
for (const auto &activeEmoteJson : emoteSetEmotes)
{
auto activeEmote = activeEmoteJson.toObject();
auto emoteData = activeEmote["data"].toObject();
if (emoteData.empty() || !checkEmoteVisibility(emoteData))
{
continue;
}
auto result = createEmote(activeEmote, emoteData, isGlobal);
if (!result.hasImages)
{
// this shouldn't happen but if it does, it will crash,
// so we don't add the emote
qCDebug(chatterinoSeventv)
<< "Emote without images:" << activeEmote;
continue;
}
auto ptr = cachedOrMake(std::move(result.emote), result.id);
emotes[result.name] = ptr;
}
return emotes;
}
} // namespace
namespace chatterino {
SeventvEmotes::SeventvEmotes()
: global_(std::make_shared<EmoteMap>())
{
}
std::shared_ptr<const EmoteMap> SeventvEmotes::globalEmotes() const
{
return this->global_.get();
}
boost::optional<EmotePtr> SeventvEmotes::globalEmote(
const EmoteName &name) const
{
auto emotes = this->global_.get();
auto it = emotes->find(name);
if (it == emotes->end())
{
return boost::none;
}
return it->second;
}
void SeventvEmotes::loadGlobalEmotes()
{
if (!Settings::instance().enableSevenTVGlobalEmotes)
{
this->global_.set(EMPTY_EMOTE_MAP);
return;
}
qCDebug(chatterinoSeventv) << "Loading 7TV Global Emotes";
NetworkRequest(API_URL_GLOBAL_EMOTE_SET, NetworkRequestType::Get)
.timeout(30000)
.onSuccess([this](const NetworkResult &result) -> Outcome {
QJsonArray parsedEmotes = result.parseJson()["emotes"].toArray();
auto emoteMap = parseEmotes(parsedEmotes, true);
qCDebug(chatterinoSeventv)
<< "Loaded" << emoteMap.size() << "7TV Global Emotes";
this->global_.set(std::make_shared<EmoteMap>(std::move(emoteMap)));
return Success;
})
.onError([](const NetworkResult &result) {
qCWarning(chatterinoSeventv)
<< "Couldn't load 7TV global emotes" << result.getData();
})
.execute();
}
void SeventvEmotes::loadChannelEmotes(const std::weak_ptr<Channel> &channel,
const QString &channelId,
std::function<void(EmoteMap &&)> callback,
bool manualRefresh)
{
qCDebug(chatterinoSeventv)
<< "Reloading 7TV Channel Emotes" << channelId << manualRefresh;
NetworkRequest(API_URL_USER.arg(channelId), NetworkRequestType::Get)
.timeout(20000)
.onSuccess([callback = std::move(callback), channel, channelId,
manualRefresh](const NetworkResult &result) -> Outcome {
auto json = result.parseJson();
auto emoteSet = json["emote_set"].toObject();
auto parsedEmotes = emoteSet["emotes"].toArray();
auto emoteMap = parseEmotes(parsedEmotes, false);
bool hasEmotes = !emoteMap.empty();
qCDebug(chatterinoSeventv)
<< "Loaded" << emoteMap.size() << "7TV Channel Emotes for"
<< channelId << "manual refresh:" << manualRefresh;
if (hasEmotes)
{
callback(std::move(emoteMap));
}
auto shared = channel.lock();
if (!shared)
{
return Success;
}
if (manualRefresh)
{
if (hasEmotes)
{
shared->addMessage(
makeSystemMessage("7TV channel emotes reloaded."));
}
else
{
shared->addMessage(
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
}
}
return Success;
})
.onError(
[channelId, channel, manualRefresh](const NetworkResult &result) {
auto shared = channel.lock();
if (!shared)
{
return;
}
if (result.status() == 404)
{
qCWarning(chatterinoSeventv)
<< "Error occurred fetching 7TV emotes: "
<< result.parseJson();
if (manualRefresh)
{
shared->addMessage(
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
}
}
else if (result.status() == NetworkResult::timedoutStatus)
{
// TODO: Auto retry in case of a timeout, with a delay
qCWarning(chatterinoSeventv)
<< "Fetching 7TV emotes for channel" << channelId
<< "failed due to timeout";
shared->addMessage(makeSystemMessage(
"Failed to fetch 7TV channel emotes. (timed out)"));
}
else
{
qCWarning(chatterinoSeventv)
<< "Error fetching 7TV emotes for channel" << channelId
<< ", error" << result.status();
shared->addMessage(
makeSystemMessage("Failed to fetch 7TV channel "
"emotes. (unknown error)"));
}
})
.execute();
}
} // namespace chatterino
+73
View File
@@ -0,0 +1,73 @@
#pragma once
#include "boost/optional.hpp"
#include "common/Aliases.hpp"
#include "common/Atomic.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include <memory>
namespace chatterino {
// https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L29-L36
enum class SeventvActiveEmoteFlag : int64_t {
None = 0LL,
// Emote is zero-width
ZeroWidth = (1LL << 0),
// Overrides Twitch Global emotes with the same name
OverrideTwitchGlobal = (1 << 16),
// Overrides Twitch Subscriber emotes with the same name
OverrideTwitchSubscriber = (1 << 17),
// Overrides BetterTTV emotes with the same name
OverrideBetterTTV = (1 << 18),
};
// https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote.model.go#L57-L70
enum class SeventvEmoteFlag : int64_t {
None = 0LL,
// The emote is private and can only be accessed by its owner, editors and moderators
Private = 1 << 0,
// The emote was verified to be an original creation by the uploader
Authentic = (1LL << 1),
// The emote is recommended to be enabled as Zero-Width
ZeroWidth = (1LL << 8),
// Content Flags
// Sexually Suggesive
ContentSexual = (1LL << 16),
// Rapid flashing
ContentEpilepsy = (1LL << 17),
// Edgy or distasteful, may be offensive to some users
ContentEdgy = (1 << 18),
// Not allowed specifically on the Twitch platform
ContentTwitchDisallowed = (1LL << 24),
};
using SeventvActiveEmoteFlags = FlagsEnum<SeventvActiveEmoteFlag>;
using SeventvEmoteFlags = FlagsEnum<SeventvEmoteFlag>;
struct Emote;
using EmotePtr = std::shared_ptr<const Emote>;
class EmoteMap;
class SeventvEmotes final
{
public:
SeventvEmotes();
std::shared_ptr<const EmoteMap> globalEmotes() const;
boost::optional<EmotePtr> globalEmote(const EmoteName &name) const;
void loadGlobalEmotes();
static void loadChannelEmotes(const std::weak_ptr<Channel> &channel,
const QString &channelId,
std::function<void(EmoteMap &&)> callback,
bool manualRefresh);
private:
Atomic<std::shared_ptr<const EmoteMap>> global_;
};
} // namespace chatterino
+41
View File
@@ -10,6 +10,7 @@
#include "providers/RecentMessagesApi.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/bttv/LoadBttvChannelEmote.hpp"
#include "providers/seventv/SeventvEmotes.hpp"
#include "providers/twitch/IrcMessageHandler.hpp"
#include "providers/twitch/PubSubManager.hpp"
#include "providers/twitch/TwitchCommon.hpp"
@@ -83,6 +84,7 @@ TwitchChannel::TwitchChannel(const QString &name)
name)
, bttvEmotes_(std::make_shared<EmoteMap>())
, ffzEmotes_(std::make_shared<EmoteMap>())
, seventvEmotes_(std::make_shared<EmoteMap>())
, mod_(false)
{
qCDebug(chatterinoTwitch) << "[TwitchChannel" << name << "] Opened";
@@ -107,6 +109,7 @@ TwitchChannel::TwitchChannel(const QString &name)
this->refreshCheerEmotes();
this->refreshFFZChannelEmotes(false);
this->refreshBTTVChannelEmotes(false);
this->refreshSevenTVChannelEmotes(false);
});
this->connected.connect([this]() {
@@ -243,6 +246,26 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh)
manualRefresh);
}
void TwitchChannel::refreshSevenTVChannelEmotes(bool manualRefresh)
{
if (!Settings::instance().enableSevenTVChannelEmotes)
{
this->seventvEmotes_.set(EMPTY_EMOTE_MAP);
return;
}
SeventvEmotes::loadChannelEmotes(
weakOf<Channel>(this), this->roomId(),
[this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
if (auto shared = weak.lock())
{
this->seventvEmotes_.set(std::make_shared<EmoteMap>(
std::forward<decltype(emoteMap)>(emoteMap)));
}
},
manualRefresh);
}
void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward)
{
assertInGuiThread();
@@ -553,6 +576,19 @@ boost::optional<EmotePtr> TwitchChannel::ffzEmote(const EmoteName &name) const
return it->second;
}
boost::optional<EmotePtr> TwitchChannel::seventvEmote(
const EmoteName &name) const
{
auto emotes = this->seventvEmotes_.get();
auto it = emotes->find(name);
if (it == emotes->end())
{
return boost::none;
}
return it->second;
}
std::shared_ptr<const EmoteMap> TwitchChannel::bttvEmotes() const
{
return this->bttvEmotes_.get();
@@ -563,6 +599,11 @@ std::shared_ptr<const EmoteMap> TwitchChannel::ffzEmotes() const
return this->ffzEmotes_.get();
}
std::shared_ptr<const EmoteMap> TwitchChannel::seventvEmotes() const
{
return this->seventvEmotes_.get();
}
const QString &TwitchChannel::subscriptionUrl()
{
return this->subscriptionUrl_;
+5
View File
@@ -52,6 +52,7 @@ class EmoteMap;
class TwitchBadges;
class FfzEmotes;
class BttvEmotes;
class SeventvEmotes;
class TwitchIrcServer;
@@ -109,11 +110,14 @@ public:
// Emotes
boost::optional<EmotePtr> bttvEmote(const EmoteName &name) const;
boost::optional<EmotePtr> ffzEmote(const EmoteName &name) const;
boost::optional<EmotePtr> seventvEmote(const EmoteName &name) const;
std::shared_ptr<const EmoteMap> bttvEmotes() const;
std::shared_ptr<const EmoteMap> ffzEmotes() const;
std::shared_ptr<const EmoteMap> seventvEmotes() const;
virtual void refreshBTTVChannelEmotes(bool manualRefresh);
virtual void refreshFFZChannelEmotes(bool manualRefresh);
virtual void refreshSevenTVChannelEmotes(bool manualRefresh);
// Badges
boost::optional<EmotePtr> ffzCustomModBadge() const;
@@ -196,6 +200,7 @@ private:
protected:
Atomic<std::shared_ptr<const EmoteMap>> bttvEmotes_;
Atomic<std::shared_ptr<const EmoteMap>> ffzEmotes_;
Atomic<std::shared_ptr<const EmoteMap>> seventvEmotes_;
Atomic<boost::optional<EmotePtr>> ffzCustomModBadge_;
Atomic<boost::optional<EmotePtr>> ffzCustomVipBadge_;
+20
View File
@@ -54,6 +54,7 @@ void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
this->reloadBTTVGlobalEmotes();
this->reloadFFZGlobalEmotes();
this->reloadSevenTVGlobalEmotes();
/* Refresh all twitch channel's live status in bulk every 30 seconds after starting chatterino */
QObject::connect(&this->bulkLiveStatusTimer_, &QTimer::timeout, [=] {
@@ -467,6 +468,10 @@ const FfzEmotes &TwitchIrcServer::getFfzEmotes() const
{
return this->ffz;
}
const SeventvEmotes &TwitchIrcServer::getSeventvEmotes() const
{
return this->seventv_;
}
void TwitchIrcServer::reloadBTTVGlobalEmotes()
{
@@ -497,4 +502,19 @@ void TwitchIrcServer::reloadAllFFZChannelEmotes()
}
});
}
void TwitchIrcServer::reloadSevenTVGlobalEmotes()
{
this->seventv_.loadGlobalEmotes();
}
void TwitchIrcServer::reloadAllSevenTVChannelEmotes()
{
this->forEachChannel([](const auto &chan) {
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->refreshSevenTVChannelEmotes(false);
}
});
}
} // namespace chatterino
+5
View File
@@ -7,6 +7,7 @@
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/ffz/FfzEmotes.hpp"
#include "providers/irc/AbstractIrcServer.hpp"
#include "providers/seventv/SeventvEmotes.hpp"
#include <chrono>
#include <memory>
@@ -37,6 +38,8 @@ public:
void reloadAllBTTVChannelEmotes();
void reloadFFZGlobalEmotes();
void reloadAllFFZChannelEmotes();
void reloadSevenTVGlobalEmotes();
void reloadAllSevenTVChannelEmotes();
Atomic<QString> lastUserThatWhisperedMe;
@@ -49,6 +52,7 @@ public:
const BttvEmotes &getBttvEmotes() const;
const FfzEmotes &getFfzEmotes() const;
const SeventvEmotes &getSeventvEmotes() const;
protected:
virtual void initializeConnection(IrcConnection *connection,
@@ -85,6 +89,7 @@ private:
BttvEmotes bttv;
FfzEmotes ffz;
SeventvEmotes seventv_;
QTimer bulkLiveStatusTimer_;
pajlada::Signals::SignalHolder signalHolder_;
@@ -7,6 +7,7 @@
#include "messages/Message.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/ffz/FfzBadges.hpp"
#include "providers/seventv/SeventvBadges.hpp"
#include "providers/twitch/TwitchBadge.hpp"
#include "providers/twitch/TwitchBadges.hpp"
#include "providers/twitch/TwitchChannel.hpp"
@@ -280,6 +281,7 @@ MessagePtr TwitchMessageBuilder::build()
this->appendChatterinoBadges();
this->appendFfzBadges();
this->appendSeventvBadges();
this->appendUsername();
@@ -1028,6 +1030,7 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
const auto &globalBttvEmotes = app->twitch->getBttvEmotes();
const auto &globalFfzEmotes = app->twitch->getFfzEmotes();
const auto &globalSeventvEmotes = app->twitch->getSeventvEmotes();
auto flags = MessageElementFlags();
auto emote = boost::optional<EmotePtr>{};
@@ -1035,8 +1038,10 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
// Emote order:
// - FrankerFaceZ Channel
// - BetterTTV Channel
// - 7TV Channel
// - FrankerFaceZ Global
// - BetterTTV Global
// - 7TV Global
if (this->twitchChannel && (emote = this->twitchChannel->ffzEmote(name)))
{
flags = MessageElementFlag::FfzEmote;
@@ -1046,6 +1051,15 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
{
flags = MessageElementFlag::BttvEmote;
}
else if (this->twitchChannel != nullptr &&
(emote = this->twitchChannel->seventvEmote(name)))
{
flags = MessageElementFlag::SevenTVEmote;
if (emote.value()->zeroWidth)
{
flags.set(MessageElementFlag::ZeroWidthEmote);
}
}
else if ((emote = globalFfzEmotes.emote(name)))
{
flags = MessageElementFlag::FfzEmote;
@@ -1059,6 +1073,14 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
flags.set(MessageElementFlag::ZeroWidthEmote);
}
}
else if ((emote = globalSeventvEmotes.globalEmote(name)))
{
flags = MessageElementFlag::SevenTVEmote;
if (emote.value()->zeroWidth)
{
flags.set(MessageElementFlag::ZeroWidthEmote);
}
}
if (emote)
{
@@ -1217,6 +1239,14 @@ void TwitchMessageBuilder::appendFfzBadges()
}
}
void TwitchMessageBuilder::appendSeventvBadges()
{
if (auto badge = getApp()->seventvBadges->getBadge({this->userId_}))
{
this->emplace<BadgeElement>(*badge, MessageElementFlag::BadgeSevenTV);
}
}
Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
{
if (this->bitsLeft == 0)
@@ -99,6 +99,7 @@ private:
void appendTwitchBadges();
void appendChatterinoBadges();
void appendFfzBadges();
void appendSeventvBadges();
Outcome tryParseCheermote(const QString &string);
bool shouldAddModerationElements() const;