UNPKG

biketag

Version:

The Javascript client API for BikeTag Games

1,205 lines 244 kB
(function(global, factory) { typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("@aws-sdk/client-s3"), require("tinycache"), require("@aws-sdk/s3-request-presigner"), require("axios"), require("imgur"), require("@sanity/client"), require("axios-cache-interceptor"), require("dequal")) : typeof define === "function" && define.amd ? define(["exports", "@aws-sdk/client-s3", "tinycache", "@aws-sdk/s3-request-presigner", "axios", "imgur", "@sanity/client", "axios-cache-interceptor", "dequal"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.BikeTagClient = {}, global.S3Client, global.TinyCache, global.S3RequestPresigner, global.axios, global.ImgurClient, global.SanityClient, global.axiosCacheInterceptor, global.dequal)); })(this, function(exports2, clientS3, TinyCache, s3RequestPresigner, axios, imgur, client, axiosCacheInterceptor, dequal) { "use strict"; var AvailableApis = /* @__PURE__ */ ((AvailableApis2) => { AvailableApis2["biketag"] = "biketag"; AvailableApis2["aws"] = "aws"; AvailableApis2["imgur"] = "imgur"; AvailableApis2["sanity"] = "sanity"; return AvailableApis2; })(AvailableApis || {}); var DataTypes = /* @__PURE__ */ ((DataTypes2) => { DataTypes2[DataTypes2["ambassador"] = 0] = "ambassador"; DataTypes2[DataTypes2["game"] = 1] = "game"; DataTypes2[DataTypes2["player"] = 2] = "player"; DataTypes2[DataTypes2["setting"] = 3] = "setting"; DataTypes2[DataTypes2["tag"] = 4] = "tag"; DataTypes2[DataTypes2["queue"] = 5] = "queue"; DataTypes2[DataTypes2["stat"] = 6] = "stat"; DataTypes2[DataTypes2["achievement"] = 7] = "achievement"; return DataTypes2; })(DataTypes || {}); var GameSettingsKeys = /* @__PURE__ */ ((GameSettingsKeys2) => { GameSettingsKeys2["achievementsEnabled"] = "achievements::enabled"; return GameSettingsKeys2; })(GameSettingsKeys || {}); var Errors = /* @__PURE__ */ ((Errors2) => { Errors2["NotImplemented"] = "method not implemented for adapter: "; return Errors2; })(Errors || {}); var HttpStatusCode = /* @__PURE__ */ ((HttpStatusCode2) => { HttpStatusCode2[HttpStatusCode2["Continue"] = 100] = "Continue"; HttpStatusCode2[HttpStatusCode2["SwitchingProtocols"] = 101] = "SwitchingProtocols"; HttpStatusCode2[HttpStatusCode2["Processing"] = 102] = "Processing"; HttpStatusCode2[HttpStatusCode2["Ok"] = 200] = "Ok"; HttpStatusCode2[HttpStatusCode2["Created"] = 201] = "Created"; HttpStatusCode2[HttpStatusCode2["Accepted"] = 202] = "Accepted"; HttpStatusCode2[HttpStatusCode2["NonAuthoritativeInformation"] = 203] = "NonAuthoritativeInformation"; HttpStatusCode2[HttpStatusCode2["NoContent"] = 204] = "NoContent"; HttpStatusCode2[HttpStatusCode2["ResetContent"] = 205] = "ResetContent"; HttpStatusCode2[HttpStatusCode2["PartialContent"] = 206] = "PartialContent"; HttpStatusCode2[HttpStatusCode2["MultiStatus"] = 207] = "MultiStatus"; HttpStatusCode2[HttpStatusCode2["AlreadyReported"] = 208] = "AlreadyReported"; HttpStatusCode2[HttpStatusCode2["ImUsed"] = 226] = "ImUsed"; HttpStatusCode2[HttpStatusCode2["MultipleChoices"] = 300] = "MultipleChoices"; HttpStatusCode2[HttpStatusCode2["MovedPermanently"] = 301] = "MovedPermanently"; HttpStatusCode2[HttpStatusCode2["Found"] = 302] = "Found"; HttpStatusCode2[HttpStatusCode2["SeeOther"] = 303] = "SeeOther"; HttpStatusCode2[HttpStatusCode2["NotModified"] = 304] = "NotModified"; HttpStatusCode2[HttpStatusCode2["UseProxy"] = 305] = "UseProxy"; HttpStatusCode2[HttpStatusCode2["SwitchProxy"] = 306] = "SwitchProxy"; HttpStatusCode2[HttpStatusCode2["TemporaryRedirect"] = 307] = "TemporaryRedirect"; HttpStatusCode2[HttpStatusCode2["PermanentRedirect"] = 308] = "PermanentRedirect"; HttpStatusCode2[HttpStatusCode2["BadRequest"] = 400] = "BadRequest"; HttpStatusCode2[HttpStatusCode2["Unauthorized"] = 401] = "Unauthorized"; HttpStatusCode2[HttpStatusCode2["PaymentRequired"] = 402] = "PaymentRequired"; HttpStatusCode2[HttpStatusCode2["Forbidden"] = 403] = "Forbidden"; HttpStatusCode2[HttpStatusCode2["NotFound"] = 404] = "NotFound"; HttpStatusCode2[HttpStatusCode2["MethodNotAllowed"] = 405] = "MethodNotAllowed"; HttpStatusCode2[HttpStatusCode2["NotAcceptable"] = 406] = "NotAcceptable"; HttpStatusCode2[HttpStatusCode2["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; HttpStatusCode2[HttpStatusCode2["RequestTimeout"] = 408] = "RequestTimeout"; HttpStatusCode2[HttpStatusCode2["Conflict"] = 409] = "Conflict"; HttpStatusCode2[HttpStatusCode2["Gone"] = 410] = "Gone"; HttpStatusCode2[HttpStatusCode2["LengthRequired"] = 411] = "LengthRequired"; HttpStatusCode2[HttpStatusCode2["PreconditionFailed"] = 412] = "PreconditionFailed"; HttpStatusCode2[HttpStatusCode2["PayloadTooLarge"] = 413] = "PayloadTooLarge"; HttpStatusCode2[HttpStatusCode2["UriTooLong"] = 414] = "UriTooLong"; HttpStatusCode2[HttpStatusCode2["UnsupportedMediaType"] = 415] = "UnsupportedMediaType"; HttpStatusCode2[HttpStatusCode2["RangeNotSatisfiable"] = 416] = "RangeNotSatisfiable"; HttpStatusCode2[HttpStatusCode2["ExpectationFailed"] = 417] = "ExpectationFailed"; HttpStatusCode2[HttpStatusCode2["IAmATeapot"] = 418] = "IAmATeapot"; HttpStatusCode2[HttpStatusCode2["MisdirectedRequest"] = 421] = "MisdirectedRequest"; HttpStatusCode2[HttpStatusCode2["UnprocessableEntity"] = 422] = "UnprocessableEntity"; HttpStatusCode2[HttpStatusCode2["Locked"] = 423] = "Locked"; HttpStatusCode2[HttpStatusCode2["FailedDependency"] = 424] = "FailedDependency"; HttpStatusCode2[HttpStatusCode2["UpgradeRequired"] = 426] = "UpgradeRequired"; HttpStatusCode2[HttpStatusCode2["PreconditionRequired"] = 428] = "PreconditionRequired"; HttpStatusCode2[HttpStatusCode2["TooManyRequests"] = 429] = "TooManyRequests"; HttpStatusCode2[HttpStatusCode2["RequestHeaderFieldsTooLarge"] = 431] = "RequestHeaderFieldsTooLarge"; HttpStatusCode2[HttpStatusCode2["UnavailableForLegalReasons"] = 451] = "UnavailableForLegalReasons"; HttpStatusCode2[HttpStatusCode2["InternalServerError"] = 500] = "InternalServerError"; HttpStatusCode2[HttpStatusCode2["NotImplemented"] = 501] = "NotImplemented"; HttpStatusCode2[HttpStatusCode2["BadGateway"] = 502] = "BadGateway"; HttpStatusCode2[HttpStatusCode2["ServiceUnavailable"] = 503] = "ServiceUnavailable"; HttpStatusCode2[HttpStatusCode2["GatewayTimeout"] = 504] = "GatewayTimeout"; HttpStatusCode2[HttpStatusCode2["HttpVersionNotSupported"] = 505] = "HttpVersionNotSupported"; HttpStatusCode2[HttpStatusCode2["VariantAlsoNegotiates"] = 506] = "VariantAlsoNegotiates"; HttpStatusCode2[HttpStatusCode2["InsufficientStorage"] = 507] = "InsufficientStorage"; HttpStatusCode2[HttpStatusCode2["LoopDetected"] = 508] = "LoopDetected"; HttpStatusCode2[HttpStatusCode2["NotExtended"] = 510] = "NotExtended"; HttpStatusCode2[HttpStatusCode2["NetworkAuthenticationRequired"] = 511] = "NetworkAuthenticationRequired"; return HttpStatusCode2; })(HttpStatusCode || {}); const cacheKeys = { sanityUrlText: `sanity::`, imageHashText: `hash::`, albumHash: `album::`, hintText: `hint::`, timeText: `time::`, playerText: `player::`, playerData: `playerData::`, playerIdText: `playerId::`, gameIdText: `gameId::`, gameSlugText: `slug::`, gameText: `game::`, locationText: `location::`, discussionText: `discussion::`, mentionText: `mention::`, tagNumberText: `tag::`, imagesText: `images::`, imageUrlText: `imageUrl::`, gpsStringText: `gpsString::`, slugText: `slug::` }; const createTagObject = (tagData = {}, foundTagData = {}) => { return { _id: tagData._id, _type: tagData._type, /// Common Tag Data game: tagData.game ?? "", slug: tagData.slug ?? "", name: tagData.name ?? "", playerId: tagData.playerId ?? "", tagnumber: tagData.tagnumber ?? 0, /// Mystery Tag Data mysteryPlayer: tagData.mysteryPlayer ?? "", mysteryImage: tagData.mysteryImage, mysteryImageUrl: tagData.mysteryImageUrl ?? "", mysteryTime: tagData.mysteryTime ?? 0, hint: tagData.hint ?? "", discussionUrl: tagData.discussionUrl ?? "", mentionUrl: tagData.mentionUrl ?? "", /// Found Tag Data foundPlayer: foundTagData.foundPlayer ?? tagData.foundPlayer ?? "", foundImage: foundTagData.foundImage ?? tagData.foundImage, foundImageUrl: foundTagData.foundImageUrl ?? tagData.foundImageUrl ?? "", foundTime: foundTagData.foundTime ?? tagData.foundTime ?? 0, foundLocation: foundTagData.foundLocation ?? tagData.foundLocation ?? "", confirmedBoundary: foundTagData.confirmedBoundary ?? tagData.confirmedBoundary ?? false, gps: foundTagData.gps ?? tagData.gps ?? "" }; }; const tagDataFields = Object.keys(createTagObject()); const tagDataReferenceFields = ["game", "player"]; const tagDataAssetFields = ["foundImage", "mysteryImage"]; const tagDataObjectFields = { foundImage: "asset->_ref", mysteryImage: "asset->_ref" }; const createGameObject = (gameData = {}) => { return { _id: gameData._id, _type: gameData._type, name: gameData.name ?? gameData.slug ?? "", ambassadors: gameData.ambassadors ?? [], settings: gameData.settings ?? [], boundary: gameData.boundary ?? {}, mainhash: gameData.mainhash ?? "", archivehash: gameData.archivehash ?? "", queuehash: gameData.queuehash ?? "", logo: gameData.logo, awsRegion: gameData.awsRegion, region: gameData.region ?? { name: gameData.name }, slug: gameData.slug ?? gameData.name ?? "" }; }; const gameDataReferenceFields = ["region", "settings"]; const gameDataArrayFields = ["ambassadors", "tags", "settings"]; const gameDataCustomFields = { settings: "[]->{key,value}", region: "name,description,zipcode,radius,tz" }; const gameDataFields = Object.keys(createGameObject()); const gameDataObjectFields = { logo: "asset->_ref" }; const createPlayerObject = (playerData = {}) => { return { _id: playerData._id, _type: playerData._type, achievements: playerData.achievements ?? [], bicon: playerData.bicon ?? "", games: playerData.games ?? (playerData.game ? [playerData.game] : []), name: playerData.name ?? "", slug: playerData.slug ?? "", tags: playerData.tags ?? [] }; }; const playerDataFields = Object.keys(createPlayerObject()); const playerDataReferenceFields = ["games", "tags", "achievements"]; const playerDataArrayFields = ["games", "tags", "achievements"]; const playerDataAssetFields = []; const playerDataObjectFields = {}; const createAmbassadorObject = (ambassadorData = {}) => { return { id: ambassadorData._id ?? ambassadorData.id ?? "", address1: ambassadorData.address1 ?? "", address2: ambassadorData.address2 ?? "", city: ambassadorData.city ?? "", country: ambassadorData.country ?? "", email: ambassadorData.email ?? "", name: ambassadorData.name ?? "", phone: ambassadorData.phone ?? "", player: ambassadorData.player ?? "", slug: ambassadorData.slug ?? "", zipcode: ambassadorData.zipcode ?? "" }; }; const ambassadorDataFields = Object.keys(createAmbassadorObject()); const ambassadorDataReferenceFields = ["player"]; const createSettingObject = (settingData = {}) => { return { slug: settingData.slug ?? "", description: settingData.description ?? "", name: settingData.name ?? "", key: settingData.key ?? "", value: settingData.value ?? "" }; }; const settingDataFields = Object.keys(createSettingObject()); const statDataReferenceFields = ["game"]; const statDataArrayFields = []; const createStatObject = (statData = {}) => { return { description: statData.description ?? "", game: statData.game ?? "", name: statData.name ?? "", key: statData.key ?? "", value: statData.value ?? "", updatedAt: statData._updatedAt ?? statData._updatedAt ?? /* @__PURE__ */ new Date() }; }; const statDataFields = [...Object.keys(createStatObject()), "_updatedAt"]; const createAchievementObject = (achievementData = {}) => { return { slug: achievementData.slug ?? "", description: achievementData.description ?? "", name: achievementData.name ?? "", key: achievementData.key ?? "", value: achievementData.value ?? "", group: achievementData.group ?? "" }; }; const achievementDataFields = Object.keys(createAchievementObject()); const awsRegions = [ "us-east-1", "us-east-2", "us-west-1", "us-west-2", "ca-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "eu-central-1", "eu-north-1", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ap-northeast-2", "ap-south-1", "sa-east-1" ]; const putCacheIfExists = (key, value, cache) => { if (cache) cache.put(key, value); }; const getCacheIfExists = (key, cache) => { if (cache) return cache.get(key); return null; }; const constructTagNumberSlug = (number, game = "") => { return `${game}-tag-${number}`; }; const isImgurCredentials = (credentials) => { return credentials?.clientId !== void 0 || credentials?.clientSecret !== void 0 || credentials?.clientId !== void 0 && credentials?.hash !== void 0; }; const isImgurApiReady = (credentials) => { if (!(credentials.clientId || credentials.hash)) { return 0; } else if (credentials.accessToken) { return 3; } else if (credentials.clientId && credentials.clientSecret) { return 2; } return 1; }; const isSanityCredentials = (credentials) => { return credentials?.projectId !== void 0; }; const isAWSCredentials = (credentials) => { return !!credentials?.accessKeyId; }; const isAWSApiReady = (credentials) => { if (credentials.accessKeyId !== void 0) { return credentials.secretAccessKey !== void 0 ? 3 : 1; } return 0; }; const isSanityApiReady = (credentials) => { if (credentials.projectId !== void 0 && credentials.dataset !== void 0 && credentials.apiVersion !== void 0) { return credentials.token !== void 0 ? 3 : 1; } return 0; }; const isBikeTagCredentials = (credentials) => { return credentials?.game !== void 0 || credentials?.clientToken !== void 0 && credentials?.clientKey !== void 0; }; const isBikeTagApiReady = (credentials) => { return credentials ? 1 : 0; }; const createImgurCredentials = (credentials, defaults = {}) => { return { hash: credentials.hash?.length ? credentials.hash : defaults.hash, queuehash: credentials.queuehash?.length ? credentials.queuehash : defaults.queuehash, archivehash: credentials.archivehash?.length ? credentials.archivehash : defaults.archivehash, clientId: credentials.clientId?.length ? credentials.clientId : defaults.clientId, clientSecret: credentials.clientSecret?.length ? credentials.clientSecret : defaults.clientSecret, accessToken: credentials.accessToken?.length ? credentials.accessToken : defaults.accessToken, refreshToken: credentials.refreshToken?.length ? credentials.refreshToken : defaults.refreshToken, rapidApiHost: credentials.rapidApiHost?.length ? credentials.rapidApiHost : defaults.rapidApiHost, rapidApiKey: credentials.rapidApiKey?.length ? credentials.rapidApiKey : defaults.rapidApiKey }; }; const assignImgurCredentials = (credentials, defaults = {}) => { const imgurCredentials = isImgurCredentials(credentials) ? createImgurCredentials(credentials, defaults) : defaults; return imgurCredentials; }; const createSanityCredentials = (credentials, defaults = {}) => { return { useCdn: credentials.token?.length ? false : typeof credentials.useCdn !== "undefined" ? credentials.useCdn : typeof defaults.useCdn !== "undefined" ? defaults.useCdn : true, projectId: credentials.projectId?.length ? credentials.projectId : defaults.projectId, dataset: credentials.dataset?.length ? credentials.dataset : defaults.dataset ?? "development", token: credentials.token?.length ? credentials.token : defaults.token ?? "", password: credentials.password?.length ? credentials.password : defaults.password, username: credentials.username?.length ? credentials.username : defaults.username, apiVersion: credentials.apiVersion?.length ? credentials.apiVersion : defaults.apiVersion ?? "2021-10-21" }; }; const assignSanityCredentials = (credentials, defaults) => { const sanityCredentials = isSanityCredentials( credentials ) ? createSanityCredentials(credentials, defaults) : defaults; return sanityCredentials; }; const createAWSCredentials = (credentials, defaults = {}) => { const region = credentials.region?.length ? credentials.region : defaults.region; let endpoint = credentials.endpoint?.length ? credentials.endpoint : defaults.endpoint; if (!endpoint && region && awsRegions.indexOf(region) === -1) { endpoint = `https://${region}.digitaloceanspaces.com`; } return { accessKeyId: credentials.accessKeyId?.length > 0 ? credentials.accessKeyId : defaults.accessKeyId, secretAccessKey: credentials.secretAccessKey?.length > 0 ? credentials.secretAccessKey : defaults.secretAccessKey, region, endpoint }; }; const assignAWSCredentials = (credentials, defaults) => { const awsCredentials = isAWSCredentials(credentials) ? createAWSCredentials(credentials, defaults) : defaults; return awsCredentials; }; const createBikeTagCredentials = (credentials, defaults = {}) => { return { game: credentials.game?.length ? credentials.game : defaults.game, host: credentials.host?.length ? credentials.host : defaults.host, verbose: credentials.verbose, cached: typeof credentials.cached !== "undefined" ? credentials.cached : defaults.cached, source: credentials.source?.length ? credentials.source : defaults.source, clientKey: credentials.clientKey?.length ? credentials.clientKey : defaults.clientKey, clientToken: credentials.clientToken?.length ? credentials.clientToken : defaults.clientToken }; }; const assignBikeTagCredentials = (credentials, defaults) => { const biketagCredentials = isBikeTagCredentials(credentials) ? createBikeTagCredentials(credentials, defaults) : defaults; return biketagCredentials; }; const assignBikeTagConfiguration = (config, defaults) => { const configuration = {}; const parsedConfig = { biketag: assignBikeTagCredentials( config, defaults?.biketag ), sanity: assignSanityCredentials( config, defaults?.sanity ), aws: assignAWSCredentials( config, defaults?.aws ), imgur: assignImgurCredentials( config, defaults?.imgur ) }; configuration.biketag = config.biketag ? { ...parsedConfig.biketag, ...createBikeTagCredentials(config.biketag) } : parsedConfig.biketag; configuration.aws = config.aws ? { ...parsedConfig.aws, ...createAWSCredentials(config.aws) } : parsedConfig.aws; configuration.sanity = config.sanity ? { ...parsedConfig.sanity, ...createSanityCredentials(config.sanity) } : parsedConfig.sanity; configuration.imgur = config.imgur ? { ...parsedConfig.imgur, ...createImgurCredentials(config.imgur) } : parsedConfig.imgur; return configuration; }; const sortTags = (tags, sort = "new", limit = 0, time = "all") => { let sorted = tags; switch (sort) { /// Leaderboard? case "top": sorted = tags.sort((a, b) => b.tagnumber - a.tagnumber); break; /// Queue case "relevance": sorted = tags.sort((a, b) => { const aHasFoundImage = a?.foundImageUrl?.length; const aHasMysteryImage = a?.mysteryImageUrl?.length; const bHasFoundImage = b?.foundImageUrl?.length; const bHasMysteryImage = b?.mysteryImageUrl?.length; const aHasBothImages = aHasFoundImage && aHasMysteryImage; const bHasBothImages = bHasFoundImage && bHasMysteryImage; const bIsBeforeA = -1; const aIsBeforeB = 1; if (!aHasBothImages && !bHasBothImages) { if (aHasFoundImage && bHasFoundImage) { return a?.foundTime - b?.foundTime; } else if (aHasFoundImage) { return aIsBeforeB; } else if (bHasFoundImage) { return aIsBeforeB; } } else if (aHasBothImages && bHasBothImages) { const firstToComplete = a?.mysteryTime - b?.mysteryTime; return firstToComplete; } else if (aHasBothImages && !bHasBothImages) { return bIsBeforeA; } else if (!aHasBothImages && bHasBothImages) { return aIsBeforeB; } return 0; }); break; /// BikeTags case "new": sorted = tags.sort((a, b) => b?.tagnumber - a?.tagnumber); break; /// BikeTags case "old": sorted = tags.sort((a, b) => b?.tagnumber - a?.tagnumber).reverse(); break; default: sorted = tags.sort((a, b) => a?.tagnumber - b?.tagnumber); break; } let timeConstraint = 0; switch (time) { case "hour": timeConstraint = 60 * 60 * 1e3; break; case "day": timeConstraint = 60 * 60 * 24 * 1e3; break; case "week": timeConstraint = 60 * 60 * 24 * 7 * 1e3; break; } if (timeConstraint) { const afterDate = Date.now() - timeConstraint; sorted = sorted.filter((t) => t.mysteryTime * 1e3 > afterDate); } return limit !== 0 ? sorted.slice(0, limit) : sorted; }; const sortPlayers = (players, sort = "new", limit = 0) => { let sorted = players; switch (sort) { case "top": sorted = players.sort((a, b) => b.tags.length - a.tags.length); break; case "comments": sorted = players.sort((a, b) => a.name.localeCompare(b.name)); break; case "new": sorted = players.reverse(); break; } return limit !== 0 ? sorted.slice(0, limit) : sorted; }; const sortAmbassadors = (ambassadors, sort = "new", limit = 0) => { const sorter = (a, b) => b.name.localeCompare(a.name); let sorted = ambassadors; switch (sort) { case "top": sorted = ambassadors.sort(sorter); break; case "new": sorted = ambassadors.reverse(); break; } return limit !== 0 ? sorted.slice(0, limit) : sorted; }; const sortSettings = (settings, sort = "new", limit = 0) => { let sorted = settings; switch (sort) { case "comments": sorted = settings.sort((a, b) => a.name.localeCompare(b.name)); break; case "new": sorted = settings.reverse(); break; } return limit !== 0 ? sorted.slice(0, limit) : sorted; }; const sortStats = (stats, sort = "new", limit = 0) => { let sorted = stats; switch (sort) { case "comments": sorted = stats.sort((a, b) => a.name.localeCompare(b.name)); break; case "new": sorted = stats.reverse(); break; } return limit !== 0 ? sorted.slice(0, limit) : sorted; }; const sortAchievements = (achievements, sort = "new", limit = 0) => { let sorted = achievements; switch (sort) { case "comments": sorted = achievements.sort((a, b) => a.name.localeCompare(b.name)); break; case "new": sorted = achievements.reverse(); break; } return limit !== 0 ? sorted.slice(0, limit) : sorted; }; const getGameAlbumFromCache = async (gameAlbumHash, cache, fallback, useCache = true) => { const cacheKey = `imgur::${cacheKeys.albumHash}${gameAlbumHash}`; const existsInCache = getCacheIfExists(cacheKey, useCache ? cache : void 0); if (existsInCache) { return existsInCache; } if (fallback) { const putIntoCache = await fallback(); putCacheIfExists(cacheKey, putIntoCache, cache); return putIntoCache; } }; const getTagDate = (time) => new Date(time * 1e3); const convertMiliseconds = (miliseconds, format) => { const total_seconds = Math.floor(miliseconds / 1e3); const total_minutes = Math.floor(total_seconds / 60); const total_hours = Math.floor(total_minutes / 60); const days = Math.floor(total_hours / 24); const seconds = total_seconds % 60; const minutes = total_minutes % 60; const hours = total_hours % 24; switch (format) { case "s": return total_seconds; case "m": return total_minutes; case "h": return total_hours; case "d": return days; default: return { d: days, h: hours, m: minutes, s: seconds }; } }; const getLongestTimeBetweenTags = (tags) => { let longestTimeBetweenTags = 0; let startDate = null; let endDate = null; let previousTag = null; let staleTagNumber = 0; const sortedTags = [...tags].reverse(); for (const tag of sortedTags) { if (previousTag !== null) { const timeBetweenTags = tag.mysteryTime - previousTag.mysteryTime; if (timeBetweenTags > longestTimeBetweenTags) { longestTimeBetweenTags = timeBetweenTags; startDate = new Date(previousTag.mysteryTime * 1e3); endDate = new Date(tag.mysteryTime * 1e3); staleTagNumber = previousTag.tagnumber; } } previousTag = tag; } return { timeBetweenTagsDays: convertMiliseconds( longestTimeBetweenTags * 1e3, "d" ), startDate, endDate, staleTagNumber }; }; const getPlayersWithLongestDailyTagStreakData = (players) => { let longestStreakDays = 0; let playerRecord; const playersData = []; for (const player of players) { if (player.name === "") { console.log( "Player has no name, and probably no real data. Ignoring player: ", player ); continue; } const playerLongestStreakData = getTagLongestDailyStreakData( player.tags ); if (playerLongestStreakData.longestStreakDaysCount > longestStreakDays) { longestStreakDays = playerLongestStreakData.longestStreakDaysCount; } playerRecord = { playerName: player.name, longestStreakData: playerLongestStreakData }; playersData.push(playerRecord); } const longestStreakPlayersData = []; for (const pd of playersData) { if (pd.longestStreakData.longestStreakDaysCount >= longestStreakDays) { longestStreakPlayersData.push(pd); } } return longestStreakPlayersData; }; const getTagLongestDailyStreakData = (tags) => { const tagDates = getUniqueTagDates(tags); tagDates.sort((a, b) => a.getTime() - b.getTime()); let streakDaysCount = 1; let streakDaysCountLongest = 1; let streakStartDate = null; let streakLongestStartDate = null; let streakEndDate = null; let previousDate = null; const oneDay = 24 * 60 * 60 * 1e3; for (const td of tagDates) { if (previousDate !== null) { const tagDateMinusOneDay = /* @__PURE__ */ new Date(); tagDateMinusOneDay.setTime(td.getTime() - oneDay); if (getIsSameDay(tagDateMinusOneDay, previousDate)) { streakDaysCount++; if (streakDaysCount === 2) { streakStartDate = previousDate; } if (streakDaysCount >= streakDaysCountLongest) { streakLongestStartDate = streakStartDate; streakDaysCountLongest = streakDaysCount; streakEndDate = td; } } else { streakDaysCount = 1; } } else { streakStartDate = td; streakEndDate = td; } previousDate = td; } const streakData = { longestStreakDaysCount: streakDaysCountLongest, longestStreakStartDate: streakDaysCountLongest > 1 ? streakLongestStartDate : null, longestStreakEndDate: streakDaysCountLongest > 1 ? streakEndDate : null }; return streakData; }; const getUniqueTagDates = (tags) => { const uniqueTagDates = []; for (const tag of tags) { if (tag.mysteryTime === 0) { continue; } const tagDate = getTagDate(tag.mysteryTime); if (uniqueTagDates.length === 0) { uniqueTagDates.push(tagDate); } else { const isAlreadyPresent = uniqueTagDates.some( (date) => getIsSameDay(date, tagDate) ); if (!isAlreadyPresent) { uniqueTagDates.push(tagDate); } } } return uniqueTagDates; }; const getIsSameDay = (d1, d2) => { return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); }; const getDaysDifference = (d1, d2) => { const timeDiff = Math.abs(d2.getTime() - d1.getTime()); const oneDay = 24 * 60 * 60 * 1e3; const diffDays = Math.ceil(timeDiff / oneDay); return diffDays; }; const getIsWithinDaysRange = (d1, d2, days) => { const diffDays = getDaysDifference(d1, d2); return diffDays <= days; }; const getGameHighestNumberTagsPerNumberDaysData = (tags, days = 1) => { let tagsPerNumberDaysHighest = 1; let previousTagDate = null; let startDate = null; let endDate = null; let tagDatesInRange = []; let tagIsInRange = false; let tagsPerNumberDaysData = { tagCount: tagsPerNumberDaysHighest, dayCount: days, startDate: null, endDate: null }; const oneDay = 24 * 60 * 60 * 1e3; const daysBack = [...Array(days).keys()]; const sortedTags = [...tags].reverse(); for (const tag of sortedTags) { const tagDate = getTagDate(tag.mysteryTime); if (previousTagDate !== null) { if (tagDatesInRange.length === 0) { tagDatesInRange = [previousTagDate]; } const tagDatesInRangeNew = [...tagDatesInRange]; for (const dayBack of daysBack) { const daysBackTime = oneDay * dayBack; const tagDateMinusDayBack = /* @__PURE__ */ new Date(); tagDateMinusDayBack.setTime(tagDate.getTime() - daysBackTime); for (const tagDateInRange of tagDatesInRange) { if (getIsWithinDaysRange(tagDateInRange, tagDateMinusDayBack, days)) { tagDatesInRangeNew.push(tagDate); tagIsInRange = true; break; } else { const indexNotInRangeDate = tagDatesInRangeNew.indexOf(tagDateInRange); tagDatesInRangeNew.splice(indexNotInRangeDate, 1); tagIsInRange = false; } } if (tagIsInRange) { break; } } tagDatesInRange = tagDatesInRangeNew; if (tagDatesInRange.length >= tagsPerNumberDaysHighest) { tagsPerNumberDaysHighest = tagDatesInRange.length; startDate = tagDatesInRange[0]; endDate = tagDatesInRange[tagDatesInRange.length - 1]; } } previousTagDate = tagDate; } tagsPerNumberDaysData = { tagCount: tagsPerNumberDaysHighest, dayCount: days, startDate, endDate }; return tagsPerNumberDaysData; }; const getPlayerHighestNumberTagsPerDayData = (player) => { const tagsPerDayData = { playerName: player.name, tagCount: null, tagDate: null }; let tagsPerDay = 1; let tagsPerDayHighest = 1; let previousTagDate = null; for (const tag of player.tags) { const tagDate = getTagDate(tag.mysteryTime); if (previousTagDate !== null && getIsSameDay(tagDate, previousTagDate)) { tagsPerDay++; } else { tagsPerDay = 1; } if (tagsPerDay > tagsPerDayHighest) { tagsPerDayHighest = tagsPerDay; tagsPerDayData.tagCount = tagsPerDayHighest; tagsPerDayData.tagDate = tagDate; } previousTagDate = tagDate; } return tagsPerDayData; }; const getPlayersWithHighestNumberTagsPerDayData = (players) => { let tagsPerDayHighest = 0; const tagsPerDayData = []; const highestTagsPerDayData = []; for (const player of players) { if (player.name === "") { console.log( "Player has no name, and probably no real data. Ignoring player: ", player ); continue; } const tagsPerDayRecord = getPlayerHighestNumberTagsPerDayData(player); tagsPerDayData.push(tagsPerDayRecord); if (tagsPerDayRecord.tagCount !== null && tagsPerDayRecord.tagCount > tagsPerDayHighest) { tagsPerDayHighest = tagsPerDayRecord.tagCount; } } for (const tpdr of tagsPerDayData) { if (tpdr.tagCount !== null && tpdr.tagCount >= tagsPerDayHighest) { highestTagsPerDayData.push(tpdr); } } return highestTagsPerDayData; }; const getExtensionFromUrl = (url) => { const match = url.match(/\.([a-zA-Z0-9]+)(?:\?|#|$)/); return match ? match[1].toLowerCase() : void 0; }; const getImageExtension = (contentType) => { const imageMimeToExt = { "image/jpeg": "jpg", "image/png": "png", "image/gif": "gif", "image/webp": "webp", "image/bmp": "bmp", "image/avif": "avif", "image/tiff": "tiff" }; return imageMimeToExt[contentType.toLowerCase()]; }; const getContentTypeFromExtension = (ext) => { const extToMime = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", bmp: "image/bmp", avif: "image/avif", tiff: "image/tiff" }; return extToMime[ext.toLowerCase()] ?? "application/octet-stream"; }; const getTagNumbersFromTextRegex = new RegExp( /((?:(?:bike\s*)?(?:\s*tag)?)(#|num|number)(\d+)(?:(?:\s*tag)?|(?:\s*proof)?))|((?:bike\s*)?(?:tag\s*)(#|num|number)?\s*(\d+))|((?:(?:found\s*#?)|(?:here'?i?s?\s*))\[?(\d+)\]?)/gi ); const getPlayerFromTextRegex = new RegExp( /((?:proof\s*(?:found\s*at\s*)?(?:\(.*\))?\s*by\s*)(.*?(?= on \[|$)))|((?:tag\s*(?:(?:\(\s*hint:\s*)?.*\))?\s*by\s*)(.+?(?= on \[|\r|\n|$))?)|((?:tag\s*(?:(?:\(\s*hint:\s*)?.*\))?\s*by\s*)(.+?(?= on \[|\r|\n))?)|((?:credit goes to:\s*)(.*)(?:\sfor finding))|(?:tag\s*)(?:number\s*)?(\d*)?(?:\s*by\s*)(.+?(?= on \[|$|\n))/i ); const getFoundLocationFromTextRegex = new RegExp( /(?:is\s*(?:at|the)?\s*?)(.*?)(?=\s*by\s+|\]|$)|(?:found)\s*(?:at)?\s*\(([^()]+(\([^()]+\)[^()]*)*)\)(.*(?:\)|]|$))|(?:found\s*at\s*\()(.*(?:\)))/im ); const getConfirmedBoundaryFromTextRegex = new RegExp( /(\s*\[.*\]✓\s*\(.*\)\s*)/im ); const getPlayerFromInfoFromTextRegex = new RegExp( /(?:\[Player\s*)(.*)(?:]\s*\+\()(.*)(?=\))/i ); const getGameFromInfoFromTextRegex = new RegExp( /(((?:{\s*)(.*)(?:}))((?:\s*\[)(.*)(?=\])\])?((?:\s*@\()(.*)(?=\))\))?)\s*::\s*(((?:\[BikeTag\s*Game\s*)(.*)(?:]))((?:\s*#\()(.*)(?=\))\))?((?:\s*\|)(.*)(?=\|)\|)?)/i ); const getGameSlugFromTextRegex = new RegExp(/((\w*)\s*bike\s*tag!?)/i); const getHintFromTextRegex = new RegExp(/(?:hint:\s*)(.*)\)/i); const getTimeFromTextRegex = new RegExp( /(?:on\s+\[(\d+\/\d+\/\d\d)@(\d+:\d+:\d+)\])/i ); const getGPSLocationFromTextRegex = new RegExp( /(([0-9]{1,2})[:|°]([0-9]{1,2})[:|'|′]?([0-9]{1,2}(?:\.[0-9]+){0,1})?["|″]([N|S]),?\s*([0-9]{1,3})[:|°]([0-9]{1,2})[:|'|′]?([0-9]{1,2}(?:\.[0-9]+){0,1})?["|″]([E|W]))|((-?\d+(\.\d+)?),\s*(-?\d+(\.\d+)?))/ ); const getImageURLsFromTextRegex = new RegExp( /https?:\/\/.*?\.[a-z]{2,4}\/.+?(?=\)|\s|$)/gi ); const getAlbumIdFromTextRegex = new RegExp( /((?:imgur.com\/)(?:(a|album|gallery)\/)(\w+))/i ); const getPlayerIdFromTextRegex = RegExp(/\[(.*)\]/i); const getDiscussionUrlFromTextRegex = RegExp(/{(.*)}/i); const getGPSCoordinatesValueFromTextRegex = RegExp(/\((.*)\)/i); const getTagnumberFromSlugRegex = RegExp(/([^-]*)([^-]*)(\d)/); const getImgurImageHashFromUrlRegex = RegExp( /(?:imgur.com\/)(.*)(?:\.)/i ); const getSanityImageUrlHashFromTextRegex = RegExp( /^(?:image-)(.*?(?=-(-png|-jpg|-jpeg|-gif)))/i ); const getCreditFromBlueskyTextRegex = RegExp( /(?:tag\s*)(?:number\s*)?(\d*)?(?:\s*by\s*)(.+?(?=$|\n))/i ); const isMysteryImageRegex = /^#\d+\s+tag\s*\(.*?\)\s+by\s+.+/i; const isFoundImageRegex = /^#\d+\s+proof\s+found\s+at\s*\(.*?\)\s+by\s+.+/i; const isFoundImageFallbackRegex = /^#\d+\s+proof\s+found\s+at\s*\(.*$/i; const BikeTagExpressions = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, getAlbumIdFromTextRegex, getConfirmedBoundaryFromTextRegex, getCreditFromBlueskyTextRegex, getDiscussionUrlFromTextRegex, getFoundLocationFromTextRegex, getGPSCoordinatesValueFromTextRegex, getGPSLocationFromTextRegex, getGameFromInfoFromTextRegex, getGameSlugFromTextRegex, getHintFromTextRegex, getImageURLsFromTextRegex, getImgurImageHashFromUrlRegex, getPlayerFromInfoFromTextRegex, getPlayerFromTextRegex, getPlayerIdFromTextRegex, getSanityImageUrlHashFromTextRegex, getTagNumbersFromTextRegex, getTagnumberFromSlugRegex, getTimeFromTextRegex, isFoundImageFallbackRegex, isFoundImageRegex, isMysteryImageRegex }, Symbol.toStringTag, { value: "Module" })); function getTagNumbersFromText$1(inputText, fallback = null, cache) { if (!inputText?.length) return fallback; const cacheKey = `${cacheKeys.tagNumberText}${inputText}`; const existingParsed = getCacheIfExists(cacheKey, cache); if (existingParsed) return existingParsed; const tagNumberText = inputText.match(getTagNumbersFromTextRegex); if (!tagNumberText) return fallback ?? []; const tagNumbers = tagNumberText.reduce((numbers, text) => { if (!text) return numbers; let tagNumber = text.match(/\d+/); tagNumber = tagNumber?.length ? tagNumber[0] : null; if (!tagNumber) return numbers; const number = Number.parseInt(tagNumber); if (numbers.indexOf(number) == -1) numbers.push(number); return numbers; }, []); if (!tagNumbers.length && fallback) { putCacheIfExists(cacheKey, fallback, cache); return fallback; } putCacheIfExists(cacheKey, tagNumbers, cache); return tagNumbers; } function getPlayerDataFromText(inputText, cache) { if (!inputText) return void 0; const cacheKey = `${cacheKeys.playerData}${inputText}`; const existingParsed = getCacheIfExists(cacheKey, cache); if (existingParsed) return existingParsed; const playerData = getPlayerFromInfoFromTextRegex.exec(inputText); if (!playerData?.length) return void 0; const player = createPlayerObject({ name: playerData[1], games: playerData[2].split(",") }); if (!player.name?.length) { putCacheIfExists(cacheKey, false, cache); return void 0; } putCacheIfExists(cacheKey, player, cache); return player; } function getGameSlugFromText(inputText, cache) { if (!inputText) return void 0; const cacheKey = `${cacheKeys.gameSlugText}${inputText}`; const existingParsed = getCacheIfExists(cacheKey, cache); if (existingParsed) return existingParsed; const gameData = getGameSlugFromTextRegex.exec(inputText); if (!gameData?.length) return void 0; const gameName = gameData[2]; if (!gameName) { putCacheIfExists(cacheKey, false, cache); return void 0; } putCacheIfExists(cacheKey, gameName, cache); return gameName; } function getGameDataFromText(inputText, cache) { if (!inputText) return void 0; const cacheKey = `${cacheKeys.gameText}${inputText}`; const existingParsed = getCacheIfExists(cacheKey, cache); if (existingParsed) return existingParsed; const gameData = getGameFromInfoFromTextRegex.exec(inputText); if (!gameData?.length) return void 0; const game = createGameObject({ name: gameData[10], ambassadors: (gameData[12] ?? "").split(","), queuehash: gameData[14], region: { name: gameData[10], description: gameData[3] }, subreddit: gameData[5], bluesky: gameData[7] }); if (!game.name?.length) { putCacheIfExists(cacheKey, false, cache); return void 0; } putCacheIfExists(cacheKey, game, cache); return game; } function getPlayerFromText$1(inputText, fallback, cache) { if (!inputText) return null; const cacheKey = `${cacheKeys.playerText}${inputText}`; const existingParsed = getCacheIfExists(cacheKey, cache); if (existingParsed) return existingParsed; const playerText = getPlayerFromTextRegex.exec(inputText); if (!playerText) return null; const tagPlayers = playerText.filter( (c) => typeof c === "string" && (c.indexOf("tag ") === -1 || c.indexOf("tag") !== 0) && (c.indexOf("proof ") === -1 || c.indexOf("proof") !== 0) && c.indexOf("to:") === -1 && c.indexOf("hint:") === -1 && (c.indexOf(" by ") === -1 || c.indexOf(" by ") !== 0) ? c : void 0 ); if (!tagPlayers?.length && fallback) ; const player = tagPlayers[0]; putCacheIfExists(cacheKey, player, cache); return player; } function getPlayerIdFromText(inputText, fallback, cache) { if (!inputText?.length) { return fallback; } const cacheKey = `${cacheKeys.playerIdText}${inputText}`; const existingParsed = getCacheIfExists(cacheKey, cache); if (existingParsed) return existingParsed; const playerIdText = getPlayerIdFromTextRegex.exec(inputText); if (!playerIdText) { return fallback; } const playerId = (playerIdText[1] || "").trim(); putCacheIfExists(cacheKey, playerId, cache); return playerId; } function getDiscussionUrlFromText$1(inputText, fallback, cache) { if (!inputText?.length) { return fallback; } const cacheKey = `${cacheKeys.discussionText}${inputText}`; const existingParsed = getCacheIfExists(cacheKey, cache); if (existingParsed) return existingParsed; const discussionUrlText = getDiscussionUrlFromTextRegex.exec(inputText); if (!discussionUrlText) { putCacheIfExists(cacheKey, fallback, cache); return fallback; } const discussionUrl = (discussionUrlText[1] || "").trim(); putCacheIfExists(cacheKey, discussionUrl, cache); return discussionUrl; } function getFoundLocationFromText$1(inputText, fallback, cache) { if (!inputText?.length) { return fallback; } const cacheKey = `${cacheKeys.locationText}${inputText}`; const existingParsed = getCacheIfExists(cacheKey, cache); if (existingParsed) return existingParsed; const foundLocationText = getFoundLocationFromTextRegex.exec(inputText); if (!foundLocationText) { putCacheIfExists(cacheKey, fallback, cache); return fallback; } const foundLocation = (foundLocationText[1] ?? foundLocationText[2] ?? "").trim(); putCacheIfExists(cacheKey, foundLocation, cache); return foundLocation; } function getConfirmedBoundaryFromText(inputText, fallback, cache) { if (!inputText?.length) { return fallback; } const cacheKey = `${cacheKeys.locationText}${inputText}`; const existingParsed = getCacheIfExists(cacheKey, cache); if (existingParsed) return existingParsed; const confirmedBoundaryText = getConfirmedBoundaryFromTextRegex.exec(inputText); if (!confirmedBoundaryText) { putCacheIfExists(cacheKey, fallback, cache); return fallback; } const confirmedBoundary = (confirmedBoundaryText[4] || "").trim(); putCacheIfExists(cacheKey, confirmedBoundary, cache); return confirmedBoundary; } function getTimeFromText(inputText, fallback, cache) { const cacheKey = `${cacheKeys.timeText}${inputText}`; const existingParsed = getCacheIfExists(cacheKey, cache); if (existingParsed) return existingParsed; const timeMatch = getTimeFromTextRegex.exec(inputText); if (!timeMatch || timeMatch?.length < 2) { fallback = fallback ?? null; return fallback; } const time = (/* @__PURE__ */ new Date(`${timeMatch[1]} ${timeMatch[2]}`)).getTime() / 1e3; putCacheIfExists(cacheKey, time, cache); return time; } function getHintFromText$1(inputText, fallback, cache) { const cacheKey = `${cacheKeys.hintText}${inputText}`; const existingParsed = getCacheIfExists(cacheKey, cache); if (existingParsed) return existingParsed; const hintMatch = getHintFromTextRegex.exec(inputText); if (!hintMatch) { fallback = fallback ?? null; putCacheIfExists(cacheKey, fallback, cache); return fallback; } const hint = (hintMatch[1] || "").trim(); putCacheIfExists(cacheKey, hint, cache); return hint; } function getBikeTagNumberFromImage(image, cache) { return image.description ? getTagNumbersFromText$1(image.description, void 0, cache)[0] : -1; } function isMysteryImage$1(image) { const description = image.description || ""; return isMysteryImageRegex.test(description); } function isFoundImage$1(image) { const description = image.description || ""; if (isFoundImageRegex.test(description)) return true; return isFoundImageFallbackRegex.test(description); } function getBikeTagFromImgurImageSet(mysteryImage, foundImage, opts) { if (!foundImage && !mysteryImage) return null; let foundImageLink; let foundImageDescription; let foundImageTitle; let foundTime; let mysteryImageLink; let mysteryImageDescription; let mysteryImageTitle; let mysteryTime; let hint; let discussionUrl; let mysteryPlayer; let foundPlayer; let foundLocation; let confirmedBoundary; if (foundImage) { foundImageLink = foundImage?.link; foundImageDescription = foundImage?.description; foundImageTitle = foundImage?.title; foundTime = getTimeFromText(foundImageDescription, foundImage?.datetime); foundPlayer = getPlayerFromText$1(foundImageDescription); foundLocation = getFoundLocationFromText$1(foundImageDescription); confirmedBoundary = getConfirmedBoundaryFromText(foundImageTitle); } if (mysteryImage) { mysteryImageLink = mysteryImage?.link; mysteryImageDescription = mysteryImage?.description; mysteryImageTitle = mysteryImage?.title; mysteryTime = getTimeFromText( mysteryImageDescription, mysteryImage?.datetime ); hint = getHintFromText$1(mysteryImageDescription); discussionUrl = getDiscussionUrlFromText$1(mysteryImageTitle); mysteryPlayer = getPlayerFromText$1(mysteryImageDescription); } const game = opts?.game || ""; const tagnumber = mysteryImageDescription ? getTagNumbersFromText$1(mysteryImageDescription)[0] : getTagNumbersFromText$1(foundImageDescription)[0]; const name = constructTagNumberSlug(tagnumber, game); const playerId = getPlayerIdFromText(mysteryImageTitle) ?? getPlayerIdFromText(foundImageTitle); let gps = foundImageDescription ? getGPSLocationFromText(foundImageDescription) : getGPSLocationFromText(mysteryImageTitle); if (gps.lat === 0 && gps.long === 0 && foundImageTitle) { gps = getGPSLocationFromText(foundImageTitle); } if (foundLocation?.length && gps.lat !== 0 && gps.long !== 0) { const gpsFromFoundLocation = getGpsStringLocationFromText(foundLocation, ""); foundLocation = foundLocation.replace(gpsFromFoundLocation, "").replace( gpsFromFoundLocation.substring(0, gpsFromFoundLocation.length - 3), "" ); } const tagData = { tagnumber, name, slug: name, game, discussionUrl, foundLocation, mysteryPlayer, foundPlayer, foundTime, mysteryTime, hint, playerId, confirmedBoundary, mysteryImageUrl: mysteryImageLink, foundImageUrl: foundImageLink, /// TODO: get found location gps from found tag gps }; return tagData; } const getImageHashFromImgurImage = (image, cache) => { return getImageHashFromText(image.link, cache); }; const isValidUpdatePayload$3 = (utp) => { return typeof utp.imageHash === "string" && (typeof utp.title === "string" || typeof utp.description === "string") || typeof utp.title === "string" || typeof utp.description === "string"; }; const isValidUploadTagImagePayload = (utp) => { return ( /// TODO: do better type checking here utp && typeof utp.image !== "undefined" && (typeof utp.title === "string" || typeof utp.description === "string") ); }; const getUpdateTagPayloadFromTagData = (payload, mystery = false, combined = false) => { return { imageHash: mystery ? getImgurMysteryImageHashFromBikeTagData(payload) : getImgurFoundImageHashFromBikeTagData(payload), title: mystery ? getImgurMysteryTitleFromBikeTagData(payload, combined) : getImgurFoundTitleFromBikeTagData(payload, combined), description: mystery ? getImgurMysteryDescriptionFromBikeTagData(payload) : getImgurFoundDescriptionFromBikeTagData(payload) }; }; function getQueueTagImagePayloadFromTagData(tagData, mystery = false) { return { album: tagData.queuehash ?? tagData.hash, type: tagData.type ?? "stream", image: mystery ? tagData.mysteryImage : tagData.foundImage, title: tagData.title ?? (myste