n8n-nodes-postwire 0.2.3 → 0.3.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.
@@ -2,24 +2,22 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PostWire = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
- const BASE = 'https://postwire.io';
6
- // Every network PostWire can publish to. `mediaNeed` mirrors the server's own rule so the node can
7
- // warn in the editor rather than letting the call fail — TikTok and YouTube refuse a post with no
8
- // video, and Instagram refuses one with no media at all.
9
- const PLATFORMS = [
10
- { name: 'TikTok', value: 'tiktok', mediaNeed: 'video' },
11
- { name: 'Instagram', value: 'instagram', mediaNeed: 'any' },
12
- { name: 'YouTube', value: 'youtube', mediaNeed: 'video' },
13
- { name: 'LinkedIn', value: 'linkedin', mediaNeed: null },
14
- { name: 'X (Twitter)', value: 'x', mediaNeed: null },
15
- { name: 'Facebook', value: 'facebook', mediaNeed: null },
16
- { name: 'Reddit', value: 'reddit', mediaNeed: null },
17
- { name: 'Bluesky', value: 'bluesky', mediaNeed: null },
18
- { name: 'Mastodon', value: 'mastodon', mediaNeed: null },
19
- { name: 'Telegram', value: 'telegram', mediaNeed: null },
20
- { name: 'Discord', value: 'discord', mediaNeed: null },
21
- ];
22
- const needOf = (value) => PLATFORMS.find((p) => p.value === value)?.mediaNeed ?? null;
5
+ const Description_1 = require("./Description");
6
+ const GenericFunctions_1 = require("./GenericFunctions");
7
+ const LegacyDescription_1 = require("./LegacyDescription");
8
+ const LEGACY = {
9
+ me: { resource: 'account', operation: 'get' },
10
+ publish: { resource: 'post', operation: 'publish', contentMode: 'text' },
11
+ generateAndPublish: { resource: 'post', operation: 'publish', contentMode: 'smart' },
12
+ generate: { resource: 'post', operation: 'generate' },
13
+ schedule: { resource: 'post', operation: 'schedule', contentMode: 'text' },
14
+ };
15
+ function locatorValue(v) {
16
+ var _a;
17
+ if (v && typeof v === 'object')
18
+ return String((_a = v.value) !== null && _a !== void 0 ? _a : '').trim();
19
+ return String(v !== null && v !== void 0 ? v : '').trim();
20
+ }
23
21
  class PostWire {
24
22
  constructor() {
25
23
  this.description = {
@@ -27,310 +25,445 @@ class PostWire {
27
25
  name: 'postWire',
28
26
  icon: { light: 'file:postwire.svg', dark: 'file:postwire.dark.svg' },
29
27
  group: ['output'],
30
- version: 1,
31
- subtitle: '={{$parameter["operation"]}}',
32
- description: 'Publish one idea natively to every social network',
33
- // An agent asking to post is exactly the use this node was built for.
28
+ version: [1, 2],
29
+ defaultVersion: 2,
30
+ subtitle: '={{$parameter["operation"] + ($parameter["resource"] ? ": " + $parameter["resource"] : "")}}',
31
+ description: 'Publish one idea natively to TikTok, Instagram, YouTube, LinkedIn, X, Bluesky and more — a different post written for each network',
34
32
  usableAsTool: true,
35
33
  defaults: { name: 'PostWire' },
36
34
  inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
37
35
  outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
38
36
  credentials: [{ name: 'postWireApi', required: true }],
39
- requestDefaults: {
40
- baseURL: BASE,
41
- headers: { 'Content-Type': 'application/json' },
42
- },
43
37
  properties: [
44
- {
45
- displayName: 'Operation',
46
- name: 'operation',
47
- type: 'options',
48
- noDataExpression: true,
49
- options: [
50
- {
51
- name: 'Get Account',
52
- value: 'me',
53
- description: 'Plan, usage this month and connected accounts',
54
- action: 'Get account',
55
- },
56
- {
57
- name: 'Publish',
58
- value: 'publish',
59
- description: 'Publish to one or more networks in a single call',
60
- action: 'Publish to social networks',
61
- },
62
- {
63
- name: 'Schedule',
64
- value: 'schedule',
65
- description: 'Queue a post for a later time',
66
- action: 'Schedule a post',
67
- },
68
- {
69
- name: 'Write a Post per Network',
70
- value: 'generate',
71
- description: 'One prompt in, a native draft out for each network — right length, hashtags and format per platform. Does not publish.',
72
- action: 'Write a native post for each network',
73
- },
74
- {
75
- name: 'Write and Publish',
76
- value: 'generateAndPublish',
77
- description: 'Write a native draft per network, then publish them',
78
- action: 'Write and publish in one step',
79
- },
80
- ],
81
- default: 'generateAndPublish',
82
- },
83
- {
84
- displayName: 'Networks',
85
- name: 'platforms',
86
- type: 'multiOptions',
87
- options: PLATFORMS.map((p) => ({ name: p.name, value: p.value })),
88
- default: ['x', 'linkedin'],
89
- required: true,
90
- description: 'Connect these in the PostWire dashboard first. TikTok, Instagram and YouTube are one OAuth click — PostWire carries the platform approvals, so there is no app review on your side.',
91
- displayOptions: {
92
- show: { operation: ['generate', 'publish', 'generateAndPublish', 'schedule'] },
93
- },
94
- },
95
- {
96
- displayName: 'Idea',
97
- name: 'prompt',
98
- type: 'string',
99
- typeOptions: { rows: 3 },
100
- default: '',
101
- required: true,
102
- placeholder: 'We shipped scheduling today — one idea, a native post per network',
103
- description: 'What you want to say. More detail gives a better draft.',
104
- displayOptions: { show: { operation: ['generate', 'generateAndPublish'] } },
105
- },
106
- {
107
- displayName: 'Text',
108
- name: 'text',
109
- type: 'string',
110
- typeOptions: { rows: 3 },
111
- default: '',
112
- description: 'Posted exactly as written to every selected network',
113
- displayOptions: { show: { operation: ['publish', 'schedule'] } },
114
- },
115
- {
116
- displayName: 'Media URL',
117
- name: 'mediaUrl',
118
- type: 'string',
119
- default: '',
120
- placeholder: 'https://example.com/clip.mp4',
121
- description: 'A direct link to an .mp4 or an image. TikTok and YouTube will not accept a post without a video, and Instagram needs a photo or a video. A Google Drive share link does not work — it returns a web page, not a file.',
122
- displayOptions: {
123
- show: { operation: ['generate', 'publish', 'generateAndPublish', 'schedule'] },
124
- },
125
- },
126
- {
127
- displayName: 'Publish At',
128
- name: 'runAt',
129
- type: 'dateTime',
130
- default: '',
131
- required: true,
132
- description: 'When to publish. PostWire validates the post now, so a missing video is caught here rather than at 7am tomorrow.',
133
- displayOptions: { show: { operation: ['schedule'] } },
134
- },
135
- {
136
- displayName: 'Options',
137
- name: 'options',
138
- type: 'collection',
139
- placeholder: 'Add option',
140
- default: {},
141
- displayOptions: {
142
- show: { operation: ['generate', 'publish', 'generateAndPublish', 'schedule'] },
143
- },
144
- options: [
145
- {
146
- displayName: 'Media Type',
147
- name: 'mediaType',
148
- type: 'options',
149
- default: 'auto',
150
- options: [
151
- { name: 'Detect From the URL', value: 'auto' },
152
- { name: 'Video', value: 'video' },
153
- { name: 'Image', value: 'image' },
154
- ],
155
- description: 'Only needed when the link has no file extension — a signed CDN URL or a share link. Detection reads the extension and, when there is none, follows what the chosen networks require.',
156
- },
157
- {
158
- displayName: 'Brand',
159
- name: 'brandId',
160
- type: 'string',
161
- default: '',
162
- description: 'Publish as a specific brand. Leave empty for your only brand.',
163
- },
164
- {
165
- displayName: 'Brand Voice',
166
- name: 'brandVoice',
167
- type: 'string',
168
- default: '',
169
- description: 'A sentence describing how you want to sound, applied to every generated draft',
170
- },
171
- {
172
- displayName: 'Title (YouTube)',
173
- name: 'title',
174
- type: 'string',
175
- default: '',
176
- description: 'Overrides the generated YouTube title',
177
- },
178
- {
179
- displayName: 'Visibility',
180
- name: 'privacy',
181
- type: 'options',
182
- options: [
183
- { name: 'Private', value: 'private' },
184
- { name: 'Public', value: 'public' },
185
- { name: 'Unlisted', value: 'unlisted' },
186
- ],
187
- default: 'public',
188
- description: 'Used by TikTok and YouTube',
189
- },
190
- ],
191
- },
38
+ ...LegacyDescription_1.legacyProperties,
39
+ Description_1.resourceProperty,
40
+ ...Description_1.operationProperties,
41
+ ...Description_1.postProperties,
42
+ ...Description_1.scheduledPostProperties,
43
+ ...Description_1.brandProperties,
44
+ ...Description_1.connectionProperties,
45
+ ...Description_1.accountProperties,
46
+ ...Description_1.mediaProperties,
192
47
  ],
193
48
  };
49
+ this.methods = {
50
+ listSearch: {
51
+ async searchBrands(filter) {
52
+ const res = await GenericFunctions_1.apiRequest.call(this, 'GET', '/api/brands');
53
+ const brands = res.brands || [];
54
+ const f = (filter || '').toLowerCase();
55
+ return {
56
+ results: brands
57
+ .filter((b) => !f || String(b.name).toLowerCase().includes(f))
58
+ .map((b) => {
59
+ var _a;
60
+ return ({
61
+ name: `${b.name}${((_a = b.platforms) === null || _a === void 0 ? void 0 : _a.length) ? ` (${b.platforms.map(GenericFunctions_1.nameOf).join(', ')})` : ''}`,
62
+ value: b.id,
63
+ });
64
+ }),
65
+ };
66
+ },
67
+ },
68
+ };
194
69
  }
195
70
  async execute() {
71
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
196
72
  const items = this.getInputData();
197
73
  const out = [];
74
+ const version = this.getNode().typeVersion;
198
75
  for (let i = 0; i < items.length; i++) {
199
76
  try {
200
- const operation = this.getNodeParameter('operation', i);
201
- if (operation === 'me') {
202
- const me = (await this.helpers.httpRequestWithAuthentication.call(this, 'postWireApi', {
203
- method: 'GET',
204
- url: `${BASE}/api/me`,
205
- json: true,
206
- }));
207
- out.push({ json: me, pairedItem: { item: i } });
77
+ let resource;
78
+ let operation;
79
+ let legacyMode;
80
+ if (version === 1) {
81
+ const legacy = LEGACY[this.getNodeParameter('operation', i)];
82
+ if (!legacy)
83
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Unknown operation', { itemIndex: i });
84
+ ({ resource, operation } = legacy);
85
+ legacyMode = legacy.contentMode;
86
+ }
87
+ else {
88
+ resource = this.getNodeParameter('resource', i);
89
+ operation = this.getNodeParameter('operation', i);
90
+ }
91
+ const push = (json) => out.push({ json, pairedItem: { item: i } });
92
+ if (resource === 'account') {
93
+ const me = await GenericFunctions_1.apiRequest.call(this, 'GET', '/api/me', undefined, undefined, i);
94
+ const simplify = version === 1 ? false : this.getNodeParameter('simplify', i, true);
95
+ if (!simplify) {
96
+ push(me);
97
+ continue;
98
+ }
99
+ const usage = me.usage || {};
100
+ const brands = me.brands || {};
101
+ push({
102
+ email: me.email,
103
+ plan: me.plan,
104
+ posts_used: usage.posts,
105
+ posts_limit: usage.limit,
106
+ brands_used: brands.used,
107
+ brands_limit: brands.limit,
108
+ connected: (me.connections || []).map((c) => c.platform),
109
+ verified: me.verified,
110
+ account_id: me.account_id,
111
+ });
112
+ continue;
113
+ }
114
+ if (resource === 'media') {
115
+ const field = this.getNodeParameter('binaryPropertyName', i);
116
+ push(await GenericFunctions_1.uploadBinary.call(this, i, field));
117
+ continue;
118
+ }
119
+ if (resource === 'brand') {
120
+ if (operation === 'getAll') {
121
+ const res = await GenericFunctions_1.apiRequest.call(this, 'GET', '/api/brands', undefined, undefined, i);
122
+ let list = res.brands || [];
123
+ if (!this.getNodeParameter('returnAll', i))
124
+ list = list.slice(0, this.getNodeParameter('limit', i));
125
+ for (const b of list)
126
+ push(b);
127
+ continue;
128
+ }
129
+ if (operation === 'create') {
130
+ const res = await GenericFunctions_1.apiRequest.call(this, 'POST', '/api/brands', {
131
+ name: this.getNodeParameter('name', i),
132
+ }, undefined, i);
133
+ push(res.brand || res);
134
+ continue;
135
+ }
136
+ const id = locatorValue(this.getNodeParameter('brand', i));
137
+ if (operation === 'update') {
138
+ const res = await GenericFunctions_1.apiRequest.call(this, 'PATCH', `/api/brands/${encodeURIComponent(id)}`, {
139
+ name: this.getNodeParameter('name', i),
140
+ }, undefined, i);
141
+ push(res.brand || res);
142
+ continue;
143
+ }
144
+ if (operation === 'delete') {
145
+ await GenericFunctions_1.apiRequest.call(this, 'DELETE', `/api/brands/${encodeURIComponent(id)}`, undefined, undefined, i);
146
+ push({ deleted: true, id });
147
+ continue;
148
+ }
149
+ }
150
+ if (resource === 'connection') {
151
+ if (operation === 'getAll') {
152
+ const me = await GenericFunctions_1.apiRequest.call(this, 'GET', '/api/me', undefined, undefined, i);
153
+ const brandName = new Map((me.brand_list || []).map((b) => [b.id, b.name]));
154
+ let list = (me.connections || []).map((c) => {
155
+ var _a;
156
+ return ({
157
+ ...c,
158
+ network: (0, GenericFunctions_1.nameOf)(c.platform),
159
+ brand_name: (_a = brandName.get(c.brand_id)) !== null && _a !== void 0 ? _a : null,
160
+ });
161
+ });
162
+ if (!this.getNodeParameter('returnAll', i))
163
+ list = list.slice(0, this.getNodeParameter('limit', i));
164
+ for (const c of list)
165
+ push(c);
166
+ continue;
167
+ }
168
+ if (operation === 'checkHealth') {
169
+ const platform = this.getNodeParameter('platform', i);
170
+ push(await GenericFunctions_1.apiRequest.call(this, 'GET', `/api/connect/${platform}/health`, undefined, undefined, i));
171
+ continue;
172
+ }
173
+ if (operation === 'createConnectLink') {
174
+ const platform = this.getNodeParameter('platform', i, '');
175
+ push(await GenericFunctions_1.apiRequest.call(this, 'POST', '/api/connect-link', platform ? { platform } : {}, undefined, i));
176
+ continue;
177
+ }
178
+ }
179
+ if (resource === 'scheduledPost') {
180
+ if (operation === 'getAll') {
181
+ const filters = this.getNodeParameter('filters', i, {});
182
+ const qs = {};
183
+ if (filters.from)
184
+ qs.from = new Date(filters.from).toISOString();
185
+ if (filters.to)
186
+ qs.to = new Date(filters.to).toISOString();
187
+ const res = await GenericFunctions_1.apiRequest.call(this, 'GET', '/api/schedule', undefined, qs, i);
188
+ let list = res.scheduled || [];
189
+ if (filters.status)
190
+ list = list.filter((r) => r.status === filters.status);
191
+ if (!this.getNodeParameter('returnAll', i))
192
+ list = list.slice(0, this.getNodeParameter('limit', i));
193
+ for (const r of list)
194
+ push(r);
195
+ continue;
196
+ }
197
+ const id = encodeURIComponent(this.getNodeParameter('scheduledPostId', i));
198
+ if (operation === 'delete') {
199
+ await GenericFunctions_1.apiRequest.call(this, 'DELETE', `/api/schedule/${id}`, undefined, undefined, i);
200
+ push({ deleted: true, id: decodeURIComponent(id) });
201
+ continue;
202
+ }
203
+ if (operation === 'update') {
204
+ const fields = this.getNodeParameter('updateFields', i, {});
205
+ const body = {};
206
+ if (fields.runAt)
207
+ body.run_at = new Date(fields.runAt).toISOString();
208
+ if (fields.label !== undefined)
209
+ body.label = fields.label;
210
+ if (!Object.keys(body).length)
211
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Nothing to update', {
212
+ itemIndex: i,
213
+ description: "Add 'Publish At' or 'Label' under 'Update Fields'.",
214
+ });
215
+ const res = await GenericFunctions_1.apiRequest.call(this, 'PATCH', `/api/schedule/${id}`, body, undefined, i);
216
+ push(res.scheduled || res);
217
+ continue;
218
+ }
219
+ }
220
+ if (resource !== 'post')
221
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Unknown operation', { itemIndex: i });
222
+ if (operation === 'getStatus') {
223
+ const platform = this.getNodeParameter('statusPlatform', i);
224
+ const id = String((_a = this.getNodeParameter('postId', i)) !== null && _a !== void 0 ? _a : '').trim();
225
+ push(await GenericFunctions_1.apiRequest.call(this, 'GET', '/api/post/status', undefined, { platform, id }, i));
208
226
  continue;
209
227
  }
210
- const platforms = this.getNodeParameter('platforms', i);
228
+ const platforms = (this.getNodeParameter('platforms', i, []) || []).filter(Boolean);
211
229
  const opts = this.getNodeParameter('options', i, {});
212
- const mediaUrl = (this.getNodeParameter('mediaUrl', i, '') || '').trim();
213
- if (!platforms?.length) {
214
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Pick at least one network.', {
230
+ if (!platforms.length)
231
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), "No network selected in 'Networks'", {
215
232
  itemIndex: i,
233
+ description: 'Pick at least one network. They must be connected in the PostWire dashboard.',
216
234
  });
235
+ if (operation === 'planWeek') {
236
+ const video = platforms.filter((p) => (0, GenericFunctions_1.needOf)(p));
237
+ if (video.length)
238
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${video.map(GenericFunctions_1.nameOf).join(', ')} cannot be in a week plan`, {
239
+ itemIndex: i,
240
+ description: 'A week plan is text only. Schedule video posts one by one with Post → Schedule.',
241
+ });
242
+ const tz = (opts.timezone || this.getTimezone() || 'UTC').trim();
243
+ let offset;
244
+ try {
245
+ offset = (0, GenericFunctions_1.tzOffsetMinutes)(tz);
246
+ }
247
+ catch {
248
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `'Timezone' is not a known time zone: "${tz}"`, {
249
+ itemIndex: i,
250
+ description: 'Use an IANA name such as America/New_York or Europe/Madrid.',
251
+ });
252
+ }
253
+ const res = await GenericFunctions_1.apiRequest.call(this, 'POST', '/api/week', {
254
+ topic: this.getNodeParameter('topic', i),
255
+ platforms,
256
+ days: this.getNodeParameter('days', i, 5),
257
+ start_hour: this.getNodeParameter('hour', i, 10),
258
+ tz_offset_minutes: offset,
259
+ brand_voice: opts.brandVoice || undefined,
260
+ }, undefined, i);
261
+ push({ ...res, timezone: tz });
262
+ continue;
217
263
  }
218
- // Media rules, checked before the request so the message names the fix rather than the symptom.
219
- //
220
- // The classifier used to answer only video-or-photo, and called everything without a
221
- // known video extension a photo. A signed CDN link, one ending in #t=30, or a share URL
222
- // with no extension at all was therefore "a photo", and the check below then threw and
223
- // stopped the whole workflow — for a video that would have published perfectly well.
224
- // Three answers now, and an unknown one is never grounds for refusing to try: the
225
- // network is the thing that gets to decide, not a regular expression over a filename.
226
- const declared = opts.mediaType || 'auto';
227
- const kind = !mediaUrl
228
- ? 'none'
229
- : declared === 'video' || declared === 'image'
230
- ? declared
231
- : /\.(mp4|mov|webm|m3u8|avi|mkv|m4v)(\?|#|$)/i.test(mediaUrl)
232
- ? 'video'
233
- : /\.(jpe?g|png|gif|webp|heic|heif|avif|bmp|tiff?)(\?|#|$)/i.test(mediaUrl)
234
- ? 'image'
235
- : 'unknown';
236
- const blocked = platforms.filter((p) => {
237
- const need = needOf(p);
238
- if (!need)
239
- return false;
240
- if (need === 'video')
241
- return kind === 'image' || kind === 'none';
242
- return kind === 'none';
243
- });
264
+ const mediaSource = version === 1
265
+ ? (this.getNodeParameter('mediaUrl', i, '') ? 'url' : 'none')
266
+ : this.getNodeParameter('mediaSource', i, 'none');
267
+ let mediaUrl = '';
268
+ let declared = opts.mediaType || 'auto';
269
+ if (mediaSource === 'url')
270
+ mediaUrl = String(this.getNodeParameter('mediaUrl', i, '') || '').trim();
271
+ if (mediaSource === 'binary') {
272
+ const field = this.getNodeParameter('binaryPropertyName', i);
273
+ const hosted = await GenericFunctions_1.uploadBinary.call(this, i, field);
274
+ mediaUrl = hosted.media_url;
275
+ const ct = String(hosted.content_type || this.helpers.assertBinaryData(i, field).mimeType || '');
276
+ if (declared === 'auto')
277
+ declared = ct.startsWith('video/') ? 'video' : ct.startsWith('image/') ? 'image' : 'auto';
278
+ }
279
+ if (operation === 'generate') {
280
+ const prompt = this.getNodeParameter('prompt', i);
281
+ const { drafts, missing } = await GenericFunctions_1.generateDrafts.call(this, i, {
282
+ prompt,
283
+ platforms,
284
+ media_url: mediaUrl || undefined,
285
+ brand_voice: opts.brandVoice || undefined,
286
+ });
287
+ push({
288
+ drafts,
289
+ ...(missing.length ? { missing } : {}),
290
+ platforms,
291
+ media_url: mediaUrl || undefined,
292
+ preview: (0, GenericFunctions_1.draftsPreview)(drafts),
293
+ });
294
+ continue;
295
+ }
296
+ const contentMode = legacyMode !== null && legacyMode !== void 0 ? legacyMode : this.getNodeParameter('contentMode', i, 'smart');
297
+ const kind = (0, GenericFunctions_1.classifyMedia)(mediaUrl, declared);
298
+ const blocked = (0, GenericFunctions_1.mediaBlocks)(platforms, kind);
244
299
  if (blocked.length) {
245
- const wantsVideo = blocked.some((p) => needOf(p) === 'video');
246
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${blocked.join(' and ')} will not accept a post without ${wantsVideo ? 'a video' : 'an image or video'}. Set Media URL to a direct file link, or remove ${blocked.length > 1 ? 'those networks' : 'that network'} from the list.`, { itemIndex: i });
300
+ const wantsVideo = blocked.some((p) => (0, GenericFunctions_1.needOf)(p) === 'video');
301
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${blocked.map(GenericFunctions_1.nameOf).join(' and ')} will not accept a post without ${wantsVideo ? 'a video' : 'an image or video'}`, {
302
+ itemIndex: i,
303
+ description: `Set 'Media' to a video URL or to "Binary File", or remove ${blocked.length > 1 ? 'those networks' : 'that network'} from 'Networks'.`,
304
+ });
305
+ }
306
+ if (mediaSource === 'url' && mediaUrl && !opts.skipMediaCheck) {
307
+ const why = await (0, GenericFunctions_1.checkMediaUrl)(this, mediaUrl, platforms);
308
+ if (why)
309
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'The media link cannot be published', {
310
+ itemIndex: i,
311
+ description: why,
312
+ });
247
313
  }
248
- // An unrecognised link still has to go in one of the two fields the API has. Send it as the
249
- // one the chosen networks need — that is the only reading of it that can succeed.
250
- const treatAsVideo = kind === 'video' || (kind === 'unknown' && platforms.some((p) => needOf(p) === 'video'));
251
- const videoUrl = mediaUrl && treatAsVideo ? mediaUrl : undefined;
252
- const photoUrl = mediaUrl && !treatAsVideo ? mediaUrl : undefined;
314
+ const treatAsVideo = kind === 'video' || (kind === 'unknown' && platforms.some((p) => (0, GenericFunctions_1.needOf)(p) === 'video'));
253
315
  let perPlatform;
254
- if (operation === 'generate' || operation === 'generateAndPublish') {
255
- const prompt = this.getNodeParameter('prompt', i);
256
- const gen = (await this.helpers.httpRequestWithAuthentication.call(this, 'postWireApi', {
257
- method: 'POST',
258
- url: `${BASE}/api/generate`,
259
- body: {
260
- prompt,
261
- platforms,
262
- media_url: mediaUrl || undefined,
263
- brand_voice: opts.brandVoice || undefined,
264
- },
265
- json: true,
266
- }));
316
+ let text;
317
+ if (contentMode === 'smart') {
318
+ const gen = await GenericFunctions_1.generateDrafts.call(this, i, {
319
+ prompt: this.getNodeParameter('prompt', i),
320
+ platforms,
321
+ media_url: mediaUrl || undefined,
322
+ brand_voice: opts.brandVoice || undefined,
323
+ });
267
324
  perPlatform = gen.drafts;
268
- if (operation === 'generate') {
269
- out.push({ json: gen, pairedItem: { item: i } });
270
- continue;
325
+ }
326
+ else if (contentMode === 'drafts') {
327
+ const raw = this.getNodeParameter('drafts', i);
328
+ let parsed = raw;
329
+ if (typeof raw === 'string') {
330
+ try {
331
+ parsed = JSON.parse(raw);
332
+ }
333
+ catch {
334
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), "'Drafts' is not valid JSON", {
335
+ itemIndex: i,
336
+ description: 'Map the output of Write Drafts with {{ $json.drafts }}, or write an object like { "x": { "text": "…" } }.',
337
+ });
338
+ }
271
339
  }
340
+ const obj = (parsed && typeof parsed === 'object' ? parsed : {});
341
+ perPlatform = (obj.drafts && typeof obj.drafts === 'object' ? obj.drafts : obj);
342
+ const missingDraft = platforms.filter((p) => { var _a; return !((_a = perPlatform === null || perPlatform === void 0 ? void 0 : perPlatform[p]) === null || _a === void 0 ? void 0 : _a.text); });
343
+ if (missingDraft.length && !mediaUrl)
344
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No draft for ${missingDraft.map(GenericFunctions_1.nameOf).join(', ')}`, {
345
+ itemIndex: i,
346
+ description: `'Drafts' needs a { "text": … } entry for every network in 'Networks'. Remove those networks or write drafts for them.`,
347
+ });
348
+ }
349
+ else {
350
+ text = this.getNodeParameter('text', i, '');
272
351
  }
352
+ const brandId = locatorValue((_b = opts.brand) !== null && _b !== void 0 ? _b : opts.brandId);
273
353
  const body = {
274
354
  platforms,
275
355
  per_platform: perPlatform,
276
- text: perPlatform ? undefined : this.getNodeParameter('text', i, ''),
277
- photo_url: photoUrl,
278
- video_url: videoUrl,
356
+ text: perPlatform ? undefined : text,
357
+ photo_url: mediaUrl && !treatAsVideo ? mediaUrl : undefined,
358
+ video_url: mediaUrl && treatAsVideo ? mediaUrl : undefined,
279
359
  title: opts.title || undefined,
280
360
  privacy: opts.privacy || undefined,
281
- brand_id: opts.brandId || undefined,
361
+ subreddit: opts.subreddit || undefined,
362
+ brand_id: brandId || undefined,
282
363
  };
283
364
  if (operation === 'schedule') {
284
365
  const runAt = this.getNodeParameter('runAt', i);
285
- const scheduled = (await this.helpers.httpRequestWithAuthentication.call(this, 'postWireApi', {
286
- method: 'POST',
287
- url: `${BASE}/api/schedule`,
288
- body: { ...body, run_at: runAt },
289
- json: true,
290
- }));
291
- out.push({ json: scheduled, pairedItem: { item: i } });
366
+ if (!runAt)
367
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), "'Publish At' is empty", {
368
+ itemIndex: i,
369
+ description: 'Pick a date and time, or map one with an expression such as {{ $json.publish_at }}.',
370
+ });
371
+ const scheduled = await GenericFunctions_1.apiRequest.call(this, 'POST', '/api/schedule', {
372
+ ...body,
373
+ run_at: new Date(runAt).toISOString(),
374
+ label: opts.label || undefined,
375
+ }, undefined, i);
376
+ push({ ...(scheduled.scheduled || scheduled), drafts: perPlatform });
292
377
  continue;
