nodebb-plugin-admin-chats 2.1.1 → 3.0.0

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/README.md CHANGED
@@ -4,8 +4,8 @@ A professional administration plugin for NodeBB that allows administrators to mo
4
4
 
5
5
  ## Main Features
6
6
 
7
- * **Global Chat Access**: Administrators can view any private or group conversation.
8
- * **Management Privileges**: Grants admins the ability to edit or delete any message in any room.
7
+ * **Global Chat Access**: Administrators and users with chat view permission can view any private or group conversation.
8
+ * **Management Privileges**: Administrators and users with chat management permission can edit or delete any message in any room.
9
9
  * **Admin Room Locking**: Administrators can lock any chat room so only admins can continue replying.
10
10
  * **Profile Integration**: Adds a "View Chats" button to the user profile menu for administrators to quickly audit a user's interactions.
11
11
  * **Non-Intrusive Monitoring**: Admins can load and view room history without being added as permanent members, maintaining a clean member list.
@@ -16,7 +16,13 @@ A professional administration plugin for NodeBB that allows administrators to mo
16
16
  ### Plugin Information
17
17
  * **Name**: Super Admin Chat Control
18
18
  * **ID**: `nodebb-plugin-admin-chats`
19
- * **Compatibility**: NodeBB versions `^3.0.0` or `^4.0.0`
19
+ * **Compatibility**: NodeBB version `^4.0.0`
20
+
21
+ ### Permissions
22
+ The plugin exposes two global permissions in the ACP:
23
+
24
+ * `admin-chats:view`: View any chat room and load history (read-only access).
25
+ * `admin-chats:manage`: Manage any chat room (edit/delete messages, lock/unlock rooms, and other moderator-level actions).
20
26
 
21
27
  ### Chat Route Behavior (`/chats`)
22
28
  NodeBB exposes a core route at `/chats` (and `/chats/:roomId/:index?`) that redirects logged-in users to their personal chat page under `/user/:userslug/chats/...`.
@@ -41,6 +47,14 @@ The plugin overrides core messaging functions to ensure full administrative cont
41
47
 
42
48
  ## Installation
43
49
 
50
+ ### Option 1: Via Admin Control Panel (Recommended)
51
+ 1. Navigate to the NodeBB Admin Control Panel (ACP).
52
+ 2. Go to **Extend** → **Plugins**.
53
+ 3. Search for "nodebb-plugin-admin-chats".
54
+ 4. Click **Install**.
55
+ 5. Activate the plugin and restart NodeBB.
56
+
57
+ ### Option 2: Via Terminal
44
58
  1. Install the plugin via terminal:
