n8n-nodes-pinterest 0.1.7 → 0.2.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.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Supports:
6
6
 
7
7
  - Cookie-based authentication (session cookie)
8
8
  - List boards (current user)
9
- - Create a Pin by uploading binary data or by letting Pinterest scrape a public image URL
9
+ - Create a Pin by letting Pinterest scrape a public image URL
10
10
 
11
11
  Notes:
12
12
 
@@ -37,16 +37,14 @@ Creates a pin using cookie-based authentication.
37
37
  Inputs:
38
38
 
39
39
  - Board (select from available boards)
40
- - Image Source:
41
- - **Upload Binary**: supply a binary property (e.g. `data`). The node uploads the file to Pinterest's S3 slot, builds the CDN URL from the returned `ETag`, and creates the pin with `method="uploaded"` (same as FS Poster).
42
- - **Scraped Image URL**: supply a direct image URL. The node calls the same `/pin/find/?url=...` flow as wp-pinterest-automatic and creates the pin with `method="scraped"`—no binary data required.
43
- - Optional: Title, Description, Link, Alt Text
40
+ - Image URL: Direct URL of the image for Pinterest to scrape
41
+ - Optional: Title, Description, Link
44
42
 
45
- Binary uploads follow the VIPResource S3 PinResource pipeline. Scraped mode just replays the legacy "pin finder" payload and works for images that are accessible over HTTPS.
43
+ The node calls the `/pin/find/?url=...` flow and creates the pin with `method="scraped"`. Works for images that are accessible over HTTPS.
46
44
 
47
45
  Additional behavior:
48
46
 
49
- - "Attach Link" (default true) still controls whether the destination link is sent. If Pinterest responds with errors such as "This site doesn't allow you to save Pins" or "Please make sure the URL is correct", the node automatically retries without the link unless Strict Plugin Mode is enabled.
47
+ - If Pinterest responds with errors such as "This site doesn't allow you to save Pins" or "Please make sure the URL is correct", the node automatically retries without the link.
50
48
 
51
49
  ## Disclaimer
52
50
 
@@ -6,18 +6,15 @@ function randomCsrf() {
6
6
  const seed = `${Date.now()}${Math.floor(Math.random() * 100000)}`;
7
7
  return Buffer.from(seed).toString('base64');
8
8
  }
