n8n-nodes-pinterest 0.1.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 ADDED
@@ -0,0 +1,47 @@
1
+ # n8n-nodes-pinterest
2
+
3
+ Community n8n nodes for Pinterest using cookie-based authentication that mimics the web flow.
4
+
5
+ Supports:
6
+
7
+ - Cookie-based authentication (session cookie)
8
+ - List boards (current user)
9
+ - Create a Pin with a binary image input
10
+
11
+ Notes:
12
+
13
+ - Uses undocumented web endpoints; use at your own risk
14
+ - May violate Pinterest's Terms of Service and can break without notice
15
+ - Consider proxies if Pinterest flags your IP
16
+
17
+ ## Install
18
+
19
+ ```
20
+ npm install n8n-nodes-pinterest
21
+ ```
22
+
23
+ Restart n8n. The node and credential will appear in the editor.
24
+
25
+ ## Credentials
26
+
27
+ Add a new credential "Pinterest Cookie" and paste your `_pinterest_sess` value from a logged-in browser session. Optionally set a proxy.
28
+
29
+ ## Node usage
30
+
31
+ ### Pinterest Cookie Node: Board → Get Many
32
+ Lists boards via Pinterest web resource endpoint.
33
+
34
+ ### Pinterest Cookie Node: Pin → Create
35
+ Creates a pin using cookie-based authentication.
36
+
37
+ Inputs:
38
+
39
+ - Board (select from available boards)
40
+ - Binary Property (e.g. `data`)
41
+ - Optional: Title, Description, Link, Alt Text
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/`.
44
+
45
+ ## Disclaimer
46
+
47
+ This package uses cookie-based authentication and undocumented endpoints. Use at your own risk and in accordance with Pinterest's Terms of Service.
@@ -0,0 +1,7 @@
1
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class PinterestCookieApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PinterestCookieApi = void 0;
4
+ class PinterestCookieApi {
5
+ constructor() {
6
+ this.name = 'pinterestCookieApi';
7
+ this.displayName = 'Pinterest Cookie';
8
+ this.documentationUrl = 'https://www.pinterest.com';
9
+ this.properties = [
10
+ {
11
+ displayName: 'Pinterest Session Cookie (_pinterest_sess)',
12
+ name: 'pinterestSess',
13
+ type: 'string',
14
+ typeOptions: { password: true },
15
+ default: '',
16
+ required: true,
17
+ description: 'Value of the _pinterest_sess cookie from a logged-in session',
18
+ },
19
+ {
20
+ displayName: 'Proxy',
21
+ name: 'proxy',
22
+ type: 'string',
23
+ default: '',
24
+ description: 'Optional HTTP proxy (e.g. http://user:pass@host:port)',
25
+ },
26
+ ];
27
+ }
28
+ }
29
+ exports.PinterestCookieApi = PinterestCookieApi;
@@ -0,0 +1,4 @@
1
+ import type { INodeType } from 'n8n-workflow';
2
+ import { PinterestCookieApi } from './credentials/PinterestCookieApi.credentials';
3
+ export declare const nodes: INodeType[];
4
+ export declare const credentials: (typeof PinterestCookieApi)[];
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.credentials = exports.nodes = void 0;
4
+ const PinterestCookie_node_1 = require("./nodes/PinterestCookie/PinterestCookie.node");
5
+ const PinterestCookieApi_credentials_1 = require("./credentials/PinterestCookieApi.credentials");
6
+ exports.nodes = [new PinterestCookie_node_1.PinterestCookie()];
7
+ exports.credentials = [PinterestCookieApi_credentials_1.PinterestCookieApi];
@@ -0,0 +1,10 @@
1
+ import type { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class PinterestCookie implements INodeType {
3
+ description: INodeTypeDescription;
4
+ methods: {
5
+ loadOptions: {
6
+ getBoardsCookie(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
7
+ };
8
+ };
9
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
10
+ }
@@ -0,0 +1,367 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PinterestCookie = void 0;
4
+ function randomCsrf() {
5
+ // similar to plugin: base64 of microtime + random
6
+ const seed = `${Date.now()}${Math.floor(Math.random() * 100000)}`;
7
+ return Buffer.from(seed).toString('base64');
8
+ }
9
+ function buildCommonHeaders(csrf) {
10
+ return {
11
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0',
12
+ 'x-pinterest-pws-handler': 'www/[username].js',
13
+ 'x-CSRFToken': csrf,
14
+ };
15
+ }
16
+ function buildCookieHeader(sess, csrf) {
17
+ return `_pinterest_sess=${sess}; csrftoken=${csrf};`;
18
+ }
19
+ const PINTEREST_BASE = 'https://www.pinterest.com';
20
+ class PinterestCookie {
21
+ constructor() {
22
+ this.description = {
23
+ displayName: 'Pinterest (Cookie)',
24
+ name: 'pinterestCookie',
25
+ icon: 'fa:pinterest',
26
+ group: ['output'],
27
+ version: 1,
28
+ subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
29
+ description: 'Pinterest via cookie method (web endpoints)',
30
+ defaults: { name: 'Pinterest (Cookie)' },
31
+ inputs: ['main'],
32
+ outputs: ['main'],
33
+ credentials: [
34
+ { name: 'pinterestCookieApi', required: true },
35
+ ],
36
+ properties: [
37
+ {
38
+ displayName: 'Resource',
39
+ name: 'resource',
40
+ type: 'options',
41
+ noDataExpression: true,
42
+ options: [
43
+ { name: 'Pin', value: 'pin' },
44
+ { name: 'Board', value: 'board' },
45
+ { name: 'Me', value: 'me' },
46
+ ],
47
+ default: 'pin',
48
+ },
49
+ // Pin operations
50
+ {
51
+ displayName: 'Operation',
52
+ name: 'operation',
53
+ type: 'options',
54
+ displayOptions: { show: { resource: ['pin'] } },
55
+ options: [
56
+ { name: 'Create', value: 'create', action: 'Create a pin' },
57
+ ],
58
+ default: 'create',
59
+ },
60
+ {
61
+ displayName: 'Board',
62
+ name: 'boardId',
63
+ type: 'options',
64
+ typeOptions: { loadOptionsMethod: 'getBoardsCookie' },
65
+ displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
66
+ default: '',
67
+ required: true,
68
+ description: 'Board to pin to',
69
+ },
70
+ {
71
+ displayName: 'Binary Property',
72
+ name: 'binaryProperty',
73
+ type: 'string',
74
+ default: 'data',
75
+ required: true,
76
+ displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
77
+ description: 'Name of the binary property containing the image',
78
+ },
79
+ {
80
+ displayName: 'Title',
81
+ name: 'title',
82
+ type: 'string',
83
+ default: '',
84
+ displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
85
+ },
86
+ {
87
+ displayName: 'Description',
88
+ name: 'description',
89
+ type: 'string',
90
+ default: '',
91
+ displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
92
+ },
93
+ {
94
+ displayName: 'Additional Fields',
95
+ name: 'additionalFields',
96
+ type: 'collection',
97
+ placeholder: 'Add Field',
98
+ default: {},
99
+ displayOptions: { show: { resource: ['pin'], operation: ['create'] } },
100
+ options: [
101
+ { displayName: 'Alt Text', name: 'alt_text', type: 'string', default: '' },
102
+ { displayName: 'Link', name: 'link', type: 'string', default: '' },
103
+ ],
104
+ },
105
+ // Board ops
106
+ {
107
+ displayName: 'Operation',
108
+ name: 'boardOperation',
109
+ type: 'options',
110
+ displayOptions: { show: { resource: ['board'] } },
111
+ options: [
112
+ { name: 'Get Many', value: 'getAll', action: 'List boards' },
113
+ ],
114
+ default: 'getAll',
115
+ },
116
+ // Me ops
117
+ {
118
+ displayName: 'Operation',
119
+ name: 'meOperation',
120
+ type: 'options',
121
+ displayOptions: { show: { resource: ['me'] } },
122
+ options: [
123
+ { name: 'Get', value: 'get', action: 'Get my info' },
124
+ ],
125
+ default: 'get',
126
+ },
127
+ ],
128
+ };
129
+ this.methods = {
130
+ loadOptions: {
131
+ async getBoardsCookie() {
132
+ var _a, _b;
133
+ const creds = (await this.getCredentials('pinterestCookieApi'));
134
+ const pinterestSess = String(creds.pinterestSess || creds.pinterestsess || '');
135
+ const proxy = creds.proxy || '';
136
+ const csrf = randomCsrf();
137
+ const headers = {
138
+ ...buildCommonHeaders(csrf),
139
+ Cookie: buildCookieHeader(pinterestSess, csrf),
140
+ };
141
+ // Fetch username first (like plugin getMyInfo)
142
+ const meResp = (await this.helpers.request({
143
+ method: 'GET',
144
+ uri: `${PINTEREST_BASE}/resource/HomefeedBadgingResource/get/`,
145
+ headers,
146
+ proxy: proxy || undefined,
147
+ }));
148
+ const meJson = JSON.parse(meResp);
149
+ const username = String(((_b = (_a = meJson.client_context) === null || _a === void 0 ? void 0 : _a.user) === null || _b === void 0 ? void 0 : _b.username) || '');
150
+ const boards = [];
151
+ let bookmark = '';
152
+ while (true) {
153
+ const data = {
154
+ options: {
155
+ isPrefetch: false,
156
+ privacy_filter: 'all',
157
+ sort: 'custom',
158
+ field_set_key: 'profile_grid_item',
159
+ username,
160
+ page_size: 25,
161
+ group_by: 'visibility',
162
+ include_archived: true,
163
+ redux_normalize_feed: true,
164
+ ...(bookmark ? { bookmarks: [bookmark] } : {}),
165
+ },
166
+ context: {},
167
+ };
168
+ const qs = { data: JSON.stringify(data) };
169
+ const resp = (await this.helpers.request({
170
+ method: 'GET',
171
+ uri: `${PINTEREST_BASE}/resource/BoardsResource/get/`,
172
+ qs,
173
+ headers,
174
+ proxy: proxy || undefined,
175
+ json: true,
176
+ }));
177
+ const resource = resp.resource_response || {};
178
+ const pageBoards = resource.data || [];
179
+ for (const b of pageBoards) {
180
+ boards.push({ id: String(b.id), name: String(b.name || b.id) });
181
+ }
182
+ const next = String(resource.bookmark || '');
183
+ if (!next || next === '-end-')
184
+ break;
185
+ bookmark = next;
186
+ }
187
+ return boards.map((b) => ({ name: b.name, value: b.id }));
188
+ },
189
+ },
190
+ };
191
+ }
192
+ async execute() {
193
+ var _a, _b, _c, _d;
194
+ const items = this.getInputData();
195
+ const returnData = [];
196
+ const creds = (await this.getCredentials('pinterestCookieApi'));
197
+ const pinterestSess = String(creds.pinterestSess || creds.pinterestsess || '');
198
+ const proxy = creds.proxy || '';
199
+ for (let i = 0; i < items.length; i++) {
200
+ try {
201
+ const resource = this.getNodeParameter('resource', i);
202
+ const csrf = randomCsrf();
203
+ const baseHeaders = buildCommonHeaders(csrf);
204
+ const cookieHeader = buildCookieHeader(pinterestSess, csrf);
205
+ if (resource === 'me') {
206
+ const resp = (await this.helpers.request({
207
+ method: 'GET',
208
+ uri: `${PINTEREST_BASE}/resource/HomefeedBadgingResource/get/`,
209
+ headers: { ...baseHeaders, Cookie: cookieHeader },
210
+ proxy: proxy || undefined,
211
+ }));
212
+ const json = JSON.parse(resp);
213
+ returnData.push({ json });
214
+ continue;
215
+ }
216
+ if (resource === 'board') {
217
+ // Fetch username first to scope boards
218
+ const meResp = (await this.helpers.request({
219
+ method: 'GET',
220
+ uri: `${PINTEREST_BASE}/resource/HomefeedBadgingResource/get/`,
221
+ headers: { ...baseHeaders, Cookie: cookieHeader },
222
+ proxy: proxy || undefined,
223
+ }));
224
+ const meJson = JSON.parse(meResp);
225
+ const username = String(((_b = (_a = meJson.client_context) === null || _a === void 0 ? void 0 : _a.user) === null || _b === void 0 ? void 0 : _b.username) || '');
226
+ const resp = (await this.helpers.request({
227
+ method: 'GET',
228
+ uri: `${PINTEREST_BASE}/resource/BoardsResource/get/`,
229
+ qs: {
230
+ data: JSON.stringify({
231
+ options: {
232
+ isPrefetch: false,
233
+ privacy_filter: 'all',
234
+ sort: 'custom',
235
+ field_set_key: 'profile_grid_item',
236
+ username,
237
+ page_size: 25,
238
+ group_by: 'visibility',
239
+ include_archived: true,
240
+ redux_normalize_feed: true,
241
+ },
242
+ context: {},
243
+ }),
244
+ },
245
+ headers: { ...baseHeaders, Cookie: cookieHeader },
246
+ proxy: proxy || undefined,
247
+ json: true,
248
+ }));
249
+ returnData.push({ json: resp });
250
+ continue;
251
+ }
252
+ if (resource === 'pin') {
253
+ const operation = this.getNodeParameter('operation', i);
254
+ if (operation === 'create') {
255
+ const boardId = this.getNodeParameter('boardId', i);
256
+ const binaryProperty = this.getNodeParameter('binaryProperty', i);
257
+ const title = this.getNodeParameter('title', i, '');
258
+ const description = this.getNodeParameter('description', i, '');
259
+ const additionalFields = this.getNodeParameter('additionalFields', i, {});
260
+ const binary = (_c = items[i].binary) === null || _c === void 0 ? void 0 : _c[binaryProperty];
261
+ if (!binary)
262
+ throw new Error(`Binary property "${binaryProperty}" is missing on item ${i}`);
263
+ // Step 1: request upload slot
264
+ const vipResp = (await this.helpers.request({
265
+ method: 'POST',
266
+ uri: `${PINTEREST_BASE}/resource/VIPResource/create/`,
267
+ form: {
268
+ source_url: '/pin-builder/',
269
+ data: '{"options":{"type":"pinimage"},"context":{}}',
270
+ },
271
+ headers: { ...baseHeaders, Cookie: cookieHeader },
272
+ proxy: proxy || undefined,
273
+ }));
274
+ const vip = JSON.parse(vipResp);
275
+ const res = vip.resource_response || {};
276
+ const data = res.data || {};
277
+ const uploadUrl = String(data.upload_url || '');
278
+ const uploadParams = data.upload_parameters || {};
279
+ if (!uploadUrl)
280
+ throw new Error('Failed to get upload URL');
281
+ // Step 2: upload image
282
+ const buffer = Buffer.from(binary.data, 'base64');
283
+ const contentType = binary.mimeType || 'image/jpeg';
284
+ const formData = {};
285
+ for (const [k, v] of Object.entries(uploadParams))
286
+ formData[k] = String(v);
287
+ formData['file'] = {
288
+ value: buffer,
289
+ options: {
290
+ filename: 'blob',
291
+ contentType,
292
+ },
293
+ };
294
+ const uploadResp = (await this.helpers.request({
295
+ method: 'POST',
296
+ uri: uploadUrl,
297
+ headers: {
298
+ Accept: '*/*',
299
+ 'Accept-Encoding': 'gzip',
300
+ 'User-Agent': baseHeaders['User-Agent'],
301
+ Origin: 'https://www.pinterest.com',
302
+ Referer: 'https://www.pinterest.com',
303
+ },
304
+ formData,
305
+ proxy: proxy || undefined,
306
+ // get full response to read headers
307
+ resolveWithFullResponse: true,
308
+ simple: false,
309
+ }));
310
+ const etagRaw = uploadResp.headers['etag'] || uploadResp.headers['ETag'] || '';
311
+ const etag = String(etagRaw).replace(/"/g, '');
312
+ if (!etag)
313
+ throw new Error('Upload failed: missing ETag');
314
+ // compute imageUrl like plugin
315
+ const imageUrl = `https://i.pinimg.com/736x/${etag[0]}${etag[1]}/${etag[2]}${etag[3]}/${etag[4]}${etag[5]}/${etag}.jpg`;
316
+ // Step 3: create pin
317
+ const sendData = {
318
+ options: {
319
+ board_id: boardId,
320
+ field_set_key: 'create_success',
321
+ skip_pin_create_log: true,
322
+ description,
323
+ alt_text: additionalFields.alt_text || '',
324
+ link: additionalFields.link || '',
325
+ title,
326
+ image_url: imageUrl,
327
+ method: 'uploaded',
328
+ upload_metric: { source: 'pinner_upload_standalone' },
329
+ user_mention_tags: [],
330
+ no_fetch_context_on_resource: false,
331
+ },
332
+ context: {},
333
+ };
334
+ const createResp = (await this.helpers.request({
335
+ method: 'POST',
336
+ uri: `${PINTEREST_BASE}/resource/PinResource/create/`,
337
+ form: {
338
+ source_url: '/pin-builder/',
339
+ data: JSON.stringify(sendData),
340
+ },
341
+ headers: { ...baseHeaders, Cookie: cookieHeader },
342
+ proxy: proxy || undefined,
343
+ }));
344
+ const created = JSON.parse(createResp);
345
+ const createdRes = created.resource_response || {};
346
+ const pinData = createdRes.data || {};
347
+ const id = String(pinData.id || '');
348
+ if (!id) {
349
+ const message = createdRes.message || ((_d = createdRes.error) === null || _d === void 0 ? void 0 : _d.message) || 'Create failed';
350
+ throw new Error(String(message));
351
+ }
352
+ returnData.push({ json: { id, link: `https://www.pinterest.com/pin/${id}` } });
353
+ }
354
+ }
355
+ }
356
+ catch (error) {
357
+ if (this.continueOnFail()) {
358
+ returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
359
+ continue;
360
+ }
361
+ throw error;
362
+ }
363
+ }
364
+ return [returnData];
365
+ }
366
+ }
367
+ exports.PinterestCookie = PinterestCookie;
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "n8n-nodes-pinterest",
3
+ "version": "0.1.0",
4
+ "description": "n8n community nodes for Pinterest v5 API (list boards, create pins)",
5
+ "keywords": [
6
+ "n8n-community-node-package",
7
+ "n8n",
8
+ "pinterest",
9
+ "pins",
10
+ "boards"
11
+ ],
12
+ "author": "Community",
13
+ "license": "MIT",
14
+ "main": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "files": [
17
+ "dist/**"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p ."
21
+ },
22
+ "n8n": {
23
+ "credentials": [
24
+ "dist/credentials/PinterestCookieApi.credentials.js"
25
+ ],
26
+ "nodes": [
27
+ "dist/nodes/PinterestCookie/PinterestCookie.node.js"
28
+ ]
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^18.19.0",
32
+ "n8n-core": "^1.58.0",
33
+ "n8n-workflow": "^1.58.0",
34
+ "typescript": "^5.4.5"
35
+ }
36
+ }