@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 } 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
|
/**
|
|
@@ -11,337 +11,331 @@ import { BaseAction } from '@memberjunction/actions';
|
|
|
11
11
|
*/
|
|
12
12
|
@RegisterClass(BaseAction, 'HootSuiteBaseAction')
|
|
13
13
|
export abstract class HootSuiteBaseAction extends BaseSocialMediaAction {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
protected get platformName(): string {
|
|
15
|
+
return 'HootSuite';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
protected get apiBaseUrl(): string {
|
|
19
|
+
return 'https://platform.hootsuite.com/v1';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Axios instance for making HTTP requests
|
|
24
|
+
*/
|
|
25
|
+
private _axiosInstance: AxiosInstance | null = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get or create axios instance with interceptors
|
|
29
|
+
*/
|
|
30
|
+
protected get axiosInstance(): AxiosInstance {
|
|
31
|
+
if (!this._axiosInstance) {
|
|
32
|
+
this._axiosInstance = axios.create({
|
|
33
|
+
baseURL: this.apiBaseUrl,
|
|
34
|
+
timeout: 30000,
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
Accept: 'application/json',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Add request interceptor for auth
|
|
42
|
+
this._axiosInstance.interceptors.request.use(
|
|
43
|
+
(config) => {
|
|
44
|
+
const token = this.getAccessToken();
|
|
45
|
+
if (token) {
|
|
46
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
47
|
+
}
|
|
48
|
+
return config;
|
|
49
|
+
},
|
|
50
|
+
(error) => Promise.reject(error)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Add response interceptor for rate limit handling
|
|
54
|
+
this._axiosInstance.interceptors.response.use(
|
|
55
|
+
(response) => {
|
|
56
|
+
// Log rate limit info
|
|
57
|
+
const rateLimitInfo = this.parseRateLimitHeaders(response.headers);
|
|
58
|
+
if (rateLimitInfo) {
|
|
59
|
+
LogStatus(`HootSuite Rate Limit - Remaining: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`);
|
|
60
|
+
}
|
|
61
|
+
return response;
|
|
62
|
+
},
|
|
63
|
+
async (error: AxiosError) => {
|
|
64
|
+
if (error.response?.status === 429) {
|
|
65
|
+
// Rate limit exceeded
|
|
66
|
+
const retryAfter = error.response.headers['retry-after'];
|
|
67
|
+
const waitTime = retryAfter ? parseInt(retryAfter) : 60;
|
|
68
|
+
await this.handleRateLimit(waitTime);
|
|
69
|
+
|
|
70
|
+
// Retry the request
|
|
71
|
+
return this._axiosInstance!.request(error.config!);
|
|
72
|
+
}
|
|
73
|
+
return Promise.reject(error);
|
|
74
|
+
}
|
|
75
|
+
);
|
|
16
76
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
77
|
+
return this._axiosInstance;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Refresh the access token using the refresh token
|
|
82
|
+
*/
|
|
83
|
+
protected async refreshAccessToken(): Promise<void> {
|
|
84
|
+
const refreshToken = this.getRefreshToken();
|
|
85
|
+
if (!refreshToken) {
|
|
86
|
+
throw new Error('No refresh token available for HootSuite');
|
|
20
87
|
}
|
|
21
88
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
headers: {
|
|
36
|
-
'Content-Type': 'application/json',
|
|
37
|
-
'Accept': 'application/json'
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Add request interceptor for auth
|
|
42
|
-
this._axiosInstance.interceptors.request.use(
|
|
43
|
-
(config) => {
|
|
44
|
-
const token = this.getAccessToken();
|
|
45
|
-
if (token) {
|
|
46
|
-
config.headers.Authorization = `Bearer ${token}`;
|
|
47
|
-
}
|
|
48
|
-
return config;
|
|
49
|
-
},
|
|
50
|
-
(error) => Promise.reject(error)
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
// Add response interceptor for rate limit handling
|
|
54
|
-
this._axiosInstance.interceptors.response.use(
|
|
55
|
-
(response) => {
|
|
56
|
-
// Log rate limit info
|
|
57
|
-
const rateLimitInfo = this.parseRateLimitHeaders(response.headers);
|
|
58
|
-
if (rateLimitInfo) {
|
|
59
|
-
LogStatus(`HootSuite Rate Limit - Remaining: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`);
|
|
60
|
-
}
|
|
61
|
-
return response;
|
|
62
|
-
},
|
|
63
|
-
async (error: AxiosError) => {
|
|
64
|
-
if (error.response?.status === 429) {
|
|
65
|
-
// Rate limit exceeded
|
|
66
|
-
const retryAfter = error.response.headers['retry-after'];
|
|
67
|
-
const waitTime = retryAfter ? parseInt(retryAfter) : 60;
|
|
68
|
-
await this.handleRateLimit(waitTime);
|
|
69
|
-
|
|
70
|
-
// Retry the request
|
|
71
|
-
return this._axiosInstance!.request(error.config!);
|
|
72
|
-
}
|
|
73
|
-
return Promise.reject(error);
|
|
74
|
-
}
|
|
75
|
-
);
|
|
89
|
+
try {
|
|
90
|
+
const response = await axios.post(
|
|
91
|
+
'https://platform.hootsuite.com/oauth2/token',
|
|
92
|
+
{
|
|
93
|
+
grant_type: 'refresh_token',
|
|
94
|
+
refresh_token: refreshToken,
|
|
95
|
+
client_id: this.getCustomAttribute(2), // Client ID stored in CustomAttribute2
|
|
96
|
+
client_secret: this.getCustomAttribute(3), // Client Secret stored in CustomAttribute3
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
101
|
+
},
|
|
76
102
|
}
|
|
77
|
-
|
|
78
|
-
}
|
|
103
|
+
);
|
|
79
104
|
|
|
80
|
-
|
|
81
|
-
* Refresh the access token using the refresh token
|
|
82
|
-
*/
|
|
83
|
-
protected async refreshAccessToken(): Promise<void> {
|
|
84
|
-
const refreshToken = this.getRefreshToken();
|
|
85
|
-
if (!refreshToken) {
|
|
86
|
-
throw new Error('No refresh token available for HootSuite');
|
|
87
|
-
}
|
|
105
|
+
const { access_token, refresh_token: newRefreshToken, expires_in } = response.data;
|
|
88
106
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
grant_type: 'refresh_token',
|
|
92
|
-
refresh_token: refreshToken,
|
|
93
|
-
client_id: this.getCustomAttribute(2), // Client ID stored in CustomAttribute2
|
|
94
|
-
client_secret: this.getCustomAttribute(3) // Client Secret stored in CustomAttribute3
|
|
95
|
-
}, {
|
|
96
|
-
headers: {
|
|
97
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const { access_token, refresh_token: newRefreshToken, expires_in } = response.data;
|
|
102
|
-
|
|
103
|
-
// Update stored tokens
|
|
104
|
-
await this.updateStoredTokens(
|
|
105
|
-
access_token,
|
|
106
|
-
newRefreshToken || refreshToken,
|
|
107
|
-
expires_in
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
LogStatus('HootSuite access token refreshed successfully');
|
|
111
|
-
} catch (error) {
|
|
112
|
-
LogError(`Failed to refresh HootSuite access token: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
113
|
-
throw error;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
107
|
+
// Update stored tokens
|
|
108
|
+
await this.updateStoredTokens(access_token, newRefreshToken || refreshToken, expires_in);
|
|
116
109
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
// First, request an upload URL
|
|
123
|
-
const uploadRequest = await this.axiosInstance.post('/media', {
|
|
124
|
-
mimeType: file.mimeType,
|
|
125
|
-
sizeBytes: file.size
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const { uploadUrl, mediaId } = uploadRequest.data;
|
|
129
|
-
|
|
130
|
-
// Upload the file to the provided URL
|
|
131
|
-
const fileData = typeof file.data === 'string'
|
|
132
|
-
? Buffer.from(file.data, 'base64')
|
|
133
|
-
: file.data;
|
|
134
|
-
|
|
135
|
-
await axios.put(uploadUrl, fileData, {
|
|
136
|
-
headers: {
|
|
137
|
-
'Content-Type': file.mimeType,
|
|
138
|
-
'Content-Length': file.size.toString()
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// Wait for processing
|
|
143
|
-
await this.waitForMediaProcessing(mediaId);
|
|
144
|
-
|
|
145
|
-
return mediaId;
|
|
146
|
-
} catch (error) {
|
|
147
|
-
LogError(`Failed to upload media to HootSuite: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
148
|
-
throw error;
|
|
149
|
-
}
|
|
110
|
+
LogStatus('HootSuite access token refreshed successfully');
|
|
111
|
+
} catch (error) {
|
|
112
|
+
LogError(`Failed to refresh HootSuite access token: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
113
|
+
throw error;
|
|
150
114
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Upload media to HootSuite
|
|
119
|
+
*/
|
|
120
|
+
protected async uploadSingleMedia(file: MediaFile): Promise<string> {
|
|
121
|
+
try {
|
|
122
|
+
// First, request an upload URL
|
|
123
|
+
const uploadRequest = await this.axiosInstance.post('/media', {
|
|
124
|
+
mimeType: file.mimeType,
|
|
125
|
+
sizeBytes: file.size,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const { uploadUrl, mediaId } = uploadRequest.data;
|
|
129
|
+
|
|
130
|
+
// Upload the file to the provided URL
|
|
131
|
+
const fileData = typeof file.data === 'string' ? Buffer.from(file.data, 'base64') : file.data;
|
|
132
|
+
|
|
133
|
+
await axios.put(uploadUrl, fileData, {
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': file.mimeType,
|
|
136
|
+
'Content-Length': file.size.toString(),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Wait for processing
|
|
141
|
+
await this.waitForMediaProcessing(mediaId);
|
|
142
|
+
|
|
143
|
+
return mediaId;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
LogError(`Failed to upload media to HootSuite: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
146
|
+
throw error;
|
|
175
147
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Wait for media to finish processing
|
|
152
|
+
*/
|
|
153
|
+
private async waitForMediaProcessing(mediaId: string, maxAttempts: number = 30): Promise<void> {
|
|
154
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
155
|
+
try {
|
|
156
|
+
const response = await this.axiosInstance.get(`/media/${mediaId}`);
|
|
157
|
+
const { state } = response.data;
|
|
158
|
+
|
|
159
|
+
if (state === 'READY') {
|
|
160
|
+
return;
|
|
161
|
+
} else if (state === 'FAILED') {
|
|
162
|
+
throw new Error('Media processing failed');
|
|
187
163
|
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Make a paginated request to HootSuite API
|
|
192
|
-
*/
|
|
193
|
-
protected async makePaginatedRequest<T>(
|
|
194
|
-
endpoint: string,
|
|
195
|
-
params: Record<string, any> = {}
|
|
196
|
-
): Promise<T[]> {
|
|
197
|
-
const results: T[] = [];
|
|
198
|
-
let cursor: string | null = null;
|
|
199
|
-
const limit = params.limit || 50;
|
|
200
|
-
|
|
201
|
-
do {
|
|
202
|
-
const queryParams: any = { ...params, limit };
|
|
203
|
-
if (cursor) {
|
|
204
|
-
queryParams.cursor = cursor;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const response = await this.axiosInstance.get(endpoint, { params: queryParams });
|
|
208
|
-
const data = response.data;
|
|
209
|
-
|
|
210
|
-
if (data.data && Array.isArray(data.data)) {
|
|
211
|
-
results.push(...data.data);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
cursor = data.cursor || null;
|
|
215
164
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
} while (cursor);
|
|
222
|
-
|
|
223
|
-
return results;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Format date for HootSuite API (ISO 8601)
|
|
228
|
-
*/
|
|
229
|
-
protected formatHootSuiteDate(date: Date | string): string {
|
|
230
|
-
if (typeof date === 'string') {
|
|
231
|
-
date = new Date(date);
|
|
165
|
+
// Wait 2 seconds before next check
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (i === maxAttempts - 1) {
|
|
169
|
+
throw new Error(`Media processing timeout for ${mediaId}`);
|
|
232
170
|
}
|
|
233
|
-
|
|
171
|
+
}
|
|
234
172
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return {
|
|
248
|
-
id: hootsuitePost.id,
|
|
249
|
-
platform: 'HootSuite',
|
|
250
|
-
profileId: hootsuitePost.socialProfileIds.join(','), // Multiple profiles possible
|
|
251
|
-
content: hootsuitePost.text,
|
|
252
|
-
mediaUrls: hootsuitePost.mediaIds || [],
|
|
253
|
-
publishedAt: this.parseHootSuiteDate(hootsuitePost.createdTime),
|
|
254
|
-
scheduledFor: hootsuitePost.scheduledTime ? this.parseHootSuiteDate(hootsuitePost.scheduledTime) : undefined,
|
|
255
|
-
platformSpecificData: {
|
|
256
|
-
state: hootsuitePost.state,
|
|
257
|
-
tags: hootsuitePost.tags,
|
|
258
|
-
location: hootsuitePost.location,
|
|
259
|
-
socialProfileIds: hootsuitePost.socialProfileIds
|
|
260
|
-
}
|
|
261
|
-
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get social profiles for the authenticated user
|
|
177
|
+
*/
|
|
178
|
+
protected async getSocialProfiles(): Promise<HootSuiteProfile[]> {
|
|
179
|
+
try {
|
|
180
|
+
const response = await this.axiosInstance.get('/socialProfiles');
|
|
181
|
+
return response.data.data || [];
|
|
182
|
+
} catch (error) {
|
|
183
|
+
LogError(`Failed to get social profiles: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
184
|
+
throw error;
|
|
262
185
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Make a paginated request to HootSuite API
|
|
190
|
+
*/
|
|
191
|
+
protected async makePaginatedRequest<T>(endpoint: string, params: Record<string, any> = {}): Promise<T[]> {
|
|
192
|
+
const results: T[] = [];
|
|
193
|
+
let cursor: string | null = null;
|
|
194
|
+
const limit = params.limit || 50;
|
|
195
|
+
|
|
196
|
+
do {
|
|
197
|
+
const queryParams: any = { ...params, limit };
|
|
198
|
+
if (cursor) {
|
|
199
|
+
queryParams.cursor = cursor;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const response = await this.axiosInstance.get(endpoint, { params: queryParams });
|
|
203
|
+
const data = response.data;
|
|
204
|
+
|
|
205
|
+
if (data.data && Array.isArray(data.data)) {
|
|
206
|
+
results.push(...data.data);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
cursor = data.cursor || null;
|
|
210
|
+
|
|
211
|
+
// Check if we've reached the desired number of results
|
|
212
|
+
if (params.maxResults && results.length >= params.maxResults) {
|
|
213
|
+
return results.slice(0, params.maxResults);
|
|
214
|
+
}
|
|
215
|
+
} while (cursor);
|
|
216
|
+
|
|
217
|
+
return results;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Format date for HootSuite API (ISO 8601)
|
|
222
|
+
*/
|
|
223
|
+
protected formatHootSuiteDate(date: Date | string): string {
|
|
224
|
+
if (typeof date === 'string') {
|
|
225
|
+
date = new Date(date);
|
|
270
226
|
}
|
|
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
|
-
|
|
227
|
+
return date.toISOString();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Parse HootSuite date string
|
|
232
|
+
*/
|
|
233
|
+
protected parseHootSuiteDate(dateString: string): Date {
|
|
234
|
+
return new Date(dateString);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Convert HootSuite post to common format
|
|
239
|
+
*/
|
|
240
|
+
protected normalizePost(hootsuitePost: HootSuitePost): SocialPost {
|
|
241
|
+
return {
|
|
242
|
+
id: hootsuitePost.id,
|
|
243
|
+
platform: 'HootSuite',
|
|
244
|
+
profileId: hootsuitePost.socialProfileIds.join(','), // Multiple profiles possible
|
|
245
|
+
content: hootsuitePost.text,
|
|
246
|
+
mediaUrls: hootsuitePost.mediaIds || [],
|
|
247
|
+
publishedAt: this.parseHootSuiteDate(hootsuitePost.createdTime),
|
|
248
|
+
scheduledFor: hootsuitePost.scheduledTime ? this.parseHootSuiteDate(hootsuitePost.scheduledTime) : undefined,
|
|
249
|
+
platformSpecificData: {
|
|
250
|
+
state: hootsuitePost.state,
|
|
251
|
+
tags: hootsuitePost.tags,
|
|
252
|
+
location: hootsuitePost.location,
|
|
253
|
+
socialProfileIds: hootsuitePost.socialProfileIds,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Search for posts - implemented in search action
|
|
260
|
+
*/
|
|
261
|
+
protected async searchPosts(params: SearchParams): Promise<SocialPost[]> {
|
|
262
|
+
// This is implemented in the search-posts.action.ts
|
|
263
|
+
throw new Error('Search posts is implemented in HootSuiteSearchPostsAction');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Handle HootSuite-specific errors
|
|
268
|
+
*/
|
|
269
|
+
protected handleHootSuiteError(error: AxiosError): never {
|
|
270
|
+
if (error.response) {
|
|
271
|
+
const { status, data } = error.response;
|
|
272
|
+
const errorData = data as any;
|
|
273
|
+
|
|
274
|
+
switch (status) {
|
|
275
|
+
case 400:
|
|
276
|
+
throw new Error(`Bad Request: ${errorData.message || 'Invalid request parameters'}`);
|
|
277
|
+
case 401:
|
|
278
|
+
throw new Error('Unauthorized: Invalid or expired access token');
|
|
279
|
+
case 403:
|
|
280
|
+
throw new Error('Forbidden: Insufficient permissions');
|
|
281
|
+
case 404:
|
|
282
|
+
throw new Error('Not Found: Resource does not exist');
|
|
283
|
+
case 429:
|
|
284
|
+
throw new Error('Rate Limit Exceeded: Too many requests');
|
|
285
|
+
case 500:
|
|
286
|
+
throw new Error('Internal Server Error: HootSuite service error');
|
|
287
|
+
default:
|
|
288
|
+
throw new Error(`HootSuite API Error (${status}): ${errorData.message || 'Unknown error'}`);
|
|
289
|
+
}
|
|
290
|
+
} else if (error.request) {
|
|
291
|
+
throw new Error('Network Error: No response from HootSuite');
|
|
292
|
+
} else {
|
|
293
|
+
throw new Error(`Request Error: ${error.message}`);
|
|
301
294
|
}
|
|
295
|
+
}
|
|
302
296
|
}
|
|
303
297
|
|
|
304
298
|
/**
|
|
305
299
|
* HootSuite-specific interfaces
|
|
306
300
|
*/
|
|
307
301
|
export interface HootSuiteProfile {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
302
|
+
id: string;
|
|
303
|
+
socialNetworkId: string;
|
|
304
|
+
socialNetworkUserId: string;
|
|
305
|
+
avatarUrl: string;
|
|
306
|
+
displayName: string;
|
|
307
|
+
type: string;
|
|
308
|
+
ownerId: string;
|
|
315
309
|
}
|
|
316
310
|
|
|
317
311
|
export interface HootSuitePost {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
312
|
+
id: string;
|
|
313
|
+
socialProfileIds: string[];
|
|
314
|
+
text: string;
|
|
315
|
+
scheduledTime?: string;
|
|
316
|
+
createdTime: string;
|
|
317
|
+
state: 'SCHEDULED' | 'PUBLISHED' | 'FAILED' | 'DRAFT';
|
|
318
|
+
mediaIds?: string[];
|
|
319
|
+
tags?: string[];
|
|
320
|
+
location?: {
|
|
321
|
+
latitude: number;
|
|
322
|
+
longitude: number;
|
|
323
|
+
};
|
|
330
324
|
}
|
|
331
325
|
|
|
332
326
|
export interface HootSuiteAnalytics {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
327
|
+
postId: string;
|
|
328
|
+
metrics: {
|
|
329
|
+
likes: number;
|
|
330
|
+
comments: number;
|
|
331
|
+
shares: number;
|
|
332
|
+
clicks: number;
|
|
333
|
+
impressions: number;
|
|
334
|
+
engagements: number;
|
|
335
|
+
reach: number;
|
|
336
|
+
};
|
|
337
|
+
period: {
|
|
338
|
+
start: string;
|
|
339
|
+
end: string;
|
|
340
|
+
};
|
|
341
|
+
}
|