n8n-nodes-upload-post 0.1.56 → 0.4.0

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.
@@ -159,6 +159,7 @@ const FIRST_COMMENT_OVERRIDES = [
159
159
  { platform: 'reddit', param: 'redditFirstComment', field: 'reddit_first_comment' },
160
160
  { platform: 'bluesky', param: 'blueskyFirstComment', field: 'bluesky_first_comment' },
161
161
  { platform: 'linkedin', param: 'linkedinFirstComment', field: 'linkedin_first_comment' },
162
+ { platform: 'tiktok', param: 'tiktokFirstComment', field: 'tiktok_first_comment', operations: ['uploadPhotos', 'uploadVideo'] },
162
163
  ];
163
164
  const getFilteredPlatforms = (operation, platforms) => {
164
165
  var _a;
@@ -394,6 +395,20 @@ const applyFacebookOptions = (ctx, operation, formData, isManualPlatform = false
394
395
  }
395
396
  }
396
397
  };
398
+ const applySharedTiktokFields = (ctx, formData) => {
399
+ const musicId = ctx.node.getNodeParameter('tiktokMusicId', ctx.itemIndex, '');
400
+ if (musicId)
401
+ formData.tiktok_music_id = musicId;
402
+ const locationId = ctx.node.getNodeParameter('tiktokLocationId', ctx.itemIndex, '');
403
+ const locationName = ctx.node.getNodeParameter('tiktokLocationName', ctx.itemIndex, '');
404
+ if (locationId && locationName) {
405
+ formData.tiktok_location_id = locationId;
406
+ formData.tiktok_location_name = locationName;
407
+ }
408
+ const isAiGenerated = ctx.node.getNodeParameter('tiktokIsAiGenerated', ctx.itemIndex, false);
409
+ if (isAiGenerated)
410
+ formData.tiktok_is_ai_generated = 'true';
411
+ };
397
412
  const applyTiktokOptions = (ctx, operation, formData) => {
398
413
  if (operation === 'uploadPhotos') {
399
414
  const autoAddMusic = ctx.node.getNodeParameter('tiktokAutoAddMusic', ctx.itemIndex, false);
@@ -410,6 +425,7 @@ const applyTiktokOptions = (ctx, operation, formData) => {
410
425
  if (photoDescription && formData.description === undefined) {
411
426
  formData.description = photoDescription;
412
427
  }
428
+ applySharedTiktokFields(ctx, formData);
413
429
  }
414
430
  else if (operation === 'uploadVideo') {
415
431
  const privacyLevel = ctx.node.getNodeParameter('tiktokPrivacyLevel', ctx.itemIndex, '');
@@ -432,6 +448,23 @@ const applyTiktokOptions = (ctx, operation, formData) => {
432
448
  formData.is_aigc = String(isAigc);
433
449
  if (postMode)
434
450
  formData.post_mode = postMode;
451
+ applySharedTiktokFields(ctx, formData);
452
+ if (formData.tiktok_music_id) {
453
+ formData.tiktok_music_volume = ctx.node.getNodeParameter('tiktokMusicVolume', ctx.itemIndex, 50);
454
+ formData.tiktok_original_sound_volume = ctx.node.getNodeParameter('tiktokOriginalSoundVolume', ctx.itemIndex, 50);
455
+ const musicStart = ctx.node.getNodeParameter('tiktokMusicStart', ctx.itemIndex, 0);
456
+ const musicEnd = ctx.node.getNodeParameter('tiktokMusicEnd', ctx.itemIndex, 0);
457
+ if (musicStart > 0)
458
+ formData.tiktok_music_start = musicStart;
459
+ if (musicEnd > 0)
460
+ formData.tiktok_music_end = musicEnd;
461
+ }
462
+ const coverImageUrl = ctx.node.getNodeParameter('tiktokCoverImageUrl', ctx.itemIndex, '');
463
+ if (coverImageUrl)
464
+ formData.tiktok_cover_image_url = coverImageUrl;
465
+ const uploadToDraft = ctx.node.getNodeParameter('tiktokUploadToDraft', ctx.itemIndex, false);
466
+ if (uploadToDraft)
467
+ formData.tiktok_upload_to_draft = 'true';
435
468
  }
436
469
  };
437
470
  const applyInstagramOptions = async (ctx, operation, formData) => {
@@ -1234,18 +1267,28 @@ const buildUserRequest = async (ctx) => {
1234
1267
  });
1235
1268
  }
1236
1269
  };
