@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,129 lines (1,128 loc) • 51.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GroupManagementService = void 0;
const common_1 = require("../types/common");
/**
* Service for handling Zalo Official Account Group Management Framework (GMF) APIs
*
* CONDITIONS FOR USING ZALO GMF GROUP MANAGEMENT:
*
* 1. GENERAL CONDITIONS:
* - OA must be granted permission to use GMF (Group Message Framework) feature
* - Access token must have "manage_group" and "group_message" scopes
* - OA must have active status and be verified
* - Must comply with limits on number of groups and members
*
* 2. CREATE NEW GROUP:
* - Group name: required, max 100 characters, no special characters
* - Description: optional, max 500 characters
* - Avatar: optional, JPG/PNG format, max 5MB
* - Initial members: max 200 people, must be users who have interacted with OA
* - OA automatically becomes admin of the group
*
* 3. MEMBER MANAGEMENT:
* - Only admins can invite/remove members
* - Invite members: max 50 people per time, users must have interacted with OA
* - Remove members: cannot remove other admins, must have at least 1 admin
* - Members can leave group themselves
*
* 4. ADMIN MANAGEMENT:
* - Only current admins can add/remove other admins
* - Must have at least 1 admin in group
* - OA always has admin rights and cannot be removed
*
* 5. LIMITS AND CONSTRAINTS:
* - Maximum groups: according to service package (usually 10-100 groups)
* - Maximum members per group: 200 people
* - Group creation frequency: max 10 groups/day
* - Member invitation frequency: max 500 invitations/day
*/
class GroupManagementService {
constructor(client, userService) {
this.client = client;
this.userService = userService;
// Zalo API endpoints - organized by functionality
this.endpoints = {
group: {
create: "https://openapi.zalo.me/v3.0/oa/group/creategroupwithoa",
get: "https://openapi.zalo.me/v3.0/oa/group/getgroup",
updateInfo: "https://openapi.zalo.me/v3.0/oa/group/updateinfo",
updateAsset: "https://openapi.zalo.me/v3.0/oa/group/updateasset",
invite: "https://openapi.zalo.me/v3.0/oa/group/invite",
listPendingInvite: "https://openapi.zalo.me/v3.0/oa/group/listpendinginvite",
acceptPendingInvite: "https://openapi.zalo.me/v3.0/oa/group/acceptpendinginvite",
rejectPendingInvite: "https://openapi.zalo.me/v3.0/oa/group/rejectpendinginvite",
removeMembers: "https://openapi.zalo.me/v3.0/oa/group/removemembers",
getGroupsOfOA: "https://openapi.zalo.me/v3.0/oa/group/getgroupsofoa",
recent: "https://openapi.zalo.me/v3.0/oa/group/listrecentchat",
conversation: "https://openapi.zalo.me/v3.0/oa/group/conversation",
addAdmins: "https://openapi.zalo.me/v3.0/oa/group/addadmins",
removeAdmins: "https://openapi.zalo.me/v3.0/oa/group/removeadmins",
conversationByGroupId: (groupId) => `https://openapi.zalo.me/v3.0/oa/group/${groupId}/conversation`,
members: "https://openapi.zalo.me/v3.0/oa/group/listmember",
delete: "https://openapi.zalo.me/v3.0/oa/group/delete",
quota: "https://openapi.zalo.me/v3.0/oa/quota/group",
},
};
}
/**
* Create GroupManagementService with UserService for enhanced member details
* @param client ZaloClient instance
* @param userService UserService instance for fetching detailed user info
* @returns GroupManagementService with enhanced capabilities
*/
static withUserService(client, userService) {
return new GroupManagementService(client, userService);
}
/**
* Create basic GroupManagementService without UserService
* @param client ZaloClient instance
* @returns GroupManagementService with basic capabilities only
*/
static basic(client) {
return new GroupManagementService(client);
}
/**
* Create new group chat with asset_id
* @param accessToken OA access token
* @param groupData Group information to create
* @returns Created group information
*
* API: POST https://openapi.zalo.me/v3.0/oa/group/creategroupwithoa
*/
async createGroup(accessToken, groupData) {
try {
// Validate input
if (!groupData.group_name || groupData.group_name.trim().length === 0) {
throw new common_1.ZaloSDKError("Group name cannot be empty", -1);
}
if (groupData.group_name.length > 100) {
throw new common_1.ZaloSDKError("Group name cannot exceed 100 characters", -1);
}
if (!groupData.asset_id || groupData.asset_id.trim().length === 0) {
throw new common_1.ZaloSDKError("Asset ID cannot be empty", -1);
}
if (groupData.member_user_ids.length > 99) {
throw new common_1.ZaloSDKError("Initial member count cannot exceed 99 people", -1);
}
if (groupData.member_user_ids.length === 0) {
throw new common_1.ZaloSDKError("Member list cannot be empty", -1);
}
const requestData = {
group_name: groupData.group_name.trim(),
...(groupData.group_description && {
group_description: groupData.group_description.trim(),
}),
asset_id: groupData.asset_id,
member_user_ids: groupData.member_user_ids,
};
const response = await this.client.apiPost(this.endpoints.group.create, accessToken, requestData);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to create group");
}
}
/**
* Helper method to extract group data from create response
* @param response Full API response
* @returns Group data only
*/
extractGroupData(response) {
return response.data;
}
/**
* Helper method to extract group info from update response
* @param response Full API response
* @returns Group info only
*/
extractGroupInfo(response) {
return response.data.group_info;
}
/**
* Helper method to extract group settings from update response
* @param response Full API response
* @returns Group settings only
*/
extractGroupSettings(response) {
return response.data.group_setting;
}
/**
* Helper method to extract asset info from update response
* @param response Full API response
* @returns Asset info only
*/
extractAssetInfo(response) {
return response.data.asset_info;
}
async updateGroupAsset(accessToken, groupIdOrUpdateData, assetId) {
try {
let requestData;
// Handle overloaded parameters
if (typeof groupIdOrUpdateData === 'string') {
// First overload: (accessToken, groupId, assetId)
if (!assetId) {
throw new common_1.ZaloSDKError("Asset ID is required when using separate parameters", -1);
}
requestData = {
group_id: groupIdOrUpdateData,
asset_id: assetId,
};
}
else {
// Second overload: (accessToken, updateData)
requestData = groupIdOrUpdateData;
}
// Validate input
if (!requestData.group_id || requestData.group_id.trim().length === 0) {
throw new common_1.ZaloSDKError("Group ID cannot be empty", -1);
}
if (!requestData.asset_id || requestData.asset_id.trim().length === 0) {
throw new common_1.ZaloSDKError("Asset ID cannot be empty", -1);
}
const response = await this.client.apiPost(this.endpoints.group.updateAsset, accessToken, requestData);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to update group asset");
}
}
/**
* Get detailed group information
* @param accessToken OA access token
* @param groupId Group ID
* @returns Detailed group information including group_info, asset_info and group_setting
*
* API: GET https://openapi.zalo.me/v3.0/oa/group/getgroup
*/
async getGroupInfo(accessToken, groupId) {
try {
// Validate access token
if (!accessToken || accessToken.trim().length === 0) {
throw new common_1.ZaloSDKError("Access token cannot be empty", -1);
}
// Validate group ID
if (!groupId || groupId.trim().length === 0) {
throw new common_1.ZaloSDKError("Group ID cannot be empty", -1);
}
const response = await this.client.apiGet(this.endpoints.group.get, accessToken, {
group_id: groupId.trim(),
});
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to get group information");
}
}
/**
* Update group information
* @param accessToken OA access token
* @param groupId Group ID
* @param updateData Information to update
* @returns Update result with full group information
*
* API: POST https://openapi.zalo.me/v3.0/oa/group/updateinfo
*/
async updateGroupInfo(accessToken, groupId, updateData) {
try {
// Validate input
if (updateData.group_name && updateData.group_name.length > 100) {
throw new common_1.ZaloSDKError("Group name cannot exceed 100 characters", -1);
}
if (updateData.group_description && updateData.group_description.length > 500) {
throw new common_1.ZaloSDKError("Group description cannot exceed 500 characters", -1);
}
// Build request data according to Zalo API
const requestData = {
group_id: groupId,
...(updateData.group_name && {
group_name: updateData.group_name.trim(),
}),
...(updateData.group_avatar && {
group_avatar: updateData.group_avatar,
}),
...(updateData.group_description && {
group_description: updateData.group_description.trim(),
}),
...(updateData.lock_send_msg !== undefined && {
lock_send_msg: updateData.lock_send_msg,
}),
...(updateData.join_appr !== undefined && {
join_appr: updateData.join_appr,
}),
...(updateData.enable_msg_history !== undefined && {
enable_msg_history: updateData.enable_msg_history,
}),
...(updateData.enable_link_join !== undefined && {
enable_link_join: updateData.enable_link_join,
}),
};
const response = await this.client.apiPost(this.endpoints.group.updateInfo, accessToken, requestData);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to update group information");
}
}
/**
* Update group avatar
* @param accessToken OA access token
* @param groupId Group ID
* @param avatarData New avatar information
* @returns Update result
* @deprecated Use updateGroupInfo() with group_avatar field instead
*/
async updateGroupAvatar(accessToken, groupId, avatarData) {
try {
const response = await this.client.apiPost(this.endpoints.group.updateInfo, accessToken, {
group_id: groupId,
...avatarData,
});
return { success: response.error === 0 };
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to update group avatar");
}
}
async inviteMembers(accessToken, groupId, inviteDataOrUserIds) {
try {
let memberUserIds;
// Handle overloaded parameters
if (Array.isArray(inviteDataOrUserIds)) {
// Second overload: (accessToken, groupId, memberUserIds)
memberUserIds = inviteDataOrUserIds;
}
else {
// First overload: (accessToken, groupId, inviteData)
memberUserIds = inviteDataOrUserIds.member_user_ids;
}
// Validate input
if (memberUserIds.length === 0) {
throw new common_1.ZaloSDKError("Member list cannot be empty", -1);
}
if (memberUserIds.length > 50) {
throw new common_1.ZaloSDKError("Cannot invite more than 50 people at once", -1);
}
const requestData = {
group_id: groupId,
member_user_ids: memberUserIds,
};
const response = await this.client.apiPost(this.endpoints.group.invite, accessToken, requestData);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to invite members");
}
}
/**
* Get list of pending members
* @param accessToken OA access token
* @param groupId Group ID
* @param offset Offset for pagination (default: 0)
* @param count Maximum number to return (default: 20, max: 50)
* @returns List of pending members
*/
async getPendingMembers(accessToken, groupId, offset = 0, count = 5) {
try {
// Validate input
if (!groupId || groupId.trim().length === 0) {
throw new common_1.ZaloSDKError("Group ID cannot be empty", -1);
}
if (offset < 0) {
throw new common_1.ZaloSDKError("Offset must be >= 0", -1);
}
if (count <= 0 || count > 50) {
throw new common_1.ZaloSDKError("Count must be from 1 to 50", -1);
}
const response = await this.client.apiGet(this.endpoints.group.listPendingInvite, accessToken, {
group_id: groupId.trim(),
offset: offset,
count: count,
});
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to get pending members");
}
}
/**
* Accept pending members to group
* @param accessToken OA access token
* @param groupId Group ID
* @param memberUserIds List of user IDs to accept
* @returns Accept result
*/
async acceptPendingMembers(accessToken, groupId, memberUserIds) {
try {
// Validate input
if (!groupId || groupId.trim().length === 0) {
throw new common_1.ZaloSDKError("Group ID cannot be empty", -1);
}
if (!memberUserIds || memberUserIds.length === 0) {
throw new common_1.ZaloSDKError("Member user IDs list cannot be empty", -1);
}
if (memberUserIds.length > 100) {
throw new common_1.ZaloSDKError("Cannot accept more than 100 members at once", -1);
}
// Validate user IDs
const validUserIds = memberUserIds.filter((id) => id && id.trim().length > 0);
if (validUserIds.length === 0) {
throw new common_1.ZaloSDKError("No valid user IDs found", -1);
}
const requestBody = {
group_id: groupId.trim(),
member_user_ids: validUserIds.map((id) => id.trim()),
};
const response = await this.client.apiPost(this.endpoints.group.acceptPendingInvite, accessToken, requestBody);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to accept pending members");
}
}
/**
* Reject pending members from group
* @param accessToken OA access token
* @param groupId Group ID
* @param memberUserIds List of user IDs to reject
* @returns Reject result
*/
async rejectPendingMembers(accessToken, groupId, memberUserIds) {
try {
// Validate input
if (!groupId || groupId.trim().length === 0) {
throw new common_1.ZaloSDKError("Group ID cannot be empty", -1);
}
if (!memberUserIds || memberUserIds.length === 0) {
throw new common_1.ZaloSDKError("Member user IDs list cannot be empty", -1);
}
if (memberUserIds.length > 100) {
throw new common_1.ZaloSDKError("Cannot reject more than 100 members at once", -1);
}
// Validate user IDs
const validUserIds = memberUserIds.filter((id) => id && id.trim().length > 0);
if (validUserIds.length === 0) {
throw new common_1.ZaloSDKError("No valid user IDs found", -1);
}
const requestBody = {
group_id: groupId.trim(),
member_user_ids: validUserIds.map((id) => id.trim()),
};
const response = await this.client.apiPost(this.endpoints.group.rejectPendingInvite, accessToken, requestBody);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to reject pending members");
}
}
/**
* Remove members from group
* @param accessToken OA access token
* @param groupId Group ID
* @param memberUserIds List of user IDs to remove
* @returns Remove result
*/
async removeMembers(accessToken, groupId, memberUserIds) {
try {
// Validate input
if (!groupId || groupId.trim().length === 0) {
throw new common_1.ZaloSDKError("Group ID cannot be empty", -1);
}
if (!memberUserIds || memberUserIds.length === 0) {
throw new common_1.ZaloSDKError("Member user IDs list cannot be empty", -1);
}
if (memberUserIds.length > 100) {
throw new common_1.ZaloSDKError("Cannot remove more than 100 members at once", -1);
}
// Validate user IDs
const validUserIds = memberUserIds.filter((id) => id && id.trim().length > 0);
if (validUserIds.length === 0) {
throw new common_1.ZaloSDKError("No valid user IDs found", -1);
}
const requestBody = {
group_id: groupId.trim(),
member_user_ids: validUserIds.map((id) => id.trim()),
};
const response = await this.client.apiPost(this.endpoints.group.removeMembers, accessToken, requestBody);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to remove members");
}
}
async addAdmins(accessToken, groupId, adminDataOrUserIds) {
try {
let memberUserIds;
// Handle overloaded parameters
if (Array.isArray(adminDataOrUserIds)) {
// Second overload: (accessToken, groupId, memberUserIds)
memberUserIds = adminDataOrUserIds;
}
else {
// First overload: (accessToken, groupId, adminData)
memberUserIds = adminDataOrUserIds.member_user_ids;
}
// Validate input
if (!memberUserIds || memberUserIds.length === 0) {
throw new common_1.ZaloSDKError("Member user IDs list cannot be empty", -1);
}
const requestData = {
group_id: groupId,
member_user_ids: memberUserIds,
};
const response = await this.client.apiPost(this.endpoints.group.addAdmins, accessToken, requestData);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to add admins");
}
}
async removeAdmins(accessToken, groupId, adminDataOrUserIds) {
try {
let memberUserIds;
// Handle overloaded parameters
if (Array.isArray(adminDataOrUserIds)) {
// Second overload: (accessToken, groupId, memberUserIds)
memberUserIds = adminDataOrUserIds;
}
else {
// First overload: (accessToken, groupId, adminData)
memberUserIds = adminDataOrUserIds.member_user_ids;
}
// Validate input
if (!memberUserIds || memberUserIds.length === 0) {
throw new common_1.ZaloSDKError("Member user IDs list cannot be empty", -1);
}
const requestData = {
group_id: groupId,
member_user_ids: memberUserIds,
};
const response = await this.client.apiPost(this.endpoints.group.removeAdmins, accessToken, requestData);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to remove admins");
}
}
/**
* Delete group chat (Disband group)
* @param accessToken OA access token
* @param groupId Group ID to delete
* @returns Delete result
*/
async deleteGroup(accessToken, groupId) {
try {
const requestData = {
group_id: groupId,
};
const response = await this.client.apiPost(this.endpoints.group.delete, accessToken, requestData);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to delete group");
}
}
/**
* Get list of OA groups
* @param accessToken OA access token
* @param offset Offset for pagination (default: 0)
* @param count Maximum number to return (default: 5, max: 50)
* @returns List of OA groups
*
* API: GET https://openapi.zalo.me/v3.0/oa/group/getgroupsofoa
*/
async getGroupsOfOA(accessToken, offset = 0, count = 5) {
try {
// Validate access token
if (!accessToken || accessToken.trim().length === 0) {
throw new common_1.ZaloSDKError("Access token cannot be empty", -1);
}
// Validate parameters
if (offset < 0) {
throw new common_1.ZaloSDKError("Offset must be >= 0", -1);
}
if (count <= 0 || count > 50) {
throw new common_1.ZaloSDKError("Count must be from 1 to 50", -1);
}
const response = await this.client.apiGet(this.endpoints.group.getGroupsOfOA, accessToken, {
offset: offset,
count: count,
});
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to get groups of OA");
}
}
/**
* Get ALL groups of OA (custom function with automatic pagination)
* @param accessToken OA access token
* @param maxGroups Maximum number of groups to retrieve (default: unlimited)
* @param progressCallback Optional callback to track progress
* @returns All OA groups with pagination info
*
* This function automatically handles pagination to retrieve all groups,
* not limited by the 50-group API limit per request.
*/
async getAllGroupsOfOA(accessToken, maxGroups, progressCallback) {
try {
// Validate input
if (!accessToken || accessToken.trim().length === 0) {
throw new common_1.ZaloSDKError("Access token cannot be empty", -1);
}
if (maxGroups !== undefined && maxGroups <= 0) {
throw new common_1.ZaloSDKError("Max groups must be greater than 0", -1);
}
const allGroups = [];
let offset = 0;
const pageSize = 50; // Maximum allowed by Zalo API
let totalCount;
let pagesFetched = 0;
let hasMore = true;
// Progress tracking
const updateProgress = (currentCount, isComplete = false) => {
if (progressCallback) {
const percentage = totalCount ? Math.round((currentCount / totalCount) * 100) : undefined;
progressCallback({
currentCount,
totalCount,
percentage,
isComplete
});
}
};
// Initial progress
updateProgress(0);
while (hasMore) {
// Check if we've reached the maximum limit
if (maxGroups && allGroups.length >= maxGroups) {
break;
}
// Calculate how many groups to request in this batch
const remainingLimit = maxGroups ? maxGroups - allGroups.length : pageSize;
const currentPageSize = Math.min(pageSize, remainingLimit);
// Fetch current page
const response = await this.getGroupsOfOA(accessToken, offset, currentPageSize);
// Handle API errors
if (response.error !== 0) {
throw new common_1.ZaloSDKError(`Failed to fetch groups at offset ${offset}: ${response.message}`, response.error);
}
// Check if we have data
if (!response.data || !response.data.groups) {
break;
}
// Update total count from first response
if (totalCount === undefined && response.data.total !== undefined) {
totalCount = response.data.total;
// If maxGroups is set and less than total, use maxGroups as effective total
if (maxGroups && maxGroups < totalCount) {
totalCount = maxGroups;
}
}
// Add groups to our collection
allGroups.push(...response.data.groups);
pagesFetched++;
// Update progress
updateProgress(allGroups.length);
// Check if we should continue
const receivedCount = response.data.groups.length;
hasMore = receivedCount === currentPageSize &&
(!maxGroups || allGroups.length < maxGroups) &&
(response.data.total === undefined || allGroups.length < response.data.total);
// Move to next page
offset += receivedCount;
// Safety check to prevent infinite loops
if (pagesFetched > 100) { // Max 5,000 groups (50 * 100)
console.warn(`getAllGroupsOfOA: Reached maximum page limit (${pagesFetched}) for OA`);
break;
}
}
// Final progress update
updateProgress(allGroups.length, true);
// Apply maxGroups limit if specified
const finalGroups = maxGroups ? allGroups.slice(0, maxGroups) : allGroups;
// Return enhanced response
return {
error: 0,
message: "Success",
data: {
total_groups: totalCount || allGroups.length,
groups: finalGroups,
pages_fetched: pagesFetched,
is_complete: !maxGroups || allGroups.length <= maxGroups
}
};
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to get all groups of OA");
}
}
/**
* Get ALL groups of OA with simple interface (no progress tracking)
* @param accessToken OA access token
* @param maxGroups Maximum number of groups to retrieve (optional)
* @returns Array of all OA groups
*
* Simplified version of getAllGroupsOfOA that returns just the groups array
*/
async getAllGroupsOfOASimple(accessToken, maxGroups) {
const response = await this.getAllGroupsOfOA(accessToken, maxGroups);
return response.data.groups;
}
/**
* Get group quota information and asset_id
* @param accessToken OA access token
* @param productType Product type (optional)
* @param quotaType Quota type (optional)
* @returns Group quota information including asset_id
*
* API: POST https://openapi.zalo.me/v3.0/oa/quota/group
*/
async getGroupQuota(accessToken, productType, quotaType) {
try {
// Validate access token
if (!accessToken || accessToken.trim().length === 0) {
throw new common_1.ZaloSDKError("Access token cannot be empty", -1);
}
const quotaRequest = {
quota_owner: "OA",
...(productType && { product_type: productType }),
...(quotaType && { quota_type: quotaType }),
};
const response = await this.client.apiPost(this.endpoints.group.quota, accessToken, quotaRequest);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to get group quota");
}
}
/**
* Get asset_id for creating GMF group
* @param accessToken OA access token
* @returns Asset_id for group creation
*/
async getAssetId(accessToken) {
try {
const quotaRequest = {
quota_owner: "OA",
};
const response = await this.client.apiPost(this.endpoints.group.quota, accessToken, quotaRequest);
if (response.error !== 0) {
throw new common_1.ZaloSDKError(response.message || "Failed to get asset_id", response.error);
}
if (response.data && response.data.length > 0) {
const activeAsset = response.data.find((asset) => asset.status === "available");
if (activeAsset) {
return activeAsset.asset_id;
}
}
throw new common_1.ZaloSDKError("No valid asset_id found for group creation. Please check your GMF package.", -1);
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to get asset_id for group creation");
}
}
/**
* Get list of asset_ids available for creating GMF groups
* @param accessToken OA access token
* @returns List of asset_ids and quota information
*/
async getAssetIds(accessToken) {
try {
const quotaRequest = {
quota_owner: "OA",
};
const response = await this.client.apiPost(this.endpoints.group.quota, accessToken, quotaRequest);
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to get asset_ids list");
}
}
/**
* Get list of recent chats
* @param accessToken OA access token
* @param offset Offset for pagination (default: 0)
* @param count Maximum number to return (default: 5, max: 50)
* @returns List of recent chats
*
* API: GET https://openapi.zalo.me/v3.0/oa/group/listrecentchat
*/
async getRecentChats(accessToken, offset = 0, count = 5) {
try {
// Validate access token
if (!accessToken || accessToken.trim().length === 0) {
throw new common_1.ZaloSDKError("Access token cannot be empty", -1);
}
// Validate parameters
if (offset < 0) {
throw new common_1.ZaloSDKError("Offset must be >= 0", -1);
}
if (count <= 0 || count > 50) {
throw new common_1.ZaloSDKError("Count must be from 1 to 50", -1);
}
const response = await this.client.apiGet(this.endpoints.group.recent, accessToken, {
offset: offset,
count: count,
});
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to get recent chats");
}
}
/**
* Lấy thông tin tin nhắn trong một nhóm
*
* Lưu ý: Ứng dụng cần được cấp quyền quản lý thông tin nhóm.
*
* @param accessToken Access token của Official Account
* @param groupId ID nhóm muốn query
* @param offset Offset muốn query (mặc định: 0)
* @param count Số lượng mong muốn query (mặc định: 5)
* @returns Lịch sử tin nhắn trong nhóm
*
* @example
* ```typescript
* const messages = await groupService.getGroupConversation(
* accessToken,
* 'f414c8f76fa586fbdfb4',
* 0,
* 2
* );
* ```
*
* API: GET https://openapi.zalo.me/v3.0/oa/group/conversation
*/
async getGroupConversation(accessToken, groupId, offset = 0, count = 5) {
try {
// Validate access token
if (!accessToken || accessToken.trim().length === 0) {
throw new common_1.ZaloSDKError("Access token cannot be empty", -1);
}
// Validate group ID
if (!groupId || groupId.trim().length === 0) {
throw new common_1.ZaloSDKError("Group ID cannot be empty", -1);
}
// Validate parameters
if (offset < 0) {
throw new common_1.ZaloSDKError("Offset must be >= 0", -1);
}
if (count <= 0) {
throw new common_1.ZaloSDKError("Count must be greater than 0", -1);
}
const response = await this.client.apiGet(this.endpoints.group.conversation, accessToken, {
group_id: groupId.trim(),
offset: offset,
count: count,
});
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to get group conversation");
}
}
/**
* Get group members list from Zalo API
* @param accessToken OA access token
* @param groupId Group ID
* @param offset Offset for pagination (default: 0)
* @param count Maximum number to return (default: 5, max: 50)
* @returns Group members list
*/
async getGroupMembers(accessToken, groupId, offset = 0, count = 5) {
try {
// Validate input
if (!groupId || groupId.trim().length === 0) {
throw new common_1.ZaloSDKError("Group ID cannot be empty", -1);
}
if (offset < 0) {
throw new common_1.ZaloSDKError("Offset must be >= 0", -1);
}
if (count <= 0 || count > 50) {
throw new common_1.ZaloSDKError("Count must be from 1 to 50", -1);
}
const response = await this.client.apiGet(this.endpoints.group.members, accessToken, {
group_id: groupId.trim(),
offset: offset,
count: count,
});
return response;
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to get group members");
}
}
/**
* Get ALL members of a group (custom function with automatic pagination)
* @param accessToken OA access token
* @param groupId Group ID
* @param maxMembers Maximum number of members to retrieve (default: unlimited)
* @param progressCallback Optional callback to track progress
* @returns All group members with pagination info
*
* This function automatically handles pagination to retrieve all members,
* not limited by the 50-member API limit per request.
*/
async getAllGroupMembers(accessToken, groupId, maxMembers, progressCallback) {
try {
// Validate input
if (!accessToken || accessToken.trim().length === 0) {
throw new common_1.ZaloSDKError("Access token cannot be empty", -1);
}
if (!groupId || groupId.trim().length === 0) {
throw new common_1.ZaloSDKError("Group ID cannot be empty", -1);
}
if (maxMembers !== undefined && maxMembers <= 0) {
throw new common_1.ZaloSDKError("Max members must be greater than 0", -1);
}
const allMembers = [];
let offset = 0;
const pageSize = 50; // Maximum allowed by Zalo API
let totalCount;
let pagesFetched = 0;
let hasMore = true;
// Progress tracking
const updateProgress = (currentCount, isComplete = false) => {
if (progressCallback) {
const percentage = totalCount ? Math.round((currentCount / totalCount) * 100) : undefined;
progressCallback({
currentCount,
totalCount,
percentage,
isComplete
});
}
};
// Initial progress
updateProgress(0);
while (hasMore) {
// Check if we've reached the maximum limit
if (maxMembers && allMembers.length >= maxMembers) {
break;
}
// Calculate how many members to request in this batch
const remainingLimit = maxMembers ? maxMembers - allMembers.length : pageSize;
const currentPageSize = Math.min(pageSize, remainingLimit);
// Fetch current page
const response = await this.getGroupMembers(accessToken, groupId, offset, currentPageSize);
// Handle API errors
if (response.error !== 0) {
throw new common_1.ZaloSDKError(`Failed to fetch members at offset ${offset}: ${response.message}`, response.error);
}
// Check if we have data
if (!response.data || !response.data.members) {
break;
}
// Update total count from first response
if (totalCount === undefined && response.data.total !== undefined) {
totalCount = response.data.total;
// If maxMembers is set and less than total, use maxMembers as effective total
if (maxMembers && maxMembers < totalCount) {
totalCount = maxMembers;
}
}
// Add members to our collection
allMembers.push(...response.data.members);
pagesFetched++;
// Update progress
updateProgress(allMembers.length);
// Check if we should continue
const receivedCount = response.data.members.length;
hasMore = receivedCount === currentPageSize &&
(!maxMembers || allMembers.length < maxMembers) &&
(response.data.total === undefined || allMembers.length < response.data.total);
// Move to next page
offset += receivedCount;
// Safety check to prevent infinite loops
if (pagesFetched > 1000) { // Max 50,000 members (50 * 1000)
console.warn(`getAllGroupMembers: Reached maximum page limit (${pagesFetched}) for group ${groupId}`);
break;
}
}
// Final progress update
updateProgress(allMembers.length, true);
// Apply maxMembers limit if specified
const finalMembers = maxMembers ? allMembers.slice(0, maxMembers) : allMembers;
// Return enhanced response
return {
error: 0,
message: "Success",
data: {
total_members: totalCount || allMembers.length,
members: finalMembers,
pages_fetched: pagesFetched,
is_complete: !maxMembers || allMembers.length <= maxMembers
}
};
}
catch (error) {
throw this.handleGroupManagementError(error, "Failed to get all group members");
}
}
/**
* Get ALL members of a group with simple interface (no progress tracking)
* @param accessToken OA access token
* @param groupId Group ID
* @param maxMembers Maximum number of members to retrieve (optional)
* @returns Array of all group members
*
* Simplified version of getAllGroupMembers that returns just the members array
*/
async getAllGroupMembersSimple(accessToken, groupId, maxMembers) {
const response = await this.getAllGroupMembers(accessToken, groupId, maxMembers);
return response.data.members;
}
/**
* Get ALL members of a group with DETAILED user information (advanced API)
* @param accessToken OA access token
* @param groupId Group ID
* @param maxMembers Maximum number of members to retrieve (default: unlimited)
* @param progressCallback Optional callback to track progress
* @param options Advanced options for fetching details
* @returns All group members with detailed user information
*
* This function:
* 1. First gets all basic member info using getAllGroupMembers()
* 2. Then fetches detailed user info for each member using UserService.getUserInfo()
* 3. Combines both basic and detailed info into EnhancedGroupMember objects
* 4. If detailed info fails to fetch, falls back to basic info only
* 5. Provides detailed progress tracking and statistics
*/
async getAllGroupMembersWithDetails(accessToken, groupId, maxMembers, progressCallback, options) {
try {
// Validate input
if (!accessToken || accessToken.trim().length === 0) {
throw new common_1.ZaloSDKError("Access token cannot be empty", -1);
}
if (!groupId || groupId.trim().length === 0) {
throw new common_1.ZaloSDKError("Group ID cannot be empty", -1);
}
// Set default options
const opts = {
detailBatchSize: 10,
detailTimeout: 5000,
continueWithoutUserService: true,
maxConcurrentRequests: 5,
...options
};
// Check if UserService is available
if (!this.userService && !opts.continueWithoutUserService) {
throw new common_1.ZaloSDKError("UserService is required for fetching detailed member information. " +
"Please provide UserService in constructor or set continueWithoutUserService to true.", -1);
}
// Progress tracking helper
const updateProgress = (currentCount, totalCount, phase, phaseDetails, isComplete = false) => {
if (progressCallback) {
const percentage = totalCount ? Math.round((currentCount / totalCount) * 100) : undefined;
progressCallback({
currentCount,
totalCount,
percentage,
isComplete,
phase,
phase_details: phaseDetails
});
}
};
// Phase 1: Get all basic member information
updateProgress(0, undefined, 'fetching_members');
const basicMembersResponse = await this.getAllGroupMembers(accessToken, groupId, maxMembers, (basicProgress) => {
updateProgress(basicProgress.currentCount, basicProgress.totalCount, 'fetching_members');
});
const basicMembers = basicMembersResponse.data.members;
const totalMembers = basicMembers.length;
// Initialize enhanced members with basic info
const enhancedMembers = basicMembers.map((member) => ({
basic_info: member,
detailed_info: null,
has_detailed_info: false
}));
// Statistics tracking
let successfulDetails = 0;
let failedDetails = 0;
// Phase 2: Fetch detailed user information (if UserService is available)
if (this.userService && totalMembers > 0) {
updateProgress(0, totalMembers, 'fetching_details', {
details_fetched: 0,
details_failed: 0,
current_batch: 0,
total_batches: Math.ceil(totalMembers / opts.detailBatchSize)
});
// Process members in batches to avoid overwhelming the API
for (let i = 0; i < totalMembers; i += opts.detailBatchSize) {
const batch = enhancedMembers.slice(i, i + opts.detailBatchSize);
const currentBatch = Math.floor(i / opts.detailBatchSize) + 1;
const totalBatches = Math.ceil(totalMembers / opts.detailBatchSize);
// Create promises for current batch with concurrency limit
const batchPromises = batch.map(async (enhancedMember) => {
const userId = enhancedMember.basic_info.user_id || enhancedMember.basic_info.oa_id;
if (!userId) {
enhancedMember.detail_fetch_error = "No user_id or oa_id available";
failedDetails++;
return;
}
try {
// Create timeout promise
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), opts.detailTimeout);
});
// Fetch user details with timeout
const userDetailPromise = this.userService.getUserInfo(accessToken, userId);
const userDetail = await Promise.race([userDetailPromise, timeoutPromise]);
enhancedMember.detailed_info = userDetail;
enhancedMember.has_detailed_info = true;
successfulDetails++;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
enhancedMember.detail_fetch_error = `Failed to fetch user details: ${errorMessage}`;
failedDetails++;
}
// Update progress for each member processed
const processedCount = successfulDetails + failedDetails;
updateProgress(processedCount, totalMembers, 'fetching_details', {
details_fetched: successfulDetails,
details_failed: failedDetails,
current_batch: currentBatch,
total_batches: totalBatches
});
});
// Process batch with concurrency limit
const chunks = [];
for (let j = 0; j < batchPromises.length; j += opts.maxConcurrentRequests) {
chunks.push(batchPromises.slice(j, j + opts.maxConcurrentRequests));
}
for (const chunk of chunks) {
await Promise.all(chunk);
}
}
}
else if (!this.userService) {
// UserService not available, mark all as failed but continue
failedDetails = totalMembers;
enhancedMembers.forEach(member => {
member.detail_fetch_error = "UserService not available";
});
}
// Final progress update
updateProgress(totalMembers, totalMembers, 'complete', {
details_fetched: successfulDetails,
details_failed: failedDetails
}, true);
// Calculate success rate
const successRate = totalMembers > 0 ? Math.round((successfulDetails / totalMembers) * 100) : 0;
// Apply maxMembers limit if specified
const finalMembers = maxMembers ? enhancedMembers.slice(0, maxMembers) : enhancedMembers;
// Return enhanced response
return {
error: 0,
message: "Success",
data: {
total_members: basicMembersResponse.data.total_members,
members: finalMembers,
pages_fetched: basicMembersResponse.data.pages_fetched,
is_complete: basicMembersResponse.data.is_complete,
detail_fetch_stats: {
total_processed: totalMembers,
successful_details: successfulDetails,