Clean up token selection

* Adjust locale
* Hide token and name fields if no account is connected
* Enable all token options by default
* Validate token every hour as required by Twitch
* Retrigger event subscriptions on token change

token
This commit is contained in:
WarmUpTill
2023-10-12 20:55:41 +02:00
committed by WarmUpTill
parent 53fcd94c5a
commit ce40b80d90
4 changed files with 173 additions and 27 deletions

View File

@@ -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."

View File

@@ -10,7 +10,7 @@ namespace advss {
static std::deque<std::shared_ptr<Item>> twitchTokens;
const std::unordered_map<std::string, std::string> TokenOption::apiIdToLocale{
const std::unordered_map<std::string, std::string> 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<std::string, std::string> &
TokenOption::GetTokenOptionMap()
{
return apiIdToLocale;
return _apiIdToLocale;
}
std::set<TokenOption> TokenOption::GetAllTokenOptions()
{
std::set<TokenOption> 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<std::string> TwitchToken::GetToken() const
{
if (!IsValid()) {
return {};
}
return _token;
}
std::shared_ptr<EventSub> TwitchToken::GetEventSub()
@@ -156,6 +179,43 @@ std::shared_ptr<EventSub> 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<QString> &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);
}

View File

@@ -6,6 +6,8 @@
#include <set>
#include <QCheckBox>
#include <QThread>
#include <QLayout>
#include <QTimer>
#include <optional>
namespace advss {
@@ -21,11 +23,12 @@ public:
static const std::unordered_map<std::string, std::string> &
GetTokenOptionMap();
static std::set<TokenOption> GetAllTokenOptions();
bool operator<(const TokenOption &other) const;
std::string apiId = "";
private:
const static std::unordered_map<std::string, std::string> apiIdToLocale;
const static std::unordered_map<std::string, std::string> _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<std::string> GetToken() const;
std::string GetUserID() const { return _userID; }
std::shared_ptr<EventSub> GetEventSub();
bool IsValid(bool forceUpdate = false) const;
private:
std::string _token;
std::string _userID;
std::set<TokenOption> _tokenOptions = {{"channel:manage:broadcast"}};
std::set<TokenOption> _tokenOptions = TokenOption::GetAllTokenOptions();
std::shared_ptr<EventSub> _eventSub;
static bool _setup;
@@ -97,17 +101,23 @@ private slots:
void TokenOptionChanged(int);
void RequestToken();
void GotToken(const std::optional<QString> &);
void CheckIfTokenValid();
private:
std::set<TokenOption> 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<std::string, QCheckBox *> _optionWidgets;
QTimer _validationTimer;
};
class TwitchConnectionSelection : public ItemSelection {

View File

@@ -57,10 +57,10 @@ static bool cacheIsValid(const std::map<Args, CacheEntry> &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 &params)
{
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<Args, CacheEntry> cache;
static std::mutex mtx;
std::lock_guard<std::mutex> 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<Args, CacheEntry> cache;
static std::mutex mtx;
std::lock_guard<std::mutex> 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<Args, CacheEntry> cache;
static std::mutex mtx;
std::lock_guard<std::mutex> 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)) {