Re-work address

This commit is contained in:
Jon Staab
2024-03-05 15:29:01 -08:00
parent 2debb7f614
commit 4e4039ea7a
4 changed files with 85 additions and 90 deletions
+56 -80
View File
@@ -1,85 +1,61 @@
import type {UnsignedEvent} from 'nostr-tools' import type {UnsignedEvent} from 'nostr-tools'
import {nip19} from 'nostr-tools' import {nip19} from 'nostr-tools'
import {GROUP_DEFINITION, COMMUNITY_DEFINITION} from './Kinds' import {GROUP_DEFINITION, COMMUNITY_DEFINITION} from './Kinds'
import {Tags} from './Tags'
export const isGroupAddress = (a: string) => a.startsWith(`${GROUP_DEFINITION}:`) export type Address = {
kind: number,
export const isCommunityAddress = (a: string) => a.startsWith(`${COMMUNITY_DEFINITION}:`) pubkey: string
identifier: string
export const isContextAddress = (a: string) => isCommunityAddress(a) || isGroupAddress(a) relays: string[]
export class Address {
readonly kind: number
constructor(
kind: string | number,
readonly pubkey: string,
readonly identifier: string,
readonly relays: string[],
) {
this.kind = parseInt(kind as string)
this.identifier = identifier || ""
}
static getKind = (a: string) => Address.fromRaw(a, []).kind
static getPubkey = (a: string) => Address.fromRaw(a, []).pubkey
static getIdentifier = (a: string) => Address.fromRaw(a, []).identifier
static fromEvent = (e: UnsignedEvent, relays: string[]) =>
new Address(e.kind, e.pubkey, Tags.fromEvent(e).get("d")?.value() || "", relays)
static fromRaw = (a: string, relays: string[]) => {
const [kind, pubkey, identifier] = a.split(":")
return new Address(kind, pubkey, identifier, relays)
}
static fromTag = (tag: string[]) => {
const [a, hint] = tag.slice(1)
const relays = hint ? [hint] : []
return this.fromRaw(a, relays)
}
static fromNaddr = (naddr: string) => {
let type
let data = {} as any
try {
({type, data} = nip19.decode(naddr) as {
type: "naddr"
data: any
})
} catch (e) {
// pass
}
if (type !== "naddr") {
throw new Error(`Invalid naddr ${naddr}`)
}
return new Address(data.kind, data.pubkey, data.identifier, data.relays)
}
asRaw = () => [this.kind, this.pubkey, this.identifier].join(":")
asTag = (mark?: string) => {
const tag = ["a", this.asRaw(), this.relays[0] || ""]
if (mark) {
tag.push(mark)
}
return tag
}
asNaddr = () => nip19.naddrEncode(this)
asFilter = () => ({
kinds: [this.kind],
authors: [this.pubkey],
"#d": [this.identifier],
})
} }
// Plain text format
export const decodeAddress = (a: string, relays: string[] = []): Address => {
const [kind, pubkey, identifier = ""] = a.split(":")
return {kind: parseInt(kind), pubkey, identifier, relays}
}
export const encodeAddress = (a: Address) => [a.kind, a.pubkey, a.identifier].join(":")
// Naddr encoding
export const addressFromNaddr = (naddr: string): Address => {
let type
let data = {} as any
try {
({type, data} = nip19.decode(naddr) as {
type: "naddr"
data: any
})
} catch (e) {
// pass
}
if (type !== "naddr") {
throw new Error(`Invalid naddr ${naddr}`)
}
return data
}
export const addressToNaddr = (a: Address): string => nip19.naddrEncode(a)
// Get from event, encode to filter
export const getIdentifier = (e: UnsignedEvent) => e.tags.find(t => t[0] === "d")?.[1] || ""
export const addressFromEvent = (e: UnsignedEvent, relays: string[] = []) =>
({kind: e.kind, pubkey: e.pubkey, identifier: getIdentifier(e), relays})
export const addressToFilter = (a: Address) =>
({kinds: [a.kind], authors: [a.pubkey], "#d": [a.identifier]})
// Utils
export const isGroupAddress = (a: Address) => a.kind === GROUP_DEFINITION
export const isCommunityAddress = (a: Address) => a.kind === COMMUNITY_DEFINITION
export const isContextAddress = (a: Address) => [GROUP_DEFINITION, COMMUNITY_DEFINITION].includes(a.kind)
+2 -2
View File
@@ -2,7 +2,7 @@ import type {Event, EventTemplate, UnsignedEvent} from 'nostr-tools'
import {verifyEvent, getEventHash} from 'nostr-tools' import {verifyEvent, getEventHash} from 'nostr-tools'
import {cached} from "./LRUCache" import {cached} from "./LRUCache"
import {now} from './Tools' import {now} from './Tools'
import {Address} from './Address' import {addressFromEvent, encodeAddress} from './Address'
import {isEphemeralKind, isReplaceableKind, isPlainReplaceableKind, isParameterizedReplaceableKind} from './Kinds' import {isEphemeralKind, isReplaceableKind, isPlainReplaceableKind, isParameterizedReplaceableKind} from './Kinds'
export type Rumor = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey' | 'id'> export type Rumor = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey' | 'id'>
@@ -43,7 +43,7 @@ export const hasValidSignature = cached<string, boolean, [Event]>({
}, },
}) })
export const getAddress = (e: UnsignedEvent) => Address.fromEvent(e, []).asRaw() export const getAddress = (e: UnsignedEvent) => encodeAddress(addressFromEvent(e))
export const getIdOrAddress = (e: Rumor) => isReplaceable(e) ? getAddress(e) : e.id export const getIdOrAddress = (e: Rumor) => isReplaceable(e) ? getAddress(e) : e.id
+7 -2
View File
@@ -3,7 +3,12 @@ import type {Rumor} from './Events'
import {getAddress, isReplaceable} from './Events' import {getAddress, isReplaceable} from './Events'
import {Tag, Tags} from './Tags' import {Tag, Tags} from './Tags'
import {first, uniq, shuffle} from './Tools' import {first, uniq, shuffle} from './Tools'
import {Address, isGroupAddress, isCommunityAddress} from './Address' import {GROUP_DEFINITION, COMMUNITY_DEFINITION} from './Kinds'
import {addressFromEvent, decodeAddress} from './Address'
const isGroupAddress = (a: string) => decodeAddress(a).kind === GROUP_DEFINITION
const isCommunityAddress = (a: string) => decodeAddress(a).kind === COMMUNITY_DEFINITION
export enum RelayMode { export enum RelayMode {
Inbox = "inbox", Inbox = "inbox",
@@ -205,7 +210,7 @@ export class Router {
} }
address = (event: UnsignedEvent) => address = (event: UnsignedEvent) =>
Address.fromEvent(event, this.Event(event).limit(3).getUrls()) addressFromEvent(event, this.Event(event).limit(3).getUrls())
} }
// Router Scenario // Router Scenario
+20 -6
View File
@@ -3,13 +3,17 @@ import {Fluent} from './Fluent'
import type {OmitStatics} from './Tools' import type {OmitStatics} from './Tools'
import {last} from './Tools' import {last} from './Tools'
import {isShareableRelayUrl} from './Relays' import {isShareableRelayUrl} from './Relays'
import {isCommunityAddress, isGroupAddress, isContextAddress} from './Address' import type {Address} from './Address'
import {encodeAddress, decodeAddress} from './Address'
import {GROUP_DEFINITION, COMMUNITY_DEFINITION} from './Kinds'
export class Tag extends (Fluent<string> as OmitStatics<typeof Fluent<string>, 'from'>) { export class Tag extends (Fluent<string> as OmitStatics<typeof Fluent<string>, 'from'>) {
static from(xs: Iterable<string>) { static from(xs: Iterable<string>) {
return new Tag(Array.from(xs)) return new Tag(Array.from(xs))
} }
static fromAddress = (a: Address) => new Tag(["a", encodeAddress(a), a.relays[0] || ""])
valueOf = () => this.xs valueOf = () => this.xs
key = () => this.xs[0] key = () => this.xs[0]
@@ -25,6 +29,16 @@ export class Tag extends (Fluent<string> as OmitStatics<typeof Fluent<string>, '
setValue = (v: string) => this.set(1, v) setValue = (v: string) => this.set(1, v)
setMark = (m: string) => this.xs.length > 2 ? this.set(this.xs.length - 2, m) : this.append(m) setMark = (m: string) => this.xs.length > 2 ? this.set(this.xs.length - 2, m) : this.append(m)
asAddress = () => decodeAddress(this.value())
isAddress = (kind?: number) => this.key() === "a" && this.value()?.startsWith(`${kind}:`)
isGroup = () => this.isAddress(GROUP_DEFINITION)
isCommunity = () => this.isAddress(COMMUNITY_DEFINITION)
isContext = () => this.isAddress(GROUP_DEFINITION) || this.isAddress(COMMUNITY_DEFINITION)
} }
export class Tags extends (Fluent<Tag> as OmitStatics<typeof Fluent<Tag>, 'from'>) { export class Tags extends (Fluent<Tag> as OmitStatics<typeof Fluent<Tag>, 'from'>) {
@@ -70,7 +84,7 @@ export class Tags extends (Fluent<Tag> as OmitStatics<typeof Fluent<Tag>, 'from'
topics = () => this.whereKey("t").values().map((t: string) => t.replace(/^#/, "")) topics = () => this.whereKey("t").values().map((t: string) => t.replace(/^#/, ""))
ancestors = () => { ancestors = () => {
const tags = this.filter(t => ["a", "e", "q"].includes(t.key()) && !isContextAddress(t.value())) const tags = this.filter(t => ["a", "e", "q"].includes(t.key()) && !t.isContext())
const roots: string[][] = [] const roots: string[][] = []
const replies: string[][] = [] const replies: string[][] = []
const mentions: string[][] = [] const mentions: string[][] = []
@@ -129,11 +143,11 @@ export class Tags extends (Fluent<Tag> as OmitStatics<typeof Fluent<Tag>, 'from'
return parents.get("e") || parents.get("a") return parents.get("e") || parents.get("a")
} }
groups = () => this.whereKey("a").filter(t => isGroupAddress(t.value())) groups = () => this.whereKey("a").filter(t => t.isGroup())
communities = () => this.whereKey("a").filter(t => isCommunityAddress(t.value())) communities = () => this.whereKey("a").filter(t => t.isCommunity())
context = () => this.whereKey("a").filter(t => isContextAddress(t.value())) context = () => this.whereKey("a").filter(t => t.isContext())
asObject = () => { asObject = () => {
const result: Record<string, string> = {} const result: Record<string, string> = {}
@@ -167,7 +181,7 @@ export class Tags extends (Fluent<Tag> as OmitStatics<typeof Fluent<Tag>, 'from'
addContext = (addresses: string[]) => this.concat(addresses.map(a => Tag.from(["a", a]))) addContext = (addresses: string[]) => this.concat(addresses.map(a => Tag.from(["a", a])))
removeContext = () => this.reject(t => t.key() === "a" && isContextAddress(t.value())) removeContext = () => this.reject(t => t.isContext())
setContext = (addresses: string[]) => this.removeContext().addContext(addresses) setContext = (addresses: string[]) => this.removeContext().addContext(addresses)