half way there

This commit is contained in:
2026-02-03 04:24:11 +01:00
parent 991cd10f40
commit 2973e038d6
274 changed files with 992 additions and 964 deletions
+57
View File
@@ -0,0 +1,57 @@
import type { GeoApiResponse, HostInfo } from '@/utils/types';
import { codes } from '@/utils/codes';
const CACHE = new Map<string, GeoApiResponse>();
export const GeoService = {
async resolve(ip: string): Promise<GeoApiResponse | null> {
if (CACHE.has(ip)) return CACHE.get(ip)!;
try {
const res = await fetch(`https://ip.albert.lol/${ip}`);
if (!res.ok) throw new Error(`Geo API Error: ${res.status}`);
const data: GeoApiResponse = await res.json();
CACHE.set(ip, data);
if (CACHE.size > 100) {
const firstKey = CACHE.keys().next().value;
if (firstKey) CACHE.delete(firstKey);
}
return data;
} catch (error) {
console.error('Geo lookup failed', error);
return null;
}
},
mapToHostInfo(url: string, ip: string, geo: GeoApiResponse | null): HostInfo {
const urlObj = new URL(url);
const info: HostInfo = {
url,
domain: urlObj.hostname,
loading: false,
error: null,
isBrowserResource: false,
network: {
ip,
hostname: geo?.hostname || urlObj.hostname,
asn: geo?.org?.split(' ')[0] || null,
org: geo?.org || 'Unknown Organization',
isLocal: false,
isBogon: geo?.bogon || false,
},
location: {
countryCode: geo?.country || null,
countryName: geo?.country ? (codes[geo.country.toLowerCase()] || geo.country) : null,
city: geo?.city || null,
region: geo?.region || null,
timezone: geo?.timezone || null,
}
};
return info;
}
};
+69
View File
@@ -0,0 +1,69 @@
import { browser } from 'wxt/browser';
const ICON_CACHE = new Map<string, Record<string, ImageData>>();
export const IconService = {
async update(tabId: number, countryCode: string | null, isLocal: boolean) {
try {
if (isLocal) {
const path = '/icon/128.png' as string;
await browser.action.setIcon({ tabId, path });
return;
}
const code = countryCode ? countryCode.toLowerCase() : 'unknown';
const imageData = await this.getIconData(code);
await browser.action.setIcon({ tabId, imageData });
} catch (e) {
console.warn('Failed to update icon', e);
}
},
async getIconData(code: string): Promise<Record<string, ImageData>> {
if (ICON_CACHE.has(code)) return ICON_CACHE.get(code)!;
const path = `/${code}.webp`;
const url = browser.runtime.getURL(path as any);
try {
const resp = await fetch(url);
if (!resp.ok) throw new Error('Icon not found');
const blob = await resp.blob();
const bitmap = await createImageBitmap(blob);
const data = await this.processBitmap(bitmap);
ICON_CACHE.set(code, data);
return data;
} catch {
// Fallback to unknown if specific flag fails
if (code !== 'unknown') return this.getIconData('unknown');
throw new Error('Failed to load fallback icon');
}
},
async processBitmap(bitmap: ImageBitmap): Promise<Record<string, ImageData>> {
const canvas = new OffscreenCanvas(128, 128);
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
// Center and contain
const ratio = Math.min(canvas.width / bitmap.width, canvas.height / bitmap.height);
const offsetX = (canvas.width - bitmap.width * ratio) / 2;
const offsetY = (canvas.height - bitmap.height * ratio) / 2;
ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, offsetX, offsetY, bitmap.width * ratio, bitmap.height * ratio);
const sizes = [16, 32, 48, 128];
const result: Record<string, ImageData> = {};
for (const size of sizes) {
const sCanvas = new OffscreenCanvas(size, size);
const sCtx = sCanvas.getContext('2d')!;
sCtx.drawImage(canvas, 0, 0, size, size);
result[size] = sCtx.getImageData(0, 0, size, size);
}
return result;
}
};
+77
View File
@@ -0,0 +1,77 @@
import { IpUtils } from '@/utils/ip';
import { StorageService } from '@/utils/storage';
import { GeoService } from './geo';
import { IconService } from './icon';
import type { HostInfo } from '@/utils/types';
export const Tab = {
/**
* Main entry point to process a tab's data
*/
async process(tabId: number, url: string, ip?: string) {
// Initial State (Loading)
const initialState: HostInfo = {
url,
domain: new URL(url).hostname,
loading: true,
error: null,
network: null,
location: null,
isBrowserResource: false
};
// Don't overwrite if we already have data for this exact URL,
// but do update if we have a new IP
const existing = await StorageService.get(tabId);
if (!existing || existing.url !== url) {
await StorageService.set(tabId, initialState);
}
// Resolve IP if missing
if (!ip) {
return;
}
// Handle Local/Private IPs
if (IpUtils.isLocalOrBogon(ip)) {
const localInfo: HostInfo = {
...initialState,
loading: false,
network: {
ip,
hostname: 'Local Network',
asn: null,
org: 'Private Network',
isLocal: true,
isBogon: false
},
location: null
};
await StorageService.set(tabId, localInfo);
await IconService.update(tabId, null, true);
return;
}
// Fetch Public Data
const geoData = await GeoService.resolve(ip);
if (!geoData) {
await StorageService.set(tabId, { ...initialState, loading: false, error: 'Failed to fetch host info' });
return;
}
// Save Final State
const finalInfo = GeoService.mapToHostInfo(url, ip, geoData);
await StorageService.set(tabId, finalInfo);
// Determine Icon
const asn = finalInfo.network?.asn;
let iconCode = finalInfo.location?.countryCode || null;
if (asn === 'AS13335') {
iconCode = 'cloudflare';
}
await IconService.update(tabId, iconCode, false);
}
};