auth-twitch.cpp 13 KB

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