n8n-nodes-pinterest 0.1.5 → 0.1.7

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 with a binary image input
9
+ - Create a Pin by uploading binary data or by letting Pinterest scrape a public image URL
10
10
 
11
11
  Notes:
12
12
 
@@ -37,14 +37,16 @@ Creates a pin using cookie-based authentication.
37
37
  Inputs:
38
38
 
39
39
  - Board (select from available boards)
40
- - Binary Property (e.g. `data`)
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.
41
43
  - Optional: Title, Description, Link, Alt Text
42
44
 
43
- Binary image comes from the input item's binary property. The node uploads the image to the `upload_url` from VIPResource, derives the final `image_url` from the `ETag`, then creates the Pin via `PinResource/create/`.
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.
44
46
 
45
47
  Additional behavior:
46
48
 
47
- - "Attach Link" (default true) inside Additional Fields controls whether the destination link is sent. If Pinterest returns an error like "This site doesn't allow you to save Pins.", the node automatically retries without the link.
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.
48
50
 
49
51
  ## Disclaimer
50
52
 
@@ -23,6 +23,21 @@ function buildCookieHeader(sess, csrf) {
23
23
  return `_pinterest_sess=${sess}; csrftoken=${csrf};`;
24
24
  }
25
25
  const PINTEREST_BASE = 'https://www.pinterest.com';
