User Flags

Adds a flag to (almost) all usernames on Geoguessr

  1. // ==UserScript==
  2. // @name User Flags
  3. // @description Adds a flag to (almost) all usernames on Geoguessr
  4. // @version 1.0.4
  5. // @license MIT
  6. // @author zorby#1431
  7. // @namespace https://greatest.deepsurf.us/en/users/986787-zorby
  8. // @match https://www.geoguessr.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
  10. // ==/UserScript==
  11.  
  12. //=====================================================================================\\
  13. // change these values however you like (make sure to hit ctrl+s afterwards) :^) \\
  14. //=====================================================================================\\
  15.  
  16.  
  17.  
  18. const BIG_FLAG_TYPE = "flagpedia"
  19. // ^^^^^^^^^ set this to either 'flagpedia' or 'geoguessr'
  20. // flagpedia flags are more detailed but harder to identify at low resolution (subjective)
  21. const SMALL_FLAG_TYPE = "geoguessr"
  22. // ^^^^^^^^^ set this to either 'flagpedia' or 'geoguessr'
  23. // geoguessr flags are less accurate but easier to identify at low resolution (subjective)
  24.  
  25.  
  26. const USE_IFOPE_IF_FLAG_IS_MISSING = true
  27. // ^^^^^ set this to 'true' if you want to use the IFOPE for users who don't have their coutry set
  28. // https://www.flagofplanetearth.com/
  29.  
  30.  
  31. const FLAGS_IN_FRIENDS_TAB = true
  32. // ^^^^^ set this to 'true' if you want to display flags in the friends tab
  33. // not recommended if you have many friends :^)
  34. const FLAGS_IN_LEADERBOARD = true
  35. // ^^^^ set this to 'false' if you don't want to display flags in leaderboards
  36. const FLAGS_IN_PROFLE_PAGE = true
  37. // ^^^^ set this to 'false' if you don't want to display flags in profile pages
  38. const FLAGS_IN_MATCHMAKING = true
  39. // ^^^^ set this to 'false' if you don't want to display flags in matchmaking lobbies
  40. const FLAGS_IN_INGAME_PAGE = true
  41. // ^^^^ set this to 'false' if you don't want to display flags ingame
  42.  
  43.  
  44.  
  45. //=====================================================================================\\
  46. // don't edit anything after this point unless you know what you're doing please :^) \\
  47. //=====================================================================================\\
  48.  
  49.  
  50.  
  51. /// CONSTANTS ///
  52. const FLAGPEDIA_FLAG_ENDPOINT = "https://flagcdn.com"
  53. const GEOGUESSR_FLAG_ENDPOINT = "https://www.geoguessr.com/static/flags"
  54. const GEOGUESSR_USER_ENDPOINT = "https://geoguessr.com/api/v3/users"
  55.  
  56. const SCRIPT_PREFIX = "uf__"
  57. const PROFIlE_FLAG_ID = SCRIPT_PREFIX + "profileFlag"
  58. const USER_FLAG_CLASS = SCRIPT_PREFIX + "userFlag"
  59.  
  60. const OBSERVER_CONFIG = {
  61. characterDataOldValue: false,
  62. subtree: true,
  63. childList: true,
  64. characterData: false
  65. }
  66.  
  67. const ERROR_MESSAGE = (wrong) => `looks like you made a typo! :O\n\nmake sure to set the big flag type to either 'flagpedia' or 'geoguessr'\n(you typed '${wrong}')\n\nyours truly, user flags script :^)`
  68.  
  69.  
  70. /// MAIN ///
  71. let bigFlagEndpoint, smallFlagEndpoint
  72.  
  73. if (BIG_FLAG_TYPE == "flagpedia") {
  74. bigFlagEndpoint = FLAGPEDIA_FLAG_ENDPOINT
  75. } else if (BIG_FLAG_TYPE == "geoguessr") {
  76. bigFlagEndpoint = GEOGUESSR_FLAG_ENDPOINT
  77. } else {
  78. alert(ERROR_MESSAGE(BIG_FLAG_TYPE))
  79. throw new Error()
  80. }
  81.  
  82. if (SMALL_FLAG_TYPE == "flagpedia") {
  83. smallFlagEndpoint = FLAGPEDIA_FLAG_ENDPOINT
  84. } else if (SMALL_FLAG_TYPE == "geoguessr") {
  85. smallFlagEndpoint = GEOGUESSR_FLAG_ENDPOINT
  86. } else {
  87. alert(ERROR_MESSAGE(SMALL_FLAG_TYPE))
  88. throw new Error()
  89. }
  90.  
  91. function bigFlag() {
  92. return `<img
  93. id="${PROFIlE_FLAG_ID}"
  94. style="margin-left: 0.4rem; vertical-align: middle; border-radius: 0.125rem; display: none;"
  95. width=30
  96. onerror="this.style.display = 'none'"
  97. >`
  98. }
  99.  
  100. function smallFlag() {
  101. return `<img
  102. class="${USER_FLAG_CLASS}"
  103. style="margin-left: 0.1rem; margin-right: 0.1rem; vertical-align: middle; border-radius: 0.08rem; display: none;"
  104. width=13
  105. onerror="this.style.display = 'none'"
  106. >`
  107. }
  108.  
  109. function pathMatches(path) {
  110. return location.pathname.match(new RegExp(`^/(?:[^/]+/)?${path}$`))
  111. }
  112.  
  113. function getFlagSvg(flagType, countryCode) {
  114. if (countryCode == null && USE_IFOPE_IF_FLAG_IS_MISSING) {
  115. return "https://upload.wikimedia.org/wikipedia/commons/e/ef/International_Flag_of_Planet_Earth.svg"
  116. }
  117.  
  118. const endpoint = flagType == "big" ? bigFlagEndpoint : smallFlagEndpoint
  119. const svgName = endpoint == GEOGUESSR_FLAG_ENDPOINT ? countryCode.toUpperCase() : countryCode
  120.  
  121. return `${endpoint}/${svgName}.svg`
  122. }
  123.  
  124. async function fillFlag(flagImage, flagType, userId) {
  125. const userData = await getUserData(userId)
  126. const countryCode = userData.countryCode
  127.  
  128. flagImage.setAttribute("src", getFlagSvg(flagType, countryCode))
  129. flagImage.style.display = "block"
  130. }
  131.  
  132. function retrieveIdFromLink(link) {
  133. if (link.endsWith("/me/profile")) {
  134. const data = document.querySelector("#__NEXT_DATA__").text
  135. const json = JSON.parse(data)
  136. return json.props.middlewareResults[1].account.user.userId
  137. }
  138. return link.split("/").at(-1)
  139. }
  140.  
  141. function isOtherProfile() {
  142. return pathMatches("user/.+")
  143. }
  144.  
  145. function isOwnProfile() {
  146. return pathMatches("me/profile")
  147. }
  148.  
  149. function isProfile() {
  150. return isOwnProfile() || isOtherProfile()
  151. }
  152.  
  153. function isBattleRoyale() {
  154. return pathMatches("battle-royale/.+")
  155. }
  156.  
  157. function isDuels() {
  158. return pathMatches("duels/.+")
  159. }
  160.  
  161. async function getUserData(id) {
  162. const response = await fetch(`${GEOGUESSR_USER_ENDPOINT}/${id}`)
  163. const json = await response.json()
  164.  
  165. return json
  166. }
  167.  
  168. function addFlagToUsername(link, position) {
  169. position = position == null ? "beforeend" : position
  170. const dry = !link.querySelector(`.${USER_FLAG_CLASS}`)
  171. if (dry) {
  172. const destination = link.querySelector(".user-nick_nickWrapper__8Tnk4")
  173. destination.insertAdjacentHTML(position, smallFlag())
  174. const flagImage = destination.querySelector(`.${USER_FLAG_CLASS}`)
  175.  
  176. if (destination.childElementCount > 2) {
  177. destination.insertBefore(flagImage, flagImage.previousElementSibling)
  178. }
  179.  
  180. fillFlag(flagImage, "small", retrieveIdFromLink(link.href))
  181. }
  182. return dry
  183. }
  184.  
  185. function addFlagToIngameUsername(link) {
  186. if (!link.querySelector(`.${USER_FLAG_CLASS}`)) {
  187. const destination = link.querySelector("span")
  188. destination.style.display = "flex"
  189. destination.innerHTML += "&nbsp;"
  190. destination.insertAdjacentHTML("beforeend", smallFlag())
  191. const flagImage = destination.lastChild
  192.  
  193. if (destination.childElementCount > 2) {
  194. destination.insertBefore(flagImage, flagImage.previousElementSibling)
  195. }
  196.  
  197. fillFlag(flagImage, "small", retrieveIdFromLink(link.href))
  198. }
  199. }
  200.  
  201. let inBattleRoyale = false
  202. let inDuels = false
  203. let lastOpenedMapHighscoreTab = 0
  204.  
  205. function onMutationsBr(mutations, observer) {
  206. if (FLAGS_IN_INGAME_PAGE) {
  207. // battle royale distance
  208. for (const link of document.querySelectorAll(".distance-player-list_name__fPSwC a")) {
  209. addFlagToIngameUsername(link)
  210. }
  211.  
  212. // battle royale countries
  213. for (const link of document.querySelectorAll(".countries-player-list_playerName__g4tnM a")) {
  214. addFlagToIngameUsername(link)
  215. }
  216. }
  217. }
  218.  
  219. function onMutationsDuels(mutations, observer) {
  220. if (FLAGS_IN_INGAME_PAGE) {
  221. const hud = document.querySelector(".hud_root__RY5pu")
  222.  
  223. const firstPlayerLink = hud.firstChild?.querySelector(".health-bar_player__9j0Vu a")
  224. if (firstPlayerLink) {
  225. addFlagToUsername(firstPlayerLink)
  226. }
  227.  
  228. const secondPlayerLink = hud.lastChild?.querySelector(".health-bar_player__9j0Vu a")
  229. if (secondPlayerLink) {
  230. if(addFlagToUsername(secondPlayerLink, "afterbegin")) {
  231. const name = secondPlayerLink.querySelector(".user-nick_nick__y4VIt")
  232. name.innerHTML = "&nbsp;" + name.innerHTML
  233. }
  234. }
  235.  
  236. if (document.querySelector(".round-score_container__s6qNg")) {
  237. const leftLink = document.querySelector(".round-score_healthLeft__TT8Kk .health-bar_player__9j0Vu a")
  238. addFlagToUsername(leftLink)
  239. const rightLink = document.querySelector(".round-score_healthRight__qgBbv .health-bar_player__9j0Vu a")
  240. if(addFlagToUsername(rightLink, "afterbegin")) {
  241. const name = rightLink.querySelector(".user-nick_nick__y4VIt")
  242. name.innerHTML = "&nbsp;" + name.innerHTML
  243. }
  244. }
  245. }
  246. }
  247.  
  248. function onMutationsStandard(mutations, observer) {
  249. if (isBattleRoyale() && document.querySelector(".game_hud__h3YxY ul") && !inBattleRoyale) {
  250. console.log("Switching to br mode!")
  251. inBattleRoyale = true
  252.  
  253. const brObserver = new MutationObserver(onMutationsBr)
  254. brObserver.observe(document.querySelector(".game_hud__h3YxY ul"), OBSERVER_CONFIG)
  255. } else if (isDuels() && document.querySelector(".game_hud__fhdo5") && !inDuels) {
  256. console.log("Switching to duels mode!")
  257. inDuels = true
  258.  
  259. const duelsObserver = new MutationObserver(onMutationsDuels)
  260. duelsObserver.observe(document.querySelector(".game_hud__fhdo5"), OBSERVER_CONFIG)
  261. } else if (inBattleRoyale && !document.querySelector(".game_hud__h3YxY ul")) {
  262. console.log("Switching to standard mode!")
  263. inBattleRoyale = false
  264. } else if (inDuels && !document.querySelector(".game_hud__fhdo5")) {
  265. console.log("Switching to standard mode!")
  266. inDuels = false
  267. }
  268.  
  269. if (inBattleRoyale || inDuels) {
  270. return
  271. }
  272.  
  273. if (FLAGS_IN_PROFLE_PAGE && isProfile()) {
  274. // user profile
  275. if (!document.querySelector(`#${PROFIlE_FLAG_ID}`)) {
  276. const destination = document.querySelector(".headline_heading__c6HiU.headline_sizeLarge__DqYNn .user-nick_root__DUfvc")
  277. destination.insertAdjacentHTML("beforeend", bigFlag())
  278. const flagImage = destination.lastChild
  279.  
  280. fillFlag(flagImage, "big", retrieveIdFromLink(location.href))
  281. }
  282. }
  283.  
  284. if (FLAGS_IN_FRIENDS_TAB) {
  285. // friends tab
  286. for (const link of document.querySelectorAll(".chat-friend_name__6GRE_ a")) {
  287. addFlagToUsername(link)
  288. }
  289. }
  290.  
  291. if (FLAGS_IN_LEADERBOARD) {
  292. // generic leaderboard
  293. for (const link of document.querySelectorAll(".leaderboard_columnContent__yA6b_.leaderboard_alignStart__KChAa a")) {
  294. addFlagToUsername(link)
  295. }
  296.  
  297. // map highscore leaderboard
  298. let tabSwitch = document.querySelector(".map-highscore_switchContainer__wCDRH div")
  299. if (tabSwitch) {
  300. const openedMapHighscoreTab = +tabSwitch.firstChild.firstChild.classList.contains("switch_hide__OuYfZ")
  301.  
  302. if (openedMapHighscoreTab != lastOpenedMapHighscoreTab) {
  303. lastOpenedMapHighscoreTab = openedMapHighscoreTab
  304. for (const link of document.querySelectorAll(".map-highscore_userWrapper__aHpCF a")) {
  305. const flag = link.querySelector(`.${USER_FLAG_CLASS}`)
  306. if (flag) {
  307. flag.remove()
  308. }
  309. }
  310. }
  311. }
  312.  
  313. for (const link of document.querySelectorAll(".map-highscore_userWrapper__aHpCF a")) {
  314. addFlagToUsername(link)
  315. }
  316. }
  317.  
  318. if (FLAGS_IN_MATCHMAKING) {
  319. // battle royale matchmaking
  320. for (const link of document.querySelectorAll(".player-card_userLink__HhoDo")) {
  321. addFlagToUsername(link)
  322. }
  323. }
  324. }
  325.  
  326. const observer = new MutationObserver(onMutationsStandard)
  327.  
  328. observer.observe(document.body, OBSERVER_CONFIG)