node-red-contrib-nostr
Version:
Node-RED nodes for seamless Nostr protocol integration. Features robust WebSocket handling, event filtering, and NPUB-based routing. Built with TypeScript for type safety and extensive testing. Perfect for Nostr automation flows.
130 lines (129 loc) • 7.41 kB
JSON
{
"name": "Multi-User Nostr Monitor",
"nodes": [
{
"id": "relay-config",
"type": "nostr-relay-config",
"name": "Main Relays",
"relay1": "wss://relay.damus.io",
"relay2": "wss://nos.lol",
"relay3": "wss://relay.nostr.band",
"pingInterval": 30
},
{
"id": "npub-list",
"type": "function",
"name": "NPub List",
"func": "// List of NPubs to monitor (example list - replace with actual NPubs)\nconst npubs = [\n 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8yz5tc68qysh7j4xz', // Jack\n 'npub1qny3tkh0acurzla8x3zy4nhrjz5zd8l9sy9jys09umwng00manysew95gx', // Snowden\n 'npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s' // Saylor\n];\n\n// Send each NPub to a different output\nnpubs.forEach((npub, index) => {\n node.send({ payload: npub, topic: `user_${index}` });\n});\n",
"outputs": 21,
"x": 130,
"y": 120,
"wires": [
["monitor-1"], ["monitor-2"], ["monitor-3"],
["monitor-4"], ["monitor-5"], ["monitor-6"],
["monitor-7"], ["monitor-8"], ["monitor-9"],
["monitor-10"], ["monitor-11"], ["monitor-12"],
["monitor-13"], ["monitor-14"], ["monitor-15"],
["monitor-16"], ["monitor-17"], ["monitor-18"],
["monitor-19"], ["monitor-20"], ["monitor-21"]
]
},
{
"id": "monitor-1",
"type": "nostr-relay",
"name": "User 1",
"relayConfig": "relay-config",
"x": 280,
"y": 120,
"wires": [["event-router"]]
},
{
"id": "event-router",
"type": "function",
"name": "Event Router",
"func": "// Route events based on kind\nconst event = msg.payload;\n\nswitch(event.kind) {\n case 1: // Text notes\n return [msg, null, null];\n case 6: // Reposts\n return [null, msg, null];\n case 7: // Reactions\n return [null, null, msg];\n default:\n return [null, null, null];\n}",
"outputs": 3,
"x": 480,
"y": 120,
"wires": [
["text-filter"],
["repost-filter"],
["reaction-filter"]
]
},
{
"id": "text-filter",
"type": "nostr-filter",
"name": "Text Notes",
"filterType": "kind",
"eventKinds": [1],
"x": 680,
"y": 80,
"wires": [["format-text"]]
},
{
"id": "repost-filter",
"type": "nostr-filter",
"name": "Reposts",
"filterType": "kind",
"eventKinds": [6],
"x": 680,
"y": 120,
"wires": [["format-repost"]]
},
{
"id": "reaction-filter",
"type": "nostr-filter",
"name": "Reactions",
"filterType": "kind",
"eventKinds": [7],
"x": 680,
"y": 160,
"wires": [["format-reaction"]]
},
{
"id": "format-text",
"type": "function",
"name": "Format Text Note",
"func": "const event = msg.payload;\n\nconst formatted = {\n type: 'text',\n time: new Date(event.created_at * 1000).toLocaleString(),\n content: event.content,\n author: event.pubkey,\n id: event.id,\n urls: event.content.match(/https?:\\/\\/[^\\s]+/g) || [],\n tags: event.tags\n};\n\nmsg.payload = formatted;\nreturn msg;",
"x": 880,
"y": 80,
"wires": [["dashboard"]]
},
{
"id": "format-repost",
"type": "function",
"name": "Format Repost",
"func": "const event = msg.payload;\n\nconst formatted = {\n type: 'repost',\n time: new Date(event.created_at * 1000).toLocaleString(),\n content: event.content,\n author: event.pubkey,\n originalPost: event.tags.find(t => t[0] === 'e')?.[1],\n id: event.id\n};\n\nmsg.payload = formatted;\nreturn msg;",
"x": 880,
"y": 120,
"wires": [["dashboard"]]
},
{
"id": "format-reaction",
"type": "function",
"name": "Format Reaction",
"func": "const event = msg.payload;\n\nconst formatted = {\n type: 'reaction',\n time: new Date(event.created_at * 1000).toLocaleString(),\n content: event.content,\n author: event.pubkey,\n targetPost: event.tags.find(t => t[0] === 'e')?.[1],\n id: event.id\n};\n\nmsg.payload = formatted;\nreturn msg;",
"x": 880,
"y": 160,
"wires": [["dashboard"]]
},
{
"id": "dashboard",
"type": "ui_template",
"name": "Interactive Dashboard",
"group": "Nostr Monitor",
"order": 1,
"width": "24",
"height": "12",
"format": "<div class=\"nostr-feed\">\n <div ng-repeat=\"event in msg.payload | limitTo:50\" \n class=\"event-card\" \n ng-class=\"event.type\">\n <div class=\"event-header\">\n <span class=\"event-time\">{{event.time}}</span>\n <span class=\"event-type\">{{event.type}}</span>\n </div>\n \n <div class=\"event-content\" ng-if=\"event.type === 'text'\">\n {{event.content}}\n <div class=\"event-urls\" ng-if=\"event.urls.length > 0\">\n <a ng-repeat=\"url in event.urls\" \n href=\"{{url}}\" \n target=\"_blank\">{{url}}</a>\n </div>\n </div>\n \n <div class=\"event-content\" ng-if=\"event.type === 'repost'\">\n Reposted: {{event.originalPost}}\n <div ng-if=\"event.content\">\n Comment: {{event.content}}\n </div>\n </div>\n \n <div class=\"event-content\" ng-if=\"event.type === 'reaction'\">\n Reacted to: {{event.targetPost}}\n <div class=\"reaction-content\">\n {{event.content}}\n </div>\n </div>\n \n <div class=\"event-footer\">\n <span class=\"event-id\">{{event.id}}</span>\n </div>\n </div>\n</div>\n\n<style>\n.nostr-feed {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n padding: 1rem;\n}\n\n.event-card {\n border: 1px solid #ddd;\n border-radius: 8px;\n padding: 1rem;\n background: white;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\n\n.event-card.text { border-left: 4px solid #2196F3; }\n.event-card.repost { border-left: 4px solid #4CAF50; }\n.event-card.reaction { border-left: 4px solid #FF9800; }\n\n.event-header {\n display: flex;\n justify-content: space-between;\n margin-bottom: 0.5rem;\n color: #666;\n}\n\n.event-content {\n margin: 1rem 0;\n white-space: pre-wrap;\n}\n\n.event-urls {\n margin-top: 0.5rem;\n}\n\n.event-urls a {\n display: block;\n color: #2196F3;\n margin: 0.25rem 0;\n}\n\n.event-footer {\n font-size: 0.8rem;\n color: #999;\n margin-top: 0.5rem;\n}\n</style>",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "local",
"x": 1080,
"y": 120,
"wires": [[]]
}
]
}