auth-twitch.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. #include "auth-twitch.hpp"
  2. #include <QPushButton>
  3. #include <QHBoxLayout>
  4. #include <QVBoxLayout>
  5. #include <qt-wrappers.hpp>
  6. #include <obs-app.hpp>
  7. #include "window-dock-browser.hpp"
  8. #include "window-basic-main.hpp"
  9. #include "remote-text.hpp"
  10. #include <json11.hpp>
  11. #include "ui-config.h"
  12. #include "obf.h"
  13. using namespace json11;
  14. /* ------------------------------------------------------------------------- */
  15. #define TWITCH_AUTH_URL "https://obsproject.com/app-auth/twitch?action=redirect"
  16. #define TWITCH_TOKEN_URL "https://obsproject.com/app-auth/twitch-token"
  17. #define ACCEPT_HEADER "Accept: application/vnd.twitchtv.v5+json"
  18. #define TWITCH_SCOPE_VERSION 1
  19. static Auth::Def twitchDef = {"Twitch", Auth::Type::OAuth_StreamKey};
  20. /* ------------------------------------------------------------------------- */
  21. TwitchAuth::TwitchAuth(const Def &d) : OAuthStreamKey(d)
  22. {
  23. if (!cef)
  24. return;
  25. cef->add_popup_whitelist_url(
  26. "https://twitch.tv/popout/frankerfacez/chat?ffz-settings",
  27. this);
  28. /* enables javascript-based popups. basically bttv popups */
  29. cef->add_popup_whitelist_url("about:blank#blocked", this);
  30. uiLoadTimer.setSingleShot(true);
  31. uiLoadTimer.setInterval(500);
  32. connect(&uiLoadTimer, &QTimer::timeout, this,
  33. &TwitchAuth::TryLoadSecondaryUIPanes);
  34. }
  35. bool TwitchAuth::GetChannelInfo()
  36. try {
  37. std::string client_id = TWITCH_CLIENTID;
  38. deobfuscate_str(&client_id[0], TWITCH_HASH);
  39. if (!GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION))
  40. return false;
  41. if (token.empty())
  42. return false;
  43. if (!key_.empty())
  44. return true;
  45. std::string auth;
  46. auth += "Authorization: OAuth ";
  47. auth += token;
  48. std::vector<std::string> headers;
  49. headers.push_back(std::string("Client-ID: ") + client_id);
  50. headers.push_back(ACCEPT_HEADER);
  51. headers.push_back(std::move(auth));
  52. std::string output;
  53. std::string error;
  54. long error_code = 0;
  55. bool success = false;
  56. auto func = [&]() {
  57. success = GetRemoteFile("https://api.twitch.tv/kraken/channel",
  58. output, error, &error_code,
  59. "application/json", nullptr, headers,
  60. nullptr, 5);
  61. };
  62. ExecThreadedWithoutBlocking(
  63. func, QTStr("Auth.LoadingChannel.Title"),
  64. QTStr("Auth.LoadingChannel.Text").arg(service()));
  65. if (error_code == 403) {
  66. OBSMessageBox::warning(OBSBasic::Get(),
  67. Str("TwitchAuth.TwoFactorFail.Title"),
  68. Str("TwitchAuth.TwoFactorFail.Text"),
  69. true);
  70. blog(LOG_WARNING, "%s: %s", __FUNCTION__,
  71. "Got 403 from Twitch, user probably does not "
  72. "have two-factor authentication enabled on "
  73. "their account");
  74. return false;
  75. }
  76. if (!success || output.empty())
  77. throw ErrorInfo("Failed to get text from remote", error);
  78. Json json = Json::parse(output, error);
  79. if (!error.empty())
  80. throw ErrorInfo("Failed to parse json", error);
  81. error = json["error"].string_value();
  82. if (!error.empty()) {
  83. if (error == "Unauthorized") {
  84. if (RetryLogin()) {
  85. return GetChannelInfo();
  86. }
  87. throw ErrorInfo(error, json["message"].string_value());
  88. }
  89. throw ErrorInfo(error,
  90. json["error_description"].string_value());
  91. }
  92. name = json["name"].string_value();
  93. key_ = json["stream_key"].string_value();
  94. return true;
  95. } catch (ErrorInfo info) {
  96. QString title = QTStr("Auth.ChannelFailure.Title");
  97. QString text = QTStr("Auth.ChannelFailure.Text")
  98. .arg(service(), info.message.c_str(),
  99. info.error.c_str());
  100. QMessageBox::warning(OBSBasic::Get(), title, text);
  101. blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(),
  102. info.error.c_str());
  103. return false;
  104. }
  105. void TwitchAuth::SaveInternal()
  106. {
  107. OBSBasic *main = OBSBasic::Get();
  108. config_set_string(main->Config(), service(), "Name", name.c_str());
  109. if (uiLoaded) {
  110. config_set_string(main->Config(), service(), "DockState",
  111. main->saveState().toBase64().constData());
  112. }
  113. OAuthStreamKey::SaveInternal();
  114. }
  115. static inline std::string get_config_str(OBSBasic *main, const char *section,
  116. const char *name)
  117. {
  118. const char *val = config_get_string(main->Config(), section, name);
  119. return val ? val : "";
  120. }
  121. bool TwitchAuth::LoadInternal()
  122. {
  123. if (!cef)
  124. return false;
  125. OBSBasic *main = OBSBasic::Get();
  126. name = get_config_str(main, service(), "Name");
  127. firstLoad = false;
  128. return OAuthStreamKey::LoadInternal();
  129. }
  130. static const char *ffz_script = "\
  131. var ffz = document.createElement('script');\
  132. ffz.setAttribute('src','https://cdn.frankerfacez.com/script/script.min.js');\
  133. document.head.appendChild(ffz);";
  134. static const char *bttv_script = "\
  135. localStorage.setItem('bttv_clickTwitchEmotes', true);\
  136. localStorage.setItem('bttv_darkenedMode', true);\
  137. localStorage.setItem('bttv_bttvGIFEmotes', true);\
  138. var bttv = document.createElement('script');\
  139. bttv.setAttribute('src','https://cdn.betterttv.net/betterttv.js');\
  140. document.head.appendChild(bttv);";
  141. static const char *referrer_script1 = "\
  142. Object.defineProperty(document, 'referrer', {get : function() { return '";
  143. static const char *referrer_script2 = "'; }});";
  144. void TwitchAuth::LoadUI()
  145. {
  146. if (!cef)
  147. return;
  148. if (uiLoaded)
  149. return;
  150. if (!GetChannelInfo())
  151. return;
  152. OBSBasic::InitBrowserPanelSafeBlock();
  153. OBSBasic *main = OBSBasic::Get();
  154. QCefWidget *browser;
  155. std::string url;
  156. std::string script;
  157. std::string moderation_tools_url;
  158. moderation_tools_url = "https://www.twitch.tv/";
  159. moderation_tools_url += name;
  160. moderation_tools_url += "/dashboard/settings/moderation?no-reload=true";
  161. /* ----------------------------------- */
  162. url = "https://www.twitch.tv/popout/";
  163. url += name;
  164. url += "/chat";
  165. QSize size = main->frameSize();
  166. QPoint pos = main->pos();
  167. chat.reset(new BrowserDock());
  168. chat->setObjectName("twitchChat");
  169. chat->resize(300, 600);
  170. chat->setMinimumSize(200, 300);
  171. chat->setWindowTitle(QTStr("Auth.Chat"));
  172. chat->setAllowedAreas(Qt::AllDockWidgetAreas);
  173. browser = cef->create_widget(nullptr, url, panel_cookies);
  174. chat->SetWidget(browser);
  175. cef->add_force_popup_url(moderation_tools_url, chat.data());
  176. script = "localStorage.setItem('twilight.theme', 1);";
  177. const int twAddonChoice =
  178. config_get_int(main->Config(), service(), "AddonChoice");
  179. if (twAddonChoice) {
  180. if (twAddonChoice & 0x1)
  181. script += bttv_script;
  182. if (twAddonChoice & 0x2)
  183. script += ffz_script;
  184. }
  185. browser->setStartupScript(script);
  186. main->addDockWidget(Qt::RightDockWidgetArea, chat.data());
  187. chatMenu.reset(main->AddDockWidget(chat.data()));
  188. /* ----------------------------------- */
  189. chat->setFloating(true);
  190. chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50);
  191. if (firstLoad) {
  192. chat->setVisible(true);
  193. } else {
  194. const char *dockStateStr = config_get_string(
  195. main->Config(), service(), "DockState");
  196. QByteArray dockState =
  197. QByteArray::fromBase64(QByteArray(dockStateStr));
  198. main->restoreState(dockState);
  199. }
  200. TryLoadSecondaryUIPanes();
  201. uiLoaded = true;
  202. }
  203. void TwitchAuth::LoadSecondaryUIPanes()
  204. {
  205. OBSBasic *main = OBSBasic::Get();
  206. QCefWidget *browser;
  207. std::string url;
  208. std::string script;
  209. QSize size = main->frameSize();
  210. QPoint pos = main->pos();
  211. script = "localStorage.setItem('twilight.theme', 1);";
  212. script += referrer_script1;
  213. script += "https://www.twitch.tv/";
  214. script += name;
  215. script += "/dashboard/live";
  216. script += referrer_script2;
  217. const int twAddonChoice =
  218. config_get_int(main->Config(), service(), "AddonChoice");
  219. if (twAddonChoice) {
  220. if (twAddonChoice & 0x1)
  221. script += bttv_script;
  222. if (twAddonChoice & 0x2)
  223. script += ffz_script;
  224. }
  225. /* ----------------------------------- */
  226. url = "https://dashboard.twitch.tv/popout/u/";
  227. url += name;
  228. url += "/stream-manager/edit-stream-info";
  229. info.reset(new BrowserDock());
  230. info->setObjectName("twitchInfo");
  231. info->resize(300, 650);
  232. info->setMinimumSize(200, 300);
  233. info->setWindowTitle(QTStr("Auth.StreamInfo"));
  234. info->setAllowedAreas(Qt::AllDockWidgetAreas);
  235. browser = cef->create_widget(nullptr, url, panel_cookies);
  236. info->SetWidget(browser);
  237. browser->setStartupScript(script);
  238. main->addDockWidget(Qt::RightDockWidgetArea, info.data());
  239. infoMenu.reset(main->AddDockWidget(info.data()));
  240. /* ----------------------------------- */
  241. url = "https://www.twitch.tv/popout/";
  242. url += name;
  243. url += "/dashboard/live/stats";
  244. stat.reset(new BrowserDock());
  245. stat->setObjectName("twitchStats");
  246. stat->resize(200, 250);
  247. stat->setMinimumSize(200, 150);
  248. stat->setWindowTitle(QTStr("TwitchAuth.Stats"));
  249. stat->setAllowedAreas(Qt::AllDockWidgetAreas);
  250. browser = cef->create_widget(nullptr, url, panel_cookies);
  251. stat->SetWidget(browser);
  252. browser->setStartupScript(script);
  253. main->addDockWidget(Qt::RightDockWidgetArea, stat.data());
  254. statMenu.reset(main->AddDockWidget(stat.data()));
  255. /* ----------------------------------- */
  256. url = "https://dashboard.twitch.tv/popout/u/";
  257. url += name;
  258. url += "/stream-manager/activity-feed";
  259. feed.reset(new BrowserDock());
  260. feed->setObjectName("twitchFeed");
  261. feed->resize(300, 650);
  262. feed->setMinimumSize(200, 300);
  263. feed->setWindowTitle(QTStr("TwitchAuth.Feed"));
  264. feed->setAllowedAreas(Qt::AllDockWidgetAreas);
  265. browser = cef->create_widget(nullptr, url, panel_cookies);
  266. feed->SetWidget(browser);
  267. browser->setStartupScript(script);
  268. main->addDockWidget(Qt::RightDockWidgetArea, feed.data());
  269. feedMenu.reset(main->AddDockWidget(feed.data()));
  270. /* ----------------------------------- */
  271. info->setFloating(true);
  272. stat->setFloating(true);
  273. feed->setFloating(true);
  274. QSize statSize = stat->frameSize();
  275. info->move(pos.x() + 50, pos.y() + 50);
  276. stat->move(pos.x() + size.width() / 2 - statSize.width() / 2,
  277. pos.y() + size.height() / 2 - statSize.height() / 2);
  278. feed->move(pos.x() + 100, pos.y() + 100);
  279. if (firstLoad) {
  280. info->setVisible(true);
  281. stat->setVisible(false);
  282. feed->setVisible(false);
  283. } else {
  284. uint32_t lastVersion = config_get_int(App()->GlobalConfig(),
  285. "General", "LastVersion");
  286. if (lastVersion <= MAKE_SEMANTIC_VERSION(23, 0, 2)) {
  287. feed->setVisible(false);
  288. }
  289. const char *dockStateStr = config_get_string(
  290. main->Config(), service(), "DockState");
  291. QByteArray dockState =
  292. QByteArray::fromBase64(QByteArray(dockStateStr));
  293. main->restoreState(dockState);
  294. }
  295. }
  296. /* Twitch.tv has an OAuth for itself. If we try to load multiple panel pages
  297. * at once before it's OAuth'ed itself, they will all try to perform the auth
  298. * process at the same time, get their own request codes, and only the last
  299. * code will be valid -- so one or more panels are guaranteed to fail.
  300. *
  301. * To solve this, we want to load just one panel first (the chat), and then all
  302. * subsequent panels should only be loaded once we know that Twitch has auth'ed
  303. * itself (if the cookie "auth-token" exists for twitch.tv).
  304. *
  305. * This is annoying to deal with. */
  306. void TwitchAuth::TryLoadSecondaryUIPanes()
  307. {
  308. QPointer<TwitchAuth> this_ = this;
  309. auto cb = [this_](bool found) {
  310. if (!this_) {
  311. return;
  312. }
  313. if (!found) {
  314. QMetaObject::invokeMethod(&this_->uiLoadTimer, "start");
  315. } else {
  316. QMetaObject::invokeMethod(this_,
  317. "LoadSecondaryUIPanes");
  318. }
  319. };
  320. panel_cookies->CheckForCookie("https://www.twitch.tv", "auth-token",
  321. cb);
  322. }
  323. bool TwitchAuth::RetryLogin()
  324. {
  325. OAuthLogin login(OBSBasic::Get(), TWITCH_AUTH_URL, false);
  326. if (login.exec() == QDialog::Rejected) {
  327. return false;
  328. }
  329. std::shared_ptr<TwitchAuth> auth =
  330. std::make_shared<TwitchAuth>(twitchDef);
  331. std::string client_id = TWITCH_CLIENTID;
  332. deobfuscate_str(&client_id[0], TWITCH_HASH);
  333. return GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION,
  334. QT_TO_UTF8(login.GetCode()), true);
  335. }
  336. std::shared_ptr<Auth> TwitchAuth::Login(QWidget *parent)
  337. {
  338. OAuthLogin login(parent, TWITCH_AUTH_URL, false);
  339. if (login.exec() == QDialog::Rejected) {
  340. return nullptr;
  341. }
  342. std::shared_ptr<TwitchAuth> auth =
  343. std::make_shared<TwitchAuth>(twitchDef);
  344. std::string client_id = TWITCH_CLIENTID;
  345. deobfuscate_str(&client_id[0], TWITCH_HASH);
  346. if (!auth->GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION,
  347. QT_TO_UTF8(login.GetCode()))) {
  348. return nullptr;
  349. }
  350. if (auth->GetChannelInfo()) {
  351. return auth;
  352. }
  353. return nullptr;
  354. }
  355. static std::shared_ptr<Auth> CreateTwitchAuth()
  356. {
  357. return std::make_shared<TwitchAuth>(twitchDef);
  358. }
  359. static void DeleteCookies()
  360. {
  361. if (panel_cookies)
  362. panel_cookies->DeleteCookies("twitch.tv", std::string());
  363. }
  364. void RegisterTwitchAuth()
  365. {
  366. OAuth::RegisterOAuth(twitchDef, CreateTwitchAuth, TwitchAuth::Login,
  367. DeleteCookies);
  368. }