library-ts/node/users/location/LocationService.ts

191 lines
4.8 KiB
TypeScript
Raw Normal View History

2025-11-10 17:41:48 +00:00
import { Files } from "../../files/Files";
import { LocationData } from "./LocationData";
import * as https from "https";
import * as zlib from "zlib";
import * as tar from "tar";
import maxmind, { CityResponse, Reader, Response } from "maxmind";
import { RJLog } from "../../log/RJLog";
export class LocationService
{
_dbPath:string;
_accountID:string;
_licenseKey:string;
_url:string = "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz";
_lookUpDB:Reader<CityResponse>;
static async create( path:string, accountID:string, licenseKey:string, url = null ):Promise<LocationService>
{
let ipToL = new LocationService();
ipToL._dbPath = path;
ipToL._accountID = accountID;
ipToL._licenseKey = licenseKey;
if ( url )
{
ipToL._url = url;
}
RJLog.log( "Starting Location Service", ipToL._dbPath, ipToL._accountID, ipToL._licenseKey, ipToL._url );
await ipToL.update();
if ( ipToL._lookUpDB == null )
{
await ipToL._loadDB();
}
return ipToL;
}
async update( forceUpdate:boolean = false ):Promise<void>
{
let needsUpdate = forceUpdate || ( await this._needsUpdate() );
if ( ! needsUpdate )
{
return Promise.resolve();
}
try
{
await this._downloadDB();
await this._loadDB();
}
catch( e )
{
RJLog.error( "Updating failed:", e );
}
return Promise.resolve();
}
async _loadDB()
{
this._lookUpDB = await maxmind.open( this._dbPath );
}
async _needsUpdate( maxAgeDays = 7 )
{
try
{
let stats = await Files.getStatistics( this._dbPath );
let ageDays = ( Date.now() - stats.mtimeMs ) / (1000 * 60 * 60 * 24);
return Promise.resolve( ageDays > maxAgeDays );
}
catch ( error )
{
return Promise.resolve( true );
}
}
async _downloadDB()
{
RJLog.log( "Downloading DB" );
await Files.ensureParentDirectoryExists( this._dbPath );
let authenticification = `${this._accountID}:${this._licenseKey}`;
let promise = new Promise<void>(
( resolve, reject ) =>
{
let sendRequest = ( url:string, writeAuthHeader:boolean ) =>
{
let options:any = {
headers:
{
"Accept": "application/gzip"
}
};
if ( writeAuthHeader )
{
let authHeader = "Basic " + Buffer.from( authenticification ).toString( "base64" );
options =
{
headers:
{
"Authorization": authHeader,
"Accept": "application/gzip"
}
};
}
RJLog.log( "Downloading at ", url, options );
https.get( url, options,
( result ) =>
{
if ( result.statusCode === 302 || result.statusCode == 301 )
{
let redirectUrl = result.headers.location;
console.log(`Redirecting to ${redirectUrl}`);
result.resume();
return sendRequest( redirectUrl, false );
}
if ( result.statusCode !== 200 )
{
RJLog.log( `Location db download failed: ${result.statusCode} ${result.statusMessage}` );
reject( new Error( `Location db download failed: ${result.statusCode} ${result.statusMessage}` ) );
return;
}
// pipe through gunzip then tar extractor, writing .mmdb to DB_DIR
let gunzip = zlib.createGunzip();
let extractor = tar.x(
{
cwd: Files.parentPath( this._dbPath ),
filter: ( tarPath ) => tarPath.endsWith( ".mmdb" ),
strip: 1
}
);
result.pipe( gunzip ).pipe( extractor );
extractor.on( "finish", () => resolve());
extractor.on( "error", ( error ) => reject( error ) );
result.on( "error", ( error ) => reject( error ) );
}
).on("error", (error) => reject(error));
}
sendRequest( this._url, true );
}
);
return promise;
}
async getLocation( ip:string ):Promise<LocationData>
{
let locationData = new LocationData();
locationData.country = "Unknown";
locationData.city = "Unknown";
locationData.km_range = 20000;
if ( this._lookUpDB !== null )
{
let geo = this._lookUpDB.get( ip );
locationData.country = geo?.country?.names?.en ?? "Unknown";
locationData.city = geo?.city?.names?.en ?? "Unknown";
locationData.km_range = geo?.location?.accuracy_radius ?? 20000;
}
return locationData;
}
}