Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 55 additions & 10 deletions dynamic-content/script-02.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,9 @@ const writer = bee.makeFeedWriter(topic, pk);
await writer.upload(batchId, upload.reference);
console.log("Feed updated at index 0");

// Brief pause to allow the node to index the feed chunk
await new Promise((r) => setTimeout(r, 1000));

// Read the latest reference from the feed
// Read the latest reference from the feed (retries until indexed)
const reader = bee.makeFeedReader(topic, owner);
const result = await reader.downloadReference();
const result = await retryFeedRead(() => reader.downloadReference());
console.log("Latest reference:", result.reference.toHex());
console.log("Current index:", result.feedIndex.toBigInt());

Expand All @@ -58,10 +55,58 @@ console.log("\nNew content hash:", upload2.reference.toHex());
await writer.upload(batchId, upload2.reference);
console.log("Feed updated at index 1");

// Brief pause to allow the node to index the feed chunk
await new Promise((r) => setTimeout(r, 1000));

// Reading the feed now returns the updated reference
const result2 = await reader.downloadReference();
// Reading the feed now returns the updated reference.
// Pass minFeedIndex so the retry waits for the new entry, not just any entry.
const result2 = await retryFeedRead(
() => reader.downloadReference(),
result.feedIndex.toBigInt() + 1n
);
console.log("Latest reference:", result2.reference.toHex());
console.log("Current index:", result2.feedIndex.toBigInt()); // 1n

// --- Retry helper ---

/**
* Retries a feed read function until it returns an entry at or above
* minFeedIndex. Logs elapsed time on each attempt and prompts the user
* to continue or exit every 10 seconds when running interactively.
*
* @param {() => Promise<{feedIndex: {toBigInt: () => bigint}, reference: any}>} fn
* @param {bigint} minFeedIndex Minimum feed index to accept (default 0n)
* @returns {Promise<any>} Resolved value of fn once the index requirement is met
*/
async function retryFeedRead(fn, minFeedIndex = 0n) {
const RETRY_INTERVAL_MS = 1_000;
const PROMPT_INTERVAL_MS = 10_000;
const start = Date.now();
let lastPrompt = start;

while (true) {
try {
const value = await fn();
if (value.feedIndex.toBigInt() >= minFeedIndex) {
if (Date.now() > start + RETRY_INTERVAL_MS) {
process.stdout.write("\n"); // clear the retrying line
}
return value;
}
// Got a stale entry — treat as a miss and keep retrying
} catch {}

const elapsed = Math.round((Date.now() - start) / 1000);
process.stdout.write(
`\rFeed not yet indexed, retrying... (${elapsed}s elapsed) `
);

const now = Date.now();
if (process.stdin.isTTY && now - lastPrompt >= PROMPT_INTERVAL_MS) {
lastPrompt = now;
process.stdout.write(
`\nStill waiting after ${elapsed}s. Press Enter to keep retrying, or Ctrl+C to exit: `
);
await new Promise((resolve) => process.stdin.once("data", resolve));
}

await new Promise((r) => setTimeout(r, RETRY_INTERVAL_MS));
}
}
2 changes: 2 additions & 0 deletions multi-author-blog/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
BEE_URL=http://localhost:1633
BATCH_ID=YOUR_BATCH_ID
6 changes: 6 additions & 0 deletions multi-author-blog/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
config.json
authors.json
alice-posts.json
bob-posts.json
*-posts.json
127 changes: 127 additions & 0 deletions multi-author-blog/add-author.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* add-author.js — Add a new author to the multi-author blog.
*
* This script:
* 1. Generates a new key for the author
* 2. Uploads an empty initial blog page for them
* 3. Creates their feed and feed manifest
* 4. Appends their entry to authors.json and re-uploads to the index feed
* 5. Updates config.json so the author can use add-post.js
*
* Run update-index.js afterwards to refresh the homepage with the new author.
*
* Usage:
* node add-author.js <name>
*
* Example:
* node add-author.js charlie
*/

import { Bee, Topic, PrivateKey } from "@ethersphere/bee-js";
import crypto from "crypto";
import { readFileSync, writeFileSync } from "fs";
import { config } from "dotenv";
config();

const [,, nameArg] = process.argv;
if (!nameArg) {
console.error("Usage: node add-author.js <name>");
process.exit(1);
}

const authorName = nameArg.toLowerCase();
const authorLabel = authorName.charAt(0).toUpperCase() + authorName.slice(1);

const bee = new Bee(process.env.BEE_URL);
const batchId = process.env.BATCH_ID;
const cfg = JSON.parse(readFileSync("config.json", "utf-8"));

if (cfg[authorName]) {
console.error(`Author "${authorName}" already exists in config.json.`);
process.exit(1);
}

// --- Step 1: Generate a key for the new author ---
const hex = "0x" + crypto.randomBytes(32).toString("hex");
const authorKey = new PrivateKey(hex);
const authorOwner = authorKey.publicKey().address();
const authorTopic = Topic.fromString(`${authorName}-posts`);

console.log(`Adding author: ${authorLabel}`);
console.log(`Address: ${authorOwner.toHex()}`);

// --- Step 2: Upload initial empty page ---
const html = generateAuthorHTML(authorLabel, []);
const upload = await bee.uploadFile(batchId, html, "index.html", {
contentType: "text/html",
});
console.log("Uploaded initial page:", upload.reference.toHex());

