replaceable-events (#1)

This commit is contained in:
Danny Morabito 2025-03-18 15:06:13 +00:00
parent fc6e1c59a5
commit 7dbb4a522f
3 changed files with 187 additions and 22 deletions

175
index.ts
View file

@ -7,9 +7,13 @@ import type {
import { import {
getCCNPrivateKey, getCCNPrivateKey,
getCCNPubkey, getCCNPubkey,
isAddressableEvent,
isArray, isArray,
isCCNReplaceableEvent,
isLocalhost, isLocalhost,
isReplaceableEvent,
isValidJSON, isValidJSON,
parseATagQuery,
randomTimeUpTo2DaysInThePast, randomTimeUpTo2DaysInThePast,
} from "./utils.ts"; } from "./utils.ts";
import * as nostrTools from "@nostr/tools"; import * as nostrTools from "@nostr/tools";
@ -171,6 +175,58 @@ function addEventToDb(
if (existingEvent) throw new EventAlreadyExistsException(); if (existingEvent) throw new EventAlreadyExistsException();
try { try {
db.run("BEGIN TRANSACTION"); db.run("BEGIN TRANSACTION");
if (isReplaceableEvent(decryptedEvent.kind)) {
sql`
UPDATE events
SET replaced = 1
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`
UPDATE events
SET replaced = 1
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);
}
}
if (isCCNReplaceableEvent(decryptedEvent.kind)) {
const dTag = decryptedEvent.tags.find((tag) => tag[0] === "d")?.[1];
sql`
UPDATE events
SET replaced = 1
WHERE kind = ${decryptedEvent.kind}
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` sql`
INSERT INTO events (id, original_id, pubkey, created_at, kind, content, sig, first_seen) VALUES ( INSERT INTO events (id, original_id, pubkey, created_at, kind, content, sig, first_seen) VALUES (
${decryptedEvent.id}, ${decryptedEvent.id},
@ -353,7 +409,7 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
}`, }`,
); );
let query = sqlPartial`SELECT * FROM events`; let query = sqlPartial`SELECT * FROM events WHERE replaced = 0`;
const filtersAreNotEmpty = filters.some((filter) => { const filtersAreNotEmpty = filters.some((filter) => {
return Object.values(filter).some((value) => { return Object.values(filter).some((value) => {
@ -362,7 +418,7 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
}); });
if (filtersAreNotEmpty) { if (filtersAreNotEmpty) {
query = mixQuery(query, sqlPartial`WHERE`); query = mixQuery(query, sqlPartial`AND`);
for (let i = 0; i < filters.length; i++) { for (let i = 0; i < filters.length; i++) {
// filters act as OR, filter groups act as AND // filters act as OR, filter groups act as AND
@ -431,22 +487,73 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
const uniqueValues = [...new Set(value)]; const uniqueValues = [...new Set(value)];
query = mixQuery(query, sqlPartial`(`); query = mixQuery(query, sqlPartial`(`);
for (let k = 0; k < uniqueValues.length; k++) { 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 !== "") {
if (isCCNReplaceableEvent(aTagInfo.kind)) {
// CCN replaceable 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 t.tag_name = 'd'
AND v.value_position = 1
AND v.value = ${aTagInfo.dTag}
)`,
);
} else {
// 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) { if (k < uniqueValues.length - 1) {
query = mixQuery(query, sqlPartial`OR`); query = mixQuery(query, sqlPartial`OR`);
} }
@ -481,12 +588,36 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
const rawTags = sql`SELECT * FROM event_tags_view WHERE event_id = ${ const rawTags = sql`SELECT * FROM event_tags_view WHERE event_id = ${
events[i].id events[i].id
}`(connection.db); }`(connection.db);
const tags: { [key: string]: string[] } = {}; const tagsByIndex = new Map<number, {
for (const item of rawTags) { name: string;
if (!tags[item.tag_name]) tags[item.tag_name] = [item.tag_name]; values: Map<number, string>;
tags[item.tag_name].push(item.tag_value); }>();
for (const tag of rawTags) {
let tagData = tagsByIndex.get(tag.tag_index);
if (!tagData) {
tagData = {
name: tag.tag_name,
values: new Map(),
};
tagsByIndex.set(tag.tag_index, tagData);
}
tagData.values.set(tag.tag_value_position, tag.tag_value);
} }
const tagsArray = Object.values(tags);
const tagsArray = Array.from(tagsByIndex.entries())
.sort(([indexA], [indexB]) => indexA - indexB)
.map(([_, tagData]) => {
const { name, values } = tagData;
return [
name,
...Array.from(values.entries())
.sort(([posA], [posB]) => posA - posB)
.map(([_, value]) => value),
];
});
const event = { const event = {
id: events[i].id, id: events[i].id,

View file

@ -0,0 +1,2 @@
ALTER TABLE events
ADD COLUMN replaced INTEGER NOT NULL DEFAULT 0;

View file

@ -67,3 +67,35 @@ export async function getCCNPrivateKey(): Promise<Uint8Array> {
); );
return decryptUint8Array(decodeBase64(encryptedPrivateKey), encryptionKey); 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 isCCNReplaceableEvent(kind: number): boolean {
return (kind >= 60000 && kind < 65536) || kind === 0;
}
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: Number.parseInt(parts[0], 10),
pubkey: parts[1],
dTag: parts.length > 2 ? parts[2] : undefined,
};
}