nodebb-plugin-admin-chats 3.5.0 → 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 -1501
- 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 -917
package/README.md
CHANGED
|
@@ -16,7 +16,13 @@ A professional administration plugin for NodeBB that allows administrators to mo
|
|
|
16
16
|
### Plugin Information
|
|
17
17
|
* **Name**: Super Admin Chat Control
|
|
18
18
|
* **ID**: `nodebb-plugin-admin-chats`
|
|
19
|
-
* **Compatibility**: NodeBB version `^4.0.0`
|
|
19
|
+
* **Compatibility**: NodeBB version `^4.0.0` (tested through 4.14)
|
|
20
|
+
|
|
21
|
+
### Code Layout
|
|
22
|
+
* `library.js` — thin entry point; wires together the modules below.
|
|
23
|
+
* `lib/access.js`, `lib/lock.js`, `lib/history.js`, `lib/hooks.js`, `lib/routes.js`, `lib/overrides.js` — server-side logic, split by concern.
|
|
24
|
+
* `templates/partials/chats/*.tpl` — overrides of core's chat partials (controls menu, message actions, empty state, recent-room list) so the lock UI, participants line and admin menu item render server-side instead of being built with client-side DOM manipulation.
|
|
25
|
+
* `static/lib/switch-chat.js`, `static/lib/lock-toggle.js`, `static/lib/live-updates.js` — client-side: admin room routing, the lock/unlock click handler, and live updates pushed over a targeted socket event when a room's lock state changes.
|
|
20
26
|
|
|
21
27
|
### Permissions
|
|
22
28
|
The plugin exposes two global permissions in the ACP:
|
|
@@ -63,12 +69,11 @@ The plugin overrides core messaging functions to ensure full administrative cont
|
|
|
63
69
|
3. Restart NodeBB.
|
|
64
70
|
|
|
65
71
|
## Client-Side Support
|
|
66
|
-
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* Cleans up the chat UI empty states for a better admin experience.
|
|
72
|
+
Lock state, the admin menu item, the empty-state copy and the participants line are all rendered server-side via the template overrides in `templates/partials/chats/`, using NodeBB's own translation system (`[[admin-chats:...]]` / `tx()`) — no client-side DOM injection or language sniffing. The `static/lib/` scripts handle the parts that have no server-side extension point:
|
|
73
|
+
* Routing chat-room switches through the admin API when viewing the all-chats admin page (`static/lib/switch-chat.js`).
|
|
74
|
+
* Wiring the lock/unlock menu item's click handler (`static/lib/lock-toggle.js`).
|
|
75
|
+
* Pushing live updates to anyone with an affected room open, via a `event:admin-chats.roomLockChanged` socket broadcast, instead of polling (`static/lib/live-updates.js`).
|
|
76
|
+
* Injecting the "View Chats" link into the profile action menu (`static/lib/profile-link.js`) — this one *is* DOM injection, and deliberately so: the account menu is a theme template (e.g. nodebb-theme-harmony's `partials/account/admin-menu.tpl`) with a hardcoded item list and no hook over it, unlike the chat templates above which core lets a plugin override by file path.
|
|
72
77
|
|
|
73
78
|
---
|
|
74
79
|
*Developed by [palmoni5](https://github.com/palmoni5/nodebb-plugin-admin-chats).*
|
package/lib/access.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const User = nodebb.require('./src/user');
|
|
4
|
+
const privileges = nodebb.require('./src/privileges');
|
|
5
|
+
const db = nodebb.require('./src/database');
|
|
6
|
+
|
|
7
|
+
const ADMIN_CHATS_PRIVILEGE = 'admin-chats:view';
|
|
8
|
+
const ADMIN_CHATS_MANAGE_PRIVILEGE = 'admin-chats:manage';
|
|
9
|
+
|
|
10
|
+
async function canAccessAdminChats(uid) {
|
|
11
|
+
if (!uid) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const [isAdmin, canView, canManage] = await Promise.all([
|
|
16
|
+
User.isAdministrator(uid),
|
|
17
|
+
privileges.global.can(ADMIN_CHATS_PRIVILEGE, uid),
|
|
18
|
+
privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, uid),
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
return isAdmin || canView || canManage;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function canManageAdminChats(uid) {
|
|
25
|
+
if (!uid) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const [isAdmin, canManage] = await Promise.all([
|
|
30
|
+
User.isAdministrator(uid),
|
|
31
|
+
privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, uid),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
return isAdmin || canManage;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function isUserMemberOfRoom(uid, roomId) {
|
|
38
|
+
if (!uid || !roomId) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return await db.isSortedSetMember(`chat:room:${roomId}:uids`, uid);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getCallerUid(payload) {
|
|
46
|
+
return payload && (
|
|
47
|
+
payload.uid ||
|
|
48
|
+
payload.callerUid ||
|
|
49
|
+
(payload.data && payload.data.uid) ||
|
|
50
|
+
(payload.message && payload.message.uid)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getRoomId(payload) {
|
|
55
|
+
return payload && (
|
|
56
|
+
payload.roomId ||
|
|
57
|
+
(payload.data && payload.data.roomId) ||
|
|
58
|
+
(payload.message && payload.message.roomId)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function guessRoomIdUidFromArgs(args) {
|
|
63
|
+
if (!Array.isArray(args) || !args.length) {
|
|
64
|
+
return { roomId: null, uid: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (args.length === 1 && args[0] && typeof args[0] === 'object') {
|
|
68
|
+
return {
|
|
69
|
+
roomId: getRoomId(args[0]),
|
|
70
|
+
uid: getCallerUid(args[0]),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
roomId: args[0],
|
|
76
|
+
uid: args[1],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
ADMIN_CHATS_PRIVILEGE,
|
|
82
|
+
ADMIN_CHATS_MANAGE_PRIVILEGE,
|
|
83
|
+
canAccessAdminChats,
|
|
84
|
+
canManageAdminChats,
|
|
85
|
+
isUserMemberOfRoom,
|
|
86
|
+
getCallerUid,
|
|
87
|
+
getRoomId,
|
|
88
|
+
guessRoomIdUidFromArgs,
|
|
89
|
+
};
|
package/lib/history.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const validator = nodebb.require('validator');
|
|
4
|
+
const Messaging = nodebb.require('./src/messaging');
|
|
5
|
+
const User = nodebb.require('./src/user');
|
|
6
|
+
const db = nodebb.require('./src/database');
|
|
7
|
+
|
|
8
|
+
function editHistoryKey(mid) {
|
|
9
|
+
return `message:${mid}:editHistory`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function getMessageEditHistory(mid) {
|
|
13
|
+
const [exists, current, rawHistory] = await Promise.all([
|
|
14
|
+
Messaging.messageExists(mid),
|
|
15
|
+
Messaging.getMessageFields(mid, ['content', 'fromuid', 'timestamp', 'edited', 'roomId', 'deleted']),
|
|
16
|
+
db.getListRange(editHistoryKey(mid), 0, -1),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const revisions = (rawHistory || []).map((item) => {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(item);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}).filter(Boolean);
|
|
26
|
+
|
|
27
|
+
const uidSet = new Set();
|
|
28
|
+
const authorUid = parseInt(current && current.fromuid, 10) || 0;
|
|
29
|
+
if (authorUid) {
|
|
30
|
+
uidSet.add(authorUid);
|
|
31
|
+
}
|
|
32
|
+
revisions.forEach((rev) => {
|
|
33
|
+
if (rev.editedByUid) {
|
|
34
|
+
uidSet.add(parseInt(rev.editedByUid, 10) || 0);
|
|
35
|
+
}
|
|
36
|
+
if (rev.fromuid) {
|
|
37
|
+
uidSet.add(parseInt(rev.fromuid, 10) || 0);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
uidSet.delete(0);
|
|
41
|
+
|
|
42
|
+
const uids = [...uidSet];
|
|
43
|
+
const userMap = new Map();
|
|
44
|
+
if (uids.length) {
|
|
45
|
+
const users = await User.getUsersFields(uids, ['uid', 'username', 'userslug']);
|
|
46
|
+
uids.forEach((uid, index) => {
|
|
47
|
+
if (users[index]) {
|
|
48
|
+
userMap.set(uid, users[index]);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const decorate = (uid) => {
|
|
54
|
+
const numeric = parseInt(uid, 10) || 0;
|
|
55
|
+
const user = userMap.get(numeric);
|
|
56
|
+
return {
|
|
57
|
+
uid: numeric,
|
|
58
|
+
username: user ? user.username : (numeric ? `uid ${numeric}` : '—'),
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
mid,
|
|
64
|
+
exists: !!exists,
|
|
65
|
+
roomId: parseInt(current && current.roomId, 10) || 0,
|
|
66
|
+
author: decorate(authorUid),
|
|
67
|
+
current: {
|
|
68
|
+
content: current ? String(current.content || '') : '',
|
|
69
|
+
edited: parseInt(current && current.edited, 10) || 0,
|
|
70
|
+
timestamp: parseInt(current && current.timestamp, 10) || 0,
|
|
71
|
+
deleted: parseInt(current && current.deleted, 10) === 1,
|
|
72
|
+
},
|
|
73
|
+
revisionCount: revisions.length,
|
|
74
|
+
revisions: revisions.map(rev => ({
|
|
75
|
+
content: String(rev.content || ''),
|
|
76
|
+
editor: decorate(rev.editedByUid),
|
|
77
|
+
replacedAt: parseInt(rev.replacedAt, 10) || 0,
|
|
78
|
+
previousEdited: parseInt(rev.previousEdited, 10) || 0,
|
|
79
|
+
})),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderMessageHistoryHtml(history) {
|
|
84
|
+
const esc = value => validator.escape(String(value == null ? '' : value));
|
|
85
|
+
const fmt = (ts) => {
|
|
86
|
+
const numeric = parseInt(ts, 10) || 0;
|
|
87
|
+
return numeric ? new Date(numeric).toISOString().replace('T', ' ').replace(/\..+$/, ' UTC') : '—';
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (!history.exists) {
|
|
91
|
+
return `<!doctype html><html lang="he" dir="rtl"><meta charset="utf-8"><title>היסטוריית עריכות</title><body style="font-family:system-ui,Arial,sans-serif;padding:2rem;">
|
|
92
|
+
<h1>הודעה ${esc(history.mid)} לא נמצאה</h1></body></html>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const timeline = history.revisions.map((rev, index) => `
|
|
96
|
+
<li class="rev">
|
|
97
|
+
<div class="meta">גרסה ${index + 1} · הוחלפה ב-${esc(fmt(rev.replacedAt))} · עורך: ${esc(rev.editor.username)}</div>
|
|
98
|
+
<pre class="content">${esc(rev.content)}</pre>
|
|
99
|
+
</li>`).join('');
|
|
100
|
+
|
|
101
|
+
const currentBlock = `
|
|
102
|
+
<li class="rev current">
|
|
103
|
+
<div class="meta">גרסה נוכחית${history.current.edited ? ` · נערכה לאחרונה ${esc(fmt(history.current.edited))}` : ''}${history.current.deleted ? ' · מחוקה' : ''}</div>
|
|
104
|
+
<pre class="content">${esc(history.current.content)}</pre>
|
|
105
|
+
</li>`;
|
|
106
|
+
|
|
107
|
+
return `<!doctype html>
|
|
108
|
+
<html lang="he" dir="rtl">
|
|
109
|
+
<head>
|
|
110
|
+
<meta charset="utf-8">
|
|
111
|
+
<meta name="robots" content="noindex,nofollow">
|
|
112
|
+
<title>היסטוריית עריכות · הודעה ${esc(history.mid)}</title>
|
|
113
|
+
<style>
|
|
114
|
+
body { font-family: system-ui, Segoe UI, Arial, sans-serif; margin: 0; padding: 2rem; background: #f5f6f8; color: #1c1e21; }
|
|
115
|
+
h1 { font-size: 1.25rem; margin: 0 0 .25rem; }
|
|
116
|
+
.sub { color: #65676b; font-size: .875rem; margin-bottom: 1.5rem; }
|
|
117
|
+
ul { list-style: none; margin: 0; padding: 0; max-width: 780px; }
|
|
118
|
+
.rev { background: #fff; border: 1px solid #dcdfe3; border-radius: 8px; padding: .75rem 1rem; margin-bottom: 1rem; }
|
|
119
|
+
.rev.current { border-color: #2d6cdf; box-shadow: 0 0 0 1px #2d6cdf33; }
|
|
120
|
+
.meta { font-size: .8rem; color: #65676b; margin-bottom: .5rem; }
|
|
121
|
+
.content { white-space: pre-wrap; word-break: break-word; margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9rem; }
|
|
122
|
+
@media (prefers-color-scheme: dark) {
|
|
123
|
+
body { background: #18191a; color: #e4e6eb; }
|
|
124
|
+
.rev { background: #242526; border-color: #3a3b3c; }
|
|
125
|
+
.meta, .sub { color: #b0b3b8; }
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
128
|
+
</head>
|
|
129
|
+
<body>
|
|
130
|
+
<h1>היסטוריית עריכות · הודעה ${esc(history.mid)}</h1>
|
|
131
|
+
<div class="sub">מחבר: ${esc(history.author.username)} · חדר: ${esc(history.roomId)} · ${esc(history.revisionCount)} עריכות קודמות</div>
|
|
132
|
+
<ul>
|
|
133
|
+
${history.revisionCount ? timeline : '<li class="rev"><div class="meta">אין עריכות קודמות מתועדות</div></li>'}
|
|
134
|
+
${currentBlock}
|
|
135
|
+
</ul>
|
|
136
|
+
</body>
|
|
137
|
+
</html>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
editHistoryKey,
|
|
142
|
+
getMessageEditHistory,
|
|
143
|
+
renderMessageHistoryHtml,
|
|
144
|
+
};
|
package/lib/hooks.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
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 privileges = nodebb.require('./src/privileges');
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
ADMIN_CHATS_PRIVILEGE, ADMIN_CHATS_MANAGE_PRIVILEGE,
|
|
10
|
+
canAccessAdminChats, canManageAdminChats, isUserMemberOfRoom, getCallerUid, getRoomId,
|
|
11
|
+
} = require('./access');
|
|
12
|
+
const {
|
|
13
|
+
isRoomLocked, getRoomLockData, canNonAdminModifyLockedRoom,
|
|
14
|
+
ensureFirstVisibleMessageHeader, buildRoomMessagesWithLockNotice, markMessagesLocked,
|
|
15
|
+
} = require('./lock');
|
|
16
|
+
|
|
17
|
+
const LOCKED_ACTION_ERROR = '[[admin-chats:errors.lockedAction]]';
|
|
18
|
+
|
|
19
|
+
const hooks = {};
|
|
20
|
+
|
|
21
|
+
hooks.registerPrivileges = async function (data) {
|
|
22
|
+
if (!data || !data.privileges || typeof data.privileges.set !== 'function') {
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
data.privileges.set(ADMIN_CHATS_PRIVILEGE, {
|
|
27
|
+
label: '[[admin-chats:admin-chats-view]]',
|
|
28
|
+
type: 'moderation',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
data.privileges.set(ADMIN_CHATS_MANAGE_PRIVILEGE, {
|
|
32
|
+
label: '[[admin-chats:admin-chats-manage]]',
|
|
33
|
+
type: 'moderation',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return data;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
hooks.filterNotificationCreate = async function (data) {
|
|
40
|
+
if (!data || !data.data) {
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const notification = data.data;
|
|
45
|
+
const chatMatch = notification.path && notification.path.match(/^\/chats\/(\d+)(?:\/(\d+))?$/);
|
|
46
|
+
if (chatMatch && notification.uid) {
|
|
47
|
+
const roomId = chatMatch[1];
|
|
48
|
+
const index = chatMatch[2];
|
|
49
|
+
const userSlug = await User.getUserField(notification.uid, 'userslug');
|
|
50
|
+
if (userSlug) {
|
|
51
|
+
notification.path = `/user/${userSlug}/chats/${roomId}${index ? `/${index}` : ''}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return data;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
hooks.filterUserNotificationsGetNotifications = async function (data) {
|
|
59
|
+
if (!data || !data.uid || !Array.isArray(data.notifications) || !data.notifications.length) {
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const userSlug = await User.getUserField(data.uid, 'userslug');
|
|
64
|
+
if (!userSlug) {
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const userPathRegex = /\/user\/[^/]+\/chats\//;
|
|
69
|
+
const chatPathRegex = /\/chats\/(\d+)(?:\/(\d+))?(?=$|[/?#])/;
|
|
70
|
+
|
|
71
|
+
data.notifications.forEach((notification) => {
|
|
72
|
+
if (!notification || !notification.path || userPathRegex.test(notification.path)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const match = notification.path.match(chatPathRegex);
|
|
76
|
+
if (!match) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const replacement = `/user/${userSlug}/chats/${match[1]}${match[2] ? `/${match[2]}` : ''}`;
|
|
80
|
+
notification.path = notification.path.replace(chatPathRegex, replacement);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return data;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// data.uid here is the profile OWNER being viewed, not the viewer — the
|
|
87
|
+
// viewer is data.callerUID (src/controllers/accounts/helpers.js's
|
|
88
|
+
// getProfileMenu). Core builds each link's url itself from `route`
|
|
89
|
+
// (relative to /user/<profile-owner-slug>/), so there's no need to resolve
|
|
90
|
+
// the slug or build the path here.
|
|
91
|
+
hooks.addProfileLink = async function (data) {
|
|
92
|
+
const viewerUid = data && data.callerUID;
|
|
93
|
+
if (!viewerUid || !await canAccessAdminChats(viewerUid)) {
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
data.links.push({
|
|
98
|
+
id: 'admin-view-chats',
|
|
99
|
+
route: 'chats',
|
|
100
|
+
icon: 'fa-comments',
|
|
101
|
+
name: '[[admin-chats:profile.viewChats]]',
|
|
102
|
+
visibility: {
|
|
103
|
+
self: true, other: true, moderator: true, globalMod: true, admin: true,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return data;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
hooks.isRoomOwner = async function (payload) {
|
|
111
|
+
const [isAdmin, canManage] = await Promise.all([
|
|
112
|
+
User.isAdministrator(payload.uid),
|
|
113
|
+
canManageAdminChats(payload.uid),
|
|
114
|
+
]);
|
|
115
|
+
if (isAdmin || canManage) {
|
|
116
|
+
payload.isOwner = true;
|
|
117
|
+
}
|
|
118
|
+
return payload;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
hooks.isUserInRoom = async function (payload) {
|
|
122
|
+
const uid = getCallerUid(payload);
|
|
123
|
+
if (await canAccessAdminChats(uid)) {
|
|
124
|
+
payload.inRoom = true;
|
|
125
|
+
}
|
|
126
|
+
return payload;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
hooks.canReply = async function (payload) {
|
|
130
|
+
const uid = getCallerUid(payload);
|
|
131
|
+
const roomId = getRoomId(payload);
|
|
132
|
+
|
|
133
|
+
const [isAdmin, canView, canManage] = await Promise.all([
|
|
134
|
+
User.isAdministrator(uid),
|
|
135
|
+
privileges.global.can(ADMIN_CHATS_PRIVILEGE, uid),
|
|
136
|
+
privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, uid),
|
|
137
|
+
]);
|
|
138
|
+
const isManager = isAdmin || canManage;
|
|
139
|
+
const hasAccess = isAdmin || canView || canManage;
|
|
140
|
+
|
|
141
|
+
if (isManager) {
|
|
142
|
+
payload.canReply = true;
|
|
143
|
+
return payload;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (roomId && hasAccess && !await isUserMemberOfRoom(uid, roomId)) {
|
|
147
|
+
payload.canReply = false;
|
|
148
|
+
return payload;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (roomId && await isRoomLocked(roomId)) {
|
|
152
|
+
payload.canReply = false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return payload;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
hooks.filterMessagingSend = async function (payload) {
|
|
159
|
+
const uid = getCallerUid(payload);
|
|
160
|
+
const roomId = getRoomId(payload);
|
|
161
|
+
|
|
162
|
+
if (!uid || !roomId) {
|
|
163
|
+
return payload;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (await User.isAdministrator(uid) || await canManageAdminChats(uid)) {
|
|
167
|
+
return payload;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (await canAccessAdminChats(uid) && !await isUserMemberOfRoom(uid, roomId)) {
|
|
171
|
+
throw new Error('[[error:no-privileges]]');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (await isRoomLocked(roomId)) {
|
|
175
|
+
throw new Error(LOCKED_ACTION_ERROR);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return payload;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
async function guardRoomModification(payload) {
|
|
182
|
+
if (await canManageAdminChats(payload.uid)) {
|
|
183
|
+
return payload;
|
|
184
|
+
}
|
|
185
|
+
if (await canNonAdminModifyLockedRoom(payload.uid, payload.roomId)) {
|
|
186
|
+
throw new Error(LOCKED_ACTION_ERROR);
|
|
187
|
+
}
|
|
188
|
+
return payload;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
hooks.filterAddUsersToRoom = guardRoomModification;
|
|
192
|
+
hooks.filterRemoveUsersFromRoom = guardRoomModification;
|
|
193
|
+
hooks.filterRenameRoom = guardRoomModification;
|
|
194
|
+
|
|
195
|
+
hooks.canGetMessages = async function (payload) {
|
|
196
|
+
if (await canAccessAdminChats(payload.callerUid)) {
|
|
197
|
+
payload.canGet = true;
|
|
198
|
+
}
|
|
199
|
+
return payload;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
hooks.canGetRecentChats = async function (payload) {
|
|
203
|
+
if (await canAccessAdminChats(payload.callerUid)) {
|
|
204
|
+
payload.canGet = true;
|
|
205
|
+
}
|
|
206
|
+
return payload;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
hooks.canGetPublicChats = async function (payload) {
|
|
210
|
+
if (await canAccessAdminChats(payload.callerUid)) {
|
|
211
|
+
payload.canGet = true;
|
|
212
|
+
}
|
|
213
|
+
return payload;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
hooks.onLoadRoom = async function (payload) {
|
|
217
|
+
const { uid, room } = payload;
|
|
218
|
+
if (!room) {
|
|
219
|
+
return payload;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const [isAdmin, canView, canManage] = await Promise.all([
|
|
223
|
+
User.isAdministrator(uid),
|
|
224
|
+
privileges.global.can(ADMIN_CHATS_PRIVILEGE, uid),
|
|
225
|
+
privileges.global.can(ADMIN_CHATS_MANAGE_PRIVILEGE, uid),
|
|
226
|
+
]);
|
|
227
|
+
const isManager = isAdmin || canManage;
|
|
228
|
+
const hasAccess = isAdmin || canView || canManage;
|
|
229
|
+
|
|
230
|
+
const lockData = await getRoomLockData(room.roomId);
|
|
231
|
+
const isLockedForUser = lockData.isLocked && !isManager;
|
|
232
|
+
|
|
233
|
+
room.adminChatLock = lockData;
|
|
234
|
+
room.adminChatLockedForViewer = isLockedForUser;
|
|
235
|
+
room.messages = markMessagesLocked(
|
|
236
|
+
ensureFirstVisibleMessageHeader(buildRoomMessagesWithLockNotice(room.messages, lockData)),
|
|
237
|
+
isLockedForUser
|
|
238
|
+
);
|
|
239
|
+
room.canReply = room.canReply && !isLockedForUser;
|
|
240
|
+
room.showUserInput = room.showUserInput && !isLockedForUser;
|
|
241
|
+
|
|
242
|
+
if (!hasAccess) {
|
|
243
|
+
return payload;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const isOfficialMember = await isUserMemberOfRoom(uid, room.roomId);
|
|
247
|
+
|
|
248
|
+
if (!isOfficialMember) {
|
|
249
|
+
await db.sortedSetRemove(`chat:room:${room.roomId}:uids:online`, uid);
|
|
250
|
+
|
|
251
|
+
if (Array.isArray(room.users)) {
|
|
252
|
+
room.users = room.users.filter(user => user && parseInt(user.uid, 10) !== parseInt(uid, 10));
|
|
253
|
+
}
|
|
254
|
+
if (room.userCount > 0) {
|
|
255
|
+
room.userCount -= 1;
|
|
256
|
+
}
|
|
257
|
+
room.groupChat = room.userCount > 2;
|
|
258
|
+
|
|
259
|
+
const allMids = await db.getSortedSetRevRange(`chat:room:${room.roomId}:mids`, 0, 49);
|
|
260
|
+
|
|
261
|
+
if (allMids.length > 0) {
|
|
262
|
+
const messages = await Messaging.getMessagesData(allMids, uid, room.roomId, false);
|
|
263
|
+
const messageCount = parseInt(await db.getObjectField(`chat:room:${room.roomId}`, 'messageCount'), 10) || 0;
|
|
264
|
+
|
|
265
|
+
messages.forEach((msg, index) => {
|
|
266
|
+
msg.index = messageCount - index - 1;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
room.messages = markMessagesLocked(
|
|
270
|
+
ensureFirstVisibleMessageHeader(buildRoomMessagesWithLockNotice(messages.reverse(), lockData)),
|
|
271
|
+
isLockedForUser
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
room.messages.forEach((msg, index) => {
|
|
275
|
+
if (index === 0) {
|
|
276
|
+
msg.newSet = true;
|
|
277
|
+
} else {
|
|
278
|
+
const prevMsg = room.messages[index - 1];
|
|
279
|
+
const prevTime = parseInt(prevMsg && prevMsg.timestamp, 10) || 0;
|
|
280
|
+
const currentTime = parseInt(msg && msg.timestamp, 10) || 0;
|
|
281
|
+
|
|
282
|
+
msg.newSet = !!(
|
|
283
|
+
currentTime > prevTime + Messaging.newMessageCutoff ||
|
|
284
|
+
parseInt(msg.fromuid, 10) !== parseInt(prevMsg.fromuid, 10) ||
|
|
285
|
+
prevMsg.system ||
|
|
286
|
+
msg.toMid
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
room.messages = ensureFirstVisibleMessageHeader(room.messages);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!isManager && !isOfficialMember) {
|
|
296
|
+
room.canReply = false;
|
|
297
|
+
room.showUserInput = false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
room.isAdmin = isManager;
|
|
301
|
+
room.isAdminOrGlobalMod = isManager;
|
|
302
|
+
payload.isAdminOrGlobalMod = isManager;
|
|
303
|
+
if (isManager) {
|
|
304
|
+
room.isOwner = true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return payload;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
module.exports = hooks;
|