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:
@@ -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)
|
||||
|
||||
Vendored
+125
-1
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user