presence.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. const presence: Presence = new Presence({
  2. clientId: "800166344023867443",
  3. }),
  4. browsingTimestamp = Math.floor(Date.now() / 1000);
  5. const enum Assets {
  6. Logo = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/logo.png",
  7. Avatar = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/0.png",
  8. Kanji = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/1.png",
  9. Radical = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/2.png",
  10. Vocabulary = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/3.png",
  11. Lessons0 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/4.png",
  12. Lessons1 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/5.png",
  13. Lessons25 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/6.png",
  14. Lessons50 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/7.png",
  15. Lessons100 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/8.png",
  16. Lessons250 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/9.png",
  17. Lessons500 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/10.png",
  18. Reviews0 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/11.png",
  19. Reviews1 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/12.png",
  20. Reviews50 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/13.png",
  21. Reviews100 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/14.png",
  22. Reviews250 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/15.png",
  23. Reviews500 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/16.png",
  24. Reviews1000 = "https://cdn.rcd.gg/PreMiD/websites/W/WaniKani/assets/17.png",
  25. }
  26. function capitalize(string: string) {
  27. return string.charAt(0).toUpperCase() + string.slice(1);
  28. }
  29. function getTypeAsset(string: string) {
  30. switch (string.toLowerCase()) {
  31. case "kanji":
  32. return Assets.Kanji;
  33. case "radical":
  34. return Assets.Radical;
  35. case "vocabulary":
  36. return Assets.Vocabulary;
  37. default:
  38. return null;
  39. }
  40. }
  41. function getReviewPresence(): PresenceData {
  42. const data: PresenceData = {},
  43. characterType = [
  44. ...document.querySelector<HTMLDivElement>(
  45. '[data-quiz-header-base-class="character-header"]'
  46. ).classList,
  47. ]
  48. .find(cls => cls.startsWith("character-header--"))
  49. .split("--")[1],
  50. completeCount = document.querySelector<HTMLDivElement>(
  51. '[data-quiz-statistics-target="completeCount"]'
  52. );
  53. data.state = `${
  54. document.querySelector<HTMLDivElement>(
  55. '[data-quiz-header-target="characters"]'
  56. ).textContent
  57. } | ${capitalize(characterType)} ${capitalize(
  58. document.querySelector<HTMLDivElement>(
  59. '[data-quiz-input-target="questionTypeContainer"]'
  60. ).dataset.questionType
  61. )}`;
  62. if (completeCount) {
  63. data.smallImageText = `${completeCount.textContent} complete, ${
  64. document.querySelector<HTMLDivElement>(
  65. '[data-quiz-statistics-target="remainingCount"]'
  66. ).textContent
  67. } remaining. (${
  68. document.querySelector<HTMLDivElement>(
  69. '[data-quiz-statistics-target="percentCorrect"]'
  70. ).textContent
  71. }%)`;
  72. }
  73. data.smallImageKey = getTypeAsset(characterType);
  74. return data;
  75. }
  76. function getLessonPresence(): PresenceData {
  77. const presenceData: PresenceData = {},
  78. totalStats = document.querySelectorAll<HTMLDivElement>(
  79. '[data-controller="subject-count-statistics"] [data-subject-count-statistics-target="count"]'
  80. );
  81. presenceData.state = `${
  82. document.querySelector<HTMLDivElement>(
  83. '[data-quiz-header-target="characters"]'
  84. ).textContent
  85. } - ${
  86. document.querySelector<HTMLDivElement>(
  87. '[data-quiz-header-target="meaning"]'
  88. ).textContent
  89. }`;
  90. presenceData.smallImageKey = getTypeAsset(
  91. [
  92. ...document.querySelector<HTMLDivElement>(
  93. '[data-quiz-header-base-class="character-header"]'
  94. ).classList,
  95. ]
  96. .find(cls => cls.startsWith("character-header--"))
  97. .split("--")[1]
  98. );
  99. if (totalStats.length === 3)
  100. presenceData.smallImageText = `${totalStats[0].textContent} radicals | ${totalStats[1].textContent} kanji | ${totalStats[2].textContent} vocab`;
  101. return presenceData;
  102. }
  103. presence.on("UpdateData", () => {
  104. const { hostname, pathname } = document.location,
  105. presenceData: PresenceData = {
  106. largeImageKey: Assets.Logo,
  107. startTimestamp: browsingTimestamp,
  108. };
  109. switch (hostname) {
  110. case "wanikani.com":
  111. case "www.wanikani.com": {
  112. switch (pathname) {
  113. case "/":
  114. case "/dashboard":
  115. case "/login": {
  116. const buttons = document.querySelector(
  117. ".lessons-and-reviews"
  118. ).children;
  119. if (buttons.length === 2) {
  120. const lessons =
  121. +buttons[0].querySelector<HTMLSpanElement>("[class*=__count]")
  122. .textContent,
  123. reviews =
  124. +buttons[1].querySelector<HTMLSpanElement>("[class*=__count]")
  125. .textContent;
  126. presenceData.details = "Viewing Dashboard";
  127. presenceData.state = `${lessons} lessons | ${reviews} reviews`;
  128. presenceData.smallImageText =
  129. document.querySelector<HTMLAnchorElement>(
  130. ".user-summary__attribute > a"
  131. ).textContent;
  132. if (lessons > reviews) {
  133. if (lessons < 25) presenceData.smallImageKey = Assets.Lessons1;
  134. else if (lessons < 50)
  135. presenceData.smallImageKey = Assets.Lessons25;
  136. else if (lessons < 100)
  137. presenceData.smallImageKey = Assets.Lessons50;
  138. else if (lessons < 250)
  139. presenceData.smallImageKey = Assets.Lessons100;
  140. else if (lessons < 500)
  141. presenceData.smallImageKey = Assets.Lessons250;
  142. else presenceData.smallImageKey = Assets.Lessons500;
  143. } else if (reviews < 1)
  144. presenceData.smallImageKey = Assets.Reviews0;
  145. else if (reviews < 50) presenceData.smallImageKey = Assets.Reviews1;
  146. else if (reviews < 100)
  147. presenceData.smallImageKey = Assets.Reviews50;
  148. else if (reviews < 250)
  149. presenceData.smallImageKey = Assets.Reviews100;
  150. else if (reviews < 500)
  151. presenceData.smallImageKey = Assets.Reviews250;
  152. else if (reviews < 1000)
  153. presenceData.smallImageKey = Assets.Reviews500;
  154. else presenceData.smallImageKey = Assets.Reviews1000;
  155. } else {
  156. presenceData.details = "Browsing";
  157. presenceData.state = "Viewing Home Page";
  158. }
  159. break;
  160. }
  161. case "/subject-lessons/picker": {
  162. presenceData.details = "Choosing Lessons";
  163. break;
  164. }
  165. case "/subjects/extra_study": {
  166. presenceData.details = `Doing ${
  167. document.querySelector<HTMLDivElement>(
  168. ".character-header__menu-title"
  169. ).textContent
  170. }`;
  171. Object.assign(presenceData, getReviewPresence());
  172. break;
  173. }
  174. case pathname.match(/^\/recent-mistakes\/.*?\/quiz$/)?.input: {
  175. presenceData.details = "Doing Extra Study: Recent Mistakes";
  176. Object.assign(presenceData, getReviewPresence());
  177. break;
  178. }
  179. case pathname.match(/^\/recent-mistakes\/.*?\/subjects\/\d+\/lesson$/)
  180. ?.input: {
  181. presenceData.details = "Doing Extra Study: Recent Mistakes Lessons";
  182. Object.assign(presenceData, getLessonPresence());
  183. break;
  184. }
  185. case "/subjects/review": {
  186. presenceData.details = "Doing Reviews";
  187. Object.assign(presenceData, getReviewPresence());
  188. break;
  189. }
  190. case pathname.match(/^\/subject-lessons\/[-\d/]+\/quiz$/)?.input: {
  191. presenceData.details = "Practicing Lessons";
  192. Object.assign(presenceData, getReviewPresence());
  193. break;
  194. }
  195. case pathname.match(/^\/subject-lessons\/[-\d/]+/)?.input: {
  196. presenceData.details = "Learning Lessons";
  197. Object.assign(presenceData, getLessonPresence());
  198. break;
  199. }
  200. case pathname.match(/^\/(radicals|kanji|vocabulary)\/.+$/)?.input: {
  201. const [, type] = pathname.split("/");
  202. let textDescription =
  203. document.querySelector<HTMLElement>(
  204. ".mnemonic-content"
  205. ).textContent;
  206. if (textDescription.length >= 50)
  207. textDescription = `${textDescription.substring(0, 50)}...`;
  208. presenceData.details = `Browsing ${capitalize(type)}`;
  209. presenceData.state = `${
  210. document.querySelector<HTMLSpanElement>(
  211. `.${type.replace(/s$/, "")}-icon`
  212. ).textContent
  213. } | ${
  214. document.querySelector<HTMLSpanElement>(
  215. `.${type.replace(/s$/, "")}-icon`
  216. ).parentNode.childNodes[4].textContent
  217. }`;
  218. presenceData.smallImageText = textDescription;
  219. presenceData.smallImageKey = getTypeAsset(type.replace(/s$/, ""));
  220. break;
  221. }
  222. case pathname.match(/^\/users\/.+$/)?.input: {
  223. presenceData.details = "Viewing User Profile";
  224. presenceData.state =
  225. document.querySelector<HTMLSpanElement>(".username").textContent;
  226. presenceData.smallImageKey = Assets.Avatar;
  227. break;
  228. }
  229. default: {
  230. presenceData.details = "Browsing";
  231. presenceData.state = `Viewing ${document.title
  232. .split(" / ")
  233. .slice(1)
  234. .join(" / ")}`;
  235. }
  236. }
  237. break;
  238. }
  239. case "knowledge.wanikani.com": {
  240. presenceData.details = "Browsing WaniKani Knowledge";
  241. presenceData.state = document.title.split(" | ")[0];
  242. break;
  243. }
  244. case "community.wanikani.com": {
  245. if (/^\/u\/.+$/.test(pathname)) {
  246. presenceData.details = "Viewing User Profile";
  247. presenceData.smallImageKey = Assets.Avatar;
  248. presenceData.state =
  249. document.querySelector<HTMLHeadingElement>(".username").textContent;
  250. break;
  251. }
  252. presenceData.details = "Browsing WaniKani Community";
  253. presenceData.state = document.title.split(" - ")[0];
  254. break;
  255. }
  256. }
  257. presence.setActivity(presenceData);
  258. });