nodebb-plugin-onekite-calendar 2.0.62 → 2.0.64
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 +51 -189
- package/lib/group-helpers.js +2 -0
- package/lib/nodebb-helpers.js +179 -0
- package/lib/realtime.js +10 -0
- package/lib/scheduler.js +4 -87
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/client.js +129 -33
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 =
|
|
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 =
|
|
49
|
+
const base = forumBaseUrl();
|
|
220
50
|
return base ? `${base}/calendar` : '';
|
|
221
51
|
}
|
|
222
52
|
|
|
@@ -1008,11 +838,19 @@ api.joinSpecialEvent = async function (req, res) {
|
|
|
1008
838
|
ev.participants = JSON.stringify(list);
|
|
1009
839
|
await dbLayer.saveSpecialEvent(ev);
|
|
1010
840
|
|
|
841
|
+
const names = await usernamesByUids(list);
|
|
842
|
+
|
|
1011
843
|
realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'participants', eid });
|
|
844
|
+
realtime.emitParticipantsUpdated({
|
|
845
|
+
type: 'special',
|
|
846
|
+
id: eid,
|
|
847
|
+
participants: list,
|
|
848
|
+
participantsUsernames: names,
|
|
849
|
+
});
|
|
1012
850
|
return res.json({
|
|
1013
851
|
ok: true,
|
|
1014
852
|
participants: list,
|
|
1015
|
-
participantsUsernames:
|
|
853
|
+
participantsUsernames: names,
|
|
1016
854
|
});
|
|
1017
855
|
};
|
|
1018
856
|
|
|
@@ -1035,8 +873,16 @@ api.leaveSpecialEvent = async function (req, res) {
|
|
|
1035
873
|
ev.participants = JSON.stringify(next);
|
|
1036
874
|
await dbLayer.saveSpecialEvent(ev);
|
|
1037
875
|
|
|
876
|
+
const names = await usernamesByUids(next);
|
|
877
|
+
|
|
1038
878
|
realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'participants', eid });
|
|
1039
|
-
|
|
879
|
+
realtime.emitParticipantsUpdated({
|
|
880
|
+
type: 'special',
|
|
881
|
+
id: eid,
|
|
882
|
+
participants: next,
|
|
883
|
+
participantsUsernames: names,
|
|
884
|
+
});
|
|
885
|
+
return res.json({ ok: true, participants: next, participantsUsernames: names });
|
|
1040
886
|
};
|
|
1041
887
|
|
|
1042
888
|
api.getCapabilities = async function (req, res) {
|
|
@@ -1202,8 +1048,16 @@ api.joinOuting = async function (req, res) {
|
|
|
1202
1048
|
o.participants = JSON.stringify(list);
|
|
1203
1049
|
await dbLayer.saveOuting(o);
|
|
1204
1050
|
|
|
1051
|
+
const names = await usernamesByUids(list);
|
|
1052
|
+
|
|
1205
1053
|
realtime.emitCalendarUpdated({ kind: 'outing', action: 'participants', oid });
|
|
1206
|
-
|
|
1054
|
+
realtime.emitParticipantsUpdated({
|
|
1055
|
+
type: 'outing',
|
|
1056
|
+
id: oid,
|
|
1057
|
+
participants: list,
|
|
1058
|
+
participantsUsernames: names,
|
|
1059
|
+
});
|
|
1060
|
+
return res.json({ ok: true, participants: list, participantsUsernames: names });
|
|
1207
1061
|
};
|
|
1208
1062
|
|
|
1209
1063
|
api.leaveOuting = async function (req, res) {
|
|
@@ -1226,8 +1080,16 @@ api.leaveOuting = async function (req, res) {
|
|
|
1226
1080
|
o.participants = JSON.stringify(next);
|
|
1227
1081
|
await dbLayer.saveOuting(o);
|
|
1228
1082
|
|
|
1083
|
+
const names = await usernamesByUids(next);
|
|
1084
|
+
|
|
1229
1085
|
realtime.emitCalendarUpdated({ kind: 'outing', action: 'participants', oid });
|
|
1230
|
-
|
|
1086
|
+
realtime.emitParticipantsUpdated({
|
|
1087
|
+
type: 'outing',
|
|
1088
|
+
id: oid,
|
|
1089
|
+
participants: next,
|
|
1090
|
+
participantsUsernames: names,
|
|
1091
|
+
});
|
|
1092
|
+
return res.json({ ok: true, participants: next, participantsUsernames: names });
|
|
1231
1093
|
};
|
|
1232
1094
|
|
|
1233
1095
|
api.createOuting = async function (req, res) {
|
|
@@ -1589,7 +1451,7 @@ api.createReservation = async function (req, res) {
|
|
|
1589
1451
|
total: resv.total || 0,
|
|
1590
1452
|
// Link to the plugin ACP page so managers can process the request quickly.
|
|
1591
1453
|
// Kept as `adminUrl` to match the email template placeholder.
|
|
1592
|
-
adminUrl: `${
|
|
1454
|
+
adminUrl: `${forumBaseUrl()}/admin/plugins/calendar-onekite`,
|
|
1593
1455
|
});
|
|
1594
1456
|
}
|
|
1595
1457
|
}
|
|
@@ -1941,7 +1803,7 @@ api.cancelReservation = async function (req, res) {
|
|
|
1941
1803
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
1942
1804
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
1943
1805
|
cancelledBy: (r.cancelledByUsername || (canceller && canceller.username) || ''),
|
|
1944
|
-
cancelledByUrl: ((r.cancelledByUsername || (canceller && canceller.username)) ? `${
|
|
1806
|
+
cancelledByUrl: ((r.cancelledByUsername || (canceller && canceller.username)) ? `${forumBaseUrl()}/user/${encodeURIComponent(String(r.cancelledByUsername || canceller.username))}` : ''),
|
|
1945
1807
|
});
|
|
1946
1808
|
}
|
|
1947
1809
|
} catch (e) {}
|
package/lib/group-helpers.js
CHANGED
|
@@ -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/realtime.js
CHANGED
|
@@ -27,6 +27,16 @@ function emitCalendarUpdated(payload) {
|
|
|
27
27
|
} catch (e) {}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function emitParticipantsUpdated(payload) {
|
|
31
|
+
try {
|
|
32
|
+
const server = getIO();
|
|
33
|
+
if (!server || !server.sockets || typeof server.sockets.emit !== 'function') return;
|
|
34
|
+
|
|
35
|
+
server.sockets.emit('event:calendar-onekite.participantsUpdated', payload || {});
|
|
36
|
+
} catch (e) {}
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
module.exports = {
|
|
31
40
|
emitCalendarUpdated,
|
|
41
|
+
emitParticipantsUpdated,
|
|
32
42
|
};
|
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
|
|
10
|
-
const
|
|
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,
|
|
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
package/plugin.json
CHANGED
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',
|
|
@@ -1686,7 +1705,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1686
1705
|
const leaveBtn = (canJoinHere && p.isParticipant)
|
|
1687
1706
|
? `<button type="button" class="btn btn-sm btn-danger ms-2 onekite-leave-special" data-eid="${eidPlain}" title="Se retirer" aria-label="Se retirer">−</button>`
|
|
1688
1707
|
: '';
|
|
1689
|
-
const participantsHtml = `<div class="mb-2" id="onekite-participants-special"><strong>Participants</strong>${joinBtn}${leaveBtn}<br>` +
|
|
1708
|
+
const participantsHtml = `<div class="mb-2" id="onekite-participants-special" data-eid="${eidPlain}"><strong>Participants</strong>${joinBtn}${leaveBtn}<br>` +
|
|
1690
1709
|
(participants.length
|
|
1691
1710
|
? participants.map((name) => {
|
|
1692
1711
|
const u = String(name || '').trim();
|
|
@@ -1725,17 +1744,24 @@ function toDatetimeLocalValue(date) {
|
|
|
1725
1744
|
close: { label: 'Fermer', className: 'btn-secondary' },
|
|
1726
1745
|
...(canDel ? {
|
|
1727
1746
|
del: {
|
|
1728
|
-
label: '
|
|
1747
|
+
label: 'Supprimer',
|
|
1729
1748
|
className: 'btn-danger',
|
|
1730
|
-
callback:
|
|
1731
|
-
|
|
1732
|
-
const
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
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
|
} : {}),
|
|
@@ -1829,7 +1855,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1829
1855
|
const leaveBtn = (canJoinHere && p.isParticipant)
|
|
1830
1856
|
? `<button type="button" class="btn btn-sm btn-danger ms-2 onekite-leave-outing" data-oid="${oidPlain}" title="Se retirer" aria-label="Se retirer">−</button>`
|
|
1831
1857
|
: '';
|
|
1832
|
-
const participantsHtml = `<div class="mb-2" id="onekite-participants-outing"><strong>Participants</strong>${joinBtn}${leaveBtn}<br>` +
|
|
1858
|
+
const participantsHtml = `<div class="mb-2" id="onekite-participants-outing" data-oid="${oidPlain}"><strong>Participants</strong>${joinBtn}${leaveBtn}<br>` +
|
|
1833
1859
|
(participants.length
|
|
1834
1860
|
? participants.map((name) => {
|
|
1835
1861
|
const u = String(name || '').trim();
|
|
@@ -1868,17 +1894,24 @@ function toDatetimeLocalValue(date) {
|
|
|
1868
1894
|
close: { label: 'Fermer', className: 'btn-secondary' },
|
|
1869
1895
|
...(canDel ? {
|
|
1870
1896
|
del: {
|
|
1871
|
-
label: '
|
|
1897
|
+
label: 'Supprimer',
|
|
1872
1898
|
className: 'btn-danger',
|
|
1873
|
-
callback:
|
|
1874
|
-
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
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: '
|
|
2077
|
+
label: 'Supprimer',
|
|
2045
2078
|
className: 'btn-danger',
|
|
2046
|
-
callback:
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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) {
|
|
@@ -2807,6 +2846,63 @@ try {
|
|
|
2807
2846
|
scheduleRefetch(cal);
|
|
2808
2847
|
} catch (e) {}
|
|
2809
2848
|
});
|
|
2849
|
+
|
|
2850
|
+
// Live update of participants list in open modals (special events & outings)
|
|
2851
|
+
socket.on('event:calendar-onekite.participantsUpdated', function (payload) {
|
|
2852
|
+
try {
|
|
2853
|
+
const p = payload || {};
|
|
2854
|
+
const type = String(p.type || '').trim();
|
|
2855
|
+
const id = String(p.id || '').trim();
|
|
2856
|
+
if (!type || !id) return;
|
|
2857
|
+
|
|
2858
|
+
const names = Array.isArray(p.participantsUsernames) ? p.participantsUsernames : [];
|
|
2859
|
+
const uids = Array.isArray(p.participants) ? p.participants.map(String) : [];
|
|
2860
|
+
|
|
2861
|
+
const uidNow = String(
|
|
2862
|
+
(window.config && window.config.uid) ||
|
|
2863
|
+
(typeof app !== 'undefined' && app && app.user && app.user.uid) ||
|
|
2864
|
+
(typeof ajaxify !== 'undefined' && ajaxify && ajaxify.data && ajaxify.data.uid) ||
|
|
2865
|
+
''
|
|
2866
|
+
);
|
|
2867
|
+
const isParticipant = uidNow && uids.includes(String(uidNow));
|
|
2868
|
+
|
|
2869
|
+
const sel = (type === 'special') ? '#onekite-participants-special' : (type === 'outing') ? '#onekite-participants-outing' : '';
|
|
2870
|
+
if (!sel) return;
|
|
2871
|
+
const candidates = document.querySelectorAll(sel);
|
|
2872
|
+
if (!candidates || !candidates.length) return;
|
|
2873
|
+
let box = null;
|
|
2874
|
+
for (const el of candidates) {
|
|
2875
|
+
if (!el) continue;
|
|
2876
|
+
const matchId = type === 'special' ? String(el.getAttribute('data-eid') || '') : String(el.getAttribute('data-oid') || '');
|
|
2877
|
+
if (matchId === id) { box = el; break; }
|
|
2878
|
+
}
|
|
2879
|
+
if (!box) return;
|
|
2880
|
+
|
|
2881
|
+
const hasControls = !!box.querySelector('.onekite-join-special, .onekite-leave-special, .onekite-join-outing, .onekite-leave-outing');
|
|
2882
|
+
let controlsHtml = '';
|
|
2883
|
+
if (hasControls && uidNow) {
|
|
2884
|
+
if (type === 'special') {
|
|
2885
|
+
controlsHtml = isParticipant
|
|
2886
|
+
? `<button type="button" class="btn btn-sm btn-danger ms-2 onekite-leave-special" data-eid="${escapeHtml(id)}" title="Se retirer" aria-label="Se retirer">−</button>`
|
|
2887
|
+
: `<button type="button" class="btn btn-sm btn-success ms-2 onekite-join-special" data-eid="${escapeHtml(id)}" title="S'ajouter" aria-label="S'ajouter">+</button>`;
|
|
2888
|
+
} else if (type === 'outing') {
|
|
2889
|
+
controlsHtml = isParticipant
|
|
2890
|
+
? `<button type="button" class="btn btn-sm btn-danger ms-2 onekite-leave-outing" data-oid="${escapeHtml(id)}" title="Se retirer" aria-label="Se retirer">−</button>`
|
|
2891
|
+
: `<button type="button" class="btn btn-sm btn-success ms-2 onekite-join-outing" data-oid="${escapeHtml(id)}" title="S'ajouter" aria-label="S'ajouter">+</button>`;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
const links = names.length
|
|
2896
|
+
? names.map((name) => {
|
|
2897
|
+
const u = String(name || '').trim();
|
|
2898
|
+
if (!u) return '';
|
|
2899
|
+
return `<a class="onekite-user-link me-2" href="${window.location.origin}/user/${encodeURIComponent(u)}">${escapeHtml(u)}</a>`;
|
|
2900
|
+
}).filter(Boolean).join('')
|
|
2901
|
+
: `<span class="text-muted">Aucun</span>`;
|
|
2902
|
+
|
|
2903
|
+
box.innerHTML = `<strong>Participants</strong>${controlsHtml}<br>${links}`;
|
|
2904
|
+
} catch (e) {}
|
|
2905
|
+
});
|
|
2810
2906
|
}
|
|
2811
2907
|
} catch (e) {}
|
|
2812
2908
|
|