@memberjunction/actions-bizapps-social 2.111.0 → 2.112.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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +6 -6
- package/dist/base/base-social.action.d.ts.map +1 -1
- package/dist/base/base-social.action.js +18 -24
- 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 +35 -34
- 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 +34 -36
- 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 +25 -27
- 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 +19 -23
- 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 +28 -32
- 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 +42 -44
- 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 +37 -39
- 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 +44 -59
- 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 +33 -31
- 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 +28 -32
- 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 +24 -26
- 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 +32 -34
- 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 +43 -52
- 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 +30 -28
- 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 +18 -20
- 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 +27 -26
- 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 +38 -59
- 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 +23 -25
- 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 +56 -60
- 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 +25 -27
- 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 +55 -45
- 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 +31 -29
- 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 +25 -23
- 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 +32 -30
- 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 +28 -30
- 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 +33 -38
- 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 +25 -26
- 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 +25 -29
- 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 +40 -47
- 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 +30 -31
- 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 +56 -58
- 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 +58 -68
- 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 +22 -25
- package/dist/providers/youtube/youtube-base.action.js.map +1 -1
- package/package.json +5 -6
- package/src/base/base-social.action.ts +217 -224
- package/src/providers/buffer/buffer-base.action.ts +435 -441
- package/src/providers/facebook/actions/boost-post.action.ts +350 -386
- package/src/providers/facebook/actions/create-album.action.ts +291 -307
- package/src/providers/facebook/actions/create-post.action.ts +224 -227
- package/src/providers/facebook/actions/get-page-insights.action.ts +383 -403
- package/src/providers/facebook/actions/get-page-posts.action.ts +214 -225
- package/src/providers/facebook/actions/get-post-insights.action.ts +300 -316
- package/src/providers/facebook/actions/respond-to-comments.action.ts +319 -336
- package/src/providers/facebook/actions/schedule-post.action.ts +289 -292
- package/src/providers/facebook/actions/search-posts.action.ts +399 -413
- package/src/providers/facebook/facebook-base.action.ts +653 -670
- package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
- package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +184 -189
- package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +160 -161
- package/src/providers/hootsuite/actions/get-analytics.action.ts +249 -254
- package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +206 -207
- package/src/providers/hootsuite/actions/get-social-profiles.action.ts +206 -205
- package/src/providers/hootsuite/actions/search-posts.action.ts +351 -369
- package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +211 -209
- package/src/providers/hootsuite/hootsuite-base.action.ts +301 -307
- package/src/providers/instagram/actions/create-post.action.ts +276 -296
- package/src/providers/instagram/actions/create-story.action.ts +378 -394
- package/src/providers/instagram/actions/get-account-insights.action.ts +384 -420
- package/src/providers/instagram/actions/get-business-posts.action.ts +233 -242
- package/src/providers/instagram/actions/get-comments.action.ts +365 -377
- package/src/providers/instagram/actions/get-post-insights.action.ts +265 -273
- package/src/providers/instagram/actions/schedule-post.action.ts +233 -235
- package/src/providers/instagram/actions/search-posts.action.ts +512 -538
- package/src/providers/instagram/instagram-base.action.ts +368 -393
- package/src/providers/linkedin/actions/create-article.action.ts +275 -266
- package/src/providers/linkedin/actions/create-post.action.ts +179 -177
- package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
- package/src/providers/linkedin/actions/get-organization-posts.action.ts +146 -147
- package/src/providers/linkedin/actions/get-personal-posts.action.ts +138 -139
- package/src/providers/linkedin/actions/get-post-analytics.action.ts +190 -189
- package/src/providers/linkedin/actions/schedule-post.action.ts +191 -189
- package/src/providers/linkedin/actions/search-posts.action.ts +275 -283
- package/src/providers/linkedin/linkedin-base.action.ts +407 -421
- package/src/providers/tiktok/tiktok-base.action.ts +305 -320
- package/src/providers/twitter/actions/create-thread.action.ts +203 -207
- package/src/providers/twitter/actions/create-tweet.action.ts +187 -188
- package/src/providers/twitter/actions/delete-tweet.action.ts +128 -129
- package/src/providers/twitter/actions/get-analytics.action.ts +402 -411
- package/src/providers/twitter/actions/get-mentions.action.ts +218 -219
- package/src/providers/twitter/actions/get-timeline.action.ts +232 -233
- package/src/providers/twitter/actions/schedule-tweet.action.ts +221 -222
- package/src/providers/twitter/actions/search-tweets.action.ts +540 -543
- package/src/providers/twitter/twitter-base.action.ts +541 -560
- package/src/providers/youtube/youtube-base.action.ts +320 -333
|
@@ -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/global';
|
|
6
6
|
import { BaseAction } from '@memberjunction/actions';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -12,471 +12,457 @@ import { BaseAction } from '@memberjunction/actions';
|
|
|
12
12
|
*/
|
|
13
13
|
@RegisterClass(BaseAction, 'LinkedInBaseAction')
|
|
14
14
|
export abstract class LinkedInBaseAction extends BaseSocialMediaAction {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
protected get platformName(): string {
|
|
16
|
+
return 'LinkedIn';
|
|
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
|
+
);
|
|
17
78
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
79
|
+
return this._axiosInstance;
|
|
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');
|
|
21
89
|
}
|
|
22
90
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
);
|
|
91
|
+
try {
|
|
92
|
+
const response = await axios.post(
|
|
93
|
+
'https://www.linkedin.com/oauth/v2/accessToken',
|
|
94
|
+
new URLSearchParams({
|
|
95
|
+
grant_type: 'refresh_token',
|
|
96
|
+
refresh_token: refreshToken,
|
|
97
|
+
client_id: this.getCustomAttribute(2) || '', // Client ID stored in CustomAttribute2
|
|
98
|
+
client_secret: this.getCustomAttribute(3) || '', // Client Secret stored in CustomAttribute3
|
|
99
|
+
}).toString(),
|
|
100
|
+
{
|
|
101
|
+
headers: {
|
|
102
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
103
|
+
},
|
|
78
104
|
}
|
|
79
|
-
|
|
80
|
-
}
|
|
105
|
+
);
|
|
81
106
|
|
|
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
|
-
}
|
|
107
|
+
const { access_token, refresh_token: newRefreshToken, expires_in } = response.data;
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
}
|
|
109
|
+
// Update stored tokens
|
|
110
|
+
await this.updateStoredTokens(access_token, newRefreshToken || refreshToken, expires_in);
|
|
121
111
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
}
|
|
112
|
+
LogStatus('LinkedIn access token refreshed successfully');
|
|
113
|
+
} catch (error) {
|
|
114
|
+
LogError(`Failed to refresh LinkedIn access token: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
115
|
+
throw error;
|
|
133
116
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get the authenticated user's profile URN
|
|
121
|
+
*/
|
|
122
|
+
protected async getCurrentUserUrn(): Promise<string> {
|
|
123
|
+
try {
|
|
124
|
+
const response = await this.axiosInstance.get('/me');
|
|
125
|
+
return `urn:li:person:${response.data.id}`;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
LogError(`Failed to get current user URN: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
128
|
+
throw error;
|
|
166
129
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get organizations the user has admin access to
|
|
134
|
+
*/
|
|
135
|
+
protected async getAdminOrganizations(): Promise<LinkedInOrganization[]> {
|
|
136
|
+
try {
|
|
137
|
+
const response = await this.axiosInstance.get('/organizationalEntityAcls', {
|
|
138
|
+
params: {
|
|
139
|
+
q: 'roleAssignee',
|
|
140
|
+
role: 'ADMINISTRATOR',
|
|
141
|
+
projection: '(elements*(*,organizationalTarget~(localizedName)))',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const organizations: LinkedInOrganization[] = [];
|
|
146
|
+
if (response.data.elements) {
|
|
147
|
+
for (const element of response.data.elements) {
|
|
148
|
+
if (element.organizationalTarget) {
|
|
149
|
+
organizations.push({
|
|
150
|
+
urn: element.organizationalTarget,
|
|
151
|
+
name: element['organizationalTarget~']?.localizedName || 'Unknown',
|
|
152
|
+
id: element.organizationalTarget.split(':').pop() || '',
|
|
198
153
|
});
|
|
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(', ')}`);
|
|
154
|
+
}
|
|
221
155
|
}
|
|
156
|
+
}
|
|
222
157
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
158
|
+
return organizations;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
LogError(`Failed to get admin organizations: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
161
|
+
throw error;
|
|
228
162
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Upload media to LinkedIn
|
|
167
|
+
*/
|
|
168
|
+
protected async uploadSingleMedia(file: MediaFile): Promise<string> {
|
|
169
|
+
try {
|
|
170
|
+
// Step 1: Register upload
|
|
171
|
+
const registerResponse = await this.axiosInstance.post('/assets?action=registerUpload', {
|
|
172
|
+
registerUploadRequest: {
|
|
173
|
+
recipes: ['urn:li:digitalmediaRecipe:feedshare-image'],
|
|
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;
|
|
240
202
|
}
|
|
203
|
+
}
|
|
241
204
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
});
|
|
205
|
+
/**
|
|
206
|
+
* Validate media file meets LinkedIn requirements
|
|
207
|
+
*/
|
|
208
|
+
protected validateMediaFile(file: MediaFile): void {
|
|
209
|
+
const supportedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
255
210
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
LogError(`Failed to get shares: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
259
|
-
throw error;
|
|
260
|
-
}
|
|
211
|
+
if (!supportedTypes.includes(file.mimeType)) {
|
|
212
|
+
throw new Error(`Unsupported media type: ${file.mimeType}. Supported types: ${supportedTypes.join(', ')}`);
|
|
261
213
|
}
|
|
262
214
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
};
|
|
215
|
+
// LinkedIn image size limits
|
|
216
|
+
const maxSize = 10 * 1024 * 1024; // 10MB
|
|
217
|
+
if (file.size > maxSize) {
|
|
218
|
+
throw new Error(`File size exceeds limit. Max: ${maxSize / 1024 / 1024}MB, Got: ${file.size / 1024 / 1024}MB`);
|
|
292
219
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
likes: linkedInAnalytics.totalShareStatistics?.likeCount || 0,
|
|
305
|
-
reach: linkedInAnalytics.totalShareStatistics?.uniqueImpressionsCount || 0,
|
|
306
|
-
platformMetrics: linkedInAnalytics
|
|
307
|
-
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create a share (post) on LinkedIn
|
|
224
|
+
*/
|
|
225
|
+
protected async createShare(shareData: LinkedInShareData): Promise<string> {
|
|
226
|
+
try {
|
|
227
|
+
const response = await this.axiosInstance.post('/ugcPosts', shareData);
|
|
228
|
+
return response.data.id;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
this.handleLinkedInError(error as AxiosError);
|
|
308
231
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get shares for a specific author (person or organization)
|
|
236
|
+
*/
|
|
237
|
+
protected async getShares(authorUrn: string, count: number = 50, start: number = 0): Promise<LinkedInShare[]> {
|
|
238
|
+
try {
|
|
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;
|
|
316
252
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Convert LinkedIn share to common format
|
|
257
|
+
*/
|
|
258
|
+
protected normalizePost(linkedInShare: LinkedInShare): SocialPost {
|
|
259
|
+
const publishedAt = new Date(linkedInShare.firstPublishedAt || linkedInShare.created.time);
|
|
260
|
+
|
|
261
|
+
// Extract media URLs
|
|
262
|
+
const mediaUrls: string[] = [];
|
|
263
|
+
if (linkedInShare.specificContent?.['com.linkedin.ugc.ShareContent']?.media) {
|
|
264
|
+
for (const media of linkedInShare.specificContent['com.linkedin.ugc.ShareContent'].media) {
|
|
265
|
+
if (media.media) {
|
|
266
|
+
mediaUrls.push(media.media);
|
|
348
267
|
}
|
|
268
|
+
}
|
|
349
269
|
}
|
|
350
270
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
271
|
+
return {
|
|
272
|
+
id: linkedInShare.id,
|
|
273
|
+
platform: 'LinkedIn',
|
|
274
|
+
profileId: linkedInShare.author,
|
|
275
|
+
content: linkedInShare.specificContent?.['com.linkedin.ugc.ShareContent']?.shareCommentary?.text || '',
|
|
276
|
+
mediaUrls: mediaUrls,
|
|
277
|
+
publishedAt: publishedAt,
|
|
278
|
+
platformSpecificData: {
|
|
279
|
+
lifecycleState: linkedInShare.lifecycleState,
|
|
280
|
+
visibility: linkedInShare.visibility,
|
|
281
|
+
distribution: linkedInShare.distribution,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Normalize LinkedIn analytics to common format
|
|
288
|
+
*/
|
|
289
|
+
protected normalizeAnalytics(linkedInAnalytics: LinkedInAnalytics): SocialAnalytics {
|
|
290
|
+
return {
|
|
291
|
+
impressions: linkedInAnalytics.totalShareStatistics?.impressionCount || 0,
|
|
292
|
+
engagements: linkedInAnalytics.totalShareStatistics?.engagement || 0,
|
|
293
|
+
clicks: linkedInAnalytics.totalShareStatistics?.clickCount || 0,
|
|
294
|
+
shares: linkedInAnalytics.totalShareStatistics?.shareCount || 0,
|
|
295
|
+
comments: linkedInAnalytics.totalShareStatistics?.commentCount || 0,
|
|
296
|
+
likes: linkedInAnalytics.totalShareStatistics?.likeCount || 0,
|
|
297
|
+
reach: linkedInAnalytics.totalShareStatistics?.uniqueImpressionsCount || 0,
|
|
298
|
+
platformMetrics: linkedInAnalytics,
|
|
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 };
|
|
382
365
|
}
|
|
366
|
+
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
383
369
|
}
|
|
384
370
|
|
|
385
371
|
/**
|
|
386
372
|
* LinkedIn-specific interfaces
|
|
387
373
|
*/
|
|
388
374
|
export interface LinkedInOrganization {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
375
|
+
urn: string;
|
|
376
|
+
name: string;
|
|
377
|
+
id: string;
|
|
392
378
|
}
|
|
393
379
|
|
|
394
380
|
export interface LinkedInShareData {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
};
|
|
409
|
-
description?: {
|
|
410
|
-
text: string;
|
|
411
|
-
};
|
|
412
|
-
}>;
|
|
381
|
+
author: string; // URN of the author (person or organization)
|
|
382
|
+
lifecycleState: 'PUBLISHED' | 'DRAFT';
|
|
383
|
+
specificContent: {
|
|
384
|
+
'com.linkedin.ugc.ShareContent': {
|
|
385
|
+
shareCommentary: {
|
|
386
|
+
text: string;
|
|
387
|
+
};
|
|
388
|
+
shareMediaCategory: 'NONE' | 'ARTICLE' | 'IMAGE' | 'VIDEO' | 'RICH';
|
|
389
|
+
media?: Array<{
|
|
390
|
+
status: 'READY';
|
|
391
|
+
media: string; // Asset URN
|
|
392
|
+
title?: {
|
|
393
|
+
text: string;
|
|
413
394
|
};
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC' | 'CONNECTIONS' | 'LOGGED_IN' | 'CONTAINER';
|
|
417
|
-
};
|
|
418
|
-
distribution?: {
|
|
419
|
-
linkedInDistributionTarget?: {
|
|
420
|
-
visibleToGuest?: boolean;
|
|
395
|
+
description?: {
|
|
396
|
+
text: string;
|
|
421
397
|
};
|
|
398
|
+
}>;
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
visibility: {
|
|
402
|
+
'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC' | 'CONNECTIONS' | 'LOGGED_IN' | 'CONTAINER';
|
|
403
|
+
};
|
|
404
|
+
distribution?: {
|
|
405
|
+
linkedInDistributionTarget?: {
|
|
406
|
+
visibleToGuest?: boolean;
|
|
422
407
|
};
|
|
408
|
+
};
|
|
423
409
|
}
|
|
424
410
|
|
|
425
411
|
export interface LinkedInShare {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
};
|
|
449
|
-
}>;
|
|
412
|
+
id: string;
|
|
413
|
+
author: string;
|
|
414
|
+
created: {
|
|
415
|
+
actor: string;
|
|
416
|
+
time: number;
|
|
417
|
+
};
|
|
418
|
+
firstPublishedAt?: number;
|
|
419
|
+
lastModified?: {
|
|
420
|
+
actor: string;
|
|
421
|
+
time: number;
|
|
422
|
+
};
|
|
423
|
+
lifecycleState: string;
|
|
424
|
+
specificContent: {
|
|
425
|
+
'com.linkedin.ugc.ShareContent': {
|
|
426
|
+
shareCommentary: {
|
|
427
|
+
text: string;
|
|
428
|
+
};
|
|
429
|
+
shareMediaCategory: string;
|
|
430
|
+
media?: Array<{
|
|
431
|
+
media: string;
|
|
432
|
+
title?: {
|
|
433
|
+
text: string;
|
|
450
434
|
};
|
|
435
|
+
}>;
|
|
451
436
|
};
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
437
|
+
};
|
|
438
|
+
visibility: {
|
|
439
|
+
'com.linkedin.ugc.MemberNetworkVisibility': string;
|
|
440
|
+
};
|
|
441
|
+
distribution?: any;
|
|
456
442
|
}
|
|
457
443
|
|
|
458
444
|
export interface LinkedInAnalytics {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
445
|
+
totalShareStatistics?: {
|
|
446
|
+
impressionCount: number;
|
|
447
|
+
clickCount: number;
|
|
448
|
+
engagement: number;
|
|
449
|
+
likeCount: number;
|
|
450
|
+
commentCount: number;
|
|
451
|
+
shareCount: number;
|
|
452
|
+
uniqueImpressionsCount: number;
|
|
453
|
+
};
|
|
454
|
+
timeRange?: {
|
|
455
|
+
start: number;
|
|
456
|
+
end: number;
|
|
457
|
+
};
|
|
472
458
|
}
|
|
473
459
|
|
|
474
460
|
export interface LinkedInArticle {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
461
|
+
author: string;
|
|
462
|
+
publishedAt: number;
|
|
463
|
+
coverImage?: string;
|
|
464
|
+
title: string;
|
|
465
|
+
description?: string;
|
|
466
|
+
content: string;
|
|
467
|
+
visibility: string;
|
|
468
|
+
}
|