feat: add support for commenting and reacting on external content
This commit is contained in:
parent
5ba5c26fcd
commit
0bb62dd3fb
76 changed files with 1635 additions and 639 deletions
|
|
@ -98,11 +98,11 @@ class ClientService extends EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
let relays: string[]
|
||||
const relaySet = new Set<string>()
|
||||
if (specifiedRelayUrls?.length) {
|
||||
relays = specifiedRelayUrls
|
||||
specifiedRelayUrls.forEach((url) => relaySet.add(url))
|
||||
} else {
|
||||
const _additionalRelayUrls: string[] = additionalRelayUrls ?? []
|
||||
additionalRelayUrls?.forEach((url) => relaySet.add(url))
|
||||
if (!specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist].includes(event.kind)) {
|
||||
const mentions: string[] = []
|
||||
event.tags.forEach(([tagName, tagValue]) => {
|
||||
|
|
@ -118,10 +118,14 @@ class ClientService extends EventTarget {
|
|||
if (mentions.length > 0) {
|
||||
const relayLists = await this.fetchRelayLists(mentions)
|
||||
relayLists.forEach((relayList) => {
|
||||
_additionalRelayUrls.push(...relayList.read.slice(0, 4))
|
||||
relayList.read.slice(0, 4).forEach((url) => relaySet.add(url))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const relayList = await this.fetchRelayList(event.pubkey)
|
||||
relayList.write.forEach((url) => relaySet.add(url))
|
||||
|
||||
if (
|
||||
[
|
||||
kinds.RelayList,
|
||||
|
|
@ -131,20 +135,23 @@ class ClientService extends EventTarget {
|
|||
ExtendedKind.RELAY_REVIEW
|
||||
].includes(event.kind)
|
||||
) {
|
||||
_additionalRelayUrls.push(...BIG_RELAY_URLS)
|
||||
BIG_RELAY_URLS.forEach((url) => relaySet.add(url))
|
||||
}
|
||||
|
||||
const relayList = await this.fetchRelayList(event.pubkey)
|
||||
relays = (relayList?.write.slice(0, 10) ?? []).concat(
|
||||
Array.from(new Set(_additionalRelayUrls)) ?? []
|
||||
)
|
||||
if (event.kind === ExtendedKind.COMMENT) {
|
||||
const rootITag = event.tags.find(tagNameEquals('I'))
|
||||
if (rootITag) {
|
||||
// For external content comments, always publish to big relays
|
||||
BIG_RELAY_URLS.forEach((url) => relaySet.add(url))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!relays.length) {
|
||||
relays.push(...BIG_RELAY_URLS)
|
||||
if (!relaySet.size) {
|
||||
BIG_RELAY_URLS.forEach((url) => relaySet.add(url))
|
||||
}
|
||||
|
||||
return relays
|
||||
return Array.from(relaySet)
|
||||
}
|
||||
|
||||
async publishEvent(relayUrls: string[], event: NEvent) {
|
||||
|
|
|
|||
|
|
@ -1,273 +0,0 @@
|
|||
import { BIG_RELAY_URLS } from '@/constants'
|
||||
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
|
||||
import { getZapInfoFromEvent } from '@/lib/event-metadata'
|
||||
import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag'
|
||||
import client from '@/services/client.service'
|
||||
import { TEmoji } from '@/types'
|
||||
import dayjs from 'dayjs'
|
||||
import { Event, Filter, kinds } from 'nostr-tools'
|
||||
|
||||
export type TNoteStats = {
|
||||
likeIdSet: Set<string>
|
||||
likes: { id: string; pubkey: string; created_at: number; emoji: TEmoji | string }[]
|
||||
repostPubkeySet: Set<string>
|
||||
reposts: { id: string; pubkey: string; created_at: number }[]
|
||||
zapPrSet: Set<string>
|
||||
zaps: { pr: string; pubkey: string; amount: number; created_at: number; comment?: string }[]
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
class NoteStatsService {
|
||||
static instance: NoteStatsService
|
||||
private noteStatsMap: Map<string, Partial<TNoteStats>> = new Map()
|
||||
private noteStatsSubscribers = new Map<string, Set<() => void>>()
|
||||
|
||||
constructor() {
|
||||
if (!NoteStatsService.instance) {
|
||||
NoteStatsService.instance = this
|
||||
}
|
||||
return NoteStatsService.instance
|
||||
}
|
||||
|
||||
async fetchNoteStats(event: Event, pubkey?: string | null) {
|
||||
const oldStats = this.noteStatsMap.get(event.id)
|
||||
let since: number | undefined
|
||||
if (oldStats?.updatedAt) {
|
||||
since = oldStats.updatedAt
|
||||
}
|
||||
const [relayList, authorProfile] = await Promise.all([
|
||||
client.fetchRelayList(event.pubkey),
|
||||
client.fetchProfile(event.pubkey)
|
||||
])
|
||||
|
||||
const replaceableCoordinate = isReplaceableEvent(event.kind)
|
||||
? getReplaceableCoordinateFromEvent(event)
|
||||
: undefined
|
||||
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
'#e': [event.id],
|
||||
kinds: [kinds.Reaction],
|
||||
limit: 500
|
||||
},
|
||||
{
|
||||
'#e': [event.id],
|
||||
kinds: [kinds.Repost],
|
||||
limit: 100
|
||||
}
|
||||
]
|
||||
|
||||
if (replaceableCoordinate) {
|
||||
filters.push(
|
||||
{
|
||||
'#a': [replaceableCoordinate],
|
||||
kinds: [kinds.Reaction],
|
||||
limit: 500
|
||||
},
|
||||
{
|
||||
'#a': [replaceableCoordinate],
|
||||
kinds: [kinds.Repost],
|
||||
limit: 100
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (authorProfile?.lightningAddress) {
|
||||
filters.push({
|
||||
'#e': [event.id],
|
||||
kinds: [kinds.Zap],
|
||||
limit: 500
|
||||
})
|
||||
|
||||
if (replaceableCoordinate) {
|
||||
filters.push({
|
||||
'#a': [replaceableCoordinate],
|
||||
kinds: [kinds.Zap],
|
||||
limit: 500
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (pubkey) {
|
||||
filters.push({
|
||||
'#e': [event.id],
|
||||
authors: [pubkey],
|
||||
kinds: [kinds.Reaction, kinds.Repost]
|
||||
})
|
||||
|
||||
if (replaceableCoordinate) {
|
||||
filters.push({
|
||||
'#a': [replaceableCoordinate],
|
||||
authors: [pubkey],
|
||||
kinds: [kinds.Reaction, kinds.Repost]
|
||||
})
|
||||
}
|
||||
|
||||
if (authorProfile?.lightningAddress) {
|
||||
filters.push({
|
||||
'#e': [event.id],
|
||||
'#P': [pubkey],
|
||||
kinds: [kinds.Zap]
|
||||
})
|
||||
|
||||
if (replaceableCoordinate) {
|
||||
filters.push({
|
||||
'#a': [replaceableCoordinate],
|
||||
'#P': [pubkey],
|
||||
kinds: [kinds.Zap]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (since) {
|
||||
filters.forEach((filter) => {
|
||||
filter.since = since
|
||||
})
|
||||
}
|
||||
const events: Event[] = []
|
||||
await client.fetchEvents(relayList.read.concat(BIG_RELAY_URLS).slice(0, 5), filters, {
|
||||
onevent: (evt) => {
|
||||
this.updateNoteStatsByEvents([evt])
|
||||
events.push(evt)
|
||||
}
|
||||
})
|
||||
this.noteStatsMap.set(event.id, {
|
||||
...(this.noteStatsMap.get(event.id) ?? {}),
|
||||
updatedAt: dayjs().unix()
|
||||
})
|
||||
return this.noteStatsMap.get(event.id) ?? {}
|
||||
}
|
||||
|
||||
subscribeNoteStats(noteId: string, callback: () => void) {
|
||||
let set = this.noteStatsSubscribers.get(noteId)
|
||||
if (!set) {
|
||||
set = new Set()
|
||||
this.noteStatsSubscribers.set(noteId, set)
|
||||
}
|
||||
set.add(callback)
|
||||
return () => {
|
||||
set?.delete(callback)
|
||||
if (set?.size === 0) this.noteStatsSubscribers.delete(noteId)
|
||||
}
|
||||
}
|
||||
|
||||
private notifyNoteStats(noteId: string) {
|
||||
const set = this.noteStatsSubscribers.get(noteId)
|
||||
if (set) {
|
||||
set.forEach((cb) => cb())
|
||||
}
|
||||
}
|
||||
|
||||
getNoteStats(id: string): Partial<TNoteStats> | undefined {
|
||||
return this.noteStatsMap.get(id)
|
||||
}
|
||||
|
||||
addZap(
|
||||
pubkey: string,
|
||||
eventId: string,
|
||||
pr: string,
|
||||
amount: number,
|
||||
comment?: string,
|
||||
created_at: number = dayjs().unix(),
|
||||
notify: boolean = true
|
||||
) {
|
||||
const old = this.noteStatsMap.get(eventId) || {}
|
||||
const zapPrSet = old.zapPrSet || new Set()
|
||||
const zaps = old.zaps || []
|
||||
if (zapPrSet.has(pr)) return
|
||||
|
||||
zapPrSet.add(pr)
|
||||
zaps.push({ pr, pubkey, amount, comment, created_at })
|
||||
this.noteStatsMap.set(eventId, { ...old, zapPrSet, zaps })
|
||||
if (notify) {
|
||||
this.notifyNoteStats(eventId)
|
||||
}
|
||||
return eventId
|
||||
}
|
||||
|
||||
updateNoteStatsByEvents(events: Event[]) {
|
||||
const updatedEventIdSet = new Set<string>()
|
||||
events.forEach((evt) => {
|
||||
let updatedEventId: string | undefined
|
||||
if (evt.kind === kinds.Reaction) {
|
||||
updatedEventId = this.addLikeByEvent(evt)
|
||||
} else if (evt.kind === kinds.Repost) {
|
||||
updatedEventId = this.addRepostByEvent(evt)
|
||||
} else if (evt.kind === kinds.Zap) {
|
||||
updatedEventId = this.addZapByEvent(evt)
|
||||
}
|
||||
if (updatedEventId) {
|
||||
updatedEventIdSet.add(updatedEventId)
|
||||
}
|
||||
})
|
||||
updatedEventIdSet.forEach((eventId) => {
|
||||
this.notifyNoteStats(eventId)
|
||||
})
|
||||
}
|
||||
|
||||
private addLikeByEvent(evt: Event) {
|
||||
const targetEventId = evt.tags.findLast(tagNameEquals('e'))?.[1]
|
||||
if (!targetEventId) return
|
||||
|
||||
const old = this.noteStatsMap.get(targetEventId) || {}
|
||||
const likeIdSet = old.likeIdSet || new Set()
|
||||
const likes = old.likes || []
|
||||
if (likeIdSet.has(evt.id)) return
|
||||
|
||||
let emoji: TEmoji | string = evt.content.trim()
|
||||
if (!emoji) return
|
||||
|
||||
if (emoji.startsWith(':') && emoji.endsWith(':')) {
|
||||
const emojiInfos = getEmojiInfosFromEmojiTags(evt.tags)
|
||||
const shortcode = emoji.split(':')[1]
|
||||
const emojiInfo = emojiInfos.find((info) => info.shortcode === shortcode)
|
||||
if (emojiInfo) {
|
||||
emoji = emojiInfo
|
||||
} else {
|
||||
emoji = '+'
|
||||
}
|
||||
}
|
||||
|
||||
likeIdSet.add(evt.id)
|
||||
likes.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at, emoji })
|
||||
this.noteStatsMap.set(targetEventId, { ...old, likeIdSet, likes })
|
||||
return targetEventId
|
||||
}
|
||||
|
||||
private addRepostByEvent(evt: Event) {
|
||||
const eventId = evt.tags.find(tagNameEquals('e'))?.[1]
|
||||
if (!eventId) return
|
||||
|
||||
const old = this.noteStatsMap.get(eventId) || {}
|
||||
const repostPubkeySet = old.repostPubkeySet || new Set()
|
||||
const reposts = old.reposts || []
|
||||
if (repostPubkeySet.has(evt.pubkey)) return
|
||||
|
||||
repostPubkeySet.add(evt.pubkey)
|
||||
reposts.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
|
||||
this.noteStatsMap.set(eventId, { ...old, repostPubkeySet, reposts })
|
||||
return eventId
|
||||
}
|
||||
|
||||
private addZapByEvent(evt: Event) {
|
||||
const info = getZapInfoFromEvent(evt)
|
||||
if (!info) return
|
||||
const { originalEventId, senderPubkey, invoice, amount, comment } = info
|
||||
if (!originalEventId || !senderPubkey) return
|
||||
|
||||
return this.addZap(
|
||||
senderPubkey,
|
||||
originalEventId,
|
||||
invoice,
|
||||
amount,
|
||||
comment,
|
||||
evt.created_at,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new NoteStatsService()
|
||||
|
||||
export default instance
|
||||
|
|
@ -24,49 +24,53 @@ class PostEditorCacheService {
|
|||
|
||||
getPostContentCache({
|
||||
defaultContent,
|
||||
parentEvent
|
||||
}: { defaultContent?: string; parentEvent?: Event } = {}) {
|
||||
parentStuff
|
||||
}: { defaultContent?: string; parentStuff?: Event | string } = {}) {
|
||||
return (
|
||||
this.postContentCache.get(this.generateCacheKey(defaultContent, parentEvent)) ??
|
||||
this.postContentCache.get(this.generateCacheKey(defaultContent, parentStuff)) ??
|
||||
defaultContent
|
||||
)
|
||||
}
|
||||
|
||||
setPostContentCache(
|
||||
{ defaultContent, parentEvent }: { defaultContent?: string; parentEvent?: Event },
|
||||
{ defaultContent, parentStuff }: { defaultContent?: string; parentStuff?: Event | string },
|
||||
content: Content
|
||||
) {
|
||||
this.postContentCache.set(this.generateCacheKey(defaultContent, parentEvent), content)
|
||||
this.postContentCache.set(this.generateCacheKey(defaultContent, parentStuff), content)
|
||||
}
|
||||
|
||||
getPostSettingsCache({
|
||||
defaultContent,
|
||||
parentEvent
|
||||
}: { defaultContent?: string; parentEvent?: Event } = {}): TPostSettings | undefined {
|
||||
return this.postSettingsCache.get(this.generateCacheKey(defaultContent, parentEvent))
|
||||
parentStuff
|
||||
}: { defaultContent?: string; parentStuff?: Event | string } = {}): TPostSettings | undefined {
|
||||
return this.postSettingsCache.get(this.generateCacheKey(defaultContent, parentStuff))
|
||||
}
|
||||
|
||||
setPostSettingsCache(
|
||||
{ defaultContent, parentEvent }: { defaultContent?: string; parentEvent?: Event },
|
||||
{ defaultContent, parentStuff }: { defaultContent?: string; parentStuff?: Event | string },
|
||||
settings: TPostSettings
|
||||
) {
|
||||
this.postSettingsCache.set(this.generateCacheKey(defaultContent, parentEvent), settings)
|
||||
this.postSettingsCache.set(this.generateCacheKey(defaultContent, parentStuff), settings)
|
||||
}
|
||||
|
||||
clearPostCache({
|
||||
defaultContent,
|
||||
parentEvent
|
||||
parentStuff
|
||||
}: {
|
||||
defaultContent?: string
|
||||
parentEvent?: Event
|
||||
parentStuff?: Event | string
|
||||
}) {
|
||||
const cacheKey = this.generateCacheKey(defaultContent, parentEvent)
|
||||
const cacheKey = this.generateCacheKey(defaultContent, parentStuff)
|
||||
this.postContentCache.delete(cacheKey)
|
||||
this.postSettingsCache.delete(cacheKey)
|
||||
}
|
||||
|
||||
generateCacheKey(defaultContent: string = '', parentEvent?: Event): string {
|
||||
return parentEvent ? parentEvent.id : defaultContent
|
||||
generateCacheKey(defaultContent: string = '', parentStuff?: Event | string): string {
|
||||
return parentStuff
|
||||
? typeof parentStuff === 'string'
|
||||
? parentStuff
|
||||
: parentStuff.id
|
||||
: defaultContent
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
328
src/services/stuff-stats.service.ts
Normal file
328
src/services/stuff-stats.service.ts
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
||||
import { getEventKey, getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
|
||||
import { getZapInfoFromEvent } from '@/lib/event-metadata'
|
||||
import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag'
|
||||
import client from '@/services/client.service'
|
||||
import { TEmoji } from '@/types'
|
||||
import dayjs from 'dayjs'
|
||||
import { Event, Filter, kinds } from 'nostr-tools'
|
||||
|
||||
export type TStuffStats = {
|
||||
likeIdSet: Set<string>
|
||||
likes: { id: string; pubkey: string; created_at: number; emoji: TEmoji | string }[]
|
||||
repostPubkeySet: Set<string>
|
||||
reposts: { id: string; pubkey: string; created_at: number }[]
|
||||
zapPrSet: Set<string>
|
||||
zaps: { pr: string; pubkey: string; amount: number; created_at: number; comment?: string }[]
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
class StuffStatsService {
|
||||
static instance: StuffStatsService
|
||||
private stuffStatsMap: Map<string, Partial<TStuffStats>> = new Map()
|
||||
private stuffStatsSubscribers = new Map<string, Set<() => void>>()
|
||||
|
||||
constructor() {
|
||||
if (!StuffStatsService.instance) {
|
||||
StuffStatsService.instance = this
|
||||
}
|
||||
return StuffStatsService.instance
|
||||
}
|
||||
|
||||
async fetchStuffStats(stuff: Event | string, pubkey?: string | null) {
|
||||
const { event, externalContent } =
|
||||
typeof stuff === 'string'
|
||||
? { event: undefined, externalContent: stuff }
|
||||
: { event: stuff, externalContent: undefined }
|
||||
const key = event ? getEventKey(event) : externalContent
|
||||
const oldStats = this.stuffStatsMap.get(key)
|
||||
let since: number | undefined
|
||||
if (oldStats?.updatedAt) {
|
||||
since = oldStats.updatedAt
|
||||
}
|
||||
const [relayList, authorProfile] = event
|
||||
? await Promise.all([client.fetchRelayList(event.pubkey), client.fetchProfile(event.pubkey)])
|
||||
: []
|
||||
|
||||
const replaceableCoordinate =
|
||||
event && isReplaceableEvent(event.kind) ? getReplaceableCoordinateFromEvent(event) : undefined
|
||||
|
||||
const filters: Filter[] = []
|
||||
|
||||
if (event) {
|
||||
filters.push(
|
||||
{
|
||||
'#e': [event.id],
|
||||
kinds: [kinds.Reaction],
|
||||
limit: 500
|
||||
},
|
||||
{
|
||||
'#e': [event.id],
|
||||
kinds: [kinds.Repost],
|
||||
limit: 100
|
||||
}
|
||||
)
|
||||
} else {
|
||||
filters.push({
|
||||
'#i': [externalContent],
|
||||
kinds: [ExtendedKind.EXTERNAL_CONTENT_REACTION],
|
||||
limit: 500
|
||||
})
|
||||
}
|
||||
|
||||
if (replaceableCoordinate) {
|
||||
filters.push(
|
||||
{
|
||||
'#a': [replaceableCoordinate],
|
||||
kinds: [kinds.Reaction],
|
||||
limit: 500
|
||||
},
|
||||
{
|
||||
'#a': [replaceableCoordinate],
|
||||
kinds: [kinds.Repost],
|
||||
limit: 100
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (event && authorProfile?.lightningAddress) {
|
||||
filters.push({
|
||||
'#e': [event.id],
|
||||
kinds: [kinds.Zap],
|
||||
limit: 500
|
||||
})
|
||||
|
||||
if (replaceableCoordinate) {
|
||||
filters.push({
|
||||
'#a': [replaceableCoordinate],
|
||||
kinds: [kinds.Zap],
|
||||
limit: 500
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (pubkey) {
|
||||
filters.push(
|
||||
event
|
||||
? {
|
||||
'#e': [event.id],
|
||||
authors: [pubkey],
|
||||
kinds: [kinds.Reaction, kinds.Repost]
|
||||
}
|
||||
: {
|
||||
'#i': [externalContent],
|
||||
authors: [pubkey],
|
||||
kinds: [ExtendedKind.EXTERNAL_CONTENT_REACTION]
|
||||
}
|
||||
)
|
||||
|
||||
if (replaceableCoordinate) {
|
||||
filters.push({
|
||||
'#a': [replaceableCoordinate],
|
||||
authors: [pubkey],
|
||||
kinds: [kinds.Reaction, kinds.Repost]
|
||||
})
|
||||
}
|
||||
|
||||
if (event && authorProfile?.lightningAddress) {
|
||||
filters.push({
|
||||
'#e': [event.id],
|
||||
'#P': [pubkey],
|
||||
kinds: [kinds.Zap]
|
||||
})
|
||||
|
||||
if (replaceableCoordinate) {
|
||||
filters.push({
|
||||
'#a': [replaceableCoordinate],
|
||||
'#P': [pubkey],
|
||||
kinds: [kinds.Zap]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (since) {
|
||||
filters.forEach((filter) => {
|
||||
filter.since = since
|
||||
})
|
||||
}
|
||||
|
||||
const relays = relayList ? relayList.read.concat(BIG_RELAY_URLS).slice(0, 5) : BIG_RELAY_URLS
|
||||
|
||||
const events: Event[] = []
|
||||
await client.fetchEvents(relays, filters, {
|
||||
onevent: (evt) => {
|
||||
this.updateStuffStatsByEvents([evt])
|
||||
events.push(evt)
|
||||
}
|
||||
})
|
||||
this.stuffStatsMap.set(key, {
|
||||
...(this.stuffStatsMap.get(key) ?? {}),
|
||||
updatedAt: dayjs().unix()
|
||||
})
|
||||
return this.stuffStatsMap.get(key) ?? {}
|
||||
}
|
||||
|
||||
subscribeStuffStats(stuffKey: string, callback: () => void) {
|
||||
let set = this.stuffStatsSubscribers.get(stuffKey)
|
||||
if (!set) {
|
||||
set = new Set()
|
||||
this.stuffStatsSubscribers.set(stuffKey, set)
|
||||
}
|
||||
set.add(callback)
|
||||
return () => {
|
||||
set?.delete(callback)
|
||||
if (set?.size === 0) this.stuffStatsSubscribers.delete(stuffKey)
|
||||
}
|
||||
}
|
||||
|
||||
private notifyStuffStats(stuffKey: string) {
|
||||
const set = this.stuffStatsSubscribers.get(stuffKey)
|
||||
if (set) {
|
||||
set.forEach((cb) => cb())
|
||||
}
|
||||
}
|
||||
|
||||
getStuffStats(stuffKey: string): Partial<TStuffStats> | undefined {
|
||||
return this.stuffStatsMap.get(stuffKey)
|
||||
}
|
||||
|
||||
addZap(
|
||||
pubkey: string,
|
||||
eventId: string,
|
||||
pr: string,
|
||||
amount: number,
|
||||
comment?: string,
|
||||
created_at: number = dayjs().unix(),
|
||||
notify: boolean = true
|
||||
) {
|
||||
const old = this.stuffStatsMap.get(eventId) || {}
|
||||
const zapPrSet = old.zapPrSet || new Set()
|
||||
const zaps = old.zaps || []
|
||||
if (zapPrSet.has(pr)) return
|
||||
|
||||
zapPrSet.add(pr)
|
||||
zaps.push({ pr, pubkey, amount, comment, created_at })
|
||||
this.stuffStatsMap.set(eventId, { ...old, zapPrSet, zaps })
|
||||
if (notify) {
|
||||
this.notifyStuffStats(eventId)
|
||||
}
|
||||
return eventId
|
||||
}
|
||||
|
||||
updateStuffStatsByEvents(events: Event[]) {
|
||||
const targetKeySet = new Set<string>()
|
||||
events.forEach((evt) => {
|
||||
let targetKey: string | undefined
|
||||
if (evt.kind === kinds.Reaction) {
|
||||
targetKey = this.addLikeByEvent(evt)
|
||||
} else if (evt.kind === ExtendedKind.EXTERNAL_CONTENT_REACTION) {
|
||||
targetKey = this.addExternalContentLikeByEvent(evt)
|
||||
} else if (evt.kind === kinds.Repost) {
|
||||
targetKey = this.addRepostByEvent(evt)
|
||||
} else if (evt.kind === kinds.Zap) {
|
||||
targetKey = this.addZapByEvent(evt)
|
||||
}
|
||||
if (targetKey) {
|
||||
targetKeySet.add(targetKey)
|
||||
}
|
||||
})
|
||||
targetKeySet.forEach((targetKey) => {
|
||||
this.notifyStuffStats(targetKey)
|
||||
})
|
||||
}
|
||||
|
||||
private addLikeByEvent(evt: Event) {
|
||||
const targetEventId = evt.tags.findLast(tagNameEquals('e'))?.[1]
|
||||
if (!targetEventId) return
|
||||
|
||||
const old = this.stuffStatsMap.get(targetEventId) || {}
|
||||
const likeIdSet = old.likeIdSet || new Set()
|
||||
const likes = old.likes || []
|
||||
if (likeIdSet.has(evt.id)) return
|
||||
|
||||
let emoji: TEmoji | string = evt.content.trim()
|
||||
if (!emoji) return
|
||||
|
||||
if (emoji.startsWith(':') && emoji.endsWith(':')) {
|
||||
const emojiInfos = getEmojiInfosFromEmojiTags(evt.tags)
|
||||
const shortcode = emoji.split(':')[1]
|
||||
const emojiInfo = emojiInfos.find((info) => info.shortcode === shortcode)
|
||||
if (emojiInfo) {
|
||||
emoji = emojiInfo
|
||||
} else {
|
||||
emoji = '+'
|
||||
}
|
||||
}
|
||||
|
||||
likeIdSet.add(evt.id)
|
||||
likes.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at, emoji })
|
||||
this.stuffStatsMap.set(targetEventId, { ...old, likeIdSet, likes })
|
||||
return targetEventId
|
||||
}
|
||||
|
||||
private addExternalContentLikeByEvent(evt: Event) {
|
||||
const target = evt.tags.findLast(tagNameEquals('i'))?.[1]
|
||||
if (!target) return
|
||||
|
||||
const old = this.stuffStatsMap.get(target) || {}
|
||||
const likeIdSet = old.likeIdSet || new Set()
|
||||
const likes = old.likes || []
|
||||
if (likeIdSet.has(evt.id)) return
|
||||
|
||||
let emoji: TEmoji | string = evt.content.trim()
|
||||
if (!emoji) return
|
||||
|
||||
if (emoji.startsWith(':') && emoji.endsWith(':')) {
|
||||
const emojiInfos = getEmojiInfosFromEmojiTags(evt.tags)
|
||||
const shortcode = emoji.split(':')[1]
|
||||
const emojiInfo = emojiInfos.find((info) => info.shortcode === shortcode)
|
||||
if (emojiInfo) {
|
||||
emoji = emojiInfo
|
||||
} else {
|
||||
emoji = '+'
|
||||
}
|
||||
}
|
||||
|
||||
likeIdSet.add(evt.id)
|
||||
likes.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at, emoji })
|
||||
this.stuffStatsMap.set(target, { ...old, likeIdSet, likes })
|
||||
return target
|
||||
}
|
||||
|
||||
private addRepostByEvent(evt: Event) {
|
||||
const eventId = evt.tags.find(tagNameEquals('e'))?.[1]
|
||||
if (!eventId) return
|
||||
|
||||
const old = this.stuffStatsMap.get(eventId) || {}
|
||||
const repostPubkeySet = old.repostPubkeySet || new Set()
|
||||
const reposts = old.reposts || []
|
||||
if (repostPubkeySet.has(evt.pubkey)) return
|
||||
|
||||
repostPubkeySet.add(evt.pubkey)
|
||||
reposts.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
|
||||
this.stuffStatsMap.set(eventId, { ...old, repostPubkeySet, reposts })
|
||||
return eventId
|
||||
}
|
||||
|
||||
private addZapByEvent(evt: Event) {
|
||||
const info = getZapInfoFromEvent(evt)
|
||||
if (!info) return
|
||||
const { originalEventId, senderPubkey, invoice, amount, comment } = info
|
||||
if (!originalEventId || !senderPubkey) return
|
||||
|
||||
return this.addZap(
|
||||
senderPubkey,
|
||||
originalEventId,
|
||||
invoice,
|
||||
amount,
|
||||
comment,
|
||||
evt.created_at,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new StuffStatsService()
|
||||
|
||||
export default instance
|
||||
Loading…
Add table
Add a link
Reference in a new issue