diff --git a/.changeset/nasty-countries-live.md b/.changeset/nasty-countries-live.md new file mode 100644 index 0000000000000..c474a69f9aabc --- /dev/null +++ b/.changeset/nasty-countries-live.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Fixes file filtering by name or type not working for non-private channels. diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index ea2b3ef840ba5..fd972dbf7f142 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -1,7 +1,7 @@ import { Team, isMeteorError } from '@rocket.chat/core-services'; import type { IIntegration, IUser, IRoom, RoomType, UserStatus } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; -import { isGroupsOnlineProps, isGroupsMessagesProps } from '@rocket.chat/rest-typings'; +import { isGroupsOnlineProps, isGroupsMessagesProps, isGroupsFilesProps } from '@rocket.chat/rest-typings'; import { check, Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; @@ -389,11 +389,13 @@ API.v1.addRoute( API.v1.addRoute( 'groups.files', - { authRequired: true }, + { authRequired: true, validateParams: isGroupsFilesProps }, { async get() { + const { typeGroup, name, roomId, roomName } = this.queryParams; + const findResult = await findPrivateGroupByIdOrName({ - params: this.queryParams, + params: roomId ? { roomId } : { roomName }, userId: this.userId, checkedArchived: false, }); @@ -401,9 +403,14 @@ API.v1.addRoute( const { offset, count } = await getPaginationItems(this.queryParams); const { sort, fields, query } = await this.parseJsonQuery(); - const ourQuery = Object.assign({}, query, { rid: findResult.rid }); + const filter = { + ...query, + rid: findResult.rid, + ...(name ? { name: { $regex: name || '', $options: 'i' } } : {}), + ...(typeGroup ? { typeGroup } : {}), + }; - const { cursor, totalCount } = await Uploads.findPaginatedWithoutThumbs(ourQuery, { + const { cursor, totalCount } = await Uploads.findPaginatedWithoutThumbs(filter, { sort: sort || { name: 1 }, skip: offset, limit: count, diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index 5f196263e1dfe..68b6f0a09b161 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -31,21 +31,12 @@ import { addUserToFileObj } from '../helpers/addUserToFileObj'; import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage'; import { getPaginationItems } from '../helpers/getPaginationItems'; -// TODO: Refact or remove - -type findDirectMessageRoomProps = - | { - roomId: string; - } - | { - username: string; - }; - const findDirectMessageRoom = async ( - keys: findDirectMessageRoomProps, + keys: { roomId?: string; username?: string }, uid: string, ): Promise<{ room: IRoom; subscription: ISubscription | null }> => { - if (!('roomId' in keys) && !('username' in keys)) { + const nameOrId = 'roomId' in keys ? keys.roomId : keys.username; + if (typeof nameOrId !== 'string') { throw new Meteor.Error('error-room-param-not-provided', 'Query param "roomId" or "username" is required'); } @@ -58,7 +49,7 @@ const findDirectMessageRoom = async ( const room = await getRoomByNameOrIdWithOptionToJoin({ user, - nameOrId: 'roomId' in keys ? keys.roomId : keys.username, + nameOrId, type: 'd', }); @@ -234,19 +225,26 @@ API.v1.addRoute( }, { async get() { + const { typeGroup, name, roomId, username } = this.queryParams; + const { offset, count } = await getPaginationItems(this.queryParams); const { sort, fields, query } = await this.parseJsonQuery(); - const { room } = await findDirectMessageRoom(this.queryParams, this.userId); + const { room } = await findDirectMessageRoom(roomId ? { roomId } : { username }, this.userId); const canAccess = await canAccessRoomIdAsync(room._id, this.userId); if (!canAccess) { return API.v1.forbidden(); } - const ourQuery = query ? { rid: room._id, ...query } : { rid: room._id }; + const filter = { + ...query, + rid: room._id, + ...(name ? { name: { $regex: name || '', $options: 'i' } } : {}), + ...(typeGroup ? { typeGroup } : {}), + }; - const { cursor, totalCount } = Uploads.findPaginatedWithoutThumbs(ourQuery, { + const { cursor, totalCount } = Uploads.findPaginatedWithoutThumbs(filter, { sort: sort || { name: 1 }, skip: offset, limit: count, diff --git a/apps/meteor/tests/data/interactions.ts b/apps/meteor/tests/data/interactions.ts index 9d5a69ce95935..e500673dc05cb 100644 --- a/apps/meteor/tests/data/interactions.ts +++ b/apps/meteor/tests/data/interactions.ts @@ -1 +1,2 @@ export const imgURL = './public/images/logo/1024x1024.png'; +export const soundURL = './public/sounds/beep.mp3'; diff --git a/apps/meteor/tests/data/uploads.helper.ts b/apps/meteor/tests/data/uploads.helper.ts index 417e34bb9c7b1..ae1641f9c1cf5 100644 --- a/apps/meteor/tests/data/uploads.helper.ts +++ b/apps/meteor/tests/data/uploads.helper.ts @@ -4,7 +4,7 @@ import { after, before, it } from 'mocha'; import type { Response } from 'supertest'; import { api, request, credentials } from './api-data'; -import { imgURL } from './interactions'; +import { imgURL, soundURL } from './interactions'; import { createVisitor } from './livechat/rooms'; import { updateSetting } from './permissions.helper'; import { createRoom, deleteRoom } from './rooms.helper'; @@ -241,4 +241,87 @@ export async function testFileUploads( }); }); }); + + it('should properly filter files by name or typeGroup', async () => { + const fileOneName = 'image-zyxwv.png'; + const fileTwoName = 'sound-abcde.png'; + const fileIdsToConfirm: string[] = []; + + // Post 2 files, one image and one audio + await Promise.all([ + request + .post(api(`rooms.media/${testRoom._id}`)) + .set(credentials) + .attach('file', imgURL, { filename: fileOneName }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(typeof res.body?.file?._id).to.equal('string'); + fileIdsToConfirm.push(res.body.file._id); + }), + request + .post(api(`rooms.media/${testRoom._id}`)) + .set(credentials) + .attach('file', soundURL, { filename: fileTwoName }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(typeof res.body?.file?._id).to.equal('string'); + fileIdsToConfirm.push(res.body.file._id); + }), + ]); + + // Confirm the files + await Promise.all( + fileIdsToConfirm.map((fileId) => + request + .post(api(`rooms.mediaConfirm/${testRoom._id}/${fileId}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200), + ), + ); + + // test filtering by name + const nameFilterTest = request + .get(api(filesEndpoint)) + .set(credentials) + .query({ + roomId: testRoom._id, + name: fileOneName, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('files').and.to.be.an('array').with.lengthOf(1); + + const { files } = res.body; + + expect(files[0].name).to.equal(fileOneName); + }); + + // test filtering by typeGroup + const typeGroupFilterTest = request + .get(api(filesEndpoint)) + .set(credentials) + .query({ + roomId: testRoom._id, + typeGroup: 'audio', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('files').and.to.be.an('array').with.lengthOf(1); + + const { files } = res.body; + + expect(files[0].name).to.equal(fileTwoName); + }); + + await Promise.all([nameFilterTest, typeGroupFilterTest]); + }); } diff --git a/packages/rest-typings/src/v1/dm/DmFileProps.ts b/packages/rest-typings/src/v1/dm/DmFileProps.ts index fa89a05523bdd..019a3e30514bb 100644 --- a/packages/rest-typings/src/v1/dm/DmFileProps.ts +++ b/packages/rest-typings/src/v1/dm/DmFileProps.ts @@ -5,61 +5,48 @@ import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; const ajv = new Ajv({ coerceTypes: true }); export type DmFileProps = PaginatedRequest< - ( - | { - roomId: string; - } - | { - username: string; - } - ) & { fields?: string } + ({ roomId: string; username?: string } | { roomId?: string; username: string }) & { name?: string; typeGroup?: string; query?: string } >; -export const isDmFileProps = ajv.compile({ - oneOf: [ - { - type: 'object', - properties: { - roomId: { - type: 'string', - }, - query: { - type: 'string', - }, - sort: { - type: 'string', - }, - count: { - type: 'number', - }, - offset: { - type: 'number', - }, - }, - required: ['roomId'], - additionalProperties: false, +const dmFilesListPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + nullable: true, }, - { - type: 'object', - properties: { - username: { - type: 'string', - }, - query: { - type: 'string', - }, - sort: { - type: 'string', - }, - count: { - type: 'number', - }, - offset: { - type: 'number', - }, - }, - required: ['username'], - additionalProperties: false, + username: { + type: 'string', + nullable: true, }, - ], -}); + offset: { + type: 'number', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + name: { + type: 'string', + nullable: true, + }, + typeGroup: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + oneOf: [{ required: ['roomId'] }, { required: ['username'] }], + required: [], + additionalProperties: false, +}; + +export const isDmFileProps = ajv.compile(dmFilesListPropsSchema); diff --git a/packages/rest-typings/src/v1/groups/BaseProps.ts b/packages/rest-typings/src/v1/groups/BaseProps.ts index 074c430699198..cb102f9450ed7 100644 --- a/packages/rest-typings/src/v1/groups/BaseProps.ts +++ b/packages/rest-typings/src/v1/groups/BaseProps.ts @@ -4,7 +4,7 @@ const ajv = new Ajv({ coerceTypes: true, }); -export type GroupsBaseProps = { roomId: string } | { roomName: string }; +export type GroupsBaseProps = { roomId: string; roomName?: string } | { roomId?: string; roomName: string }; export const withGroupBaseProperties = (properties: Record = {}, required: string[] = []) => ({ oneOf: [ diff --git a/packages/rest-typings/src/v1/groups/GroupsFilesProps.ts b/packages/rest-typings/src/v1/groups/GroupsFilesProps.ts index fc5ba3b73e622..bfbd83d7388a7 100644 --- a/packages/rest-typings/src/v1/groups/GroupsFilesProps.ts +++ b/packages/rest-typings/src/v1/groups/GroupsFilesProps.ts @@ -1,32 +1,56 @@ import Ajv from 'ajv'; import type { GroupsBaseProps } from './BaseProps'; -import { withGroupBaseProperties } from './BaseProps'; import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; const ajv = new Ajv({ coerceTypes: true, }); -export type GroupsFilesProps = PaginatedRequest; +export type GroupsFilesProps = PaginatedRequest & { + name?: string; + typeGroup?: string; +}; -const GroupsFilesPropsSchema = withGroupBaseProperties({ - count: { - type: 'number', - nullable: true, +const GroupsFilesPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + nullable: true, + }, + roomName: { + type: 'string', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + name: { + type: 'string', + nullable: true, + }, + typeGroup: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, }, - sort: { - type: 'string', - nullable: true, - }, - query: { - type: 'string', - nullable: true, - }, - offset: { - type: 'number', - nullable: true, - }, -}); + oneOf: [{ required: ['roomId'] }, { required: ['roomName'] }], + required: [], + additionalProperties: true, // keep additional properties for backwards compatibility, otherwise this would be a breaking change +}; export const isGroupsFilesProps = ajv.compile(GroupsFilesPropsSchema);