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
|
|
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
|
-
-
|
|
41
|
-
- Optional: Title, Description, Link
|
|
40
|
+
- Image URL: Direct URL of the image for Pinterest to scrape
|
|
41
|
+
- Optional: Title, Description, Link
|
|
42
42
|
|
|
43
|
-
|
|
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
|
-
-
|
|
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
|
|
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};`;
|
|
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: '
|
|
101
|
-
name: '
|
|
112
|
+
displayName: 'Image URL',
|
|
113
|
+
name: 'imageUrl',
|
|
102
114
|
type: 'string',
|
|
103
|
-
default: '
|
|
115
|
+
default: '',
|
|
104
116
|
required: true,
|
|
105
117
|
displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
|
|
106
|
-
description: '
|
|
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: '
|
|
124
|
-
name: '
|
|
125
|
-
type: '
|
|
126
|
-
|
|
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
|
|
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
|
|
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
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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 || ((
|
|
452
|
-
const msgDetail = ((
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (
|
|
492
|
-
|
|
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
|
}
|