nodebb-plugin-onekite-calendar 2.0.55 → 2.0.56
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 +118 -1
- package/library.js +4 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/client.js +88 -0
package/lib/api.js
CHANGED
|
@@ -108,6 +108,54 @@ function normalizeAllowedGroups(raw) {
|
|
|
108
108
|
return s.split(',').map(v => String(v).trim().replace(/^"+|"+$/g, '')).filter(Boolean);
|
|
109
109
|
}
|
|
110
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
|
+
|
|
111
159
|
// NOTE: Avoid per-group async checks (groups.isMember) when possible.
|
|
112
160
|
|
|
113
161
|
|
|
@@ -897,6 +945,7 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
897
945
|
|
|
898
946
|
// Anyone who can see the calendar can view special events, but creator username
|
|
899
947
|
// is only visible to moderators/allowed users or the creator.
|
|
948
|
+
const participants = normalizeUidList(ev.participants);
|
|
900
949
|
const out = {
|
|
901
950
|
eid: ev.eid,
|
|
902
951
|
title: ev.title || '',
|
|
@@ -906,7 +955,11 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
906
955
|
lat: ev.lat || '',
|
|
907
956
|
lon: ev.lon || '',
|
|
908
957
|
notes: ev.notes || '',
|
|
958
|
+
participants,
|
|
959
|
+
participantsUsernames: await usernamesByUids(participants),
|
|
909
960
|
canDeleteSpecial: canSpecialDelete,
|
|
961
|
+
canJoin: uid ? await canCreateSpecial(uid, settings) : false,
|
|
962
|
+
isParticipant: uid ? participants.includes(String(uid)) : false,
|
|
910
963
|
icsUrl: links.icsUrl,
|
|
911
964
|
googleCalUrl: links.googleCalUrl,
|
|
912
965
|
};
|
|
@@ -916,6 +969,33 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
916
969
|
return res.json(out);
|
|
917
970
|
};
|
|
918
971
|
|
|
972
|
+
api.joinSpecialEvent = async function (req, res) {
|
|
973
|
+
const settings = await getSettings();
|
|
974
|
+
const uid = req.uid;
|
|
975
|
+
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
976
|
+
const ok = await canCreateSpecial(uid, settings);
|
|
977
|
+
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
978
|
+
|
|
979
|
+
const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
|
|
980
|
+
if (!eid) return res.status(400).json({ error: 'bad-id' });
|
|
981
|
+
|
|
982
|
+
const ev = await dbLayer.getSpecialEvent(eid);
|
|
983
|
+
if (!ev) return res.status(404).json({ error: 'not-found' });
|
|
984
|
+
|
|
985
|
+
const list = normalizeUidList(ev.participants);
|
|
986
|
+
const sUid = String(uid);
|
|
987
|
+
if (!list.includes(sUid)) list.push(sUid);
|
|
988
|
+
ev.participants = JSON.stringify(list);
|
|
989
|
+
await dbLayer.saveSpecialEvent(ev);
|
|
990
|
+
|
|
991
|
+
realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'participants', eid });
|
|
992
|
+
return res.json({
|
|
993
|
+
ok: true,
|
|
994
|
+
participants: list,
|
|
995
|
+
participantsUsernames: await usernamesByUids(list),
|
|
996
|
+
});
|
|
997
|
+
};
|
|
998
|
+
|
|
919
999
|
api.getCapabilities = async function (req, res) {
|
|
920
1000
|
const settings = await getSettings();
|
|
921
1001
|
const uid = req.uid || 0;
|
|
@@ -974,6 +1054,8 @@ api.createSpecialEvent = async function (req, res) {
|
|
|
974
1054
|
uid: String(req.uid),
|
|
975
1055
|
username: u && u.username ? String(u.username) : '',
|
|
976
1056
|
createdAt: String(Date.now()),
|
|
1057
|
+
// Self-join by default (creator is a participant)
|
|
1058
|
+
participants: JSON.stringify([String(req.uid)]),
|
|
977
1059
|
};
|
|
978
1060
|
await dbLayer.saveSpecialEvent(ev);
|
|
979
1061
|
|
|
@@ -993,10 +1075,15 @@ api.deleteSpecialEvent = async function (req, res) {
|
|
|
993
1075
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
994
1076
|
const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
|
|
995
1077
|
if (!eid) return res.status(400).json({ error: 'bad-id' });
|
|
1078
|
+
// Fetch the full event before deletion so Discord (and logs) can include
|
|
1079
|
+
// the same information as on creation (title, dates, creator, etc.).
|
|
1080
|
+
const ev = await dbLayer.getSpecialEvent(eid);
|
|
1081
|
+
if (!ev) return res.status(404).json({ error: 'not-found' });
|
|
1082
|
+
|
|
996
1083
|
await dbLayer.removeSpecialEvent(eid);
|
|
997
1084
|
|
|
998
1085
|
try {
|
|
999
|
-
await discord.notifySpecialEventDeleted(settings, Object.assign({},
|
|
1086
|
+
await discord.notifySpecialEventDeleted(settings, Object.assign({}, ev, { deletedBy: String(req.uid || '') }));
|
|
1000
1087
|
} catch (e) {}
|
|
1001
1088
|
|
|
1002
1089
|
realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'deleted', eid });
|
|
@@ -1028,6 +1115,7 @@ api.getOutingDetails = async function (req, res) {
|
|
|
1028
1115
|
end: new Date(parseInt(o.end, 10)),
|
|
1029
1116
|
});
|
|
1030
1117
|
|
|
1118
|
+
const participants = normalizeUidList(o.participants);
|
|
1031
1119
|
const out = {
|
|
1032
1120
|
oid: o.oid,
|
|
1033
1121
|
title: o.title || '',
|
|
@@ -1037,6 +1125,10 @@ api.getOutingDetails = async function (req, res) {
|
|
|
1037
1125
|
lat: o.lat || '',
|
|
1038
1126
|
lon: o.lon || '',
|
|
1039
1127
|
notes: o.notes || '',
|
|
1128
|
+
participants,
|
|
1129
|
+
participantsUsernames: await usernamesByUids(participants),
|
|
1130
|
+
canJoin: uid ? await canRequest(uid, settings, Date.now()) : false,
|
|
1131
|
+
isParticipant: uid ? participants.includes(String(uid)) : false,
|
|
1040
1132
|
canDeleteOuting: canMod || (uid && String(uid) === String(o.uid)),
|
|
1041
1133
|
icsUrl: links.icsUrl,
|
|
1042
1134
|
googleCalUrl: links.googleCalUrl,
|
|
@@ -1047,6 +1139,30 @@ api.getOutingDetails = async function (req, res) {
|
|
|
1047
1139
|
return res.json(out);
|
|
1048
1140
|
};
|
|
1049
1141
|
|
|
1142
|
+
api.joinOuting = async function (req, res) {
|
|
1143
|
+
const settings = await getSettings();
|
|
1144
|
+
const uid = req.uid;
|
|
1145
|
+
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
1146
|
+
// Outings share the same rights as reservations/locations.
|
|
1147
|
+
const ok = await canRequest(uid, settings, Date.now());
|
|
1148
|
+
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
1149
|
+
|
|
1150
|
+
const oid = String(req.params.oid || '').replace(/^outing:/, '').trim();
|
|
1151
|
+
if (!oid) return res.status(400).json({ error: 'bad-id' });
|
|
1152
|
+
|
|
1153
|
+
const o = await dbLayer.getOuting(oid);
|
|
1154
|
+
if (!o) return res.status(404).json({ error: 'not-found' });
|
|
1155
|
+
|
|
1156
|
+
const list = normalizeUidList(o.participants);
|
|
1157
|
+
const sUid = String(uid);
|
|
1158
|
+
if (!list.includes(sUid)) list.push(sUid);
|
|
1159
|
+
o.participants = JSON.stringify(list);
|
|
1160
|
+
await dbLayer.saveOuting(o);
|
|
1161
|
+
|
|
1162
|
+
realtime.emitCalendarUpdated({ kind: 'outing', action: 'participants', oid });
|
|
1163
|
+
return res.json({ ok: true, participants: list, participantsUsernames: await usernamesByUids(list) });
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1050
1166
|
api.createOuting = async function (req, res) {
|
|
1051
1167
|
const settings = await getSettings();
|
|
1052
1168
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
@@ -1103,6 +1219,7 @@ api.createOuting = async function (req, res) {
|
|
|
1103
1219
|
uid: String(req.uid),
|
|
1104
1220
|
username: u && u.username ? String(u.username) : '',
|
|
1105
1221
|
createdAt: String(Date.now()),
|
|
1222
|
+
participants: JSON.stringify([String(req.uid)]),
|
|
1106
1223
|
};
|
|
1107
1224
|
await dbLayer.saveOuting(o);
|
|
1108
1225
|
|
package/library.js
CHANGED
|
@@ -82,11 +82,15 @@ Plugin.init = async function (params) {
|
|
|
82
82
|
router.post('/api/v3/plugins/calendar-onekite/special-events', ...publicExpose, api.createSpecialEvent);
|
|
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
|
+
// Participants (self-join) for special events
|
|
86
|
+
router.post('/api/v3/plugins/calendar-onekite/special-events/:eid/participants', ...publicExpose, api.joinSpecialEvent);
|
|
85
87
|
|
|
86
88
|
// Outings (prévisions de sortie)
|
|
87
89
|
router.post('/api/v3/plugins/calendar-onekite/outings', ...publicExpose, api.createOuting);
|
|
88
90
|
router.get('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.getOutingDetails);
|
|
89
91
|
router.delete('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.deleteOuting);
|
|
92
|
+
// Participants (self-join) for outings
|
|
93
|
+
router.post('/api/v3/plugins/calendar-onekite/outings/:oid/participants', ...publicExpose, api.joinOuting);
|
|
90
94
|
|
|
91
95
|
// Calendar export (ICS)
|
|
92
96
|
// Note: reservations are protected by a signature in the querystring.
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1674,6 +1674,19 @@ function toDatetimeLocalValue(date) {
|
|
|
1674
1674
|
const lon = Number(p.pickupLon || p.lon);
|
|
1675
1675
|
const hasCoords = Number.isFinite(lat) && Number.isFinite(lon);
|
|
1676
1676
|
const notes = String(p.notes || '').trim();
|
|
1677
|
+
const participants = Array.isArray(p.participantsUsernames) ? p.participantsUsernames : [];
|
|
1678
|
+
const joinBtn = (p.canJoin && !p.isParticipant)
|
|
1679
|
+
? `<a href="#" class="onekite-join-special ms-2" data-eid="${escapeHtml(String(p.eid || '').replace(/^special:/, ''))}" title="S'ajouter"><i class="fa fa-plus"></i></a>`
|
|
1680
|
+
: '';
|
|
1681
|
+
const participantsHtml = `<div class="mb-2" id="onekite-participants-special"><strong>Participants</strong>${joinBtn}<br>` +
|
|
1682
|
+
(participants.length
|
|
1683
|
+
? participants.map((name) => {
|
|
1684
|
+
const u = String(name || '').trim();
|
|
1685
|
+
if (!u) return '';
|
|
1686
|
+
return `<a class="onekite-user-link me-2" href="${window.location.origin}/user/${encodeURIComponent(u)}">${escapeHtml(u)}</a>`;
|
|
1687
|
+
}).filter(Boolean).join('')
|
|
1688
|
+
: `<span class="text-muted">Aucun</span>`) +
|
|
1689
|
+
`</div>`;
|
|
1677
1690
|
const icsUrl = String(p.icsUrl || (p.extendedProps && p.extendedProps.icsUrl) || '').trim();
|
|
1678
1691
|
const googleCalUrl = String(p.googleCalUrl || (p.extendedProps && p.extendedProps.googleCalUrl) || '').trim();
|
|
1679
1692
|
const calHtml = (icsUrl || googleCalUrl)
|
|
@@ -1692,6 +1705,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1692
1705
|
${userLine}
|
|
1693
1706
|
<div class="mb-2"><strong>Période</strong><br>${escapeHtml(formatDtWithTime(ev.start))} → ${escapeHtml(formatDtWithTime(ev.end))}</div>
|
|
1694
1707
|
${calHtml}
|
|
1708
|
+
${participantsHtml}
|
|
1695
1709
|
${addr ? `<div class="mb-2"><strong>Adresse</strong><br>${addrHtml}</div>` : ''}
|
|
1696
1710
|
${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
|
|
1697
1711
|
`;
|
|
@@ -1719,6 +1733,36 @@ function toDatetimeLocalValue(date) {
|
|
|
1719
1733
|
} : {}),
|
|
1720
1734
|
},
|
|
1721
1735
|
});
|
|
1736
|
+
// Self-join handler
|
|
1737
|
+
try {
|
|
1738
|
+
dlg.on('shown.bs.modal', () => {
|
|
1739
|
+
dlg.find('.onekite-join-special').off('click').on('click', async (e2) => {
|
|
1740
|
+
e2.preventDefault();
|
|
1741
|
+
const btn = e2.currentTarget;
|
|
1742
|
+
if (!btn) return;
|
|
1743
|
+
try {
|
|
1744
|
+
btn.classList.add('disabled');
|
|
1745
|
+
const eid = String(btn.getAttribute('data-eid') || '').trim();
|
|
1746
|
+
if (!eid) return;
|
|
1747
|
+
const r = await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}/participants`, { method: 'POST' });
|
|
1748
|
+
const names = Array.isArray(r && r.participantsUsernames) ? r.participantsUsernames : [];
|
|
1749
|
+
const box = dlg.find('#onekite-participants-special');
|
|
1750
|
+
if (box && box.length) {
|
|
1751
|
+
const links = names.length
|
|
1752
|
+
? names.map((name) => {
|
|
1753
|
+
const u = String(name || '').trim();
|
|
1754
|
+
if (!u) return '';
|
|
1755
|
+
return `<a class="onekite-user-link me-2" href="${window.location.origin}/user/${encodeURIComponent(u)}">${escapeHtml(u)}</a>`;
|
|
1756
|
+
}).filter(Boolean).join('')
|
|
1757
|
+
: `<span class="text-muted">Aucun</span>`;
|
|
1758
|
+
box.html(`<strong>Participants</strong><br>${links}`);
|
|
1759
|
+
}
|
|
1760
|
+
} catch (e3) {
|
|
1761
|
+
showAlert('error', "Impossible de s'ajouter.");
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
});
|
|
1765
|
+
} catch (e) {}
|
|
1722
1766
|
try {
|
|
1723
1767
|
dlg.on('hidden.bs.modal', function () { isDialogOpen = false; });
|
|
1724
1768
|
} catch (e) {
|
|
@@ -1738,6 +1782,19 @@ function toDatetimeLocalValue(date) {
|
|
|
1738
1782
|
const lon = Number(p.lon || p.pickupLon);
|
|
1739
1783
|
const hasCoords = Number.isFinite(lat) && Number.isFinite(lon);
|
|
1740
1784
|
const notes = String(p.notes || '').trim();
|
|
1785
|
+
const participants = Array.isArray(p.participantsUsernames) ? p.participantsUsernames : [];
|
|
1786
|
+
const joinBtn = (p.canJoin && !p.isParticipant)
|
|
1787
|
+
? `<a href="#" class="onekite-join-outing ms-2" data-oid="${escapeHtml(String(p.oid || '').replace(/^outing:/, ''))}" title="S'ajouter"><i class="fa fa-plus"></i></a>`
|
|
1788
|
+
: '';
|
|
1789
|
+
const participantsHtml = `<div class="mb-2" id="onekite-participants-outing"><strong>Participants</strong>${joinBtn}<br>` +
|
|
1790
|
+
(participants.length
|
|
1791
|
+
? participants.map((name) => {
|
|
1792
|
+
const u = String(name || '').trim();
|
|
1793
|
+
if (!u) return '';
|
|
1794
|
+
return `<a class="onekite-user-link me-2" href="${window.location.origin}/user/${encodeURIComponent(u)}">${escapeHtml(u)}</a>`;
|
|
1795
|
+
}).filter(Boolean).join('')
|
|
1796
|
+
: `<span class="text-muted">Aucun</span>`) +
|
|
1797
|
+
`</div>`;
|
|
1741
1798
|
const icsUrl = String(p.icsUrl || (p.extendedProps && p.extendedProps.icsUrl) || '').trim();
|
|
1742
1799
|
const googleCalUrl = String(p.googleCalUrl || (p.extendedProps && p.extendedProps.googleCalUrl) || '').trim();
|
|
1743
1800
|
const calHtml = (icsUrl || googleCalUrl)
|
|
@@ -1756,6 +1813,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1756
1813
|
${userLine}
|
|
1757
1814
|
<div class="mb-2"><strong>Période</strong><br>${escapeHtml(formatDtWithTime(ev.start))} → ${escapeHtml(formatDtWithTime(ev.end))}</div>
|
|
1758
1815
|
${calHtml}
|
|
1816
|
+
${participantsHtml}
|
|
1759
1817
|
${addr ? `<div class="mb-2"><strong>Adresse</strong><br>${addrHtml}</div>` : ''}
|
|
1760
1818
|
${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
|
|
1761
1819
|
`;
|
|
@@ -1783,6 +1841,36 @@ function toDatetimeLocalValue(date) {
|
|
|
1783
1841
|
} : {}),
|
|
1784
1842
|
},
|
|
1785
1843
|
});
|
|
1844
|
+
// Self-join handler
|
|
1845
|
+
try {
|
|
1846
|
+
dlg.on('shown.bs.modal', () => {
|
|
1847
|
+
dlg.find('.onekite-join-outing').off('click').on('click', async (e2) => {
|
|
1848
|
+
e2.preventDefault();
|
|
1849
|
+
const btn = e2.currentTarget;
|
|
1850
|
+
if (!btn) return;
|
|
1851
|
+
try {
|
|
1852
|
+
btn.classList.add('disabled');
|
|
1853
|
+
const oid = String(btn.getAttribute('data-oid') || '').trim();
|
|
1854
|
+
if (!oid) return;
|
|
1855
|
+
const r = await fetchJson(`/api/v3/plugins/calendar-onekite/outings/${encodeURIComponent(oid)}/participants`, { method: 'POST' });
|
|
1856
|
+
const names = Array.isArray(r && r.participantsUsernames) ? r.participantsUsernames : [];
|
|
1857
|
+
const box = dlg.find('#onekite-participants-outing');
|
|
1858
|
+
if (box && box.length) {
|
|
1859
|
+
const links = names.length
|
|
1860
|
+
? names.map((name) => {
|
|
1861
|
+
const u = String(name || '').trim();
|
|
1862
|
+
if (!u) return '';
|
|
1863
|
+
return `<a class="onekite-user-link me-2" href="${window.location.origin}/user/${encodeURIComponent(u)}">${escapeHtml(u)}</a>`;
|
|
1864
|
+
}).filter(Boolean).join('')
|
|
1865
|
+
: `<span class="text-muted">Aucun</span>`;
|
|
1866
|
+
box.html(`<strong>Participants</strong><br>${links}`);
|
|
1867
|
+
}
|
|
1868
|
+
} catch (e3) {
|
|
1869
|
+
showAlert('error', "Impossible de s'ajouter.");
|
|
1870
|
+
}
|
|
1871
|
+
});
|
|
1872
|
+
});
|
|
1873
|
+
} catch (e) {}
|
|
1786
1874
|
try {
|
|
1787
1875
|
dlg.on('hidden.bs.modal', function () { isDialogOpen = false; });
|
|
1788
1876
|
} catch (e) {
|