chore: Deprecate `channels.images` in favor of `rooms.images` (#32141)

pull/31655/head^2
Kevin Aleman 2 years ago committed by GitHub
parent 28b4678d21
commit 9902554388
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      .changeset/slow-cows-dance.md
  2. 2
      apps/meteor/app/api/server/api.ts
  3. 1
      apps/meteor/app/api/server/definition.ts
  4. 4
      apps/meteor/app/api/server/v1/channels.ts
  5. 48
      apps/meteor/app/api/server/v1/rooms.ts
  6. 2
      apps/meteor/client/lib/lists/ImagesList.ts
  7. 6
      apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts
  8. 142
      apps/meteor/tests/end-to-end/api/09-rooms.js
  9. 14
      packages/rest-typings/src/v1/channels/ChannelsImagesProps.ts
  10. 4
      packages/rest-typings/src/v1/channels/channels.ts
  11. 1
      packages/rest-typings/src/v1/channels/index.ts
  12. 38
      packages/rest-typings/src/v1/rooms.ts

@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/rest-typings": patch
---
Deprecate `channels.images` in favor of `rooms.images`. `Rooms` endpoints are more broad and should interact with all types of rooms. `Channels` on the other hand are specific to public channels.
This change is to keep the semantics and conventions of the endpoints

@ -589,7 +589,7 @@ export class APIClass<TBasePath extends string = ''> extends Restivus {
try {
if (options.deprecationVersion) {
apiDeprecationLogger.endpoint(this.request.route, options.deprecationVersion, this.response);
apiDeprecationLogger.endpoint(this.request.route, options.deprecationVersion, this.response, options.deprecationInfo || '');
}
await api.enforceRateLimit(objectForRateLimitMatch, this.request, this.response, this.userId);

@ -96,6 +96,7 @@ export type Options = (
validateParams?: ValidateFunction | { [key in Method]?: ValidateFunction };
authOrAnonRequired?: true;
deprecationVersion?: string;
deprecationInfo?: string;
};
export type PartialThis = {

@ -18,7 +18,7 @@ import {
isChannelsConvertToTeamProps,
isChannelsSetReadOnlyProps,
isChannelsDeleteProps,
isChannelsImagesProps,
isRoomsImagesProps,
} from '@rocket.chat/rest-typings';
import { Meteor } from 'meteor/meteor';
@ -806,7 +806,7 @@ API.v1.addRoute(
API.v1.addRoute(
'channels.images',
{ authRequired: true, validateParams: isChannelsImagesProps },
{ authRequired: true, validateParams: isRoomsImagesProps, deprecationVersion: '7.0.0', deprecationInfo: 'Use /v1/rooms.images instead.' },
{
async get() {
const room = await Rooms.findOneById<Pick<IRoom, '_id' | 't' | 'teamId' | 'prid'>>(this.queryParams.roomId, {

@ -1,8 +1,8 @@
import { Media } from '@rocket.chat/core-services';
import type { IRoom } from '@rocket.chat/core-typings';
import { Messages, Rooms, Users } from '@rocket.chat/models';
import type { IRoom, IUpload } from '@rocket.chat/core-typings';
import { Messages, Rooms, Users, Uploads } from '@rocket.chat/models';
import type { Notifications } from '@rocket.chat/rest-typings';
import { isGETRoomsNameExists } from '@rocket.chat/rest-typings';
import { isGETRoomsNameExists, isRoomsImagesProps } from '@rocket.chat/rest-typings';
import { Meteor } from 'meteor/meteor';
import { isTruthy } from '../../../../lib/isTruthy';
@ -386,6 +386,48 @@ API.v1.addRoute(
},
);
API.v1.addRoute(
'rooms.images',
{ authRequired: true, validateParams: isRoomsImagesProps },
{
async get() {
const room = await Rooms.findOneById<Pick<IRoom, '_id' | 't' | 'teamId' | 'prid'>>(this.queryParams.roomId, {
projection: { t: 1, teamId: 1, prid: 1 },
});
if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) {
return API.v1.unauthorized();
}
let initialImage: IUpload | null = null;
if (this.queryParams.startingFromId) {
initialImage = await Uploads.findOneById(this.queryParams.startingFromId);
}
const { offset, count } = await getPaginationItems(this.queryParams);
const { cursor, totalCount } = Uploads.findImagesByRoomId(room._id, initialImage?.uploadedAt, {
skip: offset,
limit: count,
});
const [files, total] = await Promise.all([cursor.toArray(), totalCount]);
// If the initial image was not returned in the query, insert it as the first element of the list
if (initialImage && !files.find(({ _id }) => _id === (initialImage as IUpload)._id)) {
files.splice(0, 0, initialImage);
}
return API.v1.success({
files,
count,
offset,
total,
});
},
},
);
API.v1.addRoute(
'rooms.adminRooms',
{ authRequired: true },

@ -6,7 +6,7 @@ type FilesMessage = Omit<IUpload, 'rid'> & Required<Pick<IUpload, 'rid'>>;
export type ImagesListOptions = {
roomId: Required<IUpload>['rid'];
startingFromId: string;
startingFromId?: string;
count?: number;
offset?: number;
};

@ -1,4 +1,4 @@
import type { ChannelsImagesProps } from '@rocket.chat/rest-typings';
import type { RoomsImagesProps } from '@rocket.chat/rest-typings';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useCallback, useEffect, useState } from 'react';
@ -7,7 +7,7 @@ import { useComponentDidUpdate } from '../../../../hooks/useComponentDidUpdate';
import { ImagesList } from '../../../../lib/lists/ImagesList';
export const useImagesList = (
options: ChannelsImagesProps,
options: RoomsImagesProps,
): {
filesList: ImagesList;
initialItemCount: number;
@ -27,7 +27,7 @@ export const useImagesList = (
}
}, [filesList, options]);
const apiEndPoint = '/v1/channels.images';
const apiEndPoint = '/v1/rooms.images';
const getFiles = useEndpoint('GET', apiEndPoint);

@ -1907,4 +1907,146 @@ describe('[Rooms]', function () {
});
});
});
describe('rooms.images', () => {
let testUserCreds = null;
before(async () => {
const user = await createUser();
testUserCreds = await login(user.username, password);
});
const uploadFile = async ({ roomId, file }) => {
const { body } = await request
.post(api(`rooms.upload/${roomId}`))
.set(credentials)
.attach('file', file)
.expect('Content-Type', 'application/json')
.expect(200);
return body.message.attachments[0];
};
const getIdFromImgPath = (link) => {
return link.split('/')[2];
};
it('should return an error when user is not logged in', async () => {
await request.get(api('rooms.images')).expect(401);
});
it('should return an error when the required parameter "roomId" is not provided', async () => {
await request.get(api('rooms.images')).set(credentials).expect(400);
});
it('should return an error when the required parameter "roomId" is not a valid room', async () => {
await request.get(api('rooms.images')).set(credentials).query({ roomId: 'invalid' }).expect(403);
});
it('should return an error when room is valid but user is not part of it', async () => {
const { body } = await createRoom({ type: 'p', name: `test-${Date.now()}` });
const {
group: { _id: roomId },
} = body;
await request.get(api('rooms.images')).set(testUserCreds).query({ roomId }).expect(403);
await deleteRoom({ type: 'p', roomId });
});
it('should return an empty array when room is valid and user is part of it but there are no images', async () => {
const { body } = await createRoom({ type: 'p', usernames: [credentials.username], name: `test-${Date.now()}` });
const {
group: { _id: roomId },
} = body;
await request
.get(api('rooms.images'))
.set(credentials)
.query({ roomId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('files').and.to.be.an('array').and.to.have.lengthOf(0);
});
await deleteRoom({ type: 'p', roomId });
});
it('should return an array of images when room is valid and user is part of it and there are images', async () => {
const { body } = await createRoom({ type: 'p', usernames: [credentials.username], name: `test-${Date.now()}` });
const {
group: { _id: roomId },
} = body;
const { title_link } = await uploadFile({
roomId,
file: fs.createReadStream(path.join(process.cwd(), imgURL)),
});
const fileId = getIdFromImgPath(title_link);
await request
.get(api('rooms.images'))
.set(credentials)
.query({ roomId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('files').and.to.be.an('array').and.to.have.lengthOf(1);
expect(res.body.files[0]).to.have.property('_id', fileId);
});
await deleteRoom({ type: 'p', roomId });
});
it('should return multiple images when room is valid and user is part of it and there are multiple images', async () => {
const { body } = await createRoom({ type: 'p', usernames: [credentials.username], name: `test-${Date.now()}` });
const {
group: { _id: roomId },
} = body;
const { title_link: link1 } = await uploadFile({
roomId,
file: fs.createReadStream(path.join(process.cwd(), imgURL)),
});
const { title_link: link2 } = await uploadFile({
roomId,
file: fs.createReadStream(path.join(process.cwd(), imgURL)),
});
const fileId1 = getIdFromImgPath(link1);
const fileId2 = getIdFromImgPath(link2);
await request
.get(api('rooms.images'))
.set(credentials)
.query({ roomId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('files').and.to.be.an('array').and.to.have.lengthOf(2);
expect(res.body.files.find((file) => file._id === fileId1)).to.exist;
expect(res.body.files.find((file) => file._id === fileId2)).to.exist;
});
await deleteRoom({ type: 'p', roomId });
});
it('should allow to filter images passing the startingFromId parameter', async () => {
const { body } = await createRoom({ type: 'p', usernames: [credentials.username], name: `test-${Date.now()}` });
const {
group: { _id: roomId },
} = body;
const { title_link } = await uploadFile({
roomId,
file: fs.createReadStream(path.join(process.cwd(), imgURL)),
});
await uploadFile({
roomId,
file: fs.createReadStream(path.join(process.cwd(), imgURL)),
});
const fileId2 = getIdFromImgPath(title_link);
await request
.get(api('rooms.images'))
.set(credentials)
.query({ roomId, startingFromId: fileId2 })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('files').and.to.be.an('array').and.to.have.lengthOf(1);
expect(res.body.files[0]).to.have.property('_id', fileId2);
});
await deleteRoom({ type: 'p', roomId });
});
});
});

