PublishingService.ts raw

   1  import { Pubkey, Timestamp } from '@/domain/shared'
   2  import { FollowList } from '@/domain/social'
   3  import { MuteList } from '@/domain/social'
   4  import { RelayList } from '@/domain/relay'
   5  import { kinds } from 'nostr-tools'
   6  
   7  /**
   8   * Draft event structure (matches TDraftEvent)
   9   */
  10  export type DraftEvent = {
  11    kind: number
  12    content: string
  13    created_at: number
  14    tags: string[][]
  15  }
  16  
  17  /**
  18   * Options for publishing a note
  19   */
  20  export type PublishNoteOptions = {
  21    parentEventId?: string
  22    mentions?: Pubkey[]
  23    hashtags?: string[]
  24    isNsfw?: boolean
  25    addClientTag?: boolean
  26  }
  27  
  28  /**
  29   * PublishingService Domain Service
  30   *
  31   * Handles creation of draft events and publishing logic.
  32   * This service encapsulates the business rules for event creation.
  33   */
  34  export class PublishingService {
  35    /**
  36     * Create a draft note event
  37     */
  38    createNoteDraft(
  39      content: string,
  40      options: PublishNoteOptions = {}
  41    ): DraftEvent {
  42      const tags: string[][] = []
  43  
  44      // Add mention tags
  45      if (options.mentions) {
  46        for (const pubkey of options.mentions) {
  47          tags.push(['p', pubkey.hex])
  48        }
  49      }
  50  
  51      // Add hashtags
  52      if (options.hashtags) {
  53        for (const hashtag of options.hashtags) {
  54          tags.push(['t', hashtag.toLowerCase()])
  55        }
  56      }
  57  
  58      // Add NSFW warning
  59      if (options.isNsfw) {
  60        tags.push(['content-warning', 'NSFW'])
  61      }
  62  
  63      // Add client tag
  64      if (options.addClientTag) {
  65        tags.push(['client', 'smesh'])
  66      }
  67  
  68      return {
  69        kind: kinds.ShortTextNote,
  70        content,
  71        created_at: Timestamp.now().unix,
  72        tags
  73      }
  74    }
  75  
  76    /**
  77     * Create a draft reaction event
  78     */
  79    createReactionDraft(
  80      targetEventId: string,
  81      targetPubkey: string,
  82      targetKind: number,
  83      emoji: string = '+'
  84    ): DraftEvent {
  85      const tags: string[][] = [
  86        ['e', targetEventId],
  87        ['p', targetPubkey]
  88      ]
  89  
  90      if (targetKind !== kinds.ShortTextNote) {
  91        tags.push(['k', targetKind.toString()])
  92      }
  93  
  94      return {
  95        kind: kinds.Reaction,
  96        content: emoji,
  97        created_at: Timestamp.now().unix,
  98        tags
  99      }
 100    }
 101  
 102    /**
 103     * Create a draft repost event
 104     */
 105    createRepostDraft(
 106      targetEventId: string,
 107      targetPubkey: string,
 108      embeddedContent?: string
 109    ): DraftEvent {
 110      return {
 111        kind: kinds.Repost,
 112        content: embeddedContent || '',
 113        created_at: Timestamp.now().unix,
 114        tags: [
 115          ['e', targetEventId],
 116          ['p', targetPubkey]
 117        ]
 118      }
 119    }
 120  
 121    /**
 122     * Create a draft follow list event from a FollowList aggregate
 123     */
 124    createFollowListDraft(followList: FollowList): DraftEvent {
 125      return followList.toDraftEvent()
 126    }
 127  
 128    /**
 129     * Create a draft mute list event from a MuteList aggregate
 130     * Note: The caller must handle encryption of private mutes
 131     */
 132    createMuteListDraft(muteList: MuteList, encryptedPrivateMutes: string = ''): DraftEvent {
 133      return muteList.toDraftEvent(encryptedPrivateMutes)
 134    }
 135  
 136    /**
 137     * Create a draft relay list event from a RelayList aggregate
 138     */
 139    createRelayListDraft(relayList: RelayList): DraftEvent {
 140      return relayList.toDraftEvent()
 141    }
 142  
 143    /**
 144     * Extract mentions from note content
 145     * Finds npub1, nprofile1, and @mentions
 146     */
 147    extractMentionsFromContent(content: string): Pubkey[] {
 148      const mentions: Pubkey[] = []
 149      const seen = new Set<string>()
 150  
 151      // Match npub1 and nprofile1
 152      const bech32Regex = /n(?:pub1|profile1)[0-9a-z]{58,}/gi
 153      const matches = content.match(bech32Regex) || []
 154  
 155      for (const match of matches) {
 156        const pubkey = Pubkey.tryFromString(match)
 157        if (pubkey && !seen.has(pubkey.hex)) {
 158          seen.add(pubkey.hex)
 159          mentions.push(pubkey)
 160        }
 161      }
 162  
 163      return mentions
 164    }
 165  
 166    /**
 167     * Extract hashtags from note content
 168     */
 169    extractHashtagsFromContent(content: string): string[] {
 170      const hashtags: string[] = []
 171      const matches = content.match(/#[\p{L}\p{N}\p{M}]+/gu) || []
 172  
 173      for (const match of matches) {
 174        const hashtag = match.slice(1).toLowerCase()
 175        if (hashtag && !hashtags.includes(hashtag)) {
 176          hashtags.push(hashtag)
 177        }
 178      }
 179  
 180      return hashtags
 181    }
 182  }
 183  
 184  /**
 185   * Singleton instance of the publishing service
 186   */
 187  export const publishingService = new PublishingService()
 188