103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
type UnknownMap = Record<string, unknown>;
|
|
|
|
export const MAP_LOCATION_CLASSIFICATIONS = ["clear", "hosting"] as const;
|
|
|
|
function objectValue(value: unknown): UnknownMap | null {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? value as UnknownMap
|
|
: null;
|
|
}
|
|
|
|
function stringValue(value: unknown) {
|
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
}
|
|
|
|
function proxyValue(value: unknown) {
|
|
if (typeof value === "boolean") return value;
|
|
if (typeof value === "string" && value.toLowerCase() === "yes") return true;
|
|
if (typeof value === "string" && value.toLowerCase() === "no") return false;
|
|
return null;
|
|
}
|
|
|
|
function coordinate(value: unknown) {
|
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
|
|
return null;
|
|
}
|
|
|
|
export interface ParsedUserLocation {
|
|
latitude: number;
|
|
longitude: number;
|
|
label: string;
|
|
}
|
|
|
|
export interface ParsedUserNetwork {
|
|
asn: string | null;
|
|
provider: string | null;
|
|
connectionType: string | null;
|
|
proxy: boolean | null;
|
|
}
|
|
|
|
export function parseUserNetwork(value: unknown): ParsedUserNetwork {
|
|
const intelligence = objectValue(value);
|
|
const network = objectValue(intelligence?.network);
|
|
const providerResponse = objectValue(intelligence?.rawResponse);
|
|
const legacyDetails = Object.values(providerResponse ?? {})
|
|
.map(objectValue)
|
|
.find((details) => details && ("type" in details || "proxy" in details));
|
|
return {
|
|
asn: stringValue(network?.asn),
|
|
provider: stringValue(network?.provider),
|
|
connectionType: stringValue(network?.connectionType) ?? stringValue(legacyDetails?.type),
|
|
proxy: proxyValue(network?.proxy) ?? proxyValue(legacyDetails?.proxy),
|
|
};
|
|
}
|
|
|
|
export function parseUserLocation(value: unknown): ParsedUserLocation | null {
|
|
const intelligence = objectValue(value);
|
|
const location = objectValue(intelligence?.location);
|
|
if (!location) return null;
|
|
const latitude = coordinate(location.latitude);
|
|
const longitude = coordinate(location.longitude);
|
|
if (latitude === null || longitude === null || latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
|
|
return null;
|
|
}
|
|
const label = [location.city, location.region, location.countryCode ?? location.country]
|
|
.filter((part): part is string => typeof part === "string" && part.trim().length > 0)
|
|
.join(", ");
|
|
return { latitude, longitude, label: label || "Approximate location unavailable" };
|
|
}
|
|
|
|
export function groupMapLocations<T extends {
|
|
latitude: number;
|
|
longitude: number;
|
|
nickname: string;
|
|
}>(locations: T[]) {
|
|
const grouped = new Map<string, { latitude: number; longitude: number; locations: T[] }>();
|
|
for (const location of locations) {
|
|
const roundedLatitude = Number(location.latitude.toFixed(2));
|
|
const latitude = roundedLatitude === 0 ? 0 : roundedLatitude;
|
|
const roundedLongitude = Number(location.longitude.toFixed(2));
|
|
const longitude = Math.abs(roundedLongitude) === 180 ? -180 : roundedLongitude;
|
|
const key = `${latitude}:${longitude}`;
|
|
const group = grouped.get(key);
|
|
if (group) group.locations.push(location);
|
|
else grouped.set(key, { latitude, longitude, locations: [location] });
|
|
}
|
|
return [...grouped.entries()].map(([key, group]) => ({
|
|
key,
|
|
latitude: group.latitude,
|
|
longitude: group.longitude,
|
|
count: group.locations.length,
|
|
nicknames: [...group.locations.map((location) => location.nickname)].sort((left, right) => left.localeCompare(right)),
|
|
locations: group.locations,
|
|
}));
|
|
}
|
|
|
|
export function projectWorldPoint(latitude: number, longitude: number, width: number, height: number) {
|
|
return {
|
|
x: ((longitude + 180) / 360) * width,
|
|
y: ((90 - latitude) / 180) * height,
|
|
};
|
|
}
|