- Updated the `seedComments` function to return an array of published comment event IDs for tracking. - Introduced `seedCommentReactions` to seed upvotes and downvotes on comments, improving interaction visibility. - Enhanced the `App.vue` and `MobileNav.vue` components to support a mobile search overlay, allowing users to search films seamlessly. - Added a new `MobileSearch` component for better search experience on mobile devices. - Implemented a search feature in `AppHeader.vue` with dropdown results for improved content discovery. Co-authored-by: Cursor <cursoragent@cursor.com>
567 lines
21 KiB
TypeScript
567 lines
21 KiB
TypeScript
/**
|
|
* Seeds the local relay with reactions (kind 17) and comments (kind 1111)
|
|
* for all IndeeHub content, so the UI has real data to display.
|
|
*
|
|
* Run after seed-profiles.ts and with the relay already running.
|
|
*/
|
|
// Polyfill WebSocket for Node.js (required by applesauce-relay / RxJS)
|
|
import WebSocket from 'ws'
|
|
if (!globalThis.WebSocket) {
|
|
;(globalThis as unknown as Record<string, unknown>).WebSocket = WebSocket
|
|
}
|
|
|
|
import { Relay } from 'applesauce-relay'
|
|
import { PrivateKeySigner } from 'applesauce-signers/signers/private-key-signer'
|
|
import {
|
|
TEST_PERSONAS,
|
|
TASTEMAKER_PERSONAS,
|
|
} from '../src/data/testPersonas.js'
|
|
|
|
const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:7777'
|
|
const ORIGIN = process.env.ORIGIN || 'http://localhost:5174'
|
|
|
|
// ── Content catalog (matching src/data/indeeHubFilms.ts) ──────────
|
|
const INDEEHUB_CONTENT = [
|
|
{ id: 'god-bless-bitcoin', title: 'God Bless Bitcoin' },
|
|
{ id: 'thethingswecarry', title: 'The Things We Carry' },
|
|
{ id: 'duel', title: 'Duel' },
|
|
{ id: '2b0d7349-c010-47a0-b584-49e1bf86ab2f', title: 'Hard Money' },
|
|
{ id: '665a4095-73b9-480d-a0a4-b2aafaf2bce4', title: 'Bitcoiners' },
|
|
{ id: '3c113b66-3bb5-4cac-90eb-965ecedc4aa2', title: 'Lekker Feeling' },
|
|
{ id: 'stranded', title: 'STRANDED' },
|
|
{ id: 'bbdb0178-0b96-4ab5-addf-ba1f029c1cb3', title: 'The Housing Bubble' },
|
|
{ id: '584f310b-2269-4b05-a09d-261a0a3c1f78', title: 'Menger' },
|
|
{ id: 'ef92cd99-7188-4c48-b4bf-0b31fdd8934e', title: 'Everybody Does It' },
|
|
{ id: 'e1bd64d6-63c9-4c91-8d91-c69f5376286e', title: 'Gods of Their Own Religion' },
|
|
{ id: 'forgingacountry', title: 'Forging a Country' },
|
|
{ id: 'home', title: 'HOME' },
|
|
{ id: 'e1f58162-9288-418e-803d-196dcde00782', title: 'Kismet' },
|
|
{ id: 'identity-theft', title: 'Identity Theft' },
|
|
{ id: 'comingto', title: 'Coming To' },
|
|
{ id: 'down-the-pch', title: 'Down the P.C.H.' },
|
|
{ id: '0cb9de15-566d-4130-b80c-d42e952bb803', title: 'Breaking Up Is Hard to Do' },
|
|
{ id: '24b6f7c6-8f56-40f2-831a-54f40b03c427', title: 'The Florist' },
|
|
{ id: '311f772f-6559-4982-8918-d0f4be9e1b76', title: 'Plastic Money' },
|
|
{ id: '5bd753b7-9ff1-4966-a1c4-b3b93c62ed5d', title: 'Time Traveling Thieves' },
|
|
{ id: '34f042bd-23d6-40f4-9707-4b3bb62fdd58', title: 'Little Billy' },
|
|
]
|
|
|
|
// ── TopDocumentaryFilms catalog (matching src/data/topDocFilms.ts) ──
|
|
const TOPDOC_CONTENT = [
|
|
{ id: 'tdf-god-bless-bitcoin', title: 'God Bless Bitcoin' },
|
|
{ id: 'tdf-bitcoin-end-of-money', title: 'Bitcoin: The End of Money as We Know It' },
|
|
{ id: 'tdf-bitcoin-beyond-bubble', title: 'Bitcoin: Beyond the Bubble' },
|
|
{ id: 'tdf-bitcoin-gospel', title: 'The Bitcoin Gospel' },
|
|
{ id: 'tdf-bitcoin-psyop', title: 'The Bitcoin Psyop' },
|
|
{ id: 'tdf-missing-cryptoqueen', title: 'The Missing Cryptoqueen' },
|
|
{ id: 'tdf-billion-dollar-scam', title: 'The Billion Dollar Scam' },
|
|
{ id: 'tdf-money-banking-fed', title: 'Money, Banking, and The Federal Reserve' },
|
|
{ id: 'tdf-american-dream', title: 'The American Dream' },
|
|
{ id: 'tdf-century-enslavement', title: 'Century of Enslavement' },
|
|
{ id: 'tdf-money-power-wall-street', title: 'Money, Power and Wall Street' },
|
|
{ id: 'tdf-gold-6000-year', title: "Gold: Man's 6000 Year Obsession" },
|
|
{ id: 'tdf-debtasized', title: 'Debtasized' },
|
|
{ id: 'tdf-crash-next-crisis', title: 'Crash: Are We Ready?' },
|
|
{ id: 'tdf-pension-gamble', title: 'The Pension Gamble' },
|
|
{ id: 'tdf-why-americans-poor', title: 'Why Americans Feel So Poor?' },
|
|
{ id: 'tdf-chain-reaction', title: 'Chain Reaction' },
|
|
{ id: 'tdf-usa-on-brink', title: 'USA on the Brink' },
|
|
{ id: 'tdf-big-four', title: 'The Big Four' },
|
|
{ id: 'tdf-congo-millionaires', title: 'Congo: Millionaires of Chaos' },
|
|
{ id: 'tdf-economics-of', title: 'The Economics Of' },
|
|
{ id: 'tdf-big-business-food', title: 'Big Business: Food Empires' },
|
|
{ id: 'tdf-so-long-superstores', title: 'So Long, Superstores?' },
|
|
]
|
|
|
|
// Combined catalog — seeder covers both sources
|
|
const CONTENT = [...INDEEHUB_CONTENT, ...TOPDOC_CONTENT]
|
|
|
|
// ── helpers ──────────────────────────────────────────────────────
|
|
type Persona = { name: string; nsec: string; pubkey: string }
|
|
|
|
function contentUrl(contentId: string): string {
|
|
return `${ORIGIN}/content/${contentId}`
|
|
}
|
|
|
|
function pick<T>(arr: T[], n: number): T[] {
|
|
const shuffled = [...arr].sort(() => Math.random() - 0.5)
|
|
return shuffled.slice(0, n)
|
|
}
|
|
|
|
function randomInt(min: number, max: number): number {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
}
|
|
|
|
// ── personas ────────────────────────────────────────────────────
|
|
const allPersonas: Persona[] = [
|
|
...(TEST_PERSONAS as unknown as Persona[]),
|
|
...(TASTEMAKER_PERSONAS as unknown as Persona[]),
|
|
]
|
|
const tastemakers: Persona[] = TASTEMAKER_PERSONAS as unknown as Persona[]
|
|
|
|
const now = Math.floor(Date.now() / 1000)
|
|
const ONE_DAY = 86400
|
|
const ONE_WEEK = 7 * ONE_DAY
|
|
|
|
// Content subsets for different activity patterns
|
|
// Draw from BOTH catalogs to ensure TopDoc films get activity
|
|
const topContent = [
|
|
...INDEEHUB_CONTENT.slice(0, 5),
|
|
...TOPDOC_CONTENT.slice(0, 5),
|
|
]
|
|
const midContent = [
|
|
...INDEEHUB_CONTENT.slice(5, 11),
|
|
...TOPDOC_CONTENT.slice(5, 11),
|
|
]
|
|
const trendingContent = [
|
|
...pick(INDEEHUB_CONTENT.slice(0, 8), 3),
|
|
...pick(TOPDOC_CONTENT.slice(0, 8), 3),
|
|
]
|
|
const tastemakerFaves = [
|
|
...pick(INDEEHUB_CONTENT.slice(0, 8), 3),
|
|
...pick(TOPDOC_CONTENT.slice(0, 8), 3),
|
|
]
|
|
|
|
// ── sample comments ─────────────────────────────────────────────
|
|
const POSITIVE_COMMENTS = [
|
|
'Absolutely incredible film. A masterpiece in every sense.',
|
|
'This movie changed my perspective on cinema. Must watch!',
|
|
'The cinematography alone is worth the price of admission.',
|
|
'One of the greatest performances I\'ve ever seen on screen.',
|
|
'Every frame is a painting. Stunning work.',
|
|
'I\'ve seen this at least 5 times and it gets better every watch.',
|
|
'The screenplay is tight, the pacing is perfect.',
|
|
'A landmark achievement in filmmaking.',
|
|
'This deserves every award it got and more.',
|
|
'Rewatched it last night — still holds up beautifully.',
|
|
'Such an important documentary. Everyone should see this.',
|
|
'The storytelling here is on another level.',
|
|
'Required viewing. This opened my eyes to things I never considered.',
|
|
'The depth of research in this is incredible. Really well done.',
|
|
'Finally a documentary that treats the subject seriously.',
|
|
'Shared this with my whole family. Everyone needs to see this.',
|
|
'This is the kind of content that changes how you see the world.',
|
|
'Brilliant piece of journalism. Respect to the filmmakers.',
|
|
]
|
|
|
|
const MIXED_COMMENTS = [
|
|
'Good but I think it\'s a bit overrated honestly.',
|
|
'Solid film, though the third act drags a little.',
|
|
'Worth watching once for sure, but I wouldn\'t rewatch.',
|
|
'Technically impressive but emotionally I felt nothing.',
|
|
'The hype is a bit much, but it\'s still a decent movie.',
|
|
'Some great moments, but also some really slow stretches.',
|
|
'I can see why people love it, just not my cup of tea.',
|
|
'Better than I expected, worse than the reviews suggest.',
|
|
'Interesting topic but the production quality could be better.',
|
|
'They barely scratched the surface on this topic. Wanted more depth.',
|
|
'Decent intro to the topic but experts won\'t learn anything new.',
|
|
]
|
|
|
|
const NEGATIVE_COMMENTS = [
|
|
'I really don\'t understand the hype around this one.',
|
|
'Couldn\'t finish it. Way too slow for my taste.',
|
|
'Overrated. There are much better films in this genre.',
|
|
'Felt more like a sales pitch than a documentary.',
|
|
]
|
|
|
|
// Documentary-specific comments for TopDoc films
|
|
const DOC_POSITIVE_COMMENTS = [
|
|
'One of the best Bitcoin documentaries out there. Really explains the fundamentals.',
|
|
'Everyone who thinks they understand money should watch this.',
|
|
'This completely changed how I think about the financial system.',
|
|
'Incredible deep dive into a topic most people don\'t understand.',
|
|
'The interviews in this are absolutely fascinating.',
|
|
'Been orange-pilled for years but this doc still taught me new things.',
|
|
'Sent this to my dad and now he finally gets it.',
|
|
'The production quality for a documentary like this is outstanding.',
|
|
'This should be required viewing in economics classes.',
|
|
'The parallels they draw to historical events are eye-opening.',
|
|
'Watched this with my skeptical friends. They were impressed.',
|
|
'Finally a balanced take on cryptocurrency. Well researched.',
|
|
'This doc captures the movement perfectly. A time capsule for future generations.',
|
|
'The personal stories in this really humanize what can feel like a dry topic.',
|
|
'Phenomenal. I\'ve recommended this to at least 20 people.',
|
|
]
|
|
|
|
const DOC_MIXED_COMMENTS = [
|
|
'Good overview but a bit surface-level for people already in the space.',
|
|
'Some parts felt dated already given how fast things move in crypto.',
|
|
'Wish they had interviewed more diverse perspectives.',
|
|
'Interesting but could have been 30 minutes shorter.',
|
|
'The first half is excellent, second half loses steam.',
|
|
'Fair attempt but misses some key nuances about the technology.',
|
|
]
|
|
|
|
// ── publishing helper ───────────────────────────────────────────
|
|
async function publishEvent(
|
|
relay: Relay,
|
|
signer: PrivateKeySigner,
|
|
event: { kind: number; content: string; tags: string[][]; created_at: number },
|
|
label: string,
|
|
): Promise<boolean> {
|
|
const signed = await signer.signEvent(event)
|
|
try {
|
|
const res = await relay.publish(signed, { timeout: 5000 })
|
|
if (!res.ok) console.warn(` ⚠ ${label}: ${res.message}`)
|
|
return true
|
|
} catch (err) {
|
|
console.error(` ✗ ${label}:`, err instanceof Error ? err.message : err)
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ── seed reactions (kind 17) ────────────────────────────────────
|
|
async function seedReactions(relay: Relay) {
|
|
console.log('\n📊 Seeding reactions (kind 17)...')
|
|
let count = 0
|
|
|
|
// Top content: lots of positive reactions
|
|
for (const item of topContent) {
|
|
const voters = pick(allPersonas, randomInt(5, 10))
|
|
for (const persona of voters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const emoji = Math.random() < 0.9 ? '+' : '-'
|
|
const age = randomInt(1 * ONE_DAY, 30 * ONE_DAY)
|
|
const ok = await publishEvent(relay, signer, {
|
|
kind: 17,
|
|
content: emoji,
|
|
tags: [
|
|
['i', contentUrl(item.id)],
|
|
['k', 'web'],
|
|
],
|
|
created_at: now - age,
|
|
}, `reaction ${persona.name}->${item.title}`)
|
|
if (ok) count++
|
|
}
|
|
}
|
|
|
|
// Mid content: moderate reactions, more mixed
|
|
for (const item of pick(midContent, 5)) {
|
|
const voters = pick(allPersonas, randomInt(2, 5))
|
|
for (const persona of voters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const emoji = Math.random() < 0.6 ? '+' : '-'
|
|
const age = randomInt(2 * ONE_DAY, 60 * ONE_DAY)
|
|
const ok = await publishEvent(relay, signer, {
|
|
kind: 17,
|
|
content: emoji,
|
|
tags: [
|
|
['i', contentUrl(item.id)],
|
|
['k', 'web'],
|
|
],
|
|
created_at: now - age,
|
|
}, `reaction ${persona.name}->${item.title}`)
|
|
if (ok) count++
|
|
}
|
|
}
|
|
|
|
// Trending content: recent reactions
|
|
for (const item of trendingContent) {
|
|
const voters = pick(allPersonas, randomInt(4, 8))
|
|
for (const persona of voters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const age = randomInt(0, ONE_WEEK)
|
|
const ok = await publishEvent(relay, signer, {
|
|
kind: 17,
|
|
content: '+',
|
|
tags: [
|
|
['i', contentUrl(item.id)],
|
|
['k', 'web'],
|
|
],
|
|
created_at: now - age,
|
|
}, `trending-reaction ${persona.name}->${item.title}`)
|
|
if (ok) count++
|
|
}
|
|
}
|
|
|
|
// Tastemaker-specific reactions
|
|
for (const item of tastemakerFaves) {
|
|
const voters = pick(tastemakers, randomInt(2, 5))
|
|
for (const persona of voters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const age = randomInt(0, 14 * ONE_DAY)
|
|
const ok = await publishEvent(relay, signer, {
|
|
kind: 17,
|
|
content: '+',
|
|
tags: [
|
|
['i', contentUrl(item.id)],
|
|
['k', 'web'],
|
|
],
|
|
created_at: now - age,
|
|
}, `tastemaker-reaction ${persona.name}->${item.title}`)
|
|
if (ok) count++
|
|
}
|
|
}
|
|
|
|
// ── Ensure EVERY TopDoc film has at least some reactions ───────
|
|
const alreadyReacted = new Set([
|
|
...topContent.map(c => c.id),
|
|
...midContent.map(c => c.id),
|
|
...trendingContent.map(c => c.id),
|
|
...tastemakerFaves.map(c => c.id),
|
|
])
|
|
const unreactedTopDoc = TOPDOC_CONTENT.filter(c => !alreadyReacted.has(c.id))
|
|
|
|
for (const item of unreactedTopDoc) {
|
|
const voters = pick(allPersonas, randomInt(3, 7))
|
|
for (const persona of voters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const emoji = Math.random() < 0.75 ? '+' : '-'
|
|
const age = randomInt(1 * ONE_DAY, 35 * ONE_DAY)
|
|
const ok = await publishEvent(relay, signer, {
|
|
kind: 17,
|
|
content: emoji,
|
|
tags: [
|
|
['i', contentUrl(item.id)],
|
|
['k', 'web'],
|
|
],
|
|
created_at: now - age,
|
|
}, `catch-all-reaction ${persona.name}->${item.title}`)
|
|
if (ok) count++
|
|
}
|
|
}
|
|
|
|
console.log(` ✓ ${count} reactions seeded`)
|
|
}
|
|
|
|
// ── comment picker (uses doc-specific comments for TopDoc films) ─
|
|
function pickComment(itemId: string, sentiment: 'positive' | 'mixed' | 'negative'): string {
|
|
const isTopDoc = itemId.startsWith('tdf-')
|
|
if (sentiment === 'positive') {
|
|
const pool = isTopDoc
|
|
? [...DOC_POSITIVE_COMMENTS, ...POSITIVE_COMMENTS]
|
|
: POSITIVE_COMMENTS
|
|
return pool[randomInt(0, pool.length - 1)]
|
|
}
|
|
if (sentiment === 'mixed') {
|
|
const pool = isTopDoc
|
|
? [...DOC_MIXED_COMMENTS, ...MIXED_COMMENTS]
|
|
: MIXED_COMMENTS
|
|
return pool[randomInt(0, pool.length - 1)]
|
|
}
|
|
return NEGATIVE_COMMENTS[randomInt(0, NEGATIVE_COMMENTS.length - 1)]
|
|
}
|
|
|
|
// ── seed comments (kind 1111) ───────────────────────────────────
|
|
// Returns array of published comment event IDs for use by seedCommentReactions
|
|
async function seedComments(relay: Relay): Promise<string[]> {
|
|
console.log('\n💬 Seeding comments (kind 1111)...')
|
|
let count = 0
|
|
const commentEventIds: string[] = []
|
|
|
|
/**
|
|
* Helper that publishes a comment and tracks the event ID if successful.
|
|
*/
|
|
async function publishComment(
|
|
relay: Relay,
|
|
signer: PrivateKeySigner,
|
|
event: { kind: number; content: string; tags: string[][]; created_at: number },
|
|
label: string,
|
|
): Promise<boolean> {
|
|
const signed = await signer.signEvent(event)
|
|
try {
|
|
const res = await relay.publish(signed, { timeout: 5000 })
|
|
if (!res.ok) {
|
|
console.warn(` ⚠ ${label}: ${res.message}`)
|
|
} else {
|
|
commentEventIds.push(signed.id)
|
|
}
|
|
return true
|
|
} catch (err) {
|
|
console.error(` ✗ ${label}:`, err instanceof Error ? err.message : err)
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Top content: several comments
|
|
for (const item of topContent) {
|
|
const url = contentUrl(item.id)
|
|
const commenters = pick(allPersonas, randomInt(3, 6))
|
|
|
|
for (const persona of commenters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const sentiment = Math.random() < 0.7 ? 'positive' : 'mixed'
|
|
const content = pickComment(item.id, sentiment)
|
|
const age = randomInt(1 * ONE_DAY, 30 * ONE_DAY)
|
|
|
|
const ok = await publishComment(relay, signer, {
|
|
kind: 1111,
|
|
content,
|
|
tags: [
|
|
['I', url],
|
|
['K', 'web'],
|
|
['i', url],
|
|
['k', 'web'],
|
|
],
|
|
created_at: now - age,
|
|
}, `comment ${persona.name}->${item.title}`)
|
|
if (ok) count++
|
|
}
|
|
}
|
|
|
|
// Mid content: occasional comments
|
|
for (const item of pick(midContent, 8)) {
|
|
const url = contentUrl(item.id)
|
|
const commenters = pick(allPersonas, randomInt(1, 3))
|
|
|
|
for (const persona of commenters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const r = Math.random()
|
|
const sentiment = r < 0.4 ? 'positive' : r < 0.8 ? 'mixed' : 'negative'
|
|
const content = pickComment(item.id, sentiment)
|
|
const age = randomInt(3 * ONE_DAY, 45 * ONE_DAY)
|
|
|
|
const ok = await publishComment(relay, signer, {
|
|
kind: 1111,
|
|
content,
|
|
tags: [
|
|
['I', url],
|
|
['K', 'web'],
|
|
['i', url],
|
|
['k', 'web'],
|
|
],
|
|
created_at: now - age,
|
|
}, `comment ${persona.name}->${item.title}`)
|
|
if (ok) count++
|
|
}
|
|
}
|
|
|
|
// Tastemaker reviews on their faves
|
|
for (const item of tastemakerFaves) {
|
|
const url = contentUrl(item.id)
|
|
const reviewers = pick(tastemakers, randomInt(1, 3))
|
|
|
|
for (const persona of reviewers) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const content = pickComment(item.id, 'positive')
|
|
const age = randomInt(0, 10 * ONE_DAY)
|
|
|
|
const ok = await publishComment(relay, signer, {
|
|
kind: 1111,
|
|
content,
|
|
tags: [
|
|
['I', url],
|
|
['K', 'web'],
|
|
['i', url],
|
|
['k', 'web'],
|
|
],
|
|
created_at: now - age,
|
|
}, `tastemaker-comment ${persona.name}->${item.title}`)
|
|
if (ok) count++
|
|
}
|
|
}
|
|
|
|
// Trending content: recent comments
|
|
for (const item of trendingContent) {
|
|
const url = contentUrl(item.id)
|
|
const commenters = pick(allPersonas, randomInt(2, 4))
|
|
|
|
for (const persona of commenters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const content = pickComment(item.id, 'positive')
|
|
const age = randomInt(0, 3 * ONE_DAY)
|
|
|
|
const ok = await publishComment(relay, signer, {
|
|
kind: 1111,
|
|
content,
|
|
tags: [
|
|
['I', url],
|
|
['K', 'web'],
|
|
['i', url],
|
|
['k', 'web'],
|
|
],
|
|
created_at: now - age,
|
|
}, `trending-comment ${persona.name}->${item.title}`)
|
|
if (ok) count++
|
|
}
|
|
}
|
|
|
|
// ── Ensure EVERY TopDoc film has at least some comments ────────
|
|
// This catches any TopDoc films not already in top/mid/trending subsets
|
|
const alreadyCommented = new Set([
|
|
...topContent.map(c => c.id),
|
|
...midContent.map(c => c.id),
|
|
...trendingContent.map(c => c.id),
|
|
...tastemakerFaves.map(c => c.id),
|
|
])
|
|
const uncommentedTopDoc = TOPDOC_CONTENT.filter(c => !alreadyCommented.has(c.id))
|
|
|
|
for (const item of uncommentedTopDoc) {
|
|
const url = contentUrl(item.id)
|
|
const commenters = pick(allPersonas, randomInt(2, 4))
|
|
|
|
for (const persona of commenters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const r = Math.random()
|
|
const sentiment = r < 0.6 ? 'positive' : r < 0.9 ? 'mixed' : 'negative'
|
|
const content = pickComment(item.id, sentiment)
|
|
const age = randomInt(2 * ONE_DAY, 40 * ONE_DAY)
|
|
|
|
const ok = await publishComment(relay, signer, {
|
|
kind: 1111,
|
|
content,
|
|
tags: [
|
|
['I', url],
|
|
['K', 'web'],
|
|
['i', url],
|
|
['k', 'web'],
|
|
],
|
|
created_at: now - age,
|
|
}, `catch-all-comment ${persona.name}->${item.title}`)
|
|
if (ok) count++
|
|
}
|
|
}
|
|
|
|
console.log(` ✓ ${count} comments seeded`)
|
|
return commentEventIds
|
|
}
|
|
|
|
// ── seed comment reactions (kind 7) ─────────────────────────────
|
|
// Seeds upvotes/downvotes on comment events so vote sorting is visible
|
|
async function seedCommentReactions(relay: Relay, commentEventIds: string[]) {
|
|
console.log('\n👍 Seeding comment reactions (kind 7)...')
|
|
let count = 0
|
|
|
|
for (const eventId of commentEventIds) {
|
|
const numVotes = randomInt(2, 8)
|
|
const voters = pick(allPersonas, numVotes)
|
|
|
|
for (const persona of voters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
// ~80% upvote, ~20% downvote for realistic distribution
|
|
const emoji = Math.random() < 0.8 ? '+' : '-'
|
|
const age = randomInt(0, 14 * ONE_DAY)
|
|
|
|
const ok = await publishEvent(relay, signer, {
|
|
kind: 7,
|
|
content: emoji,
|
|
tags: [['e', eventId], ['k', '1111']],
|
|
created_at: now - age,
|
|
}, `comment-reaction ${persona.name}->${eventId.slice(0, 8)}`)
|
|
if (ok) count++
|
|
}
|
|
}
|
|
|
|
console.log(` ✓ ${count} comment reactions seeded`)
|
|
}
|
|
|
|
// ── main ────────────────────────────────────────────────────────
|
|
async function main() {
|
|
console.log('🎬 Seeding activity data into relay at', RELAY_URL)
|
|
|
|
const relay = new Relay(RELAY_URL)
|
|
|
|
await seedReactions(relay)
|
|
const commentEventIds = await seedComments(relay)
|
|
await seedCommentReactions(relay, commentEventIds)
|
|
|
|
console.log('\n✅ Done! Activity seeded successfully.')
|
|
setTimeout(() => process.exit(0), 1000)
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Fatal:', err)
|
|
process.exit(1)
|
|
})
|