@memberjunction/actions-bizapps-social
Version:
Social Media Actions for MemberJunction - Twitter, LinkedIn, Facebook, Instagram, TikTok, YouTube, HootSuite, Buffer
295 lines • 12.4 kB
JavaScript
;
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