'use strict'; const winston = require('winston'); const validator = require('validator'); const batch = require('../batch'); const db = require('../database'); const notifications = require('../notifications'); const user = require('../user'); const io = require('../socket.io'); const activitypub = require('../activitypub'); const plugins = require('../plugins'); const utils = require('../utils'); module.exports = function (Messaging) { Messaging.setUserNotificationSetting = async (uid, roomId, value) => { if (parseInt(value, 10) === -1) { // go back to default return await db.deleteObjectField(`chat:room:${roomId}:notification:settings`, uid); } await db.setObjectField(`chat:room:${roomId}:notification:settings`, uid, parseInt(value, 10)); }; Messaging.getUidsNotificationSetting = async (uids, roomId) => { const [settings, roomData] = await Promise.all([ db.getObjectFields(`chat:room:${roomId}:notification:settings`, uids), Messaging.getRoomData(roomId, ['notificationSetting']), ]); return uids.map(uid => parseInt(settings[uid] || roomData.notificationSetting, 10)); }; Messaging.markRoomNotificationsRead = async (uid, roomId) => { const chatNids = await db.getSortedSetScan({ key: `uid:${uid}:notifications:unread`, match: `chat_${roomId}_*`, }); if (chatNids.length) { await notifications.markReadMultiple(chatNids, uid); await user.notifications.pushCount(uid); } }; Messaging.notifyUsersInRoom = async (fromUid, roomId, messageObj) => { const isPublic = parseInt(await db.getObjectField(`chat:room:${roomId}`, 'public'), 10) === 1; let data = { roomId: roomId, fromUid: fromUid, message: messageObj, public: isPublic, }; data = await plugins.hooks.fire('filter:messaging.notify', data); if (!data) { return; } // delivers full message to all online users in roomId io.in(`chat_room_${roomId}`).emit('event:chats.receive', data); const unreadData = { roomId, fromUid, public: isPublic }; if (isPublic && !messageObj.system) { // delivers unread public msg to all online users on the chats page io.in(`chat_room_public_${roomId}`).emit('event:chats.public.unread', unreadData); } if (messageObj.system) { return; } // push unread count only for private rooms if (!isPublic) { const uids = await Messaging.getAllUidsInRoomFromSet(`chat:room:${roomId}:uids:online`); unreadData.teaser = { content: validator.escape( String(utils.stripHTMLTags(utils.decodeHTMLEntities(messageObj.content))) ), user: messageObj.fromUser, timestampISO: messageObj.timestampISO, }; Messaging.pushUnreadCount(uids, unreadData); } try { await Promise.all([ sendNotification(fromUid, roomId, messageObj), !isPublic && utils.isNumber(fromUid) ? activitypub.out.create.privateNote(messageObj) : null, ]); } catch (err) { winston.error(`[messaging/notifications] Unabled to send notification\n${err.stack}`); } }; async function sendNotification(fromUid, roomId, messageObj) { const [settings, roomData, realtimeUids] = await Promise.all([ db.getObject(`chat:room:${roomId}:notification:settings`), Messaging.getRoomData(roomId), io.getUidsInRoom(`chat_room_${roomId}`), ]); const roomDefault = roomData.notificationSetting; const uidsToNotify = []; const { ALLMESSAGES } = Messaging.notificationSettings; await batch.processSortedSet(`chat:room:${roomId}:uids:online`, async (uids) => { uids = uids.filter( uid => utils.isNumber(uid) && (parseInt((settings && settings[uid]) || roomDefault, 10) === ALLMESSAGES) && String(fromUid) !== String(uid) && !realtimeUids.includes(parseInt(uid, 10)) ); const hasRead = await Messaging.hasRead(uids, roomId); uidsToNotify.push(...uids.filter((uid, index) => !hasRead[index])); }, { reverse: true, batch: 500, interval: 100, }); if (uidsToNotify.length) { const { displayname } = messageObj.fromUser; const isGroupChat = await Messaging.isGroupChat(roomId); const roomName = roomData.roomName || `[[modules:chat.room-id, ${roomId}]]`; const notifData = { type: isGroupChat ? 'new-group-chat' : 'new-chat', subject: roomData.roomName ? `[[email:notif.chat.new-message-from-user-in-room, ${displayname}, ${roomName}]]` : `[[email:notif.chat.new-message-from-user, ${displayname}]]`, bodyShort: isGroupChat || roomData.roomName ? `[[notifications:new-message-in, ${roomName}]]` : `[[notifications:new-message-from, ${displayname}]]`, bodyLong: messageObj.content, nid: `chat_${roomId}_${fromUid}_${Date.now()}`, mergeId: `new-chat|${roomId}`, // as roomId is the differentiator, no distinction between direct vs. group req'd. from: fromUid, roomId, roomName, path: `/chats/${messageObj.roomId}`, }; if (roomData.public) { const icon = Messaging.getRoomIcon(roomData); notifData.type = 'new-public-chat'; notifData.roomIcon = icon; notifData.subject = `[[email:notif.chat.new-message-from-user-in-room, ${displayname}, ${roomName}]]`; notifData.bodyShort = `[[notifications:user-posted-in-public-room, ${displayname}, ${icon}, ${roomName}]]`; notifData.mergeId = `notifications:user-posted-in-public-room|${roomId}`; } const notification = await notifications.create(notifData); await notifications.push(notification, uidsToNotify); } } };