diff --git a/packages/rocketchat-lib/lib/getUserNotificationPreference.js b/packages/rocketchat-lib/lib/getUserNotificationPreference.js new file mode 100644 index 000000000000..27ae25337cc0 --- /dev/null +++ b/packages/rocketchat-lib/lib/getUserNotificationPreference.js @@ -0,0 +1,28 @@ +RocketChat.getUserNotificationPreference = function _getUserNotificationPreference(user, pref) { + if (typeof user === 'string') { + user = RocketChat.models.Users.findOneById(user); + } + + let preferenceKey; + switch (pref) { + case 'desktop': preferenceKey = 'desktopNotifications'; break; + case 'mobile': preferenceKey = 'mobileNotifications'; break; + case 'email': preferenceKey = 'emailNotificationMode'; break; + } + + if (user && user.settings && user.settings.preferences && user.settings.preferences[preferenceKey] !== 'default') { + return { + value: user.settings.preferences[preferenceKey], + origin: 'user' + }; + } + const serverValue = RocketChat.settings.get(`Accounts_Default_User_Preferences_${ preferenceKey }`); + if (serverValue) { + return { + value: serverValue, + origin: 'server' + }; + } + + return null; +}; diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 9f3ba06887f2..cfc092292f67 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -68,6 +68,7 @@ Package.onUse(function(api) { api.addFiles('lib/MessageTypes.js'); api.addFiles('lib/templateVarHandler.js'); + api.addFiles('lib/getUserNotificationPreference.js'); api.addFiles('lib/getUserPreference.js'); api.addFiles('server/lib/bugsnag.js', 'server'); @@ -112,7 +113,6 @@ Package.onUse(function(api) { api.addFiles('server/lib/notifyUsersOnMessage.js', 'server'); api.addFiles('server/lib/processDirectEmail.js', 'server'); api.addFiles('server/lib/roomTypes.js', 'server'); - api.addFiles('server/lib/sendEmailOnMessage.js', 'server'); api.addFiles('server/lib/sendNotificationsOnMessage.js', 'server'); api.addFiles('server/lib/validateEmailDomain.js', 'server'); diff --git a/packages/rocketchat-lib/server/functions/createRoom.js b/packages/rocketchat-lib/server/functions/createRoom.js index 4fa617950ee8..d028294354b0 100644 --- a/packages/rocketchat-lib/server/functions/createRoom.js +++ b/packages/rocketchat-lib/server/functions/createRoom.js @@ -64,7 +64,7 @@ RocketChat.createRoom = function(type, name, owner, members, readOnly, extraData room = RocketChat.models.Rooms.createWithFullRoomData(room); for (const username of members) { - const member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }}); + const member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1, 'settings.preferences': 1 }}); const isTheOwner = username === owner.username; if (!member) { continue; diff --git a/packages/rocketchat-lib/server/functions/notifications/audio.js b/packages/rocketchat-lib/server/functions/notifications/audio.js new file mode 100644 index 000000000000..1c45b18f336a --- /dev/null +++ b/packages/rocketchat-lib/server/functions/notifications/audio.js @@ -0,0 +1,35 @@ +export function shouldNotifyAudio({ + disableAllMessageNotifications, + status, + audioNotifications, + hasMentionToAll, + hasMentionToHere, + isHighlighted, + hasMentionToUser +}) { + if (disableAllMessageNotifications && audioNotifications == null) { + return false; + } + + if (status === 'busy' || audioNotifications === 'nothing') { + return false; + } + + if (!audioNotifications && RocketChat.settings.get('Accounts_Default_User_Preferences_audioNotifications') === 'all') { + return true; + } + + return (!disableAllMessageNotifications && (hasMentionToAll || hasMentionToHere)) || isHighlighted || audioNotifications === 'all' || hasMentionToUser; +} + +export function notifyAudioUser(userId, message, room) { + RocketChat.Notifications.notifyUser(userId, 'audioNotification', { + payload: { + _id: message._id, + rid: message.rid, + sender: message.u, + type: room.t, + name: room.name + } + }); +} diff --git a/packages/rocketchat-lib/server/functions/notifications/desktop.js b/packages/rocketchat-lib/server/functions/notifications/desktop.js new file mode 100644 index 000000000000..a520c216ea4f --- /dev/null +++ b/packages/rocketchat-lib/server/functions/notifications/desktop.js @@ -0,0 +1,94 @@ +import { parseMessageText } from './index'; + +/** + * Replaces @username with full name + * + * @param {string} message The message to replace + * @param {object[]} mentions Array of mentions used to make replacements + * + * @returns {string} + */ +function replaceMentionedUsernamesWithFullNames(message, mentions) { + if (!mentions || !mentions.length) { + return message; + } + mentions.forEach((mention) => { + const user = RocketChat.models.Users.findOneById(mention._id); + if (user && user.name) { + message = message.replace(`@${ mention.username }`, user.name); + } + }); + return message; +} + +/** + * Send notification to user + * + * @param {string} userId The user to notify + * @param {object} user The sender + * @param {object} room The room send from + * @param {number} duration Duration of notification + */ +export function notifyDesktopUser(userId, user, message, room, duration) { + + const UI_Use_Real_Name = RocketChat.settings.get('UI_Use_Real_Name') === true; + message.msg = parseMessageText(message, userId); + + if (UI_Use_Real_Name) { + message.msg = replaceMentionedUsernamesWithFullNames(message.msg, message.mentions); + } + + let title = ''; + let text = ''; + if (room.t === 'd') { + title = UI_Use_Real_Name ? user.name : `@${ user.username }`; + text = message.msg; + } else if (room.name) { + title = `#${ room.name }`; + text = `${ UI_Use_Real_Name ? user.name : user.username }: ${ message.msg }`; + } else { + return; + } + + RocketChat.Notifications.notifyUser(userId, 'notification', { + title, + text, + duration, + payload: { + _id: message._id, + rid: message.rid, + sender: message.u, + type: room.t, + name: room.name + } + }); +} + +export function shouldNotifyDesktop({ + disableAllMessageNotifications, + status, + desktopNotifications, + hasMentionToAll, + hasMentionToHere, + isHighlighted, + hasMentionToUser +}) { + if (disableAllMessageNotifications && desktopNotifications == null) { + return false; + } + + if (status === 'busy' || desktopNotifications === 'nothing') { + return false; + } + + if (!desktopNotifications) { + if (RocketChat.settings.get('Accounts_Default_User_Preferences_desktopNotifications') === 'all') { + return true; + } + if (RocketChat.settings.get('Accounts_Default_User_Preferences_desktopNotifications') === 'nothing') { + return false; + } + } + + return (!disableAllMessageNotifications && (hasMentionToAll || hasMentionToHere)) || isHighlighted || desktopNotifications === 'all' || hasMentionToUser; +} diff --git a/packages/rocketchat-lib/server/functions/notifications/email.js b/packages/rocketchat-lib/server/functions/notifications/email.js new file mode 100644 index 000000000000..e0a887855cf5 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/notifications/email.js @@ -0,0 +1,176 @@ +import s from 'underscore.string'; + +let contentHeader; +RocketChat.settings.get('Email_Header', (key, value) => { + contentHeader = RocketChat.placeholders.replace(value || ''); +}); + +let contentFooter; +RocketChat.settings.get('Email_Footer', (key, value) => { + contentFooter = RocketChat.placeholders.replace(value || ''); +}); + +const divisorMessage = '
'; + +function getEmailContent({ message, user, room }) { + const lng = user && user.language || RocketChat.settings.get('language') || 'en'; + + const roomName = s.escapeHTML(`#${ RocketChat.roomTypes.getRoomName(room.t, room) }`); + const userName = s.escapeHTML(RocketChat.settings.get('UI_Use_Real_Name') ? message.u.name || message.u.username : message.u.username); + + const header = TAPi18n.__(room.t === 'd' ? 'User_sent_a_message_to_you' : 'User_sent_a_message_on_channel', { + username: userName, + channel: roomName, + lng + }); + + if (message.msg !== '') { + let messageContent = s.escapeHTML(message.msg); + message = RocketChat.callbacks.run('renderMessage', message); + if (message.tokens && message.tokens.length > 0) { + message.tokens.forEach((token) => { + token.text = token.text.replace(/([^\$])(\$[^\$])/gm, '$1$$$2'); + messageContent = messageContent.replace(token.token, token.text); + }); + } + return `${ header }

${ messageContent.replace(/\n/gm, '
') }`; + } + + if (message.file) { + const fileHeader = TAPi18n.__(room.t === 'd' ? 'User_uploaded_a_file_to_you' : 'User_uploaded_a_file_on_channel', { + username: userName, + channel: roomName, + lng + }); + + let content = `${ TAPi18n.__('Attachment_File_Uploaded') }: ${ s.escapeHTML(message.file.name) }`; + + if (message.attachments && message.attachments.length === 1 && message.attachments[0].description !== '') { + content += `

${ s.escapeHTML(message.attachments[0].description) }`; + } + + return `${ fileHeader }

${ content }`; + } + + if (message.attachments.length > 0) { + const [ attachment ] = message.attachments; + + let content = ''; + + if (attachment.title) { + content += `${ s.escapeHTML(attachment.title) }
`; + } + if (attachment.text) { + content += `${ s.escapeHTML(attachment.text) }
`; + } + + return `${ header }

${ content }`; + } + + return header; +} + +function getMessageLink(room, sub) { + const roomPath = RocketChat.roomTypes.getRouteLink(room.t, sub); + const path = Meteor.absoluteUrl(roomPath ? roomPath.replace(/^\//, '') : ''); + const style = [ + 'color: #fff;', + 'padding: 9px 12px;', + 'border-radius: 4px;', + 'background-color: #04436a;', + 'text-decoration: none;' + ].join(' '); + const message = TAPi18n.__('Offline_Link_Message'); + return `

${ message }`; +} + +export function sendEmail({ message, user, subscription, room, emailAddress, toAll }) { + let emailSubject; + const username = RocketChat.settings.get('UI_Use_Real_Name') ? message.u.name : message.u.username; + + if (room.t === 'd') { + emailSubject = RocketChat.placeholders.replace(RocketChat.settings.get('Offline_DM_Email'), { + user: username, + room: RocketChat.roomTypes.getRoomName(room.t, room) + }); + } else if (toAll) { + emailSubject = RocketChat.placeholders.replace(RocketChat.settings.get('Offline_Mention_All_Email'), { + user: username, + room: RocketChat.roomTypes.getRoomName(room.t, room) + }); + } else { + emailSubject = RocketChat.placeholders.replace(RocketChat.settings.get('Offline_Mention_Email'), { + user: username, + room: RocketChat.roomTypes.getRoomName(room.t, room) + }); + } + const content = getEmailContent({ + message, + user, + room + }); + + const link = getMessageLink(room, subscription); + + if (RocketChat.settings.get('Direct_Reply_Enable')) { + contentFooter = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Footer_Direct_Reply') || ''); + } + + const email = { + to: emailAddress, + subject: emailSubject, + html: contentHeader + content + divisorMessage + link + contentFooter + }; + + // using user full-name/channel name in from address + if (room.t === 'd') { + email.from = `${ String(message.u.name).replace(/@/g, '%40').replace(/[<>,]/g, '') } <${ RocketChat.settings.get('From_Email') }>`; + } else { + email.from = `${ String(room.name).replace(/@/g, '%40').replace(/[<>,]/g, '') } <${ RocketChat.settings.get('From_Email') }>`; + } + // If direct reply enabled, email content with headers + if (RocketChat.settings.get('Direct_Reply_Enable')) { + email.headers = { + // Reply-To header with format "username+messageId@domain" + 'Reply-To': `${ RocketChat.settings.get('Direct_Reply_Username').split('@')[0].split(RocketChat.settings.get('Direct_Reply_Separator'))[0] }${ RocketChat.settings.get('Direct_Reply_Separator') }${ message._id }@${ RocketChat.settings.get('Direct_Reply_Username').split('@')[1] }` + }; + } + + Meteor.defer(() => { + Email.send(email); + }); +} + +export function shouldNotifyEmail({ + disableAllMessageNotifications, + statusConnection, + emailNotifications, + isHighlighted, + hasMentionToUser, + hasMentionToAll +}) { + + // use connected (don't need to send him an email) + if (statusConnection === 'online') { + return false; + } + + // user/room preference to nothing + if (emailNotifications === 'nothing') { + return false; + } + + // no user or room preference + if (emailNotifications == null) { + if (disableAllMessageNotifications) { + return false; + } + + // default server preference is disabled + if (RocketChat.settings.get('Accounts_Default_User_Preferences_emailNotificationMode') === 'disabled') { + return false; + } + } + + return isHighlighted || emailNotifications === 'all' || hasMentionToUser || (!disableAllMessageNotifications && hasMentionToAll); +} diff --git a/packages/rocketchat-lib/server/functions/notifications/index.js b/packages/rocketchat-lib/server/functions/notifications/index.js new file mode 100644 index 000000000000..d9f6e74d65bb --- /dev/null +++ b/packages/rocketchat-lib/server/functions/notifications/index.js @@ -0,0 +1,46 @@ +import s from 'underscore.string'; + +/** +* This function returns a string ready to be shown in the notification +* +* @param {object} message the message to be parsed +*/ +export function parseMessageText(message, userId) { + const user = RocketChat.models.Users.findOneById(userId); + const lng = user && user.language || RocketChat.settings.get('language') || 'en'; + + if (!message.msg && message.attachments && message.attachments[0]) { + message.msg = message.attachments[0].image_type ? TAPi18n.__('User_uploaded_image', {lng}) : TAPi18n.__('User_uploaded_file', {lng}); + } + message.msg = RocketChat.callbacks.run('beforeNotifyUser', message.msg); + + return message.msg; +} + +/** + * Checks if a message contains a user highlight + * + * @param {string} message + * @param {array|undefined} highlights + * + * @returns {boolean} + */ +export function messageContainsHighlight(message, highlights) { + if (! highlights || highlights.length === 0) { return false; } + + return highlights.some(function(highlight) { + const regexp = new RegExp(s.escapeRegExp(highlight), 'i'); + return regexp.test(message.msg); + }); +} + +export function callJoinRoom(user, rid) { + return new Promise((resolve, reject) => { + Meteor.runAsUser(user._id, () => Meteor.call('joinRoom', rid, (error, result) => { + if (error) { + return reject(error); + } + return resolve(result); + })); + }); +} diff --git a/packages/rocketchat-lib/server/functions/notifications/mobile.js b/packages/rocketchat-lib/server/functions/notifications/mobile.js new file mode 100644 index 000000000000..820839b3fad9 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/notifications/mobile.js @@ -0,0 +1,74 @@ +import { parseMessageText } from './index'; + +const CATEGORY_MESSAGE = 'MESSAGE'; +const CATEGORY_MESSAGE_NOREPLY = 'MESSAGE_NOREPLY'; + +let alwaysNotifyMobileBoolean; +RocketChat.settings.get('Notifications_Always_Notify_Mobile', (key, value) => { + alwaysNotifyMobileBoolean = value; +}); + +// function getBadgeCount(userId) { +// const subscriptions = RocketChat.models.Subscriptions.findUnreadByUserId(userId).fetch(); + +// return subscriptions.reduce((unread, sub) => { +// return sub.unread + unread; +// }, 0); +// } + +function canSendMessageToRoom(room, username) { + return !((room.muted || []).includes(username)); +} + +export function sendSinglePush({ room, message, userId, receiverUsername, senderUsername }) { + RocketChat.PushNotification.send({ + roomId: message.rid, + payload: { + host: Meteor.absoluteUrl(), + rid: message.rid, + sender: message.u, + type: room.t, + name: room.name + }, + roomName: RocketChat.settings.get('Push_show_username_room') ? `#${ RocketChat.roomTypes.getRoomName(room.t, room) }` : '', + username: RocketChat.settings.get('Push_show_username_room') ? senderUsername : '', + message: RocketChat.settings.get('Push_show_message') ? parseMessageText(message, userId) : ' ', + // badge: getBadgeCount(userIdToNotify), + usersTo: { + userId + }, + category: canSendMessageToRoom(room, receiverUsername) ? CATEGORY_MESSAGE : CATEGORY_MESSAGE_NOREPLY + }); +} + +export function shouldNotifyMobile({ + disableAllMessageNotifications, + mobilePushNotifications, + hasMentionToAll, + isHighlighted, + hasMentionToUser, + statusConnection +}) { + if (disableAllMessageNotifications && mobilePushNotifications == null) { + return false; + } + + if (mobilePushNotifications === 'nothing') { + return false; + } + + if (!alwaysNotifyMobileBoolean && statusConnection === 'online') { + return false; + } + + if (!mobilePushNotifications) { + if (RocketChat.settings.get('Accounts_Default_User_Preferences_mobileNotifications') === 'all') { + return true; + } + if (RocketChat.settings.get('Accounts_Default_User_Preferences_mobileNotifications') === 'nothing') { + return false; + } + } + + return (!disableAllMessageNotifications && hasMentionToAll) || isHighlighted || mobilePushNotifications === 'all' || hasMentionToUser; +} diff --git a/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js b/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js index f673c56ec103..45502db7b461 100644 --- a/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js +++ b/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js @@ -2,7 +2,25 @@ import _ from 'underscore'; import s from 'underscore.string'; import moment from 'moment'; -RocketChat.callbacks.add('afterSaveMessage', function(message, room) { +/** + * Chechs if a messages contains a user highlight + * + * @param {string} message + * @param {array|undefined} highlights + * + * @returns {boolean} + */ + +export function messageContainsHighlight(message, highlights) { + if (! highlights || highlights.length === 0) { return false; } + + return highlights.some(function(highlight) { + const regexp = new RegExp(s.escapeRegExp(highlight), 'i'); + return regexp.test(message.msg); + }); +} + +function notifyUsersOnMessage(message, room) { // skips this callback if the message was edited and increments it if the edit was way in the past (aka imported) if (message.editedAt && Math.abs(moment(message.editedAt).diff()) > 60000) { //TODO: Review as I am not sure how else to get around this as the incrementing of the msgs count shouldn't be in this callback @@ -22,29 +40,6 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { return message; } - /** - * Chechs if a messages contains a user highlight - * - * @param {string} message - * @param {array|undefined} highlights - * - * @returns {boolean} - */ - function messageContainsHighlight(message, highlights) { - if (! highlights || highlights.length === 0) { return false; } - - let has = false; - highlights.some(function(highlight) { - const regexp = new RegExp(s.escapeRegExp(highlight), 'i'); - if (regexp.test(message.msg)) { - has = true; - return true; - } - }); - - return has; - } - if (room != null) { let toAll = false; let toHere = false; @@ -94,6 +89,7 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { incUnread = 1; } RocketChat.models.Subscriptions.incGroupMentionsAndUnreadForRoomIdExcludingUserId(room._id, message.u._id, 1, incUnread); + } else if ((mentionIds && mentionIds.length > 0) || (highlightsIds && highlightsIds.length > 0)) { let incUnread = 0; if (['all_messages', 'user_mentions_only', 'user_and_group_mentions_only'].includes(unreadCount)) { @@ -111,8 +107,11 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { // Update all other subscriptions to alert their owners but witout incrementing // the unread counter, as it is only for mentions and direct messages + // We now set alert and open properties in two separate update commands. This proved to be more efficient on MongoDB - because it uses a more efficient index. RocketChat.models.Subscriptions.setAlertForRoomIdExcludingUserId(message.rid, message.u._id); + RocketChat.models.Subscriptions.setOpenForRoomIdExcludingUserId(message.rid, message.u._id); return message; +} -}, RocketChat.callbacks.priority.LOW, 'notifyUsersOnMessage'); +RocketChat.callbacks.add('afterSaveMessage', notifyUsersOnMessage, RocketChat.callbacks.priority.LOW, 'notifyUsersOnMessage'); diff --git a/packages/rocketchat-lib/server/lib/roomTypes.js b/packages/rocketchat-lib/server/lib/roomTypes.js index 68aec9b5ef6f..e6ee83c8bf70 100644 --- a/packages/rocketchat-lib/server/lib/roomTypes.js +++ b/packages/rocketchat-lib/server/lib/roomTypes.js @@ -31,6 +31,9 @@ RocketChat.roomTypes = new class roomTypesServer extends RoomTypesCommon { return this.roomTypes[roomType] && this.roomTypes[roomType].roomFind; } + getRoomName(roomType, roomData) { + return this.roomTypes[roomType] && this.roomTypes[roomType].roomName && this.roomTypes[roomType].roomName(roomData); + } /** * Run the publish for a room type diff --git a/packages/rocketchat-lib/server/lib/sendEmailOnMessage.js b/packages/rocketchat-lib/server/lib/sendEmailOnMessage.js deleted file mode 100644 index 860f2232860a..000000000000 --- a/packages/rocketchat-lib/server/lib/sendEmailOnMessage.js +++ /dev/null @@ -1,269 +0,0 @@ -import moment from 'moment'; -import s from 'underscore.string'; - -function getEmailContent({ messageContent, message, user, room }) { - const lng = user && user.language || RocketChat.settings.get('language') || 'en'; - - const roomName = s.escapeHTML(`#${ RocketChat.settings.get('UI_Allow_room_names_with_special_chars') ? room.fname || room.name : room.name }`); - - const userName = s.escapeHTML(RocketChat.settings.get('UI_Use_Real_Name') ? message.u.name || message.u.username : message.u.username); - - const header = TAPi18n.__(room.t === 'd' ? 'User_sent_a_message_to_you' : 'User_sent_a_message_on_channel', { - username: userName, - channel: roomName, - lng - }); - - if (messageContent) { - return `${ header }

${ messageContent }`; - } - - if (message.file) { - const fileHeader = TAPi18n.__(room.t === 'd' ? 'User_uploaded_a_file_to_you' : 'User_uploaded_a_file_on_channel', { - username: userName, - channel: roomName, - lng - }); - - let content = `${ TAPi18n.__('Attachment_File_Uploaded') }: ${ s.escapeHTML(message.file.name) }`; - - if (message.attachments && message.attachments.length === 1 && message.attachments[0].description !== '') { - content += `

${ s.escapeHTML(message.attachments[0].description) }`; - } - - return `${ fileHeader }

${ content }`; - } - - if (message.attachments.length > 0) { - const [ attachment ] = message.attachments; - - let content = ''; - - if (attachment.title) { - content += `${ s.escapeHTML(attachment.title) }
`; - } - if (attachment.text) { - content += `${ s.escapeHTML(attachment.text) }
`; - } - - return `${ header }

${ content }`; - } - - return header; -} - -RocketChat.callbacks.add('afterSaveMessage', function(message, room) { - // skips this callback if the message was edited - if (message.editedAt) { - return message; - } - - if (message.ts && Math.abs(moment(message.ts).diff()) > 60000) { - return message; - } - - const getMessageLink = (room, sub) => { - const roomPath = RocketChat.roomTypes.getRouteLink(room.t, sub); - const path = Meteor.absoluteUrl(roomPath ? roomPath.replace(/^\//, '') : ''); - const style = [ - 'color: #fff;', - 'padding: 9px 12px;', - 'border-radius: 4px;', - 'background-color: #04436a;', - 'text-decoration: none;' - ].join(' '); - const message = TAPi18n.__('Offline_Link_Message'); - return `

${ message }`; - }; - - const divisorMessage = '


'; - - let messageHTML; - - if (message.msg !== '') { - messageHTML = s.escapeHTML(message.msg); - message = RocketChat.callbacks.run('renderMessage', message); - if (message.tokens && message.tokens.length > 0) { - message.tokens.forEach((token) => { - token.text = token.text.replace(/([^\$])(\$[^\$])/gm, '$1$$$2'); - messageHTML = messageHTML.replace(token.token, token.text); - }); - } - messageHTML = messageHTML.replace(/\n/gm, '
'); - } - - const header = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Header') || ''); - let footer = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Footer') || ''); - - const usersToSendEmail = {}; - if (room.t === 'd') { - usersToSendEmail[message.rid.replace(message.u._id, '')] = 'direct'; - } else { - let isMentionAll = message.mentions.find(mention => mention._id === 'all'); - - if (isMentionAll) { - const maxMembersForNotification = RocketChat.settings.get('Notifications_Max_Room_Members'); - if (maxMembersForNotification !== 0 && room.usernames.length > maxMembersForNotification) { - isMentionAll = undefined; - } - } - - let query; - if (isMentionAll) { - // Query all users in room limited by the max room members setting - query = RocketChat.models.Subscriptions.findByRoomId(room._id); - } else { - // Query only mentioned users, will be always a few users - const userIds = message.mentions.map(mention => mention._id); - query = RocketChat.models.Subscriptions.findByRoomIdAndUserIdsOrAllMessages(room._id, userIds); - } - - query.forEach(sub => { - if (sub.disableNotifications) { - return delete usersToSendEmail[sub.u._id]; - } - - const { emailNotifications, muteGroupMentions } = sub; - - if (emailNotifications === 'nothing') { - return delete usersToSendEmail[sub.u._id]; - } - - if (isMentionAll && muteGroupMentions) { - return delete usersToSendEmail[sub.u._id]; - } - - const mentionedUser = isMentionAll || message.mentions.find(mention => mention._id === sub.u._id); - - if (emailNotifications === 'default' || emailNotifications == null) { - if (mentionedUser) { - return usersToSendEmail[sub.u._id] = 'default'; - } - return delete usersToSendEmail[sub.u._id]; - } - - if (emailNotifications === 'mentions' && mentionedUser) { - return usersToSendEmail[sub.u._id] = 'mention'; - } - - if (emailNotifications === 'all') { - return usersToSendEmail[sub.u._id] = 'all'; - } - }); - } - const userIdsToSendEmail = Object.keys(usersToSendEmail); - - let defaultLink; - - const linkByUser = {}; - if (RocketChat.roomTypes.hasCustomLink(room.t)) { - RocketChat.models.Subscriptions.findByRoomIdAndUserIds(room._id, userIdsToSendEmail).forEach((sub) => { - linkByUser[sub.u._id] = getMessageLink(room, sub); - }); - } else { - defaultLink = getMessageLink(room, { - name: room.name - }); - } - - if (userIdsToSendEmail.length > 0) { - const usersOfMention = RocketChat.models.Users.getUsersToSendOfflineEmail(userIdsToSendEmail).fetch(); - - if (usersOfMention && usersOfMention.length > 0) { - usersOfMention.forEach((user) => { - const emailNotificationMode = RocketChat.getUserPreference(user, 'emailNotificationMode'); - if (usersToSendEmail[user._id] === 'default') { - if (emailNotificationMode === 'all') { //Mention/DM - usersToSendEmail[user._id] = 'mention'; - } else { - return; - } - } - - if (usersToSendEmail[user._id] === 'direct') { - const userEmailPreferenceIsDisabled = emailNotificationMode === 'disabled'; - const directMessageEmailPreference = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(message.rid, message.rid.replace(message.u._id, '')).emailNotifications; - - if (directMessageEmailPreference === 'nothing') { - return; - } - - if ((directMessageEmailPreference === 'default' || directMessageEmailPreference == null) && userEmailPreferenceIsDisabled) { - return; - } - } - - // Checks if user is in the room he/she is mentioned (unless it's public channel) - if (room.t !== 'c' && room.usernames.indexOf(user.username) === -1) { - return; - } - - // Footer in case direct reply is enabled. - if (RocketChat.settings.get('Direct_Reply_Enable')) { - footer = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Footer_Direct_Reply') || ''); - } - - let emailSubject; - const username = RocketChat.settings.get('UI_Use_Real_Name') ? message.u.name : message.u.username; - const roomName = RocketChat.settings.get('UI_Allow_room_names_with_special_chars') ? room.fname : room.name; - switch (usersToSendEmail[user._id]) { - case 'all': - emailSubject = RocketChat.placeholders.replace(RocketChat.settings.get('Offline_Mention_All_Email'), { - user: username, - room: roomName || room.label - }); - break; - case 'direct': - emailSubject = RocketChat.placeholders.replace(RocketChat.settings.get('Offline_DM_Email'), { - user: username, - room: roomName - }); - break; - case 'mention': - emailSubject = RocketChat.placeholders.replace(RocketChat.settings.get('Offline_Mention_Email'), { - user: username, - room: roomName - }); - break; - } - user.emails.some((email) => { - if (email.verified) { - const content = getEmailContent({ - messageContent: messageHTML, - message, - user, - room - }); - email = { - to: email.address, - subject: emailSubject, - html: header + content + divisorMessage + (linkByUser[user._id] || defaultLink) + footer - }; - // using user full-name/channel name in from address - if (room.t === 'd') { - email.from = `${ String(message.u.name).replace(/@/g, '%40').replace(/[<>,]/g, '') } <${ RocketChat.settings.get('From_Email') }>`; - } else { - email.from = `${ String(room.name).replace(/@/g, '%40').replace(/[<>,]/g, '') } <${ RocketChat.settings.get('From_Email') }>`; - } - // If direct reply enabled, email content with headers - if (RocketChat.settings.get('Direct_Reply_Enable')) { - email.headers = { - // Reply-To header with format "username+messageId@domain" - 'Reply-To': `${ RocketChat.settings.get('Direct_Reply_Username').split('@')[0].split(RocketChat.settings.get('Direct_Reply_Separator'))[0] }${ RocketChat.settings.get('Direct_Reply_Separator') }${ message._id }@${ RocketChat.settings.get('Direct_Reply_Username').split('@')[1] }` - }; - } - - Meteor.defer(() => { - Email.send(email); - }); - - return true; - } - }); - }); - } - } - - return message; - -}, RocketChat.callbacks.priority.LOW, 'sendEmailOnMessage'); diff --git a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js index e3c811d19366..e36e02776134 100644 --- a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js +++ b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js @@ -1,168 +1,130 @@ -/* globals Push */ -import _ from 'underscore'; -import s from 'underscore.string'; import moment from 'moment'; -const CATEGORY_MESSAGE = 'MESSAGE'; -const CATEGORY_MESSAGE_NOREPLY = 'MESSAGE_NOREPLY'; - -/** - * Replaces @username with full name - * - * @param {string} message The message to replace - * @param {object[]} mentions Array of mentions used to make replacements - * - * @returns {string} - */ -function replaceMentionedUsernamesWithFullNames(message, mentions) { - if (!mentions || !mentions.length) { - return message; +import { callJoinRoom, messageContainsHighlight } from '../functions/notifications/'; +import { sendEmail, shouldNotifyEmail } from '../functions/notifications/email'; +import { sendSinglePush, shouldNotifyMobile } from '../functions/notifications/mobile'; +import { notifyDesktopUser, shouldNotifyDesktop } from '../functions/notifications/desktop'; +import { notifyAudioUser, shouldNotifyAudio } from '../functions/notifications/audio'; + +const sendNotification = ({ + subscription, + sender, + hasMentionToAll, + hasMentionToHere, + message, + room, + mentionIds, + disableAllMessageNotifications +}) => { + + // don't notify the sender + if (subscription.u._id === sender._id) { + return; } - mentions.forEach((mention) => { - const user = RocketChat.models.Users.findOneById(mention._id); - if (user && user.name) { - message = message.replace(`@${ mention.username }`, user.name); - } - }); - return message; -} -function canSendMessageToRoom(room, username) { - return !((room.muted || []).includes(username)); -} - -/** - * This function returns a string ready to be shown in the notification - * - * @param {object} message the message to be parsed - */ -function parseMessageText(message, userId) { - const user = RocketChat.models.Users.findOneById(userId); - const lng = user && user.language || RocketChat.settings.get('language') || 'en'; - - if (!message.msg && message.attachments && message.attachments[0]) { - message.msg = message.attachments[0].image_type ? TAPi18n.__('User_uploaded_image', {lng}) : TAPi18n.__('User_uploaded_file', {lng}); + // notifications disabled + if (subscription.disableNotifications) { + return; } - message.msg = RocketChat.callbacks.run('beforeNotifyUser', message.msg); - return message.msg; -} -/** - * Send notification to user - * - * @param {string} userId The user to notify - * @param {object} user The sender - * @param {object} room The room send from - * @param {number} duration Duration of notification - */ -function notifyDesktopUser(userId, user, message, room, duration) { - - const UI_Use_Real_Name = RocketChat.settings.get('UI_Use_Real_Name') === true; - message.msg = parseMessageText(message, userId); - - if (UI_Use_Real_Name) { - message.msg = replaceMentionedUsernamesWithFullNames(message.msg, message.mentions); + // dont send notification to users who ignored the sender + if (Array.isArray(subscription.ignored) && subscription.ignored.find(sender._id)) { + return; } - let title = ''; - let text = ''; - if (room.t === 'd') { - title = UI_Use_Real_Name ? user.name : `@${ user.username }`; - text = message.msg; - } else if (room.name) { - title = `#${ room.name }`; - text = `${ user.username }: ${ message.msg }`; - } + const hasMentionToUser = mentionIds.includes(subscription.u._id); - if (title === '' || text === '') { + // mute group notifications (@here and @all) if not directly mentioned as well + if (!hasMentionToUser && subscription.muteGroupMentions && (hasMentionToAll || hasMentionToHere)) { return; } - RocketChat.Notifications.notifyUser(userId, 'notification', { - title, - text, - duration, - payload: { - _id: message._id, - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name - } - }); -} + const receiver = RocketChat.models.Users.findOneById(subscription.u._id); -function notifyAudioUser(userId, message, room) { - RocketChat.Notifications.notifyUser(userId, 'audioNotification', { - payload: { - _id: message._id, - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name - } - }); -} + if (!receiver || !receiver.active) { + return; + } -/** - * Checks if a message contains a user highlight - * - * @param {string} message - * @param {array|undefined} highlights - * - * @returns {boolean} - */ -function messageContainsHighlight(message, highlights) { - if (! highlights || highlights.length === 0) { return false; } - - let has = false; - highlights.some(function(highlight) { - const regexp = new RegExp(s.escapeRegExp(highlight), 'i'); - if (regexp.test(message.msg)) { - has = true; - return true; - } - }); - - return has; -} + const isHighlighted = messageContainsHighlight(message, receiver.settings && receiver.settings.preferences && receiver.settings.preferences.highlights); + + const { + audioNotifications, + desktopNotifications, + mobilePushNotifications, + emailNotifications + } = subscription; + + let notificationSent = false; + + // busy users don't receive audio notification + if (shouldNotifyAudio({ + disableAllMessageNotifications, + status: receiver.status, + audioNotifications, + hasMentionToAll, + hasMentionToHere, + isHighlighted, + hasMentionToUser + })) { + notifyAudioUser(subscription.u._id, message, room); + } -function getBadgeCount(userId) { - const subscriptions = RocketChat.models.Subscriptions.findUnreadByUserId(userId).fetch(); + // busy users don't receive desktop notification + if (shouldNotifyDesktop({ + disableAllMessageNotifications, + status: receiver.status, + desktopNotifications, + hasMentionToAll, + hasMentionToHere, + isHighlighted, + hasMentionToUser + })) { + notificationSent = true; + notifyDesktopUser(subscription.u._id, sender, message, room, subscription.desktopNotificationDuration); + } - return subscriptions.reduce((unread, sub) => { - return sub.unread + unread; - }, 0); -} + if (shouldNotifyMobile({ + disableAllMessageNotifications, + mobilePushNotifications, + hasMentionToAll, + isHighlighted, + hasMentionToUser, + statusConnection: receiver.statusConnection + })) { + notificationSent = true; + + sendSinglePush({ + room, + message, + userId: subscription.u._id, + senderUsername: sender.username, + receiverUsername: receiver.username + }); + } -const sendPushNotifications = (userIdsToPushNotify = [], message, room, push_room, push_username, push_message, pushUsernames) => { - if (userIdsToPushNotify.length > 0 && Push.enabled === true) { - // send a push notification for each user individually (to get his/her badge count) - userIdsToPushNotify.forEach((userIdToNotify) => { - RocketChat.PushNotification.send({ - roomId: message.rid, - roomName: push_room, - username: push_username, - message: push_message, - badge: getBadgeCount(userIdToNotify), - payload: { - host: Meteor.absoluteUrl(), - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name - }, - usersTo: { - userId: userIdToNotify - }, - category: canSendMessageToRoom(room, pushUsernames[userIdToNotify]) ? CATEGORY_MESSAGE : CATEGORY_MESSAGE_NOREPLY - }); + if (receiver.emails && shouldNotifyEmail({ + disableAllMessageNotifications, + statusConnection: receiver.statusConnection, + emailNotifications, + isHighlighted, + hasMentionToUser, + hasMentionToAll + })) { + receiver.emails.some((email) => { + if (email.verified) { + sendEmail({ message, receiver, subscription, room, emailAddress: email.address }); + + return true; + } }); } + + if (notificationSent) { + RocketChat.Sandstorm.notify(message, [subscription.u._id], `@${ sender.username }: ${ message.msg }`, room.t === 'p' ? 'privateMessage' : 'message'); + } }; -const callJoin = (user, rid) => user.active && Meteor.runAsUser(user._id, () => Meteor.call('joinRoom', rid)); -RocketChat.callbacks.add('afterSaveMessage', function(message, room, userId) { +function sendAllNotifications(message, room) { // skips this callback if the message was edited if (message.editedAt) { @@ -173,312 +135,80 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room, userId) { return message; } - const pushUsernames = {}; - - const user = (room.t !== 'l') ? RocketChat.models.Users.findOneById(message.u._id) : room.v; - - if (!user) { + if (!room || room.t == null) { return message; } - /* - Increment unread couter if direct messages - */ - const settings = { - alwaysNotifyDesktopUsers: [], - dontNotifyDesktopUsers: [], - alwaysNotifyMobileUsers: [], - dontNotifyMobileUsers: [], - desktopNotificationDurations: {}, - alwaysNotifyAudioUsers: [], - dontNotifyAudioUsers: [], - audioNotificationValues: {}, - dontNotifyUsersOnGroupMentions: [] - }; - - /** - * Checks if a given user can be notified - * - * @param {string} id - * @param {string} type - mobile|desktop - * - * @returns {boolean} - */ - function canBeNotified(id, type) { - const types = { - desktop: [ 'dontNotifyDesktopUsers', 'alwaysNotifyDesktopUsers' ], - mobile: [ 'dontNotifyMobileUsers', 'alwaysNotifyMobileUsers' ], - audio: [ 'dontNotifyAudioUsers', 'alwaysNotifyAudioUsers' ] - }; - - return (settings[types[type][0]].indexOf(id) === -1 || settings[types[type][1]].indexOf(id) !== -1); + const sender = (room.t !== 'l') ? RocketChat.models.Users.findOneById(message.u._id) : room.v; + if (!sender) { + return message; } // Don't fetch all users if room exceeds max members const maxMembersForNotification = RocketChat.settings.get('Notifications_Max_Room_Members'); const disableAllMessageNotifications = room.usernames.length > maxMembersForNotification && maxMembersForNotification !== 0; - const subscriptions = RocketChat.models.Subscriptions.findNotificationPreferencesByRoom(room._id, disableAllMessageNotifications) || []; - const userIds = []; - subscriptions.forEach(s => userIds.push(s.u._id)); - const users = {}; - - RocketChat.models.Users.findUsersByIds(userIds, { fields: { 'settings.preferences': 1 } }).forEach((user) => { - users[user._id] = user; - }); - - subscriptions.forEach(subscription => { - if (subscription.disableNotifications) { - settings.dontNotifyDesktopUsers.push(subscription.u._id); - settings.dontNotifyMobileUsers.push(subscription.u._id); - settings.dontNotifyAudioUsers.push(subscription.u._id); - return; - } - - if (Array.isArray(subscription.ignored) && subscription.ignored.find(message.u._id)) { - return; - } - - const { - audioNotifications = RocketChat.getUserPreference(users[subscription.u._id], 'audioNotifications'), - desktopNotifications = RocketChat.getUserPreference(users[subscription.u._id], 'desktopNotifications'), - mobilePushNotifications = RocketChat.getUserPreference(users[subscription.u._id], 'mobileNotifications') - } = subscription; - - if (audioNotifications === 'all' && !disableAllMessageNotifications) { - settings.alwaysNotifyAudioUsers.push(subscription.u._id); - } - if (desktopNotifications === 'all' && !disableAllMessageNotifications) { - settings.alwaysNotifyDesktopUsers.push(subscription.u._id); - } else if (desktopNotifications === 'nothing') { - settings.dontNotifyDesktopUsers.push(subscription.u._id); - } - if (mobilePushNotifications === 'all' && !disableAllMessageNotifications) { - settings.alwaysNotifyMobileUsers.push(subscription.u._id); - } else if (mobilePushNotifications === 'nothing') { - settings.dontNotifyMobileUsers.push(subscription.u._id); - } - - settings.audioNotificationValues[subscription.u._id] = subscription.audioNotificationValue; - settings.desktopNotificationDurations[subscription.u._id] = subscription.desktopNotificationDuration; - - if (subscription.muteGroupMentions) { - settings.dontNotifyUsersOnGroupMentions.push(subscription.u._id); - } - }); - let userIdsForAudio = []; - let userIdsToNotify = []; - let userIdsToPushNotify = []; - const mentions = []; - const alwaysNotifyMobileBoolean = RocketChat.settings.get('Notifications_Always_Notify_Mobile'); - - const usersWithHighlights = RocketChat.models.Users.findUsersByUsernamesWithHighlights(room.usernames, { fields: { '_id': 1, 'settings.preferences.highlights': 1 }}).fetch() - .filter(user => messageContainsHighlight(message, user.settings.preferences.highlights)); - - let push_message = ' '; - //Set variables depending on Push Notification settings - if (RocketChat.settings.get('Push_show_message')) { - push_message = parseMessageText(message, userId); - } - - let push_username = ''; - let push_room = ''; - if (RocketChat.settings.get('Push_show_username_room')) { - push_username = user.username; - push_room = `#${ room.name }`; - } - - if (room.t == null || room.t === 'd') { - const userOfMentionId = message.rid.replace(message.u._id, ''); - const userOfMention = RocketChat.models.Users.findOne({ - _id: userOfMentionId - }, { - fields: { - username: 1, - statusConnection: 1 - } - }); - - // Always notify Sandstorm - if (userOfMention != null) { - RocketChat.Sandstorm.notify(message, [userOfMention._id], - `@${ user.username }: ${ message.msg }`, 'privateMessage'); - - if (canBeNotified(userOfMentionId, 'desktop')) { - const duration = settings.desktopNotificationDurations[userOfMention._id]; - notifyDesktopUser(userOfMention._id, user, message, room, duration); - } - - if (canBeNotified(userOfMentionId, 'mobile')) { - if (Push.enabled === true && (userOfMention.statusConnection !== 'online' || alwaysNotifyMobileBoolean === true)) { - RocketChat.PushNotification.send({ - roomId: message.rid, - username: push_username, - message: push_message, - badge: getBadgeCount(userOfMention._id), - payload: { - host: Meteor.absoluteUrl(), - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name - }, - usersTo: { - userId: userOfMention._id - }, - category: canSendMessageToRoom(room, userOfMention.username) ? CATEGORY_MESSAGE : CATEGORY_MESSAGE_NOREPLY - }); - return message; - } - } - } + // the find bellow is crucial. all subscription records returned will receive at least one kind of notification. + // the query is defined by the server's default values and Notifications_Max_Room_Members setting. + let subscriptions; + if (disableAllMessageNotifications) { + subscriptions = RocketChat.models.Subscriptions.findAllMessagesNotificationPreferencesByRoom(room._id); } else { - const mentionIds = (message.mentions || []).map(({_id}) => _id); - const toAll = mentionIds.includes('all'); - const toHere = mentionIds.includes('here'); - - if (mentionIds.length + settings.alwaysNotifyDesktopUsers.length > 0) { - let desktopMentionIds = _.union(mentionIds, settings.alwaysNotifyDesktopUsers); - desktopMentionIds = _.difference(desktopMentionIds, settings.dontNotifyDesktopUsers); - - let usersOfDesktopMentions = RocketChat.models.Users.find({ - _id: { - $in: desktopMentionIds - } - }, { - fields: { - _id: 1, - username: 1, - active: 1 - } - }).fetch(); - mentions.push(...usersOfDesktopMentions); - if (room.t !== 'c') { - usersOfDesktopMentions = _.reject(usersOfDesktopMentions, (usersOfMentionItem) => { - return room.usernames.indexOf(usersOfMentionItem.username) === -1; - }); - } - - userIdsToNotify = _.pluck(usersOfDesktopMentions, '_id'); - } - - if (mentionIds.length + settings.alwaysNotifyMobileUsers.length > 0) { - let mobileMentionIds = _.union(mentionIds, settings.alwaysNotifyMobileUsers); - mobileMentionIds = _.difference(mobileMentionIds, settings.dontNotifyMobileUsers); - - const usersOfMobileMentionsQuery = { - _id: { - $in: mobileMentionIds - } - }; - - if (alwaysNotifyMobileBoolean !== true) { - usersOfMobileMentionsQuery.statusConnection = { $ne: 'online' }; - } - - let usersOfMobileMentions = RocketChat.models.Users.find(usersOfMobileMentionsQuery, { - fields: { - _id: 1, - username: 1, - statusConnection: 1, - active: 1 - } - }).fetch(); - - mentions.push(...usersOfMobileMentions); - if (room.t !== 'c') { - usersOfMobileMentions = _.reject(usersOfMobileMentions, usersOfMentionItem => !room.usernames.includes(usersOfMentionItem.username)); - } + const mentionsFilter = { $in: ['all', 'mentions'] }; + const excludesNothingFilter = { $ne: 'nothing' }; + + // evaluate if doing three specific finds is better than evaluting all results + subscriptions = RocketChat.models.Subscriptions.findNotificationPreferencesByRoom({ + roomId: room._id, + desktopFilter: RocketChat.settings.get('Accounts_Default_User_Preferences_desktopNotifications') === 'nothing' ? mentionsFilter : excludesNothingFilter, + emailFilter: RocketChat.settings.get('Accounts_Default_User_Preferences_emailNotificationMode') === 'disabled' ? mentionsFilter : excludesNothingFilter, + mobileFilter: RocketChat.settings.get('Accounts_Default_User_Preferences_mobileNotifications') === 'nothing' ? mentionsFilter : excludesNothingFilter + }); + } - userIdsToPushNotify = usersOfMobileMentions.map(userMobile => { - pushUsernames[userMobile._id] = userMobile.username; - return userMobile._id; - }); - } - - if (mentionIds.length + settings.alwaysNotifyAudioUsers.length > 0) { - let audioMentionIds = _.union(mentionIds, settings.alwaysNotifyAudioUsers); - audioMentionIds = _.difference(audioMentionIds, userIdsToNotify); - - let usersOfAudioMentions = RocketChat.models.Users.find({ _id: { $in: audioMentionIds }, statusConnection: { - $ne:'offline' - } }, { - fields: { - _id: 1, - username: 1, - active: 1 - } - }).fetch(); - mentions.push(...usersOfAudioMentions); - if (room.t !== 'c') { - usersOfAudioMentions = _.reject(usersOfAudioMentions, (usersOfMentionItem) => { - return room.usernames.indexOf(usersOfMentionItem.username) === -1; + const mentionIds = (message.mentions || []).map(({_id}) => _id); + const hasMentionToAll = mentionIds.includes('all'); + const hasMentionToHere = mentionIds.includes('here'); + + subscriptions.forEach((subscription) => sendNotification({ + subscription, + sender, + hasMentionToAll, + hasMentionToHere, + message, + room, + mentionIds, + disableAllMessageNotifications + })); + + // on public channels, if a mentioned user is not member of the channel yet, he will first join the channel and then be notified based on his preferences. + if (room.t === 'c') { + Promise.all(message.mentions + .filter(({ _id, username }) => _id !== 'here' && _id !== 'all' && !room.usernames.includes(username)) + .map(async(user) => { + await callJoinRoom(user, room._id); + + return user._id; + }) + ).then((users) => { + users.forEach((userId) => { + const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(room._id, userId); + + sendNotification({ + subscription, + sender, + hasMentionToAll, + hasMentionToHere, + message, + room, + mentionIds }); - } - - userIdsForAudio = _.pluck(usersOfAudioMentions, '_id'); - } - - if (room.t === 'c') { - mentions.filter(user => !room.usernames.includes(user.username)) - .forEach(user =>callJoin(user, room._id)); - } - - if ([toAll, toHere].some(e => e) && room.usernames && room.usernames.length > 0) { - RocketChat.models.Users.find({ - username: { $in: room.usernames }, - _id: { $ne: user._id } - }, { - fields: { - _id: 1, - username: 1, - status: 1, - statusConnection: 1 - } - }).forEach(function({ status, _id, username, statusConnection }) { // user - if (Array.isArray(settings.dontNotifyUsersOnGroupMentions) && settings.dontNotifyUsersOnGroupMentions.includes(_id)) { - return; - } - - if (['online', 'away', 'busy'].includes(status) && !(settings.dontNotifyDesktopUsers || []).includes(_id)) { - userIdsToNotify.push(_id); - userIdsForAudio.push(_id); - } - if (toAll && statusConnection !== 'online' && !(settings.dontNotifyMobileUsers || []).includes(_id)) { - pushUsernames[_id] = username; - return userIdsToPushNotify.push(_id); - } - if (toAll && statusConnection !== 'online') { - userIdsForAudio.push(_id); - } }); - } - - if (usersWithHighlights.length > 0) { - const highlightsIds = _.pluck(usersWithHighlights, '_id'); - userIdsForAudio = userIdsForAudio.concat(highlightsIds); - userIdsToNotify = userIdsToNotify.concat(highlightsIds); - userIdsToPushNotify = userIdsToPushNotify.concat(highlightsIds); - } - - userIdsToNotify = _.without(_.compact(_.unique(userIdsToNotify)), message.u._id); - userIdsToPushNotify = _.without(_.compact(_.unique(userIdsToPushNotify)), message.u._id); - userIdsForAudio = _.without(_.compact(_.unique(userIdsForAudio)), message.u._id); - - for (const usersOfMentionId of userIdsToNotify) { - const duration = settings.desktopNotificationDurations[usersOfMentionId]; - notifyDesktopUser(usersOfMentionId, user, message, room, duration); - } - for (const usersOfMentionId of userIdsForAudio) { - notifyAudioUser(usersOfMentionId, message, room); - } - sendPushNotifications(userIdsToPushNotify, message, room, push_room, push_username, push_message, pushUsernames); - - const allUserIdsToNotify = _.unique(userIdsToNotify.concat(userIdsToPushNotify)); - RocketChat.Sandstorm.notify(message, allUserIdsToNotify, - `@${ user.username }: ${ message.msg }`, room.t === 'p' ? 'privateMessage' : 'message'); + }); } return message; +} + +RocketChat.callbacks.add('afterSaveMessage', sendAllNotifications, RocketChat.callbacks.priority.LOW, 'sendNotificationsOnMessage'); -}, RocketChat.callbacks.priority.LOW, 'sendNotificationOnMessage'); diff --git a/packages/rocketchat-lib/server/models/Subscriptions.js b/packages/rocketchat-lib/server/models/Subscriptions.js index 116751db301d..32881ca97283 100644 --- a/packages/rocketchat-lib/server/models/Subscriptions.js +++ b/packages/rocketchat-lib/server/models/Subscriptions.js @@ -11,7 +11,9 @@ class ModelSubscriptions extends RocketChat.models._Base { this.tryEnsureIndex({ 'u._id': 1, 'name': 1, 't': 1, 'code': 1 }, { unique: 1 }); this.tryEnsureIndex({ 'open': 1 }); this.tryEnsureIndex({ 'alert': 1 }); - this.tryEnsureIndex({ 'unread': 1 }); + + this.tryEnsureIndex({ rid: 1, 'u._id': 1, open: 1 }); + this.tryEnsureIndex({ 'ts': 1 }); this.tryEnsureIndex({ 'ls': 1 }); this.tryEnsureIndex({ 'audioNotifications': 1 }, { sparse: 1 }); @@ -475,15 +477,28 @@ class ModelSubscriptions extends RocketChat.models._Base { 'u._id': { $ne: userId }, - $or: [ - { alert: { $ne: true } }, - { open: { $ne: true } } - ] + alert: { $ne: true } + }; + + const update = { + $set: { + alert: true + } + }; + return this.update(query, update, { multi: true }); + } + + setOpenForRoomIdExcludingUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': { + $ne: userId + }, + open: { $ne: true } }; const update = { $set: { - alert: true, open: true } }; @@ -596,6 +611,108 @@ class ModelSubscriptions extends RocketChat.models._Base { return this.update(query, update, { multi: true }); } + clearDesktopNotificationUserPreferences(userId) { + const query = { + 'u._id': userId, + desktopPrefOrigin: 'user' + }; + + const update = { + $unset: { + desktopNotifications: 1, + desktopPrefOrigin: 1 + } + }; + + return this.update(query, update, { multi: true }); + } + + updateDesktopNotificationUserPreferences(userId, desktopNotifications) { + const query = { + 'u._id': userId, + desktopPrefOrigin: { + $ne: 'subscription' + } + }; + + const update = { + $set: { + desktopNotifications, + desktopPrefOrigin: 'user' + } + }; + + return this.update(query, update, { multi: true }); + } + + clearMobileNotificationUserPreferences(userId) { + const query = { + 'u._id': userId, + mobilePrefOrigin: 'user' + }; + + const update = { + $unset: { + mobilePushNotifications: 1, + mobilePrefOrigin: 1 + } + }; + + return this.update(query, update, { multi: true }); + } + + updateMobileNotificationUserPreferences(userId, mobilePushNotifications) { + const query = { + 'u._id': userId, + mobilePrefOrigin: { + $ne: 'subscription' + } + }; + + const update = { + $set: { + mobilePushNotifications, + mobilePrefOrigin: 'user' + } + }; + + return this.update(query, update, { multi: true }); + } + + clearEmailNotificationUserPreferences(userId) { + const query = { + 'u._id': userId, + emailPrefOrigin: 'user' + }; + + const update = { + $unset: { + emailNotifications: 1, + emailPrefOrigin: 1 + } + }; + + return this.update(query, update, { multi: true }); + } + + updateEmailNotificationUserPreferences(userId, emailNotifications) { + const query = { + 'u._id': userId, + emailPrefOrigin: { + $ne: 'subscription' + } + }; + + const update = { + $set: { + emailNotifications, + emailPrefOrigin: 'user' + } + }; + + return this.update(query, update, { multi: true }); + } + // INSERT createWithRoomAndUser(room, user, extraData) { const subscription = { @@ -617,6 +734,27 @@ class ModelSubscriptions extends RocketChat.models._Base { } }; + const { + desktopNotifications, + mobileNotifications, + emailNotificationMode + } = (user.settings && user.settings.preferences) || {}; + + if (desktopNotifications && desktopNotifications !== 'default') { + subscription.desktopNotifications = desktopNotifications; + subscription.desktopPrefOrigin = 'user'; + } + + if (mobileNotifications && mobileNotifications !== 'default') { + subscription.mobilePushNotifications = mobileNotifications; + subscription.mobilePrefOrigin = 'user'; + } + + if (emailNotificationMode && emailNotificationMode !== 'default') { + subscription.emailNotifications = emailNotificationMode === 'disabled' ? 'nothing' : user.settings.preferences.emailNotificationMode; + subscription.emailPrefOrigin = 'user'; + } + _.extend(subscription, extraData); return this.insert(subscription); diff --git a/packages/rocketchat-lib/server/startup/settings.js b/packages/rocketchat-lib/server/startup/settings.js index 9434185df598..5d21ad446de4 100644 --- a/packages/rocketchat-lib/server/startup/settings.js +++ b/packages/rocketchat-lib/server/startup/settings.js @@ -180,7 +180,7 @@ RocketChat.settings.addGroup('Accounts', function() { }); this.section('Accounts_Default_User_Preferences', function() { - this.add('Accounts_Default_User_Preferences_enableAutoAway', false, { + this.add('Accounts_Default_User_Preferences_enableAutoAway', true, { type: 'boolean', 'public': true, i18nLabel: 'Enable_Auto_Away' diff --git a/packages/rocketchat-push-notifications/server/methods/saveNotificationSettings.js b/packages/rocketchat-push-notifications/server/methods/saveNotificationSettings.js index c4d3cdb3dbbb..314cc081601d 100644 --- a/packages/rocketchat-push-notifications/server/methods/saveNotificationSettings.js +++ b/packages/rocketchat-push-notifications/server/methods/saveNotificationSettings.js @@ -12,13 +12,35 @@ Meteor.methods({ updateMethod: (subscription, value) => RocketChat.models.Subscriptions.updateAudioNotificationsById(subscription._id, value) }, 'desktopNotifications': { - updateMethod: (subscription, value) => RocketChat.models.Subscriptions.updateDesktopNotificationsById(subscription._id, value) + updateMethod: (subscription, value) => { + if (value === 'default') { + const userPref = RocketChat.getUserNotificationPreference(Meteor.userId(), 'desktop'); + RocketChat.models.Subscriptions.updateDesktopNotificationsById(subscription._id, userPref.origin === 'server' ? null : userPref); + } else { + RocketChat.models.Subscriptions.updateDesktopNotificationsById(subscription._id, { value, origin: 'subscription' }); + } + } }, 'mobilePushNotifications': { - updateMethod: (subscription, value) => RocketChat.models.Subscriptions.updateMobilePushNotificationsById(subscription._id, value) + updateMethod: (subscription, value) => { + if (value === 'default') { + const userPref = RocketChat.getUserNotificationPreference(Meteor.userId(), 'mobile'); + RocketChat.models.Subscriptions.updateMobilePushNotificationsById(subscription._id, userPref.origin === 'server' ? null : userPref); + } else { + RocketChat.models.Subscriptions.updateMobilePushNotificationsById(subscription._id, { value, origin: 'subscription' }); + } + } }, 'emailNotifications': { - updateMethod: (subscription, value) => RocketChat.models.Subscriptions.updateEmailNotificationsById(subscription._id, value) + updateMethod: (subscription, value) => { + if (value === 'default') { + const userPref = RocketChat.getUserNotificationPreference(Meteor.userId(), 'email'); + userPref.value = userPref.value === 'disabled' ? 'nothing' : userPref.value; + RocketChat.models.Subscriptions.updateEmailNotificationsById(subscription._id, userPref.origin === 'server' ? null : userPref); + } else { + RocketChat.models.Subscriptions.updateEmailNotificationsById(subscription._id, { value, origin: 'subscription' }); + } + } }, 'unreadAlert': { updateMethod: (subscription, value) => RocketChat.models.Subscriptions.updateUnreadAlertById(subscription._id, value) diff --git a/packages/rocketchat-push-notifications/server/models/Subscriptions.js b/packages/rocketchat-push-notifications/server/models/Subscriptions.js index ce988d39a332..c769297354c8 100644 --- a/packages/rocketchat-push-notifications/server/models/Subscriptions.js +++ b/packages/rocketchat-push-notifications/server/models/Subscriptions.js @@ -35,10 +35,16 @@ RocketChat.models.Subscriptions.updateDesktopNotificationsById = function(_id, d const update = {}; - if (desktopNotifications === 'default') { - update.$unset = { desktopNotifications: 1 }; + if (desktopNotifications === null) { + update.$unset = { + desktopNotifications: 1, + desktopPrefOrigin: 1 + }; } else { - update.$set = { desktopNotifications }; + update.$set = { + desktopNotifications: desktopNotifications.value, + desktopPrefOrigin: desktopNotifications.origin + }; } return this.update(query, update); @@ -65,10 +71,16 @@ RocketChat.models.Subscriptions.updateMobilePushNotificationsById = function(_id const update = {}; - if (mobilePushNotifications === 'default') { - update.$unset = { mobilePushNotifications: 1 }; + if (mobilePushNotifications === null) { + update.$unset = { + mobilePushNotifications: 1, + mobilePrefOrigin: 1 + }; } else { - update.$set = { mobilePushNotifications }; + update.$set = { + mobilePushNotifications: mobilePushNotifications.value, + mobilePrefOrigin: mobilePushNotifications.origin + }; } return this.update(query, update); @@ -79,11 +91,19 @@ RocketChat.models.Subscriptions.updateEmailNotificationsById = function(_id, ema _id }; - const update = { - $set: { - emailNotifications - } - }; + const update = {}; + + if (emailNotifications === null) { + update.$unset = { + emailNotifications: 1, + emailPrefOrigin: 1 + }; + } else { + update.$set = { + emailNotifications: emailNotifications.value, + emailPrefOrigin: emailNotifications.origin + }; + } return this.update(query, update); }; @@ -189,27 +209,6 @@ RocketChat.models.Subscriptions.findDontNotifyMobileUsersByRoomId = function(roo return this.find(query); }; -RocketChat.models.Subscriptions.findNotificationPreferencesByRoom = function(roomId, explicit) { - const query = { - rid: roomId, - 'u._id': {$exists: true} - }; - - if (explicit) { - query.$or = [ - {audioNotifications: {$exists: true}}, - {audioNotificationValue: {$exists: true}}, - {desktopNotifications: {$exists: true}}, - {desktopNotificationDuration: {$exists: true}}, - {mobilePushNotifications: {$exists: true}}, - {disableNotifications: {$exists: true}}, - {muteGroupMentions: {$exists: true}} - ]; - } - - return this.find(query, { fields: { 'u._id': 1, audioNotifications: 1, audioNotificationValue: 1, desktopNotificationDuration: 1, desktopNotifications: 1, mobilePushNotifications: 1, disableNotifications: 1, muteGroupMentions: 1 } }); -}; - RocketChat.models.Subscriptions.findWithSendEmailByRoomId = function(roomId) { const query = { rid: roomId, @@ -220,3 +219,56 @@ RocketChat.models.Subscriptions.findWithSendEmailByRoomId = function(roomId) { return this.find(query, { fields: { emailNotifications: 1, u: 1 } }); }; + + +RocketChat.models.Subscriptions.findNotificationPreferencesByRoom = function({ roomId: rid, desktopFilter: desktopNotifications, mobileFilter: mobilePushNotifications, emailFilter: emailNotifications }) { + const query = { + rid, + 'u._id': {$exists: true}, + $or: [ + { desktopNotifications }, + { mobilePushNotifications }, + { emailNotifications } + ] + }; + + return this._db.find(query, { + fields: { + 'u._id': 1, + audioNotifications: 1, + audioNotificationValue: 1, + desktopNotificationDuration: 1, + desktopNotifications: 1, + mobilePushNotifications: 1, + emailNotifications: 1, + disableNotifications: 1, + muteGroupMentions: 1 + } + }); +}; + +RocketChat.models.Subscriptions.findAllMessagesNotificationPreferencesByRoom = function(roomId) { + const query = { + rid: roomId, + 'u._id': {$exists: true}, + $or: [ + { desktopNotifications: { $in: ['all', 'mentions'] } }, + { mobilePushNotifications: { $in: ['all', 'mentions'] } }, + { emailNotifications: { $in: ['all', 'mentions'] } } + ] + }; + + return this._db.find(query, { + fields: { + 'u._id': 1, + audioNotifications: 1, + audioNotificationValue: 1, + desktopNotificationDuration: 1, + desktopNotifications: 1, + mobilePushNotifications: 1, + emailNotifications: 1, + disableNotifications: 1, + muteGroupMentions: 1 + } + }); +}; diff --git a/packages/rocketchat-ui-account/client/accountPreferences.html b/packages/rocketchat-ui-account/client/accountPreferences.html index ac820da9c198..4e4e64807519 100644 --- a/packages/rocketchat-ui-account/client/accountPreferences.html +++ b/packages/rocketchat-ui-account/client/accountPreferences.html @@ -107,6 +107,7 @@

{{_ "Notifications"}}

@@ -319,7 +320,7 @@

{{_ "My Data"}}

- +
{{/if}} diff --git a/packages/rocketchat-ui-account/client/accountPreferences.js b/packages/rocketchat-ui-account/client/accountPreferences.js index 9dcbb99ff2b6..937e66eb6897 100644 --- a/packages/rocketchat-ui-account/client/accountPreferences.js +++ b/packages/rocketchat-ui-account/client/accountPreferences.js @@ -9,6 +9,11 @@ const notificationLabels = { nothing: 'Nothing' }; +const emailLabels = { + disabled: 'Email_Notification_Mode_Disabled', + all: 'Email_Notification_Mode_All' +}; + function checkedSelected(property, value, defaultValue=undefined) { if (defaultValue && defaultValue.hash) { defaultValue = undefined; @@ -84,6 +89,9 @@ Template.accountPreferences.helpers({ defaultMobileNotification() { return notificationLabels[RocketChat.settings.get('Accounts_Default_User_Preferences_mobileNotifications')]; }, + defaultEmailNotification() { + return emailLabels[RocketChat.settings.get('Accounts_Default_User_Preferences_emailNotificationMode')]; + }, showRoles() { return RocketChat.settings.get('UI_DisplayRoles'); }, diff --git a/server/methods/saveUserPreferences.js b/server/methods/saveUserPreferences.js index a694c51dbb12..2ae56398f5b3 100644 --- a/server/methods/saveUserPreferences.js +++ b/server/methods/saveUserPreferences.js @@ -45,6 +45,12 @@ Meteor.methods({ return false; } + const { + desktopNotifications: oldDesktopNotifications, + mobileNotifications: oldMobileNotifications, + emailNotificationMode: oldEmailNotifications + } = (user.settings && user.settings.preferences) || {}; + if (user.settings == null) { RocketChat.models.Users.clearSettings(user._id); } @@ -57,14 +63,39 @@ Meteor.methods({ settings.mergeChannels = ['1', true].includes(settings.mergeChannels); } - - if (settings.roomsListExhibitionMode != null) { settings.roomsListExhibitionMode = ['category', 'unread', 'activity'].includes(settings.roomsListExhibitionMode) ? settings.roomsListExhibitionMode : 'category'; } RocketChat.models.Users.setPreferences(user._id, settings); + // propagate changed notification preferences + Meteor.defer(() => { + if (oldDesktopNotifications !== settings.desktopNotifications) { + if (settings.desktopNotifications === 'default') { + RocketChat.models.Subscriptions.clearDesktopNotificationUserPreferences(user._id); + } else { + RocketChat.models.Subscriptions.updateDesktopNotificationUserPreferences(user._id, settings.desktopNotifications); + } + } + + if (oldMobileNotifications !== settings.mobileNotifications) { + if (settings.mobileNotifications === 'default') { + RocketChat.models.Subscriptions.clearMobileNotificationUserPreferences(user._id); + } else { + RocketChat.models.Subscriptions.updateMobileNotificationUserPreferences(user._id, settings.mobileNotifications); + } + } + + if (oldEmailNotifications !== settings.emailNotificationMode) { + if (settings.emailNotificationMode === 'default') { + RocketChat.models.Subscriptions.clearEmailNotificationUserPreferences(user._id); + } else { + RocketChat.models.Subscriptions.updateEmailNotificationUserPreferences(user._id, settings.emailNotificationMode === 'disabled' ? 'nothing' : settings.emailNotificationMode); + } + } + }); + return true; } }); diff --git a/server/startup/migrations/v116.js b/server/startup/migrations/v116.js new file mode 100644 index 000000000000..3df0f661678e --- /dev/null +++ b/server/startup/migrations/v116.js @@ -0,0 +1,88 @@ +RocketChat.Migrations.add({ + version: 116, + up() { + RocketChat.models.Subscriptions.tryDropIndex({ + unread: 1 + }); + + // set pref origin to all existing preferences + RocketChat.models.Subscriptions.update({ + desktopNotifications: { $exists: true } + }, { + $set: { + desktopPrefOrigin: 'subscription' + } + }, { + multi: true + }); + RocketChat.models.Subscriptions.update({ + mobilePushNotifications: { $exists: true } + }, { + $set: { + mobilePrefOrigin: 'subscription' + } + }, { + multi: true + }); + RocketChat.models.Subscriptions.update({ + emailNotifications: { $exists: true } + }, { + $set: { + emailPrefOrigin: 'subscription' + } + }, { + multi: true + }); + + // set user preferences on subscriptions + RocketChat.models.Users.find({ + $or: [ + { 'settings.preferences.desktopNotifications': { $exists: true } }, + { 'settings.preferences.mobileNotifications': { $exists: true } }, + { 'settings.preferences.emailNotificationMode': { $exists: true } } + ] + }).forEach(user => { + if (user.settings.preferences.desktopNotifications && user.settings.preferences.desktopNotifications !== 'default') { + RocketChat.models.Subscriptions.update({ + 'u._id': user._id, + desktopPrefOrigin: { $exists: false } + }, { + $set: { + desktopNotifications: user.settings.preferences.desktopNotifications, + desktopPrefOrigin: 'user' + } + }, { + multi: true + }); + } + + if (user.settings.preferences.mobileNotifications && user.settings.preferences.mobileNotifications !== 'default') { + RocketChat.models.Subscriptions.update({ + 'u._id': user._id, + mobilePrefOrigin: { $exists: false } + }, { + $set: { + mobileNotifications: user.settings.preferences.mobileNotifications, + mobilePrefOrigin: 'user' + } + }, { + multi: true + }); + } + + if (user.settings.preferences.emailNotificationMode && user.settings.preferences.emailNotificationMode !== 'default') { + RocketChat.models.Subscriptions.update({ + 'u._id': user._id, + emailPrefOrigin: { $exists: false } + }, { + $set: { + emailNotifications: user.settings.preferences.emailNotificationMode === 'disabled' ? 'nothing' : user.settings.preferences.emailNotificationMode, + emailPrefOrigin: 'user' + } + }, { + multi: true + }); + } + }); + } +}); diff --git a/tests/end-to-end/ui/11-admin.js b/tests/end-to-end/ui/11-admin.js index d48070b8dc3e..95b0bcda7529 100644 --- a/tests/end-to-end/ui/11-admin.js +++ b/tests/end-to-end/ui/11-admin.js @@ -731,8 +731,8 @@ describe('[Administration]', () => { admin.accountsEnableAutoAwayFalse.isVisible().should.be.true; }); it('the enable auto away field value should be true', () => { - admin.accountsEnableAutoAwayTrue.isSelected().should.be.false; - admin.accountsEnableAutoAwayFalse.isSelected().should.be.true; + admin.accountsEnableAutoAwayTrue.isSelected().should.be.true; + admin.accountsEnableAutoAwayFalse.isSelected().should.be.false; }); it('it should show the idle timeout limit field', () => {