@meltwater/conversations-api-services 1.1.5 → 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.
@@ -5,6 +5,8 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.getInsights = getInsights;
7
7
  exports.getProfile = getProfile;
8
+ exports.manageReply = manageReply;
9
+ exports.quote = quote;
8
10
  exports.reply = reply;
9
11
  exports.repost = repost;
10
12
  var _superagent = _interopRequireDefault(require("superagent"));
@@ -17,25 +19,68 @@ const MAX_RETRY_COUNT = 11;
17
19
  const SHORT_WAIT_TIME_MS = 10000; // 10 seconds
18
20
  const LONG_WAIT_TIME_MS = 60000; // 60 seconds
19
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
+ */
20
55
  async function confirmCreationId(accessToken, creationId, logger) {
21
- for (let retry = 0; retry <= MAX_RETRY_COUNT; retry++) {
56
+ const operation = async () => {
22
57
  const response = await _superagent.default.get(`${THREADS_URL}/${creationId}`).set('Accept', 'application/json').set('Content-Type', 'application/json').query({
23
58
  access_token: accessToken,
24
59
  fields: 'id,status,error_message'
25
60
  }).send();
26
61
  if (response.body.status === 'IN_PROGRESS') {
27
- const waitTime = retry === 0 ? SHORT_WAIT_TIME_MS : LONG_WAIT_TIME_MS;
28
- (0, _loggerHelpers.loggerInfo)(logger, `Creation ID ${creationId} in progress. Retry ${retry}. Waiting ${waitTime / 1000} seconds.`);
29
- await new Promise(resolve => setTimeout(resolve, waitTime));
30
- } else {
31
- (0, _loggerHelpers.loggerDebug)(logger, 'Threads Response status', response.status);
32
- return response;
62
+ throw new Error('Status is still IN_PROGRESS');
33
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
72
+ throw error;
34
73
  }
35
- const error = new Error(`Creation ID ${creationId} is taking too long.`);
36
- error.code = 408; // Request Timeout
37
- throw error;
38
74
  }
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
+ */
39
84
  async function sendRequest(apiUrl, params, logger) {
40
85
  try {
41
86
  const response = await _superagent.default.get(apiUrl).set('Accept', 'application/json').set('Content-Type', 'application/json').query(params).send();
@@ -50,6 +95,15 @@ async function sendRequest(apiUrl, params, logger) {
50
95
  throw err;
51
96
  }
52
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
+ */
53
107
  async function requestApi(apiUrl, params, logger) {
54
108
  try {
55
109
  const response = await _superagent.default.post(apiUrl).set('Accept', 'application/json').set('Content-Type', 'application/json').query(params).send();
@@ -64,6 +118,15 @@ async function requestApi(apiUrl, params, logger) {
64
118
  throw err;
65
119
  }
66
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
+ */
67
130
  async function reply(accessToken, inReplyToId, text, asset, logger) {
68
131
  const query = await constructReplyQuery(accessToken, inReplyToId, text, asset, logger);
69
132
  let response = await requestApi(THREADS_PUBLISH_URL, query, logger);
@@ -72,6 +135,33 @@ async function reply(accessToken, inReplyToId, text, asset, logger) {
72
135
  });
73
136
  return response.body;
74
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
+ */
75
165
  async function repost(token, externalId, logger) {
76
166
  const postId = (0, _externalIdHelpers.removePrefix)(externalId);
77
167
  let response = await requestApi(`${THREADS_URL}/${postId}/repost`, {
@@ -82,6 +172,15 @@ async function repost(token, externalId, logger) {
82
172
  });
83
173
  return response.body;
84
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
+ */
85
184
  async function getProfile(token, externalId, fields, logger) {
86
185
  const userId = (0, _externalIdHelpers.removePrefix)(externalId);
87
186
  let response = await sendRequest(`${THREADS_URL}/${userId}?fields=${fields}`, {
@@ -92,6 +191,15 @@ async function getProfile(token, externalId, fields, logger) {
92
191
  });
93
192
  return response.body;
94
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
+ */
95
203
  async function getInsights(token, externalId, metric, logger) {
96
204
  const mediaId = (0, _externalIdHelpers.removePrefix)(externalId);
97
205
  let response = await sendRequest(`${THREADS_URL}/${mediaId}/insights`, {
@@ -103,11 +211,45 @@ async function getInsights(token, externalId, metric, logger) {
103
211
  });
104
212
  return response.body;
105
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
+ */
106
247
  async function constructReplyQuery(accessToken, inReplyToId, text, asset, logger) {
248
+ let isQuote = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
107
249
  const baseQuery = {
108
250
  access_token: accessToken,
109
- reply_to_id: inReplyToId,
110
- text
251
+ text,
252
+ [isQuote ? 'quote_post_id' : 'in_reply_to_id']: inReplyToId
111
253
  };
112
254
  const mediaQuery = getMediaQuery(asset);
113
255
  const query = {
@@ -126,6 +268,12 @@ async function constructReplyQuery(accessToken, inReplyToId, text, asset, logger
126
268
  throw error;
127
269
  }
128
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
+ */
129
277
  function getMediaQuery(asset) {
130
278
  if (!asset) {
131
279
  return {
@@ -7,25 +7,68 @@ const MAX_RETRY_COUNT = 11;
7
7
  const SHORT_WAIT_TIME_MS = 10000; // 10 seconds
8
8
  const LONG_WAIT_TIME_MS = 60000; // 60 seconds
9
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
+ */
10
43
  async function confirmCreationId(accessToken, creationId, logger) {
11
- for (let retry = 0; retry <= MAX_RETRY_COUNT; retry++) {
44
+ const operation = async () => {
12
45
  const response = await superagent.get(`${THREADS_URL}/${creationId}`).set('Accept', 'application/json').set('Content-Type', 'application/json').query({
13
46
  access_token: accessToken,
14
47
  fields: 'id,status,error_message'
15
48
  }).send();
16
49
  if (response.body.status === 'IN_PROGRESS') {
17
- const waitTime = retry === 0 ? SHORT_WAIT_TIME_MS : LONG_WAIT_TIME_MS;
18
- loggerInfo(logger, `Creation ID ${creationId} in progress. Retry ${retry}. Waiting ${waitTime / 1000} seconds.`);
19
- await new Promise(resolve => setTimeout(resolve, waitTime));
20
- } else {
21
- loggerDebug(logger, 'Threads Response status', response.status);
22
- return response;
50
+ throw new Error('Status is still IN_PROGRESS');
23
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
60
+ throw error;
24
61
  }
25
- const error = new Error(`Creation ID ${creationId} is taking too long.`);
26
- error.code = 408; // Request Timeout
27
- throw error;
28
62
  }
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
+ */
29
72
  async function sendRequest(apiUrl, params, logger) {
30
73
  try {
31
74
  const response = await superagent.get(apiUrl).set('Accept', 'application/json').set('Content-Type', 'application/json').query(params).send();
@@ -40,6 +83,15 @@ async function sendRequest(apiUrl, params, logger) {
40
83
  throw err;
41
84
  }
42
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
+ */
43
95
  async function requestApi(apiUrl, params, logger) {
44
96
  try {
45
97
  const response = await superagent.post(apiUrl).set('Accept', 'application/json').set('Content-Type', 'application/json').query(params).send();
@@ -54,6 +106,15 @@ async function requestApi(apiUrl, params, logger) {
54
106
  throw err;
55
107
  }
56
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
+ */
57
118
  export async function reply(accessToken, inReplyToId, text, asset, logger) {
58
119
  const query = await constructReplyQuery(accessToken, inReplyToId, text, asset, logger);
59
120
  let response = await requestApi(THREADS_PUBLISH_URL, query, logger);
@@ -62,6 +123,33 @@ export async function reply(accessToken, inReplyToId, text, asset, logger) {
62
123
  });
63
124
  return response.body;
64
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
+ */
65
153
  export async function repost(token, externalId, logger) {
66
154
  const postId = removePrefix(externalId);
67
155
  let response = await requestApi(`${THREADS_URL}/${postId}/repost`, {
@@ -72,6 +160,15 @@ export async function repost(token, externalId, logger) {
72
160
  });
73
161
  return response.body;
74
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
+ */
75
172
  export async function getProfile(token, externalId, fields, logger) {
76
173
  const userId = removePrefix(externalId);
77
174
  let response = await sendRequest(`${THREADS_URL}/${userId}?fields=${fields}`, {
@@ -82,6 +179,15 @@ export async function getProfile(token, externalId, fields, logger) {
82
179
  });
83
180
  return response.body;
84
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
+ */
85
191
  export async function getInsights(token, externalId, metric, logger) {
86
192
  const mediaId = removePrefix(externalId);
87
193
  let response = await sendRequest(`${THREADS_URL}/${mediaId}/insights`, {
@@ -93,11 +199,45 @@ export async function getInsights(token, externalId, metric, logger) {
93
199
  });
94
200
  return response.body;
95
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
+ */
96
235
  async function constructReplyQuery(accessToken, inReplyToId, text, asset, logger) {
236
+ let isQuote = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
97
237
  const baseQuery = {
98
238
  access_token: accessToken,
99
- reply_to_id: inReplyToId,
100
- text
239
+ text,
240
+ [isQuote ? 'quote_post_id' : 'in_reply_to_id']: inReplyToId
101
241
  };
102
242
  const mediaQuery = getMediaQuery(asset);
103
243
  const query = {
@@ -116,6 +256,12 @@ async function constructReplyQuery(accessToken, inReplyToId, text, asset, logger
116
256
  throw error;
117
257
  }
118
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
+ */
119
265
  function getMediaQuery(asset) {
120
266
  if (!asset) {
121
267
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltwater/conversations-api-services",
3
- "version": "1.1.5",
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",
@@ -13,8 +13,46 @@ const MAX_RETRY_COUNT = 11;
13
13
  const SHORT_WAIT_TIME_MS = 10000; // 10 seconds
14
14
  const LONG_WAIT_TIME_MS = 60000; // 60 seconds
15
15
 
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;
27
+
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
+ }
35
+
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
+ }
44
+ }
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
+ */
16
54
  async function confirmCreationId(accessToken, creationId, logger) {
17
- for (let retry = 0; retry <= MAX_RETRY_COUNT; retry++) {
55
+ const operation = async () => {
18
56
  const response = await superagent
19
57
  .get(`${THREADS_URL}/${creationId}`)
20
58
  .set('Accept', 'application/json')
@@ -26,26 +64,30 @@ async function confirmCreationId(accessToken, creationId, logger) {
26
64
  .send();
27
65
 
28
66
  if (response.body.status === 'IN_PROGRESS') {
29
- const waitTime =
30
- retry === 0 ? SHORT_WAIT_TIME_MS : LONG_WAIT_TIME_MS;
31
- loggerInfo(
32
- logger,
33
- `Creation ID ${creationId} in progress. Retry ${retry}. Waiting ${
34
- waitTime / 1000
35
- } seconds.`
36
- );
37
- await new Promise((resolve) => setTimeout(resolve, waitTime));
38
- } else {
39
- loggerDebug(logger, 'Threads Response status', response.status);
40
- return response;
67
+ throw new Error('Status is still IN_PROGRESS');
41
68
  }
42
- }
43
69
 
44
- const error = new Error(`Creation ID ${creationId} is taking too long.`);
45
- error.code = 408; // Request Timeout
46
- throw error;
70
+ loggerDebug(logger, 'Threads Response status', response.status);
71
+ return response;
72
+ };
73
+
74
+ try {
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
+ }
47
81
  }
48
82
 
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
+ */
49
91
  async function sendRequest(apiUrl, params, logger) {
50
92
  try {
51
93
  const response = await superagent
@@ -68,6 +110,14 @@ async function sendRequest(apiUrl, params, logger) {
68
110
  }
69
111
  }
70
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
+ */
71
121
  async function requestApi(apiUrl, params, logger) {
72
122
  try {
73
123
  const response = await superagent
@@ -90,6 +140,14 @@ async function requestApi(apiUrl, params, logger) {
90
140
  }
91
141
  }
92
142
 
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
+ */
93
151
  export async function reply(accessToken, inReplyToId, text, asset, logger) {
94
152
  const query = await constructReplyQuery(
95
153
  accessToken,
@@ -107,6 +165,39 @@ export async function reply(accessToken, inReplyToId, text, asset, logger) {
107
165
  return response.body;
108
166
  }
109
167
 
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(
179
+ accessToken,
180
+ inReplyToId,
181
+ text,
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
+ */
110
201
  export async function repost(token, externalId, logger) {
111
202
  const postId = removePrefix(externalId);
112
203
  let response = await requestApi(
@@ -121,6 +212,14 @@ export async function repost(token, externalId, logger) {
121
212
  return response.body;
122
213
  }
123
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
+ */
124
223
  export async function getProfile(token, externalId, fields, logger) {
125
224
  const userId = removePrefix(externalId);
126
225
  let response = await sendRequest(
@@ -135,6 +234,14 @@ export async function getProfile(token, externalId, fields, logger) {
135
234
  return response.body;
136
235
  }
137
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
+ */
138
245
  export async function getInsights(token, externalId, metric, logger) {
139
246
  const mediaId = removePrefix(externalId);
140
247
  let response = await sendRequest(
@@ -149,17 +256,50 @@ export async function getInsights(token, externalId, metric, logger) {
149
256
  return response.body;
150
257
  }
151
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
+ */
152
291
  async function constructReplyQuery(
153
292
  accessToken,
154
293
  inReplyToId,
155
294
  text,
156
295
  asset,
157
- logger
296
+ logger,
297
+ isQuote = false
158
298
  ) {
159
299
  const baseQuery = {
160
300
  access_token: accessToken,
161
- reply_to_id: inReplyToId,
162
301
  text,
302
+ [isQuote ? 'quote_post_id' : 'in_reply_to_id']: inReplyToId,
163
303
  };
164
304
 
165
305
  const mediaQuery = getMediaQuery(asset);
@@ -182,6 +322,11 @@ async function constructReplyQuery(
182
322
  }
183
323
  }
184
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
+ */
185
330
  function getMediaQuery(asset) {
186
331
  if (!asset) {
187
332
  return { media_type: 'TEXT' };