UNPKG

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
{ "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": [[]] } ] }