n8n-nodes-upload-post 0.1.48 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,7 +36,7 @@ The node provides the following operations grouped for clarity:
36
36
  - Supports file uploads and photo URLs (comma-separated list).
37
37
  - Common parameters: User Identifier, Platform Names or IDs, Title / Main Content, Description (optional), Photos (Files or URLs), Scheduled Date (optional).
38
38
  - Title/Description overrides per platform: `instagram_title`, `facebook_title`, `tiktok_title`, `linkedin_title`, `x_title`, `youtube_title`, `pinterest_title`, `threads_title`.
39
- - Platform-specific parameters available for: Facebook, Instagram, LinkedIn, Pinterest, TikTok, X (Twitter).
39
+ - Platform-specific parameters available for: Discord, Facebook, Instagram, LinkedIn, Pinterest, Telegram, TikTok, X (Twitter).
40
40
  - **Pinterest**: Requires Board selection via dynamic selector.
41
41
  - **Instagram**: Media Type (Image/Stories), TikTok: Auto-add music, disable comments, brand content toggles.
42
42
  - **Facebook**: Page selection via dynamic selector.
@@ -47,7 +47,7 @@ The node provides the following operations grouped for clarity:
47
47
  - Supports file uploads and video URLs.
48
48
  - Common parameters: User Identifier, Platform Names or IDs, Title / Main Content, Description (optional), Video (File or URL), Scheduled Date (optional).
49
49
  - Title/Description overrides per platform: `instagram_title`, `facebook_title`, `tiktok_title`, `linkedin_title`, `x_title`, `youtube_title`, `pinterest_title`.
50
- - Platform-specific parameters available for: Facebook, Instagram, LinkedIn, Pinterest, Threads, TikTok, X (Twitter), YouTube.
50
+ - Platform-specific parameters available for: Discord, Facebook, Instagram, LinkedIn, Pinterest, Telegram, Threads, TikTok, X (Twitter), YouTube.
51
51
  - **YouTube**: Custom thumbnail (URL/binary), tags, category, privacy, embeddable, license, public stats, made for kids, synthetic media declaration, geo-restrictions, paid product placement, recording date, default language/audio language.
52
52
  - **Facebook**: Page selection, video state (Published/Draft), media type (Reels/Stories).
53
53
  - **Instagram**: Media type (Reels/Stories), share to feed, collaborators, cover URL, audio name, user tags, location ID, thumb offset.
@@ -58,7 +58,7 @@ The node provides the following operations grouped for clarity:
58
58
 
59
59
  - **Upload Text**: Upload a text-based post to supported platforms.
60
60
  - Common parameters: User Identifier, Platform Names or IDs, Title / Main Content (used as post content), Scheduled Date (optional).
61
- - Platform-specific parameters available for: Facebook, LinkedIn, Reddit, Threads, X (Twitter).
61
+ - Platform-specific parameters available for: Discord, Facebook, LinkedIn, Reddit, Telegram, Threads, X (Twitter).
62
62
  - **Facebook**: Page selection, link URL for preview.
63
63
  - **LinkedIn**: Page selection ("Me" for personal profile).
64
64
  - **X (Twitter)**: Reply settings, poll options (2-4 options, 5-10080 min duration), post URL, quote tweet ID, geo place ID, super followers exclusivity, community ID, sharing options, direct message deep link, card URI, long text handling.
@@ -70,8 +70,18 @@ The node provides the following operations grouped for clarity:
70
70
  - Parameters: Request ID.
71
71
  - **Get Upload History**: List past uploads.
72
72
  - Parameters: Page (default 1), Limit (default 20). Limit can be 20, 50, or 100.
73
- - **Get Analytics**: Retrieve aggregated analytics for uploads.
74
- - Optional filters: From Date, To Date, User Filter, Platforms.
73
+ - **Get Analytics**: Retrieve aggregated profile analytics.
74
+ - Parameters: Profile Username, Platforms (required — at least one).
75
+ - Supported: Facebook, Instagram, LinkedIn, Pinterest, Reddit, Threads, TikTok, X, YouTube. Analytics are not available for Bluesky, Discord, Google Business or Telegram.
76
+ - **Get Post Analytics**: Per-post metrics for an upload, by `request_id`.
77
+ - Parameters: Request ID, Platform (optional filter).
78
+ - **Get Post Analytics by Platform ID**: Per-post metrics for a post published outside Upload-Post.
79
+ - Parameters: Platform Post ID, Platform, Profile Username.
80
+ - **Get Total Impressions**: Impressions aggregated across connected platforms.
81
+ - Parameters: Profile Username, Period or Start/End Date, Platforms, Breakdown.
82
+ - **Get Platform Metrics**: Lists which metrics each platform exposes.
83
+ - **Get Reddit Detailed Posts**: Detailed Reddit posts with full media information.
84
+ - Parameters: Profile Username.
75
85
 
