linkedin-secret-sauce 0.12.4 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/enrichment/matching.js +161 -81
  2. package/dist/enrichment/providers/bounceban.js +19 -10
  3. package/dist/enrichment/providers/construct.js +97 -89
  4. package/dist/enrichment/providers/cosiall.js +2 -2
  5. package/dist/enrichment/providers/hunter.js +2 -1
  6. package/dist/enrichment/providers/ldd.js +4 -2
  7. package/dist/enrichment/providers/smartprospect.d.ts +5 -2
  8. package/dist/enrichment/providers/smartprospect.js +64 -27
  9. package/dist/enrichment/providers/snovio.js +7 -3
  10. package/dist/enrichment/providers/trykitt.d.ts +2 -2
  11. package/dist/enrichment/providers/trykitt.js +86 -21
  12. package/dist/index.d.ts +1 -1
  13. package/dist/linkedin-api.d.ts +113 -1
  14. package/dist/linkedin-api.js +836 -3
  15. package/dist/parsers/profile-parser.js +1 -0
  16. package/dist/types.d.ts +164 -0
  17. package/docs/api/assets/hierarchy.js +1 -1
  18. package/docs/api/assets/navigation.js +1 -1
  19. package/docs/api/assets/search.js +1 -1
  20. package/docs/api/classes/LinkedInClientError.html +4 -4
  21. package/docs/api/functions/_testGetAccountCookies.html +2 -2
  22. package/docs/api/functions/_testGetAccountEntry.html +2 -2
  23. package/docs/api/functions/_testGetAllAccountIds.html +2 -2
  24. package/docs/api/functions/_testGetPoolState.html +2 -2
  25. package/docs/api/functions/adminResetAccount.html +1 -1
  26. package/docs/api/functions/adminSetCooldown.html +1 -1
  27. package/docs/api/functions/analyzeProfilePosts.html +14 -0
  28. package/docs/api/functions/buildCookieHeader.html +1 -1
  29. package/docs/api/functions/clearAllSmartLeadTokens.html +2 -2
  30. package/docs/api/functions/clearRequestHistory.html +1 -1
  31. package/docs/api/functions/clearSessionAccount.html +1 -1
  32. package/docs/api/functions/clearSmartLeadToken.html +2 -2
  33. package/docs/api/functions/createEnrichmentClient.html +2 -2
  34. package/docs/api/functions/extractCsrfToken.html +1 -1
  35. package/docs/api/functions/extractLinkedInHandle.html +2 -2
  36. package/docs/api/functions/fetchCookiesFromCosiall.html +2 -2
  37. package/docs/api/functions/fetchProfileEmailsFromCosiall.html +2 -2
  38. package/docs/api/functions/forceRefreshCookies.html +1 -1
  39. package/docs/api/functions/getAccountForSession.html +1 -1
  40. package/docs/api/functions/getAccountsSummary.html +1 -1
  41. package/docs/api/functions/getCompaniesBatch.html +2 -2
  42. package/docs/api/functions/getCompanyById.html +2 -2
  43. package/docs/api/functions/getCompanyByUrl.html +1 -1
  44. package/docs/api/functions/getConfig.html +1 -1
  45. package/docs/api/functions/getCookiePoolHealth.html +1 -1
  46. package/docs/api/functions/getPostComments.html +11 -0
  47. package/docs/api/functions/getPostHistory.html +12 -0
  48. package/docs/api/functions/getPostReactions.html +12 -0
  49. package/docs/api/functions/getProfileByUrn.html +2 -2
  50. package/docs/api/functions/getProfileByVanity.html +2 -2
  51. package/docs/api/functions/getProfilesBatch.html +1 -1
  52. package/docs/api/functions/getRequestHistory.html +1 -1
  53. package/docs/api/functions/getSalesNavigatorProfileDetails.html +1 -1
  54. package/docs/api/functions/getSalesNavigatorProfileFull.html +2 -2
  55. package/docs/api/functions/getSmartLeadToken.html +1 -1
  56. package/docs/api/functions/getSmartLeadTokenCacheStats.html +2 -2
  57. package/docs/api/functions/getSmartLeadUser.html +2 -2
  58. package/docs/api/functions/getSnapshot.html +1 -1
  59. package/docs/api/functions/getYearsAtCompanyOptions.html +2 -2
  60. package/docs/api/functions/getYearsInPositionOptions.html +2 -2
  61. package/docs/api/functions/getYearsOfExperienceOptions.html +2 -2
  62. package/docs/api/functions/incrementMetric.html +1 -1
  63. package/docs/api/functions/initializeCookiePool.html +1 -1
  64. package/docs/api/functions/initializeLinkedInClient.html +1 -1
  65. package/docs/api/functions/isBusinessEmail.html +2 -2
  66. package/docs/api/functions/isDisposableDomain.html +2 -2
  67. package/docs/api/functions/isDisposableEmail.html +2 -2
  68. package/docs/api/functions/isPersonalDomain.html +2 -2
  69. package/docs/api/functions/isPersonalEmail.html +2 -2
  70. package/docs/api/functions/isRoleAccount.html +2 -2
  71. package/docs/api/functions/isValidEmailSyntax.html +2 -2
  72. package/docs/api/functions/parseFullProfile.html +2 -2
  73. package/docs/api/functions/parseSalesSearchResults.html +1 -1
  74. package/docs/api/functions/reportAccountFailure.html +1 -1
  75. package/docs/api/functions/reportAccountSuccess.html +1 -1
  76. package/docs/api/functions/resolveCompanyUniversalName.html +1 -1
  77. package/docs/api/functions/searchSalesLeads.html +2 -2
  78. package/docs/api/functions/selectAccountForRequest.html +1 -1
  79. package/docs/api/functions/setAccountForSession.html +1 -1
  80. package/docs/api/functions/typeahead.html +1 -1
  81. package/docs/api/functions/verifyEmailMx.html +1 -1
  82. package/docs/api/hierarchy.html +1 -1
  83. package/docs/api/index.html +2 -2
  84. package/docs/api/interfaces/AccountCookies.html +2 -2
  85. package/docs/api/interfaces/AnalyzedPost.html +8 -0
  86. package/docs/api/interfaces/BatchEnrichmentOptions.html +8 -8
  87. package/docs/api/interfaces/CacheAdapter.html +4 -4
  88. package/docs/api/interfaces/CanonicalEmail.html +8 -8
  89. package/docs/api/interfaces/CommentAuthor.html +8 -0
  90. package/docs/api/interfaces/Company.html +2 -2
  91. package/docs/api/interfaces/ConstructConfig.html +5 -5
  92. package/docs/api/interfaces/CosiallProfileEmailsResponse.html +6 -6
  93. package/docs/api/interfaces/EnrichmentCandidate.html +4 -4
  94. package/docs/api/interfaces/EnrichmentClient.html +6 -6
  95. package/docs/api/interfaces/EnrichmentClientConfig.html +7 -7
  96. package/docs/api/interfaces/EnrichmentLogger.html +3 -3
  97. package/docs/api/interfaces/EnrichmentOptions.html +6 -6
  98. package/docs/api/interfaces/HunterConfig.html +3 -3
  99. package/docs/api/interfaces/LddConfig.html +3 -3
  100. package/docs/api/interfaces/LddProfileData.html +2 -2
  101. package/docs/api/interfaces/LinkedInClientConfig.html +2 -2
  102. package/docs/api/interfaces/LinkedInCookie.html +2 -2
  103. package/docs/api/interfaces/LinkedInPosition.html +2 -2
  104. package/docs/api/interfaces/LinkedInProfile.html +2 -2
  105. package/docs/api/interfaces/LinkedInSpotlightBadge.html +2 -2
  106. package/docs/api/interfaces/LinkedInTenure.html +2 -2
  107. package/docs/api/interfaces/Metrics.html +2 -2
  108. package/docs/api/interfaces/MetricsSnapshot.html +2 -2
  109. package/docs/api/interfaces/PostAnalytics.html +11 -0
  110. package/docs/api/interfaces/PostComment.html +8 -0
  111. package/docs/api/interfaces/PostCommentsResult.html +7 -0
  112. package/docs/api/interfaces/PostHistoryResult.html +4 -0
  113. package/docs/api/interfaces/PostReaction.html +5 -0
  114. package/docs/api/interfaces/PostReactionsResult.html +5 -0
  115. package/docs/api/interfaces/ProfileAnalysisOptions.html +14 -0
  116. package/docs/api/interfaces/ProfileAnalysisResult.html +9 -0
  117. package/docs/api/interfaces/ProfileEducation.html +2 -2
  118. package/docs/api/interfaces/ProfileEmailsLookupOptions.html +5 -5
  119. package/docs/api/interfaces/ProfilePosition.html +2 -2
  120. package/docs/api/interfaces/ProfilePost.html +14 -0
  121. package/docs/api/interfaces/ProfileSkill.html +2 -2
  122. package/docs/api/interfaces/ProviderResult.html +6 -6
  123. package/docs/api/interfaces/ProvidersConfig.html +6 -6
  124. package/docs/api/interfaces/ReactionActor.html +9 -0
  125. package/docs/api/interfaces/RequestHistoryEntry.html +2 -2
  126. package/docs/api/interfaces/SalesLeadSearchResult.html +2 -2
  127. package/docs/api/interfaces/SalesNavigatorContactInfo.html +2 -2
  128. package/docs/api/interfaces/SalesNavigatorPosition.html +2 -2
  129. package/docs/api/interfaces/SalesNavigatorProfile.html +2 -2
  130. package/docs/api/interfaces/SalesNavigatorProfileFull.html +4 -4
  131. package/docs/api/interfaces/SearchSalesResult.html +2 -2
  132. package/docs/api/interfaces/SmartLeadAuthConfig.html +4 -4
  133. package/docs/api/interfaces/SmartLeadCredentials.html +2 -2
  134. package/docs/api/interfaces/SmartLeadLoginResponse.html +2 -2
  135. package/docs/api/interfaces/SmartLeadUser.html +2 -2
  136. package/docs/api/interfaces/SmartProspectConfig.html +8 -8
  137. package/docs/api/interfaces/SmartProspectContact.html +2 -2
  138. package/docs/api/interfaces/SmartProspectSearchFilters.html +21 -21
  139. package/docs/api/interfaces/TypeaheadItem.html +2 -2
  140. package/docs/api/interfaces/TypeaheadResult.html +2 -2
  141. package/docs/api/interfaces/VerificationResult.html +9 -9
  142. package/docs/api/types/CostCallback.html +2 -2
  143. package/docs/api/types/Geo.html +2 -2
  144. package/docs/api/types/LddApiResponse.html +1 -1
  145. package/docs/api/types/LinkedInReactionType.html +2 -0
  146. package/docs/api/types/ProviderFunc.html +2 -2
  147. package/docs/api/types/ProviderName.html +2 -2
  148. package/docs/api/types/SalesSearchFilters.html +2 -2
  149. package/docs/api/types/TypeaheadType.html +1 -1
  150. package/docs/api/variables/COMPANY_SIZE_OPTIONS.html +1 -1
  151. package/docs/api/variables/DEFAULT_PROVIDER_ORDER.html +2 -2
  152. package/docs/api/variables/DISPOSABLE_DOMAINS.html +2 -2
  153. package/docs/api/variables/FUNCTION_OPTIONS.html +1 -1
  154. package/docs/api/variables/INDUSTRY_OPTIONS.html +1 -1
  155. package/docs/api/variables/LANGUAGE_OPTIONS.html +1 -1
  156. package/docs/api/variables/PERSONAL_DOMAINS.html +2 -2
  157. package/docs/api/variables/PROVIDER_COSTS.html +2 -2
  158. package/docs/api/variables/REGION_OPTIONS.html +1 -1
  159. package/docs/api/variables/SENIORITY_OPTIONS.html +2 -2
  160. package/docs/api/variables/YEARS_OPTIONS.html +1 -1
  161. package/package.json +1 -1
