refactor: Convert chat endpoints to TS (#28417)

pull/28432/head^2
Marcos Spessatto Defendi 3 years ago committed by GitHub
parent f53b6aab82
commit 0edb79acac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 294
      apps/meteor/app/api/server/v1/chat.ts
  2. 1
      apps/meteor/tests/end-to-end/api/05-chat.js
  3. 1
      packages/rest-typings/src/index.ts
  4. 377
      packages/rest-typings/src/v1/chat.ts

@ -1,26 +1,23 @@
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { Messages as MessagesRaw } from '@rocket.chat/models';
import { Messages, Users, Rooms, Subscriptions } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { IMessage } from '@rocket.chat/core-typings';
import { Messages } from '../../../models/server';
import { canAccessRoom, canAccessRoomId, roomAccessAttributes, hasPermission } from '../../../authorization/server';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import { processWebhookMessage } from '../../../lib/server';
import { executeSendMessage } from '../../../lib/server/methods/sendMessage';
import { executeSetReaction } from '../../../reactions/server/setReaction';
import { API } from '../api';
import Rooms from '../../../models/server/models/Rooms';
import Users from '../../../models/server/models/Users';
import Subscriptions from '../../../models/server/models/Subscriptions';
import { processWebhookMessage } from '../../../lib/server';
import { settings } from '../../../settings/server';
import { findMentionedMessages, findStarredMessages, findDiscussionsFromRoom } from '../lib/messages';
import { executeSetReaction } from '../../../reactions/server/setReaction';
import { findDiscussionsFromRoom, findMentionedMessages, findStarredMessages } from '../lib/messages';
import { executeSendMessage } from '../../../lib/server/methods/sendMessage';
API.v1.addRoute(
'chat.delete',
{ authRequired: true },
{
post() {
async post() {
check(
this.bodyParams,
Match.ObjectIncluding({
@ -30,7 +27,7 @@ API.v1.addRoute(
}),
);
const msg = Messages.findOneById(this.bodyParams.msgId, { fields: { u: 1, rid: 1 } });
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}".`);
@ -44,13 +41,13 @@ API.v1.addRoute(
return API.v1.failure('Unauthorized. You must have the permission "force-delete-message" to delete other\'s message as them.');
}
Meteor.runAsUser(this.bodyParams.asUser ? msg.u._id : this.userId, () => {
await Meteor.runAsUser(this.bodyParams.asUser ? msg.u._id : this.userId, () => {
Meteor.call('deleteMessage', { _id: msg._id });
});
return API.v1.success({
_id: msg._id,
ts: Date.now(),
ts: Date.now().toString(),
message: msg,
});
},
@ -61,7 +58,7 @@ API.v1.addRoute(
'chat.syncMessages',
{ authRequired: true },
{
get() {
async get() {
const { roomId, lastUpdate } = this.queryParams;
if (!roomId) {
@ -74,10 +71,7 @@ API.v1.addRoute(
throw new Meteor.Error('error-roomId-param-invalid', 'The "lastUpdate" query parameter must be a valid date.');
}
let result;
Meteor.runAsUser(this.userId, () => {
result = Meteor.call('messages/get', roomId, { lastUpdate: new Date(lastUpdate) });
});
const result = await Meteor.call('messages/get', roomId, { lastUpdate: new Date(lastUpdate) });
if (!result) {
return API.v1.failure();
@ -85,7 +79,7 @@ API.v1.addRoute(
return API.v1.success({
result: {
updated: normalizeMessagesForUser(result.updated, this.userId),
updated: await normalizeMessagesForUser(result.updated, this.userId),
deleted: result.deleted,
},
});
@ -95,23 +89,22 @@ API.v1.addRoute(
API.v1.addRoute(
'chat.getMessage',
{ authRequired: true },
{
get() {
authRequired: true,
},
{
async get() {
if (!this.queryParams.msgId) {
return API.v1.failure('The "msgId" query parameter must be provided.');
}
let msg;
Meteor.runAsUser(this.userId, () => {
msg = Meteor.call('getSingleMessage', this.queryParams.msgId);
});
const msg = await Meteor.call('getSingleMessage', this.queryParams.msgId);
if (!msg) {
return API.v1.failure();
}
const [message] = normalizeMessagesForUser([msg], this.userId);
const [message] = await normalizeMessagesForUser([msg], this.userId);
return API.v1.success({
message,
@ -124,23 +117,20 @@ API.v1.addRoute(
'chat.pinMessage',
{ authRequired: true },
{
post() {
if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) {
async post() {
if (!this.bodyParams.messageId?.trim()) {
throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is missing.');
}
const msg = Messages.findOneById(this.bodyParams.messageId);
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.');
}
let pinnedMessage;
Meteor.runAsUser(this.userId, () => {
pinnedMessage = Meteor.call('pinMessage', msg);
});
const pinnedMessage = await Meteor.call('pinMessage', msg);
const [message] = normalizeMessagesForUser([pinnedMessage], this.userId);
const [message] = await normalizeMessagesForUser([pinnedMessage], this.userId);
return API.v1.success({
message,
@ -153,14 +143,14 @@ API.v1.addRoute(
'chat.postMessage',
{ authRequired: true },
{
post() {
const messageReturn = processWebhookMessage(this.bodyParams, this.user)[0];
async post() {
const messageReturn = await processWebhookMessage(this.bodyParams, this.user)[0];
if (!messageReturn) {
return API.v1.failure('unknown-error');
}
const [message] = normalizeMessagesForUser([messageReturn.message], this.userId);
const [message] = await normalizeMessagesForUser([messageReturn.message], this.userId);
return API.v1.success({
ts: Date.now(),
@ -175,7 +165,7 @@ API.v1.addRoute(
'chat.search',
{ authRequired: true },
{
get() {
async get() {
const { roomId, searchText } = this.queryParams;
const { offset, count } = this.getPaginationItems();
@ -187,13 +177,10 @@ API.v1.addRoute(
throw new Meteor.Error('error-searchText-param-not-provided', 'The required "searchText" query param is missing.');
}
let result;
Meteor.runAsUser(this.userId, () => {
result = Meteor.call('messageSearch', searchText, roomId, count, offset).message.docs;
});
const result = await Meteor.call('messageSearch', searchText, roomId, count, offset).message.docs;
return API.v1.success({
messages: normalizeMessagesForUser(result, this.userId),
messages: await normalizeMessagesForUser(result, this.userId),
});
},
},
@ -206,13 +193,13 @@ API.v1.addRoute(
'chat.sendMessage',
{ authRequired: true },
{
post() {
async post() {
if (!this.bodyParams.message) {
throw new Meteor.Error('error-invalid-params', 'The "message" parameter must be provided.');
}
const sent = executeSendMessage(this.userId, this.bodyParams.message);
const [message] = normalizeMessagesForUser([sent], this.userId);
const sent = await executeSendMessage(this.userId, this.bodyParams.message as Pick<IMessage, 'rid'>);
const [message] = await normalizeMessagesForUser([sent], this.userId);
return API.v1.success({
message,
@ -225,24 +212,22 @@ API.v1.addRoute(
'chat.starMessage',
{ authRequired: true },
{
post() {
if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) {
async post() {
if (!this.bodyParams.messageId?.trim()) {
throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.');
}
const msg = Messages.findOneById(this.bodyParams.messageId);
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.');
}
Meteor.runAsUser(this.userId, () =>
Meteor.call('starMessage', {
_id: msg._id,
rid: msg.rid,
starred: true,
}),
);
await Meteor.call('starMessage', {
_id: msg._id,
rid: msg.rid,
starred: true,
});
return API.v1.success();
},
@ -253,18 +238,18 @@ API.v1.addRoute(
'chat.unPinMessage',
{ authRequired: true },
{
post() {
if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) {
async post() {
if (!this.bodyParams.messageId?.trim()) {
throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.');
}
const msg = Messages.findOneById(this.bodyParams.messageId);
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.');
}
Meteor.runAsUser(this.userId, () => Meteor.call('unpinMessage', msg));
await Meteor.call('unpinMessage', msg);
return API.v1.success();
},
@ -275,24 +260,22 @@ API.v1.addRoute(
'chat.unStarMessage',
{ authRequired: true },
{
post() {
if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) {
async post() {
if (!this.bodyParams.messageId?.trim()) {
throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.');
}
const msg = Messages.findOneById(this.bodyParams.messageId);
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.');
}
Meteor.runAsUser(this.userId, () =>
Meteor.call('starMessage', {
_id: msg._id,
rid: msg.rid,
starred: false,
}),
);
await Meteor.call('starMessage', {
_id: msg._id,
rid: msg.rid,
starred: false,
});
return API.v1.success();
},
@ -303,7 +286,7 @@ API.v1.addRoute(
'chat.update',
{ authRequired: true },
{
post() {
async post() {
check(
this.bodyParams,
Match.ObjectIncluding({
@ -313,7 +296,7 @@ API.v1.addRoute(
}),
);
const msg = Messages.findOneById(this.bodyParams.msgId);
const msg = await Messages.findOneById(this.bodyParams.msgId);
// Ensure the message exists
if (!msg) {
@ -325,11 +308,10 @@ API.v1.addRoute(
}
// Permission checks are already done in the updateMessage method, so no need to duplicate them
Meteor.runAsUser(this.userId, () => {
Meteor.call('updateMessage', { _id: msg._id, msg: this.bodyParams.text, rid: msg.rid });
});
await Meteor.call('updateMessage', { _id: msg._id, msg: this.bodyParams.text, rid: msg.rid });
const [message] = normalizeMessagesForUser([Messages.findOneById(msg._id)], this.userId);
const updatedMessage = await Messages.findOneById(msg._id);
const [message] = await normalizeMessagesForUser(updatedMessage ? [updatedMessage] : [], this.userId);
return API.v1.success({
message,
@ -342,24 +324,24 @@ API.v1.addRoute(
'chat.react',
{ authRequired: true },
{
post() {
if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) {
async post() {
if (!this.bodyParams.messageId?.trim()) {
throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is missing.');
}
const msg = Messages.findOneById(this.bodyParams.messageId);
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 = this.bodyParams.emoji || this.bodyParams.reaction;
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.');
}
Meteor.runAsUser(this.userId, () => Promise.await(executeSetReaction(emoji, msg._id, this.bodyParams.shouldReact)));
await executeSetReaction(emoji, msg._id, this.bodyParams.shouldReact);
return API.v1.success();
},
@ -370,7 +352,7 @@ API.v1.addRoute(
'chat.reportMessage',
{ authRequired: true },
{
post() {
async post() {
const { messageId, description } = this.bodyParams;
if (!messageId) {
return API.v1.failure('The required "messageId" param is missing.');
@ -380,7 +362,7 @@ API.v1.addRoute(
return API.v1.failure('The required "description" param is missing.');
}
Meteor.call('reportMessage', messageId, description);
await Meteor.call('reportMessage', messageId, description);
return API.v1.success();
},
@ -391,21 +373,21 @@ API.v1.addRoute(
'chat.ignoreUser',
{ authRequired: true },
{
get() {
async get() {
const { rid, userId } = this.queryParams;
let { ignore = true } = this.queryParams;
ignore = typeof ignore === 'string' ? /true|1/.test(ignore) : ignore;
if (!rid || !rid.trim()) {
if (!rid?.trim()) {
throw new Meteor.Error('error-room-id-param-not-provided', 'The required "rid" param is missing.');
}
if (!userId || !userId.trim()) {
if (!userId?.trim()) {
throw new Meteor.Error('error-user-id-param-not-provided', 'The required "userId" param is missing.');
}
Meteor.runAsUser(this.userId, () => Meteor.call('ignoreUser', { rid, userId, ignore }));
await Meteor.call('ignoreUser', { rid, userId, ignore });
return API.v1.success();
},
@ -430,7 +412,7 @@ API.v1.addRoute(
throw new Meteor.Error('The "since" query parameter must be a valid date.');
}
const { cursor, totalCount } = MessagesRaw.trashFindPaginatedDeletedAfter(
const { cursor, totalCount } = await Messages.trashFindPaginatedDeletedAfter(
new Date(since),
{ rid: roomId },
{
@ -468,7 +450,7 @@ API.v1.addRoute(
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}
const { cursor, totalCount } = MessagesRaw.findPaginatedPinnedByRoom(roomId, {
const { cursor, totalCount } = await Messages.findPaginatedPinnedByRoom(roomId, {
skip: offset,
limit: count,
});
@ -476,7 +458,7 @@ API.v1.addRoute(
const [messages, total] = await Promise.all([cursor.toArray(), totalCount]);
return API.v1.success({
messages: normalizeMessagesForUser(messages, this.userId),
messages: await normalizeMessagesForUser(messages, this.userId),
count: messages.length,
offset,
total,
@ -498,25 +480,25 @@ API.v1.addRoute(
const { offset, count } = this.getPaginationItems();
const { sort, fields, query } = this.parseJsonQuery();
if (!settings.get('Threads_enabled')) {
if (!settings.get<boolean>('Threads_enabled')) {
throw new Meteor.Error('error-not-allowed', 'Threads Disabled');
}
const user = Users.findOneById(this.userId, { fields: { _id: 1 } });
const room = Rooms.findOneById(rid, { fields: { ...roomAccessAttributes, t: 1, _id: 1 } });
const user = await Users.findOneById(this.userId, { projection: { _id: 1 } });
const room = await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } });
if (!canAccessRoom(room, user)) {
if (!room || !user || !canAccessRoom(room, user)) {
throw new Meteor.Error('error-not-allowed', 'Not Allowed');
}
const typeThread = {
_hidden: { $ne: true },
...(type === 'following' && { replies: { $in: [this.userId] } }),
...(type === 'unread' && { _id: { $in: Subscriptions.findOneByRoomIdAndUserId(room._id, user._id).tunread } }),
msg: new RegExp(escapeRegExp(text), 'i'),
...(type === 'unread' && { _id: { $in: (await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id))?.tunread || [] } }),
msg: new RegExp(escapeRegExp(text || ''), 'i'),
};
const threadQuery = { ...query, ...typeThread, rid: room._id, tcount: { $exists: true } };
const { cursor, totalCount } = MessagesRaw.findPaginated(threadQuery, {
const { cursor, totalCount } = await Messages.findPaginated(threadQuery, {
sort: sort || { tlm: -1 },
skip: offset,
limit: count,
@ -526,7 +508,7 @@ API.v1.addRoute(
const [threads, total] = await Promise.all([cursor.toArray(), totalCount]);
return API.v1.success({
threads: normalizeMessagesForUser(threads, this.userId),
threads: await normalizeMessagesForUser(threads, this.userId),
count: threads.length,
offset,
total,
@ -539,12 +521,12 @@ API.v1.addRoute(
'chat.syncThreadsList',
{ authRequired: true },
{
get() {
async get() {
const { rid } = this.queryParams;
const { query, fields, sort } = this.parseJsonQuery();
const { updatedSince } = this.queryParams;
let updatedSinceDate;
if (!settings.get('Threads_enabled')) {
if (!settings.get<boolean>('Threads_enabled')) {
throw new Meteor.Error('error-not-allowed', 'Threads Disabled');
}
if (!rid) {
@ -558,17 +540,26 @@ API.v1.addRoute(
} else {
updatedSinceDate = new Date(updatedSince);
}
const user = Users.findOneById(this.userId, { fields: { _id: 1 } });
const room = Rooms.findOneById(rid, { fields: { ...roomAccessAttributes, t: 1, _id: 1 } });
const user = await Users.findOneById(this.userId, { projection: { _id: 1 } });
const room = await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } });
if (!canAccessRoom(room, user)) {
if (!room || !user || !canAccessRoom(room, user)) {
throw new Meteor.Error('error-not-allowed', 'Not Allowed');
}
const threadQuery = Object.assign({}, query, { rid, tcount: { $exists: true } });
return API.v1.success({
threads: {
update: Messages.find({ ...threadQuery, _updatedAt: { $gt: updatedSinceDate } }, { fields, sort }).fetch(),
remove: Messages.trashFindDeletedAfter(updatedSinceDate, threadQuery, { fields, sort }).fetch(),
update: await Messages.find(
{ ...threadQuery, _updatedAt: { $gt: updatedSinceDate } },
{
sort,
projection: fields,
},
).toArray(),
remove: await Messages.trashFindDeletedAfter(updatedSinceDate, threadQuery, {
sort,
projection: fields,
}).toArray(),
},
});
},
@ -590,17 +581,17 @@ API.v1.addRoute(
if (!tmid) {
throw new Meteor.Error('error-invalid-params', 'The required "tmid" query param is missing.');
}
const thread = Messages.findOneById(tmid, { fields: { rid: 1 } });
if (!thread || !thread.rid) {
const thread = await Messages.findOneById(tmid, { projection: { rid: 1 } });
if (!thread?.rid) {
throw new Meteor.Error('error-invalid-message', 'Invalid Message');
}
const user = Users.findOneById(this.userId, { fields: { _id: 1 } });
const room = Rooms.findOneById(thread.rid, { fields: { ...roomAccessAttributes, t: 1, _id: 1 } });
const user = await Users.findOneById(this.userId, { projection: { _id: 1 } });
const room = await Rooms.findOneById(thread.rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } });
if (!canAccessRoom(room, user)) {
if (!room || !user || !canAccessRoom(room, user)) {
throw new Meteor.Error('error-not-allowed', 'Not Allowed');
}
const { cursor, totalCount } = MessagesRaw.findPaginated(
const { cursor, totalCount } = await Messages.findPaginated(
{ ...query, tmid },
{
sort: sort || { ts: 1 },
@ -626,12 +617,12 @@ API.v1.addRoute(
'chat.syncThreadMessages',
{ authRequired: true },
{
get() {
async get() {
const { tmid } = this.queryParams;
const { query, fields, sort } = this.parseJsonQuery();
const { updatedSince } = this.queryParams;
let updatedSinceDate;
if (!settings.get('Threads_enabled')) {
if (!settings.get<boolean>('Threads_enabled')) {
throw new Meteor.Error('error-not-allowed', 'Threads Disabled');
}
if (!tmid) {
@ -645,20 +636,20 @@ API.v1.addRoute(
} else {
updatedSinceDate = new Date(updatedSince);
}
const thread = Messages.findOneById(tmid, { fields: { rid: 1 } });
if (!thread || !thread.rid) {
const thread = await Messages.findOneById(tmid, { projection: { rid: 1 } });
if (!thread?.rid) {
throw new Meteor.Error('error-invalid-message', 'Invalid Message');
}
const user = Users.findOneById(this.userId, { fields: { _id: 1 } });
const room = Rooms.findOneById(thread.rid, { fields: { ...roomAccessAttributes, t: 1, _id: 1 } });
const user = await Users.findOneById(this.userId, { projection: { _id: 1 } });
const room = await Rooms.findOneById(thread.rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } });
if (!canAccessRoom(room, user)) {
if (!room || !user || !canAccessRoom(room, user)) {
throw new Meteor.Error('error-not-allowed', 'Not Allowed');
}
return API.v1.success({
messages: {
update: Messages.find({ ...query, tmid, _updatedAt: { $gt: updatedSinceDate } }, { fields, sort }).fetch(),
remove: Messages.trashFindDeletedAfter(updatedSinceDate, { ...query, tmid }, { fields, sort }).fetch(),
update: await Messages.find({ ...query, tmid, _updatedAt: { $gt: updatedSinceDate } }, { projection: fields, sort }).toArray(),
remove: await Messages.trashFindDeletedAfter(updatedSinceDate, { ...query, tmid }, { projection: fields, sort }).toArray(),
},
});
},
@ -669,13 +660,15 @@ API.v1.addRoute(
'chat.followMessage',
{ authRequired: true },
{
post() {
async post() {
const { mid } = this.bodyParams;
if (!mid) {
throw new Meteor.Error('The required "mid" body param is missing.');
}
Meteor.runAsUser(this.userId, () => Meteor.call('followMessage', { mid }));
await Meteor.call('followMessage', { mid });
return API.v1.success();
},
},
@ -685,13 +678,15 @@ API.v1.addRoute(
'chat.unfollowMessage',
{ authRequired: true },
{
post() {
async post() {
const { mid } = this.bodyParams;
if (!mid) {
throw new Meteor.Error('The required "mid" body param is missing.');
}
Meteor.runAsUser(this.userId, () => Meteor.call('unfollowMessage', { mid }));
await Meteor.call('unfollowMessage', { mid });
return API.v1.success();
},
},
@ -701,24 +696,23 @@ API.v1.addRoute(
'chat.getMentionedMessages',
{ authRequired: true },
{
get() {
async get() {
const { roomId } = this.queryParams;
const { sort } = this.parseJsonQuery();
const { offset, count } = this.getPaginationItems();
if (!roomId) {
throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.');
}
const messages = Promise.await(
findMentionedMessages({
uid: this.userId,
roomId,
pagination: {
offset,
count,
sort,
},
}),
);
const messages = await findMentionedMessages({
uid: this.userId,
roomId,
pagination: {
offset,
count,
sort,
},
});
return API.v1.success(messages);
},
},
@ -728,7 +722,7 @@ API.v1.addRoute(
'chat.getStarredMessages',
{ authRequired: true },
{
get() {
async get() {
const { roomId } = this.queryParams;
const { sort } = this.parseJsonQuery();
const { offset, count } = this.getPaginationItems();
@ -736,19 +730,17 @@ API.v1.addRoute(
if (!roomId) {
throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.');
}
const messages = Promise.await(
findStarredMessages({
uid: this.userId,
roomId,
pagination: {
offset,
count,
sort,
},
}),
);
const messages = await findStarredMessages({
uid: this.userId,
roomId,
pagination: {
offset,
count,
sort,
},
});
messages.messages = normalizeMessagesForUser(messages.messages, this.userId);
messages.messages = await normalizeMessagesForUser(messages.messages, this.userId);
return API.v1.success(messages);
},
@ -770,7 +762,7 @@ API.v1.addRoute(
const messages = await findDiscussionsFromRoom({
uid: this.userId,
roomId,
text,
text: text || '',
pagination: {
offset,
count,

@ -2662,6 +2662,7 @@ describe('Threads', () => {
.set(credentials)
.query({
tmid: threadMessage.tmid,
updatedSince: 'updatedSince',
})
.expect('Content-Type', 'application/json')
.expect(400)

@ -247,3 +247,4 @@ export * from './v1/import';
export * from './v1/voip';
export * from './v1/email-inbox';
export * from './v1/federation';
export * from './v1/chat';

@ -1,6 +1,8 @@
import type { IMessage, IRoom, ReadReceipt } from '@rocket.chat/core-typings';
import type { IMessage, IRoom, MessageAttachment, ReadReceipt } from '@rocket.chat/core-typings';
import Ajv from 'ajv';
import type { PaginatedRequest } from '../helpers/PaginatedRequest';
const ajv = new Ajv({
coerceTypes: true,
});
@ -14,13 +16,60 @@ const chatSendMessageSchema = {
properties: {
message: {
type: 'object',
properties: {
_id: {
type: 'string',
nullable: true,
},
rid: {
type: 'string',
},
tmid: {
type: 'string',
nullable: true,
},
msg: {
type: 'string',
nullable: true,
},
alias: {
type: 'string',
nullable: true,
},
emoji: {
type: 'string',
nullable: true,
},
tshow: {
type: 'boolean',
nullable: true,
},
avatar: {
type: 'string',
nullable: true,
},
attachments: {
type: 'array',
items: {
type: 'object',
},
nullable: true,
},
blocks: {
type: 'array',
items: {
type: 'object',
},
nullable: true,
},
},
},
},
required: ['message'],
additionalProperties: false,
};
export const isChatSendMessageProps = ajv.compile<ChatFollowMessage>(chatSendMessageSchema);
export const isChatSendMessageProps = ajv.compile<ChatSendMessage>(chatSendMessageSchema);
type ChatFollowMessage = {
mid: IMessage['_id'];
@ -74,51 +123,51 @@ const ChatGetMessageSchema = {
export const isChatGetMessageProps = ajv.compile<ChatGetMessage>(ChatGetMessageSchema);
type ChatStarMessage = {
msgId: IMessage['_id'];
messageId: IMessage['_id'];
};
const ChatStarMessageSchema = {
type: 'object',
properties: {
msgId: {
messageId: {
type: 'string',
},
},
required: ['msgId'],
required: ['messageId'],
additionalProperties: false,
};
export const isChatStarMessageProps = ajv.compile<ChatStarMessage>(ChatStarMessageSchema);
type ChatUnstarMessage = {
msgId: IMessage['_id'];
messageId: IMessage['_id'];
};
const ChatUnstarMessageSchema = {
type: 'object',
properties: {
msgId: {
messageId: {
type: 'string',
},
},
required: ['msgId'],
required: ['messageId'],
additionalProperties: false,
};
export const isChatUnstarMessageProps = ajv.compile<ChatUnstarMessage>(ChatUnstarMessageSchema);
type ChatPinMessage = {
msgId: IMessage['_id'];
messageId: IMessage['_id'];
};
const ChatPinMessageSchema = {
type: 'object',
properties: {
msgId: {
messageId: {
type: 'string',
},
},
required: ['msgId'],
required: ['messageId'],
additionalProperties: false,
};
@ -141,12 +190,10 @@ const ChatUnpinMessageSchema = {
export const isChatUnpinMessageProps = ajv.compile<ChatUnpinMessage>(ChatUnpinMessageSchema);
type ChatGetDiscussions = {
type ChatGetDiscussions = PaginatedRequest<{
roomId: IRoom['_id'];
text?: string;
offset: number;
count: number;
};
}>;
const ChatGetDiscussionsSchema = {
type: 'object',
@ -160,12 +207,14 @@ const ChatGetDiscussionsSchema = {
},
offset: {
type: 'number',
nullable: true,
},
count: {
type: 'number',
nullable: true,
},
},
required: ['roomId', 'offset', 'count'],
required: ['roomId'],
additionalProperties: false,
};
@ -192,13 +241,11 @@ const ChatReportMessageSchema = {
export const isChatReportMessageProps = ajv.compile<ChatReportMessage>(ChatReportMessageSchema);
type ChatGetThreadsList = {
type ChatGetThreadsList = PaginatedRequest<{
rid: IRoom['_id'];
type: 'unread' | 'following' | 'all';
text?: string;
offset: number;
count: number;
};
}>;
const ChatGetThreadsListSchema = {
type: 'object',
@ -208,6 +255,7 @@ const ChatGetThreadsListSchema = {
},
type: {
type: 'string',
nullable: true,
},
text: {
type: 'string',
@ -222,7 +270,7 @@ const ChatGetThreadsListSchema = {
nullable: true,
},
},
required: ['rid', 'type'],
required: ['rid'],
additionalProperties: false,
};
@ -252,6 +300,7 @@ export const isChatSyncThreadsListProps = ajv.compile<ChatSyncThreadsList>(ChatS
type ChatDelete = {
msgId: IMessage['_id'];
roomId: IRoom['_id'];
asUser?: boolean;
};
const ChatDeleteSchema = {
@ -263,6 +312,10 @@ const ChatDeleteSchema = {
roomId: {
type: 'string',
},
asUser: {
type: 'boolean',
nullable: true,
},
},
required: ['msgId', 'roomId'],
additionalProperties: false,
@ -270,7 +323,9 @@ const ChatDeleteSchema = {
export const isChatDeleteProps = ajv.compile<ChatDelete>(ChatDeleteSchema);
type ChatReact = { emoji: string; messageId: IMessage['_id'] } | { reaction: string; messageId: IMessage['_id'] };
type ChatReact =
| { emoji: string; messageId: IMessage['_id']; shouldReact?: boolean }
| { reaction: string; messageId: IMessage['_id']; shouldReact?: boolean };
const ChatReactSchema = {
oneOf: [
@ -283,6 +338,10 @@ const ChatReactSchema = {
messageId: {
type: 'string',
},
shouldReact: {
type: 'boolean',
nullable: true,
},
},
required: ['emoji', 'messageId'],
additionalProperties: false,
@ -296,6 +355,10 @@ const ChatReactSchema = {
messageId: {
type: 'string',
},
shouldReact: {
type: 'boolean',
nullable: true,
},
},
required: ['reaction', 'messageId'],
additionalProperties: false,
@ -334,12 +397,10 @@ const ChatIgnoreUserSchema = {
export const isChatIgnoreUserProps = ajv.compile<ChatIgnoreUser>(ChatIgnoreUserSchema);
type ChatSearch = {
type ChatSearch = PaginatedRequest<{
roomId: IRoom['_id'];
searchText: string;
count: number;
offset: number;
};
}>;
const ChatSearchSchema = {
type: 'object',
@ -352,12 +413,14 @@ const ChatSearchSchema = {
},
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
},
required: ['roomId', 'searchText', 'count', 'offset'],
required: ['roomId', 'searchText'],
additionalProperties: false,
};
@ -405,35 +468,46 @@ const ChatGetMessageReadReceiptsSchema = {
export const isChatGetMessageReadReceiptsProps = ajv.compile<ChatGetMessageReadReceipts>(ChatGetMessageReadReceiptsSchema);
type ChatPostMessage = {
type GetStarredMessages = {
roomId: IRoom['_id'];
text?: string;
count?: number;
offset?: number;
sort?: string;
};
const ChatPostMessageSchema = {
const GetStarredMessagesSchema = {
type: 'object',
properties: {
roomId: {
type: 'string',
},
text: {
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
sort: {
type: 'string',
nullable: true,
},
},
required: ['roomId'],
additionalProperties: false,
};
export const isChatPostMessageSchemaProps = ajv.compile<ChatPostMessage>(ChatPostMessageSchema);
export const isChatGetStarredMessagesProps = ajv.compile<GetStarredMessages>(GetStarredMessagesSchema);
type GetStarredMessages = {
type GetPinnedMessages = {
roomId: IRoom['_id'];
count?: number;
offset?: number;
sort?: string;
};
const GetStarredMessagesSchema = {
const GetPinnedMessagesSchema = {
type: 'object',
properties: {
roomId: {
@ -456,16 +530,16 @@ const GetStarredMessagesSchema = {
additionalProperties: false,
};
export const isChatGetStarredMessagesPayload = ajv.compile<GetStarredMessages>(GetStarredMessagesSchema);
export const isChatGetPinnedMessagesProps = ajv.compile<GetPinnedMessages>(GetPinnedMessagesSchema);
type GetPinnedMessages = {
type GetMentionedMessages = {
roomId: IRoom['_id'];
count?: number;
offset?: number;
sort?: string;
};
const GetPinnedMessagesSchema = {
const GetMentionedMessagesSchema = {
type: 'object',
properties: {
roomId: {
@ -488,21 +562,43 @@ const GetPinnedMessagesSchema = {
additionalProperties: false,
};
export const isChatGetPinnedMessagesPayload = ajv.compile<GetPinnedMessages>(GetPinnedMessagesSchema);
export const isChatGetMentionedMessagesProps = ajv.compile<GetMentionedMessages>(GetMentionedMessagesSchema);
type GetMentionedMessages = {
type ChatSyncMessages = {
roomId: IRoom['_id'];
count?: number;
offset?: number;
sort?: string;
lastUpdate: string;
};
const GetMentionedMessagesSchema = {
const ChatSyncMessagesSchema = {
type: 'object',
properties: {
roomId: {
type: 'string',
},
lastUpdate: {
type: 'string',
},
},
required: ['roomId', 'lastUpdate'],
additionalProperties: false,
};
export const isChatSyncMessagesProps = ajv.compile<ChatSyncMessages>(ChatSyncMessagesSchema);
type ChatSyncThreadMessages = PaginatedRequest<{
tmid: string;
updatedSince: string;
}>;
const ChatSyncThreadMessagesSchema = {
type: 'object',
properties: {
tmid: {
type: 'string',
},
updatedSince: {
type: 'string',
},
count: {
type: 'number',
nullable: true,
@ -516,15 +612,156 @@ const GetMentionedMessagesSchema = {
nullable: true,
},
},
required: ['roomId'],
required: ['tmid', 'updatedSince'],
additionalProperties: false,
};
export const isChatSyncThreadMessagesProps = ajv.compile<ChatSyncThreadMessages>(ChatSyncThreadMessagesSchema);
type ChatGetThreadMessages = PaginatedRequest<{
tmid: string;
}>;
const ChatGetThreadMessagesSchema = {
type: 'object',
properties: {
tmid: {
type: 'string',
},
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
sort: {
type: 'string',
nullable: true,
},
},
required: ['tmid'],
additionalProperties: false,
};
export const isChatGetThreadMessagesProps = ajv.compile<ChatGetThreadMessages>(ChatGetThreadMessagesSchema);
type ChatGetDeletedMessages = PaginatedRequest<{
roomId: IRoom['_id'];
since: string;
}>;
const ChatGetDeletedMessagesSchema = {
type: 'object',
properties: {
roomId: {
type: 'string',
},
since: {
type: 'string',
},
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
sort: {
type: 'string',
nullable: true,
},
},
required: ['roomId', 'since'],
additionalProperties: false,
};
export const isChatGetMentionedMessagesPayload = ajv.compile<GetMentionedMessages>(GetMentionedMessagesSchema);
export const isChatGetDeletedMessagesProps = ajv.compile<ChatGetDeletedMessages>(ChatGetDeletedMessagesSchema);
type ChatPostMessage =
| { roomId: string; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] }
| { channel: string; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] };
const ChatPostMessageSchema = {
oneOf: [
{
type: 'object',
properties: {
roomId: {
type: 'string',
},
text: {
type: 'string',
nullable: true,
},
alias: {
type: 'string',
nullable: true,
},
emoji: {
type: 'string',
nullable: true,
},
avatar: {
type: 'string',
nullable: true,
},
attachments: {
type: 'array',
items: {
type: 'object',
},
nullable: true,
},
},
required: ['roomId'],
additionalProperties: false,
},
{
type: 'object',
properties: {
channel: {
type: 'string',
},
text: {
type: 'string',
nullable: true,
},
alias: {
type: 'string',
nullable: true,
},
emoji: {
type: 'string',
nullable: true,
},
avatar: {
type: 'string',
nullable: true,
},
attachments: {
type: 'array',
items: {
type: 'object',
},
nullable: true,
},
},
required: ['channel'],
additionalProperties: false,
},
],
};
export const isChatPostMessageProps = ajv.compile<ChatPostMessage>(ChatPostMessageSchema);
export type ChatEndpoints = {
'/v1/chat.sendMessage': {
POST: (params: ChatSendMessage) => IMessage;
POST: (params: ChatSendMessage) => {
message: IMessage;
};
};
'/v1/chat.getMessage': {
GET: (params: ChatGetMessage) => {
@ -544,7 +781,9 @@ export type ChatEndpoints = {
POST: (params: ChatUnstarMessage) => void;
};
'/v1/chat.pinMessage': {
POST: (params: ChatPinMessage) => void;
POST: (params: ChatPinMessage) => {
message: IMessage;
};
};
'/v1/chat.unPinMessage': {
POST: (params: ChatUnpinMessage) => void;
@ -592,15 +831,12 @@ export type ChatEndpoints = {
};
'/v1/chat.update': {
POST: (params: ChatUpdate) => {
messages: IMessage;
message: IMessage;
};
};
'/v1/chat.getMessageReadReceipts': {
GET: (params: ChatGetMessageReadReceipts) => { receipts: ReadReceipt[] };
};
'/v1/chat.postMessage': {
POST: (params: ChatPostMessage) => IMessage;
};
'/v1/chat.getStarredMessages': {
GET: (params: GetStarredMessages) => {
messages: IMessage[];
@ -625,4 +861,43 @@ export type ChatEndpoints = {
total: number;
};
};
'/v1/chat.syncMessages': {
GET: (params: ChatSyncMessages) => {
result: {
updated: IMessage[];
deleted: IMessage[];
};
};
};
'/v1/chat.postMessage': {
POST: (params: ChatPostMessage) => {
ts: number;
channel: IRoom;
message: IMessage;
};
};
'/v1/chat.syncThreadMessages': {
GET: (params: ChatSyncThreadMessages) => {
messages: {
update: IMessage[];
remove: IMessage[];
};
};
};
'/v1/chat.getThreadMessages': {
GET: (params: ChatGetThreadMessages) => {
messages: IMessage[];
count: number;
offset: number;
total: number;
};
};
'/v1/chat.getDeletedMessages': {
GET: (params: ChatGetDeletedMessages) => {
messages: IMessage[];
count: number;
offset: number;
total: number;
};
};
};

Loading…
Cancel
Save