@meltwater/conversations-api-services 1.1.22 → 1.1.24

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.
@@ -361,6 +361,19 @@ async function postApi(apiUrl, accessToken, payload, logger) {
361
361
  }
362
362
  response = await request;
363
363
  } catch (err) {
364
+ // Handle specific case where content is already marked as spam
365
+ if (err?.response?.body?.error?.error_subcode === 1446036 && err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request") {
366
+ (0, _loggerHelpers.loggerInfo)(logger, `Content already marked as spam - treating as successful hide operation`, {
367
+ responseBody: JSON.stringify(err.response.body)
368
+ });
369
+ return {
370
+ status: 200,
371
+ body: {
372
+ success: true,
373
+ message: "Content already hidden (marked as spam)"
374
+ }
375
+ };
376
+ }
364
377
  (0, _loggerHelpers.loggerDebug)(logger, `Failed to call facebook api`, err);
365
378
  throw err;
366
379
  }
@@ -190,6 +190,19 @@ class FacebookApiClient {
190
190
  access_token: accessToken
191
191
  }).send(payload);
192
192
  } catch (err) {
193
+ // Handle specific case where content is already marked as spam
194
+ if (err?.response?.body?.error?.error_subcode === 1446036 && err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request") {
195
+ loggerChild.info(`Content already marked as spam - treating as successful hide operation for documentId ${documentId}`, {
196
+ responseBody: JSON.stringify(err.response.body)
197
+ });
198
+ return {
199
+ status: 200,
200
+ body: {
201
+ success: true,
202
+ message: "Content already hidden (marked as spam)"
203
+ }
204
+ };
205
+ }
193
206
  let errorText = '';
194
207
  if (err && err.response && err.response.body && err.response.body.error) {
195
208
  errorText = `Failed to call facebook api for documentId ${documentId}: ${err.response.body.error.message}`;
@@ -14,6 +14,7 @@ var _logger = _interopRequireDefault(require("../../lib/logger.js"));
14
14
  var _loggerHelpers = require("../../lib/logger.helpers.js");
15
15
  var _linkedinNative = require("./linkedin.native.js");
16
16
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
17
+ const LINKEDIN_VERSION = '202511';
17
18
  class LinkedInApiClient {
18
19
  constructor() {
19
20
  this.credentialsAPI = new _credentialsApiClient.CredentialsApiClient();
@@ -63,7 +64,7 @@ class LinkedInApiClient {
63
64
  response = await _superagent.default.delete(query).timeout(5000).set({
64
65
  Authorization: `Bearer ${credential.token}`,
65
66
  'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
66
- 'LinkedIn-Version': '202311'
67
+ 'LinkedIn-Version': LINKEDIN_VERSION
67
68
  });
68
69
  } else {
69
70
  loggerChild.debug(`${documentId} - sending to linkedin `, {
@@ -74,7 +75,7 @@ class LinkedInApiClient {
74
75
  .set({
75
76
  Authorization: `Bearer ${credential.token}`,
76
77
  'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
77
- 'LinkedIn-Version': '202311'
78
+ 'LinkedIn-Version': LINKEDIN_VERSION
78
79
  }).send(payload);
79
80
  }
80
81
  loggerChild.info(`Native Linkedin API Like Comment Response for documentId ${documentId}`, {
@@ -184,7 +185,7 @@ class LinkedInApiClient {
184
185
  Authorization: `Bearer ${credential.token}`,
185
186
  'Content-Type': 'application/json',
186
187
  'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
187
- 'LinkedIn-Version': '202311'
188
+ 'LinkedIn-Version': LINKEDIN_VERSION
188
189
  }).send(body);
189
190
  response = {
190
191
  status: 200,
@@ -213,7 +214,7 @@ class LinkedInApiClient {
213
214
  Authorization: `Bearer ${credential.token}`,
214
215
  'Content-type': 'application/json',
215
216
  'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
216
- 'LinkedIn-Version': '202311'
217
+ 'LinkedIn-Version': LINKEDIN_VERSION
217
218
  }).send(body);
218
219
  response = {
219
220
  status: 200,
@@ -238,7 +239,7 @@ class LinkedInApiClient {
238
239
  .set({
239
240
  Authorization: `Bearer ${token}`,
240
241
  'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
241
- 'LinkedIn-Version': '202311'
242
+ 'LinkedIn-Version': LINKEDIN_VERSION
242
243
  });
243
244
  } catch (error) {
244
245
  loggerChild.error('Error in getAllStats', error);
@@ -292,7 +293,7 @@ class LinkedInApiClient {
292
293
  .set({
293
294
  Authorization: `Bearer ${credential.token}`,
294
295
  'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
295
- 'LinkedIn-Version': '202311'
296
+ 'LinkedIn-Version': LINKEDIN_VERSION
296
297
  });
297
298
  } catch (error) {
298
299
  loggerChild.error('Error in getImageMedia', error);
@@ -332,7 +333,7 @@ class LinkedInApiClient {
332
333
  .set({
333
334
  Authorization: `Bearer ${credential.token}`,
334
335
  'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
335
- 'LinkedIn-Version': '202311'
336
+ 'LinkedIn-Version': LINKEDIN_VERSION
336
337
  });
337
338
  } catch (error) {
338
339
  loggerChild.error('Error in getVideoMedia', error);
@@ -399,7 +400,7 @@ class LinkedInApiClient {
399
400
  'Content-Type': 'application/octet-stream',
400
401
  Authorization: `Bearer ${token}`,
401
402
  'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
402
- 'LinkedIn-Version': '202311'
403
+ 'LinkedIn-Version': LINKEDIN_VERSION
403
404
  }).send(data).catch(err => {
404
405
  _logger.default.error(err);
405
406
  throw err;
@@ -416,7 +417,7 @@ async function getProfile(urn, token) {
416
417
  }).timeout(5000).set({
417
418
  Authorization: `Bearer ${token}`,
418
419
  'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
419
- 'LinkedIn-Version': '202311'
420
+ 'LinkedIn-Version': LINKEDIN_VERSION
420
421
  }).then(result => result.body);
421
422
  } catch (error) {
422
423
  _logger.default.error(`Failed requesting LinkedIn API for profileId ${urn}`, error);
@@ -25,7 +25,7 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
25
25
  const LINKEDIN_API = "https://api.linkedin.com/v2";
26
26
  const LINKEDIN_API_REST = "https://api.linkedin.com/rest";
27
27
  const LINKEDIN_API_VERSION = "2.0.0";
28
- const LINKEDIN_VERSION = "202411";
28
+ const LINKEDIN_VERSION = "202511";
29
29
  async function like(token, externalId, socialAccountId, logger) {
30
30
  let response;
31
31
  let payload = {
@@ -374,16 +374,18 @@ async function getOrganization(urn, token, logger) {
374
374
  let [,,, id] = urn.split(':');
375
375
  let organization;
376
376
  try {
377
- organization = await _superagent.default.get(`${LINKEDIN_API}/organizations/${fixedEncodeURIComponent(id)}`).query({
378
- projection: '(id,localizedName,vanityName,logoV2(original~:playableStreams))'
379
- }).timeout(5000) // 5 seconds
377
+ // Use the newer REST API endpoint instead of deprecated v2/organizations
378
+ organization = await _superagent.default.get(`${LINKEDIN_API_REST}/organizationsLookup?ids=List(${fixedEncodeURIComponent(id)})`).timeout(5000) // 5 seconds
380
379
  .set({
381
380
  Authorization: `Bearer ${token}`,
382
381
  'X-RestLi-Protocol-Version': LINKEDIN_API_VERSION,
383
382
  'LinkedIn-Version': LINKEDIN_VERSION
384
- }).then(result => result.body);
383
+ }).then(result => {
384
+ const res = result.body;
385
+ return res.results[id];
386
+ });
385
387
  } catch (error) {
386
- if (error.response.body.message.includes('ADMIN_ONLY')) {
388
+ if (error.response && error.response.body && error.response.body.message && error.response.body.message.includes('ADMIN_ONLY')) {
387
389
  return {
388
390
  id: 'private'
389
391
  };
@@ -29,6 +29,112 @@ var _axios = _interopRequireDefault(require("axios"));
29
29
  var _fs = _interopRequireDefault(require("fs"));
30
30
  var _socialTwit = _interopRequireDefault(require("@meltwater/social-twit"));
31
31
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
32
+ const TWITTER_API_CONFIG = {
33
+ v1_1: {
34
+ base_url: 'https://api.twitter.com/1.1'
35
+ },
36
+ v2: {
37
+ base_url: 'https://api.x.com/2',
38
+ default_user_fields: 'id,name,username,profile_image_url,public_metrics,description,created_at,verified,protected'
39
+ }
40
+ };
41
+
42
+ // Helper function to create v2 API URLs with consistent field selection
43
+ function createTwitterV2Url(endpoint) {
44
+ let additionalParams = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
45
+ const baseUrl = `${TWITTER_API_CONFIG.v2.base_url}/${endpoint}`;
46
+ const defaultFields = {
47
+ 'user.fields': TWITTER_API_CONFIG.v2.default_user_fields
48
+ };
49
+ const allParams = {
50
+ ...defaultFields,
51
+ ...additionalParams
52
+ };
53
+ const queryString = Object.entries(allParams).map(_ref => {
54
+ let [key, value] = _ref;
55
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
56
+ }).join('&');
57
+ return queryString ? `${baseUrl}?${queryString}` : baseUrl;
58
+ }
59
+
60
+ // Helper function to make v2 API requests with fallback to v1.1
61
+ async function makeTwitterV2Request(v2Endpoint, v1FallbackQuery, token, logger) {
62
+ let requestOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
63
+ // Try v2 API first - it supports OAuth 1.0a for user lookup endpoints and has better rate limits!
64
+ try {
65
+ const v2Url = createTwitterV2Url(v2Endpoint, requestOptions.v2Params);
66
+ const result = await getRequest({
67
+ token,
68
+ uri: v2Url,
69
+ attachUrlPrefix: false,
70
+ convertPayloadToUri: false,
71
+ logger
72
+ });
73
+ return {
74
+ success: true,
75
+ data: result.data?.data,
76
+ source: 'v2'
77
+ };
78
+ } catch (e) {
79
+ (0, _loggerHelpers.loggerWarn)(logger, `Twitter API v2 request failed for ${v2Endpoint}, falling back to v1.1`, e);
80
+
81
+ // Fallback to v1.1 if v2 fails
82
+ if (v1FallbackQuery && requestOptions.enableFallback !== false) {
83
+ try {
84
+ (0, _loggerHelpers.loggerInfo)(logger, `Falling back to v1.1 API for ${v2Endpoint}`);
85
+ const fallbackResult = await (requestOptions.fallbackMethod === 'post' ? postRequest : getRequest)({
86
+ token,
87
+ uri: v1FallbackQuery,
88
+ logger
89
+ });
90
+ return {
91
+ success: true,
92
+ data: fallbackResult.data,
93
+ source: 'v1.1'
94
+ };
95
+ } catch (fallbackError) {
96
+ (0, _loggerHelpers.loggerError)(logger, `Both v2 and v1.1 APIs failed for ${v2Endpoint}`, fallbackError);
97
+ }
98
+ }
99
+ return {
100
+ success: false,
101
+ error: e,
102
+ source: 'failed'
103
+ };
104
+ }
105
+ }
106
+ function normalizeUserData(user) {
107
+ let apiVersion = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'v1.1';
108
+ if (!user) return user;
109
+ const normalized = {
110
+ ...user
111
+ };
112
+ if (apiVersion === 'v2') {
113
+ normalized.screen_name = user.username;
114
+ normalized.id_str = user.id;
115
+ normalized.profile_image_url_https = user.profile_image_url;
116
+ } else {
117
+ normalized.username = user.screen_name;
118
+ normalized.id = user.id_str;
119
+ normalized.profile_image_url = user.profile_image_url_https;
120
+ }
121
+ return normalized;
122
+ }
123
+
124
+ /**
125
+ * Normalizes an array of user data or a single user object
126
+ * @param {Object|Array} users - User object or array of user objects from Twitter API
127
+ * @param {string} apiVersion - API version used ('v1.1' or 'v2')
128
+ * @returns {Object|Array} Normalized user data with both screen_name and username
129
+ */
130
+ function normalizeUsersData(users) {
131
+ let apiVersion = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'v1.1';
132
+ if (!users) return users;
133
+ if (Array.isArray(users)) {
134
+ return users.map(user => normalizeUserData(user, apiVersion));
135
+ }
136
+ return normalizeUserData(users, apiVersion);
137
+ }
32
138
  function addOAuthToToken(token, TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET) {
33
139
  if (!token.oauth) {
34
140
  token.oauth = (0, _oauth.default)({
@@ -50,31 +156,49 @@ async function getUserInfoFromHandles(token, handles, logger) {
50
156
  try {
51
157
  const handlesJoin = handles.join(',');
52
158
  if (!handlesJoin.length) {
53
- loggerWarn(logger, `No handles provided to twitterNative getUserInfoFromHandles`);
159
+ (0, _loggerHelpers.loggerWarn)(logger, `No handles provided to twitterNative getUserInfoFromHandles`);
54
160
  return [];
55
161
  }
56
- let query = `users/lookup.json?screen_name=${handlesJoin}`;
57
- let result = await postRequest({
58
- token,
59
- uri: query,
60
- logger
162
+
163
+ // Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
164
+ // v2 API supports up to 100 usernames per request vs v1.1's limit
165
+ const result = await makeTwitterV2Request('users/by',
166
+ // v2 endpoint
167
+ `users/lookup.json?screen_name=${handlesJoin}`,
168
+ // v1.1 fallback
169
+ token, logger, {
170
+ v2Params: {
171
+ usernames: handlesJoin
172
+ },
173
+ fallbackMethod: 'post'
61
174
  });
62
- return result.data;
175
+ const normalizedData = normalizeUsersData(result.data, result.source);
176
+ return normalizedData || [];
63
177
  } catch (e) {
64
- (0, _loggerHelpers.loggerError)(logger, `Error getting UserInfo`, e);
178
+ (0, _loggerHelpers.loggerError)(logger, `Unexpected error in getUserInfoFromHandles`, e);
179
+ return [];
65
180
  }
66
181
  }
67
182
  async function getUserInfoFromHandle(token, handleId, logger) {
68
183
  try {
69
- let query = `users/show.json?user_id=${(0, _externalIdHelpers.removePrefix)(handleId)}`;
70
- let result = await getRequest({
71
- token,
72
- uri: query,
73
- logger
74
- });
75
- return result.data;
184
+ if (!handleId) {
185
+ (0, _loggerHelpers.loggerWarn)(logger, `Invalid handleId provided to getUserInfoFromHandle: ${JSON.stringify(handleId)}`);
186
+ throw new Error(`Invalid handleId: ${handleId}`);
187
+ }
188
+ // Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
189
+ const result = await makeTwitterV2Request(`users/${(0, _externalIdHelpers.removePrefix)(handleId)}`,
190
+ // v2 endpoint
191
+ `users/show.json?user_id=${(0, _externalIdHelpers.removePrefix)(handleId)}`,
192
+ // v1.1 fallback
193
+ token, logger);
194
+ if (result.success) {
195
+ // Normalize the user data to ensure both screen_name and username are present
196
+ return normalizeUserData(result.data, result.source);
197
+ } else {
198
+ throw result.error || new Error('Failed to get user info from both APIs');
199
+ }
76
200
  } catch (e) {
77
- (0, _loggerHelpers.loggerError)(logger, `Error getting UserInfo`, e);
201
+ (0, _loggerHelpers.loggerError)(logger, `Error in getUserInfoFromHandle with handleId: ${JSON.stringify(handleId)}`, e);
78
202
  }
79
203
  }
80
204
  async function getDirectMessageImage(token, imageUrl, logger) {
@@ -403,7 +527,7 @@ async function publishTweet(token, payload, query, isDirectMessage, logger) {
403
527
  throw err;
404
528
  }
405
529
  }
406
- async function postRequest(_ref) {
530
+ async function postRequest(_ref2) {
407
531
  let {
408
532
  token,
409
533
  uri,
@@ -411,7 +535,7 @@ async function postRequest(_ref) {
411
535
  attachUrlPrefix = true,
412
536
  convertPayloadToUri = true,
413
537
  logger
414
- } = _ref;
538
+ } = _ref2;
415
539
  return doRequest({
416
540
  requestMethod: 'post',
417
541
  token,
@@ -422,7 +546,7 @@ async function postRequest(_ref) {
422
546
  logger
423
547
  });
424
548
  }
425
- async function getRequest(_ref2) {
549
+ async function getRequest(_ref3) {
426
550
  let {
427
551
  token,
428
552
  uri,
@@ -430,7 +554,7 @@ async function getRequest(_ref2) {
430
554
  attachUrlPrefix = true,
431
555
  convertPayloadToUri = true,
432
556
  logger
433
- } = _ref2;
557
+ } = _ref3;
434
558
  return doRequest({
435
559
  requestMethod: 'get',
436
560
  payload,
@@ -441,7 +565,7 @@ async function getRequest(_ref2) {
441
565
  logger
442
566
  });
443
567
  }
444
- async function deleteRequest(_ref3) {
568
+ async function deleteRequest(_ref4) {
445
569
  let {
446
570
  token,
447
571
  uri,
@@ -449,7 +573,7 @@ async function deleteRequest(_ref3) {
449
573
  attachUrlPrefix = false,
450
574
  convertPayloadToUri = false,
451
575
  logger
452
- } = _ref3;
576
+ } = _ref4;
453
577
  return doRequest({
454
578
  requestMethod: 'delete',
455
579
  token,
@@ -465,7 +589,7 @@ function fixedEncodeURIComponent(str) {
465
589
  return '%' + c.charCodeAt(0).toString(16);
466
590
  });
467
591
  }
468
- async function doRequest(_ref4) {
592
+ async function doRequest(_ref5) {
469
593
  let {
470
594
  requestMethod,
471
595
  token,
@@ -474,7 +598,7 @@ async function doRequest(_ref4) {
474
598
  attachUrlPrefix = true,
475
599
  convertPayloadToUri = true,
476
600
  logger
477
- } = _ref4;
601
+ } = _ref5;
478
602
  if (payload && convertPayloadToUri) {
479
603
  uri = uri + (uri.endsWith('?') ? '' : '?') + Object.keys(payload).map(key => {
480
604
  return fixedEncodeURIComponent(key) + '=' + fixedEncodeURIComponent(payload[key]);
@@ -513,7 +637,7 @@ async function doRequest(_ref4) {
513
637
  });
514
638
  } catch (e) {
515
639
  if (e.response.status == 503) {
516
- loggerWarn(logger, `Twitter API 503 error - Service Unavailable`, {
640
+ (0, _loggerHelpers.loggerWarn)(logger, `Twitter API 503 error - Service Unavailable`, {
517
641
  url,
518
642
  convertPayloadToUri,
519
643
  payload: JSON.stringify(payload)
@@ -6,7 +6,10 @@ 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|net):/g, '');
9
+ if (!document || typeof document !== 'string') {
10
+ return document; // Return as-is if not a valid string
11
+ }
12
+ return document.replace(/id\:[a-zA-Z]+\.(com|net):/g, '');
10
13
  }
11
14
  function twitterPrefixCheck(socialOriginType, value) {
12
15
  let result = value;
@@ -342,6 +342,19 @@ async function postApi(apiUrl, accessToken, payload, logger) {
342
342
  }
343
343
  response = await request;
344
344
  } catch (err) {
345
+ // Handle specific case where content is already marked as spam
346
+ if (err?.response?.body?.error?.error_subcode === 1446036 && err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request") {
347
+ loggerInfo(logger, `Content already marked as spam - treating as successful hide operation`, {
348
+ responseBody: JSON.stringify(err.response.body)
349
+ });
350
+ return {
351
+ status: 200,
352
+ body: {
353
+ success: true,
354
+ message: "Content already hidden (marked as spam)"
355
+ }
356
+ };
357
+ }
345
358
  loggerDebug(logger, `Failed to call facebook api`, err);
346
359
  throw err;
347
360
  }
@@ -181,6 +181,19 @@ export class FacebookApiClient {
181
181
  access_token: accessToken
182
182
  }).send(payload);
183
183
  } catch (err) {
184
+ // Handle specific case where content is already marked as spam
185
+ if (err?.response?.body?.error?.error_subcode === 1446036 && err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request") {
186
+ loggerChild.info(`Content already marked as spam - treating as successful hide operation for documentId ${documentId}`, {
187
+ responseBody: JSON.stringify(err.response.body)
188
+ });
189
+ return {
190
+ status: 200,
191
+ body: {
192
+ success: true,
193
+ message: "Content already hidden (marked as spam)"
194
+ }
195
+ };
196
+ }
184
197
  let errorText = '';
185
198
  if (err && err.response && err.response.body && err.response.body.error) {
186
199
  errorText = `Failed to call facebook api for documentId ${documentId}: ${err.response.body.error.message}`;
@@ -4,6 +4,7 @@ import { CredentialsApiClient } from '../http/credentialsApi.client.js';
4
4
  import logger from '../../lib/logger.js';
5
5
  import { MeltwaterAttributes } from '../../lib/logger.helpers.js';
6
6
  import { getOrganization as getOrganizationNative, getOrganizations as getOrganizationsNative, deleteMessage as deleteMessageNative } from './linkedin.native.js';
7
+ const LINKEDIN_VERSION = '202511';
7
8
  export class LinkedInApiClient {
8
9
  constructor() {
9
10
  this.credentialsAPI = new CredentialsApiClient();
@@ -53,7 +54,7 @@ export class LinkedInApiClient {
53
54
  response = await superagent.delete(query).timeout(5000).set({
54
55
  Authorization: `Bearer ${credential.token}`,
55
56
  'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
56
- 'LinkedIn-Version': '202311'
57
+ 'LinkedIn-Version': LINKEDIN_VERSION
57
58
  });
58
59
  } else {
59
60
  loggerChild.debug(`${documentId} - sending to linkedin `, {
@@ -64,7 +65,7 @@ export class LinkedInApiClient {
64
65
  .set({
65
66
  Authorization: `Bearer ${credential.token}`,
66
67
  'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
67
- 'LinkedIn-Version': '202311'
68
+ 'LinkedIn-Version': LINKEDIN_VERSION
68
69
  }).send(payload);
69
70
  }
70
71
  loggerChild.info(`Native Linkedin API Like Comment Response for documentId ${documentId}`, {
@@ -174,7 +175,7 @@ export class LinkedInApiClient {
174
175
  Authorization: `Bearer ${credential.token}`,
175
176
  'Content-Type': 'application/json',
176
177
  'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
177
- 'LinkedIn-Version': '202311'
178
+ 'LinkedIn-Version': LINKEDIN_VERSION
178
179
  }).send(body);
179
180
  response = {
180
181
  status: 200,
@@ -203,7 +204,7 @@ export class LinkedInApiClient {
203
204
  Authorization: `Bearer ${credential.token}`,
204
205
  'Content-type': 'application/json',
205
206
  'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
206
- 'LinkedIn-Version': '202311'
207
+ 'LinkedIn-Version': LINKEDIN_VERSION
207
208
  }).send(body);
208
209
  response = {
209
210
  status: 200,
@@ -228,7 +229,7 @@ export class LinkedInApiClient {
228
229
  .set({
229
230
  Authorization: `Bearer ${token}`,
230
231
  'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
231
- 'LinkedIn-Version': '202311'
232
+ 'LinkedIn-Version': LINKEDIN_VERSION
232
233
  });
233
234
  } catch (error) {
234
235
  loggerChild.error('Error in getAllStats', error);
@@ -282,7 +283,7 @@ export class LinkedInApiClient {
282
283
  .set({
283
284
  Authorization: `Bearer ${credential.token}`,
284
285
  'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
285
- 'LinkedIn-Version': '202311'
286
+ 'LinkedIn-Version': LINKEDIN_VERSION
286
287
  });
287
288
  } catch (error) {
288
289
  loggerChild.error('Error in getImageMedia', error);
@@ -322,7 +323,7 @@ export class LinkedInApiClient {
322
323
  .set({
323
324
  Authorization: `Bearer ${credential.token}`,
324
325
  'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
325
- 'LinkedIn-Version': '202311'
326
+ 'LinkedIn-Version': LINKEDIN_VERSION
326
327
  });
327
328
  } catch (error) {
328
329
  loggerChild.error('Error in getVideoMedia', error);
@@ -389,7 +390,7 @@ export class LinkedInApiClient {
389
390
  'Content-Type': 'application/octet-stream',
390
391
  Authorization: `Bearer ${token}`,
391
392
  'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
392
- 'LinkedIn-Version': '202311'
393
+ 'LinkedIn-Version': LINKEDIN_VERSION
393
394
  }).send(data).catch(err => {
394
395
  logger.error(err);
395
396
  throw err;
@@ -405,7 +406,7 @@ export async function getProfile(urn, token) {
405
406
  }).timeout(5000).set({
406
407
  Authorization: `Bearer ${token}`,
407
408
  'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
408
- 'LinkedIn-Version': '202311'
409
+ 'LinkedIn-Version': LINKEDIN_VERSION
409
410
  }).then(result => result.body);
410
411
  } catch (error) {
411
412
  logger.error(`Failed requesting LinkedIn API for profileId ${urn}`, error);
@@ -3,7 +3,7 @@ import { loggerDebug, loggerError, loggerInfo } from '../../lib/logger.helpers.j
3
3
  const LINKEDIN_API = "https://api.linkedin.com/v2";
4
4
  const LINKEDIN_API_REST = "https://api.linkedin.com/rest";
5
5
  const LINKEDIN_API_VERSION = "2.0.0";
6
- const LINKEDIN_VERSION = "202411";
6
+ const LINKEDIN_VERSION = "202511";
7
7
  export async function like(token, externalId, socialAccountId, logger) {
8
8
  let response;
9
9
  let payload = {
@@ -352,16 +352,18 @@ export async function getOrganization(urn, token, logger) {
352
352
  let [,,, id] = urn.split(':');
353
353
  let organization;
354
354
  try {
355
- organization = await superagent.get(`${LINKEDIN_API}/organizations/${fixedEncodeURIComponent(id)}`).query({
356
- projection: '(id,localizedName,vanityName,logoV2(original~:playableStreams))'
357
- }).timeout(5000) // 5 seconds
355
+ // Use the newer REST API endpoint instead of deprecated v2/organizations
356
+ organization = await superagent.get(`${LINKEDIN_API_REST}/organizationsLookup?ids=List(${fixedEncodeURIComponent(id)})`).timeout(5000) // 5 seconds
358
357
  .set({
359
358
  Authorization: `Bearer ${token}`,
360
359
  'X-RestLi-Protocol-Version': LINKEDIN_API_VERSION,
361
360
  'LinkedIn-Version': LINKEDIN_VERSION
362
- }).then(result => result.body);
361
+ }).then(result => {
362
+ const res = result.body;
363
+ return res.results[id];
364
+ });
363
365
  } catch (error) {
364
- if (error.response.body.message.includes('ADMIN_ONLY')) {
366
+ if (error.response && error.response.body && error.response.body.message && error.response.body.message.includes('ADMIN_ONLY')) {
365
367
  return {
366
368
  id: 'private'
367
369
  };
@@ -1,11 +1,117 @@
1
1
  import superagent from 'superagent';
2
2
  import { removePrefix } from '../../lib/externalId.helpers.js';
3
- import { loggerDebug, loggerError, loggerInfo } from '../../lib/logger.helpers.js';
3
+ import { loggerDebug, loggerError, loggerInfo, loggerWarn } from '../../lib/logger.helpers.js';
4
4
  import OAuth from 'oauth-1.0a';
5
5
  import crypto from 'crypto';
6
6
  import axios from 'axios';
7
7
  import fs from 'fs';
8
8
  import Twit from '@meltwater/social-twit';
9
+ const TWITTER_API_CONFIG = {
10
+ v1_1: {
11
+ base_url: 'https://api.twitter.com/1.1'
12
+ },
13
+ v2: {
14
+ base_url: 'https://api.x.com/2',
15
+ default_user_fields: 'id,name,username,profile_image_url,public_metrics,description,created_at,verified,protected'
16
+ }
17
+ };
18
+
19
+ // Helper function to create v2 API URLs with consistent field selection
20
+ function createTwitterV2Url(endpoint) {
21
+ let additionalParams = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
22
+ const baseUrl = `${TWITTER_API_CONFIG.v2.base_url}/${endpoint}`;
23
+ const defaultFields = {
24
+ 'user.fields': TWITTER_API_CONFIG.v2.default_user_fields
25
+ };
26
+ const allParams = {
27
+ ...defaultFields,
28
+ ...additionalParams
29
+ };
30
+ const queryString = Object.entries(allParams).map(_ref => {
31
+ let [key, value] = _ref;
32
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
33
+ }).join('&');
34
+ return queryString ? `${baseUrl}?${queryString}` : baseUrl;
35
+ }
36
+
37
+ // Helper function to make v2 API requests with fallback to v1.1
38
+ async function makeTwitterV2Request(v2Endpoint, v1FallbackQuery, token, logger) {
39
+ let requestOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
40
+ // Try v2 API first - it supports OAuth 1.0a for user lookup endpoints and has better rate limits!
41
+ try {
42
+ const v2Url = createTwitterV2Url(v2Endpoint, requestOptions.v2Params);
43
+ const result = await getRequest({
44
+ token,
45
+ uri: v2Url,
46
+ attachUrlPrefix: false,
47
+ convertPayloadToUri: false,
48
+ logger
49
+ });
50
+ return {
51
+ success: true,
52
+ data: result.data?.data,
53
+ source: 'v2'
54
+ };
55
+ } catch (e) {
56
+ loggerWarn(logger, `Twitter API v2 request failed for ${v2Endpoint}, falling back to v1.1`, e);
57
+
58
+ // Fallback to v1.1 if v2 fails
59
+ if (v1FallbackQuery && requestOptions.enableFallback !== false) {
60
+ try {
61
+ loggerInfo(logger, `Falling back to v1.1 API for ${v2Endpoint}`);
62
+ const fallbackResult = await (requestOptions.fallbackMethod === 'post' ? postRequest : getRequest)({
63
+ token,
64
+ uri: v1FallbackQuery,
65
+ logger
66
+ });
67
+ return {
68
+ success: true,
69
+ data: fallbackResult.data,
70
+ source: 'v1.1'
71
+ };
72
+ } catch (fallbackError) {
73
+ loggerError(logger, `Both v2 and v1.1 APIs failed for ${v2Endpoint}`, fallbackError);
74
+ }
75
+ }
76
+ return {
77
+ success: false,
78
+ error: e,
79
+ source: 'failed'
80
+ };
81
+ }
82
+ }
83
+ function normalizeUserData(user) {
84
+ let apiVersion = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'v1.1';
85
+ if (!user) return user;
86
+ const normalized = {
87
+ ...user
88
+ };
89
+ if (apiVersion === 'v2') {
90
+ normalized.screen_name = user.username;
91
+ normalized.id_str = user.id;
92
+ normalized.profile_image_url_https = user.profile_image_url;
93
+ } else {
94
+ normalized.username = user.screen_name;
95
+ normalized.id = user.id_str;
96
+ normalized.profile_image_url = user.profile_image_url_https;
97
+ }
98
+ return normalized;
99
+ }
100
+
101
+ /**
102
+ * Normalizes an array of user data or a single user object
103
+ * @param {Object|Array} users - User object or array of user objects from Twitter API
104
+ * @param {string} apiVersion - API version used ('v1.1' or 'v2')
105
+ * @returns {Object|Array} Normalized user data with both screen_name and username
106
+ */
107
+ function normalizeUsersData(users) {
108
+ let apiVersion = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'v1.1';
109
+ if (!users) return users;
110
+ if (Array.isArray(users)) {
111
+ return users.map(user => normalizeUserData(user, apiVersion));
112
+ }
113
+ return normalizeUserData(users, apiVersion);
114
+ }
9
115
  export function addOAuthToToken(token, TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET) {
10
116
  if (!token.oauth) {
11
117
  token.oauth = OAuth({
@@ -30,28 +136,46 @@ export async function getUserInfoFromHandles(token, handles, logger) {
30
136
  loggerWarn(logger, `No handles provided to twitterNative getUserInfoFromHandles`);
31
137
  return [];
32
138
  }
33
- let query = `users/lookup.json?screen_name=${handlesJoin}`;
34
- let result = await postRequest({
35
- token,
36
- uri: query,
37
- logger
139
+
140
+ // Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
141
+ // v2 API supports up to 100 usernames per request vs v1.1's limit
142
+ const result = await makeTwitterV2Request('users/by',
143
+ // v2 endpoint
144
+ `users/lookup.json?screen_name=${handlesJoin}`,
145
+ // v1.1 fallback
146
+ token, logger, {
147
+ v2Params: {
148
+ usernames: handlesJoin
149
+ },
150
+ fallbackMethod: 'post'
38
151
  });
39
- return result.data;
152
+ const normalizedData = normalizeUsersData(result.data, result.source);
153
+ return normalizedData || [];
40
154
  } catch (e) {
41
- loggerError(logger, `Error getting UserInfo`, e);
155
+ loggerError(logger, `Unexpected error in getUserInfoFromHandles`, e);
156
+ return [];
42
157
  }
43
158
  }
44
159
  export async function getUserInfoFromHandle(token, handleId, logger) {
45
160
  try {
46
- let query = `users/show.json?user_id=${removePrefix(handleId)}`;
47
- let result = await getRequest({
48
- token,
49
- uri: query,
50
- logger
51
- });
52
- return result.data;
161
+ if (!handleId) {
162
+ loggerWarn(logger, `Invalid handleId provided to getUserInfoFromHandle: ${JSON.stringify(handleId)}`);
163
+ throw new Error(`Invalid handleId: ${handleId}`);
164
+ }
165
+ // Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
166
+ const result = await makeTwitterV2Request(`users/${removePrefix(handleId)}`,
167
+ // v2 endpoint
168
+ `users/show.json?user_id=${removePrefix(handleId)}`,
169
+ // v1.1 fallback
170
+ token, logger);
171
+ if (result.success) {
172
+ // Normalize the user data to ensure both screen_name and username are present
173
+ return normalizeUserData(result.data, result.source);
174
+ } else {
175
+ throw result.error || new Error('Failed to get user info from both APIs');
176
+ }
53
177
  } catch (e) {
54
- loggerError(logger, `Error getting UserInfo`, e);
178
+ loggerError(logger, `Error in getUserInfoFromHandle with handleId: ${JSON.stringify(handleId)}`, e);
55
179
  }
56
180
  }
57
181
  export async function getDirectMessageImage(token, imageUrl, logger) {
@@ -380,7 +504,7 @@ async function publishTweet(token, payload, query, isDirectMessage, logger) {
380
504
  throw err;
381
505
  }
382
506
  }
383
- async function postRequest(_ref) {
507
+ async function postRequest(_ref2) {
384
508
  let {
385
509
  token,
386
510
  uri,
@@ -388,7 +512,7 @@ async function postRequest(_ref) {
388
512
  attachUrlPrefix = true,
389
513
  convertPayloadToUri = true,
390
514
  logger
391
- } = _ref;
515
+ } = _ref2;
392
516
  return doRequest({
393
517
  requestMethod: 'post',
394
518
  token,
@@ -399,7 +523,7 @@ async function postRequest(_ref) {
399
523
  logger
400
524
  });
401
525
  }
402
- async function getRequest(_ref2) {
526
+ async function getRequest(_ref3) {
403
527
  let {
404
528
  token,
405
529
  uri,
@@ -407,7 +531,7 @@ async function getRequest(_ref2) {
407
531
  attachUrlPrefix = true,
408
532
  convertPayloadToUri = true,
409
533
  logger
410
- } = _ref2;
534
+ } = _ref3;
411
535
  return doRequest({
412
536
  requestMethod: 'get',
413
537
  payload,
@@ -418,7 +542,7 @@ async function getRequest(_ref2) {
418
542
  logger
419
543
  });
420
544
  }
421
- async function deleteRequest(_ref3) {
545
+ async function deleteRequest(_ref4) {
422
546
  let {
423
547
  token,
424
548
  uri,
@@ -426,7 +550,7 @@ async function deleteRequest(_ref3) {
426
550
  attachUrlPrefix = false,
427
551
  convertPayloadToUri = false,
428
552
  logger
429
- } = _ref3;
553
+ } = _ref4;
430
554
  return doRequest({
431
555
  requestMethod: 'delete',
432
556
  token,
@@ -442,7 +566,7 @@ function fixedEncodeURIComponent(str) {
442
566
  return '%' + c.charCodeAt(0).toString(16);
443
567
  });
444
568
  }
445
- async function doRequest(_ref4) {
569
+ async function doRequest(_ref5) {
446
570
  let {
447
571
  requestMethod,
448
572
  token,
@@ -451,7 +575,7 @@ async function doRequest(_ref4) {
451
575
  attachUrlPrefix = true,
452
576
  convertPayloadToUri = true,
453
577
  logger
454
- } = _ref4;
578
+ } = _ref5;
455
579
  if (payload && convertPayloadToUri) {
456
580
  uri = uri + (uri.endsWith('?') ? '' : '?') + Object.keys(payload).map(key => {
457
581
  return fixedEncodeURIComponent(key) + '=' + fixedEncodeURIComponent(payload[key]);
@@ -1,5 +1,8 @@
1
1
  export function removePrefix(document) {
2
- return document && document.replace(/id\:[a-zA-Z]+\.(com|net):/g, '');
2
+ if (!document || typeof document !== 'string') {
3
+ return document; // Return as-is if not a valid string
4
+ }
5
+ return document.replace(/id\:[a-zA-Z]+\.(com|net):/g, '');
3
6
  }
4
7
  export function twitterPrefixCheck(socialOriginType, value) {
5
8
  let result = value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltwater/conversations-api-services",
3
- "version": "1.1.22",
3
+ "version": "1.1.24",
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",
@@ -545,6 +545,20 @@ async function postApi(
545
545
 
546
546
  response = await request;
547
547
  } catch (err) {
548
+ // Handle specific case where content is already marked as spam
549
+ if (
550
+ err?.response?.body?.error?.error_subcode === 1446036 &&
551
+ err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request"
552
+ ) {
553
+ loggerInfo(logger, `Content already marked as spam - treating as successful hide operation`, {
554
+ responseBody: JSON.stringify(err.response.body),
555
+ });
556
+ return {
557
+ status: 200,
558
+ body: { success: true, message: "Content already hidden (marked as spam)" }
559
+ };
560
+ }
561
+
548
562
  loggerDebug(logger, `Failed to call facebook api`, err);
549
563
  throw err;
550
564
  }
@@ -232,6 +232,20 @@ export class FacebookApiClient {
232
232
  .query({ access_token: accessToken })
233
233
  .send(payload);
234
234
  } catch (err) {
235
+ // Handle specific case where content is already marked as spam
236
+ if (
237
+ err?.response?.body?.error?.error_subcode === 1446036 &&
238
+ err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request"
239
+ ) {
240
+ loggerChild.info(`Content already marked as spam - treating as successful hide operation for documentId ${documentId}`, {
241
+ responseBody: JSON.stringify(err.response.body),
242
+ });
243
+ return {
244
+ status: 200,
245
+ body: { success: true, message: "Content already hidden (marked as spam)" }
246
+ };
247
+ }
248
+
235
249
  let errorText = '';
236
250
  if (
237
251
  err &&
@@ -9,6 +9,8 @@ import {
9
9
  deleteMessage as deleteMessageNative,
10
10
  } from './linkedin.native.js'
11
11
 
12
+ const LINKEDIN_VERSION = '202511';
13
+
12
14
  export class LinkedInApiClient {
13
15
  constructor() {
14
16
  this.credentialsAPI = new CredentialsApiClient();
@@ -71,7 +73,7 @@ export class LinkedInApiClient {
71
73
  'X-RestLi-Protocol-Version': configuration.get(
72
74
  'LINKEDIN_API_VERSION'
73
75
  ),
74
- 'LinkedIn-Version': '202311',
76
+ 'LinkedIn-Version': LINKEDIN_VERSION,
75
77
  });
76
78
  } else {
77
79
  loggerChild.debug(`${documentId} - sending to linkedin `, {
@@ -86,7 +88,7 @@ export class LinkedInApiClient {
86
88
  'X-RestLi-Protocol-Version': configuration.get(
87
89
  'LINKEDIN_API_VERSION'
88
90
  ),
89
- 'LinkedIn-Version': '202311',
91
+ 'LinkedIn-Version': LINKEDIN_VERSION,
90
92
  })
91
93
  .send(payload);
92
94
  }
@@ -221,7 +223,7 @@ export class LinkedInApiClient {
221
223
  'X-RestLi-Protocol-Version': configuration.get(
222
224
  'LINKEDIN_API_VERSION'
223
225
  ),
224
- 'LinkedIn-Version': '202311',
226
+ 'LinkedIn-Version': LINKEDIN_VERSION,
225
227
  })
226
228
  .send(body);
227
229
 
@@ -272,7 +274,7 @@ export class LinkedInApiClient {
272
274
  'X-RestLi-Protocol-Version': configuration.get(
273
275
  'LINKEDIN_API_VERSION'
274
276
  ),
275
- 'LinkedIn-Version': '202311',
277
+ 'LinkedIn-Version': LINKEDIN_VERSION,
276
278
  })
277
279
  .send(body);
278
280
 
@@ -312,7 +314,7 @@ export class LinkedInApiClient {
312
314
  'X-RestLi-Protocol-Version': configuration.get(
313
315
  'LINKEDIN_API_VERSION'
314
316
  ),
315
- 'LinkedIn-Version': '202311',
317
+ 'LinkedIn-Version': LINKEDIN_VERSION,
316
318
  });
317
319
  } catch (error) {
318
320
  loggerChild.error('Error in getAllStats', error);
@@ -380,7 +382,7 @@ export class LinkedInApiClient {
380
382
  'X-RestLi-Protocol-Version': configuration.get(
381
383
  'LINKEDIN_API_VERSION'
382
384
  ),
383
- 'LinkedIn-Version': '202311',
385
+ 'LinkedIn-Version': LINKEDIN_VERSION,
384
386
  });
385
387
  } catch (error) {
386
388
  loggerChild.error('Error in getImageMedia', error);
@@ -439,7 +441,7 @@ export class LinkedInApiClient {
439
441
  'X-RestLi-Protocol-Version': configuration.get(
440
442
  'LINKEDIN_API_VERSION'
441
443
  ),
442
- 'LinkedIn-Version': '202311',
444
+ 'LinkedIn-Version': LINKEDIN_VERSION,
443
445
  });
444
446
  } catch (error) {
445
447
  loggerChild.error('Error in getVideoMedia', error);
@@ -513,7 +515,7 @@ export class LinkedInApiClient {
513
515
  'X-RestLi-Protocol-Version': configuration.get(
514
516
  'LINKEDIN_API_VERSION'
515
517
  ),
516
- 'LinkedIn-Version': '202311',
518
+ 'LinkedIn-Version': LINKEDIN_VERSION,
517
519
  })
518
520
  .send(data)
519
521
  .catch((err) => {
@@ -540,7 +542,7 @@ export async function getProfile(urn, token) {
540
542
  'X-RestLi-Protocol-Version': configuration.get(
541
543
  'LINKEDIN_API_VERSION'
542
544
  ),
543
- 'LinkedIn-Version': '202311',
545
+ 'LinkedIn-Version': LINKEDIN_VERSION,
544
546
  })
545
547
  .then((result) => result.body);
546
548
  } catch (error) {
@@ -5,7 +5,7 @@ import { loggerDebug, loggerError, loggerInfo } from '../../lib/logger.helpers.j
5
5
  const LINKEDIN_API = "https://api.linkedin.com/v2";
6
6
  const LINKEDIN_API_REST = "https://api.linkedin.com/rest";
7
7
  const LINKEDIN_API_VERSION = "2.0.0";
8
- const LINKEDIN_VERSION = "202411";
8
+ const LINKEDIN_VERSION = "202511";
9
9
 
10
10
  export async function like(token, externalId, socialAccountId, logger) {
11
11
  let response;
@@ -474,21 +474,21 @@ export async function getOrganization(urn, token, logger) {
474
474
 
475
475
  let organization;
476
476
  try {
477
+ // Use the newer REST API endpoint instead of deprecated v2/organizations
477
478
  organization = await superagent
478
- .get(`${LINKEDIN_API}/organizations/${fixedEncodeURIComponent(id)}`)
479
- .query({
480
- projection:
481
- '(id,localizedName,vanityName,logoV2(original~:playableStreams))',
482
- })
479
+ .get(`${LINKEDIN_API_REST}/organizationsLookup?ids=List(${fixedEncodeURIComponent(id)})`)
483
480
  .timeout(5000) // 5 seconds
484
481
  .set({
485
482
  Authorization: `Bearer ${token}`,
486
483
  'X-RestLi-Protocol-Version': LINKEDIN_API_VERSION,
487
484
  'LinkedIn-Version': LINKEDIN_VERSION,
488
485
  })
489
- .then((result) => result.body);
486
+ .then((result) => {
487
+ const res = result.body;
488
+ return res.results[id];
489
+ });
490
490
  } catch (error) {
491
- if (error.response.body.message.includes('ADMIN_ONLY')) {
491
+ if (error.response && error.response.body && error.response.body.message && error.response.body.message.includes('ADMIN_ONLY')) {
492
492
  return { id: 'private' };
493
493
  }
494
494
 
@@ -1,12 +1,105 @@
1
1
  import superagent from 'superagent';
2
2
  import { removePrefix } from '../../lib/externalId.helpers.js';
3
- import { loggerDebug, loggerError, loggerInfo } from '../../lib/logger.helpers.js';
3
+ import { loggerDebug, loggerError, loggerInfo, loggerWarn } from '../../lib/logger.helpers.js';
4
4
  import OAuth from 'oauth-1.0a';
5
5
  import crypto from 'crypto';
6
6
  import axios from 'axios';
7
7
  import fs from 'fs';
8
8
  import Twit from '@meltwater/social-twit';
9
9
 
10
+ const TWITTER_API_CONFIG = {
11
+ v1_1: {
12
+ base_url: 'https://api.twitter.com/1.1',
13
+ },
14
+ v2: {
15
+ base_url: 'https://api.x.com/2',
16
+ default_user_fields: 'id,name,username,profile_image_url,public_metrics,description,created_at,verified,protected'
17
+ }
18
+ };
19
+
20
+ // Helper function to create v2 API URLs with consistent field selection
21
+ function createTwitterV2Url(endpoint, additionalParams = {}) {
22
+ const baseUrl = `${TWITTER_API_CONFIG.v2.base_url}/${endpoint}`;
23
+ const defaultFields = { 'user.fields': TWITTER_API_CONFIG.v2.default_user_fields };
24
+ const allParams = { ...defaultFields, ...additionalParams };
25
+
26
+ const queryString = Object.entries(allParams)
27
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
28
+ .join('&');
29
+
30
+ return queryString ? `${baseUrl}?${queryString}` : baseUrl;
31
+ }
32
+
33
+ // Helper function to make v2 API requests with fallback to v1.1
34
+ async function makeTwitterV2Request(v2Endpoint, v1FallbackQuery, token, logger, requestOptions = {}) {
35
+ // Try v2 API first - it supports OAuth 1.0a for user lookup endpoints and has better rate limits!
36
+ try {
37
+ const v2Url = createTwitterV2Url(v2Endpoint, requestOptions.v2Params);
38
+
39
+ const result = await getRequest({
40
+ token,
41
+ uri: v2Url,
42
+ attachUrlPrefix: false,
43
+ convertPayloadToUri: false,
44
+ logger
45
+ });
46
+ return { success: true, data: result.data?.data, source: 'v2' };
47
+ } catch (e) {
48
+ loggerWarn(logger, `Twitter API v2 request failed for ${v2Endpoint}, falling back to v1.1`, e);
49
+
50
+ // Fallback to v1.1 if v2 fails
51
+ if (v1FallbackQuery && requestOptions.enableFallback !== false) {
52
+ try {
53
+ loggerInfo(logger, `Falling back to v1.1 API for ${v2Endpoint}`);
54
+ const fallbackResult = await (requestOptions.fallbackMethod === 'post' ? postRequest : getRequest)({
55
+ token,
56
+ uri: v1FallbackQuery,
57
+ logger
58
+ });
59
+ return { success: true, data: fallbackResult.data, source: 'v1.1' };
60
+ } catch (fallbackError) {
61
+ loggerError(logger, `Both v2 and v1.1 APIs failed for ${v2Endpoint}`, fallbackError);
62
+ }
63
+ }
64
+
65
+ return { success: false, error: e, source: 'failed' };
66
+ }
67
+ }
68
+
69
+ function normalizeUserData(user, apiVersion = 'v1.1') {
70
+ if (!user) return user;
71
+
72
+ const normalized = { ...user };
73
+
74
+ if (apiVersion === 'v2') {
75
+ normalized.screen_name = user.username;
76
+ normalized.id_str = user.id;
77
+ normalized.profile_image_url_https = user.profile_image_url;
78
+ } else {
79
+ normalized.username = user.screen_name;
80
+ normalized.id = user.id_str;
81
+ normalized.profile_image_url = user.profile_image_url_https;
82
+ }
83
+
84
+ return normalized;
85
+ }
86
+
87
+ /**
88
+ * Normalizes an array of user data or a single user object
89
+ * @param {Object|Array} users - User object or array of user objects from Twitter API
90
+ * @param {string} apiVersion - API version used ('v1.1' or 'v2')
91
+ * @returns {Object|Array} Normalized user data with both screen_name and username
92
+ */
93
+ function normalizeUsersData(users, apiVersion = 'v1.1') {
94
+ if (!users) return users;
95
+
96
+ if (Array.isArray(users)) {
97
+ return users.map(user => normalizeUserData(user, apiVersion));
98
+ }
99
+
100
+ return normalizeUserData(users, apiVersion);
101
+ }
102
+
10
103
 
11
104
 
12
105
  export function addOAuthToToken(token, TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET) {
@@ -37,21 +130,50 @@ export async function getUserInfoFromHandles(token, handles, logger) {
37
130
  loggerWarn(logger,`No handles provided to twitterNative getUserInfoFromHandles`);
38
131
  return [];
39
132
  }
40
- let query = `users/lookup.json?screen_name=${handlesJoin}`;
41
- let result = await postRequest({ token, uri: query, logger });
42
- return result.data;
133
+
134
+ // Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
135
+ // v2 API supports up to 100 usernames per request vs v1.1's limit
136
+ const result = await makeTwitterV2Request(
137
+ 'users/by', // v2 endpoint
138
+ `users/lookup.json?screen_name=${handlesJoin}`, // v1.1 fallback
139
+ token,
140
+ logger,
141
+ {
142
+ v2Params: { usernames: handlesJoin },
143
+ fallbackMethod: 'post'
144
+ }
145
+ );
146
+
147
+ const normalizedData = normalizeUsersData(result.data, result.source);
148
+ return normalizedData || [];
43
149
  } catch (e) {
44
- loggerError(logger,`Error getting UserInfo`, e);
150
+ loggerError(logger, `Unexpected error in getUserInfoFromHandles`, e);
151
+ return [];
45
152
  }
46
153
  }
47
154
 
48
155
  export async function getUserInfoFromHandle(token, handleId, logger) {
49
156
  try {
50
- let query = `users/show.json?user_id=${removePrefix(handleId)}`;
51
- let result = await getRequest({ token, uri: query, logger });
52
- return result.data;
157
+ if (!handleId) {
158
+ loggerWarn(logger, `Invalid handleId provided to getUserInfoFromHandle: ${JSON.stringify(handleId)}`);
159
+ throw new Error(`Invalid handleId: ${handleId}`);
160
+ }
161
+ // Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
162
+ const result = await makeTwitterV2Request(
163
+ `users/${removePrefix(handleId)}`, // v2 endpoint
164
+ `users/show.json?user_id=${removePrefix(handleId)}`, // v1.1 fallback
165
+ token,
166
+ logger
167
+ );
168
+
169
+ if (result.success) {
170
+ // Normalize the user data to ensure both screen_name and username are present
171
+ return normalizeUserData(result.data, result.source);
172
+ } else {
173
+ throw result.error || new Error('Failed to get user info from both APIs');
174
+ }
53
175
  } catch (e) {
54
- loggerError(logger,`Error getting UserInfo`, e);
176
+ loggerError(logger, `Error in getUserInfoFromHandle with handleId: ${JSON.stringify(handleId)}`, e);
55
177
  }
56
178
  }
57
179
 
@@ -1,5 +1,8 @@
1
1
  export function removePrefix(document) {
2
- return document && document.replace(/id\:[a-zA-Z]+\.(com|net):/g, '');
2
+ if (!document || typeof document !== 'string') {
3
+ return document; // Return as-is if not a valid string
4
+ }
5
+ return document.replace(/id\:[a-zA-Z]+\.(com|net):/g, '');
3
6
  }
4
7
 
5
8
  export function twitterPrefixCheck(socialOriginType, value) {