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
+1
View File
@@ -6,6 +6,7 @@
- Minor: Badges now link to their home page like emotes in the context menu. (#6437)
- Minor: Fixed usercard resizing improperly without recent messages. (#6496)
- Minor: Added setting for character limit of deleted messages. (#6491)
- Minor: Added message introspection and manipulation. (#6353)
- Minor: Added link support to plugin message API. (#6386, #6527)
- Minor: Added a description for the logging option under moderation tab. (#6514)
- Minor: Added a JSON API for plugins (`require('chatterino.json')`). (#6420)
+125 -1
View File
@@ -160,8 +160,24 @@ declare namespace c2 {
var WebSocket: WebSocketConstructor;
interface Message {
__dummy: void; // avoid being an empty interface
flags: MessageFlag;
id: string;
parse_time: number;
search_text: string;
message_text: string;
login_name: string;
display_name: string;
localized_name: string;
user_id: string;
channel_name: string;
username_color: string;
server_received_time: number;
highlight_color: string | null;
frozen: boolean;
elements(): MessageElement[];
append_element(init: MessageElementInit): void;
}
interface MessageConstructor {
new: (this: void, init: MessageInit) => Message;
}
@@ -184,6 +200,14 @@ declare namespace c2 {
elements?: MessageElementInit[];
}
interface MessageElementBase {
flags: MessageElementFlag;
tooltip: string;
trailing_space: boolean;
link: Link;
add_flags(flags: MessageElementFlag): void;
}
interface MessageElementInitBase {
tooltip?: string;
trailing_space?: boolean;
@@ -192,6 +216,25 @@ declare namespace c2 {
type MessageColor = "text" | "link" | "system" | string;
type MessageElement =
| TextElement
| SingleLineTextElement
| MentionElement
| TimestampElement
| TwitchModerationElement
| LinebreakElement
| ReplyCurveElement
| LinkElement
| EmoteElement
| LayeredEmoteElement
| ImageElement
| CircularImageElement
| ScalingImageElement
| BadgeElement
| ModBadgeElement
| VipBadgeElement
| FfzBadgeElement;
type MessageElementInit =
| TextElementInit
| SingleLineTextElementInit
@@ -201,6 +244,13 @@ declare namespace c2 {
| LinebreakElementInit
| ReplyCurveElementInit;
interface TextElement extends MessageElementBase {
type: "text";
words: string[];
color: string;
style: c2.FontStyle;
}
interface TextElementInit extends MessageElementInitBase {
type: "text";
text: string;
@@ -209,6 +259,13 @@ declare namespace c2 {
style?: FontStyle;
}
interface SingleLineTextElement extends MessageElementBase {
type: "single-line-text";
words: string[];
color: string;
style: c2.FontStyle;
}
interface SingleLineTextElementInit extends MessageElementInitBase {
type: "single-line-text";
text: string;
@@ -217,6 +274,14 @@ declare namespace c2 {
style?: FontStyle;
}
interface MentionElement extends Omit<TextElement, "type"> {
type: "mention";
display_name: string;
login_name: string;
fallback_color: string;
user_color: string;
}
interface MentionElementInit extends MessageElementInitBase {
type: "mention";
display_name: string;
@@ -225,24 +290,83 @@ declare namespace c2 {
user_color: MessageColor;
}
interface TimestampElement extends MessageElementBase {
type: "timestamp";
time: number;
}
interface TimestampElementInit extends MessageElementInitBase {
type: "timestamp";
time?: number;
}
interface TwitchModerationElement extends MessageElementBase {
type: "twitch-moderation";
}
interface TwitchModerationElementInit extends MessageElementInitBase {
type: "twitch-moderation";
}
interface LinebreakElement extends MessageElementBase {
type: "linebreak";
}
interface LinebreakElementInit extends MessageElementInitBase {
type: "linebreak";
flags?: MessageElementFlag;
}
interface ReplyCurveElement extends MessageElementBase {
type: "reply-curve";
}
interface ReplyCurveElementInit extends MessageElementInitBase {
type: "reply-curve";
}
interface LinkElement extends Omit<TextElement, "type"> {
type: "link";
lowercase: string;
original: string;
}
interface EmoteElement extends MessageElementBase {
type: "emote";
}
interface LayeredEmoteElement extends MessageElementBase {
type: "layered-emote";
}
interface ImageElement extends MessageElementBase {
type: "image";
}
interface CircularImageElement extends MessageElementBase {
type: "circular-image";
}
interface ScalingImageElement extends MessageElementBase {
type: "scaling-image";
}
interface BadgeElement extends MessageElementBase {
type: "badge";
}
interface ModBadgeElement extends Omit<BadgeElement, "type"> {
type: "mod-badge";
}
interface VipBadgeElement extends Omit<BadgeElement, "type"> {
type: "ffz-badge";
}
interface FfzBadgeElement extends Omit<BadgeElement, "type"> {
type: "ffz-badge";
}
interface Link {
type: LinkType;
value: string;
+150 -50
View File
@@ -299,10 +299,160 @@ function c2.HTTPRequest.create(method, url) end
-- Begin src/controllers/plugins/api/Message.hpp
---@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`)
@@ -320,58 +470,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
+72 -1
View File
@@ -620,7 +620,26 @@ Closes the WebSocket connection.
#### `Message`
Allows creation of rich chat messages. This is currently limited but is expected to be expanded soon.
Allows creation and modification of rich chat messages. A `Message` is
Chatterino's representation of a chat message or any system message.
The interface to Lua is currently limited but is expected to be expanded soon.
Messages can be added to a [`Channel`](#channel). Once a message is added to a
channel, it can't be modified anymore (except for its `flags`). These messages
are termed "frozen" (immutable). You can check for this using the `frozen`
property:
```lua
local my_msg = c2.Message.new({ id = "foobar" })
assert(not my_msg.frozen)
my_channel:add_message(my_msg)
assert(my_msg.frozen)
```
A message has the same properties that it takes in its constructor (e.g.
`msg.id` or `msg.flags`). If the message is not frozen (mutable), these
properties can be modified. New elements can be added with
[`append_element`](#messageappend_elementdata).
##### `Message.new(data)`
@@ -654,6 +673,58 @@ end)
The full range of options can be found in the typing files ([LuaLS](./lua-meta/globals.lua), [TypeScript](./chatterino.d.ts)).
##### `Message:elements()`
Gets a reference to the message's elements. This can be indexed or iterated
though. Through the return value, you can also remove elements. Note that
currently, new elements can't be added here (use `Message:append_element`).
The return value can be thought of as a table of `MessageElement`s.
```lua
local my_msg = c2.Message.new({
id = "foo",
elements = {
{ type = "text", text = "foo" },
{ type = "text", text = "bar" },
{ type = "text", text = "baz" },
}
})
local elements = my_msg:elements()
assert(#elements == 3)
assert(elements[1].words[1] == "foo")
assert(elements[2].words[1] == "bar")
elements:erase(2) -- erase "bar"
assert(#elements == 2)
assert(elements[1].words[1] == "foo")
assert(elements[2].words[1] == "baz")
```
##### `Message:append_element(data)`
Creates a message element from a table (`data`) and appends it to the message.
The structure of the table matches the one taken in `Message.new`'s `elements`.
```lua
local my_msg = c2.Message.new({ id = "foo" })
my_msg:append_element({
type = "text",
text = "My text element",
})
```
#### `MessageElement`
A reference to an element inside a `Message` (essentially an in index into the
elements). Note that modifications to the elements of the parent `Message` might
change the actual element this is referring to.
To distinguish different types of elements, check the `type` property. This
takes the same values as `element.type` in the `Message` constructor.
The full range of properties can be found in the typing files ([LuaLS](./lua-meta/globals.lua), [TypeScript](./chatterino.d.ts)).
#### `LinkType` enum
This table describes links available to plugins.
+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
+48
View File
@@ -0,0 +1,48 @@
-- These tests mirror the code example from the docs - make sure they work
local tests = {
frozen_messages = function()
local my_msg = c2.Message.new({ id = "foobar" })
assert(not my_msg.frozen)
local chan = c2.Channel.by_name("mm2pl")
assert(chan)
chan:add_message(my_msg)
assert(my_msg.frozen)
end,
element_access = function()
local my_msg = c2.Message.new({
id = "foo",
elements = {
{ type = "text", text = "foo" },
{ type = "text", text = "bar" },
{ type = "text", text = "baz" },
}
})
local elements = my_msg:elements()
assert(#elements == 3)
assert(elements[1].words[1] == "foo")
assert(elements[2].words[1] == "bar")
elements:erase(2) -- erase "bar"
assert(#elements == 2)
assert(elements[1].words[1] == "foo")
assert(elements[2].words[1] == "baz")
end,
append_element = function()
local my_msg = c2.Message.new({ id = "foo" })
local els = my_msg:elements()
assert(#els == 0)
my_msg:append_element({
type = "text",
text = "My text element",
})
assert(#els == 1)
assert(els[1].type == "text")
end
}
for name, fn in pairs(tests) do
local ok, res = pcall(fn)
if not ok then
error(name .. " failed: " .. res)
end
end
+434
View File
@@ -14,6 +14,7 @@
# include "controllers/plugins/SolTypes.hpp" // IWYU pragma: keep
# include "lib/Snapshot.hpp"
# include "messages/Message.hpp"
# include "messages/MessageElement.hpp"
# include "mocks/BaseApplication.hpp"
# include "mocks/Channel.hpp"
# include "mocks/EmoteController.hpp"
@@ -1018,6 +1019,417 @@ TEST_F(PluginTest, ChannelAddMessage)
ASSERT_EQ(added[5].first, logged[2]);
}
TEST_F(PluginTest, MessageFrozenFlag)
{
configure();
sol::protected_function isFrozenFn = lua->script(R"lua(
return function(msg)
return msg.frozen
end
)lua");
sol::protected_function setFrozenFn = lua->script(R"lua(
return function(msg, val)
msg.frozen = val
end
)lua");
auto liquid = std::make_shared<Message>();
auto res = isFrozenFn(liquid);
ASSERT_TRUE(res.valid());
ASSERT_FALSE(res.get<bool>());
auto frozen = std::make_shared<Message>();
frozen->freeze();
res = isFrozenFn(frozen);
ASSERT_TRUE(res.valid());
ASSERT_TRUE(res.get<bool>());
// we shouldn't be able to modify the flag
ASSERT_FALSE(setFrozenFn(liquid, true).valid());
ASSERT_FALSE(setFrozenFn(liquid, false).valid());
ASSERT_FALSE(setFrozenFn(frozen, true).valid());
ASSERT_FALSE(setFrozenFn(frozen, false).valid());
}
TEST_F(PluginTest, MessageFlagModification)
{
configure();
sol::protected_function pfn = lua->script(R"lua(
return function(msg)
assert(msg.flags == c2.MessageFlag.Debug)
msg.flags = c2.MessageFlag.System
assert(msg.flags == c2.MessageFlag.System)
end
)lua");
sol::protected_function isFrozenFn = lua->script(R"lua(
return function(msg)
return msg.frozen
end
)lua");
auto liquid = std::make_shared<Message>();
liquid->flags = MessageFlag::Debug;
auto res = pfn(liquid);
ASSERT_TRUE(res.valid());
// for the flags, it shouldn't matter if the message is frozen
auto frozen = std::make_shared<Message>();
frozen->flags = MessageFlag::Debug;
frozen->freeze();
res = pfn(frozen);
ASSERT_TRUE(res.valid());
}
TEST_F(PluginTest, MessageModification)
{
configure();
// Test that we can modify properties and that Lua sees the modification
sol::table tests = lua->script(R"lua(
return {
function(msg)
msg.parse_time = 1234567
end,
function(msg)
assert(msg.id == "abc")
msg.id = "1234"
assert(msg.id == "1234")
end,
function(msg)
assert(msg.search_text == "search")
msg.search_text = "query"
assert(msg.search_text == "query")
end,
function(msg)
assert(msg.message_text == "msg")
msg.message_text = "text"
assert(msg.message_text == "text")
end,
function(msg)
assert(msg.login_name == "login")
msg.login_name = "name"
assert(msg.login_name == "name")
end,
function(msg)
assert(msg.display_name == "display")
msg.display_name = "name"
assert(msg.display_name == "name")
end,
function(msg)
assert(msg.localized_name == "localized")
msg.localized_name = "name"
assert(msg.localized_name == "name")
end,
function(msg)
assert(msg.user_id == "id")
msg.user_id = "id"
assert(msg.user_id == "id")
end,
function(msg)
assert(msg.channel_name == "channel")
msg.channel_name = "name"
assert(msg.channel_name == "name")
end,
function(msg)
assert(msg.username_color == "#ffaabbcc")
msg.username_color = "#ccbbaaff"
assert(msg.username_color == "#ccbbaaff")
end,
function(msg)
assert(msg.server_received_time == 1230000)
msg.server_received_time = 1240000
assert(msg.server_received_time == 1240000)
end,
function(msg)
print(msg.highlight_color)
assert(msg.highlight_color == "#ff223344")
msg.highlight_color = "#44332211"
assert(msg.highlight_color == "#44332211")
end,
function(msg)
assert(#msg:elements() == 2)
msg:append_element({ type = "linebreak" })
assert(#msg:elements() == 3)
assert(msg:elements()[3].type == "linebreak")
end,
}
)lua");
auto makeMsg = [] {
auto msg = std::make_shared<Message>();
msg->flags = MessageFlag::Debug;
msg->id = "abc";
msg->searchText = "search";
msg->messageText = "msg";
msg->loginName = "login";
msg->displayName = "display";
msg->localizedName = "localized";
msg->userID = "id";
msg->channelName = "channel";
msg->usernameColor = QColor(0xaabbcc);
msg->serverReceivedTime = QDateTime::fromMSecsSinceEpoch(1230000);
msg->highlightColor = std::make_shared<QColor>(0x223344);
msg->elements.push_back(
std::make_unique<TextElement>("lol", MessageElementFlag::Text));
msg->elements.push_back(
std::make_unique<TextElement>("wow", MessageElementFlag::Text));
return msg;
};
auto liquid = makeMsg();
ASSERT_TRUE(tests.valid());
for (const auto &[_key, cb] : tests)
{
sol::protected_function pf = cb;
auto res = pf(liquid);
if (!res.valid())
{
sol::error err = res;
ASSERT_TRUE(false) << err.what();
}
}
// If the message is frozen, all modifications should fail with an error
auto frozen = makeMsg();
frozen->freeze();
for (const auto &[key, cb] : tests)
{
sol::protected_function pf = cb;
auto res = pf(frozen);
ASSERT_FALSE(res.valid());
sol::error err = res;
ASSERT_EQ(std::string_view(err.what()), "Message is frozen");
}
}
TEST_F(PluginTest, MessageConstness)
{
configure();
sol::protected_function pfn = lua->script(R"lua(
return function(msg)
assert(msg.login_name == "hello")
msg.login_name = "alien"
end
)lua");
auto msg = std::make_shared<Message>();
msg->loginName = "hello";
MessagePtr cmsg = msg;
auto res = pfn(cmsg);
ASSERT_TRUE(res.valid());
cmsg->freeze();
res = pfn(cmsg);
ASSERT_FALSE(res.valid());
}
// Test that we can access properties of message elements
TEST_F(PluginTest, MessageElementAccess)
{
configure();
sol::protected_function pfn = lua->script(R"lua(
return function(msg, idx, prop)
return msg:elements()[idx][prop]
end
)lua");
auto msg = std::make_shared<Message>();
msg->elements.emplace_back(
std::make_unique<TextElement>("my text", MessageElementFlag::Text));
msg->elements.emplace_back(std::make_unique<SingleLineTextElement>(
"single line", MessageElementFlag::Text));
msg->elements.emplace_back(std::make_unique<CircularImageElement>(
ImagePtr{}, 2, QColor(0xabcdef), MessageElementFlag::ReplyButton));
msg->elements.emplace_back(std::make_unique<MentionElement>(
"display", "login", MessageColor::Text, MessageColor::System));
msg->elements[1]->setTooltip("tooltip");
msg->elements[2]->setTrailingSpace(false);
msg->freeze();
auto getAll = [&](std::string_view key) {
return std::array{
pfn(msg, 1, key).get<sol::object>(),
pfn(msg, 2, key).get<sol::object>(),
pfn(msg, 3, key).get<sol::object>(),
pfn(msg, 4, key).get<sol::object>(),
};
};
auto types = getAll("type");
ASSERT_EQ(types[0].as<std::string>(), "text");
ASSERT_EQ(types[1].as<std::string>(), "single-line-text");
ASSERT_EQ(types[2].as<std::string>(), "circular-image");
ASSERT_EQ(types[3].as<std::string>(), "mention");
auto flags = getAll("flags");
ASSERT_EQ(flags[0].as<MessageElementFlag>(), MessageElementFlag::Text);
ASSERT_EQ(flags[1].as<MessageElementFlag>(), MessageElementFlag::Text);
ASSERT_EQ(flags[2].as<MessageElementFlag>(),
MessageElementFlag::ReplyButton);
ASSERT_EQ(flags[3].as<MessageElementFlag>(),
(MessageElementFlags(MessageElementFlag::Text,
MessageElementFlag::Mention)));
auto tooltips = getAll("tooltip");
ASSERT_EQ(tooltips[0].as<std::string>(), "");
ASSERT_EQ(tooltips[1].as<std::string>(), "tooltip");
ASSERT_EQ(tooltips[2].as<std::string>(), "");
ASSERT_EQ(tooltips[3].as<std::string>(), "");
auto spaces = getAll("trailing_space");
ASSERT_TRUE(spaces[0].as<bool>());
ASSERT_TRUE(spaces[1].as<bool>());
ASSERT_FALSE(spaces[2].as<bool>());
ASSERT_TRUE(spaces[3].as<bool>());
// Properties only found on _some_ elements should not error
// (like non existent properties)
auto paddings = getAll("padding");
ASSERT_TRUE(paddings[0].is<std::nullptr_t>());
ASSERT_TRUE(paddings[1].is<std::nullptr_t>());
ASSERT_EQ(paddings[2].as<int>(), 2);
ASSERT_TRUE(paddings[3].is<std::nullptr_t>());
auto words = getAll("words");
ASSERT_EQ(words[0].as<std::vector<std::string>>(),
(std::vector<std::string>{"my", "text"}));
ASSERT_EQ(words[1].as<std::vector<std::string>>(),
(std::vector<std::string>{"single", "line"}));
ASSERT_TRUE(words[2].is<std::nullptr_t>());
// mention elements are also text elements
ASSERT_EQ(words[3].as<std::vector<std::string>>(),
(std::vector<std::string>{"display"}));
auto userLogins = getAll("user_login_name");
ASSERT_TRUE(userLogins[0].is<std::nullptr_t>());
ASSERT_TRUE(userLogins[1].is<std::nullptr_t>());
ASSERT_TRUE(userLogins[2].is<std::nullptr_t>());
ASSERT_EQ(userLogins[3].as<std::string>(), "login");
auto times = getAll("time");
ASSERT_TRUE(times[0].is<std::nullptr_t>());
ASSERT_TRUE(times[1].is<std::nullptr_t>());
ASSERT_TRUE(times[2].is<std::nullptr_t>());
ASSERT_TRUE(times[3].is<std::nullptr_t>());
auto nonExistent = getAll("non_existent");
ASSERT_TRUE(nonExistent[0].is<std::nullptr_t>());
ASSERT_TRUE(nonExistent[1].is<std::nullptr_t>());
ASSERT_TRUE(nonExistent[2].is<std::nullptr_t>());
ASSERT_TRUE(nonExistent[3].is<std::nullptr_t>());
auto links = getAll("link");
ASSERT_TRUE(links[0].is<Link>());
ASSERT_TRUE(links[1].is<Link>());
ASSERT_TRUE(links[2].is<Link>());
ASSERT_TRUE(links[3].is<Link>());
// test that accessing anything outside the elements vector causes an error
auto res = pfn(msg, 0, "flags");
ASSERT_FALSE(res.valid());
res = pfn(msg, 42, "flags");
ASSERT_FALSE(res.valid());
}
// Test that we can modify properties of message elements
TEST_F(PluginTest, MessageElementModification)
{
configure();
sol::protected_function pfn = lua->script(R"lua(
return function(msg, idx, prop, val)
msg:elements()[idx][prop] = val
end
)lua");
// same as MessageElementAccess...
auto msg = std::make_shared<Message>();
msg->elements.emplace_back(
std::make_unique<TextElement>("my text", MessageElementFlag::Text));
msg->elements.emplace_back(std::make_unique<SingleLineTextElement>(
"single line", MessageElementFlag::Text));
msg->elements.emplace_back(std::make_unique<CircularImageElement>(
ImagePtr{}, 2, QColor(0xabcdef), MessageElementFlag::ReplyButton));
msg->elements.emplace_back(std::make_unique<MentionElement>(
"display", "login", MessageColor::Text, MessageColor::System));
msg->elements[1]->setTooltip("tooltip");
msg->elements[2]->setTrailingSpace(false);
// ...but we don't freeze the message here
auto setAll = [&](std::string_view key, auto value) {
for (size_t i = 1; i <= 4; i++)
{
EXPECT_TRUE(pfn(msg, i, key, value).valid());
}
};
setAll("tooltip", "tool");
ASSERT_EQ(msg->elements[0]->getTooltip(), "tool");
ASSERT_EQ(msg->elements[1]->getTooltip(), "tool");
ASSERT_EQ(msg->elements[2]->getTooltip(), "tool");
ASSERT_EQ(msg->elements[3]->getTooltip(), "tool");
setAll("trailing_space", false);
ASSERT_FALSE(msg->elements[0]->hasTrailingSpace());
ASSERT_FALSE(msg->elements[1]->hasTrailingSpace());
ASSERT_FALSE(msg->elements[2]->hasTrailingSpace());
ASSERT_FALSE(msg->elements[3]->hasTrailingSpace());
pfn(msg, 1, "link", Link{Link::CopyToClipboard, "foo"});
pfn(msg, 2, "link", Link{Link::CopyToClipboard, "foo"});
pfn(msg, 3, "link", Link{Link::CopyToClipboard, "foo"});
// can't modify links of mention elements
ASSERT_FALSE(
pfn(msg, 4, "link", Link{Link::CopyToClipboard, "foo"}).valid());
ASSERT_EQ(msg->elements[0]->getLink().type, Link::CopyToClipboard);
ASSERT_EQ(msg->elements[0]->getLink().value, "foo");
ASSERT_EQ(msg->elements[0]->getTooltip(), "<b>Copy to clipboard</b>");
ASSERT_EQ(msg->elements[1]->getLink().type, Link::CopyToClipboard);
ASSERT_EQ(msg->elements[1]->getLink().value, "foo");
ASSERT_EQ(msg->elements[2]->getLink().type, Link::CopyToClipboard);
ASSERT_EQ(msg->elements[2]->getLink().value, "foo");
auto expectErr = [&](std::string_view key, auto value) {
for (size_t i = 1; i <= 4; i++)
{
auto result = pfn(msg, i, key, value);
EXPECT_FALSE(result.valid()) << key;
}
};
expectErr("type", "something");
expectErr("trailing_space", "something");
// can't set these types
expectErr("link", Link{Link::ViewThread, "foo"});
expectErr("link", Link{Link::AutoModAllow, "foo"});
// We can't modify these yet
expectErr("padding", 1);
expectErr("background", 0xabcdef12);
expectErr("words", QStringList{"a", "b"});
expectErr("color", 0x1234);
expectErr("style", FontStyle::ChatMedium);
expectErr("lowercase", "abc");
expectErr("original", "or");
expectErr("fallback_color", "system");
expectErr("user_color", "system");
expectErr("user_login_name", "system");
expectErr("time", 42);
// test that accessing anything outside the elements vector causes an error
auto res = pfn(msg, 0, "trailing_space", true);
ASSERT_FALSE(res.valid());
res = pfn(msg, 42, "trailing_space", true);
ASSERT_FALSE(res.valid());
// we can't modify anything on frozen messages
msg->freeze();
expectErr("tooltip", "tool");
expectErr("trailing_space", false);
expectErr("padding", 1);
}
/// Test that both C++ exceptions and luaL_error properly unwind the stack.
TEST_F(PluginTest, LuaUnwind)
{
@@ -1139,6 +1551,28 @@ TEST_P(PluginJsonTest, Run)
INSTANTIATE_TEST_SUITE_P(PluginJson, PluginJsonTest,
testing::ValuesIn(discoverLuaTests("json")));
class PluginMessageTest : public PluginTest,
public ::testing::WithParamInterface<QString>
{
};
TEST_P(PluginMessageTest, Run)
{
configure();
auto pfr = lua->safe_script_file(luaTestPath("message", GetParam()));
EXPECT_TRUE(pfr.valid());
if (!pfr.valid())
{
qDebug() << "Test" << GetParam() << "failed:";
sol::error err = pfr;
qDebug() << err.what();
return;
}
}
INSTANTIATE_TEST_SUITE_P(PluginMessage, PluginMessageTest,
testing::ValuesIn(discoverLuaTests("message")));
// verify that all snapshots are included
TEST(PluginMessageConstructionTest, Integrity)
{