@meltwater/conversations-api-services 1.2.2 → 1.3.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Repository to contain all conversations api services shared across our services.
4
4
 
5
- ## Maintainers
5
+ ## Maintainers.
6
6
 
7
7
  > Maintained by Phoenix (phoenix@meltwater.com).
8
8
  > Contact us via #eng-team-phoenix in Slack.
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.addOAuthToToken = addOAuthToToken;
7
+ exports.createTweet = createTweet;
7
8
  exports.followUser = followUser;
8
9
  exports.getAuthenticatedUser = getAuthenticatedUser;
9
10
  exports.getBrandUserRelationship = getBrandUserRelationship;
@@ -505,6 +506,23 @@ async function getBrandUserRelationship(token, profileId, brandProfileId, logger
505
506
  (0, _loggerHelpers.loggerDebug)(logger, `Error in getting user brand friendship info: ${profileId} and ${brandProfileId}`, error);
506
507
  }
507
508
  }
509
+ async function createTweet(token, text, attachment, logger) {
510
+ let mediaId;
511
+ if (attachment) {
512
+ // Upload media for regular tweets (not DM, so use 'og' but will map to tweet_image/video/gif)
513
+ mediaId = await uploadMedia(attachment, token, 'og', logger);
514
+ }
515
+ const payload = {
516
+ text,
517
+ ...(attachment && {
518
+ media: {
519
+ media_ids: [mediaId]
520
+ }
521
+ })
522
+ };
523
+ let query = 'https://api.twitter.com/2/tweets';
524
+ return publishTweet(token, payload, query, false, logger);
525
+ }
508
526
  async function reply(token, text, attachment, inReplyToId, removeInReplyToId, logger) {
509
527
  let mediaId;
510
528
  if (attachment) {
@@ -535,8 +553,16 @@ async function privateMessage(token, text, attachment, profileId, logger) {
535
553
  let query = `https://api.x.com/2/dm_conversations/with/${(0, _externalIdHelpers.removePrefix)(profileId)}/messages`;
536
554
  let mediaId;
537
555
  if (attachment) {
556
+ (0, _loggerHelpers.loggerDebug)(logger, 'Uploading media for DM', {
557
+ attachmentUrl: attachment.url,
558
+ attachmentMimeType: attachment.mimeType
559
+ });
538
560
  // discussionType only matters if DM or Not
539
561
  mediaId = await uploadMedia(attachment, token, 'dm', logger);
562
+ (0, _loggerHelpers.loggerInfo)(logger, 'Media uploaded for DM', {
563
+ mediaId,
564
+ mediaIdType: typeof mediaId
565
+ });
540
566
  }
541
567
  const payload = {
542
568
  text: text || ' ',
@@ -546,6 +572,11 @@ async function privateMessage(token, text, attachment, profileId, logger) {
546
572
  }]
547
573
  })
548
574
  };
575
+ (0, _loggerHelpers.loggerDebug)(logger, 'Sending DM with payload', {
576
+ query,
577
+ payload: JSON.stringify(payload),
578
+ hasAttachment: !!attachment
579
+ });
549
580
  return publishDirectMessage(token, payload, query, logger);
550
581
  }
551
582
  async function retweetWithComment(token, text, attachment, logger) {
@@ -717,7 +748,7 @@ async function publishDirectMessage(token, payload, query, logger) {
717
748
  const normalizedResponse = normalizeTwitterResponse(response, 'dm', logger);
718
749
  return normalizedResponse;
719
750
  } catch (err) {
720
- (0, _loggerHelpers.loggerError)(logger, `Twitter DM exception details`, err);
751
+ (0, _loggerHelpers.loggerError)(logger, `Twitter DM send failed`, err);
721
752
  throw err;
722
753
  }
723
754
  }
