feat: add support for NIP-09 deletion events and filtering deleted events

This commit is contained in:
Danny Morabito 2025-04-28 17:15:46 +02:00
parent 36c7401fa8
commit da442cabff
3 changed files with 131 additions and 1 deletions

126
index.ts
View file

@ -31,6 +31,7 @@ import {
isAddressableEvent, isAddressableEvent,
isArray, isArray,
isCCNReplaceableEvent, isCCNReplaceableEvent,
isDeleteEvent,
isLocalhost, isLocalhost,
isReplaceableEvent, isReplaceableEvent,
isValidJSON, isValidJSON,
@ -135,6 +136,11 @@ function addEventToDb(
if (existingEvent) throw new EventAlreadyExistsException(); if (existingEvent) throw new EventAlreadyExistsException();
if (isDeleteEvent(decryptedEvent.kind)) {
handleDeletionEvent(decryptedEvent, encryptedEvent, ccnPubkey);
return;
}
const isInvite = const isInvite =
decryptedEvent.tags.findIndex( decryptedEvent.tags.findIndex(
(tag: string[]) => tag[0] === 'type' && tag[1] === 'invite', (tag: string[]) => tag[0] === 'type' && tag[1] === 'invite',
@ -486,6 +492,8 @@ function filtersMatchingEvent(
event: NostrEvent, event: NostrEvent,
connection: UserConnection, connection: UserConnection,
): string[] { ): string[] {
if (isDeleteEvent(event.kind)) return [];
const matching = []; const matching = [];
for (const subscription of connection.subscriptions.keys()) { for (const subscription of connection.subscriptions.keys()) {
const filters = connection.subscriptions.get(subscription); const filters = connection.subscriptions.get(subscription);
@ -530,7 +538,7 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
return log.warn('No active CCN found'); return log.warn('No active CCN found');
} }
let query = sqlPartial`SELECT * FROM events WHERE replaced = 0 AND ccn_pubkey = ${activeCCN.pubkey}`; let query = sqlPartial`SELECT * FROM events WHERE replaced = 0 AND deleted = 0 AND ccn_pubkey = ${activeCCN.pubkey}`;
const filtersAreNotEmpty = filters.some((filter) => { const filtersAreNotEmpty = filters.some((filter) => {
return Object.values(filter).some((value) => { return Object.values(filter).some((value) => {
@ -1041,6 +1049,122 @@ function handleCCNCommands(
} }
} }
function handleDeletionEvent(
deletionEvent: nostrTools.VerifiedEvent,
encryptedEvent: nostrTools.VerifiedEvent,
ccnPubkey: string,
) {
const eventIds: string[] = [];
const aTagRefs: { kind: number; pubkey: string; dTag?: string }[] = [];
for (const tag of deletionEvent.tags) {
if (tag[0] === 'e' && tag[1]) {
eventIds.push(tag[1]);
} else if (tag[0] === 'a' && tag[1]) {
const parsedATag = parseATagQuery(tag[1]);
if (parsedATag.kind && parsedATag.pubkey) {
aTagRefs.push(parsedATag);
}
}
}
if (eventIds.length === 0 && aTagRefs.length === 0) {
return;
}
try {
db.run('BEGIN TRANSACTION');
sql`
INSERT INTO events (id, original_id, pubkey, created_at, kind, content, sig, first_seen, ccn_pubkey) VALUES (
${deletionEvent.id},
${encryptedEvent.id},
${deletionEvent.pubkey},
${deletionEvent.created_at},
${deletionEvent.kind},
${deletionEvent.content},
${deletionEvent.sig},
unixepoch(),
${ccnPubkey}
)
`(db);
if (deletionEvent.tags) {
for (let i = 0; i < deletionEvent.tags.length; i++) {
const tag = sql`
INSERT INTO event_tags(event_id, tag_name, tag_index) VALUES (
${deletionEvent.id},
${deletionEvent.tags[i][0]},
${i}
) RETURNING tag_id
`(db)[0];
for (let j = 1; j < deletionEvent.tags[i].length; j++) {
sql`
INSERT INTO event_tags_values(tag_id, value_position, value) VALUES (
${tag.tag_id},
${j},
${deletionEvent.tags[i][j]}
)
`(db);
}
}
}
if (eventIds.length > 0) {
sql`
UPDATE events
SET deleted = 1
WHERE id IN (${eventIds})
AND pubkey = ${deletionEvent.pubkey}
AND ccn_pubkey = ${ccnPubkey}
`(db);
}
if (aTagRefs.length > 0) {
const withDTag = aTagRefs.filter((ref) => ref.dTag);
const withoutDTag = aTagRefs.filter((ref) => !ref.dTag);
if (withoutDTag.length > 0) {
const kinds = withoutDTag.map((ref) => ref.kind);
sql`
UPDATE events
SET deleted = 1
WHERE kind IN (${kinds})
AND pubkey = ${deletionEvent.pubkey}
AND ccn_pubkey = ${ccnPubkey}
AND created_at <= ${deletionEvent.created_at}
`(db);
}
for (const aTagRef of withDTag) {
sql`
UPDATE events
SET deleted = 1
WHERE kind = ${aTagRef.kind}
AND pubkey = ${deletionEvent.pubkey}
AND ccn_pubkey = ${ccnPubkey}
AND created_at <= ${deletionEvent.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 = ${aTagRef.dTag}
)
)
`(db);
}
}
db.run('COMMIT TRANSACTION');
} catch (e) {
db.run('ROLLBACK TRANSACTION');
throw e;
}
}
Deno.serve({ Deno.serve({
port: 6942, port: 6942,
handler: (request) => { handler: (request) => {

View file

@ -0,0 +1,2 @@
ALTER TABLE events ADD COLUMN deleted INTEGER DEFAULT 0;
CREATE INDEX idx_events_deleted ON events(deleted);

View file

@ -127,6 +127,10 @@ export function isRegularEvent(kind: number): boolean {
); );
} }
export function isDeleteEvent(kind: number): boolean {
return kind === 5;
}
export function isCCNReplaceableEvent(kind: number): boolean { export function isCCNReplaceableEvent(kind: number): boolean {
return kind >= 60000 && kind < 65536; return kind >= 60000 && kind < 65536;
} }