feat: show BTTV Pro badges (#6625)

Reviewed-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix
2025-12-07 15:15:58 +01:00
committed by GitHub
parent ab8e0751b1
commit 0aff6fbe83
31 changed files with 394 additions and 82 deletions
+27
View File
@@ -0,0 +1,27 @@
#include "providers/bttv/BttvBadges.hpp"
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
namespace chatterino {
QString BttvBadges::idForBadge(const QJsonObject &badgeJson) const
{
return badgeJson["url"].toString();
}
EmotePtr BttvBadges::createBadge(const QString &id,
const QJsonObject & /* badgeJson */) const
{
auto emote = Emote{
.name = EmoteName{},
.images = ImageSet(Image::fromUrl(Url{id}, 18.0 / 72.0)),
.tooltip = Tooltip{"BTTV Pro"},
.homePage = Url{},
.id = EmoteId{id},
};
return std::make_shared<const Emote>(std::move(emote));
}
} // namespace chatterino
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "util/BadgeRegistry.hpp"
#include <QJsonObject>
#include <memory>
namespace chatterino {
struct Emote;
using EmotePtr = std::shared_ptr<const Emote>;
class BttvBadges : public BadgeRegistry
{
public:
BttvBadges() = default;
protected:
QString idForBadge(const QJsonObject &badgeJson) const override;
EmotePtr createBadge(const QString &id,
const QJsonObject &badgeJson) const override;
};
} // namespace chatterino
+20
View File
@@ -24,6 +24,7 @@ public:
BttvLiveUpdatesPrivate &operator=(const BttvLiveUpdatesPrivate &&) = delete;
std::shared_ptr<BttvLiveUpdateClient> makeClient();
std::shared_ptr<BttvLiveUpdateClient> anyClient();
// Contains all joined Twitch channel-ids
std::unordered_set<QString> joinedChannels;
@@ -50,6 +51,15 @@ std::shared_ptr<BttvLiveUpdateClient> BttvLiveUpdatesPrivate::makeClient()
return std::make_shared<BttvLiveUpdateClient>(this->parent);
}
std::shared_ptr<BttvLiveUpdateClient> BttvLiveUpdatesPrivate::anyClient()
{
if (this->clients().empty())
{
return nullptr;
}
return this->clients().begin()->second;
}
BttvLiveUpdates::BttvLiveUpdates(QString host)
: private_(std::make_unique<BttvLiveUpdatesPrivate>(*this, std::move(host)))
{
@@ -77,6 +87,16 @@ void BttvLiveUpdates::partChannel(const QString &id)
}
}
void BttvLiveUpdates::broadcastMe(const QString &channelID,
const QString &userID)
{
auto client = this->private_->anyClient();
if (client)
{
client->broadcastMe(channelID, userID);
}
}
void BttvLiveUpdates::stop()
{
this->private_->stop();
+2
View File
@@ -39,6 +39,8 @@ public:
*/
void joinChannel(const QString &channelID, const QString &userID);
void broadcastMe(const QString &channelID, const QString &userID);
/**
* Parts a twitch channel by its id (without any prefix like 'twitch:')
* if it's joined.
@@ -1,11 +1,14 @@
#include "providers/bttv/liveupdates/BttvLiveUpdateClient.hpp"
#include "Application.hpp"
#include "providers/bttv/BttvBadges.hpp"
#include "providers/bttv/BttvLiveUpdates.hpp"
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QStringBuilder>
namespace chatterino {
@@ -67,7 +70,25 @@ void BttvLiveUpdateClient::onMessage(const QByteArray &msg)
}
else if (eventType == "lookup_user")
{
// ignored
auto message = BttvLiveUpdateUserUpdateMessage(eventData);
if (!message.validate())
{
qCDebug(chatterinoBttv) << "Invalid user update message" << json;
return;
}
if (!message.hasBadge())
{
return;
}
auto *app = tryGetApp();
if (app)
{
auto *bttvBadges = app->getBttvBadges();
auto badgeID = bttvBadges->registerBadge(message.badgeObject);
bttvBadges->assignBadgeToUser(badgeID, UserId{message.userID});
}
}
else
{
@@ -75,4 +96,21 @@ void BttvLiveUpdateClient::onMessage(const QByteArray &msg)
}
}
void BttvLiveUpdateClient::broadcastMe(const QString &channelID,
const QString &userID)
{
QJsonObject obj{
{"name", "broadcast_me"},
{"data",
QJsonObject{
{"provider", "twitch"},
{"providerId", userID},
{"channel", QString(u"twitch:" % channelID)},
}},
};
QJsonDocument doc(obj);
this->sendText(doc.toJson(QJsonDocument::Compact));
}
} // namespace chatterino
@@ -15,6 +15,8 @@ public:
void onMessage(const QByteArray &msg) /* override */;
void broadcastMe(const QString &channelID, const QString &userID);
private:
BttvLiveUpdates &manager;
};
@@ -2,6 +2,8 @@
namespace {
using namespace Qt::Literals;
bool tryParseChannelId(QString &channelId)
{
if (!channelId.startsWith("twitch:"))
@@ -49,4 +51,21 @@ bool BttvLiveUpdateEmoteRemoveMessage::validate() const
!this->channelID.isEmpty();
}
BttvLiveUpdateUserUpdateMessage::BttvLiveUpdateUserUpdateMessage(
const QJsonObject &json)
: userID(json["providerId"_L1].toString())
, badgeObject(json["badge"_L1].toObject())
{
}
bool BttvLiveUpdateUserUpdateMessage::validate() const
{
return !this->userID.isEmpty();
}
bool BttvLiveUpdateUserUpdateMessage::hasBadge() const
{
return !this->badgeObject.isEmpty();
}
} // namespace chatterino
@@ -35,4 +35,14 @@ private:
bool badChannelID_;
};
struct BttvLiveUpdateUserUpdateMessage {
BttvLiveUpdateUserUpdateMessage(const QJsonObject &json);
QString userID;
QJsonObject badgeObject;
bool validate() const;
bool hasBadge() const;
};
} // namespace chatterino
@@ -61,6 +61,11 @@ public:
this->ws_.close();
}
void sendText(const QByteArray &data)
{
this->ws_.sendText(data);
}
protected:
/**
* @return true if this client subscribed to this subscription
+7 -51
View File
@@ -4,74 +4,30 @@
#include "messages/Image.hpp"
#include "providers/seventv/SeventvEmotes.hpp"
#include <QJsonArray>
#include <QUrl>
#include <QUrlQuery>
namespace chatterino {
std::optional<EmotePtr> SeventvBadges::getBadge(const UserId &id) const
QString SeventvBadges::idForBadge(const QJsonObject &badgeJson) const
{
std::shared_lock lock(this->mutex_);
auto it = this->badgeMap_.find(id.string);
if (it != this->badgeMap_.end())
{
return it->second;
}
return std::nullopt;
return badgeJson["id"].toString();
}
void SeventvBadges::assignBadgeToUser(const QString &badgeID,
const UserId &userID)
EmotePtr SeventvBadges::createBadge(const QString &id,
const QJsonObject &badgeJson) const
{
const std::unique_lock lock(this->mutex_);
const auto badgeIt = this->knownBadges_.find(badgeID);
if (badgeIt != this->knownBadges_.end())
{
this->badgeMap_[userID.string] = badgeIt->second;
}
}
void SeventvBadges::clearBadgeFromUser(const QString &badgeID,
const UserId &userID)
{
const std::unique_lock lock(this->mutex_);
const auto it = this->badgeMap_.find(userID.string);
if (it != this->badgeMap_.end() && it->second->id.string == badgeID)
{
this->badgeMap_.erase(userID.string);
}
}
void SeventvBadges::registerBadge(const QJsonObject &badgeJson)
{
const auto badgeID = badgeJson["id"].toString();
const std::unique_lock lock(this->mutex_);
if (this->knownBadges_.find(badgeID) != this->knownBadges_.end())
{
return;
}
auto emote = Emote{
.name = EmoteName{},
.images = SeventvEmotes::createImageSet(badgeJson, true),
.tooltip = Tooltip{badgeJson["tooltip"].toString()},
.homePage = Url{},
.id = EmoteId{badgeID},
.id = EmoteId{id},
};
if (emote.images.getImage1()->isEmpty())
{
return; // Bad images
return nullptr; // Bad images
}
this->knownBadges_[badgeID] =
std::make_shared<const Emote>(std::move(emote));
return std::make_shared<const Emote>(std::move(emote));
}
} // namespace chatterino
+7 -26
View File
@@ -1,44 +1,25 @@
#pragma once
#include "common/Aliases.hpp"
#include "util/QStringHash.hpp"
#include "util/BadgeRegistry.hpp"
#include <QJsonObject>
#include <memory>
#include <optional>
#include <shared_mutex>
#include <unordered_map>
namespace chatterino {
struct Emote;
using EmotePtr = std::shared_ptr<const Emote>;
class SeventvBadges
class SeventvBadges : public BadgeRegistry
{
public:
// Return the badge, if any, that is assigned to the user
std::optional<EmotePtr> getBadge(const UserId &id) const;
SeventvBadges() = default;
// Assign the given badge to the user
void assignBadgeToUser(const QString &badgeID, const UserId &userID);
// Remove the given badge from the user
void clearBadgeFromUser(const QString &badgeID, const UserId &userID);
// Register a new known badge
// The json object will contain all information about the badge, like its ID & its images
void registerBadge(const QJsonObject &badgeJson);
private:
// Mutex for both `badgeMap_` and `knownBadges_`
mutable std::shared_mutex mutex_;
// user-id => badge
std::unordered_map<QString, EmotePtr> badgeMap_;
// badge-id => badge
std::unordered_map<QString, EmotePtr> knownBadges_;
protected:
QString idForBadge(const QJsonObject &badgeJson) const override;
EmotePtr createBadge(const QString &id,
const QJsonObject &badgeJson) const override;
};
} // namespace chatterino
+26
View File
@@ -831,6 +831,7 @@ void TwitchChannel::sendMessage(const QString &message)
bool messageSent = false;
this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent);
this->updateBttvActivity();
this->updateSevenTVActivity();
if (messageSent)
@@ -2256,6 +2257,31 @@ std::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string) const
return std::nullopt;
}
void TwitchChannel::updateBttvActivity()
{
if (!getApp()->getBttvLiveUpdates())
{
return;
}
auto now = QDateTime::currentDateTimeUtc();
if (this->nextBttvActivity_.isValid() && now < this->nextBttvActivity_)
{
return;
}
this->nextBttvActivity_ = now.addSecs(60);
qCDebug(chatterinoBttv) << "Sending activity in" << this->getName();
auto acc = getApp()->getAccounts()->twitch.getCurrent();
if (acc->isAnon())
{
return;
}
getApp()->getBttvLiveUpdates()->broadcastMe(this->roomId(),
acc->getUserId());
}
void TwitchChannel::updateSevenTVActivity()
{
static const QString seventvActivityUrl =
+5
View File
@@ -377,6 +377,9 @@ private:
/** Joins (subscribes to) a Twitch channel for updates on BTTV. */
void joinBttvChannel() const;
void updateBttvActivity();
/**
* Indicates an activity to 7TV in this channel for this user.
* This is done at most once every 60s.
@@ -514,6 +517,8 @@ private:
*/
QDateTime nextSeventvActivity_;
QDateTime nextBttvActivity_;
/** The platform of the last live emote update ("7TV", "BTTV", "FFZ"). */
QString lastLiveUpdateEmotePlatform_;
/** The actor name of the last live emote update. */