UNPKG

@warriorteam/redai-zalo-sdk

Version:

Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account v3.0, ZNS with Full Type Safety, Consultation Service, Broadcast Service, Group Messaging with List APIs, Social APIs, Enhanced Article Management, Promotion Service v3.0 with Multip

1,136 lines 62.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConsultationService = void 0; const common_1 = require("../types/common"); /** * Service xử lý các API tin nhắn tư vấn của Zalo Official Account * * Tin nhắn tư vấn (CS - Customer Service) là loại tin nhắn đặc biệt * cho phép OA gửi tin nhắn chủ động đến người dùng trong khung thời gian nhất định * * ĐIỀU KIỆN GỬI TIN TƯ VẤN: * * 1. THỜI GIAN GỬI: * - Chỉ được gửi trong vòng 48 giờ kể từ khi người dùng tương tác cuối cùng với OA * - Tương tác bao gồm: gửi tin nhắn, nhấn button, gọi điện, truy cập website từ OA * * 2. NỘI DUNG TIN NHẮN: * - Phải liên quan đến tư vấn, hỗ trợ khách hàng * - Bao gồm: trả lời câu hỏi, hướng dẫn sử dụng, hỗ trợ kỹ thuật * - Không được chứa nội dung quảng cáo trực tiếp * * 3. TẦN SUẤT GỬI: * - Không giới hạn số lượng tin nhắn tư vấn trong ngày * - Tuy nhiên cần tuân thủ nguyên tắc không spam * * 4. NGƯỜI DÙNG: * - Người dùng phải đã follow OA * - Người dùng không được block OA * - Người dùng phải có tương tác gần đây với OA */ class ConsultationService { constructor(client) { this.client = client; // Zalo API endpoint for consultation messages this.endpoint = "https://openapi.zalo.me/v3.0/oa/message/cs"; } /** * Gửi tin nhắn tư vấn văn bản * @param accessToken Access token của Official Account * @param recipient Thông tin người nhận * @param message Nội dung tin nhắn văn bản * @returns Thông tin tin nhắn đã gửi */ async sendTextMessage(accessToken, recipient, message) { try { // Validate text length theo quy định của Zalo if (!message.text || message.text.trim().length === 0) { throw new common_1.ZaloSDKError("Nội dung tin nhắn không được để trống", -1); } if (message.text.length > 2000) { throw new common_1.ZaloSDKError("Nội dung tin nhắn không được vượt quá 2000 ký tự", -1); } // Request structure theo API spec const request = { recipient: { user_id: recipient.user_id, }, message: { text: message.text, }, }; const result = await this.client.apiPost(this.endpoint, accessToken, request); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.message || "Failed to send consultation text message", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No response data received", -1); } return result.data; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send consultation text message: ${error.message}`, -1, error); } } /** * Gửi tin nhắn tư vấn trích dẫn (quote message) * @param accessToken Access token của Official Account * @param recipient Thông tin người nhận * @param text Nội dung tin nhắn trả lời (tối đa 2000 ký tự) * @param quoteMessageId ID của tin nhắn muốn trích dẫn * @returns Thông tin tin nhắn đã gửi với quota information */ async sendQuoteMessage(accessToken, recipient, text, quoteMessageId) { try { // Validate input parameters if (!text || text.trim().length === 0) { throw new common_1.ZaloSDKError("Nội dung tin nhắn không được để trống", -1); } if (text.length > 2000) { throw new common_1.ZaloSDKError("Nội dung tin nhắn không được vượt quá 2000 ký tự", -1); } if (!quoteMessageId || quoteMessageId.trim().length === 0) { throw new common_1.ZaloSDKError("Quote message ID không được để trống", -1); } if (!recipient.user_id || recipient.user_id.trim().length === 0) { throw new common_1.ZaloSDKError("User ID không được để trống", -1); } // Request structure theo API spec const request = { recipient: { user_id: recipient.user_id, }, message: { text: text.trim(), quote_message_id: quoteMessageId.trim(), }, }; const result = await this.client.apiPost(this.endpoint, accessToken, request); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.message || "Failed to send consultation quote message", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No response data received", -1); } return result.data; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send consultation quote message: ${error.message}`, -1, error); } } /** * Tạo ConsultationQuoteMessage object * @param text Nội dung tin nhắn trả lời * @param quoteMessageId ID của tin nhắn muốn trích dẫn * @param quoteContent Nội dung của tin nhắn được trích dẫn (optional) * @returns ConsultationQuoteMessage object */ createQuoteMessage(text, quoteMessageId, quoteContent) { return { type: "consultation_quote", text: text.trim(), quote: { message_id: quoteMessageId.trim(), content: quoteContent || "", }, }; } /** * Gửi tin nhắn tư vấn hình ảnh bằng URL * * API Specification: * - URL: https://openapi.zalo.me/v3.0/oa/message/cs * - Method: POST * - Content-Type: application/json * * @param accessToken Access token của Official Account * @param userId ID người nhận (user_id) * @param imageUrl URL trực tiếp đến hình ảnh (jpg, png, tối đa 1MB, tỷ lệ tối ưu 16:9) * @param text Tiêu đề ảnh (optional, tối đa 2000 ký tự) * @returns Thông tin tin nhắn đã gửi với quota information */ async sendImageMessage(accessToken, userId, imageUrl, text) { try { if (!imageUrl || imageUrl.trim().length === 0) { throw new common_1.ZaloSDKError("URL hình ảnh không được để trống", -1); } if (text && text.length > 2000) { throw new common_1.ZaloSDKError("Tiêu đề ảnh không được vượt quá 2000 ký tự", -1); } // Request structure theo API specification v3.0 const request = { recipient: { user_id: userId, }, message: { ...(text && text.trim().length > 0 && { text: text.trim() }), attachment: { type: "template", payload: { template_type: "media", elements: [ { media_type: "image", url: imageUrl.trim(), }, ], }, }, }, }; const result = await this.client.apiPost(this.endpoint, accessToken, request); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.message || "Failed to send consultation image message", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No response data received", -1); } return result.data; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send consultation image message: ${error.message}`, -1, error); } } /** * Gửi tin nhắn tư vấn hình ảnh bằng attachment_id (ảnh đã upload trước) * * API Specification: * - URL: https://openapi.zalo.me/v3.0/oa/message/cs * - Method: POST * - Content-Type: application/json * - Lưu ý: Chỉ sử dụng attachment_id HOẶC url, không được dùng cả hai * * @param accessToken Access token của Official Account * @param userId ID người nhận (user_id) * @param attachmentId ID của ảnh đã upload trước đó (từ API upload ảnh) * @param text Tiêu đề ảnh (optional, tối đa 2000 ký tự) * @returns Thông tin tin nhắn đã gửi với quota information */ async sendImageByAttachmentId(accessToken, userId, attachmentId, text) { try { if (!attachmentId || attachmentId.trim().length === 0) { throw new common_1.ZaloSDKError("Attachment ID không được để trống", -1); } if (text && text.length > 2000) { throw new common_1.ZaloSDKError("Tiêu đề ảnh không được vượt quá 2000 ký tự", -1); } // Request structure theo API specification v3.0 const request = { recipient: { user_id: userId, }, message: { ...(text && text.trim().length > 0 && { text: text.trim() }), attachment: { type: "template", payload: { template_type: "media", elements: [ { media_type: "image", attachment_id: attachmentId.trim(), }, ], }, }, }, }; const result = await this.client.apiPost(this.endpoint, accessToken, request); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.message || "Failed to send consultation image message", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No response data received", -1); } return result.data; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send consultation image message: ${error.message}`, -1, error); } } /** * Gửi tin nhắn tư vấn hình ảnh (hỗ trợ cả URL và attachment_id) * * API Specification: * - URL: https://openapi.zalo.me/v3.0/oa/message/cs * - Method: POST * - Content-Type: application/json * - Lưu ý: Chỉ sử dụng MỘT trong hai: imageUrl HOẶC attachmentId * * @param accessToken Access token của Official Account * @param userId ID người nhận (user_id) * @param options Tùy chọn gửi ảnh * @param options.imageUrl URL trực tiếp đến hình ảnh (jpg, png, tối đa 1MB) * @param options.attachmentId ID của ảnh đã upload trước đó * @param options.text Tiêu đề ảnh (optional, tối đa 2000 ký tự) * @returns Thông tin tin nhắn đã gửi với quota information */ async sendImage(accessToken, userId, options) { try { // Validation: phải có ít nhất một trong hai if (!options.imageUrl && !options.attachmentId) { throw new common_1.ZaloSDKError("Phải cung cấp imageUrl hoặc attachmentId", -1); } // Validation: không được có cả hai if (options.imageUrl && options.attachmentId) { throw new common_1.ZaloSDKError("Chỉ được sử dụng imageUrl HOẶC attachmentId, không được cả hai", -1); } // Validation imageUrl if (options.imageUrl && options.imageUrl.trim().length === 0) { throw new common_1.ZaloSDKError("URL hình ảnh không được để trống", -1); } // Validation attachmentId if (options.attachmentId && options.attachmentId.trim().length === 0) { throw new common_1.ZaloSDKError("Attachment ID không được để trống", -1); } // Validation text if (options.text && options.text.length > 2000) { throw new common_1.ZaloSDKError("Tiêu đề ảnh không được vượt quá 2000 ký tự", -1); } // Tạo element dựa trên loại input const element = { media_type: "image", }; if (options.imageUrl) { element.url = options.imageUrl.trim(); } else if (options.attachmentId) { element.attachment_id = options.attachmentId.trim(); } // Request structure theo API specification v3.0 const request = { recipient: { user_id: userId, }, message: { ...(options.text && options.text.trim().length > 0 && { text: options.text.trim() }), attachment: { type: "template", payload: { template_type: "media", elements: [element], }, }, }, }; const result = await this.client.apiPost(this.endpoint, accessToken, request); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.message || "Failed to send consultation image message", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No response data received", -1); } return result.data; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send consultation image message: ${error.message}`, -1, error); } } /** * Gửi tin nhắn tư vấn ảnh GIF động * * API Specification: * - URL: https://openapi.zalo.me/v3.0/oa/message/cs * - Method: POST * - Content-Type: application/json * - Lưu ý: width và height là BẮT BUỘC cho media_type = "gif" * * @param accessToken Access token của Official Account * @param userId ID người nhận (user_id) * @param gifUrl URL trực tiếp đến ảnh GIF (tối đa 1MB) * @param width Chiều rộng của ảnh GIF (bắt buộc, > 0) * @param height Chiều cao của ảnh GIF (bắt buộc, > 0) * @param text Tiêu đề ảnh (optional, tối đa 2000 ký tự) * @returns Thông tin tin nhắn đã gửi với quota information */ async sendGifMessage(accessToken, userId, gifUrl, width, height, text) { try { if (!gifUrl || gifUrl.trim().length === 0) { throw new common_1.ZaloSDKError("URL GIF không được để trống", -1); } if (width <= 0 || height <= 0) { throw new common_1.ZaloSDKError("Chiều rộng và chiều cao phải lớn hơn 0", -1); } if (text && text.length > 2000) { throw new common_1.ZaloSDKError("Tiêu đề ảnh không được vượt quá 2000 ký tự", -1); } // Request structure theo API specification v3.0 const request = { recipient: { user_id: userId, }, message: { ...(text && text.trim().length > 0 && { text: text.trim() }), attachment: { type: "template", payload: { template_type: "media", elements: [ { media_type: "gif", url: gifUrl.trim(), width: width, height: height, }, ], }, }, }, }; const result = await this.client.apiPost(this.endpoint, accessToken, request); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.message || "Failed to send consultation gif message", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No response data received", -1); } return result.data; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send consultation gif message: ${error.message}`, -1, error); } } /** * Gửi tin nhắn tư vấn đính kèm file * Endpoint: https://openapi.zalo.me/v3.0/oa/message/cs * Method: POST * * @param accessToken Access token của Official Account * @param userId ID người nhận (user_id) * @param fileToken Token của file đã upload (từ API upload file) * @returns Thông tin tin nhắn đã gửi * * Note: Cần sử dụng API upload file trước để lấy token */ async sendFileMessage(accessToken, userId, fileToken) { try { if (!fileToken || fileToken.trim().length === 0) { throw new common_1.ZaloSDKError("File token không được để trống", -1); } // Request structure theo API spec const request = { recipient: { user_id: userId, }, message: { attachment: { type: "file", payload: { token: fileToken, }, }, }, }; const result = await this.client.apiPost(this.endpoint, accessToken, request); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.message || "Failed to send consultation file message", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No response data received", -1); } return result.data; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send consultation file message: ${error.message}`, -1, error); } } /** * Gửi tin nhắn tư vấn theo mẫu yêu cầu thông tin người dùng * Endpoint: https://openapi.zalo.me/v3.0/oa/message/cs * Method: POST * * @param accessToken Access token của Official Account * @param userId ID người nhận (user_id) * @param title Tiêu đề hiển thị của template (tối đa 100 ký tự) * @param subtitle Tiêu đề phụ của template (tối đa 500 ký tự) * @param imageUrl Đường dẫn đến ảnh * @returns Thông tin tin nhắn đã gửi */ async sendRequestUserInfoMessage(accessToken, userId, title, subtitle, imageUrl) { try { // Validation theo API spec if (!title || title.trim().length === 0) { throw new common_1.ZaloSDKError("Tiêu đề không được để trống", -1); } if (title.length > 100) { throw new common_1.ZaloSDKError("Tiêu đề không được vượt quá 100 ký tự", -1); } if (!subtitle || subtitle.trim().length === 0) { throw new common_1.ZaloSDKError("Tiêu đề phụ không được để trống", -1); } if (subtitle.length > 500) { throw new common_1.ZaloSDKError("Tiêu đề phụ không được vượt quá 500 ký tự", -1); } if (!imageUrl || imageUrl.trim().length === 0) { throw new common_1.ZaloSDKError("URL hình ảnh không được để trống", -1); } // Request structure theo API spec const request = { recipient: { user_id: userId, }, message: { attachment: { type: "template", payload: { template_type: "request_user_info", elements: [ { title: title, subtitle: subtitle, image_url: imageUrl, }, ], }, }, }, }; const result = await this.client.apiPost(this.endpoint, accessToken, request); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.message || "Failed to send request user info message", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No response data received", -1); } return result.data; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send request user info message: ${error.message}`, -1, error); } } /** * Gửi tin nhắn tư vấn kèm Sticker * Endpoint: https://openapi.zalo.me/v3.0/oa/message/cs * Method: POST * * @param accessToken Access token của Official Account * @param userId ID người nhận (user_id) * @param stickerAttachmentId ID của sticker (lấy từ https://stickers.zaloapp.com/) * @returns Thông tin tin nhắn đã gửi * * Note: Sticker ID lấy từ nguồn https://stickers.zaloapp.com/ * Xem video hướng dẫn: https://vimeo.com/649330161 */ async sendStickerMessage(accessToken, userId, stickerAttachmentId) { try { if (!stickerAttachmentId || stickerAttachmentId.trim().length === 0) { throw new common_1.ZaloSDKError("Sticker attachment ID không được để trống", -1); } // Request structure theo API spec const request = { recipient: { user_id: userId, }, message: { attachment: { type: "template", payload: { template_type: "media", elements: [ { media_type: "sticker", attachment_id: stickerAttachmentId, }, ], }, }, }, }; const result = await this.client.apiPost(this.endpoint, accessToken, request); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.message || "Failed to send consultation sticker message", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No response data received", -1); } return result.data; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send consultation sticker message: ${error.message}`, -1, error); } } /** * Gửi chuỗi tin nhắn tư vấn với delay tùy chỉnh * Hỗ trợ tất cả các loại tin nhắn: text, image, gif, file, sticker, request_user_info * * @param request Thông tin request gửi chuỗi tin nhắn * @returns Kết quả gửi từng tin nhắn */ async sendMessageSequence(request) { const startTime = Date.now(); const results = []; let successCount = 0; let failureCount = 0; try { // Validate input if (!request.accessToken || request.accessToken.trim().length === 0) { throw new common_1.ZaloSDKError("Access token không được để trống", -1); } if (!request.userId || request.userId.trim().length === 0) { throw new common_1.ZaloSDKError("User ID không được để trống", -1); } if (!request.messages || request.messages.length === 0) { throw new common_1.ZaloSDKError("Danh sách tin nhắn không được để trống", -1); } // Gửi từng tin nhắn theo thứ tự for (let i = 0; i < request.messages.length; i++) { const message = request.messages[i]; const messageResult = { index: i, type: message.type, success: false, timestamp: Date.now(), }; try { let response; // Gửi tin nhắn theo loại switch (message.type) { case "text": if (!message.text) { throw new Error("Text không được để trống cho text message"); } response = await this.sendTextMessage(request.accessToken, { user_id: request.userId }, { type: "text", text: message.text }); break; case "image": if (message.imageUrl) { response = await this.sendImageMessage(request.accessToken, request.userId, message.imageUrl, message.text); } else if (message.attachmentId) { response = await this.sendImageByAttachmentId(request.accessToken, request.userId, message.attachmentId, message.text); } else { throw new Error("imageUrl hoặc attachmentId là bắt buộc cho image message"); } break; case "gif": if (!message.gifUrl || !message.width || !message.height) { throw new Error("gifUrl, width và height là bắt buộc cho gif message"); } response = await this.sendGifMessage(request.accessToken, request.userId, message.gifUrl, message.width, message.height, message.text); break; case "file": if (!message.fileToken) { throw new Error("fileToken là bắt buộc cho file message"); } response = await this.sendFileMessage(request.accessToken, request.userId, message.fileToken); break; case "sticker": if (!message.stickerAttachmentId) { throw new Error("stickerAttachmentId là bắt buộc cho sticker message"); } response = await this.sendStickerMessage(request.accessToken, request.userId, message.stickerAttachmentId); break; case "request_user_info": if (!message.title || !message.subtitle || !message.imageUrl) { throw new Error("title, subtitle và imageUrl là bắt buộc cho request_user_info message"); } response = await this.sendRequestUserInfoMessage(request.accessToken, request.userId, message.title, message.subtitle, message.imageUrl); break; default: throw new Error(`Loại tin nhắn không được hỗ trợ: ${message.type}`); } // Ghi nhận thành công messageResult.success = true; messageResult.response = response; successCount++; } catch (error) { // Ghi nhận thất bại messageResult.success = false; messageResult.error = error instanceof Error ? error.message : String(error); failureCount++; } results.push(messageResult); // Delay trước khi gửi tin nhắn tiếp theo (trừ tin nhắn cuối cùng) if (i < request.messages.length - 1) { const delayTime = message.delay ?? request.defaultDelay ?? 0; if (delayTime > 0) { await this.sleep(delayTime); } } } const totalDuration = Date.now() - startTime; return { successCount, failureCount, results, totalDuration, }; } catch (error) { // Lỗi tổng quát const totalDuration = Date.now() - startTime; if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send message sequence: ${error.message}`, -1, { successCount, failureCount, results, totalDuration, }); } } /** * Gửi chuỗi tin nhắn tư vấn tới nhiều users với callback tracking * * @param request Thông tin request gửi chuỗi tin nhắn tới nhiều users * @returns Kết quả gửi tin nhắn cho tất cả users */ async sendMessageSequenceToMultipleUsers(request) { const startTime = Date.now(); const userResults = []; let successfulUsers = 0; let failedUsers = 0; let totalSuccessfulMessages = 0; let totalFailedMessages = 0; try { // Validate input if (!request.accessToken || request.accessToken.trim().length === 0) { throw new common_1.ZaloSDKError("Access token không được để trống", -1); } if (!request.userIds || request.userIds.length === 0) { throw new common_1.ZaloSDKError("Danh sách user IDs không được để trống", -1); } if (!request.messages || request.messages.length === 0) { throw new common_1.ZaloSDKError("Danh sách tin nhắn không được để trống", -1); } // Loại bỏ user IDs trùng lặp và rỗng const uniqueUserIds = [...new Set(request.userIds.filter(id => id && id.trim().length > 0))]; if (uniqueUserIds.length === 0) { throw new common_1.ZaloSDKError("Không có user ID hợp lệ nào", -1); } // Gửi tin nhắn cho từng user tuần tự for (let i = 0; i < uniqueUserIds.length; i++) { const userId = uniqueUserIds[i]; const userStartTime = Date.now(); // Callback: Bắt đầu gửi cho user if (request.onProgress) { request.onProgress({ userId, userIndex: i, totalUsers: uniqueUserIds.length, status: 'started', startTime: userStartTime, }); } const userResult = { userId, userIndex: i, success: false, startTime: userStartTime, endTime: 0, duration: 0, }; try { // Gửi chuỗi tin nhắn cho user này const messageSequenceResult = await this.sendMessageSequence({ accessToken: request.accessToken, userId, messages: request.messages, defaultDelay: request.defaultDelay, }); // Ghi nhận thành công const userEndTime = Date.now(); userResult.success = true; userResult.messageSequenceResult = messageSequenceResult; userResult.endTime = userEndTime; userResult.duration = userEndTime - userStartTime; successfulUsers++; totalSuccessfulMessages += messageSequenceResult.successCount; totalFailedMessages += messageSequenceResult.failureCount; // Callback: Hoàn thành thành công if (request.onProgress) { request.onProgress({ userId, userIndex: i, totalUsers: uniqueUserIds.length, status: 'completed', result: messageSequenceResult, startTime: userStartTime, endTime: userEndTime, }); } } catch (error) { // Ghi nhận thất bại const userEndTime = Date.now(); const errorMessage = error instanceof Error ? error.message : String(error); userResult.success = false; userResult.error = errorMessage; userResult.endTime = userEndTime; userResult.duration = userEndTime - userStartTime; failedUsers++; // Với user thất bại, coi như tất cả tin nhắn đều thất bại totalFailedMessages += request.messages.length; // Callback: Thất bại if (request.onProgress) { request.onProgress({ userId, userIndex: i, totalUsers: uniqueUserIds.length, status: 'failed', error: errorMessage, startTime: userStartTime, endTime: userEndTime, }); } } userResults.push(userResult); // Delay giữa các user (trừ user cuối cùng) if (i < uniqueUserIds.length - 1 && request.delayBetweenUsers && request.delayBetweenUsers > 0) { await this.sleep(request.delayBetweenUsers); } } const totalDuration = Date.now() - startTime; const totalMessages = uniqueUserIds.length * request.messages.length; return { totalUsers: uniqueUserIds.length, successfulUsers, failedUsers, userResults, totalDuration, messageStats: { totalSuccessfulMessages, totalFailedMessages, totalMessages, }, }; } catch (error) { const totalDuration = Date.now() - startTime; if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send message sequence to multiple users: ${error.message}`, -1, { totalUsers: request.userIds?.length || 0, successfulUsers, failedUsers, userResults, totalDuration, messageStats: { totalSuccessfulMessages, totalFailedMessages, totalMessages: (request.userIds?.length || 0) * (request.messages?.length || 0), }, }); } } /** * Gửi tin nhắn tư vấn tùy chỉnh tới nhiều users với callback tracking * Mỗi user có thể có bộ tin nhắn riêng biệt * * @param request Thông tin request gửi tin nhắn tùy chỉnh tới nhiều users * @returns Kết quả gửi tin nhắn cho tất cả users */ async sendCustomMessageSequenceToMultipleUsers(request) { const startTime = Date.now(); const userResults = []; let successfulUsers = 0; let failedUsers = 0; let totalSuccessfulMessages = 0; let totalFailedMessages = 0; try { // Validate input if (!request.accessToken || request.accessToken.trim().length === 0) { throw new common_1.ZaloSDKError("Access token không được để trống", -1); } if (!request.userCustomMessages || request.userCustomMessages.length === 0) { throw new common_1.ZaloSDKError("Danh sách user custom messages không được để trống", -1); } // Validate từng user custom message const validUserCustomMessages = []; const seenUserIds = new Set(); for (let i = 0; i < request.userCustomMessages.length; i++) { const userCustomMessage = request.userCustomMessages[i]; // Validate userId if (!userCustomMessage.userId || userCustomMessage.userId.trim().length === 0) { throw new common_1.ZaloSDKError(`User custom message ${i + 1}: userId không được để trống`, -1); } // Check duplicate userId if (seenUserIds.has(userCustomMessage.userId)) { throw new common_1.ZaloSDKError(`User ID "${userCustomMessage.userId}" bị trùng lặp`, -1); } seenUserIds.add(userCustomMessage.userId); // Validate messages if (!userCustomMessage.messages || userCustomMessage.messages.length === 0) { throw new common_1.ZaloSDKError(`User custom message ${i + 1}: danh sách tin nhắn không được để trống`, -1); } validUserCustomMessages.push(userCustomMessage); } // Gửi tin nhắn cho từng user tuần tự for (let i = 0; i < validUserCustomMessages.length; i++) { const userCustomMessage = validUserCustomMessages[i]; const userId = userCustomMessage.userId; const userStartTime = Date.now(); // Callback: Bắt đầu gửi cho user if (request.onProgress) { request.onProgress({ userId, userIndex: i, totalUsers: validUserCustomMessages.length, status: 'started', startTime: userStartTime, }); } const userResult = { userId, userIndex: i, success: false, startTime: userStartTime, endTime: 0, duration: 0, }; try { // Gửi chuỗi tin nhắn tùy chỉnh cho user này const messageSequenceResult = await this.sendMessageSequence({ accessToken: request.accessToken, userId, messages: userCustomMessage.messages, defaultDelay: request.defaultDelay, }); // Ghi nhận thành công const userEndTime = Date.now(); userResult.success = true; userResult.messageSequenceResult = messageSequenceResult; userResult.endTime = userEndTime; userResult.duration = userEndTime - userStartTime; successfulUsers++; totalSuccessfulMessages += messageSequenceResult.successCount; totalFailedMessages += messageSequenceResult.failureCount; // Callback: Hoàn thành thành công if (request.onProgress) { request.onProgress({ userId, userIndex: i, totalUsers: validUserCustomMessages.length, status: 'completed', result: messageSequenceResult, startTime: userStartTime, endTime: userEndTime, }); } } catch (error) { // Ghi nhận thất bại const userEndTime = Date.now(); const errorMessage = error instanceof Error ? error.message : String(error); userResult.success = false; userResult.error = errorMessage; userResult.endTime = userEndTime; userResult.duration = userEndTime - userStartTime; failedUsers++; // Với user thất bại, coi như tất cả tin nhắn đều thất bại totalFailedMessages += userCustomMessage.messages.length; // Callback: Thất bại if (request.onProgress) { request.onProgress({ userId, userIndex: i, totalUsers: validUserCustomMessages.length, status: 'failed', error: errorMessage, startTime: userStartTime, endTime: userEndTime, }); } } userResults.push(userResult); // Delay giữa các user (trừ user cuối cùng) if (i < validUserCustomMessages.length - 1 && request.delayBetweenUsers && request.delayBetweenUsers > 0) { await this.sleep(request.delayBetweenUsers); } } const totalDuration = Date.now() - startTime; // Tính tổng số tin nhắn const totalMessages = validUserCustomMessages.reduce((total, userCustomMessage) => { return total + userCustomMessage.messages.length; }, 0); return { totalUsers: validUserCustomMessages.length, successfulUsers, failedUsers, userResults, totalDuration, messageStats: { totalSuccessfulMessages, totalFailedMessages, totalMessages, }, }; } catch (error) { const totalDuration = Date.now() - startTime; if (error instanceof common_1.ZaloSDKError) { throw error; } // Tính tổng số tin nhắn cho error response const totalMessages = request.userCustomMessages?.reduce((total, userCustomMessage) => { return total + (userCustomMessage.messages?.length || 0); }, 0) || 0; throw new common_1.ZaloSDKError(`Failed to send custom message sequence to multiple users: ${error.message}`, -1, { totalUsers: request.userCustomMessages?.length || 0, successfulUsers, failedUsers, userResults, totalDuration, messageStats: { totalSuccessfulMessages, totalFailedMessages, totalMessages, }, }); } } /** * Gửi tin nhắn template tổng quát với elements và buttons * Endpoint: https://openapi.zalo.me/v3.0/oa/message/cs * Method: POST * * @param accessToken Access token của Official Account * @param userId ID người nhận (user_id) * @param templateType Loại template (ví dụ: "request_user_info") * @param elements Mảng elements (tối đa 5 phần tử) * @param buttons Mảng buttons (tối đa 5 phần tử, optional) * @returns Thông tin tin nhắn đã gửi */ async sendTemplateMessage(accessToken, userId, templateType, elements, buttons) { try { // Validation cơ bản if (!templateType || templateType.trim().length === 0) { throw new common_1.ZaloSDKError("Template type không được để trống", -1); } if (!elements || elements.length === 0) { throw new common_1.ZaloSDKError("Elements không được để trống", -1); } if (elements.length > 5) { throw new common_1.ZaloSDKError("Elements không được vượt quá 5 phần tử", -1); } if (buttons && buttons.length > 5) { throw new common_1.ZaloSDKError("Buttons không được vượt quá 5 phần tử", -1); } // Validate từng element elements.forEach((element, index) => { this.validateTemplateElement(element, index); }); // Validate từng button (nếu có) if (buttons) { buttons.forEach((button, index) => { this.validateTemplateButton(button, index); }); } // Request structure theo API specification const request = { recipient: { user_id: userId, }, message: { attachment: { type: "template", payload: { template_type: templateType, elements: elements, ...(buttons && buttons.length > 0 && { buttons: buttons }), }, }, }, }; const result = await this.client.apiPost(this.endpoint, accessToken, request); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.message || "Failed to send template message", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No response data received", -1); } return result.data; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to send template message: ${error.message}`, -1, error); } } /** * Validate template element theo quy tắc API * @param element Template element cần validate * @param index Index của element trong mảng (để báo lỗi) */ validateTemplateElement(element, index) { // title là bắt buộc, ≤ 100 ký tự if (!element.title || element.title.trim().length === 0) { throw new common_1.ZaloSDKError(`Element ${index + 1}: title không được để trống`, -1); } if (element.title.length > 100) { throw new common_1.ZaloSDKError(`Element ${index + 1}: title không được vượt quá 100 ký tự`, -1); } // subtitle: bắt buộc cho element đầu tiên, tùy chọn cho các element sau, ≤ 500 ký tự if (index === 0) { if (!element.subtitle || element.subtitle.trim().length === 0) { throw new common_1.ZaloSDKError("Element đầu tiên phải có subtitle", -1); } } if (element.subtitle && element.subtitle.length > 500) { throw new common_1.ZaloSDKError(`Element ${index + 1}: subtitle không được vượt quá 500 ký tự`, -1); } // Validate default_action nếu có if (element.default_action) { this.validateTemplateAction(element.default_action, `Element ${index + 1} default_action`); } } /** * Validate template button theo quy tắc API * @param button Template button cần validate * @param index Index của button trong mảng (để báo lỗi) */ validateTemplateButton(button, index) { // title là bắt buộc, ≤ 100 ký tự if (!button.title || button.title.trim().length === 0) { throw new common_1.ZaloSDKError(`Button ${index + 1}: title không được để trống`, -1); } if (button.title.length > 100) { throw new common_1.ZaloSDKError(`Button ${index + 1}: title không được vượt quá 100 ký tự`, -1); } // type là bắt buộc if (!button.type || button.type.trim().length === 0) { throw new common_1.ZaloSDKError(`Button ${index + 1}: type không được để trống`, -1); } // Validate payload theo type this.validateButtonPayload(button, index); } /** * Validate template action (cho default_action) * @param action Template action cần validate * @param context Context để báo lỗi */ validateTemplateAction(action, context) { if (!action.type || act