nodebb-plugin-facebook-post 1.0.30 → 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 +49 -5
- 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;
|
|
@@ -73,16 +75,33 @@ function absolutizeUrl(url) {
|
|
|
73
75
|
return `${base}/${url}`;
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
function getApexDomain(hostname) {
|
|
79
|
+
const parts = hostname.split('.');
|
|
80
|
+
return parts.length <= 2 ? hostname : parts.slice(-2).join('.');
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
function isForumHosted(urlAbs) {
|
|
77
84
|
const base = getForumBaseUrl();
|
|
78
85
|
if (!base) return false;
|
|
79
86
|
try {
|
|
80
|
-
|
|
87
|
+
const imgHost = new URL(urlAbs).host;
|
|
88
|
+
const forumHost = new URL(base).host;
|
|
89
|
+
return imgHost === forumHost || getApexDomain(imgHost) === getApexDomain(forumHost);
|
|
81
90
|
} catch {
|
|
82
91
|
return false;
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
|
|
95
|
+
function decodeHtmlEntities(str) {
|
|
96
|
+
return str
|
|
97
|
+
.replace(/&/g, '&')
|
|
98
|
+
.replace(/</g, '<')
|
|
99
|
+
.replace(/>/g, '>')
|
|
100
|
+
.replace(/"/g, '"')
|
|
101
|
+
.replace(/&#x([0-9a-fA-F]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
102
|
+
.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(Number(dec)));
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
function extractImageUrlsFromContent(rawContent) {
|
|
87
106
|
const urls = new Set();
|
|
88
107
|
if (!rawContent) return [];
|
|
@@ -190,13 +209,14 @@ async function uploadPhotoToFacebook(urlAbs) {
|
|
|
190
209
|
return resp.data && resp.data.id;
|
|
191
210
|
}
|
|
192
211
|
|
|
193
|
-
async function publishFeedPost({ message, link, photoIds }) {
|
|
212
|
+
async function publishFeedPost({ message, link, photoIds, placeId }) {
|
|
194
213
|
const endpoint = `https://graph.facebook.com/${settings.fbGraphVersion}/${settings.fbPageId}/feed`;
|
|
195
214
|
|
|
196
215
|
const form = new URLSearchParams();
|
|
197
216
|
form.append('message', String(message || ''));
|
|
198
217
|
form.append('access_token', String(settings.fbPageAccessToken));
|
|
199
218
|
if (link) form.append('link', String(link));
|
|
219
|
+
if (placeId) form.append('place', String(placeId));
|
|
200
220
|
if (Array.isArray(photoIds) && photoIds.length) {
|
|
201
221
|
photoIds.forEach((id, idx) => {
|
|
202
222
|
form.append(`attached_media[${idx}]`, JSON.stringify({ media_fbid: id }));
|
|
@@ -212,7 +232,7 @@ async function publishFeedPost({ message, link, photoIds }) {
|
|
|
212
232
|
|
|
213
233
|
async function postToFacebook(ctx) {
|
|
214
234
|
const rawContent = ctx.post.content || '';
|
|
215
|
-
const topicTitle = ctx.topic.title || 'Nouveau sujet';
|
|
235
|
+
const topicTitle = decodeHtmlEntities(ctx.topic.title || 'Nouveau sujet');
|
|
216
236
|
const author = ctx.user.username || 'Quelqu\u2019un';
|
|
217
237
|
const link = ctx.topicUrlAbs;
|
|
218
238
|
|
|
@@ -235,7 +255,7 @@ async function postToFacebook(ctx) {
|
|
|
235
255
|
if (id) photoIds.push(id);
|
|
236
256
|
}
|
|
237
257
|
|
|
238
|
-
return publishFeedPost({ message, link, photoIds });
|
|
258
|
+
return publishFeedPost({ message, link, photoIds, placeId: trimStr(ctx.post.fbPlaceId) });
|
|
239
259
|
}
|
|
240
260
|
|
|
241
261
|
const Plugin = {};
|
|
@@ -283,12 +303,36 @@ Plugin.init = async function (params) {
|
|
|
283
303
|
const allowedGroups = parseAllowedGroupsList();
|
|
284
304
|
if (!allowedGroups.length) return res.json({ allowed: false, reason: 'no_groups_configured' });
|
|
285
305
|
const ok = await userIsAllowed(uid);
|
|
286
|
-
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 });
|
|
287
307
|
} catch {
|
|
288
308
|
return res.json({ allowed: false, reason: 'error' });
|
|
289
309
|
}
|
|
290
310
|
});
|
|
291
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
|
+
});
|
|
292
336
|
|
|
293
337
|
};
|
|
294
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
|
});
|