@elizaos/plugin-nostr 2.0.3-beta.5 → 2.0.3-beta.7
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/accounts.d.ts +15 -0
- package/dist/accounts.d.ts.map +1 -0
- package/dist/accounts.js +139 -0
- package/dist/accounts.js.map +1 -0
- package/dist/connector-account-provider.d.ts +20 -0
- package/dist/connector-account-provider.d.ts.map +1 -0
- package/dist/connector-account-provider.js +78 -0
- package/dist/connector-account-provider.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/identityContext.d.ts +6 -0
- package/dist/providers/identityContext.d.ts.map +1 -0
- package/dist/providers/identityContext.js +69 -0
- package/dist/providers/identityContext.js.map +1 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +5 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/service.d.ts +116 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +980 -0
- package/dist/service.js.map +1 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +192 -0
- package/dist/types.js.map +1 -0
- package/package.json +3 -3
package/dist/service.js
ADDED
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nostr service implementation for ElizaOS.
|
|
3
|
+
*/
|
|
4
|
+
import { ChannelType, createUniqueUuid, logger, Service, } from "@elizaos/core";
|
|
5
|
+
import { finalizeEvent, getPublicKey, SimplePool, verifyEvent, } from "nostr-tools";
|
|
6
|
+
import { decrypt, encrypt } from "nostr-tools/nip04";
|
|
7
|
+
import { listNostrAccountIds, normalizeNostrAccountId, readNostrAccountId, resolveDefaultNostrAccountId, resolveNostrAccountSettings, } from "./accounts.js";
|
|
8
|
+
import { NOSTR_SERVICE_NAME, NostrConfigurationError, NostrEventTypes, normalizePubkey, pubkeyToNpub, splitMessageForNostr, validatePrivateKey, } from "./types.js";
|
|
9
|
+
const NOSTR_CONNECTOR_CONTEXTS = ["social", "connectors"];
|
|
10
|
+
const NOSTR_CONNECTOR_CAPABILITIES = [
|
|
11
|
+
"send_message",
|
|
12
|
+
"fetch_messages",
|
|
13
|
+
"resolve_targets",
|
|
14
|
+
"user_context",
|
|
15
|
+
];
|
|
16
|
+
function getNostrTargetMetadata(target) {
|
|
17
|
+
const metadata = target.metadata;
|
|
18
|
+
return metadata && typeof metadata === "object"
|
|
19
|
+
? metadata
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Append agent-generated attachment URLs to Nostr text (#8876). Nostr has no
|
|
24
|
+
* separate attachment field — kind:1 notes and DMs carry media as URLs in the
|
|
25
|
+
* content, which clients render inline. So a generated/sent attachment becomes a
|
|
26
|
+
* URL appended to the text rather than being dropped. http(s) URLs only.
|
|
27
|
+
*/
|
|
28
|
+
function appendNostrAttachmentUrls(text, content) {
|
|
29
|
+
const urls = Array.isArray(content.attachments)
|
|
30
|
+
? content.attachments
|
|
31
|
+
.map((media) => (typeof media?.url === "string" ? media.url.trim() : ""))
|
|
32
|
+
.filter((url) => /^https?:\/\//i.test(url))
|
|
33
|
+
: [];
|
|
34
|
+
if (urls.length === 0)
|
|
35
|
+
return text;
|
|
36
|
+
return [text, ...urls].filter(Boolean).join("\n");
|
|
37
|
+
}
|
|
38
|
+
function clampLimit(value, defaultValue, max) {
|
|
39
|
+
if (!Number.isFinite(value)) {
|
|
40
|
+
return defaultValue;
|
|
41
|
+
}
|
|
42
|
+
return Math.min(Math.max(1, Math.floor(value)), max);
|
|
43
|
+
}
|
|
44
|
+
function isSafeRelayUrl(relay) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = new URL(relay);
|
|
47
|
+
return ((parsed.protocol === "wss:" || parsed.protocol === "ws:") &&
|
|
48
|
+
!parsed.username &&
|
|
49
|
+
!parsed.password);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function isEventShape(value) {
|
|
56
|
+
if (!value || typeof value !== "object")
|
|
57
|
+
return false;
|
|
58
|
+
const event = value;
|
|
59
|
+
return (typeof event.id === "string" &&
|
|
60
|
+
typeof event.pubkey === "string" &&
|
|
61
|
+
typeof event.content === "string" &&
|
|
62
|
+
typeof event.kind === "number" &&
|
|
63
|
+
Number.isFinite(event.kind) &&
|
|
64
|
+
typeof event.created_at === "number" &&
|
|
65
|
+
Number.isFinite(event.created_at) &&
|
|
66
|
+
Array.isArray(event.tags) &&
|
|
67
|
+
typeof event.sig === "string");
|
|
68
|
+
}
|
|
69
|
+
function normalizeEventTags(tags) {
|
|
70
|
+
if (!Array.isArray(tags))
|
|
71
|
+
return [];
|
|
72
|
+
return tags
|
|
73
|
+
.filter(Array.isArray)
|
|
74
|
+
.map((tag) => tag.filter((value) => typeof value === "string"))
|
|
75
|
+
.filter((tag) => tag.length > 0);
|
|
76
|
+
}
|
|
77
|
+
function createdAtMs(event) {
|
|
78
|
+
return Number.isFinite(event.created_at) && event.created_at > 0
|
|
79
|
+
? event.created_at * 1000
|
|
80
|
+
: Date.now();
|
|
81
|
+
}
|
|
82
|
+
export class NostrService extends Service {
|
|
83
|
+
static serviceType = NOSTR_SERVICE_NAME;
|
|
84
|
+
capabilityDescription = "Provides Nostr protocol integration for encrypted direct messages";
|
|
85
|
+
settings = null;
|
|
86
|
+
pool = null;
|
|
87
|
+
privateKey = null;
|
|
88
|
+
connected = false;
|
|
89
|
+
seenEventIds = new Set();
|
|
90
|
+
accountServices = new Map();
|
|
91
|
+
/**
|
|
92
|
+
* Start the Nostr service.
|
|
93
|
+
*/
|
|
94
|
+
static async start(runtime) {
|
|
95
|
+
logger.info("Starting Nostr service...");
|
|
96
|
+
const service = new NostrService(runtime);
|
|
97
|
+
await service.initialize();
|
|
98
|
+
return service;
|
|
99
|
+
}
|
|
100
|
+
static registerSendHandlers(runtime, serviceInstance) {
|
|
101
|
+
if (!serviceInstance) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const accountService of serviceInstance.getAccountServiceList()) {
|
|
105
|
+
const accountId = accountService.getAccountId(runtime);
|
|
106
|
+
accountService.registerPostConnector(runtime);
|
|
107
|
+
const sendHandler = accountService.handleSendMessage.bind(accountService);
|
|
108
|
+
if (typeof runtime.registerMessageConnector === "function") {
|
|
109
|
+
const registration = {
|
|
110
|
+
source: "nostr",
|
|
111
|
+
accountId,
|
|
112
|
+
label: "Nostr",
|
|
113
|
+
description: "Nostr encrypted DM connector using NIP-04.",
|
|
114
|
+
capabilities: [...NOSTR_CONNECTOR_CAPABILITIES],
|
|
115
|
+
supportedTargetKinds: ["user", "contact"],
|
|
116
|
+
contexts: [...NOSTR_CONNECTOR_CONTEXTS],
|
|
117
|
+
metadata: {
|
|
118
|
+
accountId,
|
|
119
|
+
service: NOSTR_SERVICE_NAME,
|
|
120
|
+
},
|
|
121
|
+
resolveTargets: accountService.resolveConnectorTargets.bind(accountService),
|
|
122
|
+
listRecentTargets: accountService.listRecentConnectorTargets.bind(accountService),
|
|
123
|
+
getUserContext: accountService.getConnectorUserContext.bind(accountService),
|
|
124
|
+
fetchMessages: accountService.fetchConnectorMessages.bind(accountService),
|
|
125
|
+
contentShaping: {
|
|
126
|
+
systemPromptFragment: "For Nostr encrypted DMs, keep messages concise. Long messages may be split by the connector for relay delivery.",
|
|
127
|
+
constraints: {
|
|
128
|
+
supportsMarkdown: false,
|
|
129
|
+
channelType: ChannelType.DM,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
sendHandler,
|
|
133
|
+
};
|
|
134
|
+
runtime.registerMessageConnector(registration);
|
|
135
|
+
runtime.logger.info({ src: "plugin:nostr", agentId: runtime.agentId }, "Registered Nostr DM connector");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
registerPostConnector(runtime) {
|
|
140
|
+
const withPostConnector = runtime;
|
|
141
|
+
if (typeof withPostConnector.registerPostConnector !== "function") {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const accountId = this.getAccountId(runtime);
|
|
145
|
+
withPostConnector.registerPostConnector({
|
|
146
|
+
source: "nostr",
|
|
147
|
+
accountId,
|
|
148
|
+
label: "Nostr",
|
|
149
|
+
description: "Nostr public note connector for publishing kind:1 notes, reading relay feeds, and NIP-50 relay search where supported.",
|
|
150
|
+
capabilities: ["post", "fetch_feed", "search_posts"],
|
|
151
|
+
contexts: ["social", "social_posting", "connectors"],
|
|
152
|
+
metadata: {
|
|
153
|
+
accountId,
|
|
154
|
+
service: NOSTR_SERVICE_NAME,
|
|
155
|
+
},
|
|
156
|
+
postHandler: this.handleSendPost.bind(this),
|
|
157
|
+
fetchFeed: this.fetchConnectorFeed.bind(this),
|
|
158
|
+
searchPosts: this.searchConnectorPosts.bind(this),
|
|
159
|
+
contentShaping: {
|
|
160
|
+
systemPromptFragment: "For Nostr notes, write plain public text. Hashtags and nostr: references are acceptable when useful; avoid Markdown-specific formatting.",
|
|
161
|
+
constraints: {
|
|
162
|
+
supportsMarkdown: false,
|
|
163
|
+
channelType: ChannelType.FEED,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
runtime.logger.info({ src: "plugin:nostr", agentId: runtime.agentId }, "Registered Nostr post connector");
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Initialize the service.
|
|
171
|
+
*/
|
|
172
|
+
async initialize() {
|
|
173
|
+
const startedAccounts = [];
|
|
174
|
+
for (const accountId of listNostrAccountIds(this.runtime)) {
|
|
175
|
+
const settings = resolveNostrAccountSettings(this.runtime, accountId);
|
|
176
|
+
if (settings.enabled === false) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const accountService = new NostrService(this.runtime);
|
|
180
|
+
await accountService.initializeAccount(accountId);
|
|
181
|
+
this.accountServices.set(accountService.getAccountId(), accountService);
|
|
182
|
+
startedAccounts.push(accountService.getAccountId());
|
|
183
|
+
}
|
|
184
|
+
if (startedAccounts.length === 0) {
|
|
185
|
+
logger.warn("No enabled Nostr accounts configured");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
logger.info(`Nostr service started ${startedAccounts.length} account(s): ${startedAccounts.join(", ")}`);
|
|
189
|
+
}
|
|
190
|
+
async initializeAccount(accountId) {
|
|
191
|
+
this.settings = this.loadSettings(accountId);
|
|
192
|
+
this.validateSettings();
|
|
193
|
+
// Initialize private key
|
|
194
|
+
this.privateKey = validatePrivateKey(this.settings.privateKey);
|
|
195
|
+
// Initialize SimplePool
|
|
196
|
+
this.pool = new SimplePool();
|
|
197
|
+
// Start subscription
|
|
198
|
+
await this.startSubscription();
|
|
199
|
+
this.connected = true;
|
|
200
|
+
logger.info(`Nostr service started (pubkey: ${this.settings.publicKey.slice(0, 16)}...)`);
|
|
201
|
+
this.runtime.emitEvent(NostrEventTypes.CONNECTION_READY, {
|
|
202
|
+
runtime: this.runtime,
|
|
203
|
+
service: this,
|
|
204
|
+
accountId: this.getAccountId(),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Stop the Nostr service.
|
|
209
|
+
*/
|
|
210
|
+
async stop() {
|
|
211
|
+
logger.info("Stopping Nostr service...");
|
|
212
|
+
if (this.accountServices.size > 0) {
|
|
213
|
+
await Promise.all(Array.from(this.accountServices.values()).map((service) => service.stop()));
|
|
214
|
+
this.accountServices.clear();
|
|
215
|
+
logger.info("Nostr service stopped");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.connected = false;
|
|
219
|
+
if (this.pool) {
|
|
220
|
+
this.pool.close(this.settings?.relays || []);
|
|
221
|
+
this.pool = null;
|
|
222
|
+
}
|
|
223
|
+
this.privateKey = null;
|
|
224
|
+
this.seenEventIds.clear();
|
|
225
|
+
logger.info("Nostr service stopped");
|
|
226
|
+
}
|
|
227
|
+
getAccountServiceList() {
|
|
228
|
+
return this.accountServices.size > 0 ? Array.from(this.accountServices.values()) : [this];
|
|
229
|
+
}
|
|
230
|
+
getDefaultAccountService() {
|
|
231
|
+
if (!this.accountServices || this.accountServices.size === 0) {
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
const defaultAccountId = normalizeNostrAccountId(resolveDefaultNostrAccountId(this.runtime));
|
|
235
|
+
return (this.accountServices.get(defaultAccountId) ?? Array.from(this.accountServices.values())[0]);
|
|
236
|
+
}
|
|
237
|
+
getAccountService(accountId) {
|
|
238
|
+
if (!this.accountServices || this.accountServices.size === 0) {
|
|
239
|
+
const ownAccountId = this.getAccountId();
|
|
240
|
+
if (normalizeNostrAccountId(accountId) !== ownAccountId) {
|
|
241
|
+
throw new Error(`Nostr account '${accountId}' is not available in this service instance`);
|
|
242
|
+
}
|
|
243
|
+
return this;
|
|
244
|
+
}
|
|
245
|
+
const normalized = normalizeNostrAccountId(accountId);
|
|
246
|
+
const service = this.accountServices.get(normalized);
|
|
247
|
+
if (!service) {
|
|
248
|
+
throw new Error(`Nostr account '${normalized}' is not available`);
|
|
249
|
+
}
|
|
250
|
+
return service;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Load settings from runtime configuration.
|
|
254
|
+
*/
|
|
255
|
+
loadSettings(accountId) {
|
|
256
|
+
const runtime = this.runtime;
|
|
257
|
+
if (!runtime) {
|
|
258
|
+
throw new NostrConfigurationError("Runtime not initialized");
|
|
259
|
+
}
|
|
260
|
+
const resolved = resolveNostrAccountSettings(runtime, accountId);
|
|
261
|
+
const allowFrom = resolved.allowFrom.map((p) => {
|
|
262
|
+
try {
|
|
263
|
+
return normalizePubkey(p.trim());
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return p.trim();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
// Derive public key
|
|
270
|
+
let publicKey = "";
|
|
271
|
+
if (resolved.privateKey) {
|
|
272
|
+
try {
|
|
273
|
+
const sk = validatePrivateKey(resolved.privateKey);
|
|
274
|
+
publicKey = getPublicKey(sk);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Will be caught in validation
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
...resolved,
|
|
282
|
+
publicKey,
|
|
283
|
+
allowFrom,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Validate the settings.
|
|
288
|
+
*/
|
|
289
|
+
validateSettings() {
|
|
290
|
+
const settings = this.settings;
|
|
291
|
+
if (!settings) {
|
|
292
|
+
throw new NostrConfigurationError("Settings not loaded");
|
|
293
|
+
}
|
|
294
|
+
if (!settings.privateKey) {
|
|
295
|
+
throw new NostrConfigurationError("NOSTR_PRIVATE_KEY is required", "NOSTR_PRIVATE_KEY");
|
|
296
|
+
}
|
|
297
|
+
if (!settings.publicKey) {
|
|
298
|
+
throw new NostrConfigurationError("Invalid private key - could not derive public key", "NOSTR_PRIVATE_KEY");
|
|
299
|
+
}
|
|
300
|
+
if (settings.relays.length === 0) {
|
|
301
|
+
throw new NostrConfigurationError("At least one relay is required", "NOSTR_RELAYS");
|
|
302
|
+
}
|
|
303
|
+
// Validate relay URLs
|
|
304
|
+
for (const relay of settings.relays) {
|
|
305
|
+
if (!isSafeRelayUrl(relay)) {
|
|
306
|
+
throw new NostrConfigurationError(`Invalid relay URL: ${relay}`, "NOSTR_RELAYS");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Start the DM subscription.
|
|
312
|
+
*/
|
|
313
|
+
async startSubscription() {
|
|
314
|
+
const settings = this.settings;
|
|
315
|
+
const pool = this.pool;
|
|
316
|
+
const privateKey = this.privateKey;
|
|
317
|
+
if (!settings || !pool || !privateKey) {
|
|
318
|
+
throw new NostrConfigurationError("Service not properly initialized");
|
|
319
|
+
}
|
|
320
|
+
const pk = settings.publicKey;
|
|
321
|
+
const since = Math.floor(Date.now() / 1000) - 120; // Last 2 minutes
|
|
322
|
+
// Subscribe to DMs (kind:4)
|
|
323
|
+
const filter = { kinds: [4], "#p": [pk], since };
|
|
324
|
+
pool.subscribeMany(settings.relays, filter, {
|
|
325
|
+
onevent: async (event) => {
|
|
326
|
+
await this.handleEvent(event);
|
|
327
|
+
},
|
|
328
|
+
oneose: () => {
|
|
329
|
+
logger.debug("Nostr EOSE received - initial sync complete");
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
logger.info(`Subscribed to ${settings.relays.length} relay(s)`);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Handle an incoming event.
|
|
336
|
+
*/
|
|
337
|
+
async handleEvent(event) {
|
|
338
|
+
if (!isEventShape(event)) {
|
|
339
|
+
logger.warn("Ignoring malformed Nostr event payload");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const settings = this.settings;
|
|
343
|
+
const privateKey = this.privateKey;
|
|
344
|
+
if (!settings || !privateKey) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
// Dedupe
|
|
348
|
+
if (this.seenEventIds.has(event.id)) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.seenEventIds.add(event.id);
|
|
352
|
+
// Limit seen set size
|
|
353
|
+
if (this.seenEventIds.size > 10000) {
|
|
354
|
+
const toDelete = Array.from(this.seenEventIds).slice(0, 5000);
|
|
355
|
+
for (const id of toDelete) {
|
|
356
|
+
this.seenEventIds.delete(id);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Skip self-messages
|
|
360
|
+
if (event.pubkey === settings.publicKey) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// Verify signature
|
|
364
|
+
if (!verifyEvent(event)) {
|
|
365
|
+
logger.warn(`Invalid signature on event ${event.id}`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// Check if this is addressed to us
|
|
369
|
+
const isToUs = event.tags.some((t) => t[0] === "p" && t[1] === settings.publicKey);
|
|
370
|
+
if (!isToUs) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
// Check DM policy
|
|
374
|
+
if (settings.dmPolicy === "disabled") {
|
|
375
|
+
logger.debug(`DM from ${event.pubkey} blocked - DMs disabled`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (settings.dmPolicy === "allowlist") {
|
|
379
|
+
const allowed = settings.allowFrom.includes(event.pubkey);
|
|
380
|
+
if (!allowed) {
|
|
381
|
+
logger.debug(`DM from ${event.pubkey} blocked - not in allowlist`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Decrypt the message (NIP-04)
|
|
386
|
+
let plaintext;
|
|
387
|
+
try {
|
|
388
|
+
logger.debug({ src: "plugin:nostr", op: "nip04:decrypt", from: event.pubkey }, "Decrypting Nostr DM");
|
|
389
|
+
plaintext = decrypt(privateKey, event.pubkey, event.content);
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
logger.warn({ src: "plugin:nostr", op: "nip04:decrypt", from: event.pubkey, err: String(err) }, "Failed to decrypt Nostr DM");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
logger.debug(`Received DM from ${event.pubkey.slice(0, 8)}...: ${plaintext.slice(0, 50)}...`);
|
|
396
|
+
// Emit event
|
|
397
|
+
if (this.runtime) {
|
|
398
|
+
this.runtime.emitEvent(NostrEventTypes.MESSAGE_RECEIVED, {
|
|
399
|
+
runtime: this.runtime,
|
|
400
|
+
accountId: this.getAccountId(),
|
|
401
|
+
from: event.pubkey,
|
|
402
|
+
text: plaintext,
|
|
403
|
+
eventId: event.id,
|
|
404
|
+
createdAt: event.created_at,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Check if the service is connected.
|
|
410
|
+
*/
|
|
411
|
+
isConnected() {
|
|
412
|
+
if (this.accountServices.size > 0) {
|
|
413
|
+
return Array.from(this.accountServices.values()).some((service) => service.isConnected());
|
|
414
|
+
}
|
|
415
|
+
return this.connected;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Get the bot's public key in hex format.
|
|
419
|
+
*/
|
|
420
|
+
getPublicKey() {
|
|
421
|
+
if (this.accountServices.size > 0) {
|
|
422
|
+
return this.getDefaultAccountService().getPublicKey();
|
|
423
|
+
}
|
|
424
|
+
return this.settings?.publicKey || "";
|
|
425
|
+
}
|
|
426
|
+
getAccountId(runtime) {
|
|
427
|
+
if (this.accountServices?.size > 0) {
|
|
428
|
+
return this.getDefaultAccountService().getAccountId(runtime);
|
|
429
|
+
}
|
|
430
|
+
return normalizeNostrAccountId(this.settings?.accountId ?? (runtime ? resolveDefaultNostrAccountId(runtime) : undefined));
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Get the bot's public key in npub format.
|
|
434
|
+
*/
|
|
435
|
+
getNpub() {
|
|
436
|
+
const pk = this.getPublicKey();
|
|
437
|
+
return pk ? pubkeyToNpub(pk) : "";
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Get connected relays.
|
|
441
|
+
*/
|
|
442
|
+
getRelays() {
|
|
443
|
+
if (this.accountServices.size > 0) {
|
|
444
|
+
return this.getDefaultAccountService().getRelays();
|
|
445
|
+
}
|
|
446
|
+
return this.settings?.relays || [];
|
|
447
|
+
}
|
|
448
|
+
async handleSendMessage(_runtime, target, content) {
|
|
449
|
+
const requestedAccountId = normalizeNostrAccountId(target.accountId ?? readNostrAccountId(content, target) ?? this.getAccountId());
|
|
450
|
+
if (this.accountServices.size > 0) {
|
|
451
|
+
await this.getAccountService(requestedAccountId).handleSendMessage(_runtime, target, content);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (requestedAccountId !== this.getAccountId()) {
|
|
455
|
+
throw new Error(`Nostr account '${requestedAccountId}' is not available in this service instance`);
|
|
456
|
+
}
|
|
457
|
+
const text = appendNostrAttachmentUrls(typeof content.text === "string" ? content.text.trim() : "", content);
|
|
458
|
+
if (!text) {
|
|
459
|
+
throw new Error("Nostr DM connector requires non-empty text content or an attachment.");
|
|
460
|
+
}
|
|
461
|
+
const metadata = getNostrTargetMetadata(target);
|
|
462
|
+
const targetPubkey = (typeof metadata?.nostrPubkey === "string" ? metadata.nostrPubkey : undefined) ??
|
|
463
|
+
(typeof target.entityId === "string" ? target.entityId : undefined) ??
|
|
464
|
+
target.channelId ??
|
|
465
|
+
target.threadId;
|
|
466
|
+
if (!targetPubkey) {
|
|
467
|
+
throw new Error("Nostr DM connector requires a pubkey target.");
|
|
468
|
+
}
|
|
469
|
+
const chunks = splitMessageForNostr(text);
|
|
470
|
+
for (const chunk of chunks) {
|
|
471
|
+
const result = await this.sendDm({ toPubkey: targetPubkey, text: chunk });
|
|
472
|
+
if (!result.success) {
|
|
473
|
+
throw new Error(result.error ?? "Failed to send Nostr DM");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async handleSendPost(runtime, content) {
|
|
478
|
+
const requestedAccountId = normalizeNostrAccountId(readNostrAccountId(content) ?? this.getAccountId());
|
|
479
|
+
if (this.accountServices.size > 0) {
|
|
480
|
+
return this.getAccountService(requestedAccountId).handleSendPost(runtime, content);
|
|
481
|
+
}
|
|
482
|
+
if (requestedAccountId !== this.getAccountId()) {
|
|
483
|
+
throw new Error(`Nostr account '${requestedAccountId}' is not available in this service instance`);
|
|
484
|
+
}
|
|
485
|
+
const text = appendNostrAttachmentUrls(typeof content.text === "string" ? content.text.trim() : "", content);
|
|
486
|
+
if (!text) {
|
|
487
|
+
throw new Error("Nostr post connector requires non-empty text content or an attachment.");
|
|
488
|
+
}
|
|
489
|
+
const result = await this.publishNote(text);
|
|
490
|
+
if (!result.success || !result.eventId) {
|
|
491
|
+
throw new Error(result.error ?? "Failed to publish Nostr note");
|
|
492
|
+
}
|
|
493
|
+
const event = {
|
|
494
|
+
id: result.eventId,
|
|
495
|
+
pubkey: this.getPublicKey(),
|
|
496
|
+
kind: 1,
|
|
497
|
+
content: text,
|
|
498
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
499
|
+
tags: [],
|
|
500
|
+
sig: "",
|
|
501
|
+
};
|
|
502
|
+
return this.nostrEventToPostMemory(runtime, event);
|
|
503
|
+
}
|
|
504
|
+
async fetchConnectorFeed(context, params = {}) {
|
|
505
|
+
const settings = this.settings;
|
|
506
|
+
const pool = this.pool;
|
|
507
|
+
if (!settings || !pool) {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
const target = params.target ?? context.target;
|
|
511
|
+
const metadata = target ? getNostrTargetMetadata(target) : undefined;
|
|
512
|
+
const author = (typeof metadata?.nostrPubkey === "string" ? metadata.nostrPubkey : undefined) ??
|
|
513
|
+
(typeof target?.entityId === "string" ? target.entityId : undefined);
|
|
514
|
+
const normalizedAuthor = author ? normalizePubkey(author) : undefined;
|
|
515
|
+
const filter = {
|
|
516
|
+
kinds: [1],
|
|
517
|
+
limit: clampLimit(params.limit, 25, 100),
|
|
518
|
+
...(normalizedAuthor ? { authors: [normalizedAuthor] } : {}),
|
|
519
|
+
...(params.cursor && Number.isFinite(Number(params.cursor))
|
|
520
|
+
? { until: Number(params.cursor) }
|
|
521
|
+
: {}),
|
|
522
|
+
};
|
|
523
|
+
const events = await pool.querySync(settings.relays, filter, { maxWait: 3000 });
|
|
524
|
+
return events
|
|
525
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
526
|
+
.map((event) => this.nostrEventToPostMemory(context.runtime, event));
|
|
527
|
+
}
|
|
528
|
+
async searchConnectorPosts(context, params) {
|
|
529
|
+
const query = params.query.trim();
|
|
530
|
+
if (!query) {
|
|
531
|
+
throw new Error("Nostr searchPosts connector requires a query.");
|
|
532
|
+
}
|
|
533
|
+
const settings = this.settings;
|
|
534
|
+
const pool = this.pool;
|
|
535
|
+
if (!settings || !pool) {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
const filter = {
|
|
539
|
+
kinds: [1],
|
|
540
|
+
search: query,
|
|
541
|
+
limit: clampLimit(params.limit, 25, 100),
|
|
542
|
+
...(params.cursor && Number.isFinite(Number(params.cursor))
|
|
543
|
+
? { until: Number(params.cursor) }
|
|
544
|
+
: {}),
|
|
545
|
+
};
|
|
546
|
+
const events = await pool.querySync(settings.relays, filter, { maxWait: 3000 });
|
|
547
|
+
return events
|
|
548
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
549
|
+
.map((event) => this.nostrEventToPostMemory(context.runtime, event));
|
|
550
|
+
}
|
|
551
|
+
async fetchConnectorMessages(context, params = {}) {
|
|
552
|
+
const settings = this.settings;
|
|
553
|
+
const pool = this.pool;
|
|
554
|
+
const privateKey = this.privateKey;
|
|
555
|
+
if (!settings || !pool || !privateKey) {
|
|
556
|
+
return [];
|
|
557
|
+
}
|
|
558
|
+
const target = params.target ?? context.target;
|
|
559
|
+
const metadata = target ? getNostrTargetMetadata(target) : undefined;
|
|
560
|
+
const targetPubkeyRaw = (typeof metadata?.nostrPubkey === "string" ? metadata.nostrPubkey : undefined) ??
|
|
561
|
+
(typeof target?.entityId === "string" ? target.entityId : undefined);
|
|
562
|
+
const targetPubkey = targetPubkeyRaw ? normalizePubkey(targetPubkeyRaw) : undefined;
|
|
563
|
+
const limit = clampLimit(params.limit, 25, 100);
|
|
564
|
+
const filters = [
|
|
565
|
+
{
|
|
566
|
+
kinds: [4],
|
|
567
|
+
"#p": [settings.publicKey],
|
|
568
|
+
...(targetPubkey ? { authors: [targetPubkey] } : {}),
|
|
569
|
+
limit,
|
|
570
|
+
},
|
|
571
|
+
...(targetPubkey
|
|
572
|
+
? [
|
|
573
|
+
{
|
|
574
|
+
kinds: [4],
|
|
575
|
+
authors: [settings.publicKey],
|
|
576
|
+
"#p": [targetPubkey],
|
|
577
|
+
limit,
|
|
578
|
+
},
|
|
579
|
+
]
|
|
580
|
+
: []),
|
|
581
|
+
];
|
|
582
|
+
const byId = new Map();
|
|
583
|
+
for (const filter of filters) {
|
|
584
|
+
const events = await pool.querySync(settings.relays, filter, { maxWait: 3000 });
|
|
585
|
+
for (const event of events) {
|
|
586
|
+
byId.set(event.id, event);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const memories = [];
|
|
590
|
+
for (const event of Array.from(byId.values()).sort((a, b) => b.created_at - a.created_at)) {
|
|
591
|
+
const isOwn = event.pubkey === settings.publicKey;
|
|
592
|
+
const peerPubkey = isOwn ? event.tags.find((tag) => tag[0] === "p")?.[1] : event.pubkey;
|
|
593
|
+
if (!peerPubkey) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const plaintext = decrypt(privateKey, peerPubkey, event.content);
|
|
598
|
+
memories.push(this.nostrEventToDmMemory(context.runtime, event, plaintext, peerPubkey));
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
logger.debug({
|
|
602
|
+
src: "plugin:nostr",
|
|
603
|
+
op: "fetchConnectorMessages",
|
|
604
|
+
eventId: event.id,
|
|
605
|
+
error: error instanceof Error ? error.message : String(error),
|
|
606
|
+
}, "Skipping Nostr DM that could not be decrypted");
|
|
607
|
+
}
|
|
608
|
+
if (memories.length >= limit) {
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return memories;
|
|
613
|
+
}
|
|
614
|
+
async resolveConnectorTargets(query, _context) {
|
|
615
|
+
const trimmed = query.trim();
|
|
616
|
+
if (!trimmed) {
|
|
617
|
+
return this.listRecentConnectorTargets(_context);
|
|
618
|
+
}
|
|
619
|
+
let pubkey;
|
|
620
|
+
try {
|
|
621
|
+
pubkey = normalizePubkey(trimmed);
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
return [];
|
|
625
|
+
}
|
|
626
|
+
return [this.buildPubkeyTarget(pubkey, 1)];
|
|
627
|
+
}
|
|
628
|
+
async listRecentConnectorTargets(_context) {
|
|
629
|
+
const allowFrom = this.settings?.allowFrom ?? [];
|
|
630
|
+
return allowFrom.map((pubkey, index) => this.buildPubkeyTarget(pubkey, 0.8 - index * 0.01));
|
|
631
|
+
}
|
|
632
|
+
async getConnectorUserContext(entityId, _context) {
|
|
633
|
+
let pubkey;
|
|
634
|
+
try {
|
|
635
|
+
pubkey = normalizePubkey(entityId);
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
entityId,
|
|
642
|
+
label: pubkeyToNpub(pubkey),
|
|
643
|
+
aliases: [pubkey, pubkeyToNpub(pubkey)],
|
|
644
|
+
handles: {
|
|
645
|
+
nostr: pubkeyToNpub(pubkey),
|
|
646
|
+
},
|
|
647
|
+
metadata: {
|
|
648
|
+
accountId: this.getAccountId(),
|
|
649
|
+
nostrPubkey: pubkey,
|
|
650
|
+
relays: this.getRelays(),
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
nostrEventToPostMemory(runtime, event) {
|
|
655
|
+
const createdAt = createdAtMs(event);
|
|
656
|
+
const entityId = event.pubkey === runtime.agentId
|
|
657
|
+
? runtime.agentId
|
|
658
|
+
: createUniqueUuid(runtime, `nostr:user:${event.pubkey}`);
|
|
659
|
+
const roomId = createUniqueUuid(runtime, `nostr:feed:${event.pubkey}`);
|
|
660
|
+
return {
|
|
661
|
+
id: createUniqueUuid(runtime, `nostr:note:${event.id}`),
|
|
662
|
+
agentId: runtime.agentId,
|
|
663
|
+
entityId,
|
|
664
|
+
roomId,
|
|
665
|
+
createdAt,
|
|
666
|
+
content: {
|
|
667
|
+
text: event.content,
|
|
668
|
+
source: "nostr",
|
|
669
|
+
url: `nostr:${event.id}`,
|
|
670
|
+
channelType: ChannelType.FEED,
|
|
671
|
+
},
|
|
672
|
+
metadata: {
|
|
673
|
+
type: "message",
|
|
674
|
+
source: "nostr",
|
|
675
|
+
accountId: this.getAccountId(runtime),
|
|
676
|
+
provider: "nostr",
|
|
677
|
+
timestamp: createdAt,
|
|
678
|
+
fromBot: event.pubkey === this.getPublicKey(),
|
|
679
|
+
messageIdFull: event.id,
|
|
680
|
+
chatType: ChannelType.FEED,
|
|
681
|
+
sender: {
|
|
682
|
+
id: event.pubkey,
|
|
683
|
+
username: pubkeyToNpub(event.pubkey),
|
|
684
|
+
},
|
|
685
|
+
nostr: {
|
|
686
|
+
accountId: this.getAccountId(runtime),
|
|
687
|
+
eventId: event.id,
|
|
688
|
+
pubkey: event.pubkey,
|
|
689
|
+
npub: pubkeyToNpub(event.pubkey),
|
|
690
|
+
kind: event.kind,
|
|
691
|
+
tags: normalizeEventTags(event.tags),
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
nostrEventToDmMemory(runtime, event, plaintext, peerPubkey) {
|
|
697
|
+
const createdAt = createdAtMs(event);
|
|
698
|
+
const senderId = event.pubkey;
|
|
699
|
+
const entityId = senderId === runtime.agentId
|
|
700
|
+
? runtime.agentId
|
|
701
|
+
: createUniqueUuid(runtime, `nostr:user:${senderId}`);
|
|
702
|
+
const roomId = createUniqueUuid(runtime, `nostr:dm:${peerPubkey}`);
|
|
703
|
+
return {
|
|
704
|
+
id: createUniqueUuid(runtime, `nostr:dm:${event.id}`),
|
|
705
|
+
agentId: runtime.agentId,
|
|
706
|
+
entityId,
|
|
707
|
+
roomId,
|
|
708
|
+
createdAt,
|
|
709
|
+
content: {
|
|
710
|
+
text: plaintext,
|
|
711
|
+
source: "nostr",
|
|
712
|
+
channelType: ChannelType.DM,
|
|
713
|
+
},
|
|
714
|
+
metadata: {
|
|
715
|
+
type: "message",
|
|
716
|
+
source: "nostr",
|
|
717
|
+
accountId: this.getAccountId(runtime),
|
|
718
|
+
provider: "nostr",
|
|
719
|
+
timestamp: createdAt,
|
|
720
|
+
fromBot: senderId === this.getPublicKey(),
|
|
721
|
+
messageIdFull: event.id,
|
|
722
|
+
chatType: ChannelType.DM,
|
|
723
|
+
sender: {
|
|
724
|
+
id: senderId,
|
|
725
|
+
username: pubkeyToNpub(senderId),
|
|
726
|
+
},
|
|
727
|
+
nostr: {
|
|
728
|
+
accountId: this.getAccountId(runtime),
|
|
729
|
+
eventId: event.id,
|
|
730
|
+
pubkey: senderId,
|
|
731
|
+
npub: pubkeyToNpub(senderId),
|
|
732
|
+
peerPubkey,
|
|
733
|
+
peerNpub: pubkeyToNpub(peerPubkey),
|
|
734
|
+
kind: event.kind,
|
|
735
|
+
tags: normalizeEventTags(event.tags),
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
buildPubkeyTarget(pubkey, score) {
|
|
741
|
+
return {
|
|
742
|
+
target: {
|
|
743
|
+
source: "nostr",
|
|
744
|
+
accountId: this.getAccountId(),
|
|
745
|
+
entityId: pubkey,
|
|
746
|
+
},
|
|
747
|
+
label: pubkeyToNpub(pubkey),
|
|
748
|
+
kind: "user",
|
|
749
|
+
description: "Nostr encrypted DM recipient",
|
|
750
|
+
score,
|
|
751
|
+
contexts: [...NOSTR_CONNECTOR_CONTEXTS],
|
|
752
|
+
metadata: {
|
|
753
|
+
accountId: this.getAccountId(),
|
|
754
|
+
nostrPubkey: pubkey,
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Send a DM to a pubkey.
|
|
760
|
+
*/
|
|
761
|
+
async sendDm(options) {
|
|
762
|
+
if (this.accountServices.size > 0) {
|
|
763
|
+
const accountId = normalizeNostrAccountId(options.accountId ?? this.getAccountId());
|
|
764
|
+
return this.getAccountService(accountId).sendDm(options);
|
|
765
|
+
}
|
|
766
|
+
const settings = this.settings;
|
|
767
|
+
const pool = this.pool;
|
|
768
|
+
const privateKey = this.privateKey;
|
|
769
|
+
if (!settings || !pool || !privateKey) {
|
|
770
|
+
return {
|
|
771
|
+
success: false,
|
|
772
|
+
error: "Service not initialized",
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const text = typeof options.text === "string" ? options.text.trim() : "";
|
|
776
|
+
if (!text) {
|
|
777
|
+
return {
|
|
778
|
+
success: false,
|
|
779
|
+
error: "DM content cannot be empty",
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
// Normalize the target pubkey
|
|
783
|
+
let toPubkey;
|
|
784
|
+
try {
|
|
785
|
+
toPubkey = normalizePubkey(options.toPubkey);
|
|
786
|
+
}
|
|
787
|
+
catch (err) {
|
|
788
|
+
return {
|
|
789
|
+
success: false,
|
|
790
|
+
error: `Invalid target pubkey: ${err}`,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
// Encrypt the message (NIP-04)
|
|
794
|
+
let ciphertext;
|
|
795
|
+
try {
|
|
796
|
+
logger.debug({ src: "plugin:nostr", op: "nip04:encrypt", to: toPubkey }, "Encrypting Nostr DM");
|
|
797
|
+
ciphertext = encrypt(privateKey, toPubkey, text);
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
return {
|
|
801
|
+
success: false,
|
|
802
|
+
error: `Encryption failed: ${err}`,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
// Create the event
|
|
806
|
+
const event = finalizeEvent({
|
|
807
|
+
kind: 4,
|
|
808
|
+
content: ciphertext,
|
|
809
|
+
tags: [["p", toPubkey]],
|
|
810
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
811
|
+
}, privateKey);
|
|
812
|
+
// Publish to relays
|
|
813
|
+
const successRelays = [];
|
|
814
|
+
const errors = [];
|
|
815
|
+
for (const relay of settings.relays) {
|
|
816
|
+
try {
|
|
817
|
+
logger.debug({ src: "plugin:nostr", op: "pool.publish", kind: 4, relay, eventId: event.id }, "Publishing Nostr DM event to relay");
|
|
818
|
+
await pool.publish([relay], event);
|
|
819
|
+
successRelays.push(relay);
|
|
820
|
+
}
|
|
821
|
+
catch (err) {
|
|
822
|
+
errors.push(`${relay}: ${err}`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (successRelays.length === 0) {
|
|
826
|
+
return {
|
|
827
|
+
success: false,
|
|
828
|
+
error: `Failed to publish to any relay: ${errors.join("; ")}`,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
logger.debug(`DM sent to ${toPubkey.slice(0, 8)}... via ${successRelays.length} relay(s)`);
|
|
832
|
+
if (this.runtime) {
|
|
833
|
+
this.runtime.emitEvent(NostrEventTypes.MESSAGE_SENT, {
|
|
834
|
+
runtime: this.runtime,
|
|
835
|
+
accountId: this.getAccountId(),
|
|
836
|
+
to: toPubkey,
|
|
837
|
+
eventId: event.id,
|
|
838
|
+
relays: successRelays,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
success: true,
|
|
843
|
+
eventId: event.id,
|
|
844
|
+
relays: successRelays,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Publish profile (kind:0).
|
|
849
|
+
*/
|
|
850
|
+
async publishProfile(profile) {
|
|
851
|
+
if (this.accountServices.size > 0) {
|
|
852
|
+
const accountId = normalizeNostrAccountId(profile.accountId ?? this.getAccountId());
|
|
853
|
+
return this.getAccountService(accountId).publishProfile(profile);
|
|
854
|
+
}
|
|
855
|
+
const settings = this.settings;
|
|
856
|
+
const pool = this.pool;
|
|
857
|
+
const privateKey = this.privateKey;
|
|
858
|
+
if (!settings || !pool || !privateKey) {
|
|
859
|
+
return {
|
|
860
|
+
success: false,
|
|
861
|
+
error: "Service not initialized",
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
// Build profile content
|
|
865
|
+
const content = JSON.stringify({
|
|
866
|
+
name: profile.name,
|
|
867
|
+
display_name: profile.displayName,
|
|
868
|
+
about: profile.about,
|
|
869
|
+
picture: profile.picture,
|
|
870
|
+
banner: profile.banner,
|
|
871
|
+
nip05: profile.nip05,
|
|
872
|
+
lud16: profile.lud16,
|
|
873
|
+
website: profile.website,
|
|
874
|
+
});
|
|
875
|
+
// Create the event
|
|
876
|
+
const event = finalizeEvent({
|
|
877
|
+
kind: 0,
|
|
878
|
+
content,
|
|
879
|
+
tags: [],
|
|
880
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
881
|
+
}, privateKey);
|
|
882
|
+
// Publish to relays
|
|
883
|
+
const successRelays = [];
|
|
884
|
+
const errors = [];
|
|
885
|
+
for (const relay of settings.relays) {
|
|
886
|
+
try {
|
|
887
|
+
logger.debug({ src: "plugin:nostr", op: "pool.publish", kind: 0, relay, eventId: event.id }, "Publishing Nostr profile event to relay");
|
|
888
|
+
await pool.publish([relay], event);
|
|
889
|
+
successRelays.push(relay);
|
|
890
|
+
}
|
|
891
|
+
catch (err) {
|
|
892
|
+
errors.push(`${relay}: ${err}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (successRelays.length === 0) {
|
|
896
|
+
return {
|
|
897
|
+
success: false,
|
|
898
|
+
error: `Failed to publish profile to any relay: ${errors.join("; ")}`,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
logger.info(`Profile published via ${successRelays.length} relay(s)`);
|
|
902
|
+
if (this.runtime) {
|
|
903
|
+
this.runtime.emitEvent(NostrEventTypes.PROFILE_PUBLISHED, {
|
|
904
|
+
runtime: this.runtime,
|
|
905
|
+
accountId: this.getAccountId(),
|
|
906
|
+
eventId: event.id,
|
|
907
|
+
relays: successRelays,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
success: true,
|
|
912
|
+
eventId: event.id,
|
|
913
|
+
relays: successRelays,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Publish a text note (kind:1).
|
|
918
|
+
*/
|
|
919
|
+
async publishNote(text, tags = []) {
|
|
920
|
+
if (this.accountServices.size > 0) {
|
|
921
|
+
return this.getDefaultAccountService().publishNote(text, tags);
|
|
922
|
+
}
|
|
923
|
+
const settings = this.settings;
|
|
924
|
+
const pool = this.pool;
|
|
925
|
+
const privateKey = this.privateKey;
|
|
926
|
+
if (!settings || !pool || !privateKey) {
|
|
927
|
+
return {
|
|
928
|
+
success: false,
|
|
929
|
+
error: "Service not initialized",
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const trimmed = text.trim();
|
|
933
|
+
if (!trimmed) {
|
|
934
|
+
return {
|
|
935
|
+
success: false,
|
|
936
|
+
error: "Note content cannot be empty",
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const event = finalizeEvent({
|
|
940
|
+
kind: 1,
|
|
941
|
+
content: trimmed,
|
|
942
|
+
tags: normalizeEventTags(tags),
|
|
943
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
944
|
+
}, privateKey);
|
|
945
|
+
const successRelays = [];
|
|
946
|
+
const errors = [];
|
|
947
|
+
for (const relay of settings.relays) {
|
|
948
|
+
try {
|
|
949
|
+
logger.debug({ src: "plugin:nostr", op: "pool.publish", kind: 1, relay, eventId: event.id }, "Publishing Nostr note to relay");
|
|
950
|
+
await pool.publish([relay], event);
|
|
951
|
+
successRelays.push(relay);
|
|
952
|
+
}
|
|
953
|
+
catch (err) {
|
|
954
|
+
errors.push(`${relay}: ${err}`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (successRelays.length === 0) {
|
|
958
|
+
return {
|
|
959
|
+
success: false,
|
|
960
|
+
error: `Failed to publish note to any relay: ${errors.join("; ")}`,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
logger.info({ src: "plugin:nostr", op: "publishNote", eventId: event.id, relays: successRelays.length }, "Nostr note published");
|
|
964
|
+
return {
|
|
965
|
+
success: true,
|
|
966
|
+
eventId: event.id,
|
|
967
|
+
relays: successRelays,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Get the settings.
|
|
972
|
+
*/
|
|
973
|
+
getSettings() {
|
|
974
|
+
if (this.accountServices.size > 0) {
|
|
975
|
+
return this.getDefaultAccountService().getSettings();
|
|
976
|
+
}
|
|
977
|
+
return this.settings;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
//# sourceMappingURL=service.js.map
|