From b78f1ceaf5772fcba492d3783940038ad417f905 Mon Sep 17 00:00:00 2001 From: Keep Creating Online Date: Fri, 3 Jan 2025 15:24:42 -0500 Subject: [PATCH] update --- README.md | 69 ++++++++++ src/collector.html | 104 +++++++++++++++ src/index.html | 70 ++++++++++ src/script.js | 202 +++++++++++++++++++++++++++++ src/styles.css | 50 +++++++ src/view-archive.html | 295 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 790 insertions(+) create mode 100644 README.md create mode 100644 src/collector.html create mode 100644 src/index.html create mode 100644 src/script.js create mode 100644 src/styles.css create mode 100644 src/view-archive.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..401e711 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Archivestr + +Archivestr is a Nostr tool for creating, browsing, and broadcasting archives. It provides a seamless way to interact with Nostr archives through a collector interface for creating archives in JSON format and a viewer for browsing archive files. + +## Features + +- **Collector**: Collects and archives Nostr data in JSON format. The collector can also broadcast the archives to Nostr relays. +- **Viewer**: Loads and browses existing archive files with sorting and detailed views of event metadata and content. + +## Project Structure + +- **`index.html`**: The landing page with navigation links to the collector and viewer tools. +- **`collector.html`**: The interface for collecting and archiving Nostr events. +- **`view-archive.html`**: The interface for browsing archived JSON files. +- **`script.js`**: The logic for interacting with Nostr relays in the collector tool. + +## Getting Started + +### Prerequisites + +Ensure you have the following installed: +- A modern web browser (Chrome, Firefox, etc.) +- Basic knowledge of Nostr events and relays + +### Installation + +1. Clone the repository: + ```bash + git clone https://github.com/your-repo/archivestr.git + ``` +2. Navigate to the project directory: + ```bash + cd archivestr + ``` +3. Open `index.html` in a browser to get started. + +## Usage + +### Collector + +1. Navigate to `collector.html`. +2. Enter the NPub (public key) and relay URLs to collect data. +3. Start collecting events, and download the archive as a JSON file. + +### Viewer + +1. Navigate to `view-archive.html`. +2. Load a JSON archive file. +3. Browse, sort, and view details of the events in the archive. + +## Development + +Feel free to contribute or customize the project: + +1. Modify `collector.html` or `view-archive.html` as needed. +2. Enhance the functionality in `script.js`. +3. Update the styling in the ` + + +
+

Nostr Archive Collector

+
+ + + + + + + + +
+ +
+
+
+ +
+ +
+ + + + + + diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..1c22e28 --- /dev/null +++ b/src/index.html @@ -0,0 +1,70 @@ + + + + + + Archivestr + + + + +
+

Archivestr

+

A Nostr Archive Creation, Browser, and Broadcaster Tool

