replaceable-events #1
3 changed files with 187 additions and 22 deletions
149
index.ts
149
index.ts
|
@ -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,8 +487,57 @@ 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 = mixQuery(
|
||||||
query,
|
query,
|
||||||
sqlPartial`id IN (
|
sqlPartial`id IN (
|
||||||
|
@ -443,10 +548,12 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
|
||||||
SELECT v.tag_id
|
SELECT v.tag_id
|
||||||
FROM event_tags_values v
|
FROM event_tags_values v
|
||||||
WHERE v.value_position = 1
|
WHERE v.value_position = 1
|
||||||
AND v.value = ${value}
|
AND v.value = ${tagValue}
|
||||||
)
|
)
|
||||||
)`,
|
)`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
const tagsArray = Object.values(tags);
|
|
||||||
|
tagData.values.set(tag.tag_value_position, tag.tag_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
2
migrations/3-replaceableEvents.sql
Normal file
2
migrations/3-replaceableEvents.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE events
|
||||||
|
ADD COLUMN replaced INTEGER NOT NULL DEFAULT 0;
|
32
utils.ts
32
utils.ts
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue