nodebb-plugin-facebook-post 1.0.33 → 1.0.34

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/library.js CHANGED
@@ -14,7 +14,6 @@ const DEFAULT_SETTINGS = {
14
14
  categoriesWhitelist: '',
15
15
  minimumReputation: 0,
16
16
  maxImages: 4,
17
- enablePlaceTagging: false,
18
17
  };
19
18
 
20
19
  function bool(v) {
@@ -52,7 +51,6 @@ async function loadSettings() {
52
51
  settings.minimumReputation = int(settings.minimumReputation, 0);
53
52
  settings.maxImages = int(settings.maxImages, DEFAULT_SETTINGS.maxImages);
54
53
  settings.categoriesWhitelist = trimStr(settings.categoriesWhitelist);
55
- settings.enablePlaceTagging = bool(settings.enablePlaceTagging);
56
54
 
57
55
  const env = readEnv();
58
56
  settings.fbGraphVersion = env.fbGraphVersion;
@@ -153,7 +151,7 @@ async function userIsAllowed(uid) {
153
151
  const ok = await Groups.isMember(uid, groupName);
154
152
  if (ok) return true;
155
153
  } catch {
156
- // ignore
154
+ // ignore individual group check errors
157
155
  }
158
156
  }
159
157
  return false;
@@ -209,14 +207,13 @@ async function uploadPhotoToFacebook(urlAbs) {
209
207
  return resp.data && resp.data.id;
210
208
  }
211
209
 
212
- async function publishFeedPost({ message, link, photoIds, placeId }) {
210
+ async function publishFeedPost({ message, link, photoIds }) {
213
211
  const endpoint = `https://graph.facebook.com/${settings.fbGraphVersion}/${settings.fbPageId}/feed`;
214
212
 
215
213
  const form = new URLSearchParams();
216
214
  form.append('message', String(message || ''));
217
215
  form.append('access_token', String(settings.fbPageAccessToken));
218
216
  if (link) form.append('link', String(link));
219
- if (placeId) form.append('place', String(placeId));
220
217
  if (Array.isArray(photoIds) && photoIds.length) {
221
218
  photoIds.forEach((id, idx) => {
222
219
  form.append(`attached_media[${idx}]`, JSON.stringify({ media_fbid: id }));
@@ -255,7 +252,7 @@ async function postToFacebook(ctx) {
255
252
  if (id) photoIds.push(id);
256
253
  }
257
254
 
258
- return publishFeedPost({ message, link, photoIds, placeId: trimStr(ctx.post.fbPlaceId) });
255
+ return publishFeedPost({ message, link, photoIds });
259
256
  }
260
257
 
261
258
  const Plugin = {};
@@ -303,44 +300,11 @@ Plugin.init = async function (params) {
303
300
  const allowedGroups = parseAllowedGroupsList();
304
301
  if (!allowedGroups.length) return res.json({ allowed: false, reason: 'no_groups_configured' });
305
302
  const ok = await userIsAllowed(uid);
306
- return res.json({ allowed: ok, reason: ok ? 'ok' : 'not_in_group', enablePlaceTagging: settings.enablePlaceTagging });
303
+ return res.json({ allowed: ok, reason: ok ? 'ok' : 'not_in_group' });
307
304
  } catch {
308
305
  return res.json({ allowed: false, reason: 'error' });
309
306
  }
310
307
  });
311
-
312
- router.get('/api/facebook-post/search-place', middleware.ensureLoggedIn, async (req, res) => {
313
- const winston = require.main.require('winston');
314
- res.set('Cache-Control', 'no-store');
315
- try {
316
- await loadSettings();
317
- if (!settings.enabled || !settings.enablePlaceTagging || !settings.fbPageAccessToken) {
318
- winston.info(`[facebook-post] search-place: abandon — enabled=${settings.enabled} enablePlaceTagging=${settings.enablePlaceTagging} token=${!!settings.fbPageAccessToken}`);
319
- return res.json({ results: [] });
320
- }
321
- const q = trimStr(req.query.q);
322
- if (q.length < 2) return res.json({ results: [] });
323
- winston.info(`[facebook-post] search-place: requête q="${q}"`);
324
- const resp = await axios.get(`https://graph.facebook.com/${settings.fbGraphVersion}/search`, {
325
- params: {
326
- type: 'place',
327
- q,
328
- fields: 'name,location,id',
329
- access_token: settings.fbPageAccessToken,
330
- limit: 8,
331
- },
332
- timeout: 8000,
333
- });
334
- const results = (resp.data && resp.data.data) || [];
335
- winston.info(`[facebook-post] search-place: ${results.length} résultat(s) pour "${q}"`);
336
- return res.json({ results });
337
- } catch (e) {
338
- const detail = e?.response ? JSON.stringify(e.response.data) : (e?.message || e);
339
- winston.error(`[facebook-post] search-place: ERREUR: ${detail}`);
340
- return res.json({ results: [], error: detail });
341
- }
342
- });
343
-
344
308
  };
345
309
 
346
310
  Plugin.addAdminNavigation = async function (header) {
@@ -354,94 +318,43 @@ Plugin.addAdminNavigation = async function (header) {
354
318
  };
355
319
 
356
320
  Plugin.onPostCreate = async function (hookData) {
357
- const winston = require.main.require('winston');
358
- try {
359
- if (!hookData?.post || !hookData?.data) {
360
- winston.info('[facebook-post] onPostCreate: hookData manquant (post ou data absent)');
361
- return hookData;
362
- }
363
- hookData.post.fbPostEnabled = bool(hookData.data.fbPostEnabled);
364
- const fbPlaceId = trimStr(hookData.data.fbPlaceId);
365
- if (fbPlaceId) hookData.post.fbPlaceId = fbPlaceId;
366
- winston.info(`[facebook-post] onPostCreate: pid=${hookData.post.pid} fbPostEnabled=${hookData.post.fbPostEnabled} fbPlaceId=${hookData.post.fbPlaceId || '(none)'}`);
367
- } catch (e) {
368
- winston.error(`[facebook-post] onPostCreate erreur: ${e?.message || e}`);
369
- }
321
+ if (!hookData?.post || !hookData?.data) return hookData;
322
+ hookData.post.fbPostEnabled = bool(hookData.data.fbPostEnabled);
370
323
  return hookData;
371
324
  };
372
325
 
373
326
  Plugin.onPostSave = async function (hookData) {
374
327
  const winston = require.main.require('winston');
375
- winston.info('[facebook-post] onPostSave: hook déclenché');
376
328
  try {
377
329
  await loadSettings();
378
- winston.info(`[facebook-post] onPostSave: settings.enabled=${settings.enabled} fbPageId=${settings.fbPageId ? 'défini' : 'MANQUANT'} fbPageAccessToken=${settings.fbPageAccessToken ? 'défini' : 'MANQUANT'}`);
379
-
380
- if (!settings.enabled) {
381
- winston.info('[facebook-post] onPostSave: abandon — plugin désactivé dans les paramètres');
382
- return;
383
- }
330
+ if (!settings.enabled) return;
384
331
 
385
332
  const rawPost = hookData && hookData.post ? hookData.post : hookData;
386
- winston.info(`[facebook-post] onPostSave: données brutes reçues — pid=${rawPost?.pid} fbPostEnabled=${rawPost?.fbPostEnabled}`);
387
-
388
333
  const ctx = await getPostContext(rawPost);
389
- if (!ctx) {
390
- winston.warn('[facebook-post] onPostSave: abandon — impossible de récupérer le contexte du post (pid invalide ?)');
391
- return;
392
- }
334
+ if (!ctx) return;
393
335
 
394
- // Restore ephemeral fields set by onPostCreate (not persisted to DB)
336
+ // Restore ephemeral field set by onPostCreate (not persisted to DB)
395
337
  ctx.post.fbPostEnabled = rawPost.fbPostEnabled;
396
- if (rawPost.fbPlaceId) ctx.post.fbPlaceId = rawPost.fbPlaceId;
397
338
 
398
- winston.info(`[facebook-post] onPostSave: contexte récupéré — pid=${ctx.post.pid} tid=${ctx.topic.tid} cid=${ctx.topic.cid} uid=${ctx.user.uid} isMainPost=${ctx.post.isMainPost} index=${ctx.post.index} mainPid=${ctx.topic.mainPid} reputation=${ctx.user.reputation}`);
399
-
400
- const allowed = await userIsAllowed(ctx.post.uid);
401
- if (!allowed) {
402
- const groups = parseAllowedGroupsList();
403
- winston.info(`[facebook-post] onPostSave: abandon — uid=${ctx.post.uid} n'est pas dans les groupes autorisés: [${groups.join(', ')}]`);
404
- return;
405
- }
406
- winston.info(`[facebook-post] onPostSave: uid=${ctx.post.uid} autorisé par les groupes`);
407
-
408
- const process = shouldProcessPost(ctx);
409
- if (!process) {
410
- const isFirstPost = (ctx.post.isMainPost === true) || (ctx.post.index === 0) || (String(ctx.post.pid) === String(ctx.topic.mainPid));
411
- const fbEnabled = bool(ctx.post.fbPostEnabled);
412
- const repOk = (ctx.user.reputation || 0) >= settings.minimumReputation;
413
- const whitelist = parseCsvInts(settings.categoriesWhitelist);
414
- const catOk = whitelist.length === 0 || whitelist.includes(parseInt(ctx.topic.cid, 10));
415
- winston.info(`[facebook-post] onPostSave: abandon — shouldProcessPost=false. Détail: isFirstPost=${isFirstPost} fbPostEnabled=${fbEnabled} reputationOk=${repOk}(${ctx.user.reputation}>=${settings.minimumReputation}) categoryOk=${catOk}(cid=${ctx.topic.cid} whitelist=[${whitelist}])`);
416
- return;
417
- }
418
- winston.info('[facebook-post] onPostSave: shouldProcessPost=true, publication en cours…');
339
+ if (!(await userIsAllowed(ctx.post.uid))) return;
340
+ if (!shouldProcessPost(ctx)) return;
419
341
 
420
342
  const Posts = require.main.require('./src/posts');
421
343
 
422
344
  const already = await Posts.getPostField(ctx.post.pid, 'fbPostedId');
423
- if (already) {
424
- winston.info(`[facebook-post] onPostSave: abandon — déjà publié (fbPostedId=${already})`);
425
- return;
426
- }
427
-
428
- const imageUrls = extractImageUrlsFromContent(ctx.post.content || '').filter(isForumHosted);
429
- winston.info(`[facebook-post] onPostSave: images hébergées sur le forum trouvées: ${imageUrls.length} [${imageUrls.join(', ')}]`);
345
+ if (already) return;
430
346
 
431
347
  const fbId = await postToFacebook(ctx);
432
348
  if (fbId) {
433
349
  await Posts.setPostField(ctx.post.pid, 'fbPostedId', fbId);
434
350
  await Posts.setPostField(ctx.post.pid, 'fbPostedAt', Date.now());
435
- winston.info(`[facebook-post] onPostSave: publication réussie — fbId=${fbId} pid=${ctx.post.pid}`);
351
+ winston.info(`[facebook-post] published pid=${ctx.post.pid} fbId=${fbId}`);
436
352
  } else {
437
- winston.warn('[facebook-post] onPostSave: postToFacebook a retourné un fbId vide (pas d\'erreur levée)');
353
+ winston.warn(`[facebook-post] postToFacebook returned no id for pid=${ctx.post.pid}`);
438
354
  }
439
355
  } catch (e) {
440
356
  const detail = e?.response ? JSON.stringify(e.response.data) : (e?.message || e);
441
- winston.error(`[facebook-post] onPostSave: ERREUR: ${detail}`);
442
- if (e?.response?.config?.url) {
443
- winston.error(`[facebook-post] onPostSave: URL appelée: ${e.response.config.url} — status: ${e.response.status}`);
444
- }
357
+ winston.error(`[facebook-post] onPostSave error: ${detail}`);
445
358
  }
446
359
  };
447
360
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-facebook-post",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
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": {
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "nodebb-plugin-facebook-post",
3
3
  "name": "Facebook Post",
4
- "description": "Publie automatiquement les nouveaux topics NodeBB sur une Page Facebook fixe (images uploads + place).",
4
+ "description": "Publie automatiquement les nouveaux topics NodeBB sur une Page Facebook fixe (images uploads).",
5
5
  "url": "https://example.invalid/nodebb-plugin-facebook-post",
6
6
  "hooks": [
7
7
  {
@@ -1,9 +1,7 @@
1
- /* global $, app */
1
+ /* global $ */
2
2
  'use strict';
3
3
 
4
4
  (function () {
5
- const DEBOUNCE_MS = 400;
6
-
7
5
  async function canPost() {
8
6
  try {
9
7
  const res = await fetch('/api/facebook-post/can-post?_=' + Date.now(), {
@@ -17,36 +15,12 @@
17
15
  }
18
16
  }
19
17
 
20
- async function searchPlace(q) {
21
- try {
22
- const res = await fetch('/api/facebook-post/search-place?q=' + encodeURIComponent(q), {
23
- credentials: 'same-origin',
24
- cache: 'no-store',
25
- });
26
- if (!res.ok) return [];
27
- const data = await res.json();
28
- return data.results || [];
29
- } catch {
30
- return [];
31
- }
32
- }
33
-
34
- function resetPlace($panel) {
35
- $panel.find('.fb-place-search').removeClass('d-none').val('');
36
- $panel.find('.fb-place-id').val('');
37
- $panel.find('.fb-place-selected').removeClass('d-inline-flex').addClass('d-none');
38
- $panel.find('.fb-place-results').hide().empty();
39
- }
40
-
41
- function injectUI($composer, perm) {
18
+ function injectUI($composer) {
42
19
  if ($composer.find('[data-fbpost-btn]').length) return;
43
20
 
44
- const showPlace = !!perm.enablePlaceTagging;
45
-
46
- // ── Bouton dans la toolbar — même structure que les boutons natifs ──
47
21
  const $li = $(`
48
22
  <li aria-label="Publier sur Facebook" data-bs-original-title="Publier sur Facebook" data-fbpost-btn>
49
- <button type="button" class="btn btn-sm btn-link text-reset position-relative"
23
+ <button type="button" class="btn btn-sm btn-link text-reset"
50
24
  aria-label="Publier sur Facebook" style="opacity:0.45;">
51
25
  <i class="fab fa-facebook-f"></i>
52
26
  </button>
@@ -54,122 +28,24 @@
54
28
  `);
55
29
  const $btn = $li.find('button');
56
30
 
57
- // ── Panneau lieu (affiché sous la toolbar quand FB est actif) ──
58
- const $placePanel = $(`
59
- <div class="fb-place-panel d-none align-items-center gap-2 px-2 py-1"
60
- style="font-size:0.85em; border-top:1px solid rgba(128,128,128,.2);">
61
- <span class="text-muted">Publier sur Facebook</span>
62
- ${showPlace ? `
63
- <div class="position-relative d-flex align-items-center gap-1 flex-grow-1 ms-2">
64
- <span class="text-muted text-nowrap">Lieu (facultatif)</span>
65
- <input type="text" class="form-control form-control-sm fb-place-search"
66
- placeholder="Rechercher…" autocomplete="off">
67
- <ul class="fb-place-results dropdown-menu p-1"
68
- style="display:none; position:absolute; z-index:9999; top:100%; left:0;
69
- min-width:220px; max-height:200px; overflow-y:auto;"></ul>
70
- </div>
71
- <span class="fb-place-selected d-none align-items-center gap-1 text-nowrap">
72
- <i class="fa fa-map-marker" style="color:#1877F2;"></i>
73
- <span class="fb-place-name fw-semibold"></span>
74
- <a href="#" class="fb-place-clear ms-1 text-danger" title="Effacer">
75
- <i class="fa fa-times"></i>
76
- </a>
77
- </span>
78
- <input type="hidden" class="fb-place-id" value="">
79
- ` : ''}
80
- </div>
81
- `);
82
-
83
- // ── Toggle actif/inactif ──
84
31
  $btn.on('click', function () {
85
32
  const active = !$(this).data('fbpost-active');
86
33
  $(this).data('fbpost-active', active);
87
- if (active) {
88
- $(this).css({ color: '#1877F2', opacity: '1' });
89
- $placePanel.removeClass('d-none').addClass('d-flex');
90
- } else {
91
- $(this).css({ color: '', opacity: '0.45' });
92
- $placePanel.removeClass('d-flex').addClass('d-none');
93
- if (showPlace) resetPlace($placePanel);
94
- }
34
+ $(this).css({ color: active ? '#1877F2' : '', opacity: active ? '1' : '0.45' });
95
35
  });
96
36
 
97
- // ── Recherche de lieu avec debounce ──
98
- if (showPlace) {
99
- let debounceTimer;
100
-
101
- $placePanel.find('.fb-place-search').on('input', function () {
102
- const q = $(this).val().trim();
103
- const $results = $placePanel.find('.fb-place-results');
104
- clearTimeout(debounceTimer);
105
- if (q.length < 2) { $results.hide().empty(); return; }
106
-
107
- debounceTimer = setTimeout(async () => {
108
- const places = await searchPlace(q);
109
- $results.empty();
110
- if (!places.length) { $results.hide(); return; }
111
-
112
- places.forEach(p => {
113
- const loc = p.location
114
- ? [p.location.city, p.location.country].filter(Boolean).join(', ')
115
- : '';
116
- $('<li>').append(
117
- $('<a href="#" class="dropdown-item rounded-1 py-1">')
118
- .attr({ 'data-place-id': p.id, 'data-place-name': p.name })
119
- .append($('<div class="fw-semibold lh-sm">').text(p.name))
120
- .append(loc ? $('<div class="text-muted small lh-sm">').text(loc) : null)
121
- .on('click', function (e) {
122
- e.preventDefault();
123
- $placePanel.find('.fb-place-id').val($(this).data('place-id'));
124
- $placePanel.find('.fb-place-name').text($(this).data('place-name'));
125
- $placePanel.find('.fb-place-selected').removeClass('d-none').addClass('d-inline-flex');
126
- $placePanel.find('.fb-place-search').addClass('d-none');
127
- $results.hide().empty();
128
- })
129
- ).appendTo($results);
130
- });
131
- $results.show();
132
- }, DEBOUNCE_MS);
133
- });
134
-
135
- $placePanel.find('.fb-place-clear').on('click', function (e) {
136
- e.preventDefault();
137
- $placePanel.find('.fb-place-id').val('');
138
- $placePanel.find('.fb-place-selected').removeClass('d-inline-flex').addClass('d-none');
139
- $placePanel.find('.fb-place-search').removeClass('d-none').val('').trigger('focus');
140
- });
141
-
142
- // Fermer le dropdown si clic ailleurs
143
- $(document).off('click.fbpost').on('click.fbpost', function (e) {
144
- if (!$(e.target).closest('.fb-place-panel').length) {
145
- $placePanel.find('.fb-place-results').hide().empty();
146
- }
147
- });
148
- }
149
-
150
- // ── Injection dans la toolbar ──
151
37
  const $toolbar = $composer.find('ul.formatting-group').first();
152
38
  if ($toolbar.length) {
153
39
  $toolbar.append($li);
154
- $toolbar.after($placePanel);
155
40
  } else {
156
- // Fallback : avant la zone de texte
157
- const $write = $composer.find('.write').first();
158
- $write.before($placePanel);
159
- $placePanel.before($li);
41
+ $composer.find('.write').first().before($li);
160
42
  }
161
43
 
162
- // ── Hook submit ──
163
44
  $(window).off('action:composer.submit.fbpost')
164
- .on('action:composer.submit.fbpost', function (ev2, data) {
165
- const enabled = !!$btn.data('fbpost-active');
45
+ .on('action:composer.submit.fbpost', function (ev, data) {
166
46
  const postData = (data && data.composerData) || (data && data.postData) || data;
167
47
  if (!postData || typeof postData !== 'object') return;
168
- postData.fbPostEnabled = enabled;
169
- if (enabled && showPlace) {
170
- const placeId = $placePanel.find('.fb-place-id').val();
171
- if (placeId) postData.fbPlaceId = placeId;
172
- }
48
+ postData.fbPostEnabled = !!$btn.data('fbpost-active');
173
49
  });
174
50
  }
175
51
 
@@ -179,7 +55,7 @@
179
55
  if (!$composer || !$composer.length) return;
180
56
  const perm = await canPost();
181
57
  if (!perm.allowed) return;
182
- injectUI($composer, perm);
58
+ injectUI($composer);
183
59
  } catch {
184
60
  // ignore
185
61
  }
@@ -1,5 +1,5 @@
1
1
  <div class="acp-page-container">
2
- <h2>Facebook/Instagram Post (Worker)</h2>
2
+ <h2>Facebook Post</h2>
3
3
 
4
4
  <form class="facebook-post-settings" role="form">
5
5
  <div class="form-check mb-3">
@@ -7,15 +7,10 @@
7
7
  <label class="form-check-label" for="enabled">Activer</label>
8
8
  </div>
9
9
 
10
- <div class="form-check mb-3">
11
- <input type="checkbox" class="form-check-input" id="publishInstagram" name="publishInstagram">
12
- <label class="form-check-label" for="publishInstagram">Publier aussi sur Instagram (uniquement si une image est présente)</label>
13
- </div>
14
-
15
10
  <hr/>
16
11
 
17
12
  <h4>Accès</h4>
18
- <p class="text-muted">Sélectionne les groupes autorisés à voir la case Publier sur Facebook dans le composer.</p>
13
+ <p class="text-muted">Sélectionne les groupes autorisés à voir la case "Publier sur Facebook" dans le composer.</p>
19
14
 
20
15
  <div class="mb-3">
21
16
  <label class="form-label" for="allowedGroupsSelect">Groupes autorisés</label>
@@ -25,7 +20,7 @@
25
20
  <!-- END allGroups -->
26
21
  </select>
27
22
  <input type="hidden" id="allowedGroups" name="allowedGroups" value="">
28
- <p class="form-text">Sans fallback : si aucun groupe nest sélectionné, personne ne verra la case.</p>
23
+ <p class="form-text">Sans fallback : si aucun groupe n'est sélectionné, personne ne verra la case.</p>
29
24
  </div>
30
25
 
31
26
  <hr/>
@@ -38,7 +33,7 @@
38
33
  </div>
39
34
 
40
35
  <div class="mb-3">
41
- <label class="form-label" for="excerptMaxLen">Longueur max de lextrait</label>
36
+ <label class="form-label" for="excerptMaxLen">Longueur max de l'extrait</label>
42
37
  <input type="number" class="form-control" id="excerptMaxLen" name="excerptMaxLen" min="50" max="5000">
43
38
  </div>
44
39
 
@@ -62,19 +57,10 @@
62
57
  <h4>Images</h4>
63
58
 
64
59
  <div class="mb-3">
65
- <label class="form-label" for="maxImages">Nombre max dimages (uploads du forum)</label>
60
+ <label class="form-label" for="maxImages">Nombre max d'images (uploads du forum)</label>
66
61
  <input type="number" class="form-control" id="maxImages" name="maxImages" min="0" max="20">
67
62
  </div>
68
63
 
69
- <hr/>
70
-
71
- <h4>Lieu / Géolocalisation</h4>
72
-
73
- <div class="form-check mb-3">
74
- <input type="checkbox" class="form-check-input" id="enablePlaceTagging" name="enablePlaceTagging">
75
- <label class="form-check-label" for="enablePlaceTagging">Activer le tag de lieu (Place ID / location_id)</label>
76
- </div>
77
-
78
- <button type="button" id="save" class="btn btn-primary">Enregistrer</button>
64
+ <button type="button" id="save" class="btn btn-primary mt-2">Enregistrer</button>
79
65
  </form>
80
66
  </div>