chatly-sdk 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +658 -0
- package/IMPROVEMENTS.md +402 -0
- package/LICENSE +21 -0
- package/README.md +1576 -162
- package/dist/index.d.ts +502 -11
- package/dist/index.js +1619 -66
- package/examples/01-basic-chat/README.md +61 -0
- package/examples/01-basic-chat/index.js +58 -0
- package/examples/01-basic-chat/package.json +13 -0
- package/examples/02-group-chat/README.md +78 -0
- package/examples/02-group-chat/index.js +76 -0
- package/examples/02-group-chat/package.json +13 -0
- package/examples/03-offline-messaging/README.md +73 -0
- package/examples/03-offline-messaging/index.js +80 -0
- package/examples/03-offline-messaging/package.json +13 -0
- package/examples/04-live-chat/README.md +80 -0
- package/examples/04-live-chat/index.js +114 -0
- package/examples/04-live-chat/package.json +13 -0
- package/examples/05-hybrid-messaging/README.md +71 -0
- package/examples/05-hybrid-messaging/index.js +106 -0
- package/examples/05-hybrid-messaging/package.json +13 -0
- package/examples/06-postgresql-integration/README.md +101 -0
- package/examples/06-postgresql-integration/adapters/groupStore.js +73 -0
- package/examples/06-postgresql-integration/adapters/messageStore.js +47 -0
- package/examples/06-postgresql-integration/adapters/userStore.js +40 -0
- package/examples/06-postgresql-integration/index.js +92 -0
- package/examples/06-postgresql-integration/package.json +14 -0
- package/examples/06-postgresql-integration/schema.sql +58 -0
- package/examples/08-customer-support/README.md +70 -0
- package/examples/08-customer-support/index.js +104 -0
- package/examples/08-customer-support/package.json +13 -0
- package/examples/README.md +105 -0
- package/jest.config.cjs +28 -0
- package/package.json +15 -6
- package/src/chat/ChatSession.ts +160 -3
- package/src/chat/GroupSession.ts +108 -1
- package/src/constants.ts +61 -0
- package/src/crypto/e2e.ts +9 -20
- package/src/crypto/utils.ts +3 -1
- package/src/index.ts +530 -63
- package/src/models/mediaTypes.ts +62 -0
- package/src/models/message.ts +4 -1
- package/src/storage/adapters.ts +36 -0
- package/src/storage/localStorage.ts +49 -0
- package/src/storage/s3Storage.ts +84 -0
- package/src/stores/adapters.ts +2 -0
- package/src/stores/memory/messageStore.ts +8 -0
- package/src/transport/adapters.ts +51 -1
- package/src/transport/memoryTransport.ts +75 -13
- package/src/transport/websocketClient.ts +269 -21
- package/src/transport/websocketServer.ts +26 -26
- package/src/utils/errors.ts +97 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/mediaUtils.ts +235 -0
- package/src/utils/messageQueue.ts +162 -0
- package/src/utils/validation.ts +99 -0
- package/test/crypto.test.ts +122 -35
- package/test/sdk.test.ts +276 -0
- package/test/validation.test.ts +64 -0
- package/tsconfig.json +11 -10
- package/tsconfig.test.json +11 -0
- package/src/ChatManager.ts +0 -103
- package/src/crypto/keyManager.ts +0 -28
package/src/index.ts
CHANGED
|
@@ -1,39 +1,135 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
1
2
|
import type { User, StoredUser } from "./models/user.js";
|
|
2
3
|
import type { Message } from "./models/message.js";
|
|
3
4
|
import type { Group } from "./models/group.js";
|
|
5
|
+
import type { MediaAttachment } from "./models/mediaTypes.js";
|
|
4
6
|
import type {
|
|
5
7
|
UserStoreAdapter,
|
|
6
8
|
MessageStoreAdapter,
|
|
7
9
|
GroupStoreAdapter,
|
|
8
10
|
} from "./stores/adapters.js";
|
|
9
11
|
import type { TransportAdapter } from "./transport/adapters.js";
|
|
12
|
+
import type { StorageProvider } from "./storage/adapters.js";
|
|
10
13
|
import { ChatSession } from "./chat/ChatSession.js";
|
|
11
14
|
import { GroupSession } from "./chat/GroupSession.js";
|
|
12
15
|
import { generateIdentityKeyPair } from "./crypto/keys.js";
|
|
13
16
|
import { generateUUID } from "./crypto/uuid.js";
|
|
17
|
+
import { logger, LogLevel } from "./utils/logger.js";
|
|
18
|
+
import { validateUsername, validateMessage, validateGroupName, validateGroupMembers } from "./utils/validation.js";
|
|
19
|
+
import { SessionError, StorageError, TransportError } from "./utils/errors.js";
|
|
20
|
+
import { MessageQueue } from "./utils/messageQueue.js";
|
|
21
|
+
import { EVENTS, ConnectionState } from "./constants.js";
|
|
14
22
|
|
|
15
23
|
export interface ChatSDKConfig {
|
|
16
24
|
userStore: UserStoreAdapter;
|
|
17
25
|
messageStore: MessageStoreAdapter;
|
|
18
26
|
groupStore: GroupStoreAdapter;
|
|
27
|
+
storageProvider?: StorageProvider;
|
|
19
28
|
transport?: TransportAdapter;
|
|
29
|
+
logLevel?: LogLevel;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
/**
|
|
23
33
|
* Main ChatSDK class - production-ready WhatsApp-style chat SDK
|
|
34
|
+
* Extends EventEmitter to provide event-driven architecture
|
|
35
|
+
*
|
|
36
|
+
* Events:
|
|
37
|
+
* - message:sent - Emitted when a message is sent
|
|
38
|
+
* - message:received - Emitted when a message is received
|
|
39
|
+
* - message:failed - Emitted when a message fails to send
|
|
40
|
+
* - connection:state - Emitted when connection state changes
|
|
41
|
+
* - session:created - Emitted when a session is created
|
|
42
|
+
* - group:created - Emitted when a group is created
|
|
43
|
+
* - user:created - Emitted when a user is created
|
|
44
|
+
* - error - Emitted when an error occurs
|
|
24
45
|
*/
|
|
25
|
-
export class ChatSDK {
|
|
46
|
+
export class ChatSDK extends EventEmitter {
|
|
26
47
|
private config: ChatSDKConfig;
|
|
27
48
|
private currentUser: User | null = null;
|
|
49
|
+
private messageQueue: MessageQueue;
|
|
28
50
|
|
|
29
51
|
constructor(config: ChatSDKConfig) {
|
|
52
|
+
super();
|
|
30
53
|
this.config = config;
|
|
54
|
+
this.messageQueue = new MessageQueue();
|
|
55
|
+
|
|
56
|
+
// Set log level
|
|
57
|
+
if (config.logLevel !== undefined) {
|
|
58
|
+
logger.setLevel(config.logLevel);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Set up transport event handlers if transport is provided
|
|
62
|
+
if (this.config.transport) {
|
|
63
|
+
this.setupTransportHandlers();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logger.info('ChatSDK initialized');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private setupTransportHandlers(): void {
|
|
70
|
+
if (!this.config.transport) return;
|
|
71
|
+
|
|
72
|
+
// Handle incoming messages
|
|
73
|
+
this.config.transport.onMessage((message: Message) => {
|
|
74
|
+
logger.debug('Message received via transport', { messageId: message.id });
|
|
75
|
+
this.emit(EVENTS.MESSAGE_RECEIVED, message);
|
|
76
|
+
|
|
77
|
+
// Store received message
|
|
78
|
+
this.config.messageStore.create(message).catch((error) => {
|
|
79
|
+
logger.error('Failed to store received message', error);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Handle connection state changes
|
|
84
|
+
if (this.config.transport.onConnectionStateChange) {
|
|
85
|
+
this.config.transport.onConnectionStateChange((state: ConnectionState) => {
|
|
86
|
+
logger.info('Connection state changed', { state });
|
|
87
|
+
this.emit(EVENTS.CONNECTION_STATE_CHANGED, state);
|
|
88
|
+
|
|
89
|
+
// Process queued messages when reconnected
|
|
90
|
+
if (state === ConnectionState.CONNECTED) {
|
|
91
|
+
this.processMessageQueue();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Handle transport errors
|
|
97
|
+
if (this.config.transport.onError) {
|
|
98
|
+
this.config.transport.onError((error: Error) => {
|
|
99
|
+
logger.error('Transport error', error);
|
|
100
|
+
this.emit(EVENTS.ERROR, error);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async processMessageQueue(): Promise<void> {
|
|
106
|
+
const pending = this.messageQueue.getPendingMessages();
|
|
107
|
+
const retryable = this.messageQueue.getRetryableMessages();
|
|
108
|
+
const toSend = [...pending, ...retryable];
|
|
109
|
+
|
|
110
|
+
logger.info('Processing message queue', { count: toSend.length });
|
|
111
|
+
|
|
112
|
+
for (const queued of toSend) {
|
|
113
|
+
try {
|
|
114
|
+
await this.config.transport!.send(queued.message);
|
|
115
|
+
this.messageQueue.markSent(queued.message.id);
|
|
116
|
+
this.emit(EVENTS.MESSAGE_SENT, queued.message);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
this.messageQueue.markFailed(
|
|
119
|
+
queued.message.id,
|
|
120
|
+
error instanceof Error ? error : new Error(String(error))
|
|
121
|
+
);
|
|
122
|
+
this.emit(EVENTS.MESSAGE_FAILED, queued.message, error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
31
125
|
}
|
|
32
126
|
|
|
33
127
|
/**
|
|
34
128
|
* Create a new user with generated identity keys
|
|
35
129
|
*/
|
|
36
130
|
async createUser(username: string): Promise<User> {
|
|
131
|
+
validateUsername(username);
|
|
132
|
+
|
|
37
133
|
const keyPair = generateIdentityKeyPair();
|
|
38
134
|
const user: User = {
|
|
39
135
|
id: generateUUID(),
|
|
@@ -43,25 +139,63 @@ export class ChatSDK {
|
|
|
43
139
|
privateKey: keyPair.privateKey,
|
|
44
140
|
};
|
|
45
141
|
|
|
46
|
-
|
|
47
|
-
|
|
142
|
+
try {
|
|
143
|
+
await this.config.userStore.create(user);
|
|
144
|
+
logger.info('User created', { userId: user.id, username: user.username });
|
|
145
|
+
this.emit(EVENTS.USER_CREATED, user);
|
|
146
|
+
return user;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const storageError = new StorageError(
|
|
149
|
+
'Failed to create user',
|
|
150
|
+
true,
|
|
151
|
+
{ username, error: error instanceof Error ? error.message : String(error) }
|
|
152
|
+
);
|
|
153
|
+
logger.error('User creation failed', storageError);
|
|
154
|
+
this.emit(EVENTS.ERROR, storageError);
|
|
155
|
+
throw storageError;
|
|
156
|
+
}
|
|
48
157
|
}
|
|
49
158
|
|
|
50
159
|
/**
|
|
51
160
|
* Import an existing user from stored data
|
|
52
161
|
*/
|
|
53
162
|
async importUser(userData: StoredUser): Promise<User> {
|
|
54
|
-
|
|
55
|
-
|
|
163
|
+
try {
|
|
164
|
+
await this.config.userStore.save(userData);
|
|
165
|
+
logger.info('User imported', { userId: userData.id });
|
|
166
|
+
return userData;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const storageError = new StorageError(
|
|
169
|
+
'Failed to import user',
|
|
170
|
+
true,
|
|
171
|
+
{ userId: userData.id, error: error instanceof Error ? error.message : String(error) }
|
|
172
|
+
);
|
|
173
|
+
logger.error('User import failed', storageError);
|
|
174
|
+
this.emit(EVENTS.ERROR, storageError);
|
|
175
|
+
throw storageError;
|
|
176
|
+
}
|
|
56
177
|
}
|
|
57
178
|
|
|
58
179
|
/**
|
|
59
180
|
* Set the current active user
|
|
60
181
|
*/
|
|
61
|
-
setCurrentUser(user: User): void {
|
|
182
|
+
async setCurrentUser(user: User): Promise<void> {
|
|
62
183
|
this.currentUser = user;
|
|
184
|
+
logger.info('Current user set', { userId: user.id, username: user.username });
|
|
185
|
+
|
|
63
186
|
if (this.config.transport) {
|
|
64
|
-
|
|
187
|
+
try {
|
|
188
|
+
await this.config.transport.connect(user.id);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const transportError = new TransportError(
|
|
191
|
+
'Failed to connect transport',
|
|
192
|
+
true,
|
|
193
|
+
{ userId: user.id, error: error instanceof Error ? error.message : String(error) }
|
|
194
|
+
);
|
|
195
|
+
logger.error('Transport connection failed', transportError);
|
|
196
|
+
this.emit(EVENTS.ERROR, transportError);
|
|
197
|
+
throw transportError;
|
|
198
|
+
}
|
|
65
199
|
}
|
|
66
200
|
}
|
|
67
201
|
|
|
@@ -79,18 +213,30 @@ export class ChatSDK {
|
|
|
79
213
|
// Create consistent session ID regardless of user order
|
|
80
214
|
const ids = [userA.id, userB.id].sort();
|
|
81
215
|
const sessionId = `${ids[0]}-${ids[1]}`;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const session = new ChatSession(sessionId, userA, userB, this.config.storageProvider);
|
|
219
|
+
await session.initialize();
|
|
220
|
+
logger.info('Chat session created', { sessionId, users: [userA.id, userB.id] });
|
|
221
|
+
this.emit(EVENTS.SESSION_CREATED, session);
|
|
222
|
+
return session;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const sessionError = new SessionError(
|
|
225
|
+
'Failed to create chat session',
|
|
226
|
+
{ sessionId, error: error instanceof Error ? error.message : String(error) }
|
|
227
|
+
);
|
|
228
|
+
logger.error('Session creation failed', sessionError);
|
|
229
|
+
this.emit(EVENTS.ERROR, sessionError);
|
|
230
|
+
throw sessionError;
|
|
231
|
+
}
|
|
85
232
|
}
|
|
86
233
|
|
|
87
234
|
/**
|
|
88
235
|
* Create a new group with members
|
|
89
236
|
*/
|
|
90
237
|
async createGroup(name: string, members: User[]): Promise<GroupSession> {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
238
|
+
validateGroupName(name);
|
|
239
|
+
validateGroupMembers(members.length);
|
|
94
240
|
|
|
95
241
|
const group: Group = {
|
|
96
242
|
id: generateUUID(),
|
|
@@ -99,24 +245,53 @@ export class ChatSDK {
|
|
|
99
245
|
createdAt: Date.now(),
|
|
100
246
|
};
|
|
101
247
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
248
|
+
try {
|
|
249
|
+
await this.config.groupStore.create(group);
|
|
250
|
+
const session = new GroupSession(group, this.config.storageProvider);
|
|
251
|
+
await session.initialize();
|
|
252
|
+
logger.info('Group created', { groupId: group.id, name: group.name, memberCount: members.length });
|
|
253
|
+
this.emit(EVENTS.GROUP_CREATED, session);
|
|
254
|
+
return session;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
const storageError = new StorageError(
|
|
257
|
+
'Failed to create group',
|
|
258
|
+
true,
|
|
259
|
+
{ groupName: name, error: error instanceof Error ? error.message : String(error) }
|
|
260
|
+
);
|
|
261
|
+
logger.error('Group creation failed', storageError);
|
|
262
|
+
this.emit(EVENTS.ERROR, storageError);
|
|
263
|
+
throw storageError;
|
|
264
|
+
}
|
|
106
265
|
}
|
|
107
266
|
|
|
108
267
|
/**
|
|
109
268
|
* Load an existing group by ID
|
|
110
269
|
*/
|
|
111
270
|
async loadGroup(id: string): Promise<GroupSession> {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
271
|
+
try {
|
|
272
|
+
const group = await this.config.groupStore.findById(id);
|
|
273
|
+
if (!group) {
|
|
274
|
+
throw new SessionError(`Group not found: ${id}`, { groupId: id });
|
|
275
|
+
}
|
|
116
276
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
277
|
+
const session = new GroupSession(group, this.config.storageProvider);
|
|
278
|
+
await session.initialize();
|
|
279
|
+
logger.debug('Group loaded', { groupId: id });
|
|
280
|
+
return session;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error instanceof SessionError) {
|
|
283
|
+
this.emit(EVENTS.ERROR, error);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
const storageError = new StorageError(
|
|
287
|
+
'Failed to load group',
|
|
288
|
+
true,
|
|
289
|
+
{ groupId: id, error: error instanceof Error ? error.message : String(error) }
|
|
290
|
+
);
|
|
291
|
+
logger.error('Group load failed', storageError);
|
|
292
|
+
this.emit(EVENTS.ERROR, storageError);
|
|
293
|
+
throw storageError;
|
|
294
|
+
}
|
|
120
295
|
}
|
|
121
296
|
|
|
122
297
|
/**
|
|
@@ -127,59 +302,201 @@ export class ChatSDK {
|
|
|
127
302
|
plaintext: string
|
|
128
303
|
): Promise<Message> {
|
|
129
304
|
if (!this.currentUser) {
|
|
130
|
-
throw new
|
|
305
|
+
throw new SessionError("No current user set. Call setCurrentUser() first.");
|
|
131
306
|
}
|
|
132
307
|
|
|
308
|
+
validateMessage(plaintext);
|
|
309
|
+
|
|
133
310
|
let message: Message;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
311
|
+
try {
|
|
312
|
+
if (session instanceof ChatSession) {
|
|
313
|
+
message = await session.encrypt(plaintext, this.currentUser.id);
|
|
314
|
+
} else {
|
|
315
|
+
message = await session.encrypt(plaintext, this.currentUser.id);
|
|
316
|
+
}
|
|
139
317
|
|
|
140
|
-
|
|
141
|
-
|
|
318
|
+
// Store the message
|
|
319
|
+
await this.config.messageStore.create(message);
|
|
320
|
+
logger.debug('Message stored', { messageId: message.id });
|
|
142
321
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
322
|
+
// Send via transport if available
|
|
323
|
+
if (this.config.transport) {
|
|
324
|
+
if (this.config.transport.isConnected()) {
|
|
325
|
+
try {
|
|
326
|
+
await this.config.transport.send(message);
|
|
327
|
+
logger.debug('Message sent via transport', { messageId: message.id });
|
|
328
|
+
this.emit(EVENTS.MESSAGE_SENT, message);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
// Queue message for retry
|
|
331
|
+
this.messageQueue.enqueue(message);
|
|
332
|
+
this.messageQueue.markFailed(
|
|
333
|
+
message.id,
|
|
334
|
+
error instanceof Error ? error : new Error(String(error))
|
|
335
|
+
);
|
|
336
|
+
logger.warn('Message send failed, queued for retry', { messageId: message.id });
|
|
337
|
+
this.emit(EVENTS.MESSAGE_FAILED, message, error);
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
// Queue message if offline
|
|
341
|
+
this.messageQueue.enqueue(message);
|
|
342
|
+
logger.info('Message queued (offline)', { messageId: message.id });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return message;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
const sendError = error instanceof Error ? error : new Error(String(error));
|
|
349
|
+
logger.error('Failed to send message', sendError);
|
|
350
|
+
this.emit(EVENTS.ERROR, sendError);
|
|
351
|
+
throw sendError;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Send a media message in a chat session (1:1 or group)
|
|
357
|
+
*/
|
|
358
|
+
async sendMediaMessage(
|
|
359
|
+
session: ChatSession | GroupSession,
|
|
360
|
+
caption: string,
|
|
361
|
+
media: MediaAttachment
|
|
362
|
+
): Promise<Message> {
|
|
363
|
+
if (!this.currentUser) {
|
|
364
|
+
throw new SessionError("No current user set. Call setCurrentUser() first.");
|
|
146
365
|
}
|
|
147
366
|
|
|
148
|
-
|
|
367
|
+
let message: Message;
|
|
368
|
+
try {
|
|
369
|
+
if (session instanceof ChatSession) {
|
|
370
|
+
message = await session.encryptMedia(caption, media, this.currentUser.id);
|
|
371
|
+
} else {
|
|
372
|
+
message = await session.encryptMedia(caption, media, this.currentUser.id);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Store the message
|
|
376
|
+
await this.config.messageStore.create(message);
|
|
377
|
+
logger.debug('Media message stored', { messageId: message.id, mediaType: media.type });
|
|
378
|
+
|
|
379
|
+
// Send via transport if available
|
|
380
|
+
if (this.config.transport) {
|
|
381
|
+
if (this.config.transport.isConnected()) {
|
|
382
|
+
try {
|
|
383
|
+
await this.config.transport.send(message);
|
|
384
|
+
logger.debug('Media message sent via transport', { messageId: message.id });
|
|
385
|
+
this.emit(EVENTS.MESSAGE_SENT, message);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
// Queue message for retry
|
|
388
|
+
this.messageQueue.enqueue(message);
|
|
389
|
+
this.messageQueue.markFailed(
|
|
390
|
+
message.id,
|
|
391
|
+
error instanceof Error ? error : new Error(String(error))
|
|
392
|
+
);
|
|
393
|
+
logger.warn('Media message send failed, queued for retry', { messageId: message.id });
|
|
394
|
+
this.emit(EVENTS.MESSAGE_FAILED, message, error);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
// Queue message if offline
|
|
398
|
+
this.messageQueue.enqueue(message);
|
|
399
|
+
logger.info('Media message queued (offline)', { messageId: message.id });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return message;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
const sendError = error instanceof Error ? error : new Error(String(error));
|
|
406
|
+
logger.error('Failed to send media message', sendError);
|
|
407
|
+
this.emit(EVENTS.ERROR, sendError);
|
|
408
|
+
throw sendError;
|
|
409
|
+
}
|
|
149
410
|
}
|
|
150
411
|
|
|
151
412
|
/**
|
|
152
413
|
* Decrypt a message
|
|
153
414
|
*/
|
|
154
415
|
async decryptMessage(message: Message, user: User): Promise<string> {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
416
|
+
try {
|
|
417
|
+
if (message.groupId) {
|
|
418
|
+
// Group message
|
|
419
|
+
const group = await this.config.groupStore.findById(message.groupId);
|
|
420
|
+
if (!group) {
|
|
421
|
+
throw new SessionError(`Group not found: ${message.groupId}`, { groupId: message.groupId });
|
|
422
|
+
}
|
|
423
|
+
const session = new GroupSession(group);
|
|
424
|
+
await session.initialize();
|
|
425
|
+
return await session.decrypt(message);
|
|
426
|
+
} else {
|
|
427
|
+
// 1:1 message - need to find the session
|
|
428
|
+
const otherUserId =
|
|
429
|
+
message.senderId === user.id ? message.receiverId : message.senderId;
|
|
430
|
+
if (!otherUserId) {
|
|
431
|
+
throw new SessionError("Invalid message: missing receiver/sender");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const otherUser = await this.config.userStore.findById(otherUserId);
|
|
435
|
+
if (!otherUser) {
|
|
436
|
+
throw new SessionError(`User not found: ${otherUserId}`, { userId: otherUserId });
|
|
437
|
+
}
|
|
171
438
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
439
|
+
// Create consistent session ID
|
|
440
|
+
const ids = [user.id, otherUser.id].sort();
|
|
441
|
+
const sessionId = `${ids[0]}-${ids[1]}`;
|
|
442
|
+
const session = new ChatSession(sessionId, user, otherUser, this.config.storageProvider);
|
|
443
|
+
await session.initializeForUser(user);
|
|
444
|
+
return await session.decrypt(message, user);
|
|
175
445
|
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
const decryptError = error instanceof Error ? error : new Error(String(error));
|
|
448
|
+
logger.error('Failed to decrypt message', decryptError);
|
|
449
|
+
this.emit(EVENTS.ERROR, decryptError);
|
|
450
|
+
throw decryptError;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
176
453
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
454
|
+
/**
|
|
455
|
+
* Decrypt a media message
|
|
456
|
+
*/
|
|
457
|
+
async decryptMediaMessage(
|
|
458
|
+
message: Message,
|
|
459
|
+
user: User
|
|
460
|
+
): Promise<{ text: string; media: MediaAttachment }> {
|
|
461
|
+
if (!message.media) {
|
|
462
|
+
throw new SessionError("Message does not contain media");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
if (message.groupId) {
|
|
467
|
+
// Group media message
|
|
468
|
+
const group = await this.config.groupStore.findById(message.groupId);
|
|
469
|
+
if (!group) {
|
|
470
|
+
throw new SessionError(`Group not found: ${message.groupId}`, { groupId: message.groupId });
|
|
471
|
+
}
|
|
472
|
+
const session = new GroupSession(group, this.config.storageProvider);
|
|
473
|
+
await session.initialize();
|
|
474
|
+
return await session.decryptMedia(message);
|
|
475
|
+
} else {
|
|
476
|
+
// 1:1 media message
|
|
477
|
+
const otherUserId =
|
|
478
|
+
message.senderId === user.id ? message.receiverId : message.senderId;
|
|
479
|
+
if (!otherUserId) {
|
|
480
|
+
throw new SessionError("Invalid message: missing receiver/sender");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const otherUser = await this.config.userStore.findById(otherUserId);
|
|
484
|
+
if (!otherUser) {
|
|
485
|
+
throw new SessionError(`User not found: ${otherUserId}`, { userId: otherUserId });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Create consistent session ID
|
|
489
|
+
const ids = [user.id, otherUser.id].sort();
|
|
490
|
+
const sessionId = `${ids[0]}-${ids[1]}`;
|
|
491
|
+
const session = new ChatSession(sessionId, user, otherUser, this.config.storageProvider);
|
|
492
|
+
await session.initializeForUser(user);
|
|
493
|
+
return await session.decryptMedia(message, user);
|
|
494
|
+
}
|
|
495
|
+
} catch (error) {
|
|
496
|
+
const decryptError = error instanceof Error ? error : new Error(String(error));
|
|
497
|
+
logger.error('Failed to decrypt media message', decryptError);
|
|
498
|
+
this.emit(EVENTS.ERROR, decryptError);
|
|
499
|
+
throw decryptError;
|
|
183
500
|
}
|
|
184
501
|
}
|
|
185
502
|
|
|
@@ -187,14 +504,154 @@ export class ChatSDK {
|
|
|
187
504
|
* Get messages for a user
|
|
188
505
|
*/
|
|
189
506
|
async getMessagesForUser(userId: string): Promise<Message[]> {
|
|
190
|
-
|
|
507
|
+
try {
|
|
508
|
+
return await this.config.messageStore.listByUser(userId);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
const storageError = new StorageError(
|
|
511
|
+
'Failed to get messages for user',
|
|
512
|
+
true,
|
|
513
|
+
{ userId, error: error instanceof Error ? error.message : String(error) }
|
|
514
|
+
);
|
|
515
|
+
logger.error('Get messages failed', storageError);
|
|
516
|
+
this.emit(EVENTS.ERROR, storageError);
|
|
517
|
+
throw storageError;
|
|
518
|
+
}
|
|
191
519
|
}
|
|
192
520
|
|
|
193
521
|
/**
|
|
194
522
|
* Get messages for a group
|
|
195
523
|
*/
|
|
196
524
|
async getMessagesForGroup(groupId: string): Promise<Message[]> {
|
|
197
|
-
|
|
525
|
+
try {
|
|
526
|
+
return await this.config.messageStore.listByGroup(groupId);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
const storageError = new StorageError(
|
|
529
|
+
'Failed to get messages for group',
|
|
530
|
+
true,
|
|
531
|
+
{ groupId, error: error instanceof Error ? error.message : String(error) }
|
|
532
|
+
);
|
|
533
|
+
logger.error('Get messages failed', storageError);
|
|
534
|
+
this.emit(EVENTS.ERROR, storageError);
|
|
535
|
+
throw storageError;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ========== Public Accessor Methods ==========
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Get the transport adapter
|
|
543
|
+
*/
|
|
544
|
+
getTransport(): TransportAdapter | undefined {
|
|
545
|
+
return this.config.transport;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Get all users
|
|
550
|
+
*/
|
|
551
|
+
async listUsers(): Promise<User[]> {
|
|
552
|
+
try {
|
|
553
|
+
return await this.config.userStore.list();
|
|
554
|
+
} catch (error) {
|
|
555
|
+
const storageError = new StorageError(
|
|
556
|
+
'Failed to list users',
|
|
557
|
+
true,
|
|
558
|
+
{ error: error instanceof Error ? error.message : String(error) }
|
|
559
|
+
);
|
|
560
|
+
logger.error('List users failed', storageError);
|
|
561
|
+
this.emit(EVENTS.ERROR, storageError);
|
|
562
|
+
throw storageError;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Get user by ID
|
|
568
|
+
*/
|
|
569
|
+
async getUserById(userId: string): Promise<User | undefined> {
|
|
570
|
+
try {
|
|
571
|
+
return await this.config.userStore.findById(userId);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
const storageError = new StorageError(
|
|
574
|
+
'Failed to get user',
|
|
575
|
+
true,
|
|
576
|
+
{ userId, error: error instanceof Error ? error.message : String(error) }
|
|
577
|
+
);
|
|
578
|
+
logger.error('Get user failed', storageError);
|
|
579
|
+
this.emit(EVENTS.ERROR, storageError);
|
|
580
|
+
throw storageError;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get all groups
|
|
586
|
+
*/
|
|
587
|
+
async listGroups(): Promise<Group[]> {
|
|
588
|
+
try {
|
|
589
|
+
return await this.config.groupStore.list();
|
|
590
|
+
} catch (error) {
|
|
591
|
+
const storageError = new StorageError(
|
|
592
|
+
'Failed to list groups',
|
|
593
|
+
true,
|
|
594
|
+
{ error: error instanceof Error ? error.message : String(error) }
|
|
595
|
+
);
|
|
596
|
+
logger.error('List groups failed', storageError);
|
|
597
|
+
this.emit(EVENTS.ERROR, storageError);
|
|
598
|
+
throw storageError;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Get connection state
|
|
604
|
+
*/
|
|
605
|
+
getConnectionState(): ConnectionState {
|
|
606
|
+
if (!this.config.transport) {
|
|
607
|
+
return ConnectionState.DISCONNECTED;
|
|
608
|
+
}
|
|
609
|
+
return this.config.transport.getConnectionState();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Check if connected
|
|
614
|
+
*/
|
|
615
|
+
isConnected(): boolean {
|
|
616
|
+
if (!this.config.transport) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
return this.config.transport.isConnected();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Disconnect transport
|
|
624
|
+
*/
|
|
625
|
+
async disconnect(): Promise<void> {
|
|
626
|
+
if (this.config.transport) {
|
|
627
|
+
await this.config.transport.disconnect();
|
|
628
|
+
logger.info('Transport disconnected');
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Reconnect transport
|
|
634
|
+
*/
|
|
635
|
+
async reconnect(): Promise<void> {
|
|
636
|
+
if (this.config.transport) {
|
|
637
|
+
await this.config.transport.reconnect();
|
|
638
|
+
logger.info('Transport reconnected');
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Get message queue status
|
|
644
|
+
*/
|
|
645
|
+
getQueueStatus(): {
|
|
646
|
+
size: number;
|
|
647
|
+
pending: number;
|
|
648
|
+
retryable: number;
|
|
649
|
+
} {
|
|
650
|
+
return {
|
|
651
|
+
size: this.messageQueue.size(),
|
|
652
|
+
pending: this.messageQueue.getPendingMessages().length,
|
|
653
|
+
retryable: this.messageQueue.getRetryableMessages().length,
|
|
654
|
+
};
|
|
198
655
|
}
|
|
199
656
|
}
|
|
200
657
|
|
|
@@ -205,8 +662,18 @@ export * from "./stores/memory/messageStore.js";
|
|
|
205
662
|
export * from "./stores/memory/groupStore.js";
|
|
206
663
|
export * from "./transport/adapters.js";
|
|
207
664
|
export * from "./transport/memoryTransport.js";
|
|
665
|
+
export * from "./transport/websocketClient.js";
|
|
208
666
|
export * from "./models/user.js";
|
|
209
667
|
export * from "./models/message.js";
|
|
210
668
|
export * from "./models/group.js";
|
|
669
|
+
export * from "./models/mediaTypes.js";
|
|
211
670
|
export * from "./chat/ChatSession.js";
|
|
212
671
|
export * from "./chat/GroupSession.js";
|
|
672
|
+
export * from "./utils/errors.js";
|
|
673
|
+
export * from "./utils/logger.js";
|
|
674
|
+
export * from "./utils/validation.js";
|
|
675
|
+
export * from "./utils/mediaUtils.js";
|
|
676
|
+
export * from "./storage/adapters.js";
|
|
677
|
+
export * from "./storage/localStorage.js";
|
|
678
|
+
export * from "./storage/s3Storage.js";
|
|
679
|
+
export * from "./constants.js";
|