@meltwater/conversations-api-services 1.1.5 → 1.1.7

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' : 'reply_to_id']: inReplyToId
111
253
  };
112
254
  const mediaQuery = getMediaQuery(asset);
113
255
  const query = {
@@ -119,13 +261,19 @@ async function constructReplyQuery(accessToken, inReplyToId, text, asset, logger
119
261
  await confirmCreationId(accessToken, containerResponse.body.id, logger);
120
262
  return {
121
263
  ...query,
122
- containerResponse
264
+ creation_id: containerResponse.body.id
123
265
  };
124
266
  } catch (error) {
125
267
  (0, _loggerHelpers.loggerError)(logger, 'Error constructing reply query', error);
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' : 'reply_to_id']: inReplyToId
101
241
  };
102
242
  const mediaQuery = getMediaQuery(asset);
103
243
  const query = {
@@ -109,13 +249,19 @@ async function constructReplyQuery(accessToken, inReplyToId, text, asset, logger
109
249
  await confirmCreationId(accessToken, containerResponse.body.id, logger);
110
250
  return {
111
251
  ...query,
112
- containerResponse
252
+ creation_id: containerResponse.body.id
113
253
  };
114
254
  } catch (error) {
115
255
  loggerError(logger, 'Error constructing reply query', error);
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.7",
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,53 @@ 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(
26
+ operation,
27
+ maxRetries,
28
+ initialWaitTime,
29
+ logger
30
+ ) {
31
+ let waitTime = initialWaitTime;
32
+
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
+
41
+ loggerInfo(
42
+ logger,
43
+ `Retry attempt ${attempt + 1} failed. Retrying in ${
44
+ waitTime / 1000
45
+ } seconds...`
46
+ );
47
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
48
+ waitTime *= 2; // Exponential backoff
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Confirms the creation ID by polling the Threads API until the status is no longer "IN_PROGRESS".
55
+ * @param {string} accessToken - The access token for authentication.
56
+ * @param {string} creationId - The ID of the creation process to confirm.
57
+ * @param {Object} logger - Logger instance for logging.
58
+ * @returns {Promise<Object>} The API response when the status is no longer "IN_PROGRESS".
59
+ * @throws {Error} If the status remains "IN_PROGRESS" after all retries.
60
+ */
16
61
  async function confirmCreationId(accessToken, creationId, logger) {
17
- for (let retry = 0; retry <= MAX_RETRY_COUNT; retry++) {
62
+ const operation = async () => {
18
63
  const response = await superagent
19
64
  .get(`${THREADS_URL}/${creationId}`)
20
65
  .set('Accept', 'application/json')
@@ -26,26 +71,39 @@ async function confirmCreationId(accessToken, creationId, logger) {
26
71
  .send();
27
72
 
28
73
  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;
74
+ throw new Error('Status is still IN_PROGRESS');
41
75
  }
42
- }
43
76
 
44
- const error = new Error(`Creation ID ${creationId} is taking too long.`);
45
- error.code = 408; // Request Timeout
46
- throw error;
77
+ loggerDebug(logger, 'Threads Response status', response.status);
78
+ return response;
79
+ };
80
+
81
+ try {
82
+ return await retryWithBackoff(
83
+ operation,
84
+ MAX_RETRY_COUNT,
85
+ SHORT_WAIT_TIME_MS,
86
+ logger
87
+ );
88
+ } catch (error) {
89
+ loggerError(
90
+ logger,
91
+ `Creation ID ${creationId} confirmation failed: ${error.message}`,
92
+ error
93
+ );
94
+ error.code = 408; // Request Timeout
95
+ throw error;
96
+ }
47
97
  }
48
98
 
99
+ /**
100
+ * Sends a GET 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
+ */
49
107
  async function sendRequest(apiUrl, params, logger) {
50
108
  try {
51
109
  const response = await superagent
@@ -68,6 +126,14 @@ async function sendRequest(apiUrl, params, logger) {
68
126
  }
69
127
  }
70
128
 
129
+ /**
130
+ * Sends a POST request to the Threads API.
131
+ * @param {string} apiUrl - The API URL to send the request to.
132
+ * @param {Object} params - Query parameters for the request.
133
+ * @param {Object} logger - Logger instance for logging.
134
+ * @returns {Promise<Object>} The API response.
135
+ * @throws {Error} If the request fails.
136
+ */
71
137
  async function requestApi(apiUrl, params, logger) {
72
138
  try {
73
139
  const response = await superagent
@@ -90,6 +156,14 @@ async function requestApi(apiUrl, params, logger) {
90
156
  }
91
157
  }
92
158
 
159
+ /**
160
+ * Publishes a post to Threads.
161
+ * @param {string} accessToken - The access token for authentication.
162
+ * @param {string} text - The text content of the post.
163
+ * @param {Object} asset - The media asset to attach to the post.
164
+ * @param {Object} logger - Logger instance for logging.
165
+ * @returns {Promise<Object>} The API response.
166
+ */
93
167
  export async function reply(accessToken, inReplyToId, text, asset, logger) {
94
168
  const query = await constructReplyQuery(
95
169
  accessToken,
@@ -107,6 +181,39 @@ export async function reply(accessToken, inReplyToId, text, asset, logger) {
107
181
  return response.body;
108
182
  }
109
183
 
184
+ /**
185
+ * Publishes a quote post to Threads.
186
+ * @param {string} accessToken - The access token for authentication.
187
+ * @param {string} inReplyToId - The ID of the post to quote.
188
+ * @param {string} text - The text content of the quote.
189
+ * @param {Object} asset - The media asset to attach to the quote.
190
+ * @param {Object} logger - Logger instance for logging.
191
+ * @returns {Promise<Object>} The API response.
192
+ */
193
+ export async function quote(accessToken, inReplyToId, text, asset, logger) {
194
+ const query = await constructReplyQuery(
195
+ accessToken,
196
+ inReplyToId,
197
+ text,
198
+ asset,
199
+ logger,
200
+ true
201
+ );
202
+ let response = await requestApi(THREADS_PUBLISH_URL, query, logger);
203
+ loggerInfo(logger, `Native Threads API Publish quote Response`, {
204
+ responseBody: JSON.stringify(response.body),
205
+ });
206
+ return response.body;
207
+ }
208
+
209
+ /**
210
+ * Publishes a post to Threads.
211
+ * @param {string} accessToken - The access token for authentication.
212
+ * @param {string} text - The text content of the post.
213
+ * @param {Object} asset - The media asset to attach to the post.
214
+ * @param {Object} logger - Logger instance for logging.
215
+ * @returns {Promise<Object>} The API response.
216
+ */
110
217
  export async function repost(token, externalId, logger) {
111
218
  const postId = removePrefix(externalId);
112
219
  let response = await requestApi(
@@ -121,6 +228,14 @@ export async function repost(token, externalId, logger) {
121
228
  return response.body;
122
229
  }
123
230
 
231
+ /**
232
+ * Retrieves the profile information of a user.
233
+ * @param {string} token - The access token for authentication.
234
+ * @param {string} externalId - The external ID of the user.
235
+ * @param {string} fields - The fields to retrieve from the profile.
236
+ * @param {Object} logger - Logger instance for logging.
237
+ * @returns {Promise<Object>} The API response containing the profile information.
238
+ */
124
239
  export async function getProfile(token, externalId, fields, logger) {
125
240
  const userId = removePrefix(externalId);
126
241
  let response = await sendRequest(
@@ -135,6 +250,14 @@ export async function getProfile(token, externalId, fields, logger) {
135
250
  return response.body;
136
251
  }
137
252
 
253
+ /**
254
+ * Retrieves the insights of a post.
255
+ * @param {string} token - The access token for authentication.
256
+ * @param {string} externalId - The external ID of the post.
257
+ * @param {string} metric - The metric to retrieve insights for.
258
+ * @param {Object} logger - Logger instance for logging.
259
+ * @returns {Promise<Object>} The API response containing the insights.
260
+ */
138
261
  export async function getInsights(token, externalId, metric, logger) {
139
262
  const mediaId = removePrefix(externalId);
140
263
  let response = await sendRequest(
@@ -149,17 +272,50 @@ export async function getInsights(token, externalId, metric, logger) {
149
272
  return response.body;
150
273
  }
151
274
 
275
+ /**
276
+ * Manages the visibility of a reply.
277
+ * @param {string} token - The access token for authentication.
278
+ * @param {string} externalId - The external ID of the reply.
279
+ * @param {boolean} hide - Whether to hide the reply.
280
+ * @param {Object} logger - Logger instance for logging.
281
+ * @returns {Promise<Object>} The API response.
282
+ */
283
+ export async function manageReply(token, externalId, hide = false, logger) {
284
+ const replyId = removePrefix(externalId);
285
+ let response = await requestApi(
286
+ `${THREADS_URL}/${replyId}/manage_reply`,
287
+ { access_token: token, hide },
288
+ logger
289
+ );
290
+
291
+ loggerInfo(logger, `Native Threads API hideReply Response`, {
292
+ responseBody: JSON.stringify(response.body),
293
+ });
294
+ return response.body;
295
+ }
296
+
297
+ /**
298
+ * Constructs the query parameters for replying to a post.
299
+ * @param {string} accessToken - The access token for authentication.
300
+ * @param {string} inReplyToId - The ID of the post to reply to.
301
+ * @param {string} text - The text content of the reply.
302
+ * @param {Object} asset - The media asset to attach to the reply.
303
+ * @param {Object} logger - Logger instance for logging.
304
+ * @param {boolean} isQuote - Whether the reply is a quote.
305
+ * @returns {Promise<Object>} The constructed query parameters.
306
+ */
152
307
  async function constructReplyQuery(
153
308
  accessToken,
154
309
  inReplyToId,
155
310
  text,
156
311
  asset,
157
- logger
312
+ logger,
313
+ isQuote = false
158
314
  ) {
159
315
  const baseQuery = {
160
316
  access_token: accessToken,
161
- reply_to_id: inReplyToId,
162
317
  text,
318
+ [isQuote ? 'quote_post_id' : 'reply_to_id']: inReplyToId,
163
319
  };
164
320
 
165
321
  const mediaQuery = getMediaQuery(asset);
@@ -175,13 +331,18 @@ async function constructReplyQuery(
175
331
 
176
332
  await confirmCreationId(accessToken, containerResponse.body.id, logger);
177
333
 
178
- return { ...query, containerResponse };
334
+ return { ...query, creation_id: containerResponse.body.id };
179
335
  } catch (error) {
180
336
  loggerError(logger, 'Error constructing reply query', error);
181
337
  throw error;
182
338
  }
183
339
  }
184
340
 
341
+ /**
342
+ * Constructs the media query parameters based on the asset type.
343
+ * @param {Object} asset - The media asset to attach to the post.
344
+ * @returns {Object} The constructed media query parameters.
345
+ */
185
346
  function getMediaQuery(asset) {
186
347
  if (!asset) {
187
348
  return { media_type: 'TEXT' };