diff --git a/.meteor/packages b/.meteor/packages index b56ad45f922..a97cca862ec 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -62,6 +62,7 @@ rocketchat:slashcommands-invite rocketchat:slashcommands-join rocketchat:slashcommands-leave rocketchat:slashcommands-kick +rocketchat:slashcommands-mute rocketchat:spotify rocketchat:statistics rocketchat:theme diff --git a/.meteor/versions b/.meteor/versions index 5ec2cf20d5f..65d76ada12a 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -152,6 +152,7 @@ rocketchat:slashcommands-invite@0.0.1 rocketchat:slashcommands-join@0.0.1 rocketchat:slashcommands-kick@0.0.1 rocketchat:slashcommands-leave@0.0.1 +rocketchat:slashcommands-mute@0.0.1 rocketchat:spotify@0.0.1 rocketchat:statistics@0.0.1 rocketchat:theme@0.0.1 diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index e761fe82684..92ba3902f8e 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -292,6 +292,8 @@ "More_unreads" : "More unreads", "Msgs" : "Msgs", "multi" : "multi", + "Mute_user" : "Mute user", + "Unmute_user" : "Unmute user", "My_Account" : "My Account", "n_messages" : "%s messages", "Name" : "Name", @@ -496,9 +498,13 @@ "User_left_female" : "Has left the channel.", "User_left_male" : "Has left the channel.", "User_logged_out" : "User is logged out", + "User_muted_in_room" : "User muted in room", + "User_unmuted_in_room" : "User unmuted in room", "User_not_found_or_incorrect_password" : "User not found or incorrect password", "User_or_channel_name" : "User or channel name", "User_removed_by" : "User __user_removed__ removed by __user_by__.", + "User_muted_by" : "User __user_muted__ muted by __user_by__.", + "User_unmuted_by" : "User __user_unmuted__ unmuted by __user_by__.", "User_removed_from_room" : "The user has been removed from the room", "User_Settings" : "User Settings", "User_updated_successfully" : "User updated successfully", @@ -526,6 +532,7 @@ "you_are_in_preview_mode_of" : "You are in preview mode of channel #__room_name__", "You_can_change_a_different_avatar_too" : "You can change a different avatar too", "You_can_use_an_emoji_as_avatar" : "You can use an emoji as avatar", + "You_have_been_muted" : "You have been muted and cannot speak in this room", "You_need_confirm_email" : "You need to confirm your email to login!", "You_need_install_an_extension_to_allow_screen_sharing" : "You need install an extension to allow screen sharing", "You_should_name_it_to_easily_manage_your_integrations" : "You should name it to easily manage your integrations.", diff --git a/packages/rocketchat-authorization/server/startup.coffee b/packages/rocketchat-authorization/server/startup.coffee index d7043df9ba7..1fc3643b4d2 100644 --- a/packages/rocketchat-authorization/server/startup.coffee +++ b/packages/rocketchat-authorization/server/startup.coffee @@ -63,6 +63,9 @@ Meteor.startup -> { _id: 'remove-user', roles : ['admin', 'site-moderator', 'moderator']} + { _id: 'mute-user', + roles : ['admin', 'site-moderator', 'moderator']} + { _id: 'ban-user', roles : ['admin', 'site-moderator', 'moderator']} diff --git a/packages/rocketchat-lib/client/MessageTypes.coffee b/packages/rocketchat-lib/client/MessageTypes.coffee index 3e374f4d7ca..3c6f3cea63a 100644 --- a/packages/rocketchat-lib/client/MessageTypes.coffee +++ b/packages/rocketchat-lib/client/MessageTypes.coffee @@ -68,3 +68,17 @@ Meteor.startup -> id: 'rtc' render: (message) -> RocketChat.callbacks.run 'renderRtcMessage', message + + RocketChat.MessageTypes.registerType + id: 'user-muted' + system: true + message: 'User_muted_by' + data: (message) -> + return { user_muted: message.msg, user_by: message.u.username } + + RocketChat.MessageTypes.registerType + id: 'user-unmuted' + system: true + message: 'User_unmuted_by' + data: (message) -> + return { user_unmuted: message.msg, user_by: message.u.username } diff --git a/packages/rocketchat-lib/server/methods/sendMessage.coffee b/packages/rocketchat-lib/server/methods/sendMessage.coffee index 527470eeb9e..6e55243fb45 100644 --- a/packages/rocketchat-lib/server/methods/sendMessage.coffee +++ b/packages/rocketchat-lib/server/methods/sendMessage.coffee @@ -15,6 +15,17 @@ Meteor.methods if not room return false + subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId room._id, user._id + console.log subscription + if not subscription or subscription.mute is true + 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 + RocketChat.sendMessage user, message, room, options # Limit a user to sending 5 msgs/second diff --git a/packages/rocketchat-lib/server/models/Messages.coffee b/packages/rocketchat-lib/server/models/Messages.coffee index b7449cddf47..c1ef1ba654c 100644 --- a/packages/rocketchat-lib/server/models/Messages.coffee +++ b/packages/rocketchat-lib/server/models/Messages.coffee @@ -272,6 +272,14 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base createCommandWithRoomIdAndUser: (command, roomId, user, extraData) -> return @createWithTypeRoomIdMessageAndUser 'command', roomId, command, user, extraData + createUserMutedWithRoomIdAndUser: (roomId, user, extraData) -> + message = user.username + return @createWithTypeRoomIdMessageAndUser 'user-muted', roomId, message, user, extraData + + createUserUnmutedWithRoomIdAndUser: (roomId, user, extraData) -> + message = user.username + return @createWithTypeRoomIdMessageAndUser 'user-unmuted', roomId, message, user, extraData + # REMOVE removeById: (_id) -> query = diff --git a/packages/rocketchat-lib/server/models/Subscriptions.coffee b/packages/rocketchat-lib/server/models/Subscriptions.coffee index 20910f4e03d..5fb46c37e1f 100644 --- a/packages/rocketchat-lib/server/models/Subscriptions.coffee +++ b/packages/rocketchat-lib/server/models/Subscriptions.coffee @@ -210,6 +210,27 @@ RocketChat.models.Subscriptions = new class extends RocketChat.models._Base return @update query, update, { multi: true } + muteUserByRoomIdAndUserId: (roomId, userId) -> + query = + rid: roomId + "u._id": userId + + update = + $set: + mute: true + + return @update query, update + + unmuteUserByRoomIdAndUserId: (roomId, userId) -> + query = + rid: roomId + "u._id": userId + + update = + $unset: + mute: true + + return @update query, update # INSERT createWithRoomAndUser: (room, user, extraData) -> diff --git a/packages/rocketchat-slashcommands-mute/client/mute.coffee b/packages/rocketchat-slashcommands-mute/client/mute.coffee new file mode 100644 index 00000000000..d812e0cc179 --- /dev/null +++ b/packages/rocketchat-slashcommands-mute/client/mute.coffee @@ -0,0 +1,3 @@ +RocketChat.slashCommands.add 'mute', null, + description: TAPi18n.__ 'Mute_someone_in_room' + params: '@username' diff --git a/packages/rocketchat-slashcommands-mute/client/unmute.coffee b/packages/rocketchat-slashcommands-mute/client/unmute.coffee new file mode 100644 index 00000000000..4d59ccb7de4 --- /dev/null +++ b/packages/rocketchat-slashcommands-mute/client/unmute.coffee @@ -0,0 +1,3 @@ +RocketChat.slashCommands.add 'unmute', null, + description: TAPi18n.__ 'Unmute_someone_in_room' + params: '@username' diff --git a/packages/rocketchat-slashcommands-mute/i18n/en.i18n.json b/packages/rocketchat-slashcommands-mute/i18n/en.i18n.json new file mode 100644 index 00000000000..f20cd657298 --- /dev/null +++ b/packages/rocketchat-slashcommands-mute/i18n/en.i18n.json @@ -0,0 +1,6 @@ +{ + "Username_doesnt_exist" : "The username `#%s` doesn't exist.", + "Username_is_not_in_this_room" : "The user `#%s` is not in this room.", + "Mute_someone_in_room" : "Mute someone in the room", + "Unmute_someone_in_room" : "Unmute someone in the room" +} diff --git a/packages/rocketchat-slashcommands-mute/package.js b/packages/rocketchat-slashcommands-mute/package.js new file mode 100644 index 00000000000..adc64c1d6dd --- /dev/null +++ b/packages/rocketchat-slashcommands-mute/package.js @@ -0,0 +1,39 @@ +Package.describe({ + name: 'rocketchat:slashcommands-mute', + version: '0.0.1', + summary: 'Command handler for the /mute command', + git: '' +}); + +Package.onUse(function(api) { + + api.versionsFrom('1.0'); + + api.use([ + 'coffeescript', + 'check', + 'rocketchat:lib@0.0.1' + ]); + + api.addFiles('client/mute.coffee', 'client'); + api.addFiles('client/unmute.coffee', 'client'); + api.addFiles('server/mute.coffee', 'server'); + api.addFiles('server/unmute.coffee', 'server'); + + // TAPi18n + api.use('templating', 'client'); + var _ = Npm.require('underscore'); + var fs = Npm.require('fs'); + tapi18nFiles = _.compact(_.map(fs.readdirSync('packages/rocketchat-slashcommands-mute/i18n'), function(filename) { + if (fs.statSync('packages/rocketchat-slashcommands-mute/i18n/' + filename).size > 16) { + return 'i18n/' + filename; + } + })); + api.use('tap:i18n@1.6.1', ['client', 'server']); + api.imply('tap:i18n'); + api.addFiles(tapi18nFiles, ['client', 'server']); +}); + +Package.onTest(function(api) { + +}); diff --git a/packages/rocketchat-slashcommands-mute/server/mute.coffee b/packages/rocketchat-slashcommands-mute/server/mute.coffee new file mode 100644 index 00000000000..d8bc3d3bc51 --- /dev/null +++ b/packages/rocketchat-slashcommands-mute/server/mute.coffee @@ -0,0 +1,40 @@ +### +# Mute is a named function that will replace /mute commands +### + +class Mute + constructor: (command, params, item) -> + if command isnt 'mute' or not Match.test params, String + return + + username = params.trim() + if username is '' + return + + username = username.replace('@', '') + + user = Meteor.users.findOne Meteor.userId() + mutedUser = RocketChat.models.Users.findOneByUsername username + room = RocketChat.models.Rooms.findOneById item.rid + + if not mutedUser? + RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { + _id: Random.id() + rid: item.rid + ts: new Date + msg: TAPi18n.__('Username_doesnt_exist', { postProcess: 'sprintf', sprintf: [ username ] }, user.language); + } + return + + if username not in (room.usernames or []) + RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { + _id: Random.id() + rid: item.rid + ts: new Date + msg: TAPi18n.__('Username_is_not_in_this_room', { postProcess: 'sprintf', sprintf: [ username ] }, user.language); + } + return + + Meteor.call 'muteUserInRoom', { rid: item.rid, username: username } + +RocketChat.slashCommands.add 'mute', Mute diff --git a/packages/rocketchat-slashcommands-mute/server/unmute.coffee b/packages/rocketchat-slashcommands-mute/server/unmute.coffee new file mode 100644 index 00000000000..e3b6ee25d82 --- /dev/null +++ b/packages/rocketchat-slashcommands-mute/server/unmute.coffee @@ -0,0 +1,40 @@ +### +# Unmute is a named function that will replace /unmute commands +### + +class Unmute + constructor: (command, params, item) -> + if command isnt 'unmute' or not Match.test params, String + return + + username = params.trim() + if username is '' + return + + username = username.replace('@', '') + + user = Meteor.users.findOne Meteor.userId() + unmutedUser = RocketChat.models.Users.findOneByUsername username + room = RocketChat.models.Rooms.findOneById item.rid + + if not unmutedUser? + RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { + _id: Random.id() + rid: item.rid + ts: new Date + msg: TAPi18n.__('Username_doesnt_exist', { postProcess: 'sprintf', sprintf: [ username ] }, user.language); + } + return + + if username not in (room.usernames or []) + RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { + _id: Random.id() + rid: item.rid + ts: new Date + msg: TAPi18n.__('Username_is_not_in_this_room', { postProcess: 'sprintf', sprintf: [ username ] }, user.language); + } + return + + Meteor.call 'unmuteUserInRoom', { rid: item.rid, username: username } + +RocketChat.slashCommands.add 'unmute', Unmute diff --git a/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.coffee b/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.coffee index 00d323ef43c..caeb439bdce 100644 --- a/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.coffee +++ b/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.coffee @@ -36,6 +36,12 @@ Template.userInfo.helpers canRemoveUser: -> return RocketChat.authz.hasAllPermission('remove-user', Session.get('openedRoom')) + canMuteUser: -> + return RocketChat.authz.hasAllPermission('mute-user', Session.get('openedRoom')) + + userMuted: -> + return ChatSubscription.findOne({rid: Session.get('openedRoom')})?.mute is true + Template.userInfo.events 'click .pvt-msg': (e) -> Meteor.call 'createDirectMessage', Session.get('showUserInfo'), (error, result) -> @@ -88,6 +94,29 @@ Template.userInfo.events else toastr.error(TAPi18n.__ 'Not_allowed') + 'click .mute-user': (e, t) -> + e.preventDefault() + rid = Session.get('openedRoom') + room = ChatRoom.findOne rid + if RocketChat.authz.hasAllPermission('mute-user', rid) + Meteor.call 'muteUserInRoom', { rid: rid, username: @user.username }, (err, result) -> + if err + return toastr.error(err.reason or err.message) + toastr.success TAPi18n.__ 'User_muted_in_room' + else + toastr.error(TAPi18n.__ 'Not_allowed') + + 'click .unmute-user': (e, t) -> + e.preventDefault() + rid = Session.get('openedRoom') + room = ChatRoom.findOne rid + if RocketChat.authz.hasAllPermission('mute-user', rid) + Meteor.call 'unmuteUserInRoom', { rid: rid, username: @user.username }, (err, result) -> + if err + return toastr.error(err.reason or err.message) + toastr.success TAPi18n.__ 'User_unmuted_in_room' + else + toastr.error(TAPi18n.__ 'Not_allowed') Template.userInfo.onCreated -> @now = new ReactiveVar moment() diff --git a/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.html b/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.html index 85fa40e7a79..bb18b652ccd 100644 --- a/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.html +++ b/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.html @@ -37,6 +37,13 @@ {{#if canRemoveUser}} {{/if}} + {{#if canMuteUser}} + {{#if userMuted}} + + {{else}} + + {{/if}} + {{/if}} {{/if}} {{/if}} diff --git a/packages/rocketchat-ui-message/message/popup/messagePopupConfig.coffee b/packages/rocketchat-ui-message/message/popup/messagePopupConfig.coffee index e8a085eebea..022ff911f78 100644 --- a/packages/rocketchat-ui-message/message/popup/messagePopupConfig.coffee +++ b/packages/rocketchat-ui-message/message/popup/messagePopupConfig.coffee @@ -88,7 +88,7 @@ Template.messagePopupConfig.helpers commands.push _id: command params: item.params - description: item.description + description: TAPi18n.__ item.description if commands.length > 10 break diff --git a/packages/rocketchat-ui/views/app/room.coffee b/packages/rocketchat-ui/views/app/room.coffee index e382d70faa3..8dafd2e346c 100644 --- a/packages/rocketchat-ui/views/app/room.coffee +++ b/packages/rocketchat-ui/views/app/room.coffee @@ -540,7 +540,7 @@ Template.room.onRendered -> firstMessage = ChatMessage.findOne firstMessageOnScreen.id if firstMessage? subscription = ChatSubscription.findOne rid: template.data._id - template.unreadCount.set ChatMessage.find({rid: template.data._id, ts: {$lt: firstMessage.ts, $gt: subscription.ls}}).count() + template.unreadCount.set ChatMessage.find({rid: template.data._id, ts: {$lt: firstMessage.ts, $gt: subscription?.ls}}).count() else template.unreadCount.set 0 , 300 diff --git a/server/methods/muteUserInRoom.coffee b/server/methods/muteUserInRoom.coffee new file mode 100644 index 00000000000..7179738b3e9 --- /dev/null +++ b/server/methods/muteUserInRoom.coffee @@ -0,0 +1,26 @@ +Meteor.methods + muteUserInRoom: (data) -> + fromId = Meteor.userId() + console.log '[methods] muteUserInRoom -> '.green, 'fromId:', fromId, 'data:', data + + check(data, Match.ObjectIncluding({ rid: String, username: String })) + + unless RocketChat.authz.hasPermission(fromId, 'mute-user', data.rid) + throw new Meteor.Error 'not-allowed', 'Not allowed' + + room = RocketChat.models.Rooms.findOneById data.rid + + if data.username not in (room?.usernames or []) + throw new Meteor.Error 'not-in-room', 'User is not in this room' + + mutedUser = RocketChat.models.Users.findOneByUsername data.username + + RocketChat.models.Subscriptions.muteUserByRoomIdAndUserId data.rid, mutedUser._id + + fromUser = RocketChat.models.Users.findOneById fromId + RocketChat.models.Messages.createUserMutedWithRoomIdAndUser data.rid, mutedUser, + u: + _id: fromUser._id + username: fromUser.username + + return true diff --git a/server/methods/unmuteUserInRoom.coffee b/server/methods/unmuteUserInRoom.coffee new file mode 100644 index 00000000000..adfbaec666e --- /dev/null +++ b/server/methods/unmuteUserInRoom.coffee @@ -0,0 +1,26 @@ +Meteor.methods + unmuteUserInRoom: (data) -> + fromId = Meteor.userId() + console.log '[methods] unmuteUserInRoom -> '.green, 'fromId:', fromId, 'data:', data + + check(data, Match.ObjectIncluding({ rid: String, username: String })) + + unless RocketChat.authz.hasPermission(fromId, 'mute-user', data.rid) + throw new Meteor.Error 'not-allowed', 'Not allowed' + + room = RocketChat.models.Rooms.findOneById data.rid + + if data.username not in (room?.usernames or []) + throw new Meteor.Error 'not-in-room', 'User is not in this room' + + unmutedUser = RocketChat.models.Users.findOneByUsername data.username + + RocketChat.models.Subscriptions.unmuteUserByRoomIdAndUserId data.rid, unmutedUser._id + + fromUser = RocketChat.models.Users.findOneById fromId + RocketChat.models.Messages.createUserUnmutedWithRoomIdAndUser data.rid, unmutedUser, + u: + _id: fromUser._id + username: fromUser.username + + return true diff --git a/server/publications/subscription.coffee b/server/publications/subscription.coffee index f188e99522e..4cc8244a9e3 100644 --- a/server/publications/subscription.coffee +++ b/server/publications/subscription.coffee @@ -15,3 +15,4 @@ Meteor.publish 'subscription', -> open: 1 alert: 1 unread: 1 + mute: 1