+ Collector + View Archive + GitHub Repository +
+ + diff --git a/src/script.js b/src/script.js new file mode 100644 index 0000000..25db9c0 --- /dev/null +++ b/src/script.js @@ -0,0 +1,202 @@ +document.addEventListener('DOMContentLoaded', () => { + const form = document.getElementById('archiveForm'); + const statusDiv = document.getElementById('status'); + const downloadBtn = document.getElementById('downloadBtn'); + const spinner = document.getElementById('spinner'); + const broadcastBtn = document.createElement('button'); + const fileInput = document.createElement('input'); + + // Configure file input for archive selection + fileInput.type = 'file'; + fileInput.accept = '.json'; + fileInput.style.display = 'none'; + form.appendChild(fileInput); + + broadcastBtn.textContent = 'Broadcast from Archive'; + broadcastBtn.style.display = 'none'; // Initially hidden + form.appendChild(broadcastBtn); + + let collectedEvents = []; + let loadedEvents = []; // Events loaded from a file + let subscriptions = []; // Track active subscriptions + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + collectedEvents = []; // Reset previous data + subscriptions = []; // Clear previous subscriptions + downloadBtn.style.display = 'none'; + broadcastBtn.style.display = 'none'; + statusDiv.innerHTML = 'Starting collection...'; + spinner.style.display = 'block'; + + const npub = document.getElementById('npub').value.trim(); + const relayInput = document.getElementById('relays').value.trim(); + const relayUrls = relayInput.split('\n').map(url => url.trim()).filter(url => url); + + let pubkey; + try { + const decoded = NostrTools.nip19.decode(npub); + if (decoded.type !== 'npub') throw new Error('Invalid type. Expected npub.'); + pubkey = decoded.data; + } catch (error) { + statusDiv.innerHTML = `Invalid NPub: ${error.message}`; + spinner.style.display = 'none'; + return; + } + + statusDiv.innerHTML = `Connecting to ${relayUrls.length} relay(s)...`; + + const pool = new NostrTools.SimplePool(); + const eoseTracker = relayUrls.reduce((acc, url) => ({ ...acc, [url]: false }), {}); + const eventIds = new Set(); + + // Update the filter to include additional kinds + const filter = { + kinds: [0, 1, 2, 3, 4, 6, 7, 10002, 30023], // Include multiple event kinds + authors: [pubkey], + limit: 1000, + }; + + relayUrls.forEach((url) => { + try { + const sub = pool.sub([url], [filter]); + + subscriptions.push(sub); + + sub.on('event', (event) => { + if (!eventIds.has(event.id)) { + switch (event.kind) { + case 0: + console.log('Profile Metadata captured:', event); + break; + case 2: + console.log('Relay Recommendation captured:', event); + break; + case 3: + console.log('Contact List captured:', event); + break; + case 4: + console.log('Encrypted DM captured:', event); + break; + case 30023: + console.log('Long-Form Content captured:', event); + break; + default: + console.log('Other event captured:', event); + } + collectedEvents.push(event); // Store the raw event data + eventIds.add(event.id); + } + }); + + sub.on('eose', () => { + eoseTracker[url] = true; + console.log(`EOSE received from ${url}`); + statusDiv.innerHTML += `
EOSE received from ${url}`; + checkCompletion(); + }); + + sub.on('error', (err) => { + console.error(`Error on ${url}:`, err); + statusDiv.innerHTML += `
Error on ${url}: ${err.message}`; + }); + } catch (err) { + console.error(`Subscription failed for ${url}:`, err); + statusDiv.innerHTML += `
Subscription failed for ${url}: ${err.message}`; + } + }); + + function checkCompletion() { + if (Object.values(eoseTracker).every((status) => status)) { + finishCollection(); + } + } + + function finishCollection() { + spinner.style.display = 'none'; + statusDiv.innerHTML += `
Collection complete. ${collectedEvents.length} event(s) collected.`; + downloadBtn.style.display = 'block'; + broadcastBtn.style.display = 'block'; + + subscriptions.forEach(sub => sub.unsub()); + pool.close(); + } + }); + + downloadBtn.addEventListener('click', () => { + const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(collectedEvents, null, 2)); + const downloadAnchor = document.createElement('a'); + downloadAnchor.setAttribute("href", dataStr); + const filename = `nostr_archive_${new Date().toISOString()}.json`; + downloadAnchor.setAttribute("download", filename); + document.body.appendChild(downloadAnchor); + downloadAnchor.click(); + downloadAnchor.remove(); + }); + + // Handle file selection for broadcasting + broadcastBtn.addEventListener('click', () => { + fileInput.click(); // Trigger file input + }); + + fileInput.addEventListener('change', (event) => { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const parsedData = JSON.parse(e.target.result); + if (!Array.isArray(parsedData)) throw new Error('Archive must be an array of events.'); + loadedEvents = parsedData; // Safely assign parsed data + statusDiv.innerHTML = `Loaded ${loadedEvents.length} event(s) from the archive. Ready to broadcast.`; + promptAndBroadcast(); + } catch (err) { + console.error('Error loading archive:', err); + statusDiv.innerHTML = `Failed to load archive: ${err.message}`; + } + }; + reader.readAsText(file); + }); + + function promptAndBroadcast() { + const relayInput = prompt('Enter relay URLs (one per line) for broadcasting:'); + if (!relayInput) return; + + const relayUrls = relayInput.split('\n').map(url => url.trim()).filter(url => url); + if (relayUrls.length === 0) { + alert('No valid relays provided.'); + return; + } + + const pool = new NostrTools.SimplePool(); + statusDiv.innerHTML = `Broadcasting to ${relayUrls.length} relay(s)...`; + + let successCount = 0; + let failureCount = 0; + + relayUrls.forEach((url) => { + for (const event of loadedEvents) { + try { + const success = pool.publish([url], event); // Publish event to relay + if (success) { + successCount++; + console.log(`Event broadcasted successfully to ${url}`); + statusDiv.innerHTML += `
Event broadcasted successfully to ${url}`; + } else { + failureCount++; + console.error(`Relay ${url} rejected the event.`); + statusDiv.innerHTML += `
Relay ${url} rejected the event.`; + } + } catch (err) { + failureCount++; + console.error(`Broadcast failed for ${url}:`, err); + statusDiv.innerHTML += `
Broadcast failed for ${url}: ${err.message}`; + } + } + }); + + pool.close(); + statusDiv.innerHTML += `
Broadcast complete: ${successCount} success(es), ${failureCount} failure(s).`; + } +}); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..d19d9db --- /dev/null +++ b/src/styles.css @@ -0,0 +1,50 @@ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + margin: 0; + padding: 0; +} + +.container { + width: 90%; + max-width: 600px; + margin: 50px auto; + background: #fff; + padding: 20px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +h1 { + text-align: center; +} + +label { + display: block; + margin-top: 15px; +} + +input[type="text"], +textarea { + width: 100%; + padding: 10px; + margin-top: 5px; + box-sizing: border-box; +} + +button { + margin-top: 20px; + padding: 10px 15px; + background-color: #007BFF; + border: none; + color: #fff; + cursor: pointer; +} + +button:hover { + background-color: #0056b3; +} + +#status { + margin-top: 20px; + min-height: 20px; +} diff --git a/src/view-archive.html b/src/view-archive.html new file mode 100644 index 0000000..4e2a8f4 --- /dev/null +++ b/src/view-archive.html @@ -0,0 +1,295 @@ + + + + + + Nostr Archive Viewer + + + +
+

Nostr Archive Viewer

+
No data loaded
+ + + + + + + + + + + + + + + +
#Time Kind Content
No data loaded. Please load a JSON archive file.
+
+ + + + \ No newline at end of file