mailship/src/digest.ts

155 lines
4.2 KiB
TypeScript
Raw Normal View History

import { neventEncode, decode } from 'nostr-tools/nip19'
import {
spec,
now,
sortBy,
groupBy,
displayList,
nth,
nthEq,
dateToSeconds,
secondsToDate,
} from '@welshman/lib'
import { parse, truncate, renderAsHtml } from '@welshman/content'
import {
TrustedEvent,
getParentId,
getIdFilters,
getReplyFilters,
NOTE,
COMMENT,
REACTION,
displayProfile,
displayPubkey,
} from '@welshman/util'
import { Loader, AdapterContext, makeLoader, SocketAdapter } from '@welshman/net'
import { Router, addMinimalFallbacks } from '@welshman/router'
import { call } from '@welshman/lib'
import { displayDuration, createElement } from './util.js'
import type { Subscription } from './alert.js'
import { sendDigest } from './mailer.js'
import {
profilesByPubkey,
loadRelaySelections,
loadProfile,
} from './repository.js'
type DigestData = {
events: TrustedEvent[]
context: TrustedEvent[]
}
export class Digest {
since: number
constructor(readonly sub: Subscription) {
this.since = sub.last_digest_at || sub.created_at
}
loadHandler = async () => {
// Default handler for building event links
return 'https://coracle.social/'
}
buildParameters = async (data: DigestData) => {
const getEventVariables = (event: TrustedEvent) => {
const parsed = truncate(parse(event), { minLength: 400, maxLength: 800, mediaLength: 50 })
return {
Link: buildLink(event, handler),
Timestamp: formatter.format(secondsToDate(event.created_at)),
Icon: profilesByPubkey.get().get(event.pubkey)?.picture,
Name: displayProfileByPubkey(event.pubkey),
Content: renderAsHtml(parsed, { createElement, renderEntity }).toString(),
Replies:
repliesByParentId.get(event.id)?.filter((e) => [COMMENT, NOTE].includes(e.kind))
?.length || 0,
Reactions: repliesByParentId.get(event.id)?.filter(spec({ kind: REACTION }))?.length || 0,
}
}
const { events, context } = data
const formatter = getFormatter()
const handler = await this.loadHandler()
const repliesByParentId = groupBy(getParentId, context)
const eventsByPubkey = groupBy((e) => e.pubkey, events)
const popular = sortBy((e) => -(repliesByParentId.get(e.id)?.length || 0), events).slice(0, 12)
const topProfiles = sortBy(
([k, ev]) => -ev.length,
Array.from(eventsByPubkey.entries()).filter(([k]) => profilesByPubkey.get().get(k))
)
return {
Total: events.length,
Duration: displayDuration(Math.floor(Date.now() / 1000) - this.since),
Popular: popular.map((e) => getEventVariables(e)),
HasPopular: popular.length > 0,
TopProfiles: displayList(topProfiles.map(([pk]) => displayProfileByPubkey(pk))),
}
}
sendFromStoredEvents = async (storedEvents: { id: string; event: TrustedEvent; relay: string }[]) => {
const events = storedEvents.map(se => se.event)
const context = [...events] // For now, context == events (no reply loading)
// Load profiles for event authors
const pubkeys = new Set(events.map(e => e.pubkey))
for (const pk of pubkeys) {
try {
await loadProfile(pk)
} catch {
// pass
}
}
const data = { events, context } as DigestData
if (data.events.length > 0) {
await sendDigest(this.sub, await this.buildParameters(data))
}
}
}
// Utilities
const buildLink = (event: TrustedEvent, handler: string) => {
const relays = Router.get().Event(event).getUrls()
const nevent = neventEncode({ ...event, relays })
if (handler.includes('<bech32>')) {
return handler.replace('<bech32>', nevent)
} else {
return handler + nevent
}
}
const displayProfileByPubkey = (pubkey: string) =>
displayProfile(profilesByPubkey.get().get(pubkey), displayPubkey(pubkey))
const renderEntity = (entity: string) => {
let display = entity.slice(0, 16) + '…'
try {
const { type, data } = decode(entity)
if (type === 'npub') {
display = '@' + displayProfileByPubkey(data)
}
if (type === 'nprofile') {
display = '@' + displayProfileByPubkey(data.pubkey)
}
} catch (e) {
// Pass
}
return display
}
const getFormatter = () => {
return new Intl.DateTimeFormat('en-US', {
dateStyle: 'short',
timeStyle: 'short',
timeZone: 'UTC',
})
}