presence.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. const presence = new Presence({
  2. clientId: "612793327510749210",
  3. });
  4. function findRanking(rankingSelector: Element) {
  5. return (
  6. rankingSelector.textContent === strings.stString ||
  7. rankingSelector.textContent === strings.ndString ||
  8. rankingSelector.textContent === strings.rdString ||
  9. rankingSelector.textContent.replace(/\d+/, "{0}") === strings.topX
  10. );
  11. }
  12. async function getStrings() {
  13. return presence.getStrings(
  14. {
  15. buttonJoinGame: "kahoot.buttonJoinGame",
  16. joiningGame: "kahoot.joiningGame",
  17. waiting: "kahoot.waiting",
  18. gameStarting: "kahoot.gameStarting",
  19. playing: "kahoot.playing",
  20. questionLoading: "kahoot.questionLoading",
  21. incorrectAnswer: "kahoot.incorrectAnswer",
  22. correctAnswer: "kahoot.correctAnswer",
  23. resultsQuestion: "kahoot.resultsQuestion",
  24. slideViewing: "kahoot.slideViewing",
  25. gameOver: "kahoot.gameOver",
  26. gameCreate: "kahoot.gameCreate",
  27. loadingPage: "kahoot.loadingPage",
  28. firstPlace: "kahoot.firstPlace",
  29. points: "kahoot.points",
  30. questionsCorrect: "kahoot.questionsCorrect",
  31. slideShowing: "kahoot.slideShowing",
  32. questionShowing: "kahoot.questionShowing",
  33. stString: "kahoot.stString",
  34. ndString: "kahoot.ndString",
  35. rdString: "kahoot.rdString",
  36. topX: "kahoot.topX",
  37. onPodium: "kahoot.onPodium",
  38. of: "kahoot.of",
  39. questionNumber: "kahoot.questionNumber",
  40. feedback: "kahoot.feedback",
  41. waitingAnswer: "kahoot.waitingAnswer",
  42. drumRoll: "kahoot.drumRoll",
  43. position: "kahoot.position",
  44. teamTalk: "kahoot.teamTalk",
  45. gameSummary: "kahoot.gameSummary",
  46. login: "kahoot.login",
  47. createHome: "kahoot.createHome",
  48. discover: "kahoot.discover",
  49. searchKahoots: "kahoot.searchKahoots",
  50. kahootDetails: "kahoot.kahootDetails",
  51. kahootProfile: "kahoot.kahootProfile",
  52. myKahoots: "kahoot.myKahoots",
  53. userReports: "kahoot.userReports",
  54. myCourses: "kahoot.myCourses",
  55. editingCourse: "kahoot.editingCourse",
  56. viewingCourse: "kahoot.viewingCourse",
  57. editingKahoot: "kahoot.editingKahoot",
  58. previewingKahoot: "kahoot.previewingKahoot",
  59. liveCourse: "kahoot.liveCourse",
  60. liveCourseActivity: "kahoot.liveCourseActivity",
  61. },
  62. await presence.getSetting<string>("lang").catch(() => "en")
  63. );
  64. }
  65. let strings: Awaited<ReturnType<typeof getStrings>>,
  66. oldLang: string = null,
  67. browsingTimestamp = Math.floor(Date.now() / 1000),
  68. // 0 - ready to be updated if needed
  69. // 1 - updated, ready to be reset to 0
  70. timestampUpdateState = 0,
  71. iframeKahootName: string;
  72. presence.on("UpdateData", async () => {
  73. const presenceData: PresenceData = {
  74. largeImageKey:
  75. "https://cdn.rcd.gg/PreMiD/websites/K/Kahoot/assets/logo.png",
  76. startTimestamp: browsingTimestamp,
  77. },
  78. [buttons, newLang] = await Promise.all([
  79. await presence.getSetting<boolean>("buttons"),
  80. await presence.getSetting<string>("lang").catch(() => "en"),
  81. ]);
  82. oldLang ??= newLang;
  83. strings ??= await getStrings();
  84. if (oldLang !== newLang) {
  85. oldLang = newLang;
  86. strings = await getStrings();
  87. }
  88. const { host, pathname } = document.location;
  89. switch (host) {
  90. case "kahoot.it": {
  91. if (
  92. pathname === "/" ||
  93. pathname.includes("/join") ||
  94. pathname === "/v2/"
  95. ) {
  96. // Home/Join screen
  97. presenceData.details = strings.joiningGame;
  98. } else if (pathname.includes("/instructions")) {
  99. // Waiting for game to start
  100. presenceData.details = strings.waiting;
  101. // Set start timestamp after joining game
  102. if (timestampUpdateState === 0) {
  103. browsingTimestamp = Math.floor(Date.now() / 1000);
  104. timestampUpdateState = 1;
  105. }
  106. } else if (pathname.includes("/start")) {
  107. // Game is starting
  108. presenceData.details = strings.gameStarting;
  109. // Allow timestamp to be reset upon a potential replay
  110. if (timestampUpdateState === 1) timestampUpdateState = 0;
  111. } else if (pathname.includes("/gameblock")) {
  112. // Playing/Answering a question
  113. const [currentQuestion, totalQuestions] = document
  114. .querySelector('[data-functional-selector="question-index-counter"]')
  115. .textContent.match(/\d+/g);
  116. presenceData.details = strings.playing;
  117. presenceData.state = `${strings.questionNumber.replace(
  118. "{0}",
  119. strings.of
  120. .replace("{0}", currentQuestion)
  121. .replace("{1}", totalQuestions)
  122. )} | ${strings.points.replace(
  123. "{0}",
  124. document.querySelector(
  125. '[data-functional-selector="bottom-bar-score"]'
  126. ).textContent
  127. )}`;
  128. } else if (pathname.includes("/getready")) {
  129. // Next question is loading
  130. const [currentQuestion, totalQuestions] = document
  131. .querySelector('[data-functional-selector="question-index-counter"]')
  132. .textContent.match(/\d+/g);
  133. presenceData.details = strings.questionLoading;
  134. presenceData.state = `${strings.questionNumber.replace(
  135. "{0}",
  136. strings.of
  137. .replace("{0}", currentQuestion)
  138. .replace("{1}", totalQuestions)
  139. )}`;
  140. } else if (pathname.includes("/teamtalk")) {
  141. // Team discussion time
  142. presenceData.details = strings.teamTalk;
  143. } else if (
  144. pathname.includes("/answer") &&
  145. !pathname.includes("/result")
  146. ) {
  147. // Waiting for question to end
  148. presenceData.details = strings.waitingAnswer;
  149. } else if (pathname.includes("/result")) {
  150. // Answer result screen
  151. const rankingSelector = document.querySelector(
  152. '[data-functional-selector="player-rank"]'
  153. );
  154. if (!rankingSelector) presenceData.details = strings.resultsQuestion;
  155. else {
  156. presenceData.details = strings.resultsQuestion;
  157. presenceData.state = `${
  158. document.querySelector(
  159. '[data-functional-selector="correct-answer"]'
  160. )
  161. ? strings.correctAnswer
  162. : strings.incorrectAnswer
  163. } | ${
  164. /\d+/.test(rankingSelector.textContent)
  165. ? strings.position.replace(
  166. "{0}",
  167. rankingSelector.textContent.match(/\d+/)[0]
  168. )
  169. : strings.onPodium
  170. } | ${strings.points.replace(
  171. "{0}",
  172. document.querySelector(
  173. '[data-functional-selector="bottom-bar-score"]'
  174. ).textContent
  175. )}`;
  176. }
  177. } else if (pathname.includes("/contentblock")) {
  178. // Viewing a slide with content
  179. presenceData.details = strings.slideViewing;
  180. } else if (pathname.includes("/ranking")) {
  181. // Viewing the final ranking
  182. const rankingSelector = document.querySelector(
  183. '[data-functional-selector="ranking-header"],[data-functional-selector="ranking-header-winners"]'
  184. );
  185. if (!rankingSelector) presenceData.details = strings.drumRoll;
  186. else {
  187. const score = document.querySelector(
  188. '[data-functional-selector="bottom-bar-score"]'
  189. ).textContent;
  190. presenceData.details = findRanking(rankingSelector)
  191. ? `${strings.gameOver} | ${
  192. rankingSelector.textContent
  193. } | ${strings.points.replace("{0}", score)}`
  194. : `${strings.gameOver} | ${strings.points.replace("{0}", score)}`;
  195. }
  196. } else if (pathname.includes("/feedback")) {
  197. // Providing feedback
  198. presenceData.details = strings.feedback;
  199. } else presenceData.details = strings.loadingPage;
  200. presence.setActivity(presenceData);
  201. break;
  202. }
  203. case "play.kahoot.it": {
  204. switch (pathname) {
  205. case "/v2/": {
  206. // Settings/game creation
  207. presenceData.details = strings.gameCreate;
  208. break;
  209. }
  210. case "/v2/lobby": {
  211. // Lobby screen
  212. presenceData.details = strings.waiting;
  213. // Set start timestamp after game has been created
  214. if (timestampUpdateState === 0) {
  215. browsingTimestamp = Math.floor(Date.now() / 1000);
  216. presenceData.startTimestamp = browsingTimestamp;
  217. timestampUpdateState = 1;
  218. }
  219. if (buttons) {
  220. const pin = document.querySelector(
  221. '[data-functional-selector="game-pin"]'
  222. )?.textContent;
  223. if (pin) {
  224. presenceData.buttons = [
  225. {
  226. label: `${strings.buttonJoinGame.replace("{0}", pin)}`,
  227. url: `https://kahoot.it/?pin=${pin}`,
  228. },
  229. ];
  230. }
  231. }
  232. break;
  233. }
  234. case "/v2/start": {
  235. // Game start
  236. presenceData.details = strings.gameStarting;
  237. // Allow timestamp to be reset upon a potential replay
  238. if (timestampUpdateState === 1) timestampUpdateState = 0;
  239. break;
  240. }
  241. case "/v2/gameover": {
  242. presenceData.details = `${strings.firstPlace.replace(
  243. "{0}",
  244. document.querySelector('[data-functional-selector="winner"]')
  245. .textContent
  246. )} | ${strings.points.replace(
  247. "{0}",
  248. document.querySelector(
  249. '[data-functional-selector="place-1"] > [data-functional-selector="total-score"]'
  250. ).textContent
  251. )}`;
  252. const correctCount = document.querySelector(
  253. '[data-functional-selector="correct-count-gold"]'
  254. );
  255. if (correctCount) {
  256. const [correct, total] = correctCount.textContent.match(/\d+/g);
  257. presenceData.state = strings.questionsCorrect.replace(
  258. "{0}",
  259. strings.of.replace("{0}", correct).replace("{1}", total)
  260. );
  261. } else
  262. presenceData.state = strings.questionsCorrect.replace("{0}", "0");
  263. break;
  264. }
  265. case "/v2/gameblock": {
  266. if (
  267. document.querySelector(
  268. '[data-functional-selector="content-block-page"]'
  269. )
  270. ) {
  271. // Showing a Slide with Content
  272. presenceData.details = strings.slideShowing;
  273. } else {
  274. // Question in progress
  275. const questionCounter = document.querySelector(
  276. '[data-functional-selector="bottom-bar-question-counter"]'
  277. );
  278. if (!questionCounter) {
  279. // Question is starting
  280. presenceData.details = strings.questionLoading;
  281. } else {
  282. // Question is in progress
  283. const [currentQuestion, totalQuestions] =
  284. questionCounter.textContent.split("/");
  285. presenceData.details = strings.questionShowing;
  286. presenceData.state = `${strings.questionNumber.replace(
  287. "{0}",
  288. `${strings.of
  289. .replace("{0}", currentQuestion)
  290. .replace("{1}", totalQuestions)}`
  291. )}`;
  292. }
  293. }
  294. break;
  295. }
  296. case "/v2/game-summary": {
  297. // Game summary
  298. presenceData.details = strings.gameSummary;
  299. break;
  300. }
  301. default:
  302. presenceData.details = strings.loadingPage;
  303. }
  304. presence.setActivity(presenceData);
  305. break;
  306. }
  307. case "create.kahoot.it": {
  308. switch (pathname) {
  309. case "/": {
  310. // Kahoot! Create home page
  311. presenceData.details = strings.createHome;
  312. presenceData.state = "create.kahoot.it";
  313. break;
  314. }
  315. case "/after/login":
  316. case "/auth/login": {
  317. // Login
  318. presenceData.details = strings.login;
  319. break;
  320. }
  321. case "/discover": {
  322. // Discover
  323. presenceData.details = strings.discover;
  324. break;
  325. }
  326. case "/search": {
  327. // Search
  328. presenceData.details = strings.searchKahoots;
  329. presenceData.state = document.querySelector<HTMLInputElement>(
  330. '[data-functional-selector="search-box__input-field"]'
  331. ).value;
  332. break;
  333. }
  334. default:
  335. if (pathname.startsWith("/details/")) {
  336. // Kahoot details
  337. presenceData.details = strings.kahootDetails;
  338. presenceData.state = document.querySelector(
  339. '[data-functional-selector="kahoot-detail__title"]'
  340. ).textContent;
  341. } else if (pathname.startsWith("/profiles/")) {
  342. // Kahoot profile
  343. presenceData.details = strings.kahootProfile;
  344. presenceData.state = (
  345. document.querySelector(
  346. '[data-functional-selector="verified-user-profile-information"] > div > div'
  347. ) ??
  348. document.querySelector(
  349. '[data-functional-selector="default-user-profile"] > div > div > div:nth-of-type(2) > div'
  350. )
  351. ).firstChild.textContent.replace(/\n/g, " ");
  352. } else if (pathname.startsWith("/my-library/kahoots/")) {
  353. // My Kahoots
  354. presenceData.details = strings.myKahoots;
  355. } else if (pathname === "/user-reports") {
  356. // Game Reports
  357. presenceData.details = strings.userReports;
  358. } else if (pathname === "/my-library/courses") {
  359. // My Courses
  360. presenceData.details = strings.myCourses;
  361. } else if (pathname.startsWith("/course/")) {
  362. if (pathname.endsWith("/edit")) {
  363. // Editing a course
  364. presenceData.details = strings.editingCourse;
  365. presenceData.state = document.querySelector<HTMLInputElement>(
  366. '[data-functional-selector="course-title-input__course_title_input"]'
  367. ).value;
  368. } else {
  369. // Viewing a course
  370. presenceData.details = strings.viewingCourse;
  371. presenceData.state = document.querySelector(
  372. '[data-functional-selector="course-details"] h2'
  373. ).textContent;
  374. }
  375. } else if (pathname.startsWith("/creator/")) {
  376. // Editing a Kahoot!
  377. presenceData.details = strings.editingKahoot;
  378. presenceData.state = document.querySelector(
  379. '[data-functional-selector="top-bar__kahoot-summary-button"] > span'
  380. ).textContent;
  381. } else if (pathname.startsWith("/preview/")) {
  382. // Previewing a Kahoot!
  383. presenceData.details = strings.previewingKahoot;
  384. presenceData.state = iframeKahootName;
  385. } else if (pathname.startsWith("/v2/live-course/")) {
  386. // Live course
  387. presenceData.details = strings.liveCourse;
  388. const preview = document.querySelector(
  389. '[data-functional-selector="mega-menu__session-title"]'
  390. );
  391. if (preview) presenceData.state = preview.textContent;
  392. else {
  393. const course = document.querySelector(
  394. '[data-functional-selector="live-course-menu__toggle-mega-menu"] > span:nth-of-type(2)'
  395. ),
  396. [number] = course.textContent.match(/^\d+/);
  397. presenceData.state = strings.liveCourseActivity
  398. .replace("{0}", number)
  399. .replace(
  400. "{1}",
  401. course.textContent.substring(number.length + 2)
  402. );
  403. }
  404. } else presenceData.details = strings.loadingPage;
  405. }
  406. presence.setActivity(presenceData);
  407. break;
  408. }
  409. }
  410. });
  411. presence.on("iFrameData", (data: string) => {
  412. iframeKahootName = data;
  413. });