nodebb-plugin-admin-chats 2.0.0 → 2.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.
package/library.js CHANGED
@@ -1,450 +1,836 @@
1
- 'use strict';
2
-
3
- const User = require.main.require('./src/user');
4
- const Messaging = require.main.require('./src/messaging');
5
- const ChatsAPI = require.main.require('./src/api/chats');
6
- const db = require.main.require('./src/database');
7
-
8
- const plugin = {};
9
- const LOCK_PREFIX = '[admin-chat-lock]';
10
- const LOCKED_MESSAGE_TEXT = '🔒 חדר זה ננעל ע"י המנהלים.';
11
-
12
- plugin.init = async function (params) {
13
- registerRoutes(params.router, params.middleware);
14
- overrideMessagingFunctions();
15
- overrideChatsApi();
16
- };
17
-
18
- plugin.addProfileLink = async function (data) {
19
- try {
20
- const userSlug = data.user ? data.user.userslug : (data.userData ? data.userData.userslug : null);
21
-
22
- if (userSlug) {
23
- data.links.push({
24
- id: 'admin-view-chats',
25
- route: 'user/' + userSlug + '/chats',
26
- icon: 'fa-comments',
27
- name: 'צפה בצ\'אטים',
28
- visibility: {
29
- self: false,
30
- other: true,
31
- moderator: false,
32
- globalMod: false,
33
- admin: true,
34
- }
35
- });
36
- }
37
- } catch (e) {
38
- console.error('[Super Admin Chats] Error adding profile link:', e);
39
- }
40
- return data;
41
- };
42
-
43
- plugin.isRoomOwner = async function (payload) {
44
- const isAdmin = await User.isAdministrator(payload.uid);
45
- if (isAdmin) {
46
- payload.isOwner = true;
47
- }
48
- return payload;
49
- };
50
-
51
- plugin.isUserInRoom = async function (payload) {
52
- const isAdmin = await User.isAdministrator(payload.uid);
53
- if (isAdmin) {
54
- payload.inRoom = true;
55
- }
56
- return payload;
57
- };
58
-
59
- plugin.canReply = async function (payload) {
60
- const uid = getCallerUid(payload);
61
- const roomId = getRoomId(payload);
62
- const isAdmin = await User.isAdministrator(uid);
63
-
64
- if (isAdmin) {
65
- payload.canReply = true;
66
- return payload;
67
- }
68
-
69
- if (roomId && await isRoomLocked(roomId)) {
70
- payload.canReply = false;
71
- }
72
-
73
- return payload;
74
- };
75
-
76
- plugin.filterMessagingSend = async function (payload) {
77
- const uid = getCallerUid(payload);
78
- const roomId = getRoomId(payload);
79
-
80
- if (!uid || !roomId) {
81
- return payload;
82
- }
83
-
84
- if (await User.isAdministrator(uid)) {
85
- return payload;
86
- }
87
-
88
- if (await isRoomLocked(roomId)) {
89
- throw new Error('[[admin-chats:errors.lockedAction]]');
90
- }
91
-
92
- return payload;
93
- };
94
- plugin.filterAddUsersToRoom = async function (payload) {
95
- if (await canNonAdminModifyLockedRoom(payload.uid, payload.roomId)) {
96
- throw new Error('[[admin-chats:errors.lockedAction]]');
97
- }
98
- return payload;
99
- };
100
-
101
- plugin.filterRemoveUsersFromRoom = async function (payload) {
102
- if (await canNonAdminModifyLockedRoom(payload.uid, payload.roomId)) {
103
- throw new Error('[[admin-chats:errors.lockedAction]]');
104
- }
105
- return payload;
106
- };
107
-
108
- plugin.filterRenameRoom = async function (payload) {
109
- if (await canNonAdminModifyLockedRoom(payload.uid, payload.roomId)) {
110
- throw new Error('[[admin-chats:errors.lockedAction]]');
111
- }
112
- return payload;
113
- };
114
-
115
- plugin.canGetMessages = async function (payload) {
116
- const isAdmin = await User.isAdministrator(payload.callerUid);
117
- if (isAdmin) {
118
- payload.canGet = true;
119
- }
120
- return payload;
121
- };
122
-
123
- plugin.canGetRecentChats = async function (payload) {
124
- const isAdmin = await User.isAdministrator(payload.callerUid);
125
- if (isAdmin) {
126
- payload.canGet = true;
127
- }
128
- return payload;
129
- };
130
-
131
- plugin.canGetPublicChats = async function (payload) {
132
- const isAdmin = await User.isAdministrator(payload.callerUid);
133
- if (isAdmin) {
134
- payload.canGet = true;
135
- }
136
- return payload;
137
- };
138
-
139
- plugin.onLoadRoom = async function (payload) {
140
- const { uid, room } = payload;
141
- if (!room) {
142
- return payload;
143
- }
144
-
145
- const isAdmin = await User.isAdministrator(uid);
146
- const lockData = await getRoomLockData(room.roomId);
147
- const isLockedForUser = lockData.isLocked && !isAdmin;
148
-
149
- room.adminChatLock = lockData;
150
- room.messages = buildRoomMessagesWithLockNotice(room.messages, lockData);
151
- room.canReply = room.canReply && !isLockedForUser;
152
- room.showUserInput = room.showUserInput && !isLockedForUser;
153
-
154
- if (!isAdmin) {
155
- return payload;
156
- }
157
-
158
- const isOfficialMember = await db.isSortedSetMember(`chat:room:${room.roomId}:uids`, uid);
159
-
160
- if (!isOfficialMember) {
161
- await db.sortedSetRemove(`chat:room:${room.roomId}:uids:online`, uid);
162
-
163
- if (Array.isArray(room.users)) {
164
- room.users = room.users.filter(user => user && parseInt(user.uid, 10) !== parseInt(uid, 10));
165
- }
166
- if (room.userCount > 0) {
167
- room.userCount -= 1;
168
- }
169
- room.groupChat = room.userCount > 2;
170
-
171
- const allMids = await db.getSortedSetRevRange(`chat:room:${room.roomId}:mids`, 0, 49);
172
-
173
- if (allMids.length > 0) {
174
- const messages = await Messaging.getMessagesData(allMids, uid, room.roomId, false);
175
- const messageCount = await db.getObjectField(`chat:room:${room.roomId}`, 'messageCount');
176
- const count = parseInt(messageCount, 10) || 0;
177
-
178
- messages.forEach((msg, index) => {
179
- msg.index = count - index - 1;
180
- });
181
-
182
- room.messages = buildRoomMessagesWithLockNotice(messages.reverse(), lockData);
183
-
184
- room.messages.forEach((msg, index) => {
185
- if (index === 0) {
186
- msg.newSet = true;
187
- } else {
188
- const prevMsg = room.messages[index - 1];
189
- msg.newSet = parseInt(msg.fromuid, 10) !== parseInt(prevMsg.fromuid, 10);
190
- }
191
- });
192
- }
193
-
194
- room.isAdmin = true;
195
- room.isOwner = true;
196
- } else {
197
- room.isAdmin = true;
198
- }
199
- return payload;
200
- };
201
-
202
- function overrideChatsApi() {
203
- if (!ChatsAPI || typeof ChatsAPI.kick !== 'function' || ChatsAPI.kick._adminChatLockWrapped) {
204
- return;
205
- }
206
-
207
- const originalKick = ChatsAPI.kick;
208
- const wrappedKick = async function (caller, data) {
209
- 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)) {
210
- throw new Error('[[admin-chats:errors.lockedAction]]');
211
- }
212
-
213
- return await originalKick.call(this, caller, data);
214
- };
215
-
216
- wrappedKick._adminChatLockWrapped = true;
217
- ChatsAPI.kick = wrappedKick;
218
- }
219
- function overrideMessagingFunctions() {
220
- const originalCanEdit = Messaging.canEdit;
221
- const originalCanDelete = Messaging.canDelete;
222
- const originalCanViewMessage = Messaging.canViewMessage;
223
- const originalCanReply = Messaging.canReply;
224
-
225
- Messaging.canEdit = async function (messageId, uid) {
226
- if (await User.isAdministrator(uid)) {
227
- return true;
228
- }
229
-
230
- const roomId = await getMessageRoomId(messageId);
231
- if (roomId && await isRoomLocked(roomId)) {
232
- throw new Error('[[admin-chats:errors.lockedAction]]');
233
- }
234
-
235
- return await originalCanEdit(messageId, uid);
236
- };
237
-
238
- Messaging.canDelete = async function (messageId, uid) {
239
- if (await User.isAdministrator(uid)) {
240
- return true;
241
- }
242
-
243
- const roomId = await getMessageRoomId(messageId);
244
- if (roomId && await isRoomLocked(roomId)) {
245
- throw new Error('[[admin-chats:errors.lockedAction]]');
246
- }
247
-
248
- return await originalCanDelete(messageId, uid);
249
- };
250
-
251
- Messaging.canViewMessage = async function (mids, roomId, uid) {
252
- if (await User.isAdministrator(uid)) {
253
- return Array.isArray(mids) ? mids.map(() => true) : true;
254
- }
255
- return await originalCanViewMessage(mids, roomId, uid);
256
- };
257
-
258
- if (typeof originalCanReply === 'function') {
259
- Messaging.canReply = async function (...args) {
260
- const guessed = guessRoomIdUidFromArgs(args);
261
- if (await User.isAdministrator(guessed.uid)) {
262
- return true;
263
- }
264
- if (guessed.roomId && await isRoomLocked(guessed.roomId)) {
265
- return false;
266
- }
267
- return await originalCanReply.apply(this, args);
268
- };
269
- }
270
- }
271
-
272
- function registerRoutes(router, middleware) {
273
- if (!router) {
274
- return;
275
- }
276
-
277
- const routeMiddleware = [];
278
- if (middleware && typeof middleware.ensureLoggedIn === 'function') {
279
- routeMiddleware.push(middleware.ensureLoggedIn);
280
- }
281
-
282
- router.post('/api/admin-chats/:roomId/lock', ...routeMiddleware, async (req, res) => {
283
- try {
284
- const actingUid = req.uid;
285
- const roomId = parseInt(req.params.roomId, 10);
286
-
287
- if (!actingUid || Number.isNaN(roomId)) {
288
- return res.status(400).json({ status: { code: 'bad-request', message: 'Invalid room id' } });
289
- }
290
-
291
- if (!await User.isAdministrator(actingUid)) {
292
- return res.status(403).json({ status: { code: 'forbidden', message: 'Admin only' } });
293
- }
294
-
295
- if (!await roomExists(roomId)) {
296
- return res.status(404).json({ status: { code: 'not-found', message: 'Room not found' } });
297
- }
298
-
299
- const shouldLock = req.body && typeof req.body.locked === 'boolean' ? req.body.locked : true;
300
- const lockData = shouldLock ?
301
- await lockRoom(roomId, actingUid) :
302
- await unlockRoom(roomId, actingUid);
303
-
304
- return res.json({
305
- status: { code: 'ok', message: 'Room updated' },
306
- roomId,
307
- lockData,
308
- });
309
- } catch (err) {
310
- console.error('[Super Admin Chats] Error updating room lock:', err);
311
- return res.status(500).json({ status: { code: 'error', message: err.message } });
312
- }
313
- });
314
- }
315
-
316
- async function canNonAdminModifyLockedRoom(uid, roomId) {
317
- if (!uid || !roomId) {
318
- return false;
319
- }
320
-
321
- if (await User.isAdministrator(uid)) {
322
- return false;
323
- }
324
-
325
- return await isRoomLocked(roomId);
326
- }
327
- async function roomExists(roomId) {
328
- const roomData = await db.getObject(`chat:room:${roomId}`);
329
- return !!roomData;
330
- }
331
-
332
- async function lockRoom(roomId, actingUid) {
333
- const now = Date.now();
334
- await db.setObject(`chat:room:${roomId}:adminLock`, {
335
- isLocked: 1,
336
- lockedBy: actingUid,
337
- lockedAt: now,
338
- });
339
-
340
- return await getRoomLockData(roomId);
341
- }
342
-
343
- async function unlockRoom(roomId, actingUid) {
344
- await db.setObject(`chat:room:${roomId}:adminLock`, {
345
- isLocked: 0,
346
- lockedBy: actingUid,
347
- lockedAt: 0,
348
- });
349
-
350
- return await getRoomLockData(roomId);
351
- }
352
-
353
- async function isRoomLocked(roomId) {
354
- const field = await db.getObjectField(`chat:room:${roomId}:adminLock`, 'isLocked');
355
- return field === '1' || field === 1 || field === true;
356
- }
357
-
358
- async function getMessageRoomId(messageId) {
359
- const roomId = await db.getObjectField(`message:${messageId}`, 'roomId');
360
- return parseInt(roomId, 10) || 0;
361
- }
362
-
363
- async function getRoomLockData(roomId) {
364
- const data = await db.getObject(`chat:room:${roomId}:adminLock`) || {};
365
- const lockedBy = parseInt(data.lockedBy, 10) || 0;
366
- const lockedAt = parseInt(data.lockedAt, 10) || 0;
367
- const isLockedValue = data.isLocked === '1' || data.isLocked === 1 || data.isLocked === true;
368
-
369
- return {
370
- isLocked: isLockedValue,
371
- lockedBy,
372
- lockedAt,
373
- };
374
- }
375
-
376
- function buildRoomMessagesWithLockNotice(messages, lockData) {
377
- const cleanedMessages = stripAdminLockMessages(messages);
378
- if (!lockData || !lockData.isLocked || !lockData.lockedAt) {
379
- return cleanedMessages;
380
- }
381
-
382
- cleanedMessages.push({
383
- content: `${LOCK_PREFIX} ${LOCKED_MESSAGE_TEXT}`,
384
- fromuid: lockData.lockedBy,
385
- uid: lockData.lockedBy,
386
- timestamp: lockData.lockedAt,
387
- datetime: lockData.lockedAt,
388
- system: true,
389
- newSet: true,
390
- });
391
-
392
- cleanedMessages.sort((left, right) => {
393
- const leftTime = parseInt(left && (left.timestamp || left.datetime), 10) || 0;
394
- const rightTime = parseInt(right && (right.timestamp || right.datetime), 10) || 0;
395
- return leftTime - rightTime;
396
- });
397
-
398
- return cleanedMessages;
399
- }
400
-
401
- function stripAdminLockMessages(messages) {
402
- const list = Array.isArray(messages) ? messages.slice() : [];
403
- return list.filter(msg => !isAdminLockMessage(msg));
404
- }
405
-
406
- function isAdminLockMessage(msg) {
407
- const content = String(msg && msg.content || '');
408
- return content.includes('admin-chat-lock') ||
409
- content.includes(LOCKED_MESSAGE_TEXT) ||
410
- content.includes('נפתח מחדש ע"י המנהלים') ||
411
- content.includes('modules:chat.system.[admin-chat-lock]');
412
- }
413
-
414
- function guessRoomIdUidFromArgs(args) {
415
- if (!Array.isArray(args) || !args.length) {
416
- return { roomId: null, uid: null };
417
- }
418
-
419
- if (args.length === 1 && args[0] && typeof args[0] === 'object') {
420
- return {
421
- roomId: getRoomId(args[0]),
422
- uid: getCallerUid(args[0]),
423
- };
424
- }
425
-
426
- return {
427
- roomId: args[0],
428
- uid: args[1],
429
- };
430
- }
431
-
432
- function getCallerUid(payload) {
433
- return payload && (
434
- payload.uid ||
435
- payload.callerUid ||
436
- (payload.data && payload.data.uid) ||
437
- (payload.message && payload.message.uid)
438
- );
439
- }
440
-
441
- function getRoomId(payload) {
442
- return payload && (
443
- payload.roomId ||
444
- (payload.data && payload.data.roomId) ||
445
- (payload.message && payload.message.roomId)
446
- );
447
- }
448
-
449
- module.exports = plugin;
450
-
1
+ 'use strict';
2
+
3
+ const User = require.main.require('./src/user');
4
+ const Messaging = require.main.require('./src/messaging');
5
+ const ChatsAPI = require.main.require('./src/api/chats');
6
+ const db = require.main.require('./src/database');
7
+ const meta = require.main.require('./src/meta');
8
+ const privileges = require.main.require('./src/privileges');
9
+ const helpers = require.main.require('./src/controllers/helpers');
10
+
11
+ const plugin = {};
12
+ const LOCK_PREFIX = '[admin-chat-lock]';
13
+ const LOCKED_MESSAGE_TEXT = '🔒 חדר זה ננעל ע"י המנהלים.';
14
+
15
+ async function getLockedActionMessage(uid) {
16
+ try {
17
+ const userData = await User.getUserData(uid);
18
+ let userLang = userData && userData.settings && userData.userLang;
19
+
20
+ // If user doesn't have specific language setting, use forum default
21
+ if (!userLang) {
22
+ const meta = require.main.require('./src/meta');
23
+ userLang = meta.config.defaultLang || 'en-GB';
24
+ }
25
+
26
+ if (userLang === 'he' || userLang === 'he-IL') {
27
+ return 'לא ניתן לבצע פעולה זו בחדר נעול';
28
+ } else {
29
+ return 'This action cannot be performed in a locked room.';
30
+ }
31
+ } catch (err) {
32
+ // Default to English if we can't determine language
33
+ return 'This action cannot be performed in a locked room.';
34
+ }
35
+ }
36
+ const ADMIN_CHAT_PAGE_SIZE = 30;
37
+ const ADMIN_CHAT_SCAN_SIZE = 100;
38
+
39
+ plugin.init = async function (params) {
40
+ registerRoutes(params.app, params.router, params.middleware);
41
+ overrideMessagingFunctions();
42
+ overrideChatsApi();
43
+ overrideCoreChatRedirect(params.controllers);
44
+ };
45
+
46
+ plugin.addScripts = async function (data) {
47
+ let isHebrew = false;
48
+
49
+ const uid = data.req && data.req.uid;
50
+ if (uid) {
51
+ try {
52
+ const userData = await User.getUserData(uid);
53
+ let userLang = userData && userData.settings && userData.userLang;
54
+
55
+ // If user doesn't have specific language setting, use forum default
56
+ if (!userLang) {
57
+ const meta = require.main.require('./src/meta');
58
+ userLang = meta.config.defaultLang || 'en-GB';
59
+ }
60
+
61
+ isHebrew = userLang === 'he' || userLang === 'he-IL';
62
+ } catch (err) {
63
+ // Default to English if error
64
+ isHebrew = false;
65
+ }
66
+ }
67
+
68
+ // Only send the lockedAction translation from server
69
+ const translations = {
70
+ lockedAction: isHebrew ? 'לא ניתן לבצע פעולה זו בחדר נעול' : 'This action cannot be performed in a locked room.',
71
+ };
72
+
73
+ data.scripts = data.scripts || [];
74
+ data.scripts.push({
75
+ src: false,
76
+ script: `window.adminChatsTranslations = ${JSON.stringify(translations)};`
77
+ });
78
+
79
+ return data;
80
+ };
81
+
82
+ plugin.addProfileLink = async function (data) {
83
+ try {
84
+ const userSlug = data.user ? data.user.userslug : (data.userData ? data.userData.userslug : null);
85
+
86
+ if (userSlug) {
87
+ data.links.push({
88
+ id: 'admin-view-chats',
89
+ route: 'user/' + userSlug + '/chats',
90
+ icon: 'fa-comments',
91
+ name: 'צפה בצ\'אטים',
92
+ visibility: {
93
+ self: false,
94
+ other: true,
95
+ moderator: false,
96
+ globalMod: false,
97
+ admin: true,
98
+ }
99
+ });
100
+ }
101
+ } catch (e) {
102
+ // Silent error handling
103
+ }
104
+ return data;
105
+ };
106
+
107
+ plugin.isRoomOwner = async function (payload) {
108
+ const isAdmin = await User.isAdministrator(payload.uid);
109
+ if (isAdmin) {
110
+ payload.isOwner = true;
111
+ }
112
+ return payload;
113
+ };
114
+
115
+ plugin.isUserInRoom = async function (payload) {
116
+ const isAdmin = await User.isAdministrator(payload.uid);
117
+ if (isAdmin) {
118
+ payload.inRoom = true;
119
+ }
120
+ return payload;
121
+ };
122
+
123
+ plugin.canReply = async function (payload) {
124
+ const uid = getCallerUid(payload);
125
+ const roomId = getRoomId(payload);
126
+ const isAdmin = await User.isAdministrator(uid);
127
+
128
+ if (isAdmin) {
129
+ payload.canReply = true;
130
+ return payload;
131
+ }
132
+
133
+ if (roomId && await isRoomLocked(roomId)) {
134
+ payload.canReply = false;
135
+ }
136
+
137
+ return payload;
138
+ };
139
+
140
+ plugin.filterMessagingSend = async function (payload) {
141
+ const uid = getCallerUid(payload);
142
+ const roomId = getRoomId(payload);
143
+
144
+ if (!uid || !roomId) {
145
+ return payload;
146
+ }
147
+
148
+ if (await User.isAdministrator(uid)) {
149
+ return payload;
150
+ }
151
+
152
+ if (await isRoomLocked(roomId)) {
153
+ const message = await getLockedActionMessage(uid);
154
+ throw new Error(message);
155
+ }
156
+
157
+ return payload;
158
+ };
159
+ plugin.filterAddUsersToRoom = async function (payload) {
160
+ if (await canNonAdminModifyLockedRoom(payload.uid, payload.roomId)) {
161
+ const message = await getLockedActionMessage(payload.uid);
162
+ throw new Error(message);
163
+ }
164
+ return payload;
165
+ };
166
+
167
+ plugin.filterRemoveUsersFromRoom = async function (payload) {
168
+ if (await canNonAdminModifyLockedRoom(payload.uid, payload.roomId)) {
169
+ const message = await getLockedActionMessage(payload.uid);
170
+ throw new Error(message);
171
+ }
172
+ return payload;
173
+ };
174
+
175
+ plugin.filterRenameRoom = async function (payload) {
176
+ if (await canNonAdminModifyLockedRoom(payload.uid, payload.roomId)) {
177
+ const message = await getLockedActionMessage(payload.uid);
178
+ throw new Error(message);
179
+ }
180
+ return payload;
181
+ };
182
+
183
+ plugin.canGetMessages = async function (payload) {
184
+ const isAdmin = await User.isAdministrator(payload.callerUid);
185
+ if (isAdmin) {
186
+ payload.canGet = true;
187
+ }
188
+ return payload;
189
+ };
190
+
191
+ plugin.canGetRecentChats = async function (payload) {
192
+ const isAdmin = await User.isAdministrator(payload.callerUid);
193
+ if (isAdmin) {
194
+ payload.canGet = true;
195
+ }
196
+ return payload;
197
+ };
198
+
199
+ plugin.canGetPublicChats = async function (payload) {
200
+ const isAdmin = await User.isAdministrator(payload.callerUid);
201
+ if (isAdmin) {
202
+ payload.canGet = true;
203
+ }
204
+ return payload;
205
+ };
206
+
207
+ plugin.onLoadRoom = async function (payload) {
208
+ const { uid, room } = payload;
209
+ if (!room) {
210
+ return payload;
211
+ }
212
+
213
+ const isAdmin = await User.isAdministrator(uid);
214
+ const lockData = await getRoomLockData(room.roomId);
215
+ const isLockedForUser = lockData.isLocked && !isAdmin;
216
+
217
+ room.adminChatLock = lockData;
218
+ room.messages = ensureFirstVisibleMessageHeader(buildRoomMessagesWithLockNotice(room.messages, lockData));
219
+ room.canReply = room.canReply && !isLockedForUser;
220
+ room.showUserInput = room.showUserInput && !isLockedForUser;
221
+
222
+ if (!isAdmin) {
223
+ return payload;
224
+ }
225
+
226
+ const isOfficialMember = await db.isSortedSetMember(`chat:room:${room.roomId}:uids`, uid);
227
+
228
+ if (!isOfficialMember) {
229
+ await db.sortedSetRemove(`chat:room:${room.roomId}:uids:online`, uid);
230
+
231
+ if (Array.isArray(room.users)) {
232
+ room.users = room.users.filter(user => user && parseInt(user.uid, 10) !== parseInt(uid, 10));
233
+ }
234
+ if (room.userCount > 0) {
235
+ room.userCount -= 1;
236
+ }
237
+ room.groupChat = room.userCount > 2;
238
+
239
+ const allMids = await db.getSortedSetRevRange(`chat:room:${room.roomId}:mids`, 0, 49);
240
+
241
+ if (allMids.length > 0) {
242
+ const messages = await Messaging.getMessagesData(allMids, uid, room.roomId, false);
243
+ const messageCount = await db.getObjectField(`chat:room:${room.roomId}`, 'messageCount');
244
+ const count = parseInt(messageCount, 10) || 0;
245
+
246
+ messages.forEach((msg, index) => {
247
+ msg.index = count - index - 1;
248
+ });
249
+
250
+ room.messages = ensureFirstVisibleMessageHeader(buildRoomMessagesWithLockNotice(messages.reverse(), lockData));
251
+
252
+ room.messages.forEach((msg, index) => {
253
+ if (index === 0) {
254
+ msg.newSet = true;
255
+ } else {
256
+ const prevMsg = room.messages[index - 1];
257
+ const prevTime = parseInt(prevMsg && prevMsg.timestamp, 10) || 0;
258
+ const currentTime = parseInt(msg && msg.timestamp, 10) || 0;
259
+
260
+ msg.newSet = !!(
261
+ currentTime > prevTime + Messaging.newMessageCutoff ||
262
+ parseInt(msg.fromuid, 10) !== parseInt(prevMsg.fromuid, 10) ||
263
+ prevMsg.system ||
264
+ msg.toMid
265
+ );
266
+ }
267
+ });
268
+
269
+ room.messages = ensureFirstVisibleMessageHeader(room.messages);
270
+ }
271
+
272
+ room.isAdmin = true;
273
+ room.isOwner = true;
274
+ } else {
275
+ room.isAdmin = true;
276
+ }
277
+ return payload;
278
+ };
279
+
280
+ function overrideCoreChatRedirect(controllers) {
281
+ const chatsController = controllers && controllers.accounts && controllers.accounts.chats;
282
+ if (!chatsController || typeof chatsController.redirectToChat !== 'function' || chatsController.redirectToChat._adminChatsWrapped) {
283
+ return;
284
+ }
285
+
286
+ const originalRedirectToChat = chatsController.redirectToChat;
287
+ const wrappedRedirectToChat = async function (req, res, next) {
288
+ if (req && req.uid && await User.isAdministrator(req.uid)) {
289
+ return await renderAdminChatsPage(req, res, next);
290
+ }
291
+
292
+ return await originalRedirectToChat.call(this, req, res, next);
293
+ };
294
+
295
+ wrappedRedirectToChat._adminChatsWrapped = true;
296
+ chatsController.redirectToChat = wrappedRedirectToChat;
297
+ }
298
+
299
+ function overrideChatsApi() {
300
+ if (!ChatsAPI || typeof ChatsAPI.kick !== 'function' || ChatsAPI.kick._adminChatLockWrapped) {
301
+ return;
302
+ }
303
+
304
+ const originalKick = ChatsAPI.kick;
305
+ const wrappedKick = async function (caller, data) {
306
+ 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)) {
307
+ const message = await getLockedActionMessage(caller.uid);
308
+ throw new Error(message);
309
+ }
310
+
311
+ return await originalKick.call(this, caller, data);
312
+ };
313
+
314
+ wrappedKick._adminChatLockWrapped = true;
315
+ ChatsAPI.kick = wrappedKick;
316
+ }
317
+ function overrideMessagingFunctions() {
318
+ const originalCanEdit = Messaging.canEdit;
319
+ const originalCanDelete = Messaging.canDelete;
320
+ const originalCanViewMessage = Messaging.canViewMessage;
321
+ const originalCanReply = Messaging.canReply;
322
+
323
+ Messaging.canEdit = async function (messageId, uid) {
324
+ if (await User.isAdministrator(uid)) {
325
+ return true;
326
+ }
327
+
328
+ const roomId = await getMessageRoomId(messageId);
329
+ if (roomId && await isRoomLocked(roomId)) {
330
+ const message = await getLockedActionMessage(uid);
331
+ throw new Error(message);
332
+ }
333
+
334
+ return await originalCanEdit(messageId, uid);
335
+ };
336
+
337
+ Messaging.canDelete = async function (messageId, uid) {
338
+ if (await User.isAdministrator(uid)) {
339
+ return true;
340
+ }
341
+
342
+ const roomId = await getMessageRoomId(messageId);
343
+ if (roomId && await isRoomLocked(roomId)) {
344
+ const message = await getLockedActionMessage(uid);
345
+ throw new Error(message);
346
+ }
347
+
348
+ return await originalCanDelete(messageId, uid);
349
+ };
350
+
351
+ Messaging.canViewMessage = async function (mids, roomId, uid) {
352
+ if (await User.isAdministrator(uid)) {
353
+ return Array.isArray(mids) ? mids.map(() => true) : true;
354
+ }
355
+ return await originalCanViewMessage(mids, roomId, uid);
356
+ };
357
+
358
+ if (typeof originalCanReply === 'function') {
359
+ Messaging.canReply = async function (...args) {
360
+ const guessed = guessRoomIdUidFromArgs(args);
361
+ if (await User.isAdministrator(guessed.uid)) {
362
+ return true;
363
+ }
364
+ if (guessed.roomId && await isRoomLocked(guessed.roomId)) {
365
+ return false;
366
+ }
367
+ return await originalCanReply.apply(this, args);
368
+ };
369
+ }
370
+ }
371
+
372
+ function registerRoutes(app, router, middleware) {
373
+ if (!router) {
374
+ return;
375
+ }
376
+
377
+ const routeMiddleware = [];
378
+ if (middleware && typeof middleware.ensureLoggedIn === 'function') {
379
+ routeMiddleware.push(middleware.ensureLoggedIn);
380
+ }
381
+
382
+ const pageMiddlewares = [
383
+ middleware.autoLocale,
384
+ middleware.applyBlacklist,
385
+ middleware.authenticateRequest,
386
+ middleware.redirectToHomeIfBanned,
387
+ middleware.maintenanceMode,
388
+ middleware.registrationComplete,
389
+ middleware.pluginHooks,
390
+ ...routeMiddleware,
391
+ middleware.pageView,
392
+ ].filter(Boolean);
393
+
394
+ const pageController = async (req, res, next) => {
395
+ try {
396
+ await renderAdminChatsPage(req, res, next);
397
+ } catch (err) {
398
+ next(err);
399
+ }
400
+ };
401
+
402
+ const pageRouter = app || router;
403
+ pageRouter.get('/chats/:roomId?/:index?', middleware.busyCheck, pageMiddlewares, middleware.buildHeader, pageController);
404
+
405
+ router.get('/api/admin-chats', ...routeMiddleware, async (req, res) => {
406
+ try {
407
+ if (!await assertAdminChatsAccess(req, res)) {
408
+ return;
409
+ }
410
+
411
+ const start = Math.max(0, parseInt(req.query.start, 10) || 0);
412
+ const data = await getAdminRecentChats(req.uid, start, ADMIN_CHAT_PAGE_SIZE);
413
+ res.json(data);
414
+ } catch (err) {
415
+ res.status(500).json({ status: { code: 'error', message: err.message } });
416
+ }
417
+ });
418
+
419
+ router.get('/api/admin-chats/page/:roomId?/:index?', ...routeMiddleware, async (req, res) => {
420
+ try {
421
+ if (!await assertAdminChatsAccess(req, res)) {
422
+ return;
423
+ }
424
+
425
+ const payload = await buildAdminChatsPayload(req);
426
+ res.json(payload);
427
+ } catch (err) {
428
+ res.status(500).json({ status: { code: 'error', message: err.message } });
429
+ }
430
+ });
431
+
432
+ router.post('/api/admin-chats/:roomId/lock', ...routeMiddleware, async (req, res) => {
433
+ try {
434
+ const actingUid = req.uid;
435
+ const roomId = parseInt(req.params.roomId, 10);
436
+
437
+ if (!actingUid || Number.isNaN(roomId)) {
438
+ return res.status(400).json({ status: { code: 'bad-request', message: 'Invalid room id' } });
439
+ }
440
+
441
+ if (!await User.isAdministrator(actingUid)) {
442
+ return res.status(403).json({ status: { code: 'forbidden', message: 'Admin only' } });
443
+ }
444
+
445
+ if (!await roomExists(roomId)) {
446
+ return res.status(404).json({ status: { code: 'not-found', message: 'Room not found' } });
447
+ }
448
+
449
+ const shouldLock = req.body && typeof req.body.locked === 'boolean' ? req.body.locked : true;
450
+ const lockData = shouldLock ?
451
+ await lockRoom(roomId, actingUid) :
452
+ await unlockRoom(roomId, actingUid);
453
+
454
+ return res.json({
455
+ status: { code: 'ok', message: 'Room updated' },
456
+ roomId,
457
+ lockData,
458
+ });
459
+ } catch (err) {
460
+ return res.status(500).json({ status: { code: 'error', message: err.message } });
461
+ }
462
+ });
463
+ }
464
+
465
+ async function renderAdminChatsPage(req, res, next) {
466
+ if (meta.config.disableChat) {
467
+ return next();
468
+ }
469
+
470
+ if (!await assertAdminChatsAccess(req, res)) {
471
+ return;
472
+ }
473
+
474
+ const payload = await buildAdminChatsPayload(req);
475
+ if (req.params.roomId && !payload.roomId) {
476
+ return next();
477
+ }
478
+
479
+ res.render('chats', payload);
480
+ }
481
+
482
+ async function assertAdminChatsAccess(req, res) {
483
+ if (!req.uid || !await User.isAdministrator(req.uid)) {
484
+ helpers.notAllowed(req, res);
485
+ return false;
486
+ }
487
+
488
+ return true;
489
+ }
490
+
491
+ async function buildAdminChatsPayload(req) {
492
+ const userslug = await User.getUserField(req.uid, 'userslug');
493
+ const [recentChats, publicRooms, privateRoomCount] = await Promise.all([
494
+ getAdminRecentChats(req.uid, 0, ADMIN_CHAT_PAGE_SIZE),
495
+ Messaging.getPublicRooms(req.uid, req.uid),
496
+ getPrivateRoomCount(),
497
+ ]);
498
+
499
+ const payload = {
500
+ title: '[[pages:chats]]',
501
+ uid: req.uid,
502
+ userslug,
503
+ adminAllChats: true,
504
+ rooms: recentChats.rooms,
505
+ nextStart: recentChats.nextStart,
506
+ publicRooms: publicRooms || [],
507
+ privateRoomCount,
508
+ bodyClasses: ['page-user-chats'],
509
+ };
510
+
511
+ const roomId = parseInt(req.params.roomId, 10) || 0;
512
+ if (!roomId) {
513
+ return payload;
514
+ }
515
+
516
+ const roomPayload = await buildAdminChatRoomPayload(req.uid, roomId, req.params.index);
517
+ if (!roomPayload) {
518
+ return payload;
519
+ }
520
+
521
+ return {
522
+ ...payload,
523
+ ...roomPayload,
524
+ };
525
+ }
526
+
527
+ async function buildAdminChatRoomPayload(uid, roomId, indexParam) {
528
+ let start = 0;
529
+ let scrollToIndex = null;
530
+
531
+ if (indexParam) {
532
+ const msgCount = await db.getObjectField(`chat:room:${roomId}`, 'messageCount');
533
+ start = Math.max(0, parseInt(msgCount, 10) - parseInt(indexParam, 10) - 49);
534
+ scrollToIndex = Math.min(msgCount, Math.max(0, parseInt(indexParam, 10) || 1));
535
+ }
536
+
537
+ const room = await Messaging.loadRoom(uid, {
538
+ uid,
539
+ roomId,
540
+ start,
541
+ });
542
+
543
+ if (!room) {
544
+ return null;
545
+ }
546
+
547
+ const [canViewInfo, canUploadImage, canUploadFile] = await privileges.global.can([
548
+ 'view:users:info', 'upload:post:image', 'upload:post:file',
549
+ ], uid);
550
+
551
+ room.title = room.roomName || room.usernames || '[[pages:chats]]';
552
+ room.bodyClasses = ['page-user-chats', 'chat-loaded'];
553
+ room.canViewInfo = canViewInfo;
554
+ room.canUpload = (canUploadImage || canUploadFile) && (meta.config.maximumFileSize > 0 || room.isAdmin);
555
+ room.scrollToIndex = scrollToIndex;
556
+
557
+ return room;
558
+ }
559
+
560
+ async function getPrivateRoomCount() {
561
+ const [totalCount, publicCount] = await Promise.all([
562
+ db.sortedSetCard('chat:rooms'),
563
+ db.sortedSetCard('chat:rooms:public'),
564
+ ]);
565
+
566
+ return Math.max(0, (parseInt(totalCount, 10) || 0) - (parseInt(publicCount, 10) || 0));
567
+ }
568
+
569
+ async function getAdminRecentChats(uid, start, limit) {
570
+ let cursor = Math.max(0, parseInt(start, 10) || 0);
571
+ const roomPairs = [];
572
+
573
+ while (roomPairs.length < limit) {
574
+ const roomIds = await db.getSortedSetRevRange('chat:rooms', cursor, cursor + ADMIN_CHAT_SCAN_SIZE - 1);
575
+ if (!roomIds.length) {
576
+ break;
577
+ }
578
+
579
+ const rooms = await Messaging.getRoomsData(roomIds);
580
+ rooms.forEach((room, index) => {
581
+ if (room && !room.public && roomPairs.length < limit) {
582
+ roomPairs.push({
583
+ roomId: roomIds[index],
584
+ room,
585
+ });
586
+ }
587
+ });
588
+
589
+ cursor += roomIds.length;
590
+ if (roomIds.length < ADMIN_CHAT_SCAN_SIZE) {
591
+ break;
592
+ }
593
+ }
594
+
595
+ const rooms = roomPairs.map(item => item.room);
596
+ const roomIds = roomPairs.map(item => item.roomId);
597
+
598
+ await enrichAdminRecentRooms(uid, roomIds, rooms);
599
+
600
+ return {
601
+ rooms,
602
+ nextStart: cursor,
603
+ };
604
+ }
605
+
606
+ async function enrichAdminRecentRooms(uid, roomIds, rooms) {
607
+ if (!roomIds.length) {
608
+ return;
609
+ }
610
+
611
+ const roomUsers = await Promise.all(roomIds.map(roomId => Messaging.getUidsInRoom(roomId, 0, -1)));
612
+ const uniqueUids = [...new Set(roomUsers.flat().filter(Boolean))];
613
+ const userMap = new Map();
614
+
615
+ if (uniqueUids.length) {
616
+ const users = await User.getUsersFields(uniqueUids, [
617
+ 'uid', 'username', 'userslug', 'displayname', 'picture', 'status', 'lastonline',
618
+ ]);
619
+ uniqueUids.forEach((memberUid, index) => {
620
+ const userData = users[index];
621
+ if (userData) {
622
+ userData.status = User.getStatus(userData);
623
+ userMap.set(String(memberUid), userData);
624
+ }
625
+ });
626
+ }
627
+
628
+ const teasers = await Promise.all(roomIds.map(roomId => getAdminRoomTeaser(roomId)));
629
+
630
+ rooms.forEach((room, index) => {
631
+ if (!room) {
632
+ return;
633
+ }
634
+
635
+ room.users = (roomUsers[index] || [])
636
+ .map(memberUid => userMap.get(String(memberUid)))
637
+ .filter(Boolean);
638
+ room.groupChat = room.userCount > 2;
639
+ room.unread = false;
640
+ room.teaser = teasers[index];
641
+ room.lastUser = room.users[0];
642
+ room.usernames = Messaging.generateUsernames(room, uid);
643
+ room.participantsLabel = buildParticipantsLabel(room.users);
644
+ room.icon = Messaging.getRoomIcon(room);
645
+ });
646
+ }
647
+
648
+ function buildParticipantsLabel(users) {
649
+ const list = Array.isArray(users) ? users.filter(Boolean) : [];
650
+ if (!list.length) {
651
+ return '';
652
+ }
653
+
654
+ const names = list.map(user => user.displayname || user.username).filter(Boolean);
655
+ if (names.length <= 5) {
656
+ return names.join(', ');
657
+ }
658
+
659
+ return `${names.slice(0, 5).join(', ')} +${names.length - 5}`;
660
+ }
661
+
662
+ async function getAdminRoomTeaser(roomId) {
663
+ const mids = await db.getSortedSetRevRange(`chat:room:${roomId}:mids`, 0, 19);
664
+ if (!mids.length) {
665
+ return null;
666
+ }
667
+
668
+ const teaser = (await Messaging.getMessagesFields(mids, ['fromuid', 'content', 'timestamp', 'deleted', 'system']))
669
+ .find(message => message && !message.deleted && !message.system && message.fromuid);
670
+
671
+ if (!teaser) {
672
+ return null;
673
+ }
674
+
675
+ const teaserUser = await User.getUserFields(teaser.fromuid, [
676
+ 'uid', 'username', 'userslug', 'displayname', 'picture', 'status', 'lastonline',
677
+ ]);
678
+
679
+ if (teaserUser) {
680
+ teaser.user = teaserUser;
681
+ }
682
+
683
+ teaser.content = String(teaser.content || '')
684
+ .replace(/<[^>]*>/g, '')
685
+ .trim();
686
+ teaser.roomId = roomId;
687
+
688
+ return teaser;
689
+ }
690
+
691
+ async function canNonAdminModifyLockedRoom(uid, roomId) {
692
+ if (!uid || !roomId) {
693
+ return false;
694
+ }
695
+
696
+ if (await User.isAdministrator(uid)) {
697
+ return false;
698
+ }
699
+
700
+ return await isRoomLocked(roomId);
701
+ }
702
+ async function roomExists(roomId) {
703
+ const roomData = await db.getObject(`chat:room:${roomId}`);
704
+ return !!roomData;
705
+ }
706
+
707
+ async function lockRoom(roomId, actingUid) {
708
+ const now = Date.now();
709
+ await db.setObject(`chat:room:${roomId}:adminLock`, {
710
+ isLocked: 1,
711
+ lockedBy: actingUid,
712
+ lockedAt: now,
713
+ });
714
+
715
+ return await getRoomLockData(roomId);
716
+ }
717
+
718
+ async function unlockRoom(roomId, actingUid) {
719
+ await db.setObject(`chat:room:${roomId}:adminLock`, {
720
+ isLocked: 0,
721
+ lockedBy: actingUid,
722
+ lockedAt: 0,
723
+ });
724
+
725
+ return await getRoomLockData(roomId);
726
+ }
727
+
728
+ async function isRoomLocked(roomId) {
729
+ const field = await db.getObjectField(`chat:room:${roomId}:adminLock`, 'isLocked');
730
+ return field === '1' || field === 1 || field === true;
731
+ }
732
+
733
+ async function getMessageRoomId(messageId) {
734
+ const roomId = await db.getObjectField(`message:${messageId}`, 'roomId');
735
+ return parseInt(roomId, 10) || 0;
736
+ }
737
+
738
+ async function getRoomLockData(roomId) {
739
+ const data = await db.getObject(`chat:room:${roomId}:adminLock`) || {};
740
+ const lockedBy = parseInt(data.lockedBy, 10) || 0;
741
+ const lockedAt = parseInt(data.lockedAt, 10) || 0;
742
+ const isLockedValue = data.isLocked === '1' || data.isLocked === 1 || data.isLocked === true;
743
+
744
+ return {
745
+ isLocked: isLockedValue,
746
+ lockedBy,
747
+ lockedAt,
748
+ };
749
+ }
750
+
751
+ function ensureFirstVisibleMessageHeader(messages) {
752
+ const list = Array.isArray(messages) ? messages.slice() : [];
753
+ const firstUserMessageIndex = list.findIndex(msg => msg && !msg.system);
754
+
755
+ if (firstUserMessageIndex !== -1) {
756
+ list[firstUserMessageIndex].newSet = true;
757
+ }
758
+
759
+ return list;
760
+ }
761
+
762
+ function buildRoomMessagesWithLockNotice(messages, lockData) {
763
+ const cleanedMessages = stripAdminLockMessages(messages);
764
+ if (!lockData || !lockData.isLocked || !lockData.lockedAt) {
765
+ return cleanedMessages;
766
+ }
767
+
768
+ cleanedMessages.push({
769
+ content: `${LOCK_PREFIX} ${LOCKED_MESSAGE_TEXT}`,
770
+ fromuid: lockData.lockedBy,
771
+ uid: lockData.lockedBy,
772
+ timestamp: lockData.lockedAt,
773
+ datetime: lockData.lockedAt,
774
+ system: true,
775
+ newSet: true,
776
+ });
777
+
778
+ cleanedMessages.sort((left, right) => {
779
+ const leftTime = parseInt(left && (left.timestamp || left.datetime), 10) || 0;
780
+ const rightTime = parseInt(right && (right.timestamp || right.datetime), 10) || 0;
781
+ return leftTime - rightTime;
782
+ });
783
+
784
+ return cleanedMessages;
785
+ }
786
+
787
+ function stripAdminLockMessages(messages) {
788
+ const list = Array.isArray(messages) ? messages.slice() : [];
789
+ return list.filter(msg => !isAdminLockMessage(msg));
790
+ }
791
+
792
+ function isAdminLockMessage(msg) {
793
+ const content = String(msg && msg.content || '');
794
+ return content.includes('admin-chat-lock') ||
795
+ content.includes(LOCKED_MESSAGE_TEXT) ||
796
+ content.includes('נפתח מחדש ע"י המנהלים') ||
797
+ content.includes('modules:chat.system.[admin-chat-lock]');
798
+ }
799
+
800
+ function guessRoomIdUidFromArgs(args) {
801
+ if (!Array.isArray(args) || !args.length) {
802
+ return { roomId: null, uid: null };
803
+ }
804
+
805
+ if (args.length === 1 && args[0] && typeof args[0] === 'object') {
806
+ return {
807
+ roomId: getRoomId(args[0]),
808
+ uid: getCallerUid(args[0]),
809
+ };
810
+ }
811
+
812
+ return {
813
+ roomId: args[0],
814
+ uid: args[1],
815
+ };
816
+ }
817
+
818
+ function getCallerUid(payload) {
819
+ return payload && (
820
+ payload.uid ||
821
+ payload.callerUid ||
822
+ (payload.data && payload.data.uid) ||
823
+ (payload.message && payload.message.uid)
824
+ );
825
+ }
826
+
827
+ function getRoomId(payload) {
828
+ return payload && (
829
+ payload.roomId ||
830
+ (payload.data && payload.data.roomId) ||
831
+ (payload.message && payload.message.roomId)
832
+ );
833
+ }
834
+
835
+ module.exports = plugin;
836
+