nostr-bray 1.2.0 → 1.4.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/dist/cli.js +42 -2
- package/dist/social/handlers.d.ts +17 -0
- package/dist/social/handlers.js +72 -0
- package/dist/social/tools.js +48 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { loadConfig } from './config.js';
|
|
|
3
3
|
import { IdentityContext } from './context.js';
|
|
4
4
|
import { RelayPool } from './relay-pool.js';
|
|
5
5
|
import { Nip65Manager } from './nip65.js';
|
|
6
|
-
import { handleSocialPost, handleSocialReply, handleSocialReact, handleSocialProfileGet, handleSocialProfileSet } from './social/handlers.js';
|
|
6
|
+
import { handleSocialPost, handleSocialReply, handleSocialReact, handleSocialProfileGet, handleSocialProfileSet, handleContactsGet, handleContactsFollow, handleContactsUnfollow } from './social/handlers.js';
|
|
7
7
|
import { handleDmSend, handleDmRead } from './social/dm.js';
|
|
8
8
|
import { handleNotifications, handleFeed } from './social/notifications.js';
|
|
9
9
|
import { handleIdentityList, handleIdentityProve, handleIdentityCreate } from './identity/handlers.js';
|
|
@@ -47,6 +47,9 @@ Social:
|
|
|
47
47
|
react <event-id> <pubkey> [emoji] React to an event
|
|
48
48
|
profile <pubkey-hex> Fetch a profile
|
|
49
49
|
profile-set <json> Set profile (add --confirm to overwrite)
|
|
50
|
+
contacts <pubkey-hex> List who a pubkey follows
|
|
51
|
+
follow <pubkey-hex> [relay] [name] Follow a pubkey
|
|
52
|
+
unfollow <pubkey-hex> Unfollow a pubkey
|
|
50
53
|
dm <pubkey-hex> "message" Send NIP-17 encrypted DM
|
|
51
54
|
dm-read Read received DMs
|
|
52
55
|
feed [--limit N] Fetch text note feed
|
|
@@ -248,6 +251,21 @@ async function run(cmdArgs) {
|
|
|
248
251
|
out(await handleSocialProfileSet(ctx, pool, { profile, confirm: hasFlag('confirm') }));
|
|
249
252
|
break;
|
|
250
253
|
}
|
|
254
|
+
case 'contacts':
|
|
255
|
+
out(await handleContactsGet(pool, ctx.activeNpub, req(1, 'contacts <pubkey-hex>')));
|
|
256
|
+
break;
|
|
257
|
+
case 'follow':
|
|
258
|
+
out(await handleContactsFollow(ctx, pool, {
|
|
259
|
+
pubkeyHex: req(1, 'follow <pubkey-hex> [relay] [petname]'),
|
|
260
|
+
relay: cmdArgs[2],
|
|
261
|
+
petname: cmdArgs[3],
|
|
262
|
+
}));
|
|
263
|
+
break;
|
|
264
|
+
case 'unfollow':
|
|
265
|
+
out(await handleContactsUnfollow(ctx, pool, {
|
|
266
|
+
pubkeyHex: req(1, 'unfollow <pubkey-hex>'),
|
|
267
|
+
}));
|
|
268
|
+
break;
|
|
251
269
|
case 'dm':
|
|
252
270
|
out(await handleDmSend(ctx, pool, {
|
|
253
271
|
recipientPubkeyHex: req(1, 'dm <pubkey-hex> "message"'),
|
|
@@ -387,9 +405,31 @@ async function run(cmdArgs) {
|
|
|
387
405
|
}
|
|
388
406
|
}
|
|
389
407
|
// === Shell mode ===
|
|
408
|
+
const ALL_COMMANDS = [
|
|
409
|
+
'whoami', 'create', 'list', 'derive', 'persona', 'switch', 'prove', 'proof-publish',
|
|
410
|
+
'backup', 'restore', 'identity-backup', 'identity-restore', 'migrate',
|
|
411
|
+
'post', 'reply', 'react', 'profile', 'profile-set', 'contacts', 'follow', 'unfollow', 'dm', 'dm-read', 'feed', 'notifications',
|
|
412
|
+
'attest', 'trust-read', 'trust-verify', 'trust-revoke', 'trust-request', 'trust-request-list',
|
|
413
|
+
'ring-prove', 'ring-verify', 'spoken-challenge', 'spoken-verify',
|
|
414
|
+
'relay-list', 'relay-set', 'relay-add', 'relay-info',
|
|
415
|
+
'zap-send', 'zap-balance', 'zap-invoice', 'zap-lookup', 'zap-transactions', 'zap-receipts', 'zap-decode',
|
|
416
|
+
'safety-configure', 'safety-activate',
|
|
417
|
+
'help', 'exit',
|
|
418
|
+
];
|
|
390
419
|
async function shell() {
|
|
391
420
|
const { createInterface } = await import('node:readline');
|
|
392
|
-
const rl = createInterface({
|
|
421
|
+
const rl = createInterface({
|
|
422
|
+
input: process.stdin,
|
|
423
|
+
output: process.stdout,
|
|
424
|
+
completer: (line) => {
|
|
425
|
+
const parts = line.split(/\s+/);
|
|
426
|
+
if (parts.length <= 1) {
|
|
427
|
+
const hits = ALL_COMMANDS.filter(c => c.startsWith(line));
|
|
428
|
+
return [hits.length ? hits : ALL_COMMANDS, line];
|
|
429
|
+
}
|
|
430
|
+
return [[], line];
|
|
431
|
+
},
|
|
432
|
+
});
|
|
393
433
|
console.log(`nostr-bray shell — ${ctx.activeNpub}`);
|
|
394
434
|
console.log('Type a command, or "help" / "exit".\n');
|
|
395
435
|
const prompt = () => new Promise(resolve => {
|
|
@@ -40,3 +40,20 @@ export declare function handleSocialProfileSet(ctx: IdentityContext, pool: Relay
|
|
|
40
40
|
profile: Record<string, unknown>;
|
|
41
41
|
confirm?: boolean;
|
|
42
42
|
}): Promise<ProfileSetResult>;
|
|
43
|
+
export interface Contact {
|
|
44
|
+
pubkey: string;
|
|
45
|
+
relay?: string;
|
|
46
|
+
petname?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Fetch the kind 3 contact list for a pubkey */
|
|
49
|
+
export declare function handleContactsGet(pool: RelayPool, npub: string, pubkeyHex: string): Promise<Contact[]>;
|
|
50
|
+
/** Follow a pubkey — fetches current contacts, adds, publishes new kind 3 */
|
|
51
|
+
export declare function handleContactsFollow(ctx: IdentityContext, pool: RelayPool, args: {
|
|
52
|
+
pubkeyHex: string;
|
|
53
|
+
relay?: string;
|
|
54
|
+
petname?: string;
|
|
55
|
+
}): Promise<PostResult>;
|
|
56
|
+
/** Unfollow a pubkey — fetches current contacts, removes, publishes new kind 3 */
|
|
57
|
+
export declare function handleContactsUnfollow(ctx: IdentityContext, pool: RelayPool, args: {
|
|
58
|
+
pubkeyHex: string;
|
|
59
|
+
}): Promise<PostResult>;
|
package/dist/social/handlers.js
CHANGED
|
@@ -105,3 +105,75 @@ export async function handleSocialProfileSet(ctx, pool, args) {
|
|
|
105
105
|
await pool.publish(ctx.activeNpub, event);
|
|
106
106
|
return { published: true, event };
|
|
107
107
|
}
|
|
108
|
+
/** Fetch the kind 3 contact list for a pubkey */
|
|
109
|
+
export async function handleContactsGet(pool, npub, pubkeyHex) {
|
|
110
|
+
const events = await pool.query(npub, {
|
|
111
|
+
kinds: [3],
|
|
112
|
+
authors: [pubkeyHex],
|
|
113
|
+
});
|
|
114
|
+
if (events.length === 0)
|
|
115
|
+
return [];
|
|
116
|
+
const best = events.reduce((a, b) => b.created_at > a.created_at ? b : a);
|
|
117
|
+
return best.tags
|
|
118
|
+
.filter(t => t[0] === 'p' && t[1])
|
|
119
|
+
.map(t => ({
|
|
120
|
+
pubkey: t[1],
|
|
121
|
+
relay: t[2] || undefined,
|
|
122
|
+
petname: t[3] || undefined,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
/** Follow a pubkey — fetches current contacts, adds, publishes new kind 3 */
|
|
126
|
+
export async function handleContactsFollow(ctx, pool, args) {
|
|
127
|
+
// Fetch existing contacts
|
|
128
|
+
const existing = await pool.query(ctx.activeNpub, {
|
|
129
|
+
kinds: [3],
|
|
130
|
+
authors: [ctx.activeNpub],
|
|
131
|
+
});
|
|
132
|
+
let tags = [];
|
|
133
|
+
if (existing.length > 0) {
|
|
134
|
+
const best = existing.reduce((a, b) => b.created_at > a.created_at ? b : a);
|
|
135
|
+
tags = best.tags.filter(t => t[0] === 'p');
|
|
136
|
+
}
|
|
137
|
+
// Don't duplicate
|
|
138
|
+
if (!tags.some(t => t[1] === args.pubkeyHex)) {
|
|
139
|
+
const newTag = ['p', args.pubkeyHex];
|
|
140
|
+
if (args.relay)
|
|
141
|
+
newTag.push(args.relay);
|
|
142
|
+
if (args.petname) {
|
|
143
|
+
if (!args.relay)
|
|
144
|
+
newTag.push('');
|
|
145
|
+
newTag.push(args.petname);
|
|
146
|
+
}
|
|
147
|
+
tags.push(newTag);
|
|
148
|
+
}
|
|
149
|
+
const sign = ctx.getSigningFunction();
|
|
150
|
+
const event = await sign({
|
|
151
|
+
kind: 3,
|
|
152
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
153
|
+
tags,
|
|
154
|
+
content: '',
|
|
155
|
+
});
|
|
156
|
+
const publish = await pool.publish(ctx.activeNpub, event);
|
|
157
|
+
return { event, publish };
|
|
158
|
+
}
|
|
159
|
+
/** Unfollow a pubkey — fetches current contacts, removes, publishes new kind 3 */
|
|
160
|
+
export async function handleContactsUnfollow(ctx, pool, args) {
|
|
161
|
+
const existing = await pool.query(ctx.activeNpub, {
|
|
162
|
+
kinds: [3],
|
|
163
|
+
authors: [ctx.activeNpub],
|
|
164
|
+
});
|
|
165
|
+
let tags = [];
|
|
166
|
+
if (existing.length > 0) {
|
|
167
|
+
const best = existing.reduce((a, b) => b.created_at > a.created_at ? b : a);
|
|
168
|
+
tags = best.tags.filter(t => t[0] === 'p' && t[1] !== args.pubkeyHex);
|
|
169
|
+
}
|
|
170
|
+
const sign = ctx.getSigningFunction();
|
|
171
|
+
const event = await sign({
|
|
172
|
+
kind: 3,
|
|
173
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
174
|
+
tags,
|
|
175
|
+
content: '',
|
|
176
|
+
});
|
|
177
|
+
const publish = await pool.publish(ctx.activeNpub, event);
|
|
178
|
+
return { event, publish };
|
|
179
|
+
}
|
package/dist/social/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { hexId } from '../validation.js';
|
|
3
|
-
import { handleSocialPost, handleSocialReply, handleSocialReact, handleSocialProfileGet, handleSocialProfileSet, } from './handlers.js';
|
|
3
|
+
import { handleSocialPost, handleSocialReply, handleSocialReact, handleSocialProfileGet, handleSocialProfileSet, handleContactsGet, handleContactsFollow, handleContactsUnfollow, } from './handlers.js';
|
|
4
4
|
import { handleDmSend, handleDmRead } from './dm.js';
|
|
5
5
|
import { handleNotifications, handleFeed } from './notifications.js';
|
|
6
6
|
export function registerSocialTools(server, deps) {
|
|
@@ -165,4 +165,51 @@ export function registerSocialTools(server, deps) {
|
|
|
165
165
|
content: [{ type: 'text', text: JSON.stringify(feed, null, 2) }],
|
|
166
166
|
};
|
|
167
167
|
});
|
|
168
|
+
server.registerTool('contacts_get', {
|
|
169
|
+
description: 'Fetch the contact list (kind 3 follows) for a pubkey. Returns pubkeys, relay hints, and petnames.',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
pubkeyHex: hexId.describe('Hex pubkey to fetch contacts for'),
|
|
172
|
+
npub: z.string().optional().describe('Bech32 npub for relay routing (defaults to active identity)'),
|
|
173
|
+
},
|
|
174
|
+
annotations: { readOnlyHint: true },
|
|
175
|
+
}, async ({ pubkeyHex, npub }) => {
|
|
176
|
+
const contacts = await handleContactsGet(deps.pool, npub ?? deps.ctx.activeNpub, pubkeyHex);
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: 'text', text: JSON.stringify(contacts, null, 2) }],
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
server.registerTool('contacts_follow', {
|
|
182
|
+
description: 'Follow a Nostr pubkey. Fetches current contact list, adds the pubkey, publishes updated kind 3.',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
pubkeyHex: hexId.describe('Hex pubkey to follow'),
|
|
185
|
+
relay: z.string().optional().describe('Relay hint for the contact'),
|
|
186
|
+
petname: z.string().optional().describe('Local petname for the contact'),
|
|
187
|
+
},
|
|
188
|
+
annotations: { readOnlyHint: false },
|
|
189
|
+
}, async ({ pubkeyHex, relay, petname }) => {
|
|
190
|
+
const result = await handleContactsFollow(deps.ctx, deps.pool, { pubkeyHex, relay, petname });
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
193
|
+
id: result.event.id,
|
|
194
|
+
followCount: result.event.tags.filter(t => t[0] === 'p').length,
|
|
195
|
+
publish: result.publish,
|
|
196
|
+
}, null, 2) }],
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
server.registerTool('contacts_unfollow', {
|
|
200
|
+
description: 'Unfollow a Nostr pubkey. Fetches current contact list, removes the pubkey, publishes updated kind 3.',
|
|
201
|
+
inputSchema: {
|
|
202
|
+
pubkeyHex: hexId.describe('Hex pubkey to unfollow'),
|
|
203
|
+
},
|
|
204
|
+
annotations: { readOnlyHint: false },
|
|
205
|
+
}, async ({ pubkeyHex }) => {
|
|
206
|
+
const result = await handleContactsUnfollow(deps.ctx, deps.pool, { pubkeyHex });
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
209
|
+
id: result.event.id,
|
|
210
|
+
followCount: result.event.tags.filter(t => t[0] === 'p').length,
|
|
211
|
+
publish: result.publish,
|
|
212
|
+
}, null, 2) }],
|
|
213
|
+
};
|
|
214
|
+
});
|
|
168
215
|
}
|
package/package.json
CHANGED