This commit is contained in:
@@ -1,223 +0,0 @@
|
||||
import {MUTES} from "@welshman/util"
|
||||
import {now} from "@welshman/lib"
|
||||
import {describe, it, expect, vi, beforeEach} from "vitest"
|
||||
import {Encryptable, asDecryptedEvent} from "../src/Encryptable"
|
||||
import type {OwnedEvent, TrustedEvent} from "../src/Events"
|
||||
|
||||
describe("Encryptable", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
// Mock encryption function
|
||||
const mockEncrypt = vi.fn(async (text: string) => `encrypted:${text}`)
|
||||
|
||||
// Realistic Nostr values
|
||||
const pub = "ee".repeat(32)
|
||||
const currentTime = now()
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create an instance with minimal event template", () => {
|
||||
const event: Partial<OwnedEvent> = {
|
||||
kind: MUTES,
|
||||
pubkey: pub,
|
||||
created_at: currentTime,
|
||||
}
|
||||
const encryptable = new Encryptable(event, {})
|
||||
|
||||
expect(encryptable.event).toBe(event)
|
||||
expect(encryptable.updates).toEqual({})
|
||||
})
|
||||
|
||||
it("should create an instance with full event template", () => {
|
||||
const event: OwnedEvent = {
|
||||
kind: MUTES,
|
||||
pubkey: pub,
|
||||
created_at: currentTime,
|
||||
content: "original encrypted content",
|
||||
tags: [["p", pub]],
|
||||
}
|
||||
const updates = {
|
||||
content: JSON.stringify({list: ["item1", "item2"]}),
|
||||
tags: [["p", pub, "wss://relay.example.com"]],
|
||||
}
|
||||
const encryptable = new Encryptable(event, updates)
|
||||
|
||||
expect(encryptable.event).toBe(event)
|
||||
expect(encryptable.updates).toBe(updates)
|
||||
})
|
||||
})
|
||||
|
||||
describe("reconcile", () => {
|
||||
it("should encrypt content updates", async () => {
|
||||
const event: Partial<OwnedEvent> = {
|
||||
kind: MUTES,
|
||||
pubkey: pub,
|
||||
created_at: currentTime,
|
||||
}
|
||||
const updates = {
|
||||
content: JSON.stringify({muted: [pub]}),
|
||||
}
|
||||
const encryptable = new Encryptable(event, updates)
|
||||
|
||||
const result = await encryptable.reconcile(mockEncrypt)
|
||||
|
||||
expect(result.content).toBe(`encrypted:${updates.content}`)
|
||||
expect(mockEncrypt).toHaveBeenCalledWith(updates.content)
|
||||
})
|
||||
|
||||
it("should encrypt tag updates", async () => {
|
||||
const event: Partial<OwnedEvent> = {
|
||||
kind: MUTES,
|
||||
pubkey: pub,
|
||||
created_at: currentTime,
|
||||
}
|
||||
const updates = {
|
||||
tags: [["p", pub, "wss://relay.example.com"]],
|
||||
}
|
||||
const encryptable = new Encryptable(event, updates)
|
||||
|
||||
const result = await encryptable.reconcile(mockEncrypt)
|
||||
|
||||
expect(result.tags[0][1]).toBe(`encrypted:${pub}`)
|
||||
expect(mockEncrypt).toHaveBeenCalledWith(pub)
|
||||
})
|
||||
|
||||
it("should handle both content and tag updates", async () => {
|
||||
const event: Partial<OwnedEvent> = {
|
||||
kind: MUTES,
|
||||
pubkey: pub,
|
||||
created_at: currentTime,
|
||||
}
|
||||
const updates = {
|
||||
content: JSON.stringify({muted: [pub]}),
|
||||
tags: [["p", pub, "wss://relay.example.com"]],
|
||||
}
|
||||
const encryptable = new Encryptable(event, updates)
|
||||
|
||||
const result = await encryptable.reconcile(mockEncrypt)
|
||||
|
||||
expect(result.content).toBe(`encrypted:${updates.content}`)
|
||||
expect(result.tags[0][1]).toBe(`encrypted:${pub}`)
|
||||
expect(mockEncrypt).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("should preserve original content when no updates", async () => {
|
||||
const event: OwnedEvent = {
|
||||
kind: MUTES,
|
||||
pubkey: pub,
|
||||
created_at: currentTime,
|
||||
content: JSON.stringify({originalList: [pub]}),
|
||||
tags: [],
|
||||
}
|
||||
const encryptable = new Encryptable(event, {})
|
||||
|
||||
const result = await encryptable.reconcile(mockEncrypt)
|
||||
|
||||
expect(result.content).toBe(event.content)
|
||||
expect(mockEncrypt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should preserve original tags when no updates", async () => {
|
||||
const event: OwnedEvent = {
|
||||
kind: MUTES,
|
||||
pubkey: pub,
|
||||
created_at: currentTime,
|
||||
content: "",
|
||||
tags: [["p", pub, "wss://relay.example.com"]],
|
||||
}
|
||||
const encryptable = new Encryptable(event, {})
|
||||
|
||||
const result = await encryptable.reconcile(mockEncrypt)
|
||||
|
||||
expect(result.tags).toEqual(event.tags)
|
||||
expect(mockEncrypt).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("asDecryptedEvent", () => {
|
||||
it("should create a decrypted event with plaintext", () => {
|
||||
const event: TrustedEvent = {
|
||||
id: "ff".repeat(32),
|
||||
sig: "00".repeat(64),
|
||||
kind: MUTES,
|
||||
pubkey: pub,
|
||||
created_at: currentTime,
|
||||
content: "encrypted content",
|
||||
tags: [],
|
||||
}
|
||||
const plaintext = {
|
||||
content: JSON.stringify({muted: [pub]}),
|
||||
tags: [["p", pub, "wss://relay.example.com"]],
|
||||
}
|
||||
|
||||
const result = asDecryptedEvent(event, plaintext)
|
||||
|
||||
expect(result).toEqual({
|
||||
...event,
|
||||
plaintext,
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle empty plaintext", () => {
|
||||
const event: TrustedEvent = {
|
||||
id: "ff".repeat(32),
|
||||
sig: "00".repeat(64),
|
||||
kind: MUTES,
|
||||
pubkey: pub,
|
||||
created_at: currentTime,
|
||||
content: "encrypted content",
|
||||
tags: [],
|
||||
}
|
||||
|
||||
const result = asDecryptedEvent(event)
|
||||
|
||||
expect(result).toEqual({
|
||||
...event,
|
||||
plaintext: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should handle encryption failures", async () => {
|
||||
const failingEncrypt = async () => {
|
||||
throw new Error("Encryption failed")
|
||||
}
|
||||
const event: Partial<OwnedEvent> = {
|
||||
kind: MUTES,
|
||||
pubkey: pub,
|
||||
created_at: currentTime,
|
||||
}
|
||||
const updates = {
|
||||
content: JSON.stringify({muted: [pub]}),
|
||||
}
|
||||
const encryptable = new Encryptable(event, updates)
|
||||
|
||||
await expect(encryptable.reconcile(failingEncrypt)).rejects.toThrow("Encryption failed")
|
||||
})
|
||||
|
||||
it("should handle partial encryption failures", async () => {
|
||||
let callCount = 0
|
||||
const partialFailingEncrypt = async () => {
|
||||
callCount++
|
||||
if (callCount > 1) throw new Error("Encryption failed")
|
||||
return "encrypted:success"
|
||||
}
|
||||
|
||||
const event: Partial<OwnedEvent> = {
|
||||
kind: MUTES,
|
||||
pubkey: pub,
|
||||
created_at: currentTime,
|
||||
}
|
||||
const updates = {
|
||||
content: JSON.stringify({muted: [pub]}),
|
||||
tags: [["p", pub]],
|
||||
}
|
||||
const encryptable = new Encryptable(event, updates)
|
||||
|
||||
await expect(encryptable.reconcile(partialFailingEncrypt)).rejects.toThrow(
|
||||
"Encryption failed",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,197 +0,0 @@
|
||||
import {now} from "@welshman/lib"
|
||||
import {HANDLER_INFORMATION} from "@welshman/util"
|
||||
import {describe, it, vi, expect, beforeEach} from "vitest"
|
||||
import {readHandlers, getHandlerKey, displayHandler, getHandlerAddress} from "../src/Handler"
|
||||
import type {TrustedEvent} from "../src/Events"
|
||||
|
||||
describe("Handler", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const pubkey = "ee".repeat(32)
|
||||
const id = "ff".repeat(32)
|
||||
const currentTime = now()
|
||||
|
||||
const createHandlerEvent = (overrides = {}): TrustedEvent => ({
|
||||
id: id,
|
||||
pubkey: pubkey,
|
||||
created_at: currentTime,
|
||||
kind: HANDLER_INFORMATION,
|
||||
tags: [
|
||||
["d", "test-handler"],
|
||||
["k", "30023"],
|
||||
["k", "30024"],
|
||||
],
|
||||
content: JSON.stringify({
|
||||
name: "Test Handler",
|
||||
image: "https://example.com/image.jpg",
|
||||
about: "Test handler description",
|
||||
website: "https://example.com",
|
||||
lud16: "user@domain.com",
|
||||
nip05: "user@domain.com",
|
||||
}),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe("readHandlers", () => {
|
||||
it("should parse valid handler event with full metadata", () => {
|
||||
const event = createHandlerEvent()
|
||||
const handlers = readHandlers(event)
|
||||
|
||||
expect(handlers).toHaveLength(2) // Two k tags
|
||||
expect(handlers[0]).toMatchObject({
|
||||
kind: 30023,
|
||||
identifier: "test-handler",
|
||||
name: "Test Handler",
|
||||
image: "https://example.com/image.jpg",
|
||||
about: "Test handler description",
|
||||
website: "https://example.com",
|
||||
lud16: "user@domain.com",
|
||||
nip05: "user@domain.com",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle display_name and picture alternatives", () => {
|
||||
const event = createHandlerEvent({
|
||||
content: JSON.stringify({
|
||||
display_name: "Test Handler",
|
||||
picture: "https://example.com/image.jpg",
|
||||
about: "Test description",
|
||||
}),
|
||||
})
|
||||
const handlers = readHandlers(event)
|
||||
|
||||
expect(handlers[0].name).toBe("Test Handler")
|
||||
expect(handlers[0].image).toBe("https://example.com/image.jpg")
|
||||
})
|
||||
|
||||
it("should return empty array if name is missing", () => {
|
||||
const event = createHandlerEvent({
|
||||
content: JSON.stringify({
|
||||
image: "https://example.com/image.jpg",
|
||||
about: "Test description",
|
||||
}),
|
||||
})
|
||||
const handlers = readHandlers(event)
|
||||
|
||||
expect(handlers).toEqual([])
|
||||
})
|
||||
|
||||
it("should return empty array if image is missing", () => {
|
||||
const event = createHandlerEvent({
|
||||
content: JSON.stringify({
|
||||
name: "Test Handler",
|
||||
about: "Test description",
|
||||
}),
|
||||
})
|
||||
const handlers = readHandlers(event)
|
||||
|
||||
expect(handlers).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle invalid JSON content", () => {
|
||||
const event = createHandlerEvent({
|
||||
content: "invalid json",
|
||||
})
|
||||
const handlers = readHandlers(event)
|
||||
|
||||
expect(handlers).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle empty content", () => {
|
||||
const event = createHandlerEvent({
|
||||
content: "",
|
||||
})
|
||||
const handlers = readHandlers(event)
|
||||
|
||||
expect(handlers).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle missing optional fields", () => {
|
||||
const event = createHandlerEvent({
|
||||
content: JSON.stringify({
|
||||
name: "Test Handler",
|
||||
image: "https://example.com/image.jpg",
|
||||
}),
|
||||
})
|
||||
const handlers = readHandlers(event)
|
||||
|
||||
expect(handlers[0]).toMatchObject({
|
||||
name: "Test Handler",
|
||||
image: "https://example.com/image.jpg",
|
||||
about: "",
|
||||
website: "",
|
||||
lud16: "",
|
||||
nip05: "",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getHandlerKey", () => {
|
||||
it("should generate correct handler key", () => {
|
||||
const event = createHandlerEvent()
|
||||
const handler = readHandlers(event)[0]
|
||||
const key = getHandlerKey(handler)
|
||||
|
||||
expect(key).toBe(`30023:31990:${pubkey}:test-handler`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("displayHandler", () => {
|
||||
it("should return handler name when available", () => {
|
||||
const event = createHandlerEvent()
|
||||
const handler = readHandlers(event)[0]
|
||||
|
||||
expect(displayHandler(handler)).toBe("Test Handler")
|
||||
})
|
||||
|
||||
it("should return fallback when handler is undefined", () => {
|
||||
expect(displayHandler(undefined, "Fallback")).toBe("Fallback")
|
||||
})
|
||||
|
||||
it("should return empty string when no fallback provided", () => {
|
||||
expect(displayHandler(undefined)).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getHandlerAddress", () => {
|
||||
it("should return web-tagged address if available", () => {
|
||||
const event = createHandlerEvent({
|
||||
tags: [
|
||||
["a", "30023:pubkey1:test", "relay1", "web"],
|
||||
["a", "30024:pubkey2:test", "relay2"],
|
||||
],
|
||||
})
|
||||
|
||||
expect(getHandlerAddress(event)).toBe("30023:pubkey1:test")
|
||||
})
|
||||
|
||||
it("should return first address if no web tag", () => {
|
||||
const event = createHandlerEvent({
|
||||
tags: [
|
||||
["a", "30023:pubkey1:test", "relay1"],
|
||||
["a", "30024:pubkey2:test", "relay2"],
|
||||
],
|
||||
})
|
||||
|
||||
expect(getHandlerAddress(event)).toBe("30023:pubkey1:test")
|
||||
})
|
||||
|
||||
it("should return undefined if no address tags", () => {
|
||||
const event = createHandlerEvent({
|
||||
tags: [["d", "test-handler"]],
|
||||
})
|
||||
|
||||
expect(getHandlerAddress(event)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle empty tags array", () => {
|
||||
const event = createHandlerEvent({
|
||||
tags: [],
|
||||
})
|
||||
|
||||
expect(getHandlerAddress(event)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,324 +0,0 @@
|
||||
import {now} from "@welshman/lib"
|
||||
import {MUTES} from "@welshman/util"
|
||||
import {describe, it, vi, expect, beforeEach} from "vitest"
|
||||
import {
|
||||
makeList,
|
||||
readList,
|
||||
getListTags,
|
||||
removeFromList,
|
||||
removeFromListByPredicate,
|
||||
addToListPublicly,
|
||||
addToListPrivately,
|
||||
} from "../src/List"
|
||||
import type {DecryptedEvent} from "../src/Encryptable"
|
||||
import type {List} from "../src/List"
|
||||
|
||||
describe("List", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const pubkey = "ee".repeat(32)
|
||||
const validEventId = "ff".repeat(32)
|
||||
const address = `30023:${pubkey}:test`
|
||||
const currentTime = now()
|
||||
|
||||
const createDecryptedEvent = (overrides = {}): DecryptedEvent => ({
|
||||
id: validEventId,
|
||||
pubkey: pubkey,
|
||||
created_at: currentTime,
|
||||
kind: MUTES,
|
||||
tags: [],
|
||||
content: "",
|
||||
plaintext: {},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe("makeList", () => {
|
||||
it("should create a list with defaults", () => {
|
||||
const list = makeList({kind: MUTES})
|
||||
expect(list).toEqual({
|
||||
kind: MUTES,
|
||||
publicTags: [],
|
||||
privateTags: [],
|
||||
})
|
||||
})
|
||||
|
||||
it("should preserve existing tags", () => {
|
||||
const list = makeList({
|
||||
kind: MUTES,
|
||||
publicTags: [["p", pubkey]],
|
||||
privateTags: [["e", validEventId]],
|
||||
})
|
||||
expect(list.publicTags).toHaveLength(1)
|
||||
expect(list.privateTags).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("readList", () => {
|
||||
it("should parse valid public tags", () => {
|
||||
const event = createDecryptedEvent({
|
||||
tags: [
|
||||
["p", pubkey],
|
||||
["e", validEventId],
|
||||
["a", address],
|
||||
["t", "test"],
|
||||
["r", "wss://relay.example.com"],
|
||||
["relay", "wss://relay.example.com"],
|
||||
["unknown", "value"],
|
||||
],
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.publicTags).toHaveLength(7)
|
||||
})
|
||||
|
||||
it("should not parse invalid public tags", () => {
|
||||
const event = createDecryptedEvent({
|
||||
tags: [
|
||||
["p", "invalid-pubkey"],
|
||||
["e", "invalid-event-id"],
|
||||
["a", "invalid-address"],
|
||||
["t", ""],
|
||||
["r", "invalid-url"],
|
||||
["relay", "invalid-url"],
|
||||
],
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.publicTags).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should parse valid private tags", () => {
|
||||
const event = createDecryptedEvent({
|
||||
plaintext: {
|
||||
content: JSON.stringify([
|
||||
["p", pubkey],
|
||||
["e", validEventId],
|
||||
]),
|
||||
},
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.privateTags).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should not parse invalid private tags", () => {
|
||||
const event = createDecryptedEvent({
|
||||
plaintext: {
|
||||
content: JSON.stringify([
|
||||
["p", "invalid-pubkey"],
|
||||
["e", "invalid-event-id"],
|
||||
]),
|
||||
},
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.privateTags).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should filter invalid tags", () => {
|
||||
const event = createDecryptedEvent({
|
||||
tags: [
|
||||
["p", "invalid-pubkey"],
|
||||
["e", "invalid-event-id"],
|
||||
["a", "invalid-address"],
|
||||
["t", ""],
|
||||
["r", "invalid-url"],
|
||||
["relay", "invalid-url"],
|
||||
],
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.publicTags).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle invalid JSON in private content", () => {
|
||||
const event = createDecryptedEvent({
|
||||
plaintext: {content: "invalid-json"},
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.privateTags).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle non-array private content", () => {
|
||||
const event = createDecryptedEvent({
|
||||
plaintext: {content: JSON.stringify({not: "an-array"})},
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.privateTags).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("getListTags", () => {
|
||||
it("should combine public and private tags", () => {
|
||||
const list: List = {
|
||||
kind: MUTES,
|
||||
publicTags: [["p", pubkey]],
|
||||
privateTags: [["e", validEventId]],
|
||||
}
|
||||
const tags = getListTags(list)
|
||||
expect(tags).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should handle undefined list", () => {
|
||||
expect(getListTags(undefined)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("removeFromList", () => {
|
||||
it("should remove matching public tags", () => {
|
||||
const list: List = {
|
||||
kind: MUTES,
|
||||
publicTags: [["p", pubkey]],
|
||||
privateTags: [],
|
||||
event: createDecryptedEvent(),
|
||||
}
|
||||
const result = removeFromList(list, pubkey)
|
||||
expect(result.event.tags).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should remove matching private tags", () => {
|
||||
const list: List = {
|
||||
kind: MUTES,
|
||||
publicTags: [],
|
||||
privateTags: [["p", pubkey]],
|
||||
event: createDecryptedEvent(),
|
||||
}
|
||||
const result = removeFromList(list, pubkey)
|
||||
const plaintext = JSON.parse(result.updates.content || "[]")
|
||||
expect(plaintext).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("removeFromListByPredicate", () => {
|
||||
it("should remove tags matching predicate", () => {
|
||||
const list: List = {
|
||||
kind: MUTES,
|
||||
publicTags: [
|
||||
["p", pubkey],
|
||||
["e", validEventId],
|
||||
],
|
||||
privateTags: [["p", pubkey]],
|
||||
event: createDecryptedEvent(),
|
||||
}
|
||||
const result = removeFromListByPredicate(list, tag => tag[0] === "p")
|
||||
expect(result.event.tags).toHaveLength(1)
|
||||
const plaintext = JSON.parse(result.updates.content || "[]")
|
||||
expect(plaintext).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("addToListPublicly", () => {
|
||||
it("should add tags to public list", () => {
|
||||
const list: List = {
|
||||
kind: MUTES,
|
||||
publicTags: [],
|
||||
privateTags: [],
|
||||
event: createDecryptedEvent(),
|
||||
}
|
||||
const result = addToListPublicly(list, ["p", pubkey])
|
||||
expect(result.event.tags).toHaveLength(1)
|
||||
expect(result.updates).toEqual({})
|
||||
})
|
||||
|
||||
it("should deduplicate tags", () => {
|
||||
const list: List = {
|
||||
kind: MUTES,
|
||||
publicTags: [["p", pubkey]],
|
||||
privateTags: [],
|
||||
event: createDecryptedEvent(),
|
||||
}
|
||||
const result = addToListPublicly(list, ["p", pubkey])
|
||||
expect(result.event.tags).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("addToListPrivately", () => {
|
||||
it("should add tags to private list", () => {
|
||||
const list: List = {
|
||||
kind: MUTES,
|
||||
publicTags: [],
|
||||
privateTags: [],
|
||||
event: createDecryptedEvent(),
|
||||
}
|
||||
const result = addToListPrivately(list, ["p", pubkey])
|
||||
const plaintext = JSON.parse(result.updates.content || "[]")
|
||||
expect(plaintext).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should deduplicate private tags", () => {
|
||||
const list: List = {
|
||||
kind: MUTES,
|
||||
publicTags: [],
|
||||
privateTags: [["p", pubkey]],
|
||||
event: createDecryptedEvent(),
|
||||
}
|
||||
const result = addToListPrivately(list, ["p", pubkey])
|
||||
const plaintext = JSON.parse(result.updates.content || "[]")
|
||||
expect(plaintext).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("tag validation", () => {
|
||||
it("should validate pubkey tags", () => {
|
||||
const event = createDecryptedEvent({
|
||||
tags: [
|
||||
["p", pubkey],
|
||||
["p", "invalid"],
|
||||
],
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.publicTags).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should validate event tags", () => {
|
||||
const event = createDecryptedEvent({
|
||||
tags: [
|
||||
["e", validEventId],
|
||||
["e", "invalid"],
|
||||
],
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.publicTags).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should validate address tags", () => {
|
||||
const event = createDecryptedEvent({
|
||||
tags: [
|
||||
["a", address],
|
||||
["a", "invalid"],
|
||||
],
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.publicTags).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should validate topic tags", () => {
|
||||
const event = createDecryptedEvent({
|
||||
tags: [
|
||||
["t", "valid-topic"],
|
||||
["t", ""],
|
||||
],
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.publicTags).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should validate relay tags", () => {
|
||||
const event = createDecryptedEvent({
|
||||
tags: [
|
||||
["r", "wss://relay.example.com"],
|
||||
["r", "invalid"],
|
||||
["relay", "wss://relay.example.com"],
|
||||
["relay", "invalid"],
|
||||
],
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.publicTags).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should accept unknown tag types", () => {
|
||||
const event = createDecryptedEvent({
|
||||
tags: [["unknown", "value"]],
|
||||
})
|
||||
const list = readList(event)
|
||||
expect(list.publicTags).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,232 +0,0 @@
|
||||
import {now} from "@welshman/lib"
|
||||
import {describe, it, vi, expect, beforeEach} from "vitest"
|
||||
import {
|
||||
makeProfile,
|
||||
readProfile,
|
||||
createProfile,
|
||||
editProfile,
|
||||
displayPubkey,
|
||||
displayProfile,
|
||||
profileHasName,
|
||||
isPublishedProfile,
|
||||
} from "../src/Profile"
|
||||
import {PROFILE} from "../src/Kinds"
|
||||
import type {TrustedEvent} from "../src/Events"
|
||||
import type {Profile, PublishedProfile} from "../src/Profile"
|
||||
|
||||
describe("Profile", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Realistic Nostr data
|
||||
const pubkey = "ee".repeat(32)
|
||||
const id = "ff".repeat(32)
|
||||
const sig = "00".repeat(64)
|
||||
const currentTime = now()
|
||||
|
||||
const createEvent = (overrides = {}): TrustedEvent => ({
|
||||
id: id,
|
||||
pubkey: pubkey,
|
||||
created_at: currentTime,
|
||||
kind: PROFILE,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: sig,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe("makeProfile", () => {
|
||||
it("should create empty profile", () => {
|
||||
const profile = makeProfile()
|
||||
expect(profile).toEqual({})
|
||||
})
|
||||
|
||||
it("should handle lud06 lightning address", () => {
|
||||
const profile = makeProfile({
|
||||
lud06:
|
||||
"lnurl1dp68gurn8ghj7um9wfmxjcm99e3k7mf0v9cxj0m385ekvcenxc6r2c35xvukxefcv5mkvv34x5ekzd3ev56nyd3hxqurzepexejxxepnxscrvwfnv9nxzcn9xq6xyefhvgcxxcmyxymnserxfq5fns",
|
||||
})
|
||||
expect(profile.lnurl).toBeDefined()
|
||||
})
|
||||
|
||||
it("should handle lud16 lightning address", () => {
|
||||
const profile = makeProfile({
|
||||
lud16: "user@domain.com",
|
||||
})
|
||||
expect(profile.lnurl).toBeDefined()
|
||||
})
|
||||
|
||||
it("should preserve other profile fields", () => {
|
||||
const profile = makeProfile({
|
||||
name: "Test User",
|
||||
about: "Test Bio",
|
||||
picture: "https://example.com/pic.jpg",
|
||||
})
|
||||
expect(profile.name).toBe("Test User")
|
||||
expect(profile.about).toBe("Test Bio")
|
||||
expect(profile.picture).toBe("https://example.com/pic.jpg")
|
||||
})
|
||||
})
|
||||
|
||||
describe("readProfile", () => {
|
||||
it("should parse valid profile content", () => {
|
||||
const event = createEvent({
|
||||
content: JSON.stringify({
|
||||
name: "Test User",
|
||||
about: "Test Bio",
|
||||
picture: "https://example.com/pic.jpg",
|
||||
lud16: "user@domain.com",
|
||||
}),
|
||||
})
|
||||
const profile = readProfile(event)
|
||||
|
||||
expect(profile.name).toBe("Test User")
|
||||
expect(profile.about).toBe("Test Bio")
|
||||
expect(profile.picture).toBe("https://example.com/pic.jpg")
|
||||
expect(profile.lnurl).toBeDefined()
|
||||
expect(profile.event).toBe(event)
|
||||
})
|
||||
|
||||
it("should handle invalid JSON content", () => {
|
||||
const event = createEvent({
|
||||
content: "invalid json",
|
||||
})
|
||||
const profile = readProfile(event)
|
||||
|
||||
expect(profile.event).toBe(event)
|
||||
expect(Object.keys(profile)).not.toContain("name")
|
||||
})
|
||||
|
||||
it("should handle empty content", () => {
|
||||
const event = createEvent({
|
||||
content: "",
|
||||
})
|
||||
const profile = readProfile(event)
|
||||
|
||||
expect(profile.event).toBe(event)
|
||||
expect(Object.keys(profile)).not.toContain("name")
|
||||
})
|
||||
})
|
||||
|
||||
describe("createProfile", () => {
|
||||
it("should create profile event template", () => {
|
||||
const profile: Profile = {
|
||||
name: "Test User",
|
||||
about: "Test Bio",
|
||||
picture: "https://example.com/pic.jpg",
|
||||
lud16: "user@domain.com",
|
||||
}
|
||||
const result = createProfile(profile)
|
||||
|
||||
expect(result.kind).toBe(PROFILE)
|
||||
expect(JSON.parse(result.content)).toMatchObject({
|
||||
name: "Test User",
|
||||
about: "Test Bio",
|
||||
picture: "https://example.com/pic.jpg",
|
||||
lud16: "user@domain.com",
|
||||
})
|
||||
})
|
||||
|
||||
it("should exclude event field from content", () => {
|
||||
const profile: Profile = {
|
||||
name: "Test User",
|
||||
event: createEvent(),
|
||||
}
|
||||
const result = createProfile(profile)
|
||||
const content = JSON.parse(result.content)
|
||||
|
||||
expect(content).not.toHaveProperty("event")
|
||||
expect(content).toHaveProperty("name")
|
||||
})
|
||||
})
|
||||
|
||||
describe("editProfile", () => {
|
||||
it("should create edit event template with existing tags", () => {
|
||||
const profile: PublishedProfile = {
|
||||
name: "Test User",
|
||||
event: createEvent({
|
||||
tags: [["p", pubkey]],
|
||||
}),
|
||||
}
|
||||
const result = editProfile(profile)
|
||||
|
||||
expect(result.kind).toBe(PROFILE)
|
||||
expect(result.tags).toEqual([["p", pubkey]])
|
||||
expect(JSON.parse(result.content)).toMatchObject({
|
||||
name: "Test User",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("displayPubkey", () => {
|
||||
it("should format pubkey correctly", () => {
|
||||
const display = displayPubkey(pubkey)
|
||||
|
||||
expect(display.length).toBe(14) // 8 + 1 + 5 characters
|
||||
})
|
||||
})
|
||||
|
||||
describe("displayProfile", () => {
|
||||
it("should display name if available", () => {
|
||||
const profile: Profile = {name: "Test User"}
|
||||
expect(displayProfile(profile)).toBe("Test User")
|
||||
})
|
||||
|
||||
it("should display display_name if name not available", () => {
|
||||
const profile: Profile = {display_name: "Test Display"}
|
||||
expect(displayProfile(profile)).toBe("Test Display")
|
||||
})
|
||||
|
||||
it("should display pubkey if no names available", () => {
|
||||
const profile: Profile = {event: createEvent()}
|
||||
expect(displayProfile(profile)).toMatch(/^npub1/)
|
||||
})
|
||||
|
||||
it("should display fallback if no profile", () => {
|
||||
expect(displayProfile(undefined, "Fallback")).toBe("Fallback")
|
||||
})
|
||||
|
||||
it("should truncate long names", () => {
|
||||
const longName = "a".repeat(100) + " " + "b".repeat(100)
|
||||
const profile: Profile = {name: longName}
|
||||
// ellipsize split at space and adds ellipsis to the end of the first part
|
||||
expect(displayProfile(profile).length).toBeLessThanOrEqual(103)
|
||||
})
|
||||
})
|
||||
|
||||
describe("profileHasName", () => {
|
||||
it("should return true if profile has name", () => {
|
||||
expect(profileHasName({name: "Test"})).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true if profile has display_name", () => {
|
||||
expect(profileHasName({display_name: "Test"})).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false if profile has no names", () => {
|
||||
expect(profileHasName({})).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false if profile is undefined", () => {
|
||||
expect(profileHasName(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isPublishedProfile", () => {
|
||||
it("should return true for published profile", () => {
|
||||
const profile: PublishedProfile = {
|
||||
name: "Test",
|
||||
event: createEvent(),
|
||||
}
|
||||
expect(isPublishedProfile(profile)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for unpublished profile", () => {
|
||||
const profile: Profile = {
|
||||
name: "Test",
|
||||
}
|
||||
expect(isPublishedProfile(profile)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,68 +0,0 @@
|
||||
import type {EventContent, TrustedEvent, EventTemplate} from "./Events.js"
|
||||
|
||||
export type Encrypt = (x: string) => Promise<string>
|
||||
|
||||
export type EncryptableUpdates = Partial<EventContent>
|
||||
|
||||
export type DecryptedEvent = TrustedEvent & {
|
||||
plaintext: EncryptableUpdates
|
||||
}
|
||||
|
||||
export const asDecryptedEvent = (event: TrustedEvent, plaintext: EncryptableUpdates = {}) =>
|
||||
({...event, plaintext}) as DecryptedEvent
|
||||
|
||||
/**
|
||||
* Represents an encryptable event with optional updates.
|
||||
*/
|
||||
export class Encryptable<T extends EventTemplate> {
|
||||
/**
|
||||
* Creates an instance of Encryptable.
|
||||
* @param event - An EventTemplate with optional tags and content.
|
||||
* @param updates - Plaintext updates to be applied to the event content.
|
||||
* @example
|
||||
* Here's an example which enables updating a private mute list:
|
||||
* ```
|
||||
* const event = {kind: 10000, content: "", tags: []} // An event, only kind is required
|
||||
* const encryptable = new Encryptable(event, {content: JSON.stringify([["e", "bad word"]])})
|
||||
* const eventTemplate = await encryptable.reconcile(myEncryptFunction)
|
||||
* ```
|
||||
*/
|
||||
constructor(
|
||||
readonly event: Partial<T>,
|
||||
readonly updates: EncryptableUpdates,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Encrypts plaintext updates and merges them into the event template.
|
||||
* @param encrypt - The encryption function to be used.
|
||||
* @returns A promise that resolves to the reconciled and encrypted event.
|
||||
*/
|
||||
async reconcile(encrypt: Encrypt) {
|
||||
const encryptContent = () => {
|
||||
if (!this.updates.content) return undefined
|
||||
|
||||
return encrypt(this.updates.content)
|
||||
}
|
||||
|
||||
const encryptTags = () => {
|
||||
if (!this.updates.tags) return undefined
|
||||
|
||||
return Promise.all(
|
||||
this.updates.tags.map(async tag => {
|
||||
tag[1] = await encrypt(tag[1])
|
||||
|
||||
return tag
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const [content, tags] = await Promise.all([encryptContent(), encryptTags()])
|
||||
|
||||
// Updates are optional. If not provided, fall back to the event's content and tags.
|
||||
return {
|
||||
...this.event,
|
||||
tags: tags || this.event.tags || [],
|
||||
content: content || this.event.content || "",
|
||||
} as T
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import {fromPairs, last, first, parseJson} from "@welshman/lib"
|
||||
import {getAddress} from "./Address.js"
|
||||
import {getAddressTags, getKindTagValues} from "./Tags.js"
|
||||
import type {TrustedEvent} from "./Events.js"
|
||||
|
||||
export type Handler = {
|
||||
kind: number
|
||||
name: string
|
||||
about: string
|
||||
image: string
|
||||
identifier: string
|
||||
event: TrustedEvent
|
||||
website?: string
|
||||
lud16?: string
|
||||
nip05?: string
|
||||
}
|
||||
|
||||
export const readHandlers = (event: TrustedEvent) => {
|
||||
const {d: identifier} = fromPairs(event.tags)
|
||||
const meta = parseJson(event.content)
|
||||
const normalizedMeta = {
|
||||
name: meta?.name || meta?.display_name || "",
|
||||
image: meta?.image || meta?.picture || "",
|
||||
about: meta?.about || "",
|
||||
website: meta?.website || "",
|
||||
lud16: meta?.lud16 || "",
|
||||
nip05: meta?.nip05 || "",
|
||||
}
|
||||
|
||||
// If our meta is missing important stuff, don't bother showing it
|
||||
if (!normalizedMeta.name || !normalizedMeta.image) {
|
||||
return []
|
||||
}
|
||||
|
||||
return getKindTagValues(event.tags).map(kind => ({
|
||||
...normalizedMeta,
|
||||
kind,
|
||||
identifier,
|
||||
event,
|
||||
})) as Handler[]
|
||||
}
|
||||
|
||||
export const getHandlerKey = (handler: Handler) => `${handler.kind}:${getAddress(handler.event)}`
|
||||
|
||||
export const displayHandler = (handler?: Handler, fallback = "") => handler?.name || fallback
|
||||
|
||||
export const getHandlerAddress = (event: TrustedEvent) => {
|
||||
const tags = getAddressTags(event.tags)
|
||||
const tag = tags.find(t => last(t) === "web") || first(tags)
|
||||
|
||||
return tag?.[1]
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import {parseJson, nth, uniqBy, nthEq} from "@welshman/lib"
|
||||
import {Address} from "./Address.js"
|
||||
import {uniqTags, getRelayTags} from "./Tags.js"
|
||||
import {isRelayUrl, RelayMode, normalizeRelayUrl} from "./Relay.js"
|
||||
import {Encryptable, DecryptedEvent} from "./Encryptable.js"
|
||||
import type {EncryptableUpdates} from "./Encryptable.js"
|
||||
|
||||
export type ListParams = {
|
||||
kind: number
|
||||
}
|
||||
|
||||
export type List = ListParams & {
|
||||
publicTags: string[][]
|
||||
privateTags: string[][]
|
||||
event?: DecryptedEvent
|
||||
}
|
||||
|
||||
export type PublishedList = Omit<List, "event"> & {
|
||||
event: DecryptedEvent
|
||||
}
|
||||
|
||||
export const makeList = (list: ListParams & Partial<List>): List => ({
|
||||
publicTags: [],
|
||||
privateTags: [],
|
||||
...list,
|
||||
})
|
||||
|
||||
const isValidTag = (tag: string[]) => {
|
||||
if (tag[0] === "p") return tag[1]?.length === 64
|
||||
if (tag[0] === "e") return tag[1]?.length === 64
|
||||
if (tag[0] === "a") return Address.isAddress(tag[1] || "")
|
||||
if (tag[0] === "t") return tag[1]?.length > 0
|
||||
if (tag[0] === "r") return isRelayUrl(tag[1])
|
||||
if (tag[0] === "relay") return isRelayUrl(tag[1])
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const readList = (event: DecryptedEvent): PublishedList => {
|
||||
const getTags = (tags: string[][]) => (Array.isArray(tags) ? tags.filter(isValidTag) : [])
|
||||
const privateTags = getTags(parseJson(event.plaintext?.content) || [])
|
||||
const publicTags = getTags(event.tags)
|
||||
|
||||
return {event, kind: event.kind, publicTags, privateTags}
|
||||
}
|
||||
|
||||
export const getListTags = (list: List | undefined) => [
|
||||
...(list?.publicTags || []),
|
||||
...(list?.privateTags || []),
|
||||
]
|
||||
|
||||
export const removeFromListByPredicate = (list: List, pred: (t: string[]) => boolean) => {
|
||||
const plaintext: EncryptableUpdates = {}
|
||||
const template = {
|
||||
kind: list.kind,
|
||||
content: list.event?.content || "",
|
||||
tags: list.publicTags.filter(t => !pred(t)),
|
||||
}
|
||||
|
||||
// Avoid redundant encrypt calls if possible
|
||||
if (list.privateTags.some(t => pred(t))) {
|
||||
plaintext.content = JSON.stringify(list.privateTags.filter(t => !pred(t)))
|
||||
}
|
||||
|
||||
return new Encryptable(template, plaintext)
|
||||
}
|
||||
|
||||
export const removeFromList = (list: List, value: string) =>
|
||||
removeFromListByPredicate(list, nthEq(1, value))
|
||||
|
||||
export const addToListPublicly = (list: List, ...tags: string[][]) => {
|
||||
const template = {
|
||||
kind: list.kind,
|
||||
content: list.event?.content || "",
|
||||
tags: uniqTags([...list.publicTags, ...tags]),
|
||||
}
|
||||
|
||||
return new Encryptable(template, {})
|
||||
}
|
||||
|
||||
export const addToListPrivately = (list: List, ...tags: string[][]) => {
|
||||
const template = {
|
||||
kind: list.kind,
|
||||
tags: list.publicTags,
|
||||
}
|
||||
|
||||
return new Encryptable(template, {
|
||||
content: JSON.stringify(uniqTags([...list.privateTags, ...tags])),
|
||||
})
|
||||
}
|
||||
|
||||
export const updateList = (
|
||||
list: List,
|
||||
{publicTags, privateTags}: {publicTags?: string[][]; privateTags?: string[][]},
|
||||
) => {
|
||||
const template = {
|
||||
kind: list.kind,
|
||||
content: list.event?.content || "",
|
||||
tags: publicTags || list.publicTags,
|
||||
}
|
||||
|
||||
const updates: EncryptableUpdates = {}
|
||||
|
||||
if (privateTags) {
|
||||
updates.content = JSON.stringify(privateTags)
|
||||
}
|
||||
|
||||
return new Encryptable(template, updates)
|
||||
}
|
||||
|
||||
export const getRelaysFromList = (list?: List, mode?: RelayMode): string[] => {
|
||||
let tags = getRelayTags(getListTags(list))
|
||||
|
||||
if (mode) {
|
||||
tags = tags.filter((t: string[]) => !t[2] || t[2] === mode)
|
||||
}
|
||||
|
||||
return uniqBy(normalizeRelayUrl, tags.map(nth(1)))
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import {npubEncode} from "nostr-tools/nip19"
|
||||
import {ellipsize, parseJson} from "@welshman/lib"
|
||||
import {TrustedEvent, EventTemplate} from "./Events.js"
|
||||
import {getLnUrl} from "./Zaps.js"
|
||||
import {PROFILE} from "./Kinds.js"
|
||||
|
||||
export type Profile = {
|
||||
name?: string
|
||||
nip05?: string
|
||||
lud06?: string
|
||||
lud16?: string
|
||||
lnurl?: string
|
||||
about?: string
|
||||
banner?: string
|
||||
picture?: string
|
||||
website?: string
|
||||
display_name?: string
|
||||
event?: TrustedEvent
|
||||
}
|
||||
|
||||
export type PublishedProfile = Omit<Profile, "event"> & {
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
export const isPublishedProfile = (profile: Profile): profile is PublishedProfile =>
|
||||
Boolean(profile.event)
|
||||
|
||||
export const makeProfile = (profile: Partial<Profile> = {}): Profile => {
|
||||
if (typeof profile.lud06 === "string") {
|
||||
const lnurl = getLnUrl(profile.lud06)
|
||||
|
||||
if (lnurl) {
|
||||
profile = {...profile, lnurl}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof profile.lud16 === "string") {
|
||||
const lnurl = getLnUrl(profile.lud16)
|
||||
|
||||
if (lnurl) {
|
||||
profile = {...profile, lnurl}
|
||||
}
|
||||
}
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
export const readProfile = (event: TrustedEvent): PublishedProfile => ({
|
||||
...makeProfile(parseJson(event.content) || {}),
|
||||
event,
|
||||
})
|
||||
|
||||
export const createProfile = ({event, ...profile}: Profile): EventTemplate => ({
|
||||
kind: PROFILE,
|
||||
content: JSON.stringify(profile),
|
||||
tags: [],
|
||||
})
|
||||
|
||||
export const editProfile = ({event, ...profile}: PublishedProfile): EventTemplate => ({
|
||||
kind: PROFILE,
|
||||
content: JSON.stringify(profile),
|
||||
tags: event.tags,
|
||||
})
|
||||
|
||||
export const displayPubkey = (pubkey: string) => {
|
||||
const d = npubEncode(pubkey)
|
||||
|
||||
return d.slice(0, 8) + "…" + d.slice(-5)
|
||||
}
|
||||
|
||||
export const displayProfile = (profile?: Profile, fallback = "") => {
|
||||
const {display_name, name, event} = profile || {}
|
||||
|
||||
if (name) return ellipsize(name, 60).trim()
|
||||
if (display_name) return ellipsize(display_name, 60).trim()
|
||||
if (event) return displayPubkey(event.pubkey).trim()
|
||||
|
||||
return fallback.trim()
|
||||
}
|
||||
|
||||
export const profileHasName = (profile?: Profile) => Boolean(profile?.name || profile?.display_name)
|
||||
@@ -1,147 +0,0 @@
|
||||
import {spec} from "@welshman/lib"
|
||||
import {
|
||||
ROOM_META,
|
||||
ROOM_DELETE,
|
||||
ROOM_CREATE,
|
||||
ROOM_EDIT_META,
|
||||
ROOM_JOIN,
|
||||
ROOM_LEAVE,
|
||||
ROOM_ADD_MEMBER,
|
||||
ROOM_REMOVE_MEMBER,
|
||||
} from "./Kinds.js"
|
||||
import {makeEvent, TrustedEvent, getIdentifier} from "./Events.js"
|
||||
import {getTag, getTagValue} from "./Tags.js"
|
||||
|
||||
export type RoomMeta = {
|
||||
h: string
|
||||
name?: string
|
||||
about?: string
|
||||
picture?: string
|
||||
pictureMeta?: string[]
|
||||
isClosed?: boolean
|
||||
isHidden?: boolean
|
||||
isPrivate?: boolean
|
||||
isRestricted?: boolean
|
||||
livekit?: boolean
|
||||
event?: TrustedEvent
|
||||
}
|
||||
|
||||
export type PublishedRoomMeta = Omit<RoomMeta, "event"> & {
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
const vowels = "a,e,i,o,u,ay,ey,oy,ou,ia,ea,ough,oo,ee,argh".split(",")
|
||||
|
||||
const consonants =
|
||||
"p,b,t,d,k,g,ch,sh,th,f,v,s,z,l,r,m,n,pl,bl,cl,gl,pr,br,tr,dr,kr,gr,fl,sl,fr,thr,str,sk,sp,st".split(
|
||||
",",
|
||||
)
|
||||
|
||||
export const generateH = () => {
|
||||
const n = (6 + Math.random() * 2) | 0
|
||||
const s = [consonants, vowels]
|
||||
|
||||
if (Math.random() < 0.5) {
|
||||
s.reverse()
|
||||
}
|
||||
|
||||
return (
|
||||
Array.from({length: n}, (_, i) =>
|
||||
s[i % 2].splice((Math.random() * s[i % 2].length) | 0, 1),
|
||||
).join("") +
|
||||
(1 + Math.floor(Math.random() * 9))
|
||||
)
|
||||
}
|
||||
|
||||
export const makeRoomMeta = (room: Partial<RoomMeta> = {}): RoomMeta => {
|
||||
return {
|
||||
h: room.h ?? generateH(),
|
||||
...room,
|
||||
}
|
||||
}
|
||||
|
||||
export const readRoomMeta = (event: TrustedEvent): PublishedRoomMeta => {
|
||||
if (event.kind !== ROOM_META) {
|
||||
throw new Error("Invalid group meta event")
|
||||
}
|
||||
|
||||
const h = getIdentifier(event)
|
||||
|
||||
if (!h) {
|
||||
throw new Error("Group meta event had no d tag")
|
||||
}
|
||||
|
||||
return {
|
||||
h,
|
||||
event,
|
||||
name: getTagValue("name", event.tags),
|
||||
about: getTagValue("about", event.tags),
|
||||
picture: getTagValue("picture", event.tags),
|
||||
pictureMeta: getTag("picture", event.tags)?.slice(2),
|
||||
isClosed: event.tags.some(spec(["closed"])),
|
||||
isHidden: event.tags.some(spec(["hidden"])),
|
||||
isPrivate: event.tags.some(spec(["private"])),
|
||||
isRestricted: event.tags.some(spec(["restricted"])),
|
||||
livekit: event.tags.some(spec(["livekit"])),
|
||||
}
|
||||
}
|
||||
|
||||
export const makeRoomCreateEvent = (room: RoomMeta) =>
|
||||
makeEvent(ROOM_CREATE, {tags: [["h", room.h]]})
|
||||
|
||||
export const makeRoomDeleteEvent = (room: RoomMeta) =>
|
||||
makeEvent(ROOM_DELETE, {tags: [["h", room.h]]})
|
||||
|
||||
export const makeRoomEditEvent = (room: RoomMeta) => {
|
||||
const tags = [["h", room.h]]
|
||||
|
||||
if (room.name) tags.push(["name", room.name])
|
||||
if (room.about) tags.push(["about", room.about])
|
||||
|
||||
if (room.picture) {
|
||||
const tag = ["picture", room.picture]
|
||||
|
||||
if (room.pictureMeta) {
|
||||
tag.push(...room.pictureMeta)
|
||||
}
|
||||
|
||||
tags.push(tag)
|
||||
}
|
||||
|
||||
if (room.isClosed) tags.push(["closed"])
|
||||
if (room.isHidden) tags.push(["hidden"])
|
||||
if (room.isPrivate) tags.push(["private"])
|
||||
if (room.isRestricted) tags.push(["restricted"])
|
||||
if (room.livekit) tags.push(["livekit"])
|
||||
|
||||
if (room.event) {
|
||||
for (const t of room.event.tags) {
|
||||
if (tags.some(spec(t.slice(0, 1)))) continue
|
||||
if (["closed", "hidden", "private", "restricted", "livekit"].includes(t[0])) continue
|
||||
|
||||
tags.push(t)
|
||||
}
|
||||
}
|
||||
|
||||
return makeEvent(ROOM_EDIT_META, {tags})
|
||||
}
|
||||
|
||||
export const makeRoomJoinEvent = (room: RoomMeta) => makeEvent(ROOM_JOIN, {tags: [["h", room.h]]})
|
||||
|
||||
export const makeRoomLeaveEvent = (room: RoomMeta) => makeEvent(ROOM_LEAVE, {tags: [["h", room.h]]})
|
||||
|
||||
export const makeRoomAddMemberEvent = (room: RoomMeta, pubkey: string) =>
|
||||
makeEvent(ROOM_ADD_MEMBER, {
|
||||
tags: [
|
||||
["h", room.h],
|
||||
["p", pubkey],
|
||||
],
|
||||
})
|
||||
|
||||
export const makeRoomRemoveMemberEvent = (room: RoomMeta, pubkey: string) =>
|
||||
makeEvent(ROOM_REMOVE_MEMBER, {
|
||||
tags: [
|
||||
["h", room.h],
|
||||
["p", pubkey],
|
||||
],
|
||||
})
|
||||
@@ -1,22 +1,17 @@
|
||||
export * from "./Address.js"
|
||||
export * from "./Blossom.js"
|
||||
export * from "./Encryptable.js"
|
||||
export * from "./Events.js"
|
||||
export * from "./Filters.js"
|
||||
export * from "./Handler.js"
|
||||
export * from "./Handles.js"
|
||||
export * from "./Keys.js"
|
||||
export * from "./Kinds.js"
|
||||
export * from "./Links.js"
|
||||
export * from "./List.js"
|
||||
export * from "./Nip42.js"
|
||||
export * from "./Nip86.js"
|
||||
export * from "./Nip98.js"
|
||||
export * from "./Pow.js"
|
||||
export * from "./Profile.js"
|
||||
export * from "./Pubkey.js"
|
||||
export * from "./Relay.js"
|
||||
export * from "./Room.js"
|
||||
export * from "./Tags.js"
|
||||
export * from "./Wallet.js"
|
||||
export * from "./Zaps.js"
|
||||
|
||||
Reference in New Issue
Block a user