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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 498 additions and 25 deletions

View file

@ -175,6 +175,7 @@ SOURCES += \
src/providers/irc/IrcMessageBuilder.cpp \ src/providers/irc/IrcMessageBuilder.cpp \
src/providers/irc/IrcServer.cpp \ src/providers/irc/IrcServer.cpp \
src/providers/LinkResolver.cpp \ src/providers/LinkResolver.cpp \
src/providers/twitch/ChannelPointReward.cpp \
src/providers/twitch/api/Helix.cpp \ src/providers/twitch/api/Helix.cpp \
src/providers/twitch/api/Kraken.cpp \ src/providers/twitch/api/Kraken.cpp \
src/providers/twitch/IrcMessageHandler.cpp \ src/providers/twitch/IrcMessageHandler.cpp \
@ -379,6 +380,7 @@ HEADERS += \
src/providers/irc/IrcMessageBuilder.hpp \ src/providers/irc/IrcMessageBuilder.hpp \
src/providers/irc/IrcServer.hpp \ src/providers/irc/IrcServer.hpp \
src/providers/LinkResolver.hpp \ src/providers/LinkResolver.hpp \
src/providers/twitch/ChannelPointReward.hpp \
src/providers/twitch/api/Helix.hpp \ src/providers/twitch/api/Helix.hpp \
src/providers/twitch/api/Kraken.hpp \ src/providers/twitch/api/Kraken.hpp \
src/providers/twitch/EmoteValue.hpp \ src/providers/twitch/EmoteValue.hpp \

@ -1 +1 @@
Subproject commit 1c38746b05d9311e73c8c8acdfdc4d36c9c551be Subproject commit 6665ccad90461c01b7fe704a98a835953d644156

View file

@ -27,6 +27,7 @@
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include "util/IsBigEndian.hpp" #include "util/IsBigEndian.hpp"
#include "util/PostToThread.hpp" #include "util/PostToThread.hpp"
#include "util/RapidjsonHelpers.hpp"
#include "widgets/Notebook.hpp" #include "widgets/Notebook.hpp"
#include "widgets/Window.hpp" #include "widgets/Window.hpp"
#include "widgets/splits/Split.hpp" #include "widgets/splits/Split.hpp"
@ -284,6 +285,21 @@ void Application::initPubsub()
chan->deleteMessage(msg->id); chan->deleteMessage(msg->id);
}); });
this->twitch.pubsub->signals_.pointReward.redeemed.connect([&](auto &data) {
QString channelId;
if (rj::getSafe(data, "channel_id", channelId))
{
const auto &chan =
this->twitch.server->getChannelOrEmptyByID(channelId);
auto channel = dynamic_cast<TwitchChannel *>(chan.get());
channel->addChannelPointReward(ChannelPointReward(data));
}
else
{
qDebug() << "Couldn't find channel id of point reward";
}
});
this->twitch.pubsub->start(); this->twitch.pubsub->start();
auto RequestModerationActions = [=]() { auto RequestModerationActions = [=]() {

View file

@ -35,6 +35,7 @@ enum class MessageFlag : uint32_t {
Debug = (1 << 18), Debug = (1 << 18),
Similar = (1 << 19), Similar = (1 << 19),
RedeemedHighlight = (1 << 20), RedeemedHighlight = (1 << 20),
RedeemedChannelPointReward = (1 << 21),
}; };
using MessageFlags = FlagsEnum<MessageFlag>; using MessageFlags = FlagsEnum<MessageFlag>;

View file

@ -32,6 +32,7 @@ struct MessageParseArgs {
bool isSentWhisper = false; bool isSentWhisper = false;
bool trimSubscriberUsername = false; bool trimSubscriberUsername = false;
bool isStaffOrBroadcaster = false; bool isStaffOrBroadcaster = false;
QString channelPointRewardId = "";
}; };
class MessageBuilder class MessageBuilder

View file

