Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- Minor: Added WebSocket API for plugins. (#6076, #6186, #6314, #6315)
- Minor: Allow for themes to set transparent values for window background on Linux. (#6137)
- Minor: Popup overlay now only draws an outline when being interacted with. (#6140)
- Minor: Added basic message API to plugins. (#5754)
- Minor: Added basic message API to plugins. (#5754, #6386)
Copy link
Member

@pajlada pajlada Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix the changelog entry (move it to the right place)

- Minor: Made filters searchable in the Settings dialog search bar. (#5890)
- Minor: Updated emojis to Unicode 16.0. (#6155)
- Minor: Allow disabling of double-click tab renaming through setting. (#6163, #6184)
Expand Down
15 changes: 15 additions & 0 deletions docs/chatterino.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ declare namespace c2 {
interface MessageElementInitBase {
tooltip?: string;
trailing_space?: boolean;
link?: Link;
}

type MessageColor = "text" | "link" | "system" | string;
Expand Down Expand Up @@ -242,6 +243,20 @@ declare namespace c2 {
type: "reply-curve";
}

interface Link {
type: LinkType;
value: string;
}

enum LinkType {
Url,
UserInfo,
UserAction,
JumpToChannel,
CopyToClipboard,
JumpToMessage,
}

enum MessageFlag {
None = 0,
System = 0,
Expand Down
12 changes: 12 additions & 0 deletions docs/plugin-meta.lua
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ c2.Message = {}
---@class MessageElementInitBase
---@field tooltip? string Tooltip text
---@field trailing_space? boolean Whether to add a trailing space after the element (default: true)
---@field link? c2.Link An action when clicking on this element. Mention and Link elements don't support this. They manage the link themselves.

---@alias MessageColor "text"|"link"|"system"|string A color for a text element - "text", "link", and "system" are special values that take the current theme into account

Expand Down Expand Up @@ -344,6 +345,17 @@ c2.Message = {}
---@param init MessageInit The message initialization table
---@return c2.Message msg The new message
function c2.Message.new(init) end
---@alias c2.Link { type: c2.LinkType, value: string } A link on a message element.
---@enum c2.LinkType
c2.LinkType = {
Url = {}, ---@type c2.LinkType.Url
UserInfo = {}, ---@type c2.LinkType.UserInfo
UserAction = {}, ---@type c2.LinkType.UserAction
JumpToChannel = {}, ---@type c2.LinkType.JumpToChannel
CopyToClipboard = {}, ---@type c2.LinkType.CopyToClipboard
JumpToMessage = {}, ---@type c2.LinkType.JumpToMessage
}

-- Begin src/singletons/Fonts.hpp

---@enum c2.FontStyle
Expand Down
13 changes: 13 additions & 0 deletions docs/wip-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,19 @@ end)

The full range of options can be found in the typing files ([LuaLS](./plugin-meta.lua), [TypeScript](./chatterino.d.ts)).

#### `LinkType` enum

This table describes links available to plugins.

| `LinkType` | `c2.Link.value` content | Action on click | Example |
| ----------------- | ---------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------- |
| `Url` | Any URI that makes sense to open | Open Link in browser | `https://example.org` |
| `UserInfo` | A Twitch username or `id:TwitchID` | Open a usercard | `mm2pl`, `id:117691339` |
| `UserAction` | Command to run or message to send | Send command/message | `/timeout mm2pl 1s test`, `!spoilers` |
| `JumpToChannel` | [Channel name](#channelget_name) | Go to already open split with given channel | `#pajlada` |
| `CopyToClipboard` | Any Unicode text | Copy value to clipboard | n/a |
| `JumpToMessage` | ID of the message | Highlight the message with given ID in current split, do nothing if it was not found | n/a |

### Input/Output API

These functions are wrappers for Lua's I/O library. Functions on file pointer
Expand Down
2 changes: 1 addition & 1 deletion scripts/make_luals_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def read_enum_variants(self) -> list[str]:
for line in line[opener:closer].split(",")
]
break
if line.startswith("enum class"):
if line.startswith("enum "):
continue

if waiting_for_end:
Expand Down
2 changes: 2 additions & 0 deletions src/controllers/plugins/PluginController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin)
c2["MessageElementFlag"] = lua::createEnumTable<MessageElementFlag>(lua);
c2["FontStyle"] = lua::createEnumTable<FontStyle>(lua);
c2["MessageContext"] = lua::createEnumTable<MessageContext>(lua);
c2["LinkType"] =
lua::createEnumTable<lua::api::message::ExposedLinkType>(lua);

sol::table io = g["io"];
io.set_function(
Expand Down
44 changes: 44 additions & 0 deletions src/controllers/plugins/SolTypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

# include "Application.hpp"
# include "common/QLogging.hpp"
# include "controllers/plugins/api/Message.hpp"
# include "controllers/plugins/LuaAPI.hpp"
# include "controllers/plugins/PluginController.hpp"
# include "messages/Link.hpp"

# include <QObject>
# include <QStringBuilder>
Expand Down Expand Up @@ -140,6 +142,48 @@ int sol_lua_push(sol::types<chatterino::lua::ThisPluginState>, lua_State *L,

} // namespace chatterino::lua

namespace chatterino {

// Link
bool sol_lua_check(sol::types<chatterino::Link>, lua_State *L, int index,
chatterino::FunctionRef<sol::check_handler_type> handler,
sol::stack::record &tracking)
{
return sol::stack::check<sol::table>(L, index, handler, tracking);
}

chatterino::Link sol_lua_get(sol::types<chatterino::Link>, lua_State *L,
int index, sol::stack::record &tracking)
{
sol::table table = sol::stack::get<sol::table>(L, index, tracking);

auto ty =
table.get<sol::optional<lua::api::message::ExposedLinkType>>("type");
if (!ty)
{
throw std::runtime_error("Missing 'type' in Link");
}
auto value = table.get<sol::optional<QString>>("value");
if (!value)
{
throw std::runtime_error("Missing 'value' in Link");
}

return {static_cast<Link::Type>(*ty), *value};
}

int sol_lua_push(sol::types<chatterino::Link>, lua_State *L,
const chatterino::Link &value)
{
sol::table table = sol::table::create(L, 0, 2);
table.set("type",
static_cast<lua::api::message::ExposedLinkType>(value.type));
table.set("value", value.value);
return sol::stack::push(L, table);
}

} // namespace chatterino

// NOLINTEND(readability-named-parameter)

#endif
7 changes: 7 additions & 0 deletions src/controllers/plugins/SolTypes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ constexpr bool IsOptional<std::optional<T>> = true;
namespace chatterino {

class Plugin;
struct Link;

} // namespace chatterino

Expand Down Expand Up @@ -188,6 +189,12 @@ SOL_STACK_FUNCTIONS(chatterino::lua::ThisPluginState)

} // namespace chatterino::lua

namespace chatterino {

SOL_STACK_FUNCTIONS(chatterino::Link)

} // namespace chatterino

SOL_STACK_FUNCTIONS(QString)
SOL_STACK_FUNCTIONS(QStringList)
SOL_STACK_FUNCTIONS(QByteArray)
Expand Down
61 changes: 55 additions & 6 deletions src/controllers/plugins/api/Message.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
#include "controllers/plugins/api/Message.hpp"

#include "Application.hpp"
#include "messages/MessageElement.hpp"

#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/api/Message.hpp"

# include "Application.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include "controllers/plugins/SolTypes.hpp"
# include "messages/Message.hpp"
# include "messages/MessageElement.hpp"

# include <sol/sol.hpp>

Expand Down Expand Up @@ -128,6 +127,7 @@ std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
{
auto type = requiredGet<QString>(tbl, "type");
std::unique_ptr<MessageElement> el;
bool linksAllowed = true;
if (type == u"text")
{
el = textElementFromTable(tbl);
Expand All @@ -139,6 +139,7 @@ std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
else if (type == u"mention")
{
el = mentionElementFromTable(tbl);
linksAllowed = false;
}
else if (type == u"timestamp")
{
Expand All @@ -155,6 +156,7 @@ std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
else if (type == u"reply-curve")
{
el = replyCurveElementFromTable();
linksAllowed = false;
}
else
{
Expand All @@ -163,7 +165,54 @@ std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
assert(el);

el->setTrailingSpace(tbl.get_or("trailing_space", true));
el->setTooltip(tbl.get_or("tooltip", QString{}));

auto link = tbl.get<sol::optional<Link>>("link");
if (link)
{
if (!linksAllowed)
{
throw std::runtime_error("'link' not supported on type='" +
type.toStdString() + '\'');
}
el->setLink(*link);
QString tooltip;

switch (link->type)
{
case Link::Url:
tooltip = QString("<b>URL:</b> %1").arg(link->value);
break;
case Link::UserAction:
tooltip = QString("<b>Command:</b> %1").arg(link->value);
break;
case Link::CopyToClipboard:
tooltip = "<b>Copy to clipboard</b>";
break;

// these links should be safe to click as they don't have any immediate action associated with them
case Link::JumpToChannel:
case Link::JumpToMessage:
case Link::UserInfo:
case Link::UserWhisper:
case Link::ReplyToMessage:
break;

// these types are not exposed to plugins
case Link::None:
case Link::AutoModAllow:
case Link::AutoModDeny:
case Link::InsertText:
case Link::OpenAccountsPage:
case Link::Reconnect:
throw std::runtime_error(
"Invalid link type. How'd this happen?");
}
el->setTooltip(tooltip);
}
else
{
el->setTooltip(tbl.get_or("tooltip", QString{}));
}

return el;
}
Expand Down
18 changes: 18 additions & 0 deletions src/controllers/plugins/api/Message.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#pragma once
#ifdef CHATTERINO_HAVE_PLUGINS
# include "messages/Link.hpp"
# include "messages/Message.hpp"

# include <sol/forward.hpp>

# include <cstdint>

namespace chatterino::lua::api::message {

/* @lua-fragment
Expand Down Expand Up @@ -32,6 +35,7 @@ c2.Message = {}
---@class MessageElementInitBase
---@field tooltip? string Tooltip text
---@field trailing_space? boolean Whether to add a trailing space after the element (default: true)
---@field link? c2.Link An action when clicking on this element. Mention and Link elements don't support this. They manage the link themselves.

---@alias MessageColor "text"|"link"|"system"|string A color for a text element - "text", "link", and "system" are special values that take the current theme into account

Expand Down Expand Up @@ -86,6 +90,20 @@ c2.Message = {}
function c2.Message.new(init) end
*/

/** @lua@alias c2.Link { type: c2.LinkType, value: string } A link on a message element. */

// We only want certain links to be usable
// Note: code dependant on this needs these values to be meaningful `Link::Type`s
/** @exposeenum c2.LinkType */
enum class ExposedLinkType : std::uint8_t {
Url = Link::Type::Url,
UserInfo = Link::Type::UserInfo,
UserAction = Link::Type::UserAction, // run a command/send message
JumpToChannel = Link::Type::JumpToChannel,
CopyToClipboard = Link::Type::CopyToClipboard,
JumpToMessage = Link::Type::JumpToMessage,
};

/**
* @includefile singletons/Fonts.hpp
* @includefile messages/MessageElement.hpp
Expand Down
4 changes: 0 additions & 4 deletions src/messages/Link.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,9 @@ struct Link {
enum Type {
None,
Url,
CloseCurrentSplit,
UserInfo,
UserTimeout,
UserBan,
UserWhisper,
InsertText,
ShowMessage,
UserAction,
AutoModAllow,
AutoModDeny,
Expand Down
16 changes: 14 additions & 2 deletions src/messages/MessageElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,7 @@ void TimestampElement::addToContainer(MessageLayoutContainer &container,
{
if (ctx.flags.hasAny(this->getFlags()))
{
this->setTooltip(this->getTooltip());
if (getSettings()->timestampFormat != this->format_)
{
this->format_ = getSettings()->timestampFormat.getValue();
Expand All @@ -1122,8 +1123,19 @@ TextElement *TimestampElement::formatTime(const QTime &time)

QString format = locale.toString(time, getSettings()->timestampFormat);

return new TextElement(format, MessageElementFlag::Timestamp,
MessageColor::System, FontStyle::TimestampMedium);
auto *text =
new TextElement(format, MessageElementFlag::Timestamp,
MessageColor::System, FontStyle::TimestampMedium);
text->setLink(this->getLink());
text->setTooltip(this->getTooltip());
return text;
}

MessageElement *TimestampElement::setLink(const Link &link)
{
MessageElement::setLink(link);
this->element_->setLink(link);
return this;
}

QJsonObject TimestampElement::toJson() const
Expand Down
1 change: 1 addition & 0 deletions src/messages/MessageElement.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ class TimestampElement : public MessageElement
const MessageLayoutContext &ctx) override;

TextElement *formatTime(const QTime &time);
MessageElement *setLink(const Link &link) override;

QJsonObject toJson() const override;

Expand Down
10 changes: 7 additions & 3 deletions tests/snapshots/PluginMessageCtor/properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
" highlight_color = '#12345678',",
" channel_name = 'channel',",
" elements = {",
" { type = 'text', text = 'aliens walking' }",
" {",
" type = 'text',",
" text = 'aliens walking',",
" link = { type = c2.LinkType.UserInfo, value = 'twitchdev' }",
" }",
" }",
"}"
],
Expand All @@ -32,8 +36,8 @@
"color": "Text",
"flags": "Text",
"link": {
"type": "None",
"value": ""
"type": "UserInfo",
"value": "twitchdev"
},
"style": "ChatMedium",
"tooltip": "",
Expand Down
Loading