auth-twitch.cpp 12 KB

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