UNPKG

@memberjunction/actions-bizapps-social

Version:

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

347 lines 14.9 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.LinkedInBaseAction = void 0; const global_1 = require("@memberjunction/global"); const base_social_action_1 = require("../../base/base-social.action"); const axios_1 = __importDefault(require("axios")); const core_1 = require("@memberjunction/core"); const actions_1 = require("@memberjunction/actions"); /** * Base class for all LinkedIn actions. * Handles LinkedIn-specific authentication, API interactions, and rate limiting. * Uses LinkedIn Marketing Developer Platform API v2. */ let LinkedInBaseAction = class LinkedInBaseAction extends base_social_action_1.BaseSocialMediaAction { get platformName() { return 'LinkedIn'; } get apiBaseUrl() { return 'https://api.linkedin.com/v2'; } /** * Axios instance for making HTTP requests */ _axiosInstance = null; /** * Get or create axios instance with interceptors */ get axiosInstance() { if (!this._axiosInstance) { this._axiosInstance = axios_1.default.create({ baseURL: this.apiBaseUrl, timeout: 30000, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Restli-Protocol-Version': '2.0.0' // LinkedIn specific header } }); // 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) => Promise.reject(error)); // Add response interceptor for rate limit handling this._axiosInstance.interceptors.response.use((response) => { // Log rate limit info const rateLimitInfo = this.parseRateLimitHeaders(response.headers); if (rateLimitInfo) { (0, core_1.LogStatus)(`LinkedIn Rate Limit - Remaining: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`); } return response; }, async (error) => { if (error.response?.status === 429) { // Rate limit exceeded const retryAfter = error.response.headers['retry-after']; const waitTime = retryAfter ? parseInt(retryAfter) : 60; await this.handleRateLimit(waitTime); // Retry the request return this._axiosInstance.request(error.config); } return Promise.reject(error); }); } return this._axiosInstance; } /** * Refresh the access token using the refresh token */ async refreshAccessToken() { const refreshToken = this.getRefreshToken(); if (!refreshToken) { throw new Error('No refresh token available for LinkedIn'); } try { const response = await axios_1.default.post('https://www.linkedin.com/oauth/v2/accessToken', new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: this.getCustomAttribute(2) || '', // Client ID stored in CustomAttribute2 client_secret: this.getCustomAttribute(3) || '' // Client Secret stored in CustomAttribute3 }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); const { access_token, refresh_token: newRefreshToken, expires_in } = response.data; // Update stored tokens await this.updateStoredTokens(access_token, newRefreshToken || refreshToken, expires_in); (0, core_1.LogStatus)('LinkedIn access token refreshed successfully'); } catch (error) { (0, core_1.LogError)(`Failed to refresh LinkedIn access token: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } } /** * Get the authenticated user's profile URN */ async getCurrentUserUrn() { try { const response = await this.axiosInstance.get('/me'); return `urn:li:person:${response.data.id}`; } catch (error) { (0, core_1.LogError)(`Failed to get current user URN: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } } /** * Get organizations the user has admin access to */ async getAdminOrganizations() { try { const response = await this.axiosInstance.get('/organizationalEntityAcls', { params: { q: 'roleAssignee', role: 'ADMINISTRATOR', projection: '(elements*(*,organizationalTarget~(localizedName)))' } }); const organizations = []; if (response.data.elements) { for (const element of response.data.elements) { if (element.organizationalTarget) { organizations.push({ urn: element.organizationalTarget, name: element['organizationalTarget~']?.localizedName || 'Unknown', id: element.organizationalTarget.split(':').pop() || '' }); } } } return organizations; } catch (error) { (0, core_1.LogError)(`Failed to get admin organizations: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } } /** * Upload media to LinkedIn */ async uploadSingleMedia(file) { try { // Step 1: Register upload const registerResponse = await this.axiosInstance.post('/assets?action=registerUpload', { registerUploadRequest: { recipes: ['urn:li:digitalmediaRecipe:feedshare-image'], owner: await this.getCurrentUserUrn(), serviceRelationships: [{ relationshipType: 'OWNER', identifier: 'urn:li:userGeneratedContent' }] } }); const uploadUrl = registerResponse.data.value.uploadMechanism['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest'].uploadUrl; const asset = registerResponse.data.value.asset; // Step 2: Upload the file const fileData = typeof file.data === 'string' ? Buffer.from(file.data, 'base64') : file.data; await axios_1.default.put(uploadUrl, fileData, { headers: { 'Authorization': `Bearer ${this.getAccessToken()}`, 'Content-Type': file.mimeType } }); // Return the asset URN return asset; } catch (error) { (0, core_1.LogError)(`Failed to upload media to LinkedIn: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } } /** * Validate media file meets LinkedIn requirements */ validateMediaFile(file) { const supportedTypes = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ]; if (!supportedTypes.includes(file.mimeType)) { throw new Error(`Unsupported media type: ${file.mimeType}. Supported types: ${supportedTypes.join(', ')}`); } // LinkedIn image size limits const maxSize = 10 * 1024 * 1024; // 10MB if (file.size > maxSize) { throw new Error(`File size exceeds limit. Max: ${maxSize / 1024 / 1024}MB, Got: ${file.size / 1024 / 1024}MB`); } } /** * Create a share (post) on LinkedIn */ async createShare(shareData) { try { const response = await this.axiosInstance.post('/ugcPosts', shareData); return response.data.id; } catch (error) { this.handleLinkedInError(error); } } /** * Get shares for a specific author (person or organization) */ async getShares(authorUrn, count = 50, start = 0) { try { const response = await this.axiosInstance.get('/ugcPosts', { params: { q: 'authors', authors: `List(${authorUrn})`, count: count, start: start } }); return response.data.elements || []; } catch (error) { (0, core_1.LogError)(`Failed to get shares: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } } /** * Convert LinkedIn share to common format */ normalizePost(linkedInShare) { const publishedAt = new Date(linkedInShare.firstPublishedAt || linkedInShare.created.time); // Extract media URLs const mediaUrls = []; if (linkedInShare.specificContent?.['com.linkedin.ugc.ShareContent']?.media) { for (const media of linkedInShare.specificContent['com.linkedin.ugc.ShareContent'].media) { if (media.media) { mediaUrls.push(media.media); } } } return { id: linkedInShare.id, platform: 'LinkedIn', profileId: linkedInShare.author, content: linkedInShare.specificContent?.['com.linkedin.ugc.ShareContent']?.shareCommentary?.text || '', mediaUrls: mediaUrls, publishedAt: publishedAt, platformSpecificData: { lifecycleState: linkedInShare.lifecycleState, visibility: linkedInShare.visibility, distribution: linkedInShare.distribution } }; } /** * Normalize LinkedIn analytics to common format */ normalizeAnalytics(linkedInAnalytics) { return { impressions: linkedInAnalytics.totalShareStatistics?.impressionCount || 0, engagements: linkedInAnalytics.totalShareStatistics?.engagement || 0, clicks: linkedInAnalytics.totalShareStatistics?.clickCount || 0, shares: linkedInAnalytics.totalShareStatistics?.shareCount || 0, comments: linkedInAnalytics.totalShareStatistics?.commentCount || 0, likes: linkedInAnalytics.totalShareStatistics?.likeCount || 0, reach: linkedInAnalytics.totalShareStatistics?.uniqueImpressionsCount || 0, platformMetrics: linkedInAnalytics }; } /** * Search for posts - implemented in search action */ async searchPosts(params) { // This is implemented in the search-posts.action.ts throw new Error('Search posts is implemented in LinkedInSearchPostsAction'); } /** * Handle LinkedIn-specific errors */ handleLinkedInError(error) { if (error.response) { const { status, data } = error.response; const errorData = data; switch (status) { case 400: throw new Error(`Bad Request: ${errorData.message || 'Invalid request parameters'}`); case 401: throw new Error('Unauthorized: Invalid or expired access token'); case 403: throw new Error('Forbidden: Insufficient permissions. Ensure the app has required LinkedIn scopes.'); case 404: throw new Error('Not Found: Resource does not exist'); case 422: throw new Error(`Unprocessable Entity: ${errorData.message || 'Invalid data provided'}`); case 429: throw new Error('Rate Limit Exceeded: Too many requests'); case 500: throw new Error('Internal Server Error: LinkedIn service error'); default: throw new Error(`LinkedIn API Error (${status}): ${errorData.message || 'Unknown error'}`); } } else if (error.request) { throw new Error('Network Error: No response from LinkedIn'); } else { throw new Error(`Request Error: ${error.message}`); } } /** * Parse LinkedIn-specific rate limit headers */ parseRateLimitHeaders(headers) { // LinkedIn uses different header names const appRemaining = headers['x-app-rate-limit-remaining']; const appLimit = headers['x-app-rate-limit-limit']; const memberRemaining = headers['x-member-rate-limit-remaining']; const memberLimit = headers['x-member-rate-limit-limit']; // Use the more restrictive limit const remaining = Math.min(appRemaining ? parseInt(appRemaining) : Infinity, memberRemaining ? parseInt(memberRemaining) : Infinity); const limit = Math.min(appLimit ? parseInt(appLimit) : Infinity, memberLimit ? parseInt(memberLimit) : Infinity); if (remaining !== Infinity && limit !== Infinity) { // LinkedIn resets rate limits at the top of each hour const now = new Date(); const reset = new Date(now); reset.setHours(reset.getHours() + 1, 0, 0, 0); return { remaining, reset, limit }; } return null; } }; exports.LinkedInBaseAction = LinkedInBaseAction; exports.LinkedInBaseAction = LinkedInBaseAction = __decorate([ (0, global_1.RegisterClass)(actions_1.BaseAction, 'LinkedInBaseAction') ], LinkedInBaseAction); //# sourceMappingURL=linkedin-base.action.js.map