0
0
mirror of https://github.com/obsproject/obs-studio.git synced 2024-09-20 04:42:18 +02:00
obs-studio/UI/auth-twitch.cpp
PatTheMav 710d99ef4d UI: Improve incremental compile times via explicit file includes
When a source file contains an explicit include with a filename
following the "moc_<actual-filename>.cpp" pattern, then CMake's
AUTOMOC generation tool will recognize the matching pair and generate
the replacement header file and add the required include directory
entries.

For all files which do contain Q_OBJECT or similar declarations but do
not have an explicit include directive, the global mocs_compilation.cpp
file will still be generated (which groups all "missing" generated
headers).

The larger this global file is, the more expensive incremental
compilation will be as this file (and all its contained generated
headers) will be re-generated regardless of whether actual changes
occurred.
2024-08-22 16:45:12 -04:00

523 lines
14 KiB
C++

#include "moc_auth-twitch.cpp"
#include <QRegularExpression>
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QUuid>
#include <qt-wrappers.hpp>
#include <obs-app.hpp>
#include "window-dock-browser.hpp"
#include "window-basic-main.hpp"
#include "remote-text.hpp"
#include <json11.hpp>
#include "ui-config.h"
#include "obf.h"
using namespace json11;
/* ------------------------------------------------------------------------- */
#define TWITCH_AUTH_URL OAUTH_BASE_URL "v1/twitch/redirect"
#define TWITCH_TOKEN_URL OAUTH_BASE_URL "v1/twitch/token"
#define TWITCH_SCOPE_VERSION 1
#define TWITCH_CHAT_DOCK_NAME "twitchChat"
#define TWITCH_INFO_DOCK_NAME "twitchInfo"
#define TWITCH_STATS_DOCK_NAME "twitchStats"
#define TWITCH_FEED_DOCK_NAME "twitchFeed"
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);
/* enables javascript-based popups. basically bttv popups */
cef->add_popup_whitelist_url("about:blank#blocked", this);
uiLoadTimer.setSingleShot(true);
uiLoadTimer.setInterval(500);
connect(&uiLoadTimer, &QTimer::timeout, this,
&TwitchAuth::TryLoadSecondaryUIPanes);
}
TwitchAuth::~TwitchAuth()
{
if (!uiLoaded)
return;
OBSBasic *main = OBSBasic::Get();
main->RemoveDockWidget(TWITCH_CHAT_DOCK_NAME);
main->RemoveDockWidget(TWITCH_INFO_DOCK_NAME);
main->RemoveDockWidget(TWITCH_STATS_DOCK_NAME);
main->RemoveDockWidget(TWITCH_FEED_DOCK_NAME);
}
bool TwitchAuth::MakeApiRequest(const char *path, Json &json_out)
{
std::string client_id = TWITCH_CLIENTID;
deobfuscate_str(&client_id[0], TWITCH_HASH);
std::string url = "https://api.twitch.tv/helix/";
url += std::string(path);
std::vector<std::string> headers;
headers.push_back(std::string("Client-ID: ") + client_id);
headers.push_back(std::string("Authorization: Bearer ") + token);
std::string output;
std::string error;
long error_code = 0;
bool success = false;
auto func = [&]() {
success = GetRemoteFile(url.c_str(), 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_out = Json::parse(output, error);
if (!error.empty())
throw ErrorInfo("Failed to parse json", error);
error = json_out["error"].string_value();
if (!error.empty())
throw ErrorInfo(error, json_out["message"].string_value());
return true;
}
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;
Json json;
bool success = MakeApiRequest("users", json);
if (!success)
return false;
name = json["data"][0]["login"].string_value();
std::string path = "streams/key?broadcaster_id=" +
json["data"][0]["id"].string_value();
success = MakeApiRequest(path.c_str(), json);
if (!success)
return false;
key_ = json["data"][0]["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());
config_set_string(main->Config(), service(), "UUID", uuid.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");
uuid = get_config_str(main, service(), "UUID");
firstLoad = false;
return OAuthStreamKey::LoadInternal();
}
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;
/* Twitch panels require a UUID, it does not actually need to be unique,
* and is generated client-side.
* It is only for preferences stored in the browser's local store. */
if (uuid.empty()) {
QString qtUuid = QUuid::createUuid().toString();
qtUuid.replace(QRegularExpression("[{}-]"), "");
uuid = qtUuid.toStdString();
}
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();
BrowserDock *chat = new BrowserDock(QTStr("Auth.Chat"));
chat->setObjectName(TWITCH_CHAT_DOCK_NAME);
chat->resize(300, 600);
chat->setMinimumSize(200, 300);
chat->setWindowTitle(QTStr("Auth.Chat"));
chat->setAllowedAreas(Qt::AllDockWidgetAreas);
browser = cef->create_widget(chat, url, panel_cookies);
chat->SetWidget(browser);
cef->add_force_popup_url(moderation_tools_url, chat);
if (App()->IsThemeDark()) {
script = "localStorage.setItem('twilight.theme', 1);";
} else {
script = "localStorage.setItem('twilight.theme', 0);";
}
const int twAddonChoice =
config_get_int(main->Config(), service(), "AddonChoice");
if (twAddonChoice) {
if (twAddonChoice & 0x1)
script += bttv_script;
if (twAddonChoice & 0x2)
script += ffz_script;
}
browser->setStartupScript(script);
main->AddDockWidget(chat, Qt::RightDockWidgetArea);
/* ----------------------------------- */
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));
if (main->isVisible() || !main->isMaximized())
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();
if (App()->IsThemeDark()) {
script = "localStorage.setItem('twilight.theme', 1);";
} else {
script = "localStorage.setItem('twilight.theme', 0);";
}
script += referrer_script1;
script += "https://www.twitch.tv/";
script += name;
script += "/dashboard/live";
script += referrer_script2;
const int twAddonChoice =
config_get_int(main->Config(), service(), "AddonChoice");
if (twAddonChoice) {
if (twAddonChoice & 0x1)
script += bttv_script;
if (twAddonChoice & 0x2)
script += ffz_script;
}
/* ----------------------------------- */
url = "https://dashboard.twitch.tv/popout/u/";
url += name;
url += "/stream-manager/edit-stream-info";
BrowserDock *info = new BrowserDock(QTStr("Auth.StreamInfo"));
info->setObjectName(TWITCH_INFO_DOCK_NAME);
info->resize(300, 650);
info->setMinimumSize(200, 300);
info->setWindowTitle(QTStr("Auth.StreamInfo"));
info->setAllowedAreas(Qt::AllDockWidgetAreas);
browser = cef->create_widget(info, url, panel_cookies);
info->SetWidget(browser);
browser->setStartupScript(script);
main->AddDockWidget(info, Qt::RightDockWidgetArea);
/* ----------------------------------- */
url = "https://www.twitch.tv/popout/";
url += name;
url += "/dashboard/live/stats";
BrowserDock *stats = new BrowserDock(QTStr("TwitchAuth.Stats"));
stats->setObjectName(TWITCH_STATS_DOCK_NAME);
stats->resize(200, 250);
stats->setMinimumSize(200, 150);
stats->setWindowTitle(QTStr("TwitchAuth.Stats"));
stats->setAllowedAreas(Qt::AllDockWidgetAreas);
browser = cef->create_widget(stats, url, panel_cookies);
stats->SetWidget(browser);
browser->setStartupScript(script);
main->AddDockWidget(stats, Qt::RightDockWidgetArea);
/* ----------------------------------- */
url = "https://dashboard.twitch.tv/popout/u/";
url += name;
url += "/stream-manager/activity-feed";
url += "?uuid=" + uuid;
BrowserDock *feed = new BrowserDock(QTStr("TwitchAuth.Feed"));
feed->setObjectName(TWITCH_FEED_DOCK_NAME);
feed->resize(300, 650);
feed->setMinimumSize(200, 300);
feed->setWindowTitle(QTStr("TwitchAuth.Feed"));
feed->setAllowedAreas(Qt::AllDockWidgetAreas);
browser = cef->create_widget(feed, url, panel_cookies);
feed->SetWidget(browser);
browser->setStartupScript(script);
main->AddDockWidget(feed, Qt::RightDockWidgetArea);
/* ----------------------------------- */
info->setFloating(true);
stats->setFloating(true);
feed->setFloating(true);
QSize statSize = stats->frameSize();
info->move(pos.x() + 50, pos.y() + 50);
stats->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);
stats->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));
if (main->isVisible() || !main->isMaximized())
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, const std::string &)
{
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;
}
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()
{
#if !defined(__APPLE__) && !defined(_WIN32)
if (QApplication::platformName().contains("wayland"))
return;
#endif
OAuth::RegisterOAuth(twitchDef, CreateTwitchAuth, TwitchAuth::Login,
DeleteCookies);
}