mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-02 20:16:04 +01:00
if there is another owner don't do anything if not then make the next user in the room the owner
498 lines
15 KiB
JavaScript
498 lines
15 KiB
JavaScript
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const validator = require('validator');
|
|
const winston = require('winston');
|
|
|
|
const db = require('../database');
|
|
const user = require('../user');
|
|
const groups = require('../groups');
|
|
const plugins = require('../plugins');
|
|
const privileges = require('../privileges');
|
|
const meta = require('../meta');
|
|
const io = require('../socket.io');
|
|
const cache = require('../cache');
|
|
const cacheCreate = require('../cacheCreate');
|
|
|
|
const roomUidCache = cacheCreate({
|
|
name: 'chat:room:uids',
|
|
max: 500,
|
|
ttl: 0,
|
|
});
|
|
|
|
const intFields = [
|
|
'roomId', 'timestamp', 'userCount',
|
|
];
|
|
|
|
module.exports = function (Messaging) {
|
|
Messaging.getRoomData = async (roomId, fields = []) => {
|
|
const data = await db.getObject(`chat:room:${roomId}`, fields);
|
|
if (!data) {
|
|
throw new Error('[[error:no-chat-room]]');
|
|
}
|
|
|
|
modifyRoomData([data], fields);
|
|
return data;
|
|
};
|
|
|
|
Messaging.getRoomsData = async (roomIds, fields = []) => {
|
|
const roomData = await db.getObjects(
|
|
roomIds.map(roomId => `chat:room:${roomId}`),
|
|
fields
|
|
);
|
|
modifyRoomData(roomData, fields);
|
|
return roomData;
|
|
};
|
|
|
|
function modifyRoomData(rooms, fields) {
|
|
rooms.forEach((data) => {
|
|
if (data) {
|
|
db.parseIntFields(data, intFields, fields);
|
|
data.roomName = validator.escape(String(data.roomName || ''));
|
|
data.public = parseInt(data.public, 10) === 1;
|
|
if (data.hasOwnProperty('groupChat')) {
|
|
data.groupChat = parseInt(data.groupChat, 10) === 1;
|
|
}
|
|
|
|
if (data.hasOwnProperty('groups') || !fields.length || fields.includes('groups')) {
|
|
try {
|
|
data.groups = JSON.parse(data.groups || '[]');
|
|
} catch (err) {
|
|
winston.error(err.stack);
|
|
data.groups = [];
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
Messaging.newRoom = async (uid, data) => {
|
|
// backwards compat. remove in 4.x
|
|
if (Array.isArray(data)) { // old usage second param used to be toUids
|
|
data = { uids: data };
|
|
}
|
|
const now = Date.now();
|
|
const roomId = await db.incrObjectField('global', 'nextChatRoomId');
|
|
const room = {
|
|
roomId: roomId,
|
|
timestamp: now,
|
|
};
|
|
|
|
if (data.hasOwnProperty('roomName') && data.roomName) {
|
|
room.roomName = String(data.roomName);
|
|
}
|
|
if (Array.isArray(data.groups) && data.groups.length) {
|
|
room.groups = JSON.stringify(data.groups);
|
|
}
|
|
const isPublic = data.type === 'public';
|
|
if (isPublic) {
|
|
room.public = 1;
|
|
}
|
|
|
|
await Promise.all([
|
|
db.setObject(`chat:room:${roomId}`, room),
|
|
db.sortedSetAdd('chat:rooms', now, roomId),
|
|
db.sortedSetAdd(`chat:room:${roomId}:owners`, now, uid),
|
|
db.sortedSetsAdd([
|
|
`chat:room:${roomId}:uids`,
|
|
`chat:room:${roomId}:uids:online`,
|
|
], now, uid),
|
|
]);
|
|
|
|
await Promise.all([
|
|
Messaging.addUsersToRoom(uid, data.uids, roomId),
|
|
isPublic ?
|
|
db.sortedSetAddBulk([
|
|
['chat:rooms:public', now, roomId],
|
|
['chat:rooms:public:order', roomId, roomId],
|
|
]) :
|
|
Messaging.addRoomToUsers(roomId, [uid].concat(data.uids), now),
|
|
]);
|
|
|
|
cache.del([
|
|
'chat:rooms:public:all',
|
|
'chat:rooms:public:order:all',
|
|
]);
|
|
|
|
if (!isPublic) {
|
|
// chat owner should also get the user-join system message
|
|
await Messaging.addSystemMessage('user-join', uid, roomId);
|
|
}
|
|
|
|
return roomId;
|
|
};
|
|
|
|
Messaging.deleteRooms = async (roomIds) => {
|
|
if (!roomIds) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
|
|
if (!Array.isArray(roomIds)) {
|
|
roomIds = [roomIds];
|
|
}
|
|
|
|
await Promise.all(roomIds.map(async (roomId) => {
|
|
const uids = await db.getSortedSetMembers(`chat:room:${roomId}:uids`);
|
|
const keys = uids
|
|
.map(uid => `uid:${uid}:chat:rooms`)
|
|
.concat(uids.map(uid => `uid:${uid}:chat:rooms:unread`));
|
|
|
|
await db.sortedSetsRemove(keys, roomId);
|
|
}));
|
|
await Promise.all([
|
|
db.deleteAll([
|
|
...roomIds.map(id => `chat:room:${id}`),
|
|
...roomIds.map(id => `chat:room:${id}:uids`),
|
|
...roomIds.map(id => `chat:room:${id}:owners`),
|
|
...roomIds.map(id => `chat:room:${id}:uids:online`),
|
|
]),
|
|
db.sortedSetRemove('chat:rooms', roomIds),
|
|
db.sortedSetRemove('chat:rooms:public', roomIds),
|
|
db.sortedSetRemove('chat:rooms:public:order', roomIds),
|
|
]);
|
|
cache.del([
|
|
'chat:rooms:public:all',
|
|
'chat:rooms:public:order:all',
|
|
]);
|
|
};
|
|
|
|
Messaging.isUserInRoom = async (uid, roomIds) => {
|
|
let single = false;
|
|
if (!Array.isArray(roomIds)) {
|
|
roomIds = [roomIds];
|
|
single = true;
|
|
}
|
|
const inRooms = await db.isMemberOfSortedSets(
|
|
roomIds.map(id => `chat:room:${id}:uids`),
|
|
uid
|
|
);
|
|
|
|
const data = await Promise.all(roomIds.map(async (roomId, idx) => {
|
|
const data = await plugins.hooks.fire('filter:messaging.isUserInRoom', {
|
|
uid: uid,
|
|
roomId: roomId,
|
|
inRoom: inRooms[idx],
|
|
});
|
|
return data.inRoom;
|
|
}));
|
|
return single ? data.pop() : data;
|
|
};
|
|
|
|
Messaging.isUsersInRoom = async (uids, roomId) => {
|
|
let single = false;
|
|
if (!Array.isArray(uids)) {
|
|
uids = [uids];
|
|
single = true;
|
|
}
|
|
|
|
const inRooms = await db.isSortedSetMembers(
|
|
`chat:room:${roomId}:uids`,
|
|
uids,
|
|
);
|
|
|
|
const data = await plugins.hooks.fire('filter:messaging.isUsersInRoom', {
|
|
uids: uids,
|
|
roomId: roomId,
|
|
inRooms: inRooms,
|
|
});
|
|
|
|
return single ? data.inRooms.pop() : data.inRooms;
|
|
};
|
|
|
|
Messaging.roomExists = async roomId => db.exists(`chat:room:${roomId}`);
|
|
|
|
Messaging.getUserCountInRoom = async roomId => db.sortedSetCard(`chat:room:${roomId}:uids`);
|
|
|
|
Messaging.isRoomOwner = async (uids, roomId) => {
|
|
const isArray = Array.isArray(uids);
|
|
if (!isArray) {
|
|
uids = [uids];
|
|
}
|
|
|
|
const isOwners = await db.isSortedSetMembers(`chat:room:${roomId}:owners`, uids);
|
|
const result = await Promise.all(isOwners.map(async (isOwner, index) => {
|
|
const payload = await plugins.hooks.fire('filter:messaging.isRoomOwner', { uid: uids[index], roomId, isOwner });
|
|
return payload.isOwner;
|
|
}));
|
|
return isArray ? result : result[0];
|
|
};
|
|
|
|
Messaging.toggleOwner = async (uid, roomId) => {
|
|
if (!(parseInt(uid, 10) > 0) || !roomId) {
|
|
return;
|
|
}
|
|
const isOwner = await Messaging.isRoomOwner(uid, roomId);
|
|
if (isOwner) {
|
|
await db.sortedSetRemove(`chat:room:${roomId}:owners`, uid);
|
|
} else {
|
|
await db.sortedSetAdd(`chat:room:${roomId}:owners`, Date.now(), uid);
|
|
}
|
|
};
|
|
|
|
Messaging.isRoomPublic = async function (roomId) {
|
|
return parseInt(await db.getObjectField(`chat:room:${roomId}`, 'public'), 10) === 1;
|
|
};
|
|
|
|
Messaging.addUsersToRoom = async function (uid, uids, roomId) {
|
|
uids = _.uniq(uids);
|
|
const inRoom = await Messaging.isUserInRoom(uid, roomId);
|
|
const payload = await plugins.hooks.fire('filter:messaging.addUsersToRoom', { uid, uids, roomId, inRoom });
|
|
|
|
if (!payload.inRoom) {
|
|
throw new Error('[[error:cant-add-users-to-chat-room]]');
|
|
}
|
|
|
|
await addUidsToRoom(payload.uids, roomId);
|
|
};
|
|
|
|
async function addUidsToRoom(uids, roomId) {
|
|
const now = Date.now();
|
|
const timestamps = uids.map(() => now);
|
|
await Promise.all([
|
|
db.sortedSetAdd(`chat:room:${roomId}:uids`, timestamps, uids),
|
|
db.sortedSetAdd(`chat:room:${roomId}:uids:online`, timestamps, uids),
|
|
]);
|
|
await updateUserCount([roomId]);
|
|
await Promise.all(uids.map(uid => Messaging.addSystemMessage('user-join', uid, roomId)));
|
|
}
|
|
|
|
Messaging.removeUsersFromRoom = async (uid, uids, roomId) => {
|
|
const [isOwner, userCount] = await Promise.all([
|
|
Messaging.isRoomOwner(uid, roomId),
|
|
Messaging.getUserCountInRoom(roomId),
|
|
]);
|
|
const payload = await plugins.hooks.fire('filter:messaging.removeUsersFromRoom', { uid, uids, roomId, isOwner, userCount });
|
|
|
|
if (!payload.isOwner) {
|
|
throw new Error('[[error:cant-remove-users-from-chat-room]]');
|
|
}
|
|
|
|
await Messaging.leaveRoom(payload.uids, payload.roomId);
|
|
};
|
|
|
|
Messaging.isGroupChat = async function (roomId) {
|
|
return (await Messaging.getRoomData(roomId)).groupChat;
|
|
};
|
|
|
|
async function updateUserCount(roomIds) {
|
|
const userCounts = await db.sortedSetsCard(roomIds.map(roomId => `chat:room:${roomId}:uids`));
|
|
const countMap = _.zipObject(roomIds, userCounts);
|
|
const groupChats = roomIds.filter((roomId, index) => userCounts[index] > 2);
|
|
const privateChats = roomIds.filter((roomId, index) => userCounts[index] <= 2);
|
|
await db.setObjectBulk([
|
|
...groupChats.map(id => [`chat:room:${id}`, { groupChat: 1, userCount: countMap[id] }]),
|
|
...privateChats.map(id => [`chat:room:${id}`, { groupChat: 0, userCount: countMap[id] }]),
|
|
]);
|
|
roomUidCache.del(roomIds.map(id => `chat:room:${id}:users`));
|
|
}
|
|
|
|
Messaging.leaveRoom = async (uids, roomId) => {
|
|
const isInRoom = await Promise.all(uids.map(uid => Messaging.isUserInRoom(uid, roomId)));
|
|
uids = uids.filter((uid, index) => isInRoom[index]);
|
|
|
|
const keys = uids
|
|
.map(uid => `uid:${uid}:chat:rooms`)
|
|
.concat(uids.map(uid => `uid:${uid}:chat:rooms:unread`));
|
|
|
|
await Promise.all([
|
|
db.sortedSetRemove([
|
|
`chat:room:${roomId}:uids`,
|
|
`chat:room:${roomId}:owners`,
|
|
`chat:room:${roomId}:uids:online`,
|
|
], uids),
|
|
db.sortedSetsRemove(keys, roomId),
|
|
]);
|
|
|
|
await Promise.all(uids.map(uid => Messaging.addSystemMessage('user-leave', uid, roomId)));
|
|
await updateOwner(roomId);
|
|
await updateUserCount([roomId]);
|
|
};
|
|
|
|
Messaging.leaveRooms = async (uid, roomIds) => {
|
|
const isInRoom = await Promise.all(roomIds.map(roomId => Messaging.isUserInRoom(uid, roomId)));
|
|
roomIds = roomIds.filter((roomId, index) => isInRoom[index]);
|
|
|
|
const roomKeys = [
|
|
...roomIds.map(roomId => `chat:room:${roomId}:uids`),
|
|
...roomIds.map(roomId => `chat:room:${roomId}:owners`),
|
|
...roomIds.map(roomId => `chat:room:${roomId}:uids:online`),
|
|
];
|
|
await Promise.all([
|
|
db.sortedSetsRemove(roomKeys, uid),
|
|
db.sortedSetRemove([
|
|
`uid:${uid}:chat:rooms`,
|
|
`uid:${uid}:chat:rooms:unread`,
|
|
], roomIds),
|
|
]);
|
|
|
|
await Promise.all(
|
|
roomIds.map(roomId => updateOwner(roomId))
|
|
.concat(roomIds.map(roomId => Messaging.addSystemMessage('user-leave', uid, roomId)))
|
|
);
|
|
await updateUserCount(roomIds);
|
|
};
|
|
|
|
async function updateOwner(roomId) {
|
|
let nextOwner = await db.getSortedSetRange(`chat:room:${roomId}:owners`, 0, 0);
|
|
if (!nextOwner.length) {
|
|
// no owners left grab next user
|
|
nextOwner = await db.getSortedSetRange(`chat:room:${roomId}:uids`, 0, 0);
|
|
const newOwner = nextOwner[0] || 0;
|
|
if (parseInt(newOwner, 10) > 0) {
|
|
await db.sortedSetAdd(`chat:room:${roomId}:owners`, Date.now(), newOwner);
|
|
}
|
|
}
|
|
}
|
|
|
|
Messaging.getAllUidsInRoomFromSet = async function (set) {
|
|
const cacheKey = `${set}:all`;
|
|
let uids = roomUidCache.get(cacheKey);
|
|
if (uids !== undefined) {
|
|
return uids;
|
|
}
|
|
uids = await Messaging.getUidsInRoomFromSet(set, 0, -1);
|
|
roomUidCache.set(cacheKey, uids);
|
|
return uids;
|
|
};
|
|
|
|
Messaging.getUidsInRoomFromSet = async (set, start, stop, reverse = false) => db[
|
|
reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'
|
|
](set, start, stop);
|
|
|
|
Messaging.getUidsInRoom = async (roomId, start, stop, reverse = false) => db[
|
|
reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'
|
|
](`chat:room:${roomId}:uids`, start, stop);
|
|
|
|
Messaging.getUsersInRoom = async (roomId, start, stop, reverse = false) => {
|
|
const users = await Messaging.getUsersInRoomFromSet(
|
|
`chat:room:${roomId}:uids`, roomId, start, stop, reverse
|
|
);
|
|
return users;
|
|
};
|
|
|
|
Messaging.getUsersInRoomFromSet = async (set, roomId, start, stop, reverse = false) => {
|
|
const uids = await Messaging.getUidsInRoomFromSet(set, start, stop, reverse);
|
|
const [users, isOwners] = await Promise.all([
|
|
user.getUsersFields(uids, ['uid', 'username', 'picture', 'status']),
|
|
Messaging.isRoomOwner(uids, roomId),
|
|
]);
|
|
|
|
return users.map((user, index) => {
|
|
user.index = start + index;
|
|
user.isOwner = isOwners[index];
|
|
return user;
|
|
});
|
|
};
|
|
|
|
Messaging.renameRoom = async function (uid, roomId, newName) {
|
|
if (!newName) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
newName = newName.trim();
|
|
if (newName.length > 75) {
|
|
throw new Error('[[error:chat-room-name-too-long]]');
|
|
}
|
|
|
|
const payload = await plugins.hooks.fire('filter:chat.renameRoom', {
|
|
uid: uid,
|
|
roomId: roomId,
|
|
newName: newName,
|
|
});
|
|
const isOwner = await Messaging.isRoomOwner(payload.uid, payload.roomId);
|
|
if (!isOwner) {
|
|
throw new Error('[[error:no-privileges]]');
|
|
}
|
|
|
|
await db.setObjectField(`chat:room:${payload.roomId}`, 'roomName', payload.newName);
|
|
await Messaging.addSystemMessage(`room-rename, ${payload.newName.replace(',', ',')}`, payload.uid, payload.roomId);
|
|
|
|
plugins.hooks.fire('action:chat.renameRoom', {
|
|
roomId: payload.roomId,
|
|
newName: payload.newName,
|
|
});
|
|
};
|
|
|
|
Messaging.canReply = async (roomId, uid) => {
|
|
const inRoom = await db.isSortedSetMember(`chat:room:${roomId}:uids`, uid);
|
|
const data = await plugins.hooks.fire('filter:messaging.canReply', { uid: uid, roomId: roomId, inRoom: inRoom, canReply: inRoom });
|
|
return data.canReply;
|
|
};
|
|
|
|
Messaging.loadRoom = async (uid, data) => {
|
|
const { roomId } = data;
|
|
const [room, inRoom, canChat, isAdmin, isGlobalMod] = await Promise.all([
|
|
Messaging.getRoomData(roomId),
|
|
Messaging.isUserInRoom(uid, roomId),
|
|
privileges.global.can('chat', uid),
|
|
user.isAdministrator(uid),
|
|
user.isGlobalModerator(uid),
|
|
]);
|
|
|
|
if (!canChat) {
|
|
throw new Error('[[error:no-privileges]]');
|
|
}
|
|
if (!room ||
|
|
(!room.public && !inRoom) ||
|
|
(room.public && (
|
|
Array.isArray(room.groups) && room.groups.length && !(await groups.isMemberOfAny(uid, room.groups)))
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
// add user to public room onload
|
|
if (room.public && !inRoom) {
|
|
await addUidsToRoom([uid], roomId);
|
|
room.userCount += 1;
|
|
} else if (inRoom) {
|
|
await db.sortedSetAdd(`chat:room:${roomId}:uids:online`, Date.now(), uid);
|
|
}
|
|
|
|
const [canReply, users, messages, settings, isOwner, onlineUids] = await Promise.all([
|
|
Messaging.canReply(roomId, uid),
|
|
Messaging.getUsersInRoomFromSet(`chat:room:${roomId}:uids:online`, roomId, 0, 39, true),
|
|
Messaging.getMessages({
|
|
callerUid: uid,
|
|
uid: data.uid || uid,
|
|
roomId: roomId,
|
|
isNew: false,
|
|
}),
|
|
user.getSettings(uid),
|
|
Messaging.isRoomOwner(uid, roomId),
|
|
io.getUidsInRoom(`chat_room_${roomId}`),
|
|
]);
|
|
|
|
users.forEach((user) => {
|
|
if (user) {
|
|
user.online = parseInt(user.uid, 10) === parseInt(uid, 10) || onlineUids.includes(String(user.uid));
|
|
}
|
|
});
|
|
|
|
room.messages = messages;
|
|
room.isOwner = isOwner;
|
|
room.users = users;
|
|
room.canReply = canReply;
|
|
room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : users.length > 2;
|
|
room.icon = Messaging.getRoomIcon(room);
|
|
room.usernames = Messaging.generateUsernames(users, uid);
|
|
room.chatWithMessage = await Messaging.generateChatWithMessage(users, uid, settings.userLang);
|
|
room.maximumUsersInChatRoom = meta.config.maximumUsersInChatRoom;
|
|
room.maximumChatMessageLength = meta.config.maximumChatMessageLength;
|
|
room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2;
|
|
room.isAdminOrGlobalMod = isAdmin || isGlobalMod;
|
|
room.isAdmin = isAdmin;
|
|
|
|
const payload = await plugins.hooks.fire('filter:messaging.loadRoom', { uid, data, room });
|
|
return payload.room;
|
|
};
|
|
|
|
const globalUserGroups = [
|
|
'registered-users', 'verified-users', 'unverified-users', 'banned-users',
|
|
];
|
|
|
|
Messaging.getRoomIcon = function (roomData) {
|
|
const hasGroups = Array.isArray(roomData.groups) && roomData.groups.length;
|
|
return !hasGroups || roomData.groups.some(group => globalUserGroups.includes(group)) ? 'fa-hashtag' : 'fa-lock';
|
|
};
|
|
};
|