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
|
|
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
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
10
|
-
|
|
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']
|
|
142
|
-
description: 'Direct URL of the image
|
|
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: '
|
|
160
|
-
name: '
|
|
161
|
-
type: '
|
|
162
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
367
|
-
|
|
368
|
-
:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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 (
|
|
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 (
|
|
409
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
},
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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}` } });
|