nodebb-plugin-admin-chats 1.0.1 → 1.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/README.md +56 -0
- package/client.js +215 -17
- package/library.js +226 -14
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Super Admin Chat Control for NodeBB
|
|
2
|
+
|
|
3
|
+
A professional administration plugin for NodeBB that allows administrators to monitor, manage, and interact with any chat room on the forum, regardless of membership.
|
|
4
|
+
|
|
5
|
+
## Main Features
|
|
6
|
+
|
|
7
|
+
* **Global Chat Access**: Administrators can view any private or group conversation.
|
|
8
|
+
* **Management Privileges**: Grants admins the ability to edit or delete any message in any room.
|
|
9
|
+
* **Admin Room Locking**: Administrators can lock any chat room so only admins can continue replying.
|
|
10
|
+
* **Profile Integration**: Adds a "View Chats" button to the user profile menu for administrators to quickly audit a user's interactions.
|
|
11
|
+
* **Non-Intrusive Monitoring**: Admins can load and view room history without being added as permanent members, maintaining a clean member list.
|
|
12
|
+
* **UI Enhancements**: Replaces generic "no chats" messages with helpful instructions for admins and supports both English and Hebrew interfaces.
|
|
13
|
+
|
|
14
|
+
## Technical Overview
|
|
15
|
+
|
|
16
|
+
### Plugin Information
|
|
17
|
+
* **Name**: Super Admin Chat Control
|
|
18
|
+
* **ID**: `nodebb-plugin-admin-chats`
|
|
19
|
+
* **Compatibility**: NodeBB versions `^3.0.0` or `^4.0.0`
|
|
20
|
+
|
|
21
|
+
### Implemented Hooks
|
|
22
|
+
The plugin utilizes several filters to elevate administrator permissions:
|
|
23
|
+
|
|
24
|
+
| Hook | Method | Functionality |
|
|
25
|
+
| :--- | :--- | :--- |
|
|
26
|
+
| `filter:messaging.isRoomOwner` | `isRoomOwner` | Treats admins as owners for permission checks. |
|
|
27
|
+
| `filter:messaging.canReply` | `canReply` | Allows admins to send messages in any room and blocks regular users in admin-locked rooms. |
|
|
28
|
+
| `filter:messaging.canGetMessages` | `canGetMessages` | Allows admins to fetch chat history. |
|
|
29
|
+
| `filter:messaging.loadRoom` | `onLoadRoom` | Manages room loading logic for non-member admins and injects lock state. |
|
|
30
|
+
| `filter:user.accountMenu` | `addProfileLink` | Injects the admin link into the profile menu. |
|
|
31
|
+
|
|
32
|
+
### Function Overrides
|
|
33
|
+
The plugin overrides core messaging functions to ensure full administrative control:
|
|
34
|
+
* **Edit/Delete**: `Messaging.canEdit` and `Messaging.canDelete` are bypassed for administrators.
|
|
35
|
+
* **Visibility**: `Messaging.canViewMessage` is modified to always return `true` for admins.
|
|
36
|
+
* **Room Locking**: `Messaging.canReply` is wrapped so non-admins cannot post in rooms locked by an admin.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
1. Install the plugin via terminal:
|
|
41
|
+
```bash
|
|
42
|
+
npm install nodebb-plugin-admin-chats
|
|
43
|
+
```
|
|
44
|
+
2. Activate the plugin in the NodeBB Admin Control Panel (ACP).
|
|
45
|
+
3. Restart NodeBB.
|
|
46
|
+
|
|
47
|
+
## Client-Side Support
|
|
48
|
+
The plugin includes a `client.js` script that:
|
|
49
|
+
* Detects the system language (English or Hebrew).
|
|
50
|
+
* Dynamically injects action buttons into the account sub-links menu.
|
|
51
|
+
* Adds an admin lock/unlock control to the chat UI.
|
|
52
|
+
* Disables the composer for non-admin users when a room is locked.
|
|
53
|
+
* Cleans up the chat UI empty states for a better admin experience.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
*Developed by [palmoni5](https://github.com/palmoni5/nodebb-plugin-admin-chats).*
|
package/client.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
$(document).ready(function() {
|
|
2
|
-
|
|
2
|
+
const LOCK_PREFIX = '[admin-chat-lock]';
|
|
3
|
+
const LOCKED_MESSAGE_TEXT = '🔒 חדר זה ננעל ע"י המנהלים.';
|
|
4
|
+
|
|
3
5
|
function isEnglishSystem() {
|
|
4
6
|
return $('html').attr('lang') && $('html').attr('lang').startsWith('en');
|
|
5
7
|
}
|
|
@@ -9,33 +11,195 @@ $(document).ready(function() {
|
|
|
9
11
|
|
|
10
12
|
$('span.text-muted.text-sm').each(function() {
|
|
11
13
|
const currentText = $(this).text().trim();
|
|
12
|
-
|
|
14
|
+
|
|
13
15
|
if (currentText.includes("אין לכם צ'אטים פעילים") || currentText === "אין לכם צ'אטים פעילים.") {
|
|
14
16
|
$(this).text("אנא בחר צ'אט מסרגל הצד.");
|
|
15
17
|
$(this).removeClass('text-muted');
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
if (currentText.includes(
|
|
19
|
-
$(this).text(
|
|
20
|
+
if (currentText.includes('You have no active chats') || currentText === 'You have no active chats.') {
|
|
21
|
+
$(this).text('Please select a chat from the sidebar.');
|
|
20
22
|
$(this).removeClass('text-muted');
|
|
21
23
|
}
|
|
22
24
|
});
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
function getRoomData() {
|
|
28
|
+
if (!ajaxify || !ajaxify.data) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return ajaxify.data.room || ajaxify.data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getLockText() {
|
|
36
|
+
if (isEnglishSystem()) {
|
|
37
|
+
return '🔒 This room was locked by the administrators.';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return LOCKED_MESSAGE_TEXT;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderLockBanner() {
|
|
44
|
+
const roomData = getRoomData();
|
|
45
|
+
const lockData = roomData && roomData.adminChatLock;
|
|
46
|
+
$('.admin-chat-lock-banner').remove();
|
|
47
|
+
|
|
48
|
+
if (!lockData || !lockData.isLocked) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const target = $('[component="chat/messages"]').first().parent();
|
|
53
|
+
if (!target.length) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
target.prepend(`
|
|
58
|
+
<div class="admin-chat-lock-banner alert alert-warning mb-2">${getLockText()}</div>
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function setComposerVisibility(shouldHide) {
|
|
63
|
+
const selectors = [
|
|
64
|
+
'[component="chat/composer"]',
|
|
65
|
+
'[component="chat/replies"]',
|
|
66
|
+
'[component="chat/input"]',
|
|
67
|
+
'[component="chat/textarea"]',
|
|
68
|
+
'.chat-input',
|
|
69
|
+
'.composer',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
selectors.forEach(function(selector) {
|
|
74
|
+
$(selector).each(function() {
|
|
75
|
+
const $element = $(this);
|
|
76
|
+
const $container = $element.closest('[component="chat/composer"], [component="chat/replies"], .chat-input, .composer').length ?
|
|
77
|
+
$element.closest('[component="chat/composer"], [component="chat/replies"], .chat-input, .composer').first() :
|
|
78
|
+
$element;
|
|
79
|
+
const domNode = $container.get(0);
|
|
80
|
+
|
|
81
|
+
if (!domNode || seen.has(domNode)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
seen.add(domNode);
|
|
86
|
+
$container.toggle(!shouldHide);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function updateComposerState() {
|
|
92
|
+
const roomData = getRoomData();
|
|
93
|
+
const lockData = roomData && roomData.adminChatLock;
|
|
94
|
+
const shouldHideComposer = !!(lockData && lockData.isLocked && !(app.user && app.user.isAdmin));
|
|
95
|
+
|
|
96
|
+
$('[component="chat/input"], [component="chat/textarea"], textarea.chat-input, .chat-input textarea')
|
|
97
|
+
.prop('disabled', shouldHideComposer)
|
|
98
|
+
.attr('placeholder', shouldHideComposer ? getLockText() : null);
|
|
99
|
+
|
|
100
|
+
$('[component="chat/send"], .chat-send, button[component="chat/submit"]').prop('disabled', shouldHideComposer);
|
|
101
|
+
|
|
102
|
+
setComposerVisibility(shouldHideComposer);
|
|
103
|
+
renderLockBanner();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeLockMessages() {
|
|
107
|
+
$('[component="chat/message/content"], .chat-message-content').each(function() {
|
|
108
|
+
const $el = $(this);
|
|
109
|
+
const text = $el.text().trim();
|
|
110
|
+
if (!text.startsWith(LOCK_PREFIX)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
$el.text(text.replace(LOCK_PREFIX, '').trim());
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getRoomMenuTargets() {
|
|
119
|
+
return $('[component="chat/header"], .chat-header, [component="chat/nav"]')
|
|
120
|
+
.find('.dropdown-menu');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderAdminLockControl() {
|
|
124
|
+
$('.admin-chat-lock-toggle-item, .admin-chat-lock-divider').remove();
|
|
125
|
+
|
|
126
|
+
if (!app.user.isAdmin) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const roomData = getRoomData();
|
|
131
|
+
const roomId = roomData && roomData.roomId;
|
|
132
|
+
if (!roomId) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const menus = getRoomMenuTargets();
|
|
137
|
+
if (!menus.length) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const lockData = roomData.adminChatLock || {};
|
|
142
|
+
const isLocked = !!lockData.isLocked;
|
|
143
|
+
const itemText = isEnglishSystem() ? (isLocked ? 'Unlock Room' : 'Lock Room') : (isLocked ? 'פתח חדר' : 'נעל חדר');
|
|
144
|
+
const iconClass = isLocked ? 'fa-lock-open' : 'fa-lock';
|
|
145
|
+
const menuItemHtml = `
|
|
146
|
+
<li role="presentation" class="admin-chat-lock-item-wrap">
|
|
147
|
+
<a href="#" role="menuitem" class="dropdown-item rounded-1 d-flex align-items-center gap-2 admin-chat-lock-toggle-item" data-room-id="${roomId}" data-locked="${isLocked}">
|
|
148
|
+
<i class="fa fa-fw ${iconClass}"></i>
|
|
149
|
+
<span>${itemText}</span>
|
|
150
|
+
</a>
|
|
151
|
+
</li>
|
|
152
|
+
<li role="presentation" class="dropdown-divider admin-chat-lock-divider"></li>
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
menus.each(function() {
|
|
156
|
+
const $menu = $(this);
|
|
157
|
+
if ($menu.find('.admin-chat-lock-toggle-item').length) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
$menu.prepend(menuItemHtml);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function toggleRoomLock(roomId, nextState) {
|
|
166
|
+
const response = await fetch(`${config.relative_path || ''}/api/admin-chats/${roomId}/lock`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
'Content-Type': 'application/json',
|
|
170
|
+
'x-csrf-token': config.csrf_token,
|
|
171
|
+
},
|
|
172
|
+
body: JSON.stringify({ locked: nextState }),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
throw new Error('Unable to update room lock');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return await response.json();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function refreshChatUi() {
|
|
183
|
+
replaceAdminEmptyStateText();
|
|
184
|
+
renderAdminLockControl();
|
|
185
|
+
updateComposerState();
|
|
186
|
+
normalizeLockMessages();
|
|
187
|
+
}
|
|
188
|
+
|
|
25
189
|
$(window).on('action:ajaxify.end', function(ev, data) {
|
|
26
|
-
|
|
27
|
-
|
|
190
|
+
const templateName = ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '';
|
|
191
|
+
|
|
192
|
+
if (app.user.isAdmin && templateName.startsWith('account/')) {
|
|
28
193
|
const userSlug = ajaxify.data.userslug || (ajaxify.data.user && ajaxify.data.user.userslug);
|
|
29
|
-
|
|
194
|
+
|
|
30
195
|
if (userSlug) {
|
|
31
|
-
const buttonText = isEnglishSystem() ?
|
|
32
|
-
|
|
33
|
-
const relativePath = config.relative_path || '';
|
|
196
|
+
const buttonText = isEnglishSystem() ? 'View Chats' : "צפיה בצ'אטים";
|
|
197
|
+
const relativePath = config.relative_path || '';
|
|
34
198
|
|
|
35
199
|
const btnHtml = `
|
|
36
200
|
<li role="presentation">
|
|
37
201
|
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="${relativePath}/user/${userSlug}/chats" role="menuitem">
|
|
38
|
-
<i class="far fa-fw fa-comments"></i>
|
|
202
|
+
<i class="far fa-fw fa-comments"></i>
|
|
39
203
|
<span>${buttonText}</span>
|
|
40
204
|
</a>
|
|
41
205
|
</li>
|
|
@@ -49,14 +213,48 @@ $(document).ready(function() {
|
|
|
49
213
|
}
|
|
50
214
|
}
|
|
51
215
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
216
|
+
const url = data && data.url ? data.url : '';
|
|
217
|
+
|
|
218
|
+
if (url.match(/^user\/.+\/chats/) || url === 'chats') {
|
|
219
|
+
refreshChatUi();
|
|
220
|
+
setTimeout(refreshChatUi, 500);
|
|
55
221
|
}
|
|
56
222
|
});
|
|
57
223
|
|
|
58
|
-
$(window).on('action:chat.loaded',
|
|
224
|
+
$(window).on('action:chat.loaded', function() {
|
|
225
|
+
refreshChatUi();
|
|
226
|
+
setTimeout(refreshChatUi, 200);
|
|
227
|
+
});
|
|
228
|
+
|
|
59
229
|
$(window).on('action:chat.closed', function() {
|
|
60
|
-
setTimeout(
|
|
230
|
+
setTimeout(refreshChatUi, 200);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
$(document).on('click', '.admin-chat-lock-toggle-item', async function(ev) {
|
|
234
|
+
ev.preventDefault();
|
|
235
|
+
|
|
236
|
+
const $button = $(this);
|
|
237
|
+
const roomId = parseInt($button.attr('data-room-id'), 10);
|
|
238
|
+
const isLocked = $button.attr('data-locked') === 'true';
|
|
239
|
+
|
|
240
|
+
if (!roomId) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
$button.addClass('disabled').attr('aria-disabled', 'true');
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const result = await toggleRoomLock(roomId, !isLocked);
|
|
248
|
+
if (ajaxify && ajaxify.data) {
|
|
249
|
+
ajaxify.data.adminChatLock = result.lockData;
|
|
250
|
+
if (ajaxify.data.room) {
|
|
251
|
+
ajaxify.data.room.adminChatLock = result.lockData;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
ajaxify.refresh();
|
|
255
|
+
} catch (err) {
|
|
256
|
+
app.alertError(isEnglishSystem() ? 'Unable to update room lock.' : 'לא ניתן לעדכן את נעילת החדר.');
|
|
257
|
+
$button.removeClass('disabled').removeAttr('aria-disabled');
|
|
258
|
+
}
|
|
61
259
|
});
|
|
62
|
-
});
|
|
260
|
+
});
|
package/library.js
CHANGED
|
@@ -3,20 +3,21 @@
|
|
|
3
3
|
const User = require.main.require('./src/user');
|
|
4
4
|
const Messaging = require.main.require('./src/messaging');
|
|
5
5
|
const db = require.main.require('./src/database');
|
|
6
|
-
const _ = require('lodash');
|
|
7
6
|
|
|
8
7
|
const plugin = {};
|
|
8
|
+
const LOCK_PREFIX = '[admin-chat-lock]';
|
|
9
|
+
const LOCKED_MESSAGE_TEXT = '🔒 חדר זה ננעל ע"י המנהלים.';
|
|
10
|
+
const UNLOCKED_MESSAGE_TEXT = '🔓 חדר זה נפתח מחדש ע"י המנהלים.';
|
|
9
11
|
|
|
10
12
|
plugin.init = async function (params) {
|
|
11
|
-
|
|
13
|
+
registerRoutes(params.router, params.middleware);
|
|
12
14
|
overrideMessagingFunctions();
|
|
13
15
|
};
|
|
14
16
|
|
|
15
|
-
|
|
16
17
|
plugin.addProfileLink = async function (data) {
|
|
17
18
|
try {
|
|
18
19
|
const userSlug = data.user ? data.user.userslug : (data.userData ? data.userData.userslug : null);
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
if (userSlug) {
|
|
21
22
|
data.links.push({
|
|
22
23
|
id: 'admin-view-chats',
|
|
@@ -55,10 +56,19 @@ plugin.isUserInRoom = async function (payload) {
|
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
plugin.canReply = async function (payload) {
|
|
58
|
-
const
|
|
59
|
+
const uid = getCallerUid(payload);
|
|
60
|
+
const roomId = getRoomId(payload);
|
|
61
|
+
const isAdmin = await User.isAdministrator(uid);
|
|
62
|
+
|
|
59
63
|
if (isAdmin) {
|
|
60
64
|
payload.canReply = true;
|
|
65
|
+
return payload;
|
|
61
66
|
}
|
|
67
|
+
|
|
68
|
+
if (roomId && await isRoomLocked(roomId)) {
|
|
69
|
+
payload.canReply = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
return payload;
|
|
63
73
|
};
|
|
64
74
|
|
|
@@ -88,36 +98,56 @@ plugin.canGetPublicChats = async function (payload) {
|
|
|
88
98
|
|
|
89
99
|
plugin.onLoadRoom = async function (payload) {
|
|
90
100
|
const { uid, room } = payload;
|
|
101
|
+
if (!room) {
|
|
102
|
+
return payload;
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
const isAdmin = await User.isAdministrator(uid);
|
|
106
|
+
const lockData = await getRoomLockData(room.roomId);
|
|
107
|
+
room.adminChatLock = lockData;
|
|
92
108
|
|
|
93
|
-
if (
|
|
109
|
+
if (lockData.isLocked) {
|
|
110
|
+
room.messages = appendLockNotice(room.messages, lockData);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!isAdmin) {
|
|
114
|
+
return payload;
|
|
115
|
+
}
|
|
94
116
|
|
|
95
117
|
const isOfficialMember = await db.isSortedSetMember(`chat:room:${room.roomId}:uids`, uid);
|
|
96
118
|
|
|
97
119
|
if (!isOfficialMember) {
|
|
98
120
|
await db.sortedSetRemove(`chat:room:${room.roomId}:uids:online`, uid);
|
|
99
|
-
|
|
121
|
+
|
|
100
122
|
if (Array.isArray(room.users)) {
|
|
101
123
|
room.users = room.users.filter(user => user && parseInt(user.uid, 10) !== parseInt(uid, 10));
|
|
102
124
|
}
|
|
103
|
-
|
|
104
125
|
if (room.userCount > 0) {
|
|
105
126
|
room.userCount -= 1;
|
|
106
127
|
}
|
|
107
128
|
room.groupChat = room.userCount > 2;
|
|
108
129
|
|
|
109
130
|
const allMids = await db.getSortedSetRevRange(`chat:room:${room.roomId}:mids`, 0, 49);
|
|
110
|
-
|
|
131
|
+
|
|
111
132
|
if (allMids.length > 0) {
|
|
112
133
|
const messages = await Messaging.getMessagesData(allMids, uid, room.roomId, false);
|
|
113
134
|
const messageCount = await db.getObjectField(`chat:room:${room.roomId}`, 'messageCount');
|
|
114
135
|
const count = parseInt(messageCount, 10) || 0;
|
|
115
|
-
|
|
136
|
+
|
|
116
137
|
messages.forEach((msg, index) => {
|
|
117
138
|
msg.index = count - index - 1;
|
|
118
139
|
});
|
|
119
|
-
|
|
120
|
-
room.messages = messages.reverse();
|
|
140
|
+
|
|
141
|
+
room.messages = appendLockNotice(messages.reverse(), lockData);
|
|
142
|
+
|
|
143
|
+
room.messages.forEach((msg, index) => {
|
|
144
|
+
if (index === 0) {
|
|
145
|
+
msg.newSet = true;
|
|
146
|
+
} else {
|
|
147
|
+
const prevMsg = room.messages[index - 1];
|
|
148
|
+
msg.newSet = parseInt(msg.fromuid, 10) !== parseInt(prevMsg.fromuid, 10);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
121
151
|
}
|
|
122
152
|
|
|
123
153
|
room.isAdmin = true;
|
|
@@ -125,15 +155,14 @@ plugin.onLoadRoom = async function (payload) {
|
|
|
125
155
|
} else {
|
|
126
156
|
room.isAdmin = true;
|
|
127
157
|
}
|
|
128
|
-
|
|
129
158
|
return payload;
|
|
130
159
|
};
|
|
131
160
|
|
|
132
161
|
function overrideMessagingFunctions() {
|
|
133
|
-
|
|
134
162
|
const originalCanEdit = Messaging.canEdit;
|
|
135
163
|
const originalCanDelete = Messaging.canDelete;
|
|
136
164
|
const originalCanViewMessage = Messaging.canViewMessage;
|
|
165
|
+
const originalCanReply = Messaging.canReply;
|
|
137
166
|
|
|
138
167
|
Messaging.canEdit = async function (messageId, uid) {
|
|
139
168
|
if (await User.isAdministrator(uid)) {
|
|
@@ -155,6 +184,189 @@ function overrideMessagingFunctions() {
|
|
|
155
184
|
}
|
|
156
185
|
return await originalCanViewMessage(mids, roomId, uid);
|
|
157
186
|
};
|
|
187
|
+
|
|
188
|
+
if (typeof originalCanReply === 'function') {
|
|
189
|
+
Messaging.canReply = async function (...args) {
|
|
190
|
+
const guessed = guessRoomIdUidFromArgs(args);
|
|
191
|
+
if (await User.isAdministrator(guessed.uid)) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (guessed.roomId && await isRoomLocked(guessed.roomId)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return await originalCanReply.apply(this, args);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
function registerRoutes(router, middleware) {
|
|
203
|
+
if (!router) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const routeMiddleware = [];
|
|
208
|
+
if (middleware && typeof middleware.ensureLoggedIn === 'function') {
|
|
209
|
+
routeMiddleware.push(middleware.ensureLoggedIn);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
router.post('/api/admin-chats/:roomId/lock', ...routeMiddleware, async (req, res) => {
|
|
213
|
+
try {
|
|
214
|
+
const actingUid = req.uid;
|
|
215
|
+
const roomId = parseInt(req.params.roomId, 10);
|
|
216
|
+
|
|
217
|
+
if (!actingUid || Number.isNaN(roomId)) {
|
|
218
|
+
return res.status(400).json({ status: { code: 'bad-request', message: 'Invalid room id' } });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!await User.isAdministrator(actingUid)) {
|
|
222
|
+
return res.status(403).json({ status: { code: 'forbidden', message: 'Admin only' } });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!await roomExists(roomId)) {
|
|
226
|
+
return res.status(404).json({ status: { code: 'not-found', message: 'Room not found' } });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const shouldLock = req.body && typeof req.body.locked === 'boolean' ? req.body.locked : true;
|
|
230
|
+
const lockData = shouldLock ?
|
|
231
|
+
await lockRoom(roomId, actingUid) :
|
|
232
|
+
await unlockRoom(roomId, actingUid);
|
|
233
|
+
|
|
234
|
+
return res.json({
|
|
235
|
+
status: { code: 'ok', message: 'Room updated' },
|
|
236
|
+
roomId,
|
|
237
|
+
lockData,
|
|
238
|
+
});
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error('[Super Admin Chats] Error updating room lock:', err);
|
|
241
|
+
return res.status(500).json({ status: { code: 'error', message: err.message } });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function roomExists(roomId) {
|
|
247
|
+
const roomData = await db.getObject(`chat:room:${roomId}`);
|
|
248
|
+
return !!roomData;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function lockRoom(roomId, actingUid) {
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
await db.setObject(`chat:room:${roomId}:adminLock`, {
|
|
254
|
+
isLocked: 1,
|
|
255
|
+
lockedBy: actingUid,
|
|
256
|
+
lockedAt: now,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await addLockSystemMessage(roomId, actingUid, true);
|
|
260
|
+
return await getRoomLockData(roomId);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function unlockRoom(roomId, actingUid) {
|
|
264
|
+
await db.setObject(`chat:room:${roomId}:adminLock`, {
|
|
265
|
+
isLocked: 0,
|
|
266
|
+
lockedBy: actingUid,
|
|
267
|
+
lockedAt: 0,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await addLockSystemMessage(roomId, actingUid, false);
|
|
271
|
+
return await getRoomLockData(roomId);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function addLockSystemMessage(roomId, actingUid, isLocked) {
|
|
275
|
+
if (typeof Messaging.addSystemMessage !== 'function') {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const messageText = isLocked ? LOCK_PREFIX + ' ' + LOCKED_MESSAGE_TEXT : LOCK_PREFIX + ' ' + UNLOCKED_MESSAGE_TEXT;
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
await Messaging.addSystemMessage(messageText, actingUid, roomId);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.error('[Super Admin Chats] Error adding lock system message:', err);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function isRoomLocked(roomId) {
|
|
289
|
+
const field = await db.getObjectField(`chat:room:${roomId}:adminLock`, 'isLocked');
|
|
290
|
+
return field === '1' || field === 1 || field === true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function getRoomLockData(roomId) {
|
|
294
|
+
const data = await db.getObject(`chat:room:${roomId}:adminLock`) || {};
|
|
295
|
+
const lockedBy = parseInt(data.lockedBy, 10) || 0;
|
|
296
|
+
const lockedAt = parseInt(data.lockedAt, 10) || 0;
|
|
297
|
+
const isLockedValue = data.isLocked === '1' || data.isLocked === 1 || data.isLocked === true;
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
isLocked: isLockedValue,
|
|
301
|
+
lockedBy,
|
|
302
|
+
lockedAt,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function appendLockNotice(messages, lockData) {
|
|
307
|
+
const list = Array.isArray(messages) ? messages.slice() : [];
|
|
308
|
+
if (!lockData || !lockData.isLocked || !lockData.lockedAt) {
|
|
309
|
+
return list;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const content = `${LOCK_PREFIX} ${LOCKED_MESSAGE_TEXT}`;
|
|
313
|
+
const exists = list.some(msg => msg && msg.content === content);
|
|
314
|
+
if (exists) {
|
|
315
|
+
return list;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
list.push({
|
|
319
|
+
content,
|
|
320
|
+
fromuid: lockData.lockedBy,
|
|
321
|
+
uid: lockData.lockedBy,
|
|
322
|
+
timestamp: lockData.lockedAt,
|
|
323
|
+
datetime: lockData.lockedAt,
|
|
324
|
+
system: true,
|
|
325
|
+
newSet: true,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
list.sort((left, right) => {
|
|
329
|
+
const leftTime = parseInt(left && (left.timestamp || left.datetime), 10) || 0;
|
|
330
|
+
const rightTime = parseInt(right && (right.timestamp || right.datetime), 10) || 0;
|
|
331
|
+
return leftTime - rightTime;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return list;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function guessRoomIdUidFromArgs(args) {
|
|
338
|
+
if (!Array.isArray(args) || !args.length) {
|
|
339
|
+
return { roomId: null, uid: null };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (args.length === 1 && args[0] && typeof args[0] === 'object') {
|
|
343
|
+
return {
|
|
344
|
+
roomId: getRoomId(args[0]),
|
|
345
|
+
uid: getCallerUid(args[0]),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
roomId: args[0],
|
|
351
|
+
uid: args[1],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function getCallerUid(payload) {
|
|
356
|
+
return payload && (
|
|
357
|
+
payload.uid ||
|
|
358
|
+
payload.callerUid ||
|
|
359
|
+
(payload.data && payload.data.uid) ||
|
|
360
|
+
(payload.message && payload.message.uid)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function getRoomId(payload) {
|
|
365
|
+
return payload && (
|
|
366
|
+
payload.roomId ||
|
|
367
|
+
(payload.data && payload.data.roomId) ||
|
|
368
|
+
(payload.message && payload.message.roomId)
|
|
369
|
+
);
|
|
158
370
|
}
|
|
159
371
|
|
|
160
372
|
module.exports = plugin;
|