Merge pull request #1744 from RocketChat/improvements/outgoing-hooks
Improvements/outgoing hookspull/1747/head
commit
a83faef69e
@ -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> |
@ -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: |
@ -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 |
Loading…
Reference in new issue