mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-28 19:30:53 +01:00
428 lines
13 KiB
JavaScript
428 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const validator = require('validator');
|
|
const winston = require('winston');
|
|
|
|
const db = require('../database');
|
|
const user = require('../user');
|
|
const meta = require('../meta');
|
|
const messaging = require('../messaging');
|
|
const notifications = require('../notifications');
|
|
const privileges = require('../privileges');
|
|
const plugins = require('../plugins');
|
|
const utils = require('../utils');
|
|
|
|
const websockets = require('../socket.io');
|
|
const socketHelpers = require('../socket.io/helpers');
|
|
|
|
const chatsAPI = module.exports;
|
|
|
|
async function rateLimitExceeded(caller, field) {
|
|
const session = caller.request ? caller.request.session : caller.session; // socket vs req
|
|
const now = Date.now();
|
|
const [isPrivileged, reputation] = await Promise.all([
|
|
user.isPrivileged(caller.uid),
|
|
user.getUserField(caller.uid, 'reputation'),
|
|
]);
|
|
const newbie = !isPrivileged && meta.config.newbieReputationThreshold > reputation;
|
|
const delay = newbie ? meta.config.newbieChatMessageDelay : meta.config.chatMessageDelay;
|
|
session[field] = session[field] || 0;
|
|
|
|
if (now - session[field] < delay) {
|
|
return true;
|
|
}
|
|
|
|
session[field] = now;
|
|
return false;
|
|
}
|
|
|
|
chatsAPI.list = async (caller, { uid = caller.uid, start, stop, page, perPage } = {}) => {
|
|
if ((!utils.isNumber(start) || !utils.isNumber(stop)) && !utils.isNumber(page)) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
|
|
if (!start && !stop && page) {
|
|
winston.warn('[api/chats] Sending `page` and `perPage` to .list() is deprecated in favour of `start` and `stop`. The deprecated parameters will be removed in v4.');
|
|
start = Math.max(0, page - 1) * perPage;
|
|
stop = start + perPage - 1;
|
|
}
|
|
|
|
return await messaging.getRecentChats(caller.uid, uid || caller.uid, start, stop);
|
|
};
|
|
|
|
chatsAPI.create = async function (caller, data) {
|
|
if (await rateLimitExceeded(caller, 'lastChatRoomCreateTime')) {
|
|
throw new Error('[[error:too-many-messages]]');
|
|
}
|
|
if (!data) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
|
|
const isPublic = data.type === 'public';
|
|
const isAdmin = await user.isAdministrator(caller.uid);
|
|
if (isPublic && !isAdmin) {
|
|
throw new Error('[[error:no-privileges]]');
|
|
}
|
|
|
|
if (!data.uids || !Array.isArray(data.uids)) {
|
|
throw new Error(`[[error:wrong-parameter-type, uids, ${typeof data.uids}, Array]]`);
|
|
}
|
|
|
|
if (!isPublic && !data.uids.length) {
|
|
throw new Error('[[error:no-users-selected]]');
|
|
}
|
|
if (isPublic && (!Array.isArray(data.groups) || !data.groups.length)) {
|
|
throw new Error('[[error:no-groups-selected]]');
|
|
}
|
|
|
|
data.notificationSetting = isPublic ?
|
|
messaging.notificationSettings.ATMENTION :
|
|
messaging.notificationSettings.ALLMESSAGES;
|
|
|
|
await Promise.all(data.uids.map(uid => messaging.canMessageUser(caller.uid, uid)));
|
|
const roomId = await messaging.newRoom(caller.uid, data);
|
|
|
|
return await messaging.getRoomData(roomId);
|
|
};
|
|
|
|
chatsAPI.getUnread = async (caller) => {
|
|
const count = await messaging.getUnreadCount(caller.uid);
|
|
return { count };
|
|
};
|
|
|
|
chatsAPI.sortPublicRooms = async (caller, { roomIds, scores }) => {
|
|
[roomIds, scores].forEach((arr) => {
|
|
if (!Array.isArray(arr) || !arr.every(value => isFinite(value))) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
});
|
|
|
|
const isAdmin = await user.isAdministrator(caller.uid);
|
|
if (!isAdmin) {
|
|
throw new Error('[[error:no-privileges]]');
|
|
}
|
|
|
|
await db.sortedSetAdd(`chat:rooms:public:order`, scores, roomIds);
|
|
require('../cache').del(`chat:rooms:public:order:all`);
|
|
};
|
|
|
|
chatsAPI.get = async (caller, { uid, roomId }) => await messaging.loadRoom(caller.uid, { uid, roomId });
|
|
|
|
chatsAPI.post = async (caller, data) => {
|
|
if (!data || !data.roomId || !caller.uid) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
|
|
({ data } = await plugins.hooks.fire('filter:messaging.send', {
|
|
data,
|
|
uid: caller.uid,
|
|
}));
|
|
|
|
await messaging.canMessageRoom(caller.uid, data.roomId);
|
|
await messaging.checkContent(data.message);
|
|
|
|
if (await rateLimitExceeded(caller, 'lastChatMessageTime')) {
|
|
throw new Error('[[error:too-many-messages]]');
|
|
}
|
|
|
|
const message = await messaging.addMessage({
|
|
uid: caller.uid,
|
|
roomId: data.roomId,
|
|
content: data.message,
|
|
toMid: data.toMid,
|
|
timestamp: Date.now(),
|
|
ip: caller.ip,
|
|
});
|
|
messaging.notifyUsersInRoom(caller.uid, data.roomId, message);
|
|
user.updateOnlineUsers(caller.uid);
|
|
|
|
return message;
|
|
};
|
|
|
|
chatsAPI.update = async (caller, data) => {
|
|
if (!data || !data.roomId) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
|
|
if (data.hasOwnProperty('name')) {
|
|
if (!data.name && data.name !== '') {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
await messaging.renameRoom(caller.uid, data.roomId, data.name);
|
|
}
|
|
const [roomData, isAdmin] = await Promise.all([
|
|
messaging.getRoomData(data.roomId),
|
|
user.isAdministrator(caller.uid),
|
|
]);
|
|
if (!roomData) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
if (data.hasOwnProperty('groups')) {
|
|
if (roomData.public && isAdmin) {
|
|
await db.setObjectField(`chat:room:${data.roomId}`, 'groups', JSON.stringify(data.groups));
|
|
}
|
|
}
|
|
if (data.hasOwnProperty('notificationSetting') && isAdmin) {
|
|
await db.setObjectField(`chat:room:${data.roomId}`, 'notificationSetting', data.notificationSetting);
|
|
}
|
|
const loadedRoom = await messaging.loadRoom(caller.uid, {
|
|
roomId: data.roomId,
|
|
});
|
|
if (data.hasOwnProperty('name')) {
|
|
const ioRoom = require('../socket.io').in(`chat_room_${data.roomId}`);
|
|
if (ioRoom) {
|
|
ioRoom.emit('event:chats.roomRename', {
|
|
roomId: data.roomId,
|
|
newName: validator.escape(String(data.name)),
|
|
chatWithMessage: loadedRoom.chatWithMessage,
|
|
});
|
|
}
|
|
}
|
|
return loadedRoom;
|
|
};
|
|
|
|
chatsAPI.rename = async (caller, data) => {
|
|
if (!data || !data.roomId || !data.name) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
return await chatsAPI.update(caller, data);
|
|
};
|
|
|
|
chatsAPI.mark = async (caller, data) => {
|
|
if (!caller.uid || !data || !data.roomId) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
const { roomId, state } = data;
|
|
if (state) {
|
|
await messaging.markUnread([caller.uid], roomId);
|
|
} else {
|
|
await messaging.markRead(caller.uid, roomId);
|
|
socketHelpers.emitToUids('event:chats.markedAsRead', { roomId: roomId }, [caller.uid]);
|
|
const nids = await user.notifications.getUnreadByField(caller.uid, 'roomId', [roomId]);
|
|
await notifications.markReadMultiple(nids, caller.uid);
|
|
user.notifications.pushCount(caller.uid);
|
|
}
|
|
|
|
socketHelpers.emitToUids('event:chats.mark', { roomId, state }, [caller.uid]);
|
|
messaging.pushUnreadCount(caller.uid);
|
|
};
|
|
|
|
chatsAPI.watch = async (caller, { roomId, state }) => {
|
|
const inRoom = await messaging.isUserInRoom(caller.uid, roomId);
|
|
if (!inRoom) {
|
|
throw new Error('[[error:no-privileges]]');
|
|
}
|
|
|
|
await messaging.setUserNotificationSetting(caller.uid, roomId, state);
|
|
};
|
|
|
|
chatsAPI.toggleTyping = async (caller, { roomId, typing }) => {
|
|
if (!utils.isNumber(roomId) || typeof typing !== 'boolean') {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
|
|
const [isInRoom, username] = await Promise.all([
|
|
messaging.isUserInRoom(caller.uid, roomId),
|
|
user.getUserField(caller.uid, 'username'),
|
|
]);
|
|
if (!isInRoom) {
|
|
throw new Error('[[error:no-privileges]]');
|
|
}
|
|
|
|
websockets.in(`chat_room_${roomId}`).emit('event:chats.typing', {
|
|
uid: caller.uid,
|
|
roomId,
|
|
typing,
|
|
username,
|
|
});
|
|
};
|
|
|
|
chatsAPI.users = async (caller, data) => {
|
|
const start = data.hasOwnProperty('start') ? data.start : 0;
|
|
const stop = start + 39;
|
|
const io = require('../socket.io');
|
|
const [isOwner, isUserInRoom, users, isAdmin, onlineUids] = await Promise.all([
|
|
messaging.isRoomOwner(caller.uid, data.roomId),
|
|
messaging.isUserInRoom(caller.uid, data.roomId),
|
|
messaging.getUsersInRoomFromSet(
|
|
`chat:room:${data.roomId}:uids:online`, data.roomId, start, stop, true
|
|
),
|
|
user.isAdministrator(caller.uid),
|
|
io.getUidsInRoom(`chat_room_${data.roomId}`),
|
|
]);
|
|
if (!isUserInRoom) {
|
|
throw new Error('[[error:no-privileges]]');
|
|
}
|
|
users.forEach((user) => {
|
|
const isSelf = parseInt(user.uid, 10) === parseInt(caller.uid, 10);
|
|
user.canKick = isOwner && !isSelf;
|
|
user.canToggleOwner = utils.isNumber(user.uid) && (isAdmin || isOwner) && !isSelf;
|
|
user.online = parseInt(user.uid, 10) === parseInt(caller.uid, 10) || onlineUids.includes(String(user.uid));
|
|
});
|
|
return { users };
|
|
};
|
|
|
|
chatsAPI.invite = async (caller, data) => {
|
|
if (!data || !data.roomId || !Array.isArray(data.uids)) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
const roomData = await messaging.getRoomData(data.roomId);
|
|
if (!roomData) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
const userCount = await messaging.getUserCountInRoom(data.roomId);
|
|
const maxUsers = meta.config.maximumUsersInChatRoom;
|
|
if (!roomData.public && maxUsers && userCount >= maxUsers) {
|
|
throw new Error('[[error:cant-add-more-users-to-chat-room]]');
|
|
}
|
|
|
|
const uidsExist = await user.exists(data.uids);
|
|
if (!uidsExist.every(Boolean)) {
|
|
throw new Error('[[error:no-user]]');
|
|
}
|
|
await Promise.all(data.uids.map(uid => messaging.canMessageUser(caller.uid, uid)));
|
|
await messaging.addUsersToRoom(caller.uid, data.uids, data.roomId);
|
|
|
|
delete data.uids;
|
|
return chatsAPI.users(caller, data);
|
|
};
|
|
|
|
chatsAPI.kick = async (caller, data) => {
|
|
if (!data || !data.roomId) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
const uidsExist = await user.exists(data.uids);
|
|
if (!uidsExist.every(Boolean)) {
|
|
throw new Error('[[error:no-user]]');
|
|
}
|
|
|
|
// Additional checks if kicking vs leaving
|
|
if (data.uids.length === 1 && parseInt(data.uids[0], 10) === caller.uid) {
|
|
await messaging.leaveRoom([caller.uid], data.roomId);
|
|
await socketHelpers.removeSocketsFromRoomByUids([caller.uid], data.roomId);
|
|
return [];
|
|
}
|
|
await messaging.removeUsersFromRoom(caller.uid, data.uids, data.roomId);
|
|
await socketHelpers.removeSocketsFromRoomByUids(data.uids, data.roomId);
|
|
delete data.uids;
|
|
return chatsAPI.users(caller, data);
|
|
};
|
|
|
|
chatsAPI.toggleOwner = async (caller, { roomId, uid, state }) => {
|
|
const [isAdmin, inRoom, isRoomOwner] = await Promise.all([
|
|
user.isAdministrator(caller.uid),
|
|
messaging.isUserInRoom(caller.uid, roomId),
|
|
messaging.isRoomOwner(caller.uid, roomId),
|
|
]);
|
|
|
|
if (!isAdmin && (!inRoom || !isRoomOwner)) {
|
|
throw new Error('[[error:no-privileges]]');
|
|
}
|
|
|
|
return await messaging.toggleOwner(uid, roomId, state);
|
|
};
|
|
|
|
chatsAPI.listMessages = async (caller, { uid = caller.uid, roomId, start = 0, direction = null } = {}) => {
|
|
if (!roomId) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
|
|
const count = 50;
|
|
let stop = start + count - 1;
|
|
if (direction === 1 || direction === -1) {
|
|
const msgCount = await db.getObjectField(`chat:room:${roomId}`, 'messageCount');
|
|
start = msgCount - start;
|
|
if (direction === 1) {
|
|
start -= count + 1;
|
|
}
|
|
stop = start + count - 1;
|
|
start = Math.max(0, start);
|
|
if (stop <= -1) {
|
|
return { messages: [] };
|
|
}
|
|
stop = Math.max(0, stop);
|
|
}
|
|
|
|
const messages = await messaging.getMessages({
|
|
callerUid: caller.uid,
|
|
uid,
|
|
roomId,
|
|
start,
|
|
count: stop - start + 1,
|
|
});
|
|
|
|
return { messages };
|
|
};
|
|
|
|
chatsAPI.getPinnedMessages = async (caller, { start, roomId }) => {
|
|
start = parseInt(start, 10) || 0;
|
|
const isInRoom = await messaging.isUserInRoom(caller.uid, roomId);
|
|
if (!isInRoom) {
|
|
throw new Error('[[error:no-privileges]]');
|
|
}
|
|
const messages = await messaging.getPinnedMessages(roomId, caller.uid, start, start + 49);
|
|
return { messages };
|
|
};
|
|
|
|
chatsAPI.getMessage = async (caller, { mid, roomId } = {}) => {
|
|
if (!mid || !roomId) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
|
|
const messages = await messaging.getMessagesData([mid], caller.uid, roomId, false);
|
|
return messages.pop();
|
|
};
|
|
|
|
chatsAPI.getRawMessage = async (caller, { mid, roomId } = {}) => {
|
|
if (!mid || !roomId) {
|
|
throw new Error('[[error:invalid-data]]');
|
|
}
|
|
|
|
const [isAdmin, canViewMessage, inRoom] = await Promise.all([
|
|
user.isAdministrator(caller.uid),
|
|
messaging.canViewMessage(mid, roomId, caller.uid),
|
|
messaging.isUserInRoom(caller.uid, roomId),
|
|
]);
|
|
|
|
if (!isAdmin && (!inRoom || !canViewMessage)) {
|
|
throw new Error('[[error:not-allowed]]');
|
|
}
|
|
|
|
const content = await messaging.getMessageField(mid, 'content');
|
|
return { content };
|
|
};
|
|
|
|
chatsAPI.getIpAddress = async (caller, { mid }) => {
|
|
const allowed = await privileges.global.can('view:users:info', caller.uid);
|
|
if (!allowed) {
|
|
throw new Error('[[error:no-privileges]]');
|
|
}
|
|
const ip = await messaging.getMessageField(mid, 'ip');
|
|
return { ip };
|
|
};
|
|
|
|
chatsAPI.editMessage = async (caller, { mid, roomId, message }) => {
|
|
await messaging.canEdit(mid, caller.uid);
|
|
await messaging.editMessage(caller.uid, mid, roomId, message);
|
|
};
|
|
|
|
chatsAPI.deleteMessage = async (caller, { mid }) => {
|
|
await messaging.canDelete(mid, caller.uid);
|
|
await messaging.deleteMessage(mid, caller.uid);
|
|
};
|
|
|
|
chatsAPI.restoreMessage = async (caller, { mid }) => {
|
|
await messaging.canDelete(mid, caller.uid);
|
|
await messaging.restoreMessage(mid, caller.uid);
|
|
};
|
|
|
|
chatsAPI.pinMessage = async (caller, { roomId, mid }) => {
|
|
await messaging.canPin(roomId, caller.uid);
|
|
await messaging.pinMessage(mid, roomId);
|
|
};
|
|
|
|
chatsAPI.unpinMessage = async (caller, { roomId, mid }) => {
|
|
await messaging.canPin(roomId, caller.uid);
|
|
await messaging.unpinMessage(mid, roomId);
|
|
};
|