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
@@ -405,9 +507,12 @@ async function getNotesWithLinks (userPubkeys, timeIntervalHours, relayUrls, ign
405
507
* @returns {String } - Formatted string with note information
406
508
*/
407
509
function formatNoteOutput ( notes ) {
510
+ // Sort notes by timestamp (newest first)
511
+ const sortedNotes = [ ...notes ] . sort ( ( a , b ) => b . created_at - a . created_at )
512
+
408
513
const output = [ ]
409
514
410
- for ( const note of notes ) {
515
+ for ( const note of sortedNotes ) {
411
516
// Get note ID as npub
412
517
const noteId = nip19 . noteEncode ( note . id )
413
518
const pubkey = nip19 . npubEncode ( note . pubkey )
@@ -500,12 +605,15 @@ function normalizeToHexPubkey (key) {
500
605
* Main function to execute the script
501
606
*/
502
607
async function main ( ) {
503
- // Load configuration from file
504
- const configPath = path . join ( __dirname , 'nostr-link-extract.config.json' )
505
- logger . info ( `Loading configuration from ${ configPath } ` )
506
- config = loadConfig ( configPath )
608
+ // Initialize database
609
+ await db . init ( )
507
610
508
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
+
509
617
logger . info ( `Starting Nostr link extraction (time interval: ${ config . timeIntervalHours } hours)` )
510
618
511
619
// Convert any npub format keys to hex
@@ -536,6 +644,9 @@ async function main () {
536
644
}
537
645
} catch ( error ) {
538
646
logger . error ( `${ error } ` )
647
+ } finally {
648
+ // Close database connection
649
+ await db . close ( )
539
650
}
540
651
}
541
652
0 commit comments