Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/migrate-chat-delete-react.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/rest-typings": minor
---

Add OpenAPI support for the chat.delete and chat.react API endpoints by migrating to a modern chained route definition syntax and utilizing AJV schemas for body and response validation.
217 changes: 153 additions & 64 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
isChatGetURLPreviewProps,
isChatUpdateProps,
isChatGetThreadsListProps,
isChatDeleteProps,
isChatSyncMessagesProps,
isChatGetMessageProps,
isChatPostMessageProps,
Expand All @@ -17,7 +16,6 @@ import {
isChatIgnoreUserProps,
isChatGetPinnedMessagesProps,
isChatGetMentionedMessagesProps,
isChatReactProps,
isChatGetDeletedMessagesProps,
isChatSyncThreadsListProps,
isChatGetThreadMessagesProps,
Expand Down Expand Up @@ -127,46 +125,6 @@ const isChatFollowMessageLocalProps = ajv.compile<ChatFollowMessageLocal>(ChatFo

const isChatUnfollowMessageLocalProps = ajv.compile<ChatUnfollowMessageLocal>(ChatUnfollowMessageLocalSchema);

API.v1.addRoute(
'chat.delete',
{ authRequired: true, validateParams: isChatDeleteProps },
{
async post() {
const msg = await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } });

if (!msg) {
return API.v1.failure(`No message found with the id of "${this.bodyParams.msgId}".`);
}

if (this.bodyParams.roomId !== msg.rid) {
return API.v1.failure('The room id provided does not match where the message is from.');
}

if (
this.bodyParams.asUser &&
msg.u._id !== this.userId &&
!(await hasPermissionAsync(this.userId, 'force-delete-message', msg.rid))
) {
return API.v1.failure('Unauthorized. You must have the permission "force-delete-message" to delete other\'s message as them.');
}

const userId = this.bodyParams.asUser ? msg.u._id : this.userId;
const user = await Users.findOneById(userId, { projection: { _id: 1 } });

if (!user) {
return API.v1.failure('User not found');
}

await deleteMessageValidatingPermission(msg, user._id);

return API.v1.success({
_id: msg._id,
ts: Date.now().toString(),
message: msg,
});
},
},
);

API.v1.addRoute(
'chat.syncMessages',
Expand Down Expand Up @@ -275,6 +233,56 @@ const isChatPinMessageProps = ajv.compile<ChatPinMessage>(ChatPinMessageSchema);

const isChatUnpinMessageProps = ajv.compile<ChatUnpinMessage>(ChatUnpinMessageSchema);

type ChatDeleteLocal = {
msgId: string;
roomId: string;
asUser?: boolean;
};

const ChatDeleteLocalSchema = {
type: 'object',
properties: {
msgId: { type: 'string' },
roomId: { type: 'string' },
asUser: { type: 'boolean', nullable: true },
},
required: ['msgId', 'roomId'],
additionalProperties: false,
};

const isChatDeleteLocalProps = ajv.compile<ChatDeleteLocal>(ChatDeleteLocalSchema);

type ChatReactLocal =
| { emoji: string; messageId: string; shouldReact?: boolean }
| { reaction: string; messageId: string; shouldReact?: boolean };

const ChatReactLocalSchema = {
oneOf: [
{
type: 'object',
properties: {
emoji: { type: 'string' },
messageId: { type: 'string', minLength: 1 },
shouldReact: { type: 'boolean', nullable: true },
},
required: ['emoji', 'messageId'],
additionalProperties: false,
},
{
type: 'object',
properties: {
reaction: { type: 'string' },
messageId: { type: 'string', minLength: 1 },
shouldReact: { type: 'boolean', nullable: true },
},
required: ['reaction', 'messageId'],
additionalProperties: false,
},
],
};

const isChatReactLocalProps = ajv.compile<ChatReactLocal>(ChatReactLocalSchema);

const chatEndpoints = API.v1
.post(
'chat.pinMessage',
Expand Down Expand Up @@ -419,6 +427,109 @@ const chatEndpoints = API.v1
});
},
)
.post(
'chat.delete',
{
authRequired: true,
body: isChatDeleteLocalProps,
response: {
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
200: ajv.compile<{ _id: string; ts: string; message: Pick<IMessage, '_id' | 'rid' | 'u'> }>({
type: 'object',
properties: {
_id: { type: 'string' },
ts: { type: 'string' },
message: {
type: 'object',
properties: {
_id: { type: 'string' },
rid: { type: 'string' },
u: { type: 'object' },
},
required: ['_id', 'rid', 'u'],
},
success: { type: 'boolean', enum: [true] },
},
required: ['_id', 'ts', 'message', 'success'],
additionalProperties: false,
}),
},
},
async function action() {
const msg = await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } });

if (!msg) {
return API.v1.failure(`No message found with the id of "${this.bodyParams.msgId}".`);
}

