UNPKG

@memberjunction/actions-bizapps-social

Version:

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

324 lines (285 loc) 11.6 kB
import { RegisterClass } from '@memberjunction/global'; import { LinkedInBaseAction, LinkedInShare } from '../linkedin-base.action'; import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base'; import { LogStatus, LogError } from '@memberjunction/core'; import { SocialPost, SearchParams } from '../../../base/base-social.action'; import { BaseAction } from '@memberjunction/actions'; /** * Action to search for historical posts on LinkedIn */ @RegisterClass(BaseAction, 'LinkedInSearchPostsAction') export class LinkedInSearchPostsAction extends LinkedInBaseAction { /** * Search for posts on LinkedIn with various filters */ protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> { const { Params, ContextUser } = params; try { // Initialize OAuth const companyIntegrationId = this.getParamValue(Params, 'CompanyIntegrationID'); if (!await this.initializeOAuth(companyIntegrationId, params)) { throw new Error('Failed to initialize OAuth connection'); } // Extract parameters const query = this.getParamValue(Params, 'Query'); const hashtags = this.getParamValue(Params, 'Hashtags'); const startDate = this.getParamValue(Params, 'StartDate'); const endDate = this.getParamValue(Params, 'EndDate'); const authorType = this.getParamValue(Params, 'AuthorType') || 'all'; // 'personal', 'organization', 'all' const organizationId = this.getParamValue(Params, 'OrganizationID'); const limit = this.getParamValue(Params, 'Limit') || 100; const offset = this.getParamValue(Params, 'Offset') || 0; const includeAnalytics = this.getParamValue(Params, 'IncludeAnalytics') === true; // Build search parameters const searchParams: SearchParams = { query: query, hashtags: hashtags, startDate: startDate ? new Date(startDate) : undefined, endDate: endDate ? new Date(endDate) : undefined, limit: limit, offset: offset }; // Perform the search const posts = await this.searchPostsImplementation( searchParams, authorType, organizationId, includeAnalytics ); LogStatus(`Found ${posts.length} posts matching search criteria`); // Update output parameters const outputParams = [...Params]; const postsParam = outputParams.find(p => p.Name === 'Posts'); if (postsParam) postsParam.Value = posts; const totalCountParam = outputParams.find(p => p.Name === 'TotalCount'); if (totalCountParam) totalCountParam.Value = posts.length; return { Success: true, ResultCode: 'SUCCESS', Message: `Successfully found ${posts.length} posts`, Params: outputParams }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { Success: false, ResultCode: 'ERROR', Message: `Failed to search posts: ${errorMessage}`, Params }; } } /** * Implementation of post search */ private async searchPostsImplementation( params: SearchParams, authorType: string, organizationId?: string, includeAnalytics: boolean = false ): Promise<SocialPost[]> { const allPosts: SocialPost[] = []; const authorsToSearch: string[] = []; // Determine which authors to search if (authorType === 'personal' || authorType === 'all') { const userUrn = await this.getCurrentUserUrn(); authorsToSearch.push(userUrn); } if (authorType === 'organization' || authorType === 'all') { if (organizationId) { authorsToSearch.push(`urn:li:organization:${organizationId}`); } else { // Get all admin organizations const orgs = await this.getAdminOrganizations(); authorsToSearch.push(...orgs.map(org => org.urn)); } } // Search posts for each author for (const authorUrn of authorsToSearch) { LogStatus(`Searching posts for author: ${authorUrn}`); // Get all posts for the author (LinkedIn doesn't support direct content search) const shares = await this.getAllSharesForAuthor(authorUrn, params.limit || 100); // Filter posts based on search criteria for (const share of shares) { if (this.matchesSearchCriteria(share, params)) { const post = this.normalizePost(share); // Optionally fetch analytics if (includeAnalytics && authorUrn.includes('organization')) { try { const analytics = await this.getPostAnalyticsForSearch(share.id, authorUrn); if (analytics) { post.analytics = this.normalizeAnalytics(analytics); } } catch (error) { LogError(`Failed to get analytics for post ${share.id}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } allPosts.push(post); } } } // Sort by date (newest first) allPosts.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime()); // Apply offset and limit const startIndex = params.offset || 0; const endIndex = startIndex + (params.limit || 100); return allPosts.slice(startIndex, endIndex); } /** * Get all shares for an author with pagination */ private async getAllSharesForAuthor(authorUrn: string, maxResults: number): Promise<LinkedInShare[]> { const allShares: LinkedInShare[] = []; let start = 0; const count = 50; // LinkedIn's typical page size while (allShares.length < maxResults) { try { const shares = await this.getShares(authorUrn, count, start); if (shares.length === 0) { break; // No more results } allShares.push(...shares); start += count; // Check if we've hit the API limit if (shares.length < count) { break; // Last page } } catch (error) { LogError(`Error fetching shares at offset ${start}: ${error instanceof Error ? error.message : 'Unknown error'}`); break; } } return allShares.slice(0, maxResults); } /** * Check if a share matches the search criteria */ private matchesSearchCriteria(share: LinkedInShare, params: SearchParams): boolean { const content = share.specificContent?.['com.linkedin.ugc.ShareContent']?.shareCommentary?.text || ''; const publishedDate = new Date(share.firstPublishedAt || share.created.time); // Check date range if (params.startDate && publishedDate < params.startDate) { return false; } if (params.endDate && publishedDate > params.endDate) { return false; } // Check query (case-insensitive) if (params.query) { const query = params.query.toLowerCase(); if (!content.toLowerCase().includes(query)) { return false; } } // Check hashtags if (params.hashtags && params.hashtags.length > 0) { const contentLower = content.toLowerCase(); const hasAllHashtags = params.hashtags.every(hashtag => { const normalizedHashtag = hashtag.toLowerCase().startsWith('#') ? hashtag.toLowerCase() : `#${hashtag.toLowerCase()}`; return contentLower.includes(normalizedHashtag); }); if (!hasAllHashtags) { return false; } } return true; } /** * Get analytics for a post during search */ private async getPostAnalyticsForSearch(shareId: string, organizationUrn: string): Promise<any> { try { const response = await this.axiosInstance.get('/organizationalEntityShareStatistics', { params: { q: 'organizationalEntity', organizationalEntity: organizationUrn, shares: `List(${shareId})` } }); if (response.data.elements && response.data.elements.length > 0) { return response.data.elements[0]; } return null; } catch (error) { // Silently fail for individual analytics requests during search return null; } } /** * Override base searchPosts method */ protected async searchPosts(params: SearchParams): Promise<SocialPost[]> { return this.searchPostsImplementation(params, 'all'); } /** * Define the parameters this action expects */ public get Params(): ActionParam[] { return [ ...this.commonSocialParams, { Name: 'Query', Type: 'Input', Value: null }, { Name: 'Hashtags', Type: 'Input', Value: null // Array of hashtags }, { Name: 'StartDate', Type: 'Input', Value: null }, { Name: 'EndDate', Type: 'Input', Value: null }, { Name: 'AuthorType', Type: 'Input', Value: 'all' // 'personal', 'organization', 'all' }, { Name: 'OrganizationID', Type: 'Input', Value: null }, { Name: 'Limit', Type: 'Input', Value: 100 }, { Name: 'Offset', Type: 'Input', Value: 0 }, { Name: 'IncludeAnalytics', Type: 'Input', Value: false }, { Name: 'Posts', Type: 'Output', Value: null }, { Name: 'TotalCount', Type: 'Output', Value: null } ]; } /** * Get action description */ public get Description(): string { return 'Searches for historical LinkedIn posts with support for content search, hashtags, date ranges, and author filtering. Retrieves posts from personal profiles and organization pages.'; } }