n8n-nodes-pinterest 0.1.6 → 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 with a binary image input
9
+ - Create a Pin by letting Pinterest scrape a public image URL
10
10
 
11
11
  Notes:
12
12
 
@@ -37,14 +37,14 @@ 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`)
41
- - Optional: Title, Description, Link, Alt Text
40
+ - Image URL: Direct URL of the image for Pinterest to scrape
41
+ - Optional: Title, Description, Link
42
42
 
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/`.
43
+ The node calls the `/pin/find/?url=...` flow and creates the pin with `method="scraped"`. Works for images that are accessible over HTTPS.
44
44
 
45
45
  Additional behavior:
46
46
 
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.
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.
48
48
 
49
49
  ## Disclaimer
50
50
 
@@ -6,23 +6,35 @@ 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};`;
24
21
  }
25
22
  const PINTEREST_BASE = 'https://www.pinterest.com';
23
+ function sanitizePinterestText(value, limit) {
24
+ if (!value)
25
+ return '';
26
+ let sanitized = value.replace(/&/g, '&').replace(/&/g, '&');
27
+ sanitized = sanitized.replace(/\r\n|\r|\n/g, '\n');
28
+ sanitized = sanitized.replace(/\t/g, ' ').trim();
29
+ if (sanitized.length > limit) {
30
+ sanitized = sanitized.slice(0, limit - 3) + '...';
31
+ }
32
+ sanitized = sanitized.replace(/"/g, '\"');
33
+ return sanitized;
34
+ }
35
+ function doubleEncodeUrl(value) {
36
+ return encodeURIComponent(encodeURIComponent(value));
37
+ }
26
38
  async function resolveBaseUrl(ctx, headers, proxy) {
27
39
  try {
28
40
  const resp = (await ctx.helpers.request({
@@ -97,13 +109,13 @@ class PinterestCookie {
97
109
  description: 'Board to pin to',
98
110
  },
99
111
  {
100
- displayName: 'Binary Property',
101
- name: 'binaryProperty',
112
+ displayName: 'Image URL',
113
+ name: 'imageUrl',
102
114
  type: 'string',
103
- default: 'data',
115
+ default: '',
104
116
  required: true,
105
117
  displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
106
- description: 'Name of the binary property containing the image',
118
+ description: 'Direct URL of the image for Pinterest to scrape',
107
119
  },
108
120
  {
109
121
  displayName: 'Title',
@@ -120,39 +132,12 @@ class PinterestCookie {
120
132
  displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
121
133
  },
122
134
  {
123
- displayName: 'Additional Fields',
124
- name: 'additionalFields',
125
- type: 'collection',
126
- placeholder: 'Add Field',
127
- default: {},
128
- displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
129
- options: [
130
- { displayName: 'Alt Text', name: 'alt_text', type: 'string', default: '' },
131
- { displayName: 'Link', name: 'link', type: 'string', default: '' },
132
- {
133
- displayName: 'Attach Link',
134
- name: 'attachLink',
135
- type: 'boolean',
136
- default: true,
137
- description: 'If disabled or link is blocked, pin will be created without a destination link',
138
- },
139
- ],
140
- },
141
- {
142
- displayName: 'Strict Plugin Mode',
143
- name: 'strictPluginMode',
144
- type: 'boolean',
145
- default: false,
146
- description: 'Force exact request shape and headers used by the referenced plugin. Disables retries/fallbacks for 1:1 parity while debugging.',
147
- displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
148
- },
149
- {
150
- displayName: 'Debug: Return Raw Response',
151
- name: 'debugRaw',
152
- type: 'boolean',
153
- default: false,
154
- 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: '',
155
139
  displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
140
+ description: 'Destination URL for the pin',
156
141
  },
157
142
  // Board ops
158
143
  {
@@ -243,7 +228,7 @@ class PinterestCookie {
243
228
  };
244
229
  }
245
230
  async execute() {
246
- var _a, _b, _c;
231
+ var _a, _b;
247
232
  const items = this.getInputData();
248
233
  const returnData = [];
249
234
  const creds = (await this.getCredentials('pinterestCookieApi'));
@@ -307,135 +292,32 @@ class PinterestCookie {
307
292
  const operation = this.getNodeParameter('operation', i);
308
293
  if (operation === 'create') {
309
294
  const boardId = this.getNodeParameter('boardId', i);
310
- const binaryProperty = this.getNodeParameter('binaryProperty', i);
295
+ const imageUrl = this.getNodeParameter('imageUrl', i);
311
296
  const title = this.getNodeParameter('title', i, '');
312
297
  const description = this.getNodeParameter('description', i, '');
313
- const additionalFields = this.getNodeParameter('additionalFields', i, {});
314
- const debugRaw = this.getNodeParameter('debugRaw', i, false);
315
- 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
- : {
298
+ const link = this.getNodeParameter('link', i, '');
299
+ const attemptCreate = async (payload, createOptions) => {
300
+ var _a, _b, _c, _d, _e;
301
+ const form = {
302
+ source_url: (_a = createOptions === null || createOptions === void 0 ? void 0 : createOptions.sourceUrl) !== null && _a !== void 0 ? _a : '/pin-builder/',
303
+ data: JSON.stringify(payload),
304
+ };
305
+ if (createOptions === null || createOptions === void 0 ? void 0 : createOptions.modulePath) {
306
+ form.module_path = createOptions.modulePath;
307
+ }
308
+ const createResp = (await this.helpers.request({
309
+ method: 'POST',
310
+ uri: `${base}/resource/PinResource/create/`,
311
+ form,
312
+ headers: {
330
313
  ...baseHeaders,
331
314
  Cookie: cookieHeader,
332
- Referer: `${base}/pin-builder/`,
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/`,
333
316
  Origin: base,
