@elizaos/plugin-nostr 2.0.0-alpha.7 → 2.0.0-beta.1
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/README.md +200 -0
- package/auto-enable.ts +20 -0
- package/dist/src/accounts.d.ts +15 -0
- package/dist/src/accounts.d.ts.map +1 -0
- package/dist/src/accounts.js +139 -0
- package/dist/src/accounts.js.map +1 -0
- package/dist/src/actions/index.d.ts +7 -0
- package/dist/src/actions/index.d.ts.map +1 -0
- package/dist/src/actions/index.js +7 -0
- package/dist/src/actions/index.js.map +1 -0
- package/dist/src/actions/publishProfile.d.ts +6 -0
- package/dist/src/actions/publishProfile.d.ts.map +1 -0
- package/dist/src/actions/publishProfile.js +157 -0
- package/dist/src/actions/publishProfile.js.map +1 -0
- package/dist/src/connector-account-provider.d.ts +20 -0
- package/dist/src/connector-account-provider.d.ts.map +1 -0
- package/dist/src/connector-account-provider.js +78 -0
- package/dist/src/connector-account-provider.js.map +1 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +66 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/providers/identityContext.d.ts +6 -0
- package/dist/src/providers/identityContext.d.ts.map +1 -0
- package/dist/src/providers/identityContext.js +69 -0
- package/dist/src/providers/identityContext.js.map +1 -0
- package/dist/src/providers/index.d.ts +5 -0
- package/dist/src/providers/index.d.ts.map +1 -0
- package/dist/src/providers/index.js +5 -0
- package/dist/src/providers/index.js.map +1 -0
- package/dist/src/providers/senderContext.d.ts +6 -0
- package/dist/src/providers/senderContext.d.ts.map +1 -0
- package/dist/src/providers/senderContext.js +64 -0
- package/dist/src/providers/senderContext.js.map +1 -0
- package/dist/src/service.d.ts +116 -0
- package/dist/src/service.d.ts.map +1 -0
- package/dist/src/service.js +915 -0
- package/dist/src/service.js.map +1 -0
- package/dist/src/toon.d.ts +2 -0
- package/dist/src/toon.d.ts.map +1 -0
- package/dist/src/toon.js +18 -0
- package/dist/src/toon.js.map +1 -0
- package/dist/src/types.d.ts +125 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +192 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +26 -11
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -876
- package/dist/index.js.map +0 -16
package/dist/index.js
DELETED
|
@@ -1,876 +0,0 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
import { logger as logger3 } from "@elizaos/core";
|
|
3
|
-
|
|
4
|
-
// src/actions/publishProfile.ts
|
|
5
|
-
import {
|
|
6
|
-
composePromptFromState,
|
|
7
|
-
ModelType,
|
|
8
|
-
parseJSONObjectFromText
|
|
9
|
-
} from "@elizaos/core";
|
|
10
|
-
|
|
11
|
-
// src/types.ts
|
|
12
|
-
import { nip19 } from "nostr-tools";
|
|
13
|
-
var MAX_NOSTR_MESSAGE_LENGTH = 4000;
|
|
14
|
-
var NOSTR_SERVICE_NAME = "nostr";
|
|
15
|
-
var DEFAULT_NOSTR_RELAYS = [
|
|
16
|
-
"wss://relay.damus.io",
|
|
17
|
-
"wss://nos.lol",
|
|
18
|
-
"wss://relay.nostr.band"
|
|
19
|
-
];
|
|
20
|
-
var NostrEventTypes;
|
|
21
|
-
((NostrEventTypes2) => {
|
|
22
|
-
NostrEventTypes2["MESSAGE_RECEIVED"] = "NOSTR_MESSAGE_RECEIVED";
|
|
23
|
-
NostrEventTypes2["MESSAGE_SENT"] = "NOSTR_MESSAGE_SENT";
|
|
24
|
-
NostrEventTypes2["RELAY_CONNECTED"] = "NOSTR_RELAY_CONNECTED";
|
|
25
|
-
NostrEventTypes2["RELAY_DISCONNECTED"] = "NOSTR_RELAY_DISCONNECTED";
|
|
26
|
-
NostrEventTypes2["PROFILE_PUBLISHED"] = "NOSTR_PROFILE_PUBLISHED";
|
|
27
|
-
NostrEventTypes2["CONNECTION_READY"] = "NOSTR_CONNECTION_READY";
|
|
28
|
-
})(NostrEventTypes ||= {});
|
|
29
|
-
|
|
30
|
-
class NostrPluginError extends Error {
|
|
31
|
-
code;
|
|
32
|
-
cause;
|
|
33
|
-
constructor(message, code, cause) {
|
|
34
|
-
super(message);
|
|
35
|
-
this.code = code;
|
|
36
|
-
this.cause = cause;
|
|
37
|
-
this.name = "NostrPluginError";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
class NostrConfigurationError extends NostrPluginError {
|
|
42
|
-
setting;
|
|
43
|
-
constructor(message, setting, cause) {
|
|
44
|
-
super(message, "CONFIGURATION_ERROR", cause);
|
|
45
|
-
this.name = "NostrConfigurationError";
|
|
46
|
-
this.setting = setting;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
class NostrRelayError extends NostrPluginError {
|
|
51
|
-
relay;
|
|
52
|
-
constructor(message, relay, cause) {
|
|
53
|
-
super(message, "RELAY_ERROR", cause);
|
|
54
|
-
this.name = "NostrRelayError";
|
|
55
|
-
this.relay = relay;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
class NostrCryptoError extends NostrPluginError {
|
|
60
|
-
constructor(message, cause) {
|
|
61
|
-
super(message, "CRYPTO_ERROR", cause);
|
|
62
|
-
this.name = "NostrCryptoError";
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
function isValidPubkey(input) {
|
|
66
|
-
if (typeof input !== "string") {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
const trimmed = input.trim();
|
|
70
|
-
if (trimmed.startsWith("npub1")) {
|
|
71
|
-
try {
|
|
72
|
-
const decoded = nip19.decode(trimmed);
|
|
73
|
-
return decoded.type === "npub";
|
|
74
|
-
} catch {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return /^[0-9a-fA-F]{64}$/.test(trimmed);
|
|
79
|
-
}
|
|
80
|
-
function normalizePubkey(input) {
|
|
81
|
-
const trimmed = input.trim();
|
|
82
|
-
if (trimmed.startsWith("npub1")) {
|
|
83
|
-
const decoded = nip19.decode(trimmed);
|
|
84
|
-
if (decoded.type !== "npub") {
|
|
85
|
-
throw new NostrCryptoError("Invalid npub key");
|
|
86
|
-
}
|
|
87
|
-
const data = decoded.data;
|
|
88
|
-
return Array.from(data).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
89
|
-
}
|
|
90
|
-
if (!/^[0-9a-fA-F]{64}$/.test(trimmed)) {
|
|
91
|
-
throw new NostrCryptoError("Pubkey must be 64 hex characters or npub format");
|
|
92
|
-
}
|
|
93
|
-
return trimmed.toLowerCase();
|
|
94
|
-
}
|
|
95
|
-
function pubkeyToNpub(hexPubkey) {
|
|
96
|
-
const normalized = normalizePubkey(hexPubkey);
|
|
97
|
-
return nip19.npubEncode(normalized);
|
|
98
|
-
}
|
|
99
|
-
function validatePrivateKey(key) {
|
|
100
|
-
const trimmed = key.trim();
|
|
101
|
-
if (trimmed.startsWith("nsec1")) {
|
|
102
|
-
const decoded = nip19.decode(trimmed);
|
|
103
|
-
if (decoded.type !== "nsec") {
|
|
104
|
-
throw new NostrCryptoError("Invalid nsec key: wrong type");
|
|
105
|
-
}
|
|
106
|
-
return decoded.data;
|
|
107
|
-
}
|
|
108
|
-
if (!/^[0-9a-fA-F]{64}$/.test(trimmed)) {
|
|
109
|
-
throw new NostrCryptoError("Private key must be 64 hex characters or nsec bech32 format");
|
|
110
|
-
}
|
|
111
|
-
const bytes = new Uint8Array(32);
|
|
112
|
-
for (let i = 0;i < 32; i++) {
|
|
113
|
-
bytes[i] = parseInt(trimmed.slice(i * 2, i * 2 + 2), 16);
|
|
114
|
-
}
|
|
115
|
-
return bytes;
|
|
116
|
-
}
|
|
117
|
-
function getPubkeyDisplayName(pubkey) {
|
|
118
|
-
const normalized = normalizePubkey(pubkey);
|
|
119
|
-
return `${normalized.slice(0, 8)}...${normalized.slice(-8)}`;
|
|
120
|
-
}
|
|
121
|
-
function splitMessageForNostr(text, maxLength = MAX_NOSTR_MESSAGE_LENGTH) {
|
|
122
|
-
if (text.length <= maxLength) {
|
|
123
|
-
return [text];
|
|
124
|
-
}
|
|
125
|
-
const chunks = [];
|
|
126
|
-
let remaining = text;
|
|
127
|
-
while (remaining.length > 0) {
|
|
128
|
-
if (remaining.length <= maxLength) {
|
|
129
|
-
chunks.push(remaining);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
let breakPoint = maxLength;
|
|
133
|
-
const newlineIndex = remaining.lastIndexOf(`
|
|
134
|
-
`, maxLength);
|
|
135
|
-
if (newlineIndex > maxLength * 0.5) {
|
|
136
|
-
breakPoint = newlineIndex + 1;
|
|
137
|
-
} else {
|
|
138
|
-
const spaceIndex = remaining.lastIndexOf(" ", maxLength);
|
|
139
|
-
if (spaceIndex > maxLength * 0.5) {
|
|
140
|
-
breakPoint = spaceIndex + 1;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
chunks.push(remaining.slice(0, breakPoint).trimEnd());
|
|
144
|
-
remaining = remaining.slice(breakPoint).trimStart();
|
|
145
|
-
}
|
|
146
|
-
return chunks;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// src/actions/publishProfile.ts
|
|
150
|
-
var PUBLISH_PROFILE_TEMPLATE = `# Task: Extract Nostr profile data
|
|
151
|
-
Based on the conversation, determine what profile information to update.
|
|
152
|
-
|
|
153
|
-
Recent conversation:
|
|
154
|
-
{{recentMessages}}
|
|
155
|
-
|
|
156
|
-
Extract any of the following profile fields that should be updated:
|
|
157
|
-
- name: Display name
|
|
158
|
-
- about: Bio/description
|
|
159
|
-
- picture: Profile picture URL
|
|
160
|
-
- banner: Banner image URL
|
|
161
|
-
- nip05: Nostr verification (user@domain.com)
|
|
162
|
-
- lud16: Lightning address (user@domain.com)
|
|
163
|
-
- website: Website URL
|
|
164
|
-
|
|
165
|
-
Respond with a JSON object containing only the fields to update:
|
|
166
|
-
\`\`\`json
|
|
167
|
-
{
|
|
168
|
-
"name": "optional name",
|
|
169
|
-
"about": "optional bio"
|
|
170
|
-
}
|
|
171
|
-
\`\`\``;
|
|
172
|
-
var publishProfile = {
|
|
173
|
-
name: "NOSTR_PUBLISH_PROFILE",
|
|
174
|
-
similes: ["UPDATE_NOSTR_PROFILE", "SET_NOSTR_PROFILE", "NOSTR_PROFILE"],
|
|
175
|
-
description: "Publish or update the bot's Nostr profile (kind:0 metadata)",
|
|
176
|
-
validate: async (_runtime, message, _state) => {
|
|
177
|
-
return message.content.source === "nostr";
|
|
178
|
-
},
|
|
179
|
-
handler: async (runtime, message, state, _options, callback) => {
|
|
180
|
-
const nostrService = runtime.getService(NOSTR_SERVICE_NAME);
|
|
181
|
-
if (!nostrService || !nostrService.isConnected()) {
|
|
182
|
-
if (callback) {
|
|
183
|
-
callback({ text: "Nostr service is not available.", source: "nostr" });
|
|
184
|
-
}
|
|
185
|
-
return { success: false, error: "Nostr service not available" };
|
|
186
|
-
}
|
|
187
|
-
const currentState = state ?? await runtime.composeState(message);
|
|
188
|
-
const prompt = await composePromptFromState({
|
|
189
|
-
template: PUBLISH_PROFILE_TEMPLATE,
|
|
190
|
-
state: currentState
|
|
191
|
-
});
|
|
192
|
-
let profileInfo = null;
|
|
193
|
-
for (let attempt = 0;attempt < 3; attempt++) {
|
|
194
|
-
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
195
|
-
prompt
|
|
196
|
-
});
|
|
197
|
-
const parsed = parseJSONObjectFromText(String(response));
|
|
198
|
-
if (parsed) {
|
|
199
|
-
profileInfo = {
|
|
200
|
-
name: parsed.name ? String(parsed.name) : undefined,
|
|
201
|
-
displayName: parsed.displayName ? String(parsed.displayName) : undefined,
|
|
202
|
-
about: parsed.about ? String(parsed.about) : undefined,
|
|
203
|
-
picture: parsed.picture ? String(parsed.picture) : undefined,
|
|
204
|
-
banner: parsed.banner ? String(parsed.banner) : undefined,
|
|
205
|
-
nip05: parsed.nip05 ? String(parsed.nip05) : undefined,
|
|
206
|
-
lud16: parsed.lud16 ? String(parsed.lud16) : undefined,
|
|
207
|
-
website: parsed.website ? String(parsed.website) : undefined
|
|
208
|
-
};
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (!profileInfo) {
|
|
213
|
-
if (callback) {
|
|
214
|
-
callback({
|
|
215
|
-
text: "I couldn't understand the profile information. Please try again.",
|
|
216
|
-
source: "nostr"
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
return { success: false, error: "Could not extract profile parameters" };
|
|
220
|
-
}
|
|
221
|
-
const result = await nostrService.publishProfile(profileInfo);
|
|
222
|
-
if (!result.success) {
|
|
223
|
-
if (callback) {
|
|
224
|
-
callback({
|
|
225
|
-
text: `Failed to publish profile: ${result.error}`,
|
|
226
|
-
source: "nostr"
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
return { success: false, error: result.error };
|
|
230
|
-
}
|
|
231
|
-
if (callback) {
|
|
232
|
-
callback({
|
|
233
|
-
text: "Profile published successfully.",
|
|
234
|
-
source: message.content.source
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
return {
|
|
238
|
-
success: true,
|
|
239
|
-
data: {
|
|
240
|
-
eventId: result.eventId,
|
|
241
|
-
relays: result.relays,
|
|
242
|
-
profile: profileInfo
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
},
|
|
246
|
-
examples: [
|
|
247
|
-
[
|
|
248
|
-
{
|
|
249
|
-
name: "{{user1}}",
|
|
250
|
-
content: { text: "Update your profile name to 'Bot Assistant'" }
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
name: "{{agent}}",
|
|
254
|
-
content: {
|
|
255
|
-
text: "I'll update my Nostr profile.",
|
|
256
|
-
actions: ["NOSTR_PUBLISH_PROFILE"]
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
]
|
|
260
|
-
]
|
|
261
|
-
};
|
|
262
|
-
// src/actions/sendDm.ts
|
|
263
|
-
import {
|
|
264
|
-
composePromptFromState as composePromptFromState2,
|
|
265
|
-
logger,
|
|
266
|
-
ModelType as ModelType2,
|
|
267
|
-
parseJSONObjectFromText as parseJSONObjectFromText2
|
|
268
|
-
} from "@elizaos/core";
|
|
269
|
-
var SEND_DM_TEMPLATE = `# Task: Extract Nostr DM parameters
|
|
270
|
-
Based on the conversation, determine what message to send and to whom.
|
|
271
|
-
|
|
272
|
-
Recent conversation:
|
|
273
|
-
{{recentMessages}}
|
|
274
|
-
|
|
275
|
-
Extract the following:
|
|
276
|
-
- text: The message content to send
|
|
277
|
-
- toPubkey: The target pubkey (npub or hex format, or "current" for the current conversation)
|
|
278
|
-
|
|
279
|
-
Respond with a JSON object:
|
|
280
|
-
\`\`\`json
|
|
281
|
-
{
|
|
282
|
-
"text": "message content here",
|
|
283
|
-
"toPubkey": "npub1... or hex pubkey or current"
|
|
284
|
-
}
|
|
285
|
-
\`\`\``;
|
|
286
|
-
var sendDm = {
|
|
287
|
-
name: "NOSTR_SEND_DM",
|
|
288
|
-
similes: ["SEND_NOSTR_DM", "NOSTR_MESSAGE", "NOSTR_TEXT", "DM_NOSTR"],
|
|
289
|
-
description: "Send an encrypted direct message via Nostr (NIP-04)",
|
|
290
|
-
validate: async (_runtime, message, _state) => {
|
|
291
|
-
return message.content.source === "nostr";
|
|
292
|
-
},
|
|
293
|
-
handler: async (runtime, message, state, _options, callback) => {
|
|
294
|
-
const nostrService = runtime.getService(NOSTR_SERVICE_NAME);
|
|
295
|
-
if (!nostrService || !nostrService.isConnected()) {
|
|
296
|
-
if (callback) {
|
|
297
|
-
callback({ text: "Nostr service is not available.", source: "nostr" });
|
|
298
|
-
}
|
|
299
|
-
return { success: false, error: "Nostr service not available" };
|
|
300
|
-
}
|
|
301
|
-
const currentState = state ?? await runtime.composeState(message);
|
|
302
|
-
const prompt = await composePromptFromState2({
|
|
303
|
-
template: SEND_DM_TEMPLATE,
|
|
304
|
-
state: currentState
|
|
305
|
-
});
|
|
306
|
-
let dmInfo = null;
|
|
307
|
-
for (let attempt = 0;attempt < 3; attempt++) {
|
|
308
|
-
const response = await runtime.useModel(ModelType2.TEXT_SMALL, {
|
|
309
|
-
prompt
|
|
310
|
-
});
|
|
311
|
-
const parsed = parseJSONObjectFromText2(String(response));
|
|
312
|
-
if (parsed?.text) {
|
|
313
|
-
dmInfo = {
|
|
314
|
-
text: String(parsed.text),
|
|
315
|
-
toPubkey: String(parsed.toPubkey || "current")
|
|
316
|
-
};
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
if (!dmInfo || !dmInfo.text) {
|
|
321
|
-
if (callback) {
|
|
322
|
-
callback({
|
|
323
|
-
text: "I couldn't understand what message you want me to send. Please try again.",
|
|
324
|
-
source: "nostr"
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
return { success: false, error: "Could not extract message parameters" };
|
|
328
|
-
}
|
|
329
|
-
let targetPubkey;
|
|
330
|
-
if (dmInfo.toPubkey && dmInfo.toPubkey !== "current") {
|
|
331
|
-
if (isValidPubkey(dmInfo.toPubkey)) {
|
|
332
|
-
try {
|
|
333
|
-
targetPubkey = normalizePubkey(dmInfo.toPubkey);
|
|
334
|
-
} catch {}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
if (!targetPubkey && currentState?.data?.senderPubkey) {
|
|
338
|
-
targetPubkey = currentState.data.senderPubkey;
|
|
339
|
-
}
|
|
340
|
-
if (!targetPubkey) {
|
|
341
|
-
if (callback) {
|
|
342
|
-
callback({
|
|
343
|
-
text: "I couldn't determine who to send the message to. Please specify a pubkey.",
|
|
344
|
-
source: "nostr"
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
return { success: false, error: "Could not determine target pubkey" };
|
|
348
|
-
}
|
|
349
|
-
const chunks = splitMessageForNostr(dmInfo.text);
|
|
350
|
-
let lastResult;
|
|
351
|
-
for (const chunk of chunks) {
|
|
352
|
-
const result = await nostrService.sendDm({
|
|
353
|
-
toPubkey: targetPubkey,
|
|
354
|
-
text: chunk
|
|
355
|
-
});
|
|
356
|
-
if (!result.success) {
|
|
357
|
-
if (callback) {
|
|
358
|
-
callback({
|
|
359
|
-
text: `Failed to send message: ${result.error}`,
|
|
360
|
-
source: "nostr"
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
return { success: false, error: result.error };
|
|
364
|
-
}
|
|
365
|
-
lastResult = { eventId: result.eventId, relays: result.relays };
|
|
366
|
-
logger.debug(`Sent Nostr DM: ${result.eventId}`);
|
|
367
|
-
}
|
|
368
|
-
if (callback) {
|
|
369
|
-
callback({
|
|
370
|
-
text: "Message sent successfully.",
|
|
371
|
-
source: message.content.source
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
return {
|
|
375
|
-
success: true,
|
|
376
|
-
data: {
|
|
377
|
-
toPubkey: targetPubkey,
|
|
378
|
-
eventId: lastResult?.eventId,
|
|
379
|
-
relays: lastResult?.relays,
|
|
380
|
-
chunksCount: chunks.length
|
|
381
|
-
}
|
|
382
|
-
};
|
|
383
|
-
},
|
|
384
|
-
examples: [
|
|
385
|
-
[
|
|
386
|
-
{
|
|
387
|
-
name: "{{user1}}",
|
|
388
|
-
content: { text: "Send them a message saying 'Hello!'" }
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
name: "{{agent}}",
|
|
392
|
-
content: {
|
|
393
|
-
text: "I'll send that DM via Nostr.",
|
|
394
|
-
actions: ["NOSTR_SEND_DM"]
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
],
|
|
398
|
-
[
|
|
399
|
-
{
|
|
400
|
-
name: "{{user1}}",
|
|
401
|
-
content: { text: "Message npub1abc... saying 'Thanks for the zap!'" }
|
|
402
|
-
},
|
|
403
|
-
{
|
|
404
|
-
name: "{{agent}}",
|
|
405
|
-
content: {
|
|
406
|
-
text: "I'll send that message to the specified pubkey.",
|
|
407
|
-
actions: ["NOSTR_SEND_DM"]
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
]
|
|
411
|
-
]
|
|
412
|
-
};
|
|
413
|
-
// src/providers/identityContext.ts
|
|
414
|
-
var identityContextProvider = {
|
|
415
|
-
name: "nostrIdentityContext",
|
|
416
|
-
description: "Provides information about the bot's Nostr identity",
|
|
417
|
-
get: async (runtime, message, state) => {
|
|
418
|
-
if (message.content.source !== "nostr") {
|
|
419
|
-
return {
|
|
420
|
-
data: {},
|
|
421
|
-
values: {},
|
|
422
|
-
text: ""
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
const nostrService = runtime.getService(NOSTR_SERVICE_NAME);
|
|
426
|
-
if (!nostrService || !nostrService.isConnected()) {
|
|
427
|
-
return {
|
|
428
|
-
data: { connected: false },
|
|
429
|
-
values: { connected: false },
|
|
430
|
-
text: ""
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
const agentName = state?.agentName || "The agent";
|
|
434
|
-
const publicKey = nostrService.getPublicKey();
|
|
435
|
-
const npub = nostrService.getNpub();
|
|
436
|
-
const relays = nostrService.getRelays();
|
|
437
|
-
const responseText = `${agentName} is connected to Nostr with pubkey ${npub}. ` + `Connected to ${relays.length} relay(s): ${relays.join(", ")}. ` + `Nostr is a decentralized social protocol using cryptographic keys for identity.`;
|
|
438
|
-
return {
|
|
439
|
-
data: {
|
|
440
|
-
publicKey,
|
|
441
|
-
npub,
|
|
442
|
-
relays,
|
|
443
|
-
relayCount: relays.length,
|
|
444
|
-
connected: true
|
|
445
|
-
},
|
|
446
|
-
values: {
|
|
447
|
-
publicKey,
|
|
448
|
-
npub,
|
|
449
|
-
relayCount: relays.length
|
|
450
|
-
},
|
|
451
|
-
text: responseText
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
// src/providers/senderContext.ts
|
|
456
|
-
var senderContextProvider = {
|
|
457
|
-
name: "nostrSenderContext",
|
|
458
|
-
description: "Provides information about the Nostr user in the current conversation",
|
|
459
|
-
get: async (runtime, message, state) => {
|
|
460
|
-
if (message.content.source !== "nostr") {
|
|
461
|
-
return {
|
|
462
|
-
data: {},
|
|
463
|
-
values: {},
|
|
464
|
-
text: ""
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
const nostrService = runtime.getService(NOSTR_SERVICE_NAME);
|
|
468
|
-
if (!nostrService || !nostrService.isConnected()) {
|
|
469
|
-
return {
|
|
470
|
-
data: { connected: false },
|
|
471
|
-
values: { connected: false },
|
|
472
|
-
text: ""
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
const agentName = state?.agentName || "The agent";
|
|
476
|
-
const senderPubkey = state?.data?.senderPubkey;
|
|
477
|
-
if (!senderPubkey) {
|
|
478
|
-
return {
|
|
479
|
-
data: { connected: true },
|
|
480
|
-
values: { connected: true },
|
|
481
|
-
text: ""
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
let senderNpub = "";
|
|
485
|
-
try {
|
|
486
|
-
senderNpub = pubkeyToNpub(senderPubkey);
|
|
487
|
-
} catch {}
|
|
488
|
-
const displayName = getPubkeyDisplayName(senderPubkey);
|
|
489
|
-
const responseText = `${agentName} is talking to ${displayName} on Nostr. ` + `Their pubkey is ${senderNpub || senderPubkey}. ` + `This is an encrypted direct message conversation using NIP-04.`;
|
|
490
|
-
return {
|
|
491
|
-
data: {
|
|
492
|
-
senderPubkey,
|
|
493
|
-
senderNpub,
|
|
494
|
-
displayName,
|
|
495
|
-
isEncrypted: true
|
|
496
|
-
},
|
|
497
|
-
values: {
|
|
498
|
-
senderPubkey,
|
|
499
|
-
senderNpub,
|
|
500
|
-
displayName
|
|
501
|
-
},
|
|
502
|
-
text: responseText
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
};
|
|
506
|
-
// src/service.ts
|
|
507
|
-
import {
|
|
508
|
-
logger as logger2,
|
|
509
|
-
Service
|
|
510
|
-
} from "@elizaos/core";
|
|
511
|
-
import {
|
|
512
|
-
finalizeEvent,
|
|
513
|
-
getPublicKey,
|
|
514
|
-
SimplePool,
|
|
515
|
-
verifyEvent
|
|
516
|
-
} from "nostr-tools";
|
|
517
|
-
import { decrypt, encrypt } from "nostr-tools/nip04";
|
|
518
|
-
class NostrService extends Service {
|
|
519
|
-
static serviceType = NOSTR_SERVICE_NAME;
|
|
520
|
-
capabilityDescription = "Provides Nostr protocol integration for encrypted direct messages";
|
|
521
|
-
settings = null;
|
|
522
|
-
pool = null;
|
|
523
|
-
privateKey = null;
|
|
524
|
-
connected = false;
|
|
525
|
-
seenEventIds = new Set;
|
|
526
|
-
static async start(runtime) {
|
|
527
|
-
logger2.info("Starting Nostr service...");
|
|
528
|
-
const service = new NostrService(runtime);
|
|
529
|
-
await service.initialize();
|
|
530
|
-
return service;
|
|
531
|
-
}
|
|
532
|
-
async initialize() {
|
|
533
|
-
this.settings = this.loadSettings();
|
|
534
|
-
this.validateSettings();
|
|
535
|
-
this.privateKey = validatePrivateKey(this.settings.privateKey);
|
|
536
|
-
this.pool = new SimplePool;
|
|
537
|
-
await this.startSubscription();
|
|
538
|
-
this.connected = true;
|
|
539
|
-
logger2.info(`Nostr service started (pubkey: ${this.settings.publicKey.slice(0, 16)}...)`);
|
|
540
|
-
this.runtime.emitEvent("NOSTR_CONNECTION_READY" /* CONNECTION_READY */, {
|
|
541
|
-
runtime: this.runtime,
|
|
542
|
-
service: this
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
async stop() {
|
|
546
|
-
logger2.info("Stopping Nostr service...");
|
|
547
|
-
this.connected = false;
|
|
548
|
-
if (this.pool) {
|
|
549
|
-
this.pool.close(this.settings?.relays || []);
|
|
550
|
-
this.pool = null;
|
|
551
|
-
}
|
|
552
|
-
this.privateKey = null;
|
|
553
|
-
this.seenEventIds.clear();
|
|
554
|
-
logger2.info("Nostr service stopped");
|
|
555
|
-
}
|
|
556
|
-
loadSettings() {
|
|
557
|
-
const runtime = this.runtime;
|
|
558
|
-
if (!runtime) {
|
|
559
|
-
throw new NostrConfigurationError("Runtime not initialized");
|
|
560
|
-
}
|
|
561
|
-
const privateKeySetting = runtime.getSetting("NOSTR_PRIVATE_KEY");
|
|
562
|
-
const privateKey = typeof privateKeySetting === "string" ? privateKeySetting : process.env.NOSTR_PRIVATE_KEY || "";
|
|
563
|
-
const relaysRawSetting = runtime.getSetting("NOSTR_RELAYS");
|
|
564
|
-
const relaysRaw = typeof relaysRawSetting === "string" ? relaysRawSetting : process.env.NOSTR_RELAYS || "";
|
|
565
|
-
const dmPolicySetting = runtime.getSetting("NOSTR_DM_POLICY");
|
|
566
|
-
const dmPolicy = typeof dmPolicySetting === "string" ? dmPolicySetting : process.env.NOSTR_DM_POLICY || "pairing";
|
|
567
|
-
const allowFromRawSetting = runtime.getSetting("NOSTR_ALLOW_FROM");
|
|
568
|
-
const allowFromRaw = typeof allowFromRawSetting === "string" ? allowFromRawSetting : process.env.NOSTR_ALLOW_FROM || "";
|
|
569
|
-
const enabledSetting = runtime.getSetting("NOSTR_ENABLED");
|
|
570
|
-
const enabled = typeof enabledSetting === "string" ? enabledSetting : process.env.NOSTR_ENABLED || "true";
|
|
571
|
-
const relays = relaysRaw ? relaysRaw.split(",").map((r) => r.trim()).filter(Boolean) : DEFAULT_NOSTR_RELAYS;
|
|
572
|
-
const allowFrom = allowFromRaw ? allowFromRaw.split(",").map((p) => {
|
|
573
|
-
try {
|
|
574
|
-
return normalizePubkey(p.trim());
|
|
575
|
-
} catch {
|
|
576
|
-
return p.trim();
|
|
577
|
-
}
|
|
578
|
-
}).filter(Boolean) : [];
|
|
579
|
-
let publicKey = "";
|
|
580
|
-
if (privateKey) {
|
|
581
|
-
try {
|
|
582
|
-
const sk = validatePrivateKey(privateKey);
|
|
583
|
-
publicKey = getPublicKey(sk);
|
|
584
|
-
} catch {}
|
|
585
|
-
}
|
|
586
|
-
return {
|
|
587
|
-
privateKey,
|
|
588
|
-
publicKey,
|
|
589
|
-
relays,
|
|
590
|
-
dmPolicy,
|
|
591
|
-
allowFrom,
|
|
592
|
-
enabled: enabled.toLowerCase() !== "false"
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
validateSettings() {
|
|
596
|
-
const settings = this.settings;
|
|
597
|
-
if (!settings) {
|
|
598
|
-
throw new NostrConfigurationError("Settings not loaded");
|
|
599
|
-
}
|
|
600
|
-
if (!settings.privateKey) {
|
|
601
|
-
throw new NostrConfigurationError("NOSTR_PRIVATE_KEY is required", "NOSTR_PRIVATE_KEY");
|
|
602
|
-
}
|
|
603
|
-
if (!settings.publicKey) {
|
|
604
|
-
throw new NostrConfigurationError("Invalid private key - could not derive public key", "NOSTR_PRIVATE_KEY");
|
|
605
|
-
}
|
|
606
|
-
if (settings.relays.length === 0) {
|
|
607
|
-
throw new NostrConfigurationError("At least one relay is required", "NOSTR_RELAYS");
|
|
608
|
-
}
|
|
609
|
-
for (const relay of settings.relays) {
|
|
610
|
-
if (!relay.startsWith("wss://") && !relay.startsWith("ws://")) {
|
|
611
|
-
throw new NostrConfigurationError(`Invalid relay URL: ${relay}`, "NOSTR_RELAYS");
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
async startSubscription() {
|
|
616
|
-
const settings = this.settings;
|
|
617
|
-
const pool = this.pool;
|
|
618
|
-
const privateKey = this.privateKey;
|
|
619
|
-
if (!settings || !pool || !privateKey) {
|
|
620
|
-
throw new NostrConfigurationError("Service not properly initialized");
|
|
621
|
-
}
|
|
622
|
-
const pk = settings.publicKey;
|
|
623
|
-
const since = Math.floor(Date.now() / 1000) - 120;
|
|
624
|
-
const filter = { kinds: [4], "#p": [pk], since };
|
|
625
|
-
pool.subscribeMany(settings.relays, [filter], {
|
|
626
|
-
onevent: async (event) => {
|
|
627
|
-
await this.handleEvent(event);
|
|
628
|
-
},
|
|
629
|
-
oneose: () => {
|
|
630
|
-
logger2.debug("Nostr EOSE received - initial sync complete");
|
|
631
|
-
}
|
|
632
|
-
});
|
|
633
|
-
logger2.info(`Subscribed to ${settings.relays.length} relay(s)`);
|
|
634
|
-
}
|
|
635
|
-
async handleEvent(event) {
|
|
636
|
-
const settings = this.settings;
|
|
637
|
-
const privateKey = this.privateKey;
|
|
638
|
-
if (!settings || !privateKey) {
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
if (this.seenEventIds.has(event.id)) {
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
this.seenEventIds.add(event.id);
|
|
645
|
-
if (this.seenEventIds.size > 1e4) {
|
|
646
|
-
const toDelete = Array.from(this.seenEventIds).slice(0, 5000);
|
|
647
|
-
for (const id of toDelete) {
|
|
648
|
-
this.seenEventIds.delete(id);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
if (event.pubkey === settings.publicKey) {
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
if (!verifyEvent(event)) {
|
|
655
|
-
logger2.warn(`Invalid signature on event ${event.id}`);
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
const isToUs = event.tags.some((t) => t[0] === "p" && t[1] === settings.publicKey);
|
|
659
|
-
if (!isToUs) {
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
if (settings.dmPolicy === "disabled") {
|
|
663
|
-
logger2.debug(`DM from ${event.pubkey} blocked - DMs disabled`);
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
if (settings.dmPolicy === "allowlist") {
|
|
667
|
-
const allowed = settings.allowFrom.includes(event.pubkey);
|
|
668
|
-
if (!allowed) {
|
|
669
|
-
logger2.debug(`DM from ${event.pubkey} blocked - not in allowlist`);
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
let plaintext;
|
|
674
|
-
try {
|
|
675
|
-
plaintext = decrypt(privateKey, event.pubkey, event.content);
|
|
676
|
-
} catch (err) {
|
|
677
|
-
logger2.warn(`Failed to decrypt DM from ${event.pubkey}: ${err}`);
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
logger2.debug(`Received DM from ${event.pubkey.slice(0, 8)}...: ${plaintext.slice(0, 50)}...`);
|
|
681
|
-
if (this.runtime) {
|
|
682
|
-
this.runtime.emitEvent("NOSTR_MESSAGE_RECEIVED" /* MESSAGE_RECEIVED */, {
|
|
683
|
-
runtime: this.runtime,
|
|
684
|
-
from: event.pubkey,
|
|
685
|
-
text: plaintext,
|
|
686
|
-
eventId: event.id,
|
|
687
|
-
createdAt: event.created_at
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
isConnected() {
|
|
692
|
-
return this.connected;
|
|
693
|
-
}
|
|
694
|
-
getPublicKey() {
|
|
695
|
-
return this.settings?.publicKey || "";
|
|
696
|
-
}
|
|
697
|
-
getNpub() {
|
|
698
|
-
const pk = this.getPublicKey();
|
|
699
|
-
return pk ? pubkeyToNpub(pk) : "";
|
|
700
|
-
}
|
|
701
|
-
getRelays() {
|
|
702
|
-
return this.settings?.relays || [];
|
|
703
|
-
}
|
|
704
|
-
async sendDm(options) {
|
|
705
|
-
const settings = this.settings;
|
|
706
|
-
const pool = this.pool;
|
|
707
|
-
const privateKey = this.privateKey;
|
|
708
|
-
if (!settings || !pool || !privateKey) {
|
|
709
|
-
return {
|
|
710
|
-
success: false,
|
|
711
|
-
error: "Service not initialized"
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
let toPubkey;
|
|
715
|
-
try {
|
|
716
|
-
toPubkey = normalizePubkey(options.toPubkey);
|
|
717
|
-
} catch (err) {
|
|
718
|
-
return {
|
|
719
|
-
success: false,
|
|
720
|
-
error: `Invalid target pubkey: ${err}`
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
let ciphertext;
|
|
724
|
-
try {
|
|
725
|
-
ciphertext = encrypt(privateKey, toPubkey, options.text);
|
|
726
|
-
} catch (err) {
|
|
727
|
-
return {
|
|
728
|
-
success: false,
|
|
729
|
-
error: `Encryption failed: ${err}`
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
const event = finalizeEvent({
|
|
733
|
-
kind: 4,
|
|
734
|
-
content: ciphertext,
|
|
735
|
-
tags: [["p", toPubkey]],
|
|
736
|
-
created_at: Math.floor(Date.now() / 1000)
|
|
737
|
-
}, privateKey);
|
|
738
|
-
const successRelays = [];
|
|
739
|
-
const errors = [];
|
|
740
|
-
for (const relay of settings.relays) {
|
|
741
|
-
try {
|
|
742
|
-
await pool.publish([relay], event);
|
|
743
|
-
successRelays.push(relay);
|
|
744
|
-
} catch (err) {
|
|
745
|
-
errors.push(`${relay}: ${err}`);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
if (successRelays.length === 0) {
|
|
749
|
-
return {
|
|
750
|
-
success: false,
|
|
751
|
-
error: `Failed to publish to any relay: ${errors.join("; ")}`
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
logger2.debug(`DM sent to ${toPubkey.slice(0, 8)}... via ${successRelays.length} relay(s)`);
|
|
755
|
-
if (this.runtime) {
|
|
756
|
-
this.runtime.emitEvent("NOSTR_MESSAGE_SENT" /* MESSAGE_SENT */, {
|
|
757
|
-
runtime: this.runtime,
|
|
758
|
-
to: toPubkey,
|
|
759
|
-
eventId: event.id,
|
|
760
|
-
relays: successRelays
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
return {
|
|
764
|
-
success: true,
|
|
765
|
-
eventId: event.id,
|
|
766
|
-
relays: successRelays
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
async publishProfile(profile) {
|
|
770
|
-
const settings = this.settings;
|
|
771
|
-
const pool = this.pool;
|
|
772
|
-
const privateKey = this.privateKey;
|
|
773
|
-
if (!settings || !pool || !privateKey) {
|
|
774
|
-
return {
|
|
775
|
-
success: false,
|
|
776
|
-
error: "Service not initialized"
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
const content = JSON.stringify({
|
|
780
|
-
name: profile.name,
|
|
781
|
-
display_name: profile.displayName,
|
|
782
|
-
about: profile.about,
|
|
783
|
-
picture: profile.picture,
|
|
784
|
-
banner: profile.banner,
|
|
785
|
-
nip05: profile.nip05,
|
|
786
|
-
lud16: profile.lud16,
|
|
787
|
-
website: profile.website
|
|
788
|
-
});
|
|
789
|
-
const event = finalizeEvent({
|
|
790
|
-
kind: 0,
|
|
791
|
-
content,
|
|
792
|
-
tags: [],
|
|
793
|
-
created_at: Math.floor(Date.now() / 1000)
|
|
794
|
-
}, privateKey);
|
|
795
|
-
const successRelays = [];
|
|
796
|
-
const errors = [];
|
|
797
|
-
for (const relay of settings.relays) {
|
|
798
|
-
try {
|
|
799
|
-
await pool.publish([relay], event);
|
|
800
|
-
successRelays.push(relay);
|
|
801
|
-
} catch (err) {
|
|
802
|
-
errors.push(`${relay}: ${err}`);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
if (successRelays.length === 0) {
|
|
806
|
-
return {
|
|
807
|
-
success: false,
|
|
808
|
-
error: `Failed to publish profile to any relay: ${errors.join("; ")}`
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
logger2.info(`Profile published via ${successRelays.length} relay(s)`);
|
|
812
|
-
if (this.runtime) {
|
|
813
|
-
this.runtime.emitEvent("NOSTR_PROFILE_PUBLISHED" /* PROFILE_PUBLISHED */, {
|
|
814
|
-
runtime: this.runtime,
|
|
815
|
-
eventId: event.id,
|
|
816
|
-
relays: successRelays
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
return {
|
|
820
|
-
success: true,
|
|
821
|
-
eventId: event.id,
|
|
822
|
-
relays: successRelays
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
getSettings() {
|
|
826
|
-
return this.settings;
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
// src/index.ts
|
|
830
|
-
var nostrPlugin = {
|
|
831
|
-
name: "nostr",
|
|
832
|
-
description: "Nostr decentralized messaging plugin for ElizaOS agents",
|
|
833
|
-
services: [NostrService],
|
|
834
|
-
actions: [sendDm, publishProfile],
|
|
835
|
-
providers: [identityContextProvider, senderContextProvider],
|
|
836
|
-
tests: [],
|
|
837
|
-
init: async (config, _runtime) => {
|
|
838
|
-
logger3.info("Initializing Nostr plugin...");
|
|
839
|
-
const hasPrivateKey = Boolean(config.NOSTR_PRIVATE_KEY || process.env.NOSTR_PRIVATE_KEY);
|
|
840
|
-
const relaysRaw = config.NOSTR_RELAYS || process.env.NOSTR_RELAYS || "";
|
|
841
|
-
const relays = relaysRaw ? relaysRaw.split(",").length : DEFAULT_NOSTR_RELAYS.length;
|
|
842
|
-
logger3.info(`Nostr plugin configuration:`);
|
|
843
|
-
logger3.info(` - Private key configured: ${hasPrivateKey ? "Yes" : "No"}`);
|
|
844
|
-
logger3.info(` - Relays: ${relays} relay(s)`);
|
|
845
|
-
logger3.info(` - DM policy: ${config.NOSTR_DM_POLICY || process.env.NOSTR_DM_POLICY || "pairing"}`);
|
|
846
|
-
if (!hasPrivateKey) {
|
|
847
|
-
logger3.warn("Nostr private key not configured. Set NOSTR_PRIVATE_KEY (hex or nsec format).");
|
|
848
|
-
}
|
|
849
|
-
logger3.info("Nostr plugin initialized");
|
|
850
|
-
}
|
|
851
|
-
};
|
|
852
|
-
var src_default = nostrPlugin;
|
|
853
|
-
export {
|
|
854
|
-
validatePrivateKey,
|
|
855
|
-
splitMessageForNostr,
|
|
856
|
-
senderContextProvider,
|
|
857
|
-
sendDm,
|
|
858
|
-
publishProfile,
|
|
859
|
-
pubkeyToNpub,
|
|
860
|
-
normalizePubkey,
|
|
861
|
-
isValidPubkey,
|
|
862
|
-
identityContextProvider,
|
|
863
|
-
getPubkeyDisplayName,
|
|
864
|
-
src_default as default,
|
|
865
|
-
NostrService,
|
|
866
|
-
NostrRelayError,
|
|
867
|
-
NostrPluginError,
|
|
868
|
-
NostrEventTypes,
|
|
869
|
-
NostrCryptoError,
|
|
870
|
-
NostrConfigurationError,
|
|
871
|
-
NOSTR_SERVICE_NAME,
|
|
872
|
-
MAX_NOSTR_MESSAGE_LENGTH,
|
|
873
|
-
DEFAULT_NOSTR_RELAYS
|
|
874
|
-
};
|
|
875
|
-
|
|
876
|
-
//# debugId=210FEFBBF268CA8064756E2164756E21
|