mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-13 04:37:14 -05:00
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:
@@ -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."
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ¶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<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)) {
|
||||
|
||||
Reference in New Issue
Block a user