mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Huge changes, it works properly now
This commit is contained in:
parent
e06618be1e
commit
f50b50736a
8 changed files with 384 additions and 379 deletions
|
@ -288,6 +288,7 @@ SOURCES += \
|
||||||
src/widgets/helper/DebugPopup.cpp \
|
src/widgets/helper/DebugPopup.cpp \
|
||||||
src/widgets/helper/EditableModelView.cpp \
|
src/widgets/helper/EditableModelView.cpp \
|
||||||
src/widgets/helper/EffectLabel.cpp \
|
src/widgets/helper/EffectLabel.cpp \
|
||||||
|
src/widgets/helper/LoginServer.cpp \
|
||||||
src/widgets/helper/NotebookButton.cpp \
|
src/widgets/helper/NotebookButton.cpp \
|
||||||
src/widgets/helper/NotebookTab.cpp \
|
src/widgets/helper/NotebookTab.cpp \
|
||||||
src/widgets/helper/QColorPicker.cpp \
|
src/widgets/helper/QColorPicker.cpp \
|
||||||
|
@ -551,6 +552,7 @@ HEADERS += \
|
||||||
src/widgets/helper/EditableModelView.hpp \
|
src/widgets/helper/EditableModelView.hpp \
|
||||||
src/widgets/helper/EffectLabel.hpp \
|
src/widgets/helper/EffectLabel.hpp \
|
||||||
src/widgets/helper/Line.hpp \
|
src/widgets/helper/Line.hpp \
|
||||||
|
src/widgets/helper/LoginServer.hpp \
|
||||||
src/widgets/helper/NotebookButton.hpp \
|
src/widgets/helper/NotebookButton.hpp \
|
||||||
src/widgets/helper/NotebookTab.hpp \
|
src/widgets/helper/NotebookTab.hpp \
|
||||||
src/widgets/helper/QColorPicker.hpp \
|
src/widgets/helper/QColorPicker.hpp \
|
||||||
|
|
|
@ -378,6 +378,8 @@ set(SOURCE_FILES
|
||||||
widgets/helper/EditableModelView.hpp
|
widgets/helper/EditableModelView.hpp
|
||||||
widgets/helper/EffectLabel.cpp
|
widgets/helper/EffectLabel.cpp
|
||||||
widgets/helper/EffectLabel.hpp
|
widgets/helper/EffectLabel.hpp
|
||||||
|
widgets/helper/LoginServer.cpp
|
||||||
|
widgets/helper/LoginServer.hpp
|
||||||
widgets/helper/NotebookButton.cpp
|
widgets/helper/NotebookButton.cpp
|
||||||
widgets/helper/NotebookButton.hpp
|
widgets/helper/NotebookButton.hpp
|
||||||
widgets/helper/NotebookTab.cpp
|
widgets/helper/NotebookTab.cpp
|
||||||
|
|
|
@ -15,9 +15,16 @@ namespace chatterino {
|
||||||
|
|
||||||
static const char *ANONYMOUS_USERNAME ATTR_UNUSED = "justinfan64537";
|
static const char *ANONYMOUS_USERNAME ATTR_UNUSED = "justinfan64537";
|
||||||
|
|
||||||
inline QByteArray getDefaultClientID()
|
inline QString getDefaultClientID()
|
||||||
{
|
{
|
||||||
return QByteArray("7ue61iz46fz11y3cugd0l3tawb4taal");
|
// return "g5zg0400k4vhrx2g6xi4hgveruamlv"; // official one, used on chatterino.com/client_login
|
||||||
|
return "t9xehoda0otdcfkzs24afh5h14wss6"; // zneix's testing instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect URI used
|
||||||
|
inline QString getRedirectURI()
|
||||||
|
{
|
||||||
|
return "http://localhost:52107/redirect";
|
||||||
}
|
}
|
||||||
|
|
||||||
static const std::vector<QColor> TWITCH_USERNAME_COLORS = {
|
static const std::vector<QColor> TWITCH_USERNAME_COLORS = {
|
||||||
|
@ -38,4 +45,28 @@ static const std::vector<QColor> TWITCH_USERNAME_COLORS = {
|
||||||
{0, 255, 127}, // SpringGreen
|
{0, 255, 127}, // SpringGreen
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const QStringList loginScopes{
|
||||||
|
// clang-format off
|
||||||
|
"user_subscriptions", //
|
||||||
|
"user_blocks_edit", // [DEPRECATED] replaced with "user:manage:blocked_users"
|
||||||
|
"user_blocks_read", // [DEPRECATED] replaced with "user:read:blocked_users"
|
||||||
|
"user_follows_edit", // [DEPRECATED] soon to be removed later since we now use "user:edit:follows"
|
||||||
|
"channel_editor", // [DEPRECATED] for /raid
|
||||||
|
"channel:moderate", //
|
||||||
|
"channel:read:redemptions", //
|
||||||
|
"chat:edit", //
|
||||||
|
"chat:read", //
|
||||||
|
"whispers:read", //
|
||||||
|
"whispers:edit", //
|
||||||
|
"channel_commercial", // [DEPRECATED] for /commercial
|
||||||
|
"channel:edit:commercial", // in case twitch upgrades things in the future (and this scope is required)
|
||||||
|
"user:edit:follows", // for (un)following
|
||||||
|
"clips:edit", // for clip creation
|
||||||
|
"channel:manage:broadcast", // for creating stream markers with /marker command, and for the /settitle and /setgame commands
|
||||||
|
"user:read:blocked_users", // [DEPRECATED] for getting list of blocked users
|
||||||
|
"user:manage:blocked_users", // [DEPRECATED] for blocking/unblocking other users
|
||||||
|
"moderator:manage:automod", // for approving/denying automod messages
|
||||||
|
// clang-format on
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -5,8 +5,10 @@
|
||||||
#include "common/NetworkRequest.hpp"
|
#include "common/NetworkRequest.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
|
#include "providers/twitch/TwitchCommon.hpp"
|
||||||
#include "util/Clipboard.hpp"
|
#include "util/Clipboard.hpp"
|
||||||
#include "util/Helpers.hpp"
|
#include "util/Helpers.hpp"
|
||||||
|
#include "widgets/helper/LoginServer.hpp"
|
||||||
|
|
||||||
#ifdef USEWINSDK
|
#ifdef USEWINSDK
|
||||||
# include <Windows.h>
|
# include <Windows.h>
|
||||||
|
@ -15,307 +17,23 @@
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QFile>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QtHttpServer/QHttpServer>
|
|
||||||
#include <QtHttpServer/QHttpServerResponder>
|
|
||||||
#include <pajlada/settings/setting.hpp>
|
#include <pajlada/settings/setting.hpp>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
namespace {
|
LoginDialog::LoginDialog(QWidget *parent)
|
||||||
|
|
||||||
void logInWithCredentials(const QString &userID, const QString &username,
|
|
||||||
const QString &clientID,
|
|
||||||
const QString &oauthToken)
|
|
||||||
{
|
|
||||||
QStringList errors;
|
|
||||||
|
|
||||||
if (userID.isEmpty())
|
|
||||||
{
|
|
||||||
errors.append("Missing user ID");
|
|
||||||
}
|
|
||||||
if (username.isEmpty())
|
|
||||||
{
|
|
||||||
errors.append("Missing username");
|
|
||||||
}
|
|
||||||
if (clientID.isEmpty())
|
|
||||||
{
|
|
||||||
errors.append("Missing Client ID");
|
|
||||||
}
|
|
||||||
if (oauthToken.isEmpty())
|
|
||||||
{
|
|
||||||
errors.append("Missing OAuth Token");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length() > 0)
|
|
||||||
{
|
|
||||||
QMessageBox messageBox;
|
|
||||||
// Set error window on top
|
|
||||||
#ifdef USEWINSDK
|
|
||||||
::SetWindowPos(HWND(messageBox.winId()), HWND_TOPMOST, 0, 0, 0, 0,
|
|
||||||
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
messageBox.setWindowTitle(
|
|
||||||
"Chatterino - invalid account credentials");
|
|
||||||
messageBox.setIcon(QMessageBox::Critical);
|
|
||||||
messageBox.setText(errors.join("<br>"));
|
|
||||||
messageBox.setStandardButtons(QMessageBox::Ok);
|
|
||||||
messageBox.exec();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string basePath = "/accounts/uid" + userID.toStdString();
|
|
||||||
pajlada::Settings::Setting<QString>::set(basePath + "/username",
|
|
||||||
username);
|
|
||||||
pajlada::Settings::Setting<QString>::set(basePath + "/userID", userID);
|
|
||||||
pajlada::Settings::Setting<QString>::set(basePath + "/clientID",
|
|
||||||
clientID);
|
|
||||||
pajlada::Settings::Setting<QString>::set(basePath + "/oauthToken",
|
|
||||||
oauthToken);
|
|
||||||
|
|
||||||
getApp()->accounts->twitch.reloadUsers();
|
|
||||||
getApp()->accounts->twitch.currentUsername = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
BasicLoginWidget::BasicLoginWidget()
|
|
||||||
{
|
|
||||||
// Initialize HTTP server and its routes
|
|
||||||
qCDebug(chatterinoWidget) << "Creating new HTTP server";
|
|
||||||
this->httpServer_ = new QHttpServer(this);
|
|
||||||
this->tcpServer_ = new QTcpServer(this->httpServer_);
|
|
||||||
this->httpServer_->bind(this->tcpServer_);
|
|
||||||
|
|
||||||
qCDebug(chatterinoWidget) << "Initializing HTTP server's routes";
|
|
||||||
this->httpServer_->route(
|
|
||||||
"/redirect", QHttpServerRequest::Method::GET,
|
|
||||||
[](const QHttpServerRequest &req, QHttpServerResponder &&resp) {
|
|
||||||
QFile redirectHTML(":/auth.html");
|
|
||||||
redirectHTML.open(QIODevice::ReadOnly);
|
|
||||||
|
|
||||||
resp.write(redirectHTML.readAll(),
|
|
||||||
{{"Access-Control-Allow-Origin", "*"},
|
|
||||||
{"Access-Control-Allow-Methods", "GET, PUT"},
|
|
||||||
{"Access-Control-Allow-Headers", "X-Access-Token"}},
|
|
||||||
QHttpServerResponder::StatusCode::Ok);
|
|
||||||
});
|
|
||||||
this->httpServer_->route(
|
|
||||||
".*", QHttpServerRequest::Method::OPTIONS,
|
|
||||||
[](const QHttpServerRequest &req, QHttpServerResponder &&resp) {
|
|
||||||
qDebug() << "options called!";
|
|
||||||
resp.write({{"Access-Control-Allow-Origin", "*"},
|
|
||||||
{"Access-Control-Allow-Methods", "GET, PUT"},
|
|
||||||
{"Access-Control-Allow-Headers", "X-Access-Token"}},
|
|
||||||
QHttpServerResponder::StatusCode::Ok);
|
|
||||||
});
|
|
||||||
this->httpServer_->route(
|
|
||||||
"/token", QHttpServerRequest::Method::PUT,
|
|
||||||
[](const QHttpServerRequest &req, QHttpServerResponder &&resp) {
|
|
||||||
if (!req.headers().contains("X-Access-Token"))
|
|
||||||
{
|
|
||||||
resp.write(QHttpServerResponder::StatusCode::BadRequest);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle token
|
|
||||||
const auto token = req.headers().value("X-Access-Token").toString();
|
|
||||||
qDebug() << token;
|
|
||||||
resp.write({{"Access-Control-Allow-Origin", "*"},
|
|
||||||
{"Access-Control-Allow-Methods", "GET, PUT"},
|
|
||||||
{"Access-Control-Allow-Headers", "X-Access-Token"}},
|
|
||||||
QHttpServerResponder::StatusCode::Ok);
|
|
||||||
});
|
|
||||||
|
|
||||||
const QString loginLink = "http://localhost:1234";
|
|
||||||
this->setLayout(&this->ui_.layout);
|
|
||||||
|
|
||||||
this->ui_.loginButton.setText("Log in (Opens in browser)");
|
|
||||||
this->ui_.pasteCodeButton.setText("Paste login info");
|
|
||||||
this->ui_.unableToOpenBrowserHelper.setWindowTitle(
|
|
||||||
"Chatterino - unable to open in browser");
|
|
||||||
this->ui_.unableToOpenBrowserHelper.setWordWrap(true);
|
|
||||||
this->ui_.unableToOpenBrowserHelper.hide();
|
|
||||||
this->ui_.unableToOpenBrowserHelper.setText(
|
|
||||||
QString("An error occurred while attempting to open <a href=\"%1\">the "
|
|
||||||
"log in link (%1)</a> - open it manually in your browser and "
|
|
||||||
"proceed from there.")
|
|
||||||
.arg(loginLink));
|
|
||||||
this->ui_.unableToOpenBrowserHelper.setOpenExternalLinks(true);
|
|
||||||
|
|
||||||
this->ui_.horizontalLayout.addWidget(&this->ui_.loginButton);
|
|
||||||
this->ui_.horizontalLayout.addWidget(&this->ui_.pasteCodeButton);
|
|
||||||
|
|
||||||
this->ui_.layout.addLayout(&this->ui_.horizontalLayout);
|
|
||||||
this->ui_.layout.addWidget(&this->ui_.unableToOpenBrowserHelper);
|
|
||||||
|
|
||||||
connect(&this->ui_.loginButton, &QPushButton::clicked, [this, loginLink]() {
|
|
||||||
// Start listening for credentials
|
|
||||||
if (!this->tcpServer_->listen(serverAddress, serverPort))
|
|
||||||
{
|
|
||||||
qCWarning(chatterinoWidget) << "Failed to start HTTP server";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
qInfo(chatterinoWidget) << QString("HTTP server Listening on %1:%2")
|
|
||||||
.arg(serverAddress.toString())
|
|
||||||
.arg(serverPort);
|
|
||||||
this->ui_.loginButton.setText("Listening...");
|
|
||||||
this->ui_.loginButton.setDisabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open login page
|
|
||||||
if (!QDesktopServices::openUrl(QUrl(loginLink)))
|
|
||||||
{
|
|
||||||
this->ui_.unableToOpenBrowserHelper.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&this->ui_.pasteCodeButton, &QPushButton::clicked, [this]() {
|
|
||||||
QStringList parameters = getClipboardText().split(";");
|
|
||||||
QString oauthToken, clientID, username, userID;
|
|
||||||
|
|
||||||
for (const auto ¶m : parameters)
|
|
||||||
{
|
|
||||||
QStringList kvParameters = param.split('=');
|
|
||||||
if (kvParameters.size() != 2)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
QString key = kvParameters[0];
|
|
||||||
QString value = kvParameters[1];
|
|
||||||
|
|
||||||
if (key == "oauth_token")
|
|
||||||
{
|
|
||||||
oauthToken = value;
|
|
||||||
}
|
|
||||||
else if (key == "client_id")
|
|
||||||
{
|
|
||||||
clientID = value;
|
|
||||||
}
|
|
||||||
else if (key == "username")
|
|
||||||
{
|
|
||||||
username = value;
|
|
||||||
}
|
|
||||||
else if (key == "user_id")
|
|
||||||
{
|
|
||||||
userID = value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
qCWarning(chatterinoWidget) << "Unknown key in code: " << key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logInWithCredentials(userID, username, clientID, oauthToken);
|
|
||||||
|
|
||||||
// Removing clipboard content to prevent accidental paste of credentials into somewhere
|
|
||||||
crossPlatformCopy("");
|
|
||||||
this->window()->close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void BasicLoginWidget::closeHttpServer()
|
|
||||||
{
|
|
||||||
// Revert login button
|
|
||||||
this->ui_.loginButton.setText("Log in (Opens in browser)");
|
|
||||||
this->ui_.loginButton.setEnabled(true);
|
|
||||||
|
|
||||||
qCDebug(chatterinoWidget) << "Closing TCP servers bind to HTTP server";
|
|
||||||
for (const auto &server : this->httpServer_->servers())
|
|
||||||
{
|
|
||||||
server->close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AdvancedLoginWidget::AdvancedLoginWidget()
|
|
||||||
{
|
|
||||||
this->setLayout(&this->ui_.layout);
|
|
||||||
|
|
||||||
this->ui_.instructionsLabel.setText("1. Fill in your username"
|
|
||||||
"\n2. Fill in your user ID"
|
|
||||||
"\n3. Fill in your client ID"
|
|
||||||
"\n4. Fill in your OAuth token"
|
|
||||||
"\n5. Press Add user");
|
|
||||||
this->ui_.instructionsLabel.setWordWrap(true);
|
|
||||||
|
|
||||||
this->ui_.layout.addWidget(&this->ui_.instructionsLabel);
|
|
||||||
this->ui_.layout.addLayout(&this->ui_.formLayout);
|
|
||||||
this->ui_.layout.addLayout(&this->ui_.buttonUpperRow.layout);
|
|
||||||
|
|
||||||
this->refreshButtons();
|
|
||||||
|
|
||||||
/// Form
|
|
||||||
this->ui_.formLayout.addRow("Username", &this->ui_.usernameInput);
|
|
||||||
this->ui_.formLayout.addRow("User ID", &this->ui_.userIDInput);
|
|
||||||
this->ui_.formLayout.addRow("Client ID", &this->ui_.clientIDInput);
|
|
||||||
this->ui_.formLayout.addRow("OAuth token", &this->ui_.oauthTokenInput);
|
|
||||||
|
|
||||||
this->ui_.oauthTokenInput.setEchoMode(QLineEdit::Password);
|
|
||||||
|
|
||||||
connect(&this->ui_.userIDInput, &QLineEdit::textChanged, [=]() {
|
|
||||||
this->refreshButtons();
|
|
||||||
});
|
|
||||||
connect(&this->ui_.usernameInput, &QLineEdit::textChanged, [=]() {
|
|
||||||
this->refreshButtons();
|
|
||||||
});
|
|
||||||
connect(&this->ui_.clientIDInput, &QLineEdit::textChanged, [=]() {
|
|
||||||
this->refreshButtons();
|
|
||||||
});
|
|
||||||
connect(&this->ui_.oauthTokenInput, &QLineEdit::textChanged, [=]() {
|
|
||||||
this->refreshButtons();
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Upper button row
|
|
||||||
|
|
||||||
this->ui_.buttonUpperRow.addUserButton.setText("Add user");
|
|
||||||
this->ui_.buttonUpperRow.clearFieldsButton.setText("Clear fields");
|
|
||||||
|
|
||||||
this->ui_.buttonUpperRow.layout.addWidget(
|
|
||||||
&this->ui_.buttonUpperRow.addUserButton);
|
|
||||||
this->ui_.buttonUpperRow.layout.addWidget(
|
|
||||||
&this->ui_.buttonUpperRow.clearFieldsButton);
|
|
||||||
|
|
||||||
connect(&this->ui_.buttonUpperRow.clearFieldsButton, &QPushButton::clicked,
|
|
||||||
[=]() {
|
|
||||||
this->ui_.userIDInput.clear();
|
|
||||||
this->ui_.usernameInput.clear();
|
|
||||||
this->ui_.clientIDInput.clear();
|
|
||||||
this->ui_.oauthTokenInput.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&this->ui_.buttonUpperRow.addUserButton, &QPushButton::clicked,
|
|
||||||
[=]() {
|
|
||||||
QString userID = this->ui_.userIDInput.text();
|
|
||||||
QString username = this->ui_.usernameInput.text();
|
|
||||||
QString clientID = this->ui_.clientIDInput.text();
|
|
||||||
QString oauthToken = this->ui_.oauthTokenInput.text();
|
|
||||||
|
|
||||||
logInWithCredentials(userID, username, clientID, oauthToken);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void AdvancedLoginWidget::refreshButtons()
|
|
||||||
{
|
|
||||||
if (this->ui_.userIDInput.text().isEmpty() ||
|
|
||||||
this->ui_.usernameInput.text().isEmpty() ||
|
|
||||||
this->ui_.clientIDInput.text().isEmpty() ||
|
|
||||||
this->ui_.oauthTokenInput.text().isEmpty())
|
|
||||||
{
|
|
||||||
this->ui_.buttonUpperRow.addUserButton.setEnabled(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this->ui_.buttonUpperRow.addUserButton.setEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LoginWidget::LoginWidget(QWidget *parent)
|
|
||||||
: QDialog(parent)
|
: QDialog(parent)
|
||||||
|
, loginServer_(new LoginServer(this))
|
||||||
|
// By default, QUrl constructor urlencodes our string so we don't have to do this ourselves
|
||||||
|
, loginLink(QString("https://id.twitch.tv/oauth2/authorize"
|
||||||
|
"?client_id=%1"
|
||||||
|
"&redirect_uri=%2"
|
||||||
|
"&response_type=%3"
|
||||||
|
"&scope=%4"
|
||||||
|
"&force_verify=%5")
|
||||||
|
.arg(getDefaultClientID(), getRedirectURI(), "token",
|
||||||
|
loginScopes.join(" "), QVariant(true).toString()))
|
||||||
{
|
{
|
||||||
#ifdef USEWINSDK
|
#ifdef USEWINSDK
|
||||||
::SetWindowPos(HWND(this->winId()), HWND_TOPMOST, 0, 0, 0, 0,
|
::SetWindowPos(HWND(this->winId()), HWND_TOPMOST, 0, 0, 0, 0,
|
||||||
|
@ -323,27 +41,155 @@ LoginWidget::LoginWidget(QWidget *parent)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
this->setWindowTitle("Chatterino - add new account");
|
this->setWindowTitle("Chatterino - add new account");
|
||||||
|
this->setLayout(&this->ui_.layout);
|
||||||
|
|
||||||
this->setLayout(&this->ui_.mainLayout);
|
// Label with explanation of what does user have to do here
|
||||||
this->ui_.mainLayout.addWidget(&this->ui_.tabWidget);
|
this->ui_.helpLabel.setText(
|
||||||
|
QString("Click on the \"Log in\" button to open <a href=\"%1\">Twitch "
|
||||||
|
"login page</a> in browser.<br>If it doesn't open "
|
||||||
|
"automatically, right click the link and open it yourself."
|
||||||
|
"<br><br>You can also paste your token from clipboard.")
|
||||||
|
.arg(this->loginLink.toString()));
|
||||||
|
this->ui_.helpLabel.setOpenExternalLinks(true);
|
||||||
|
|
||||||
this->ui_.tabWidget.addTab(&this->ui_.basic, "Basic");
|
// Label that ensures this is babyproof
|
||||||
this->ui_.tabWidget.addTab(&this->ui_.advanced, "Advanced");
|
this->ui_.warningLabel.setText("DO NOT SHOW THIS ON STREAM!!!");
|
||||||
|
this->ui_.warningLabel.setStyleSheet(
|
||||||
|
"QLabel { color: red; font-weight: 900;"
|
||||||
|
"font-size: 3em; text-align: center; }");
|
||||||
|
|
||||||
|
// Login buttons
|
||||||
|
this->ui_.loginButton.setText(LOGIN_BUTTON_START);
|
||||||
|
this->ui_.pasteTokenButton.setText(TOKEN_BUTTON_START);
|
||||||
|
|
||||||
|
// Separate box with Close button
|
||||||
this->ui_.buttonBox.setStandardButtons(QDialogButtonBox::Close);
|
this->ui_.buttonBox.setStandardButtons(QDialogButtonBox::Close);
|
||||||
|
|
||||||
QObject::connect(&this->ui_.buttonBox, &QDialogButtonBox::rejected,
|
// Add everything to layout
|
||||||
[this]() {
|
this->ui_.layout.addWidget(&this->ui_.helpLabel);
|
||||||
|
this->ui_.layout.addWidget(&this->ui_.warningLabel);
|
||||||
|
|
||||||
|
this->ui_.buttons.addWidget(&this->ui_.loginButton);
|
||||||
|
this->ui_.buttons.addWidget(&this->ui_.pasteTokenButton);
|
||||||
|
this->ui_.layout.addLayout(&this->ui_.buttons);
|
||||||
|
|
||||||
|
this->ui_.layout.addWidget(&this->ui_.buttonBox);
|
||||||
|
|
||||||
|
// Connect to button events
|
||||||
|
QObject::connect(&this->ui_.buttonBox, &QDialogButtonBox::rejected, [this] {
|
||||||
this->close();
|
this->close();
|
||||||
});
|
});
|
||||||
|
|
||||||
this->ui_.mainLayout.addWidget(&this->ui_.buttonBox);
|
QObject::connect(&this->ui_.loginButton, &QPushButton::clicked, [this] {
|
||||||
|
// Start listening for credentials
|
||||||
|
if (!this->loginServer_->listen())
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoWidget) << "Failed to start HTTP server";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qInfo(chatterinoWidget) << "HTTP server Listening on " +
|
||||||
|
this->loginServer_->getAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open login page
|
||||||
|
QDesktopServices::openUrl(this->loginLink);
|
||||||
|
this->deactivateLoginButton();
|
||||||
|
});
|
||||||
|
|
||||||
|
QObject::connect(&this->ui_.pasteTokenButton, &QPushButton::clicked, [this] {
|
||||||
|
this->ui_.pasteTokenButton.setText(VALIDATING_TOKEN);
|
||||||
|
this->ui_.pasteTokenButton.setDisabled(true);
|
||||||
|
this->logInWithToken(
|
||||||
|
getClipboardText(),
|
||||||
|
[] {
|
||||||
|
// success
|
||||||
|
// Removing clipboard content to prevent accidental paste of credentials into somewhere
|
||||||
|
crossPlatformCopy(QString());
|
||||||
|
},
|
||||||
|
[this] {
|
||||||
|
// failure
|
||||||
|
this->ui_.pasteTokenButton.setText("Invalid token pasted!");
|
||||||
|
},
|
||||||
|
[this] {
|
||||||
|
// finally
|
||||||
|
// Add some sort of "rate-limit" to not spam logInWithToken
|
||||||
|
QTimer::singleShot(3000, [this] {
|
||||||
|
this->ui_.pasteTokenButton.setText(TOKEN_BUTTON_START);
|
||||||
|
this->ui_.pasteTokenButton.setEnabled(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoginWidget::hideEvent(QHideEvent *event)
|
void LoginDialog::deactivateLoginButton()
|
||||||
|
{
|
||||||
|
this->ui_.loginButton.setText("Continue in browser...");
|
||||||
|
this->ui_.loginButton.setDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoginDialog::activateLoginButton()
|
||||||
|
{
|
||||||
|
this->ui_.loginButton.setText(LOGIN_BUTTON_START);
|
||||||
|
this->ui_.loginButton.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoginDialog::logInWithToken(QString token,
|
||||||
|
std::function<void()> successCallback,
|
||||||
|
std::function<void()> failureCallback,
|
||||||
|
std::function<void()> finallyCallback)
|
||||||
|
{
|
||||||
|
NetworkRequest("https://id.twitch.tv/oauth2/validate")
|
||||||
|
.timeout(5 * 1000)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "OAuth " + token)
|
||||||
|
.onSuccess([successCallback, failureCallback, token,
|
||||||
|
this](NetworkResult result) -> Outcome {
|
||||||
|
auto root = result.parseJson();
|
||||||
|
TokenValidationResponse validation(root);
|
||||||
|
|
||||||
|
if (validation.clientId.isEmpty() || validation.login.isEmpty() ||
|
||||||
|
validation.userId.isEmpty())
|
||||||
|
{
|
||||||
|
failureCallback();
|
||||||
|
return Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update account settings and call it a success
|
||||||
|
std::string basePath =
|
||||||
|
"/accounts/uid" + validation.userId.toStdString();
|
||||||
|
pajlada::Settings::Setting<QString>::set(basePath + "/username",
|
||||||
|
validation.login);
|
||||||
|
pajlada::Settings::Setting<QString>::set(basePath + "/userID",
|
||||||
|
validation.userId);
|
||||||
|
pajlada::Settings::Setting<QString>::set(basePath + "/clientID",
|
||||||
|
validation.clientId);
|
||||||
|
pajlada::Settings::Setting<QString>::set(basePath + "/oauthToken",
|
||||||
|
token);
|
||||||
|
|
||||||
|
getApp()->accounts->twitch.reloadUsers();
|
||||||
|
getApp()->accounts->twitch.currentUsername = validation.login;
|
||||||
|
// Closing the window will emit hideEvent which will close the server
|
||||||
|
// however, any already extablished connections will not terminate immidiatelly,
|
||||||
|
// so it's fine to close the window already
|
||||||
|
this->window()->close();
|
||||||
|
|
||||||
|
successCallback();
|
||||||
|
return Success;
|
||||||
|
})
|
||||||
|
.onError([failureCallback](auto result) {
|
||||||
|
failureCallback();
|
||||||
|
})
|
||||||
|
.finally(finallyCallback)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoginDialog::hideEvent(QHideEvent *event)
|
||||||
{
|
{
|
||||||
// Make the port free
|
// Make the port free
|
||||||
this->ui_.basic.closeHttpServer();
|
this->loginServer_->close();
|
||||||
|
// Restore login button
|
||||||
|
this->activateLoginButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -1,97 +1,81 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/NetworkRequest.hpp"
|
||||||
|
#include "common/Outcome.hpp"
|
||||||
#include "widgets/BaseWidget.hpp"
|
#include "widgets/BaseWidget.hpp"
|
||||||
|
#include "widgets/helper/LoginServer.hpp"
|
||||||
|
|
||||||
#include <QAction>
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QButtonGroup>
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QFormLayout>
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QHeaderView>
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QTabWidget>
|
|
||||||
#include <QTcpServer>
|
|
||||||
#include <QTcpSocket>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QtCore/QVariant>
|
#include <QtCore/QVariant>
|
||||||
#include <QtHttpServer/QHttpServer>
|
|
||||||
|
#define LOGIN_BUTTON_START "Click to Log in"
|
||||||
|
#define TOKEN_BUTTON_START "Paste token from clipboard"
|
||||||
|
#define VALIDATING_TOKEN "Validating your token..."
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
class BasicLoginWidget : public QWidget
|
class LoginServer;
|
||||||
|
|
||||||
|
struct TokenValidationResponse {
|
||||||
|
QString clientId;
|
||||||
|
QString login;
|
||||||
|
QString userId;
|
||||||
|
std::vector<QString> scopes;
|
||||||
|
int expiresIn;
|
||||||
|
|
||||||
|
explicit TokenValidationResponse(QJsonObject root)
|
||||||
|
: clientId(root.value("client_id").toString())
|
||||||
|
, login(root.value("login").toString())
|
||||||
|
, userId(root.value("user_id").toString())
|
||||||
|
, expiresIn(root.value("expires_in").toInt())
|
||||||
|
{
|
||||||
|
for (const auto &scope : root.value("scopes").toArray())
|
||||||
|
{
|
||||||
|
this->scopes.emplace_back(scope.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class LoginDialog : public QDialog
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BasicLoginWidget();
|
LoginDialog(QWidget *parent);
|
||||||
|
|
||||||
|
private:
|
||||||
struct {
|
struct {
|
||||||
QVBoxLayout layout;
|
QVBoxLayout layout;
|
||||||
QHBoxLayout horizontalLayout;
|
|
||||||
|
QLabel helpLabel;
|
||||||
|
QLabel warningLabel;
|
||||||
|
|
||||||
|
QHBoxLayout buttons;
|
||||||
QPushButton loginButton;
|
QPushButton loginButton;
|
||||||
QPushButton pasteCodeButton;
|
QPushButton pasteTokenButton;
|
||||||
QLabel unableToOpenBrowserHelper;
|
|
||||||
} ui_;
|
|
||||||
|
|
||||||
void closeHttpServer();
|
|
||||||
|
|
||||||
private:
|
|
||||||
// Local server listening to login data
|
|
||||||
const QHostAddress serverAddress{QHostAddress::LocalHost};
|
|
||||||
static const int serverPort = 52107;
|
|
||||||
QHttpServer *httpServer_;
|
|
||||||
QTcpServer *tcpServer_;
|
|
||||||
};
|
|
||||||
|
|
||||||
class AdvancedLoginWidget : public QWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
AdvancedLoginWidget();
|
|
||||||
|
|
||||||
void refreshButtons();
|
|
||||||
|
|
||||||
struct {
|
|
||||||
QVBoxLayout layout;
|
|
||||||
|
|
||||||
QLabel instructionsLabel;
|
|
||||||
|
|
||||||
QFormLayout formLayout;
|
|
||||||
|
|
||||||
QLineEdit userIDInput;
|
|
||||||
QLineEdit usernameInput;
|
|
||||||
QLineEdit clientIDInput;
|
|
||||||
QLineEdit oauthTokenInput;
|
|
||||||
|
|
||||||
struct {
|
|
||||||
QHBoxLayout layout;
|
|
||||||
|
|
||||||
QPushButton addUserButton;
|
|
||||||
QPushButton clearFieldsButton;
|
|
||||||
} buttonUpperRow;
|
|
||||||
} ui_;
|
|
||||||
};
|
|
||||||
|
|
||||||
class LoginWidget : public QDialog
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
LoginWidget(QWidget *parent);
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct {
|
|
||||||
QVBoxLayout mainLayout;
|
|
||||||
|
|
||||||
QTabWidget tabWidget;
|
|
||||||
|
|
||||||
QDialogButtonBox buttonBox;
|
QDialogButtonBox buttonBox;
|
||||||
|
|
||||||
BasicLoginWidget basic;
|
|
||||||
|
|
||||||
AdvancedLoginWidget advanced;
|
|
||||||
} ui_;
|
} ui_;
|
||||||
|
|
||||||
|
// Local server listening to login data returned from Twitch
|
||||||
|
LoginServer *loginServer_;
|
||||||
|
QUrl loginLink;
|
||||||
|
|
||||||
|
void deactivateLoginButton();
|
||||||
|
void activateLoginButton();
|
||||||
|
void logInWithToken(QString token, std::function<void()> successCallback,
|
||||||
|
std::function<void()> failureCallback,
|
||||||
|
std::function<void()> finallyCallback);
|
||||||
|
|
||||||
void hideEvent(QHideEvent *e) override;
|
void hideEvent(QHideEvent *e) override;
|
||||||
|
|
||||||
|
friend class LoginServer;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
104
src/widgets/helper/LoginServer.cpp
Normal file
104
src/widgets/helper/LoginServer.cpp
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
#include "widgets/helper/LoginServer.hpp"
|
||||||
|
|
||||||
|
#include "common/QLogging.hpp"
|
||||||
|
#include "widgets/dialogs/LoginDialog.hpp"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
LoginServer::LoginServer(LoginDialog *parent)
|
||||||
|
: parent_(parent)
|
||||||
|
, http_(new QHttpServer())
|
||||||
|
, tcp_(new QTcpServer(this->http_))
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoWidget) << "Creating new HTTP server";
|
||||||
|
this->http_->bind(this->tcp_);
|
||||||
|
this->initializeRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString LoginServer::getAddress()
|
||||||
|
{
|
||||||
|
return QString("%1:%2")
|
||||||
|
.arg(this->bind_.ip.toString())
|
||||||
|
.arg(this->bind_.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LoginServer::listen()
|
||||||
|
{
|
||||||
|
return this->tcp_->listen(this->bind_.ip, this->bind_.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoginServer::close()
|
||||||
|
{
|
||||||
|
// There's no way to close QHttpServer, so we have to work with our QTcpServer instead
|
||||||
|
qCDebug(chatterinoWidget) << "Closing TCP server bound to HTTP server";
|
||||||
|
this->tcp_->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoginServer::initializeRoutes()
|
||||||
|
{
|
||||||
|
// Redirect page containing JS script that takes token from URL fragment and calls /token
|
||||||
|
this->http_->route(
|
||||||
|
"/redirect", QHttpServerRequest::Method::GET,
|
||||||
|
[](QHttpServerResponder &&resp) {
|
||||||
|
QFile redirectPage(":/auth.html");
|
||||||
|
redirectPage.open(QIODevice::ReadOnly);
|
||||||
|
|
||||||
|
resp.write(redirectPage.readAll(),
|
||||||
|
{{"Access-Control-Allow-Origin", "*"},
|
||||||
|
{"Access-Control-Allow-Methods", "GET, PUT"},
|
||||||
|
{"Access-Control-Allow-Headers", "X-Access-Token"}},
|
||||||
|
QHttpServerResponder::StatusCode::Ok);
|
||||||
|
});
|
||||||
|
// For CORS, letting the browser know that it's fine to make a cross-origin request
|
||||||
|
this->http_->route(
|
||||||
|
".*", QHttpServerRequest::Method::OPTIONS,
|
||||||
|
[](QHttpServerResponder &&resp) {
|
||||||
|
resp.write({{"Access-Control-Allow-Origin", "*"},
|
||||||
|
{"Access-Control-Allow-Methods", "GET, PUT"},
|
||||||
|
{"Access-Control-Allow-Headers", "X-Access-Token"}},
|
||||||
|
QHttpServerResponder::StatusCode::Ok);
|
||||||
|
});
|
||||||
|
// Endpoint called from /redirect that processes token passed in headers
|
||||||
|
// Returns no content, but different headers indicating token's validity
|
||||||
|
this->http_->route(
|
||||||
|
"/token", QHttpServerRequest::Method::PUT,
|
||||||
|
[this](const QHttpServerRequest &req, QHttpServerResponder &&resp) {
|
||||||
|
// Access token wasn't specified
|
||||||
|
if (!req.headers().contains("X-Access-Token"))
|
||||||
|
{
|
||||||
|
resp.write(QHttpServerResponder::StatusCode::BadRequest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate token
|
||||||
|
const auto token = req.headers().value("X-Access-Token").toString();
|
||||||
|
|
||||||
|
auto respPtr =
|
||||||
|
std::make_shared<QHttpServerResponder>(std::move(resp));
|
||||||
|
this->parent_->ui_.loginButton.setText(VALIDATING_TOKEN);
|
||||||
|
this->parent_->logInWithToken(
|
||||||
|
token,
|
||||||
|
[respPtr] {
|
||||||
|
respPtr->write(
|
||||||
|
{{"Access-Control-Allow-Origin", "*"},
|
||||||
|
{"Access-Control-Allow-Methods", "GET, PUT"},
|
||||||
|
{"Access-Control-Allow-Headers", "X-Access-Token"}},
|
||||||
|
QHttpServerResponder::StatusCode::Ok);
|
||||||
|
},
|
||||||
|
[respPtr] {
|
||||||
|
respPtr->write(
|
||||||
|
{{"Access-Control-Allow-Origin", "*"},
|
||||||
|
{"Access-Control-Allow-Methods", "GET, PUT"},
|
||||||
|
{"Access-Control-Allow-Headers", "X-Access-Token"}},
|
||||||
|
QHttpServerResponder::StatusCode::BadRequest);
|
||||||
|
},
|
||||||
|
[this] {
|
||||||
|
this->close();
|
||||||
|
this->parent_->activateLoginButton();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
36
src/widgets/helper/LoginServer.hpp
Normal file
36
src/widgets/helper/LoginServer.hpp
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "widgets/dialogs/LoginDialog.hpp"
|
||||||
|
|
||||||
|
#include <QHttpServer>
|
||||||
|
#include <QTcpServer>
|
||||||
|
#include <QtHttpServer/QHttpServer>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class LoginDialog;
|
||||||
|
|
||||||
|
class LoginServer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LoginServer(LoginDialog *parent);
|
||||||
|
|
||||||
|
QString getAddress();
|
||||||
|
bool listen();
|
||||||
|
void close();
|
||||||
|
|
||||||
|
private:
|
||||||
|
LoginDialog *parent_;
|
||||||
|
|
||||||
|
const struct {
|
||||||
|
QHostAddress ip = QHostAddress::LocalHost;
|
||||||
|
int port = 52107;
|
||||||
|
} bind_;
|
||||||
|
|
||||||
|
QHttpServer *http_;
|
||||||
|
QTcpServer *tcp_;
|
||||||
|
|
||||||
|
void initializeRoutes();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -33,10 +33,10 @@ AccountsPage::AccountsPage()
|
||||||
view->getTableView()->horizontalHeader()->setStretchLastSection(true);
|
view->getTableView()->horizontalHeader()->setStretchLastSection(true);
|
||||||
|
|
||||||
view->addButtonPressed.connect([this] {
|
view->addButtonPressed.connect([this] {
|
||||||
static auto loginWidget = new LoginWidget(this);
|
static auto loginDialog = new LoginDialog(this);
|
||||||
|
|
||||||
loginWidget->show();
|
loginDialog->show();
|
||||||
loginWidget->raise();
|
loginDialog->raise();
|
||||||
});
|
});
|
||||||
|
|
||||||
view->getTableView()->setStyleSheet("background: #333");
|
view->getTableView()->setStyleSheet("background: #333");
|
||||||
|
|
Loading…
Reference in a new issue