Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into develop

pull/1747/head
Diego Sampaio 10 years ago
commit 7466f55b80
  1. 2
      Dockerfile
  2. 2
      install.sh
  3. 4
      packages/rocketchat-authorization/server/startup.coffee
  4. 10
      packages/rocketchat-integrations/client/route.coffee
  5. 20
      packages/rocketchat-integrations/client/views/integrations.html
  6. 7
      packages/rocketchat-integrations/client/views/integrationsIncoming.coffee
  7. 2
      packages/rocketchat-integrations/client/views/integrationsIncoming.html
  8. 4
      packages/rocketchat-integrations/client/views/integrationsNew.html
  9. 170
      packages/rocketchat-integrations/client/views/integrationsOutgoing.coffee
  10. 101
      packages/rocketchat-integrations/client/views/integrationsOutgoing.html
  11. 15
      packages/rocketchat-integrations/package.js
  12. 29
      packages/rocketchat-integrations/server/api/api.coffee
  13. 19
      packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.coffee
  14. 4
      packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.coffee
  15. 12
      packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.coffee
  16. 73
      packages/rocketchat-integrations/server/methods/outgoing/addOutgoingIntegration.coffee
  17. 13
      packages/rocketchat-integrations/server/methods/outgoing/deleteOutgoingIntegration.coffee
  18. 84
      packages/rocketchat-integrations/server/methods/outgoing/updateOutgoingIntegration.coffee
  19. 94
      packages/rocketchat-integrations/server/triggers.coffee
  20. 2
      packages/rocketchat-lib/server/functions/sendMessage.coffee
  21. 2
      packages/rocketchat-livechat/app/.meteor/packages
  22. 1
      packages/rocketchat-livechat/app/.meteor/versions
  23. 2
      packages/rocketchat-livechat/app/client/lib/chatMessages.coffee
  24. 1
      packages/rocketchat-livechat/app/client/lib/collections.coffee
  25. 2
      packages/rocketchat-livechat/app/client/routes/router.coffee
  26. 23
      packages/rocketchat-livechat/app/client/stylesheets/main.less
  27. 8
      packages/rocketchat-livechat/app/client/views/livechatWindow.html
  28. 40
      packages/rocketchat-livechat/app/client/views/livechatWindow.js
  29. 50
      packages/rocketchat-livechat/app/client/views/register.coffee
  30. 11
      packages/rocketchat-livechat/app/client/views/register.html
  31. 68
      packages/rocketchat-livechat/app/client/views/register.js
  32. 1
      packages/rocketchat-livechat/app/i18n/en.i18n.json
  33. 1
      packages/rocketchat-livechat/config.js
  34. 1
      packages/rocketchat-livechat/i18n/en.i18n.json
  35. 1
      packages/rocketchat-livechat/package.js
  36. 2
      packages/rocketchat-livechat/server/lib/getNextAgent.js
  37. 5
      packages/rocketchat-livechat/server/methods/registerGuest.js
  38. 14
      packages/rocketchat-livechat/server/methods/sendMessageLivechat.js
  39. 8
      packages/rocketchat-livechat/server/models/LivechatDepartment.js
  40. 3
      packages/rocketchat-livechat/server/publications/availableDepartments.js
  41. 5
      packages/rocketchat-slashcommands-invite/server.coffee
  42. 2
      packages/rocketchat-ui-message/message/message.coffee

@ -16,7 +16,7 @@ RUN gpg --keyserver ha.pool.sks-keyservers.net --recv-keys 0E163286C20D07B9787EB
WORKDIR /app
RUN curl -fSL "https://s3.amazonaws.com/rocketchatbuild/develop.rocket.chat-v.latest.tgz" -o rocket.chat.tgz \
RUN curl -fSL "https://s3.amazonaws.com/rocketchatbuild/rocket.chat-develop.tgz" -o rocket.chat.tgz \
&& tar zxvf ./rocket.chat.tgz \
&& rm ./rocket.chat.tgz \
&& cd /app/bundle/programs/server \

@ -8,7 +8,7 @@ if [ "$1" == "development" ]; then
fi
cd $ROOTPATH
curl -fSL "https://s3.amazonaws.com/rocketchatbuild/demo.rocket.chat-v.latest.tgz" -o rocket.chat.tgz
curl -fSL "https://s3.amazonaws.com/rocketchatbuild/rocket.chat-develop.tgz" -o rocket.chat.tgz
tar zxf rocket.chat.tgz && rm rocket.chat.tgz
cd $ROOTPATH/bundle/programs/server
npm install

