UNPKG

@memberjunction/actions-bizapps-social

Version:

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

420 lines 16 kB
"use strict"; 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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BufferBaseAction = 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 = __importDefault(require("axios")); const form_data_1 = __importDefault(require("form-data")); const actions_1 = require("@memberjunction/actions"); /** * Base class for all Buffer social media actions. * Handles Buffer-specific authentication and API interaction patterns. */ let BufferBaseAction = class BufferBaseAction extends base_social_action_1.BaseSocialMediaAction { get platformName() { return 'Buffer'; } get apiBaseUrl() { return 'https://api.bufferapp.com/1'; } axiosInstance = null; /** * Get axios instance with authentication */ getAxiosInstance() { if (!this.axiosInstance) { this.axiosInstance = axios_1.default.create({ baseURL: this.apiBaseUrl, timeout: 30000, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); // Add request interceptor for auth this.axiosInstance.interceptors.request.use((config) => { const token = this.getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { return Promise.reject(error); }); // Add response interceptor for rate limiting this.axiosInstance.interceptors.response.use((response) => { // Log rate limit headers if present const headers = response.headers; const rateLimitInfo = this.parseRateLimitHeaders(headers); if (rateLimitInfo) { (0, core_1.LogStatus)(`Buffer API - Remaining requests: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`); } 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) : 60); // Retry the request return this.axiosInstance.request(error.config); } return Promise.reject(error); }); } return this.axiosInstance; } /** * Refresh access token using refresh token */ async refreshAccessToken() { const refreshToken = this.getRefreshToken(); if (!refreshToken) { throw new Error('No refresh token available for Buffer'); } try { const response = await axios_1.default.post(`${this.apiBaseUrl}/oauth2/token`, { client_id: this.getCustomAttribute(1), // Store Buffer client ID in CustomAttribute1 client_secret: this.getCustomAttribute(2), // Store Buffer client secret in CustomAttribute2 refresh_token: refreshToken, grant_type: 'refresh_token' }); const { access_token, expires_in } = response.data; await this.updateStoredTokens(access_token, refreshToken, // Buffer doesn't rotate refresh tokens expires_in); (0, core_1.LogStatus)('Buffer access token refreshed successfully'); } catch (error) { (0, core_1.LogError)('Failed to refresh Buffer access token:', error); throw new Error('Failed to refresh Buffer access token'); } } /** * Get Buffer profiles for the authenticated user */ async getProfiles() { try { const response = await this.getAxiosInstance().get('/profiles.json'); return response.data || []; } catch (error) { (0, core_1.LogError)('Failed to get Buffer profiles:', error); throw error; } } /** * Upload media to Buffer */ async uploadSingleMedia(file) { try { const formData = new form_data_1.default(); // Convert base64 to buffer if needed const buffer = typeof file.data === 'string' ? Buffer.from(file.data, 'base64') : file.data; formData.append('media', buffer, { filename: file.filename, contentType: file.mimeType }); const response = await this.getAxiosInstance().post('/updates/media/upload.json', formData, { headers: { ...formData.getHeaders() } }); if (response.data && response.data.media && response.data.media[0]) { return response.data.media[0].picture; // URL of uploaded media } throw new Error('Failed to upload media to Buffer'); } catch (error) { (0, core_1.LogError)('Buffer media upload failed:', error); throw error; } } /** * Create a Buffer update (post) */ async createUpdate(profileIds, text, media, scheduledAt, options) { const data = { profile_ids: profileIds, text: text, shorten: options?.shorten !== false // Default to true }; if (media && media.length > 0) { data.media = media; } if (scheduledAt) { data.scheduled_at = Math.floor(scheduledAt.getTime() / 1000); // Unix timestamp } else if (options?.now) { data.now = true; } if (options?.top) { data.top = true; } if (options?.attachment !== undefined) { data.attachment = options.attachment; } try { const response = await this.getAxiosInstance().post('/updates/create.json', data); return response.data; } catch (error) { (0, core_1.LogError)('Failed to create Buffer update:', error); throw error; } } /** * Get updates (posts) from Buffer */ async getUpdates(profileId, status, options) { const params = { page: options?.page || 1, count: options?.count || 10, utc: options?.utc !== false // Default to true }; if (options?.since) { params.since = Math.floor(options.since.getTime() / 1000); } try { const response = await this.getAxiosInstance().get(`/profiles/${profileId}/updates/${status}.json`, { params }); return response.data; } catch (error) { (0, core_1.LogError)(`Failed to get ${status} updates from Buffer:`, error); throw error; } } /** * Delete a Buffer update */ async deleteUpdate(updateId) { try { const response = await this.getAxiosInstance().post(`/updates/${updateId}/destroy.json`); return response.data.success === true; } catch (error) { (0, core_1.LogError)('Failed to delete Buffer update:', error); throw error; } } /** * Reorder updates in the queue */ async reorderUpdates(profileId, updateIds, offset) { const data = { order: updateIds }; if (offset !== undefined) { data.offset = offset; } try { const response = await this.getAxiosInstance().post(`/profiles/${profileId}/updates/reorder.json`, data); return response.data; } catch (error) { (0, core_1.LogError)('Failed to reorder Buffer updates:', error); throw error; } } /** * Get analytics for sent posts */ async getAnalytics(updateId) { try { const response = await this.getAxiosInstance().get(`/updates/${updateId}/interactions.json`); return response.data; } catch (error) { (0, core_1.LogError)('Failed to get Buffer analytics:', error); throw error; } } /** * Search posts implementation for Buffer * Buffer doesn't have a native search API, so we'll fetch posts and filter client-side */ async searchPosts(params) { const posts = []; const profileIds = params.profileIds || []; // If no profile IDs provided, get all profiles if (profileIds.length === 0) { const profiles = await this.getProfiles(); profileIds.push(...profiles.map(p => p.id)); } // Fetch sent posts from each profile for (const profileId of profileIds) { try { let page = 1; let hasMore = true; while (hasMore && posts.length < (params.limit || 100)) { const result = await this.getUpdates(profileId, 'sent', { page: page, count: 100, since: params.startDate }); if (result.updates && result.updates.length > 0) { for (const update of result.updates) { const post = this.normalizePost(update); // Apply filters if (this.matchesSearchCriteria(post, params)) { posts.push(post); if (posts.length >= (params.limit || 100)) { hasMore = false; break; } } } page++; hasMore = result.updates.length === 100; } else { hasMore = false; } } } catch (error) { (0, core_1.LogError)(`Failed to search posts for profile ${profileId}:`, error); } } // Sort by published date descending posts.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime()); // Apply offset if specified if (params.offset) { return posts.slice(params.offset, params.offset + (params.limit || 100)); } return posts.slice(0, params.limit || 100); } /** * Check if a post matches search criteria */ matchesSearchCriteria(post, params) { // Check date range if (params.endDate && post.publishedAt > params.endDate) { return false; } // Check query text if (params.query) { const query = params.query.toLowerCase(); const content = post.content.toLowerCase(); if (!content.includes(query)) { return false; } } // Check hashtags if (params.hashtags && params.hashtags.length > 0) { const postHashtags = this.extractHashtags(post.content); const hasMatchingHashtag = params.hashtags.some(tag => postHashtags.includes(tag.toLowerCase().replace('#', ''))); if (!hasMatchingHashtag) { return false; } } return true; } /** * Extract hashtags from post content */ extractHashtags(content) { const regex = /#(\w+)/g; const hashtags = []; let match; while ((match = regex.exec(content)) !== null) { hashtags.push(match[1].toLowerCase()); } return hashtags; } /** * Normalize Buffer post to common format */ normalizePost(bufferPost) { const media = []; if (bufferPost.media) { if (bufferPost.media.picture) { media.push(bufferPost.media.picture); } if (bufferPost.media.link) { media.push(bufferPost.media.link); } } return { id: bufferPost.id, platform: 'Buffer', profileId: bufferPost.profile_id, content: bufferPost.text || '', mediaUrls: media, publishedAt: new Date(bufferPost.sent_at * 1000), // Convert Unix timestamp scheduledFor: bufferPost.due_at ? new Date(bufferPost.due_at * 1000) : undefined, analytics: bufferPost.statistics ? this.normalizeAnalytics(bufferPost.statistics) : undefined, platformSpecificData: { profileService: bufferPost.profile_service, status: bufferPost.status, userId: bufferPost.user_id, viaName: bufferPost.via, sourceUrl: bufferPost.source_url, day: bufferPost.day, dueTime: bufferPost.due_time, mediaDescription: bufferPost.media?.description, mediaTitle: bufferPost.media?.title, mediaLink: bufferPost.media?.link } }; } /** * Normalize Buffer analytics to common format */ normalizeAnalytics(bufferStats) { return { impressions: bufferStats.reach || 0, engagements: (bufferStats.clicks || 0) + (bufferStats.favorites || 0) + (bufferStats.mentions || 0) + (bufferStats.retweets || 0) + (bufferStats.shares || 0) + (bufferStats.comments || 0), clicks: bufferStats.clicks || 0, shares: bufferStats.shares || bufferStats.retweets || 0, comments: bufferStats.comments || bufferStats.mentions || 0, likes: bufferStats.favorites || bufferStats.likes || 0, reach: bufferStats.reach || 0, saves: undefined, videoViews: undefined, platformMetrics: bufferStats }; } /** * Map Buffer error to our error codes */ mapBufferError(error) { if (error.response) { const status = error.response.status; const data = error.response.data; if (status === 401) { return 'INVALID_TOKEN'; } else if (status === 429) { return 'RATE_LIMIT'; } else if (status === 403) { return 'INSUFFICIENT_PERMISSIONS'; } else if (status === 404) { return 'POST_NOT_FOUND'; } else if (data?.error?.includes('media')) { return 'INVALID_MEDIA'; } } return 'PLATFORM_ERROR'; } }; exports.BufferBaseAction = BufferBaseAction; exports.BufferBaseAction = BufferBaseAction = __decorate([ (0, global_1.RegisterClass)(actions_1.BaseAction, 'BufferBaseAction') ], BufferBaseAction); //# sourceMappingURL=buffer-base.action.js.map