n8n-nodes-pinterest 0.1.7 → 0.2.1

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,84 +108,40 @@ 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',
146
122
  name: 'title',
147
123
  type: 'string',
148
124
  default: '',
125
+ typeOptions: { maxLength: 100 },
149
126
  displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
127
+ description: 'Maximum 100 characters',
150
128
  },
151
129
  {
152
130
  displayName: 'Description',
153
131
  name: 'description',
154
132
  type: 'string',
155
133
  default: '',
134
+ typeOptions: { maxLength: 800 },
156
135
  displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
136
+ description: 'Maximum 800 characters',
157
137
  },
158
138
  {
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.',
139
+ displayName: 'Link',
140
+ name: 'link',
141
+ type: 'string',
142
+ default: '',
191
143
  displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
144
+ description: 'Destination URL for the pin',
192
145
  },
193
146
  // Board ops
194
147
  {
@@ -279,7 +232,7 @@ class PinterestCookie {
279
232
  };
280
233
  }
281
234
  async execute() {
282
- var _a, _b, _c, _d;
235
+ var _a, _b;
283
236
  const items = this.getInputData();
284
237
  const returnData = [];
285
238
  const creds = (await this.getCredentials('pinterestCookieApi'));
@@ -343,13 +296,10 @@ class PinterestCookie {
343
296
  const operation = this.getNodeParameter('operation', i);
344
297
  if (operation === 'create') {
345
298
  const boardId = this.getNodeParameter('boardId', i);
346
- const imageSource = this.getNodeParameter('imageSource', i, 'uploadBinary');
299
+ const imageUrl = this.getNodeParameter('imageUrl', i);
347
300
  const title = this.getNodeParameter('title', i, '');
348
301
  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;
302
+ const link = this.getNodeParameter('link', i, '');
353
303
  const attemptCreate = async (payload, createOptions) => {
354
304
  var _a, _b, _c, _d, _e;
355
305
  const form = {
@@ -363,16 +313,14 @@ class PinterestCookie {
363
313
  method: 'POST',
364
314
  uri: `${base}/resource/PinResource/create/`,
365
315
  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
- },
316
+ headers: {
317
+ ...baseHeaders,
318
+ Cookie: cookieHeader,
319
+ 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/`,
320
+ Origin: base,
321
+ 'X-Pinterest-Source-Url': (_c = createOptions === null || createOptions === void 0 ? void 0 : createOptions.sourceUrl) !== null && _c !== void 0 ? _c : '/pin-builder/',
322
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
323
+ },
376
324
  proxy: proxy || undefined,
377
325
  simple: false,
378
326
  resolveWithFullResponse: true,
@@ -398,171 +346,47 @@ class PinterestCookie {
398
346
  if (lastResult.ok) {
399
347
  return lastResult;
400
348
  }
401
- if (!strictPluginMode && isLinkError(lastResult.msg)) {
349
+ if (isLinkError(lastResult.msg)) {
402
350
  continue;
403
351
  }
404
352
  }
405
353
  return lastResult;
406
354
  };
407
355
  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);
356
+ if (!imageUrl) {
357
+ throw new Error('Image URL is required.');
444
358
  }
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
- });
359
+ const sanitizedTitle = sanitizePinterestText(title, 100);
360
+ const sanitizedDesc = sanitizePinterestText(description, 500);
361
+ const scrapedPayload = {
362
+ options: {
363
+ method: 'scraped',
364
+ title: sanitizedTitle,
365
+ description: sanitizedDesc,
366
+ link: link,
367
+ image_url: imageUrl,
368
+ share_facebook: false,
369
+ board_id: boardId,
370
+ scrape_metric: { source: 'www_url_scrape' },
371
+ },
372
+ context: {},
373
+ };
374
+ const scrapedOptions = {
375
+ sourceUrl: `/pin/find/?url=${doubleEncodeUrl(imageUrl)}`,
376
+ 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)',
377
+ };
378
+ const strategies = [
379
+ { payload: scrapedPayload, options: scrapedOptions },
380
+ ];
381
+ if (link) {
382
+ const withoutLink = JSON.parse(JSON.stringify(scrapedPayload));
383
+ if (withoutLink.options) {
384
+ withoutLink.options.link = '';
558
385
  }
559
- result = await runStrategies(strategies);
386
+ strategies.push({ payload: withoutLink, options: scrapedOptions });
560
387
  }
388
+ result = await runStrategies(strategies);
561
389
  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
390
  throw new Error(result.msg || result.body || 'Create failed');
567
391
  }
568
392
  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.1",
4
4
  "description": "n8n community nodes for Pinterest v5 API (list boards, create pins)",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",