From 1c2e35bd936d594aaa0a00b3decd76fc8908ed6e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Wed, 19 Apr 2017 14:02:52 -0300 Subject: [PATCH] finished conversion rocketchat-lib --- packages/rocketchat-lib/package.js | 25 +- .../server/functions/Notifications.coffee | 119 --- .../server/functions/Notifications.js | 132 +++ .../functions/checkUsernameAvailability.js | 20 + .../server/functions/sendMessage.coffee | 50 -- .../server/functions/sendMessage.js | 59 ++ .../server/functions/setUsername.coffee | 77 -- .../server/functions/setUsername.js | 85 ++ .../server/functions/settings.coffee | 248 ------ .../server/functions/settings.js | 283 +++++++ .../server/methods/sendMessage.coffee | 64 -- .../server/methods/sendMessage.js | 81 ++ .../rocketchat-lib/server/models/Messages.js | 580 +++++++++++++ .../rocketchat-lib/server/models/Rooms.js | 760 ++++++++++++++++++ .../rocketchat-lib/server/models/Settings.js | 178 ++++ .../server/models/Subscriptions.js | 570 +++++++++++++ .../rocketchat-lib/server/models/Uploads.js | 91 +++ .../rocketchat-lib/server/models/Users.js | 538 +++++++++++++ 18 files changed, 3389 insertions(+), 571 deletions(-) delete mode 100644 packages/rocketchat-lib/server/functions/Notifications.coffee create mode 100644 packages/rocketchat-lib/server/functions/Notifications.js create mode 100644 packages/rocketchat-lib/server/functions/checkUsernameAvailability.js delete mode 100644 packages/rocketchat-lib/server/functions/sendMessage.coffee create mode 100644 packages/rocketchat-lib/server/functions/sendMessage.js delete mode 100644 packages/rocketchat-lib/server/functions/setUsername.coffee create mode 100644 packages/rocketchat-lib/server/functions/setUsername.js delete mode 100644 packages/rocketchat-lib/server/functions/settings.coffee create mode 100644 packages/rocketchat-lib/server/functions/settings.js delete mode 100644 packages/rocketchat-lib/server/methods/sendMessage.coffee create mode 100644 packages/rocketchat-lib/server/methods/sendMessage.js create mode 100644 packages/rocketchat-lib/server/models/Messages.js create mode 100644 packages/rocketchat-lib/server/models/Rooms.js create mode 100644 packages/rocketchat-lib/server/models/Settings.js create mode 100644 packages/rocketchat-lib/server/models/Subscriptions.js create mode 100644 packages/rocketchat-lib/server/models/Uploads.js create mode 100644 packages/rocketchat-lib/server/models/Users.js diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 14a27f270f0..b3bccca8c18 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -21,7 +21,6 @@ Package.onUse(function(api) { api.use('reactive-var'); api.use('reactive-dict'); api.use('accounts-base'); - api.use('coffeescript'); api.use('ecmascript'); api.use('random'); api.use('check'); @@ -72,7 +71,7 @@ Package.onUse(function(api) { api.addFiles('server/functions/addUserToDefaultChannels.js', 'server'); api.addFiles('server/functions/addUserToRoom.js', 'server'); api.addFiles('server/functions/archiveRoom.js', 'server'); - api.addFiles('server/functions/checkUsernameAvailability.coffee', 'server'); + api.addFiles('server/functions/checkUsernameAvailability.js', 'server'); api.addFiles('server/functions/checkEmailAvailability.js', 'server'); api.addFiles('server/functions/createRoom.js', 'server'); api.addFiles('server/functions/deleteMessage.js', 'server'); @@ -82,15 +81,15 @@ Package.onUse(function(api) { api.addFiles('server/functions/removeUserFromRoom.js', 'server'); api.addFiles('server/functions/saveUser.js', 'server'); api.addFiles('server/functions/saveCustomFields.js', 'server'); - api.addFiles('server/functions/sendMessage.coffee', 'server'); - api.addFiles('server/functions/settings.coffee', 'server'); + api.addFiles('server/functions/sendMessage.js', 'server'); + api.addFiles('server/functions/settings.js', 'server'); api.addFiles('server/functions/setUserAvatar.js', 'server'); - api.addFiles('server/functions/setUsername.coffee', 'server'); + api.addFiles('server/functions/setUsername.js', 'server'); api.addFiles('server/functions/setRealName.js', 'server'); api.addFiles('server/functions/setEmail.js', 'server'); api.addFiles('server/functions/unarchiveRoom.js', 'server'); api.addFiles('server/functions/updateMessage.js', 'server'); - api.addFiles('server/functions/Notifications.coffee', 'server'); + api.addFiles('server/functions/Notifications.js', 'server'); // SERVER LIB api.addFiles('server/lib/configLogger.js', 'server'); @@ -104,13 +103,13 @@ Package.onUse(function(api) { // SERVER MODELS api.addFiles('server/models/_Base.js', 'server'); - api.addFiles('server/models/Messages.coffee', 'server'); + api.addFiles('server/models/Messages.js', 'server'); api.addFiles('server/models/Reports.js', 'server'); - api.addFiles('server/models/Rooms.coffee', 'server'); - api.addFiles('server/models/Settings.coffee', 'server'); - api.addFiles('server/models/Subscriptions.coffee', 'server'); - api.addFiles('server/models/Uploads.coffee', 'server'); - api.addFiles('server/models/Users.coffee', 'server'); + api.addFiles('server/models/Rooms.js', 'server'); + api.addFiles('server/models/Settings.js', 'server'); + api.addFiles('server/models/Subscriptions.js', 'server'); + api.addFiles('server/models/Uploads.js', 'server'); + api.addFiles('server/models/Users.js', 'server'); api.addFiles('server/oauth/oauth.js', 'server'); api.addFiles('server/oauth/google.js', 'server'); @@ -153,7 +152,7 @@ Package.onUse(function(api) { api.addFiles('server/methods/robotMethods.js', 'server'); api.addFiles('server/methods/saveSetting.js', 'server'); api.addFiles('server/methods/sendInvitationEmail.js', 'server'); - api.addFiles('server/methods/sendMessage.coffee', 'server'); + api.addFiles('server/methods/sendMessage.js', 'server'); api.addFiles('server/methods/sendSMTPTestEmail.js', 'server'); api.addFiles('server/methods/setAdminStatus.js', 'server'); api.addFiles('server/methods/setRealName.js', 'server'); diff --git a/packages/rocketchat-lib/server/functions/Notifications.coffee b/packages/rocketchat-lib/server/functions/Notifications.coffee deleted file mode 100644 index 9e08663db6c..00000000000 --- a/packages/rocketchat-lib/server/functions/Notifications.coffee +++ /dev/null @@ -1,119 +0,0 @@ -RocketChat.Notifications = new class - constructor: -> - self = @ - - @debug = false - - @streamAll = new Meteor.Streamer 'notify-all' - @streamLogged = new Meteor.Streamer 'notify-logged' - @streamRoom = new Meteor.Streamer 'notify-room' - @streamRoomUsers = new Meteor.Streamer 'notify-room-users' - @streamUser = new Meteor.Streamer 'notify-user' - - - @streamAll.allowWrite('none') - @streamLogged.allowWrite('none') - @streamRoom.allowWrite('none') - @streamRoomUsers.allowWrite (eventName, args...) -> - [roomId, e] = eventName.split('/') - - user = Meteor.users.findOne @userId, {fields: {username: 1}} - if RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(roomId, @userId)? - subscriptions = RocketChat.models.Subscriptions.findByRoomIdAndNotUserId(roomId, @userId).fetch() - for subscription in subscriptions - RocketChat.Notifications.notifyUser(subscription.u._id, e, args...) - - return false - - @streamUser.allowWrite('logged') - - @streamAll.allowRead('all') - - @streamLogged.allowRead('logged') - - @streamRoom.allowRead (eventName) -> - if not @userId? then return false - - roomId = eventName.split('/')[0] - - user = Meteor.users.findOne @userId, {fields: {username: 1}} - room = RocketChat.models.Rooms.findOneById(roomId) - if room.t is 'l' and room.v._id is user._id - return true - - return room.usernames.indexOf(user.username) > -1 - - @streamRoomUsers.allowRead('none'); - - @streamUser.allowRead (eventName) -> - userId = eventName.split('/')[0] - return @userId? and @userId is userId - - - notifyAll: (eventName, args...) -> - console.log 'notifyAll', arguments if @debug is true - - args.unshift eventName - @streamAll.emit.apply @streamAll, args - - notifyLogged: (eventName, args...) -> - console.log 'notifyLogged', arguments if @debug is true - - args.unshift eventName - @streamLogged.emit.apply @streamLogged, args - - notifyRoom: (room, eventName, args...) -> - console.log 'notifyRoom', arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoom.emit.apply @streamRoom, args - - notifyUser: (userId, eventName, args...) -> - console.log 'notifyUser', arguments if @debug is true - - args.unshift "#{userId}/#{eventName}" - @streamUser.emit.apply @streamUser, args - - - notifyAllInThisInstance: (eventName, args...) -> - console.log 'notifyAll', arguments if @debug is true - - args.unshift eventName - @streamAll.emitWithoutBroadcast.apply @streamAll, args - - notifyLoggedInThisInstance: (eventName, args...) -> - console.log 'notifyLogged', arguments if @debug is true - - args.unshift eventName - @streamLogged.emitWithoutBroadcast.apply @streamLogged, args - - notifyRoomInThisInstance: (room, eventName, args...) -> - console.log 'notifyRoomAndBroadcast', arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoom.emitWithoutBroadcast.apply @streamRoom, args - - notifyUserInThisInstance: (userId, eventName, args...) -> - console.log 'notifyUserAndBroadcast', arguments if @debug is true - - args.unshift "#{userId}/#{eventName}" - @streamUser.emitWithoutBroadcast.apply @streamUser, args - - -## Permissions for client - -# Enable emit for event typing for rooms and add username to event data -func = (eventName, username) -> - [room, e] = eventName.split('/') - - if e is 'webrtc' - return true - - if e is 'typing' - user = Meteor.users.findOne(@userId, {fields: {username: 1}}) - if user?.username is username - return true - - return false - -RocketChat.Notifications.streamRoom.allowWrite func diff --git a/packages/rocketchat-lib/server/functions/Notifications.js b/packages/rocketchat-lib/server/functions/Notifications.js new file mode 100644 index 00000000000..89fec97a279 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/Notifications.js @@ -0,0 +1,132 @@ +RocketChat.Notifications = new class { + constructor() { + this.debug = false; + this.streamAll = new Meteor.Streamer('notify-all'); + this.streamLogged = new Meteor.Streamer('notify-logged'); + this.streamRoom = new Meteor.Streamer('notify-room'); + this.streamRoomUsers = new Meteor.Streamer('notify-room-users'); + this.streamUser = new Meteor.Streamer('notify-user'); + this.streamAll.allowWrite('none'); + this.streamLogged.allowWrite('none'); + this.streamRoom.allowWrite('none'); + this.streamRoomUsers.allowWrite(function(eventName, ...args) { + const [roomId, e] = eventName.split('/'); + // const user = Meteor.users.findOne(this.userId, { + // fields: { + // username: 1 + // } + // }); + if (RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId) != null) { + const subscriptions = RocketChat.models.Subscriptions.findByRoomIdAndNotUserId(roomId, this.userId).fetch(); + subscriptions.forEach(subscription => RocketChat.Notifications.notifyUser(subscription.u._id, e, ...args)); + } + return false; + }); + this.streamUser.allowWrite('logged'); + this.streamAll.allowRead('all'); + this.streamLogged.allowRead('logged'); + this.streamRoom.allowRead(function(eventName) { + if (this.userId == null) { + return false; + } + const [roomId] = eventName.split('/'); + const user = Meteor.users.findOne(this.userId, { + fields: { + username: 1 + } + }); + const room = RocketChat.models.Rooms.findOneById(roomId); + if (room.t === 'l' && room.v._id === user._id) { + return true; + } + return room.usernames.indexOf(user.username) > -1; + }); + this.streamRoomUsers.allowRead('none'); + this.streamUser.allowRead(function(eventName) { + const [userId] = eventName.split('/'); + return (this.userId != null) && this.userId === userId; + }); + } + + notifyAll(eventName, ...args) { + if (this.debug === true) { + console.log('notifyAll', arguments); + } + args.unshift(eventName); + return this.streamAll.emit.apply(this.streamAll, args); + } + + notifyLogged(eventName, ...args) { + if (this.debug === true) { + console.log('notifyLogged', arguments); + } + args.unshift(eventName); + return this.streamLogged.emit.apply(this.streamLogged, args); + } + + notifyRoom(room, eventName, ...args) { + if (this.debug === true) { + console.log('notifyRoom', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoom.emit.apply(this.streamRoom, args); + } + + notifyUser(userId, eventName, ...args) { + if (this.debug === true) { + console.log('notifyUser', arguments); + } + args.unshift(`${ userId }/${ eventName }`); + return this.streamUser.emit.apply(this.streamUser, args); + } + + notifyAllInThisInstance(eventName, ...args) { + if (this.debug === true) { + console.log('notifyAll', arguments); + } + args.unshift(eventName); + return this.streamAll.emitWithoutBroadcast.apply(this.streamAll, args); + } + + notifyLoggedInThisInstance(eventName, ...args) { + if (this.debug === true) { + console.log('notifyLogged', arguments); + } + args.unshift(eventName); + return this.streamLogged.emitWithoutBroadcast.apply(this.streamLogged, args); + } + + notifyRoomInThisInstance(room, eventName, ...args) { + if (this.debug === true) { + console.log('notifyRoomAndBroadcast', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoom.emitWithoutBroadcast.apply(this.streamRoom, args); + } + + notifyUserInThisInstance(userId, eventName, ...args) { + if (this.debug === true) { + console.log('notifyUserAndBroadcast', arguments); + } + args.unshift(`${ userId }/${ eventName }`); + return this.streamUser.emitWithoutBroadcast.apply(this.streamUser, args); + } +}; + +RocketChat.Notifications.streamRoom.allowWrite(function(eventName, username) { + const [, e] = eventName.split('/'); + if (e === 'webrtc') { + return true; + } + if (e === 'typing') { + const user = Meteor.users.findOne(this.userId, { + fields: { + username: 1 + } + }); + if (user != null && user.username === username) { + return true; + } + } + return false; +}); diff --git a/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js new file mode 100644 index 00000000000..01e8f8c1b10 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js @@ -0,0 +1,20 @@ +RocketChat.checkUsernameAvailability = function(username) { + return RocketChat.settings.get('Accounts_BlockedUsernameList', function(key, value) { + const usernameBlackList = _.map(value.split(','), function(username) { + return username.trim(); + }); + if (usernameBlackList.length !== 0) { + if (usernameBlackList.every(restrictedUsername => { + const regex = new RegExp(`^${ s.escapeRegExp(restrictedUsername) }$`, 'i'); + return !regex.test(s.trim(s.escapeRegExp(username))); + })) { + return !Meteor.users.findOne({ + username: { + $regex: new RegExp(`^${ s.trim(s.escapeRegExp(username)) }$`, 'i') + } + }); + } + return false; + } + }); +}; diff --git a/packages/rocketchat-lib/server/functions/sendMessage.coffee b/packages/rocketchat-lib/server/functions/sendMessage.coffee deleted file mode 100644 index bdce87faba4..00000000000 --- a/packages/rocketchat-lib/server/functions/sendMessage.coffee +++ /dev/null @@ -1,50 +0,0 @@ -RocketChat.sendMessage = (user, message, room, upsert = false) -> - if not user or not message or not room._id - return false - - unless message.ts? - message.ts = new Date() - - message.u = _.pick user, ['_id','username'] - - if not Match.test(message.msg, String) - message.msg = '' - - message.rid = room._id - - if not room.usernames? || room.usernames.length is 0 - updated_room = RocketChat.models.Rooms.findOneById(room._id) - if updated_room? - room = updated_room - else - room.usernames = [] - - if message.parseUrls isnt false - if urls = message.msg.match /([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g - message.urls = urls.map (url) -> url: url - - message = RocketChat.callbacks.run 'beforeSaveMessage', message - - # Avoid saving sandstormSessionId to the database - sandstormSessionId = null - if message.sandstormSessionId - sandstormSessionId = message.sandstormSessionId - delete message.sandstormSessionId - - if message._id? and upsert - _id = message._id - delete message._id - RocketChat.models.Messages.upsert {_id: _id, 'u._id': message.u._id}, message - message._id = _id - else - message._id = RocketChat.models.Messages.insert message - - ### - Defer other updates as their return is not interesting to the user - ### - Meteor.defer -> - # Execute all callbacks - message.sandstormSessionId = sandstormSessionId - RocketChat.callbacks.run 'afterSaveMessage', message, room - - return message diff --git a/packages/rocketchat-lib/server/functions/sendMessage.js b/packages/rocketchat-lib/server/functions/sendMessage.js new file mode 100644 index 00000000000..d0e4181e733 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/sendMessage.js @@ -0,0 +1,59 @@ +RocketChat.sendMessage = function(user, message, room, upsert = false) { + if (!user || !message || !room._id) { + return false; + } + if (message.ts == null) { + message.ts = new Date(); + } + message.u = _.pick(user, ['_id', 'username', 'name']); + if (!Match.test(message.msg, String)) { + message.msg = ''; + } + message.rid = room._id; + if (room.usernames || room.usernames.length === 0) { + const updated_room = RocketChat.models.Rooms.findOneById(room._id); + if (updated_room != null) { + room = updated_room; + } else { + room.usernames = []; + } + } + if (message.parseUrls !== false) { + const urls = message.msg.match(/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g); + if (urls) { + message.urls = urls.map(function(url) { + return { + url + }; + }); + } + } + message = RocketChat.callbacks.run('beforeSaveMessage', message); + // Avoid saving sandstormSessionId to the database + let sandstormSessionId = null; + if (message.sandstormSessionId) { + sandstormSessionId = message.sandstormSessionId; + delete message.sandstormSessionId; + } + if (message._id && upsert) { + const _id = message._id; + delete message._id; + RocketChat.models.Messages.upsert({ + _id, + 'u._id': message.u._id + }, message); + message._id = _id; + } else { + message._id = RocketChat.models.Messages.insert(message); + } + + /* + Defer other updates as their return is not interesting to the user + */ + Meteor.defer(() => { + // Execute all callbacks + message.sandstormSessionId = sandstormSessionId; + return RocketChat.callbacks.run('afterSaveMessage', message, room); + }); + return message; +}; diff --git a/packages/rocketchat-lib/server/functions/setUsername.coffee b/packages/rocketchat-lib/server/functions/setUsername.coffee deleted file mode 100644 index a3afa4a49e9..00000000000 --- a/packages/rocketchat-lib/server/functions/setUsername.coffee +++ /dev/null @@ -1,77 +0,0 @@ -RocketChat._setUsername = (userId, username) -> - username = s.trim username - if not userId or not username - return false - - try - nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$' - catch - nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$' - - if not nameValidation.test username - return false - - user = RocketChat.models.Users.findOneById userId - - # User already has desired username, return - if user.username is username - return user - - previousUsername = user.username - - # Check username availability or if the user already owns a different casing of the name - if ( !previousUsername or !(username.toLowerCase() == previousUsername.toLowerCase())) - unless RocketChat.checkUsernameAvailability username - return false - - # If first time setting username, send Enrollment Email - try - if not previousUsername and user.emails?.length > 0 and RocketChat.settings.get 'Accounts_Enrollment_Email' - Accounts.sendEnrollmentEmail(user._id) - catch error - - user.username = username - - # If first time setting username, check if should set default avatar - if not previousUsername and RocketChat.settings.get('Accounts_SetDefaultAvatar') is true - avatarSuggestions = getAvatarSuggestionForUser user - for service, avatarData of avatarSuggestions - if service isnt 'gravatar' - RocketChat.setUserAvatar(user, avatarData.blob, avatarData.contentType, service) - gravatar = null - break - else - gravatar = avatarData - if gravatar? - RocketChat.setUserAvatar(user, gravatar.blob, gravatar.contentType, 'gravatar') - - # Username is available; if coming from old username, update all references - if previousUsername - RocketChat.models.Messages.updateAllUsernamesByUserId user._id, username - RocketChat.models.Messages.updateUsernameOfEditByUserId user._id, username - - RocketChat.models.Messages.findByMention(previousUsername).forEach (msg) -> - updatedMsg = msg.msg.replace(new RegExp("@#{previousUsername}", "ig"), "@#{username}") - RocketChat.models.Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername msg._id, previousUsername, username, updatedMsg - - RocketChat.models.Rooms.replaceUsername previousUsername, username - RocketChat.models.Rooms.replaceMutedUsername previousUsername, username - RocketChat.models.Rooms.replaceUsernameOfUserByUserId user._id, username - - RocketChat.models.Subscriptions.setUserUsernameByUserId user._id, username - RocketChat.models.Subscriptions.setNameForDirectRoomsWithOldName previousUsername, username - - rs = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent("#{previousUsername}.jpg")) - if rs? - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{username}.jpg") - ws = RocketChatFileAvatarInstance.createWriteStream encodeURIComponent("#{username}.jpg"), rs.contentType - ws.on 'end', Meteor.bindEnvironment -> - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{previousUsername}.jpg") - rs.readStream.pipe(ws) - - # Set new username - RocketChat.models.Users.setUsername user._id, username - return user - -RocketChat.setUsername = RocketChat.RateLimiter.limitFunction RocketChat._setUsername, 1, 60000, - 0: () -> return not Meteor.userId() or not RocketChat.authz.hasPermission(Meteor.userId(), 'edit-other-user-info') # Administrators have permission to change others usernames, so don't limit those diff --git a/packages/rocketchat-lib/server/functions/setUsername.js b/packages/rocketchat-lib/server/functions/setUsername.js new file mode 100644 index 00000000000..455e6cfbc67 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/setUsername.js @@ -0,0 +1,85 @@ + +RocketChat._setUsername = function(userId, u) { + const username = s.trim(u); + if (!userId || !username) { + return false; + } + let nameValidation; + try { + nameValidation = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); + } catch (error) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + if (!nameValidation.test(username)) { + return false; + } + const user = RocketChat.models.Users.findOneById(userId); + // User already has desired username, return + if (user.username === username) { + return user; + } + const previousUsername = user.username; + // Check username availability or if the user already owns a different casing of the name + if (!previousUsername || !(username.toLowerCase() === previousUsername.toLowerCase())) { + if (!RocketChat.checkUsernameAvailability(username)) { + return false; + } + } + //If first time setting username, send Enrollment Email + try { + if (!previousUsername && user.emails && user.emails.length > 0 && RocketChat.settings.get('Accounts_Enrollment_Email')) { + Accounts.sendEnrollmentEmail(user._id); + } + } catch (e) { + console.error(e); + } + /* globals getAvatarSuggestionForUser */ + user.username = username; + if (!previousUsername && RocketChat.settings.get('Accounts_SetDefaultAvatar') === true) { + const avatarSuggestions = getAvatarSuggestionForUser(user); + let gravatar; + Object.keys(avatarSuggestions).some(service => { + const avatarData = avatarSuggestions[service]; + if (service !== 'gravatar') { + RocketChat.setUserAvatar(user, avatarData.blob, avatarData.contentType, service); + gravatar = null; + return true; + } else { + gravatar = avatarData; + } + }); + if (gravatar != null) { + RocketChat.setUserAvatar(user, gravatar.blob, gravatar.contentType, 'gravatar'); + } + } + // Username is available; if coming from old username, update all references + if (previousUsername) { + RocketChat.models.Messages.updateAllUsernamesByUserId(user._id, username); + RocketChat.models.Messages.updateUsernameOfEditByUserId(user._id, username); + RocketChat.models.Messages.findByMention(previousUsername).forEach(function(msg) { + const updatedMsg = msg.msg.replace(new RegExp(`@${ previousUsername }`, 'ig'), `@${ username }`); + return RocketChat.models.Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername(msg._id, previousUsername, username, updatedMsg); + }); + RocketChat.models.Rooms.replaceUsername(previousUsername, username); + RocketChat.models.Rooms.replaceMutedUsername(previousUsername, username); + RocketChat.models.Rooms.replaceUsernameOfUserByUserId(user._id, username); + RocketChat.models.Subscriptions.setUserUsernameByUserId(user._id, username); + RocketChat.models.Subscriptions.setNameForDirectRoomsWithOldName(previousUsername, username); + const rs = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent(`${ previousUsername }.jpg`)); + if (rs != null) { + RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ username }.jpg`)); + const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${ username }.jpg`), rs.contentType); + ws.on('end', Meteor.bindEnvironment(() => RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ previousUsername }.jpg`)))); + rs.readStream.pipe(ws); + } + } + // Set new username* + RocketChat.models.Users.setUsername(user._id, username); + return user; +}; + +RocketChat.setUsername = RocketChat.RateLimiter.limitFunction(RocketChat._setUsername, 1, 60000, { + [0](userId) { + return !userId || !RocketChat.authz.hasPermission(userId, 'edit-other-user-info'); + } +}); diff --git a/packages/rocketchat-lib/server/functions/settings.coffee b/packages/rocketchat-lib/server/functions/settings.coffee deleted file mode 100644 index 1098cd7941d..00000000000 --- a/packages/rocketchat-lib/server/functions/settings.coffee +++ /dev/null @@ -1,248 +0,0 @@ -blockedSettings = {} -process.env.SETTINGS_BLOCKED?.split(',').forEach (settingId) -> - blockedSettings[settingId] = 1 - -hiddenSettings = {} -process.env.SETTINGS_HIDDEN?.split(',').forEach (settingId) -> - hiddenSettings[settingId] = 1 - -RocketChat.settings._sorter = {} - -### -# Add a setting -# @param {String} _id -# @param {Mixed} value -# @param {Object} setting -### -RocketChat.settings.add = (_id, value, options = {}) -> - # console.log '[functions] RocketChat.settings.add -> '.green, 'arguments:', arguments - - if not _id or - not value? and not process?.env?['OVERWRITE_SETTING_' + _id]? - return false - - RocketChat.settings._sorter[options.group] ?= 0 - - options.packageValue = value - options.valueSource = 'packageValue' - options.hidden = false - options.blocked = options.blocked || false - options.sorter ?= RocketChat.settings._sorter[options.group]++ - - if options.enableQuery? - options.enableQuery = JSON.stringify options.enableQuery - - if options.i18nDefaultQuery? - options.i18nDefaultQuery = JSON.stringify options.i18nDefaultQuery - - if process?.env?[_id]? - value = process.env[_id] - if value.toLowerCase() is "true" - value = true - else if value.toLowerCase() is "false" - value = false - options.processEnvValue = value - options.valueSource = 'processEnvValue' - - else if Meteor.settings?[_id]? - value = Meteor.settings[_id] - options.meteorSettingsValue = value - options.valueSource = 'meteorSettingsValue' - - if not options.i18nLabel? - options.i18nLabel = _id - - # Default description i18n key will be the setting name + "_Description" (eg: LDAP_Enable -> LDAP_Enable_Description) - if not options.i18nDescription? - options.i18nDescription = "#{_id}_Description" - - if blockedSettings[_id]? - options.blocked = true - - if hiddenSettings[_id]? - options.hidden = true - - if process?.env?['OVERWRITE_SETTING_' + _id]? - value = process.env['OVERWRITE_SETTING_' + _id] - if value.toLowerCase() is "true" - value = true - else if value.toLowerCase() is "false" - value = false - options.value = value - options.processEnvValue = value - options.valueSource = 'processEnvValue' - - updateOperations = - $set: options - $setOnInsert: - createdAt: new Date - - if options.editor? - updateOperations.$setOnInsert.editor = options.editor - delete options.editor - - if not options.value? - if options.force is true - updateOperations.$set.value = options.packageValue - else - updateOperations.$setOnInsert.value = value - - query = _.extend { _id: _id }, updateOperations.$set - - if not options.section? - updateOperations.$unset = { section: 1 } - query.section = { $exists: false } - - existantSetting = RocketChat.models.Settings.db.findOne(query) - - if existantSetting? - if not existantSetting.editor? and updateOperations.$setOnInsert.editor? - updateOperations.$set.editor = updateOperations.$setOnInsert.editor - delete updateOperations.$setOnInsert.editor - else - updateOperations.$set.ts = new Date - - return RocketChat.models.Settings.upsert { _id: _id }, updateOperations - - - -### -# Add a setting group -# @param {String} _id -### -RocketChat.settings.addGroup = (_id, options = {}, cb) -> - # console.log '[functions] RocketChat.settings.addGroup -> '.green, 'arguments:', arguments - - if not _id - return false - - if _.isFunction(options) - cb = options - options = {} - - if not options.i18nLabel? - options.i18nLabel = _id - - if not options.i18nDescription? - options.i18nDescription = "#{_id}_Description" - - options.ts = new Date - options.blocked = false - options.hidden = false - - if blockedSettings[_id]? - options.blocked = true - - if hiddenSettings[_id]? - options.hidden = true - - RocketChat.models.Settings.upsert { _id: _id }, - $set: options - $setOnInsert: - type: 'group' - createdAt: new Date - - if cb? - cb.call - add: (id, value, options = {}) -> - options.group = _id - RocketChat.settings.add id, value, options - - section: (section, cb) -> - cb.call - add: (id, value, options = {}) -> - options.group = _id - options.section = section - RocketChat.settings.add id, value, options - - return - - -### -# Remove a setting by id -# @param {String} _id -### -RocketChat.settings.removeById = (_id) -> - # console.log '[functions] RocketChat.settings.add -> '.green, 'arguments:', arguments - - if not _id - return false - - return RocketChat.models.Settings.removeById _id - - -### -# Update a setting by id -# @param {String} _id -### -RocketChat.settings.updateById = (_id, value, editor) -> - # console.log '[functions] RocketChat.settings.updateById -> '.green, 'arguments:', arguments - - if not _id or not value? - return false - - if editor? - return RocketChat.models.Settings.updateValueAndEditorById _id, value, editor - - return RocketChat.models.Settings.updateValueById _id, value - - -### -# Update options of a setting by id -# @param {String} _id -### -RocketChat.settings.updateOptionsById = (_id, options) -> - # console.log '[functions] RocketChat.settings.updateOptionsById -> '.green, 'arguments:', arguments - - if not _id or not options? - return false - - return RocketChat.models.Settings.updateOptionsById _id, options - - -### -# Update a setting by id -# @param {String} _id -### -RocketChat.settings.clearById = (_id) -> - # console.log '[functions] RocketChat.settings.clearById -> '.green, 'arguments:', arguments - - if not _id? - return false - - return RocketChat.models.Settings.updateValueById _id, undefined - - -### -# Update a setting by id -### -RocketChat.settings.init = -> - RocketChat.settings.initialLoad = true - RocketChat.models.Settings.find().observe - added: (record) -> - Meteor.settings[record._id] = record.value - if record.env is true - process.env[record._id] = record.value - RocketChat.settings.load record._id, record.value, RocketChat.settings.initialLoad - changed: (record) -> - Meteor.settings[record._id] = record.value - if record.env is true - process.env[record._id] = record.value - RocketChat.settings.load record._id, record.value, RocketChat.settings.initialLoad - removed: (record) -> - delete Meteor.settings[record._id] - if record.env is true - delete process.env[record._id] - RocketChat.settings.load record._id, undefined, RocketChat.settings.initialLoad - RocketChat.settings.initialLoad = false - - for fn in RocketChat.settings.afterInitialLoad - fn(Meteor.settings) - - -RocketChat.settings.afterInitialLoad = [] - -RocketChat.settings.onAfterInitialLoad = (fn) -> - RocketChat.settings.afterInitialLoad.push(fn) - if RocketChat.settings.initialLoad is false - fn(Meteor.settings) diff --git a/packages/rocketchat-lib/server/functions/settings.js b/packages/rocketchat-lib/server/functions/settings.js new file mode 100644 index 00000000000..4fa9a3f2b40 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/settings.js @@ -0,0 +1,283 @@ +const blockedSettings = {}; + +if (process.env.SETTINGS_BLOCKED) { + process.env.SETTINGS_BLOCKED.split(',').forEach((settingId) => blockedSettings[settingId] = 1); +} + +const hiddenSettings = {}; +if (process.env.SETTINGS_HIDDEN) { + process.env.SETTINGS_HIDDEN.split(',').forEach((settingId) => hiddenSettings[settingId] = 1); +} + +RocketChat.settings._sorter = {}; + + +/* +* Add a setting +* @param {String} _id +* @param {Mixed} value +* @param {Object} setting +*/ + +RocketChat.settings.add = function(_id, value, options = {}) { + if (options == null) { + options = {}; + } + if (!_id || value == null) { + return false; + } + if (RocketChat.settings._sorter[options.group] == null) { + RocketChat.settings._sorter[options.group] = 0; + } + options.packageValue = value; + options.valueSource = 'packageValue'; + options.hidden = false; + options.blocked = options.blocked || false; + if (options.sorter == null) { + options.sorter = RocketChat.settings._sorter[options.group]++; + } + if (options.enableQuery != null) { + options.enableQuery = JSON.stringify(options.enableQuery); + } + if (options.i18nDefaultQuery != null) { + options.i18nDefaultQuery = JSON.stringify(options.i18nDefaultQuery); + } + if (typeof process !== 'undefined' && process.env && process.env._id) { + let value = process.env[_id]; + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } + options.processEnvValue = value; + options.valueSource = 'processEnvValue'; + } else if (Meteor.settings && Meteor.settings) { + const value = Meteor.settings[_id]; + options.meteorSettingsValue = value; + options.valueSource = 'meteorSettingsValue'; + } + if (options.i18nLabel == null) { + options.i18nLabel = _id; + } + if (options.i18nDescription == null) { + options.i18nDescription = `${ _id }_Description`; + } + if (blockedSettings[_id] != null) { + options.blocked = true; + } + if (hiddenSettings[_id] != null) { + options.hidden = true; + } + if (typeof process !== 'undefined' && process.env && process.env[`OVERWRITE_SETTING_${ _id }`]) { + let value = process.env[`OVERWRITE_SETTING_${ _id }`]; + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } + options.value = value; + options.processEnvValue = value; + options.valueSource = 'processEnvValue'; + } + const updateOperations = { + $set: options, + $setOnInsert: { + createdAt: new Date + } + }; + if (options.editor != null) { + updateOperations.$setOnInsert.editor = options.editor; + delete options.editor; + } + if (options.value == null) { + if (options.force === true) { + updateOperations.$set.value = options.packageValue; + } else { + updateOperations.$setOnInsert.value = value; + } + } + const query = _.extend({ + _id + }, updateOperations.$set); + if (options.section == null) { + updateOperations.$unset = { + section: 1 + }; + query.section = { + $exists: false + }; + } + const existantSetting = RocketChat.models.Settings.db.findOne(query); + if (existantSetting != null) { + if (existantSetting.editor == null && updateOperations.$setOnInsert.editor != null) { + updateOperations.$set.editor = updateOperations.$setOnInsert.editor; + delete updateOperations.$setOnInsert.editor; + } + } else { + updateOperations.$set.ts = new Date; + } + return RocketChat.models.Settings.upsert({ + _id + }, updateOperations); +}; + + +/* +* Add a setting group +* @param {String} _id +*/ + +RocketChat.settings.addGroup = function(_id, options = {}, cb) { + if (!_id) { + return false; + } + if (_.isFunction(options)) { + cb = options; + options = {}; + } + if (options.i18nLabel == null) { + options.i18nLabel = _id; + } + if (options.i18nDescription == null) { + options.i18nDescription = `${ _id }_Description`; + } + options.ts = new Date; + options.blocked = false; + options.hidden = false; + if (blockedSettings[_id] != null) { + options.blocked = true; + } + if (hiddenSettings[_id] != null) { + options.hidden = true; + } + RocketChat.models.Settings.upsert({ + _id + }, { + $set: options, + $setOnInsert: { + type: 'group', + createdAt: new Date + } + }); + if (cb != null) { + cb.call({ + add(id, value, options) { + if (options == null) { + options = {}; + } + options.group = _id; + return RocketChat.settings.add(id, value, options); + }, + section(section, cb) { + return cb.call({ + add(id, value, options) { + if (options == null) { + options = {}; + } + options.group = _id; + options.section = section; + return RocketChat.settings.add(id, value, options); + } + }); + } + }); + } +}; + + +/* +* Remove a setting by id +* @param {String} _id +*/ + +RocketChat.settings.removeById = function(_id) { + if (!_id) { + return false; + } + return RocketChat.models.Settings.removeById(_id); +}; + + +/* +* Update a setting by id +* @param {String} _id +*/ + +RocketChat.settings.updateById = function(_id, value, editor) { + if (!_id || value == null) { + return false; + } + if (editor != null) { + return RocketChat.models.Settings.updateValueAndEditorById(_id, value, editor); + } + return RocketChat.models.Settings.updateValueById(_id, value); +}; + + +/* +* Update options of a setting by id +* @param {String} _id +*/ + +RocketChat.settings.updateOptionsById = function(_id, options) { + if (!_id || options == null) { + return false; + } + return RocketChat.models.Settings.updateOptionsById(_id, options); +}; + + +/* +* Update a setting by id +* @param {String} _id +*/ + +RocketChat.settings.clearById = function(_id) { + if (_id == null) { + return false; + } + return RocketChat.models.Settings.updateValueById(_id, undefined); +}; + + +/* +* Update a setting by id +*/ + +RocketChat.settings.init = function() { + RocketChat.settings.initialLoad = true; + RocketChat.models.Settings.find().observe({ + added(record) { + Meteor.settings[record._id] = record.value; + if (record.env === true) { + process.env[record._id] = record.value; + } + return RocketChat.settings.load(record._id, record.value, RocketChat.settings.initialLoad); + }, + changed(record) { + Meteor.settings[record._id] = record.value; + if (record.env === true) { + process.env[record._id] = record.value; + } + return RocketChat.settings.load(record._id, record.value, RocketChat.settings.initialLoad); + }, + removed(record) { + delete Meteor.settings[record._id]; + if (record.env === true) { + delete process.env[record._id]; + } + return RocketChat.settings.load(record._id, undefined, RocketChat.settings.initialLoad); + } + }); + RocketChat.settings.initialLoad = false; + RocketChat.settings.afterInitialLoad.forEach(fn => fn(Meteor.settings)); +}; + +RocketChat.settings.afterInitialLoad = []; + +RocketChat.settings.onAfterInitialLoad = function(fn) { + RocketChat.settings.afterInitialLoad.push(fn); + if (RocketChat.settings.initialLoad === false) { + return fn(Meteor.settings); + } +}; diff --git a/packages/rocketchat-lib/server/methods/sendMessage.coffee b/packages/rocketchat-lib/server/methods/sendMessage.coffee deleted file mode 100644 index 95e8f71a66a..00000000000 --- a/packages/rocketchat-lib/server/methods/sendMessage.coffee +++ /dev/null @@ -1,64 +0,0 @@ -import moment from 'moment' - -Meteor.methods - sendMessage: (message) -> - - check message, Object - - if not Meteor.userId() - throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'sendMessage' }) - - if message.ts - tsDiff = Math.abs(moment(message.ts).diff()) - if tsDiff > 60000 - throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', { method: 'sendMessage', message_ts: message.ts, server_ts: new Date().getTime() }) - else if tsDiff > 10000 - message.ts = new Date() - else - message.ts = new Date() - - if message.msg?.length > RocketChat.settings.get('Message_MaxAllowedSize') - throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', { method: 'sendMessage' }) - - user = RocketChat.models.Users.findOneById Meteor.userId(), fields: username: 1, name: 1 - - room = Meteor.call 'canAccessRoom', message.rid, user._id - - if not room - return false - - subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId()); - if subscription and (subscription.blocked or subscription.blocker) - RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { - _id: Random.id() - rid: room._id - ts: new Date - msg: TAPi18n.__('room_is_blocked', {}, user.language) - } - return false - - if user.username in (room.muted or []) - RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { - _id: Random.id() - rid: room._id - ts: new Date - msg: TAPi18n.__('You_have_been_muted', {}, user.language) - } - return false - - message.alias = user.name if not message.alias? and RocketChat.settings.get 'Message_SetNameToAliasEnabled' - if Meteor.settings.public.sandstorm - message.sandstormSessionId = this.connection.sandstormSessionId() - - RocketChat.metrics.messagesSent.inc() # This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 - RocketChat.sendMessage user, message, room - -# Limit a user, who does not have the "bot" role, to sending 5 msgs/second -DDPRateLimiter.addRule - type: 'method' - name: 'sendMessage' - userId: (userId) -> - user = RocketChat.models.Users.findOneById(userId) - return true if not user?.roles - return 'bot' not in user.roles -, 5, 1000 diff --git a/packages/rocketchat-lib/server/methods/sendMessage.js b/packages/rocketchat-lib/server/methods/sendMessage.js new file mode 100644 index 00000000000..2f6e41502fc --- /dev/null +++ b/packages/rocketchat-lib/server/methods/sendMessage.js @@ -0,0 +1,81 @@ +import moment from 'moment'; + +Meteor.methods({ + sendMessage(message) { + check(message, Object); + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'sendMessage' + }); + } + if (message.ts) { + const tsDiff = Math.abs(moment(message.ts).diff()); + if (tsDiff > 60000) { + throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', { + method: 'sendMessage', + message_ts: message.ts, + server_ts: new Date().getTime() + }); + } else if (tsDiff > 10000) { + message.ts = new Date(); + } + } else { + message.ts = new Date(); + } + if (message.msg && message.msg.length > RocketChat.settings.get('Message_MaxAllowedSize')) { + throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', { + method: 'sendMessage' + }); + } + const user = RocketChat.models.Users.findOneById(Meteor.userId(), { + fields: { + username: 1, + name: 1 + } + }); + const room = Meteor.call('canAccessRoom', message.rid, user._id); + if (!room) { + return false; + } + const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId()); + if (subscription && subscription.blocked || subscription.blocker) { + RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { + _id: Random.id(), + rid: room._id, + ts: new Date, + msg: TAPi18n.__('room_is_blocked', {}, user.language) + }); + return false; + } + + if ((room.muted||[]).includes(user.username)) { + RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { + _id: Random.id(), + rid: room._id, + ts: new Date, + msg: TAPi18n.__('You_have_been_muted', {}, user.language) + }); + return false; + } + if (message.alias == null && RocketChat.settings.get('Message_SetNameToAliasEnabled')) { + message.alias = user.name; + } + if (Meteor.settings['public'].sandstorm) { + message.sandstormSessionId = this.connection.sandstormSessionId(); + } + RocketChat.metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 + return RocketChat.sendMessage(user, message, room); + } +}); +// Limit a user, who does not have the "bot" role, to sending 5 msgs/second +DDPRateLimiter.addRule({ + type: 'method', + name: 'sendMessage', + userId(userId) { + const user = RocketChat.models.Users.findOneById(userId); + if (user == null || !user.roles) { + return true; + } + return user.roles.includes('bot'); + } +}, 5, 1000); diff --git a/packages/rocketchat-lib/server/models/Messages.js b/packages/rocketchat-lib/server/models/Messages.js new file mode 100644 index 00000000000..dabf1893663 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Messages.js @@ -0,0 +1,580 @@ +RocketChat.models.Messages = new class extends RocketChat.models._Base { + constructor() { + super('message'); + + this.tryEnsureIndex({ 'rid': 1, 'ts': 1 }); + this.tryEnsureIndex({ 'ts': 1 }); + this.tryEnsureIndex({ 'u._id': 1 }); + this.tryEnsureIndex({ 'editedAt': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'editedBy._id': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'rid': 1, 't': 1, 'u._id': 1 }); + this.tryEnsureIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }); + this.tryEnsureIndex({ 'msg': 'text' }); + this.tryEnsureIndex({ 'file._id': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'mentions.username': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'pinned': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'snippeted': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'location': '2dsphere' }); + this.tryEnsureIndex({ 'slackBotId': 1, 'slackTs': 1 }, { sparse: 1 }); + } + + // FIND + findByMention(username, options) { + const query = {'mentions.username': username}; + + return this.find(query, options); + } + + findVisibleByMentionAndRoomId(username, rid, options) { + const query = { + _hidden: { $ne: true }, + 'mentions.username': username, + rid + }; + + return this.find(query, options); + } + + findVisibleByRoomId(roomId, options) { + const query = { + _hidden: { + $ne: true + }, + + rid: roomId + }; + + return this.find(query, options); + } + + findVisibleByRoomIdNotContainingTypes(roomId, types, options) { + const query = { + _hidden: { + $ne: true + }, + + rid: roomId + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findInvisibleByRoomId(roomId, options) { + const query = { + _hidden: true, + rid: roomId + }; + + return this.find(query, options); + } + + findVisibleByRoomIdAfterTimestamp(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestamp(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lt: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestampInclusive(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lte: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestamps(roomId, afterTimestamp, beforeTimestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: afterTimestamp, + $lt: beforeTimestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestampsInclusive(roomId, afterTimestamp, beforeTimestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gte: afterTimestamp, + $lte: beforeTimestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestampNotContainingTypes(roomId, timestamp, types, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lt: timestamp + } + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestampsNotContainingTypes(roomId, afterTimestamp, beforeTimestamp, types, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: afterTimestamp, + $lt: beforeTimestamp + } + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findVisibleCreatedOrEditedAfterTimestamp(timestamp, options) { + const query = { + _hidden: { $ne: true }, + $or: [{ + ts: { + $gt: timestamp + } + }, + { + 'editedAt': { + $gt: timestamp + } + } + ] + }; + + return this.find(query, options); + } + + findStarredByUserAtRoom(userId, roomId, options) { + const query = { + _hidden: { $ne: true }, + 'starred._id': userId, + rid: roomId + }; + + return this.find(query, options); + } + + findPinnedByRoom(roomId, options) { + const query = { + t: { $ne: 'rm' }, + _hidden: { $ne: true }, + pinned: true, + rid: roomId + }; + + return this.find(query, options); + } + + findSnippetedByRoom(roomId, options) { + const query = { + _hidden: { $ne: true }, + snippeted: true, + rid: roomId + }; + + return this.find(query, options); + } + + getLastTimestamp(options) { + if (options == null) { options = {}; } + const query = { ts: { $exists: 1 } }; + options.sort = { ts: -1 }; + options.limit = 1; + const [message] = this.find(query, options).fetch(); + return message && message.ts; + } + + findByRoomIdAndMessageIds(rid, messageIds, options) { + const query = { + rid, + _id: { + $in: messageIds + } + }; + + return this.find(query, options); + } + + findOneBySlackBotIdAndSlackTs(slackBotId, slackTs) { + const query = { + slackBotId, + slackTs + }; + + return this.findOne(query); + } + + findOneBySlackTs(slackTs) { + const query = {slackTs}; + + return this.findOne(query); + } + + cloneAndSaveAsHistoryById(_id) { + const me = RocketChat.models.Users.findOneById(Meteor.userId()); + const record = this.findOneById(_id); + record._hidden = true; + record.parent = record._id; + record.editedAt = new Date; + record.editedBy = { + _id: Meteor.userId(), + username: me.username + }; + delete record._id; + return this.insert(record); + } + + // UPDATE + setHiddenById(_id, hidden) { + if (hidden == null) { hidden = true; } + const query = {_id}; + + const update = { + $set: { + _hidden: hidden + } + }; + + return this.update(query, update); + } + + setAsDeletedByIdAndUser(_id, user) { + const query = {_id}; + + const update = { + $set: { + msg: '', + t: 'rm', + urls: [], + mentions: [], + attachments: [], + reactions: [], + editedAt: new Date(), + editedBy: { + _id: user._id, + username: user.username + } + } + }; + + return this.update(query, update); + } + + setPinnedByIdAndUserId(_id, pinnedBy, pinned, pinnedAt) { + if (pinned == null) { pinned = true; } + if (pinnedAt == null) { pinnedAt = 0; } + const query = {_id}; + + const update = { + $set: { + pinned, + pinnedAt: pinnedAt || new Date, + pinnedBy + } + }; + + return this.update(query, update); + } + + setSnippetedByIdAndUserId(message, snippetName, snippetedBy, snippeted, snippetedAt) { + if (snippeted == null) { snippeted = true; } + if (snippetedAt == null) { snippetedAt = 0; } + const query = {_id: message._id}; + + const msg = `\`\`\`${ message.msg }\`\`\``; + + const update = { + $set: { + msg, + snippeted, + snippetedAt: snippetedAt || new Date, + snippetedBy, + snippetName + } + }; + + return this.update(query, update); + } + + setUrlsById(_id, urls) { + const query = {_id}; + + const update = { + $set: { + urls + } + }; + + return this.update(query, update); + } + + updateAllUsernamesByUserId(userId, username) { + const query = {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + updateUsernameOfEditByUserId(userId, username) { + const query = {'editedBy._id': userId}; + + const update = { + $set: { + 'editedBy.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + updateUsernameAndMessageOfMentionByIdAndOldUsername(_id, oldUsername, newUsername, newMessage) { + const query = { + _id, + 'mentions.username': oldUsername + }; + + const update = { + $set: { + 'mentions.$.username': newUsername, + 'msg': newMessage + } + }; + + return this.update(query, update); + } + + updateUserStarById(_id, userId, starred) { + let update; + const query = {_id}; + + if (starred) { + update = { + $addToSet: { + starred: { _id: userId } + } + }; + } else { + update = { + $pull: { + starred: { _id: Meteor.userId() } + } + }; + } + + return this.update(query, update); + } + + upgradeEtsToEditAt() { + const query = {ets: { $exists: 1 }}; + + const update = { + $rename: { + 'ets': 'editedAt' + } + }; + + return this.update(query, update, { multi: true }); + } + + setMessageAttachments(_id, attachments) { + const query = {_id}; + + const update = { + $set: { + attachments + } + }; + + return this.update(query, update); + } + + setSlackBotIdAndSlackTs(_id, slackBotId, slackTs) { + const query = {_id}; + + const update = { + $set: { + slackBotId, + slackTs + } + }; + + return this.update(query, update); + } + + + // INSERT + createWithTypeRoomIdMessageAndUser(type, roomId, message, user, extraData) { + const room = RocketChat.models.Rooms.findOneById(roomId, { fields: { sysMes: 1 }}); + if ((room != null ? room.sysMes : undefined) === false) { + return; + } + const record = { + t: type, + rid: roomId, + ts: new Date, + msg: message, + u: { + _id: user._id, + username: user.username + }, + groupable: false + }; + + _.extend(record, extraData); + + record._id = this.insertOrUpsert(record); + RocketChat.models.Rooms.incMsgCountById(room._id, 1); + return record; + } + + createUserJoinWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('uj', roomId, message, user, extraData); + } + + createUserLeaveWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('ul', roomId, message, user, extraData); + } + + createUserRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('ru', roomId, message, user, extraData); + } + + createUserAddedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('au', roomId, message, user, extraData); + } + + createCommandWithRoomIdAndUser(command, roomId, user, extraData) { + return this.createWithTypeRoomIdMessageAndUser('command', roomId, command, user, extraData); + } + + createUserMutedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('user-muted', roomId, message, user, extraData); + } + + createUserUnmutedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('user-unmuted', roomId, message, user, extraData); + } + + createNewModeratorWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('new-moderator', roomId, message, user, extraData); + } + + createModeratorRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('moderator-removed', roomId, message, user, extraData); + } + + createNewOwnerWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('new-owner', roomId, message, user, extraData); + } + + createOwnerRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('owner-removed', roomId, message, user, extraData); + } + + createSubscriptionRoleAddedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('subscription-role-added', roomId, message, user, extraData); + } + + createSubscriptionRoleRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('subscription-role-removed', roomId, message, user, extraData); + } + + // REMOVE + removeById(_id) { + const query = {_id}; + + return this.remove(query); + } + + removeByRoomId(roomId) { + const query = {rid: roomId}; + + return this.remove(query); + } + + removeByUserId(userId) { + const query = {'u._id': userId}; + + return this.remove(query); + } + + getMessageByFileId(fileID) { + return this.findOne({ 'file._id': fileID }); + } +}; diff --git a/packages/rocketchat-lib/server/models/Rooms.js b/packages/rocketchat-lib/server/models/Rooms.js new file mode 100644 index 00000000000..763155e1c83 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Rooms.js @@ -0,0 +1,760 @@ +class ModelRooms extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'name': 1 }, { unique: 1, sparse: 1 }); + this.tryEnsureIndex({ 'default': 1 }); + this.tryEnsureIndex({ 'usernames': 1 }); + this.tryEnsureIndex({ 't': 1 }); + this.tryEnsureIndex({ 'u._id': 1 }); + + this.cache.ignoreUpdatedFields.push('msgs', 'lm'); + this.cache.ensureIndex(['t', 'name'], 'unique'); + this.cache.options = {fields: {usernames: 0}}; + } + + findOneByIdOrName(_idOrName, options) { + const query = { + $or: [{ + _id: _idOrName + }, { + name: _idOrName + }] + }; + + return this.findOne(query, options); + } + + findOneByImportId(_id, options) { + const query = {importIds: _id}; + + return this.findOne(query, options); + } + + findOneByName(name, options) { + const query = {name}; + + return this.findOne(query, options); + } + + findOneByNameAndType(name, type, options) { + const query = { + name, + t: type + }; + + return this.findOne(query, options); + } + + findOneByIdContainingUsername(_id, username, options) { + const query = { + _id, + usernames: username + }; + + return this.findOne(query, options); + } + + findOneByNameAndTypeNotContainingUsername(name, type, username, options) { + const query = { + name, + t: type, + usernames: { + $ne: username + } + }; + + return this.findOne(query, options); + } + + + // FIND + + findById(roomId, options) { + return this.find({ _id: roomId }, options); + } + + findByIds(roomIds, options) { + return this.find({ _id: {$in: [].concat(roomIds)} }, options); + } + + findByType(type, options) { + const query = {t: type}; + + return this.find(query, options); + } + + findByTypes(types, options) { + const query = { + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByUserId(userId, options) { + const query = {'u._id': userId}; + + return this.find(query, options); + } + + findBySubscriptionUserId(userId, options) { + let data; + if (this.useCache) { + data = RocketChat.models.Subscriptions.findByUserId(userId).fetch(); + data = data.map(function(item) { + if (item._room) { + return item._room; + } + console.log('Empty Room for Subscription', item); + return {}; + }); + return this.arrayToCursor(this.processQueryOptionsOnResult(data, options)); + } + + data = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch(); + data = data.map(item => item.rid); + + const query = { + _id: { + $in: data + } + }; + + return this.find(query, options); + } + + findBySubscriptionUserIdUpdatedAfter(userId, _updatedAt, options) { + if (this.useCache) { + let data = RocketChat.models.Subscriptions.findByUserId(userId).fetch(); + data = data.map(function(item) { + if (item._room) { + return item._room; + } + console.log('Empty Room for Subscription', item); + return {}; + }); + data = data.filter(item => item._updatedAt > _updatedAt); + return this.arrayToCursor(this.processQueryOptionsOnResult(data, options)); + } + + let ids = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch(); + ids = ids.map(item => item.rid); + + const query = { + _id: { + $in: ids + }, + _updatedAt: { + $gt: _updatedAt + } + }; + + return this.find(query, options); + } + + findByNameContaining(name, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const query = { + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByNameContainingTypesWithUsername(name, types, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const $or = []; + for (const type of Array.from(types)) { + const obj = {name: nameRegex, t: type.type}; + if (type.username != null) { + obj.usernames = type.username; + } + if (type.ids != null) { + obj._id = {$in: type.ids}; + } + $or.push(obj); + } + + const query = {$or}; + + return this.find(query, options); + } + + findContainingTypesWithUsername(types, options) { + + const $or = []; + for (const type of Array.from(types)) { + const obj = {t: type.type}; + if (type.username != null) { + obj.usernames = type.username; + } + if (type.ids != null) { + obj._id = {$in: type.ids}; + } + $or.push(obj); + } + + const query = {$or}; + + return this.find(query, options); + } + + findByNameContainingAndTypes(name, types, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const query = { + t: { + $in: types + }, + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByNameAndTypeNotContainingUsername(name, type, username, options) { + const query = { + t: type, + name, + usernames: { + $ne: username + } + }; + + return this.find(query, options); + } + + findByNameStartingAndTypes(name, types, options) { + const nameRegex = new RegExp(`^${ s.trim(s.escapeRegExp(name)) }`, 'i'); + + const query = { + t: { + $in: types + }, + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByDefaultAndTypes(defaultValue, types, options) { + const query = { + default: defaultValue, + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByTypeContainingUsername(type, username, options) { + const query = { + t: type, + usernames: username + }; + + return this.find(query, options); + } + + findByTypeContainingUsernames(type, username, options) { + const query = { + t: type, + usernames: { $all: [].concat(username) } + }; + + return this.find(query, options); + } + + findByTypesAndNotUserIdContainingUsername(types, userId, username, options) { + const query = { + t: { + $in: types + }, + uid: { + $ne: userId + }, + usernames: username + }; + + return this.find(query, options); + } + + findByContainingUsername(username, options) { + const query = {usernames: username}; + + return this.find(query, options); + } + + findByTypeAndName(type, name, options) { + if (this.useCache) { + return this.cache.findByIndex('t,name', [type, name], options); + } + + const query = { + name, + t: type + }; + + return this.find(query, options); + } + + findByTypeAndNameContainingUsername(type, name, username, options) { + const query = { + name, + t: type, + usernames: username + }; + + return this.find(query, options); + } + + findByTypeAndArchivationState(type, archivationstate, options) { + const query = {t: type}; + + if (archivationstate) { + query.archived = true; + } else { + query.archived = { $ne: true }; + } + + return this.find(query, options); + } + + // UPDATE + addImportIds(_id, importIds) { + importIds = [].concat(importIds); + const query = {_id}; + + const update = { + $addToSet: { + importIds: { + $each: importIds + } + } + }; + + return this.update(query, update); + } + + archiveById(_id) { + const query = {_id}; + + const update = { + $set: { + archived: true + } + }; + + return this.update(query, update); + } + + unarchiveById(_id) { + const query = {_id}; + + const update = { + $set: { + archived: false + } + }; + + return this.update(query, update); + } + + addUsernameById(_id, username, muted) { + const query = {_id}; + + const update = { + $addToSet: { + usernames: username + } + }; + + if (muted) { + update.$addToSet.muted = username; + } + + return this.update(query, update); + } + + addUsernamesById(_id, usernames) { + const query = {_id}; + + const update = { + $addToSet: { + usernames: { + $each: usernames + } + } + }; + + return this.update(query, update); + } + + addUsernameByName(name, username) { + const query = {name}; + + const update = { + $addToSet: { + usernames: username + } + }; + + return this.update(query, update); + } + + removeUsernameById(_id, username) { + const query = {_id}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update); + } + + removeUsernamesById(_id, usernames) { + const query = {_id}; + + const update = { + $pull: { + usernames: { + $in: usernames + } + } + }; + + return this.update(query, update); + } + + removeUsernameFromAll(username) { + const query = {usernames: username}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update, { multi: true }); + } + + removeUsernameByName(name, username) { + const query = {name}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update); + } + + setNameById(_id, name) { + const query = {_id}; + + const update = { + $set: { + name + } + }; + + return this.update(query, update); + } + + incMsgCountById(_id, inc) { + if (inc == null) { inc = 1; } + const query = {_id}; + + const update = { + $inc: { + msgs: inc + } + }; + + return this.update(query, update); + } + + incMsgCountAndSetLastMessageTimestampById(_id, inc, lastMessageTimestamp) { + if (inc == null) { inc = 1; } + const query = {_id}; + + const update = { + $set: { + lm: lastMessageTimestamp + }, + $inc: { + msgs: inc + } + }; + + return this.update(query, update); + } + + replaceUsername(previousUsername, username) { + const query = {usernames: previousUsername}; + + const update = { + $set: { + 'usernames.$': username + } + }; + + return this.update(query, update, { multi: true }); + } + + replaceMutedUsername(previousUsername, username) { + const query = {muted: previousUsername}; + + const update = { + $set: { + 'muted.$': username + } + }; + + return this.update(query, update, { multi: true }); + } + + replaceUsernameOfUserByUserId(userId, username) { + const query = {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + setJoinCodeById(_id, joinCode) { + let update; + const query = {_id}; + + if ((joinCode != null ? joinCode.trim() : undefined) !== '') { + update = { + $set: { + joinCodeRequired: true, + joinCode + } + }; + } else { + update = { + $set: { + joinCodeRequired: false + }, + $unset: { + joinCode: 1 + } + }; + } + + return this.update(query, update); + } + + setUserById(_id, user) { + const query = {_id}; + + const update = { + $set: { + u: { + _id: user._id, + username: user.username + } + } + }; + + return this.update(query, update); + } + + setTypeById(_id, type) { + const query = {_id}; + const update = { + $set: { + t: type + } + }; + if (type === 'p') { + update.$unset = {default: ''}; + } + + return this.update(query, update); + } + + setTopicById(_id, topic) { + const query = {_id}; + + const update = { + $set: { + topic + } + }; + + return this.update(query, update); + } + + setAnnouncementById(_id, announcement) { + const query = {_id}; + + const update = { + $set: { + announcement + } + }; + + return this.update(query, update); + } + + muteUsernameByRoomId(_id, username) { + const query = {_id}; + + const update = { + $addToSet: { + muted: username + } + }; + + return this.update(query, update); + } + + unmuteUsernameByRoomId(_id, username) { + const query = {_id}; + + const update = { + $pull: { + muted: username + } + }; + + return this.update(query, update); + } + + saveDefaultById(_id, defaultValue) { + const query = {_id}; + + const update = { + $set: { + default: defaultValue === 'true' + } + }; + + return this.update(query, update); + } + + setTopicAndTagsById(_id, topic, tags) { + const setData = {}; + const unsetData = {}; + + if (topic != null) { + if (!_.isEmpty(s.trim(topic))) { + setData.topic = s.trim(topic); + } else { + unsetData.topic = 1; + } + } + + if (tags != null) { + if (!_.isEmpty(s.trim(tags))) { + setData.tags = s.trim(tags).split(',').map(tag => s.trim(tag)); + } else { + unsetData.tags = 1; + } + } + + const update = {}; + + if (!_.isEmpty(setData)) { + update.$set = setData; + } + + if (!_.isEmpty(unsetData)) { + update.$unset = unsetData; + } + + if (_.isEmpty(update)) { + return; + } + + return this.update({ _id }, update); + } + + // INSERT + createWithTypeNameUserAndUsernames(type, name, user, usernames, extraData) { + const room = { + name, + t: type, + usernames, + msgs: 0, + u: { + _id: user._id, + username: user.username + } + }; + + _.extend(room, extraData); + + room._id = this.insert(room); + return room; + } + + createWithIdTypeAndName(_id, type, name, extraData) { + const room = { + _id, + ts: new Date(), + t: type, + name, + usernames: [], + msgs: 0 + }; + + _.extend(room, extraData); + + this.insert(room); + return room; + } + + + // REMOVE + removeById(_id) { + const query = {_id}; + + return this.remove(query); + } + + removeByTypeContainingUsername(type, username) { + const query = { + t: type, + usernames: username + }; + + return this.remove(query); + } +} + +RocketChat.models.Rooms = new ModelRooms('room', true); diff --git a/packages/rocketchat-lib/server/models/Settings.js b/packages/rocketchat-lib/server/models/Settings.js new file mode 100644 index 00000000000..7da29bee37f --- /dev/null +++ b/packages/rocketchat-lib/server/models/Settings.js @@ -0,0 +1,178 @@ +class ModelSettings extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'blocked': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'hidden': 1 }, { sparse: 1 }); + } + + // FIND + findById(_id) { + const query = {_id}; + + return this.find(query); + } + + findOneNotHiddenById(_id) { + const query = { + _id, + hidden: { $ne: true } + }; + + return this.findOne(query); + } + + findByIds(_id = []) { + _id = [].concat(_id); + + const query = { + _id: { + $in: _id + } + }; + + return this.find(query); + } + + findByRole(role, options) { + const query = {role}; + + return this.find(query, options); + } + + findPublic(options) { + const query = {public: true}; + + return this.find(query, options); + } + + findNotHiddenPublic(ids = []) { + const filter = { + hidden: { $ne: true }, + public: true + }; + + if (ids.length > 0) { + filter._id = + {$in: ids}; + } + + return this.find(filter, { fields: {_id: 1, value: 1} }); + } + + findNotHiddenPublicUpdatedAfter(updatedAt) { + const filter = { + hidden: { $ne: true }, + public: true, + _updatedAt: { + $gt: updatedAt + } + }; + + return this.find(filter, { fields: {_id: 1, value: 1} }); + } + + findNotHiddenPrivate() { + return this.find({ + hidden: { $ne: true }, + public: { $ne: true } + }); + } + + findNotHidden(options) { + return this.find({ hidden: { $ne: true } }, options); + } + + findNotHiddenUpdatedAfter(updatedAt) { + return this.find({ + hidden: { $ne: true }, + _updatedAt: { + $gt: updatedAt + } + }); + } + + // UPDATE + updateValueById(_id, value) { + const query = { + blocked: { $ne: true }, + value: { $ne: value }, + _id + }; + + const update = { + $set: { + value + } + }; + + return this.update(query, update); + } + + updateValueAndEditorById(_id, value, editor) { + const query = { + blocked: { $ne: true }, + value: { $ne: value }, + _id + }; + + const update = { + $set: { + value, + editor + } + }; + + return this.update(query, update); + } + + updateValueNotHiddenById(_id, value) { + const query = { + _id, + hidden: { $ne: true }, + blocked: { $ne: true } + }; + + const update = { + $set: { + value + } + }; + + return this.update(query, update); + } + + updateOptionsById(_id, options) { + const query = { + blocked: { $ne: true }, + _id + }; + + const update = {$set: options}; + + return this.update(query, update); + } + + // INSERT + createWithIdAndValue(_id, value) { + const record = { + _id, + value, + _createdAt: new Date + }; + + return this.insert(record); + } + + // REMOVE + removeById(_id) { + const query = { + blocked: { $ne: true }, + _id + }; + + return this.remove(query); + } +} + +RocketChat.models.Settings = new ModelSettings('settings', true); diff --git a/packages/rocketchat-lib/server/models/Subscriptions.js b/packages/rocketchat-lib/server/models/Subscriptions.js new file mode 100644 index 00000000000..67986040e02 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Subscriptions.js @@ -0,0 +1,570 @@ +class ModelSubscriptions extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'rid': 1, 'u._id': 1 }, { unique: 1 }); + this.tryEnsureIndex({ 'rid': 1, 'alert': 1, 'u._id': 1 }); + this.tryEnsureIndex({ 'rid': 1, 'roles': 1 }); + this.tryEnsureIndex({ 'u._id': 1, 'name': 1, 't': 1 }); + 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({ 'ts': 1 }); + this.tryEnsureIndex({ 'ls': 1 }); + this.tryEnsureIndex({ 'audioNotification': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'desktopNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'mobilePushNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'emailNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'autoTranslate': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'autoTranslateLanguage': 1 }, { sparse: 1 }); + + this.cache.ensureIndex('rid', 'array'); + this.cache.ensureIndex('u._id', 'array'); + this.cache.ensureIndex('name', 'array'); + this.cache.ensureIndex(['rid', 'u._id'], 'unique'); + this.cache.ensureIndex(['name', 'u._id'], 'unique'); + } + + + // FIND ONE + findOneByRoomIdAndUserId(roomId, userId) { + if (this.useCache) { + return this.cache.findByIndex('rid,u._id', [roomId, userId]).fetch(); + } + const query = { + rid: roomId, + 'u._id': userId + }; + + return this.findOne(query); + } + + findOneByRoomNameAndUserId(roomName, userId) { + if (this.useCache) { + return this.cache.findByIndex('name,u._id', [roomName, userId]).fetch(); + } + const query = { + name: roomName, + 'u._id': userId + }; + + return this.findOne(query); + } + + // FIND + findByUserId(userId, options) { + if (this.useCache) { + return this.cache.findByIndex('u._id', userId, options); + } + + const query = + {'u._id': userId}; + + return this.find(query, options); + } + + findByUserIdUpdatedAfter(userId, updatedAt, options) { + const query = { + 'u._id': userId, + _updatedAt: { + $gt: updatedAt + } + }; + + return this.find(query, options); + } + + // FIND + findByRoomIdAndRoles(roomId, roles, options) { + roles = [].concat(roles); + const query = { + 'rid': roomId, + 'roles': { $in: roles } + }; + + return this.find(query, options); + } + + findByType(types, options) { + const query = { + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByTypeAndUserId(type, userId, options) { + const query = { + t: type, + 'u._id': userId + }; + + return this.find(query, options); + } + + findByTypeNameAndUserId(type, name, userId, options) { + const query = { + t: type, + name, + 'u._id': userId + }; + + return this.find(query, options); + } + + findByRoomId(roomId, options) { + if (this.useCache) { + return this.cache.findByIndex('rid', roomId, options); + } + + const query = + {rid: roomId}; + + return this.find(query, options); + } + + findByRoomIdAndNotUserId(roomId, userId, options) { + const query = { + rid: roomId, + 'u._id': { + $ne: userId + } + }; + + return this.find(query, options); + } + + getLastSeen(options) { + if (options == null) { options = {}; } + const query = { ls: { $exists: 1 } }; + options.sort = { ls: -1 }; + options.limit = 1; + const [subscription] = this.find(query, options).fetch(); + return subscription && subscription.ls; + } + + findByRoomIdAndUserIds(roomId, userIds) { + const query = { + rid: roomId, + 'u._id': { + $in: userIds + } + }; + + return this.find(query); + } + + // UPDATE + archiveByRoomId(roomId) { + const query = + {rid: roomId}; + + const update = { + $set: { + alert: false, + open: false, + archived: true + } + }; + + return this.update(query, update, { multi: true }); + } + + unarchiveByRoomId(roomId) { + const query = + {rid: roomId}; + + const update = { + $set: { + alert: false, + open: true, + archived: false + } + }; + + return this.update(query, update, { multi: true }); + } + + hideByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + alert: false, + open: false + } + }; + + return this.update(query, update); + } + + openByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true + } + }; + + return this.update(query, update); + } + + setAsReadByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true, + alert: false, + unread: 0, + ls: new Date + } + }; + + return this.update(query, update); + } + + setAsUnreadByRoomIdAndUserId(roomId, userId, firstMessageUnreadTimestamp) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true, + alert: true, + ls: firstMessageUnreadTimestamp + } + }; + + return this.update(query, update); + } + + setFavoriteByRoomIdAndUserId(roomId, userId, favorite) { + if (favorite == null) { favorite = true; } + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + f: favorite + } + }; + + return this.update(query, update); + } + + updateNameAndAlertByRoomId(roomId, name) { + const query = + {rid: roomId}; + + const update = { + $set: { + name, + alert: true + } + }; + + return this.update(query, update, { multi: true }); + } + + updateNameByRoomId(roomId, name) { + const query = + {rid: roomId}; + + const update = { + $set: { + name + } + }; + + return this.update(query, update, { multi: true }); + } + + setUserUsernameByUserId(userId, username) { + const query = + {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + setNameForDirectRoomsWithOldName(oldName, name) { + const query = { + name: oldName, + t: 'd' + }; + + const update = { + $set: { + name + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadOfDirectForRoomIdExcludingUserId(roomId, userId, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + t: 'd', + 'u._id': { + $ne: userId + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadForRoomIdExcludingUserId(roomId, userId, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + 'u._id': { + $ne: userId + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadForRoomIdAndUserIds(roomId, userIds, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + 'u._id': { + $in: userIds + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + setAlertForRoomIdExcludingUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': { + $ne: userId + }, + $or: [ + { alert: { $ne: true } }, + { open: { $ne: true } } + ] + }; + + const update = { + $set: { + alert: true, + open: true + } + }; + + return this.update(query, update, { multi: true }); + } + + setBlockedByRoomId(rid, blocked, blocker) { + const query = { + rid, + 'u._id': blocked + }; + + const update = { + $set: { + blocked: true + } + }; + + const query2 = { + rid, + 'u._id': blocker + }; + + const update2 = { + $set: { + blocker: true + } + }; + + return this.update(query, update) && this.update(query2, update2); + } + + unsetBlockedByRoomId(rid, blocked, blocker) { + const query = { + rid, + 'u._id': blocked + }; + + const update = { + $unset: { + blocked: 1 + } + }; + + const query2 = { + rid, + 'u._id': blocker + }; + + const update2 = { + $unset: { + blocker: 1 + } + }; + + return this.update(query, update) && this.update(query2, update2); + } + + updateTypeByRoomId(roomId, type) { + const query = + {rid: roomId}; + + const update = { + $set: { + t: type + } + }; + + return this.update(query, update, { multi: true }); + } + + addRoleById(_id, role) { + const query = + {_id}; + + const update = { + $addToSet: { + roles: role + } + }; + + return this.update(query, update); + } + + removeRoleById(_id, role) { + const query = + {_id}; + + const update = { + $pull: { + roles: role + } + }; + + return this.update(query, update); + } + + setArchivedByUsername(username, archived) { + const query = { + t: 'd', + name: username + }; + + const update = { + $set: { + archived + } + }; + + return this.update(query, update, { multi: true }); + } + + // INSERT + createWithRoomAndUser(room, user, extraData) { + const subscription = { + open: false, + alert: false, + unread: 0, + ts: room.ts, + rid: room._id, + name: room.name, + t: room.t, + u: { + _id: user._id, + username: user.username + } + }; + + _.extend(subscription, extraData); + + return this.insert(subscription); + } + + + // REMOVE + removeByUserId(userId) { + const query = + {'u._id': userId}; + + return this.remove(query); + } + + removeByRoomId(roomId) { + const query = + {rid: roomId}; + + return this.remove(query); + } + + removeByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + return this.remove(query); + } +} + +RocketChat.models.Subscriptions = new ModelSubscriptions('subscription', true); diff --git a/packages/rocketchat-lib/server/models/Uploads.js b/packages/rocketchat-lib/server/models/Uploads.js new file mode 100644 index 00000000000..252d48fb878 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Uploads.js @@ -0,0 +1,91 @@ +RocketChat.models.Uploads = new class extends RocketChat.models._Base { + constructor() { + super('uploads'); + + this.tryEnsureIndex({ 'rid': 1 }); + this.tryEnsureIndex({ 'uploadedAt': 1 }); + } + + findNotHiddenFilesOfRoom(roomId, limit) { + const fileQuery = { + rid: roomId, + complete: true, + uploading: false, + _hidden: { + $ne: true + } + }; + + const fileOptions = { + limit, + sort: { + uploadedAt: -1 + }, + fields: { + _id: 1, + userId: 1, + rid: 1, + name: 1, + description: 1, + type: 1, + url: 1, + uploadedAt: 1 + } + }; + + return this.find(fileQuery, fileOptions); + } + + insertFileInit(roomId, userId, store, file, extra) { + const fileData = { + rid: roomId, + userId, + store, + complete: false, + uploading: true, + progress: 0, + extension: s.strRightBack(file.name, '.'), + uploadedAt: new Date() + }; + + _.extend(fileData, file, extra); + + if ((this.model.direct != null ? this.model.direct.insert : undefined) != null) { + file = this.model.direct.insert(fileData); + } else { + file = this.insert(fileData); + } + + return file; + } + + updateFileComplete(fileId, userId, file) { + let result; + if (!fileId) { + return; + } + + const filter = { + _id: fileId, + userId + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1 + } + }; + + update.$set = _.extend(file, update.$set); + + if ((this.model.direct != null ? this.model.direct.insert : undefined) != null) { + result = this.model.direct.update(filter, update); + } else { + result = this.update(filter, update); + } + + return result; + } +}; diff --git a/packages/rocketchat-lib/server/models/Users.js b/packages/rocketchat-lib/server/models/Users.js new file mode 100644 index 00000000000..941416475c6 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Users.js @@ -0,0 +1,538 @@ +class ModelUsers extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'roles': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'name': 1 }); + this.tryEnsureIndex({ 'lastLogin': 1 }); + this.tryEnsureIndex({ 'status': 1 }); + this.tryEnsureIndex({ 'active': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'statusConnection': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'type': 1 }); + + this.cache.ensureIndex('username', 'unique'); + } + + findOneByImportId(_id, options) { + return this.findOne({ importIds: _id }, options); + } + + findOneByUsername(username, options) { + const query = {username}; + + return this.findOne(query, options); + } + + findOneByEmailAddress(emailAddress, options) { + const query = {'emails.address': new RegExp(`^${ s.escapeRegExp(emailAddress) }$`, 'i')}; + + return this.findOne(query, options); + } + + findOneAdmin(admin, options) { + const query = {admin}; + + return this.findOne(query, options); + } + + findOneByIdAndLoginToken(_id, token, options) { + const query = { + _id, + 'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token) + }; + + return this.findOne(query, options); + } + + + // FIND + findById(userId) { + const query = {_id: userId}; + + return this.find(query); + } + + findUsersNotOffline(options) { + const query = { + username: { + $exists: 1 + }, + status: { + $in: ['online', 'away', 'busy'] + } + }; + + return this.find(query, options); + } + + + findByUsername(username, options) { + const query = {username}; + + return this.find(query, options); + } + + findUsersByUsernamesWithHighlights(usernames, options) { + if (this.useCache) { + const result = { + fetch() { + return RocketChat.models.Users.getDynamicView('highlights').data().filter(record => usernames.indexOf(record.username) > -1); + }, + count() { + return result.fetch().length; + }, + forEach(fn) { + return result.fetch().forEach(fn); + } + }; + return result; + } + + const query = { + username: { $in: usernames }, + 'settings.preferences.highlights.0': { + $exists: true + } + }; + + return this.find(query, options); + } + + findActiveByUsernameOrNameRegexWithExceptions(searchTerm, exceptions, options) { + if (exceptions == null) { exceptions = []; } + if (options == null) { options = {}; } + if (!_.isArray(exceptions)) { + exceptions = [ exceptions ]; + } + + const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i'); + const query = { + $or: [{ + username: termRegex + }, { + name: termRegex + }], + active: true, + type: { + $in: ['user', 'bot'] + }, + $and: [{ + username: { + $exists: true + } + }, { + username: { + $nin: exceptions + } + }] + }; + + return this.find(query, options); + } + + findByActiveUsersExcept(searchTerm, exceptions, options) { + if (exceptions == null) { exceptions = []; } + if (options == null) { options = {}; } + if (!_.isArray(exceptions)) { + exceptions = [ exceptions ]; + } + + const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i'); + const query = { + $and: [ + { + active: true, + $or: [ + { + username: termRegex + }, + { + name: termRegex + } + ] + }, + { + username: { $exists: true, $nin: exceptions } + } + ] + }; + + return this.find(query, options); + } + + findUsersByNameOrUsername(nameOrUsername, options) { + const query = { + username: { + $exists: 1 + }, + + $or: [ + {name: nameOrUsername}, + {username: nameOrUsername} + ], + + type: { + $in: ['user'] + } + }; + + return this.find(query, options); + } + + findByUsernameNameOrEmailAddress(usernameNameOrEmailAddress, options) { + const query = { + $or: [ + {name: usernameNameOrEmailAddress}, + {username: usernameNameOrEmailAddress}, + {'emails.address': usernameNameOrEmailAddress} + ], + type: { + $in: ['user', 'bot'] + } + }; + + return this.find(query, options); + } + + findLDAPUsers(options) { + const query = {ldap: true}; + + return this.find(query, options); + } + + findCrowdUsers(options) { + const query = {crowd: true}; + + return this.find(query, options); + } + + getLastLogin(options) { + if (options == null) { options = {}; } + const query = { lastLogin: { $exists: 1 } }; + options.sort = { lastLogin: -1 }; + options.limit = 1; + const [user] = this.find(query, options).fetch(); + return user && user.lastLogin; + } + + findUsersByUsernames(usernames, options) { + const query = { + username: { + $in: usernames + } + }; + + return this.find(query, options); + } + + // UPDATE + addImportIds(_id, importIds) { + importIds = [].concat(importIds); + + const query = {_id}; + + const update = { + $addToSet: { + importIds: { + $each: importIds + } + } + }; + + return this.update(query, update); + } + + updateLastLoginById(_id) { + const update = { + $set: { + lastLogin: new Date + } + }; + + return this.update(_id, update); + } + + setServiceId(_id, serviceName, serviceId) { + const update = + {$set: {}}; + + const serviceIdKey = `services.${ serviceName }.id`; + update.$set[serviceIdKey] = serviceId; + + return this.update(_id, update); + } + + setUsername(_id, username) { + const update = + {$set: {username}}; + + return this.update(_id, update); + } + + setEmail(_id, email) { + const update = { + $set: { + emails: [{ + address: email, + verified: false + } + ] + } + }; + + return this.update(_id, update); + } + + setEmailVerified(_id, email) { + const query = { + _id, + emails: { + $elemMatch: { + address: email, + verified: false + } + } + }; + + const update = { + $set: { + 'emails.$.verified': true + } + }; + + return this.update(query, update); + } + + setName(_id, name) { + const update = { + $set: { + name + } + }; + + return this.update(_id, update); + } + + setCustomFields(_id, fields) { + const values = {}; + Object.keys(fields).reduce(key => { + const value = fields[key]; + values[`customFields.${ key }`] = value; + }); + + const update = {$set: values}; + + return this.update(_id, update); + } + + setAvatarOrigin(_id, origin) { + const update = { + $set: { + avatarOrigin: origin + } + }; + + return this.update(_id, update); + } + + unsetAvatarOrigin(_id) { + const update = { + $unset: { + avatarOrigin: 1 + } + }; + + return this.update(_id, update); + } + + setUserActive(_id, active) { + if (active == null) { active = true; } + const update = { + $set: { + active + } + }; + + return this.update(_id, update); + } + + setAllUsersActive(active) { + const update = { + $set: { + active + } + }; + + return this.update({}, update, { multi: true }); + } + + unsetLoginTokens(_id) { + const update = { + $set: { + 'services.resume.loginTokens' : [] + } + }; + + return this.update(_id, update); + } + + unsetRequirePasswordChange(_id) { + const update = { + $unset: { + 'requirePasswordChange' : true, + 'requirePasswordChangeReason' : true + } + }; + + return this.update(_id, update); + } + + resetPasswordAndSetRequirePasswordChange(_id, requirePasswordChange, requirePasswordChangeReason) { + const update = { + $unset: { + 'services.password': 1 + }, + $set: { + requirePasswordChange, + requirePasswordChangeReason + } + }; + + return this.update(_id, update); + } + + setLanguage(_id, language) { + const update = { + $set: { + language + } + }; + + return this.update(_id, update); + } + + setProfile(_id, profile) { + const update = { + $set: { + 'settings.profile': profile + } + }; + + return this.update(_id, update); + } + + setPreferences(_id, preferences) { + const update = { + $set: { + 'settings.preferences': preferences + } + }; + + return this.update(_id, update); + } + + setUtcOffset(_id, utcOffset) { + const query = { + _id, + utcOffset: { + $ne: utcOffset + } + }; + + const update = { + $set: { + utcOffset + } + }; + + return this.update(query, update); + } + + saveUserById(_id, data) { + const setData = {}; + const unsetData = {}; + + if (data.name != null) { + if (!_.isEmpty(s.trim(data.name))) { + setData.name = s.trim(data.name); + } else { + unsetData.name = 1; + } + } + + if (data.email != null) { + if (!_.isEmpty(s.trim(data.email))) { + setData.emails = [{address: s.trim(data.email)}]; + } else { + unsetData.emails = 1; + } + } + + if (data.phone != null) { + if (!_.isEmpty(s.trim(data.phone))) { + setData.phone = [{phoneNumber: s.trim(data.phone)}]; + } else { + unsetData.phone = 1; + } + } + + const update = {}; + + if (!_.isEmpty(setData)) { + update.$set = setData; + } + + if (!_.isEmpty(unsetData)) { + update.$unset = unsetData; + } + + if (_.isEmpty(update)) { + return true; + } + + return this.update({ _id }, update); + } + +// INSERT + create(data) { + const user = { + createdAt: new Date, + avatarOrigin: 'none' + }; + + _.extend(user, data); + + return this.insert(user); + } + + +// REMOVE + removeById(_id) { + return this.remove(_id); + } + +/* +Find users to send a message by email if: +- he is not online +- has a verified email +- has not disabled email notifications +- `active` is equal to true (false means they were deactivated and can't login) +*/ + getUsersToSendOfflineEmail(usersIds) { + const query = { + _id: { + $in: usersIds + }, + active: true, + status: 'offline', + statusConnection: { + $ne: 'online' + }, + 'emails.verified': true + }; + + return this.find(query, { fields: { name: 1, username: 1, emails: 1, 'settings.preferences.emailNotificationMode': 1 } }); + } +} + +RocketChat.models.Users = new ModelUsers(Meteor.users, true); -- GitLab