nodebb-plugin-admin-chats 3.4.3 → 3.5.1
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 +12 -7
- package/lib/access.js +89 -0
- package/lib/history.js +144 -0
- package/lib/hooks.js +310 -0
- package/lib/lock.js +145 -0
- package/lib/overrides.js +186 -0
- package/lib/routes.js +432 -0
- package/library.js +5 -1482
- package/package.json +1 -1
- package/plugin.json +6 -6
- package/static/lib/live-updates.js +42 -0
- package/static/lib/lock-toggle.js +57 -0
- package/static/lib/profile-link.js +98 -0
- package/static/lib/switch-chat.js +234 -0
- package/templates/partials/chats/message-window.tpl +60 -0
- package/templates/partials/chats/message.tpl +78 -0
- package/templates/partials/chats/options.tpl +111 -0
- package/templates/partials/chats/recent_room.tpl +60 -0
- package/.github/workflows/npm-publish.yml +0 -62
- package/.vscode/settings.json +0 -3
- package/client.js +0 -920
package/lib/routes.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const User = nodebb.require('./src/user');
|
|
4
|
+
const Messaging = nodebb.require('./src/messaging');
|
|
5
|
+
const db = nodebb.require('./src/database');
|
|
6
|
+
const meta = nodebb.require('./src/meta');
|
|
7
|
+
const privileges = nodebb.require('./src/privileges');
|
|
8
|
+
const helpers = nodebb.require('./src/controllers/helpers');
|
|
9
|
+
const validator = nodebb.require('validator');
|
|
10
|
+
const sockets = nodebb.require('./src/socket.io');
|
|
11
|
+
|
|
12
|
+
const { canAccessAdminChats, canManageAdminChats } = require('./access');
|
|
13
|
+
const { getMessageEditHistory, renderMessageHistoryHtml } = require('./history');
|
|
14
|
+
const { roomExists, lockRoom, unlockRoom, getRoomLockData } = require('./lock');
|
|
15
|
+
|
|
16
|
+
const ADMIN_CHAT_PAGE_SIZE = 30;
|
|
17
|
+
const ADMIN_CHAT_SCAN_SIZE = 100;
|
|
18
|
+
|
|
19
|
+
async function assertAdminChatsAccess(req, res) {
|
|
20
|
+
if (!req.uid || !await canAccessAdminChats(req.uid)) {
|
|
21
|
+
helpers.notAllowed(req, res);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function renderAdminChatsPage(req, res, next) {
|
|
29
|
+
if (meta.config.disableChat) {
|
|
30
|
+
return next();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!await assertAdminChatsAccess(req, res)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const payload = await buildAdminChatsPayload(req);
|
|
38
|
+
if (req.params.roomId && !payload.roomId) {
|
|
39
|
+
return next();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
res.render('chats', payload);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function buildAdminChatsPayload(req, options = {}) {
|
|
46
|
+
// Like core (controllers/accounts/chats.js): when the client only switches
|
|
47
|
+
// rooms (?switch=1 on the JSON endpoint), skip rebuilding the room list —
|
|
48
|
+
// scanning every chat room on each switch made room-opening slow on mobile
|
|
49
|
+
const isSwitch = !!options.isSwitch;
|
|
50
|
+
const userslug = await User.getUserField(req.uid, 'userslug');
|
|
51
|
+
|
|
52
|
+
const payload = {
|
|
53
|
+
title: '[[pages:chats]]',
|
|
54
|
+
uid: req.uid,
|
|
55
|
+
userslug,
|
|
56
|
+
adminAllChats: true,
|
|
57
|
+
bodyClasses: ['page-user-chats'],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (!isSwitch) {
|
|
61
|
+
const [recentChats, publicRooms, privateRoomCount] = await Promise.all([
|
|
62
|
+
getAdminRecentChats(req.uid, 0, ADMIN_CHAT_PAGE_SIZE),
|
|
63
|
+
Messaging.getPublicRooms(req.uid, req.uid),
|
|
64
|
+
getPrivateRoomCount(),
|
|
65
|
+
]);
|
|
66
|
+
payload.rooms = recentChats.rooms;
|
|
67
|
+
payload.nextStart = recentChats.nextStart;
|
|
68
|
+
payload.publicRooms = publicRooms || [];
|
|
69
|
+
payload.privateRoomCount = privateRoomCount;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const roomId = parseInt(req.params.roomId, 10) || 0;
|
|
73
|
+
if (!roomId) {
|
|
74
|
+
return payload;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const roomPayload = await buildAdminChatRoomPayload(req.uid, roomId, req.params.index);
|
|
78
|
+
if (!roomPayload) {
|
|
79
|
+
return payload;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...payload,
|
|
84
|
+
...roomPayload,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function buildAdminChatRoomPayload(uid, roomId, indexParam) {
|
|
89
|
+
let start = 0;
|
|
90
|
+
let scrollToIndex = null;
|
|
91
|
+
|
|
92
|
+
if (indexParam) {
|
|
93
|
+
const msgCount = await db.getObjectField(`chat:room:${roomId}`, 'messageCount');
|
|
94
|
+
start = Math.max(0, parseInt(msgCount, 10) - parseInt(indexParam, 10) - 49);
|
|
95
|
+
scrollToIndex = Math.min(msgCount, Math.max(0, parseInt(indexParam, 10) || 1));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const room = await Messaging.loadRoom(uid, { uid, roomId, start });
|
|
99
|
+
if (!room) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Root fix for "message stays unread": core only clears unread state from the
|
|
104
|
+
// client when the room's nav element carries the `unread` class, which the
|
|
105
|
+
// admin list never sets. Mark the room read for the viewer on every load.
|
|
106
|
+
try {
|
|
107
|
+
await Messaging.markRead(uid, roomId);
|
|
108
|
+
await Messaging.pushUnreadCount(uid);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// never let read-state bookkeeping break room rendering
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const [canViewInfo, canUploadImage, canUploadFile] = await privileges.global.can([
|
|
114
|
+
'view:users:info', 'upload:post:image', 'upload:post:file',
|
|
115
|
+
], uid);
|
|
116
|
+
|
|
117
|
+
room.title = room.roomName || room.usernames || '[[pages:chats]]';
|
|
118
|
+
room.bodyClasses = ['page-user-chats', 'chat-loaded'];
|
|
119
|
+
room.canViewInfo = canViewInfo;
|
|
120
|
+
room.canUpload = (canUploadImage || canUploadFile) && (meta.config.maximumFileSize > 0 || room.isAdmin);
|
|
121
|
+
room.scrollToIndex = scrollToIndex;
|
|
122
|
+
|
|
123
|
+
return room;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function getPrivateRoomCount() {
|
|
127
|
+
const [totalCount, publicCount] = await Promise.all([
|
|
128
|
+
db.sortedSetCard('chat:rooms'),
|
|
129
|
+
db.sortedSetCard('chat:rooms:public'),
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
return Math.max(0, (parseInt(totalCount, 10) || 0) - (parseInt(publicCount, 10) || 0));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function getAdminRecentChats(uid, start, limit) {
|
|
136
|
+
let cursor = Math.max(0, parseInt(start, 10) || 0);
|
|
137
|
+
const roomPairs = [];
|
|
138
|
+
|
|
139
|
+
while (roomPairs.length < limit) {
|
|
140
|
+
const roomIds = await db.getSortedSetRevRange('chat:rooms', cursor, cursor + ADMIN_CHAT_SCAN_SIZE - 1);
|
|
141
|
+
if (!roomIds.length) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const rooms = await Messaging.getRoomsData(roomIds);
|
|
146
|
+
rooms.forEach((room, index) => {
|
|
147
|
+
if (room && !room.public && roomPairs.length < limit) {
|
|
148
|
+
roomPairs.push({ roomId: roomIds[index], room });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
cursor += roomIds.length;
|
|
153
|
+
if (roomIds.length < ADMIN_CHAT_SCAN_SIZE) {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const rooms = roomPairs.map(item => item.room);
|
|
159
|
+
const roomIds = roomPairs.map(item => item.roomId);
|
|
160
|
+
|
|
161
|
+
await enrichAdminRecentRooms(uid, roomIds, rooms);
|
|
162
|
+
|
|
163
|
+
return { rooms, nextStart: cursor };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function enrichAdminRecentRooms(uid, roomIds, rooms) {
|
|
167
|
+
if (!roomIds.length) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const roomUsers = await Promise.all(roomIds.map(roomId => Messaging.getUidsInRoom(roomId, 0, -1)));
|
|
172
|
+
const uniqueUids = [...new Set(roomUsers.flat().filter(Boolean))];
|
|
173
|
+
const userMap = new Map();
|
|
174
|
+
|
|
175
|
+
if (uniqueUids.length) {
|
|
176
|
+
const users = await User.getUsersFields(uniqueUids, [
|
|
177
|
+
'uid', 'username', 'userslug', 'displayname', 'picture', 'status', 'lastonline',
|
|
178
|
+
]);
|
|
179
|
+
uniqueUids.forEach((memberUid, index) => {
|
|
180
|
+
const userData = users[index];
|
|
181
|
+
if (userData) {
|
|
182
|
+
userData.status = User.getStatus(userData);
|
|
183
|
+
userMap.set(String(memberUid), userData);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const teasers = await Promise.all(roomIds.map(roomId => getAdminRoomTeaser(roomId)));
|
|
189
|
+
|
|
190
|
+
rooms.forEach((room, index) => {
|
|
191
|
+
if (!room) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
room.users = (roomUsers[index] || [])
|
|
196
|
+
.map(memberUid => userMap.get(String(memberUid)))
|
|
197
|
+
.filter(Boolean);
|
|
198
|
+
room.groupChat = room.userCount > 2;
|
|
199
|
+
room.unread = false;
|
|
200
|
+
room.teaser = teasers[index];
|
|
201
|
+
room.lastUser = room.users[0];
|
|
202
|
+
room.usernames = Messaging.generateUsernames(room, uid);
|
|
203
|
+
room.participantsLabel = buildParticipantsLabel(room.users);
|
|
204
|
+
room.icon = Messaging.getRoomIcon(room);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildParticipantsLabel(users) {
|
|
209
|
+
const list = Array.isArray(users) ? users.filter(Boolean) : [];
|
|
210
|
+
if (!list.length) {
|
|
211
|
+
return '';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const names = list.map(user => user.displayname || user.username).filter(Boolean);
|
|
215
|
+
if (names.length <= 5) {
|
|
216
|
+
return names.join(', ');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return `${names.slice(0, 5).join(', ')} +${names.length - 5}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function getAdminRoomTeaser(roomId) {
|
|
223
|
+
const mids = await db.getSortedSetRevRange(`chat:room:${roomId}:mids`, 0, 19);
|
|
224
|
+
if (!mids.length) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const teaser = (await Messaging.getMessagesFields(mids, ['fromuid', 'content', 'timestamp', 'deleted', 'system']))
|
|
229
|
+
.find(message => message && !message.deleted && !message.system && message.fromuid);
|
|
230
|
+
|
|
231
|
+
if (!teaser) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const teaserUser = await User.getUserFields(teaser.fromuid, [
|
|
236
|
+
'uid', 'username', 'userslug', 'displayname', 'picture', 'status', 'lastonline',
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
if (teaserUser) {
|
|
240
|
+
teaser.user = teaserUser;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
teaser.content = String(teaser.content || '').replace(/<[^>]*>/g, '').trim();
|
|
244
|
+
teaser.roomId = roomId;
|
|
245
|
+
|
|
246
|
+
return teaser;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function registerRoutes(app, router, middleware) {
|
|
250
|
+
if (!router) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const routeMiddleware = [];
|
|
255
|
+
if (middleware && typeof middleware.ensureLoggedIn === 'function') {
|
|
256
|
+
routeMiddleware.push(middleware.ensureLoggedIn);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const pageMiddlewares = [
|
|
260
|
+
middleware.autoLocale,
|
|
261
|
+
middleware.applyBlacklist,
|
|
262
|
+
middleware.authenticateRequest,
|
|
263
|
+
middleware.redirectToHomeIfBanned,
|
|
264
|
+
middleware.maintenanceMode,
|
|
265
|
+
middleware.registrationComplete,
|
|
266
|
+
middleware.pluginHooks,
|
|
267
|
+
...routeMiddleware,
|
|
268
|
+
middleware.pageView,
|
|
269
|
+
].filter(Boolean);
|
|
270
|
+
|
|
271
|
+
const pageController = async (req, res, next) => {
|
|
272
|
+
try {
|
|
273
|
+
// Users without admin-chats access get core's behaviour: redirect to
|
|
274
|
+
// their own chats page (helpers.redirect also handles API/ajaxify
|
|
275
|
+
// requests via X-Redirect, so navigation works in one hop)
|
|
276
|
+
if (req.uid && !await canAccessAdminChats(req.uid)) {
|
|
277
|
+
const userSlug = await User.getUserField(req.uid, 'userslug');
|
|
278
|
+
if (userSlug) {
|
|
279
|
+
const suffix = req.params.roomId ? `/${req.params.roomId}${req.params.index ? `/${req.params.index}` : ''}` : '';
|
|
280
|
+
return helpers.redirect(res, `/user/${userSlug}/chats${suffix}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await renderAdminChatsPage(req, res, next);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
next(err);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const pageRouter = app || router;
|
|
291
|
+
pageRouter.get('/chats/:roomId?/:index?', middleware.busyCheck, pageMiddlewares, middleware.buildHeader, pageController);
|
|
292
|
+
// Also own the page-API route. Without it, ajaxify entry into /chats/<roomId>
|
|
293
|
+
// (e.g. picking a room from the mobile chat dropdown) hit core's redirect
|
|
294
|
+
// route and bounced through an extra hop before the room finally rendered —
|
|
295
|
+
// showing the chat list in between. Registered on the core router before
|
|
296
|
+
// core's own routes, and covered by middleware.prepareAPI (mounted earlier),
|
|
297
|
+
// so res.render answers with ajaxify JSON in a single round-trip.
|
|
298
|
+
router.get('/api/chats/:roomId?/:index?', pageMiddlewares, pageController);
|
|
299
|
+
|
|
300
|
+
router.get('/api/admin-chats', ...routeMiddleware, async (req, res) => {
|
|
301
|
+
try {
|
|
302
|
+
if (!await assertAdminChatsAccess(req, res)) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const start = Math.max(0, parseInt(req.query.start, 10) || 0);
|
|
307
|
+
const data = await getAdminRecentChats(req.uid, start, ADMIN_CHAT_PAGE_SIZE);
|
|
308
|
+
res.json(data);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
res.status(500).json({ status: { code: 'error', message: err.message } });
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
router.get('/api/admin-chats/page/:roomId?/:index?', ...routeMiddleware, async (req, res) => {
|
|
315
|
+
try {
|
|
316
|
+
if (!await assertAdminChatsAccess(req, res)) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const payload = await buildAdminChatsPayload(req, { isSwitch: parseInt(req.query.switch, 10) === 1 });
|
|
321
|
+
res.json(payload);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
res.status(500).json({ status: { code: 'error', message: err.message } });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
router.post('/api/admin-chats/:roomId/lock', ...routeMiddleware, async (req, res) => {
|
|
328
|
+
try {
|
|
329
|
+
const actingUid = req.uid;
|
|
330
|
+
const roomId = parseInt(req.params.roomId, 10);
|
|
331
|
+
|
|
332
|
+
if (!actingUid || Number.isNaN(roomId)) {
|
|
333
|
+
return res.status(400).json({ status: { code: 'bad-request', message: 'Invalid room id' } });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!await User.isAdministrator(actingUid) && !await canManageAdminChats(actingUid)) {
|
|
337
|
+
return res.status(403).json({ status: { code: 'forbidden', message: 'Admin or manage privilege required' } });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!await roomExists(roomId)) {
|
|
341
|
+
return res.status(404).json({ status: { code: 'not-found', message: 'Room not found' } });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const shouldLock = req.body && typeof req.body.locked === 'boolean' ? req.body.locked : true;
|
|
345
|
+
const lockData = shouldLock ? await lockRoom(roomId, actingUid) : await unlockRoom(roomId, actingUid);
|
|
346
|
+
|
|
347
|
+
// Push the new lock state to anyone with that room open right now —
|
|
348
|
+
// same broadcast pattern core uses for messages (src/messaging/notifications.js),
|
|
349
|
+
// so an open window updates live instead of relying on client-side polling.
|
|
350
|
+
sockets.in(`chat_room_${roomId}`).emit('event:admin-chats.roomLockChanged', { roomId, lockData });
|
|
351
|
+
|
|
352
|
+
return res.json({ status: { code: 'ok', message: 'Room updated' }, roomId, lockData });
|
|
353
|
+
} catch (err) {
|
|
354
|
+
return res.status(500).json({ status: { code: 'error', message: err.message } });
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
router.get('/api/admin-chats/:roomId/lock', ...routeMiddleware, async (req, res) => {
|
|
359
|
+
try {
|
|
360
|
+
const roomId = parseInt(req.params.roomId, 10);
|
|
361
|
+
|
|
362
|
+
if (Number.isNaN(roomId)) {
|
|
363
|
+
return res.status(400).json({ status: { code: 'bad-request', message: 'Invalid room id' } });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!await roomExists(roomId)) {
|
|
367
|
+
return res.status(404).json({ status: { code: 'not-found', message: 'Room not found' } });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const lockData = await getRoomLockData(roomId);
|
|
371
|
+
return res.json({ status: { code: 'ok', message: 'Lock data retrieved' }, roomId, lockData });
|
|
372
|
+
} catch (err) {
|
|
373
|
+
return res.status(500).json({ status: { code: 'error', message: err.message } });
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// --- Hidden chat edit-history viewer ---------------------------------
|
|
378
|
+
// These two routes are intentionally NOT referenced from any template,
|
|
379
|
+
// client script, menu or API payload. They do not appear in the admin
|
|
380
|
+
// chats UI, so a "regular" administrator who has not read this source
|
|
381
|
+
// will never encounter them. They are still protected by
|
|
382
|
+
// assertAdminChatsAccess(), so only privileged accounts can read data
|
|
383
|
+
// even if the URL is discovered. Paths:
|
|
384
|
+
// GET /api/admin-chats/history/:mid -> JSON
|
|
385
|
+
// GET /admin-chats/history/:mid -> standalone HTML page
|
|
386
|
+
router.get('/api/admin-chats/history/:mid', ...routeMiddleware, async (req, res) => {
|
|
387
|
+
try {
|
|
388
|
+
if (!await assertAdminChatsAccess(req, res)) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const mid = parseInt(req.params.mid, 10);
|
|
393
|
+
if (Number.isNaN(mid)) {
|
|
394
|
+
return res.status(400).json({ status: { code: 'bad-request', message: 'Invalid message id' } });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const history = await getMessageEditHistory(mid);
|
|
398
|
+
return res.json({ status: { code: 'ok', message: 'Edit history retrieved' }, ...history });
|
|
399
|
+
} catch (err) {
|
|
400
|
+
return res.status(500).json({ status: { code: 'error', message: err.message } });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
router.get('/admin-chats/history/:mid', ...routeMiddleware, async (req, res) => {
|
|
405
|
+
try {
|
|
406
|
+
if (!await assertAdminChatsAccess(req, res)) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const mid = parseInt(req.params.mid, 10);
|
|
411
|
+
if (Number.isNaN(mid)) {
|
|
412
|
+
return res.status(400).type('html').send('<h1>400</h1><p>Invalid message id</p>');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const history = await getMessageEditHistory(mid);
|
|
416
|
+
return res.type('html').send(renderMessageHistoryHtml(history));
|
|
417
|
+
} catch (err) {
|
|
418
|
+
return res.status(500).type('html').send(`<h1>500</h1><pre>${validator.escape(String(err.message))}</pre>`);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
module.exports = {
|
|
424
|
+
ADMIN_CHAT_PAGE_SIZE,
|
|
425
|
+
assertAdminChatsAccess,
|
|
426
|
+
renderAdminChatsPage,
|
|
427
|
+
buildAdminChatsPayload,
|
|
428
|
+
buildAdminChatRoomPayload,
|
|
429
|
+
getAdminRecentChats,
|
|
430
|
+
buildParticipantsLabel,
|
|
431
|
+
registerRoutes,
|
|
432
|
+
};
|