Add support for non-highlight channel point rewards (#1809)

This commit is contained in:
Jonas Schmitt
2020-08-08 15:37:22 +02:00
committed by GitHub
parent 1bd3d10eef
commit 11b8948290
23 changed files with 498 additions and 25 deletions
+127
View File
@@ -0,0 +1,127 @@
#include "ChannelPointReward.hpp"
#include "util/RapidjsonHelpers.hpp"
namespace chatterino {
QString parseRewardImage(const rapidjson::Value &obj, const char *key,
bool &result)
{
QString url;
if (!(result = rj::getSafe(obj, key, url)))
{
qDebug() << "No url value found for key in reward image object:" << key;
return "";
}
return url;
}
ChannelPointReward::ChannelPointReward(rapidjson::Value &redemption)
{
rapidjson::Value user;
if (!(this->hasParsedSuccessfully =
rj::getSafeObject(redemption, "user", user)))
{
qDebug() << "No user info found for redemption";
return;
}
rapidjson::Value reward;
if (!(this->hasParsedSuccessfully =
rj::getSafeObject(redemption, "reward", reward)))
{
qDebug() << "No reward info found for redemption";
return;
}
if (!(this->hasParsedSuccessfully = rj::getSafe(reward, "id", this->id)))
{
qDebug() << "No id found for reward";
return;
}
if (!(this->hasParsedSuccessfully =
rj::getSafe(reward, "channel_id", this->channelId)))
{
qDebug() << "No channel_id found for reward";
return;
}
if (!(this->hasParsedSuccessfully =
rj::getSafe(reward, "title", this->title)))
{
qDebug() << "No title found for reward";
return;
}
if (!(this->hasParsedSuccessfully =
rj::getSafe(reward, "cost", this->cost)))
{
qDebug() << "No cost found for reward";
return;
}
if (!(this->hasParsedSuccessfully = rj::getSafe(
reward, "is_user_input_required", this->isUserInputRequired)))
{
qDebug() << "No information if user input is required found for reward";
return;
}
// We don't need to store user information for rewards with user input
// because we will get the user info from a corresponding IRC message
if (!this->isUserInputRequired)
{
this->parseUser(user);
}
rapidjson::Value obj;
if (rj::getSafeObject(reward, "image", obj) && !obj.IsNull() &&
obj.IsObject())
{
this->image = ImageSet{
Image::fromUrl(
{parseRewardImage(obj, "url_1x", this->hasParsedSuccessfully)},
1),
Image::fromUrl(
{parseRewardImage(obj, "url_2x", this->hasParsedSuccessfully)},
0.5),
Image::fromUrl(
{parseRewardImage(obj, "url_4x", this->hasParsedSuccessfully)},
0.25),
};
}
else
{
static const ImageSet defaultImage{
Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("1.png")}, 1),
Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("2.png")}, 0.5),
Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("4.png")}, 0.25)};
this->image = defaultImage;
}
}
void ChannelPointReward::parseUser(rapidjson::Value &user)
{
if (!(this->hasParsedSuccessfully = rj::getSafe(user, "id", this->user.id)))
{
qDebug() << "No id found for user in reward";
return;
}
if (!(this->hasParsedSuccessfully =
rj::getSafe(user, "login", this->user.login)))
{
qDebug() << "No login name found for user in reward";
return;
}
if (!(this->hasParsedSuccessfully =
rj::getSafe(user, "display_name", this->user.displayName)))
{
qDebug() << "No display name found for user in reward";
return;
}
}
} // namespace chatterino
@@ -0,0 +1,33 @@
#pragma once
#include "common/Aliases.hpp"
#include "messages/Image.hpp"
#include "messages/ImageSet.hpp"
#define TWITCH_CHANNEL_POINT_REWARD_URL(x) \
QString("https://static-cdn.jtvnw.net/custom-reward-images/default-%1") \
.arg(x)
namespace chatterino {
struct ChannelPointReward {
ChannelPointReward(rapidjson::Value &reward);
ChannelPointReward() = delete;
QString id;
QString channelId;
QString title;
int cost;
ImageSet image;
bool hasParsedSuccessfully = false;
bool isUserInputRequired = false;
struct {
QString id;
QString login;
QString displayName;
} user;
private:
void parseUser(rapidjson::Value &user);
};
} // namespace chatterino
+30 -5
View File
@@ -1,4 +1,4 @@
#include "IrcMessageHandler.hpp"
#include "IrcMessageHandler.hpp"
#include "Application.hpp"
#include "controllers/accounts/AccountController.hpp"
@@ -215,6 +215,32 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
args.isStaffOrBroadcaster = true;
}
auto channel = dynamic_cast<TwitchChannel *>(chan.get());
const auto &tags = _message->tags();
if (const auto &it = tags.find("custom-reward-id"); it != tags.end())
{
const auto rewardId = it.value().toString();
if (!channel->isChannelPointRewardKnown(rewardId))
{
// Need to wait for pubsub reward notification
auto clone = _message->clone();
channel->channelPointRewardAdded.connect(
[=, &server](ChannelPointReward reward) {
if (reward.id == rewardId)
{
this->addMessage(clone, target, content, server, isSub,
isAction);
clone->deleteLater();
return true;
}
return false;
});
return;
}
args.channelPointRewardId = rewardId;
}
TwitchMessageBuilder builder(chan.get(), _message, args, content, isAction);
if (isSub || !builder.isIgnored())
@@ -224,7 +250,6 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
builder->flags.set(MessageFlag::Subscription);
builder->flags.unset(MessageFlag::Highlighted);
}
auto msg = builder.build();
IrcMessageHandler::setSimilarityFlags(msg, chan);
@@ -399,9 +424,9 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message)
if (chan->isEmpty())
{
qDebug()
<< "[IrcMessageHandler:handleClearMessageMessage] Twitch channel"
<< chanName << "not found";
qDebug() << "[IrcMessageHandler:handleClearMessageMessage] Twitch "
"channel"
<< chanName << "not found";
return;
}
+48
View File
@@ -804,6 +804,26 @@ void PubSub::listenToChannelModerationActions(
this->listenToTopic(topic, account);
}
void PubSub::listenToChannelPointRewards(const QString &channelID,
std::shared_ptr<TwitchAccount> account)
{
static const QString topicFormat("community-points-channel-v1.%1");
assert(!channelID.isEmpty());
assert(account != nullptr);
QString userID = account->getUserId();
auto topic = topicFormat.arg(channelID);
if (this->isListeningToTopic(topic))
{
return;
}
qDebug() << "Listen to topic" << topic;
this->listenToTopic(topic, account);
}
void PubSub::listenToTopic(const QString &topic,
std::shared_ptr<TwitchAccount> account)
{
@@ -1093,6 +1113,34 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
// Invoke handler function
handlerIt->second(data, topicParts[2]);
}
else if (topic.startsWith("community-points-channel-v1."))
{
std::string pointEventType;
if (!rj::getSafe(msg, "type", pointEventType))
{
qDebug() << "Bad channel point event data";
return;
}
if (pointEventType == "reward-redeemed")
{
if (!rj::getSafeObject(msg, "data", msg))
{
qDebug() << "No data found for redeemed reward";
return;
}
if (!rj::getSafeObject(msg, "redemption", msg))
{
qDebug() << "No redemption info found for redeemed reward";
return;
}
this->signals_.pointReward.redeemed.invoke(msg);
}
else
{
qDebug() << "Invalid point event type:" << pointEventType.c_str();
}
}
else
{
qDebug() << "Unknown topic:" << topic;
+7
View File
@@ -122,6 +122,10 @@ public:
Signal<const rapidjson::Value &> received;
Signal<const rapidjson::Value &> sent;
} whisper;
struct {
Signal<rapidjson::Value &> redeemed;
} pointReward;
} signals_;
void listenToWhispers(std::shared_ptr<TwitchAccount> account);
@@ -131,6 +135,9 @@ public:
void listenToChannelModerationActions(
const QString &channelID, std::shared_ptr<TwitchAccount> account);
void listenToChannelPointRewards(const QString &channelID,
std::shared_ptr<TwitchAccount> account);
std::vector<std::unique_ptr<rapidjson::Document>> requests;
private:
+45
View File
@@ -235,6 +235,50 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh)
manualRefresh);
}
void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward)
{
if (!reward.hasParsedSuccessfully)
{
return;
}
if (!reward.isUserInputRequired)
{
MessageBuilder builder;
TwitchMessageBuilder::appendChannelPointRewardMessage(reward, &builder);
this->addMessage(builder.release());
return;
}
bool result;
{
auto channelPointRewards = this->channelPointRewards_.access();
result = channelPointRewards->try_emplace(reward.id, reward).second;
}
if (result)
{
this->channelPointRewardAdded.invoke(reward);
}
}
bool TwitchChannel::isChannelPointRewardKnown(const QString &rewardId)
{
const auto &pointRewards = this->channelPointRewards_.accessConst();
const auto &it = pointRewards->find(rewardId);
return it != pointRewards->end();
}
boost::optional<ChannelPointReward> TwitchChannel::channelPointReward(
const QString &rewardId) const
{
auto rewards = this->channelPointRewards_.accessConst();
auto it = rewards->find(rewardId);
if (it == rewards->end())
return boost::none;
return it->second;
}
void TwitchChannel::sendMessage(const QString &message)
{
auto app = getApp();
@@ -660,6 +704,7 @@ void TwitchChannel::refreshPubsub()
auto account = getApp()->accounts->twitch.getCurrent();
getApp()->twitch2->pubsub->listenToChannelModerationActions(roomId,
account);
getApp()->twitch2->pubsub->listenToChannelPointRewards(roomId, account);
}
void TwitchChannel::refreshChatters()
+10
View File
@@ -7,6 +7,7 @@
#include "common/Outcome.hpp"
#include "common/UniqueAccess.hpp"
#include "common/UsernameSet.hpp"
#include "providers/twitch/ChannelPointReward.hpp"
#include "providers/twitch/TwitchEmotes.hpp"
#include "providers/twitch/api/Helix.hpp"
@@ -108,6 +109,14 @@ public:
pajlada::Signals::NoArgSignal liveStatusChanged;
pajlada::Signals::NoArgSignal roomModesChanged;
// Channel point rewards
pajlada::Signals::SelfDisconnectingSignal<ChannelPointReward>
channelPointRewardAdded;
void addChannelPointReward(const ChannelPointReward &reward);
bool isChannelPointRewardKnown(const QString &rewardId);
boost::optional<ChannelPointReward> channelPointReward(
const QString &rewardId) const;
private:
struct NameOptions {
QString displayName;
@@ -158,6 +167,7 @@ private:
UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>>
badgeSets_; // "subscribers": { "0": ... "3": ... "6": ...
UniqueAccess<std::vector<CheerEmoteSet>> cheerEmoteSets_;
UniqueAccess<std::map<QString, ChannelPointReward>> channelPointRewards_;
bool mod_ = false;
bool vip_ = false;
@@ -192,6 +192,17 @@ MessagePtr TwitchMessageBuilder::build()
this->parseRoomID();
// If it is a reward it has to be appended first
if (this->args.channelPointRewardId != "")
{
const auto &reward = this->twitchChannel->channelPointReward(
this->args.channelPointRewardId);
if (reward)
{
this->appendChannelPointRewardMessage(reward.get(), this);
}
}
this->appendChannelName();
if (this->tags.contains("rm-deleted"))
@@ -1130,4 +1141,34 @@ Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
return Success;
}
void TwitchMessageBuilder::appendChannelPointRewardMessage(
const ChannelPointReward &reward, MessageBuilder *builder)
{
QString redeemed = "Redeemed";
if (!reward.isUserInputRequired)
{
builder->emplace<TextElement>(
reward.user.login, MessageElementFlag::ChannelPointReward,
MessageColor::Text, FontStyle::ChatMediumBold);
redeemed = "redeemed";
}
builder->emplace<TextElement>(redeemed,
MessageElementFlag::ChannelPointReward);
builder->emplace<TextElement>(
reward.title, MessageElementFlag::ChannelPointReward,
MessageColor::Text, FontStyle::ChatMediumBold);
builder->emplace<ScalingImageElement>(
reward.image, MessageElementFlag::ChannelPointReward);
builder->emplace<TextElement>(
QString::number(reward.cost), MessageElementFlag::ChannelPointReward,
MessageColor::Text, FontStyle::ChatMediumBold);
if (reward.isUserInputRequired)
{
builder->emplace<LinebreakElement>(
MessageElementFlag::ChannelPointReward);
}
builder->message().flags.set(MessageFlag::RedeemedChannelPointReward);
}
} // namespace chatterino
@@ -1,8 +1,9 @@
#pragma once
#pragma once
#include "common/Aliases.hpp"
#include "common/Outcome.hpp"
#include "messages/SharedMessageBuilder.hpp"
#include "providers/twitch/ChannelPointReward.hpp"
#include "providers/twitch/TwitchBadge.hpp"
#include <IrcMessage>
@@ -42,6 +43,9 @@ public:
void triggerHighlights() override;
MessagePtr build() override;
static void appendChannelPointRewardMessage(
const ChannelPointReward &reward, MessageBuilder *builder);
private:
void parseUsernameColor() override;
void parseUsername() override;