@sunnoy/wecom 1.1.2 → 1.3.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/webhook.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { WecomCrypto } from "./crypto.js";
2
2
  import { logger } from "./logger.js";
3
- import { MessageDeduplicator, randomString } from "./utils.js";
3
+ import { MessageDeduplicator } from "./utils.js";
4
4
 
5
5
  /**
6
6
  * WeCom AI Bot Webhook Handler
@@ -12,306 +12,453 @@ import { MessageDeduplicator, randomString } from "./utils.js";
12
12
  * - Response uses stream message format
13
13
  */
14
14
  export class WecomWebhook {
15
- config;
16
- crypto;
17
- deduplicator = new MessageDeduplicator();
18
-
19
- constructor(config) {
20
- this.config = config;
21
- this.crypto = new WecomCrypto(config.token, config.encodingAesKey);
22
- logger.debug("WecomWebhook initialized (AI Bot mode)");
15
+ config;
16
+ crypto;
17
+ deduplicator = new MessageDeduplicator();
18
+
19
+ /** Sentinel returned when a message is a duplicate (caller should ACK 200). */
20
+ static DUPLICATE = Symbol.for("wecom.duplicate");
21
+
22
+ constructor(config) {
23
+ this.config = config;
24
+ this.crypto = new WecomCrypto(config.token, config.encodingAesKey);
25
+ logger.debug("WecomWebhook initialized (AI Bot mode)");
26
+ }
27
+
28
+ // =========================================================================
29
+ // URL Verification (GET request)
30
+ // =========================================================================
31
+ handleVerify(query) {
32
+ const signature = query.msg_signature;
33
+ const timestamp = query.timestamp;
34
+ const nonce = query.nonce;
35
+ const echostr = query.echostr;
36
+
37
+ if (!signature || !timestamp || !nonce || !echostr) {
38
+ logger.warn("Missing parameters in verify request", { query });
39
+ return null;
23
40
  }
24
41
 
25
- // =========================================================================
26
- // URL Verification (GET request)
27
- // =========================================================================
28
- handleVerify(query) {
29
- const signature = query.msg_signature;
30
- const timestamp = query.timestamp;
31
- const nonce = query.nonce;
32
- const echostr = query.echostr;
33
-
34
- if (!signature || !timestamp || !nonce || !echostr) {
35
- logger.warn("Missing parameters in verify request", { query });
36
- return null;
37
- }
38
-
39
- logger.debug("Handling verify request", { timestamp, nonce });
42
+ logger.debug("Handling verify request", { timestamp, nonce });
40
43
 
41
- const calcSignature = this.crypto.getSignature(timestamp, nonce, echostr);
42
- if (calcSignature !== signature) {
43
- logger.error("Signature mismatch in verify", {
44
- expected: signature,
45
- calculated: calcSignature,
46
- });
47
- return null;
48
- }
49
-
50
- try {
51
- const result = this.crypto.decrypt(echostr);
52
- logger.info("URL verification successful");
53
- return result.message;
54
- }
55
- catch (e) {
56
- logger.error("Decrypt failed in verify", {
57
- error: e instanceof Error ? e.message : String(e),
58
- });
59
- return null;
60
- }
44
+ const calcSignature = this.crypto.getSignature(timestamp, nonce, echostr);
45
+ if (calcSignature !== signature) {
46
+ logger.error("Signature mismatch in verify", {
47
+ expected: signature,
48
+ calculated: calcSignature,
49
+ });
50
+ return null;
61
51
  }
62
52
 
63
- // =========================================================================
64
- // Message Handling (POST request)
65
- // AI Bot uses JSON format, not XML
66
- // =========================================================================
67
- async handleMessage(query, body) {
68
- const signature = query.msg_signature;
69
- const timestamp = query.timestamp;
70
- const nonce = query.nonce;
71
-
72
- if (!signature || !timestamp || !nonce) {
73
- logger.warn("Missing parameters in message request", { query });
74
- return null;
75
- }
76
-
77
- // 1. Parse JSON body to get encrypt field
78
- let encrypt;
79
- try {
80
- const jsonBody = JSON.parse(body);
81
- encrypt = jsonBody.encrypt;
82
- logger.debug("Parsed request body", { hasEncrypt: !!encrypt });
83
- }
84
- catch (e) {
85
- logger.error("Failed to parse request body as JSON", {
86
- error: e instanceof Error ? e.message : String(e),
87
- body: body.substring(0, 200),
88
- });
89
- return null;
90
- }
91
-
92
- if (!encrypt) {
93
- logger.error("No encrypt field in body");
94
- return null;
95
- }
53
+ try {
54
+ const result = this.crypto.decrypt(echostr);
55
+ logger.info("URL verification successful");
56
+ return result.message;
57
+ } catch (e) {
58
+ logger.error("Decrypt failed in verify", {
59
+ error: e instanceof Error ? e.message : String(e),
60
+ });
61
+ return null;
62
+ }
63
+ }
64
+
65
+ // =========================================================================
66
+ // Message Handling (POST request)
67
+ // AI Bot uses JSON format, not XML
68
+ // =========================================================================
69
+ async handleMessage(query, body) {
70
+ const signature = query.msg_signature;
71
+ const timestamp = query.timestamp;
72
+ const nonce = query.nonce;
73
+
74
+ if (!signature || !timestamp || !nonce) {
75
+ logger.warn("Missing parameters in message request", { query });
76
+ return null;
77
+ }
96
78
 
97
- // 2. Verify signature
98
- const calcSignature = this.crypto.getSignature(timestamp, nonce, encrypt);
99
- if (calcSignature !== signature) {
100
- logger.error("Signature mismatch in message", {
101
- expected: signature,
102
- calculated: calcSignature,
103
- });
104
- return null;
105
- }
79
+ // 1. Parse JSON body to get encrypt field
80
+ let encrypt;
81
+ try {
82
+ const jsonBody = JSON.parse(body);
83
+ encrypt = jsonBody.encrypt;
84
+ logger.debug("Parsed request body", { hasEncrypt: !!encrypt });
85
+ } catch (e) {
86
+ logger.error("Failed to parse request body as JSON", {
87
+ error: e instanceof Error ? e.message : String(e),
88
+ body: body.substring(0, 200),
89
+ });
90
+ return null;
91
+ }
106
92
 
107
- // 3. Decrypt
108
- let decryptedContent;
109
- try {
110
- const result = this.crypto.decrypt(encrypt);
111
- decryptedContent = result.message;
112
- logger.debug("Decrypted content", { content: decryptedContent.substring(0, 300) });
113
- }
114
- catch (e) {
115
- logger.error("Message decrypt failed", {
116
- error: e instanceof Error ? e.message : String(e),
117
- });
118
- return null;
119
- }
93
+ if (!encrypt) {
94
+ logger.error("No encrypt field in body");
95
+ return null;
96
+ }
120
97
 
121
- // 4. Parse decrypted JSON content (AI Bot format)
122
- let data;
123
- try {
124
- data = JSON.parse(decryptedContent);
125
- logger.debug("Parsed message data", { msgtype: data.msgtype, keys: Object.keys(data), text: JSON.stringify(data.text) });
126
- }
127
- catch (e) {
128
- logger.error("Failed to parse decrypted content as JSON", {
129
- error: e instanceof Error ? e.message : String(e),
130
- content: decryptedContent.substring(0, 200),
131
- });
132
- return null;
133
- }
98
+ // 2. Verify signature
99
+ const calcSignature = this.crypto.getSignature(timestamp, nonce, encrypt);
100
+ if (calcSignature !== signature) {
101
+ logger.error("Signature mismatch in message", {
102
+ expected: signature,
103
+ calculated: calcSignature,
104
+ });
105
+ return null;
106
+ }
134
107
 
135
- // 5. Process based on message type
136
- const msgtype = data.msgtype;
137
-
138
- if (msgtype === "text") {
139
- // AI Bot format: text.content
140
- const content = data.text?.content || "";
141
- const msgId = data.msgid || `msg_${Date.now()}`;
142
- const fromUser = data.from?.userid || ""; // Note: "userid" not "user_id"
143
- const responseUrl = data.response_url || "";
144
- const chatType = data.chattype || "single"; // "single" 或 "group"
145
- const chatId = data.chatid || ""; // 群聊 ID(仅群聊时存在)
146
- const aibotId = data.aibotid || ""; // 机器人 ID
147
-
148
- // 解析引用消息(可选)
149
- const quote = data.quote ? {
150
- msgType: data.quote.msgtype,
151
- content: data.quote.text?.content || data.quote.image?.url || "",
152
- } : null;
153
-
154
- // Check for duplicates
155
- if (this.deduplicator.isDuplicate(msgId)) {
156
- logger.debug("Duplicate message ignored", { msgId });
157
- return null;
158
- }
159
-
160
- logger.info("Received text message", {
161
- fromUser,
162
- chatType,
163
- chatId: chatId || "(private)",
164
- content: content.substring(0, 50)
165
- });
166
-
167
- return {
168
- message: {
169
- msgId,
170
- msgType: "text",
171
- content,
172
- fromUser,
173
- chatType,
174
- chatId, // 群聊 ID
175
- aibotId, // 机器人 ID
176
- quote, // 引用消息
177
- responseUrl, // For async response
178
- },
179
- query: { timestamp, nonce },
180
- };
181
- }
182
- else if (msgtype === "stream") {
183
- // Stream continuation request from WeCom
184
- const streamId = data.stream?.id;
185
- logger.debug("Received stream refresh request", { streamId });
186
- return {
187
- stream: {
188
- id: streamId,
189
- },
190
- query: { timestamp, nonce },
191
- rawData: data, // 保留完整数据用于调试
192
- };
193
- }
194
- else if (msgtype === "image") {
195
- const imageUrl = data.image?.url;
196
- const msgId = data.msgid || `msg_${Date.now()}`;
197
- const fromUser = data.from?.userid || "";
198
- const responseUrl = data.response_url || "";
199
- logger.info("Received image message", { fromUser, imageUrl });
200
-
201
- return {
202
- message: {
203
- msgId,
204
- msgType: "image",
205
- imageUrl,
206
- fromUser,
207
- responseUrl,
208
- },
209
- query: { timestamp, nonce },
210
- };
211
- }
212
- else if (msgtype === "voice") {
213
- // Voice message (single chat only) - WeCom automatically transcribes to text
214
- const content = data.voice?.content || "";
215
- const msgId = data.msgid || `msg_${Date.now()}`;
216
- const fromUser = data.from?.userid || "";
217
- const responseUrl = data.response_url || "";
218
- const chatType = data.chattype || "single";
219
- const chatId = data.chatid || "";
220
-
221
- // Check for duplicates
222
- if (this.deduplicator.isDuplicate(msgId)) {
223
- logger.debug("Duplicate voice message ignored", { msgId });
224
- return null;
225
- }
226
-
227
- // Validate content
228
- if (!content.trim()) {
229
- logger.warn("Empty voice message received", { msgId, fromUser });
230
- return null;
231
- }
232
-
233
- logger.info("Received voice message (auto-transcribed by WeCom)", {
234
- fromUser,
235
- chatType,
236
- chatId: chatId || "(private)",
237
- originalType: "voice",
238
- transcribedLength: content.length,
239
- preview: content.substring(0, 50)
240
- });
241
-
242
- // Treat voice as text since WeCom already transcribed it
243
- return {
244
- message: {
245
- msgId,
246
- msgType: "text",
247
- content,
248
- fromUser,
249
- chatType,
250
- chatId,
251
- responseUrl,
252
- },
253
- query: { timestamp, nonce },
254
- };
255
- }
256
- else if (msgtype === "event") {
257
- logger.info("Received event", { event: data.event });
258
- return {
259
- event: data.event,
260
- query: { timestamp, nonce },
261
- };
262
- }
263
- else if (msgtype === "mixed") {
264
- logger.warn("Mixed message type not fully supported", { data });
265
- return null;
266
- }
267
- else {
268
- logger.warn("Unknown message type", { msgtype });
269
- return null;
270
- }
108
+ // 3. Decrypt
109
+ let decryptedContent;
110
+ try {
111
+ const result = this.crypto.decrypt(encrypt);
112
+ decryptedContent = result.message;
113
+ logger.debug("Decrypted content", { content: decryptedContent.substring(0, 300) });
114
+ } catch (e) {
115
+ logger.error("Message decrypt failed", {
116
+ error: e instanceof Error ? e.message : String(e),
117
+ });
118
+ return null;
271
119
  }
272
120
 
273
- // =========================================================================
274
- // Build Stream Response (AI Bot format)
275
- // 完整支持企业微信流式消息所有字段
276
- // =========================================================================
277
- buildStreamResponse(streamId, content, finish, timestamp, nonce, options = {}) {
278
- const stream = {
279
- id: streamId,
280
- finish: finish,
281
- content: content, // 最长20480字节,utf8编码
282
- };
283
-
284
- // 可选: 图文混排消息列表 (仅在finish=true时支持image)
285
- if (options.msgItem && options.msgItem.length > 0) {
286
- stream.msg_item = options.msgItem;
287
- }
121
+ // 4. Parse decrypted JSON content (AI Bot format)
122
+ let data;
123
+ try {
124
+ data = JSON.parse(decryptedContent);
125
+ logger.debug("Parsed message data", {
126
+ msgtype: data.msgtype,
127
+ keys: Object.keys(data),
128
+ text: JSON.stringify(data.text),
129
+ });
130
+ } catch (e) {
131
+ logger.error("Failed to parse decrypted content as JSON", {
132
+ error: e instanceof Error ? e.message : String(e),
133
+ content: decryptedContent.substring(0, 200),
134
+ });
135
+ return null;
136
+ }
288
137
 
289
- // 可选: 用户反馈追踪ID (首次回复时设置,最长256字节)
290
- if (options.feedbackId) {
291
- stream.feedback = { id: options.feedbackId };
138
+ // 5. Process based on message type
139
+ const msgtype = data.msgtype;
140
+
141
+ if (msgtype === "text") {
142
+ // AI Bot format: text.content
143
+ const content = data.text?.content || "";
144
+ const msgId = data.msgid || `msg_${Date.now()}`;
145
+ const fromUser = data.from?.userid || ""; // Note: "userid" not "user_id"
146
+ const responseUrl = data.response_url || "";
147
+ const chatType = data.chattype || "single";
148
+ const chatId = data.chatid || "";
149
+ const aibotId = data.aibotid || "";
150
+
151
+ // Parse quoted message metadata when present.
152
+ const quote = data.quote
153
+ ? {
154
+ msgType: data.quote.msgtype,
155
+ content: data.quote.text?.content || data.quote.image?.url || "",
156
+ }
157
+ : null;
158
+
159
+ // Check for duplicates
160
+ if (this.deduplicator.isDuplicate(msgId)) {
161
+ logger.debug("Duplicate message ignored", { msgId });
162
+ return WecomWebhook.DUPLICATE;
163
+ }
164
+
165
+ logger.info("Received text message", {
166
+ fromUser,
167
+ chatType,
168
+ chatId: chatId || "(private)",
169
+ content: content.substring(0, 50),
170
+ });
171
+
172
+ return {
173
+ message: {
174
+ msgId,
175
+ msgType: "text",
176
+ content,
177
+ fromUser,
178
+ chatType,
179
+ chatId,
180
+ aibotId,
181
+ quote,
182
+ responseUrl,
183
+ },
184
+ query: { timestamp, nonce },
185
+ };
186
+ } else if (msgtype === "stream") {
187
+ // Stream continuation request from WeCom
188
+ const streamId = data.stream?.id;
189
+ logger.debug("Received stream refresh request", { streamId });
190
+ return {
191
+ stream: {
192
+ id: streamId,
193
+ },
194
+ query: { timestamp, nonce },
195
+ rawData: data,
196
+ };
197
+ } else if (msgtype === "image") {
198
+ const imageUrl = data.image?.url;
199
+ const msgId = data.msgid || `msg_${Date.now()}`;
200
+ const fromUser = data.from?.userid || "";
201
+ const responseUrl = data.response_url || "";
202
+ const chatType = data.chattype || "single";
203
+ const chatId = data.chatid || "";
204
+
205
+ if (this.deduplicator.isDuplicate(msgId)) {
206
+ logger.debug("Duplicate image message ignored", { msgId });
207
+ return WecomWebhook.DUPLICATE;
208
+ }
209
+
210
+ logger.info("Received image message", { fromUser, chatType, imageUrl });
211
+
212
+ return {
213
+ message: {
214
+ msgId,
215
+ msgType: "image",
216
+ imageUrl,
217
+ fromUser,
218
+ chatType,
219
+ chatId,
220
+ responseUrl,
221
+ },
222
+ query: { timestamp, nonce },
223
+ };
224
+ } else if (msgtype === "voice") {
225
+ // Voice message (single chat only) - WeCom automatically transcribes to text
226
+ const content = data.voice?.content || "";
227
+ const msgId = data.msgid || `msg_${Date.now()}`;
228
+ const fromUser = data.from?.userid || "";
229
+ const responseUrl = data.response_url || "";
230
+ const chatType = data.chattype || "single";
231
+ const chatId = data.chatid || "";
232
+
233
+ // Check for duplicates
234
+ if (this.deduplicator.isDuplicate(msgId)) {
235
+ logger.debug("Duplicate voice message ignored", { msgId });
236
+ return WecomWebhook.DUPLICATE;
237
+ }
238
+
239
+ // Validate content
240
+ if (!content.trim()) {
241
+ logger.warn("Empty voice message received", { msgId, fromUser });
242
+ return null;
243
+ }
244
+
245
+ logger.info("Received voice message (auto-transcribed by WeCom)", {
246
+ fromUser,
247
+ chatType,
248
+ chatId: chatId || "(private)",
249
+ originalType: "voice",
250
+ transcribedLength: content.length,
251
+ preview: content.substring(0, 50),
252
+ });
253
+
254
+ // Treat voice as text since WeCom already transcribed it
255
+ return {
256
+ message: {
257
+ msgId,
258
+ msgType: "text",
259
+ content,
260
+ fromUser,
261
+ chatType,
262
+ chatId,
263
+ responseUrl,
264
+ },
265
+ query: { timestamp, nonce },
266
+ };
267
+ } else if (msgtype === "event") {
268
+ logger.info("Received event", { event: data.event });
269
+ return {
270
+ event: data.event,
271
+ query: { timestamp, nonce },
272
+ };
273
+ } else if (msgtype === "mixed") {
274
+ // Mixed message: array of text + image items.
275
+ const msgId = data.msgid || `msg_${Date.now()}`;
276
+ const fromUser = data.from?.userid || "";
277
+ const responseUrl = data.response_url || "";
278
+ const chatType = data.chattype || "single";
279
+ const chatId = data.chatid || "";
280
+ const aibotId = data.aibotid || "";
281
+
282
+ if (this.deduplicator.isDuplicate(msgId)) {
283
+ logger.debug("Duplicate mixed message ignored", { msgId });
284
+ return WecomWebhook.DUPLICATE;
285
+ }
286
+
287
+ const msgItems = data.mixed?.msg_item || [];
288
+ const textParts = [];
289
+ const imageUrls = [];
290
+
291
+ for (const item of msgItems) {
292
+ if (item.msgtype === "text" && item.text?.content) {
293
+ textParts.push(item.text.content);
294
+ } else if (item.msgtype === "image" && item.image?.url) {
295
+ imageUrls.push(item.image.url);
292
296
  }
293
-
294
- const plain = {
295
- msgtype: "stream",
296
- stream: stream,
297
- };
298
-
299
- const plainStr = JSON.stringify(plain);
300
- const encrypted = this.crypto.encrypt(plainStr);
301
- const signature = this.crypto.getSignature(timestamp, nonce, encrypted);
302
-
303
- return JSON.stringify({
304
- encrypt: encrypted,
305
- msgsignature: signature,
306
- timestamp: timestamp,
307
- nonce: nonce,
308
- });
297
+ }
298
+
299
+ const content = textParts.join("\n");
300
+
301
+ logger.info("Received mixed message", {
302
+ fromUser,
303
+ chatType,
304
+ chatId: chatId || "(private)",
305
+ textParts: textParts.length,
306
+ imageCount: imageUrls.length,
307
+ contentPreview: content.substring(0, 50),
308
+ });
309
+
310
+ return {
311
+ message: {
312
+ msgId,
313
+ msgType: "mixed",
314
+ content,
315
+ imageUrls,
316
+ fromUser,
317
+ chatType,
318
+ chatId,
319
+ aibotId,
320
+ responseUrl,
321
+ },
322
+ query: { timestamp, nonce },
323
+ };
324
+ } else if (msgtype === "file") {
325
+ const fileUrl = data.file?.url || "";
326
+ const fileName = data.file?.name || data.file?.filename || "";
327
+ const msgId = data.msgid || `msg_${Date.now()}`;
328
+ const fromUser = data.from?.userid || "";
329
+ const responseUrl = data.response_url || "";
330
+ const chatType = data.chattype || "single";
331
+ const chatId = data.chatid || "";
332
+
333
+ if (this.deduplicator.isDuplicate(msgId)) {
334
+ logger.debug("Duplicate file message ignored", { msgId });
335
+ return WecomWebhook.DUPLICATE;
336
+ }
337
+
338
+ logger.info("Received file message", { fromUser, fileName, fileUrl: fileUrl.substring(0, 80) });
339
+
340
+ return {
341
+ message: {
342
+ msgId,
343
+ msgType: "file",
344
+ fileUrl,
345
+ fileName,
346
+ fromUser,
347
+ chatType,
348
+ chatId,
349
+ responseUrl,
350
+ },
351
+ query: { timestamp, nonce },
352
+ };
353
+ } else if (msgtype === "location") {
354
+ const msgId = data.msgid || `msg_${Date.now()}`;
355
+ const fromUser = data.from?.userid || "";
356
+ const responseUrl = data.response_url || "";
357
+ const chatType = data.chattype || "single";
358
+ const chatId = data.chatid || "";
359
+ const latitude = data.location?.latitude || "";
360
+ const longitude = data.location?.longitude || "";
361
+ const name = data.location?.name || data.location?.label || "";
362
+
363
+ if (this.deduplicator.isDuplicate(msgId)) {
364
+ logger.debug("Duplicate location message ignored", { msgId });
365
+ return WecomWebhook.DUPLICATE;
366
+ }
367
+
368
+ const content = name
369
+ ? `[位置] ${name} (${latitude}, ${longitude})`
370
+ : `[位置] ${latitude}, ${longitude}`;
371
+
372
+ logger.info("Received location message", { fromUser, latitude, longitude, name });
373
+
374
+ return {
375
+ message: {
376
+ msgId,
377
+ msgType: "text",
378
+ content,
379
+ fromUser,
380
+ chatType,
381
+ chatId,
382
+ responseUrl,
383
+ },
384
+ query: { timestamp, nonce },
385
+ };
386
+ } else if (msgtype === "link") {
387
+ const msgId = data.msgid || `msg_${Date.now()}`;
388
+ const fromUser = data.from?.userid || "";
389
+ const responseUrl = data.response_url || "";
390
+ const chatType = data.chattype || "single";
391
+ const chatId = data.chatid || "";
392
+ const title = data.link?.title || "";
393
+ const description = data.link?.description || "";
394
+ const url = data.link?.url || "";
395
+
396
+ if (this.deduplicator.isDuplicate(msgId)) {
397
+ logger.debug("Duplicate link message ignored", { msgId });
398
+ return WecomWebhook.DUPLICATE;
399
+ }
400
+
401
+ const parts = [];
402
+ if (title) parts.push(`[链接] ${title}`);
403
+ if (description) parts.push(description);
404
+ if (url) parts.push(url);
405
+ const content = parts.join("\n") || "[链接]";
406
+
407
+ logger.info("Received link message", { fromUser, title, url: url.substring(0, 80) });
408
+
409
+ return {
410
+ message: {
411
+ msgId,
412
+ msgType: "text",
413
+ content,
414
+ fromUser,
415
+ chatType,
416
+ chatId,
417
+ responseUrl,
418
+ },
419
+ query: { timestamp, nonce },
420
+ };
421
+ } else {
422
+ logger.warn("Unknown message type", { msgtype });
423
+ return null;
424
+ }
425
+ }
426
+
427
+ // =========================================================================
428
+ // Build Stream Response (AI Bot format)
429
+ // Supports all core WeCom stream response fields used by this plugin.
430
+ // =========================================================================
431
+ buildStreamResponse(streamId, content, finish, timestamp, nonce, options = {}) {
432
+ const stream = {
433
+ id: streamId,
434
+ finish: finish,
435
+ content: content,
436
+ };
437
+
438
+ // Optional mixed media list (images are valid on finished responses).
439
+ if (options.msgItem && options.msgItem.length > 0) {
440
+ stream.msg_item = options.msgItem;
309
441
  }
310
442
 
311
- /**
312
- * Build success acknowledgment (no reply)
313
- */
314
- buildSuccessAck() {
315
- return "success";
443
+ // Optional feedback tracking id.
444
+ if (options.feedbackId) {
445
+ stream.feedback = { id: options.feedbackId };
316
446
  }
447
+
448
+ const plain = {
449
+ msgtype: "stream",
450
+ stream: stream,
451
+ };
452
+
453
+ const plainStr = JSON.stringify(plain);
454
+ const encrypted = this.crypto.encrypt(plainStr);
455
+ const signature = this.crypto.getSignature(timestamp, nonce, encrypted);
456
+
457
+ return JSON.stringify({
458
+ encrypt: encrypted,
459
+ msgsignature: signature,
460
+ timestamp: timestamp,
461
+ nonce: nonce,
462
+ });
463
+ }
317
464
  }