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