auth-twitch.cpp 11 KB

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