Browse Source

UI: Add Mixer integration

jp9000 6 years ago
parent
commit
4710c04204
6 changed files with 394 additions and 0 deletions
  1. 24 0
      UI/CMakeLists.txt
  2. 310 0
      UI/auth-mixer.cpp
  3. 30 0
      UI/auth-mixer.hpp
  4. 1 0
      UI/data/locale/en-US.ini
  5. 21 0
      UI/ui-config.h.in
  6. 8 0
      UI/window-basic-main.cpp

+ 24 - 0
UI/CMakeLists.txt

@@ -19,6 +19,20 @@ project(obs)
 
 
 set(DISABLE_UPDATE_MODULE TRUE CACHE BOOL "Disables building the update module")
 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_INCLUDE_CURRENT_DIR TRUE)
 set(CMAKE_AUTOMOC TRUE)
 set(CMAKE_AUTOMOC TRUE)
 
 
@@ -39,6 +53,7 @@ endif()
 
 
 
 
 include_directories(${FFMPEG_INCLUDE_DIRS})
 include_directories(${FFMPEG_INCLUDE_DIRS})
+include_directories(${CMAKE_CURRENT_BINARY_DIR})
 include_directories(SYSTEM "obs-frontend-api")
 include_directories(SYSTEM "obs-frontend-api")
 include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs")
 include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs")
 include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/deps/libff")
 include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/deps/libff")
@@ -114,6 +129,15 @@ if(BROWSER_AVAILABLE_INTERNAL)
 		obf.h
 		obf.h
 		auth-oauth.hpp
 		auth-oauth.hpp
 		)
 		)
+
+	if(MIXER_ENABLED)
+		list(APPEND obs_PLATFORM_SOURCES
+			auth-mixer.cpp
+			)
+		list(APPEND obs_PLATFORM_HEADERS
+			auth-mixer.hpp
+			)
+	endif()
 endif()
 endif()
 
 
 set(obs_libffutil_SOURCES
 set(obs_libffutil_SOURCES

+ 310 - 0
UI/auth-mixer.cpp

@@ -0,0 +1,310 @@
+#include "auth-mixer.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 <json11.hpp>
+
+#include <ctime>
+
+#include "ui-config.h"
+#include "obf.h"
+
+using namespace json11;
+
+#include <browser-panel.hpp>
+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<std::string> 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<QCefWidget> 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<MixerAuth> auth = std::make_shared<MixerAuth>(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<Auth> 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<MixerAuth> auth = std::make_shared<MixerAuth>(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<Auth> CreateMixerAuth()
+{
+	return std::make_shared<MixerAuth>(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);
+}

+ 30 - 0
UI/auth-mixer.hpp

@@ -0,0 +1,30 @@
+#pragma once
+
+#include "auth-oauth.hpp"
+
+class MixerChat;
+
+class MixerAuth : public OAuthStreamKey {
+	Q_OBJECT
+
+	QSharedPointer<MixerChat> chat;
+	QSharedPointer<QAction> 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<Auth> Login(QWidget *parent);
+};

+ 1 - 0
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.LoadingChannel.Text="Loading channel information for %1, please wait.."
 Auth.ChannelFailure.Title="Failed to load channel"
 Auth.ChannelFailure.Title="Failed to load channel"
 Auth.ChannelFailure.Text="Failed to load channel information for %1\n\n%2: %3"
 Auth.ChannelFailure.Text="Failed to load channel information for %1\n\n%2: %3"
+Auth.Chat="Chat"
 
 
 # copy filters
 # copy filters
 Copy.Filters="Copy Filters"
 Copy.Filters="Copy Filters"

+ 21 - 0
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@

+ 8 - 0
UI/window-basic-main.cpp

@@ -78,6 +78,8 @@ using namespace std;
 #include <browser-panel.hpp>
 #include <browser-panel.hpp>
 #endif
 #endif
 
 
+#include "ui-config.h"
+
 struct QCef;
 struct QCef;
 struct QCefCookieManager;
 struct QCefCookieManager;
 
 
@@ -189,12 +191,18 @@ void assignDockToggle(QDockWidget *dock, QAction *action)
 			handleMenuToggle);
 			handleMenuToggle);
 }
 }
 
 
+extern void RegisterMixerAuth();
+
 OBSBasic::OBSBasic(QWidget *parent)
 OBSBasic::OBSBasic(QWidget *parent)
 	: OBSMainWindow  (parent),
 	: OBSMainWindow  (parent),
 	  ui             (new Ui::OBSBasic)
 	  ui             (new Ui::OBSBasic)
 {
 {
 	setAttribute(Qt::WA_NativeWindow);
 	setAttribute(Qt::WA_NativeWindow);
 
 
+#if MIXER_ENABLED
+	RegisterMixerAuth();
+#endif
+
 	setAcceptDrops(true);
 	setAcceptDrops(true);
 
 
 	api = InitializeAPIInterface(this);
 	api = InitializeAPIInterface(this);