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 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({}, { eid }, { deletedBy: String(req.uid || '') }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.55",
3
+ "version": "2.0.56",
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.55"
42
+ "version": "2.0.56"
43
43
  }
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) {