@memberjunction/actions-bizapps-social 2.112.0 → 2.113.1
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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +13 -0
- package/dist/base/base-social.action.d.ts.map +1 -1
- package/dist/base/base-social.action.js +24 -18
- package/dist/base/base-social.action.js.map +1 -1
- package/dist/providers/buffer/buffer-base.action.d.ts.map +1 -1
- package/dist/providers/buffer/buffer-base.action.js +34 -35
- package/dist/providers/buffer/buffer-base.action.js.map +1 -1
- package/dist/providers/facebook/actions/boost-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/boost-post.action.js +33 -33
- package/dist/providers/facebook/actions/boost-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/create-album.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/create-album.action.js +36 -34
- package/dist/providers/facebook/actions/create-album.action.js.map +1 -1
- package/dist/providers/facebook/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/create-post.action.js +20 -20
- package/dist/providers/facebook/actions/create-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-page-insights.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-page-insights.action.js +27 -25
- package/dist/providers/facebook/actions/get-page-insights.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-page-posts.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-page-posts.action.js +23 -19
- package/dist/providers/facebook/actions/get-page-posts.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-post-insights.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-post-insights.action.js +32 -28
- package/dist/providers/facebook/actions/get-post-insights.action.js.map +1 -1
- package/dist/providers/facebook/actions/respond-to-comments.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/respond-to-comments.action.js +44 -42
- package/dist/providers/facebook/actions/respond-to-comments.action.js.map +1 -1
- package/dist/providers/facebook/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/schedule-post.action.js +29 -29
- package/dist/providers/facebook/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/search-posts.action.js +39 -37
- package/dist/providers/facebook/actions/search-posts.action.js.map +1 -1
- package/dist/providers/facebook/facebook-base.action.d.ts.map +1 -1
- package/dist/providers/facebook/facebook-base.action.js +59 -44
- package/dist/providers/facebook/facebook-base.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js +31 -33
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.js +32 -28
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js +19 -19
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-analytics.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-analytics.action.js +26 -24
- package/dist/providers/hootsuite/actions/get-analytics.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js +22 -22
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-social-profiles.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-social-profiles.action.js +34 -32
- package/dist/providers/hootsuite/actions/get-social-profiles.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/search-posts.action.js +52 -43
- package/dist/providers/hootsuite/actions/search-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.js +28 -30
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/hootsuite-base.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/hootsuite-base.action.js +20 -18
- package/dist/providers/hootsuite/hootsuite-base.action.js.map +1 -1
- package/dist/providers/instagram/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/create-post.action.js +26 -27
- package/dist/providers/instagram/actions/create-post.action.js.map +1 -1
- package/dist/providers/instagram/actions/create-story.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/create-story.action.js +35 -35
- package/dist/providers/instagram/actions/create-story.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-account-insights.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-account-insights.action.js +59 -38
- package/dist/providers/instagram/actions/get-account-insights.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-business-posts.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-business-posts.action.js +29 -29
- package/dist/providers/instagram/actions/get-business-posts.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-comments.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-comments.action.js +36 -36
- package/dist/providers/instagram/actions/get-comments.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-post-insights.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-post-insights.action.js +25 -23
- package/dist/providers/instagram/actions/get-post-insights.action.js.map +1 -1
- package/dist/providers/instagram/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/schedule-post.action.js +25 -25
- package/dist/providers/instagram/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/instagram/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/search-posts.action.js +60 -56
- package/dist/providers/instagram/actions/search-posts.action.js.map +1 -1
- package/dist/providers/instagram/instagram-base.action.d.ts.map +1 -1
- package/dist/providers/instagram/instagram-base.action.js +27 -25
- package/dist/providers/instagram/instagram-base.action.js.map +1 -1
- package/dist/providers/linkedin/actions/create-article.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/create-article.action.js +45 -55
- package/dist/providers/linkedin/actions/create-article.action.js.map +1 -1
- package/dist/providers/linkedin/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/create-post.action.js +29 -31
- package/dist/providers/linkedin/actions/create-post.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-followers.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-followers.action.js +28 -28
- package/dist/providers/linkedin/actions/get-followers.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-organization-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-organization-posts.action.js +20 -20
- package/dist/providers/linkedin/actions/get-organization-posts.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-personal-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-personal-posts.action.js +19 -19
- package/dist/providers/linkedin/actions/get-personal-posts.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-post-analytics.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-post-analytics.action.js +23 -25
- package/dist/providers/linkedin/actions/get-post-analytics.action.js.map +1 -1
- package/dist/providers/linkedin/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/schedule-post.action.js +30 -32
- package/dist/providers/linkedin/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/linkedin/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/search-posts.action.js +30 -28
- package/dist/providers/linkedin/actions/search-posts.action.js.map +1 -1
- package/dist/providers/linkedin/linkedin-base.action.d.ts.map +1 -1
- package/dist/providers/linkedin/linkedin-base.action.js +38 -33
- package/dist/providers/linkedin/linkedin-base.action.js.map +1 -1
- package/dist/providers/tiktok/tiktok-base.action.d.ts.map +1 -1
- package/dist/providers/tiktok/tiktok-base.action.js +26 -25
- package/dist/providers/tiktok/tiktok-base.action.js.map +1 -1
- package/dist/providers/twitter/actions/create-thread.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/create-thread.action.js +29 -25
- package/dist/providers/twitter/actions/create-thread.action.js.map +1 -1
- package/dist/providers/twitter/actions/create-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/create-tweet.action.js +23 -23
- package/dist/providers/twitter/actions/create-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/delete-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/delete-tweet.action.js +19 -19
- package/dist/providers/twitter/actions/delete-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-analytics.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-analytics.action.js +47 -40
- package/dist/providers/twitter/actions/get-analytics.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-mentions.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-mentions.action.js +31 -30
- package/dist/providers/twitter/actions/get-mentions.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-timeline.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-timeline.action.js +29 -29
- package/dist/providers/twitter/actions/get-timeline.action.js.map +1 -1
- package/dist/providers/twitter/actions/schedule-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/schedule-tweet.action.js +26 -26
- package/dist/providers/twitter/actions/schedule-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/search-tweets.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/search-tweets.action.js +58 -56
- package/dist/providers/twitter/actions/search-tweets.action.js.map +1 -1
- package/dist/providers/twitter/twitter-base.action.d.ts.map +1 -1
- package/dist/providers/twitter/twitter-base.action.js +68 -58
- package/dist/providers/twitter/twitter-base.action.js.map +1 -1
- package/dist/providers/youtube/youtube-base.action.d.ts +1 -1
- package/dist/providers/youtube/youtube-base.action.d.ts.map +1 -1
- package/dist/providers/youtube/youtube-base.action.js +25 -22
- package/dist/providers/youtube/youtube-base.action.js.map +1 -1
- package/package.json +6 -5
- package/src/base/base-social.action.ts +224 -217
- package/src/providers/buffer/buffer-base.action.ts +441 -435
- package/src/providers/facebook/actions/boost-post.action.ts +386 -350
- package/src/providers/facebook/actions/create-album.action.ts +307 -291
- package/src/providers/facebook/actions/create-post.action.ts +227 -224
- package/src/providers/facebook/actions/get-page-insights.action.ts +403 -383
- package/src/providers/facebook/actions/get-page-posts.action.ts +225 -214
- package/src/providers/facebook/actions/get-post-insights.action.ts +316 -300
- package/src/providers/facebook/actions/respond-to-comments.action.ts +336 -319
- package/src/providers/facebook/actions/schedule-post.action.ts +292 -289
- package/src/providers/facebook/actions/search-posts.action.ts +413 -399
- package/src/providers/facebook/facebook-base.action.ts +670 -653
- package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
- package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +189 -184
- package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +161 -160
- package/src/providers/hootsuite/actions/get-analytics.action.ts +254 -249
- package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +207 -206
- package/src/providers/hootsuite/actions/get-social-profiles.action.ts +205 -206
- package/src/providers/hootsuite/actions/search-posts.action.ts +369 -351
- package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +209 -211
- package/src/providers/hootsuite/hootsuite-base.action.ts +307 -301
- package/src/providers/instagram/actions/create-post.action.ts +296 -276
- package/src/providers/instagram/actions/create-story.action.ts +394 -378
- package/src/providers/instagram/actions/get-account-insights.action.ts +420 -384
- package/src/providers/instagram/actions/get-business-posts.action.ts +242 -233
- package/src/providers/instagram/actions/get-comments.action.ts +377 -365
- package/src/providers/instagram/actions/get-post-insights.action.ts +273 -265
- package/src/providers/instagram/actions/schedule-post.action.ts +235 -233
- package/src/providers/instagram/actions/search-posts.action.ts +538 -512
- package/src/providers/instagram/instagram-base.action.ts +393 -368
- package/src/providers/linkedin/actions/create-article.action.ts +266 -275
- package/src/providers/linkedin/actions/create-post.action.ts +177 -179
- package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
- package/src/providers/linkedin/actions/get-organization-posts.action.ts +147 -146
- package/src/providers/linkedin/actions/get-personal-posts.action.ts +139 -138
- package/src/providers/linkedin/actions/get-post-analytics.action.ts +189 -190
- package/src/providers/linkedin/actions/schedule-post.action.ts +189 -191
- package/src/providers/linkedin/actions/search-posts.action.ts +283 -275
- package/src/providers/linkedin/linkedin-base.action.ts +421 -407
- package/src/providers/tiktok/tiktok-base.action.ts +320 -305
- package/src/providers/twitter/actions/create-thread.action.ts +207 -203
- package/src/providers/twitter/actions/create-tweet.action.ts +188 -187
- package/src/providers/twitter/actions/delete-tweet.action.ts +129 -128
- package/src/providers/twitter/actions/get-analytics.action.ts +411 -402
- package/src/providers/twitter/actions/get-mentions.action.ts +219 -218
- package/src/providers/twitter/actions/get-timeline.action.ts +233 -232
- package/src/providers/twitter/actions/schedule-tweet.action.ts +222 -221
- package/src/providers/twitter/actions/search-tweets.action.ts +543 -540
- package/src/providers/twitter/twitter-base.action.ts +560 -541
- package/src/providers/youtube/youtube-base.action.ts +333 -320
|
@@ -2,7 +2,7 @@ import { RegisterClass } from '@memberjunction/global';
|
|
|
2
2
|
import { BaseSocialMediaAction, MediaFile, SocialPost, SearchParams, SocialAnalytics } from '../../base/base-social.action';
|
|
3
3
|
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
4
4
|
import { ActionParam } from '@memberjunction/actions-base';
|
|
5
|
-
import { LogStatus, LogError } from '@memberjunction/
|
|
5
|
+
import { LogStatus, LogError } from '@memberjunction/core';
|
|
6
6
|
import { BaseAction } from '@memberjunction/actions';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -12,457 +12,471 @@ import { BaseAction } from '@memberjunction/actions';
|
|
|
12
12
|
*/
|
|
13
13
|
@RegisterClass(BaseAction, 'LinkedInBaseAction')
|
|
14
14
|
export abstract class LinkedInBaseAction extends BaseSocialMediaAction {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
protected get apiBaseUrl(): string {
|
|
20
|
-
return 'https://api.linkedin.com/v2';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Axios instance for making HTTP requests
|
|
25
|
-
*/
|
|
26
|
-
private _axiosInstance: AxiosInstance | null = null;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Get or create axios instance with interceptors
|
|
30
|
-
*/
|
|
31
|
-
protected get axiosInstance(): AxiosInstance {
|
|
32
|
-
if (!this._axiosInstance) {
|
|
33
|
-
this._axiosInstance = axios.create({
|
|
34
|
-
baseURL: this.apiBaseUrl,
|
|
35
|
-
timeout: 30000,
|
|
36
|
-
headers: {
|
|
37
|
-
'Content-Type': 'application/json',
|
|
38
|
-
Accept: 'application/json',
|
|
39
|
-
'X-Restli-Protocol-Version': '2.0.0', // LinkedIn specific header
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// Add request interceptor for auth
|
|
44
|
-
this._axiosInstance.interceptors.request.use(
|
|
45
|
-
(config) => {
|
|
46
|
-
const token = this.getAccessToken();
|
|
47
|
-
if (token) {
|
|
48
|
-
config.headers.Authorization = `Bearer ${token}`;
|
|
49
|
-
}
|
|
50
|
-
return config;
|
|
51
|
-
},
|
|
52
|
-
(error) => Promise.reject(error)
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
// Add response interceptor for rate limit handling
|
|
56
|
-
this._axiosInstance.interceptors.response.use(
|
|
57
|
-
(response) => {
|
|
58
|
-
// Log rate limit info
|
|
59
|
-
const rateLimitInfo = this.parseRateLimitHeaders(response.headers);
|
|
60
|
-
if (rateLimitInfo) {
|
|
61
|
-
LogStatus(`LinkedIn Rate Limit - Remaining: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`);
|
|
62
|
-
}
|
|
63
|
-
return response;
|
|
64
|
-
},
|
|
65
|
-
async (error: AxiosError) => {
|
|
66
|
-
if (error.response?.status === 429) {
|
|
67
|
-
// Rate limit exceeded
|
|
68
|
-
const retryAfter = error.response.headers['retry-after'];
|
|
69
|
-
const waitTime = retryAfter ? parseInt(retryAfter) : 60;
|
|
70
|
-
await this.handleRateLimit(waitTime);
|
|
71
|
-
|
|
72
|
-
// Retry the request
|
|
73
|
-
return this._axiosInstance!.request(error.config!);
|
|
74
|
-
}
|
|
75
|
-
return Promise.reject(error);
|
|
76
|
-
}
|
|
77
|
-
);
|
|
15
|
+
protected get platformName(): string {
|
|
16
|
+
return 'LinkedIn';
|
|
78
17
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Refresh the access token using the refresh token
|
|
84
|
-
*/
|
|
85
|
-
protected async refreshAccessToken(): Promise<void> {
|
|
86
|
-
const refreshToken = this.getRefreshToken();
|
|
87
|
-
if (!refreshToken) {
|
|
88
|
-
throw new Error('No refresh token available for LinkedIn');
|
|
18
|
+
|
|
19
|
+
protected get apiBaseUrl(): string {
|
|
20
|
+
return 'https://api.linkedin.com/v2';
|
|
89
21
|
}
|
|
90
22
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Axios instance for making HTTP requests
|
|
25
|
+
*/
|
|
26
|
+
private _axiosInstance: AxiosInstance | null = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get or create axios instance with interceptors
|
|
30
|
+
*/
|
|
31
|
+
protected get axiosInstance(): AxiosInstance {
|
|
32
|
+
if (!this._axiosInstance) {
|
|
33
|
+
this._axiosInstance = axios.create({
|
|
34
|
+
baseURL: this.apiBaseUrl,
|
|
35
|
+
timeout: 30000,
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
'Accept': 'application/json',
|
|
39
|
+
'X-Restli-Protocol-Version': '2.0.0' // LinkedIn specific header
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Add request interceptor for auth
|
|
44
|
+
this._axiosInstance.interceptors.request.use(
|
|
45
|
+
(config) => {
|
|
46
|
+
const token = this.getAccessToken();
|
|
47
|
+
if (token) {
|
|
48
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
49
|
+
}
|
|
50
|
+
return config;
|
|
51
|
+
},
|
|
52
|
+
(error) => Promise.reject(error)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Add response interceptor for rate limit handling
|
|
56
|
+
this._axiosInstance.interceptors.response.use(
|
|
57
|
+
(response) => {
|
|
58
|
+
// Log rate limit info
|
|
59
|
+
const rateLimitInfo = this.parseRateLimitHeaders(response.headers);
|
|
60
|
+
if (rateLimitInfo) {
|
|
61
|
+
LogStatus(`LinkedIn Rate Limit - Remaining: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`);
|
|
62
|
+
}
|
|
63
|
+
return response;
|
|
64
|
+
},
|
|
65
|
+
async (error: AxiosError) => {
|
|
66
|
+
if (error.response?.status === 429) {
|
|
67
|
+
// Rate limit exceeded
|
|
68
|
+
const retryAfter = error.response.headers['retry-after'];
|
|
69
|
+
const waitTime = retryAfter ? parseInt(retryAfter) : 60;
|
|
70
|
+
await this.handleRateLimit(waitTime);
|
|
71
|
+
|
|
72
|
+
// Retry the request
|
|
73
|
+
return this._axiosInstance!.request(error.config!);
|
|
74
|
+
}
|
|
75
|
+
return Promise.reject(error);
|
|
76
|
+
}
|
|
77
|
+
);
|
|
104
78
|
}
|
|
105
|
-
|
|
79
|
+
return this._axiosInstance;
|
|
80
|
+
}
|
|
106
81
|
|
|
107
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Refresh the access token using the refresh token
|
|
84
|
+
*/
|
|
85
|
+
protected async refreshAccessToken(): Promise<void> {
|
|
86
|
+
const refreshToken = this.getRefreshToken();
|
|
87
|
+
if (!refreshToken) {
|
|
88
|
+
throw new Error('No refresh token available for LinkedIn');
|
|
89
|
+
}
|
|
108
90
|
|
|
109
|
-
|
|
110
|
-
|
|
91
|
+
try {
|
|
92
|
+
const response = await axios.post('https://www.linkedin.com/oauth/v2/accessToken',
|
|
93
|
+
new URLSearchParams({
|
|
94
|
+
grant_type: 'refresh_token',
|
|
95
|
+
refresh_token: refreshToken,
|
|
96
|
+
client_id: this.getCustomAttribute(2) || '', // Client ID stored in CustomAttribute2
|
|
97
|
+
client_secret: this.getCustomAttribute(3) || '' // Client Secret stored in CustomAttribute3
|
|
98
|
+
}).toString(),
|
|
99
|
+
{
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const { access_token, refresh_token: newRefreshToken, expires_in } = response.data;
|
|
107
|
+
|
|
108
|
+
// Update stored tokens
|
|
109
|
+
await this.updateStoredTokens(
|
|
110
|
+
access_token,
|
|
111
|
+
newRefreshToken || refreshToken,
|
|
112
|
+
expires_in
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
LogStatus('LinkedIn access token refreshed successfully');
|
|
116
|
+
} catch (error) {
|
|
117
|
+
LogError(`Failed to refresh LinkedIn access token: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
111
121
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Get the authenticated user's profile URN
|
|
124
|
+
*/
|
|
125
|
+
protected async getCurrentUserUrn(): Promise<string> {
|
|
126
|
+
try {
|
|
127
|
+
const response = await this.axiosInstance.get('/me');
|
|
128
|
+
return `urn:li:person:${response.data.id}`;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
LogError(`Failed to get current user URN: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
116
133
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get organizations the user has admin access to
|
|
137
|
+
*/
|
|
138
|
+
protected async getAdminOrganizations(): Promise<LinkedInOrganization[]> {
|
|
139
|
+
try {
|
|
140
|
+
const response = await this.axiosInstance.get('/organizationalEntityAcls', {
|
|
141
|
+
params: {
|
|
142
|
+
q: 'roleAssignee',
|
|
143
|
+
role: 'ADMINISTRATOR',
|
|
144
|
+
projection: '(elements*(*,organizationalTarget~(localizedName)))'
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const organizations: LinkedInOrganization[] = [];
|
|
149
|
+
if (response.data.elements) {
|
|
150
|
+
for (const element of response.data.elements) {
|
|
151
|
+
if (element.organizationalTarget) {
|
|
152
|
+
organizations.push({
|
|
153
|
+
urn: element.organizationalTarget,
|
|
154
|
+
name: element['organizationalTarget~']?.localizedName || 'Unknown',
|
|
155
|
+
id: element.organizationalTarget.split(':').pop() || ''
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return organizations;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
LogError(`Failed to get admin organizations: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
129
166
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Upload media to LinkedIn
|
|
170
|
+
*/
|
|
171
|
+
protected async uploadSingleMedia(file: MediaFile): Promise<string> {
|
|
172
|
+
try {
|
|
173
|
+
// Step 1: Register upload
|
|
174
|
+
const registerResponse = await this.axiosInstance.post('/assets?action=registerUpload', {
|
|
175
|
+
registerUploadRequest: {
|
|
176
|
+
recipes: ['urn:li:digitalmediaRecipe:feedshare-image'],
|
|
177
|
+
owner: await this.getCurrentUserUrn(),
|
|
178
|
+
serviceRelationships: [{
|
|
179
|
+
relationshipType: 'OWNER',
|
|
180
|
+
identifier: 'urn:li:userGeneratedContent'
|
|
181
|
+
}]
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const uploadUrl = registerResponse.data.value.uploadMechanism['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest'].uploadUrl;
|
|
186
|
+
const asset = registerResponse.data.value.asset;
|
|
187
|
+
|
|
188
|
+
// Step 2: Upload the file
|
|
189
|
+
const fileData = typeof file.data === 'string'
|
|
190
|
+
? Buffer.from(file.data, 'base64')
|
|
191
|
+
: file.data;
|
|
192
|
+
|
|
193
|
+
await axios.put(uploadUrl, fileData, {
|
|
194
|
+
headers: {
|
|
195
|
+
'Authorization': `Bearer ${this.getAccessToken()}`,
|
|
196
|
+
'Content-Type': file.mimeType
|
|
197
|
+
}
|
|
153
198
|
});
|
|
154
|
-
|
|
199
|
+
|
|
200
|
+
// Return the asset URN
|
|
201
|
+
return asset;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
LogError(`Failed to upload media to LinkedIn: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Validate media file meets LinkedIn requirements
|
|
210
|
+
*/
|
|
211
|
+
protected validateMediaFile(file: MediaFile): void {
|
|
212
|
+
const supportedTypes = [
|
|
213
|
+
'image/jpeg',
|
|
214
|
+
'image/png',
|
|
215
|
+
'image/gif',
|
|
216
|
+
'image/webp'
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
if (!supportedTypes.includes(file.mimeType)) {
|
|
220
|
+
throw new Error(`Unsupported media type: ${file.mimeType}. Supported types: ${supportedTypes.join(', ')}`);
|
|
155
221
|
}
|
|
156
|
-
}
|
|
157
222
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
223
|
+
// LinkedIn image size limits
|
|
224
|
+
const maxSize = 10 * 1024 * 1024; // 10MB
|
|
225
|
+
if (file.size > maxSize) {
|
|
226
|
+
throw new Error(`File size exceeds limit. Max: ${maxSize / 1024 / 1024}MB, Got: ${file.size / 1024 / 1024}MB`);
|
|
227
|
+
}
|
|
162
228
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
owner: await this.getCurrentUserUrn(),
|
|
175
|
-
serviceRelationships: [
|
|
176
|
-
{
|
|
177
|
-
relationshipType: 'OWNER',
|
|
178
|
-
identifier: 'urn:li:userGeneratedContent',
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const uploadUrl = registerResponse.data.value.uploadMechanism['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest'].uploadUrl;
|
|
185
|
-
const asset = registerResponse.data.value.asset;
|
|
186
|
-
|
|
187
|
-
// Step 2: Upload the file
|
|
188
|
-
const fileData = typeof file.data === 'string' ? Buffer.from(file.data, 'base64') : file.data;
|
|
189
|
-
|
|
190
|
-
await axios.put(uploadUrl, fileData, {
|
|
191
|
-
headers: {
|
|
192
|
-
Authorization: `Bearer ${this.getAccessToken()}`,
|
|
193
|
-
'Content-Type': file.mimeType,
|
|
194
|
-
},
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// Return the asset URN
|
|
198
|
-
return asset;
|
|
199
|
-
} catch (error) {
|
|
200
|
-
LogError(`Failed to upload media to LinkedIn: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
201
|
-
throw error;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create a share (post) on LinkedIn
|
|
232
|
+
*/
|
|
233
|
+
protected async createShare(shareData: LinkedInShareData): Promise<string> {
|
|
234
|
+
try {
|
|
235
|
+
const response = await this.axiosInstance.post('/ugcPosts', shareData);
|
|
236
|
+
return response.data.id;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
this.handleLinkedInError(error as AxiosError);
|
|
239
|
+
}
|
|
202
240
|
}
|
|
203
|
-
}
|
|
204
241
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Get shares for a specific author (person or organization)
|
|
244
|
+
*/
|
|
245
|
+
protected async getShares(authorUrn: string, count: number = 50, start: number = 0): Promise<LinkedInShare[]> {
|
|
246
|
+
try {
|
|
247
|
+
const response = await this.axiosInstance.get('/ugcPosts', {
|
|
248
|
+
params: {
|
|
249
|
+
q: 'authors',
|
|
250
|
+
authors: `List(${authorUrn})`,
|
|
251
|
+
count: count,
|
|
252
|
+
start: start
|
|
253
|
+
}
|
|
254
|
+
});
|
|
210
255
|
|
|
211
|
-
|
|
212
|
-
|
|
256
|
+
return response.data.elements || [];
|
|
257
|
+
} catch (error) {
|
|
258
|
+
LogError(`Failed to get shares: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
213
261
|
}
|
|
214
262
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Convert LinkedIn share to common format
|
|
265
|
+
*/
|
|
266
|
+
protected normalizePost(linkedInShare: LinkedInShare): SocialPost {
|
|
267
|
+
const publishedAt = new Date(linkedInShare.firstPublishedAt || linkedInShare.created.time);
|
|
268
|
+
|
|
269
|
+
// Extract media URLs
|
|
270
|
+
const mediaUrls: string[] = [];
|
|
271
|
+
if (linkedInShare.specificContent?.['com.linkedin.ugc.ShareContent']?.media) {
|
|
272
|
+
for (const media of linkedInShare.specificContent['com.linkedin.ugc.ShareContent'].media) {
|
|
273
|
+
if (media.media) {
|
|
274
|
+
mediaUrls.push(media.media);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
id: linkedInShare.id,
|
|
281
|
+
platform: 'LinkedIn',
|
|
282
|
+
profileId: linkedInShare.author,
|
|
283
|
+
content: linkedInShare.specificContent?.['com.linkedin.ugc.ShareContent']?.shareCommentary?.text || '',
|
|
284
|
+
mediaUrls: mediaUrls,
|
|
285
|
+
publishedAt: publishedAt,
|
|
286
|
+
platformSpecificData: {
|
|
287
|
+
lifecycleState: linkedInShare.lifecycleState,
|
|
288
|
+
visibility: linkedInShare.visibility,
|
|
289
|
+
distribution: linkedInShare.distribution
|
|
290
|
+
}
|
|
291
|
+
};
|
|
219
292
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Normalize LinkedIn analytics to common format
|
|
296
|
+
*/
|
|
297
|
+
protected normalizeAnalytics(linkedInAnalytics: LinkedInAnalytics): SocialAnalytics {
|
|
298
|
+
return {
|
|
299
|
+
impressions: linkedInAnalytics.totalShareStatistics?.impressionCount || 0,
|
|
300
|
+
engagements: linkedInAnalytics.totalShareStatistics?.engagement || 0,
|
|
301
|
+
clicks: linkedInAnalytics.totalShareStatistics?.clickCount || 0,
|
|
302
|
+
shares: linkedInAnalytics.totalShareStatistics?.shareCount || 0,
|
|
303
|
+
comments: linkedInAnalytics.totalShareStatistics?.commentCount || 0,
|
|
304
|
+
likes: linkedInAnalytics.totalShareStatistics?.likeCount || 0,
|
|
305
|
+
reach: linkedInAnalytics.totalShareStatistics?.uniqueImpressionsCount || 0,
|
|
306
|
+
platformMetrics: linkedInAnalytics
|
|
307
|
+
};
|
|
231
308
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const response = await this.axiosInstance.get('/ugcPosts', {
|
|
240
|
-
params: {
|
|
241
|
-
q: 'authors',
|
|
242
|
-
authors: `List(${authorUrn})`,
|
|
243
|
-
count: count,
|
|
244
|
-
start: start,
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
return response.data.elements || [];
|
|
249
|
-
} catch (error) {
|
|
250
|
-
LogError(`Failed to get shares: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
251
|
-
throw error;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Search for posts - implemented in search action
|
|
312
|
+
*/
|
|
313
|
+
protected async searchPosts(params: SearchParams): Promise<SocialPost[]> {
|
|
314
|
+
// This is implemented in the search-posts.action.ts
|
|
315
|
+
throw new Error('Search posts is implemented in LinkedInSearchPostsAction');
|
|
252
316
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Handle LinkedIn-specific errors
|
|
320
|
+
*/
|
|
321
|
+
protected handleLinkedInError(error: AxiosError): never {
|
|
322
|
+
if (error.response) {
|
|
323
|
+
const { status, data } = error.response;
|
|
324
|
+
const errorData = data as any;
|
|
325
|
+
|
|
326
|
+
switch (status) {
|
|
327
|
+
case 400:
|
|
328
|
+
throw new Error(`Bad Request: ${errorData.message || 'Invalid request parameters'}`);
|
|
329
|
+
case 401:
|
|
330
|
+
throw new Error('Unauthorized: Invalid or expired access token');
|
|
331
|
+
case 403:
|
|
332
|
+
throw new Error('Forbidden: Insufficient permissions. Ensure the app has required LinkedIn scopes.');
|
|
333
|
+
case 404:
|
|
334
|
+
throw new Error('Not Found: Resource does not exist');
|
|
335
|
+
case 422:
|
|
336
|
+
throw new Error(`Unprocessable Entity: ${errorData.message || 'Invalid data provided'}`);
|
|
337
|
+
case 429:
|
|
338
|
+
throw new Error('Rate Limit Exceeded: Too many requests');
|
|
339
|
+
case 500:
|
|
340
|
+
throw new Error('Internal Server Error: LinkedIn service error');
|
|
341
|
+
default:
|
|
342
|
+
throw new Error(`LinkedIn API Error (${status}): ${errorData.message || 'Unknown error'}`);
|
|
343
|
+
}
|
|
344
|
+
} else if (error.request) {
|
|
345
|
+
throw new Error('Network Error: No response from LinkedIn');
|
|
346
|
+
} else {
|
|
347
|
+
throw new Error(`Request Error: ${error.message}`);
|
|
267
348
|
}
|
|
268
|
-
}
|
|
269
349
|
}
|
|
270
350
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Search for posts - implemented in search action
|
|
304
|
-
*/
|
|
305
|
-
protected async searchPosts(params: SearchParams): Promise<SocialPost[]> {
|
|
306
|
-
// This is implemented in the search-posts.action.ts
|
|
307
|
-
throw new Error('Search posts is implemented in LinkedInSearchPostsAction');
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Handle LinkedIn-specific errors
|
|
312
|
-
*/
|
|
313
|
-
protected handleLinkedInError(error: AxiosError): never {
|
|
314
|
-
if (error.response) {
|
|
315
|
-
const { status, data } = error.response;
|
|
316
|
-
const errorData = data as any;
|
|
317
|
-
|
|
318
|
-
switch (status) {
|
|
319
|
-
case 400:
|
|
320
|
-
throw new Error(`Bad Request: ${errorData.message || 'Invalid request parameters'}`);
|
|
321
|
-
case 401:
|
|
322
|
-
throw new Error('Unauthorized: Invalid or expired access token');
|
|
323
|
-
case 403:
|
|
324
|
-
throw new Error('Forbidden: Insufficient permissions. Ensure the app has required LinkedIn scopes.');
|
|
325
|
-
case 404:
|
|
326
|
-
throw new Error('Not Found: Resource does not exist');
|
|
327
|
-
case 422:
|
|
328
|
-
throw new Error(`Unprocessable Entity: ${errorData.message || 'Invalid data provided'}`);
|
|
329
|
-
case 429:
|
|
330
|
-
throw new Error('Rate Limit Exceeded: Too many requests');
|
|
331
|
-
case 500:
|
|
332
|
-
throw new Error('Internal Server Error: LinkedIn service error');
|
|
333
|
-
default:
|
|
334
|
-
throw new Error(`LinkedIn API Error (${status}): ${errorData.message || 'Unknown error'}`);
|
|
335
|
-
}
|
|
336
|
-
} else if (error.request) {
|
|
337
|
-
throw new Error('Network Error: No response from LinkedIn');
|
|
338
|
-
} else {
|
|
339
|
-
throw new Error(`Request Error: ${error.message}`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Parse LinkedIn-specific rate limit headers
|
|
345
|
-
*/
|
|
346
|
-
protected parseRateLimitHeaders(headers: any): { remaining: number; reset: Date; limit: number } | null {
|
|
347
|
-
// LinkedIn uses different header names
|
|
348
|
-
const appRemaining = headers['x-app-rate-limit-remaining'];
|
|
349
|
-
const appLimit = headers['x-app-rate-limit-limit'];
|
|
350
|
-
const memberRemaining = headers['x-member-rate-limit-remaining'];
|
|
351
|
-
const memberLimit = headers['x-member-rate-limit-limit'];
|
|
352
|
-
|
|
353
|
-
// Use the more restrictive limit
|
|
354
|
-
const remaining = Math.min(appRemaining ? parseInt(appRemaining) : Infinity, memberRemaining ? parseInt(memberRemaining) : Infinity);
|
|
355
|
-
|
|
356
|
-
const limit = Math.min(appLimit ? parseInt(appLimit) : Infinity, memberLimit ? parseInt(memberLimit) : Infinity);
|
|
357
|
-
|
|
358
|
-
if (remaining !== Infinity && limit !== Infinity) {
|
|
359
|
-
// LinkedIn resets rate limits at the top of each hour
|
|
360
|
-
const now = new Date();
|
|
361
|
-
const reset = new Date(now);
|
|
362
|
-
reset.setHours(reset.getHours() + 1, 0, 0, 0);
|
|
363
|
-
|
|
364
|
-
return { remaining, reset, limit };
|
|
365
|
-
}
|
|
351
|
+
/**
|
|
352
|
+
* Parse LinkedIn-specific rate limit headers
|
|
353
|
+
*/
|
|
354
|
+
protected parseRateLimitHeaders(headers: any): { remaining: number; reset: Date; limit: number; } | null {
|
|
355
|
+
// LinkedIn uses different header names
|
|
356
|
+
const appRemaining = headers['x-app-rate-limit-remaining'];
|
|
357
|
+
const appLimit = headers['x-app-rate-limit-limit'];
|
|
358
|
+
const memberRemaining = headers['x-member-rate-limit-remaining'];
|
|
359
|
+
const memberLimit = headers['x-member-rate-limit-limit'];
|
|
360
|
+
|
|
361
|
+
// Use the more restrictive limit
|
|
362
|
+
const remaining = Math.min(
|
|
363
|
+
appRemaining ? parseInt(appRemaining) : Infinity,
|
|
364
|
+
memberRemaining ? parseInt(memberRemaining) : Infinity
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const limit = Math.min(
|
|
368
|
+
appLimit ? parseInt(appLimit) : Infinity,
|
|
369
|
+
memberLimit ? parseInt(memberLimit) : Infinity
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
if (remaining !== Infinity && limit !== Infinity) {
|
|
373
|
+
// LinkedIn resets rate limits at the top of each hour
|
|
374
|
+
const now = new Date();
|
|
375
|
+
const reset = new Date(now);
|
|
376
|
+
reset.setHours(reset.getHours() + 1, 0, 0, 0);
|
|
377
|
+
|
|
378
|
+
return { remaining, reset, limit };
|
|
379
|
+
}
|
|
366
380
|
|
|
367
|
-
|
|
368
|
-
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
369
383
|
}
|
|
370
384
|
|
|
371
385
|
/**
|
|
372
386
|
* LinkedIn-specific interfaces
|
|
373
387
|
*/
|
|
374
388
|
export interface LinkedInOrganization {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
389
|
+
urn: string;
|
|
390
|
+
name: string;
|
|
391
|
+
id: string;
|
|
378
392
|
}
|
|
379
393
|
|
|
380
394
|
export interface LinkedInShareData {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
395
|
+
author: string; // URN of the author (person or organization)
|
|
396
|
+
lifecycleState: 'PUBLISHED' | 'DRAFT';
|
|
397
|
+
specificContent: {
|
|
398
|
+
'com.linkedin.ugc.ShareContent': {
|
|
399
|
+
shareCommentary: {
|
|
400
|
+
text: string;
|
|
401
|
+
};
|
|
402
|
+
shareMediaCategory: 'NONE' | 'ARTICLE' | 'IMAGE' | 'VIDEO' | 'RICH';
|
|
403
|
+
media?: Array<{
|
|
404
|
+
status: 'READY';
|
|
405
|
+
media: string; // Asset URN
|
|
406
|
+
title?: {
|
|
407
|
+
text: string;
|
|
408
|
+
};
|
|
409
|
+
description?: {
|
|
410
|
+
text: string;
|
|
411
|
+
};
|
|
412
|
+
}>;
|
|
394
413
|
};
|
|
395
|
-
description?: {
|
|
396
|
-
text: string;
|
|
397
|
-
};
|
|
398
|
-
}>;
|
|
399
414
|
};
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
415
|
+
visibility: {
|
|
416
|
+
'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC' | 'CONNECTIONS' | 'LOGGED_IN' | 'CONTAINER';
|
|
417
|
+
};
|
|
418
|
+
distribution?: {
|
|
419
|
+
linkedInDistributionTarget?: {
|
|
420
|
+
visibleToGuest?: boolean;
|
|
421
|
+
};
|
|
407
422
|
};
|
|
408
|
-
};
|
|
409
423
|
}
|
|
410
424
|
|
|
411
425
|
export interface LinkedInShare {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
426
|
+
id: string;
|
|
427
|
+
author: string;
|
|
428
|
+
created: {
|
|
429
|
+
actor: string;
|
|
430
|
+
time: number;
|
|
431
|
+
};
|
|
432
|
+
firstPublishedAt?: number;
|
|
433
|
+
lastModified?: {
|
|
434
|
+
actor: string;
|
|
435
|
+
time: number;
|
|
436
|
+
};
|
|
437
|
+
lifecycleState: string;
|
|
438
|
+
specificContent: {
|
|
439
|
+
'com.linkedin.ugc.ShareContent': {
|
|
440
|
+
shareCommentary: {
|
|
441
|
+
text: string;
|
|
442
|
+
};
|
|
443
|
+
shareMediaCategory: string;
|
|
444
|
+
media?: Array<{
|
|
445
|
+
media: string;
|
|
446
|
+
title?: {
|
|
447
|
+
text: string;
|
|
448
|
+
};
|
|
449
|
+
}>;
|
|
434
450
|
};
|
|
435
|
-
}>;
|
|
436
451
|
};
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
distribution?: any;
|
|
452
|
+
visibility: {
|
|
453
|
+
'com.linkedin.ugc.MemberNetworkVisibility': string;
|
|
454
|
+
};
|
|
455
|
+
distribution?: any;
|
|
442
456
|
}
|
|
443
457
|
|
|
444
458
|
export interface LinkedInAnalytics {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
459
|
+
totalShareStatistics?: {
|
|
460
|
+
impressionCount: number;
|
|
461
|
+
clickCount: number;
|
|
462
|
+
engagement: number;
|
|
463
|
+
likeCount: number;
|
|
464
|
+
commentCount: number;
|
|
465
|
+
shareCount: number;
|
|
466
|
+
uniqueImpressionsCount: number;
|
|
467
|
+
};
|
|
468
|
+
timeRange?: {
|
|
469
|
+
start: number;
|
|
470
|
+
end: number;
|
|
471
|
+
};
|
|
458
472
|
}
|
|
459
473
|
|
|
460
474
|
export interface LinkedInArticle {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
475
|
+
author: string;
|
|
476
|
+
publishedAt: number;
|
|
477
|
+
coverImage?: string;
|
|
478
|
+
title: string;
|
|
479
|
+
description?: string;
|
|
480
|
+
content: string;
|
|
481
|
+
visibility: string;
|
|
482
|
+
}
|