26
+ function sanitizePinterestText(value, limit) {
27
+ if (!value)
28
+ return '';
29
+ let sanitized = value.replace(/&/g, '&').replace(/&/g, '&');
30
+ sanitized = sanitized.replace(/\r\n|\r|\n/g, '\n');
31
+ sanitized = sanitized.replace(/\t/g, ' ').trim();
32
+ if (sanitized.length > limit) {
33
+ sanitized = sanitized.slice(0, limit - 3) + '...';
34
+ }
35
+ sanitized = sanitized.replace(/"/g, '\"');
36
+ return sanitized;
37
+ }
38
+ function doubleEncodeUrl(value) {
39
+ return encodeURIComponent(encodeURIComponent(value));
40
+ }
26
41
  async function resolveBaseUrl(ctx, headers, proxy) {
27
42
  try {
28
43
  const resp = (await ctx.helpers.request({
@@ -96,15 +111,36 @@ class PinterestCookie {
96
111
  required: true,
97
112
  description: 'Board to pin to',
98
113
  },
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
+ },
99
126
  {
100
127
  displayName: 'Binary Property',
101
128
  name: 'binaryProperty',
102
129
  type: 'string',
103
130
  default: 'data',
104
131
  required: true,
105
- displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
132
+ displayOptions: { show: { resource: ['pin'], operation: ['create'], imageSource: ['uploadBinary'] } },
106
133
  description: 'Name of the binary property containing the image',
107
134
  },
135
+ {
136
+ displayName: 'Image URL',
137
+ name: 'imageUrl',
138
+ type: 'string',
139
+ default: '',
140
+ required: true,
141
+ displayOptions: { show: { resource: ['pin'], operation: ['create'], imageSource: ['scrapedUrl'] } },
142
+ description: 'Direct URL of the image when using the scraped method',
143
+ },
108
144
  {
109
145
  displayName: 'Title',
110
146
  name: 'title',
@@ -243,7 +279,7 @@ class PinterestCookie {
243
279
  };
244
280
  }
245
281
  async execute() {
246
- var _a, _b, _c;
282
+ var _a, _b, _c, _d;
247
283
  const items = this.getInputData();
248
284
  const returnData = [];
249
285
  const creds = (await this.getCredentials('pinterestCookieApi'));
@@ -307,135 +343,37 @@ class PinterestCookie {
307
343
  const operation = this.getNodeParameter('operation', i);
308
344
  if (operation === 'create') {
309
345
  const boardId = this.getNodeParameter('boardId', i);
310
- const binaryProperty = this.getNodeParameter('binaryProperty', i);
346
+ const imageSource = this.getNodeParameter('imageSource', i, 'uploadBinary');
311
347
  const title = this.getNodeParameter('title', i, '');
312
348
  const description = this.getNodeParameter('description', i, '');
313
349
  const additionalFields = this.getNodeParameter('additionalFields', i, {});
314
- const debugRaw = this.getNodeParameter('debugRaw', i, false);
315
350
  const strictPluginMode = this.getNodeParameter('strictPluginMode', i, false);
316
- const binary = (_c = items[i].binary) === null || _c === void 0 ? void 0 : _c[binaryProperty];
317
- if (!binary)
318
- throw new Error(`Binary property "${binaryProperty}" is missing on item ${i}`);
319
- // Step 1: request upload slot
320
- const vipResp = (await this.helpers.request({
321
- method: 'POST',
322
- uri: `${base}/resource/VIPResource/create/`,
323
- form: {
324
- source_url: '/pin-builder/',
325
- data: '{"options":{"type":"pinimage"},"context":{}}',
326
- },
327
- headers: strictPluginMode
328
- ? { ...buildCommonHeaders(csrf, true), Cookie: cookieHeader }
329
- : {
330
- ...baseHeaders,
331
- Cookie: cookieHeader,
332
- Referer: `${base}/pin-builder/`,
333
- Origin: base,
334
- 'X-Pinterest-Source-Url': '/pin-builder/',
335
- 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
336
- },
337
- proxy: proxy || undefined,
338
- }));
339
- const vip = JSON.parse(vipResp);
340
- const res = vip.resource_response || {};
341
- const data = res.data || {};
342
- const uploadUrl = String(data.upload_url || '');
343
- const uploadParams = data.upload_parameters || {};
344
- if (!uploadUrl)
345
- throw new Error('Failed to get upload URL');
346
- // Step 2: upload image
347
- const buffer = Buffer.from(binary.data, 'base64');
348
- const contentType = binary.mimeType || 'image/jpeg';
349
- const formData = {};
350
- for (const [k, v] of Object.entries(uploadParams))
351
- formData[k] = String(v);
352
- formData['file'] = {
353
- value: buffer,
354
- options: {
355
- filename: 'blob',
356
- contentType,
357
- },
358
- };
359
- const uploadResp = (await this.helpers.request({
360
- method: 'POST',
361
- uri: uploadUrl,
362
- headers: {
363
- Accept: '*/*',
364
- 'Accept-Encoding': 'gzip',
365
- 'User-Agent': strictPluginMode
366
- ? 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'
367
- : baseHeaders['User-Agent'],
368
- Origin: strictPluginMode ? 'https://www.pinterest.com' : base,
369
- Referer: strictPluginMode ? 'https://www.pinterest.com' : `${base}/pin-builder/`,
370
- ...(strictPluginMode
371
- ? {
372
- 'sec-ch-ua': '".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"',
373
- 'sec-ch-ua-mobile': '?0',
374
- 'sec-ch-ua-full': '?1',
375
- 'sec-ch-ua-platform': '"Windows"',
376
- 'Sec-Fetch-Dest': 'empty',
377
- 'Sec-Fetch-Mode': 'cors',
378
- 'Sec-Fetch-Site': 'same-origin',
379
- Connection: 'keep-alive',
380
- }
381
- : {}),
382
- },
383
- formData,
384
- proxy: proxy || undefined,
385
- // get full response to read headers
386
- resolveWithFullResponse: true,
387
- simple: false,
388
- }));
389
- const etagRaw = uploadResp.headers['etag'] || uploadResp.headers['ETag'] || '';
390
- const etag = String(etagRaw).replace(/"/g, '');
391
- if (!etag)
392
- throw new Error('Upload failed: missing ETag');
393
- // compute imageUrl like plugin
394
- const imageUrl = `https://i.pinimg.com/736x/${etag[0]}${etag[1]}/${etag[2]}${etag[3]}/${etag[4]}${etag[5]}/${etag}.jpg`;
395
- // Step 3: create pin
351
+ const debugRaw = this.getNodeParameter('debugRaw', i, false);
396
352
  const attachLink = (additionalFields.attachLink !== undefined) ? Boolean(additionalFields.attachLink) : true;
397
- // Build payload
398
- const sendData = {
399
- options: {
400
- board_id: String(boardId),
401
- field_set_key: 'create_success',
402
- skip_pin_create_log: true,
403
- description: String(description || ''),
404
- alt_text: String(additionalFields.alt_text || ''),
405
- // Strict mode: always include link key
406
- link: String(additionalFields.link || ''),
407
- // Non-strict: allow disabling link entirely
408
- ...(strictPluginMode ? {} : (!attachLink ? { link: '' } : {})),
409
- title: String(title || ''),
410
- image_url: String(imageUrl),
411
- method: 'uploaded',
412
- upload_metric: { source: 'pinner_upload_standalone' },
413
- user_mention_tags: [],
414
- no_fetch_context_on_resource: false,
415
- },
416
- context: {},
417
- };
418
- const attemptCreate = async (payload) => {
419
- var _a, _b;
353
+ const attemptCreate = async (payload, createOptions) => {
354
+ var _a, _b, _c, _d, _e;
355
+ const form = {
356
+ source_url: (_a = createOptions === null || createOptions === void 0 ? void 0 : createOptions.sourceUrl) !== null && _a !== void 0 ? _a : '/pin-builder/',
357
+ data: JSON.stringify(payload),
358
+ };
359
+ if (createOptions === null || createOptions === void 0 ? void 0 : createOptions.modulePath) {
360
+ form.module_path = createOptions.modulePath;
361
+ }
420
362
  const createResp = (await this.helpers.request({
421
363
  method: 'POST',
422
364
  uri: `${base}/resource/PinResource/create/`,
423
- form: {
424
- source_url: '/pin-builder/',
425
- data: JSON.stringify(payload),
426
- },
365
+ form,
427
366
  headers: strictPluginMode
428
367
  ? { ...buildCommonHeaders(csrf, true), Cookie: cookieHeader }
429
368
  : {
430
369
  ...baseHeaders,
431
370
  Cookie: cookieHeader,
432
- Referer: `${base}/pin-builder/`,
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/`,
433
372
  Origin: base,
434
- 'X-Pinterest-Source-Url': '/pin-builder/',
373
+ 'X-Pinterest-Source-Url': (_c = createOptions === null || createOptions === void 0 ? void 0 : createOptions.sourceUrl) !== null && _c !== void 0 ? _c : '/pin-builder/',
435
374
  'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
436
375
  },
437
376
  proxy: proxy || undefined,
438
- // Get body even on 4xx
439
377
  simple: false,
440
378
  resolveWithFullResponse: true,
441
379
  }));
@@ -448,52 +386,184 @@ class PinterestCookie {
448
386
  const createdRes = created.resource_response || {};
449
387
  const pinData = createdRes.data || {};
450
388
  const id = String(pinData.id || '');
451
- const msg = createdRes.message || ((_a = createdRes.error) === null || _a === void 0 ? void 0 : _a.message) || '';
452
- const msgDetail = ((_b = createdRes.error) === null || _b === void 0 ? void 0 : _b.message_detail) || '';
389
+ const msg = createdRes.message || ((_d = createdRes.error) === null || _d === void 0 ? void 0 : _d.message) || '';
390
+ const msgDetail = ((_e = createdRes.error) === null || _e === void 0 ? void 0 : _e.message_detail) || '';
453
391
  return { ok: Boolean(id), id, msg: String(msg || msgDetail || ''), body, payload };
454
392
  };
455
- let result = { ok: false };
456
- if (strictPluginMode) {
457
- result = await attemptCreate(sendData);
458
- if (!result.ok) {
459
- if (debugRaw && this.continueOnFail()) {
460
- returnData.push({ json: { error: result.msg || 'Create failed', response: result.body, request: sendData } });
393
+ const isLinkError = (msg) => /doesn['’]?t allow you to save Pins|does not allow you to save Pins|please make sure the url is correct/i.test(msg || '');
394
+ const runStrategies = async (strategies) => {
395
+ let lastResult = { ok: false };
396
+ for (const strat of strategies) {
397
+ lastResult = await attemptCreate(strat.payload, strat.options);
398
+ if (lastResult.ok) {
399
+ return lastResult;
400
+ }
401
+ if (!strictPluginMode && isLinkError(lastResult.msg)) {
461
402
  continue;
462
403
  }
463
- throw new Error(result.msg || result.body || 'Create failed');
464
404
  }
405
+ return lastResult;
406
+ };
407
+ 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);
465
444
  }
466
445
  else {
467
- // Try strategies progressively to avoid generic errors
468
- const strategies = [];
469
- strategies.push(JSON.parse(JSON.stringify(sendData)));
470
- const s2 = JSON.parse(JSON.stringify(sendData));
471
- if (s2.options)
472
- delete s2.options.link;
473
- strategies.push(s2);
474
- strategies.push({
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,
475
483
  options: {
476
- board_id: boardId,
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),
477
525
  field_set_key: 'create_success',
478
- image_url: imageUrl,
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),
479
532
  method: 'uploaded',
533
+ upload_metric: { source: 'pinner_upload_standalone' },
534
+ user_mention_tags: [],
535
+ no_fetch_context_on_resource: false,
480
536
  },
481
537
  context: {},
482
- });
483
- for (const strat of strategies) {
484
- result = await attemptCreate(strat);
485
- if (result.ok)
486
- break;
487
- if (/doesn\'t allow you to save Pins|does not allow you to save Pins/i.test(result.msg || ''))
488
- continue;
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
+ });
489
558
  }
490
- if (!result.ok) {
491
- if (debugRaw && this.continueOnFail()) {
492
- returnData.push({ json: { error: result.msg || 'Create failed', response: result.body, request: strategies[strategies.length - 1] } });
493
- continue;
494
- }
495
- throw new Error(result.msg || result.body || 'Create failed');
559
+ result = await runStrategies(strategies);
560
+ }
561
+ 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;
496
565
  }
566
+ throw new Error(result.msg || result.body || 'Create failed');
497
567
  }
498
568
  returnData.push({ json: { id: result.id, link: `https://www.pinterest.com/pin/${result.id}` } });
499
569
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-pinterest",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "n8n community nodes for Pinterest v5 API (list boards, create pins)",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",