UNPKG

@warriorteam/redai-zalo-sdk

Version:

Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account, ZNS, Consultation Service, Group Messaging, and Social APIs

591 lines 24.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ArticleService = void 0; const article_1 = require("../types/article"); const common_1 = require("../types/common"); /** * Service for handling Zalo Official Account Article Management APIs * * CONDITIONS FOR USING ZALO ARTICLE MANAGEMENT: * * 1. GENERAL CONDITIONS: * - OA must have permission to create articles * - Access token must have "manage_article" scope * - OA must have active status and be verified * * 2. ARTICLE CREATION: * - Title: required, max 150 characters * - Author: required for normal articles, max 50 characters * - Description: required, max 300 characters * - Image size: max 1MB per image * - Status: 'show' (publish immediately) or 'hide' (draft) * - Comment: 'show' (allow comments) or 'hide' (disable comments) * * 3. CONTENT VALIDATION: * - Normal articles: must have cover, body content, and author * - Video articles: must have video_id and avatar * - Body items: support text, image, video, and product types * - Tracking links: must be valid URLs * * 4. LIMITS AND CONSTRAINTS: * - Article list: max 100 items per request * - Processing time: use token to check progress * - Update frequency: reasonable intervals recommended */ class ArticleService { constructor(client) { this.client = client; // Zalo API endpoints - organized by functionality this.endpoints = { article: { create: "https://openapi.zalo.me/v2.0/article/create", update: "https://openapi.zalo.me/v2.0/article/update", remove: "https://openapi.zalo.me/v2.0/article/remove", verify: "https://openapi.zalo.me/v2.0/article/verify", detail: "https://openapi.zalo.me/v2.0/article/getdetail", list: "https://openapi.zalo.me/v2.0/article/getslice", }, }; } /** * Create normal article with rich content * @param accessToken OA access token * @param request Normal article information * @returns Token for tracking creation progress */ async createNormalArticle(accessToken, request) { try { // Validate input this.validateNormalArticleRequest(request); // Process tracking link - use default if not provided or invalid let processedTrackingLink = "https://v2.redai.vn/auth"; if (request.tracking_link && this.isValidTrackingLink(request.tracking_link)) { processedTrackingLink = request.tracking_link; } const data = { type: request.type, title: request.title, author: request.author, cover: request.cover, description: request.description, body: request.body, related_medias: request.related_medias || [], tracking_link: processedTrackingLink, status: request.status || article_1.ArticleStatus.HIDE, comment: request.comment || article_1.CommentStatus.SHOW, }; const response = await this.client.apiPost(this.endpoints.article.create, accessToken, data); return response; } catch (error) { throw this.handleArticleError(error, "Failed to create normal article"); } } /** * Create video article * @param accessToken OA access token * @param request Video article information * @returns Token for tracking creation progress */ async createVideoArticle(accessToken, request) { try { // Validate input this.validateVideoArticleRequest(request); const data = { type: request.type, title: request.title, description: request.description, video_id: request.video_id, avatar: request.avatar, status: request.status || article_1.ArticleStatus.HIDE, comment: request.comment || article_1.CommentStatus.SHOW, }; const response = await this.client.apiPost(this.endpoints.article.create, accessToken, data); return response; } catch (error) { throw this.handleArticleError(error, "Failed to create video article"); } } /** * Create article (automatically detect type) * @param accessToken OA access token * @param request Article information * @returns Token for tracking creation progress */ async createArticle(accessToken, request) { if (request.type === article_1.ArticleType.NORMAL) { return this.createNormalArticle(accessToken, request); } else if (request.type === article_1.ArticleType.VIDEO) { return this.createVideoArticle(accessToken, request); } else { throw new common_1.ZaloSDKError('Invalid article type. Only "normal" and "video" are supported', -1); } } /** * Check article creation progress * @param accessToken OA access token * @param token Token from article creation response * @returns Progress information */ async checkArticleProcess(accessToken, token) { try { if (!token || token.trim() === "") { throw new common_1.ZaloSDKError("Token cannot be empty", -1); } // Use verifyArticle method to check progress const response = await this.verifyArticle(accessToken, token); // Convert response to standard format const result = { article_id: response?.id, status: response?.id ? "success" : "processing", created_time: Date.now(), updated_time: Date.now(), }; return result; } catch (error) { // Handle token expiration - means article processing is completed if (error instanceof common_1.ZaloSDKError && error.message.includes("token was expired")) { return { status: "success", error_message: "Token expired - article processing completed", created_time: Date.now(), updated_time: Date.now(), }; } // Handle API errors - article may still be processing if (error instanceof common_1.ZaloSDKError && error.message.includes("API")) { return { status: "processing", created_time: Date.now(), updated_time: Date.now(), }; } // Handle response structure errors if (error instanceof TypeError) { return { status: "processing", error_message: "Article is being processed", created_time: Date.now(), updated_time: Date.now(), }; } throw this.handleArticleError(error, "Failed to check article process"); } } /** * Verify article and get article ID * @param accessToken OA access token * @param token Token from article creation or update response * @returns Article ID */ async verifyArticle(accessToken, token) { try { if (!token || token.trim() === "") { throw new common_1.ZaloSDKError("Token cannot be empty", -1); } const data = { token: token, }; const response = await this.client.apiPost(this.endpoints.article.verify, accessToken, data); return response.data; } catch (error) { throw this.handleArticleError(error, "Failed to verify article"); } } /** * Get article details * @param accessToken OA access token * @param articleId Article ID * @returns Article details */ async getArticleDetail(accessToken, articleId) { try { if (!articleId || articleId.trim() === "") { throw new common_1.ZaloSDKError("Article ID cannot be empty", -1); } const response = await this.client.apiGet(this.endpoints.article.detail, accessToken, { id: articleId }); return response.data; } catch (error) { throw this.handleArticleError(error, "Failed to get article detail"); } } /** * Get article list * @param accessToken OA access token * @param request List parameters * @returns Article list */ async getArticleList(accessToken, request) { try { // Validate input this.validateArticleListRequest(request); const response = await this.client.apiGet(this.endpoints.article.list, accessToken, { offset: request.offset, limit: request.limit, type: request.type, }); return response; } catch (error) { throw this.handleArticleError(error, "Failed to get article list"); } } /** * Remove article * @param accessToken OA access token * @param articleId Article ID to remove * @returns Removal result */ async removeArticle(accessToken, articleId) { try { if (!articleId || articleId.trim() === "") { throw new common_1.ZaloSDKError("Article ID cannot be empty", -1); } const data = { id: articleId, }; const response = await this.client.apiPost(this.endpoints.article.remove, accessToken, data); return response; } catch (error) { // Handle case where article may already be deleted if (error instanceof common_1.ZaloSDKError && error.message.includes("No data received")) { return { message: "Article has been deleted or does not exist", }; } throw this.handleArticleError(error, "Failed to remove article"); } } handleArticleError(error, defaultMessage) { if (error instanceof common_1.ZaloSDKError) { return error; } if (error.response?.data) { const errorData = error.response.data; return new common_1.ZaloSDKError(`${defaultMessage}: ${errorData.message || errorData.error || "Unknown error"}`, errorData.error || -1, errorData); } return new common_1.ZaloSDKError(`${defaultMessage}: ${error.message || "Unknown error"}`, -1, error); } /** * Update normal article * @param accessToken OA access token * @param request Update information * @returns Token for tracking update progress */ async updateNormalArticle(accessToken, request) { try { // Validate input this.validateUpdateNormalArticleRequest(request); // Process tracking link let processedTrackingLink = "https://v2.redai.vn/auth"; if (request.tracking_link && this.isValidTrackingLink(request.tracking_link)) { processedTrackingLink = request.tracking_link; } const data = { id: request.id, type: request.type, title: request.title, author: request.author, cover: request.cover, description: request.description, body: request.body, related_medias: request.related_medias || [], tracking_link: processedTrackingLink, status: request.status || article_1.ArticleStatus.HIDE, comment: request.comment || article_1.CommentStatus.SHOW, }; const response = await this.client.apiPost(this.endpoints.article.update, accessToken, data); return response; } catch (error) { throw this.handleArticleError(error, "Failed to update normal article"); } } /** * Update video article * @param accessToken OA access token * @param request Update information * @returns Token for tracking update progress */ async updateVideoArticle(accessToken, request) { try { // Validate input this.validateUpdateVideoArticleRequest(request); const data = { id: request.id, type: request.type, title: request.title, description: request.description, video_id: request.video_id, avatar: request.avatar, status: request.status || article_1.ArticleStatus.HIDE, comment: request.comment || article_1.CommentStatus.SHOW, }; const response = await this.client.apiPost(this.endpoints.article.update, accessToken, data); return response; } catch (error) { throw this.handleArticleError(error, "Failed to update video article"); } } /** * Update article (automatically detect type) * @param accessToken OA access token * @param request Update information * @returns Token for tracking update progress */ async updateArticle(accessToken, request) { if (request.type === article_1.ArticleType.NORMAL) { return this.updateNormalArticle(accessToken, request); } else if (request.type === article_1.ArticleType.VIDEO) { return this.updateVideoArticle(accessToken, request); } else { throw new common_1.ZaloSDKError('Invalid article type. Only "normal" and "video" are supported', -1); } } /** * Update article status only * @param accessToken OA access token * @param articleId Article ID to update * @param status New status ('show' or 'hide') * @param comment Optional comment status ('show' or 'hide') * @returns Token for tracking update progress */ async updateArticleStatus(accessToken, articleId, status, comment) { try { // Validate input if (!articleId || articleId.trim() === "") { throw new common_1.ZaloSDKError("Article ID cannot be empty", -1); } if (!Object.values(article_1.ArticleStatus).includes(status)) { throw new common_1.ZaloSDKError(`Invalid status. Must be one of: ${Object.values(article_1.ArticleStatus).join(', ')}`, -1); } if (comment && !Object.values(article_1.CommentStatus).includes(comment)) { throw new common_1.ZaloSDKError(`Invalid comment status. Must be one of: ${Object.values(article_1.CommentStatus).join(', ')}`, -1); } // 1. Get article details first const articleDetail = await this.getArticleDetail(accessToken, articleId); // 2. Prepare update request based on article type let updateRequest; if (articleDetail.type === article_1.ArticleType.NORMAL) { // For normal articles const normalDetail = articleDetail; updateRequest = { id: articleId, type: article_1.ArticleType.NORMAL, title: normalDetail.title, author: normalDetail.author, cover: normalDetail.cover, description: normalDetail.description, body: normalDetail.body, related_medias: normalDetail.related_medias, tracking_link: normalDetail.tracking_link, status: status, comment: comment || normalDetail.comment, }; } else if (articleDetail.type === article_1.ArticleType.VIDEO) { // For video articles const videoDetail = articleDetail; updateRequest = { id: articleId, type: article_1.ArticleType.VIDEO, title: videoDetail.title, description: videoDetail.description, video_id: videoDetail.video_id, avatar: videoDetail.avatar, status: status, comment: comment || videoDetail.comment, }; } else { throw new common_1.ZaloSDKError(`Unsupported article type: ${articleDetail.type}`, -1); } // 3. Update the article with new status return await this.updateArticle(accessToken, updateRequest); } catch (error) { throw this.handleArticleError(error, "Failed to update article status"); } } // ==================== VALIDATION METHODS ==================== /** * Validate normal article creation request */ validateNormalArticleRequest(request) { if (!request.title || request.title.trim() === "") { throw new common_1.ZaloSDKError("Title cannot be empty", -1); } if (request.title.length > article_1.ARTICLE_CONSTRAINTS.TITLE_MAX_LENGTH) { throw new common_1.ZaloSDKError(`Title cannot exceed ${article_1.ARTICLE_CONSTRAINTS.TITLE_MAX_LENGTH} characters`, -1); } if (!request.author || request.author.trim() === "") { throw new common_1.ZaloSDKError("Author cannot be empty", -1); } if (request.author.length > article_1.ARTICLE_CONSTRAINTS.AUTHOR_MAX_LENGTH) { throw new common_1.ZaloSDKError(`Author name cannot exceed ${article_1.ARTICLE_CONSTRAINTS.AUTHOR_MAX_LENGTH} characters`, -1); } if (!request.description || request.description.trim() === "") { throw new common_1.ZaloSDKError("Description cannot be empty", -1); } if (request.description.length > article_1.ARTICLE_CONSTRAINTS.DESCRIPTION_MAX_LENGTH) { throw new common_1.ZaloSDKError(`Description cannot exceed ${article_1.ARTICLE_CONSTRAINTS.DESCRIPTION_MAX_LENGTH} characters`, -1); } if (!request.cover) { throw new common_1.ZaloSDKError("Cover information cannot be empty", -1); } if (!request.body || request.body.length === 0) { throw new common_1.ZaloSDKError("Article body cannot be empty", -1); } // Validate cover this.validateCover(request.cover); // Validate body items request.body.forEach((item, index) => { this.validateBodyItem(item, index); }); } /** * Validate video article creation request */ validateVideoArticleRequest(request) { if (!request.title || request.title.trim() === "") { throw new common_1.ZaloSDKError("Title cannot be empty", -1); } if (request.title.length > article_1.ARTICLE_CONSTRAINTS.TITLE_MAX_LENGTH) { throw new common_1.ZaloSDKError(`Title cannot exceed ${article_1.ARTICLE_CONSTRAINTS.TITLE_MAX_LENGTH} characters`, -1); } if (!request.description || request.description.trim() === "") { throw new common_1.ZaloSDKError("Description cannot be empty", -1); } if (request.description.length > article_1.ARTICLE_CONSTRAINTS.DESCRIPTION_MAX_LENGTH) { throw new common_1.ZaloSDKError(`Description cannot exceed ${article_1.ARTICLE_CONSTRAINTS.DESCRIPTION_MAX_LENGTH} characters`, -1); } if (!request.video_id || request.video_id.trim() === "") { throw new common_1.ZaloSDKError("Video ID cannot be empty", -1); } if (!request.avatar || request.avatar.trim() === "") { throw new common_1.ZaloSDKError("Avatar URL cannot be empty", -1); } } /** * Validate article list request */ validateArticleListRequest(request) { if (request.offset < 0) { throw new common_1.ZaloSDKError("Offset must be >= 0", -1); } if (request.limit <= 0) { throw new common_1.ZaloSDKError("Limit must be > 0", -1); } if (request.limit > article_1.ARTICLE_CONSTRAINTS.LIST_MAX_LIMIT) { throw new common_1.ZaloSDKError(`Limit cannot exceed ${article_1.ARTICLE_CONSTRAINTS.LIST_MAX_LIMIT}`, -1); } if (!["normal", "video"].includes(request.type)) { throw new common_1.ZaloSDKError('Type must be "normal" or "video"', -1); } } /** * Validate normal article update request */ validateUpdateNormalArticleRequest(request) { if (!request.id || request.id.trim() === "") { throw new common_1.ZaloSDKError("Article ID cannot be empty", -1); } // Use the same validation as creation this.validateNormalArticleRequest(request); } /** * Validate video article update request */ validateUpdateVideoArticleRequest(request) { if (!request.id || request.id.trim() === "") { throw new common_1.ZaloSDKError("Article ID cannot be empty", -1); } // Use the same validation as creation this.validateVideoArticleRequest(request); } /** * Validate cover information */ validateCover(cover) { if (!cover.cover_type) { throw new common_1.ZaloSDKError("Cover type cannot be empty", -1); } if (!["photo", "video"].includes(cover.cover_type)) { throw new common_1.ZaloSDKError('Cover type must be "photo" or "video"', -1); } if (cover.cover_type === article_1.CoverType.PHOTO && (!cover.photo_url || cover.photo_url.trim() === "")) { throw new common_1.ZaloSDKError('Photo URL cannot be empty when cover_type is "photo"', -1); } if (cover.cover_type === article_1.CoverType.VIDEO) { if (!cover.video_id || cover.video_id.trim() === "") { throw new common_1.ZaloSDKError('Video ID cannot be empty when cover_type is "video"', -1); } if (cover.cover_view && !["horizontal", "vertical", "square"].includes(cover.cover_view)) { throw new common_1.ZaloSDKError('cover_view must be "horizontal", "vertical" or "square"', -1); } } if (!["show", "hide"].includes(cover.status)) { throw new common_1.ZaloSDKError('Cover status must be "show" or "hide"', -1); } } /** * Validate body item */ validateBodyItem(item, index) { if (!item.type) { throw new common_1.ZaloSDKError(`Content type at position ${index + 1} cannot be empty`, -1); } if (!["text", "image", "video", "product"].includes(item.type)) { throw new common_1.ZaloSDKError(`Content type at position ${index + 1} must be "text", "image", "video" or "product"`, -1); } switch (item.type) { case article_1.BodyItemType.TEXT: if (!item.content || item.content.trim() === "") { throw new common_1.ZaloSDKError(`Text content at position ${index + 1} cannot be empty`, -1); } break; case article_1.BodyItemType.IMAGE: if (!item.url || item.url.trim() === "") { throw new common_1.ZaloSDKError(`Image URL at position ${index + 1} cannot be empty`, -1); } break; case article_1.BodyItemType.VIDEO: if (!item.video_id && !item.url) { throw new common_1.ZaloSDKError(`Video at position ${index + 1} must have video_id or url`, -1); } break; case article_1.BodyItemType.PRODUCT: if (!item.id || item.id.trim() === "") { throw new common_1.ZaloSDKError(`Product ID at position ${index + 1} cannot be empty`, -1); } break; } } isValidTrackingLink(url) { try { new URL(url); return true; } catch { return false; } } } exports.ArticleService = ArticleService; //# sourceMappingURL=article.service.js.map