diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index e7424982..d2b48e89 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -945,14 +945,14 @@ AdvSceneSwitcher.twitchToken.add="Add new connection" AdvSceneSwitcher.twitchToken.configure="Configure Twitch connection settings" AdvSceneSwitcher.twitchToken.value="Token:" AdvSceneSwitcher.twitchToken.invalid="Invalid twitch token" -AdvSceneSwitcher.twitchToken.request="Request token" +AdvSceneSwitcher.twitchToken.request="Connect Account" AdvSceneSwitcher.twitchToken.request.waiting="Waiting for token approval ..." AdvSceneSwitcher.twitchToken.request.fail="Failed to get token!" AdvSceneSwitcher.twitchToken.request.fail.browser="Authentication failed! (%1)\nYou can close this window now." AdvSceneSwitcher.twitchToken.request.fail.stateMismatch="State mismatch" AdvSceneSwitcher.twitchToken.request.success="Successfully received token!" AdvSceneSwitcher.twitchToken.request.success.browser="Authentication successful! You can close this window now." -AdvSceneSwitcher.twitchToken.request.notSet="No token set - Please request new token!" +AdvSceneSwitcher.twitchToken.request.notSet="Account is not connected!" AdvSceneSwitcher.twitchToken.permissions="Token permissions:" AdvSceneSwitcher.twitchToken.analytics.readExtensions="View analytics data for the Twitch Extensions owned by the authenticated account." AdvSceneSwitcher.twitchToken.analytics.readGames="View analytics data for the games owned by the authenticated account." diff --git a/src/macro-external/twitch/token.cpp b/src/macro-external/twitch/token.cpp index 05f824bd..cce071c0 100644 --- a/src/macro-external/twitch/token.cpp +++ b/src/macro-external/twitch/token.cpp @@ -10,7 +10,7 @@ namespace advss { static std::deque> twitchTokens; -const std::unordered_map TokenOption::apiIdToLocale{ +const std::unordered_map TokenOption::_apiIdToLocale{ {"channel:manage:broadcast", "AdvSceneSwitcher.twitchToken.channel.manageBroadcast"}, {"clips:edit", "AdvSceneSwitcher.twitchToken.clips.edit"}, @@ -74,13 +74,23 @@ void TokenOption::Save(obs_data_t *obj) const std::string TokenOption::GetLocale() const { - return apiIdToLocale.at(apiId); + return _apiIdToLocale.at(apiId); } const std::unordered_map & TokenOption::GetTokenOptionMap() { - return apiIdToLocale; + return _apiIdToLocale; +} + +std::set TokenOption::GetAllTokenOptions() +{ + std::set result; + for (const auto &[optionStr, _] : _apiIdToLocale) { + TokenOption option = {optionStr}; + result.emplace(option); + } + return result; } bool TokenOption::operator<(const TokenOption &other) const @@ -146,6 +156,19 @@ void TwitchToken::SetToken(const std::string &value) _userID = obs_data_get_string(arrayObj, "id"); _name = obs_data_get_string(arrayObj, "display_name"); } + + // Trigger resubscribes with new token + if (_eventSub) { + _eventSub->ClearActiveSubscriptions(); + } +} + +std::optional TwitchToken::GetToken() const +{ + if (!IsValid()) { + return {}; + } + return _token; } std::shared_ptr TwitchToken::GetEventSub() @@ -156,6 +179,43 @@ std::shared_ptr TwitchToken::GetEventSub() return _eventSub; } +bool TwitchToken::IsValid(bool forceUpdate) const +{ + static std::chrono::system_clock::time_point queryTime; + static std::string lastQueryToken; + static httplib::Result response; + static httplib::Client cli("https://id.twitch.tv"); + httplib::Headers headers{{"Authorization", "OAuth " + _token}}; + + auto currentTime = std::chrono::system_clock::now(); + auto diff = currentTime - queryTime; + const bool cacheIsTooOld = diff >= std::chrono::hours(1); + + const bool tokenChanged = lastQueryToken != _token; + if (tokenChanged) { + response = + cli.Get("/oauth2/validate", httplib::Params{}, headers); + queryTime = std::chrono::system_clock::now(); + lastQueryToken = _token; + return response && response->status == 200; + } + + // No point in checking again as token will not become valid again + if (!forceUpdate && response && response->status != 200) { + blog(LOG_INFO, "Twitch token %s is not valid!", _name.c_str()); + return false; + } + + if (!forceUpdate && !cacheIsTooOld && response) { + return response->status == 200; + } + + response = cli.Get("/oauth2/validate", httplib::Params{}, headers); + queryTime = std::chrono::system_clock::now(); + lastQueryToken = _token; + return response && response->status == 200; +} + TwitchToken *GetTwitchTokenByName(const QString &name) { return GetTwitchTokenByName(name.toStdString()); @@ -287,7 +347,8 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog( obs_module_text("AdvSceneSwitcher.twitchToken.request"))), _showToken(new QPushButton()), _currentTokenValue(new QLineEdit()), - _tokenStatus(new QLabel()) + _tokenStatus(new QLabel()), + _generalSettingsGrid(new QGridLayout()) { _showToken->setMaximumWidth(22); _showToken->setFlat(true); @@ -298,6 +359,7 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog( _currentTokenValue->setText(QString::fromStdString(settings._token)); _name->setReadOnly(true); + _showNameEmptyWarning = false; QWidget::connect(_requestToken, SIGNAL(clicked()), this, SLOT(RequestToken())); @@ -308,28 +370,29 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog( QWidget::connect(&_tokenGrabber, &TokenGrabberThread::GotToken, this, &TwitchTokenSettingsDialog::GotToken); - auto generalSettingsGrid = new QGridLayout(); int row = 0; - generalSettingsGrid->addWidget( + _generalSettingsGrid->addWidget( new QLabel( obs_module_text("AdvSceneSwitcher.twitchToken.name")), row, 0); - auto nameLayout = new QHBoxLayout; + auto nameLayout = new QHBoxLayout(); nameLayout->addWidget(_name); nameLayout->addWidget(_nameHint); - generalSettingsGrid->addLayout(nameLayout, row, 1); + _generalSettingsGrid->addLayout(nameLayout, row, 1); + _nameRow = row; ++row; - generalSettingsGrid->addWidget( + _generalSettingsGrid->addWidget( new QLabel( obs_module_text("AdvSceneSwitcher.twitchToken.value")), row, 0); - auto tokenValueLayout = new QHBoxLayout; + auto tokenValueLayout = new QHBoxLayout(); tokenValueLayout->addWidget(_currentTokenValue); tokenValueLayout->addWidget(_showToken); - generalSettingsGrid->addLayout(tokenValueLayout, row, 1); + _generalSettingsGrid->addLayout(tokenValueLayout, row, 1); + _tokenValueRow = row; ++row; - generalSettingsGrid->addWidget(_requestToken, row, 0); - generalSettingsGrid->addWidget(_tokenStatus, row, 1); + _generalSettingsGrid->addWidget(_requestToken, row, 0); + _generalSettingsGrid->addWidget(_tokenStatus, row, 1); auto optionsGrid = new QGridLayout(); row = 0; @@ -350,7 +413,7 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog( auto contentWidget = new QWidget(scrollArea); auto layout = new QVBoxLayout(contentWidget); - layout->addLayout(generalSettingsGrid); + layout->addLayout(_generalSettingsGrid); layout->addWidget(optionsBox); layout->setContentsMargins(0, 0, 0, 0); scrollArea->setWidget(contentWidget); @@ -364,6 +427,7 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog( if (settings._token.empty()) { _tokenStatus->setText(obs_module_text( "AdvSceneSwitcher.twitchToken.request.notSet")); + SetTokenInfoVisible(false); } HideToken(); @@ -372,6 +436,29 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog( } _currentToken = settings; + + QWidget::connect(&_validationTimer, &QTimer::timeout, this, + &TwitchTokenSettingsDialog::CheckIfTokenValid); + _validationTimer.start(10000); + CheckIfTokenValid(); +} + +void TwitchTokenSettingsDialog::SetTokenInfoVisible(bool visible) +{ + SetGridLayoutRowVisible(_generalSettingsGrid, _nameRow, visible); + SetGridLayoutRowVisible(_generalSettingsGrid, _tokenValueRow, visible); +} + +void TwitchTokenSettingsDialog::CheckIfTokenValid() +{ + if (_currentToken._token.empty()) { + return; + } + if (_currentToken.IsValid(true)) { + return; + } + _tokenStatus->setText( + obs_module_text("AdvSceneSwitcher.twitchToken.request.notSet")); } void TwitchTokenSettingsDialog::ShowToken() @@ -392,11 +479,32 @@ void TwitchTokenSettingsDialog::TokenOptionChanged(int) PulseWidget(_requestToken, Qt::green, QColor(0, 0, 0, 0), true); } _name->setText(""); + SetTokenInfoVisible(false); QMetaObject::invokeMethod(this, "NameChanged", Q_ARG(const QString &, "")); + _tokenStatus->setText( + obs_module_text("AdvSceneSwitcher.twitchToken.request.notSet")); _currentTokenValue->setText(""); } +static void revokeToken(const std::string &token) +{ + httplib::Client cli("https://id.twitch.tv"); + auto response = cli.Post("/oauth2/revoke", + std::string("client_id=") + GetClientID() + + "&token=" + token, + "application/x-www-form-urlencoded"); + if (!response) { + auto err = response.error(); + blog(LOG_INFO, "Failed to revoke token: %s", + httplib::to_string(err).c_str()); + return; + } + if (response->status != 200) { + blog(LOG_INFO, "Failed to revoke token: %d", response->status); + } +} + static std::string generateStateString() { const char *chars = @@ -481,10 +589,14 @@ void TwitchTokenSettingsDialog::GotToken(const std::optional &value) auto name = QString::fromStdString(_currentToken._name); _name->setText(name); _name->textEdited(name); + QMetaObject::invokeMethod(this, "NameChanged", + Q_ARG(const QString &, name)); + SetTokenInfoVisible(true); } else { _tokenStatus->setText(obs_module_text( "AdvSceneSwitcher.twitchToken.request.fail")); _name->setText(""); + SetTokenInfoVisible(false); } _requestToken->setEnabled(true); } diff --git a/src/macro-external/twitch/token.hpp b/src/macro-external/twitch/token.hpp index e19dc4d1..ec4e0d26 100644 --- a/src/macro-external/twitch/token.hpp +++ b/src/macro-external/twitch/token.hpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include namespace advss { @@ -21,11 +23,12 @@ public: static const std::unordered_map & GetTokenOptionMap(); + static std::set GetAllTokenOptions(); bool operator<(const TokenOption &other) const; std::string apiId = ""; private: - const static std::unordered_map apiIdToLocale; + const static std::unordered_map _apiIdToLocale; }; class TwitchToken : public Item { @@ -41,14 +44,15 @@ public: bool OptionIsEnabled(const TokenOption &) const; void SetToken(const std::string &); bool IsEmpty() const { return _token.empty(); } - std::string GetToken() const { return _token; } + std::optional GetToken() const; std::string GetUserID() const { return _userID; } std::shared_ptr GetEventSub(); + bool IsValid(bool forceUpdate = false) const; private: std::string _token; std::string _userID; - std::set _tokenOptions = {{"channel:manage:broadcast"}}; + std::set _tokenOptions = TokenOption::GetAllTokenOptions(); std::shared_ptr _eventSub; static bool _setup; @@ -97,17 +101,23 @@ private slots: void TokenOptionChanged(int); void RequestToken(); void GotToken(const std::optional &); + void CheckIfTokenValid(); private: std::set GetEnabledOptions(); + void SetTokenInfoVisible(bool); QPushButton *_requestToken; QPushButton *_showToken; QLineEdit *_currentTokenValue; QLabel *_tokenStatus; + QGridLayout *_generalSettingsGrid; + int _nameRow = -1; + int _tokenValueRow = -1; TokenGrabberThread _tokenGrabber; TwitchToken _currentToken; std::unordered_map _optionWidgets; + QTimer _validationTimer; }; class TwitchConnectionSelection : public ItemSelection { diff --git a/src/macro-external/twitch/twitch-helpers.cpp b/src/macro-external/twitch/twitch-helpers.cpp index 1a5391cb..d50742c6 100644 --- a/src/macro-external/twitch/twitch-helpers.cpp +++ b/src/macro-external/twitch/twitch-helpers.cpp @@ -57,10 +57,10 @@ static bool cacheIsValid(const std::map &cache, return it != cache.end() && !chacheIsTooOld(it->second); } -static httplib::Headers getTokenRequestHeaders(const TwitchToken &token) +static httplib::Headers getTokenRequestHeaders(const std::string &token) { return { - {"Authorization", "Bearer " + token.GetToken()}, + {"Authorization", "Bearer " + token}, {"Client-Id", clientID.data()}, }; } @@ -70,7 +70,11 @@ RequestResult SendGetRequest(const std::string &uri, const std::string &path, const httplib::Params ¶ms) { httplib::Client cli(uri); - auto headers = getTokenRequestHeaders(token); + auto tokenStr = token.GetToken(); + if (!tokenStr) { + return {}; + } + auto headers = getTokenRequestHeaders(*tokenStr); auto response = cli.Get(path, params, headers); if (!response) { auto err = response.error(); @@ -96,7 +100,11 @@ RequestResult SendGetRequest(const std::string &uri, const std::string &path, static std::map cache; static std::mutex mtx; std::lock_guard lock(mtx); - auto headers = getTokenRequestHeaders(token); + auto tokenStr = token.GetToken(); + if (!tokenStr) { + return {}; + } + auto headers = getTokenRequestHeaders(*tokenStr); Args args(uri, path, "", params, headers); if (useCache && cacheIsValid(cache, args)) { auto it = cache.find(args); @@ -112,7 +120,11 @@ RequestResult SendPostRequest(const std::string &uri, const std::string &path, const TwitchToken &token, const OBSData &data) { httplib::Client cli(uri); - auto headers = getTokenRequestHeaders(token); + auto tokenStr = token.GetToken(); + if (!tokenStr) { + return {}; + } + auto headers = getTokenRequestHeaders(*tokenStr); auto json = obs_data_get_json(data); std::string body = json ? json : ""; auto response = cli.Post(path, headers, body, "application/json"); @@ -140,7 +152,11 @@ RequestResult SendPostRequest(const std::string &uri, const std::string &path, static std::map cache; static std::mutex mtx; std::lock_guard lock(mtx); - auto headers = getTokenRequestHeaders(token); + auto tokenStr = token.GetToken(); + if (!tokenStr) { + return {}; + } + auto headers = getTokenRequestHeaders(*tokenStr); auto jsonCstr = obs_data_get_json(data); Args args(uri, path, jsonCstr ? jsonCstr : "", {}, headers); if (useCache && cacheIsValid(cache, args)) { @@ -157,7 +173,11 @@ RequestResult SendPatchRequest(const std::string &uri, const std::string &path, const TwitchToken &token, const OBSData &data) { httplib::Client cli(uri); - auto headers = getTokenRequestHeaders(token); + auto tokenStr = token.GetToken(); + if (!tokenStr) { + return {}; + } + auto headers = getTokenRequestHeaders(*tokenStr); auto json = obs_data_get_json(data); std::string body = json ? json : ""; auto response = cli.Patch(path, headers, body, "application/json"); @@ -185,7 +205,11 @@ RequestResult SendPatchRequest(const std::string &uri, const std::string &path, static std::map cache; static std::mutex mtx; std::lock_guard lock(mtx); - auto headers = getTokenRequestHeaders(token); + auto tokenStr = token.GetToken(); + if (!tokenStr) { + return {}; + } + auto headers = getTokenRequestHeaders(*tokenStr); auto jsonCstr = obs_data_get_json(data); Args args(uri, path, jsonCstr ? jsonCstr : "", {}, headers); if (useCache && cacheIsValid(cache, args)) {