diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bf3e9c8..90c06798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Minor: Consolidate font picking into one dialog. (#6531) - Minor: Added a menu action to sort tabs alphabetically. (#6551) - Minor: Fixed "edit hotkey" dialog opening like a normal window. (#6540) +- Minor: Added an API to get the current Twitch account in plugins. (#6554) - Minor: Added a setting to show the stream title in live messages. (#6572) - Minor: Added broadcaster-only `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605) - Minor: Added broadcaster-only `/prediction` command to start a prediction. (#6583) diff --git a/docs/chatterino.d.ts b/docs/chatterino.d.ts index 860f058e..9385d55f 100644 --- a/docs/chatterino.d.ts +++ b/docs/chatterino.d.ts @@ -376,6 +376,16 @@ declare namespace c2 { Original, Repost, } + + class TwitchAccount implements IWeakResource { + is_valid(): boolean; + user_login(): string; + user_id(): string; + color(): string; + is_anon(): boolean; + } + + function current_account(): TwitchAccount; } declare module "chatterino.json" { diff --git a/docs/lua-meta/globals.lua b/docs/lua-meta/globals.lua index a3047e96..33445c64 100644 --- a/docs/lua-meta/globals.lua +++ b/docs/lua-meta/globals.lua @@ -54,6 +54,38 @@ c2.ChannelType = { -- End src/common/Channel.hpp +-- Begin src/controllers/plugins/api/Accounts.hpp + + +---@class c2.TwitchAccount +c2.TwitchAccount = {} + +--- Returns true if the account this object points to is valid. +--- If the object expired, returns false +--- +---@return boolean success +function c2.TwitchAccount:is_valid() end + +---@return string user_login The (login) name of the account +function c2.TwitchAccount:login() end + +---@return string user_id The Twitch user ID of the account +function c2.TwitchAccount:id() end + +---@return string? color Color in chat of this account. `nil` if not yet known +function c2.TwitchAccount:color() end + +---@return boolean is_anon `true` if this account is an anonymous account (no associated Twitch user) +function c2.TwitchAccount:is_anon() end + +---@return string str +function c2.TwitchAccount:__tostring() end + +---Gets the currently logged in Twitch account. This account might be an anonymous one (see `is_anon`). +---@return c2.TwitchAccount account +function c2.current_account() end +-- End src/controllers/plugins/api/Accounts.hpp + -- Begin src/controllers/plugins/api/ChannelRef.hpp -- Begin src/providers/twitch/TwitchChannel.hpp diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md index ebc0cf74..17b8091b 100644 --- a/docs/wip-plugins.md +++ b/docs/wip-plugins.md @@ -219,6 +219,51 @@ c2.register_callback( ) ``` +#### `current_account()` + +Returns a `TwitchAccount` representing the current account. + +#### `TwitchAccount` + +Represents a Twitch account. There can only be one selected at a time but +unselected accounts stay valid. An account can become invalid if removed by the +user in the settings. Using an invalid account produces an error. + +```lua +local acc = c2.current_account() + +print(acc:is_valid()) -- true unless user removed account +``` + +##### `TwitchAccount:login()` + +Returns the login name of the account. This string may only contain lowercase ASCII. + +```lua +print(acc:login()) -- "mm2pl" +``` + +##### `TwitchAccount:id()` + +Returns the Twitch user ID of the account. This uniquely and persistently identifies the account. + +```lua +print(acc:id()) -- "117691339" +``` + +##### `TwitchAccount:color()` + +Returns the color in chat of this account. If the user has not sent any +messages this will be `nil`. + +```lua +print(acc:color()) -- "#ffdaa520" +``` + +##### `TwitchAccount:is_anon()` + +Returns `true` if this account is an anonymous account (no associated Twitch user). + #### `ChannelType` enum This table describes channel types Chatterino supports. The values behind the diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ce23735f..fbff2511 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -250,6 +250,8 @@ set(SOURCE_FILES controllers/pings/MutedChannelModel.hpp + controllers/plugins/api/Accounts.cpp + controllers/plugins/api/Accounts.hpp controllers/plugins/api/ChannelRef.cpp controllers/plugins/api/ChannelRef.hpp controllers/plugins/api/EventType.hpp diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index 3a003f34..e32c3517 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -82,6 +82,7 @@ sol::table toTable(lua_State *L, const CompletionEvent &ev); /** * @includefile common/Channel.hpp + * @includefile controllers/plugins/api/Accounts.hpp * @includefile controllers/plugins/api/ChannelRef.hpp * @includefile controllers/plugins/api/HTTPResponse.hpp * @includefile controllers/plugins/api/HTTPRequest.hpp diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 07ce82ca..d13d095e 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -7,6 +7,7 @@ # include "common/QLogging.hpp" # include "controllers/commands/CommandContext.hpp" # include "controllers/commands/CommandController.hpp" +# include "controllers/plugins/api/Accounts.hpp" # include "controllers/plugins/api/ChannelRef.hpp" # include "controllers/plugins/api/HTTPRequest.hpp" # include "controllers/plugins/api/HTTPResponse.hpp" @@ -232,6 +233,7 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin) lua::api::HTTPRequest::createUserType(c2); lua::api::WebSocket::createUserType(c2, plugin); lua::api::message::createUserType(c2); + lua::api::createAccounts(c2); c2["ChannelType"] = lua::createEnumTable(lua); c2["HTTPMethod"] = lua::createEnumTable(lua); c2["EventType"] = lua::createEnumTable(lua); diff --git a/src/controllers/plugins/api/Accounts.cpp b/src/controllers/plugins/api/Accounts.cpp new file mode 100644 index 00000000..44e8d7ae --- /dev/null +++ b/src/controllers/plugins/api/Accounts.cpp @@ -0,0 +1,97 @@ +#include "controllers/plugins/api/Accounts.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/plugins/SolTypes.hpp" // IWYU pragma: keep +#include "providers/twitch/TwitchAccount.hpp" +#include "util/WeakPtrHelpers.hpp" + +#include + +#include + +namespace { + +using namespace chatterino; + +class WeakTwitchAccount +{ +public: + WeakTwitchAccount(std::weak_ptr account) + : weak(std::move(account)) + { + } + + QString login() const + { + return this->shared()->getUserName(); + } + + QString id() const + { + return this->shared()->getUserId(); + } + + std::optional color() const + { + auto c = this->shared()->color(); + if (c.isValid()) + { + return c.name(QColor::HexArgb); + } + return std::nullopt; + } + + bool isAnon() const + { + return this->shared()->isAnon(); + } + + bool isValid() const + { + return !this->weak.expired(); + } + + bool operator==(const WeakTwitchAccount &other) const + { + return weakOwnerEquals(this->weak, other.weak); + } + +private: + std::shared_ptr shared() const + { + auto shared = this->weak.lock(); + if (!shared) + { + throw std::runtime_error("Expired account"); + } + return shared; + } + + std::weak_ptr weak; +}; + +} // namespace + +namespace chatterino::lua::api { + +void createAccounts(sol::table &c2) +{ + c2.new_usertype( + // clang-format off + "TwitchAccount", sol::no_constructor, + sol::meta_function::to_string, &WeakTwitchAccount::login, + "is_valid", &WeakTwitchAccount::isValid, + "login", &WeakTwitchAccount::login, + "id", &WeakTwitchAccount::id, + "color", &WeakTwitchAccount::color, + "is_anon", &WeakTwitchAccount::isAnon + // clang-format on + ); + + c2.set_function("current_account", [] { + return WeakTwitchAccount(getApp()->getAccounts()->twitch.getCurrent()); + }); +} + +} // namespace chatterino::lua::api diff --git a/src/controllers/plugins/api/Accounts.hpp b/src/controllers/plugins/api/Accounts.hpp new file mode 100644 index 00000000..b9ee3e1c --- /dev/null +++ b/src/controllers/plugins/api/Accounts.hpp @@ -0,0 +1,42 @@ +#pragma once + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include + +namespace chatterino::lua::api { + +/* @lua-fragment +---@class c2.TwitchAccount +c2.TwitchAccount = {} + +--- Returns true if the account this object points to is valid. +--- If the object expired, returns false +--- +---@return boolean success +function c2.TwitchAccount:is_valid() end + +---@return string user_login The (login) name of the account +function c2.TwitchAccount:login() end + +---@return string user_id The Twitch user ID of the account +function c2.TwitchAccount:id() end + +---@return string? color Color in chat of this account. `nil` if not yet known +function c2.TwitchAccount:color() end + +---@return boolean is_anon `true` if this account is an anonymous account (no associated Twitch user) +function c2.TwitchAccount:is_anon() end + +---@return string str +function c2.TwitchAccount:__tostring() end + +---Gets the currently logged in Twitch account. This account might be an anonymous one (see `is_anon`). +---@return c2.TwitchAccount account +function c2.current_account() end +*/ +void createAccounts(sol::table &c2); + +} // namespace chatterino::lua::api + +#endif diff --git a/tests/src/Plugins.cpp b/tests/src/Plugins.cpp index 5a84faf0..0e0fe9f8 100644 --- a/tests/src/Plugins.cpp +++ b/tests/src/Plugins.cpp @@ -1,7 +1,9 @@ +#include "mocks/Helix.hpp" #ifdef CHATTERINO_HAVE_PLUGINS # include "Application.hpp" # include "common/Channel.hpp" # include "common/network/NetworkCommon.hpp" +# include "controllers/accounts/AccountController.hpp" # include "controllers/commands/Command.hpp" // IWYU pragma: keep # include "controllers/commands/CommandController.hpp" # include "controllers/plugins/api/ChannelRef.hpp" @@ -43,6 +45,15 @@ const QString TEST_SETTINGS = R"( "enabledPlugins": [ "test" ] + }, + "accounts": { + "uid117166826": { + "username": "testaccount_420", + "userID": "117166826", + "clientID": "abc", + "oauthToken": "def" + }, + "current": "testaccount_420" } } )"; @@ -107,11 +118,18 @@ public: return &this->logging; } + AccountController *getAccounts() override + { + return &this->accounts; + } + PluginController plugins; mock::Logging logging; CommandController commands; mock::EmoteController emotes; MockTwitch twitch; + AccountController accounts; + mock::Helix helix; }; QDir luaTestBaseDir(const QString &category) @@ -200,6 +218,8 @@ protected: this->channel = app->twitch.mm2pl; this->rawpl->dataDirectory().mkpath("."); + initializeHelix(&this->app->helix); + this->app->accounts.load(); } void TearDown() override @@ -1124,4 +1144,18 @@ TEST(PluginMessageConstructionTest, Integrity) ASSERT_FALSE(UPDATE_SNAPSHOTS); // make sure fixtures are actually tested } +TEST_F(PluginTest, testAccounts) +{ + configure(); + + auto res = lua->script(R"lua( + local current = c2.current_account() + assert(current:login() == "testaccount_420") + assert(current:id() == "117166826") + assert(current:color() == nil) -- unset + assert(not current:is_anon()) + )lua"); + ASSERT_TRUE(res.valid()) << res.get().what(); +} + #endif