nodebb-plugin-facebook-post 1.0.34 → 1.0.35

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.
Files changed (3) hide show
  1. package/CLAUDE.md +2 -1
  2. package/library.js +125 -9
  3. package/package.json +1 -1
package/CLAUDE.md CHANGED
@@ -20,8 +20,9 @@ There is no build step for this plugin. NodeBB handles asset bundling.
20
20
 
21
21
  **Environment variables (required at NodeBB startup):**
22
22
  - `NODEBB_FB_PAGE_ID` – Facebook Page ID
23
- - `NODEBB_FB_PAGE_ACCESS_TOKEN` – Page access token
23
+ - `NODEBB_FB_PAGE_ACCESS_TOKEN` – Page access token (also used for Instagram Graph API)
24
24
  - `NODEBB_FB_GRAPH_VERSION` – optional, defaults to `v25.0`
25
+ - `NODEBB_IG_USER_ID` – optional; Instagram Business Account ID. If omitted, fetched automatically from the linked Page via `/{pageId}?fields=instagram_business_account`
25
26
 
26
27
  ## Architecture
27
28
 
package/library.js CHANGED
@@ -38,6 +38,7 @@ function readEnv() {
38
38
  fbGraphVersion: trimStr(env.NODEBB_FB_GRAPH_VERSION) || 'v25.0',
39
39
  fbPageId: trimStr(env.NODEBB_FB_PAGE_ID),
40
40
  fbPageAccessToken: trimStr(env.NODEBB_FB_PAGE_ACCESS_TOKEN),
41
+ igUserId: trimStr(env.NODEBB_IG_USER_ID),
41
42
  };
42
43
  }
43
44
 
@@ -56,6 +57,7 @@ async function loadSettings() {
56
57
  settings.fbGraphVersion = env.fbGraphVersion;
57
58
  settings.fbPageId = env.fbPageId;
58
59
  settings.fbPageAccessToken = env.fbPageAccessToken;
60
+ settings.igUserId = env.igUserId;
59
61
  }
60
62
 
