nodebb-plugin-facebook-post 1.0.16 → 1.0.18
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/CLAUDE.md +68 -0
- package/library.js +23 -138
- package/package.json +1 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
NodeBB 4.x plugin that automatically publishes new forum topics to a fixed Facebook Page (association page). Only the first post of a topic is ever published, and only when the user opts in via a composer checkbox.
|
|
8
|
+
|
|
9
|
+
## Installation & Deployment
|
|
10
|
+
|
|
11
|
+
There is no build step for this plugin. NodeBB handles asset bundling.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# After modifying the plugin, copy it into the NodeBB instance and rebuild:
|
|
15
|
+
./nodebb build
|
|
16
|
+
./nodebb restart
|
|
17
|
+
|
|
18
|
+
# Activate via: ACP → Plugins → Facebook Post
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Environment variables (required at NodeBB startup):**
|
|
22
|
+
- `NODEBB_FB_PAGE_ID` – Facebook Page ID
|
|
23
|
+
- `NODEBB_FB_PAGE_ACCESS_TOKEN` – Page access token
|
|
24
|
+
- `NODEBB_FB_GRAPH_VERSION` – optional, defaults to `v25.0`
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
### File Map
|
|
29
|
+
|
|
30
|
+
| File | Role |
|
|
31
|
+
|------|------|
|
|
32
|
+
| `library.js` | Server-side plugin entry point (Node.js) |
|
|
33
|
+
| `plugin.json` | NodeBB manifest: hooks, static dirs, client scripts |
|
|
34
|
+
| `static/lib/composer.js` | Client JS injected into the NodeBB post composer |
|
|
35
|
+
| `static/lib/admin.js` | Client JS for the ACP settings page |
|
|
36
|
+
| `static/templates/admin/facebook-post.tpl` | NodeBB Benchpress template for ACP page |
|
|
37
|
+
|
|
38
|
+
### Hook Flow
|
|
39
|
+
|
|
40
|
+
1. **`static:app.load` → `Plugin.init`**
|
|
41
|
+
Loads settings, registers Express routes on the NodeBB router:
|
|
42
|
+
- `GET /admin/plugins/facebook-post` – ACP page (HTML)
|
|
43
|
+
- `GET /api/admin/plugins/facebook-post` – ACP page (JSON, for ajaxify)
|
|
44
|
+
- `GET /api/facebook-post/can-post` – called by composer.js to check if the logged-in user may see the Facebook UI
|
|
45
|
+
|
|
46
|
+
2. **`filter:post.create` → `Plugin.onPostCreate`**
|
|
47
|
+
Copies `fbPostEnabled` and `fbPlaceId` from the composer submit payload into the post object so they survive into the next hook.
|
|
48
|
+
|
|
49
|
+
3. **`action:post.save` → `Plugin.onPostSave`**
|
|
50
|
+
Main publication logic: validates guards, calls Facebook Graph API via `postToFacebook()`. Stores `fbPostedId` and `fbPostedAt` on the post to prevent double-posting.
|
|
51
|
+
|
|
52
|
+
### Settings Split
|
|
53
|
+
|
|
54
|
+
- **Sensitive** (env vars, never stored in DB): `fbPageId`, `fbPageAccessToken`, `fbGraphVersion`
|
|
55
|
+
- **Non-sensitive** (stored in NodeBB DB via `meta.settings` with key `facebook-post`): `enabled`, `includeExcerpt`, `excerptMaxLen`, `categoriesWhitelist`, `minimumReputation`, `maxImages`, `enablePlaceTagging`, `allowedGroups`
|
|
56
|
+
|
|
57
|
+
Both are merged in `loadSettings()`, which is called on each relevant request to pick up live DB changes without restart.
|
|
58
|
+
|
|
59
|
+
### Facebook Publication Flow (`postToFacebook`)
|
|
60
|
+
|
|
61
|
+
1. Extract image URLs from post content (Markdown `![]()`, HTML `<img>`, bare URLs) — only forum-hosted images pass the `isForumHosted()` filter.
|
|
62
|
+
2. Upload each image to `/{pageId}/photos` with `published=false` to get photo IDs.
|
|
63
|
+
3. POST to `/{pageId}/feed` with `attached_media[]`, optional `place`, and `link`.
|
|
64
|
+
|
|
65
|
+
### Client-Side (composer.js)
|
|
66
|
+
|
|
67
|
+
Listens to `action:composer.loaded`. Calls `/api/facebook-post/can-post`; if allowed, injects a checkbox and optional Place ID field into the composer. Hooks into `filter:composer.submit.fbpost` to append `fbPostEnabled` and `fbPlaceId` to the submit payload.
|
|
68
|
+
|
package/library.js
CHANGED
|
@@ -4,26 +4,16 @@ const axios = require('axios');
|
|
|
4
4
|
|
|
5
5
|
let meta = {};
|
|
6
6
|
let settings = {};
|
|
7
|
-
let appRef;
|
|
8
7
|
|
|
9
8
|
const SETTINGS_KEY = 'facebook-post';
|
|
10
9
|
|
|
11
|
-
// Non-sensitive settings kept in ACP DB
|
|
12
10
|
const DEFAULT_SETTINGS = {
|
|
13
11
|
enabled: false,
|
|
14
|
-
|
|
15
|
-
// Behaviour
|
|
16
12
|
includeExcerpt: true,
|
|
17
13
|
excerptMaxLen: 220,
|
|
18
|
-
|
|
19
|
-
// Filters
|
|
20
|
-
categoriesWhitelist: '', // comma-separated cids, empty = all
|
|
14
|
+
categoriesWhitelist: '',
|
|
21
15
|
minimumReputation: 0,
|
|
22
|
-
|
|
23
|
-
// Image handling
|
|
24
16
|
maxImages: 4,
|
|
25
|
-
|
|
26
|
-
// Location
|
|
27
17
|
enablePlaceTagging: true,
|
|
28
18
|
};
|
|
29
19
|
|
|
@@ -44,16 +34,11 @@ function parseCsvInts(csv) {
|
|
|
44
34
|
}
|
|
45
35
|
|
|
46
36
|
function readEnv() {
|
|
47
|
-
const env = process.env
|
|
48
|
-
// Fixed Facebook page target (association page)
|
|
37
|
+
const env = process.env;
|
|
49
38
|
return {
|
|
50
39
|
fbGraphVersion: trimStr(env.NODEBB_FB_GRAPH_VERSION) || 'v25.0',
|
|
51
40
|
fbPageId: trimStr(env.NODEBB_FB_PAGE_ID),
|
|
52
41
|
fbPageAccessToken: trimStr(env.NODEBB_FB_PAGE_ACCESS_TOKEN),
|
|
53
|
-
|
|
54
|
-
// Restrict availability to NodeBB groups (comma-separated)
|
|
55
|
-
// If empty => nobody can use Facebook posting
|
|
56
|
-
allowedGroups: trimStr(env.NODEBB_FB_ALLOWED_GROUPS || ''),
|
|
57
42
|
};
|
|
58
43
|
}
|
|
59
44
|
|
|
@@ -62,28 +47,24 @@ async function loadSettings() {
|
|
|
62
47
|
settings = Object.assign({}, DEFAULT_SETTINGS, raw || {});
|
|
63
48
|
|
|
64
49
|
settings.enabled = bool(settings.enabled);
|
|
65
|
-
|
|
66
50
|
settings.includeExcerpt = bool(settings.includeExcerpt);
|
|
67
51
|
settings.excerptMaxLen = int(settings.excerptMaxLen, DEFAULT_SETTINGS.excerptMaxLen);
|
|
68
|
-
|
|
69
52
|
settings.minimumReputation = int(settings.minimumReputation, 0);
|
|
70
53
|
settings.maxImages = int(settings.maxImages, DEFAULT_SETTINGS.maxImages);
|
|
71
54
|
settings.enablePlaceTagging = bool(settings.enablePlaceTagging);
|
|
72
|
-
|
|
73
55
|
settings.categoriesWhitelist = trimStr(settings.categoriesWhitelist);
|
|
74
56
|
|
|
75
|
-
// ENV overrides / secrets
|
|
76
57
|
const env = readEnv();
|
|
77
58
|
settings.fbGraphVersion = env.fbGraphVersion;
|
|
78
59
|
settings.fbPageId = env.fbPageId;
|
|
79
60
|
settings.fbPageAccessToken = env.fbPageAccessToken;
|
|
80
|
-
settings.allowedGroups = env.allowedGroups;
|
|
81
61
|
}
|
|
82
62
|
|
|
83
63
|
function getForumBaseUrl() {
|
|
84
64
|
const nconf = require.main.require('nconf');
|
|
85
65
|
return trimStr(nconf.get('url')) || '';
|
|
86
66
|
}
|
|
67
|
+
|
|
87
68
|
function absolutizeUrl(url) {
|
|
88
69
|
const base = getForumBaseUrl();
|
|
89
70
|
if (!url) return '';
|
|
@@ -91,14 +72,15 @@ function absolutizeUrl(url) {
|
|
|
91
72
|
if (url.startsWith('//')) return `https:${url}`;
|
|
92
73
|
if (!base) return url;
|
|
93
74
|
if (url.startsWith('/')) return base + url;
|
|
94
|
-
return base
|
|
75
|
+
return `${base}/${url}`;
|
|
95
76
|
}
|
|
77
|
+
|
|
96
78
|
function isForumHosted(urlAbs) {
|
|
97
79
|
const base = getForumBaseUrl();
|
|
98
80
|
if (!base) return false;
|
|
99
81
|
try {
|
|
100
82
|
return new URL(urlAbs).host === new URL(base).host;
|
|
101
|
-
} catch
|
|
83
|
+
} catch {
|
|
102
84
|
return false;
|
|
103
85
|
}
|
|
104
86
|
}
|
|
@@ -107,12 +89,10 @@ function extractImageUrlsFromContent(rawContent) {
|
|
|
107
89
|
const urls = new Set();
|
|
108
90
|
if (!rawContent) return [];
|
|
109
91
|
|
|
110
|
-
// Markdown images
|
|
111
92
|
const mdImg = /!\[[^\]]*]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
112
93
|
let m;
|
|
113
94
|
while ((m = mdImg.exec(rawContent)) !== null) urls.add(m[1]);
|
|
114
95
|
|
|
115
|
-
// HTML images
|
|
116
96
|
const htmlImg = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
|
|
117
97
|
while ((m = htmlImg.exec(rawContent)) !== null) urls.add(m[1]);
|
|
118
98
|
|
|
@@ -132,25 +112,7 @@ function sanitizeExcerpt(text, maxLen) {
|
|
|
132
112
|
.replace(/<[^>]*>/g, '');
|
|
133
113
|
if (!s) return '';
|
|
134
114
|
if (s.length <= maxLen) return s;
|
|
135
|
-
return s.slice(0,
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function normalizeAllowedGroupsValue(value) {
|
|
139
|
-
if (!value) return '';
|
|
140
|
-
if (Array.isArray(value)) {
|
|
141
|
-
return value.map(String).map(v => v.trim()).filter(Boolean).join(',');
|
|
142
|
-
}
|
|
143
|
-
const s = String(value).trim();
|
|
144
|
-
if (!s) return '';
|
|
145
|
-
if (s.startsWith('[')) {
|
|
146
|
-
try {
|
|
147
|
-
const parsed = JSON.parse(s);
|
|
148
|
-
if (Array.isArray(parsed)) {
|
|
149
|
-
return parsed.map(String).map(v => v.trim()).filter(Boolean).join(',');
|
|
150
|
-
}
|
|
151
|
-
} catch (_) {}
|
|
152
|
-
}
|
|
153
|
-
return s;
|
|
115
|
+
return s.slice(0, maxLen - 1).trimEnd() + '\u2026';
|
|
154
116
|
}
|
|
155
117
|
|
|
156
118
|
function parseAllowedGroupsList() {
|
|
@@ -159,7 +121,6 @@ function parseAllowedGroupsList() {
|
|
|
159
121
|
return s.split(',').map(v => v.trim()).filter(Boolean);
|
|
160
122
|
}
|
|
161
123
|
|
|
162
|
-
|
|
163
124
|
function getUidFromReq(req) {
|
|
164
125
|
return req && (req.uid || (req.user && req.user.uid) || (req.session && req.session.uid));
|
|
165
126
|
}
|
|
@@ -170,14 +131,11 @@ async function userIsAllowed(uid) {
|
|
|
170
131
|
|
|
171
132
|
const Groups = require.main.require('./src/groups');
|
|
172
133
|
for (const groupName of allowed) {
|
|
173
|
-
// Group names are case-sensitive in NodeBB
|
|
174
|
-
// If any match => allowed
|
|
175
|
-
// groups.isMember(uid, groupName) returns boolean
|
|
176
134
|
try {
|
|
177
135
|
// eslint-disable-next-line no-await-in-loop
|
|
178
136
|
const ok = await Groups.isMember(uid, groupName);
|
|
179
137
|
if (ok) return true;
|
|
180
|
-
} catch
|
|
138
|
+
} catch {
|
|
181
139
|
// ignore
|
|
182
140
|
}
|
|
183
141
|
}
|
|
@@ -207,36 +165,23 @@ async function getPostContext(postData) {
|
|
|
207
165
|
|
|
208
166
|
function shouldProcessPost(ctx) {
|
|
209
167
|
if (!ctx || !ctx.post || !ctx.topic || !ctx.user) return false;
|
|
210
|
-
|
|
211
|
-
// Plugin enabled
|
|
212
168
|
if (!settings.enabled) return false;
|
|
213
|
-
|
|
214
|
-
// Secrets set
|
|
215
169
|
if (!settings.fbPageId || !settings.fbPageAccessToken) return false;
|
|
216
170
|
|
|
217
|
-
// Only first post of topic
|
|
218
171
|
const isFirstPost = (ctx.post.isMainPost === true) || (ctx.post.index === 0);
|
|
219
172
|
if (!isFirstPost) return false;
|
|
220
173
|
|
|
221
|
-
// User opted-in on composer checkbox
|
|
222
174
|
if (!bool(ctx.post.fbPostEnabled)) return false;
|
|
223
|
-
|
|
224
|
-
// reputation filter
|
|
225
175
|
if ((ctx.user.reputation || 0) < settings.minimumReputation) return false;
|
|
226
176
|
|
|
227
|
-
// category whitelist
|
|
228
177
|
const whitelist = parseCsvInts(settings.categoriesWhitelist);
|
|
229
|
-
if (whitelist.length > 0)
|
|
230
|
-
const cid = parseInt(ctx.topic.cid, 10);
|
|
231
|
-
if (!whitelist.includes(cid)) return false;
|
|
232
|
-
}
|
|
178
|
+
if (whitelist.length > 0 && !whitelist.includes(parseInt(ctx.topic.cid, 10))) return false;
|
|
233
179
|
|
|
234
180
|
return true;
|
|
235
181
|
}
|
|
236
182
|
|
|
237
183
|
async function uploadPhotoToFacebook(urlAbs) {
|
|
238
184
|
const endpoint = `https://graph.facebook.com/${settings.fbGraphVersion}/${settings.fbPageId}/photos`;
|
|
239
|
-
|
|
240
185
|
const resp = await axios.post(endpoint, null, {
|
|
241
186
|
params: {
|
|
242
187
|
url: urlAbs,
|
|
@@ -245,7 +190,6 @@ async function uploadPhotoToFacebook(urlAbs) {
|
|
|
245
190
|
},
|
|
246
191
|
timeout: 20000,
|
|
247
192
|
});
|
|
248
|
-
|
|
249
193
|
return resp.data && resp.data.id;
|
|
250
194
|
}
|
|
251
195
|
|
|
@@ -255,10 +199,8 @@ async function publishFeedPost({ message, link, photoIds, placeId }) {
|
|
|
255
199
|
const form = new URLSearchParams();
|
|
256
200
|
form.append('message', String(message || ''));
|
|
257
201
|
form.append('access_token', String(settings.fbPageAccessToken));
|
|
258
|
-
|
|
259
202
|
if (link) form.append('link', String(link));
|
|
260
203
|
if (placeId) form.append('place', String(placeId));
|
|
261
|
-
|
|
262
204
|
if (Array.isArray(photoIds) && photoIds.length) {
|
|
263
205
|
photoIds.forEach((id, idx) => {
|
|
264
206
|
form.append(`attached_media[${idx}]`, JSON.stringify({ media_fbid: id }));
|
|
@@ -269,42 +211,28 @@ async function publishFeedPost({ message, link, photoIds, placeId }) {
|
|
|
269
211
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
270
212
|
timeout: 20000,
|
|
271
213
|
});
|
|
272
|
-
|
|
273
214
|
return resp.data && resp.data.id;
|
|
274
215
|
}
|
|
275
216
|
|
|
276
217
|
async function postToFacebook(ctx) {
|
|
277
218
|
const rawContent = ctx.post.content || '';
|
|
278
219
|
const topicTitle = ctx.topic.title || 'Nouveau sujet';
|
|
279
|
-
const author = ctx.user.username || 'Quelqu
|
|
220
|
+
const author = ctx.user.username || 'Quelqu\u2019un';
|
|
280
221
|
const link = ctx.topicUrlAbs;
|
|
281
222
|
|
|
282
223
|
const excerpt = settings.includeExcerpt ? sanitizeExcerpt(rawContent, settings.excerptMaxLen) : '';
|
|
283
224
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
.
|
|
287
|
-
.filter(isForumHosted);
|
|
225
|
+
const imageUrls = extractImageUrlsFromContent(rawContent)
|
|
226
|
+
.filter(isForumHosted)
|
|
227
|
+
.slice(0, Math.max(0, settings.maxImages));
|
|
288
228
|
|
|
289
|
-
|
|
229
|
+
const placeId = (settings.enablePlaceTagging && trimStr(ctx.post.fbPlaceId)) || null;
|
|
290
230
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const lines = [];
|
|
299
|
-
lines.push(`📝 ${topicTitle}`);
|
|
300
|
-
if (excerpt) lines.push(`\n${excerpt}`);
|
|
301
|
-
lines.push(`\n🔗 ${link}`);
|
|
302
|
-
lines.push(`\n— ${author}`);
|
|
303
|
-
const message = lines.join('\n');
|
|
304
|
-
|
|
305
|
-
if (!imageUrls.length) {
|
|
306
|
-
return await publishFeedPost({ message, link, photoIds: [], placeId });
|
|
307
|
-
}
|
|
231
|
+
const lines = [`\uD83D\uDCDD ${topicTitle}`];
|
|
232
|
+
if (excerpt) lines.push(excerpt);
|
|
233
|
+
lines.push(`\uD83D\uDD17 ${link}`);
|
|
234
|
+
lines.push(`\u2014 ${author}`);
|
|
235
|
+
const message = lines.join('\n\n');
|
|
308
236
|
|
|
309
237
|
const photoIds = [];
|
|
310
238
|
for (const urlAbs of imageUrls) {
|
|
@@ -313,18 +241,16 @@ async function postToFacebook(ctx) {
|
|
|
313
241
|
if (id) photoIds.push(id);
|
|
314
242
|
}
|
|
315
243
|
|
|
316
|
-
return
|
|
244
|
+
return publishFeedPost({ message, link, photoIds, placeId });
|
|
317
245
|
}
|
|
318
246
|
|
|
319
247
|
const Plugin = {};
|
|
320
248
|
|
|
321
249
|
Plugin.init = async function (params) {
|
|
322
|
-
// NodeBB v4.9.x: params provides { router, middleware, meta, ... }
|
|
323
250
|
const { router, middleware } = params;
|
|
324
251
|
|
|
325
252
|
meta = (params && params.meta) ? params.meta : require.main.require('./src/meta');
|
|
326
253
|
|
|
327
|
-
// Used to render group list in ACP
|
|
328
254
|
const groups = require.main.require('./src/groups');
|
|
329
255
|
const db = require.main.require('./src/database');
|
|
330
256
|
|
|
@@ -335,11 +261,9 @@ Plugin.init = async function (params) {
|
|
|
335
261
|
if (!names?.length) {
|
|
336
262
|
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
337
263
|
}
|
|
338
|
-
|
|
339
264
|
const data = await groups.getGroupsData(
|
|
340
265
|
(names || []).filter(name => !groups.isPrivilegeGroup(name))
|
|
341
266
|
);
|
|
342
|
-
|
|
343
267
|
return (data || [])
|
|
344
268
|
.filter(g => g?.name && g.name.toLowerCase() !== 'guests')
|
|
345
269
|
.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
@@ -366,42 +290,10 @@ Plugin.init = async function (params) {
|
|
|
366
290
|
if (!allowedGroups.length) return res.json({ allowed: false, reason: 'no_groups_configured' });
|
|
367
291
|
const ok = await userIsAllowed(uid);
|
|
368
292
|
return res.json({ allowed: ok, reason: ok ? 'ok' : 'not_in_group' });
|
|
369
|
-
} catch
|
|
293
|
+
} catch {
|
|
370
294
|
return res.json({ allowed: false, reason: 'error' });
|
|
371
295
|
}
|
|
372
296
|
});
|
|
373
|
-
} catch (err) {
|
|
374
|
-
return res.json({ allowed: false });
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
router.post('/api/admin/plugins/facebook-post/mark-posted', middleware.ensureLoggedIn, async (req, res) => {
|
|
379
|
-
try {
|
|
380
|
-
await loadSettings();
|
|
381
|
-
|
|
382
|
-
const secret = trimStr(req.body && req.body.secret);
|
|
383
|
-
if (!secret || secret !== settings.workerSecret) {
|
|
384
|
-
return res.status(403).json({ error: 'bad_secret' });
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const ok = await ensureAdmin(req);
|
|
388
|
-
if (!ok) return res.status(403).json({ error: 'forbidden' });
|
|
389
|
-
|
|
390
|
-
const pid = parseInt(req.body && req.body.pid, 10);
|
|
391
|
-
const fbPostedId = trimStr(req.body && req.body.fbPostedId);
|
|
392
|
-
const fbPostedAt = parseInt(req.body && req.body.fbPostedAt, 10) || Date.now();
|
|
393
|
-
if (!Number.isFinite(pid) || !fbPostedId) return res.status(400).json({ error: 'bad_payload' });
|
|
394
|
-
|
|
395
|
-
const Posts = require.main.require('./src/posts');
|
|
396
|
-
await Posts.setPostField(pid, 'fbPostedId', fbPostedId);
|
|
397
|
-
await Posts.setPostField(pid, 'fbPostedAt', fbPostedAt);
|
|
398
|
-
await Posts.deletePostField(pid, 'fbQueueJobId');
|
|
399
|
-
await Posts.deletePostField(pid, 'fbQueuedAt');
|
|
400
|
-
return res.json({ ok: true });
|
|
401
|
-
} catch (err) {
|
|
402
|
-
return res.status(500).json({ error: 'failed' });
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
297
|
};
|
|
406
298
|
|
|
407
299
|
Plugin.addAdminNavigation = async function (header) {
|
|
@@ -414,17 +306,13 @@ Plugin.addAdminNavigation = async function (header) {
|
|
|
414
306
|
return header;
|
|
415
307
|
};
|
|
416
308
|
|
|
417
|
-
|
|
418
|
-
// Save custom composer data into post fields
|
|
419
309
|
Plugin.onPostCreate = async function (hookData) {
|
|
420
310
|
try {
|
|
421
311
|
if (!hookData?.post || !hookData?.data) return hookData;
|
|
422
|
-
|
|
423
312
|
hookData.post.fbPostEnabled = bool(hookData.data.fbPostEnabled);
|
|
424
|
-
|
|
425
313
|
const fbPlaceId = trimStr(hookData.data.fbPlaceId);
|
|
426
314
|
if (fbPlaceId) hookData.post.fbPlaceId = fbPlaceId;
|
|
427
|
-
} catch
|
|
315
|
+
} catch {
|
|
428
316
|
// swallow
|
|
429
317
|
}
|
|
430
318
|
return hookData;
|
|
@@ -439,7 +327,6 @@ Plugin.onPostSave = async function (hookData) {
|
|
|
439
327
|
const ctx = await getPostContext(hookData && hookData.post ? hookData.post : hookData);
|
|
440
328
|
if (!ctx) return;
|
|
441
329
|
|
|
442
|
-
// Allowed group check (server-side enforcement)
|
|
443
330
|
const allowed = await userIsAllowed(ctx.post.uid);
|
|
444
331
|
if (!allowed) return;
|
|
445
332
|
|
|
@@ -447,7 +334,6 @@ Plugin.onPostSave = async function (hookData) {
|
|
|
447
334
|
|
|
448
335
|
const Posts = require.main.require('./src/posts');
|
|
449
336
|
|
|
450
|
-
// Avoid double-post
|
|
451
337
|
const already = await Posts.getPostField(ctx.post.pid, 'fbPostedId');
|
|
452
338
|
if (already) return;
|
|
453
339
|
|
|
@@ -457,10 +343,9 @@ Plugin.onPostSave = async function (hookData) {
|
|
|
457
343
|
await Posts.setPostField(ctx.post.pid, 'fbPostedAt', Date.now());
|
|
458
344
|
}
|
|
459
345
|
} catch (e) {
|
|
460
|
-
const detail = e
|
|
346
|
+
const detail = e?.response ? JSON.stringify(e.response.data) : (e?.message || e);
|
|
461
347
|
winston.error(`[facebook-post] Failed: ${detail}`);
|
|
462
348
|
}
|
|
463
349
|
};
|
|
464
350
|
|
|
465
351
|
module.exports = Plugin;
|
|
466
|
-
|
package/package.json
CHANGED