Add some utils, add support for nip44 encryption to nip46 signer
This commit is contained in:
@@ -91,6 +91,8 @@ export const mergeRight = <T extends Record<string, any>>(a: T, b: T) => ({...a,
|
||||
|
||||
export const between = (low: number, high: number, n: number) => n > low && n < high
|
||||
|
||||
export const randomInt = (min = 0, max = 9) => min + Math.round(Math.random()) * (max - min)
|
||||
|
||||
export const randomId = (): string => Math.random().toString().slice(2)
|
||||
|
||||
export const stripProtocol = (url: string) => url.replace(/.*:\/\//, "")
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {nip04, finalizeEvent, getPublicKey} from "nostr-tools"
|
||||
import {finalizeEvent, getPublicKey} from "nostr-tools"
|
||||
import {hexToBytes} from '@noble/hashes/utils'
|
||||
import {Emitter, tryCatch, randomId, sleep, equals, now} from "@welshman/lib"
|
||||
import {createEvent, TrustedEvent, EventTemplate, NOSTR_CONNECT} from "@welshman/util"
|
||||
import {subscribe, publish, Subscription} from "@welshman/net"
|
||||
import {ISigner, decrypt, hash, own} from '../util'
|
||||
import {nip04, nip44, ISigner, decrypt, hash, own} from '../util'
|
||||
import {Nip01Signer} from './nip01'
|
||||
|
||||
export type Algorithm = "nip04" | "nip44"
|
||||
|
||||
export type Nip46Handler = {
|
||||
relays: string[]
|
||||
pubkey?: string
|
||||
@@ -20,10 +22,6 @@ export type Nip46Response = {
|
||||
|
||||
let singleton: Nip46Broker
|
||||
|
||||
// FIXME set the full list of requested perms
|
||||
const Perms =
|
||||
"nip04_encrypt,nip04_decrypt,sign_event:0,sign_event:1,sign_event:4,sign_event:6,sign_event:7"
|
||||
|
||||
export class Nip46Broker extends Emitter {
|
||||
#sub: Subscription
|
||||
#signer: ISigner
|
||||
@@ -31,14 +29,15 @@ export class Nip46Broker extends Emitter {
|
||||
#closed = false
|
||||
#connectResult: any
|
||||
|
||||
static get(pubkey: string, secret: string, handler: Nip46Handler) {
|
||||
static get(pubkey: string, secret: string, handler: Nip46Handler, algorithm: Algorithm = "nip04") {
|
||||
if (
|
||||
singleton?.pubkey !== pubkey ||
|
||||
singleton?.secret !== secret ||
|
||||
!equals(singleton?.handler, handler)
|
||||
!equals(singleton?.handler, handler) ||
|
||||
singleton?.algorithm !== algorithm
|
||||
) {
|
||||
singleton?.teardown()
|
||||
singleton = new Nip46Broker(pubkey, secret, handler)
|
||||
singleton = new Nip46Broker(pubkey, secret, handler, algorithm)
|
||||
}
|
||||
|
||||
return singleton
|
||||
@@ -48,6 +47,7 @@ export class Nip46Broker extends Emitter {
|
||||
readonly pubkey: string,
|
||||
readonly secret: string,
|
||||
readonly handler: Nip46Handler,
|
||||
readonly algorithm: Algorithm
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -98,7 +98,8 @@ export class Nip46Broker extends Emitter {
|
||||
const id = randomId()
|
||||
const pubkey = admin ? this.handler.pubkey! : this.pubkey
|
||||
const payload = JSON.stringify({id, method, params})
|
||||
const content = await nip04.encrypt(this.secret, pubkey, payload)
|
||||
const crypt = this.algorithm === "nip04" ? nip04 : nip44
|
||||
const content = await crypt.encrypt(pubkey, this.secret, payload)
|
||||
const template = createEvent(NOSTR_CONNECT, {content, tags: [["p", pubkey]]})
|
||||
const event = finalizeEvent(template, this.secret as any)
|
||||
|
||||
@@ -119,17 +120,17 @@ export class Nip46Broker extends Emitter {
|
||||
})
|
||||
}
|
||||
|
||||
createAccount = (username: string) => {
|
||||
createAccount = (username: string, perms = "") => {
|
||||
if (!this.handler.domain) {
|
||||
throw new Error("Unable to create an account without a handler domain")
|
||||
}
|
||||
|
||||
return this.request("create_account", [username, this.handler.domain, "", Perms], true)
|
||||
return this.request("create_account", [username, this.handler.domain, "", perms], true)
|
||||
}
|
||||
|
||||
connect = async (token = "") => {
|
||||
connect = async (token = "", perms = "") => {
|
||||
if (!this.#connectResult) {
|
||||
const params = [this.pubkey, token, Perms]
|
||||
const params = [this.pubkey, token, perms]
|
||||
|
||||
this.#connectResult = await this.request("connect", params)
|
||||
}
|
||||
|
||||
+16
-20
@@ -1,7 +1,8 @@
|
||||
import {throttle} from "throttle-debounce"
|
||||
import {derived} from "svelte/store"
|
||||
import type {Readable, Writable} from "svelte/store"
|
||||
import type {Readable, Updater, Writable, Subscriber, Unsubscriber} from "svelte/store"
|
||||
import {identity, batch, partition, first} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import type {Repository} from "@welshman/util"
|
||||
import {matchFilters, getIdAndAddress, getIdFilters} from "@welshman/util"
|
||||
import type {Filter, CustomEvent} from "@welshman/util"
|
||||
@@ -16,12 +17,10 @@ export const getter = <T>(store: Readable<T>) => {
|
||||
return () => value
|
||||
}
|
||||
|
||||
type Stop = () => void
|
||||
type Sub<T> = (x: T) => void
|
||||
type Start<T> = (set: Sub<T>) => Stop
|
||||
type Start<T> = (set: Subscriber<T>) => Unsubscriber
|
||||
|
||||
export const custom = <T>(start: Start<T>, opts: {throttle?: number} = {}) => {
|
||||
const subs: Sub<T>[] = []
|
||||
const subs: Subscriber<T>[] = []
|
||||
|
||||
let value: T
|
||||
let stop: () => void
|
||||
@@ -36,7 +35,7 @@ export const custom = <T>(start: Start<T>, opts: {throttle?: number} = {}) => {
|
||||
|
||||
return {
|
||||
set,
|
||||
subscribe: (sub: Sub<T>) => {
|
||||
subscribe: (sub: Subscriber<T>) => {
|
||||
if (opts.throttle) {
|
||||
sub = throttle(opts.throttle, sub)
|
||||
}
|
||||
@@ -71,8 +70,8 @@ export function withGetter<T>(store: Readable<T> | Writable<T>) {
|
||||
export const throttled = <T>(delay: number, store: Readable<T>) =>
|
||||
custom(set => store.subscribe(throttle(delay, set)))
|
||||
|
||||
export const createEventStore = (repository: Repository) => {
|
||||
let subs: Sub<CustomEvent[]>[] = []
|
||||
export const createEventStore = (repository: Repository): Writable<CustomEvent[]> => {
|
||||
let subs: Subscriber<CustomEvent[]>[] = []
|
||||
|
||||
const onUpdate = throttle(300, () => {
|
||||
const $events = repository.dump()
|
||||
@@ -83,9 +82,9 @@ export const createEventStore = (repository: Repository) => {
|
||||
})
|
||||
|
||||
return {
|
||||
get: () => repository.dump(),
|
||||
set: (events: CustomEvent[]) => repository.load(events),
|
||||
subscribe: (f: Sub<CustomEvent[]>) => {
|
||||
update: (f: Updater<CustomEvent[]>) => repository.load(f(repository.dump())),
|
||||
subscribe: (f: Subscriber<CustomEvent[]>) => {
|
||||
f(repository.dump())
|
||||
|
||||
subs.push(f)
|
||||
@@ -105,17 +104,15 @@ export const createEventStore = (repository: Repository) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const deriveEventsMapped = <T>({
|
||||
export const deriveEventsMapped = <T>(repository: Repository, {
|
||||
filters,
|
||||
repository,
|
||||
eventToItem,
|
||||
itemToEvent,
|
||||
throttle = 300,
|
||||
includeDeleted = false,
|
||||
}: {
|
||||
filters: Filter[]
|
||||
repository: Repository,
|
||||
eventToItem: (event: CustomEvent) => T | Promise<T>
|
||||
eventToItem: (event: CustomEvent) => Maybe<T | Promise<T>>
|
||||
itemToEvent: (item: T) => CustomEvent
|
||||
throttle?: number
|
||||
includeDeleted?: boolean
|
||||
@@ -205,24 +202,23 @@ export const deriveEventsMapped = <T>({
|
||||
return () => repository.off("update", onUpdate)
|
||||
}, {throttle})
|
||||
|
||||
export const deriveEvents = (opts: {repository: Repository, filters: Filter[], includeDeleted?: boolean}) =>
|
||||
deriveEventsMapped<CustomEvent>({
|
||||
export const deriveEvents = (repository: Repository, opts: {filters: Filter[], includeDeleted?: boolean}) =>
|
||||
deriveEventsMapped<CustomEvent>(repository, {
|
||||
...opts,
|
||||
eventToItem: identity,
|
||||
itemToEvent: identity,
|
||||
})
|
||||
|
||||
export const deriveEvent = ({repository, idOrAddress}: {repository: Repository, idOrAddress: string}) =>
|
||||
export const deriveEvent = (repository: Repository, idOrAddress: string) =>
|
||||
derived(
|
||||
deriveEvents({
|
||||
repository,
|
||||
deriveEvents(repository, {
|
||||
filters: getIdFilters([idOrAddress]),
|
||||
includeDeleted: true,
|
||||
}),
|
||||
first
|
||||
)
|
||||
|
||||
export const deriveIsDeletedByAddress = ({repository, event}: {repository: Repository, event: CustomEvent}) =>
|
||||
export const deriveIsDeletedByAddress = (repository: Repository, event: CustomEvent) =>
|
||||
custom<boolean>(setter => {
|
||||
setter(repository.isDeletedByAddress(event))
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ export class Address {
|
||||
|
||||
export const getAddress = (e: AddressableEvent) => Address.fromEvent(e).toString()
|
||||
|
||||
export const isGroupAddress = (a: string, ...args: unknown[]) => Address.from(a).kind === GROUP
|
||||
export const isGroupAddress = (a: string, ...args: unknown[]) => Address.isAddress(a) && Address.from(a).kind === GROUP
|
||||
|
||||
export const isCommunityAddress = (a: string, ...args: unknown[]) => Address.from(a).kind === COMMUNITY
|
||||
export const isCommunityAddress = (a: string, ...args: unknown[]) => Address.isAddress(a) && Address.from(a).kind === COMMUNITY
|
||||
|
||||
export const isContextAddress = (a: string, ...args: unknown[]) => [GROUP, COMMUNITY].includes(Address.from(a).kind)
|
||||
export const isContextAddress = (a: string, ...args: unknown[]) => Address.isAddress(a) && [GROUP, COMMUNITY].includes(Address.from(a).kind)
|
||||
|
||||
+38
-32
@@ -1,6 +1,6 @@
|
||||
import {EventTemplate} from 'nostr-tools'
|
||||
import type {OmitStatics} from '@welshman/lib'
|
||||
import {Fluent, nth, ensurePlural} from '@welshman/lib'
|
||||
import {Fluent, nth, nthEq, ensurePlural} from '@welshman/lib'
|
||||
import {isRelayUrl, normalizeRelayUrl} from './Relay'
|
||||
import {Address, isContextAddress} from './Address'
|
||||
import {GROUP, COMMUNITY} from './Kinds'
|
||||
@@ -76,37 +76,7 @@ export class Tags extends (Fluent<Tag> as OmitStatics<typeof Fluent<Tag>, 'from'
|
||||
topics = () => this.whereKey("t").values().map((t: string) => t.replace(/^#/, ""))
|
||||
|
||||
ancestors = (x?: boolean) => {
|
||||
const tags = this.filterByKey(["a", "e", "q"]).reject(t => t.isContext())
|
||||
const mentionTags = tags.whereKey("q")
|
||||
const roots: string[][] = []
|
||||
const replies: string[][] = []
|
||||
const mentions: string[][] = []
|
||||
|
||||
const dispatchTags = (thisTags: Tags) =>
|
||||
thisTags.forEach((t: Tag, i: number) => {
|
||||
if (t.nth(3) === 'root') {
|
||||
if (tags.filter(t => t.nth(3) === "reply").count() === 0) {
|
||||
replies.push(t.valueOf())
|
||||
} else {
|
||||
roots.push(t.valueOf())
|
||||
}
|
||||
} else if (t.nth(3) === 'reply') {
|
||||
replies.push(t.valueOf())
|
||||
} else if (t.nth(3) === 'mention') {
|
||||
mentions.push(t.valueOf())
|
||||
} else if (i === thisTags.count() - 1) {
|
||||
replies.push(t.valueOf())
|
||||
} else if (i === 0) {
|
||||
roots.push(t.valueOf())
|
||||
} else {
|
||||
mentions.push(t.valueOf())
|
||||
}
|
||||
})
|
||||
|
||||
// Add different types separately so positional logic works
|
||||
dispatchTags(tags.whereKey("e"))
|
||||
dispatchTags(tags.whereKey("a").filter(t => Boolean(t.nth(3))))
|
||||
mentionTags.forEach((t: Tag) => mentions.push(t.valueOf()))
|
||||
const {roots, replies, mentions} = getAncestorTags(this.unwrap())
|
||||
|
||||
return {
|
||||
roots: Tags.wrap(roots),
|
||||
@@ -240,3 +210,39 @@ export const getKindTags = (tags: string[][]) =>
|
||||
tags.filter(t => ["k"].includes(t[0]) && t[1].match(/^\d+$/))
|
||||
|
||||
export const getKindTagValues = (tags: string[][]) => getKindTags(tags).map(t => parseInt(t[1]))
|
||||
|
||||
export const getAncestorTags = (tags: string[][]) => {
|
||||
const validTags = tags.filter(t => ["a", "e", "q"].includes(t[0]) && !isContextAddress(t[1]))
|
||||
const mentionTags = validTags.filter(nthEq(0, "q"))
|
||||
const roots: string[][] = []
|
||||
const replies: string[][] = []
|
||||
const mentions: string[][] = []
|
||||
|
||||
const dispatchTags = (thisTags: string[][]) =>
|
||||
thisTags.forEach((t: string[], i: number) => {
|
||||
if (t[3] === 'root') {
|
||||
if (validTags.filter(nthEq(3, "reply")).length === 0) {
|
||||
replies.push(t)
|
||||
} else {
|
||||
roots.push(t)
|
||||
}
|
||||
} else if (t[3] === 'reply') {
|
||||
replies.push(t)
|
||||
} else if (t[3] === 'mention') {
|
||||
mentions.push(t)
|
||||
} else if (i === thisTags.length - 1) {
|
||||
replies.push(t)
|
||||
} else if (i === 0) {
|
||||
roots.push(t)
|
||||
} else {
|
||||
mentions.push(t)
|
||||
}
|
||||
})
|
||||
|
||||
// Add different types separately so positional logic works
|
||||
dispatchTags(validTags.filter(nthEq(0, "e")))
|
||||
dispatchTags(validTags.filter(nthEq(0, "a")).filter(t => Boolean(t[3])))
|
||||
mentionTags.forEach((t: string[]) => mentions.push(t))
|
||||
|
||||
return {roots, replies, mentions}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user