{{{_ "If the handler wishes to post a response back into the Slack channel, the following JSON should be returned as the body of the response:"}}}
+
{{{exampleJson}}}
+
{{{_ "Empty bodies or bodies with an empty text property will simply be ignored. Non-200 responses will be retried a reasonable number of times. A response will be posted using the alias and avatar specified above. You can override these informations as in the example above."}}}
+
+
+
+ {{#nrr nrrargs 'message' example}}{{/nrr}}
+
+
+
+
+ {{#if data.token}}
+
+ {{/if}}
+
+
+
+ {{else}}
+ {{_ "Not_authorized"}}
+ {{/if}}
+
+
diff --git a/packages/rocketchat-integrations/package.js b/packages/rocketchat-integrations/package.js
index fe6df9670d812..06aebfe354d39 100644
--- a/packages/rocketchat-integrations/package.js
+++ b/packages/rocketchat-integrations/package.js
@@ -13,6 +13,7 @@ Package.onUse(function(api) {
api.use('underscore');
api.use('simple:highlight.js');
api.use('rocketchat:lib@0.0.1');
+ api.use('alanning:roles@1.2.12');
api.use('kadira:flow-router', 'client');
api.use('templating', 'client');
@@ -29,6 +30,8 @@ Package.onUse(function(api) {
api.addFiles('client/views/integrationsNew.coffee', 'client');
api.addFiles('client/views/integrationsIncoming.html', 'client');
api.addFiles('client/views/integrationsIncoming.coffee', 'client');
+ api.addFiles('client/views/integrationsOutgoing.html', 'client');
+ api.addFiles('client/views/integrationsOutgoing.coffee', 'client');
// stylesheets
api.addAssets('client/stylesheets/integrations.less', 'server');
@@ -40,13 +43,19 @@ Package.onUse(function(api) {
api.addFiles('server/publications/integrations.coffee', 'server');
// methods
- api.addFiles('server/methods/addIntegration.coffee', 'server');
- api.addFiles('server/methods/updateIntegration.coffee', 'server');
- api.addFiles('server/methods/deleteIntegration.coffee', 'server');
+ api.addFiles('server/methods/incoming/addIncomingIntegration.coffee', 'server');
+ api.addFiles('server/methods/incoming/updateIncomingIntegration.coffee', 'server');
+ api.addFiles('server/methods/incoming/deleteIncomingIntegration.coffee', 'server');
+ api.addFiles('server/methods/outgoing/addOutgoingIntegration.coffee', 'server');
+ api.addFiles('server/methods/outgoing/updateOutgoingIntegration.coffee', 'server');
+ api.addFiles('server/methods/outgoing/deleteOutgoingIntegration.coffee', 'server');
// api
api.addFiles('server/api/api.coffee', 'server');
+
+ api.addFiles('server/triggers.coffee', 'server');
+
var _ = Npm.require('underscore');
var fs = Npm.require('fs');
tapi18nFiles = _.compact(_.map(fs.readdirSync('packages/rocketchat-integrations/i18n'), function(filename) {
diff --git a/packages/rocketchat-integrations/server/api/api.coffee b/packages/rocketchat-integrations/server/api/api.coffee
index 068df884161f0..c8043272fcc51 100644
--- a/packages/rocketchat-integrations/server/api/api.coffee
+++ b/packages/rocketchat-integrations/server/api/api.coffee
@@ -38,8 +38,9 @@ Api.addRoute ':integrationId/:userId/:token', authRequired: true,
error: 'invalid-channel'
rid = room._id
- Meteor.runAsUser user._id, ->
- Meteor.call 'joinRoom', room._id
+ if room.t is 'c'
+ Meteor.runAsUser user._id, ->
+ Meteor.call 'joinRoom', room._id
when '@'
roomUser = RocketChat.models.Users.findOne
@@ -60,7 +61,7 @@ Api.addRoute ':integrationId/:userId/:token', authRequired: true,
if not room
Meteor.runAsUser user._id, ->
- Meteor.call 'createDirectMessage', roomUser._id
+ Meteor.call 'createDirectMessage', roomUser.username
room = RocketChat.models.Rooms.findOne(rid)
else
@@ -100,3 +101,29 @@ Api.addRoute ':integrationId/:userId/:token', authRequired: true,
statusCode: 200
body:
success: true
+
+
+Api.addRoute 'manageintegrations/:integrationId/:userId/:token', authRequired: true,
+ post: ->
+ if @bodyParams?.payload?
+ @bodyParams = JSON.parse @bodyParams.payload
+
+ integration = RocketChat.models.Integrations.findOne(@urlParams.integrationId)
+ user = RocketChat.models.Users.findOne(@userId)
+
+ if not integration?
+ return {} =
+ statusCode: 400
+ body:
+ success: false
+ error: 'Invalid integraiton id'
+
+ switch @bodyParams.action
+ when 'addOutgoingIntegration'
+ Meteor.runAsUser user._id, =>
+ Meteor.call 'addOutgoingIntegration', @bodyParams.data
+
+ return {} =
+ statusCode: 200
+ body:
+ success: true
diff --git a/packages/rocketchat-integrations/server/methods/addIntegration.coffee b/packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.coffee
similarity index 66%
rename from packages/rocketchat-integrations/server/methods/addIntegration.coffee
rename to packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.coffee
index 454b87d6bd236..67c33d156f6a7 100644
--- a/packages/rocketchat-integrations/server/methods/addIntegration.coffee
+++ b/packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.coffee
@@ -1,22 +1,22 @@
Meteor.methods
- addIntegration: (integration) ->
+ addIncomingIntegration: (integration) ->
if not RocketChat.authz.hasPermission @userId, 'manage-integrations'
throw new Meteor.Error 'not_authorized'
if not _.isString(integration.channel)
- throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel must be string'
+ throw new Meteor.Error 'invalid_channel', '[methods] addIncomingIntegration -> channel must be string'
if integration.channel.trim() is ''
- throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel can\'t be empty'
+ throw new Meteor.Error 'invalid_channel', '[methods] addIncomingIntegration -> channel can\'t be empty'
if integration.channel[0] not in ['@', '#']
- throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel should start with # or @'
+ throw new Meteor.Error 'invalid_channel', '[methods] addIncomingIntegration -> channel should start with # or @'
if not _.isString(integration.username)
- throw new Meteor.Error 'invalid_username', '[methods] addIntegration -> username must be string'
+ throw new Meteor.Error 'invalid_username', '[methods] addIncomingIntegration -> username must be string'
if integration.username.trim() is ''
- throw new Meteor.Error 'invalid_username', '[methods] addIntegration -> username can\'t be empty'
+ throw new Meteor.Error 'invalid_username', '[methods] addIncomingIntegration -> username can\'t be empty'
record = undefined
channelType = integration.channel[0]
@@ -37,12 +37,12 @@ Meteor.methods
]
if record is undefined
- throw new Meteor.Error 'channel_does_not_exists', "[methods] addIntegration -> The channel does not exists"
+ throw new Meteor.Error 'channel_does_not_exists', "[methods] addIncomingIntegration -> The channel does not exists"
user = RocketChat.models.Users.findOne({username: integration.username})
if not user?
- throw new Meteor.Error 'user_does_not_exists', "[methods] addIntegration -> The username does not exists"
+ throw new Meteor.Error 'user_does_not_exists', "[methods] addIncomingIntegration -> The username does not exists"
stampedToken = Accounts._generateStampedLoginToken()
hashStampedToken = Accounts._hashStampedToken(stampedToken)
@@ -53,6 +53,7 @@ Meteor.methods
hashedToken: hashStampedToken.hashedToken
integration: true
+ integration.type = 'webhook-incoming'
integration.token = hashStampedToken.hashedToken
integration.userId = user._id
integration._createdAt = new Date
@@ -60,6 +61,8 @@ Meteor.methods
RocketChat.models.Users.update {_id: user._id}, updateObj
+ Roles.addUsersToRoles user._id, 'bot', 'bot'
+
integration._id = RocketChat.models.Integrations.insert integration
return integration
diff --git a/packages/rocketchat-integrations/server/methods/deleteIntegration.coffee b/packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.coffee
similarity index 75%
rename from packages/rocketchat-integrations/server/methods/deleteIntegration.coffee
rename to packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.coffee
index d6f22c27701d2..692bb014d273b 100644
--- a/packages/rocketchat-integrations/server/methods/deleteIntegration.coffee
+++ b/packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.coffee
@@ -1,12 +1,12 @@
Meteor.methods
- deleteIntegration: (integrationId) ->
+ deleteIncomingIntegration: (integrationId) ->
if not RocketChat.authz.hasPermission @userId, 'manage-integrations'
throw new Meteor.Error 'not_authorized'
integration = RocketChat.models.Integrations.findOne(integrationId)
if not integration?
- throw new Meteor.Error 'invalid_integration', '[methods] addIntegration -> integration not found'
+ throw new Meteor.Error 'invalid_integration', '[methods] deleteIncomingIntegration -> integration not found'
updateObj =
$pull:
diff --git a/packages/rocketchat-integrations/server/methods/updateIntegration.coffee b/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.coffee
similarity index 67%
rename from packages/rocketchat-integrations/server/methods/updateIntegration.coffee
rename to packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.coffee
index e09c565efb094..25787a2898755 100644
--- a/packages/rocketchat-integrations/server/methods/updateIntegration.coffee
+++ b/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.coffee
@@ -1,19 +1,19 @@
Meteor.methods
- updateIntegration: (integrationId, integration) ->
+ updateIncomingIntegration: (integrationId, integration) ->
if not RocketChat.authz.hasPermission @userId, 'manage-integrations'
throw new Meteor.Error 'not_authorized'
if not _.isString(integration.channel)
- throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel must be string'
+ throw new Meteor.Error 'invalid_channel', '[methods] updateIncomingIntegration -> channel must be string'
if integration.channel.trim() is ''
- throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel can\'t be empty'
+ throw new Meteor.Error 'invalid_channel', '[methods] updateIncomingIntegration -> channel can\'t be empty'
if integration.channel[0] not in ['@', '#']
- throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel should start with # or @'
+ throw new Meteor.Error 'invalid_channel', '[methods] updateIncomingIntegration -> channel should start with # or @'
if not RocketChat.models.Integrations.findOne(integrationId)?
- throw new Meteor.Error 'invalid_integration', '[methods] addIntegration -> integration not found'
+ throw new Meteor.Error 'invalid_integration', '[methods] updateIncomingIntegration -> integration not found'
record = undefined
channelType = integration.channel[0]
@@ -34,7 +34,7 @@ Meteor.methods
]
if record is undefined
- throw new Meteor.Error 'channel_does_not_exists', "[methods] addIntegration -> The channel does not exists"
+ throw new Meteor.Error 'channel_does_not_exists', "[methods] updateIncomingIntegration -> The channel does not exists"
RocketChat.models.Integrations.update integrationId,
$set:
diff --git a/packages/rocketchat-integrations/server/methods/outgoing/addOutgoingIntegration.coffee b/packages/rocketchat-integrations/server/methods/outgoing/addOutgoingIntegration.coffee
new file mode 100644
index 0000000000000..c27c7c1244184
--- /dev/null
+++ b/packages/rocketchat-integrations/server/methods/outgoing/addOutgoingIntegration.coffee
@@ -0,0 +1,73 @@
+Meteor.methods
+ addOutgoingIntegration: (integration) ->
+ if not RocketChat.authz.hasPermission(@userId, 'manage-integrations') and not RocketChat.authz.hasPermission(@userId, 'manage-integrations', 'bot')
+ throw new Meteor.Error 'not_authorized'
+
+ if integration.username.trim() is ''
+ throw new Meteor.Error 'invalid_username', '[methods] addOutgoingIntegration -> username can\'t be empty'
+
+ if not Match.test integration.urls, [String]
+ throw new Meteor.Error 'invalid_urls', '[methods] addOutgoingIntegration -> urls must be an array'
+
+ for url, index in integration.urls
+ delete integration.urls[index] if url.trim() is ''
+
+ integration.urls = _.without integration.urls, [undefined]
+
+ if integration.urls.length is 0
+ throw new Meteor.Error 'invalid_urls', '[methods] addOutgoingIntegration -> urls is required'
+
+ if integration.channel?.trim() isnt '' and integration.channel[0] not in ['@', '#']
+ throw new Meteor.Error 'invalid_channel', '[methods] addOutgoingIntegration -> channel should start with # or @'
+
+ if not integration.token? or integration.token?.trim() is ''
+ throw new Meteor.Error 'invalid_token', '[methods] addOutgoingIntegration -> token is required'
+
+ if integration.triggerWords?
+ if not Match.test integration.triggerWords, [String]
+ throw new Meteor.Error 'invalid_triggerWords', '[methods] addOutgoingIntegration -> triggerWords must be an array'
+
+ for triggerWord, index in integration.triggerWords
+ delete integration.triggerWords[index] if triggerWord.trim() is ''
+
+ integration.triggerWords = _.without integration.triggerWords, [undefined]
+
+ if integration.triggerWords.length is 0 and not integration.channel?
+ throw new Meteor.Error 'invalid_triggerWords', '[methods] addOutgoingIntegration -> triggerWords is required if channel is empty'
+
+
+ if integration.channel?.trim() isnt ''
+ record = undefined
+ channelType = integration.channel[0]
+ channel = integration.channel.substr(1)
+
+ switch channelType
+ when '#'
+ record = RocketChat.models.Rooms.findOne
+ $or: [
+ {_id: channel}
+ {name: channel}
+ ]
+ when '@'
+ record = RocketChat.models.Users.findOne
+ $or: [
+ {_id: channel}
+ {username: channel}
+ ]
+
+ if record is undefined
+ throw new Meteor.Error 'channel_does_not_exists', "[methods] addOutgoingIntegration -> The channel does not exists"
+
+ user = RocketChat.models.Users.findOne({username: integration.username})
+
+ if not user?
+ throw new Meteor.Error 'user_does_not_exists', "[methods] addOutgoingIntegration -> The username does not exists"
+
+ integration.type = 'webhook-outgoing'
+ integration.userId = user._id
+ integration._createdAt = new Date
+ integration._createdBy = RocketChat.models.Users.findOne @userId, {fields: {username: 1}}
+
+ integration._id = RocketChat.models.Integrations.insert integration
+
+ return integration
diff --git a/packages/rocketchat-integrations/server/methods/outgoing/deleteOutgoingIntegration.coffee b/packages/rocketchat-integrations/server/methods/outgoing/deleteOutgoingIntegration.coffee
new file mode 100644
index 0000000000000..b13af8bdd5144
--- /dev/null
+++ b/packages/rocketchat-integrations/server/methods/outgoing/deleteOutgoingIntegration.coffee
@@ -0,0 +1,13 @@
+Meteor.methods
+ deleteOutgoingIntegration: (integrationId) ->
+ if not RocketChat.authz.hasPermission @userId, 'manage-integrations'
+ throw new Meteor.Error 'not_authorized'
+
+ integration = RocketChat.models.Integrations.findOne(integrationId)
+
+ if not integration?
+ throw new Meteor.Error 'invalid_integration', '[methods] deleteOutgoingIntegration -> integration not found'
+
+ RocketChat.models.Integrations.remove _id: integrationId
+
+ return true
diff --git a/packages/rocketchat-integrations/server/methods/outgoing/updateOutgoingIntegration.coffee b/packages/rocketchat-integrations/server/methods/outgoing/updateOutgoingIntegration.coffee
new file mode 100644
index 0000000000000..d91c552aed199
--- /dev/null
+++ b/packages/rocketchat-integrations/server/methods/outgoing/updateOutgoingIntegration.coffee
@@ -0,0 +1,84 @@
+Meteor.methods
+ updateOutgoingIntegration: (integrationId, integration) ->
+ if not RocketChat.authz.hasPermission @userId, 'manage-integrations'
+ throw new Meteor.Error 'not_authorized'
+
+ if integration.username.trim() is ''
+ throw new Meteor.Error 'invalid_username', '[methods] updateOutgoingIntegration -> username can\'t be empty'
+
+ if not Match.test integration.urls, [String]
+ throw new Meteor.Error 'invalid_urls', '[methods] updateOutgoingIntegration -> urls must be an array'
+
+ for url, index in integration.urls
+ delete integration.urls[index] if url.trim() is ''
+
+ integration.urls = _.without integration.urls, [undefined]
+
+ if integration.urls.length is 0
+ throw new Meteor.Error 'invalid_urls', '[methods] updateOutgoingIntegration -> urls is required'
+
+ if integration.channel?.trim() isnt '' and integration.channel[0] not in ['@', '#']
+ throw new Meteor.Error 'invalid_channel', '[methods] updateOutgoingIntegration -> channel should start with # or @'
+
+ if not integration.token? or integration.token?.trim() is ''
+ throw new Meteor.Error 'invalid_token', '[methods] updateOutgoingIntegration -> token is required'
+
+ if integration.triggerWords?
+ if not Match.test integration.triggerWords, [String]
+ throw new Meteor.Error 'invalid_triggerWords', '[methods] updateOutgoingIntegration -> triggerWords must be an array'
+
+ for triggerWord, index in integration.triggerWords
+ delete integration.triggerWords[index] if triggerWord.trim() is ''
+
+ integration.triggerWords = _.without integration.triggerWords, [undefined]
+
+ if integration.triggerWords.length is 0 and not integration.channel?
+ throw new Meteor.Error 'invalid_triggerWords', '[methods] updateOutgoingIntegration -> triggerWords is required if channel is empty'
+
+ if not RocketChat.models.Integrations.findOne(integrationId)?
+ throw new Meteor.Error 'invalid_integration', '[methods] updateOutgoingIntegration -> integration not found'
+
+
+ if integration.channel?.trim() isnt ''
+ record = undefined
+ channelType = integration.channel[0]
+ channel = integration.channel.substr(1)
+
+ switch channelType
+ when '#'
+ record = RocketChat.models.Rooms.findOne
+ $or: [
+ {_id: channel}
+ {name: channel}
+ ]
+ when '@'
+ record = RocketChat.models.Users.findOne
+ $or: [
+ {_id: channel}
+ {username: channel}
+ ]
+
+ if record is undefined
+ throw new Meteor.Error 'channel_does_not_exists', "[methods] updateOutgoingIntegration -> The channel does not exists"
+
+ user = RocketChat.models.Users.findOne({username: integration.username})
+
+ if not user?
+ throw new Meteor.Error 'user_does_not_exists', "[methods] updateOutgoingIntegration -> The username does not exists"
+
+ RocketChat.models.Integrations.update integrationId,
+ $set:
+ name: integration.name
+ avatar: integration.avatar
+ emoji: integration.emoji
+ alias: integration.alias
+ channel: integration.channel
+ username: integration.username
+ userId: user._id
+ urls: integration.urls
+ token: integration.token
+ triggerWords: integration.triggerWords
+ _updatedAt: new Date
+ _updatedBy: RocketChat.models.Users.findOne @userId, {fields: {username: 1}}
+
+ return RocketChat.models.Integrations.findOne(integrationId)
diff --git a/packages/rocketchat-integrations/server/triggers.coffee b/packages/rocketchat-integrations/server/triggers.coffee
new file mode 100644
index 0000000000000..9a610cbfc670d
--- /dev/null
+++ b/packages/rocketchat-integrations/server/triggers.coffee
@@ -0,0 +1,94 @@
+triggers = {}
+
+RocketChat.models.Integrations.find({type: 'webhook-outgoing'}).observe
+ added: (record) ->
+ channel = record.channel or '__any'
+ triggers[channel] ?= {}
+ triggers[channel][record._id] = record
+
+ changed: (record) ->
+ channel = record.channel or '__any'
+ triggers[channel] ?= {}
+ triggers[channel][record._id] = record
+
+ removed: (record) ->
+ channel = record.channel or '__any'
+ delete triggers[channel][record._id]
+
+
+ExecuteTriggerUrl = (url, trigger, message, room, tries=0) ->
+ console.log tries
+ word = undefined
+ if trigger.triggerWords?.length > 0
+ for triggerWord in trigger.triggerWords
+ if message.msg.indexOf(triggerWord) is 0
+ word = triggerWord
+ break
+
+ # Stop if there are triggerWords but none match
+ if not word?
+ return
+
+ data =
+ token: trigger.token
+ # team_id=T0001
+ # team_domain=example
+ channel_id: room._id
+ channel_name: room.name
+ timestamp: message.ts
+ user_id: message.u._id
+ user_name: message.u.username
+ text: message.msg
+
+ if word?
+ data.trigger_word = word
+
+ opts =
+ data: data
+ npmRequestOptions:
+ rejectUnauthorized: !RocketChat.settings.get 'Allow_Invalid_SelfSigned_Certs'
+ strictSSL: !RocketChat.settings.get 'Allow_Invalid_SelfSigned_Certs'
+ headers:
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36'
+
+ HTTP.call 'POST', url, opts, (error, result) ->
+ console.log error, result
+ if not result? or result.statusCode isnt 200
+ if tries <= 6
+ # Try again in 0.1s, 1s, 10s, 1m40s, 16m40s, 2h46m40s and 27h46m40s
+ Meteor.setTimeout ->
+ ExecuteTriggerUrl url, trigger, message, room, tries+1
+ , Math.pow(10, tries+2)
+ return
+
+ # TODO process return and insert message if necessary
+
+
+
+ExecuteTrigger = (trigger, message, room) ->
+ for url in trigger.urls
+ ExecuteTriggerUrl url, trigger, message, room
+
+
+ExecuteTriggers = (message, room) ->
+ if not room?
+ return
+
+ triggersToExecute = []
+
+ if triggers['#'+room._id]?
+ triggersToExecute.push trigger for key, trigger of triggers['#'+room._id]
+
+ if triggers['#'+room.name]?
+ triggersToExecute.push trigger for key, trigger of triggers['#'+room.name]
+
+ if triggers.__any?
+ triggersToExecute.push trigger for key, trigger of triggers.__any
+
+ for triggerToExecute in triggersToExecute
+ ExecuteTrigger triggerToExecute, message, room
+
+ return message
+
+
+RocketChat.callbacks.add 'afterSaveMessage', ExecuteTriggers, RocketChat.callbacks.priority.LOW
diff --git a/packages/rocketchat-lib/server/functions/sendMessage.coffee b/packages/rocketchat-lib/server/functions/sendMessage.coffee
index 20620d1c9dc55..6b14009c97ccb 100644
--- a/packages/rocketchat-lib/server/functions/sendMessage.coffee
+++ b/packages/rocketchat-lib/server/functions/sendMessage.coffee
@@ -30,7 +30,7 @@ RocketChat.sendMessage = (user, message, room, options) ->
###
Meteor.defer ->
- RocketChat.callbacks.run 'afterSaveMessage', message
+ RocketChat.callbacks.run 'afterSaveMessage', message, room
###
Update all the room activity tracker fields
diff --git a/packages/rocketchat-slashcommands-invite/server.coffee b/packages/rocketchat-slashcommands-invite/server.coffee
index 36ae07981e531..4b619023139d4 100644
--- a/packages/rocketchat-slashcommands-invite/server.coffee
+++ b/packages/rocketchat-slashcommands-invite/server.coffee
@@ -37,8 +37,9 @@ class Invite
}
return
- Meteor.runAsUser user._id, ->
- Meteor.call 'joinRoom', item.rid
+ Meteor.call 'addUserToRoom',
+ rid: item.rid
+ username: user.username
RocketChat.slashCommands.add 'invite', Invite
diff --git a/packages/rocketchat-ui-message/message/message.coffee b/packages/rocketchat-ui-message/message/message.coffee
index 3e1b3aef09911..bf417c06df4ce 100644
--- a/packages/rocketchat-ui-message/message/message.coffee
+++ b/packages/rocketchat-ui-message/message/message.coffee
@@ -128,7 +128,7 @@ Template.message.onViewRendered = (context) ->
else
$currentNode.removeClass('new-day')
- if previousDataset.groupable is 'false'
+ if previousDataset.groupable is 'false' or currentDataset.groupable is 'false'
$currentNode.removeClass('sequential')
else
if previousDataset.username isnt currentDataset.username or parseInt(currentDataset.timestamp) - parseInt(previousDataset.timestamp) > RocketChat.settings.get('Message_GroupingPeriod') * 1000