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