diff --git a/packages/rocketchat-api/server/v1/channels.js b/packages/rocketchat-api/server/v1/channels.js index eb76ca5a2038..826525922410 100644 --- a/packages/rocketchat-api/server/v1/channels.js +++ b/packages/rocketchat-api/server/v1/channels.js @@ -671,6 +671,24 @@ RocketChat.API.v1.addRoute('channels.rename', { authRequired: true }, { } }); +RocketChat.API.v1.addRoute('channels.setCustomFields', { authRequired: true }, { + post() { + if (!this.bodyParams.customFields || !(typeof this.bodyParams.customFields === 'object')) { + return RocketChat.API.v1.failure('The bodyParam "customFields" is required with a type like object.'); + } + + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult._id, 'roomCustomFields', this.bodyParams.customFields); + }); + + return RocketChat.API.v1.success({ + channel: RocketChat.models.Rooms.findOneById(findResult._id, { fields: RocketChat.API.v1.defaultFieldsToExclude }) + }); + } +}); + RocketChat.API.v1.addRoute('channels.setDescription', { authRequired: true }, { post() { if (!this.bodyParams.description || !this.bodyParams.description.trim()) { diff --git a/packages/rocketchat-api/server/v1/groups.js b/packages/rocketchat-api/server/v1/groups.js index 3826a38bca9d..3c1e58d86b08 100644 --- a/packages/rocketchat-api/server/v1/groups.js +++ b/packages/rocketchat-api/server/v1/groups.js @@ -540,6 +540,24 @@ RocketChat.API.v1.addRoute('groups.rename', { authRequired: true }, { } }); +RocketChat.API.v1.addRoute('groups.setCustomFields', { authRequired: true }, { + post() { + if (!this.bodyParams.customFields || !(typeof this.bodyParams.customFields === 'object')) { + return RocketChat.API.v1.failure('The bodyParam "customFields" is required with a type like object.'); + } + + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult.rid, 'roomCustomFields', this.bodyParams.customFields); + }); + + return RocketChat.API.v1.success({ + group: RocketChat.models.Rooms.findOneById(findResult.rid, { fields: RocketChat.API.v1.defaultFieldsToExclude }) + }); + } +}); + RocketChat.API.v1.addRoute('groups.setDescription', { authRequired: true }, { post() { if (!this.bodyParams.description || !this.bodyParams.description.trim()) { diff --git a/packages/rocketchat-channel-settings/package.js b/packages/rocketchat-channel-settings/package.js index 3f35a159bb40..68a96d705921 100644 --- a/packages/rocketchat-channel-settings/package.js +++ b/packages/rocketchat-channel-settings/package.js @@ -28,6 +28,7 @@ Package.onUse(function(api) { 'server/functions/saveReactWhenReadOnly.js', 'server/functions/saveRoomType.js', 'server/functions/saveRoomTopic.js', + 'server/functions/saveRoomCustomFields.js', 'server/functions/saveRoomAnnouncement.js', 'server/functions/saveRoomName.js', 'server/functions/saveRoomReadOnly.js', diff --git a/packages/rocketchat-channel-settings/server/functions/saveRoomCustomFields.js b/packages/rocketchat-channel-settings/server/functions/saveRoomCustomFields.js new file mode 100644 index 000000000000..8f7a8f61024d --- /dev/null +++ b/packages/rocketchat-channel-settings/server/functions/saveRoomCustomFields.js @@ -0,0 +1,18 @@ +RocketChat.saveRoomCustomFields = function(rid, roomCustomFields) { + if (!Match.test(rid, String)) { + throw new Meteor.Error('invalid-room', 'Invalid room', { + 'function': 'RocketChat.saveRoomCustomFields' + }); + } + if (!Match.test(roomCustomFields, Object)) { + throw new Meteor.Error('invalid-roomCustomFields-type', 'Invalid roomCustomFields type', { + 'function': 'RocketChat.saveRoomCustomFields' + }); + } + const ret = RocketChat.models.Rooms.setCustomFieldsById(rid, roomCustomFields); + + // Update customFields of any user's Subscription related with this rid + RocketChat.models.Subscriptions.updateCustomFieldsByRoomId(rid, roomCustomFields); + + return ret; +}; diff --git a/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js b/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js index dab07dd47f52..f872f188c24d 100644 --- a/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js +++ b/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js @@ -1,4 +1,4 @@ -const fields = ['roomName', 'roomTopic', 'roomAnnouncement', 'roomDescription', 'roomType', 'readOnly', 'reactWhenReadOnly', 'systemMessages', 'default', 'joinCode', 'tokenpass', 'streamingOptions']; +const fields = ['roomName', 'roomTopic', 'roomAnnouncement', 'roomCustomFields', 'roomDescription', 'roomType', 'readOnly', 'reactWhenReadOnly', 'systemMessages', 'default', 'joinCode', 'tokenpass', 'streamingOptions']; Meteor.methods({ saveRoomSettings(rid, settings, value) { if (!Meteor.userId()) { @@ -86,6 +86,11 @@ Meteor.methods({ RocketChat.saveRoomAnnouncement(rid, value, user); } break; + case 'roomCustomFields': + if (value !== room.customFields) { + RocketChat.saveRoomCustomFields(rid, value); + } + break; case 'roomDescription': if (value !== room.description) { RocketChat.saveRoomDescription(rid, value, user); diff --git a/packages/rocketchat-lib/server/models/Rooms.js b/packages/rocketchat-lib/server/models/Rooms.js index de9f17a8038f..43a1c4fe7396 100644 --- a/packages/rocketchat-lib/server/models/Rooms.js +++ b/packages/rocketchat-lib/server/models/Rooms.js @@ -692,6 +692,18 @@ class ModelRooms extends RocketChat.models._Base { return this.update(query, update); } + setCustomFieldsById(_id, customFields) { + const query = {_id}; + + const update = { + $set: { + customFields + } + }; + + return this.update(query, update); + } + muteUsernameByRoomId(_id, username) { const query = {_id}; diff --git a/packages/rocketchat-lib/server/models/Subscriptions.js b/packages/rocketchat-lib/server/models/Subscriptions.js index 116751db301d..a94968d41dcb 100644 --- a/packages/rocketchat-lib/server/models/Subscriptions.js +++ b/packages/rocketchat-lib/server/models/Subscriptions.js @@ -542,6 +542,18 @@ class ModelSubscriptions extends RocketChat.models._Base { return this.update(query, update) && this.update(query2, update2); } + updateCustomFieldsByRoomId(rid, cfields) { + const query = {rid}; + const customFields = cfields || {}; + const update = { + $set: { + customFields + } + }; + + return this.update(query, update, { multi: true }); + } + updateTypeByRoomId(roomId, type) { const query = { rid: roomId }; diff --git a/tests/end-to-end/api/02-channels.js b/tests/end-to-end/api/02-channels.js index a241a76be432..11540fd9e5e8 100644 --- a/tests/end-to-end/api/02-channels.js +++ b/tests/end-to-end/api/02-channels.js @@ -483,6 +483,189 @@ describe('[Channels]', function() { .end(done); }); + + describe('/channels.setCustomFields:', () => { + let cfchannel; + it('create channel with customFields', (done) => { + const customFields = {'field0':'value0'}; + request.post(api('channels.create')) + .set(credentials) + .send({ + name: `channel.cf.${ Date.now() }`, + customFields + }) + .end((err, res) => { + cfchannel = res.body.channel; + done(); + }); + }); + it('get customFields using channels.info', (done) => { + request.get(api('channels.info')) + .set(credentials) + .query({ + roomId: cfchannel._id + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('channel.customFields.field0', 'value0'); + }) + .end(done); + }); + it('change customFields', async(done) => { + const customFields = {'field9':'value9'}; + request.post(api('channels.setCustomFields')) + .set(credentials) + .send({ + roomId: cfchannel._id, + customFields + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('channel._id'); + expect(res.body).to.have.nested.property('channel.name', cfchannel.name); + expect(res.body).to.have.nested.property('channel.t', 'c'); + expect(res.body).to.have.nested.property('channel.customFields.field9', 'value9'); + expect(res.body).to.have.not.nested.property('channel.customFields.field0', 'value0'); + }) + .end(done); + }); + it('get customFields using channels.info', (done) => { + request.get(api('channels.info')) + .set(credentials) + .query({ + roomId: cfchannel._id + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('channel.customFields.field9', 'value9'); + }) + .end(done); + }); + it('delete channels with customFields', (done) => { + request.post(api('channels.delete')) + .set(credentials) + .send({ + roomName: cfchannel.name + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + it('create channel without customFields', (done) => { + request.post(api('channels.create')) + .set(credentials) + .send({ + name: `channel.cf.${ Date.now() }` + }) + .end((err, res) => { + cfchannel = res.body.channel; + done(); + }); + }); + it('set customFields with one nested field', async(done) => { + const customFields = {'field1':'value1'}; + request.post(api('channels.setCustomFields')) + .set(credentials) + .send({ + roomId: cfchannel._id, + customFields + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('channel._id'); + expect(res.body).to.have.nested.property('channel.name', cfchannel.name); + expect(res.body).to.have.nested.property('channel.t', 'c'); + expect(res.body).to.have.nested.property('channel.customFields.field1', 'value1'); + }) + .end(done); + }); + it('set customFields with multiple nested fields', async(done) => { + const customFields = {'field2':'value2', 'field3':'value3', 'field4':'value4'}; + + request.post(api('channels.setCustomFields')) + .set(credentials) + .send({ + roomName: cfchannel.name, + customFields + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('channel._id'); + expect(res.body).to.have.nested.property('channel.name', cfchannel.name); + expect(res.body).to.have.nested.property('channel.t', 'c'); + expect(res.body).to.have.nested.property('channel.customFields.field2', 'value2'); + expect(res.body).to.have.nested.property('channel.customFields.field3', 'value3'); + expect(res.body).to.have.nested.property('channel.customFields.field4', 'value4'); + expect(res.body).to.have.not.nested.property('channel.customFields.field1', 'value1'); + }) + .end(done); + }); + it('set customFields to empty object', async(done) => { + const customFields = {}; + + request.post(api('channels.setCustomFields')) + .set(credentials) + .send({ + roomName: cfchannel.name, + customFields + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('channel._id'); + expect(res.body).to.have.nested.property('channel.name', cfchannel.name); + expect(res.body).to.have.nested.property('channel.t', 'c'); + expect(res.body).to.have.not.nested.property('channel.customFields.field2', 'value2'); + expect(res.body).to.have.not.nested.property('channel.customFields.field3', 'value3'); + expect(res.body).to.have.not.nested.property('channel.customFields.field4', 'value4'); + }) + .end(done); + }); + it('set customFields as a string -> should return 400', async(done) => { + const customFields = ''; + + request.post(api('channels.setCustomFields')) + .set(credentials) + .send({ + roomName: cfchannel.name, + customFields + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + }) + .end(done); + }); + it('delete channel with empty customFields', (done) => { + request.post(api('channels.delete')) + .set(credentials) + .send({ + roomName: cfchannel.name + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + }); + it('/channels.setJoinCode', async(done) => { const roomInfo = await getRoomInfo(channel._id); diff --git a/tests/end-to-end/api/03-groups.js b/tests/end-to-end/api/03-groups.js index 8ce141a0ac32..534689471738 100644 --- a/tests/end-to-end/api/03-groups.js +++ b/tests/end-to-end/api/03-groups.js @@ -424,6 +424,188 @@ describe('groups', function() { .end(done); }); + describe('/groups.setCustomFields:', () => { + let cfchannel; + it('create group with customFields', (done) => { + const customFields = {'field0':'value0'}; + request.post(api('groups.create')) + .set(credentials) + .send({ + name: `channel.cf.${ Date.now() }`, + customFields + }) + .end((err, res) => { + cfchannel = res.body.group; + done(); + }); + }); + it('get customFields using groups.info', (done) => { + request.get(api('groups.info')) + .set(credentials) + .query({ + roomId: cfchannel._id + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group.customFields.field0', 'value0'); + }) + .end(done); + }); + it('change customFields', async(done) => { + const customFields = {'field9':'value9'}; + request.post(api('groups.setCustomFields')) + .set(credentials) + .send({ + roomId: cfchannel._id, + customFields + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', cfchannel.name); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.customFields.field9', 'value9'); + expect(res.body).to.have.not.nested.property('group.customFields.field0', 'value0'); + }) + .end(done); + }); + it('get customFields using groups.info', (done) => { + request.get(api('groups.info')) + .set(credentials) + .query({ + roomId: cfchannel._id + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group.customFields.field9', 'value9'); + }) + .end(done); + }); + it('delete group with customFields', (done) => { + request.post(api('groups.delete')) + .set(credentials) + .send({ + roomName: cfchannel.name + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + it('create group without customFields', (done) => { + request.post(api('groups.create')) + .set(credentials) + .send({ + name: `channel.cf.${ Date.now() }` + }) + .end((err, res) => { + cfchannel = res.body.group; + done(); + }); + }); + it('set customFields with one nested field', async(done) => { + const customFields = {'field1':'value1'}; + request.post(api('groups.setCustomFields')) + .set(credentials) + .send({ + roomId: cfchannel._id, + customFields + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', cfchannel.name); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.customFields.field1', 'value1'); + }) + .end(done); + }); + it('set customFields with multiple nested fields', async(done) => { + const customFields = {'field2':'value2', 'field3':'value3', 'field4':'value4'}; + + request.post(api('groups.setCustomFields')) + .set(credentials) + .send({ + roomName: cfchannel.name, + customFields + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', cfchannel.name); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.customFields.field2', 'value2'); + expect(res.body).to.have.nested.property('group.customFields.field3', 'value3'); + expect(res.body).to.have.nested.property('group.customFields.field4', 'value4'); + expect(res.body).to.have.not.nested.property('group.customFields.field1', 'value1'); + }) + .end(done); + }); + it('set customFields to empty object', async(done) => { + const customFields = {}; + + request.post(api('groups.setCustomFields')) + .set(credentials) + .send({ + roomName: cfchannel.name, + customFields + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', cfchannel.name); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.not.nested.property('group.customFields.field2', 'value2'); + expect(res.body).to.have.not.nested.property('group.customFields.field3', 'value3'); + expect(res.body).to.have.not.nested.property('group.customFields.field4', 'value4'); + }) + .end(done); + }); + it('set customFields as a string -> should return 400', async(done) => { + const customFields = ''; + + request.post(api('groups.setCustomFields')) + .set(credentials) + .send({ + roomName: cfchannel.name, + customFields + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + }) + .end(done); + }); + it('delete group with empty customFields', (done) => { + request.post(api('groups.delete')) + .set(credentials) + .send({ + roomName: cfchannel.name + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + }); + describe('/groups.delete', () => { let testGroup; it('/groups.create', (done) => {