diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 1639a1254051a1491f0ea1345ef5a99cbf0bc099..f86beb23e819d37f28bc999b0806da42e23d5d73 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -19,6 +19,20 @@ project(obs) set(DISABLE_UPDATE_MODULE TRUE CACHE BOOL "Disables building the update module") +if(NOT DEFINED MIXER_CLIENTID OR "${MIXER_CLIENTID}" STREQUAL "" OR + NOT DEFINED MIXER_HASH OR "${MIXER_HASH}" STREQUAL "" OR + NOT BROWSER_AVAILABLE_INTERNAL) + set(MIXER_ENABLED FALSE) + set(MIXER_CLIENTID "") + set(MIXER_HASH "0") +else() + set(MIXER_ENABLED TRUE) +endif() + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/ui-config.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/ui-config.h") + set(CMAKE_INCLUDE_CURRENT_DIR TRUE) set(CMAKE_AUTOMOC TRUE) @@ -39,6 +53,7 @@ endif() include_directories(${FFMPEG_INCLUDE_DIRS}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) include_directories(SYSTEM "obs-frontend-api") include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs") include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/deps/libff") @@ -114,6 +129,15 @@ if(BROWSER_AVAILABLE_INTERNAL) obf.h auth-oauth.hpp ) + + if(MIXER_ENABLED) + list(APPEND obs_PLATFORM_SOURCES + auth-mixer.cpp + ) + list(APPEND obs_PLATFORM_HEADERS + auth-mixer.hpp + ) + endif() endif() set(obs_libffutil_SOURCES diff --git a/UI/auth-mixer.cpp b/UI/auth-mixer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c3045155377163224004d8abe3d34477b61ce8d0 --- /dev/null +++ b/UI/auth-mixer.cpp @@ -0,0 +1,310 @@ +#include "auth-mixer.hpp" + +#include +#include +#include + +#include +#include + +#include "window-basic-main.hpp" +#include "remote-text.hpp" + +#include + +#include + +#include "ui-config.h" +#include "obf.h" + +using namespace json11; + +#include +extern QCef *cef; +extern QCefCookieManager *panel_cookies; + +/* ------------------------------------------------------------------------- */ + +#define MIXER_AUTH_URL \ + "https://obsproject.com/app-auth/mixer?action=redirect" +#define MIXER_TOKEN_URL \ + "https://obsproject.com/app-auth/mixer-token" + +#define MIXER_SCOPE_VERSION 1 + +static Auth::Def mixerDef = { + "Mixer", + Auth::Type::OAuth_StreamKey +}; + +/* ------------------------------------------------------------------------- */ + +MixerAuth::MixerAuth(const Def &d) + : OAuthStreamKey(d) +{ +} + +bool MixerAuth::GetChannelInfo() +try { + std::string client_id = MIXER_CLIENTID; + deobfuscate_str(&client_id[0], MIXER_HASH); + + if (!GetToken(MIXER_TOKEN_URL, client_id, MIXER_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; + + if (id.empty()) { + auto func = [&] () { + success = GetRemoteFile( + "https://mixer.com/api/v1/users/current", + output, + error, + nullptr, + "application/json", + nullptr, + headers, + nullptr, + 5); + }; + + ExecuteFuncSafeBlockMsgBox( + func, + QTStr("Auth.LoadingChannel.Title"), + QTStr("Auth.LoadingChannel.Text").arg(service())); + if (!success || output.empty()) + throw ErrorInfo("Failed to get user info 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()) + throw ErrorInfo(error, + json["error_description"].string_value()); + + id = std::to_string(json["channel"]["id"].int_value()); + name = json["channel"]["token"].string_value(); + } + + /* ------------------ */ + + std::string url; + url += "https://mixer.com/api/v1/channels/"; + url += id; + url += "/details"; + + output.clear(); + + auto func = [&] () { + success = GetRemoteFile( + url.c_str(), + output, + error, + nullptr, + "application/json", + nullptr, + headers, + nullptr, + 5); + }; + + ExecuteFuncSafeBlockMsgBox( + 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_ = id + "-" + 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 MixerAuth::SaveInternal() +{ + OBSBasic *main = OBSBasic::Get(); + config_set_string(main->Config(), service(), "Name", name.c_str()); + config_set_string(main->Config(), service(), "Id", id.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 MixerAuth::LoadInternal() +{ + OBSBasic *main = OBSBasic::Get(); + name = get_config_str(main, service(), "Name"); + id = get_config_str(main, service(), "Id"); + firstLoad = false; + return OAuthStreamKey::LoadInternal(); +} + +class MixerChat : public QDockWidget { +public: + inline MixerChat() : QDockWidget() {} + + QScopedPointer widget; +}; + +void MixerAuth::LoadUI() +{ + if (uiLoaded) + return; + if (!GetChannelInfo()) + return; + + OBSBasic::InitBrowserPanelSafeBlock(true); + OBSBasic *main = OBSBasic::Get(); + + std::string url; + url += "https://mixer.com/embed/chat/"; + url += id; + + QSize size = main->frameSize(); + QPoint pos = main->pos(); + + chat.reset(new MixerChat()); + chat->setObjectName("mixerChat"); + chat->resize(300, 600); + chat->setMinimumSize(200, 300); + chat->setWindowTitle(QTStr("Auth.Chat")); + chat->setAllowedAreas(Qt::AllDockWidgetAreas); + + QCefWidget *browser = cef->create_widget(nullptr, url, panel_cookies); + chat->setWidget(browser); + + 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); + } + + uiLoaded = true; +} + +bool MixerAuth::RetryLogin() +{ + OAuthLogin login(OBSBasic::Get(), MIXER_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(mixerDef); + std::string client_id = MIXER_CLIENTID; + deobfuscate_str(&client_id[0], MIXER_HASH); + + return GetToken(MIXER_TOKEN_URL, client_id, MIXER_SCOPE_VERSION, + QT_TO_UTF8(login.GetCode()), true); +} + +std::shared_ptr MixerAuth::Login(QWidget *parent) +{ + OAuthLogin login(parent, MIXER_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(mixerDef); + + std::string client_id = TWITCH_CLIENTID; + deobfuscate_str(&client_id[0], TWITCH_HASH); + + if (!auth->GetToken(MIXER_TOKEN_URL, client_id, MIXER_SCOPE_VERSION, + QT_TO_UTF8(login.GetCode()))) { + return nullptr; + } + + std::string error; + if (auth->GetChannelInfo()) { + return auth; + } + + return nullptr; +} + +static std::shared_ptr CreateMixerAuth() +{ + return std::make_shared(mixerDef); +} + +static void DeleteCookies() +{ + if (panel_cookies) { + panel_cookies->DeleteCookies("mixer.com", std::string()); + panel_cookies->DeleteCookies("microsoft.com", std::string()); + } +} + +void RegisterMixerAuth() +{ + OAuth::RegisterOAuth( + mixerDef, + CreateMixerAuth, + MixerAuth::Login, + DeleteCookies); +} diff --git a/UI/auth-mixer.hpp b/UI/auth-mixer.hpp new file mode 100644 index 0000000000000000000000000000000000000000..e87b507cb6a224bd6e4721c3ad3bd8b31902fd8b --- /dev/null +++ b/UI/auth-mixer.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "auth-oauth.hpp" + +class MixerChat; + +class MixerAuth : public OAuthStreamKey { + Q_OBJECT + + QSharedPointer chat; + QSharedPointer chatMenu; + bool uiLoaded = false; + + std::string name; + std::string id; + + virtual bool RetryLogin() override; + + virtual void SaveInternal() override; + virtual bool LoadInternal() override; + + bool GetChannelInfo(); + + virtual void LoadUI() override; + +public: + MixerAuth(const Def &d); + + static std::shared_ptr Login(QWidget *parent); +}; diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 2c5bdeb3472d4e024dcfd864d0b56831cc5426ec..e66a979e930561a8a172f65e1def62ca9d2641fa 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -102,6 +102,7 @@ Auth.LoadingChannel.Title="Loading channel information.." Auth.LoadingChannel.Text="Loading channel information for %1, please wait.." Auth.ChannelFailure.Title="Failed to load channel" Auth.ChannelFailure.Text="Failed to load channel information for %1\n\n%2: %3" +Auth.Chat="Chat" # copy filters Copy.Filters="Copy Filters" diff --git a/UI/ui-config.h.in b/UI/ui-config.h.in new file mode 100644 index 0000000000000000000000000000000000000000..58a10200834d807cca7f5214950ed5d6dff704d4 --- /dev/null +++ b/UI/ui-config.h.in @@ -0,0 +1,21 @@ +#pragma once + +#ifndef TRUE +#define TRUE 1 +#endif + +#ifndef ON +#define ON 1 +#endif + +#ifndef FALSE +#define FALSE 0 +#endif + +#ifndef OFF +#define OFF 0 +#endif + +#define MIXER_ENABLED @MIXER_ENABLED@ +#define MIXER_CLIENTID "@MIXER_CLIENTID@" +#define MIXER_HASH 0x@MIXER_HASH@ diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 76013997d5cedaad85ebf31f32b789ecdc0f71cd..b51d9c9ca66bf85d3c470b5f6913d1cff0b5bb4f 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -78,6 +78,8 @@ using namespace std; #include #endif +#include "ui-config.h" + struct QCef; struct QCefCookieManager; @@ -189,12 +191,18 @@ void assignDockToggle(QDockWidget *dock, QAction *action) handleMenuToggle); } +extern void RegisterMixerAuth(); + OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow (parent), ui (new Ui::OBSBasic) { setAttribute(Qt::WA_NativeWindow); +#if MIXER_ENABLED + RegisterMixerAuth(); +#endif + setAcceptDrops(true); api = InitializeAPIInterface(this);