auth-mixer.cpp 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. #include "auth-mixer.hpp"
  2. #include <QPushButton>
  3. #include <QHBoxLayout>
  4. #include <QVBoxLayout>
  5. #include <qt-wrappers.hpp>
  6. #include <obs-app.hpp>
  7. #include "window-basic-main.hpp"
  8. #include "remote-text.hpp"
  9. #include <json11.hpp>
  10. #include <ctime>
  11. #include "ui-config.h"
  12. #include "obf.h"
  13. using namespace json11;
  14. #include <browser-panel.hpp>
  15. extern QCef *cef;
  16. extern QCefCookieManager *panel_cookies;
  17. /* ------------------------------------------------------------------------- */
  18. #define MIXER_AUTH_URL \
  19. "https://obsproject.com/app-auth/mixer?action=redirect"
  20. #define MIXER_TOKEN_URL \
  21. "https://obsproject.com/app-auth/mixer-token"
  22. #define MIXER_SCOPE_VERSION 1
  23. static Auth::Def mixerDef = {
  24. "Mixer",
  25. Auth::Type::OAuth_StreamKey
  26. };
  27. /* ------------------------------------------------------------------------- */
  28. MixerAuth::MixerAuth(const Def &d)
  29. : OAuthStreamKey(d)
  30. {
  31. }
  32. bool MixerAuth::GetChannelInfo(bool allow_retry)
  33. try {
  34. std::string client_id = MIXER_CLIENTID;
  35. deobfuscate_str(&client_id[0], MIXER_HASH);
  36. if (!GetToken(MIXER_TOKEN_URL, client_id, MIXER_SCOPE_VERSION))
  37. return false;
  38. if (token.empty())
  39. return false;
  40. if (!key_.empty())
  41. return true;
  42. std::string auth;
  43. auth += "Authorization: Bearer ";
  44. auth += token;
  45. std::vector<std::string> headers;
  46. headers.push_back(std::string("Client-ID: ") + client_id);
  47. headers.push_back(std::move(auth));
  48. std::string output;
  49. std::string error;
  50. Json json;
  51. bool success;
  52. if (id.empty()) {
  53. auto func = [&] () {
  54. success = GetRemoteFile(
  55. "https://mixer.com/api/v1/users/current",
  56. output,
  57. error,
  58. nullptr,
  59. "application/json",
  60. nullptr,
  61. headers,
  62. nullptr,
  63. 5);
  64. };
  65. ExecThreadedWithoutBlocking(
  66. func,
  67. QTStr("Auth.LoadingChannel.Title"),
  68. QTStr("Auth.LoadingChannel.Text").arg(service()));
  69. if (!success || output.empty())
  70. throw ErrorInfo("Failed to get user info from remote",
  71. error);
  72. Json json = Json::parse(output, error);
  73. if (!error.empty())
  74. throw ErrorInfo("Failed to parse json", error);
  75. error = json["error"].string_value();
  76. if (!error.empty())
  77. throw ErrorInfo(error,
  78. json["error_description"].string_value());
  79. id = std::to_string(json["channel"]["id"].int_value());
  80. name = json["channel"]["token"].string_value();
  81. }
  82. /* ------------------ */
  83. std::string url;
  84. url += "https://mixer.com/api/v1/channels/";
  85. url += id;
  86. url += "/details";
  87. output.clear();
  88. auto func = [&] () {
  89. success = GetRemoteFile(
  90. url.c_str(),
  91. output,
  92. error,
  93. nullptr,
  94. "application/json",
  95. nullptr,
  96. headers,
  97. nullptr,
  98. 5);
  99. };
  100. ExecThreadedWithoutBlocking(
  101. func,
  102. QTStr("Auth.LoadingChannel.Title"),
  103. QTStr("Auth.LoadingChannel.Text").arg(service()));
  104. if (!success || output.empty())
  105. throw ErrorInfo("Failed to get stream key from remote", error);
  106. json = Json::parse(output, error);
  107. if (!error.empty())
  108. throw ErrorInfo("Failed to parse json", error);
  109. error = json["error"].string_value();
  110. if (!error.empty())
  111. throw ErrorInfo(error, json["error_description"].string_value());
  112. std::string key_suffix = json["streamKey"].string_value();
  113. /* Mixer does not throw an error; instead it gives you the channel data
  114. * json without the data you normally have privileges for, which means
  115. * it'll be an empty stream key usually. So treat empty stream key as
  116. * an error. */
  117. if (key_suffix.empty()) {
  118. if (allow_retry && RetryLogin()) {
  119. return GetChannelInfo(false);
  120. }
  121. throw ErrorInfo("Auth Failure", "Could not get channel data");
  122. }
  123. key_ = id + "-" + key_suffix;
  124. return true;
  125. } catch (ErrorInfo info) {
  126. QString title = QTStr("Auth.ChannelFailure.Title");
  127. QString text = QTStr("Auth.ChannelFailure.Text")
  128. .arg(service(), info.message.c_str(), info.error.c_str());
  129. QMessageBox::warning(OBSBasic::Get(), title, text);
  130. blog(LOG_WARNING, "%s: %s: %s",
  131. __FUNCTION__,
  132. info.message.c_str(),
  133. info.error.c_str());
  134. return false;
  135. }
  136. void MixerAuth::SaveInternal()
  137. {
  138. OBSBasic *main = OBSBasic::Get();
  139. config_set_string(main->Config(), service(), "Name", name.c_str());
  140. config_set_string(main->Config(), service(), "Id", id.c_str());
  141. if (uiLoaded) {
  142. config_set_string(main->Config(), service(), "DockState",
  143. main->saveState().toBase64().constData());
  144. }
  145. OAuthStreamKey::SaveInternal();
  146. }
  147. static inline std::string get_config_str(
  148. OBSBasic *main,
  149. const char *section,
  150. const char *name)
  151. {
  152. const char *val = config_get_string(main->Config(), section, name);
  153. return val ? val : "";
  154. }
  155. bool MixerAuth::LoadInternal()
  156. {
  157. if (!cef)
  158. return false;
  159. OBSBasic *main = OBSBasic::Get();
  160. name = get_config_str(main, service(), "Name");
  161. id = get_config_str(main, service(), "Id");
  162. firstLoad = false;
  163. return OAuthStreamKey::LoadInternal();
  164. }
  165. class MixerChat : public QDockWidget {
  166. public:
  167. inline MixerChat() : QDockWidget() {}
  168. QScopedPointer<QCefWidget> widget;
  169. };
  170. void MixerAuth::LoadUI()
  171. {
  172. if (!cef)
  173. return;
  174. if (uiLoaded)
  175. return;
  176. if (!GetChannelInfo())
  177. return;
  178. OBSBasic::InitBrowserPanelSafeBlock();
  179. OBSBasic *main = OBSBasic::Get();
  180. std::string url;
  181. url += "https://mixer.com/embed/chat/";
  182. url += id;
  183. QSize size = main->frameSize();
  184. QPoint pos = main->pos();
  185. chat.reset(new MixerChat());
  186. chat->setObjectName("mixerChat");
  187. chat->resize(300, 600);
  188. chat->setMinimumSize(200, 300);
  189. chat->setWindowTitle(QTStr("Auth.Chat"));
  190. chat->setAllowedAreas(Qt::AllDockWidgetAreas);
  191. QCefWidget *browser = cef->create_widget(nullptr, url, panel_cookies);
  192. chat->setWidget(browser);
  193. main->addDockWidget(Qt::RightDockWidgetArea, chat.data());
  194. chatMenu.reset(main->AddDockWidget(chat.data()));
  195. /* ----------------------------------- */
  196. chat->setFloating(true);
  197. chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50);
  198. if (firstLoad) {
  199. chat->setVisible(true);
  200. } else {
  201. const char *dockStateStr = config_get_string(main->Config(),
  202. service(), "DockState");
  203. QByteArray dockState =
  204. QByteArray::fromBase64(QByteArray(dockStateStr));
  205. main->restoreState(dockState);
  206. }
  207. uiLoaded = true;
  208. }
  209. bool MixerAuth::RetryLogin()
  210. {
  211. if (!cef)
  212. return false;
  213. OAuthLogin login(OBSBasic::Get(), MIXER_AUTH_URL, false);
  214. cef->add_popup_whitelist_url("about:blank", &login);
  215. if (login.exec() == QDialog::Rejected) {
  216. return false;
  217. }
  218. std::shared_ptr<MixerAuth> auth = std::make_shared<MixerAuth>(mixerDef);
  219. std::string client_id = MIXER_CLIENTID;
  220. deobfuscate_str(&client_id[0], MIXER_HASH);
  221. return GetToken(MIXER_TOKEN_URL, client_id, MIXER_SCOPE_VERSION,
  222. QT_TO_UTF8(login.GetCode()), true);
  223. }
  224. std::shared_ptr<Auth> MixerAuth::Login(QWidget *parent)
  225. {
  226. if (!cef) {
  227. return nullptr;
  228. }
  229. OAuthLogin login(parent, MIXER_AUTH_URL, false);
  230. cef->add_popup_whitelist_url("about:blank", &login);
  231. if (login.exec() == QDialog::Rejected) {
  232. return nullptr;
  233. }
  234. std::shared_ptr<MixerAuth> auth = std::make_shared<MixerAuth>(mixerDef);
  235. std::string client_id = MIXER_CLIENTID;
  236. deobfuscate_str(&client_id[0], MIXER_HASH);
  237. if (!auth->GetToken(MIXER_TOKEN_URL, client_id, MIXER_SCOPE_VERSION,
  238. QT_TO_UTF8(login.GetCode()))) {
  239. return nullptr;
  240. }
  241. std::string error;
  242. if (auth->GetChannelInfo(false)) {
  243. return auth;
  244. }
  245. return nullptr;
  246. }
  247. static std::shared_ptr<Auth> CreateMixerAuth()
  248. {
  249. return std::make_shared<MixerAuth>(mixerDef);
  250. }
  251. static void DeleteCookies()
  252. {
  253. if (panel_cookies) {
  254. panel_cookies->DeleteCookies("mixer.com", std::string());
  255. panel_cookies->DeleteCookies("microsoft.com", std::string());
  256. }
  257. }
  258. void RegisterMixerAuth()
  259. {
  260. OAuth::RegisterOAuth(
  261. mixerDef,
  262. CreateMixerAuth,
  263. MixerAuth::Login,
  264. DeleteCookies);
  265. }