@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,7 +5,6 @@ import OAuth from 'oauth-1.0a';
5
5
  import crypto from 'crypto';
6
6
  import axios from 'axios';
7
7
  import fs from 'fs';
8
- import Twit from '@meltwater/social-twit';
9
8
  const TWITTER_API_CONFIG = {
10
9
  v1_1: {
11
10
  base_url: 'https://api.twitter.com/1.1'
@@ -112,6 +111,28 @@ function normalizeUsersData(users) {
112
111
  }
113
112
  return normalizeUserData(users, apiVersion);
114
113
  }
114
+
115
+ /**
116
+ * Get authenticated user information using Twitter API v2
117
+ * @param {Object} token - OAuth token object
118
+ * @param {Object} logger - Logger instance
119
+ * @returns {Promise<Object>} User data from v2 API
120
+ */
121
+ export async function getAuthenticatedUser(token, logger) {
122
+ try {
123
+ const result = await getRequest({
124
+ token,
125
+ uri: 'https://api.x.com/2/users/me',
126
+ attachUrlPrefix: false,
127
+ convertPayloadToUri: false,
128
+ logger
129
+ });
130
+ return result.data;
131
+ } catch (e) {
132
+ loggerError(logger, `Error getting authenticated user info`, e);
133
+ throw e;
134
+ }
135
+ }
115
136
  export function addOAuthToToken(token, TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET) {
116
137
  if (!token.oauth) {
117
138
  token.oauth = OAuth({
@@ -207,7 +228,7 @@ export async function getCurrentInfo(token, externalId, logger) {
207
228
  });
208
229
 
209
230
  // this makes it so 'retweeted' is only true on a retweet that isn't a comment
210
- // sometimes 'retweeted' is true if user retweeted with comment, sometimes not :sadness:
231
+ // sometimes 'retweeted' is true if user retweeted with comment, sometimes not
211
232
  result.data.retweeted = !!result.data.current_user_retweet;
212
233
  return result.data.data[0].public_metrics;
213
234
  } catch (error) {
@@ -270,12 +291,20 @@ export async function unRetweet(token, sourceId, externalId, logger) {
270
291
  }
271
292
  export async function like(token, externalId, logger) {
272
293
  try {
294
+ // Get the authenticated user's ID first
295
+ const userInfo = await getAuthenticatedUser(token, logger);
296
+ const userId = userInfo.data.id;
273
297
  let response = await postRequest({
274
298
  token,
275
- uri: 'favorites/create.json?id=' + removePrefix(externalId),
299
+ uri: `https://api.x.com/2/users/${userId}/likes`,
300
+ payload: {
301
+ tweet_id: removePrefix(externalId)
302
+ },
303
+ attachUrlPrefix: false,
304
+ convertPayloadToUri: false,
276
305
  logger
277
306
  });
278
- if (response.data.favorited !== undefined && response.resp.statusCode === 200) {
307
+ if (response.data.data?.liked !== undefined && response.resp.statusCode === 200) {
279
308
  return true;
280
309
  } else {
281
310
  loggerInfo(logger, 'Twitter Error in like user statusCode non 200 ', {
@@ -290,12 +319,17 @@ export async function like(token, externalId, logger) {
290
319
  }
291
320
  export async function unLike(token, externalId, logger) {
292
321
  try {
293
- let response = await postRequest({
322
+ // Get the authenticated user's ID first
323
+ const userInfo = await getAuthenticatedUser(token, logger);
324
+ const userId = userInfo.data.id;
325
+ let response = await deleteRequest({
294
326
  token,
295
- uri: 'favorites/destroy.json?id=' + removePrefix(externalId),
327
+ uri: `https://api.x.com/2/users/${userId}/likes/${removePrefix(externalId)}`,
328
+ attachUrlPrefix: false,
329
+ convertPayloadToUri: false,
296
330
  logger
297
331
  });
298
- if (response.data.favorited !== undefined && response.resp.statusCode === 200) {
332
+ if (response.data.data?.liked !== undefined && response.resp.statusCode === 200) {
299
333
  return true;
300
334
  } else {
301
335
  return false;
@@ -307,16 +341,20 @@ export async function unLike(token, externalId, logger) {
307
341
  }
308
342
  export async function followUser(token, profileId, logger) {
309
343
  try {
344
+ // Get the authenticated user's ID first
345
+ const userInfo = await getAuthenticatedUser(token, logger);
346
+ const userId = userInfo.data.id;
310
347
  let response = await postRequest({
311
348
  token,
312
- uri: 'friendships/create.json',
349
+ uri: `https://api.x.com/2/users/${userId}/following`,
313
350
  payload: {
314
- user_id: removePrefix(profileId),
315
- follow: true
351
+ target_user_id: removePrefix(profileId)
316
352
  },
353
+ attachUrlPrefix: false,
354
+ convertPayloadToUri: false,
317
355
  logger
318
356
  });
319
- if (response.data.following !== undefined) {
357
+ if (response.data.data?.following !== undefined) {
320
358
  return true;
321
359
  } else {
322
360
  return false;
@@ -327,15 +365,17 @@ export async function followUser(token, profileId, logger) {
327
365
  }
328
366
  export async function unFollowUser(token, profileId, logger) {
329
367
  try {
330
- let response = await postRequest({
368
+ // Get the authenticated user's ID first
369
+ const userInfo = await getAuthenticatedUser(token, logger);
370
+ const userId = userInfo.data.id;
371
+ let response = await deleteRequest({
331
372
  token,
332
- uri: 'friendships/destroy.json',
333
- payload: {
334
- user_id: removePrefix(profileId)
335
- },
373
+ uri: `https://api.x.com/2/users/${userId}/following/${removePrefix(profileId)}`,
374
+ attachUrlPrefix: false,
375
+ convertPayloadToUri: false,
336
376
  logger
337
377
  });
338
- if (response.data.following !== undefined) {
378
+ if (response.data.data?.following !== undefined) {
339
379
  return true;
340
380
  } else {
341
381
  return false;
@@ -346,26 +386,34 @@ export async function unFollowUser(token, profileId, logger) {
346
386
  }
347
387
  export async function userFollowStatus(token, profileId, logger) {
348
388
  try {
389
+ // Get the authenticated user's ID first
390
+ const userInfo = await getAuthenticatedUser(token, logger);
391
+ const userId = userInfo.data.id;
392
+
393
+ // Check if authenticated user is following the target user
394
+ // We need to check the following list with pagination support
349
395
  let response = await getRequest({
350
396
  token,
351
- uri: 'friendships/lookup.json',
397
+ uri: `https://api.x.com/2/users/${userId}/following`,
352
398
  payload: {
353
- user_id: removePrefix(profileId)
399
+ max_results: 1000 // Max allowed per request
354
400
  },
401
+ attachUrlPrefix: false,
402
+ convertPayloadToUri: true,
355
403
  logger
356
404
  });
357
- if (response.data.length > 0) {
358
- if (response.data[0].connections.length > 0) {
359
- for (var i = 0; i < response.data[0].connections.length; i++) {
360
- if (response.data[0].connections[i] === 'following') {
361
- return true;
362
- }
363
- }
364
- return false;
365
- } else {
366
- return false;
405
+ if (response.data?.data) {
406
+ // Check if the profileId is in the following list
407
+ const isFollowing = response.data.data.some(user => user.id === removePrefix(profileId));
408
+ if (isFollowing) {
409
+ return true;
367
410
  }
411
+
412
+ // If not found and there's a next page, we might need to paginate
413
+ // For now, return false if not in first page
414
+ return false;
368
415
  }
416
+ return false;
369
417
  } catch (error) {
370
418
  loggerDebug(logger, `Error in getting user follow status: ${profileId}`, error);
371
419
  if (error.message != 'Bad Request') {
@@ -377,16 +425,58 @@ export async function userFollowStatus(token, profileId, logger) {
377
425
  }
378
426
  export async function getBrandUserRelationship(token, profileId, brandProfileId, logger) {
379
427
  try {
380
- let {
381
- data: {
382
- relationship
383
- } = {}
384
- } = await getRequest({
385
- token,
386
- uri: `friendships/show.json?source_id=${brandProfileId}&target_id=${removePrefix(profileId)}`,
387
- logger
388
- });
389
- return relationship;
428
+ // In v2 API, we need to make separate calls to check following/followers status
429
+ // Check if brand follows user
430
+ let brandFollowsUser = false;
431
+ let userFollowsBrand = false;
432
+ try {
433
+ const followingResponse = await getRequest({
434
+ token,
435
+ uri: `https://api.x.com/2/users/${brandProfileId}/following`,
436
+ payload: {
437
+ max_results: 1000
438
+ },
439
+ attachUrlPrefix: false,
440
+ convertPayloadToUri: true,
441
+ logger
442
+ });
443
+ if (followingResponse.data?.data) {
444
+ brandFollowsUser = followingResponse.data.data.some(user => user.id === removePrefix(profileId));
445
+ }
446
+ } catch (e) {
447
+ loggerWarn(logger, 'Error checking if brand follows user', e);
448
+ }
449
+ try {
450
+ const followersResponse = await getRequest({
451
+ token,
452
+ uri: `https://api.x.com/2/users/${brandProfileId}/followers`,
453
+ payload: {
454
+ max_results: 1000
455
+ },
456
+ attachUrlPrefix: false,
457
+ convertPayloadToUri: true,
458
+ logger
459
+ });
460
+ if (followersResponse.data?.data) {
461
+ userFollowsBrand = followersResponse.data.data.some(user => user.id === removePrefix(profileId));
462
+ }
463
+ } catch (e) {
464
+ loggerWarn(logger, 'Error checking if user follows brand', e);
465
+ }
466
+
467
+ // Build a relationship object similar to v1.1 format
468
+ return {
469
+ source: {
470
+ id_str: brandProfileId,
471
+ following: brandFollowsUser,
472
+ followed_by: userFollowsBrand
473
+ },
474
+ target: {
475
+ id_str: removePrefix(profileId),
476
+ following: userFollowsBrand,
477
+ followed_by: brandFollowsUser
478
+ }
479
+ };
390
480
  } catch (error) {
391
481
  loggerDebug(logger, `Error in getting user brand friendship info: ${profileId} and ${brandProfileId}`, error);
392
482
  }
@@ -418,34 +508,21 @@ export async function reply(token, text, attachment, inReplyToId, removeInReplyT
418
508
  return publishTweet(token, payload, query, false, logger);
419
509
  }
420
510
  export async function privateMessage(token, text, attachment, profileId, logger) {
421
- let query = 'direct_messages/events/new.json';
511
+ let query = `https://api.x.com/2/dm_conversations/with/${removePrefix(profileId)}/messages`;
422
512
  let mediaId;
423
513
  if (attachment) {
424
514
  // discussionType only matters if DM or Not
425
515
  mediaId = await uploadMedia(attachment, token, 'dm', logger);
426
516
  }
427
517
  const payload = {
428
- event: {
429
- type: 'message_create',
430
- message_create: {
431
- target: {
432
- recipient_id: removePrefix(profileId)
433
- },
434
- message_data: {
435
- text: text || ' ',
436
- ...(attachment && {
437
- attachment: {
438
- type: 'media',
439
- media: {
440
- id: mediaId
441
- }
442
- }
443
- })
444
- }
445
- }
446
- }
518
+ text: text || ' ',
519
+ ...(attachment && {
520
+ attachments: [{
521
+ media_id: mediaId
522
+ }]
523
+ })
447
524
  };
448
- return publishTweet(token, payload, query, true, logger);
525
+ return publishDirectMessage(token, payload, query, logger);
449
526
  }
450
527
  export async function retweetWithComment(token, text, attachment, logger) {
451
528
  let mediaId;
@@ -464,6 +541,84 @@ export async function retweetWithComment(token, text, attachment, logger) {
464
541
  let query = 'https://api.twitter.com/2/tweets';
465
542
  return publishTweet(token, payload, query, false, logger);
466
543
  }
544
+
545
+ /**
546
+ * Normalizes Twitter API responses to ensure consistent structure across v1.1 and v2
547
+ * This is critical for backward compatibility - consumers expect certain fields to exist
548
+ *
549
+ * @param {Object} response - The response object to normalize
550
+ * @param {string} responseType - Type of response: 'dm' for direct messages, 'tweet' for tweets
551
+ * @param {Object} logger - Logger instance for debugging
552
+ * @param {Object} fullResponse - Full API response including includes (for extracting author_id from expansions)
553
+ * @returns {Object} Normalized response with consistent field structure
554
+ */
555
+ function normalizeTwitterResponse(response, responseType, logger) {
556
+ let fullResponse = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
557
+ if (!response || !response.data) {
558
+ return response;
559
+ }
560
+ loggerDebug(logger, `Normalizing Twitter API response (${responseType})`, {
561
+ beforeNormalization: JSON.stringify(response.data)
562
+ });
563
+
564
+ // Handle Twitter API v2 Direct Message responses
565
+ // v2 DM structure: { dm_event_id, dm_conversation_id }
566
+ // v1.1 expected structure: { id, author_id }
567
+ if (responseType === 'dm' && response.data.dm_event_id) {
568
+ const normalizedData = {
569
+ ...response.data,
570
+ // Add v1.1-compatible fields for backward compatibility
571
+ id: response.data.dm_event_id,
572
+ // Note: author_id is intentionally not set here. The caller (twitterApi.client.js)
573
+ // will set the correct author_id from the original document since dm_conversation_id
574
+ // doesn't reliably indicate the sender (it's just "user1-user2" format)
575
+ author_id: null
576
+ };
577
+ loggerInfo(logger, `Normalized Twitter API v2 DM response to v1.1 format`, {
578
+ dm_event_id: response.data.dm_event_id,
579
+ normalized_id: normalizedData.id,
580
+ dm_conversation_id: response.data.dm_conversation_id,
581
+ normalized_author_id: normalizedData.author_id
582
+ });
583
+ return {
584
+ ...response,
585
+ data: normalizedData
586
+ };
587
+ }
588
+
589
+ // Handle Twitter API v2 Tweet responses
590
+ // v2 has 'id' field but author_id is in expansions (includes.users)
591
+ if (responseType === 'tweet' && response.data.id) {
592
+ const normalizedData = {
593
+ ...response.data
594
+ };
595
+
596
+ // If author_id is missing but we have includes.users from expansions
597
+ if (!normalizedData.author_id && fullResponse?.includes?.users?.[0]?.id) {
598
+ normalizedData.author_id = fullResponse.includes.users[0].id;
599
+ loggerInfo(logger, `Added author_id to tweet response from expansions`, {
600
+ tweet_id: normalizedData.id,
601
+ author_id: normalizedData.author_id
602
+ });
603
+ } else if (normalizedData.author_id) {
604
+ loggerDebug(logger, `Twitter v2 tweet response already has 'author_id'`, {
605
+ id: response.data.id,
606
+ author_id: normalizedData.author_id
607
+ });
608
+ } else {
609
+ loggerWarn(logger, `Twitter v2 tweet response missing author_id and no expansions available`, {
610
+ id: response.data.id,
611
+ hasIncludes: !!fullResponse?.includes,
612
+ hasUsers: !!fullResponse?.includes?.users
613
+ });
614
+ }
615
+ return {
616
+ ...response,
617
+ data: normalizedData
618
+ };
619
+ }
620
+ return response;
621
+ }
467
622
  async function publishTweet(token, payload, query, isDirectMessage, logger) {
468
623
  try {
469
624
  let nativeResponse = await postRequest({
@@ -498,12 +653,50 @@ async function publishTweet(token, payload, query, isDirectMessage, logger) {
498
653
  loggerDebug(logger, `Twitter the data response is`, {
499
654
  response: JSON.stringify(response)
500
655
  });
501
- return response;
656
+
657
+ // Normalize the response to ensure backward compatibility with v1.1 structure
658
+ // Pass full nativeResponse to extract author_id from expansions if needed
659
+ const normalizedResponse = normalizeTwitterResponse(response, 'tweet', logger, nativeResponse.data // Full response with includes.users for author_id
660
+ );
661
+ return normalizedResponse;
502
662
  } catch (err) {
503
663
  loggerError(logger, `Twitter publish exception details`, err);
504
664
  throw err;
505
665
  }
506
666
  }
667
+ async function publishDirectMessage(token, payload, query, logger) {
668
+ try {
669
+ let nativeResponse = await postRequest({
670
+ token,
671
+ uri: query,
672
+ payload,
673
+ convertPayloadToUri: false,
674
+ attachUrlPrefix: false
675
+ });
676
+ loggerInfo(logger, `finished sending DM via Twitter API v2`, {
677
+ data: JSON.stringify(nativeResponse.data)
678
+ });
679
+ const response = nativeResponse.resp ? {
680
+ statusCode: nativeResponse.resp.statusCode,
681
+ statusMessage: nativeResponse.resp.statusCode === 201 || nativeResponse.resp.statusCode === 200 ? 'Success' : 'Unknown Failure',
682
+ data: nativeResponse.data.data
683
+ } : {
684
+ statusCode: nativeResponse.statusCode,
685
+ statusMessage: nativeResponse.message
686
+ };
687
+ loggerDebug(logger, `Twitter DM response is`, {
688
+ response: JSON.stringify(response)
689
+ });
690
+
691
+ // Normalize the response to ensure backward compatibility with v1.1 structure
692
+ // This is CRITICAL - v2 DM responses have dm_event_id instead of id
693
+ const normalizedResponse = normalizeTwitterResponse(response, 'dm', logger);
694
+ return normalizedResponse;
695
+ } catch (err) {
696
+ loggerError(logger, `Twitter DM exception details`, err);
697
+ throw err;
698
+ }
699
+ }
507
700
  async function postRequest(_ref2) {
508
701
  let {
509
702
  token,
@@ -662,32 +855,177 @@ function generateFilePath(mimeType, dirname) {
662
855
  }
663
856
  // local
664
857
  async function uploadMedia(attachment, token, discussionType, logger) {
665
- return new Promise(async (resolve, reject) => {
666
- let filePath = generateFilePath(attachment.mimeType, attachment.__dirname);
858
+ let filePath;
859
+ try {
860
+ filePath = generateFilePath(attachment.mimeType, attachment.__dirname);
667
861
  await downloadImage(attachment.url, filePath);
668
- const T = new Twit({
669
- consumer_key: token.consumer_key,
670
- consumer_secret: token.consumer_secret,
671
- access_token: token.token,
672
- access_token_secret: token.tokenSecret
862
+ const mediaCategory = discussionType === 'dm' ? 'dm_image' : 'tweet_image';
863
+ const fileStats = fs.statSync(filePath);
864
+ const fileSize = fileStats.size;
865
+ const fileData = fs.readFileSync(filePath);
866
+
867
+ // For small files, use simple upload
868
+ if (fileSize < 5000000) {
869
+ // 5MB
870
+ const mediaIdString = await simpleMediaUpload(fileData, attachment.mimeType, token, logger);
871
+ removeMedia(filePath, logger);
872
+ return mediaIdString;
873
+ } else {
874
+ // For large files, use chunked upload
875
+ const mediaIdString = await chunkedMediaUpload(filePath, fileSize, attachment.mimeType, mediaCategory, token, logger);
876
+ removeMedia(filePath, logger);
877
+ return mediaIdString;
878
+ }
879
+ } catch (e) {
880
+ loggerError(logger, `Failed uploading media`, e);
881
+ if (filePath) {
882
+ removeMedia(filePath, logger);
883
+ }
884
+ throw e;
885
+ }
886
+ }
887
+
888
+ // Simple upload for small media files
889
+ async function simpleMediaUpload(fileData, mimeType, token, logger) {
890
+ try {
891
+ const base64Data = fileData.toString('base64');
892
+ const mediaType = mimeType.split('/')[0]; // 'image', 'video', etc.
893
+
894
+ const url = 'https://upload.twitter.com/1.1/media/upload.json';
895
+ const {
896
+ Authorization
897
+ } = token.oauth.toHeader(token.oauth.authorize({
898
+ url,
899
+ method: 'post',
900
+ data: {}
901
+ }, {
902
+ key: token.token,
903
+ secret: token.tokenSecret
904
+ }));
905
+ const result = await superagent.post(url).set('Authorization', Authorization).send({
906
+ media_data: base64Data,
907
+ media_type: mimeType
673
908
  });
674
- let mediaCategory = discussionType === 'dm' ? 'dm' : 'tweet';
675
- try {
676
- T.postMediaChunked({
677
- file_path: filePath
678
- }, mediaCategory, function (err, data, response) {
679
- if (err) {
680
- reject(err);
681
- }
682
- resolve(data.media_id_string);
683
- removeMedia(filePath, logger);
909
+ loggerDebug(logger, 'Media uploaded successfully (simple)', {
910
+ media_id: result.body.media_id_string
911
+ });
912
+ return result.body.media_id_string;
913
+ } catch (e) {
914
+ loggerError(logger, 'Error in simple media upload', e);
915
+ throw e;
916
+ }
917
+ }
918
+
919
+ // Chunked upload for large media files
920
+ async function chunkedMediaUpload(filePath, fileSize, mimeType, mediaCategory, token, logger) {
921
+ try {
922
+ const mediaType = mimeType.split('/')[0];
923
+
924
+ // Step 1: INIT
925
+ const initResponse = await mediaUploadRequest({
926
+ command: 'INIT',
927
+ total_bytes: fileSize,
928
+ media_type: mimeType,
929
+ media_category: mediaCategory
930
+ }, token, logger);
931
+ const mediaId = initResponse.media_id_string;
932
+ loggerDebug(logger, 'Media upload INIT successful', {
933
+ mediaId
934
+ });
935
+
936
+ // Step 2: APPEND (upload in chunks)
937
+ const chunkSize = 5 * 1024 * 1024; // 5MB chunks
938
+ const fileBuffer = fs.readFileSync(filePath);
939
+ let segmentIndex = 0;
940
+ for (let offset = 0; offset < fileSize; offset += chunkSize) {
941
+ const chunk = fileBuffer.slice(offset, Math.min(offset + chunkSize, fileSize));
942
+ const base64Chunk = chunk.toString('base64');
943
+ await mediaUploadRequest({
944
+ command: 'APPEND',
945
+ media_id: mediaId,
946
+ media_data: base64Chunk,
947
+ segment_index: segmentIndex
948
+ }, token, logger);
949
+ loggerDebug(logger, `Media chunk ${segmentIndex} uploaded`, {
950
+ mediaId,
951
+ segmentIndex
684
952
  });
685
- } catch (e) {
686
- loggerError(logger, `Failed posting media`, e);
687
- // this is just a safety precaution
688
- removeMedia(filePath, logger);
953
+ segmentIndex++;
689
954
  }
690
- });
955
+
956
+ // Step 3: FINALIZE
957
+ const finalizeResponse = await mediaUploadRequest({
958
+ command: 'FINALIZE',
959
+ media_id: mediaId
960
+ }, token, logger);
961
+ loggerDebug(logger, 'Media upload FINALIZE successful', {
962
+ mediaId
963
+ });
964
+
965
+ // Step 4: Check processing status if needed
966
+ if (finalizeResponse.processing_info) {
967
+ await waitForMediaProcessing(mediaId, token, logger);
968
+ }
969
+ return mediaId;
970
+ } catch (e) {
971
+ loggerError(logger, 'Error in chunked media upload', e);
972
+ throw e;
973
+ }
974
+ }
975
+
976
+ // Make a media upload request
977
+ async function mediaUploadRequest(params, token, logger) {
978
+ const url = 'https://upload.twitter.com/1.1/media/upload.json';
979
+ const {
980
+ Authorization
981
+ } = token.oauth.toHeader(token.oauth.authorize({
982
+ url,
983
+ method: 'post',
984
+ data: {}
985
+ }, {
986
+ key: token.token,
987
+ secret: token.tokenSecret
988
+ }));
989
+ try {
990
+ const result = await superagent.post(url).set('Authorization', Authorization).send(params);
991
+ return result.body;
992
+ } catch (e) {
993
+ loggerError(logger, `Media upload request failed for command: ${params.command}`, e);
994
+ throw e;
995
+ }
996
+ }
997
+
998
+ // Wait for media processing to complete
999
+ async function waitForMediaProcessing(mediaId, token, logger) {
1000
+ let maxAttempts = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 20;
1001
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1002
+ const statusResponse = await mediaUploadRequest({
1003
+ command: 'STATUS',
1004
+ media_id: mediaId
1005
+ }, token, logger);
1006
+ if (!statusResponse.processing_info) {
1007
+ return; // Processing complete
1008
+ }
1009
+ const state = statusResponse.processing_info.state;
1010
+ if (state === 'succeeded') {
1011
+ loggerDebug(logger, 'Media processing succeeded', {
1012
+ mediaId
1013
+ });
1014
+ return;
1015
+ }
1016
+ if (state === 'failed') {
1017
+ throw new Error(`Media processing failed: ${statusResponse.processing_info.error?.message || 'Unknown error'}`);
1018
+ }
1019
+
1020
+ // Still processing, wait before checking again
1021
+ const waitTime = statusResponse.processing_info.check_after_secs || 1;
1022
+ loggerDebug(logger, `Media still processing, waiting ${waitTime}s`, {
1023
+ mediaId,
1024
+ state
1025
+ });
1026
+ await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
1027
+ }
1028
+ throw new Error('Media processing timeout');
691
1029
  }
692
1030
  function removeMedia(file, logger) {
693
1031
  try {
@@ -699,5 +1037,4 @@ function removeMedia(file, logger) {
699
1037
  } catch (e) {
700
1038
  loggerError(logger, `failed trying to remove media ${file} it may have already been removed`);
701
1039
  }
702
- }
703
- ;
1040
+ }