From 79807c1dddf061899462a3986d34061fdce93870 Mon Sep 17 00:00:00 2001 From: Sandwich <299465+dskvr@users.noreply.github.com> Date: Fri, 24 Jan 2025 08:40:55 +0700 Subject: [PATCH] begin fixing agents --- libraries/nocap-route66/README.md | 0 libraries/nocap-route66/package.json | 69 +++++ libraries/nocap-route66/src/Transform.test.ts | 180 +++++++++++++ libraries/nocap-route66/src/Transform.ts | 126 +++++++++ libraries/nocap-route66/src/index.ts | 2 + libraries/nocap-route66/src/kinds/0.ts | 65 +++++ libraries/nocap-route66/src/kinds/10166.ts | 133 ++++++++++ .../nocap-route66/src/kinds/30166.test.ts | 196 ++++++++++++++ libraries/nocap-route66/src/kinds/30166.ts | 249 ++++++++++++++++++ libraries/nocap-route66/src/kinds/index.ts | 2 + libraries/nocap-route66/src/types/global.d.ts | 16 ++ libraries/nocap-route66/tsconfig.json | 24 ++ libraries/nocap-route66/webpack.config.js | 65 +++++ 13 files changed, 1127 insertions(+) create mode 100644 libraries/nocap-route66/README.md create mode 100644 libraries/nocap-route66/package.json create mode 100644 libraries/nocap-route66/src/Transform.test.ts create mode 100644 libraries/nocap-route66/src/Transform.ts create mode 100644 libraries/nocap-route66/src/index.ts create mode 100644 libraries/nocap-route66/src/kinds/0.ts create mode 100644 libraries/nocap-route66/src/kinds/10166.ts create mode 100644 libraries/nocap-route66/src/kinds/30166.test.ts create mode 100644 libraries/nocap-route66/src/kinds/30166.ts create mode 100644 libraries/nocap-route66/src/kinds/index.ts create mode 100644 libraries/nocap-route66/src/types/global.d.ts create mode 100644 libraries/nocap-route66/tsconfig.json create mode 100644 libraries/nocap-route66/webpack.config.js diff --git a/libraries/nocap-route66/README.md b/libraries/nocap-route66/README.md new file mode 100644 index 00000000..e69de29b diff --git a/libraries/nocap-route66/package.json b/libraries/nocap-route66/package.json new file mode 100644 index 00000000..734dbe4c --- /dev/null +++ b/libraries/nocap-route66/package.json @@ -0,0 +1,69 @@ +{ + "name": "@nostrwatch/nocap-route66", + "type": "module", + "version": "0.0.1", + "description": "A library for transforming NOCAP output into Nostr events of kind 30166", + "entry": "src/index.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/node/index.js", + "types": "./dist/index.d.ts" + }, + "./web": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./umd": { + "import": "./dist/umd/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsc && tsc-alias && webpack --config webpack.config.js", + "build:browser": "webpack --config webpack.config.js --env target=web", + "build:node": "webpack --config webpack.config.js --env target=node", + "test": "vitest", + "lint": "eslint . --ext .ts", + "clean": "rimraf dist" + }, + "repository": { + "type": "git", + "url": "https://github.com/yourusername/nocapd-route66.git" + }, + "keywords": [ + "nostr", + "route66", + "nocap", + "event", + "library" + ], + "author": "Your Name ", + "license": "MIT", + "dependencies": { + "@nostrwatch/logger": "*", + "@nostrwatch/nocap": "workspace:^", + "nostr-geotags": "0.7.1", + "nostr-tools": "2.7.2" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "eslint": "^8.0.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "prettier": "^3.4.2", + "rimraf": "^5.0.0", + "ts-loader": "^9.0.0", + "tsc-alias": "^1.8.10", + "typescript": "^5.0.0", + "vitest": "^0.34.1", + "webpack": "^5.0.0", + "webpack-cli": "^5.0.0" + }, + "files": [ + "dist/**/*" + ], + "engines": { + "node": ">=14.0.0" + } +} diff --git a/libraries/nocap-route66/src/Transform.test.ts b/libraries/nocap-route66/src/Transform.test.ts new file mode 100644 index 00000000..9ae55536 --- /dev/null +++ b/libraries/nocap-route66/src/Transform.test.ts @@ -0,0 +1,180 @@ +// Transform.test.ts +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Transform } from './Transform.js'; + +// Mock dependencies if necessary +vi.mock('@nostrwatch/logger', () => ({ + Logger: class { + err = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + }, + default: class { + err = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + } +})); + +vi.mock('nostr-tools', () => ({ + getEventHash: vi.fn().mockReturnValue('mocked_event_hash'), +})); + +describe('Transform Class', () => { + const pubkey = 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; + let baseInstance: Transform; + + // Create a concrete subclass for testing + class TestBase extends Transform { + constructor(kind: number, pubkey: string) { + super(kind, pubkey); + } + + generateTags(check: any): string[][] { + return [['tag1', 'value1'], ['tag2', 'value2']]; + } + } + + beforeEach(() => { + baseInstance = new TestBase(9999, pubkey); + }); + + describe('Initialization', () => { + it('should create an instance of Transform', () => { + expect(baseInstance).toBeInstanceOf(Transform); + expect(baseInstance.kind).toBe(9999); + expect(baseInstance.pubkey).toBe(pubkey); + expect(baseInstance.logger).toBeDefined(); + }); + + it('should throw an error if kind is undefined', () => { + expect(() => new TestBase(undefined as unknown as number, pubkey)).toThrow( + 'Kind must be defined' + ); + }); + + it('should throw an error if pubkey is undefined', () => { + expect(() => new TestBase(9999, undefined as unknown as string)).toThrow( + 'DAEMON_PUBKEY must be defined' + ); + }); + }); + + describe('tpl()', () => { + it('should return a template with default values', () => { + const tpl = baseInstance.tpl(); + expect(tpl).toEqual({ + id: null, + pubkey, + kind: 9999, + created_at: expect.any(Number), + tags: [], + content: '', + }); + }); + + it('should include data if provided', () => { + const data = { + checked_at: 1620000000, + content: 'Test Content', + tags: [['test', 'tag']], + }; + const tpl = baseInstance.tpl(data); + expect(tpl).toEqual({ + id: null, + pubkey, + kind: 9999, + created_at: 1620000000, + tags: [['test', 'tag']], + content: 'Test Content', + }); + }); + }); + + describe('generateEvent()', () => { + it('should generate an event with correct properties', () => { + const data = { + url: 'wss://example.com', + info: { data: { key: 'value' } }, + }; + const event = baseInstance.generateEvent(data); + expect(event.id).toBe('mocked_event_hash'); + expect(event.pubkey).toBe(pubkey); + expect(event.kind).toBe(9999); + expect(event.created_at).toBeGreaterThan(0); + expect(event.tags).toEqual([['tag1', 'value1'], ['tag2', 'value2']]); + expect(event.content).toBe(JSON.stringify({ key: 'value' })); + }); + + it('should handle errors in content serialization', () => { + const data = { + url: "wss://someurl.xyz", + info: { data: undefined }, + }; + // Simulate error in JSON.stringify + const originalStringify = JSON.stringify; + JSON.stringify = () => { + throw new Error('Serialization error'); + }; + + const event = baseInstance.generateEvent(data); + expect(event.content).toBe('{}'); + // Restore JSON.stringify + JSON.stringify = originalStringify; + }); + }); + + describe('dedupLabels()', () => { + it('should deduplicate labels correctly', () => { + const tags = [ + ['L', 'label1'], + ['L', 'label2'], + ['l', 'value1', 'label1'], + ['l', 'value2', 'label1'], + ['l', 'value1', 'label1'], // Duplicate + ['L', 'label2'], + ['l', 'value3', 'label2'], + ]; + const dedupedTags = baseInstance.dedupLabels(tags); + console.log('dedupedTags', dedupedTags); + expect(dedupedTags).toEqual(expect.arrayContaining([ + ['L', 'label1'], + ['L', 'label2'], + ['l', 'value1', 'label1'], + ['l', 'value2', 'label1'], + ['l', 'value3', 'label2'], + ])); + }); + }); + + + describe('removeLabels()', () => { + it('should remove labels correctly', () => { + const tags = [ + ['L', 'label1'], + ['l', 'value1', 'label1'], + ['t', 'tag1'], + ['l', 'value2', 'label1'], + ['L', 'label2'], + ['p', 'pubkey'], + ]; + const filteredTags = baseInstance.removeLabels(tags); + expect(filteredTags).toEqual([ + ['t', 'tag1'], + ['p', 'pubkey'], + ]); + }); + }); + + describe('json()', () => { + it('should return the current event', () => { + const data = { + url: 'wss://example.com', + }; + baseInstance.generateEvent(data); + const event = baseInstance.json(); + expect(event).toBeDefined(); + expect(event.id).toBe('mocked_event_hash'); + }); + }); +}); diff --git a/libraries/nocap-route66/src/Transform.ts b/libraries/nocap-route66/src/Transform.ts new file mode 100644 index 00000000..2a49d267 --- /dev/null +++ b/libraries/nocap-route66/src/Transform.ts @@ -0,0 +1,126 @@ +// KindBase.ts +import { + getEventHash, +} from 'nostr-tools'; +import Logger from '@nostrwatch/logger'; +import { type IResult } from '@nostrwatch/nocap'; + + +export interface ITransform { + generateEvent(data: IResult): any; + generateTags(data: IResult): string[][]; + dedupLabels(tags: string[][]): string[][]; + removeLabels(tags: string[][]): string[][]; +} + +export abstract class Transform { + logger: Logger; + event: any; + + constructor(public kind: number, public pubkey: string) { + if (!kind) { + throw new Error('Kind must be defined'); + } + if (!pubkey) { + throw new Error('DAEMON_PUBKEY must be defined'); + } + this.logger = new Logger(`@nostrwatch/publisher/event: ${kind}`); + } + + tpl(data?: any) { + const id = null; + const pubkey = this.pubkey; + const kind = this.kind; + const created_at = data?.checked_at + ? data.checked_at + : Math.round(Date.now() / 1000); + const content = data?.content ?? ''; + const tags = data?.tags ?? []; + + return { id, pubkey, kind, created_at, tags, content }; + } + + json() { + return this.event; + } + + dedupLabels(tags: string[][]) { + const labels = new Set(); + let deduped = tags.filter((tag) => { + if (tag[0] === 'L') { + if (labels.has(tag[1])) { + return false; + } + labels.add(tag[1]); + } + return true; + }); + + const lTags = new Map(); + deduped.forEach((tag) => { + if (tag[0] === 'l') { + const label = tag[2]; + const value = tag[1]; + if (!lTags.has(label)) { + lTags.set(label, new Set()); + } + const labelMap = lTags.get(label) + if (labelMap.has(value)) { + return; + } + labelMap.add(value); + lTags.set(label, labelMap); + } + }); + + deduped = deduped.filter( tag => tag[0] !== 'l'); + + deduped.push( + ...Array.from(lTags.entries()).flatMap(([key, values]) => { + return Array.from(values).map((value) => ['l', value, key]); + }) + ); + + return deduped; + } + + removeLabels(tags: string[][]) { + const labels = new Set(); + return tags.filter((tag) => { + if (tag[0] === 'L' || tag[0] === 'l') { + return false + } + return true; + }); + } + + + generateEvent(data: IResult) { + this.event = this._generateEvent(data); + this.event.id = getEventHash(this.event); + return this.event; + } + + protected _generateEvent(data: IResult) { + let content = '{}'; + const tags = this.generateTags(data); + const nip11 = data?.info?.data; + + if (nip11) { + try { + content = JSON.stringify(nip11); + } catch (e) { + this.logger.err(`generateEvent(): Error: ${e}`); + this.logger.info(nip11); + } + } + + return { + ...this.tpl(), + content, + tags, + }; + } + + abstract generateTags(check: IResult): string[][]; +} diff --git a/libraries/nocap-route66/src/index.ts b/libraries/nocap-route66/src/index.ts new file mode 100644 index 00000000..51fab420 --- /dev/null +++ b/libraries/nocap-route66/src/index.ts @@ -0,0 +1,2 @@ +export { Transform } from './Transform.js'; +export { Kind30166 } from './kinds/index.js'; \ No newline at end of file diff --git a/libraries/nocap-route66/src/kinds/0.ts b/libraries/nocap-route66/src/kinds/0.ts new file mode 100644 index 00000000..e5619162 --- /dev/null +++ b/libraries/nocap-route66/src/kinds/0.ts @@ -0,0 +1,65 @@ +// import { IResult } from '@nostrwatch/nocap'; +// import { Transform } from '../Transform'; + +// /** +// * Represents a specific kind of event with additional content generation capabilities. +// */ +// export class Kind0 extends Transform { + + +// constructor(pubkey: string) { +// super(0, pubkey); +// this.discoverable = { pubkey: true }; +// this.human_readable = false; +// this.machine_readable = true; +// } + +// generateTags(check: IResult): string[][] { +// let tags: string[][] = []; +// return tags; +// } + +// /** +// * Generates an event with tags based on provided data. +// * @param {IResult} data The data to generate content from. +// * @returns {object} The generated event. +// */ +// generateEvent(data: IResult): Record { +// let tags: string[][] = []; +// const content = Kind0.generateContent(data); + +// const event = { +// ...this.tpl(), +// content, +// tags +// }; + +// return event; +// } + +// /** +// * Generates content for the event based on provided data. +// * @param {object} data The data to generate content from. +// * @returns {string} The generated content. +// */ +// static generateContent(data: Record): string { +// let content = ""; +// try { +// content = JSON.stringify(data); +// } catch (e) { +// console.dir(`Kind0::generateContent(): Error: ${e}`); +// throw new Error('Was not able to stringify data for kind 0 content field.'); +// } +// if (!content) content = "{}"; +// return content; +// } + +// /** +// * Parses the content of an event. +// * @param {Record} event The event to parse. +// * @returns {any} The parsed content. +// */ +// static parse(event: Record): any { +// return JSON.parse(event.content); +// } +// } diff --git a/libraries/nocap-route66/src/kinds/10166.ts b/libraries/nocap-route66/src/kinds/10166.ts new file mode 100644 index 00000000..5f226f36 --- /dev/null +++ b/libraries/nocap-route66/src/kinds/10166.ts @@ -0,0 +1,133 @@ +import ngeotags from 'nostr-geotags'; +import { type ITransform, Transform } from '../Transform.js'; + +export interface MonitorData { + frequency?: string; + kinds?: number[]; + counts?: number[]; + checks?: string[]; + timeouts?: Record; + geo?: { + data: GeoData[]; + }; +} + +export interface GeoData { + isp?: string; + as?: string; + asname?: string; + [key: string]: any; +} + +/** + * Represents a specific kind of publisher with additional tagging capabilities. + */ +export class Kind10166 extends Transform implements ITransform { + constructor(pubkey: string) { + super(10166, pubkey); + } + + /** + * Generates an event with tags based on provided data. + * @param {MonitorData} data The data to generate tags from. + * @returns {string[][]} The generated tags. + */ + generateTags(data: MonitorData): string[][] { + let tags: string[][] = []; + + tags = this.addFrequencyTags(tags, data); + tags = this.addKindsTags(tags, data); + tags = this.addCountsTags(tags, data); + tags = this.addChecksTags(tags, data); + tags = this.addTimeoutTags(tags, data); + tags = this.addGeoTags(tags, data.geo?.data); + + return tags; + } + + private addFrequencyTags(tags: string[][], data: MonitorData): string[][] { + if (data?.frequency) { + tags.push(['frequency', data.frequency]); + } + return tags; + } + + private addKindsTags(tags: string[][], data: MonitorData): string[][] { + if (data?.kinds) { + data.kinds.map(kind => kind.toString()).forEach(kind => tags.push(['k', kind])); + } + return tags; + } + + private addCountsTags(tags: string[][], data: MonitorData): string[][] { + if (data?.counts) { + data.counts.map(count => count.toString()).forEach(count => tags.push(['n', count])); + } + return tags; + } + + private addChecksTags(tags: string[][], data: MonitorData): string[][] { + if (data?.checks) { + data.checks.map(check => check.toString()).forEach(check => tags.push(['c', check])); + } + return tags; + } + + private addTimeoutTags(tags: string[][], data: MonitorData): string[][] { + if (data?.timeouts) { + Object.keys(data.timeouts || {}).forEach(key => { + if(data?.timeouts?.[key]) { + tags.push(['timeout', key, data.timeouts[key].toString()]); + } + }); + } + return tags; + } + + private addGeoTags(tags: string[][], geoData?: GeoData[]): string[][] { + if (!geoData || !Array.isArray(geoData)) return tags; + + let ispTags: string[][] = []; + let geoTags: string[][] = []; + + geoData.forEach((geo) => { + ispTags = this.addGeoIspTags(ispTags, geo); + geoTags = this.addGeoLocationTags(geoTags, geo); + }); + + geoTags = [...this.removeLabels(geoTags), ...this.dedupLabels(geoTags)]; + ispTags = this.dedupLabels(ispTags); + tags.push(...ispTags, ...geoTags); + return tags; + } + + private addGeoIspTags(tags: string[][], geo: GeoData): string[][] { + const ispFields = [ + { key: 'isp', label: 'host.isp' }, + { key: 'as', label: 'host.as' }, + { key: 'asname', label: 'host.asn' }, + ]; + + ispFields.forEach(({ key, label }) => { + if (geo[key]) { + tags.push(['L', label]); + tags.push(['l', geo[key], label]); + } + }); + + return tags; + } + + private addGeoLocationTags(tags: string[][], geo: GeoData): string[][] { + const gOpts = { + isoAsNamespace: false, + geohash: true, + gps: false, + countryCode: true, + countryName: true, + regionCode: true, + }; + const geoTags = ngeotags(geo, gOpts) as string[][]; + return [...tags, ...geoTags]; + } +} diff --git a/libraries/nocap-route66/src/kinds/30166.test.ts b/libraries/nocap-route66/src/kinds/30166.test.ts new file mode 100644 index 00000000..6e2b7e74 --- /dev/null +++ b/libraries/nocap-route66/src/kinds/30166.test.ts @@ -0,0 +1,196 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Kind30166 } from './30166.js'; +import ngeotags from 'nostr-geotags'; + +vi.mock('nostr-geotags', () => ({ + default: vi.fn(), +})); + +vi.mock('@nostrwatch/logger', () => ({ + Logger: class { + err = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + }, + default: class { + err = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + } +})); + +describe('Kind30166', () => { + const pubkey = 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; + let kind30166: Kind30166; + + beforeEach(() => { + kind30166 = new Kind30166(pubkey); + }); + + describe('Initialization', () => { + it('should create an instance of Kind30166', () => { + expect(kind30166).toBeInstanceOf(Kind30166); + expect(kind30166.kind).toBe(30166); + }); + }); + + describe('generateEvent()', () => { + it('should generate event with empty content if no nip11 data', () => { + const checkData = { + url: 'wss://example.com', + }; + const event = kind30166.generateEvent(checkData); + expect(event.content).toBe('{}'); + expect(event.tags).toBeDefined(); + expect(event.tags.length).toBeGreaterThan(0); + }); + + it('should generate event with nip11 content', () => { + const nip11Data = { name: 'Test NIP11' }; + const checkData = { + url: 'wss://example.com', + info: { data: nip11Data }, + }; + const event = kind30166.generateEvent(checkData); + expect(event.content).toBe(JSON.stringify(nip11Data)); + }); + }); + + describe('generateTags()', () => { + it('should add basic tags correctly', () => { + const checkData = { + url: 'wss://example.com', + open: { duration: 100.5 }, + read: { duration: 200 }, + write: { duration: 300 }, + }; + const tags = kind30166.generateTags(checkData); + expect(tags).toContainEqual(['d', 'wss://example.com']); + expect(tags).toContainEqual(['rtt-open', '101']); + expect(tags).toContainEqual(['rtt-read', '200']); + expect(tags).toContainEqual(['rtt-write', '300']); + }); + + it('should add network tag if present', () => { + const checkData = { + url: 'wss://example.com', + network: 'mainnet', + }; + const tags = kind30166.generateTags(checkData); + expect(tags).toContainEqual(['n', 'mainnet']); + }); + + describe('Info Tags', () => { + it('should handle info tags correctly', () => { + const infoData = { + pubkey: pubkey, + supported_nips: [1, 2], + language_tags: ['en', 'es'], + tags: ['tag1', 'tag2'], + limitation: { auth_required: true, payment_required: false }, + software: 'TestSoftware', + version: '1.0.0', + }; + const checkData = { + url: 'wss://example.com', + info: { data: infoData }, + }; + const tags = kind30166.generateTags(checkData); + + expect(tags).toContainEqual(['p', pubkey]); + expect(tags).toContainEqual(['N', '1']); + expect(tags).toContainEqual(['N', '2']); + expect(tags).toContainEqual(['L', 'ISO-639-1']); + expect(tags).toContainEqual(['l', 'en', 'ISO-639-1']); + expect(tags).toContainEqual(['l', 'es', 'ISO-639-1']); + expect(tags).toContainEqual(['t', 'tag1']); + expect(tags).toContainEqual(['t', 'tag2']); + expect(tags).toContainEqual(['R', 'auth']); + expect(tags).toContainEqual(['R', '!payment']); + expect(tags).toContainEqual(['s', 'TestSoftware']); + expect(tags).toContainEqual(['L', 'nip11.version']); + expect(tags).toContainEqual(['l', '1.0.0', 'nip11.version']); + }); + }); + + describe('SSL Tags', () => { + it('should handle SSL tags correctly for wss protocol', () => { + const sslData = { + valid_from: new Date(Date.now() - 100000).toISOString(), + valid_to: new Date(Date.now() + 100000).toISOString(), + }; + const checkData = { + url: 'wss://example.com', + ssl: { data: sslData }, + }; + const tags = kind30166.generateTags(checkData); + expect(tags).toContainEqual(['R', 'ssl']); + }); + + it('should handle SSL tags correctly for non-wss protocol', () => { + const checkData = { + url: 'ws://example.com', + }; + const tags = kind30166.generateTags(checkData); + expect(tags).toContainEqual(['R', '!ssl']); + }); + }); + + describe('DNS Tags', () => { + it('should handle DNS tags correctly', () => { + const dnsData = { + ipv4: ['192.168.1.1'], + ipv6: ['::1'], + }; + const checkData = { + url: 'wss://example.com', + dns: { data: dnsData }, + duration: 100, + }; + const tags = kind30166.generateTags(checkData); + + expect(tags).toContainEqual(['L', 'dns.ipv4']); + expect(tags).toContainEqual(['l', '192.168.1.1', 'dns.ipv4']); + expect(tags).toContainEqual(['L', 'dns.ipv6']); + expect(tags).toContainEqual(['l', '::1', 'dns.ipv6']); + }); + }); + + describe('Geo Tags', () => { + it('should handle geo tags correctly', () => { + const geoData = [ + { + isp: 'Test ISP', + as: 'AS12345', + asname: 'Test AS Name', + countryCode: 'US', + countryName: 'United States', + regionCode: 'CA', + }, + ]; + const checkData = { + url: 'wss://example.com', + geo: { data: geoData }, + }; + + // Mock ngeotags response + const mockNgeotags = ngeotags as any; + mockNgeotags.mockReturnValue([ + ['L', 'geo'], + ['l', 'US', 'geo'], + ]); + + const tags = kind30166.generateTags(checkData); + + expect(tags).toContainEqual(['L', 'host.isp']); + expect(tags).toContainEqual(['l', 'Test ISP', 'host.isp']); + expect(tags).toContainEqual(['L', 'host.as']); + expect(tags).toContainEqual(['l', 'AS12345', 'host.as']); + expect(tags).toContainEqual(['L', 'host.asn']); + expect(tags).toContainEqual(['l', 'Test AS Name', 'host.asn']); + expect(tags).toContainEqual(['L', 'geo']); + expect(tags).toContainEqual(['l', 'US', 'geo']); + }); + }); + }); +}); diff --git a/libraries/nocap-route66/src/kinds/30166.ts b/libraries/nocap-route66/src/kinds/30166.ts new file mode 100644 index 00000000..89756132 --- /dev/null +++ b/libraries/nocap-route66/src/kinds/30166.ts @@ -0,0 +1,249 @@ + // Kind30166.ts + import { type ITransform, Transform } from '../Transform.js'; + import ngeotags from 'nostr-geotags'; + + import type { IResult, IResultDataData } from "@nostrwatch/nocap" + + export interface GeoData { + isp?: string; + as?: string; + asname?: string; + [key: string]: any; + } + + export class Kind30166 extends Transform implements ITransform { + private child: boolean = false; + + constructor(pubkey: string) { + super(30166, pubkey); + } + + generateTags(check: IResult): string[][] { + this.child = check?.parent ? true : false; + + let tags: string[][] = []; + const { protocol } = new URL(check.url); + + tags = this.addBasicTags(tags, check); + tags = this.addNetworkTags(tags, check); + tags = this.addInfoTags(tags, check.info?.data); + tags = this.addGeoTags(tags, check.geo?.data); + + // if(!this.child) + tags = this.addSslTags(tags, check.ssl?.data, protocol); + tags = this.addDnsTags(tags, check.dns?.data); + + if(this.child) + tags = this.addParentReferenceTags(tags, check); + + tags.push(['l', 'draft7', 'route66.draft']); + + return this.dedupLabels(tags); + } + + private addParentReferenceTags(tags: string[][], check: IResult): string[][] { + tags.push(['a', `30166:${this.pubkey}:${check.parent}`]) + return tags + } + + private addBasicTags(tags: string[][], check: IResult): string[][] { + tags.push(['d', check.url]); + + const durations: Array<'open' | 'read' | 'write'> = ['open', 'read', 'write']; + durations.forEach((type) => { + const duration = check[type]?.duration; + if (duration && duration > 0) { + tags.push([`rtt-${type}`, String(Math.round(duration))]); + } + }); + return tags + } + + private addNetworkTags(tags: string[][], check: IResult): string[][] { + if (check.network) { + tags.push(['n', check.network]); + } + return tags + } + + private addInfoTags(tags: string[][], info?: any): string[][] { + if (!info) return tags; + this.addPubkeyTag(tags, info.pubkey); + this.addSupportedNips(tags, info.supported_nips); + this.addLanguageTags(tags, info.language_tags); + this.addCustomTags(tags, info.tags); + this.addLimitationTags(tags, info.limitation); + this.addSoftwareTags(tags, info.software, info.version); + return tags + } + + private addPubkeyTag(tags: string[][], pubkey?: string): string[][] { + if (pubkey && /^[0-9a-f]{64}$/.test(pubkey)) { + tags.push(['p', pubkey]); + } + return tags + } + + private addSupportedNips(tags: string[][], nips?: number[]): string[][] { + if (nips) { + nips.forEach((nip) => tags.push(['N', String(nip)])); + } + return tags + } + + private addLanguageTags(tags: string[][], languages?: string[]): string[][] { + if (languages) { + tags.push(['L', 'ISO-639-1']); + languages.forEach((lang) => tags.push(['l', lang, 'ISO-639-1'])); + } + + return tags + } + + private addCustomTags(tags: string[][], customTags?: string[]): string[][] { + if (customTags) { + customTags.forEach((tag) => tags.push(['t', tag])); + } + + return tags + } + + private addLimitationTags( + tags: string[][], + limitation?: { auth_required?: boolean; payment_required?: boolean } + ): string[][] + { + if (limitation) { + tags.push(['R', limitation.auth_required ? 'auth' : '!auth']); + tags.push(['R', limitation.payment_required ? 'payment' : '!payment']); + } + + return tags + } + + private addSoftwareTags( + tags: string[][], + software?: string, + version?: string + ): string[][] + { + if (software) { + tags.push(['s', software]); + } + if (version) { + tags.push(['L', 'nip11.version']); + tags.push(['l', version, 'nip11.version']); + } + + return tags + } + + private addSslTags( + tags: string[][], + sslData: any, + protocol: string + ): string[][] { + if (protocol === 'wss:' && sslData) { + const validFrom = new Date(sslData.valid_from).getTime(); + const validTo = new Date(sslData.valid_to).getTime(); + const isValid = validFrom < Date.now() && validTo > Date.now(); + tags.push(['R', isValid ? 'ssl' : '!ssl']); + } else { + tags.push(['R', '!ssl']); + } + + return tags + } + + private addDnsTags(tags: string[][], dnsData: any): string[][] { + if (!dnsData) return tags; + + ['ipv4', 'ipv6'].forEach((type) => { + if (dnsData[type]?.length) { + tags.push(['L', `dns.${type}`]); + dnsData[type].forEach((ip: string) => + tags.push(['l', ip, `dns.${type}`]) + ); + } + }); + + return tags + } + + private addGeoTags(tags: string[][], geoData?: IResultDataData): string[][] { + if (!geoData || !Array.isArray(geoData)) return tags; + + let ispTags: string[][] = []; + let geoTags: string[][] = []; + + geoData.forEach((geo) => { + ispTags = this.addGeoIspTags(ispTags, geo); + geoTags = this.addGeoLocationTags(geoTags, geo); + }); + + geoTags = [...this.removeLabels(geoTags), ...this.dedupLabels(geoTags)]; + ispTags = this.dedupLabels(ispTags); + tags.push(...ispTags, ...geoTags); + return tags + } + + private addGeoIspTags(tags: string[][], geo: GeoData): string[][] { + const ispFields = [ + { key: 'isp', label: 'host.isp' }, + { key: 'as', label: 'host.as' }, + { key: 'asname', label: 'host.asn' }, + ]; + + ispFields.forEach(({ key, label }) => { + if (geo[key]) { + tags.push(['L', label]); + tags.push(['l', geo[key], label]); + } + }); + + return tags; + } + + private addGeoLocationTags(tags: string[][], geo: GeoData): string[][] { + const gOpts = { + isoAsNamespace: false, + geohash: true, + gps: false, + countryCode: true, + countryName: true, + regionCode: true, + }; + const geoTags = ngeotags(geo, gOpts) as string[][]; + return [...tags, ...geoTags]; + } + + dedupLabels(tags: string[][]): string[][] { + const dedupedTags: string[][] = []; + const keys: Map> = new Map(); + + tags.forEach((item) => { + if (item[0] === 'L') { + const key = item[1]; + if (!keys.has(key)) { + keys.set(key, new Set()); + dedupedTags.push(item); + } + } else if (item[0] === 'l') { + const key = item[2]; + const value = item[1]; + if (keys.has(key) && !keys.get(key)!.has(value)) { + keys.get(key)!.add(value); + dedupedTags.push(item); + } + } else { + dedupedTags.push(item); + } + }); + + return dedupedTags; + } + + removeLabels(tags: string[][]): string[][] { + return tags.filter((t) => t[0] !== 'l' && t[0] !== 'L'); + } + } diff --git a/libraries/nocap-route66/src/kinds/index.ts b/libraries/nocap-route66/src/kinds/index.ts new file mode 100644 index 00000000..5fc6e11c --- /dev/null +++ b/libraries/nocap-route66/src/kinds/index.ts @@ -0,0 +1,2 @@ +export { Kind30166 } from './30166.js'; +export { Kind10166 } from './10166.js'; \ No newline at end of file diff --git a/libraries/nocap-route66/src/types/global.d.ts b/libraries/nocap-route66/src/types/global.d.ts new file mode 100644 index 00000000..b5de711b --- /dev/null +++ b/libraries/nocap-route66/src/types/global.d.ts @@ -0,0 +1,16 @@ +declare module '@nostrwatch/logger' { + export default class Logger { + constructor(name: string, log_level?: string, split_logs?: boolean); + + logger: createLogger.LoggerInstance; + log_level: string; + split_logs: boolean; + + fatal(message: string): void; + error(message: string): void; + err(message: string): void; + warn(message: string): void; + info(message: string): void; + debug(message: string): void; + } +} \ No newline at end of file diff --git a/libraries/nocap-route66/tsconfig.json b/libraries/nocap-route66/tsconfig.json new file mode 100644 index 00000000..57148a77 --- /dev/null +++ b/libraries/nocap-route66/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM"], + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*/*.test.js"], + "tsc-alias": { + "resolveFullPaths": true, + "resolveFullExtension": ".js" + } +} diff --git a/libraries/nocap-route66/webpack.config.js b/libraries/nocap-route66/webpack.config.js new file mode 100644 index 00000000..79dbf628 --- /dev/null +++ b/libraries/nocap-route66/webpack.config.js @@ -0,0 +1,65 @@ +const path = require('path'); + +/** + * Base configuration shared between both targets. + */ +const baseConfig = { + entry: './src/index.ts', + devtool: 'source-map', + module: { + rules: [ + { + test: /\.ts$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: ['.ts', '.js'], + }, + externals: { + 'nostr-tools': 'nostr-tools', + 'nostr-geotags': 'nostr-geotags', + '@nostrwatch/logger': '@nostrwatch/logger', + }, + optimization: { + usedExports: false, + minimize: false, + }, +}; + + +/** + * Configuration for the umd build. + */ +const umdConfig = { + ...baseConfig, + target: 'web', + output: { + path: path.resolve(__dirname, 'dist', 'umd'), + filename: 'index.js', + library: { + name: '@nostrwatch/nocap-route66', + type: 'umd', + }, + globalObject: 'this', + }, +}; + +/** + * Configuration for the cjs build. + */ +const cjsConfig = { + ...baseConfig, + target: 'node', + output: { + path: path.resolve(__dirname, 'dist', 'cjs'), + filename: 'index.js', + library: { + type: 'commonjs2', + }, + }, +}; + +module.exports = [umdConfig, cjsConfig];