61
63
  function getForumBaseUrl() {
@@ -227,6 +229,100 @@ async function publishFeedPost({ message, link, photoIds }) {
227
229
  return resp.data && resp.data.id;
228
230
  }
229
231
 
232
+ let cachedIgUserId = null;
233
+
234
+ async function getInstagramUserId() {
235
+ if (settings.igUserId) return settings.igUserId;
236
+ if (cachedIgUserId) return cachedIgUserId;
237
+ const resp = await axios.get(
238
+ `https://graph.facebook.com/${settings.fbGraphVersion}/${settings.fbPageId}`,
239
+ {
240
+ params: { fields: 'instagram_business_account', access_token: settings.fbPageAccessToken },
241
+ timeout: 10000,
242
+ }
243
+ );
244
+ const igId = resp.data && resp.data.instagram_business_account && resp.data.instagram_business_account.id;
245
+ if (!igId) throw new Error('No Instagram Business Account linked to this Facebook Page');
246
+ cachedIgUserId = igId;
247
+ return igId;
248
+ }
249
+
250
+ async function postToInstagram(ctx) {
251
+ const rawContent = ctx.post.content || '';
252
+ const topicTitle = decodeHtmlEntities(ctx.topic.title || 'Nouveau sujet');
253
+ const author = ctx.user.username || 'Quelqu\u2019un';
254
+ const link = ctx.topicUrlAbs;
255
+
256
+ const imageUrls = extractImageUrlsFromContent(rawContent)
257
+ .filter(isForumHosted)
258
+ .slice(0, Math.max(0, settings.maxImages));
259
+
260
+ // Instagram feed posts require at least one image
261
+ if (!imageUrls.length) return null;
262
+
263
+ const excerpt = settings.includeExcerpt ? sanitizeExcerpt(rawContent, settings.excerptMaxLen) : '';
264
+ const lines = [`\uD83D\uDCDD ${topicTitle}`];
265
+ if (excerpt) lines.push(excerpt);
266
+ lines.push(`\uD83D\uDD17 ${link}`);
267
+ lines.push(`\u2014 ${author}`);
268
+ const caption = lines.join('\n\n');
269
+
270
+ const igUserId = await getInstagramUserId();
271
+ const baseUrl = `https://graph.facebook.com/${settings.fbGraphVersion}/${igUserId}`;
272
+
273
+ if (imageUrls.length === 1) {
274
+ const containerResp = await axios.post(`${baseUrl}/media`, null, {
275
+ params: { image_url: imageUrls[0], caption, access_token: settings.fbPageAccessToken },
276
+ timeout: 20000,
277
+ });
278
+ const containerId = containerResp.data && containerResp.data.id;
279
+ if (!containerId) return null;
280
+
281
+ const publishResp = await axios.post(`${baseUrl}/media_publish`, null, {
282
+ params: { creation_id: containerId, access_token: settings.fbPageAccessToken },
283
+ timeout: 20000,
284
+ });
285
+ return publishResp.data && publishResp.data.id;
286
+ }
287
+
288
+ // Carousel
289
+ const childIds = [];
290
+ for (const url of imageUrls) {
291
+ // eslint-disable-next-line no-await-in-loop
292
+ const childResp = await axios.post(`${baseUrl}/media`, null, {
293
+ params: {
294
+ image_url: url,
295
+ media_type: 'IMAGE',
296
+ is_carousel_item: true,
297
+ access_token: settings.fbPageAccessToken,
298
+ },
299
+ timeout: 20000,
300
+ });
301
+ const childId = childResp.data && childResp.data.id;
302
+ if (childId) childIds.push(childId);
303
+ }
304
+ if (!childIds.length) return null;
305
+
306
+ const form = new URLSearchParams();
307
+ form.append('media_type', 'CAROUSEL');
308
+ form.append('caption', caption);
309
+ childIds.forEach(id => form.append('children', id));
310
+ form.append('access_token', settings.fbPageAccessToken);
311
+
312
+ const carouselResp = await axios.post(`${baseUrl}/media`, form, {
313
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
314
+ timeout: 20000,
315
+ });
316
+ const carouselId = carouselResp.data && carouselResp.data.id;
317
+ if (!carouselId) return null;
318
+
319
+ const publishResp = await axios.post(`${baseUrl}/media_publish`, null, {
320
+ params: { creation_id: carouselId, access_token: settings.fbPageAccessToken },
321
+ timeout: 20000,
322
+ });
323
+ return publishResp.data && publishResp.data.id;
324
+ }
325
+
230
326
  async function postToFacebook(ctx) {
231
327
  const rawContent = ctx.post.content || '';
232
328
  const topicTitle = decodeHtmlEntities(ctx.topic.title || 'Nouveau sujet');
@@ -341,16 +437,36 @@ Plugin.onPostSave = async function (hookData) {
341
437
 
342
438
  const Posts = require.main.require('./src/posts');
343
439
 
344
- const already = await Posts.getPostField(ctx.post.pid, 'fbPostedId');
345
- if (already) return;
440
+ const fbAlready = await Posts.getPostField(ctx.post.pid, 'fbPostedId');
441
+ if (!fbAlready) {
442
+ try {
443
+ const fbId = await postToFacebook(ctx);
444
+ if (fbId) {
445
+ await Posts.setPostField(ctx.post.pid, 'fbPostedId', fbId);
446
+ await Posts.setPostField(ctx.post.pid, 'fbPostedAt', Date.now());
447
+ winston.info(`[facebook-post] facebook published pid=${ctx.post.pid} fbId=${fbId}`);
448
+ } else {
449
+ winston.warn(`[facebook-post] postToFacebook returned no id for pid=${ctx.post.pid}`);
450
+ }
451
+ } catch (fbErr) {
452
+ const detail = fbErr?.response ? JSON.stringify(fbErr.response.data) : (fbErr?.message || fbErr);
453
+ winston.error(`[facebook-post] facebook error pid=${ctx.post.pid}: ${detail}`);
454
+ }
455
+ }
346
456
 
347
- const fbId = await postToFacebook(ctx);
348
- if (fbId) {
349
- await Posts.setPostField(ctx.post.pid, 'fbPostedId', fbId);
350
- await Posts.setPostField(ctx.post.pid, 'fbPostedAt', Date.now());
351
- winston.info(`[facebook-post] published pid=${ctx.post.pid} fbId=${fbId}`);
352
- } else {
353
- winston.warn(`[facebook-post] postToFacebook returned no id for pid=${ctx.post.pid}`);
457
+ const igAlready = await Posts.getPostField(ctx.post.pid, 'igPostedId');
458
+ if (!igAlready) {
459
+ try {
460
+ const igId = await postToInstagram(ctx);
461
+ if (igId) {
462
+ await Posts.setPostField(ctx.post.pid, 'igPostedId', igId);
463
+ await Posts.setPostField(ctx.post.pid, 'igPostedAt', Date.now());
464
+ winston.info(`[facebook-post] instagram published pid=${ctx.post.pid} igId=${igId}`);
465
+ }
466
+ } catch (igErr) {
467
+ const detail = igErr?.response ? JSON.stringify(igErr.response.data) : (igErr?.message || igErr);
468
+ winston.error(`[facebook-post] instagram error pid=${ctx.post.pid}: ${detail}`);
469
+ }
354
470
  }
355
471
  } catch (e) {
356
472
  const detail = e?.response ? JSON.stringify(e.response.data) : (e?.message || e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-facebook-post",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "description": "Auto-post new NodeBB topics to a fixed Facebook Page (text + NodeBB uploads + place id).",
5
5
  "main": "library.js",
6
6
  "dependencies": {