From 7e238843e921f5f3e023f5a2a324e4f77c6bfd5d Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Thu, 5 Dec 2024 22:14:49 +0100 Subject: [PATCH 1/6] feat(plugins): add basic message construction --- src/CMakeLists.txt | 2 + src/controllers/plugins/LuaUtilities.hpp | 11 +- src/controllers/plugins/PluginController.cpp | 20 ++ src/controllers/plugins/api/ChannelRef.cpp | 27 +++ src/controllers/plugins/api/ChannelRef.hpp | 10 + src/controllers/plugins/api/Message.cpp | 218 ++++++++++++++++++ src/controllers/plugins/api/Message.hpp | 13 ++ tests/snapshots/PluginMessageCtor/empty.json | 23 ++ .../PluginMessageCtor/linebreak-element.json | 70 ++++++ .../PluginMessageCtor/mention-element.json | 102 ++++++++ .../PluginMessageCtor/properties.json | 58 +++++ .../reply-curve-element.json | 70 ++++++ .../single-line-text-element.json | 187 +++++++++++++++ .../PluginMessageCtor/text-element.json | 187 +++++++++++++++ .../PluginMessageCtor/timestamp-element.json | 138 +++++++++++ .../PluginMessageCtor/twitch-moderation.json | 70 ++++++ tests/src/Plugins.cpp | 163 ++++++++++++- tests/src/lib/Snapshot.hpp | 4 +- 18 files changed, 1364 insertions(+), 9 deletions(-) create mode 100644 src/controllers/plugins/api/Message.cpp create mode 100644 src/controllers/plugins/api/Message.hpp create mode 100644 tests/snapshots/PluginMessageCtor/empty.json create mode 100644 tests/snapshots/PluginMessageCtor/linebreak-element.json create mode 100644 tests/snapshots/PluginMessageCtor/mention-element.json create mode 100644 tests/snapshots/PluginMessageCtor/properties.json create mode 100644 tests/snapshots/PluginMessageCtor/reply-curve-element.json create mode 100644 tests/snapshots/PluginMessageCtor/single-line-text-element.json create mode 100644 tests/snapshots/PluginMessageCtor/text-element.json create mode 100644 tests/snapshots/PluginMessageCtor/timestamp-element.json create mode 100644 tests/snapshots/PluginMessageCtor/twitch-moderation.json diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e6385e2d161..5222f2da976 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -237,6 +237,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/LuaAPI.cpp controllers/plugins/LuaAPI.hpp controllers/plugins/LuaUtilities.cpp diff --git a/src/controllers/plugins/LuaUtilities.hpp b/src/controllers/plugins/LuaUtilities.hpp index 0f7bdc53f7e..0fd0d4760f4 100644 --- a/src/controllers/plugins/LuaUtilities.hpp +++ b/src/controllers/plugins/LuaUtilities.hpp @@ -111,19 +111,18 @@ class StackGuard * * @returns Sol reference to the table */ -template +template requires std::is_enum_v sol::table createEnumTable(sol::state_view &lua) { constexpr auto values = magic_enum::enum_values(); - 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(v); - std::string str(name); - - out.raw_set(str, v); + out.raw_set(magic_enum::enum_name(v), v); } + (out.raw_set(magic_enum::enum_name(), Additional), ...); + return out; } diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 1a2bc3a1042..16dfdc70e66 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -11,6 +11,7 @@ # 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/LuaAPI.hpp" # include "controllers/plugins/LuaUtilities.hpp" # include "controllers/plugins/SolTypes.hpp" @@ -220,10 +221,29 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin) lua::api::ChannelRef::createUserType(c2); lua::api::HTTPResponse::createUserType(c2); lua::api::HTTPRequest::createUserType(c2); + lua::api::message::createUserType(c2); c2["ChannelType"] = lua::createEnumTable(lua); c2["HTTPMethod"] = lua::createEnumTable(lua); c2["EventType"] = lua::createEnumTable(lua); c2["LogLevel"] = lua::createEnumTable(lua); + c2["MessageFlag"] = + lua::createEnumTable(lua); + c2["MessageElementFlag"] = + lua::createEnumTable(lua); + c2["FontStyle"] = lua::createEnumTable(lua); + c2["MessageContext"] = lua::createEnumTable(lua); sol::table io = g["io"]; io.set_function( diff --git a/src/controllers/plugins/api/ChannelRef.cpp b/src/controllers/plugins/api/ChannelRef.cpp index b9bced3a0cf..1f1662475f1 100644 --- a/src/controllers/plugins/api/ChannelRef.cpp +++ b/src/controllers/plugins/api/ChannelRef.cpp @@ -88,6 +88,32 @@ void ChannelRef::add_system_message(QString text) this->strong()->addSystemMessage(text); } +void ChannelRef::add_message(std::shared_ptr &msg, + sol::variadic_args va) +{ + MessageContext ctx = [&] { + if (va.size() >= 1) + { + return va.get(); + } + return MessageContext::Original; + }(); + auto overrideFlags = [&]() -> std::optional { + if (va.size() >= 2) + { + auto flags = va.get>(1); + if (flags) + { + return MessageFlags{*flags}; + } + return {}; + } + return {}; + }(); + + this->strong()->addMessage(msg, 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 diff --git a/src/controllers/plugins/api/ChannelRef.hpp b/src/controllers/plugins/api/ChannelRef.hpp index 9d4455739a5..a174ef046ee 100644 --- a/src/controllers/plugins/api/ChannelRef.hpp +++ b/src/controllers/plugins/api/ChannelRef.hpp @@ -71,6 +71,16 @@ struct ChannelRef { */ 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, sol::variadic_args va); + /** * Returns true for twitch channels. * Compares the channel Type. Note that enum values aren't guaranteed, just diff --git a/src/controllers/plugins/api/Message.cpp b/src/controllers/plugins/api/Message.cpp new file mode 100644 index 00000000000..02e7d3ba3fd --- /dev/null +++ b/src/controllers/plugins/api/Message.cpp @@ -0,0 +1,218 @@ +#include "controllers/plugins/api/Message.hpp" + +#include "messages/MessageElement.hpp" + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include "controllers/plugins/SolTypes.hpp" +# include "messages/Message.hpp" + +# include + +namespace { + +using namespace chatterino; + +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); +} + +std::unique_ptr textElementFromTable(const sol::table &tbl) +{ + return std::make_unique( + tbl["text"], tbl.get_or("flags", MessageElementFlag::Text), + tryMakeMessageColor(tbl.get_or("color", QString{})), + tbl.get_or("style", FontStyle::ChatMedium)); +} + +std::unique_ptr singleLineTextElementFromTable( + const sol::table &tbl) +{ + return std::make_unique( + tbl["text"], tbl.get_or("flags", MessageElementFlag::Text), + tryMakeMessageColor(tbl.get_or("color", QString{})), + tbl.get_or("style", FontStyle::ChatMedium)); +} + +std::unique_ptr mentionElementFromTable(const sol::table &tbl) +{ + // no flags! + return std::make_unique( + tbl.get("display_name"), tbl.get("login_name"), + tryMakeMessageColor(tbl.get("fallback_color")), + tryMakeMessageColor(tbl.get("user_color"))); +} + +std::unique_ptr timestampElementFromTable( + const sol::table &tbl) +{ + // no flags! + auto time = tbl.get>("time"); + if (time) + { + return std::make_unique( + QDateTime::fromMSecsSinceEpoch(*time).time()); + } + return std::make_unique(); +} + +std::unique_ptr twitchModerationElementFromTable() +{ + // no flags! + return std::make_unique(); +} + +std::unique_ptr linebreakElementFromTable( + const sol::table &tbl) +{ + return std::make_unique( + tbl.get_or("flags", MessageElementFlag::None)); +} + +std::unique_ptr replyCurveElementFromTable() +{ + // no flags! + return std::make_unique(); +} + +std::unique_ptr elementFromTable(const sol::table &tbl) +{ + QString type = tbl["type"]; + std::unique_ptr 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 messageFromTable(const sol::table &tbl) +{ + auto msg = std::make_shared(); + 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>("parse_time"); + if (parseTime) + { + msg->parseTime = QDateTime::fromMSecsSinceEpoch(*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{}); + // 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>("server_received_time"); + if (serverReceivedTime) + { + msg->serverReceivedTime = + QDateTime::fromMSecsSinceEpoch(*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(highlightColor); + } + + // missing: replyThread + // missing: replyParent + // missing: count + + auto elements = tbl.get>("elements"); + if (elements) + { + auto size = elements->size(); + for (size_t i = 1; i <= size; i++) + { + msg->elements.emplace_back( + elementFromTable(elements->get(i))); + } + } + + // missing: reward + return msg; +} + +} // namespace + +namespace chatterino::lua::api::message { + +void createUserType(sol::table &c2) +{ + c2.new_usertype("Message", sol::factories([](sol::table tbl) { + return messageFromTable(tbl); + })); +} + +} // namespace chatterino::lua::api::message + +#endif diff --git a/src/controllers/plugins/api/Message.hpp b/src/controllers/plugins/api/Message.hpp new file mode 100644 index 00000000000..f5dc5573dc1 --- /dev/null +++ b/src/controllers/plugins/api/Message.hpp @@ -0,0 +1,13 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS +# include "messages/Message.hpp" + +# include + +namespace chatterino::lua::api::message { + +void createUserType(sol::table &c2); + +} // namespace chatterino::lua::api::message + +#endif diff --git a/tests/snapshots/PluginMessageCtor/empty.json b/tests/snapshots/PluginMessageCtor/empty.json new file mode 100644 index 00000000000..cbf25c8e218 --- /dev/null +++ b/tests/snapshots/PluginMessageCtor/empty.json @@ -0,0 +1,23 @@ +{ + "input": "msg = {}", + "output": { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + ], + "flags": "", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "", + "searchText": "", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } +} diff --git a/tests/snapshots/PluginMessageCtor/linebreak-element.json b/tests/snapshots/PluginMessageCtor/linebreak-element.json new file mode 100644 index 00000000000..7113cd0e33f --- /dev/null +++ b/tests/snapshots/PluginMessageCtor/linebreak-element.json @@ -0,0 +1,70 @@ +{ + "input": [ + "msg = {elements={", + " { type = 'linebreak' },", + " { type = 'linebreak', flags = c2.MessageElementFlag.BttvEmote },", + " { type = 'linebreak', tooltip = 't' },", + " { type = 'linebreak', trailing_space = false },", + "}}" + ], + "output": { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "flags": "", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "LinebreakElement" + }, + { + "flags": "BttvEmoteImage|BttvEmoteText", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "LinebreakElement" + }, + { + "flags": "", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "t", + "trailingSpace": true, + "type": "LinebreakElement" + }, + { + "flags": "", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": false, + "type": "LinebreakElement" + } + ], + "flags": "", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "", + "searchText": "", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } +} diff --git a/tests/snapshots/PluginMessageCtor/mention-element.json b/tests/snapshots/PluginMessageCtor/mention-element.json new file mode 100644 index 00000000000..492f8649813 --- /dev/null +++ b/tests/snapshots/PluginMessageCtor/mention-element.json @@ -0,0 +1,102 @@ +{ + "input": [ + "msg = {elements={", + " { type = 'mention', display_name = 'd', login_name = 'l', fallback_color = 'red', user_color = 'green' },", + " { type = 'mention', display_name = 'd', login_name = 'l', fallback_color = 'red', user_color = 'green', flags = c2.MessageElementFlag.BttvEmote },", + " { type = 'mention', display_name = 'd', login_name = 'l', fallback_color = 'red', user_color = 'green', tooltip = 't' },", + " { type = 'mention', display_name = 'd', login_name = 'l', fallback_color = 'red', user_color = 'green', trailing_space = false },", + "}}" + ], + "output": { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "Text", + "fallbackColor": "#ffff0000", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "#ff008000", + "userLoginName": "l", + "words": [ + "d" + ] + }, + { + "color": "Text", + "fallbackColor": "#ffff0000", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "#ff008000", + "userLoginName": "l", + "words": [ + "d" + ] + }, + { + "color": "Text", + "fallbackColor": "#ffff0000", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "t", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "#ff008000", + "userLoginName": "l", + "words": [ + "d" + ] + }, + { + "color": "Text", + "fallbackColor": "#ffff0000", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "MentionElement", + "userColor": "#ff008000", + "userLoginName": "l", + "words": [ + "d" + ] + } + ], + "flags": "", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "", + "searchText": "", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } +} diff --git a/tests/snapshots/PluginMessageCtor/properties.json b/tests/snapshots/PluginMessageCtor/properties.json new file mode 100644 index 00000000000..fb8ec0a2a5f --- /dev/null +++ b/tests/snapshots/PluginMessageCtor/properties.json @@ -0,0 +1,58 @@ +{ + "input": [ + "msg = {", + " flags = c2.MessageFlag.System | c2.MessageFlag.Disabled,", + " id = 'foo-bar',", + " parse_time = 420000,", + " search_text = 'search',", + " message_text = 'message',", + " login_name = 'login',", + " display_name = 'display',", + " localized_name = 'local',", + " username_color = 'blue',", + " server_received_time = 1230000,", + " highlight_color = '#12345678',", + " channel_name = 'channel',", + " elements = {", + " { type = 'text', text = 'aliens walking' }", + " }", + "}" + ], + "output": { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "channel", + "count": 1, + "displayName": "display", + "elements": [ + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "aliens", + "walking" + ] + } + ], + "flags": "System|Disabled", + "highlightColor": "#12345678", + "id": "foo-bar", + "localizedName": "local", + "loginName": "login", + "messageText": "message", + "searchText": "search", + "serverReceivedTime": "1970-01-01T01:20:30", + "timeoutUser": "", + "usernameColor": "#ff0000ff" + } +} diff --git a/tests/snapshots/PluginMessageCtor/reply-curve-element.json b/tests/snapshots/PluginMessageCtor/reply-curve-element.json new file mode 100644 index 00000000000..73479fb2eb2 --- /dev/null +++ b/tests/snapshots/PluginMessageCtor/reply-curve-element.json @@ -0,0 +1,70 @@ +{ + "input": [ + "msg = {elements={", + " { type = 'reply-curve' },", + " { type = 'reply-curve', flags = c2.MessageElementFlag.BttvEmote },", + " { type = 'reply-curve', tooltip = 't' },", + " { type = 'reply-curve', trailing_space = false },", + "}}" + ], + "output": { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ReplyCurveElement" + }, + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "ReplyCurveElement" + }, + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "t", + "trailingSpace": true, + "type": "ReplyCurveElement" + }, + { + "flags": "RepliedMessage", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": false, + "type": "ReplyCurveElement" + } + ], + "flags": "", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "", + "searchText": "", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } +} diff --git a/tests/snapshots/PluginMessageCtor/single-line-text-element.json b/tests/snapshots/PluginMessageCtor/single-line-text-element.json new file mode 100644 index 00000000000..8e46f30166d --- /dev/null +++ b/tests/snapshots/PluginMessageCtor/single-line-text-element.json @@ -0,0 +1,187 @@ +{ + "input": [ + "msg = {elements={", + " { type = 'single-line-text', text = '' },", + " { type = 'single-line-text', text = 'foo' },", + " { type = 'single-line-text', text = 'foo bar' },", + " { type = 'single-line-text', text = 'foo\\nbar' },", + " { type = 'single-line-text', text = 'foo', flags = c2.MessageElementFlag.Text | c2.MessageElementFlag.Timestamp },", + " { type = 'single-line-text', text = 'foo', color = 'text' },", + " { type = 'single-line-text', text = 'foo', color = 'system' },", + " { type = 'single-line-text', text = 'foo', color = 'link' },", + " { type = 'single-line-text', text = 'foo', color = 'green' },", + " { type = 'single-line-text', text = 'foo', style = c2.FontStyle.ChatSmall },", + "}}" + ], + "output": { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "foo" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "foo", + "bar" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "foo\nbar" + ] + }, + { + "color": "Text", + "flags": "Text|Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "foo" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "foo" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "foo" + ] + }, + { + "color": "Link", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "foo" + ] + }, + { + "color": "#ff008000", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "foo" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatSmall", + "tooltip": "", + "trailingSpace": true, + "type": "SingleLineTextElement", + "words": [ + "foo" + ] + } + ], + "flags": "", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "", + "searchText": "", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } +} diff --git a/tests/snapshots/PluginMessageCtor/text-element.json b/tests/snapshots/PluginMessageCtor/text-element.json new file mode 100644 index 00000000000..67cce345cd2 --- /dev/null +++ b/tests/snapshots/PluginMessageCtor/text-element.json @@ -0,0 +1,187 @@ +{ + "input": [ + "msg = {elements={", + " { type = 'text', text = '' },", + " { type = 'text', text = 'foo' },", + " { type = 'text', text = 'foo bar' },", + " { type = 'text', text = 'foo\\nbar' },", + " { type = 'text', text = 'foo', flags = c2.MessageElementFlag.Text | c2.MessageElementFlag.Timestamp },", + " { type = 'text', text = 'foo', color = 'text' },", + " { type = 'text', text = 'foo', color = 'system' },", + " { type = 'text', text = 'foo', color = 'link' },", + " { type = 'text', text = 'foo', color = 'green' },", + " { type = 'text', text = 'foo', style = c2.FontStyle.ChatSmall },", + "}}" + ], + "output": { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo", + "bar" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo\nbar" + ] + }, + { + "color": "Text", + "flags": "Text|Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo" + ] + }, + { + "color": "Link", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo" + ] + }, + { + "color": "#ff008000", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatSmall", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "foo" + ] + } + ], + "flags": "", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "", + "searchText": "", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } +} diff --git a/tests/snapshots/PluginMessageCtor/timestamp-element.json b/tests/snapshots/PluginMessageCtor/timestamp-element.json new file mode 100644 index 00000000000..76b12ac8f6e --- /dev/null +++ b/tests/snapshots/PluginMessageCtor/timestamp-element.json @@ -0,0 +1,138 @@ +{ + "input": [ + "msg = {elements={", + " { type = 'timestamp' },", + " { type = 'timestamp', time = 1230000, flags = c2.MessageElementFlag.BttvEmote },", + " { type = 'timestamp', tooltip = 't' },", + " { type = 'timestamp', trailing_space = false },", + "}}" + ], + "output": { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:00" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:00:00", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "1:20" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "01:20:30", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:00" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:00:00", + "tooltip": "t", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:00" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:00:00", + "tooltip": "", + "trailingSpace": false, + "type": "TimestampElement" + } + ], + "flags": "", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "", + "searchText": "", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } +} diff --git a/tests/snapshots/PluginMessageCtor/twitch-moderation.json b/tests/snapshots/PluginMessageCtor/twitch-moderation.json new file mode 100644 index 00000000000..334eac757fb --- /dev/null +++ b/tests/snapshots/PluginMessageCtor/twitch-moderation.json @@ -0,0 +1,70 @@ +{ + "input": [ + "msg = {elements={", + " { type = 'twitch-moderation' },", + " { type = 'twitch-moderation', flags = c2.MessageElementFlag.BttvEmote },", + " { type = 'twitch-moderation', tooltip = 't' },", + " { type = 'twitch-moderation', trailing_space = false },", + "}}" + ], + "output": { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "t", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": false, + "type": "TwitchModerationElement" + } + ], + "flags": "", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "", + "searchText": "", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } +} diff --git a/tests/src/Plugins.cpp b/tests/src/Plugins.cpp index 588d3c2fffe..63e6c94782b 100644 --- a/tests/src/Plugins.cpp +++ b/tests/src/Plugins.cpp @@ -9,6 +9,8 @@ # include "controllers/plugins/PluginController.hpp" # include "controllers/plugins/PluginPermission.hpp" # include "controllers/plugins/SolTypes.hpp" // IWYU pragma: keep +# include "lib/Snapshot.hpp" +# include "messages/Message.hpp" # include "mocks/BaseApplication.hpp" # include "mocks/Channel.hpp" # include "mocks/Emotes.hpp" @@ -31,6 +33,8 @@ using chatterino::mock::MockChannel; namespace { +constexpr bool UPDATE_SNAPSHOTS = false; + const QString TEST_SETTINGS = R"( { "plugins": { @@ -103,7 +107,7 @@ class MockApplication : public mock::BaseApplication } PluginController plugins; - mock::EmptyLogging logging; + mock::Logging logging; CommandController commands; mock::Emotes emotes; MockTwitch twitch; @@ -638,4 +642,161 @@ TEST_F(PluginTest, tryCallTest) } } +TEST_F(PluginTest, MessageElementFlag) +{ + configure(); + lua->script(R"lua( + values = {} + for k, v in pairs(c2.MessageElementFlag) do + table.insert(values, ("%s=0x%x"):format(k, v)) + end + table.sort(values) + out = table.concat(values, ",") + )lua"); + + const char *VALUES = "AlwaysShow=0x2000000," + "BadgeChannelAuthority=0x8000," + "BadgeChatterino=0x40000," + "BadgeFfz=0x80000," + "BadgeGlobalAuthority=0x2000," + "BadgePredictions=0x4000," + "BadgeSevenTV=0x1000000000," + "BadgeSharedChannel=0x2000000000," + "BadgeSubscription=0x10000," + "BadgeVanity=0x20000," + "Badges=0x30000fe000," + "BitsAmount=0x200000," + "BitsAnimated=0x1000," + "BitsStatic=0x800," + "BttvEmote=0xc0," + "BttvEmoteImage=0x40," + "BttvEmoteText=0x80," + "ChannelName=0x100000," + "ChannelPointReward=0x100," + "ChannelPointRewardImage=0x110," + "Collapsed=0x4000000," + "Default=0x34022fea5e," + "EmojiAll=0x1800000," + "EmojiImage=0x800000," + "EmojiText=0x1000000," + "EmoteImages=0x400000250," + "EmoteText=0x8000004a0," + "FfzEmote=0x600," + "FfzEmoteImage=0x200," + "FfzEmoteText=0x400," + "LowercaseLinks=0x20000000," + "Mention=0x8000000," + "Misc=0x1," + "ModeratorTools=0x400000," + "None=0x0," + "RepliedMessage=0x100000000," + "ReplyButton=0x200000000," + "SevenTVEmote=0xc00000000," + "SevenTVEmoteImage=0x400000000," + "SevenTVEmoteText=0x800000000," + "Text=0x2," + "Timestamp=0x8," + "TwitchEmote=0x30," + "TwitchEmoteImage=0x10," + "TwitchEmoteText=0x20," + "Username=0x4"; + + std::string got = (*lua)["out"]; + ASSERT_EQ(got, VALUES); +} + +TEST_F(PluginTest, ChannelAddMessage) +{ + configure(); + lua->script(R"lua( + function do_it(chan) + local Repost = c2.MessageContext.Repost + local Original = c2.MessageContext.Original + chan:add_message(c2.Message.new({ id = "1" })) + chan:add_message(c2.Message.new({ id = "2" }), Repost) + chan:add_message(c2.Message.new({ id = "3" }), Original, nil) + chan:add_message(c2.Message.new({ id = "4" }), Repost, c2.MessageFlag.DoNotLog) + chan:add_message(c2.Message.new({ id = "5" }), Original, c2.MessageFlag.DoNotLog) + chan:add_message(c2.Message.new({ id = "6" }), Original, c2.MessageFlag.System) + end + )lua"); + + auto chan = std::make_shared("mock"); + + std::vector logged; + EXPECT_CALL(this->app->logging, addMessage) + .Times(3) + .WillRepeatedly( + [&](const auto &, const auto &msg, const auto &, const auto &) { + logged.emplace_back(msg); + }); + + std::vector>> added; + std::ignore = chan->messageAppended.connect([&](auto &&...args) { + added.emplace_back(std::forward(args)...); + }); + + (*lua)["do_it"](lua::api::ChannelRef(chan)); + + ASSERT_EQ(added.size(), 6); + ASSERT_EQ(added[0].first->id, "1"); + ASSERT_FALSE(added[0].second.has_value()); + ASSERT_EQ(added[1].first->id, "2"); + ASSERT_FALSE(added[1].second.has_value()); + ASSERT_EQ(added[2].first->id, "3"); + ASSERT_FALSE(added[2].second.has_value()); + ASSERT_EQ(added[3].first->id, "4"); + ASSERT_EQ(added[3].second, MessageFlags{MessageFlag::DoNotLog}); + ASSERT_EQ(added[4].first->id, "5"); + ASSERT_EQ(added[4].second, MessageFlags{MessageFlag::DoNotLog}); + ASSERT_EQ(added[5].first->id, "6"); + ASSERT_EQ(added[5].second, MessageFlags{MessageFlag::System}); + + ASSERT_EQ(logged.size(), 3); + ASSERT_EQ(added[0].first, logged[0]); + ASSERT_EQ(added[2].first, logged[1]); + ASSERT_EQ(added[5].first, logged[2]); +} + +class PluginMessageConstructionTest + : public PluginTest, + public ::testing::WithParamInterface +{ +}; +TEST_P(PluginMessageConstructionTest, Run) +{ + auto fixture = testlib::Snapshot::read("PluginMessageCtor", GetParam()); + + configure(); + std::string script; + if (fixture->input().isArray()) + { + for (auto line : fixture->input().toArray()) + { + script += line.toString().toStdString() + '\n'; + } + } + else + { + script = fixture->inputString().toStdString() + '\n'; + } + + script += "out = c2.Message.new(msg)"; + lua->script(script); + + Message *got = (*lua)["out"]; + + ASSERT_TRUE(fixture->run(got->toJson(), UPDATE_SNAPSHOTS)); +} + +INSTANTIATE_TEST_SUITE_P( + PluginMessageConstruction, PluginMessageConstructionTest, + testing::ValuesIn(testlib::Snapshot::discover("PluginMessageCtor"))); + +// verify that all snapshots are included +TEST(PluginMessageConstructionTest, Integrity) +{ + ASSERT_FALSE(UPDATE_SNAPSHOTS); // make sure fixtures are actually tested +} + #endif diff --git a/tests/src/lib/Snapshot.hpp b/tests/src/lib/Snapshot.hpp index 39f663eaf2c..9408c2e1ede 100644 --- a/tests/src/lib/Snapshot.hpp +++ b/tests/src/lib/Snapshot.hpp @@ -41,9 +41,9 @@ namespace chatterino::testlib { /// /// TEST_P(ExampleTest, Run) { /// auto fixture = testlib::Snapshot::read("category", GetParam()); -/// auto output = functionToTest(fixture.input()); // or input{String,Utf8} +/// auto output = functionToTest(fixture->input()); // or input{String,Utf8} /// // if snapshots are supposed to be updated, this will write the output -/// ASSERT_TRUE(fixture.run(output, UPDATE_SNAPSHOTS)); +/// ASSERT_TRUE(fixture->run(output, UPDATE_SNAPSHOTS)); /// } /// /// INSTANTIATE_TEST_SUITE_P(ExampleInstance, ExampleTest, From 9a02517e88f304fbf2c4b92d23b76eb74d05b76a Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 8 Dec 2024 17:39:14 +0100 Subject: [PATCH 2/6] feat: add types --- docs/plugin-meta.lua | 222 ++++++++++++++++++++++++ scripts/make_luals_meta.py | 117 ++++++++++--- src/common/enums/MessageContext.hpp | 2 + src/controllers/plugins/LuaAPI.hpp | 1 + src/controllers/plugins/api/Message.hpp | 87 ++++++++++ src/messages/MessageElement.hpp | 1 + src/messages/MessageFlag.hpp | 1 + src/singletons/Fonts.hpp | 1 + 8 files changed, 408 insertions(+), 24 deletions(-) diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua index 27cdf87868c..9cadca66e87 100644 --- a/docs/plugin-meta.lua +++ b/docs/plugin-meta.lua @@ -110,6 +110,13 @@ function c2.Channel:send_message(message, execute_commands) end ---@param message string function c2.Channel:add_system_message(message) end +--- Adds a message client-side +--- +---@param message c2.Message +---@param context? c2.MessageContext The context of the message being added +---@param override_flags? c2.MessageFlag|nil Flags to override the message's flags (some splits might filter for this) +function c2.Channel:add_message(message, context, override_flags) end + --- Returns true for twitch channels. --- Compares the channel Type. Note that enum values aren't guaranteed, just --- that they are equal to the exposed enum. @@ -255,6 +262,221 @@ function c2.HTTPRequest.create(method, url) end -- End src/controllers/plugins/api/HTTPRequest.hpp +-- Begin src/controllers/plugins/api/Message.hpp + + +---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 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" 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 +-- Begin src/singletons/Fonts.hpp + +---@enum c2.FontStyle +c2.FontStyle = { + Tiny = {}, ---@type c2.FontStyle.Tiny + ChatSmall = {}, ---@type c2.FontStyle.ChatSmall + ChatMediumSmall = {}, ---@type c2.FontStyle.ChatMediumSmall + ChatMedium = {}, ---@type c2.FontStyle.ChatMedium + ChatMediumBold = {}, ---@type c2.FontStyle.ChatMediumBold + ChatMediumItalic = {}, ---@type c2.FontStyle.ChatMediumItalic + ChatLarge = {}, ---@type c2.FontStyle.ChatLarge + ChatVeryLarge = {}, ---@type c2.FontStyle.ChatVeryLarge + UiMedium = {}, ---@type c2.FontStyle.UiMedium + UiMediumBold = {}, ---@type c2.FontStyle.UiMediumBold + UiTabs = {}, ---@type c2.FontStyle.UiTabs + EndType = {}, ---@type c2.FontStyle.EndType + ChatStart = {}, ---@type c2.FontStyle.ChatStart + ChatEnd = {}, ---@type c2.FontStyle.ChatEnd +} + +-- End src/singletons/Fonts.hpp + +-- Begin src/messages/MessageElement.hpp + +---@enum c2.MessageElementFlag +c2.MessageElementFlag = { + None = 0, + Misc = 0, + Text = 0, + Username = 0, + Timestamp = 0, + TwitchEmoteImage = 0, + TwitchEmoteText = 0, + TwitchEmote = 0, + BttvEmoteImage = 0, + BttvEmoteText = 0, + BttvEmote = 0, + ChannelPointReward = 0, + ChannelPointRewardImage = 0, + FfzEmoteImage = 0, + FfzEmoteText = 0, + FfzEmote = 0, + SevenTVEmoteImage = 0, + SevenTVEmoteText = 0, + SevenTVEmote = 0, + EmoteImages = 0, + EmoteText = 0, + BitsStatic = 0, + BitsAnimated = 0, + BadgeSharedChannel = 0, + BadgeGlobalAuthority = 0, + BadgePredictions = 0, + BadgeChannelAuthority = 0, + BadgeSubscription = 0, + BadgeVanity = 0, + BadgeChatterino = 0, + BadgeSevenTV = 0, + BadgeFfz = 0, + Badges = 0, + ChannelName = 0, + BitsAmount = 0, + ModeratorTools = 0, + EmojiImage = 0, + EmojiText = 0, + EmojiAll = 0, + AlwaysShow = 0, + Collapsed = 0, + Mention = 0, + LowercaseLinks = 0, + RepliedMessage = 0, + ReplyButton = 0, + Default = 0, +} + +-- End src/messages/MessageElement.hpp + +-- Begin src/messages/MessageFlag.hpp + +---@enum c2.MessageFlag +c2.MessageFlag = { + None = 0, + System = 0, + Timeout = 0, + Highlighted = 0, + DoNotTriggerNotification = 0, + Centered = 0, + Disabled = 0, + DisableCompactEmotes = 0, + Collapsed = 0, + ConnectedMessage = 0, + DisconnectedMessage = 0, + Untimeout = 0, + PubSub = 0, + Subscription = 0, + DoNotLog = 0, + AutoMod = 0, + RecentMessage = 0, + Whisper = 0, + HighlightedWhisper = 0, + Debug = 0, + Similar = 0, + RedeemedHighlight = 0, + RedeemedChannelPointReward = 0, + ShowInMentions = 0, + FirstMessage = 0, + ReplyMessage = 0, + ElevatedMessage = 0, + SubscribedThread = 0, + CheerMessage = 0, + LiveUpdatesAdd = 0, + LiveUpdatesRemove = 0, + LiveUpdatesUpdate = 0, + AutoModOffendingMessageHeader = 0, + AutoModOffendingMessage = 0, + LowTrustUsers = 0, + RestrictedMessage = 0, + MonitoredMessage = 0, + Action = 0, + SharedMessage = 0, + AutoModBlockedTerm = 0, +} + +-- End src/messages/MessageFlag.hpp + +-- Begin src/common/enums/MessageContext.hpp + +---@enum c2.MessageContext +c2.MessageContext = { + Original = {}, ---@type c2.MessageContext.Original + Repost = {}, ---@type c2.MessageContext.Repost +} + +-- End src/common/enums/MessageContext.hpp + +-- End src/controllers/plugins/api/Message.hpp + -- Begin src/common/network/NetworkCommon.hpp ---@enum c2.HTTPMethod diff --git a/scripts/make_luals_meta.py b/scripts/make_luals_meta.py index e1dafe496c1..2b0076da481 100755 --- a/scripts/make_luals_meta.py +++ b/scripts/make_luals_meta.py @@ -27,6 +27,13 @@ @lua@class and @lua@field have special treatment when it comes to generation of spacing new lines Non-command lines of comments are written with a space after '---' + +To insert larger fragments of documentation, comments like +/* @lua-fragment +---@class foo +... +*/ +will be inserted as-is. """ from io import TextIOWrapper @@ -55,10 +62,18 @@ def strip_line(line: str): return re.sub(r"^/\*\*|^\*|\*/$", "", line).strip() +def strip_comments(line: str): + return re.sub(r"//.*|/\*.*?\*/", "", line).strip() + + def is_comment_start(line: str): return line.startswith("/**") +def is_fragment_start(line: str): + return line.startswith("/* @lua-fragment") + + def is_enum_class(line: str): return line.startswith("enum class") @@ -96,20 +111,26 @@ def next_line(self) -> Optional[str]: return self.lines[self.line_idx - 1].strip() return None - def next_doc_comment(self) -> Optional[list[str]]: - """Reads a documentation comment (/** ... */) and advances the cursor""" - lines = [] + def next_item(self) -> list[str] | str | None: + """Finds the next documentation comment or fragment and advances the cursor""" # find the start - while (line := self.next_line()) is not None and not is_comment_start(line): - pass - if line is None: - return None + while (line := self.next_line()) is not None: + if is_comment_start(line): + return self._read_doc_comment(line) + elif is_fragment_start(line): + return self._read_fragment() + return None + + def _read_doc_comment(self, starting_line: str) -> Optional[list[str]]: + """Reads a documentation comment (/** ... */) and advances the cursor""" + lines = [] + line = starting_line stripped = strip_line(line) if stripped: lines.append(stripped) - if stripped.endswith("*/"): + if line.endswith("*/"): return lines if lines else None while (line := self.next_line()) is not None: @@ -131,6 +152,17 @@ def next_doc_comment(self) -> Optional[list[str]]: return lines if lines else None + def _read_fragment(self) -> Optional[str]: + """Reads an inline fragment comment (/* @lua-fragment ... */) and advances the cursor""" + + body = "" + while (line := self.next_line()) is not None: + if line.endswith("*/"): + break + body += "\n" + line + + return body + def read_class_body(self) -> list[list[str]]: """The reader must be at the first line of the class/struct body. All comments inside the class are returned.""" items = [] @@ -143,7 +175,8 @@ def read_class_body(self) -> list[list[str]]: nesting += line.count("{") - line.count("}") self.next_line() continue - doc = self.next_doc_comment() + doc = self.next_item() + assert not isinstance(doc, str), "Fragment inside class body found" if not doc: break items.append(doc) @@ -153,6 +186,7 @@ def read_enum_variants(self) -> list[str]: """The reader must be before an enum class definition (possibly with some comments before). It returns all variants.""" items = [] is_comment = False + waiting_for_end = False while (line := self.peek_line()) is not None and not line.startswith("};"): self.next_line() if is_comment: @@ -175,7 +209,16 @@ def read_enum_variants(self) -> list[str]: if line.startswith("enum class"): continue - items.append(line.rstrip(",")) + if waiting_for_end: + if line.endswith(","): + waiting_for_end = False + continue + m = re.match(r"^([\w_]+)", line) + if not m: + continue + + items.append(m.group(1)) + waiting_for_end = not strip_comments(line).endswith(",") return items @@ -214,6 +257,25 @@ def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper): out.write(f"function {name}({lua_params}) end\n\n") +def write_enum( + name: str, + description: str | None, + variants: list[str], + is_flags: bool, + out: TextIOWrapper, +): + if description: + out.write(f"--- {description}\n") + out.write(f"---@enum {name}\n") + out.write(f"{name} = {{\n") + + def value(variant): + return "0," if is_flags else f"{{}}, ---@type {name}.{variant}" + + out.write("\n".join([f" {variant} = {value(variant)}" for variant in variants])) + out.write("\n}\n\n") + + def read_file(path: Path, out: TextIOWrapper): print("Reading", path.relative_to(repo_root)) with path.open("r") as f: @@ -221,9 +283,14 @@ def read_file(path: Path, out: TextIOWrapper): reader = Reader(lines) while reader.has_next(): - doc_comment = reader.next_doc_comment() + doc_comment = reader.next_item() if not doc_comment: break + elif isinstance(doc_comment, str): + out.write(doc_comment) + out.write("\n") + continue + header_comment = None if not doc_comment[0].startswith("@"): if len(doc_comment) == 1: @@ -241,21 +308,23 @@ def read_file(path: Path, out: TextIOWrapper): reader.line_no(), f"Invalid enum exposure - one command expected, got {len(header)}", ) - name = header[0].split(" ", 1)[1] - printmsg(path, reader.line_no(), f"enum {name}") - if header_comment: - out.write(f"--- {header_comment}\n") - out.write(f"---@enum {name}\n") - out.write(f"{name} = {{\n") - out.write( - "\n".join( - [ - f" {variant} = {{}}, ---@type {name}.{variant}" - for variant in reader.read_enum_variants() - ] + args = header[0].split(" ")[1:] + if len(args) < 1 or len(args) > 2: + panic( + path, + reader.line_no(), + f"Invalid @exposeenum - expected 2 arguments, got {len(args)}", ) + name = args[0] + is_flags = len(args) >= 2 and args[1] == "[flags]" + printmsg(path, reader.line_no(), f"enum {name}") + write_enum( + name, + header_comment, + reader.read_enum_variants(), + is_flags, + out, ) - out.write("\n}\n\n") continue # class diff --git a/src/common/enums/MessageContext.hpp b/src/common/enums/MessageContext.hpp index 669e5531511..5eaa7aed6ec 100644 --- a/src/common/enums/MessageContext.hpp +++ b/src/common/enums/MessageContext.hpp @@ -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 diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index bd83dee5aff..cd05af505b6 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -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 common/network/NetworkCommon.hpp */ diff --git a/src/controllers/plugins/api/Message.hpp b/src/controllers/plugins/api/Message.hpp index f5dc5573dc1..85dd03e1c4d 100644 --- a/src/controllers/plugins/api/Message.hpp +++ b/src/controllers/plugins/api/Message.hpp @@ -6,6 +6,93 @@ 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 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" 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 diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index c19ed5c0c8a..951bc34ff74 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -31,6 +31,7 @@ using ImagePtr = std::shared_ptr; struct Emote; using EmotePtr = std::shared_ptr; +/** @exposeenum c2.MessageElementFlag [flags] */ enum class MessageElementFlag : int64_t { None = 0LL, Misc = (1LL << 0), diff --git a/src/messages/MessageFlag.hpp b/src/messages/MessageFlag.hpp index 306587a0982..29a8fa92420 100644 --- a/src/messages/MessageFlag.hpp +++ b/src/messages/MessageFlag.hpp @@ -6,6 +6,7 @@ namespace chatterino { +/** @exposeenum c2.MessageFlag [flags] */ enum class MessageFlag : std::int64_t { None = 0LL, System = (1LL << 0), diff --git a/src/singletons/Fonts.hpp b/src/singletons/Fonts.hpp index e6ea324a04a..26e6be3395a 100644 --- a/src/singletons/Fonts.hpp +++ b/src/singletons/Fonts.hpp @@ -14,6 +14,7 @@ namespace chatterino { class Settings; class Paths; +/** @exposeenum c2.FontStyle */ enum class FontStyle : uint8_t { Tiny, ChatSmall, From 5dee992fcd3bdde99321eee8ff80f3a81b75c5ba Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 8 Dec 2024 18:05:53 +0100 Subject: [PATCH 3/6] fix: tests --- src/controllers/plugins/api/Message.cpp | 22 +++++++++++++++---- .../PluginMessageCtor/properties.json | 2 +- .../PluginMessageCtor/timestamp-element.json | 4 ++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/controllers/plugins/api/Message.cpp b/src/controllers/plugins/api/Message.cpp index 02e7d3ba3fd..dd0ae7a87fe 100644 --- a/src/controllers/plugins/api/Message.cpp +++ b/src/controllers/plugins/api/Message.cpp @@ -1,5 +1,6 @@ #include "controllers/plugins/api/Message.hpp" +#include "Application.hpp" #include "messages/MessageElement.hpp" #ifdef CHATTERINO_HAVE_PLUGINS @@ -13,6 +14,20 @@ 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) { @@ -70,7 +85,7 @@ std::unique_ptr timestampElementFromTable( if (time) { return std::make_unique( - QDateTime::fromMSecsSinceEpoch(*time).time()); + datetimeFromOffset(*time).time()); } return std::make_unique(); } @@ -147,7 +162,7 @@ std::shared_ptr messageFromTable(const sol::table &tbl) auto parseTime = tbl.get>("parse_time"); if (parseTime) { - msg->parseTime = QDateTime::fromMSecsSinceEpoch(*parseTime).time(); + msg->parseTime = datetimeFromOffset(*parseTime).time(); } msg->id = tbl.get_or("id", QString{}); @@ -169,8 +184,7 @@ std::shared_ptr messageFromTable(const sol::table &tbl) tbl.get>("server_received_time"); if (serverReceivedTime) { - msg->serverReceivedTime = - QDateTime::fromMSecsSinceEpoch(*serverReceivedTime); + msg->serverReceivedTime = datetimeFromOffset(*serverReceivedTime); } // missing: badges diff --git a/tests/snapshots/PluginMessageCtor/properties.json b/tests/snapshots/PluginMessageCtor/properties.json index fb8ec0a2a5f..33f56755d77 100644 --- a/tests/snapshots/PluginMessageCtor/properties.json +++ b/tests/snapshots/PluginMessageCtor/properties.json @@ -51,7 +51,7 @@ "loginName": "login", "messageText": "message", "searchText": "search", - "serverReceivedTime": "1970-01-01T01:20:30", + "serverReceivedTime": "1970-01-01T00:20:30Z", "timeoutUser": "", "usernameColor": "#ff0000ff" } diff --git a/tests/snapshots/PluginMessageCtor/timestamp-element.json b/tests/snapshots/PluginMessageCtor/timestamp-element.json index 76b12ac8f6e..4d37a94216b 100644 --- a/tests/snapshots/PluginMessageCtor/timestamp-element.json +++ b/tests/snapshots/PluginMessageCtor/timestamp-element.json @@ -56,7 +56,7 @@ "trailingSpace": true, "type": "TextElement", "words": [ - "1:20" + "0:20" ] }, "flags": "Timestamp", @@ -65,7 +65,7 @@ "type": "None", "value": "" }, - "time": "01:20:30", + "time": "00:20:30", "tooltip": "", "trailingSpace": true, "type": "TimestampElement" From 311ac91944f382887c8c0c4158bdfaadbd7f240e Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 8 Dec 2024 18:32:20 +0100 Subject: [PATCH 4/6] why does ubuntu 22.04 compare case insensitively? --- tests/src/Plugins.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/src/Plugins.cpp b/tests/src/Plugins.cpp index 63e6c94782b..b49fe25e3f9 100644 --- a/tests/src/Plugins.cpp +++ b/tests/src/Plugins.cpp @@ -650,7 +650,7 @@ TEST_F(PluginTest, MessageElementFlag) for k, v in pairs(c2.MessageElementFlag) do table.insert(values, ("%s=0x%x"):format(k, v)) end - table.sort(values) + table.sort(values, function(a, b) return a:lower() > b:lower() end) out = table.concat(values, ",") )lua"); @@ -660,11 +660,11 @@ TEST_F(PluginTest, MessageElementFlag) "BadgeFfz=0x80000," "BadgeGlobalAuthority=0x2000," "BadgePredictions=0x4000," + "Badges=0x30000fe000," "BadgeSevenTV=0x1000000000," "BadgeSharedChannel=0x2000000000," "BadgeSubscription=0x10000," "BadgeVanity=0x20000," - "Badges=0x30000fe000," "BitsAmount=0x200000," "BitsAnimated=0x1000," "BitsStatic=0x800," From 41f44ebf7ef78ae6da6ea5009c12911773f3e6fa Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 8 Dec 2024 18:33:13 +0100 Subject: [PATCH 5/6] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad89b6710a6..94358712efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ - Minor: Added a setting to hide the scrollbar highlights. (#5732) - Minor: The window layout is now backed up like the other settings. (#5647) - Minor: Added `flags.similar` filter variable, allowing you to filter messages filtered by the R9K feature. (#5747) +- Minor: Added basic message API to plugins. (#5754) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426, #5612) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) From f223c5169e49ae6e0a66b79ac81f5ec851d1d6ad Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 8 Dec 2024 18:51:45 +0100 Subject: [PATCH 6/6] silly --- tests/src/Plugins.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/Plugins.cpp b/tests/src/Plugins.cpp index b49fe25e3f9..ee468c1fafa 100644 --- a/tests/src/Plugins.cpp +++ b/tests/src/Plugins.cpp @@ -650,7 +650,7 @@ TEST_F(PluginTest, MessageElementFlag) for k, v in pairs(c2.MessageElementFlag) do table.insert(values, ("%s=0x%x"):format(k, v)) end - table.sort(values, function(a, b) return a:lower() > b:lower() end) + table.sort(values, function(a, b) return a:lower() < b:lower() end) out = table.concat(values, ",") )lua");