@meltwater/conversations-api-services 1.2.1 → 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 +1 -1
- package/dist/cjs/data-access/http/twitter.native.js +171 -13
- package/dist/cjs/data-access/index.js +1 -2
- package/dist/esm/data-access/http/twitter.native.js +170 -13
- package/package.json +1 -5
- package/src/data-access/http/twitter.native.js +217 -16
- package/link-for-testing.sh +0 -22
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) {
|
|
@@ -565,6 +596,84 @@ async function retweetWithComment(token, text, attachment, logger) {
|
|
|
565
596
|
let query = 'https://api.twitter.com/2/tweets';
|
|
566
597
|
return publishTweet(token, payload, query, false, logger);
|
|
567
598
|
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Normalizes Twitter API responses to ensure consistent structure across v1.1 and v2
|
|
602
|
+
* This is critical for backward compatibility - consumers expect certain fields to exist
|
|
603
|
+
*
|
|
604
|
+
* @param {Object} response - The response object to normalize
|
|
605
|
+
* @param {string} responseType - Type of response: 'dm' for direct messages, 'tweet' for tweets
|
|
606
|
+
* @param {Object} logger - Logger instance for debugging
|
|
607
|
+
* @param {Object} fullResponse - Full API response including includes (for extracting author_id from expansions)
|
|
608
|
+
* @returns {Object} Normalized response with consistent field structure
|
|
609
|
+
*/
|
|
610
|
+
function normalizeTwitterResponse(response, responseType, logger) {
|
|
611
|
+
let fullResponse = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
|
|
612
|
+
if (!response || !response.data) {
|
|
613
|
+
return response;
|
|
614
|
+
}
|
|
615
|
+
(0, _loggerHelpers.loggerDebug)(logger, `Normalizing Twitter API response (${responseType})`, {
|
|
616
|
+
beforeNormalization: JSON.stringify(response.data)
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Handle Twitter API v2 Direct Message responses
|
|
620
|
+
// v2 DM structure: { dm_event_id, dm_conversation_id }
|
|
621
|
+
// v1.1 expected structure: { id, author_id }
|
|
622
|
+
if (responseType === 'dm' && response.data.dm_event_id) {
|
|
623
|
+
const normalizedData = {
|
|
624
|
+
...response.data,
|
|
625
|
+
// Add v1.1-compatible fields for backward compatibility
|
|
626
|
+
id: response.data.dm_event_id,
|
|
627
|
+
// Note: author_id is intentionally not set here. The caller (twitterApi.client.js)
|
|
628
|
+
// will set the correct author_id from the original document since dm_conversation_id
|
|
629
|
+
// doesn't reliably indicate the sender (it's just "user1-user2" format)
|
|
630
|
+
author_id: null
|
|
631
|
+
};
|
|
632
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Normalized Twitter API v2 DM response to v1.1 format`, {
|
|
633
|
+
dm_event_id: response.data.dm_event_id,
|
|
634
|
+
normalized_id: normalizedData.id,
|
|
635
|
+
dm_conversation_id: response.data.dm_conversation_id,
|
|
636
|
+
normalized_author_id: normalizedData.author_id
|
|
637
|
+
});
|
|
638
|
+
return {
|
|
639
|
+
...response,
|
|
640
|
+
data: normalizedData
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Handle Twitter API v2 Tweet responses
|
|
645
|
+
// v2 has 'id' field but author_id is in expansions (includes.users)
|
|
646
|
+
if (responseType === 'tweet' && response.data.id) {
|
|
647
|
+
const normalizedData = {
|
|
648
|
+
...response.data
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// If author_id is missing but we have includes.users from expansions
|
|
652
|
+
if (!normalizedData.author_id && fullResponse?.includes?.users?.[0]?.id) {
|
|
653
|
+
normalizedData.author_id = fullResponse.includes.users[0].id;
|
|
654
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Added author_id to tweet response from expansions`, {
|
|
655
|
+
tweet_id: normalizedData.id,
|
|
656
|
+
author_id: normalizedData.author_id
|
|
657
|
+
});
|
|
658
|
+
} else if (normalizedData.author_id) {
|
|
659
|
+
(0, _loggerHelpers.loggerDebug)(logger, `Twitter v2 tweet response already has 'author_id'`, {
|
|
660
|
+
id: response.data.id,
|
|
661
|
+
author_id: normalizedData.author_id
|
|
662
|
+
});
|
|
663
|
+
} else {
|
|
664
|
+
(0, _loggerHelpers.loggerWarn)(logger, `Twitter v2 tweet response missing author_id and no expansions available`, {
|
|
665
|
+
id: response.data.id,
|
|
666
|
+
hasIncludes: !!fullResponse?.includes,
|
|
667
|
+
hasUsers: !!fullResponse?.includes?.users
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
...response,
|
|
672
|
+
data: normalizedData
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
return response;
|
|
676
|
+
}
|
|
568
677
|
async function publishTweet(token, payload, query, isDirectMessage, logger) {
|
|
569
678
|
try {
|
|
570
679
|
let nativeResponse = await postRequest({
|
|
@@ -599,7 +708,12 @@ async function publishTweet(token, payload, query, isDirectMessage, logger) {
|
|
|
599
708
|
(0, _loggerHelpers.loggerDebug)(logger, `Twitter the data response is`, {
|
|
600
709
|
response: JSON.stringify(response)
|
|
601
710
|
});
|
|
602
|
-
|
|
711
|
+
|
|
712
|
+
// Normalize the response to ensure backward compatibility with v1.1 structure
|
|
713
|
+
// Pass full nativeResponse to extract author_id from expansions if needed
|
|
714
|
+
const normalizedResponse = normalizeTwitterResponse(response, 'tweet', logger, nativeResponse.data // Full response with includes.users for author_id
|
|
715
|
+
);
|
|
716
|
+
return normalizedResponse;
|
|
603
717
|
} catch (err) {
|
|
604
718
|
(0, _loggerHelpers.loggerError)(logger, `Twitter publish exception details`, err);
|
|
605
719
|
throw err;
|
|
@@ -628,9 +742,13 @@ async function publishDirectMessage(token, payload, query, logger) {
|
|
|
628
742
|
(0, _loggerHelpers.loggerDebug)(logger, `Twitter DM response is`, {
|
|
629
743
|
response: JSON.stringify(response)
|
|
630
744
|
});
|
|
631
|
-
|
|
745
|
+
|
|
746
|
+
// Normalize the response to ensure backward compatibility with v1.1 structure
|
|
747
|
+
// This is CRITICAL - v2 DM responses have dm_event_id instead of id
|
|
748
|
+
const normalizedResponse = normalizeTwitterResponse(response, 'dm', logger);
|
|
749
|
+
return normalizedResponse;
|
|
632
750
|
} catch (err) {
|
|
633
|
-
(0, _loggerHelpers.loggerError)(logger, `Twitter DM
|
|
751
|
+
(0, _loggerHelpers.loggerError)(logger, `Twitter DM send failed`, err);
|
|
634
752
|
throw err;
|
|
635
753
|
}
|
|
636
754
|
}
|
|
@@ -796,7 +914,17 @@ async function uploadMedia(attachment, token, discussionType, logger) {
|
|
|
796
914
|
try {
|
|
797
915
|
filePath = generateFilePath(attachment.mimeType, attachment.__dirname);
|
|
798
916
|
await downloadImage(attachment.url, filePath);
|
|
799
|
-
|
|
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
|
+
}
|
|
800
928
|
const fileStats = _fs.default.statSync(filePath);
|
|
801
929
|
const fileSize = fileStats.size;
|
|
802
930
|
const fileData = _fs.default.readFileSync(filePath);
|
|
@@ -804,7 +932,7 @@ async function uploadMedia(attachment, token, discussionType, logger) {
|
|
|
804
932
|
// For small files, use simple upload
|
|
805
933
|
if (fileSize < 5000000) {
|
|
806
934
|
// 5MB
|
|
807
|
-
const mediaIdString = await simpleMediaUpload(fileData, attachment.mimeType, token, logger);
|
|
935
|
+
const mediaIdString = await simpleMediaUpload(fileData, attachment.mimeType, mediaCategory, token, logger);
|
|
808
936
|
removeMedia(filePath, logger);
|
|
809
937
|
return mediaIdString;
|
|
810
938
|
} else {
|
|
@@ -823,12 +951,14 @@ async function uploadMedia(attachment, token, discussionType, logger) {
|
|
|
823
951
|
}
|
|
824
952
|
|
|
825
953
|
// Simple upload for small media files
|
|
826
|
-
async function simpleMediaUpload(fileData, mimeType, token, logger) {
|
|
954
|
+
async function simpleMediaUpload(fileData, mimeType, mediaCategory, token, logger) {
|
|
827
955
|
try {
|
|
828
956
|
const base64Data = fileData.toString('base64');
|
|
829
957
|
const mediaType = mimeType.split('/')[0]; // 'image', 'video', etc.
|
|
830
958
|
|
|
831
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
|
|
832
962
|
const {
|
|
833
963
|
Authorization
|
|
834
964
|
} = token.oauth.toHeader(token.oauth.authorize({
|
|
@@ -839,10 +969,9 @@ async function simpleMediaUpload(fileData, mimeType, token, logger) {
|
|
|
839
969
|
key: token.token,
|
|
840
970
|
secret: token.tokenSecret
|
|
841
971
|
}));
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
});
|
|
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);
|
|
846
975
|
(0, _loggerHelpers.loggerDebug)(logger, 'Media uploaded successfully (simple)', {
|
|
847
976
|
media_id: result.body.media_id_string
|
|
848
977
|
});
|
|
@@ -913,18 +1042,47 @@ async function chunkedMediaUpload(filePath, fileSize, mimeType, mediaCategory, t
|
|
|
913
1042
|
// Make a media upload request
|
|
914
1043
|
async function mediaUploadRequest(params, token, logger) {
|
|
915
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
|
|
916
1065
|
const {
|
|
917
1066
|
Authorization
|
|
918
1067
|
} = token.oauth.toHeader(token.oauth.authorize({
|
|
919
|
-
url,
|
|
1068
|
+
url: url,
|
|
1069
|
+
// Base URL only
|
|
920
1070
|
method: 'post',
|
|
921
|
-
data:
|
|
1071
|
+
data: queryParams // Query params go here for signature
|
|
922
1072
|
}, {
|
|
923
1073
|
key: token.token,
|
|
924
1074
|
secret: token.tokenSecret
|
|
925
1075
|
}));
|
|
926
1076
|
try {
|
|
927
|
-
|
|
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;
|
|
928
1086
|
return result.body;
|
|
929
1087
|
} catch (e) {
|
|
930
1088
|
(0, _loggerHelpers.loggerError)(logger, `Media upload request failed for command: ${params.command}`, e);
|
|
@@ -31,8 +31,7 @@ var TwitterNative = _interopRequireWildcard(require("./http/twitter.native.js"))
|
|
|
31
31
|
var InstagramNative = _interopRequireWildcard(require("./http/instagram.native.js"));
|
|
32
32
|
var ThreadsNative = _interopRequireWildcard(require("./http/threads.native.js"));
|
|
33
33
|
var AssetManagerClient = _interopRequireWildcard(require("./http/assetManager.client.js"));
|
|
34
|
-
function
|
|
35
|
-
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
34
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
36
35
|
const DocumentHelperFunctions = {
|
|
37
36
|
...messageHelpers,
|
|
38
37
|
...applicationTagFunctions,
|
|
@@ -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) {
|
|
@@ -541,6 +571,84 @@ export async function retweetWithComment(token, text, attachment, logger) {
|
|
|
541
571
|
let query = 'https://api.twitter.com/2/tweets';
|
|
542
572
|
return publishTweet(token, payload, query, false, logger);
|
|
543
573
|
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Normalizes Twitter API responses to ensure consistent structure across v1.1 and v2
|
|
577
|
+
* This is critical for backward compatibility - consumers expect certain fields to exist
|
|
578
|
+
*
|
|
579
|
+
* @param {Object} response - The response object to normalize
|
|
580
|
+
* @param {string} responseType - Type of response: 'dm' for direct messages, 'tweet' for tweets
|
|
581
|
+
* @param {Object} logger - Logger instance for debugging
|
|
582
|
+
* @param {Object} fullResponse - Full API response including includes (for extracting author_id from expansions)
|
|
583
|
+
* @returns {Object} Normalized response with consistent field structure
|
|
584
|
+
*/
|
|
585
|
+
function normalizeTwitterResponse(response, responseType, logger) {
|
|
586
|
+
let fullResponse = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
|
|
587
|
+
if (!response || !response.data) {
|
|
588
|
+
return response;
|
|
589
|
+
}
|
|
590
|
+
loggerDebug(logger, `Normalizing Twitter API response (${responseType})`, {
|
|
591
|
+
beforeNormalization: JSON.stringify(response.data)
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Handle Twitter API v2 Direct Message responses
|
|
595
|
+
// v2 DM structure: { dm_event_id, dm_conversation_id }
|
|
596
|
+
// v1.1 expected structure: { id, author_id }
|
|
597
|
+
if (responseType === 'dm' && response.data.dm_event_id) {
|
|
598
|
+
const normalizedData = {
|
|
599
|
+
...response.data,
|
|
600
|
+
// Add v1.1-compatible fields for backward compatibility
|
|
601
|
+
id: response.data.dm_event_id,
|
|
602
|
+
// Note: author_id is intentionally not set here. The caller (twitterApi.client.js)
|
|
603
|
+
// will set the correct author_id from the original document since dm_conversation_id
|
|
604
|
+
// doesn't reliably indicate the sender (it's just "user1-user2" format)
|
|
605
|
+
author_id: null
|
|
606
|
+
};
|
|
607
|
+
loggerInfo(logger, `Normalized Twitter API v2 DM response to v1.1 format`, {
|
|
608
|
+
dm_event_id: response.data.dm_event_id,
|
|
609
|
+
normalized_id: normalizedData.id,
|
|
610
|
+
dm_conversation_id: response.data.dm_conversation_id,
|
|
611
|
+
normalized_author_id: normalizedData.author_id
|
|
612
|
+
});
|
|
613
|
+
return {
|
|
614
|
+
...response,
|
|
615
|
+
data: normalizedData
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Handle Twitter API v2 Tweet responses
|
|
620
|
+
// v2 has 'id' field but author_id is in expansions (includes.users)
|
|
621
|
+
if (responseType === 'tweet' && response.data.id) {
|
|
622
|
+
const normalizedData = {
|
|
623
|
+
...response.data
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// If author_id is missing but we have includes.users from expansions
|
|
627
|
+
if (!normalizedData.author_id && fullResponse?.includes?.users?.[0]?.id) {
|
|
628
|
+
normalizedData.author_id = fullResponse.includes.users[0].id;
|
|
629
|
+
loggerInfo(logger, `Added author_id to tweet response from expansions`, {
|
|
630
|
+
tweet_id: normalizedData.id,
|
|
631
|
+
author_id: normalizedData.author_id
|
|
632
|
+
});
|
|
633
|
+
} else if (normalizedData.author_id) {
|
|
634
|
+
loggerDebug(logger, `Twitter v2 tweet response already has 'author_id'`, {
|
|
635
|
+
id: response.data.id,
|
|
636
|
+
author_id: normalizedData.author_id
|
|
637
|
+
});
|
|
638
|
+
} else {
|
|
639
|
+
loggerWarn(logger, `Twitter v2 tweet response missing author_id and no expansions available`, {
|
|
640
|
+
id: response.data.id,
|
|
641
|
+
hasIncludes: !!fullResponse?.includes,
|
|
642
|
+
hasUsers: !!fullResponse?.includes?.users
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
...response,
|
|
647
|
+
data: normalizedData
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
return response;
|
|
651
|
+
}
|
|
544
652
|
async function publishTweet(token, payload, query, isDirectMessage, logger) {
|
|
545
653
|
try {
|
|
546
654
|
let nativeResponse = await postRequest({
|
|
@@ -575,7 +683,12 @@ async function publishTweet(token, payload, query, isDirectMessage, logger) {
|
|
|
575
683
|
loggerDebug(logger, `Twitter the data response is`, {
|
|
576
684
|
response: JSON.stringify(response)
|
|
577
685
|
});
|
|
578
|
-
|
|
686
|
+
|
|
687
|
+
// Normalize the response to ensure backward compatibility with v1.1 structure
|
|
688
|
+
// Pass full nativeResponse to extract author_id from expansions if needed
|
|
689
|
+
const normalizedResponse = normalizeTwitterResponse(response, 'tweet', logger, nativeResponse.data // Full response with includes.users for author_id
|
|
690
|
+
);
|
|
691
|
+
return normalizedResponse;
|
|
579
692
|
} catch (err) {
|
|
580
693
|
loggerError(logger, `Twitter publish exception details`, err);
|
|
581
694
|
throw err;
|
|
@@ -604,9 +717,13 @@ async function publishDirectMessage(token, payload, query, logger) {
|
|
|
604
717
|
loggerDebug(logger, `Twitter DM response is`, {
|
|
605
718
|
response: JSON.stringify(response)
|
|
606
719
|
});
|
|
607
|
-
|
|
720
|
+
|
|
721
|
+
// Normalize the response to ensure backward compatibility with v1.1 structure
|
|
722
|
+
// This is CRITICAL - v2 DM responses have dm_event_id instead of id
|
|
723
|
+
const normalizedResponse = normalizeTwitterResponse(response, 'dm', logger);
|
|
724
|
+
return normalizedResponse;
|
|
608
725
|
} catch (err) {
|
|
609
|
-
loggerError(logger, `Twitter DM
|
|
726
|
+
loggerError(logger, `Twitter DM send failed`, err);
|
|
610
727
|
throw err;
|
|
611
728
|
}
|
|
612
729
|
}
|
|
@@ -772,7 +889,17 @@ async function uploadMedia(attachment, token, discussionType, logger) {
|
|
|
772
889
|
try {
|
|
773
890
|
filePath = generateFilePath(attachment.mimeType, attachment.__dirname);
|
|
774
891
|
await downloadImage(attachment.url, filePath);
|
|
775
|
-
|
|
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
|
+
}
|
|
776
903
|
const fileStats = fs.statSync(filePath);
|
|
777
904
|
const fileSize = fileStats.size;
|
|
778
905
|
const fileData = fs.readFileSync(filePath);
|
|
@@ -780,7 +907,7 @@ async function uploadMedia(attachment, token, discussionType, logger) {
|
|
|
780
907
|
// For small files, use simple upload
|
|
781
908
|
if (fileSize < 5000000) {
|
|
782
909
|
// 5MB
|
|
783
|
-
const mediaIdString = await simpleMediaUpload(fileData, attachment.mimeType, token, logger);
|
|
910
|
+
const mediaIdString = await simpleMediaUpload(fileData, attachment.mimeType, mediaCategory, token, logger);
|
|
784
911
|
removeMedia(filePath, logger);
|
|
785
912
|
return mediaIdString;
|
|
786
913
|
} else {
|
|
@@ -799,12 +926,14 @@ async function uploadMedia(attachment, token, discussionType, logger) {
|
|
|
799
926
|
}
|
|
800
927
|
|
|
801
928
|
// Simple upload for small media files
|
|
802
|
-
async function simpleMediaUpload(fileData, mimeType, token, logger) {
|
|
929
|
+
async function simpleMediaUpload(fileData, mimeType, mediaCategory, token, logger) {
|
|
803
930
|
try {
|
|
804
931
|
const base64Data = fileData.toString('base64');
|
|
805
932
|
const mediaType = mimeType.split('/')[0]; // 'image', 'video', etc.
|
|
806
933
|
|
|
807
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
|
|
808
937
|
const {
|
|
809
938
|
Authorization
|
|
810
939
|
} = token.oauth.toHeader(token.oauth.authorize({
|
|
@@ -815,10 +944,9 @@ async function simpleMediaUpload(fileData, mimeType, token, logger) {
|
|
|
815
944
|
key: token.token,
|
|
816
945
|
secret: token.tokenSecret
|
|
817
946
|
}));
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
});
|
|
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);
|
|
822
950
|
loggerDebug(logger, 'Media uploaded successfully (simple)', {
|
|
823
951
|
media_id: result.body.media_id_string
|
|
824
952
|
});
|
|
@@ -889,18 +1017,47 @@ async function chunkedMediaUpload(filePath, fileSize, mimeType, mediaCategory, t
|
|
|
889
1017
|
// Make a media upload request
|
|
890
1018
|
async function mediaUploadRequest(params, token, logger) {
|
|
891
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
|
|
892
1040
|
const {
|
|
893
1041
|
Authorization
|
|
894
1042
|
} = token.oauth.toHeader(token.oauth.authorize({
|
|
895
|
-
url,
|
|
1043
|
+
url: url,
|
|
1044
|
+
// Base URL only
|
|
896
1045
|
method: 'post',
|
|
897
|
-
data:
|
|
1046
|
+
data: queryParams // Query params go here for signature
|
|
898
1047
|
}, {
|
|
899
1048
|
key: token.token,
|
|
900
1049
|
secret: token.tokenSecret
|
|
901
1050
|
}));
|
|
902
1051
|
try {
|
|
903
|
-
|
|
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;
|
|
904
1061
|
return result.body;
|
|
905
1062
|
} catch (e) {
|
|
906
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",
|
|
@@ -28,10 +28,6 @@
|
|
|
28
28
|
"keywords": [],
|
|
29
29
|
"author": "",
|
|
30
30
|
"license": "ISC",
|
|
31
|
-
"publishConfig": {
|
|
32
|
-
"registry": "https://registry.npmjs.org/",
|
|
33
|
-
"access": "public"
|
|
34
|
-
},
|
|
35
31
|
"bugs": {
|
|
36
32
|
"url": "https://github.com/meltwater/conversations-api-services/issues"
|
|
37
33
|
},
|
|
@@ -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
|
|
|
@@ -693,6 +725,111 @@ export async function retweetWithComment(token, text, attachment, logger) {
|
|
|
693
725
|
return publishTweet(token, payload, query, false, logger);
|
|
694
726
|
}
|
|
695
727
|
|
|
728
|
+
/**
|
|
729
|
+
* Normalizes Twitter API responses to ensure consistent structure across v1.1 and v2
|
|
730
|
+
* This is critical for backward compatibility - consumers expect certain fields to exist
|
|
731
|
+
*
|
|
732
|
+
* @param {Object} response - The response object to normalize
|
|
733
|
+
* @param {string} responseType - Type of response: 'dm' for direct messages, 'tweet' for tweets
|
|
734
|
+
* @param {Object} logger - Logger instance for debugging
|
|
735
|
+
* @param {Object} fullResponse - Full API response including includes (for extracting author_id from expansions)
|
|
736
|
+
* @returns {Object} Normalized response with consistent field structure
|
|
737
|
+
*/
|
|
738
|
+
function normalizeTwitterResponse(
|
|
739
|
+
response,
|
|
740
|
+
responseType,
|
|
741
|
+
logger,
|
|
742
|
+
fullResponse = null
|
|
743
|
+
) {
|
|
744
|
+
if (!response || !response.data) {
|
|
745
|
+
return response;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
loggerDebug(logger, `Normalizing Twitter API response (${responseType})`, {
|
|
749
|
+
beforeNormalization: JSON.stringify(response.data),
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// Handle Twitter API v2 Direct Message responses
|
|
753
|
+
// v2 DM structure: { dm_event_id, dm_conversation_id }
|
|
754
|
+
// v1.1 expected structure: { id, author_id }
|
|
755
|
+
if (responseType === 'dm' && response.data.dm_event_id) {
|
|
756
|
+
const normalizedData = {
|
|
757
|
+
...response.data,
|
|
758
|
+
// Add v1.1-compatible fields for backward compatibility
|
|
759
|
+
id: response.data.dm_event_id,
|
|
760
|
+
// Note: author_id is intentionally not set here. The caller (twitterApi.client.js)
|
|
761
|
+
// will set the correct author_id from the original document since dm_conversation_id
|
|
762
|
+
// doesn't reliably indicate the sender (it's just "user1-user2" format)
|
|
763
|
+
author_id: null,
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
loggerInfo(
|
|
767
|
+
logger,
|
|
768
|
+
`Normalized Twitter API v2 DM response to v1.1 format`,
|
|
769
|
+
{
|
|
770
|
+
dm_event_id: response.data.dm_event_id,
|
|
771
|
+
normalized_id: normalizedData.id,
|
|
772
|
+
dm_conversation_id: response.data.dm_conversation_id,
|
|
773
|
+
normalized_author_id: normalizedData.author_id,
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
return {
|
|
778
|
+
...response,
|
|
779
|
+
data: normalizedData,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Handle Twitter API v2 Tweet responses
|
|
784
|
+
// v2 has 'id' field but author_id is in expansions (includes.users)
|
|
785
|
+
if (responseType === 'tweet' && response.data.id) {
|
|
786
|
+
const normalizedData = { ...response.data };
|
|
787
|
+
|
|
788
|
+
// If author_id is missing but we have includes.users from expansions
|
|
789
|
+
if (
|
|
790
|
+
!normalizedData.author_id &&
|
|
791
|
+
fullResponse?.includes?.users?.[0]?.id
|
|
792
|
+
) {
|
|
793
|
+
normalizedData.author_id = fullResponse.includes.users[0].id;
|
|
794
|
+
|
|
795
|
+
loggerInfo(
|
|
796
|
+
logger,
|
|
797
|
+
`Added author_id to tweet response from expansions`,
|
|
798
|
+
{
|
|
799
|
+
tweet_id: normalizedData.id,
|
|
800
|
+
author_id: normalizedData.author_id,
|
|
801
|
+
}
|
|
802
|
+
);
|
|
803
|
+
} else if (normalizedData.author_id) {
|
|
804
|
+
loggerDebug(
|
|
805
|
+
logger,
|
|
806
|
+
`Twitter v2 tweet response already has 'author_id'`,
|
|
807
|
+
{
|
|
808
|
+
id: response.data.id,
|
|
809
|
+
author_id: normalizedData.author_id,
|
|
810
|
+
}
|
|
811
|
+
);
|
|
812
|
+
} else {
|
|
813
|
+
loggerWarn(
|
|
814
|
+
logger,
|
|
815
|
+
`Twitter v2 tweet response missing author_id and no expansions available`,
|
|
816
|
+
{
|
|
817
|
+
id: response.data.id,
|
|
818
|
+
hasIncludes: !!fullResponse?.includes,
|
|
819
|
+
hasUsers: !!fullResponse?.includes?.users,
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return {
|
|
825
|
+
...response,
|
|
826
|
+
data: normalizedData,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return response;
|
|
831
|
+
}
|
|
832
|
+
|
|
696
833
|
async function publishTweet(token, payload, query, isDirectMessage, logger) {
|
|
697
834
|
try {
|
|
698
835
|
let nativeResponse = await postRequest({
|
|
@@ -735,10 +872,21 @@ async function publishTweet(token, payload, query, isDirectMessage, logger) {
|
|
|
735
872
|
statusCode: nativeResponse.statusCode,
|
|
736
873
|
statusMessage: nativeResponse.message,
|
|
737
874
|
};
|
|
875
|
+
|
|
738
876
|
loggerDebug(logger, `Twitter the data response is`, {
|
|
739
877
|
response: JSON.stringify(response),
|
|
740
878
|
});
|
|
741
|
-
|
|
879
|
+
|
|
880
|
+
// Normalize the response to ensure backward compatibility with v1.1 structure
|
|
881
|
+
// Pass full nativeResponse to extract author_id from expansions if needed
|
|
882
|
+
const normalizedResponse = normalizeTwitterResponse(
|
|
883
|
+
response,
|
|
884
|
+
'tweet',
|
|
885
|
+
logger,
|
|
886
|
+
nativeResponse.data // Full response with includes.users for author_id
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
return normalizedResponse;
|
|
742
890
|
} catch (err) {
|
|
743
891
|
loggerError(logger, `Twitter publish exception details`, err);
|
|
744
892
|
throw err;
|
|
@@ -773,12 +921,22 @@ async function publishDirectMessage(token, payload, query, logger) {
|
|
|
773
921
|
statusCode: nativeResponse.statusCode,
|
|
774
922
|
statusMessage: nativeResponse.message,
|
|
775
923
|
};
|
|
924
|
+
|
|
776
925
|
loggerDebug(logger, `Twitter DM response is`, {
|
|
777
926
|
response: JSON.stringify(response),
|
|
778
927
|
});
|
|
779
|
-
|
|
928
|
+
|
|
929
|
+
// Normalize the response to ensure backward compatibility with v1.1 structure
|
|
930
|
+
// This is CRITICAL - v2 DM responses have dm_event_id instead of id
|
|
931
|
+
const normalizedResponse = normalizeTwitterResponse(
|
|
932
|
+
response,
|
|
933
|
+
'dm',
|
|
934
|
+
logger
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
return normalizedResponse;
|
|
780
938
|
} catch (err) {
|
|
781
|
-
loggerError(logger, `Twitter DM
|
|
939
|
+
loggerError(logger, `Twitter DM send failed`, err);
|
|
782
940
|
throw err;
|
|
783
941
|
}
|
|
784
942
|
}
|
|
@@ -971,8 +1129,20 @@ async function uploadMedia(attachment, token, discussionType, logger) {
|
|
|
971
1129
|
filePath = generateFilePath(attachment.mimeType, attachment.__dirname);
|
|
972
1130
|
await downloadImage(attachment.url, filePath);
|
|
973
1131
|
|
|
974
|
-
|
|
975
|
-
|
|
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
|
+
|
|
976
1146
|
const fileStats = fs.statSync(filePath);
|
|
977
1147
|
const fileSize = fileStats.size;
|
|
978
1148
|
const fileData = fs.readFileSync(filePath);
|
|
@@ -983,6 +1153,7 @@ async function uploadMedia(attachment, token, discussionType, logger) {
|
|
|
983
1153
|
const mediaIdString = await simpleMediaUpload(
|
|
984
1154
|
fileData,
|
|
985
1155
|
attachment.mimeType,
|
|
1156
|
+
mediaCategory,
|
|
986
1157
|
token,
|
|
987
1158
|
logger
|
|
988
1159
|
);
|
|
@@ -1011,12 +1182,14 @@ async function uploadMedia(attachment, token, discussionType, logger) {
|
|
|
1011
1182
|
}
|
|
1012
1183
|
|
|
1013
1184
|
// Simple upload for small media files
|
|
1014
|
-
async function simpleMediaUpload(fileData, mimeType, token, logger) {
|
|
1185
|
+
async function simpleMediaUpload(fileData, mimeType, mediaCategory, token, logger) {
|
|
1015
1186
|
try {
|
|
1016
1187
|
const base64Data = fileData.toString('base64');
|
|
1017
1188
|
const mediaType = mimeType.split('/')[0]; // 'image', 'video', etc.
|
|
1018
1189
|
|
|
1019
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
|
|
1020
1193
|
const { Authorization } = token.oauth.toHeader(
|
|
1021
1194
|
token.oauth.authorize(
|
|
1022
1195
|
{
|
|
@@ -1031,13 +1204,12 @@ async function simpleMediaUpload(fileData, mimeType, token, logger) {
|
|
|
1031
1204
|
)
|
|
1032
1205
|
);
|
|
1033
1206
|
|
|
1207
|
+
// Use .field() for multipart/form-data (OAuth ignores multipart body)
|
|
1034
1208
|
const result = await superagent
|
|
1035
1209
|
.post(url)
|
|
1036
1210
|
.set('Authorization', Authorization)
|
|
1037
|
-
.
|
|
1038
|
-
|
|
1039
|
-
media_type: mimeType,
|
|
1040
|
-
});
|
|
1211
|
+
.field('media_data', base64Data)
|
|
1212
|
+
.field('media_type', mimeType);
|
|
1041
1213
|
|
|
1042
1214
|
loggerDebug(logger, 'Media uploaded successfully (simple)', {
|
|
1043
1215
|
media_id: result.body.media_id_string,
|
|
@@ -1136,12 +1308,34 @@ async function chunkedMediaUpload(
|
|
|
1136
1308
|
async function mediaUploadRequest(params, token, logger) {
|
|
1137
1309
|
const url = 'https://upload.twitter.com/1.1/media/upload.json';
|
|
1138
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
|
|
1139
1333
|
const { Authorization } = token.oauth.toHeader(
|
|
1140
1334
|
token.oauth.authorize(
|
|
1141
1335
|
{
|
|
1142
|
-
url,
|
|
1336
|
+
url: url, // Base URL only
|
|
1143
1337
|
method: 'post',
|
|
1144
|
-
data:
|
|
1338
|
+
data: queryParams, // Query params go here for signature
|
|
1145
1339
|
},
|
|
1146
1340
|
{
|
|
1147
1341
|
key: token.token,
|
|
@@ -1151,11 +1345,18 @@ async function mediaUploadRequest(params, token, logger) {
|
|
|
1151
1345
|
);
|
|
1152
1346
|
|
|
1153
1347
|
try {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
.
|
|
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
|
+
}
|
|
1158
1358
|
|
|
1359
|
+
const result = await request;
|
|
1159
1360
|
return result.body;
|
|
1160
1361
|
} catch (e) {
|
|
1161
1362
|
loggerError(
|
package/link-for-testing.sh
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# Script to rebuild and re-link conversations-api-services for testing with engage-conversations-api
|
|
4
|
-
|
|
5
|
-
set -e
|
|
6
|
-
|
|
7
|
-
echo "🔨 Building conversations-api-services..."
|
|
8
|
-
npm run build
|
|
9
|
-
|
|
10
|
-
echo "🔗 Creating npm link..."
|
|
11
|
-
npm link
|
|
12
|
-
|
|
13
|
-
echo "🔗 Linking in engage-conversations-api..."
|
|
14
|
-
cd ../engage-conversations-api
|
|
15
|
-
npm link @meltwater/conversations-api-services
|
|
16
|
-
|
|
17
|
-
echo "✅ Successfully linked conversations-api-services for testing!"
|
|
18
|
-
echo ""
|
|
19
|
-
echo "📋 To unlink when done testing:"
|
|
20
|
-
echo " cd ../engage-conversations-api"
|
|
21
|
-
echo " npm unlink @meltwater/conversations-api-services"
|
|
22
|
-
echo " npm install"
|