'use strict'; const validator = require('validator'); const db = require('../database'); const meta = require('../meta'); const notifications = require('../notifications'); const plugins = require('../plugins'); const Messaging = require('../messaging'); const utils = require('../utils'); const server = require('./'); const user = require('../user'); const privileges = require('../privileges'); const SocketModules = module.exports; SocketModules.chats = {}; SocketModules.sounds = {}; SocketModules.settings = {}; /* Chat */ SocketModules.chats.getRaw = async function (socket, data) { if (!data || !data.hasOwnProperty('mid')) { throw new Error('[[error:invalid-data]]'); } const roomId = await Messaging.getMessageField(data.mid, 'roomId'); const [isAdmin, hasMessage, inRoom] = await Promise.all([ user.isAdministrator(socket.uid), db.isSortedSetMember('uid:' + socket.uid + ':chat:room:' + roomId + ':mids', data.mid), Messaging.isUserInRoom(socket.uid, roomId), ]); if (!isAdmin && (!inRoom || !hasMessage)) { throw new Error('[[error:not-allowed]]'); } return await Messaging.getMessageField(data.mid, 'content'); }; SocketModules.chats.isDnD = async function (socket, uid) { const status = await db.getObjectField('user:' + uid, 'status'); return status === 'dnd'; }; SocketModules.chats.newRoom = async function (socket, data) { if (!data) { throw new Error('[[error:invalid-data]]'); } if (rateLimitExceeded(socket)) { throw new Error('[[error:too-many-messages]]'); } const canChat = await privileges.global.can('chat', socket.uid); if (!canChat) { throw new Error('[[error:no-privileges]]'); } await Messaging.canMessageUser(socket.uid, data.touid); return await Messaging.newRoom(socket.uid, [data.touid]); }; SocketModules.chats.send = async function (socket, data) { if (!data || !data.roomId || !socket.uid) { throw new Error('[[error:invalid-data]]'); } if (rateLimitExceeded(socket)) { throw new Error('[[error:too-many-messages]]'); } const canChat = await privileges.global.can('chat', socket.uid); if (!canChat) { throw new Error('[[error:no-privileges]]'); } const results = await plugins.fireHook('filter:messaging.send', { data: data, uid: socket.uid, }); data = results.data; await Messaging.canMessageRoom(socket.uid, data.roomId); const message = await Messaging.sendMessage({ uid: socket.uid, roomId: data.roomId, content: data.message, timestamp: Date.now(), ip: socket.ip, }); Messaging.notifyUsersInRoom(socket.uid, data.roomId, message); user.updateOnlineUsers(socket.uid); return message; }; function rateLimitExceeded(socket) { const now = Date.now(); socket.lastChatMessageTime = socket.lastChatMessageTime || 0; if (now - socket.lastChatMessageTime < meta.config.chatMessageDelay) { return true; } socket.lastChatMessageTime = now; return false; } SocketModules.chats.loadRoom = async function (socket, data) { if (!data || !data.roomId) { throw new Error('[[error:invalid-data]]'); } return await Messaging.loadRoom(socket.uid, data); }; SocketModules.chats.getUsersInRoom = async function (socket, data) { if (!data || !data.roomId) { throw new Error('[[error:invalid-data]]'); } const [userData, isOwner] = await Promise.all([ Messaging.getUsersInRoom(data.roomId, 0, -1), Messaging.isRoomOwner(socket.uid, data.roomId), ]); userData.forEach((user) => { user.canKick = (parseInt(user.uid, 10) !== parseInt(socket.uid, 10)) && isOwner; }); return userData; }; SocketModules.chats.addUserToRoom = async function (socket, data) { if (!data || !data.roomId || !data.username) { throw new Error('[[error:invalid-data]]'); } const canChat = await privileges.global.can('chat', socket.uid); if (!canChat) { throw new Error('[[error:no-privileges]]'); } const userCount = await Messaging.getUserCountInRoom(data.roomId); const maxUsers = meta.config.maximumUsersInChatRoom; if (maxUsers && userCount >= maxUsers) { throw new Error('[[error:cant-add-more-users-to-chat-room]]'); } const uid = await user.getUidByUsername(data.username); if (!uid) { throw new Error('[[error:no-user]]'); } if (socket.uid === parseInt(uid, 10)) { throw new Error('[[error:cant-chat-with-yourself]]'); } const [settings, isAdminOrGlobalMod, isFollowing] = await Promise.all([ user.getSettings(uid), user.isAdminOrGlobalMod(socket.uid), user.isFollowing(uid, socket.uid), ]); if (settings.restrictChat && !isAdminOrGlobalMod && !isFollowing) { throw new Error('[[error:chat-restricted]]'); } await Messaging.addUsersToRoom(socket.uid, [uid], data.roomId); }; SocketModules.chats.removeUserFromRoom = async function (socket, data) { if (!data || !data.roomId) { throw new Error('[[error:invalid-data]]'); } const exists = await user.exists(data.uid); if (!exists) { throw new Error('[[error:no-user]]'); } await Messaging.removeUsersFromRoom(socket.uid, [data.uid], data.roomId); }; SocketModules.chats.leave = async function (socket, roomid) { if (!socket.uid || !roomid) { throw new Error('[[error:invalid-data]]'); } await Messaging.leaveRoom([socket.uid], roomid); }; SocketModules.chats.edit = async function (socket, data) { if (!data || !data.roomId || !data.message) { throw new Error('[[error:invalid-data]]'); } await Messaging.canEdit(data.mid, socket.uid); await Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message); }; SocketModules.chats.delete = async function (socket, data) { if (!data || !data.roomId || !data.messageId) { throw new Error('[[error:invalid-data]]'); } await Messaging.canDelete(data.messageId, socket.uid); await Messaging.deleteMessage(data.messageId); }; SocketModules.chats.restore = async function (socket, data) { if (!data || !data.roomId || !data.messageId) { throw new Error('[[error:invalid-data]]'); } await Messaging.canDelete(data.messageId, socket.uid); await Messaging.restoreMessage(data.messageId); }; SocketModules.chats.canMessage = async function (socket, roomId) { await Messaging.canMessageRoom(socket.uid, roomId); }; SocketModules.chats.markRead = async function (socket, roomId) { if (!socket.uid || !roomId) { throw new Error('[[error:invalid-data]]'); } const [uidsInRoom] = await Promise.all([ Messaging.getUidsInRoom(roomId, 0, -1), Messaging.markRead(socket.uid, roomId), ]); Messaging.pushUnreadCount(socket.uid); server.in('uid_' + socket.uid).emit('event:chats.markedAsRead', { roomId: roomId }); if (!uidsInRoom.includes(String(socket.uid))) { return; } // Mark notification read const nids = uidsInRoom.filter(uid => parseInt(uid, 10) !== socket.uid) .map(uid => 'chat_' + uid + '_' + roomId); await notifications.markReadMultiple(nids, socket.uid); await user.notifications.pushCount(socket.uid); }; SocketModules.chats.markAllRead = async function (socket) { await Messaging.markAllRead(socket.uid); Messaging.pushUnreadCount(socket.uid); }; SocketModules.chats.renameRoom = async function (socket, data) { if (!data || !data.roomId || !data.newName) { throw new Error('[[error:invalid-data]]'); } await Messaging.renameRoom(socket.uid, data.roomId, data.newName); const uids = await Messaging.getUidsInRoom(data.roomId, 0, -1); const eventData = { roomId: data.roomId, newName: validator.escape(String(data.newName)) }; uids.forEach(function (uid) { server.in('uid_' + uid).emit('event:chats.roomRename', eventData); }); }; SocketModules.chats.getRecentChats = async function (socket, data) { if (!data || !utils.isNumber(data.after) || !utils.isNumber(data.uid)) { throw new Error('[[error:invalid-data]]'); } const start = parseInt(data.after, 10); const stop = start + 9; return await Messaging.getRecentChats(socket.uid, data.uid, start, stop); }; SocketModules.chats.hasPrivateChat = async function (socket, uid) { if (socket.uid <= 0 || uid <= 0) { throw new Error('[[error:invalid-data]]'); } return await Messaging.hasPrivateChat(socket.uid, uid); }; SocketModules.chats.getMessages = async function (socket, data) { if (!socket.uid || !data || !data.uid || !data.roomId) { throw new Error('[[error:invalid-data]]'); } return await Messaging.getMessages({ callerUid: socket.uid, uid: data.uid, roomId: data.roomId, start: parseInt(data.start, 10) || 0, count: 50, }); }; SocketModules.chats.getIP = async function (socket, mid) { const allowed = await privileges.global.can('view:users:info', socket.uid); if (!allowed) { throw new Error('[[error:no-privilege]]'); } return await Messaging.getMessageField(mid, 'ip'); }; /* Sounds */ SocketModules.sounds.getUserSoundMap = async function getUserSoundMap(socket) { return await meta.sounds.getUserSoundMap(socket.uid); }; require('../promisify')(SocketModules);