nodebb-plugin-onekite-calendar 2.0.61 → 2.0.63

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,7 +3,6 @@
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');
@@ -13,193 +12,24 @@ const logger = require.main.require('./src/logger');
13
12
  const dbLayer = require('./db');
14
13
  const { getSettings } = require('./settings');
15
14
 
16
- // Resolve group identifiers from ACP.
17
- // Admins may enter a group *name* ("Test") or a *slug* ("onekite-ffvl-2026").
18
- // Depending on NodeBB version and group type (system/custom), some core methods
19
- // accept one or the other. We try both to be tolerant.
20
- async function getMembersByGroupIdentifier(groupIdentifier) {
21
- const id = String(groupIdentifier || '').trim();
22
- if (!id) return [];
23
-
24
- // First try direct.
25
- let members = [];
26
- try {
27
- members = await groups.getMembers(id, 0, -1);
28
- } catch (e) {
29
- members = [];
30
- }
31
- if (Array.isArray(members) && members.length) return members;
32
-
33
- // Then try slug -> groupName mapping when available.
34
- if (typeof groups.getGroupNameByGroupSlug === 'function') {
35
- let groupName = null;
36
- try {
37
- if (groups.getGroupNameByGroupSlug.length >= 2) {
38
- groupName = await new Promise((resolve) => {
39
- groups.getGroupNameByGroupSlug(id, (err, name) => resolve(err ? null : name));
40
- });
41
- } else {
42
- groupName = await groups.getGroupNameByGroupSlug(id);
43
- }
44
- } catch (e) {
45
- groupName = null;
46
- }
47
-
48
- if (groupName && String(groupName).trim() && String(groupName).trim() !== id) {
49
- try {
50
- members = await groups.getMembers(String(groupName).trim(), 0, -1);
51
- } catch (e) {
52
- members = [];
53
- }
54
- if (Array.isArray(members) && members.length) return members;
55
- }
56
- }
57
-
58
- return Array.isArray(members) ? members : [];
59
- }
60
-
61
-
62
- function normalizeUids(members) {
63
- if (!Array.isArray(members)) return [];
64
- const out = [];
65
- for (const m of members) {
66
- if (Number.isInteger(m)) { out.push(m); continue; }
67
- if (typeof m === 'string' && m.trim() && !Number.isNaN(parseInt(m, 10))) { out.push(parseInt(m, 10)); continue; }
68
- if (m && typeof m === 'object' && (Number.isInteger(m.uid) || (typeof m.uid === 'string' && m.uid.trim()))) {
69
- const u = Number.isInteger(m.uid) ? m.uid : parseInt(m.uid, 10);
70
- if (!Number.isNaN(u)) out.push(u);
71
- }
72
- }
73
- // de-dupe
74
- return Array.from(new Set(out));
75
- }
76
-
77
- // Fast membership check without N calls to groups.isMember.
78
- // NodeBB's groups.getUserGroups([uid]) returns an array (per uid) of group objects.
79
- // We compare against both group slugs and names to be tolerant with older settings.
80
- async function userInAnyGroup(uid, allowed) {
81
- if (!uid || !Array.isArray(allowed) || !allowed.length) return false;
82
- const ug = await groups.getUserGroups([uid]);
83
- const list = (ug && ug[0]) ? ug[0] : [];
84
- const seen = new Set();
85
- for (const g of list) {
86
- if (!g) continue;
87
- if (g.slug) seen.add(String(g.slug));
88
- if (g.name) seen.add(String(g.name));
89
- if (g.groupName) seen.add(String(g.groupName));
90
- if (g.displayName) seen.add(String(g.displayName));
91
- }
92
- return allowed.some(v => seen.has(String(v)));
93
- }
94
-
95
-
96
- function normalizeAllowedGroups(raw) {
97
- if (!raw) return [];
98
- if (Array.isArray(raw)) return raw.map(v => String(v).trim()).filter(Boolean);
99
- const s = String(raw).trim();
100
- if (!s) return [];
101
- // Some admin UIs store JSON arrays as strings
102
- if ((s.startsWith('[') && s.endsWith(']')) || (s.startsWith('"[') && s.endsWith(']"'))) {
103
- try {
104
- const parsed = JSON.parse(s.startsWith('"') ? JSON.parse(s) : s);
105
- if (Array.isArray(parsed)) return parsed.map(v => String(v).trim()).filter(Boolean);
106
- } catch (e) {}
107
- }
108
- return s.split(',').map(v => String(v).trim().replace(/^"+|"+$/g, '')).filter(Boolean);
109
- }
110
-
111
- function parseJsonArrayField(v) {
112
- if (!v) return [];
113
- if (Array.isArray(v)) return v;
114
- const s = String(v).trim();
115
- if (!s) return [];
116
- try {
117
- const parsed = JSON.parse(s);
118
- return Array.isArray(parsed) ? parsed : [];
119
- } catch (e) {
120
- // legacy fallback: comma-separated
121
- return s.split(',').map((x) => String(x).trim()).filter(Boolean);
122
- }
123
- }
124
-
125
- function normalizeUidList(uids) {
126
- const out = [];
127
- const arr = Array.isArray(uids) ? uids : parseJsonArrayField(uids);
128
- for (const u of arr) {
129
- const n = Number.isInteger(u) ? u : parseInt(String(u || '').trim(), 10);
130
- if (Number.isFinite(n) && n > 0) out.push(String(n));
131
- }
132
- return Array.from(new Set(out));
133
- }
134
-
135
- async function usernamesByUids(uids) {
136
- const ids = normalizeUidList(uids);
137
- if (!ids.length) return [];
138
- try {
139
- if (typeof user.getUsersFields === 'function') {
140
- let rows = await user.getUsersFields(ids, ['username']);
141
- if (Array.isArray(rows)) {
142
- rows = rows.map((row, idx) => (row ? Object.assign({ uid: ids[idx] }, row) : null));
143
- }
144
- return (rows || []).map((r) => r && r.username ? String(r.username) : '').filter(Boolean);
145
- }
146
- } catch (e) {}
147
- // Fallback (slower)
148
- const rows = await Promise.all(ids.map(async (uid) => {
149
- try {
150
- const r = await user.getUserFields(uid, ['username']);
151
- return r && r.username ? String(r.username) : '';
152
- } catch (e) {
153
- return '';
154
- }
155
- }));
156
- return rows.filter(Boolean);
157
- }
158
-
159
- // NOTE: Avoid per-group async checks (groups.isMember) when possible.
160
-
15
+ const nb = require("./nodebb-helpers");
16
+ const {
17
+ normalizeAllowedGroups,
18
+ normalizeUids,
19
+ getMembersByGroupIdentifier,
20
+ userInAnyGroup,
21
+ forumBaseUrl,
22
+ sendEmail,
23
+ normalizeUidList,
24
+ usernamesByUids,
25
+ } = nb;
161
26
 
