Geoguessr Custom Emotes

Allows you to use many custom emotes and some commands in the Geoguessr chat

当前为 2022-10-21 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Geoguessr Custom Emotes
  3. // @description Allows you to use many custom emotes and some commands in the Geoguessr chat
  4. // @version 2.1.0
  5. // @author victheturtle#5159
  6. // @license MIT
  7. // @match https://www.geoguessr.com/*
  8. // @icon https://www.geoguessr.com/_next/static/images/emote-gg-cf17a1f5d51d0ed53f01c65e941beb6d.png
  9. // @namespace https://gf.qytechs.cn/users/967692-victheturtle
  10. // ==/UserScript==
  11.  
  12. let geoguessrCustomEmotes = {};
  13.  
  14. const customEmotesInjectedClass = "custom-emotes-injected";
  15. const getAllNewMessages = () => document.querySelectorAll(`div[class*="chat-message_messageContent__"]:not([class*="${customEmotesInjectedClass}"])`);
  16.  
  17. const _cndic = {};
  18. const hrefset = new Set();
  19. async function scanStyles() {
  20. for (let node of document.querySelectorAll('head link[rel="preload"], head style[data-n-href*=".css"]')) {
  21. const href = node.href || location.origin+node.dataset.nHref;
  22. if (hrefset.has(href)) continue;
  23. hrefset.add(href);
  24. await fetch(href)
  25. .then(res => res.text())
  26. .then(stylesheet => {
  27. for (let className of stylesheet.split(".")) {
  28. const ind = className.indexOf("__");
  29. if (ind != -1) _cndic[className.substr(0, ind+2)] = className.substr(0, ind+7);
  30. };
  31. });
  32. };
  33. }
  34. const cn = (classNameStart) => _cndic[classNameStart]; // cn("status_section__") -> "status_section__8uP8o"
  35.  
  36. const emoteInjectionTemplate = (emoteSrc) => `</span>
  37. <span class="${cn("chat-message_emoteWrapper__")}"><img src="${emoteSrc}" class="${cn("chat-message_messageEmote__")}"></span>
  38. <span class="${cn("chat-message_messageText__")}">`;
  39.  
  40. async function fetchWithCors(url, method, body) {
  41. return await fetch(url, {
  42. "headers": {
  43. "accept": "*/*",
  44. "accept-language": "en-US,en;q=0.8",
  45. "content-type": "application/json",
  46. "sec-fetch-dest": "empty",
  47. "sec-fetch-mode": "cors",
  48. "sec-fetch-site": "same-site",
  49. "sec-gpc": "1",
  50. "x-client": "web"
  51. },
  52. "referrer": "https://www.geoguessr.com/",
  53. "referrerPolicy": "strict-origin-when-cross-origin",
  54. "body": (method == "GET") ? null : JSON.stringify(body),
  55. "method": method,
  56. "mode": "cors",
  57. "credentials": "include"
  58. });
  59. };
  60.  
  61. const getGameId = () => location.pathname.split("/")[2];
  62. const getPartyId = async () => await fetchWithCors(getLobbyApi(getGameId()), "POST", {})
  63. .then(it => it.json()).then(it => it.partyId);
  64. const getPlayerId = async (nick) => await fetchWithCors(getLobbyApi(getGameId()), "POST", {})
  65. .then(it => it.json()).then(it => it.players.filter(it => it.nick == nick)[0].playerId);
  66. const getLobbyApi = (gameId) => `https://game-server.geoguessr.com/api/lobby/${gameId}/join`;
  67. const getKickApi = (gameId) => `https://game-server.geoguessr.com/api/lobby/${gameId}/kick`;
  68. const getBanApi = (partyId) => `https://www.geoguessr.com/api/v4/parties/${partyId}/ban`;
  69. const getRoundNumberApi = (gameId) => `https://game-server.geoguessr.com/api/duels/${gameId}/`;
  70. const getRoundNumber = async () => await fetchWithCors(getRoundNumberApi(getGameId()), "GET")
  71. .then(it => it.json()).then(it => it.currentRoundNumber);
  72. const getGuessApi = (gameId) => `https://game-server.geoguessr.com/api/duels/${gameId}/guess`;
  73.  
  74. async function ban(nick) {
  75. const playerId = await getPlayerId(nick);
  76. const partyId = await getPartyId();
  77. fetchWithCors(getKickApi(getGameId()), "POST", {playerId: playerId}).catch(e => console.log(e));
  78. fetchWithCors(getBanApi(partyId), "POST", {userId: playerId, ban: true}).catch(e => console.log(e));
  79. };
  80.  
  81. async function unban(nick) {
  82. const playerId = await getPlayerId(nick);
  83. const partyId = await getPartyId();
  84. fetchWithCors(getBanApi(partyId), "POST", {userId: playerId, ban: false}).catch(e => console.log(e));
  85. };
  86.  
  87. async function openProfile(nick) {
  88. const playerId = await getPlayerId(nick);
  89. window.open("/user/"+playerId);
  90. };
  91.  
  92. async function guessEiffelTower() {
  93. const rn = await getRoundNumber();
  94. fetchWithCors(getGuessApi(getGameId()), "POST", {"lat": 48.85837, "lng": 2.29448, "roundNumber": rn}).catch(e => console.log(e));
  95. };
  96.  
  97. function handleCommand(type, args, isSelf) {
  98. try {
  99. if (type == "/ban") {
  100. if (args.length != 0 && isSelf) ban(args);
  101. } else if (type == "/unban") {
  102. if (args.length != 0 && isSelf) unban(args);
  103. } else if (type == "/mute") {
  104. if (args.length != 0 && isSelf) localStorage.setItem("CustomEmotesMuted"+args, "1");
  105. } else if (type == "/unmute") {
  106. if (args.length != 0 && isSelf) localStorage.setItem("CustomEmotesMuted"+args, "0");
  107. } else if (type == "/check") {
  108. if (args.length != 0 && isSelf) openProfile(args);
  109. } else if (type == "/eiffel") {
  110. if (location.pathname.includes("duel") && isSelf) guessEiffelTower();
  111. }
  112. } catch (e) { console.log(e); };
  113. };
  114.  
  115. let observer = new MutationObserver((mutations) => {
  116. const newMessages = getAllNewMessages();
  117. if (newMessages.length == 0) return;
  118. scanStyles().then(() => {
  119. for (let message of newMessages) {
  120. let words = message.innerHTML.split(/((?:<|>|&lt;|&gt;|,| |\.)+)/g);
  121. const author = words[6];
  122. const isSelf = message.parentNode.className.includes("isSelf");
  123. if (!isSelf && localStorage.getItem("CustomEmotesMuted"+author) == "1") {
  124. message.innerHTML = words.slice(0, 14).join("") + "[Muted]" + words.slice(words.length-4).join("");
  125. } else if (words.length >= 19 && words[14][0] == "/") {
  126. handleCommand(words[14], words.slice(16, words.length-4).join(""), isSelf)
  127. } else {
  128. for (let i=0; i<words.length; i+=2) {
  129. if (words[i] == "") continue;
  130. const lowercaseWord = words[i].toLowerCase();
  131. for (let emoteName in geoguessrCustomEmotes) {
  132. if (lowercaseWord == emoteName.toLowerCase()) {
  133. words[i] = emoteInjectionTemplate(geoguessrCustomEmotes[emoteName]);
  134. break;
  135. }
  136. }
  137. }
  138. message.innerHTML = words.join("");
  139. }
  140. message.classList.add(customEmotesInjectedClass);
  141. }})
  142. });
  143.  
  144. async function fetchEmotesRepository() {
  145. const lastTimeFetched = localStorage.getItem("CustomEmotesLastFetched")*1
  146. if (Date.now() - lastTimeFetched < 60*1000) { // Github API has a limit rate of 60 requests per hour so prevent more than 1 request per minute
  147. return localStorage.getItem("CustomEmotesStored")
  148. } else {
  149. const emotesRepositoryContent = await fetch("https://api.github.com/gists/7e5046589b0f020c1ec80629c582cca6")
  150. .then(it => it.json())
  151. .then(it => it.files["GeoguessrCustomEmotesRepository.json"].content);
  152. localStorage.setItem("CustomEmotesStored", emotesRepositoryContent);
  153. localStorage.setItem("CustomEmotesLastFetched", Date.now());
  154. return emotesRepositoryContent;
  155. }
  156. }
  157.  
  158. (() => {
  159. fetchEmotesRepository().then(emotesRepositoryContent => {
  160. geoguessrCustomEmotes = JSON.parse(emotesRepositoryContent);
  161. observer.observe(document.body, { subtree: true, childList: true });
  162. }).catch(err => console.log(`Geoguessr Custom Emotes error at fetchEmotesRepository(): ${err}`));
  163. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址