@nekosuneprojects/vector-sdk 1.0.2 → 1.0.3
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/bot.d.ts +43 -0
- package/dist/bot.js +210 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.js +42 -0
- package/dist/crypto.d.ts +9 -0
- package/dist/crypto.js +19 -0
- package/dist/demo.d.ts +49 -0
- package/dist/demo.js +210 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/keys.d.ts +7 -0
- package/dist/keys.js +33 -0
- package/dist/metadata.d.ts +42 -0
- package/dist/metadata.js +66 -0
- package/dist/subscription.d.ts +10 -0
- package/dist/subscription.js +20 -0
- package/dist/upload.d.ts +20 -0
- package/dist/upload.js +93 -0
- package/package.json +2 -1
package/dist/bot.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ClientConfig, VectorClient } from './client.js';
|
|
2
|
+
import { ProgressCallback } from './upload.js';
|
|
3
|
+
export interface ImageMetadata {
|
|
4
|
+
blurhash: string;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class AttachmentFile {
|
|
9
|
+
bytes: Buffer;
|
|
10
|
+
extension: string;
|
|
11
|
+
imgMeta?: ImageMetadata | undefined;
|
|
12
|
+
constructor(bytes: Buffer, extension: string, imgMeta?: ImageMetadata | undefined);
|
|
13
|
+
static fromPath(path: string): Promise<AttachmentFile>;
|
|
14
|
+
static fromBytes(bytes: Buffer, extension?: string): Promise<AttachmentFile>;
|
|
15
|
+
}
|
|
16
|
+
export declare function loadFile(path: string): Promise<AttachmentFile>;
|
|
17
|
+
export declare function createProgressCallback(): ProgressCallback;
|
|
18
|
+
export declare class VectorBot {
|
|
19
|
+
name: string;
|
|
20
|
+
displayName: string;
|
|
21
|
+
about: string;
|
|
22
|
+
picture: string;
|
|
23
|
+
banner: string;
|
|
24
|
+
nip05: string;
|
|
25
|
+
lud16: string;
|
|
26
|
+
readonly publicKey: string;
|
|
27
|
+
readonly privateKey: string;
|
|
28
|
+
readonly privateKeyBytes: Uint8Array;
|
|
29
|
+
readonly client: VectorClient;
|
|
30
|
+
private constructor();
|
|
31
|
+
static quick(privateKey: string): Promise<VectorBot>;
|
|
32
|
+
static new(privateKey: string, name: string, displayName: string, about: string, picture: string, banner: string, nip05: string, lud16: string, clientConfig?: ClientConfig): Promise<VectorBot>;
|
|
33
|
+
getChat(recipient: string): Channel;
|
|
34
|
+
}
|
|
35
|
+
export declare class Channel {
|
|
36
|
+
readonly recipient: string;
|
|
37
|
+
readonly baseBot: VectorBot;
|
|
38
|
+
constructor(recipient: string, baseBot: VectorBot);
|
|
39
|
+
sendPrivateMessage(message: string): Promise<boolean>;
|
|
40
|
+
sendReaction(referenceId: string, emoji: string): Promise<boolean>;
|
|
41
|
+
sendTypingIndicator(): Promise<boolean>;
|
|
42
|
+
sendPrivateFile(file?: AttachmentFile): Promise<boolean>;
|
|
43
|
+
}
|
package/dist/bot.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import mime from 'mime-types';
|
|
3
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
4
|
+
import { finalizeEvent } from 'nostr-tools/pure';
|
|
5
|
+
import * as kinds from 'nostr-tools/kinds';
|
|
6
|
+
import * as nip04 from 'nostr-tools/nip04';
|
|
7
|
+
import * as nip59 from 'nostr-tools/nip59';
|
|
8
|
+
import { buildClient } from './client.js';
|
|
9
|
+
import { createMetadata } from './metadata.js';
|
|
10
|
+
import { calculateFileHash, encryptData, generateEncryptionParams } from './crypto.js';
|
|
11
|
+
import { getServerConfig, uploadDataWithProgress } from './upload.js';
|
|
12
|
+
import { normalizePublicKey } from './keys.js';
|
|
13
|
+
export class AttachmentFile {
|
|
14
|
+
constructor(bytes, extension, imgMeta) {
|
|
15
|
+
this.bytes = bytes;
|
|
16
|
+
this.extension = extension;
|
|
17
|
+
this.imgMeta = imgMeta;
|
|
18
|
+
}
|
|
19
|
+
static async fromPath(path) {
|
|
20
|
+
const bytes = await fs.readFile(path);
|
|
21
|
+
return AttachmentFile.fromBytes(bytes);
|
|
22
|
+
}
|
|
23
|
+
static async fromBytes(bytes, extension) {
|
|
24
|
+
const resolvedExtension = extension ?? (await inferExtensionFromBytes(bytes));
|
|
25
|
+
return new AttachmentFile(bytes, resolvedExtension);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function loadFile(path) {
|
|
29
|
+
return AttachmentFile.fromPath(path);
|
|
30
|
+
}
|
|
31
|
+
async function inferExtensionFromBytes(bytes) {
|
|
32
|
+
const fileType = await fileTypeFromBuffer(bytes);
|
|
33
|
+
return fileType?.ext ?? 'bin';
|
|
34
|
+
}
|
|
35
|
+
export function createProgressCallback() {
|
|
36
|
+
return (percentage) => {
|
|
37
|
+
if (percentage !== null) {
|
|
38
|
+
console.log(`Upload progress: ${percentage}%`);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function sanitizeUrl(candidate, fallback) {
|
|
43
|
+
try {
|
|
44
|
+
return new URL(candidate).toString();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error('Invalid URL provided, falling back', error);
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export class VectorBot {
|
|
52
|
+
constructor(privateKey, name, displayName, about, picture, banner, nip05, lud16, client) {
|
|
53
|
+
this.name = name;
|
|
54
|
+
this.displayName = displayName;
|
|
55
|
+
this.about = about;
|
|
56
|
+
this.picture = picture;
|
|
57
|
+
this.banner = banner;
|
|
58
|
+
this.nip05 = nip05;
|
|
59
|
+
this.lud16 = lud16;
|
|
60
|
+
this.privateKey = privateKey;
|
|
61
|
+
this.privateKeyBytes = client.privateKeyBytes;
|
|
62
|
+
this.publicKey = client.publicKey;
|
|
63
|
+
this.client = client;
|
|
64
|
+
}
|
|
65
|
+
static async quick(privateKey) {
|
|
66
|
+
return VectorBot.new(privateKey, 'vector bot', 'Vector Bot', 'vector bot created with quick', 'https://example.com/avatar.png', 'https://example.com/banner.png', 'example@example.com', 'example@example.com');
|
|
67
|
+
}
|
|
68
|
+
static async new(privateKey, name, displayName, about, picture, banner, nip05, lud16, clientConfig) {
|
|
69
|
+
const resolvedPicture = sanitizeUrl(picture, 'https://example.com/avatar.png');
|
|
70
|
+
const resolvedBanner = sanitizeUrl(banner, 'https://example.com/banner.png');
|
|
71
|
+
const client = buildClient(privateKey, clientConfig);
|
|
72
|
+
const metadata = createMetadata(name, displayName, about, resolvedPicture, resolvedBanner, nip05, lud16);
|
|
73
|
+
try {
|
|
74
|
+
await client.setMetadata(metadata);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error('Failed to set metadata', error);
|
|
78
|
+
}
|
|
79
|
+
return new VectorBot(client.privateKey, name, displayName, about, resolvedPicture, resolvedBanner, nip05, lud16, client);
|
|
80
|
+
}
|
|
81
|
+
getChat(recipient) {
|
|
82
|
+
return new Channel(recipient, this);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export class Channel {
|
|
86
|
+
constructor(recipient, baseBot) {
|
|
87
|
+
this.recipient = normalizePublicKey(recipient);
|
|
88
|
+
this.baseBot = baseBot;
|
|
89
|
+
}
|
|
90
|
+
async sendPrivateMessage(message) {
|
|
91
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
92
|
+
const tags = [
|
|
93
|
+
['p', this.recipient],
|
|
94
|
+
['ms', (Date.now() % 1000).toString()],
|
|
95
|
+
];
|
|
96
|
+
let sent = false;
|
|
97
|
+
try {
|
|
98
|
+
const payload = await nip04.encrypt(this.baseBot.privateKey, this.recipient, message);
|
|
99
|
+
const event = finalizeEvent({
|
|
100
|
+
kind: kinds.EncryptedDirectMessage,
|
|
101
|
+
created_at: createdAt,
|
|
102
|
+
tags,
|
|
103
|
+
content: payload,
|
|
104
|
+
}, this.baseBot.privateKeyBytes);
|
|
105
|
+
await this.baseBot.client.publishEvent(event);
|
|
106
|
+
sent = true;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error('Failed to send NIP-04 private message', error);
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const rumor = {
|
|
113
|
+
kind: kinds.PrivateDirectMessage,
|
|
114
|
+
created_at: createdAt,
|
|
115
|
+
tags,
|
|
116
|
+
content: message,
|
|
117
|
+
};
|
|
118
|
+
const wrapped = nip59.wrapEvent(rumor, this.baseBot.privateKeyBytes, this.recipient);
|
|
119
|
+
await this.baseBot.client.publishEvent(wrapped);
|
|
120
|
+
sent = true;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.error('Failed to send NIP-59 gift-wrap', error);
|
|
124
|
+
}
|
|
125
|
+
return sent;
|
|
126
|
+
}
|
|
127
|
+
async sendReaction(referenceId, emoji) {
|
|
128
|
+
try {
|
|
129
|
+
const event = finalizeEvent({
|
|
130
|
+
kind: kinds.Reaction,
|
|
131
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
132
|
+
tags: [
|
|
133
|
+
['e', referenceId],
|
|
134
|
+
['p', this.recipient],
|
|
135
|
+
['ms', (Date.now() % 1000).toString()],
|
|
136
|
+
],
|
|
137
|
+
content: emoji,
|
|
138
|
+
}, this.baseBot.privateKeyBytes);
|
|
139
|
+
await this.baseBot.client.publishEvent(event);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
console.error('Failed to send reaction', error);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async sendTypingIndicator() {
|
|
148
|
+
try {
|
|
149
|
+
const now = Math.floor(Date.now() / 1000);
|
|
150
|
+
const event = finalizeEvent({
|
|
151
|
+
kind: kinds.Application,
|
|
152
|
+
created_at: now,
|
|
153
|
+
tags: [
|
|
154
|
+
['p', this.recipient],
|
|
155
|
+
['ms', (Date.now() % 1000).toString()],
|
|
156
|
+
['expiration', (now + 3600).toString()],
|
|
157
|
+
],
|
|
158
|
+
content: 'typing',
|
|
159
|
+
}, this.baseBot.privateKeyBytes);
|
|
160
|
+
await this.baseBot.client.publishEvent(event);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error('Failed to send typing indicator', error);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async sendPrivateFile(file) {
|
|
169
|
+
if (!file) {
|
|
170
|
+
console.error('No file provided for sendPrivateFile');
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const rawMimeType = mime.lookup(file.extension);
|
|
175
|
+
const mimeType = typeof rawMimeType === 'string' ? rawMimeType : 'application/octet-stream';
|
|
176
|
+
const params = generateEncryptionParams();
|
|
177
|
+
const encrypted = encryptData(file.bytes, params);
|
|
178
|
+
const fileHash = calculateFileHash(file.bytes);
|
|
179
|
+
const serverConfig = await getServerConfig();
|
|
180
|
+
const progressCallback = createProgressCallback();
|
|
181
|
+
const url = await uploadDataWithProgress(this.baseBot.privateKey, serverConfig, encrypted, mimeType, undefined, progressCallback);
|
|
182
|
+
const tags = [
|
|
183
|
+
['p', this.recipient],
|
|
184
|
+
['file-type', mimeType],
|
|
185
|
+
['size', encrypted.length.toString()],
|
|
186
|
+
['encryption-algorithm', 'aes-gcm'],
|
|
187
|
+
['decryption-key', params.key],
|
|
188
|
+
['decryption-nonce', params.nonce],
|
|
189
|
+
['ox', fileHash],
|
|
190
|
+
['ms', (Date.now() % 1000).toString()],
|
|
191
|
+
];
|
|
192
|
+
if (file.imgMeta) {
|
|
193
|
+
tags.push(['blurhash', file.imgMeta.blurhash]);
|
|
194
|
+
tags.push(['dim', `${file.imgMeta.width}x${file.imgMeta.height}`]);
|
|
195
|
+
}
|
|
196
|
+
const event = finalizeEvent({
|
|
197
|
+
kind: kinds.FileMessage,
|
|
198
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
199
|
+
tags,
|
|
200
|
+
content: url,
|
|
201
|
+
}, this.baseBot.privateKeyBytes);
|
|
202
|
+
await this.baseBot.client.publishEvent(event);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.error('Failed to send private file', error);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SimplePool, Event } from 'nostr-tools';
|
|
2
|
+
import type { Metadata } from './metadata.js';
|
|
3
|
+
export interface ClientConfig {
|
|
4
|
+
proxy?: string;
|
|
5
|
+
defaultRelays?: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare class VectorClient {
|
|
8
|
+
readonly pool: SimplePool;
|
|
9
|
+
readonly relays: string[];
|
|
10
|
+
readonly publicKey: string;
|
|
11
|
+
readonly privateKey: string;
|
|
12
|
+
readonly privateKeyBytes: Uint8Array;
|
|
13
|
+
constructor(keys: string, config?: ClientConfig);
|
|
14
|
+
setMetadata(metadata: Metadata): Promise<void>;
|
|
15
|
+
publishEvent(event: Event, relays?: string[]): Promise<void>;
|
|
16
|
+
private publish;
|
|
17
|
+
}
|
|
18
|
+
export declare function buildClient(keys: string, config?: ClientConfig): VectorClient;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SimplePool } from 'nostr-tools';
|
|
2
|
+
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
|
|
3
|
+
import { normalizePrivateKey } from './keys.js';
|
|
4
|
+
const DEFAULT_RELAYS = [
|
|
5
|
+
'wss://jskitty.cat/nostr',
|
|
6
|
+
'wss://relay.damus.io',
|
|
7
|
+
'wss://auth.nostr1.com',
|
|
8
|
+
'wss://nostr.computingcache.com',
|
|
9
|
+
];
|
|
10
|
+
export class VectorClient {
|
|
11
|
+
constructor(keys, config) {
|
|
12
|
+
this.pool = new SimplePool();
|
|
13
|
+
const normalized = normalizePrivateKey(keys);
|
|
14
|
+
this.privateKey = normalized.hex;
|
|
15
|
+
this.privateKeyBytes = normalized.bytes;
|
|
16
|
+
this.publicKey = getPublicKey(this.privateKeyBytes);
|
|
17
|
+
this.relays = config?.defaultRelays ?? DEFAULT_RELAYS;
|
|
18
|
+
}
|
|
19
|
+
async setMetadata(metadata) {
|
|
20
|
+
const event = finalizeEvent({
|
|
21
|
+
kind: 0,
|
|
22
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
23
|
+
tags: [],
|
|
24
|
+
content: JSON.stringify(metadata),
|
|
25
|
+
}, this.privateKeyBytes);
|
|
26
|
+
await this.publish(event, this.relays);
|
|
27
|
+
}
|
|
28
|
+
async publishEvent(event, relays) {
|
|
29
|
+
return this.publish(event, relays ?? this.relays);
|
|
30
|
+
}
|
|
31
|
+
async publish(event, relays) {
|
|
32
|
+
const results = await Promise.allSettled(this.pool.publish(relays, event));
|
|
33
|
+
const rejected = results.find((result) => result.status === 'rejected');
|
|
34
|
+
if (rejected && rejected.status === 'rejected') {
|
|
35
|
+
const reason = rejected.reason instanceof Error ? rejected.reason : new Error(String(rejected.reason));
|
|
36
|
+
throw reason;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function buildClient(keys, config) {
|
|
41
|
+
return new VectorClient(keys, config);
|
|
42
|
+
}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface EncryptionParams {
|
|
2
|
+
key: string;
|
|
3
|
+
nonce: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class CryptoError extends Error {
|
|
6
|
+
}
|
|
7
|
+
export declare function generateEncryptionParams(): EncryptionParams;
|
|
8
|
+
export declare function encryptData(data: Buffer | Uint8Array, params: EncryptionParams): Buffer;
|
|
9
|
+
export declare function calculateFileHash(data: Buffer): string;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { randomBytes, createCipheriv, createHash } from 'crypto';
|
|
2
|
+
export class CryptoError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export function generateEncryptionParams() {
|
|
5
|
+
const key = randomBytes(32).toString('hex');
|
|
6
|
+
const nonce = randomBytes(16).toString('hex');
|
|
7
|
+
return { key, nonce };
|
|
8
|
+
}
|
|
9
|
+
export function encryptData(data, params) {
|
|
10
|
+
const key = Buffer.from(params.key, 'hex');
|
|
11
|
+
const nonce = Buffer.from(params.nonce, 'hex');
|
|
12
|
+
const cipher = createCipheriv('aes-256-gcm', key, nonce);
|
|
13
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
14
|
+
const tag = cipher.getAuthTag();
|
|
15
|
+
return Buffer.concat([encrypted, tag]);
|
|
16
|
+
}
|
|
17
|
+
export function calculateFileHash(data) {
|
|
18
|
+
return createHash('sha256').update(data).digest('hex');
|
|
19
|
+
}
|
package/dist/demo.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import type { Event } from 'nostr-tools';
|
|
3
|
+
export type BotProfile = {
|
|
4
|
+
name: string;
|
|
5
|
+
displayName: string;
|
|
6
|
+
about: string;
|
|
7
|
+
picture: string;
|
|
8
|
+
banner: string;
|
|
9
|
+
nip05: string;
|
|
10
|
+
lud16: string;
|
|
11
|
+
};
|
|
12
|
+
export type BotClientOptions = {
|
|
13
|
+
privateKey: string;
|
|
14
|
+
relays: string[];
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
profile?: Partial<BotProfile>;
|
|
17
|
+
reconnect?: boolean;
|
|
18
|
+
reconnectIntervalMs?: number;
|
|
19
|
+
};
|
|
20
|
+
export type MessageTags = {
|
|
21
|
+
pubkey: string;
|
|
22
|
+
kind: number;
|
|
23
|
+
rawEvent: Event;
|
|
24
|
+
wrapped?: boolean;
|
|
25
|
+
displayName?: string;
|
|
26
|
+
};
|
|
27
|
+
export declare class VectorBotClient extends EventEmitter {
|
|
28
|
+
private bot?;
|
|
29
|
+
private giftWrapSubscription?;
|
|
30
|
+
private dmSubscription?;
|
|
31
|
+
private readonly options;
|
|
32
|
+
private readonly profileCache;
|
|
33
|
+
private readonly connectionState;
|
|
34
|
+
private readonly reconnectingRelays;
|
|
35
|
+
private connectionMonitor?;
|
|
36
|
+
constructor(options: BotClientOptions);
|
|
37
|
+
connect(): Promise<void>;
|
|
38
|
+
sendMessage(recipient: string, message: string): Promise<boolean>;
|
|
39
|
+
sendFile(recipient: string, filePath: string): Promise<boolean>;
|
|
40
|
+
close(): void;
|
|
41
|
+
private startConnectionMonitor;
|
|
42
|
+
private reconnectRelay;
|
|
43
|
+
private setupSubscriptions;
|
|
44
|
+
private handleGiftWrap;
|
|
45
|
+
private handleDirectMessage;
|
|
46
|
+
private emitMessage;
|
|
47
|
+
private getProfile;
|
|
48
|
+
private log;
|
|
49
|
+
}
|
package/dist/demo.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import * as nip04 from 'nostr-tools/nip04';
|
|
3
|
+
import * as nip59 from 'nostr-tools/nip59';
|
|
4
|
+
import { EncryptedDirectMessage, PrivateDirectMessage } from 'nostr-tools/kinds';
|
|
5
|
+
import { VectorBot } from './bot.js';
|
|
6
|
+
import { createGiftWrapSubscription } from './subscription.js';
|
|
7
|
+
import { loadFile } from './bot.js';
|
|
8
|
+
export class VectorBotClient extends EventEmitter {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super();
|
|
11
|
+
this.profileCache = new Map();
|
|
12
|
+
this.connectionState = new Map();
|
|
13
|
+
this.reconnectingRelays = new Set();
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
async connect() {
|
|
17
|
+
if (!this.options.privateKey) {
|
|
18
|
+
throw new Error('Missing private key for bot client');
|
|
19
|
+
}
|
|
20
|
+
if (!this.options.relays.length) {
|
|
21
|
+
throw new Error('At least one relay is required');
|
|
22
|
+
}
|
|
23
|
+
const profile = {
|
|
24
|
+
name: 'vector-bot',
|
|
25
|
+
displayName: 'Vector Bot',
|
|
26
|
+
about: 'Vector bot created with the SDK',
|
|
27
|
+
picture: 'https://example.com/avatar.png',
|
|
28
|
+
banner: 'https://example.com/banner.png',
|
|
29
|
+
nip05: '',
|
|
30
|
+
lud16: '',
|
|
31
|
+
...this.options.profile,
|
|
32
|
+
};
|
|
33
|
+
const bot = await VectorBot.new(this.options.privateKey, profile.name, profile.displayName, profile.about, profile.picture, profile.banner, profile.nip05, profile.lud16, { defaultRelays: this.options.relays });
|
|
34
|
+
this.bot = bot;
|
|
35
|
+
this.log('Connected. Bot public key:', bot.publicKey);
|
|
36
|
+
this.setupSubscriptions(bot);
|
|
37
|
+
this.startConnectionMonitor(bot);
|
|
38
|
+
this.emit('ready', {
|
|
39
|
+
pubkey: bot.publicKey,
|
|
40
|
+
profile: {
|
|
41
|
+
name: profile.name,
|
|
42
|
+
displayName: profile.displayName,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async sendMessage(recipient, message) {
|
|
47
|
+
if (!this.bot) {
|
|
48
|
+
throw new Error('Bot is not connected');
|
|
49
|
+
}
|
|
50
|
+
const channel = this.bot.getChat(recipient);
|
|
51
|
+
const sent = await channel.sendPrivateMessage(message);
|
|
52
|
+
this.log('Sent message to', recipient, 'status:', sent);
|
|
53
|
+
return sent;
|
|
54
|
+
}
|
|
55
|
+
async sendFile(recipient, filePath) {
|
|
56
|
+
if (!this.bot) {
|
|
57
|
+
throw new Error('Bot is not connected');
|
|
58
|
+
}
|
|
59
|
+
const channel = this.bot.getChat(recipient);
|
|
60
|
+
const file = await loadFile(filePath);
|
|
61
|
+
const sent = await channel.sendPrivateFile(file);
|
|
62
|
+
this.log('Sent file to', recipient, 'status:', sent);
|
|
63
|
+
return sent;
|
|
64
|
+
}
|
|
65
|
+
close() {
|
|
66
|
+
if (!this.bot) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (this.connectionMonitor) {
|
|
70
|
+
clearInterval(this.connectionMonitor);
|
|
71
|
+
this.connectionMonitor = undefined;
|
|
72
|
+
}
|
|
73
|
+
this.giftWrapSubscription?.close('shutdown');
|
|
74
|
+
this.dmSubscription?.close('shutdown');
|
|
75
|
+
this.bot.client.pool.close(this.bot.client.relays);
|
|
76
|
+
}
|
|
77
|
+
startConnectionMonitor(bot) {
|
|
78
|
+
if (this.connectionMonitor) {
|
|
79
|
+
clearInterval(this.connectionMonitor);
|
|
80
|
+
}
|
|
81
|
+
const shouldReconnect = this.options.reconnect !== false;
|
|
82
|
+
const interval = this.options.reconnectIntervalMs ?? 15000;
|
|
83
|
+
this.connectionMonitor = setInterval(() => {
|
|
84
|
+
const status = bot.client.pool.listConnectionStatus();
|
|
85
|
+
for (const relay of bot.client.relays) {
|
|
86
|
+
const connected = status.get(relay) ?? false;
|
|
87
|
+
const previous = this.connectionState.get(relay);
|
|
88
|
+
this.connectionState.set(relay, connected);
|
|
89
|
+
if (previous === undefined) {
|
|
90
|
+
if (!connected) {
|
|
91
|
+
this.emit('disconnect', { relay, error: new Error('Relay disconnected') });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (!connected && previous) {
|
|
95
|
+
this.emit('disconnect', { relay, error: new Error('Relay disconnected') });
|
|
96
|
+
}
|
|
97
|
+
else if (connected && previous === false) {
|
|
98
|
+
this.emit('reconnect', { relay });
|
|
99
|
+
}
|
|
100
|
+
if (shouldReconnect && !connected) {
|
|
101
|
+
this.reconnectRelay(bot, relay);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}, interval);
|
|
105
|
+
}
|
|
106
|
+
async reconnectRelay(bot, relay) {
|
|
107
|
+
if (this.reconnectingRelays.has(relay)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.reconnectingRelays.add(relay);
|
|
111
|
+
try {
|
|
112
|
+
await bot.client.pool.ensureRelay(relay);
|
|
113
|
+
this.connectionState.set(relay, true);
|
|
114
|
+
this.emit('reconnect', { relay });
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this.emit('error', error);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
this.reconnectingRelays.delete(relay);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
setupSubscriptions(bot) {
|
|
124
|
+
const giftWrapFilter = createGiftWrapSubscription(bot.publicKey);
|
|
125
|
+
this.giftWrapSubscription = bot.client.pool.subscribe(bot.client.relays, giftWrapFilter, {
|
|
126
|
+
onevent: (event) => this.handleGiftWrap(bot, event),
|
|
127
|
+
onclose: (reasons) => {
|
|
128
|
+
this.log('Gift-wrap subscription closed:', reasons);
|
|
129
|
+
this.emit('disconnect', { relay: 'gift-wrap', error: new Error(reasons.join(', ')) });
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
const dmFilter = {
|
|
133
|
+
kinds: [EncryptedDirectMessage, PrivateDirectMessage],
|
|
134
|
+
'#p': [bot.publicKey],
|
|
135
|
+
};
|
|
136
|
+
this.dmSubscription = bot.client.pool.subscribe(bot.client.relays, dmFilter, {
|
|
137
|
+
onevent: (event) => this.handleDirectMessage(bot, event),
|
|
138
|
+
onclose: (reasons) => {
|
|
139
|
+
this.log('DM subscription closed:', reasons);
|
|
140
|
+
this.emit('disconnect', { relay: 'dm', error: new Error(reasons.join(', ')) });
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
handleGiftWrap(bot, event) {
|
|
145
|
+
try {
|
|
146
|
+
const rumor = nip59.unwrapEvent(event, bot.privateKeyBytes);
|
|
147
|
+
this.log('Gift-wrap rumor:', rumor);
|
|
148
|
+
if (rumor.kind === PrivateDirectMessage && rumor.content) {
|
|
149
|
+
this.emitMessage(bot, rumor.pubkey, rumor.kind, event, rumor.content, true);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
this.log('Failed to unwrap gift-wrap:', error);
|
|
154
|
+
this.emit('error', error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
handleDirectMessage(bot, event) {
|
|
158
|
+
if (event.kind !== EncryptedDirectMessage) {
|
|
159
|
+
this.log('Unhandled DM event:', event);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const message = nip04.decrypt(bot.privateKey, event.pubkey, event.content);
|
|
164
|
+
this.emitMessage(bot, event.pubkey, event.kind, event, message, false);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
this.log('Failed to decrypt NIP-04 DM:', error);
|
|
168
|
+
this.emit('error', error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async emitMessage(bot, pubkey, kind, rawEvent, content, wrapped) {
|
|
172
|
+
const profile = await this.getProfile(bot, pubkey);
|
|
173
|
+
this.emit('message', profile?.displayName || profile?.name || pubkey, {
|
|
174
|
+
pubkey,
|
|
175
|
+
kind,
|
|
176
|
+
rawEvent,
|
|
177
|
+
wrapped,
|
|
178
|
+
displayName: profile?.displayName || profile?.name,
|
|
179
|
+
}, content, false);
|
|
180
|
+
}
|
|
181
|
+
async getProfile(bot, pubkey) {
|
|
182
|
+
if (this.profileCache.has(pubkey)) {
|
|
183
|
+
return this.profileCache.get(pubkey);
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const event = await bot.client.pool.get(bot.client.relays, { kinds: [0], authors: [pubkey], limit: 1 });
|
|
187
|
+
if (event && event.content) {
|
|
188
|
+
const metadata = JSON.parse(event.content);
|
|
189
|
+
const profile = {
|
|
190
|
+
name: metadata.name,
|
|
191
|
+
displayName: metadata.displayName,
|
|
192
|
+
};
|
|
193
|
+
this.profileCache.set(pubkey, profile);
|
|
194
|
+
return profile;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
this.log('Failed to load profile for', pubkey, error);
|
|
199
|
+
this.emit('error', error);
|
|
200
|
+
}
|
|
201
|
+
this.profileCache.set(pubkey, {});
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
log(...args) {
|
|
205
|
+
if (!this.options.debug) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
console.log('[vector-bot]', ...args);
|
|
209
|
+
}
|
|
210
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/keys.d.ts
ADDED
package/dist/keys.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { nip19 } from 'nostr-tools';
|
|
2
|
+
import { bytesToHex, hexToBytes } from 'nostr-tools/utils';
|
|
3
|
+
export class KeyFormatError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export function normalizePrivateKey(privateKey) {
|
|
6
|
+
const trimmed = privateKey.trim();
|
|
7
|
+
if (trimmed.startsWith('nsec1')) {
|
|
8
|
+
const decoded = nip19.decode(trimmed);
|
|
9
|
+
if (decoded.type !== 'nsec') {
|
|
10
|
+
throw new KeyFormatError('Invalid nsec private key');
|
|
11
|
+
}
|
|
12
|
+
return { hex: bytesToHex(decoded.data), bytes: decoded.data };
|
|
13
|
+
}
|
|
14
|
+
if (!/^[0-9a-fA-F]{64}$/.test(trimmed)) {
|
|
15
|
+
throw new KeyFormatError('Private key must be a 32-byte hex string or nsec');
|
|
16
|
+
}
|
|
17
|
+
const hex = trimmed.toLowerCase();
|
|
18
|
+
return { hex, bytes: hexToBytes(hex) };
|
|
19
|
+
}
|
|
20
|
+
export function normalizePublicKey(publicKey) {
|
|
21
|
+
const trimmed = publicKey.trim();
|
|
22
|
+
if (trimmed.startsWith('npub1')) {
|
|
23
|
+
const decoded = nip19.decode(trimmed);
|
|
24
|
+
if (decoded.type !== 'npub') {
|
|
25
|
+
throw new KeyFormatError('Invalid npub public key');
|
|
26
|
+
}
|
|
27
|
+
return decoded.data;
|
|
28
|
+
}
|
|
29
|
+
if (!/^[0-9a-fA-F]{64}$/.test(trimmed)) {
|
|
30
|
+
throw new KeyFormatError('Public key must be a 32-byte hex string or npub');
|
|
31
|
+
}
|
|
32
|
+
return trimmed.toLowerCase();
|
|
33
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface Metadata {
|
|
2
|
+
name: string;
|
|
3
|
+
displayName: string;
|
|
4
|
+
about: string;
|
|
5
|
+
picture?: string;
|
|
6
|
+
banner?: string;
|
|
7
|
+
nip05?: string;
|
|
8
|
+
lud16?: string;
|
|
9
|
+
bot?: true;
|
|
10
|
+
}
|
|
11
|
+
export interface MetadataConfigFields {
|
|
12
|
+
name: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
about: string;
|
|
15
|
+
picture?: string;
|
|
16
|
+
banner?: string;
|
|
17
|
+
nip05?: string;
|
|
18
|
+
lud16?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class MetadataConfig {
|
|
21
|
+
name: string;
|
|
22
|
+
displayName: string;
|
|
23
|
+
about: string;
|
|
24
|
+
picture?: string | undefined;
|
|
25
|
+
banner?: string | undefined;
|
|
26
|
+
nip05?: string | undefined;
|
|
27
|
+
lud16?: string | undefined;
|
|
28
|
+
constructor(name: string, displayName: string, about: string, picture?: string | undefined, banner?: string | undefined, nip05?: string | undefined, lud16?: string | undefined);
|
|
29
|
+
build(): Metadata;
|
|
30
|
+
}
|
|
31
|
+
export declare class MetadataConfigBuilder {
|
|
32
|
+
private config;
|
|
33
|
+
name(value: string): this;
|
|
34
|
+
displayName(value: string): this;
|
|
35
|
+
about(value: string): this;
|
|
36
|
+
picture(value: string): this;
|
|
37
|
+
banner(value: string): this;
|
|
38
|
+
nip05(value: string): this;
|
|
39
|
+
lud16(value: string): this;
|
|
40
|
+
build(): Metadata;
|
|
41
|
+
}
|
|
42
|
+
export declare function createMetadata(name: string, displayName: string, about: string, picture?: string, banner?: string, nip05?: string, lud16?: string): Metadata;
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export class MetadataConfig {
|
|
2
|
+
constructor(name, displayName, about, picture, banner, nip05, lud16) {
|
|
3
|
+
this.name = name;
|
|
4
|
+
this.displayName = displayName;
|
|
5
|
+
this.about = about;
|
|
6
|
+
this.picture = picture;
|
|
7
|
+
this.banner = banner;
|
|
8
|
+
this.nip05 = nip05;
|
|
9
|
+
this.lud16 = lud16;
|
|
10
|
+
}
|
|
11
|
+
build() {
|
|
12
|
+
return {
|
|
13
|
+
name: this.name,
|
|
14
|
+
displayName: this.displayName,
|
|
15
|
+
about: this.about,
|
|
16
|
+
picture: this.picture,
|
|
17
|
+
banner: this.banner,
|
|
18
|
+
nip05: this.nip05,
|
|
19
|
+
lud16: this.lud16,
|
|
20
|
+
bot: true,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class MetadataConfigBuilder {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.config = {
|
|
27
|
+
name: '',
|
|
28
|
+
displayName: '',
|
|
29
|
+
about: '',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
name(value) {
|
|
33
|
+
this.config.name = value;
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
displayName(value) {
|
|
37
|
+
this.config.displayName = value;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
about(value) {
|
|
41
|
+
this.config.about = value;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
picture(value) {
|
|
45
|
+
this.config.picture = value;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
banner(value) {
|
|
49
|
+
this.config.banner = value;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
nip05(value) {
|
|
53
|
+
this.config.nip05 = value;
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
lud16(value) {
|
|
57
|
+
this.config.lud16 = value;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
build() {
|
|
61
|
+
return new MetadataConfig(this.config.name, this.config.displayName, this.config.about, this.config.picture, this.config.banner, this.config.nip05, this.config.lud16).build();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function createMetadata(name, displayName, about, picture, banner, nip05, lud16) {
|
|
65
|
+
return new MetadataConfig(name, displayName, about, picture, banner, nip05, lud16).build();
|
|
66
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Filter } from 'nostr-tools';
|
|
2
|
+
export declare class SubscriptionError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export interface SubscriptionConfig {
|
|
5
|
+
pubkey: string;
|
|
6
|
+
kind: number;
|
|
7
|
+
limit: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_SUBSCRIPTION_CONFIG: SubscriptionConfig;
|
|
10
|
+
export declare function createGiftWrapSubscription(pubkey: string, kind?: number, limit?: number): Filter;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { GiftWrap } from 'nostr-tools/kinds';
|
|
2
|
+
export class SubscriptionError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export const DEFAULT_SUBSCRIPTION_CONFIG = {
|
|
5
|
+
pubkey: '',
|
|
6
|
+
kind: GiftWrap,
|
|
7
|
+
limit: 0,
|
|
8
|
+
};
|
|
9
|
+
export function createGiftWrapSubscription(pubkey, kind, limit) {
|
|
10
|
+
const resolvedKind = kind ?? GiftWrap;
|
|
11
|
+
const resolvedLimit = limit ?? 0;
|
|
12
|
+
if (resolvedLimit > 1000) {
|
|
13
|
+
throw new SubscriptionError('Limit exceeds maximum allowed value (1000)');
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
kinds: [resolvedKind],
|
|
17
|
+
'#p': [pubkey],
|
|
18
|
+
limit: resolvedLimit,
|
|
19
|
+
};
|
|
20
|
+
}
|
package/dist/upload.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ServerConfig {
|
|
2
|
+
api_url: string;
|
|
3
|
+
}
|
|
4
|
+
export declare class UploadConfig {
|
|
5
|
+
connectTimeout: number;
|
|
6
|
+
stallThreshold: number;
|
|
7
|
+
poolMaxIdle: number;
|
|
8
|
+
constructor(connectTimeout?: number, stallThreshold?: number, poolMaxIdle?: number);
|
|
9
|
+
}
|
|
10
|
+
export declare class UploadParams {
|
|
11
|
+
retryCount: number;
|
|
12
|
+
retrySpacing: number;
|
|
13
|
+
chunkSize: number;
|
|
14
|
+
constructor(retryCount?: number, retrySpacing?: number, chunkSize?: number);
|
|
15
|
+
}
|
|
16
|
+
export type ProgressCallback = (percentage: number | null, bytes?: number) => void;
|
|
17
|
+
export declare class UploadError extends Error {
|
|
18
|
+
}
|
|
19
|
+
export declare function getServerConfig(): Promise<ServerConfig>;
|
|
20
|
+
export declare function uploadDataWithProgress(signerPrivateKey: string, config: ServerConfig, fileData: Buffer, mimeType: string | undefined, proxy: string | undefined, progressCallback: ProgressCallback, params?: UploadParams, _config?: UploadConfig): Promise<string>;
|
package/dist/upload.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import FormData from 'form-data';
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
4
|
+
import * as secp from '@noble/secp256k1';
|
|
5
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
6
|
+
import { getPublicKey } from 'nostr-tools/pure';
|
|
7
|
+
import { normalizePrivateKey } from './keys.js';
|
|
8
|
+
const TRUSTED_PRIVATE_NIP96 = 'https://medea-1-swiss.vectorapp.io';
|
|
9
|
+
export class UploadConfig {
|
|
10
|
+
constructor(connectTimeout = 5000, stallThreshold = 200, poolMaxIdle = 2) {
|
|
11
|
+
this.connectTimeout = connectTimeout;
|
|
12
|
+
this.stallThreshold = stallThreshold;
|
|
13
|
+
this.poolMaxIdle = poolMaxIdle;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class UploadParams {
|
|
17
|
+
constructor(retryCount = 3, retrySpacing = 2000, chunkSize = 64 * 1024) {
|
|
18
|
+
this.retryCount = retryCount;
|
|
19
|
+
this.retrySpacing = retrySpacing;
|
|
20
|
+
this.chunkSize = chunkSize;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class UploadError extends Error {
|
|
24
|
+
}
|
|
25
|
+
let cachedConfig = null;
|
|
26
|
+
export async function getServerConfig() {
|
|
27
|
+
if (cachedConfig) {
|
|
28
|
+
return cachedConfig;
|
|
29
|
+
}
|
|
30
|
+
const response = await fetch(TRUSTED_PRIVATE_NIP96);
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new UploadError('Failed to fetch NIP-96 server configuration');
|
|
33
|
+
}
|
|
34
|
+
const payload = (await response.json());
|
|
35
|
+
if (!payload.api_url) {
|
|
36
|
+
throw new UploadError('Malformed server configuration');
|
|
37
|
+
}
|
|
38
|
+
cachedConfig = { api_url: payload.api_url };
|
|
39
|
+
return cachedConfig;
|
|
40
|
+
}
|
|
41
|
+
async function buildNip98Authorization(privateKey, method, url, payload) {
|
|
42
|
+
const normalized = normalizePrivateKey(privateKey);
|
|
43
|
+
const dataHash = bytesToHex(sha256(payload));
|
|
44
|
+
const message = `${method.toUpperCase()}\n${url}\n${dataHash}`;
|
|
45
|
+
const messageHash = bytesToHex(sha256(Buffer.from(message, 'utf8')));
|
|
46
|
+
const signature = bytesToHex(await secp.sign(messageHash, normalized.hex));
|
|
47
|
+
const publicKey = getPublicKey(normalized.bytes);
|
|
48
|
+
return `NIP98 ${publicKey}:${signature}`;
|
|
49
|
+
}
|
|
50
|
+
export async function uploadDataWithProgress(signerPrivateKey, config, fileData, mimeType, proxy, progressCallback, params = new UploadParams(), _config = new UploadConfig()) {
|
|
51
|
+
let lastError = null;
|
|
52
|
+
for (let attempt = 0; attempt <= params.retryCount; attempt += 1) {
|
|
53
|
+
if (attempt > 0) {
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, params.retrySpacing));
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const authHeader = await buildNip98Authorization(signerPrivateKey, 'POST', config.api_url, fileData);
|
|
58
|
+
const form = new FormData();
|
|
59
|
+
form.append('file', fileData, {
|
|
60
|
+
filename: 'attachment',
|
|
61
|
+
contentType: mimeType ?? 'application/octet-stream',
|
|
62
|
+
});
|
|
63
|
+
progressCallback(0, 0);
|
|
64
|
+
const response = await fetch(config.api_url, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: authHeader,
|
|
68
|
+
...form.getHeaders(),
|
|
69
|
+
},
|
|
70
|
+
body: form,
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new UploadError(`Upload failed (${response.status})`);
|
|
74
|
+
}
|
|
75
|
+
progressCallback(100, fileData.length);
|
|
76
|
+
const payload = (await response.json());
|
|
77
|
+
if (payload.status === 'error') {
|
|
78
|
+
throw new UploadError(payload.message ?? 'Upload server reported an error');
|
|
79
|
+
}
|
|
80
|
+
const tags = payload.nip94_event?.tags ?? [];
|
|
81
|
+
const urlTag = tags.find((tag) => tag[0] === 'u' || tag[0] === 'url');
|
|
82
|
+
const url = urlTag?.[1];
|
|
83
|
+
if (!url) {
|
|
84
|
+
throw new UploadError('Upload response is missing a URL tag');
|
|
85
|
+
}
|
|
86
|
+
return url;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
lastError = err instanceof Error ? err : new UploadError('Unknown upload error');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw lastError ?? new UploadError('Upload failed without a recorded error');
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nekosuneprojects/vector-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Node.js reimplementation of the Vector Bot SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
13
|
+
"prepack": "npm run build",
|
|
13
14
|
"test": "npm run build"
|
|
14
15
|
},
|
|
15
16
|
"repository": {
|