-
Token ({{_ "optional"}})
-
-
+
+
+ {{#if hasTypeSelected}}
+
+
+ {{#if shouldDisplayChannel}}
+
+ {{/if}}
+ {{#if shouldDisplayTriggerWords}}
+
+ {{/if}}
+ {{#if shouldDisplayTargetRoom}}
+
+ {{/if}}
+
+
+
+
+
+
+
+
+
+
+
+ {{#nrr nrrargs 'message' example}}{{/nrr}}
+
+ {{/if}}
-
+ {{/unless}}
+
+
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 @@
+
+
+
+
+ {{#unless hasPermission}}
+
{{_ "You_are_not_authorized_to_view_this_page"}}
+ {{else}}
+
{{_ "Back_to_integration_detail"}}
+
+
+ {{/unless}}
+
+
+
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);
});