@@ -883,7 +914,17 @@ async function uploadMedia(attachment, token, discussionType, logger) {
883
914
  try {
884
915
  filePath = generateFilePath(attachment.mimeType, attachment.__dirname);
885
916
  await downloadImage(attachment.url, filePath);
886
- const mediaCategory = discussionType === 'dm' ? 'dm_image' : 'tweet_image';
917
+
918
+ // Determine media category based on mime type and discussion type
919
+ const mimeType = attachment.mimeType.toLowerCase();
920
+ let mediaCategory;
921
+ if (mimeType.startsWith('video/')) {
922
+ mediaCategory = discussionType === 'dm' ? 'dm_video' : 'tweet_video';
923
+ } else if (mimeType === 'image/gif') {
924
+ mediaCategory = discussionType === 'dm' ? 'dm_gif' : 'tweet_gif';
925
+ } else {
926
+ mediaCategory = discussionType === 'dm' ? 'dm_image' : 'tweet_image';
927
+ }
887
928
  const fileStats = _fs.default.statSync(filePath);
888
929
  const fileSize = fileStats.size;
889
930
  const fileData = _fs.default.readFileSync(filePath);
@@ -891,7 +932,7 @@ async function uploadMedia(attachment, token, discussionType, logger) {
891
932
  // For small files, use simple upload
892
933
  if (fileSize < 5000000) {
893
934
  // 5MB
894
- const mediaIdString = await simpleMediaUpload(fileData, attachment.mimeType, token, logger);
935
+ const mediaIdString = await simpleMediaUpload(fileData, attachment.mimeType, mediaCategory, token, logger);
895
936
  removeMedia(filePath, logger);
896
937
  return mediaIdString;
897
938
  } else {
@@ -910,12 +951,14 @@ async function uploadMedia(attachment, token, discussionType, logger) {
910
951
  }
911
952
 
912
953
  // Simple upload for small media files
913
- async function simpleMediaUpload(fileData, mimeType, token, logger) {
954
+ async function simpleMediaUpload(fileData, mimeType, mediaCategory, token, logger) {
914
955
  try {
915
956
  const base64Data = fileData.toString('base64');
916
957
  const mediaType = mimeType.split('/')[0]; // 'image', 'video', etc.
917
958
 
918
959
  const url = 'https://upload.twitter.com/1.1/media/upload.json';
960
+
961
+ // Original working code: empty OAuth signature, let superagent handle content-type
919
962
  const {
920
963
  Authorization
921
964
  } = token.oauth.toHeader(token.oauth.authorize({
@@ -926,10 +969,9 @@ async function simpleMediaUpload(fileData, mimeType, token, logger) {
926
969
  key: token.token,
927
970
  secret: token.tokenSecret
928
971
  }));
929
- const result = await _superagent.default.post(url).set('Authorization', Authorization).send({
930
- media_data: base64Data,
931
- media_type: mimeType
932
- });
972
+
973
+ // Use .field() for multipart/form-data (OAuth ignores multipart body)
974
+ const result = await _superagent.default.post(url).set('Authorization', Authorization).field('media_data', base64Data).field('media_type', mimeType);
933
975
  (0, _loggerHelpers.loggerDebug)(logger, 'Media uploaded successfully (simple)', {
934
976
  media_id: result.body.media_id_string
935
977
  });
@@ -1000,18 +1042,47 @@ async function chunkedMediaUpload(filePath, fileSize, mimeType, mediaCategory, t
1000
1042
  // Make a media upload request
1001
1043
  async function mediaUploadRequest(params, token, logger) {
1002
1044
  const url = 'https://upload.twitter.com/1.1/media/upload.json';
1045
+
1046
+ // Separate query params from body params
1047
+ // Query params (everything except media_data) must be in OAuth signature
1048
+ const queryParams = {};
1049
+ const bodyParams = {};
1050
+ for (const key in params) {
1051
+ if (key === 'media_data') {
1052
+ // media_data goes in POST body only
1053
+ bodyParams[key] = params[key];
1054
+ } else {
1055
+ // All other params go in query string and OAuth signature
1056
+ queryParams[key] = String(params[key]);
1057
+ }
1058
+ }
1059
+
1060
+ // Build full URL with query params
1061
+ const queryString = Object.keys(queryParams).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`).join('&');
1062
+ const fullUrl = queryString ? `${url}?${queryString}` : url;
1063
+
1064
+ // OAuth signature: base URL + query params in data field
1003
1065
  const {
1004
1066
  Authorization
1005
1067
  } = token.oauth.toHeader(token.oauth.authorize({
1006
- url,
1068
+ url: url,
1069
+ // Base URL only
1007
1070
  method: 'post',
1008
- data: {}
1071
+ data: queryParams // Query params go here for signature
1009
1072
  }, {
1010
1073
  key: token.token,
1011
1074
  secret: token.tokenSecret
1012
1075
  }));
1013
1076
  try {
1014
- const result = await _superagent.default.post(url).set('Authorization', Authorization).send(params);
1077
+ // Don't set content-type, let superagent handle it
1078
+ // For commands with media_data, it will use application/x-www-form-urlencoded
1079
+ const request = _superagent.default.post(fullUrl).set('Authorization', Authorization);
1080
+
1081
+ // Only set type if we have body params
1082
+ if (Object.keys(bodyParams).length > 0) {
1083
+ request.type('form').send(bodyParams);
1084
+ }
1085
+ const result = await request;
1015
1086
  return result.body;
1016
1087
  } catch (e) {
1017
1088
  (0, _loggerHelpers.loggerError)(logger, `Media upload request failed for command: ${params.command}`, e);
@@ -481,6 +481,23 @@ export async function getBrandUserRelationship(token, profileId, brandProfileId,
481
481
  loggerDebug(logger, `Error in getting user brand friendship info: ${profileId} and ${brandProfileId}`, error);
482
482
  }
483
483
  }
484
+ export async function createTweet(token, text, attachment, logger) {
485
+ let mediaId;
486
+ if (attachment) {
487
+ // Upload media for regular tweets (not DM, so use 'og' but will map to tweet_image/video/gif)
488
+ mediaId = await uploadMedia(attachment, token, 'og', logger);
489
+ }
490
+ const payload = {
491
+ text,
492
+ ...(attachment && {
493
+ media: {
494
+ media_ids: [mediaId]
495
+ }
496
+ })
497
+ };
498
+ let query = 'https://api.twitter.com/2/tweets';
499
+ return publishTweet(token, payload, query, false, logger);
500
+ }
484
501
  export async function reply(token, text, attachment, inReplyToId, removeInReplyToId, logger) {
485
502
  let mediaId;
486
503
  if (attachment) {
@@ -511,8 +528,16 @@ export async function privateMessage(token, text, attachment, profileId, logger)
511
528
  let query = `https://api.x.com/2/dm_conversations/with/${removePrefix(profileId)}/messages`;
512
529
  let mediaId;
513
530
  if (attachment) {
531
+ loggerDebug(logger, 'Uploading media for DM', {
532
+ attachmentUrl: attachment.url,
533
+ attachmentMimeType: attachment.mimeType
534
+ });
514
535
  // discussionType only matters if DM or Not
515
536
  mediaId = await uploadMedia(attachment, token, 'dm', logger);
537
+ loggerInfo(logger, 'Media uploaded for DM', {
538
+ mediaId,
539
+ mediaIdType: typeof mediaId
540
+ });
516
541
  }
517
542
  const payload = {
518
543
  text: text || ' ',
@@ -522,6 +547,11 @@ export async function privateMessage(token, text, attachment, profileId, logger)
522
547
  }]
523
548
  })
524
549
  };
550
+ loggerDebug(logger, 'Sending DM with payload', {
551
+ query,
552
+ payload: JSON.stringify(payload),
553
+ hasAttachment: !!attachment
554
+ });
525
555
  return publishDirectMessage(token, payload, query, logger);
526
556
  }
527
557
  export async function retweetWithComment(token, text, attachment, logger) {
@@ -693,7 +723,7 @@ async function publishDirectMessage(token, payload, query, logger) {
693
723
  const normalizedResponse = normalizeTwitterResponse(response, 'dm', logger);
694
724
  return normalizedResponse;
695
725
  } catch (err) {
696
- loggerError(logger, `Twitter DM exception details`, err);
726
+ loggerError(logger, `Twitter DM send failed`, err);
697
727
  throw err;
698
728
  }
699
729
  }
@@ -859,7 +889,17 @@ async function uploadMedia(attachment, token, discussionType, logger) {
859
889
  try {
860
890
  filePath = generateFilePath(attachment.mimeType, attachment.__dirname);
861
891
  await downloadImage(attachment.url, filePath);
862
- const mediaCategory = discussionType === 'dm' ? 'dm_image' : 'tweet_image';
892
+
893
+ // Determine media category based on mime type and discussion type
894
+ const mimeType = attachment.mimeType.toLowerCase();
895
+ let mediaCategory;
896
+ if (mimeType.startsWith('video/')) {
897
+ mediaCategory = discussionType === 'dm' ? 'dm_video' : 'tweet_video';
898
+ } else if (mimeType === 'image/gif') {
899
+ mediaCategory = discussionType === 'dm' ? 'dm_gif' : 'tweet_gif';
900
+ } else {
901
+ mediaCategory = discussionType === 'dm' ? 'dm_image' : 'tweet_image';
902
+ }
863
903
  const fileStats = fs.statSync(filePath);
864
904
  const fileSize = fileStats.size;
865
905
  const fileData = fs.readFileSync(filePath);
@@ -867,7 +907,7 @@ async function uploadMedia(attachment, token, discussionType, logger) {
867
907
  // For small files, use simple upload
868
908
  if (fileSize < 5000000) {
869
909
  // 5MB
870
- const mediaIdString = await simpleMediaUpload(fileData, attachment.mimeType, token, logger);
910
+ const mediaIdString = await simpleMediaUpload(fileData, attachment.mimeType, mediaCategory, token, logger);
871
911
  removeMedia(filePath, logger);
872
912
  return mediaIdString;
873
913
  } else {
@@ -886,12 +926,14 @@ async function uploadMedia(attachment, token, discussionType, logger) {
886
926
  }
887
927
 
888
928
  // Simple upload for small media files
889
- async function simpleMediaUpload(fileData, mimeType, token, logger) {
929
+ async function simpleMediaUpload(fileData, mimeType, mediaCategory, token, logger) {
890
930
  try {
891
931
  const base64Data = fileData.toString('base64');
892
932
  const mediaType = mimeType.split('/')[0]; // 'image', 'video', etc.
893
933
 
894
934
  const url = 'https://upload.twitter.com/1.1/media/upload.json';
935
+
936
+ // Original working code: empty OAuth signature, let superagent handle content-type
895
937
  const {
896
938
  Authorization
897
939
  } = token.oauth.toHeader(token.oauth.authorize({
@@ -902,10 +944,9 @@ async function simpleMediaUpload(fileData, mimeType, token, logger) {
902
944
  key: token.token,
903
945
  secret: token.tokenSecret
904
946
  }));
905
- const result = await superagent.post(url).set('Authorization', Authorization).send({
906
- media_data: base64Data,
907
- media_type: mimeType
908
- });
947
+
948
+ // Use .field() for multipart/form-data (OAuth ignores multipart body)
949
+ const result = await superagent.post(url).set('Authorization', Authorization).field('media_data', base64Data).field('media_type', mimeType);
909
950
  loggerDebug(logger, 'Media uploaded successfully (simple)', {
910
951
  media_id: result.body.media_id_string
911
952
  });
@@ -976,18 +1017,47 @@ async function chunkedMediaUpload(filePath, fileSize, mimeType, mediaCategory, t
976
1017
  // Make a media upload request
977
1018
  async function mediaUploadRequest(params, token, logger) {
978
1019
  const url = 'https://upload.twitter.com/1.1/media/upload.json';
1020
+
1021
+ // Separate query params from body params
1022
+ // Query params (everything except media_data) must be in OAuth signature
1023
+ const queryParams = {};
1024
+ const bodyParams = {};
1025
+ for (const key in params) {
1026
+ if (key === 'media_data') {
1027
+ // media_data goes in POST body only
1028
+ bodyParams[key] = params[key];
1029
+ } else {
1030
+ // All other params go in query string and OAuth signature
1031
+ queryParams[key] = String(params[key]);
1032
+ }
1033
+ }
1034
+
1035
+ // Build full URL with query params
1036
+ const queryString = Object.keys(queryParams).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`).join('&');
1037
+ const fullUrl = queryString ? `${url}?${queryString}` : url;
1038
+
1039
+ // OAuth signature: base URL + query params in data field
979
1040
  const {
980
1041
  Authorization
981
1042
  } = token.oauth.toHeader(token.oauth.authorize({
982
- url,
1043
+ url: url,
1044
+ // Base URL only
983
1045
  method: 'post',
984
- data: {}
1046
+ data: queryParams // Query params go here for signature
985
1047
  }, {
986
1048
  key: token.token,
987
1049
  secret: token.tokenSecret
988
1050
  }));
989
1051
  try {
990
- const result = await superagent.post(url).set('Authorization', Authorization).send(params);
1052
+ // Don't set content-type, let superagent handle it
1053
+ // For commands with media_data, it will use application/x-www-form-urlencoded
1054
+ const request = superagent.post(fullUrl).set('Authorization', Authorization);
1055
+
1056
+ // Only set type if we have body params
1057
+ if (Object.keys(bodyParams).length > 0) {
1058
+ request.type('form').send(bodyParams);
1059
+ }
1060
+ const result = await request;
991
1061
  return result.body;
992
1062
  } catch (e) {
993
1063
  loggerError(logger, `Media upload request failed for command: ${params.command}`, e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltwater/conversations-api-services",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
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",
@@ -609,6 +609,25 @@ export async function getBrandUserRelationship(
609
609
  }
610
610
  }
611
611
 
612
+ export async function createTweet(token, text, attachment, logger) {
613
+ let mediaId;
614
+ if (attachment) {
615
+ // Upload media for regular tweets (not DM, so use 'og' but will map to tweet_image/video/gif)
616
+ mediaId = await uploadMedia(attachment, token, 'og', logger);
617
+ }
618
+ const payload = {
619
+ text,
620
+ ...(attachment && {
621
+ media: {
622
+ media_ids: [mediaId],
623
+ },
624
+ }),
625
+ };
626
+
627
+ let query = 'https://api.twitter.com/2/tweets';
628
+ return publishTweet(token, payload, query, false, logger);
629
+ }
630
+
612
631
  export async function reply(
613
632
  token,
614
633
  text,
@@ -658,8 +677,16 @@ export async function privateMessage(
658
677
  )}/messages`;
659
678
  let mediaId;
660
679
  if (attachment) {
680
+ loggerDebug(logger, 'Uploading media for DM', {
681
+ attachmentUrl: attachment.url,
682
+ attachmentMimeType: attachment.mimeType,
683
+ });
661
684
  // discussionType only matters if DM or Not
662
685
  mediaId = await uploadMedia(attachment, token, 'dm', logger);
686
+ loggerInfo(logger, 'Media uploaded for DM', {
687
+ mediaId,
688
+ mediaIdType: typeof mediaId,
689
+ });
663
690
  }
664
691
  const payload = {
665
692
  text: text || ' ',
@@ -671,6 +698,11 @@ export async function privateMessage(
671
698
  ],
672
699
  }),
673
700
  };
701
+ loggerDebug(logger, 'Sending DM with payload', {
702
+ query,
703
+ payload: JSON.stringify(payload),
704
+ hasAttachment: !!attachment,
705
+ });
674
706
  return publishDirectMessage(token, payload, query, logger);
675
707
  }
676
708
 
@@ -904,7 +936,7 @@ async function publishDirectMessage(token, payload, query, logger) {
904
936
 
905
937
  return normalizedResponse;
906
938
  } catch (err) {
907
- loggerError(logger, `Twitter DM exception details`, err);
939
+ loggerError(logger, `Twitter DM send failed`, err);
908
940
  throw err;
909
941
  }
910
942
  }
@@ -1097,8 +1129,20 @@ async function uploadMedia(attachment, token, discussionType, logger) {
1097
1129
  filePath = generateFilePath(attachment.mimeType, attachment.__dirname);
1098
1130
  await downloadImage(attachment.url, filePath);
1099
1131
 
1100
- const mediaCategory =
1101
- discussionType === 'dm' ? 'dm_image' : 'tweet_image';
1132
+ // Determine media category based on mime type and discussion type
1133
+ const mimeType = attachment.mimeType.toLowerCase();
1134
+ let mediaCategory;
1135
+
1136
+ if (mimeType.startsWith('video/')) {
1137
+ mediaCategory =
1138
+ discussionType === 'dm' ? 'dm_video' : 'tweet_video';
1139
+ } else if (mimeType === 'image/gif') {
1140
+ mediaCategory = discussionType === 'dm' ? 'dm_gif' : 'tweet_gif';
1141
+ } else {
1142
+ mediaCategory =
1143
+ discussionType === 'dm' ? 'dm_image' : 'tweet_image';
1144
+ }
1145
+
1102
1146
  const fileStats = fs.statSync(filePath);
1103
1147
  const fileSize = fileStats.size;
1104
1148
  const fileData = fs.readFileSync(filePath);
@@ -1109,6 +1153,7 @@ async function uploadMedia(attachment, token, discussionType, logger) {
1109
1153
  const mediaIdString = await simpleMediaUpload(
1110
1154
  fileData,
1111
1155
  attachment.mimeType,
1156
+ mediaCategory,
1112
1157
  token,
1113
1158
  logger
1114
1159
  );
@@ -1137,12 +1182,14 @@ async function uploadMedia(attachment, token, discussionType, logger) {
1137
1182
  }
1138
1183
 
1139
1184
  // Simple upload for small media files
1140
- async function simpleMediaUpload(fileData, mimeType, token, logger) {
1185
+ async function simpleMediaUpload(fileData, mimeType, mediaCategory, token, logger) {
1141
1186
  try {
1142
1187
  const base64Data = fileData.toString('base64');
1143
1188
  const mediaType = mimeType.split('/')[0]; // 'image', 'video', etc.
1144
1189
 
1145
1190
  const url = 'https://upload.twitter.com/1.1/media/upload.json';
1191
+
1192
+ // Original working code: empty OAuth signature, let superagent handle content-type
1146
1193
  const { Authorization } = token.oauth.toHeader(
1147
1194
  token.oauth.authorize(
1148
1195
  {
@@ -1157,13 +1204,12 @@ async function simpleMediaUpload(fileData, mimeType, token, logger) {
1157
1204
  )
1158
1205
  );
1159
1206
 
1207
+ // Use .field() for multipart/form-data (OAuth ignores multipart body)
1160
1208
  const result = await superagent
1161
1209
  .post(url)
1162
1210
  .set('Authorization', Authorization)
1163
- .send({
1164
- media_data: base64Data,
1165
- media_type: mimeType,
1166
- });
1211
+ .field('media_data', base64Data)
1212
+ .field('media_type', mimeType);
1167
1213
 
1168
1214
  loggerDebug(logger, 'Media uploaded successfully (simple)', {
1169
1215
  media_id: result.body.media_id_string,
@@ -1262,12 +1308,34 @@ async function chunkedMediaUpload(
1262
1308
  async function mediaUploadRequest(params, token, logger) {
1263
1309
  const url = 'https://upload.twitter.com/1.1/media/upload.json';
1264
1310
 
1311
+ // Separate query params from body params
1312
+ // Query params (everything except media_data) must be in OAuth signature
1313
+ const queryParams = {};
1314
+ const bodyParams = {};
1315
+
1316
+ for (const key in params) {
1317
+ if (key === 'media_data') {
1318
+ // media_data goes in POST body only
1319
+ bodyParams[key] = params[key];
1320
+ } else {
1321
+ // All other params go in query string and OAuth signature
1322
+ queryParams[key] = String(params[key]);
1323
+ }
1324
+ }
1325
+
1326
+ // Build full URL with query params
1327
+ const queryString = Object.keys(queryParams)
1328
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`)
1329
+ .join('&');
1330
+ const fullUrl = queryString ? `${url}?${queryString}` : url;
1331
+
1332
+ // OAuth signature: base URL + query params in data field
1265
1333
  const { Authorization } = token.oauth.toHeader(
1266
1334
  token.oauth.authorize(
1267
1335
  {
1268
- url,
1336
+ url: url, // Base URL only
1269
1337
  method: 'post',
1270
- data: {},
1338
+ data: queryParams, // Query params go here for signature
1271
1339
  },
1272
1340
  {
1273
1341
  key: token.token,
@@ -1277,11 +1345,18 @@ async function mediaUploadRequest(params, token, logger) {
1277
1345
  );
1278
1346
 
1279
1347
  try {
1280
- const result = await superagent
1281
- .post(url)
1282
- .set('Authorization', Authorization)
1283
- .send(params);
1348
+ // Don't set content-type, let superagent handle it
1349
+ // For commands with media_data, it will use application/x-www-form-urlencoded
1350
+ const request = superagent
1351
+ .post(fullUrl)
1352
+ .set('Authorization', Authorization);
1353
+
1354
+ // Only set type if we have body params
1355
+ if (Object.keys(bodyParams).length > 0) {
1356
+ request.type('form').send(bodyParams);
1357
+ }
1284
1358
 
1359
+ const result = await request;
1285
1360
  return result.body;
1286
1361
  } catch (e) {
1287
1362
  loggerError(