nodebb-plugin-facebook-post 1.0.31 → 1.0.32
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 +30 -3
- package/package.json +1 -1
- package/static/lib/composer.js +141 -28
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,36 @@ 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
|
+
res.set('Cache-Control', 'no-store');
|
|
314
|
+
try {
|
|
315
|
+
await loadSettings();
|
|
316
|
+
if (!settings.enabled || !settings.enablePlaceTagging || !settings.fbPageAccessToken) {
|
|
317
|
+
return res.json({ results: [] });
|
|
318
|
+
}
|
|
319
|
+
const q = trimStr(req.query.q);
|
|
320
|
+
if (q.length < 2) return res.json({ results: [] });
|
|
321
|
+
const resp = await axios.get(`https://graph.facebook.com/${settings.fbGraphVersion}/search`, {
|
|
322
|
+
params: {
|
|
323
|
+
type: 'place',
|
|
324
|
+
q,
|
|
325
|
+
fields: 'name,location,id',
|
|
326
|
+
access_token: settings.fbPageAccessToken,
|
|
327
|
+
limit: 8,
|
|
328
|
+
},
|
|
329
|
+
timeout: 8000,
|
|
330
|
+
});
|
|
331
|
+
return res.json({ results: (resp.data && resp.data.data) || [] });
|
|
332
|
+
} catch {
|
|
333
|
+
return res.json({ results: [] });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
309
336
|
|
|
310
337
|
};
|
|
311
338
|
|
package/package.json
CHANGED
package/static/lib/composer.js
CHANGED
|
@@ -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
|
|
15
|
+
} catch {
|
|
14
16
|
return { allowed: false };
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
function
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
<i class="fab fa-facebook-f" style="color:#1877F2; flex-shrink:0;"></i>
|
|
62
|
+
<span class="text-muted">Publier sur Facebook</span>
|
|
63
|
+
${showPlace ? `
|
|
64
|
+
<div class="position-relative flex-grow-1 ms-2">
|
|
65
|
+
<input type="text" class="form-control form-control-sm fb-place-search"
|
|
66
|
+
placeholder="Lieu (optionnel)…" 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 =
|
|
31
|
-
$(this).data('fbpost-active',
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
//
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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()
|
|
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 = !!$
|
|
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
|
-
|
|
70
|
-
} catch (err) {
|
|
182
|
+
injectUI($composer, perm);
|
|
183
|
+
} catch {
|
|
71
184
|
// ignore
|
|
72
185
|
}
|
|
73
186
|
});
|