UNPKG

@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
"use strict"; 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