Skip to content
6 changes: 6 additions & 0 deletions .changeset/fast-walls-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/rest-typings": patch
---

Add OpenAPI support for the Rocket.Chat chat.pinMessage API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation.
100 changes: 86 additions & 14 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { Message } from '@rocket.chat/core-services';
import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings';
import { Messages, Users, Rooms, Subscriptions } from '@rocket.chat/models';
import {
ajv,
isChatReportMessageProps,
isChatGetURLPreviewProps,
isChatUpdateProps,
isChatGetThreadsListProps,
isChatDeleteProps,
isChatSyncMessagesProps,
isChatGetMessageProps,
isChatPinMessageProps,
isChatPostMessageProps,
isChatSearchProps,
isChatSendMessageProps,
Expand Down Expand Up @@ -56,6 +56,7 @@ import { followMessage } from '../../../threads/server/methods/followMessage';
import { unfollowMessage } from '../../../threads/server/methods/unfollowMessage';
import { MessageTypes } from '../../../ui-utils/server';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import type { ExtractRoutesFromAPI } from '../ApiClass';
import { API } from '../api';
import { getPaginationItems } from '../helpers/getPaginationItems';
import { findDiscussionsFromRoom, findMentionedMessages, findStarredMessages } from '../lib/messages';
Expand Down Expand Up @@ -172,25 +173,87 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
type ChatPinMessage = {
messageId: IMessage['_id'];
};

const ChatPinMessageSchema = {
type: 'object',
properties: {
messageId: {
type: 'string',
minLength: 1,
},
},
required: ['messageId'],
additionalProperties: false,
};

const isChatPinMessageProps = ajv.compile<ChatPinMessage>(ChatPinMessageSchema);

const chatPinMessageEndpoints = API.v1.post(
'chat.pinMessage',
{ authRequired: true, validateParams: isChatPinMessageProps },
{
async post() {
const msg = await Messages.findOneById(this.bodyParams.messageId);
authRequired: true,
body: isChatPinMessageProps,
response: {
400: ajv.compile<{
error?: string;
errorType?: string;
stack?: string;
details?: string;
}>({
type: 'object',
properties: {
success: { type: 'boolean', enum: [false] },
stack: { type: 'string' },
error: { type: 'string' },
errorType: { type: 'string' },
details: { type: 'string' },
},
required: ['success'],
additionalProperties: false,
}),
401: ajv.compile({
type: 'object',
properties: {
success: { type: 'boolean', enum: [false] },
status: { type: 'string' },
message: { type: 'string' },
error: { type: 'string' },
errorType: { type: 'string' },
},
required: ['success'],
additionalProperties: false,
}),
200: ajv.compile<{ message: IMessage }>({
type: 'object',
properties: {
message: { $ref: '#/components/schemas/IMessage' },
success: {
type: 'boolean',
enum: [true],
},
},
required: ['message', '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.');
}
if (!msg) {
throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.');
}

const pinnedMessage = await pinMessage(msg, this.userId);
const pinnedMessage = await pinMessage(msg, this.userId);

const [message] = await normalizeMessagesForUser([pinnedMessage], this.userId);
const [message] = await normalizeMessagesForUser([pinnedMessage], this.userId);

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

Expand Down Expand Up @@ -845,3 +908,12 @@ API.v1.addRoute(
},
},
);

type ChatPinMessageEndpoints = ExtractRoutesFromAPI<typeof chatPinMessageEndpoints>;

export type ChatEndpoints = ChatPinMessageEndpoints;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends ChatPinMessageEndpoints {}
}
3 changes: 2 additions & 1 deletion packages/core-typings/src/Ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import typia from 'typia';

import type { ICustomSound } from './ICustomSound';
import type { IInvite } from './IInvite';
import type { IMessage } from './IMessage';
import type { ISubscription } from './ISubscription';

export const schemas = typia.json.schemas<[ISubscription | IInvite | ICustomSound], '3.0'>();
export const schemas = typia.json.schemas<[ISubscription | IInvite | ICustomSound | IMessage], '3.0'>();
2 changes: 1 addition & 1 deletion packages/core-typings/src/IMessage/IMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export interface IMessage extends IRocketChatRecord {
attachments?: MessageAttachment[];

reactions?: {
[key: string]: { names?: (string | undefined)[]; usernames: string[]; federationReactionEventIds?: Record<string, string> };
[key: string]: { names?: string[]; usernames: string[]; federationReactionEventIds?: Record<string, string> };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While working with Typia to generate schemas for IMessage. I ran into an error with this type undefined. typia doesnt allow undefined inside the interface

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};

private?: boolean;
Expand Down
23 changes: 0 additions & 23 deletions packages/rest-typings/src/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,24 +175,6 @@ const ChatUnstarMessageSchema = {

export const isChatUnstarMessageProps = ajv.compile<ChatUnstarMessage>(ChatUnstarMessageSchema);

type ChatPinMessage = {
messageId: IMessage['_id'];
};

const ChatPinMessageSchema = {
type: 'object',
properties: {
messageId: {
type: 'string',
minLength: 1,
},
},
required: ['messageId'],
additionalProperties: false,
};

export const isChatPinMessageProps = ajv.compile<ChatPinMessage>(ChatPinMessageSchema);

type ChatUnpinMessage = {
messageId: IMessage['_id'];
};
Expand Down Expand Up @@ -952,11 +934,6 @@ export type ChatEndpoints = {
'/v1/chat.unStarMessage': {
POST: (params: ChatUnstarMessage) => void;
};
'/v1/chat.pinMessage': {
POST: (params: ChatPinMessage) => {
message: IMessage;
};
};
'/v1/chat.unPinMessage': {
POST: (params: ChatUnpinMessage) => void;
};
Expand Down
Loading