@meltwater/conversations-api-services 1.1.4 → 1.1.6
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/dist/cjs/data-access/http/threads.native.js +272 -65
- package/dist/cjs/lib/externalId.helpers.js +1 -1
- package/dist/esm/data-access/http/threads.native.js +266 -64
- package/dist/esm/lib/externalId.helpers.js +1 -1
- package/package.json +1 -1
- package/src/data-access/http/threads.native.js +313 -114
- package/src/lib/externalId.helpers.js +1 -1
|
@@ -3,90 +3,297 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.getInsights = getInsights;
|
|
7
|
+
exports.getProfile = getProfile;
|
|
8
|
+
exports.manageReply = manageReply;
|
|
9
|
+
exports.quote = quote;
|
|
10
|
+
exports.reply = reply;
|
|
11
|
+
exports.repost = repost;
|
|
7
12
|
var _superagent = _interopRequireDefault(require("superagent"));
|
|
8
13
|
var _externalIdHelpers = require("../../lib/externalId.helpers.js");
|
|
9
14
|
var _loggerHelpers = require("../../lib/logger.helpers.js");
|
|
10
15
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
16
|
const THREADS_URL = 'https://graph.threads.net/v1.0';
|
|
12
17
|
const THREADS_PUBLISH_URL = `${THREADS_URL}/me/threads_publish`;
|
|
18
|
+
const MAX_RETRY_COUNT = 11;
|
|
19
|
+
const SHORT_WAIT_TIME_MS = 10000; // 10 seconds
|
|
20
|
+
const LONG_WAIT_TIME_MS = 60000; // 60 seconds
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Utility function to retry an asynchronous operation with exponential backoff.
|
|
24
|
+
* @param {Function} operation - The async operation to retry.
|
|
25
|
+
* @param {number} maxRetries - Maximum number of retries.
|
|
26
|
+
* @param {number} initialWaitTime - Initial wait time in milliseconds.
|
|
27
|
+
* @param {Function} logger - Logger instance for logging retries.
|
|
28
|
+
* @returns {Promise<any>} The result of the operation.
|
|
29
|
+
* @throws {Error} If the operation fails after all retries.
|
|
30
|
+
*/
|
|
31
|
+
async function retryWithBackoff(operation, maxRetries, initialWaitTime, logger) {
|
|
32
|
+
let waitTime = initialWaitTime;
|
|
33
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
34
|
+
try {
|
|
35
|
+
return await operation();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (attempt === maxRetries) {
|
|
38
|
+
throw error; // Rethrow error if max retries are reached
|
|
39
|
+
}
|
|
40
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Retry attempt ${attempt + 1} failed. Retrying in ${waitTime / 1000} seconds...`);
|
|
41
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
42
|
+
waitTime *= 2; // Exponential backoff
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Confirms the creation ID by polling the Threads API until the status is no longer "IN_PROGRESS".
|
|
49
|
+
* @param {string} accessToken - The access token for authentication.
|
|
50
|
+
* @param {string} creationId - The ID of the creation process to confirm.
|
|
51
|
+
* @param {Object} logger - Logger instance for logging.
|
|
52
|
+
* @returns {Promise<Object>} The API response when the status is no longer "IN_PROGRESS".
|
|
53
|
+
* @throws {Error} If the status remains "IN_PROGRESS" after all retries.
|
|
54
|
+
*/
|
|
13
55
|
async function confirmCreationId(accessToken, creationId, logger) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
(0, _loggerHelpers.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
56
|
+
const operation = async () => {
|
|
57
|
+
const response = await _superagent.default.get(`${THREADS_URL}/${creationId}`).set('Accept', 'application/json').set('Content-Type', 'application/json').query({
|
|
58
|
+
access_token: accessToken,
|
|
59
|
+
fields: 'id,status,error_message'
|
|
60
|
+
}).send();
|
|
61
|
+
if (response.body.status === 'IN_PROGRESS') {
|
|
62
|
+
throw new Error('Status is still IN_PROGRESS');
|
|
63
|
+
}
|
|
64
|
+
(0, _loggerHelpers.loggerDebug)(logger, 'Threads Response status', response.status);
|
|
65
|
+
return response;
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
return await retryWithBackoff(operation, MAX_RETRY_COUNT, SHORT_WAIT_TIME_MS, logger);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
(0, _loggerHelpers.loggerError)(logger, `Creation ID ${creationId} confirmation failed: ${error.message}`, error);
|
|
71
|
+
error.code = 408; // Request Timeout
|
|
28
72
|
throw error;
|
|
29
73
|
}
|
|
30
|
-
(0, _loggerHelpers.loggerDebug)(logger, 'Threads Response status', response.status);
|
|
31
|
-
return response;
|
|
32
74
|
}
|
|
33
|
-
|
|
34
|
-
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sends a GET request to the Threads API.
|
|
78
|
+
* @param {string} apiUrl - The API URL to send the request to.
|
|
79
|
+
* @param {Object} params - Query parameters for the request.
|
|
80
|
+
* @param {Object} logger - Logger instance for logging.
|
|
81
|
+
* @returns {Promise<Object>} The API response.
|
|
82
|
+
* @throws {Error} If the request fails.
|
|
83
|
+
*/
|
|
84
|
+
async function sendRequest(apiUrl, params, logger) {
|
|
35
85
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
86
|
+
const response = await _superagent.default.get(apiUrl).set('Accept', 'application/json').set('Content-Type', 'application/json').query(params).send();
|
|
87
|
+
if (response.status !== 200) {
|
|
88
|
+
throw new Error(`Unexpected response status: ${response.status}`);
|
|
89
|
+
}
|
|
90
|
+
(0, _loggerHelpers.loggerDebug)(logger, 'Threads Response status', response.status);
|
|
91
|
+
return response;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const errorMessage = err?.response?.body?.error?.message || err.message;
|
|
94
|
+
(0, _loggerHelpers.loggerError)(logger, `Failed to call Threads API: ${errorMessage}`, err);
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sends a POST request to the Threads API.
|
|
101
|
+
* @param {string} apiUrl - The API URL to send the request to.
|
|
102
|
+
* @param {Object} params - Query parameters for the request.
|
|
103
|
+
* @param {Object} logger - Logger instance for logging.
|
|
104
|
+
* @returns {Promise<Object>} The API response.
|
|
105
|
+
* @throws {Error} If the request fails.
|
|
106
|
+
*/
|
|
107
|
+
async function requestApi(apiUrl, params, logger) {
|
|
108
|
+
try {
|
|
109
|
+
const response = await _superagent.default.post(apiUrl).set('Accept', 'application/json').set('Content-Type', 'application/json').query(params).send();
|
|
110
|
+
if (response.status !== 200) {
|
|
111
|
+
throw new Error(`Unexpected response status: ${response.status}`);
|
|
112
|
+
}
|
|
113
|
+
(0, _loggerHelpers.loggerDebug)(logger, 'Threads Response status', response.status);
|
|
114
|
+
return response;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const errorMessage = err?.response?.body?.error?.message || err.message;
|
|
117
|
+
(0, _loggerHelpers.loggerError)(logger, `Failed to call Threads API: ${errorMessage}`, err);
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Publishes a post to Threads.
|
|
124
|
+
* @param {string} accessToken - The access token for authentication.
|
|
125
|
+
* @param {string} text - The text content of the post.
|
|
126
|
+
* @param {Object} asset - The media asset to attach to the post.
|
|
127
|
+
* @param {Object} logger - Logger instance for logging.
|
|
128
|
+
* @returns {Promise<Object>} The API response.
|
|
129
|
+
*/
|
|
130
|
+
async function reply(accessToken, inReplyToId, text, asset, logger) {
|
|
131
|
+
const query = await constructReplyQuery(accessToken, inReplyToId, text, asset, logger);
|
|
132
|
+
let response = await requestApi(THREADS_PUBLISH_URL, query, logger);
|
|
133
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Native Threads API Publish reply Response`, {
|
|
134
|
+
responseBody: JSON.stringify(response.body)
|
|
135
|
+
});
|
|
136
|
+
return response.body;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Publishes a quote post to Threads.
|
|
141
|
+
* @param {string} accessToken - The access token for authentication.
|
|
142
|
+
* @param {string} inReplyToId - The ID of the post to quote.
|
|
143
|
+
* @param {string} text - The text content of the quote.
|
|
144
|
+
* @param {Object} asset - The media asset to attach to the quote.
|
|
145
|
+
* @param {Object} logger - Logger instance for logging.
|
|
146
|
+
* @returns {Promise<Object>} The API response.
|
|
147
|
+
*/
|
|
148
|
+
async function quote(accessToken, inReplyToId, text, asset, logger) {
|
|
149
|
+
const query = await constructReplyQuery(accessToken, inReplyToId, text, asset, logger, true);
|
|
150
|
+
let response = await requestApi(THREADS_PUBLISH_URL, query, logger);
|
|
151
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Native Threads API Publish quote Response`, {
|
|
152
|
+
responseBody: JSON.stringify(response.body)
|
|
153
|
+
});
|
|
154
|
+
return response.body;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Publishes a post to Threads.
|
|
159
|
+
* @param {string} accessToken - The access token for authentication.
|
|
160
|
+
* @param {string} text - The text content of the post.
|
|
161
|
+
* @param {Object} asset - The media asset to attach to the post.
|
|
162
|
+
* @param {Object} logger - Logger instance for logging.
|
|
163
|
+
* @returns {Promise<Object>} The API response.
|
|
164
|
+
*/
|
|
165
|
+
async function repost(token, externalId, logger) {
|
|
166
|
+
const postId = (0, _externalIdHelpers.removePrefix)(externalId);
|
|
167
|
+
let response = await requestApi(`${THREADS_URL}/${postId}/repost`, {
|
|
168
|
+
access_token: token
|
|
169
|
+
}, logger);
|
|
170
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Native Threads API repost Response`, {
|
|
171
|
+
responseBody: JSON.stringify(response.body)
|
|
172
|
+
});
|
|
173
|
+
return response.body;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Retrieves the profile information of a user.
|
|
178
|
+
* @param {string} token - The access token for authentication.
|
|
179
|
+
* @param {string} externalId - The external ID of the user.
|
|
180
|
+
* @param {string} fields - The fields to retrieve from the profile.
|
|
181
|
+
* @param {Object} logger - Logger instance for logging.
|
|
182
|
+
* @returns {Promise<Object>} The API response containing the profile information.
|
|
183
|
+
*/
|
|
184
|
+
async function getProfile(token, externalId, fields, logger) {
|
|
185
|
+
const userId = (0, _externalIdHelpers.removePrefix)(externalId);
|
|
186
|
+
let response = await sendRequest(`${THREADS_URL}/${userId}?fields=${fields}`, {
|
|
187
|
+
access_token: token
|
|
188
|
+
}, logger);
|
|
189
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Native Threads API getProfile Response`, {
|
|
190
|
+
responseBody: JSON.stringify(response.body)
|
|
191
|
+
});
|
|
192
|
+
return response.body;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Retrieves the insights of a post.
|
|
197
|
+
* @param {string} token - The access token for authentication.
|
|
198
|
+
* @param {string} externalId - The external ID of the post.
|
|
199
|
+
* @param {string} metric - The metric to retrieve insights for.
|
|
200
|
+
* @param {Object} logger - Logger instance for logging.
|
|
201
|
+
* @returns {Promise<Object>} The API response containing the insights.
|
|
202
|
+
*/
|
|
203
|
+
async function getInsights(token, externalId, metric, logger) {
|
|
204
|
+
const mediaId = (0, _externalIdHelpers.removePrefix)(externalId);
|
|
205
|
+
let response = await sendRequest(`${THREADS_URL}/${mediaId}/insights`, {
|
|
206
|
+
access_token: token,
|
|
207
|
+
metric
|
|
208
|
+
}, logger);
|
|
209
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Native Threads API getInsights Response`, {
|
|
210
|
+
responseBody: JSON.stringify(response.body)
|
|
211
|
+
});
|
|
212
|
+
return response.body;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Manages the visibility of a reply.
|
|
217
|
+
* @param {string} token - The access token for authentication.
|
|
218
|
+
* @param {string} externalId - The external ID of the reply.
|
|
219
|
+
* @param {boolean} hide - Whether to hide the reply.
|
|
220
|
+
* @param {Object} logger - Logger instance for logging.
|
|
221
|
+
* @returns {Promise<Object>} The API response.
|
|
222
|
+
*/
|
|
223
|
+
async function manageReply(token, externalId) {
|
|
224
|
+
let hide = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
225
|
+
let logger = arguments.length > 3 ? arguments[3] : undefined;
|
|
226
|
+
const replyId = (0, _externalIdHelpers.removePrefix)(externalId);
|
|
227
|
+
let response = await requestApi(`${THREADS_URL}/${replyId}/manage_reply`, {
|
|
228
|
+
access_token: token,
|
|
229
|
+
hide
|
|
230
|
+
}, logger);
|
|
231
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Native Threads API hideReply Response`, {
|
|
232
|
+
responseBody: JSON.stringify(response.body)
|
|
233
|
+
});
|
|
234
|
+
return response.body;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Constructs the query parameters for replying to a post.
|
|
239
|
+
* @param {string} accessToken - The access token for authentication.
|
|
240
|
+
* @param {string} inReplyToId - The ID of the post to reply to.
|
|
241
|
+
* @param {string} text - The text content of the reply.
|
|
242
|
+
* @param {Object} asset - The media asset to attach to the reply.
|
|
243
|
+
* @param {Object} logger - Logger instance for logging.
|
|
244
|
+
* @param {boolean} isQuote - Whether the reply is a quote.
|
|
245
|
+
* @returns {Promise<Object>} The constructed query parameters.
|
|
246
|
+
*/
|
|
247
|
+
async function constructReplyQuery(accessToken, inReplyToId, text, asset, logger) {
|
|
248
|
+
let isQuote = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
|
|
249
|
+
const baseQuery = {
|
|
250
|
+
access_token: accessToken,
|
|
251
|
+
text,
|
|
252
|
+
[isQuote ? 'quote_post_id' : 'in_reply_to_id']: inReplyToId
|
|
253
|
+
};
|
|
254
|
+
const mediaQuery = getMediaQuery(asset);
|
|
255
|
+
const query = {
|
|
256
|
+
...baseQuery,
|
|
257
|
+
...mediaQuery
|
|
258
|
+
};
|
|
259
|
+
try {
|
|
260
|
+
const containerResponse = await _superagent.default.post(`${THREADS_URL}/me/threads`).set('Accept', 'application/json').set('Content-Type', 'application/json').query(query).send();
|
|
261
|
+
await confirmCreationId(accessToken, containerResponse.body.id, logger);
|
|
262
|
+
return {
|
|
263
|
+
...query,
|
|
264
|
+
containerResponse
|
|
40
265
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
266
|
+
} catch (error) {
|
|
267
|
+
(0, _loggerHelpers.loggerError)(logger, 'Error constructing reply query', error);
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Constructs the media query parameters based on the asset type.
|
|
274
|
+
* @param {Object} asset - The media asset to attach to the post.
|
|
275
|
+
* @returns {Object} The constructed media query parameters.
|
|
276
|
+
*/
|
|
277
|
+
function getMediaQuery(asset) {
|
|
278
|
+
if (!asset) {
|
|
279
|
+
return {
|
|
280
|
+
media_type: 'TEXT'
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
switch (asset.type) {
|
|
284
|
+
case 'image':
|
|
285
|
+
return {
|
|
49
286
|
media_type: 'IMAGE',
|
|
50
287
|
image_url: asset.url,
|
|
51
288
|
alt_text: asset.altText ?? ''
|
|
52
289
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
...query,
|
|
290
|
+
case 'video':
|
|
291
|
+
return {
|
|
56
292
|
media_type: 'VIDEO',
|
|
57
293
|
video_url: asset.url,
|
|
58
294
|
alt_text: asset.altText ?? ''
|
|
59
295
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
await confirmCreationId(accessToken, containerResponse.body.id, logger);
|
|
63
|
-
response = await _superagent.default.post(THREADS_PUBLISH_URL).set('Accept', 'application/json').set('Content-Type', 'application/json').query({
|
|
64
|
-
access_token: accessToken,
|
|
65
|
-
creation_id: containerResponse.body.id
|
|
66
|
-
}).send();
|
|
67
|
-
} catch (err) {
|
|
68
|
-
if (err && err.response && err.response.body && err.response.body.error) {
|
|
69
|
-
(0, _loggerHelpers.loggerError)(logger, `Failed to call threads api: ${err.response.body.error.message}`);
|
|
70
|
-
} else {
|
|
71
|
-
(0, _loggerHelpers.loggerError)(logger, `Failed to call threads api`, err);
|
|
72
|
-
}
|
|
73
|
-
throw err;
|
|
74
|
-
}
|
|
75
|
-
if (response.status !== 200) {
|
|
76
|
-
(0, _loggerHelpers.loggerError)(logger, `Failed to call threads api`, {
|
|
77
|
-
responseBody: JSON.stringify(response.body)
|
|
78
|
-
});
|
|
79
|
-
let error = new Error(`Failed to call threads api`);
|
|
80
|
-
error.code = response.status;
|
|
81
|
-
throw error;
|
|
296
|
+
default:
|
|
297
|
+
throw new Error(`Unsupported asset type: ${asset.type}`);
|
|
82
298
|
}
|
|
83
|
-
(0, _loggerHelpers.loggerDebug)(logger, 'Threads Response status', response.status);
|
|
84
|
-
return response;
|
|
85
|
-
}
|
|
86
|
-
async function comment(accessToken, inReplyToId, text, asset, logger) {
|
|
87
|
-
let response = await requestApi(`${THREADS_URL}/me/threads`, accessToken, (0, _externalIdHelpers.removePrefix)(inReplyToId), text, asset, logger);
|
|
88
|
-
(0, _loggerHelpers.loggerInfo)(logger, `Native Threads API Publish Comment Response`, {
|
|
89
|
-
responseBody: JSON.stringify(response.body)
|
|
90
|
-
});
|
|
91
|
-
return response.body;
|
|
92
299
|
}
|
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.removePrefix = removePrefix;
|
|
7
7
|
exports.twitterPrefixCheck = twitterPrefixCheck;
|
|
8
8
|
function removePrefix(document) {
|
|
9
|
-
return document && document.replace(/id\:[a-zA-Z]+\.com:/g, '');
|
|
9
|
+
return document && document.replace(/id\:[a-zA-Z]+\.(com|net):/g, '');
|
|
10
10
|
}
|
|
11
11
|
function twitterPrefixCheck(socialOriginType, value) {
|
|
12
12
|
let result = value;
|
|
@@ -3,83 +3,285 @@ import { removePrefix } from '../../lib/externalId.helpers.js';
|
|
|
3
3
|
import { loggerDebug, loggerError, loggerInfo } from '../../lib/logger.helpers.js';
|
|
4
4
|
const THREADS_URL = 'https://graph.threads.net/v1.0';
|
|
5
5
|
const THREADS_PUBLISH_URL = `${THREADS_URL}/me/threads_publish`;
|
|
6
|
+
const MAX_RETRY_COUNT = 11;
|
|
7
|
+
const SHORT_WAIT_TIME_MS = 10000; // 10 seconds
|
|
8
|
+
const LONG_WAIT_TIME_MS = 60000; // 60 seconds
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Utility function to retry an asynchronous operation with exponential backoff.
|
|
12
|
+
* @param {Function} operation - The async operation to retry.
|
|
13
|
+
* @param {number} maxRetries - Maximum number of retries.
|
|
14
|
+
* @param {number} initialWaitTime - Initial wait time in milliseconds.
|
|
15
|
+
* @param {Function} logger - Logger instance for logging retries.
|
|
16
|
+
* @returns {Promise<any>} The result of the operation.
|
|
17
|
+
* @throws {Error} If the operation fails after all retries.
|
|
18
|
+
*/
|
|
19
|
+
async function retryWithBackoff(operation, maxRetries, initialWaitTime, logger) {
|
|
20
|
+
let waitTime = initialWaitTime;
|
|
21
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
return await operation();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (attempt === maxRetries) {
|
|
26
|
+
throw error; // Rethrow error if max retries are reached
|
|
27
|
+
}
|
|
28
|
+
loggerInfo(logger, `Retry attempt ${attempt + 1} failed. Retrying in ${waitTime / 1000} seconds...`);
|
|
29
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
30
|
+
waitTime *= 2; // Exponential backoff
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Confirms the creation ID by polling the Threads API until the status is no longer "IN_PROGRESS".
|
|
37
|
+
* @param {string} accessToken - The access token for authentication.
|
|
38
|
+
* @param {string} creationId - The ID of the creation process to confirm.
|
|
39
|
+
* @param {Object} logger - Logger instance for logging.
|
|
40
|
+
* @returns {Promise<Object>} The API response when the status is no longer "IN_PROGRESS".
|
|
41
|
+
* @throws {Error} If the status remains "IN_PROGRESS" after all retries.
|
|
42
|
+
*/
|
|
6
43
|
async function confirmCreationId(accessToken, creationId, logger) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
const operation = async () => {
|
|
45
|
+
const response = await superagent.get(`${THREADS_URL}/${creationId}`).set('Accept', 'application/json').set('Content-Type', 'application/json').query({
|
|
46
|
+
access_token: accessToken,
|
|
47
|
+
fields: 'id,status,error_message'
|
|
48
|
+
}).send();
|
|
49
|
+
if (response.body.status === 'IN_PROGRESS') {
|
|
50
|
+
throw new Error('Status is still IN_PROGRESS');
|
|
51
|
+
}
|
|
52
|
+
loggerDebug(logger, 'Threads Response status', response.status);
|
|
53
|
+
return response;
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
return await retryWithBackoff(operation, MAX_RETRY_COUNT, SHORT_WAIT_TIME_MS, logger);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
loggerError(logger, `Creation ID ${creationId} confirmation failed: ${error.message}`, error);
|
|
59
|
+
error.code = 408; // Request Timeout
|
|
21
60
|
throw error;
|
|
22
61
|
}
|
|
23
|
-
loggerDebug(logger, 'Threads Response status', response.status);
|
|
24
|
-
return response;
|
|
25
62
|
}
|
|
26
|
-
|
|
27
|
-
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sends a GET request to the Threads API.
|
|
66
|
+
* @param {string} apiUrl - The API URL to send the request to.
|
|
67
|
+
* @param {Object} params - Query parameters for the request.
|
|
68
|
+
* @param {Object} logger - Logger instance for logging.
|
|
69
|
+
* @returns {Promise<Object>} The API response.
|
|
70
|
+
* @throws {Error} If the request fails.
|
|
71
|
+
*/
|
|
72
|
+
async function sendRequest(apiUrl, params, logger) {
|
|
28
73
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
74
|
+
const response = await superagent.get(apiUrl).set('Accept', 'application/json').set('Content-Type', 'application/json').query(params).send();
|
|
75
|
+
if (response.status !== 200) {
|
|
76
|
+
throw new Error(`Unexpected response status: ${response.status}`);
|
|
77
|
+
}
|
|
78
|
+
loggerDebug(logger, 'Threads Response status', response.status);
|
|
79
|
+
return response;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const errorMessage = err?.response?.body?.error?.message || err.message;
|
|
82
|
+
loggerError(logger, `Failed to call Threads API: ${errorMessage}`, err);
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Sends a POST request to the Threads API.
|
|
89
|
+
* @param {string} apiUrl - The API URL to send the request to.
|
|
90
|
+
* @param {Object} params - Query parameters for the request.
|
|
91
|
+
* @param {Object} logger - Logger instance for logging.
|
|
92
|
+
* @returns {Promise<Object>} The API response.
|
|
93
|
+
* @throws {Error} If the request fails.
|
|
94
|
+
*/
|
|
95
|
+
async function requestApi(apiUrl, params, logger) {
|
|
96
|
+
try {
|
|
97
|
+
const response = await superagent.post(apiUrl).set('Accept', 'application/json').set('Content-Type', 'application/json').query(params).send();
|
|
98
|
+
if (response.status !== 200) {
|
|
99
|
+
throw new Error(`Unexpected response status: ${response.status}`);
|
|
100
|
+
}
|
|
101
|
+
loggerDebug(logger, 'Threads Response status', response.status);
|
|
102
|
+
return response;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const errorMessage = err?.response?.body?.error?.message || err.message;
|
|
105
|
+
loggerError(logger, `Failed to call Threads API: ${errorMessage}`, err);
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Publishes a post to Threads.
|
|
112
|
+
* @param {string} accessToken - The access token for authentication.
|
|
113
|
+
* @param {string} text - The text content of the post.
|
|
114
|
+
* @param {Object} asset - The media asset to attach to the post.
|
|
115
|
+
* @param {Object} logger - Logger instance for logging.
|
|
116
|
+
* @returns {Promise<Object>} The API response.
|
|
117
|
+
*/
|
|
118
|
+
export async function reply(accessToken, inReplyToId, text, asset, logger) {
|
|
119
|
+
const query = await constructReplyQuery(accessToken, inReplyToId, text, asset, logger);
|
|
120
|
+
let response = await requestApi(THREADS_PUBLISH_URL, query, logger);
|
|
121
|
+
loggerInfo(logger, `Native Threads API Publish reply Response`, {
|
|
122
|
+
responseBody: JSON.stringify(response.body)
|
|
123
|
+
});
|
|
124
|
+
return response.body;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Publishes a quote post to Threads.
|
|
129
|
+
* @param {string} accessToken - The access token for authentication.
|
|
130
|
+
* @param {string} inReplyToId - The ID of the post to quote.
|
|
131
|
+
* @param {string} text - The text content of the quote.
|
|
132
|
+
* @param {Object} asset - The media asset to attach to the quote.
|
|
133
|
+
* @param {Object} logger - Logger instance for logging.
|
|
134
|
+
* @returns {Promise<Object>} The API response.
|
|
135
|
+
*/
|
|
136
|
+
export async function quote(accessToken, inReplyToId, text, asset, logger) {
|
|
137
|
+
const query = await constructReplyQuery(accessToken, inReplyToId, text, asset, logger, true);
|
|
138
|
+
let response = await requestApi(THREADS_PUBLISH_URL, query, logger);
|
|
139
|
+
loggerInfo(logger, `Native Threads API Publish quote Response`, {
|
|
140
|
+
responseBody: JSON.stringify(response.body)
|
|
141
|
+
});
|
|
142
|
+
return response.body;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Publishes a post to Threads.
|
|
147
|
+
* @param {string} accessToken - The access token for authentication.
|
|
148
|
+
* @param {string} text - The text content of the post.
|
|
149
|
+
* @param {Object} asset - The media asset to attach to the post.
|
|
150
|
+
* @param {Object} logger - Logger instance for logging.
|
|
151
|
+
* @returns {Promise<Object>} The API response.
|
|
152
|
+
*/
|
|
153
|
+
export async function repost(token, externalId, logger) {
|
|
154
|
+
const postId = removePrefix(externalId);
|
|
155
|
+
let response = await requestApi(`${THREADS_URL}/${postId}/repost`, {
|
|
156
|
+
access_token: token
|
|
157
|
+
}, logger);
|
|
158
|
+
loggerInfo(logger, `Native Threads API repost Response`, {
|
|
159
|
+
responseBody: JSON.stringify(response.body)
|
|
160
|
+
});
|
|
161
|
+
return response.body;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Retrieves the profile information of a user.
|
|
166
|
+
* @param {string} token - The access token for authentication.
|
|
167
|
+
* @param {string} externalId - The external ID of the user.
|
|
168
|
+
* @param {string} fields - The fields to retrieve from the profile.
|
|
169
|
+
* @param {Object} logger - Logger instance for logging.
|
|
170
|
+
* @returns {Promise<Object>} The API response containing the profile information.
|
|
171
|
+
*/
|
|
172
|
+
export async function getProfile(token, externalId, fields, logger) {
|
|
173
|
+
const userId = removePrefix(externalId);
|
|
174
|
+
let response = await sendRequest(`${THREADS_URL}/${userId}?fields=${fields}`, {
|
|
175
|
+
access_token: token
|
|
176
|
+
}, logger);
|
|
177
|
+
loggerInfo(logger, `Native Threads API getProfile Response`, {
|
|
178
|
+
responseBody: JSON.stringify(response.body)
|
|
179
|
+
});
|
|
180
|
+
return response.body;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Retrieves the insights of a post.
|
|
185
|
+
* @param {string} token - The access token for authentication.
|
|
186
|
+
* @param {string} externalId - The external ID of the post.
|
|
187
|
+
* @param {string} metric - The metric to retrieve insights for.
|
|
188
|
+
* @param {Object} logger - Logger instance for logging.
|
|
189
|
+
* @returns {Promise<Object>} The API response containing the insights.
|
|
190
|
+
*/
|
|
191
|
+
export async function getInsights(token, externalId, metric, logger) {
|
|
192
|
+
const mediaId = removePrefix(externalId);
|
|
193
|
+
let response = await sendRequest(`${THREADS_URL}/${mediaId}/insights`, {
|
|
194
|
+
access_token: token,
|
|
195
|
+
metric
|
|
196
|
+
}, logger);
|
|
197
|
+
loggerInfo(logger, `Native Threads API getInsights Response`, {
|
|
198
|
+
responseBody: JSON.stringify(response.body)
|
|
199
|
+
});
|
|
200
|
+
return response.body;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Manages the visibility of a reply.
|
|
205
|
+
* @param {string} token - The access token for authentication.
|
|
206
|
+
* @param {string} externalId - The external ID of the reply.
|
|
207
|
+
* @param {boolean} hide - Whether to hide the reply.
|
|
208
|
+
* @param {Object} logger - Logger instance for logging.
|
|
209
|
+
* @returns {Promise<Object>} The API response.
|
|
210
|
+
*/
|
|
211
|
+
export async function manageReply(token, externalId) {
|
|
212
|
+
let hide = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
213
|
+
let logger = arguments.length > 3 ? arguments[3] : undefined;
|
|
214
|
+
const replyId = removePrefix(externalId);
|
|
215
|
+
let response = await requestApi(`${THREADS_URL}/${replyId}/manage_reply`, {
|
|
216
|
+
access_token: token,
|
|
217
|
+
hide
|
|
218
|
+
}, logger);
|
|
219
|
+
loggerInfo(logger, `Native Threads API hideReply Response`, {
|
|
220
|
+
responseBody: JSON.stringify(response.body)
|
|
221
|
+
});
|
|
222
|
+
return response.body;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Constructs the query parameters for replying to a post.
|
|
227
|
+
* @param {string} accessToken - The access token for authentication.
|
|
228
|
+
* @param {string} inReplyToId - The ID of the post to reply to.
|
|
229
|
+
* @param {string} text - The text content of the reply.
|
|
230
|
+
* @param {Object} asset - The media asset to attach to the reply.
|
|
231
|
+
* @param {Object} logger - Logger instance for logging.
|
|
232
|
+
* @param {boolean} isQuote - Whether the reply is a quote.
|
|
233
|
+
* @returns {Promise<Object>} The constructed query parameters.
|
|
234
|
+
*/
|
|
235
|
+
async function constructReplyQuery(accessToken, inReplyToId, text, asset, logger) {
|
|
236
|
+
let isQuote = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
|
|
237
|
+
const baseQuery = {
|
|
238
|
+
access_token: accessToken,
|
|
239
|
+
text,
|
|
240
|
+
[isQuote ? 'quote_post_id' : 'in_reply_to_id']: inReplyToId
|
|
241
|
+
};
|
|
242
|
+
const mediaQuery = getMediaQuery(asset);
|
|
243
|
+
const query = {
|
|
244
|
+
...baseQuery,
|
|
245
|
+
...mediaQuery
|
|
246
|
+
};
|
|
247
|
+
try {
|
|
248
|
+
const containerResponse = await superagent.post(`${THREADS_URL}/me/threads`).set('Accept', 'application/json').set('Content-Type', 'application/json').query(query).send();
|
|
249
|
+
await confirmCreationId(accessToken, containerResponse.body.id, logger);
|
|
250
|
+
return {
|
|
251
|
+
...query,
|
|
252
|
+
containerResponse
|
|
33
253
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
254
|
+
} catch (error) {
|
|
255
|
+
loggerError(logger, 'Error constructing reply query', error);
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Constructs the media query parameters based on the asset type.
|
|
262
|
+
* @param {Object} asset - The media asset to attach to the post.
|
|
263
|
+
* @returns {Object} The constructed media query parameters.
|
|
264
|
+
*/
|
|
265
|
+
function getMediaQuery(asset) {
|
|
266
|
+
if (!asset) {
|
|
267
|
+
return {
|
|
268
|
+
media_type: 'TEXT'
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
switch (asset.type) {
|
|
272
|
+
case 'image':
|
|
273
|
+
return {
|
|
42
274
|
media_type: 'IMAGE',
|
|
43
275
|
image_url: asset.url,
|
|
44
276
|
alt_text: asset.altText ?? ''
|
|
45
277
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
...query,
|
|
278
|
+
case 'video':
|
|
279
|
+
return {
|
|
49
280
|
media_type: 'VIDEO',
|
|
50
281
|
video_url: asset.url,
|
|
51
282
|
alt_text: asset.altText ?? ''
|
|
52
283
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await confirmCreationId(accessToken, containerResponse.body.id, logger);
|
|
56
|
-
response = await superagent.post(THREADS_PUBLISH_URL).set('Accept', 'application/json').set('Content-Type', 'application/json').query({
|
|
57
|
-
access_token: accessToken,
|
|
58
|
-
creation_id: containerResponse.body.id
|
|
59
|
-
}).send();
|
|
60
|
-
} catch (err) {
|
|
61
|
-
if (err && err.response && err.response.body && err.response.body.error) {
|
|
62
|
-
loggerError(logger, `Failed to call threads api: ${err.response.body.error.message}`);
|
|
63
|
-
} else {
|
|
64
|
-
loggerError(logger, `Failed to call threads api`, err);
|
|
65
|
-
}
|
|
66
|
-
throw err;
|
|
67
|
-
}
|
|
68
|
-
if (response.status !== 200) {
|
|
69
|
-
loggerError(logger, `Failed to call threads api`, {
|
|
70
|
-
responseBody: JSON.stringify(response.body)
|
|
71
|
-
});
|
|
72
|
-
let error = new Error(`Failed to call threads api`);
|
|
73
|
-
error.code = response.status;
|
|
74
|
-
throw error;
|
|
284
|
+
default:
|
|
285
|
+
throw new Error(`Unsupported asset type: ${asset.type}`);
|
|
75
286
|
}
|
|
76
|
-
loggerDebug(logger, 'Threads Response status', response.status);
|
|
77
|
-
return response;
|
|
78
|
-
}
|
|
79
|
-
export async function comment(accessToken, inReplyToId, text, asset, logger) {
|
|
80
|
-
let response = await requestApi(`${THREADS_URL}/me/threads`, accessToken, removePrefix(inReplyToId), text, asset, logger);
|
|
81
|
-
loggerInfo(logger, `Native Threads API Publish Comment Response`, {
|
|
82
|
-
responseBody: JSON.stringify(response.body)
|
|
83
|
-
});
|
|
84
|
-
return response.body;
|
|
85
287
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function removePrefix(document) {
|
|
2
|
-
return document && document.replace(/id\:[a-zA-Z]+\.com:/g, '');
|
|
2
|
+
return document && document.replace(/id\:[a-zA-Z]+\.(com|net):/g, '');
|
|
3
3
|
}
|
|
4
4
|
export function twitterPrefixCheck(socialOriginType, value) {
|
|
5
5
|
let result = value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meltwater/conversations-api-services",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "Repository to contain all conversations api services shared across our services",
|
|
5
5
|
"main": "dist/cjs/data-access/index.js",
|
|
6
6
|
"module": "dist/esm/data-access/index.js",
|
|
@@ -1,152 +1,351 @@
|
|
|
1
1
|
import superagent from 'superagent';
|
|
2
2
|
import { removePrefix } from '../../lib/externalId.helpers.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
loggerDebug,
|
|
5
|
+
loggerError,
|
|
6
|
+
loggerInfo,
|
|
7
|
+
} from '../../lib/logger.helpers.js';
|
|
5
8
|
|
|
6
9
|
const THREADS_URL = 'https://graph.threads.net/v1.0';
|
|
7
10
|
const THREADS_PUBLISH_URL = `${THREADS_URL}/me/threads_publish`;
|
|
8
11
|
|
|
12
|
+
const MAX_RETRY_COUNT = 11;
|
|
13
|
+
const SHORT_WAIT_TIME_MS = 10000; // 10 seconds
|
|
14
|
+
const LONG_WAIT_TIME_MS = 60000; // 60 seconds
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.send();
|
|
22
|
-
if(response.body.status == 'IN_PROGRESS' && retry < 11){
|
|
23
|
-
// small wait for first attempt, sometimes its just a text post that doesn't take long
|
|
24
|
-
loggerInfo(logger,
|
|
25
|
-
`Creation ID is in progress ${creationId} retry ${retry} Waiting ${retry ? 60 : 10} seconds`
|
|
26
|
-
);
|
|
27
|
-
await new Promise((resolve) => setTimeout(resolve, retry ? 60000 : 10000));
|
|
28
|
-
return await confirmCreationId(accessToken, creationId, logger, retry + 1);
|
|
29
|
-
}else if(response.body.status == 'IN_PROGRESS' && retry == 11){
|
|
30
|
-
let error = new Error(
|
|
31
|
-
`Creation ID is in progress BUT TAKING TOO LONG ${creationId}`
|
|
32
|
-
);
|
|
33
|
-
error.code = response.status;
|
|
34
|
-
throw error
|
|
35
|
-
}
|
|
16
|
+
/**
|
|
17
|
+
* Utility function to retry an asynchronous operation with exponential backoff.
|
|
18
|
+
* @param {Function} operation - The async operation to retry.
|
|
19
|
+
* @param {number} maxRetries - Maximum number of retries.
|
|
20
|
+
* @param {number} initialWaitTime - Initial wait time in milliseconds.
|
|
21
|
+
* @param {Function} logger - Logger instance for logging retries.
|
|
22
|
+
* @returns {Promise<any>} The result of the operation.
|
|
23
|
+
* @throws {Error} If the operation fails after all retries.
|
|
24
|
+
*/
|
|
25
|
+
async function retryWithBackoff(operation, maxRetries, initialWaitTime, logger) {
|
|
26
|
+
let waitTime = initialWaitTime;
|
|
36
27
|
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
29
|
+
try {
|
|
30
|
+
return await operation();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (attempt === maxRetries) {
|
|
33
|
+
throw error; // Rethrow error if max retries are reached
|
|
34
|
+
}
|
|
39
35
|
|
|
40
|
-
|
|
36
|
+
loggerInfo(
|
|
37
|
+
logger,
|
|
38
|
+
`Retry attempt ${attempt + 1} failed. Retrying in ${waitTime / 1000} seconds...`
|
|
39
|
+
);
|
|
40
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
41
|
+
waitTime *= 2; // Exponential backoff
|
|
42
|
+
}
|
|
43
|
+
}
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Confirms the creation ID by polling the Threads API until the status is no longer "IN_PROGRESS".
|
|
48
|
+
* @param {string} accessToken - The access token for authentication.
|
|
49
|
+
* @param {string} creationId - The ID of the creation process to confirm.
|
|
50
|
+
* @param {Object} logger - Logger instance for logging.
|
|
51
|
+
* @returns {Promise<Object>} The API response when the status is no longer "IN_PROGRESS".
|
|
52
|
+
* @throws {Error} If the status remains "IN_PROGRESS" after all retries.
|
|
53
|
+
*/
|
|
54
|
+
async function confirmCreationId(accessToken, creationId, logger) {
|
|
55
|
+
const operation = async () => {
|
|
56
|
+
const response = await superagent
|
|
57
|
+
.get(`${THREADS_URL}/${creationId}`)
|
|
58
|
+
.set('Accept', 'application/json')
|
|
59
|
+
.set('Content-Type', 'application/json')
|
|
60
|
+
.query({
|
|
61
|
+
access_token: accessToken,
|
|
62
|
+
fields: 'id,status,error_message',
|
|
63
|
+
})
|
|
64
|
+
.send();
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
) {
|
|
52
|
-
let response = {};
|
|
66
|
+
if (response.body.status === 'IN_PROGRESS') {
|
|
67
|
+
throw new Error('Status is still IN_PROGRESS');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
loggerDebug(logger, 'Threads Response status', response.status);
|
|
71
|
+
return response;
|
|
72
|
+
};
|
|
53
73
|
|
|
54
74
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
query = {
|
|
63
|
-
...query,
|
|
64
|
-
media_type: 'TEXT',
|
|
65
|
-
};
|
|
66
|
-
} else if (asset.type === 'image') {
|
|
67
|
-
query = {
|
|
68
|
-
...query,
|
|
69
|
-
media_type: 'IMAGE',
|
|
70
|
-
image_url: asset.url,
|
|
71
|
-
alt_text: asset.altText ?? '',
|
|
72
|
-
}
|
|
73
|
-
} else if (asset.type === 'video') {
|
|
74
|
-
query = {
|
|
75
|
-
...query,
|
|
76
|
-
media_type: 'VIDEO',
|
|
77
|
-
video_url: asset.url,
|
|
78
|
-
alt_text: asset.altText ?? '',
|
|
79
|
-
}
|
|
80
|
-
}
|
|
75
|
+
return await retryWithBackoff(operation, MAX_RETRY_COUNT, SHORT_WAIT_TIME_MS, logger);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
loggerError(logger, `Creation ID ${creationId} confirmation failed: ${error.message}`, error);
|
|
78
|
+
error.code = 408; // Request Timeout
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Sends a GET request to the Threads API.
|
|
85
|
+
* @param {string} apiUrl - The API URL to send the request to.
|
|
86
|
+
* @param {Object} params - Query parameters for the request.
|
|
87
|
+
* @param {Object} logger - Logger instance for logging.
|
|
88
|
+
* @returns {Promise<Object>} The API response.
|
|
89
|
+
* @throws {Error} If the request fails.
|
|
90
|
+
*/
|
|
91
|
+
async function sendRequest(apiUrl, params, logger) {
|
|
92
|
+
try {
|
|
93
|
+
const response = await superagent
|
|
94
|
+
.get(apiUrl)
|
|
84
95
|
.set('Accept', 'application/json')
|
|
85
96
|
.set('Content-Type', 'application/json')
|
|
86
|
-
.query(
|
|
97
|
+
.query(params)
|
|
87
98
|
.send();
|
|
88
|
-
|
|
89
|
-
response
|
|
90
|
-
.
|
|
99
|
+
|
|
100
|
+
if (response.status !== 200) {
|
|
101
|
+
throw new Error(`Unexpected response status: ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
loggerDebug(logger, 'Threads Response status', response.status);
|
|
105
|
+
return response;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const errorMessage = err?.response?.body?.error?.message || err.message;
|
|
108
|
+
loggerError(logger, `Failed to call Threads API: ${errorMessage}`, err);
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Sends a POST request to the Threads API.
|
|
115
|
+
* @param {string} apiUrl - The API URL to send the request to.
|
|
116
|
+
* @param {Object} params - Query parameters for the request.
|
|
117
|
+
* @param {Object} logger - Logger instance for logging.
|
|
118
|
+
* @returns {Promise<Object>} The API response.
|
|
119
|
+
* @throws {Error} If the request fails.
|
|
120
|
+
*/
|
|
121
|
+
async function requestApi(apiUrl, params, logger) {
|
|
122
|
+
try {
|
|
123
|
+
const response = await superagent
|
|
124
|
+
.post(apiUrl)
|
|
91
125
|
.set('Accept', 'application/json')
|
|
92
126
|
.set('Content-Type', 'application/json')
|
|
93
|
-
.query(
|
|
94
|
-
access_token: accessToken,
|
|
95
|
-
creation_id: containerResponse.body.id,
|
|
96
|
-
})
|
|
127
|
+
.query(params)
|
|
97
128
|
.send();
|
|
98
129
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
err &&
|
|
102
|
-
err.response &&
|
|
103
|
-
err.response.body &&
|
|
104
|
-
err.response.body.error
|
|
105
|
-
) {
|
|
106
|
-
loggerError(logger,
|
|
107
|
-
`Failed to call threads api: ${err.response.body.error.message}`
|
|
108
|
-
);
|
|
109
|
-
} else {
|
|
110
|
-
loggerError(logger,
|
|
111
|
-
`Failed to call threads api`,
|
|
112
|
-
err
|
|
113
|
-
);
|
|
130
|
+
if (response.status !== 200) {
|
|
131
|
+
throw new Error(`Unexpected response status: ${response.status}`);
|
|
114
132
|
}
|
|
133
|
+
|
|
134
|
+
loggerDebug(logger, 'Threads Response status', response.status);
|
|
135
|
+
return response;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
const errorMessage = err?.response?.body?.error?.message || err.message;
|
|
138
|
+
loggerError(logger, `Failed to call Threads API: ${errorMessage}`, err);
|
|
115
139
|
throw err;
|
|
116
140
|
}
|
|
141
|
+
}
|
|
117
142
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Publishes a post to Threads.
|
|
145
|
+
* @param {string} accessToken - The access token for authentication.
|
|
146
|
+
* @param {string} text - The text content of the post.
|
|
147
|
+
* @param {Object} asset - The media asset to attach to the post.
|
|
148
|
+
* @param {Object} logger - Logger instance for logging.
|
|
149
|
+
* @returns {Promise<Object>} The API response.
|
|
150
|
+
*/
|
|
151
|
+
export async function reply(accessToken, inReplyToId, text, asset, logger) {
|
|
152
|
+
const query = await constructReplyQuery(
|
|
153
|
+
accessToken,
|
|
154
|
+
inReplyToId,
|
|
155
|
+
text,
|
|
156
|
+
asset,
|
|
157
|
+
logger
|
|
158
|
+
);
|
|
159
|
+
let response = await requestApi(THREADS_PUBLISH_URL, query, logger);
|
|
131
160
|
|
|
132
|
-
|
|
133
|
-
|
|
161
|
+
loggerInfo(logger, `Native Threads API Publish reply Response`, {
|
|
162
|
+
responseBody: JSON.stringify(response.body),
|
|
163
|
+
});
|
|
134
164
|
|
|
165
|
+
return response.body;
|
|
166
|
+
}
|
|
135
167
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Publishes a quote post to Threads.
|
|
170
|
+
* @param {string} accessToken - The access token for authentication.
|
|
171
|
+
* @param {string} inReplyToId - The ID of the post to quote.
|
|
172
|
+
* @param {string} text - The text content of the quote.
|
|
173
|
+
* @param {Object} asset - The media asset to attach to the quote.
|
|
174
|
+
* @param {Object} logger - Logger instance for logging.
|
|
175
|
+
* @returns {Promise<Object>} The API response.
|
|
176
|
+
*/
|
|
177
|
+
export async function quote(accessToken, inReplyToId, text, asset, logger) {
|
|
178
|
+
const query = await constructReplyQuery(
|
|
139
179
|
accessToken,
|
|
140
|
-
|
|
180
|
+
inReplyToId,
|
|
141
181
|
text,
|
|
142
182
|
asset,
|
|
183
|
+
logger,
|
|
184
|
+
true
|
|
185
|
+
);
|
|
186
|
+
let response = await requestApi(THREADS_PUBLISH_URL, query, logger);
|
|
187
|
+
loggerInfo(logger, `Native Threads API Publish quote Response`, {
|
|
188
|
+
responseBody: JSON.stringify(response.body),
|
|
189
|
+
});
|
|
190
|
+
return response.body;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Publishes a post to Threads.
|
|
195
|
+
* @param {string} accessToken - The access token for authentication.
|
|
196
|
+
* @param {string} text - The text content of the post.
|
|
197
|
+
* @param {Object} asset - The media asset to attach to the post.
|
|
198
|
+
* @param {Object} logger - Logger instance for logging.
|
|
199
|
+
* @returns {Promise<Object>} The API response.
|
|
200
|
+
*/
|
|
201
|
+
export async function repost(token, externalId, logger) {
|
|
202
|
+
const postId = removePrefix(externalId);
|
|
203
|
+
let response = await requestApi(
|
|
204
|
+
`${THREADS_URL}/${postId}/repost`,
|
|
205
|
+
{ access_token: token },
|
|
143
206
|
logger
|
|
144
207
|
);
|
|
145
208
|
|
|
146
|
-
loggerInfo(logger,
|
|
147
|
-
|
|
148
|
-
|
|
209
|
+
loggerInfo(logger, `Native Threads API repost Response`, {
|
|
210
|
+
responseBody: JSON.stringify(response.body),
|
|
211
|
+
});
|
|
212
|
+
return response.body;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Retrieves the profile information of a user.
|
|
217
|
+
* @param {string} token - The access token for authentication.
|
|
218
|
+
* @param {string} externalId - The external ID of the user.
|
|
219
|
+
* @param {string} fields - The fields to retrieve from the profile.
|
|
220
|
+
* @param {Object} logger - Logger instance for logging.
|
|
221
|
+
* @returns {Promise<Object>} The API response containing the profile information.
|
|
222
|
+
*/
|
|
223
|
+
export async function getProfile(token, externalId, fields, logger) {
|
|
224
|
+
const userId = removePrefix(externalId);
|
|
225
|
+
let response = await sendRequest(
|
|
226
|
+
`${THREADS_URL}/${userId}?fields=${fields}`,
|
|
227
|
+
{ access_token: token },
|
|
228
|
+
logger
|
|
149
229
|
);
|
|
150
230
|
|
|
231
|
+
loggerInfo(logger, `Native Threads API getProfile Response`, {
|
|
232
|
+
responseBody: JSON.stringify(response.body),
|
|
233
|
+
});
|
|
151
234
|
return response.body;
|
|
152
|
-
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Retrieves the insights of a post.
|
|
239
|
+
* @param {string} token - The access token for authentication.
|
|
240
|
+
* @param {string} externalId - The external ID of the post.
|
|
241
|
+
* @param {string} metric - The metric to retrieve insights for.
|
|
242
|
+
* @param {Object} logger - Logger instance for logging.
|
|
243
|
+
* @returns {Promise<Object>} The API response containing the insights.
|
|
244
|
+
*/
|
|
245
|
+
export async function getInsights(token, externalId, metric, logger) {
|
|
246
|
+
const mediaId = removePrefix(externalId);
|
|
247
|
+
let response = await sendRequest(
|
|
248
|
+
`${THREADS_URL}/${mediaId}/insights`,
|
|
249
|
+
{ access_token: token, metric },
|
|
250
|
+
logger
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
loggerInfo(logger, `Native Threads API getInsights Response`, {
|
|
254
|
+
responseBody: JSON.stringify(response.body),
|
|
255
|
+
});
|
|
256
|
+
return response.body;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Manages the visibility of a reply.
|
|
261
|
+
* @param {string} token - The access token for authentication.
|
|
262
|
+
* @param {string} externalId - The external ID of the reply.
|
|
263
|
+
* @param {boolean} hide - Whether to hide the reply.
|
|
264
|
+
* @param {Object} logger - Logger instance for logging.
|
|
265
|
+
* @returns {Promise<Object>} The API response.
|
|
266
|
+
*/
|
|
267
|
+
export async function manageReply(token, externalId, hide = false, logger) {
|
|
268
|
+
const replyId = removePrefix(externalId);
|
|
269
|
+
let response = await requestApi(
|
|
270
|
+
`${THREADS_URL}/${replyId}/manage_reply`,
|
|
271
|
+
{ access_token: token, hide },
|
|
272
|
+
logger
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
loggerInfo(logger, `Native Threads API hideReply Response`, {
|
|
276
|
+
responseBody: JSON.stringify(response.body),
|
|
277
|
+
});
|
|
278
|
+
return response.body;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Constructs the query parameters for replying to a post.
|
|
283
|
+
* @param {string} accessToken - The access token for authentication.
|
|
284
|
+
* @param {string} inReplyToId - The ID of the post to reply to.
|
|
285
|
+
* @param {string} text - The text content of the reply.
|
|
286
|
+
* @param {Object} asset - The media asset to attach to the reply.
|
|
287
|
+
* @param {Object} logger - Logger instance for logging.
|
|
288
|
+
* @param {boolean} isQuote - Whether the reply is a quote.
|
|
289
|
+
* @returns {Promise<Object>} The constructed query parameters.
|
|
290
|
+
*/
|
|
291
|
+
async function constructReplyQuery(
|
|
292
|
+
accessToken,
|
|
293
|
+
inReplyToId,
|
|
294
|
+
text,
|
|
295
|
+
asset,
|
|
296
|
+
logger,
|
|
297
|
+
isQuote = false
|
|
298
|
+
) {
|
|
299
|
+
const baseQuery = {
|
|
300
|
+
access_token: accessToken,
|
|
301
|
+
text,
|
|
302
|
+
[isQuote ? 'quote_post_id' : 'in_reply_to_id']: inReplyToId,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const mediaQuery = getMediaQuery(asset);
|
|
306
|
+
const query = { ...baseQuery, ...mediaQuery };
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const containerResponse = await superagent
|
|
310
|
+
.post(`${THREADS_URL}/me/threads`)
|
|
311
|
+
.set('Accept', 'application/json')
|
|
312
|
+
.set('Content-Type', 'application/json')
|
|
313
|
+
.query(query)
|
|
314
|
+
.send();
|
|
315
|
+
|
|
316
|
+
await confirmCreationId(accessToken, containerResponse.body.id, logger);
|
|
317
|
+
|
|
318
|
+
return { ...query, containerResponse };
|
|
319
|
+
} catch (error) {
|
|
320
|
+
loggerError(logger, 'Error constructing reply query', error);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Constructs the media query parameters based on the asset type.
|
|
327
|
+
* @param {Object} asset - The media asset to attach to the post.
|
|
328
|
+
* @returns {Object} The constructed media query parameters.
|
|
329
|
+
*/
|
|
330
|
+
function getMediaQuery(asset) {
|
|
331
|
+
if (!asset) {
|
|
332
|
+
return { media_type: 'TEXT' };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
switch (asset.type) {
|
|
336
|
+
case 'image':
|
|
337
|
+
return {
|
|
338
|
+
media_type: 'IMAGE',
|
|
339
|
+
image_url: asset.url,
|
|
340
|
+
alt_text: asset.altText ?? '',
|
|
341
|
+
};
|
|
342
|
+
case 'video':
|
|
343
|
+
return {
|
|
344
|
+
media_type: 'VIDEO',
|
|
345
|
+
video_url: asset.url,
|
|
346
|
+
alt_text: asset.altText ?? '',
|
|
347
|
+
};
|
|
348
|
+
default:
|
|
349
|
+
throw new Error(`Unsupported asset type: ${asset.type}`);
|
|
350
|
+
}
|
|
351
|
+
}
|