45
59
  ```bash
46
60
  npm install nodebb-plugin-admin-chats
@@ -57,5 +71,4 @@ The plugin includes a `client.js` script that:
57
71
  * Cleans up the chat UI empty states for a better admin experience.
58
72
 
59
73
  ---
60
- *Developed by [palmoni5](https://github.com/palmoni5/nodebb-plugin-admin-chats).*
61
-
74
+ *Developed by [palmoni5](https://github.com/palmoni5/nodebb-plugin-admin-chats).*
package/client.js CHANGED
@@ -1,4 +1,6 @@
1
1
  $(document).ready(function() {
2
+ syncAdminAccessFromAjaxify();
3
+ fetchAdminPrivileges();
2
4
  const LOCK_PREFIX = '[admin-chat-lock]';
3
5
  const roomStateCache = new Map();
4
6
  const TEXT = {
@@ -25,12 +27,106 @@ $(document).ready(function() {
25
27
  function isEnglishSystem() {
26
28
  return $('html').attr('lang') && $('html').attr('lang').startsWith('en');
27
29
  }
28
-
29
30
  function t(key) {
30
31
  const dict = isEnglishSystem() ? TEXT.en : TEXT.he;
31
32
  return dict[key];
32
33
  }
33
34
 
35
+ function hasAdminChatsAccess() {
36
+ return !!((ajaxify && ajaxify.data && (ajaxify.data.adminChatsAccess || ajaxify.data.adminChatsCanManage)) || window.adminChatsAccess || window.adminChatsCanManage || (app.user && app.user.isAdmin));
37
+ }
38
+
39
+ function isAdminUser() {
40
+ return !!((ajaxify && ajaxify.data && ajaxify.data.adminChatsIsAdmin) || window.adminChatsIsAdmin || (app.user && app.user.isAdmin));
41
+ }
42
+
43
+ async function fetchAdminPrivileges() {
44
+ if (!hasAdminChatsAccess() || window.adminChatsPrivilegesPending) {
45
+ return;
46
+ }
47
+
48
+ window.adminChatsPrivilegesPending = true;
49
+ try {
50
+ const response = await fetch(`${config.relative_path || ''}/api/admin-chats/privileges`, {
51
+ headers: {
52
+ 'x-csrf-token': config.csrf_token,
53
+ },
54
+ credentials: 'same-origin',
55
+ });
56
+
57
+ if (!response.ok) {
58
+ return;
59
+ }
60
+
61
+ const payload = await response.json();
62
+ if (payload && typeof payload.adminChatsAccess === 'boolean') {
63
+ window.adminChatsAccess = payload.adminChatsAccess;
64
+ }
65
+ if (payload && typeof payload.adminChatsCanManage === 'boolean') {
66
+ window.adminChatsCanManage = payload.adminChatsCanManage;
67
+ }
68
+ if (payload && typeof payload.adminChatsIsAdmin === 'boolean') {
69
+ window.adminChatsIsAdmin = payload.adminChatsIsAdmin;
70
+ }
71
+ } catch (err) {
72
+ // ignore
73
+ } finally {
74
+ window.adminChatsPrivilegesPending = false;
75
+ }
76
+ }
77
+
78
+ function syncAdminAccessFromAjaxify() {
79
+ if (!ajaxify || !ajaxify.data) {
80
+ return;
81
+ }
82
+ if (typeof ajaxify.data.adminChatsAccess === 'boolean') {
83
+ window.adminChatsAccess = ajaxify.data.adminChatsAccess;
84
+ }
85
+ if (typeof ajaxify.data.adminChatsCanManage === 'boolean') {
86
+ window.adminChatsCanManage = ajaxify.data.adminChatsCanManage;
87
+ }
88
+ if (typeof ajaxify.data.adminChatsIsAdmin === 'boolean') {
89
+ window.adminChatsIsAdmin = ajaxify.data.adminChatsIsAdmin;
90
+ }
91
+ }
92
+
93
+ function canManageChats() {
94
+ return !!((ajaxify && ajaxify.data && ajaxify.data.adminChatsCanManage) || window.adminChatsCanManage || isAdminUser());
95
+ }
96
+
97
+ let adminChatsAccessProbe = null;
98
+
99
+ function resolveAdminChatsAccess() {
100
+ if (hasAdminChatsAccess()) {
101
+ return Promise.resolve(true);
102
+ }
103
+ if (window.adminChatsAccessChecked) {
104
+ return Promise.resolve(false);
105
+ }
106
+ if (adminChatsAccessProbe) {
107
+ return adminChatsAccessProbe;
108
+ }
109
+
110
+ adminChatsAccessProbe = fetch(`${config.relative_path || ''}/api/admin-chats?start=0`, {
111
+ headers: {
112
+ 'x-csrf-token': config.csrf_token,
113
+ },
114
+ credentials: 'same-origin',
115
+ }).then((response) => {
116
+ const ok = response && response.ok;
117
+ window.adminChatsAccess = ok;
118
+ return ok;
119
+ }).catch(() => {
120
+ window.adminChatsAccess = false;
121
+ return false;
122
+ }).finally(() => {
123
+ window.adminChatsAccessChecked = true;
124
+ adminChatsAccessProbe = null;
125
+ });
126
+
127
+ return adminChatsAccessProbe;
128
+ }
129
+
34
130
  // Get lockedAction message from server translations only (based on forum settings)
35
131
  function getLockedActionMessage() {
36
132
  if (window.adminChatsTranslations && window.adminChatsTranslations.lockedAction) {
@@ -173,7 +269,7 @@ $(document).ready(function() {
173
269
  }
174
270
 
175
271
  function replaceAdminEmptyStateText() {
176
- if (!app.user.isAdmin) {
272
+ if (!hasAdminChatsAccess()) {
177
273
  return;
178
274
  }
179
275
 
@@ -247,19 +343,50 @@ $(document).ready(function() {
247
343
  credentials: 'same-origin',
248
344
  });
249
345
  if (!response.ok) {
346
+ if (hasAdminChatsAccess() && !isAdminAllChatsPage()) {
347
+ const adminResponse = await fetch(`${config.relative_path || ''}/api/admin-chats/page/${roomId}`, {
348
+ headers: {
349
+ 'x-csrf-token': config.csrf_token,
350
+ },
351
+ credentials: 'same-origin',
352
+ });
353
+ if (!adminResponse.ok) {
354
+ return null;
355
+ }
356
+ const adminPayload = await adminResponse.json();
357
+ const adminRoom = adminPayload && adminPayload.roomId ? adminPayload : (adminPayload && adminPayload.room ? adminPayload.room : null);
358
+ if (adminRoom) {
359
+ setCachedRoomData(roomId, adminRoom);
360
+ }
361
+ return adminRoom;
362
+ }
250
363
  return null;
251
364
  }
252
365
 
253
366
  const payload = await response.json();
254
367
  const roomData = payload && payload.roomId ? payload : (payload && payload.room ? payload.room : null);
368
+ if (roomData && !roomData.adminChatLock && hasAdminChatsAccess() && !isAdminAllChatsPage()) {
369
+ const adminResponse = await fetch(`${config.relative_path || ''}/api/admin-chats/page/${roomId}`, {
370
+ headers: {
371
+ 'x-csrf-token': config.csrf_token,
372
+ },
373
+ credentials: 'same-origin',
374
+ });
375
+ if (adminResponse.ok) {
376
+ const adminPayload = await adminResponse.json();
377
+ const adminRoom = adminPayload && adminPayload.roomId ? adminPayload : (adminPayload && adminPayload.room ? adminPayload.room : null);
378
+ if (adminRoom && adminRoom.adminChatLock) {
379
+ roomData.adminChatLock = adminRoom.adminChatLock;
380
+ }
381
+ }
382
+ }
255
383
  if (roomData) {
256
384
  setCachedRoomData(roomId, roomData);
257
385
  }
258
386
  return roomData;
259
387
  }
260
-
261
388
  function isLockedForUser(roomData) {
262
- return !!(roomData && roomData.adminChatLock && roomData.adminChatLock.isLocked && !(app.user && app.user.isAdmin));
389
+ return !!(roomData && roomData.adminChatLock && roomData.adminChatLock.isLocked && !canManageChats());
263
390
  }
264
391
 
265
392
  function setComposerHidden($window, hidden) {
@@ -314,6 +441,15 @@ $(document).ready(function() {
314
441
  .toggleClass('hidden', hidden);
315
442
  }
316
443
 
444
+ function updateDeleteRestoreVisibility($window) {
445
+ $window.find('[component="chat/message"]').each(function() {
446
+ const $message = $(this);
447
+ const isDeleted = $message.hasClass('deleted');
448
+ $message.find('[data-action="delete"]').toggleClass('hidden', isDeleted).toggle(!isDeleted);
449
+ $message.find('[data-action="restore"]').toggleClass('hidden', !isDeleted).toggle(isDeleted);
450
+ });
451
+ }
452
+
317
453
  function normalizeLockMessages($window) {
318
454
  $window.find('[component="chat/system-message"] > div').each(function() {
319
455
  const $el = $(this);
@@ -336,7 +472,7 @@ $(document).ready(function() {
336
472
  function renderAdminLockControl($window, roomData) {
337
473
  $window.find('.admin-chat-lock-toggle-item, .admin-chat-lock-divider, .admin-chat-lock-item-wrap').remove();
338
474
 
339
- if (!app.user.isAdmin || !roomData || !roomData.roomId) {
475
+ if (!canManageChats() || !roomData || !roomData.roomId) {
340
476
  return;
341
477
  }
342
478
 
@@ -382,6 +518,7 @@ $(document).ready(function() {
382
518
  const hidden = isLockedForUser(roomData);
383
519
  setComposerHidden($window, hidden);
384
520
  updateLockedActionVisibility($window, hidden);
521
+ updateDeleteRestoreVisibility($window);
385
522
  renderLockBanner($window, hidden);
386
523
  renderAdminLockControl($window, roomData);
387
524
  normalizeLockMessages($window);
@@ -452,28 +589,13 @@ $(document).ready(function() {
452
589
  }
453
590
 
454
591
  $(window).on('action:ajaxify.end', function(ev, data) {
592
+ syncAdminAccessFromAjaxify();
593
+ fetchAdminPrivileges();
455
594
  const templateName = ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '';
456
595
 
457
- if (app.user.isAdmin && templateName.startsWith('account/')) {
458
- const userSlug = ajaxify.data.userslug || (ajaxify.data.user && ajaxify.data.user.userslug);
459
-
460
- if (userSlug) {
461
- const relativePath = config.relative_path || '';
462
- const btnHtml = `
463
- <li role="presentation">
464
- <a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="${relativePath}/user/${userSlug}/chats" role="menuitem">
465
- <i class="far fa-fw fa-comments"></i>
466
- <span>${t('viewChats')}</span>
467
- </a>
468
- </li>
469
- <li role="presentation" class="dropdown-divider"></li>
470
- `;
471
- const menu = $('.account-sub-links');
472
- if (menu.length) {
473
- menu.find(`a[href*="/user/${userSlug}/chats"]`).parent().remove();
474
- menu.prepend(btnHtml);
475
- }
476
- }
596
+ if (templateName.startsWith('account/')) {
597
+ injectProfileChatsMenu();
598
+ setTimeout(injectProfileChatsMenu, 200);
477
599
  }
478
600
 
479
601
  if (isAdminAllChatsPage()) {
@@ -538,6 +660,13 @@ $(document).ready(function() {
538
660
  setTimeout(refreshChatUi, 0);
539
661
  });
540
662
 
663
+ if (window.socket) {
664
+ socket.removeListener('event:chats.delete', refreshChatUi);
665
+ socket.removeListener('event:chats.restore', refreshChatUi);
666
+ socket.on('event:chats.delete', function() { setTimeout(refreshChatUi, 0); });
667
+ socket.on('event:chats.restore', function() { setTimeout(refreshChatUi, 0); });
668
+ }
669
+
541
670
  // Handle browser back/forward navigation
542
671
  $(window).on('popstate', function() {
543
672
  if (isAdminAllChatsPage()) {
@@ -573,6 +702,16 @@ $(document).ready(function() {
573
702
  }
574
703
  }
575
704
 
705
+ $(document).on('click', '[data-action="delete"], [data-action="restore"]', function() {
706
+ const $window = $(this).closest('[component="chat/message/window"]');
707
+ if (!$window.length) {
708
+ return;
709
+ }
710
+ setTimeout(function() {
711
+ updateDeleteRestoreVisibility($window);
712
+ }, 50);
713
+ });
714
+
576
715
  $(document).on('click', '.admin-chat-lock-toggle-item', async function(ev) {
577
716
  ev.preventDefault();
578
717
 
@@ -591,7 +730,7 @@ $(document).ready(function() {
591
730
  const currentRoom = ajaxify && ajaxify.data && (ajaxify.data.room || ajaxify.data);
592
731
  if (currentRoom && parseInt(currentRoom.roomId, 10) === roomId) {
593
732
  currentRoom.adminChatLock = result.lockData;
594
- currentRoom.canReply = !result.lockData.isLocked || (app.user && app.user.isAdmin);
733
+ currentRoom.canReply = !result.lockData.isLocked || canManageChats();
595
734
  currentRoom.showUserInput = currentRoom.canReply;
596
735
  if (ajaxify.data.room) {
597
736
  ajaxify.data.room.adminChatLock = result.lockData;
@@ -602,7 +741,7 @@ $(document).ready(function() {
602
741
 
603
742
  const cached = getCachedRoomData(roomId) || { roomId };
604
743
  cached.adminChatLock = result.lockData;
605
- cached.canReply = !result.lockData.isLocked || (app.user && app.user.isAdmin);
744
+ cached.canReply = !result.lockData.isLocked || canManageChats();
606
745
  cached.showUserInput = cached.canReply;
607
746
  setCachedRoomData(roomId, cached);
608
747
  await refreshChatUi();
@@ -626,4 +765,3 @@ $(document).ready(function() {
626
765
  app.alertError._adminChatsWrapped = true;
627
766
  }
628
767
  });
629
-
package/library.js CHANGED
@@ -11,6 +11,8 @@ const helpers = require.main.require('./src/controllers/helpers');
11
11
  const plugin = {};
12
12
  const LOCK_PREFIX = '[admin-chat-lock]';
13
13
  const LOCKED_MESSAGE_TEXT = '🔒 חדר זה ננעל ע"י המנהלים.';
14
+ const ADMIN_CHATS_PRIVILEGE = 'admin-chats:view';
15
+ const ADMIN_CHATS_MANAGE_PRIVILEGE = 'admin-chats:manage';
14
16
 
15
17
  async function getLockedActionMessage(uid) {
16
18
  try {
@@ -36,12 +38,63 @@ async function getLockedActionMessage(uid) {
36
38
  const ADMIN_CHAT_PAGE_SIZE = 30;
37
39
  const ADMIN_CHAT_SCAN_SIZE = 100;
38
40
 
41
+ async function canAccessAdminChats(uid) {
42
+ if (!uid) {
43
+ return false;
44
+ }
45
+
46
+ const [isAdmin, canView, canManage] = await Promise.all([
47
+ User.isAdministrator(uid),
48
+ privileges.global.can(ADMIN_CHATS_PRIVILEGE, uid),
49
+ privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, uid),
50
+ ]);
51
+
52
+ return isAdmin || canView || canManage;
53
+ }
54
+
55
+ async function canManageAdminChats(uid) {
56
+ if (!uid) {
57
+ return false;
58
+ }
59
+
60
+ const [isAdmin, canManage] = await Promise.all([
61
+ User.isAdministrator(uid),
62
+ privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, uid),
63
+ ]);
64
+
65
+ return isAdmin || canManage;
66
+ }
67
+
68
+ async function isUserMemberOfRoom(uid, roomId) {
69
+ if (!uid || !roomId) {
70
+ return false;
71
+ }
72
+
73
+ return await db.isSortedSetMember(`chat:room:${roomId}:uids`, uid);
74
+ }
39
75
  plugin.init = async function (params) {
40
76
  registerRoutes(params.app, params.router, params.middleware);
41
77
  overrideMessagingFunctions();
42
78
  overrideChatsApi();
43
79
  overrideCoreChatRedirect(params.controllers);
44
80
  };
81
+ plugin.registerPrivileges = async function (data) {
82
+ if (!data || !data.privileges || typeof data.privileges.set !== 'function') {
83
+ return data;
84
+ }
85
+
86
+ data.privileges.set(ADMIN_CHATS_PRIVILEGE, {
87
+ label: '[[admin-chats:admin-chats-view]]',
88
+ type: 'moderation',
89
+ });
90
+
91
+ data.privileges.set(ADMIN_CHATS_MANAGE_PRIVILEGE, {
92
+ label: '[[admin-chats:admin-chats-manage]]',
93
+ type: 'moderation',
94
+ });
95
+
96
+ return data;
97
+ };
45
98
 
46
99
  plugin.filterNotificationCreate = async function (data) {
47
100
  if (!data || !data.data) {
@@ -127,10 +180,14 @@ plugin.addScripts = async function (data) {
127
180
  lockedAction: isHebrew ? 'לא ניתן לבצע פעולה זו בחדר נעול' : 'This action cannot be performed in a locked room.',
128
181
  };
129
182
 
183
+ const isAdmin = uid ? await User.isAdministrator(uid) : false;
184
+ const adminChatsManage = uid ? (isAdmin || await privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, uid)) : false;
185
+ const adminChatsAccess = uid ? (isAdmin || adminChatsManage || await privileges.global.can(ADMIN_CHATS_PRIVILEGE, uid)) : false;
186
+
130
187
  data.scripts = data.scripts || [];
131
188
  data.scripts.push({
132
189
  src: false,
133
- script: `window.adminChatsTranslations = ${JSON.stringify(translations)};`
190
+ script: `window.adminChatsTranslations = ${JSON.stringify(translations)}; window.adminChatsAccess = ${adminChatsAccess}; window.adminChatsIsAdmin = ${isAdmin}; window.adminChatsCanManage = ${adminChatsManage};`
134
191
  });
135
192
 
136
193
  return data;
@@ -138,6 +195,11 @@ plugin.addScripts = async function (data) {
138
195
 
139
196
  plugin.addProfileLink = async function (data) {
140
197
  try {
198
+ const uid = data && (data.uid || (data.req && data.req.uid));
199
+ if (!uid || !await canAccessAdminChats(uid)) {
200
+ return data;
201
+ }
202
+
141
203
  const userSlug = data.user ? data.user.userslug : (data.userData ? data.userData.userslug : null);
142
204
 
143
205
  if (userSlug) {
@@ -145,12 +207,12 @@ plugin.addProfileLink = async function (data) {
145
207
  id: 'admin-view-chats',
146
208
  route: 'user/' + userSlug + '/chats',
147
209
  icon: 'fa-comments',
148
- name: 'צפה בצ\'אטים',
210
+ name: '[[admin-chats:profile.viewChats]]',
149
211
  visibility: {
150
- self: false,
212
+ self: true,
151
213
  other: true,
152
- moderator: false,
153
- globalMod: false,
214
+ moderator: true,
215
+ globalMod: true,
154
216
  admin: true,
155
217
  }
156
218
  });
@@ -162,16 +224,19 @@ plugin.addProfileLink = async function (data) {
162
224
  };
163
225
 
164
226
  plugin.isRoomOwner = async function (payload) {
165
- const isAdmin = await User.isAdministrator(payload.uid);
166
- if (isAdmin) {
227
+ const [isAdmin, canManage] = await Promise.all([
228
+ User.isAdministrator(payload.uid),
229
+ canManageAdminChats(payload.uid),
230
+ ]);
231
+ if (isAdmin || canManage) {
167
232
  payload.isOwner = true;
168
233
  }
169
234
  return payload;
170
235
  };
171
236
 
172
237
  plugin.isUserInRoom = async function (payload) {
173
- const isAdmin = await User.isAdministrator(payload.uid);
174
- if (isAdmin) {
238
+ const uid = getCallerUid(payload);
239
+ if (await canAccessAdminChats(uid)) {
175
240
  payload.inRoom = true;
176
241
  }
177
242
  return payload;
@@ -180,13 +245,28 @@ plugin.isUserInRoom = async function (payload) {
180
245
  plugin.canReply = async function (payload) {
181
246
  const uid = getCallerUid(payload);
182
247
  const roomId = getRoomId(payload);
183
- const isAdmin = await User.isAdministrator(uid);
184
248
 
185
- if (isAdmin) {
249
+ const [isAdmin, canView, canManage] = await Promise.all([
250
+ User.isAdministrator(uid),
251
+ privileges.global.can(ADMIN_CHATS_PRIVILEGE, uid),
252
+ privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, uid),
253
+ ]);
254
+ const isManager = isAdmin || canManage;
255
+ const hasAccess = isAdmin || canView || canManage;
256
+
257
+ if (isManager) {
186
258
  payload.canReply = true;
187
259
  return payload;
188
260
  }
189
261
 
262
+ if (roomId && hasAccess) {
263
+ const isMember = await isUserMemberOfRoom(uid, roomId);
264
+ if (!isMember) {
265
+ payload.canReply = false;
266
+ return payload;
267
+ }
268
+ }
269
+
190
270
  if (roomId && await isRoomLocked(roomId)) {
191
271
  payload.canReply = false;
192
272
  }
@@ -206,6 +286,17 @@ plugin.filterMessagingSend = async function (payload) {
206
286
  return payload;
207
287
  }
208
288
 
289
+ if (await canManageAdminChats(uid)) {
290
+ return payload;
291
+ }
292
+
293
+ if (await canAccessAdminChats(uid)) {
294
+ const isMember = await isUserMemberOfRoom(uid, roomId);
295
+ if (!isMember) {
296
+ throw new Error('[[error:no-privileges]]');
297
+ }
298
+ }
299
+
209
300
  if (await isRoomLocked(roomId)) {
210
301
  const message = await getLockedActionMessage(uid);
211
302
  throw new Error(message);
@@ -214,6 +305,9 @@ plugin.filterMessagingSend = async function (payload) {
214
305
  return payload;
215
306
  };
216
307
  plugin.filterAddUsersToRoom = async function (payload) {
308
+ if (await canManageAdminChats(payload.uid)) {
309
+ return payload;
310
+ }
217
311
  if (await canNonAdminModifyLockedRoom(payload.uid, payload.roomId)) {
218
312
  const message = await getLockedActionMessage(payload.uid);
219
313
  throw new Error(message);
@@ -222,6 +316,9 @@ plugin.filterAddUsersToRoom = async function (payload) {
222
316
  };
223
317
 
224
318
  plugin.filterRemoveUsersFromRoom = async function (payload) {
319
+ if (await canManageAdminChats(payload.uid)) {
320
+ return payload;
321
+ }
225
322
  if (await canNonAdminModifyLockedRoom(payload.uid, payload.roomId)) {
226
323
  const message = await getLockedActionMessage(payload.uid);
227
324
  throw new Error(message);
@@ -230,6 +327,9 @@ plugin.filterRemoveUsersFromRoom = async function (payload) {
230
327
  };
231
328
 
232
329
  plugin.filterRenameRoom = async function (payload) {
330
+ if (await canManageAdminChats(payload.uid)) {
331
+ return payload;
332
+ }
233
333
  if (await canNonAdminModifyLockedRoom(payload.uid, payload.roomId)) {
234
334
  const message = await getLockedActionMessage(payload.uid);
235
335
  throw new Error(message);
@@ -238,24 +338,21 @@ plugin.filterRenameRoom = async function (payload) {
238
338
  };
239
339
 
240
340
  plugin.canGetMessages = async function (payload) {
241
- const isAdmin = await User.isAdministrator(payload.callerUid);
242
- if (isAdmin) {
341
+ if (await canAccessAdminChats(payload.callerUid)) {
243
342
  payload.canGet = true;
244
343
  }
245
344
  return payload;
246
345
  };
247
346
 
248
347
  plugin.canGetRecentChats = async function (payload) {
249
- const isAdmin = await User.isAdministrator(payload.callerUid);
250
- if (isAdmin) {
348
+ if (await canAccessAdminChats(payload.callerUid)) {
251
349
  payload.canGet = true;
252
350
  }
253
351
  return payload;
254
352
  };
255
353
 
256
354
  plugin.canGetPublicChats = async function (payload) {
257
- const isAdmin = await User.isAdministrator(payload.callerUid);
258
- if (isAdmin) {
355
+ if (await canAccessAdminChats(payload.callerUid)) {
259
356
  payload.canGet = true;
260
357
  }
261
358
  return payload;
@@ -267,20 +364,27 @@ plugin.onLoadRoom = async function (payload) {
267
364
  return payload;
268
365
  }
269
366
 
270
- const isAdmin = await User.isAdministrator(uid);
367
+ const [isAdmin, canView, canManage] = await Promise.all([
368
+ User.isAdministrator(uid),
369
+ privileges.global.can(ADMIN_CHATS_PRIVILEGE, uid),
370
+ privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, uid),
371
+ ]);
372
+ const isManager = isAdmin || canManage;
373
+ const hasAccess = isAdmin || canView || canManage;
374
+
271
375
  const lockData = await getRoomLockData(room.roomId);
272
- const isLockedForUser = lockData.isLocked && !isAdmin;
376
+ const isLockedForUser = lockData.isLocked && !isManager;
273
377
 
274
378
  room.adminChatLock = lockData;
275
379
  room.messages = ensureFirstVisibleMessageHeader(buildRoomMessagesWithLockNotice(room.messages, lockData));
276
380
  room.canReply = room.canReply && !isLockedForUser;
277
381
  room.showUserInput = room.showUserInput && !isLockedForUser;
278
382
 
279
- if (!isAdmin) {
383
+ if (!hasAccess) {
280
384
  return payload;
281
385
  }
282
386
 
283
- const isOfficialMember = await db.isSortedSetMember(`chat:room:${room.roomId}:uids`, uid);
387
+ const isOfficialMember = await isUserMemberOfRoom(uid, room.roomId);
284
388
 
285
389
  if (!isOfficialMember) {
286
390
  await db.sortedSetRemove(`chat:room:${room.roomId}:uids:online`, uid);
@@ -325,12 +429,24 @@ plugin.onLoadRoom = async function (payload) {
325
429
 
326
430
  room.messages = ensureFirstVisibleMessageHeader(room.messages);
327
431
  }
432
+ }
328
433
 
329
- room.isAdmin = true;
434
+ if (!isManager && !isOfficialMember) {
435
+ room.canReply = false;
436
+ room.showUserInput = false;
437
+ }
438
+
439
+ room.isAdmin = isManager;
440
+ room.isAdminOrGlobalMod = isManager;
441
+ payload.isAdminOrGlobalMod = isManager;
442
+ if (isManager) {
330
443
  room.isOwner = true;
331
- } else {
332
- room.isAdmin = true;
333
444
  }
445
+ room.adminChatsAccess = hasAccess;
446
+ payload.adminChatsAccess = hasAccess;
447
+ payload.adminChatsCanManage = isManager;
448
+ payload.adminChatsIsAdmin = isAdmin;
449
+
334
450
  return payload;
335
451
  };
336
452
 
@@ -342,7 +458,7 @@ function overrideCoreChatRedirect(controllers) {
342
458
 
343
459
  const originalRedirectToChat = chatsController.redirectToChat;
344
460
  const wrappedRedirectToChat = async function (req, res, next) {
345
- if (req && req.uid && await User.isAdministrator(req.uid)) {
461
+ if (req && req.uid && await canAccessAdminChats(req.uid)) {
346
462
  return await renderAdminChatsPage(req, res, next);
347
463
  }
348
464
 
@@ -360,7 +476,7 @@ function overrideChatsApi() {
360
476
 
361
477
  const originalKick = ChatsAPI.kick;
362
478
  const wrappedKick = async function (caller, data) {
363
- if (caller && !await User.isAdministrator(caller.uid) && data && data.roomId && Array.isArray(data.uids) && data.uids.length === 1 && parseInt(data.uids[0], 10) === parseInt(caller.uid, 10) && await isRoomLocked(data.roomId)) {
479
+ if (caller && !await User.isAdministrator(caller.uid) && !await canManageAdminChats(caller.uid) && data && data.roomId && Array.isArray(data.uids) && data.uids.length === 1 && parseInt(data.uids[0], 10) === parseInt(caller.uid, 10) && await isRoomLocked(data.roomId)) {
364
480
  const message = await getLockedActionMessage(caller.uid);
365
481
  throw new Error(message);
366
482
  }
@@ -382,6 +498,10 @@ function overrideMessagingFunctions() {
382
498
  return true;
383
499
  }
384
500
 
501
+ if (await canManageAdminChats(uid)) {
502
+ return true;
503
+ }
504
+
385
505
  const roomId = await getMessageRoomId(messageId);
386
506
  if (roomId && await isRoomLocked(roomId)) {
387
507
  const message = await getLockedActionMessage(uid);
@@ -396,6 +516,10 @@ function overrideMessagingFunctions() {
396
516
  return true;
397
517
  }
398
518
 
519
+ if (await canManageAdminChats(uid)) {
520
+ return true;
521
+ }
522
+
399
523
  const roomId = await getMessageRoomId(messageId);
400
524
  if (roomId && await isRoomLocked(roomId)) {
401
525
  const message = await getLockedActionMessage(uid);
@@ -406,7 +530,7 @@ function overrideMessagingFunctions() {
406
530
  };
407
531
 
408
532
  Messaging.canViewMessage = async function (mids, roomId, uid) {
409
- if (await User.isAdministrator(uid)) {
533
+ if (await canAccessAdminChats(uid)) {
410
534
  return Array.isArray(mids) ? mids.map(() => true) : true;
411
535
  }
412
536
  return await originalCanViewMessage(mids, roomId, uid);
@@ -415,9 +539,23 @@ function overrideMessagingFunctions() {
415
539
  if (typeof originalCanReply === 'function') {
416
540
  Messaging.canReply = async function (...args) {
417
541
  const guessed = guessRoomIdUidFromArgs(args);
418
- if (await User.isAdministrator(guessed.uid)) {
542
+ const [isAdmin, canView, canManage] = await Promise.all([
543
+ User.isAdministrator(guessed.uid),
544
+ privileges.global.can(ADMIN_CHATS_PRIVILEGE, guessed.uid),
545
+ privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, guessed.uid),
546
+ ]);
547
+ const isManager = isAdmin || canManage;
548
+ const hasAccess = isAdmin || canView || canManage;
549
+
550
+ if (isManager) {
419
551
  return true;
420
552
  }
553
+ if (guessed.roomId && hasAccess) {
554
+ const isMember = await isUserMemberOfRoom(guessed.uid, guessed.roomId);
555
+ if (!isMember) {
556
+ return false;
557
+ }
558
+ }
421
559
  if (guessed.roomId && await isRoomLocked(guessed.roomId)) {
422
560
  return false;
423
561
  }
@@ -450,8 +588,8 @@ function registerRoutes(app, router, middleware) {
450
588
 
451
589
  const pageController = async (req, res, next) => {
452
590
  try {
453
- // If user is not admin, redirect to user-specific chat URL
454
- if (req.uid && !await User.isAdministrator(req.uid)) {
591
+ // If user is not allowed, redirect to user-specific chat URL
592
+ if (req.uid && !await canAccessAdminChats(req.uid)) {
455
593
  const userSlug = await User.getUserField(req.uid, 'userslug');
456
594
  if (userSlug && req.params.roomId) {
457
595
  const redirectUrl = `${req.baseUrl || ''}/user/${userSlug}/chats/${req.params.roomId}${req.params.index ? `/${req.params.index}` : ''}`;
@@ -482,6 +620,25 @@ function registerRoutes(app, router, middleware) {
482
620
  }
483
621
  });
484
622
 
623
+
624
+ router.get('/api/admin-chats/privileges', ...routeMiddleware, async (req, res) => {
625
+ try {
626
+ if (!await assertAdminChatsAccess(req, res)) {
627
+ return;
628
+ }
629
+
630
+ const isAdmin = await User.isAdministrator(req.uid);
631
+ const canManage = isAdmin || await privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, req.uid);
632
+ res.json({
633
+ adminChatsAccess: true,
634
+ adminChatsCanManage: canManage,
635
+ adminChatsIsAdmin: isAdmin,
636
+ });
637
+ } catch (err) {
638
+ res.status(500).json({ status: { code: 'error', message: err.message } });
639
+ }
640
+ });
641
+
485
642
  router.get('/api/admin-chats/page/:roomId?/:index?', ...routeMiddleware, async (req, res) => {
486
643
  try {
487
644
  if (!await assertAdminChatsAccess(req, res)) {
@@ -504,8 +661,8 @@ function registerRoutes(app, router, middleware) {
504
661
  return res.status(400).json({ status: { code: 'bad-request', message: 'Invalid room id' } });
505
662
  }
506
663
 
507
- if (!await User.isAdministrator(actingUid)) {
508
- return res.status(403).json({ status: { code: 'forbidden', message: 'Admin only' } });
664
+ if (!await User.isAdministrator(actingUid) && !await canManageAdminChats(actingUid)) {
665
+ return res.status(403).json({ status: { code: 'forbidden', message: 'Admin or manage privilege required' } });
509
666
  }
510
667
 
511
668
  if (!await roomExists(roomId)) {
@@ -546,7 +703,7 @@ async function renderAdminChatsPage(req, res, next) {
546
703
  }
547
704
 
548
705
  async function assertAdminChatsAccess(req, res) {
549
- if (!req.uid || !await User.isAdministrator(req.uid)) {
706
+ if (!req.uid || !await canAccessAdminChats(req.uid)) {
550
707
  helpers.notAllowed(req, res);
551
708
  return false;
552
709
  }
@@ -556,6 +713,13 @@ async function assertAdminChatsAccess(req, res) {
556
713
 
557
714
  async function buildAdminChatsPayload(req) {
558
715
  const userslug = await User.getUserField(req.uid, 'userslug');
716
+ const [isAdmin, canView, canManage] = await Promise.all([
717
+ User.isAdministrator(req.uid),
718
+ privileges.global.can(ADMIN_CHATS_PRIVILEGE, req.uid),
719
+ privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, req.uid),
720
+ ]);
721
+ const adminChatsManage = isAdmin || canManage;
722
+ const adminChatsAccess = isAdmin || canView || canManage;
559
723
  const [recentChats, publicRooms, privateRoomCount] = await Promise.all([
560
724
  getAdminRecentChats(req.uid, 0, ADMIN_CHAT_PAGE_SIZE),
561
725
  Messaging.getPublicRooms(req.uid, req.uid),
@@ -567,6 +731,9 @@ async function buildAdminChatsPayload(req) {
567
731
  uid: req.uid,
568
732
  userslug,
569
733
  adminAllChats: true,
734
+ adminChatsAccess,
735
+ adminChatsIsAdmin: isAdmin,
736
+ adminChatsCanManage: adminChatsManage,
570
737
  rooms: recentChats.rooms,
571
738
  nextStart: recentChats.nextStart,
572
739
  publicRooms: publicRooms || [],
@@ -763,6 +930,10 @@ async function canNonAdminModifyLockedRoom(uid, roomId) {
763
930
  return false;
764
931
  }
765
932
 
933
+ if (await canManageAdminChats(uid)) {
934
+ return false;
935
+ }
936
+
766
937
  return await isRoomLocked(roomId);
767
938
  }
768
939
  async function roomExists(roomId) {
@@ -899,6 +1070,3 @@ function getRoomId(payload) {
899
1070
  }
900
1071
 
901
1072
  module.exports = plugin;
902
-
903
-
904
-
package/package.json CHANGED
@@ -1,11 +1,8 @@
1
1
  {
2
2
  "name": "nodebb-plugin-admin-chats",
3
- "version": "2.1.1",
3
+ "version": "3.0.0",
4
4
  "description": "A plugin that allows the administrator to view and manage user chats.",
5
5
  "main": "library.js",
6
- "dependencies": {
7
- "lodash": "^4.17.21"
8
- },
9
6
  "nbbpm": {
10
7
  "compatibility": "^4.0.0"
11
8
  }
package/plugin.json CHANGED
@@ -71,9 +71,11 @@
71
71
  {
72
72
  "hook": "filter:user.notifications.getNotifications",
73
73
  "method": "filterUserNotificationsGetNotifications"
74
+ },
75
+ {
76
+ "hook": "static:privileges.global.init",
77
+ "method": "registerPrivileges"
74
78
  }
75
- ]
79
+ ],
80
+ "languages": "public/languages"
76
81
  }
77
-
78
-
79
-
@@ -5,5 +5,7 @@
5
5
  "errors.update": "Unable to update room lock.",
6
6
  "profile.viewChats": "View Chats",
7
7
  "empty.selectChat": "Please select a chat from the sidebar.",
8
- "errors.lockedAction": "This action cannot be performed in a locked room."
8
+ "errors.lockedAction": "This action cannot be performed in a locked room.",
9
+ "admin-chats-view": "View all chats",
10
+ "admin-chats-manage": "Manage chat rooms"
9
11
  }
@@ -5,5 +5,7 @@
5
5
  "errors.update": "לא ניתן לעדכן את נעילת החדר.",
6
6
  "profile.viewChats": "צפיה בצ'אטים",
7
7
  "empty.selectChat": "אנא בחר צ'אט מסרגל הצד.",
8
- "errors.lockedAction": "לא ניתן לבצע פעולה זו בחדר נעול"
8
+ "errors.lockedAction": "לא ניתן לבצע פעולה זו בחדר נעול",
9
+ "admin-chats-view": "צפיה בכל הצ'אטים",
10
+ "admin-chats-manage": "ניהול חדרי צ'אט"
9
11
  }