nodebb-plugin-admin-chats 1.0.2 → 1.1.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.
Files changed (4) hide show
  1. package/README.md +56 -0
  2. package/client.js +215 -17
  3. package/library.js +219 -13
  4. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # Super Admin Chat Control for NodeBB
2
+
3
+ A professional administration plugin for NodeBB that allows administrators to monitor, manage, and interact with any chat room on the forum, regardless of membership.
4
+
5
+ ## Main Features
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.
9
+ * **Admin Room Locking**: Administrators can lock any chat room so only admins can continue replying.
10
+ * **Profile Integration**: Adds a "View Chats" button to the user profile menu for administrators to quickly audit a user's interactions.
11
+ * **Non-Intrusive Monitoring**: Admins can load and view room history without being added as permanent members, maintaining a clean member list.
12
+ * **UI Enhancements**: Replaces generic "no chats" messages with helpful instructions for admins and supports both English and Hebrew interfaces.
13
+
14
+ ## Technical Overview
15
+
16
+ ### Plugin Information
17
+ * **Name**: Super Admin Chat Control
18
+ * **ID**: `nodebb-plugin-admin-chats`
19
+ * **Compatibility**: NodeBB versions `^3.0.0` or `^4.0.0`
20
+
21
+ ### Implemented Hooks
22
+ The plugin utilizes several filters to elevate administrator permissions:
23
+
24
+ | Hook | Method | Functionality |
25
+ | :--- | :--- | :--- |
26
+ | `filter:messaging.isRoomOwner` | `isRoomOwner` | Treats admins as owners for permission checks. |
27
+ | `filter:messaging.canReply` | `canReply` | Allows admins to send messages in any room and blocks regular users in admin-locked rooms. |
28
+ | `filter:messaging.canGetMessages` | `canGetMessages` | Allows admins to fetch chat history. |
29
+ | `filter:messaging.loadRoom` | `onLoadRoom` | Manages room loading logic for non-member admins and injects lock state. |
30
+ | `filter:user.accountMenu` | `addProfileLink` | Injects the admin link into the profile menu. |
31
+
32
+ ### Function Overrides
33
+ The plugin overrides core messaging functions to ensure full administrative control:
34
+ * **Edit/Delete**: `Messaging.canEdit` and `Messaging.canDelete` are bypassed for administrators.
35
+ * **Visibility**: `Messaging.canViewMessage` is modified to always return `true` for admins.
36
+ * **Room Locking**: `Messaging.canReply` is wrapped so non-admins cannot post in rooms locked by an admin.
37
+
38
+ ## Installation
39
+
40
+ 1. Install the plugin via terminal:
41
+ ```bash
42
+ npm install nodebb-plugin-admin-chats
43
+ ```
44
+ 2. Activate the plugin in the NodeBB Admin Control Panel (ACP).
45
+ 3. Restart NodeBB.
46
+
47
+ ## Client-Side Support
48
+ The plugin includes a `client.js` script that:
49
+ * Detects the system language (English or Hebrew).
50
+ * Dynamically injects action buttons into the account sub-links menu.
51
+ * Adds an admin lock/unlock control to the chat UI.
52
+ * Disables the composer for non-admin users when a room is locked.
53
+ * Cleans up the chat UI empty states for a better admin experience.
54
+
55
+ ---
56
+ *Developed by [palmoni5](https://github.com/palmoni5/nodebb-plugin-admin-chats).*
package/client.js CHANGED
@@ -1,5 +1,7 @@
1
1
  $(document).ready(function() {
2
-
2
+ const LOCK_PREFIX = '[admin-chat-lock]';
3
+ const LOCKED_MESSAGE_TEXT = '🔒 חדר זה ננעל ע"י המנהלים.';
4
+
3
5
  function isEnglishSystem() {
4
6
  return $('html').attr('lang') && $('html').attr('lang').startsWith('en');
5
7
  }
@@ -9,33 +11,195 @@ $(document).ready(function() {
9
11
 
10
12
  $('span.text-muted.text-sm').each(function() {
11
13
  const currentText = $(this).text().trim();
12
-
14
+
13
15
  if (currentText.includes("אין לכם צ'אטים פעילים") || currentText === "אין לכם צ'אטים פעילים.") {
14
16
  $(this).text("אנא בחר צ'אט מסרגל הצד.");
15
17
  $(this).removeClass('text-muted');
16
18
  }
17
19
 
18
- if (currentText.includes("You have no active chats") || currentText === "You have no active chats.") {
19
- $(this).text("Please select a chat from the sidebar.");
20
+ if (currentText.includes('You have no active chats') || currentText === 'You have no active chats.') {
21
+ $(this).text('Please select a chat from the sidebar.');
20
22
  $(this).removeClass('text-muted');
21
23
  }
22
24
  });
23
25
  }
24
26
 
27
+ function getRoomData() {
28
+ if (!ajaxify || !ajaxify.data) {
29
+ return null;
30
+ }
31
+
32
+ return ajaxify.data.room || ajaxify.data;
33
+ }
34
+
35
+ function getLockText() {
36
+ if (isEnglishSystem()) {
37
+ return '🔒 This room was locked by the administrators.';
38
+ }
39
+
40
+ return LOCKED_MESSAGE_TEXT;
41
+ }
42
+
43
+ function renderLockBanner() {
44
+ const roomData = getRoomData();
45
+ const lockData = roomData && roomData.adminChatLock;
46
+ $('.admin-chat-lock-banner').remove();
47
+
48
+ if (!lockData || !lockData.isLocked) {
49
+ return;
50
+ }
51
+
52
+ const target = $('[component="chat/messages"]').first().parent();
53
+ if (!target.length) {
54
+ return;
55
+ }
56
+
57
+ target.prepend(`
58
+ <div class="admin-chat-lock-banner alert alert-warning mb-2">${getLockText()}</div>
59
+ `);
60
+ }
61
+
62
+ function setComposerVisibility(shouldHide) {
63
+ const selectors = [
64
+ '[component="chat/composer"]',
65
+ '[component="chat/replies"]',
66
+ '[component="chat/input"]',
67
+ '[component="chat/textarea"]',
68
+ '.chat-input',
69
+ '.composer',
70
+ ];
71
+
72
+ const seen = new Set();
73
+ selectors.forEach(function(selector) {
74
+ $(selector).each(function() {
75
+ const $element = $(this);
76
+ const $container = $element.closest('[component="chat/composer"], [component="chat/replies"], .chat-input, .composer').length ?
77
+ $element.closest('[component="chat/composer"], [component="chat/replies"], .chat-input, .composer').first() :
78
+ $element;
79
+ const domNode = $container.get(0);
80
+
81
+ if (!domNode || seen.has(domNode)) {
82
+ return;
83
+ }
84
+
85
+ seen.add(domNode);
86
+ $container.toggle(!shouldHide);
87
+ });
88
+ });
89
+ }
90
+
91
+ function updateComposerState() {
92
+ const roomData = getRoomData();
93
+ const lockData = roomData && roomData.adminChatLock;
94
+ const shouldHideComposer = !!(lockData && lockData.isLocked && !(app.user && app.user.isAdmin));
95
+
96
+ $('[component="chat/input"], [component="chat/textarea"], textarea.chat-input, .chat-input textarea')
97
+ .prop('disabled', shouldHideComposer)
98
+ .attr('placeholder', shouldHideComposer ? getLockText() : null);
99
+
100
+ $('[component="chat/send"], .chat-send, button[component="chat/submit"]').prop('disabled', shouldHideComposer);
101
+
102
+ setComposerVisibility(shouldHideComposer);
103
+ renderLockBanner();
104
+ }
105
+
106
+ function normalizeLockMessages() {
107
+ $('[component="chat/message/content"], .chat-message-content').each(function() {
108
+ const $el = $(this);
109
+ const text = $el.text().trim();
110
+ if (!text.startsWith(LOCK_PREFIX)) {
111
+ return;
112
+ }
113
+
114
+ $el.text(text.replace(LOCK_PREFIX, '').trim());
115
+ });
116
+ }
117
+
118
+ function getRoomMenuTargets() {
119
+ return $('[component="chat/header"], .chat-header, [component="chat/nav"]')
120
+ .find('.dropdown-menu');
121
+ }
122
+
123
+ function renderAdminLockControl() {
124
+ $('.admin-chat-lock-toggle-item, .admin-chat-lock-divider').remove();
125
+
126
+ if (!app.user.isAdmin) {
127
+ return;
128
+ }
129
+
130
+ const roomData = getRoomData();
131
+ const roomId = roomData && roomData.roomId;
132
+ if (!roomId) {
133
+ return;
134
+ }
135
+
136
+ const menus = getRoomMenuTargets();
137
+ if (!menus.length) {
138
+ return;
139
+ }
140
+
141
+ const lockData = roomData.adminChatLock || {};
142
+ const isLocked = !!lockData.isLocked;
143
+ const itemText = isEnglishSystem() ? (isLocked ? 'Unlock Room' : 'Lock Room') : (isLocked ? 'פתח חדר' : 'נעל חדר');
144
+ const iconClass = isLocked ? 'fa-lock-open' : 'fa-lock';
145
+ const menuItemHtml = `
146
+ <li role="presentation" class="admin-chat-lock-item-wrap">
147
+ <a href="#" role="menuitem" class="dropdown-item rounded-1 d-flex align-items-center gap-2 admin-chat-lock-toggle-item" data-room-id="${roomId}" data-locked="${isLocked}">
148
+ <i class="fa fa-fw ${iconClass}"></i>
149
+ <span>${itemText}</span>
150
+ </a>
151
+ </li>
152
+ <li role="presentation" class="dropdown-divider admin-chat-lock-divider"></li>
153
+ `;
154
+
155
+ menus.each(function() {
156
+ const $menu = $(this);
157
+ if ($menu.find('.admin-chat-lock-toggle-item').length) {
158
+ return;
159
+ }
160
+
161
+ $menu.prepend(menuItemHtml);
162
+ });
163
+ }
164
+
165
+ async function toggleRoomLock(roomId, nextState) {
166
+ const response = await fetch(`${config.relative_path || ''}/api/admin-chats/${roomId}/lock`, {
167
+ method: 'POST',
168
+ headers: {
169
+ 'Content-Type': 'application/json',
170
+ 'x-csrf-token': config.csrf_token,
171
+ },
172
+ body: JSON.stringify({ locked: nextState }),
173
+ });
174
+
175
+ if (!response.ok) {
176
+ throw new Error('Unable to update room lock');
177
+ }
178
+
179
+ return await response.json();
180
+ }
181
+
182
+ function refreshChatUi() {
183
+ replaceAdminEmptyStateText();
184
+ renderAdminLockControl();
185
+ updateComposerState();
186
+ normalizeLockMessages();
187
+ }
188
+
25
189
  $(window).on('action:ajaxify.end', function(ev, data) {
26
-
27
- if (app.user.isAdmin && ajaxify.data.template.name.startsWith('account/')) {
190
+ const templateName = ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '';
191
+
192
+ if (app.user.isAdmin && templateName.startsWith('account/')) {
28
193
  const userSlug = ajaxify.data.userslug || (ajaxify.data.user && ajaxify.data.user.userslug);
29
-
194
+
30
195
  if (userSlug) {
31
- const buttonText = isEnglishSystem() ? "View Chats" : "צפיה בצ'אטים";
32
-
33
- const relativePath = config.relative_path || '';
196
+ const buttonText = isEnglishSystem() ? 'View Chats' : "צפיה בצ'אטים";
197
+ const relativePath = config.relative_path || '';
34
198
 
35
199
  const btnHtml = `
36
200
  <li role="presentation">
37
201
  <a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="${relativePath}/user/${userSlug}/chats" role="menuitem">
38
- <i class="far fa-fw fa-comments"></i>
202
+ <i class="far fa-fw fa-comments"></i>
39
203
  <span>${buttonText}</span>
40
204
  </a>
41
205
  </li>
@@ -49,14 +213,48 @@ $(document).ready(function() {
49
213
  }
50
214
  }
51
215
 
52
- if (data.url.match(/^user\/.+\/chats/) || data.url === 'chats') {
53
- replaceAdminEmptyStateText();
54
- setTimeout(replaceAdminEmptyStateText, 500);
216
+ const url = data && data.url ? data.url : '';
217
+
218
+ if (url.match(/^user\/.+\/chats/) || url === 'chats') {
219
+ refreshChatUi();
220
+ setTimeout(refreshChatUi, 500);
55
221
  }
56
222
  });
57
223
 
58
- $(window).on('action:chat.loaded', replaceAdminEmptyStateText);
224
+ $(window).on('action:chat.loaded', function() {
225
+ refreshChatUi();
226
+ setTimeout(refreshChatUi, 200);
227
+ });
228
+
59
229
  $(window).on('action:chat.closed', function() {
60
- setTimeout(replaceAdminEmptyStateText, 200);
230
+ setTimeout(refreshChatUi, 200);
231
+ });
232
+
233
+ $(document).on('click', '.admin-chat-lock-toggle-item', async function(ev) {
234
+ ev.preventDefault();
235
+
236
+ const $button = $(this);
237
+ const roomId = parseInt($button.attr('data-room-id'), 10);
238
+ const isLocked = $button.attr('data-locked') === 'true';
239
+
240
+ if (!roomId) {
241
+ return;
242
+ }
243
+
244
+ $button.addClass('disabled').attr('aria-disabled', 'true');
245
+
246
+ try {
247
+ const result = await toggleRoomLock(roomId, !isLocked);
248
+ if (ajaxify && ajaxify.data) {
249
+ ajaxify.data.adminChatLock = result.lockData;
250
+ if (ajaxify.data.room) {
251
+ ajaxify.data.room.adminChatLock = result.lockData;
252
+ }
253
+ }
254
+ ajaxify.refresh();
255
+ } catch (err) {
256
+ app.alertError(isEnglishSystem() ? 'Unable to update room lock.' : 'לא ניתן לעדכן את נעילת החדר.');
257
+ $button.removeClass('disabled').removeAttr('aria-disabled');
258
+ }
61
259
  });
62
- });
260
+ });
package/library.js CHANGED
@@ -3,20 +3,21 @@
3
3
  const User = require.main.require('./src/user');
4
4
  const Messaging = require.main.require('./src/messaging');
5
5
  const db = require.main.require('./src/database');
6
- const _ = require('lodash');
7
6
 
8
7
  const plugin = {};
8
+ const LOCK_PREFIX = '[admin-chat-lock]';
9
+ const LOCKED_MESSAGE_TEXT = '🔒 חדר זה ננעל ע"י המנהלים.';
10
+ const UNLOCKED_MESSAGE_TEXT = '🔓 חדר זה נפתח מחדש ע"י המנהלים.';
9
11
 
10
12
  plugin.init = async function (params) {
11
-
13
+ registerRoutes(params.router, params.middleware);
12
14
  overrideMessagingFunctions();
13
15
  };
14
16
 
15
-
16
17
  plugin.addProfileLink = async function (data) {
17
18
  try {
18
19
  const userSlug = data.user ? data.user.userslug : (data.userData ? data.userData.userslug : null);
19
-
20
+
20
21
  if (userSlug) {
21
22
  data.links.push({
22
23
  id: 'admin-view-chats',
@@ -55,10 +56,19 @@ plugin.isUserInRoom = async function (payload) {
55
56
  };
56
57
 
57
58
  plugin.canReply = async function (payload) {
58
- const isAdmin = await User.isAdministrator(payload.uid);
59
+ const uid = getCallerUid(payload);
60
+ const roomId = getRoomId(payload);
61
+ const isAdmin = await User.isAdministrator(uid);
62
+
59
63
  if (isAdmin) {
60
64
  payload.canReply = true;
65
+ return payload;
61
66
  }
67
+
68
+ if (roomId && await isRoomLocked(roomId)) {
69
+ payload.canReply = false;
70
+ }
71
+
62
72
  return payload;
63
73
  };
64
74
 
@@ -88,14 +98,27 @@ plugin.canGetPublicChats = async function (payload) {
88
98
 
89
99
  plugin.onLoadRoom = async function (payload) {
90
100
  const { uid, room } = payload;
101
+ if (!room) {
102
+ return payload;
103
+ }
104
+
91
105
  const isAdmin = await User.isAdministrator(uid);
92
- if (!isAdmin || !room) return payload;
106
+ const lockData = await getRoomLockData(room.roomId);
107
+ room.adminChatLock = lockData;
108
+
109
+ if (lockData.isLocked) {
110
+ room.messages = appendLockNotice(room.messages, lockData);
111
+ }
112
+
113
+ if (!isAdmin) {
114
+ return payload;
115
+ }
93
116
 
94
117
  const isOfficialMember = await db.isSortedSetMember(`chat:room:${room.roomId}:uids`, uid);
95
118
 
96
119
  if (!isOfficialMember) {
97
120
  await db.sortedSetRemove(`chat:room:${room.roomId}:uids:online`, uid);
98
-
121
+
99
122
  if (Array.isArray(room.users)) {
100
123
  room.users = room.users.filter(user => user && parseInt(user.uid, 10) !== parseInt(uid, 10));
101
124
  }
@@ -105,17 +128,17 @@ plugin.onLoadRoom = async function (payload) {
105
128
  room.groupChat = room.userCount > 2;
106
129
 
107
130
  const allMids = await db.getSortedSetRevRange(`chat:room:${room.roomId}:mids`, 0, 49);
108
-
131
+
109
132
  if (allMids.length > 0) {
110
133
  const messages = await Messaging.getMessagesData(allMids, uid, room.roomId, false);
111
134
  const messageCount = await db.getObjectField(`chat:room:${room.roomId}`, 'messageCount');
112
135
  const count = parseInt(messageCount, 10) || 0;
113
-
136
+
114
137
  messages.forEach((msg, index) => {
115
138
  msg.index = count - index - 1;
116
139
  });
117
-
118
- room.messages = messages.reverse();
140
+
141
+ room.messages = appendLockNotice(messages.reverse(), lockData);
119
142
 
120
143
  room.messages.forEach((msg, index) => {
121
144
  if (index === 0) {
@@ -126,7 +149,7 @@ plugin.onLoadRoom = async function (payload) {
126
149
  }
127
150
  });
128
151
  }
129
-
152
+
130
153
  room.isAdmin = true;
131
154
  room.isOwner = true;
132
155
  } else {
@@ -136,10 +159,10 @@ plugin.onLoadRoom = async function (payload) {
136
159
  };
137
160
 
138
161
  function overrideMessagingFunctions() {
139
-
140
162
  const originalCanEdit = Messaging.canEdit;
141
163
  const originalCanDelete = Messaging.canDelete;
142
164
  const originalCanViewMessage = Messaging.canViewMessage;
165
+ const originalCanReply = Messaging.canReply;
143
166
 
144
167
  Messaging.canEdit = async function (messageId, uid) {
145
168
  if (await User.isAdministrator(uid)) {
@@ -161,6 +184,189 @@ function overrideMessagingFunctions() {
161
184
  }
162
185
  return await originalCanViewMessage(mids, roomId, uid);
163
186
  };
187
+
188
+ if (typeof originalCanReply === 'function') {
189
+ Messaging.canReply = async function (...args) {
190
+ const guessed = guessRoomIdUidFromArgs(args);
191
+ if (await User.isAdministrator(guessed.uid)) {
192
+ return true;
193
+ }
194
+ if (guessed.roomId && await isRoomLocked(guessed.roomId)) {
195
+ return false;
196
+ }
197
+ return await originalCanReply.apply(this, args);
198
+ };
199
+ }
200
+ };
201
+
202
+ function registerRoutes(router, middleware) {
203
+ if (!router) {
204
+ return;
205
+ }
206
+
207
+ const routeMiddleware = [];
208
+ if (middleware && typeof middleware.ensureLoggedIn === 'function') {
209
+ routeMiddleware.push(middleware.ensureLoggedIn);
210
+ }
211
+
212
+ router.post('/api/admin-chats/:roomId/lock', ...routeMiddleware, async (req, res) => {
213
+ try {
214
+ const actingUid = req.uid;
215
+ const roomId = parseInt(req.params.roomId, 10);
216
+
217
+ if (!actingUid || Number.isNaN(roomId)) {
218
+ return res.status(400).json({ status: { code: 'bad-request', message: 'Invalid room id' } });
219
+ }
220
+
221
+ if (!await User.isAdministrator(actingUid)) {
222
+ return res.status(403).json({ status: { code: 'forbidden', message: 'Admin only' } });
223
+ }
224
+
225
+ if (!await roomExists(roomId)) {
226
+ return res.status(404).json({ status: { code: 'not-found', message: 'Room not found' } });
227
+ }
228
+
229
+ const shouldLock = req.body && typeof req.body.locked === 'boolean' ? req.body.locked : true;
230
+ const lockData = shouldLock ?
231
+ await lockRoom(roomId, actingUid) :
232
+ await unlockRoom(roomId, actingUid);
233
+
234
+ return res.json({
235
+ status: { code: 'ok', message: 'Room updated' },
236
+ roomId,
237
+ lockData,
238
+ });
239
+ } catch (err) {
240
+ console.error('[Super Admin Chats] Error updating room lock:', err);
241
+ return res.status(500).json({ status: { code: 'error', message: err.message } });
242
+ }
243
+ });
244
+ }
245
+
246
+ async function roomExists(roomId) {
247
+ const roomData = await db.getObject(`chat:room:${roomId}`);
248
+ return !!roomData;
249
+ }
250
+
251
+ async function lockRoom(roomId, actingUid) {
252
+ const now = Date.now();
253
+ await db.setObject(`chat:room:${roomId}:adminLock`, {
254
+ isLocked: 1,
255
+ lockedBy: actingUid,
256
+ lockedAt: now,
257
+ });
258
+
259
+ await addLockSystemMessage(roomId, actingUid, true);
260
+ return await getRoomLockData(roomId);
261
+ }
262
+
263
+ async function unlockRoom(roomId, actingUid) {
264
+ await db.setObject(`chat:room:${roomId}:adminLock`, {
265
+ isLocked: 0,
266
+ lockedBy: actingUid,
267
+ lockedAt: 0,
268
+ });
269
+
270
+ await addLockSystemMessage(roomId, actingUid, false);
271
+ return await getRoomLockData(roomId);
272
+ }
273
+
274
+ async function addLockSystemMessage(roomId, actingUid, isLocked) {
275
+ if (typeof Messaging.addSystemMessage !== 'function') {
276
+ return;
277
+ }
278
+
279
+ const messageText = isLocked ? LOCK_PREFIX + ' ' + LOCKED_MESSAGE_TEXT : LOCK_PREFIX + ' ' + UNLOCKED_MESSAGE_TEXT;
280
+
281
+ try {
282
+ await Messaging.addSystemMessage(messageText, actingUid, roomId);
283
+ } catch (err) {
284
+ console.error('[Super Admin Chats] Error adding lock system message:', err);
285
+ }
286
+ }
287
+
288
+ async function isRoomLocked(roomId) {
289
+ const field = await db.getObjectField(`chat:room:${roomId}:adminLock`, 'isLocked');
290
+ return field === '1' || field === 1 || field === true;
291
+ }
292
+
293
+ async function getRoomLockData(roomId) {
294
+ const data = await db.getObject(`chat:room:${roomId}:adminLock`) || {};
295
+ const lockedBy = parseInt(data.lockedBy, 10) || 0;
296
+ const lockedAt = parseInt(data.lockedAt, 10) || 0;
297
+ const isLockedValue = data.isLocked === '1' || data.isLocked === 1 || data.isLocked === true;
298
+
299
+ return {
300
+ isLocked: isLockedValue,
301
+ lockedBy,
302
+ lockedAt,
303
+ };
304
+ }
305
+
306
+ function appendLockNotice(messages, lockData) {
307
+ const list = Array.isArray(messages) ? messages.slice() : [];
308
+ if (!lockData || !lockData.isLocked || !lockData.lockedAt) {
309
+ return list;
310
+ }
311
+
312
+ const content = `${LOCK_PREFIX} ${LOCKED_MESSAGE_TEXT}`;
313
+ const exists = list.some(msg => msg && msg.content === content);
314
+ if (exists) {
315
+ return list;
316
+ }
317
+
318
+ list.push({
319
+ content,
320
+ fromuid: lockData.lockedBy,
321
+ uid: lockData.lockedBy,
322
+ timestamp: lockData.lockedAt,
323
+ datetime: lockData.lockedAt,
324
+ system: true,
325
+ newSet: true,
326
+ });
327
+
328
+ list.sort((left, right) => {
329
+ const leftTime = parseInt(left && (left.timestamp || left.datetime), 10) || 0;
330
+ const rightTime = parseInt(right && (right.timestamp || right.datetime), 10) || 0;
331
+ return leftTime - rightTime;
332
+ });
333
+
334
+ return list;
335
+ }
336
+
337
+ function guessRoomIdUidFromArgs(args) {
338
+ if (!Array.isArray(args) || !args.length) {
339
+ return { roomId: null, uid: null };
340
+ }
341
+
342
+ if (args.length === 1 && args[0] && typeof args[0] === 'object') {
343
+ return {
344
+ roomId: getRoomId(args[0]),
345
+ uid: getCallerUid(args[0]),
346
+ };
347
+ }
348
+
349
+ return {
350
+ roomId: args[0],
351
+ uid: args[1],
352
+ };
353
+ }
354
+
355
+ function getCallerUid(payload) {
356
+ return payload && (
357
+ payload.uid ||
358
+ payload.callerUid ||
359
+ (payload.data && payload.data.uid) ||
360
+ (payload.message && payload.message.uid)
361
+ );
362
+ }
363
+
364
+ function getRoomId(payload) {
365
+ return payload && (
366
+ payload.roomId ||
367
+ (payload.data && payload.data.roomId) ||
368
+ (payload.message && payload.message.roomId)
369
+ );
164
370
  }
165
371
 
166
372
  module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-admin-chats",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "A plugin that allows the administrator to view and manage user chats.",
5
5
  "main": "library.js",
6
6
  "dependencies": {