feat(plugins): add basic message construction (#5754)
This commit is contained in:
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
/** @exposeenum c2.MessageFlag [flags] */
|
||||
enum class MessageFlag : std::int64_t {
|
||||
None = 0LL,
|
||||
System = (1LL << 0),
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace chatterino {
|
||||
class Settings;
|
||||
class Paths;
|
||||
|
||||
/** @exposeenum c2.FontStyle */
|
||||
enum class FontStyle : uint8_t {
|
||||
Tiny,
|
||||
ChatSmall,
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user