diff --git a/packages/rocketchat-api/server/v1/integrations.js b/packages/rocketchat-api/server/v1/integrations.js index eb7919366cb2..d07c9ec1fa17 100644 --- a/packages/rocketchat-api/server/v1/integrations.js +++ b/packages/rocketchat-api/server/v1/integrations.js @@ -7,13 +7,15 @@ RocketChat.API.v1.addRoute('integrations.create', { authRequired: true }, { username: String, urls: [String], channel: String, + event: String, triggerWords: Match.Maybe([String]), alias: Match.Maybe(String), avatar: Match.Maybe(String), emoji: Match.Maybe(String), token: Match.Maybe(String), scriptEnabled: Boolean, - script: Match.Maybe(String) + script: Match.Maybe(String), + targetChannel: Match.Maybe(String) })); let integration; @@ -32,6 +34,37 @@ RocketChat.API.v1.addRoute('integrations.create', { authRequired: true }, { } }); +RocketChat.API.v1.addRoute('integrations.history', { authRequired: true }, { + get: function() { + if (!RocketChat.authz.hasPermission(this.userId, 'manage-integrations')) { + return RocketChat.API.v1.unauthorized(); + } + + if (!this.queryParams.id || this.queryParams.id.trim() === '') { + return RocketChat.API.v1.failure('Invalid integration id.'); + } + + const id = this.queryParams.id; + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { 'integration._id': id }); + const history = RocketChat.models.IntegrationHistory.find(ourQuery, { + sort: sort ? sort : { _updatedAt: -1 }, + skip: offset, + limit: count, + fields + }).fetch(); + + return RocketChat.API.v1.success({ + history, + offset, + items: history.length, + total: RocketChat.models.IntegrationHistory.find(ourQuery).count() + }); + } +}); + RocketChat.API.v1.addRoute('integrations.list', { authRequired: true }, { get: function() { if (!RocketChat.authz.hasPermission(this.userId, 'manage-integrations')) { diff --git a/packages/rocketchat-file-upload/server/methods/sendFileMessage.js b/packages/rocketchat-file-upload/server/methods/sendFileMessage.js index efd2101094dc..a63279a695fd 100644 --- a/packages/rocketchat-file-upload/server/methods/sendFileMessage.js +++ b/packages/rocketchat-file-upload/server/methods/sendFileMessage.js @@ -46,17 +46,24 @@ Meteor.methods({ attachment.video_size = file.size; } - const msg = Object.assign({ + const user = Meteor.user(); + let msg = Object.assign({ _id: Random.id(), rid: roomId, + ts: new Date(), msg: '', file: { - _id: file._id + _id: file._id, + name: file.name }, groupable: false, attachments: [attachment] }, msgData); - return Meteor.call('sendMessage', msg); + msg = Meteor.call('sendMessage', msg); + + Meteor.defer(() => RocketChat.callbacks.run('afterFileUpload', { user, room, message: msg })); + + return msg; } }); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 8bb9e707a598..571c573e2d12 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -230,6 +230,7 @@ "Back": "Back", "Back_to_applications": "Back to applications", "Back_to_integrations": "Back to integrations", + "Back_to_integration_detail": "Back to the integration detail", "Back_to_login": "Back to login", "Back_to_permissions": "Back to permissions", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "Beta feature. Depends on Video Conference to be enabled.", @@ -299,6 +300,7 @@ "Choose_the_username_that_this_integration_will_post_as": "Choose the username that this integration will post as.", "clear": "Clear", "clear_cache_now": "Clear cache now", + "clear_history": "Clear History", "Clear_all_unreads_question": "Clear all unreads?", "Click_here": "Click here", "Click_here_for_more_info": "Click here for more info", @@ -330,6 +332,7 @@ "Create_new": "Create new", "Created_at": "Created at", "Created_at_s_by_s": "Created at %s by %s", + "Created_at_s_by_s_triggered_by_s": "Created at %s by %s triggered by %s", "CROWD_Reject_Unauthorized": "Reject Unauthorized", "CRM_Integration": "CRM Integration", "Current_Chats": "Current Chats", @@ -522,6 +525,8 @@ "error-you-are-last-owner": "You are the last owner. Please set new owner before leaving the room.", "Error_changing_password": "Error changing password", "Esc_to": "Esc to", + "Event_Trigger": "Event Trigger", + "Event_Trigger_Description": "Select which type of event will trigger this Outgoing WebHook Integration", "every_30_minutes": "Once every 30 minutes", "every_hour": "Once every hour", "every_six_hours": "Once every six hours", @@ -671,11 +676,41 @@ "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instructions to your visitor fill the form to send a message", "Impersonate_user": "Impersonate User", "Impersonate_user_description": "When enabled, integration posts as the user that triggered integration", + "Incoming_WebHook": "Incoming WebHook", + "Integration_Advanced_Settings": "Advanced Settings", "Integration_added": "Integration has been added", + "Integration_History_Cleared": "Integration History Successfully Cleared", "Integration_Incoming_WebHook": "Incoming WebHook Integration", "Integration_New": "New Integration", + "Integrations_Outgoing_Type_FileUploaded": "File Uploaded", + "Integrations_Outgoing_Type_RoomArchived": "Room Archived", + "Integrations_Outgoing_Type_RoomCreated": "Room Created (public and private)", + "Integrations_Outgoing_Type_RoomJoined": "User Joined Room", + "Integrations_Outgoing_Type_RoomLeft": "User Left Room", + "Integrations_Outgoing_Type_SendMessage": "Message Sent", + "Integrations_Outgoing_Type_UserCreated": "User Created", "Integration_Outgoing_WebHook": "Outgoing WebHook Integration", - "Integration_updated": "Integration has been updated", + "Integration_Outgoing_WebHook_History": "Outgoing WebHook Integration History", + "Integration_Outgoing_WebHook_No_History": "This outgoing webhook integration has yet to have any history recorded.", + "Integration_Outgoing_WebHook_History_Time_Triggered": "Time Integration Triggered", + "Integration_Outgoing_WebHook_History_Time_Ended_Or_Error": "Time it Ended or Error'd", + "Integration_Outgoing_WebHook_History_Trigger_Step": "Last Trigger Step", + "Integration_Outgoing_WebHook_History_Messages_Sent_From_Prepare_Script": "Messages Sent from Prepare Step", + "Integration_Outgoing_WebHook_History_Messages_Sent_From_Process_Script": "Messages Sent from Process Response Step", + "Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger": "Data Passed to Integration", + "Integration_Outgoing_WebHook_History_Data_Passed_To_URL": "Data Passed to URL", + "Integration_Outgoing_WebHook_History_Http_Response_Error": "HTTP Response Error", + "Integration_Outgoing_WebHook_History_Http_Response": "HTTP Response", + "Integration_Outgoing_WebHook_History_Error_Stacktrace": "Error Stacktrace", + "Integration_Retry_Failed_Url_Calls": "Retry Failed Url Calls", + "Integration_Retry_Failed_Url_Calls_Description": "Should the integration try a reasonable amount of time if the call out to the url fails?", + "Integration_Retry_Count": "Retry Count", + "Integration_Retry_Count_Description": "How many times should the integration be tried if the call to the url fails?", + "Integration_Retry_Delay": "Retry Delay", + "Integration_Retry_Delay_Description": "Which delay algorithm should the retrying use? 10^x or 2^x or x*2", + "Integration_Word_Trigger_Placement": "Word Placement Anywhere", + "Integration_Word_Trigger_Placement_Description": "Should the Word be Triggered when placed anywhere in the sentence other than the beginning?", + "Integration_updated": "Integration has been updated.", "Integrations": "Integrations", "Integrations_for_all_channels": "Enter all_public_channels to listen on all public channels, all_private_groups to listen on all private groups, and all_direct_messages to listen to all direct messages.", "InternalHubot": "Internal Hubot", @@ -990,6 +1025,7 @@ "No_Encryption": "No Encryption", "No_group_with_name_%s_was_found": "No private group with name \"%s\" was found!", "No_groups_yet": "You have no private groups yet.", + "No_integration_found": "No integration found by the provided id.", "No_livechats": "You have no livechats.", "No_mentions_found": "No mentions found", "No_pinned_messages": "No pinned messages", @@ -1057,6 +1093,8 @@ "others": "others", "OTR": "OTR", "OTR_is_only_available_when_both_users_are_online": "OTR is only available when both users are online", + "Outgoing_WebHook": "Outgoing WebHook", + "Outgoing_WebHook_Description": "Get data out of Rocket.Chat in real-time.", "Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "Override URL to which files are uploaded. This url also used for downloads unless a CDN is given", "Page_title": "Page title", "Page_URL": "Page URL", @@ -1366,6 +1404,8 @@ "System_messages": "System Messages", "Tag": "Tag", "Take_it": "Take it!", + "TargetRoom": "Target Room", + "TargetRoom_Description": "The room where messages will be sent which are a result of this event being fired. Only one target room is allowed and it must exist.", "Test_Connection": "Test Connection", "Test_Desktop_Notifications": "Test Desktop Notifications", "Thank_you_exclamation_mark": "Thank you!", diff --git a/packages/rocketchat-integrations/client/collection.coffee b/packages/rocketchat-integrations/client/collection.coffee deleted file mode 100644 index dd1f798db858..000000000000 --- a/packages/rocketchat-integrations/client/collection.coffee +++ /dev/null @@ -1 +0,0 @@ -@ChatIntegrations = new Meteor.Collection 'rocketchat_integrations' diff --git a/packages/rocketchat-integrations/client/collections.js b/packages/rocketchat-integrations/client/collections.js new file mode 100644 index 000000000000..8a2e3ac04ff4 --- /dev/null +++ b/packages/rocketchat-integrations/client/collections.js @@ -0,0 +1,2 @@ +this.ChatIntegrations = new Meteor.Collection('rocketchat_integrations'); +this.ChatIntegrationHistory = new Meteor.Collection('rocketchat_integration_history'); diff --git a/packages/rocketchat-integrations/client/route.coffee b/packages/rocketchat-integrations/client/route.coffee deleted file mode 100644 index 76989898f937..000000000000 --- a/packages/rocketchat-integrations/client/route.coffee +++ /dev/null @@ -1,41 +0,0 @@ -FlowRouter.route '/admin/integrations', - name: 'admin-integrations' - subscriptions: (params, queryParams) -> - this.register 'integrations', Meteor.subscribe('integrations') - action: (params) -> - BlazeLayout.render 'main', - center: 'pageSettingsContainer' - pageTitle: t('Integrations') - pageTemplate: 'integrations' - -FlowRouter.route '/admin/integrations/new', - name: 'admin-integrations-new' - subscriptions: (params, queryParams) -> - this.register 'integrations', Meteor.subscribe('integrations') - action: (params) -> - BlazeLayout.render 'main', - center: 'pageSettingsContainer' - pageTitle: t('Integration_New') - pageTemplate: 'integrationsNew' - -FlowRouter.route '/admin/integrations/incoming/:id?', - name: 'admin-integrations-incoming' - subscriptions: (params, queryParams) -> - this.register 'integrations', Meteor.subscribe('integrations') - action: (params) -> - BlazeLayout.render 'main', - center: 'pageSettingsContainer' - pageTitle: t('Integration_Incoming_WebHook') - pageTemplate: 'integrationsIncoming' - params: params - -FlowRouter.route '/admin/integrations/outgoing/:id?', - name: 'admin-integrations-outgoing' - subscriptions: (params, queryParams) -> - this.register 'integrations', Meteor.subscribe('integrations') - action: (params) -> - BlazeLayout.render 'main', - center: 'pageSettingsContainer' - pageTitle: t('Integration_Outgoing_WebHook') - pageTemplate: 'integrationsOutgoing' - params: params diff --git a/packages/rocketchat-integrations/client/route.js b/packages/rocketchat-integrations/client/route.js new file mode 100644 index 000000000000..3e3ac93228b3 --- /dev/null +++ b/packages/rocketchat-integrations/client/route.js @@ -0,0 +1,62 @@ +FlowRouter.route('/admin/integrations', { + name: 'admin-integrations', + subscriptions() { + this.register('integrations', Meteor.subscribe('integrations')); + }, + action() { + return BlazeLayout.render('main', { + center: 'integrations', + pageTitle: t('Integrations') + }); + } +}); + +FlowRouter.route('/admin/integrations/new', { + name: 'admin-integrations-new', + subscriptions() { + this.register('integrations', Meteor.subscribe('integrations')); + }, + action() { + return BlazeLayout.render('main', { + center: 'integrationsNew', + pageTitle: t('Integration_New') + }); + } +}); + +FlowRouter.route('/admin/integrations/incoming/:id?', { + name: 'admin-integrations-incoming', + subscriptions() { + this.register('integrations', Meteor.subscribe('integrations')); + }, + action(params) { + return BlazeLayout.render('main', { + center: 'pageSettingsContainer', + pageTitle: t('Integration_Incoming_WebHook'), + pageTemplate: 'integrationsIncoming', + params + }); + } +}); + +FlowRouter.route('/admin/integrations/outgoing/:id?', { + name: 'admin-integrations-outgoing', + action(params) { + return BlazeLayout.render('main', { + center: 'integrationsOutgoing', + pageTitle: t('Integration_Outgoing_WebHook'), + params + }); + } +}); + +FlowRouter.route('/admin/integrations/outgoing/:id?/history', { + name: 'admin-integrations-outgoing-history', + action(params) { + return BlazeLayout.render('main', { + center: 'integrationsOutgoingHistory', + pageTitle: t('Integration_Outgoing_WebHook_History'), + params + }); + } +}); diff --git a/packages/rocketchat-integrations/client/startup.coffee b/packages/rocketchat-integrations/client/startup.coffee deleted file mode 100644 index 737251db50cd..000000000000 --- a/packages/rocketchat-integrations/client/startup.coffee +++ /dev/null @@ -1,5 +0,0 @@ -RocketChat.AdminBox.addOption - href: 'admin-integrations' - i18nLabel: 'Integrations' - permissionGranted: -> - return RocketChat.authz.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']) diff --git a/packages/rocketchat-integrations/client/startup.js b/packages/rocketchat-integrations/client/startup.js new file mode 100644 index 000000000000..8462e3d4cb21 --- /dev/null +++ b/packages/rocketchat-integrations/client/startup.js @@ -0,0 +1,5 @@ +RocketChat.AdminBox.addOption({ + href: 'admin-integrations', + i18nLabel: 'Integrations', + permissionGranted: () => RocketChat.authz.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']) +}); diff --git a/packages/rocketchat-integrations/client/views/integrations.coffee b/packages/rocketchat-integrations/client/views/integrations.coffee deleted file mode 100644 index b182b2566b2a..000000000000 --- a/packages/rocketchat-integrations/client/views/integrations.coffee +++ /dev/null @@ -1,11 +0,0 @@ -import moment from 'moment' - -Template.integrations.helpers - hasPermission: -> - return RocketChat.authz.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']) - - integrations: -> - return ChatIntegrations.find() - - dateFormated: (date) -> - return moment(date).format('L LT') diff --git a/packages/rocketchat-integrations/client/views/integrations.html b/packages/rocketchat-integrations/client/views/integrations.html index 52f196b1c8b2..9cfc4580cffb 100644 --- a/packages/rocketchat-integrations/client/views/integrations.html +++ b/packages/rocketchat-integrations/client/views/integrations.html @@ -1,57 +1,68 @@ diff --git a/packages/rocketchat-integrations/client/views/integrations.js b/packages/rocketchat-integrations/client/views/integrations.js new file mode 100644 index 000000000000..1a32a52700a4 --- /dev/null +++ b/packages/rocketchat-integrations/client/views/integrations.js @@ -0,0 +1,17 @@ +/* global ChatIntegrations */ +import moment from 'moment'; + +Template.integrations.helpers({ + hasPermission() { + return RocketChat.authz.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']); + }, + integrations() { + return ChatIntegrations.find(); + }, + dateFormated(date) { + return moment(date).format('L LT'); + }, + eventTypeI18n(event) { + return TAPi18n.__(RocketChat.integrations.outgoingEvents[event].label); + } +}); diff --git a/packages/rocketchat-integrations/client/views/integrationsIncoming.coffee b/packages/rocketchat-integrations/client/views/integrationsIncoming.coffee deleted file mode 100644 index e35e5ebaed93..000000000000 --- a/packages/rocketchat-integrations/client/views/integrationsIncoming.coffee +++ /dev/null @@ -1,207 +0,0 @@ -import toastr from 'toastr' -Template.integrationsIncoming.onCreated -> - @record = new ReactiveVar - username: 'rocket.cat' - - -Template.integrationsIncoming.helpers - - hasPermission: -> - return RocketChat.authz.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']) - - data: -> - params = Template.instance().data.params?() - - if params?.id? - data = null - if RocketChat.authz.hasAllPermission 'manage-integrations' - data = ChatIntegrations.findOne({_id: params.id}) - else if RocketChat.authz.hasAllPermission 'manage-own-integrations' - data = ChatIntegrations.findOne({_id: params.id, "_createdBy._id": Meteor.userId()}) - if data? - data.url = Meteor.absoluteUrl("hooks/#{data._id}/#{data.token}") - data.completeToken = "#{data._id}/#{data.token}" - Template.instance().record.set data - 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: 'Example message' - 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: 'Example message' - 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 - - curl: -> - record = Template.instance().record.get() - - if not record.url? - return - - data = - username: record.alias - icon_emoji: record.emoji - icon_url: record.avatar - text: 'Example message' - 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 "curl -X POST -H 'Content-Type: application/json' --data '#{JSON.stringify(data)}' #{record.url}" - - editorOptions: -> - return {} = - lineNumbers: true - mode: "javascript" - gutters: [ - # "CodeMirror-lint-markers" - "CodeMirror-linenumbers" - "CodeMirror-foldgutter" - ] - # lint: true - foldGutter: true - # lineWrapping: true - matchBrackets: true - autoCloseBrackets: true - matchTags: true, - showTrailingSpace: true - highlightSelectionMatches: true - - -Template.integrationsIncoming.events - "blur input": (e, t) -> - value = t.record.curValue or {} - - value.name = $('[name=name]').val().trim() - value.alias = $('[name=alias]').val().trim() - value.emoji = $('[name=emoji]').val().trim() - value.avatar = $('[name=avatar]').val().trim() - value.channel = $('[name=channel]').val().trim() - value.username = $('[name=username]').val().trim() - - t.record.set value - - "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 "deleteIncomingIntegration", params.id, (err, data) -> - if err - handleError err - else - swal - title: t('Deleted') - text: t('Your_entry_has_been_deleted') - type: 'success' - timer: 1000 - showConfirmButton: false - - FlowRouter.go "admin-integrations" - - "click .button-fullscreen": -> - codeMirrorBox = $('.code-mirror-box') - codeMirrorBox.addClass('code-mirror-box-fullscreen content-background-color') - codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh() - - "click .button-restore": -> - codeMirrorBox = $('.code-mirror-box') - codeMirrorBox.removeClass('code-mirror-box-fullscreen content-background-color') - codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh() - - "click .submit > .save": -> - enabled = $('[name=enabled]:checked').val().trim() - 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() - scriptEnabled = $('[name=scriptEnabled]:checked').val().trim() - script = $('[name=script]').val().trim() - - if channel is '' - return toastr.error TAPi18n.__("The_channel_name_is_required") - - if username is '' - return toastr.error TAPi18n.__("The_username_is_required") - - integration = - enabled: enabled is '1' - channel: channel - alias: alias if alias isnt '' - emoji: emoji if emoji isnt '' - avatar: avatar if avatar isnt '' - name: name if name isnt '' - script: script if script isnt '' - scriptEnabled: scriptEnabled is '1' - - params = Template.instance().data.params?() - if params?.id? - Meteor.call "updateIncomingIntegration", params.id, integration, (err, data) -> - if err? - return handleError(err) - - toastr.success TAPi18n.__("Integration_updated") - else - integration.username = username - - Meteor.call "addIncomingIntegration", integration, (err, data) -> - if err? - return handleError(err) - - toastr.success TAPi18n.__("Integration_added") - FlowRouter.go "admin-integrations-incoming", {id: data._id} diff --git a/packages/rocketchat-integrations/client/views/integrationsIncoming.js b/packages/rocketchat-integrations/client/views/integrationsIncoming.js new file mode 100644 index 000000000000..8749d7dbfc14 --- /dev/null +++ b/packages/rocketchat-integrations/client/views/integrationsIncoming.js @@ -0,0 +1,253 @@ +/* global ChatIntegrations, hljs */ +import toastr from 'toastr'; + +Template.integrationsIncoming.onCreated(function _incomingIntegrationsOnCreated() { + return this.record = new ReactiveVar({ + username: 'rocket.cat' + }); +}); + +Template.integrationsIncoming.helpers({ + hasPermission() { + return RocketChat.authz.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']); + }, + + data() { + const params = Template.instance().data.params ? Template.instance().data.params() : undefined; + + if (params && params.id) { + let data; + if (RocketChat.authz.hasAllPermission('manage-integrations')) { + data = ChatIntegrations.findOne({ _id: params.id }); + } else if (RocketChat.authz.hasAllPermission('manage-own-integrations')) { + data = ChatIntegrations.findOne({_id: params.id, '_createdBy._id': Meteor.userId() }); + } + + if (data) { + const completeToken = `${data._id}/${data.token}`; + data.url = Meteor.absoluteUrl(`hooks/${completeToken}`); + data.completeToken = completeToken; + Template.instance().record.set(data); + return data; + } + } + + return Template.instance().record.curValue; + }, + + example() { + const record = Template.instance().record.get(); + return { + _id: Random.id(), + alias: record.alias, + emoji: record.emoji, + avatar: record.avatar, + msg: 'Example message', + 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() { + const record = Template.instance().record.get(); + const data = { + username: record.alias, + icon_emoji: record.emoji, + icon_url: record.avatar, + text: 'Example message', + 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' + }] + }; + + const invalidData = [null, '']; + Object.keys(data).forEach((key) => { + if (invalidData.includes(data[key])) { + delete data[key]; + } + }); + + return hljs.highlight('json', JSON.stringify(data, null, 2)).value; + }, + + curl() { + const record = Template.instance().record.get(); + + if (!record.url) { + return; + } + + const data = { + username: record.alias, + icon_emoji: record.emoji, + icon_url: record.avatar, + text: 'Example message', + 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' + }] + }; + + const invalidData = [null, '']; + Object.keys(data).forEach((key) => { + if (invalidData.includes(data[key])) { + delete data[key]; + } + }); + + return `curl -X POST -H 'Content-Type: application/json' --data 'payload=${JSON.stringify(data)}' ${record.url}`; + }, + + editorOptions() { + return { + lineNumbers: true, + mode: 'javascript', + gutters: [ + // 'CodeMirror-lint-markers' + 'CodeMirror-linenumbers', + 'CodeMirror-foldgutter' + ], + // lint: true, + foldGutter: true, + // lineWrapping: true, + matchBrackets: true, + autoCloseBrackets: true, + matchTags: true, + showTrailingSpace: true, + highlightSelectionMatches: true + }; + } +}); + +Template.integrationsIncoming.events({ + 'blur input': (e, t) => { + const value = t.record.curValue || {}; + + value.name = $('[name=name]').val().trim(); + value.alias = $('[name=alias]').val().trim(); + value.emoji = $('[name=emoji]').val().trim(); + value.avatar = $('[name=avatar]').val().trim(); + value.channel = $('[name=channel]').val().trim(); + value.username = $('[name=username]').val().trim(); + + t.record.set(value); + }, + + 'click .submit > .delete': () => { + const 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('deleteIncomingIntegration', params.id, (err) => { + if (err) { + handleError(err); + } else { + swal({ + title: t('Deleted'), + text: t('Your_entry_has_been_deleted'), + type: 'success', + timer: 1000, + showConfirmButton: false + }); + + FlowRouter.go('admin-integrations'); + } + }); + }); + }, + + 'click .button-fullscreen': () => { + const codeMirrorBox = $('.code-mirror-box'); + codeMirrorBox.addClass('code-mirror-box-fullscreen content-background-color'); + codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh(); + }, + + 'click .button-restore': () => { + const codeMirrorBox = $('.code-mirror-box'); + codeMirrorBox.removeClass('code-mirror-box-fullscreen content-background-color'); + codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh(); + }, + + 'click .submit > .save': () => { + const enabled = $('[name=enabled]:checked').val().trim(); + const name = $('[name=name]').val().trim(); + const alias = $('[name=alias]').val().trim(); + const emoji = $('[name=emoji]').val().trim(); + const avatar = $('[name=avatar]').val().trim(); + const channel = $('[name=channel]').val().trim(); + const username = $('[name=username]').val().trim(); + const scriptEnabled = $('[name=scriptEnabled]:checked').val().trim(); + const script = $('[name=script]').val().trim(); + + if (channel === '') { + return toastr.error(TAPi18n.__('The_channel_name_is_required')); + } + + if (username === '') { + return toastr.error(TAPi18n.__('The_username_is_required')); + } + + const integration = { + enabled: enabled === '1', + channel: channel, + alias: alias !== '' ? alias : undefined, + emoji: emoji !== '' ? emoji : undefined, + avatar: avatar !== '' ? avatar : undefined, + name: name !== '' ? name : undefined, + script: script !== '' ? script : undefined, + scriptEnabled: scriptEnabled === '1' + }; + + const params = Template.instance().data.params ? Template.instance().data.params() : undefined; + if (params && params.id) { + Meteor.call('updateIncomingIntegration', params.id, integration, (err) => { + if (err) { + return handleError(err); + } + + toastr.success(TAPi18n.__('Integration_updated')); + }); + } else { + integration.username = username; + + Meteor.call('addIncomingIntegration', integration, (err, data) => { + if (err) { + return handleError(err); + } + + toastr.success(TAPi18n.__('Integration_added')); + FlowRouter.go('admin-integrations-incoming', { id: data._id }); + }); + } + } +}); diff --git a/packages/rocketchat-integrations/client/views/integrationsNew.coffee b/packages/rocketchat-integrations/client/views/integrationsNew.coffee deleted file mode 100644 index 2320aa40f4c7..000000000000 --- a/packages/rocketchat-integrations/client/views/integrationsNew.coffee +++ /dev/null @@ -1,3 +0,0 @@ -Template.integrationsNew.helpers - hasPermission: -> - return RocketChat.authz.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']) diff --git a/packages/rocketchat-integrations/client/views/integrationsNew.html b/packages/rocketchat-integrations/client/views/integrationsNew.html index f4425ff8c3d2..3f051088066b 100644 --- a/packages/rocketchat-integrations/client/views/integrationsNew.html +++ b/packages/rocketchat-integrations/client/views/integrationsNew.html @@ -1,44 +1,52 @@ diff --git a/packages/rocketchat-integrations/client/views/integrationsNew.js b/packages/rocketchat-integrations/client/views/integrationsNew.js new file mode 100644 index 000000000000..530a1cb3ceb4 --- /dev/null +++ b/packages/rocketchat-integrations/client/views/integrationsNew.js @@ -0,0 +1,5 @@ +Template.integrationsNew.helpers({ + hasPermission() { + return RocketChat.authz.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']); + } +}); diff --git a/packages/rocketchat-integrations/client/views/integrationsOutgoing.coffee b/packages/rocketchat-integrations/client/views/integrationsOutgoing.coffee deleted file mode 100644 index bb13783bfefd..000000000000 --- a/packages/rocketchat-integrations/client/views/integrationsOutgoing.coffee +++ /dev/null @@ -1,209 +0,0 @@ -import toastr from 'toastr' -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.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']) - - data: -> - params = Template.instance().data.params?() - - if params?.id? - data = null - if RocketChat.authz.hasAllPermission 'manage-integrations' - data = ChatIntegrations.findOne({_id: params.id}) - else if RocketChat.authz.hasAllPermission 'manage-own-integrations' - data = ChatIntegrations.findOne({_id: params.id, "_createdBy._id": Meteor.userId()}) - 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 - - editorOptions: -> - return {} = - lineNumbers: true - mode: "javascript" - gutters: [ - # "CodeMirror-lint-markers" - "CodeMirror-linenumbers" - "CodeMirror-foldgutter" - ] - # lint: true - foldGutter: true - # lineWrapping: true - matchBrackets: true - autoCloseBrackets: true - matchTags: true, - showTrailingSpace: true - highlightSelectionMatches: true - - -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) -> - if err - handleError(err) - else - swal - title: t('Deleted') - text: t('Your_entry_has_been_deleted') - type: 'success' - timer: 1000 - showConfirmButton: false - - FlowRouter.go "admin-integrations" - - "click .button-fullscreen": -> - $('.code-mirror-box').addClass('code-mirror-box-fullscreen content-background-color'); - $('.CodeMirror')[0].CodeMirror.refresh() - - "click .button-restore": -> - $('.code-mirror-box').removeClass('code-mirror-box-fullscreen content-background-color'); - $('.CodeMirror')[0].CodeMirror.refresh() - - "click .submit > .save": -> - enabled = $('[name=enabled]:checked').val().trim() - name = $('[name=name]').val().trim() - impersonateUser = $('[name=impersonateUser]:checked').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() - scriptEnabled = $('[name=scriptEnabled]:checked').val().trim() - script = $('[name=script]').val().trim() - - if username is '' and impersonateUser is '0' - 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] - - 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 = - enabled: enabled is '1' - username: username - channel: channel if channel isnt '' - 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 '' - script: script if script isnt '' - scriptEnabled: scriptEnabled is '1' - impersonateUser: impersonateUser is '1' - - params = Template.instance().data.params?() - if params?.id? - Meteor.call "updateOutgoingIntegration", params.id, integration, (err, data) -> - if err? - return handleError err - - toastr.success TAPi18n.__("Integration_updated") - else - Meteor.call "addOutgoingIntegration", integration, (err, data) -> - if err? - return handleError(err) - - toastr.success TAPi18n.__("Integration_added") - FlowRouter.go "admin-integrations-outgoing", {id: data._id} diff --git a/packages/rocketchat-integrations/client/views/integrationsOutgoing.html b/packages/rocketchat-integrations/client/views/integrationsOutgoing.html index 17c4baa48ec1..2a2e6b9e8656 100644 --- a/packages/rocketchat-integrations/client/views/integrationsOutgoing.html +++ b/packages/rocketchat-integrations/client/views/integrationsOutgoing.html @@ -1,145 +1,236 @@ diff --git a/packages/rocketchat-integrations/client/views/integrationsOutgoing.js b/packages/rocketchat-integrations/client/views/integrationsOutgoing.js new file mode 100644 index 000000000000..b45edce1993e --- /dev/null +++ b/packages/rocketchat-integrations/client/views/integrationsOutgoing.js @@ -0,0 +1,361 @@ +/* global ChatIntegrations, hljs */ + +import toastr from 'toastr'; + +Template.integrationsOutgoing.onCreated(function _integrationsOutgoingOnCreated() { + this.record = new ReactiveVar({ + username: 'rocket.cat', + token: Random.id(24), + retryFailedCalls: true, + retryCount: 6, + retryDelay: 'powers-of-ten' + }); + + this.updateRecord = () => { + this.record.set({ + enabled: $('[name=enabled]:checked').val().trim() === '1', + event: $('[name=event]').val().trim(), + name: $('[name=name]').val().trim(), + alias: $('[name=alias]').val().trim(), + emoji: $('[name=emoji]').val().trim(), + avatar: $('[name=avatar]').val().trim(), + channel: $('[name=channel]').val()? $('[name=channel]').val().trim() : undefined, + username: $('[name=username]').val().trim(), + triggerWords: $('[name=triggerWords]').val() ? $('[name=triggerWords]').val().trim() : undefined, + urls: $('[name=urls]').val().trim(), + token: $('[name=token]').val().trim(), + scriptEnabled: $('[name=scriptEnabled]:checked').val().trim() === '1', + script: $('[name=script]').val().trim(), + targetRoom: $('[name=targetRoom]').val() ? $('[name=targetRoom]').val().trim() : undefined, + triggerWordAnywhere: $('[name=triggerWordAnywhere]').val() ? $('[name=triggerWordAnywhere]').val().trim() : undefined, + retryFailedCalls: $('[name=retryFailedCalls]:checked').val().trim() === '1', + retryCount: $('[name=retryCount]').val() ? $('[name=retryCount]').val().trim() : 6, + retryDelay: $('[name=retryDelay]').val() ? $('[name=retryDelay]').val().trim() : 'powers-of-ten' + }); + }; + + this.autorun(() => { + const id = this.data && this.data.params && this.data.params().id; + + if (id) { + const sub = this.subscribe('integrations'); + if (sub.ready()) { + let intRecord; + + if (RocketChat.authz.hasAllPermission('manage-integrations')) { + intRecord = ChatIntegrations.findOne({ _id: id }); + } else if (RocketChat.authz.hasAllPermission('manage-own-integrations')) { + intRecord = ChatIntegrations.findOne({ _id: id, '_createdBy._id': Meteor.userId() }); + } + + if (intRecord) { + this.record.set(intRecord); + } else { + toastr.error(TAPi18n.__('No_integration_found')); + FlowRouter.go('admin-integrations'); + } + } + } + }); +}); + +Template.integrationsOutgoing.helpers({ + join(arr, sep) { + if (!arr || !arr.join) { + return arr; + } + + return arr.join(sep); + }, + + showHistoryButton() { + return this.params && this.params() && typeof this.params().id !== 'undefined'; + }, + + hasPermission() { + return RocketChat.authz.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']); + }, + + data() { + return Template.instance().record.get(); + }, + + canDelete() { + return this.params && this.params() && typeof this.params().id !== 'undefined'; + }, + + eventTypes() { + return Object.values(RocketChat.integrations.outgoingEvents); + }, + + hasTypeSelected() { + const record = Template.instance().record.get(); + + return typeof record.event === 'string' && record.event !== ''; + }, + + shouldDisplayChannel() { + const record = Template.instance().record.get(); + + return typeof record.event === 'string' && RocketChat.integrations.outgoingEvents[record.event].use.channel; + }, + + shouldDisplayTriggerWords() { + const record = Template.instance().record.get(); + + return typeof record.event === 'string' && RocketChat.integrations.outgoingEvents[record.event].use.triggerWords; + }, + + shouldDisplayTargetRoom() { + const record = Template.instance().record.get(); + + return typeof record.event === 'string' && RocketChat.integrations.outgoingEvents[record.event].use.targetRoom; + }, + + example() { + const 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() { + const record = Template.instance().record.get(); + const 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' + }] + }; + + const invalidData = [null, '']; + Object.keys(data).forEach((key) => { + if (invalidData.includes(data[key])) { + delete data[key]; + } + }); + + return hljs.highlight('json', JSON.stringify(data, null, 2)).value; + }, + + editorOptions() { + return { + lineNumbers: true, + mode: 'javascript', + gutters: [ + // "CodeMirror-lint-markers", + 'CodeMirror-linenumbers', + 'CodeMirror-foldgutter' + ], + // lint: true, + foldGutter: true, + // lineWrapping: true, + matchBrackets: true, + autoCloseBrackets: true, + matchTags: true, + showTrailingSpace: true, + highlightSelectionMatches: true + }; + } +}); + +Template.integrationsOutgoing.events({ + 'blur input': (e, t) => { + t.updateRecord(); + }, + + 'click input[type=radio]': (e, t) => { + t.updateRecord(); + }, + + 'change select[name=event]': (e, t) => { + const record = t.record.get(); + record.event = $('[name=event]').val().trim(); + + t.record.set(record); + }, + + 'click .button.history': () => { + FlowRouter.go(`/admin/integrations/outgoing/${FlowRouter.getParam('id')}/history`); + }, + + 'click .expand': (e) => { + $(e.currentTarget).closest('.section').removeClass('section-collapsed'); + $(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__('Collapse')); + $('.CodeMirror').each((index, codeMirror) => codeMirror.CodeMirror.refresh()); + }, + + 'click .collapse': (e) => { + $(e.currentTarget).closest('.section').addClass('section-collapsed'); + $(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__('Expand')); + }, + + 'click .submit > .delete': () => { + const 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) => { + if (err) { + handleError(err); + } else { + swal({ + title: t('Deleted'), + text: t('Your_entry_has_been_deleted'), + type: 'success', + timer: 1000, + showConfirmButton: false + }); + + FlowRouter.go('admin-integrations'); + } + }); + }); + }, + + 'click .button-fullscreen': () => { + $('.code-mirror-box').addClass('code-mirror-box-fullscreen content-background-color'); + $('.CodeMirror')[0].CodeMirror.refresh(); + }, + + 'click .button-restore': () => { + $('.code-mirror-box').removeClass('code-mirror-box-fullscreen content-background-color'); + $('.CodeMirror')[0].CodeMirror.refresh(); + }, + + 'click .submit > .save': () => { + const event = $('[name=event]').val().trim(); + const enabled = $('[name=enabled]:checked').val().trim(); + const name = $('[name=name]').val().trim(); + const impersonateUser = $('[name=impersonateUser]:checked').val().trim(); + const alias = $('[name=alias]').val().trim(); + const emoji = $('[name=emoji]').val().trim(); + const avatar = $('[name=avatar]').val().trim(); + const username = $('[name=username]').val().trim(); + const token = $('[name=token]').val().trim(); + const scriptEnabled = $('[name=scriptEnabled]:checked').val().trim(); + const script = $('[name=script]').val().trim(); + const retryFailedCalls = $('[name=retryFailedCalls]:checked').val().trim(); + let urls = $('[name=urls]').val().trim(); + + if (username === '' && impersonateUser === '0') { + return toastr.error(TAPi18n.__('The_username_is_required')); + } + + urls = urls.split('\n').filter((url) => url.trim() !== ''); + if (urls.length === 0) { + return toastr.error(TAPi18n.__('You_should_inform_one_url_at_least')); + } + + let triggerWords, triggerWordAnywhere; + if (RocketChat.integrations.outgoingEvents[event].use.triggerWords) { + triggerWords = $('[name=triggerWords]').val().trim(); + triggerWords = triggerWords.split(',').filter((word) => word.trim() !== ''); + + triggerWordAnywhere = $('[name=triggerWordAnywhere]').val().trim(); + } + + let channel; + if (RocketChat.integrations.outgoingEvents[event].use.channel) { + channel = $('[name=channel]').val().trim(); + + if (!channel || channel.trim() === '') { + return toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__('Channel') })); + } + } + + let targetRoom; + if (RocketChat.integrations.outgoingEvents[event].use.targetRoom) { + targetRoom = $('[name=targetRoom]').val().trim(); + + if (!targetRoom || targetRoom.trim() === '') { + return toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__('TargetRoom') })); + } + } + + let retryCount, retryDelay; + if (retryFailedCalls === '1') { + retryCount = parseInt($('[name=retryCount]').val().trim()); + retryDelay: $('[name=retryDelay]').val().trim(); + } + + const integration = { + event: event !== '' ? event : undefined, + enabled: enabled === '1', + username: username, + channel: channel !== '' ? channel : undefined, + targetRoom: targetRoom !== '' ? targetRoom : undefined, + alias: alias !== '' ? alias : undefined, + emoji: emoji !== '' ? emoji : undefined, + avatar: avatar !== '' ? avatar : undefined, + name: name !== '' ? name : undefined, + triggerWords: triggerWords !== '' ? triggerWords : undefined, + urls: urls !== '' ? urls : undefined, + token: token !== '' ? token : undefined, + script: script !== '' ? script : undefined, + scriptEnabled: scriptEnabled === '1', + impersonateUser: impersonateUser === '1', + retryFailedCalls: retryFailedCalls === '1', + retryCount: retryCount ? retryCount : 6, + retryDelay: retryDelay ? retryDelay : 'powers-of-ten', + triggerWordAnywhere: triggerWordAnywhere === '1' + }; + + const params = Template.instance().data.params? Template.instance().data.params() : undefined; + if (params && params.id) { + Meteor.call('updateOutgoingIntegration', params.id, integration, (err) => { + if (err) { + return handleError(err); + } + + toastr.success(TAPi18n.__('Integration_updated')); + }); + } else { + Meteor.call('addOutgoingIntegration', integration, (err, data) => { + if (err) { + return handleError(err); + } + + toastr.success(TAPi18n.__('Integration_added')); + FlowRouter.go('admin-integrations-outgoing', { id: data._id }); + }); + } + } +}); diff --git a/packages/rocketchat-integrations/client/views/integrationsOutgoingHistory.html b/packages/rocketchat-integrations/client/views/integrationsOutgoingHistory.html new file mode 100644 index 000000000000..eb57c122e3ad --- /dev/null +++ b/packages/rocketchat-integrations/client/views/integrationsOutgoingHistory.html @@ -0,0 +1,148 @@ + diff --git a/packages/rocketchat-integrations/client/views/integrationsOutgoingHistory.js b/packages/rocketchat-integrations/client/views/integrationsOutgoingHistory.js new file mode 100644 index 000000000000..e2c2ddc0a1cc --- /dev/null +++ b/packages/rocketchat-integrations/client/views/integrationsOutgoingHistory.js @@ -0,0 +1,160 @@ +/* global ChatIntegrations, ChatIntegrationHistory, hljs */ +import moment from 'moment'; +import toastr from 'toastr'; + +Template.integrationsOutgoingHistory.onCreated(function _integrationsOutgoingHistoryOnCreated() { + this.hasMore = new ReactiveVar(false); + this.limit = new ReactiveVar(25); + this.autorun(() => { + const id = this.data && this.data.params && this.data.params().id; + + if (id) { + const sub = this.subscribe('integrations'); + if (sub.ready()) { + let intRecord; + + if (RocketChat.authz.hasAllPermission('manage-integrations')) { + intRecord = ChatIntegrations.findOne({ _id: id }); + } else if (RocketChat.authz.hasAllPermission('manage-own-integrations')) { + intRecord = ChatIntegrations.findOne({ _id: id, '_createdBy._id': Meteor.userId() }); + } + + if (!intRecord) { + toastr.error(TAPi18n.__('No_integration_found')); + FlowRouter.go('admin-integrations'); + } + + const historySub = this.subscribe('integrationHistory', intRecord._id, this.limit.get()); + if (historySub.ready()) { + if (ChatIntegrationHistory.find().count() > this.limit.get()) { + this.hasMore.set(true); + } + } + } + } else { + toastr.error(TAPi18n.__('No_integration_found')); + FlowRouter.go('admin-integrations'); + } + }); +}); + +Template.integrationsOutgoingHistory.helpers({ + hasPermission() { + return RocketChat.authz.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']); + }, + + hasMore() { + return Template.instance().hasMore.get(); + }, + + histories() { + return ChatIntegrationHistory.find().fetch().sort((a, b) => { + if (+a._updatedAt < +b._updatedAt) { + return 1; + } + + if (+a._updatedAt > +b._updatedAt) { + return -1; + } + + return 0; + }); + }, + + hasProperty(history, property) { + return typeof history[property] !== 'undefined' || history[property] != null; + }, + + iconClass(history) { + if (typeof history.error !== 'undefined' && history.error) { + return 'icon-cancel-circled error-color'; + } else if (history.finished) { + return 'icon-ok-circled success-color'; + } else { + return 'icon-help-circled'; + } + }, + + statusI18n(error) { + return typeof error !== 'undefined' && error ? TAPi18n.__('Failure') : TAPi18n.__('Success'); + }, + + formatDate(date) { + return moment(date).format('L LTS'); + }, + + formatDateDetail(date) { + return moment(date).format('L HH:mm:ss:SSSS'); + }, + + eventTypei18n(event) { + return TAPi18n.__(RocketChat.integrations.outgoingEvents[event].label); + }, + + jsonStringify(data) { + return data ? hljs.highlight('json', JSON.stringify(data, null, 2)).value : ''; + }, + + hljsStack(errorStack) { + if (!errorStack) { + return ''; + } else if (typeof errorStack === 'object') { + return hljs.highlight('json', JSON.stringify(errorStack, null, 2)).value; + } else { + return hljs.highlight('json', errorStack).value; + } + }, + + integrationId() { + return this.params && this.params() && this.params().id; + } +}); + +Template.integrationsOutgoingHistory.events({ + 'click .expand': (e) => { + $(e.currentTarget).closest('.section').removeClass('section-collapsed'); + $(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__('Collapse')); + $('.CodeMirror').each((index, codeMirror) => codeMirror.CodeMirror.refresh()); + }, + + 'click .collapse': (e) => { + $(e.currentTarget).closest('.section').addClass('section-collapsed'); + $(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__('Expand')); + }, + + 'click .replay': (e, t) => { + if (!t || !t.data || !t.data.params || !t.data.params().id) { + return; + } + + const historyId = $(e.currentTarget).attr('data-history-id'); + + Meteor.call('replayOutgoingIntegration', { integrationId: t.data.params().id, historyId }, (e) => { + if (e) { + handleError(e); + return; + } + }); + }, + + 'click .clear-history': (e, t) => { + if (!t || !t.data || !t.data.params || !t.data.params().id) { + return; + } + + Meteor.call('clearIntegrationHistory', t.data.params().id, (e) => { + if (e) { + handleError(e); + return; + } + + toastr.success(TAPi18n.__('Integration_History_Cleared')); + }); + }, + + 'scroll .content': _.throttle((e, instance) => { + if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight) { + instance.limit.set(instance.limit.get() + 25); + } + }, 200) +}); diff --git a/packages/rocketchat-integrations/lib/rocketchat.coffee b/packages/rocketchat-integrations/lib/rocketchat.coffee deleted file mode 100644 index c04702a3fffd..000000000000 --- a/packages/rocketchat-integrations/lib/rocketchat.coffee +++ /dev/null @@ -1 +0,0 @@ -RocketChat.integrations = {} diff --git a/packages/rocketchat-integrations/lib/rocketchat.js b/packages/rocketchat-integrations/lib/rocketchat.js new file mode 100644 index 000000000000..60f9488c696f --- /dev/null +++ b/packages/rocketchat-integrations/lib/rocketchat.js @@ -0,0 +1,67 @@ +RocketChat.integrations = { + outgoingEvents: { + sendMessage: { + label: 'Integrations_Outgoing_Type_SendMessage', + value: 'sendMessage', + use: { + channel: true, + triggerWords: true, + targetRoom: false + } + }, + fileUploaded: { + label: 'Integrations_Outgoing_Type_FileUploaded', + value: 'fileUploaded', + use: { + channel: true, + triggerWords: false, + targetRoom: false + } + }, + roomArchived: { + label: 'Integrations_Outgoing_Type_RoomArchived', + value: 'roomArchived', + use: { + channel: false, + triggerWords: false, + targetRoom: false + } + }, + roomCreated: { + label: 'Integrations_Outgoing_Type_RoomCreated', + value: 'roomCreated', + use: { + channel: false, + triggerWords: false, + targetRoom: false + } + }, + roomJoined: { + label: 'Integrations_Outgoing_Type_RoomJoined', + value: 'roomJoined', + use: { + channel: true, + triggerWords: false, + targetRoom: false + } + }, + roomLeft: { + label: 'Integrations_Outgoing_Type_RoomLeft', + value: 'roomLeft', + use: { + channel: true, + triggerWords: false, + targetRoom: false + } + }, + userCreated: { + label: 'Integrations_Outgoing_Type_UserCreated', + value: 'userCreated', + use: { + channel: false, + triggerWords: false, + targetRoom: true + } + } + } +}; diff --git a/packages/rocketchat-integrations/package.js b/packages/rocketchat-integrations/package.js index d339476abdc5..5570f3cf0306 100644 --- a/packages/rocketchat-integrations/package.js +++ b/packages/rocketchat-integrations/package.js @@ -22,45 +22,53 @@ Package.onUse(function(api) { api.use('kadira:flow-router', 'client'); api.use('templating', 'client'); - api.addFiles('lib/rocketchat.coffee', ['server', 'client']); - api.addFiles('client/collection.coffee', ['client']); - api.addFiles('client/startup.coffee', 'client'); - api.addFiles('client/route.coffee', 'client'); + api.addFiles('lib/rocketchat.js', ['server', 'client']); + + // items + api.addFiles('client/collections.js', 'client'); + api.addFiles('client/startup.js', 'client'); + api.addFiles('client/route.js', 'client'); // views api.addFiles('client/views/integrations.html', 'client'); - api.addFiles('client/views/integrations.coffee', 'client'); + api.addFiles('client/views/integrations.js', 'client'); api.addFiles('client/views/integrationsNew.html', 'client'); - api.addFiles('client/views/integrationsNew.coffee', 'client'); + api.addFiles('client/views/integrationsNew.js', 'client'); api.addFiles('client/views/integrationsIncoming.html', 'client'); - api.addFiles('client/views/integrationsIncoming.coffee', 'client'); + api.addFiles('client/views/integrationsIncoming.js', 'client'); api.addFiles('client/views/integrationsOutgoing.html', 'client'); - api.addFiles('client/views/integrationsOutgoing.coffee', 'client'); + api.addFiles('client/views/integrationsOutgoing.js', 'client'); + api.addFiles('client/views/integrationsOutgoingHistory.html', 'client'); + api.addFiles('client/views/integrationsOutgoingHistory.js', 'client'); // stylesheets api.addFiles('client/stylesheets/integrations.less', 'client'); api.addFiles('server/logger.js', 'server'); - api.addFiles('server/lib/validation.coffee', 'server'); + api.addFiles('server/lib/validation.js', 'server'); - api.addFiles('server/models/Integrations.coffee', 'server'); + api.addFiles('server/models/Integrations.js', 'server'); + api.addFiles('server/models/IntegrationHistory.js', 'server'); // publications - api.addFiles('server/publications/integrations.coffee', 'server'); + api.addFiles('server/publications/integrations.js', 'server'); + api.addFiles('server/publications/integrationHistory.js', 'server'); // methods - 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.addFiles('server/methods/incoming/addIncomingIntegration.js', 'server'); + api.addFiles('server/methods/incoming/updateIncomingIntegration.js', 'server'); + api.addFiles('server/methods/incoming/deleteIncomingIntegration.js', 'server'); + api.addFiles('server/methods/outgoing/addOutgoingIntegration.js', 'server'); + api.addFiles('server/methods/outgoing/updateOutgoingIntegration.js', 'server'); + api.addFiles('server/methods/outgoing/replayOutgoingIntegration.js', 'server'); + api.addFiles('server/methods/outgoing/deleteOutgoingIntegration.js', 'server'); + api.addFiles('server/methods/clearIntegrationHistory.js', 'server'); // api api.addFiles('server/api/api.coffee', 'server'); - - api.addFiles('server/triggers.coffee', 'server'); + api.addFiles('server/lib/triggerHandler.js', 'server'); + api.addFiles('server/triggers.js', 'server'); api.addFiles('server/processWebhookMessage.js', 'server'); }); diff --git a/packages/rocketchat-integrations/server/lib/triggerHandler.js b/packages/rocketchat-integrations/server/lib/triggerHandler.js new file mode 100644 index 000000000000..27a05d886023 --- /dev/null +++ b/packages/rocketchat-integrations/server/lib/triggerHandler.js @@ -0,0 +1,762 @@ +/* global logger, processWebhookMessage */ +import moment from 'moment'; + +RocketChat.integrations.triggerHandler = new class RocketChatIntegrationHandler { + constructor() { + this.vm = Npm.require('vm'); + this.successResults = [200, 201, 202]; + this.compiledScripts = {}; + this.triggers = {}; + + RocketChat.models.Integrations.find({type: 'webhook-outgoing'}).observe({ + added: (record) => { + this.addIntegration(record); + }, + + changed: (record) => { + this.removeIntegration(record); + this.addIntegration(record); + }, + + removed: (record) => { + this.removeIntegration(record); + } + }); + } + + addIntegration(record) { + logger.outgoing.debug(`Adding the integration ${record.name} of the event ${record.event}!`); + let channels; + if (record.event && !RocketChat.integrations.outgoingEvents[record.event].use.channel) { + logger.outgoing.debug('The integration doesnt rely on channels.'); + //We don't use any channels, so it's special ;) + channels = ['__any']; + } else if (_.isEmpty(record.channel)) { + logger.outgoing.debug('The integration had an empty channel property, so it is going on all the public channels.'); + channels = ['all_public_channels']; + } else { + logger.outgoing.debug('The integration is going on these channels:', record.channel); + channels = [].concat(record.channel); + } + + for (const channel of channels) { + if (!this.triggers[channel]) { + this.triggers[channel] = {}; + } + + this.triggers[channel][record._id] = record; + } + } + + removeIntegration(record) { + for (const trigger of Object.values(this.triggers)) { + delete trigger[record._id]; + } + } + + updateHistory({ historyId, step, integration, event, data, triggerWord, ranPrepareScript, prepareSentMessage, processSentMessage, resultMessage, finished, url, httpCallData, httpError, httpResult, error, errorStack }) { + const history = { + type: 'outgoing-webhook', + step + }; + + // Usually is only added on initial insert + if (integration) { + history.integration = integration; + } + + // Usually is only added on initial insert + if (event) { + history.event = event; + } + + if (data) { + history.data = data; + + if (data.user) { + history.data.user = _.omit(data.user, ['meta', '$loki', 'services']); + } + + if (data.room) { + history.data.room = _.omit(data.room, ['meta', '$loki', 'usernames']); + history.data.room.usernames = ['this_will_be_filled_in_with_usernames_when_replayed']; + } + } + + if (triggerWord) { + history.triggerWord = triggerWord; + } + + if (typeof ranPrepareScript !== 'undefined') { + history.ranPrepareScript = ranPrepareScript; + } + + if (prepareSentMessage) { + history.prepareSentMessage = prepareSentMessage; + } + + if (processSentMessage) { + history.processSentMessage = processSentMessage; + } + + if (resultMessage) { + history.resultMessage = resultMessage; + } + + if (typeof finished !== 'undefined') { + history.finished = finished; + } + + if (url) { + history.url = url; + } + + if (typeof httpCallData !== 'undefined') { + history.httpCallData = httpCallData; + } + + if (httpError) { + history.httpError = httpError; + } + + if (typeof httpResult !== 'undefined') { + history.httpResult = httpResult; + } + + if (typeof error !== 'undefined') { + history.error = error; + } + + if (typeof errorStack !== 'undefined') { + history.errorStack = errorStack; + } + + if (historyId) { + RocketChat.models.IntegrationHistory.update({ _id: historyId }, { $set: history }); + return historyId; + } else { + history._createdAt = new Date(); + return RocketChat.models.IntegrationHistory.insert(Object.assign({ _id: Random.id() }, history)); + } + } + + //Trigger is the trigger, nameOrId is a string which is used to try and find a room, room is a room, message is a message, and data contains "user_name" if trigger.impersonateUser is truthful. + sendMessage({ trigger, nameOrId = '', room, message, data }) { + let user; + //Try to find the user who we are impersonating + if (trigger.impersonateUser) { + user = RocketChat.models.Users.findOneByUsername(data.user_name); + } + + //If they don't exist (aka the trigger didn't contain a user) then we set the user based upon the + //configured username for the integration since this is required at all times. + if (!user) { + user = RocketChat.models.Users.findOneByUsername(trigger.username); + } + + let tmpRoom; + if (nameOrId || trigger.targetRoom) { + tmpRoom = RocketChat.getRoomByNameOrIdWithOptionToJoin({ currentUserId: user._id, nameOrId: nameOrId || trigger.targetRoom, errorOnEmpty: false }) || room; + } else { + tmpRoom = room; + } + + //If no room could be found, we won't be sending any messages but we'll warn in the logs + if (!tmpRoom) { + logger.outgoing.warn(`The Integration "${trigger.name}" doesn't have a room configured nor did it provide a room to send the message to.`); + return; + } + + logger.outgoing.debug(`Found a room for ${trigger.name} which is: ${tmpRoom.name} with a type of ${tmpRoom.t}`); + + message.bot = { i: trigger._id }; + + const defaultValues = { + alias: trigger.alias, + avatar: trigger.avatar, + emoji: trigger.emoji + }; + + if (tmpRoom.t === 'd') { + message.channel = '@' + tmpRoom._id; + } else { + message.channel = '#' + tmpRoom._id; + } + + message = processWebhookMessage(message, user, defaultValues); + return message; + } + + getIntegrationScript(integration) { + const compiledScript = this.compiledScripts[integration._id]; + if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) { + return compiledScript.script; + } + + const script = integration.scriptCompiled; + const store = {}; + const sandbox = { + _, s, console, moment, + Store: { + set: (key, val) => store[key] = val, + get: (key) => store[key] + }, + HTTP: (method, url, options) => { + try { + return { + result: HTTP.call(method, url, options) + }; + } catch (error) { + return { error }; + } + } + }; + + let vmScript; + try { + logger.outgoing.info('Will evaluate script of Trigger', integration.name); + logger.outgoing.debug(script); + + vmScript = this.vm.createScript(script, 'script.js'); + + vmScript.runInNewContext(sandbox); + + if (sandbox.Script) { + this.compiledScripts[integration._id] = { + script: new sandbox.Script(), + store, + _updatedAt: integration._updatedAt + }; + + return this.compiledScripts[integration._id].script; + } + } catch (e) { + logger.outgoing.error(`Error evaluating Script in Trigger ${integration.name}:`); + logger.outgoing.error(script.replace(/^/gm, ' ')); + logger.outgoing.error('Stack Trace:'); + logger.outgoing.error(e.stack.replace(/^/gm, ' ')); + throw new Meteor.Error('error-evaluating-script'); + } + + if (!sandbox.Script) { + logger.outgoing.error(`Class "Script" not in Trigger ${integration.name}:`); + throw new Meteor.Error('class-script-not-found'); + } + } + + hasScriptAndMethod(integration, method) { + if (integration.scriptEnabled !== true || !integration.scriptCompiled || integration.scriptCompiled.trim() === '') { + return false; + } + + let script; + try { + script = this.getIntegrationScript(integration); + } catch (e) { + return false; + } + + return typeof script[method] !== 'undefined'; + } + + executeScript(integration, method, params, historyId) { + let script; + try { + script = this.getIntegrationScript(integration); + } catch (e) { + this.updateHistory({ historyId, step: 'execute-script-getting-script', error: true, errorStack: e }); + return; + } + + if (!script[method]) { + logger.outgoing.error(`Method "${method}" no found in the Integration "${integration.name}"`); + this.updateHistory({ historyId, step: `execute-script-no-method-${method}` }); + return; + } + + try { + const store = this.compiledScripts[integration._id].store; + const sandbox = { + _, s, console, moment, + Store: { + set: (key, val) => store[key] = val, + get: (key) => store[key] + }, + HTTP: (method, url, options) => { + try { + return { + result: HTTP.call(method, url, options) + }; + } catch (error) { + return { error }; + } + }, + script, + method, + params + }; + + this.updateHistory({ historyId, step: `execute-script-before-running-${method}` }); + const result = this.vm.runInNewContext('script[method](params)', sandbox, { timeout: 3000 }); + + logger.outgoing.debug(`Script method "${method}" result of the Integration "${integration.name}" is:`); + logger.outgoing.debug(result); + + return result; + } catch (e) { + this.updateHistory({ historyId, step: `execute-script-error-running-${method}`, error: true, errorStack: e.stack.replace(/^/gm, ' ') }); + logger.outgoing.error(`Error running Script in the Integration ${integration.name}:`); + logger.outgoing.debug(integration.scriptCompiled.replace(/^/gm, ' ')); // Only output the compiled script if debugging is enabled, so the logs don't get spammed. + logger.outgoing.error('Stack:'); + logger.outgoing.error(e.stack.replace(/^/gm, ' ')); + return; + } + } + + eventNameArgumentsToObject() { + const argObject = { + event: arguments[0] + }; + + switch (argObject.event) { + case 'sendMessage': + if (arguments.length >= 3) { + argObject.message = arguments[1]; + argObject.room = arguments[2]; + } + break; + case 'fileUploaded': + if (arguments.length >= 2) { + const arghhh = arguments[1]; + argObject.user = arghhh.user; + argObject.room = arghhh.room; + argObject.message = arghhh.message; + } + break; + case 'roomArchived': + if (arguments.length >= 3) { + argObject.room = arguments[1]; + argObject.user = arguments[2]; + } + break; + case 'roomCreated': + if (arguments.length >= 3) { + argObject.owner = arguments[1]; + argObject.room = arguments[2]; + } + break; + case 'roomJoined': + case 'roomLeft': + if (arguments.length >= 3) { + argObject.user = arguments[1]; + argObject.room = arguments[2]; + } + break; + case 'userCreated': + if (arguments.length >= 2) { + argObject.user = arguments[1]; + } + break; + default: + logger.outgoing.warn(`An Unhandled Trigger Event was called: ${argObject.event}`); + argObject.event = undefined; + break; + } + + logger.outgoing.debug(`Got the event arguments for the event: ${argObject.event}`, argObject); + + return argObject; + } + + mapEventArgsToData(data, { event, message, room, owner, user }) { + switch (event) { + case 'sendMessage': + data.channel_id = room._id; + data.channel_name = room.name; + data.message_id = message._id; + data.timestamp = message.ts; + data.user_id = message.u._id; + data.user_name = message.u.username; + data.text = message.msg; + + if (message.alias) { + data.alias = message.alias; + } + + if (message.bot) { + data.bot = message.bot; + } + break; + case 'fileUploaded': + data.channel_id = room._id; + data.channel_name = room.name; + data.message_id = message._id; + data.timestamp = message.ts; + data.user_id = message.u._id; + data.user_name = message.u.username; + data.text = message.msg; + data.user = user; + data.room = room; + data.message = message; + + if (message.alias) { + data.alias = message.alias; + } + + if (message.bot) { + data.bot = message.bot; + } + break; + case 'roomCreated': + data.channel_id = room._id; + data.channel_name = room.name; + data.timestamp = room.ts; + data.user_id = owner._id; + data.user_name = owner.username; + data.owner = owner; + data.room = room; + break; + case 'roomArchived': + case 'roomJoined': + case 'roomLeft': + data.timestamp = new Date(); + data.channel_id = room._id; + data.channel_name = room.name; + data.user_id = user._id; + data.user_name = user.username; + data.user = user; + data.room = room; + + if (user.type === 'bot') { + data.bot = true; + } + break; + case 'userCreated': + data.timestamp = user.createdAt; + data.user_id = user._id; + data.user_name = user.username; + data.user = user; + + if (user.type === 'bot') { + data.bot = true; + } + break; + default: + break; + } + } + + executeTriggers() { + logger.outgoing.debug('Execute Trigger:', arguments[0]); + + const argObject = this.eventNameArgumentsToObject(...arguments); + const { event, message, room } = argObject; + + //Each type of event should have an event and a room attached, otherwise we + //wouldn't know how to handle the trigger nor would we have anywhere to send the + //result of the integration + if (!event) { + return; + } + + const triggersToExecute = []; + + logger.outgoing.debug('Starting search for triggers for the room:', room ? room._id : '__any'); + if (room) { + switch (room.t) { + case 'd': + const id = room._id.replace(message.u._id, ''); + const username = _.without(room.usernames, message.u.username)[0]; + + if (this.triggers['@'+id]) { + for (const trigger of Object.values(this.triggers['@'+id])) { + triggersToExecute.push(trigger); + } + } + + if (this.triggers.all_direct_messages) { + for (const trigger of Object.values(this.triggers.all_direct_messages)) { + triggersToExecute.push(trigger); + } + } + + if (id !== username && this.triggers['@'+username]) { + for (const trigger of Object.values(this.triggers['@'+username])) { + triggersToExecute.push(trigger); + } + } + break; + + case 'c': + if (this.triggers.all_public_channels) { + for (const trigger of Object.values(this.triggers.all_public_channels)) { + triggersToExecute.push(trigger); + } + } + + if (this.triggers['#'+room._id]) { + for (const trigger of Object.values(this.triggers['#'+room._id])) { + triggersToExecute.push(trigger); + } + } + + if (room._id !== room.name && this.triggers['#'+room.name]) { + for (const trigger of Object.values(this.triggers['#'+room.name])) { + triggersToExecute.push(trigger); + } + } + break; + + default: + if (this.triggers.all_private_groups) { + for (const trigger of Object.values(this.triggers.all_private_groups)) { + triggersToExecute.push(trigger); + } + } + + if (this.triggers['#'+room._id]) { + for (const trigger of Object.values(this.triggers['#'+room._id])) { + triggersToExecute.push(trigger); + } + } + + if (room._id !== room.name && this.triggers['#'+room.name]) { + for (const trigger of Object.values(this.triggers['#'+room.name])) { + triggersToExecute.push(trigger); + } + } + break; + } + } else if (this.triggers.__any) { + //For outgoing integration which don't rely on rooms. + for (const trigger of Object.values(this.triggers.__any)) { + triggersToExecute.push(trigger); + } + } + + logger.outgoing.debug(`Found ${triggersToExecute.length} to iterate over and see if the match the event.`); + + for (const triggerToExecute of triggersToExecute) { + logger.outgoing.debug(`Is ${triggerToExecute.name} enabled, ${triggerToExecute.enabled}, and what is the event? ${triggerToExecute.event}`); + if (triggerToExecute.enabled === true && triggerToExecute.event === event) { + this.executeTrigger(triggerToExecute, argObject); + } + } + } + + executeTrigger(trigger, argObject) { + for (const url of trigger.urls) { + this.executeTriggerUrl(url, trigger, argObject, 0); + } + } + + executeTriggerUrl(url, trigger, { event, message, room, owner, user }, theHistoryId, tries = 0) { + logger.outgoing.debug(`Starting to execute trigger: ${trigger.name} (${trigger._id})`); + + let word; + //Not all triggers/events support triggerWords + if (RocketChat.integrations.outgoingEvents[event].use.triggerWords) { + if (trigger.triggerWords && trigger.triggerWords.length > 0) { + for (const triggerWord of trigger.triggerWords) { + if (!trigger.triggerWordAnywhere && message.msg.indexOf(triggerWord) === 0) { + word = triggerWord; + break; + } else if (trigger.triggerWordAnywhere && message.msg.includes(triggerWord)) { + word = triggerWord; + break; + } + } + + // Stop if there are triggerWords but none match + if (!word) { + return; + } + } + } + + const historyId = this.updateHistory({ step: 'start-execute-trigger-url', integration: trigger, event }); + + const data = { + token: trigger.token, + bot: false + }; + + if (word) { + data.trigger_word = word; + } + + this.mapEventArgsToData(data, { trigger, event, message, room, owner, user }); + this.updateHistory({ historyId, step: 'mapped-args-to-data', data, triggerWord: word }); + + logger.outgoing.info(`Will be executing the Integration "${trigger.name}" to the url: ${url}`); + logger.outgoing.debug(data); + + let opts = { + params: {}, + method: 'POST', + url, + data, + auth: undefined, + 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' + } + }; + + if (this.hasScriptAndMethod(trigger, 'prepare_outgoing_request')) { + opts = this.executeScript(trigger, 'prepare_outgoing_request', { request: opts }, historyId); + } + + this.updateHistory({ historyId, step: 'after-maybe-ran-prepare', ranPrepareScript: true }); + + if (!opts) { + this.updateHistory({ historyId, step: 'after-prepare-no-opts', finished: true }); + return; + } + + if (opts.message) { + const prepareMessage = this.sendMessage({ trigger, room, message: opts.message, data }); + this.updateHistory({ historyId, step: 'after-prepare-send-message', prepareSentMessage: prepareMessage }); + } + + if (!opts.url || !opts.method) { + this.updateHistory({ historyId, step: 'after-prepare-no-url_or_method', finished: true }); + return; + } + + this.updateHistory({ historyId, step: 'pre-http-call', url: opts.url, httpCallData: opts.data }); + HTTP.call(opts.method, opts.url, opts, (error, result) => { + if (!result) { + logger.outgoing.warn(`Result for the Integration ${trigger.name} to ${url} is empty`); + } else { + logger.outgoing.info(`Status code for the Integration ${trigger.name} to ${url} is ${result.statusCode}`); + } + + this.updateHistory({ historyId, step: 'after-http-call', httpError: error, httpResult: result }); + + if (this.hasScriptAndMethod(trigger, 'process_outgoing_response')) { + const sandbox = { + request: opts, + response: { + error, + status_code: result ? result.statusCode : undefined, //These values will be undefined to close issues #4175, #5762, and #5896 + content: result ? result.data : undefined, + content_raw: result ? result.content : undefined, + headers: result ? result.headers : {} + } + }; + + const scriptResult = this.executeScript(trigger, 'process_outgoing_response', sandbox, historyId); + + if (scriptResult && scriptResult.content) { + const resultMessage = this.sendMessage({ trigger, room, message: scriptResult.content, data }); + this.updateHistory({ historyId, step: 'after-process-send-message', processSentMessage: resultMessage, finished: true }); + return; + } + + if (scriptResult === false) { + this.updateHistory({ historyId, step: 'after-process-false-result', finished: true }); + return; + } + } + + // if the result contained nothing or wasn't a successful statusCode + if (!result || !this.successResults.includes(result.statusCode)) { + if (error) { + logger.outgoing.error(`Error for the Integration "${trigger.name}" to ${url} is:`); + logger.outgoing.error(error); + } + + if (result) { + logger.outgoing.error(`Error for the Integration "${trigger.name}" to ${url} is:`); + logger.outgoing.error(result); + + if (result.statusCode === 410) { + this.updateHistory({ historyId, step: 'after-process-http-status-410', error: true }); + logger.outgoing.error(`Disabling the Integration "${trigger.name}" because the status code was 401 (Gone).`); + RocketChat.models.Integrations.update({ _id: trigger._id }, { $set: { enabled: false }}); + return; + } + + if (result.statusCode === 500) { + this.updateHistory({ historyId, step: 'after-process-http-status-500', error: true }); + logger.outgoing.error(`Error "500" for the Integration "${trigger.name}" to ${url}.`); + logger.outgoing.error(result.content); + return; + } + } + + if (trigger.retryFailedCalls) { + if (tries < trigger.retryCount && trigger.retryDelay) { + this.updateHistory({ historyId, error: true, step: `going-to-retry-${tries + 1}` }); + + let waitTime; + + switch (trigger.retryDelay) { + case 'powers-of-ten': + // Try again in 0.1s, 1s, 10s, 1m40s, 16m40s, 2h46m40s, 27h46m40s, etc + waitTime = Math.pow(10, tries + 2); + break; + case 'powers-of-two': + // 2 seconds, 4 seconds, 8 seconds + waitTime = Math.pow(2, tries + 1) * 1000; + break; + case 'increments-of-two': + // 2 second, 4 seconds, 6 seconds, etc + waitTime = (tries + 1) * 2 * 1000; + break; + default: + const er = new Error('The integration\'s retryDelay setting is invalid.'); + this.updateHistory({ historyId, step: 'failed-and-retry-delay-is-invalid', error: true, errorStack: er.stack }); + return; + } + + logger.outgoing.info(`Trying the Integration ${trigger.name} to ${url} again in ${waitTime} milliseconds.`); + Meteor.setTimeout(() => { + this.executeTriggerUrl(url, trigger, { event, message, room, owner, user }, historyId, tries + 1); + }, waitTime); + } else { + this.updateHistory({ historyId, step: 'too-many-retries', error: true }); + } + } else { + this.updateHistory({ historyId, step: 'failed-and-not-configured-to-retry', error: true }); + } + + return; + } + + //process outgoing webhook response as a new message + if (result && this.successResults.includes(result.statusCode)) { + if (result && result.data && (result.data.text || result.data.attachments)) { + const resultMsg = this.sendMessage({ trigger, room, message: result.data, data }); + this.updateHistory({ historyId, step: 'url-response-sent-message', resultMessage: resultMsg, finished: true }); + } + } + }); + } + + replay(integration, history) { + if (!integration || integration.type !== 'webhook-outgoing') { + throw new Meteor.Error('integration-type-must-be-outgoing', 'The integration type to replay must be an outgoing webhook.'); + } + + if (!history || !history.data) { + throw new Meteor.Error('history-data-must-be-defined', 'The history data must be defined to replay an integration.'); + } + + const event = history.event; + const message = RocketChat.models.Messages.findOneById(history.data.message_id); + const room = RocketChat.models.Rooms.findOneById(history.data.channel_id); + const user = RocketChat.models.Users.findOneById(history.data.user_id); + let owner; + + if (history.data.owner && history.data.owner._id) { + owner = RocketChat.models.Users.findOneById(history.data.owner._id); + } + + this.executeTriggerUrl(history.url, integration, { event, message, room, owner, user }); + } +}; diff --git a/packages/rocketchat-integrations/server/lib/validation.coffee b/packages/rocketchat-integrations/server/lib/validation.coffee deleted file mode 100644 index f41fd125deda..000000000000 --- a/packages/rocketchat-integrations/server/lib/validation.coffee +++ /dev/null @@ -1,93 +0,0 @@ -RocketChat.integrations.validateOutgoing = (integration, userId) -> - if integration.channel?.trim? and integration.channel.trim() is '' - delete integration.channel - - if integration.username.trim() is '' - throw new Meteor.Error 'error-invalid-username', 'Invalid username', { method: 'addOutgoingIntegration' } - - if not Match.test integration.urls, [String] - throw new Meteor.Error 'error-invalid-urls', 'Invalid URLs', { method: 'addOutgoingIntegration' } - - 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 'error-invalid-urls', 'Invalid URLs', { method: 'addOutgoingIntegration' } - - if not Match.test integration.channel, String - throw new Meteor.Error 'error-invalid-channel', 'Invalid Channel', { method: 'addOutgoingIntegration' } - - channels = _.map(integration.channel.split(','), (channel) -> s.trim(channel)) - - scopedChannels = ['all_public_channels', 'all_private_groups', 'all_direct_messages'] - for channel in channels - if channel[0] not in ['@', '#'] and channel not in scopedChannels - throw new Meteor.Error 'error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', { method: 'updateIncomingIntegration' } - - if integration.triggerWords? - if not Match.test integration.triggerWords, [String] - throw new Meteor.Error 'error-invalid-triggerWords', 'Invalid triggerWords', { method: 'addOutgoingIntegration' } - - for triggerWord, index in integration.triggerWords - delete integration.triggerWords[index] if triggerWord.trim() is '' - - integration.triggerWords = _.without integration.triggerWords, [undefined] - - if integration.scriptEnabled is true and integration.script? and integration.script.trim() isnt '' - try - babelOptions = Babel.getDefaultOptions({ runtime: false }) - babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }) - - integration.scriptCompiled = Babel.compile(integration.script, babelOptions).code - integration.scriptError = undefined - catch e - integration.scriptCompiled = undefined - integration.scriptError = _.pick e, 'name', 'message', 'stack' - - - for channel in channels - if channel in scopedChannels - if channel is 'all_public_channels' - #No special permissions needed to add integration to public channels - else if not RocketChat.authz.hasPermission userId, 'manage-integrations' - throw new Meteor.Error 'error-invalid-channel', 'Invalid Channel', { method: 'addOutgoingIntegration' } - else - record = undefined - channelType = channel[0] - channel = 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 'error-invalid-room', 'Invalid room', { method: 'addOutgoingIntegration' } - - if record.usernames? and - (not RocketChat.authz.hasPermission userId, 'manage-integrations') and - (RocketChat.authz.hasPermission userId, 'manage-own-integrations') and - Meteor.user()?.username not in record.usernames - throw new Meteor.Error 'error-invalid-channel', 'Invalid Channel', { method: 'addOutgoingIntegration' } - - user = RocketChat.models.Users.findOne({username: integration.username}) - - if not user? - throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'addOutgoingIntegration' } - - integration.type = 'webhook-outgoing' - integration.userId = user._id - integration.channel = channels - - return integration diff --git a/packages/rocketchat-integrations/server/lib/validation.js b/packages/rocketchat-integrations/server/lib/validation.js new file mode 100644 index 000000000000..9da2094cb763 --- /dev/null +++ b/packages/rocketchat-integrations/server/lib/validation.js @@ -0,0 +1,155 @@ +/* global Babel */ +const scopedChannels = ['all_public_channels', 'all_private_groups', 'all_direct_messages']; +const validChannelChars = ['@', '#']; + +function _verifyRequiredFields(integration) { + if (!integration.event || !Match.test(integration.event, String) || integration.event.trim() === '' || !RocketChat.integrations.outgoingEvents[integration.event]) { + throw new Meteor.Error('error-invalid-event-type', 'Invalid event type', { function: 'validateOutgoing._verifyRequiredFields' }); + } + + if (!integration.username || !Match.test(integration.username, String) || integration.username.trim() === '') { + throw new Meteor.Error('error-invalid-username', 'Invalid username', { function: 'validateOutgoing._verifyRequiredFields' }); + } + + if (RocketChat.integrations.outgoingEvents[integration.event].use.targetRoom && !integration.targetRoom) { + throw new Meteor.Error('error-invalid-targetRoom', 'Invalid Target Room', { function: 'validateOutgoing._verifyRequiredFields' }); + } + + if (!Match.test(integration.urls, [String])) { + throw new Meteor.Error('error-invalid-urls', 'Invalid URLs', { function: 'validateOutgoing._verifyRequiredFields' }); + } + + for (const [index, url] of integration.urls.entries()) { + if (url.trim() === '') { + delete integration.urls[index]; + } + } + + integration.urls = _.without(integration.urls, [undefined]); + + if (integration.urls.length === 0) { + throw new Meteor.Error('error-invalid-urls', 'Invalid URLs', { function: 'validateOutgoing._verifyRequiredFields' }); + } +} + +function _verifyUserHasPermissionForChannels(integration, userId, channels) { + for (let channel of channels) { + if (scopedChannels.includes(channel)) { + if (channel === 'all_public_channels') { + // No special permissions needed to add integration to public channels + } else if (!RocketChat.authz.hasPermission(userId, 'manage-integrations')) { + throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', { function: 'validateOutgoing._verifyUserHasPermissionForChannels' }); + } + } else { + let record; + const channelType = channel[0]; + channel = channel.substr(1); + + switch (channelType) { + case '#': + record = RocketChat.models.Rooms.findOne({ + $or: [ + {_id: channel}, + {name: channel} + ] + }); + break; + case '@': + record = RocketChat.models.Users.findOne({ + $or: [ + {_id: channel}, + {username: channel} + ] + }); + break; + } + + if (!record) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { function: 'validateOutgoing._verifyUserHasPermissionForChannels' }); + } + + if (record.usernames && !RocketChat.authz.hasPermission(userId, 'manage-integrations') && RocketChat.authz.hasPermission(userId, 'manage-own-integrations') && !record.usernames.includes(Meteor.user().username)) { + throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', { function: 'validateOutgoing._verifyUserHasPermissionForChannels' }); + } + } + } +} + +function _verifyRetryInformation(integration) { + if (!integration.retryFailedCalls) { + return; + } + + // Don't allow negative retry counts + integration.retryCount = integration.retryCount && parseInt(integration.retryCount) > 0 ? parseInt(integration.retryCount) : 4; + integration.retryDelay = !integration.retryDelay || !integration.retryDelay.trim() ? 'powers-of-ten' : integration.retryDelay.toLowerCase(); +} + +RocketChat.integrations.validateOutgoing = function _validateOutgoing(integration, userId) { + if (integration.channel && Match.test(integration.channel, String) && integration.channel.trim() === '') { + delete integration.channel; + } + + //Moved to it's own function to statisfy the complexity rule + _verifyRequiredFields(integration); + + let channels = []; + if (RocketChat.integrations.outgoingEvents[integration.event].use.channel) { + if (!Match.test(integration.channel, String)) { + throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', { function: 'validateOutgoing' }); + } else { + channels = _.map(integration.channel.split(','), (channel) => s.trim(channel)); + + for (const channel of channels) { + if (!validChannelChars.includes(channel[0]) && !scopedChannels.includes(channel.toLowerCase())) { + throw new Meteor.Error('error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', { function: 'validateOutgoing' }); + } + } + } + } else if (!RocketChat.authz.hasPermission(userId, 'manage-integrations')) { + throw new Meteor.Error('error-invalid-permissions', 'Invalid permission for required Integration creation.', { function: 'validateOutgoing' }); + } + + if (RocketChat.integrations.outgoingEvents[integration.event].use.triggerWords && integration.triggerWords) { + if (!Match.test(integration.triggerWords, [String])) { + throw new Meteor.Error('error-invalid-triggerWords', 'Invalid triggerWords', { function: 'validateOutgoing' }); + } + + for (const [index, triggerWord] of integration.triggerWords) { + if (triggerWord.trim() === '') { + delete integration.triggerWords[index]; + } + } + + integration.triggerWords = _.without(integration.triggerWords, [undefined]); + } else { + delete integration.triggerWords; + } + + if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { + try { + const babelOptions = Object.assign(Babel.getDefaultOptions({ runtime: false }), { compact: true, minified: true, comments: false }); + + integration.scriptCompiled = Babel.compile(integration.script, babelOptions).code; + integration.scriptError = undefined; + } catch (e) { + integration.scriptCompiled = undefined; + integration.scriptError = _.pick(e, 'name', 'message', 'stack'); + } + } + + _verifyUserHasPermissionForChannels(integration, userId, channels); + _verifyRetryInformation(integration); + + const user = RocketChat.models.Users.findOne({ username: integration.username }); + + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'validateOutgoing' }); + } + + integration.type = 'webhook-outgoing'; + integration.userId = user._id; + integration.channel = channels; + + return integration; +}; diff --git a/packages/rocketchat-integrations/server/methods/clearIntegrationHistory.js b/packages/rocketchat-integrations/server/methods/clearIntegrationHistory.js new file mode 100644 index 000000000000..4d4d2c7ca122 --- /dev/null +++ b/packages/rocketchat-integrations/server/methods/clearIntegrationHistory.js @@ -0,0 +1,21 @@ +Meteor.methods({ + clearIntegrationHistory(integrationId) { + let integration; + + if (RocketChat.authz.hasPermission(this.userId, 'manage-integrations') || RocketChat.authz.hasPermission(this.userId, 'manage-integrations', 'bot')) { + integration = RocketChat.models.Integrations.findOne(integrationId); + } else if (RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations') || RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations', 'bot')) { + integration = RocketChat.models.Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId }}); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'clearIntegrationHistory' }); + } + + if (!integration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'clearIntegrationHistory' }); + } + + RocketChat.models.IntegrationHistory.removeByIntegrationId(integrationId); + + return true; + } +}); diff --git a/packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.coffee b/packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.coffee deleted file mode 100644 index 66335efef3e4..000000000000 --- a/packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.coffee +++ /dev/null @@ -1,81 +0,0 @@ -Meteor.methods - addIncomingIntegration: (integration) -> - if (not RocketChat.authz.hasPermission @userId, 'manage-integrations') and (not RocketChat.authz.hasPermission @userId, 'manage-own-integrations') - throw new Meteor.Error 'not_authorized' - - if not _.isString(integration.channel) - throw new Meteor.Error 'error-invalid-channel', 'Invalid channel', { method: 'addIncomingIntegration' } - - if integration.channel.trim() is '' - throw new Meteor.Error 'error-invalid-channel', 'Invalid channel', { method: 'addIncomingIntegration' } - - channels = _.map(integration.channel.split(','), (channel) -> s.trim(channel)) - - for channel in channels - if channel[0] not in ['@', '#'] - throw new Meteor.Error 'error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', { method: 'updateIncomingIntegration' } - - if not _.isString(integration.username) - throw new Meteor.Error 'error-invalid-username', 'Invalid username', { method: 'addIncomingIntegration' } - - if integration.username.trim() is '' - throw new Meteor.Error 'error-invalid-username', 'Invalid username', { method: 'addIncomingIntegration' } - - if integration.scriptEnabled is true and integration.script? and integration.script.trim() isnt '' - try - babelOptions = Babel.getDefaultOptions({ runtime: false }) - babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }) - - integration.scriptCompiled = Babel.compile(integration.script, babelOptions).code - integration.scriptError = undefined - catch e - integration.scriptCompiled = undefined - integration.scriptError = _.pick e, 'name', 'message', 'stack' - - for channel in channels - record = undefined - channelType = channel[0] - channel = 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 'error-invalid-room', 'Invalid room', { method: 'addIncomingIntegration' } - - if record.usernames? and - (not RocketChat.authz.hasPermission @userId, 'manage-integrations') and - (RocketChat.authz.hasPermission @userId, 'manage-own-integrations') and - Meteor.user()?.username not in record.usernames - throw new Meteor.Error 'error-invalid-channel', 'Invalid Channel', { method: 'addIncomingIntegration' } - - user = RocketChat.models.Users.findOne({username: integration.username}) - - if not user? - throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'addIncomingIntegration' } - - token = Random.id(48) - - integration.type = 'webhook-incoming' - integration.token = token - integration.channel = channels - integration.userId = user._id - integration._createdAt = new Date - integration._createdBy = RocketChat.models.Users.findOne @userId, {fields: {username: 1}} - - RocketChat.models.Roles.addUserRoles user._id, 'bot' - - integration._id = RocketChat.models.Integrations.insert integration - - return integration diff --git a/packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.js b/packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.js new file mode 100644 index 000000000000..12f2828bd608 --- /dev/null +++ b/packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.js @@ -0,0 +1,97 @@ +/* global Babel */ +const validChannelChars = ['@', '#']; + +Meteor.methods({ + addIncomingIntegration(integration) { + if (!RocketChat.authz.hasPermission(this.userId, 'manage-integrations') && !RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations')) { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'addIncomingIntegration' }); + } + + if (!_.isString(integration.channel)) { + throw new Meteor.Error('error-invalid-channel', 'Invalid channel', { method: 'addIncomingIntegration' }); + } + + if (integration.channel.trim() === '') { + throw new Meteor.Error('error-invalid-channel', 'Invalid channel', { method: 'addIncomingIntegration' }); + } + + const channels = _.map(integration.channel.split(','), (channel) => s.trim(channel)); + + for (const channel of channels) { + if (!validChannelChars.includes(channel[0])) { + throw new Meteor.Error('error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', { method: 'updateIncomingIntegration' }); + } + } + + if (!_.isString(integration.username) || integration.username.trim() === '') { + throw new Meteor.Error('error-invalid-username', 'Invalid username', { method: 'addIncomingIntegration' }); + } + + if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { + try { + let babelOptions = Babel.getDefaultOptions({ runtime: false }); + babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }); + + integration.scriptCompiled = Babel.compile(integration.script, babelOptions).code; + integration.scriptError = undefined; + } catch (e) { + integration.scriptCompiled = undefined; + integration.scriptError = _.pick(e, 'name', 'message', 'stack'); + } + } + + for (let channel of channels) { + let record; + const channelType = channel[0]; + channel = channel.substr(1); + + switch (channelType) { + case '#': + record = RocketChat.models.Rooms.findOne({ + $or: [ + {_id: channel}, + {name: channel} + ] + }); + break; + case '@': + record = RocketChat.models.Users.findOne({ + $or: [ + {_id: channel}, + {username: channel} + ] + }); + break; + } + + if (!record) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'addIncomingIntegration' }); + } + + if (record.usernames && !RocketChat.authz.hasPermission(this.userId, 'manage-integrations') && RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations') && !record.usernames.includes(Meteor.user().username)) { + throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', { method: 'addIncomingIntegration' }); + } + } + + const user = RocketChat.models.Users.findOne({username: integration.username}); + + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addIncomingIntegration' }); + } + + const token = Random.id(48); + + integration.type = 'webhook-incoming'; + integration.token = token; + integration.channel = channels; + integration.userId = user._id; + integration._createdAt = new Date(); + integration._createdBy = RocketChat.models.Users.findOne(this.userId, {fields: {username: 1}}); + + RocketChat.models.Roles.addUserRoles(user._id, 'bot'); + + integration._id = RocketChat.models.Integrations.insert(integration); + + return integration; + } +}); diff --git a/packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.coffee b/packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.coffee deleted file mode 100644 index fe7e259f8d0e..000000000000 --- a/packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Meteor.methods - deleteIncomingIntegration: (integrationId) -> - integration = null - - if RocketChat.authz.hasPermission @userId, 'manage-integrations' - integration = RocketChat.models.Integrations.findOne(integrationId) - else if RocketChat.authz.hasPermission @userId, 'manage-own-integrations' - integration = RocketChat.models.Integrations.findOne(integrationId, { fields : {"_createdBy._id": @userId} }) - else - throw new Meteor.Error 'not_authorized' - - if not integration? - throw new Meteor.Error 'error-invalid-integration', 'Invalid integration', { method: 'deleteIncomingIntegration' } - - RocketChat.models.Integrations.remove _id: integrationId - - return true diff --git a/packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.js b/packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.js new file mode 100644 index 000000000000..d542f75ec318 --- /dev/null +++ b/packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.js @@ -0,0 +1,21 @@ +Meteor.methods({ + deleteIncomingIntegration(integrationId) { + let integration; + + if (RocketChat.authz.hasPermission(this.userId, 'manage-integrations')) { + integration = RocketChat.models.Integrations.findOne(integrationId); + } else if (RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations')) { + integration = RocketChat.models.Integrations.findOne(integrationId, { fields : { '_createdBy._id': this.userId }}); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteIncomingIntegration' }); + } + + if (!integration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteIncomingIntegration' }); + } + + RocketChat.models.Integrations.remove({ _id: integrationId }); + + return true; + } +}); diff --git a/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.coffee b/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.coffee deleted file mode 100644 index ce0db5d79dfc..000000000000 --- a/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.coffee +++ /dev/null @@ -1,84 +0,0 @@ -Meteor.methods - updateIncomingIntegration: (integrationId, integration) -> - if not _.isString(integration.channel) - throw new Meteor.Error 'error-invalid-channel', 'Invalid channel', { method: 'updateIncomingIntegration' } - - if integration.channel.trim() is '' - throw new Meteor.Error 'error-invalid-channel', 'Invalid channel', { method: 'updateIncomingIntegration' } - - channels = _.map(integration.channel.split(','), (channel) -> s.trim(channel)) - - for channel in channels - if channel[0] not in ['@', '#'] - throw new Meteor.Error 'error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', { method: 'updateIncomingIntegration' } - - currentIntegration = null - - if RocketChat.authz.hasPermission @userId, 'manage-integrations' - currentIntegration = RocketChat.models.Integrations.findOne(integrationId) - else if RocketChat.authz.hasPermission @userId, 'manage-own-integrations' - currentIntegration = RocketChat.models.Integrations.findOne({"_id": integrationId, "_createdBy._id": @userId}) - else - throw new Meteor.Error 'not_authorized' - - if not currentIntegration? - throw new Meteor.Error 'error-invalid-integration', 'Invalid integration', { method: 'updateIncomingIntegration' } - - if integration.scriptEnabled is true and integration.script? and integration.script.trim() isnt '' - try - babelOptions = Babel.getDefaultOptions({ runtime: false }) - babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }) - - integration.scriptCompiled = Babel.compile(integration.script, babelOptions).code - integration.scriptError = undefined - catch e - integration.scriptCompiled = undefined - integration.scriptError = _.pick e, 'name', 'message', 'stack' - - for channel in channels - record = undefined - channelType = channel[0] - channel = 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 'error-invalid-room', 'Invalid room', { method: 'updateIncomingIntegration' } - - if record.usernames? and - (not RocketChat.authz.hasPermission @userId, 'manage-integrations') and - (RocketChat.authz.hasPermission @userId, 'manage-own-integrations') and - Meteor.user()?.username not in record.usernames - throw new Meteor.Error 'error-invalid-channel', 'Invalid Channel', { method: 'updateIncomingIntegration' } - - user = RocketChat.models.Users.findOne({username: currentIntegration.username}) - RocketChat.models.Roles.addUserRoles user._id, 'bot' - - RocketChat.models.Integrations.update integrationId, - $set: - enabled: integration.enabled - name: integration.name - avatar: integration.avatar - emoji: integration.emoji - alias: integration.alias - channel: channels - script: integration.script - scriptEnabled: integration.scriptEnabled - scriptCompiled: integration.scriptCompiled - scriptError: integration.scriptError - _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/methods/incoming/updateIncomingIntegration.js b/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js new file mode 100644 index 000000000000..9a4c4a878e4a --- /dev/null +++ b/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js @@ -0,0 +1,100 @@ +/* global Babel */ +const validChannelChars = ['@', '#']; + +Meteor.methods({ + updateIncomingIntegration(integrationId, integration) { + if (!_.isString(integration.channel) || integration.channel.trim() === '') { + throw new Meteor.Error('error-invalid-channel', 'Invalid channel', { method: 'updateIncomingIntegration' }); + } + + const channels = _.map(integration.channel.split(','), (channel) => s.trim(channel)); + + for (const channel of channels) { + if (!validChannelChars.includes(channel[0])) { + throw new Meteor.Error('error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', { method: 'updateIncomingIntegration' }); + } + } + + let currentIntegration; + + if (RocketChat.authz.hasPermission(this.userId, 'manage-integrations')) { + currentIntegration = RocketChat.models.Integrations.findOne(integrationId); + } else if (RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations')) { + currentIntegration = RocketChat.models.Integrations.findOne({ _id: integrationId, '_createdBy._id': this.userId }); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'updateIncomingIntegration' }); + } + + if (!currentIntegration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'updateIncomingIntegration' }); + } + + if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { + try { + let babelOptions = Babel.getDefaultOptions({ runtime: false }); + babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }); + + integration.scriptCompiled = Babel.compile(integration.script, babelOptions).code; + integration.scriptError = undefined; + } catch (e) { + integration.scriptCompiled = undefined; + integration.scriptError = _.pick(e, 'name', 'message', 'stack'); + } + } + + for (let channel of channels) { + const channelType = channel[0]; + channel = channel.substr(1); + let record; + + switch (channelType) { + case '#': + record = RocketChat.models.Rooms.findOne({ + $or: [ + {_id: channel}, + {name: channel} + ] + }); + break; + case '@': + record = RocketChat.models.Users.findOne({ + $or: [ + {_id: channel}, + {username: channel} + ] + }); + break; + } + + if (!record) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'updateIncomingIntegration' }); + } + + if (record.usernames && !RocketChat.authz.hasPermission(this.userId, 'manage-integrations') && RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations') && !record.usernames.includes(Meteor.user().username)) { + throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', { method: 'updateIncomingIntegration' }); + } + } + + const user = RocketChat.models.Users.findOne({ username: currentIntegration.username }); + RocketChat.models.Roles.addUserRoles(user._id, 'bot'); + + RocketChat.models.Integrations.update(integrationId, { + $set: { + enabled: integration.enabled, + name: integration.name, + avatar: integration.avatar, + emoji: integration.emoji, + alias: integration.alias, + channel: channels, + script: integration.script, + scriptEnabled: integration.scriptEnabled, + scriptCompiled: integration.scriptCompiled, + scriptError: integration.scriptError, + _updatedAt: new Date(), + _updatedBy: RocketChat.models.Users.findOne(this.userId, {fields: {username: 1}}) + } + }); + + return RocketChat.models.Integrations.findOne(integrationId); + } +}); diff --git a/packages/rocketchat-integrations/server/methods/outgoing/addOutgoingIntegration.coffee b/packages/rocketchat-integrations/server/methods/outgoing/addOutgoingIntegration.coffee deleted file mode 100644 index af42d9bb2915..000000000000 --- a/packages/rocketchat-integrations/server/methods/outgoing/addOutgoingIntegration.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Meteor.methods - addOutgoingIntegration: (integration) -> - - if (not RocketChat.authz.hasPermission @userId, 'manage-integrations') and - not (RocketChat.authz.hasPermission @userId, 'manage-own-integrations') and - not (RocketChat.authz.hasPermission @userId, 'manage-integrations', 'bot') and - not (RocketChat.authz.hasPermission @userId, 'manage-own-integrations', 'bot') - throw new Meteor.Error 'not_authorized' - - integration = RocketChat.integrations.validateOutgoing(integration, @userId) - - 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/addOutgoingIntegration.js b/packages/rocketchat-integrations/server/methods/outgoing/addOutgoingIntegration.js new file mode 100644 index 000000000000..7166b4ed025e --- /dev/null +++ b/packages/rocketchat-integrations/server/methods/outgoing/addOutgoingIntegration.js @@ -0,0 +1,18 @@ +Meteor.methods({ + addOutgoingIntegration(integration) { + if (!RocketChat.authz.hasPermission(this.userId, 'manage-integrations') + && !RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations') + && !RocketChat.authz.hasPermission(this.userId, 'manage-integrations', 'bot') + && !RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations', 'bot')) { + throw new Meteor.Error('not_authorized'); + } + + integration = RocketChat.integrations.validateOutgoing(integration, this.userId); + + integration._createdAt = new Date(); + integration._createdBy = RocketChat.models.Users.findOne(this.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 deleted file mode 100644 index 9aed8ec0db4c..000000000000 --- a/packages/rocketchat-integrations/server/methods/outgoing/deleteOutgoingIntegration.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Meteor.methods - deleteOutgoingIntegration: (integrationId) -> - integration = null - - if RocketChat.authz.hasPermission(@userId, 'manage-integrations') or RocketChat.authz.hasPermission(@userId, 'manage-integrations', 'bot') - integration = RocketChat.models.Integrations.findOne(integrationId) - else if RocketChat.authz.hasPermission(@userId, 'manage-own-integrations') or RocketChat.authz.hasPermission(@userId, 'manage-own-integrations', 'bot') - integration = RocketChat.models.Integrations.findOne(integrationId, { fields : {"_createdBy._id": @userId} }) - else - throw new Meteor.Error 'not_authorized' - - if not integration? - throw new Meteor.Error 'error-invalid-integration', 'Invalid integration', { method: 'deleteOutgoingIntegration' } - - RocketChat.models.Integrations.remove _id: integrationId - - return true diff --git a/packages/rocketchat-integrations/server/methods/outgoing/deleteOutgoingIntegration.js b/packages/rocketchat-integrations/server/methods/outgoing/deleteOutgoingIntegration.js new file mode 100644 index 000000000000..da6a9697ef9b --- /dev/null +++ b/packages/rocketchat-integrations/server/methods/outgoing/deleteOutgoingIntegration.js @@ -0,0 +1,22 @@ +Meteor.methods({ + deleteOutgoingIntegration(integrationId) { + let integration; + + if (RocketChat.authz.hasPermission(this.userId, 'manage-integrations') || RocketChat.authz.hasPermission(this.userId, 'manage-integrations', 'bot')) { + integration = RocketChat.models.Integrations.findOne(integrationId); + } else if (RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations') || RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations', 'bot')) { + integration = RocketChat.models.Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId }}); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteOutgoingIntegration' }); + } + + if (!integration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteOutgoingIntegration' }); + } + + RocketChat.models.Integrations.remove({ _id: integrationId }); + RocketChat.models.IntegrationHistory.removeByIntegrationId(integrationId); + + return true; + } +}); diff --git a/packages/rocketchat-integrations/server/methods/outgoing/replayOutgoingIntegration.js b/packages/rocketchat-integrations/server/methods/outgoing/replayOutgoingIntegration.js new file mode 100644 index 000000000000..3213ebec012d --- /dev/null +++ b/packages/rocketchat-integrations/server/methods/outgoing/replayOutgoingIntegration.js @@ -0,0 +1,27 @@ +Meteor.methods({ + replayOutgoingIntegration({ integrationId, historyId }) { + let integration; + + if (RocketChat.authz.hasPermission(this.userId, 'manage-integrations') || RocketChat.authz.hasPermission(this.userId, 'manage-integrations', 'bot')) { + integration = RocketChat.models.Integrations.findOne(integrationId); + } else if (RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations') || RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations', 'bot')) { + integration = RocketChat.models.Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId }}); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'replayOutgoingIntegration' }); + } + + if (!integration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'replayOutgoingIntegration' }); + } + + const history = RocketChat.models.IntegrationHistory.findOneByIntegrationIdAndHistoryId(integration._id, historyId); + + if (!history) { + throw new Meteor.Error('error-invalid-integration-history', 'Invalid Integration History', { method: 'replayOutgoingIntegration' }); + } + + RocketChat.integrations.triggerHandler.replay(integration, history); + + return true; + } +}); diff --git a/packages/rocketchat-integrations/server/methods/outgoing/updateOutgoingIntegration.coffee b/packages/rocketchat-integrations/server/methods/outgoing/updateOutgoingIntegration.coffee deleted file mode 100644 index 6761c3b1c682..000000000000 --- a/packages/rocketchat-integrations/server/methods/outgoing/updateOutgoingIntegration.coffee +++ /dev/null @@ -1,42 +0,0 @@ -Meteor.methods - updateOutgoingIntegration: (integrationId, integration) -> - integration = RocketChat.integrations.validateOutgoing(integration, @userId) - - if not integration.token? or integration.token?.trim() is '' - throw new Meteor.Error 'error-invalid-token', 'Invalid token', { method: 'updateOutgoingIntegration' } - - currentIntegration = null - - if RocketChat.authz.hasPermission @userId, 'manage-integrations' - currentIntegration = RocketChat.models.Integrations.findOne(integrationId) - else if RocketChat.authz.hasPermission @userId, 'manage-own-integrations' - currentIntegration = RocketChat.models.Integrations.findOne({"_id": integrationId, "_createdBy._id": @userId}) - else - throw new Meteor.Error 'not_authorized' - - if not currentIntegration? - throw new Meteor.Error 'invalid_integration', '[methods] updateOutgoingIntegration -> integration not found' - - - RocketChat.models.Integrations.update integrationId, - $set: - enabled: integration.enabled - name: integration.name - avatar: integration.avatar - emoji: integration.emoji - alias: integration.alias - channel: integration.channel - impersonateUser: integration.impersonateUser - username: integration.username - userId: integration.userId - urls: integration.urls - token: integration.token - script: integration.script - scriptEnabled: integration.scriptEnabled - scriptCompiled: integration.scriptCompiled - scriptError: integration.scriptError - 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/methods/outgoing/updateOutgoingIntegration.js b/packages/rocketchat-integrations/server/methods/outgoing/updateOutgoingIntegration.js new file mode 100644 index 000000000000..aaf1f3376bc3 --- /dev/null +++ b/packages/rocketchat-integrations/server/methods/outgoing/updateOutgoingIntegration.js @@ -0,0 +1,54 @@ +Meteor.methods({ + updateOutgoingIntegration(integrationId, integration) { + integration = RocketChat.integrations.validateOutgoing(integration, this.userId); + + if (!integration.token || integration.token.trim() === '') { + throw new Meteor.Error('error-invalid-token', 'Invalid token', { method: 'updateOutgoingIntegration' }); + } + + let currentIntegration; + + if (RocketChat.authz.hasPermission(this.userId, 'manage-integrations')) { + currentIntegration = RocketChat.models.Integrations.findOne(integrationId); + } else if (RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations')) { + currentIntegration = RocketChat.models.Integrations.findOne({ _id: integrationId, '_createdBy._id': this.userId }); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'updateOutgoingIntegration' }); + } + + if (!currentIntegration) { + throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found'); + } + + RocketChat.models.Integrations.update(integrationId, { + $set: { + event: integration.event, + enabled: integration.enabled, + name: integration.name, + avatar: integration.avatar, + emoji: integration.emoji, + alias: integration.alias, + channel: integration.channel, + targetRoom: integration.targetRoom, + impersonateUser: integration.impersonateUser, + username: integration.username, + userId: integration.userId, + urls: integration.urls, + token: integration.token, + script: integration.script, + scriptEnabled: integration.scriptEnabled, + scriptCompiled: integration.scriptCompiled, + scriptError: integration.scriptError, + triggerWords: integration.triggerWords, + retryFailedCalls: integration.retryFailedCalls, + retryCount: integration.retryCount, + retryDelay: integration.retryDelay, + triggerWordAnywhere: integration.triggerWordAnywhere, + _updatedAt: new Date(), + _updatedBy: RocketChat.models.Users.findOne(this.userId, {fields: {username: 1}}) + } + }); + + return RocketChat.models.Integrations.findOne(integrationId); + } +}); diff --git a/packages/rocketchat-integrations/server/models/IntegrationHistory.js b/packages/rocketchat-integrations/server/models/IntegrationHistory.js new file mode 100644 index 000000000000..004d2cc9edb7 --- /dev/null +++ b/packages/rocketchat-integrations/server/models/IntegrationHistory.js @@ -0,0 +1,37 @@ +RocketChat.models.IntegrationHistory = new class IntegrationHistory extends RocketChat.models._Base { + constructor() { + super('integration_history'); + } + + findByType(type, options) { + if (type !== 'outgoing-webhook' || type !== 'incoming-webhook') { + throw new Meteor.Error('invalid-integration-type'); + } + + return this.find({ type }, options); + } + + findByIntegrationId(id, options) { + return this.find({ 'integration._id': id }, options); + } + + findByIntegrationIdAndCreatedBy(id, creatorId, options) { + return this.find({ 'integration._id': id, 'integration._createdBy._id': creatorId }, options); + } + + findOneByIntegrationIdAndHistoryId(integrationId, historyId) { + return this.findOne({ 'integration._id': integrationId, _id: historyId }); + } + + findByEventName(event, options) { + return this.find({ event }, options); + } + + findFailed(options) { + return this.find({ error: true }, options); + } + + removeByIntegrationId(integrationId) { + return this.remove({ 'integration._id': integrationId }); + } +}; diff --git a/packages/rocketchat-integrations/server/models/Integrations.coffee b/packages/rocketchat-integrations/server/models/Integrations.coffee deleted file mode 100644 index 21a570244746..000000000000 --- a/packages/rocketchat-integrations/server/models/Integrations.coffee +++ /dev/null @@ -1,13 +0,0 @@ -RocketChat.models.Integrations = new class extends RocketChat.models._Base - constructor: -> - super('integrations') - - - # FIND - # findByRole: (role, options) -> - # query = - # roles: role - - # return @find query, options - - # CREATE diff --git a/packages/rocketchat-integrations/server/models/Integrations.js b/packages/rocketchat-integrations/server/models/Integrations.js new file mode 100644 index 000000000000..3d76d3ef1ce1 --- /dev/null +++ b/packages/rocketchat-integrations/server/models/Integrations.js @@ -0,0 +1,17 @@ +RocketChat.models.Integrations = new class Integrations extends RocketChat.models._Base { + constructor() { + super('integrations'); + } + + findByType(type, options) { + if (type !== 'webhook-incoming' && type !== 'webhook-outgoing') { + throw new Meteor.Error('invalid-type-to-find'); + } + + return this.find({ type }, options); + } + + disableByUserId(userId) { + return this.update({ userId }, { $set: { enabled: false }}, { multi: true }); + } +}; diff --git a/packages/rocketchat-integrations/server/processWebhookMessage.js b/packages/rocketchat-integrations/server/processWebhookMessage.js index 3d3b27f244c4..aa81be89adbb 100644 --- a/packages/rocketchat-integrations/server/processWebhookMessage.js +++ b/packages/rocketchat-integrations/server/processWebhookMessage.js @@ -1,91 +1,31 @@ -function retrieveRoomInfo({ currentUserId, channel, ignoreEmpty=false }) { - const room = RocketChat.models.Rooms.findOneByIdOrName(channel); - if (!_.isObject(room) && !ignoreEmpty) { - throw new Meteor.Error('invalid-channel'); - } - - if (room && room.t === 'c') { - //Check if the user already has a Subscription or not, this avoids this issue: https://github.com/RocketChat/Rocket.Chat/issues/5477 - const sub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(room._id, currentUserId); - - if (!sub) { - Meteor.runAsUser(currentUserId, function() { - return Meteor.call('joinRoom', room._id); - }); - } - } - - return room; -} - -function retrieveDirectMessageInfo({ currentUserId, channel, findByUserIdOnly=false }) { - let roomUser = undefined; - - if (findByUserIdOnly) { - roomUser = RocketChat.models.Users.findOneById(channel); - } else { - roomUser = RocketChat.models.Users.findOne({ - $or: [{ _id: channel }, { username: channel }] - }); - } - - const rid = _.isObject(roomUser) ? [currentUserId, roomUser._id].sort().join('') : channel; - let room = RocketChat.models.Rooms.findOneById(rid); - - if (!_.isObject(room)) { - if (!_.isObject(roomUser)) { - throw new Meteor.Error('invalid-channel'); - } - - room = Meteor.runAsUser(currentUserId, function() { - const {rid} = Meteor.call('createDirectMessage', roomUser.username); - return RocketChat.models.Rooms.findOneById(rid); - }); - } +this.processWebhookMessage = function(messageObj, user, defaultValues = { channel: '', alias: '', avatar: '', emoji: '' }) { + const sentData = []; + const channels = [].concat(messageObj.channel || messageObj.roomId || defaultValues.channel); - return room; -} + for (const channel of channels) { + const channelType = channel[0]; -this.processWebhookMessage = function(messageObj, user, defaultValues) { - var attachment, channel, channels, channelType, i, len, message, ref, room, ret; - ret = []; - - if (!defaultValues) { - defaultValues = { - channel: '', - alias: '', - avatar: '', - emoji: '' - }; - } - - channel = messageObj.channel || messageObj.roomId || defaultValues.channel; - - channels = [].concat(channel); - - for (channel of channels) { - channelType = channel[0]; - - channel = channel.substr(1); + let channelValue = channel.substr(1); + let room; switch (channelType) { case '#': - room = retrieveRoomInfo({ currentUserId: user._id, channel }); + room = RocketChat.getRoomByNameOrIdWithOptionToJoin({ currentUserId: user._id, nameOrId: channelValue, joinChannel: true }); break; case '@': - room = retrieveDirectMessageInfo({ currentUserId: user._id, channel }); + room = RocketChat.getRoomByNameOrIdWithOptionToJoin({ currentUserId: user._id, nameOrId: channelValue, type: 'd' }); break; default: - channel = channelType + channel; + channelValue = channelType + channelValue; //Try to find the room by id or name if they didn't include the prefix. - room = retrieveRoomInfo({ currentUserId: user._id, channel, ignoreEmpty: true }); + room = RocketChat.getRoomByNameOrIdWithOptionToJoin({ currentUserId: user._id, nameOrId: channelValue, joinChannel: true, errorOnEmpty: false }); if (room) { break; } //We didn't get a room, let's try finding direct messages - room = retrieveDirectMessageInfo({ currentUserId: user._id, channel, findByUserIdOnly: true }); + room = RocketChat.getRoomByNameOrIdWithOptionToJoin({ currentUserId: user._id, nameOrId: channelValue, type: 'd', tryDirectByUserIdOnly: true }); if (room) { break; } @@ -99,7 +39,7 @@ this.processWebhookMessage = function(messageObj, user, defaultValues) { messageObj.attachments = undefined; } - message = { + const message = { alias: messageObj.username || messageObj.alias || defaultValues.alias, msg: _.trim(messageObj.text || messageObj.msg || ''), attachments: messageObj.attachments, @@ -119,9 +59,8 @@ this.processWebhookMessage = function(messageObj, user, defaultValues) { } if (_.isArray(message.attachments)) { - ref = message.attachments; - for (i = 0, len = ref.length; i < len; i++) { - attachment = ref[i]; + for (let i = 0; i < message.attachments.length; i++) { + const attachment = message.attachments[i]; if (attachment.msg) { attachment.text = _.trim(attachment.msg); delete attachment.msg; @@ -129,8 +68,9 @@ this.processWebhookMessage = function(messageObj, user, defaultValues) { } } - var messageReturn = RocketChat.sendMessage(user, message, room); - ret.push({ channel: channel, message: messageReturn }); + const messageReturn = RocketChat.sendMessage(user, message, room); + sentData.push({ channel: channel, message: messageReturn }); } - return ret; + + return sentData; }; diff --git a/packages/rocketchat-integrations/server/publications/integrationHistory.js b/packages/rocketchat-integrations/server/publications/integrationHistory.js new file mode 100644 index 000000000000..22d8f68e4278 --- /dev/null +++ b/packages/rocketchat-integrations/server/publications/integrationHistory.js @@ -0,0 +1,13 @@ +Meteor.publish('integrationHistory', function _integrationHistoryPublication(integrationId, limit = 25) { + if (!this.userId) { + return this.ready(); + } + + if (RocketChat.authz.hasPermission(this.userId, 'manage-integrations')) { + return RocketChat.models.IntegrationHistory.findByIntegrationId(integrationId, { sort: { _updatedAt: -1 }, limit }); + } else if (RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations')) { + return RocketChat.models.IntegrationHistory.findByIntegrationIdAndCreatedBy(integrationId, this.userId, { sort: { _updatedAt: -1 }, limit }); + } else { + throw new Meteor.Error('not-authorized'); + } +}); diff --git a/packages/rocketchat-integrations/server/publications/integrations.coffee b/packages/rocketchat-integrations/server/publications/integrations.coffee deleted file mode 100644 index bd2209f64e49..000000000000 --- a/packages/rocketchat-integrations/server/publications/integrations.coffee +++ /dev/null @@ -1,10 +0,0 @@ -Meteor.publish 'integrations', -> - unless @userId - return @ready() - - if RocketChat.authz.hasPermission @userId, 'manage-integrations' - return RocketChat.models.Integrations.find() - else if RocketChat.authz.hasPermission @userId, 'manage-own-integrations' - return RocketChat.models.Integrations.find({"_createdBy._id": @userId}) - else - throw new Meteor.Error "not-authorized" diff --git a/packages/rocketchat-integrations/server/publications/integrations.js b/packages/rocketchat-integrations/server/publications/integrations.js new file mode 100644 index 000000000000..065081dbabea --- /dev/null +++ b/packages/rocketchat-integrations/server/publications/integrations.js @@ -0,0 +1,13 @@ +Meteor.publish('integrations', function _integrationPublication() { + if (!this.userId) { + return this.ready(); + } + + if (RocketChat.authz.hasPermission(this.userId, 'manage-integrations')) { + return RocketChat.models.Integrations.find(); + } else if (RocketChat.authz.hasPermission(this.userId, 'manage-own-integrations')) { + return RocketChat.models.Integrations.find({ '_createdBy._id': this.userId }); + } else { + throw new Meteor.Error('not-authorized'); + } +}); diff --git a/packages/rocketchat-integrations/server/triggers.coffee b/packages/rocketchat-integrations/server/triggers.coffee deleted file mode 100644 index dbf7fcbc7f69..000000000000 --- a/packages/rocketchat-integrations/server/triggers.coffee +++ /dev/null @@ -1,341 +0,0 @@ -vm = Npm.require('vm') - -compiledScripts = {} - -getIntegrationScript = (integration) -> - compiledScript = compiledScripts[integration._id] - if compiledScript? and +compiledScript._updatedAt is +integration._updatedAt - return compiledScript.script - - script = integration.scriptCompiled - vmScript = undefined - store = {} - sandbox = - _: _ - s: s - console: console - Store: - set: (key, val) -> - return store[key] = val - get: (key) -> - return store[key] - HTTP: (method, url, options) -> - try - return {} = - result: HTTP.call method, url, options - catch e - return {} = - error: e - - try - logger.outgoing.info 'Will evaluate script of Trigger', integration.name - logger.outgoing.debug script - - vmScript = vm.createScript script, 'script.js' - - vmScript.runInNewContext sandbox - - if sandbox.Script? - compiledScripts[integration._id] = - script: new sandbox.Script() - _updatedAt: integration._updatedAt - - return compiledScripts[integration._id].script - catch e - logger.outgoing.error '[Error evaluating Script in Trigger', integration.name, ':]' - logger.outgoing.error script.replace(/^/gm, ' ') - logger.outgoing.error "[Stack:]" - logger.outgoing.error e.stack.replace(/^/gm, ' ') - throw new Meteor.Error 'error-evaluating-script' - - if not sandbox.Script? - logger.outgoing.error '[Class "Script" not in Trigger', integration.name, ']' - throw new Meteor.Error 'class-script-not-found' - - -triggers = {} - -hasScriptAndMethod = (integration, method) -> - if integration.scriptEnabled isnt true or not integration.scriptCompiled? or integration.scriptCompiled.trim() is '' - return false - - script = undefined - try - script = getIntegrationScript(integration) - catch e - return - - return script[method]? - -executeScript = (integration, method, params) -> - script = undefined - try - script = getIntegrationScript(integration) - catch e - return - - if not script[method]? - logger.outgoing.error '[Method "', method, '" not found in Trigger', integration.name, ']' - return - - try - sandbox = - _: _ - s: s - console: console - Store: - set: (key, val) -> - return store[key] = val - get: (key) -> - return store[key] - HTTP: (method, url, options) -> - try - return {} = - result: HTTP.call method, url, options - catch e - return {} = - error: e - script: script - method: method - params: params - result = vm.runInNewContext('script[method](params)', sandbox, { timeout: 3000 }) - - logger.outgoing.debug '[Script method [', method, '] result of Trigger', integration.name, ':]' - logger.outgoing.debug result - - return result - catch e - logger.outgoing.error '[Error running Script in Trigger', integration.name, ':]' - logger.outgoing.error integration.scriptCompiled.replace(/^/gm, ' ') - logger.outgoing.error "[Stack:]" - logger.outgoing.error e.stack.replace(/^/gm, ' ') - return - - -addIntegration = (record) -> - if _.isEmpty(record.channel) - channels = [ '__any' ] - else - channels = [].concat(record.channel) - - for channel in channels - triggers[channel] ?= {} - triggers[channel][record._id] = record - -removeIntegration = (record) -> - for channel, trigger of triggers - delete trigger[record._id] - -RocketChat.models.Integrations.find({type: 'webhook-outgoing'}).observe - added: (record) -> - addIntegration(record) - - changed: (record) -> - removeIntegration(record) - addIntegration(record) - - removed: (record) -> - removeIntegration(record) - - -ExecuteTriggerUrl = (url, trigger, message, room, tries=0) -> - 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 = - message_id: message._id - token: trigger.token - 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 message.alias? - data.alias = message.alias - - if message.bot? - data.bot = message.bot - else - data.bot = false - - if word? - data.trigger_word = word - - logger.outgoing.info 'Will execute trigger', trigger.name, 'to', url - logger.outgoing.debug data - - sendMessage = (message) -> - if trigger.impersonateUser ? false - user = RocketChat.models.Users.findOneByUsername(data.user_name) - else - user = RocketChat.models.Users.findOneByUsername(trigger.username) - - message.bot = - i: trigger._id - - defaultValues = - alias: trigger.alias - avatar: trigger.avatar - emoji: trigger.emoji - - if room.t is 'd' - message.channel = '@'+room._id - else - message.channel = '#'+room._id - - message = processWebhookMessage message, user, defaultValues - - - opts = - params: {} - method: 'POST' - url: url - data: data - auth: undefined - 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' - - if hasScriptAndMethod(trigger, 'prepare_outgoing_request') - sandbox = - request: opts - - opts = executeScript trigger, 'prepare_outgoing_request', sandbox - - if not opts? - return - - if opts.message? - sendMessage opts.message - - if not opts.url? or not opts.method? - return - - HTTP.call opts.method, opts.url, opts, (error, result) -> - if not result? - logger.outgoing.info 'Result for trigger', trigger.name, 'to', url, 'is empty' - else - logger.outgoing.info 'Status code for trigger', trigger.name, 'to', url, 'is', result.statusCode - - scriptResult = undefined - if hasScriptAndMethod(trigger, 'process_outgoing_response') - sandbox = - request: opts - response: - error: error - status_code: result.statusCode - content: result.data - content_raw: result.content - headers: result.headers - - scriptResult = executeScript trigger, 'process_outgoing_response', sandbox - - if scriptResult?.content - sendMessage scriptResult.content - return - - if scriptResult is false - return - - if not result? or result.statusCode not in [200, 201, 202] - if error? - logger.outgoing.error 'Error for trigger', trigger.name, 'to', url, error - if result? - logger.outgoing.error 'Error for trigger', trigger.name, 'to', url, result - - if result.statusCode is 410 - RocketChat.models.Integrations.remove _id: trigger._id - return - - if result.statusCode is 500 - logger.outgoing.error 'Error [500] for trigger', trigger.name, 'to', url - logger.outgoing.error result.content - return - - if tries <= 6 - # Try again in 0.1s, 1s, 10s, 1m40s, 16m40s, 2h46m40s and 27h46m40s - waitTime = Math.pow(10, tries+2) - logger.outgoing.info 'Trying trigger', trigger.name, 'to', url, 'again in', waitTime, 'seconds' - Meteor.setTimeout -> - ExecuteTriggerUrl url, trigger, message, room, tries+1 - , waitTime - - return - - # process outgoing webhook response as a new message - if result?.statusCode in [200, 201, 202] - if result?.data?.text? or result?.data?.attachments? - sendMessage result.data - - -ExecuteTrigger = (trigger, message, room) -> - for url in trigger.urls - ExecuteTriggerUrl url, trigger, message, room - - -ExecuteTriggers = (message, room) -> - if not room? - return - - triggersToExecute = [] - - switch room.t - when 'd' - id = room._id.replace(message.u._id, '') - - username = _.without room.usernames, message.u.username - username = username[0] - - if triggers['@'+id]? - triggersToExecute.push trigger for key, trigger of triggers['@'+id] - - if triggers.all_direct_messages? - triggersToExecute.push trigger for key, trigger of triggers.all_direct_messages - - if id isnt username and triggers['@'+username]? - triggersToExecute.push trigger for key, trigger of triggers['@'+username] - - when 'c' - if triggers.__any? - triggersToExecute.push trigger for key, trigger of triggers.__any - - if triggers.all_public_channels? - triggersToExecute.push trigger for key, trigger of triggers.all_public_channels - - if triggers['#'+room._id]? - triggersToExecute.push trigger for key, trigger of triggers['#'+room._id] - - if room._id isnt room.name and triggers['#'+room.name]? - triggersToExecute.push trigger for key, trigger of triggers['#'+room.name] - - else - if triggers.all_private_groups? - triggersToExecute.push trigger for key, trigger of triggers.all_private_groups - - if triggers['#'+room._id]? - triggersToExecute.push trigger for key, trigger of triggers['#'+room._id] - - if room._id isnt room.name and triggers['#'+room.name]? - triggersToExecute.push trigger for key, trigger of triggers['#'+room.name] - - - for triggerToExecute in triggersToExecute - if triggerToExecute.enabled is true - ExecuteTrigger triggerToExecute, message, room - - return message - - -RocketChat.callbacks.add 'afterSaveMessage', ExecuteTriggers, RocketChat.callbacks.priority.LOW, 'ExecuteTriggers' diff --git a/packages/rocketchat-integrations/server/triggers.js b/packages/rocketchat-integrations/server/triggers.js new file mode 100644 index 000000000000..ce0ebdbedda5 --- /dev/null +++ b/packages/rocketchat-integrations/server/triggers.js @@ -0,0 +1,14 @@ +const callbackHandler = function _callbackHandler(eventType) { + return function _wrapperFunction() { + return RocketChat.integrations.triggerHandler.executeTriggers(eventType, ...arguments); + }; +}; + +RocketChat.callbacks.add('afterSaveMessage', callbackHandler('sendMessage'), RocketChat.callbacks.priority.LOW); +RocketChat.callbacks.add('afterCreateChannel', callbackHandler('roomCreated'), RocketChat.callbacks.priority.LOW); +RocketChat.callbacks.add('afterCreatePrivateGroup', callbackHandler('roomCreated'), RocketChat.callbacks.priority.LOW); +RocketChat.callbacks.add('afterCreateUser', callbackHandler('userCreated'), RocketChat.callbacks.priority.LOW); +RocketChat.callbacks.add('afterJoinRoom', callbackHandler('roomJoined'), RocketChat.callbacks.priority.LOW); +RocketChat.callbacks.add('afterLeaveRoom', callbackHandler('roomLeft'), RocketChat.callbacks.priority.LOW); +RocketChat.callbacks.add('afterRoomArchived', callbackHandler('roomArchived'), RocketChat.callbacks.priority.LOW); +RocketChat.callbacks.add('afterFileUpload', callbackHandler('fileUploaded'), RocketChat.callbacks.priority.LOW); diff --git a/packages/rocketchat-lib/lib/callbacks.coffee b/packages/rocketchat-lib/lib/callbacks.coffee index 7cda012cb30b..d0e3feb8ff32 100644 --- a/packages/rocketchat-lib/lib/callbacks.coffee +++ b/packages/rocketchat-lib/lib/callbacks.coffee @@ -92,7 +92,7 @@ RocketChat.callbacks.run = (hook, item, constant) -> else console.log String(currentTime), hook, callback.id, callback.stack?.split?('\n')[2]?.match(/\(.+\)/)?[0] - return callbackResult + return if typeof callbackResult == 'undefined' then result else callbackResult , item if RocketChat.callbacks.showTotalTime is true diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 61bcc5cd1a74..652f2de2b5c3 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -78,6 +78,7 @@ Package.onUse(function(api) { api.addFiles('server/functions/deleteMessage.js', 'server'); api.addFiles('server/functions/deleteUser.js', 'server'); api.addFiles('server/functions/getFullUserData.js', 'server'); + api.addFiles('server/functions/getRoomByNameOrIdWithOptionToJoin.js', 'server'); api.addFiles('server/functions/removeUserFromRoom.js', 'server'); api.addFiles('server/functions/saveUser.js', 'server'); api.addFiles('server/functions/saveCustomFields.js', 'server'); diff --git a/packages/rocketchat-lib/server/functions/addUserToRoom.js b/packages/rocketchat-lib/server/functions/addUserToRoom.js index c3f8f9677bbf..89057df8670f 100644 --- a/packages/rocketchat-lib/server/functions/addUserToRoom.js +++ b/packages/rocketchat-lib/server/functions/addUserToRoom.js @@ -8,7 +8,7 @@ RocketChat.addUserToRoom = function(rid, user, inviter, silenced) { return; } - if (room.t === 'c') { + if (room.t === 'c' || room.t === 'p') { RocketChat.callbacks.run('beforeJoinRoom', user, room); } @@ -35,7 +35,7 @@ RocketChat.addUserToRoom = function(rid, user, inviter, silenced) { } } - if (room.t === 'c') { + if (room.t === 'c' || room.t === 'p') { Meteor.defer(function() { RocketChat.callbacks.run('afterJoinRoom', user, room); }); diff --git a/packages/rocketchat-lib/server/functions/archiveRoom.js b/packages/rocketchat-lib/server/functions/archiveRoom.js index ef2aafeffe47..f48be588c92f 100644 --- a/packages/rocketchat-lib/server/functions/archiveRoom.js +++ b/packages/rocketchat-lib/server/functions/archiveRoom.js @@ -1,4 +1,6 @@ RocketChat.archiveRoom = function(rid) { RocketChat.models.Rooms.archiveById(rid); RocketChat.models.Subscriptions.archiveByRoomId(rid); + + RocketChat.callbacks.run('afterRoomArchived', RocketChat.models.Rooms.findOneById(rid), Meteor.user()); }; diff --git a/packages/rocketchat-lib/server/functions/createRoom.js b/packages/rocketchat-lib/server/functions/createRoom.js index 4390c75c60be..3e6512ea0166 100644 --- a/packages/rocketchat-lib/server/functions/createRoom.js +++ b/packages/rocketchat-lib/server/functions/createRoom.js @@ -88,6 +88,10 @@ RocketChat.createRoom = function(type, name, owner, members, readOnly, extraData Meteor.defer(() => { RocketChat.callbacks.run('afterCreateChannel', owner, room); }); + } else if (type === 'p') { + Meteor.defer(() => { + RocketChat.callbacks.run('afterCreatePrivateGroup', owner, room); + }); } return { diff --git a/packages/rocketchat-lib/server/functions/deleteUser.js b/packages/rocketchat-lib/server/functions/deleteUser.js index 4184ff5658ef..9a560a4372e8 100644 --- a/packages/rocketchat-lib/server/functions/deleteUser.js +++ b/packages/rocketchat-lib/server/functions/deleteUser.js @@ -25,5 +25,7 @@ RocketChat.deleteUser = function(userId) { RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(user.username + '.jpg')); } + RocketChat.models.Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. + RocketChat.models.Users.removeById(userId); // Remove user from users database }; diff --git a/packages/rocketchat-lib/server/functions/getRoomByNameOrIdWithOptionToJoin.js b/packages/rocketchat-lib/server/functions/getRoomByNameOrIdWithOptionToJoin.js new file mode 100644 index 000000000000..10b8999e07ad --- /dev/null +++ b/packages/rocketchat-lib/server/functions/getRoomByNameOrIdWithOptionToJoin.js @@ -0,0 +1,78 @@ +/* globals RocketChat */ +RocketChat.getRoomByNameOrIdWithOptionToJoin = function _getRoomByNameOrIdWithOptionToJoin({ currentUserId, nameOrId, type='', tryDirectByUserIdOnly=false, joinChannel=true, errorOnEmpty=true }) { + let room; + + //If the nameOrId starts with #, then let's try to find a channel or group + if (nameOrId.startsWith('#')) { + nameOrId = nameOrId.substring(1); + room = RocketChat.models.Rooms.findOneByIdOrName(nameOrId); + } else if (nameOrId.startsWith('@') || type === 'd') { + //If the nameOrId starts with @ OR type is 'd', then let's try just a direct message + nameOrId = nameOrId.replace('@', ''); + + let roomUser; + if (tryDirectByUserIdOnly) { + roomUser = RocketChat.models.Users.findOneById(nameOrId); + } else { + roomUser = RocketChat.models.Users.findOne({ + $or: [{ _id: nameOrId }, { username: nameOrId }] + }); + } + + const rid = _.isObject(roomUser) ? [currentUserId, roomUser._id].sort().join('') : nameOrId; + room = RocketChat.models.Rooms.findOneById(rid); + + //If the room hasn't been found yet, let's try some more + if (!_.isObject(room)) { + //If the roomUser wasn't found, then there's no destination to point towards + //so return out based upon errorOnEmpty + if (!_.isObject(roomUser)) { + if (errorOnEmpty) { + throw new Meteor.Error('invalid-channel'); + } else { + return; + } + } + + room = Meteor.runAsUser(currentUserId, function() { + const {rid} = Meteor.call('createDirectMessage', roomUser.username); + return RocketChat.models.Rooms.findOneById(rid); + }); + } + } else { + //Otherwise, we'll treat this as a channel or group. + room = RocketChat.models.Rooms.findOneByIdOrName(nameOrId); + } + + //If no room was found, handle the room return based upon errorOnEmpty + if (!room && errorOnEmpty) { + throw new Meteor.Error('invalid-channel'); + } else if (!room) { + return; + } + + //If a room was found and they provided a type to search, then check + //and if the type found isn't what we're looking for then handle + //the return based upon errorOnEmpty + if (type && room.t !== type) { + if (errorOnEmpty) { + throw new Meteor.Error('invalid-channel'); + } else { + return; + } + } + + //If the room type is channel and joinChannel has been passed, try to join them + //if they can't join the room, this will error out! + if (room.t === 'c' && joinChannel) { + const sub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(room._id, currentUserId); + + if (!sub) { + Meteor.runAsUser(currentUserId, function() { + return Meteor.call('joinRoom', room._id); + }); + } + } + + return room; +}; diff --git a/packages/rocketchat-lib/server/methods/sendMessage.coffee b/packages/rocketchat-lib/server/methods/sendMessage.coffee index 2d121dc4ed22..95e8f71a66ab 100644 --- a/packages/rocketchat-lib/server/methods/sendMessage.coffee +++ b/packages/rocketchat-lib/server/methods/sendMessage.coffee @@ -49,9 +49,9 @@ Meteor.methods message.alias = user.name if not message.alias? and RocketChat.settings.get 'Message_SetNameToAliasEnabled' if Meteor.settings.public.sandstorm message.sandstormSessionId = this.connection.sandstormSessionId() - + + RocketChat.metrics.messagesSent.inc() # This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 RocketChat.sendMessage user, message, room - RocketChat.metrics.messagesSent.inc() # Limit a user, who does not have the "bot" role, to sending 5 msgs/second DDPRateLimiter.addRule diff --git a/packages/rocketchat-lib/server/methods/setUsername.js b/packages/rocketchat-lib/server/methods/setUsername.js index c61b74f16d4c..b64b1ddc5b3b 100644 --- a/packages/rocketchat-lib/server/methods/setUsername.js +++ b/packages/rocketchat-lib/server/methods/setUsername.js @@ -44,6 +44,9 @@ Meteor.methods({ if (!user.username) { Meteor.runAsUser(user._id, () => Meteor.call('joinDefaultChannels', joinDefaultChannelsSilenced)); + Meteor.defer(function() { + return RocketChat.callbacks.run('afterCreateUser', RocketChat.models.Users.findOneById(user._id)); + }); } return username; diff --git a/packages/rocketchat-livechat/server/hooks/externalMessage.js b/packages/rocketchat-livechat/server/hooks/externalMessage.js index 4455f25d2c01..be655a524a46 100644 --- a/packages/rocketchat-livechat/server/hooks/externalMessage.js +++ b/packages/rocketchat-livechat/server/hooks/externalMessage.js @@ -15,7 +15,7 @@ RocketChat.settings.get('Livechat_Knowledge_Apiai_Language', function(key, value RocketChat.callbacks.add('afterSaveMessage', function(message, room) { // skips this callback if the message was edited - if (message.editedAt) { + if (!message || message.editedAt) { return message; } diff --git a/packages/rocketchat-livechat/server/hooks/markRoomResponded.js b/packages/rocketchat-livechat/server/hooks/markRoomResponded.js index 155ba30cad13..23f9af55792d 100644 --- a/packages/rocketchat-livechat/server/hooks/markRoomResponded.js +++ b/packages/rocketchat-livechat/server/hooks/markRoomResponded.js @@ -1,6 +1,6 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { // skips this callback if the message was edited - if (message.editedAt) { + if (!message || message.editedAt) { return message; } diff --git a/server/lib/accounts.js b/server/lib/accounts.js index efe965202b99..5dad2be2517c 100644 --- a/server/lib/accounts.js +++ b/server/lib/accounts.js @@ -136,9 +136,6 @@ Accounts.insertUserDoc = _.wrap(Accounts.insertUserDoc, function(insertUserDoc, } RocketChat.authz.addUserRoles(_id, roles); - Meteor.defer(function() { - return RocketChat.callbacks.run('afterCreateUser', options, user); - }); return _id; }); diff --git a/server/startup/migrations/v089.js b/server/startup/migrations/v089.js new file mode 100644 index 000000000000..4b8dcff3a849 --- /dev/null +++ b/server/startup/migrations/v089.js @@ -0,0 +1,17 @@ +RocketChat.Migrations.add({ + version: 89, + up() { + const outgoingIntegrations = RocketChat.models.Integrations.find({ type: 'webhook-outgoing' }, { fields: { name: 1 }}).fetch(); + + outgoingIntegrations.forEach((i) => { + RocketChat.models.Integrations.update(i._id, { $set: { event: 'sendMessage', retryFailedCalls: true, retryCount: 6, retryDelay: 'powers-of-ten' }}); + }); + }, + down() { + const outgoingIntegrations = RocketChat.models.Integrations.find({ type: 'webhook-outgoing', event: { $ne: 'sendMessage' }}, { fields: { name: 1 }}).fetch(); + + outgoingIntegrations.forEach((i) => { + RocketChat.models.Integrations.update(i._id, { $set: { enabled: false }}); + }); + } +}); diff --git a/tests/end-to-end/api/06-integrations.js b/tests/end-to-end/api/06-integrations.js index 3999ceea82dc..b4ea93f08ca2 100644 --- a/tests/end-to-end/api/06-integrations.js +++ b/tests/end-to-end/api/06-integrations.js @@ -23,7 +23,8 @@ describe('integrations', function() { triggerWords: ['!guggy'], alias: 'guggy', avatar: 'http://res.guggy.com/logo_128.png', - emoji: ':ghost:' + emoji: ':ghost:', + event: 'sendMessage' }) .expect('Content-Type', 'application/json') .expect(200) @@ -34,6 +35,7 @@ describe('integrations', function() { expect(res.body).to.have.deep.property('integration.type', 'webhook-outgoing'); expect(res.body).to.have.deep.property('integration.enabled', true); expect(res.body).to.have.deep.property('integration.username', 'rocket.cat'); + expect(res.body).to.have.deep.property('integration.event', 'sendMessage'); }) .end(done); });