@warriorteam/redai-zalo-sdk
Version:
Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account, ZNS, Consultation Service, Group Messaging, and Social APIs
306 lines • 13.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.VideoUploadService = void 0;
const article_1 = require("../types/article");
const common_1 = require("../types/common");
/**
* 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);
// Create FormData for upload
const formData = new FormData();
if (Buffer.isBuffer(file)) {
// Handle Buffer input - create a new ArrayBuffer to ensure compatibility
const actualFilename = filename || "video.mp4";
const arrayBuffer = new ArrayBuffer(file.length);
const uint8Array = new Uint8Array(arrayBuffer);
file.copy(uint8Array);
const blob = new Blob([uint8Array], {
type: this.getMimeTypeFromFilename(actualFilename),
});
formData.append("file", blob, actualFilename);
}
else {
// Handle File input
const actualFilename = file.name || filename || "video.mp4";
formData.append("file", file, actualFilename);
}
const response = await this.client.apiPostFormData(this.endpoints.video.upload, accessToken, formData);
return response.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