import * as loader from "tc-loader"; import {Stage} from "tc-loader"; import {LogCategory, logTrace} from "tc-shared/log"; import jsonp from 'simple-jsonp-promise'; interface GeoLocationInfo { /* The country code */ country: string, city?: string, region?: string, timezone?: string } interface GeoLocationResolver { name() : string; resolve() : Promise; } const kLocalCacheKey = "geo-info"; type GeoLocationCache = { version: 1, timestamp: number, info: GeoLocationInfo, } class GeoLocationProvider { private readonly resolver: GeoLocationResolver[]; private currentResolverIndex: number; private cachedInfo: GeoLocationInfo | undefined; private lookupPromise: Promise; constructor() { this.resolver = [ new GeoResolverIpInfo(), new GeoResolverIpData() ]; this.currentResolverIndex = 0; } loadCache() { this.doLoadCache(); if(!this.cachedInfo) { this.lookupPromise = this.doQueryInfo(); } } private doLoadCache() : GeoLocationInfo { try { const rawItem = localStorage.getItem(kLocalCacheKey); if(!rawItem) { return undefined; } const info: GeoLocationCache = JSON.parse(rawItem); if(info.version !== 1) { throw tr("invalid version number"); } if(info.timestamp + 2 * 24 * 60 * 60 * 1000 < Date.now()) { throw tr("cache is too old"); } if(info.timestamp + 2 * 60 * 60 * 1000 > Date.now()) { logTrace(LogCategory.GENERAL, tr("Geo cache is less than 2hrs old. Don't updating.")); this.lookupPromise = Promise.resolve(info.info); } else { this.lookupPromise = this.doQueryInfo(); } this.cachedInfo = info.info; } catch (error) { logTrace(LogCategory.GENERAL, tr("Failed to load geo resolve cache: %o"), error); } } async queryInfo(timeout: number) : Promise { return await new Promise(resolve => { if(!this.lookupPromise) { resolve(this.cachedInfo); return; } const timeoutId = typeof timeout === "number" ? setTimeout(() => resolve(this.cachedInfo), timeout) : -1; this.lookupPromise.then(result => { clearTimeout(timeoutId); resolve(result); }); }); } private async doQueryInfo() : Promise { while(this.currentResolverIndex < this.resolver.length) { const resolver = this.resolver[this.currentResolverIndex++]; try { const info = await resolver.resolve(); logTrace(LogCategory.GENERAL, tr("Successfully resolved geo info from %s: %o"), resolver.name(), info); localStorage.setItem(kLocalCacheKey, JSON.stringify({ version: 1, timestamp: Date.now(), info: info } as GeoLocationCache)); return info; } catch (error) { logTrace(LogCategory.GENERAL, tr("Geo resolver %s failed: %o. Trying next one."), resolver.name(), error); } } logTrace(LogCategory.GENERAL, tr("All geo resolver failed.")); return undefined; } } class GeoResolverIpData implements GeoLocationResolver { name(): string { return "ipdata.co"; } async resolve(): Promise { const response = await fetch("https://api.ipdata.co/?api-key=test"); const json = await response.json(); if(!("country_code" in json)) { throw tr("missing country code"); } return { country: json["country_code"], region: json["region"], city: json["city"], timezone: json["time_zone"]["name"] } } } class GeoResolverIpInfo implements GeoLocationResolver { name(): string { return "ipinfo.io"; } async resolve(): Promise { const response = await jsonp("http://ipinfo.io"); if(!("country" in response)) { throw tr("missing country"); } return { country: response["country"], city: response["city"], region: response["region"], timezone: response["timezone"] } } } export let geoLocationProvider: GeoLocationProvider; /* The client services depend on this */ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { priority: 35, function: async () => { geoLocationProvider = new GeoLocationProvider(); geoLocationProvider.loadCache(); }, name: "geo services" });