nodebb-plugin-admin-chats 3.8.2 → 3.8.4
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 +42 -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/static/lib/switch-chat.js +42 -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,46 @@ 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. This includes the viewer's own slug: an admin
|
|
55
|
+
// reading a room they are not in must stay on the all-chats page.
|
|
56
|
+
function overrideCoreChatsPage(controllers) {
|
|
57
|
+
const chatsController = controllers && controllers.accounts && controllers.accounts.chats;
|
|
58
|
+
if (!chatsController || typeof chatsController.get !== 'function' || chatsController.get._adminChatsWrapped) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const originalGet = chatsController.get;
|
|
63
|
+
const wrappedGet = async function (req, res, next) {
|
|
64
|
+
const roomId = parseInt(req.params.roomid, 10);
|
|
65
|
+
const isSwitch = res.locals.isAPI && parseInt(req.query.switch, 10) === 1;
|
|
66
|
+
if (roomId && !isSwitch && req.uid && await canAccessAdminChats(req.uid)) {
|
|
67
|
+
const uid = await User.getUidByUserslug(req.params.userslug);
|
|
68
|
+
if (uid) {
|
|
69
|
+
const [room, isOwnerInRoom, isCallerInRoom] = await Promise.all([
|
|
70
|
+
Messaging.getRoomData(roomId),
|
|
71
|
+
isUserMemberOfRoom(uid, roomId),
|
|
72
|
+
isUserMemberOfRoom(req.uid, roomId),
|
|
73
|
+
]);
|
|
74
|
+
if (room && !room.public && !isOwnerInRoom) {
|
|
75
|
+
const index = parseInt(req.params.index, 10);
|
|
76
|
+
const suffix = `${roomId}${index ? `/${index}` : ''}`;
|
|
77
|
+
const callerSlug = isCallerInRoom && await User.getUserField(req.uid, 'userslug');
|
|
78
|
+
return helpers.redirect(res, callerSlug ? `/user/${callerSlug}/chats/${suffix}` : `/chats/${suffix}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return await originalGet.call(this, req, res, next);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
wrappedGet._adminChatsWrapped = true;
|
|
87
|
+
chatsController.get = wrappedGet;
|
|
88
|
+
}
|
|
89
|
+
|
|
49
90
|
function overrideChatsApi() {
|
|
50
91
|
if (!ChatsAPI || typeof ChatsAPI.kick !== 'function' || ChatsAPI.kick._adminChatLockWrapped) {
|
|
51
92
|
return;
|
|
@@ -181,6 +222,7 @@ function overrideMessagingFunctions() {
|
|
|
181
222
|
module.exports = {
|
|
182
223
|
getMessageRoomId,
|
|
183
224
|
overrideCoreChatRedirect,
|
|
225
|
+
overrideCoreChatsPage,
|
|
184
226
|
overrideChatsApi,
|
|
185
227
|
overrideMessagingFunctions,
|
|
186
228
|
};
|
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
|
+
});
|
|
@@ -215,6 +215,48 @@ $(document).ready(function () {
|
|
|
215
215
|
});
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// Core's chat scroll handler (updateUrlWithMessageIndex) always rewrites the
|
|
219
|
+
// address to user/<viewer-slug>/chats/<roomId>/<index>. On the all-chats page
|
|
220
|
+
// the viewer is usually not in the room, and that URL renders the room from
|
|
221
|
+
// the viewer's (missing) join timestamp — so a reload/back landed on a room
|
|
222
|
+
// that could not scroll past the first batch. Keep the admin URL scheme.
|
|
223
|
+
function patchReplaceStateForAdminAll() {
|
|
224
|
+
if (!history.replaceState || history.replaceState._adminAllChatsPatched) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const originalReplaceState = history.replaceState;
|
|
229
|
+
const patchedReplaceState = function (state, title, url) {
|
|
230
|
+
const userslug = ajaxify && ajaxify.data && ajaxify.data.userslug;
|
|
231
|
+
if (isAdminAllChatsPage() && userslug && typeof url === 'string') {
|
|
232
|
+
const prefix = `${config.relative_path || ''}/user/${userslug}/chats/`;
|
|
233
|
+
const parsed = new URL(url, window.location.href);
|
|
234
|
+
// Non-ASCII slugs come back percent-encoded in URL#pathname
|
|
235
|
+
let pathname = parsed.pathname;
|
|
236
|
+
try {
|
|
237
|
+
pathname = decodeURIComponent(pathname);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
// keep the raw pathname
|
|
240
|
+
}
|
|
241
|
+
const match = pathname.startsWith(prefix) &&
|
|
242
|
+
pathname.slice(prefix.length).match(/^(\d+(?:\/\d+)?)$/);
|
|
243
|
+
if (match) {
|
|
244
|
+
const newUrl = getAdminChatsPageUrl(match[1]);
|
|
245
|
+
parsed.pathname = `${config.relative_path || ''}/${newUrl}`;
|
|
246
|
+
url = parsed.href;
|
|
247
|
+
if (state && typeof state.url === 'string') {
|
|
248
|
+
state = { ...state, url: newUrl + parsed.search };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return originalReplaceState.call(this, state, title, url);
|
|
253
|
+
};
|
|
254
|
+
patchedReplaceState._adminAllChatsPatched = true;
|
|
255
|
+
history.replaceState = patchedReplaceState;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
patchReplaceStateForAdminAll();
|
|
259
|
+
|
|
218
260
|
$(window).on('action:ajaxify.end', function (ev, data) {
|
|
219
261
|
if (isAdminAllChatsPage()) {
|
|
220
262
|
patchForumChatsForAdminAll();
|