feat(plugins): allow message introspection (#6353)

This allows users to introspect messages (i.e. look at the elements they're made up of).

Reviewed-by: Mm2PL <mm2pl+gh@kotmisia.pl>
This commit is contained in:
nerix
2025-12-07 15:52:59 +01:00
committed by GitHub
parent 0aff6fbe83
commit 74b4674c6b
12 changed files with 1836 additions and 210 deletions
+39
View File
@@ -50,4 +50,43 @@ QString MessageColor::toString() const
}
}
QString MessageColor::toLua() const
{
switch (this->type_)
{
case Type::Custom:
return this->customColor_.name(QColor::HexArgb);
case Type::Text:
return QStringLiteral("text");
case Type::System:
return QStringLiteral("system");
case Type::Link:
return QStringLiteral("link");
default:
return {};
}
}
MessageColor MessageColor::fromLua(const QString &spec, Type fallback)
{
if (spec.isEmpty())
{
return fallback;
}
if (spec == u"text")
{
return MessageColor::Text;
}
if (spec == u"link")
{
return MessageColor::Link;
}
if (spec == u"system")
{
return MessageColor::System;
}
// custom
return QColor(spec);
}
} // namespace chatterino
+4
View File
@@ -17,6 +17,10 @@ struct MessageColor {
QString toString() const;
QString toLua() const;
static MessageColor fromLua(const QString &spec,
Type fallback = Type::Text);
bool operator==(const MessageColor &other) const noexcept
{
return this->type_ == other.type_ &&
+102 -24
View File
@@ -144,6 +144,11 @@ QJsonObject ImageElement::toJson() const
return base;
}
std::string_view ImageElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
CircularImageElement::CircularImageElement(ImagePtr image, int padding,
QColor background,
MessageElementFlags flags)
@@ -178,6 +183,11 @@ QJsonObject CircularImageElement::toJson() const
return base;
}
std::string_view CircularImageElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
// EMOTE
EmoteElement::EmoteElement(const EmotePtr &emote, MessageElementFlags flags,
const MessageColor &textElementColor)
@@ -266,6 +276,11 @@ QJsonObject EmoteElement::toJson() const
return base;
}
std::string_view EmoteElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
LayeredEmoteElement::LayeredEmoteElement(
std::vector<LayeredEmoteElement::Emote> &&emotes, MessageElementFlags flags,
const MessageColor &textElementColor)
@@ -461,6 +476,11 @@ QJsonObject LayeredEmoteElement::toJson() const
return base;
}
std::string_view LayeredEmoteElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
// BADGE
BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags)
: MessageElement(flags)
@@ -508,6 +528,11 @@ QJsonObject BadgeElement::toJson() const
return base;
}
std::string_view BadgeElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
// MOD BADGE
ModBadgeElement::ModBadgeElement(const EmotePtr &data,
MessageElementFlags flags_)
@@ -534,6 +559,11 @@ QJsonObject ModBadgeElement::toJson() const
return base;
}
std::string_view ModBadgeElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
// VIP BADGE
VipBadgeElement::VipBadgeElement(const EmotePtr &data,
MessageElementFlags flags_)
@@ -557,6 +587,11 @@ QJsonObject VipBadgeElement::toJson() const
return base;
}
std::string_view VipBadgeElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
// FFZ Badge
FfzBadgeElement::FfzBadgeElement(const EmotePtr &data,
MessageElementFlags flags_, QColor color_)
@@ -583,6 +618,11 @@ QJsonObject FfzBadgeElement::toJson() const
return base;
}
std::string_view FfzBadgeElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
// TEXT
TextElement::TextElement(const QString &text, MessageElementFlags flags,
const MessageColor &color, FontStyle style)
@@ -827,6 +867,11 @@ QJsonObject TextElement::toJson() const
return base;
}
std::string_view TextElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
SingleLineTextElement::SingleLineTextElement(const QString &text,
MessageElementFlags flags,
const MessageColor &color,
@@ -834,11 +879,8 @@ SingleLineTextElement::SingleLineTextElement(const QString &text,
: MessageElement(flags)
, color_(color)
, style_(style)
, words_(text.split(' '))
{
for (const auto &word : text.split(' '))
{
this->words_.push_back({word, -1});
}
}
void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
@@ -872,7 +914,7 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
QString currentText;
bool firstIteration = true;
for (Word &word : this->words_)
for (const auto &word : this->words_)
{
if (firstIteration)
{
@@ -885,7 +927,7 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
bool done = false;
for (const auto &parsedWord :
app->getEmotes()->getEmojis()->parse(word.text))
app->getEmotes()->getEmojis()->parse(word))
{
if (parsedWord.type() == typeid(QString))
{
@@ -958,11 +1000,7 @@ QJsonObject SingleLineTextElement::toJson() const
{
auto base = MessageElement::toJson();
base["type"_L1] = u"SingleLineTextElement"_s;
QJsonArray words;
for (const auto &word : this->words_)
{
words.append(word.text);
}
QJsonArray words = QJsonArray::fromStringList(this->words_);
base["words"_L1] = words;
base["color"_L1] = this->color_.toString();
base["style"_L1] = qmagicenum::enumNameString(this->style_);
@@ -970,6 +1008,11 @@ QJsonObject SingleLineTextElement::toJson() const
return base;
}
std::string_view SingleLineTextElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
LinkElement::LinkElement(const Parsed &parsed, const QString &fullUrl,
MessageElementFlags flags, const MessageColor &color,
FontStyle style)
@@ -1005,14 +1048,19 @@ QJsonObject LinkElement::toJson() const
return base;
}
std::string_view LinkElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
MentionElement::MentionElement(const QString &displayName, QString loginName_,
MessageColor fallbackColor_,
MessageColor userColor_)
: TextElement(displayName,
{MessageElementFlag::Text, MessageElementFlag::Mention})
, fallbackColor(fallbackColor_)
, userColor(userColor_)
, userLoginName(std::move(loginName_))
, fallbackColor_(fallbackColor_)
, userColor_(userColor_)
, userLoginName_(std::move(loginName_))
{
}
@@ -1021,9 +1069,9 @@ MentionElement::MentionElement(const QString &displayName, QString loginName_,
MessageColor fallbackColor_, QColor userColor_)
: TextElement(displayName,
{MessageElementFlag::Text, MessageElementFlag::Mention})
, fallbackColor(fallbackColor_)
, userColor(userColor_.isValid() ? userColor_ : fallbackColor_)
, userLoginName(std::move(loginName_))
, fallbackColor_(fallbackColor_)
, userColor_(userColor_.isValid() ? userColor_ : fallbackColor_)
, userLoginName_(std::move(loginName_))
{
}
@@ -1037,11 +1085,11 @@ void MentionElement::addToContainer(MessageLayoutContainer &container,
{
if (getSettings()->colorUsernames)
{
this->color_ = this->userColor;
this->color_ = this->userColor_;
}
else
{
this->color_ = this->fallbackColor;
this->color_ = this->fallbackColor_;
}
if (getSettings()->boldUsernames)
@@ -1067,26 +1115,31 @@ MessageElement *MentionElement::setLink(const Link &link)
Link MentionElement::getLink() const
{
if (this->userLoginName.isEmpty())
if (this->userLoginName_.isEmpty())
{
// Some rare mention elements don't have the knowledge of the login name
return {};
}
return {Link::UserInfo, this->userLoginName};
return {Link::UserInfo, this->userLoginName_};
}
QJsonObject MentionElement::toJson() const
{
auto base = TextElement::toJson();
base["type"_L1] = u"MentionElement"_s;
base["fallbackColor"_L1] = this->fallbackColor.toString();
base["userColor"_L1] = this->userColor.toString();
base["userLoginName"_L1] = this->userLoginName;
base["fallbackColor"_L1] = this->fallbackColor_.toString();
base["userColor"_L1] = this->userColor_.toString();
base["userLoginName"_L1] = this->userLoginName_;
return base;
}
std::string_view MentionElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
// TIMESTAMP
TimestampElement::TimestampElement()
: TimestampElement(getApp()->isTest() ? QTime::fromMSecsSinceStartOfDay(0)
@@ -1150,6 +1203,11 @@ QJsonObject TimestampElement::toJson() const
return base;
}
std::string_view TimestampElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
// TWITCH MODERATION
TwitchModerationElement::TwitchModerationElement()
: MessageElement(MessageElementFlag::ModeratorTools)
@@ -1194,6 +1252,11 @@ QJsonObject TwitchModerationElement::toJson() const
return base;
}
std::string_view TwitchModerationElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
LinebreakElement::LinebreakElement(MessageElementFlags flags)
: MessageElement(flags)
{
@@ -1216,6 +1279,11 @@ QJsonObject LinebreakElement::toJson() const
return base;
}
std::string_view LinebreakElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
ScalingImageElement::ScalingImageElement(ImageSet images,
MessageElementFlags flags)
: MessageElement(flags)
@@ -1249,6 +1317,11 @@ QJsonObject ScalingImageElement::toJson() const
return base;
}
std::string_view ScalingImageElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
ReplyCurveElement::ReplyCurveElement()
: MessageElement(MessageElementFlag::RepliedMessage)
{
@@ -1279,4 +1352,9 @@ QJsonObject ReplyCurveElement::toJson() const
return base;
}
std::string_view ReplyCurveElement::type() const
{
return std::remove_pointer_t<decltype(this)>::TYPE;
}
} // namespace chatterino
+115 -8
View File
@@ -184,6 +184,12 @@ public:
virtual QJsonObject toJson() const;
/// The type name for this message element. Used for Lua plugins.
///
/// This must be unique per element. It should return the static `TYPE`
/// member.
virtual std::string_view type() const = 0;
protected:
MessageElement(MessageElementFlags flags);
bool trailingSpace = true;
@@ -198,12 +204,15 @@ private:
class ImageElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "image";
ImageElement(ImagePtr image, MessageElementFlags flags);
void addToContainer(MessageLayoutContainer &container,
const MessageLayoutContext &ctx) override;
QJsonObject toJson() const override;
std::string_view type() const override;
private:
ImagePtr image_;
@@ -213,6 +222,8 @@ private:
class CircularImageElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "circular-image";
CircularImageElement(ImagePtr image, int padding, QColor background,
MessageElementFlags flags);
@@ -220,6 +231,16 @@ public:
const MessageLayoutContext &ctx) override;
QJsonObject toJson() const override;
std::string_view type() const override;
int padding() const
{
return this->padding_;
}
QColor background() const
{
return this->background_;
}
private:
ImagePtr image_;
@@ -231,6 +252,8 @@ private:
class TextElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "text";
TextElement(const QString &text, MessageElementFlags flags,
const MessageColor &color = MessageColor::Text,
FontStyle style = FontStyle::ChatMedium);
@@ -240,6 +263,7 @@ public:
const MessageLayoutContext &ctx) override;
QJsonObject toJson() const override;
std::string_view type() const override;
const MessageColor &color() const noexcept;
FontStyle fontStyle() const noexcept;
@@ -247,6 +271,11 @@ public:
void appendText(QStringView text);
void appendText(const QString &text);
QStringList words() const
{
return this->words_;
}
protected:
QStringList words_;
@@ -258,6 +287,8 @@ protected:
class SingleLineTextElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "single-line-text";
SingleLineTextElement(const QString &text, MessageElementFlags flags,
const MessageColor &color = MessageColor::Text,
FontStyle style = FontStyle::ChatMedium);
@@ -267,21 +298,33 @@ public:
const MessageLayoutContext &ctx) override;
QJsonObject toJson() const override;
std::string_view type() const override;
const MessageColor &color() const
{
return this->color_;
}
FontStyle fontStyle() const
{
return this->style_;
}
QStringList words() const
{
return this->words_;
}
private:
MessageColor color_;
FontStyle style_;
struct Word {
QString text;
int width = -1;
};
std::vector<Word> words_;
QStringList words_;
};
class LinkElement : public TextElement
{
public:
static constexpr std::string_view TYPE = "link";
struct Parsed {
QString lowercase;
QString original;
@@ -309,7 +352,17 @@ public:
return &this->linkInfo_;
}
QStringList lowercase() const
{
return this->lowercase_;
}
QStringList original() const
{
return this->original_;
}
QJsonObject toJson() const override;
std::string_view type() const override;
private:
LinkInfo linkInfo_;
@@ -331,6 +384,8 @@ private:
class MentionElement : public TextElement
{
public:
static constexpr std::string_view TYPE = "mention";
explicit MentionElement(const QString &displayName, QString loginName_,
MessageColor fallbackColor_,
MessageColor userColor_);
@@ -352,20 +407,34 @@ public:
MessageElement *setLink(const Link &link) override;
Link getLink() const override;
const MessageColor &fallbackColor() const
{
return this->fallbackColor_;
}
const MessageColor &userColor() const
{
return this->userColor_;
}
QString userLoginName() const
{
return this->userLoginName_;
}
QJsonObject toJson() const override;
std::string_view type() const override;
private:
/**
* The color of the element in case the "Colorize @usernames" is disabled
**/
MessageColor fallbackColor;
MessageColor fallbackColor_;
/**
* The color of the element in case the "Colorize @usernames" is enabled
**/
MessageColor userColor;
MessageColor userColor_;
QString userLoginName;
QString userLoginName_;
};
// contains emote data and will pick the emote based on :
@@ -374,6 +443,8 @@ private:
class EmoteElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "emote";
EmoteElement(const EmotePtr &data, MessageElementFlags flags_,
const MessageColor &textElementColor = MessageColor::Text);
@@ -382,6 +453,7 @@ public:
EmotePtr getEmote() const;
QJsonObject toJson() const override;
std::string_view type() const override;
protected:
virtual MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
@@ -403,6 +475,8 @@ private:
class LayeredEmoteElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "layered-emote";
struct Emote {
EmotePtr ptr;
MessageElementFlags flags;
@@ -424,6 +498,7 @@ public:
const std::vector<QString> &getEmoteTooltips() const;
QJsonObject toJson() const override;
std::string_view type() const override;
private:
MessageLayoutElement *makeImageLayoutElement(
@@ -444,6 +519,8 @@ private:
class BadgeElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "badge";
BadgeElement(const EmotePtr &data, MessageElementFlags flags_);
void addToContainer(MessageLayoutContainer &container,
@@ -452,6 +529,7 @@ public:
EmotePtr getEmote() const;
QJsonObject toJson() const override;
std::string_view type() const override;
protected:
virtual MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
@@ -464,9 +542,12 @@ private:
class ModBadgeElement : public BadgeElement
{
public:
static constexpr std::string_view TYPE = "mod-badge";
ModBadgeElement(const EmotePtr &data, MessageElementFlags flags_);
QJsonObject toJson() const override;
std::string_view type() const override;
protected:
MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
@@ -476,9 +557,12 @@ protected:
class VipBadgeElement : public BadgeElement
{
public:
static constexpr std::string_view TYPE = "vip-badge";
VipBadgeElement(const EmotePtr &data, MessageElementFlags flags_);
QJsonObject toJson() const override;
std::string_view type() const override;
protected:
MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
@@ -488,10 +572,13 @@ protected:
class FfzBadgeElement : public BadgeElement
{
public:
static constexpr std::string_view TYPE = "ffz-badge";
FfzBadgeElement(const EmotePtr &data, MessageElementFlags flags_,
QColor color_);
QJsonObject toJson() const override;
std::string_view type() const override;
protected:
MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
@@ -503,6 +590,8 @@ protected:
class TimestampElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "timestamp";
TimestampElement();
TimestampElement(QTime time_);
~TimestampElement() override = default;
@@ -513,7 +602,13 @@ public:
TextElement *formatTime(const QTime &time);
MessageElement *setLink(const Link &link) override;
QTime time() const
{
return this->time_;
}
QJsonObject toJson() const override;
std::string_view type() const override;
private:
QTime time_;
@@ -526,36 +621,45 @@ private:
class TwitchModerationElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "twitch-moderation";
TwitchModerationElement();
void addToContainer(MessageLayoutContainer &container,
const MessageLayoutContext &ctx) override;
QJsonObject toJson() const override;
std::string_view type() const override;
};
// Forces a linebreak
class LinebreakElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "linebreak";
LinebreakElement(MessageElementFlags flags);
void addToContainer(MessageLayoutContainer &container,
const MessageLayoutContext &ctx) override;
QJsonObject toJson() const override;
std::string_view type() const override;
};
// Image element which will pick the quality of the image based on ui scale
class ScalingImageElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "scaling-image";
ScalingImageElement(ImageSet images, MessageElementFlags flags);
void addToContainer(MessageLayoutContainer &container,
const MessageLayoutContext &ctx) override;
QJsonObject toJson() const override;
std::string_view type() const override;
private:
ImageSet images_;
@@ -564,12 +668,15 @@ private:
class ReplyCurveElement : public MessageElement
{
public:
static constexpr std::string_view TYPE = "reply-curve";
ReplyCurveElement();
void addToContainer(MessageLayoutContainer &container,
const MessageLayoutContext &ctx) override;
QJsonObject toJson() const override;
std::string_view type() const override;
};
} // namespace chatterino