feat: Allow Incoming Webhooks to override destination channel (#26875)

Co-authored-by: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com>
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
Co-authored-by: matheusbsilva137 <matheus_barbosa137@hotmail.com>
Co-authored-by: Diego Sampaio <chinello@gmail.com>
pull/29099/head^2
Luciano Marcos Pierdona Junior 3 years ago committed by GitHub
parent 7832a40a6d
commit 12d97e16c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      .changeset/shy-bees-serve.md
  2. 4
      apps/meteor/app/integrations/server/api/api.js
  3. 1
      apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts
  4. 1
      apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts
  5. 14
      apps/meteor/client/views/admin/integrations/IncomingWebhookForm.js
  6. 1
      apps/meteor/client/views/admin/integrations/edit/EditIncomingWebhook.js
  7. 1
      apps/meteor/client/views/admin/integrations/new/NewIncomingWebhook.js
  8. 1
      apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
  9. 1
      apps/meteor/server/startup/migrations/index.ts
  10. 20
      apps/meteor/server/startup/migrations/v298.ts
  11. 1
      apps/meteor/tests/end-to-end/api/02-channels.js
  12. 1
      apps/meteor/tests/end-to-end/api/03-groups.js
  13. 120
      apps/meteor/tests/end-to-end/api/07-incoming-integrations.js
  14. 3
      packages/core-typings/src/IIntegration.ts
  15. 5
      packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts
  16. 13
      packages/rest-typings/src/v1/integrations/IntegrationsUpdateProps.ts

@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-typings": patch
"@rocket.chat/rest-typings": patch
---
feat: Allow Incoming Webhooks to override destination channel

@ -276,6 +276,10 @@ async function executeIntegrationRest() {
return API.v1.success();
}
if ((this.bodyParams.channel || this.bodyParams.roomId) && !this.integration.overrideDestinationChannelEnabled) {
return API.v1.failure('overriding destination channel is disabled for this integration');
}
this.bodyParams.bot = { i: this.integration._id };
try {

@ -30,6 +30,7 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn
alias: Match.Maybe(String),
emoji: Match.Maybe(String),
scriptEnabled: Boolean,
overrideDestinationChannelEnabled: Boolean,
script: Match.Maybe(String),
avatar: Match.Maybe(String),
}),

