n8n-nodes-bulkpublish 1.0.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,83 @@
1
+ # n8n-nodes-bulkpublish
2
+
3
+ n8n community node for [BulkPublish](https://bulkpublish.com) — publish to 11 social media platforms from your n8n workflows.
4
+
5
+ ## Supported Platforms
6
+
7
+ Facebook, Instagram, X (Twitter), TikTok, YouTube, Threads, Bluesky, Pinterest, Google Business Profile, LinkedIn, Mastodon
8
+
9
+ ## Installation
10
+
11
+ In your n8n instance:
12
+
13
+ 1. Go to **Settings > Community Nodes**
14
+ 2. Enter `n8n-nodes-bulkpublish`
15
+ 3. Click Install
16
+
17
+ Or via CLI:
18
+
19
+ ```bash
20
+ npm install n8n-nodes-bulkpublish
21
+ ```
22
+
23
+ ## Credentials
24
+
25
+ 1. Sign up at [app.bulkpublish.com](https://app.bulkpublish.com/register)
26
+ 2. Go to **Settings > Developer** and create an API key
27
+ 3. In n8n, add a **BulkPublish API** credential with your key
28
+
29
+ ## Operations
30
+
31
+ ### Post
32
+ - **Create** — Create a draft or scheduled post to one or more channels
33
+ - **Get** — Get a post with platform statuses
34
+ - **List** — List posts with filters (status, search, date)
35
+ - **Update** — Update a draft or scheduled post
36
+ - **Delete** — Delete a post
37
+ - **Publish** — Publish a draft immediately
38
+ - **Retry** — Retry failed platforms
39
+
40
+ ### Channel
41
+ - **List** — List all connected social media channels
42
+ - **Get** — Get a channel by ID
43
+ - **Health Check** — Check channel token health
44
+ - **Get Options** — Get platform-specific options (Pinterest boards, YouTube playlists, LinkedIn organizations)
45
+
46
+ ### Media
47
+ - **Upload** — Upload an image or video file
48
+ - **List** — List uploaded media files
49
+ - **Delete** — Delete a media file
50
+
51
+ ### Label
52
+ - **Create** — Create a label
53
+ - **List** — List all labels
54
+ - **Delete** — Delete a label
55
+
56
+ ### Analytics
57
+ - **Summary** — Get analytics summary for a date range
58
+ - **Engagement** — Get engagement data grouped by time period
59
+
60
+ ### Schedule
61
+ - **List** — List recurring schedules
62
+ - **Delete** — Delete a schedule
63
+
64
+ ## Example Workflows
65
+
66
+ ### Auto-post RSS feed items
67
+ 1. **RSS Feed Trigger** → reads new items
68
+ 2. **BulkPublish** → creates a scheduled post for each item
69
+
70
+ ### Post on GitHub release
71
+ 1. **GitHub Trigger** → new release event
72
+ 2. **BulkPublish** → creates and publishes announcement to all channels
73
+
74
+ ## Links
75
+
76
+ - [API Docs](https://app.bulkpublish.com/docs)
77
+ - [SDK & Examples](https://github.com/azeemkafridi/bulkpublish-api)
78
+ - [npm](https://www.npmjs.com/package/bulkpublish)
79
+ - [PyPI](https://pypi.org/project/bulkpublish/)
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
2
+ <rect width="48" height="48" rx="8" fill="#6366f1"/>
3
+ <path d="M14 16h20v3H14zm0 6.5h20v3H14zm0 6.5h14v3H14z" fill="#fff"/>
4
+ </svg>
@@ -0,0 +1,9 @@
1
+ import { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class BulkPublishApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ authenticate: IAuthenticateGeneric;
8
+ test: ICredentialTestRequest;
9
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BulkPublishApi = void 0;
4
+ class BulkPublishApi {
5
+ name = 'bulkPublishApi';
6
+ displayName = 'BulkPublish API';
7
+ documentationUrl = 'https://github.com/azeemkafridi/bulkpublish-api';
8
+ properties = [
9
+ {
10
+ displayName: 'API Key',
11
+ name: 'apiKey',
12
+ type: 'string',
13
+ typeOptions: { password: true },
14
+ default: '',
15
+ required: true,
16
+ description: 'Your BulkPublish API key (starts with bp_). Get one at https://app.bulkpublish.com/settings/developer',
17
+ },
18
+ ];
19
+ authenticate = {
20
+ type: 'generic',
21
+ properties: {
22
+ headers: {
23
+ Authorization: '=Bearer {{$credentials.apiKey}}',
24
+ },
25
+ },
26
+ };
27
+ test = {
28
+ request: {
29
+ baseURL: 'https://app.bulkpublish.com',
30
+ url: '/api/channels',
31
+ method: 'GET',
32
+ },
33
+ };
34
+ }
35
+ exports.BulkPublishApi = BulkPublishApi;
@@ -0,0 +1,5 @@
1
+ import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class BulkPublish implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,500 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BulkPublish = void 0;
4
+ const BASE_URL = 'https://app.bulkpublish.com';
5
+ class BulkPublish {
6
+ description = {
7
+ displayName: 'BulkPublish',
8
+ name: 'bulkPublish',
9
+ icon: 'file:icon.svg',
10
+ group: ['output'],
11
+ version: 1,
12
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
13
+ description: 'Publish to 11 social media platforms — Facebook, Instagram, X, TikTok, YouTube, Threads, Bluesky, Pinterest, LinkedIn, Mastodon, Google Business Profile',
14
+ defaults: { name: 'BulkPublish' },
15
+ inputs: ['main'],
16
+ outputs: ['main'],
17
+ credentials: [{ name: 'bulkPublishApi', required: true }],
18
+ properties: [
19
+ // ── Resource ─────────────────────────────────────────────
20
+ {
21
+ displayName: 'Resource',
22
+ name: 'resource',
23
+ type: 'options',
24
+ noDataExpression: true,
25
+ options: [
26
+ { name: 'Post', value: 'post' },
27
+ { name: 'Channel', value: 'channel' },
28
+ { name: 'Media', value: 'media' },
29
+ { name: 'Label', value: 'label' },
30
+ { name: 'Analytics', value: 'analytics' },
31
+ { name: 'Schedule', value: 'schedule' },
32
+ ],
33
+ default: 'post',
34
+ },
35
+ // ── Post Operations ──────────────────────────────────────
36
+ {
37
+ displayName: 'Operation',
38
+ name: 'operation',
39
+ type: 'options',
40
+ noDataExpression: true,
41
+ displayOptions: { show: { resource: ['post'] } },
42
+ options: [
43
+ { name: 'Create', value: 'create', action: 'Create a post' },
44
+ { name: 'Get', value: 'get', action: 'Get a post' },
45
+ { name: 'List', value: 'list', action: 'List posts' },
46
+ { name: 'Update', value: 'update', action: 'Update a post' },
47
+ { name: 'Delete', value: 'delete', action: 'Delete a post' },
48
+ { name: 'Publish', value: 'publish', action: 'Publish a draft immediately' },
49
+ { name: 'Retry', value: 'retry', action: 'Retry failed platforms' },
50
+ ],
51
+ default: 'create',
52
+ },
53
+ // Post: Create fields
54
+ {
55
+ displayName: 'Content',
56
+ name: 'content',
57
+ type: 'string',
58
+ typeOptions: { rows: 4 },
59
+ default: '',
60
+ required: true,
61
+ displayOptions: { show: { resource: ['post'], operation: ['create'] } },
62
+ description: 'The post text content',
63
+ },
64
+ {
65
+ displayName: 'Channels (JSON)',
66
+ name: 'channels',
67
+ type: 'string',
68
+ default: '[{"channelId": 1, "platform": "facebook"}]',
69
+ required: true,
70
+ displayOptions: { show: { resource: ['post'], operation: ['create'] } },
71
+ description: 'JSON array of channels: [{"channelId": number, "platform": string}]. Get IDs from the Channel > List operation.',
72
+ },
73
+ {
74
+ displayName: 'Status',
75
+ name: 'status',
76
+ type: 'options',
77
+ options: [
78
+ { name: 'Draft', value: 'draft' },
79
+ { name: 'Scheduled', value: 'scheduled' },
80
+ ],
81
+ default: 'draft',
82
+ displayOptions: { show: { resource: ['post'], operation: ['create'] } },
83
+ },
84
+ {
85
+ displayName: 'Scheduled At',
86
+ name: 'scheduledAt',
87
+ type: 'dateTime',
88
+ default: '',
89
+ displayOptions: { show: { resource: ['post'], operation: ['create'], status: ['scheduled'] } },
90
+ description: 'When to publish (ISO 8601)',
91
+ },
92
+ {
93
+ displayName: 'Timezone',
94
+ name: 'timezone',
95
+ type: 'string',
96
+ default: 'UTC',
97
+ displayOptions: { show: { resource: ['post'], operation: ['create'], status: ['scheduled'] } },
98
+ description: 'IANA timezone (e.g. America/New_York)',
99
+ },
100
+ {
101
+ displayName: 'Media File IDs',
102
+ name: 'mediaFiles',
103
+ type: 'string',
104
+ default: '',
105
+ displayOptions: { show: { resource: ['post'], operation: ['create'] } },
106
+ description: 'Comma-separated media file IDs (from Media > Upload)',
107
+ },
108
+ {
109
+ displayName: 'Label IDs',
110
+ name: 'labelIds',
111
+ type: 'string',
112
+ default: '',
113
+ displayOptions: { show: { resource: ['post'], operation: ['create'] } },
114
+ description: 'Comma-separated label IDs',
115
+ },
116
+ {
117
+ displayName: 'Platform Content (JSON)',
118
+ name: 'platformContent',
119
+ type: 'string',
120
+ default: '',
121
+ displayOptions: { show: { resource: ['post'], operation: ['create'] } },
122
+ description: 'Per-platform text overrides: {"x": "Short", "linkedin": "Long version"}',
123
+ },
124
+ // Post: Get/Update/Delete/Publish/Retry — ID
125
+ {
126
+ displayName: 'Post ID',
127
+ name: 'postId',
128
+ type: 'number',
129
+ default: 0,
130
+ required: true,
131
+ displayOptions: { show: { resource: ['post'], operation: ['get', 'update', 'delete', 'publish', 'retry'] } },
132
+ },
133
+ // Post: Update fields
134
+ {
135
+ displayName: 'Content',
136
+ name: 'updateContent',
137
+ type: 'string',
138
+ typeOptions: { rows: 4 },
139
+ default: '',
140
+ displayOptions: { show: { resource: ['post'], operation: ['update'] } },
141
+ description: 'New post content (leave empty to keep current)',
142
+ },
143
+ // Post: List filters
144
+ {
145
+ displayName: 'Status Filter',
146
+ name: 'statusFilter',
147
+ type: 'options',
148
+ options: [
149
+ { name: 'All', value: '' },
150
+ { name: 'Draft', value: 'draft' },
151
+ { name: 'Scheduled', value: 'scheduled' },
152
+ { name: 'Published', value: 'published' },
153
+ { name: 'Failed', value: 'failed' },
154
+ ],
155
+ default: '',
156
+ displayOptions: { show: { resource: ['post'], operation: ['list'] } },
157
+ },
158
+ {
159
+ displayName: 'Limit',
160
+ name: 'limit',
161
+ type: 'number',
162
+ default: 20,
163
+ displayOptions: { show: { resource: ['post'], operation: ['list'] } },
164
+ },
165
+ // ── Channel Operations ───────────────────────────────────
166
+ {
167
+ displayName: 'Operation',
168
+ name: 'operation',
169
+ type: 'options',
170
+ noDataExpression: true,
171
+ displayOptions: { show: { resource: ['channel'] } },
172
+ options: [
173
+ { name: 'List', value: 'list', action: 'List channels' },
174
+ { name: 'Get', value: 'get', action: 'Get a channel' },
175
+ { name: 'Health Check', value: 'health', action: 'Check channel health' },
176
+ { name: 'Get Options', value: 'options', action: 'Get platform options (boards, playlists, orgs)' },
177
+ ],
178
+ default: 'list',
179
+ },
180
+ {
181
+ displayName: 'Channel ID',
182
+ name: 'channelId',
183
+ type: 'number',
184
+ default: 0,
185
+ required: true,
186
+ displayOptions: { show: { resource: ['channel'], operation: ['get', 'health', 'options'] } },
187
+ },
188
+ // ── Media Operations ─────────────────────────────────────
189
+ {
190
+ displayName: 'Operation',
191
+ name: 'operation',
192
+ type: 'options',
193
+ noDataExpression: true,
194
+ displayOptions: { show: { resource: ['media'] } },
195
+ options: [
196
+ { name: 'Upload', value: 'upload', action: 'Upload a media file' },
197
+ { name: 'List', value: 'list', action: 'List media files' },
198
+ { name: 'Delete', value: 'delete', action: 'Delete a media file' },
199
+ ],
200
+ default: 'upload',
201
+ },
202
+ {
203
+ displayName: 'Binary Property',
204
+ name: 'binaryProperty',
205
+ type: 'string',
206
+ default: 'data',
207
+ required: true,
208
+ displayOptions: { show: { resource: ['media'], operation: ['upload'] } },
209
+ description: 'Name of the binary property containing the file to upload',
210
+ },
211
+ {
212
+ displayName: 'Media ID',
213
+ name: 'mediaId',
214
+ type: 'number',
215
+ default: 0,
216
+ required: true,
217
+ displayOptions: { show: { resource: ['media'], operation: ['delete'] } },
218
+ },
219
+ // ── Label Operations ─────────────────────────────────────
220
+ {
221
+ displayName: 'Operation',
222
+ name: 'operation',
223
+ type: 'options',
224
+ noDataExpression: true,
225
+ displayOptions: { show: { resource: ['label'] } },
226
+ options: [
227
+ { name: 'Create', value: 'create', action: 'Create a label' },
228
+ { name: 'List', value: 'list', action: 'List labels' },
229
+ { name: 'Delete', value: 'delete', action: 'Delete a label' },
230
+ ],
231
+ default: 'list',
232
+ },
233
+ {
234
+ displayName: 'Label Name',
235
+ name: 'labelName',
236
+ type: 'string',
237
+ default: '',
238
+ required: true,
239
+ displayOptions: { show: { resource: ['label'], operation: ['create'] } },
240
+ },
241
+ {
242
+ displayName: 'Label Color',
243
+ name: 'labelColor',
244
+ type: 'string',
245
+ default: '#6366f1',
246
+ displayOptions: { show: { resource: ['label'], operation: ['create'] } },
247
+ description: 'Hex color code',
248
+ },
249
+ {
250
+ displayName: 'Label ID',
251
+ name: 'labelId',
252
+ type: 'number',
253
+ default: 0,
254
+ required: true,
255
+ displayOptions: { show: { resource: ['label'], operation: ['delete'] } },
256
+ },
257
+ // ── Analytics Operations ─────────────────────────────────
258
+ {
259
+ displayName: 'Operation',
260
+ name: 'operation',
261
+ type: 'options',
262
+ noDataExpression: true,
263
+ displayOptions: { show: { resource: ['analytics'] } },
264
+ options: [
265
+ { name: 'Summary', value: 'summary', action: 'Get analytics summary' },
266
+ { name: 'Engagement', value: 'engagement', action: 'Get engagement data' },
267
+ ],
268
+ default: 'summary',
269
+ },
270
+ {
271
+ displayName: 'From',
272
+ name: 'from',
273
+ type: 'dateTime',
274
+ default: '',
275
+ required: true,
276
+ displayOptions: { show: { resource: ['analytics'] } },
277
+ },
278
+ {
279
+ displayName: 'To',
280
+ name: 'to',
281
+ type: 'dateTime',
282
+ default: '',
283
+ required: true,
284
+ displayOptions: { show: { resource: ['analytics'] } },
285
+ },
286
+ // ── Schedule Operations ──────────────────────────────────
287
+ {
288
+ displayName: 'Operation',
289
+ name: 'operation',
290
+ type: 'options',
291
+ noDataExpression: true,
292
+ displayOptions: { show: { resource: ['schedule'] } },
293
+ options: [
294
+ { name: 'List', value: 'list', action: 'List recurring schedules' },
295
+ { name: 'Delete', value: 'delete', action: 'Delete a schedule' },
296
+ ],
297
+ default: 'list',
298
+ },
299
+ {
300
+ displayName: 'Schedule ID',
301
+ name: 'scheduleId',
302
+ type: 'number',
303
+ default: 0,
304
+ required: true,
305
+ displayOptions: { show: { resource: ['schedule'], operation: ['delete'] } },
306
+ },
307
+ ],
308
+ };
309
+ async execute() {
310
+ const items = this.getInputData();
311
+ const returnData = [];
312
+ const resource = this.getNodeParameter('resource', 0);
313
+ const operation = this.getNodeParameter('operation', 0);
314
+ for (let i = 0; i < items.length; i++) {
315
+ let responseData;
316
+ // ── Posts ───────────────────────────────────────────────
317
+ if (resource === 'post') {
318
+ if (operation === 'create') {
319
+ const channelsJson = this.getNodeParameter('channels', i);
320
+ const body = {
321
+ content: this.getNodeParameter('content', i),
322
+ channels: JSON.parse(channelsJson),
323
+ status: this.getNodeParameter('status', i),
324
+ };
325
+ const scheduledAt = this.getNodeParameter('scheduledAt', i, '');
326
+ if (scheduledAt)
327
+ body.scheduledAt = scheduledAt;
328
+ const timezone = this.getNodeParameter('timezone', i, 'UTC');
329
+ if (timezone)
330
+ body.timezone = timezone;
331
+ const mediaStr = this.getNodeParameter('mediaFiles', i, '');
332
+ if (mediaStr)
333
+ body.mediaFiles = mediaStr.split(',').map((s) => parseInt(s.trim(), 10)).filter(Boolean);
334
+ const labelStr = this.getNodeParameter('labelIds', i, '');
335
+ if (labelStr)
336
+ body.labelIds = labelStr.split(',').map((s) => parseInt(s.trim(), 10)).filter(Boolean);
337
+ const pcStr = this.getNodeParameter('platformContent', i, '');
338
+ if (pcStr)
339
+ body.platformContent = JSON.parse(pcStr);
340
+ responseData = await this.helpers.httpRequest({
341
+ method: 'POST', url: `${BASE_URL}/api/posts`, body, json: true,
342
+ });
343
+ }
344
+ else if (operation === 'get') {
345
+ const id = this.getNodeParameter('postId', i);
346
+ responseData = await this.helpers.httpRequest({
347
+ method: 'GET', url: `${BASE_URL}/api/posts/${id}`, json: true,
348
+ });
349
+ }
350
+ else if (operation === 'list') {
351
+ const qs = { limit: this.getNodeParameter('limit', i) };
352
+ const status = this.getNodeParameter('statusFilter', i, '');
353
+ if (status)
354
+ qs.status = status;
355
+ responseData = await this.helpers.httpRequest({
356
+ method: 'GET', url: `${BASE_URL}/api/posts`, qs, json: true,
357
+ });
358
+ }
359
+ else if (operation === 'update') {
360
+ const id = this.getNodeParameter('postId', i);
361
+ const body = {};
362
+ const content = this.getNodeParameter('updateContent', i, '');
363
+ if (content)
364
+ body.content = content;
365
+ responseData = await this.helpers.httpRequest({
366
+ method: 'PUT', url: `${BASE_URL}/api/posts/${id}`, body, json: true,
367
+ });
368
+ }
369
+ else if (operation === 'delete') {
370
+ const id = this.getNodeParameter('postId', i);
371
+ responseData = await this.helpers.httpRequest({
372
+ method: 'DELETE', url: `${BASE_URL}/api/posts/${id}`, json: true,
373
+ });
374
+ }
375
+ else if (operation === 'publish') {
376
+ const id = this.getNodeParameter('postId', i);
377
+ responseData = await this.helpers.httpRequest({
378
+ method: 'POST', url: `${BASE_URL}/api/posts/${id}/publish`, json: true,
379
+ });
380
+ }
381
+ else if (operation === 'retry') {
382
+ const id = this.getNodeParameter('postId', i);
383
+ responseData = await this.helpers.httpRequest({
384
+ method: 'POST', url: `${BASE_URL}/api/posts/${id}/retry`, json: true,
385
+ });
386
+ }
387
+ }
388
+ // ── Channels ───────────────────────────────────────────
389
+ else if (resource === 'channel') {
390
+ if (operation === 'list') {
391
+ responseData = await this.helpers.httpRequest({
392
+ method: 'GET', url: `${BASE_URL}/api/channels`, json: true,
393
+ });
394
+ }
395
+ else if (operation === 'get') {
396
+ const id = this.getNodeParameter('channelId', i);
397
+ responseData = await this.helpers.httpRequest({
398
+ method: 'GET', url: `${BASE_URL}/api/channels/${id}`, json: true,
399
+ });
400
+ }
401
+ else if (operation === 'health') {
402
+ const id = this.getNodeParameter('channelId', i);
403
+ responseData = await this.helpers.httpRequest({
404
+ method: 'GET', url: `${BASE_URL}/api/channels/${id}/health`, json: true,
405
+ });
406
+ }
407
+ else if (operation === 'options') {
408
+ const id = this.getNodeParameter('channelId', i);
409
+ responseData = await this.helpers.httpRequest({
410
+ method: 'GET', url: `${BASE_URL}/api/channels/${id}/options`, json: true,
411
+ });
412
+ }
413
+ }
414
+ // ── Media ──────────────────────────────────────────────
415
+ else if (resource === 'media') {
416
+ if (operation === 'upload') {
417
+ const binaryProperty = this.getNodeParameter('binaryProperty', i);
418
+ const binaryData = this.helpers.assertBinaryData(i, binaryProperty);
419
+ const buffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty);
420
+ responseData = await this.helpers.httpRequest({
421
+ method: 'POST',
422
+ url: `${BASE_URL}/api/media`,
423
+ body: buffer,
424
+ headers: {
425
+ 'Content-Type': binaryData.mimeType,
426
+ 'Content-Disposition': `attachment; filename="${binaryData.fileName || 'upload'}"`,
427
+ },
428
+ json: false,
429
+ });
430
+ if (typeof responseData === 'string')
431
+ responseData = JSON.parse(responseData);
432
+ }
433
+ else if (operation === 'list') {
434
+ responseData = await this.helpers.httpRequest({
435
+ method: 'GET', url: `${BASE_URL}/api/media`, json: true,
436
+ });
437
+ }
438
+ else if (operation === 'delete') {
439
+ const id = this.getNodeParameter('mediaId', i);
440
+ responseData = await this.helpers.httpRequest({
441
+ method: 'DELETE', url: `${BASE_URL}/api/media/${id}`, json: true,
442
+ });
443
+ }
444
+ }
445
+ // ── Labels ─────────────────────────────────────────────
446
+ else if (resource === 'label') {
447
+ if (operation === 'create') {
448
+ responseData = await this.helpers.httpRequest({
449
+ method: 'POST', url: `${BASE_URL}/api/labels`, json: true,
450
+ body: {
451
+ name: this.getNodeParameter('labelName', i),
452
+ color: this.getNodeParameter('labelColor', i),
453
+ },
454
+ });
455
+ }
456
+ else if (operation === 'list') {
457
+ responseData = await this.helpers.httpRequest({
458
+ method: 'GET', url: `${BASE_URL}/api/labels`, json: true,
459
+ });
460
+ }
461
+ else if (operation === 'delete') {
462
+ const id = this.getNodeParameter('labelId', i);
463
+ responseData = await this.helpers.httpRequest({
464
+ method: 'DELETE', url: `${BASE_URL}/api/labels/${id}`, json: true,
465
+ });
466
+ }
467
+ }
468
+ // ── Analytics ──────────────────────────────────────────
469
+ else if (resource === 'analytics') {
470
+ const from = this.getNodeParameter('from', i);
471
+ const to = this.getNodeParameter('to', i);
472
+ const endpoint = operation === 'engagement' ? 'engagement' : 'summary';
473
+ responseData = await this.helpers.httpRequest({
474
+ method: 'GET', url: `${BASE_URL}/api/analytics/${endpoint}`,
475
+ qs: { from, to }, json: true,
476
+ });
477
+ }
478
+ // ── Schedules ──────────────────────────────────────────
479
+ else if (resource === 'schedule') {
480
+ if (operation === 'list') {
481
+ responseData = await this.helpers.httpRequest({
482
+ method: 'GET', url: `${BASE_URL}/api/schedules`, json: true,
483
+ });
484
+ }
485
+ else if (operation === 'delete') {
486
+ const id = this.getNodeParameter('scheduleId', i);
487
+ responseData = await this.helpers.httpRequest({
488
+ method: 'DELETE', url: `${BASE_URL}/api/schedules/${id}`, json: true,
489
+ });
490
+ }
491
+ }
492
+ const items = Array.isArray(responseData) ? responseData : [responseData];
493
+ for (const item of items) {
494
+ returnData.push({ json: item });
495
+ }
496
+ }
497
+ return [returnData];
498
+ }
499
+ }
500
+ exports.BulkPublish = BulkPublish;
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
2
+ <rect width="48" height="48" rx="8" fill="#6366f1"/>
3
+ <path d="M14 16h20v3H14zm0 6.5h20v3H14zm0 6.5h14v3H14z" fill="#fff"/>
4
+ </svg>
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "n8n-nodes-bulkpublish",
3
+ "version": "1.0.0",
4
+ "description": "n8n community node for BulkPublish — publish to 11 social media platforms from n8n workflows",
5
+ "keywords": [
6
+ "n8n-community-node-package",
7
+ "n8n-nodes",
8
+ "social-media",
9
+ "publishing",
10
+ "scheduling",
11
+ "automation",
12
+ "facebook",
13
+ "instagram",
14
+ "twitter",
15
+ "tiktok",
16
+ "linkedin",
17
+ "youtube",
18
+ "threads",
19
+ "bluesky",
20
+ "pinterest"
21
+ ],
22
+ "license": "MIT",
23
+ "author": "BulkPublish",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/azeemkafridi/n8n-nodes-bulkpublish"
27
+ },
28
+ "main": "dist/nodes/BulkPublish/BulkPublish.node.js",
29
+ "scripts": {
30
+ "build": "tsc && gulp build:icons",
31
+ "lint": "eslint nodes/**/*.ts credentials/**/*.ts",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "n8n": {
35
+ "n8nNodesApiVersion": 1,
36
+ "credentials": [
37
+ "dist/credentials/BulkPublishApi.credentials.js"
38
+ ],
39
+ "nodes": [
40
+ "dist/nodes/BulkPublish/BulkPublish.node.js"
41
+ ]
42
+ },
43
+ "devDependencies": {
44
+ "n8n-workflow": "^1.0.0",
45
+ "typescript": "^5.5.0",
46
+ "gulp": "^5.0.0",
47
+ "@types/node": "^22.0.0"
48
+ },
49
+ "files": [
50
+ "dist"
51
+ ]
52
+ }