diff --git a/.gitignore b/.gitignore index 9ad985b..fda10bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Custom ignores data/ frontend/build/ +nostr/strfry-db # Logs logs diff --git a/config.js b/config.js index 5ea3715..3ac0a88 100644 --- a/config.js +++ b/config.js @@ -21,6 +21,7 @@ const config = { ln_wallet: process.env.LN_WALLET, ln_api_key: process.env.LN_API_KEY, ln_admin_key: process.env.LN_ADMIN_KEY, + nostr_key: process.env.NOSTR_KEY, data: 'data', uploads: 'data/uploads', assets: 'data/assets', diff --git a/docker-compose.yml b/docker-compose.yml index a2b3db0..573f767 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,3 +47,11 @@ services: - ./data:/data depends_on: - ipfs + + nostr: + image: dockurr/strfry:latest + ports: + - 7777:7777 + volumes: + - ./nostr/strfry-db:/app/strfry-db + - ./nostr/strfry.conf:/etc/strfry.conf diff --git a/nostr.js b/nostr.js new file mode 100644 index 0000000..167f3a5 --- /dev/null +++ b/nostr.js @@ -0,0 +1,134 @@ + +const WebSocket = require('ws'); +const config = require('./config'); + +const { + finishEvent, + validateEvent, + verifySignature, + getPublicKey +} = require('nostr-tools'); + +const RELAYS = {}; + +function createMessage(message) { + let event = { + kind: 1, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: message, + pubkey: getPublicKey(config.nostr_key), + }; + + finishEvent(event, config.nostr_key); + + const valid = validateEvent(event); + const verified = verifySignature(event); + + if (valid && verified) { + return event; + } +}; + +function openRelay(wsurl) { + const ws = new WebSocket(wsurl); + + ws.on('open', () => { + console.log(`ws open ${wsurl}`); + RELAYS[wsurl] = ws; + }); + + ws.on('error', (error) => { + console.log(`ws error from ${wsurl}: ${error}`); + }); + + ws.on('message', (event) => { + console.log(`ws message from ${wsurl}: ${event} `); + }); + + ws.on('close', () => { + console.log(`ws close ${wsurl}`); + delete RELAYS[wsurl]; + }); +} + +function closeRelays() { + for (let key in RELAYS) { + const ws = RELAYS[key]; + ws.close(); + } +} + +function countOpenRelays() { + let count = 0; + + for (let key in RELAYS) { + const ws = RELAYS[key]; + + if (ws.readyState === WebSocket.OPEN) { + count++; + } + } + + return count; +} + +function subscribeToRelays() { + const filters = { + kinds: [1], + limit: 1000, + }; + + sendRequest(filters); +} + +function sendEvent(event) { + const message = JSON.stringify(["EVENT", event]); + + for (let key in RELAYS) { + const ws = RELAYS[key]; + + if (ws.readyState === WebSocket.OPEN) { + ws.send(message); + console.log(`ws event sent to ${key}`); + } + else { + console.log(`ws ${key} not open`); + } + } +} + +function sendMessage(message) { + const event = createMessage(message); + + if (event) { + sendEvent(event); + } +} + +function sendRequest(filters) { + const sub = "foo"; + const message = JSON.stringify(["REQ", sub, filters]); + + for (let key in RELAYS) { + const ws = RELAYS[key]; + + if (ws.readyState === WebSocket.OPEN) { + ws.send(message); + console.log(`ws req sent to ${key}`); + } + else { + console.log(`ws ${key} not open`); + } + } +} + +module.exports = { + closeRelays, + countOpenRelays, + createMessage, + openRelay, + sendEvent, + sendMessage, + subscribeToRelays, +}; diff --git a/nostr/strfry.conf b/nostr/strfry.conf new file mode 100644 index 0000000..4935041 --- /dev/null +++ b/nostr/strfry.conf @@ -0,0 +1,139 @@ +## +## Default strfry config +## + +# Directory that contains the strfry LMDB database (restart required) +db = "./strfry-db/" + +dbParams { + # Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required) + maxreaders = 256 + + # Size of mmap() to use when loading LMDB (default is 10TB, does *not* correspond to disk-space used) (restart required) + mapsize = 10995116277760 + + # Disables read-ahead when accessing the LMDB mapping. Reduces IO activity when DB size is larger than RAM. (restart required) + noReadAhead = false +} + +events { + # Maximum size of normalised JSON, in bytes + maxEventSize = 65536 + + # Events newer than this will be rejected + rejectEventsNewerThanSeconds = 900 + + # Events older than this will be rejected + rejectEventsOlderThanSeconds = 94608000 + + # Ephemeral events older than this will be rejected + rejectEphemeralEventsOlderThanSeconds = 60 + + # Ephemeral events will be deleted from the DB when older than this + ephemeralEventsLifetimeSeconds = 300 + + # Maximum number of tags allowed + maxNumTags = 2000 + + # Maximum size for tag values, in bytes + maxTagValSize = 1024 +} + +relay { + # Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required) + #bind = "127.0.0.1" + bind = "0.0.0.0" + + # Port to open for the nostr websocket protocol (restart required) + port = 7777 + + # Set OS-limit on maximum number of open files/sockets (if 0, don't attempt to set) (restart required) + nofiles = 1000000 + + # HTTP header that contains the client's real IP, before reverse proxying (ie x-real-ip) (MUST be all lower-case) + realIpHeader = "" + + info { + # NIP-11: Name of this server. Short/descriptive (< 30 characters) + name = "artx.market nostr-relay" + + # NIP-11: Detailed information about relay, free-form + description = "This is a strfry instance." + + # NIP-11: Administrative nostr pubkey, for contact purposes + pubkey = "e8d667dd0c571ba799390a392b690dd7b5491f484690ba922ab3f17965fb2139" + + # NIP-11: Alternative administrative contact (email, website, etc) + contact = "davidmc@gmail.com" + } + + # Maximum accepted incoming websocket frame size (should be larger than max event) (restart required) + maxWebsocketPayloadSize = 131072 + + # Websocket-level PING message frequency (should be less than any reverse proxy idle timeouts) (restart required) + autoPingSeconds = 55 + + # If TCP keep-alive should be enabled (detect dropped connections to upstream reverse proxy) + enableTcpKeepalive = false + + # How much uninterrupted CPU time a REQ query should get during its DB scan + queryTimesliceBudgetMicroseconds = 10000 + + # Maximum records that can be returned per filter + maxFilterLimit = 500 + + # Maximum number of subscriptions (concurrent REQs) a connection can have open at any time + maxSubsPerConnection = 20 + + writePolicy { + # If non-empty, path to an executable script that implements the writePolicy plugin logic + plugin = "" + } + + compression { + # Use permessage-deflate compression if supported by client. Reduces bandwidth, but slight increase in CPU (restart required) + enabled = true + + # Maintain a sliding window buffer for each connection. Improves compression, but uses more memory (restart required) + slidingWindow = true + } + + logging { + # Dump all incoming messages + dumpInAll = false + + # Dump all incoming EVENT messages + dumpInEvents = false + + # Dump all incoming REQ/CLOSE messages + dumpInReqs = false + + # Log performance metrics for initial REQ database scans + dbScanPerf = false + + # Log reason for invalid event rejection? Can be disabled to silence excessive logging + invalidEvents = true + } + + numThreads { + # Ingester threads: route incoming requests, validate events/sigs (restart required) + ingester = 3 + + # reqWorker threads: Handle initial DB scan for events (restart required) + reqWorker = 3 + + # reqMonitor threads: Handle filtering of new events (restart required) + reqMonitor = 3 + + # negentropy threads: Handle negentropy protocol messages (restart required) + negentropy = 2 + } + + negentropy { + # Support negentropy protocol messages + enabled = true + + # Maximum records that sync will process before returning an error + maxSyncEvents = 1000000 + } +} diff --git a/package-lock.json b/package-lock.json index 5ee13d1..5e7cd92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,13 +19,15 @@ "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "node-cron": "^3.0.2", + "nostr-tools": "^1.17.0", "passport": "^0.6.0", "passport-lnurl-auth": "^1.5.1", "path": "^0.12.7", "rimraf": "^5.0.1", "serve-static": "^1.15.0", "sharp": "^0.32.1", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "ws": "^8.14.2" }, "devDependencies": { "jest": "^29.7.0" @@ -1244,16 +1246,35 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/ciphers": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz", + "integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", - "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -1264,6 +1285,42 @@ "node": ">=14" } }, + "node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "dependencies": { + "@noble/curves": "~1.1.0", + "@noble/hashes": "~1.3.1", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1968,6 +2025,20 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -2458,9 +2529,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.565", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.565.tgz", - "integrity": "sha512-XbMoT6yIvg2xzcbs5hCADi0dXBh4//En3oFXmtPX+jiyyiCTiM9DGFT2SLottjpEs9Z8Mh8SqahbR96MaHfuSg==", + "version": "1.4.566", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.566.tgz", + "integrity": "sha512-mv+fAy27uOmTVlUULy15U3DVJ+jg+8iyKH1bpwboCRhtDC69GKf1PPTZvEIhCyDr81RFqfxZJYrbgp933a1vtg==", "dev": true }, "node_modules/elliptic": { @@ -2885,20 +2956,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4870,6 +4927,27 @@ "node": ">=0.10.0" } }, + "node_modules/nostr-tools": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz", + "integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==", + "dependencies": { + "@noble/ciphers": "0.2.0", + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@scure/base": "1.1.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -6300,6 +6378,20 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -6470,6 +6562,26 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 57c86b2..dc7ef0d 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,15 @@ "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "node-cron": "^3.0.2", + "nostr-tools": "^1.17.0", "passport": "^0.6.0", "passport-lnurl-auth": "^1.5.1", "path": "^0.12.7", "rimraf": "^5.0.1", "serve-static": "^1.15.0", "sharp": "^0.32.1", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "ws": "^8.14.2" }, "devDependencies": { "jest": "^29.7.0" diff --git a/test-nostr.js b/test-nostr.js new file mode 100644 index 0000000..859a31a --- /dev/null +++ b/test-nostr.js @@ -0,0 +1,21 @@ +const nostr = require('./nostr'); + +async function main() { + + nostr.openRelay('ws://taranis.local:4848'); + nostr.openRelay('ws://localhost:7777'); + + while (nostr.countOpenRelays() < 1) { + console.log('waiting to open relays...'); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + nostr.subscribeToRelays(); + + while (true) { + nostr.sendMessage(`hello again! ${new Date().toISOString()}`); + await new Promise(resolve => setTimeout(resolve, 10000)); + } +} + +main();