@meltwater/conversations-api-services 1.1.24 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.addOAuthToToken = addOAuthToToken;
|
|
7
7
|
exports.followUser = followUser;
|
|
8
|
+
exports.getAuthenticatedUser = getAuthenticatedUser;
|
|
8
9
|
exports.getBrandUserRelationship = getBrandUserRelationship;
|
|
9
10
|
exports.getCurrentInfo = getCurrentInfo;
|
|
10
11
|
exports.getDirectMessageImage = getDirectMessageImage;
|
|
@@ -27,7 +28,6 @@ var _oauth = _interopRequireDefault(require("oauth-1.0a"));
|
|
|
27
28
|
var _crypto = _interopRequireDefault(require("crypto"));
|
|
28
29
|
var _axios = _interopRequireDefault(require("axios"));
|
|
29
30
|
var _fs = _interopRequireDefault(require("fs"));
|
|
30
|
-
var _socialTwit = _interopRequireDefault(require("@meltwater/social-twit"));
|
|
31
31
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
32
32
|
const TWITTER_API_CONFIG = {
|
|
33
33
|
v1_1: {
|
|
@@ -135,6 +135,28 @@ function normalizeUsersData(users) {
|
|
|
135
135
|
}
|
|
136
136
|
return normalizeUserData(users, apiVersion);
|
|
137
137
|
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get authenticated user information using Twitter API v2
|
|
141
|
+
* @param {Object} token - OAuth token object
|
|
142
|
+
* @param {Object} logger - Logger instance
|
|
143
|
+
* @returns {Promise<Object>} User data from v2 API
|
|
144
|
+
*/
|
|
145
|
+
async function getAuthenticatedUser(token, logger) {
|
|
146
|
+
try {
|
|
147
|
+
const result = await getRequest({
|
|
148
|
+
token,
|
|
149
|
+
uri: 'https://api.x.com/2/users/me',
|
|
150
|
+
attachUrlPrefix: false,
|
|
151
|
+
convertPayloadToUri: false,
|
|
152
|
+
logger
|
|
153
|
+
});
|
|
154
|
+
return result.data;
|
|
155
|
+
} catch (e) {
|
|
156
|
+
(0, _loggerHelpers.loggerError)(logger, `Error getting authenticated user info`, e);
|
|
157
|
+
throw e;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
138
160
|
function addOAuthToToken(token, TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET) {
|
|
139
161
|
if (!token.oauth) {
|
|
140
162
|
token.oauth = (0, _oauth.default)({
|
|
@@ -230,7 +252,7 @@ async function getCurrentInfo(token, externalId, logger) {
|
|
|
230
252
|
});
|
|
231
253
|
|
|
232
254
|
// this makes it so 'retweeted' is only true on a retweet that isn't a comment
|
|
233
|
-
// sometimes 'retweeted' is true if user retweeted with comment, sometimes not
|
|
255
|
+
// sometimes 'retweeted' is true if user retweeted with comment, sometimes not
|
|
234
256
|
result.data.retweeted = !!result.data.current_user_retweet;
|
|
235
257
|
return result.data.data[0].public_metrics;
|
|
236
258
|
} catch (error) {
|
|
@@ -293,12 +315,20 @@ async function unRetweet(token, sourceId, externalId, logger) {
|
|
|
293
315
|
}
|
|
294
316
|
async function like(token, externalId, logger) {
|
|
295
317
|
try {
|
|
318
|
+
// Get the authenticated user's ID first
|
|
319
|
+
const userInfo = await getAuthenticatedUser(token, logger);
|
|
320
|
+
const userId = userInfo.data.id;
|
|
296
321
|
let response = await postRequest({
|
|
297
322
|
token,
|
|
298
|
-
uri:
|
|
323
|
+
uri: `https://api.x.com/2/users/${userId}/likes`,
|
|
324
|
+
payload: {
|
|
325
|
+
tweet_id: (0, _externalIdHelpers.removePrefix)(externalId)
|
|
326
|
+
},
|
|
327
|
+
attachUrlPrefix: false,
|
|
328
|
+
convertPayloadToUri: false,
|
|
299
329
|
logger
|
|
300
330
|
});
|
|
301
|
-
if (response.data.
|
|
331
|
+
if (response.data.data?.liked !== undefined && response.resp.statusCode === 200) {
|
|
302
332
|
return true;
|
|
303
333
|
} else {
|
|
304
334
|
(0, _loggerHelpers.loggerInfo)(logger, 'Twitter Error in like user statusCode non 200 ', {
|
|
@@ -313,12 +343,17 @@ async function like(token, externalId, logger) {
|
|
|
313
343
|
}
|
|
314
344
|
async function unLike(token, externalId, logger) {
|
|
315
345
|
try {
|
|
316
|
-
|
|
346
|
+
// Get the authenticated user's ID first
|
|
347
|
+
const userInfo = await getAuthenticatedUser(token, logger);
|
|
348
|
+
const userId = userInfo.data.id;
|
|
349
|
+
let response = await deleteRequest({
|
|
317
350
|
token,
|
|
318
|
-
uri:
|
|
351
|
+
uri: `https://api.x.com/2/users/${userId}/likes/${(0, _externalIdHelpers.removePrefix)(externalId)}`,
|
|
352
|
+
attachUrlPrefix: false,
|
|
353
|
+
convertPayloadToUri: false,
|
|
319
354
|
logger
|
|
320
355
|
});
|
|
321
|
-
if (response.data.
|
|
356
|
+
if (response.data.data?.liked !== undefined && response.resp.statusCode === 200) {
|
|
322
357
|
return true;
|
|
323
358
|
} else {
|
|
324
359
|
return false;
|
|
@@ -330,16 +365,20 @@ async function unLike(token, externalId, logger) {
|
|
|
330
365
|
}
|
|
331
366
|
async function followUser(token, profileId, logger) {
|
|
332
367
|
try {
|
|
368
|
+
// Get the authenticated user's ID first
|
|
369
|
+
const userInfo = await getAuthenticatedUser(token, logger);
|
|
370
|
+
const userId = userInfo.data.id;
|
|
333
371
|
let response = await postRequest({
|
|
334
372
|
token,
|
|
335
|
-
uri:
|
|
373
|
+
uri: `https://api.x.com/2/users/${userId}/following`,
|
|
336
374
|
payload: {
|
|
337
|
-
|
|
338
|
-
follow: true
|
|
375
|
+
target_user_id: (0, _externalIdHelpers.removePrefix)(profileId)
|
|
339
376
|
},
|
|
377
|
+
attachUrlPrefix: false,
|
|
378
|
+
convertPayloadToUri: false,
|
|
340
379
|
logger
|
|
341
380
|
});
|
|
342
|
-
if (response.data.following !== undefined) {
|
|
381
|
+
if (response.data.data?.following !== undefined) {
|
|
343
382
|
return true;
|
|
344
383
|
} else {
|
|
345
384
|
return false;
|
|
@@ -350,15 +389,17 @@ async function followUser(token, profileId, logger) {
|
|
|
350
389
|
}
|
|
351
390
|
async function unFollowUser(token, profileId, logger) {
|
|
352
391
|
try {
|
|
353
|
-
|
|
392
|
+
// Get the authenticated user's ID first
|
|
393
|
+
const userInfo = await getAuthenticatedUser(token, logger);
|
|
394
|
+
const userId = userInfo.data.id;
|
|
395
|
+
let response = await deleteRequest({
|
|
354
396
|
token,
|
|
355
|
-
uri:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
},
|
|
397
|
+
uri: `https://api.x.com/2/users/${userId}/following/${(0, _externalIdHelpers.removePrefix)(profileId)}`,
|
|
398
|
+
attachUrlPrefix: false,
|
|
399
|
+
convertPayloadToUri: false,
|
|
359
400
|
logger
|
|
360
401
|
});
|
|
361
|
-
if (response.data.following !== undefined) {
|
|
402
|
+
if (response.data.data?.following !== undefined) {
|
|
362
403
|
return true;
|
|
363
404
|
} else {
|
|
364
405
|
return false;
|
|
@@ -369,26 +410,34 @@ async function unFollowUser(token, profileId, logger) {
|
|
|
369
410
|
}
|
|
370
411
|
async function userFollowStatus(token, profileId, logger) {
|
|
371
412
|
try {
|
|
413
|
+
// Get the authenticated user's ID first
|
|
414
|
+
const userInfo = await getAuthenticatedUser(token, logger);
|
|
415
|
+
const userId = userInfo.data.id;
|
|
416
|
+
|
|
417
|
+
// Check if authenticated user is following the target user
|
|
418
|
+
// We need to check the following list with pagination support
|
|
372
419
|
let response = await getRequest({
|
|
373
420
|
token,
|
|
374
|
-
uri:
|
|
421
|
+
uri: `https://api.x.com/2/users/${userId}/following`,
|
|
375
422
|
payload: {
|
|
376
|
-
|
|
423
|
+
max_results: 1000 // Max allowed per request
|
|
377
424
|
},
|
|
425
|
+
attachUrlPrefix: false,
|
|
426
|
+
convertPayloadToUri: true,
|
|
378
427
|
logger
|
|
379
428
|
});
|
|
380
|
-
if (response.data
|
|
381
|
-
if
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
return false;
|
|
388
|
-
} else {
|
|
389
|
-
return false;
|
|
429
|
+
if (response.data?.data) {
|
|
430
|
+
// Check if the profileId is in the following list
|
|
431
|
+
const isFollowing = response.data.data.some(user => user.id === (0, _externalIdHelpers.removePrefix)(profileId));
|
|
432
|
+
if (isFollowing) {
|
|
433
|
+
return true;
|
|
390
434
|
}
|
|
435
|
+
|
|
436
|
+
// If not found and there's a next page, we might need to paginate
|
|
437
|
+
// For now, return false if not in first page
|
|
438
|
+
return false;
|
|
391
439
|
}
|
|
440
|
+
return false;
|
|
392
441
|
} catch (error) {
|
|
393
442
|
(0, _loggerHelpers.loggerDebug)(logger, `Error in getting user follow status: ${profileId}`, error);
|
|
394
443
|
if (error.message != 'Bad Request') {
|
|
@@ -400,16 +449,58 @@ async function userFollowStatus(token, profileId, logger) {
|
|
|
400
449
|
}
|
|
401
450
|
async function getBrandUserRelationship(token, profileId, brandProfileId, logger) {
|
|
402
451
|
try {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
452
|
+
// In v2 API, we need to make separate calls to check following/followers status
|
|
453
|
+
// Check if brand follows user
|
|
454
|
+
let brandFollowsUser = false;
|
|
455
|
+
let userFollowsBrand = false;
|
|
456
|
+
try {
|
|
457
|
+
const followingResponse = await getRequest({
|
|
458
|
+
token,
|
|
459
|
+
uri: `https://api.x.com/2/users/${brandProfileId}/following`,
|
|
460
|
+
payload: {
|
|
461
|
+
max_results: 1000
|
|
462
|
+
},
|
|
463
|
+
attachUrlPrefix: false,
|
|
464
|
+
convertPayloadToUri: true,
|
|
465
|
+
logger
|
|
466
|
+
});
|
|
467
|
+
if (followingResponse.data?.data) {
|
|
468
|
+
brandFollowsUser = followingResponse.data.data.some(user => user.id === (0, _externalIdHelpers.removePrefix)(profileId));
|
|
469
|
+
}
|
|
470
|
+
} catch (e) {
|
|
471
|
+
(0, _loggerHelpers.loggerWarn)(logger, 'Error checking if brand follows user', e);
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const followersResponse = await getRequest({
|
|
475
|
+
token,
|
|
476
|
+
uri: `https://api.x.com/2/users/${brandProfileId}/followers`,
|
|
477
|
+
payload: {
|
|
478
|
+
max_results: 1000
|
|
479
|
+
},
|
|
480
|
+
attachUrlPrefix: false,
|
|
481
|
+
convertPayloadToUri: true,
|
|
482
|
+
logger
|
|
483
|
+
});
|
|
484
|
+
if (followersResponse.data?.data) {
|
|
485
|
+
userFollowsBrand = followersResponse.data.data.some(user => user.id === (0, _externalIdHelpers.removePrefix)(profileId));
|
|
486
|
+
}
|
|
487
|
+
} catch (e) {
|
|
488
|
+
(0, _loggerHelpers.loggerWarn)(logger, 'Error checking if user follows brand', e);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Build a relationship object similar to v1.1 format
|
|
492
|
+
return {
|
|
493
|
+
source: {
|
|
494
|
+
id_str: brandProfileId,
|
|
495
|
+
following: brandFollowsUser,
|
|
496
|
+
followed_by: userFollowsBrand
|
|
497
|
+
},
|
|
498
|
+
target: {
|
|
499
|
+
id_str: (0, _externalIdHelpers.removePrefix)(profileId),
|
|
500
|
+
following: userFollowsBrand,
|
|
501
|
+
followed_by: brandFollowsUser
|
|
502
|
+
}
|
|
503
|
+
};
|
|
413
504
|
} catch (error) {
|
|
414
505
|
(0, _loggerHelpers.loggerDebug)(logger, `Error in getting user brand friendship info: ${profileId} and ${brandProfileId}`, error);
|
|
415
506
|
}
|
|
@@ -441,34 +532,21 @@ async function reply(token, text, attachment, inReplyToId, removeInReplyToId, lo
|
|
|
441
532
|
return publishTweet(token, payload, query, false, logger);
|
|
442
533
|
}
|
|
443
534
|
async function privateMessage(token, text, attachment, profileId, logger) {
|
|
444
|
-
let query =
|
|
535
|
+
let query = `https://api.x.com/2/dm_conversations/with/${(0, _externalIdHelpers.removePrefix)(profileId)}/messages`;
|
|
445
536
|
let mediaId;
|
|
446
537
|
if (attachment) {
|
|
447
538
|
// discussionType only matters if DM or Not
|
|
448
539
|
mediaId = await uploadMedia(attachment, token, 'dm', logger);
|
|
449
540
|
}
|
|
450
541
|
const payload = {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
message_data: {
|
|
458
|
-
text: text || ' ',
|
|
459
|
-
...(attachment && {
|
|
460
|
-
attachment: {
|
|
461
|
-
type: 'media',
|
|
462
|
-
media: {
|
|
463
|
-
id: mediaId
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
})
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
542
|
+
text: text || ' ',
|
|
543
|
+
...(attachment && {
|
|
544
|
+
attachments: [{
|
|
545
|
+
media_id: mediaId
|
|
546
|
+
}]
|
|
547
|
+
})
|
|
470
548
|
};
|
|
471
|
-
return
|
|
549
|
+
return publishDirectMessage(token, payload, query, logger);
|
|
472
550
|
}
|
|
473
551
|
async function retweetWithComment(token, text, attachment, logger) {
|
|
474
552
|
let mediaId;
|
|
@@ -487,6 +565,84 @@ async function retweetWithComment(token, text, attachment, logger) {
|
|
|
487
565
|
let query = 'https://api.twitter.com/2/tweets';
|
|
488
566
|
return publishTweet(token, payload, query, false, logger);
|
|
489
567
|
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Normalizes Twitter API responses to ensure consistent structure across v1.1 and v2
|
|
571
|
+
* This is critical for backward compatibility - consumers expect certain fields to exist
|
|
572
|
+
*
|
|
573
|
+
* @param {Object} response - The response object to normalize
|
|
574
|
+
* @param {string} responseType - Type of response: 'dm' for direct messages, 'tweet' for tweets
|
|
575
|
+
* @param {Object} logger - Logger instance for debugging
|
|
576
|
+
* @param {Object} fullResponse - Full API response including includes (for extracting author_id from expansions)
|
|
577
|
+
* @returns {Object} Normalized response with consistent field structure
|
|
578
|
+
*/
|
|
579
|
+
function normalizeTwitterResponse(response, responseType, logger) {
|
|
580
|
+
let fullResponse = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
|
|
581
|
+
if (!response || !response.data) {
|
|
582
|
+
return response;
|
|
583
|
+
}
|
|
584
|
+
(0, _loggerHelpers.loggerDebug)(logger, `Normalizing Twitter API response (${responseType})`, {
|
|
585
|
+
beforeNormalization: JSON.stringify(response.data)
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Handle Twitter API v2 Direct Message responses
|
|
589
|
+
// v2 DM structure: { dm_event_id, dm_conversation_id }
|
|
590
|
+
// v1.1 expected structure: { id, author_id }
|
|
591
|
+
if (responseType === 'dm' && response.data.dm_event_id) {
|
|
592
|
+
const normalizedData = {
|
|
593
|
+
...response.data,
|
|
594
|
+
// Add v1.1-compatible fields for backward compatibility
|
|
595
|
+
id: response.data.dm_event_id,
|
|
596
|
+
// Note: author_id is intentionally not set here. The caller (twitterApi.client.js)
|
|
597
|
+
// will set the correct author_id from the original document since dm_conversation_id
|
|
598
|
+
// doesn't reliably indicate the sender (it's just "user1-user2" format)
|
|
599
|
+
author_id: null
|
|
600
|
+
};
|
|
601
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Normalized Twitter API v2 DM response to v1.1 format`, {
|
|
602
|
+
dm_event_id: response.data.dm_event_id,
|
|
603
|
+
normalized_id: normalizedData.id,
|
|
604
|
+
dm_conversation_id: response.data.dm_conversation_id,
|
|
605
|
+
normalized_author_id: normalizedData.author_id
|
|
606
|
+
});
|
|
607
|
+
return {
|
|
608
|
+
...response,
|
|
609
|
+
data: normalizedData
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Handle Twitter API v2 Tweet responses
|
|
614
|
+
// v2 has 'id' field but author_id is in expansions (includes.users)
|
|
615
|
+
if (responseType === 'tweet' && response.data.id) {
|
|
616
|
+
const normalizedData = {
|
|
617
|
+
...response.data
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// If author_id is missing but we have includes.users from expansions
|
|
621
|
+
if (!normalizedData.author_id && fullResponse?.includes?.users?.[0]?.id) {
|
|
622
|
+
normalizedData.author_id = fullResponse.includes.users[0].id;
|
|
623
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Added author_id to tweet response from expansions`, {
|
|
624
|
+
tweet_id: normalizedData.id,
|
|
625
|
+
author_id: normalizedData.author_id
|
|
626
|
+
});
|
|
627
|
+
} else if (normalizedData.author_id) {
|
|
628
|
+
(0, _loggerHelpers.loggerDebug)(logger, `Twitter v2 tweet response already has 'author_id'`, {
|
|
629
|
+
id: response.data.id,
|
|
630
|
+
author_id: normalizedData.author_id
|
|
631
|
+
});
|
|
632
|
+
} else {
|
|
633
|
+
(0, _loggerHelpers.loggerWarn)(logger, `Twitter v2 tweet response missing author_id and no expansions available`, {
|
|
634
|
+
id: response.data.id,
|
|
635
|
+
hasIncludes: !!fullResponse?.includes,
|
|
636
|
+
hasUsers: !!fullResponse?.includes?.users
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
...response,
|
|
641
|
+
data: normalizedData
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
return response;
|
|
645
|
+
}
|
|
490
646
|
async function publishTweet(token, payload, query, isDirectMessage, logger) {
|
|
491
647
|
try {
|
|
492
648
|
let nativeResponse = await postRequest({
|
|
@@ -521,12 +677,50 @@ async function publishTweet(token, payload, query, isDirectMessage, logger) {
|
|
|
521
677
|
(0, _loggerHelpers.loggerDebug)(logger, `Twitter the data response is`, {
|
|
522
678
|
response: JSON.stringify(response)
|
|
523
679
|
});
|
|
524
|
-
|
|
680
|
+
|
|
681
|
+
// Normalize the response to ensure backward compatibility with v1.1 structure
|
|
682
|
+
// Pass full nativeResponse to extract author_id from expansions if needed
|
|
683
|
+
const normalizedResponse = normalizeTwitterResponse(response, 'tweet', logger, nativeResponse.data // Full response with includes.users for author_id
|
|
684
|
+
);
|
|
685
|
+
return normalizedResponse;
|
|
525
686
|
} catch (err) {
|
|
526
687
|
(0, _loggerHelpers.loggerError)(logger, `Twitter publish exception details`, err);
|
|
527
688
|
throw err;
|
|
528
689
|
}
|
|
529
690
|
}
|
|
691
|
+
async function publishDirectMessage(token, payload, query, logger) {
|
|
692
|
+
try {
|
|
693
|
+
let nativeResponse = await postRequest({
|
|
694
|
+
token,
|
|
695
|
+
uri: query,
|
|
696
|
+
payload,
|
|
697
|
+
convertPayloadToUri: false,
|
|
698
|
+
attachUrlPrefix: false
|
|
699
|
+
});
|
|
700
|
+
(0, _loggerHelpers.loggerInfo)(logger, `finished sending DM via Twitter API v2`, {
|
|
701
|
+
data: JSON.stringify(nativeResponse.data)
|
|
702
|
+
});
|
|
703
|
+
const response = nativeResponse.resp ? {
|
|
704
|
+
statusCode: nativeResponse.resp.statusCode,
|
|
705
|
+
statusMessage: nativeResponse.resp.statusCode === 201 || nativeResponse.resp.statusCode === 200 ? 'Success' : 'Unknown Failure',
|
|
706
|
+
data: nativeResponse.data.data
|
|
707
|
+
} : {
|
|
708
|
+
statusCode: nativeResponse.statusCode,
|
|
709
|
+
statusMessage: nativeResponse.message
|
|
710
|
+
};
|
|
711
|
+
(0, _loggerHelpers.loggerDebug)(logger, `Twitter DM response is`, {
|
|
712
|
+
response: JSON.stringify(response)
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Normalize the response to ensure backward compatibility with v1.1 structure
|
|
716
|
+
// This is CRITICAL - v2 DM responses have dm_event_id instead of id
|
|
717
|
+
const normalizedResponse = normalizeTwitterResponse(response, 'dm', logger);
|
|
718
|
+
return normalizedResponse;
|
|
719
|
+
} catch (err) {
|
|
720
|
+
(0, _loggerHelpers.loggerError)(logger, `Twitter DM exception details`, err);
|
|
721
|
+
throw err;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
530
724
|
async function postRequest(_ref2) {
|
|
531
725
|
let {
|
|
532
726
|
token,
|
|
@@ -685,32 +879,177 @@ function generateFilePath(mimeType, dirname) {
|
|
|
685
879
|
}
|
|
686
880
|
// local
|
|
687
881
|
async function uploadMedia(attachment, token, discussionType, logger) {
|
|
688
|
-
|
|
689
|
-
|
|
882
|
+
let filePath;
|
|
883
|
+
try {
|
|
884
|
+
filePath = generateFilePath(attachment.mimeType, attachment.__dirname);
|
|
690
885
|
await downloadImage(attachment.url, filePath);
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
886
|
+
const mediaCategory = discussionType === 'dm' ? 'dm_image' : 'tweet_image';
|
|
887
|
+
const fileStats = _fs.default.statSync(filePath);
|
|
888
|
+
const fileSize = fileStats.size;
|
|
889
|
+
const fileData = _fs.default.readFileSync(filePath);
|
|
890
|
+
|
|
891
|
+
// For small files, use simple upload
|
|
892
|
+
if (fileSize < 5000000) {
|
|
893
|
+
// 5MB
|
|
894
|
+
const mediaIdString = await simpleMediaUpload(fileData, attachment.mimeType, token, logger);
|
|
895
|
+
removeMedia(filePath, logger);
|
|
896
|
+
return mediaIdString;
|
|
897
|
+
} else {
|
|
898
|
+
// For large files, use chunked upload
|
|
899
|
+
const mediaIdString = await chunkedMediaUpload(filePath, fileSize, attachment.mimeType, mediaCategory, token, logger);
|
|
900
|
+
removeMedia(filePath, logger);
|
|
901
|
+
return mediaIdString;
|
|
902
|
+
}
|
|
903
|
+
} catch (e) {
|
|
904
|
+
(0, _loggerHelpers.loggerError)(logger, `Failed uploading media`, e);
|
|
905
|
+
if (filePath) {
|
|
906
|
+
removeMedia(filePath, logger);
|
|
907
|
+
}
|
|
908
|
+
throw e;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Simple upload for small media files
|
|
913
|
+
async function simpleMediaUpload(fileData, mimeType, token, logger) {
|
|
914
|
+
try {
|
|
915
|
+
const base64Data = fileData.toString('base64');
|
|
916
|
+
const mediaType = mimeType.split('/')[0]; // 'image', 'video', etc.
|
|
917
|
+
|
|
918
|
+
const url = 'https://upload.twitter.com/1.1/media/upload.json';
|
|
919
|
+
const {
|
|
920
|
+
Authorization
|
|
921
|
+
} = token.oauth.toHeader(token.oauth.authorize({
|
|
922
|
+
url,
|
|
923
|
+
method: 'post',
|
|
924
|
+
data: {}
|
|
925
|
+
}, {
|
|
926
|
+
key: token.token,
|
|
927
|
+
secret: token.tokenSecret
|
|
928
|
+
}));
|
|
929
|
+
const result = await _superagent.default.post(url).set('Authorization', Authorization).send({
|
|
930
|
+
media_data: base64Data,
|
|
931
|
+
media_type: mimeType
|
|
696
932
|
});
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
933
|
+
(0, _loggerHelpers.loggerDebug)(logger, 'Media uploaded successfully (simple)', {
|
|
934
|
+
media_id: result.body.media_id_string
|
|
935
|
+
});
|
|
936
|
+
return result.body.media_id_string;
|
|
937
|
+
} catch (e) {
|
|
938
|
+
(0, _loggerHelpers.loggerError)(logger, 'Error in simple media upload', e);
|
|
939
|
+
throw e;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Chunked upload for large media files
|
|
944
|
+
async function chunkedMediaUpload(filePath, fileSize, mimeType, mediaCategory, token, logger) {
|
|
945
|
+
try {
|
|
946
|
+
const mediaType = mimeType.split('/')[0];
|
|
947
|
+
|
|
948
|
+
// Step 1: INIT
|
|
949
|
+
const initResponse = await mediaUploadRequest({
|
|
950
|
+
command: 'INIT',
|
|
951
|
+
total_bytes: fileSize,
|
|
952
|
+
media_type: mimeType,
|
|
953
|
+
media_category: mediaCategory
|
|
954
|
+
}, token, logger);
|
|
955
|
+
const mediaId = initResponse.media_id_string;
|
|
956
|
+
(0, _loggerHelpers.loggerDebug)(logger, 'Media upload INIT successful', {
|
|
957
|
+
mediaId
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// Step 2: APPEND (upload in chunks)
|
|
961
|
+
const chunkSize = 5 * 1024 * 1024; // 5MB chunks
|
|
962
|
+
const fileBuffer = _fs.default.readFileSync(filePath);
|
|
963
|
+
let segmentIndex = 0;
|
|
964
|
+
for (let offset = 0; offset < fileSize; offset += chunkSize) {
|
|
965
|
+
const chunk = fileBuffer.slice(offset, Math.min(offset + chunkSize, fileSize));
|
|
966
|
+
const base64Chunk = chunk.toString('base64');
|
|
967
|
+
await mediaUploadRequest({
|
|
968
|
+
command: 'APPEND',
|
|
969
|
+
media_id: mediaId,
|
|
970
|
+
media_data: base64Chunk,
|
|
971
|
+
segment_index: segmentIndex
|
|
972
|
+
}, token, logger);
|
|
973
|
+
(0, _loggerHelpers.loggerDebug)(logger, `Media chunk ${segmentIndex} uploaded`, {
|
|
974
|
+
mediaId,
|
|
975
|
+
segmentIndex
|
|
707
976
|
});
|
|
708
|
-
|
|
709
|
-
(0, _loggerHelpers.loggerError)(logger, `Failed posting media`, e);
|
|
710
|
-
// this is just a safety precaution
|
|
711
|
-
removeMedia(filePath, logger);
|
|
977
|
+
segmentIndex++;
|
|
712
978
|
}
|
|
713
|
-
|
|
979
|
+
|
|
980
|
+
// Step 3: FINALIZE
|
|
981
|
+
const finalizeResponse = await mediaUploadRequest({
|
|
982
|
+
command: 'FINALIZE',
|
|
983
|
+
media_id: mediaId
|
|
984
|
+
}, token, logger);
|
|
985
|
+
(0, _loggerHelpers.loggerDebug)(logger, 'Media upload FINALIZE successful', {
|
|
986
|
+
mediaId
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// Step 4: Check processing status if needed
|
|
990
|
+
if (finalizeResponse.processing_info) {
|
|
991
|
+
await waitForMediaProcessing(mediaId, token, logger);
|
|
992
|
+
}
|
|
993
|
+
return mediaId;
|
|
994
|
+
} catch (e) {
|
|
995
|
+
(0, _loggerHelpers.loggerError)(logger, 'Error in chunked media upload', e);
|
|
996
|
+
throw e;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Make a media upload request
|
|
1001
|
+
async function mediaUploadRequest(params, token, logger) {
|
|
1002
|
+
const url = 'https://upload.twitter.com/1.1/media/upload.json';
|
|
1003
|
+
const {
|
|
1004
|
+
Authorization
|
|
1005
|
+
} = token.oauth.toHeader(token.oauth.authorize({
|
|
1006
|
+
url,
|
|
1007
|
+
method: 'post',
|
|
1008
|
+
data: {}
|
|
1009
|
+
}, {
|
|
1010
|
+
key: token.token,
|
|
1011
|
+
secret: token.tokenSecret
|
|
1012
|
+
}));
|
|
1013
|
+
try {
|
|
1014
|
+
const result = await _superagent.default.post(url).set('Authorization', Authorization).send(params);
|
|
1015
|
+
return result.body;
|
|
1016
|
+
} catch (e) {
|
|
1017
|
+
(0, _loggerHelpers.loggerError)(logger, `Media upload request failed for command: ${params.command}`, e);
|
|
1018
|
+
throw e;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Wait for media processing to complete
|
|
1023
|
+
async function waitForMediaProcessing(mediaId, token, logger) {
|
|
1024
|
+
let maxAttempts = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 20;
|
|
1025
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1026
|
+
const statusResponse = await mediaUploadRequest({
|
|
1027
|
+
command: 'STATUS',
|
|
1028
|
+
media_id: mediaId
|
|
1029
|
+
}, token, logger);
|
|
1030
|
+
if (!statusResponse.processing_info) {
|
|
1031
|
+
return; // Processing complete
|
|
1032
|
+
}
|
|
1033
|
+
const state = statusResponse.processing_info.state;
|
|
1034
|
+
if (state === 'succeeded') {
|
|
1035
|
+
(0, _loggerHelpers.loggerDebug)(logger, 'Media processing succeeded', {
|
|
1036
|
+
mediaId
|
|
1037
|
+
});
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (state === 'failed') {
|
|
1041
|
+
throw new Error(`Media processing failed: ${statusResponse.processing_info.error?.message || 'Unknown error'}`);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Still processing, wait before checking again
|
|
1045
|
+
const waitTime = statusResponse.processing_info.check_after_secs || 1;
|
|
1046
|
+
(0, _loggerHelpers.loggerDebug)(logger, `Media still processing, waiting ${waitTime}s`, {
|
|
1047
|
+
mediaId,
|
|
1048
|
+
state
|
|
1049
|
+
});
|
|
1050
|
+
await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
|
|
1051
|
+
}
|
|
1052
|
+
throw new Error('Media processing timeout');
|
|
714
1053
|
}
|
|
715
1054
|
function removeMedia(file, logger) {
|
|
716
1055
|
try {
|
|
@@ -722,5 +1061,4 @@ function removeMedia(file, logger) {
|
|
|
722
1061
|
} catch (e) {
|
|
723
1062
|
(0, _loggerHelpers.loggerError)(logger, `failed trying to remove media ${file} it may have already been removed`);
|
|
724
1063
|
}
|
|
725
|
-
}
|
|
726
|
-
;
|
|
1064
|
+
}
|
|
@@ -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,
|