76
86
  ### Scheduled Posts
77
87
  - **List Scheduled Posts**: Lists future scheduled jobs.
@@ -102,7 +112,9 @@ Refer to the [Upload Post API Documentation](https://docs.upload-post.com) for d
102
112
 
103
113
  Some uploads are processed asynchronously and the API returns immediately with a `request_id`. This happens when:
104
114
  - You enable "Upload Asynchronously" in the node, or
105
- - The upload takes longer than ~59 seconds and the API switches to async mode automatically.
115
+ - The upload exceeds the server's synchronous wait window, in which case the API switches to async mode automatically.
116
+
117
+ Leaving "Upload Asynchronously" enabled is recommended. Turning it off holds the HTTP connection open for the whole upload, which makes a proxy or execution timeout — and therefore a retry — more likely.
106
118
 
107
119
  You have two ways to handle this in n8n:
108
120
 
@@ -117,6 +129,14 @@ You have two ways to handle this in n8n:
117
129
  - Add a Wait node (e.g., 10s), then call "Get Upload Status" passing the `request_id`.
118
130
  - Loop with an IF node until the status is final (success/failed) or a max attempts limit is reached.
119
131
 
132
+ ### Duplicate posts and retries
133
+
134
+ Every upload sends an `Idempotency-Key`. The API collapses two uploads carrying the same key within a 24-hour window into a single post, so a retried upload does not publish twice.
135
+
136
+ The key is derived from the workflow execution, the node and the item index, so it stays the same when n8n's "Retry On Fail" re-runs the node, and changes when you genuinely start a new execution. Set the "Idempotency Key" field yourself only if you drive retries from outside n8n and need to control the key.
137
+
138
+ If you saw duplicate posts on an older version of this node, upgrade: versions before 0.1.51 sent no key at all, and a node-level retry after a network timeout would publish the post a second time.
139
+
120
140
  ### Platform-Specific Options
121
141
 
122
142
  #### Facebook
@@ -70,6 +70,15 @@ const ensureArrayFromCommaSeparated = (value) => {
70
70
  .map(item => item.trim())
71
71
  .filter(item => item.length > 0);
72
72
  };
73
+ const TERMINAL_UPLOAD_STATUSES = ['completed', 'failed', 'error', 'retryable', 'scheduled', 'not_found'];
74
+ const buildIdempotencyKey = (ctx) => {
75
+ var _a;
76
+ const override = String((_a = ctx.node.getNodeParameter('idempotencyKey', ctx.itemIndex, '')) !== null && _a !== void 0 ? _a : '').trim();
77
+ if (override) {
78
+ return override;
79
+ }
80
+ return `n8n:${ctx.node.getExecutionId()}:${ctx.node.getNode().id}:${ctx.itemIndex}`;
81
+ };
73
82
  const getBinaryFieldFromItem = async (ctx, propertyName, errorLabel) => {
74
83
  var _a, _b;
75
84
  const { node, itemIndex, items } = ctx;
@@ -93,9 +102,9 @@ const getBinaryFieldFromItem = async (ctx, propertyName, errorLabel) => {
93
102
  }
94
103
  };
95
104
  const PLATFORM_SUPPORT = {
96
- uploadPhotos: ['bluesky', 'facebook', 'instagram', 'linkedin', 'pinterest', 'threads', 'tiktok', 'x', 'reddit', 'google_business'],
97
- uploadVideo: ['bluesky', 'facebook', 'instagram', 'linkedin', 'pinterest', 'threads', 'tiktok', 'x', 'youtube', 'google_business'],
98
- uploadText: ['bluesky', 'facebook', 'linkedin', 'reddit', 'threads', 'x', 'google_business'],
105
+ uploadPhotos: ['bluesky', 'discord', 'facebook', 'instagram', 'linkedin', 'pinterest', 'telegram', 'threads', 'tiktok', 'x', 'reddit', 'google_business'],
106
+ uploadVideo: ['bluesky', 'discord', 'facebook', 'instagram', 'linkedin', 'pinterest', 'telegram', 'threads', 'tiktok', 'x', 'youtube', 'google_business'],
107
+ uploadText: ['bluesky', 'discord', 'facebook', 'linkedin', 'reddit', 'telegram', 'threads', 'x', 'google_business'],
99
108
  uploadDocument: ['linkedin'],
100
109
  };
101
110
  const DESCRIPTION_ENABLED_PLATFORMS = new Set(['linkedin', 'facebook', 'youtube', 'pinterest', 'tiktok']);
@@ -217,6 +226,10 @@ const prepareUploadBase = (ctx, operation) => {
217
226
  }
218
227
  const uploadAsync = ctx.node.getNodeParameter('uploadAsync', ctx.itemIndex);
219
228
  formData.async_upload = String(uploadAsync);
229
+ const autogenerate = ctx.node.getNodeParameter('autogenerate', ctx.itemIndex, false);
230
+ if (autogenerate) {
231
+ formData.autogenerate = 'true';
232
+ }
220
233
  const rawPlatforms = ctx.node.getNodeParameter('platform', ctx.itemIndex);
221
234
  const platforms = getFilteredPlatforms(operation, Array.isArray(rawPlatforms) ? rawPlatforms : []);
222
235
  formData['platform[]'] = platforms;
@@ -835,10 +848,12 @@ const buildMonitoringRequest = (ctx) => {
835
848
  case 'getAnalytics': {
836
849
  const profileUsername = ctx.node.getNodeParameter('analyticsProfileUsername', ctx.itemIndex);
837
850
  const analyticsPlatforms = ctx.node.getNodeParameter('analyticsPlatforms', ctx.itemIndex, []);
838
- const qs = {};
839
- if (Array.isArray(analyticsPlatforms) && analyticsPlatforms.length > 0) {
840
- qs.platforms = analyticsPlatforms.join(',');
851
+ if (!Array.isArray(analyticsPlatforms) || analyticsPlatforms.length === 0) {
852
+ throw new n8n_workflow_1.NodeOperationError(ctx.node.getNode(), 'Select at least one platform: the analytics API rejects a request without "platforms".', {
853
+ itemIndex: ctx.itemIndex,
854
+ });
841
855
  }
856
+ const qs = { platforms: analyticsPlatforms.join(',') };
842
857
  return {
843
858
  endpoint: `/analytics/${encodeURIComponent(profileUsername)}`,
844
859
  method: 'GET',
@@ -847,6 +862,77 @@ const buildMonitoringRequest = (ctx) => {
847
862
  waitForCompletion: false,
848
863
  };
849
864
  }
865
+ case 'getPostAnalytics': {
866
+ const requestId = ctx.node.getNodeParameter('postAnalyticsRequestId', ctx.itemIndex);
867
+ const platform = ctx.node.getNodeParameter('postAnalyticsPlatform', ctx.itemIndex, '');
868
+ const qs = {};
869
+ if (platform) {
870
+ qs.platform = platform;
871
+ }
872
+ return {
873
+ endpoint: `/uploadposts/post-analytics/${encodeURIComponent(requestId)}`,
874
+ method: 'GET',
875
+ qs,
876
+ isUploadOperation: false,
877
+ waitForCompletion: false,
878
+ };
879
+ }
880
+ case 'getPostAnalyticsByPlatformId': {
881
+ return {
882
+ endpoint: '/uploadposts/post-analytics',
883
+ method: 'GET',
884
+ qs: {
885
+ platform_post_id: ctx.node.getNodeParameter('platformPostId', ctx.itemIndex),
886
+ platform: ctx.node.getNodeParameter('nativePostPlatform', ctx.itemIndex),
887
+ user: ctx.node.getNodeParameter('nativePostProfileUsername', ctx.itemIndex),
888
+ },
889
+ isUploadOperation: false,
890
+ waitForCompletion: false,
891
+ };
892
+ }
893
+ case 'getPlatformMetrics': {
894
+ return {
895
+ endpoint: '/uploadposts/platform-metrics',
896
+ method: 'GET',
897
+ isUploadOperation: false,
898
+ waitForCompletion: false,
899
+ };
900
+ }
901
+ case 'getTotalImpressions': {
902
+ const profileUsername = ctx.node.getNodeParameter('impressionsProfileUsername', ctx.itemIndex);
903
+ const qs = {};
904
+ const period = ctx.node.getNodeParameter('impressionsPeriod', ctx.itemIndex, '');
905
+ const startDate = ctx.node.getNodeParameter('impressionsStartDate', ctx.itemIndex, '');
906
+ const endDate = ctx.node.getNodeParameter('impressionsEndDate', ctx.itemIndex, '');
907
+ const platforms = ctx.node.getNodeParameter('impressionsPlatforms', ctx.itemIndex, []);
908
+ const breakdown = ctx.node.getNodeParameter('impressionsBreakdown', ctx.itemIndex, false);
909
+ if (period)
910
+ qs.period = period;
911
+ if (startDate)
912
+ qs.start_date = startDate;
913
+ if (endDate)
914
+ qs.end_date = endDate;
915
+ if (Array.isArray(platforms) && platforms.length > 0)
916
+ qs.platform = platforms.join(',');
917
+ if (breakdown)
918
+ qs.breakdown = 'true';
919
+ return {
920
+ endpoint: `/uploadposts/total-impressions/${encodeURIComponent(profileUsername)}`,
921
+ method: 'GET',
922
+ qs,
923
+ isUploadOperation: false,
924
+ waitForCompletion: false,
925
+ };
926
+ }
927
+ case 'getRedditDetailedPosts': {
928
+ return {
929
+ endpoint: '/uploadposts/reddit/detailed-posts/',
930
+ method: 'GET',
931
+ qs: { profile_username: ctx.node.getNodeParameter('redditProfileUsername', ctx.itemIndex) },
932
+ isUploadOperation: false,
933
+ waitForCompletion: false,
934
+ };
935
+ }
850
936
  case 'listScheduled': {
851
937
  return {
852
938
  endpoint: '/uploadposts/schedule',
@@ -929,6 +1015,7 @@ const buildUserRequest = (ctx) => {
929
1015
  const readonlyCalendar = ctx.node.getNodeParameter('readonlyCalendar', ctx.itemIndex, false);
930
1016
  const connectTitle = ctx.node.getNodeParameter('connectTitle', ctx.itemIndex, '');
931
1017
  const connectDescription = ctx.node.getNodeParameter('connectDescription', ctx.itemIndex, '');
1018
+ const language = ctx.node.getNodeParameter('language', ctx.itemIndex, '');
932
1019
  const body = { username };
933
1020
  if (redirectUrl)
934
1021
  body.redirect_url = redirectUrl;
@@ -944,6 +1031,8 @@ const buildUserRequest = (ctx) => {
944
1031
  body.connect_title = connectTitle;
945
1032
  if (connectDescription)
946
1033
  body.connect_description = connectDescription;
1034
+ if (language)
1035
+ body.language = language;
947
1036
  return {
948
1037
  endpoint: '/uploadposts/users/generate-jwt',
949
1038
  method: 'POST',
@@ -1153,8 +1242,7 @@ const pollUploadStatus = async (node, requestId, pollInterval, pollTimeout) => {
1153
1242
  const statusData = await node.helpers.httpRequestWithAuthentication.call(node, 'uploadPostApi', statusOptions);
1154
1243
  finalData = statusData;
1155
1244
  const statusValue = (statusData && statusData.status);
1156
- if ((statusData && statusData.success === true) ||
1157
- (typeof statusValue === 'string' && ['success', 'completed', 'failed', 'error', 'scheduled', 'queued', 'pending'].includes(statusValue.toLowerCase()))) {
1245
+ if (typeof statusValue === 'string' && TERMINAL_UPLOAD_STATUSES.includes(statusValue.toLowerCase())) {
1158
1246
  break;
1159
1247
  }
1160
1248
  }
@@ -1203,9 +1291,9 @@ class UploadPost {
1203
1291
  noDataExpression: true,
1204
1292
  options: [
1205
1293
  { name: 'Upload Document', value: 'uploadDocument', action: 'Upload a document', description: 'Upload a document (PDF, PPT, PPTX, DOC, DOCX) as a native carousel/viewer (Supports: LinkedIn only)' },
1206
- { name: 'Upload Photo(s)', value: 'uploadPhotos', action: 'Upload photos', description: 'Upload one or more photos (Supports: TikTok, Instagram, LinkedIn, Facebook, X, Threads)' },
1207
- { name: 'Upload Text', value: 'uploadText', action: 'Upload a text post', description: 'Upload a text-based post (Supports: X, LinkedIn, Facebook, Threads)' },
1208
- { name: 'Upload Video', value: 'uploadVideo', action: 'Upload a video', description: 'Upload a single video (Supports: TikTok, Instagram, LinkedIn, YouTube, Facebook, X, Threads)' },
1294
+ { name: 'Upload Photo(s)', value: 'uploadPhotos', action: 'Upload photos', description: 'Upload one or more photos (Supports: TikTok, Instagram, LinkedIn, Facebook, X, Threads, Discord, Telegram)' },
1295
+ { name: 'Upload Text', value: 'uploadText', action: 'Upload a text post', description: 'Upload a text-based post (Supports: X, LinkedIn, Facebook, Threads, Discord, Telegram)' },
1296
+ { name: 'Upload Video', value: 'uploadVideo', action: 'Upload a video', description: 'Upload a single video (Supports: TikTok, Instagram, LinkedIn, YouTube, Facebook, X, Threads, Discord, Telegram)' },
1209
1297
  ],
1210
1298
  default: 'uploadPhotos',
1211
1299
  displayOptions: { show: { resource: ['uploads'] } },
@@ -1220,6 +1308,11 @@ class UploadPost {
1220
1308
  { name: 'Edit Scheduled Post', value: 'editScheduled', action: 'Edit scheduled post', description: 'Edit schedule details (like date/time) by job ID' },
1221
1309
  { name: 'Get Analytics', value: 'getAnalytics', action: 'Get analytics', description: 'Retrieve aggregated analytics for uploads' },
1222
1310
  { name: 'Get Job Status', value: 'getJobStatus', action: 'Get job status', description: 'Check the status of a scheduled or queued post using the job_id' },
1311
+ { name: 'Get Platform Metrics', value: 'getPlatformMetrics', action: 'Get platform metrics', description: 'List the analytics metrics available for each platform' },
1312
+ { name: 'Get Post Analytics', value: 'getPostAnalytics', action: 'Get post analytics', description: 'Retrieve per-post analytics for an upload using its request_id' },
1313
+ { 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' },
1314
+ { name: 'Get Reddit Detailed Posts', value: 'getRedditDetailedPosts', action: 'Get reddit detailed posts', description: 'Retrieve detailed Reddit posts with full media information' },
1315
+ { name: 'Get Total Impressions', value: 'getTotalImpressions', action: 'Get total impressions', description: 'Retrieve impressions aggregated across connected platforms for a date range' },
1223
1316
  { name: 'Get Upload History', value: 'getHistory', action: 'Get upload history', description: 'List past uploads with optional filters' },
1224
1317
  { name: 'Get Upload Status', value: 'getStatus', action: 'Get upload status', description: 'Check the status of an upload using the request_id' },
1225
1318
  { name: 'List Scheduled Posts', value: 'listScheduled', action: 'List scheduled posts', description: 'List your scheduled (future) posts' },
@@ -1670,13 +1763,37 @@ class UploadPost {
1670
1763
  name: 'uploadAsync',
1671
1764
  type: 'boolean',
1672
1765
  default: true,
1673
- description: 'Whether to process the upload asynchronously and return immediately. If you set to false but the upload takes longer than 59 seconds, it will automatically switch to asynchronous processing to avoid timeouts. In that case, use the request_id with the Upload Status endpoint to check the upload status and result.',
1766
+ description: 'Whether to process the upload asynchronously and return immediately. Recommended: leaving this off holds the HTTP connection open, and if it exceeds the server\'s synchronous wait window the upload switches to asynchronous processing anyway. In that case, use the request_id with the Upload Status endpoint to check the upload status and result.',
1674
1767
  displayOptions: {
1675
1768
  show: {
1676
1769
  operation: ['uploadPhotos', 'uploadVideo', 'uploadText', 'uploadDocument']
1677
1770
  }
1678
1771
  },
1679
1772
  },
1773
+ {
1774
+ displayName: 'Idempotency Key',
1775
+ name: 'idempotencyKey',
1776
+ type: 'string',
1777
+ default: '',
1778
+ description: 'Key the API uses to collapse duplicate uploads within 24 hours. Leave empty to derive one automatically from the execution, node and item, which is what prevents a retried upload from posting twice. Set it explicitly only when you drive retries yourself.',
1779
+ displayOptions: {
1780
+ show: {
1781
+ operation: ['uploadPhotos', 'uploadVideo', 'uploadText', 'uploadDocument']
1782
+ }
1783
+ },
1784
+ },
1785
+ {
1786
+ displayName: 'Autogenerate Copy With AI',
1787
+ name: 'autogenerate',
1788
+ type: 'boolean',
1789
+ default: false,
1790
+ description: 'Whether the server should use AI to generate native per-platform title/description from the media and fill any platform field left empty',
1791
+ displayOptions: {
1792
+ show: {
1793
+ operation: ['uploadPhotos', 'uploadVideo']
1794
+ }
1795
+ },
1796
+ },
1680
1797
  {
1681
1798
  displayName: 'Wait for Completion',
1682
1799
  name: 'waitForCompletion',
@@ -1800,19 +1917,171 @@ class UploadPost {
1800
1917
  displayOptions: { show: { operation: ["editScheduled"] } },
1801
1918
  },
1802
1919
  {
1803
- displayName: 'Platforms (Optional)',
1920
+ displayName: 'Platforms',
1804
1921
  name: 'analyticsPlatforms',
1805
1922
  type: 'multiOptions',
1923
+ required: true,
1806
1924
  options: [
1925
+ { name: 'Facebook', value: 'facebook' },
1807
1926
  { name: 'Instagram', value: 'instagram' },
1808
1927
  { name: 'LinkedIn', value: 'linkedin' },
1809
- { name: 'Facebook', value: 'facebook' },
1928
+ { name: 'Pinterest', value: 'pinterest' },
1929
+ { name: 'Reddit', value: 'reddit' },
1930
+ { name: 'Threads', value: 'threads' },
1931
+ { name: 'TikTok', value: 'tiktok' },
1810
1932
  { name: 'X (Twitter)', value: 'x' },
1933
+ { name: 'YouTube', value: 'youtube' },
1811
1934
  ],
1812
1935
  default: [],
1813
- description: 'Platforms to fetch analytics for (comma-joined in request)',
1936
+ description: 'Platforms to fetch analytics for. Analytics are not available for Bluesky, Discord, Google Business or Telegram.',
1814
1937
  displayOptions: { show: { operation: ['getAnalytics'] } },
1815
1938
  },
1939
+ {
1940
+ displayName: 'Request ID',
1941
+ name: 'postAnalyticsRequestId',
1942
+ type: 'string',
1943
+ required: true,
1944
+ default: '',
1945
+ description: 'The request_id returned by the upload you want metrics for',
1946
+ displayOptions: { show: { operation: ['getPostAnalytics'] } },
1947
+ },
1948
+ {
1949
+ displayName: 'Platform',
1950
+ name: 'postAnalyticsPlatform',
1951
+ type: 'options',
1952
+ options: [
1953
+ { name: 'All Platforms', value: '' },
1954
+ { name: 'Facebook', value: 'facebook' },
1955
+ { name: 'Instagram', value: 'instagram' },
1956
+ { name: 'LinkedIn', value: 'linkedin' },
1957
+ { name: 'Pinterest', value: 'pinterest' },
1958
+ { name: 'Reddit', value: 'reddit' },
1959
+ { name: 'Threads', value: 'threads' },
1960
+ { name: 'TikTok', value: 'tiktok' },
1961
+ { name: 'X (Twitter)', value: 'x' },
1962
+ { name: 'YouTube', value: 'youtube' },
1963
+ ],
1964
+ default: '',
1965
+ description: 'Restrict the result to a single platform',
1966
+ displayOptions: { show: { operation: ['getPostAnalytics'] } },
1967
+ },
1968
+ {
1969
+ displayName: 'Platform Post ID',
1970
+ name: 'platformPostId',
1971
+ type: 'string',
1972
+ required: true,
1973
+ default: '',
1974
+ description: 'The post ID as assigned by the social platform itself',
1975
+ displayOptions: { show: { operation: ['getPostAnalyticsByPlatformId'] } },
1976
+ },
1977
+ {
1978
+ displayName: 'Platform',
1979
+ name: 'nativePostPlatform',
1980
+ type: 'options',
1981
+ required: true,
1982
+ options: [
1983
+ { name: 'Facebook', value: 'facebook' },
1984
+ { name: 'Instagram', value: 'instagram' },
1985
+ { name: 'LinkedIn', value: 'linkedin' },
1986
+ { name: 'Pinterest', value: 'pinterest' },
1987
+ { name: 'Reddit', value: 'reddit' },
1988
+ { name: 'Threads', value: 'threads' },
1989
+ { name: 'TikTok', value: 'tiktok' },
1990
+ { name: 'X (Twitter)', value: 'x' },
1991
+ { name: 'YouTube', value: 'youtube' },
1992
+ ],
1993
+ default: 'instagram',
1994
+ description: 'Platform the post lives on',
1995
+ displayOptions: { show: { operation: ['getPostAnalyticsByPlatformId'] } },
1996
+ },
1997
+ {
1998
+ displayName: 'Profile Username',
1999
+ name: 'nativePostProfileUsername',
2000
+ type: 'string',
2001
+ required: true,
2002
+ default: '',
2003
+ description: 'Upload-Post profile that owns the connected account',
2004
+ displayOptions: { show: { operation: ['getPostAnalyticsByPlatformId'] } },
2005
+ },
2006
+ {
2007
+ displayName: 'Profile Username',
2008
+ name: 'impressionsProfileUsername',
2009
+ type: 'string',
2010
+ required: true,
2011
+ default: '',
2012
+ description: 'Profile username to aggregate impressions for',
2013
+ displayOptions: { show: { operation: ['getTotalImpressions'] } },
2014
+ },
2015
+ {
2016
+ displayName: 'Period',
2017
+ name: 'impressionsPeriod',
2018
+ type: 'options',
2019
+ options: [
2020
+ { name: 'Custom Range (Use Start/End Date)', value: '' },
2021
+ { name: 'Last 3 Months', value: 'last_3months' },
2022
+ { name: 'Last Day', value: 'last_day' },
2023
+ { name: 'Last Month', value: 'last_month' },
2024
+ { name: 'Last Week', value: 'last_week' },
2025
+ { name: 'Last Year', value: 'last_year' },
2026
+ ],
2027
+ default: '',
2028
+ description: 'Preset range. Leave on custom range to use Start and End Date instead. Defaults to the last 30 days when nothing is set.',
2029
+ displayOptions: { show: { operation: ['getTotalImpressions'] } },
2030
+ },
2031
+ {
2032
+ displayName: 'Start Date',
2033
+ name: 'impressionsStartDate',
2034
+ type: 'string',
2035
+ default: '',
2036
+ placeholder: 'YYYY-MM-DD',
2037
+ description: 'Start of a custom date range. Ignored when Period is set.',
2038
+ displayOptions: { show: { operation: ['getTotalImpressions'] } },
2039
+ },
2040
+ {
2041
+ displayName: 'End Date',
2042
+ name: 'impressionsEndDate',
2043
+ type: 'string',
2044
+ default: '',
2045
+ placeholder: 'YYYY-MM-DD',
2046
+ description: 'End of a custom date range. Ignored when Period is set.',
2047
+ displayOptions: { show: { operation: ['getTotalImpressions'] } },
2048
+ },
2049
+ {
2050
+ displayName: 'Platforms',
2051
+ name: 'impressionsPlatforms',
2052
+ type: 'multiOptions',
2053
+ options: [
2054
+ { name: 'Facebook', value: 'facebook' },
2055
+ { name: 'Instagram', value: 'instagram' },
2056
+ { name: 'LinkedIn', value: 'linkedin' },
2057
+ { name: 'Pinterest', value: 'pinterest' },
2058
+ { name: 'Reddit', value: 'reddit' },
2059
+ { name: 'Threads', value: 'threads' },
2060
+ { name: 'TikTok', value: 'tiktok' },
2061
+ { name: 'X (Twitter)', value: 'x' },
2062
+ { name: 'YouTube', value: 'youtube' },
2063
+ ],
2064
+ default: [],
2065
+ description: 'Restrict the aggregate to these platforms. Leave empty for all.',
2066
+ displayOptions: { show: { operation: ['getTotalImpressions'] } },
2067
+ },
2068
+ {
2069
+ displayName: 'Breakdown',
2070
+ name: 'impressionsBreakdown',
2071
+ type: 'boolean',
2072
+ default: false,
2073
+ description: 'Whether to return impressions broken down per platform instead of a single total',
2074
+ displayOptions: { show: { operation: ['getTotalImpressions'] } },
2075
+ },
2076
+ {
2077
+ displayName: 'Profile Username',
2078
+ name: 'redditProfileUsername',
2079
+ type: 'string',
2080
+ required: true,
2081
+ default: '',
2082
+ description: 'Profile username whose Reddit posts you want',
2083
+ displayOptions: { show: { operation: ['getRedditDetailedPosts'] } },
2084
+ },
1816
2085
  {
1817
2086
  displayName: 'New User Identifier',
1818
2087
  name: 'newUser',
@@ -1864,9 +2133,11 @@ class UploadPost {
1864
2133
  name: 'jwtPlatforms',
1865
2134
  type: 'multiOptions',
1866
2135
  options: [
2136
+ { name: 'Discord', value: 'discord' },
1867
2137
  { name: 'Facebook', value: 'facebook' },
1868
2138
  { name: 'Instagram', value: 'instagram' },
1869
2139
  { name: 'LinkedIn', value: 'linkedin' },
2140
+ { name: 'Telegram', value: 'telegram' },
1870
2141
  { name: 'Threads', value: 'threads' },
1871
2142
  { name: 'TikTok', value: 'tiktok' },
1872
2143
  { name: 'X (Twitter)', value: 'x' },
@@ -1908,6 +2179,22 @@ class UploadPost {
1908
2179
  description: 'Optional custom description for the connection page',
1909
2180
  displayOptions: { show: { operation: ['generateJwt'] } },
1910
2181
  },
2182
+ {
2183
+ displayName: 'Language',
2184
+ name: 'language',
2185
+ type: 'options',
2186
+ default: '',
2187
+ description: 'Force the connection page language for this profile. When set to Auto-Detect, the page detects the visitor browser language and falls back to English.',
2188
+ options: [
2189
+ { name: 'Auto-Detect (Default)', value: '' },
2190
+ { name: 'English', value: 'en' },
2191
+ { name: 'French', value: 'fr' },
2192
+ { name: 'German', value: 'de' },
2193
+ { name: 'Portuguese', value: 'pt' },
2194
+ { name: 'Spanish', value: 'es' },
2195
+ ],
2196
+ displayOptions: { show: { operation: ['generateJwt'] } },
2197
+ },
1911
2198
  {
1912
2199
  displayName: 'JWT',
1913
2200
  name: 'jwtToken',
@@ -3171,21 +3458,23 @@ class UploadPost {
3171
3458
  const operation = this.getCurrentNodeParameter('operation');
3172
3459
  const allPlatforms = [
3173
3460
  { name: 'Bluesky', value: 'bluesky' },
3461
+ { name: 'Discord', value: 'discord' },
3174
3462
  { name: 'Facebook', value: 'facebook' },
3175
3463
  { name: 'Google Business', value: 'google_business' },
3176
3464
  { name: 'Instagram', value: 'instagram' },
3177
3465
  { name: 'LinkedIn', value: 'linkedin' },
3178
3466
  { name: 'Pinterest', value: 'pinterest' },
3179
3467
  { name: 'Reddit', value: 'reddit' },
3468
+ { name: 'Telegram', value: 'telegram' },
3180
3469
  { name: 'Threads', value: 'threads' },
3181
3470
  { name: 'TikTok', value: 'tiktok' },
3182
3471
  { name: 'X (Twitter)', value: 'x' },
3183
3472
  { name: 'YouTube', value: 'youtube' },
3184
3473
  ];
3185
3474
  const platformSupport = {
3186
- uploadPhotos: ['bluesky', 'facebook', 'google_business', 'instagram', 'linkedin', 'pinterest', 'threads', 'tiktok', 'x', 'reddit'],
3187
- uploadVideo: ['bluesky', 'facebook', 'google_business', 'instagram', 'linkedin', 'pinterest', 'threads', 'tiktok', 'x', 'youtube'],
3188
- uploadText: ['bluesky', 'facebook', 'google_business', 'linkedin', 'reddit', 'threads', 'x'],
3475
+ uploadPhotos: ['bluesky', 'discord', 'facebook', 'google_business', 'instagram', 'linkedin', 'pinterest', 'telegram', 'threads', 'tiktok', 'x', 'reddit'],
3476
+ uploadVideo: ['bluesky', 'discord', 'facebook', 'google_business', 'instagram', 'linkedin', 'pinterest', 'telegram', 'threads', 'tiktok', 'x', 'youtube'],
3477
+ uploadText: ['bluesky', 'discord', 'facebook', 'google_business', 'linkedin', 'reddit', 'telegram', 'threads', 'x'],
3189
3478
  };
3190
3479
  const supportedPlatforms = platformSupport[operation] || [];
3191
3480
  const filtered = allPlatforms.filter(platform => supportedPlatforms.includes(platform.value));
@@ -3308,7 +3597,7 @@ class UploadPost {
3308
3597
  };
3309
3598
  }
3310
3599
  async execute() {
3311
- var _a, _b;
3600
+ var _a, _b, _c;
3312
3601
  const items = this.getInputData();
3313
3602
  const returnData = [];
3314
3603
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
@@ -3327,6 +3616,13 @@ class UploadPost {
3327
3616
  if (config.headers) {
3328
3617
  requestOptions.headers = config.headers;
3329
3618
  }
3619
+ if (config.isUploadOperation) {
3620
+ requestOptions.headers = {
3621
+ ...((_a = requestOptions.headers) !== null && _a !== void 0 ? _a : {}),
3622
+ 'Idempotency-Key': buildIdempotencyKey(ctx),
3623
+ 'X-Upload-Post-Source': 'n8n',
3624
+ };
3625
+ }
3330
3626
  if (config.qs) {
3331
3627
  requestOptions.qs = config.qs;
3332
3628
  }
@@ -3351,7 +3647,7 @@ class UploadPost {
3351
3647
  ? responseData.request_id
3352
3648
  : undefined;
3353
3649
  if (requestId) {
3354
- finalData = await pollUploadStatus(this, requestId, (_a = config.pollInterval) !== null && _a !== void 0 ? _a : 10, (_b = config.pollTimeout) !== null && _b !== void 0 ? _b : 600);
3650
+ finalData = await pollUploadStatus(this, requestId, (_b = config.pollInterval) !== null && _b !== void 0 ? _b : 10, (_c = config.pollTimeout) !== null && _c !== void 0 ? _c : 600);
3355
3651
  }
3356
3652
  }
3357
3653
  returnData.push({