implement NIP-02 #449
@ -9,10 +9,10 @@ A strongly-typed nostr client for Node and the browser.
|
||||
The goal of the project is to have all of the following implemented
|
||||
and tested against a real-world relay implementation.
|
||||
|
||||
_Progress: 6/34 (18%)._
|
||||
_Progress: 7/34 (20%)._
|
||||
|
||||
- [X] NIP-01: Basic protocol flow description
|
||||
- [ ] NIP-02: Contact List and Petnames
|
||||
- [X] NIP-02: Contact List and Petnames
|
||||
- [ ] NIP-03: OpenTimestamps Attestations for Events
|
||||
- [X] NIP-04: Encrypted Direct Message
|
||||
- [ ] NIP-05: Mapping Nostr keys to DNS-based internet identifiers
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { NostrError } from "../common"
|
||||
import { NostrError, parseJson } from "../common"
|
||||
import { SubscriptionId } from "."
|
||||
import { EventId, RawEvent } from "../event"
|
||||
import WebSocket from "isomorphic-ws"
|
||||
@ -8,7 +8,7 @@ import { Filters } from "../filters"
|
||||
* The connection to a relay. This is the lowest layer of the nostr protocol.
|
||||
* The only responsibility of this type is to send and receive
|
||||
* well-formatted nostr messages on the underlying websocket. All other details of the protocol
|
||||
* are handled by `Nostr`.
|
||||
* are handled by `Nostr`. This type does not know anything about event semantics.
|
||||
*
|
||||
* @see Nostr
|
||||
*/
|
||||
@ -321,11 +321,3 @@ function parseEventData(json: { [key: string]: unknown }): RawEvent {
|
||||
}
|
||||
return json as unknown as RawEvent
|
||||
}
|
||||
|
||||
function parseJson(data: string) {
|
||||
try {
|
||||
return JSON.parse(data)
|
||||
} catch (e) {
|
||||
throw new NostrError(`invalid event json: ${data}`)
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,17 @@ export function defined<T>(v: T | undefined | null): T {
|
||||
return v
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the JSON and throw a @see {@link NostrError} in case of error.
|
||||
*/
|
||||
export function parseJson(data: string) {
|
||||
try {
|
||||
return JSON.parse(data)
|
||||
} catch (e) {
|
||||
throw new NostrError(`invalid json: ${e}: ${data}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The error thrown by this library.
|
||||
*/
|
||||
|
83
packages/nostr/src/event/contact-list.ts
Normal file
83
packages/nostr/src/event/contact-list.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { EventKind, RawEvent, Unsigned } from "."
|
||||
import { NostrError } from "../common"
|
||||
import { PublicKey } from "../crypto"
|
||||
|
||||
/**
|
||||
* Contact list event.
|
||||
*
|
||||
* Related NIPs: NIP-02.
|
||||
*/
|
||||
export interface ContactList extends RawEvent {
|
||||
kind: EventKind.ContactList
|
||||
|
||||
/**
|
||||
* Get the contacts in from the contact list.
|
||||
*/
|
||||
getContacts(): Contact[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A contact from the contact list.
|
||||
*/
|
||||
export interface Contact {
|
||||
pubkey: PublicKey
|
||||
relay?: URL
|
||||
petname?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a contact list event.
|
||||
*/
|
||||
export function createContactList(contacts: Contact[]): Unsigned<ContactList> {
|
||||
return {
|
||||
kind: EventKind.ContactList,
|
||||
tags: contacts.map((contact) => [
|
||||
"p",
|
||||
contact.pubkey,
|
||||
contact.relay?.toString() ?? "",
|
||||
contact.petname ?? "",
|
||||
]),
|
||||
content: "",
|
||||
getContacts,
|
||||
}
|
||||
}
|
||||
|
||||
export function getContacts(this: ContactList): Contact[] {
|
||||
return this.tags
|
||||
.filter((tags) => tags[0] === "p")
|
||||
.map((tags) => {
|
||||
// The first element is the pubkey.
|
||||
const pubkey = tags[1]
|
||||
if (pubkey === undefined) {
|
||||
throw new NostrError(
|
||||
`missing contact pubkey for contact list event: ${JSON.stringify(
|
||||
this
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
// The second element is the optional relay URL.
|
||||
let relay: URL | undefined
|
||||
try {
|
||||
if (tags[2] !== undefined && tags[2] !== "") {
|
||||
relay = new URL(tags[2])
|
||||
}
|
||||
} catch (e) {
|
||||
throw new NostrError(
|
||||
`invalid relay URL for contact list event: ${JSON.stringify(this)}`
|
||||
)
|
||||
}
|
||||
|
||||
// The third element is the optional petname.
|
||||
let petname: string | undefined
|
||||
if (tags[3] !== undefined && tags[3] !== "") {
|
||||
petname = tags[3]
|
||||
}
|
||||
|
||||
return {
|
||||
pubkey,
|
||||
relay,
|
||||
petname,
|
||||
}
|
||||
})
|
||||
}
|
124
packages/nostr/src/event/direct-message.ts
Normal file
124
packages/nostr/src/event/direct-message.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import {
|
||||
EventId,
|
||||
EventKind,
|
||||
RawEvent,
|
||||
signEvent,
|
||||
Unsigned,
|
||||
UnsignedWithPubkey,
|
||||
} from "."
|
||||
import { defined, NostrError } from "../common"
|
||||
import {
|
||||
aesDecryptBase64,
|
||||
aesEncryptBase64,
|
||||
getPublicKey,
|
||||
HexOrBechPrivateKey,
|
||||
parsePrivateKey,
|
||||
parsePublicKey,
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
} from "../crypto"
|
||||
|
||||
/**
|
||||
* An encrypted direct message event.
|
||||
*
|
||||
* Related NIPs: NIP-04.
|
||||
*/
|
||||
export interface DirectMessage extends RawEvent {
|
||||
kind: EventKind.DirectMessage
|
||||
|
||||
/**
|
||||
* Get the message plaintext, or undefined if you are not the recipient.
|
||||
*/
|
||||
getMessage(priv?: HexOrBechPrivateKey): Promise<string | undefined>
|
||||
/**
|
||||
* Get the recipient pubkey.
|
||||
*/
|
||||
getRecipient(): PublicKey
|
||||
/**
|
||||
* Get the event ID of the previous message.
|
||||
*/
|
||||
getPrevious(): EventId | undefined
|
||||
}
|
||||
|
||||
// TODO Since you already require the private key, maybe this should return the message already signed?
|
||||
// With NIP-07 the parameter will be optional, then what?
|
||||
/**
|
||||
* Create an encrypted direct message event.
|
||||
*/
|
||||
export async function createDirectMessage(
|
||||
{
|
||||
message,
|
||||
recipient,
|
||||
}: {
|
||||
message: string
|
||||
recipient: PublicKey
|
||||
},
|
||||
priv?: PrivateKey
|
||||
): Promise<DirectMessage> {
|
||||
recipient = parsePublicKey(recipient)
|
||||
if (priv === undefined) {
|
||||
// TODO Use NIP-07
|
||||
throw new NostrError("todo")
|
||||
} else {
|
||||
priv = parsePrivateKey(priv)
|
||||
const { data, iv } = await aesEncryptBase64(priv, recipient, message)
|
||||
return await signEvent(
|
||||
{
|
||||
kind: EventKind.DirectMessage,
|
||||
tags: [["p", recipient]],
|
||||
content: `${data}?iv=${iv}`,
|
||||
getMessage,
|
||||
getRecipient,
|
||||
getPrevious,
|
||||
},
|
||||
priv
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMessage(
|
||||
this: UnsignedWithPubkey<DirectMessage>,
|
||||
priv?: HexOrBechPrivateKey
|
||||
): Promise<string | undefined> {
|
||||
if (priv !== undefined) {
|
||||
priv = parsePrivateKey(priv)
|
||||
}
|
||||
const [data, iv] = this.content.split("?iv=")
|
||||
if (data === undefined || iv === undefined) {
|
||||
throw new NostrError(`invalid direct message content ${this.content}`)
|
||||
}
|
||||
if (priv === undefined) {
|
||||
// TODO Try to use NIP-07
|
||||
throw new NostrError("todo")
|
||||
} else if (getPublicKey(priv) === this.getRecipient()) {
|
||||
return await aesDecryptBase64(this.pubkey, priv, { data, iv })
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getRecipient(this: Unsigned<RawEvent>): PublicKey {
|
||||
const recipientTag = this.tags.find((tag) => tag[0] === "p")
|
||||
if (typeof recipientTag?.[1] !== "string") {
|
||||
throw new NostrError(
|
||||
`expected "p" tag to be of type string, but got ${
|
||||
recipientTag?.[1]
|
||||
} in ${JSON.stringify(this)}`
|
||||
)
|
||||
}
|
||||
return recipientTag[1]
|
||||
}
|
||||
|
||||
export function getPrevious(this: Unsigned<RawEvent>): EventId | undefined {
|
||||
const previousTag = this.tags.find((tag) => tag[0] === "e")
|
||||
if (previousTag === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (typeof previousTag[1] !== "string") {
|
||||
throw new NostrError(
|
||||
`expected "e" tag to be of type string, but got ${
|
||||
previousTag?.[1]
|
||||
} in ${JSON.stringify(this)}`
|
||||
)
|
||||
}
|
||||
return previousTag[1]
|
||||
}
|
@ -1,20 +1,32 @@
|
||||
import {
|
||||
PublicKey,
|
||||
PrivateKey,
|
||||
sha256,
|
||||
schnorrSign,
|
||||
schnorrVerify,
|
||||
parsePublicKey,
|
||||
aesDecryptBase64,
|
||||
getPublicKey,
|
||||
HexOrBechPrivateKey,
|
||||
parsePrivateKey,
|
||||
aesEncryptBase64,
|
||||
} from "./crypto"
|
||||
import { defined, Timestamp, unixTimestamp, NostrError } from "./common"
|
||||
} from "../crypto"
|
||||
import { Timestamp, unixTimestamp, NostrError } from "../common"
|
||||
import { TextNote } from "./text"
|
||||
import { getUserMetadata, SetMetadata } from "./set-metadata"
|
||||
import {
|
||||
DirectMessage,
|
||||
getMessage,
|
||||
getPrevious,
|
||||
getRecipient,
|
||||
} from "./direct-message"
|
||||
import { ContactList, getContacts } from "./contact-list"
|
||||
|
||||
// TODO Add remaining event types
|
||||
|
||||
// TODO
|
||||
// Think about this more
|
||||
// Perhaps the best option is for all these factory methods to have an overload which also accept a private
|
||||
// key as last parameter and return the event already signed
|
||||
// Or maybe opts: { sign?: boolean | HexOrBechPrivateKey } setting sign to true should use nip07, setting
|
||||
// it to a string will use that string as the private key
|
||||
|
||||
export enum EventKind {
|
||||
SetMetadata = 0, // NIP-01
|
||||
TextNote = 1, // NIP-01
|
||||
@ -48,50 +60,22 @@ export interface RawEvent {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface SetMetadata extends RawEvent {
|
||||
kind: EventKind.SetMetadata
|
||||
|
||||
/**
|
||||
* Get the user metadata specified in this event.
|
||||
*/
|
||||
getUserMetadata(): UserMetadata
|
||||
}
|
||||
|
||||
export interface TextNote extends RawEvent {
|
||||
kind: EventKind.TextNote
|
||||
}
|
||||
|
||||
interface DirectMessage extends RawEvent {
|
||||
kind: EventKind.DirectMessage
|
||||
|
||||
/**
|
||||
* Get the message plaintext, or undefined if this client is not the recipient.
|
||||
*/
|
||||
getMessage(priv?: HexOrBechPrivateKey): Promise<string | undefined>
|
||||
/**
|
||||
* Get the recipient pubkey.
|
||||
*/
|
||||
getRecipient(): PublicKey
|
||||
/**
|
||||
* Get the event ID of the previous message.
|
||||
*/
|
||||
getPrevious(): EventId | undefined
|
||||
}
|
||||
|
||||
export interface Unknown extends RawEvent {
|
||||
kind: Exclude<
|
||||
EventKind,
|
||||
EventKind.SetMetadata | EventKind.TextNote | EventKind.DirectMessage
|
||||
| EventKind.SetMetadata
|
||||
| EventKind.TextNote
|
||||
| EventKind.DirectMessage
|
||||
| EventKind.ContactList
|
||||
>
|
||||
}
|
||||
|
||||
export type Event = SetMetadata | TextNote | DirectMessage | Unknown
|
||||
|
||||
export interface UserMetadata {
|
||||
name: string
|
||||
about: string
|
||||
picture: string
|
||||
}
|
||||
export type Event =
|
||||
| SetMetadata
|
||||
| TextNote
|
||||
| ContactList
|
||||
| DirectMessage
|
||||
| Unknown
|
||||
|
||||
/**
|
||||
* Event ID encoded as hex.
|
||||
@ -110,10 +94,11 @@ export type Unsigned<T extends Event | RawEvent> = {
|
||||
pubkey?: PublicKey
|
||||
}
|
||||
|
||||
// TODO This doesn't need to be exposed by the lib
|
||||
/**
|
||||
* Same as @see {@link Unsigned}, but with the pubkey field.
|
||||
*/
|
||||
type UnsignedWithPubkey<T extends Event | RawEvent> = {
|
||||
export type UnsignedWithPubkey<T extends Event | RawEvent> = {
|
||||
[Property in keyof T as Exclude<
|
||||
Property,
|
||||
"id" | "sig" | "created_at"
|
||||
@ -149,59 +134,10 @@ export async function signEvent<T extends Event | RawEvent>(
|
||||
}
|
||||
}
|
||||
|
||||
export function createTextNote(content: string): Unsigned<TextNote> {
|
||||
return {
|
||||
kind: EventKind.TextNote,
|
||||
tags: [],
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
export function createSetMetadata(
|
||||
content: UserMetadata
|
||||
): Unsigned<SetMetadata> {
|
||||
return {
|
||||
kind: EventKind.SetMetadata,
|
||||
tags: [],
|
||||
content: JSON.stringify(content),
|
||||
getUserMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This is incomplete
|
||||
// TODO Since you already have the private key, maybe this should return the message already signed?
|
||||
// Think about this more
|
||||
// Perhaps the best option is for all these factory methods to have an overload which also accept a private
|
||||
// key as last parameter and return the event already signed, whereas for this method that would be
|
||||
// mandatory
|
||||
// E.g. opts: { sign?: boolean | HexOrBechPrivateKey } setting sign to true should use nip07
|
||||
export async function createDirectMessage({
|
||||
message,
|
||||
recipient,
|
||||
priv,
|
||||
}: {
|
||||
message: string
|
||||
recipient: PublicKey
|
||||
priv: PrivateKey
|
||||
}): Promise<Unsigned<DirectMessage>> {
|
||||
recipient = parsePublicKey(recipient)
|
||||
priv = parsePrivateKey(priv)
|
||||
const { data, iv } = await aesEncryptBase64(priv, recipient, message)
|
||||
return {
|
||||
kind: EventKind.DirectMessage,
|
||||
tags: [["p", recipient]],
|
||||
content: `${data}?iv=${iv}`,
|
||||
getMessage,
|
||||
getRecipient,
|
||||
getPrevious,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an event from its raw format.
|
||||
*/
|
||||
export async function parseEvent(event: RawEvent): Promise<Event> {
|
||||
// TODO Validate all the fields. Lowercase hex fields, etc. Make sure everything is correct.
|
||||
if (event.id !== (await serializeEventId(event))) {
|
||||
throw new NostrError(
|
||||
`invalid id ${event.id} for event ${JSON.stringify(
|
||||
@ -213,6 +149,9 @@ export async function parseEvent(event: RawEvent): Promise<Event> {
|
||||
throw new NostrError(`invalid signature for event ${JSON.stringify(event)}`)
|
||||
}
|
||||
|
||||
// TODO Validate all the fields. Lowercase hex fields, etc. Make sure everything is correct.
|
||||
// TODO Also validate that tags have at least one element
|
||||
|
||||
if (event.kind === EventKind.TextNote) {
|
||||
return {
|
||||
...event,
|
||||
@ -238,6 +177,14 @@ export async function parseEvent(event: RawEvent): Promise<Event> {
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === EventKind.ContactList) {
|
||||
return {
|
||||
...event,
|
||||
kind: EventKind.ContactList,
|
||||
getContacts,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...event,
|
||||
kind: event.kind,
|
||||
@ -258,72 +205,6 @@ async function serializeEventId(
|
||||
return await sha256(Uint8Array.from(charCodes(serialized)))
|
||||
}
|
||||
|
||||
function getUserMetadata(this: Unsigned<RawEvent>): UserMetadata {
|
||||
const userMetadata = parseJson(this.content)
|
||||
if (
|
||||
typeof userMetadata.name !== "string" ||
|
||||
typeof userMetadata.about !== "string" ||
|
||||
typeof userMetadata.picture !== "string"
|
||||
) {
|
||||
throw new NostrError(
|
||||
`invalid user metadata ${userMetadata} in ${JSON.stringify(this)}`
|
||||
)
|
||||
}
|
||||
return userMetadata
|
||||
}
|
||||
|
||||
async function getMessage(
|
||||
this: UnsignedWithPubkey<DirectMessage>,
|
||||
priv?: HexOrBechPrivateKey
|
||||
): Promise<string | undefined> {
|
||||
if (priv !== undefined) {
|
||||
priv = parsePrivateKey(priv)
|
||||
}
|
||||
const [data, iv] = this.content.split("?iv=")
|
||||
if (data === undefined || iv === undefined) {
|
||||
throw new NostrError(`invalid direct message content ${this.content}`)
|
||||
}
|
||||
if (priv === undefined) {
|
||||
// TODO Try to use NIP-07
|
||||
throw new NostrError("todo")
|
||||
} else if (getPublicKey(priv) === this.getRecipient()) {
|
||||
return await aesDecryptBase64(this.pubkey, priv, { data, iv })
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getRecipient(this: Unsigned<RawEvent>): PublicKey {
|
||||
const recipientTag = this.tags.find((tag) => tag[0] === "p")
|
||||
if (typeof recipientTag?.[1] !== "string") {
|
||||
throw new NostrError(
|
||||
`expected "p" tag to be of type string, but got ${
|
||||
recipientTag?.[1]
|
||||
} in ${JSON.stringify(this)}`
|
||||
)
|
||||
}
|
||||
return recipientTag[1]
|
||||
}
|
||||
|
||||
function getPrevious(this: Unsigned<RawEvent>): EventId | undefined {
|
||||
const previousTag = this.tags.find((tag) => tag[0] === "e")
|
||||
if (typeof previousTag?.[1] !== "string") {
|
||||
throw new NostrError(
|
||||
`expected "e" tag to be of type string, but got ${
|
||||
previousTag?.[1]
|
||||
} in ${JSON.stringify(this)}`
|
||||
)
|
||||
}
|
||||
return defined(previousTag?.[1])
|
||||
}
|
||||
|
||||
function parseJson(data: string) {
|
||||
try {
|
||||
return JSON.parse(data)
|
||||
} catch (e) {
|
||||
throw new NostrError(`invalid json: ${e}: ${data}`)
|
||||
}
|
||||
}
|
||||
|
||||
function* charCodes(data: string): Iterable<number> {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
yield data.charCodeAt(i)
|
50
packages/nostr/src/event/set-metadata.ts
Normal file
50
packages/nostr/src/event/set-metadata.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { EventKind, RawEvent, Unsigned } from "."
|
||||
import { NostrError, parseJson } from "../common"
|
||||
|
||||
/**
|
||||
* Set metadata event. Used for disseminating use profile information.
|
||||
*
|
||||
* Related NIPs: NIP-01.
|
||||
*/
|
||||
export interface SetMetadata extends RawEvent {
|
||||
kind: EventKind.SetMetadata
|
||||
|
||||
/**
|
||||
* Get the user metadata specified in this event.
|
||||
*/
|
||||
getUserMetadata(): UserMetadata
|
||||
}
|
||||
|
||||
export interface UserMetadata {
|
||||
name: string
|
||||
about: string
|
||||
picture: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a set metadata event.
|
||||
*/
|
||||
export function createSetMetadata(
|
||||
content: UserMetadata
|
||||
): Unsigned<SetMetadata> {
|
||||
return {
|
||||
kind: EventKind.SetMetadata,
|
||||
tags: [],
|
||||
content: JSON.stringify(content),
|
||||
getUserMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserMetadata(this: Unsigned<RawEvent>): UserMetadata {
|
||||
const userMetadata = parseJson(this.content)
|
||||
if (
|
||||
typeof userMetadata.name !== "string" ||
|
||||
typeof userMetadata.about !== "string" ||
|
||||
typeof userMetadata.picture !== "string"
|
||||
) {
|
||||
throw new NostrError(
|
||||
`invalid user metadata ${userMetadata} in ${JSON.stringify(this)}`
|
||||
)
|
||||
}
|
||||
return userMetadata
|
||||
}
|
18
packages/nostr/src/event/text.ts
Normal file
18
packages/nostr/src/event/text.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { EventKind, RawEvent, Unsigned } from "."
|
||||
|
||||
/**
|
||||
* A text note event. Used for transmitting user posts.
|
||||
*
|
||||
* Related NIPs: NIP-01.
|
||||
*/
|
||||
export interface TextNote extends RawEvent {
|
||||
kind: EventKind.TextNote
|
||||
}
|
||||
|
||||
export function createTextNote(content: string): Unsigned<TextNote> {
|
||||
return {
|
||||
kind: EventKind.TextNote,
|
||||
tags: [],
|
||||
content,
|
||||
}
|
||||
}
|
56
packages/nostr/test/contact-list.ts
Normal file
56
packages/nostr/test/contact-list.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import assert from "assert"
|
||||
import { EventKind, signEvent } from "../src/event"
|
||||
import { createContactList } from "../src/event/contact-list"
|
||||
import { setup } from "./setup"
|
||||
|
||||
describe("contact-list", () => {
|
||||
it("publish and receive the contact list", (done) => {
|
||||
setup(done, ({ publisher, subscriber, subscriberSecret, done }) => {
|
||||
const contacts = [
|
||||
{
|
||||
pubkey:
|
||||
"db9df52f7fcaf30b2718ad17e4c5521058bb20b95073b5c4ff53221b36447c4f",
|
||||
relay: undefined,
|
||||
petname: undefined,
|
||||
},
|
||||
{
|
||||
pubkey:
|
||||
"94d5ce4cb06f67cab69a2f6e28e0a795222a74ac6a1dd6223743913cc99eaf37",
|
||||
relay: new URL("ws://example.com"),
|
||||
petname: undefined,
|
||||
},
|
||||
{
|
||||
pubkey:
|
||||
"e6e9a25dbf3e931c991f43c97378e294c25f59e88adc91eda11ed17249a00c20",
|
||||
relay: undefined,
|
||||
petname: "john",
|
||||
},
|
||||
{
|
||||
pubkey:
|
||||
"13d629a3a879f2157199491408711ff5e1450002a9f9d8b0ad750f1c6b96661d",
|
||||
relay: new URL("ws://example2.com"),
|
||||
petname: "jack",
|
||||
},
|
||||
]
|
||||
|
||||
subscriber.on("event", ({ event }) => {
|
||||
assert.strictEqual(event.kind, EventKind.ContactList)
|
||||
assert.strictEqual(event.content, "")
|
||||
if (event.kind === EventKind.ContactList) {
|
||||
assert.deepStrictEqual(event.getContacts(), contacts)
|
||||
}
|
||||
done()
|
||||
})
|
||||
|
||||
subscriber.subscribe([])
|
||||
|
||||
// After the subscription event sync is done, publish the test event.
|
||||
subscriber.on("eose", async () => {
|
||||
// TODO No signEvent, have a convenient way to do this
|
||||
publisher.publish(
|
||||
await signEvent(createContactList(contacts), subscriberSecret)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -1,7 +1,8 @@
|
||||
import { createDirectMessage, EventKind, signEvent } from "../src/event"
|
||||
import { EventKind, signEvent } from "../src/event"
|
||||
import { parsePublicKey } from "../src/crypto"
|
||||
import assert from "assert"
|
||||
import { setup } from "./setup"
|
||||
import { createDirectMessage } from "../src/event/direct-message"
|
||||
|
||||
describe("dm", () => {
|
||||
const message = "for your eyes only"
|
||||
@ -112,12 +113,11 @@ describe("dm", () => {
|
||||
|
||||
subscriber.on("eose", async () => {
|
||||
// TODO No signEvent, do something more convenient
|
||||
const event = await signEvent(
|
||||
await createDirectMessage({
|
||||
const event = await createDirectMessage(
|
||||
{
|
||||
message,
|
||||
recipient: recipientPubkey,
|
||||
priv: publisherSecret,
|
||||
}),
|
||||
},
|
||||
publisherSecret
|
||||
)
|
||||
publisher.publish(event)
|
||||
|
@ -1,13 +1,8 @@
|
||||
import {
|
||||
createTextNote,
|
||||
EventKind,
|
||||
signEvent,
|
||||
TextNote,
|
||||
Unsigned,
|
||||
} from "../src/event"
|
||||
import { EventKind, signEvent, Unsigned } from "../src/event"
|
||||
import { parsePublicKey } from "../src/crypto"
|
||||
import assert from "assert"
|
||||
import { setup } from "./setup"
|
||||
import { createTextNote, TextNote } from "../src/event/text"
|
||||
|
||||
describe("text note", () => {
|
||||
const note = "hello world"
|
||||
@ -41,18 +36,20 @@ describe("text note", () => {
|
||||
const subscriptionId = subscriber.subscribe([])
|
||||
|
||||
// After the subscription event sync is done, publish the test event.
|
||||
subscriber.on("eose", (id, nostr) => {
|
||||
subscriber.on("eose", async (id, nostr) => {
|
||||
assert.strictEqual(nostr, subscriber)
|
||||
assert.strictEqual(id, subscriptionId)
|
||||
|
||||
// TODO No signEvent, have a convenient way to do this
|
||||
signEvent(
|
||||
{
|
||||
...createTextNote(note),
|
||||
created_at: timestamp,
|
||||
} as Unsigned<TextNote>,
|
||||
publisherSecret
|
||||
).then((event) => publisher.publish(event))
|
||||
publisher.publish(
|
||||
await signEvent(
|
||||
{
|
||||
...createTextNote(note),
|
||||
created_at: timestamp,
|
||||
} as Unsigned<TextNote>,
|
||||
publisherSecret
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user