|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +const { execSync } = require('child_process') |
| 4 | +module.paths.push(execSync('npm config get prefix').toString().trim() + '/lib/node_modules') |
1 | 5 | const WebSocket = require('ws') // You might need to install this: npm install ws
|
2 | 6 | const { nip19 } = require('nostr-tools') // Keep this for formatting
|
3 | 7 | const fs = require('fs')
|
4 | 8 | const path = require('path')
|
| 9 | +const sqlite3 = require('sqlite3').verbose() // Add this at the top with other requires |
5 | 10 |
|
6 | 11 | // ANSI color codes
|
7 | 12 | const colors = {
|
@@ -39,6 +44,91 @@ const colors = {
|
39 | 44 | }
|
40 | 45 | }
|
41 | 46 |
|
| 47 | +// Add these new database utility functions after the color definitions but before the config |
| 48 | +const db = { |
| 49 | + connection: null, |
| 50 | + |
| 51 | + async init () { |
| 52 | + return new Promise((resolve, reject) => { |
| 53 | + const dbPath = path.join(__dirname, 'nostr-links.db') |
| 54 | + this.connection = new sqlite3.Database(dbPath, (err) => { |
| 55 | + if (err) { |
| 56 | + logger.error(`Error opening database: ${err.message}`) |
| 57 | + reject(err) |
| 58 | + return |
| 59 | + } |
| 60 | + |
| 61 | + this.connection.run(` |
| 62 | + CREATE TABLE IF NOT EXISTS notes ( |
| 63 | + id TEXT PRIMARY KEY, |
| 64 | + pubkey TEXT, |
| 65 | + content TEXT, |
| 66 | + created_at INTEGER, |
| 67 | + metadata TEXT, |
| 68 | + processed_at INTEGER |
| 69 | + ) |
| 70 | + `, (err) => { |
| 71 | + if (err) { |
| 72 | + logger.error(`Error creating table: ${err.message}`) |
| 73 | + reject(err) |
| 74 | + return |
| 75 | + } |
| 76 | + resolve() |
| 77 | + }) |
| 78 | + }) |
| 79 | + }) |
| 80 | + }, |
| 81 | + |
| 82 | + async getLatestNoteTimestamp () { |
| 83 | + return new Promise((resolve, reject) => { |
| 84 | + this.connection.get( |
| 85 | + 'SELECT MAX(created_at) as latest FROM notes', |
| 86 | + (err, row) => { |
| 87 | + if (err) { |
| 88 | + reject(err) |
| 89 | + return |
| 90 | + } |
| 91 | + resolve(row?.latest || 0) |
| 92 | + } |
| 93 | + ) |
| 94 | + }) |
| 95 | + }, |
| 96 | + |
| 97 | + async saveNote (note) { |
| 98 | + return new Promise((resolve, reject) => { |
| 99 | + const metadata = note.userMetadata ? JSON.stringify(note.userMetadata) : null |
| 100 | + this.connection.run( |
| 101 | + `INSERT OR IGNORE INTO notes (id, pubkey, content, created_at, metadata, processed_at) |
| 102 | + VALUES (?, ?, ?, ?, ?, ?)`, |
| 103 | + [note.id, note.pubkey, note.content, note.created_at, metadata, Math.floor(Date.now() / 1000)], |
| 104 | + (err) => { |
| 105 | + if (err) { |
| 106 | + reject(err) |
| 107 | + return |
| 108 | + } |
| 109 | + resolve() |
| 110 | + } |
| 111 | + ) |
| 112 | + }) |
| 113 | + }, |
| 114 | + |
| 115 | + async close () { |
| 116 | + return new Promise((resolve, reject) => { |
| 117 | + if (this.connection) { |
| 118 | + this.connection.close((err) => { |
| 119 | + if (err) { |
| 120 | + reject(err) |
| 121 | + return |
| 122 | + } |
| 123 | + resolve() |
| 124 | + }) |
| 125 | + } else { |
| 126 | + resolve() |
| 127 | + } |
| 128 | + }) |
| 129 | + } |
| 130 | +} |
| 131 | + |
42 | 132 | // Default configuration
|
43 | 133 | let config = {
|
44 | 134 | userPubkeys: [],
|
@@ -236,9 +326,16 @@ async function fetchEvents (relayUrls, filter, timeoutMs = 10000) {
|
236 | 326 | * @returns {Promise<Array>} - Array of note objects containing external links within the time interval
|
237 | 327 | */
|
238 | 328 | async function getNotesWithLinks (userPubkeys, timeIntervalHours, relayUrls, ignorePubkeys = []) {
|
| 329 | + // Get the latest stored note timestamp |
| 330 | + const latestStoredTimestamp = await db.getLatestNoteTimestamp() |
| 331 | + |
239 | 332 | // Calculate the cutoff time in seconds (Nostr uses UNIX timestamp)
|
240 | 333 | const now = Math.floor(Date.now() / 1000)
|
241 |
| - const cutoffTime = now - (timeIntervalHours * 60 * 60) |
| 334 | + // Use the later of: configured time interval or latest stored note |
| 335 | + const configuredCutoff = now - (timeIntervalHours * 60 * 60) |
| 336 | + const cutoffTime = Math.max(configuredCutoff, latestStoredTimestamp) |
| 337 | + |
| 338 | + logger.debug(`Using cutoff time: ${new Date(cutoffTime * 1000).toISOString()}`) |
242 | 339 |
|
243 | 340 | const allNotesWithLinks = []
|
244 | 341 | const allFollowedPubkeys = new Set() // To collect all followed pubkeys
|
@@ -395,6 +492,11 @@ async function getNotesWithLinks (userPubkeys, timeIntervalHours, relayUrls, ign
|
395 | 492 | logger.progress(`Completed processing all ${totalBatches} batches`)
|
396 | 493 | }
|
397 | 494 |
|
| 495 | + // After processing notes and before returning, save them to the database |
| 496 | + for (const note of allNotesWithLinks) { |
| 497 | + await db.saveNote(note) |
| 498 | + } |
| 499 | + |
398 | 500 | return allNotesWithLinks
|
399 | 501 | }
|
400 | 502 |
|
@@ -503,12 +605,15 @@ function normalizeToHexPubkey (key) {
|
503 | 605 | * Main function to execute the script
|
504 | 606 | */
|
505 | 607 | async function main () {
|
506 |
| - // Load configuration from file |
507 |
| - const configPath = path.join(__dirname, 'nostr-link-extract.config.json') |
508 |
| - logger.info(`Loading configuration from ${configPath}`) |
509 |
| - config = loadConfig(configPath) |
| 608 | + // Initialize database |
| 609 | + await db.init() |
510 | 610 |
|
511 | 611 | try {
|
| 612 | + // Load configuration from file |
| 613 | + const configPath = path.join(__dirname, 'nostr-link-extract.config.json') |
| 614 | + logger.info(`Loading configuration from ${configPath}`) |
| 615 | + config = loadConfig(configPath) |
| 616 | + |
512 | 617 | logger.info(`Starting Nostr link extraction (time interval: ${config.timeIntervalHours} hours)`)
|
513 | 618 |
|
514 | 619 | // Convert any npub format keys to hex
|
@@ -539,6 +644,9 @@ async function main () {
|
539 | 644 | }
|
540 | 645 | } catch (error) {
|
541 | 646 | logger.error(`${error}`)
|
| 647 | + } finally { |
| 648 | + // Close database connection |
| 649 | + await db.close() |
542 | 650 | }
|
543 | 651 | }
|
544 | 652 |
|
|
0 commit comments