334
- 'X-Pinterest-Source-Url': '/pin-builder/',
317
+ 'X-Pinterest-Source-Url': (_c = createOptions === null || createOptions === void 0 ? void 0 : createOptions.sourceUrl) !== null && _c !== void 0 ? _c : '/pin-builder/',
335
318
  'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
336
319
  },
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
396
- 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;
420
- const createResp = (await this.helpers.request({
421
- method: 'POST',
422
- uri: `${base}/resource/PinResource/create/`,
423
- form: {
424
- source_url: '/pin-builder/',
425
- data: JSON.stringify(payload),
426
- },
427
- headers: strictPluginMode
428
- ? { ...buildCommonHeaders(csrf, true), Cookie: cookieHeader }
429
- : {
430
- ...baseHeaders,
431
- Cookie: cookieHeader,
432
- Referer: `${base}/pin-builder/`,
433
- Origin: base,
434
- 'X-Pinterest-Source-Url': '/pin-builder/',
435
- 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
436
- },
437
320
  proxy: proxy || undefined,
438
- // Get body even on 4xx
439
321
  simple: false,
440
322
  resolveWithFullResponse: true,
441
323
  }));
@@ -448,53 +330,60 @@ class PinterestCookie {
448
330
  const createdRes = created.resource_response || {};
449
331
  const pinData = createdRes.data || {};
450
332
  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) || '';
333
+ const msg = createdRes.message || ((_d = createdRes.error) === null || _d === void 0 ? void 0 : _d.message) || '';
334
+ const msgDetail = ((_e = createdRes.error) === null || _e === void 0 ? void 0 : _e.message_detail) || '';
453
335
  return { ok: Boolean(id), id, msg: String(msg || msgDetail || ''), body, payload };
454
336
  };
455
- let result = { ok: false };
456
337
  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 || '');
457
- if (strictPluginMode) {
458
- result = await attemptCreate(sendData);
459
- if (!result.ok) {
460
- if (debugRaw && this.continueOnFail()) {
461
- returnData.push({ json: { error: result.msg || 'Create failed', response: result.body, request: sendData } });
338
+ const runStrategies = async (strategies) => {
339
+ let lastResult = { ok: false };
340
+ for (const strat of strategies) {
341
+ lastResult = await attemptCreate(strat.payload, strat.options);
342
+ if (lastResult.ok) {
343
+ return lastResult;
344
+ }
345
+ if (isLinkError(lastResult.msg)) {
462
346
  continue;
463
347
  }
464
- throw new Error(result.msg || result.body || 'Create failed');
465
348
  }
349
+ return lastResult;
350
+ };
351
+ let result = { ok: false };
352
+ if (!imageUrl) {
353
+ throw new Error('Image URL is required.');
466
354
  }
467
- else {
468
- // Try strategies progressively to avoid generic errors
469
- const strategies = [];
470
- strategies.push(JSON.parse(JSON.stringify(sendData)));
471
- const s2 = JSON.parse(JSON.stringify(sendData));
472
- if (s2.options)
473
- delete s2.options.link;
474
- strategies.push(s2);
475
- strategies.push({
476
- options: {
477
- board_id: boardId,
478
- field_set_key: 'create_success',
479
- image_url: imageUrl,
480
- method: 'uploaded',
481
- },
482
- context: {},
483
- });
484
- for (const strat of strategies) {
485
- result = await attemptCreate(strat);
486
- if (result.ok)
487
- break;
488
- if (isLinkError(result.msg))
489
- continue;
490
- }
491
- if (!result.ok) {
492
- if (debugRaw && this.continueOnFail()) {
493
- returnData.push({ json: { error: result.msg || 'Create failed', response: result.body, request: strategies[strategies.length - 1] } });
494
- continue;
495
- }
496
- throw new Error(result.msg || result.body || 'Create failed');
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 = '';
497
381
  }
382
+ strategies.push({ payload: withoutLink, options: scrapedOptions });
383
+ }
384
+ result = await runStrategies(strategies);
385
+ if (!result.ok) {
386
+ throw new Error(result.msg || result.body || 'Create failed');
498
387
  }
499
388
  returnData.push({ json: { id: result.id, link: `https://www.pinterest.com/pin/${result.id}` } });
500
389
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-pinterest",
3
- "version": "0.1.6",
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",