162
27
  const helloasso = require('./helloasso');
163
28
  const discord = require('./discord');
164
29
  const realtime = require('./realtime');
165
30
 
166
- // Email helper (NodeBB 4.x): always send by uid.
167
- // Subject must be provided inside params.subject.
168
- async function sendEmail(template, uid, subject, data) {
169
- const toUid = Number.isInteger(uid) ? uid : (uid ? parseInt(uid, 10) : NaN);
170
- if (!Number.isInteger(toUid) || toUid <= 0) return;
171
-
172
- const params = Object.assign({}, data || {}, subject ? { subject } : {});
173
-
174
- try {
175
- if (typeof emailer.send !== 'function') return;
176
- // NodeBB 4.x: send(template, uid, params)
177
- // NOTE: Do NOT branch on function.length: it is unreliable once the function
178
- // is wrapped/bound (common in production builds) and can lead to params being
179
- // dropped, resulting in empty email bodies and missing subjects.
180
- await emailer.send(template, toUid, params);
181
- } catch (err) {
182
- // eslint-disable-next-line no-console
183
- console.warn('[calendar-onekite] Failed to send email', { template, uid: toUid, err: String((err && err.message) || err) });
184
- }
185
- }
186
-
187
- function normalizeBaseUrl(meta) {
188
- // Prefer meta.config.url, fallback to nconf.get('url')
189
- let base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
190
- if (!base) {
191
- base = String(nconf.get('url') || '').trim();
192
- }
193
- base = String(base || '').trim().replace(/\/$/, '');
194
- // Ensure absolute with scheme
195
- if (base && !/^https?:\/\//i.test(base)) {
196
- base = `https://${base.replace(/^\/\//, '')}`;
197
- }
198
- return base;
199
- }
200
-
201
31
  function normalizeCallbackUrl(configured, meta) {
202
- const base = normalizeBaseUrl(meta);
32
+ const base = forumBaseUrl();
203
33
  let url = (configured || '').trim();
204
34
  if (!url) {
205
35
  // Default webhook endpoint (recommended): namespaced under /plugins
@@ -216,7 +46,7 @@ function normalizeCallbackUrl(configured, meta) {
216
46
  }
217
47
 
218
48
  function normalizeReturnUrl(meta) {
219
- const base = normalizeBaseUrl(meta);
49
+ const base = forumBaseUrl();
220
50
  return base ? `${base}/calendar` : '';
221
51
  }
222
52
 
@@ -420,6 +250,26 @@ async function canCreateSpecial(uid, settings) {
420
250
  return false;
421
251
  }
422
252
 
253
+
254
+ async function canJoinSpecial(uid, settings) {
255
+ if (!uid) return false;
256
+ // Admins always allowed
257
+ try {
258
+ const isAdmin = await groups.isMember(uid, 'administrators');
259
+ if (isAdmin) return true;
260
+ } catch (e) {}
261
+
262
+ // Special-event creators can join/leave
263
+ if (await canCreateSpecial(uid, settings)) return true;
264
+
265
+ // Location creator groups can also participate (even if they can't create events)
266
+ const allowed = normalizeAllowedGroups(settings.creatorGroups || '');
267
+ if (!allowed.length) return false;
268
+ return userInAnyGroup(uid, allowed);
269
+ }
270
+
271
+
272
+
423
273
  async function canDeleteSpecial(uid, settings) {
424
274
  if (!uid) return false;
425
275
  try {
@@ -958,7 +808,7 @@ api.getSpecialEventDetails = async function (req, res) {
958
808
  participants,
959
809
  participantsUsernames: await usernamesByUids(participants),
960
810
  canDeleteSpecial: canSpecialDelete,
961
- canJoin: uid ? await canCreateSpecial(uid, settings) : false,
811
+ canJoin: uid ? await canJoinSpecial(uid, settings) : false,
962
812
  isParticipant: uid ? participants.includes(String(uid)) : false,
963
813
  icsUrl: links.icsUrl,
964
814
  googleCalUrl: links.googleCalUrl,
@@ -973,7 +823,7 @@ api.joinSpecialEvent = async function (req, res) {
973
823
  const settings = await getSettings();
974
824
  const uid = req.uid;
975
825
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
976
- const ok = await canCreateSpecial(uid, settings);
826
+ const ok = await canJoinSpecial(uid, settings);
977
827
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
978
828
 
979
829
  const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
@@ -1000,7 +850,7 @@ api.leaveSpecialEvent = async function (req, res) {
1000
850
  const settings = await getSettings();
1001
851
  const uid = req.uid;
1002
852
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
1003
- const ok = await canCreateSpecial(uid, settings);
853
+ const ok = await canJoinSpecial(uid, settings);
1004
854
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
1005
855
 
1006
856
  const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
@@ -1569,7 +1419,7 @@ api.createReservation = async function (req, res) {
1569
1419
  total: resv.total || 0,
1570
1420
  // Link to the plugin ACP page so managers can process the request quickly.
1571
1421
  // Kept as `adminUrl` to match the email template placeholder.
1572
- adminUrl: `${normalizeBaseUrl(meta)}/admin/plugins/calendar-onekite`,
1422
+ adminUrl: `${forumBaseUrl()}/admin/plugins/calendar-onekite`,
1573
1423
  });
1574
1424
  }
1575
1425
  }
@@ -1921,7 +1771,7 @@ api.cancelReservation = async function (req, res) {
1921
1771
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
1922
1772
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
1923
1773
  cancelledBy: (r.cancelledByUsername || (canceller && canceller.username) || ''),
1924
- cancelledByUrl: ((r.cancelledByUsername || (canceller && canceller.username)) ? `${normalizeBaseUrl(meta)}/user/${encodeURIComponent(String(r.cancelledByUsername || canceller.username))}` : ''),
1774
+ cancelledByUrl: ((r.cancelledByUsername || (canceller && canceller.username)) ? `${forumBaseUrl()}/user/${encodeURIComponent(String(r.cancelledByUsername || canceller.username))}` : ''),
1925
1775
  });
1926
1776
  }
1927
1777
  } catch (e) {}
@@ -29,3 +29,5 @@ async function getGroupNameBySlug(slug) {
29
29
  }
30
30
  });
