- Introduced WebSocket polyfill for Node.js in seed-activity.ts and seed-profiles.ts to support applesauce-relay and RxJS. - Updated package.json and package-lock.json to include 'ws' version 8.19.0 and '@types/ws' version 8.18.1 as dependencies. Co-authored-by: Cursor <cursoragent@cursor.com>
347 lines
12 KiB
TypeScript
347 lines
12 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 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' },
|
|
]
|
|
|
|
// ── 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
|
|
const topContent = CONTENT.slice(0, 8)
|
|
const midContent = CONTENT.slice(8, 16)
|
|
const trendingContent = pick(CONTENT.slice(0, 12), 5)
|
|
const tastemakerFaves = pick(CONTENT.slice(0, 10), 6)
|
|
|
|
// ── 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.',
|
|
]
|
|
|
|
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.',
|
|
]
|
|
|
|
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.',
|
|
]
|
|
|
|
// ── 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++
|
|
}
|
|
}
|
|
|
|
console.log(` ✓ ${count} reactions seeded`)
|
|
}
|
|
|
|
// ── 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(2, 5))
|
|
|
|
for (const persona of commenters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const comments =
|
|
Math.random() < 0.7 ? POSITIVE_COMMENTS : MIXED_COMMENTS
|
|
const content = comments[randomInt(0, comments.length - 1)]
|
|
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, 4)) {
|
|
const url = contentUrl(item.id)
|
|
const commenters = pick(allPersonas, randomInt(1, 2))
|
|
|
|
for (const persona of commenters) {
|
|
const signer = PrivateKeySigner.fromKey(persona.nsec)
|
|
const pool = [...MIXED_COMMENTS, ...NEGATIVE_COMMENTS]
|
|
const content = pool[randomInt(0, pool.length - 1)]
|
|
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.slice(0, 4)) {
|
|
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 = POSITIVE_COMMENTS[randomInt(0, POSITIVE_COMMENTS.length - 1)]
|
|
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.slice(0, 3)) {
|
|
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 = POSITIVE_COMMENTS[randomInt(0, POSITIVE_COMMENTS.length - 1)]
|
|
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++
|
|
}
|
|
}
|
|
|
|
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)
|
|
})
|