mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Merge branch 'codex/feature/prettier-peer-list'
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { COUNTRY_FLAG_HEIGHT, COUNTRY_FLAG_WIDTH, getCountryFlagPosition, getCountryLabel, normalizeCountryCode } from '../country-flags';
|
||||
|
||||
describe('country-flags', () => {
|
||||
it('places the first sprite cell at the origin', () => {
|
||||
expect(getCountryFlagPosition('ad')).toEqual({ x: 0, y: 0 });
|
||||
});
|
||||
|
||||
it('maps a code to a grid-aligned sprite position', () => {
|
||||
const position = getCountryFlagPosition('us');
|
||||
expect(position).toBeDefined();
|
||||
expect(position!.x % COUNTRY_FLAG_WIDTH).toBe(0);
|
||||
expect(position!.y % COUNTRY_FLAG_HEIGHT).toBe(0);
|
||||
expect(position!.x).toBeGreaterThanOrEqual(0);
|
||||
expect(position!.x).toBeLessThan(16 * COUNTRY_FLAG_WIDTH);
|
||||
});
|
||||
|
||||
// Exact positions from the original 4chan flags.css (the source of flags-1.png).
|
||||
it('matches the known sprite positions from flags.css', () => {
|
||||
expect(getCountryFlagPosition('us')).toEqual({ x: 240, y: 154 });
|
||||
expect(getCountryFlagPosition('ru')).toEqual({ x: 64, y: 132 });
|
||||
expect(getCountryFlagPosition('pl')).toEqual({ x: 128, y: 121 });
|
||||
expect(getCountryFlagPosition('jp')).toEqual({ x: 112, y: 77 });
|
||||
expect(getCountryFlagPosition('de')).toEqual({ x: 176, y: 33 });
|
||||
expect(getCountryFlagPosition('gb')).toEqual({ x: 32, y: 55 });
|
||||
expect(getCountryFlagPosition('br')).toEqual({ x: 240, y: 11 });
|
||||
expect(getCountryFlagPosition('za')).toEqual({ x: 0, y: 176 });
|
||||
});
|
||||
|
||||
it('normalizes uk to gb and rejects unknown codes', () => {
|
||||
expect(normalizeCountryCode('UK')).toBe('gb');
|
||||
expect(normalizeCountryCode('zz')).toBeUndefined();
|
||||
expect(getCountryFlagPosition('zz')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns a human-readable label for ISO codes', () => {
|
||||
expect(getCountryLabel('de')).toBeTruthy();
|
||||
expect(getCountryLabel('zz')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { extractIpv4FromAddress, getApproximateCountryCode, getApproximateLatLon, isPrivateOrReservedIpv4 } from '../peer-geo';
|
||||
|
||||
const addr = (ip: string) => `/ip4/${ip}/tcp/4001/ws/p2p/12D3KooWExample`;
|
||||
|
||||
describe('extractIpv4FromAddress', () => {
|
||||
it('extracts the IPv4 from a multiaddr', () => {
|
||||
expect(extractIpv4FromAddress('/ip4/147.75.84.175/tcp/4001/ws')).toBe('147.75.84.175');
|
||||
});
|
||||
|
||||
it('returns null for IPv6 and plain DNS multiaddrs', () => {
|
||||
expect(extractIpv4FromAddress('/ip6/2606:4700::1111/tcp/4001')).toBeNull();
|
||||
expect(extractIpv4FromAddress('/dns4/relay.example.org/tcp/443/wss')).toBeNull();
|
||||
});
|
||||
|
||||
it('extracts an IPv4 embedded with dashes in a DNS hostname', () => {
|
||||
expect(extractIpv4FromAddress('/dns4/91-234-56-78.host.example/tcp/443/wss')).toBe('91.234.56.78');
|
||||
expect(extractIpv4FromAddress('/dns4/ip-203-0-113-9.provider.net/tcp/443/wss')).toBe('203.0.113.9');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPrivateOrReservedIpv4', () => {
|
||||
it('flags private and reserved ranges', () => {
|
||||
for (const ip of ['10.0.0.1', '172.16.5.4', '192.168.1.10', '127.0.0.1', '169.254.1.1', '100.64.0.1', '0.0.0.0', '239.255.0.1']) {
|
||||
expect(isPrivateOrReservedIpv4(ip)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('treats public addresses as routable', () => {
|
||||
for (const ip of ['8.8.8.8', '147.75.84.175', '1.1.1.1', '80.80.80.80']) {
|
||||
expect(isPrivateOrReservedIpv4(ip)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApproximateLatLon', () => {
|
||||
it('returns null when the peer cannot be placed offline', () => {
|
||||
expect(getApproximateLatLon('/ip4/192.168.1.5/tcp/4001')).toBeNull();
|
||||
expect(getApproximateLatLon('/dns4/relay.example.org/tcp/443/wss')).toBeNull();
|
||||
expect(getApproximateLatLon('/ip6/2606:4700::1111/tcp/4001')).toBeNull();
|
||||
});
|
||||
|
||||
it('is deterministic for the same address', () => {
|
||||
expect(getApproximateLatLon(addr('8.8.8.8'))).toEqual(getApproximateLatLon(addr('8.8.8.8')));
|
||||
});
|
||||
|
||||
it('places addresses in the expected continental region', () => {
|
||||
const na = getApproximateLatLon(addr('8.8.8.8'));
|
||||
expect(na?.lon).toBeLessThan(-80); // North America
|
||||
expect(na?.lat).toBeGreaterThan(30);
|
||||
|
||||
const eu = getApproximateLatLon(addr('80.80.80.80'));
|
||||
expect(eu?.lon).toBeGreaterThan(0);
|
||||
expect(eu?.lon).toBeLessThan(30);
|
||||
expect(eu?.lat).toBeGreaterThan(40);
|
||||
|
||||
const as = getApproximateLatLon(addr('1.1.1.1'));
|
||||
expect(as?.lon).toBeGreaterThan(90);
|
||||
|
||||
const af = getApproximateLatLon(addr('41.0.0.1'));
|
||||
expect(af?.lon).toBeGreaterThan(8);
|
||||
expect(af?.lon).toBeLessThan(34);
|
||||
expect(af?.lat).toBeLessThan(12);
|
||||
|
||||
const sa = getApproximateLatLon(addr('200.0.0.1'));
|
||||
expect(sa?.lon).toBeLessThan(-45);
|
||||
expect(sa?.lat).toBeLessThan(-5);
|
||||
});
|
||||
|
||||
it('stays within valid coordinate bounds', () => {
|
||||
const loc = getApproximateLatLon(addr('203.0.113.7'));
|
||||
expect(loc).not.toBeNull();
|
||||
expect(loc!.lat).toBeGreaterThanOrEqual(-85);
|
||||
expect(loc!.lat).toBeLessThanOrEqual(85);
|
||||
expect(loc!.lon).toBeGreaterThanOrEqual(-180);
|
||||
expect(loc!.lon).toBeLessThanOrEqual(180);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApproximateCountryCode', () => {
|
||||
it('returns undefined when the peer cannot be placed offline', () => {
|
||||
expect(getApproximateCountryCode('/ip4/10.0.0.1/tcp/4001')).toBeUndefined();
|
||||
expect(getApproximateCountryCode('/dns4/relay.example.org/tcp/443/wss')).toBeUndefined();
|
||||
expect(getApproximateCountryCode('/ip6/2606:4700::1111/tcp/4001')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('is deterministic and returns a known 2-letter code for public peers', () => {
|
||||
const code = getApproximateCountryCode(addr('8.8.8.8'));
|
||||
expect(code).toBe(getApproximateCountryCode(addr('8.8.8.8')));
|
||||
expect(code).toMatch(/^[a-z]{2}$/);
|
||||
});
|
||||
|
||||
it('derives a code from an IPv4 embedded in a DNS hostname', () => {
|
||||
expect(getApproximateCountryCode('/dns4/91-234-56-78.host.example/tcp/443/wss')).toMatch(/^[a-z]{2}$/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,300 @@
|
||||
// Country flag sprite mapping for /assets/icons/flags-1.png.
|
||||
//
|
||||
// The sprite packs 16x11px flags in a 16-column, row-major grid; a flag's sprite
|
||||
// position is its index in COUNTRY_FLAG_CODES, so this order must match the PNG
|
||||
// exactly. This order is taken verbatim from the original 4chan flags.css sprite
|
||||
// (flags.8.png), which is the source of this PNG.
|
||||
|
||||
export const COUNTRY_FLAG_WIDTH = 16;
|
||||
export const COUNTRY_FLAG_HEIGHT = 11;
|
||||
const COUNTRY_FLAG_COLUMNS = 16;
|
||||
|
||||
const COUNTRY_FLAG_CODES = [
|
||||
'ad',
|
||||
'ae',
|
||||
'af',
|
||||
'ag',
|
||||
'ai',
|
||||
'al',
|
||||
'am',
|
||||
'an',
|
||||
'ao',
|
||||
'aq',
|
||||
'ar',
|
||||
'as',
|
||||
'at',
|
||||
'au',
|
||||
'aw',
|
||||
'ax',
|
||||
'az',
|
||||
'ba',
|
||||
'bb',
|
||||
'bd',
|
||||
'be',
|
||||
'bf',
|
||||
'bg',
|
||||
'bh',
|
||||
'bi',
|
||||
'bj',
|
||||
'bl',
|
||||
'bm',
|
||||
'bn',
|
||||
'bo',
|
||||
'bq',
|
||||
'br',
|
||||
'bs',
|
||||
'bt',
|
||||
'bv',
|
||||
'bw',
|
||||
'by',
|
||||
'bz',
|
||||
'ca',
|
||||
'catalonia',
|
||||
'cc',
|
||||
'cd',
|
||||
'cf',
|
||||
'cg',
|
||||
'ch',
|
||||
'ci',
|
||||
'ck',
|
||||
'cl',
|
||||
'cm',
|
||||
'cn',
|
||||
'co',
|
||||
'cr',
|
||||
'cs',
|
||||
'cu',
|
||||
'cv',
|
||||
'cw',
|
||||
'cx',
|
||||
'cy',
|
||||
'cz',
|
||||
'de',
|
||||
'dj',
|
||||
'dk',
|
||||
'dm',
|
||||
'do',
|
||||
'dz',
|
||||
'ec',
|
||||
'ee',
|
||||
'eg',
|
||||
'eh',
|
||||
'xe',
|
||||
'er',
|
||||
'es',
|
||||
'et',
|
||||
'eu',
|
||||
'fam',
|
||||
'fi',
|
||||
'fj',
|
||||
'fk',
|
||||
'fm',
|
||||
'fo',
|
||||
'fr',
|
||||
'ga',
|
||||
'gb',
|
||||
'gd',
|
||||
'ge',
|
||||
'gf',
|
||||
'gg',
|
||||
'gh',
|
||||
'gi',
|
||||
'gl',
|
||||
'gm',
|
||||
'gn',
|
||||
'gp',
|
||||
'gq',
|
||||
'gr',
|
||||
'gs',
|
||||
'gt',
|
||||
'gu',
|
||||
'gw',
|
||||
'gy',
|
||||
'hk',
|
||||
'hm',
|
||||
'hn',
|
||||
'hr',
|
||||
'ht',
|
||||
'hu',
|
||||
'id',
|
||||
'ie',
|
||||
'il',
|
||||
'im',
|
||||
'in',
|
||||
'io',
|
||||
'iq',
|
||||
'ir',
|
||||
'is',
|
||||
'it',
|
||||
'je',
|
||||
'jm',
|
||||
'jo',
|
||||
'jp',
|
||||
'ke',
|
||||
'kg',
|
||||
'kh',
|
||||
'ki',
|
||||
'km',
|
||||
'kn',
|
||||
'kp',
|
||||
'kr',
|
||||
'kw',
|
||||
'ky',
|
||||
'kz',
|
||||
'la',
|
||||
'lb',
|
||||
'lc',
|
||||
'li',
|
||||
'lk',
|
||||
'lr',
|
||||
'ls',
|
||||
'lt',
|
||||
'lu',
|
||||
'lv',
|
||||
'ly',
|
||||
'ma',
|
||||
'mc',
|
||||
'md',
|
||||
'me',
|
||||
'mf',
|
||||
'mg',
|
||||
'mh',
|
||||
'mk',
|
||||
'ml',
|
||||
'mm',
|
||||
'mn',
|
||||
'mo',
|
||||
'mp',
|
||||
'mq',
|
||||
'mr',
|
||||
'ms',
|
||||
'mt',
|
||||
'mu',
|
||||
'mv',
|
||||
'mw',
|
||||
'mx',
|
||||
'my',
|
||||
'mz',
|
||||
'na',
|
||||
'nc',
|
||||
'ne',
|
||||
'nf',
|
||||
'ng',
|
||||
'ni',
|
||||
'nl',
|
||||
'no',
|
||||
'np',
|
||||
'nr',
|
||||
'nu',
|
||||
'nz',
|
||||
'om',
|
||||
'pa',
|
||||
'pe',
|
||||
'pf',
|
||||
'pg',
|
||||
'ph',
|
||||
'pk',
|
||||
'pl',
|
||||
'pm',
|
||||
'pn',
|
||||
'pr',
|
||||
'ps',
|
||||
'pt',
|
||||
'pw',
|
||||
'py',
|
||||
'qa',
|
||||
're',
|
||||
'ro',
|
||||
'rs',
|
||||
'ru',
|
||||
'rw',
|
||||
'sa',
|
||||
'sb',
|
||||
'sc',
|
||||
'xs',
|
||||
'sd',
|
||||
'se',
|
||||
'sg',
|
||||
'sh',
|
||||
'si',
|
||||
'sj',
|
||||
'sk',
|
||||
'sl',
|
||||
'sm',
|
||||
'sn',
|
||||
'so',
|
||||
'sr',
|
||||
'ss',
|
||||
'st',
|
||||
'sv',
|
||||
'sx',
|
||||
'sy',
|
||||
'sz',
|
||||
'tc',
|
||||
'td',
|
||||
'tf',
|
||||
'tg',
|
||||
'th',
|
||||
'tj',
|
||||
'tk',
|
||||
'tl',
|
||||
'tm',
|
||||
'tn',
|
||||
'to',
|
||||
'tr',
|
||||
'tt',
|
||||
'tv',
|
||||
'tw',
|
||||
'tz',
|
||||
'ua',
|
||||
'ug',
|
||||
'um',
|
||||
'us',
|
||||
'uy',
|
||||
'uz',
|
||||
'va',
|
||||
'vc',
|
||||
've',
|
||||
'vg',
|
||||
'vi',
|
||||
'vn',
|
||||
'vu',
|
||||
'xw',
|
||||
'wf',
|
||||
'ws',
|
||||
'xk',
|
||||
'xx',
|
||||
'ye',
|
||||
'yt',
|
||||
'za',
|
||||
'zm',
|
||||
'zw',
|
||||
] as const;
|
||||
|
||||
const COUNTRY_FLAG_INDEX_BY_CODE: ReadonlyMap<string, number> = new Map(COUNTRY_FLAG_CODES.map((code, index) => [code, index]));
|
||||
|
||||
const countryDisplayNames = typeof Intl.DisplayNames === 'function' ? new Intl.DisplayNames(undefined, { type: 'region' }) : undefined;
|
||||
|
||||
export const normalizeCountryCode = (value: string | undefined) => {
|
||||
const code = value?.trim().toLowerCase();
|
||||
if (!code) return undefined;
|
||||
if (code === 'uk') return 'gb';
|
||||
return COUNTRY_FLAG_INDEX_BY_CODE.has(code) ? code : undefined;
|
||||
};
|
||||
|
||||
export const getCountryFlagPosition = (value: string | undefined): { x: number; y: number } | undefined => {
|
||||
const code = normalizeCountryCode(value);
|
||||
const index = code === undefined ? undefined : COUNTRY_FLAG_INDEX_BY_CODE.get(code);
|
||||
if (index === undefined) return undefined;
|
||||
return {
|
||||
x: (index % COUNTRY_FLAG_COLUMNS) * COUNTRY_FLAG_WIDTH,
|
||||
y: Math.floor(index / COUNTRY_FLAG_COLUMNS) * COUNTRY_FLAG_HEIGHT,
|
||||
};
|
||||
};
|
||||
|
||||
export const getCountryLabel = (value: string | undefined) => {
|
||||
const code = normalizeCountryCode(value);
|
||||
if (!code) return undefined;
|
||||
const regionName = code.length === 2 ? countryDisplayNames?.of(code.toUpperCase()) : undefined;
|
||||
return regionName ?? code.toUpperCase();
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
// Offline, approximate peer geolocation for the P2P stats world map.
|
||||
//
|
||||
// This intentionally avoids any external geolocation API: 5chan is serverless and
|
||||
// privacy-focused, so we must not leak the set of peers a user is connected to.
|
||||
// Instead we map an IPv4 address to its Regional Internet Registry (RIR) region at
|
||||
// continent resolution, using the coarse IANA /8 allocation table below. Positions
|
||||
// are therefore approximate ("roughly which continent"), not precise coordinates.
|
||||
|
||||
export type LatLon = { lat: number; lon: number };
|
||||
|
||||
type Region = 'AF' | 'AS' | 'EU' | 'NA' | 'SA';
|
||||
|
||||
const REGION_CENTROIDS: Record<Region, LatLon> = {
|
||||
AF: { lat: 4, lon: 21 },
|
||||
AS: { lat: 30, lon: 105 },
|
||||
EU: { lat: 50, lon: 15 },
|
||||
NA: { lat: 39, lon: -97 },
|
||||
SA: { lat: -15, lon: -58 },
|
||||
};
|
||||
|
||||
// Approximate first-octet (/8) -> RIR region, applied in order (later wins).
|
||||
// Coarse and not authoritative; uncovered octets fall back to NA (ARIN-heavy
|
||||
// legacy space). Good enough for a continent-level dot on a map.
|
||||
const REGION_RANGES: [number, number, Region][] = [
|
||||
// APNIC (Asia / Pacific)
|
||||
[1, 1, 'AS'],
|
||||
[14, 14, 'AS'],
|
||||
[27, 27, 'AS'],
|
||||
[36, 36, 'AS'],
|
||||
[39, 39, 'AS'],
|
||||
[42, 43, 'AS'],
|
||||
[49, 49, 'AS'],
|
||||
[58, 61, 'AS'],
|
||||
[101, 103, 'AS'],
|
||||
[106, 106, 'AS'],
|
||||
[110, 126, 'AS'],
|
||||
[133, 133, 'AS'],
|
||||
[150, 153, 'AS'],
|
||||
[163, 163, 'AS'],
|
||||
[171, 171, 'AS'],
|
||||
[175, 175, 'AS'],
|
||||
[180, 183, 'AS'],
|
||||
[202, 203, 'AS'],
|
||||
[210, 211, 'AS'],
|
||||
[218, 223, 'AS'],
|
||||
// RIPE NCC (Europe / Middle East)
|
||||
[2, 2, 'EU'],
|
||||
[5, 5, 'EU'],
|
||||
[25, 25, 'EU'],
|
||||
[31, 31, 'EU'],
|
||||
[37, 37, 'EU'],
|
||||
[46, 46, 'EU'],
|
||||
[51, 51, 'EU'],
|
||||
[53, 53, 'EU'],
|
||||
[57, 57, 'EU'],
|
||||
[62, 62, 'EU'],
|
||||
[77, 95, 'EU'],
|
||||
[109, 109, 'EU'],
|
||||
[141, 141, 'EU'],
|
||||
[145, 145, 'EU'],
|
||||
[151, 151, 'EU'],
|
||||
[176, 176, 'EU'],
|
||||
[178, 178, 'EU'],
|
||||
[185, 185, 'EU'],
|
||||
[188, 188, 'EU'],
|
||||
[193, 195, 'EU'],
|
||||
[212, 213, 'EU'],
|
||||
[217, 217, 'EU'],
|
||||
// AFRINIC (Africa)
|
||||
[41, 41, 'AF'],
|
||||
[102, 102, 'AF'],
|
||||
[105, 105, 'AF'],
|
||||
[154, 156, 'AF'],
|
||||
[196, 197, 'AF'],
|
||||
// LACNIC (Latin America / Caribbean)
|
||||
[177, 177, 'SA'],
|
||||
[179, 179, 'SA'],
|
||||
[181, 181, 'SA'],
|
||||
[186, 187, 'SA'],
|
||||
[189, 191, 'SA'],
|
||||
[200, 201, 'SA'],
|
||||
];
|
||||
|
||||
const REGION_BY_OCTET: Region[] = (() => {
|
||||
const table: Region[] = Array.from({ length: 256 }, () => 'NA' as Region);
|
||||
for (const [start, end, region] of REGION_RANGES) {
|
||||
for (let octet = start; octet <= end; octet++) table[octet] = region;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
export const extractIpv4FromAddress = (address: string): string | null => {
|
||||
const direct = /\/ip4\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/.exec(address);
|
||||
if (direct) return direct[1];
|
||||
// Some peers are reached via a DNS name that embeds the IPv4 with dashes, e.g.
|
||||
// /dns4/91-234-56-78.host.example -> 91.234.56.78
|
||||
const dashed = /\/dns[46]?\/[^/]*?(?<!\d)(\d{1,3})-(\d{1,3})-(\d{1,3})-(\d{1,3})(?![\d-])/.exec(address);
|
||||
if (dashed) return `${dashed[1]}.${dashed[2]}.${dashed[3]}.${dashed[4]}`;
|
||||
return null;
|
||||
};
|
||||
|
||||
const parseOctets = (ip: string): number[] | null => {
|
||||
const parts = ip.split('.').map(Number);
|
||||
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;
|
||||
return parts;
|
||||
};
|
||||
|
||||
export const isPrivateOrReservedIpv4 = (ip: string): boolean => {
|
||||
const parts = parseOctets(ip);
|
||||
if (!parts) return true;
|
||||
const [a, b] = parts;
|
||||
if (a === 0 || a === 10 || a === 127) return true; // this-network, private, loopback
|
||||
if (a === 169 && b === 254) return true; // link-local
|
||||
if (a === 172 && b >= 16 && b <= 31) return true; // private
|
||||
if (a === 192 && b === 168) return true; // private
|
||||
if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT
|
||||
if (a >= 224) return true; // multicast / reserved
|
||||
return false;
|
||||
};
|
||||
|
||||
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
|
||||
|
||||
const hashOctets = (parts: number[]) => {
|
||||
let hash = 0;
|
||||
for (const part of parts) hash = (Math.imul(hash, 131) + part) | 0;
|
||||
return Math.abs(hash);
|
||||
};
|
||||
|
||||
// Maps a peer's multiaddr to an approximate lat/lon, or null when it cannot be
|
||||
// placed offline (private/reserved IPv4, IPv6, or a DNS address).
|
||||
export const getApproximateLatLon = (address: string): LatLon | null => {
|
||||
const ip = extractIpv4FromAddress(address);
|
||||
if (!ip || isPrivateOrReservedIpv4(ip)) return null;
|
||||
const parts = parseOctets(ip);
|
||||
if (!parts) return null;
|
||||
|
||||
const centroid = REGION_CENTROIDS[REGION_BY_OCTET[parts[0]]];
|
||||
const hash = hashOctets(parts);
|
||||
// Deterministic intra-region spread so peers don't stack on one point. The
|
||||
// region is the meaningful signal; the offset is cosmetic.
|
||||
const lonOffset = ((hash % 1000) / 1000 - 0.5) * 24; // +/- 12 deg
|
||||
const latOffset = ((Math.floor(hash / 1000) % 1000) / 1000 - 0.5) * 14; // +/- 7 deg
|
||||
return { lat: clamp(centroid.lat + latOffset, -85, 85), lon: clamp(centroid.lon + lonOffset, -180, 180) };
|
||||
};
|
||||
|
||||
// Representative countries per region. The RIR table only resolves to a continent,
|
||||
// so the flag is a deterministic, approximate pick from the region's common
|
||||
// countries — consistent with the map's "approximate locations" framing, not real
|
||||
// per-peer country geolocation.
|
||||
const REGION_COUNTRIES: Record<Region, string[]> = {
|
||||
AF: ['za', 'ng', 'eg', 'ke', 'ma', 'dz', 'tn'],
|
||||
AS: ['jp', 'cn', 'sg', 'in', 'kr', 'hk', 'id', 'vn', 'th', 'tw'],
|
||||
EU: ['de', 'nl', 'fr', 'gb', 'ru', 'se', 'fi', 'pl', 'it', 'es', 'ua', 'ro'],
|
||||
NA: ['us', 'us', 'ca', 'us', 'mx'],
|
||||
SA: ['br', 'ar', 'cl', 'co', 'pe'],
|
||||
};
|
||||
|
||||
// Approximate 2-letter country code for a peer, or undefined when it cannot be
|
||||
// placed offline. See REGION_COUNTRIES: this is region-level, not precise.
|
||||
export const getApproximateCountryCode = (address: string): string | undefined => {
|
||||
const ip = extractIpv4FromAddress(address);
|
||||
if (!ip || isPrivateOrReservedIpv4(ip)) return undefined;
|
||||
const parts = parseOctets(ip);
|
||||
if (!parts) return undefined;
|
||||
const pool = REGION_COUNTRIES[REGION_BY_OCTET[parts[0]]];
|
||||
return pool[hashOctets(parts) % pool.length];
|
||||
};
|
||||
Reference in New Issue
Block a user