@ -676,4 +676,43 @@ void IrcTextElement::addToContainer(MessageLayoutContainer &container,
} }
} }
LinebreakElement::LinebreakElement(MessageElementFlags flags)
: MessageElement(flags)
{
}
void LinebreakElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
if (flags.hasAny(this->getFlags()))
{
container.breakLine();
}
}
ScalingImageElement::ScalingImageElement(ImageSet images,
MessageElementFlags flags)
: MessageElement(flags)
, images_(images)
{
}
void ScalingImageElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
if (flags.hasAny(this->getFlags()))
{
const auto &image =
this->images_.getImageOrLoaded(container.getScale());
if (image->isEmpty())
return;
auto size = QSize(image->width() * container.getScale(),
image->height() * container.getScale());
container.addElement((new ImageLayoutElement(*this, image, size))
->setLink(this->getLink()));
}
}
} // namespace chatterino } // namespace chatterino

View file

@ -4,6 +4,7 @@
#include "messages/Link.hpp" #include "messages/Link.hpp"
#include "messages/MessageColor.hpp" #include "messages/MessageColor.hpp"
#include "singletons/Fonts.hpp" #include "singletons/Fonts.hpp"
#include "src/messages/ImageSet.hpp"
#include <QRect> #include <QRect>
#include <QString> #include <QString>
@ -39,6 +40,9 @@ enum class MessageElementFlag {
BttvEmoteImage = (1 << 6), BttvEmoteImage = (1 << 6),
BttvEmoteText = (1 << 7), BttvEmoteText = (1 << 7),
BttvEmote = BttvEmoteImage | BttvEmoteText, BttvEmote = BttvEmoteImage | BttvEmoteText,
ChannelPointReward = (1 << 8),
FfzEmoteImage = (1 << 10), FfzEmoteImage = (1 << 10),
FfzEmoteText = (1 << 11), FfzEmoteText = (1 << 11),
FfzEmote = FfzEmoteImage | FfzEmoteText, FfzEmote = FfzEmoteImage | FfzEmoteText,
@ -321,4 +325,26 @@ private:
std::vector<Word> words_; std::vector<Word> words_;
}; };
// Forces a linebreak
class LinebreakElement : public MessageElement
{
public:
LinebreakElement(MessageElementFlags flags);
void addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags) override;
};
// Image element which will pick the quality of the image based on ui scale
class ScalingImageElement : public MessageElement
{
public:
ScalingImageElement(ImageSet images, MessageElementFlags flags);
void addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags) override;
private:
ImageSet images_;
};
} // namespace chatterino } // namespace chatterino

View file

@ -169,7 +169,7 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
// Painting // Painting
void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex,
Selection &selection, bool isLastReadMessage, Selection &selection, bool isLastReadMessage,
bool isWindowFocused) bool isWindowFocused, bool isMentions)
{ {
auto app = getApp(); auto app = getApp();
QPixmap *pixmap = this->buffer_.get(); QPixmap *pixmap = this->buffer_.get();
@ -220,6 +220,14 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex,
app->themes->messages.disabled); app->themes->messages.disabled);
} }
if (!isMentions &&
this->message_->flags.has(MessageFlag::RedeemedChannelPointReward))
{
painter.fillRect(
0, y, this->scale_ * 4, pixmap->height(),
*ColorProvider::instance().color(ColorType::Subscription));
}
// draw selection // draw selection
if (!selection.isEmpty()) if (!selection.isEmpty())
{ {

View file

@ -47,7 +47,7 @@ public:
// Painting // Painting
void paint(QPainter &painter, int width, int y, int messageIndex, void paint(QPainter &painter, int width, int y, int messageIndex,
Selection &selection, bool isLastReadMessage, Selection &selection, bool isLastReadMessage,
bool isWindowFocused); bool isWindowFocused, bool isMentions);
void invalidateBuffer(); void invalidateBuffer();
void deleteBuffer(); void deleteBuffer();
void deleteCache(); void deleteCache();

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

View file

@ -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

View file

@ -1,4 +1,4 @@
#include "IrcMessageHandler.hpp" #include "IrcMessageHandler.hpp"
#include "Application.hpp" #include "Application.hpp"
#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/AccountController.hpp"
@ -215,6 +215,32 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
args.isStaffOrBroadcaster = true; 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); TwitchMessageBuilder builder(chan.get(), _message, args, content, isAction);
if (isSub || !builder.isIgnored()) if (isSub || !builder.isIgnored())
@ -224,7 +250,6 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
builder->flags.set(MessageFlag::Subscription); builder->flags.set(MessageFlag::Subscription);
builder->flags.unset(MessageFlag::Highlighted); builder->flags.unset(MessageFlag::Highlighted);
} }
auto msg = builder.build(); auto msg = builder.build();
IrcMessageHandler::setSimilarityFlags(msg, chan); IrcMessageHandler::setSimilarityFlags(msg, chan);
@ -399,8 +424,8 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message)
if (chan->isEmpty()) if (chan->isEmpty())
{ {
qDebug() qDebug() << "[IrcMessageHandler:handleClearMessageMessage] Twitch "
<< "[IrcMessageHandler:handleClearMessageMessage] Twitch channel" "channel"
<< chanName << "not found"; << chanName << "not found";
return; return;
} }

