nostr-inbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/package.json +44 -0
- package/src/cli.cjs +193 -0
- package/src/filters.cjs +207 -0
- package/src/inbox.cjs +245 -0
- package/src/index.cjs +16 -0
- package/src/poller.cjs +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeletor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# nostr-inbox
|
|
2
|
+
|
|
3
|
+
Unified Nostr notification stream for AI agents. Mentions, DMs, DVM requests, zaps, trust attestations, marketplace events — one event loop instead of polling five things.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install nostr-inbox
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### Streaming (long-running)
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
const { createInbox } = require('nostr-inbox');
|
|
17
|
+
|
|
18
|
+
const inbox = createInbox({
|
|
19
|
+
pubkey: 'your-hex-pubkey',
|
|
20
|
+
relays: ['wss://relay.damus.io', 'wss://nos.lol']
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
inbox.on('notification', (n) => {
|
|
24
|
+
console.log(`[${n.type}] from ${n.from.slice(0, 12)}... — ${n.content}`);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// High-priority only (DMs, DVM requests, marketplace bids)
|
|
28
|
+
inbox.on('urgent', (n) => {
|
|
29
|
+
console.log(`🔴 URGENT: ${n.type} from ${n.from.slice(0, 12)}...`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Type-specific handlers
|
|
33
|
+
inbox.on('zap', (n) => console.log(`⚡ Zapped!`));
|
|
34
|
+
inbox.on('dvm_request', (n) => console.log(`⚙️ DVM job incoming`));
|
|
35
|
+
inbox.on('trust', (n) => console.log(`🛡️ New trust attestation`));
|
|
36
|
+
|
|
37
|
+
await inbox.start();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Polling (one-shot)
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
const { poll } = require('nostr-inbox');
|
|
44
|
+
|
|
45
|
+
const result = await poll({
|
|
46
|
+
pubkey: 'your-hex-pubkey',
|
|
47
|
+
since: Math.floor(Date.now() / 1000) - 3600 // last hour
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.log(`${result.total} notifications (${result.urgent} urgent)`);
|
|
51
|
+
|
|
52
|
+
for (const [type, items] of Object.entries(result.byType)) {
|
|
53
|
+
console.log(` ${type}: ${items.length}`);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## CLI
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Watch real-time notifications
|
|
61
|
+
nostr-inbox watch --pubkey <hex>
|
|
62
|
+
|
|
63
|
+
# One-shot poll (last hour)
|
|
64
|
+
nostr-inbox poll --pubkey <hex>
|
|
65
|
+
|
|
66
|
+
# Filter by channel
|
|
67
|
+
nostr-inbox watch --pubkey <hex> --channels mentions,dms,zaps
|
|
68
|
+
|
|
69
|
+
# JSON output (for piping)
|
|
70
|
+
nostr-inbox poll --pubkey <hex> --json
|
|
71
|
+
|
|
72
|
+
# Only urgent notifications
|
|
73
|
+
nostr-inbox watch --pubkey <hex> --quiet
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Notification Types
|
|
77
|
+
|
|
78
|
+
| Type | Kind(s) | Priority | Description |
|
|
79
|
+
|------|---------|----------|-------------|
|
|
80
|
+
| `mention` | 1, 1111 | medium | Text notes / Clawstr comments tagging you |
|
|
81
|
+
| `dm` | 4, 1059 | **high** | Encrypted DMs (NIP-04 + NIP-17 gift wrap) |
|
|
82
|
+
| `dvm_request` | 5000-5099 | **high** | Someone wants you to do work (NIP-90) |
|
|
83
|
+
| `dvm_result` | 6000-6099 | medium | Response to your DVM request |
|
|
84
|
+
| `dvm_feedback` | 7000 | low | DVM processing status |
|
|
85
|
+
| `zap` | 9735 | medium | Lightning zap receipt |
|
|
86
|
+
| `reaction` | 7 | low | Likes / reactions |
|
|
87
|
+
| `trust` | 1985 | medium | ai.wot attestation about you |
|
|
88
|
+
| `trust_network` | 1985 | low | ai.wot attestation about others |
|
|
89
|
+
| `marketplace_bid` | 950 | **high** | Bid on your task (agent-escrow) |
|
|
90
|
+
| `marketplace_delivery` | 951 | **high** | Work submitted for your task |
|
|
91
|
+
| `marketplace_resolution` | 952 | **high** | Task approved / disputed |
|
|
92
|
+
|
|
93
|
+
## Channels
|
|
94
|
+
|
|
95
|
+
Enable/disable notification types:
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
const inbox = createInbox({
|
|
99
|
+
pubkey: '...',
|
|
100
|
+
channels: {
|
|
101
|
+
mentions: true, // Text note mentions
|
|
102
|
+
dms: true, // Encrypted DMs
|
|
103
|
+
dvmRequests: true, // DVM work requests
|
|
104
|
+
dvmResults: true, // DVM results
|
|
105
|
+
zaps: true, // Lightning zaps
|
|
106
|
+
reactions: false, // Likes (noisy, disable if you want)
|
|
107
|
+
trust: true, // ai.wot attestations
|
|
108
|
+
marketplace: true // agent-escrow events
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## API
|
|
114
|
+
|
|
115
|
+
### `createInbox(opts)` → `inbox`
|
|
116
|
+
|
|
117
|
+
Creates a streaming inbox.
|
|
118
|
+
|
|
119
|
+
**Options:**
|
|
120
|
+
- `pubkey` (string, required) — Your hex pubkey
|
|
121
|
+
- `relays` (string[]) — Relay URLs (default: damus, nos.lol, primal)
|
|
122
|
+
- `channels` (object) — Enable/disable notification types
|
|
123
|
+
- `since` (number) — Unix timestamp, only events after this
|
|
124
|
+
- `dedup` (boolean) — Deduplicate events (default: true)
|
|
125
|
+
- `reconnectMs` (number) — Reconnect delay (default: 5000)
|
|
126
|
+
|
|
127
|
+
**Methods:**
|
|
128
|
+
- `inbox.start()` — Connect and begin streaming
|
|
129
|
+
- `inbox.stop()` — Disconnect
|
|
130
|
+
- `inbox.status()` — Get connection status
|
|
131
|
+
- `inbox.waitFor(type, timeoutMs)` — Promise that resolves on next event of type
|
|
132
|
+
- `inbox.collect(durationMs, filter)` — Collect events for a duration
|
|
133
|
+
|
|
134
|
+
**Events:**
|
|
135
|
+
- `notification` — Every notification
|
|
136
|
+
- `urgent` — High-priority only
|
|
137
|
+
- `<type>` — Type-specific (e.g., `zap`, `dm`, `dvm_request`)
|
|
138
|
+
- `connected` / `started` / `stopped` — Lifecycle
|
|
139
|
+
- `error` — Connection errors
|
|
140
|
+
|
|
141
|
+
### `poll(opts)` → `{ total, urgent, notifications, byType }`
|
|
142
|
+
|
|
143
|
+
One-shot fetch. Same options as `createInbox` plus `timeoutMs`.
|
|
144
|
+
|
|
145
|
+
## Interop
|
|
146
|
+
|
|
147
|
+
Built for the agent economy stack:
|
|
148
|
+
- [agent-escrow](https://github.com/jeletor/agent-escrow) — Marketplace events (bids, deliveries, resolutions)
|
|
149
|
+
- [ai-wot](https://github.com/jeletor/ai-wot) — Trust attestation notifications
|
|
150
|
+
- [agent-discovery](https://github.com/jeletor/agent-discovery) — Service announcements
|
|
151
|
+
- [lightning-agent](https://github.com/jeletor/lightning-agent) — Zap handling
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nostr-inbox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unified Nostr notification stream for AI agents — mentions, DMs, DVM requests, zaps, WoT attestations in one event loop",
|
|
5
|
+
"main": "src/index.cjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nostr-inbox": "src/cli.cjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.cjs"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"nostr",
|
|
14
|
+
"notifications",
|
|
15
|
+
"inbox",
|
|
16
|
+
"ai",
|
|
17
|
+
"agent",
|
|
18
|
+
"dvm",
|
|
19
|
+
"nip-90",
|
|
20
|
+
"zap",
|
|
21
|
+
"mentions",
|
|
22
|
+
"ai-wot",
|
|
23
|
+
"lightning",
|
|
24
|
+
"streaming"
|
|
25
|
+
],
|
|
26
|
+
"author": "Jeletor <jeletor@jeletor.com>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/jeletor/nostr-inbox.git"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"nostr-tools": "^2.12.0",
|
|
34
|
+
"ws": "^8.18.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src/",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
]
|
|
44
|
+
}
|
package/src/cli.cjs
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { createInbox, poll, KINDS } = require('./index.cjs');
|
|
5
|
+
|
|
6
|
+
const PRIORITY_COLORS = {
|
|
7
|
+
high: '\x1b[31m', // red
|
|
8
|
+
medium: '\x1b[33m', // yellow
|
|
9
|
+
low: '\x1b[90m' // gray
|
|
10
|
+
};
|
|
11
|
+
const RESET = '\x1b[0m';
|
|
12
|
+
const BOLD = '\x1b[1m';
|
|
13
|
+
|
|
14
|
+
function usage() {
|
|
15
|
+
console.log(`nostr-inbox — Unified Nostr notification stream for AI agents
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
nostr-inbox watch [options] Stream notifications in real-time
|
|
19
|
+
nostr-inbox poll [options] One-shot check (fetch and exit)
|
|
20
|
+
nostr-inbox help Show this help
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--pubkey <hex> Your Nostr pubkey (hex)
|
|
24
|
+
--relays <urls> Comma-separated relay URLs
|
|
25
|
+
--since <timestamp> Only events after this Unix timestamp
|
|
26
|
+
--since-ago <seconds> Events from N seconds ago (default: 3600)
|
|
27
|
+
--channels <types> Comma-separated: mentions,dms,dvmRequests,dvmResults,zaps,reactions,trust,marketplace
|
|
28
|
+
--json Output raw JSON (one per line)
|
|
29
|
+
--quiet Only show urgent notifications
|
|
30
|
+
|
|
31
|
+
Environment:
|
|
32
|
+
NOSTR_PUBKEY Your pubkey (hex)
|
|
33
|
+
NOSTR_RELAYS Comma-separated relay URLs
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseArgs(args) {
|
|
38
|
+
const result = { _: [] };
|
|
39
|
+
for (let i = 0; i < args.length; i++) {
|
|
40
|
+
if (args[i].startsWith('--')) {
|
|
41
|
+
const key = args[i].slice(2);
|
|
42
|
+
const val = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true;
|
|
43
|
+
result[key] = val;
|
|
44
|
+
} else {
|
|
45
|
+
result._.push(args[i]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseChannels(str) {
|
|
52
|
+
if (!str) return {};
|
|
53
|
+
const channels = {};
|
|
54
|
+
// Start with all disabled, enable only specified
|
|
55
|
+
const allOff = {
|
|
56
|
+
mentions: false, dms: false, dvmRequests: false, dvmResults: false,
|
|
57
|
+
zaps: false, reactions: false, trust: false, marketplace: false
|
|
58
|
+
};
|
|
59
|
+
for (const ch of str.split(',')) {
|
|
60
|
+
allOff[ch.trim()] = true;
|
|
61
|
+
}
|
|
62
|
+
return allOff;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatNotification(n) {
|
|
66
|
+
const color = PRIORITY_COLORS[n.priority] || '';
|
|
67
|
+
const time = new Date(n.createdAt).toISOString().slice(11, 19);
|
|
68
|
+
const from = n.from.slice(0, 12) + '...';
|
|
69
|
+
const content = n.content ? n.content.slice(0, 120).replace(/\n/g, ' ') : '';
|
|
70
|
+
|
|
71
|
+
const typeIcons = {
|
|
72
|
+
mention: '💬',
|
|
73
|
+
dm: '✉️',
|
|
74
|
+
dvm_request: '⚙️',
|
|
75
|
+
dvm_result: '📦',
|
|
76
|
+
dvm_feedback: '📝',
|
|
77
|
+
zap: '⚡',
|
|
78
|
+
reaction: '❤️',
|
|
79
|
+
trust: '🛡️',
|
|
80
|
+
trust_network: '🌐',
|
|
81
|
+
marketplace_bid: '💰',
|
|
82
|
+
marketplace_delivery: '📋',
|
|
83
|
+
marketplace_resolution: '✅',
|
|
84
|
+
unknown: '❓'
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const icon = typeIcons[n.type] || '❓';
|
|
88
|
+
return `${color}[${time}] ${icon} ${BOLD}${n.type}${RESET}${color} from ${from}${RESET} ${content}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function main() {
|
|
92
|
+
const args = parseArgs(process.argv.slice(2));
|
|
93
|
+
const command = args._[0] || 'help';
|
|
94
|
+
|
|
95
|
+
if (command === 'help' || args.help) {
|
|
96
|
+
usage();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const pubkey = args.pubkey || process.env.NOSTR_PUBKEY;
|
|
101
|
+
if (!pubkey) {
|
|
102
|
+
console.error('Error: --pubkey or NOSTR_PUBKEY required');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const relays = (args.relays || process.env.NOSTR_RELAYS || 'wss://relay.damus.io,wss://nos.lol,wss://relay.primal.net').split(',');
|
|
107
|
+
const channels = args.channels ? parseChannels(args.channels) : {};
|
|
108
|
+
const json = args.json === true;
|
|
109
|
+
const quiet = args.quiet === true;
|
|
110
|
+
|
|
111
|
+
if (command === 'poll') {
|
|
112
|
+
const sinceAgo = args['since-ago'] ? parseInt(args['since-ago'], 10) : 3600;
|
|
113
|
+
const since = args.since ? parseInt(args.since, 10) : Math.floor(Date.now() / 1000) - sinceAgo;
|
|
114
|
+
|
|
115
|
+
const result = await poll({ pubkey, relays, channels, since });
|
|
116
|
+
|
|
117
|
+
if (json) {
|
|
118
|
+
console.log(JSON.stringify(result, null, 2));
|
|
119
|
+
} else {
|
|
120
|
+
console.log(`\n📬 ${result.total} notifications (${result.urgent} urgent) since ${new Date(since * 1000).toISOString()}\n`);
|
|
121
|
+
|
|
122
|
+
if (result.total === 0) {
|
|
123
|
+
console.log(' Nothing new.');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Group display
|
|
128
|
+
for (const [type, items] of Object.entries(result.byType)) {
|
|
129
|
+
console.log(` ${type} (${items.length}):`);
|
|
130
|
+
for (const n of items.slice(0, 10)) {
|
|
131
|
+
console.log(` ${formatNotification(n)}`);
|
|
132
|
+
}
|
|
133
|
+
if (items.length > 10) console.log(` ... and ${items.length - 10} more`);
|
|
134
|
+
console.log();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (command === 'watch') {
|
|
141
|
+
const sinceAgo = args['since-ago'] ? parseInt(args['since-ago'], 10) : 60;
|
|
142
|
+
const since = args.since ? parseInt(args.since, 10) : Math.floor(Date.now() / 1000) - sinceAgo;
|
|
143
|
+
|
|
144
|
+
const inbox = createInbox({
|
|
145
|
+
pubkey,
|
|
146
|
+
relays,
|
|
147
|
+
channels,
|
|
148
|
+
since
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
inbox.on('connected', ({ relay }) => {
|
|
152
|
+
if (!json) console.log(` ✓ Connected to ${relay}`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
inbox.on('started', ({ connected, total }) => {
|
|
156
|
+
if (!json) console.log(`\n📬 Watching ${connected}/${total} relays. Ctrl+C to stop.\n`);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
inbox.on('notification', (n) => {
|
|
160
|
+
if (quiet && n.priority !== 'high') return;
|
|
161
|
+
if (json) {
|
|
162
|
+
console.log(JSON.stringify(n));
|
|
163
|
+
} else {
|
|
164
|
+
console.log(formatNotification(n));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
inbox.on('error', ({ relay, error }) => {
|
|
169
|
+
if (!json) console.error(` ✗ ${relay}: ${error}`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await inbox.start();
|
|
173
|
+
|
|
174
|
+
// Graceful shutdown
|
|
175
|
+
process.on('SIGINT', () => {
|
|
176
|
+
inbox.stop();
|
|
177
|
+
console.log('\n👋 Stopped.');
|
|
178
|
+
process.exit(0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Keep alive
|
|
182
|
+
await new Promise(() => {});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.error(`Unknown command: ${command}`);
|
|
186
|
+
usage();
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
main().catch(err => {
|
|
191
|
+
console.error('Error:', err.message);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
});
|
package/src/filters.cjs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Nostr event kinds we care about
|
|
5
|
+
*/
|
|
6
|
+
const KINDS = {
|
|
7
|
+
// Core
|
|
8
|
+
TEXT_NOTE: 1,
|
|
9
|
+
DM_ENCRYPTED: 4, // NIP-04 encrypted DM
|
|
10
|
+
REACTION: 7,
|
|
11
|
+
|
|
12
|
+
// DVM (NIP-90)
|
|
13
|
+
DVM_REQUEST_BASE: 5000, // 5000-5999
|
|
14
|
+
DVM_RESULT_BASE: 6000, // 6000-6999
|
|
15
|
+
DVM_FEEDBACK: 7000,
|
|
16
|
+
|
|
17
|
+
// Zaps (NIP-57)
|
|
18
|
+
ZAP_RECEIPT: 9735,
|
|
19
|
+
ZAP_REQUEST: 9734,
|
|
20
|
+
|
|
21
|
+
// Trust (ai.wot)
|
|
22
|
+
LABEL: 1985, // NIP-32
|
|
23
|
+
|
|
24
|
+
// Gift wrap DM (NIP-17)
|
|
25
|
+
GIFT_WRAP: 1059,
|
|
26
|
+
SEAL: 13,
|
|
27
|
+
|
|
28
|
+
// Agent discovery
|
|
29
|
+
AGENT_SERVICE: 38990, // agent-discovery kind
|
|
30
|
+
|
|
31
|
+
// Marketplace (agent-escrow)
|
|
32
|
+
TASK: 30950,
|
|
33
|
+
BID: 950,
|
|
34
|
+
DELIVERY: 951,
|
|
35
|
+
RESOLUTION: 952,
|
|
36
|
+
|
|
37
|
+
// Clawstr (NIP-22)
|
|
38
|
+
COMMENT: 1111
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build subscription filters for a pubkey
|
|
43
|
+
*
|
|
44
|
+
* @param {string} pubkey - Hex pubkey to watch
|
|
45
|
+
* @param {Object} channels - Which notification channels to enable
|
|
46
|
+
* @param {number} [since] - Unix timestamp, only events after this
|
|
47
|
+
*/
|
|
48
|
+
function buildFilters(pubkey, channels = {}, since = null) {
|
|
49
|
+
const {
|
|
50
|
+
mentions = true,
|
|
51
|
+
dms = true,
|
|
52
|
+
dvmRequests = true,
|
|
53
|
+
dvmResults = true,
|
|
54
|
+
zaps = true,
|
|
55
|
+
reactions = true,
|
|
56
|
+
trust = true,
|
|
57
|
+
marketplace = true
|
|
58
|
+
} = channels;
|
|
59
|
+
|
|
60
|
+
const filters = [];
|
|
61
|
+
const sinceObj = since ? { since } : {};
|
|
62
|
+
|
|
63
|
+
// Mentions: kind 1 events that tag our pubkey
|
|
64
|
+
if (mentions) {
|
|
65
|
+
filters.push({
|
|
66
|
+
kinds: [KINDS.TEXT_NOTE, KINDS.COMMENT],
|
|
67
|
+
'#p': [pubkey],
|
|
68
|
+
...sinceObj
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// DMs: kind 4 events addressed to us
|
|
73
|
+
if (dms) {
|
|
74
|
+
filters.push({
|
|
75
|
+
kinds: [KINDS.DM_ENCRYPTED],
|
|
76
|
+
'#p': [pubkey],
|
|
77
|
+
...sinceObj
|
|
78
|
+
});
|
|
79
|
+
// Also gift-wrapped DMs (NIP-17)
|
|
80
|
+
filters.push({
|
|
81
|
+
kinds: [KINDS.GIFT_WRAP],
|
|
82
|
+
'#p': [pubkey],
|
|
83
|
+
...sinceObj
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// DVM requests: kind 5xxx tagged to us (we're a DVM provider)
|
|
88
|
+
if (dvmRequests) {
|
|
89
|
+
const dvmRequestKinds = [];
|
|
90
|
+
for (let k = 5000; k < 5100; k++) dvmRequestKinds.push(k);
|
|
91
|
+
filters.push({
|
|
92
|
+
kinds: dvmRequestKinds,
|
|
93
|
+
'#p': [pubkey],
|
|
94
|
+
...sinceObj
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// DVM results: kind 6xxx tagged to us (we requested something)
|
|
99
|
+
if (dvmResults) {
|
|
100
|
+
const dvmResultKinds = [];
|
|
101
|
+
for (let k = 6000; k < 6100; k++) dvmResultKinds.push(k);
|
|
102
|
+
filters.push({
|
|
103
|
+
kinds: dvmResultKinds,
|
|
104
|
+
'#p': [pubkey],
|
|
105
|
+
...sinceObj
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Zaps: receipts tagged to our pubkey
|
|
110
|
+
if (zaps) {
|
|
111
|
+
filters.push({
|
|
112
|
+
kinds: [KINDS.ZAP_RECEIPT],
|
|
113
|
+
'#p': [pubkey],
|
|
114
|
+
...sinceObj
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Reactions: to our events (tagged with our pubkey)
|
|
119
|
+
if (reactions) {
|
|
120
|
+
filters.push({
|
|
121
|
+
kinds: [KINDS.REACTION],
|
|
122
|
+
'#p': [pubkey],
|
|
123
|
+
...sinceObj
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Trust: ai.wot attestations about us
|
|
128
|
+
if (trust) {
|
|
129
|
+
filters.push({
|
|
130
|
+
kinds: [KINDS.LABEL],
|
|
131
|
+
'#p': [pubkey],
|
|
132
|
+
'#L': ['ai.wot'],
|
|
133
|
+
...sinceObj
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Marketplace: tasks tagged to us, bids on our tasks, deliveries, resolutions
|
|
138
|
+
if (marketplace) {
|
|
139
|
+
filters.push({
|
|
140
|
+
kinds: [KINDS.BID, KINDS.DELIVERY, KINDS.RESOLUTION],
|
|
141
|
+
'#p': [pubkey],
|
|
142
|
+
...sinceObj
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return filters;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Classify an event into a notification type
|
|
151
|
+
*/
|
|
152
|
+
function classifyEvent(event, myPubkey) {
|
|
153
|
+
const kind = event.kind;
|
|
154
|
+
|
|
155
|
+
// DMs
|
|
156
|
+
if (kind === KINDS.DM_ENCRYPTED || kind === KINDS.GIFT_WRAP) {
|
|
157
|
+
return { type: 'dm', priority: 'high' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// DVM requests (someone wants us to do work)
|
|
161
|
+
if (kind >= 5000 && kind < 6000) {
|
|
162
|
+
return { type: 'dvm_request', priority: 'high', dvmKind: kind };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// DVM results (response to our request)
|
|
166
|
+
if (kind >= 6000 && kind < 7000) {
|
|
167
|
+
return { type: 'dvm_result', priority: 'medium', dvmKind: kind - 1000 };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// DVM feedback
|
|
171
|
+
if (kind === KINDS.DVM_FEEDBACK) {
|
|
172
|
+
return { type: 'dvm_feedback', priority: 'low' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Zaps
|
|
176
|
+
if (kind === KINDS.ZAP_RECEIPT) {
|
|
177
|
+
return { type: 'zap', priority: 'medium' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Trust attestations
|
|
181
|
+
if (kind === KINDS.LABEL) {
|
|
182
|
+
const isAboutMe = event.tags.some(t => t[0] === 'p' && t[1] === myPubkey);
|
|
183
|
+
if (isAboutMe) {
|
|
184
|
+
return { type: 'trust', priority: 'medium' };
|
|
185
|
+
}
|
|
186
|
+
return { type: 'trust_network', priority: 'low' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Marketplace
|
|
190
|
+
if (kind === KINDS.BID) return { type: 'marketplace_bid', priority: 'high' };
|
|
191
|
+
if (kind === KINDS.DELIVERY) return { type: 'marketplace_delivery', priority: 'high' };
|
|
192
|
+
if (kind === KINDS.RESOLUTION) return { type: 'marketplace_resolution', priority: 'high' };
|
|
193
|
+
|
|
194
|
+
// Reactions
|
|
195
|
+
if (kind === KINDS.REACTION) {
|
|
196
|
+
return { type: 'reaction', priority: 'low' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Mentions (text notes / comments that tag us)
|
|
200
|
+
if (kind === KINDS.TEXT_NOTE || kind === KINDS.COMMENT) {
|
|
201
|
+
return { type: 'mention', priority: 'medium' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { type: 'unknown', priority: 'low' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = { KINDS, buildFilters, classifyEvent };
|
package/src/inbox.cjs
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
const { Relay, useWebSocketImplementation } = require('nostr-tools/relay');
|
|
5
|
+
const { buildFilters, classifyEvent } = require('./filters.cjs');
|
|
6
|
+
|
|
7
|
+
// Use ws in Node.js
|
|
8
|
+
try {
|
|
9
|
+
const WebSocket = require('ws');
|
|
10
|
+
useWebSocketImplementation(WebSocket);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
// Browser environment
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create an inbox that streams Nostr notifications
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} opts
|
|
19
|
+
* @param {string} opts.pubkey - Your hex pubkey
|
|
20
|
+
* @param {string[]} [opts.relays] - Relay URLs
|
|
21
|
+
* @param {Object} [opts.channels] - Which notification types to enable
|
|
22
|
+
* @param {number} [opts.since] - Unix timestamp, only events after this
|
|
23
|
+
* @param {boolean} [opts.dedup] - Deduplicate events by ID (default: true)
|
|
24
|
+
* @param {Function} [opts.onEvent] - Callback for each event (alternative to EventEmitter)
|
|
25
|
+
* @param {Function} [opts.onError] - Error callback
|
|
26
|
+
* @param {number} [opts.reconnectMs] - Reconnect delay on disconnect (default: 5000)
|
|
27
|
+
* @param {number} [opts.connectTimeoutMs] - Connection timeout (default: 10000)
|
|
28
|
+
*/
|
|
29
|
+
function createInbox(opts) {
|
|
30
|
+
const {
|
|
31
|
+
pubkey,
|
|
32
|
+
relays: relayUrls = ['wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net'],
|
|
33
|
+
channels = {},
|
|
34
|
+
since = null,
|
|
35
|
+
dedup = true,
|
|
36
|
+
onEvent = null,
|
|
37
|
+
onError = null,
|
|
38
|
+
reconnectMs = 5000,
|
|
39
|
+
connectTimeoutMs = 10000
|
|
40
|
+
} = opts;
|
|
41
|
+
|
|
42
|
+
if (!pubkey) throw new Error('pubkey is required');
|
|
43
|
+
|
|
44
|
+
const emitter = new EventEmitter();
|
|
45
|
+
const seen = new Set(); // Event ID dedup
|
|
46
|
+
const connectedRelays = []; // Active relay connections
|
|
47
|
+
let running = false;
|
|
48
|
+
let latestTimestamp = since || Math.floor(Date.now() / 1000) - 60; // default: last minute
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Process an incoming event
|
|
52
|
+
*/
|
|
53
|
+
function handleEvent(event) {
|
|
54
|
+
// Dedup
|
|
55
|
+
if (dedup && seen.has(event.id)) return;
|
|
56
|
+
if (dedup) {
|
|
57
|
+
seen.add(event.id);
|
|
58
|
+
// Prevent memory leak — keep last 10k IDs
|
|
59
|
+
if (seen.size > 10000) {
|
|
60
|
+
const arr = Array.from(seen);
|
|
61
|
+
for (let i = 0; i < 5000; i++) seen.delete(arr[i]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Track latest timestamp for reconnection
|
|
66
|
+
if (event.created_at > latestTimestamp) {
|
|
67
|
+
latestTimestamp = event.created_at;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Classify
|
|
71
|
+
const classification = classifyEvent(event, pubkey);
|
|
72
|
+
|
|
73
|
+
const notification = {
|
|
74
|
+
id: event.id,
|
|
75
|
+
type: classification.type,
|
|
76
|
+
priority: classification.priority,
|
|
77
|
+
from: event.pubkey,
|
|
78
|
+
content: event.content,
|
|
79
|
+
kind: event.kind,
|
|
80
|
+
tags: event.tags,
|
|
81
|
+
createdAt: event.created_at * 1000,
|
|
82
|
+
raw: event,
|
|
83
|
+
...classification
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Emit
|
|
87
|
+
emitter.emit('notification', notification);
|
|
88
|
+
emitter.emit(classification.type, notification);
|
|
89
|
+
if (classification.priority === 'high') {
|
|
90
|
+
emitter.emit('urgent', notification);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Callback
|
|
94
|
+
if (onEvent) {
|
|
95
|
+
try { onEvent(notification); } catch (e) { /* user error */ }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Connect to a single relay with auto-reconnect
|
|
101
|
+
*/
|
|
102
|
+
async function connectRelay(url) {
|
|
103
|
+
try {
|
|
104
|
+
const relay = await Promise.race([
|
|
105
|
+
Relay.connect(url),
|
|
106
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), connectTimeoutMs))
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const filters = buildFilters(pubkey, channels, latestTimestamp);
|
|
110
|
+
|
|
111
|
+
relay.subscribe(filters, {
|
|
112
|
+
onevent: handleEvent,
|
|
113
|
+
oneose() {
|
|
114
|
+
// Initial sync complete for this relay
|
|
115
|
+
emitter.emit('synced', { relay: url });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
connectedRelays.push({ url, relay });
|
|
120
|
+
emitter.emit('connected', { relay: url });
|
|
121
|
+
|
|
122
|
+
return relay;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const error = { relay: url, error: err.message };
|
|
125
|
+
emitter.emit('error', error);
|
|
126
|
+
if (onError) onError(error);
|
|
127
|
+
|
|
128
|
+
// Schedule reconnect
|
|
129
|
+
if (running) {
|
|
130
|
+
setTimeout(() => connectRelay(url), reconnectMs);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Start the inbox — connect to all relays and begin streaming
|
|
138
|
+
*/
|
|
139
|
+
async function start() {
|
|
140
|
+
if (running) return;
|
|
141
|
+
running = true;
|
|
142
|
+
|
|
143
|
+
emitter.emit('starting', { relays: relayUrls });
|
|
144
|
+
|
|
145
|
+
await Promise.allSettled(relayUrls.map(connectRelay));
|
|
146
|
+
|
|
147
|
+
emitter.emit('started', {
|
|
148
|
+
connected: connectedRelays.length,
|
|
149
|
+
total: relayUrls.length
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return emitter;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Stop the inbox — close all connections
|
|
157
|
+
*/
|
|
158
|
+
function stop() {
|
|
159
|
+
running = false;
|
|
160
|
+
for (const { relay } of connectedRelays) {
|
|
161
|
+
try { relay.close(); } catch (e) { /* ignore */ }
|
|
162
|
+
}
|
|
163
|
+
connectedRelays.length = 0;
|
|
164
|
+
emitter.emit('stopped');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get current status
|
|
169
|
+
*/
|
|
170
|
+
function status() {
|
|
171
|
+
return {
|
|
172
|
+
running,
|
|
173
|
+
relays: {
|
|
174
|
+
connected: connectedRelays.length,
|
|
175
|
+
total: relayUrls.length,
|
|
176
|
+
urls: connectedRelays.map(r => r.url)
|
|
177
|
+
},
|
|
178
|
+
seen: seen.size,
|
|
179
|
+
latestTimestamp,
|
|
180
|
+
channels
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Wait for the next event of a specific type (promise-based)
|
|
186
|
+
*/
|
|
187
|
+
function waitFor(type, timeoutMs = 30000) {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const timer = setTimeout(() => {
|
|
190
|
+
emitter.off(type, handler);
|
|
191
|
+
reject(new Error(`Timeout waiting for ${type}`));
|
|
192
|
+
}, timeoutMs);
|
|
193
|
+
|
|
194
|
+
function handler(notification) {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
resolve(notification);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
emitter.once(type, handler);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Collect events for a duration, then return them
|
|
205
|
+
*/
|
|
206
|
+
function collect(durationMs = 5000, filter = null) {
|
|
207
|
+
return new Promise((resolve) => {
|
|
208
|
+
const events = [];
|
|
209
|
+
|
|
210
|
+
function handler(notification) {
|
|
211
|
+
if (!filter || filter(notification)) {
|
|
212
|
+
events.push(notification);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
emitter.on('notification', handler);
|
|
217
|
+
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
emitter.off('notification', handler);
|
|
220
|
+
resolve(events);
|
|
221
|
+
}, durationMs);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
// Lifecycle
|
|
227
|
+
start,
|
|
228
|
+
stop,
|
|
229
|
+
status,
|
|
230
|
+
|
|
231
|
+
// EventEmitter interface
|
|
232
|
+
on: emitter.on.bind(emitter),
|
|
233
|
+
off: emitter.off.bind(emitter),
|
|
234
|
+
once: emitter.once.bind(emitter),
|
|
235
|
+
|
|
236
|
+
// Utilities
|
|
237
|
+
waitFor,
|
|
238
|
+
collect,
|
|
239
|
+
|
|
240
|
+
// Direct access
|
|
241
|
+
emitter
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = { createInbox };
|
package/src/index.cjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createInbox } = require('./inbox.cjs');
|
|
4
|
+
const { poll } = require('./poller.cjs');
|
|
5
|
+
const { KINDS, buildFilters, classifyEvent } = require('./filters.cjs');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
// Main API
|
|
9
|
+
createInbox,
|
|
10
|
+
poll,
|
|
11
|
+
|
|
12
|
+
// Utilities
|
|
13
|
+
KINDS,
|
|
14
|
+
buildFilters,
|
|
15
|
+
classifyEvent
|
|
16
|
+
};
|
package/src/poller.cjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Relay, useWebSocketImplementation } = require('nostr-tools/relay');
|
|
4
|
+
const { buildFilters, classifyEvent } = require('./filters.cjs');
|
|
5
|
+
|
|
6
|
+
// Use ws in Node.js
|
|
7
|
+
try {
|
|
8
|
+
const WebSocket = require('ws');
|
|
9
|
+
useWebSocketImplementation(WebSocket);
|
|
10
|
+
} catch (e) {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One-shot poll: connect, fetch events since timestamp, disconnect
|
|
14
|
+
*
|
|
15
|
+
* Useful for agents that check periodically rather than streaming.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} opts
|
|
18
|
+
* @param {string} opts.pubkey - Your hex pubkey
|
|
19
|
+
* @param {string[]} [opts.relays] - Relay URLs
|
|
20
|
+
* @param {Object} [opts.channels] - Which notification types to check
|
|
21
|
+
* @param {number} [opts.since] - Unix timestamp
|
|
22
|
+
* @param {number} [opts.timeoutMs] - Query timeout
|
|
23
|
+
*/
|
|
24
|
+
async function poll(opts) {
|
|
25
|
+
const {
|
|
26
|
+
pubkey,
|
|
27
|
+
relays: relayUrls = ['wss://relay.damus.io', 'wss://nos.lol'],
|
|
28
|
+
channels = {},
|
|
29
|
+
since = Math.floor(Date.now() / 1000) - 3600, // default: last hour
|
|
30
|
+
timeoutMs = 10000
|
|
31
|
+
} = opts;
|
|
32
|
+
|
|
33
|
+
if (!pubkey) throw new Error('pubkey is required');
|
|
34
|
+
|
|
35
|
+
const events = new Map();
|
|
36
|
+
const filters = buildFilters(pubkey, channels, since);
|
|
37
|
+
|
|
38
|
+
// Connect to relays and collect events
|
|
39
|
+
const relayPromises = relayUrls.map(async (url) => {
|
|
40
|
+
try {
|
|
41
|
+
const relay = await Promise.race([
|
|
42
|
+
Relay.connect(url),
|
|
43
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), timeoutMs))
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
await new Promise((resolve) => {
|
|
47
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
48
|
+
relay.subscribe(filters, {
|
|
49
|
+
onevent(event) {
|
|
50
|
+
events.set(event.id, event);
|
|
51
|
+
},
|
|
52
|
+
oneose() {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
resolve();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
try { relay.close(); } catch (e) {}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// Relay unavailable — skip
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await Promise.allSettled(relayPromises);
|
|
66
|
+
|
|
67
|
+
// Classify and sort
|
|
68
|
+
const notifications = Array.from(events.values())
|
|
69
|
+
.map(event => {
|
|
70
|
+
const classification = classifyEvent(event, pubkey);
|
|
71
|
+
return {
|
|
72
|
+
id: event.id,
|
|
73
|
+
type: classification.type,
|
|
74
|
+
priority: classification.priority,
|
|
75
|
+
from: event.pubkey,
|
|
76
|
+
content: event.content,
|
|
77
|
+
kind: event.kind,
|
|
78
|
+
tags: event.tags,
|
|
79
|
+
createdAt: event.created_at * 1000,
|
|
80
|
+
raw: event,
|
|
81
|
+
...classification
|
|
82
|
+
};
|
|
83
|
+
})
|
|
84
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
85
|
+
|
|
86
|
+
// Group by type
|
|
87
|
+
const byType = {};
|
|
88
|
+
for (const n of notifications) {
|
|
89
|
+
if (!byType[n.type]) byType[n.type] = [];
|
|
90
|
+
byType[n.type].push(n);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Summary
|
|
94
|
+
const urgent = notifications.filter(n => n.priority === 'high');
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
total: notifications.length,
|
|
98
|
+
urgent: urgent.length,
|
|
99
|
+
notifications,
|
|
100
|
+
byType,
|
|
101
|
+
since,
|
|
102
|
+
queriedAt: Date.now()
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { poll };
|