if (this.bodyParams.roomId !== msg.rid) {
return API.v1.failure('The room id provided does not match where the message is from.');
}

if (
this.bodyParams.asUser &&
msg.u._id !== this.userId &&
!(await hasPermissionAsync(this.userId, 'force-delete-message', msg.rid))
) {
return API.v1.failure('Unauthorized. You must have the permission "force-delete-message" to delete other\'s message as them.');
}

const userId = this.bodyParams.asUser ? msg.u._id : this.userId;
const user = await Users.findOneById(userId, { projection: { _id: 1 } });

if (!user) {
return API.v1.failure('User not found');
}

await deleteMessageValidatingPermission(msg, user._id);

return API.v1.success({
_id: msg._id,
ts: Date.now().toString(),
message: msg,
});
},
)
.post(
'chat.react',
{
authRequired: true,
body: isChatReactLocalProps,
response: {
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
200: ajv.compile<void>({
type: 'object',
properties: {
success: {
type: 'boolean',
enum: [true],
},
},
required: ['success'],
additionalProperties: false,
}),
},
},
async function action() {
const msg = await Messages.findOneById(this.bodyParams.messageId);

if (!msg) {
throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.');
}

const emoji = 'emoji' in this.bodyParams ? this.bodyParams.emoji : (this.bodyParams as { reaction: string }).reaction;

if (!emoji) {
throw new Meteor.Error('error-emoji-param-not-provided', 'The required "emoji" param is missing.');
}

await executeSetReaction(this.userId, emoji, msg, this.bodyParams.shouldReact);

return API.v1.success();
},
)
.post(
'chat.starMessage',
{
Expand Down Expand Up @@ -653,29 +764,7 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'chat.react',
{ authRequired: true, validateParams: isChatReactProps },
{
async post() {
const msg = await Messages.findOneById(this.bodyParams.messageId);

if (!msg) {
throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.');
}

const emoji = 'emoji' in this.bodyParams ? this.bodyParams.emoji : (this.bodyParams as { reaction: string }).reaction;

if (!emoji) {
throw new Meteor.Error('error-emoji-param-not-provided', 'The required "emoji" param is missing.');
}

await executeSetReaction(this.userId, emoji, msg, this.bodyParams.shouldReact);

return API.v1.success();
},
},
);

API.v1.addRoute(
'chat.reportMessage',
Expand Down
83 changes: 2 additions & 81 deletions packages/rest-typings/src/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,79 +218,6 @@ const ChatSyncThreadsListSchema = {

export const isChatSyncThreadsListProps = ajv.compile<ChatSyncThreadsList>(ChatSyncThreadsListSchema);

type ChatDelete = {
msgId: IMessage['_id'];
roomId: IRoom['_id'];
asUser?: boolean;
};

const ChatDeleteSchema = {
type: 'object',
properties: {
msgId: {
type: 'string',
},
roomId: {
type: 'string',
},
asUser: {
type: 'boolean',
nullable: true,
},
},
required: ['msgId', 'roomId'],
additionalProperties: false,
};

export const isChatDeleteProps = ajv.compile<ChatDelete>(ChatDeleteSchema);

type ChatReact =
| { emoji: string; messageId: IMessage['_id']; shouldReact?: boolean }
| { reaction: string; messageId: IMessage['_id']; shouldReact?: boolean };

const ChatReactSchema = {
oneOf: [
{
type: 'object',
properties: {
emoji: {
type: 'string',
},
messageId: {
type: 'string',
minLength: 1,
},
shouldReact: {
type: 'boolean',
nullable: true,
},
},
required: ['emoji', 'messageId'],
additionalProperties: false,
},
{
type: 'object',
properties: {
reaction: {
type: 'string',
},
messageId: {
type: 'string',
minLength: 1,
},
shouldReact: {
type: 'boolean',
nullable: true,
},
},
required: ['reaction', 'messageId'],
additionalProperties: false,
},
],
};

export const isChatReactProps = ajv.compile<ChatReact>(ChatReactSchema);

/**
* The param `ignore` cannot be boolean, since this is a GET method. Use strings 'true' or 'false' instead.
* @param {string} ignore
Expand Down Expand Up @@ -920,15 +847,9 @@ export type ChatEndpoints = {
};
};
};
'/v1/chat.delete': {
POST: (params: ChatDelete) => {
_id: string;
ts: string;
message: Pick<IMessage, '_id' | 'rid' | 'u'>;
};
};

'/v1/chat.react': {
POST: (params: ChatReact) => void;
POST: (params: { emoji: string; messageId: string; shouldReact?: boolean } | { reaction: string; messageId: string; shouldReact?: boolean }) => void;
};
'/v1/chat.ignoreUser': {
GET: (params: ChatIgnoreUser) => void;
Expand Down