mirror of
https://git.mirrors.martin98.com/https://github.com/jina-ai/reader
synced 2025-04-19 04:10:08 +08:00

* wip * wip * fix * wip * fix: add jitter to user cache * cd * fix * fix * fix: user cache age comparison * fix: try to partition apiroll query * bump: deps * wip * cd * feat: fallback for serp * fix * cd * fix * fix * serp: stop hiding expense * serp: enable fallback by default
133 lines
3.2 KiB
TypeScript
133 lines
3.2 KiB
TypeScript
import { container, singleton } from 'tsyringe';
|
|
import fsp from 'fs/promises';
|
|
import { CityResponse, Reader } from 'maxmind';
|
|
import { AsyncService, AutoCastable, Prop, runOnce } from 'civkit';
|
|
import { GlobalLogger } from './logger';
|
|
import path from 'path';
|
|
import { Threaded } from './threaded';
|
|
|
|
export enum GEOIP_SUPPORTED_LANGUAGES {
|
|
EN = 'en',
|
|
ZH_CN = 'zh-CN',
|
|
JA = 'ja',
|
|
DE = 'de',
|
|
FR = 'fr',
|
|
ES = 'es',
|
|
PT_BR = 'pt-BR',
|
|
RU = 'ru',
|
|
}
|
|
|
|
export class GeoIPInfo extends AutoCastable {
|
|
@Prop()
|
|
code?: string;
|
|
|
|
@Prop()
|
|
name?: string;
|
|
}
|
|
|
|
export class GeoIPCountryInfo extends GeoIPInfo {
|
|
@Prop()
|
|
eu?: boolean;
|
|
}
|
|
|
|
export class GeoIPCityResponse extends AutoCastable {
|
|
@Prop()
|
|
continent?: GeoIPInfo;
|
|
|
|
@Prop()
|
|
country?: GeoIPCountryInfo;
|
|
|
|
@Prop({
|
|
arrayOf: GeoIPInfo
|
|
})
|
|
subdivisions?: GeoIPInfo[];
|
|
|
|
@Prop()
|
|
city?: string;
|
|
|
|
@Prop({
|
|
arrayOf: Number
|
|
})
|
|
coordinates?: [number, number, number];
|
|
|
|
@Prop()
|
|
timezone?: string;
|
|
}
|
|
|
|
@singleton()
|
|
export class GeoIPService extends AsyncService {
|
|
|
|
logger = this.globalLogger.child({ service: this.constructor.name });
|
|
|
|
mmdbCity!: Reader<CityResponse>;
|
|
|
|
constructor(
|
|
protected globalLogger: GlobalLogger,
|
|
) {
|
|
super(...arguments);
|
|
}
|
|
|
|
|
|
override async init() {
|
|
await this.dependencyReady();
|
|
|
|
this.emit('ready');
|
|
}
|
|
|
|
@runOnce()
|
|
async _lazyload() {
|
|
const mmdpPath = path.resolve(__dirname, '..', '..', 'licensed', 'GeoLite2-City.mmdb');
|
|
|
|
const dbBuff = await fsp.readFile(mmdpPath, { flag: 'r', encoding: null });
|
|
|
|
this.mmdbCity = new Reader<CityResponse>(dbBuff);
|
|
|
|
this.logger.info(`Loaded GeoIP database, ${dbBuff.byteLength} bytes`);
|
|
}
|
|
|
|
|
|
@Threaded()
|
|
async lookupCity(ip: string, lang: GEOIP_SUPPORTED_LANGUAGES = GEOIP_SUPPORTED_LANGUAGES.EN) {
|
|
await this._lazyload();
|
|
|
|
const r = this.mmdbCity.get(ip);
|
|
|
|
if (!r) {
|
|
return undefined;
|
|
}
|
|
|
|
return GeoIPCityResponse.from({
|
|
continent: r.continent ? {
|
|
code: r.continent?.code,
|
|
name: r.continent?.names?.[lang] || r.continent?.names?.en,
|
|
} : undefined,
|
|
country: r.country ? {
|
|
code: r.country?.iso_code,
|
|
name: r.country?.names?.[lang] || r.country?.names.en,
|
|
eu: r.country?.is_in_european_union,
|
|
} : undefined,
|
|
city: r.city?.names?.[lang] || r.city?.names?.en,
|
|
subdivisions: r.subdivisions?.map((x) => ({
|
|
code: x.iso_code,
|
|
name: x.names?.[lang] || x.names?.en,
|
|
})),
|
|
coordinates: r.location ? [
|
|
r.location.latitude, r.location.longitude, r.location.accuracy_radius
|
|
] : undefined,
|
|
timezone: r.location?.time_zone,
|
|
});
|
|
}
|
|
|
|
@Threaded()
|
|
async lookupCities(ips: string[], lang: GEOIP_SUPPORTED_LANGUAGES = GEOIP_SUPPORTED_LANGUAGES.EN) {
|
|
const r = (await Promise.all(ips.map((ip) => this.lookupCity(ip, lang)))).filter(Boolean) as GeoIPCityResponse[];
|
|
|
|
return r;
|
|
}
|
|
|
|
}
|
|
|
|
const instance = container.resolve(GeoIPService);
|
|
|
|
export default instance;
|