@ -159,6 +159,7 @@ Meteor.methods<ServerMethods>({
channel: channels,
script: integration.script,
scriptEnabled: integration.scriptEnabled,
overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled,
_updatedAt: new Date(),
_updatedBy: await Users.findOne({ _id: this.userId }, { projection: { username: 1 } }),
},

@ -11,7 +11,7 @@ export default function IncomingWebhookForm({ formValues, formHandlers, extraDat
const absoluteUrl = useAbsoluteUrl();
const { enabled, channel, username, name, alias, avatar, emoji, scriptEnabled, script } = formValues;
const { enabled, channel, username, name, alias, avatar, emoji, scriptEnabled, script, overrideDestinationChannelEnabled } = formValues;
const {
handleEnabled,
@ -22,6 +22,7 @@ export default function IncomingWebhookForm({ formValues, formHandlers, extraDat
handleAvatar,
handleEmoji,
handleScriptEnabled,
handleOverrideDestinationChannelEnabled,
handleScript,
} = formHandlers;
@ -149,6 +150,17 @@ export default function IncomingWebhookForm({ formValues, formHandlers, extraDat
),
[emoji, handleEmoji, t],
)}
{useMemo(
() => (
<Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Override_Destination_Channel')}
<ToggleSwitch checked={overrideDestinationChannelEnabled} onChange={handleOverrideDestinationChannelEnabled} />
</Field.Label>
</Field>
),
[t, overrideDestinationChannelEnabled, handleOverrideDestinationChannelEnabled],
)}
{useMemo(
() => (
<Field>

@ -17,6 +17,7 @@ const getInitialValue = (data) => {
avatar: data.avatar ?? '',
emoji: data.emoji ?? '',
scriptEnabled: data.scriptEnabled,
overrideDestinationChannelEnabled: data.overrideDestinationChannelEnabled,
script: data.script,
};
return initialValue;

@ -15,6 +15,7 @@ const initialState = {
avatar: '',
emoji: '',
scriptEnabled: false,
overrideDestinationChannelEnabled: false,
script: '',
};

@ -3810,6 +3810,7 @@
"Outgoing_WebHook_Description": "Get data out of Rocket.Chat in real-time.",
"Output_format": "Output format",
"Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "Override URL to which files are uploaded. This url also used for downloads unless a CDN is given",
"Override_Destination_Channel": "Allow to overwrite destination channel in the body parameters",
"Owner": "Owner",
"Play": "Play",
"Page_not_exist_or_not_permission": "The page does not exist or you may not have access permission",

@ -31,4 +31,5 @@ import './v294';
import './v295';
import './v296';
import './v297';
import './v298';
import './xrun';

@ -0,0 +1,20 @@
import { Integrations } from '@rocket.chat/models';
import { addMigration } from '../../lib/migrations';
addMigration({
version: 298,
name: 'Set overrideDestinationChannelEnabled for all incoming webhook integrations',
async up() {
await Integrations.updateMany(
{
type: 'webhook-incoming',
},
{
$set: {
overrideDestinationChannelEnabled: true,
},
},
);
},
});

@ -1137,6 +1137,7 @@ describe('[Channels]', function () {
alias: 'test',
username: 'rocket.cat',
scriptEnabled: false,
overrideDestinationChannelEnabled: true,
channel: `#${createdChannel.name}`,
},
userCredentials,

@ -901,6 +901,7 @@ describe('[Groups]', function () {
alias: 'test',
username: 'rocket.cat',
scriptEnabled: false,
overrideDestinationChannelEnabled: true,
channel: `#${createdGroup.name}`,
},
userCredentials,

@ -5,6 +5,7 @@ import { updatePermission } from '../../data/permissions.helper';
import { createIntegration, removeIntegration } from '../../data/integration.helper';
import { createUser, login } from '../../data/users.helper';
import { password } from '../../data/user';
import { createRoom } from '../../data/rooms.helper.js';
describe('[Incoming Integrations]', function () {
this.retries(0);
@ -13,6 +14,8 @@ describe('[Incoming Integrations]', function () {
let integrationCreatedByAnUser;
let user;
let userCredentials;
let channel;
let testChannelName;
before((done) => getCredentials(done));
@ -20,8 +23,15 @@ describe('[Incoming Integrations]', function () {
updatePermission('manage-incoming-integrations', [])
.then(() => updatePermission('manage-own-incoming-integrations', []))
.then(() => updatePermission('manage-own-outgoing-integrations', []))
.then(() => updatePermission('manage-outgoing-integrations', []))
.then(done);
.then(() => updatePermission('manage-outgoing-integrations', []));
testChannelName = `channel.test.${Date.now()}-${Math.random()}`;
createRoom({ type: 'c', name: testChannelName }).end((err, res) => {
channel = res.body.channel;
return done();
});
});
after((done) => {
@ -45,6 +55,7 @@ describe('[Incoming Integrations]', function () {
alias: 'test',
username: 'rocket.cat',
scriptEnabled: false,
overrideDestinationChannelEnabled: true,
channel: '#general',
})
.expect('Content-Type', 'application/json')
@ -69,6 +80,7 @@ describe('[Incoming Integrations]', function () {
alias: 'test',
username: 'rocket.cat',
scriptEnabled: false,
overrideDestinationChannelEnabled: true,
channel: '#general',
})
.expect('Content-Type', 'application/json')
@ -92,6 +104,7 @@ describe('[Incoming Integrations]', function () {
alias: 'test',
username: 'rocket.cat',
scriptEnabled: false,
overrideDestinationChannelEnabled: true,
channel: '#general',
})
.expect('Content-Type', 'application/json')
@ -116,6 +129,7 @@ describe('[Incoming Integrations]', function () {
alias: 'test',
username: 'rocket.cat',
scriptEnabled: false,
overrideDestinationChannelEnabled: false,
channel: '#general',
})
.expect('Content-Type', 'application/json')
@ -142,6 +156,7 @@ describe('[Incoming Integrations]', function () {
alias: 'test2',
username: 'rocket.cat',
scriptEnabled: false,
overrideDestinationChannelEnabled: false,
channel: '#general',
})
.expect('Content-Type', 'application/json')
@ -165,6 +180,105 @@ describe('[Incoming Integrations]', function () {
.expect(200)
.end(done);
});
it("should return an error when sending 'channel' field telling its configuration is disabled", (done) => {
request
.post(`/hooks/${integration._id}/${integration.token}`)
.send({
text: 'Example message',
channel: [testChannelName],
})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'overriding destination channel is disabled for this integration');
})
.end(done);
});
it("should return an error when sending 'roomId' field telling its configuration is disabled", (done) => {
request
.post(`/hooks/${integration._id}/${integration.token}`)
.send({
text: 'Example message',
roomId: channel._id,
})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'overriding destination channel is disabled for this integration');
})
.end(done);
});
it('should send a message for a channel that is specified in the webhooks configuration', (done) => {
const successfulMesssage = `Message sent successfully at #${Date.now()}`;
request
.post(`/hooks/${integration._id}/${integration.token}`)
.send({
text: successfulMesssage,
})
.expect(200)
.end(() => {
return request
.get(api('channels.messages'))
.set(credentials)
.query({
roomId: 'GENERAL',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').and.to.be.an('array');
expect(!!res.body.messages.find((m) => m.msg === successfulMesssage)).to.be.true;
})
.end(done);
});
});
it('should send a message for a channel that is not specified in the webhooks configuration', async () => {
await request
.put(api('integrations.update'))
.set(credentials)
.send({
type: 'webhook-incoming',
overrideDestinationChannelEnabled: true,
integrationId: integration._id,
username: 'rocket.cat',
channel: '#general',
scriptEnabled: true,
enabled: true,
name: integration.name,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('integration');
expect(res.body.integration.overrideDestinationChannelEnabled).to.be.equal(true);
});
const successfulMesssage = `Message sent successfully at #${Date.now()}`;
await request
.post(`/hooks/${integration._id}/${integration.token}`)
.send({
text: successfulMesssage,
channel: [testChannelName],
})
.expect(200);
return request
.get(api('channels.messages'))
.set(credentials)
.query({
roomId: channel._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').and.to.be.an('array');
expect(!!res.body.messages.find((m) => m.msg === successfulMesssage)).to.be.true;
});
});
});
describe('[/integrations.history]', () => {
@ -200,6 +314,7 @@ describe('[Incoming Integrations]', function () {
alias: 'test',
username: 'rocket.cat',
scriptEnabled: false,
overrideDestinationChannelEnabled: true,
channel: '#general',
},
userCredentials,
@ -381,6 +496,7 @@ describe('[Incoming Integrations]', function () {
alias: 'test updated',
username: 'rocket.cat',
scriptEnabled: true,
overrideDestinationChannelEnabled: true,
channel: '#general',
integrationId: integration._id,
})

@ -10,6 +10,7 @@ export interface IIncomingIntegration extends IRocketChatRecord {
channel: string[];
token: string;
overrideDestinationChannelEnabled: boolean;
scriptEnabled: boolean;
script: string;
scriptCompiled?: string;
@ -84,7 +85,7 @@ export type INewOutgoingIntegration = Omit<
};
export type IUpdateIncomingIntegration = Omit<
IOutgoingIntegration,
IIncomingIntegration,
'type' | 'channel' | 'scriptCompiled' | 'scriptError' | '_createdBy' | '_createdAt' | 'userId' | 'token' | 'username'
> & {
channel?: string;

@ -8,6 +8,7 @@ export type IntegrationsCreateProps =
type: 'webhook-incoming';
username: string;
channel: string;
overrideDestinationChannelEnabled: boolean;
scriptEnabled: boolean;
script?: string;
name: string;
@ -67,6 +68,10 @@ const integrationsCreateSchema = {
type: 'boolean',
nullable: false,
},
overrideDestinationChannelEnabled: {
type: 'boolean',
nullable: false,
},
script: {
type: 'string',
nullable: true,

@ -9,6 +9,7 @@ export type IntegrationsUpdateProps =
integrationId: string;
channel: string;
scriptEnabled: boolean;
overrideDestinationChannelEnabled: boolean;
script?: string;
name: string;
enabled: boolean;
@ -69,6 +70,10 @@ const integrationsUpdateSchema = {
type: 'boolean',
nullable: false,
},
overrideDestinationChannelEnabled: {
type: 'boolean',
nullable: false,
},
script: {
type: 'string',
nullable: true,
@ -94,7 +99,7 @@ const integrationsUpdateSchema = {
nullable: true,
},
},
required: ['integrationId', 'type', 'channel', 'scriptEnabled', 'name', 'enabled'],
required: ['integrationId', 'type', 'channel', 'scriptEnabled', 'overrideDestinationChannelEnabled', 'name', 'enabled'],
additionalProperties: true,
},
{
@ -157,6 +162,10 @@ const integrationsUpdateSchema = {
type: 'boolean',
nullable: false,
},
overrideDestinationChannelEnabled: {
type: 'boolean',
nullable: false,
},
script: {
type: 'string',
nullable: true,
@ -202,7 +211,7 @@ const integrationsUpdateSchema = {
nullable: true,
},
},
required: ['type', 'username', 'channel', 'event', 'scriptEnabled', 'name', 'enabled'],
required: ['type', 'username', 'channel', 'event', 'scriptEnabled', 'overrideDestinationChannelEnabled', 'name', 'enabled'],
additionalProperties: false,
},
],

Loading…
Cancel
Save