Files
indee-demo/scripts/seed-activity.ts
Dorian 35bc78b890 Enhance content management and user interaction features
- Introduced a new content source toggle in the profile and app header to switch between IndeeHub and TopDoc films.
- Updated the content fetching logic to dynamically load content based on the selected source.
- Enhanced the seeding process to include a combined catalog of IndeeHub and TopDoc films, ensuring diverse content availability.
- Improved user interaction by preventing duplicate reactions and ensuring a smoother voting experience across comments and content.
- Added support for Amber login (NIP-55) for Android users, integrating it into the existing authentication flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 14:24:52 +00:00

510 lines
19 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) ───────────────────────────────────
async function seedComments(relay: Relay) {
console.log('\n💬 Seeding comments (kind 1111)...')
let count = 0
// 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 publishEvent(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 publishEvent(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 publishEvent(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 publishEvent(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 publishEvent(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`)
}
// ── main ────────────────────────────────────────────────────────
async function main() {
console.log('🎬 Seeding activity data into relay at', RELAY_URL)
const relay = new Relay(RELAY_URL)
await seedReactions(relay)
await seedComments(relay)
console.log('\n✅ Done! Activity seeded successfully.')
setTimeout(() => process.exit(0), 1000)
}
main().catch((err) => {
console.error('Fatal:', err)
process.exit(1)
})