forked from Kieran/snort
Merge pull request #478 from v0l/nostr-package-nip09-event-deletion
`nostr` package: NIP-09 event deletion
This commit is contained in:
commit
293510069c
@ -12,5 +12,6 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
"require-await": "error",
|
"require-await": "error",
|
||||||
eqeqeq: "error",
|
eqeqeq: "error",
|
||||||
|
"object-shorthand": "warn",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ _Progress: 8/34 (23%)._
|
|||||||
- [x] NIP-05: Mapping Nostr keys to DNS-based internet identifiers
|
- [x] NIP-05: Mapping Nostr keys to DNS-based internet identifiers
|
||||||
- [ ] NIP-06: Basic key derivation from mnemonic seed phrase
|
- [ ] NIP-06: Basic key derivation from mnemonic seed phrase
|
||||||
- [ ] NIP-07: window.nostr capability for web browsers
|
- [ ] NIP-07: window.nostr capability for web browsers
|
||||||
- [ ] NIP-09: Event Deletion
|
- [x] NIP-09: Event Deletion
|
||||||
- [ ] NIP-10: Conventions for clients' use of `e` and `p` tags in text events
|
- [ ] NIP-10: Conventions for clients' use of `e` and `p` tags in text events
|
||||||
- TODO Check if this applies
|
- TODO Check if this applies
|
||||||
- [x] NIP-11: Relay Information Document
|
- [x] NIP-11: Relay Information Document
|
||||||
|
48
packages/nostr/src/event/deletion.ts
Normal file
48
packages/nostr/src/event/deletion.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { EventId, EventKind, RawEvent, signEvent } from "."
|
||||||
|
import { NostrError } from "../common"
|
||||||
|
import { HexOrBechPrivateKey } from "../crypto"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A deletion event. Used for marking published events as deleted.
|
||||||
|
*
|
||||||
|
* Related NIPs: NIP-09.
|
||||||
|
*/
|
||||||
|
export interface Deletion extends RawEvent {
|
||||||
|
kind: EventKind.Deletion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The IDs of events to delete.
|
||||||
|
*/
|
||||||
|
getEvents(): EventId[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a deletion event.
|
||||||
|
*/
|
||||||
|
export function createDeletion(
|
||||||
|
{ events, content }: { events: EventId[]; content?: string },
|
||||||
|
priv?: HexOrBechPrivateKey
|
||||||
|
): Promise<Deletion> {
|
||||||
|
return signEvent(
|
||||||
|
{
|
||||||
|
kind: EventKind.Deletion,
|
||||||
|
tags: events.map((id) => ["e", id]),
|
||||||
|
content: content ?? "",
|
||||||
|
getEvents,
|
||||||
|
},
|
||||||
|
priv
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEvents(this: Deletion): EventId[] {
|
||||||
|
return this.tags
|
||||||
|
.filter((tag) => tag[0] === "e")
|
||||||
|
.map((tag) => {
|
||||||
|
if (tag[1] === undefined) {
|
||||||
|
throw new NostrError(
|
||||||
|
`invalid deletion event tag: ${JSON.stringify(tag)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return tag[1]
|
||||||
|
})
|
||||||
|
}
|
@ -21,6 +21,7 @@ import {
|
|||||||
getRecipient,
|
getRecipient,
|
||||||
} from "./direct-message"
|
} from "./direct-message"
|
||||||
import { ContactList, getContacts } from "./contact-list"
|
import { ContactList, getContacts } from "./contact-list"
|
||||||
|
import { Deletion, getEvents } from "./deletion"
|
||||||
|
|
||||||
// TODO Add remaining event types
|
// TODO Add remaining event types
|
||||||
|
|
||||||
@ -69,8 +70,9 @@ export interface Unknown extends RawEvent {
|
|||||||
EventKind,
|
EventKind,
|
||||||
| EventKind.SetMetadata
|
| EventKind.SetMetadata
|
||||||
| EventKind.TextNote
|
| EventKind.TextNote
|
||||||
| EventKind.DirectMessage
|
|
||||||
| EventKind.ContactList
|
| EventKind.ContactList
|
||||||
|
| EventKind.DirectMessage
|
||||||
|
| EventKind.Deletion
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +81,7 @@ export type Event =
|
|||||||
| TextNote
|
| TextNote
|
||||||
| ContactList
|
| ContactList
|
||||||
| DirectMessage
|
| DirectMessage
|
||||||
|
| Deletion
|
||||||
| Unknown
|
| Unknown
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -189,6 +192,14 @@ export async function parseEvent(event: RawEvent): Promise<Event> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.kind === EventKind.Deletion) {
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
kind: EventKind.Deletion,
|
||||||
|
getEvents,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...event,
|
...event,
|
||||||
kind: event.kind,
|
kind: event.kind,
|
||||||
|
72
packages/nostr/test/deletion.ts
Normal file
72
packages/nostr/test/deletion.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { EventKind } from "../src/event"
|
||||||
|
import { parsePublicKey } from "../src/crypto"
|
||||||
|
import assert from "assert"
|
||||||
|
import { setup } from "./setup"
|
||||||
|
import { createTextNote } from "../src/event/text"
|
||||||
|
import { createDeletion } from "../src/event/deletion"
|
||||||
|
|
||||||
|
describe("deletion", () => {
|
||||||
|
// Test that a deletion event deletes existing events. Test that the deletion event
|
||||||
|
// is propagated to subscribers.
|
||||||
|
it("deletes existing events", (done) => {
|
||||||
|
setup(
|
||||||
|
done,
|
||||||
|
({
|
||||||
|
publisher,
|
||||||
|
publisherSecret,
|
||||||
|
publisherPubkey,
|
||||||
|
subscriber,
|
||||||
|
timestamp,
|
||||||
|
done,
|
||||||
|
}) => {
|
||||||
|
// The event ID to delete.
|
||||||
|
let textNoteId: string
|
||||||
|
// The deletion event ID.
|
||||||
|
let deletionId: string
|
||||||
|
|
||||||
|
// Expect the deletion event (and not the text note event).
|
||||||
|
subscriber.on("event", ({ event }) => {
|
||||||
|
assert.strictEqual(event.kind, EventKind.Deletion)
|
||||||
|
assert.strictEqual(event.id, deletionId)
|
||||||
|
assert.strictEqual(event.pubkey, parsePublicKey(publisherPubkey))
|
||||||
|
assert.strictEqual(event.created_at, timestamp)
|
||||||
|
assert.strictEqual(event.content, "")
|
||||||
|
if (event.kind === EventKind.Deletion) {
|
||||||
|
assert.deepStrictEqual(event.getEvents(), [textNoteId])
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
createTextNote("hello world", publisherSecret).then((textNote) => {
|
||||||
|
textNoteId = textNote.id
|
||||||
|
publisher.publish({
|
||||||
|
...textNote,
|
||||||
|
created_at: timestamp,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
publisher.on("ok", async ({ eventId, ok }) => {
|
||||||
|
assert.strictEqual(ok, true)
|
||||||
|
|
||||||
|
if (eventId === textNoteId) {
|
||||||
|
// After the text note has been published, delete it.
|
||||||
|
const deletion = await createDeletion(
|
||||||
|
{ events: [textNoteId] },
|
||||||
|
publisherSecret
|
||||||
|
)
|
||||||
|
deletionId = deletion.id
|
||||||
|
publisher.publish({
|
||||||
|
...deletion,
|
||||||
|
created_at: timestamp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventId === deletionId) {
|
||||||
|
// After the deletion has been published, subscribe to the publisher.
|
||||||
|
subscriber.subscribe([])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user