@@ -37,6 +37,7 @@ exports.COMPANY_SIZE_OPTIONS = exports.INDUSTRY_OPTIONS = exports.LANGUAGE_OPTIO
37
37
  exports.getProfileByVanity = getProfileByVanity;
38
38
  exports.getProfileByUrn = getProfileByUrn;
39
39
  exports.searchSalesLeads = searchSalesLeads;
40
+ exports.analyzeProfilePosts = analyzeProfilePosts;
40
41
  exports.getProfilesBatch = getProfilesBatch;
41
42
  exports.resolveCompanyUniversalName = resolveCompanyUniversalName;
42
43
  exports.getCompanyById = getCompanyById;
@@ -49,6 +50,9 @@ exports.getYearsOfExperienceOptions = getYearsOfExperienceOptions;
49
50
  exports.getSalesNavigatorProfileDetails = getSalesNavigatorProfileDetails;
50
51
  exports.getSalesNavigatorProfileFull = getSalesNavigatorProfileFull;
51
52
  exports.extractLinkedInHandle = extractLinkedInHandle;
53
+ exports.getPostReactions = getPostReactions;
54
+ exports.getPostComments = getPostComments;
55
+ exports.getPostHistory = getPostHistory;
52
56
  const config_1 = require("./config");
53
57
  const http_client_1 = require("./http-client");
54
58
  const profile_parser_1 = require("./parsers/profile-parser");
@@ -93,7 +97,10 @@ const inflightByVanity = new Map();
93
97
  */
