@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
326 lines • 14 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.VideoUploadService = void 0;
const article_1 = require("../types/article");
const common_1 = require("../types/common");
const axios_1 = __importDefault(require("axios"));
const form_data_1 = __importDefault(require("form-data"));
/**
* Service for handling Zalo Official Account Video Upload APIs
*
* CONDITIONS FOR USING ZALO VIDEO UPLOAD:
*
* 1. GENERAL CONDITIONS:
* - OA must have permission to upload videos
* - Access token must have "manage_article" scope
* - OA must have active status and be verified
*
* 2. VIDEO FILE REQUIREMENTS:
* - File formats: MP4, AVI only
* - Maximum file size: 50MB
* - Video processing: asynchronous, use token to check status
* - Processing time: varies based on video size and quality
*
* 3. UPLOAD PROCESS:
* - Step 1: Upload video file and get token
* - Step 2: Use token to check processing status
* - Step 3: Get video_id when processing is complete
* - Step 4: Use video_id in article creation
*
* 4. STATUS CODES:
* - 0: Trạng thái không xác định
* - 1: Video đã được xử lý thành công và có thể sử dụng
* - 2: Video đã bị khóa
* - 3: Video đang được xử lý
* - 4: Video xử lý thất bại
* - 5: Video đã bị xóa
*/
class VideoUploadService {
constructor(client) {
this.client = client;
// Zalo API endpoints - organized by functionality
this.endpoints = {
video: {
upload: "https://openapi.zalo.me/v2.0/article/upload_video/preparevideo",
verify: "https://openapi.zalo.me/v2.0/article/upload_video/verify",
info: "https://openapi.zalo.me/v2.0/article/video/info",
},
};
}
/**
* Upload video file for article
* @param accessToken OA access token
* @param file Video file (Buffer or File)
* @param filename Original filename (optional, will be generated if not provided)
* @returns Token for tracking upload progress
*/
async uploadVideo(accessToken, file, filename) {
try {
// Validate file
this.validateVideoFile(file, filename);
// Prepare filename
const actualFilename = Buffer.isBuffer(file)
? filename || "video.mp4"
: file.name || filename || "video.mp4";
// Convert file to Buffer if needed
let bufferToUpload;
if (Buffer.isBuffer(file)) {
bufferToUpload = file;
}
else {
// Convert File (browser-like) to Buffer for Node upload
const arrayBuffer = await file.arrayBuffer();
bufferToUpload = Buffer.from(arrayBuffer);
}
// Use direct axios call with FormData (same as successful direct upload)
const formData = new form_data_1.default();
formData.append('file', bufferToUpload, {
filename: actualFilename,
contentType: this.getMimeTypeFromFilename(actualFilename),
});
const response = await axios_1.default.post(this.endpoints.video.upload, formData, {
headers: {
...formData.getHeaders(),
'access_token': accessToken,
},
timeout: 300000, // 5 minutes timeout
maxContentLength: Infinity,
maxBodyLength: Infinity,
});
// Parse response according to Zalo API format
const responseData = response.data;
if (responseData.error && responseData.error !== 0) {
throw new common_1.ZaloSDKError(`Zalo API error: ${responseData.message || 'Unknown error'}`, responseData.error);
}
return responseData.data;
}
catch (error) {
throw this.handleVideoUploadError(error, "Failed to upload video");
}
}
/**
* Check video upload status
* @param accessToken OA access token
* @param token Token from video upload response
* @returns Video status information
*/
async checkVideoStatus(accessToken, token) {
try {
if (!token || token.trim() === "") {
throw new common_1.ZaloSDKError("Token cannot be empty", -1);
}
// According to docs, token should be in header, not query params
const response = await this.client.apiGetWithHeaders(this.endpoints.video.verify, {
access_token: accessToken,
token: token,
});
return response.data;
}
catch (error) {
throw this.handleVideoUploadError(error, "Failed to check video status");
}
}
/**
* Wait for video upload completion with polling
* @param accessToken OA access token
* @param token Token from video upload response
* @param maxWaitTime Maximum wait time in milliseconds (default: 5 minutes)
* @param pollInterval Polling interval in milliseconds (default: 5 seconds)
* @returns Final video status when completed
*/
async waitForUploadCompletion(accessToken, token, maxWaitTime = 5 * 60 * 1000, // 5 minutes
pollInterval = 5 * 1000 // 5 seconds
) {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
try {
const status = await this.checkVideoStatus(accessToken, token);
// Status 1 = Video đã được xử lý thành công và có thể sử dụng
if (status.status === article_1.VideoUploadStatus.SUCCESS && status.video_id) {
return status;
}
// Status 4 = Video xử lý thất bại
if (status.status === article_1.VideoUploadStatus.FAILED) {
throw new common_1.ZaloSDKError(`Video processing failed: ${status.status_message}`, status.convert_error_code);
}
// Status 2 = Video đã bị khóa
if (status.status === article_1.VideoUploadStatus.LOCKED) {
throw new common_1.ZaloSDKError(`Video has been locked: ${status.status_message}`, -1);
}
// Status 5 = Video đã bị xóa
if (status.status === article_1.VideoUploadStatus.DELETED) {
throw new common_1.ZaloSDKError(`Video has been deleted: ${status.status_message}`, -1);
}
// Continue polling for status 3 (đang được xử lý) or 0 (không xác định)
if (status.status === article_1.VideoUploadStatus.PROCESSING || status.status === article_1.VideoUploadStatus.UNKNOWN) {
await this.sleep(pollInterval);
continue;
}
// Unknown status
throw new common_1.ZaloSDKError(`Unknown video processing status: ${status.status}`, -1);
}
catch (error) {
if (error instanceof common_1.ZaloSDKError) {
throw error;
}
// Continue polling on network errors
await this.sleep(pollInterval);
}
}
throw new common_1.ZaloSDKError(`Video processing timeout after ${maxWaitTime / 1000} seconds`, -1);
}
/**
* Upload video from URL
* @param accessToken OA access token
* @param videoUrl URL of the video to upload
* @returns Token for tracking upload progress
*/
async uploadVideoFromUrl(accessToken, videoUrl) {
try {
if (!videoUrl || videoUrl.trim() === "") {
throw new common_1.ZaloSDKError("Video URL cannot be empty", -1);
}
// Validate URL format
try {
new URL(videoUrl);
}
catch {
throw new common_1.ZaloSDKError("Invalid video URL format", -1);
}
// Download video from URL
const response = await fetch(videoUrl);
if (!response.ok) {
throw new common_1.ZaloSDKError(`Failed to download video from URL: ${response.statusText}`, -1);
}
const videoBuffer = Buffer.from(await response.arrayBuffer());
const filename = this.extractFilenameFromUrl(videoUrl) || "video.mp4";
// Upload the downloaded video
return await this.uploadVideo(accessToken, videoBuffer, filename);
}
catch (error) {
throw this.handleVideoUploadError(error, "Failed to upload video from URL");
}
}
/**
* Get video information by video ID
* @param accessToken OA access token
* @param videoId Video ID
* @returns Video information
*/
async getVideoInfo(accessToken, videoId) {
try {
if (!videoId || videoId.trim() === "") {
throw new common_1.ZaloSDKError("Video ID cannot be empty", -1);
}
const response = await this.client.apiGet(this.endpoints.video.info, accessToken, { video_id: videoId });
return response;
}
catch (error) {
throw this.handleVideoUploadError(error, "Failed to get video info");
}
}
/**
* Upload video and wait for completion - Combined method
* @param accessToken OA access token
* @param file Video file (Buffer or File)
* @param filename Original filename (optional, will be generated if not provided)
* @param maxWaitTime Maximum wait time in milliseconds (default: 5 minutes)
* @param pollInterval Polling interval in milliseconds (default: 5 seconds)
* @returns Final video status when completed
*/
async uploadVideoAndWaitForCompletion(accessToken, file, filename, maxWaitTime = 5 * 60 * 1000, // 5 minutes
pollInterval = 5 * 1000 // 5 seconds
) {
try {
// Step 1: Upload video and get token
const uploadResult = await this.uploadVideo(accessToken, file, filename);
// Step 2: Wait for upload completion and return final status
return await this.waitForUploadCompletion(accessToken, uploadResult.token, maxWaitTime, pollInterval);
}
catch (error) {
throw this.handleVideoUploadError(error, "Failed to upload video and wait for completion");
}
}
// ==================== VALIDATION METHODS ====================
/**
* Validate video file
*/
validateVideoFile(file, filename) {
if (!file) {
throw new common_1.ZaloSDKError("Video file cannot be empty", -1);
}
let fileSize;
let fileName;
let mimeType;
if (Buffer.isBuffer(file)) {
fileSize = file.length;
fileName = filename || "video.mp4";
mimeType = this.getMimeTypeFromFilename(fileName);
}
else {
// Handle File input with proper type casting
const fileObj = file;
fileSize = fileObj.size;
fileName = fileObj.name || filename || "video.mp4";
mimeType = fileObj.type || this.getMimeTypeFromFilename(fileName);
}
// Check file size (50MB)
if (fileSize > article_1.VIDEO_CONSTRAINTS.maxSize) {
throw new common_1.ZaloSDKError(`Video file size exceeds ${article_1.VIDEO_CONSTRAINTS.maxSize / (1024 * 1024)}MB limit`, -1);
}
// Check file extension
const fileExtension = fileName
.toLowerCase()
.substring(fileName.lastIndexOf("."));
if (!article_1.VIDEO_CONSTRAINTS.allowedExtensions.includes(fileExtension)) {
throw new common_1.ZaloSDKError(`Unsupported video format. Only ${article_1.VIDEO_CONSTRAINTS.allowedExtensions.join(", ")} are supported`, -1);
}
// Check MIME type
if (!article_1.VIDEO_CONSTRAINTS.allowedMimeTypes.includes(mimeType)) {
throw new common_1.ZaloSDKError(`Unsupported video MIME type. Only ${article_1.VIDEO_CONSTRAINTS.allowedMimeTypes.join(", ")} are supported`, -1);
}
}
// ==================== HELPER METHODS ====================
getMimeTypeFromFilename(filename) {
const extension = filename
.toLowerCase()
.substring(filename.lastIndexOf("."));
switch (extension) {
case ".mp4":
return "video/mp4";
case ".avi":
return "video/x-msvideo";
default:
return "video/mp4"; // Default fallback
}
}
extractFilenameFromUrl(url) {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const filename = pathname.substring(pathname.lastIndexOf("/") + 1);
return filename || null;
}
catch {
return null;
}
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
handleVideoUploadError(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);
}
}
exports.VideoUploadService = VideoUploadService;
//# sourceMappingURL=video-upload.service.js.map