1270
+ const COMMENT_ACTIONS_WITHOUT_POST = new Set(['like', 'unlike']);
1237
1271
  const buildInstagramRequest = (ctx) => {
1238
1272
  switch (ctx.operation) {
1239
1273
  case 'getPostComments': {
1240
1274
  const user = ctx.node.getNodeParameter('instagramUser', ctx.itemIndex);
1275
+ const platform = ctx.node.getNodeParameter('commentPlatform', ctx.itemIndex, 'instagram');
1241
1276
  const postId = ctx.node.getNodeParameter('instagramPostId', ctx.itemIndex);
1242
- const qs = { platform: 'instagram', user };
1277
+ const qs = { platform, user };
1243
1278
  if (postId.startsWith('http://') || postId.startsWith('https://')) {
1279
+ if (platform === 'tiktok') {
1280
+ throw new n8n_workflow_1.NodeOperationError(ctx.node.getNode(), 'TikTok comments are looked up by video ID, not by URL. Paste the numeric video ID instead.', {
1281
+ itemIndex: ctx.itemIndex,
1282
+ });
1283
+ }
1244
1284
  qs.post_url = postId;
1245
1285
  }
1246
1286
  else {
1247
1287
  qs.post_id = postId;
1248
1288
  }
1289
+ const commentId = ctx.node.getNodeParameter('commentsCommentId', ctx.itemIndex, '');
1290
+ if (commentId)
1291
+ qs.comment_id = commentId;
1249
1292
  return {
1250
1293
  endpoint: '/uploadposts/comments',
1251
1294
  method: 'GET',
@@ -1278,12 +1321,91 @@ const buildInstagramRequest = (ctx) => {
1278
1321
  waitForCompletion: false,
1279
1322
  };
1280
1323
  }
1324
+ case 'commentAction': {
1325
+ const user = ctx.node.getNodeParameter('instagramUser', ctx.itemIndex);
1326
+ const platform = ctx.node.getNodeParameter('commentActionPlatform', ctx.itemIndex, 'tiktok');
1327
+ const action = ctx.node.getNodeParameter('commentModerationAction', ctx.itemIndex);
1328
+ const body = {
1329
+ platform,
1330
+ user,
1331
+ comment_id: ctx.node.getNodeParameter('instagramCommentId', ctx.itemIndex),
1332
+ action,
1333
+ };
1334
+ if (!COMMENT_ACTIONS_WITHOUT_POST.has(action)) {
1335
+ const postId = ctx.node.getNodeParameter('commentActionPostId', ctx.itemIndex, '');
1336
+ if (!postId) {
1337
+ throw new n8n_workflow_1.NodeOperationError(ctx.node.getNode(), `A Post ID is required to ${action} a comment.`, {
1338
+ itemIndex: ctx.itemIndex,
1339
+ });
1340
+ }
1341
+ body.post_id = postId;
1342
+ }
1343
+ return {
1344
+ endpoint: '/uploadposts/comments/action',
1345
+ method: 'POST',
1346
+ body,
1347
+ isUploadOperation: false,
1348
+ waitForCompletion: false,
1349
+ };
1350
+ }
1281
1351
  default:
1282
1352
  throw new n8n_workflow_1.NodeOperationError(ctx.node.getNode(), `Unsupported Instagram operation: ${ctx.operation}`, {
1283
1353
  itemIndex: ctx.itemIndex,
1284
1354
  });
1285
1355
  }
1286
1356
  };
1357
+ const buildInsightsRequest = (ctx) => {
1358
+ const user = ctx.node.getNodeParameter('insightsUser', ctx.itemIndex);
1359
+ const platform = ctx.node.getNodeParameter('insightsPlatform', ctx.itemIndex, 'tiktok');
1360
+ switch (ctx.operation) {
1361
+ case 'getAudience': {
1362
+ const qs = { user, platform };
1363
+ const startDate = ctx.node.getNodeParameter('audienceStartDate', ctx.itemIndex, '');
1364
+ if (startDate)
1365
+ qs.start_date = startDate.slice(0, 10);
1366
+ const endDate = ctx.node.getNodeParameter('audienceEndDate', ctx.itemIndex, '');
1367
+ if (endDate)
1368
+ qs.end_date = endDate.slice(0, 10);
1369
+ const benchmarkCategory = ctx.node.getNodeParameter('audienceBenchmarkCategory', ctx.itemIndex, '');
1370
+ if (benchmarkCategory)
1371
+ qs.benchmark_category = benchmarkCategory;
1372
+ return {
1373
+ endpoint: '/uploadposts/audience',
1374
+ method: 'GET',
1375
+ qs,
1376
+ isUploadOperation: false,
1377
+ waitForCompletion: false,
1378
+ };
1379
+ }
1380
+ case 'getSuggestions': {
1381
+ const qs = {
1382
+ user,
1383
+ platform,
1384
+ type: ctx.node.getNodeParameter('suggestionsType', ctx.itemIndex),
1385
+ };
1386
+ const query = ctx.node.getNodeParameter('suggestionsQuery', ctx.itemIndex, '');
1387
+ if (query)
1388
+ qs.q = query;
1389
+ const countryCode = ctx.node.getNodeParameter('suggestionsCountryCode', ctx.itemIndex, '');
1390
+ if (countryCode)
1391
+ qs.country_code = countryCode;
1392
+ const language = ctx.node.getNodeParameter('suggestionsLanguage', ctx.itemIndex, '');
1393
+ if (language)
1394
+ qs.language = language;
1395
+ return {
1396
+ endpoint: '/uploadposts/suggestions',
1397
+ method: 'GET',
1398
+ qs,
1399
+ isUploadOperation: false,
1400
+ waitForCompletion: false,
1401
+ };
1402
+ }
1403
+ default:
1404
+ throw new n8n_workflow_1.NodeOperationError(ctx.node.getNode(), `Unsupported insights operation: ${ctx.operation}`, {
1405
+ itemIndex: ctx.itemIndex,
1406
+ });
1407
+ }
1408
+ };
1287
1409
  const buildRequestConfig = async (ctx) => {
1288
1410
  if (ctx.operation === 'uploadPhotos') {
1289
1411
  return buildUploadPhotosRequest(ctx);
@@ -1301,6 +1423,9 @@ const buildRequestConfig = async (ctx) => {
1301
1423
  if (resource === 'instagram') {
1302
1424
  return buildInstagramRequest(ctx);
1303
1425
  }
1426
+ if (resource === 'insights') {
1427
+ return buildInsightsRequest(ctx);
1428
+ }
1304
1429
  if (resource === 'monitoring') {
1305
1430
  return buildMonitoringRequest(ctx);
1306
1431
  }
@@ -1404,9 +1529,10 @@ class UploadPost {
1404
1529
  type: 'options',
1405
1530
  noDataExpression: true,
1406
1531
  options: [
1407
- { name: 'Upload', value: 'uploads' },
1408
- { name: 'Instagram', value: 'instagram' },
1532
+ { name: 'Comment', value: 'instagram' },
1533
+ { name: 'Insight', value: 'insights' },
1409
1534
  { name: 'Status & History', value: 'monitoring' },
1535
+ { name: 'Upload', value: 'uploads' },
1410
1536
  { name: 'User', value: 'users' },
1411
1537
  ],
1412
1538
  default: 'uploads',
@@ -1437,7 +1563,7 @@ class UploadPost {
1437
1563
  { name: 'Get Cached Post Analytics', value: 'getCachedPostAnalytics', action: 'Get cached post analytics', description: 'Replay per-post metrics already fetched for a profile instead of querying the platforms again, so it is not subject to the live analytics rate limit (100 requests / 5 minutes). Only contains posts previously fetched through Get Post Analytics; there is no background refresh, so captured_at is the last time that post was read live. Use Get Post Analytics to refresh a post, and this operation for bulk re-reads.' },
1438
1564
  { name: 'Get Job Status', value: 'getJobStatus', action: 'Get job status', description: 'Check the status of a scheduled or queued post using the job_id' },
1439
1565
  { name: 'Get Platform Metrics', value: 'getPlatformMetrics', action: 'Get platform metrics', description: 'List the analytics metrics available for each platform' },
1440
- { name: 'Get Post Analytics', value: 'getPostAnalytics', action: 'Get post analytics', description: 'Retrieve per-post analytics for an upload using its request_id' },
1566
+ { name: 'Get Post Analytics', value: 'getPostAnalytics', action: 'Get post analytics', description: 'Retrieve per-post analytics for an upload using its request_id. On TikTok post_metrics also carries retention, impression_sources, audience_types, new_followers, reach and the watch times.' },
1441
1567
  { name: 'Get Post Analytics by Platform ID', value: 'getPostAnalyticsByPlatformId', action: 'Get post analytics by platform id', description: 'Retrieve per-post analytics using the native platform post ID, for posts not published through Upload-Post' },
1442
1568
  { name: 'Get Reddit Detailed Posts', value: 'getRedditDetailedPosts', action: 'Get reddit detailed posts', description: 'Retrieve detailed Reddit posts with full media information' },
1443
1569
  { name: 'Get Total Impressions', value: 'getTotalImpressions', action: 'Get total impressions', description: 'Retrieve impressions aggregated across connected platforms for a date range' },
@@ -1473,9 +1599,10 @@ class UploadPost {
1473
1599
  type: 'options',
1474
1600
  noDataExpression: true,
1475
1601
  options: [
1476
- { name: 'Get Post Comments', value: 'getPostComments', action: 'Get post comments', description: 'Retrieve all comments on a specific Instagram post' },
1477
- { name: 'Private Reply to Comment', value: 'privateReplyToComment', action: 'Private reply to comment', description: 'Send a private reply (DM) to the author of a comment' },
1478
- { name: 'Public Reply to Comment', value: 'publicReplyToComment', action: 'Public reply to comment', description: 'Post a public reply visible under the original comment' },
1602
+ { name: 'Comment Action', value: 'commentAction', action: 'Act on a comment', description: 'Moderate one comment: hide, unhide, like, unlike, pin or unpin it. Platform says which connected network the comment lives on. Post ID is required to hide, unhide, pin or unpin, and is never sent for like or unlike.' },
1603
+ { name: 'Get Post Comments', value: 'getPostComments', action: 'Get post comments', description: 'Retrieve the comments on a post from the network given by Platform (Instagram, Facebook, YouTube, LinkedIn or TikTok). Fill Comment ID to get the replies to that comment instead of the top-level comments of the post.' },
1604
+ { name: 'Private Reply to Comment', value: 'privateReplyToComment', action: 'Private reply to comment', description: 'Send a private reply (DM) to the author of a comment. Instagram only.' },
1605
+ { name: 'Public Reply to Comment', value: 'publicReplyToComment', action: 'Public reply to comment', description: 'Post a public reply visible under the original comment. Instagram only.' },
1479
1606
  ],
1480
1607
  default: 'getPostComments',
1481
1608
  displayOptions: { show: { resource: ['instagram'] } },
@@ -1492,7 +1619,40 @@ class UploadPost {
1492
1619
  displayOptions: {
1493
1620
  show: {
1494
1621
  resource: ['instagram'],
1495
- operation: ['getPostComments', 'privateReplyToComment', 'publicReplyToComment'],
1622
+ operation: ['getPostComments', 'privateReplyToComment', 'publicReplyToComment', 'commentAction'],
1623
+ },
1624
+ },
1625
+ },
1626
+ {
1627
+ displayName: 'Platform',
1628
+ name: 'commentPlatform',
1629
+ type: 'options',
1630
+ options: [
1631
+ { name: 'Facebook', value: 'facebook' },
1632
+ { name: 'Instagram', value: 'instagram' },
1633
+ { name: 'LinkedIn', value: 'linkedin' },
1634
+ { name: 'TikTok', value: 'tiktok' },
1635
+ { name: 'YouTube', value: 'youtube' },
1636
+ ],
1637
+ default: 'instagram',
1638
+ description: 'Network the post lives on. TikTok needs the "comments" capability on the profile (see the capabilities array returned by List Users); an account connected before that capability existed has to reconnect TikTok.',
1639
+ displayOptions: {
1640
+ show: {
1641
+ resource: ['instagram'],
1642
+ operation: ['getPostComments'],
1643
+ },
1644
+ },
1645
+ },
1646
+ {
1647
+ displayName: 'Comment ID',
1648
+ name: 'commentsCommentId',
1649
+ type: 'string',
1650
+ default: '',
1651
+ description: 'Leave empty for the top-level comments of the post. Set it to a comment ID to get the replies to that comment instead: it is the same question with one parameter more.',
1652
+ displayOptions: {
1653
+ show: {
1654
+ resource: ['instagram'],
1655
+ operation: ['getPostComments'],
1496
1656
  },
1497
1657
  },
1498
1658
  },
@@ -1502,7 +1662,7 @@ class UploadPost {
1502
1662
  type: 'string',
1503
1663
  required: true,
1504
1664
  default: '',
1505
- description: 'Numeric media ID or full Instagram post URL (e.g., https://www.instagram.com/p/ABC123/)',
1665
+ description: 'Post identifier. Instagram and Facebook accept the numeric media ID or the post URL, YouTube the video ID, LinkedIn the post URN, TikTok the video ID (TikTok has no URL lookup).',
1506
1666
  displayOptions: {
1507
1667
  show: {
1508
1668
  resource: ['instagram'],
@@ -1516,11 +1676,55 @@ class UploadPost {
1516
1676
  type: 'string',
1517
1677
  required: true,
1518
1678
  default: '',
1519
- description: 'The ID of the comment to reply to (from Get Post Comments)',
1679
+ description: 'The ID of the comment to reply to or act on (from Get Post Comments)',
1520
1680
  displayOptions: {
1521
1681
  show: {
1522
1682
  resource: ['instagram'],
1523
- operation: ['privateReplyToComment', 'publicReplyToComment'],
1683
+ operation: ['privateReplyToComment', 'publicReplyToComment', 'commentAction'],
1684
+ },
1685
+ },
1686
+ },
1687
+ {
1688
+ displayName: 'Platform',
1689
+ name: 'commentActionPlatform',
1690
+ type: 'options',
1691
+ required: true,
1692
+ options: [
1693
+ { name: 'TikTok', value: 'tiktok' },
1694
+ ],
1695
+ default: 'tiktok',
1696
+ description: 'Network the comment lives on. TikTok is the network that answers this question today; any other one replies with platform_not_supported and the list of the ones that can. On TikTok the profile needs the "comments" capability (see the capabilities array returned by List Users); an account connected before that capability existed has to reconnect TikTok.',
1697
+ displayOptions: { show: { resource: ['instagram'], operation: ['commentAction'] } },
1698
+ },
1699
+ {
1700
+ displayName: 'Action',
1701
+ name: 'commentModerationAction',
1702
+ type: 'options',
1703
+ required: true,
1704
+ options: [
1705
+ { name: 'Hide', value: 'hide' },
1706
+ { name: 'Like', value: 'like' },
1707
+ { name: 'Pin', value: 'pin' },
1708
+ { name: 'Unhide', value: 'unhide' },
1709
+ { name: 'Unlike', value: 'unlike' },
1710
+ { name: 'Unpin', value: 'unpin' },
1711
+ ],
1712
+ default: 'hide',
1713
+ description: 'What to do with the comment. Each action carries its own inverse, so there is no separate toggle: hide/unhide change its visibility, like/unlike react to it as the account, pin/unpin move it to the top of the thread.',
1714
+ displayOptions: { show: { resource: ['instagram'], operation: ['commentAction'] } },
1715
+ },
1716
+ {
1717
+ displayName: 'Post ID',
1718
+ name: 'commentActionPostId',
1719
+ type: 'string',
1720
+ required: true,
1721
+ default: '',
1722
+ description: 'Native ID of the post the comment hangs from, as the platform assigns it (on TikTok, the video ID). Required to hide, unhide, pin or unpin; it is not sent for like or unlike, which the API rejects when it carries a post.',
1723
+ displayOptions: {
1724
+ show: {
1725
+ resource: ['instagram'],
1726
+ operation: ['commentAction'],
1727
+ commentModerationAction: ['hide', 'unhide', 'pin', 'unpin'],
1524
1728
  },
1525
1729
  },
1526
1730
  },
@@ -1539,6 +1743,107 @@ class UploadPost {
1539
1743
  },
1540
1744
  },
1541
1745
  },
1746
+ {
1747
+ displayName: 'Operation',
1748
+ name: 'operation',
1749
+ type: 'options',
1750
+ noDataExpression: true,
1751
+ options: [
1752
+ { name: 'Get Audience', value: 'getAudience', action: 'Get audience', description: 'Ask a connected network who follows the profile: audience countries, cities, age brackets and genders, how many followers are online each hour, daily followers gained and lost, profile actions and the bio. Platform says which network answers. Pass a Benchmark Category to also get that niche averages (comments, engagement rate, follower count and growth, likes, shares, video count and views) next to the numbers of the account; every response lists the 25 accepted categories in benchmark_categories. Use this to know WHO the audience is and WHEN to post; use Get Post Analytics for how a given post did.' },
1753
+ { name: 'Get Suggestions', value: 'getSuggestions', action: 'Get suggestions', description: 'Ask a connected network what to write about: hashtags (each with its view count) or the searches people run, around a seed word. Type picks which of the two, Platform says which network answers. Use this before writing a caption to pick hashtags or find the wording an audience searches for.' },
1754
+ ],
1755
+ default: 'getAudience',
1756
+ displayOptions: { show: { resource: ['insights'] } },
1757
+ },
1758
+ {
1759
+ displayName: 'User Identifier Name or ID',
1760
+ name: 'insightsUser',
1761
+ type: 'options',
1762
+ noDataExpression: true,
1763
+ required: true,
1764
+ default: '',
1765
+ description: 'Upload-Post profile whose connected account answers the question. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
1766
+ typeOptions: { loadOptionsMethod: 'getUserProfiles' },
1767
+ displayOptions: { show: { resource: ['insights'] } },
1768
+ },
1769
+ {
1770
+ displayName: 'Platform',
1771
+ name: 'insightsPlatform',
1772
+ type: 'options',
1773
+ required: true,
1774
+ options: [
1775
+ { name: 'TikTok', value: 'tiktok' },
1776
+ ],
1777
+ default: 'tiktok',
1778
+ description: 'Network that answers the question. TikTok is the network that answers this question today; any other one replies with platform_not_supported and the list of the ones that can. On TikTok the profile needs the "profile_analytics" capability (see the capabilities array returned by List Users).',
1779
+ displayOptions: { show: { resource: ['insights'] } },
1780
+ },
1781
+ {
1782
+ displayName: 'Start Date',
1783
+ name: 'audienceStartDate',
1784
+ type: 'string',
1785
+ default: '',
1786
+ placeholder: 'YYYY-MM-DD',
1787
+ description: 'First day of the window. The server clamps the range to 60 days at most and always below today, so a wider window is trimmed instead of rejected. Leave empty for the default range.',
1788
+ displayOptions: { show: { resource: ['insights'], operation: ['getAudience'] } },
1789
+ },
1790
+ {
1791
+ displayName: 'End Date',
1792
+ name: 'audienceEndDate',
1793
+ type: 'string',
1794
+ default: '',
1795
+ placeholder: 'YYYY-MM-DD',
1796
+ description: 'Last day of the window. The server moves it below today, because there are no audience numbers for the current day yet.',
1797
+ displayOptions: { show: { resource: ['insights'], operation: ['getAudience'] } },
1798
+ },
1799
+ {
1800
+ displayName: 'Benchmark Category',
1801
+ name: 'audienceBenchmarkCategory',
1802
+ type: 'string',
1803
+ default: '',
1804
+ placeholder: 'SOFTWARE_AND_APPS',
1805
+ description: 'Niche to compare the account against, which adds a benchmark object to the response. Leave empty to skip the comparison: the accepted values come back in benchmark_categories on every response anyway, so no extra call is needed to fill a picker.',
1806
+ displayOptions: { show: { resource: ['insights'], operation: ['getAudience'] } },
1807
+ },
1808
+ {
1809
+ displayName: 'Type',
1810
+ name: 'suggestionsType',
1811
+ type: 'options',
1812
+ required: true,
1813
+ options: [
1814
+ { name: 'Hashtags', value: 'hashtags' },
1815
+ { name: 'Keywords', value: 'keywords' },
1816
+ ],
1817
+ default: 'hashtags',
1818
+ description: 'Which suggestions you want. Hashtags answers with a list of name and view_count; keywords answers with the searches people run around the word.',
1819
+ displayOptions: { show: { resource: ['insights'], operation: ['getSuggestions'] } },
1820
+ },
1821
+ {
1822
+ displayName: 'Query',
1823
+ name: 'suggestionsQuery',
1824
+ type: 'string',
1825
+ default: '',
1826
+ description: 'Seed word to get suggestions around, written without the "#"',
1827
+ displayOptions: { show: { resource: ['insights'], operation: ['getSuggestions'] } },
1828
+ },
1829
+ {
1830
+ displayName: 'Country Code',
1831
+ name: 'suggestionsCountryCode',
1832
+ type: 'string',
1833
+ default: '',
1834
+ placeholder: 'ES',
1835
+ description: 'ISO 3166-1 alpha-2 country to bias the suggestions towards, e.g. ES or US',
1836
+ displayOptions: { show: { resource: ['insights'], operation: ['getSuggestions'] } },
1837
+ },
1838
+ {
1839
+ displayName: 'Language',
1840
+ name: 'suggestionsLanguage',
1841
+ type: 'string',
1842
+ default: '',
1843
+ placeholder: 'es',
1844
+ description: 'Language code to bias the suggestions towards, e.g. es or en',
1845
+ displayOptions: { show: { resource: ['insights'], operation: ['getSuggestions'] } },
1846
+ },
1542
1847
  {
1543
1848
  displayName: 'User Identifier Name or ID',
1544
1849
  name: 'user',
@@ -1593,7 +1898,7 @@ class UploadPost {
1593
1898
  name: 'firstComment',
1594
1899
  type: 'string',
1595
1900
  default: '',
1596
- description: 'Text to post as the first comment (or reply) immediately after publishing. Supported on Instagram, Facebook, X, Threads, YouTube, Reddit, Bluesky.',
1901
+ description: 'Text to post as the first comment (or reply) immediately after publishing. Supported on Instagram, Facebook, X, Threads, YouTube, Reddit, Bluesky, LinkedIn and TikTok. On TikTok the profile needs the "comments" capability (see the capabilities array returned by List Users).',
1597
1902
  displayOptions: { show: { resource: ['uploads'], operation: ['uploadPhotos', 'uploadVideo', 'uploadText'] } },
1598
1903
  },
1599
1904
  {
@@ -1873,6 +2178,14 @@ class UploadPost {
1873
2178
  description: 'Optional override for LinkedIn first comment. If provided, overrides the generic First Comment for LinkedIn.',
1874
2179
  displayOptions: { show: { operation: ['uploadPhotos', 'uploadVideo', 'uploadText', 'uploadDocument'], platform: ['linkedin', '__manual_platform__'] } },
1875
2180
  },
2181
+ {
2182
+ displayName: 'TikTok First Comment (Override)',
2183
+ name: 'tiktokFirstComment',
2184
+ type: 'string',
2185
+ default: '',
2186
+ description: 'Optional override for the TikTok first comment. If provided, overrides the generic First Comment for TikTok. The profile needs the "comments" capability (see the capabilities array returned by List Users); an account connected before that capability existed has to reconnect TikTok.',
2187
+ displayOptions: { show: { operation: ['uploadPhotos', 'uploadVideo'], platform: ['tiktok', '__manual_platform__'] } },
2188
+ },
1876
2189
  {
1877
2190
  displayName: 'Photos (Files or URLs)',
1878
2191
  name: 'photos',
@@ -2842,13 +3155,14 @@ class UploadPost {
2842
3155
  name: 'tiktokPrivacyLevel',
2843
3156
  type: 'options',
2844
3157
  options: [
2845
- { name: 'Public to Everyone', value: 'PUBLIC_TO_EVERYONE' },
2846
- { name: 'Mutual Follow Friends', value: 'MUTUAL_FOLLOW_FRIENDS' },
3158
+ { name: 'Account Default', value: '' },
2847
3159
  { name: 'Follower of Creator', value: 'FOLLOWER_OF_CREATOR' },
3160
+ { name: 'Mutual Follow Friends', value: 'MUTUAL_FOLLOW_FRIENDS' },
3161
+ { name: 'Public to Everyone', value: 'PUBLIC_TO_EVERYONE' },
2848
3162
  { name: 'Self Only', value: 'SELF_ONLY' },
2849
3163
  ],
2850
- default: 'PUBLIC_TO_EVERYONE',
2851
- description: 'Privacy setting for TikTok video (PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, etc.). Only for Upload Video.',
3164
+ default: '',
3165
+ description: 'Privacy setting for the TikTok video. TikTok decides per account which levels are available: a private account has no Public to Everyone, and asking for one the account does not have fails with tiktok_privacy_unavailable listing the allowed ones. Account Default sends nothing and lets TikTok apply the account\'s own setting, which is the only value guaranteed to work on every account. Only for Upload Video.',
2852
3166
  displayOptions: {
2853
3167
  show: {
2854
3168
  operation: ['uploadVideo'],
@@ -2951,6 +3265,140 @@ class UploadPost {
2951
3265
  }
2952
3266
  },
2953
3267
  },
3268
+ {
3269
+ displayName: 'TikTok Music ID',
3270
+ name: 'tiktokMusicId',
3271
+ type: 'string',
3272
+ default: '',
3273
+ description: 'Commercial Music Library track ID to add to the post: the ID field returned by the TikTok trending-music or music-search endpoint, not its commercial_music_id. To find a track by song or artist, call GET /api/uploadposts/tiktok/music/search from an HTTP Request node first. Works on video and photo posts alike, but a photo post takes the ID alone: the volume and trim options below are video-only. Needs the music capability on the TikTok connection (see capabilities in the user-profiles response); without it the field is ignored with a warning and the post still publishes. Leave empty to skip.',
3274
+ displayOptions: {
3275
+ show: {
3276
+ operation: ['uploadVideo', 'uploadPhotos'],
3277
+ platform: ['tiktok', '__manual_platform__']
3278
+ }
3279
+ },
3280
+ },
3281
+ {
3282
+ displayName: 'TikTok Music Volume',
3283
+ name: 'tiktokMusicVolume',
3284
+ type: 'number',
3285
+ default: 50,
3286
+ typeOptions: { minValue: 0, maxValue: 100 },
3287
+ description: 'Volume (0-100) of the Commercial Music Library track. Only sent when a TikTok Music ID is set.',
3288
+ displayOptions: {
3289
+ show: {
3290
+ operation: ['uploadVideo'],
3291
+ platform: ['tiktok', '__manual_platform__']
3292
+ }
3293
+ },
3294
+ },
3295
+ {
3296
+ displayName: 'TikTok Music Start (Ms)',
3297
+ name: 'tiktokMusicStart',
3298
+ type: 'number',
3299
+ default: 0,
3300
+ typeOptions: { minValue: 0 },
3301
+ description: 'Start offset (ms) of the music track. Only sent when a TikTok Music ID is set and this is above 0.',
3302
+ displayOptions: {
3303
+ show: {
3304
+ operation: ['uploadVideo'],
3305
+ platform: ['tiktok', '__manual_platform__']
3306
+ }
3307
+ },
3308
+ },
3309
+ {
3310
+ displayName: 'TikTok Music End (Ms)',
3311
+ name: 'tiktokMusicEnd',
3312
+ type: 'number',
3313
+ default: 0,
3314
+ typeOptions: { minValue: 0 },
3315
+ description: 'End offset (ms) of the music track. Only sent when a TikTok Music ID is set and this is above 0.',
3316
+ displayOptions: {
3317
+ show: {
3318
+ operation: ['uploadVideo'],
3319
+ platform: ['tiktok', '__manual_platform__']
3320
+ }
3321
+ },
3322
+ },
3323
+ {
3324
+ displayName: 'TikTok Original Sound Volume',
3325
+ name: 'tiktokOriginalSoundVolume',
3326
+ type: 'number',
3327
+ default: 50,
3328
+ typeOptions: { minValue: 0, maxValue: 100 },
3329
+ description: 'Volume (0-100) of the original video audio when music is added. Only sent when a TikTok Music ID is set. Keep it above 0 so the original audio is not muted.',
3330
+ displayOptions: {
3331
+ show: {
3332
+ operation: ['uploadVideo'],
3333
+ platform: ['tiktok', '__manual_platform__']
3334
+ }
3335
+ },
3336
+ },
3337
+ {
3338
+ displayName: 'TikTok Location ID',
3339
+ name: 'tiktokLocationId',
3340
+ type: 'string',
3341
+ default: '',
3342
+ description: 'Location ID to tag on the post (video or photos). Needs the location capability on the TikTok connection (see capabilities in the user-profiles response) and a TikTok Location Name — the tag is only sent when both are filled in. Without the capability the field is ignored with a warning and the post still publishes. Leave empty to skip.',
3343
+ displayOptions: {
3344
+ show: {
3345
+ operation: ['uploadVideo', 'uploadPhotos'],
3346
+ platform: ['tiktok', '__manual_platform__']
3347
+ }
3348
+ },
3349
+ },
3350
+ {
3351
+ displayName: 'TikTok Location Name',
3352
+ name: 'tiktokLocationName',
3353
+ type: 'string',
3354
+ default: '',
3355
+ description: 'Location name matching the TikTok Location ID. TikTok requires both together, so the location tag is only sent when this and the ID are both filled in.',
3356
+ displayOptions: {
3357
+ show: {
3358
+ operation: ['uploadVideo', 'uploadPhotos'],
3359
+ platform: ['tiktok', '__manual_platform__']
3360
+ }
3361
+ },
3362
+ },
3363
+ {
3364
+ displayName: 'TikTok Cover Image URL',
3365
+ name: 'tiktokCoverImageUrl',
3366
+ type: 'string',
3367
+ default: '',
3368
+ description: 'Custom cover image URL for the video. Needs the cover_image capability on the TikTok connection (see capabilities in the user-profiles response); without it the field is ignored with a warning and the post still publishes. Takes priority over TikTok Cover Timestamp.',
3369
+ displayOptions: {
3370
+ show: {
3371
+ operation: ['uploadVideo'],
3372
+ platform: ['tiktok', '__manual_platform__']
3373
+ }
3374
+ },
3375
+ },
3376
+ {
3377
+ displayName: 'TikTok Is AI Generated',
3378
+ name: 'tiktokIsAiGenerated',
3379
+ type: 'boolean',
3380
+ default: false,
3381
+ description: 'Whether to disclose the post as AI-generated on TikTok. Works on video and photo posts. Only sent when enabled.',
3382
+ displayOptions: {
3383
+ show: {
3384
+ operation: ['uploadVideo', 'uploadPhotos'],
3385
+ platform: ['tiktok', '__manual_platform__']
3386
+ }
3387
+ },
3388
+ },
3389
+ {
3390
+ displayName: 'TikTok Upload to Draft',
3391
+ name: 'tiktokUploadToDraft',
3392
+ type: 'boolean',
3393
+ default: false,
3394
+ description: 'Whether to send the video to TikTok drafts instead of publishing it. Needs the draft capability on the TikTok connection (see capabilities in the user-profiles response); without it the field is ignored with a warning and the post still publishes. When enabled TikTok ignores the rest of the post settings.',
3395
+ displayOptions: {
3396
+ show: {
3397
+ operation: ['uploadVideo'],
3398
+ platform: ['tiktok', '__manual_platform__']
3399
+ }
3400
+ },
3401
+ },
2954
3402
  {
2955
3403
  displayName: 'Instagram Media Type',
2956
3404
  name: 'instagramMediaType',