183 lines
6.3 KiB
TypeScript
183 lines
6.3 KiB
TypeScript
import { finalizeEvent, nip19, SimplePool, type Event, type EventTemplate } from 'nostr-tools'
|
|
import { hexToBytes } from 'nostr-tools/utils'
|
|
import type { Article, NostrProfile } from '@/types/nostr'
|
|
import type { PublishResult } from '../publishResult'
|
|
import { checkZapReceipt as checkZapReceiptHelper } from '../nostrZapVerification'
|
|
import { createArticleSubscription, parseArticleOrPresentationFromEvent } from './articles'
|
|
import { getCachedArticleById } from './cache'
|
|
import { getDecryptedArticleContent, getPrivateContent } from './decryption'
|
|
import { buildImmediatePublishStatuses, buildUnsignedEventTemplate, publishEventNonBlocking } from './publish'
|
|
|
|
class NostrService {
|
|
private pool: SimplePool | null = null
|
|
private privateKey: string | null = null
|
|
private publicKey: string | null = null
|
|
|
|
constructor() {
|
|
if (typeof window !== 'undefined') {
|
|
this.initializePool()
|
|
}
|
|
}
|
|
|
|
private initializePool(): void {
|
|
this.pool = new SimplePool()
|
|
}
|
|
|
|
setPrivateKey(privateKey: string): void {
|
|
this.privateKey = privateKey
|
|
try {
|
|
const decoded = nip19.decode(privateKey)
|
|
if (decoded.type === 'nsec' && typeof decoded.data === 'string') {
|
|
this.privateKey = decoded.data
|
|
}
|
|
} catch {
|
|
// Assume it's already a hex string
|
|
}
|
|
}
|
|
|
|
getPrivateKey(): string | null {
|
|
return this.privateKey
|
|
}
|
|
|
|
getPublicKey(): string | null {
|
|
return this.publicKey
|
|
}
|
|
|
|
setPublicKey(publicKey: string): void {
|
|
this.publicKey = publicKey
|
|
try {
|
|
const decoded = nip19.decode(publicKey)
|
|
if (decoded.type === 'npub' && typeof decoded.data === 'string') {
|
|
this.publicKey = decoded.data
|
|
}
|
|
} catch {
|
|
// Assume it's already a hex string
|
|
}
|
|
}
|
|
|
|
async publishEvent(eventTemplate: EventTemplate): Promise<Event | null>
|
|
async publishEvent(eventTemplate: EventTemplate, returnStatus: false): Promise<Event | null>
|
|
async publishEvent(eventTemplate: EventTemplate, returnStatus: true): Promise<PublishResult>
|
|
async publishEvent(eventTemplate: EventTemplate, returnStatus: boolean = false): Promise<Event | null | PublishResult> {
|
|
if (!this.privateKey || !this.pool) {
|
|
throw new Error('Private key not set or pool not initialized')
|
|
}
|
|
|
|
const unsignedEvent = buildUnsignedEventTemplate(eventTemplate)
|
|
const event = finalizeEvent(unsignedEvent, hexToBytes(this.privateKey))
|
|
|
|
const { relaySessionManager } = await import('../relaySessionManager')
|
|
const activeRelays = await relaySessionManager.getActiveRelays()
|
|
|
|
void publishEventNonBlocking({ pool: this.pool, event, activeRelays, relaySessionManager })
|
|
|
|
if (returnStatus) {
|
|
const relayStatuses = await buildImmediatePublishStatuses(activeRelays)
|
|
return { event, relayStatuses }
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
subscribeToArticles(callback: (article: Article) => void, limit: number = 100): () => void {
|
|
if (typeof window === 'undefined') {
|
|
throw new Error('Cannot subscribe on server side')
|
|
}
|
|
if (!this.pool) {
|
|
this.initializePool()
|
|
}
|
|
if (!this.pool) {
|
|
throw new Error('Pool not initialized')
|
|
}
|
|
|
|
const sub = createArticleSubscription({ pool: this.pool, limit })
|
|
sub.on('event', (event: Event): void => {
|
|
void (async (): Promise<void> => {
|
|
const parsed = await parseArticleOrPresentationFromEvent(event)
|
|
if (parsed) {
|
|
callback(parsed)
|
|
}
|
|
})()
|
|
})
|
|
|
|
return (): void => sub.unsub()
|
|
}
|
|
|
|
async getArticleById(eventId: string): Promise<Article | null> {
|
|
return getCachedArticleById(eventId)
|
|
}
|
|
|
|
getPrivateContent(eventId: string, authorPubkey: string): Promise<string | null> {
|
|
if (!this.privateKey || !this.pool || !this.publicKey) {
|
|
throw new Error('Private key not set or pool not initialized')
|
|
}
|
|
return getPrivateContent({ pool: this.pool, eventId, authorPubkey, privateKey: this.privateKey, publicKey: this.publicKey })
|
|
}
|
|
|
|
async getDecryptedArticleContent(eventId: string, authorPubkey: string): Promise<string | null> {
|
|
if (!this.privateKey || !this.pool || !this.publicKey) {
|
|
throw new Error('Private key not set or pool not initialized')
|
|
}
|
|
return getDecryptedArticleContent({ pool: this.pool, eventId, authorPubkey, privateKey: this.privateKey, publicKey: this.publicKey })
|
|
}
|
|
|
|
async getProfile(_pubkey: string): Promise<NostrProfile | null> {
|
|
return null
|
|
}
|
|
|
|
async updateProfile(updates: Partial<NostrProfile>): Promise<void> {
|
|
if (!this.privateKey || !this.publicKey) {
|
|
throw new Error('Private key and public key must be set to update profile')
|
|
}
|
|
const existingProfile = await this.getProfile(this.publicKey)
|
|
const currentProfile: NostrProfile = existingProfile ?? { pubkey: this.publicKey }
|
|
const updatedProfile: NostrProfile = { ...currentProfile, ...updates, pubkey: this.publicKey }
|
|
const profileEvent: EventTemplate = {
|
|
kind: 0,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [],
|
|
content: JSON.stringify({
|
|
name: updatedProfile.name,
|
|
about: updatedProfile.about,
|
|
picture: updatedProfile.picture,
|
|
nip05: updatedProfile.nip05,
|
|
lud16: updatedProfile.lud16,
|
|
lud06: updatedProfile.lud06,
|
|
}),
|
|
}
|
|
await this.publishEvent(profileEvent)
|
|
}
|
|
|
|
async createZapRequest(targetPubkey: string, targetEventId: string, amount: number, extraTags: string[][] = []): Promise<Event> {
|
|
if (!this.privateKey) {
|
|
throw new Error('Private key not set')
|
|
}
|
|
const { getPrimaryRelay } = await import('../config')
|
|
const relayUrl = await getPrimaryRelay()
|
|
const zapRequest: EventTemplate = {
|
|
kind: 9734,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [['p', targetPubkey], ['e', targetEventId], ['amount', amount.toString()], ['relays', relayUrl], ...extraTags],
|
|
content: '',
|
|
}
|
|
const event = await this.publishEvent(zapRequest)
|
|
if (!event) {
|
|
throw new Error('Failed to create zap request')
|
|
}
|
|
return event
|
|
}
|
|
|
|
checkZapReceipt(targetPubkey: string, targetEventId: string, amount: number, userPubkey?: string): Promise<boolean> {
|
|
if (!this.publicKey || !this.pool) {
|
|
return Promise.resolve(false)
|
|
}
|
|
return checkZapReceiptHelper({ pool: this.pool, targetPubkey, targetEventId, amount, userPubkey: userPubkey ?? this.publicKey })
|
|
}
|
|
|
|
getPool(): SimplePool | null {
|
|
return this.pool
|
|
}
|
|
}
|
|
|
|
export const nostrService = new NostrService()
|