From 4c554c91a6cb983c5af953e4728594fd4a76ca7c Mon Sep 17 00:00:00 2001 From: Danny Morabito Date: Mon, 17 Mar 2025 19:14:49 +0100 Subject: [PATCH] Implement Replaceable and Addressable Events --- index.ts | 97 +++++++++++++++++++++++++++++++++++++++++++++++--------- utils.ts | 26 +++++++++++++++ 2 files changed, 108 insertions(+), 15 deletions(-) diff --git a/index.ts b/index.ts index 7cfb67a..71d54db 100644 --- a/index.ts +++ b/index.ts @@ -7,9 +7,12 @@ import type { import { getCCNPrivateKey, getCCNPubkey, + isAddressableEvent, isArray, isLocalhost, + isReplaceableEvent, isValidJSON, + parseATagQuery, randomTimeUpTo2DaysInThePast, } from "./utils.ts"; import * as nostrTools from "@nostr/tools"; @@ -171,6 +174,36 @@ function addEventToDb( if (existingEvent) throw new EventAlreadyExistsException(); try { db.run("BEGIN TRANSACTION"); + if (isReplaceableEvent(decryptedEvent.kind)) { + sql` + DELETE FROM events + WHERE kind = ${decryptedEvent.kind} + AND pubkey = ${decryptedEvent.pubkey} + AND created_at < ${decryptedEvent.created_at} + `(db); + } + + if (isAddressableEvent(decryptedEvent.kind)) { + const dTag = decryptedEvent.tags.find((tag) => tag[0] === "d")?.[1]; + if (dTag) { + sql` + DELETE FROM events + WHERE kind = ${decryptedEvent.kind} + AND pubkey = ${decryptedEvent.pubkey} + AND created_at < ${decryptedEvent.created_at} + AND id IN ( + SELECT event_id FROM event_tags + WHERE tag_name = 'd' + AND tag_id IN ( + SELECT tag_id FROM event_tags_values + WHERE value_position = 1 + AND value = ${dTag} + ) + ) + `(db); + } + } + sql` INSERT INTO events (id, original_id, pubkey, created_at, kind, content, sig, first_seen) VALUES ( ${decryptedEvent.id}, @@ -431,22 +464,56 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) { const uniqueValues = [...new Set(value)]; query = mixQuery(query, sqlPartial`(`); for (let k = 0; k < uniqueValues.length; k++) { - const value = uniqueValues[k] as string; + const tagValue = uniqueValues[k] as string; + if (tag === "a") { + const aTagInfo = parseATagQuery(tagValue); + + if (aTagInfo.dTag && aTagInfo.dTag !== "") { + // Addressable event reference + query = mixQuery( + query, + sqlPartial`id IN ( + SELECT e.id + FROM events e + JOIN event_tags t ON e.id = t.event_id + JOIN event_tags_values v ON t.tag_id = v.tag_id + WHERE e.kind = ${aTagInfo.kind} + AND e.pubkey = ${aTagInfo.pubkey} + AND t.tag_name = 'd' + AND v.value_position = 1 + AND v.value = ${aTagInfo.dTag} + )`, + ); + } else { + // Replaceable event reference + query = mixQuery( + query, + sqlPartial`id IN ( + SELECT id + FROM events + WHERE kind = ${aTagInfo.kind} + AND pubkey = ${aTagInfo.pubkey} + )`, + ); + } + } else { + // Regular tag handling (unchanged) + query = mixQuery( + query, + sqlPartial`id IN ( + SELECT t.event_id + FROM event_tags t + WHERE t.tag_name = ${tag} + AND t.tag_id IN ( + SELECT v.tag_id + FROM event_tags_values v + WHERE v.value_position = 1 + AND v.value = ${tagValue} + ) + )`, + ); + } - query = mixQuery( - query, - sqlPartial`id IN ( - SELECT t.event_id - FROM event_tags t - WHERE t.tag_name = ${tag} - AND t.tag_id IN ( - SELECT v.tag_id - FROM event_tags_values v - WHERE v.value_position = 1 - AND v.value = ${value} - ) - )`, - ); if (k < uniqueValues.length - 1) { query = mixQuery(query, sqlPartial`OR`); } diff --git a/utils.ts b/utils.ts index 1ec0095..5c7c5c6 100644 --- a/utils.ts +++ b/utils.ts @@ -67,3 +67,29 @@ export async function getCCNPrivateKey(): Promise { ); return decryptUint8Array(decodeBase64(encryptedPrivateKey), encryptionKey); } + +export function isReplaceableEvent(kind: number): boolean { + return (kind >= 10000 && kind < 20000) || kind === 0 || kind === 3; +} + +export function isAddressableEvent(kind: number): boolean { + return kind >= 30000 && kind < 40000; +} + +export function isRegularEvent(kind: number): boolean { + return (kind >= 1000 && kind < 10000) || + (kind >= 4 && kind < 45) || + kind === 1 || + kind === 2; +} + +export function parseATagQuery(aTagValue: string): { kind: number, pubkey: string, dTag?: string } { + const parts = aTagValue.split(':'); + if (parts.length < 2) return { kind: 0, pubkey: '' }; + + return { + kind: parseInt(parts[0], 10), + pubkey: parts[1], + dTag: parts.length > 2 ? parts[2] : undefined + }; +}