From 340a4a6799b2bbdc14777b346584d871b7909ebd Mon Sep 17 00:00:00 2001 From: Sepehr Safari Date: Thu, 18 Jan 2024 17:13:31 +0330 Subject: [PATCH 1/3] implement nip99 --- nip99.ts | 229 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 nip99.ts diff --git a/nip99.ts b/nip99.ts new file mode 100644 index 0000000..c4fe830 --- /dev/null +++ b/nip99.ts @@ -0,0 +1,229 @@ +import { Event, EventTemplate } from './core.ts' +import { ClassifiedListing, DraftClassifiedListing } from './kinds.ts' +import { finalizeEvent, generateSecretKey } from './pure.ts' + +/** + * Represents the details of a price. + * @example { amount: '100', currency: 'USD', frequency: 'month' } + * @example { amount: '100', currency: 'EUR' } + */ +export type PriceDetails = { + /** + * The amount of the price. + */ + amount: string + /** + * The currency of the price in 3-letter ISO 4217 format. + * @example 'USD' + */ + currency: string + /** + * The optional frequency of payment. + * Can be one of: 'hour', 'day', 'week', 'month', 'year', or a custom string. + */ + frequency?: string +} + +/** + * Represents a classified listing object. + */ +export type ClassifiedListingObject = { + /** + * Whether the listing is a draft or not. + */ + isDraft: boolean + /** + * A title of the listing. + */ + title: string + /** + * A short summary or tagline. + */ + summary: string + /** + * A description in Markdown format. + */ + content: string + /** + * Timestamp in unix seconds of when the listing was published. + */ + publishedAt: string + /** + * Location of the listing. + * @example 'NYC' + */ + location: string + /** + * Price details. + */ + price: PriceDetails + /** + * Images of the listing with optional dimensions. + */ + images: Array<{ + url: string + dimensions?: string + }> + /** + * Tags/Hashtags (i.e. categories, keywords, etc.) + */ + hashtags: string[] + /** + * Other standard tags. + * @example "g", a geohash for more precise location + */ + additionalTags: Record +} + +/** + * Validates an event to ensure it is a valid classified listing event. + * @param event - The event to validate. + * @returns True if the event is valid, false otherwise. + */ +export function validateEvent(event: Event): boolean { + if (![ClassifiedListing, DraftClassifiedListing].includes(event.kind)) return false + + const requiredTags = ['d', 'title', 'summary', 'location', 'published_at', 'price'] + const requiredTagCount = requiredTags.length + const tagCounts: Record = {} + + if (event.tags.length < requiredTagCount) return false + + for (const tag of event.tags) { + if (tag.length < 2) return false + + const [tagName, ...tagValues] = tag + + if (tagName == 'published_at') { + const timestamp = parseInt(tagValues[0]) + if (isNaN(timestamp)) return false + } else if (tagName == 'price') { + if (tagValues.length < 2) return false + + const price = parseInt(tagValues[0]) + if (isNaN(price) || tagValues[1].length != 3) return false + } else if ((tagName == 'e' || tagName == 'a') && tag.length != 3) { + return false + } + + if (requiredTags.includes(tagName)) { + tagCounts[tagName] = (tagCounts[tagName] || 0) + 1 + } + } + + return Object.values(tagCounts).every(count => count == 1) && Object.keys(tagCounts).length == requiredTagCount +} + +/** + * Parses an event and returns a classified listing object. + * @param event - The event to parse. + * @returns The classified listing object. + * @throws Error if the event is invalid. + */ +export function parseEvent(event: Event): ClassifiedListingObject { + if (!validateEvent(event)) { + throw new Error('Invalid event') + } + + const listing: ClassifiedListingObject = { + isDraft: event.kind === DraftClassifiedListing, + title: '', + summary: '', + content: event.content, + publishedAt: '', + location: '', + price: { + amount: '', + currency: '', + }, + images: [], + hashtags: [], + additionalTags: {}, + } + + for (let i = 0; i < event.tags.length; i++) { + const tag = event.tags[i] + const [tagName, ...tagValues] = tag + + if (tagName == 'title') { + listing.title = tagValues[0] + } else if (tagName == 'summary') { + listing.summary = tagValues[0] + } else if (tagName == 'published_at') { + listing.publishedAt = tagValues[0] + } else if (tagName == 'location') { + listing.location = tagValues[0] + } else if (tagName == 'price') { + listing.price.amount = tagValues[0] + listing.price.currency = tagValues[1] + + if (tagValues.length == 3) { + listing.price.frequency = tagValues[2] + } + } else if (tagName == 'image') { + listing.images.push({ + url: tagValues[0], + dimensions: tagValues?.[1] ?? undefined, + }) + } else if (tagName == 't') { + listing.hashtags.push(tagValues[0]) + } else if (tagName == 'e' || tagName == 'a') { + listing.additionalTags[tagName] = [...tagValues] + } + } + + return listing +} + +/** + * Generates an event template based on a classified listing object. + * + * @param listing - The classified listing object. + * @returns The event template. + */ +export function generateEventTemplate(listing: ClassifiedListingObject): EventTemplate { + const priceTag = ['price', listing.price.amount, listing.price.currency] + if (listing.price.frequency) priceTag.push(listing.price.frequency) + + const tags: string[][] = [ + ['d', listing.title.trim().toLowerCase().replace(/ /g, '-')], + ['title', listing.title], + ['published_at', listing.publishedAt], + ['summary', listing.summary], + ['location', listing.location], + priceTag, + ] + + for (let i = 0; i < listing.images.length; i++) { + const image = listing.images[i] + const imageTag = ['image', image.url] + if (image.dimensions) imageTag.push(image.dimensions) + + tags.push(imageTag) + } + + for (let i = 0; i < listing.hashtags.length; i++) { + const t = listing.hashtags[i] + + tags.push(['t', t]) + } + + for (const [key, value] of Object.entries(listing.additionalTags)) { + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + const val = value[i] + + tags.push([key, val]) + } + } else { + tags.push([key, value]) + } + } + + return { + kind: listing.isDraft ? DraftClassifiedListing : ClassifiedListing, + content: listing.content, + tags, + created_at: Math.floor(Date.now() / 1000), + } +} From 6d7ad2267791093cdaee10e488d44862f6f76b36 Mon Sep 17 00:00:00 2001 From: Sepehr Safari Date: Thu, 18 Jan 2024 17:13:39 +0330 Subject: [PATCH 2/3] add test cases for nip99 --- nip99.test.ts | 506 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 nip99.test.ts diff --git a/nip99.test.ts b/nip99.test.ts new file mode 100644 index 0000000..442cd6b --- /dev/null +++ b/nip99.test.ts @@ -0,0 +1,506 @@ +import { describe, expect, test } from 'bun:test' + +import { Event } from './core' +import { ClassifiedListing, DraftClassifiedListing } from './kinds' +import { ClassifiedListingObject, generateEventTemplate, parseEvent, validateEvent } from './nip99' +import { finalizeEvent, generateSecretKey } from './pure' + +describe('validateEvent', () => { + test('should return true for a valid classified listing event', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(true) + }) + + test('should return false when the "d" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + // Missing 'd' tag + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "title" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + // Missing 'title' tag + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "summary" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + // Missing 'summary' tag + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "published_at" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + // Missing 'published_at' tag + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "location" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + // Missing 'location' tag + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "price" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + // Missing 'price' tag + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "published_at" tag is not a valid timestamp', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', 'not-a-valid-timestamp'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "price" tag has not a valid price', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', 'not-a-valid-price', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "price" tag has not a valid currency', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'not-a-valid-currency'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "price" tag has not a valid number of elements', () => { + const sk = generateSecretKey() + const event1: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ], + }, + sk, + ) + + expect(validateEvent(event1)).toBe(false) + }) + + test('should return false when the "a" tag has not a valid number of elements', () => { + const sk = generateSecretKey() + const event1: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['a', 'extra1'], + ['a', 'extra2', 'value2', 'extra3'], + ], + }, + sk, + ) + + const event2: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['e', 'extra1'], + ['e', 'extra2', 'value2', 'extra3'], + ], + }, + sk, + ) + + expect(validateEvent(event1)).toBe(false) + expect(validateEvent(event2)).toBe(false) + }) +}) + +describe('parseEvent', () => { + test('should parse a valid event', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: DraftClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + const expectedListing = { + title: 'Sample Title', + summary: 'Sample Summary', + publishedAt: '1296962229', + location: 'NYC', + price: { + amount: '100', + currency: 'USD', + }, + images: [ + { + url: 'https://example.com/image1.jpg', + dimensions: '800x600', + }, + { + url: 'https://example.com/image2.jpg', + }, + ], + hashtags: ['tag1', 'tag2'], + additionalTags: { + e: ['value1', 'value2'], + a: ['value1', 'value2'], + }, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + isDraft: true, + } + + expect(parseEvent(event)).toEqual(expectedListing) + }) + + test('should throw an error for an invalid event', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: DraftClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + // Missing 'd' tag + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(() => parseEvent(event)).toThrow(Error) + }) +}) + +describe('generateEventTemplate', () => { + test('should generate the correct event template for a classified listing', () => { + const listing: ClassifiedListingObject = { + title: 'Sample Title', + summary: 'Sample Summary', + publishedAt: '1296962229', + location: 'NYC', + price: { + amount: '100', + currency: 'USD', + }, + images: [ + { + url: 'https://example.com/image1.jpg', + dimensions: '800x600', + }, + { + url: 'https://example.com/image2.jpg', + }, + ], + hashtags: ['tag1', 'tag2'], + additionalTags: { + extra1: 'value1', + extra2: 'value2', + }, + content: + 'Lorem ipsum dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + isDraft: true, + } + + const expectedEventTemplate = { + kind: DraftClassifiedListing, + content: + 'Lorem ipsum dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['published_at', '1296962229'], + ['summary', 'Sample Summary'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['extra1', 'value1'], + ['extra2', 'value2'], + ], + created_at: expect.any(Number), + } + + expect(generateEventTemplate(listing)).toEqual(expectedEventTemplate) + }) +}) From f6ed374f2fce714c0a60243fe2909c9525824f44 Mon Sep 17 00:00:00 2001 From: Sepehr Safari Date: Thu, 18 Jan 2024 17:16:24 +0330 Subject: [PATCH 3/3] remove un-used imports --- nip99.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/nip99.ts b/nip99.ts index c4fe830..fa89a41 100644 --- a/nip99.ts +++ b/nip99.ts @@ -1,6 +1,5 @@ import { Event, EventTemplate } from './core.ts' import { ClassifiedListing, DraftClassifiedListing } from './kinds.ts' -import { finalizeEvent, generateSecretKey } from './pure.ts' /** * Represents the details of a price.