293
378
  }
294
- const response = (await this.helpers.httpRequestWithAuthentication.call(this, 'postWireApi', {
295
- method: 'POST',
296
- url: `${BASE}/api/post`,
297
- body,
298
- json: true,
299
- }));
300
- // One n8n item per network, so a Filter or IF node downstream can act on the failures
301
- // alone instead of unpacking an array by hand.
379
+ if (opts.dryRun) {
380
+ const me = await GenericFunctions_1.apiRequest.call(this, 'GET', '/api/me', undefined, undefined, i);
381
+ const conns = (me.connections || []).filter((c) => !brandId || c.brand_id === brandId);
382
+ for (const p of platforms) {
383
+ const draft = (perPlatform === null || perPlatform === void 0 ? void 0 : perPlatform[p]) || {};
384
+ const t = String((_d = (_c = draft.text) !== null && _c !== void 0 ? _c : text) !== null && _d !== void 0 ? _d : '');
385
+ const issues = [];
386
+ if (!conns.some((c) => c.platform === p))
387
+ issues.push(`${(0, GenericFunctions_1.nameOf)(p)} is not connected${brandId ? ' to this brand' : ''}`);
388
+ const max = (0, GenericFunctions_1.maxOf)(p);
389
+ if (max && t.length > max)
390
+ issues.push(perPlatform
391
+ ? `${t.length} characters, over the ${max} limit`
392
+ : `${t.length} characters; PostWire will shorten it to ${max}`);
393
+ if (!t.trim() && !mediaUrl)
394
+ issues.push('no text');
395
+ push({
396
+ dryRun: true,
397
+ platform: p,
398
+ ready: !issues.some((s) => !s.includes('will shorten')),
399
+ text: t,
400
+ title: (_e = draft.title) !== null && _e !== void 0 ? _e : body.title,
401
+ tags: draft.tags,
402
+ media_url: mediaUrl || undefined,
403
+ issues,
404
+ });
405
+ }
406
+ continue;
407
+ }
408
+ let idem = String(opts.idempotencyKey || '').trim();
409
+ if (!idem && contentMode === 'smart') {
410
+ const exec = (_f = this.getExecutionId) === null || _f === void 0 ? void 0 : _f.call(this);
411
+ if (exec)
412
+ idem = `n8n:${exec}:${this.getNode().name}:${i}`;
413
+ }
414
+ if (idem)
415
+ body.idempotency_key = idem.slice(0, 200);
416
+ const response = await GenericFunctions_1.apiRequest.call(this, 'POST', '/api/post', body, undefined, i);
302
417
  const results = (response.results || [response]).filter(Boolean);