View file

@ -804,6 +804,26 @@ void PubSub::listenToChannelModerationActions(
this->listenToTopic(topic, account); 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, void PubSub::listenToTopic(const QString &topic,
std::shared_ptr<TwitchAccount> account) std::shared_ptr<TwitchAccount> account)
{ {
@ -1093,6 +1113,34 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
// Invoke handler function // Invoke handler function
handlerIt->second(data, topicParts[2]); 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 else
{ {
qDebug() << "Unknown topic:" << topic; qDebug() << "Unknown topic:" << topic;

View file

@ -122,6 +122,10 @@ public:
Signal<const rapidjson::Value &> received; Signal<const rapidjson::Value &> received;
Signal<const rapidjson::Value &> sent; Signal<const rapidjson::Value &> sent;
} whisper; } whisper;
struct {
Signal<rapidjson::Value &> redeemed;
} pointReward;
} signals_; } signals_;
void listenToWhispers(std::shared_ptr<TwitchAccount> account); void listenToWhispers(std::shared_ptr<TwitchAccount> account);
@ -131,6 +135,9 @@ public:
void listenToChannelModerationActions( void listenToChannelModerationActions(
const QString &channelID, std::shared_ptr<TwitchAccount> account); 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; std::vector<std::unique_ptr<rapidjson::Document>> requests;
private: private:

View file

@ -235,6 +235,50 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh)
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) void TwitchChannel::sendMessage(const QString &message)
{ {
auto app = getApp(); auto app = getApp();
@ -660,6 +704,7 @@ void TwitchChannel::refreshPubsub()
auto account = getApp()->accounts->twitch.getCurrent(); auto account = getApp()->accounts->twitch.getCurrent();
getApp()->twitch2->pubsub->listenToChannelModerationActions(roomId, getApp()->twitch2->pubsub->listenToChannelModerationActions(roomId,
account); account);
getApp()->twitch2->pubsub->listenToChannelPointRewards(roomId, account);
} }
void TwitchChannel::refreshChatters() void TwitchChannel::refreshChatters()

View file

