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