@ -1,14 +0,0 @@
import Ajv from 'ajv';
const ajv = new Ajv({
coerceTypes: true,
});
export type ChannelsImagesProps = {
roomId: string;
startingFromId: string;
count?: number;
offset?: number;
};
const channelsImagesPropsSchema = {};
export const isChannelsImagesProps = ajv.compile<ChannelsImagesProps>(channelsImagesPropsSchema);

@ -2,6 +2,7 @@ import type { IUpload, IUploadWithUser, IMessage, IRoom, ITeam, IGetRoomRoles, I
import type { PaginatedRequest } from '../../helpers/PaginatedRequest';
import type { PaginatedResult } from '../../helpers/PaginatedResult';
import type { RoomsImagesProps } from '../rooms';
import type { ChannelsAddAllProps } from './ChannelsAddAllProps';
import type { ChannelsArchiveProps } from './ChannelsArchiveProps';
import type { ChannelsConvertToTeamProps } from './ChannelsConvertToTeamProps';
@ -10,7 +11,6 @@ import type { ChannelsDeleteProps } from './ChannelsDeleteProps';
import type { ChannelsGetAllUserMentionsByChannelProps } from './ChannelsGetAllUserMentionsByChannelProps';
import type { ChannelsGetIntegrationsProps } from './ChannelsGetIntegrationsProps';
import type { ChannelsHistoryProps } from './ChannelsHistoryProps';
import type { ChannelsImagesProps } from './ChannelsImagesProps';
import type { ChannelsInviteProps } from './ChannelsInviteProps';
import type { ChannelsJoinProps } from './ChannelsJoinProps';
import type { ChannelsKickProps } from './ChannelsKickProps';
@ -40,7 +40,7 @@ export type ChannelsEndpoints = {
}>;
};
'/v1/channels.images': {
GET: (params: ChannelsImagesProps) => PaginatedResult<{
GET: (params: RoomsImagesProps) => PaginatedResult<{
files: IUpload[];
}>;
};

@ -7,7 +7,6 @@ export * from './ChannelsCreateProps';
export * from './ChannelsDeleteProps';
export * from './ChannelsGetAllUserMentionsByChannelProps';
export * from './ChannelsHistoryProps';
export * from './ChannelsImagesProps';
export * from './ChannelsJoinProps';
export * from './ChannelsKickProps';
export * from './ChannelsLeaveProps';

@ -1,4 +1,4 @@
import type { IMessage, IRoom, IUser, RoomAdminFieldsType } from '@rocket.chat/core-typings';
import type { IMessage, IRoom, IUser, RoomAdminFieldsType, IUpload } from '@rocket.chat/core-typings';
import Ajv from 'ajv';
import type { PaginatedRequest } from '../helpers/PaginatedRequest';
@ -436,6 +436,37 @@ export type Notifications = {
type RoomsGetDiscussionsProps = PaginatedRequest<BaseRoomsProps>;
export type RoomsImagesProps = {
roomId: string;
startingFromId?: string;
count?: number;
offset?: number;
};
const roomsImagesPropsSchema = {
type: 'object',
properties: {
roomId: {
type: 'string',
},
startingFromId: {
type: 'string',
nullable: true,
},
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
},
required: ['roomId'],
additionalProperties: false,
};
export const isRoomsImagesProps = ajv.compile<RoomsImagesProps>(roomsImagesPropsSchema);
export type RoomsEndpoints = {
'/v1/rooms.autocomplete.channelAndPrivate': {
GET: (params: RoomsAutoCompleteChannelAndPrivateProps) => {
@ -573,4 +604,9 @@ export type RoomsEndpoints = {
discussions: IRoom[];
}>;
};
'/v1/rooms.images': {
GET: (params: RoomsImagesProps) => PaginatedResult<{
files: IUpload[];
}>;
};
};

Loading…
Cancel
Save