UNPKG

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