eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1#!/usr/bin/env node
2// πŸ€– tools/marketing-shots/seed-team.mjs β€” seed the "The Commons" civic-tech
3// demo space.
4//
5// Seeds a running local lchat instance (started by serve.sh / `./tools/dev
6// shots up`) with a multi-thread team space that matches the marketing web
7// frame (website .../hero/team-web.png): the space is "The Commons", its
8// flagship all-members thread "The commons 🌱" carries the exact web-frame
9// conversation (Aoife / Marcus / Devi + a reply + reactions), and five more
10// team threads give the Mac/iPhone thread list a realistic spread.
11//
12// Everything is FICTIONAL. Seeding goes through the real E2EE protocol (see
13// demo-lib.mjs) β€” the server only ever sees ciphertext.
14//
15// Usage: node tools/marketing-shots/seed-team.mjs --base http://127.0.0.1:8848 --admin <token>
16// --base the running instance (default http://127.0.0.1:8848, or $LCHAT_BASE)
17// --admin its admin token (default $LCHAT_ADMIN_TOKEN)
18// --link-origin origin to print the device link on (default = --base)
19//
20// Idempotency: this always seeds a FRESH set of threads/messages. serve.sh
21// starts from a clean DB each run, so re-seeding a clean DB is the norm; running
22// it twice against the SAME DB would duplicate the space (harmless for captures,
23// but not intended). It rewrites .demo-state.json and mints a fresh device link.
24
25import {
26 provisionMember,
27 createThread,
28 postMessage,
29 postReaction,
30 mintDeviceLink,
31 writeState,
32 today,
33} from "./demo-lib.mjs";
34
35function arg(name, def) {
36 const i = process.argv.indexOf(`--${name}`);
37 return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : def;
38}
39
40const base = arg("base", process.env.LCHAT_BASE || "http://127.0.0.1:8848");
41const adminToken = arg("admin", process.env.LCHAT_ADMIN_TOKEN || "");
42const linkOrigin = arg("link-origin", base);
43if (!adminToken) {
44 console.error("seed-team: no admin token (pass --admin <token> or set LCHAT_ADMIN_TOKEN)");
45 process.exit(1);
46}
47
48async function main() {
49 // Members (6). Cass is the operator's signed-in identity (admin); the other
50 // five are the people whose messages fill the space.
51 console.error("seed-team: provisioning members…");
52 const cass = await provisionMember(base, adminToken, "Cass", { grantsAdmin: true });
53 const aoife = await provisionMember(base, adminToken, "Aoife");
54 const marcus = await provisionMember(base, adminToken, "Marcus");
55 const devi = await provisionMember(base, adminToken, "Devi");
56 const niamh = await provisionMember(base, adminToken, "Niamh");
57 const tomas = await provisionMember(base, adminToken, "TomΓ‘s");
58 const everyone = [cass, aoife, marcus, devi, niamh, tomas];
59
60 // Threads are created + populated in ascending recency so "The commons 🌱"
61 // ends up the most-recently-active β†’ top of the list (like the family shot's
62 // "Everyone"). Cass never posts LAST in a thread, so no preview reads "You:".
63 console.error("seed-team: creating threads + messages…");
64
65 // ── Watercooler β˜• ──────────────────────────────────────────────
66 {
67 const t = await createThread(base, "Watercooler β˜•", cass, everyone);
68 await postMessage(base, t, aoife, "anyone tried the new place on Dame St?", today(8, 2));
69 await postMessage(base, t, tomas, "coffee run in 10, who's in β˜•", today(8, 6));
70 }
71
72 // ── Roadmap ────────────────────────────────────────────────────
73 {
74 const t = await createThread(base, "Roadmap", cass, everyone);
75 await postMessage(base, t, marcus, "what's the one thing that unblocks the most people?", today(8, 15));
76 await postMessage(base, t, niamh, "moved the transparency portal to next sprint", today(8, 20));
77 }
78
79 // ── Field notes πŸ““ ──────────────────────────────────────────────
80 {
81 const t = await createThread(base, "Field notes πŸ““", cass, everyone);
82 await postMessage(base, t, niamh, "librarian in Phibsboro wants the bin map printed as a poster", today(8, 25));
83 await postMessage(base, t, devi, "three councillors asked where the data came from today πŸ““", today(8, 30));
84 }
85
86 // ── Shipping πŸš€ ────────────────────────────────────────────────
87 {
88 const t = await createThread(base, "Shipping πŸš€", cass, everyone);
89 await postMessage(base, t, marcus, "bin map's passing lighthouse, merging now", today(8, 34));
90 await postMessage(base, t, aoife, "bin map v2 is live β€” dark mode + cycle lanes πŸš€", today(8, 38));
91 }
92
93 // ── Standup ────────────────────────────────────────────────────
94 // A richer thread with a Cass (own) bubble + a reaction, so it's a good
95 // ALTERNATIVE for the Mac open-thread (variety from the web frame).
96 {
97 const t = await createThread(base, "Standup", cass, everyone);
98 await postMessage(base, t, marcus, "y'day: shipped the bin map. today: vote-day banner. blockers: none", today(8, 40));
99 const mine = await postMessage(base, t, cass, "nice β€” I'll take the councillor data requests", today(8, 42));
100 await postReaction(base, t, marcus, mine, cass, "πŸ‘", today(8, 43));
101 await postMessage(base, t, devi, "scraper's green again after the site redesign", today(8, 45));
102 await postMessage(base, t, marcus, "pushed the vote-day banner. live now", today(8, 47));
103 }
104
105 // ── The commons 🌱 (flagship β€” matches team-web.png exactly) ────
106 const commons = await createThread(base, "The commons 🌱", cass, everyone);
107 await postMessage(base, commons, aoife, "housing numbers came out as another 94-page pdf πŸ™ƒ", today(9, 12));
108 const marcusMsg = await postMessage(base, commons, marcus, "the bin map shipped in four days. 3,000 users.", today(9, 13));
109 // Reactions on Marcus's message β†’ ❀️ Γ—1, πŸ”₯ Γ—2 (as in the web frame). Posted
110 // BEFORE the final message so the thread's last row is a normal message
111 // (a trailing reaction would blank the list preview).
112 await postReaction(base, commons, aoife, marcusMsg, marcus, "❀️", today(9, 13));
113 await postReaction(base, commons, devi, marcusMsg, marcus, "πŸ”₯", today(9, 13));
114 await postReaction(base, commons, niamh, marcusMsg, marcus, "πŸ”₯", today(9, 14));
115 // Cass's reply to Marcus (the orange/own bubble with the quoted context).
116 await postMessage(base, commons, cass, "ugly and useful beats perfect and pending", today(9, 15), {
117 replyTo: { id: marcusMsg.id, name: "Marcus", snippet: "the bin map shipped in four days. 3,000 users." },
118 });
119 // Devi's message is LAST β†’ the list preview reads "Devi: scraper tonight then…"
120 await postMessage(base, commons, devi, "scraper tonight then. map live before the vote 🌱", today(9, 16));
121
122 // Persist what device-link.mjs needs to re-mint links for Cass.
123 writeState({
124 base,
125 adminToken,
126 seededAt: new Date().toISOString(),
127 spaceName: "The Commons",
128 cass: { userId: cass.userId, publicKey: cass.publicKey, privateKey: cass.privateKey },
129 members: everyone.map((m) => ({ name: m.name, userId: m.userId })),
130 });
131
132 const link = await mintDeviceLink(base, adminToken, cass, linkOrigin, "You");
133 console.error("seed-team: done.");
134 console.log(link); // stdout = just the link, so callers can capture it
135}
136
137main().catch((e) => {
138 console.error("seed-team:", e.message);
139 process.exit(1);
140});
141