94
98
  async function getProfileByVanity(vanity) {
95
99
  const cfg = (0, config_1.getConfig)();
96
- const key = String(vanity || "").toLowerCase();
100
+ // Extract vanity from LinkedIn URL if full URL is provided
101
+ const input = String(vanity || "").trim();
102
+ const urlMatch = input.match(/linkedin\.com\/in\/([^/?#]+)/i);
103
+ const key = (urlMatch ? decodeURIComponent(urlMatch[1]) : input).toLowerCase();
97
104
  try {
98
105
  (0, logger_1.log)("info", "api.start", {
99
106
  operation: "getProfileByVanity",
@@ -113,7 +120,7 @@ async function getProfileByVanity(vanity) {
113
120
  (0, metrics_1.incrementMetric)("inflightDedupeHits");
114
121
  return inflight;
115
122
  }
116
- const url = `${LINKEDIN_API_BASE}/identity/dash/profiles?q=memberIdentity&memberIdentity=${encodeURIComponent(vanity)}&decorationId=com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities-35`;
123
+ const url = `${LINKEDIN_API_BASE}/identity/dash/profiles?q=memberIdentity&memberIdentity=${encodeURIComponent(key)}&decorationId=com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities-35`;
117
124
  const p = (async () => {
118
125
  try {
119
126
  let raw;
@@ -137,7 +144,7 @@ async function getProfileByVanity(vanity) {
137
144
  }
138
145
  let prof;
139
146
  try {
140
- prof = (0, profile_parser_1.parseFullProfile)(raw, vanity);
147
+ prof = (0, profile_parser_1.parseFullProfile)(raw, key);
141
148
  }
142
149
  catch {
143
150
  try {
@@ -468,6 +475,199 @@ async function searchSalesLeads(keywords, options) {
468
475
  (0, metrics_1.incrementMetric)("searchCacheMisses");
469
476
  return result;
470
477
  }
478
+ // ---------------------------------------------------------------------------
479
+ // High-Level Convenience API
480
+ // ---------------------------------------------------------------------------
481
+ /**
482
+ * Analyzes a LinkedIn profile's posts with engagement data (reactions and comments).
483
+ * This is a high-level convenience function that orchestrates multiple API calls:
484
+ * 1. Fetches the profile by vanity URL or URN
485
+ * 2. Retrieves the profile's post history
486
+ * 3. Fetches reactions and comments for each post
487
+ *
488
+ * @param profileIdentifier - LinkedIn vanity URL, full profile URL, or FSD profile URN
489
+ * @param options - Analysis options for controlling fetch behavior
490
+ * @returns Complete analysis result with profile, posts, engagement data, and summary statistics
491
+ *
492
+ * @example
493
+ * ```typescript
494
+ * // Analyze by vanity URL
495
+ * const analysis = await analyzeProfilePosts('john-doe');
496
+ * console.log(analysis.summary.totalReactions);
497
+ * console.log(analysis.summary.avgCommentsPerPost);
498
+ *
499
+ * // Analyze by full LinkedIn URL
500
+ * const analysis = await analyzeProfilePosts('https://www.linkedin.com/in/john-doe');
501
+ *
502
+ * // Analyze with options
503
+ * const analysis = await analyzeProfilePosts('john-doe', {
504
+ * postsCount: 20,
505
+ * reactionsPerPost: 50,
506
+ * commentsPerPost: 25,
507
+ * concurrency: 5,
508
+ * });
509
+ *
510
+ * // Access individual post engagement
511
+ * for (const analyzed of analysis.analyzedPosts) {
512
+ * console.log(`Post: ${analyzed.post.text?.substring(0, 50)}...`);
513
+ * console.log(` Reactions: ${analyzed.reactions.totalCount}`);
514
+ * console.log(` Comments: ${analyzed.comments.totalCount}`);
515
+ * }
516
+ * ```
517
+ */
518
+ async function analyzeProfilePosts(profileIdentifier, options) {
519
+ const postsCount = Math.min(options?.postsCount ?? 10, 100);
520
+ const reactionsPerPost = Math.min(options?.reactionsPerPost ?? 10, 100);
521
+ const commentsPerPost = Math.min(options?.commentsPerPost ?? 10, 100);
522
+ const concurrency = Math.max(1, Math.min(options?.concurrency ?? 3, 10));
523
+ const skipReactions = options?.skipReactions ?? false;
524
+ const skipComments = options?.skipComments ?? false;
525
+ try {
526
+ (0, logger_1.log)("info", "api.start", {
527
+ operation: "analyzeProfilePosts",
528
+ selector: profileIdentifier,
529
+ options: { postsCount, reactionsPerPost, commentsPerPost, concurrency },
530
+ });
531
+ }
532
+ catch { }
533
+ // Step 1: Fetch profile
534
+ const input = String(profileIdentifier || "").trim();
535
+ let profile;
536
+ // Determine if it's a URN or vanity
537
+ const isUrn = input.startsWith("urn:li:") || /^[A-Za-z0-9_-]{20,}$/.test(input);
538
+ const isUrl = input.includes("linkedin.com/in/");
539
+ if (isUrn && !isUrl) {
540
+ profile = await getProfileByUrn(input);
541
+ }
542
+ else {
543
+ // Extract vanity from URL or use as-is
544
+ profile = await getProfileByVanity(input);
545
+ }
546
+ // Step 2: Fetch post history
547
+ const fsdKey = profile.fsdProfileUrn?.replace("urn:li:fsd_profile:", "") ||
548
+ profile.objectUrn?.replace("urn:li:member:", "");
549
+ if (!fsdKey) {
550
+ throw new errors_1.LinkedInClientError("Could not extract profile key for post history", errors_1.ERROR_CODES.PARSE_ERROR, 500);
551
+ }
552
+ const postHistory = await getPostHistory(fsdKey, { count: postsCount });
553
+ // Step 3: Fetch engagement data for each post with controlled concurrency
554
+ const analyzedPosts = [];
555
+ const postsToAnalyze = postHistory.posts.filter((p) => p.activityUrn);
556
+ // Worker function for concurrent processing
557
+ let idx = 0;
558
+ async function worker() {
559
+ while (idx < postsToAnalyze.length) {
560
+ const myIdx = idx++;
561
+ const post = postsToAnalyze[myIdx];
562
+ if (!post.activityUrn)
563
+ continue;
564
+ let reactions = {
565
+ reactions: [],
566
+ totalCount: 0,
567
+ page: { start: 0, count: 0, total: 0, hasMore: false },
568
+ };
569
+ let comments = {
570
+ comments: [],
571
+ totalCount: 0,
572
+ page: { start: 0, count: 0, total: 0, hasMore: false },
573
+ };
574
+ // Fetch reactions if not skipped
575
+ if (!skipReactions) {
576
+ try {
577
+ reactions = await getPostReactions(post.activityUrn, {
578
+ count: reactionsPerPost,
579
+ });
580
+ }
581
+ catch (e) {
582
+ try {
583
+ (0, logger_1.log)("warn", "api.analyzeProfilePosts.reactionsFailed", {
584
+ activityUrn: post.activityUrn,
585
+ error: e.message,
586
+ });
587
+ }
588
+ catch { }
589
+ }
590
+ }
591
+ // Fetch comments if not skipped
592
+ if (!skipComments) {
593
+ try {
594
+ comments = await getPostComments(post.activityUrn, {
595
+ commentsCount: commentsPerPost,
596
+ });
597
+ }
598
+ catch (e) {
599
+ try {
600
+ (0, logger_1.log)("warn", "api.analyzeProfilePosts.commentsFailed", {
601
+ activityUrn: post.activityUrn,
602
+ error: e.message,
603
+ });
604
+ }
605
+ catch { }
606
+ }
607
+ }
608
+ analyzedPosts[myIdx] = { post, reactions, comments };
609
+ }
610
+ }
611
+ // Run workers concurrently
612
+ const workers = Array.from({ length: Math.min(concurrency, postsToAnalyze.length) }, () => worker());
613
+ await Promise.all(workers);
614
+ // Filter out any undefined entries (shouldn't happen, but be safe)
615
+ const validAnalyzedPosts = analyzedPosts.filter((p) => p !== undefined);
616
+ // Step 4: Calculate summary statistics
617
+ let totalReactions = 0;
618
+ let totalComments = 0;
619
+ const reactionsByType = {
620
+ LIKE: 0,
621
+ CELEBRATE: 0,
622
+ SUPPORT: 0,
623
+ LOVE: 0,
624
+ INSIGHTFUL: 0,
625
+ FUNNY: 0,
626
+ INTEREST: 0,
627
+ APPRECIATION: 0,
628
+ PRAISE: 0,
629
+ EMPATHY: 0,
630
+ ENTERTAINMENT: 0,
631
+ };
632
+ for (const analyzed of validAnalyzedPosts) {
633
+ totalReactions += analyzed.reactions.totalCount;
634
+ totalComments += analyzed.comments.totalCount;
635
+ for (const reaction of analyzed.reactions.reactions) {
636
+ if (reaction.reactionType) {
637
+ reactionsByType[reaction.reactionType] =
638
+ (reactionsByType[reaction.reactionType] || 0) + 1;
639
+ }
640
+ }
641
+ }
642
+ const totalPosts = validAnalyzedPosts.length;
643
+ const avgReactionsPerPost = totalPosts > 0 ? totalReactions / totalPosts : 0;
644
+ const avgCommentsPerPost = totalPosts > 0 ? totalComments / totalPosts : 0;
645
+ const result = {
646
+ profile,
647
+ analyzedPosts: validAnalyzedPosts,
648
+ summary: {
649
+ totalPosts,
650
+ totalReactions,
651
+ totalComments,
652
+ avgReactionsPerPost: Math.round(avgReactionsPerPost * 100) / 100,
653
+ avgCommentsPerPost: Math.round(avgCommentsPerPost * 100) / 100,
654
+ reactionsByType,
655
+ },
656
+ };
657
+ try {
658
+ (0, logger_1.log)("info", "api.ok", {
659
+ operation: "analyzeProfilePosts",
660
+ selector: profileIdentifier,
661
+ summary: {
662
+ totalPosts,
663
+ totalReactions,
664
+ totalComments,
665
+ },
666
+ });
667
+ }
668
+ catch { }
669
+ return result;
670
+ }
471
671
  async function getProfilesBatch(vanities, concurrency = 4) {
472
672
  const limit = Math.max(1, Math.min(concurrency || 1, 16));
473
673
  const results = Array.from({ length: vanities.length }, () => null);
@@ -1053,3 +1253,636 @@ function extractLinkedInHandle(flagshipProfileUrl) {
1053
1253
  const match = flagshipProfileUrl.match(/linkedin\.com\/in\/([^\/\?]+)/i);
1054
1254
  return match ? match[1] : null;
1055
1255
  }
1256
+ // ---------------------------------------------------------------------------
1257
+ // Post Engagement APIs (Voyager GraphQL)
1258
+ // ---------------------------------------------------------------------------
1259
+ // GraphQL query IDs for post engagement endpoints (discovered via network inspection)
1260
+ const REACTIONS_QUERY_ID = "voyagerSocialDashReactions.41ebf31a9f4c4a84e35a49d5abc9010b";
1261
+ // Comments use voyagerFeedDashUpdates endpoint (not voyagerSocialDashComments which returns 400)
1262
+ const FEED_UPDATES_QUERY_ID = "voyagerFeedDashUpdates.ca43379417f0bcc4a7e2031d6c063250";
1263
+ /**
1264
+ * Parses LinkedIn relative time strings into approximate Unix timestamps.
1265
+ * LinkedIn returns times like "2d", "1w", "3mo", "1yr" instead of absolute timestamps.
1266
+ *
1267
+ * @param relativeText - Relative time string (e.g., "2d", "1w", "3mo", "2 days ago")
1268
+ * @returns Unix timestamp in milliseconds, or undefined if parsing fails
1269
+ */
1270
+ function parseRelativeTime(relativeText) {
1271
+ if (!relativeText)
1272
+ return undefined;
1273
+ const now = Date.now();
1274
+ const text = relativeText.toLowerCase().trim();
1275
+ // Match patterns like "2d", "1w", "3mo", "1yr", "2 days ago", "1 week ago", etc.
1276
+ // Also handles "2d •" format (with trailing bullet)
1277
+ const match = text.match(/^(\d+)\s*(s|m|h|d|w|mo|yr|sec|min|hour|day|week|month|year)s?\b/i);
1278
+ if (!match) {
1279
+ // Try "X time ago" format
1280
+ const agoMatch = text.match(/(\d+)\s*(second|minute|hour|day|week|month|year)s?\s*ago/i);
1281
+ if (!agoMatch)
1282
+ return undefined;
1283
+ const [, numStr, unit] = agoMatch;
1284
+ const num = parseInt(numStr, 10);
1285
+ if (isNaN(num))
1286
+ return undefined;
1287
+ const msPerUnit = {
1288
+ second: 1000,
1289
+ minute: 60 * 1000,
1290
+ hour: 60 * 60 * 1000,
1291
+ day: 24 * 60 * 60 * 1000,
1292
+ week: 7 * 24 * 60 * 60 * 1000,
1293
+ month: 30 * 24 * 60 * 60 * 1000,
1294
+ year: 365 * 24 * 60 * 60 * 1000,
1295
+ };
1296
+ const unitKey = unit.toLowerCase();
1297
+ const ms = msPerUnit[unitKey];
1298
+ if (!ms)
1299
+ return undefined;
1300
+ return now - num * ms;
1301
+ }
1302
+ const [, numStr, unit] = match;
1303
+ const num = parseInt(numStr, 10);
1304
+ if (isNaN(num))
1305
+ return undefined;
1306
+ // Map short units to milliseconds
1307
+ const msPerUnit = {
1308
+ s: 1000,
1309
+ sec: 1000,
1310
+ m: 60 * 1000,
1311
+ min: 60 * 1000,
1312
+ h: 60 * 60 * 1000,
1313
+ hour: 60 * 60 * 1000,
1314
+ d: 24 * 60 * 60 * 1000,
1315
+ day: 24 * 60 * 60 * 1000,
1316
+ w: 7 * 24 * 60 * 60 * 1000,
1317
+ week: 7 * 24 * 60 * 60 * 1000,
1318
+ mo: 30 * 24 * 60 * 60 * 1000,
1319
+ month: 30 * 24 * 60 * 60 * 1000,
1320
+ yr: 365 * 24 * 60 * 60 * 1000,
1321
+ year: 365 * 24 * 60 * 60 * 1000,
1322
+ };
1323
+ const unitKey = unit.toLowerCase();
1324
+ const ms = msPerUnit[unitKey];
1325
+ if (!ms)
1326
+ return undefined;
1327
+ return now - num * ms;
1328
+ }
1329
+ /**
1330
+ * Extracts the activity ID from various LinkedIn URN formats.
1331
+ * @param activityUrnOrId - Activity URN (urn:li:activity:xxx) or bare ID
1332
+ * @returns The numeric activity ID
1333
+ */
1334
+ function extractActivityId(activityUrnOrId) {
1335
+ const input = String(activityUrnOrId || "").trim();
1336
+ // Match urn:li:activity:1234567890
1337
+ const urnMatch = input.match(/^urn:li:activity:(\d+)$/i);
1338
+ if (urnMatch)
1339
+ return urnMatch[1];
1340
+ // Match bare numeric ID
1341
+ if (/^\d+$/.test(input))
1342
+ return input;
1343
+ throw new errors_1.LinkedInClientError("Invalid activity URN or ID format", errors_1.ERROR_CODES.INVALID_INPUT, 400);
1344
+ }
1345
+ /**
1346
+ * Fetches reactions (likes, celebrates, etc.) for a LinkedIn post.
1347
+ * Uses the Voyager GraphQL API with pagination support.
1348
+ *
1349
+ * @param activityUrn - The activity URN (e.g., "urn:li:activity:7419061111293083648") or bare ID
1350
+ * @param options - Pagination options
1351
+ * @param options.start - Pagination offset (default: 0)
1352
+ * @param options.count - Number of reactions to fetch (default: 10, max: 100)
1353
+ * @returns PostReactionsResult with reactions array and pagination info
1354
+ * @throws LinkedInClientError with code NOT_FOUND if post doesn't exist
1355
+ *
1356
+ * @example
1357
+ * ```typescript
1358
+ * // Get first 10 reactions
1359
+ * const reactions = await getPostReactions('urn:li:activity:7419061111293083648');
1360
+ * console.log(reactions.totalCount); // 37
1361
+ * console.log(reactions.reactions[0].reactionType); // "LIKE"
1362
+ *
1363
+ * // Paginate through all reactions
1364
+ * const page2 = await getPostReactions('7419061111293083648', { start: 10, count: 10 });
1365
+ * ```
1366
+ */
1367
+ async function getPostReactions(activityUrn, options) {
1368
+ const activityId = extractActivityId(activityUrn);
1369
+ const start = Number.isFinite(options?.start) ? Number(options.start) : 0;
1370
+ const count = Math.min(Number.isFinite(options?.count) ? Number(options.count) : 10, 100);
1371
+ const threadUrn = `urn:li:activity:${activityId}`;
1372
+ const variables = `(count:${count},start:${start},threadUrn:${encodeURIComponent(threadUrn)})`;
1373
+ const url = `${LINKEDIN_API_BASE}/graphql?includeWebMetadata=true&variables=${variables}&queryId=${REACTIONS_QUERY_ID}`;
1374
+ try {
1375
+ (0, logger_1.log)("info", "api.start", {
1376
+ operation: "getPostReactions",
1377
+ selector: { activityId, start, count },
1378
+ });
1379
+ }
1380
+ catch { }
1381
+ let raw;
1382
+ try {
1383
+ raw = await (0, http_client_1.executeLinkedInRequest)({ url }, "getPostReactions");
1384
+ }
1385
+ catch (e) {
1386
+ const status = e?.status ?? 0;
1387
+ if (status === 404) {
1388
+ try {
1389
+ (0, logger_1.log)("warn", "api.notFound", {
1390
+ operation: "getPostReactions",
1391
+ selector: activityId,
1392
+ status,
1393
+ });
1394
+ }
1395
+ catch { }
1396
+ throw new errors_1.LinkedInClientError("Post not found", errors_1.ERROR_CODES.NOT_FOUND, 404);
1397
+ }
1398
+ throw e;
1399
+ }
1400
+ // Parse the GraphQL response
1401
+ // Response structure: { data: { data: { socialDashReactionsByReactionType: { paging, *elements } } } }
1402
+ const rr = raw;
1403
+ const outerData = rr?.data;
1404
+ const innerData = outerData?.data;
1405
+ const reactionsData = innerData?.socialDashReactionsByReactionType;
1406
+ // Extract paging info
1407
+ const paging = reactionsData?.paging;
1408
+ const total = paging?.total ?? 0;
1409
+ // Extract reactions from *elements (note the asterisk prefix)
1410
+ // Format: "urn:li:fsd_reaction:(urn:li:fsd_profile:ABC,urn:li:activity:123,0)"
1411
+ // The last number (0) is the reaction type: 0=LIKE, 1=CELEBRATE, 2=SUPPORT, 3=LOVE, 4=INSIGHTFUL, 5=FUNNY
1412
+ const elementsRefs = reactionsData?.["*elements"] || [];
1413
+ const reactions = [];
1414
+ const reactionTypeMap = {
1415
+ "0": "LIKE",
1416
+ "1": "CELEBRATE",
1417
+ "2": "SUPPORT",
1418
+ "3": "LOVE",
1419
+ "4": "INSIGHTFUL",
1420
+ "5": "FUNNY",
1421
+ };
1422
+ for (const urn of elementsRefs) {
1423
+ if (!urn || typeof urn !== "string")
1424
+ continue;
1425
+ // Parse URN: urn:li:fsd_reaction:(urn:li:fsd_profile:ABC,urn:li:activity:123,0)
1426
+ const match = urn.match(/fsd_reaction:\(([^,]+),([^,]+),(\d+)\)/);
1427
+ if (!match)
1428
+ continue;
1429
+ const [, actorUrn, , reactionTypeNum] = match;
1430
+ const reactionType = reactionTypeMap[reactionTypeNum] || "LIKE";
1431
+ // Extract actor URN (profile or company)
1432
+ const isCompany = actorUrn?.includes("fsd_company:");
1433
+ const actorId = actorUrn?.match(/:([\w-]+)$/)?.[1];
1434
+ reactions.push({
1435
+ reactionType,
1436
+ actor: actorId
1437
+ ? {
1438
+ entityUrn: actorUrn,
1439
+ isCompany,
1440
+ }
1441
+ : undefined,
1442
+ });
1443
+ }
1444
+ const result = {
1445
+ reactions,
1446
+ totalCount: total,
1447
+ page: {
1448
+ start,
1449
+ count,
1450
+ total,
1451
+ hasMore: start + count < total,
1452
+ },
1453
+ };
1454
+ try {
1455
+ (0, logger_1.log)("info", "api.ok", {
1456
+ operation: "getPostReactions",
1457
+ selector: activityId,
1458
+ totalCount: total,
1459
+ fetchedCount: reactions.length,
1460
+ });
1461
+ }
1462
+ catch { }
1463
+ return result;
1464
+ }
1465
+ /**
1466
+ * Fetches comments for a LinkedIn post.
1467
+ * Uses the voyagerFeedDashUpdates GraphQL API.
1468
+ *
1469
+ * @param activityUrn - The activity URN (e.g., "urn:li:activity:7419061111293083648") or bare ID
1470
+ * @param options - Fetch options
1471
+ * @param options.commentsCount - Number of comments to fetch (default: 10, max: 100)
1472
+ * @returns PostCommentsResult with comments array
1473
+ * @throws LinkedInClientError with code NOT_FOUND if post doesn't exist
1474
+ *
1475
+ * @example
1476
+ * ```typescript
1477
+ * // Get comments for a post
1478
+ * const comments = await getPostComments('urn:li:activity:7419061111293083648');
1479
+ * console.log(comments.totalCount);
1480
+ *
1481
+ * // Get more comments
1482
+ * const moreComments = await getPostComments('7419061111293083648', { commentsCount: 50 });
1483
+ * ```
1484
+ */
1485
+ async function getPostComments(activityUrn, options) {
1486
+ const activityId = extractActivityId(activityUrn);
1487
+ const commentsCount = Math.min(Number.isFinite(options?.commentsCount)
1488
+ ? Number(options.commentsCount)
1489
+ : 10, 100);
1490
+ // Build variables for voyagerFeedDashUpdates endpoint
1491
+ // Format: (commentsCount:10,likesCount:10,includeCommentsFirstReply:true,includeReactions:false,moduleKey:feed-item:desktop,urnOrNss:urn:li:activity:XXX)
1492
+ const activityUrnFull = `urn:li:activity:${activityId}`;
1493
+ const variables = `(commentsCount:${commentsCount},likesCount:10,includeCommentsFirstReply:true,includeReactions:false,moduleKey:feed-item%3Adesktop,urnOrNss:${encodeURIComponent(activityUrnFull)})`;
1494
+ const url = `${LINKEDIN_API_BASE}/graphql?includeWebMetadata=true&variables=${variables}&queryId=${FEED_UPDATES_QUERY_ID}`;
1495
+ try {
1496
+ (0, logger_1.log)("info", "api.start", {
1497
+ operation: "getPostComments",
1498
+ selector: { activityId, commentsCount },
1499
+ });
1500
+ }
1501
+ catch { }
1502
+ let raw;
1503
+ try {
1504
+ raw = await (0, http_client_1.executeLinkedInRequest)({ url }, "getPostComments");
1505
+ }
1506
+ catch (e) {
1507
+ const status = e?.status ?? 0;
1508
+ if (status === 404) {
1509
+ try {
1510
+ (0, logger_1.log)("warn", "api.notFound", {
1511
+ operation: "getPostComments",
1512
+ selector: activityId,
1513
+ status,
1514
+ });
1515
+ }
1516
+ catch { }
1517
+ throw new errors_1.LinkedInClientError("Post not found", errors_1.ERROR_CODES.NOT_FOUND, 404);
1518
+ }
1519
+ throw e;
1520
+ }
1521
+ // Parse the voyagerFeedDashUpdates GraphQL response
1522
+ // Response has: { data: {...}, included: [...] }
1523
+ const rr = raw;
1524
+ const included = rr?.included;
1525
+ const comments = [];
1526
+ let totalCount = 0;
1527
+ if (Array.isArray(included)) {
1528
+ // Find comments in included array - they have $type containing "Comment"
1529
+ for (const item of included) {
1530
+ const type = item?.$type;
1531
+ const entityUrn = item?.entityUrn;
1532
+ // Look for comment entities
1533
+ if (type && type.includes("Comment") && entityUrn?.includes("comment")) {
1534
+ // Extract comment text - try multiple possible locations
1535
+ let text;
1536
+ // Try commentary.text.text (nested structure) or commentary.text directly
1537
+ const commentaryObj = item.commentary;
1538
+ if (commentaryObj) {
1539
+ const commentaryTextObj = commentaryObj.text;
1540
+ if (typeof commentaryTextObj === "string") {
1541
+ text = commentaryTextObj;
1542
+ }
1543
+ else if (commentaryTextObj &&
1544
+ typeof commentaryTextObj === "object") {
1545
+ text = commentaryTextObj.text;
1546
+ }
1547
+ }
1548
+ // Try content field
1549
+ if (!text && item.content) {
1550
+ const contentObj = item.content;
1551
+ if (typeof contentObj === "string") {
1552
+ text = contentObj;
1553
+ }
1554
+ else if (contentObj && typeof contentObj === "object") {
1555
+ const contentText = contentObj.text;
1556
+ if (typeof contentText === "string") {
1557
+ text = contentText;
1558
+ }
1559
+ else if (contentText && typeof contentText === "object") {
1560
+ text = contentText.text;
1561
+ }
1562
+ }
1563
+ }
1564
+ // Try direct text field
1565
+ if (!text) {
1566
+ text = item.commentText;
1567
+ }
1568
+ // Skip if no text found (probably a hide action or other non-comment entity)
1569
+ if (!text)
1570
+ continue;
1571
+ // Extract author info from commenter lockup
1572
+ const commenterLockup = item.commenter;
1573
+ const titleObj = commenterLockup?.title;
1574
+ const authorName = titleObj?.text;
1575
+ const subtitleObj = commenterLockup?.subtitle;
1576
+ const authorHeadline = subtitleObj?.text;
1577
+ // Extract profile URL
1578
+ const navigationCtx = commenterLockup?.navigationContext;
1579
+ const authorProfileUrl = navigationCtx?.url;
1580
+ // Parse author name
1581
+ let authorFirstName;
1582
+ let authorLastName;
1583
+ if (authorName) {
1584
+ const nameParts = authorName.split(" ");
1585
+ authorFirstName = nameParts[0];
1586
+ authorLastName = nameParts.slice(1).join(" ") || undefined;
1587
+ }
1588
+ const author = authorName
1589
+ ? {
1590
+ entityUrn: item.commenterProfileId,
1591
+ firstName: authorFirstName,
1592
+ lastName: authorLastName,
1593
+ headline: authorHeadline,
1594
+ profileUrl: authorProfileUrl,
1595
+ }
1596
+ : undefined;
1597
+ // Extract engagement metrics
1598
+ const socialDetail = item.socialDetail;
1599
+ const totalCounts = socialDetail?.totalSocialActivityCounts;
1600
+ const numLikes = totalCounts?.numLikes ??
1601
+ item.numLikes;
1602
+ const numReplies = totalCounts?.numComments ??
1603
+ item.numReplies;
1604
+ // Extract timestamp
1605
+ const createdAt = item.createdTime;
1606
+ comments.push({
1607
+ entityUrn,
1608
+ text,
1609
+ author,
1610
+ createdAt,
1611
+ numLikes,
1612
+ numReplies,
1613
+ });
1614
+ }
1615
+ // Also look for social counts to get total comment count
1616
+ if (entityUrn?.includes("socialActivityCounts") &&
1617
+ entityUrn.includes(activityId)) {
1618
+ totalCount = item.numComments ?? totalCount;
1619
+ }
1620
+ }
1621
+ }
1622
+ // If we didn't find totalCount from socialActivityCounts, use the comments array length
1623
+ if (totalCount === 0) {
1624
+ totalCount = comments.length;
1625
+ }
1626
+ const result = {
1627
+ comments,
1628
+ totalCount,
1629
+ page: {
1630
+ start: 0,
1631
+ count: commentsCount,
1632
+ total: totalCount,
1633
+ hasMore: comments.length < totalCount,
1634
+ },
1635
+ };
1636
+ try {
1637
+ (0, logger_1.log)("info", "api.ok", {
1638
+ operation: "getPostComments",
1639
+ selector: activityId,
1640
+ totalCount,
1641
+ fetchedCount: comments.length,
1642
+ });
1643
+ }
1644
+ catch { }
1645
+ return result;
1646
+ }
1647
+ /**
1648
+ * Fetches a user's post history (their published posts).
1649
+ * Uses the Voyager profileUpdatesV2 API with pagination support.
1650
+ *
1651
+ * @param profileUrn - The FSD profile URN (e.g., "urn:li:fsd_profile:ABC123xyz") or bare key
1652
+ * @param options - Pagination options
1653
+ * @param options.start - Pagination offset (default: 0)
1654
+ * @param options.count - Number of posts to fetch (default: 10, max: 100)
1655
+ * @returns PostHistoryResult with posts array and pagination info
1656
+ * @throws LinkedInClientError with code NOT_FOUND if profile doesn't exist
1657
+ *
1658
+ * @example
1659
+ * ```typescript
1660
+ * // Get first 10 posts
1661
+ * const posts = await getPostHistory('ABC123xyz');
1662
+ * console.log(posts.posts.length);
1663
+ *
1664
+ * // Paginate
1665
+ * const page2 = await getPostHistory('ABC123xyz', { start: 10, count: 10 });
1666
+ * ```
1667
+ */
1668
+ async function getPostHistory(profileUrn, options) {
1669
+ const input = String(profileUrn || "").trim();
1670
+ // Extract FSD key from various URN formats
1671
+ const keyMatch = input.match(/^urn:li:fsd_profile:([^\s/]+)$/i)?.[1] ||
1672
+ input.match(/^urn:li:fs_salesProfile:\(([^,\s)]+)/i)?.[1] ||
1673
+ (input.match(/^[A-Za-z0-9_-]+$/) ? input : null);
1674
+ if (!keyMatch) {
1675
+ throw new errors_1.LinkedInClientError("Invalid profile URN or key", errors_1.ERROR_CODES.INVALID_INPUT, 400);
1676
+ }
1677
+ const start = Number.isFinite(options?.start) ? Number(options.start) : 0;
1678
+ const count = Math.min(Number.isFinite(options?.count) ? Number(options.count) : 10, 100);
1679
+ const profileUrnEncoded = encodeURIComponent(`urn:li:fsd_profile:${keyMatch}`);
1680
+ const url = `${LINKEDIN_API_BASE}/identity/profileUpdatesV2?count=${count}&includeLongTermHistory=true&moduleKey=member-shares%3Aphone&profileUrn=${profileUrnEncoded}&q=memberShareFeed&start=${start}`;
1681
+ try {
1682
+ (0, logger_1.log)("info", "api.start", {
1683
+ operation: "getPostHistory",
1684
+ selector: { profileKey: keyMatch, start, count },
1685
+ });
1686
+ }
1687
+ catch { }
1688
+ let raw;
1689
+ try {
1690
+ raw = await (0, http_client_1.executeLinkedInRequest)({ url }, "getPostHistory");
1691
+ }
1692
+ catch (e) {
1693
+ const status = e?.status ?? 0;
1694
+ if (status === 404) {
1695
+ try {
1696
+ (0, logger_1.log)("warn", "api.notFound", {
1697
+ operation: "getPostHistory",
1698
+ selector: keyMatch,
1699
+ status,
1700
+ });
1701
+ }
1702
+ catch { }
1703
+ throw new errors_1.LinkedInClientError("Profile not found", errors_1.ERROR_CODES.NOT_FOUND, 404);
1704
+ }
1705
+ throw e;
1706
+ }
1707
+ // Parse the response
1708
+ const rr = raw;
1709
+ // The response can have two formats:
1710
+ // 1. Old format: { elements: [...], paging: {...} }
1711
+ // 2. New dash format: { data: {...}, included: [...] }
1712
+ const included = rr?.included;
1713
+ const dataObj = rr?.data;
1714
+ // Extract paging info
1715
+ const paging = (dataObj?.paging || rr?.paging);
1716
+ const total = paging?.total;
1717
+ // Extract posts - handle both formats
1718
+ let elements = [];
1719
+ if (Array.isArray(rr?.elements)) {
1720
+ // Old format
1721
+ elements = rr.elements;
1722
+ }
1723
+ else if (Array.isArray(included)) {
1724
+ // New dash format - posts are in included array with specific $type
1725
+ const dataElements = dataObj?.elements;
1726
+ if (Array.isArray(dataElements)) {
1727
+ elements = dataElements;
1728
+ }
1729
+ else {
1730
+ // Filter included array for update/activity types
1731
+ elements = included.filter((item) => {
1732
+ const type = item?.$type;
1733
+ return (type &&
1734
+ (type.includes("Update") ||
1735
+ type.includes("Activity") ||
1736
+ type.includes("share") ||
1737
+ type.includes("Share")));
1738
+ });
1739
+ }
1740
+ }
1741
+ const posts = [];
1742
+ // Build a map of activity URN -> relative time from actor.subDescription
1743
+ // LinkedIn returns relative time like "2d", "1w", "3mo" instead of absolute timestamps
1744
+ const relativeTimeMap = new Map();
1745
+ if (Array.isArray(included)) {
1746
+ for (const item of included) {
1747
+ const entityUrn = item?.entityUrn;
1748
+ if (!entityUrn?.includes("fs_updateV2:"))
1749
+ continue;
1750
+ const activityMatch = entityUrn.match(/activity:(\d+)/);
1751
+ if (!activityMatch)
1752
+ continue;
1753
+ const activityUrn = `urn:li:activity:${activityMatch[1]}`;
1754
+ const actor = item.actor;
1755
+ const subDesc = actor?.subDescription;
1756
+ const relativeText = subDesc?.text || subDesc?.accessibilityText;
1757
+ if (relativeText) {
1758
+ const timestamp = parseRelativeTime(relativeText);
1759
+ if (timestamp) {
1760
+ relativeTimeMap.set(activityUrn, timestamp);
1761
+ }
1762
+ }
1763
+ }
1764
+ }
1765
+ // Build a map of activity URN -> social counts from included array
1766
+ const socialCountsMap = new Map();
1767
+ if (Array.isArray(included)) {
1768
+ for (const item of included) {
1769
+ const urn = item?.entityUrn;
1770
+ // Match fs_socialActivityCounts:urn:li:activity:XXX
1771
+ if (urn && urn.includes("fs_socialActivityCounts")) {
1772
+ const activityMatch = urn.match(/activity:(\d+)/);
1773
+ if (activityMatch) {
1774
+ const activityUrn = `urn:li:activity:${activityMatch[1]}`;
1775
+ socialCountsMap.set(activityUrn, {
1776
+ numLikes: item?.numLikes,
1777
+ numComments: item?.numComments,
1778
+ numShares: item?.numShares,
1779
+ });
1780
+ }
1781
+ }
1782
+ }
1783
+ }
1784
+ for (const elem of elements) {
1785
+ const el = elem;
1786
+ if (!el)
1787
+ continue;
1788
+ const entityUrn = el.entityUrn;
1789
+ // Only process actual posts (fs_updateV2 with activity in URN)
1790
+ // Skip action entries, socialActivityCounts, miniProfile, etc.
1791
+ if (!entityUrn || !entityUrn.includes("fs_updateV2:"))
1792
+ continue;
1793
+ // Extract activity URN from entity URN
1794
+ // Format: urn:li:fs_updateV2:(urn:li:activity:7418215535290662912,MEMBER_SHARES,...)
1795
+ const activityMatch = entityUrn.match(/activity:(\d+)/);
1796
+ const activityUrn = activityMatch
1797
+ ? `urn:li:activity:${activityMatch[1]}`
1798
+ : undefined;
1799
+ // Extract text from commentary or directly
1800
+ const commentary = el.commentary;
1801
+ const textObj = commentary?.text;
1802
+ let text = textObj?.text;
1803
+ // Fallback: check direct text field
1804
+ if (!text && el.text) {
1805
+ text = el.text;
1806
+ }
1807
+ // Get social counts from map or from element directly
1808
+ let numLikes;
1809
+ let numComments;
1810
+ let numShares;
1811
+ if (activityUrn && socialCountsMap.has(activityUrn)) {
1812
+ const counts = socialCountsMap.get(activityUrn);
1813
+ numLikes = counts.numLikes;
1814
+ numComments = counts.numComments;
1815
+ numShares = counts.numShares;
1816
+ }
1817
+ else {
1818
+ // Try extracting from socialDetail in the element
1819
+ const socialDetail = el.socialDetail;
1820
+ const socialCounts = socialDetail?.totalSocialActivityCounts;
1821
+ numLikes = socialCounts?.numLikes;
1822
+ numComments = socialCounts?.numComments;
1823
+ numShares = socialCounts?.numShares;
1824
+ }
1825
+ // Extract timestamp - try multiple possible paths
1826
+ let createdAt = el.createdTime;
1827
+ if (!createdAt) {
1828
+ // Try actor.timestamp or publishedAt
1829
+ const actor = el.actor;
1830
+ createdAt =
1831
+ actor?.timestamp ||
1832
+ el.publishedAt ||
1833
+ el.createdAt;
1834
+ }
1835
+ // Fallback to parsed relative time from actor.subDescription (e.g., "2d", "1w")
1836
+ if (!createdAt && activityUrn && relativeTimeMap.has(activityUrn)) {
1837
+ createdAt = relativeTimeMap.get(activityUrn);
1838
+ }
1839
+ // Check if reshared
1840
+ const resharedUpdate = el.resharedUpdate;
1841
+ const reshared = !!resharedUpdate;
1842
+ const originalPostUrn = resharedUpdate?.activityUrn;
1843
+ // Only add posts that have text content (skip empty entries)
1844
+ if (text || activityUrn) {
1845
+ posts.push({
1846
+ activityUrn,
1847
+ entityUrn,
1848
+ text,
1849
+ createdAt,
1850
+ numLikes,
1851
+ numComments,
1852
+ numShares,
1853
+ reshared,
1854
+ originalPostUrn,
1855
+ });
1856
+ }
1857
+ }
1858
+ // Deduplicate posts by activityUrn
1859
+ const seenUrns = new Set();
1860
+ const uniquePosts = posts.filter((p) => {
1861
+ if (!p.activityUrn)
1862
+ return true;
1863
+ if (seenUrns.has(p.activityUrn))
1864
+ return false;
1865
+ seenUrns.add(p.activityUrn);
1866
+ return true;
1867
+ });
1868
+ const hasMore = total ? start + count < total : elements.length === count;
1869
+ const result = {
1870
+ posts: uniquePosts,
1871
+ page: {
1872
+ start,
1873
+ count,
1874
+ total,
1875
+ hasMore,
1876
+ },
1877
+ };
1878
+ try {
1879
+ (0, logger_1.log)("info", "api.ok", {
1880
+ operation: "getPostHistory",
1881
+ selector: keyMatch,
1882
+ totalCount: total,
1883
+ fetchedCount: uniquePosts.length,
1884
+ });
1885
+ }
1886
+ catch { }
1887
+ return result;
1888
+ }