0
0
mirror of https://github.com/obsproject/obs-studio.git synced 2024-09-20 13:08:50 +02:00
obs-studio/UI/auth-twitch.cpp
jp9000 f53df7da64 clang-format: Apply formatting
Code submissions have continually suffered from formatting
inconsistencies that constantly have to be addressed.  Using
clang-format simplifies this by making code formatting more consistent,
and allows automation of the code formatting so that maintainers can
focus more on the code itself instead of code formatting.
2019-06-23 23:49:10 -07:00

465 lines
12 KiB
C++

#include "auth-twitch.hpp"
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <qt-wrappers.hpp>
#include <obs-app.hpp>
#include "window-basic-main.hpp"
#include "remote-text.hpp"
#include "window-dock.hpp"
#include <json11.hpp>
#include "ui-config.h"
#include "obf.h"
using namespace json11;
#include <browser-panel.hpp>
extern QCef *cef;
extern QCefCookieManager *panel_cookies;
/* ------------------------------------------------------------------------- */
#define TWITCH_AUTH_URL "https://obsproject.com/app-auth/twitch?action=redirect"
#define TWITCH_TOKEN_URL "https://obsproject.com/app-auth/twitch-token"
#define ACCEPT_HEADER "Accept: application/vnd.twitchtv.v5+json"
#define TWITCH_SCOPE_VERSION 1
static Auth::Def twitchDef = {"Twitch", Auth::Type::OAuth_StreamKey};
/* ------------------------------------------------------------------------- */
TwitchAuth::TwitchAuth(const Def &d) : OAuthStreamKey(d)
{
if (!cef)
return;
cef->add_popup_whitelist_url(
"https://twitch.tv/popout/frankerfacez/chat?ffz-settings",
this);
uiLoadTimer.setSingleShot(true);
uiLoadTimer.setInterval(500);
connect(&uiLoadTimer, &QTimer::timeout, this,
&TwitchAuth::TryLoadSecondaryUIPanes);
}
bool TwitchAuth::GetChannelInfo()
try {
std::string client_id = TWITCH_CLIENTID;
deobfuscate_str(&client_id[0], TWITCH_HASH);
if (!GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION))
return false;
if (token.empty())
return false;
if (!key_.empty())
return true;
std::string auth;
auth += "Authorization: OAuth ";
auth += token;
std::vector<std::string> headers;
headers.push_back(std::string("Client-ID: ") + client_id);
headers.push_back(ACCEPT_HEADER);
headers.push_back(std::move(auth));
std::string output;
std::string error;
long error_code = 0;
bool success = false;
auto func = [&]() {
success = GetRemoteFile("https://api.twitch.tv/kraken/channel",
output, error, &error_code,
"application/json", nullptr, headers,
nullptr, 5);
};
ExecThreadedWithoutBlocking(
func, QTStr("Auth.LoadingChannel.Title"),
QTStr("Auth.LoadingChannel.Text").arg(service()));
if (error_code == 403) {
OBSMessageBox::warning(OBSBasic::Get(),
Str("TwitchAuth.TwoFactorFail.Title"),
Str("TwitchAuth.TwoFactorFail.Text"),
true);
blog(LOG_WARNING, "%s: %s", __FUNCTION__,
"Got 403 from Twitch, user probably does not "
"have two-factor authentication enabled on "
"their account");
return false;
}
if (!success || output.empty())
throw ErrorInfo("Failed to get text from remote", error);
Json json = Json::parse(output, error);
if (!error.empty())
throw ErrorInfo("Failed to parse json", error);
error = json["error"].string_value();
if (!error.empty()) {
if (error == "Unauthorized") {
if (RetryLogin()) {
return GetChannelInfo();
}
throw ErrorInfo(error, json["message"].string_value());
}
throw ErrorInfo(error,
json["error_description"].string_value());
}
name = json["name"].string_value();
key_ = json["stream_key"].string_value();
return true;
} catch (ErrorInfo info) {
QString title = QTStr("Auth.ChannelFailure.Title");
QString text = QTStr("Auth.ChannelFailure.Text")
.arg(service(), info.message.c_str(),
info.error.c_str());
QMessageBox::warning(OBSBasic::Get(), title, text);
blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(),
info.error.c_str());
return false;
}
void TwitchAuth::SaveInternal()
{
OBSBasic *main = OBSBasic::Get();
config_set_string(main->Config(), service(), "Name", name.c_str());
if (uiLoaded) {
config_set_string(main->Config(), service(), "DockState",
main->saveState().toBase64().constData());
}
OAuthStreamKey::SaveInternal();
}
static inline std::string get_config_str(OBSBasic *main, const char *section,
const char *name)
{
const char *val = config_get_string(main->Config(), section, name);
return val ? val : "";
}
bool TwitchAuth::LoadInternal()
{
if (!cef)
return false;
OBSBasic *main = OBSBasic::Get();
name = get_config_str(main, service(), "Name");
firstLoad = false;
return OAuthStreamKey::LoadInternal();
}
class TwitchWidget : public OBSDock {
public:
inline TwitchWidget() : OBSDock() {}
QScopedPointer<QCefWidget> widget;
inline void SetWidget(QCefWidget *widget_)
{
setWidget(widget_);
widget.reset(widget_);
}
};
static const char *ffz_script = "\
var ffz = document.createElement('script');\
ffz.setAttribute('src','https://cdn.frankerfacez.com/script/script.min.js');\
document.head.appendChild(ffz);";
static const char *bttv_script = "\
localStorage.setItem('bttv_clickTwitchEmotes', true);\
localStorage.setItem('bttv_darkenedMode', true);\
localStorage.setItem('bttv_bttvGIFEmotes', true);\
var bttv = document.createElement('script');\
bttv.setAttribute('src','https://cdn.betterttv.net/betterttv.js');\
document.head.appendChild(bttv);";
static const char *referrer_script1 = "\
Object.defineProperty(document, 'referrer', {get : function() { return '";
static const char *referrer_script2 = "'; }});";
void TwitchAuth::LoadUI()
{
if (!cef)
return;
if (uiLoaded)
return;
if (!GetChannelInfo())
return;
OBSBasic::InitBrowserPanelSafeBlock();
OBSBasic *main = OBSBasic::Get();
QCefWidget *browser;
std::string url;
std::string script;
std::string moderation_tools_url;
moderation_tools_url = "https://www.twitch.tv/";
moderation_tools_url += name;
moderation_tools_url += "/dashboard/settings/moderation?no-reload=true";
/* ----------------------------------- */
url = "https://www.twitch.tv/popout/";
url += name;
url += "/chat";
QSize size = main->frameSize();
QPoint pos = main->pos();
chat.reset(new TwitchWidget());
chat->setObjectName("twitchChat");
chat->resize(300, 600);
chat->setMinimumSize(200, 300);
chat->setWindowTitle(QTStr("Auth.Chat"));
chat->setAllowedAreas(Qt::AllDockWidgetAreas);
browser = cef->create_widget(nullptr, url, panel_cookies);
chat->SetWidget(browser);
cef->add_force_popup_url(moderation_tools_url, chat.data());
script = bttv_script;
script += ffz_script;
browser->setStartupScript(script);
main->addDockWidget(Qt::RightDockWidgetArea, chat.data());
chatMenu.reset(main->AddDockWidget(chat.data()));
/* ----------------------------------- */
chat->setFloating(true);
chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50);
if (firstLoad) {
chat->setVisible(true);
} else {
const char *dockStateStr = config_get_string(
main->Config(), service(), "DockState");
QByteArray dockState =
QByteArray::fromBase64(QByteArray(dockStateStr));
main->restoreState(dockState);
}
TryLoadSecondaryUIPanes();
uiLoaded = true;
}
void TwitchAuth::LoadSecondaryUIPanes()
{
OBSBasic *main = OBSBasic::Get();
QCefWidget *browser;
std::string url;
std::string script;
QSize size = main->frameSize();
QPoint pos = main->pos();
script = "localStorage.setItem('twilight.theme', 1);";
script += referrer_script1;
script += "https://www.twitch.tv/";
script += name;
script += "/dashboard/live";
script += referrer_script2;
script += bttv_script;
script += ffz_script;
/* ----------------------------------- */
url = "https://www.twitch.tv/popout/";
url += name;
url += "/dashboard/live/stream-info";
info.reset(new TwitchWidget());
info->setObjectName("twitchInfo");
info->resize(300, 650);
info->setMinimumSize(200, 300);
info->setWindowTitle(QTStr("Auth.StreamInfo"));
info->setAllowedAreas(Qt::AllDockWidgetAreas);
browser = cef->create_widget(nullptr, url, panel_cookies);
info->SetWidget(browser);
browser->setStartupScript(script);
main->addDockWidget(Qt::RightDockWidgetArea, info.data());
infoMenu.reset(main->AddDockWidget(info.data()));
/* ----------------------------------- */
url = "https://www.twitch.tv/popout/";
url += name;
url += "/dashboard/live/stats";
stat.reset(new TwitchWidget());
stat->setObjectName("twitchStats");
stat->resize(200, 250);
stat->setMinimumSize(200, 150);
stat->setWindowTitle(QTStr("TwitchAuth.Stats"));
stat->setAllowedAreas(Qt::AllDockWidgetAreas);
browser = cef->create_widget(nullptr, url, panel_cookies);
stat->SetWidget(browser);
browser->setStartupScript(script);
main->addDockWidget(Qt::RightDockWidgetArea, stat.data());
statMenu.reset(main->AddDockWidget(stat.data()));
/* ----------------------------------- */
url = "https://www.twitch.tv/popout/";
url += name;
url += "/dashboard/live/activity-feed";
feed.reset(new TwitchWidget());
feed->setObjectName("twitchFeed");
feed->resize(300, 650);
feed->setMinimumSize(200, 300);
feed->setWindowTitle(QTStr("TwitchAuth.Feed"));
feed->setAllowedAreas(Qt::AllDockWidgetAreas);
browser = cef->create_widget(nullptr, url, panel_cookies);
feed->SetWidget(browser);
browser->setStartupScript(script);
main->addDockWidget(Qt::RightDockWidgetArea, feed.data());
feedMenu.reset(main->AddDockWidget(feed.data()));
/* ----------------------------------- */
info->setFloating(true);
stat->setFloating(true);
feed->setFloating(true);
QSize statSize = stat->frameSize();
info->move(pos.x() + 50, pos.y() + 50);
stat->move(pos.x() + size.width() / 2 - statSize.width() / 2,
pos.y() + size.height() / 2 - statSize.height() / 2);
feed->move(pos.x() + 100, pos.y() + 100);
if (firstLoad) {
info->setVisible(true);
stat->setVisible(false);
feed->setVisible(false);
} else {
uint32_t lastVersion = config_get_int(App()->GlobalConfig(),
"General", "LastVersion");
if (lastVersion <= MAKE_SEMANTIC_VERSION(23, 0, 2)) {
feed->setVisible(false);
}
const char *dockStateStr = config_get_string(
main->Config(), service(), "DockState");
QByteArray dockState =
QByteArray::fromBase64(QByteArray(dockStateStr));
main->restoreState(dockState);
}
}
/* Twitch.tv has an OAuth for itself. If we try to load multiple panel pages
* at once before it's OAuth'ed itself, they will all try to perform the auth
* process at the same time, get their own request codes, and only the last
* code will be valid -- so one or more panels are guaranteed to fail.
*
* To solve this, we want to load just one panel first (the chat), and then all
* subsequent panels should only be loaded once we know that Twitch has auth'ed
* itself (if the cookie "auth-token" exists for twitch.tv).
*
* This is annoying to deal with. */
void TwitchAuth::TryLoadSecondaryUIPanes()
{
QPointer<TwitchAuth> this_ = this;
auto cb = [this_](bool found) {
if (!this_) {
return;
}
if (!found) {
QMetaObject::invokeMethod(&this_->uiLoadTimer, "start");
} else {
QMetaObject::invokeMethod(this_,
"LoadSecondaryUIPanes");
}
};
panel_cookies->CheckForCookie("https://www.twitch.tv", "auth-token",
cb);
}
bool TwitchAuth::RetryLogin()
{
OAuthLogin login(OBSBasic::Get(), TWITCH_AUTH_URL, false);
if (login.exec() == QDialog::Rejected) {
return false;
}
std::shared_ptr<TwitchAuth> auth =
std::make_shared<TwitchAuth>(twitchDef);
std::string client_id = TWITCH_CLIENTID;
deobfuscate_str(&client_id[0], TWITCH_HASH);
return GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION,
QT_TO_UTF8(login.GetCode()), true);
}
std::shared_ptr<Auth> TwitchAuth::Login(QWidget *parent)
{
OAuthLogin login(parent, TWITCH_AUTH_URL, false);
if (login.exec() == QDialog::Rejected) {
return nullptr;
}
std::shared_ptr<TwitchAuth> auth =
std::make_shared<TwitchAuth>(twitchDef);
std::string client_id = TWITCH_CLIENTID;
deobfuscate_str(&client_id[0], TWITCH_HASH);
if (!auth->GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION,
QT_TO_UTF8(login.GetCode()))) {
return nullptr;
}
std::string error;
if (auth->GetChannelInfo()) {
return auth;
}
return nullptr;
}
static std::shared_ptr<Auth> CreateTwitchAuth()
{
return std::make_shared<TwitchAuth>(twitchDef);
}
static void DeleteCookies()
{
if (panel_cookies)
panel_cookies->DeleteCookies("twitch.tv", std::string());
}
void RegisterTwitchAuth()
{
OAuth::RegisterOAuth(twitchDef, CreateTwitchAuth, TwitchAuth::Login,
DeleteCookies);
}