nodebb-plugin-admin-chats 3.8.2 → 3.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/overrides.js +41 -0
- package/lib/routes.js +63 -0
- package/library.js +2 -1
- package/package.json +1 -1
- package/plugin.json +2 -1
- package/static/lib/chat-search.js +80 -0
package/lib/overrides.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
const { isRoomLocked } = require('./lock');
|
|
17
17
|
const { editHistoryKey } = require('./history');
|
|
18
18
|
const privileges = nodebb.require('./src/privileges');
|
|
19
|
+
const helpers = nodebb.require('./src/controllers/helpers');
|
|
19
20
|
|
|
20
21
|
// NodeBB translates `[[namespace:key]]` error messages itself (both in
|
|
21
22
|
// app.alertError on the client and anywhere else the message surfaces) —
|
|
@@ -46,6 +47,45 @@ function overrideCoreChatRedirect(controllers, renderAdminChatsPage) {
|
|
|
46
47
|
chatsController.redirectToChat = wrappedRedirectToChat;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
// /user/<slug>/chats/<roomId> renders the room as seen by <slug>: core loads
|
|
51
|
+
// its messages from that user's join timestamp, so for a private room the
|
|
52
|
+
// user is not in it comes out empty (title only). Send the viewer to a URL
|
|
53
|
+
// that can actually show the room — their own chats page if they are in it,
|
|
54
|
+
// otherwise the all-chats page.
|
|
55
|
+
function overrideCoreChatsPage(controllers) {
|
|
56
|
+
const chatsController = controllers && controllers.accounts && controllers.accounts.chats;
|
|
57
|
+
if (!chatsController || typeof chatsController.get !== 'function' || chatsController.get._adminChatsWrapped) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const originalGet = chatsController.get;
|
|
62
|
+
const wrappedGet = async function (req, res, next) {
|
|
63
|
+
const roomId = parseInt(req.params.roomid, 10);
|
|
64
|
+
const isSwitch = res.locals.isAPI && parseInt(req.query.switch, 10) === 1;
|
|
65
|
+
if (roomId && !isSwitch && req.uid && await canAccessAdminChats(req.uid)) {
|
|
66
|
+
const uid = await User.getUidByUserslug(req.params.userslug);
|
|
67
|
+
if (uid && parseInt(uid, 10) !== parseInt(req.uid, 10)) {
|
|
68
|
+
const [room, isOwnerInRoom, isCallerInRoom] = await Promise.all([
|
|
69
|
+
Messaging.getRoomData(roomId),
|
|
70
|
+
isUserMemberOfRoom(uid, roomId),
|
|
71
|
+
isUserMemberOfRoom(req.uid, roomId),
|
|
72
|
+
]);
|
|
73
|
+
if (room && !room.public && !isOwnerInRoom) {
|
|
74
|
+
const index = parseInt(req.params.index, 10);
|
|
75
|
+
const suffix = `${roomId}${index ? `/${index}` : ''}`;
|
|
76
|
+
const callerSlug = isCallerInRoom && await User.getUserField(req.uid, 'userslug');
|
|
77
|
+
return helpers.redirect(res, callerSlug ? `/user/${callerSlug}/chats/${suffix}` : `/chats/${suffix}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return await originalGet.call(this, req, res, next);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
wrappedGet._adminChatsWrapped = true;
|
|
86
|
+
chatsController.get = wrappedGet;
|
|
87
|
+
}
|
|
88
|
+
|
|
49
89
|
function overrideChatsApi() {
|
|
50
90
|
if (!ChatsAPI || typeof ChatsAPI.kick !== 'function' || ChatsAPI.kick._adminChatLockWrapped) {
|
|
51
91
|
return;
|
|
@@ -181,6 +221,7 @@ function overrideMessagingFunctions() {
|
|
|
181
221
|
module.exports = {
|
|
182
222
|
getMessageRoomId,
|
|
183
223
|
overrideCoreChatRedirect,
|
|
224
|
+
overrideCoreChatsPage,
|
|
184
225
|
overrideChatsApi,
|
|
185
226
|
overrideMessagingFunctions,
|
|
186
227
|
};
|
package/lib/routes.js
CHANGED
|
@@ -14,6 +14,7 @@ const { roomExists, lockRoom, unlockRoom, getRoomLockData } = require('./lock');
|
|
|
14
14
|
const { ACTIVITY_SET, pruneActivityEntries } = require('./activity');
|
|
15
15
|
|
|
16
16
|
const ADMIN_CHAT_PAGE_SIZE = 30;
|
|
17
|
+
const ADMIN_CHAT_SEARCH_LIMIT = 100;
|
|
17
18
|
|
|
18
19
|
async function assertAdminChatsAccess(req, res) {
|
|
19
20
|
if (!req.uid || !await canAccessAdminChats(req.uid)) {
|
|
@@ -202,6 +203,41 @@ async function enrichAdminRecentRooms(uid, roomIds, rooms) {
|
|
|
202
203
|
});
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
async function searchAdminChats(uid, query) {
|
|
207
|
+
const lowerQuery = String(query).toLowerCase();
|
|
208
|
+
const allRoomIds = await db.getSortedSetRevRange(ACTIVITY_SET, 0, -1);
|
|
209
|
+
const [allRooms, roomUids] = await Promise.all([
|
|
210
|
+
Messaging.getRoomsData(allRoomIds, ['roomName', 'public']),
|
|
211
|
+
db.getSortedSetsMembers(allRoomIds.map(roomId => `chat:room:${roomId}:uids`)),
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const uniqueUids = [...new Set(roomUids.flat().filter(Boolean))];
|
|
215
|
+
const users = await User.getUsersFields(uniqueUids, ['username', 'displayname']);
|
|
216
|
+
const matchingUids = new Set();
|
|
217
|
+
users.forEach((user, index) => {
|
|
218
|
+
if (user && (
|
|
219
|
+
(user.displayname && user.displayname.toLowerCase().includes(lowerQuery)) ||
|
|
220
|
+
(user.username && user.username.toLowerCase().includes(lowerQuery))
|
|
221
|
+
)) {
|
|
222
|
+
matchingUids.add(String(uniqueUids[index]));
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const roomIds = allRoomIds.filter((roomId, index) => {
|
|
227
|
+
const room = allRooms[index];
|
|
228
|
+
if (!room || room.public) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return (room.roomName && room.roomName.toLowerCase().includes(lowerQuery)) ||
|
|
232
|
+
(roomUids[index] || []).some(memberUid => matchingUids.has(String(memberUid)));
|
|
233
|
+
}).slice(0, ADMIN_CHAT_SEARCH_LIMIT);
|
|
234
|
+
|
|
235
|
+
const rooms = await Messaging.getRoomsData(roomIds);
|
|
236
|
+
await enrichAdminRecentRooms(uid, roomIds, rooms);
|
|
237
|
+
|
|
238
|
+
return { rooms: rooms.filter(Boolean) };
|
|
239
|
+
}
|
|
240
|
+
|
|
205
241
|
function buildParticipantsLabel(users) {
|
|
206
242
|
const list = Array.isArray(users) ? users.filter(Boolean) : [];
|
|
207
243
|
if (!list.length) {
|
|
@@ -308,6 +344,33 @@ function registerRoutes(app, router, middleware) {
|
|
|
308
344
|
}
|
|
309
345
|
});
|
|
310
346
|
|
|
347
|
+
// Core's chat list search (GET /api/v3/chats/search) always searches the
|
|
348
|
+
// caller's own rooms, so on the all-chats page and on another user's chats
|
|
349
|
+
// page it would list the viewer's chats instead of the ones being viewed.
|
|
350
|
+
router.get('/api/admin-chats/search', ...routeMiddleware, async (req, res) => {
|
|
351
|
+
try {
|
|
352
|
+
if (!await assertAdminChatsAccess(req, res)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const query = String(req.query.query || '').trim();
|
|
357
|
+
const uid = parseInt(req.query.uid, 10) || 0;
|
|
358
|
+
let data;
|
|
359
|
+
if (uid) {
|
|
360
|
+
data = query ?
|
|
361
|
+
await Messaging.searchRecentChats(req.uid, uid, query) :
|
|
362
|
+
await Messaging.getRecentChats(req.uid, uid, 0, ADMIN_CHAT_PAGE_SIZE - 1);
|
|
363
|
+
} else {
|
|
364
|
+
data = query ?
|
|
365
|
+
await searchAdminChats(req.uid, query) :
|
|
366
|
+
await getAdminRecentChats(req.uid, 0, ADMIN_CHAT_PAGE_SIZE);
|
|
367
|
+
}
|
|
368
|
+
res.json(data);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
res.status(500).json({ status: { code: 'error', message: err.message } });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
311
374
|
router.get('/api/admin-chats/page/:roomId?/:index?', ...routeMiddleware, async (req, res) => {
|
|
312
375
|
try {
|
|
313
376
|
if (!await assertAdminChatsAccess(req, res)) {
|
package/library.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const hooks = require('./lib/hooks');
|
|
4
4
|
const { registerRoutes, renderAdminChatsPage } = require('./lib/routes');
|
|
5
|
-
const { overrideMessagingFunctions, overrideChatsApi, overrideCoreChatRedirect } = require('./lib/overrides');
|
|
5
|
+
const { overrideMessagingFunctions, overrideChatsApi, overrideCoreChatRedirect, overrideCoreChatsPage } = require('./lib/overrides');
|
|
6
6
|
const { overrideForActivityTracking, backfillActivitySet } = require('./lib/activity');
|
|
7
7
|
|
|
8
8
|
const plugin = Object.assign({}, hooks);
|
|
@@ -12,6 +12,7 @@ plugin.init = async function (params) {
|
|
|
12
12
|
overrideMessagingFunctions();
|
|
13
13
|
overrideChatsApi();
|
|
14
14
|
overrideCoreChatRedirect(params.controllers, renderAdminChatsPage);
|
|
15
|
+
overrideCoreChatsPage(params.controllers);
|
|
15
16
|
overrideForActivityTracking();
|
|
16
17
|
await backfillActivitySet();
|
|
17
18
|
};
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Core's chat list search (public/src/client/chats/search.js) always searches
|
|
4
|
+
// the caller's own rooms. On the admin all-chats page and on another user's
|
|
5
|
+
// chats page that lists the viewer's chats instead of the ones on screen, and
|
|
6
|
+
// opening one of them lands on /user/<other>/chats/<roomId> for a room that
|
|
7
|
+
// user is not in. Core binds its listener with no handle to remove it, so the
|
|
8
|
+
// input is swapped for a clone and searched through the plugin's endpoint.
|
|
9
|
+
$(document).ready(function () {
|
|
10
|
+
function getSearchUid() {
|
|
11
|
+
const data = ajaxify && ajaxify.data;
|
|
12
|
+
if (!data) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (data.adminAllChats) {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
const uid = parseInt(data.uid, 10) || 0;
|
|
19
|
+
if (!uid || uid === parseInt(app.user.uid, 10)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return uid;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function search(query, uid) {
|
|
26
|
+
const params = new URLSearchParams({ query });
|
|
27
|
+
if (uid) {
|
|
28
|
+
params.set('uid', uid);
|
|
29
|
+
}
|
|
30
|
+
const response = await fetch(`${config.relative_path || ''}/api/admin-chats/search?${params.toString()}`, {
|
|
31
|
+
credentials: 'same-origin',
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Received ${response.status}`);
|
|
35
|
+
}
|
|
36
|
+
return await response.json();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function takeOverSearch() {
|
|
40
|
+
const uid = getSearchUid();
|
|
41
|
+
const searchEl = document.getElementById('search-chats');
|
|
42
|
+
const listEl = document.getElementById('private-rooms');
|
|
43
|
+
if (uid === null || !searchEl || !listEl || searchEl.hasAttribute('data-admin-chats-search')) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const replacement = searchEl.cloneNode(true);
|
|
48
|
+
replacement.value = searchEl.value;
|
|
49
|
+
replacement.setAttribute('data-admin-chats-search', '1');
|
|
50
|
+
searchEl.replaceWith(replacement);
|
|
51
|
+
|
|
52
|
+
let lastQuery = replacement.value.trim();
|
|
53
|
+
replacement.addEventListener('keyup', utils.debounce(async function () {
|
|
54
|
+
const query = replacement.value.trim();
|
|
55
|
+
if (query === lastQuery) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
lastQuery = query;
|
|
59
|
+
try {
|
|
60
|
+
const data = await search(query, uid);
|
|
61
|
+
if (query !== lastQuery) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const html = await app.parseAndTranslate('chats', 'rooms', data);
|
|
65
|
+
$(listEl).html(html);
|
|
66
|
+
html.find('.timeago').timeago();
|
|
67
|
+
listEl.setAttribute('data-nextstart', data.nextStart || 0);
|
|
68
|
+
listEl.setAttribute('data-prevstart', 0);
|
|
69
|
+
// Both infinite-scroll loaders (core's and switch-chat.js) bail out
|
|
70
|
+
// while `loading` is set; keep them from appending the unfiltered
|
|
71
|
+
// list under the search results.
|
|
72
|
+
listEl.toggleAttribute('loading', !!query);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
app.alertError(err);
|
|
75
|
+
}
|
|
76
|
+
}, 300));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
$(window).on('action:chat.loaded', takeOverSearch);
|
|
80
|
+
});
|