UNPKG

@memberjunction/actions-bizapps-social

Version:

Social Media Actions for MemberJunction - Twitter, LinkedIn, Facebook, Instagram, TikTok, YouTube, HootSuite, Buffer

295 lines 12.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TikTokBaseAction = void 0; const global_1 = require("@memberjunction/global"); const base_social_action_1 = require("../../base/base-social.action"); const core_1 = require("@memberjunction/core"); const axios_1 = __importStar(require("axios")); const actions_1 = require("@memberjunction/actions"); /** * Base class for all TikTok social media actions. * Handles TikTok-specific authentication and API interaction patterns. */ let TikTokBaseAction = class TikTokBaseAction extends base_social_action_1.BaseSocialMediaAction { get platformName() { return 'TikTok'; } get apiBaseUrl() { return 'https://open-api.tiktok.com'; } axiosInstance = null; /** * Initialize axios instance with interceptors */ getAxiosInstance() { if (!this.axiosInstance) { this.axiosInstance = axios_1.default.create({ baseURL: this.apiBaseUrl, timeout: 30000, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); // Request interceptor for logging this.axiosInstance.interceptors.request.use((config) => { this.logApiRequest(config.method?.toUpperCase() || 'GET', config.url || '', config.data); return config; }, (error) => { (0, core_1.LogError)(`TikTok API Request Error: ${error.message}`); return Promise.reject(error); }); // Response interceptor for logging and error handling this.axiosInstance.interceptors.response.use((response) => { this.logApiResponse(response.data); return response; }, async (error) => { if (error.response?.status === 429) { // Rate limit hit const retryAfter = error.response.headers['retry-after']; await this.handleRateLimit(retryAfter ? parseInt(retryAfter) : undefined); // Retry the request return this.axiosInstance?.request(error.config); } (0, core_1.LogError)(`TikTok API Response Error: ${error.message}`); return Promise.reject(error); }); } return this.axiosInstance; } /** * Make authenticated TikTok API request */ async makeTikTokRequest(endpoint, method = 'GET', data, params) { const token = this.getAccessToken(); if (!token) { throw new Error('No access token available for TikTok'); } const axios = this.getAxiosInstance(); try { const response = await axios.request({ method, url: endpoint, data, params, headers: { 'Authorization': `Bearer ${token}` } }); return response.data; } catch (error) { if (error instanceof axios_1.AxiosError) { if (error.response?.status === 401) { // Token might be expired, try to refresh await this.refreshAccessToken(); // Retry with new token const newToken = this.getAccessToken(); const retryResponse = await axios.request({ method, url: endpoint, data, params, headers: { 'Authorization': `Bearer ${newToken}` } }); return retryResponse.data; } const errorMessage = error.response?.data?.error?.message || error.message; throw new Error(`TikTok API error: ${errorMessage}`); } throw error; } } /** * Refresh TikTok access token */ async refreshAccessToken() { const refreshToken = this.getRefreshToken(); if (!refreshToken) { throw new Error('No refresh token available for TikTok'); } try { const response = await axios_1.default.post(`${this.apiBaseUrl}/oauth/refresh_token/`, { client_key: this.getCustomAttribute(2), // Store client key in CustomAttribute2 grant_type: 'refresh_token', refresh_token: refreshToken }); const { access_token, refresh_token: newRefreshToken, expires_in } = response.data.data; // Update stored tokens await this.updateStoredTokens(access_token, newRefreshToken, expires_in); (0, core_1.LogStatus)('TikTok access token refreshed successfully'); } catch (error) { (0, core_1.LogError)(`Failed to refresh TikTok access token: ${error}`); throw new Error('Failed to refresh TikTok access token'); } } /** * Upload media to TikTok (requires special approval) */ async uploadSingleMedia(file) { // TikTok video upload requires special approval // This is a placeholder implementation throw new Error('TikTok video upload requires special API approval. Please use TikTok Creator Studio for video uploads.'); } /** * Search posts (videos) - TikTok only allows searching user's own videos */ async searchPosts(params) { // TikTok doesn't provide a general search API for security/privacy reasons // We can only search within a user's own videos const videos = await this.getUserVideos(); let filtered = videos; // Apply filters if provided if (params.query) { const query = params.query.toLowerCase(); filtered = filtered.filter(video => video.title.toLowerCase().includes(query) || video.description.toLowerCase().includes(query)); } if (params.hashtags && params.hashtags.length > 0) { filtered = filtered.filter(video => { const videoHashtags = this.extractHashtags(video.description); return params.hashtags.some((tag) => videoHashtags.includes(tag.toLowerCase())); }); } if (params.startDate) { const startTime = new Date(params.startDate).getTime() / 1000; filtered = filtered.filter(video => video.create_time >= startTime); } if (params.endDate) { const endTime = new Date(params.endDate).getTime() / 1000; filtered = filtered.filter(video => video.create_time <= endTime); } // Apply limit and offset if (params.offset) { filtered = filtered.slice(params.offset); } if (params.limit) { filtered = filtered.slice(0, params.limit); } return filtered.map(video => this.normalizePost(video)); } /** * Get user's videos */ async getUserVideos() { const response = await this.makeTikTokRequest('/v2/video/list/', 'GET', undefined, { fields: 'id,share_url,title,description,duration,cover_image_url,share_count,view_count,like_count,comment_count,create_time' }); return response.data?.videos || []; } /** * Convert TikTok video to common post format */ normalizePost(video) { return { id: video.id, platform: this.platformName, profileId: this.getCustomAttribute(1) || '', // Store user ID in CustomAttribute1 content: video.description || video.title, mediaUrls: [video.cover_image_url], publishedAt: new Date(video.create_time * 1000), analytics: { impressions: video.view_count, engagements: video.like_count + video.comment_count + video.share_count, clicks: 0, // Not available in TikTok API shares: video.share_count, comments: video.comment_count, likes: video.like_count, reach: video.view_count, videoViews: video.view_count, platformMetrics: { duration: video.duration, shareUrl: video.share_url } }, platformSpecificData: { ...video, videoUrl: video.share_url } }; } /** * Normalize TikTok analytics to common format */ normalizeAnalytics(platformData) { return { impressions: platformData.view_count || 0, engagements: (platformData.like_count || 0) + (platformData.comment_count || 0) + (platformData.share_count || 0), clicks: 0, // Not available in TikTok API shares: platformData.share_count || 0, comments: platformData.comment_count || 0, likes: platformData.like_count || 0, reach: platformData.view_count || 0, videoViews: platformData.view_count || 0, platformMetrics: platformData }; } /** * Extract hashtags from video description */ extractHashtags(description) { const hashtagRegex = /#(\w+)/g; const matches = description.match(hashtagRegex) || []; return matches.map(tag => tag.substring(1).toLowerCase()); } /** * Get current user info */ async getCurrentUser() { const response = await this.makeTikTokRequest('/v2/user/info/', 'GET', undefined, { fields: 'open_id,union_id,avatar_url,display_name,bio_description,profile_deep_link,is_verified,follower_count,following_count,likes_count' }); return response.data?.user; } /** * Validate TikTok-specific media requirements */ validateMediaFile(file) { const allowedTypes = ['video/mp4', 'video/quicktime', 'video/webm']; if (!allowedTypes.includes(file.mimeType)) { throw new Error(`TikTok only supports video files. Got: ${file.mimeType}`); } // Max file size: 287.6 MB const maxSize = 287.6 * 1024 * 1024; if (file.size > maxSize) { throw new Error(`File size exceeds TikTok limit of 287.6 MB. Got: ${(file.size / 1024 / 1024).toFixed(2)} MB`); } } }; exports.TikTokBaseAction = TikTokBaseAction; exports.TikTokBaseAction = TikTokBaseAction = __decorate([ (0, global_1.RegisterClass)(actions_1.BaseAction, 'TikTokBaseAction') ], TikTokBaseAction); //# sourceMappingURL=tiktok-base.action.js.map