@ -7,6 +7,7 @@
#include "common/Outcome.hpp" #include "common/Outcome.hpp"
#include "common/UniqueAccess.hpp" #include "common/UniqueAccess.hpp"
#include "common/UsernameSet.hpp" #include "common/UsernameSet.hpp"
#include "providers/twitch/ChannelPointReward.hpp"
#include "providers/twitch/TwitchEmotes.hpp" #include "providers/twitch/TwitchEmotes.hpp"
#include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Helix.hpp"
@ -108,6 +109,14 @@ public:
pajlada::Signals::NoArgSignal liveStatusChanged; pajlada::Signals::NoArgSignal liveStatusChanged;
pajlada::Signals::NoArgSignal roomModesChanged; 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: private:
struct NameOptions { struct NameOptions {
QString displayName; QString displayName;
@ -158,6 +167,7 @@ private:
UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>> UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>>
badgeSets_; // "subscribers": { "0": ... "3": ... "6": ... badgeSets_; // "subscribers": { "0": ... "3": ... "6": ...
UniqueAccess<std::vector<CheerEmoteSet>> cheerEmoteSets_; UniqueAccess<std::vector<CheerEmoteSet>> cheerEmoteSets_;
UniqueAccess<std::map<QString, ChannelPointReward>> channelPointRewards_;
bool mod_ = false; bool mod_ = false;
bool vip_ = false; bool vip_ = false;

View file

@ -192,6 +192,17 @@ MessagePtr TwitchMessageBuilder::build()
this->parseRoomID(); 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(); this->appendChannelName();
if (this->tags.contains("rm-deleted")) if (this->tags.contains("rm-deleted"))
@ -1130,4 +1141,34 @@ Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
return Success; 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 } // namespace chatterino

View file

@ -1,8 +1,9 @@
#pragma once #pragma once
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include "common/Outcome.hpp" #include "common/Outcome.hpp"
#include "messages/SharedMessageBuilder.hpp" #include "messages/SharedMessageBuilder.hpp"
#include "providers/twitch/ChannelPointReward.hpp"
#include "providers/twitch/TwitchBadge.hpp" #include "providers/twitch/TwitchBadge.hpp"
#include <IrcMessage> #include <IrcMessage>
@ -42,6 +43,9 @@ public:
void triggerHighlights() override; void triggerHighlights() override;
MessagePtr build() override; MessagePtr build() override;
static void appendChannelPointRewardMessage(
const ChannelPointReward &reward, MessageBuilder *builder);
private: private:
void parseUsernameColor() override; void parseUsernameColor() override;
void parseUsername() override; void parseUsername() override;

View file

@ -171,6 +171,7 @@ void WindowManager::updateWordTypeMask()
: MEF::NonBoldUsername); : MEF::NonBoldUsername);
flags.set(settings->lowercaseDomains ? MEF::LowercaseLink flags.set(settings->lowercaseDomains ? MEF::LowercaseLink
: MEF::OriginalLink); : MEF::OriginalLink);
flags.set(MEF::ChannelPointReward);
// update flags // update flags
MessageElementFlags newFlags = static_cast<MessageElementFlags>(flags); MessageElementFlags newFlags = static_cast<MessageElementFlags>(flags);

View file

@ -28,5 +28,22 @@ namespace rj {
return std::string(buffer.GetString()); return std::string(buffer.GetString());
} }
bool getSafeObject(rapidjson::Value &obj, const char *key,
rapidjson::Value &out)
{
if (!checkJsonValue(obj, key))
{
return false;
}
out = obj[key].Move();
return true;
}
bool checkJsonValue(const rapidjson::Value &obj, const char *key)
{
return obj.IsObject() && !obj.IsNull() && obj.HasMember(key);
}
} // namespace rj } // namespace rj
} // namespace chatterino } // namespace chatterino

View file

@ -67,20 +67,12 @@ namespace rj {
arr.PushBack(pajlada::Serialize<Type>::get(value, a), a); arr.PushBack(pajlada::Serialize<Type>::get(value, a), a);
} }
bool checkJsonValue(const rapidjson::Value &obj, const char *key);
template <typename Type> template <typename Type>
bool getSafe(const rapidjson::Value &obj, const char *key, Type &out) bool getSafe(const rapidjson::Value &obj, const char *key, Type &out)
{ {
if (!obj.IsObject()) if (!checkJsonValue(obj, key))
{
return false;
}
if (!obj.HasMember(key))
{
return false;
}
if (obj.IsNull())
{ {
return false; return false;
} }
@ -100,6 +92,9 @@ namespace rj {
return !error; return !error;
} }
bool getSafeObject(rapidjson::Value &obj, const char *key,
rapidjson::Value &out);
std::string stringify(const rapidjson::Value &value); std::string stringify(const rapidjson::Value &value);
} // namespace rj } // namespace rj

