mirror of
https://github.com/PR0M3TH3AN/Archivestr.git
synced 2025-09-07 14:58:53 +00:00
update
This commit is contained in:
69
README.md
Normal file
69
README.md
Normal file
@@ -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 `<style>` sections or move styles to external CSS files.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please fork the repository and submit a pull request with your changes.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## GitHub Repository
|
||||
|
||||
Visit the [GitHub repository](https://github.com/your-repo/archivestr) for the source code and updates.
|
104
src/collector.html
Normal file
104
src/collector.html
Normal file
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Nostr Archive Collector</title>
|
||||
<style>
|
||||
/* Basic styling for better UX */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
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;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
margin-top: 20px;
|
||||
padding: 10px 15px;
|
||||
background: #28a745;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
#status {
|
||||
margin-top: 20px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
#downloadBtn {
|
||||
background: #007bff;
|
||||
}
|
||||
#spinner {
|
||||
display: none;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
/* Simple CSS Spinner */
|
||||
.loader {
|
||||
border: 8px solid #f3f3f3;
|
||||
border-top: 8px solid #007bff;
|
||||
border-radius: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Nostr Archive Collector</h1>
|
||||
<form id="archiveForm">
|
||||
<label for="npub">NPub (Public Key):</label>
|
||||
<input type="text" id="npub" name="npub" placeholder="Enter NPub" required>
|
||||
<!-- Removed pattern and title attributes -->
|
||||
|
||||
<label for="relays">Relay URLs (one per line):</label>
|
||||
<textarea id="relays" name="relays" rows="5" placeholder="wss://relay1.example.com
|
||||
wss://relay2.example.com" required></textarea>
|
||||
|
||||
<button type="submit">Start Collecting</button>
|
||||
</form>
|
||||
|
||||
<div id="spinner">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
<div id="status"></div>
|
||||
<button id="downloadBtn" style="display: none;">Download Archive</button>
|
||||
</div>
|
||||
|
||||
<!-- Include nostr-tools library via CDN (correct path) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/nostr-tools@1.7.0/lib/nostr.bundle.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
70
src/index.html
Normal file
70
src/index.html
Normal file
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Archivestr</title>
|
||||
<meta name="description" content="Archivestr: A Nostr Archive Creation, Browser, and Broadcaster Tool">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9f9f9;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
padding: 20px 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 24px;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 30px;
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
padding: 10px 15px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
a:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.github-link {
|
||||
margin-top: 20px;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.github-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Archivestr</h1>
|
||||
<p>A Nostr Archive Creation, Browser, and Broadcaster Tool</p>
|
||||
<a href="collector.html">Collector</a>
|
||||
<a href="view-archive.html">View Archive</a>
|
||||
<a class="github-link" href="https://github.com/your-repo/archivestr" target="_blank">GitHub Repository</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
202
src/script.js
Normal file
202
src/script.js
Normal file
@@ -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 += `<br>EOSE received from ${url}`;
|
||||
checkCompletion();
|
||||
});
|
||||
|
||||
sub.on('error', (err) => {
|
||||
console.error(`Error on ${url}:`, err);
|
||||
statusDiv.innerHTML += `<br>Error on ${url}: ${err.message}`;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Subscription failed for ${url}:`, err);
|
||||
statusDiv.innerHTML += `<br>Subscription failed for ${url}: ${err.message}`;
|
||||
}
|
||||
});
|
||||
|
||||
function checkCompletion() {
|
||||
if (Object.values(eoseTracker).every((status) => status)) {
|
||||
finishCollection();
|
||||
}
|
||||
}
|
||||
|
||||
function finishCollection() {
|
||||
spinner.style.display = 'none';
|
||||
statusDiv.innerHTML += `<br>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 += `<br>Event broadcasted successfully to ${url}`;
|
||||
} else {
|
||||
failureCount++;
|
||||
console.error(`Relay ${url} rejected the event.`);
|
||||
statusDiv.innerHTML += `<br>Relay ${url} rejected the event.`;
|
||||
}
|
||||
} catch (err) {
|
||||
failureCount++;
|
||||
console.error(`Broadcast failed for ${url}:`, err);
|
||||
statusDiv.innerHTML += `<br>Broadcast failed for ${url}: ${err.message}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pool.close();
|
||||
statusDiv.innerHTML += `<br>Broadcast complete: ${successCount} success(es), ${failureCount} failure(s).`;
|
||||
}
|
||||
});
|
50
src/styles.css
Normal file
50
src/styles.css
Normal file
@@ -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;
|
||||
}
|
295
src/view-archive.html
Normal file
295
src/view-archive.html
Normal file
@@ -0,0 +1,295 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nostr Archive Viewer</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.stats {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
input[type="file"] {
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}
|
||||
th, td {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
cursor: pointer;
|
||||
background: #f4f4f4;
|
||||
position: relative;
|
||||
}
|
||||
th:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.content-cell {
|
||||
max-width: 600px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.sort-indicator::after {
|
||||
content: '⬍';
|
||||
margin-left: 5px;
|
||||
}
|
||||
.sort-asc::after {
|
||||
content: '↑';
|
||||
}
|
||||
.sort-desc::after {
|
||||
content: '↓';
|
||||
}
|
||||
.number-column {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Nostr Archive Viewer</h1>
|
||||
<div id="stats" class="stats">No data loaded</div>
|
||||
<input type="file" id="fileInput" accept=".json">
|
||||
<table id="dataTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="number-column">#</th>
|
||||
<th data-sort="created_at">Time <span class="sort-indicator"></span></th>
|
||||
<th data-sort="kind">Kind <span class="sort-indicator"></span></th>
|
||||
<th>Content</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="empty-message">
|
||||
<td colspan="4">No data loaded. Please load a JSON archive file.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('fileInput').addEventListener('change', handleFileLoad);
|
||||
|
||||
let archiveData = [];
|
||||
let currentSort = { column: 'created_at', order: 'desc' };
|
||||
|
||||
function handleFileLoad(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.target.result);
|
||||
if (!Array.isArray(data)) throw new Error('Invalid archive format: Expected an array.');
|
||||
archiveData = data;
|
||||
updateStats();
|
||||
sortTable(currentSort.column); // Initial sort
|
||||
} catch (err) {
|
||||
alert(`Failed to load archive: ${err.message}`);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const statsDiv = document.getElementById('stats');
|
||||
const kindCounts = archiveData.reduce((acc, event) => {
|
||||
acc[event.kind] = (acc[event.kind] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const totalEvents = archiveData.length;
|
||||
let statsText = `Total Events: ${totalEvents}<br>`;
|
||||
statsText += 'Most Common Types: ';
|
||||
|
||||
// Get top 3 most common kinds
|
||||
const topKinds = Object.entries(kindCounts)
|
||||
.sort(([,a], [,b]) => b - a)
|
||||
.slice(0, 3)
|
||||
.map(([kind, count]) => `${getKindLabel(parseInt(kind))}: ${count}`);
|
||||
|
||||
statsText += topKinds.join(' | ');
|
||||
|
||||
statsDiv.innerHTML = statsText;
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const tbody = document.querySelector('#dataTable tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (archiveData.length === 0) {
|
||||
tbody.innerHTML = `<tr class="empty-message">
|
||||
<td colspan="4">No data loaded. Please load a JSON archive file.</td>
|
||||
</tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
archiveData.forEach((event, index) => {
|
||||
const row = document.createElement('tr');
|
||||
try {
|
||||
row.innerHTML = `
|
||||
<td class="number-column">${index + 1}</td>
|
||||
<td>${new Date(event.created_at * 1000).toLocaleString()}</td>
|
||||
<td>${getKindLabel(event.kind)}</td>
|
||||
<td class="content-cell">${getContentDisplay(event)}</td>
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error(`Error rendering event ${event.id}:`, err);
|
||||
row.innerHTML = `
|
||||
<td class="number-column">${index + 1}</td>
|
||||
<td>${new Date(event.created_at * 1000).toLocaleString()}</td>
|
||||
<td>${getKindLabel(event.kind)}</td>
|
||||
<td class="content-cell">Error loading content</td>
|
||||
`;
|
||||
}
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// Update sort indicators
|
||||
document.querySelectorAll('#dataTable th[data-sort] .sort-indicator').forEach(indicator => {
|
||||
indicator.className = 'sort-indicator';
|
||||
});
|
||||
const currentHeader = document.querySelector(`th[data-sort="${currentSort.column}"] .sort-indicator`);
|
||||
if (currentHeader) {
|
||||
currentHeader.classList.add(`sort-${currentSort.order}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getKindLabel(kind) {
|
||||
const kinds = {
|
||||
0: 'Profile Metadata',
|
||||
1: 'Short Text Note',
|
||||
2: 'Recommend Relay',
|
||||
3: 'Contacts',
|
||||
4: 'Encrypted DM',
|
||||
5: 'Event Deletion',
|
||||
6: 'Repost',
|
||||
7: 'Reaction',
|
||||
8: 'Badge Award',
|
||||
40: 'Channel Creation',
|
||||
41: 'Channel Metadata',
|
||||
42: 'Channel Message',
|
||||
43: 'Channel Hide Message',
|
||||
44: 'Channel Mute User',
|
||||
1984: 'Reporting',
|
||||
9734: 'Zap Request',
|
||||
9735: 'Zap',
|
||||
10000: 'Mute List',
|
||||
10001: 'Pin List',
|
||||
10002: 'Relay List Metadata',
|
||||
30000: 'Follow List',
|
||||
30001: 'Generic List',
|
||||
30002: 'Relay List',
|
||||
30003: 'Bookmark List',
|
||||
30004: 'Communities',
|
||||
30008: 'Profile Badges',
|
||||
30009: 'Badge Definition',
|
||||
30017: 'Create or update a stall',
|
||||
30018: 'Create or update a product',
|
||||
30023: 'Long-form Content',
|
||||
30078: 'Application-specific Data',
|
||||
30402: 'Classified Listing',
|
||||
31989: 'Handler Recommendation',
|
||||
31990: 'Handler Information'
|
||||
};
|
||||
return `${kind} (${kinds[kind] || 'Reserved/Custom'})`;
|
||||
}
|
||||
|
||||
function getContentDisplay(event) {
|
||||
try {
|
||||
if (event.kind === 0) {
|
||||
const profile = JSON.parse(event.content || '{}');
|
||||
return `Name: ${profile.name || 'N/A'}<br>
|
||||
About: ${profile.about || 'N/A'}<br>
|
||||
Picture: ${profile.picture || 'N/A'}`;
|
||||
} else if (event.kind === 30023) {
|
||||
// Display full long-form content
|
||||
const content = event.content || '';
|
||||
return content.replace(/\n/g, '<br>');
|
||||
} else if (event.kind === 4) {
|
||||
return `Encrypted Message: ${event.content || 'N/A'}`;
|
||||
} else if (event.kind === 7) {
|
||||
// Special handling for reactions
|
||||
return `Reaction: ${event.content || '+'}`;
|
||||
} else if (event.kind === 6) {
|
||||
// Special handling for reposts
|
||||
return `Repost: ${event.content || 'No content'}`;
|
||||
} else if (event.kind === 3) {
|
||||
// Special handling for contact lists
|
||||
try {
|
||||
const contacts = JSON.parse(event.content || '[]');
|
||||
return `Contact List: ${contacts.length} contacts`;
|
||||
} catch {
|
||||
return `Contact List: Unable to parse`;
|
||||
}
|
||||
} else if (event.kind === 1) {
|
||||
// Display full short text note
|
||||
return event.content ? event.content.replace(/\n/g, '<br>') : 'No content available';
|
||||
}
|
||||
return event.content || 'No content available';
|
||||
} catch (err) {
|
||||
console.error(`Error processing event ${event.id}:`, err);
|
||||
return 'Error processing content';
|
||||
}
|
||||
}
|
||||
|
||||
function sortTable(column) {
|
||||
if (currentSort.column === column) {
|
||||
currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
currentSort.column = column;
|
||||
currentSort.order = 'desc'; // Default to newest first
|
||||
}
|
||||
|
||||
const order = currentSort.order === 'asc' ? 1 : -1;
|
||||
archiveData.sort((a, b) => {
|
||||
if (a[column] < b[column]) return -1 * order;
|
||||
if (a[column] > b[column]) return 1 * order;
|
||||
return 0;
|
||||
});
|
||||
|
||||
renderTable();
|
||||
}
|
||||
|
||||
document.querySelectorAll('#dataTable th[data-sort]').forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
sortTable(th.dataset.sort);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user