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
+596 -76
View File
@@ -27,29 +27,6 @@ QDateTime datetimeFromOffset(qint64 offset)
return dt;
}
MessageColor tryMakeMessageColor(const QString &name,
MessageColor fallback = MessageColor::Text)
{
if (name.isEmpty())
{
return fallback;
}
if (name == u"text")
{
return MessageColor::Text;
}
if (name == u"link")
{
return MessageColor::Link;
}
if (name == u"system")
{
return MessageColor::System;
}
// custom
return QColor(name);
}
template <typename T>
T requiredGet(const sol::table &tbl, auto &&key)
{
@@ -67,7 +44,7 @@ std::unique_ptr<TextElement> textElementFromTable(const sol::table &tbl)
return std::make_unique<TextElement>(
requiredGet<QString>(tbl, "text"),
tbl.get_or("flags", MessageElementFlag::Text),
tryMakeMessageColor(tbl.get_or("color", QString{})),
MessageColor::fromLua(tbl.get_or("color", QString{})),
tbl.get_or("style", FontStyle::ChatMedium));
}
@@ -77,7 +54,7 @@ std::unique_ptr<SingleLineTextElement> singleLineTextElementFromTable(
return std::make_unique<SingleLineTextElement>(
requiredGet<QString>(tbl, "text"),
tbl.get_or("flags", MessageElementFlag::Text),
tryMakeMessageColor(tbl.get_or("color", QString{})),
MessageColor::fromLua(tbl.get_or("color", QString{})),
tbl.get_or("style", FontStyle::ChatMedium));
}
@@ -87,8 +64,8 @@ std::unique_ptr<MentionElement> mentionElementFromTable(const sol::table &tbl)
return std::make_unique<MentionElement>(
requiredGet<QString>(tbl, "display_name"),
requiredGet<QString>(tbl, "login_name"),
tryMakeMessageColor(requiredGet<QString>(tbl, "fallback_color")),
tryMakeMessageColor(requiredGet<QString>(tbl, "user_color")));
MessageColor::fromLua(requiredGet<QString>(tbl, "fallback_color")),
MessageColor::fromLua(requiredGet<QString>(tbl, "user_color")));
}
std::unique_ptr<TimestampElement> timestampElementFromTable(
@@ -123,37 +100,75 @@ std::unique_ptr<ReplyCurveElement> replyCurveElementFromTable()
return std::make_unique<ReplyCurveElement>();
}
void setLinkOn(MessageElement *el, const Link &link)
{
el->setLink(link);
QString tooltip;
switch (link.type)
{
case Link::Url:
tooltip = QString("<b>URL:</b> %1").arg(link.value);
break;
case Link::UserAction:
tooltip = QString("<b>Command:</b> %1").arg(link.value);
break;
case Link::CopyToClipboard:
tooltip = "<b>Copy to clipboard</b>";
break;
// these links should be safe to click as they don't have any immediate action associated with them
case Link::InsertText:
case Link::JumpToChannel:
case Link::JumpToMessage:
case Link::UserInfo:
case Link::UserWhisper:
case Link::ReplyToMessage:
break;
// these types are not exposed to plugins
case Link::None:
case Link::AutoModAllow:
case Link::AutoModDeny:
case Link::OpenAccountsPage:
case Link::Reconnect:
case Link::ViewThread:
throw std::runtime_error("Invalid link type. How'd this happen?");
}
el->setTooltip(tooltip);
}
std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
{
auto type = requiredGet<QString>(tbl, "type");
auto type = requiredGet<std::string>(tbl, "type");
std::unique_ptr<MessageElement> el;
bool linksAllowed = true;
if (type == u"text")
if (type == TextElement::TYPE)
{
el = textElementFromTable(tbl);
}
else if (type == u"single-line-text")
else if (type == SingleLineTextElement::TYPE)
{
el = singleLineTextElementFromTable(tbl);
}
else if (type == u"mention")
else if (type == MentionElement::TYPE)
{
el = mentionElementFromTable(tbl);
linksAllowed = false;
}
else if (type == u"timestamp")
else if (type == TimestampElement::TYPE)
{
el = timestampElementFromTable(tbl);
}
else if (type == u"twitch-moderation")
else if (type == TwitchModerationElement::TYPE)
{
el = twitchModerationElementFromTable();
}
else if (type == u"linebreak")
else if (type == LinebreakElement::TYPE)
{
el = linebreakElementFromTable(tbl);
}
else if (type == u"reply-curve")
else if (type == ReplyCurveElement::TYPE)
{
el = replyCurveElementFromTable();
linksAllowed = false;
@@ -171,44 +186,10 @@ std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
{
if (!linksAllowed)
{
throw std::runtime_error("'link' not supported on type='" +
type.toStdString() + '\'');
throw std::runtime_error("'link' not supported on type='" + type +
'\'');
}
el->setLink(*link);
QString tooltip;
switch (link->type)
{
case Link::Url:
tooltip = QString("<b>URL:</b> %1").arg(link->value);
break;
case Link::UserAction:
tooltip = QString("<b>Command:</b> %1").arg(link->value);
break;
case Link::CopyToClipboard:
tooltip = "<b>Copy to clipboard</b>";
break;
// these links should be safe to click as they don't have any immediate action associated with them
case Link::InsertText:
case Link::JumpToChannel:
case Link::JumpToMessage:
case Link::UserInfo:
case Link::UserWhisper:
case Link::ReplyToMessage:
break;
// these types are not exposed to plugins
case Link::None:
case Link::AutoModAllow:
case Link::AutoModDeny:
case Link::OpenAccountsPage:
case Link::ViewThread:
case Link::Reconnect:
throw std::runtime_error(
"Invalid link type. How'd this happen?");
}
el->setTooltip(tooltip);
setLinkOn(el.get(), *link);
}
else
{
@@ -282,16 +263,555 @@ std::shared_ptr<Message> messageFromTable(const sol::table &tbl)
return msg;
}
void checkWritable(Message *msg)
{
if (msg->frozen)
{
throw std::runtime_error("Message is frozen");
}
}
template <typename T>
struct MemberPtrTraits;
template <class T, class C>
struct MemberPtrTraits<T C::*> {
using Type = T;
using Object = C;
};
template <auto T>
decltype(auto) memberAccessor()
{
return sol::property(
[](Message *msg) {
return msg->*T;
},
[](Message *msg, typename MemberPtrTraits<decltype(T)>::Type v) {
checkWritable(msg);
msg->*T = std::forward<decltype(v)>(v);
});
}
} // namespace
namespace chatterino::lua::api::message {
struct ElementRef {
ElementRef() = default;
ElementRef(std::shared_ptr<Message> msg, size_t index)
: msg(std::move(msg))
, index(index)
{
}
MessageElement *element() const
{
if (!this->msg || this->index >= this->msg->elements.size())
{
return nullptr;
}
checkWritable(this->msg.get());
return this->msg->elements[this->index].get();
}
const MessageElement *constElement() const
{
if (!this->msg || this->index >= this->msg->elements.size())
{
return nullptr;
}
return this->msg->elements[this->index].get();
}
MessageElement &ref() const
{
auto *el = this->element();
if (!el)
{
throw std::runtime_error("Element does not exist or expired");
}
return *el;
}
const MessageElement &cref() const
{
const auto *el = this->constElement();
if (!el)
{
throw std::runtime_error("Element does not exist or expired");
}
return *el;
}
/// Cast this element to `T`. Otherwise nullopt is returned.
/// Use `.map()` to access the content.
template <typename T>
sol::optional<T &> as() const
{
// using ref() to error if the reference is invalid
auto *el = dynamic_cast<T *>(&this->ref());
if (!el)
{
return sol::nullopt;
}
return *el;
}
/// Cast this element to `const T`. Otherwise nullopt is returned.
/// Use `.map()` to access the content.
template <typename T>
sol::optional<const T &> asConst() const
{
// using cref() to error if the reference is invalid
const auto *el = dynamic_cast<const T *>(&this->cref());
if (!el)
{
return sol::nullopt;
}
return *el;
}
template <typename T>
bool is() const
{
return dynamic_cast<const T *>(&this->cref()) != nullptr;
}
/// Visit this element by dynamic casting
template <typename... T>
auto visit(auto &&...cb) const
{
static_assert(sizeof...(T) == sizeof...(cb) && sizeof...(T) > 0);
// infer the returned type inside the optional
using Cb0 = std::tuple_element_t<0, std::tuple<decltype(cb)...>>;
using T0 = std::tuple_element_t<0, std::tuple<T...>>;
using TReturn = std::invoke_result_t<Cb0, T0 &>;
return this->visitOne<TReturn, T...>(std::forward<decltype(cb)>(cb)...);
}
bool operator==(const ElementRef &rhs) const
{
return this->msg.get() == rhs.msg.get() && this->index == rhs.index;
}
std::shared_ptr<Message> msg;
size_t index = 0;
private:
template <bool Const>
decltype(auto) maybeConstElement() const
{
if constexpr (Const)
{
return this->constElement();
}
else
{
return this->element();
}
}
/// Run one callback
///
/// This is called recursively.
/// If the callback returns something, we return an `optional<T>` otherwise
/// we return `void`.
template <typename TReturn, typename T, typename... Rest>
auto visitOne(auto &&cb, auto &&...rest) const
-> std::conditional_t<std::is_void_v<TReturn>, void,
sol::optional<TReturn>>
{
auto *el =
dynamic_cast<T *>(this->maybeConstElement<std::is_const_v<T>>());
if (!el)
{
if constexpr (sizeof...(rest) == 0)
{
if constexpr (std::is_void_v<
std::invoke_result_t<decltype(cb), T &>>)
{
return;
}
else
{
return sol::nullopt;
}
}
else
{
return visitOne<TReturn, Rest...>(
std::forward<decltype(rest)>(rest)...);
}
}
return std::invoke(cb, *el);
}
};
struct ElementIterator {
using difference_type = std::ptrdiff_t;
using value_type = ElementRef;
ElementIterator() = default;
ElementIterator(std::shared_ptr<Message> msg, size_t index)
: current(std::move(msg), index)
{
}
ElementRef operator*() const
{
return this->current;
}
ElementRef operator[](size_t i) const
{
return {this->current.msg, this->current.index + i};
}
ElementIterator &operator+=(difference_type diff)
{
this->current.index += diff;
return *this;
}
ElementIterator &operator++()
{
return *this += 1;
}
ElementIterator operator++(int) // postfix increment
{
auto tmp = *this;
++*this;
return tmp;
}
friend ElementIterator operator+(difference_type diff,
const ElementIterator &it)
{
return {it.current.msg, it.current.index + diff};
}
friend ElementIterator operator+(const ElementIterator &it,
difference_type diff)
{
return {it.current.msg, it.current.index + diff};
}
ElementIterator &operator-=(difference_type diff)
{
this->current.index -= diff;
return *this;
}
ElementIterator &operator--()
{
return *this -= 1;
}
ElementIterator operator--(int) // postfix decrement
{
auto tmp = *this;
--*this;
return tmp;
}
friend difference_type operator-(const ElementIterator &lhs,
const ElementIterator &rhs)
{
return static_cast<difference_type>(lhs.current.index) -
static_cast<difference_type>(rhs.current.index);
}
friend ElementIterator operator-(difference_type diff,
const ElementIterator &it)
{
return {it.current.msg, it.current.index - diff};
}
friend ElementIterator operator-(const ElementIterator &it,
difference_type diff)
{
return {it.current.msg, it.current.index - diff};
}
bool operator==(const ElementIterator &rhs) const
{
return this->current == rhs.current;
}
bool operator<(const ElementIterator &rhs) const
{
return this->current.index < rhs.current.index;
}
bool operator<=(const ElementIterator &rhs) const
{
return this->current.index <= rhs.current.index;
}
bool operator>(const ElementIterator &rhs) const
{
return this->current.index > rhs.current.index;
}
bool operator>=(const ElementIterator &rhs) const
{
return this->current.index >= rhs.current.index;
}
ElementRef current;
};
static_assert(std::random_access_iterator<ElementIterator>);
struct MessageElements {
using value_type = ElementIterator::value_type;
using iterator = ElementIterator;
using size_type = size_t;
explicit MessageElements(std::shared_ptr<Message> msg)
: msg(std::move(msg))
{
}
~MessageElements() = default;
MessageElements(const MessageElements &) = delete;
MessageElements(MessageElements &&) = default;
MessageElements &operator=(const MessageElements &) = delete;
MessageElements &operator=(MessageElements &&) = default;
ElementIterator begin() const
{
return {this->msg, 0};
}
ElementIterator end() const
{
return {this->msg, this->msg->elements.size()};
}
size_type size() const
{
if (!this->msg)
{
return 0;
}
return this->msg->elements.size();
}
// NOLINTNEXTLINE
size_type max_size() const
{
return this->size(); // we can't insert
}
bool empty() const
{
if (!this->msg)
{
return true;
}
return this->msg->elements.empty();
}
// NOLINTNEXTLINE
void push_back(ElementIterator::value_type /* v */) const
{
throw std::runtime_error("Insertion is not supported");
}
// NOLINTNEXTLINE
void erase(ElementIterator it) const
{
if (it.current.msg != this->msg || !this->msg ||
it.current.index >= this->msg->elements.size())
{
throw std::runtime_error("Can't erase here");
}
checkWritable(this->msg.get());
this->msg->elements.erase(this->msg->elements.begin() +
static_cast<ptrdiff_t>(it.current.index));
}
std::shared_ptr<Message> msg;
};
void createUserType(sol::table &c2)
{
c2.new_usertype<Message>("Message",
sol::factories([](const sol::table &tbl) {
return messageFromTable(tbl);
}));
c2.new_usertype<ElementRef>(
"MessageElement", sol::no_constructor, //
"type", sol::property([](const ElementRef &el) {
return el.cref().type();
}),
"flags", sol::property([](const ElementRef &el) {
return el.cref().getFlags().value();
}),
"add_flags",
[](const ElementRef &el, MessageElementFlag flag) {
el.ref().addFlags(flag);
},
"link",
sol::property(
[](const ElementRef &el) {
return el.cref().getLink();
},
[](const ElementRef &el, const Link &link) {
if (el.is<MentionElement>() || el.is<LinkElement>())
{
throw std::runtime_error(
"Setting a link on this element is unsupported");
}
setLinkOn(&el.ref(), link);
}),
"tooltip",
sol::property(
[](const ElementRef &el) {
return el.cref().getTooltip();
},
[](const ElementRef &el, const QString &tooltip) {
el.ref().setTooltip(tooltip);
}),
"trailing_space",
sol::property(
[](const ElementRef &el) {
return el.cref().hasTrailingSpace();
},
[](const ElementRef &el, bool trailingSpace) {
el.ref().setTrailingSpace(trailingSpace);
}),
"padding", sol::property([](const ElementRef &el) {
return el.asConst<CircularImageElement>().map(
&CircularImageElement::padding);
}),
"background", sol::property([](const ElementRef &el) {
return el.as<CircularImageElement>().map(
[](const CircularImageElement &el) {
return el.background().name(QColor::HexArgb);
});
}),
"words", sol::property([](const ElementRef &el) {
return el.visit<const TextElement, const SingleLineTextElement>(
&TextElement::words, &SingleLineTextElement::words);
}),
"color", sol::property([](const ElementRef &el) {
return el.visit<const TextElement, const SingleLineTextElement>(
[](const TextElement &el) {
return el.color().toLua();
},
[](const SingleLineTextElement &el) {
return el.color().toLua();
});
}),
"style", sol::property([](const ElementRef &el) {
return el.visit<const TextElement, const SingleLineTextElement>(
&TextElement::fontStyle, &SingleLineTextElement::fontStyle);
}),
"lowercase", sol::property([](const ElementRef &el) {
return el.asConst<LinkElement>().map(&LinkElement::lowercase);
}),
"original", sol::property([](const ElementRef &el) {
return el.asConst<LinkElement>().map(&LinkElement::original);
}),
"fallback_color", sol::property([](const ElementRef &el) {
return el.asConst<MentionElement>().map(
[](const MentionElement &el) {
return el.fallbackColor().toLua();
});
}),
"user_color", sol::property([](const ElementRef &el) {
return el.asConst<MentionElement>().map(
[](const MentionElement &el) {
return el.userColor().toLua();
});
}),
"user_login_name", sol::property([](const ElementRef &el) {
return el.asConst<MentionElement>().map(
&MentionElement::userLoginName);
}),
"time", sol::property([](const ElementRef &el) {
return el.asConst<TimestampElement>().map(
[](const TimestampElement &el) {
return QDateTime(QDate::currentDate(), el.time())
.toMSecsSinceEpoch();
});
}));
c2.new_usertype<Message>(
"Message", sol::factories([](const sol::table &tbl) {
return messageFromTable(tbl);
}),
"flags",
sol::property(
[](Message *msg) {
return msg->flags.value();
},
[](Message *msg, MessageFlag f) {
// flags are always mutable
msg->flags = f;
}),
"parse_time",
sol::property(
[](Message *msg) {
return QDateTime(QDate::currentDate(), msg->parseTime)
.toMSecsSinceEpoch();
},
[](Message *msg, qint64 ms) {
checkWritable(msg);
msg->parseTime = datetimeFromOffset(ms).time();
}),
"id", memberAccessor<&Message::id>(), //
"search_text", memberAccessor<&Message::searchText>(), //
"message_text", memberAccessor<&Message::messageText>(), //
"login_name", memberAccessor<&Message::loginName>(), //
"display_name", memberAccessor<&Message::displayName>(), //
"localized_name", memberAccessor<&Message::localizedName>(), //
"user_id", memberAccessor<&Message::userID>(), //
"channel_name", memberAccessor<&Message::channelName>(), //
"username_color",
sol::property(
[](Message *msg) {
return msg->usernameColor.name(QColor::HexArgb);
},
[](Message *msg, std::string_view sv) {
checkWritable(msg);
msg->usernameColor = QColor::fromString(sv);
}),
"server_received_time",
sol::property(
[](Message *msg) {
return msg->serverReceivedTime.toMSecsSinceEpoch();
},
[](Message *msg, qint64 ms) {
checkWritable(msg);
msg->serverReceivedTime = datetimeFromOffset(ms);
}),
"highlight_color",
sol::property(
[](Message *msg) {
if (!msg->highlightColor)
{
return QString{};
}
return msg->highlightColor->name(QColor::HexArgb);
},
[](Message *msg, std::string_view sv) {
checkWritable(msg);
if (sv.empty())
{
msg->highlightColor.reset();
}
else
{
msg->highlightColor =
std::make_shared<QColor>(QColor::fromString(sv));
}
}),
// must be read only (but it might be helpful for generic Lua functions)
"frozen", sol::property([](Message *msg) {
return msg->frozen;
}),
"elements",
[](const std::shared_ptr<Message> &msg) {
return MessageElements(msg);
},
"append_element",
[](Message *msg, const sol::table &tbl) {
checkWritable(msg);
auto el = elementFromTable(tbl);
if (el)
{
msg->elements.emplace_back(std::move(el));
}
});
}
} // namespace chatterino::lua::api::message
+150 -50
View File
@@ -10,10 +10,160 @@
namespace chatterino::lua::api::message {
/* @lua-fragment
---@class c2.MessageElementBase
---@field flags c2.MessageElementFlag The element's flags
---@field tooltip string The tooltip (if any)
---@field trailing_space boolean Whether to add a trailing space after the element
---@field link c2.Link An action when clicking on this element. Mention and Link elements don't support this. They manage the link themselves.
c2.MessageElementBase = {}
-- ^^^ this is kinda fake - this table doesn't exist in Lua, we only declare it to add methods
--- Add flags to this element
---
---@param flags c2.MessageElementFlag
function c2.MessageElementBase:add_flags(flags) end
---A base table to initialize a new message element
---@class MessageElementInitBase
---@field tooltip? string Tooltip text
---@field trailing_space? boolean Whether to add a trailing space after the element (default: true)
---@field link? c2.Link An action when clicking on this element. Mention and Link elements don't support this. They manage the link themselves.
---@class c2.TextElement : c2.MessageElementBase
---@field type "text"
---@field words string[] The words of this element
---@field color string The color of the text
---@field style c2.FontStyle The font style of the text
---A table to initialize a new message text element
---@class TextElementInit : MessageElementInitBase
---@field type "text" The type of the element
---@field text string The text of this element
---@field flags? c2.MessageElementFlag Message element flags (see `c2.MessageElementFlags`)
---@field color? MessageColor The color of the text
---@field style? c2.FontStyle The font style of the text
---@class c2.SingleLineTextElement : c2.MessageElementBase
---@field type "single-line-text"
---@field words string[] The words of this element
---@field color string The color of the text
---@field style c2.FontStyle The font style of the text
---A table to initialize a new message single-line text element
---@class SingleLineTextElementInit : MessageElementInitBase
---@field type "single-line-text" The type of the element
---@field text string The text of this element
---@field flags? c2.MessageElementFlag Message element flags (see `c2.MessageElementFlags`)
---@field color? MessageColor The color of the text
---@field style? c2.FontStyle The font style of the text
---@class c2.MentionElement : c2.TextElement
---@field type "mention"
---@field login_name string The login name of the mentioned user
---@field fallback_color MessageColor The color of the element in case the "Colorize @usernames" is disabled
---@field user_color MessageColor The color of the element in case the "Colorize @usernames" is enabled
---A table to initialize a new mention element
---@class MentionElementInit : MessageElementInitBase
---@field type "mention" The type of the element
---@field display_name string The display name of the mentioned user
---@field login_name string The login name of the mentioned user
---@field fallback_color MessageColor The color of the element in case the "Colorize @usernames" is disabled
---@field user_color MessageColor The color of the element in case the "Colorize @usernames" is enabled
---@class c2.TimestampElement : c2.MessageElementBase
---@field type "timestamp"
---@field time number The time of the timestamp (in milliseconds since epoch).
---A table to initialize a new timestamp element
---@class TimestampElementInit : MessageElementInitBase
---@field type "timestamp" The type of the element
---@field time number? The time of the timestamp (in milliseconds since epoch). If not provided, the current time is used.
---@class c2.TwitchModerationElement : c2.MessageElementBase
---@field type "twitch-moderation"
---A table to initialize a new Twitch moderation element (all the custom moderation buttons)
---@class TwitchModerationElementInit : MessageElementInitBase
---@field type "twitch-moderation" The type of the element
---@class c2.LinebreakElement : c2.MessageElementBase
---@field type "linebreak"
---A table to initialize a new linebreak element
---@class LinebreakElementInit : MessageElementInitBase
---@field type "linebreak" The type of the element
---@field flags? c2.MessageElementFlag Message element flags (see `c2.MessageElementFlags`)
---@class c2.ReplyCurveElement : c2.MessageElementBase
---@field type "reply-curve"
---A table to initialize a new reply curve element
---@class ReplyCurveElementInit : MessageElementInitBase
---@field type "reply-curve" The type of the element
---@class c2.LinkElement : c2.TextElement
---@field type "link"
---@class c2.EmoteElement : c2.MessageElementBase
---@field type "emote"
---@class c2.LayeredEmoteElement : c2.MessageElementBase
---@field type "layered-emote"
---@class c2.ImageElement : c2.MessageElementBase
---@field type "image"
---@class c2.CircularImageElement : c2.MessageElementBase
---@field type "circular-image"
---@class c2.ScalingImageElement : c2.MessageElementBase
---@field type "scaling-image"
---@class c2.BadgeElement : c2.MessageElementBase
---@field type "badge"
---@class c2.ModBadgeElement : c2.BadgeElement
---@field type "mod-badge"
---@class c2.VipBadgeElement : c2.BadgeElement
---@field type "vip-badge"
---@class c2.FfzBadgeElement : c2.BadgeElement
---@field type "ffz-badge"
---@alias MessageElement c2.TextElement|c2.SingleLineTextElement|c2.MentionElement|c2.TimestampElement|c2.TwitchModerationElement|c2.LinebreakElement|c2.ReplyCurveElement|c2.LinkElement|c2.EmoteElement|c2.LayeredEmoteElement|c2.ImageElement|c2.CircularImageElement|c2.ScalingImageElement|c2.BadgeElement|c2.ModBadgeElement|c2.VipBadgeElement|c2.FfzBadgeElement
---@alias MessageElementInit TextElementInit|SingleLineTextElementInit|MentionElementInit|TimestampElementInit|TwitchModerationElementInit|LinebreakElementInit|ReplyCurveElementInit
---A chat message
---@class c2.Message
---@field flags c2.MessageFlag The message's flags
---@field parse_time number Time the message was parsed (in milliseconds since epoch)
---@field id string The message ID
---@field search_text string Text to check when searching for messages
---@field message_text string Text content of this message (used for filters for example)
---@field login_name string The login name of the sender
---@field display_name string The dispay name of the sender
---@field localized_name string The localized name of the sender (this is used for CJK names, otherwise it's empty)
---@field user_id string The ID of the sender
---@field channel_name string The name of the channel this message appeared in
---@field username_color string The color of the username
---@field server_received_time number The time the server received the message (in milliseconds since epoch)
---@field highlight_color string The color of the highlight or empty
---@field frozen boolean If this is set, Lua plugins can't modify this message (as it's visible to the user).
c2.Message = {}
--- The elements this message is made up of
---
---@return MessageElement[] elements
function c2.Message:elements() end
--- Add an element to this message.
---
---@param init MessageElementInit The element to add
function c2.Message:append_element(init) end
---A table to initialize a new message
---@class MessageInit
---@field flags? c2.MessageFlag Message flags (see `c2.MessageFlags`)
@@ -31,58 +181,8 @@ c2.Message = {}
---@field highlight_color? string|nil The color of the highlight (if any)
---@field elements? MessageElementInit[] The elements of the message
---A base table to initialize a new message element
---@class MessageElementInitBase
---@field tooltip? string Tooltip text
---@field trailing_space? boolean Whether to add a trailing space after the element (default: true)
---@field link? c2.Link An action when clicking on this element. Mention and Link elements don't support this. They manage the link themselves.
---@alias MessageColor "text"|"link"|"system"|string A color for a text element - "text", "link", and "system" are special values that take the current theme into account
---A table to initialize a new message text element
---@class TextElementInit : MessageElementInitBase
---@field type "text" The type of the element
---@field text string The text of this element
---@field flags? c2.MessageElementFlag Message element flags (see `c2.MessageElementFlags`)
---@field color? MessageColor The color of the text
---@field style? c2.FontStyle The font style of the text
---A table to initialize a new message single-line text element
---@class SingleLineTextElementInit : MessageElementInitBase
---@field type "single-line-text" The type of the element
---@field text string The text of this element
---@field flags? c2.MessageElementFlag Message element flags (see `c2.MessageElementFlags`)
---@field color? MessageColor The color of the text
---@field style? c2.FontStyle The font style of the text
---A table to initialize a new mention element
---@class MentionElementInit : MessageElementInitBase
---@field type "mention" The type of the element
---@field display_name string The display name of the mentioned user
---@field login_name string The login name of the mentioned user
---@field fallback_color MessageColor The color of the element in case the "Colorize @usernames" is disabled
---@field user_color MessageColor The color of the element in case the "Colorize @usernames" is enabled
---A table to initialize a new timestamp element
---@class TimestampElementInit : MessageElementInitBase
---@field type "timestamp" The type of the element
---@field time number? The time of the timestamp (in milliseconds since epoch). If not provided, the current time is used.
---A table to initialize a new Twitch moderation element (all the custom moderation buttons)
---@class TwitchModerationElementInit : MessageElementInitBase
---@field type "twitch-moderation" The type of the element
---A table to initialize a new linebreak element
---@class LinebreakElementInit : MessageElementInitBase
---@field type "linebreak" The type of the element
---@field flags? c2.MessageElementFlag Message element flags (see `c2.MessageElementFlags`)
---A table to initialize a new reply curve element
---@class ReplyCurveElementInit : MessageElementInitBase
---@field type "reply-curve" The type of the element
---@alias MessageElementInit TextElementInit|SingleLineTextElementInit|MentionElementInit|TimestampElementInit|TwitchModerationElementInit|LinebreakElementInit|ReplyCurveElementInit
--- Creates a new message
---
---@param init MessageInit The message initialization table
+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