From aa7d71696e696d60fc9e76b9fadc1c921654ce1f Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 3 Jun 2022 15:43:44 -0300 Subject: [PATCH 01/12] Chore: remove duplicated NotFoundPage.js (#25749) --- .../client/views/notFound/NotFoundPage.js | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 apps/meteor/client/views/notFound/NotFoundPage.js diff --git a/apps/meteor/client/views/notFound/NotFoundPage.js b/apps/meteor/client/views/notFound/NotFoundPage.js deleted file mode 100644 index fd78d24879c1..000000000000 --- a/apps/meteor/client/views/notFound/NotFoundPage.js +++ /dev/null @@ -1,63 +0,0 @@ -import { Box, Button, ButtonGroup, Flex, Margins } from '@rocket.chat/fuselage'; -import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -function NotFoundPage() { - const t = useTranslation(); - const homeRoute = useRoute('home'); - - const handleGoToPreviousPageClick = () => { - window.history.back(); - }; - - const handleGoHomeClick = () => { - homeRoute.push(); - }; - - return ( - - - - - - - 404 - - - - {t('Oops_page_not_found')} - - - - {t('Sorry_page_you_requested_does_not_exist_or_was_deleted')} - - - - - - - - - - - - ); -} - -export default NotFoundPage; From 9317fe9dcef95373403de75aad7dcc418d5e47db Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 3 Jun 2022 16:26:55 -0300 Subject: [PATCH 02/12] Update CODEOWNERS --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 534f23be0100..a3ae6a000b42 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,3 +9,4 @@ /_templates/ @RocketChat/chat-engine /apps/meteor/client/ @RocketChat/frontend /apps/meteor/tests/ @RocketChat/chat-engine +/apps/meteor/app/apps/ @RocketChat/apps From 7e8d6d24081669f109a9e24b387cb350f4a9297b Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 3 Jun 2022 17:43:16 -0300 Subject: [PATCH 03/12] Update package.json (#25755) --- apps/meteor/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 7a9c64e9ca61..39ddbbd81a67 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -28,7 +28,7 @@ "obj:dev": "TEST_MODE=true yarn dev", "stylelint": "stylelint \"app/**/*.css\" \"client/**/*.css\" \"app/**/*.less\" \"client/**/*.less\" \"ee/**/*.less\"", "stylelint:fix": "stylelint --fix \"app/**/*.css\" \"client/**/*.css\" \"app/**/*.less\" \"client/**/*.less\" \"ee/**/*.less\"", - "typecheck": "cross-env NODE_OPTIONS=\"--max-old-space-size=4092\" tsc --noEmit --skipLibCheck", + "typecheck": "cross-env NODE_OPTIONS=\"--max-old-space-size=8184\" tsc --noEmit --skipLibCheck", "deploy": "npm run build && pm2 startOrRestart pm2.json", "coverage": "nyc -r html mocha --config ./.mocharc.js", "testci": "node .scripts/start.js", From 0b1073ecde996288d75f0274de7a6c614b180c51 Mon Sep 17 00:00:00 2001 From: Felipe <84182706+felipe-rod123@users.noreply.github.com> Date: Fri, 3 Jun 2022 17:57:34 -0300 Subject: [PATCH 04/12] Chore: add Ajv JSON Schema to api/v1 (#25601) --- apps/meteor/app/api/server/v1/im.ts | 11 +- .../imports/server/rest/departments.ts | 7 +- .../Omnichannel/hooks/useDepartmentsList.ts | 5 +- .../omnichannel/directory/calls/CallTable.tsx | 4 +- .../omnichannel/queueList/hooks/useQuery.ts | 6 +- .../tests/end-to-end/api/04-direct-message.js | 1 - packages/rest-typings/src/index.ts | 2 + packages/rest-typings/src/v1/banners.ts | 86 +- packages/rest-typings/src/v1/chat.ts | 420 ++++++++- packages/rest-typings/src/v1/cloud.ts | 72 +- packages/rest-typings/src/v1/customSounds.ts | 37 +- .../rest-typings/src/v1/customUserStatus.ts | 34 +- packages/rest-typings/src/v1/directory.ts | 40 +- .../rest-typings/src/v1/dm/DmCloseProps.ts | 20 + .../rest-typings/src/v1/dm/DmHistoryProps.ts | 56 ++ .../rest-typings/src/v1/dm/DmKickProps.ts | 24 + .../rest-typings/src/v1/dm/DmLeaveProps.ts | 20 + packages/rest-typings/src/v1/dm/im.ts | 22 +- packages/rest-typings/src/v1/dns.ts | 44 +- packages/rest-typings/src/v1/e2e.ts | 96 +- packages/rest-typings/src/v1/email-inbox.ts | 181 +++- packages/rest-typings/src/v1/emojiCustom.ts | 43 +- packages/rest-typings/src/v1/groups.ts | 356 +++++++- packages/rest-typings/src/v1/invites.ts | 43 +- packages/rest-typings/src/v1/ldap.ts | 25 +- packages/rest-typings/src/v1/licenses.ts | 24 +- packages/rest-typings/src/v1/oauthapps.ts | 4 +- packages/rest-typings/src/v1/omnichannel.ts | 854 ++++++++++++++++-- packages/rest-typings/src/v1/permissions.ts | 30 +- packages/rest-typings/src/v1/push.ts | 56 +- packages/rest-typings/src/v1/roles.ts | 68 +- packages/rest-typings/src/v1/rooms.ts | 451 +++++++-- packages/rest-typings/src/v1/statistics.ts | 59 +- packages/rest-typings/src/v1/users.ts | 126 ++- .../rest-typings/src/v1/videoConference.ts | 28 +- packages/rest-typings/src/v1/voip.ts | 518 ++++++++++- 36 files changed, 3577 insertions(+), 296 deletions(-) create mode 100644 packages/rest-typings/src/v1/dm/DmCloseProps.ts create mode 100644 packages/rest-typings/src/v1/dm/DmHistoryProps.ts create mode 100644 packages/rest-typings/src/v1/dm/DmKickProps.ts create mode 100644 packages/rest-typings/src/v1/dm/DmLeaveProps.ts diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index 76542f1e5582..5795f9601ac2 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -2,7 +2,14 @@ * Docs: https://github.com/RocketChat/developer-docs/blob/master/reference/api/rest-api/endpoints/team-collaboration-endpoints/im-endpoints */ import type { IMessage, IRoom, ISetting, ISubscription, IUpload, IUser } from '@rocket.chat/core-typings'; -import { isDmDeleteProps, isDmFileProps, isDmMemberProps, isDmMessagesProps, isDmCreateProps } from '@rocket.chat/rest-typings'; +import { + isDmDeleteProps, + isDmFileProps, + isDmMemberProps, + isDmMessagesProps, + isDmCreateProps, + isDmHistoryProps, +} from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; @@ -240,7 +247,7 @@ API.v1.addRoute( API.v1.addRoute( ['dm.history', 'im.history'], - { authRequired: true }, + { authRequired: true, validateParams: isDmHistoryProps }, { async get() { const { offset = 0, count = 20 } = this.getPaginationItems(); diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index ab8ee8a66e42..e501782744d4 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -1,3 +1,4 @@ +import { isLivechatDepartmentProps } from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; import { API } from '../../../../api/server'; @@ -14,9 +15,9 @@ import { API.v1.addRoute( 'livechat/department', - { authRequired: true }, + { authRequired: true, validateParams: isLivechatDepartmentProps }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); @@ -26,7 +27,7 @@ API.v1.addRoute( findDepartments({ userId: this.userId, text, - enabled, + enabled: enabled === 'true', onlyMyDepartments: onlyMyDepartments === 'true', excludeDepartmentId, pagination: { diff --git a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts index a80ea72447ab..1d87a9f6e6da 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts @@ -27,9 +27,8 @@ export const useDepartmentsList = ( const t = useTranslation(); const [itemsList, setItemsList] = useState(() => new RecordList()); const reload = useCallback(() => setItemsList(new RecordList()), []); - const endpoint = 'livechat/department'; - const getDepartments = useEndpoint('GET', endpoint); + const getDepartments = useEndpoint('GET', 'livechat/department'); useComponentDidUpdate(() => { options && reload(); @@ -44,7 +43,7 @@ export const useDepartmentsList = ( count: end + start, sort: `{ "name": 1 }`, excludeDepartmentId: options.excludeDepartmentId, - enabled: options.enabled, + enabled: options.enabled ? 'true' : 'false', }); const items = departments diff --git a/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx b/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx index 2c9485b74e4d..7d964af4c16a 100644 --- a/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx +++ b/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx @@ -22,7 +22,7 @@ const useQuery = ( userIdLoggedIn: string | null, ): { sort: string; - open: boolean; + open: 'false'; roomName: string; agents: string[]; count?: number; @@ -31,7 +31,7 @@ const useQuery = ( useMemo( () => ({ sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), - open: false, + open: 'false', roomName: text || '', agents: userIdLoggedIn ? [userIdLoggedIn] : [], ...(itemsPerPage && { count: itemsPerPage }), diff --git a/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts b/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts index 880c157455d4..76a5fe26dfcb 100644 --- a/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts +++ b/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts @@ -12,7 +12,7 @@ type useQueryType = ( debouncedSort: [string, 'asc' | 'desc'], ) => { agentId?: ILivechatAgent['_id']; - includeOfflineAgents?: boolean; + includeOfflineAgents?: 'true' | 'false'; departmentId?: ILivechatAgent['_id']; offset: number; count: number; @@ -25,7 +25,7 @@ export const useQuery: useQueryType = ({ servedBy, status, departmentId, itemsPe useMemo(() => { const query: { agentId?: string; - includeOflineAgents?: boolean; + includeOflineAgents?: 'true' | 'false'; departmentId?: string; sort: string; count: number; @@ -39,7 +39,7 @@ export const useQuery: useQueryType = ({ servedBy, status, departmentId, itemsPe }; if (status !== 'online') { - query.includeOflineAgents = true; + query.includeOflineAgents = 'true'; } if (servedBy) { query.agentId = servedBy; diff --git a/apps/meteor/tests/end-to-end/api/04-direct-message.js b/apps/meteor/tests/end-to-end/api/04-direct-message.js index d1f6e82dc75e..9eb450539ab9 100644 --- a/apps/meteor/tests/end-to-end/api/04-direct-message.js +++ b/apps/meteor/tests/end-to-end/api/04-direct-message.js @@ -170,7 +170,6 @@ describe('[Direct Messages]', function () { .set(credentials) .query({ roomId: directMessage._id, - userId: 'rocket.cat', }) .expect('Content-Type', 'application/json') .expect(200) diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index a8dfe71b3cf0..75274d973509 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -157,7 +157,9 @@ export * from './v1/channels/ChannelsConvertToTeamProps'; export * from './v1/channels/ChannelsSetReadOnlyProps'; export * from './v1/channels/ChannelsDeleteProps'; export * from './v1/dm'; +export * from './v1/dm/DmHistoryProps'; export * from './v1/integrations'; +export * from './v1/omnichannel'; export * from './v1/oauthapps'; export * from './helpers/PaginatedRequest'; export * from './helpers/PaginatedResult'; diff --git a/packages/rest-typings/src/v1/banners.ts b/packages/rest-typings/src/v1/banners.ts index 2af16f69620d..7045e2383a14 100644 --- a/packages/rest-typings/src/v1/banners.ts +++ b/packages/rest-typings/src/v1/banners.ts @@ -1,26 +1,104 @@ +import Ajv from 'ajv'; import type { BannerPlatform, IBanner } from '@rocket.chat/core-typings'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type BannersGetNew = { + platform: BannerPlatform; + bid: IBanner['_id']; +}; + +const BannersGetNewSchema = { + type: 'object', + properties: { + platform: { + type: 'string', + enum: ['1', '2'], + }, + bid: { + type: 'string', + }, + }, + required: ['platform', 'bid'], + additionalProperties: false, +}; + +export const isBannersGetNewProps = ajv.compile(BannersGetNewSchema); + +type BannersId = { + platform: BannerPlatform; +}; + +const BannersIdSchema = { + type: 'object', + properties: { + platform: { + type: 'string', + }, + }, + required: ['platform'], + additionalProperties: false, +}; + +export const isBannersIdProps = ajv.compile(BannersIdSchema); + +type Banners = { + platform: BannerPlatform; +}; + +const BannersSchema = { + type: 'object', + properties: { + platform: { + type: 'string', + }, + }, + required: ['platform'], + additionalProperties: false, +}; + +export const isBannersProps = ajv.compile(BannersSchema); + +type BannersDismiss = { + bannerId: string; +}; + +const BannersDismissSchema = { + type: 'object', + properties: { + bannerId: { + type: 'string', + }, + }, + required: ['bannerId'], + additionalProperties: false, +}; + +export const isBannersDismissProps = ajv.compile(BannersDismissSchema); + export type BannersEndpoints = { /* @deprecated */ 'banners.getNew': { - GET: (params: { platform: BannerPlatform; bid: IBanner['_id'] }) => { + GET: (params: BannersGetNew) => { banners: IBanner[]; }; }; 'banners/:id': { - GET: (params: { platform: BannerPlatform }) => { + GET: (params: BannersId) => { banners: IBanner[]; }; }; 'banners': { - GET: (params: { platform: BannerPlatform }) => { + GET: (params: Banners) => { banners: IBanner[]; }; }; 'banners.dismiss': { - POST: (params: { bannerId: string }) => void; + POST: (params: BannersDismiss) => void; }; }; diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index e22ab320c99f..1754c302f5a9 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -1,46 +1,432 @@ import type { IMessage, IRoom, ReadReceipt } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type ChatFollowMessage = { + mid: IMessage['_id']; +}; + +const chatFollowMessageSchema = { + type: 'object', + properties: { + mid: { + type: 'string', + }, + }, + required: ['mid'], + additionalProperties: false, +}; + +export const isChatFollowMessageProps = ajv.compile(chatFollowMessageSchema); + +type ChatUnfollowMessage = { + mid: IMessage['_id']; +}; + +const chatUnfollowMessageSchema = { + type: 'object', + properties: { + mid: { + type: 'string', + }, + }, + required: ['mid'], + additionalProperties: false, +}; + +export const isChatUnfollowMessageProps = ajv.compile(chatUnfollowMessageSchema); + +type ChatGetMessage = { + msgId: IMessage['_id']; +}; + +const ChatGetMessageSchema = { + type: 'object', + properties: { + msgId: { + type: 'string', + }, + }, + required: ['msgId'], + additionalProperties: false, +}; + +export const isChatGetMessageProps = ajv.compile(ChatGetMessageSchema); + +type ChatStarMessage = { + msgId: IMessage['_id']; +}; + +const ChatStarMessageSchema = { + type: 'object', + properties: { + msgId: { + type: 'string', + }, + }, + required: ['msgId'], + additionalProperties: false, +}; + +export const isChatStarMessageProps = ajv.compile(ChatStarMessageSchema); + +type ChatUnstarMessage = { + msgId: IMessage['_id']; +}; + +const ChatUnstarMessageSchema = { + type: 'object', + properties: { + msgId: { + type: 'string', + }, + }, + required: ['msgId'], + additionalProperties: false, +}; + +export const isChatUnstarMessageProps = ajv.compile(ChatUnstarMessageSchema); + +type ChatPinMessage = { + msgId: IMessage['_id']; +}; + +const ChatPinMessageSchema = { + type: 'object', + properties: { + msgId: { + type: 'string', + }, + }, + required: ['msgId'], + additionalProperties: false, +}; + +export const isChatPinMessageProps = ajv.compile(ChatPinMessageSchema); + +type ChatUnpinMessage = { + messageId: IMessage['_id']; +}; + +const ChatUnpinMessageSchema = { + type: 'object', + properties: { + messageId: { + type: 'string', + }, + }, + required: ['messageId'], + additionalProperties: false, +}; + +export const isChatUnpinMessageProps = ajv.compile(ChatUnpinMessageSchema); + +type ChatGetDiscussions = { + roomId: IRoom['_id']; + text?: string; + offset: number; + count: number; +}; + +const ChatGetDiscussionsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + text: { + type: 'string', + nullable: true, + }, + offset: { + type: 'number', + }, + count: { + type: 'number', + }, + }, + required: ['roomId', 'offset', 'count'], + additionalProperties: false, +}; + +export const isChatGetDiscussionsProps = ajv.compile(ChatGetDiscussionsSchema); + +type ChatReportMessage = { + messageId: IMessage['_id']; + description: string; +}; + +const ChatReportMessageSchema = { + type: 'object', + properties: { + messageId: { + type: 'string', + }, + description: { + type: 'string', + }, + }, + required: ['messageId', 'description'], + additionalProperties: false, +}; + +export const isChatReportMessageProps = ajv.compile(ChatReportMessageSchema); + +type ChatGetThreadsList = { + rid: IRoom['_id']; + type: 'unread' | 'following' | 'all'; + text?: string; + offset: number; + count: number; +}; + +const ChatGetThreadsListSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + type: { + type: 'string', + }, + text: { + type: 'string', + nullable: true, + }, + offset: { + type: 'number', + }, + count: { + type: 'number', + }, + }, + required: ['rid', 'type', 'offset', 'count'], + additionalProperties: false, +}; + +export const isChatGetThreadsListProps = ajv.compile(ChatGetThreadsListSchema); + +type ChatSyncThreadsList = { + rid: IRoom['_id']; + updatedSince: string; +}; + +const ChatSyncThreadsListSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + updatedSince: { + type: 'string', + }, + }, + required: ['rid', 'updatedSince'], + additionalProperties: false, +}; + +export const isChatSyncThreadsListProps = ajv.compile(ChatSyncThreadsListSchema); + +type ChatDelete = { + msgId: IMessage['_id']; + roomId: IRoom['_id']; +}; + +const ChatDeleteSchema = { + type: 'object', + properties: { + msgId: { + type: 'string', + }, + roomId: { + type: 'string', + }, + }, + required: ['msgId', 'roomId'], + additionalProperties: false, +}; + +export const isChatDeleteProps = ajv.compile(ChatDeleteSchema); + +type ChatReact = { emoji: string; messageId: IMessage['_id'] } | { reaction: string; messageId: IMessage['_id'] }; + +const ChatReactSchema = { + oneOf: [ + { + type: 'object', + properties: { + emoji: { + type: 'string', + }, + messageId: { + type: 'string', + }, + }, + required: ['emoji', 'messageId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + reaction: { + type: 'string', + }, + messageId: { + type: 'string', + }, + }, + required: ['reaction', 'messageId'], + additionalProperties: false, + }, + ], +}; + +export const isChatReactProps = ajv.compile(ChatReactSchema); + +/** + * The param `ignore` cannot be boolean, since this is a GET method. Use strings 'true' or 'false' instead. + * @param {string} ignore + */ +type ChatIgnoreUser = { + rid: string; + userId: string; + ignore: string; +}; + +const ChatIgnoreUserSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + userId: { + type: 'string', + }, + ignore: { + type: 'string', + }, + }, + required: ['rid', 'userId', 'ignore'], + additionalProperties: false, +}; + +export const isChatIgnoreUserProps = ajv.compile(ChatIgnoreUserSchema); + +type ChatSearch = { + roomId: IRoom['_id']; + searchText: string; + count: number; + offset: number; +}; + +const ChatSearchSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + searchText: { + type: 'string', + }, + count: { + type: 'number', + }, + offset: { + type: 'number', + }, + }, + required: ['roomId', 'searchText', 'count', 'offset'], + additionalProperties: false, +}; + +export const isChatSearchProps = ajv.compile(ChatSearchSchema); + +type ChatUpdate = { + roomId: IRoom['_id']; + msgId: string; + text: string; +}; + +const ChatUpdateSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + msgId: { + type: 'string', + }, + text: { + type: 'string', + }, + }, + required: ['roomId', 'msgId', 'text'], + additionalProperties: false, +}; + +export const isChatUpdateProps = ajv.compile(ChatUpdateSchema); + +type ChatGetMessageReadReceipts = { + messageId: IMessage['_id']; +}; + +const ChatGetMessageReadReceiptsSchema = { + type: 'object', + properties: { + messageId: { + type: 'string', + }, + }, + required: ['messageId'], + additionalProperties: false, +}; + +export const isChatGetMessageReadReceiptsProps = ajv.compile(ChatGetMessageReadReceiptsSchema); export type ChatEndpoints = { 'chat.getMessage': { - GET: (params: { msgId: IMessage['_id'] }) => { + GET: (params: ChatGetMessage) => { message: IMessage; }; }; 'chat.followMessage': { - POST: (params: { mid: IMessage['_id'] }) => void; + POST: (params: ChatFollowMessage) => void; }; 'chat.unfollowMessage': { - POST: (params: { mid: IMessage['_id'] }) => void; + POST: (params: ChatUnfollowMessage) => void; }; 'chat.starMessage': { - POST: (params: { messageId: IMessage['_id'] }) => void; + POST: (params: ChatStarMessage) => void; }; 'chat.unStarMessage': { - POST: (params: { messageId: IMessage['_id'] }) => void; + POST: (params: ChatUnstarMessage) => void; }; 'chat.pinMessage': { - POST: (params: { messageId: IMessage['_id'] }) => void; + POST: (params: ChatPinMessage) => void; }; 'chat.unPinMessage': { - POST: (params: { messageId: IMessage['_id'] }) => void; + POST: (params: ChatUnpinMessage) => void; }; 'chat.reportMessage': { - POST: (params: { messageId: IMessage['_id']; description: string }) => void; + POST: (params: ChatReportMessage) => void; }; 'chat.getDiscussions': { - GET: (params: { roomId: IRoom['_id']; text?: string; offset: number; count: number }) => { + GET: (params: ChatGetDiscussions) => { messages: IMessage[]; total: number; }; }; 'chat.getThreadsList': { - GET: (params: { rid: IRoom['_id']; type: 'unread' | 'following' | 'all'; text?: string; offset: number; count: number }) => { + GET: (params: ChatGetThreadsList) => { threads: IMessage[]; total: number; }; }; 'chat.syncThreadsList': { - GET: (params: { rid: IRoom['_id']; updatedSince: string }) => { + GET: (params: ChatSyncThreadsList) => { threads: { update: IMessage[]; remove: IMessage[]; @@ -48,29 +434,29 @@ export type ChatEndpoints = { }; }; 'chat.delete': { - POST: (params: { msgId: string; roomId: string }) => { + POST: (params: ChatDelete) => { _id: string; ts: string; message: Pick; }; }; 'chat.react': { - POST: (params: { emoji: string; messageId: string } | { reaction: string; messageId: string }) => void; + POST: (params: ChatReact) => void; }; 'chat.ignoreUser': { - GET: (params: { rid: string; userId: string; ignore: boolean }) => {}; + GET: (params: ChatIgnoreUser) => {}; }; 'chat.search': { - GET: (params: { roomId: IRoom['_id']; searchText: string; count: number; offset: number }) => { + GET: (params: ChatSearch) => { messages: IMessage[]; }; }; 'chat.update': { - POST: (params: { roomId: IRoom['_id']; msgId: string; text: string }) => { + POST: (params: ChatUpdate) => { messages: IMessage; }; }; 'chat.getMessageReadReceipts': { - GET: (params: { messageId: string }) => { receipts: ReadReceipt[] }; + GET: (params: ChatGetMessageReadReceipts) => { receipts: ReadReceipt[] }; }; }; diff --git a/packages/rest-typings/src/v1/cloud.ts b/packages/rest-typings/src/v1/cloud.ts index c5f56e26f097..e27289566711 100644 --- a/packages/rest-typings/src/v1/cloud.ts +++ b/packages/rest-typings/src/v1/cloud.ts @@ -1,16 +1,82 @@ import type { CloudRegistrationIntentData, CloudConfirmationPollData, CloudRegistrationStatus } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type CloudManualRegister = { + cloudBlob: string; +}; + +const CloudManualRegisterSchema = { + type: 'object', + properties: { + cloudBlob: { + type: 'string', + }, + }, + required: ['cloudBlob'], + additionalProperties: false, +}; + +export const isCloudManualRegisterProps = ajv.compile(CloudManualRegisterSchema); + +type CloudCreateRegistrationIntent = { + resend: boolean; + email: string; +}; + +const CloudCreateRegistrationIntentSchema = { + type: 'object', + properties: { + resend: { + type: 'boolean', + }, + email: { + type: 'string', + }, + }, + required: ['resend', 'email'], + additionalProperties: false, +}; + +export const isCloudCreateRegistrationIntentProps = ajv.compile(CloudCreateRegistrationIntentSchema); + +type CloudConfirmationPoll = { + deviceCode: string; + resend?: string; +}; + +const CloudConfirmationPollSchema = { + type: 'object', + properties: { + deviceCode: { + type: 'string', + }, + resend: { + type: 'string', + nullable: true, + }, + }, + required: ['deviceCode'], + optionalProperties: ['resend'], + additionalProperties: false, +}; + +export const isCloudConfirmationPollProps = ajv.compile(CloudConfirmationPollSchema); export type CloudEndpoints = { 'cloud.manualRegister': { - POST: (params: { cloudBlob: string }) => void; + POST: (params: CloudManualRegister) => void; }; 'cloud.createRegistrationIntent': { - POST: (params: { resend: boolean; email: string }) => { + POST: (params: CloudCreateRegistrationIntent) => { intentData: CloudRegistrationIntentData; }; }; 'cloud.confirmationPoll': { - GET: (params: { deviceCode: string; resend?: boolean }) => { + GET: (params: CloudConfirmationPoll) => { pollData: CloudConfirmationPollData; }; }; diff --git a/packages/rest-typings/src/v1/customSounds.ts b/packages/rest-typings/src/v1/customSounds.ts index 1f39263abb67..84efd66ecb60 100644 --- a/packages/rest-typings/src/v1/customSounds.ts +++ b/packages/rest-typings/src/v1/customSounds.ts @@ -1,10 +1,43 @@ -import type { ICustomSound } from '../../../core-typings/dist'; +import type { ICustomSound } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type CustomSoundsList = PaginatedRequest<{ query: string }>; + +const CustomSoundsListSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + }, + }, + required: ['query'], + additionalProperties: false, +}; + +export const isCustomSoundsListProps = ajv.compile(CustomSoundsListSchema); + export type CustomSoundEndpoint = { 'custom-sounds.list': { - GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ + GET: (params: CustomSoundsList) => PaginatedResult<{ sounds: ICustomSound[]; }>; }; diff --git a/packages/rest-typings/src/v1/customUserStatus.ts b/packages/rest-typings/src/v1/customUserStatus.ts index 943aebb9fdca..dfd30aa9a117 100644 --- a/packages/rest-typings/src/v1/customUserStatus.ts +++ b/packages/rest-typings/src/v1/customUserStatus.ts @@ -1,11 +1,43 @@ import type { ICustomUserStatus, IUserStatus } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type CustomUserStatusListProps = PaginatedRequest<{ query: string }>; + +const CustomUserStatusListSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + }, + }, + required: ['query'], + additionalProperties: false, +}; + +export const isCustomUserStatusListProps = ajv.compile(CustomUserStatusListSchema); + export type CustomUserStatusEndpoints = { 'custom-user-status.list': { - GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ + GET: (params: CustomUserStatusListProps) => PaginatedResult<{ statuses: IUserStatus[]; }>; }; diff --git a/packages/rest-typings/src/v1/directory.ts b/packages/rest-typings/src/v1/directory.ts index fda67ddf9810..af0ad5be6d8d 100644 --- a/packages/rest-typings/src/v1/directory.ts +++ b/packages/rest-typings/src/v1/directory.ts @@ -1,14 +1,42 @@ import type { IRoom } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; +import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type DirectoryProps = PaginatedRequest<{}>; + +const DirectorySchema = { + type: 'object', + properties: { + query: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isDirectoryProps = ajv.compile(DirectorySchema); + export type DirectoryEndpoint = { directory: { - GET: (params: { - query: { [key: string]: string }; - count: number; - offset: number; - sort: { [key: string]: number }; - }) => PaginatedResult<{ result: IRoom[] }>; + GET: (params: DirectoryProps) => PaginatedResult<{ result: IRoom[] }>; }; }; diff --git a/packages/rest-typings/src/v1/dm/DmCloseProps.ts b/packages/rest-typings/src/v1/dm/DmCloseProps.ts new file mode 100644 index 000000000000..1a1ea759680a --- /dev/null +++ b/packages/rest-typings/src/v1/dm/DmCloseProps.ts @@ -0,0 +1,20 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +export type DmCloseProps = { + roomId: string; +}; + +const DmClosePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isDmCloseProps = ajv.compile(DmClosePropsSchema); diff --git a/packages/rest-typings/src/v1/dm/DmHistoryProps.ts b/packages/rest-typings/src/v1/dm/DmHistoryProps.ts new file mode 100644 index 000000000000..0a836961f366 --- /dev/null +++ b/packages/rest-typings/src/v1/dm/DmHistoryProps.ts @@ -0,0 +1,56 @@ +import type { PaginatedRequest } from '@rocket.chat/rest-typings/src/helpers/PaginatedRequest'; +import Ajv from 'ajv'; + +const ajv = new Ajv(); + +export type DmHistoryProps = PaginatedRequest<{ + roomId: string; + latest?: string; + oldest?: string; + inclusive?: 'false' | 'true'; + unreads?: 'true' | 'false'; + showThreadMessages?: 'false' | 'true'; +}>; + +const DmHistoryPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + minLength: 1, + }, + latest: { + type: 'string', + minLength: 1, + }, + showThreadMessages: { + type: 'string', + enum: ['false', 'true'], + }, + oldest: { + type: 'string', + minLength: 1, + }, + inclusive: { + type: 'string', + enum: ['false', 'true'], + }, + unreads: { + type: 'string', + enum: ['true', 'false'], + }, + count: { + type: 'number', + }, + offset: { + type: 'number', + }, + sort: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isDmHistoryProps = ajv.compile(DmHistoryPropsSchema); diff --git a/packages/rest-typings/src/v1/dm/DmKickProps.ts b/packages/rest-typings/src/v1/dm/DmKickProps.ts new file mode 100644 index 000000000000..beca20ef5dc8 --- /dev/null +++ b/packages/rest-typings/src/v1/dm/DmKickProps.ts @@ -0,0 +1,24 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +type DmKickProps = { + roomId: string; + userId: string; +}; + +const DmKickPropsSchema: JSONSchemaType = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + userId: { + type: 'string', + }, + }, + required: ['roomId', 'userId'], + additionalProperties: false, +}; + +export const isDmKickProps = ajv.compile(DmKickPropsSchema); diff --git a/packages/rest-typings/src/v1/dm/DmLeaveProps.ts b/packages/rest-typings/src/v1/dm/DmLeaveProps.ts new file mode 100644 index 000000000000..a91ee8ba9a91 --- /dev/null +++ b/packages/rest-typings/src/v1/dm/DmLeaveProps.ts @@ -0,0 +1,20 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +export type DmLeaveProps = { + roomId: string; +}; + +const DmLeavePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isDmLeaveProps = ajv.compile(DmLeavePropsSchema); diff --git a/packages/rest-typings/src/v1/dm/im.ts b/packages/rest-typings/src/v1/dm/im.ts index 927f7ac3458f..87c4adfe4fa7 100644 --- a/packages/rest-typings/src/v1/dm/im.ts +++ b/packages/rest-typings/src/v1/dm/im.ts @@ -2,9 +2,12 @@ import type { IMessage, IRoom, IUser, IUpload } from '@rocket.chat/core-typings' import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; import type { PaginatedResult } from '../../helpers/PaginatedResult'; +import type { DmCloseProps } from './DmCloseProps'; import type { DmCreateProps } from './DmCreateProps'; import type { DmDeleteProps } from './DmDeleteProps'; import type { DmFileProps } from './DmFileProps'; +import type { DmHistoryProps } from './DmHistoryProps'; +import type { DmLeaveProps } from './DmLeaveProps'; import type { DmMemberProps } from './DmMembersProps'; import type { DmMessagesProps } from './DmMessagesProps'; @@ -18,7 +21,13 @@ export type ImEndpoints = { POST: (params: DmDeleteProps) => void; }; 'im.close': { - POST: (params: { roomId: string }) => void; + POST: (params: DmCloseProps) => void; + }; + 'im.kick': { + POST: (params: DmCloseProps) => void; + }; + 'im.leave': { + POST: (params: DmLeaveProps) => void; }; 'im.counters': { GET: (params: { roomId: string; userId?: string }) => { @@ -37,16 +46,7 @@ export type ImEndpoints = { }>; }; 'im.history': { - GET: ( - params: PaginatedRequest<{ - roomId: string; - latest?: string; - oldest?: string; - inclusive?: string; - unreads?: string; - showThreadMessages?: string; - }>, - ) => { + GET: (params: DmHistoryProps) => { messages: Pick[]; }; }; diff --git a/packages/rest-typings/src/v1/dns.ts b/packages/rest-typings/src/v1/dns.ts index b2d553e036f2..28a630ea3352 100644 --- a/packages/rest-typings/src/v1/dns.ts +++ b/packages/rest-typings/src/v1/dns.ts @@ -1,11 +1,51 @@ +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type DnsResolveTxtProps = { + url: string; +}; + +const dnsResolveTxtPropsSchema = { + type: 'object', + properties: { + url: { + type: 'string', + }, + }, + required: ['url'], + additionalProperties: false, +}; + +export const isDnsResolveTxtProps = ajv.compile(dnsResolveTxtPropsSchema); + +type DnsResolveSrvProps = { + url: string; +}; + +const DnsResolveSrvSchema = { + type: 'object', + properties: { + url: { + type: 'string', + }, + }, + required: ['url'], + additionalProperties: false, +}; + +export const isDnsResolveSrvProps = ajv.compile(DnsResolveSrvSchema); + export type DnsEndpoints = { 'dns.resolve.srv': { - GET: (params: { url: string }) => { + GET: (params: DnsResolveSrvProps) => { resolved: Record; }; }; 'dns.resolve.txt': { - POST: (params: { url: string }) => { + POST: (params: DnsResolveTxtProps) => { resolved: string; // resolved: Record; }; diff --git a/packages/rest-typings/src/v1/e2e.ts b/packages/rest-typings/src/v1/e2e.ts index d8bf5f77312c..62ce4110ddbb 100644 --- a/packages/rest-typings/src/v1/e2e.ts +++ b/packages/rest-typings/src/v1/e2e.ts @@ -1,19 +1,107 @@ +/* eslint-disable @typescript-eslint/camelcase */ import type { IUser } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type E2eSetUserPublicAndPrivateKeysProps = { + public_key: string; + private_key: string; +}; + +const E2eSetUserPublicAndPrivateKeysSchema = { + type: 'object', + properties: { + public_key: { + type: 'string', + }, + private_key: { + type: 'string', + }, + }, + required: ['public_key', 'private_key'], + additionalProperties: false, +}; + +export const isE2eSetUserPublicAndPrivateKeysProps = ajv.compile(E2eSetUserPublicAndPrivateKeysSchema); + +type E2eGetUsersOfRoomWithoutKeyProps = { rid: string }; + +const E2eGetUsersOfRoomWithoutKeySchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +export const isE2eGetUsersOfRoomWithoutKeyProps = ajv.compile(E2eGetUsersOfRoomWithoutKeySchema); + +type E2eUpdateGroupKeyProps = { + uid: string; + rid: string; + key: string; +}; + +const E2eUpdateGroupKeySchema = { + type: 'object', + properties: { + uid: { + type: 'string', + }, + rid: { + type: 'string', + }, + key: { + type: 'string', + }, + }, + required: ['uid', 'rid', 'key'], + additionalProperties: false, +}; + +export const isE2eUpdateGroupKeyProps = ajv.compile(E2eUpdateGroupKeySchema); + +type E2eSetRoomKeyIdProps = { + rid: string; + keyID: string; +}; + +const E2eSetRoomKeyIdSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + keyID: { + type: 'string', + }, + }, + required: ['rid', 'keyID'], + additionalProperties: false, +}; + +export const isE2eSetRoomKeyIdProps = ajv.compile(E2eSetRoomKeyIdSchema); export type E2eEndpoints = { 'e2e.setUserPublicAndPrivateKeys': { - POST: (params: { public_key: string; private_key: string }) => void; + POST: (params: E2eSetUserPublicAndPrivateKeysProps) => void; }; 'e2e.getUsersOfRoomWithoutKey': { - GET: (params: { rid: string }) => { + GET: (params: E2eGetUsersOfRoomWithoutKeyProps) => { users: Pick[]; }; }; 'e2e.updateGroupKey': { - POST: (params: { uid: string; rid: string; key: string }) => {}; + POST: (params: E2eUpdateGroupKeyProps) => {}; }; 'e2e.setRoomKeyID': { - POST: (params: { rid: string; keyID: string }) => {}; + POST: (params: E2eSetRoomKeyIdProps) => {}; }; 'e2e.fetchMyKeys': { GET: () => { public_key: string; private_key: string }; diff --git a/packages/rest-typings/src/v1/email-inbox.ts b/packages/rest-typings/src/v1/email-inbox.ts index afa4745ed86d..d0347fe7e829 100644 --- a/packages/rest-typings/src/v1/email-inbox.ts +++ b/packages/rest-typings/src/v1/email-inbox.ts @@ -1,43 +1,174 @@ import type { IEmailInbox } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type EmailInboxListProps = PaginatedRequest<{ query?: string }>; + +const EmailInboxListPropsSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isEmailInboxList = ajv.compile(EmailInboxListPropsSchema); + +type EmailInboxProps = { + _id?: string; + name: string; + email: string; + active: boolean; // POST method + description: string; + senderInfo: string; + department: string; + smtp: { + password: string; + port: number; + secure: boolean; + server: string; + username: string; + }; + imap: { + password: string; + port: number; + secure: boolean; + server: string; + username: string; + }; +}; + +const EmailInboxPropsSchema = { + type: 'object', + properties: { + _id: { + type: 'string', + nullable: true, + }, + name: { + type: 'string', + }, + email: { + type: 'string', + }, + active: { + type: 'boolean', + }, + description: { + type: 'string', + }, + senderInfo: { + type: 'string', + }, + department: { + type: 'string', + }, + + smtp: { + type: 'object', + properties: { + password: { + type: 'string', + }, + port: { + type: 'number', + }, + secure: { + type: 'boolean', + }, + server: { + type: 'string', + }, + username: { + type: 'string', + }, + }, + required: ['password', 'port', 'secure', 'server', 'username'], + additionalProperties: false, + }, + + imap: { + type: 'object', + properties: { + password: { + type: 'string', + }, + port: { + type: 'number', + }, + secure: { + type: 'boolean', + }, + server: { + type: 'string', + }, + username: { + type: 'string', + }, + }, + required: ['password', 'port', 'secure', 'server', 'username'], + additionalProperties: false, + }, + }, + + required: ['name', 'email', 'active', 'description', 'senderInfo', 'department', 'smtp', 'imap'], + additionalProperties: false, +}; + +export const isEmailInbox = ajv.compile(EmailInboxPropsSchema); + +type EmailInboxSearchProps = { + email: string; +}; + +const EmailInboxSearchPropsSchema = { + type: 'object', + properties: { + email: { + type: 'string', + }, + }, + required: ['email'], + additionalProperties: false, +}; + +export const isEmailInboxSearch = ajv.compile(EmailInboxSearchPropsSchema); + export type EmailInboxEndpoints = { 'email-inbox.list': { - GET: (params: PaginatedRequest<{ query?: string }>) => PaginatedResult<{ emailInboxes: IEmailInbox[] }>; + GET: (params: EmailInboxListProps) => PaginatedResult<{ emailInboxes: IEmailInbox[] }>; }; 'email-inbox': { - POST: (params: { - _id?: string; - name: string; - email: string; - active: boolean; - description: string; - senderInfo: string; - department: string; - smtp: { - password: string; - port: number; - secure: boolean; - server: string; - username: string; - }; - imap: { - password: string; - port: number; - secure: boolean; - server: string; - username: string; - }; - }) => { _id: string }; + POST: (params: EmailInboxProps) => { _id: string }; }; 'email-inbox/:_id': { GET: (params: void) => IEmailInbox | null; DELETE: (params: void) => { _id: string }; }; 'email-inbox.search': { - GET: (params: { email: string }) => { emailInbox: IEmailInbox | null }; + GET: (params: EmailInboxSearchProps) => { emailInbox: IEmailInbox | null }; }; 'email-inbox.send-test/:_id': { POST: (params: void) => { _id: string }; diff --git a/packages/rest-typings/src/v1/emojiCustom.ts b/packages/rest-typings/src/v1/emojiCustom.ts index 0a52dddc940e..2532879dd9a9 100644 --- a/packages/rest-typings/src/v1/emojiCustom.ts +++ b/packages/rest-typings/src/v1/emojiCustom.ts @@ -1,8 +1,47 @@ import type { ICustomEmojiDescriptor } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type emojiCustomDeleteProps = { + emojiId: ICustomEmojiDescriptor['_id']; +}; + +const emojiCustomDeletePropsSchema = { + type: 'object', + properties: { + emojiId: { + type: 'string', + }, + }, + required: ['emojiId'], + additionalProperties: false, +}; + +export const isEmojiCustomDelete = ajv.compile(emojiCustomDeletePropsSchema); + +type emojiCustomList = { + query: string; +}; + +const emojiCustomListSchema = { + type: 'object', + properties: { + query: { + type: 'string', + }, + }, + required: ['query'], + additionalProperties: false, +}; + +export const isemojiCustomList = ajv.compile(emojiCustomListSchema); + export type EmojiCustomEndpoints = { 'emoji-custom.all': { GET: (params: PaginatedRequest<{ query: string }, 'name'>) => { @@ -10,13 +49,13 @@ export type EmojiCustomEndpoints = { } & PaginatedResult; }; 'emoji-custom.list': { - GET: (params: { query: string }) => { + GET: (params: emojiCustomList) => { emojis?: { update: ICustomEmojiDescriptor[]; }; }; }; 'emoji-custom.delete': { - POST: (params: { emojiId: ICustomEmojiDescriptor['_id'] }) => void; + POST: (params: emojiCustomDeleteProps) => void; }; }; diff --git a/packages/rest-typings/src/v1/groups.ts b/packages/rest-typings/src/v1/groups.ts index 5cb5db378ffc..10540a3ecd62 100644 --- a/packages/rest-typings/src/v1/groups.ts +++ b/packages/rest-typings/src/v1/groups.ts @@ -1,16 +1,330 @@ import type { IMessage, IRoom, ITeam, IGetRoomRoles, IUser, IUpload } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type GroupsFilesProps = { + roomId: IRoom['_id']; + count: number; + sort: string; + query: string; +}; + +const GroupsFilesPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + count: { + type: 'number', + }, + sort: { + type: 'string', + }, + query: { + type: 'string', + }, + }, + required: ['roomId', 'count', 'sort', 'query'], + additionalProperties: false, +}; + +export const isGroupsFilesProps = ajv.compile(GroupsFilesPropsSchema); + +type GroupsMembersProps = { + roomId: IRoom['_id']; + offset?: number; + count?: number; + filter?: string; + status?: string[]; +}; + +const GroupsMembersPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + offset: { + type: 'number', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + filter: { + type: 'string', + nullable: true, + }, + status: { + type: 'array', + items: { type: 'string' }, + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsMembersProps = ajv.compile(GroupsMembersPropsSchema); + +type GroupsArchiveProps = { + roomId: IRoom['_id']; +}; + +const GroupsArchivePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsArchiveProps = ajv.compile(GroupsArchivePropsSchema); + +type GroupsUnarchiveProps = { + roomId: IRoom['_id']; +}; + +const GroupsUnarchivePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsUnarchiveProps = ajv.compile(GroupsUnarchivePropsSchema); + +type GroupsCreateProps = { + name: string; + members: string[]; + readOnly: boolean; + extraData: { + broadcast: boolean; + encrypted: boolean; + teamId?: string; + }; +}; + +const GroupsCreatePropsSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + members: { + type: 'array', + items: { type: 'string' }, + }, + readOnly: { + type: 'boolean', + }, + extraData: { + type: 'object', + properties: { + broadcast: { + type: 'boolean', + }, + encrypted: { + type: 'boolean', + }, + teamId: { + type: 'string', + nullable: true, + }, + }, + required: ['broadcast', 'encrypted'], + additionalProperties: false, + }, + }, + required: ['name', 'members', 'readOnly', 'extraData'], + additionalProperties: false, +}; + +export const isGroupsCreateProps = ajv.compile(GroupsCreatePropsSchema); + +type GroupsConvertToTeamProps = { + roomId: string; + roomName: string; +}; + +const GroupsConvertToTeamPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + roomName: { + type: 'string', + }, + }, + required: ['roomId', 'roomName'], + additionalProperties: false, +}; + +export const isGroupsConvertToTeamProps = ajv.compile(GroupsConvertToTeamPropsSchema); + +type GroupsCountersProps = { + roomId: string; +}; + +const GroupsCountersPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsCountersProps = ajv.compile(GroupsCountersPropsSchema); + +type GroupsCloseProps = { + roomId: string; +}; + +const GroupsClosePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsCloseProps = ajv.compile(GroupsClosePropsSchema); + +type GroupsDeleteProps = { + roomId: string; +}; + +const GroupsDeletePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsDeleteProps = ajv.compile(GroupsDeletePropsSchema); + +type GroupsLeaveProps = { + roomId: string; +}; + +const GroupsLeavePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsLeaveProps = ajv.compile(GroupsLeavePropsSchema); + +type GroupsRolesProps = { + roomId: string; +}; + +const GroupsRolesPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsRolesProps = ajv.compile(GroupsRolesPropsSchema); + +type GroupsKickProps = { + roomId: string; + userId: string; +}; + +const GroupsKickPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + userId: { + type: 'string', + }, + }, + required: ['roomId', 'userId'], + additionalProperties: false, +}; + +export const isGroupsKickProps = ajv.compile(GroupsKickPropsSchema); + +type GroupsMessageProps = PaginatedRequest<{ + roomId: IRoom['_id']; +}>; + +const GroupsMessagePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsMessageProps = ajv.compile(GroupsMessagePropsSchema); + export type GroupsEndpoints = { 'groups.files': { - GET: (params: PaginatedRequest<{ roomId: IRoom['_id']; query: string }>) => PaginatedResult<{ + GET: (params: GroupsFilesProps) => PaginatedResult<{ files: IUpload[]; }>; }; 'groups.members': { - GET: (params: { roomId: IRoom['_id']; offset?: number; count?: number; filter?: string; status?: string[] }) => { + GET: (params: GroupsMembersProps) => { count: number; offset: number; members: IUser[]; @@ -23,30 +337,21 @@ export type GroupsEndpoints = { }>; }; 'groups.archive': { - POST: (params: { roomId: string }) => void; + POST: (params: GroupsArchiveProps) => void; }; 'groups.unarchive': { - POST: (params: { roomId: string }) => void; + POST: (params: GroupsUnarchiveProps) => void; }; 'groups.create': { - POST: (params: { - name: string; - members: string[]; - readOnly: boolean; - extraData: { - broadcast: boolean; - encrypted: boolean; - teamId?: string; - }; - }) => { + POST: (params: GroupsCreateProps) => { group: Partial; }; }; 'groups.convertToTeam': { - POST: (params: { roomId: string; roomName: string }) => { team: ITeam }; + POST: (params: GroupsConvertToTeamProps) => { team: ITeam }; }; 'groups.counters': { - GET: (params: { roomId: string }) => { + GET: (params: GroupsCountersProps) => { joined: boolean; members: number; unreads: number; @@ -57,28 +362,23 @@ export type GroupsEndpoints = { }; }; 'groups.close': { - POST: (params: { roomId: string }) => {}; + POST: (params: GroupsCloseProps) => {}; }; 'groups.kick': { - POST: (params: { roomId: string; userId: string }) => {}; + POST: (params: GroupsKickProps) => {}; }; 'groups.delete': { - POST: (params: { roomId: string }) => {}; + POST: (params: GroupsDeleteProps) => {}; }; 'groups.leave': { - POST: (params: { roomId: string }) => {}; + POST: (params: GroupsLeaveProps) => {}; }; 'groups.roles': { - GET: (params: { roomId: string }) => { roles: IGetRoomRoles[] }; + GET: (params: GroupsRolesProps) => { roles: IGetRoomRoles[] }; }; 'groups.messages': { - GET: (params: { - roomId: IRoom['_id']; - query: { 'mentions._id': { $in: string[] } } | { 'starred._id': { $in: string[] } } | { pinned: boolean }; - offset: number; - sort: { ts: number }; - }) => { + GET: (params: GroupsMessageProps) => PaginatedResult<{ messages: IMessage[]; - }; + }>; }; }; diff --git a/packages/rest-typings/src/v1/invites.ts b/packages/rest-typings/src/v1/invites.ts index 200302c99711..fda658af2267 100644 --- a/packages/rest-typings/src/v1/invites.ts +++ b/packages/rest-typings/src/v1/invites.ts @@ -1,4 +1,43 @@ import type { IInvite, IRoom } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type v1UseInviteTokenProps = { + token: string; +}; + +const v1UseInviteTokenPropsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isV1UseInviteTokenProps = ajv.compile(v1UseInviteTokenPropsSchema); + +type v1ValidateInviteTokenProps = { + token: string; +}; + +const v1ValidateInviteTokenPropsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isV1ValidateInviteTokenProps = ajv.compile(v1ValidateInviteTokenPropsSchema); export type InvitesEndpoints = { 'listInvites': { @@ -8,7 +47,7 @@ export type InvitesEndpoints = { DELETE: () => void; }; '/v1/useInviteToken': { - POST: (params: { token: string }) => { + POST: (params: v1UseInviteTokenProps) => { room: { rid: IRoom['_id']; prid: IRoom['prid']; @@ -19,6 +58,6 @@ export type InvitesEndpoints = { }; }; '/v1/validateInviteToken': { - POST: (params: { token: string }) => { valid: boolean }; + POST: (params: v1ValidateInviteTokenProps) => { valid: boolean }; }; }; diff --git a/packages/rest-typings/src/v1/ldap.ts b/packages/rest-typings/src/v1/ldap.ts index 45c482c0957d..3fd343cf4c03 100644 --- a/packages/rest-typings/src/v1/ldap.ts +++ b/packages/rest-typings/src/v1/ldap.ts @@ -1,3 +1,26 @@ +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type ldapTestSearchProps = { + username: string; +}; + +const ldapTestSearchPropsSchema = { + type: 'object', + properties: { + username: { + type: 'string', + }, + }, + required: ['username'], + additionalProperties: false, +}; + +export const isLdapTestSearch = ajv.compile(ldapTestSearchPropsSchema); + export type LDAPEndpoints = { 'ldap.testConnection': { POST: () => { @@ -5,7 +28,7 @@ export type LDAPEndpoints = { }; }; 'ldap.testSearch': { - POST: (params: { username: string }) => { + POST: (params: ldapTestSearchProps) => { message: string; }; }; diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index f1bb124a956b..de477d37a813 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,11 +1,33 @@ import type { ILicense } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type licensesAddProps = { + license: string; +}; + +const licensesAddPropsSchema = { + type: 'object', + properties: { + license: { + type: 'string', + }, + }, + required: ['license'], + additionalProperties: false, +}; + +export const isLicensesAddProps = ajv.compile(licensesAddPropsSchema); export type LicensesEndpoints = { 'licenses.get': { GET: () => { licenses: Array }; }; 'licenses.add': { - POST: (params: { license: string }) => void; + POST: (params: licensesAddProps) => void; }; 'licenses.maxActiveUsers': { GET: () => { maxActiveUsers: number | null; activeUsers: number }; diff --git a/packages/rest-typings/src/v1/oauthapps.ts b/packages/rest-typings/src/v1/oauthapps.ts index 89f993d50b21..4928257efd1e 100644 --- a/packages/rest-typings/src/v1/oauthapps.ts +++ b/packages/rest-typings/src/v1/oauthapps.ts @@ -1,7 +1,9 @@ import type { IOAuthApps, IUser } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; -const ajv = new Ajv(); +const ajv = new Ajv({ + coerceTypes: true, +}); export type OauthAppsGetParams = { clientId: string } | { appId: string }; diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 3b29ad0b9274..fc2ab9e23fd9 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -13,12 +13,770 @@ import type { IRoom, ISetting, } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; type booleanString = 'true' | 'false'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type LivechatVisitorsInfo = { + visitorId: string; +}; + +const LivechatVisitorsInfoSchema = { + type: 'object', + properties: { + visitorId: { + type: 'string', + }, + }, + required: ['visitorId'], + additionalProperties: false, +}; + +export const isLivechatVisitorsInfoProps = ajv.compile(LivechatVisitorsInfoSchema); + +type LivechatRoomOnHold = { + roomId: IRoom['_id']; +}; + +const LivechatRoomOnHoldSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isLivechatRoomOnHoldProps = ajv.compile(LivechatRoomOnHoldSchema); + +type LivechatDepartmentId = { + onlyMyDepartments?: booleanString; + includeAgents?: booleanString; +}; + +const LivechatDepartmentIdSchema = { + type: 'object', + properties: { + onlyMyDepartments: { + type: 'string', + nullable: true, + }, + includeAgents: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isLivechatDepartmentIdProps = ajv.compile(LivechatDepartmentIdSchema); + +type LivechatDepartmentAutocomplete = { + selector: string; + onlyMyDepartments: booleanString; +}; + +const LivechatDepartmentAutocompleteSchema = { + type: 'object', + properties: { + selector: { + type: 'string', + }, + onlyMyDepartments: { + type: 'string', + }, + }, + required: ['selector', 'onlyMyDepartments'], + additionalProperties: false, +}; + +export const isLivechatDepartmentAutocompleteProps = ajv.compile(LivechatDepartmentAutocompleteSchema); + +type LivechatDepartmentDepartmentIdAgentsGET = { + sort: string; +}; + +const LivechatDepartmentDepartmentIdAgentsGETSchema = { + type: 'object', + properties: { + sort: { + type: 'string', + }, + }, + required: ['sort'], + additionalProperties: false, +}; + +export const isLivechatDepartmentDepartmentIdAgentsGETProps = ajv.compile( + LivechatDepartmentDepartmentIdAgentsGETSchema, +); + +type LivechatDepartmentDepartmentIdAgentsPOST = { + upsert: string[]; + remove: string[]; +}; + +const LivechatDepartmentDepartmentIdAgentsPOSTSchema = { + type: 'object', + properties: { + upsert: { + type: 'array', + items: { + type: 'string', + }, + }, + remove: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + required: ['upsert', 'remove'], + additionalProperties: false, +}; + +export const isLivechatDepartmentDepartmentIdAgentsPOSTProps = ajv.compile( + LivechatDepartmentDepartmentIdAgentsPOSTSchema, +); + +type LivechatVisitorTokenGet = { + token: string; +}; + +const LivechatVisitorTokenGetSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isLivechatVisitorTokenGetProps = ajv.compile(LivechatVisitorTokenGetSchema); + +type LivechatVisitorTokenDelete = { + token: string; +}; + +const LivechatVisitorTokenDeleteSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isLivechatVisitorTokenDeleteProps = ajv.compile(LivechatVisitorTokenDeleteSchema); + +type LivechatVisitorTokenRoom = { + token: string; +}; + +const LivechatVisitorTokenRoomSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isLivechatVisitorTokenRoomProps = ajv.compile(LivechatVisitorTokenRoomSchema); + +type LivechatVisitorCallStatus = { + token: string; + callStatus: string; + rid: string; + callId: string; +}; + +const LivechatVisitorCallStatusSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + callStatus: { + type: 'string', + }, + rid: { + type: 'string', + }, + callId: { + type: 'string', + }, + }, + required: ['token', 'callStatus', 'rid', 'callId'], + additionalProperties: false, +}; + +export const isLivechatVisitorCallStatusProps = ajv.compile(LivechatVisitorCallStatusSchema); + +type LivechatVisitorStatus = { + token: string; + status: string; +}; + +const LivechatVisitorStatusSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + status: { + type: 'string', + }, + }, + required: ['token', 'status'], + additionalProperties: false, +}; + +export const isLivechatVisitorStatusProps = ajv.compile(LivechatVisitorStatusSchema); + +type LiveChatRoomJoin = { + roomId: string; +}; + +const LiveChatRoomJoinSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isLiveChatRoomJoinProps = ajv.compile(LiveChatRoomJoinSchema); + +type LivechatMonitorsListProps = PaginatedRequest<{ text: string }>; + +const LivechatMonitorsListSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatMonitorsListProps = ajv.compile(LivechatMonitorsListSchema); + +type LivechatTagsListProps = PaginatedRequest<{ text: string }, 'name'>; + +const LivechatTagsListSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatTagsListProps = ajv.compile(LivechatTagsListSchema); + +type LivechatDepartmentProps = PaginatedRequest<{ + text: string; + onlyMyDepartments?: booleanString; + enabled?: booleanString; + excludeDepartmentId?: string; +}>; + +const LivechatDepartmentSchema = { + type: 'object', + properties: { + text: { + type: 'string', + nullable: true, + }, + onlyMyDepartments: { + type: 'string', + enum: ['true', 'false'], + nullable: true, + }, + enabled: { + type: 'string', + nullable: true, + }, + excludeDepartmentId: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + fields: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isLivechatDepartmentProps = ajv.compile(LivechatDepartmentSchema); + +type LivechatDepartmentsAvailableByUnitIdProps = PaginatedRequest<{ text: string }>; + +const LivechatDepartmentsAvailableByUnitIdSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatDepartmentsAvailableByUnitIdProps = ajv.compile( + LivechatDepartmentsAvailableByUnitIdSchema, +); + +type LivechatDepartmentsByUnitProps = PaginatedRequest<{ text: string }>; + +const LivechatDepartmentsByUnitSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatDepartmentsByUnitProps = ajv.compile(LivechatDepartmentsByUnitSchema); + +type LivechatDepartmentsByUnitIdProps = PaginatedRequest<{ text: string }>; + +const LivechatDepartmentsByUnitIdSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatDepartmentsByUnitIdProps = ajv.compile(LivechatDepartmentsByUnitIdSchema); + +type LivechatUsersManagerGETProps = PaginatedRequest<{ text?: string }>; + +const LivechatUsersManagerGETSchema = { + type: 'object', + properties: { + text: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isLivechatUsersManagerGETProps = ajv.compile(LivechatUsersManagerGETSchema); + +type LivechatUsersManagerPOSTProps = PaginatedRequest<{ username: string }>; + +const LivechatUsersManagerPOSTSchema = { + type: 'object', + properties: { + username: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['username'], + additionalProperties: false, +}; + +export const isLivechatUsersManagerPOSTProps = ajv.compile(LivechatUsersManagerPOSTSchema); + +type LivechatQueueProps = { + agentId?: string; + includeOfflineAgents?: booleanString; + departmentId?: string; + offset: number; + count: number; + sort: string; +}; + +const LivechatQueuePropsSchema = { + type: 'object', + properties: { + agentId: { + type: 'string', + nullable: true, + }, + includeOfflineAgents: { + type: 'string', + nullable: true, + }, + departmentId: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + }, + offset: { + type: 'number', + }, + sort: { + type: 'string', + }, + }, + required: ['count', 'offset', 'sort'], + additionalProperties: false, +}; + +export const isLivechatQueueProps = ajv.compile(LivechatQueuePropsSchema); + +type CannedResponsesProps = PaginatedRequest<{ + scope?: string; + departmentId?: string; + text?: string; +}>; + +const CannedResponsesPropsSchema = { + type: 'object', + properties: { + scope: { + type: 'string', + nullable: true, + }, + departmentId: { + type: 'string', + nullable: true, + }, + text: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isCannedResponsesProps = ajv.compile(CannedResponsesPropsSchema); + +type LivechatCustomFieldsProps = PaginatedRequest<{ text: string }>; + +const LivechatCustomFieldsSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatCustomFieldsProps = ajv.compile(LivechatCustomFieldsSchema); + +type LivechatRoomsProps = { + guest: string; + fname: string; + servedBy: string[]; + status: string; + department: string; + from: string; + to: string; + customFields: any; + current: number; + itemsPerPage: number; + tags: string[]; +}; + +const LivechatRoomsSchema = { + type: 'object', + properties: { + guest: { + type: 'string', + }, + fname: { + type: 'string', + }, + servedBy: { + type: 'array', + items: { + type: 'string', + }, + }, + status: { + type: 'string', + }, + department: { + type: 'string', + }, + from: { + type: 'string', + }, + to: { + type: 'string', + }, + customFields: { + type: 'object', + nullable: true, + }, + current: { + type: 'number', + }, + itemsPerPage: { + type: 'number', + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + required: ['guest', 'fname', 'servedBy', 'status', 'department', 'from', 'to', 'current', 'itemsPerPage'], + additionalProperties: false, +}; + +export const isLivechatRoomsProps = ajv.compile(LivechatRoomsSchema); + +type LivechatRidMessagesProps = PaginatedRequest<{ query: string }>; + +const LivechatRidMessagesSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + }, + }, + required: ['query'], + additionalProperties: false, +}; + +export const isLivechatRidMessagesProps = ajv.compile(LivechatRidMessagesSchema); + +type LivechatUsersAgentProps = PaginatedRequest<{ text?: string }>; + +const LivechatUsersAgentSchema = { + type: 'object', + properties: { + text: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isLivechatUsersAgentProps = ajv.compile(LivechatUsersAgentSchema); + export type OmnichannelEndpoints = { 'livechat/appearance': { GET: () => { @@ -26,7 +784,7 @@ export type OmnichannelEndpoints = { }; }; 'livechat/visitors.info': { - GET: (params: { visitorId: string }) => { + GET: (params: LivechatVisitorsInfo) => { visitor: { visitorEmails: Array<{ address: string; @@ -35,30 +793,23 @@ export type OmnichannelEndpoints = { }; }; 'livechat/room.onHold': { - POST: (params: { roomId: IRoom['_id'] }) => void; + POST: (params: LivechatRoomOnHold) => void; }; 'livechat/room.join': { - GET: (params: { roomId: IRoom['_id'] }) => { success: boolean }; + GET: (params: LiveChatRoomJoin) => { success: boolean }; }; 'livechat/monitors.list': { - GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + GET: (params: LivechatMonitorsListProps) => PaginatedResult<{ monitors: ILivechatMonitor[]; }>; }; 'livechat/tags.list': { - GET: (params: PaginatedRequest<{ text: string }, 'name'>) => PaginatedResult<{ + GET: (params: LivechatTagsListProps) => PaginatedResult<{ tags: ILivechatTag[]; }>; }; 'livechat/department': { - GET: ( - params: PaginatedRequest<{ - text: string; - onlyMyDepartments?: booleanString; - enabled?: boolean; - excludeDepartmentId?: string; - }>, - ) => PaginatedResult<{ + GET: (params: LivechatDepartmentProps) => PaginatedResult<{ departments: ILivechatDepartment[]; }>; POST: (params: { department: Partial; agents: string[] }) => { @@ -67,7 +818,7 @@ export type OmnichannelEndpoints = { }; }; 'livechat/department/:_id': { - GET: (params: { onlyMyDepartments?: booleanString; includeAgents?: booleanString }) => { + GET: (params: LivechatDepartmentId) => { department: ILivechatDepartmentRecord | null; agents?: ILivechatDepartmentAgents[]; }; @@ -78,27 +829,27 @@ export type OmnichannelEndpoints = { DELETE: () => void; }; 'livechat/department.autocomplete': { - GET: (params: { selector: string; onlyMyDepartments: booleanString }) => { + GET: (params: LivechatDepartmentAutocomplete) => { items: ILivechatDepartment[]; }; }; 'livechat/department/:departmentId/agents': { - GET: (params: { sort: string }) => PaginatedResult<{ agents: ILivechatDepartmentAgents[] }>; - POST: (params: { upsert: string[]; remove: string[] }) => void; + GET: (params: LivechatDepartmentDepartmentIdAgentsGET) => PaginatedResult<{ agents: ILivechatDepartmentAgents[] }>; + POST: (params: LivechatDepartmentDepartmentIdAgentsPOST) => void; }; 'livechat/departments.available-by-unit/:id': { - GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + GET: (params: LivechatDepartmentsAvailableByUnitIdProps) => PaginatedResult<{ departments: ILivechatDepartment[]; }>; }; 'livechat/departments.by-unit/': { - GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + GET: (params: LivechatDepartmentsByUnitProps) => PaginatedResult<{ departments: ILivechatDepartment[]; }>; }; 'livechat/departments.by-unit/:id': { - GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + GET: (params: LivechatDepartmentsByUnitIdProps) => PaginatedResult<{ departments: ILivechatDepartment[]; }>; }; @@ -110,7 +861,7 @@ export type OmnichannelEndpoints = { }; 'livechat/custom-fields': { - GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + GET: (params: LivechatCustomFieldsProps) => PaginatedResult<{ customFields: [ { _id: string; @@ -120,28 +871,23 @@ export type OmnichannelEndpoints = { }>; }; 'livechat/rooms': { - GET: (params: { - guest: string; - fname: string; - servedBy: string[]; - status: string; - department: string; - from: string; - to: string; - customFields: any; - current: number; - itemsPerPage: number; - tags: string[]; - }) => PaginatedResult<{ + GET: (params: LivechatRoomsProps) => PaginatedResult<{ rooms: IOmnichannelRoom[]; }>; }; 'livechat/:rid/messages': { - GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ + GET: (params: LivechatRidMessagesProps) => PaginatedResult<{ messages: IMessage[]; }>; }; + 'livechat/users/manager': { + GET: (params: LivechatUsersManagerGETProps) => PaginatedResult<{ + users: ILivechatAgent[]; + }>; + POST: (params: { username: string }) => { success: boolean }; + }; + 'livechat/users/manager/:_id': { GET: ( params: PaginatedRequest<{ @@ -151,11 +897,11 @@ export type OmnichannelEndpoints = { DELETE: () => void; }; - 'livechat/users/manager': { + 'livechat/users/agent': { GET: (params: PaginatedRequest<{ text?: string }>) => PaginatedResult<{ users: ILivechatAgent[]; }>; - POST: (params: { username: string }) => { success: boolean }; + POST: (params: LivechatUsersManagerPOSTProps) => { success: boolean }; }; 'livechat/users/agent/:_id': { @@ -167,13 +913,6 @@ export type OmnichannelEndpoints = { DELETE: () => { success: boolean }; }; - 'livechat/users/agent': { - GET: (params: PaginatedRequest<{ text?: string }>) => PaginatedResult<{ - users: ILivechatAgent[]; - }>; - POST: (params: { username: string }) => { success: boolean }; - }; - 'livechat/visitor': { POST: (params: { visitor: ILivechatVisitorDTO }) => { visitor: ILivechatVisitor; @@ -181,39 +920,32 @@ export type OmnichannelEndpoints = { }; 'livechat/visitor/:token': { - GET: (params: { token: string }) => { visitor: ILivechatVisitor }; - DELETE: (params: { token: string }) => { + GET: (params: LivechatVisitorTokenGet) => { visitor: ILivechatVisitor }; + DELETE: (params: LivechatVisitorTokenDelete) => { visitor: { _id: string; ts: string }; }; }; 'livechat/visitor/:token/room': { - GET: (params: { token: string }) => { rooms: IOmnichannelRoom[] }; + GET: (params: LivechatVisitorTokenRoom) => { rooms: IOmnichannelRoom[] }; }; 'livechat/visitor.callStatus': { - POST: (params: { token: string; callStatus: string; rid: string; callId: string }) => { + POST: (params: LivechatVisitorCallStatus) => { token: string; callStatus: string; }; }; 'livechat/visitor.status': { - POST: (params: { token: string; status: string }) => { + POST: (params: LivechatVisitorStatus) => { token: string; status: string; }; }; 'livechat/queue': { - GET: (params: { - agentId?: ILivechatAgent['_id']; - includeOfflineAgents?: boolean; - departmentId?: ILivechatAgent['_id']; - offset: number; - count: number; - sort: string; - }) => { + GET: (params: LivechatQueueProps) => { queue: { chats: number; department: { _id: string; name: string }; @@ -229,13 +961,7 @@ export type OmnichannelEndpoints = { }; 'canned-responses': { - GET: ( - params: PaginatedRequest<{ - scope?: string; - departmentId?: string; - text?: string; - }>, - ) => PaginatedResult<{ + GET: (params: CannedResponsesProps) => PaginatedResult<{ cannedResponses: IOmnichannelCannedResponse[]; }>; }; diff --git a/packages/rest-typings/src/v1/permissions.ts b/packages/rest-typings/src/v1/permissions.ts index 85506bf1aff8..3128db4ec259 100644 --- a/packages/rest-typings/src/v1/permissions.ts +++ b/packages/rest-typings/src/v1/permissions.ts @@ -1,13 +1,33 @@ -import Ajv, { JSONSchemaType } from 'ajv'; +import Ajv from 'ajv'; import type { IPermission } from '@rocket.chat/core-typings'; -const ajv = new Ajv(); +const ajv = new Ajv({ + coerceTypes: true, +}); + +type PermissionsListAllProps = { + updatedSince?: string; +}; + +const permissionListAllSchema = { + type: 'object', + properties: { + updatedSince: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isPermissionsListAll = ajv.compile(permissionListAllSchema); type PermissionsUpdateProps = { permissions: { _id: string; roles: string[] }[]; }; -const permissionUpdatePropsSchema: JSONSchemaType = { +const permissionUpdatePropsSchema = { type: 'object', properties: { permissions: { @@ -31,11 +51,11 @@ const permissionUpdatePropsSchema: JSONSchemaType = { additionalProperties: false, }; -export const isBodyParamsValidPermissionUpdate = ajv.compile(permissionUpdatePropsSchema); +export const isBodyParamsValidPermissionUpdate = ajv.compile(permissionUpdatePropsSchema); export type PermissionsEndpoints = { 'permissions.listAll': { - GET: (params: { updatedSince?: string }) => { + GET: (params: PermissionsListAllProps) => { update: IPermission[]; remove: IPermission[]; }; diff --git a/packages/rest-typings/src/v1/push.ts b/packages/rest-typings/src/v1/push.ts index d0b07c6afbca..54568328bc42 100644 --- a/packages/rest-typings/src/v1/push.ts +++ b/packages/rest-typings/src/v1/push.ts @@ -1,12 +1,64 @@ import type { IMessage, IPushNotificationConfig, IPushTokenTypes, IPushToken } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type PushTokenProps = { + id?: string; + type: IPushTokenTypes; + value: string; + appName: string; +}; + +const PushTokenPropsSchema = { + type: 'object', + properties: { + id: { + type: 'string', + nullable: true, + }, + type: { + type: 'string', + }, + value: { + type: 'string', + }, + appName: { + type: 'string', + }, + }, + required: ['type', 'value', 'appName'], + additionalProperties: false, +}; + +export const isPushTokenProps = ajv.compile(PushTokenPropsSchema); + +type PushGetProps = { + id: string; +}; + +const PushGetPropsSchema = { + type: 'object', + properties: { + id: { + type: 'string', + }, + }, + required: ['id'], + additionalProperties: false, +}; + +export const isPushGetProps = ajv.compile(PushGetPropsSchema); export type PushEndpoints = { 'push.token': { - POST: (payload: { id?: string; type: IPushTokenTypes; value: string; appName: string }) => { result: IPushToken }; + POST: (payload: PushTokenProps) => { result: IPushToken }; DELETE: (payload: { token: string }) => void; }; 'push.get': { - GET: (params: { id: string }) => { + GET: (params: PushGetProps) => { data: { message: IMessage; notification: IPushNotificationConfig; diff --git a/packages/rest-typings/src/v1/roles.ts b/packages/rest-typings/src/v1/roles.ts index 6e3d729a0222..8c4135fc4b5b 100644 --- a/packages/rest-typings/src/v1/roles.ts +++ b/packages/rest-typings/src/v1/roles.ts @@ -1,11 +1,15 @@ -import Ajv, { JSONSchemaType } from 'ajv'; +import Ajv from 'ajv'; import type { RocketChatRecordDeleted, IRole, IUserInRole } from '@rocket.chat/core-typings'; -const ajv = new Ajv(); +import type { PaginatedRequest } from '../helpers/PaginatedRequest'; + +const ajv = new Ajv({ + coerceTypes: true, +}); type RoleCreateProps = Pick & Partial>; -const roleCreatePropsSchema: JSONSchemaType = { +const roleCreatePropsSchema = { type: 'object', properties: { name: { @@ -29,14 +33,14 @@ const roleCreatePropsSchema: JSONSchemaType = { additionalProperties: false, }; -export const isRoleCreateProps = ajv.compile(roleCreatePropsSchema); +export const isRoleCreateProps = ajv.compile(roleCreatePropsSchema); type RoleUpdateProps = { roleId: IRole['_id']; name: IRole['name']; } & Partial; -const roleUpdatePropsSchema: JSONSchemaType = { +const roleUpdatePropsSchema = { type: 'object', properties: { roleId: { @@ -63,11 +67,11 @@ const roleUpdatePropsSchema: JSONSchemaType = { additionalProperties: false, }; -export const isRoleUpdateProps = ajv.compile(roleUpdatePropsSchema); +export const isRoleUpdateProps = ajv.compile(roleUpdatePropsSchema); type RoleDeleteProps = { roleId: IRole['_id'] }; -const roleDeletePropsSchema: JSONSchemaType = { +const roleDeletePropsSchema = { type: 'object', properties: { roleId: { @@ -78,7 +82,7 @@ const roleDeletePropsSchema: JSONSchemaType = { additionalProperties: false, }; -export const isRoleDeleteProps = ajv.compile(roleDeletePropsSchema); +export const isRoleDeleteProps = ajv.compile(roleDeletePropsSchema); type RoleAddUserToRoleProps = { username: string; @@ -88,7 +92,7 @@ type RoleAddUserToRoleProps = { roomId?: string; }; -const roleAddUserToRolePropsSchema: JSONSchemaType = { +const roleAddUserToRolePropsSchema = { type: 'object', properties: { username: { @@ -111,7 +115,7 @@ const roleAddUserToRolePropsSchema: JSONSchemaType = { additionalProperties: false, }; -export const isRoleAddUserToRoleProps = ajv.compile(roleAddUserToRolePropsSchema); +export const isRoleAddUserToRoleProps = ajv.compile(roleAddUserToRolePropsSchema); type RoleRemoveUserFromRoleProps = { username: string; @@ -122,7 +126,7 @@ type RoleRemoveUserFromRoleProps = { scope?: string; }; -const roleRemoveUserFromRolePropsSchema: JSONSchemaType = { +const roleRemoveUserFromRolePropsSchema = { type: 'object', properties: { username: { @@ -149,7 +153,45 @@ const roleRemoveUserFromRolePropsSchema: JSONSchemaType(roleRemoveUserFromRolePropsSchema); + +type RolesGetUsersInRoleProps = PaginatedRequest<{ + roomId?: string; + role: string; +}>; + +const RolesGetUsersInRolePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + nullable: true, + }, + role: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['role'], + additionalProperties: false, +}; + +export const isRolesGetUsersInRoleProps = ajv.compile(RolesGetUsersInRolePropsSchema); type RoleSyncProps = { updatedSince?: string; @@ -182,7 +224,7 @@ export type RolesEndpoints = { }; 'roles.getUsersInRole': { - GET: (params: { roomId?: string; role: string; offset?: number; count?: number }) => { + GET: (params: RolesGetUsersInRoleProps) => { users: IUserInRole[]; total: number; }; diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 647127c4b469..aca408dc1109 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -1,16 +1,406 @@ +/* eslint-disable @typescript-eslint/camelcase */ import type { IMessage, IRoom, IUser, RoomAdminFieldsType } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type RoomsAutoCompleteChannelAndPrivateProps = { selector: string }; + +const RoomsAutoCompleteChannelAndPrivateSchema = { + type: 'object', + properties: { + selector: { + type: 'string', + }, + }, + required: ['selector'], + additionalProperties: false, +}; + +export const isRoomsAutoCompleteChannelAndPrivateProps = ajv.compile( + RoomsAutoCompleteChannelAndPrivateSchema, +); + +type RoomsAutocompleteChannelAndPrivateWithPaginationProps = PaginatedRequest<{ selector: string }>; + +const RoomsAutocompleteChannelAndPrivateWithPaginationSchema = { + type: 'object', + properties: { + selector: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['selector'], + additionalProperties: false, +}; + +export const isRoomsAutocompleteChannelAndPrivateWithPaginationProps = ajv.compile( + RoomsAutocompleteChannelAndPrivateWithPaginationSchema, +); + +type RoomsAutocompleteAvailableForTeamsProps = { name: string }; + +const RoomsAutocompleteAvailableForTeamsSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + }, + required: ['name'], + additionalProperties: false, +}; + +export const isRoomsAutocompleteAvailableForTeamsProps = ajv.compile( + RoomsAutocompleteAvailableForTeamsSchema, +); + +type RoomsInfoProps = { roomId: string } | { roomName: string }; + +const RoomsInfoSchema = { + oneOf: [ + { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + roomName: { + type: 'string', + }, + }, + required: ['roomName'], + additionalProperties: false, + }, + ], +}; + +export const isRoomsInfoProps = ajv.compile(RoomsInfoSchema); + +type RoomsCreateDiscussionProps = { + prid: IRoom['_id']; + pmid?: IMessage['_id']; + t_name: string; // IRoom['fname'] + users?: IUser['username'][]; + encrypted?: boolean; + reply?: string; +}; + +const RoomsCreateDiscussionSchema = { + type: 'object', + properties: { + prid: { + type: 'string', + }, + pmid: { + type: 'string', + nullable: true, + }, + t_name: { + type: 'string', + nullable: true, + }, + users: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + encrypted: { + type: 'boolean', + nullable: true, + }, + reply: { + type: 'string', + nullable: true, + }, + }, + required: ['prid', 't_name'], + additionalProperties: false, +}; + +export const isRoomsCreateDiscussionProps = ajv.compile(RoomsCreateDiscussionSchema); + +type RoomsExportProps = { + rid: IRoom['_id']; + type: 'email' | 'file'; + toUsers?: IUser['username'][]; + toEmails?: string[]; + additionalEmails?: string; + subject?: string; + messages?: IMessage['_id'][]; + dateFrom?: string; + dateTo?: string; + format?: 'html' | 'json'; +}; + +const RoomsExportSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + type: { + type: 'string', + nullable: true, + }, + toUsers: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + toEmails: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + additionalEmails: { + type: 'string', + nullable: true, + }, + subject: { + type: 'string', + nullable: true, + }, + messages: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + dateFrom: { + type: 'string', + nullable: true, + }, + dateTo: { + type: 'string', + nullable: true, + }, + format: { + type: 'string', + nullable: true, + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +export const isRoomsExportProps = ajv.compile(RoomsExportSchema); + +type RoomsAdminRoomsProps = PaginatedRequest<{ + filter?: string; + types?: string[]; +}>; + +const RoomsAdminRoomsSchema = { + type: 'object', + properties: { + filter: { + type: 'string', + nullable: true, + }, + types: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isRoomsAdminRoomsProps = ajv.compile(RoomsAdminRoomsSchema); + +type RoomsAdminRoomsGetRoomProps = { rid?: string }; + +const RoomsAdminRoomsGetRoomSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isRoomsAdminRoomsGetRoomProps = ajv.compile(RoomsAdminRoomsGetRoomSchema); + +type RoomsChangeArchivationStateProps = { rid: string; action?: string }; + +const RoomsChangeArchivationStateSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + action: { + type: 'string', + nullable: true, + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +export const isRoomsChangeArchivationStateProps = ajv.compile(RoomsChangeArchivationStateSchema); + +type RoomsSaveRoomSettingsProps = { + rid: string; + roomAvatar?: string; + featured?: boolean; + roomName?: string; + roomTopic?: string; + roomAnnouncement?: string; + roomDescription?: string; + roomType?: IRoom['t']; + readOnly?: boolean; + reactWhenReadOnly?: boolean; + default?: boolean; + tokenpass?: string; + encrypted?: boolean; + favorite?: { + defaultValue?: boolean; + favorite?: boolean; + }; +}; + +const RoomsSaveRoomSettingsSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + roomAvatar: { + type: 'string', + nullable: true, + }, + featured: { + type: 'boolean', + nullable: true, + }, + roomName: { + type: 'string', + nullable: true, + }, + roomTopic: { + type: 'string', + nullable: true, + }, + roomAnnouncement: { + type: 'string', + nullable: true, + }, + roomDescription: { + type: 'string', + nullable: true, + }, + roomType: { + type: 'string', + nullable: true, + }, + readOnly: { + type: 'boolean', + nullable: true, + }, + reactWhenReadOnly: { + type: 'boolean', + nullable: true, + }, + default: { + type: 'boolean', + nullable: true, + }, + tokenpass: { + type: 'string', + nullable: true, + }, + encrypted: { + type: 'boolean', + nullable: true, + }, + favorite: { + type: 'object', + properties: { + defaultValue: { + type: 'boolean', + nullable: true, + }, + favorite: { + type: 'boolean', + nullable: true, + }, + }, + nullable: true, + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +export const isRoomsSaveRoomSettingsProps = ajv.compile(RoomsSaveRoomSettingsSchema); + export type RoomsEndpoints = { 'rooms.autocomplete.channelAndPrivate': { - GET: (params: { selector: string }) => { + GET: (params: RoomsAutoCompleteChannelAndPrivateProps) => { items: IRoom[]; }; }; 'rooms.autocomplete.channelAndPrivate.withPagination': { - GET: (params: { selector: string; offset?: number; count?: number; sort?: string }) => { + GET: (params: RoomsAutocompleteChannelAndPrivateWithPaginationProps) => { items: IRoom[]; count: number; offset: number; @@ -18,81 +408,40 @@ export type RoomsEndpoints = { }; }; 'rooms.autocomplete.availableForTeams': { - GET: (params: { name: string }) => { + GET: (params: RoomsAutocompleteAvailableForTeamsProps) => { items: IRoom[]; }; }; 'rooms.info': { - GET: (params: { roomId: string } | { roomName: string }) => { + GET: (params: RoomsInfoProps) => { room: IRoom; }; }; 'rooms.createDiscussion': { - POST: (params: { - prid: IRoom['_id']; - pmid?: IMessage['_id']; - t_name: IRoom['fname']; - users?: IUser['username'][]; - encrypted?: boolean; - reply?: string; - }) => { + POST: (params: RoomsCreateDiscussionProps) => { discussion: IRoom; }; }; 'rooms.export': { - POST: (params: { - rid: IRoom['_id']; - type: 'email' | 'file'; - toUsers?: IUser['username'][]; - toEmails?: string[]; - additionalEmails?: string; - subject?: string; - messages?: IMessage['_id'][]; - dateFrom?: string; - dateTo?: string; - format?: 'html' | 'json'; - }) => { + POST: (params: RoomsExportProps) => { missing?: []; success: boolean; }; }; 'rooms.adminRooms': { - GET: ( - params: PaginatedRequest<{ - filter?: string; - types?: string[]; - }>, - ) => PaginatedResult<{ rooms: Pick[] }>; + GET: (params: RoomsAdminRoomsProps) => PaginatedResult<{ rooms: Pick[] }>; }; 'rooms.adminRooms.getRoom': { - GET: (params: { rid?: string }) => Pick; + GET: (params: RoomsAdminRoomsGetRoomProps) => Pick; }; 'rooms.saveRoomSettings': { - POST: (params: { - rid: string; - roomAvatar?: string; - featured?: boolean; - roomName?: string; - roomTopic?: string; - roomAnnouncement?: string; - roomDescription?: string; - roomType?: IRoom['t']; - readOnly?: boolean; - reactWhenReadOnly?: boolean; - default?: boolean; - tokenpass?: string; - encrypted?: boolean; - favorite?: { - defaultValue?: boolean; - favorite?: boolean; - }; - }) => { + POST: (params: RoomsSaveRoomSettingsProps) => { success: boolean; rid: string; }; }; 'rooms.changeArchivationState': { - POST: (params: { rid: string; action?: string }) => { + POST: (params: RoomsChangeArchivationStateProps) => { success: boolean; }; }; diff --git a/packages/rest-typings/src/v1/statistics.ts b/packages/rest-typings/src/v1/statistics.ts index b3fd2fb723c3..837bbf1afa0e 100644 --- a/packages/rest-typings/src/v1/statistics.ts +++ b/packages/rest-typings/src/v1/statistics.ts @@ -1,4 +1,7 @@ import type { IStats } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +import type { PaginatedRequest } from '../helpers/PaginatedRequest'; type OTREnded = { rid: string }; @@ -18,12 +21,64 @@ export type TelemetryPayload = { params: Param[]; }; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type StatisticsProps = { refresh?: 'true' | 'false' }; + +const StatisticsSchema = { + type: 'object', + properties: { + refresh: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isStatisticsProps = ajv.compile(StatisticsSchema); + +type StatisticsListProps = PaginatedRequest<{ fields?: string }>; + +const StatisticsListSchema = { + type: 'object', + properties: { + fields: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isStatisticsListProps = ajv.compile(StatisticsListSchema); + export type StatisticsEndpoints = { 'statistics': { - GET: (params: { refresh?: 'true' | 'false' }) => IStats; + GET: (params: StatisticsProps) => IStats; }; 'statistics.list': { - GET: (params: { offset?: number; count?: number; sort?: string; fields?: string; query?: string }) => { + GET: (params: StatisticsListProps) => { statistics: IStats[]; count: number; offset: number; diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 0f9951f4dfec..4b311d1af3d2 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -1,26 +1,140 @@ import type { ITeam, IUser } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type UsersInfo = { userId?: IUser['_id']; userName?: IUser['username'] }; + +const UsersInfoSchema = { + type: 'object', + properties: { + userId: { + type: 'string', + nullable: true, + }, + userName: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isUsersInfoProps = ajv.compile(UsersInfoSchema); + +type Users2faSendEmailCode = { emailOrUsername: string }; + +const Users2faSendEmailCodeSchema = { + type: 'object', + properties: { + emailOrUsername: { + type: 'string', + }, + }, + required: ['emailOrUsername'], + additionalProperties: false, +}; + +export const isUsers2faSendEmailCodeProps = ajv.compile(Users2faSendEmailCodeSchema); + +type UsersAutocomplete = { selector: string }; + +const UsersAutocompleteSchema = { + type: 'object', + properties: { + selector: { + type: 'string', + }, + }, + required: ['selector'], + additionalProperties: false, +}; + +export const isUsersAutocompleteProps = ajv.compile(UsersAutocompleteSchema); + +type UsersListTeams = { userId: IUser['_id'] }; + +const UsersListTeamsSchema = { + type: 'object', + properties: { + userId: { + type: 'string', + }, + }, + required: ['userId'], + additionalProperties: false, +}; + +export const isUsersListTeamsProps = ajv.compile(UsersListTeamsSchema); + +type UsersSetAvatar = { userId?: IUser['_id']; username?: IUser['username']; avatarUrl?: string }; + +const UsersSetAvatarSchema = { + type: 'object', + properties: { + userId: { + type: 'string', + nullable: true, + }, + username: { + type: 'string', + nullable: true, + }, + avatarUrl: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isUsersSetAvatarProps = ajv.compile(UsersSetAvatarSchema); + +type UsersResetAvatar = { userId?: IUser['_id']; username?: IUser['username'] }; + +const UsersResetAvatarSchema = { + type: 'object', + properties: { + userId: { + type: 'string', + nullable: true, + }, + username: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isUsersResetAvatarProps = ajv.compile(UsersResetAvatarSchema); export type UsersEndpoints = { 'users.info': { - GET: (params: { userId?: IUser['_id']; userName?: IUser['username'] }) => { + GET: (params: UsersInfo) => { user: IUser; }; }; 'users.2fa.sendEmailCode': { - POST: (params: { emailOrUsername: string }) => void; + POST: (params: Users2faSendEmailCode) => void; }; 'users.autocomplete': { - GET: (params: { selector: string }) => { + GET: (params: UsersAutocomplete) => { items: Required>[]; }; }; 'users.listTeams': { - GET: (params: { userId: IUser['_id'] }) => { teams: Array }; + GET: (params: UsersListTeams) => { teams: Array }; }; 'users.setAvatar': { - POST: (params: { userId?: IUser['_id']; username?: IUser['username']; avatarUrl?: string }) => void; + POST: (params: UsersSetAvatar) => void; }; 'users.resetAvatar': { - POST: (params: { userId?: IUser['_id']; username?: IUser['username'] }) => void; + POST: (params: UsersResetAvatar) => void; }; }; diff --git a/packages/rest-typings/src/v1/videoConference.ts b/packages/rest-typings/src/v1/videoConference.ts index 4030043e2f02..c314e43deb6c 100644 --- a/packages/rest-typings/src/v1/videoConference.ts +++ b/packages/rest-typings/src/v1/videoConference.ts @@ -1,8 +1,34 @@ import type { IRoom } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type VideoConferenceJitsiUpdateTimeout = { roomId: IRoom['_id']; joiningNow?: boolean }; + +const VideoConferenceJitsiUpdateTimeoutSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + joiningNow: { + type: 'boolean', + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isVideoConferenceJitsiUpdateTimeoutProps = ajv.compile( + VideoConferenceJitsiUpdateTimeoutSchema, +); export type VideoConferenceEndpoints = { 'video-conference/jitsi.update-timeout': { - POST: (params: { roomId: IRoom['_id']; joiningNow?: boolean }) => { + POST: (params: VideoConferenceJitsiUpdateTimeout) => { jitsiTimeout: number; }; }; diff --git a/packages/rest-typings/src/v1/voip.ts b/packages/rest-typings/src/v1/voip.ts index 5398586927a3..fbdeafc23fef 100644 --- a/packages/rest-typings/src/v1/voip.ts +++ b/packages/rest-typings/src/v1/voip.ts @@ -8,72 +8,534 @@ import type { IVoipExtensionWithAgentInfo, IManagementServerConnectionStatus, IRegistrationInfo, - VoipClientEvents, } from '@rocket.chat/core-typings'; +import { VoipClientEvents } from '@rocket.chat/core-typings'; +import Ajv, { JSONSchemaType } from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +/** *************************************************/ +type CustomSoundsList = PaginatedRequest<{ query: string }>; + +const CustomSoundsListSchema: JSONSchemaType = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isCustomSoundsListProps = ajv.compile(CustomSoundsListSchema); + +type ConnectorExtensionGetRegistrationInfoByUserId = { id: string }; + +const ConnectorExtensionGetRegistrationInfoByUserIdSchema: JSONSchemaType = { + type: 'object', + properties: { + id: { + type: 'string', + }, + }, + required: ['id'], + additionalProperties: false, +}; + +export const isConnectorExtensionGetRegistrationInfoByUserIdProps = ajv.compile( + ConnectorExtensionGetRegistrationInfoByUserIdSchema, +); + +type VoipQueuesGetQueuedCallsForThisExtension = { extension: string }; + +const VoipQueuesGetQueuedCallsForThisExtensionSchema: JSONSchemaType = { + type: 'object', + properties: { + extension: { + type: 'string', + }, + }, + required: ['extension'], + additionalProperties: false, +}; + +export const isVoipQueuesGetQueuedCallsForThisExtensionProps = ajv.compile( + VoipQueuesGetQueuedCallsForThisExtensionSchema, +); + +type VoipQueuesGetMembershipSubscription = { extension: string }; + +const VoipQueuesGetMembershipSubscriptionSchema: JSONSchemaType = { + type: 'object', + properties: { + extension: { + type: 'string', + }, + }, + required: ['extension'], + additionalProperties: false, +}; + +export const isVoipQueuesGetMembershipSubscriptionProps = ajv.compile( + VoipQueuesGetMembershipSubscriptionSchema, +); + +type OmnichannelExtensions = PaginatedRequest<{ + status?: string; + agentId?: string; + queues?: string[]; + extension?: string; +}>; + +const OmnichannelExtensionsSchema: JSONSchemaType = { + type: 'object', + properties: { + status: { + type: 'string', + nullable: true, + }, + agentId: { + type: 'string', + nullable: true, + }, + queues: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + extension: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isOmnichannelExtensionsProps = ajv.compile(OmnichannelExtensionsSchema); + +type OmnichannelExtension = + | { + userId: string; + type: 'free' | 'allocated' | 'available'; + } + | { + username: string; + type: 'free' | 'allocated' | 'available'; + }; + +const OmnichannelExtensionSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + userId: { + type: 'string', + }, + type: { + type: 'string', + enum: ['free', 'allocated', 'available'], + }, + }, + required: ['userId', 'type'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + username: { + type: 'string', + }, + type: { + type: 'string', + enum: ['free', 'allocated', 'available'], + }, + }, + required: ['username', 'type'], + additionalProperties: false, + }, + ], +}; + +export const isOmnichannelExtensionProps = ajv.compile(OmnichannelExtensionSchema); + +type OmnichannelAgentExtensionGET = { username: string }; + +const OmnichannelAgentExtensionGETSchema: JSONSchemaType = { + type: 'object', + properties: { + username: { + type: 'string', + }, + }, + required: ['username'], + additionalProperties: false, +}; + +export const isOmnichannelAgentExtensionGETProps = ajv.compile(OmnichannelAgentExtensionGETSchema); + +type OmnichannelAgentExtensionPOST = { userId: string; extension: string } | { username: string; extension: string }; + +const OmnichannelAgentExtensionPOSTSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + userId: { + type: 'string', + }, + extension: { + type: 'string', + }, + }, + required: ['userId', 'extension'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + username: { + type: 'string', + }, + extension: { + type: 'string', + }, + }, + required: ['username', 'extension'], + additionalProperties: false, + }, + ], +}; + +export const isOmnichannelAgentExtensionPOSTProps = ajv.compile(OmnichannelAgentExtensionPOSTSchema); + +type OmnichannelAgentExtensionDELETE = { username: string }; + +const OmnichannelAgentExtensionDELETESchema: JSONSchemaType = { + type: 'object', + properties: { + username: { + type: 'string', + }, + }, + required: ['username'], + additionalProperties: false, +}; + +export const isOmnichannelAgentExtensionDELETEProps = ajv.compile(OmnichannelAgentExtensionDELETESchema); + +type OmnichannelAgentsAvailable = PaginatedRequest<{ text?: string; includeExtension?: string }>; + +const OmnichannelAgentsAvailableSchema: JSONSchemaType = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + text: { + type: 'string', + nullable: true, + }, + includeExtension: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isOmnichannelAgentsAvailableProps = ajv.compile(OmnichannelAgentsAvailableSchema); + +type VoipEvents = { event: VoipClientEvents; rid: string; comment?: string }; + +const VoipEventsSchema: JSONSchemaType = { + type: 'object', + properties: { + event: { + type: 'string', + enum: Object.values(VoipClientEvents), + }, + rid: { + type: 'string', + }, + comment: { + type: 'string', + nullable: true, + }, + }, + required: ['event', 'rid'], + additionalProperties: false, +}; + +export const isVoipEventsProps = ajv.compile(VoipEventsSchema); + +type VoipRoom = { token: string; agentId: ILivechatAgent['_id'] } | { rid: string; token: string }; + +const VoipRoomSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + token: { + type: 'string', + }, + agentId: { + type: 'string', + }, + }, + required: ['token', 'agentId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + rid: { + type: 'string', + }, + token: { + type: 'string', + }, + }, + required: ['rid', 'token'], + additionalProperties: false, + }, + ], +}; + +export const isVoipRoomProps = ajv.compile(VoipRoomSchema); + +type VoipManagementServerCheckConnection = { host: string; port: string; username: string; password: string }; + +const VoipManagementServerCheckConnectionSchema: JSONSchemaType = { + type: 'object', + properties: { + host: { + type: 'string', + }, + port: { + type: 'string', + }, + username: { + type: 'string', + }, + password: { + type: 'string', + }, + }, + required: ['host', 'port', 'username', 'password'], + additionalProperties: false, +}; + +export const isVoipManagementServerCheckConnectionProps = ajv.compile( + VoipManagementServerCheckConnectionSchema, +); + +type VoipCallServerCheckConnection = { websocketUrl: string; host: string; port: string; path: string }; + +const VoipCallServerCheckConnectionSchema: JSONSchemaType = { + type: 'object', + properties: { + websocketUrl: { + type: 'string', + }, + host: { + type: 'string', + }, + port: { + type: 'string', + }, + path: { + type: 'string', + }, + }, + required: ['websocketUrl', 'host', 'port', 'path'], + additionalProperties: false, +}; + +export const isVoipCallServerCheckConnectionProps = ajv.compile(VoipCallServerCheckConnectionSchema); + +type VoipRooms = { + agents?: string[]; + open?: 'true' | 'false'; + createdAt?: string; + closedAt?: string; + tags?: string[]; + queue?: string; + visitorId?: string; +}; + +const VoipRoomsSchema: JSONSchemaType = { + type: 'object', + properties: { + agents: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + open: { + type: 'string', + enum: ['true', 'false'], + nullable: true, + }, + createdAt: { + type: 'string', + nullable: true, + }, + closedAt: { + type: 'string', + nullable: true, + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + queue: { + type: 'string', + nullable: true, + }, + visitorId: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isVoipRoomsProps = ajv.compile(VoipRoomsSchema); + +type VoipRoomClose = { rid: string; token: string; comment: string; tags?: string[] }; + +const VoipRoomCloseSchema: JSONSchemaType = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + token: { + type: 'string', + }, + comment: { + type: 'string', + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['rid', 'token', 'comment'], + additionalProperties: false, +}; + +export const isVoipRoomCloseProps = ajv.compile(VoipRoomCloseSchema); + export type VoipEndpoints = { 'connector.extension.getRegistrationInfoByUserId': { - GET: (params: { id: string }) => IRegistrationInfo | { result: string }; + GET: (params: ConnectorExtensionGetRegistrationInfoByUserId) => IRegistrationInfo | { result: string }; }; 'voip/queues.getSummary': { GET: () => { summary: IQueueSummary[] }; }; 'voip/queues.getQueuedCallsForThisExtension': { - GET: (params: { extension: string }) => IQueueMembershipDetails; + GET: (params: VoipQueuesGetQueuedCallsForThisExtension) => IQueueMembershipDetails; }; 'voip/queues.getMembershipSubscription': { - GET: (params: { extension: string }) => IQueueMembershipSubscription; + GET: (params: VoipQueuesGetMembershipSubscription) => IQueueMembershipSubscription; }; 'omnichannel/extensions': { - GET: ( - params: PaginatedRequest<{ status?: string; agentId?: string; queues?: string[]; extension?: string }>, - ) => PaginatedResult<{ extensions: IVoipExtensionWithAgentInfo[] }>; + GET: (params: OmnichannelExtensions) => PaginatedResult<{ extensions: IVoipExtensionWithAgentInfo[] }>; }; 'omnichannel/extension': { - GET: ( - params: { userId: string; type: 'free' | 'allocated' | 'available' } | { username: string; type: 'free' | 'allocated' | 'available' }, - ) => { + GET: (params: OmnichannelExtension) => { extensions: string[]; }; }; 'omnichannel/agent/extension': { - GET: (params: { username: string }) => { extension: Pick }; - POST: (params: { userId: string; extension: string } | { username: string; extension: string }) => void; - DELETE: (params: { username: string }) => void; + GET: (params: OmnichannelAgentExtensionGET) => { extension: Pick }; + POST: (params: OmnichannelAgentExtensionPOST) => void; + DELETE: (params: OmnichannelAgentExtensionDELETE) => void; }; 'omnichannel/agents/available': { - GET: (params: PaginatedRequest<{ text?: string; includeExtension?: string }>) => PaginatedResult<{ agents: ILivechatAgent[] }>; + GET: (params: OmnichannelAgentsAvailable) => PaginatedResult<{ agents: ILivechatAgent[] }>; }; 'voip/events': { - POST: (params: { event: VoipClientEvents; rid: string; comment?: string }) => void; + POST: (params: VoipEvents) => void; }; 'voip/room': { - GET: (params: { token: string; agentId: ILivechatAgent['_id'] } | { rid: string; token: string }) => { + GET: (params: VoipRoom) => { room: IVoipRoom; newRoom: boolean; }; }; 'voip/managementServer/checkConnection': { - GET: (params: { host: string; port: string; username: string; password: string }) => IManagementServerConnectionStatus; + GET: (params: VoipManagementServerCheckConnection) => IManagementServerConnectionStatus; }; 'voip/callServer/checkConnection': { - GET: (params: { websocketUrl: string; host: string; port: string; path: string }) => IManagementServerConnectionStatus; + GET: (params: VoipCallServerCheckConnection) => IManagementServerConnectionStatus; }; 'voip/rooms': { - GET: (params: { - agents?: string[]; - open?: boolean; - createdAt?: string; - closedAt?: string; - tags?: string[]; - queue?: string; - visitorId?: string; - }) => PaginatedResult<{ rooms: IVoipRoom[] }>; + GET: (params: VoipRooms) => PaginatedResult<{ rooms: IVoipRoom[] }>; }; 'voip/room.close': { - POST: (params: { rid: string; token: string; comment: string; tags?: string[] }) => { rid: string }; + POST: (params: VoipRoomClose) => { rid: string }; }; }; From 46535096a37238403445ccc047e928eda1de4311 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 3 Jun 2022 23:37:41 -0300 Subject: [PATCH 05/12] Chore: Prune Messages contextualBar rewrite (#25757) --- .../PruneMessages/PruneMessages.stories.tsx | 2 +- .../{PruneMessages.js => PruneMessages.tsx} | 72 +++++--- ...imeRow.js => PruneMessagesDateTimeRow.tsx} | 18 +- ...sWithData.js => PruneMessagesWithData.tsx} | 160 ++++++++---------- packages/rest-typings/src/v1/rooms.ts | 14 ++ 5 files changed, 146 insertions(+), 120 deletions(-) rename apps/meteor/client/views/room/contextualBar/PruneMessages/{PruneMessages.js => PruneMessages.tsx} (66%) rename apps/meteor/client/views/room/contextualBar/PruneMessages/{DateTimeRow.js => PruneMessagesDateTimeRow.tsx} (51%) rename apps/meteor/client/views/room/contextualBar/PruneMessages/{PruneMessagesWithData.js => PruneMessagesWithData.tsx} (58%) diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx index 6bbe382b39b0..557baddcb9cf 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx @@ -20,6 +20,6 @@ export const Default = Template.bind({}); export const WithCallout = Template.bind({}); WithCallout.args = { - pinned: true, + values: { pinned: true }, callOutText: 'This is a callout', }; diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.js b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx similarity index 66% rename from apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.js rename to apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx index 3bb7dae29d2e..5010a838a6b3 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.js +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx @@ -1,36 +1,51 @@ +import { IUser } from '@rocket.chat/core-typings'; import { Field, ButtonGroup, Button, CheckBox, Callout } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement } from 'react'; import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; import VerticalBar from '../../../../components/VerticalBar'; -import DateTimeRow from './DateTimeRow'; +import PruneMessagesDateTimeRow from './PruneMessagesDateTimeRow'; +import { initialValues } from './PruneMessagesWithData'; + +type PruneMessagesProps = { + callOutText?: string; + validateText?: string; + users: IUser['username'][]; + values: Record; + handlers: Record void>; + onClickClose: () => void; + onClickPrune: () => void; + onChangeUsers: (value: IUser['username'], action?: string) => void; +}; const PruneMessages = ({ callOutText, validateText, - newerDateTime, - handleNewerDateTime, - olderDateTime, - handleOlderDateTime, - users, - inclusive, - pinned, - discussion, - threads, - attached, - handleInclusive, - handlePinned, - handleDiscussion, - handleThreads, - handleAttached, + values, + handlers, onClickClose, onClickPrune, onChangeUsers, -}) => { +}: PruneMessagesProps): ReactElement => { const t = useTranslation(); + const { newerDate, newerTime, olderDate, olderTime, users, inclusive, pinned, discussion, threads, attached } = + values as typeof initialValues; + + const { + handleNewerDate, + handleNewerTime, + handleOlderDate, + handleOlderTime, + handleInclusive, + handlePinned, + handleDiscussion, + handleThreads, + handleAttached, + } = handlers; + const inclusiveCheckboxId = useUniqueId(); const pinnedCheckboxId = useUniqueId(); const discussionCheckboxId = useUniqueId(); @@ -45,55 +60,56 @@ const PruneMessages = ({ {onClickClose && } - - - + + {t('Only_from_users')} - {t('Inclusive')} - {t('RetentionPolicy_DoNotPrunePinned')} - {t('RetentionPolicy_DoNotPruneDiscussion')} - {t('RetentionPolicy_DoNotPruneThreads')} - {t('Files_only')} - {callOutText && !validateText && {callOutText}} {validateText && {validateText}} - diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/DateTimeRow.js b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesDateTimeRow.tsx similarity index 51% rename from apps/meteor/client/views/room/contextualBar/PruneMessages/DateTimeRow.js rename to apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesDateTimeRow.tsx index f4cc00c44aa5..464aa82f0461 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/DateTimeRow.js +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesDateTimeRow.tsx @@ -1,7 +1,19 @@ import { Field, InputBox, Box, Margins } from '@rocket.chat/fuselage'; -import React from 'react'; +import React, { ReactElement } from 'react'; -const DateTimeRow = ({ label, dateTime, handleDateTime }) => ( +type PruneMessagesDateTimeRowProps = { + label: string; + dateTime: { + date: string; + time: string; + }; + handleDateTime: { + date: (eventOrValue: unknown) => void; + time: (eventOrValue: unknown) => void; + }; +}; + +const PruneMessagesDateTimeRow = ({ label, dateTime, handleDateTime }: PruneMessagesDateTimeRowProps): ReactElement => ( {label} @@ -13,4 +25,4 @@ const DateTimeRow = ({ label, dateTime, handleDateTime }) => ( ); -export default DateTimeRow; +export default PruneMessagesDateTimeRow; diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.js b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx similarity index 58% rename from apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.js rename to apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx index 3717d0ba9215..dbf3f00b4cd3 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.js +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx @@ -1,24 +1,26 @@ +import { IRoom, IUser, isDirectMessageRoom } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useUserRoom, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import moment from 'moment'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, ReactElement } from 'react'; import GenericModal from '../../../../components/GenericModal'; import { useForm } from '../../../../hooks/useForm'; +import { ToolboxContextValue } from '../../lib/Toolbox/ToolboxContext'; import PruneMessages from './PruneMessages'; -const getTimeZoneOffset = function () { +const getTimeZoneOffset = (): string => { const offset = new Date().getTimezoneOffset(); const absOffset = Math.abs(offset); return `${offset < 0 ? '+' : '-'}${`00${Math.floor(absOffset / 60)}`.slice(-2)}:${`00${absOffset % 60}`.slice(-2)}`; }; -const initialValues = { +export const initialValues = { newerDate: '', newerTime: '', olderDate: '', olderTime: '', - users: [], + users: [] as IUser['username'][], inclusive: false, pinned: false, discussion: false, @@ -26,94 +28,88 @@ const initialValues = { attached: false, }; -const PruneMessagesWithData = ({ rid, tabBar }) => { +const DEFAULT_PRUNE_LIMIT = 2000; + +const PruneMessagesWithData = ({ rid, tabBar }: { rid: IRoom['_id']; tabBar: ToolboxContextValue['tabBar'] }): ReactElement => { const t = useTranslation(); const room = useUserRoom(rid); - room.type = room.t; - room.rid = rid; - const { name, usernames } = room; - const setModal = useSetModal(); - const onClickClose = useMutableCallback(() => tabBar && tabBar.close()); + const onClickClose = useMutableCallback(() => tabBar?.close()); const closeModal = useCallback(() => setModal(null), [setModal]); const dispatchToastMessage = useToastMessageDispatch(); - const pruneMessages = useEndpoint('POST', 'rooms.cleanHistory'); + const pruneMessagesAction = useEndpoint('POST', 'rooms.cleanHistory'); const [fromDate, setFromDate] = useState(new Date('0001-01-01T00:00:00Z')); const [toDate, setToDate] = useState(new Date('9999-12-31T23:59:59Z')); - const [callOutText, setCallOutText] = useState(); - const [validateText, setValidateText] = useState(); + const [callOutText, setCallOutText] = useState(); + const [validateText, setValidateText] = useState(); const [counter, setCounter] = useState(0); const { values, handlers, reset } = useForm(initialValues); - const { newerDate, newerTime, olderDate, olderTime, users, inclusive, pinned, discussion, threads, attached } = values; - - const { - handleNewerDate, - handleNewerTime, - handleOlderDate, - handleOlderTime, - handleUsers, - handleInclusive, - handlePinned, - handleDiscussion, - handleThreads, - handleAttached, - } = handlers; - - const onChangeUsers = useMutableCallback((value, action) => { + const { newerDate, newerTime, olderDate, olderTime, users, inclusive, pinned, discussion, threads, attached } = + values as typeof initialValues; + const { handleUsers } = handlers; + + const onChangeUsers = useMutableCallback((value: IUser['username'], action?: string) => { if (!action) { if (users.includes(value)) { return; } return handleUsers([...users, value]); } - handleUsers(users.filter((current) => current !== value)); - }); - - const handlePrune = useMutableCallback(async () => { - const limit = 2000; - - try { - if (counter === limit) { - return; - } - const { count } = await pruneMessages({ - roomId: rid, - latest: toDate, - oldest: fromDate, - inclusive, - limit, - excludePinned: pinned, - filesOnly: attached, - ignoreDiscussion: discussion, - ignoreThreads: threads, - users, - }); - - setCounter(count); - - if (count < 1) { - throw new Error(t('No_messages_found_to_prune')); - } - - dispatchToastMessage({ type: 'success', message: `${count} ${t('messages_pruned')}` }); - closeModal(); - reset(); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error.message }); - closeModal(); - } + return handleUsers(users.filter((current) => current !== value)); }); - const handleModal = () => { - setModal( - + const handlePrune = useMutableCallback((): void => { + const handlePruneAction = async (): Promise => { + const limit = DEFAULT_PRUNE_LIMIT; + + try { + if (counter === limit) { + return; + } + + const { count } = await pruneMessagesAction({ + roomId: rid, + latest: toDate.toISOString(), + oldest: fromDate.toISOString(), + inclusive, + limit, + excludePinned: pinned, + filesOnly: attached, + ignoreDiscussion: discussion, + ignoreThreads: threads, + users, + }); + + setCounter(count); + + if (count < 1) { + throw new Error(t('No_messages_found_to_prune')); + } + + dispatchToastMessage({ type: 'success', message: `${count} ${t('messages_pruned')}` }); + closeModal(); + reset(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error.message }); + closeModal(); + } + }; + + return setModal( + {t('Prune_Modal')} , ); - }; + }); useEffect(() => { if (newerDate) { @@ -166,7 +162,7 @@ const PruneMessagesWithData = ({ rid, tabBar }) => { setCallOutText( t('Prune_Warning_all', { postProcess: 'sprintf', - sprintf: [filesOrMessages, name || usernames?.join(' x ')], + sprintf: [filesOrMessages, room && isDirectMessageRoom(room) && (room.name || room.usernames?.join(' x '))], }) + exceptPinned + ifFrom, @@ -190,31 +186,19 @@ const PruneMessagesWithData = ({ rid, tabBar }) => { ); } - setValidateText(); - }, [newerDate, olderDate, fromDate, toDate, attached, name, t, pinned, users, usernames]); + setValidateText(undefined); + }, [newerDate, olderDate, fromDate, toDate, attached, t, pinned, users, room]); return ( ); }; diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index aca408dc1109..a57757a8476d 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -417,6 +417,20 @@ export type RoomsEndpoints = { room: IRoom; }; }; + 'rooms.cleanHistory': { + POST: (params: { + roomId: IRoom['_id']; + latest: string; + oldest: string; + inclusive?: boolean; + excludePinned?: boolean; + filesOnly?: boolean; + users?: IUser['username'][]; + limit?: number; + ignoreDiscussion?: boolean; + ignoreThreads?: boolean; + }) => { _id: IRoom['_id']; count: number; success: boolean }; + }; 'rooms.createDiscussion': { POST: (params: RoomsCreateDiscussionProps) => { discussion: IRoom; From b092f833574ce32c2b2714eab40a7898643ee67b Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 3 Jun 2022 23:39:17 -0300 Subject: [PATCH 06/12] Chore: Keyboard shortcuts contextualBar rewrite (#25753) --- .../KeyboardShortcutSection.tsx | 19 ++++++++++++ .../KeyboardShortcuts/KeyboardShortcuts.js | 31 ------------------- .../KeyboardShortcuts.stories.tsx | 8 ++--- .../KeyboardShortcuts/KeyboardShortcuts.tsx | 31 +++++++++++++++++++ .../KeyboardShortcutsWithClose.js | 11 ------- .../KeyboardShortcutsWithData.tsx | 12 +++++++ .../KeyboardShortcuts/ShortcutSection.js | 14 --------- .../contextualBar/KeyboardShortcuts/index.ts | 2 +- 8 files changed, 67 insertions(+), 61 deletions(-) create mode 100644 apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutSection.tsx delete mode 100644 apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.js create mode 100644 apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.tsx delete mode 100644 apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithClose.js create mode 100644 apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithData.tsx delete mode 100644 apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/ShortcutSection.js diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutSection.tsx b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutSection.tsx new file mode 100644 index 000000000000..29ebdf68db66 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutSection.tsx @@ -0,0 +1,19 @@ +import { Box, Divider } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; + +type KeyboardShortcutSectionProps = { + title: string; + command: string; +}; + +const KeyboardShortcutSection = ({ title, command }: KeyboardShortcutSectionProps): ReactElement => ( + + + {title} + + + {command} + +); + +export default KeyboardShortcutSection; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.js b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.js deleted file mode 100644 index 452f4b706855..000000000000 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.js +++ /dev/null @@ -1,31 +0,0 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import VerticalBar from '../../../../components/VerticalBar'; -import ShortcutSection from './ShortcutSection'; - -const KeyboardShortcuts = ({ handleClose }) => { - const t = useTranslation(); - - return ( - <> - - - {t('Keyboard_Shortcuts_Title')} - {handleClose && } - - - - - - - - - - - - - ); -}; - -export default KeyboardShortcuts; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx index 65b69378e337..5ed77d36f85f 100644 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx @@ -2,16 +2,16 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; import VerticalBar from '../../../../components/VerticalBar'; -import KeyboardShortcutsWithClose from './KeyboardShortcutsWithClose'; +import KeyboardShortcutsWithData from './KeyboardShortcutsWithData'; export default { title: 'Room/Contextual Bar/KeyboardShortcut', - component: KeyboardShortcutsWithClose, + component: KeyboardShortcutsWithData, parameters: { layout: 'fullscreen', }, decorators: [(fn) => {fn()}], -} as ComponentMeta; +} as ComponentMeta; -export const Default: ComponentStory = (args) => ; +export const Default: ComponentStory = (args) => ; Default.storyName = 'KeyboardShortcuts'; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.tsx b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.tsx new file mode 100644 index 000000000000..500becc48764 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { memo, ReactElement } from 'react'; + +import VerticalBar from '../../../../components/VerticalBar'; +import KeyboardShortcutSection from './KeyboardShortcutSection'; + +const KeyboardShortcuts = ({ handleClose }: { handleClose: () => void }): ReactElement => { + const t = useTranslation(); + + return ( + <> + + + {t('Keyboard_Shortcuts_Title')} + {handleClose && } + + + + + + + + + + + + + ); +}; + +export default memo(KeyboardShortcuts); diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithClose.js b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithClose.js deleted file mode 100644 index 94111937261c..000000000000 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithClose.js +++ /dev/null @@ -1,11 +0,0 @@ -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import React, { memo } from 'react'; - -import KeyboardShortcuts from './KeyboardShortcuts'; - -const KeyboardShortcutsWithClose = ({ tabBar }) => { - const handleClose = useMutableCallback(() => tabBar && tabBar.close()); - return ; -}; - -export default memo(KeyboardShortcutsWithClose); diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithData.tsx b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithData.tsx new file mode 100644 index 000000000000..afee820bb10a --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithData.tsx @@ -0,0 +1,12 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import React, { ReactElement } from 'react'; + +import { ToolboxContextValue } from '../../lib/Toolbox/ToolboxContext'; +import KeyboardShortcuts from './KeyboardShortcuts'; + +const KeyboardShortcutsWithData = ({ tabBar }: { tabBar: ToolboxContextValue['tabBar'] }): ReactElement => { + const handleClose = useMutableCallback(() => tabBar?.close()); + return ; +}; + +export default KeyboardShortcutsWithData; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/ShortcutSection.js b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/ShortcutSection.js deleted file mode 100644 index a540a22161aa..000000000000 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/ShortcutSection.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Box, Divider } from '@rocket.chat/fuselage'; -import React from 'react'; - -const ShortcutSection = ({ title, command }) => ( - - - {title} - - - {command} - -); - -export default ShortcutSection; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/index.ts b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/index.ts index dd578fbd1237..5d1134a343aa 100644 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/index.ts +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/index.ts @@ -1 +1 @@ -export { default } from './KeyboardShortcutsWithClose'; +export { default } from './KeyboardShortcutsWithData'; From 68595d096be6d60488f6e676fb0469c469b3bbbb Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 3 Jun 2022 23:40:22 -0300 Subject: [PATCH 07/12] Chore: Replace AnnouncementModal in favor of GenericModal (#25752) --- apps/meteor/client/templates.ts | 2 -- .../views/room/Announcement/Announcement.tsx | 16 +++++++-- .../room/Announcement/AnnouncementModal.tsx | 36 ------------------- 3 files changed, 13 insertions(+), 41 deletions(-) delete mode 100644 apps/meteor/client/views/room/Announcement/AnnouncementModal.tsx diff --git a/apps/meteor/client/templates.ts b/apps/meteor/client/templates.ts index 08bbb32610ae..36d18aeb84d2 100644 --- a/apps/meteor/client/templates.ts +++ b/apps/meteor/client/templates.ts @@ -120,8 +120,6 @@ createTemplateForComponent('channelFilesList', () => import('./views/room/contex renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), }); -createTemplateForComponent('RoomAnnouncement', () => import('./views/room/Announcement')); - createTemplateForComponent('PruneMessages', () => import('./views/room/contextualBar/PruneMessages'), { renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), }); diff --git a/apps/meteor/client/views/room/Announcement/Announcement.tsx b/apps/meteor/client/views/room/Announcement/Announcement.tsx index 6c2bc0c81436..2691ea9b7a7c 100644 --- a/apps/meteor/client/views/room/Announcement/Announcement.tsx +++ b/apps/meteor/client/views/room/Announcement/Announcement.tsx @@ -1,10 +1,11 @@ +import { Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal } from '@rocket.chat/ui-contexts'; +import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import React, { FC, MouseEvent } from 'react'; +import GenericModal from '../../../components/GenericModal'; import MarkdownText from '../../../components/MarkdownText'; import AnnouncementComponent from './AnnouncementComponent'; -import AnnouncementModal from './AnnouncementModal'; type AnnouncementParams = { announcement: string; @@ -12,6 +13,7 @@ type AnnouncementParams = { }; const Announcement: FC = ({ announcement, announcementDetails }) => { + const t = useTranslation(); const setModal = useSetModal(); const closeModal = useMutableCallback(() => setModal(null)); const handleClick = (e: MouseEvent): void => { @@ -23,7 +25,15 @@ const Announcement: FC = ({ announcement, announcementDetail return; } - announcementDetails ? announcementDetails() : setModal({announcement}); + announcementDetails + ? announcementDetails() + : setModal( + + + + + , + ); }; return announcement ? ( diff --git a/apps/meteor/client/views/room/Announcement/AnnouncementModal.tsx b/apps/meteor/client/views/room/Announcement/AnnouncementModal.tsx deleted file mode 100644 index f5003b06358d..000000000000 --- a/apps/meteor/client/views/room/Announcement/AnnouncementModal.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Button, ButtonGroup, Box, Modal } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { FC } from 'react'; - -import MarkdownText from '../../../components/MarkdownText'; - -type AnnouncementModalParams = { - onClose: () => void; - confirmLabel?: string; - children?: string; -}; - -const AnnouncementModal: FC = ({ onClose, confirmLabel = 'Close', children, ...props }) => { - const t = useTranslation(); - - return ( - - - {t('Announcement')} - - - - - - - - - - - - - - ); -}; - -export default AnnouncementModal; From 55cfc8433ecafa90fd92158566d78d818941087c Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 3 Jun 2022 23:41:23 -0300 Subject: [PATCH 08/12] Chore: AutoTranslate contextualBar rewrite (#25751) --- .../AutoTranslate/AutoTranslate.tsx | 36 ++++++++++++------- ...eWithData.js => AutoTranslateWithData.tsx} | 27 +++++++------- packages/core-typings/src/ISubscription.ts | 2 +- packages/rest-typings/src/index.ts | 3 +- packages/rest-typings/src/v1/autoTranslate.ts | 13 +++++++ 5 files changed, 52 insertions(+), 29 deletions(-) rename apps/meteor/client/views/room/contextualBar/AutoTranslate/{AutoTranslateWithData.js => AutoTranslateWithData.tsx} (65%) create mode 100644 packages/rest-typings/src/v1/autoTranslate.ts diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx index 30b815fbb098..e3c5b743c7df 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx @@ -1,20 +1,27 @@ import { FieldGroup, Field, ToggleSwitch, Select } from '@rocket.chat/fuselage'; import type { SelectOption } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement, ChangeEvent } from 'react'; import VerticalBar from '../../../../components/VerticalBar'; type AutoTranslateProps = { language: string; languages: SelectOption[]; - handleSwitch: (event?: any) => void; + handleSwitch: (e: ChangeEvent) => void; translateEnable: boolean | undefined; handleChangeLanguage: (value: string) => void; handleClose?: () => void; }; -const AutoTranslate = ({ language, languages, handleSwitch, translateEnable, handleChangeLanguage, handleClose }: AutoTranslateProps) => { +const AutoTranslate = ({ + language, + languages, + handleSwitch, + translateEnable, + handleChangeLanguage, + handleClose, +}: AutoTranslateProps): ReactElement => { const t = useTranslation(); return ( @@ -24,17 +31,20 @@ const AutoTranslate = ({ language, languages, handleSwitch, translateEnable, han {t('Auto_Translate')} {handleClose && } - + - {t('Automatic_Translation')} - - - - - {t('Language')} - - + + diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.js b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx similarity index 65% rename from apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.js rename to apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx index cf8efdfe2773..f34171b39422 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.js +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx @@ -1,26 +1,25 @@ +import { IRoom } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useUserSubscription, useLanguage } from '@rocket.chat/ui-contexts'; -import React, { useMemo, useEffect, useState, memo } from 'react'; +import React, { useMemo, useEffect, useState, memo, ReactElement } from 'react'; import { useEndpointActionExperimental } from '../../../../hooks/useEndpointActionExperimental'; import { useEndpointData } from '../../../../hooks/useEndpointData'; import { useTabBarClose } from '../../providers/ToolboxProvider'; import AutoTranslate from './AutoTranslate'; -const AutoTranslateWithData = ({ rid }) => { - const close = useTabBarClose(); +const AutoTranslateWithData = ({ rid }: { rid: IRoom['_id'] }): ReactElement => { + const handleClose = useTabBarClose(); const userLanguage = useLanguage(); const subscription = useUserSubscription(rid); + const [currentLanguage, setCurrentLanguage] = useState(subscription?.autoTranslateLanguage ?? ''); + const saveSettings = useEndpointActionExperimental('POST', 'autotranslate.saveSettings'); - const { value: data } = useEndpointData( + const { value: translateData } = useEndpointData( 'autotranslate.getSupportedLanguages', useMemo(() => ({ targetLanguage: userLanguage }), [userLanguage]), ); - const [currentLanguage, setCurrentLanguage] = useState(subscription.autoTranslateLanguage); - - const saveSettings = useEndpointActionExperimental('POST', 'autotranslate.saveSettings'); - const handleChangeLanguage = useMutableCallback((value) => { setCurrentLanguage(value); @@ -40,23 +39,23 @@ const AutoTranslateWithData = ({ rid }) => { }); useEffect(() => { - if (!subscription.autoTranslate) { + if (!subscription?.autoTranslate) { return; } - if (!subscription.autoTranslateLanguage) { + if (!subscription?.autoTranslateLanguage) { handleChangeLanguage(userLanguage); } - }, [subscription.autoTranslate, subscription.autoTranslateLanguage, handleChangeLanguage, userLanguage]); + }, [subscription?.autoTranslate, subscription?.autoTranslateLanguage, handleChangeLanguage, userLanguage]); return ( [value.language, value.name]) : []} + languages={translateData ? translateData.languages.map((language) => [language.language, language.name]) : []} handleSwitch={handleSwitch} handleChangeLanguage={handleChangeLanguage} - translateEnable={!!subscription.autoTranslate} - handleClose={close} + translateEnable={!!subscription?.autoTranslate} + handleClose={handleClose} /> ); }; diff --git a/packages/core-typings/src/ISubscription.ts b/packages/core-typings/src/ISubscription.ts index b105402605d9..e0f07d277eba 100644 --- a/packages/core-typings/src/ISubscription.ts +++ b/packages/core-typings/src/ISubscription.ts @@ -53,7 +53,7 @@ export interface ISubscription extends IRocketChatRecord { blocked?: unknown; blocker?: unknown; autoTranslate?: unknown; - autoTranslateLanguage?: unknown; + autoTranslateLanguage?: string; disableNotifications?: unknown; muteGroupMentions?: unknown; ignored?: unknown; diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 75274d973509..0a9dbb202fe3 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -2,6 +2,7 @@ import type { KeyOfEach } from '@rocket.chat/core-typings'; import type { AppsEndpoints } from './apps'; +import type { AutoTranslateEndpoints } from './v1/autoTranslate'; import type { ReplacePlaceholders } from './helpers/ReplacePlaceholders'; import type { BannersEndpoints } from './v1/banners'; import type { ChannelsEndpoints } from './v1/channels'; @@ -72,7 +73,7 @@ export interface Endpoints EmailInboxEndpoints, WebdavEndpoints, OAuthAppsEndpoint, - AppsEndpoints {} + AutoTranslateEndpoints {} type OperationsByPathPattern = TPathPattern extends any ? OperationsByPathPatternAndMethod diff --git a/packages/rest-typings/src/v1/autoTranslate.ts b/packages/rest-typings/src/v1/autoTranslate.ts new file mode 100644 index 000000000000..9f265c9aa6d4 --- /dev/null +++ b/packages/rest-typings/src/v1/autoTranslate.ts @@ -0,0 +1,13 @@ +import type { ISupportedLanguage } from '@rocket.chat/core-typings'; + +export type AutoTranslateEndpoints = { + 'autotranslate.getSupportedLanguages': { + GET: (params: { targetLanguage: string }) => { languages: ISupportedLanguage[] }; + }; + 'autotranslate.saveSettings': { + POST: (params: { roomId: string; field: string; value: boolean; defaultLanguage?: string }) => void; + }; + 'autotranslate.translateMessage': { + POST: (params: { messageId: string; targetLanguage?: string }) => void; + }; +}; From 81304fe9d8fe68c8aa51d5ddb0b96a9d98162219 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 3 Jun 2022 20:42:39 -0600 Subject: [PATCH 09/12] Chore: migrate katex to ts (#25501) --- .../app/katex/client/{index.js => index.ts} | 111 +++++++++++++----- .../app/katex/server/{index.js => index.ts} | 0 apps/meteor/client/components/Katex.tsx | 2 +- .../client/startup/renderMessage/katex.ts | 2 +- apps/meteor/package.json | 1 + yarn.lock | 8 ++ 6 files changed, 94 insertions(+), 30 deletions(-) rename apps/meteor/app/katex/client/{index.js => index.ts} (55%) rename apps/meteor/app/katex/server/{index.js => index.ts} (100%) diff --git a/apps/meteor/app/katex/client/index.js b/apps/meteor/app/katex/client/index.ts similarity index 55% rename from apps/meteor/app/katex/client/index.js rename to apps/meteor/app/katex/client/index.ts index 139b420af26b..a2cee08f78e4 100644 --- a/apps/meteor/app/katex/client/index.js +++ b/apps/meteor/app/katex/client/index.ts @@ -1,52 +1,71 @@ import { Random } from 'meteor/random'; -import katex from 'katex'; +import KatexPackage from 'katex'; import { unescapeHTML, escapeHTML } from '@rocket.chat/string-helpers'; - import 'katex/dist/katex.min.css'; import './style.css'; +import { IMessage } from '@rocket.chat/core-typings'; class Boundary { - length() { + start: number; + + end: number; + + length(): number { return this.end - this.start; } - extract(str) { + extract(str: string): string { return str.substr(this.start, this.length()); } } +type Delimiter = { + opener: string; + closer: string; + displayMode: boolean; + enabled: () => boolean; +}; + +type OpeningDelimiter = { options: Delimiter; pos: number }; + +type LatexBoundary = { outer: Boundary; inner: Boundary }; + class Katex { - constructor(katex, { dollarSyntax, parenthesisSyntax }) { + katex: KatexPackage; + + delimitersMap: Delimiter[]; + + constructor(katex: KatexPackage, { dollarSyntax, parenthesisSyntax }: { dollarSyntax: boolean; parenthesisSyntax: boolean }) { this.katex = katex; this.delimitersMap = [ { opener: '\\[', closer: '\\]', displayMode: true, - enabled: () => parenthesisSyntax, + enabled: (): boolean => parenthesisSyntax, }, { opener: '\\(', closer: '\\)', displayMode: false, - enabled: () => parenthesisSyntax, + enabled: (): boolean => parenthesisSyntax, }, { opener: '$$', closer: '$$', displayMode: true, - enabled: () => dollarSyntax, + enabled: (): boolean => dollarSyntax, }, { opener: '$', closer: '$', displayMode: false, - enabled: () => dollarSyntax, + enabled: (): boolean => dollarSyntax, }, ]; } - findOpeningDelimiter(str, start) { + findOpeningDelimiter(str: string, start: number): OpeningDelimiter | null { const matches = this.delimitersMap .filter((options) => options.enabled()) .map((options) => ({ @@ -70,7 +89,7 @@ class Katex { return match; } - getLatexBoundaries(str, { options: { closer }, pos }) { + getLatexBoundaries(str: string, { options: { closer }, pos }: OpeningDelimiter): LatexBoundary | null { const closerIndex = str.substr(pos + closer.length).indexOf(closer); if (closerIndex < 0) { return null; @@ -92,15 +111,17 @@ class Katex { } // Searches for the first latex block in the given string - findLatex(str) { + findLatex(str: string): (LatexBoundary & { options: Delimiter }) | null { let start = 0; let openingDelimiterMatch; while ((openingDelimiterMatch = this.findOpeningDelimiter(str, start++)) != null) { const match = this.getLatexBoundaries(str, openingDelimiterMatch); - if (match && match.inner.extract(str).trim().length) { - match.options = openingDelimiterMatch.options; - return match; + if (match?.inner.extract(str).trim().length) { + return { + ...match, + options: openingDelimiterMatch.options, + }; } } @@ -109,7 +130,7 @@ class Katex { // Breaks a message to what comes before, after and to the content of a // matched latex block - extractLatex(str, match) { + extractLatex(str: string, match: LatexBoundary): { before: string; latex: string; after: string } { const before = str.substr(0, match.outer.start); const after = str.substr(match.outer.end); let latex = match.inner.extract(str); @@ -123,9 +144,9 @@ class Katex { // Takes a latex math string and the desired display mode and renders it // to HTML using the KaTeX library - renderLatex = (latex, displayMode) => { + renderLatex = (latex: string, displayMode: Delimiter['displayMode']): string => { try { - return this.katex.renderToString(latex, { + return KatexPackage.renderToString(latex, { displayMode, macros: { '\\href': '\\@secondoftwo', @@ -137,11 +158,15 @@ class Katex { }; // Takes a string and renders all latex blocks inside it - render(str, renderFunction) { + render(str: string, renderFunction: (latex: string, displayMode: Delimiter['displayMode']) => string): string { let result = ''; while (this.findLatex(str) != null) { // Find the first latex block in the string const match = this.findLatex(str); + if (!match) { + continue; + } + const parts = this.extractLatex(str, match); // Add to the reuslt what comes before the latex block as well as @@ -155,7 +180,11 @@ class Katex { return result; } - renderMessage = (message) => { + public renderMessage(message: string): string; + + public renderMessage(message: IMessage): IMessage; + + public renderMessage(message: string | IMessage): string | IMessage { if (typeof message === 'string') { return this.render(message, this.renderLatex); } @@ -170,7 +199,7 @@ class Katex { message.html = this.render(message.html, (latex, displayMode) => { const token = `=!=${Random.id()}=!=`; - message.tokens.push({ + message.tokens?.push({ token, text: this.renderLatex(latex, displayMode), }); @@ -178,14 +207,40 @@ class Katex { }); return message; - }; + } } -export const createKatexMessageRendering = (options) => { - const instance = new Katex(katex, options); - return (message) => instance.renderMessage(message); -}; +export function createKatexMessageRendering( + options: { + dollarSyntax: boolean; + parenthesisSyntax: boolean; + }, + _isMessage: true, +): (message: IMessage) => IMessage; +export function createKatexMessageRendering( + options: { + dollarSyntax: boolean; + parenthesisSyntax: boolean; + }, + _isMessage: false, +): (message: string) => string; +export function createKatexMessageRendering( + options: { + dollarSyntax: boolean; + parenthesisSyntax: boolean; + }, + _isMessage: true | false, +): ((message: string) => string) | ((message: IMessage) => IMessage) { + const instance = new Katex(KatexPackage, options); + if (_isMessage) { + return (message: IMessage): IMessage => instance.renderMessage(message); + } + return (message: string): string => instance.renderMessage(message); +} -export const getKatexHtml = (text, katex) => { - return createKatexMessageRendering({ dollarSyntax: katex.dollarSyntaxEnabled, parenthesisSyntax: katex.parenthesisSyntaxEnabled })(text); +export const getKatexHtml = (text: string, katex: { dollarSyntaxEnabled: boolean; parenthesisSyntaxEnabled: boolean }): string => { + return createKatexMessageRendering( + { dollarSyntax: katex.dollarSyntaxEnabled, parenthesisSyntax: katex.parenthesisSyntaxEnabled }, + false, + )(text); }; diff --git a/apps/meteor/app/katex/server/index.js b/apps/meteor/app/katex/server/index.ts similarity index 100% rename from apps/meteor/app/katex/server/index.js rename to apps/meteor/app/katex/server/index.ts diff --git a/apps/meteor/client/components/Katex.tsx b/apps/meteor/client/components/Katex.tsx index 6a66a1157191..a7cb3f279f4b 100644 --- a/apps/meteor/client/components/Katex.tsx +++ b/apps/meteor/client/components/Katex.tsx @@ -11,7 +11,7 @@ type KatexProps = { }; const Katex = ({ text, options }: KatexProps): ReactElement => ( - + ); export default memo(Katex); diff --git a/apps/meteor/client/startup/renderMessage/katex.ts b/apps/meteor/client/startup/renderMessage/katex.ts index 005181c9b92e..68b76006c117 100644 --- a/apps/meteor/client/startup/renderMessage/katex.ts +++ b/apps/meteor/client/startup/renderMessage/katex.ts @@ -19,7 +19,7 @@ Meteor.startup(() => { }; import('../../../app/katex/client').then(({ createKatexMessageRendering }) => { - const renderMessage = createKatexMessageRendering(options); + const renderMessage = createKatexMessageRendering(options, true); callbacks.remove('renderMessage', 'katex'); callbacks.add('renderMessage', renderMessage, callbacks.priority.HIGH + 1, 'katex'); }); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 39ddbbd81a67..976b71cf6f2d 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -207,6 +207,7 @@ "@slack/client": "^4.12.0", "@slack/rtm-api": "^6.0.0", "@types/cookie": "^0.5.1", + "@types/katex": "^0.14.0", "@types/lodash": "^4.14.182", "@types/lodash.debounce": "^4.0.6", "@types/object-path": "^0.11.1", diff --git a/yarn.lock b/yarn.lock index c3570d34a32f..b8f3140d3d2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4799,6 +4799,7 @@ __metadata: "@types/jsdom": ^16.2.12 "@types/jsdom-global": ^3.0.2 "@types/jsrsasign": ^9.0.3 + "@types/katex": ^0.14.0 "@types/ldapjs": ^2.2.1 "@types/less": ^3.0.2 "@types/lodash": ^4.14.182 @@ -7277,6 +7278,13 @@ __metadata: languageName: node linkType: hard +"@types/katex@npm:^0.14.0": + version: 0.14.0 + resolution: "@types/katex@npm:0.14.0" + checksum: 330e0d0337ba48c87f5b793965fbad673653789bf6e50dfe8d726a7b0cbefd37195055e31503aae629814aa79447e4f23a4b87ad1ac565c0d9a9d9978836f39b + languageName: node + linkType: hard + "@types/keyv@npm:^3.1.1": version: 3.1.4 resolution: "@types/keyv@npm:3.1.4" From d2c6ed97d835c777778cd72d06b578107b0a2ce8 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Sun, 5 Jun 2022 13:45:44 +0530 Subject: [PATCH 10/12] Chore: Messages raw model rewrite to ts (#25761) * Messages raw model rewrtite to ts Co-authored-by: Pierre Lehnen --- .gitignore | 3 +- apps/meteor/app/api/server/v1/im.ts | 4 +- .../server/raw/{Messages.js => Messages.ts} | 128 +++++++++++++----- .../modules/watchers/watchers.module.ts | 2 +- 4 files changed, 96 insertions(+), 41 deletions(-) rename apps/meteor/app/models/server/raw/{Messages.js => Messages.ts} (50%) diff --git a/.gitignore b/.gitignore index 816c05071f59..310be7ef939d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ yarn-error.log* !.yarn/sdks !.yarn/versions -.nvmrc \ No newline at end of file +.nvmrc +.idea/ diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index 5795f9601ac2..0236d350b212 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -358,7 +358,7 @@ API.v1.addRoute( sort: sortObj, skip: offset, limit: count, - ...(fields && { fields }), + ...(fields && { projection: fields }), }).toArray(); return API.v1.success({ @@ -410,7 +410,7 @@ API.v1.addRoute( sort: sort || { ts: -1 }, skip: offset, limit: count, - fields, + projection: fields, }).toArray(); if (!msgs) { diff --git a/apps/meteor/app/models/server/raw/Messages.js b/apps/meteor/app/models/server/raw/Messages.ts similarity index 50% rename from apps/meteor/app/models/server/raw/Messages.js rename to apps/meteor/app/models/server/raw/Messages.ts index 7f84cac25420..ee81c791180a 100644 --- a/apps/meteor/app/models/server/raw/Messages.js +++ b/apps/meteor/app/models/server/raw/Messages.ts @@ -1,10 +1,25 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; +import type { IMessage, IRoom, IUser, MessageTypesValues, ILivechatDepartment } from '@rocket.chat/core-typings'; +import type { PaginatedRequest } from '@rocket.chat/rest-typings'; +import type { + AggregationCursor, + Cursor, + FilterQuery, + FindOneOptions, + WithoutProjection, + Collection, + CollectionAggregationOptions, +} from 'mongodb'; import { BaseRaw } from './BaseRaw'; -export class MessagesRaw extends BaseRaw { - findVisibleByMentionAndRoomId(username, rid, options) { - const query = { +export class MessagesRaw extends BaseRaw { + findVisibleByMentionAndRoomId( + username: IUser['username'], + rid: IRoom['_id'], + options: WithoutProjection>, + ): Cursor { + const query: FilterQuery = { '_hidden': { $ne: true }, 'mentions.username': username, rid, @@ -13,8 +28,12 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } - findStarredByUserAtRoom(userId, roomId, options) { - const query = { + findStarredByUserAtRoom( + userId: IUser['_id'], + roomId: IRoom['_id'], + options: WithoutProjection>, + ): Cursor { + const query: FilterQuery = { '_hidden': { $ne: true }, 'starred._id': userId, 'rid': roomId, @@ -23,21 +42,21 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } - findByRoomIdAndType(roomId, type, options) { - const query = { + findByRoomIdAndType( + roomId: IRoom['_id'], + type: IMessage['t'], + options: WithoutProjection> = {}, + ): Cursor { + const query: FilterQuery = { rid: roomId, t: type, }; - if (options == null) { - options = {}; - } - return this.find(query, options); } - findSnippetedByRoom(roomId, options) { - const query = { + findSnippetedByRoom(roomId: IRoom['_id'], options: WithoutProjection>): Cursor { + const query: FilterQuery = { _hidden: { $ne: true }, snippeted: true, rid: roomId, @@ -46,14 +65,15 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } - findDiscussionsByRoom(rid, options) { - const query = { rid, drid: { $exists: true } }; + // TODO: do we need this? currently not used anywhere + findDiscussionsByRoom(rid: IRoom['_id'], options: WithoutProjection>): Cursor { + const query: FilterQuery = { rid, drid: { $exists: true } }; return this.find(query, options); } - findDiscussionsByRoomAndText(rid, text, options) { - const query = { + findDiscussionsByRoomAndText(rid: IRoom['_id'], text: string, options: WithoutProjection>): Cursor { + const query: FilterQuery = { rid, drid: { $exists: true }, msg: new RegExp(escapeRegExp(text), 'i'), @@ -62,7 +82,20 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } - findAllNumberOfTransferredRooms({ start, end, departmentId, onlyCount = false, options = {} }) { + findAllNumberOfTransferredRooms({ + start, + end, + departmentId, + onlyCount = false, + options = {}, + }: { + start: string; + end: string; + departmentId: ILivechatDepartment['_id']; + onlyCount: boolean; + options: PaginatedRequest; + }): AggregationCursor { + // FIXME: aggregation type definitions const match = { $match: { t: 'livechat_transfer_history', @@ -98,7 +131,7 @@ export class MessagesRaw extends BaseRaw { numberOfTransferredRooms: 1, }, }; - const firstParams = [match, lookup, unwind]; + const firstParams: Exclude['aggregate']>[0], undefined> = [match, lookup, unwind]; if (departmentId) { firstParams.push({ $match: { @@ -121,8 +154,8 @@ export class MessagesRaw extends BaseRaw { return this.col.aggregate(params, { allowDiskUse: true }); } - getTotalOfMessagesSentByDate({ start, end, options = {} }) { - const params = [ + getTotalOfMessagesSentByDate({ start, end, options = {} }: { start: Date; end: Date; options?: PaginatedRequest }): Promise { + const params: Exclude['aggregate']>[0], undefined> = [ { $match: { t: { $exists: false }, ts: { $gte: start, $lte: end } } }, { $lookup: { @@ -179,7 +212,7 @@ export class MessagesRaw extends BaseRaw { return this.col.aggregate(params).toArray(); } - findLivechatClosedMessages(rid, options) { + findLivechatClosedMessages(rid: IRoom['_id'], options: WithoutProjection>): Cursor { return this.find( { rid, @@ -189,32 +222,53 @@ export class MessagesRaw extends BaseRaw { ); } - async countRoomsWithStarredMessages(options) { - const [queryResult] = await this.col - .aggregate( - [{ $match: { 'starred._id': { $exists: true } } }, { $group: { _id: '$rid' } }, { $group: { _id: null, total: { $sum: 1 } } }], + async countRoomsWithStarredMessages(options: CollectionAggregationOptions): Promise { + const queryResult = await this.col + .aggregate<{ _id: null; total: number }>( + [ + { $match: { 'starred._id': { $exists: true } } }, + { $group: { _id: '$rid' } }, + { + $group: { + _id: null, + total: { $sum: 1 }, + }, + }, + ], options, ) - .toArray(); + .next(); return queryResult?.total || 0; } - async countRoomsWithPinnedMessages(options) { - const [queryResult] = await this.col - .aggregate([{ $match: { pinned: true } }, { $group: { _id: '$rid' } }, { $group: { _id: null, total: { $sum: 1 } } }], options) - .toArray(); + async countRoomsWithPinnedMessages(options: CollectionAggregationOptions): Promise { + const queryResult = await this.col + .aggregate<{ _id: null; total: number }>( + [ + { $match: { pinned: true } }, + { $group: { _id: '$rid' } }, + { + $group: { + _id: null, + total: { $sum: 1 }, + }, + }, + ], + options, + ) + .next(); return queryResult?.total || 0; } - async countE2EEMessages(options) { + async countE2EEMessages(options: WithoutProjection>): Promise { return this.find({ t: 'e2e' }, options).count(); } - findPinned(options) { - const query = { - t: { $ne: 'rm' }, + findPinned(options: WithoutProjection>): Cursor { + const query: FilterQuery = { + t: { $ne: 'rm' as MessageTypesValues }, _hidden: { $ne: true }, pinned: true, }; @@ -222,8 +276,8 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } - findStarred(options) { - const query = { + findStarred(options: WithoutProjection>): Cursor { + const query: FilterQuery = { '_hidden': { $ne: true }, 'starred._id': { $exists: true }, }; diff --git a/apps/meteor/server/modules/watchers/watchers.module.ts b/apps/meteor/server/modules/watchers/watchers.module.ts index b4914d742181..f3567981dc14 100644 --- a/apps/meteor/server/modules/watchers/watchers.module.ts +++ b/apps/meteor/server/modules/watchers/watchers.module.ts @@ -122,7 +122,7 @@ export function initWatchers(models: IModelsParam, broadcast: BroadcastCallback, switch (clientAction) { case 'inserted': case 'updated': - const message: IMessage | undefined = data ?? (await Messages.findOne({ _id: id })); + const message: IMessage | null = data ?? (await Messages.findOne({ _id: id })); if (!message) { return; } From 881e7627526f52ba6cabc84eded2916cf37cf064 Mon Sep 17 00:00:00 2001 From: Kunal Verma <67605729+Kunalvrm555@users.noreply.github.com> Date: Sun, 5 Jun 2022 23:06:34 +0530 Subject: [PATCH 11/12] [FIX] user status Offline misnamed as Invisible in Custom Status edit dropdown menu (#24796) * Invisible to Offline * accomodate #25265 (Convert to tsx)+ * Rename UserStatusMenu.js to UserStatusMenu.tsx * Use correct file path Co-authored-by: Debdut Chakraborty Co-authored-by: dougfabris --- apps/meteor/client/components/UserStatusMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/components/UserStatusMenu.tsx b/apps/meteor/client/components/UserStatusMenu.tsx index c435f26cc8e1..5df7dd7c33cc 100644 --- a/apps/meteor/client/components/UserStatusMenu.tsx +++ b/apps/meteor/client/components/UserStatusMenu.tsx @@ -42,7 +42,7 @@ const UserStatusMenu = ({ ]; if (allowInvisibleStatus) { - statuses.push([UserStatusType.OFFLINE, renderOption(UserStatusType.OFFLINE, t('Invisible'))]); + statuses.push([UserStatusType.OFFLINE, renderOption(UserStatusType.OFFLINE, t('Offline'))]); } return statuses; From e682030213afd15a635a3c73b07815cb37af2126 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Mon, 6 Jun 2022 11:20:35 -0300 Subject: [PATCH 12/12] [IMPROVE] Refactor + unit tests for federation-v2 (#25680) * refactor: refactor matrix bridge to support testing + unit tests * fix: fix conflict * fix: types on test * fix: fix tests * chore: rename bridged to federated * chore: remove duplicated config * fix: fix yarn.lock * chore: add migration for the renamed field * chore: fix lint * fix: type --- .../server/application/RoomServiceReceiver.ts | 217 +++++++++ .../server/application/RoomServiceSender.ts | 128 +++++ .../application/input/RoomReceiverDto.ts | 63 +++ .../server/application/input/RoomSenderDto.ts | 21 + .../meteor/app/federation-v2/server/bridge.ts | 85 ---- .../meteor/app/federation-v2/server/config.ts | 43 -- .../server/data-interface/index.ts | 9 - .../server/data-interface/message.ts | 17 - .../server/data-interface/room.ts | 8 - .../server/data-interface/user.ts | 8 - .../server/domain/FederatedRoom.ts | 78 ++++ .../server/domain/FederatedUser.ts | 38 ++ .../server/domain/IFederationBridge.ts | 25 + .../app/federation-v2/server/eventHandler.ts | 50 -- .../federation-v2/server/events/createRoom.ts | 187 -------- .../app/federation-v2/server/events/index.ts | 6 - .../server/events/roomMembership.ts | 86 ---- .../server/events/sendMessage.ts | 28 -- .../server/events/setRoomJoinRules.ts | 56 --- .../server/events/setRoomName.ts | 42 -- .../server/events/setRoomTopic.ts | 22 - apps/meteor/app/federation-v2/server/index.ts | 37 +- .../server/infrastructure/Factory.ts | 90 ++++ .../server/infrastructure/matrix/Bridge.ts | 177 +++++++ .../matrix/converters/RoomReceiver.ts | 152 ++++++ .../matrix}/definitions/IMatrixEvent.ts | 2 +- .../IMatrixEventContentAddMemberToRoom.ts | 0 .../IMatrixEventContentCreateRoom.ts | 2 +- .../IMatrixEventContentSendMessage.ts | 0 .../IMatrixEventContentSetRoomJoinRules.ts | 4 +- .../IMatrixEventContentSetRoomName.ts | 0 .../IMatrixEventContentSetRoomTopic.ts | 0 .../definitions/IMatrixEventContent/index.ts | 12 +- .../matrix}/definitions/MatrixEventType.ts | 12 +- .../matrix/handlers/BaseEvent.ts | 16 + .../infrastructure/matrix/handlers/Room.ts | 65 +++ .../infrastructure/matrix/handlers/index.ts | 16 + .../infrastructure/queue/InMemoryQueue.ts | 16 + .../rocket-chat/adapters/Message.ts | 9 + .../rocket-chat/adapters/Notification.ts | 14 + .../rocket-chat/adapters/Room.ts | 103 +++++ .../rocket-chat/adapters/Settings.ts | 192 ++++++++ .../rocket-chat/adapters/User.ts | 85 ++++ .../rocket-chat/adapters}/logger.ts | 2 +- .../rocket-chat/converters/RoomSender.ts | 34 ++ .../server/matrix-client/index.ts | 9 - .../server/matrix-client/message.ts | 25 - .../server/matrix-client/room.ts | 68 --- .../server/matrix-client/user.ts | 174 ------- .../server/methods/checkBridgedRoomExists.ts | 7 - apps/meteor/app/federation-v2/server/queue.ts | 16 - .../app/federation-v2/server/settings.ts | 136 ------ .../app/federation-v2/server/startup.ts | 37 -- .../app/lib/client/methods/sendMessage.js | 6 +- .../app/lib/server/methods/sendMessage.js | 13 +- apps/meteor/app/models/server/raw/Rooms.js | 4 +- .../app/slashcommands-bridge/server/index.ts | 70 +-- apps/meteor/app/ui-sidenav/client/roomList.js | 10 +- apps/meteor/package.json | 2 + .../server/modules/watchers/publishFields.ts | 2 +- .../meteor/server/startup/migrations/index.ts | 1 + apps/meteor/server/startup/migrations/v266.ts | 19 + .../application/RoomServiceReceiver.spec.ts | 436 ++++++++++++++++++ .../application/RoomServiceSender.spec.ts | 311 +++++++++++++ .../unit/domain/FederatedRoom.spec.ts | 169 +++++++ .../unit/domain/FederatedUser.spec.ts | 43 ++ .../matrix/converters/RoomReceiver.spec.ts | 328 +++++++++++++ .../matrix/handlers/BaseEvent.spec.ts | 45 ++ .../handlers/MatrixEventsHandler.spec.ts | 25 + .../queue/InMemoryQueue.spec.ts | 28 ++ .../rocket-chat/adapters/Room.spec.ts | 3 + .../rocket-chat/adapters/Settings.spec.ts | 3 + .../rocket-chat/adapters/User.spec.ts | 5 + .../rocket-chat/converters/RoomSender.spec.ts | 72 +++ packages/core-typings/src/IRoom.ts | 1 + yarn.lock | 101 +++- 76 files changed, 3234 insertions(+), 1192 deletions(-) create mode 100644 apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts create mode 100644 apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts create mode 100644 apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts create mode 100644 apps/meteor/app/federation-v2/server/application/input/RoomSenderDto.ts delete mode 100644 apps/meteor/app/federation-v2/server/bridge.ts delete mode 100644 apps/meteor/app/federation-v2/server/config.ts delete mode 100644 apps/meteor/app/federation-v2/server/data-interface/index.ts delete mode 100644 apps/meteor/app/federation-v2/server/data-interface/message.ts delete mode 100644 apps/meteor/app/federation-v2/server/data-interface/room.ts delete mode 100644 apps/meteor/app/federation-v2/server/data-interface/user.ts create mode 100644 apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts create mode 100644 apps/meteor/app/federation-v2/server/domain/FederatedUser.ts create mode 100644 apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts delete mode 100644 apps/meteor/app/federation-v2/server/eventHandler.ts delete mode 100644 apps/meteor/app/federation-v2/server/events/createRoom.ts delete mode 100644 apps/meteor/app/federation-v2/server/events/index.ts delete mode 100644 apps/meteor/app/federation-v2/server/events/roomMembership.ts delete mode 100644 apps/meteor/app/federation-v2/server/events/sendMessage.ts delete mode 100644 apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts delete mode 100644 apps/meteor/app/federation-v2/server/events/setRoomName.ts delete mode 100644 apps/meteor/app/federation-v2/server/events/setRoomTopic.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/Factory.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts rename apps/meteor/app/federation-v2/server/{ => infrastructure/matrix}/definitions/IMatrixEvent.ts (84%) rename apps/meteor/app/federation-v2/server/{ => infrastructure/matrix}/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts (100%) rename apps/meteor/app/federation-v2/server/{ => infrastructure/matrix}/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts (64%) rename apps/meteor/app/federation-v2/server/{ => infrastructure/matrix}/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts (100%) rename apps/meteor/app/federation-v2/server/{ => infrastructure/matrix}/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts (61%) rename apps/meteor/app/federation-v2/server/{ => infrastructure/matrix}/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts (100%) rename apps/meteor/app/federation-v2/server/{ => infrastructure/matrix}/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts (100%) rename apps/meteor/app/federation-v2/server/{ => infrastructure/matrix}/definitions/IMatrixEventContent/index.ts (57%) rename apps/meteor/app/federation-v2/server/{ => infrastructure/matrix}/definitions/MatrixEventType.ts (51%) create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/queue/InMemoryQueue.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Notification.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts rename apps/meteor/app/federation-v2/server/{ => infrastructure/rocket-chat/adapters}/logger.ts (73%) create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender.ts delete mode 100644 apps/meteor/app/federation-v2/server/matrix-client/index.ts delete mode 100644 apps/meteor/app/federation-v2/server/matrix-client/message.ts delete mode 100644 apps/meteor/app/federation-v2/server/matrix-client/room.ts delete mode 100644 apps/meteor/app/federation-v2/server/matrix-client/user.ts delete mode 100644 apps/meteor/app/federation-v2/server/methods/checkBridgedRoomExists.ts delete mode 100644 apps/meteor/app/federation-v2/server/queue.ts delete mode 100644 apps/meteor/app/federation-v2/server/settings.ts delete mode 100644 apps/meteor/app/federation-v2/server/startup.ts create mode 100644 apps/meteor/server/startup/migrations/v266.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceReceiver.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceSender.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedRoom.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedUser.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/BaseEvent.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/MatrixEventsHandler.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/queue/InMemoryQueue.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Room.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Settings.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/User.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/converters/RoomSender.spec.ts diff --git a/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts b/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts new file mode 100644 index 000000000000..49898c6ec2a7 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts @@ -0,0 +1,217 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { FederatedRoom } from '../domain/FederatedRoom'; +import { FederatedUser } from '../domain/FederatedUser'; +import { EVENT_ORIGIN, IFederationBridge } from '../domain/IFederationBridge'; +import { RocketChatMessageAdapter } from '../infrastructure/rocket-chat/adapters/Message'; +import { RocketChatRoomAdapter } from '../infrastructure/rocket-chat/adapters/Room'; +import { RocketChatSettingsAdapter } from '../infrastructure/rocket-chat/adapters/Settings'; +import { RocketChatUserAdapter } from '../infrastructure/rocket-chat/adapters/User'; +import { + FederationRoomCreateInputDto, + FederationRoomChangeMembershipDto, + FederationRoomSendInternalMessageDto, + FederationRoomChangeJoinRulesDto, + FederationRoomChangeNameDto, + FederationRoomChangeTopicDto, +} from './input/RoomReceiverDto'; + +export class FederationRoomServiceReceiver { + constructor( + private rocketRoomAdapter: RocketChatRoomAdapter, + private rocketUserAdapter: RocketChatUserAdapter, + private rocketMessageAdapter: RocketChatMessageAdapter, + private rocketSettingsAdapter: RocketChatSettingsAdapter, + private bridge: IFederationBridge, + ) {} // eslint-disable-line no-empty-function + + public async createRoom(roomCreateInput: FederationRoomCreateInputDto): Promise { + const { + externalRoomId, + externalInviterId, + normalizedInviterId, + externalRoomName, + normalizedRoomId, + roomType, + wasInternallyProgramaticallyCreated = false, + } = roomCreateInput; + + if ((await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId)) || wasInternallyProgramaticallyCreated) { + return; + } + + if (!(await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(externalInviterId); + const name = externalUserProfileInformation?.displayname || normalizedInviterId; + const federatedCreatorUser = FederatedUser.createInstance(externalInviterId, { + name, + username: normalizedInviterId, + existsOnlyOnProxyServer: false, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedCreatorUser); + } + const creator = await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId); + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + normalizedRoomId, + creator as FederatedUser, + roomType || RoomType.CHANNEL, + externalRoomName, + ); + await this.rocketRoomAdapter.createFederatedRoom(newFederatedRoom); + } + + public async changeRoomMembership(roomChangeMembershipInput: FederationRoomChangeMembershipDto): Promise { + const { + externalRoomId, + normalizedInviteeId, + normalizedRoomId, + normalizedInviterId, + externalRoomName, + externalInviteeId, + externalInviterId, + inviteeUsernameOnly, + inviterUsernameOnly, + eventOrigin, + roomType, + leave, + } = roomChangeMembershipInput; + const affectedFederatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!affectedFederatedRoom && eventOrigin === EVENT_ORIGIN.LOCAL) { + throw new Error(`Could not find room with external room id: ${externalRoomId}`); + } + const isInviterFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + externalInviterId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + const isInviteeFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + externalInviteeId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + + if (!(await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(externalInviterId); + const name = externalUserProfileInformation.displayname || normalizedInviterId; + const username = isInviterFromTheSameHomeServer ? inviterUsernameOnly : normalizedInviterId; + const federatedInviterUser = FederatedUser.createInstance(externalInviterId, { + name, + username, + existsOnlyOnProxyServer: isInviterFromTheSameHomeServer, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedInviterUser); + } + + if (!(await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviteeId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(externalInviteeId); + const name = externalUserProfileInformation.displayname || normalizedInviteeId; + const username = isInviteeFromTheSameHomeServer ? inviteeUsernameOnly : normalizedInviteeId; + const federatedInviteeUser = FederatedUser.createInstance(externalInviteeId, { + name, + username, + existsOnlyOnProxyServer: isInviteeFromTheSameHomeServer, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedInviteeUser); + } + + const federatedInviteeUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviteeId); + const federatedInviterUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId); + + if (!affectedFederatedRoom && eventOrigin === EVENT_ORIGIN.REMOTE) { + const members = [federatedInviterUser, federatedInviteeUser] as any[]; + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + normalizedRoomId, + federatedInviterUser as FederatedUser, + roomType, + externalRoomName, + members, + ); + + await this.rocketRoomAdapter.createFederatedRoom(newFederatedRoom); + await this.bridge.joinRoom(externalRoomId, externalInviteeId); + } + const federatedRoom = affectedFederatedRoom || (await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId)); + + if (leave) { + return this.rocketRoomAdapter.removeUserFromRoom( + federatedRoom as FederatedRoom, + federatedInviteeUser as FederatedUser, + federatedInviterUser as FederatedUser, + ); + } + await this.rocketRoomAdapter.addUserToRoom( + federatedRoom as FederatedRoom, + federatedInviteeUser as FederatedUser, + federatedInviterUser as FederatedUser, + ); + } + + public async receiveExternalMessage(roomSendInternalMessageInput: FederationRoomSendInternalMessageDto): Promise { + const { externalRoomId, externalSenderId, text } = roomSendInternalMessageInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + const senderUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalSenderId); + if (!senderUser) { + return; + } + + await this.rocketMessageAdapter.sendMessage(senderUser, text, federatedRoom); + } + + public async changeJoinRules(roomJoinRulesChangeInput: FederationRoomChangeJoinRulesDto): Promise { + const { externalRoomId, roomType } = roomJoinRulesChangeInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + if (federatedRoom.isDirectMessage()) { + return; + } + + federatedRoom.setRoomType(roomType); + await this.rocketRoomAdapter.updateRoomType(federatedRoom); + } + + public async changeRoomName(roomChangeNameInput: FederationRoomChangeNameDto): Promise { + const { externalRoomId, normalizedRoomName } = roomChangeNameInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + if (federatedRoom.isDirectMessage()) { + return; + } + + federatedRoom.changeRoomName(normalizedRoomName); + + await this.rocketRoomAdapter.updateRoomName(federatedRoom); + } + + public async changeRoomTopic(roomChangeTopicInput: FederationRoomChangeTopicDto): Promise { + const { externalRoomId, roomTopic } = roomChangeTopicInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + if (federatedRoom.isDirectMessage()) { + return; + } + + federatedRoom.changeRoomTopic(roomTopic); + + await this.rocketRoomAdapter.updateRoomTopic(federatedRoom); + } +} diff --git a/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts b/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts new file mode 100644 index 000000000000..2be2283ddd86 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts @@ -0,0 +1,128 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; + +import { FederatedRoom } from '../domain/FederatedRoom'; +import { FederatedUser } from '../domain/FederatedUser'; +import { IFederationBridge } from '../domain/IFederationBridge'; +import { RocketChatNotificationAdapter } from '../infrastructure/rocket-chat/adapters/Notification'; +import { RocketChatRoomAdapter } from '../infrastructure/rocket-chat/adapters/Room'; +import { RocketChatSettingsAdapter } from '../infrastructure/rocket-chat/adapters/Settings'; +import { RocketChatUserAdapter } from '../infrastructure/rocket-chat/adapters/User'; +import { FederationRoomInviteUserDto, FederationRoomSendExternalMessageDto } from './input/RoomSenderDto'; + +export class FederationRoomServiceSender { + constructor( + private rocketRoomAdapter: RocketChatRoomAdapter, + private rocketUserAdapter: RocketChatUserAdapter, + private rocketSettingsAdapter: RocketChatSettingsAdapter, + private rocketNotificationAdapter: RocketChatNotificationAdapter, + private bridge: IFederationBridge, + ) {} // eslint-disable-line no-empty-function + + public async inviteUserToAFederatedRoom(roomInviteUserInput: FederationRoomInviteUserDto): Promise { + const { normalizedInviteeId, rawInviteeId, internalInviterId, inviteeUsernameOnly, internalRoomId } = roomInviteUserInput; + + if (!(await this.rocketUserAdapter.getFederatedUserByInternalId(internalInviterId))) { + const internalUser = (await this.rocketUserAdapter.getInternalUserById(internalInviterId)) as IUser; + const externalInviterId = await this.bridge.createUser( + internalUser.username as string, + internalUser.name as string, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + const federatedInviterUser = FederatedUser.createInstance(externalInviterId, { + name: internalUser.name as string, + username: internalUser.username as string, + existsOnlyOnProxyServer: true, + }); + await this.rocketUserAdapter.createFederatedUser(federatedInviterUser); + } + + if (!(await this.rocketUserAdapter.getFederatedUserByInternalUsername(normalizedInviteeId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(rawInviteeId); + const name = externalUserProfileInformation?.displayname || normalizedInviteeId; + const federatedInviteeUser = FederatedUser.createInstance(rawInviteeId, { + name, + username: normalizedInviteeId, + existsOnlyOnProxyServer: false, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedInviteeUser); + } + + const federatedInviterUser = (await this.rocketUserAdapter.getFederatedUserByInternalId(internalInviterId)) as FederatedUser; + const federatedInviteeUser = (await this.rocketUserAdapter.getFederatedUserByInternalUsername(normalizedInviteeId)) as FederatedUser; + const isInviteeFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + rawInviteeId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + + if (!(await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId))) { + const internalRoom = (await this.rocketRoomAdapter.getInternalRoomById(internalRoomId)) as IRoom; + const roomName = (internalRoom.fname || internalRoom.name) as string; + const externalRoomId = await this.bridge.createRoom( + federatedInviterUser.externalId, + federatedInviteeUser.externalId, + internalRoom.t as RoomType, + roomName, + internalRoom.topic, + ); + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + externalRoomId, + federatedInviterUser, + internalRoom.t as RoomType, + roomName, + ); + await this.rocketRoomAdapter.updateFederatedRoomByInternalRoomId(internalRoom._id, newFederatedRoom); + } + + const federatedRoom = (await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId)) as FederatedRoom; + const wasInvitedWhenTheRoomWasCreated = federatedRoom.isDirectMessage(); + if (isInviteeFromTheSameHomeServer) { + await this.bridge.createUser( + inviteeUsernameOnly, + federatedInviteeUser.internalReference.name as string, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + await this.bridge.inviteToRoom(federatedRoom.externalId, federatedInviterUser.externalId, federatedInviteeUser.externalId); + await this.bridge.joinRoom(federatedRoom.externalId, federatedInviteeUser.externalId); + } else if (!wasInvitedWhenTheRoomWasCreated) { + this.bridge.inviteToRoom(federatedRoom.externalId, federatedInviterUser.externalId, federatedInviteeUser.externalId).catch(() => { + this.rocketNotificationAdapter.notifyWithEphemeralMessage( + 'Federation_Matrix_only_owners_can_invite_users', + federatedInviterUser?.internalReference?._id, + internalRoomId, + federatedInviterUser?.internalReference?.language, + ); + }); + } + await this.rocketRoomAdapter.addUserToRoom(federatedRoom, federatedInviteeUser, federatedInviterUser); + } + + public async sendMessageFromRocketChat(roomSendExternalMessageInput: FederationRoomSendExternalMessageDto): Promise { + const { internalRoomId, internalSenderId, message } = roomSendExternalMessageInput; + + const federatedSender = await this.rocketUserAdapter.getFederatedUserByInternalId(internalSenderId); + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId); + + if (!federatedSender) { + throw new Error(`Could not find user id for ${internalSenderId}`); + } + if (!federatedRoom) { + throw new Error(`Could not find room id for ${internalRoomId}`); + } + + await this.bridge.sendMessage(federatedRoom.externalId, federatedSender.externalId, message.msg); + + return message; + } + + public async isAFederatedRoom(internalRoomId: string): Promise { + if (!internalRoomId) { + return false; + } + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId); + + return Boolean(federatedRoom?.isFederated()); + } +} diff --git a/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts b/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts new file mode 100644 index 000000000000..ed85b194d67d --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts @@ -0,0 +1,63 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { EVENT_ORIGIN } from '../../domain/IFederationBridge'; + +class BaseRoom { + externalRoomId: string; + + normalizedRoomId: string; +} + +export class FederationRoomCreateInputDto extends BaseRoom { + externalInviterId: string; + + normalizedInviterId: string; + + wasInternallyProgramaticallyCreated?: boolean; + + externalRoomName?: string; + + roomType?: RoomType; +} + +export class FederationRoomChangeMembershipDto extends BaseRoom { + externalInviterId: string; + + normalizedInviterId: string; + + inviterUsernameOnly: string; + + externalInviteeId: string; + + normalizedInviteeId: string; + + inviteeUsernameOnly: string; + + roomType: RoomType; + + eventOrigin: EVENT_ORIGIN; + + leave?: boolean; + + externalRoomName?: string; +} + +export class FederationRoomSendInternalMessageDto extends BaseRoom { + externalSenderId: string; + + normalizedSenderId: string; + + text: string; +} + +export class FederationRoomChangeJoinRulesDto extends BaseRoom { + roomType: RoomType; +} + +export class FederationRoomChangeNameDto extends BaseRoom { + normalizedRoomName: string; +} + +export class FederationRoomChangeTopicDto extends BaseRoom { + roomTopic: string; +} diff --git a/apps/meteor/app/federation-v2/server/application/input/RoomSenderDto.ts b/apps/meteor/app/federation-v2/server/application/input/RoomSenderDto.ts new file mode 100644 index 000000000000..700216105866 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/input/RoomSenderDto.ts @@ -0,0 +1,21 @@ +import { IMessage } from '@rocket.chat/core-typings'; + +export class FederationRoomInviteUserDto { + internalInviterId: string; + + internalRoomId: string; + + rawInviteeId: string; + + normalizedInviteeId: string; + + inviteeUsernameOnly: string; +} + +export class FederationRoomSendExternalMessageDto { + internalRoomId: string; + + internalSenderId: string; + + message: IMessage; +} diff --git a/apps/meteor/app/federation-v2/server/bridge.ts b/apps/meteor/app/federation-v2/server/bridge.ts deleted file mode 100644 index 895e472de021..000000000000 --- a/apps/meteor/app/federation-v2/server/bridge.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { Bridge as MatrixBridge } from '@rocket.chat/forked-matrix-appservice-bridge'; - -import { settings } from '../../settings/server'; -import { Settings } from '../../models/server/raw'; -import type { IMatrixEvent } from './definitions/IMatrixEvent'; -import type { MatrixEventType } from './definitions/MatrixEventType'; -import { addToQueue } from './queue'; -import { getRegistrationInfo } from './config'; -import { bridgeLogger } from './logger'; - -class Bridge { - private bridgeInstance: MatrixBridge; - - private isRunning = false; - - public async start(): Promise { - try { - await this.stop(); - await this.createInstance(); - - if (!this.isRunning) { - await this.bridgeInstance.run(this.getBridgePort()); - this.isRunning = true; - } - } catch (e) { - bridgeLogger.error('Failed to initialize the matrix-appservice-bridge.', e); - - bridgeLogger.error('Disabling Matrix Bridge. Please resolve error and try again'); - Settings.updateValueById('Federation_Matrix_enabled', false); - } - } - - public async stop(): Promise { - if (!this.isRunning) { - return; - } - // the http server can take some minutes to shutdown and this promise to be resolved - await this.bridgeInstance?.close(); - this.isRunning = false; - } - - public async getRoomStateByRoomId(userId: string, roomId: string): Promise[]> { - return Array.from(((await this.getInstance().getIntent(userId).roomState(roomId)) as IMatrixEvent[]) || []); - } - - public getInstance(): MatrixBridge { - return this.bridgeInstance; - } - - private async createInstance(): Promise { - bridgeLogger.info('Performing Dynamic Import of matrix-appservice-bridge'); - - // Dynamic import to prevent Rocket.Chat from loading the module until needed and then handle if that fails - const { Bridge: MatrixBridge, AppServiceRegistration } = await import('@rocket.chat/forked-matrix-appservice-bridge'); - - this.bridgeInstance = new MatrixBridge({ - homeserverUrl: settings.get('Federation_Matrix_homeserver_url'), - domain: settings.get('Federation_Matrix_homeserver_domain'), - registration: AppServiceRegistration.fromObject(getRegistrationInfo()), - disableStores: true, - controller: { - onAliasQuery: (alias, matrixRoomId): void => { - console.log('onAliasQuery', alias, matrixRoomId); - }, - onEvent: async (request /* , context*/): Promise => { - // Get the event - const event = request.getData() as unknown as IMatrixEvent; - - addToQueue(event); - }, - onLog: async (line, isError): Promise => { - console.log(line, isError); - }, - }, - }); - } - - private getBridgePort(): number { - const [, , port] = settings.get('Federation_Matrix_bridge_url').split(':'); - - return parseInt(port); - } -} - -export const matrixBridge = new Bridge(); diff --git a/apps/meteor/app/federation-v2/server/config.ts b/apps/meteor/app/federation-v2/server/config.ts deleted file mode 100644 index d1bad2455d80..000000000000 --- a/apps/meteor/app/federation-v2/server/config.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { AppServiceOutput } from '@rocket.chat/forked-matrix-appservice-bridge'; - -import { settings } from '../../settings/server'; - -export type bridgeUrlTuple = [string, string, number]; - -export function getRegistrationInfo(): AppServiceOutput { - /* eslint-disable @typescript-eslint/camelcase */ - return { - id: settings.get('Federation_Matrix_id'), - hs_token: settings.get('Federation_Matrix_hs_token'), - as_token: settings.get('Federation_Matrix_as_token'), - url: settings.get('Federation_Matrix_bridge_url'), - sender_localpart: settings.get('Federation_Matrix_bridge_localpart'), - namespaces: { - users: [ - { - exclusive: false, - // Reserve these MXID's (usernames) - regex: `.*`, - }, - ], - aliases: [ - { - exclusive: false, - // Reserve these room aliases - regex: `.*`, - }, - ], - rooms: [ - { - exclusive: false, - // This regex is used to define which rooms we listen to with the bridge. - // This does not reserve the rooms like the other namespaces. - regex: '.*', - }, - ], - }, - rate_limited: false, - protocols: null, - }; - /* eslint-enable @typescript-eslint/camelcase */ -} diff --git a/apps/meteor/app/federation-v2/server/data-interface/index.ts b/apps/meteor/app/federation-v2/server/data-interface/index.ts deleted file mode 100644 index 18e2fbf7020f..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as message from './message'; -import * as room from './room'; -import * as user from './user'; - -export const dataInterface = { - message: message.normalize, - room: room.normalize, - user: user.normalize, -}; diff --git a/apps/meteor/app/federation-v2/server/data-interface/message.ts b/apps/meteor/app/federation-v2/server/data-interface/message.ts deleted file mode 100644 index 7d27732f93e6..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/message.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IMessage, IUser } from '@rocket.chat/core-typings'; - -import { dataInterface } from '.'; - -interface INormalizedMessage extends IMessage { - u: Required>; -} - -export const normalize = async (message: IMessage): Promise => { - // TODO: normalize the entire payload (if needed) - const normalizedMessage: INormalizedMessage = message as INormalizedMessage; - - // Normalize the user - normalizedMessage.u = (await dataInterface.user(message.u._id)) as Required>; - - return normalizedMessage; -}; diff --git a/apps/meteor/app/federation-v2/server/data-interface/room.ts b/apps/meteor/app/federation-v2/server/data-interface/room.ts deleted file mode 100644 index df1d2163badf..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/room.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IRoom } from '@rocket.chat/core-typings'; - -import { Rooms } from '../../../models/server'; - -export const normalize = async (roomId: string): Promise => { - // Normalize the user - return Rooms.findOneById(roomId); -}; diff --git a/apps/meteor/app/federation-v2/server/data-interface/user.ts b/apps/meteor/app/federation-v2/server/data-interface/user.ts deleted file mode 100644 index 15fb48843428..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/user.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IUser } from '@rocket.chat/core-typings'; - -import { Users } from '../../../models/server'; - -export const normalize = async (userId: string): Promise => { - // Normalize the user - return Users.findOneById(userId); -}; diff --git a/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts b/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts new file mode 100644 index 000000000000..c82b078c49e7 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts @@ -0,0 +1,78 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { IRoom, IUser } from '@rocket.chat/core-typings'; + +import { FederatedUser } from './FederatedUser'; + +export class FederatedRoom { + public externalId: string; + + public members?: FederatedUser[]; + + public internalReference: IRoom; + + // eslint-disable-next-line + private constructor() {} + + private static generateTemporaryName(normalizedExternalId: string): string { + return `Federation-${normalizedExternalId}`; + } + + public static createInstance( + externalId: string, + normalizedExternalId: string, + creator: FederatedUser, + type: RoomType, + name?: string, + members?: IUser[], + ): FederatedRoom { + const roomName = name || FederatedRoom.generateTemporaryName(normalizedExternalId); + return Object.assign(new FederatedRoom(), { + externalId, + ...(type === RoomType.DIRECT_MESSAGE ? { members } : {}), + internalReference: { + t: type, + name: roomName, + fname: roomName, + u: creator.internalReference, + }, + }); + } + + public static build(): FederatedRoom { + return new FederatedRoom(); + } + + public isDirectMessage(): boolean { + return this.internalReference?.t === RoomType.DIRECT_MESSAGE; + } + + public setRoomType(type: RoomType): void { + if (this.isDirectMessage()) { + throw new Error('Its not possible to change a direct message type'); + } + this.internalReference.t = type; + } + + public changeRoomName(name: string): void { + if (this.isDirectMessage()) { + throw new Error('Its not possible to change a direct message name'); + } + this.internalReference.name = name; + this.internalReference.fname = name; + } + + public changeRoomTopic(topic: string): void { + if (this.isDirectMessage()) { + throw new Error('Its not possible to change a direct message topic'); + } + this.internalReference.description = topic; + } + + public getMembers(): IUser[] { + return this.isDirectMessage() && this.members && this.members.length > 0 ? this.members.map((user) => user.internalReference) : []; + } + + public isFederated(): boolean { + return this.internalReference?.federated === true; + } +} diff --git a/apps/meteor/app/federation-v2/server/domain/FederatedUser.ts b/apps/meteor/app/federation-v2/server/domain/FederatedUser.ts new file mode 100644 index 000000000000..225b0fabc003 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/domain/FederatedUser.ts @@ -0,0 +1,38 @@ +import { IUser } from '@rocket.chat/core-typings'; + +export interface IFederatedUserCreationParams { + name: string; + username: string; + existsOnlyOnProxyServer: boolean; +} + +export class FederatedUser { + public externalId: string; + + public internalReference: IUser; + + public existsOnlyOnProxyServer: boolean; + + // eslint-disable-next-line + private constructor() {} + + public static createInstance(externalId: string, params: IFederatedUserCreationParams): FederatedUser { + return Object.assign(new FederatedUser(), { + externalId, + existsOnlyOnProxyServer: params.existsOnlyOnProxyServer, + internalReference: { + username: params.username, + name: params.name, + type: 'user', + status: 'online', + active: true, + roles: ['user'], + requirePasswordChange: false, + }, + }); + } + + public static build(): FederatedUser { + return new FederatedUser(); + } +} diff --git a/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts b/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts new file mode 100644 index 000000000000..86310ec30f17 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts @@ -0,0 +1,25 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +export interface IFederationBridge { + start(): Promise; + stop(): Promise; + onFederationAvailabilityChanged(enabled: boolean): Promise; + getUserProfileInformation(externalUserId: string): Promise; + joinRoom(externalRoomId: string, externalUserId: string): Promise; + createRoom( + externalCreatorId: string, + externalInviteeId: string, + roomType: RoomType, + roomName: string, + roomTopic?: string, + ): Promise; + inviteToRoom(externalRoomId: string, externalInviterId: string, externalInviteeId: string): Promise; + sendMessage(externalRoomId: string, externaSenderId: string, text: string): Promise; + createUser(username: string, name: string, domain: string): Promise; + isUserIdFromTheSameHomeserver(externalUserId: string, domain: string): boolean; +} + +export enum EVENT_ORIGIN { + LOCAL = 'LOCAL', + REMOTE = 'REMOTE', +} diff --git a/apps/meteor/app/federation-v2/server/eventHandler.ts b/apps/meteor/app/federation-v2/server/eventHandler.ts deleted file mode 100644 index 166ed1199adc..000000000000 --- a/apps/meteor/app/federation-v2/server/eventHandler.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { IMatrixEvent } from './definitions/IMatrixEvent'; -import { MatrixEventType } from './definitions/MatrixEventType'; -import { handleRoomMembership, handleCreateRoom, handleSendMessage, setRoomJoinRules, setRoomName, setRoomTopic } from './events'; - -export const eventHandler = async (event: IMatrixEvent): Promise => { - console.log(`Processing ${event.type}...`, JSON.stringify(event, null, 2)); - - switch (event.type) { - case MatrixEventType.CREATE_ROOM: { - await handleCreateRoom(event as IMatrixEvent); - - break; - } - case MatrixEventType.ROOM_MEMBERSHIP: { - await handleRoomMembership(event as IMatrixEvent); - - break; - } - case MatrixEventType.SET_ROOM_JOIN_RULES: { - await setRoomJoinRules(event as IMatrixEvent); - - break; - } - case MatrixEventType.SET_ROOM_NAME: { - await setRoomName(event as IMatrixEvent); - - break; - } - case MatrixEventType.SET_ROOM_TOPIC: { - await setRoomTopic(event as IMatrixEvent); - - break; - } - case MatrixEventType.SEND_MESSAGE: { - await handleSendMessage(event as IMatrixEvent); - - break; - } - // case MatrixEventType.SET_ROOM_POWER_LEVELS: - // case MatrixEventType.SET_ROOM_CANONICAL_ALIAS: - // case MatrixEventType.SET_ROOM_HISTORY_VISIBILITY: - // case MatrixEventType.SET_ROOM_GUEST_ACCESS: { - // console.log(`Ignoring ${event.type}`); - // - // break; - // } - default: - console.log(`Could not find handler for ${event.type}`, event); - } -}; diff --git a/apps/meteor/app/federation-v2/server/events/createRoom.ts b/apps/meteor/app/federation-v2/server/events/createRoom.ts deleted file mode 100644 index 5cca3032c455..000000000000 --- a/apps/meteor/app/federation-v2/server/events/createRoom.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { IRoom, RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import { ICreatedRoom } from '@rocket.chat/core-typings'; -import { IUser } from '@rocket.chat/apps-engine/definition/users'; - -import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '../../../models/server'; -import { Rooms } from '../../../models/server/raw'; -import { createRoom } from '../../../lib/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; -import { checkBridgedRoomExists } from '../methods/checkBridgedRoomExists'; -import { matrixClient } from '../matrix-client'; -import { SetRoomJoinRules } from '../definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; -import { matrixBridge } from '../bridge'; -import { setRoomJoinRules } from './setRoomJoinRules'; -import { setRoomName } from './setRoomName'; -import { handleRoomMembership } from './roomMembership'; - -const removeUselessCharacterFromMatrixRoomId = (matrixRoomId: string): string => { - const prefixedRoomIdOnly = matrixRoomId.split(':')[0]; - const prefix = '!'; - - return prefixedRoomIdOnly?.replace(prefix, ''); -}; - -const generateRoomNameForLocalServer = (matrixRoomId: string, matrixRoomName?: string): string => { - return matrixRoomName || `Federation-${removeUselessCharacterFromMatrixRoomId(matrixRoomId)}`; -}; - -const createLocalRoomAsync = async (roomType: RoomType, roomName: string, creator: IUser, members: IUser[] = []): Promise => { - return new Promise((resolve) => resolve(createRoom(roomType, roomName, creator.username, members as any[]) as ICreatedRoom)); -}; - -const createBridgedRecordRoom = async (roomId: IRoom['id'], matrixRoomId: string): Promise => - new Promise((resolve) => resolve(MatrixBridgedRoom.insert({ rid: roomId, mri: matrixRoomId }))); - -const createLocalUserIfNecessary = async (matrixUserId: string): Promise => { - const { uid } = await matrixClient.user.createLocal(matrixUserId); - - return uid; -}; - -const applyRoomStateIfNecessary = async (matrixRoomId: string, roomState?: IMatrixEvent[]): Promise => { - // TODO: this should be better - /* eslint-disable no-await-in-loop */ - for (const state of roomState || []) { - switch (state.type) { - case 'm.room.create': - continue; - case 'm.room.join_rules': { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/camelcase - await setRoomJoinRules({ room_id: matrixRoomId, ...state }); - - break; - } - case 'm.room.name': { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/camelcase - await setRoomName({ room_id: matrixRoomId, ...state }); - - break; - } - case 'm.room.member': { - // @ts-ignore - if (state.content.membership === 'join') { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/camelcase,@typescript-eslint/no-use-before-define - await handleRoomMembership({ room_id: matrixRoomId, ...state }); - } - - break; - } - } - } - /* eslint-enable no-await-in-loop */ -}; - -const mapLocalAndExternal = async (roomId: string, matrixRoomId: string): Promise => { - await createBridgedRecordRoom(roomId, matrixRoomId); - await Rooms.setAsBridged(roomId); -}; - -const tryToGetDataFromExternalRoom = async ( - senderMatrixUserId: string, - matrixRoomId: string, - roomState: IMatrixEvent[] = [], -): Promise> => { - const finalRoomState = - roomState && roomState?.length > 0 ? roomState : await matrixBridge.getRoomStateByRoomId(senderMatrixUserId, matrixRoomId); - const externalRoomName = finalRoomState.find((stateEvent: Record) => stateEvent.type === MatrixEventType.SET_ROOM_NAME) - ?.content?.name; - const externalRoomJoinRule = finalRoomState.find( - (stateEvent: Record) => stateEvent.type === MatrixEventType.SET_ROOM_JOIN_RULES, - )?.content?.join_rule; - - return { - externalRoomName, - externalRoomJoinRule, - }; -}; - -export const createLocalDirectMessageRoom = async (matrixRoomId: string, creator: IUser, affectedUser: IUser): Promise => { - const { _id: roomId } = await createLocalRoomAsync(RoomType.DIRECT_MESSAGE, generateRoomNameForLocalServer(matrixRoomId), creator, [ - creator, - affectedUser, - ]); - await mapLocalAndExternal(roomId, matrixRoomId); - - return roomId; -}; - -export const getLocalRoomType = (matrixJoinRule = '', matrixRoomIsDirect = false): RoomType => { - const mapping: Record = { - [SetRoomJoinRules.JOIN]: RoomType.CHANNEL, - [SetRoomJoinRules.INVITE]: RoomType.PRIVATE_GROUP, - }; - const roomType = mapping[matrixJoinRule] || RoomType.CHANNEL; - - return roomType === RoomType.PRIVATE_GROUP && matrixRoomIsDirect ? RoomType.DIRECT_MESSAGE : roomType; -}; - -export const createLocalChannelsRoom = async ( - matrixRoomId: string, - senderMatrixUserId: string, - creator: IUser, - roomState?: IMatrixEvent[], -): Promise => { - let roomName = ''; - let joinRule; - - try { - const { externalRoomName, externalRoomJoinRule } = await tryToGetDataFromExternalRoom(senderMatrixUserId, matrixRoomId, roomState); - roomName = externalRoomName; - joinRule = externalRoomJoinRule; - } catch (err) { - // no-op - } - const { rid: roomId } = await createLocalRoomAsync( - getLocalRoomType(joinRule), - generateRoomNameForLocalServer(matrixRoomId, roomName), - creator, - ); - await mapLocalAndExternal(roomId, matrixRoomId); - - return roomId; -}; - -export const processFirstAccessFromExternalServer = async ( - matrixRoomId: string, - senderMatrixUserId: string, - affectedMatrixUserId: string, - senderUser: IUser, - affectedUser: IUser, - isDirect = false, - roomState: IMatrixEvent[], -): Promise => { - let roomId; - if (isDirect) { - roomId = await createLocalDirectMessageRoom(matrixRoomId, senderUser, affectedUser); - } else { - roomId = await createLocalChannelsRoom(matrixRoomId, senderMatrixUserId, senderUser, roomState); - } - - await applyRoomStateIfNecessary(matrixRoomId, roomState); - await matrixBridge.getInstance().getIntent(affectedMatrixUserId).join(matrixRoomId); - - return roomId; -}; - -export const handleCreateRoom = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - sender, - content: { was_programatically_created: wasProgramaticallyCreated = false }, - } = event; - - // Check if the room already exists and if so, ignore - const roomExists = await checkBridgedRoomExists(matrixRoomId); - if (roomExists || wasProgramaticallyCreated) { - return; - } - - const bridgedUserId = await MatrixBridgedUser.getId(sender); - const creator = await Users.findOneById(bridgedUserId || (await createLocalUserIfNecessary(sender))); - - await createLocalChannelsRoom(matrixRoomId, sender, creator); -}; diff --git a/apps/meteor/app/federation-v2/server/events/index.ts b/apps/meteor/app/federation-v2/server/events/index.ts deleted file mode 100644 index ef403e8e78cd..000000000000 --- a/apps/meteor/app/federation-v2/server/events/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './createRoom'; -export * from './roomMembership'; -export * from './sendMessage'; -export * from './setRoomJoinRules'; -export * from './setRoomName'; -export * from './setRoomTopic'; diff --git a/apps/meteor/app/federation-v2/server/events/roomMembership.ts b/apps/meteor/app/federation-v2/server/events/roomMembership.ts deleted file mode 100644 index d51233c1f14e..000000000000 --- a/apps/meteor/app/federation-v2/server/events/roomMembership.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { IUser } from '@rocket.chat/apps-engine/definition/users'; - -import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '../../../models/server'; -import { addUserToRoom, removeUserFromRoom } from '../../../lib/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; -import { AddMemberToRoomMembership } from '../definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom'; -import { matrixClient } from '../matrix-client'; -import { processFirstAccessFromExternalServer } from './createRoom'; - -const extractServerNameFromMatrixUserId = (matrixRoomId = ''): string => matrixRoomId.split(':')[1]; - -const addUserToRoomAsync = async (roomId: string, affectedUser: IUser, senderUser?: IUser): Promise => { - new Promise((resolve) => resolve(addUserToRoom(roomId, affectedUser as any, senderUser as any))); -}; - -export const handleRoomMembership = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - sender: senderMatrixUserId, - state_key: affectedMatrixUserId, - content: { membership, is_direct: isDirect = false }, - invite_room_state: roomState, - } = event; - - // Find the bridged room id - let roomId = await MatrixBridgedRoom.getId(matrixRoomId); - const fromADifferentServer = - extractServerNameFromMatrixUserId(senderMatrixUserId) !== extractServerNameFromMatrixUserId(affectedMatrixUserId); - - // If there is no room id, throw error - if (!roomId && !fromADifferentServer) { - throw new Error(`Could not find room with matrixRoomId: ${matrixRoomId}`); - } - - // Find the sender user - const senderUserId = await MatrixBridgedUser.getId(senderMatrixUserId); - let senderUser = await Users.findOneById(senderUserId); - // If the sender user does not exist, it means we need to create it - if (!senderUser) { - const { uid } = await matrixClient.user.createLocal(senderMatrixUserId); - - senderUser = Users.findOneById(uid); - } - - // Find the affected user - const affectedUserId = await MatrixBridgedUser.getId(affectedMatrixUserId); - let affectedUser = await Users.findOneById(affectedUserId); - // If the affected user does not exist, it means we need to create it - if (!affectedUser) { - const { uid } = await matrixClient.user.createLocal(affectedMatrixUserId); - - affectedUser = Users.findOneById(uid); - } - - if (!roomId && fromADifferentServer) { - roomId = await processFirstAccessFromExternalServer( - matrixRoomId, - senderMatrixUserId, - affectedMatrixUserId, - senderUser, - affectedUser, - isDirect, - roomState as IMatrixEvent[], - ); - } - - if (!roomId) { - return; - } - - switch (membership) { - case AddMemberToRoomMembership.JOIN: - await addUserToRoomAsync(roomId, affectedUser); - break; - case AddMemberToRoomMembership.INVITE: - // TODO: this should be a local invite - await addUserToRoomAsync(roomId, affectedUser, senderUser); - break; - case AddMemberToRoomMembership.LEAVE: - await removeUserFromRoom(roomId, affectedUser, { - byUser: senderUser, - }); - break; - } -}; diff --git a/apps/meteor/app/federation-v2/server/events/sendMessage.ts b/apps/meteor/app/federation-v2/server/events/sendMessage.ts deleted file mode 100644 index c70577d1e2af..000000000000 --- a/apps/meteor/app/federation-v2/server/events/sendMessage.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; -import { sendMessage } from '../../../lib/server'; -import { Rooms } from '../../../models/server/raw'; - -export const sendMessageAsync = async (user: any, msg: any, room: any): Promise => - new Promise((resolve) => resolve(sendMessage(user, msg, room))); - -export const handleSendMessage = async (event: IMatrixEvent): Promise => { - const { room_id: matrixRoomId, sender } = event; - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - if (!roomId) { - return; - } - - // Find the bridged user id - const userId = await MatrixBridgedUser.getId(sender); - - // Find the user - const user = await Users.findOneById(userId); - - const room = await Rooms.findOneById(roomId); - - await sendMessageAsync(user, { msg: event.content.body }, room); -}; diff --git a/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts b/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts deleted file mode 100644 index e95bf691bf43..000000000000 --- a/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; - -import { Rooms, Subscriptions } from '../../../models/server/raw'; -import { MatrixBridgedRoom } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; -import { SetRoomJoinRules } from '../definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; - -export const setRoomJoinRules = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - content: { join_rule: joinRule }, - } = event; - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - if (!roomId) { - return; - } - - const localRoom = await Rooms.findOneById(roomId); - - if (!localRoom || localRoom?.t === RoomType.DIRECT_MESSAGE) { - return; - } - - let type; - - switch (joinRule) { - case SetRoomJoinRules.INVITE: - type = RoomType.PRIVATE_GROUP; - break; - case SetRoomJoinRules.JOIN: - default: - type = RoomType.CHANNEL; - } - - await Rooms.update( - { _id: roomId }, - { - $set: { - t: type, - }, - }, - ); - - await Subscriptions.update( - { rid: roomId }, - { - $set: { - t: type, - }, - }, - { multi: true }, - ); -}; diff --git a/apps/meteor/app/federation-v2/server/events/setRoomName.ts b/apps/meteor/app/federation-v2/server/events/setRoomName.ts deleted file mode 100644 index 243791841d70..000000000000 --- a/apps/meteor/app/federation-v2/server/events/setRoomName.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Rooms, Subscriptions } from '../../../models/server/raw'; -import { MatrixBridgedRoom } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; - -export const setRoomName = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - content: { name }, - } = event; - - // Normalize room name - const normalizedName = name.replace('@', ''); - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - - if (!roomId) { - return; - } - - await Rooms.update( - { _id: roomId }, - { - $set: { - name: normalizedName, - fname: normalizedName, - }, - }, - ); - - await Subscriptions.update( - { rid: roomId }, - { - $set: { - name: normalizedName, - fname: normalizedName, - }, - }, - { multi: true }, - ); -}; diff --git a/apps/meteor/app/federation-v2/server/events/setRoomTopic.ts b/apps/meteor/app/federation-v2/server/events/setRoomTopic.ts deleted file mode 100644 index d75d38df651e..000000000000 --- a/apps/meteor/app/federation-v2/server/events/setRoomTopic.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MatrixBridgedRoom, Rooms } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; - -export const setRoomTopic = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - content: { topic }, - } = event; - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - - Rooms.update( - { _id: roomId }, - { - $set: { - description: topic, - }, - }, - ); -}; diff --git a/apps/meteor/app/federation-v2/server/index.ts b/apps/meteor/app/federation-v2/server/index.ts index e331badfd006..d2d90966d4d0 100644 --- a/apps/meteor/app/federation-v2/server/index.ts +++ b/apps/meteor/app/federation-v2/server/index.ts @@ -1,4 +1,35 @@ -import './settings'; -import { startBridge } from './startup'; +import { FederationFactory } from './infrastructure/Factory'; -startBridge(); +const PROCESSING_CONCURRENCY = 1; + +const rocketSettingsAdapter = FederationFactory.buildRocketSettingsAdapter(); +rocketSettingsAdapter.initialize(); +const queueInstance = FederationFactory.buildQueue(); +const federation = FederationFactory.buildBridge(rocketSettingsAdapter, queueInstance); +const rocketRoomAdapter = FederationFactory.buildRocketRoomAdapter(); +const rocketUserAdapter = FederationFactory.buildRocketUserAdapter(); +const rocketMessageAdapter = FederationFactory.buildRocketMessageAdapter(); +const rocketNotificationAdapter = FederationFactory.buildRocketNotificationdapter(); + +const federationRoomServiceReceiver = FederationFactory.buildRoomServiceReceiver( + rocketRoomAdapter, + rocketUserAdapter, + rocketMessageAdapter, + rocketSettingsAdapter, + federation, +); +const federationEventsHandler = FederationFactory.buildEventHandlers(federationRoomServiceReceiver); + +export const federationRoomServiceSender = FederationFactory.buildRoomServiceSender( + rocketRoomAdapter, + rocketUserAdapter, + rocketSettingsAdapter, + rocketNotificationAdapter, + federation, +); + +(async (): Promise => { + queueInstance.setHandler(federationEventsHandler.handleEvent.bind(federationEventsHandler), PROCESSING_CONCURRENCY); + await federation.start(); + await rocketSettingsAdapter.onFederationEnabledStatusChanged(federation.onFederationAvailabilityChanged.bind(federation)); +})(); diff --git a/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts b/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts new file mode 100644 index 000000000000..36a20f827c10 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts @@ -0,0 +1,90 @@ +import { FederationRoomServiceReceiver } from '../application/RoomServiceReceiver'; +import { FederationRoomServiceSender } from '../application/RoomServiceSender'; +import { MatrixBridge } from './matrix/Bridge'; +import { MatrixEventsHandler } from './matrix/handlers'; +import { + MatrixRoomCreatedHandler, + MatrixRoomJoinRulesChangedHandler, + MatrixRoomMembershipChangedHandler, + MatrixRoomMessageSentHandler, + MatrixRoomNameChangedHandler, + MatrixRoomTopicChangedHandler, +} from './matrix/handlers/Room'; +import { InMemoryQueue } from './queue/InMemoryQueue'; +import { RocketChatMessageAdapter } from './rocket-chat/adapters/Message'; +import { RocketChatRoomAdapter } from './rocket-chat/adapters/Room'; +import { RocketChatSettingsAdapter } from './rocket-chat/adapters/Settings'; +import { RocketChatUserAdapter } from './rocket-chat/adapters/User'; +import { IFederationBridge } from '../domain/IFederationBridge'; +import { RocketChatNotificationAdapter } from './rocket-chat/adapters/Notification'; + +export class FederationFactory { + public static buildRocketSettingsAdapter(): RocketChatSettingsAdapter { + return new RocketChatSettingsAdapter(); + } + + public static buildRocketRoomAdapter(): RocketChatRoomAdapter { + return new RocketChatRoomAdapter(); + } + + public static buildRocketUserAdapter(): RocketChatUserAdapter { + return new RocketChatUserAdapter(); + } + + public static buildRocketMessageAdapter(): RocketChatMessageAdapter { + return new RocketChatMessageAdapter(); + } + + public static buildRocketNotificationdapter(): RocketChatNotificationAdapter { + return new RocketChatNotificationAdapter(); + } + + public static buildQueue(): InMemoryQueue { + return new InMemoryQueue(); + } + + public static buildRoomServiceReceiver( + rocketRoomAdapter: RocketChatRoomAdapter, + rocketUserAdapter: RocketChatUserAdapter, + rocketMessageAdapter: RocketChatMessageAdapter, + rocketSettingsAdapter: RocketChatSettingsAdapter, + bridge: IFederationBridge, + ): FederationRoomServiceReceiver { + return new FederationRoomServiceReceiver(rocketRoomAdapter, rocketUserAdapter, rocketMessageAdapter, rocketSettingsAdapter, bridge); + } + + public static buildRoomServiceSender( + rocketRoomAdapter: RocketChatRoomAdapter, + rocketUserAdapter: RocketChatUserAdapter, + rocketSettingsAdapter: RocketChatSettingsAdapter, + rocketNotificationAdapter: RocketChatNotificationAdapter, + bridge: IFederationBridge, + ): FederationRoomServiceSender { + return new FederationRoomServiceSender(rocketRoomAdapter, rocketUserAdapter, rocketSettingsAdapter, rocketNotificationAdapter, bridge); + } + + public static buildBridge(rocketSettingsAdapter: RocketChatSettingsAdapter, queue: InMemoryQueue): IFederationBridge { + return new MatrixBridge( + rocketSettingsAdapter.getApplicationServiceId(), + rocketSettingsAdapter.getHomeServerUrl(), + rocketSettingsAdapter.getHomeServerDomain(), + rocketSettingsAdapter.getBridgeUrl(), + rocketSettingsAdapter.getBridgePort(), + rocketSettingsAdapter.generateRegistrationFileObject(), + queue.addToQueue.bind(queue), + ); + } + + public static buildEventHandlers(roomServiceReceive: FederationRoomServiceReceiver): MatrixEventsHandler { + const EVENT_HANDLERS = [ + new MatrixRoomCreatedHandler(roomServiceReceive), + new MatrixRoomMembershipChangedHandler(roomServiceReceive), + new MatrixRoomJoinRulesChangedHandler(roomServiceReceive), + new MatrixRoomNameChangedHandler(roomServiceReceive), + new MatrixRoomTopicChangedHandler(roomServiceReceive), + new MatrixRoomMessageSentHandler(roomServiceReceive), + ]; + + return new MatrixEventsHandler(EVENT_HANDLERS); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts new file mode 100644 index 000000000000..acb2824f6903 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts @@ -0,0 +1,177 @@ +import { AppServiceOutput, Bridge } from '@rocket.chat/forked-matrix-appservice-bridge'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { IFederationBridge } from '../../domain/IFederationBridge'; +import { bridgeLogger } from '../rocket-chat/adapters/logger'; +import { IMatrixEvent } from './definitions/IMatrixEvent'; +import { MatrixEventType } from './definitions/MatrixEventType'; + +export class MatrixBridge implements IFederationBridge { + private bridgeInstance: Bridge; + + private isRunning = false; + + constructor( + private appServiceId: string, + private homeServerUrl: string, + private homeServerDomain: string, + private bridgeUrl: string, + private bridgePort: number, + private homeServerRegistrationFile: Record, + private eventHandler: Function, + ) { + this.logInfo(); + } + + public async onFederationAvailabilityChanged(enabled: boolean): Promise { + if (!enabled) { + await this.stop(); + return; + } + await this.start(); + } + + public async start(): Promise { + try { + await this.stop(); + await this.createInstance(); + + if (!this.isRunning) { + await this.bridgeInstance.run(this.bridgePort); + this.isRunning = true; + } + } catch (e) { + bridgeLogger.error('Failed to initialize the matrix-appservice-bridge.', e); + bridgeLogger.error('Disabling Matrix Bridge. Please resolve error and try again'); + + // await this.settingsAdapter.disableFederation(); + } + } + + public async stop(): Promise { + if (!this.isRunning) { + return; + } + // the http server might take some minutes to shutdown, and this promise can take some time to be resolved + await this.bridgeInstance?.close(); + this.isRunning = false; + } + + public async getUserProfileInformation(externalUserId: string): Promise { + try { + return this.bridgeInstance.getIntent(externalUserId).getProfileInfo(externalUserId); + } catch (err) { + // no-op + } + } + + public async joinRoom(externalRoomId: string, externalUserId: string): Promise { + await this.bridgeInstance.getIntent(externalUserId).join(externalRoomId); + } + + public async inviteToRoom(externalRoomId: string, externalInviterId: string, externalInviteeId: string): Promise { + await this.bridgeInstance.getIntent(externalInviterId).invite(externalRoomId, externalInviteeId); + } + + public async createUser(username: string, name: string, domain: string): Promise { + const matrixUserId = `@${username?.toLowerCase()}:${domain}`; + const intent = this.bridgeInstance.getIntent(matrixUserId); + + await intent.ensureProfile(name); + await intent.setDisplayName(`${username} (${name})`); + + return matrixUserId; + } + + public async createRoom( + externalCreatorId: string, + externalInviteeId: string, + roomType: RoomType, + roomName: string, + roomTopic?: string, + ): Promise { + const intent = this.bridgeInstance.getIntent(externalCreatorId); + + const visibility = roomType === 'p' || roomType === 'd' ? 'invite' : 'public'; + const preset = roomType === 'p' || roomType === 'd' ? 'private_chat' : 'public_chat'; + + // Create the matrix room + const matrixRoom = await intent.createRoom({ + createAsClient: true, + options: { + name: roomName, + topic: roomTopic, + visibility, + preset, + ...this.parametersForDirectMessagesIfNecessary(roomType, externalInviteeId), + // eslint-disable-next-line @typescript-eslint/camelcase + creation_content: { + // eslint-disable-next-line @typescript-eslint/camelcase + was_internally_programatically_created: true, + }, + }, + }); + + return matrixRoom.room_id; + } + + public async sendMessage(externalRoomId: string, externaSenderId: string, text: string): Promise { + await this.bridgeInstance.getIntent(externaSenderId).sendText(externalRoomId, text); + } + + public isUserIdFromTheSameHomeserver(externalUserId: string, domain: string): boolean { + const userDomain = externalUserId.includes(':') ? externalUserId.split(':').pop() : ''; + + return userDomain === domain; + } + + public getInstance(): IFederationBridge { + return this; + } + + private parametersForDirectMessagesIfNecessary = (roomType: RoomType, invitedUserId: string): Record => { + return roomType === RoomType.DIRECT_MESSAGE + ? { + // eslint-disable-next-line @typescript-eslint/camelcase + is_direct: true, + invite: [invitedUserId], + } + : {}; + }; + + private logInfo(): void { + bridgeLogger.info(`Running Federation V2: + id: ${this.appServiceId} + bridgeUrl: ${this.bridgeUrl} + homeserverURL: ${this.homeServerUrl} + homeserverDomain: ${this.homeServerDomain} + `); + } + + private async createInstance(): Promise { + bridgeLogger.info('Performing Dynamic Import of matrix-appservice-bridge'); + + // Dynamic import to prevent Rocket.Chat from loading the module until needed and then handle if that fails + const { Bridge, AppServiceRegistration } = await import('@rocket.chat/forked-matrix-appservice-bridge'); + + this.bridgeInstance = new Bridge({ + homeserverUrl: this.homeServerUrl, + domain: this.homeServerDomain, + registration: AppServiceRegistration.fromObject(this.homeServerRegistrationFile as AppServiceOutput), + disableStores: true, + controller: { + onAliasQuery: (alias, matrixRoomId): void => { + console.log('onAliasQuery', alias, matrixRoomId); + }, + onEvent: async (request /* , context*/): Promise => { + // Get the event + const event = request.getData() as unknown as IMatrixEvent; + this.eventHandler(event); + }, + onLog: async (line, isError): Promise => { + console.log(line, isError); + }, + }, + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts new file mode 100644 index 000000000000..9d845516cd75 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts @@ -0,0 +1,152 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { + FederationRoomChangeJoinRulesDto, + FederationRoomChangeMembershipDto, + FederationRoomChangeNameDto, + FederationRoomChangeTopicDto, + FederationRoomCreateInputDto, + FederationRoomSendInternalMessageDto, +} from '../../../application/input/RoomReceiverDto'; +import { EVENT_ORIGIN } from '../../../domain/IFederationBridge'; +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { AddMemberToRoomMembership } from '../definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom'; +import { RoomJoinRules } from '../definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; +import { MatrixEventType } from '../definitions/MatrixEventType'; + +export class MatrixRoomReceiverConverter { + public static toRoomCreateDto(externalEvent: IMatrixEvent): FederationRoomCreateInputDto { + return Object.assign(new FederationRoomCreateInputDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + ...MatrixRoomReceiverConverter.tryToGetExternalInfoFromTheRoomState( + externalEvent.invite_room_state || externalEvent.unsigned?.invite_room_state, + ), + externalInviterId: externalEvent.sender, + normalizedInviterId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.sender), + wasInternallyProgramaticallyCreated: externalEvent.content?.was_internally_programatically_created || false, + }); + } + + public static toChangeRoomMembershipDto( + externalEvent: IMatrixEvent, + ): FederationRoomChangeMembershipDto { + return Object.assign(new FederationRoomChangeMembershipDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + ...MatrixRoomReceiverConverter.tryToGetExternalInfoFromTheRoomState( + externalEvent.invite_room_state || externalEvent.unsigned?.invite_room_state, + externalEvent.content?.is_direct, + ), + externalInviterId: externalEvent.sender, + normalizedInviterId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.sender), + externalInviteeId: externalEvent.state_key, + normalizedInviteeId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.state_key), + inviteeUsernameOnly: MatrixRoomReceiverConverter.formatMatrixUserIdToRCUsernameFormat(externalEvent.state_key), + inviterUsernameOnly: MatrixRoomReceiverConverter.formatMatrixUserIdToRCUsernameFormat(externalEvent.sender), + eventOrigin: MatrixRoomReceiverConverter.getEventOrigin(externalEvent.sender, externalEvent.state_key), + leave: externalEvent.content?.membership === AddMemberToRoomMembership.LEAVE, + }); + } + + public static toSendRoomMessageDto(externalEvent: IMatrixEvent): FederationRoomSendInternalMessageDto { + return Object.assign(new FederationRoomSendInternalMessageDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + externalSenderId: externalEvent.sender, + normalizedSenderId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.sender), + text: externalEvent.content?.body, + }); + } + + public static toRoomChangeJoinRulesDto( + externalEvent: IMatrixEvent, + ): FederationRoomChangeJoinRulesDto { + return Object.assign(new FederationRoomChangeJoinRulesDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + roomType: MatrixRoomReceiverConverter.convertMatrixJoinRuleToRCRoomType(externalEvent.content?.join_rule), + }); + } + + public static toRoomChangeNameDto(externalEvent: IMatrixEvent): FederationRoomChangeNameDto { + return Object.assign(new FederationRoomChangeNameDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + normalizedRoomName: MatrixRoomReceiverConverter.normalizeRoomNameToRCFormat(externalEvent.content?.name), + }); + } + + public static toRoomChangeTopicDto(externalEvent: IMatrixEvent): FederationRoomChangeTopicDto { + return Object.assign(new FederationRoomChangeTopicDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + roomTopic: externalEvent.content?.topic, + }); + } + + private static convertMatrixUserIdFormatToRCFormat(matrixUserId = ''): string { + return matrixUserId.replace('@', ''); + } + + private static convertMatrixRoomIdFormatToRCFormat(matrixRoomId = ''): string { + const prefixedRoomIdOnly = matrixRoomId.split(':')[0]; + const prefix = '!'; + + return prefixedRoomIdOnly?.replace(prefix, ''); + } + + private static normalizeRoomNameToRCFormat(matrixRoomName = ''): string { + return matrixRoomName.replace('@', ''); + } + + private static formatMatrixUserIdToRCUsernameFormat(matrixUserId = ''): string { + return matrixUserId.split(':')[0]?.replace('@', ''); + } + + private static getEventOrigin(inviterId = '', inviteeId = ''): EVENT_ORIGIN { + const fromADifferentServer = + MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviterId) !== + MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviteeId); + + return fromADifferentServer ? EVENT_ORIGIN.REMOTE : EVENT_ORIGIN.LOCAL; + } + + private static extractServerNameFromMatrixUserId(matrixUserId = ''): string { + const splitted = matrixUserId.split(':'); + + return splitted.length > 1 ? splitted[1] : ''; + } + + private static getBasicRoomsFields(externalRoomId: string): Record { + return { + externalRoomId, + normalizedRoomId: MatrixRoomReceiverConverter.convertMatrixRoomIdFormatToRCFormat(externalRoomId), + }; + } + + private static convertMatrixJoinRuleToRCRoomType(matrixJoinRule: RoomJoinRules, matrixRoomIsDirect = false): RoomType { + const mapping: Record = { + [RoomJoinRules.JOIN]: RoomType.CHANNEL, + [RoomJoinRules.INVITE]: RoomType.PRIVATE_GROUP, + }; + const roomType = mapping[matrixJoinRule] || RoomType.CHANNEL; + + return roomType === RoomType.PRIVATE_GROUP && matrixRoomIsDirect ? RoomType.DIRECT_MESSAGE : roomType; + } + + private static tryToGetExternalInfoFromTheRoomState( + roomState: Record[] = [], + matrixRoomIsDirect = false, + ): Record { + if (roomState.length === 0) { + return {}; + } + const externalRoomName = roomState.find((stateEvent: Record) => stateEvent.type === MatrixEventType.ROOM_NAME_CHANGED) + ?.content?.name; + const externalRoomJoinRule = roomState.find( + (stateEvent: Record) => stateEvent.type === MatrixEventType.ROOM_JOIN_RULES_CHANGED, + )?.content?.join_rule; + + return { + ...(externalRoomName ? { externalRoomName } : {}), + ...(externalRoomJoinRule + ? { roomType: MatrixRoomReceiverConverter.convertMatrixJoinRuleToRCRoomType(externalRoomJoinRule, matrixRoomIsDirect) } + : {}), + }; + } +} diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEvent.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEvent.ts similarity index 84% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEvent.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEvent.ts index 7111057ec55e..3470ab6481bc 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEvent.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEvent.ts @@ -11,6 +11,6 @@ export interface IMatrixEvent { sender: string; state_key: string; type: T; - unsigned: { age: number }; + unsigned: { age: number; invite_room_state: Record[] }; user_id: string; } diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts similarity index 64% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts index 6180c0356200..f9e80f615808 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts @@ -1,5 +1,5 @@ export interface IMatrixEventContentCreateRoom { creator: string; room_version: string; - was_programatically_created?: boolean; + was_internally_programatically_created?: boolean; } diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts similarity index 61% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts index 920f9bb53777..f97fa09d8aa7 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts @@ -1,8 +1,8 @@ -export enum SetRoomJoinRules { +export enum RoomJoinRules { JOIN = 'public', INVITE = 'invite', } export interface IMatrixEventContentSetRoomJoinRules { - join_rule: SetRoomJoinRules; + join_rule: RoomJoinRules; } diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/index.ts similarity index 57% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/index.ts index 7615779282e1..3c1d5b52f076 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/index.ts @@ -7,10 +7,10 @@ import { IMatrixEventContentSetRoomName } from './IMatrixEventContentSetRoomName import { IMatrixEventContentSetRoomTopic } from './IMatrixEventContentSetRoomTopic'; export type EventContent = { - [MatrixEventType.CREATE_ROOM]: IMatrixEventContentCreateRoom; - [MatrixEventType.ROOM_MEMBERSHIP]: IMatrixEventContentAddMemberToRoom; - [MatrixEventType.SET_ROOM_JOIN_RULES]: IMatrixEventContentSetRoomJoinRules; - [MatrixEventType.SET_ROOM_NAME]: IMatrixEventContentSetRoomName; - [MatrixEventType.SET_ROOM_TOPIC]: IMatrixEventContentSetRoomTopic; - [MatrixEventType.SEND_MESSAGE]: IMatrixEventContentSendMessage; + [MatrixEventType.ROOM_CREATED]: IMatrixEventContentCreateRoom; + [MatrixEventType.ROOM_MEMBERSHIP_CHANGED]: IMatrixEventContentAddMemberToRoom; + [MatrixEventType.ROOM_JOIN_RULES_CHANGED]: IMatrixEventContentSetRoomJoinRules; + [MatrixEventType.ROOM_NAME_CHANGED]: IMatrixEventContentSetRoomName; + [MatrixEventType.ROOM_TOPIC_CHANGED]: IMatrixEventContentSetRoomTopic; + [MatrixEventType.ROOM_MESSAGE_SENT]: IMatrixEventContentSendMessage; }; diff --git a/apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts similarity index 51% rename from apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts index 14d4f0bb0ecb..bb58a0d71825 100644 --- a/apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts @@ -1,12 +1,12 @@ export enum MatrixEventType { - CREATE_ROOM = 'm.room.create', - ROOM_MEMBERSHIP = 'm.room.member', + ROOM_CREATED = 'm.room.create', + ROOM_MEMBERSHIP_CHANGED = 'm.room.member', // SET_ROOM_POWER_LEVELS = 'm.room.power_levels', // SET_ROOM_CANONICAL_ALIAS = 'm.room.canonical_alias', - SET_ROOM_JOIN_RULES = 'm.room.join_rules', + ROOM_JOIN_RULES_CHANGED = 'm.room.join_rules', // SET_ROOM_HISTORY_VISIBILITY = 'm.room.history_visibility', // SET_ROOM_GUEST_ACCESS = 'm.room.guest_access', - SET_ROOM_NAME = 'm.room.name', - SET_ROOM_TOPIC = 'm.room.topic', - SEND_MESSAGE = 'm.room.message', + ROOM_NAME_CHANGED = 'm.room.name', + ROOM_TOPIC_CHANGED = 'm.room.topic', + ROOM_MESSAGE_SENT = 'm.room.message', } diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent.ts new file mode 100644 index 000000000000..25e179bfae7e --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent.ts @@ -0,0 +1,16 @@ +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { MatrixEventType } from '../definitions/MatrixEventType'; + +export abstract class MatrixBaseEventHandler { + private type: T; + + public abstract handle(externalEvent: IMatrixEvent): Promise; + + protected constructor(type: T) { + this.type = type; + } + + public equals(type: MatrixEventType): boolean { + return this.type === type; + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts new file mode 100644 index 000000000000..47d10be8fb73 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts @@ -0,0 +1,65 @@ +import { FederationRoomServiceReceiver } from '../../../application/RoomServiceReceiver'; +import { MatrixRoomReceiverConverter } from '../converters/RoomReceiver'; +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { MatrixEventType } from '../definitions/MatrixEventType'; +import { MatrixBaseEventHandler } from './BaseEvent'; + +export class MatrixRoomCreatedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_CREATED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.createRoom(MatrixRoomReceiverConverter.toRoomCreateDto(externalEvent)); + } +} + +export class MatrixRoomMembershipChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_MEMBERSHIP_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeRoomMembership(MatrixRoomReceiverConverter.toChangeRoomMembershipDto(externalEvent)); + } +} + +export class MatrixRoomJoinRulesChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_JOIN_RULES_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeJoinRules(MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto(externalEvent)); + } +} + +export class MatrixRoomNameChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_NAME_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeRoomName(MatrixRoomReceiverConverter.toRoomChangeNameDto(externalEvent)); + } +} + +export class MatrixRoomTopicChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_TOPIC_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeRoomTopic(MatrixRoomReceiverConverter.toRoomChangeTopicDto(externalEvent)); + } +} + +export class MatrixRoomMessageSentHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_MESSAGE_SENT); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.receiveExternalMessage(MatrixRoomReceiverConverter.toSendRoomMessageDto(externalEvent)); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts new file mode 100644 index 000000000000..67b361681492 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts @@ -0,0 +1,16 @@ +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { MatrixEventType } from '../definitions/MatrixEventType'; +import { MatrixBaseEventHandler } from './BaseEvent'; + +export class MatrixEventsHandler { + // eslint-disable-next-line no-empty-function + constructor(private handlers: MatrixBaseEventHandler[]) {} + + public async handleEvent(event: IMatrixEvent): Promise { + const handler = this.handlers.find((handler) => handler.equals(event.type)); + if (!handler) { + return console.log(`Could not find handler for ${event.type}`, event); + } + return handler?.handle(event); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/queue/InMemoryQueue.ts b/apps/meteor/app/federation-v2/server/infrastructure/queue/InMemoryQueue.ts new file mode 100644 index 000000000000..fc4ea1106d26 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/queue/InMemoryQueue.ts @@ -0,0 +1,16 @@ +import * as fastq from 'fastq'; + +export class InMemoryQueue { + private instance: any; + + public setHandler(handler: Function, concurrency: number): void { + this.instance = fastq.promise(handler as any, concurrency); + } + + public addToQueue(task: Record): void { + if (!this.instance) { + throw new Error('You need to set the handler first'); + } + this.instance.push(task).catch(console.error); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts new file mode 100644 index 000000000000..3f82f77a6a19 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts @@ -0,0 +1,9 @@ +import { sendMessage } from '../../../../../lib/server'; +import { FederatedRoom } from '../../../domain/FederatedRoom'; +import { FederatedUser } from '../../../domain/FederatedUser'; + +export class RocketChatMessageAdapter { + public async sendMessage(user: FederatedUser, text: string, room: FederatedRoom): Promise { + new Promise((resolve) => resolve(sendMessage(user.internalReference, { msg: text }, room.internalReference))); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Notification.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Notification.ts new file mode 100644 index 000000000000..8e07d94cf12a --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Notification.ts @@ -0,0 +1,14 @@ +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; + +import { api } from '../../../../../../server/sdk/api'; + +export class RocketChatNotificationAdapter { + public notifyWithEphemeralMessage(i18nMessageKey: string, userId: string, roomId: string, language = 'en'): void { + api.broadcast('notify.ephemeralMessage', userId, roomId, { + msg: TAPi18n.__(i18nMessageKey, { + postProcess: 'sprintf', + lng: language, + }), + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts new file mode 100644 index 000000000000..44ec3ee5e5ad --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts @@ -0,0 +1,103 @@ +import { ICreatedRoom, IRoom } from '@rocket.chat/core-typings'; + +import { MatrixBridgedRoom } from '../../../../../models/server'; +import { FederatedRoom } from '../../../domain/FederatedRoom'; +import { createRoom, addUserToRoom, removeUserFromRoom } from '../../../../../lib/server'; +import { Rooms, Subscriptions } from '../../../../../models/server/raw'; +import { FederatedUser } from '../../../domain/FederatedUser'; + +export class RocketChatRoomAdapter { + public async getFederatedRoomByExternalId(externalRoomId: string): Promise { + const internalBridgedRoomId = MatrixBridgedRoom.getId(externalRoomId); + if (!internalBridgedRoomId) { + return; + } + const room = await Rooms.findOneById(internalBridgedRoomId); + + return this.createFederatedRoomInstance(externalRoomId, room); + } + + public async getFederatedRoomByInternalId(internalRoomId: string): Promise { + const externalRoomId = MatrixBridgedRoom.getMatrixId(internalRoomId); + if (!externalRoomId) { + return; + } + const room = await Rooms.findOneById(internalRoomId); + + return this.createFederatedRoomInstance(externalRoomId, room); + } + + public async getInternalRoomById(internalRoomId: string): Promise { + return Rooms.findOneById(internalRoomId); + } + + public async createFederatedRoom(federatedRoom: FederatedRoom): Promise { + const members = federatedRoom.getMembers(); + const { rid, _id } = createRoom( + federatedRoom.internalReference.t, + federatedRoom.internalReference.name, + federatedRoom.internalReference.u.username as string, + members, + ) as ICreatedRoom; + const roomId = rid || _id; + MatrixBridgedRoom.insert({ rid: roomId, mri: federatedRoom.externalId }); + await Rooms.setAsFederated(roomId); + } + + public async updateFederatedRoomByInternalRoomId(internalRoomId: string, federatedRoom: FederatedRoom): Promise { + MatrixBridgedRoom.upsert({ rid: internalRoomId }, { rid: internalRoomId, mri: federatedRoom.externalId }); + await Rooms.setAsFederated(internalRoomId); + } + + public async addUserToRoom(federatedRoom: FederatedRoom, inviteeUser: FederatedUser, inviterUser?: FederatedUser): Promise { + return new Promise((resolve) => + resolve(addUserToRoom(federatedRoom.internalReference._id, inviteeUser.internalReference, inviterUser?.internalReference) as any), + ); + } + + public async removeUserFromRoom(federatedRoom: FederatedRoom, affectedUser: FederatedUser, byUser: FederatedUser): Promise { + return new Promise((resolve) => + resolve( + removeUserFromRoom(federatedRoom.internalReference._id, affectedUser.internalReference, { + byUser: byUser.internalReference, + }) as any, + ), + ); + } + + public async updateRoomType(federatedRoom: FederatedRoom): Promise { + await Rooms.update({ _id: federatedRoom.internalReference._id }, { $set: { t: federatedRoom.internalReference.t } }); + await Subscriptions.update( + { rid: federatedRoom.internalReference._id }, + { $set: { t: federatedRoom.internalReference.t } }, + { multi: true }, + ); + } + + public async updateRoomName(federatedRoom: FederatedRoom): Promise { + await Rooms.update( + { _id: federatedRoom.internalReference._id }, + { $set: { name: federatedRoom.internalReference.name, fname: federatedRoom.internalReference.fname } }, + ); + await Subscriptions.update( + { rid: federatedRoom.internalReference._id }, + { $set: { name: federatedRoom.internalReference.name, fname: federatedRoom.internalReference.fname } }, + { multi: true }, + ); + } + + public async updateRoomTopic(federatedRoom: FederatedRoom): Promise { + await Rooms.update( + { _id: federatedRoom.internalReference._id }, + { $set: { description: federatedRoom.internalReference.description } }, + ); + } + + private createFederatedRoomInstance(externalRoomId: string, room: IRoom): FederatedRoom { + const federatedRoom = FederatedRoom.build(); + federatedRoom.externalId = externalRoomId; + federatedRoom.internalReference = room; + + return federatedRoom; + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings.ts new file mode 100644 index 000000000000..71ff9af73461 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings.ts @@ -0,0 +1,192 @@ +import yaml from 'js-yaml'; +import { SHA256 } from 'meteor/sha'; + +import { Settings } from '../../../../../models/server/raw'; +import { settings, settingsRegistry } from '../../../../../settings/server'; + +const EVERYTHING_REGEX = '.*'; +const LISTEN_RULES = EVERYTHING_REGEX; + +export class RocketChatSettingsAdapter { + public initialize(): void { + this.addFederationSettings(); + this.watchChangesAndUpdateRegistrationFile(); + } + + public getApplicationServiceId(): string { + return settings.get('Federation_Matrix_id'); + } + + public getApplicationHomeServerToken(): string { + return settings.get('Federation_Matrix_hs_token'); + } + + public getApplicationApplicationServiceToken(): string { + return settings.get('Federation_Matrix_as_token'); + } + + public getBridgeUrl(): string { + return settings.get('Federation_Matrix_bridge_url'); + } + + public getBridgePort(): number { + const [, , port] = this.getBridgeUrl().split(':'); + + return parseInt(port); + } + + public getHomeServerUrl(): string { + return settings.get('Federation_Matrix_homeserver_url'); + } + + public getHomeServerDomain(): string { + return settings.get('Federation_Matrix_homeserver_domain'); + } + + public getBridgeBotUsername(): string { + return settings.get('Federation_Matrix_bridge_localpart'); + } + + public async disableFederation(): Promise { + await Settings.updateValueById('Federation_Matrix_enabled', false); + } + + public onFederationEnabledStatusChanged(callback: Function): void { + settings.watchMultiple( + [ + 'Federation_Matrix_enabled', + 'Federation_Matrix_id', + 'Federation_Matrix_hs_token', + 'Federation_Matrix_as_token', + 'Federation_Matrix_homeserver_url', + 'Federation_Matrix_homeserver_domain', + 'Federation_Matrix_bridge_url', + 'Federation_Matrix_bridge_localpart', + ], + ([enabled]) => callback(enabled), + ); + } + + public generateRegistrationFileObject(): Record { + /* eslint-disable @typescript-eslint/camelcase */ + return { + id: this.getApplicationServiceId(), + hs_token: this.getApplicationHomeServerToken(), + as_token: this.getApplicationApplicationServiceToken(), + url: this.getBridgeUrl(), + sender_localpart: this.getBridgeBotUsername(), + namespaces: { + users: [ + { + exclusive: false, + regex: LISTEN_RULES, + }, + ], + rooms: [ + { + exclusive: false, + regex: LISTEN_RULES, + }, + ], + aliases: [ + { + exclusive: false, + regex: LISTEN_RULES, + }, + ], + }, + }; + /* eslint-enable @typescript-eslint/camelcase */ + } + + private async updateRegistrationFile(): Promise { + await Settings.updateValueById('Federation_Matrix_registration_file', yaml.dump(this.generateRegistrationFileObject())); + } + + private watchChangesAndUpdateRegistrationFile(): void { + settings.watchMultiple( + [ + 'Federation_Matrix_id', + 'Federation_Matrix_hs_token', + 'Federation_Matrix_as_token', + 'Federation_Matrix_homeserver_url', + 'Federation_Matrix_homeserver_domain', + 'Federation_Matrix_bridge_url', + 'Federation_Matrix_bridge_localpart', + ], + this.updateRegistrationFile.bind(this), + ); + } + + private addFederationSettings(): void { + settingsRegistry.addGroup('Federation', function () { + this.section('Matrix Bridge', function () { + this.add('Federation_Matrix_enabled', false, { + readonly: false, + type: 'boolean', + i18nLabel: 'Federation_Matrix_enabled', + i18nDescription: 'Federation_Matrix_enabled_desc', + alert: 'Federation_Matrix_Enabled_Alert', + }); + + const uniqueId = settings.get('uniqueID'); + const hsToken = SHA256(`hs_${uniqueId}`); + const asToken = SHA256(`as_${uniqueId}`); + + this.add('Federation_Matrix_id', `rocketchat_${uniqueId}`, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_id', + i18nDescription: 'Federation_Matrix_id_desc', + }); + + this.add('Federation_Matrix_hs_token', hsToken, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_hs_token', + i18nDescription: 'Federation_Matrix_hs_token_desc', + }); + + this.add('Federation_Matrix_as_token', asToken, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_as_token', + i18nDescription: 'Federation_Matrix_as_token_desc', + }); + + this.add('Federation_Matrix_homeserver_url', 'http://localhost:8008', { + type: 'string', + i18nLabel: 'Federation_Matrix_homeserver_url', + i18nDescription: 'Federation_Matrix_homeserver_url_desc', + alert: 'Federation_Matrix_homeserver_url_alert', + }); + + this.add('Federation_Matrix_homeserver_domain', 'local.rocket.chat', { + type: 'string', + i18nLabel: 'Federation_Matrix_homeserver_domain', + i18nDescription: 'Federation_Matrix_homeserver_domain_desc', + alert: 'Federation_Matrix_homeserver_domain_alert', + }); + + this.add('Federation_Matrix_bridge_url', 'http://host.docker.internal:3300', { + type: 'string', + i18nLabel: 'Federation_Matrix_bridge_url', + i18nDescription: 'Federation_Matrix_bridge_url_desc', + }); + + this.add('Federation_Matrix_bridge_localpart', 'rocket.cat', { + type: 'string', + i18nLabel: 'Federation_Matrix_bridge_localpart', + i18nDescription: 'Federation_Matrix_bridge_localpart_desc', + }); + + this.add('Federation_Matrix_registration_file', '', { + readonly: true, + type: 'code', + i18nLabel: 'Federation_Matrix_registration_file', + i18nDescription: 'Federation_Matrix_registration_file_desc', + }); + }); + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts new file mode 100644 index 000000000000..a2f2c26f8e6c --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts @@ -0,0 +1,85 @@ +import { IUser } from '@rocket.chat/core-typings'; + +import { MatrixBridgedUser, Users } from '../../../../../models/server'; +import { FederatedUser } from '../../../domain/FederatedUser'; + +export class RocketChatUserAdapter { + public async getFederatedUserByExternalId(externalUserId: string): Promise { + const internalBridgedUserId = MatrixBridgedUser.getId(externalUserId); + if (!internalBridgedUserId) { + return; + } + + const user = await Users.findOneById(internalBridgedUserId); + + return this.createFederatedUserInstance(externalUserId, user); + } + + public async getFederatedUserByInternalId(internalUserId: string): Promise { + const internalBridgedUserId = MatrixBridgedUser.getById(internalUserId); + if (!internalBridgedUserId) { + return; + } + const { uid: userId, mui: externalUserId } = internalBridgedUserId; + const user = await Users.findOneById(userId); + + return this.createFederatedUserInstance(externalUserId, user); + } + + public async getFederatedUserByInternalUsername(username: string): Promise { + const user = await Users.findOneByUsername(username); + if (!user) { + return; + } + const internalBridgedUserId = MatrixBridgedUser.getById(user._id); + if (!internalBridgedUserId) { + return; + } + const { mui: externalUserId } = internalBridgedUserId; + + return this.createFederatedUserInstance(externalUserId, user); + } + + public async getInternalUserById(userId: string): Promise { + return Users.findOneById(userId); + } + + public async createFederatedUser(federatedUser: FederatedUser): Promise { + const existingLocalUser = await Users.findOneByUsername(federatedUser.internalReference.username); + if (existingLocalUser) { + return MatrixBridgedUser.upsert( + { uid: existingLocalUser._id }, + { + uid: existingLocalUser._id, + mui: federatedUser.externalId, + remote: !federatedUser.existsOnlyOnProxyServer, + }, + ); + } + const newLocalUserId = await Users.create({ + username: federatedUser.internalReference.username, + type: federatedUser.internalReference.type, + status: federatedUser.internalReference.status, + active: federatedUser.internalReference.active, + roles: federatedUser.internalReference.roles, + name: federatedUser.internalReference.name, + requirePasswordChange: federatedUser.internalReference.requirePasswordChange, + }); + MatrixBridgedUser.upsert( + { uid: newLocalUserId }, + { + uid: newLocalUserId, + mui: federatedUser.externalId, + remote: !federatedUser.existsOnlyOnProxyServer, + }, + ); + } + + private createFederatedUserInstance(externalUserId: string, user: IUser): FederatedUser { + const federatedUser = FederatedUser.build(); + federatedUser.externalId = externalUserId; + federatedUser.internalReference = user; + + return federatedUser; + } +} diff --git a/apps/meteor/app/federation-v2/server/logger.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts similarity index 73% rename from apps/meteor/app/federation-v2/server/logger.ts rename to apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts index 0b88f48bfde6..331ed9f5f4b4 100644 --- a/apps/meteor/app/federation-v2/server/logger.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts @@ -1,4 +1,4 @@ -import { Logger } from '../../logger/server'; +import { Logger } from '../../../../../logger/server'; const logger = new Logger('Federation_Matrix'); diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender.ts new file mode 100644 index 000000000000..76d6937f206b --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender.ts @@ -0,0 +1,34 @@ +import { IMessage } from '@rocket.chat/core-typings'; + +import { FederationRoomInviteUserDto, FederationRoomSendExternalMessageDto } from '../../../application/input/RoomSenderDto'; + +export class FederationRoomSenderConverter { + public static toRoomInviteUserDto( + internalInviterId: string, + internalRoomId: string, + externalInviteeId: string, + ): FederationRoomInviteUserDto { + const normalizedInviteeId = externalInviteeId.replace('@', ''); + const inviteeUsernameOnly = externalInviteeId.split(':')[0]?.replace('@', ''); + + return Object.assign(new FederationRoomInviteUserDto(), { + internalInviterId, + internalRoomId, + rawInviteeId: externalInviteeId, + normalizedInviteeId, + inviteeUsernameOnly, + }); + } + + public static toSendExternalMessageDto( + internalSenderId: string, + internalRoomId: string, + message: IMessage, + ): FederationRoomSendExternalMessageDto { + return Object.assign(new FederationRoomSendExternalMessageDto(), { + internalRoomId, + internalSenderId, + message, + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/matrix-client/index.ts b/apps/meteor/app/federation-v2/server/matrix-client/index.ts deleted file mode 100644 index 68664d6e9cdf..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as message from './message'; -import * as room from './room'; -import * as user from './user'; - -export const matrixClient = { - message, - room, - user, -}; diff --git a/apps/meteor/app/federation-v2/server/matrix-client/message.ts b/apps/meteor/app/federation-v2/server/matrix-client/message.ts deleted file mode 100644 index a6a9d8626632..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/message.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IMessage } from '@rocket.chat/core-typings'; - -import { MatrixBridgedRoom, MatrixBridgedUser } from '../../../models/server'; -import { matrixBridge } from '../bridge'; - -export const send = async (message: IMessage): Promise => { - // Retrieve the matrix user - const userMatrixId = MatrixBridgedUser.getMatrixId(message.u._id); - - // Retrieve the matrix room - const roomMatrixId = MatrixBridgedRoom.getMatrixId(message.rid); - - if (!userMatrixId) { - throw new Error(`Could not find user matrix id for ${message.u._id}`); - } - - if (!roomMatrixId) { - throw new Error(`Could not find room matrix id for ${message.rid}`); - } - - const intent = matrixBridge.getInstance().getIntent(userMatrixId); - await intent.sendText(roomMatrixId, message.msg || '...not-supported...'); - - return message; -}; diff --git a/apps/meteor/app/federation-v2/server/matrix-client/room.ts b/apps/meteor/app/federation-v2/server/matrix-client/room.ts deleted file mode 100644 index e031de4e4b2b..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/room.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import { IRoom, IUser } from '@rocket.chat/core-typings'; - -import { MatrixBridgedRoom, MatrixBridgedUser } from '../../../models/server'; -import { matrixBridge } from '../bridge'; -import { Rooms } from '../../../models/server/raw'; - -interface ICreateRoomResult { - rid: string; - mri: string; -} - -const parametersForDirectMessagesIfNecessary = (room: IRoom, invitedUserId: string): Record => { - return room.t === RoomType.DIRECT_MESSAGE - ? { - // eslint-disable-next-line @typescript-eslint/camelcase - is_direct: true, - invite: [invitedUserId], - } - : {}; -}; - -export const create = async (inviterUser: IUser, room: IRoom, invitedUserId: string): Promise => { - // Check if this room already exists (created by another method) - // and if so, ignore the callback - const roomMatrixId = MatrixBridgedRoom.getMatrixId(room._id); - if (roomMatrixId) { - return { rid: room._id, mri: roomMatrixId }; - } - - // Retrieve the matrix user - const userMatrixId = MatrixBridgedUser.getMatrixId(inviterUser._id); - - if (!userMatrixId) { - throw new Error(`Could not find user matrix id for ${inviterUser._id}`); - } - - const intent = matrixBridge.getInstance().getIntent(userMatrixId); - - const visibility = room.t === 'p' || room.t === 'd' ? 'invite' : 'public'; - const preset = room.t === 'p' || room.t === 'd' ? 'private_chat' : 'public_chat'; - - // Create the matrix room - const matrixRoom = await intent.createRoom({ - createAsClient: true, - options: { - name: room.fname || room.name, - topic: room.topic, - visibility, - preset, - ...parametersForDirectMessagesIfNecessary(room, invitedUserId), - // eslint-disable-next-line @typescript-eslint/camelcase - creation_content: { - // eslint-disable-next-line @typescript-eslint/camelcase - was_programatically_created: true, - }, - }, - }); - // Add to the map - MatrixBridgedRoom.insert({ rid: room._id, mri: matrixRoom.room_id }); - - await Rooms.setAsBridged(room._id); - - // Add our user TODO: Doing this I think is un-needed since our user is the creator of the room. With it in.. there were errors - // await intent.invite(matrixRoom.room_id, userMatrixId); - - return { rid: room._id, mri: matrixRoom.room_id }; -}; diff --git a/apps/meteor/app/federation-v2/server/matrix-client/user.ts b/apps/meteor/app/federation-v2/server/matrix-client/user.ts deleted file mode 100644 index 9e6ba092e9b4..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/user.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { MatrixProfileInfo } from '@rocket.chat/forked-matrix-bot-sdk'; -import { IUser } from '@rocket.chat/core-typings'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { matrixBridge } from '../bridge'; -import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '../../../models/server'; -import { addUserToRoom } from '../../../lib/server/functions'; -import { matrixClient } from '.'; -import { dataInterface } from '../data-interface'; -import { settings } from '../../../settings/server'; -import { api } from '../../../../server/sdk/api'; - -interface ICreateUserResult { - uid: string; - mui: string; - remote: boolean; -} - -const removeUselessCharsFromMatrixId = (matrixUserId = ''): string => matrixUserId.replace('@', ''); -const formatUserIdAsRCUsername = (userId = ''): string => removeUselessCharsFromMatrixId(userId.split(':')[0]); - -export const invite = async (inviterId: string, roomId: string, invitedId: string): Promise => { - console.log(`[${inviterId}-${invitedId}-${roomId}] Inviting user ${invitedId} to ${roomId}...`); - - // Find the inviter user - let bridgedInviterUser = MatrixBridgedUser.getById(inviterId); - // Get the user - const inviterUser = await dataInterface.user(inviterId); - - // Determine if the user is local or remote - let invitedUserMatrixId = invitedId; - const invitedUserDomain = invitedId.includes(':') ? invitedId.split(':').pop() : ''; - const invitedUserIsRemote = invitedUserDomain && invitedUserDomain !== settings.get('Federation_Matrix_homeserver_domain'); - - // Find the invited user in Rocket.Chats users - // TODO: this should be refactored asap, since these variable value changes lead us to confusion - let invitedUser = Users.findOneByUsername(removeUselessCharsFromMatrixId(invitedId)); - - if (!invitedUser) { - // Create the invited user - const { uid } = await matrixClient.user.createLocal(invitedUserMatrixId); - invitedUser = Users.findOneById(uid); - } - - // The inviters user doesn't yet exist in matrix - if (!bridgedInviterUser) { - console.log(`[${inviterId}-${invitedId}-${roomId}] Creating remote inviter user...`); - - // Create the missing user - bridgedInviterUser = await matrixClient.user.createRemote(inviterUser); - - console.log(`[${inviterId}-${invitedId}-${roomId}] Inviter user created as ${bridgedInviterUser.mui}...`); - } - - // Find the bridged room id - let matrixRoomId = await MatrixBridgedRoom.getMatrixId(roomId); - - // Get the room - const room = await dataInterface.room(roomId); - - if (!matrixRoomId) { - console.log(`[${inviterId}-${invitedId}-${roomId}] Creating remote room...`); - - // Create the missing room - const { mri } = await matrixClient.room.create({ _id: inviterId } as IUser, room, invitedId); - - matrixRoomId = mri; - - console.log(`[${inviterId}-${invitedId}-${roomId}] Remote room created as ${matrixRoomId}...`); - } - - // If the invited user is not remote, let's ensure it exists remotely - if (!invitedUserIsRemote) { - console.log(`[${inviterId}-${invitedId}-${roomId}] Creating remote invited user...`); - - // Check if we already have a matrix id for that user - const existingMatrixId = MatrixBridgedUser.getMatrixId(invitedUser._id); - - if (!existingMatrixId) { - const { mui } = await matrixClient.user.createRemote(invitedUser); - - invitedUserMatrixId = mui; - } else { - invitedUserMatrixId = existingMatrixId; - } - - console.log(`[${inviterId}-${invitedId}-${roomId}] Invited user created as ${invitedUserMatrixId}...`); - } - - console.log(`[${inviterId}-${invitedId}-${roomId}] Inviting the user to the room...`); - // Invite && Auto-join if the user is Rocket.Chat controlled - if (!invitedUserIsRemote) { - // Invite the user to the room - await matrixBridge.getInstance().getIntent(bridgedInviterUser.mui).invite(matrixRoomId, invitedUserMatrixId); - - console.log(`[${inviterId}-${invitedId}-${roomId}] Auto-join room...`); - - await matrixBridge.getInstance().getIntent(invitedUserMatrixId).join(matrixRoomId); - } else if (room.t !== 'd') { - // Invite the user to the room but don't wait as this is dependent on the user accepting the invite because we don't control this user - matrixBridge - .getInstance() - .getIntent(bridgedInviterUser.mui) - .invite(matrixRoomId, invitedUserMatrixId) - .catch(() => { - api.broadcast('notify.ephemeralMessage', inviterId, roomId, { - msg: TAPi18n.__('Federation_Matrix_only_owners_can_invite_users', { - postProcess: 'sprintf', - lng: settings.get('Language') || 'en', - }), - }); - }); - } - - // Add the matrix user to the invited room - addUserToRoom(roomId, invitedUser, inviterUser, false); -}; - -export const createRemote = async (u: IUser): Promise => { - const matrixUserId = `@${u.username?.toLowerCase()}:${settings.get('Federation_Matrix_homeserver_domain')}`; - - console.log(`Creating remote user ${matrixUserId}...`); - - const intent = matrixBridge.getInstance().getIntent(matrixUserId); - - await intent.ensureProfile(u.name); - - await intent.setDisplayName(`${u.username} (${u.name})`); - - const payload = { uid: u._id, mui: matrixUserId, remote: true }; - - MatrixBridgedUser.upsert({ uid: u._id }, payload); - - return payload; -}; - -const createLocalUserIfNotExists = async (userId = '', profileInfo: MatrixProfileInfo = {}): Promise => { - const existingUser = await Users.findOneByUsername(formatUserIdAsRCUsername(userId)); - - if (existingUser) { - return existingUser._id; - } - - return Users.create({ - username: removeUselessCharsFromMatrixId(userId), - type: 'user', - status: 'online', - active: true, - roles: ['user'], - name: profileInfo.displayname, - requirePasswordChange: false, - }); -}; - -export const createLocal = async (matrixUserId: string): Promise => { - console.log(`Creating local user ${matrixUserId}...`); - - const intent = matrixBridge.getInstance().getIntent(matrixUserId); - - let currentProfile: MatrixProfileInfo = {}; - - try { - currentProfile = await intent.getProfileInfo(matrixUserId); - } catch (err) { - // no-op - } - - const uid = await createLocalUserIfNotExists(matrixUserId, currentProfile); - const payload = { uid, mui: matrixUserId, remote: false }; - - MatrixBridgedUser.upsert({ uid }, payload); - - return payload; -}; diff --git a/apps/meteor/app/federation-v2/server/methods/checkBridgedRoomExists.ts b/apps/meteor/app/federation-v2/server/methods/checkBridgedRoomExists.ts deleted file mode 100644 index 7db759e1cc38..000000000000 --- a/apps/meteor/app/federation-v2/server/methods/checkBridgedRoomExists.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MatrixBridgedRoom } from '../../../models/server'; - -export const checkBridgedRoomExists = async (matrixRoomId: string): Promise => { - const existingRoomId = MatrixBridgedRoom.getId(matrixRoomId); - - return !!existingRoomId; -}; diff --git a/apps/meteor/app/federation-v2/server/queue.ts b/apps/meteor/app/federation-v2/server/queue.ts deleted file mode 100644 index f1f0ea02061e..000000000000 --- a/apps/meteor/app/federation-v2/server/queue.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Create the queue -import { queueAsPromised } from 'fastq'; -import * as fastq from 'fastq'; - -import { IMatrixEvent } from './definitions/IMatrixEvent'; -import { MatrixEventType } from './definitions/MatrixEventType'; -import { eventHandler } from './eventHandler'; - -export const matrixEventQueue: queueAsPromised> = fastq.promise(eventHandler, 1); - -export const addToQueue = (event: IMatrixEvent): void => { - console.log(`Queueing ${event.type}...`); - - // TODO: Handle error - matrixEventQueue.push(event).catch((err) => console.error(err)); -}; diff --git a/apps/meteor/app/federation-v2/server/settings.ts b/apps/meteor/app/federation-v2/server/settings.ts deleted file mode 100644 index f10264a3ea37..000000000000 --- a/apps/meteor/app/federation-v2/server/settings.ts +++ /dev/null @@ -1,136 +0,0 @@ -import yaml from 'js-yaml'; -import { SHA256 } from 'meteor/sha'; - -import { getRegistrationInfo } from './config'; -import { Settings } from '../../models/server/raw'; -import { settings, settingsRegistry } from '../../settings/server'; - -settingsRegistry.addGroup('Federation', function () { - this.section('Matrix Bridge', async function () { - this.add('Federation_Matrix_enabled', false, { - readonly: false, - type: 'boolean', - i18nLabel: 'Federation_Matrix_enabled', - i18nDescription: 'Federation_Matrix_enabled_desc', - alert: 'Federation_Matrix_Enabled_Alert', - }); - - const uniqueId = await settings.get('uniqueID'); - const hsToken = SHA256(`hs_${uniqueId}`); - const asToken = SHA256(`as_${uniqueId}`); - - this.add('Federation_Matrix_id', `rocketchat_${uniqueId}`, { - readonly: true, - type: 'string', - i18nLabel: 'Federation_Matrix_id', - i18nDescription: 'Federation_Matrix_id_desc', - }); - - this.add('Federation_Matrix_hs_token', hsToken, { - readonly: true, - type: 'string', - i18nLabel: 'Federation_Matrix_hs_token', - i18nDescription: 'Federation_Matrix_hs_token_desc', - }); - - this.add('Federation_Matrix_as_token', asToken, { - readonly: true, - type: 'string', - i18nLabel: 'Federation_Matrix_as_token', - i18nDescription: 'Federation_Matrix_as_token_desc', - }); - - this.add('Federation_Matrix_homeserver_url', 'http://localhost:8008', { - type: 'string', - i18nLabel: 'Federation_Matrix_homeserver_url', - i18nDescription: 'Federation_Matrix_homeserver_url_desc', - alert: 'Federation_Matrix_homeserver_url_alert', - }); - - this.add('Federation_Matrix_homeserver_domain', 'local.rocket.chat', { - type: 'string', - i18nLabel: 'Federation_Matrix_homeserver_domain', - i18nDescription: 'Federation_Matrix_homeserver_domain_desc', - alert: 'Federation_Matrix_homeserver_domain_alert', - }); - - this.add('Federation_Matrix_bridge_url', 'http://host.docker.internal:3300', { - type: 'string', - i18nLabel: 'Federation_Matrix_bridge_url', - i18nDescription: 'Federation_Matrix_bridge_url_desc', - }); - - this.add('Federation_Matrix_bridge_localpart', 'rocket.cat', { - type: 'string', - i18nLabel: 'Federation_Matrix_bridge_localpart', - i18nDescription: 'Federation_Matrix_bridge_localpart_desc', - }); - - this.add('Federation_Matrix_registration_file', '', { - readonly: true, - type: 'code', - i18nLabel: 'Federation_Matrix_registration_file', - i18nDescription: 'Federation_Matrix_registration_file_desc', - }); - }); -}); - -let registrationFile = {}; - -const updateRegistrationFile = async function (): Promise { - const registrationInfo = getRegistrationInfo(); - - // eslint-disable-next-line @typescript-eslint/camelcase - const { id, hs_token, as_token, sender_localpart } = registrationInfo; - let { url } = registrationInfo; - - if (!url || !url.includes(':')) { - url = `${url}:3300`; - } - - /* eslint-disable @typescript-eslint/camelcase */ - registrationFile = { - id, - hs_token, - as_token, - url, - sender_localpart, - namespaces: { - users: [ - { - exclusive: false, - regex: '.*', - }, - ], - rooms: [ - { - exclusive: false, - regex: '.*', - }, - ], - aliases: [ - { - exclusive: false, - regex: '.*', - }, - ], - }, - }; - /* eslint-enable @typescript-eslint/camelcase */ - - // Update the registration file - await Settings.updateValueById('Federation_Matrix_registration_file', yaml.dump(registrationFile)); -}; - -settings.watchMultiple( - [ - 'Federation_Matrix_id', - 'Federation_Matrix_hs_token', - 'Federation_Matrix_as_token', - 'Federation_Matrix_homeserver_url', - 'Federation_Matrix_homeserver_domain', - 'Federation_Matrix_bridge_url', - 'Federation_Matrix_bridge_localpart', - ], - updateRegistrationFile, -); diff --git a/apps/meteor/app/federation-v2/server/startup.ts b/apps/meteor/app/federation-v2/server/startup.ts deleted file mode 100644 index a1495878788c..000000000000 --- a/apps/meteor/app/federation-v2/server/startup.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { settings } from '../../settings/server'; -import { matrixBridge } from './bridge'; -import { bridgeLogger, setupLogger } from './logger'; - -const watchChanges = (): void => { - settings.watchMultiple( - [ - 'Federation_Matrix_enabled', - 'Federation_Matrix_id', - 'Federation_Matrix_hs_token', - 'Federation_Matrix_as_token', - 'Federation_Matrix_homeserver_url', - 'Federation_Matrix_homeserver_domain', - 'Federation_Matrix_bridge_url', - 'Federation_Matrix_bridge_localpart', - ], - async ([enabled]) => { - setupLogger.info(`Federation Matrix is ${enabled ? 'enabled' : 'disabled'}`); - if (!enabled) { - await matrixBridge.stop(); - return; - } - await matrixBridge.start(); - }, - ); -}; - -export const startBridge = (): void => { - watchChanges(); - - bridgeLogger.info(`Running Federation V2: - id: ${settings.get('Federation_Matrix_id')} - bridgeUrl: ${settings.get('Federation_Matrix_bridge_url')} - homeserverURL: ${settings.get('Federation_Matrix_homeserver_url')} - homeserverDomain: ${settings.get('Federation_Matrix_homeserver_domain')} - `); -}; diff --git a/apps/meteor/app/lib/client/methods/sendMessage.js b/apps/meteor/app/lib/client/methods/sendMessage.js index 8dfafeeb5f94..8d3d9bd9532e 100644 --- a/apps/meteor/app/lib/client/methods/sendMessage.js +++ b/apps/meteor/app/lib/client/methods/sendMessage.js @@ -32,9 +32,9 @@ Meteor.methods({ message.unread = true; } - // If the room is bridged, send the message to matrix only - const { bridged } = Rooms.findOne({ _id: message.rid }, { fields: { bridged: 1 } }); - if (bridged) { + // If the room is federated, send the message to matrix only + const { federated } = Rooms.findOne({ _id: message.rid }, { fields: { federated: 1 } }); + if (federated) { return; } diff --git a/apps/meteor/app/lib/server/methods/sendMessage.js b/apps/meteor/app/lib/server/methods/sendMessage.js index 91e223322b36..3cf002ba4e0b 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.js +++ b/apps/meteor/app/lib/server/methods/sendMessage.js @@ -7,13 +7,14 @@ import { hasPermission } from '../../../authorization'; import { metrics } from '../../../metrics'; import { settings } from '../../../settings'; import { messageProperties } from '../../../ui-utils'; -import { Users, Messages, Rooms } from '../../../models'; +import { Users, Messages } from '../../../models'; import { sendMessage } from '../functions'; import { RateLimiter } from '../lib'; import { canSendMessage } from '../../../authorization/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { api } from '../../../../server/sdk/api'; -import { matrixClient } from '../../../federation-v2/server/matrix-client'; +import { federationRoomServiceSender } from '../../../federation-v2/server'; +import { FederationRoomSenderConverter } from '../../../federation-v2/server/infrastructure/rocket-chat/converters/RoomSender'; export function executeSendMessage(uid, message) { if (message.tshow && !message.tmid) { @@ -106,10 +107,10 @@ Meteor.methods({ } try { - // If the room is bridged, send the message to matrix only - const { bridged } = Rooms.findOne({ _id: message.rid }, { fields: { bridged: 1 } }); - if (bridged) { - return matrixClient.message.send({ ...message, u: { _id: uid } }); + if (Promise.await(federationRoomServiceSender.isAFederatedRoom(message.rid))) { + return federationRoomServiceSender.sendMessageFromRocketChat( + FederationRoomSenderConverter.toSendExternalMessageDto(uid, message.rid, message), + ); } return executeSendMessage(uid, message); diff --git a/apps/meteor/app/models/server/raw/Rooms.js b/apps/meteor/app/models/server/raw/Rooms.js index 0d86223b42a8..807b3d365aea 100644 --- a/apps/meteor/app/models/server/raw/Rooms.js +++ b/apps/meteor/app/models/server/raw/Rooms.js @@ -461,8 +461,8 @@ export class RoomsRaw extends BaseRaw { ]); } - setAsBridged(roomId) { - return this.updateOne({ _id: roomId }, { $set: { bridged: true } }); + setAsFederated(roomId) { + return this.updateOne({ _id: roomId }, { $set: { federated: true } }); } findByE2E(options) { diff --git a/apps/meteor/app/slashcommands-bridge/server/index.ts b/apps/meteor/app/slashcommands-bridge/server/index.ts index fa1c1e9d2f9f..b9396a596312 100644 --- a/apps/meteor/app/slashcommands-bridge/server/index.ts +++ b/apps/meteor/app/slashcommands-bridge/server/index.ts @@ -2,36 +2,40 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; import { slashCommands } from '../../utils/lib/slashCommand'; -import { matrixClient } from '../../federation-v2/server/matrix-client'; - -slashCommands.add( - 'bridge', - function Bridge(_command, stringParams, item): void { - if (_command !== 'bridge' || !Match.test(stringParams, String)) { - return; - } - - const [command, ...params] = stringParams.split(' '); - - const { rid: roomId } = item; - - switch (command) { - case 'invite': - // Invite a user - // Example: /bridge invite rc_helena:b.rc.allskar.com - const [userId] = params; - - const currentUserId = Meteor.userId(); - - if (currentUserId) { - Promise.await(matrixClient.user.invite(currentUserId, roomId, `@${userId.replace('@', '')}`)); - } - - break; - } - }, - { - description: 'Invites_an_user_to_a_bridged_room', - params: '#command #user', - }, -); +import { federationRoomServiceSender } from '../../federation-v2/server'; +import { FederationRoomSenderConverter } from '../../federation-v2/server/infrastructure/rocket-chat/converters/RoomSender'; + +function Bridge(_command: 'bridge', stringParams: string | undefined, item: Record): void { + if (_command !== 'bridge' || !Match.test(stringParams, String)) { + return; + } + + const [command, ...params] = stringParams.split(' '); + + const { rid: roomId } = item; + + switch (command) { + case 'invite': + // Invite a user + // Example: /bridge invite rc_helena:b.rc.allskar.com + const [userId] = params; + + const currentUserId = Meteor.userId(); + + if (currentUserId) { + const invitee = `@${userId.replace('@', '')}`; + Promise.await( + federationRoomServiceSender.inviteUserToAFederatedRoom( + FederationRoomSenderConverter.toRoomInviteUserDto(currentUserId, roomId, invitee), + ), + ); + } + + break; + } +} + +slashCommands.add('bridge', Bridge, { + description: 'Invites_an_user_to_a_bridged_room', + params: '#command #user', +}); diff --git a/apps/meteor/app/ui-sidenav/client/roomList.js b/apps/meteor/app/ui-sidenav/client/roomList.js index 902441e0b0fa..3dfdf1189230 100644 --- a/apps/meteor/app/ui-sidenav/client/roomList.js +++ b/apps/meteor/app/ui-sidenav/client/roomList.js @@ -172,7 +172,7 @@ const mergeSubRoom = (subscription) => { departmentId: 1, source: 1, queuedAt: 1, - bridged: 1, + federated: 1, }, }; @@ -214,7 +214,7 @@ const mergeSubRoom = (subscription) => { ts, source, queuedAt, - bridged, + federated, } = room; subscription.lm = subscription.lr ? new Date(Math.max(subscription.lr, lastRoomUpdate)) : lastRoomUpdate; @@ -253,7 +253,7 @@ const mergeSubRoom = (subscription) => { ts, source, queuedAt, - bridged, + federated, }); }; @@ -297,7 +297,7 @@ const mergeRoomSub = (room) => { ts, source, queuedAt, - bridged, + federated, } = room; Subscriptions.update( @@ -338,7 +338,7 @@ const mergeRoomSub = (room) => { ts, source, queuedAt, - bridged, + federated, ...getLowerCaseNames(room, sub.name, sub.fname), }, }, diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 976b71cf6f2d..ddbe4132fac6 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -121,6 +121,7 @@ "@types/rewire": "^2.5.28", "@types/semver": "^7.3.9", "@types/sharp": "^0.30.2", + "@types/sinon": "^10.0.11", "@types/string-strip-html": "^5.0.0", "@types/supertest": "^2.0.11", "@types/toastr": "^2.1.39", @@ -165,6 +166,7 @@ "postcss-url": "^10.1.3", "prettier": "2.6.2", "rewire": "^6.0.0", + "sinon": "^14.0.0", "source-map": "^0.7.3", "stylelint": "^13.13.1", "stylelint-order": "^4.1.0", diff --git a/apps/meteor/server/modules/watchers/publishFields.ts b/apps/meteor/server/modules/watchers/publishFields.ts index bede67ffd03f..6a0ff581ef50 100644 --- a/apps/meteor/server/modules/watchers/publishFields.ts +++ b/apps/meteor/server/modules/watchers/publishFields.ts @@ -105,7 +105,7 @@ export const roomFields = { queuedAt: 1, // Federation fields - bridged: 1, + federated: 1, // fields used by DMs usernames: 1, diff --git a/apps/meteor/server/startup/migrations/index.ts b/apps/meteor/server/startup/migrations/index.ts index 045c08a18f4e..f293f36a4640 100644 --- a/apps/meteor/server/startup/migrations/index.ts +++ b/apps/meteor/server/startup/migrations/index.ts @@ -89,4 +89,5 @@ import './v262'; import './v263'; import './v264'; import './v265'; +import './v266'; import './xrun'; diff --git a/apps/meteor/server/startup/migrations/v266.ts b/apps/meteor/server/startup/migrations/v266.ts new file mode 100644 index 000000000000..a2c016b1c571 --- /dev/null +++ b/apps/meteor/server/startup/migrations/v266.ts @@ -0,0 +1,19 @@ +import { addMigration } from '../../lib/migrations'; +import { Rooms } from '../../../app/models/server/raw'; + +addMigration({ + version: 266, + async up() { + await Rooms.updateMany( + { bridged: true }, + { + $set: { + federated: true, + }, + $unset: { + bridged: 1, + }, + }, + ); + }, +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceReceiver.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceReceiver.spec.ts new file mode 100644 index 000000000000..11050f2681fb --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceReceiver.spec.ts @@ -0,0 +1,436 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import '../../../lib/server.mocks'; + +import { FederationRoomServiceReceiver } from '../../../../../../app/federation-v2/server/application/RoomServiceReceiver'; +import { FederatedUser } from '../../../../../../app/federation-v2/server/domain/FederatedUser'; +import { FederatedRoom } from '../../../../../../app/federation-v2/server/domain/FederatedRoom'; +import { EVENT_ORIGIN } from '../../../../../../app/federation-v2/server/domain/IFederationBridge'; + +describe('Federation - Application - FederationRoomServiceReceiver', () => { + let service: FederationRoomServiceReceiver; + const roomAdapter = { + getFederatedRoomByExternalId: sinon.stub(), + createFederatedRoom: sinon.stub(), + removeUserFromRoom: sinon.stub(), + addUserToRoom: sinon.stub(), + updateRoomType: sinon.stub(), + updateRoomName: sinon.stub(), + updateRoomTopic: sinon.stub(), + }; + const userAdapter = { + getFederatedUserByExternalId: sinon.stub(), + createFederatedUser: sinon.stub(), + }; + const messageAdapter = { + sendMessage: sinon.stub(), + }; + const settingsAdapter = { + getHomeServerDomain: sinon.stub(), + }; + const bridge = { + getUserProfileInformation: sinon.stub().resolves({}), + isUserIdFromTheSameHomeserver: sinon.stub(), + joinRoom: sinon.stub(), + }; + + beforeEach(() => { + service = new FederationRoomServiceReceiver( + roomAdapter as any, + userAdapter as any, + messageAdapter as any, + settingsAdapter as any, + bridge as any, + ); + }); + + afterEach(() => { + roomAdapter.getFederatedRoomByExternalId.reset(); + roomAdapter.createFederatedRoom.reset(); + roomAdapter.removeUserFromRoom.reset(); + roomAdapter.addUserToRoom.reset(); + roomAdapter.updateRoomType.reset(); + roomAdapter.updateRoomName.reset(); + roomAdapter.updateRoomTopic.reset(); + userAdapter.getFederatedUserByExternalId.reset(); + userAdapter.createFederatedUser.reset(); + messageAdapter.sendMessage.reset(); + settingsAdapter.getHomeServerDomain.reset(); + bridge.isUserIdFromTheSameHomeserver.reset(); + bridge.joinRoom.reset(); + }); + + describe('#createRoom()', () => { + it('should NOT create users nor room if the room already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves({} as any); + await service.createRoom({} as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should NOT create users nor room if the room was created internally and programatically', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.createRoom({ wasInternallyProgramaticallyCreated: true } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should NOT create the creator if it already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves({} as any); + await service.createRoom({} as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the creator if it does not exists yet', async () => { + const creator = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(creator); + await service.createRoom({ externalInviterId: 'externalInviterId', normalizedInviterId: 'normalizedInviterId' } as any); + + expect(userAdapter.createFederatedUser.calledWith(creator)).to.be.true; + }); + + it('should create the room if it does not exists yet', async () => { + const creator = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(creator); + await service.createRoom({ + externalInviterId: 'externalInviterId', + normalizedInviterId: 'normalizedInviterId', + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + externalRoomName: 'externalRoomName', + } as any); + + const room = FederatedRoom.createInstance( + 'externalRoomId', + 'normalizedRoomId', + creator as FederatedUser, + RoomType.CHANNEL, + 'externalRoomName', + ); + expect(roomAdapter.createFederatedRoom.calledWith(room)).to.be.true; + }); + }); + + describe('#changeRoomMembership()', () => { + it('should throw an error if the room does not exists AND event origin is equal to LOCAL', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + try { + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + } catch (e: any) { + expect(e.message).to.be.equal('Could not find room with external room id: externalRoomId'); + } + }); + + it('should NOT throw an error if the room already exists AND event origin is equal to LOCAL', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + + expect(bridge.isUserIdFromTheSameHomeserver.called).to.be.true; + }); + + it('should NOT throw an error if the room already exists AND event origin is equal to REMOTE', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.REMOTE } as any); + + expect(bridge.isUserIdFromTheSameHomeserver.called).to.be.true; + }); + + it('should NOT create the inviter if it already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves({} as any); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the inviter if it does not exists', async () => { + const inviter = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + bridge.isUserIdFromTheSameHomeserver.onCall(0).resolves(false); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + externalInviterId: 'externalInviterId', + normalizedInviterId: 'normalizedInviterId', + } as any); + + expect(userAdapter.createFederatedUser.calledWith(inviter)).to.be.true; + }); + + it('should NOT create the invitee if it already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(undefined); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + + expect(userAdapter.createFederatedUser.calledOnce).to.be.true; + }); + + it('should create the invitee if it does not exists', async () => { + const invitee = FederatedUser.createInstance('externalInviteeId', { + name: 'normalizedInviteeId', + username: 'normalizedInviteeId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(undefined); + bridge.isUserIdFromTheSameHomeserver.onCall(1).resolves(false); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(userAdapter.createFederatedUser.calledWith(invitee)).to.be.true; + }); + + it('should create the room if it does not exists yet AND the event origin is REMOTE', async () => { + const inviter = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + const invitee = inviter; + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + bridge.isUserIdFromTheSameHomeserver.onCall(1).resolves(false); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(2).resolves(inviter); + userAdapter.getFederatedUserByExternalId.onCall(3).resolves(invitee); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.REMOTE, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', inviter as FederatedUser, RoomType.CHANNEL); + expect(roomAdapter.createFederatedRoom.calledWith(room)).to.be.true; + expect(bridge.joinRoom.calledWith('externalRoomId', 'externalInviteeId')).to.be.true; + }); + + it('should NOT create the room if it already exists yet AND the event origin is REMOTE', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.REMOTE, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should NOT create the room if it already exists yet AND the event origin is REMOTE', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should remove the user from room if its a LEAVE event', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + leave: true, + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.removeUserFromRoom.called).to.be.true; + expect(roomAdapter.addUserToRoom.called).to.be.false; + }); + + it('should add the user from room if its NOT a LEAVE event', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + leave: false, + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.removeUserFromRoom.called).to.be.false; + expect(roomAdapter.addUserToRoom.called).to.be.true; + }); + }); + + describe('#receiveExternalMessage()', () => { + it('should NOT send a message if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.receiveExternalMessage({ + text: 'text', + } as any); + + expect(messageAdapter.sendMessage.called).to.be.false; + }); + + it('should NOT send a message if the sender does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves(undefined); + await service.receiveExternalMessage({ + text: 'text', + } as any); + + expect(messageAdapter.sendMessage.called).to.be.false; + }); + + it('should send a message if the room and the sender already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves({} as any); + await service.receiveExternalMessage({ + text: 'text', + } as any); + + expect(messageAdapter.sendMessage.calledWith({}, 'text', {})).to.be.true; + }); + }); + + describe('#changeJoinRules()', () => { + it('should NOT change the room type if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.changeJoinRules({ + roomType: RoomType.CHANNEL, + } as any); + + expect(roomAdapter.updateRoomType.called).to.be.false; + }); + + it('should NOT change the room type if it exists and is a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeJoinRules({ + roomType: RoomType.CHANNEL, + } as any); + + expect(roomAdapter.updateRoomType.called).to.be.false; + }); + + it('should change the room type if it exists and is NOT a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.PRIVATE_GROUP; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeJoinRules({ + roomType: RoomType.CHANNEL, + } as any); + room.internalReference.t = RoomType.CHANNEL; + expect(roomAdapter.updateRoomType.calledWith(room)).to.be.true; + }); + }); + + describe('#changeRoomName()', () => { + it('should NOT change the room name if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.changeRoomName({ + normalizedRoomName: 'normalizedRoomName', + } as any); + + expect(roomAdapter.updateRoomName.called).to.be.false; + }); + + it('should NOT change the room name if it exists and is a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomName({ + normalizedRoomName: 'normalizedRoomName', + } as any); + + expect(roomAdapter.updateRoomName.called).to.be.false; + }); + + it('should change the room name if it exists and is NOT a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.PRIVATE_GROUP; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomName({ + roomnormalizedRoomNameType: 'normalizedRoomName', + } as any); + room.internalReference.name = 'normalizedRoomName'; + room.internalReference.fname = 'normalizedRoomName'; + expect(roomAdapter.updateRoomName.calledWith(room)).to.be.true; + }); + }); + + describe('#changeRoomTopic()', () => { + it('should NOT change the room topic if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.changeRoomTopic({ + roomTopic: 'roomTopic', + } as any); + + expect(roomAdapter.updateRoomTopic.called).to.be.false; + }); + + it('should NOT change the room topic if it exists and is a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomTopic({ + roomTopic: 'roomTopic', + } as any); + + expect(roomAdapter.updateRoomTopic.called).to.be.false; + }); + + it('should change the room topic if it exists and is NOT a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.PRIVATE_GROUP; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomTopic({ + roomTopic: 'roomTopic', + } as any); + room.internalReference.description = 'roomTopic'; + expect(roomAdapter.updateRoomTopic.calledWith(room)).to.be.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceSender.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceSender.spec.ts new file mode 100644 index 000000000000..8fa7719fb2ad --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceSender.spec.ts @@ -0,0 +1,311 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { FederationRoomServiceSender } from '../../../../../../app/federation-v2/server/application/RoomServiceSender'; +import { FederatedUser } from '../../../../../../app/federation-v2/server/domain/FederatedUser'; +import { FederatedRoom } from '../../../../../../app/federation-v2/server/domain/FederatedRoom'; + +describe('Federation - Application - FederationRoomServiceSender', () => { + let service: FederationRoomServiceSender; + const roomAdapter = { + getFederatedRoomByExternalId: sinon.stub(), + getFederatedRoomByInternalId: sinon.stub(), + createFederatedRoom: sinon.stub(), + updateFederatedRoomByInternalRoomId: sinon.stub(), + removeUserFromRoom: sinon.stub(), + addUserToRoom: sinon.stub(), + getInternalRoomById: sinon.stub(), + }; + const userAdapter = { + getFederatedUserByExternalId: sinon.stub(), + getFederatedUserByInternalId: sinon.stub(), + createFederatedUser: sinon.stub(), + getInternalUserById: sinon.stub(), + getFederatedUserByInternalUsername: sinon.stub(), + }; + const settingsAdapter = { + getHomeServerDomain: sinon.stub(), + }; + const bridge = { + getUserProfileInformation: sinon.stub().resolves({}), + isUserIdFromTheSameHomeserver: sinon.stub(), + sendMessage: sinon.stub(), + createUser: sinon.stub(), + inviteToRoom: sinon.stub().returns(new Promise((resolve) => resolve({}))), + createRoom: sinon.stub(), + joinRoom: sinon.stub(), + }; + const notificationAdapter = {}; + const room = FederatedRoom.build(); + const user = FederatedRoom.build(); + + beforeEach(() => { + service = new FederationRoomServiceSender( + roomAdapter as any, + userAdapter as any, + settingsAdapter as any, + notificationAdapter as any, + bridge as any, + ); + }); + + afterEach(() => { + roomAdapter.getFederatedRoomByExternalId.reset(); + roomAdapter.getFederatedRoomByInternalId.reset(); + roomAdapter.createFederatedRoom.reset(); + roomAdapter.updateFederatedRoomByInternalRoomId.reset(); + roomAdapter.addUserToRoom.reset(); + roomAdapter.getInternalRoomById.reset(); + userAdapter.getFederatedUserByExternalId.reset(); + userAdapter.getFederatedUserByInternalId.reset(); + userAdapter.getInternalUserById.reset(); + userAdapter.createFederatedUser.reset(); + userAdapter.getFederatedUserByInternalUsername.reset(); + settingsAdapter.getHomeServerDomain.reset(); + bridge.isUserIdFromTheSameHomeserver.reset(); + bridge.sendMessage.reset(); + bridge.createUser.reset(); + bridge.createRoom.reset(); + bridge.inviteToRoom.reset(); + bridge.joinRoom.reset(); + }); + + describe('#inviteUserToAFederatedRoom()', () => { + it('should NOT create the inviter user if the user already exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.resolves(user); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({} as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the inviter user both externally and internally if it does not exists', async () => { + userAdapter.getFederatedUserByInternalUsername.resolves(user); + userAdapter.getFederatedUserByInternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByInternalId.onCall(1).resolves(user); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + bridge.createUser.resolves('externalInviterId'); + await service.inviteUserToAFederatedRoom({ externalInviterId: 'externalInviterId' } as any); + const inviter = FederatedUser.createInstance('externalInviterId', { + name: 'name', + username: 'username', + existsOnlyOnProxyServer: true, + }); + expect(bridge.createUser.calledWith('username', 'name', 'domain')).to.be.true; + expect(userAdapter.createFederatedUser.calledWith(inviter)).to.be.true; + }); + + it('should NOT create the invitee user if the user already exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.resolves({} as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the invitee user internally if it does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByInternalUsername.onCall(1).resolves({} as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + const invitee = FederatedUser.createInstance('rawInviteeId', { + name: 'normalizedInviteeId', + username: 'normalizedInviteeId', + existsOnlyOnProxyServer: false, + }); + + expect(userAdapter.createFederatedUser.calledWith(invitee)).to.be.true; + }); + + it('should NOT create the room if it already exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.resolves({} as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should create the room both externally and internally if it does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ externalId: 'externalInviteeId' } as any); + roomAdapter.getInternalRoomById.resolves({ _id: 'internalRoomId', t: RoomType.CHANNEL, name: 'roomName', topic: 'topic' } as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + bridge.createUser.resolves('externalInviterId'); + bridge.createRoom.resolves('externalRoomId'); + roomAdapter.getFederatedRoomByInternalId.onCall(0).resolves(undefined); + roomAdapter.getFederatedRoomByInternalId.onCall(1).resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + const roomResult = FederatedRoom.createInstance('externalRoomId', 'externalRoomId', user as any, RoomType.CHANNEL, 'roomName'); + + expect(bridge.createRoom.calledWith('externalInviterId', 'externalInviteeId', RoomType.CHANNEL, 'roomName', 'topic')).to.be.true; + expect(roomAdapter.updateFederatedRoomByInternalRoomId.calledWith('internalRoomId', roomResult)).to.be.true; + }); + + it('should create, invite and join the user to the room in the proxy home server if the invitee is from the same homeserver', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + roomAdapter.getInternalRoomById.resolves({ _id: 'internalRoomId', t: RoomType.CHANNEL, name: 'roomName', topic: 'topic' } as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + room.externalId = 'externalRoomId'; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + settingsAdapter.getHomeServerDomain.returns('domain'); + bridge.isUserIdFromTheSameHomeserver.resolves(true); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(bridge.createUser.calledWith('inviteeUsernameOnly', 'usernameInvitee', 'domain')).to.be.true; + expect(bridge.inviteToRoom.calledWith('externalRoomId', 'externalInviterId', 'externalInviteeId')).to.be.true; + expect(bridge.joinRoom.calledWith('externalRoomId', 'externalInviteeId')).to.be.true; + }); + + it('should invite the user to an external room if the room is NOT direct message(on DMs, they are invited during the creational process)', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + roomAdapter.getInternalRoomById.resolves({ + _id: 'internalRoomId', + t: RoomType.DIRECT_MESSAGE, + name: 'roomName', + topic: 'topic', + } as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + room.externalId = 'externalRoomId'; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.isUserIdFromTheSameHomeserver.resolves(false); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(bridge.inviteToRoom.calledWith('externalRoomId', 'externalInviterId', 'externalInviteeId')).to.be.true; + }); + + it('should NOT invite any user externally if the user is not from the same home server AND it was already invited when creating the room', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.isUserIdFromTheSameHomeserver.resolves(false); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(bridge.inviteToRoom.called).to.be.false; + expect(bridge.createUser.called).to.be.false; + expect(bridge.joinRoom.called).to.be.false; + }); + + it('should always add the user to the internal room', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.isUserIdFromTheSameHomeserver.resolves(false); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(roomAdapter.addUserToRoom.called).to.be.true; + }); + }); + + describe('#sendMessageFromRocketChat()', () => { + it('should throw an error if the sender does not exists ', async () => { + userAdapter.getFederatedUserByInternalId.resolves(undefined); + try { + await service.sendMessageFromRocketChat({ internalSenderId: 'internalSenderId' } as any); + } catch (e: any) { + expect(e.message).to.be.equal('Could not find user id for internalSenderId'); + } + }); + + it('should throw an error if the room does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves({} as any); + roomAdapter.getFederatedRoomByInternalId.resolves(undefined); + try { + await service.sendMessageFromRocketChat({ internalRoomId: 'internalRoomId' } as any); + } catch (e: any) { + expect(e.message).to.be.equal('Could not find room id for internalRoomId'); + } + }); + + it('should send the message through the bridge', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalId' } as any); + roomAdapter.getFederatedRoomByInternalId.resolves({ externalId: 'externalId' } as any); + await service.sendMessageFromRocketChat({ message: { msg: 'text' } } as any); + expect(bridge.sendMessage.calledWith('externalId', 'externalId', 'text')).to.be.true; + }); + }); + + describe('#isAFederatedRoom()', () => { + it('should return false if internalRoomId is undefined', async () => { + expect(await service.isAFederatedRoom('')).to.be.false; + }); + + it('should return false if the room does not exist', async () => { + roomAdapter.getFederatedRoomByInternalId.resolves(undefined); + expect(await service.isAFederatedRoom('')).to.be.false; + }); + + it('should return true if the room is NOT federated', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.federated = false; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + expect(await service.isAFederatedRoom('internalRoomId')).to.be.false; + }); + + it('should return true if the room is federated', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.federated = true; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + expect(await service.isAFederatedRoom('internalRoomId')).to.be.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedRoom.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedRoom.spec.ts new file mode 100644 index 000000000000..b6d7f0011368 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedRoom.spec.ts @@ -0,0 +1,169 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { expect } from 'chai'; + +import { FederatedRoom } from '../../../../../../app/federation-v2/server/domain/FederatedRoom'; + +describe('Federation - Domain - FederatedRoom', () => { + const members = [{ internalReference: { id: 'userId' } }, { internalReference: { id: 'userId2' } }] as any; + + describe('#createInstance()', () => { + it('should set the internal room name when it was provided', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, 'p' as any, 'myRoomName'); + expect(federatedRoom.internalReference.name).to.be.equal('myRoomName'); + expect(federatedRoom.internalReference.fname).to.be.equal('myRoomName'); + }); + + it('should generate automatically a room name when it was not provided', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, 'p' as any); + expect(federatedRoom.internalReference.name).to.be.equal('Federation-externalId'); + expect(federatedRoom.internalReference.fname).to.be.equal('Federation-externalId'); + }); + + it('should set the members property when the room is a direct message one', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + expect(federatedRoom.members).to.be.eql(members); + }); + + it('should NOT set the members property when the room is NOT a direct message one', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.CHANNEL, + '', + members, + ); + expect(federatedRoom.members).to.be.undefined; + }); + + it('should return an instance of FederatedRoom', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.CHANNEL); + expect(federatedRoom).to.be.instanceOf(FederatedRoom); + }); + }); + + describe('#isDirectMessage()', () => { + it('should return true if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(federatedRoom.isDirectMessage()).to.be.true; + }); + + it('should return false if its NOT a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.CHANNEL); + expect(federatedRoom.isDirectMessage()).to.be.false; + }); + }); + + describe('#setRoomType()', () => { + it('should set the Room type if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.setRoomType(RoomType.CHANNEL); + expect(federatedRoom.internalReference.t).to.be.equal(RoomType.CHANNEL); + }); + + it('should throw an error when trying to set the room type if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.setRoomType(RoomType.CHANNEL)).to.be.throw('Its not possible to change a direct message type'); + }); + }); + + describe('#changeRoomName()', () => { + it('should change the Room name if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.changeRoomName('newName'); + expect(federatedRoom.internalReference.name).to.be.equal('newName'); + expect(federatedRoom.internalReference.fname).to.be.equal('newName'); + }); + + it('should throw an error when trying to change the room name if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.changeRoomName('newName')).to.be.throw('Its not possible to change a direct message name'); + }); + }); + + describe('#changeRoomTopic()', () => { + it('should change the Room topic if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.changeRoomTopic('newName'); + expect(federatedRoom.internalReference.description).to.be.equal('newName'); + }); + + it('should throw an error when trying to change the room topic if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.changeRoomTopic('newName')).to.be.throw('Its not possible to change a direct message topic'); + }); + }); + + describe('#changeRoomTopic()', () => { + it('should change the Room topic if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.changeRoomTopic('newName'); + expect(federatedRoom.internalReference.description).to.be.equal('newName'); + }); + + it('should throw an error when trying to change the room topic if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.changeRoomTopic('newName')).to.be.throw('Its not possible to change a direct message topic'); + }); + }); + + describe('#getMembers()', () => { + it('should return the internalReference members if the room is a direct message', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + expect(federatedRoom.getMembers()).to.be.eql(members.map((user: any) => user.internalReference)); + }); + + it('should return an empty array if the room is not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.CHANNEL, + '', + members, + ); + expect(federatedRoom.getMembers()).to.be.eql([]); + }); + }); + + describe('#isFederated()', () => { + it('should return true if the room is federated', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + federatedRoom.internalReference.federated = true; + expect(federatedRoom.isFederated()).to.be.true; + }); + + it('should return false if the room is NOT federated', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + expect(federatedRoom.isFederated()).to.be.false; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedUser.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedUser.spec.ts new file mode 100644 index 000000000000..386877bbd858 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedUser.spec.ts @@ -0,0 +1,43 @@ +import { expect } from 'chai'; + +import { FederatedUser } from '../../../../../../app/federation-v2/server/domain/FederatedUser'; + +describe('Federation - Domain - FederatedUser', () => { + describe('#createInstance()', () => { + it('should set the internal user name when it was provided', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: '', + username: 'username', + existsOnlyOnProxyServer: false, + }); + expect(federatedUser.internalReference.username).to.be.equal('username'); + }); + + it('should set the internal name when it was provided', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: 'name', + username: '', + existsOnlyOnProxyServer: false, + }); + expect(federatedUser.internalReference.name).to.be.equal('name'); + }); + + it('should set the existsOnlyOnProxyServer it was provided', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: '', + username: 'username', + existsOnlyOnProxyServer: true, + }); + expect(federatedUser.existsOnlyOnProxyServer).to.be.true; + }); + + it('should return an instance of FederatedUser', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: '', + username: 'username', + existsOnlyOnProxyServer: false, + }); + expect(federatedUser).to.be.instanceOf(FederatedUser); + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts new file mode 100644 index 000000000000..31a67945fa15 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts @@ -0,0 +1,328 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { expect } from 'chai'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { MatrixRoomReceiverConverter } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver'; +import { + FederationRoomCreateInputDto, + FederationRoomChangeMembershipDto, + FederationRoomSendInternalMessageDto, + FederationRoomChangeJoinRulesDto, + FederationRoomChangeNameDto, + FederationRoomChangeTopicDto, +} from '../../../../../../../../app/federation-v2/server/application/input/RoomReceiverDto'; +import { MatrixEventType } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType'; +import { RoomJoinRules } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; +import { AddMemberToRoomMembership } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom'; +import { EVENT_ORIGIN } from '../../../../../../../../app/federation-v2/server/domain/IFederationBridge'; + +describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', () => { + describe('#toRoomCreateDto()', () => { + const event = { + content: { was_internally_programatically_created: true, name: 'roomName' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomCreateInputDto', () => { + expect(MatrixRoomReceiverConverter.toRoomCreateDto({} as any)).to.be.instanceOf(FederationRoomCreateInputDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should return the external room name and room type when the room state is present on the event and it has the correct events', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }, + ]; + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert to the expected (private) room type when the join rule is equal to INVITE', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.INVITE } }, + ]; + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.PRIVATE_GROUP); + }); + + it('should convert to the expected (channel) room type when the join rule is equal to JOIN', () => { + const state = [{ type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }]; + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ invite_room_state: state } as any); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert the inviter id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ sender: event.sender } as any); + expect(result.normalizedInviterId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should set wasInternallyProgramaticallyCreated accordingly to the event', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ content: event.content } as any); + expect(result.wasInternallyProgramaticallyCreated).to.be.true; + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + externalInviterId: '@marcos.defendi:matrix.org', + normalizedInviterId: 'marcos.defendi:matrix.org', + wasInternallyProgramaticallyCreated: true, + }); + }); + }); + + describe('#toChangeRoomMembershipDto()', () => { + const event = { + content: { name: 'roomName' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + state_key: '@marcos.defendi2:matrix.org', + }; + + it('should return an instance of FederationRoomChangeMembershipDto', () => { + expect(MatrixRoomReceiverConverter.toChangeRoomMembershipDto({} as any)).to.be.instanceOf(FederationRoomChangeMembershipDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should return the external room name and room type when the room state is present on the event and it has the correct events', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }, + ]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert to the expected (private) room type when the join rule is equal to INVITE', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.INVITE } }, + ]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.PRIVATE_GROUP); + }); + + it('should convert to the expected (channel) room type when the join rule is equal to JOIN', () => { + const state = [{ type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ invite_room_state: state } as any); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert to the expected (direct) room type when the join rule is equal to INVITE and its a direct message', () => { + const state = [{ type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.INVITE } }]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ + invite_room_state: state, + content: { is_direct: true }, + } as any); + expect(result.roomType).to.be.equal(RoomType.DIRECT_MESSAGE); + }); + + it('should convert the inviter id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: event.sender } as any); + expect(result.normalizedInviterId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should convert the invitee id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ state_key: event.sender } as any); + expect(result.normalizedInviteeId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should convert the inviter id to the a rc-format username like (without any @ in it and just the part before the ":")', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: event.sender } as any); + expect(result.inviterUsernameOnly).to.be.equal('marcos.defendi'); + }); + + it('should convert the invitee id to the a rc-format username like (without any @ in it and just the part before the ":")', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ state_key: event.sender } as any); + expect(result.inviteeUsernameOnly).to.be.equal('marcos.defendi'); + }); + + it('should set leave to true if its a LEAVE event', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ + content: { membership: AddMemberToRoomMembership.LEAVE }, + } as any); + expect(result.leave).to.be.true; + }); + + it('should set leave to false if its NOT a LEAVE event', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ + content: { membership: AddMemberToRoomMembership.JOIN }, + } as any); + expect(result.leave).to.be.false; + }); + + it('should set the event origin as REMOTE if the users are from different home servers', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: 'a:matrix.org', state_key: 'a:matrix2.org' } as any); + expect(result.eventOrigin).to.be.equal(EVENT_ORIGIN.REMOTE); + }); + + it('should set the event origin as LOCAL if the users are from different home servers', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: 'a:matrix.org', state_key: 'a:matrix.org' } as any); + expect(result.eventOrigin).to.be.equal(EVENT_ORIGIN.LOCAL); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + externalInviterId: '@marcos.defendi:matrix.org', + normalizedInviterId: 'marcos.defendi:matrix.org', + externalInviteeId: '@marcos.defendi2:matrix.org', + normalizedInviteeId: 'marcos.defendi2:matrix.org', + inviteeUsernameOnly: 'marcos.defendi2', + inviterUsernameOnly: 'marcos.defendi', + eventOrigin: EVENT_ORIGIN.LOCAL, + leave: false, + }); + }); + }); + + describe('#toSendRoomMessageDto()', () => { + const event = { + content: { body: 'msg' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomSendInternalMessageDto', () => { + expect(MatrixRoomReceiverConverter.toSendRoomMessageDto({} as any)).to.be.instanceOf(FederationRoomSendInternalMessageDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toSendRoomMessageDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert the sender id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toSendRoomMessageDto({ sender: event.sender } as any); + expect(result.normalizedSenderId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toSendRoomMessageDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + externalSenderId: '@marcos.defendi:matrix.org', + normalizedSenderId: 'marcos.defendi:matrix.org', + text: 'msg', + }); + }); + }); + + describe('#toRoomChangeJoinRulesDto()', () => { + const event = { + content: { join_rule: RoomJoinRules.JOIN }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomChangeJoinRulesDto', () => { + expect(MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({} as any)).to.be.instanceOf(FederationRoomChangeJoinRulesDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert to the expected (private) room type when the join rule is equal to INVITE', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({ content: { join_rule: RoomJoinRules.INVITE } } as any); + expect(result.roomType).to.be.equal(RoomType.PRIVATE_GROUP); + }); + + it('should convert to the expected (channel) room type when the join rule is equal to JOIN', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({ content: { join_rule: RoomJoinRules.JOIN } } as any); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + roomType: RoomType.CHANNEL, + }); + }); + }); + + describe('#toRoomChangeNameDto()', () => { + const event = { + content: { name: '@roomName' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of toRoomChangeNameDto', () => { + expect(MatrixRoomReceiverConverter.toRoomChangeNameDto({} as any)).to.be.instanceOf(FederationRoomChangeNameDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeNameDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert the roomName to a normalized version without starting with @', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeNameDto({ content: event.content } as any); + expect(result.normalizedRoomName).to.be.equal('roomName'); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeNameDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + normalizedRoomName: 'roomName', + }); + }); + }); + + describe('#toRoomChangeTopicDto()', () => { + const event = { + content: { topic: 'room topic' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomChangeTopicDto', () => { + expect(MatrixRoomReceiverConverter.toRoomChangeTopicDto({} as any)).to.be.instanceOf(FederationRoomChangeTopicDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeTopicDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeTopicDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + roomTopic: 'room topic', + }); + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/BaseEvent.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/BaseEvent.spec.ts new file mode 100644 index 000000000000..a8ff82b762f2 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/BaseEvent.spec.ts @@ -0,0 +1,45 @@ +import { expect, spy } from 'chai'; + +import { MatrixBaseEventHandler } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent'; + +describe('Federation - Infrastructure - Matrix - MatrixBaseEventHandler', () => { + describe('#equals()', () => { + class MyHandler extends MatrixBaseEventHandler { + public constructor(type: any) { + super(type); + } + + public handle(): Promise { + throw new Error('Method not implemented.'); + } + } + const myHandler = new MyHandler('type' as any); + + it('should return true if the type is equals to the provided one', () => { + expect(myHandler.equals('type' as any)).to.be.true; + }); + + it('should return false if the type is different to the provided one', () => { + expect(myHandler.equals('different' as any)).to.be.false; + }); + }); + + describe('#handle()', () => { + const spyFn = spy(); + class MyHandler extends MatrixBaseEventHandler { + public constructor(type: any) { + super(type); + } + + public async handle(): Promise { + spyFn(); + } + } + const myHandler = new MyHandler('type' as any); + + it('should call the handler fn in the implementated class', () => { + myHandler.handle(); + expect(spyFn).to.be.called; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/MatrixEventsHandler.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/MatrixEventsHandler.spec.ts new file mode 100644 index 000000000000..bbdde4978286 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/MatrixEventsHandler.spec.ts @@ -0,0 +1,25 @@ +import { expect, spy } from 'chai'; + +import { MatrixEventsHandler } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/handlers'; + +describe('Federation - Infrastructure - Matrix - MatrixEventsHandler', () => { + describe('#handleEvent()', () => { + const spyFn = spy(); + const myHandler = new MatrixEventsHandler([ + { + equals: (eventType: string): boolean => eventType === 'eventType', + handle: spyFn, + }, + ] as any); + + it('should call the handler fn properly', async () => { + await myHandler.handleEvent({ type: 'eventType' } as any); + expect(spyFn).to.have.been.called.with({ type: 'eventType' }); + }); + + it('should NOT call the handler if there is no handler for the event', async () => { + await myHandler.handleEvent({ type: 'eventType2' } as any); + expect(spyFn).to.not.be.called; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/queue/InMemoryQueue.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/queue/InMemoryQueue.spec.ts new file mode 100644 index 000000000000..a79437b3494d --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/queue/InMemoryQueue.spec.ts @@ -0,0 +1,28 @@ +import { expect, spy } from 'chai'; +import mock from 'mock-require'; + +import { InMemoryQueue } from '../../../../../../../app/federation-v2/server/infrastructure/queue/InMemoryQueue'; + +mock('fastq', { + promise: (handler: Function) => ({ + push: async (task: any): Promise => handler(task), + }), +}); + +describe('Federation - Infrastructure - Queue - InMemoryQueue', () => { + const queue = new InMemoryQueue(); + + describe('#addToQueue()', () => { + it('should throw an error if the instance was not set beforehand', () => { + expect(() => queue.addToQueue({})).to.throw('You need to set the handler first'); + }); + + it('should push the task to the queue instance to be handled when the instance was properly defined', () => { + const spiedCb = spy(); + const concurrency = 1; + queue.setHandler(spiedCb, concurrency); + queue.addToQueue({ task: 'my-task' }); + expect(spiedCb).to.have.been.called.with({ task: 'my-task' }); + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Room.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Room.spec.ts new file mode 100644 index 000000000000..19edac2efe00 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Room.spec.ts @@ -0,0 +1,3 @@ +// describe('Federation - Infrastructure - RocketChat - RocketChatRoomAdapter', () => { + +// }); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Settings.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Settings.spec.ts new file mode 100644 index 000000000000..f5d7859a0c1c --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Settings.spec.ts @@ -0,0 +1,3 @@ +// describe('Federation - Infrastructure - RocketChat - RocketChatSettingsAdapter', () => { + +// }); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/User.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/User.spec.ts new file mode 100644 index 000000000000..29bf5f30b0aa --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/User.spec.ts @@ -0,0 +1,5 @@ +// // import { expect } from 'chai'; + +// describe('Federation - Infrastructure - RocketChat - RocketChatUserAdapter', () => { +// +// }); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/converters/RoomSender.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/converters/RoomSender.spec.ts new file mode 100644 index 000000000000..575f51ec3a92 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/converters/RoomSender.spec.ts @@ -0,0 +1,72 @@ +import { expect } from 'chai'; +import { IMessage } from '@rocket.chat/core-typings'; + +import { FederationRoomSenderConverter } from '../../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender'; +import { + FederationRoomInviteUserDto, + FederationRoomSendExternalMessageDto, +} from '../../../../../../../../app/federation-v2/server/application/input/RoomSenderDto'; + +describe('Federation - Infrastructure - RocketChat - FederationRoomSenderConverter', () => { + describe('#toRoomInviteUserDto()', () => { + it('should return an instance of FederationRoomInviteUserDto', () => { + expect( + FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', 'externalInviteeId'), + ).to.be.instanceOf(FederationRoomInviteUserDto); + }); + + it('should return the normalizedInviteeId property without any @ if any', () => { + expect( + FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', '@externalInviteeId:server-name.com') + .normalizedInviteeId, + ).to.be.equal('externalInviteeId:server-name.com'); + }); + + it('should return the inviteeUsernameOnly property without any @ if any and only the first part before ":"', () => { + expect( + FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', '@externalInviteeId:server-name.com') + .inviteeUsernameOnly, + ).to.be.equal('externalInviteeId'); + }); + + it('should return the normalizedInviteeId AND inviteeUsernameOnly equals to the rawInviteeId if it does not have any special chars', () => { + const result = FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', 'externalInviteeId'); + expect(result.rawInviteeId).to.be.equal('externalInviteeId'); + expect(result.normalizedInviteeId).to.be.equal('externalInviteeId'); + expect(result.inviteeUsernameOnly).to.be.equal('externalInviteeId'); + }); + + it('should have all the properties set', () => { + const internalInviterId = 'internalInviterId'; + const internalRoomId = 'internalRoomId'; + const externalInviteeId = 'externalInviteeId'; + const result: any = FederationRoomSenderConverter.toRoomInviteUserDto(internalInviterId, internalRoomId, externalInviteeId); + expect(result).to.be.eql({ + internalInviterId, + internalRoomId, + rawInviteeId: externalInviteeId, + normalizedInviteeId: externalInviteeId, + inviteeUsernameOnly: externalInviteeId, + }); + }); + }); + describe('#toSendExternalMessageDto()', () => { + it('should return an instance of FederationRoomSendExternalMessageDto', () => { + expect( + FederationRoomSenderConverter.toSendExternalMessageDto('internalSenderId', 'internalRoomId', { msg: 'text' } as IMessage), + ).to.be.instanceOf(FederationRoomSendExternalMessageDto); + }); + + it('should have all the properties set', () => { + const internalSenderId = 'internalSenderId'; + const internalRoomId = 'internalRoomId'; + const msg = { msg: 'text' } as IMessage; + const result: any = FederationRoomSenderConverter.toSendExternalMessageDto(internalSenderId, internalRoomId, msg); + expect(result).to.be.eql({ + internalSenderId, + internalRoomId, + message: msg, + }); + }); + }); +}); diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index dc09fe72a312..647fbe290171 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -84,6 +84,7 @@ export interface IRoom extends IRocketChatRecord { description?: string; createdOTR?: boolean; e2eKeyId?: string; + federated?: boolean; channel?: { _id: string }; } diff --git a/yarn.lock b/yarn.lock index b8f3140d3d2a..b6f5c8dcf825 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4826,6 +4826,7 @@ __metadata: "@types/rewire": ^2.5.28 "@types/semver": ^7.3.9 "@types/sharp": ^0.30.2 + "@types/sinon": ^10.0.11 "@types/speakeasy": ^2.0.7 "@types/string-strip-html": ^5.0.0 "@types/supertest": ^2.0.11 @@ -4984,6 +4985,7 @@ __metadata: rewire: ^6.0.0 semver: ^7.3.7 sharp: ^0.30.4 + sinon: ^14.0.0 sip.js: ^0.20.0 sodium-native: ^3.3.0 sodium-plus: ^0.9.0 @@ -5248,7 +5250,7 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^1.7.0": +"@sinonjs/commons@npm:^1.6.0, @sinonjs/commons@npm:^1.7.0, @sinonjs/commons@npm:^1.8.3": version: 1.8.3 resolution: "@sinonjs/commons@npm:1.8.3" dependencies: @@ -5257,6 +5259,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:>=5, @sinonjs/fake-timers@npm:^9.1.2": + version: 9.1.2 + resolution: "@sinonjs/fake-timers@npm:9.1.2" + dependencies: + "@sinonjs/commons": ^1.7.0 + checksum: 7d3aef54e17c1073101cb64d953157c19d62a40e261a30923fa1ee337b049c5f29cc47b1f0c477880f42b5659848ba9ab897607ac8ea4acd5c30ddcfac57fca6 + languageName: node + linkType: hard + "@sinonjs/fake-timers@npm:^8.0.1": version: 8.1.0 resolution: "@sinonjs/fake-timers@npm:8.1.0" @@ -5266,6 +5277,24 @@ __metadata: languageName: node linkType: hard +"@sinonjs/samsam@npm:^6.1.1": + version: 6.1.1 + resolution: "@sinonjs/samsam@npm:6.1.1" + dependencies: + "@sinonjs/commons": ^1.6.0 + lodash.get: ^4.4.2 + type-detect: ^4.0.8 + checksum: a09b0914bf573f0da82bd03c64ba413df81a7c173818dc3f0a90c2652240ac835ef583f4d52f0b215e626633c91a4095c255e0669f6ead97241319f34f05e7fc + languageName: node + linkType: hard + +"@sinonjs/text-encoding@npm:^0.7.1": + version: 0.7.1 + resolution: "@sinonjs/text-encoding@npm:0.7.1" + checksum: 130de0bb568c5f8a611ec21d1a4e3f80ab0c5ec333010f49cfc1adc5cba6d8808699c8a587a46b0f0b016a1f4c1389bc96141e773e8460fcbb441875b2e91ba7 + languageName: node + linkType: hard + "@slack/client@npm:^4.12.0": version: 4.12.0 resolution: "@slack/client@npm:4.12.0" @@ -7807,6 +7836,22 @@ __metadata: languageName: node linkType: hard +"@types/sinon@npm:^10.0.11": + version: 10.0.11 + resolution: "@types/sinon@npm:10.0.11" + dependencies: + "@types/sinonjs__fake-timers": "*" + checksum: 196f3e26985dca5dfb593592e4b64463e536c047a9f43aa2b328b16024a3b0e3fb27b7a3f3972c6ef75749f55012737eb6c63a1c2e9782b7fe5cbbd25f75fd62 + languageName: node + linkType: hard + +"@types/sinonjs__fake-timers@npm:*": + version: 8.1.2 + resolution: "@types/sinonjs__fake-timers@npm:8.1.2" + checksum: bbc73a5ab6c0ec974929392f3d6e1e8db4ebad97ec506d785301e1c3d8a4f98a35b1aa95b97035daef02886fd8efd7788a2fa3ced2ec7105988bfd8dce61eedd + languageName: node + linkType: hard + "@types/sizzle@npm:*": version: 2.3.3 resolution: "@types/sizzle@npm:2.3.3" @@ -13944,6 +13989,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^5.0.0": + version: 5.1.0 + resolution: "diff@npm:5.1.0" + checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90 + languageName: node + linkType: hard + "diffie-hellman@npm:^5.0.0": version: 5.0.3 resolution: "diffie-hellman@npm:5.0.3" @@ -21338,6 +21390,13 @@ __metadata: languageName: node linkType: hard +"just-extend@npm:^4.0.2": + version: 4.2.1 + resolution: "just-extend@npm:4.2.1" + checksum: ff9fdede240fad313efeeeb68a660b942e5586d99c0058064c78884894a2690dc09bba44c994ad4e077e45d913fef01a9240c14a72c657b53687ac58de53b39c + languageName: node + linkType: hard + "jwa@npm:^1.4.1": version: 1.4.1 resolution: "jwa@npm:1.4.1" @@ -24088,6 +24147,19 @@ __metadata: languageName: node linkType: hard +"nise@npm:^5.1.1": + version: 5.1.1 + resolution: "nise@npm:5.1.1" + dependencies: + "@sinonjs/commons": ^1.8.3 + "@sinonjs/fake-timers": ">=5" + "@sinonjs/text-encoding": ^0.7.1 + just-extend: ^4.0.2 + path-to-regexp: ^1.7.0 + checksum: d8be29e84a014743c9a10f428fac86f294ac5f92bed1f606fe9b551e935f494d8e0ce1af8a12673c6014010ec7f771f2d48aa5c8e116f223eb4f40c5e1ab44b3 + languageName: node + linkType: hard + "nkeys.js@npm:^1.0.0-9": version: 1.0.0-9 resolution: "nkeys.js@npm:1.0.0-9" @@ -25750,6 +25822,15 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^1.7.0": + version: 1.8.0 + resolution: "path-to-regexp@npm:1.8.0" + dependencies: + isarray: 0.0.1 + checksum: 709f6f083c0552514ef4780cb2e7e4cf49b0cc89a97439f2b7cc69a608982b7690fb5d1720a7473a59806508fc2dae0be751ba49f495ecf89fd8fbc62abccbcd + languageName: node + linkType: hard + "path-to-regexp@npm:^6.2.0": version: 6.2.0 resolution: "path-to-regexp@npm:6.2.0" @@ -30106,6 +30187,20 @@ __metadata: languageName: node linkType: hard +"sinon@npm:^14.0.0": + version: 14.0.0 + resolution: "sinon@npm:14.0.0" + dependencies: + "@sinonjs/commons": ^1.8.3 + "@sinonjs/fake-timers": ^9.1.2 + "@sinonjs/samsam": ^6.1.1 + diff: ^5.0.0 + nise: ^5.1.1 + supports-color: ^7.2.0 + checksum: b2aeeb0cdc2cd30f904ccbcd60bae4e1b3dcf3aeeface09c1832db0336be0dbaa461f3b91b769bed84f05c83d45d5072a9da7ee14bc7289daeda2a1214fe173c + languageName: node + linkType: hard + "sip.js@npm:^0.20.0": version: 0.20.0 resolution: "sip.js@npm:0.20.0" @@ -31457,7 +31552,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": +"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -32666,7 +32761,7 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:4.0.8, type-detect@npm:^4.0.0, type-detect@npm:^4.0.5": +"type-detect@npm:4.0.8, type-detect@npm:^4.0.0, type-detect@npm:^4.0.5, type-detect@npm:^4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" checksum: 62b5628bff67c0eb0b66afa371bd73e230399a8d2ad30d852716efcc4656a7516904570cd8631a49a3ce57c10225adf5d0cbdcb47f6b0255fe6557c453925a15