nodebb-plugin-onekite-calendar 2.0.66 → 2.0.67
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/lib/admin.js +17 -115
- package/lib/api.js +57 -186
- package/lib/db.js +6 -3
- package/lib/discord.js +2 -7
- package/lib/helloassoWebhook.js +20 -49
- package/lib/nodebb-helpers.js +11 -174
- package/lib/scheduler.js +33 -22
- package/lib/shared.js +427 -0
- package/lib/utils.js +10 -65
- package/lib/widgets.js +2 -6
- package/package.json +1 -1
package/lib/shared.js
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Centralised helpers used across api.js, admin.js, helloassoWebhook.js,
|
|
5
|
+
* scheduler.js, discord.js and widgets.js.
|
|
6
|
+
*
|
|
7
|
+
* Previously these were duplicated (sometimes with subtle divergences) in
|
|
8
|
+
* multiple files. This module is the single source of truth.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const nconf = require.main.require('nconf');
|
|
13
|
+
const meta = require.main.require('./src/meta');
|
|
14
|
+
const emailer = require.main.require('./src/emailer');
|
|
15
|
+
const groups = require.main.require('./src/groups');
|
|
16
|
+
const user = require.main.require('./src/user');
|
|
17
|
+
|
|
18
|
+
const { getGroupNameBySlug } = require('./group-helpers');
|
|
19
|
+
|
|
20
|
+
// ─── Base URL ────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function forumBaseUrl() {
|
|
23
|
+
// Try meta.config first (runtime-accurate), then nconf fallback.
|
|
24
|
+
let base = '';
|
|
25
|
+
try {
|
|
26
|
+
base = (meta && meta.config && (meta.config.url || meta.config['url']))
|
|
27
|
+
? String(meta.config.url || meta.config['url'])
|
|
28
|
+
: '';
|
|
29
|
+
} catch (_) { /* ignore */ }
|
|
30
|
+
if (!base) {
|
|
31
|
+
try { base = String(nconf.get('url') || ''); } catch (_) { /* ignore */ }
|
|
32
|
+
}
|
|
33
|
+
base = base.trim().replace(/\/$/, '');
|
|
34
|
+
if (base && !/^https?:\/\//i.test(base)) {
|
|
35
|
+
base = `https://${base.replace(/^\/\//, '')}`;
|
|
36
|
+
}
|
|
37
|
+
return base;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── HMAC / signing ──────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function hmacSecret() {
|
|
43
|
+
try {
|
|
44
|
+
const s = String(nconf.get('secret') || '').trim();
|
|
45
|
+
if (s) return s;
|
|
46
|
+
} catch (_) { /* ignore */ }
|
|
47
|
+
return 'calendar-onekite';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function signCalendarLink(type, id, uid) {
|
|
51
|
+
try {
|
|
52
|
+
const msg = `${String(type)}:${String(id)}:${String(uid || 0)}`;
|
|
53
|
+
return crypto.createHmac('sha256', hmacSecret()).update(msg).digest('hex');
|
|
54
|
+
} catch (_) {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Date formatting ─────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
function formatFR(tsOrIso) {
|
|
62
|
+
const ts = (typeof tsOrIso === 'string' && tsOrIso.includes('-'))
|
|
63
|
+
? Date.parse(tsOrIso)
|
|
64
|
+
: Number(tsOrIso);
|
|
65
|
+
const d = new Date(Number.isFinite(ts) ? ts : tsOrIso);
|
|
66
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
67
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
68
|
+
const yyyy = d.getFullYear();
|
|
69
|
+
return `${dd}/${mm}/${yyyy}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatFRShort(tsOrIso) {
|
|
73
|
+
const ts = (typeof tsOrIso === 'string' && tsOrIso.includes('-'))
|
|
74
|
+
? Date.parse(tsOrIso)
|
|
75
|
+
: Number(tsOrIso);
|
|
76
|
+
const d = new Date(Number.isFinite(ts) ? ts : tsOrIso);
|
|
77
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
78
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
79
|
+
const yy = String(d.getFullYear()).slice(-2);
|
|
80
|
+
return `${dd}/${mm}/${yy}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Group / UID normalisation ───────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function normalizeAllowedGroups(raw) {
|
|
86
|
+
if (!raw) return [];
|
|
87
|
+
if (Array.isArray(raw)) {
|
|
88
|
+
return raw.map((v) => String(v || '').trim()).filter(Boolean);
|
|
89
|
+
}
|
|
90
|
+
const s = String(raw || '').trim();
|
|
91
|
+
if (!s) return [];
|
|
92
|
+
|
|
93
|
+
// Handle JSON-encoded arrays (ACP can store them this way)
|
|
94
|
+
if (s.startsWith('[') && s.endsWith(']')) {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(s);
|
|
97
|
+
if (Array.isArray(parsed)) {
|
|
98
|
+
return parsed.map((v) => String(v || '').trim()).filter(Boolean);
|
|
99
|
+
}
|
|
100
|
+
} catch (_) { /* fall through */ }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return s
|
|
104
|
+
.split(',')
|
|
105
|
+
.map((v) => String(v || '').trim().replace(/^"+|"+$/g, ''))
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeUids(members) {
|
|
110
|
+
if (!Array.isArray(members)) return [];
|
|
111
|
+
const out = [];
|
|
112
|
+
for (const m of members) {
|
|
113
|
+
if (Number.isInteger(m)) { out.push(m); continue; }
|
|
114
|
+
if (typeof m === 'string' && m.trim()) {
|
|
115
|
+
const n = parseInt(m, 10);
|
|
116
|
+
if (Number.isFinite(n)) { out.push(n); continue; }
|
|
117
|
+
}
|
|
118
|
+
if (m && typeof m === 'object' && m.uid != null) {
|
|
119
|
+
const n = Number.isInteger(m.uid) ? m.uid : parseInt(String(m.uid).trim(), 10);
|
|
120
|
+
if (Number.isFinite(n)) out.push(n);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return Array.from(new Set(out)).filter((u) => Number.isInteger(u) && u > 0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeUidList(uids) {
|
|
127
|
+
if (!uids) return [];
|
|
128
|
+
const arr = Array.isArray(uids) ? uids : (() => {
|
|
129
|
+
const s = String(uids || '').trim();
|
|
130
|
+
if (!s) return [];
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(s);
|
|
133
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
134
|
+
} catch (_) {
|
|
135
|
+
return s.split(',').map((x) => String(x).trim()).filter(Boolean);
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const u of arr) {
|
|
141
|
+
const n = Number.isInteger(u) ? u : parseInt(String(u || '').trim(), 10);
|
|
142
|
+
if (Number.isFinite(n) && n > 0) out.push(String(n));
|
|
143
|
+
}
|
|
144
|
+
return Array.from(new Set(out));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Group membership helpers ────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
async function getMembersByGroupIdentifier(groupIdentifier) {
|
|
150
|
+
const id = String(groupIdentifier || '').trim();
|
|
151
|
+
if (!id) return [];
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const members = await groups.getMembers(id, 0, -1);
|
|
155
|
+
if (Array.isArray(members) && members.length) return members;
|
|
156
|
+
} catch (_) { /* ignore */ }
|
|
157
|
+
|
|
158
|
+
const groupName = await getGroupNameBySlug(id);
|
|
159
|
+
if (groupName && String(groupName).trim() && String(groupName).trim() !== id) {
|
|
160
|
+
try {
|
|
161
|
+
const members = await groups.getMembers(String(groupName).trim(), 0, -1);
|
|
162
|
+
return Array.isArray(members) ? members : [];
|
|
163
|
+
} catch (_) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function userInAnyGroup(uid, allowedGroups) {
|
|
171
|
+
const allowed = normalizeAllowedGroups(allowedGroups);
|
|
172
|
+
if (!uid || !allowed.length) return false;
|
|
173
|
+
|
|
174
|
+
const ug = await groups.getUserGroups([uid]);
|
|
175
|
+
const list = (ug && ug[0]) ? ug[0] : [];
|
|
176
|
+
|
|
177
|
+
const seen = new Set();
|
|
178
|
+
for (const g of list) {
|
|
179
|
+
if (!g) continue;
|
|
180
|
+
if (g.slug) seen.add(String(g.slug));
|
|
181
|
+
if (g.name) seen.add(String(g.name));
|
|
182
|
+
if (g.groupName) seen.add(String(g.groupName));
|
|
183
|
+
if (g.displayName) seen.add(String(g.displayName));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return allowed.some((v) => seen.has(String(v)));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── User helpers ────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
async function usernamesByUids(uids) {
|
|
192
|
+
const ids = normalizeUidList(uids);
|
|
193
|
+
if (!ids.length) return [];
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
if (typeof user.getUsersFields === 'function') {
|
|
197
|
+
const rows = await user.getUsersFields(ids, ['username']);
|
|
198
|
+
return (rows || []).map((r) => (r && r.username ? String(r.username) : '')).filter(Boolean);
|
|
199
|
+
}
|
|
200
|
+
} catch (_) { /* fall through */ }
|
|
201
|
+
|
|
202
|
+
const rows = await Promise.all(ids.map(async (uid) => {
|
|
203
|
+
try {
|
|
204
|
+
const r = await user.getUserFields(uid, ['username']);
|
|
205
|
+
return r && r.username ? String(r.username) : '';
|
|
206
|
+
} catch (_) { return ''; }
|
|
207
|
+
}));
|
|
208
|
+
return rows.filter(Boolean);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── Email ───────────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
async function sendEmail(template, uid, subject, data) {
|
|
214
|
+
const toUid = Number.isInteger(uid) ? uid : (uid ? parseInt(String(uid), 10) : NaN);
|
|
215
|
+
if (!Number.isFinite(toUid) || toUid <= 0) return;
|
|
216
|
+
|
|
217
|
+
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
if (typeof emailer.send !== 'function') return;
|
|
221
|
+
await emailer.send(template, toUid, params);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
// eslint-disable-next-line no-console
|
|
224
|
+
console.warn('[calendar-onekite] Failed to send email', {
|
|
225
|
+
template,
|
|
226
|
+
uid: toUid,
|
|
227
|
+
err: err && err.message ? err.message : String(err),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Calendar export helpers (ICS / Google Calendar) ─────────────────────────
|
|
233
|
+
|
|
234
|
+
function ymdToCompact(ymd) {
|
|
235
|
+
return String(ymd || '').replace(/-/g, '');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function dtToGCalUtc(dt) {
|
|
239
|
+
const d = (dt instanceof Date) ? dt : new Date(dt);
|
|
240
|
+
return d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build ICS download URL + Google Calendar template URL.
|
|
245
|
+
*
|
|
246
|
+
* Accepts two calling conventions for backward-compatibility:
|
|
247
|
+
* (a) { type, id, uid, title, details, location, allDay, start, end, startYmd, endYmd }
|
|
248
|
+
* (used by api.js for all entity types)
|
|
249
|
+
* (b) { rid, uid, itemNames, pickupAddress, startYmd, endYmd }
|
|
250
|
+
* (used by admin.js / helloassoWebhook.js for reservations only)
|
|
251
|
+
*/
|
|
252
|
+
function buildCalendarLinks(opts) {
|
|
253
|
+
// Normalise the two calling conventions
|
|
254
|
+
const type = opts.type || 'reservation';
|
|
255
|
+
const id = opts.id || opts.rid || '';
|
|
256
|
+
const uid = Number(opts.uid) || 0;
|
|
257
|
+
const itemNames = Array.isArray(opts.itemNames) ? opts.itemNames : [];
|
|
258
|
+
const defaultTitle = itemNames.length ? `Location - ${itemNames.join(', ')}` : 'Location';
|
|
259
|
+
const title = String(opts.title || defaultTitle).trim() || 'Évènement';
|
|
260
|
+
const details = String(opts.details || '').trim();
|
|
261
|
+
const location = String(opts.location || opts.pickupAddress || '').trim();
|
|
262
|
+
const isAllDay = opts.allDay !== undefined ? !!opts.allDay : true;
|
|
263
|
+
|
|
264
|
+
// Signed ICS URL
|
|
265
|
+
const sig = signCalendarLink(type, String(id), uid);
|
|
266
|
+
const icsPath = `/plugins/calendar-onekite/ics/${encodeURIComponent(type)}/${encodeURIComponent(id)}?uid=${encodeURIComponent(String(uid))}&sig=${encodeURIComponent(sig)}`;
|
|
267
|
+
const icsUrl = `${forumBaseUrl()}${icsPath}`;
|
|
268
|
+
|
|
269
|
+
// Google Calendar template URL
|
|
270
|
+
let dates = '';
|
|
271
|
+
if (isAllDay) {
|
|
272
|
+
dates = `${ymdToCompact(opts.startYmd)}/${ymdToCompact(opts.endYmd)}`;
|
|
273
|
+
} else {
|
|
274
|
+
dates = `${dtToGCalUtc(opts.start)}/${dtToGCalUtc(opts.end)}`;
|
|
275
|
+
}
|
|
276
|
+
const gcal = new URL('https://calendar.google.com/calendar/render');
|
|
277
|
+
gcal.searchParams.set('action', 'TEMPLATE');
|
|
278
|
+
gcal.searchParams.set('text', title);
|
|
279
|
+
if (dates) gcal.searchParams.set('dates', dates);
|
|
280
|
+
if (details) gcal.searchParams.set('details', details);
|
|
281
|
+
if (location) gcal.searchParams.set('location', location);
|
|
282
|
+
|
|
283
|
+
return { icsUrl, googleCalUrl: gcal.toString() };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── HelloAsso URL helpers ───────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
function normalizeCallbackUrl(configured) {
|
|
289
|
+
const base = forumBaseUrl();
|
|
290
|
+
let url = (configured || '').trim();
|
|
291
|
+
if (!url) {
|
|
292
|
+
url = base ? `${base}/plugins/calendar-onekite/helloasso` : '';
|
|
293
|
+
}
|
|
294
|
+
if (url && url.startsWith('/') && base) {
|
|
295
|
+
url = `${base}${url}`;
|
|
296
|
+
}
|
|
297
|
+
if (url && !/^https?:\/\//i.test(url)) {
|
|
298
|
+
url = `https://${url.replace(/^\/\//, '')}`;
|
|
299
|
+
}
|
|
300
|
+
return url;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function normalizeReturnUrl() {
|
|
304
|
+
const base = forumBaseUrl();
|
|
305
|
+
return base ? `${base}/calendar` : '';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function autoFormSlugForYear(year) {
|
|
309
|
+
return `locations-materiel-${year}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function autoCreatorGroupForYear(year) {
|
|
313
|
+
return `onekite-ffvl-${year}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ─── HelloAsso item name builder ─────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
function buildHelloAssoItemName(baseLabel, itemNames, start, end) {
|
|
319
|
+
const base = String(baseLabel || '').trim();
|
|
320
|
+
const items = Array.isArray(itemNames)
|
|
321
|
+
? itemNames.map((s) => String(s || '').trim()).filter(Boolean)
|
|
322
|
+
: [];
|
|
323
|
+
const range = (start && end) ? (`Du ${formatFR(start)} au ${formatFR(end)}`) : '';
|
|
324
|
+
|
|
325
|
+
let out = '';
|
|
326
|
+
if (base) out += base;
|
|
327
|
+
if (items.length) out += (out ? ' — ' : '') + items.map((it) => '• ' + it).join(' ');
|
|
328
|
+
if (range) out += (out ? ' — ' : '') + range;
|
|
329
|
+
|
|
330
|
+
out = String(out || '').trim();
|
|
331
|
+
if (!out) out = 'Réservation matériel';
|
|
332
|
+
|
|
333
|
+
// HelloAsso constraint: itemName max 250 chars
|
|
334
|
+
if (out.length > 250) {
|
|
335
|
+
out = out.slice(0, 249).trimEnd() + '…';
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ─── Calendar-day arithmetic ─────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Calendar-day difference (end exclusive) computed purely from Y/M/D.
|
|
344
|
+
* Uses UTC midnights to avoid any dependency on local timezone or DST.
|
|
345
|
+
*/
|
|
346
|
+
function calendarDaysExclusiveYmd(startYmd, endYmd) {
|
|
347
|
+
try {
|
|
348
|
+
const m1 = /^\d{4}-\d{2}-\d{2}$/.exec(String(startYmd || '').trim());
|
|
349
|
+
const m2 = /^\d{4}-\d{2}-\d{2}$/.exec(String(endYmd || '').trim());
|
|
350
|
+
if (!m1 || !m2) return null;
|
|
351
|
+
const [sy, sm, sd] = String(startYmd).split('-').map((x) => parseInt(x, 10));
|
|
352
|
+
const [ey, em, ed] = String(endYmd).split('-').map((x) => parseInt(x, 10));
|
|
353
|
+
const sUtc = Date.UTC(sy, sm - 1, sd);
|
|
354
|
+
const eUtc = Date.UTC(ey, em - 1, ed);
|
|
355
|
+
const diff = Math.floor((eUtc - sUtc) / (24 * 60 * 60 * 1000));
|
|
356
|
+
return Math.max(1, diff);
|
|
357
|
+
} catch (_) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function yearFromTs(ts) {
|
|
363
|
+
const d = new Date(Number(ts));
|
|
364
|
+
return Number.isFinite(d.getTime()) ? d.getFullYear() : new Date().getFullYear();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ─── Misc ────────────────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
function arrayifyNames(obj) {
|
|
370
|
+
return (Array.isArray(obj && obj.itemNames)
|
|
371
|
+
? obj.itemNames
|
|
372
|
+
: (obj && obj.itemName ? [obj.itemName] : [])
|
|
373
|
+
).filter(Boolean);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getSetting(settings, key, fallback) {
|
|
377
|
+
const v = settings && Object.prototype.hasOwnProperty.call(settings, key) ? settings[key] : undefined;
|
|
378
|
+
if (v == null || v === '') return fallback;
|
|
379
|
+
if (typeof v === 'object' && v && typeof v.value !== 'undefined') return v.value;
|
|
380
|
+
return v;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ─── Exports ─────────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
module.exports = {
|
|
386
|
+
// URLs
|
|
387
|
+
forumBaseUrl,
|
|
388
|
+
normalizeCallbackUrl,
|
|
389
|
+
normalizeReturnUrl,
|
|
390
|
+
autoFormSlugForYear,
|
|
391
|
+
autoCreatorGroupForYear,
|
|
392
|
+
|
|
393
|
+
// HMAC / signing
|
|
394
|
+
hmacSecret,
|
|
395
|
+
signCalendarLink,
|
|
396
|
+
|
|
397
|
+
// Dates
|
|
398
|
+
formatFR,
|
|
399
|
+
formatFRShort,
|
|
400
|
+
ymdToCompact,
|
|
401
|
+
dtToGCalUtc,
|
|
402
|
+
calendarDaysExclusiveYmd,
|
|
403
|
+
yearFromTs,
|
|
404
|
+
|
|
405
|
+
// Groups / UIDs
|
|
406
|
+
normalizeAllowedGroups,
|
|
407
|
+
normalizeUids,
|
|
408
|
+
normalizeUidList,
|
|
409
|
+
getMembersByGroupIdentifier,
|
|
410
|
+
userInAnyGroup,
|
|
411
|
+
|
|
412
|
+
// Users
|
|
413
|
+
usernamesByUids,
|
|
414
|
+
|
|
415
|
+
// Email
|
|
416
|
+
sendEmail,
|
|
417
|
+
|
|
418
|
+
// Calendar links
|
|
419
|
+
buildCalendarLinks,
|
|
420
|
+
|
|
421
|
+
// HelloAsso
|
|
422
|
+
buildHelloAssoItemName,
|
|
423
|
+
|
|
424
|
+
// Misc
|
|
425
|
+
arrayifyNames,
|
|
426
|
+
getSetting,
|
|
427
|
+
};
|
package/lib/utils.js
CHANGED
|
@@ -1,70 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const v = settings && Object.prototype.hasOwnProperty.call(settings, key) ? settings[key] : undefined;
|
|
7
|
-
if (v == null || v === '') return fallback;
|
|
8
|
-
if (typeof v === 'object' && v && typeof v.value !== 'undefined') return v.value;
|
|
9
|
-
return v;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function formatFR(tsOrIso) {
|
|
13
|
-
const ts = (typeof tsOrIso === 'string' && tsOrIso.includes('-')) ? Date.parse(tsOrIso) : Number(tsOrIso);
|
|
14
|
-
const d = new Date(Number.isFinite(ts) ? ts : tsOrIso);
|
|
15
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
16
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
17
|
-
const yyyy = d.getFullYear();
|
|
18
|
-
return `${dd}/${mm}/${yyyy}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function formatFRShort(tsOrIso) {
|
|
22
|
-
const ts = (typeof tsOrIso === 'string' && tsOrIso.includes('-')) ? Date.parse(tsOrIso) : Number(tsOrIso);
|
|
23
|
-
const d = new Date(Number.isFinite(ts) ? ts : tsOrIso);
|
|
24
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
25
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
26
|
-
return `${dd}/${mm}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function forumBaseUrl() {
|
|
30
|
-
return String(nconf.get('url') || '').trim().replace(/\/$/, '');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function normalizeAllowedGroups(raw) {
|
|
34
|
-
if (!raw) return [];
|
|
35
|
-
if (Array.isArray(raw)) {
|
|
36
|
-
return raw.map((s) => String(s || '').trim()).filter(Boolean);
|
|
37
|
-
}
|
|
38
|
-
return String(raw)
|
|
39
|
-
.split(',')
|
|
40
|
-
.map((s) => String(s || '').trim())
|
|
41
|
-
.filter(Boolean);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function normalizeUids(members) {
|
|
45
|
-
if (!Array.isArray(members)) return [];
|
|
46
|
-
const out = [];
|
|
47
|
-
for (const m of members) {
|
|
48
|
-
if (Number.isInteger(m)) { out.push(m); continue; }
|
|
49
|
-
if (typeof m === 'string' && m.trim() && !Number.isNaN(parseInt(m, 10))) { out.push(parseInt(m, 10)); continue; }
|
|
50
|
-
if (m && typeof m === 'object' && (Number.isInteger(m.uid) || (typeof m.uid === 'string' && m.uid.trim()))) {
|
|
51
|
-
const u = Number.isInteger(m.uid) ? m.uid : parseInt(m.uid, 10);
|
|
52
|
-
if (!Number.isNaN(u)) out.push(u);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return Array.from(new Set(out)).filter((u) => Number.isInteger(u) && u > 0);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function arrayifyNames(obj) {
|
|
59
|
-
return (Array.isArray(obj && obj.itemNames) ? obj.itemNames : (obj && obj.itemName ? [obj.itemName] : [])).filter(Boolean);
|
|
60
|
-
}
|
|
3
|
+
// Thin re-export layer for backward compatibility.
|
|
4
|
+
// All helpers now live in ./shared.js.
|
|
5
|
+
const shared = require('./shared');
|
|
61
6
|
|
|
62
7
|
module.exports = {
|
|
63
|
-
getSetting,
|
|
64
|
-
formatFR,
|
|
65
|
-
formatFRShort,
|
|
66
|
-
forumBaseUrl,
|
|
67
|
-
normalizeAllowedGroups,
|
|
68
|
-
normalizeUids,
|
|
69
|
-
arrayifyNames,
|
|
8
|
+
getSetting: shared.getSetting,
|
|
9
|
+
formatFR: shared.formatFR,
|
|
10
|
+
formatFRShort: shared.formatFRShort,
|
|
11
|
+
forumBaseUrl: shared.forumBaseUrl,
|
|
12
|
+
normalizeAllowedGroups: shared.normalizeAllowedGroups,
|
|
13
|
+
normalizeUids: shared.normalizeUids,
|
|
14
|
+
arrayifyNames: shared.arrayifyNames,
|
|
70
15
|
};
|
package/lib/widgets.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
function forumBaseUrl() {
|
|
6
|
-
return String(nconf.get('url') || '').trim().replace(/\/$/, '');
|
|
7
|
-
}
|
|
3
|
+
const { forumBaseUrl } = require('./shared');
|
|
8
4
|
|
|
9
5
|
function escapeHtml(s) {
|
|
10
6
|
return String(s || '')
|
|
@@ -331,7 +327,7 @@ dateClick: function() { window.location.href = calUrl; },
|
|
|
331
327
|
const title = (ep.itemNameLine || ep.title || ev.title || '').toString();
|
|
332
328
|
const statusLabel = (function(s){
|
|
333
329
|
const map = {
|
|
334
|
-
pending: 'En attente de validation
|
|
330
|
+
pending: 'En attente de validation',
|
|
335
331
|
awaiting_payment: 'Validée – paiement en attente',
|
|
336
332
|
paid: 'Payée',
|
|
337
333
|
rejected: 'Rejetée',
|
package/package.json
CHANGED