303
418
  for (const res of results) {
304
- out.push({ json: res, pairedItem: { item: i } });
419
+ const extra = {};
420
+ if (res.ok === false) {
421
+ const hint = (0, GenericFunctions_1.hintFor)(res.platform, String(res.error || ''), res.code);
422
+ if (hint)
423
+ extra.hint = hint;
424
+ }
425
+ if (response.notice)
426
+ extra.notice = response.notice;
427
+ push({ ...res, ...extra });
305
428
  }
306
- // A 200 whose every network failed used to leave the node green with the error buried
307
- // in the output, so a scheduled flow could publish nothing for weeks and never say so.
308
- // If not one network went out, that is a failed execution.
309
- const failed = results.filter((res) => res?.ok === false);
429
+ const failed = results.filter((res) => (res === null || res === void 0 ? void 0 : res.ok) === false);
310
430
  if (results.length && failed.length === results.length) {
311
431
  const why = failed
312
- .map((f) => `${f.platform || '?'}: ${f.error || f.code || 'failed'}`)
432
+ .map((f) => `${(0, GenericFunctions_1.nameOf)(f.platform || '?')}: ${f.error || f.code || 'failed'}`)
313
433
  .join('; ');
314
- const limited = failed.find((f) => f.code === 'brand_limit_reached' || f.upgrade_url);
315
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `PostWire published to none of the ${results.length} selected network(s). ${why}` +
316
- (limited?.upgrade_url ? ` — upgrade: ${limited.upgrade_url}` : ''), { itemIndex: i });
434
+ const first = failed[0];
435
+ const fix = (0, GenericFunctions_1.hintFor)(first.platform, String(first.error || ''), first.code) ||
436
+ (0, GenericFunctions_1.describeFailure)(first.code, first).description;
437
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `PostWire published to none of the ${results.length} selected network(s)`, { itemIndex: i, description: `${why}${fix ? ` — ${fix}` : ''}` });
317
438
  }
