presence.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. const presence = new Presence({
  2. clientId: "809093093600133165",
  3. }),
  4. browsingTimestamp = Math.floor(Date.now() / 1000);
  5. let cached: {
  6. videoURL: string;
  7. video: HTMLVideoElement;
  8. };
  9. async function getStrings() {
  10. return presence.getStrings(
  11. {
  12. playing: "general.playing",
  13. paused: "general.paused",
  14. live: "general.live",
  15. browse: "general.browsing",
  16. forYou: "tiktok.forYou",
  17. following: "tiktok.following",
  18. buttonViewProfile: "general.buttonViewProfile",
  19. viewProfile: "general.viewProfile",
  20. viewAProfile: "general.viewAProfile",
  21. viewTikTok: "tiktok.viewing",
  22. buttonViewTikTok: "tiktok.buttonViewTikTok",
  23. browseThrough: "tiktok.browseThrough",
  24. watchingLive: "general.watchingLive",
  25. readingADM: "general.readingADM",
  26. exploringWithTag: "tiktok.exploringWithTag",
  27. viewAPlaylist: "general.viewAPlaylist",
  28. buttonWatchStream: "general.buttonWatchStream",
  29. },
  30. await presence.getSetting<string>("lang").catch(() => "en")
  31. );
  32. }
  33. function getVideo(video: HTMLVideoElement | null) {
  34. const videoURL = video?.src;
  35. if (
  36. videoURL &&
  37. (!cached?.videoURL || !cached?.video || cached?.videoURL !== videoURL)
  38. ) {
  39. cached = {
  40. videoURL,
  41. video,
  42. };
  43. return;
  44. } else if (!video && cached.video) return cached.video;
  45. }
  46. let strings: Awaited<ReturnType<typeof getStrings>>,
  47. oldLang: string = null;
  48. presence.on("UpdateData", async () => {
  49. const presenceData: PresenceData = {
  50. type: ActivityType.Watching,
  51. largeImageKey:
  52. "https://cdn.rcd.gg/PreMiD/websites/T/TikTok/assets/logo.png",
  53. startTimestamp: browsingTimestamp,
  54. },
  55. [newLang, privacy, buttons, showProfileUsernames] = await Promise.all([
  56. presence.getSetting<string>("lang").catch(() => "en"),
  57. presence.getSetting<boolean>("privacy"),
  58. presence.getSetting<boolean>("buttons"),
  59. presence.getSetting<boolean>("show-profile-usernames"),
  60. ]),
  61. { pathname, hostname, href } = document.location,
  62. lang = document.querySelector("html")?.getAttribute("lang"),
  63. username =
  64. document.querySelector('[data-e2e="user-profile-nickname"]')
  65. ?.textContent ??
  66. document.querySelector('[data-e2e="user-subtitle"]')?.textContent ??
  67. document
  68. .querySelector('[data-e2e="browser-nickname"]')
  69. ?.querySelector('[class*="SpanNickName"]')?.textContent ??
  70. document.querySelector('[data-e2e="browser-nickname"]')?.firstElementChild
  71. ?.textContent, // Username
  72. userid =
  73. document.querySelector('[data-e2e="user-profile-uid"]')?.textContent ??
  74. document.querySelector('[data-e2e="user-title"]')?.textContent ??
  75. document.querySelector('[data-e2e="browse-username"]')?.textContent, //Userid (so @userid)
  76. description =
  77. document.querySelector('[data-e2e="user-profile-live-title"]')
  78. ?.textContent ??
  79. document.querySelector('[data-e2e="browse-video-desc"]')?.textContent; // Video/livestream description
  80. if (oldLang !== newLang || !strings) {
  81. oldLang = newLang;
  82. strings = await getStrings();
  83. }
  84. switch (true) {
  85. case pathname === "/following":
  86. case pathname.includes("foryou"):
  87. case pathname === "/":
  88. case pathname === `/${lang}`:
  89. case pathname === `/${lang}/`: {
  90. if (!privacy) delete presenceData.startTimestamp;
  91. const videos = Array.from(document.querySelectorAll("video")).find(
  92. video => !video.paused
  93. );
  94. let video: HTMLVideoElement;
  95. if (videos) {
  96. video = videos;
  97. getVideo(videos);
  98. } else video = getVideo(null);
  99. const baseEl = video?.closest(
  100. '[data-e2e="recommend-list-item-container"]'
  101. ),
  102. userId = baseEl?.querySelector('h3[data-e2e="video-author-uniqueid"]'),
  103. tiktokURL = `https://www.tiktok.com/@${userId?.textContent}/video/${
  104. video
  105. ?.closest('[class="tiktok-web-player no-controls"]')
  106. ?.getAttribute("id")
  107. ?.split("-")?.[2]
  108. }`,
  109. creatorURL = `https://${hostname}${
  110. userId?.parentElement?.getAttribute("href") ?? ""
  111. }/`,
  112. tiktokURLMatch = tiktokURL.match(
  113. /https:\/\/www[.]tiktok[.]com\/@.*\/video\/[0-9]{19}/
  114. ),
  115. creatorURLMatch = creatorURL.match(
  116. /http(s)?:\/\/(www[.])?tiktok\.com\/@([\w.]{0,23}\w)(?:\/\S*)?\//
  117. )?.[0],
  118. paused =
  119. video
  120. ?.closest('div[data-e2e="feed-video"]')
  121. ?.querySelector('[data-e2e="video-play"]')
  122. ?.getAttribute("aria-label")
  123. ?.toLowerCase() === "pause";
  124. presenceData.details = privacy
  125. ? strings.browseThrough
  126. : userId &&
  127. baseEl?.querySelector('[data-e2e="video-author-nickname"]')
  128. ?.textContent
  129. ? `${
  130. baseEl?.querySelector('[data-e2e="video-author-nickname"]')
  131. ?.textContent
  132. } (@${userId?.textContent})`
  133. : strings.browseThrough;
  134. presenceData.state = baseEl?.querySelector(
  135. '[data-e2e="video-desc"]'
  136. )?.textContent;
  137. if (tiktokURLMatch && creatorURLMatch) {
  138. presenceData.buttons = [
  139. { label: strings.buttonViewTikTok, url: tiktokURL },
  140. {
  141. label: strings.buttonViewProfile,
  142. url: creatorURL,
  143. },
  144. ];
  145. } else if (creatorURLMatch) {
  146. presenceData.buttons = [
  147. {
  148. label: strings.buttonViewProfile,
  149. url: creatorURL,
  150. },
  151. ];
  152. } else if (tiktokURLMatch) {
  153. presenceData.buttons = [
  154. { label: strings.buttonViewTikTok, url: tiktokURL },
  155. ];
  156. }
  157. if (!paused && video?.duration && video?.currentTime) {
  158. [presenceData.startTimestamp, presenceData.endTimestamp] =
  159. presence.getTimestampsfromMedia(video);
  160. }
  161. presenceData.smallImageKey = paused ? Assets.Pause : Assets.Play;
  162. presenceData.smallImageText = paused ? strings.paused : strings.playing;
  163. break;
  164. }
  165. case pathname.includes("/video/"): {
  166. if (!privacy) delete presenceData.startTimestamp;
  167. const vidEl = document.querySelector("video");
  168. let video: {
  169. paused: boolean;
  170. currentTime: number;
  171. duration: number;
  172. };
  173. if (!vidEl) {
  174. video = {
  175. paused:
  176. !!document.querySelector('[aria-label="Pause"]') ||
  177. !!document.querySelector("[class*='DivPlayIconContainer']"),
  178. currentTime: presence.timestampFromFormat(
  179. document
  180. .querySelector("[class*='DivSeekBarTimeContainer']")
  181. ?.textContent?.split("/")[0]
  182. ),
  183. duration: presence.timestampFromFormat(
  184. document
  185. .querySelector("[class*='DivSeekBarTimeContainer']")
  186. ?.textContent?.split("/")[1]
  187. ),
  188. };
  189. } else {
  190. video = {
  191. paused: vidEl?.paused,
  192. currentTime: vidEl.currentTime,
  193. duration: vidEl.duration,
  194. };
  195. }
  196. presenceData.details = privacy
  197. ? strings.browseThrough
  198. : `${username} (@${userid})`;
  199. presenceData.state = description;
  200. presenceData.smallImageKey = video.paused ? Assets.Pause : Assets.Play;
  201. presenceData.smallImageText = video.paused
  202. ? strings.paused
  203. : strings.playing;
  204. if (!video.paused) {
  205. [presenceData.startTimestamp, presenceData.endTimestamp] =
  206. presence.getTimestamps(video.currentTime, video.duration);
  207. }
  208. presenceData.buttons = [
  209. { label: strings.buttonViewTikTok, url: href },
  210. {
  211. label: strings.buttonViewProfile,
  212. url: `https://www.tiktok.com/@${userid}`,
  213. },
  214. ];
  215. break;
  216. }
  217. case pathname === "/live": {
  218. const videos = Array.from(document.querySelectorAll("video")).find(
  219. video => !video.paused
  220. );
  221. let video: HTMLVideoElement;
  222. if (videos) {
  223. video = videos;
  224. getVideo(videos);
  225. } else video = getVideo(null);
  226. presenceData.details = privacy
  227. ? strings.watchingLive
  228. : `${strings.watchingLive} - ${
  229. video?.parentElement?.querySelector('[class*="SpanNickName"]')
  230. ?.textContent
  231. }`;
  232. presenceData.state = video?.parentElement?.querySelectorAll(
  233. '[class="css-1g1mtx-DivDetailsLine eawfp3g1"]'
  234. )?.[1]?.textContent;
  235. presenceData.smallImageKey = video?.paused ? Assets.Pause : Assets.Live;
  236. presenceData.smallImageText = video?.paused
  237. ? strings.paused
  238. : strings.live;
  239. presenceData.buttons = [
  240. {
  241. label: strings.buttonWatchStream,
  242. url: `https://www.tiktok.com/${
  243. video?.parentElement?.querySelector('[class*="SpanNickName"]')
  244. ?.textContent
  245. }/live`,
  246. },
  247. {
  248. label: strings.buttonViewProfile,
  249. url: `https://www.tiktok.com/${
  250. video?.parentElement?.querySelector('[class*="SpanNickName"]')
  251. ?.textContent
  252. }`,
  253. },
  254. ];
  255. break;
  256. }
  257. case pathname.includes("/live"): {
  258. const video = document.querySelector("video");
  259. presenceData.details = privacy
  260. ? strings.watchingLive
  261. : `${strings.watchingLive} - ${username} (@${userid})`;
  262. presenceData.state = description;
  263. presenceData.smallImageKey = video?.paused ? Assets.Pause : Assets.Live;
  264. presenceData.smallImageText = video?.paused
  265. ? strings.paused
  266. : strings.live;
  267. presenceData.buttons = [
  268. { label: strings.buttonWatchStream, url: href },
  269. {
  270. label: strings.buttonViewProfile,
  271. url: href?.split("/live")?.[0],
  272. },
  273. ];
  274. break;
  275. }
  276. case pathname.includes("/@"): {
  277. const playlistMenu = document
  278. .querySelector('[class*="DivModalContainer eo04fh215"]')
  279. ?.querySelector('[class*="Title"]');
  280. presenceData.details =
  281. privacy || !showProfileUsernames
  282. ? strings.viewAProfile
  283. : `${strings.viewProfile} ${username} (@${userid})`;
  284. if (showProfileUsernames) {
  285. presenceData.buttons = [
  286. { label: strings.buttonViewProfile, url: href },
  287. ];
  288. }
  289. presenceData.state = playlistMenu
  290. ? `${strings.viewAPlaylist} - ${playlistMenu?.textContent}`
  291. : document.querySelector('p[aria-selected="true"]')?.textContent;
  292. break;
  293. }
  294. case pathname.includes("/explore"): {
  295. presenceData.details = privacy
  296. ? strings.browseThrough
  297. : strings.exploringWithTag;
  298. presenceData.state = document.querySelector(
  299. ".css-1hs87dt-ButtonCategoryItemContainer"
  300. )?.textContent;
  301. break;
  302. }
  303. case pathname.includes("/messages"): {
  304. presenceData.details = privacy
  305. ? strings.browseThrough
  306. : strings.readingADM;
  307. break;
  308. }
  309. }
  310. if ((!buttons || privacy) && presenceData.buttons)
  311. delete presenceData.buttons;
  312. if (privacy && presenceData.state) delete presenceData.state;
  313. if (privacy && presenceData.endTimestamp) delete presenceData.endTimestamp;
  314. if (privacy && presenceData.smallImageKey) delete presenceData.smallImageKey;
  315. presence.setActivity(presenceData);
  316. });