@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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
1101
|
-
|
|
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
|
-
.
|
|
1164
|
-
|
|
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
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
.
|
|
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(
|