@ -103,14 +103,14 @@ Meteor.startup ->
roles : ['admin']}
{ _id: 'manage-integrations',
roles : ['admin']}
roles : ['admin', 'bot']}
]
#alanning:roles
roles = _.pluck(Roles.getAllRoles().fetch(), 'name');
for permission in permissions
RocketChat.models.Permissions.upsert( permission._id, {$setOnInsert : permission })
RocketChat.models.Permissions.upsert( permission._id, {$set: permission })
for role in permission.roles
unless role in roles
Roles.createRole role

@ -24,3 +24,13 @@ FlowRouter.route '/admin/integrations/incoming/:id?',
pageTitle: t('Integration_Incoming_WebHook')
pageTemplate: 'integrationsIncoming'
params: params
FlowRouter.route '/admin/integrations/outgoing/:id?',
name: 'admin-integrations-outgoing'
action: (params) ->
BlazeLayout.render 'main',
center: 'pageSettingsContainer'
pageTitle: t('Integration_Outgoing_WebHook')
pageTemplate: 'integrationsOutgoing'
params: params

@ -7,6 +7,7 @@
<div class="section">
<div class="admin-integrations-new-panel">
{{#each integrations}}
{{#if $eq type 'webhook-incoming'}}
<a href="{{pathFor "admin-integrations-incoming" id=_id}}">
<div class="admin-integrations-new-item">
<i class="icon-login"></i>
@ -24,9 +25,28 @@
<i class="icon-angle-right"></i>
</div>
</a>
{{/if}}
{{else}}
<h1>{{_ "There_is_no_integrations"}}</h1>
{{/each}}
{{#each integrations}}
{{#if $eq type 'webhook-outgoing'}}
<a href="{{pathFor "admin-integrations-outgoing" id=_id}}">
<div class="admin-integrations-new-item">
<i class="icon-login"></i>
<div class="admin-integrations-new-item-body">
<div class="admin-integrations-new-item-title">
Outgoing WebHook {{#if name}}- {{name}}{{/if}}
</div>
<div class="admin-integrations-new-item-description">
{{{_ "Created_at_s_by_s" (dateFormated _createdAt) _createdBy.username}}}
</div>
</div>
<i class="icon-angle-right"></i>
</div>
</a>
{{/if}}
{{/each}}
</div>
</div>
</div>

@ -108,7 +108,7 @@ Template.integrationsIncoming.events
closeOnConfirm: false
html: false
, ->
Meteor.call "deleteIntegration", params.id, (err, data) ->
Meteor.call "deleteIncomingIntegration", params.id, (err, data) ->
swal
title: t('Deleted')
text: t('Your_entry_has_been_deleted')
@ -141,16 +141,15 @@ Template.integrationsIncoming.events
params = Template.instance().data.params?()
if params?.id?
Meteor.call "updateIntegration", params.id, integration, (err, data) ->
Meteor.call "updateIncomingIntegration", params.id, integration, (err, data) ->
if err?
return toastr.error TAPi18n.__(err.error)
toastr.success TAPi18n.__("Integration_updated")
else
integration.type = 'webhook-incoming'
integration.username = username
Meteor.call "addIntegration", integration, (err, data) ->
Meteor.call "addIncomingIntegration", integration, (err, data) ->
if err?
return toastr.error TAPi18n.__(err.error)

@ -52,7 +52,7 @@
<div>
<input type="text" name="emoji" value="{{data.emoji}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "You_can_use_an_emoji_as_avatar"}}</div>
<div class="settings-description">{{_ "Example_s" ":ghost:"}}</div>
<div class="settings-description">{{{_ "Example_s" ":ghost:"}}}</div>
</div>
</div>
{{#if data.token}}

@ -20,7 +20,7 @@
<i class="icon-angle-right"></i>
</div>
</a>
<!-- <a href="{{pathFor "admin-integrations-incoming"}}">
<a href="{{pathFor "admin-integrations-outgoing"}}">
<div class="admin-integrations-new-item">
<i class="icon-logout"></i>
<div class="admin-integrations-new-item-body">
@ -33,7 +33,7 @@
</div>
<i class="icon-angle-right"></i>
</div>
</a> -->
</a>
</div>
</div>
</div>

@ -0,0 +1,170 @@
Template.integrationsOutgoing.onCreated ->
@record = new ReactiveVar
username: 'rocket.cat'
token: Random.id(24)
Template.integrationsOutgoing.helpers
join: (arr, sep) ->
if not arr?.join?
return arr
return arr.join sep
hasPermission: ->
return RocketChat.authz.hasAllPermission 'manage-integrations'
data: ->
params = Template.instance().data.params?()
if params?.id?
data = ChatIntegrations.findOne({_id: params.id})
if data?
if not data.token?
data.token = Random.id(24)
return data
return Template.instance().record.curValue
example: ->
record = Template.instance().record.get()
return {} =
_id: Random.id()
alias: record.alias
emoji: record.emoji
avatar: record.avatar
msg: 'Response text'
bot:
i: Random.id()
groupable: false
attachments: [{
title: "Rocket.Chat"
title_link: "https://rocket.chat"
text: "Rocket.Chat, the best open source chat"
image_url: "https://rocket.chat/images/mockup.png"
color: "#764FA5"
}]
ts: new Date
u:
_id: Random.id()
username: record.username
exampleJson: ->
record = Template.instance().record.get()
data =
username: record.alias
icon_emoji: record.emoji
icon_url: record.avatar
text: 'Response text'
attachments: [{
title: "Rocket.Chat"
title_link: "https://rocket.chat"
text: "Rocket.Chat, the best open source chat"
image_url: "https://rocket.chat/images/mockup.png"
color: "#764FA5"
}]
for key, value of data
delete data[key] if value in [null, ""]
return hljs.highlight('json', JSON.stringify(data, null, 2)).value
Template.integrationsOutgoing.events
"blur input": (e, t) ->
t.record.set
name: $('[name=name]').val().trim()
alias: $('[name=alias]').val().trim()
emoji: $('[name=emoji]').val().trim()
avatar: $('[name=avatar]').val().trim()
channel: $('[name=channel]').val().trim()
username: $('[name=username]').val().trim()
triggerWords: $('[name=triggerWords]').val().trim()
urls: $('[name=urls]').val().trim()
token: $('[name=token]').val().trim()
"click .submit > .delete": ->
params = Template.instance().data.params()
swal
title: t('Are_you_sure')
text: t('You_will_not_be_able_to_recover')
type: 'warning'
showCancelButton: true
confirmButtonColor: '#DD6B55'
confirmButtonText: t('Yes_delete_it')
cancelButtonText: t('Cancel')
closeOnConfirm: false
html: false
, ->
Meteor.call "deleteOutgoingIntegration", params.id, (err, data) ->
swal
title: t('Deleted')
text: t('Your_entry_has_been_deleted')
type: 'success'
timer: 1000
showConfirmButton: false
FlowRouter.go "admin-integrations"
"click .submit > .save": ->
name = $('[name=name]').val().trim()
alias = $('[name=alias]').val().trim()
emoji = $('[name=emoji]').val().trim()
avatar = $('[name=avatar]').val().trim()
channel = $('[name=channel]').val().trim()
username = $('[name=username]').val().trim()
triggerWords = $('[name=triggerWords]').val().trim()
urls = $('[name=urls]').val().trim()
token = $('[name=token]').val().trim()
if username is ''
return toastr.error TAPi18n.__("The_username_is_required")
triggerWords = triggerWords.split(',')
for triggerWord, index in triggerWords
triggerWords[index] = triggerWord.trim()
delete triggerWords[index] if triggerWord.trim() is ''
triggerWords = _.without triggerWords, [undefined]
if triggerWords.length is 0 and channel.trim() is ''
return toastr.error TAPi18n.__("You should inform at least one trigger word if you do not inform a channel")
urls = urls.split('\n')
for url, index in urls
urls[index] = url.trim()
delete urls[index] if url.trim() is ''
urls = _.without urls, [undefined]
if urls.length is 0
return toastr.error TAPi18n.__("You_should_inform_one_url_at_least")
integration =
channel: channel
username: username
alias: alias if alias isnt ''
emoji: emoji if emoji isnt ''
avatar: avatar if avatar isnt ''
name: name if name isnt ''
triggerWords: triggerWords if triggerWords isnt ''
urls: urls if urls isnt ''
token: token if token isnt ''
params = Template.instance().data.params?()
if params?.id?
Meteor.call "updateOutgoingIntegration", params.id, integration, (err, data) ->
if err?
return toastr.error TAPi18n.__(err.error)
toastr.success TAPi18n.__("Integration_updated")
else
Meteor.call "addOutgoingIntegration", integration, (err, data) ->
if err?
return toastr.error TAPi18n.__(err.error)
toastr.success TAPi18n.__("Integration_added")
FlowRouter.go "admin-integrations-outgoing", {id: data._id}

@ -0,0 +1,101 @@
<template name="integrationsOutgoing">
<div class="permissions-manager">
{{#if hasPermission}}
<a href="{{pathFor "admin-integrations"}}"><i class="icon-angle-left"></i> {{_ "Back_to_integrations"}}</a><br><br>
<div class="rocket-form">
<div class="section">
<div class="section-content">
<div class="input-line double-col">
<label>{{_ "Name"}} ({{_ "optional"}})</label>
<div>
<input type="text" name="name" value="{{data.name}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "You_should_name_it_to_easily_manage_your_integrations"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Channel"}}</label>
<div>
<input type="text" name="channel" value="{{data.channel}}" placeholder="{{_ 'User_or_channel_name'}}" />
<div class="settings-description">{{_ "Optional channel to listen on"}}</div>
<div class="settings-description">{{{_ "Start_with_s_for_user_or_s_for_channel_Eg_s_or_s" "@" "#" "@john" "#general"}}}</div>
<div class="settings-description">{{{_ "Leave empty to listen <strong>any</strong> channel"}}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Trigger_Words"}}</label>
<div>
<input type="text" name="triggerWords" value="{{join data.triggerWords ','}}" />
<div class="settings-description">{{_ "When a line starts with one of these words, post to the URL(s) below"}}</div>
<div class="settings-description">{{_ "Optional if a channel is chosen"}}</div>
<div class="settings-description">{{_ "Separate multiple words with commas"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "URLs"}}</label>
<div>
<textarea name="urls" style="height: 100px;">{{join data.urls "\n"}}</textarea>
<div class="settings-description">{{_ "Enter as many URLs as you like, one per line, please"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Post_as"}}</label>
<div>
<input type="text" name="username" value="{{data.username}}" />
<div class="settings-description">{{_ "Choose_the_username_that_this_integration_will_post_as"}}</div>
<div class="settings-description">{{_ "Should_exists_a_user_with_this_username"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Alias"}} ({{_ "optional"}})</label>
<div>
<input type="text" name="alias" value="{{data.alias}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "Choose_the_alias_that_will_appear_before_the_username_in_messages"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Avatar_URL"}} ({{_ "optional"}})</label>
<div>
<input type="url" name="avatar" value="{{data.avatar}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "You_can_change_a_different_avatar_too"}}</div>
<div class="settings-description">{{_ "Should_be_a_URL_of_an_image"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Emoji"}} ({{_ "optional"}})</label>
<div>
<input type="text" name="emoji" value="{{data.emoji}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "You_can_use_an_emoji_as_avatar"}}</div>
<div class="settings-description">{{{_ "Example_s" ":ghost:"}}}</div>
</div>
</div>
<div class="input-line double-col">
<label>Token</label>
<div>
<input type="text" name="token" value="{{data.token}}" />
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Responding"}}</label>
<div>
<div class="settings-description">{{{_ "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:"}}}</div>
<pre><code class="hljs json json-example">{{{exampleJson}}}</code></pre>
<div class="settings-description">{{{_ "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."}}}</div>
</div>
</div>
<div class="input-line message-example">
{{#nrr nrrargs 'message' example}}{{/nrr}}
</div>
</div>
</div>
<div class="submit">
{{#if data.token}}
<button class="button red delete"><i class="icon-trash"></i><span>{{_ "Delete"}}</span></button>
{{/if}}
<button class="button save"><i class="icon-send"></i><span>{{_ "Save_changes"}}</span></button>
</div>
</div>
{{else}}
{{_ "Not_authorized"}}
{{/if}}
</div>
</template>

@ -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) {

@ -38,6 +38,7 @@ Api.addRoute ':integrationId/:userId/:token', authRequired: true,
error: 'invalid-channel'
rid = room._id
if room.t is 'c'
Meteor.runAsUser user._id, ->
Meteor.call 'joinRoom', room._id
@ -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

@ -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

@ -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:

@ -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:

@ -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

@ -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

@ -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)

@ -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

@ -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

@ -36,3 +36,5 @@ accounts-password
standard-minifiers
tap:i18n
kevohagan:sweetalert
ecmascript
es5-shim

@ -27,6 +27,7 @@ ecmascript@0.1.6
ecmascript-runtime@0.2.6
ejson@1.0.7
email@1.0.8
es5-shim@4.1.14
geojson-utils@1.0.4
html-tools@1.0.5
htmljs@1.0.5

@ -93,7 +93,7 @@ class @ChatMessages
showError error.reason
if not Meteor.userId()
Meteor.call 'registerGuest', visitor.getToken(), (error, result) ->
Meteor.call 'livechat:registerGuest', { token: visitor.getToken() }, (error, result) ->
if error?
return showError error.reason

@ -2,3 +2,4 @@
@ChatRoom = new Meteor.Collection 'rocketchat_room'
@Settings = new Meteor.Collection 'rocketchat_settings'
@Trigger = new Meteor.Collection 'rocketchat_livechat_trigger'
@Department = new Meteor.Collection 'rocketchat_livechat_department'

@ -9,4 +9,4 @@ FlowRouter.route '/livechat',
]
action: ->
BlazeLayout.render 'main', {center: 'room'}
BlazeLayout.render 'main', {center: 'livechatWindow'}

@ -1,4 +1,3 @@
@import url(//fonts.googleapis.com/css?family=Roboto:300,400,500,600,700&subset=latin,cyrillic-ext,greek-ext,greek,vietnamese,latin-ext,cyrillic);
@import "_variables.less";
@import "utils/_lesshat.import.less";
@import "utils/_reset.import.less";
@ -13,11 +12,9 @@ html, body {
height: 100%;
}
body {
padding: 0;
margin: 0;
font-size: 10pt;
font-family: "Roboto", "HelveticaNeue", sans-serif;
// font-size: 14px;
font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif, "Meiryo UI";
font-size: 0.8rem;
color: @primary-font-color;
height: 100%;
width: 100%;
@ -36,6 +33,11 @@ textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
padding: 5px;
margin: 5px;
border: 1px solid #E7E7E7;
border-radius: 5px;
outline: none;
}
input:focus {
@ -54,6 +56,7 @@ input:focus {
word-spacing: 0;
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.125);
border: none;
border-radius: 0;
line-height: 16px;
position: relative;
cursor: pointer;background-color: #FFF;
@ -367,9 +370,7 @@ input:focus {
padding-right: 38px;
overflow-y: auto;
resize: none;
border: 1px solid #E7E7E7;
// margin: 10px;
border-radius: 5px;
margin: 0;
max-height: 200px;
width: 100%;
font-size: 12px;
@ -378,7 +379,6 @@ input:focus {
line-height: normal;
background-color: #fff;
position: relative;
outline: none;
}
}
}
@ -399,11 +399,10 @@ input:focus {
border-right: 1px solid #E7E7E7;
padding: 5px;
input, button {
input, button, select {
display: block;
padding: 5px;
margin: 5px;
}
.error {
display: none;
// width: 100%;

@ -1,4 +1,4 @@
<template name="room">
<template name="livechatWindow">
{{#if livechatStartedEnabled}}
<div class="livechat-room">
<div class="title" style="background-color:{{color}}">
@ -12,10 +12,10 @@
</div>
{{#if livechatEnabled}}
{{#if showMessages}}
{{> messages}}
{{else}}
{{#if showRegisterForm}}
{{> register}}
{{else}}
{{> messages}}
{{/if}}
{{else}}
<div class="offline">{{_ "We_are_offline_Sorry_for_the_inconvenience"}}</div>

@ -1,60 +1,62 @@
Template.room.helpers({
title: function() {
Template.livechatWindow.helpers({
title() {
var ref;
if (!Template.instance().subscriptionsReady()) {
return '';
}
return ((ref = Settings.findOne('Livechat_title')) != null ? ref.value : void 0) || 'Rocket.Chat';
},
color: function() {
color() {
var ref;
if (!Template.instance().subscriptionsReady()) {
return 'transparent';
}
return ((ref = Settings.findOne('Livechat_title_color')) != null ? ref.value : void 0) || '#C1272D';
},
popoutActive: function() {
popoutActive() {
return FlowRouter.getQueryParam('mode') === 'popout';
},
showMessages: function() {
return Session.get('triggered') || Meteor.userId();
showRegisterForm() {
if (Session.get('triggered') || Meteor.userId()) {
return false;
}
var form = Settings.findOne('Livechat_registration_form');
return form.value;
},
livechatStartedEnabled: function() {
livechatStartedEnabled() {
return Template.instance().startedEnabled.get() !== null;
},
livechatEnabled: function() {
livechatEnabled() {
return Template.instance().startedEnabled.get();
}
});
Template.room.events({
'click .title': function() {
Template.livechatWindow.events({
'click .title'() {
parentCall('toggleWindow');
},
'click .popout': function(event) {
'click .popout'(event) {
event.stopPropagation();
parentCall('openPopout');
}
});
Template.room.onCreated(function() {
self = this;
self.startedEnabled = new ReactiveVar(null);
Template.livechatWindow.onCreated(function() {
this.startedEnabled = new ReactiveVar(null);
self.subscribe('settings', ['Livechat_title', 'Livechat_title_color', 'Livechat_enabled']);
this.subscribe('settings', ['Livechat_title', 'Livechat_title_color', 'Livechat_enabled', 'Livechat_registration_form']);
var initialCheck = true;
self.autorun(function() {
if (self.subscriptionsReady()) {
this.autorun(() => {
if (this.subscriptionsReady()) {
var enabled = Settings.findOne('Livechat_enabled');
if (enabled !== undefined) {
if (!enabled.value && initialCheck) {
parentCall('removeWidget');
}
initialCheck = false;
self.startedEnabled.set(enabled.value);
this.startedEnabled.set(enabled.value);
}
}
});

@ -1,50 +0,0 @@
Template.register.helpers
error: ->
return Template.instance().error.get()
title: ->
return '' unless Template.instance().subscriptionsReady()
return Settings.findOne('Livechat_title')?.value or 'Rocket.Chat'
color: ->
return 'transparent' unless Template.instance().subscriptionsReady()
return Settings.findOne('Livechat_title_color')?.value or '#C1272D'
welcomeMessage: ->
return ""
Template.register.events
'submit #livechat-registration': (e, instance) ->
e.preventDefault()
$name = instance.$('input[name=name]')
$email = instance.$('input[name=email]')
unless $name.val().trim() and $email.val().trim()
return instance.showError TAPi18n.__('Please_fill_name_and_email')
else
Meteor.call 'registerGuest', visitor.getToken(), $name.val(), $email.val(), (error, result) ->
if error?
return instance.showError error.reason
Meteor.loginWithToken result.token, (error) ->
if error
return instance.showError error.reason
'click .error': (e, instance) ->
instance.hideError()
Template.register.onCreated ->
@subscribe 'settings', ['Livechat_title', 'Livechat_title_color']
@error = new ReactiveVar
@showError = (msg) =>
$('.error').addClass('show')
@error.set msg
return
@hideError = =>
$('.error').removeClass('show')
@error.set()
return

@ -9,7 +9,16 @@
</label>
<input type="text" name="name" id="guestName" placeholder="{{_ "Name"}}">
<input type="email" name="email" id="guestEmail" placeholder="{{_ "E-mail"}}">
<button type="submit" id="btnEntrar" class="-btn"> {{_ "Start_Chat"}} </button>
{{#if hasDepartments}}
<select name="department">
<option value="">{{_ "Select_a_department"}}</option>
{{#each departments}}
<option value="{{_id}}">{{name}}</option>
{{/each}}
</select>
{{/if}}
<button type="submit" id="btnEntrar" class="button"> {{_ "Start_Chat"}} </button>
</form>
</template>

@ -0,0 +1,68 @@
Template.register.helpers({
error() {
return Template.instance().error.get();
},
welcomeMessage() {
return "";
},
hasDepartments() {
return Department.find().count() > 1;
},
departments() {
return Department.find();
}
});
Template.register.events({
'submit #livechat-registration' (e, instance) {
var $email, $name;
e.preventDefault();
$name = instance.$('input[name=name]');
$email = instance.$('input[name=email]');
if (!($name.val().trim() && $email.val().trim())) {
return instance.showError(TAPi18n.__('Please_fill_name_and_email'));
} else {
var departmentId = instance.$('select[name=department]').val();
if (!departmentId) {
var department = Department.findOne();
if (department) {
departmentId = department._id;
}
}
var guest = {
token: visitor.getToken(),
name: $name.val(),
email: $email.val(),
department: departmentId
};
Meteor.call('livechat:registerGuest', guest, function(error, result) {
if (error != null) {
return instance.showError(error.reason);
}
Meteor.loginWithToken(result.token, function(error) {
if (error) {
return instance.showError(error.reason);
}
});
});
}
},
'click .error' (e, instance) {
return instance.hideError();
}
});
Template.register.onCreated(function() {
this.subscribe('livechat:availableDepartments');
this.error = new ReactiveVar;
this.showError = (msg) => {
$('.error').addClass('show');
this.error.set(msg);
};
this.hideError = () => {
$('.error').removeClass('show');
this.error.set();
};
});

@ -8,6 +8,7 @@
"Installation" : "Installation",
"Please_answer_survey" : "Please take a moment to answer a quick survey about this chat",
"Please_fill_name_and_email" : "Please fill name and e-mail",
"Select_a_department" : "Select a department",
"Skip" : "Skip",
"Start_Chat" : "Start Chat",
"Survey" : "Survey",

@ -3,4 +3,5 @@ Meteor.startup(function() {
RocketChat.settings.add('Livechat_title' , 'Rocket.Chat', { type: 'string', group: 'Livechat', public: true });
RocketChat.settings.add('Livechat_title_color' , '#C1272D', { type: 'string', group: 'Livechat', public: true });
RocketChat.settings.add('Livechat_enabled' , true, { type: 'boolean', group: 'Livechat', public: true });
RocketChat.settings.add('Livechat_registration_form' , true, { type: 'boolean', group: 'Livechat', public: true, i18nLabel: 'Show_preregistration_form' });
});

@ -41,6 +41,7 @@
"Saved" : "Saved",
"Selected_agents" : "Selected agents",
"Send_a_message" : "Send a message",
"Show_preregistration_form" : "Show pre-registration form",
"Theme" : "Theme",
"There_are_no_agents_added_to_this_department_yet" : "There are no agents added to this department yet.",
"Time_in_seconds" : "Time in seconds",

@ -93,6 +93,7 @@ Package.onUse(function(api) {
api.addFiles('server/lib/getNextAgent.js', 'server');
// publications
api.addFiles('server/publications/availableDepartments.js', 'server');
api.addFiles('server/publications/departmentAgents.js', 'server');
api.addFiles('server/publications/livechatAgents.js', 'server');
api.addFiles('server/publications/livechatManagers.js', 'server');

@ -2,7 +2,7 @@ this.getNextAgent = function(department) {
var agentFilter = {};
if (department) {
return RocketChat.models.LivechatDepartment.getNextAgent(department);
return RocketChat.models.LivechatDepartmentAgents.getNextAgentForDepartment(department);
} else {
return RocketChat.models.Users.getNextAgent();
}

@ -1,5 +1,5 @@
Meteor.methods({
registerGuest: function(token, name, email) {
'livechat:registerGuest': function({ token, name, email, department } = {}) {
var pass, qt, user, userData, userExists, userId, inc = 0;
check(token, String);
@ -35,7 +35,8 @@ Meteor.methods({
}
userData = {
username: user,
globalRoles: 'livechat-guest'
globalRoles: 'livechat-guest',
department: department
};
userId = Accounts.insertUserDoc({}, userData);

@ -7,13 +7,23 @@ Meteor.methods({
guest = Meteor.users.findOne(Meteor.userId(), {
fields: {
username: 1
username: 1,
department: 1
}
});
room = RocketChat.models.Rooms.findOneById(message.rid);
if (room == null) {
agent = getNextAgent();
// if no department selected verify if there is only one active and use it
if (!guest.department) {
var departments = RocketChat.models.LivechatDepartment.findEnabledWithAgents();
if (departments.count() === 1) {
guest.department = departments.fetch()[0]._id;
}
}
agent = getNextAgent(guest.department);
if (!agent) {
throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
}

@ -63,6 +63,14 @@ class LivechatDepartment extends RocketChat.models._Base {
query = { _id: _id };
return this.remove(query);
}
findEnabledWithAgents() {
var query = {
numAgents: { $gt: 0 },
enabled: true
};
return this.find(query);
}
}
RocketChat.models.LivechatDepartment = new LivechatDepartment();

@ -0,0 +1,3 @@
Meteor.publish('livechat:availableDepartments', function() {
return RocketChat.models.LivechatDepartment.findEnabledWithAgents();
});

@ -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

@ -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

Loading…
Cancel
Save