nodebb-plugin-facebook-post 1.0.31 → 1.0.33

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,6 +14,7 @@ const DEFAULT_SETTINGS = {
14
14
  categoriesWhitelist: '',
15
15
  minimumReputation: 0,
16
16
  maxImages: 4,
17
+ enablePlaceTagging: false,
17
18
  };
18
19
 
19
20
  function bool(v) {
@@ -51,6 +52,7 @@ async function loadSettings() {
51
52
  settings.minimumReputation = int(settings.minimumReputation, 0);
52
53
  settings.maxImages = int(settings.maxImages, DEFAULT_SETTINGS.maxImages);
53
54
  settings.categoriesWhitelist = trimStr(settings.categoriesWhitelist);
55
+ settings.enablePlaceTagging = bool(settings.enablePlaceTagging);
54
56
 
55
57
  const env = readEnv();
56
58
  settings.fbGraphVersion = env.fbGraphVersion;
@@ -207,13 +209,14 @@ async function uploadPhotoToFacebook(urlAbs) {
207
209
  return resp.data && resp.data.id;
208
210
  }
209
211
 
210
- async function publishFeedPost({ message, link, photoIds }) {
212
+ async function publishFeedPost({ message, link, photoIds, placeId }) {
211
213
  const endpoint = `https://graph.facebook.com/${settings.fbGraphVersion}/${settings.fbPageId}/feed`;
212
214
 
213
215
  const form = new URLSearchParams();
214
216
  form.append('message', String(message || ''));
215
217
  form.append('access_token', String(settings.fbPageAccessToken));
216
218
  if (link) form.append('link', String(link));
219
+ if (placeId) form.append('place', String(placeId));
217
220
  if (Array.isArray(photoIds) && photoIds.length) {
218
221
  photoIds.forEach((id, idx) => {
219
222
  form.append(`attached_media[${idx}]`, JSON.stringify({ media_fbid: id }));
@@ -252,7 +255,7 @@ async function postToFacebook(ctx) {
252
255
  if (id) photoIds.push(id);
253
256
  }
254
257
 
255
- return publishFeedPost({ message, link, photoIds });
258
+ return publishFeedPost({ message, link, photoIds, placeId: trimStr(ctx.post.fbPlaceId) });
256
259
  }
257
260
 
258
261
  const Plugin = {};
@@ -300,12 +303,43 @@ Plugin.init = async function (params) {
300
303
  const allowedGroups = parseAllowedGroupsList();
301
304
  if (!allowedGroups.length) return res.json({ allowed: false, reason: 'no_groups_configured' });
302
305
  const ok = await userIsAllowed(uid);
303
- return res.json({ allowed: ok, reason: ok ? 'ok' : 'not_in_group' });
306
+ return res.json({ allowed: ok, reason: ok ? 'ok' : 'not_in_group', enablePlaceTagging: settings.enablePlaceTagging });
304
307
  } catch {
305
308
  return res.json({ allowed: false, reason: 'error' });
306
309
  }
307
310
  });
308
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
+ });
309
343
 
310
344
  };
311
345
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-facebook-post",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
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": {
@@ -2,6 +2,8 @@
2
2
  'use strict';
3
3
 
4
4
  (function () {
5
+ const DEBOUNCE_MS = 400;
6
+
5
7
  async function canPost() {
6
8
  try {
7
9
  const res = await fetch('/api/facebook-post/can-post?_=' + Date.now(), {
@@ -10,51 +12,164 @@
10
12
  });
11
13
  if (!res.ok) return { allowed: false };
12
14
  return await res.json();
13
- } catch (err) {
15
+ } catch {
14
16
  return { allowed: false };
15
17
  }
16
18
  }
17
19
 
18
- function injectUI($composer) {
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) {
19
42
  if ($composer.find('[data-fbpost-btn]').length) return;
20
43
 
21
- const $btn = $(`
22
- <button type="button" class="btn btn-sm btn-link" data-fbpost-btn
23
- title="Publier ce topic sur Facebook (cliquer pour activer)"
24
- style="opacity:0.4; color:#1877F2; font-size:1.2em; padding:2px 6px;">
25
- <i class="fab fa-facebook-f"></i>
26
- </button>
44
+ const showPlace = !!perm.enablePlaceTagging;
45
+
46
+ // ── Bouton dans la toolbar même structure que les boutons natifs ──
47
+ const $li = $(`
48
+ <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"
50
+ aria-label="Publier sur Facebook" style="opacity:0.45;">
51
+ <i class="fab fa-facebook-f"></i>
52
+ </button>
53
+ </li>
27
54
  `);
55
+ const $btn = $li.find('button');
28
56
 
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 ──
29
84
  $btn.on('click', function () {
30
- const active = $(this).data('fbpost-active');
31
- $(this).data('fbpost-active', !active)
32
- .attr('title', !active
33
- ? 'Publier sur Facebook (actif — cliquer pour désactiver)'
34
- : 'Publier ce topic sur Facebook (cliquer pour activer)')
35
- .css('opacity', !active ? '1' : '0.4');
85
+ const active = !$(this).data('fbpost-active');
86
+ $(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
+ }
36
95
  });
37
96
 
38
- // Injecter dans la barre d'outils du composeur
39
- const $toolbar = $composer.find('.toolbar, .formatting-bar, [data-toolbar]').first();
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
+ const $toolbar = $composer.find('ul.formatting-group').first();
40
152
  if ($toolbar.length) {
41
- // Si la toolbar contient des <li>, on enveloppe
42
- if ($toolbar.find('li').length) {
43
- $toolbar.append($('<li>').append($btn));
44
- } else {
45
- $toolbar.append($btn);
46
- }
153
+ $toolbar.append($li);
154
+ $toolbar.after($placePanel);
47
155
  } else {
48
156
  // Fallback : avant la zone de texte
49
- $composer.find('.write').first().before($btn);
157
+ const $write = $composer.find('.write').first();
158
+ $write.before($placePanel);
159
+ $placePanel.before($li);
50
160
  }
51
161
 
162
+ // ── Hook submit ──
52
163
  $(window).off('action:composer.submit.fbpost')
53
164
  .on('action:composer.submit.fbpost', function (ev2, data) {
54
- const enabled = !!$composer.find('[data-fbpost-btn]').data('fbpost-active');
165
+ const enabled = !!$btn.data('fbpost-active');
55
166
  const postData = (data && data.composerData) || (data && data.postData) || data;
56
167
  if (!postData || typeof postData !== 'object') return;
57
168
  postData.fbPostEnabled = enabled;
169
+ if (enabled && showPlace) {
170
+ const placeId = $placePanel.find('.fb-place-id').val();
171
+ if (placeId) postData.fbPlaceId = placeId;
172
+ }
58
173
  });
59
174
  }
60
175
 
@@ -62,12 +177,10 @@
62
177
  try {
63
178
  const $composer = data && data.composer ? $(data.composer) : $('.composer');
64
179
  if (!$composer || !$composer.length) return;
65
-
66
180
  const perm = await canPost();
67
181
  if (!perm.allowed) return;
68
-
69
- injectUI($composer);
70
- } catch (err) {
182
+ injectUI($composer, perm);
183
+ } catch {
71
184
  // ignore
72
185
  }
73
186
  });