nodebb-plugin-onekite-calendar 2.0.41 → 2.0.43
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 +18 -0
- package/lib/api.js +227 -57
- package/lib/db.js +36 -0
- package/lib/discord.js +55 -27
- package/library.js +7 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/admin.js +36 -0
- package/public/client.js +282 -350
- package/templates/admin/plugins/calendar-onekite.tpl +32 -5
package/lib/admin.js
CHANGED
|
@@ -305,6 +305,24 @@ admin.purgeSpecialEventsByYear = async function (req, res) {
|
|
|
305
305
|
return res.json({ ok: true, removed: count });
|
|
306
306
|
};
|
|
307
307
|
|
|
308
|
+
admin.purgeOutingsByYear = async function (req, res) {
|
|
309
|
+
const year = (req.body && req.body.year ? String(req.body.year) : '').trim();
|
|
310
|
+
if (!/^\d{4}$/.test(year)) {
|
|
311
|
+
return res.status(400).json({ error: 'invalid-year' });
|
|
312
|
+
}
|
|
313
|
+
const y = parseInt(year, 10);
|
|
314
|
+
const startTs = new Date(Date.UTC(y, 0, 1)).getTime();
|
|
315
|
+
const endTs = new Date(Date.UTC(y + 1, 0, 1)).getTime() - 1;
|
|
316
|
+
|
|
317
|
+
const ids = await dbLayer.listOutingIdsByStartRange(startTs, endTs, 200000);
|
|
318
|
+
let count = 0;
|
|
319
|
+
for (const oid of ids) {
|
|
320
|
+
await dbLayer.removeOuting(oid);
|
|
321
|
+
count++;
|
|
322
|
+
}
|
|
323
|
+
return res.json({ ok: true, removed: count });
|
|
324
|
+
};
|
|
325
|
+
|
|
308
326
|
// Debug endpoint to validate HelloAsso connectivity and item loading
|
|
309
327
|
|
|
310
328
|
|
package/lib/api.js
CHANGED
|
@@ -359,17 +359,30 @@ async function auditLog(action, actorUid, payload) {
|
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
async function canCreateSpecial(uid, settings
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
362
|
+
async function canCreateSpecial(uid, settings) {
|
|
363
|
+
if (!uid) return false;
|
|
364
|
+
try {
|
|
365
|
+
const isAdmin = await groups.isMember(uid, 'administrators');
|
|
366
|
+
if (isAdmin) return true;
|
|
367
|
+
} catch (e) {}
|
|
368
|
+
const allowed = normalizeAllowedGroups(settings.specialCreatorGroups || '');
|
|
369
|
+
if (!allowed.length) return false;
|
|
370
|
+
if (await userInAnyGroup(uid, allowed)) return true;
|
|
371
|
+
|
|
372
|
+
return false;
|
|
367
373
|
}
|
|
368
374
|
|
|
369
|
-
async function canDeleteSpecial(uid, settings
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
375
|
+
async function canDeleteSpecial(uid, settings) {
|
|
376
|
+
if (!uid) return false;
|
|
377
|
+
try {
|
|
378
|
+
const isAdmin = await groups.isMember(uid, 'administrators');
|
|
379
|
+
if (isAdmin) return true;
|
|
380
|
+
} catch (e) {}
|
|
381
|
+
const allowed = normalizeAllowedGroups(settings.specialDeleterGroups || settings.specialCreatorGroups || '');
|
|
382
|
+
if (!allowed.length) return false;
|
|
383
|
+
if (await userInAnyGroup(uid, allowed)) return true;
|
|
384
|
+
|
|
385
|
+
return false;
|
|
373
386
|
}
|
|
374
387
|
|
|
375
388
|
function eventsFor(resv) {
|
|
@@ -429,36 +442,23 @@ function eventsFor(resv) {
|
|
|
429
442
|
}
|
|
430
443
|
|
|
431
444
|
function eventsForSpecial(ev) {
|
|
432
|
-
const kind = String(ev.kind || 'event');
|
|
433
445
|
const start = new Date(parseInt(ev.start, 10));
|
|
434
446
|
const end = new Date(parseInt(ev.end, 10));
|
|
435
447
|
const startIso = start.toISOString();
|
|
436
448
|
const endIso = end.toISOString();
|
|
437
|
-
|
|
438
|
-
// Color coding:
|
|
439
|
-
// - event: purple
|
|
440
|
-
// - outing: blue
|
|
441
|
-
const palette = (kind === 'outing')
|
|
442
|
-
? { bg: '#0d6efd', border: '#0d6efd' }
|
|
443
|
-
: { bg: '#8e44ad', border: '#8e44ad' };
|
|
444
|
-
|
|
445
|
-
const defaultTitle = (kind === 'outing') ? 'Sortie' : 'Évènement';
|
|
446
|
-
|
|
447
449
|
return {
|
|
448
450
|
id: `special:${ev.eid}`,
|
|
449
|
-
title: `${ev.title ||
|
|
451
|
+
title: `${ev.title || 'Évènement'}`.trim(),
|
|
450
452
|
allDay: false,
|
|
451
453
|
start: startIso,
|
|
452
454
|
end: endIso,
|
|
453
|
-
backgroundColor:
|
|
454
|
-
borderColor:
|
|
455
|
+
backgroundColor: '#8e44ad',
|
|
456
|
+
borderColor: '#8e44ad',
|
|
455
457
|
textColor: '#ffffff',
|
|
456
458
|
extendedProps: {
|
|
457
459
|
type: 'special',
|
|
458
|
-
kind,
|
|
459
460
|
eid: ev.eid,
|
|
460
|
-
|
|
461
|
-
title: ev.title || '',
|
|
461
|
+
title: ev.title || '',
|
|
462
462
|
notes: ev.notes || '',
|
|
463
463
|
pickupAddress: ev.address || '',
|
|
464
464
|
pickupLat: ev.lat || '',
|
|
@@ -469,6 +469,35 @@ function eventsForSpecial(ev) {
|
|
|
469
469
|
};
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
+
|
|
473
|
+
function eventsForOuting(o) {
|
|
474
|
+
const start = new Date(parseInt(o.start, 10));
|
|
475
|
+
const end = new Date(parseInt(o.end, 10));
|
|
476
|
+
const startIso = start.toISOString();
|
|
477
|
+
const endIso = end.toISOString();
|
|
478
|
+
return {
|
|
479
|
+
id: `outing:${o.oid}`,
|
|
480
|
+
title: `${o.title || 'Prévision de sortie'}`.trim(),
|
|
481
|
+
allDay: false,
|
|
482
|
+
start: startIso,
|
|
483
|
+
end: endIso,
|
|
484
|
+
backgroundColor: '#2980b9',
|
|
485
|
+
borderColor: '#2980b9',
|
|
486
|
+
textColor: '#ffffff',
|
|
487
|
+
extendedProps: {
|
|
488
|
+
type: 'outing',
|
|
489
|
+
oid: o.oid,
|
|
490
|
+
title: o.title || '',
|
|
491
|
+
notes: o.notes || '',
|
|
492
|
+
pickupAddress: o.address || '',
|
|
493
|
+
pickupLat: o.lat || '',
|
|
494
|
+
pickupLon: o.lon || '',
|
|
495
|
+
createdBy: o.uid || 0,
|
|
496
|
+
username: o.username || '',
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
472
501
|
const api = {};
|
|
473
502
|
|
|
474
503
|
function computeEtag(payload) {
|
|
@@ -493,8 +522,8 @@ api.getEvents = async function (req, res) {
|
|
|
493
522
|
const settings = await meta.settings.get('calendar-onekite');
|
|
494
523
|
const canMod = req.uid ? await canValidate(req.uid, settings) : false;
|
|
495
524
|
const widgetMode = String((req.query && req.query.widget) || '') === '1';
|
|
496
|
-
const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings
|
|
497
|
-
const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings
|
|
525
|
+
const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings) : false;
|
|
526
|
+
const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings) : false;
|
|
498
527
|
|
|
499
528
|
// Fetch a wider window because an event can start before the query range
|
|
500
529
|
// and still overlap.
|
|
@@ -611,7 +640,7 @@ api.getEvents = async function (req, res) {
|
|
|
611
640
|
type: 'special',
|
|
612
641
|
eid: sev.eid,
|
|
613
642
|
canCreateSpecial: canSpecialCreate,
|
|
614
|
-
canDeleteSpecial:
|
|
643
|
+
canDeleteSpecial: canSpecialDelete,
|
|
615
644
|
},
|
|
616
645
|
};
|
|
617
646
|
if (sev.username && (canMod || canSpecialDelete || (req.uid && String(req.uid) === String(sev.uid)))) {
|
|
@@ -623,6 +652,41 @@ api.getEvents = async function (req, res) {
|
|
|
623
652
|
// ignore
|
|
624
653
|
}
|
|
625
654
|
|
|
655
|
+
|
|
656
|
+
// Outings (prévisions de sortie)
|
|
657
|
+
try {
|
|
658
|
+
const outingIds = await dbLayer.listOutingIdsByStartRange(wideStart, endTs, 8000);
|
|
659
|
+
const outings = await dbLayer.getOutings(outingIds);
|
|
660
|
+
for (const o of (outings || [])) {
|
|
661
|
+
if (!o) continue;
|
|
662
|
+
const oStart = parseInt(o.start, 10);
|
|
663
|
+
const oEnd = parseInt(o.end, 10);
|
|
664
|
+
if (!(oStart < endTs && startTs < oEnd)) continue;
|
|
665
|
+
const full = eventsForOuting(o);
|
|
666
|
+
const minimal = {
|
|
667
|
+
id: full.id,
|
|
668
|
+
title: full.title,
|
|
669
|
+
allDay: full.allDay,
|
|
670
|
+
start: full.start,
|
|
671
|
+
end: full.end,
|
|
672
|
+
backgroundColor: full.backgroundColor,
|
|
673
|
+
borderColor: full.borderColor,
|
|
674
|
+
textColor: full.textColor,
|
|
675
|
+
extendedProps: {
|
|
676
|
+
type: 'outing',
|
|
677
|
+
oid: o.oid,
|
|
678
|
+
canDeleteOuting: canMod || (req.uid && String(req.uid) === String(o.uid)),
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
if (o.username && (canMod || (req.uid && String(req.uid) === String(o.uid)))) {
|
|
682
|
+
minimal.extendedProps.username = String(o.username);
|
|
683
|
+
}
|
|
684
|
+
out.push(minimal);
|
|
685
|
+
}
|
|
686
|
+
} catch (e) {
|
|
687
|
+
// ignore
|
|
688
|
+
}
|
|
689
|
+
|
|
626
690
|
// Stable ordering -> stable ETag
|
|
627
691
|
out.sort((a, b) => {
|
|
628
692
|
const as = String(a.start || '');
|
|
@@ -696,19 +760,17 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
696
760
|
|
|
697
761
|
const settings = await meta.settings.get('calendar-onekite');
|
|
698
762
|
const canMod = await canValidate(uid, settings);
|
|
763
|
+
const canSpecialDelete = await canDeleteSpecial(uid, settings);
|
|
699
764
|
|
|
700
765
|
const eid = String(req.params.eid || '').trim();
|
|
701
766
|
if (!eid) return res.status(400).json({ error: 'missing-eid' });
|
|
702
767
|
const ev = await dbLayer.getSpecialEvent(eid);
|
|
703
768
|
if (!ev) return res.status(404).json({ error: 'not-found' });
|
|
704
769
|
|
|
705
|
-
const canSpecialDeleteResolved = await canDeleteSpecial(uid, settings, ev && ev.start ? Number(ev.start) : Date.now());
|
|
706
|
-
|
|
707
770
|
// Anyone who can see the calendar can view special events, but creator username
|
|
708
771
|
// is only visible to moderators/allowed users or the creator.
|
|
709
772
|
const out = {
|
|
710
773
|
eid: ev.eid,
|
|
711
|
-
kind: String(ev.kind || 'event'),
|
|
712
774
|
title: ev.title || '',
|
|
713
775
|
start: ev.start,
|
|
714
776
|
end: ev.end,
|
|
@@ -716,9 +778,9 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
716
778
|
lat: ev.lat || '',
|
|
717
779
|
lon: ev.lon || '',
|
|
718
780
|
notes: ev.notes || '',
|
|
719
|
-
canDeleteSpecial:
|
|
781
|
+
canDeleteSpecial: canSpecialDelete,
|
|
720
782
|
};
|
|
721
|
-
if (ev.username && (canMod ||
|
|
783
|
+
if (ev.username && (canMod || canSpecialDelete || (uid && String(uid) === String(ev.uid)))) {
|
|
722
784
|
out.username = String(ev.username);
|
|
723
785
|
}
|
|
724
786
|
return res.json(out);
|
|
@@ -730,28 +792,37 @@ api.getCapabilities = async function (req, res) {
|
|
|
730
792
|
const canMod = uid ? await canValidate(uid, settings) : false;
|
|
731
793
|
res.json({
|
|
732
794
|
canModerate: canMod,
|
|
733
|
-
canCreateSpecial: uid ? await canCreateSpecial(uid, settings
|
|
734
|
-
canDeleteSpecial: uid ? await canDeleteSpecial(uid, settings
|
|
795
|
+
canCreateSpecial: uid ? await canCreateSpecial(uid, settings) : false,
|
|
796
|
+
canDeleteSpecial: uid ? await canDeleteSpecial(uid, settings) : false,
|
|
797
|
+
canCreateOuting: uid ? await canRequest(uid, settings, Date.now()) : false,
|
|
735
798
|
});
|
|
736
799
|
};
|
|
737
800
|
|
|
738
801
|
api.createSpecialEvent = async function (req, res) {
|
|
739
802
|
const settings = await meta.settings.get('calendar-onekite');
|
|
740
803
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
804
|
+
const ok = await canCreateSpecial(req.uid, settings);
|
|
805
|
+
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
741
806
|
|
|
742
|
-
const
|
|
743
|
-
const safeKind = (kind === 'outing') ? 'outing' : 'event';
|
|
744
|
-
|
|
807
|
+
const title = String((req.body && req.body.title) || '').trim() || 'Évènement';
|
|
745
808
|
const startTs = toTs(req.body && req.body.start);
|
|
746
809
|
const endTs = toTs(req.body && req.body.end);
|
|
747
810
|
if (!Number.isFinite(startTs) || !Number.isFinite(endTs) || !(startTs < endTs)) {
|
|
748
811
|
return res.status(400).json({ error: 'bad-dates' });
|
|
749
812
|
}
|
|
750
813
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
814
|
+
// Business rule: nothing can be created in the past.
|
|
815
|
+
try {
|
|
816
|
+
const today0 = new Date();
|
|
817
|
+
today0.setHours(0, 0, 0, 0);
|
|
818
|
+
const today0ts = today0.getTime();
|
|
819
|
+
if (startTs < today0ts) {
|
|
820
|
+
return res.status(400).json({
|
|
821
|
+
error: 'date-too-soon',
|
|
822
|
+
message: "Impossible de créer pour une date passée.",
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
} catch (e) {}
|
|
755
826
|
const address = String((req.body && req.body.address) || '').trim();
|
|
756
827
|
const notes = String((req.body && req.body.notes) || '').trim();
|
|
757
828
|
const lat = String((req.body && req.body.lat) || '').trim();
|
|
@@ -761,7 +832,6 @@ api.createSpecialEvent = async function (req, res) {
|
|
|
761
832
|
const eid = crypto.randomUUID();
|
|
762
833
|
const ev = {
|
|
763
834
|
eid,
|
|
764
|
-
kind: safeKind,
|
|
765
835
|
title,
|
|
766
836
|
start: String(startTs),
|
|
767
837
|
end: String(endTs),
|
|
@@ -773,42 +843,142 @@ api.createSpecialEvent = async function (req, res) {
|
|
|
773
843
|
username: u && u.username ? String(u.username) : '',
|
|
774
844
|
createdAt: String(Date.now()),
|
|
775
845
|
};
|
|
776
|
-
|
|
777
846
|
await dbLayer.saveSpecialEvent(ev);
|
|
778
847
|
|
|
779
|
-
// Discord notifications (separate webhooks per kind)
|
|
780
848
|
try {
|
|
781
|
-
await discord.
|
|
849
|
+
await discord.notifySpecialEventCreated(settings, ev);
|
|
782
850
|
} catch (e) {}
|
|
783
851
|
|
|
784
852
|
// Real-time refresh for all viewers
|
|
785
|
-
realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'created', eid: ev.eid
|
|
853
|
+
realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'created', eid: ev.eid });
|
|
786
854
|
res.json({ ok: true, eid });
|
|
787
855
|
};
|
|
788
856
|
|
|
789
857
|
api.deleteSpecialEvent = async function (req, res) {
|
|
790
858
|
const settings = await meta.settings.get('calendar-onekite');
|
|
791
859
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
792
|
-
|
|
860
|
+
const ok = await canDeleteSpecial(req.uid, settings);
|
|
861
|
+
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
793
862
|
const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
|
|
794
863
|
if (!eid) return res.status(400).json({ error: 'bad-id' });
|
|
864
|
+
await dbLayer.removeSpecialEvent(eid);
|
|
795
865
|
|
|
796
|
-
|
|
797
|
-
|
|
866
|
+
try {
|
|
867
|
+
await discord.notifySpecialEventDeleted(settings, Object.assign({}, { eid }, { deletedBy: String(req.uid || '') }));
|
|
868
|
+
} catch (e) {}
|
|
869
|
+
|
|
870
|
+
realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'deleted', eid });
|
|
871
|
+
res.json({ ok: true });
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
api.getOutingDetails = async function (req, res) {
|
|
875
|
+
const uid = req.uid;
|
|
876
|
+
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
877
|
+
|
|
878
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
879
|
+
const canMod = await canValidate(uid, settings);
|
|
880
|
+
|
|
881
|
+
const oid = String(req.params.oid || '').trim();
|
|
882
|
+
if (!oid) return res.status(400).json({ error: 'missing-oid' });
|
|
883
|
+
const o = await dbLayer.getOuting(oid);
|
|
884
|
+
if (!o) return res.status(404).json({ error: 'not-found' });
|
|
885
|
+
|
|
886
|
+
const out = {
|
|
887
|
+
oid: o.oid,
|
|
888
|
+
title: o.title || '',
|
|
889
|
+
start: o.start,
|
|
890
|
+
end: o.end,
|
|
891
|
+
address: o.address || '',
|
|
892
|
+
lat: o.lat || '',
|
|
893
|
+
lon: o.lon || '',
|
|
894
|
+
notes: o.notes || '',
|
|
895
|
+
canDeleteOuting: canMod || (uid && String(uid) === String(o.uid)),
|
|
896
|
+
};
|
|
897
|
+
if (o.username && (canMod || (uid && String(uid) === String(o.uid)))) {
|
|
898
|
+
out.username = String(o.username);
|
|
899
|
+
}
|
|
900
|
+
return res.json(out);
|
|
901
|
+
};
|
|
798
902
|
|
|
799
|
-
|
|
903
|
+
api.createOuting = async function (req, res) {
|
|
904
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
905
|
+
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
800
906
|
|
|
801
|
-
const
|
|
907
|
+
const startTs = toTs(req.body && req.body.start);
|
|
908
|
+
const ok = await canRequest(req.uid, settings, startTs);
|
|
802
909
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
803
910
|
|
|
804
|
-
|
|
911
|
+
const title = String((req.body && req.body.title) || '').trim() || 'Prévision de sortie';
|
|
912
|
+
const endTs = toTs(req.body && req.body.end);
|
|
913
|
+
if (!Number.isFinite(startTs) || !Number.isFinite(endTs) || !(startTs < endTs)) {
|
|
914
|
+
return res.status(400).json({ error: 'bad-dates' });
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Business rule: nothing can be created in the past.
|
|
918
|
+
try {
|
|
919
|
+
const today0 = new Date();
|
|
920
|
+
today0.setHours(0, 0, 0, 0);
|
|
921
|
+
const today0ts = today0.getTime();
|
|
922
|
+
if (startTs < today0ts) {
|
|
923
|
+
return res.status(400).json({
|
|
924
|
+
error: 'date-too-soon',
|
|
925
|
+
message: "Impossible de créer pour une date passée.",
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
} catch (e) {}
|
|
929
|
+
|
|
930
|
+
const address = String((req.body && req.body.address) || '').trim();
|
|
931
|
+
const notes = String((req.body && req.body.notes) || '').trim();
|
|
932
|
+
const lat = String((req.body && req.body.lat) || '').trim();
|
|
933
|
+
const lon = String((req.body && req.body.lon) || '').trim();
|
|
934
|
+
|
|
935
|
+
const u = await user.getUserFields(req.uid, ['username']);
|
|
936
|
+
const oid = crypto.randomUUID();
|
|
937
|
+
const o = {
|
|
938
|
+
oid,
|
|
939
|
+
title,
|
|
940
|
+
start: String(startTs),
|
|
941
|
+
end: String(endTs),
|
|
942
|
+
address,
|
|
943
|
+
notes,
|
|
944
|
+
lat,
|
|
945
|
+
lon,
|
|
946
|
+
uid: String(req.uid),
|
|
947
|
+
username: u && u.username ? String(u.username) : '',
|
|
948
|
+
createdAt: String(Date.now()),
|
|
949
|
+
};
|
|
950
|
+
await dbLayer.saveOuting(o);
|
|
805
951
|
|
|
806
952
|
try {
|
|
807
|
-
await discord.
|
|
953
|
+
await discord.notifyOutingCreated(settings, o);
|
|
808
954
|
} catch (e) {}
|
|
809
955
|
|
|
810
|
-
realtime.emitCalendarUpdated({ kind: '
|
|
811
|
-
res.json({ ok: true });
|
|
956
|
+
realtime.emitCalendarUpdated({ kind: 'outing', action: 'created', oid: o.oid });
|
|
957
|
+
return res.json({ ok: true, oid });
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
api.deleteOuting = async function (req, res) {
|
|
961
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
962
|
+
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
963
|
+
|
|
964
|
+
const oid = String(req.params.oid || '').replace(/^outing:/, '').trim();
|
|
965
|
+
if (!oid) return res.status(400).json({ error: 'bad-id' });
|
|
966
|
+
|
|
967
|
+
const o = await dbLayer.getOuting(oid);
|
|
968
|
+
if (!o) return res.status(404).json({ error: 'not-found' });
|
|
969
|
+
|
|
970
|
+
const canMod = await canValidate(req.uid, settings);
|
|
971
|
+
const isOwner = String(o.uid) === String(req.uid);
|
|
972
|
+
if (!canMod && !isOwner) return res.status(403).json({ error: 'not-allowed' });
|
|
973
|
+
|
|
974
|
+
await dbLayer.removeOuting(oid);
|
|
975
|
+
|
|
976
|
+
try {
|
|
977
|
+
await discord.notifyOutingDeleted(settings, Object.assign({}, o, { deletedBy: String(req.uid || '') }));
|
|
978
|
+
} catch (e) {}
|
|
979
|
+
|
|
980
|
+
realtime.emitCalendarUpdated({ kind: 'outing', action: 'deleted', oid });
|
|
981
|
+
return res.json({ ok: true });
|
|
812
982
|
};
|
|
813
983
|
|
|
814
984
|
api.getItems = async function (req, res) {
|
package/lib/db.js
CHANGED
|
@@ -10,6 +10,10 @@ const KEY_CHECKOUT_INTENT_TO_RID = 'calendar-onekite:helloasso:checkoutIntentToR
|
|
|
10
10
|
const KEY_SPECIAL_ZSET = 'calendar-onekite:special';
|
|
11
11
|
const KEY_SPECIAL_OBJ = (eid) => `calendar-onekite:special:${eid}`;
|
|
12
12
|
|
|
13
|
+
// Outings (previsions de sortie)
|
|
14
|
+
const KEY_OUTING_ZSET = 'calendar-onekite:outings';
|
|
15
|
+
const KEY_OUTING_OBJ = (oid) => `calendar-onekite:outings:${oid}`;
|
|
16
|
+
|
|
13
17
|
// Maintenance (simple ON/OFF per item, no dates)
|
|
14
18
|
const KEY_MAINTENANCE_ZSET = 'calendar-onekite:maintenance:itemIds';
|
|
15
19
|
|
|
@@ -26,6 +30,10 @@ function specialKey(eid) {
|
|
|
26
30
|
return KEY_SPECIAL_OBJ(eid);
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
function outingKey(oid) {
|
|
34
|
+
return KEY_OUTING_OBJ(oid);
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
async function getReservation(rid) {
|
|
30
38
|
return await db.getObject(KEY_OBJ(rid));
|
|
31
39
|
}
|
|
@@ -208,6 +216,34 @@ module.exports = {
|
|
|
208
216
|
const stop = Math.max(0, (parseInt(limit, 10) || 2000) - 1);
|
|
209
217
|
return await db.getSortedSetRangeByScore(KEY_SPECIAL_ZSET, start, stop, startTs, endTs);
|
|
210
218
|
},
|
|
219
|
+
|
|
220
|
+
// Outings
|
|
221
|
+
getOuting: async (oid) => await db.getObject(KEY_OUTING_OBJ(oid)),
|
|
222
|
+
getOutings: async (oids) => {
|
|
223
|
+
const ids = Array.isArray(oids) ? oids.filter(Boolean) : [];
|
|
224
|
+
if (!ids.length) return [];
|
|
225
|
+
const keys = ids.map(outingKey);
|
|
226
|
+
const rows = await db.getObjects(keys);
|
|
227
|
+
return (rows || []).map((row, idx) => {
|
|
228
|
+
if (!row) return null;
|
|
229
|
+
if (!row.oid) row.oid = String(ids[idx]);
|
|
230
|
+
return row;
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
saveOuting: async (o) => {
|
|
234
|
+
await db.setObject(KEY_OUTING_OBJ(o.oid), o);
|
|
235
|
+
await db.sortedSetAdd(KEY_OUTING_ZSET, o.start, o.oid);
|
|
236
|
+
},
|
|
237
|
+
removeOuting: async (oid) => {
|
|
238
|
+
await db.sortedSetRemove(KEY_OUTING_ZSET, oid);
|
|
239
|
+
await db.delete(KEY_OUTING_OBJ(oid));
|
|
240
|
+
},
|
|
241
|
+
listOutingIdsByStartRange: async (startTs, endTs, limit = 5000) => {
|
|
242
|
+
const start = 0;
|
|
243
|
+
const stop = Math.max(0, (parseInt(limit, 10) || 5000) - 1);
|
|
244
|
+
return await db.getSortedSetRangeByScore(KEY_OUTING_ZSET, start, stop, startTs, endTs);
|
|
245
|
+
},
|
|
246
|
+
|
|
211
247
|
listReservationIdsByStartRange,
|
|
212
248
|
listAllReservationIds,
|
|
213
249
|
|
package/lib/discord.js
CHANGED
|
@@ -184,39 +184,39 @@ async function notifyReservationCancelled(settings, reservation) {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
|
|
187
|
-
function buildSpecialWebhookPayload(action, ev) {
|
|
188
|
-
const kind = String((ev && ev.kind) || 'event');
|
|
189
|
-
const webhookUsername = kind === 'outing'
|
|
190
|
-
? (action === 'deleted' ? 'Onekite • Sortie • Annulation' : 'Onekite • Sortie')
|
|
191
|
-
: (action === 'deleted' ? 'Onekite • Évènement • Annulation' : 'Onekite • Évènement');
|
|
192
187
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
188
|
+
// --------------------------
|
|
189
|
+
// Special events & outings
|
|
190
|
+
// --------------------------
|
|
191
|
+
|
|
192
|
+
function buildSimpleCalendarPayload(kind, label, entity, opts) {
|
|
193
|
+
const options = opts || {};
|
|
194
|
+
const calUrl = options.calUrl || 'https://www.onekite.com/calendar';
|
|
195
|
+
const webhookUsername = options.webhookUsername || `Onekite • ${label}`;
|
|
197
196
|
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const address = ev && ev.address ? String(ev.address) : '';
|
|
202
|
-
const notes = ev && ev.notes ? String(ev.notes) : '';
|
|
197
|
+
const title = kind === 'deleted' ? '❌ ' + label + ' annulé(e)' : label + ' créé(e)';
|
|
198
|
+
const showIcon = kind === 'deleted';
|
|
199
|
+
const embedTitle = showIcon ? title : title.replace(/^❌\s*/, '');
|
|
203
200
|
|
|
201
|
+
const start = entity && entity.start ? Number(entity.start) : NaN;
|
|
202
|
+
const end = entity && entity.end ? Number(entity.end) : NaN;
|
|
204
203
|
const fields = [];
|
|
205
|
-
|
|
204
|
+
const name = entity && entity.title ? String(entity.title) : '';
|
|
205
|
+
if (name) fields.push({ name: 'Titre', value: name, inline: false });
|
|
206
206
|
if (Number.isFinite(start) && Number.isFinite(end)) {
|
|
207
207
|
fields.push({ name: 'Période', value: `Du ${formatFRShort(start)} au ${formatFRShort(end)}`, inline: false });
|
|
208
208
|
}
|
|
209
|
-
|
|
210
|
-
if (
|
|
209
|
+
const username = entity && entity.username ? String(entity.username) : '';
|
|
210
|
+
if (username) fields.push({ name: 'Membre', value: username, inline: true });
|
|
211
211
|
|
|
212
212
|
return {
|
|
213
213
|
username: webhookUsername,
|
|
214
214
|
content: '',
|
|
215
215
|
embeds: [
|
|
216
216
|
{
|
|
217
|
-
title,
|
|
217
|
+
title: embedTitle,
|
|
218
218
|
url: calUrl,
|
|
219
|
-
description:
|
|
219
|
+
description: kind === 'deleted' ? 'Une entrée a été annulée.' : 'Une nouvelle entrée a été créée.',
|
|
220
220
|
fields,
|
|
221
221
|
footer: { text: 'Onekite • Calendrier' },
|
|
222
222
|
timestamp: new Date().toISOString(),
|
|
@@ -225,23 +225,51 @@ function buildSpecialWebhookPayload(action, ev) {
|
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
async function
|
|
229
|
-
const
|
|
230
|
-
const url = kind === 'outing'
|
|
231
|
-
? String((settings && settings.discordWebhookUrlOutings) || '').trim()
|
|
232
|
-
: String((settings && settings.discordWebhookUrlEvents) || '').trim();
|
|
228
|
+
async function notifySpecialEventCreated(settings, ev) {
|
|
229
|
+
const url = settings && settings.discordWebhookUrlEvents ? String(settings.discordWebhookUrlEvents).trim() : '';
|
|
233
230
|
if (!url) return;
|
|
231
|
+
try {
|
|
232
|
+
await postWebhook(url, buildSimpleCalendarPayload('created', 'Évènement', ev, { webhookUsername: 'Onekite • Évènement' }));
|
|
233
|
+
} catch (e) {
|
|
234
|
+
console.warn('[calendar-onekite] Discord webhook failed (event created)', e && e.message ? e.message : String(e));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
234
237
|
|
|
238
|
+
async function notifySpecialEventDeleted(settings, ev) {
|
|
239
|
+
const url = settings && settings.discordWebhookUrlEvents ? String(settings.discordWebhookUrlEvents).trim() : '';
|
|
240
|
+
if (!url) return;
|
|
235
241
|
try {
|
|
236
|
-
await postWebhook(url,
|
|
242
|
+
await postWebhook(url, buildSimpleCalendarPayload('deleted', 'Évènement', ev, { webhookUsername: 'Onekite • Annulation' }));
|
|
237
243
|
} catch (e) {
|
|
238
|
-
console.warn('[calendar-onekite] Discord webhook failed (
|
|
244
|
+
console.warn('[calendar-onekite] Discord webhook failed (event deleted)', e && e.message ? e.message : String(e));
|
|
239
245
|
}
|
|
240
246
|
}
|
|
241
247
|
|
|
248
|
+
async function notifyOutingCreated(settings, o) {
|
|
249
|
+
const url = settings && settings.discordWebhookUrlOutings ? String(settings.discordWebhookUrlOutings).trim() : '';
|
|
250
|
+
if (!url) return;
|
|
251
|
+
try {
|
|
252
|
+
await postWebhook(url, buildSimpleCalendarPayload('created', 'Sortie', o, { webhookUsername: 'Onekite • Sortie' }));
|
|
253
|
+
} catch (e) {
|
|
254
|
+
console.warn('[calendar-onekite] Discord webhook failed (outing created)', e && e.message ? e.message : String(e));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function notifyOutingDeleted(settings, o) {
|
|
259
|
+
const url = settings && settings.discordWebhookUrlOutings ? String(settings.discordWebhookUrlOutings).trim() : '';
|
|
260
|
+
if (!url) return;
|
|
261
|
+
try {
|
|
262
|
+
await postWebhook(url, buildSimpleCalendarPayload('deleted', 'Sortie', o, { webhookUsername: 'Onekite • Annulation' }));
|
|
263
|
+
} catch (e) {
|
|
264
|
+
console.warn('[calendar-onekite] Discord webhook failed (outing deleted)', e && e.message ? e.message : String(e));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
242
267
|
module.exports = {
|
|
243
268
|
notifyReservationRequested,
|
|
244
269
|
notifyPaymentReceived,
|
|
245
270
|
notifyReservationCancelled,
|
|
246
|
-
|
|
271
|
+
notifySpecialEventCreated,
|
|
272
|
+
notifySpecialEventDeleted,
|
|
273
|
+
notifyOutingCreated,
|
|
274
|
+
notifyOutingDeleted,
|
|
247
275
|
};
|
package/library.js
CHANGED
|
@@ -83,6 +83,11 @@ Plugin.init = async function (params) {
|
|
|
83
83
|
router.get('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.getSpecialEventDetails);
|
|
84
84
|
router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.deleteSpecialEvent);
|
|
85
85
|
|
|
86
|
+
// Outings (prévisions de sortie)
|
|
87
|
+
router.post('/api/v3/plugins/calendar-onekite/outings', ...publicExpose, api.createOuting);
|
|
88
|
+
router.get('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.getOutingDetails);
|
|
89
|
+
router.delete('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.deleteOuting);
|
|
90
|
+
|
|
86
91
|
// Admin API (JSON)
|
|
87
92
|
const adminBases = ['/api/v3/admin/plugins/calendar-onekite'];
|
|
88
93
|
|
|
@@ -103,6 +108,8 @@ Plugin.init = async function (params) {
|
|
|
103
108
|
|
|
104
109
|
// Purge special events by year
|
|
105
110
|
router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
|
|
111
|
+
// Purge outings by year
|
|
112
|
+
router.post(`${base}/outings/purge`, ...adminMws, admin.purgeOutingsByYear);
|
|
106
113
|
});
|
|
107
114
|
|
|
108
115
|
// HelloAsso callback endpoint (hardened)
|
package/package.json
CHANGED
package/plugin.json
CHANGED