Geoguessr Custom Emotes

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

  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.2.3
  5. // @author victheturtle#5159
  6. // @license MIT
  7. // @require https://gf.qytechs.cn/scripts/460322-geoguessr-styles-scan/code/Geoguessr%20Styles%20Scan.js?version=1151668
  8. // @match https://www.geoguessr.com/*
  9. // @icon https://www.geoguessr.com/_next/static/images/emote-gg-cf17a1f5d51d0ed53f01c65e941beb6d.png
  10. // @namespace https://gf.qytechs.cn/users/967692-victheturtle
  11. // ==/UserScript==
  12.  
  13.  
  14. // REPLACE WITH THE TAGS OF YOUR 6 FAVOURITE EMOTES
  15. // LIST OF AVAILABLE EMOTE TAGS AT https://gist.github.com/GreenEyedBear/7e5046589b0f020c1ec80629c582cca6
  16. const FAVOURITES = [
  17. "tf",
  18. "FatChamp",
  19. ":gg:",
  20. "FeelsBadMan",
  21. ":goat:",
  22. ":wave:",
  23. ]
  24.  
  25. /* Geoguessr defaults:
  26. const FAVOURITES = [
  27. ":confused:",
  28. ":cry:",
  29. ":gg:",
  30. ":happy:",
  31. ":mindblown:",
  32. ":wave:",
  33. ]
  34. */
  35.  
  36.  
  37. let geoguessrCustomEmotes = {};
  38.  
  39. const customEmotesInjectedClass = "custom-emotes-injected";
  40. const getAllNewMessages = () => document.querySelectorAll(`div[class*="chat-message_messageContent__"]:not([class*="${customEmotesInjectedClass}"])`);
  41.  
  42. let favouritesInjected = false;
  43. const getEmoteSelectorBox = () => document.querySelector(`div[class*="chat-input_optionSelector__"]`);
  44.  
  45. let accessToken = "";
  46. const originalSend = WebSocket.prototype.send;
  47. let messageSocket = null;
  48. WebSocket.prototype.send = function(...args) {
  49. try {
  50. const sent = JSON.parse(...args);
  51. if (sent.code == "Subscribe" && sent.topic.startsWith("chat:InGame:TextMessages:")) {
  52. accessToken = sent.accessToken;
  53. messageSocket = this;
  54. }
  55. } catch(e) {}
  56. return originalSend.call(this, ...args);
  57. };
  58.  
  59. function sendFavouriteEmote(txt) {
  60. if (messageSocket == null) return;
  61. messageSocket.send(JSON.stringify(
  62. {code: 'ChatMessage', topic: 'chat:InGame:TextMessages:'+getGameId(), payload: txt, accessToken: accessToken}
  63. ));
  64. }
  65.  
  66. const GGemotes = {
  67. ":confused:": "https://www.geoguessr.com/_next/static/images/emote-confused-e0cf85ababd0222d0a5afdd1e197643b.png",
  68. ":cry:": "https://www.geoguessr.com/_next/static/images/emote-cry-d6a31832e6fbb210bbc7f51a5a566b43.png",
  69. ":gg:": "https://www.geoguessr.com/_next/static/images/emote-gg-cf17a1f5d51d0ed53f01c65e941beb6d.png",
  70. ":happy:": "https://www.geoguessr.com/_next/static/images/emote-happy-072e991610e1235c10a134dac75b128c.png",
  71. ":mindblown:": "https://www.geoguessr.com/_next/static/images/emote-mindblown-d1f80fc9fd1cb031bbfb3de1240e03e5.png",
  72. ":wave:": "https://www.geoguessr.com/_next/static/images/emote-wave-da1dd3859051c109583d2f3cda5824f8.png",
  73. }
  74.  
  75. function addFavouriteEmotes(emoteSelectorBox) {
  76. emoteSelectorBox.innerHTML = "";
  77. let chatInput = document.querySelector(`input[class*="chat-input_textInput__"]`);
  78. Element.prototype.addTrustedEventListener = function () {
  79. let args = [...arguments]
  80. return this.addEventListener(...args);
  81. }
  82. chatInput.addTrustedEventListener('input',function(e) {
  83. if (!e.isTrusted) {
  84. this.value += e.data;
  85. this.defaultValue = this.value;
  86. }
  87. }, false);
  88. for (let i=0; i<6; i++) {
  89. const button = document.createElement("button");
  90. button.innerHTML = `<img src="${GGemotes[FAVOURITES[i]] || geoguessrCustomEmotes[FAVOURITES[i]]}"><span>${FAVOURITES[i]}</span>`;
  91. button.onclick = () => sendFavouriteEmote(FAVOURITES[i]);
  92. emoteSelectorBox.appendChild(button);
  93. };
  94. };
  95.  
  96. const emoteInjectionTemplate = (emoteSrc) => `</span>
  97. <span class="${cn("chat-message_emoteWrapper__")}"><img src="${emoteSrc}" class="${cn("chat-message_messageEmote__")}"></span>
  98. <span class="${cn("chat-message_messageText__")}">`;
  99.  
  100. async function fetchWithCors(url, method, body) {
  101. return await fetch(url, {
  102. "headers": {
  103. "accept": "*/*",
  104. "accept-language": "en-US,en;q=0.8",
  105. "content-type": "application/json",
  106. "sec-fetch-dest": "empty",
  107. "sec-fetch-mode": "cors",
  108. "sec-fetch-site": "same-site",
  109. "sec-gpc": "1",
  110. "x-client": "web"
  111. },
  112. "referrer": "https://www.geoguessr.com/",
  113. "referrerPolicy": "strict-origin-when-cross-origin",
  114. "body": (method == "GET") ? null : JSON.stringify(body),
  115. "method": method,
  116. "mode": "cors",
  117. "credentials": "include"
  118. });
  119. };
  120.  
  121. const getGameId = () => ((location.pathname.split("/")[2].length > 20) ? location.pathname.split("/")[2] : location.pathname.split("/")[3]);
  122. const getPartyId = async () => await fetchWithCors(getLobbyApi(getGameId()), "POST", {})
  123. .then(it => it.json()).then(it => it.partyId);
  124. const getPlayerId = async (nick) => await fetchWithCors(getLobbyApi(getGameId()), "POST", {})
  125. .then(it => it.json()).then(it => {
  126. let matches = it.players.filter(it => it.nick.toLowerCase() == nick.toLowerCase()).map(it => it.playerId).sort();
  127. return matches[matches.length-1];
  128. });
  129. const getLobbyApi = (gameId) => `https://game-server.geoguessr.com/api/lobby/${gameId}/join`;
  130. const getKickApi = (gameId) => `https://game-server.geoguessr.com/api/lobby/${gameId}/kick`;
  131. const getBanApi = (partyId) => `https://www.geoguessr.com/api/v4/parties/${partyId}/ban`;
  132. const getRoundNumberApi = (gameId) => `https://game-server.geoguessr.com/api/duels/${gameId}/`;
  133. const getRoundNumber = async () => await fetchWithCors(getRoundNumberApi(getGameId()), "GET")
  134. .then(it => it.json()).then(it => it.currentRoundNumber);
  135. const getGuessApi = (gameId) => `https://game-server.geoguessr.com/api/duels/${gameId}/guess`;
  136.  
  137. async function ban(nick) {
  138. const playerId = await getPlayerId(nick);
  139. const partyId = await getPartyId();
  140. fetchWithCors(getKickApi(getGameId()), "POST", {playerId: playerId}).catch(e => console.log(e));
  141. fetchWithCors(getBanApi(partyId), "POST", {userId: playerId, ban: true}).catch(e => console.log(e));
  142. };
  143.  
  144. async function unban(nick) {
  145. const playerId = await getPlayerId(nick);
  146. const partyId = await getPartyId();
  147. fetchWithCors(getBanApi(partyId), "POST", {userId: playerId, ban: false}).catch(e => console.log(e));
  148. };
  149.  
  150. async function openProfile(nick) {
  151. const playerId = await getPlayerId(nick);
  152. window.open("/user/"+playerId);
  153. };
  154.  
  155. async function guessEiffelTower() {
  156. const rn = await getRoundNumber();
  157. fetchWithCors(getGuessApi(getGameId()), "POST", {"lat": 48.85837, "lng": 2.29448, "roundNumber": rn}).catch(e => console.log(e));
  158. };
  159.  
  160. function handleCommand(type, args, isSelf) {
  161. try {
  162. console.log(type)
  163. console.log(args)
  164. if (type == "/ban") {
  165. if (args.length != 0 && isSelf) ban(args);
  166. } else if (type == "/unban") {
  167. if (args.length != 0 && isSelf) unban(args);
  168. } else if (type == "/mute") {
  169. if (args.length != 0 && isSelf) localStorage.setItem("CustomEmotesMuted"+args.toLowerCase(), "1");
  170. } else if (type == "/unmute") {
  171. if (args.length != 0 && isSelf) localStorage.setItem("CustomEmotesMuted"+args.toLowerCase(), "0");
  172. } else if (type == "/check") {
  173. if (args.length != 0 && isSelf) openProfile(args);
  174. } else if (type == "/eiffel") {
  175. if (location.pathname.includes("duel") && isSelf) guessEiffelTower();
  176. }
  177. } catch (e) { console.log(e); };
  178. };
  179.  
  180. function injectCustomEmotes(words) {
  181. for (let i=0; i<words.length; i+=2) {
  182. if (words[i] == "") continue;
  183. const lowercaseWord = words[i].toLowerCase();
  184. for (let emoteName in geoguessrCustomEmotes) {
  185. if (lowercaseWord == emoteName.toLowerCase() || lowercaseWord[0] == ":" && lowercaseWord == ":"+emoteName.toLowerCase()+":") {
  186. words[i] = emoteInjectionTemplate(geoguessrCustomEmotes[emoteName]);
  187. break;
  188. }
  189. }
  190. }
  191. return words.join("");
  192. }
  193.  
  194. function deleteEmptyTextTags() {
  195. for (let emptyTextTags of document.getElementsByClassName(cn("chat-message_messageText__"))) {
  196. if (emptyTextTags.innerHTML == "") emptyTextTags.remove();
  197. }
  198. }
  199.  
  200. let observer = new MutationObserver((mutations) => {
  201. const emoteSelectorBox = getEmoteSelectorBox();
  202. if (emoteSelectorBox == null) {
  203. favouritesInjected = false;
  204. } else if (!favouritesInjected && Object.keys(geoguessrCustomEmotes).length !== 0) {
  205. favouritesInjected = true;
  206. addFavouriteEmotes(emoteSelectorBox);
  207. };
  208. deleteEmptyTextTags();
  209. const newMessages = getAllNewMessages();
  210. if (newMessages.length == 0) return;
  211. for (let message of newMessages) {
  212. if (message.classList.contains(customEmotesInjectedClass)) continue;
  213. message.classList.add(customEmotesInjectedClass);
  214. const words = message.innerHTML.split(/((?:<|>|&lt;|&gt;|,| |\.)+)/g);
  215. const author = message.innerHTML.split(/(?:<|>)+/)[2];
  216. const messageContentStart = words.indexOf("><");
  217. const isSelf = message.parentNode.className.includes("isSelf");
  218. if (!isSelf && localStorage.getItem("CustomEmotesMuted"+author.toLowerCase()) == "1") {
  219. requireClassName('chat-message_messageText__').then(textStyle => {
  220. message.innerHTML = words.slice(0, messageContentStart+1).join("") + `span class="${textStyle}" style="color:silver">[Muted]</span` + words.slice(words.length-3).join("");
  221. });
  222. } else {
  223. if (words.length >= messageContentStart+10 && words[messageContentStart+5][0] == "/") {
  224. handleCommand(words[messageContentStart+5], words.slice(messageContentStart+7, words.length-4).join(""), isSelf)
  225. }
  226. scanStyles().then(() => {
  227. message.innerHTML = injectCustomEmotes(words);
  228. });
  229. }
  230. };
  231. deleteEmptyTextTags();
  232. });
  233.  
  234. async function fetchEmotesRepository() {
  235. const lastTimeFetched = localStorage.getItem("CustomEmotesLastFetched")*1
  236. 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
  237. return localStorage.getItem("CustomEmotesStored")
  238. } else {
  239. const emotesRepositoryContent = await fetch("https://api.github.com/gists/7e5046589b0f020c1ec80629c582cca6")
  240. .then(it => it.json())
  241. .then(it => it.files["GeoguessrCustomEmotesRepository.json"].content);
  242. localStorage.setItem("CustomEmotesStored", emotesRepositoryContent);
  243. localStorage.setItem("CustomEmotesLastFetched", Date.now());
  244. return emotesRepositoryContent;
  245. }
  246. }
  247.  
  248. (() => {
  249. fetchEmotesRepository().then(emotesRepositoryContent => {
  250. geoguessrCustomEmotes = JSON.parse(emotesRepositoryContent);
  251. observer.observe(document.body, { subtree: true, childList: true });
  252. }).catch(err => console.log(`Geoguessr Custom Emotes error at fetchEmotesRepository(): ${err}`));
  253. })();

QingJ © 2025

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