nodebb-plugin-onekite-calendar 1.0.3 → 1.0.4

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/api.js CHANGED
@@ -3,14 +3,50 @@
3
3
  const crypto = require('crypto');
4
4
 
5
5
  const meta = require.main.require('./src/meta');
6
- const emailer = require.main.require('./src/emailer');
7
6
  const nconf = require.main.require('nconf');
8
7
  const user = require.main.require('./src/user');
9
8
  const groups = require.main.require('./src/groups');
10
9
  const db = require.main.require('./src/database');
11
- const logger = require.main.require('./src/logger');
10
+ // logger available if you want to debug locally; we avoid noisy logs in prod.
12
11
 
13
12
  const dbLayer = require('./db');
13
+ const { sendEmail } = require('./email');
14
+ const log = require('./log');
15
+
16
+ // Ultra-perf: short-lived in-memory cache for the events feed.
17
+ // We cache a "base" event list without per-user privileges, then decorate
18
+ // it per request. This reduces DB load when many users/widgets refresh.
19
+ const EVENTS_CACHE = new Map();
20
+ const EVENTS_CACHE_TTL_MS = 30 * 1000;
21
+ const EVENTS_CACHE_MAX = 50;
22
+
23
+ function eventsCacheKey(startTs, endTs) {
24
+ // Keep the key stable and short.
25
+ return `${Math.max(0, Number(startTs) || 0)}:${Math.max(0, Number(endTs) || 0)}`;
26
+ }
27
+
28
+ function getEventsCache(key) {
29
+ const hit = EVENTS_CACHE.get(key);
30
+ if (!hit) return null;
31
+ if ((Date.now() - hit.ts) > EVENTS_CACHE_TTL_MS) {
32
+ EVENTS_CACHE.delete(key);
33
+ return null;
34
+ }
35
+ return hit.data;
36
+ }
37
+
38
+ function setEventsCache(key, data) {
39
+ EVENTS_CACHE.set(key, { ts: Date.now(), data });
40
+ if (EVENTS_CACHE.size > EVENTS_CACHE_MAX) {
41
+ // Drop oldest entries.
42
+ const entries = Array.from(EVENTS_CACHE.entries());
43
+ entries.sort((a, b) => (a[1]?.ts || 0) - (b[1]?.ts || 0));
44
+ const toDrop = Math.max(1, EVENTS_CACHE.size - EVENTS_CACHE_MAX);
45
+ for (let i = 0; i < toDrop; i++) {
46
+ EVENTS_CACHE.delete(entries[i][0]);
47
+ }
48
+ }
49
+ }
14
50
 
15
51
  // Fast membership check without N calls to groups.isMember.
16
52
  // NodeBB's groups.getUserGroups([uid]) returns an array (per uid) of group objects.
@@ -52,36 +88,7 @@ function normalizeAllowedGroups(raw) {
52
88
  const helloasso = require('./helloasso');
53
89
  const discord = require('./discord');
54
90
 
55
- // Email helper: NodeBB's Emailer signature differs across versions.
56
- // We try the common forms. Any failure is logged for debugging.
57
- async function sendEmail(template, toEmail, subject, data) {
58
- if (!toEmail) return;
59
- try {
60
- // NodeBB core signature (historically):
61
- // Emailer.sendToEmail(template, email, language, params[, callback])
62
- // Subject is not a positional arg; it must be injected (either by NodeBB itself
63
- // or via filter:email.modify). We always pass it in params.subject.
64
- const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
65
- const params = Object.assign({}, data || {}, subject ? { subject } : {});
66
- if (typeof emailer.sendToEmail === 'function') {
67
- await emailer.sendToEmail(template, toEmail, language, params);
68
- return;
69
- }
70
- // Fallback for older/unusual builds (rare)
71
- if (typeof emailer.send === 'function') {
72
- // Some builds accept (template, email, language, params)
73
- if (emailer.send.length >= 4) {
74
- await emailer.send(template, toEmail, language, params);
75
- return;
76
- }
77
- // Some builds accept (template, email, params)
78
- await emailer.send(template, toEmail, params);
79
- }
80
- } catch (err) {
81
- // eslint-disable-next-line no-console
82
- console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err) });
83
- }
84
- }
91
+ // Email helper is in lib/email.js (no logs here).
85
92
 
