diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index ad81324d4..b86372472 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -39,6 +39,16 @@ else() set(MIXER_ENABLED TRUE) endif() +if(NOT DEFINED RESTREAM_CLIENTID OR "${RESTREAM_CLIENTID}" STREQUAL "" OR + NOT DEFINED RESTREAM_HASH OR "${RESTREAM_HASH}" STREQUAL "" OR + NOT BROWSER_AVAILABLE_INTERNAL) + set(RESTREAM_ENABLED FALSE) + set(RESTREAM_CLIENTID "") + set(RESTREAM_HASH "0") +else() + set(RESTREAM_ENABLED TRUE) +endif() + configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/ui-config.h.in" "${CMAKE_CURRENT_BINARY_DIR}/ui-config.h") @@ -159,6 +169,15 @@ if(BROWSER_AVAILABLE_INTERNAL) auth-mixer.hpp ) endif() + + if(RESTREAM_ENABLED) + list(APPEND obs_PLATFORM_SOURCES + auth-restream.cpp + ) + list(APPEND obs_PLATFORM_HEADERS + auth-restream.hpp + ) + endif() endif() set(obs_libffutil_SOURCES diff --git a/UI/auth-restream.cpp b/UI/auth-restream.cpp new file mode 100644 index 000000000..c45f6890f --- /dev/null +++ b/UI/auth-restream.cpp @@ -0,0 +1,283 @@ +#include "auth-restream.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include "window-basic-main.hpp" +#include "remote-text.hpp" +#include "ui-config.h" +#include "obf.h" +#include + +using namespace json11; + +extern QCef *cef; +extern QCefCookieManager *panel_cookies; + +/* ------------------------------------------------------------------------- */ + +#define RESTREAM_AUTH_URL "https://obsproject.com/app-auth/restream?action=redirect" +#define RESTREAM_TOKEN_URL "https://obsproject.com/app-auth/restream-token" +#define RESTREAM_STREAMKEY_URL "https://api.restream.io/v2/user/streamKey" +#define RESTREAM_SCOPE_VERSION 1 + + +static Auth::Def restreamDef = { + "Restream", + Auth::Type::OAuth_StreamKey +}; + +/* ------------------------------------------------------------------------- */ + +RestreamAuth::RestreamAuth(const Def &d) + : OAuthStreamKey(d) +{ +} + +bool RestreamAuth::GetChannelInfo() +try { + std::string client_id = RESTREAM_CLIENTID; + deobfuscate_str(&client_id[0], RESTREAM_HASH); + + if (!GetToken(RESTREAM_TOKEN_URL, client_id, RESTREAM_SCOPE_VERSION)) + return false; + if (token.empty()) + return false; + if (!key_.empty()) + return true; + + std::string auth; + auth += "Authorization: Bearer "; + auth += token; + + std::vector headers; + headers.push_back(std::string("Client-ID: ") + client_id); + headers.push_back(std::move(auth)); + + std::string output; + std::string error; + Json json; + bool success; + + auto func = [&] () { + success = GetRemoteFile( + RESTREAM_STREAMKEY_URL, + output, + error, + nullptr, + "application/json", + nullptr, + headers, + nullptr, + 5); + }; + + ExecThreadedWithoutBlocking( + func, + QTStr("Auth.LoadingChannel.Title"), + QTStr("Auth.LoadingChannel.Text").arg(service())); + if (!success || output.empty()) + throw ErrorInfo("Failed to get stream key from remote", error); + + json = Json::parse(output, error); + if (!error.empty()) + throw ErrorInfo("Failed to parse json", error); + + error = json["error"].string_value(); + if (!error.empty()) + throw ErrorInfo(error, json["error_description"].string_value()); + + key_ = json["streamKey"].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 RestreamAuth::SaveInternal() +{ + OBSBasic *main = OBSBasic::Get(); + 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 RestreamAuth::LoadInternal() +{ + firstLoad = false; + return OAuthStreamKey::LoadInternal(); +} + +class RestreamWidget : public QDockWidget { +public: + inline RestreamWidget() : QDockWidget() {} + + QScopedPointer widget; +}; + +void RestreamAuth::LoadUI() +{ + if (uiLoaded) + return; + if (!GetChannelInfo()) + return; + + OBSBasic::InitBrowserPanelSafeBlock(); + OBSBasic *main = OBSBasic::Get(); + + QCefWidget *browser; + std::string url; + std::string script; + + /* ----------------------------------- */ + + url = "https://restream.io/chat-application"; + + QSize size = main->frameSize(); + QPoint pos = main->pos(); + + chat.reset(new RestreamWidget()); + chat->setObjectName("restreamChat"); + chat->resize(420, 600); + chat->setMinimumSize(380, 300); + chat->setWindowTitle(QTStr("Auth.Chat")); + chat->setAllowedAreas(Qt::AllDockWidgetAreas); + + browser = cef->create_widget(nullptr, url, panel_cookies); + chat->setWidget(browser); + + main->addDockWidget(Qt::RightDockWidgetArea, chat.data()); + chatMenu.reset(main->AddDockWidget(chat.data())); + + /* ----------------------------------- */ + + url = "https://restream.io/titles/embed"; + + info.reset(new RestreamWidget()); + info->setObjectName("restreamInfo"); + info->resize(410, 600); + info->setMinimumSize(380, 300); + info->setWindowTitle(QTStr("Auth.StreamInfo")); + info->setAllowedAreas(Qt::AllDockWidgetAreas); + + browser = cef->create_widget(nullptr, url, panel_cookies); + info->setWidget(browser); + + main->addDockWidget(Qt::RightDockWidgetArea, info.data()); + infoMenu.reset(main->AddDockWidget(info.data())); + + /* ----------------------------------- */ + + chat->setFloating(true); + info->setFloating(true); + chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50); + info->move(pos.x() + 40, pos.y() + 50); + + if (firstLoad) { + chat->setVisible(true); + info->setVisible(true); + } + else { + const char *dockStateStr = config_get_string(main->Config(), + service(), "DockState"); + QByteArray dockState = + QByteArray::fromBase64(QByteArray(dockStateStr)); + main->restoreState(dockState); + } + + uiLoaded = true; +} + +bool RestreamAuth::RetryLogin() +{ + OAuthLogin login(OBSBasic::Get(), RESTREAM_AUTH_URL, false); + cef->add_popup_whitelist_url("about:blank", &login); + if (login.exec() == QDialog::Rejected) { + return false; + } + + std::shared_ptr auth = + std::make_shared(restreamDef); + + std::string client_id = RESTREAM_CLIENTID; + deobfuscate_str(&client_id[0], RESTREAM_HASH); + + return GetToken(RESTREAM_TOKEN_URL, client_id, + RESTREAM_SCOPE_VERSION, + QT_TO_UTF8(login.GetCode()), true); +} + +std::shared_ptr RestreamAuth::Login(QWidget *parent) +{ + OAuthLogin login(parent, RESTREAM_AUTH_URL, false); + cef->add_popup_whitelist_url("about:blank", &login); + + if (login.exec() == QDialog::Rejected) { + return nullptr; + } + + std::shared_ptr auth = + std::make_shared(restreamDef); + + std::string client_id = RESTREAM_CLIENTID; + deobfuscate_str(&client_id[0], RESTREAM_HASH); + + if (!auth->GetToken(RESTREAM_TOKEN_URL, client_id, + RESTREAM_SCOPE_VERSION, + QT_TO_UTF8(login.GetCode()))) { + return nullptr; + } + + std::string error; + if (auth->GetChannelInfo()) { + return auth; + } + + return nullptr; +} + +static std::shared_ptr CreateRestreamAuth() +{ + return std::make_shared(restreamDef); +} + +static void DeleteCookies() +{ + if (panel_cookies) { + panel_cookies->DeleteCookies("restream.io", std::string()); + } +} + +void RegisterRestreamAuth() +{ + OAuth::RegisterOAuth( + restreamDef, + CreateRestreamAuth, + RestreamAuth::Login, + DeleteCookies); +} diff --git a/UI/auth-restream.hpp b/UI/auth-restream.hpp new file mode 100644 index 000000000..5e66fb096 --- /dev/null +++ b/UI/auth-restream.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "auth-oauth.hpp" + +class RestreamWidget; + +class RestreamAuth : public OAuthStreamKey { + Q_OBJECT + + QSharedPointer chat; + QSharedPointer info; + QSharedPointer chatMenu; + QSharedPointer infoMenu; + bool uiLoaded = false; + + virtual bool RetryLogin() override; + + virtual void SaveInternal() override; + virtual bool LoadInternal() override; + + bool GetChannelInfo(); + + virtual void LoadUI() override; + +public: + RestreamAuth(const Def &d); + + static std::shared_ptr Login(QWidget *parent); +}; diff --git a/UI/ui-config.h.in b/UI/ui-config.h.in index b31de90d8..d2c7dd5fe 100644 --- a/UI/ui-config.h.in +++ b/UI/ui-config.h.in @@ -23,3 +23,7 @@ #define MIXER_ENABLED @MIXER_ENABLED@ #define MIXER_CLIENTID "@MIXER_CLIENTID@" #define MIXER_HASH 0x@MIXER_HASH@ + +#define RESTREAM_ENABLED @RESTREAM_ENABLED@ +#define RESTREAM_CLIENTID "@RESTREAM_CLIENTID@" +#define RESTREAM_HASH 0x@RESTREAM_HASH@ diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index eb5a74882..b72065c0d 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -193,6 +193,7 @@ void assignDockToggle(QDockWidget *dock, QAction *action) extern void RegisterTwitchAuth(); extern void RegisterMixerAuth(); +extern void RegisterRestreamAuth(); OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow (parent), @@ -206,6 +207,9 @@ OBSBasic::OBSBasic(QWidget *parent) #if MIXER_ENABLED RegisterMixerAuth(); #endif +#if RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif setAcceptDrops(true);