nodebb-plugin-onekite-calendar 1.0.14 → 1.0.20
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/CHANGELOG.md +1 -0
- package/lib/api.js +96 -14
- package/lib/discord.js +31 -4
- package/lib/scheduler.js +56 -13
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/templates/admin/plugins/calendar-onekite.tpl +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
## 1.0.3
|
|
4
4
|
- Suppression du texte d’archivage dans le toast de purge (plus de « 0 archivés »)
|
|
5
5
|
- Renommage du plugin : nodebb-plugin-onekite-calendar
|
|
6
|
+
- Discord : notification « ❌ Réservation annulée » (annulation manuelle + annulation automatique) + option ACP
|
|
6
7
|
|
|
7
8
|
## 1.0.2
|
|
8
9
|
- Purge calendrier : suppression réelle des réservations (aucune logique d’archivage)
|
package/lib/api.js
CHANGED
|
@@ -12,6 +12,51 @@ const logger = require.main.require('./src/logger');
|
|
|
12
12
|
|
|
13
13
|
const dbLayer = require('./db');
|
|
14
14
|
|
|
15
|
+
// Resolve group identifiers from ACP.
|
|
16
|
+
// Admins may enter a group *name* ("Test") or a *slug* ("onekite-ffvl-2026").
|
|
17
|
+
// Depending on NodeBB version and group type (system/custom), some core methods
|
|
18
|
+
// accept one or the other. We try both to be tolerant.
|
|
19
|
+
async function getMembersByGroupIdentifier(groupIdentifier) {
|
|
20
|
+
const id = String(groupIdentifier || '').trim();
|
|
21
|
+
if (!id) return [];
|
|
22
|
+
|
|
23
|
+
// First try direct.
|
|
24
|
+
let members = [];
|
|
25
|
+
try {
|
|
26
|
+
members = await groups.getMembers(id, 0, -1);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
members = [];
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(members) && members.length) return members;
|
|
31
|
+
|
|
32
|
+
// Then try slug -> groupName mapping when available.
|
|
33
|
+
if (typeof groups.getGroupNameByGroupSlug === 'function') {
|
|
34
|
+
let groupName = null;
|
|
35
|
+
try {
|
|
36
|
+
if (groups.getGroupNameByGroupSlug.length >= 2) {
|
|
37
|
+
groupName = await new Promise((resolve) => {
|
|
38
|
+
groups.getGroupNameByGroupSlug(id, (err, name) => resolve(err ? null : name));
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
groupName = await groups.getGroupNameByGroupSlug(id);
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
groupName = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (groupName && String(groupName).trim() && String(groupName).trim() !== id) {
|
|
48
|
+
try {
|
|
49
|
+
members = await groups.getMembers(String(groupName).trim(), 0, -1);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
members = [];
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(members) && members.length) return members;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Array.isArray(members) ? members : [];
|
|
58
|
+
}
|
|
59
|
+
|
|
15
60
|
// Fast membership check without N calls to groups.isMember.
|
|
16
61
|
// NodeBB's groups.getUserGroups([uid]) returns an array (per uid) of group objects.
|
|
17
62
|
// We compare against both group slugs and names to be tolerant with older settings.
|
|
@@ -56,30 +101,50 @@ const discord = require('./discord');
|
|
|
56
101
|
// We try the common forms. Any failure is logged for debugging.
|
|
57
102
|
async function sendEmail(template, toEmail, subject, data) {
|
|
58
103
|
if (!toEmail) return;
|
|
104
|
+
|
|
105
|
+
// Language can come from plugin settings or NodeBB config depending on version.
|
|
106
|
+
let lang = 'fr';
|
|
107
|
+
try {
|
|
108
|
+
const s = await meta.settings.get('calendar-onekite').catch(() => ({}));
|
|
109
|
+
lang = (s && s.defaultLang) || (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
110
|
+
} catch (e) {
|
|
111
|
+
lang = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Subject is not a positional arg; it must be injected via params.subject.
|
|
115
|
+
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
116
|
+
|
|
117
|
+
// Prefer sendToEmail when available (most consistent across versions).
|
|
59
118
|
try {
|
|
60
|
-
// NodeBB core signature (historically):
|
|
61
|
-
// Emailer.sendToEmail(template, email, language, params[, callback])
|
|
62
|
-
// Subject is not a positional arg; it must be injected (either by NodeBB itself
|
|
63
|
-
// or via filter:email.modify). We always pass it in params.subject.
|
|
64
|
-
const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
65
|
-
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
66
119
|
if (typeof emailer.sendToEmail === 'function') {
|
|
67
|
-
|
|
120
|
+
// NodeBB: sendToEmail(template, email, language, params)
|
|
121
|
+
if (emailer.sendToEmail.length >= 4) {
|
|
122
|
+
await emailer.sendToEmail(template, toEmail, lang, params);
|
|
123
|
+
} else {
|
|
124
|
+
// Older signature: sendToEmail(template, email, params)
|
|
125
|
+
await emailer.sendToEmail(template, toEmail, params);
|
|
126
|
+
}
|
|
68
127
|
return;
|
|
69
128
|
}
|
|
70
|
-
|
|
129
|
+
} catch (err) {
|
|
130
|
+
// eslint-disable-next-line no-console
|
|
131
|
+
console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String((err && err.message) || err) });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Very old/unusual builds: try send() but only if it clearly accepts an email.
|
|
135
|
+
try {
|
|
71
136
|
if (typeof emailer.send === 'function') {
|
|
72
137
|
// Some builds accept (template, email, language, params)
|
|
73
138
|
if (emailer.send.length >= 4) {
|
|
74
|
-
await emailer.send(template, toEmail,
|
|
75
|
-
|
|
139
|
+
await emailer.send(template, toEmail, lang, params);
|
|
140
|
+
} else {
|
|
141
|
+
// Some builds accept (template, email, params)
|
|
142
|
+
await emailer.send(template, toEmail, params);
|
|
76
143
|
}
|
|
77
|
-
// Some builds accept (template, email, params)
|
|
78
|
-
await emailer.send(template, toEmail, params);
|
|
79
144
|
}
|
|
80
145
|
} catch (err) {
|
|
81
146
|
// eslint-disable-next-line no-console
|
|
82
|
-
console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err) });
|
|
147
|
+
console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String((err && err.message) || err) });
|
|
83
148
|
}
|
|
84
149
|
}
|
|
85
150
|
|
|
@@ -710,7 +775,7 @@ api.createReservation = async function (req, res) {
|
|
|
710
775
|
const requester = await user.getUserFields(uid, ['username', 'email']);
|
|
711
776
|
const itemsLabel = (resv.itemNames || []).join(', ');
|
|
712
777
|
for (const g of notifyGroups) {
|
|
713
|
-
const members = await
|
|
778
|
+
const members = await getMembersByGroupIdentifier(g);
|
|
714
779
|
const uids = Array.isArray(members) ? members : [];
|
|
715
780
|
|
|
716
781
|
// Batch fetch user email/username when supported by this NodeBB version.
|
|
@@ -935,6 +1000,23 @@ api.cancelReservation = async function (req, res) {
|
|
|
935
1000
|
r.cancelledBy = uid;
|
|
936
1001
|
|
|
937
1002
|
await dbLayer.saveReservation(r);
|
|
1003
|
+
|
|
1004
|
+
// Discord webhook (optional)
|
|
1005
|
+
try {
|
|
1006
|
+
await discord.notifyReservationCancelled(settings, {
|
|
1007
|
+
rid: r.rid,
|
|
1008
|
+
uid: r.uid,
|
|
1009
|
+
username: r.username || '',
|
|
1010
|
+
itemIds: r.itemIds || [],
|
|
1011
|
+
itemNames: r.itemNames || [],
|
|
1012
|
+
start: r.start,
|
|
1013
|
+
end: r.end,
|
|
1014
|
+
status: r.status,
|
|
1015
|
+
cancelledAt: r.cancelledAt,
|
|
1016
|
+
cancelledBy: r.cancelledBy,
|
|
1017
|
+
});
|
|
1018
|
+
} catch (e) {}
|
|
1019
|
+
|
|
938
1020
|
return res.json({ ok: true, status: 'cancelled' });
|
|
939
1021
|
};
|
|
940
1022
|
|
package/lib/discord.js
CHANGED
|
@@ -88,7 +88,9 @@ function buildReservationMessage(kind, reservation) {
|
|
|
88
88
|
function buildWebhookPayload(kind, reservation) {
|
|
89
89
|
// Discord "regroupe" visuellement les messages consécutifs d'un même auteur.
|
|
90
90
|
// En utilisant un username différent par action, on obtient un message bien distinct.
|
|
91
|
-
const webhookUsername = kind === 'paid'
|
|
91
|
+
const webhookUsername = kind === 'paid'
|
|
92
|
+
? 'Onekite • Paiement'
|
|
93
|
+
: (kind === 'cancelled' ? 'Onekite • Annulation' : 'Onekite • Réservation');
|
|
92
94
|
|
|
93
95
|
const calUrl = 'https://www.onekite.com/calendar';
|
|
94
96
|
const username = reservation && reservation.username ? String(reservation.username) : '';
|
|
@@ -98,20 +100,29 @@ function buildWebhookPayload(kind, reservation) {
|
|
|
98
100
|
const start = reservation && reservation.start ? Number(reservation.start) : NaN;
|
|
99
101
|
const end = reservation && reservation.end ? Number(reservation.end) : NaN;
|
|
100
102
|
|
|
101
|
-
const title = kind === 'paid'
|
|
103
|
+
const title = kind === 'paid'
|
|
104
|
+
? '💳 Paiement reçu'
|
|
105
|
+
: (kind === 'cancelled' ? '❌ Réservation annulée' : '⏳ Demande de réservation');
|
|
102
106
|
|
|
103
107
|
const fields = [];
|
|
104
108
|
if (username) {
|
|
105
109
|
fields.push({ name: 'Membre', value: username, inline: true });
|
|
106
110
|
}
|
|
107
111
|
if (items.length) {
|
|
108
|
-
const label = kind === '
|
|
112
|
+
const label = kind === 'request' ? 'Matériel demandé' : 'Matériel';
|
|
109
113
|
fields.push({ name: label, value: items.map((it) => `• ${it}`).join('\n'), inline: false });
|
|
110
114
|
}
|
|
111
115
|
if (Number.isFinite(start) && Number.isFinite(end)) {
|
|
112
116
|
fields.push({ name: 'Période', value: `Du ${formatFRShort(start)} au ${formatFRShort(end)}`, inline: false });
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
if (kind === 'cancelled') {
|
|
120
|
+
const cancelledAt = reservation && reservation.cancelledAt ? Number(reservation.cancelledAt) : NaN;
|
|
121
|
+
if (Number.isFinite(cancelledAt)) {
|
|
122
|
+
fields.push({ name: 'Annulée le', value: formatFRShort(cancelledAt), inline: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
115
126
|
return {
|
|
116
127
|
username: webhookUsername,
|
|
117
128
|
// On laisse "content" vide pour privilégier l'embed (plus lisible sur Discord)
|
|
@@ -122,7 +133,9 @@ function buildWebhookPayload(kind, reservation) {
|
|
|
122
133
|
url: calUrl,
|
|
123
134
|
description: kind === 'paid'
|
|
124
135
|
? 'Un paiement a été reçu pour une réservation.'
|
|
125
|
-
:
|
|
136
|
+
: (kind === 'cancelled'
|
|
137
|
+
? 'Une réservation a été annulée.'
|
|
138
|
+
: 'Une nouvelle demande de réservation a été créée.'),
|
|
126
139
|
fields,
|
|
127
140
|
footer: { text: 'Onekite • Calendrier' },
|
|
128
141
|
timestamp: new Date().toISOString(),
|
|
@@ -157,7 +170,21 @@ async function notifyPaymentReceived(settings, reservation) {
|
|
|
157
170
|
}
|
|
158
171
|
}
|
|
159
172
|
|
|
173
|
+
async function notifyReservationCancelled(settings, reservation) {
|
|
174
|
+
const url = settings && settings.discordWebhookUrl ? String(settings.discordWebhookUrl).trim() : '';
|
|
175
|
+
if (!url) return;
|
|
176
|
+
if (!isEnabled(settings.discordNotifyOnCancelled, true)) return;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await postWebhook(url, buildWebhookPayload('cancelled', reservation));
|
|
180
|
+
} catch (e) {
|
|
181
|
+
// eslint-disable-next-line no-console
|
|
182
|
+
console.warn('[calendar-onekite] Discord webhook failed (cancelled)', e && e.message ? e.message : String(e));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
160
186
|
module.exports = {
|
|
161
187
|
notifyReservationRequested,
|
|
162
188
|
notifyPaymentReceived,
|
|
189
|
+
notifyReservationCancelled,
|
|
163
190
|
};
|
package/lib/scheduler.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const meta = require.main.require('./src/meta');
|
|
4
4
|
const db = require.main.require('./src/database');
|
|
5
5
|
const dbLayer = require('./db');
|
|
6
|
+
const discord = require('./discord');
|
|
6
7
|
|
|
7
8
|
let timer = null;
|
|
8
9
|
|
|
@@ -58,28 +59,47 @@ async function processAwaitingPayment() {
|
|
|
58
59
|
|
|
59
60
|
async function sendEmail(template, toEmail, subject, data) {
|
|
60
61
|
if (!toEmail) return;
|
|
62
|
+
|
|
63
|
+
// Language can come from plugin settings or NodeBB config depending on version.
|
|
64
|
+
let lang = 'fr';
|
|
61
65
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const s = await meta.settings.get('calendar-onekite').catch(() => ({}));
|
|
67
|
+
lang = (s && s.defaultLang) || (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
68
|
+
} catch (e) {
|
|
69
|
+
lang = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Subject is NOT a positional argument; it must be provided in params.subject.
|
|
73
|
+
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
68
74
|
|
|
75
|
+
try {
|
|
69
76
|
if (typeof emailer.sendToEmail === 'function') {
|
|
70
|
-
|
|
77
|
+
// NodeBB: sendToEmail(template, email, language, params)
|
|
78
|
+
if (emailer.sendToEmail.length >= 4) {
|
|
79
|
+
await emailer.sendToEmail(template, toEmail, lang, params);
|
|
80
|
+
} else {
|
|
81
|
+
// Older signature: sendToEmail(template, email, params)
|
|
82
|
+
await emailer.sendToEmail(template, toEmail, params);
|
|
83
|
+
}
|
|
71
84
|
return;
|
|
72
85
|
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// eslint-disable-next-line no-console
|
|
88
|
+
console.warn('[calendar-onekite] Failed to send email (scheduler)', {
|
|
89
|
+
template,
|
|
90
|
+
toEmail,
|
|
91
|
+
err: String((err && err.message) || err),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
73
94
|
|
|
74
|
-
|
|
95
|
+
// Very old/unusual builds: try send() but only if it clearly accepts an email.
|
|
96
|
+
try {
|
|
75
97
|
if (typeof emailer.send === 'function') {
|
|
76
|
-
// Some builds accept (template, email, language, params)
|
|
77
98
|
if (emailer.send.length >= 4) {
|
|
78
|
-
await emailer.send(template, toEmail,
|
|
79
|
-
|
|
99
|
+
await emailer.send(template, toEmail, lang, params);
|
|
100
|
+
} else {
|
|
101
|
+
await emailer.send(template, toEmail, params);
|
|
80
102
|
}
|
|
81
|
-
// Some builds accept (template, email, params)
|
|
82
|
-
await emailer.send(template, toEmail, params);
|
|
83
103
|
}
|
|
84
104
|
} catch (err) {
|
|
85
105
|
// eslint-disable-next-line no-console
|
|
@@ -144,6 +164,12 @@ async function processAwaitingPayment() {
|
|
|
144
164
|
const expiredKey = 'calendar-onekite:email:expiredSent';
|
|
145
165
|
const firstExpired = await db.setAdd(expiredKey, rid);
|
|
146
166
|
const shouldEmail = !!firstExpired;
|
|
167
|
+
|
|
168
|
+
// Guard Discord notification across clustered NodeBB processes
|
|
169
|
+
const discordKey = 'calendar-onekite:discord:cancelledSent';
|
|
170
|
+
const firstDiscord = await db.setAdd(discordKey, rid);
|
|
171
|
+
const shouldDiscord = !!firstDiscord;
|
|
172
|
+
|
|
147
173
|
const u = await user.getUserFields(r.uid, ['username', 'email']);
|
|
148
174
|
if (shouldEmail && u && u.email) {
|
|
149
175
|
await sendEmail('calendar-onekite_expired', u.email, 'Location matériel - Rappel', {
|
|
@@ -154,6 +180,23 @@ async function processAwaitingPayment() {
|
|
|
154
180
|
delayMinutes: holdMins,
|
|
155
181
|
});
|
|
156
182
|
}
|
|
183
|
+
|
|
184
|
+
if (shouldDiscord) {
|
|
185
|
+
try {
|
|
186
|
+
await discord.notifyReservationCancelled(settings, {
|
|
187
|
+
rid: r.rid || rid,
|
|
188
|
+
uid: r.uid,
|
|
189
|
+
username: (u && u.username) ? u.username : (r.username || ''),
|
|
190
|
+
itemIds: r.itemIds || [],
|
|
191
|
+
itemNames: r.itemNames || [],
|
|
192
|
+
start: r.start,
|
|
193
|
+
end: r.end,
|
|
194
|
+
status: 'cancelled',
|
|
195
|
+
cancelledAt: now,
|
|
196
|
+
cancelledBy: 'system',
|
|
197
|
+
});
|
|
198
|
+
} catch (e) {}
|
|
199
|
+
}
|
|
157
200
|
await dbLayer.removeReservation(rid);
|
|
158
201
|
}
|
|
159
202
|
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -78,6 +78,15 @@
|
|
|
78
78
|
</select>
|
|
79
79
|
</div>
|
|
80
80
|
|
|
81
|
+
<div class="mb-3">
|
|
82
|
+
<label class="form-label">Envoyer une notification à l'annulation</label>
|
|
83
|
+
<select class="form-select" name="discordNotifyOnCancelled">
|
|
84
|
+
<option value="1">Oui</option>
|
|
85
|
+
<option value="0">Non</option>
|
|
86
|
+
</select>
|
|
87
|
+
<div class="form-text">Inclut les annulations manuelles et les annulations automatiques (paiement non reçu dans les délais).</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
81
90
|
<h4 class="mt-4">HelloAsso</h4>
|
|
82
91
|
|
|
83
92
|
<div class="mb-3">
|