nodebb-plugin-onekite-calendar 1.0.4 → 1.0.5

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