@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
JavaScript
"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