318
439
  }
319
440
  catch (error) {
320
441
  if (this.continueOnFail()) {
321
- out.push({ json: { error: error.message }, pairedItem: { item: i } });
442
+ out.push({
443
+ json: {
444
+ error: error.message,
445
+ description: (_g = error.description) !== null && _g !== void 0 ? _g : undefined,
446
+ },
447
+ pairedItem: { item: i },
448
+ });
322
449
  continue;
323
450
  }
324
- // Our own checks already throw NodeOperationError with a message that names the fix;
325
- // re-wrapping those would bury it. Anything else is an HTTP failure, and n8n renders
326
- // a NodeApiError with the node's context instead of a bare fetch stack.
327
- if (error instanceof n8n_workflow_1.NodeOperationError) {
328
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, { itemIndex: i });
329
- }
330
- throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { itemIndex: i });
451
+ const e = error;
452
+ if (error instanceof n8n_workflow_1.NodeApiError)
453
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), {}, {
454
+ message: e.message,
455
+ description: (_h = e.description) !== null && _h !== void 0 ? _h : undefined,
456
+ httpCode: (_j = e.httpCode) !== null && _j !== void 0 ? _j : undefined,
457
+ itemIndex: i,
458
+ });
459
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
460
+ itemIndex: i,
461
+ description: (_k = error.description) !== null && _k !== void 0 ? _k : undefined,
462
+ });
331
463
  }
332
464
  }
333
465
  return [out];
334
466
  }
335
467
  }
336
468
  exports.PostWire = PostWire;
469
+ //# sourceMappingURL=PostWire.node.js.map