UNPKG

@memberjunction/actions-bizapps-social

Version:

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

413 lines (367 loc) 13.3 kB
import { RegisterClass } from '@memberjunction/global'; import { FacebookBaseAction } from '../facebook-base.action'; import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base'; import { SocialMediaErrorCode } from '../../../base/base-social.action'; import { LogStatus, LogError } from '@memberjunction/core'; import axios from 'axios'; import { BaseAction } from '@memberjunction/actions'; /** * Boosts (promotes) a Facebook post to reach a wider audience. * Creates a simple ad campaign to increase post visibility. */ @RegisterClass(BaseAction, 'FacebookBoostPostAction') export class FacebookBoostPostAction extends FacebookBaseAction { /** * Get action description */ public get Description(): string { return 'Boosts (promotes) a Facebook post to reach a wider audience through paid advertising'; } /** * Define the parameters for this action */ public get Params(): ActionParam[] { return [ ...this.commonSocialParams, { Name: 'PostID', Type: 'Input', Value: null, }, { Name: 'AdAccountID', Type: 'Input', Value: null, }, { Name: 'Budget', Type: 'Input', Value: null, }, { Name: 'Duration', Type: 'Input', Value: 7, }, { Name: 'Objective', Type: 'Input', Value: 'POST_ENGAGEMENT', }, { Name: 'AudienceType', Type: 'Input', Value: 'AUTO', }, { Name: 'TargetingSpec', Type: 'Input', Value: null, }, { Name: 'StartTime', Type: 'Input', Value: null, }, { Name: 'CallToAction', Type: 'Input', Value: null, } ]; } /** * Execute the action */ protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> { const { Params, ContextUser } = params; try { // Validate required parameters const companyIntegrationId = this.getParamValue(Params, 'CompanyIntegrationID'); const postId = this.getParamValue(Params, 'PostID'); const adAccountId = this.getParamValue(Params, 'AdAccountID'); const budget = this.getParamValue(Params, 'Budget') as number; if (!companyIntegrationId) { return { Success: false, Message: 'CompanyIntegrationID is required', ResultCode: 'INVALID_TOKEN' }; } if (!postId) { return { Success: false, Message: 'PostID is required', ResultCode: 'MISSING_REQUIRED_PARAM' }; } if (!adAccountId) { return { Success: false, Message: 'AdAccountID is required', ResultCode: 'MISSING_REQUIRED_PARAM' }; } if (!budget || budget <= 0) { return { Success: false, Message: 'Budget must be a positive number', ResultCode: 'INVALID_BUDGET' }; } // Initialize OAuth if (!await this.initializeOAuth(companyIntegrationId)) { return { Success: false, Message: 'Failed to initialize Facebook OAuth connection', ResultCode: 'INVALID_TOKEN' }; } // Get parameters const duration = this.getParamValue(Params, 'Duration') as number || 7; const objective = this.getParamValue(Params, 'Objective') as string || 'POST_ENGAGEMENT'; const audienceType = this.getParamValue(Params, 'AudienceType') as string || 'AUTO'; const targetingSpec = this.getParamValue(Params, 'TargetingSpec') as any; const startTime = this.getParamValue(Params, 'StartTime') as string; const callToAction = this.getParamValue(Params, 'CallToAction') as string; // Validate duration if (duration < 1 || duration > 30) { return { Success: false, Message: 'Duration must be between 1 and 30 days', ResultCode: 'INVALID_DURATION' }; } // Extract page ID from post ID const pageId = postId.split('_')[0]; // Get page access token const pageToken = await this.getPageAccessToken(pageId); LogStatus(`Creating boost campaign for post ${postId}...`); // Step 1: Create campaign const campaignName = `Boost Post ${postId} - ${new Date().toISOString()}`; const campaign = await this.createCampaign(adAccountId, campaignName, objective); // Step 2: Create ad set with targeting const adSetName = `Ad Set for ${postId}`; const targeting = this.buildTargeting(audienceType, targetingSpec, pageId); const adSet = await this.createAdSet( adAccountId, campaign.id, adSetName, budget, duration, targeting, startTime ); // Step 3: Create ad creative from post const creative = await this.createCreativeFromPost(adAccountId, postId, pageToken, callToAction); // Step 4: Create the ad const adName = `Boosted Post ${postId}`; const ad = await this.createAd(adAccountId, adSet.id, creative.id, adName); // Get boost summary const boostSummary = { campaignId: campaign.id, adSetId: adSet.id, adId: ad.id, creativeId: creative.id, postId, budget, duration, objective, audienceType, startTime: adSet.start_time, endTime: adSet.end_time, status: ad.status, reviewStatus: ad.review_feedback?.global_review_status || 'PENDING', previewUrl: `https://www.facebook.com/ads/manager/account/campaigns?act=${adAccountId}&selected_campaign_ids=${campaign.id}` }; LogStatus(`Successfully created boost campaign for post ${postId}`); return { Success: true, Message: 'Post boost created successfully', ResultCode: 'SUCCESS', Params }; } catch (error) { LogError(`Failed to boost Facebook post: ${error instanceof Error ? error.message : 'Unknown error'}`); if (this.isAuthError(error)) { return this.handleOAuthError(error); } // Check for specific ad-related errors if (error instanceof Error) { if (error.message.includes('permissions')) { return { Success: false, Message: 'Insufficient permissions. Ensure the token has ads_management permission.', ResultCode: 'INSUFFICIENT_PERMISSIONS' }; } if (error.message.includes('budget')) { return { Success: false, Message: 'Invalid budget. Check minimum budget requirements for your currency.', ResultCode: 'INVALID_BUDGET' }; } } return { Success: false, Message: error instanceof Error ? error.message : 'Unknown error occurred', ResultCode: 'ERROR' }; } } /** * Create a campaign */ private async createCampaign(adAccountId: string, name: string, objective: string): Promise<any> { const response = await this.axiosInstance.post( `/${adAccountId}/campaigns`, { name, objective, status: 'PAUSED', // Start paused for safety special_ad_categories: [] // Required field } ); return response.data; } /** * Create an ad set */ private async createAdSet( adAccountId: string, campaignId: string, name: string, budget: number, durationDays: number, targeting: any, startTime?: string ): Promise<any> { const now = new Date(); const start = startTime ? new Date(startTime) : now; const end = new Date(start); end.setDate(end.getDate() + durationDays); // Calculate daily budget const dailyBudget = Math.ceil((budget * 100) / durationDays); // Convert to cents const response = await this.axiosInstance.post( `/${adAccountId}/adsets`, { name, campaign_id: campaignId, daily_budget: dailyBudget, billing_event: 'IMPRESSIONS', optimization_goal: this.getOptimizationGoal(campaignId), bid_strategy: 'LOWEST_COST_WITHOUT_CAP', targeting, start_time: start.toISOString(), end_time: end.toISOString(), status: 'PAUSED' } ); return response.data; } /** * Create creative from existing post */ private async createCreativeFromPost( adAccountId: string, postId: string, pageToken: string, callToAction?: string ): Promise<any> { const creativeData: any = { name: `Creative for ${postId}`, object_story_id: postId }; if (callToAction) { // Get post details to add CTA const postResponse = await axios.get( `${this.apiBaseUrl}/${postId}`, { params: { access_token: pageToken, fields: 'permalink_url' } } ); creativeData.call_to_action = { type: callToAction, value: { link: postResponse.data.permalink_url } }; } const response = await this.axiosInstance.post( `/${adAccountId}/adcreatives`, creativeData ); return response.data; } /** * Create the ad */ private async createAd( adAccountId: string, adSetId: string, creativeId: string, name: string ): Promise<any> { const response = await this.axiosInstance.post( `/${adAccountId}/ads`, { name, adset_id: adSetId, creative: { creative_id: creativeId }, status: 'PAUSED' } ); return response.data; } /** * Build targeting specification */ private buildTargeting(audienceType: string, customTargeting: any, pageId: string): any { const baseTargeting: any = { geo_locations: { countries: ['US'] // Default to US, can be overridden } }; switch (audienceType) { case 'FANS': baseTargeting.connections = [pageId]; break; case 'FANS_AND_CONNECTIONS': baseTargeting.connections = [pageId]; baseTargeting.friends_of_connections = [pageId]; break; case 'CUSTOM': if (customTargeting) { return { ...baseTargeting, ...customTargeting }; } break; case 'AUTO': default: // Facebook will automatically optimize targeting break; } // Add any custom targeting on top if (customTargeting && audienceType !== 'CUSTOM') { Object.assign(baseTargeting, customTargeting); } return baseTargeting; } /** * Get optimization goal based on objective */ private getOptimizationGoal(objective: string): string { const goalMap: Record<string, string> = { 'POST_ENGAGEMENT': 'POST_ENGAGEMENT', 'REACH': 'REACH', 'LINK_CLICKS': 'LINK_CLICKS', 'PAGE_LIKES': 'PAGE_LIKES', 'BRAND_AWARENESS': 'AD_RECALL_LIFT', 'VIDEO_VIEWS': 'VIDEO_VIEWS' }; return goalMap[objective] || 'POST_ENGAGEMENT'; } }