feat(plugins): add basic message construction (#5754)

This commit is contained in:
nerix
2025-07-31 15:36:39 +02:00
committed by GitHub
parent 7e7c4112d7
commit a4478748f7
29 changed files with 2070 additions and 36 deletions
+2
View File
@@ -250,6 +250,8 @@ set(SOURCE_FILES
controllers/plugins/api/HTTPResponse.hpp
controllers/plugins/api/IOWrapper.cpp
controllers/plugins/api/IOWrapper.hpp
controllers/plugins/api/Message.cpp
controllers/plugins/api/Message.hpp
controllers/plugins/api/WebSocket.cpp
controllers/plugins/api/WebSocket.hpp
controllers/plugins/LuaAPI.cpp
+2
View File
@@ -2,6 +2,8 @@
namespace chatterino {
/** @exposeenum c2.MessageContext */
/// Context of the message being added to a channel
enum class MessageContext {
/// This message is the original
+1
View File
@@ -85,6 +85,7 @@ sol::table toTable(lua_State *L, const CompletionEvent &ev);
* @includefile controllers/plugins/api/ChannelRef.hpp
* @includefile controllers/plugins/api/HTTPResponse.hpp
* @includefile controllers/plugins/api/HTTPRequest.hpp
* @includefile controllers/plugins/api/Message.hpp
* @includefile controllers/plugins/api/WebSocket.hpp
* @includefile common/network/NetworkCommon.hpp
*/
+5 -6
View File
@@ -111,19 +111,18 @@ public:
*
* @returns Sol reference to the table
*/
template <typename T>
template <typename T, T... Additional>
requires std::is_enum_v<T>
sol::table createEnumTable(sol::state_view &lua)
{
constexpr auto values = magic_enum::enum_values<T>();
auto out = lua.create_table(0, values.size());
auto out = lua.create_table(0, values.size() + sizeof...(Additional));
for (const T v : values)
{
std::string_view name = magic_enum::enum_name<T>(v);
std::string str(name);
out.raw_set(str, v);
out.raw_set(magic_enum::enum_name<T>(v), v);
}
(out.raw_set(magic_enum::enum_name<Additional>(), Additional), ...);
return out;
}
@@ -11,11 +11,13 @@
# include "controllers/plugins/api/HTTPRequest.hpp"
# include "controllers/plugins/api/HTTPResponse.hpp"
# include "controllers/plugins/api/IOWrapper.hpp"
# include "controllers/plugins/api/Message.hpp"
# include "controllers/plugins/api/WebSocket.hpp"
# include "controllers/plugins/LuaAPI.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include "controllers/plugins/SolTypes.hpp"
# include "messages/MessageBuilder.hpp"
# include "messages/MessageElement.hpp"
# include "singletons/Paths.hpp"
# include "singletons/Settings.hpp"
@@ -222,10 +224,16 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin)
lua::api::HTTPResponse::createUserType(c2);
lua::api::HTTPRequest::createUserType(c2);
lua::api::WebSocket::createUserType(c2, plugin);
lua::api::message::createUserType(c2);
c2["ChannelType"] = lua::createEnumTable<Channel::Type>(lua);
c2["HTTPMethod"] = lua::createEnumTable<NetworkRequestType>(lua);
c2["EventType"] = lua::createEnumTable<lua::api::EventType>(lua);
c2["LogLevel"] = lua::createEnumTable<lua::api::LogLevel>(lua);
c2["MessageFlag"] =
lua::createEnumTable<MessageFlag, MessageFlag::None>(lua);
c2["MessageElementFlag"] = lua::createEnumTable<MessageElementFlag>(lua);
c2["FontStyle"] = lua::createEnumTable<FontStyle>(lua);
c2["MessageContext"] = lua::createEnumTable<MessageContext>(lua);
sol::table io = g["io"];
io.set_function(
@@ -88,6 +88,32 @@ void ChannelRef::add_system_message(QString text)
this->strong()->addSystemMessage(text);
}
void ChannelRef::add_message(std::shared_ptr<Message> &message,
sol::variadic_args va)
{
MessageContext ctx = [&] {
if (va.size() >= 1)
{
return va.get<MessageContext>();
}
return MessageContext::Original;
}();
auto overrideFlags = [&]() -> std::optional<MessageFlags> {
if (va.size() >= 2)
{
auto flags = va.get<std::optional<MessageFlag>>(1);
if (flags)
{
return MessageFlags{*flags};
}
return {};
}
return {};
}();
this->strong()->addMessage(message, ctx, overrideFlags);
}
bool ChannelRef::is_twitch_channel()
{
return this->strong()->isTwitchChannel();
@@ -168,6 +194,7 @@ void ChannelRef::createUserType(sol::table &c2)
"get_display_name", &ChannelRef::get_display_name,
"send_message", &ChannelRef::send_message,
"add_system_message", &ChannelRef::add_system_message,
"add_message", &ChannelRef::add_message,
"is_twitch_channel", &ChannelRef::is_twitch_channel,
// TwitchChannel
@@ -71,6 +71,16 @@ public:
*/
void add_system_message(QString text);
/**
* Adds a message client-side
*
* @lua@param message c2.Message
* @lua@param context? c2.MessageContext The context of the message being added
* @lua@param override_flags? c2.MessageFlag|nil Flags to override the message's flags (some splits might filter for this)
* @exposed c2.Channel:add_message
*/
void add_message(std::shared_ptr<Message> &message, sol::variadic_args va);
/**
* Returns true for twitch channels.
* Compares the channel Type. Note that enum values aren't guaranteed, just
+249
View File
@@ -0,0 +1,249 @@
#include "controllers/plugins/api/Message.hpp"
#include "Application.hpp"
#include "messages/MessageElement.hpp"
#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/SolTypes.hpp"
# include "messages/Message.hpp"
# include <sol/sol.hpp>
namespace {
using namespace chatterino;
QDateTime datetimeFromOffset(qint64 offset)
{
auto dt = QDateTime::fromMSecsSinceEpoch(offset);
# ifdef CHATTERINO_WITH_TESTS
if (getApp()->isTest())
{
return dt.toUTC();
}
# endif
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)
{
auto v = tbl.get<sol::optional<T>>(std::forward<decltype(key)>(key));
if (!v)
{
throw std::runtime_error(std::string{"Missing required property: "} +
key);
}
return *std::move(v);
}
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{})),
tbl.get_or("style", FontStyle::ChatMedium));
}
std::unique_ptr<SingleLineTextElement> singleLineTextElementFromTable(
const sol::table &tbl)
{
return std::make_unique<SingleLineTextElement>(
requiredGet<QString>(tbl, "text"),
tbl.get_or("flags", MessageElementFlag::Text),
tryMakeMessageColor(tbl.get_or("color", QString{})),
tbl.get_or("style", FontStyle::ChatMedium));
}
std::unique_ptr<MentionElement> mentionElementFromTable(const sol::table &tbl)
{
// no flags!
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")));
}
std::unique_ptr<TimestampElement> timestampElementFromTable(
const sol::table &tbl)
{
// no flags!
auto time = tbl.get<std::optional<qint64>>("time");
if (time)
{
return std::make_unique<TimestampElement>(
datetimeFromOffset(*time).time());
}
return std::make_unique<TimestampElement>();
}
std::unique_ptr<TwitchModerationElement> twitchModerationElementFromTable()
{
// no flags!
return std::make_unique<TwitchModerationElement>();
}
std::unique_ptr<LinebreakElement> linebreakElementFromTable(
const sol::table &tbl)
{
return std::make_unique<LinebreakElement>(
tbl.get_or("flags", MessageElementFlag::None));
}
std::unique_ptr<ReplyCurveElement> replyCurveElementFromTable()
{
// no flags!
return std::make_unique<ReplyCurveElement>();
}
std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
{
auto type = requiredGet<QString>(tbl, "type");
std::unique_ptr<MessageElement> el;
if (type == u"text")
{
el = textElementFromTable(tbl);
}
else if (type == u"single-line-text")
{
el = singleLineTextElementFromTable(tbl);
}
else if (type == u"mention")
{
el = mentionElementFromTable(tbl);
}
else if (type == u"timestamp")
{
el = timestampElementFromTable(tbl);
}
else if (type == u"twitch-moderation")
{
el = twitchModerationElementFromTable();
}
else if (type == u"linebreak")
{
el = linebreakElementFromTable(tbl);
}
else if (type == u"reply-curve")
{
el = replyCurveElementFromTable();
}
else
{
throw std::runtime_error("Invalid message type");
}
assert(el);
el->setTrailingSpace(tbl.get_or("trailing_space", true));
el->setTooltip(tbl.get_or("tooltip", QString{}));
return el;
}
std::shared_ptr<Message> messageFromTable(const sol::table &tbl)
{
auto msg = std::make_shared<Message>();
msg->flags = tbl.get_or("flags", MessageFlag::None);
// This takes a UTC offset (not the milliseconds since the start of the day)
auto parseTime = tbl.get<std::optional<qint64>>("parse_time");
if (parseTime)
{
msg->parseTime = datetimeFromOffset(*parseTime).time();
}
msg->id = tbl.get_or("id", QString{});
msg->searchText = tbl.get_or("search_text", QString{});
msg->messageText = tbl.get_or("message_text", QString{});
msg->loginName = tbl.get_or("login_name", QString{});
msg->displayName = tbl.get_or("display_name", QString{});
msg->localizedName = tbl.get_or("localized_name", QString{});
msg->userID = tbl.get_or("user_id", QString{});
// missing: timeoutUser
msg->channelName = tbl.get_or("channel_name", QString{});
auto usernameColor = tbl.get_or("username_color", QString{});
if (!usernameColor.isEmpty())
{
msg->usernameColor = QColor(usernameColor);
}
auto serverReceivedTime =
tbl.get<std::optional<qint64>>("server_received_time");
if (serverReceivedTime)
{
msg->serverReceivedTime = datetimeFromOffset(*serverReceivedTime);
}
// missing: badges
// missing: badgeInfos
// we construct a color on the fly here
auto highlightColor = tbl.get_or("highlight_color", QString{});
if (!highlightColor.isEmpty())
{
msg->highlightColor = std::make_shared<QColor>(highlightColor);
}
// missing: replyThread
// missing: replyParent
// missing: count
auto elements = tbl.get<std::optional<sol::table>>("elements");
if (elements)
{
auto size = elements->size();
for (size_t i = 1; i <= size; i++)
{
msg->elements.emplace_back(
elementFromTable(elements->get<sol::table>(i)));
}
}
// missing: reward
return msg;
}
} // namespace
namespace chatterino::lua::api::message {
void createUserType(sol::table &c2)
{
c2.new_usertype<Message>("Message",
sol::factories([](const sol::table &tbl) {
return messageFromTable(tbl);
}));
}
} // namespace chatterino::lua::api::message
#endif
+101
View File
@@ -0,0 +1,101 @@
#pragma once
#ifdef CHATTERINO_HAVE_PLUGINS
# include "messages/Message.hpp"
# include <sol/forward.hpp>
namespace chatterino::lua::api::message {
/* @lua-fragment
---A chat message
---@class c2.Message
c2.Message = {}
---A table to initialize a new message
---@class MessageInit
---@field flags? c2.MessageFlag Message flags (see `c2.MessageFlags`)
---@field id? string The (ideally unique) message ID
---@field parse_time? number Time the message was parsed (in milliseconds since epoch)
---@field search_text? string Text to that is compared when searching for messages
---@field message_text? string The message text (used for filters for example)
---@field login_name? string The login name of the sender
---@field display_name? string The display 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 user who sent the message
---@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|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)
---@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
---@return c2.Message msg The new message
function c2.Message.new(init) end
*/
/**
* @includefile singletons/Fonts.hpp
* @includefile messages/MessageElement.hpp
* @includefile messages/MessageFlag.hpp
* @includefile common/enums/MessageContext.hpp
*/
/// Creates the c2.Message user type
void createUserType(sol::table &c2);
} // namespace chatterino::lua::api::message
#endif
+1
View File
@@ -31,6 +31,7 @@ using ImagePtr = std::shared_ptr<Image>;
struct Emote;
using EmotePtr = std::shared_ptr<const Emote>;
/** @exposeenum c2.MessageElementFlag [flags] */
enum class MessageElementFlag : int64_t {
None = 0LL,
Misc = (1LL << 0),
+1
View File
@@ -6,6 +6,7 @@
namespace chatterino {
/** @exposeenum c2.MessageFlag [flags] */
enum class MessageFlag : std::int64_t {
None = 0LL,
System = (1LL << 0),
+1
View File
@@ -14,6 +14,7 @@ namespace chatterino {
class Settings;
class Paths;
/** @exposeenum c2.FontStyle */
enum class FontStyle : uint8_t {
Tiny,
ChatSmall,
+8 -2
View File
@@ -2131,10 +2131,9 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
element->getTooltip(), getTooltipScale(scale)));
}
}
else
else if (auto *linkElement = dynamic_cast<LinkElement *>(element))
{
auto thumbnailSize = getSettings()->thumbnailSize;
auto *linkElement = dynamic_cast<LinkElement *>(element);
if (linkElement)
{
if (linkElement->linkInfo()->isPending())
@@ -2145,6 +2144,13 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
this->setLinkInfoTooltip(linkElement->linkInfo());
}
}
else
{
this->tooltipWidget_->setOne(TooltipEntry{
.image = nullptr,
.text = element->getTooltip(),
});
}
this->tooltipWidget_->moveTo(
event->globalPosition().toPoint() + QPoint(16, 16),