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

780 lines 34.4 kB
"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