// --- Step 3: Create feed and manifest ---
const writer = bee.makeFeedWriter(authorTopic, authorKey);
await writer.upload(batchId, upload.reference);

const manifest = await bee.createFeedManifest(batchId, authorTopic, authorOwner);
console.log("Feed manifest:", manifest.toHex());

// --- Step 4: Append to authors.json and re-upload to index feed ---
const authors = JSON.parse(readFileSync("authors.json", "utf-8"));

if (authors.find((a) => a.name.toLowerCase() === authorName)) {
console.error(`Author "${authorName}" already exists in authors.json.`);
process.exit(1);
}

authors.push({
name: authorLabel,
topic: `${authorName}-posts`,
owner: authorOwner.toHex(),
feedManifest: manifest.toHex(),
});
const authorsJson = JSON.stringify(authors, null, 2);
writeFileSync("authors.json", authorsJson);

const indexUpload = await bee.uploadFile(batchId, authorsJson, "authors.json", {
contentType: "application/json",
});
const adminKey = new PrivateKey(cfg.admin.privateKey);
const indexTopic = Topic.fromString(cfg.topics.index);
const indexWriter = bee.makeFeedWriter(indexTopic, adminKey);
await indexWriter.upload(batchId, indexUpload.reference);
console.log("Index feed updated.");

// --- Step 5: Update config.json ---
cfg[authorName] = {
privateKey: authorKey.toHex(),
owner: authorOwner.toHex(),
};
cfg.topics[authorName] = `${authorName}-posts`;
cfg.manifests[authorName] = manifest.toHex();
writeFileSync("config.json", JSON.stringify(cfg, null, 2));

console.log(`\nAuthor "${authorLabel}" added! (${authors.length} authors total)`);
console.log(`Their feed: ${process.env.BEE_URL}/bzz/${manifest.toHex()}/`);
console.log(`Run update-index.js to refresh the homepage.`);

function generateAuthorHTML(name, posts) {
const items = posts
.map(
(p) => `
<div style="border:1px solid #ddd; padding:12px; margin:8px 0; border-radius:4px;">
<h2 style="margin:0 0 4px 0;">${p.title}</h2>
<small style="color:#888;">${p.date}</small>
<p>${p.body}</p>
</div>`
)
.join("\n");

return `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>${name}'s Blog</title></head>
<body style="max-width:680px; margin:40px auto; font-family:sans-serif;">
<h1>${name}'s Blog</h1>
<p>${posts.length} post${posts.length !== 1 ? "s" : ""}</p>
${items || "<p><em>No posts yet.</em></p>"}
</body>
</html>`;
}
90 changes: 90 additions & 0 deletions multi-author-blog/add-post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* add-post.js — Publish a new post as an author.
*
* Each author independently controls their own feed. This script:
* 1. Loads the author's key and topic from config.json
* 2. Appends the new post to the author's local post list
* 3. Regenerates the author's HTML page
* 4. Uploads it and updates the author's feed
*
* Usage:
* node add-post.js <alice|bob> "Post title" "Post body"
*/

import { Bee, Topic, PrivateKey } from "@ethersphere/bee-js";
import { readFileSync, writeFileSync } from "fs";
import { config } from "dotenv";
config();

const [,, authorArg, title, ...bodyWords] = process.argv;
const body = bodyWords.join(" ");

if (!authorArg || !title || !body) {
console.error('Usage: node add-post.js <alice|bob> "Post title" "Post body"');
process.exit(1);
}

const bee = new Bee(process.env.BEE_URL);
const batchId = process.env.BATCH_ID;
const cfg = JSON.parse(readFileSync("config.json", "utf-8"));

const author = cfg[authorArg];
if (!author) {
console.error(`Unknown author: ${authorArg}`);
process.exit(1);
}

const pk = new PrivateKey(author.privateKey);
const topic = Topic.fromString(cfg.topics[authorArg]);

// Load or initialize the author's post list
const postsFile = `${authorArg}-posts.json`;
let posts = [];
try {
posts = JSON.parse(readFileSync(postsFile, "utf-8"));
} catch {
// First post — file doesn't exist yet
}

const newPost = { title, body, date: new Date().toISOString() };
posts.push(newPost);
writeFileSync(postsFile, JSON.stringify(posts, null, 2));

// Regenerate the author's page HTML
const html = generateAuthorHTML(
authorArg.charAt(0).toUpperCase() + authorArg.slice(1),
posts
);

// Upload and update the author's feed
const upload = await bee.uploadFile(batchId, html, "index.html", {
contentType: "text/html",
});
const writer = bee.makeFeedWriter(topic, pk);
await writer.upload(batchId, upload.reference);

console.log(`Post published by ${authorArg}! (${posts.length} total)`);
console.log("View: " + `${process.env.BEE_URL}/bzz/${cfg.manifests[authorArg]}/`);

function generateAuthorHTML(name, posts) {
const items = posts
.map(
(p) => `
<div style="border:1px solid #ddd; padding:12px; margin:8px 0; border-radius:4px;">
<h2 style="margin:0 0 4px 0;">${p.title}</h2>
<small style="color:#888;">${p.date}</small>
<p>${p.body}</p>
</div>`
)
.join("\n");

return `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>${name}'s Blog</title></head>
<body style="max-width:680px; margin:40px auto; font-family:sans-serif;">
<h1>${name}'s Blog</h1>
<p>${posts.length} post${posts.length !== 1 ? "s" : ""}</p>
${items}
</body>
</html>`;
}
Loading