31
31
  }
32
+
33
+ module.exports = { getGroupNameBySlug };
@@ -0,0 +1,179 @@
1
+ 'use strict';
2
+
3
+ const nconf = require.main.require('nconf');
4
+ const meta = require.main.require('./src/meta');
5
+ const emailer = require.main.require('./src/emailer');
6
+ const groups = require.main.require('./src/groups');
7
+ const user = require.main.require('./src/user');
8
+
9
+ const { getGroupNameBySlug } = require('./group-helpers');
10
+
11
+ function normalizeAllowedGroups(raw) {
12
+ if (!raw) return [];
13
+ if (Array.isArray(raw)) {
14
+ return raw.map((v) => String(v || '').trim()).filter(Boolean);
15
+ }
16
+ const s = String(raw || '').trim();
17
+ if (!s) return [];
18
+
19
+ if (s.startsWith('[') && s.endsWith(']')) {
20
+ try {
21
+ const parsed = JSON.parse(s);
22
+ if (Array.isArray(parsed)) {
23
+ return parsed.map((v) => String(v || '').trim()).filter(Boolean);
24
+ }
25
+ } catch (e) {
26
+ // fall through
27
+ }
28
+ }
29
+
30
+ return s
31
+ .split(',')
32
+ .map((v) => String(v || '').trim().replace(/^"+|"+$/g, ''))
33
+ .filter(Boolean);
34
+ }
35
+
36
+ function normalizeUids(members) {
37
+ if (!Array.isArray(members)) return [];
38
+ const out = [];
39
+ for (const m of members) {
40
+ if (Number.isInteger(m)) {
41
+ out.push(m);
42
+ continue;
43
+ }
44
+ if (typeof m === 'string' && m.trim()) {
45
+ const n = parseInt(m, 10);
46
+ if (Number.isFinite(n)) out.push(n);
47
+ continue;
48
+ }
49
+ if (m && typeof m === 'object' && m.uid != null) {
50
+ const n = Number.isInteger(m.uid) ? m.uid : parseInt(String(m.uid).trim(), 10);
51
+ if (Number.isFinite(n)) out.push(n);
52
+ }
53
+ }
54
+ return Array.from(new Set(out)).filter((u) => Number.isInteger(u) && u > 0);
55
+ }
56
+
57
+ async function getMembersByGroupIdentifier(groupIdentifier) {
58
+ const id = String(groupIdentifier || '').trim();
59
+ if (!id) return [];
60
+
61
+ try {
62
+ const members = await groups.getMembers(id, 0, -1);
63
+ if (Array.isArray(members) && members.length) return members;
64
+ } catch (e) {
65
+ // ignore
66
+ }
67
+
68
+ const groupName = await getGroupNameBySlug(id);
69
+ if (groupName && String(groupName).trim() && String(groupName).trim() !== id) {
70
+ try {
71
+ const members = await groups.getMembers(String(groupName).trim(), 0, -1);
72
+ return Array.isArray(members) ? members : [];
73
+ } catch (e) {
74
+ return [];
75
+ }
76
+ }
77
+
78
+ return [];
79
+ }
80
+
81
+ async function userInAnyGroup(uid, allowedGroups) {
82
+ const allowed = normalizeAllowedGroups(allowedGroups);
83
+ if (!uid || !allowed.length) return false;
84
+
85
+ const ug = await groups.getUserGroups([uid]);
86
+ const list = (ug && ug[0]) ? ug[0] : [];
87
+
88
+ const seen = new Set();
89
+ for (const g of list) {
90
+ if (!g) continue;
91
+ if (g.slug) seen.add(String(g.slug));
92
+ if (g.name) seen.add(String(g.name));
93
+ if (g.groupName) seen.add(String(g.groupName));
94
+ if (g.displayName) seen.add(String(g.displayName));
95
+ }
96
+
97
+ return allowed.some((v) => seen.has(String(v)));
98
+ }
99
+
100
+ function forumBaseUrl() {
101
+ let base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
102
+ if (!base) base = String(nconf.get('url') || '').trim();
103
+ base = String(base || '').trim().replace(/\/$/, '');
104
+ if (base && !/^https?:\/\//i.test(base)) {
105
+ base = `https://${base.replace(/^\/\//, '')}`;
106
+ }
107
+ return base;
108
+ }
109
+
110
+ async function sendEmail(template, uid, subject, params) {
111
+ const toUid = Number.isInteger(uid) ? uid : parseInt(String(uid || '').trim(), 10);
112
+ if (!Number.isFinite(toUid) || toUid <= 0) return;
113
+
114
+ const payload = Object.assign({}, params || {}, subject ? { subject } : {});
115
+
116
+ try {
117
+ if (typeof emailer.send !== 'function') return;
118
+ await emailer.send(template, toUid, payload);
119
+ } catch (err) {
120
+ // eslint-disable-next-line no-console
121
+ console.warn('[calendar-onekite] Failed to send email', { template, uid: toUid, err: String((err && err.message) || err) });
122
+ }
123
+ }
124
+
125
+ function normalizeUidList(uids) {
126
+ if (!uids) return [];
127
+ const arr = Array.isArray(uids) ? uids : (() => {
128
+ const s = String(uids || '').trim();
129
+ if (!s) return [];
130
+ try {
131
+ const parsed = JSON.parse(s);
132
+ return Array.isArray(parsed) ? parsed : [];
133
+ } catch (e) {
134
+ return s.split(',').map((x) => String(x).trim()).filter(Boolean);
135
+ }
136
+ })();
137
+
138
+ const out = [];
139
+ for (const u of arr) {
140
+ const n = Number.isInteger(u) ? u : parseInt(String(u || '').trim(), 10);
141
+ if (Number.isFinite(n) && n > 0) out.push(String(n));
142
+ }
143
+ return Array.from(new Set(out));
144
+ }
145
+
146
+ async function usernamesByUids(uids) {
147
+ const ids = normalizeUidList(uids);
148
+ if (!ids.length) return [];
149
+
150
+ try {
151
+ if (typeof user.getUsersFields === 'function') {
152
+ const rows = await user.getUsersFields(ids, ['username']);
153
+ return (rows || []).map((r) => (r && r.username ? String(r.username) : '')).filter(Boolean);
154
+ }
155
+ } catch (e) {
156
+ // fall through
157
+ }
158
+
159
+ const rows = await Promise.all(ids.map(async (uid) => {
160
+ try {
161
+ const r = await user.getUserFields(uid, ['username']);
162
+ return r && r.username ? String(r.username) : '';
163
+ } catch (e) {
164
+ return '';
165
+ }
166
+ }));
167
+ return rows.filter(Boolean);
168
+ }
169
+
170
+ module.exports = {
171
+ normalizeAllowedGroups,
172
+ normalizeUids,
173
+ getMembersByGroupIdentifier,
174
+ userInAnyGroup,
175
+ forumBaseUrl,
176
+ sendEmail,
177
+ normalizeUidList,
178
+ usernamesByUids,
179
+ };
package/lib/scheduler.js CHANGED
@@ -6,8 +6,8 @@ const dbLayer = require('./db');
6
6
  const discord = require('./discord');
