#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 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; } template T requiredGet(const sol::table &tbl, auto &&key) { auto v = tbl.get>(std::forward(key)); if (!v) { throw std::runtime_error(std::string{"Missing required property: "} + key); } return *std::move(v); } std::unique_ptr textElementFromTable(const sol::table &tbl) { return std::make_unique( requiredGet(tbl, "text"), tbl.get_or("flags", MessageElementFlag::Text), MessageColor::fromLua(tbl.get_or("color", QString{})), tbl.get_or("style", FontStyle::ChatMedium)); } std::unique_ptr singleLineTextElementFromTable( const sol::table &tbl) { return std::make_unique( requiredGet(tbl, "text"), tbl.get_or("flags", MessageElementFlag::Text), MessageColor::fromLua(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( requiredGet(tbl, "display_name"), requiredGet(tbl, "login_name"), MessageColor::fromLua(requiredGet(tbl, "fallback_color")), MessageColor::fromLua(requiredGet(tbl, "user_color"))); } std::unique_ptr timestampElementFromTable( const sol::table &tbl) { // no flags! auto time = tbl.get>("time"); if (time) { return std::make_unique( datetimeFromOffset(*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(); } void setLinkOn(MessageElement *el, const Link &link) { el->setLink(link); QString tooltip; switch (link.type) { case Link::Url: tooltip = QString("URL: %1").arg(link.value); break; case Link::UserAction: tooltip = QString("Command: %1").arg(link.value); break; case Link::CopyToClipboard: tooltip = "Copy to clipboard"; break; // these links should be safe to click as they don't have any immediate action associated with them case Link::InsertText: 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::OpenAccountsPage: case Link::Reconnect: case Link::ViewThread: throw std::runtime_error("Invalid link type. How'd this happen?"); } el->setTooltip(tooltip); } std::unique_ptr elementFromTable(const sol::table &tbl) { auto type = requiredGet(tbl, "type"); std::unique_ptr el; bool linksAllowed = true; if (type == TextElement::TYPE) { el = textElementFromTable(tbl); } else if (type == SingleLineTextElement::TYPE) { el = singleLineTextElementFromTable(tbl); } else if (type == MentionElement::TYPE) { el = mentionElementFromTable(tbl); linksAllowed = false; } else if (type == TimestampElement::TYPE) { el = timestampElementFromTable(tbl); } else if (type == TwitchModerationElement::TYPE) { el = twitchModerationElementFromTable(); } else if (type == LinebreakElement::TYPE) { el = linebreakElementFromTable(tbl); } else if (type == ReplyCurveElement::TYPE) { el = replyCurveElementFromTable(); linksAllowed = false; } else { throw std::runtime_error("Invalid message type"); } assert(el); el->setTrailingSpace(tbl.get_or("trailing_space", true)); auto link = tbl.get>("link"); if (link) { if (!linksAllowed) { throw std::runtime_error("'link' not supported on type='" + type + '\''); } setLinkOn(el.get(), *link); } else { 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 = 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>("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(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; } void checkWritable(Message *msg) { if (msg->frozen) { throw std::runtime_error("Message is frozen"); } } template struct MemberPtrTraits; template struct MemberPtrTraits { using Type = T; using Object = C; }; template decltype(auto) memberAccessor() { return sol::property( [](Message *msg) { return msg->*T; }, [](Message *msg, typename MemberPtrTraits::Type v) { checkWritable(msg); msg->*T = std::forward(v); }); } } // namespace namespace chatterino::lua::api::message { struct ElementRef { ElementRef() = default; ElementRef(std::shared_ptr msg, size_t index) : msg(std::move(msg)) , index(index) { } MessageElement *element() const { if (!this->msg || this->index >= this->msg->elements.size()) { return nullptr; } checkWritable(this->msg.get()); return this->msg->elements[this->index].get(); } const MessageElement *constElement() const { if (!this->msg || this->index >= this->msg->elements.size()) { return nullptr; } return this->msg->elements[this->index].get(); } MessageElement &ref() const { auto *el = this->element(); if (!el) { throw std::runtime_error("Element does not exist or expired"); } return *el; } const MessageElement &cref() const { const auto *el = this->constElement(); if (!el) { throw std::runtime_error("Element does not exist or expired"); } return *el; } /// Cast this element to `T`. Otherwise nullopt is returned. /// Use `.map()` to access the content. template sol::optional as() const { // using ref() to error if the reference is invalid auto *el = dynamic_cast(&this->ref()); if (!el) { return sol::nullopt; } return *el; } /// Cast this element to `const T`. Otherwise nullopt is returned. /// Use `.map()` to access the content. template sol::optional asConst() const { // using cref() to error if the reference is invalid const auto *el = dynamic_cast(&this->cref()); if (!el) { return sol::nullopt; } return *el; } template bool is() const { return dynamic_cast(&this->cref()) != nullptr; } /// Visit this element by dynamic casting template auto visit(auto &&...cb) const { static_assert(sizeof...(T) == sizeof...(cb) && sizeof...(T) > 0); // infer the returned type inside the optional using Cb0 = std::tuple_element_t<0, std::tuple>; using T0 = std::tuple_element_t<0, std::tuple>; using TReturn = std::invoke_result_t; return this->visitOne(std::forward(cb)...); } bool operator==(const ElementRef &rhs) const { return this->msg.get() == rhs.msg.get() && this->index == rhs.index; } std::shared_ptr msg; size_t index = 0; private: template decltype(auto) maybeConstElement() const { if constexpr (Const) { return this->constElement(); } else { return this->element(); } } /// Run one callback /// /// This is called recursively. /// If the callback returns something, we return an `optional` otherwise /// we return `void`. template auto visitOne(auto &&cb, auto &&...rest) const -> std::conditional_t, void, sol::optional> { auto *el = dynamic_cast(this->maybeConstElement>()); if (!el) { if constexpr (sizeof...(rest) == 0) { if constexpr (std::is_void_v< std::invoke_result_t>) { return; } else { return sol::nullopt; } } else { return this->visitOne( std::forward(rest)...); } } return std::invoke(cb, *el); } }; struct ElementIterator { using difference_type = std::ptrdiff_t; using value_type = ElementRef; ElementIterator() = default; ElementIterator(std::shared_ptr msg, size_t index) : current(std::move(msg), index) { } ElementRef operator*() const { return this->current; } ElementRef operator[](size_t i) const { return {this->current.msg, this->current.index + i}; } ElementIterator &operator+=(difference_type diff) { this->current.index += diff; return *this; } ElementIterator &operator++() { return *this += 1; } ElementIterator operator++(int) // postfix increment { auto tmp = *this; ++*this; return tmp; } friend ElementIterator operator+(difference_type diff, const ElementIterator &it) { return {it.current.msg, it.current.index + diff}; } friend ElementIterator operator+(const ElementIterator &it, difference_type diff) { return {it.current.msg, it.current.index + diff}; } ElementIterator &operator-=(difference_type diff) { this->current.index -= diff; return *this; } ElementIterator &operator--() { return *this -= 1; } ElementIterator operator--(int) // postfix decrement { auto tmp = *this; --*this; return tmp; } friend difference_type operator-(const ElementIterator &lhs, const ElementIterator &rhs) { return static_cast(lhs.current.index) - static_cast(rhs.current.index); } friend ElementIterator operator-(difference_type diff, const ElementIterator &it) { return {it.current.msg, it.current.index - diff}; } friend ElementIterator operator-(const ElementIterator &it, difference_type diff) { return {it.current.msg, it.current.index - diff}; } bool operator==(const ElementIterator &rhs) const { return this->current == rhs.current; } bool operator<(const ElementIterator &rhs) const { return this->current.index < rhs.current.index; } bool operator<=(const ElementIterator &rhs) const { return this->current.index <= rhs.current.index; } bool operator>(const ElementIterator &rhs) const { return this->current.index > rhs.current.index; } bool operator>=(const ElementIterator &rhs) const { return this->current.index >= rhs.current.index; } ElementRef current; }; static_assert(std::random_access_iterator); struct MessageElements { using value_type = ElementIterator::value_type; using iterator = ElementIterator; using size_type = size_t; explicit MessageElements(std::shared_ptr msg) : msg(std::move(msg)) { } ~MessageElements() = default; MessageElements(const MessageElements &) = delete; MessageElements(MessageElements &&) = default; MessageElements &operator=(const MessageElements &) = delete; MessageElements &operator=(MessageElements &&) = default; ElementIterator begin() const { return {this->msg, 0}; } ElementIterator end() const { return {this->msg, this->msg->elements.size()}; } size_type size() const { if (!this->msg) { return 0; } return this->msg->elements.size(); } // NOLINTNEXTLINE size_type max_size() const { return this->size(); // we can't insert } bool empty() const { if (!this->msg) { return true; } return this->msg->elements.empty(); } // NOLINTNEXTLINE void push_back(ElementIterator::value_type /* v */) const { throw std::runtime_error("Insertion is not supported"); } // NOLINTNEXTLINE void erase(ElementIterator it) const { if (it.current.msg != this->msg || !this->msg || it.current.index >= this->msg->elements.size()) { throw std::runtime_error("Can't erase here"); } checkWritable(this->msg.get()); this->msg->elements.erase(this->msg->elements.begin() + static_cast(it.current.index)); } std::shared_ptr msg; }; void createUserType(sol::table &c2) { c2.new_usertype( "MessageElement", sol::no_constructor, // "type", sol::property([](const ElementRef &el) { return el.cref().type(); }), "flags", sol::property([](const ElementRef &el) { return el.cref().getFlags().value(); }), "add_flags", [](const ElementRef &el, MessageElementFlag flag) { el.ref().addFlags(flag); }, "link", sol::property( [](const ElementRef &el) { return el.cref().getLink(); }, [](const ElementRef &el, const Link &link) { if (el.is() || el.is()) { throw std::runtime_error( "Setting a link on this element is unsupported"); } setLinkOn(&el.ref(), link); }), "tooltip", sol::property( [](const ElementRef &el) { return el.cref().getTooltip(); }, [](const ElementRef &el, const QString &tooltip) { el.ref().setTooltip(tooltip); }), "trailing_space", sol::property( [](const ElementRef &el) { return el.cref().hasTrailingSpace(); }, [](const ElementRef &el, bool trailingSpace) { el.ref().setTrailingSpace(trailingSpace); }), "padding", sol::property([](const ElementRef &el) { return el.asConst().map( &CircularImageElement::padding); }), "background", sol::property([](const ElementRef &el) { return el.as().map( [](const CircularImageElement &el) { return el.background().name(QColor::HexArgb); }); }), "words", sol::property([](const ElementRef &el) { return el.visit( &TextElement::words, &SingleLineTextElement::words); }), "color", sol::property([](const ElementRef &el) { return el.visit( [](const TextElement &el) { return el.color().toLua(); }, [](const SingleLineTextElement &el) { return el.color().toLua(); }); }), "style", sol::property([](const ElementRef &el) { return el.visit( &TextElement::fontStyle, &SingleLineTextElement::fontStyle); }), "lowercase", sol::property([](const ElementRef &el) { return el.asConst().map(&LinkElement::lowercase); }), "original", sol::property([](const ElementRef &el) { return el.asConst().map(&LinkElement::original); }), "fallback_color", sol::property([](const ElementRef &el) { return el.asConst().map( [](const MentionElement &el) { return el.fallbackColor().toLua(); }); }), "user_color", sol::property([](const ElementRef &el) { return el.asConst().map( [](const MentionElement &el) { return el.userColor().toLua(); }); }), "user_login_name", sol::property([](const ElementRef &el) { return el.asConst().map( &MentionElement::userLoginName); }), "time", sol::property([](const ElementRef &el) { return el.asConst().map( [](const TimestampElement &el) { return QDateTime(QDate::currentDate(), el.time()) .toMSecsSinceEpoch(); }); })); c2.new_usertype( "Message", sol::factories([](const sol::table &tbl) { return messageFromTable(tbl); }), "flags", sol::property( [](Message *msg) { return msg->flags.value(); }, [](Message *msg, MessageFlag f) { // flags are always mutable msg->flags = f; }), "parse_time", sol::property( [](Message *msg) { return QDateTime(QDate::currentDate(), msg->parseTime) .toMSecsSinceEpoch(); }, [](Message *msg, qint64 ms) { checkWritable(msg); msg->parseTime = datetimeFromOffset(ms).time(); }), "id", memberAccessor<&Message::id>(), // "search_text", memberAccessor<&Message::searchText>(), // "message_text", memberAccessor<&Message::messageText>(), // "login_name", memberAccessor<&Message::loginName>(), // "display_name", memberAccessor<&Message::displayName>(), // "localized_name", memberAccessor<&Message::localizedName>(), // "user_id", memberAccessor<&Message::userID>(), // "channel_name", memberAccessor<&Message::channelName>(), // "username_color", sol::property( [](Message *msg) { return msg->usernameColor.name(QColor::HexArgb); }, [](Message *msg, std::string_view sv) { checkWritable(msg); msg->usernameColor = QColor::fromString(sv); }), "server_received_time", sol::property( [](Message *msg) { return msg->serverReceivedTime.toMSecsSinceEpoch(); }, [](Message *msg, qint64 ms) { checkWritable(msg); msg->serverReceivedTime = datetimeFromOffset(ms); }), "highlight_color", sol::property( [](Message *msg) { if (!msg->highlightColor) { return QString{}; } return msg->highlightColor->name(QColor::HexArgb); }, [](Message *msg, std::string_view sv) { checkWritable(msg); if (sv.empty()) { msg->highlightColor.reset(); } else { msg->highlightColor = std::make_shared(QColor::fromString(sv)); } }), // must be read only (but it might be helpful for generic Lua functions) "frozen", sol::property([](Message *msg) { return msg->frozen; }), "elements", [](const std::shared_ptr &msg) { return MessageElements(msg); }, "append_element", [](Message *msg, const sol::table &tbl) { checkWritable(msg); auto el = elementFromTable(tbl); if (el) { msg->elements.emplace_back(std::move(el)); } }); } } // namespace chatterino::lua::api::message #endif