86
93
  function normalizeBaseUrl(meta) {
87
94
  // Prefer meta.config.url, fallback to nconf.get('url')
@@ -102,7 +109,8 @@ function normalizeCallbackUrl(configured, meta) {
102
109
  let url = (configured || '').trim();
103
110
  if (!url) {
104
111
  // Default webhook endpoint (recommended): namespaced under /plugins
105
- url = base ? `${base}/plugins/calendar-onekite/helloasso` : '';
112
+ // Prefer the new namespace; legacy endpoint remains available.
113
+ url = base ? `${base}/plugins/onekite-calendar/helloasso` : '';
106
114
  }
107
115
  if (url && url.startsWith('/') && base) {
108
116
  url = `${base}${url}`;
@@ -328,7 +336,7 @@ api.getEvents = async function (req, res) {
328
336
  const startTs = toTs(req.query.start) || 0;
329
337
  const endTs = toTs(req.query.end) || (Date.now() + 365 * 24 * 3600 * 1000);
330
338
 
331
- const settings = await meta.settings.get('calendar-onekite');
339
+ const settings = await meta.settings.get('onekite-calendar');
332
340
  const canMod = req.uid ? await canValidate(req.uid, settings) : false;
333
341
  const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings) : false;
334
342
  const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings) : false;
@@ -336,20 +344,89 @@ api.getEvents = async function (req, res) {
336
344
  // Fetch a wider window because an event can start before the query range
337
345
  // and still overlap.
338
346
  const wideStart = Math.max(0, startTs - 366 * 24 * 3600 * 1000);
339
- const ids = await dbLayer.listReservationIdsByStartRange(wideStart, endTs, 5000);
347
+
348
+ const cacheKey = eventsCacheKey(startTs, endTs);
349
+ let base = getEventsCache(cacheKey);
350
+ if (!base) {
351
+ const baseOut = [];
352
+
353
+ const ids = await dbLayer.listReservationIdsByStartRange(wideStart, endTs, 5000);
354
+ // Batch fetch = major perf win when there are many reservations.
355
+ const reservations = await dbLayer.getReservations(ids);
356
+ for (const r of (reservations || [])) {
357
+ if (!r) continue;
358
+ if (!['pending', 'awaiting_payment', 'paid'].includes(r.status)) continue;
359
+ const rStart = parseInt(r.start, 10);
360
+ const rEnd = parseInt(r.end, 10);
361
+ if (!(rStart < endTs && startTs < rEnd)) continue;
362
+
363
+ const evs = eventsFor(r);
364
+ const hasPay = (r.status === 'awaiting_payment' && r.paymentUrl && (/^https?:\/\//i).test(String(r.paymentUrl)));
365
+ for (const ev of (evs || [])) {
366
+ const p = ev.extendedProps || {};
367
+ baseOut.push({
368
+ id: ev.id,
369
+ title: ev.title,
370
+ backgroundColor: ev.backgroundColor,
371
+ borderColor: ev.borderColor,
372
+ textColor: ev.textColor,
373
+ allDay: ev.allDay,
374
+ start: ev.start,
375
+ end: ev.end,
376
+ extendedProps: {
377
+ type: 'reservation',
378
+ rid: p.rid,
379
+ status: p.status,
380
+ uid: p.uid,
381
+ _username: r.username ? String(r.username) : '',
382
+ _hasPayment: !!hasPay,
383
+ },
384
+ });
385
+ }
386
+ }
387
+
388
+ // Special events
389
+ try {
390
+ const specialIds = await dbLayer.listSpecialIdsByStartRange(wideStart, endTs, 5000);
391
+ const specials = await dbLayer.getSpecialEvents(specialIds);
392
+ for (const sev of (specials || [])) {
393
+ if (!sev) continue;
394
+ const sStart = parseInt(sev.start, 10);
395
+ const sEnd = parseInt(sev.end, 10);
396
+ if (!(sStart < endTs && startTs < sEnd)) continue;
397
+ const full = eventsForSpecial(sev);
398
+ baseOut.push({
399
+ id: full.id,
400
+ title: full.title,
401
+ allDay: full.allDay,
402
+ start: full.start,
403
+ end: full.end,
404
+ backgroundColor: full.backgroundColor,
405
+ borderColor: full.borderColor,
406
+ textColor: full.textColor,
407
+ extendedProps: {
408
+ type: 'special',
409
+ eid: sev.eid,
410
+ uid: sev.uid,
411
+ _username: sev.username ? String(sev.username) : '',
412
+ },
413
+ });
414
+ }
415
+ } catch (e) {
416
+ // ignore
417
+ }
418
+
419
+ base = baseOut;
420
+ setEventsCache(cacheKey, baseOut);
421
+ }
422
+
340
423
  const out = [];
341
- // Batch fetch = major perf win when there are many reservations.
342
- const reservations = await dbLayer.getReservations(ids);
343
- for (const r of (reservations || [])) {
344
- if (!r) continue;
345
- // Only show active statuses
346
- if (!['pending', 'awaiting_payment', 'paid'].includes(r.status)) continue;
347
- const rStart = parseInt(r.start, 10);
348
- const rEnd = parseInt(r.end, 10);
349
- if (!(rStart < endTs && startTs < rEnd)) continue; // overlap check
350
- const evs = eventsFor(r);
351
- for (const ev of evs) {
352
- const p = ev.extendedProps || {};
424
+ const uidStr = (req.uid || req.uid === 0) ? String(req.uid) : '';
425
+ for (const ev of (base || [])) {
426
+ if (!ev) continue;
427
+ const p = ev.extendedProps || {};
428
+ if (p.type === 'reservation') {
429
+ const ownerOrMod = !!(p.uid && uidStr && String(p.uid) === uidStr) || canMod;
353
430
  const minimal = {
354
431
  id: ev.id,
355
432
  title: ev.title,
@@ -367,53 +444,30 @@ api.getEvents = async function (req, res) {
367
444
  canModerate: canMod,
368
445
  },
369
446
  };
370
- // Only expose username on the event list to owner/moderators.
371
- if (r.username && ((req.uid && String(req.uid) === String(r.uid)) || canMod)) {
372
- minimal.extendedProps.username = String(r.username);
373
- }
374
- // Let the UI decide if a "Payer" button might exist, without exposing the URL in list.
375
- if (r.status === 'awaiting_payment' && r.paymentUrl && (/^https?:\/\//i).test(String(r.paymentUrl))) {
376
- if ((req.uid && String(req.uid) === String(r.uid)) || canMod) {
377
- minimal.extendedProps.hasPayment = true;
378
- }
379
- }
447
+ if (p._username && ownerOrMod) minimal.extendedProps.username = String(p._username);
448
+ if (p._hasPayment && ownerOrMod) minimal.extendedProps.hasPayment = true;
380
449
  out.push(minimal);
381
- }
382
- }
383
-
384
- // Special events
385
- try {
386
- const specialIds = await dbLayer.listSpecialIdsByStartRange(wideStart, endTs, 5000);
387
- const specials = await dbLayer.getSpecialEvents(specialIds);
388
- for (const sev of (specials || [])) {
389
- if (!sev) continue;
390
- const sStart = parseInt(sev.start, 10);
391
- const sEnd = parseInt(sev.end, 10);
392
- if (!(sStart < endTs && startTs < sEnd)) continue;
393
- const full = eventsForSpecial(sev);
450
+ } else if (p.type === 'special') {
451
+ const ownerOrPriv = canMod || canSpecialDelete || (p.uid && uidStr && String(p.uid) === uidStr);
394
452
  const minimal = {
395
- id: full.id,
396
- title: full.title,
397
- allDay: full.allDay,
398
- start: full.start,
399
- end: full.end,
400
- backgroundColor: full.backgroundColor,
401
- borderColor: full.borderColor,
402
- textColor: full.textColor,
453
+ id: ev.id,
454
+ title: ev.title,
455
+ allDay: ev.allDay,
456
+ start: ev.start,
457
+ end: ev.end,
458
+ backgroundColor: ev.backgroundColor,
459
+ borderColor: ev.borderColor,
460
+ textColor: ev.textColor,
403
461
  extendedProps: {
404
462
  type: 'special',
405
- eid: sev.eid,
463
+ eid: p.eid,
406
464
  canCreateSpecial: canSpecialCreate,
407
465
  canDeleteSpecial: canSpecialDelete,
408
466
  },
409
467
  };
410
- if (sev.username && (canMod || canSpecialDelete || (req.uid && String(req.uid) === String(sev.uid)))) {
411
- minimal.extendedProps.username = String(sev.username);
412
- }
468
+ if (p._username && ownerOrPriv) minimal.extendedProps.username = String(p._username);
413
469
  out.push(minimal);
414
470
  }
415
- } catch (e) {
416
- // ignore
417
471
  }
418
472
 
419
473
  // Stable ordering -> stable ETag
@@ -428,7 +482,8 @@ api.getEvents = async function (req, res) {
428
482
 
429
483
  const etag = computeEtag(out);
430
484
  res.setHeader('ETag', etag);
431
- res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
485
+ // Short caching + ETag revalidation keeps the feed snappy and reduces bandwidth.
486
+ res.setHeader('Cache-Control', 'private, max-age=15, must-revalidate');
432
487
  if (String(req.headers['if-none-match'] || '') === etag) {
433
488
  return res.status(304).end();
434
489
  }
@@ -440,7 +495,7 @@ api.getReservationDetails = async function (req, res) {
440
495
  const uid = req.uid;
441
496
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
442
497
 
443
- const settings = await meta.settings.get('calendar-onekite');
498
+ const settings = await meta.settings.get('onekite-calendar');
444
499
  const canMod = await canValidate(uid, settings);
445
500
 
446
501
  const rid = String(req.params.rid || '').trim();
@@ -482,7 +537,7 @@ api.getSpecialEventDetails = async function (req, res) {
482
537
  const uid = req.uid;
483
538
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
484
539
 
485
- const settings = await meta.settings.get('calendar-onekite');
540
+ const settings = await meta.settings.get('onekite-calendar');
486
541
  const canMod = await canValidate(uid, settings);
487
542
  const canSpecialDelete = await canDeleteSpecial(uid, settings);
488
543
 
@@ -511,7 +566,7 @@ api.getSpecialEventDetails = async function (req, res) {
511
566
  };
512
567
 
513
568
  api.getCapabilities = async function (req, res) {
514
- const settings = await meta.settings.get('calendar-onekite');
569
+ const settings = await meta.settings.get('onekite-calendar');
515
570
  const uid = req.uid || 0;
516
571
  const canMod = uid ? await canValidate(uid, settings) : false;
517
572
  res.json({
@@ -522,7 +577,7 @@ api.getCapabilities = async function (req, res) {
522
577
  };
523
578
 
524
579
  api.createSpecialEvent = async function (req, res) {
525
- const settings = await meta.settings.get('calendar-onekite');
580
+ const settings = await meta.settings.get('onekite-calendar');
526
581
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
527
582
  const ok = await canCreateSpecial(req.uid, settings);
528
583
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
@@ -558,7 +613,7 @@ api.createSpecialEvent = async function (req, res) {
558
613
  };
559
614
 
560
615
  api.deleteSpecialEvent = async function (req, res) {
561
- const settings = await meta.settings.get('calendar-onekite');
616
+ const settings = await meta.settings.get('onekite-calendar');
562
617
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
563
618
  const ok = await canDeleteSpecial(req.uid, settings);
564
619
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
@@ -569,7 +624,7 @@ api.deleteSpecialEvent = async function (req, res) {
569
624
  };
570
625
 
571
626
  api.getItems = async function (req, res) {
572
- const settings = await meta.settings.get('calendar-onekite');
627
+ const settings = await meta.settings.get('onekite-calendar');
573
628
 
574
629
  const env = settings.helloassoEnv || 'prod';
575
630
  const token = await helloasso.getAccessToken({
@@ -610,7 +665,7 @@ api.createReservation = async function (req, res) {
610
665
  const uid = req.uid;
611
666
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
612
667
 
613
- const settings = await meta.settings.get('calendar-onekite');
668
+ const settings = await meta.settings.get('onekite-calendar');
614
669
  const startPreview = toTs(req.body.start);
615
670
  const ok = await canRequest(uid, settings, startPreview);
616
671
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
@@ -707,7 +762,7 @@ api.createReservation = async function (req, res) {
707
762
 
708
763
  for (const md of (usersData || [])) {
709
764
  if (md && md.email) {
710
- await sendEmail('calendar-onekite_pending', md.email, 'Location matériel - Demande de réservation', {
765
+ await sendEmail('onekite-calendar_pending', md.email, 'Location matériel - Demande de réservation', {
711
766
  username: md.username,
712
767
  requester: requester.username,
713
768
  itemName: itemsLabel,
@@ -722,7 +777,7 @@ api.createReservation = async function (req, res) {
722
777
  }
723
778
  }
724
779
  } catch (e) {
725
- console.warn('[calendar-onekite] Failed to send pending email', e && e.message ? e.message : e);
780
+ log.warn('Failed to send pending email', e && e.message ? e.message : e);
726
781
  }
727
782
 
728
783
  // Discord webhook (optional)
@@ -746,7 +801,7 @@ api.createReservation = async function (req, res) {
746
801
  api.approveReservation = async function (req, res) {
747
802
  const uid = req.uid;
748
803
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
749
- const settings = await meta.settings.get('calendar-onekite');
804
+ const settings = await meta.settings.get('onekite-calendar');
750
805
  const ok = await canValidate(uid, settings);
751
806
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
752
807
 
@@ -773,7 +828,7 @@ api.approveReservation = async function (req, res) {
773
828
  }
774
829
  // Create HelloAsso payment link on validation
775
830
  try {
776
- const settings2 = await meta.settings.get('calendar-onekite');
831
+ const settings2 = await meta.settings.get('onekite-calendar');
777
832
  const token = await helloasso.getAccessToken({ env: settings2.helloassoEnv || 'prod', clientId: settings2.helloassoClientId, clientSecret: settings2.helloassoClientSecret });
778
833
  const payer = await user.getUserFields(r.uid, ['email']);
779
834
  const year = yearFromTs(r.start);
@@ -788,7 +843,7 @@ api.approveReservation = async function (req, res) {
788
843
  totalAmount: (() => {
789
844
  const cents = Math.max(0, Math.round((Number(r.total) || 0) * 100));
790
845
  if (!cents) {
791
- console.warn('[calendar-onekite] HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
846
+ log.warn('HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
792
847
  }
793
848
  return cents;
794
849
  })(),
@@ -797,7 +852,7 @@ api.approveReservation = async function (req, res) {
797
852
  // Can be overridden via ACP setting `helloassoCallbackUrl`.
798
853
  callbackUrl: normalizeReturnUrl(meta),
799
854
  webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
800
- itemName: 'Réservation matériel OneKite',
855
+ itemName: 'Réservation matériel Onekite',
801
856
  containsDonation: false,
802
857
  metadata: { reservationId: String(rid) },
803
858
  });
@@ -809,7 +864,7 @@ api.approveReservation = async function (req, res) {
809
864
  r.checkoutIntentId = checkoutIntentId;
810
865
  }
811
866
  } else {
812
- console.warn('[calendar-onekite] HelloAsso payment link not created (approve API)', { rid });
867
+ log.warn('HelloAsso payment link not created (approve API)', { rid });
813
868
  }
814
869
  } catch (e) {
815
870
  // ignore payment link errors, admin can retry
@@ -825,7 +880,7 @@ api.approveReservation = async function (req, res) {
825
880
  const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
826
881
  ? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
827
882
  : '';
828
- await sendEmail('calendar-onekite_approved', requester.email, 'Location matériel - Réservation validée', {
883
+ await sendEmail('onekite-calendar_approved', requester.email, 'Location matériel - Réservation validée', {
829
884
  username: requester.username,
830
885
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
831
886
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
@@ -849,7 +904,7 @@ api.approveReservation = async function (req, res) {
849
904
  api.refuseReservation = async function (req, res) {
850
905
  const uid = req.uid;
851
906
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
852
- const settings = await meta.settings.get('calendar-onekite');
907
+ const settings = await meta.settings.get('onekite-calendar');
853
908
  const ok = await canValidate(uid, settings);
854
909
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
855
910
 
@@ -864,7 +919,7 @@ api.refuseReservation = async function (req, res) {
864
919
 
865
920
  const requester = await user.getUserFields(r.uid, ['username', 'email']);
866
921
  if (requester && requester.email) {
867
- await sendEmail('calendar-onekite_refused', requester.email, 'Location matériel - Demande de réservation', {
922
+ await sendEmail('onekite-calendar_refused', requester.email, 'Location matériel - Demande de réservation', {
868
923
  username: requester.username,
869
924
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
870
925
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
@@ -884,7 +939,7 @@ api.cancelReservation = async function (req, res) {
884
939
  const uid = req.uid;
885
940
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
886
941
 
887
- const settings = await meta.settings.get('calendar-onekite');
942
+ const settings = await meta.settings.get('onekite-calendar');
888
943
  const rid = String(req.params.rid || '').trim();
889
944
  if (!rid) return res.status(400).json({ error: 'missing-rid' });
890
945
 
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ // Primary namespace for this plugin (NodeBB 4.x)
4
+ module.exports = {
5
+ NAMESPACE: 'onekite-calendar',
6
+ WIDGET_ID: 'onekite-calendar-twoweeks',
7
+ };
@@ -3,8 +3,8 @@
3
3
  const controllers = {};
4
4
 
5
5
  controllers.renderCalendar = async function (req, res) {
6
- res.render('calendar-onekite', {
7
- title: 'Calendar',
6
+ res.render('onekite-calendar', {
7
+ title: 'Calendrier',
8
8
  });
9
9
  };
10
10
 
package/lib/db.js CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  const db = require.main.require('./src/database');
4
4
 
5
- const KEY_ZSET = 'calendar-onekite:reservations';
6
- const KEY_OBJ = (rid) => `calendar-onekite:reservation:${rid}`;
7
- const KEY_CHECKOUT_INTENT_TO_RID = 'calendar-onekite:helloasso:checkoutIntentToRid';
5
+ const KEY_ZSET = 'onekite-calendar:reservations';
6
+ const KEY_OBJ = (rid) => `onekite-calendar:reservation:${rid}`;
7
+ const KEY_CHECKOUT_INTENT_TO_RID = 'onekite-calendar:helloasso:checkoutIntentToRid';
8
8
 
9
9
  // Special events (non-reservation events shown in a different colour)
10
- const KEY_SPECIAL_ZSET = 'calendar-onekite:special';
11
- const KEY_SPECIAL_OBJ = (eid) => `calendar-onekite:special:${eid}`;
10
+ const KEY_SPECIAL_ZSET = 'onekite-calendar:special';
11
+ const KEY_SPECIAL_OBJ = (eid) => `onekite-calendar:special:${eid}`;
12
12
 
13
13
  // Helpers
14
14
  function reservationKey(rid) {
package/lib/discord.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const https = require('https');
4
+ const log = require('./log');
4
5
  const { URL } = require('url');
5
6
 
6
7
  function isEnabled(v, defaultValue) {
@@ -36,7 +37,7 @@ function postWebhook(webhookUrl, payload) {
36
37
  headers: {
37
38
  'Content-Type': 'application/json',
38
39
  'Content-Length': body.length,
39
- 'User-Agent': 'nodebb-plugin-calendar-onekite',
40
+ 'User-Agent': 'nodebb-plugin-onekite-calendar',
40
41
  },
41
42
  }, (res) => {
42
43
  const ok = res.statusCode && res.statusCode >= 200 && res.statusCode < 300;
@@ -88,7 +89,7 @@ function buildReservationMessage(kind, reservation) {
88
89
  function buildWebhookPayload(kind, reservation) {
89
90
  // Discord "regroupe" visuellement les messages consécutifs d'un même auteur.
90
91
  // En utilisant un username différent par action, on obtient un message bien distinct.
91
- const webhookUsername = kind === 'paid' ? 'OneKite • Paiement' : 'OneKite • Réservation';
92
+ const webhookUsername = kind === 'paid' ? 'Onekite • Paiement' : 'Onekite • Réservation';
92
93
 
93
94
  const calUrl = 'https://www.onekite.com/calendar';
94
95
  const username = reservation && reservation.username ? String(reservation.username) : '';
@@ -124,7 +125,7 @@ function buildWebhookPayload(kind, reservation) {
124
125
  ? 'Un paiement a été reçu pour une réservation.'
125
126
  : 'Une nouvelle demande de réservation a été créée.',
126
127
  fields,
127
- footer: { text: 'OneKite • Calendrier' },
128
+ footer: { text: 'Onekite • Calendrier' },
128
129
  timestamp: new Date().toISOString(),
129
130
  },
130
131
  ],
@@ -140,7 +141,7 @@ async function notifyReservationRequested(settings, reservation) {
140
141
  await postWebhook(url, buildWebhookPayload('request', reservation));
141
142
  } catch (e) {
142
143
  // eslint-disable-next-line no-console
143
- console.warn('[calendar-onekite] Discord webhook failed (request)', e && e.message ? e.message : String(e));
144
+ log.warn('Discord webhook failed (request)', e && e.message ? e.message : String(e));
144
145
  }
145
146
  }
146
147
 
@@ -153,7 +154,7 @@ async function notifyPaymentReceived(settings, reservation) {
153
154
  await postWebhook(url, buildWebhookPayload('paid', reservation));
154
155
  } catch (e) {
155
156
  // eslint-disable-next-line no-console
156
- console.warn('[calendar-onekite] Discord webhook failed (paid)', e && e.message ? e.message : String(e));
157
+ log.warn('Discord webhook failed (paid)', e && e.message ? e.message : String(e));
157
158
  }
158
159
  }
159
160
 
package/lib/email.js ADDED
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const meta = require.main.require('./src/meta');
4
+ const emailer = require.main.require('./src/emailer');
5
+
6
+ function defaultLanguage() {
7
+ return (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
8
+ }
9
+
10
+ /**
11
+ * Send a transactional email using NodeBB's emailer (NodeBB 4.x).
12
+ */
13
+ async function sendEmail(template, toEmail, subject, data) {
14
+ if (!toEmail) return;
15
+ if (!emailer || typeof emailer.sendToEmail !== 'function') {
16
+ throw new Error('Emailer not available (sendToEmail missing)');
17
+ }
18
+
19
+ const language = defaultLanguage();
20
+ const params = Object.assign({}, data || {}, subject ? { subject } : {});
21
+ await emailer.sendToEmail(template, toEmail, language, params);
22
+ }
23
+
24
+ module.exports = {
25
+ sendEmail,
26
+ };
package/lib/helloasso.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const https = require('https');
4
+ const log = require('./log');
4
5
 
5
6
  function requestJson(method, url, headers = {}, bodyObj = null) {
6
7
  return new Promise((resolve) => {
@@ -284,11 +285,11 @@ async function listCatalogItems({ env, token, organizationSlug, formType, formSl
284
285
  async function createCheckoutIntent({ env, token, organizationSlug, formType, formSlug, totalAmount, payerEmail, callbackUrl, webhookUrl, itemName, containsDonation, metadata }) {
285
286
  if (!token || !organizationSlug) return null;
286
287
  if (!callbackUrl || !/^https?:\/\//i.test(String(callbackUrl))) {
287
- console.warn('[calendar-onekite] HelloAsso invalid return/back/error URL', { callbackUrl });
288
+ log.warn('HelloAsso invalid return/back/error URL', { callbackUrl });
288
289
  return null;
289
290
  }
290
291
  if (webhookUrl && !/^https?:\/\//i.test(String(webhookUrl))) {
291
- console.warn('[calendar-onekite] HelloAsso invalid webhook URL', { webhookUrl });
292
+ log.warn('HelloAsso invalid webhook URL', { webhookUrl });
292
293
  }
293
294
  // Checkout intents are created at organization level.
294
295
  const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(organizationSlug)}/checkout-intents`;
@@ -311,7 +312,7 @@ async function createCheckoutIntent({ env, token, organizationSlug, formType, fo
311
312
  // Log the error payload to help diagnose configuration issues (slug, env, urls, amount, etc.)
312
313
  try {
313
314
  // eslint-disable-next-line no-console
314
- console.warn('[calendar-onekite] HelloAsso checkout-intent failed', { status, json });
315
+ log.warn('HelloAsso checkout-intent failed', { status, json });
315
316
  } catch (e) { /* ignore */ }
316
317
  return null;
317
318
  }
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('crypto');
4
+ const log = require('./log');
4
5
 
5
6
  const db = require.main.require('./src/database');
6
7
  const meta = require.main.require('./src/meta');
@@ -19,10 +20,10 @@ const dbLayer = require('./db');
19
20
  const helloasso = require('./helloasso');
20
21
  const discord = require('./discord');
21
22
 
22
- const SETTINGS_KEY = 'calendar-onekite';
23
+ const SETTINGS_KEY = 'onekite-calendar';
23
24
 
24
25
  // Replay protection: store processed payment ids.
25
- const PROCESSED_KEY = 'calendar-onekite:helloasso:processedPayments';
26
+ const PROCESSED_KEY = 'onekite-calendar:helloasso:processedPayments';
26
27
 
27
28
  async function sendEmail(template, toEmail, subject, data) {
28
29
  const uidFromData = data && Number.isInteger(data.uid) ? data.uid : null;
@@ -67,7 +68,7 @@ async function sendEmail(template, toEmail, subject, data) {
67
68
  // fall back to sendToEmail only.
68
69
  } catch (err) {
69
70
  // eslint-disable-next-line no-console
70
- console.warn('[calendar-onekite] Failed to send email (webhook)', { template, toEmail, err: String(err && err.message || err) });
71
+ log.warn('Failed to send email (webhook)', { template, toEmail, err: String(err && err.message || err) });
71
72
  }
72
73
  }
73
74
 
@@ -281,7 +282,7 @@ async function handler(req, res, next) {
281
282
 
282
283
  if (allowedIps.length && clientIp && !allowedIps.includes(clientIp)) {
283
284
  // eslint-disable-next-line no-console
284
- console.warn('[calendar-onekite] HelloAsso webhook blocked by IP allowlist', { ip: clientIp, allowed: allowedIps });
285
+ log.warn('HelloAsso webhook blocked by IP allowlist', { ip: clientIp, allowed: allowedIps });
285
286
  return res.status(403).json({ ok: false, error: 'ip-not-allowed' });
286
287
  }
287
288
 
@@ -324,7 +325,7 @@ async function handler(req, res, next) {
324
325
  }
325
326
  if (!resolvedRid) {
326
327
  // eslint-disable-next-line no-console
327
- console.warn('[calendar-onekite] HelloAsso webhook missing reservationId in metadata', { eventType: payload && payload.eventType, paymentId, checkoutIntentId });
328
+ log.warn('HelloAsso webhook missing reservationId in metadata', { eventType: payload && payload.eventType, paymentId, checkoutIntentId });
328
329
  // Do NOT mark as processed: if metadata/config is fixed later, a manual replay may be possible.
329
330
  return res.json({ ok: true, processed: false, missingReservationId: true });
330
331
  }
@@ -347,14 +348,14 @@ async function handler(req, res, next) {
347
348
  // Real-time notify: refresh calendars for all viewers (owner + validators/admins)
348
349
  try {
349
350
  if (io && io.sockets && typeof io.sockets.emit === 'function') {
350
- io.sockets.emit('event:calendar-onekite.reservationUpdated', { rid: r.rid, status: r.status });
351
+ io.sockets.emit('event:onekite-calendar.reservationUpdated', { rid: r.rid, status: r.status });
351
352
  }
352
353
  } catch (e) {}
353
354
 
354
355
  // Notify requester
355
356
  const requester = await user.getUserFields(r.uid, ['username', 'email']);
356
357
  if (requester && requester.email) {
357
- await sendEmail('calendar-onekite_paid', requester.email, 'Location matériel - Paiement reçu', {
358
+ await sendEmail('onekite-calendar_paid', requester.email, 'Location matériel - Paiement reçu', {
358
359
  uid: parseInt(r.uid, 10),
359
360
  username: requester.username,
360
361
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),