Profile / Thread styles

This commit is contained in:
2023-07-24 15:30:21 +01:00
parent d292e658fc
commit 076d5d8cd5
67 changed files with 684 additions and 602 deletions

View File

@ -226,12 +226,12 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "EVENT") {
if (typeof json[1] !== "string") {
throw new NostrError(
`second element of "EVENT" should be a string, but wasn't: ${data}`
`second element of "EVENT" should be a string, but wasn't: ${data}`,
)
}
if (typeof json[2] !== "object") {
throw new NostrError(
`second element of "EVENT" should be an object, but wasn't: ${data}`
`second element of "EVENT" should be an object, but wasn't: ${data}`,
)
}
const event = parseEventData(json[2])
@ -246,7 +246,7 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "NOTICE") {
if (typeof json[1] !== "string") {
throw new NostrError(
`second element of "NOTICE" should be a string, but wasn't: ${data}`
`second element of "NOTICE" should be a string, but wasn't: ${data}`,
)
}
return {
@ -259,17 +259,17 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "OK") {
if (typeof json[1] !== "string") {
throw new NostrError(
`second element of "OK" should be a string, but wasn't: ${data}`
`second element of "OK" should be a string, but wasn't: ${data}`,
)
}
if (typeof json[2] !== "boolean") {
throw new NostrError(
`third element of "OK" should be a boolean, but wasn't: ${data}`
`third element of "OK" should be a boolean, but wasn't: ${data}`,
)
}
if (typeof json[3] !== "string") {
throw new NostrError(
`fourth element of "OK" should be a string, but wasn't: ${data}`
`fourth element of "OK" should be a string, but wasn't: ${data}`,
)
}
return {
@ -284,7 +284,7 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "EOSE") {
if (typeof json[1] !== "string") {
throw new NostrError(
`second element of "EOSE" should be a string, but wasn't: ${data}`
`second element of "EOSE" should be a string, but wasn't: ${data}`,
)
}
return {
@ -312,7 +312,7 @@ function parseEventData(json: { [key: string]: unknown }): RawEvent {
typeof json["kind"] !== "number" ||
!(json["tags"] instanceof Array) ||
!json["tags"].every(
(x) => x instanceof Array && x.every((y) => typeof y === "string")
(x) => x instanceof Array && x.every((y) => typeof y === "string"),
) ||
typeof json["content"] !== "string" ||
typeof json["sig"] !== "string"

View File

@ -13,7 +13,7 @@ export class EventEmitter extends Base {
override addListener(eventName: "newListener", listener: NewListener): this
override addListener(
eventName: "removeListener",
listener: RemoveListener
listener: RemoveListener,
): this
override addListener(eventName: "open", listener: OpenListener): this
override addListener(eventName: "close", listener: CloseListener): this
@ -36,7 +36,7 @@ export class EventEmitter extends Base {
override emit(
eventName: "eose",
subscriptionId: SubscriptionId,
nostr: Nostr
nostr: Nostr,
): boolean
override emit(eventName: "error", err: unknown, nostr: Nostr): boolean
override emit(eventName: EventName, ...args: unknown[]): boolean {
@ -101,11 +101,11 @@ export class EventEmitter extends Base {
override prependListener(
eventName: "newListener",
listener: NewListener
listener: NewListener,
): this
override prependListener(
eventName: "removeListener",
listener: RemoveListener
listener: RemoveListener,
): this
override prependListener(eventName: "open", listener: OpenListener): this
override prependListener(eventName: "close", listener: CloseListener): this
@ -120,30 +120,30 @@ export class EventEmitter extends Base {
override prependOnceListener(
eventName: "newListener",
listener: NewListener
listener: NewListener,
): this
override prependOnceListener(
eventName: "removeListener",
listener: RemoveListener
listener: RemoveListener,
): this
override prependOnceListener(eventName: "open", listener: OpenListener): this
override prependOnceListener(
eventName: "close",
listener: CloseListener
listener: CloseListener,
): this
override prependOnceListener(
eventName: "event",
listener: EventListener
listener: EventListener,
): this
override prependOnceListener(
eventName: "notice",
listener: NoticeListener
listener: NoticeListener,
): this
override prependOnceListener(eventName: "ok", listener: OkListener): this
override prependOnceListener(eventName: "eose", listener: EoseListener): this
override prependOnceListener(
eventName: "error",
listener: ErrorListener
listener: ErrorListener,
): this
override prependOnceListener(eventName: EventName, listener: Listener): this {
return super.prependOnceListener(eventName, listener)
@ -156,7 +156,7 @@ export class EventEmitter extends Base {
override removeListener(eventName: "newListener", listener: NewListener): this
override removeListener(
eventName: "removeListener",
listener: RemoveListener
listener: RemoveListener,
): this
override removeListener(eventName: "open", listener: OpenListener): this
override removeListener(eventName: "close", listener: CloseListener): this

View File

@ -43,18 +43,18 @@ export class Nostr extends EventEmitter {
*/
open(
url: URL | string,
opts?: { read?: boolean; write?: boolean; fetchInfo?: boolean }
opts?: { read?: boolean; write?: boolean; fetchInfo?: boolean },
): void {
const relayUrl = new URL(url)
// If the connection already exists, update the options.
const existingConn = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString()
(c) => c.relay.url.toString() === relayUrl.toString(),
)
if (existingConn !== undefined) {
if (opts === undefined) {
throw new NostrError(
`called connect with existing connection ${url}, but options were not specified`
`called connect with existing connection ${url}, but options were not specified`,
)
}
if (opts.read !== undefined) {
@ -88,7 +88,7 @@ export class Nostr extends EventEmitter {
event: parseEvent(msg.event),
subscriptionId: msg.subscriptionId,
},
this
this,
)
} else if (msg.kind === "notice") {
this.emit("notice", msg.notice, this)
@ -101,7 +101,7 @@ export class Nostr extends EventEmitter {
ok: msg.ok,
message: msg.message,
},
this
this,
)
} else if (msg.kind === "eose") {
this.emit("eose", msg.subscriptionId, this)
@ -116,13 +116,13 @@ export class Nostr extends EventEmitter {
onOpen: async () => {
// Update the connection readyState.
const conn = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString()
(c) => c.relay.url.toString() === relayUrl.toString(),
)
if (conn === undefined) {
this.#error(
new NostrError(
`bug: expected connection to ${relayUrl.toString()} to be in the map`
)
`bug: expected connection to ${relayUrl.toString()} to be in the map`,
),
)
} else {
if (conn.relay.readyState !== ReadyState.CONNECTING) {
@ -130,8 +130,8 @@ export class Nostr extends EventEmitter {
new NostrError(
`bug: expected connection to ${relayUrl.toString()} to have readyState CONNECTING, got ${
conn.relay.readyState
}`
)
}`,
),
)
}
conn.relay = {
@ -148,13 +148,13 @@ export class Nostr extends EventEmitter {
onClose: () => {
// Update the connection readyState.
const conn = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString()
(c) => c.relay.url.toString() === relayUrl.toString(),
)
if (conn === undefined) {
this.#error(
new NostrError(
`bug: expected connection to ${relayUrl.toString()} to be in the map`
)
`bug: expected connection to ${relayUrl.toString()} to be in the map`,
),
)
} else {
conn.relay.readyState = ReadyState.CLOSED
@ -207,7 +207,7 @@ export class Nostr extends EventEmitter {
}
const relayUrl = new URL(url)
const c = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString()
(c) => c.relay.url.toString() === relayUrl.toString(),
)
if (c === undefined) {
throw new NostrError(`connection to ${url} doesn't exist`)
@ -231,7 +231,7 @@ export class Nostr extends EventEmitter {
*/
subscribe(
filters: Filters[],
subscriptionId: SubscriptionId = randomSubscriptionId()
subscriptionId: SubscriptionId = randomSubscriptionId(),
): SubscriptionId {
this.#subscriptions.set(subscriptionId, filters)
for (const { conn, read } of this.#conns.values()) {

View File

@ -75,32 +75,32 @@ export async function fetchRelayInfo(url: URL | string): Promise<RelayInfo> {
info.name = undefined
throw new NostrError(
`invalid relay info, expected "name" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
if (info.description !== undefined && typeof info.description !== "string") {
info.description = undefined
throw new NostrError(
`invalid relay info, expected "description" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
if (info.pubkey !== undefined && typeof info.pubkey !== "string") {
info.pubkey = undefined
throw new NostrError(
`invalid relay info, expected "pubkey" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
if (info.contact !== undefined && typeof info.contact !== "string") {
info.contact = undefined
throw new NostrError(
`invalid relay info, expected "contact" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
if (info.supported_nips !== undefined) {
@ -109,16 +109,16 @@ export async function fetchRelayInfo(url: URL | string): Promise<RelayInfo> {
info.supported_nips = undefined
throw new NostrError(
`invalid relay info, expected "supported_nips" elements to be numbers: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
} else {
info.supported_nips = undefined
throw new NostrError(
`invalid relay info, expected "supported_nips" to be an array: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
}
@ -126,16 +126,16 @@ export async function fetchRelayInfo(url: URL | string): Promise<RelayInfo> {
info.software = undefined
throw new NostrError(
`invalid relay info, expected "software" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
if (info.version !== undefined && typeof info.version !== "string") {
info.version = undefined
throw new NostrError(
`invalid relay info, expected "version" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
return info

View File

@ -99,7 +99,7 @@ export function schnorrVerify(sig: Hex, data: Hex, key: PublicKey): boolean {
export async function aesEncryptBase64(
sender: PrivateKey,
recipient: PublicKey,
plaintext: string
plaintext: string,
): Promise<AesEncryptedBase64> {
const sharedPoint = secp.secp256k1.getSharedSecret(sender, "02" + recipient)
const sharedKey = sharedPoint.slice(1, 33)
@ -109,7 +109,7 @@ export async function aesEncryptBase64(
sharedKey,
{ name: "AES-CBC" },
false,
["encrypt"]
["encrypt"],
)
const iv = window.crypto.getRandomValues(new Uint8Array(16))
const data = new TextEncoder().encode(plaintext)
@ -119,7 +119,7 @@ export async function aesEncryptBase64(
iv,
},
key,
data
data,
)
return {
data: base64.fromByteArray(new Uint8Array(encrypted)),
@ -131,7 +131,7 @@ export async function aesEncryptBase64(
const cipher = crypto.createCipheriv(
"aes-256-cbc",
Buffer.from(sharedKey),
iv
iv,
)
let encrypted = cipher.update(plaintext, "utf8", "base64")
encrypted += cipher.final("base64")
@ -145,7 +145,7 @@ export async function aesEncryptBase64(
export async function aesDecryptBase64(
sender: PublicKey,
recipient: PrivateKey,
{ data, iv }: AesEncryptedBase64
{ data, iv }: AesEncryptedBase64,
): Promise<string> {
const sharedPoint = secp.secp256k1.getSharedSecret(recipient, "02" + sender)
const sharedKey = sharedPoint.slice(1, 33)
@ -157,7 +157,7 @@ export async function aesDecryptBase64(
sharedKey,
{ name: "AES-CBC" },
false,
["decrypt"]
["decrypt"],
)
const plaintext = await window.crypto.subtle.decrypt(
{
@ -165,7 +165,7 @@ export async function aesDecryptBase64(
iv: decodedIv,
},
importedKey,
decodedData
decodedData,
)
return new TextDecoder().decode(plaintext)
} else {
@ -173,7 +173,7 @@ export async function aesDecryptBase64(
const decipher = crypto.createDecipheriv(
"aes-256-cbc",
Buffer.from(sharedKey),
base64.toByteArray(iv)
base64.toByteArray(iv),
)
const plaintext = decipher.update(data, "base64", "utf8")
return plaintext + decipher.final()

View File

@ -30,7 +30,7 @@ export interface Contact {
*/
export function createContactList(
contacts: Contact[],
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<ContactList> {
return signEvent(
{
@ -44,7 +44,7 @@ export function createContactList(
content: "",
getContacts,
},
priv
priv,
)
}
@ -57,8 +57,8 @@ export function getContacts(this: ContactList): Contact[] {
if (pubkey === undefined) {
throw new NostrError(
`missing contact pubkey for contact list event: ${JSON.stringify(
this
)}`
this,
)}`,
)
}
@ -70,7 +70,7 @@ export function getContacts(this: ContactList): Contact[] {
}
} catch (e) {
throw new NostrError(
`invalid relay URL for contact list event: ${JSON.stringify(this)}`
`invalid relay URL for contact list event: ${JSON.stringify(this)}`,
)
}

View File

@ -21,7 +21,7 @@ export interface Deletion extends RawEvent {
*/
export function createDeletion(
{ events, content }: { events: EventId[]; content?: string },
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<Deletion> {
return signEvent(
{
@ -30,7 +30,7 @@ export function createDeletion(
content: content ?? "",
getEvents,
},
priv
priv,
)
}
@ -40,7 +40,7 @@ export function getEvents(this: Deletion): EventId[] {
.map((tag) => {
if (tag[1] === undefined) {
throw new NostrError(
`invalid deletion event tag: ${JSON.stringify(tag)}`
`invalid deletion event tag: ${JSON.stringify(tag)}`,
)
}
return tag[1]

View File

@ -46,7 +46,7 @@ export async function createDirectMessage(
message: string
recipient: PublicKey
},
priv?: PrivateKey
priv?: PrivateKey,
): Promise<DirectMessage> {
recipient = parsePublicKey(recipient)
if (priv === undefined) {
@ -66,7 +66,7 @@ export async function createDirectMessage(
getRecipient,
getPrevious,
},
priv
priv,
)
} else {
priv = parsePrivateKey(priv)
@ -80,14 +80,14 @@ export async function createDirectMessage(
getRecipient,
getPrevious,
},
priv
priv,
)
}
}
export async function getMessage(
this: DirectMessage,
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<string | undefined> {
if (priv !== undefined) {
priv = parsePrivateKey(priv)
@ -114,9 +114,9 @@ export function getRecipient(this: DirectMessage): 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)}`
`expected "p" tag to be of type string, but got ${recipientTag?.[1]} in ${JSON.stringify(
this,
)}`,
)
}
return recipientTag[1]
@ -129,9 +129,9 @@ export function getPrevious(this: DirectMessage): EventId | 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)}`
`expected "e" tag to be of type string, but got ${previousTag?.[1]} in ${JSON.stringify(
this,
)}`,
)
}
return previousTag[1]

View File

@ -122,7 +122,7 @@ type UnsignedWithPubkey<T extends Event | RawEvent> = {
*/
export async function signEvent<T extends RawEvent>(
event: Unsigned<T>,
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<T> {
event.created_at ??= unixTimestamp()
if (priv !== undefined) {
@ -130,7 +130,7 @@ export async function signEvent<T extends RawEvent>(
event.pubkey = getPublicKey(priv)
const id = serializeEventId(
// This conversion is safe because the pubkey field is set above.
event as unknown as UnsignedWithPubkey<T>
event as unknown as UnsignedWithPubkey<T>,
)
event.id = id
event.sig = schnorrSign(id, priv)
@ -162,8 +162,8 @@ export function parseEvent(event: RawEvent): Event {
if (event.id !== serializeEventId(event)) {
throw new NostrError(
`invalid id ${event.id} for event ${JSON.stringify(
event
)}, expected ${serializeEventId(event)}`
event,
)}, expected ${serializeEventId(event)}`,
)
}
if (!schnorrVerify(event.sig, event.id, event.pubkey)) {

View File

@ -22,7 +22,7 @@ export interface SetMetadata extends RawEvent {
* @return The internet identifier. `undefined` if there is no internet identifier.
*/
verifyInternetIdentifier(
opts?: VerificationOptions
opts?: VerificationOptions,
): Promise<InternetIdentifier | undefined>
}
@ -38,7 +38,7 @@ export interface UserMetadata {
*/
export function createSetMetadata(
content: UserMetadata,
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<SetMetadata> {
return signEvent(
{
@ -48,7 +48,7 @@ export function createSetMetadata(
getUserMetadata,
verifyInternetIdentifier,
},
priv
priv,
)
}
@ -60,7 +60,7 @@ export function getUserMetadata(this: SetMetadata): UserMetadata {
typeof userMetadata.picture !== "string"
) {
throw new NostrError(
`invalid user metadata ${userMetadata} in ${JSON.stringify(this)}`
`invalid user metadata ${userMetadata} in ${JSON.stringify(this)}`,
)
}
return userMetadata
@ -68,7 +68,7 @@ export function getUserMetadata(this: SetMetadata): UserMetadata {
export async function verifyInternetIdentifier(
this: SetMetadata,
opts?: VerificationOptions
opts?: VerificationOptions,
): Promise<InternetIdentifier | undefined> {
const metadata = this.getUserMetadata()
if (metadata.nip05 === undefined) {
@ -81,14 +81,14 @@ export async function verifyInternetIdentifier(
!/^[a-zA-Z0-9-_]+$/.test(name)
) {
throw new NostrError(
`invalid NIP-05 internet identifier: ${metadata.nip05}`
`invalid NIP-05 internet identifier: ${metadata.nip05}`,
)
}
const res = await fetch(
`${
opts?.https === false ? "http" : "https"
}://${domain}/.well-known/nostr.json?name=${name}`,
{ redirect: "error" }
{ redirect: "error" },
)
const wellKnown = await res.json()
const pubkey = wellKnown.names?.[name]
@ -96,7 +96,7 @@ export async function verifyInternetIdentifier(
throw new NostrError(
`invalid NIP-05 internet identifier: ${
metadata.nip05
} pubkey does not match, ${JSON.stringify(wellKnown)}`
} pubkey does not match, ${JSON.stringify(wellKnown)}`,
)
}
const relays = wellKnown.relays?.[pubkey]

View File

@ -12,7 +12,7 @@ export interface TextNote extends RawEvent {
export function createTextNote(
content: string,
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<TextNote> {
return signEvent(
{
@ -20,6 +20,6 @@ export function createTextNote(
tags: [],
content,
},
priv
priv,
)
}

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@ -18,7 +18,7 @@ app.use("/", (req: express.Request, res: express.Response) => {
.readdirSync(path.join(__dirname, "..", "..", "dist", "test"))
.filter(
(f) =>
f.startsWith("test.") && !f.endsWith(".map") && !f.endsWith(".d.ts")
f.startsWith("test.") && !f.endsWith(".map") && !f.endsWith(".d.ts"),
)
.map((src) => `<script src="${src}"></script>`)
.join("\n")

View File

@ -30,7 +30,7 @@ export interface Setup {
export async function setup(
done: (e?: unknown) => void,
test: (setup: Setup) => void | Promise<void>
test: (setup: Setup) => void | Promise<void>,
) {
try {
await restartRelay()
@ -55,7 +55,7 @@ export async function setup(
const { data, iv } = await aesEncryptBase64(
parsePrivateKey(publisherSecret),
pubkey,
plaintext
plaintext,
)
return `${data}?iv=${iv}`
},
@ -67,7 +67,7 @@ export async function setup(
{
data,
iv,
}
},
)
},
},

View File

@ -52,7 +52,7 @@ describe("deletion", () => {
// After the text note has been published, delete it.
const deletion = await createDeletion(
{ events: [textNoteId] },
publisherSecret
publisherSecret,
)
deletionId = deletion.id
publisher.publish({
@ -66,7 +66,7 @@ describe("deletion", () => {
subscriber.subscribe([])
}
})
}
},
)
})
})

View File

@ -34,16 +34,16 @@ describe("direct-message", () => {
if (event.kind === EventKind.DirectMessage) {
assert.strictEqual(
event.getRecipient(),
parsePublicKey(subscriberPubkey)
parsePublicKey(subscriberPubkey),
)
assert.strictEqual(
await event.getMessage(subscriberSecret),
message
message,
)
}
done()
}
},
)
const subscriptionId = subscriber.subscribe([])
@ -54,11 +54,11 @@ describe("direct-message", () => {
message,
recipient: subscriberPubkey,
},
publisherSecret
publisherSecret,
)
publisher.publish(event)
})
}
},
)
})
@ -92,11 +92,11 @@ describe("direct-message", () => {
if (event.kind === EventKind.DirectMessage) {
assert.strictEqual(
event.getRecipient(),
parsePublicKey(recipientPubkey)
parsePublicKey(recipientPubkey),
)
assert.strictEqual(
await event.getMessage(subscriberSecret),
undefined
undefined,
)
}
@ -104,7 +104,7 @@ describe("direct-message", () => {
} catch (e) {
done(e)
}
}
},
)
const subscriptionId = subscriber.subscribe([])
@ -116,11 +116,11 @@ describe("direct-message", () => {
message,
recipient: recipientPubkey,
},
publisherSecret
publisherSecret,
)
publisher.publish(event)
})
}
},
)
})
})

View File

@ -34,7 +34,7 @@ describe("internet-identifier", () => {
picture: "",
nip05: "bob@localhost:12647",
},
publisherSecret
publisherSecret,
)),
})
})
@ -66,7 +66,7 @@ describe("internet-identifier", () => {
name: "",
picture: "",
},
publisherSecret
publisherSecret,
)),
})
})

View File

@ -16,7 +16,7 @@ describe("relay info", () => {
assert.ok((relay.info.supported_nips?.length ?? 0) > 0)
assert.strictEqual(
relay.info.software,
"https://git.sr.ht/~gheartsfield/nostr-rs-relay"
"https://git.sr.ht/~gheartsfield/nostr-rs-relay",
)
assert.strictEqual(relay.info.version, "0.8.8")
}

View File

@ -43,12 +43,12 @@ describe("set metadata", () => {
publisher.publish({
...(await createSetMetadata(
{ name, about, picture },
publisherSecret
publisherSecret,
)),
created_at: timestamp,
})
})
}
},
)
})
})

View File

@ -30,7 +30,7 @@ describe("text note", () => {
assert.strictEqual(event.content, note)
assert.strictEqual(actualSubscriptionId, subscriptionId)
done()
}
},
)
const subscriptionId = subscriber.subscribe([])
@ -45,7 +45,7 @@ describe("text note", () => {
created_at: timestamp,
})
})
}
},
)
})