@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
780 lines • 34.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GroupMessageService = void 0;
const common_1 = require("../types/common");
/**
* Service for handling Zalo Official Account Group Message Framework (GMF) APIs
*
* CONDITIONS FOR USING ZALO GMF:
*
* 1. OPT-IN CONDITIONS FOR SENDING GROUP MESSAGES:
* - OA must have the group_id of the chat group
* - OA must be added to the chat group by group admin
* - OA must have permission to send messages in the group (granted by group admin)
* - Chat group must be active (not locked or disbanded)
*
* 2. ACCESS PERMISSIONS:
* - Application needs to be granted group chat management permissions
* - Access token must have "manage_group" scope or equivalent
* - OA must be authenticated and have active status
*
* 3. LIMITS AND CONSTRAINTS:
* - Can only send messages to groups that OA has joined
* - Cannot send messages to private groups that OA hasn't been invited to
* - Must comply with Zalo's message sending frequency limits
* - Message content must comply with Zalo's content policy
*
* 4. SUPPORTED MESSAGE TYPES:
* - Text message: Plain text messages
* - Image message: Image messages (JPG, PNG, GIF - max 5MB)
* - File message: File attachments (max 25MB)
* - Sticker message: Stickers from Zalo collection
* - Mention message: Tag/mention specific members
*
* 5. TECHNICAL REQUIREMENTS:
* - Use HTTPS for all API calls
* - Content-Type: application/json for text/mention messages
* - Content-Type: multipart/form-data for file/image uploads
*/
class GroupMessageService {
constructor(client) {
this.client = client;
// Zalo API endpoints - organized by functionality
this.endpoints = {
// Group message endpoints
message: {
group: "https://openapi.zalo.me/v3.0/oa/group/message",
},
// Group management endpoints
group: {
getInfo: "https://openapi.zalo.me/v3.0/oa/group/getinfo",
getMembers: "https://openapi.zalo.me/v3.0/oa/group/getmembers",
},
};
}
/**
* Send text message to group
* @param accessToken OA access token
* @param groupId Group ID
* @param message Text message content
* @returns Send result
*/
async sendTextMessage(accessToken, groupId, message) {
try {
const response = await this.client.apiPost(this.endpoints.message.group, accessToken, {
recipient: {
group_id: groupId,
},
message: {
text: message.text,
},
});
return response;
}
catch (error) {
throw this.handleGroupMessageError(error, "Failed to send text message to group");
}
}
/**
* Send image message to group
* @param accessToken OA access token
* @param groupId Group ID
* @param message Image message content
* @returns Send result
*/
async sendImageMessage(accessToken, groupId, message) {
try {
// Validate that either imageUrl or attachmentId is provided
if (!message.imageUrl && !message.attachmentId) {
throw new Error("Either imageUrl or attachmentId must be provided");
}
// Validate caption length
if (message.caption && message.caption.length > 2000) {
throw new Error("Caption cannot exceed 2000 characters");
}
// Prepare attachment payload
const attachmentPayload = {
type: "template",
payload: {
template_type: "media",
elements: [
{
media_type: "image",
...(message.attachmentId
? { attachment_id: message.attachmentId }
: { url: message.imageUrl }),
},
],
},
};
// Prepare message payload with correct structure
const messagePayload = {
attachment: attachmentPayload,
};
// Add text caption if provided - must be at same level as attachment
if (message.caption) {
messagePayload.text = message.caption;
}
const response = await this.client.apiPost(this.endpoints.message.group, accessToken, {
recipient: {
group_id: groupId,
},
message: messagePayload,
});
return response;
}
catch (error) {
throw this.handleGroupMessageError(error, "Failed to send image message to group");
}
}
/**
* Send file message to group
* @param accessToken OA access token
* @param groupId Group ID
* @param message File message content
* @returns Send result
*/
async sendFileMessage(accessToken, groupId, message) {
try {
const response = await this.client.apiPost(this.endpoints.message.group, accessToken, {
recipient: {
group_id: groupId,
},
message: {
attachment: {
type: "file",
payload: {
token: message.fileToken,
},
},
},
});
return response;
}
catch (error) {
throw this.handleGroupMessageError(error, "Failed to send file message to group");
}
}
/**
* Send sticker message to group
* @param accessToken OA access token
* @param groupId Group ID
* @param message Sticker message content
* @returns Send result
*/
async sendStickerMessage(accessToken, groupId, message) {
try {
const response = await this.client.apiPost(this.endpoints.message.group, accessToken, {
recipient: {
group_id: groupId,
},
message: {
attachment: {
type: "template",
payload: {
template_type: "media",
elements: [
{
media_type: "sticker",
attachment_id: message.stickerId,
},
],
},
},
},
});
return response;
}
catch (error) {
throw this.handleGroupMessageError(error, "Failed to send sticker message to group");
}
}
/**
* Send mention message to group
* @param accessToken OA access token
* @param groupId Group ID
* @param message Mention message content
* @returns Send result
*/
async sendMentionMessage(accessToken, groupId, message) {
try {
const response = await this.client.apiPost(this.endpoints.message.group, accessToken, {
recipient: {
group_id: groupId,
},
message: {
text: message.text,
mention: message.mentions,
},
});
return response;
}
catch (error) {
throw this.handleGroupMessageError(error, "Failed to send mention message to group");
}
}
/**
* Get group information
* @param accessToken OA access token
* @param groupId Group ID
* @returns Group information
*/
async getGroupInfo(accessToken, groupId) {
try {
const response = await this.client.apiGet(this.endpoints.group.getInfo, accessToken, {
group_id: groupId,
});
return response;
}
catch (error) {
throw this.handleGroupMessageError(error, "Failed to get group information");
}
}
/**
* Get group members
* @param accessToken OA access token
* @param groupId Group ID
* @param offset Offset for pagination
* @param count Number of members to retrieve
* @returns Group members list
*/
async getGroupMembers(accessToken, groupId, offset = 0, count = 50) {
try {
const response = await this.client.apiGet(this.endpoints.group.getMembers, accessToken, {
group_id: groupId,
offset,
count,
});
return response;
}
catch (error) {
throw this.handleGroupMessageError(error, "Failed to get group members");
}
}
/**
* Gửi danh sách tin nhắn tới 1 group với delay tùy chỉnh
* Hỗ trợ tất cả các loại tin nhắn: text, image, file, sticker, mention
*
* @param request Thông tin request gửi danh sách tin nhắn
* @returns Kết quả gửi từng tin nhắn
*/
async sendMessageListToGroup(request) {
const startTime = Date.now();
const messageResults = [];
let successfulMessages = 0;
let failedMessages = 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.groupId || request.groupId.trim().length === 0) {
throw new common_1.ZaloSDKError("Group 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 messageStartTime = Date.now();
// Callback: Bắt đầu gửi tin nhắn
if (request.onProgress) {
request.onProgress({
groupId: request.groupId,
messageIndex: i,
totalMessages: request.messages.length,
messageType: message.type,
status: 'started',
startTime: messageStartTime,
});
}
const messageResult = {
messageIndex: i,
messageType: message.type,
success: false,
startTime: messageStartTime,
endTime: 0,
duration: 0,
};
try {
let result;
// 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");
}
result = await this.sendTextMessage(request.accessToken, request.groupId, { type: "text", text: message.text });
break;
case "image":
if (!message.imageUrl && !message.attachmentId) {
throw new Error("imageUrl hoặc attachmentId là bắt buộc cho image message");
}
result = await this.sendImageMessage(request.accessToken, request.groupId, {
type: "image",
imageUrl: message.imageUrl,
attachmentId: message.attachmentId,
caption: message.caption,
});
break;
case "file":
if (!message.fileToken) {
throw new Error("fileToken là bắt buộc cho file message");
}
result = await this.sendFileMessage(request.accessToken, request.groupId, {
type: "file",
fileToken: message.fileToken,
fileName: message.fileName,
});
break;
case "sticker":
if (!message.stickerId) {
throw new Error("stickerId là bắt buộc cho sticker message");
}
result = await this.sendStickerMessage(request.accessToken, request.groupId, {
type: "sticker",
stickerId: message.stickerId,
});
break;
case "mention":
if (!message.text || !message.mentions) {
throw new Error("text và mentions là bắt buộc cho mention message");
}
result = await this.sendMentionMessage(request.accessToken, request.groupId, {
type: "mention",
text: message.text,
mentions: message.mentions,
});
break;
default:
throw new Error(`Loại tin nhắn không được hỗ trợ: ${message.type}`);
}
// Ghi nhận thành công
const messageEndTime = Date.now();
messageResult.success = true;
messageResult.result = result;
messageResult.endTime = messageEndTime;
messageResult.duration = messageEndTime - messageStartTime;
successfulMessages++;
// Callback: Hoàn thành thành công
if (request.onProgress) {
request.onProgress({
groupId: request.groupId,
messageIndex: i,
totalMessages: request.messages.length,
messageType: message.type,
status: 'completed',
result: result,
startTime: messageStartTime,
endTime: messageEndTime,
});
}
}
catch (error) {
// Ghi nhận thất bại
const messageEndTime = Date.now();
const errorMessage = error instanceof Error ? error.message : String(error);
messageResult.success = false;
messageResult.error = errorMessage;
messageResult.endTime = messageEndTime;
messageResult.duration = messageEndTime - messageStartTime;
failedMessages++;
// Callback: Thất bại
if (request.onProgress) {
request.onProgress({
groupId: request.groupId,
messageIndex: i,
totalMessages: request.messages.length,
messageType: message.type,
status: 'failed',
error: errorMessage,
startTime: messageStartTime,
endTime: messageEndTime,
});
}
}
messageResults.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 {
groupId: request.groupId,
totalMessages: request.messages.length,
successfulMessages,
failedMessages,
messageResults,
totalDuration,
};
}
catch (error) {
const totalDuration = Date.now() - startTime;
if (error instanceof common_1.ZaloSDKError) {
throw error;
}
throw new common_1.ZaloSDKError(`Failed to send message list to group: ${error.message}`, -1, {
groupId: request.groupId,
totalMessages: request.messages?.length || 0,
successfulMessages,
failedMessages,
messageResults,
totalDuration,
});
}
}
handleGroupMessageError(error, defaultMessage) {
let errorDetails = '';
// Type guard for axios error with response
if (error &&
typeof error === 'object' &&
'response' in error &&
error.response &&
typeof error.response === 'object' &&
'data' in error.response) {
const errorData = error.response.data;
const errorCode = errorData.error || error.response.status;
const errorMessage = errorData.message || errorData.error_description || "Unknown error";
errorDetails = `Error ${errorCode}: ${errorMessage}`;
// Add additional debug info if available
if (errorData.error_name) {
errorDetails += ` (${errorData.error_name})`;
}
// Log detailed error for debugging
console.error(`[GroupMessageService] ${defaultMessage}`, {
errorCode,
errorMessage,
errorData,
url: error.config?.url,
method: error.config?.method,
requestData: error.config?.data
});
return new Error(`${defaultMessage}: ${errorDetails}`);
}
// Handle network or other errors
const errorObj = error;
console.error(`[GroupMessageService] ${defaultMessage}`, {
message: errorObj.message,
stack: errorObj.stack,
url: error.config?.url,
method: error.config?.method,
});
return new Error(`${defaultMessage}: ${errorObj.message || "Unknown error"}`);
}
/**
* Gửi danh sách tin nhắn tới nhiều groups với callback tracking
*
* @param request Thông tin request gửi danh sách tin nhắn tới nhiều groups
* @returns Kết quả gửi tin nhắn cho tất cả groups
*/
async sendMessageListToMultipleGroups(request) {
const startTime = Date.now();
const groupResults = [];
let successfulGroups = 0;
let failedGroups = 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.groupIds || request.groupIds.length === 0) {
throw new common_1.ZaloSDKError("Danh sách group 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ỏ group IDs trùng lặp và rỗng
const uniqueGroupIds = [...new Set(request.groupIds.filter(id => id && id.trim().length > 0))];
if (uniqueGroupIds.length === 0) {
throw new common_1.ZaloSDKError("Không có group ID hợp lệ nào", -1);
}
// Gửi tin nhắn cho từng group tuần tự
for (let i = 0; i < uniqueGroupIds.length; i++) {
const groupId = uniqueGroupIds[i];
const groupStartTime = Date.now();
// Callback: Bắt đầu gửi cho group
if (request.onProgress) {
request.onProgress({
groupId,
groupIndex: i,
totalGroups: uniqueGroupIds.length,
status: 'started',
startTime: groupStartTime,
});
}
const groupResult = {
groupId,
groupIndex: i,
success: false,
startTime: groupStartTime,
endTime: 0,
duration: 0,
};
try {
// Gửi danh sách tin nhắn cho group này
const messageListResult = await this.sendMessageListToGroup({
accessToken: request.accessToken,
groupId,
messages: request.messages,
defaultDelay: request.defaultDelay,
// Không truyền onProgress để tránh callback lồng nhau
});
// Ghi nhận thành công
const groupEndTime = Date.now();
groupResult.success = true;
groupResult.messageListResult = messageListResult;
groupResult.endTime = groupEndTime;
groupResult.duration = groupEndTime - groupStartTime;
successfulGroups++;
totalSuccessfulMessages += messageListResult.successfulMessages;
totalFailedMessages += messageListResult.failedMessages;
// Callback: Hoàn thành thành công
if (request.onProgress) {
request.onProgress({
groupId,
groupIndex: i,
totalGroups: uniqueGroupIds.length,
status: 'completed',
result: messageListResult,
startTime: groupStartTime,
endTime: groupEndTime,
});
}
}
catch (error) {
// Ghi nhận thất bại
const groupEndTime = Date.now();
const errorMessage = error instanceof Error ? error.message : String(error);
groupResult.success = false;
groupResult.error = errorMessage;
groupResult.endTime = groupEndTime;
groupResult.duration = groupEndTime - groupStartTime;
failedGroups++;
// Với group 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({
groupId,
groupIndex: i,
totalGroups: uniqueGroupIds.length,
status: 'failed',
error: errorMessage,
startTime: groupStartTime,
endTime: groupEndTime,
});
}
}
groupResults.push(groupResult);
// Delay giữa các groups (trừ group cuối cùng)
if (i < uniqueGroupIds.length - 1 && request.delayBetweenGroups && request.delayBetweenGroups > 0) {
await this.sleep(request.delayBetweenGroups);
}
}
const totalDuration = Date.now() - startTime;
const totalMessages = uniqueGroupIds.length * request.messages.length;
return {
totalGroups: uniqueGroupIds.length,
successfulGroups,
failedGroups,
groupResults,
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 list to multiple groups: ${error.message}`, -1, {
totalGroups: request.groupIds?.length || 0,
successfulGroups,
failedGroups,
groupResults,
totalDuration,
messageStats: {
totalSuccessfulMessages,
totalFailedMessages,
totalMessages: (request.groupIds?.length || 0) * (request.messages?.length || 0),
},
});
}
}
/**
* Gửi tin nhắn cá nhân hóa tới nhiều groups - mỗi group có bộ tin nhắn riêng
*
* @param request Thông tin request gửi tin nhắn cá nhân hóa tới nhiều groups
* @returns Kết quả gửi tin nhắn cho tất cả groups
*/
async sendPersonalizedMessageToMultipleGroups(request) {
const startTime = Date.now();
const groupResults = [];
let successfulGroups = 0;
let failedGroups = 0;
let totalSuccessfulMessages = 0;
let totalFailedMessages = 0;
let totalMessages = 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.personalizedMessages || request.personalizedMessages.length === 0) {
throw new common_1.ZaloSDKError("Danh sách tin nhắn cá nhân hóa không được để trống", -1);
}
// Validate từng group message
for (let i = 0; i < request.personalizedMessages.length; i++) {
const personalizedMessage = request.personalizedMessages[i];
if (!personalizedMessage.groupId || personalizedMessage.groupId.trim().length === 0) {
throw new common_1.ZaloSDKError(`Group ID tại index ${i} không được để trống`, -1);
}
if (!personalizedMessage.messages || personalizedMessage.messages.length === 0) {
throw new common_1.ZaloSDKError(`Danh sách tin nhắn cho group ${personalizedMessage.groupId} không được để trống`, -1);
}
}
// Loại bỏ groups trùng lặp dựa trên groupId
const uniquePersonalizedMessages = request.personalizedMessages.filter((message, index, self) => self.findIndex(m => m.groupId === message.groupId) === index &&
message.groupId.trim().length > 0);
if (uniquePersonalizedMessages.length === 0) {
throw new common_1.ZaloSDKError("Không có group message hợp lệ nào", -1);
}
// Tính tổng số tin nhắn
totalMessages = uniquePersonalizedMessages.reduce((sum, pm) => sum + pm.messages.length, 0);
// Gửi tin nhắn cho từng group tuần tự
for (let i = 0; i < uniquePersonalizedMessages.length; i++) {
const personalizedMessage = uniquePersonalizedMessages[i];
const groupStartTime = Date.now();
// Callback: Bắt đầu gửi cho group
if (request.onProgress) {
request.onProgress({
groupId: personalizedMessage.groupId,
groupIndex: i,
totalGroups: uniquePersonalizedMessages.length,
status: 'started',
startTime: groupStartTime,
});
}
const groupResult = {
groupId: personalizedMessage.groupId,
groupIndex: i,
success: false,
startTime: groupStartTime,
endTime: 0,
duration: 0,
};
try {
// Gửi danh sách tin nhắn cá nhân hóa cho group này
const messageListResult = await this.sendMessageListToGroup({
accessToken: request.accessToken,
groupId: personalizedMessage.groupId,
messages: personalizedMessage.messages,
defaultDelay: request.defaultDelay,
// Không truyền onProgress để tránh callback lồng nhau
});
// Ghi nhận thành công
const groupEndTime = Date.now();
groupResult.success = true;
groupResult.messageListResult = messageListResult;
groupResult.endTime = groupEndTime;
groupResult.duration = groupEndTime - groupStartTime;
successfulGroups++;
totalSuccessfulMessages += messageListResult.successfulMessages;
totalFailedMessages += messageListResult.failedMessages;
// Callback: Hoàn thành thành công
if (request.onProgress) {
request.onProgress({
groupId: personalizedMessage.groupId,
groupIndex: i,
totalGroups: uniquePersonalizedMessages.length,
status: 'completed',
result: messageListResult,
startTime: groupStartTime,
endTime: groupEndTime,
});
}
}
catch (error) {
// Ghi nhận thất bại
const groupEndTime = Date.now();
const errorMessage = error instanceof Error ? error.message : String(error);
groupResult.success = false;
groupResult.error = errorMessage;
groupResult.endTime = groupEndTime;
groupResult.duration = groupEndTime - groupStartTime;
failedGroups++;
// Với group thất bại, coi như tất cả tin nhắn của group đó đều thất bại
totalFailedMessages += personalizedMessage.messages.length;
// Callback: Thất bại
if (request.onProgress) {
request.onProgress({
groupId: personalizedMessage.groupId,
groupIndex: i,
totalGroups: uniquePersonalizedMessages.length,
status: 'failed',
error: errorMessage,
startTime: groupStartTime,
endTime: groupEndTime,
});
}
}
groupResults.push(groupResult);
// Delay giữa các groups (trừ group cuối cùng)
if (i < uniquePersonalizedMessages.length - 1 && request.delayBetweenGroups && request.delayBetweenGroups > 0) {
await this.sleep(request.delayBetweenGroups);
}
}
const totalDuration = Date.now() - startTime;
return {
totalGroups: uniquePersonalizedMessages.length,
successfulGroups,
failedGroups,
groupResults,
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 personalized messages to multiple groups: ${error.message}`, -1, {
totalGroups: request.personalizedMessages?.length || 0,
successfulGroups,
failedGroups,
groupResults,
totalDuration,
messageStats: {
totalSuccessfulMessages,
totalFailedMessages,
totalMessages,
},
});
}
}
/**
* Utility method để sleep/delay
* @param ms Thời gian delay tính bằng milliseconds
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
exports.GroupMessageService = GroupMessageService;
//# sourceMappingURL=group-message.service.js.map