@meltwater/conversations-api-services 1.1.24 → 1.2.1

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,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.addOAuthToToken = addOAuthToToken;
7
7
  exports.followUser = followUser;
8
+ exports.getAuthenticatedUser = getAuthenticatedUser;
8
9
  exports.getBrandUserRelationship = getBrandUserRelationship;
9
10
  exports.getCurrentInfo = getCurrentInfo;
10
11
  exports.getDirectMessageImage = getDirectMessageImage;
@@ -27,7 +28,6 @@ var _oauth = _interopRequireDefault(require("oauth-1.0a"));
27
28
  var _crypto = _interopRequireDefault(require("crypto"));
28
29
  var _axios = _interopRequireDefault(require("axios"));
29
30
  var _fs = _interopRequireDefault(require("fs"));
30
- var _socialTwit = _interopRequireDefault(require("@meltwater/social-twit"));
31
31
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
32
32
  const TWITTER_API_CONFIG = {
33
33
  v1_1: {
@@ -135,6 +135,28 @@ function normalizeUsersData(users) {
135
135
  }
136
136
  return normalizeUserData(users, apiVersion);
137
137
  }
138
+
139
+ /**
140
+ * Get authenticated user information using Twitter API v2
141
+ * @param {Object} token - OAuth token object
142
+ * @param {Object} logger - Logger instance
143
+ * @returns {Promise<Object>} User data from v2 API
144
+ */
145
+ async function getAuthenticatedUser(token, logger) {
146
+ try {
147
+ const result = await getRequest({
148
+ token,
149
+ uri: 'https://api.x.com/2/users/me',
150
+ attachUrlPrefix: false,
151
+ convertPayloadToUri: false,
152
+ logger
153
+ });
154
+ return result.data;
155
+ } catch (e) {
156
+ (0, _loggerHelpers.loggerError)(logger, `Error getting authenticated user info`, e);
157
+ throw e;
158
+ }
159
+ }
138
160
  function addOAuthToToken(token, TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET) {
139
161
  if (!token.oauth) {
140
162
  token.oauth = (0, _oauth.default)({
@@ -230,7 +252,7 @@ async function getCurrentInfo(token, externalId, logger) {
230
252
  });
231
253
 
232
254
  // this makes it so 'retweeted' is only true on a retweet that isn't a comment
233
- // sometimes 'retweeted' is true if user retweeted with comment, sometimes not :sadness:
255
+ // sometimes 'retweeted' is true if user retweeted with comment, sometimes not
234
256
  result.data.retweeted = !!result.data.current_user_retweet;
235
257
  return result.data.data[0].public_metrics;
236
258
  } catch (error) {
@@ -293,12 +315,20 @@ async function unRetweet(token, sourceId, externalId, logger) {
293
315
  }
294
316
  async function like(token, externalId, logger) {
295
317
  try {
318
+ // Get the authenticated user's ID first
319
+ const userInfo = await getAuthenticatedUser(token, logger);
320
+ const userId = userInfo.data.id;
296
321
  let response = await postRequest({
297
322
  token,
298
- uri: 'favorites/create.json?id=' + (0, _externalIdHelpers.removePrefix)(externalId),
323
+ uri: `https://api.x.com/2/users/${userId}/likes`,
324
+ payload: {
325
+ tweet_id: (0, _externalIdHelpers.removePrefix)(externalId)
326
+ },
327
+ attachUrlPrefix: false,
328
+ convertPayloadToUri: false,
299
329
  logger
300
330
  });
301
- if (response.data.favorited !== undefined && response.resp.statusCode === 200) {
331
+ if (response.data.data?.liked !== undefined && response.resp.statusCode === 200) {
302
332
  return true;
303
333
  } else {
304
334
  (0, _loggerHelpers.loggerInfo)(logger, 'Twitter Error in like user statusCode non 200 ', {
@@ -313,12 +343,17 @@ async function like(token, externalId, logger) {
313
343
  }
314
344
  async function unLike(token, externalId, logger) {
315
345
  try {
316
- let response = await postRequest({
346
+ // Get the authenticated user's ID first
347
+ const userInfo = await getAuthenticatedUser(token, logger);
348
+ const userId = userInfo.data.id;
349
+ let response = await deleteRequest({
317
350
  token,
318
- uri: 'favorites/destroy.json?id=' + (0, _externalIdHelpers.removePrefix)(externalId),
351
+ uri: `https://api.x.com/2/users/${userId}/likes/${(0, _externalIdHelpers.removePrefix)(externalId)}`,
352
+ attachUrlPrefix: false,
353
+ convertPayloadToUri: false,
319
354
  logger
320
355
  });
321
- if (response.data.favorited !== undefined && response.resp.statusCode === 200) {
356
+ if (response.data.data?.liked !== undefined && response.resp.statusCode === 200) {
322
357
  return true;
323
358
  } else {
324
359
  return false;
@@ -330,16 +365,20 @@ async function unLike(token, externalId, logger) {
330
365
  }
331
366
  async function followUser(token, profileId, logger) {
332
367
  try {
368
+ // Get the authenticated user's ID first
369
+ const userInfo = await getAuthenticatedUser(token, logger);
370
+ const userId = userInfo.data.id;
333
371
  let response = await postRequest({
334
372
  token,
335
- uri: 'friendships/create.json',
373
+ uri: `https://api.x.com/2/users/${userId}/following`,
336
374
  payload: {
337
- user_id: (0, _externalIdHelpers.removePrefix)(profileId),
338
- follow: true
375
+ target_user_id: (0, _externalIdHelpers.removePrefix)(profileId)
339
376
  },
377
+ attachUrlPrefix: false,
378
+ convertPayloadToUri: false,
340
379
  logger
341
380
  });
342
- if (response.data.following !== undefined) {
381
+ if (response.data.data?.following !== undefined) {
343
382
  return true;
344
383
  } else {
345
384
  return false;
@@ -350,15 +389,17 @@ async function followUser(token, profileId, logger) {
350
389
  }
351
390
  async function unFollowUser(token, profileId, logger) {
352
391
  try {
353
- let response = await postRequest({
392
+ // Get the authenticated user's ID first
393
+ const userInfo = await getAuthenticatedUser(token, logger);
394
+ const userId = userInfo.data.id;
395
+ let response = await deleteRequest({
354
396
  token,
355
- uri: 'friendships/destroy.json',
356
- payload: {
357
- user_id: (0, _externalIdHelpers.removePrefix)(profileId)
358
- },
397
+ uri: `https://api.x.com/2/users/${userId}/following/${(0, _externalIdHelpers.removePrefix)(profileId)}`,
398
+ attachUrlPrefix: false,
399
+ convertPayloadToUri: false,
359
400
  logger
360
401
  });
361
- if (response.data.following !== undefined) {
402
+ if (response.data.data?.following !== undefined) {
362
403
  return true;
363
404
  } else {
364
405
  return false;
@@ -369,26 +410,34 @@ async function unFollowUser(token, profileId, logger) {
369
410
  }
370
411
  async function userFollowStatus(token, profileId, logger) {
371
412
  try {
413
+ // Get the authenticated user's ID first
414
+ const userInfo = await getAuthenticatedUser(token, logger);
415
+ const userId = userInfo.data.id;
416
+
417
+ // Check if authenticated user is following the target user
418
+ // We need to check the following list with pagination support
372
419
  let response = await getRequest({
373
420
  token,
374
- uri: 'friendships/lookup.json',
421
+ uri: `https://api.x.com/2/users/${userId}/following`,
375
422
  payload: {
376
- user_id: (0, _externalIdHelpers.removePrefix)(profileId)
423
+ max_results: 1000 // Max allowed per request
377
424
  },
425
+ attachUrlPrefix: false,
426
+ convertPayloadToUri: true,
378
427
  logger
379
428
  });
380
- if (response.data.length > 0) {
381
- if (response.data[0].connections.length > 0) {
382
- for (var i = 0; i < response.data[0].connections.length; i++) {
383
- if (response.data[0].connections[i] === 'following') {
384
- return true;
385
- }
386
- }
387
- return false;
388
- } else {
389
- return false;
429
+ if (response.data?.data) {
430
+ // Check if the profileId is in the following list
431
+ const isFollowing = response.data.data.some(user => user.id === (0, _externalIdHelpers.removePrefix)(profileId));
432
+ if (isFollowing) {
433
+ return true;
390
434
  }
435
+
436
+ // If not found and there's a next page, we might need to paginate
437
+ // For now, return false if not in first page
438
+ return false;
391
439
  }
440
+ return false;
392
441
  } catch (error) {
393
442
  (0, _loggerHelpers.loggerDebug)(logger, `Error in getting user follow status: ${profileId}`, error);
394
443
  if (error.message != 'Bad Request') {
@@ -400,16 +449,58 @@ async function userFollowStatus(token, profileId, logger) {
400
449
  }
401
450
  async function getBrandUserRelationship(token, profileId, brandProfileId, logger) {
402
451
  try {
403
- let {
404
- data: {
405
- relationship
406
- } = {}
407
- } = await getRequest({
408
- token,
409
- uri: `friendships/show.json?source_id=${brandProfileId}&target_id=${(0, _externalIdHelpers.removePrefix)(profileId)}`,
410
- logger
411
- });
412
- return relationship;
452
+ // In v2 API, we need to make separate calls to check following/followers status
453
+ // Check if brand follows user
454
+ let brandFollowsUser = false;
455
+ let userFollowsBrand = false;
456
+ try {
457
+ const followingResponse = await getRequest({
458
+ token,
459
+ uri: `https://api.x.com/2/users/${brandProfileId}/following`,
460
+ payload: {
461
+ max_results: 1000
462
+ },
463
+ attachUrlPrefix: false,
464
+ convertPayloadToUri: true,
465
+ logger
466
+ });
467
+ if (followingResponse.data?.data) {
468
+ brandFollowsUser = followingResponse.data.data.some(user => user.id === (0, _externalIdHelpers.removePrefix)(profileId));
469
+ }
470
+ } catch (e) {
471
+ (0, _loggerHelpers.loggerWarn)(logger, 'Error checking if brand follows user', e);
472
+ }
473
+ try {
474
+ const followersResponse = await getRequest({
475
+ token,
476
+ uri: `https://api.x.com/2/users/${brandProfileId}/followers`,
477
+ payload: {
478
+ max_results: 1000
479
+ },
480
+ attachUrlPrefix: false,
481
+ convertPayloadToUri: true,
482
+ logger
483
+ });
484
+ if (followersResponse.data?.data) {
485
+ userFollowsBrand = followersResponse.data.data.some(user => user.id === (0, _externalIdHelpers.removePrefix)(profileId));
486
+ }
487
+ } catch (e) {
488
+ (0, _loggerHelpers.loggerWarn)(logger, 'Error checking if user follows brand', e);
489
+ }
490
+
491
+ // Build a relationship object similar to v1.1 format
492
+ return {
493
+ source: {
494
+ id_str: brandProfileId,
495
+ following: brandFollowsUser,
496
+ followed_by: userFollowsBrand
497
+ },
498
+ target: {
499
+ id_str: (0, _externalIdHelpers.removePrefix)(profileId),
500
+ following: userFollowsBrand,
501
+ followed_by: brandFollowsUser
502
+ }
503
+ };
413
504
  } catch (error) {
414
505
  (0, _loggerHelpers.loggerDebug)(logger, `Error in getting user brand friendship info: ${profileId} and ${brandProfileId}`, error);
415
506
  }
@@ -441,34 +532,21 @@ async function reply(token, text, attachment, inReplyToId, removeInReplyToId, lo
441
532
  return publishTweet(token, payload, query, false, logger);
442
533
  }
443
534
  async function privateMessage(token, text, attachment, profileId, logger) {
444
- let query = 'direct_messages/events/new.json';
535
+ let query = `https://api.x.com/2/dm_conversations/with/${(0, _externalIdHelpers.removePrefix)(profileId)}/messages`;
445
536
  let mediaId;
446
537
  if (attachment) {
447
538
  // discussionType only matters if DM or Not
448
539
  mediaId = await uploadMedia(attachment, token, 'dm', logger);
449
540
  }
450
541
  const payload = {
451
- event: {
452
- type: 'message_create',
453
- message_create: {
454
- target: {
455
- recipient_id: (0, _externalIdHelpers.removePrefix)(profileId)
456
- },
457
- message_data: {
458
- text: text || ' ',
459
- ...(attachment && {
460
- attachment: {
461
- type: 'media',
462
- media: {
463
- id: mediaId
464
- }
465
- }
466
- })
467
- }
468
- }
469
- }
542
+ text: text || ' ',
543
+ ...(attachment && {
544
+ attachments: [{
545
+ media_id: mediaId
546
+ }]
547
+ })
470
548
  };
471
- return publishTweet(token, payload, query, true, logger);
549
+ return publishDirectMessage(token, payload, query, logger);
472
550
  }
473
551
  async function retweetWithComment(token, text, attachment, logger) {
474
552
  let mediaId;
@@ -527,6 +605,35 @@ async function publishTweet(token, payload, query, isDirectMessage, logger) {
527
605
  throw err;
528
606
  }
529
607
  }
608
+ async function publishDirectMessage(token, payload, query, logger) {
609
+ try {
610
+ let nativeResponse = await postRequest({
611
+ token,
612
+ uri: query,
613
+ payload,
614
+ convertPayloadToUri: false,
615
+ attachUrlPrefix: false
616
+ });
617
+ (0, _loggerHelpers.loggerInfo)(logger, `finished sending DM via Twitter API v2`, {
618
+ data: JSON.stringify(nativeResponse.data)
619
+ });
620
+ const response = nativeResponse.resp ? {
621
+ statusCode: nativeResponse.resp.statusCode,
622
+ statusMessage: nativeResponse.resp.statusCode === 201 || nativeResponse.resp.statusCode === 200 ? 'Success' : 'Unknown Failure',
623
+ data: nativeResponse.data.data
624
+ } : {
625
+ statusCode: nativeResponse.statusCode,
626
+ statusMessage: nativeResponse.message
627
+ };
628
+ (0, _loggerHelpers.loggerDebug)(logger, `Twitter DM response is`, {
629
+ response: JSON.stringify(response)
630
+ });
631
+ return response;
632
+ } catch (err) {
633
+ (0, _loggerHelpers.loggerError)(logger, `Twitter DM exception details`, err);
634
+ throw err;
635
+ }
636
+ }
530
637
  async function postRequest(_ref2) {
531
638
  let {
532
639
  token,
@@ -685,32 +792,177 @@ function generateFilePath(mimeType, dirname) {
685
792
  }
686
793
  // local
687
794
  async function uploadMedia(attachment, token, discussionType, logger) {
688
- return new Promise(async (resolve, reject) => {
689
- let filePath = generateFilePath(attachment.mimeType, attachment.__dirname);
795
+ let filePath;
796
+ try {
797
+ filePath = generateFilePath(attachment.mimeType, attachment.__dirname);
690
798
  await downloadImage(attachment.url, filePath);
691
- const T = new _socialTwit.default({
692
- consumer_key: token.consumer_key,
693
- consumer_secret: token.consumer_secret,
694
- access_token: token.token,
695
- access_token_secret: token.tokenSecret
799
+ const mediaCategory = discussionType === 'dm' ? 'dm_image' : 'tweet_image';
800
+ const fileStats = _fs.default.statSync(filePath);
801
+ const fileSize = fileStats.size;
802
+ const fileData = _fs.default.readFileSync(filePath);
803
+
804
+ // For small files, use simple upload
805
+ if (fileSize < 5000000) {
806
+ // 5MB
807
+ const mediaIdString = await simpleMediaUpload(fileData, attachment.mimeType, token, logger);
808
+ removeMedia(filePath, logger);
809
+ return mediaIdString;
810
+ } else {
811
+ // For large files, use chunked upload
812
+ const mediaIdString = await chunkedMediaUpload(filePath, fileSize, attachment.mimeType, mediaCategory, token, logger);
813
+ removeMedia(filePath, logger);
814
+ return mediaIdString;
815
+ }
816
+ } catch (e) {
817
+ (0, _loggerHelpers.loggerError)(logger, `Failed uploading media`, e);
818
+ if (filePath) {
819
+ removeMedia(filePath, logger);
820
+ }
821
+ throw e;
822
+ }
823
+ }
824
+
825
+ // Simple upload for small media files
826
+ async function simpleMediaUpload(fileData, mimeType, token, logger) {
827
+ try {
828
+ const base64Data = fileData.toString('base64');
829
+ const mediaType = mimeType.split('/')[0]; // 'image', 'video', etc.
830
+
831
+ const url = 'https://upload.twitter.com/1.1/media/upload.json';
832
+ const {
833
+ Authorization
834
+ } = token.oauth.toHeader(token.oauth.authorize({
835
+ url,
836
+ method: 'post',
837
+ data: {}
838
+ }, {
839
+ key: token.token,
840
+ secret: token.tokenSecret
841
+ }));
842
+ const result = await _superagent.default.post(url).set('Authorization', Authorization).send({
843
+ media_data: base64Data,
844
+ media_type: mimeType
696
845
  });
697
- let mediaCategory = discussionType === 'dm' ? 'dm' : 'tweet';
698
- try {
699
- T.postMediaChunked({
700
- file_path: filePath
701
- }, mediaCategory, function (err, data, response) {
702
- if (err) {
703
- reject(err);
704
- }
705
- resolve(data.media_id_string);
706
- removeMedia(filePath, logger);
846
+ (0, _loggerHelpers.loggerDebug)(logger, 'Media uploaded successfully (simple)', {
847
+ media_id: result.body.media_id_string
848
+ });
849
+ return result.body.media_id_string;
850
+ } catch (e) {
851
+ (0, _loggerHelpers.loggerError)(logger, 'Error in simple media upload', e);
852
+ throw e;
853
+ }
854
+ }
855
+
856
+ // Chunked upload for large media files
857
+ async function chunkedMediaUpload(filePath, fileSize, mimeType, mediaCategory, token, logger) {
858
+ try {
859
+ const mediaType = mimeType.split('/')[0];
860
+
861
+ // Step 1: INIT
862
+ const initResponse = await mediaUploadRequest({
863
+ command: 'INIT',
864
+ total_bytes: fileSize,
865
+ media_type: mimeType,
866
+ media_category: mediaCategory
867
+ }, token, logger);
868
+ const mediaId = initResponse.media_id_string;
869
+ (0, _loggerHelpers.loggerDebug)(logger, 'Media upload INIT successful', {
870
+ mediaId
871
+ });
872
+
873
+ // Step 2: APPEND (upload in chunks)
874
+ const chunkSize = 5 * 1024 * 1024; // 5MB chunks
875
+ const fileBuffer = _fs.default.readFileSync(filePath);
876
+ let segmentIndex = 0;
877
+ for (let offset = 0; offset < fileSize; offset += chunkSize) {
878
+ const chunk = fileBuffer.slice(offset, Math.min(offset + chunkSize, fileSize));
879
+ const base64Chunk = chunk.toString('base64');
880
+ await mediaUploadRequest({
881
+ command: 'APPEND',
882
+ media_id: mediaId,
883
+ media_data: base64Chunk,
884
+ segment_index: segmentIndex
885
+ }, token, logger);
886
+ (0, _loggerHelpers.loggerDebug)(logger, `Media chunk ${segmentIndex} uploaded`, {
887
+ mediaId,
888
+ segmentIndex
707
889
  });
708
- } catch (e) {
709
- (0, _loggerHelpers.loggerError)(logger, `Failed posting media`, e);
710
- // this is just a safety precaution
711
- removeMedia(filePath, logger);
890
+ segmentIndex++;
712
891
  }
713
- });
892
+
893
+ // Step 3: FINALIZE
894
+ const finalizeResponse = await mediaUploadRequest({
895
+ command: 'FINALIZE',
896
+ media_id: mediaId
897
+ }, token, logger);
898
+ (0, _loggerHelpers.loggerDebug)(logger, 'Media upload FINALIZE successful', {
899
+ mediaId
900
+ });
901
+
902
+ // Step 4: Check processing status if needed
903
+ if (finalizeResponse.processing_info) {
904
+ await waitForMediaProcessing(mediaId, token, logger);
905
+ }
906
+ return mediaId;
907
+ } catch (e) {
908
+ (0, _loggerHelpers.loggerError)(logger, 'Error in chunked media upload', e);
909
+ throw e;
910
+ }
911
+ }
912
+
913
+ // Make a media upload request
914
+ async function mediaUploadRequest(params, token, logger) {
915
+ const url = 'https://upload.twitter.com/1.1/media/upload.json';
916
+ const {
917
+ Authorization
918
+ } = token.oauth.toHeader(token.oauth.authorize({
919
+ url,
920
+ method: 'post',
921
+ data: {}
922
+ }, {
923
+ key: token.token,
924
+ secret: token.tokenSecret
925
+ }));
926
+ try {
927
+ const result = await _superagent.default.post(url).set('Authorization', Authorization).send(params);
928
+ return result.body;
929
+ } catch (e) {
930
+ (0, _loggerHelpers.loggerError)(logger, `Media upload request failed for command: ${params.command}`, e);
931
+ throw e;
932
+ }
933
+ }
934
+
935
+ // Wait for media processing to complete
936
+ async function waitForMediaProcessing(mediaId, token, logger) {
937
+ let maxAttempts = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 20;
938
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
939
+ const statusResponse = await mediaUploadRequest({
940
+ command: 'STATUS',
941
+ media_id: mediaId
942
+ }, token, logger);
943
+ if (!statusResponse.processing_info) {
944
+ return; // Processing complete
945
+ }
946
+ const state = statusResponse.processing_info.state;
947
+ if (state === 'succeeded') {
948
+ (0, _loggerHelpers.loggerDebug)(logger, 'Media processing succeeded', {
949
+ mediaId
950
+ });
951
+ return;
952
+ }
953
+ if (state === 'failed') {
954
+ throw new Error(`Media processing failed: ${statusResponse.processing_info.error?.message || 'Unknown error'}`);
955
+ }
956
+
957
+ // Still processing, wait before checking again
958
+ const waitTime = statusResponse.processing_info.check_after_secs || 1;
959
+ (0, _loggerHelpers.loggerDebug)(logger, `Media still processing, waiting ${waitTime}s`, {
960
+ mediaId,
961
+ state
962
+ });
963
+ await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
964
+ }
965
+ throw new Error('Media processing timeout');
714
966
  }
715
967
  function removeMedia(file, logger) {
716
968
  try {
@@ -722,5 +974,4 @@ function removeMedia(file, logger) {
722
974
  } catch (e) {
723
975
  (0, _loggerHelpers.loggerError)(logger, `failed trying to remove media ${file} it may have already been removed`);
724
976
  }
725
- }
726
- ;
977
+ }