9
- function buildCommonHeaders(csrf, strict = false) {
10
- const base = {
9
+ function buildCommonHeaders(csrf) {
10
+ return {
11
11
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0',
12
12
  'x-pinterest-pws-handler': 'www/[username].js',
13
13
  'x-CSRFToken': csrf,
14
+ 'Accept-Language': 'en-US,en;q=0.9',
15
+ 'Accept': 'application/json, text/plain, */*',
16
+ 'X-Requested-With': 'XMLHttpRequest',
14
17
  };
15
- if (!strict) {
16
- base['Accept-Language'] = 'en-US,en;q=0.9';
17
- base['Accept'] = 'application/json, text/plain, */*';
18
- base['X-Requested-With'] = 'XMLHttpRequest';
19
- }
20
- return base;
21
18
  }
22
19
  function buildCookieHeader(sess, csrf) {
23
20
  return `_pinterest_sess=${sess}; csrftoken=${csrf};`;
@@ -111,35 +108,14 @@ class PinterestCookie {
111
108
  required: true,
112
109
  description: 'Board to pin to',
113
110
  },
114
- {
115
- displayName: 'Image Source',
116
- name: 'imageSource',
117
- type: 'options',
118
- options: [
119
- { name: 'Upload Binary', value: 'uploadBinary' },
120
- { name: 'Scraped Image URL', value: 'scrapedUrl' },
121
- ],
122
- default: 'uploadBinary',
123
- displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
124
- description: 'Choose whether to upload a binary image or let Pinterest scrape an external URL',
125
- },
126
- {
127
- displayName: 'Binary Property',
128
- name: 'binaryProperty',
129
- type: 'string',
130
- default: 'data',
131
- required: true,
132
- displayOptions: { show: { resource: ['pin'], operation: ['create'], imageSource: ['uploadBinary'] } },
133
- description: 'Name of the binary property containing the image',
134
- },
135
111
  {
136
112
  displayName: 'Image URL',
137
113
  name: 'imageUrl',
138
114
  type: 'string',
139
115
  default: '',
140
116
  required: true,
141
- displayOptions: { show: { resource: ['pin'], operation: ['create'], imageSource: ['scrapedUrl'] } },
142
- description: 'Direct URL of the image when using the scraped method',
117
+ displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
118
+ description: 'Direct URL of the image for Pinterest to scrape',
143
119
  },
144
120
  {
145
121
  displayName: 'Title',
@@ -156,39 +132,12 @@ class PinterestCookie {
156
132
  displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
157
133
  },
158
134
  {
159
- displayName: 'Additional Fields',
160
- name: 'additionalFields',
161
- type: 'collection',
162
- placeholder: 'Add Field',
163
- default: {},
164
- displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
165
- options: [
166
- { displayName: 'Alt Text', name: 'alt_text', type: 'string', default: '' },
167
- { displayName: 'Link', name: 'link', type: 'string', default: '' },
168
- {
169
- displayName: 'Attach Link',
170
- name: 'attachLink',
171
- type: 'boolean',
172
- default: true,
173
- description: 'If disabled or link is blocked, pin will be created without a destination link',
174
- },
175
- ],
176
- },
177
- {
178
- displayName: 'Strict Plugin Mode',
179
- name: 'strictPluginMode',
180
- type: 'boolean',
181
- default: false,
182
- description: 'Force exact request shape and headers used by the referenced plugin. Disables retries/fallbacks for 1:1 parity while debugging.',
183
- displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
184
- },
185
- {
186
- displayName: 'Debug: Return Raw Response',
187
- name: 'debugRaw',
188
- type: 'boolean',
189
- default: false,
190
- description: 'When enabled, include the full server response body and request payload in the output for troubleshooting. The node will not throw if Continue On Fail is enabled.',
135
+ displayName: 'Link',
136
+ name: 'link',
137
+ type: 'string',
138
+ default: '',
191
139
  displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
140
+ description: 'Destination URL for the pin',
192
141
  },
193
142
  // Board ops
194
143
  {
@@ -279,7 +228,7 @@ class PinterestCookie {
279
228
  };
280
229
  }
281
230
  async execute() {
282
- var _a, _b, _c, _d;
231
+ var _a, _b;
283
232
  const items = this.getInputData();
284
233
  const returnData = [];
285
234
  const creds = (await this.getCredentials('pinterestCookieApi'));
@@ -343,13 +292,10 @@ class PinterestCookie {
343
292
  const operation = this.getNodeParameter('operation', i);
344
293
  if (operation === 'create') {
345
294
  const boardId = this.getNodeParameter('boardId', i);
346
- const imageSource = this.getNodeParameter('imageSource', i, 'uploadBinary');
295
+ const imageUrl = this.getNodeParameter('imageUrl', i);
347
296
  const title = this.getNodeParameter('title', i, '');
348
297
  const description = this.getNodeParameter('description', i, '');
349
- const additionalFields = this.getNodeParameter('additionalFields', i, {});
350
- const strictPluginMode = this.getNodeParameter('strictPluginMode', i, false);
351
- const debugRaw = this.getNodeParameter('debugRaw', i, false);
352
- const attachLink = (additionalFields.attachLink !== undefined) ? Boolean(additionalFields.attachLink) : true;
298
+ const link = this.getNodeParameter('link', i, '');
353
299
  const attemptCreate = async (payload, createOptions) => {
354
300
  var _a, _b, _c, _d, _e;
355
301
  const form = {
@@ -363,16 +309,14 @@ class PinterestCookie {
363
309
  method: 'POST',
364
310
  uri: `${base}/resource/PinResource/create/`,
365
311
  form,
366
- headers: strictPluginMode
367
- ? { ...buildCommonHeaders(csrf, true), Cookie: cookieHeader }
368
- : {
369
- ...baseHeaders,
370
- Cookie: cookieHeader,
371
- Referer: ((_b = createOptions === null || createOptions === void 0 ? void 0 : createOptions.sourceUrl) === null || _b === void 0 ? void 0 : _b.startsWith('/pin/find')) ? `${base}/pin/find/` : `${base}/pin-builder/`,
372
- Origin: base,
373
- 'X-Pinterest-Source-Url': (_c = createOptions === null || createOptions === void 0 ? void 0 : createOptions.sourceUrl) !== null && _c !== void 0 ? _c : '/pin-builder/',
374
- 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
375
- },
312
+ headers: {
313
+ ...baseHeaders,
314
+ Cookie: cookieHeader,
315
+ Referer: ((_b = createOptions === null || createOptions === void 0 ? void 0 : createOptions.sourceUrl) === null || _b === void 0 ? void 0 : _b.startsWith('/pin/find')) ? `${base}/pin/find/` : `${base}/pin-builder/`,
316
+ Origin: base,
317
+ 'X-Pinterest-Source-Url': (_c = createOptions === null || createOptions === void 0 ? void 0 : createOptions.sourceUrl) !== null && _c !== void 0 ? _c : '/pin-builder/',
318
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
319
+ },
376
320
  proxy: proxy || undefined,
377
321
  simple: false,
378
322
  resolveWithFullResponse: true,
@@ -398,171 +342,47 @@ class PinterestCookie {
398
342
  if (lastResult.ok) {
399
343
  return lastResult;
400
344
  }
401
- if (!strictPluginMode && isLinkError(lastResult.msg)) {
345
+ if (isLinkError(lastResult.msg)) {
402
346
  continue;
403
347
  }
404
348
  }
405
349
  return lastResult;
406
350
  };
407
351
  let result = { ok: false };
408
- if (imageSource === 'scrapedUrl') {
409
- const imageUrl = this.getNodeParameter('imageUrl', i, '');
410
- if (!imageUrl) {
411
- throw new Error('Image URL is required when using the scraped method.');
412
- }
413
- const sanitizedTitle = sanitizePinterestText(title, 100);
414
- const sanitizedDesc = sanitizePinterestText(description, 500);
415
- const linkValue = attachLink ? String((_c = additionalFields.link) !== null && _c !== void 0 ? _c : '') : '';
416
- const scrapedPayload = {
417
- options: {
418
- method: 'scraped',
419
- title: sanitizedTitle,
420
- description: sanitizedDesc,
421
- link: linkValue,
422
- image_url: imageUrl,
423
- share_facebook: false,
424
- board_id: boardId,
425
- scrape_metric: { source: 'www_url_scrape' },
426
- },
427
- context: {},
428
- };
429
- const scrapedOptions = {
430
- sourceUrl: `/pin/find/?url=${doubleEncodeUrl(imageUrl)}`,
431
- modulePath: 'App()%3EModalManager%3EModal%3EPinCreate%3EBoardPicker%3ESelectList(view_type%3DpinCreate%2C+selected_section_index%3Dundefined%2C+selected_item_index%3Dundefined%2C+highlight_matched_text%3Dtrue%2C+suppress_hover_events%3Dundefined%2C+scroll_selected_item_into_view%3Dtrue%2C+select_first_item_after_update%3Dfalse%2C+item_module%3D%5Bobject+Object%5D)',
432
- };
433
- const strategies = [
434
- { payload: scrapedPayload, options: scrapedOptions },
435
- ];
436
- if (!strictPluginMode && linkValue) {
437
- const withoutLink = JSON.parse(JSON.stringify(scrapedPayload));
438
- if (withoutLink.options) {
439
- withoutLink.options.link = '';
440
- }
441
- strategies.push({ payload: withoutLink, options: scrapedOptions });
442
- }
443
- result = await runStrategies(strategies);
352
+ if (!imageUrl) {
353
+ throw new Error('Image URL is required.');
444
354
  }
445
- else {
446
- const binaryProperty = this.getNodeParameter('binaryProperty', i);
447
- const binary = (_d = items[i].binary) === null || _d === void 0 ? void 0 : _d[binaryProperty];
448
- if (!binary)
449
- throw new Error(`Binary property "${binaryProperty}" is missing on item ${i}`);
450
- const vipResp = (await this.helpers.request({
451
- method: 'POST',
452
- uri: `${base}/resource/VIPResource/create/`,
453
- form: {
454
- source_url: '/pin-builder/',
455
- data: '{"options":{"type":"pinimage"},"context":{}}',
456
- },
457
- headers: strictPluginMode
458
- ? { ...buildCommonHeaders(csrf, true), Cookie: cookieHeader }
459
- : {
460
- ...baseHeaders,
461
- Cookie: cookieHeader,
462
- Referer: `${base}/pin-builder/`,
463
- Origin: base,
464
- 'X-Pinterest-Source-Url': '/pin-builder/',
465
- 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
466
- },
467
- proxy: proxy || undefined,
468
- }));
469
- const vip = JSON.parse(vipResp);
470
- const res = vip.resource_response || {};
471
- const data = res.data || {};
472
- const uploadUrl = String(data.upload_url || '');
473
- const uploadParams = data.upload_parameters || {};
474
- if (!uploadUrl)
475
- throw new Error('Failed to get upload URL');
476
- const buffer = Buffer.from(binary.data, 'base64');
477
- const contentType = binary.mimeType || 'image/jpeg';
478
- const formData = {};
479
- for (const [k, v] of Object.entries(uploadParams))
480
- formData[k] = String(v);
481
- formData['file'] = {
482
- value: buffer,
483
- options: {
484
- filename: 'blob',
485
- contentType,
486
- },
487
- };
488
- const uploadResp = (await this.helpers.request({
489
- method: 'POST',
490
- uri: uploadUrl,
491
- headers: {
492
- Accept: '*/*',
493
- 'Accept-Encoding': 'gzip',
494
- 'User-Agent': strictPluginMode
495
- ? 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'
496
- : baseHeaders['User-Agent'],
497
- Origin: strictPluginMode ? 'https://www.pinterest.com' : base,
498
- Referer: strictPluginMode ? 'https://www.pinterest.com' : `${base}/pin-builder/`,
499
- ...(strictPluginMode
500
- ? {
501
- 'sec-ch-ua': '".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"',
502
- 'sec-ch-ua-mobile': '?0',
503
- 'sec-ch-ua-full': '?1',
504
- 'sec-ch-ua-platform': '"Windows"',
505
- 'Sec-Fetch-Dest': 'empty',
506
- 'Sec-Fetch-Mode': 'cors',
507
- 'Sec-Fetch-Site': 'same-origin',
508
- Connection: 'keep-alive',
509
- }
510
- : {}),
511
- },
512
- formData,
513
- proxy: proxy || undefined,
514
- resolveWithFullResponse: true,
515
- simple: false,
516
- }));
517
- const etagRaw = uploadResp.headers['etag'] || uploadResp.headers['ETag'] || '';
518
- const etag = String(etagRaw).replace(/"/g, '');
519
- if (!etag)
520
- throw new Error('Upload failed: missing ETag');
521
- const imageUrl = `https://i.pinimg.com/736x/${etag[0]}${etag[1]}/${etag[2]}${etag[3]}/${etag[4]}${etag[5]}/${etag}.jpg`;
522
- const sendData = {
523
- options: {
524
- board_id: String(boardId),
525
- field_set_key: 'create_success',
526
- skip_pin_create_log: true,
527
- description: String(description || ''),
528
- alt_text: String(additionalFields.alt_text || ''),
529
- link: attachLink && additionalFields.link ? String(additionalFields.link) : '',
530
- title: String(title || ''),
531
- image_url: String(imageUrl),
532
- method: 'uploaded',
533
- upload_metric: { source: 'pinner_upload_standalone' },
534
- user_mention_tags: [],
535
- no_fetch_context_on_resource: false,
536
- },
537
- context: {},
538
- };
539
- const strategies = [
540
- { payload: JSON.parse(JSON.stringify(sendData)) },
541
- ];
542
- if (!strictPluginMode) {
543
- const withoutLink = JSON.parse(JSON.stringify(sendData));
544
- if (withoutLink.options)
545
- delete withoutLink.options.link;
546
- strategies.push({ payload: withoutLink });
547
- strategies.push({
548
- payload: {
549
- options: {
550
- board_id: boardId,
551
- field_set_key: 'create_success',
552
- image_url: imageUrl,
553
- method: 'uploaded',
554
- },
555
- context: {},
556
- },
557
- });
355
+ const sanitizedTitle = sanitizePinterestText(title, 100);
356
+ const sanitizedDesc = sanitizePinterestText(description, 500);
357
+ const scrapedPayload = {
358
+ options: {
359
+ method: 'scraped',
360
+ title: sanitizedTitle,
361
+ description: sanitizedDesc,
362
+ link: link,
363
+ image_url: imageUrl,
364
+ share_facebook: false,
365
+ board_id: boardId,
366
+ scrape_metric: { source: 'www_url_scrape' },
367
+ },
368
+ context: {},
369
+ };
370
+ const scrapedOptions = {
371
+ sourceUrl: `/pin/find/?url=${doubleEncodeUrl(imageUrl)}`,
372
+ modulePath: 'App()%3EModalManager%3EModal%3EPinCreate%3EBoardPicker%3ESelectList(view_type%3DpinCreate%2C+selected_section_index%3Dundefined%2C+selected_item_index%3Dundefined%2C+highlight_matched_text%3Dtrue%2C+suppress_hover_events%3Dundefined%2C+scroll_selected_item_into_view%3Dtrue%2C+select_first_item_after_update%3Dfalse%2C+item_module%3D%5Bobject+Object%5D)',
373
+ };
374
+ const strategies = [
375
+ { payload: scrapedPayload, options: scrapedOptions },
376
+ ];
377
+ if (link) {
378
+ const withoutLink = JSON.parse(JSON.stringify(scrapedPayload));
379
+ if (withoutLink.options) {
380
+ withoutLink.options.link = '';
558
381
  }
559
- result = await runStrategies(strategies);
382
+ strategies.push({ payload: withoutLink, options: scrapedOptions });
560
383
  }
384
+ result = await runStrategies(strategies);
561
385
  if (!result.ok) {
562
- if (debugRaw && this.continueOnFail()) {
563
- returnData.push({ json: { error: result.msg || 'Create failed', response: result.body, request: result.payload } });
564
- continue;
565
- }
566
386
  throw new Error(result.msg || result.body || 'Create failed');
567
387
  }
568
388
  returnData.push({ json: { id: result.id, link: `https://www.pinterest.com/pin/${result.id}` } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-pinterest",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "n8n community nodes for Pinterest v5 API (list boards, create pins)",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",