Add support for non-highlight channel point rewards (#1809)
This commit is contained in:
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user