View file

@ -25,6 +25,8 @@
#include "widgets/splits/SplitContainer.hpp" #include "widgets/splits/SplitContainer.hpp"
#ifdef C_DEBUG #ifdef C_DEBUG
# include <rapidjson/document.h>
# include "providers/twitch/PubsubClient.hpp"
# include "util/SampleCheerMessages.hpp" # include "util/SampleCheerMessages.hpp"
# include "util/SampleLinks.hpp" # include "util/SampleLinks.hpp"
#endif #endif
@ -239,6 +241,12 @@ void Window::addDebugStuff()
linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should pass: )" + getValidLinks().join(' ')); linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should pass: )" + getValidLinks().join(' '));
linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should NOT pass: )" + getInvalidLinks().join(' ')); linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should NOT pass: )" + getInvalidLinks().join(' '));
linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should technically pass but we choose not to parse them: )" + getValidButIgnoredLinks().join(' ')); linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should technically pass but we choose not to parse them: )" + getValidButIgnoredLinks().join(' '));
// channel point reward test
const char *channelRewardMessage = "{ \"type\": \"MESSAGE\", \"data\": { \"topic\": \"community-points-channel-v1.11148817\", \"message\": { \"type\": \"reward-redeemed\", \"data\": { \"timestamp\": \"2020-07-13T20:19:31.430785354Z\", \"redemption\": { \"id\": \"b9628798-1b4e-4122-b2a6-031658df6755\", \"user\": { \"id\": \"91800084\", \"login\": \"cranken1337\", \"display_name\": \"cranken1337\" }, \"channel_id\": \"11148817\", \"redeemed_at\": \"2020-07-13T20:19:31.345237005Z\", \"reward\": { \"id\": \"313969fe-cc9f-4a0a-83c6-172acbd96957\", \"channel_id\": \"11148817\", \"title\": \"annoying reward pogchamp\", \"prompt\": \"\", \"cost\": 3000, \"is_user_input_required\": true, \"is_sub_only\": false, \"image\": null, \"default_image\": { \"url_1x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-1.png\", \"url_2x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-2.png\", \"url_4x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-4.png\" }, \"background_color\": \"#52ACEC\", \"is_enabled\": true, \"is_paused\": false, \"is_in_stock\": true, \"max_per_stream\": { \"is_enabled\": false, \"max_per_stream\": 0 }, \"should_redemptions_skip_request_queue\": false, \"template_id\": null, \"updated_for_indicator_at\": \"2020-01-20T04:33:33.624956679Z\" }, \"user_input\": \"wow, amazing reward\", \"status\": \"UNFULFILLED\", \"cursor\": \"Yjk2Mjg3OTgtMWI0ZS00MTIyLWIyYTYtMDMxNjU4ZGY2NzU1X18yMDIwLTA3LTEzVDIwOjE5OjMxLjM0NTIzNzAwNVo=\" } } } } }";
const char *channelRewardMessage2 = "{ \"type\": \"MESSAGE\", \"data\": { \"topic\": \"community-points-channel-v1.11148817\", \"message\": { \"type\": \"reward-redeemed\", \"data\": { \"timestamp\": \"2020-07-13T20:19:31.430785354Z\", \"redemption\": { \"id\": \"b9628798-1b4e-4122-b2a6-031658df6755\", \"user\": { \"id\": \"91800084\", \"login\": \"cranken1337\", \"display_name\": \"cranken1337\" }, \"channel_id\": \"11148817\", \"redeemed_at\": \"2020-07-13T20:19:31.345237005Z\", \"reward\": { \"id\": \"313969fe-cc9f-4a0a-83c6-172acbd96957\", \"channel_id\": \"11148817\", \"title\": \"annoying reward pogchamp\", \"prompt\": \"\", \"cost\": 3000, \"is_user_input_required\": false, \"is_sub_only\": false, \"image\": null, \"default_image\": { \"url_1x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-1.png\", \"url_2x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-2.png\", \"url_4x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-4.png\" }, \"background_color\": \"#52ACEC\", \"is_enabled\": true, \"is_paused\": false, \"is_in_stock\": true, \"max_per_stream\": { \"is_enabled\": false, \"max_per_stream\": 0 }, \"should_redemptions_skip_request_queue\": false, \"template_id\": null, \"updated_for_indicator_at\": \"2020-01-20T04:33:33.624956679Z\" }, \"status\": \"UNFULFILLED\", \"cursor\": \"Yjk2Mjg3OTgtMWI0ZS00MTIyLWIyYTYtMDMxNjU4ZGY2NzU1X18yMDIwLTA3LTEzVDIwOjE5OjMxLjM0NTIzNzAwNVo=\" } } } } }";
const char *channelRewardIRCMessage(R"(@badge-info=subscriber/43;badges=subscriber/42;color=#1E90FF;custom-reward-id=313969fe-cc9f-4a0a-83c6-172acbd96957;display-name=Cranken1337;emotes=;flags=;id=3cee3f27-a1d0-44d1-a606-722cebdad08b;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1594756484132;turbo=0;user-id=91800084;user-type= :cranken1337!cranken1337@cranken1337.tmi.twitch.tv PRIVMSG #pajlada :wow, amazing reward)");
// clang-format on // clang-format on
createWindowShortcut(this, "F6", [=] { createWindowShortcut(this, "F6", [=] {
@ -265,13 +273,28 @@ void Window::addDebugStuff()
}); });
createWindowShortcut(this, "F9", [=] { createWindowShortcut(this, "F9", [=] {
auto *dialog = new WelcomeDialog(); rapidjson::Document doc;
dialog->setAttribute(Qt::WA_DeleteOnClose); auto app = getApp();
dialog->show(); static bool alt = true;
if (alt)
{
doc.Parse(channelRewardMessage);
app->twitch.server->addFakeMessage(channelRewardIRCMessage);
app->twitch.pubsub->signals_.pointReward.redeemed.invoke(
doc["data"]["message"]["data"]["redemption"]);
alt = !alt;
}
else
{
doc.Parse(channelRewardMessage2);
app->twitch.pubsub->signals_.pointReward.redeemed.invoke(
doc["data"]["message"]["data"]["redemption"]);
alt = !alt;
}
}); });
#endif #endif
} } // namespace chatterino
void Window::addShortcuts() void Window::addShortcuts()
{ {

View file

@ -856,6 +856,7 @@ MessageElementFlags ChannelView::getFlags() const
if (this->channel_ == app->twitch.server->mentionsChannel) if (this->channel_ == app->twitch.server->mentionsChannel)
{ {
flags.set(MessageElementFlag::ChannelName); flags.set(MessageElementFlag::ChannelName);
flags.unset(MessageElementFlag::ChannelPointReward);
} }
} }
@ -902,6 +903,9 @@ void ChannelView::drawMessages(QPainter &painter)
MessageLayout *end = nullptr; MessageLayout *end = nullptr;
bool windowFocused = this->window() == QApplication::activeWindow(); bool windowFocused = this->window() == QApplication::activeWindow();
auto app = getApp();
bool isMentions = this->channel_ == app->twitch.server->mentionsChannel;
for (size_t i = start; i < messagesSnapshot.size(); ++i) for (size_t i = start; i < messagesSnapshot.size(); ++i)
{ {
MessageLayout *layout = messagesSnapshot[i].get(); MessageLayout *layout = messagesSnapshot[i].get();
@ -913,7 +917,7 @@ void ChannelView::drawMessages(QPainter &painter)
} }
layout->paint(painter, DRAW_WIDTH, y, i, this->selection_, layout->paint(painter, DRAW_WIDTH, y, i, this->selection_,
isLastMessage, windowFocused); isLastMessage, windowFocused, isMentions);
y += layout->getHeight(); y += layout->getHeight();