feat(plugins): allow message introspection (#6353)
This allows users to introspect messages (i.e. look at the elements they're made up of). Reviewed-by: Mm2PL <mm2pl+gh@kotmisia.pl>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
-- These tests mirror the code example from the docs - make sure they work
|
||||
local tests = {
|
||||
frozen_messages = function()
|
||||
local my_msg = c2.Message.new({ id = "foobar" })
|
||||
assert(not my_msg.frozen)
|
||||
local chan = c2.Channel.by_name("mm2pl")
|
||||
assert(chan)
|
||||
chan:add_message(my_msg)
|
||||
assert(my_msg.frozen)
|
||||
end,
|
||||
element_access = function()
|
||||
local my_msg = c2.Message.new({
|
||||
id = "foo",
|
||||
elements = {
|
||||
{ type = "text", text = "foo" },
|
||||
{ type = "text", text = "bar" },
|
||||
{ type = "text", text = "baz" },
|
||||
}
|
||||
})
|
||||
local elements = my_msg:elements()
|
||||
assert(#elements == 3)
|
||||
assert(elements[1].words[1] == "foo")
|
||||
assert(elements[2].words[1] == "bar")
|
||||
|
||||
elements:erase(2) -- erase "bar"
|
||||
assert(#elements == 2)
|
||||
assert(elements[1].words[1] == "foo")
|
||||
assert(elements[2].words[1] == "baz")
|
||||
end,
|
||||
append_element = function()
|
||||
local my_msg = c2.Message.new({ id = "foo" })
|
||||
local els = my_msg:elements()
|
||||
assert(#els == 0)
|
||||
my_msg:append_element({
|
||||
type = "text",
|
||||
text = "My text element",
|
||||
})
|
||||
assert(#els == 1)
|
||||
assert(els[1].type == "text")
|
||||
end
|
||||
}
|
||||
|
||||
for name, fn in pairs(tests) do
|
||||
local ok, res = pcall(fn)
|
||||
if not ok then
|
||||
error(name .. " failed: " .. res)
|
||||
end
|
||||
end
|
||||
@@ -14,6 +14,7 @@
|
||||
# include "controllers/plugins/SolTypes.hpp" // IWYU pragma: keep
|
||||
# include "lib/Snapshot.hpp"
|
||||
# include "messages/Message.hpp"
|
||||
# include "messages/MessageElement.hpp"
|
||||
# include "mocks/BaseApplication.hpp"
|
||||
# include "mocks/Channel.hpp"
|
||||
# include "mocks/EmoteController.hpp"
|
||||
@@ -1018,6 +1019,417 @@ TEST_F(PluginTest, ChannelAddMessage)
|
||||
ASSERT_EQ(added[5].first, logged[2]);
|
||||
}
|
||||
|
||||
TEST_F(PluginTest, MessageFrozenFlag)
|
||||
{
|
||||
configure();
|
||||
sol::protected_function isFrozenFn = lua->script(R"lua(
|
||||
return function(msg)
|
||||
return msg.frozen
|
||||
end
|
||||
)lua");
|
||||
sol::protected_function setFrozenFn = lua->script(R"lua(
|
||||
return function(msg, val)
|
||||
msg.frozen = val
|
||||
end
|
||||
)lua");
|
||||
|
||||
auto liquid = std::make_shared<Message>();
|
||||
auto res = isFrozenFn(liquid);
|
||||
ASSERT_TRUE(res.valid());
|
||||
ASSERT_FALSE(res.get<bool>());
|
||||
|
||||
auto frozen = std::make_shared<Message>();
|
||||
frozen->freeze();
|
||||
res = isFrozenFn(frozen);
|
||||
ASSERT_TRUE(res.valid());
|
||||
ASSERT_TRUE(res.get<bool>());
|
||||
|
||||
// we shouldn't be able to modify the flag
|
||||
ASSERT_FALSE(setFrozenFn(liquid, true).valid());
|
||||
ASSERT_FALSE(setFrozenFn(liquid, false).valid());
|
||||
ASSERT_FALSE(setFrozenFn(frozen, true).valid());
|
||||
ASSERT_FALSE(setFrozenFn(frozen, false).valid());
|
||||
}
|
||||
|
||||
TEST_F(PluginTest, MessageFlagModification)
|
||||
{
|
||||
configure();
|
||||
sol::protected_function pfn = lua->script(R"lua(
|
||||
return function(msg)
|
||||
assert(msg.flags == c2.MessageFlag.Debug)
|
||||
msg.flags = c2.MessageFlag.System
|
||||
assert(msg.flags == c2.MessageFlag.System)
|
||||
end
|
||||
)lua");
|
||||
sol::protected_function isFrozenFn = lua->script(R"lua(
|
||||
return function(msg)
|
||||
return msg.frozen
|
||||
end
|
||||
)lua");
|
||||
|
||||
auto liquid = std::make_shared<Message>();
|
||||
liquid->flags = MessageFlag::Debug;
|
||||
auto res = pfn(liquid);
|
||||
ASSERT_TRUE(res.valid());
|
||||
|
||||
// for the flags, it shouldn't matter if the message is frozen
|
||||
auto frozen = std::make_shared<Message>();
|
||||
frozen->flags = MessageFlag::Debug;
|
||||
frozen->freeze();
|
||||
res = pfn(frozen);
|
||||
ASSERT_TRUE(res.valid());
|
||||
}
|
||||
|
||||
TEST_F(PluginTest, MessageModification)
|
||||
{
|
||||
configure();
|
||||
|
||||
// Test that we can modify properties and that Lua sees the modification
|
||||
sol::table tests = lua->script(R"lua(
|
||||
return {
|
||||
function(msg)
|
||||
msg.parse_time = 1234567
|
||||
end,
|
||||
function(msg)
|
||||
assert(msg.id == "abc")
|
||||
msg.id = "1234"
|
||||
assert(msg.id == "1234")
|
||||
end,
|
||||
function(msg)
|
||||
assert(msg.search_text == "search")
|
||||
msg.search_text = "query"
|
||||
assert(msg.search_text == "query")
|
||||
end,
|
||||
function(msg)
|
||||
assert(msg.message_text == "msg")
|
||||
msg.message_text = "text"
|
||||
assert(msg.message_text == "text")
|
||||
end,
|
||||
function(msg)
|
||||
assert(msg.login_name == "login")
|
||||
msg.login_name = "name"
|
||||
assert(msg.login_name == "name")
|
||||
end,
|
||||
function(msg)
|
||||
assert(msg.display_name == "display")
|
||||
msg.display_name = "name"
|
||||
assert(msg.display_name == "name")
|
||||
end,
|
||||
function(msg)
|
||||
assert(msg.localized_name == "localized")
|
||||
msg.localized_name = "name"
|
||||
assert(msg.localized_name == "name")
|
||||
end,
|
||||
function(msg)
|
||||
assert(msg.user_id == "id")
|
||||
msg.user_id = "id"
|
||||
assert(msg.user_id == "id")
|
||||
end,
|
||||
function(msg)
|
||||
assert(msg.channel_name == "channel")
|
||||
msg.channel_name = "name"
|
||||
assert(msg.channel_name == "name")
|
||||
end,
|
||||
function(msg)
|
||||
assert(msg.username_color == "#ffaabbcc")
|
||||
msg.username_color = "#ccbbaaff"
|
||||
assert(msg.username_color == "#ccbbaaff")
|
||||
end,
|
||||
function(msg)
|
||||
assert(msg.server_received_time == 1230000)
|
||||
msg.server_received_time = 1240000
|
||||
assert(msg.server_received_time == 1240000)
|
||||
end,
|
||||
function(msg)
|
||||
print(msg.highlight_color)
|
||||
assert(msg.highlight_color == "#ff223344")
|
||||
msg.highlight_color = "#44332211"
|
||||
assert(msg.highlight_color == "#44332211")
|
||||
end,
|
||||
function(msg)
|
||||
assert(#msg:elements() == 2)
|
||||
msg:append_element({ type = "linebreak" })
|
||||
assert(#msg:elements() == 3)
|
||||
assert(msg:elements()[3].type == "linebreak")
|
||||
end,
|
||||
}
|
||||
)lua");
|
||||
|
||||
auto makeMsg = [] {
|
||||
auto msg = std::make_shared<Message>();
|
||||
msg->flags = MessageFlag::Debug;
|
||||
msg->id = "abc";
|
||||
msg->searchText = "search";
|
||||
msg->messageText = "msg";
|
||||
msg->loginName = "login";
|
||||
msg->displayName = "display";
|
||||
msg->localizedName = "localized";
|
||||
msg->userID = "id";
|
||||
msg->channelName = "channel";
|
||||
msg->usernameColor = QColor(0xaabbcc);
|
||||
msg->serverReceivedTime = QDateTime::fromMSecsSinceEpoch(1230000);
|
||||
msg->highlightColor = std::make_shared<QColor>(0x223344);
|
||||
msg->elements.push_back(
|
||||
std::make_unique<TextElement>("lol", MessageElementFlag::Text));
|
||||
msg->elements.push_back(
|
||||
std::make_unique<TextElement>("wow", MessageElementFlag::Text));
|
||||
return msg;
|
||||
};
|
||||
|
||||
auto liquid = makeMsg();
|
||||
ASSERT_TRUE(tests.valid());
|
||||
for (const auto &[_key, cb] : tests)
|
||||
{
|
||||
sol::protected_function pf = cb;
|
||||
auto res = pf(liquid);
|
||||
if (!res.valid())
|
||||
{
|
||||
sol::error err = res;
|
||||
ASSERT_TRUE(false) << err.what();
|
||||
}
|
||||
}
|
||||
|
||||
// If the message is frozen, all modifications should fail with an error
|
||||
auto frozen = makeMsg();
|
||||
frozen->freeze();
|
||||
for (const auto &[key, cb] : tests)
|
||||
{
|
||||
sol::protected_function pf = cb;
|
||||
auto res = pf(frozen);
|
||||
ASSERT_FALSE(res.valid());
|
||||
sol::error err = res;
|
||||
ASSERT_EQ(std::string_view(err.what()), "Message is frozen");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(PluginTest, MessageConstness)
|
||||
{
|
||||
configure();
|
||||
sol::protected_function pfn = lua->script(R"lua(
|
||||
return function(msg)
|
||||
assert(msg.login_name == "hello")
|
||||
msg.login_name = "alien"
|
||||
end
|
||||
)lua");
|
||||
|
||||
auto msg = std::make_shared<Message>();
|
||||
msg->loginName = "hello";
|
||||
MessagePtr cmsg = msg;
|
||||
|
||||
auto res = pfn(cmsg);
|
||||
ASSERT_TRUE(res.valid());
|
||||
cmsg->freeze();
|
||||
res = pfn(cmsg);
|
||||
ASSERT_FALSE(res.valid());
|
||||
}
|
||||
|
||||
// Test that we can access properties of message elements
|
||||
TEST_F(PluginTest, MessageElementAccess)
|
||||
{
|
||||
configure();
|
||||
sol::protected_function pfn = lua->script(R"lua(
|
||||
return function(msg, idx, prop)
|
||||
return msg:elements()[idx][prop]
|
||||
end
|
||||
)lua");
|
||||
|
||||
auto msg = std::make_shared<Message>();
|
||||
msg->elements.emplace_back(
|
||||
std::make_unique<TextElement>("my text", MessageElementFlag::Text));
|
||||
msg->elements.emplace_back(std::make_unique<SingleLineTextElement>(
|
||||
"single line", MessageElementFlag::Text));
|
||||
msg->elements.emplace_back(std::make_unique<CircularImageElement>(
|
||||
ImagePtr{}, 2, QColor(0xabcdef), MessageElementFlag::ReplyButton));
|
||||
msg->elements.emplace_back(std::make_unique<MentionElement>(
|
||||
"display", "login", MessageColor::Text, MessageColor::System));
|
||||
|
||||
msg->elements[1]->setTooltip("tooltip");
|
||||
msg->elements[2]->setTrailingSpace(false);
|
||||
msg->freeze();
|
||||
|
||||
auto getAll = [&](std::string_view key) {
|
||||
return std::array{
|
||||
pfn(msg, 1, key).get<sol::object>(),
|
||||
pfn(msg, 2, key).get<sol::object>(),
|
||||
pfn(msg, 3, key).get<sol::object>(),
|
||||
pfn(msg, 4, key).get<sol::object>(),
|
||||
};
|
||||
};
|
||||
|
||||
auto types = getAll("type");
|
||||
ASSERT_EQ(types[0].as<std::string>(), "text");
|
||||
ASSERT_EQ(types[1].as<std::string>(), "single-line-text");
|
||||
ASSERT_EQ(types[2].as<std::string>(), "circular-image");
|
||||
ASSERT_EQ(types[3].as<std::string>(), "mention");
|
||||
|
||||
auto flags = getAll("flags");
|
||||
ASSERT_EQ(flags[0].as<MessageElementFlag>(), MessageElementFlag::Text);
|
||||
ASSERT_EQ(flags[1].as<MessageElementFlag>(), MessageElementFlag::Text);
|
||||
ASSERT_EQ(flags[2].as<MessageElementFlag>(),
|
||||
MessageElementFlag::ReplyButton);
|
||||
ASSERT_EQ(flags[3].as<MessageElementFlag>(),
|
||||
(MessageElementFlags(MessageElementFlag::Text,
|
||||
MessageElementFlag::Mention)));
|
||||
|
||||
auto tooltips = getAll("tooltip");
|
||||
ASSERT_EQ(tooltips[0].as<std::string>(), "");
|
||||
ASSERT_EQ(tooltips[1].as<std::string>(), "tooltip");
|
||||
ASSERT_EQ(tooltips[2].as<std::string>(), "");
|
||||
ASSERT_EQ(tooltips[3].as<std::string>(), "");
|
||||
|
||||
auto spaces = getAll("trailing_space");
|
||||
ASSERT_TRUE(spaces[0].as<bool>());
|
||||
ASSERT_TRUE(spaces[1].as<bool>());
|
||||
ASSERT_FALSE(spaces[2].as<bool>());
|
||||
ASSERT_TRUE(spaces[3].as<bool>());
|
||||
|
||||
// Properties only found on _some_ elements should not error
|
||||
// (like non existent properties)
|
||||
auto paddings = getAll("padding");
|
||||
ASSERT_TRUE(paddings[0].is<std::nullptr_t>());
|
||||
ASSERT_TRUE(paddings[1].is<std::nullptr_t>());
|
||||
ASSERT_EQ(paddings[2].as<int>(), 2);
|
||||
ASSERT_TRUE(paddings[3].is<std::nullptr_t>());
|
||||
|
||||
auto words = getAll("words");
|
||||
ASSERT_EQ(words[0].as<std::vector<std::string>>(),
|
||||
(std::vector<std::string>{"my", "text"}));
|
||||
ASSERT_EQ(words[1].as<std::vector<std::string>>(),
|
||||
(std::vector<std::string>{"single", "line"}));
|
||||
ASSERT_TRUE(words[2].is<std::nullptr_t>());
|
||||
// mention elements are also text elements
|
||||
ASSERT_EQ(words[3].as<std::vector<std::string>>(),
|
||||
(std::vector<std::string>{"display"}));
|
||||
|
||||
auto userLogins = getAll("user_login_name");
|
||||
ASSERT_TRUE(userLogins[0].is<std::nullptr_t>());
|
||||
ASSERT_TRUE(userLogins[1].is<std::nullptr_t>());
|
||||
ASSERT_TRUE(userLogins[2].is<std::nullptr_t>());
|
||||
ASSERT_EQ(userLogins[3].as<std::string>(), "login");
|
||||
|
||||
auto times = getAll("time");
|
||||
ASSERT_TRUE(times[0].is<std::nullptr_t>());
|
||||
ASSERT_TRUE(times[1].is<std::nullptr_t>());
|
||||
ASSERT_TRUE(times[2].is<std::nullptr_t>());
|
||||
ASSERT_TRUE(times[3].is<std::nullptr_t>());
|
||||
|
||||
auto nonExistent = getAll("non_existent");
|
||||
ASSERT_TRUE(nonExistent[0].is<std::nullptr_t>());
|
||||
ASSERT_TRUE(nonExistent[1].is<std::nullptr_t>());
|
||||
ASSERT_TRUE(nonExistent[2].is<std::nullptr_t>());
|
||||
ASSERT_TRUE(nonExistent[3].is<std::nullptr_t>());
|
||||
|
||||
auto links = getAll("link");
|
||||
ASSERT_TRUE(links[0].is<Link>());
|
||||
ASSERT_TRUE(links[1].is<Link>());
|
||||
ASSERT_TRUE(links[2].is<Link>());
|
||||
ASSERT_TRUE(links[3].is<Link>());
|
||||
|
||||
// test that accessing anything outside the elements vector causes an error
|
||||
auto res = pfn(msg, 0, "flags");
|
||||
ASSERT_FALSE(res.valid());
|
||||
res = pfn(msg, 42, "flags");
|
||||
ASSERT_FALSE(res.valid());
|
||||
}
|
||||
|
||||
// Test that we can modify properties of message elements
|
||||
TEST_F(PluginTest, MessageElementModification)
|
||||
{
|
||||
configure();
|
||||
sol::protected_function pfn = lua->script(R"lua(
|
||||
return function(msg, idx, prop, val)
|
||||
msg:elements()[idx][prop] = val
|
||||
end
|
||||
)lua");
|
||||
|
||||
// same as MessageElementAccess...
|
||||
auto msg = std::make_shared<Message>();
|
||||
msg->elements.emplace_back(
|
||||
std::make_unique<TextElement>("my text", MessageElementFlag::Text));
|
||||
msg->elements.emplace_back(std::make_unique<SingleLineTextElement>(
|
||||
"single line", MessageElementFlag::Text));
|
||||
msg->elements.emplace_back(std::make_unique<CircularImageElement>(
|
||||
ImagePtr{}, 2, QColor(0xabcdef), MessageElementFlag::ReplyButton));
|
||||
msg->elements.emplace_back(std::make_unique<MentionElement>(
|
||||
"display", "login", MessageColor::Text, MessageColor::System));
|
||||
|
||||
msg->elements[1]->setTooltip("tooltip");
|
||||
msg->elements[2]->setTrailingSpace(false);
|
||||
// ...but we don't freeze the message here
|
||||
|
||||
auto setAll = [&](std::string_view key, auto value) {
|
||||
for (size_t i = 1; i <= 4; i++)
|
||||
{
|
||||
EXPECT_TRUE(pfn(msg, i, key, value).valid());
|
||||
}
|
||||
};
|
||||
setAll("tooltip", "tool");
|
||||
ASSERT_EQ(msg->elements[0]->getTooltip(), "tool");
|
||||
ASSERT_EQ(msg->elements[1]->getTooltip(), "tool");
|
||||
ASSERT_EQ(msg->elements[2]->getTooltip(), "tool");
|
||||
ASSERT_EQ(msg->elements[3]->getTooltip(), "tool");
|
||||
|
||||
setAll("trailing_space", false);
|
||||
ASSERT_FALSE(msg->elements[0]->hasTrailingSpace());
|
||||
ASSERT_FALSE(msg->elements[1]->hasTrailingSpace());
|
||||
ASSERT_FALSE(msg->elements[2]->hasTrailingSpace());
|
||||
ASSERT_FALSE(msg->elements[3]->hasTrailingSpace());
|
||||
|
||||
pfn(msg, 1, "link", Link{Link::CopyToClipboard, "foo"});
|
||||
pfn(msg, 2, "link", Link{Link::CopyToClipboard, "foo"});
|
||||
pfn(msg, 3, "link", Link{Link::CopyToClipboard, "foo"});
|
||||
// can't modify links of mention elements
|
||||
ASSERT_FALSE(
|
||||
pfn(msg, 4, "link", Link{Link::CopyToClipboard, "foo"}).valid());
|
||||
|
||||
ASSERT_EQ(msg->elements[0]->getLink().type, Link::CopyToClipboard);
|
||||
ASSERT_EQ(msg->elements[0]->getLink().value, "foo");
|
||||
ASSERT_EQ(msg->elements[0]->getTooltip(), "<b>Copy to clipboard</b>");
|
||||
ASSERT_EQ(msg->elements[1]->getLink().type, Link::CopyToClipboard);
|
||||
ASSERT_EQ(msg->elements[1]->getLink().value, "foo");
|
||||
ASSERT_EQ(msg->elements[2]->getLink().type, Link::CopyToClipboard);
|
||||
ASSERT_EQ(msg->elements[2]->getLink().value, "foo");
|
||||
|
||||
auto expectErr = [&](std::string_view key, auto value) {
|
||||
for (size_t i = 1; i <= 4; i++)
|
||||
{
|
||||
auto result = pfn(msg, i, key, value);
|
||||
EXPECT_FALSE(result.valid()) << key;
|
||||
}
|
||||
};
|
||||
expectErr("type", "something");
|
||||
expectErr("trailing_space", "something");
|
||||
|
||||
// can't set these types
|
||||
expectErr("link", Link{Link::ViewThread, "foo"});
|
||||
expectErr("link", Link{Link::AutoModAllow, "foo"});
|
||||
|
||||
// We can't modify these yet
|
||||
expectErr("padding", 1);
|
||||
expectErr("background", 0xabcdef12);
|
||||
expectErr("words", QStringList{"a", "b"});
|
||||
expectErr("color", 0x1234);
|
||||
expectErr("style", FontStyle::ChatMedium);
|
||||
expectErr("lowercase", "abc");
|
||||
expectErr("original", "or");
|
||||
expectErr("fallback_color", "system");
|
||||
expectErr("user_color", "system");
|
||||
expectErr("user_login_name", "system");
|
||||
expectErr("time", 42);
|
||||
|
||||
// test that accessing anything outside the elements vector causes an error
|
||||
auto res = pfn(msg, 0, "trailing_space", true);
|
||||
ASSERT_FALSE(res.valid());
|
||||
res = pfn(msg, 42, "trailing_space", true);
|
||||
ASSERT_FALSE(res.valid());
|
||||
|
||||
// we can't modify anything on frozen messages
|
||||
msg->freeze();
|
||||
expectErr("tooltip", "tool");
|
||||
expectErr("trailing_space", false);
|
||||
expectErr("padding", 1);
|
||||
}
|
||||
|
||||
/// Test that both C++ exceptions and luaL_error properly unwind the stack.
|
||||
TEST_F(PluginTest, LuaUnwind)
|
||||
{
|
||||
@@ -1139,6 +1551,28 @@ TEST_P(PluginJsonTest, Run)
|
||||
INSTANTIATE_TEST_SUITE_P(PluginJson, PluginJsonTest,
|
||||
testing::ValuesIn(discoverLuaTests("json")));
|
||||
|
||||
class PluginMessageTest : public PluginTest,
|
||||
public ::testing::WithParamInterface<QString>
|
||||
{
|
||||
};
|
||||
TEST_P(PluginMessageTest, Run)
|
||||
{
|
||||
configure();
|
||||
|
||||
auto pfr = lua->safe_script_file(luaTestPath("message", GetParam()));
|
||||
EXPECT_TRUE(pfr.valid());
|
||||
if (!pfr.valid())
|
||||
{
|
||||
qDebug() << "Test" << GetParam() << "failed:";
|
||||
sol::error err = pfr;
|
||||
qDebug() << err.what();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(PluginMessage, PluginMessageTest,
|
||||
testing::ValuesIn(discoverLuaTests("message")));
|
||||
|
||||
// verify that all snapshots are included
|
||||
TEST(PluginMessageConstructionTest, Integrity)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user