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