7
7
  const realtime = require('./realtime');
8
8
  const nconf = require.main.require('nconf');
9
- const groups = require.main.require('./src/groups');
10
- const utils = require('./utils');
9
+ const utils = require("./utils");
10
+ const nb = require("./nodebb-helpers");
11
11
  const { getSettings } = require('./settings');
12
12
 
13
13
  let timer = null;
@@ -53,50 +53,8 @@ async function addOnce(key, value) {
53
53
  }
54
54
  }
55
55
 
56
- const { getSetting, formatFR, forumBaseUrl, normalizeAllowedGroups, normalizeUids, arrayifyNames } = utils;
57
-
58
- // Resolve group identifiers from ACP (name or slug) and return UIDs.
59
- async function getMembersByGroupIdentifier(groupIdentifier) {
60
- const id = String(groupIdentifier || '').trim();
61
- if (!id) return [];
62
-
63
- let members = [];
64
- try {
65
- members = await groups.getMembers(id, 0, -1);
66
- } catch (e) {
67
- members = [];
68
- }
69
- if (Array.isArray(members) && members.length) return members;
70
-
71
- if (typeof groups.getGroupNameByGroupSlug === 'function') {
72
- let groupName = null;
73
- try {
74
- if (groups.getGroupNameByGroupSlug.length >= 2) {
75
- groupName = await new Promise((resolve) => {
76
- groups.getGroupNameByGroupSlug(id, (err, name) => resolve(err ? null : name));
77
- });
78
- } else {
79
- groupName = await groups.getGroupNameByGroupSlug(id);
80
- }
81
- } catch (e) {
82
- groupName = null;
83
- }
84
-
85
- if (groupName && String(groupName).trim() && String(groupName).trim() !== id) {
86
- try {
87
- members = await groups.getMembers(String(groupName).trim(), 0, -1);
88
- } catch (e) {
89
- members = [];
90
- }
91
- if (Array.isArray(members) && members.length) return members;
92
- }
93
- }
94
-
95
- return Array.isArray(members) ? members : [];
96
- }
97
-
98
- // normalizeUids/normalizeAllowedGroups are provided by utils
99
-
56
+ const { getSetting, formatFR, arrayifyNames } = utils;
57
+ const { forumBaseUrl, normalizeAllowedGroups, normalizeUids, getMembersByGroupIdentifier, sendEmail } = nb;
100
58
  async function getValidatorUids(settings) {
101
59
  const out = new Set();
102
60
  // Always include administrators
@@ -144,26 +102,8 @@ async function expirePending() {
144
102
  const validatorReminderMins = parseInt(getSetting(settings, 'validatorReminderMinutesPending', '0'), 10) || 0;
145
103
  const now = Date.now();
146
104
 
147
- const emailer = require.main.require('./src/emailer');
148
105
  const user = require.main.require('./src/user');
149
106
 
150
- async function sendEmail(template, uid, subject, data) {
151
- const toUid = Number.isInteger(uid) ? uid : (uid ? parseInt(uid, 10) : NaN);
152
- if (!Number.isInteger(toUid) || toUid <= 0) return;
153
- const params = Object.assign({}, data || {}, subject ? { subject } : {});
154
- try {
155
- if (typeof emailer.send !== 'function') return;
156
- await emailer.send(template, toUid, params);
157
- } catch (err) {
158
- // eslint-disable-next-line no-console
159
- console.warn('[calendar-onekite] Failed to send email (scheduler)', {
160
- template,
161
- uid: toUid,
162
- err: String((err && err.message) || err),
163
- });
164
- }
165
- }
166
-
167
107
  const adminUrl = (() => {
168
108
  const base = forumBaseUrl();
169
109
  return base ? `${base}/admin/plugins/calendar-onekite` : '/admin/plugins/calendar-onekite';
@@ -268,31 +208,8 @@ async function processAwaitingPayment() {
268
208
  const ids = await dbLayer.listAllReservationIds(5000);
269
209
  if (!ids || !ids.length) return;
270
210
 
271
- const emailer = require.main.require('./src/emailer');
272
211
  const user = require.main.require('./src/user');
273
212
 
274
- // NodeBB 4.x: always send by uid.
275
- async function sendEmail(template, uid, subject, data) {
276
- const toUid = Number.isInteger(uid) ? uid : (uid ? parseInt(uid, 10) : NaN);
277
- if (!Number.isInteger(toUid) || toUid <= 0) return;
278
-
279
- const params = Object.assign({}, data || {}, subject ? { subject } : {});
280
-
281
- try {
282
- if (typeof emailer.send !== 'function') return;
283
- // NodeBB 4.x: send(template, uid, params)
284
- // Do NOT branch on function.length (unreliable once wrapped/bound).
285
- await emailer.send(template, toUid, params);
286
- } catch (err) {
287
- // eslint-disable-next-line no-console
288
- console.warn('[calendar-onekite] Failed to send email (scheduler)', {
289
- template,
290
- uid: toUid,
291
- err: String((err && err.message) || err),
292
- });
293
- }
294
- }
295
-
296
213
  const adminUrl = (() => {
297
214
  const base = forumBaseUrl();
298
215
  return base ? `${base}/admin/plugins/calendar-onekite` : '/admin/plugins/calendar-onekite';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.61",
3
+ "version": "2.0.63",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "2.0.61"
42
+ "version": "2.0.63"
43
43
  }
package/public/client.js CHANGED
@@ -568,6 +568,25 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
568
568
  alert(msg);
569
569
  }
570
570
 
571
+ function confirmDeletion(message) {
572
+ const msg = String(message || 'Confirmer la suppression ?');
573
+ return new Promise((resolve) => {
574
+ try {
575
+ bootbox.confirm({
576
+ title: 'Confirmation',
577
+ message: msg,
578
+ buttons: {
579
+ cancel: { label: 'Annuler', className: 'btn-secondary' },
580
+ confirm: { label: 'Supprimer', className: 'btn-danger' },
581
+ },
582
+ callback: (result) => resolve(!!result),
583
+ });
584
+ } catch (e) {
585
+ resolve(false);
586
+ }
587
+ });
588
+ }
589
+
571
590
  async function fetchJson(url, opts) {
572
591
  const res = await fetch(url, {
573
592
  credentials: 'same-origin',
@@ -1725,17 +1744,24 @@ function toDatetimeLocalValue(date) {
1725
1744
  close: { label: 'Fermer', className: 'btn-secondary' },
1726
1745
  ...(canDel ? {
1727
1746
  del: {
1728
- label: 'Annuler',
1747
+ label: 'Supprimer',
1729
1748
  className: 'btn-danger',
1730
- callback: async () => {
1731
- try {
1732
- const eid = String(p.eid || ev.id).replace(/^special:/, '');
1733
- await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}`, { method: 'DELETE' });
1734
- showAlert('success', 'Évènement supprimé.');
1735
- calendar.refetchEvents();
1736
- } catch (e) {
1737
- showAlert('error', 'Suppression impossible.');
1738
- }
1749
+ callback: () => {
1750
+ (async () => {
1751
+ const ok = await confirmDeletion('Supprimer cet évènement ?');
1752
+ if (!ok) return;
1753
+ try {
1754
+ const eid = String(p.eid || ev.id).replace(/^special:/, '');
1755
+ await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}`, { method: 'DELETE' });
1756
+ showAlert('success', 'Évènement supprimé.');
1757
+ invalidateEventsCache();
1758
+ scheduleRefetch(calendar);
1759
+ try { dlg.modal('hide'); } catch (e) { try { bootbox.hideAll(); } catch (e2) {} }
1760
+ } catch (e) {
1761
+ showAlert('error', 'Suppression impossible.');
1762
+ }
1763
+ })();
1764
+ return false;
1739
1765
  },
1740
1766
  },
1741
1767
  } : {}),
@@ -1868,17 +1894,24 @@ function toDatetimeLocalValue(date) {
1868
1894
  close: { label: 'Fermer', className: 'btn-secondary' },
1869
1895
  ...(canDel ? {
1870
1896
  del: {
1871
- label: 'Annuler',
1897
+ label: 'Supprimer',
1872
1898
  className: 'btn-danger',
1873
- callback: async () => {
1874
- try {
1875
- const oid = String(p.oid || ev.id).replace(/^outing:/, '');
1876
- await fetchJson(`/api/v3/plugins/calendar-onekite/outings/${encodeURIComponent(oid)}`, { method: 'DELETE' });
1877
- showAlert('success', 'Prévision annulée.');
1878
- calendar.refetchEvents();
1879
- } catch (e) {
1880
- showAlert('error', 'Annulation impossible.');
1881
- }
1899
+ callback: () => {
1900
+ (async () => {
1901
+ const ok = await confirmDeletion('Supprimer cette sortie ?');
1902
+ if (!ok) return;
1903
+ try {
1904
+ const oid = String(p.oid || ev.id).replace(/^outing:/, '');
1905
+ await fetchJson(`/api/v3/plugins/calendar-onekite/outings/${encodeURIComponent(oid)}`, { method: 'DELETE' });
1906
+ showAlert('success', 'Sortie supprimée.');
1907
+ invalidateEventsCache();
1908
+ scheduleRefetch(calendar);
1909
+ try { dlg.modal('hide'); } catch (e) { try { bootbox.hideAll(); } catch (e2) {} }
1910
+ } catch (e) {
1911
+ showAlert('error', 'Suppression impossible.');
1912
+ }
1913
+ })();
1914
+ return false;
1882
1915
  },
1883
1916
  },
1884
1917
  } : {}),
@@ -2041,18 +2074,24 @@ function toDatetimeLocalValue(date) {
2041
2074
  };
2042
2075
 
2043
2076
  const cancelBtn = showCancel ? {
2044
- label: 'Annuler',
2077
+ label: 'Supprimer',
2045
2078
  className: 'btn-danger',
2046
- callback: async () => {
2047
- try {
2048
- if (!lockAction(`cancel:${rid}`, 1200)) return false;
2049
- await cancelReservation(rid);
2050
- showAlert('success', 'Réservation annulée.');
2051
- invalidateEventsCache();
2052
- scheduleRefetch(calendar);
2053
- } catch (e) {
2054
- showAlert('error', 'Annulation impossible.');
2055
- }
2079
+ callback: () => {
2080
+ (async () => {
2081
+ const ok = await confirmDeletion('Supprimer cette location ?');
2082
+ if (!ok) return;
2083
+ try {
2084
+ if (!lockAction(`cancel:${rid}`, 1200)) return;
2085
+ await cancelReservation(rid);
2086
+ showAlert('success', 'Location supprimée.');
2087
+ invalidateEventsCache();
2088
+ scheduleRefetch(calendar);
2089
+ try { bootbox.hideAll(); } catch (e) {}
2090
+ } catch (e) {
2091
+ showAlert('error', 'Suppression impossible.');
2092
+ }
2093
+ })();
2094
+ return false;
2056
2095
  },
2057
2096
  } : null;
2058
2097
  if (showPay) {