diff --git a/.changeset/chatty-dingos-bathe.md b/.changeset/chatty-dingos-bathe.md new file mode 100644 index 00000000000..e09c5e1c667 --- /dev/null +++ b/.changeset/chatty-dingos-bathe.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/apps-engine': minor +'@rocket.chat/apps': minor +'@rocket.chat/meteor': minor +--- + +Adds a new method to the Apps-Engine that allows apps to retrieve multiple rooms from database diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 861f7ade14d..ec0f6c8b3bc 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -1,9 +1,9 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IMessage, IMessageRaw } from '@rocket.chat/apps-engine/definition/messages'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IRoom, IRoomRaw } from '@rocket.chat/apps-engine/definition/rooms'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import type { GetMessagesOptions } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; +import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; import { RoomBridge } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom, IMessage as ICoreMessage } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms, Messages } from '@rocket.chat/models'; @@ -17,6 +17,41 @@ import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFrom import { createChannelMethod } from '../../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup'; +const rawRoomProjection: FindOptions['projection'] = { + _id: 1, + fname: 1, + name: 1, + usernames: 1, + members: 1, + uids: 1, + default: 1, + ro: 1, + sysMes: 1, + msgs: 1, + ts: 1, + _updatedAt: 1, + closedAt: 1, + lm: 1, + description: 1, + customFields: 1, + prid: 1, + teamId: 1, + teamMain: 1, + livechatData: 1, + waitingResponse: 1, + open: 1, + source: 1, + closer: 1, + t: 1, + u: 1, + v: 1, + contactId: 1, + departmentId: 1, + closedBy: 1, + servedBy: 1, + responseBy: 1, +}; + export class AppRoomBridge extends RoomBridge { constructor(private readonly orch: IAppServerOrchestrator) { super(); @@ -151,6 +186,37 @@ export class AppRoomBridge extends RoomBridge { return promises as Promise; } + protected async getAllRooms(filters: GetRoomsFilters = {}, options: GetRoomsOptions = {}, appId: string): Promise> { + this.orch.debugLog(`The App ${appId} is getting all rooms with options`, options); + + const { limit = 100, skip = 0 } = options; + + const findOptions: FindOptions = { + sort: { ts: -1 }, + skip, + limit: Math.min(limit, 100), + projection: rawRoomProjection, + }; + + const { types, discussions, teams } = filters; + + const rooms: IRoomRaw[] = []; + + const roomConverter = this.orch.getConverters()?.get('rooms'); + if (!roomConverter) { + throw new Error('Room converter not found'); + } + + for await (const room of Rooms.findAllByTypesAndDiscussionAndTeam({ types, discussions, teams }, findOptions)) { + const converted = await roomConverter.convertRoomRaw(room); + if (converted) { + rooms.push(converted); + } + } + + return rooms; + } + protected async getDirectByUsernames(usernames: Array, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting direct room by usernames: "${usernames}"`); const room = await Rooms.findDirectRoomContainingAllUsernames(usernames, {}); diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 4152b12bbe5..c81792a0bb3 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -20,6 +20,106 @@ export class AppRoomsConverter { return this.convertRoom(room); } + convertRoomRaw(room) { + if (!room) { + return undefined; + } + + const mapUserLookup = (user) => + user && { + _id: user._id ?? user.id, + ...(user.username && { username: user.username }), + ...(user.name && { name: user.name }), + }; + + const map = { + id: '_id', + displayName: 'fname', + slugifiedName: 'name', + members: 'members', + userIds: 'uids', + usernames: 'usernames', + messageCount: 'msgs', + createdAt: 'ts', + updatedAt: '_updatedAt', + closedAt: 'closedAt', + lastModifiedAt: 'lm', + customFields: 'customFields', + livechatData: 'livechatData', + isWaitingResponse: 'waitingResponse', + isOpen: 'open', + description: 'description', + source: 'source', + closer: 'closer', + teamId: 'teamId', + isTeamMain: 'teamMain', + isDefault: 'default', + isReadOnly: 'ro', + contactId: 'contactId', + departmentId: 'departmentId', + parentRoomId: 'prid', + visitor: (data) => { + const { v } = data; + if (!v) { + return undefined; + } + + delete data.v; + + const { _id: id, ...rest } = v; + + return { + id, + ...rest, + }; + }, + displaySystemMessages: (data) => { + const { sysMes } = data; + delete data.sysMes; + return typeof sysMes === 'undefined' ? true : sysMes; + }, + type: (data) => { + const result = this._convertTypeToApp(data.t); + delete data.t; + return result; + }, + creator: (data) => { + if (!data.u) { + return undefined; + } + const creator = mapUserLookup(data.u); + delete data.u; + return creator; + }, + closedBy: (data) => { + if (!data.closedBy) { + return undefined; + } + const { closedBy } = data; + delete data.closedBy; + return mapUserLookup(closedBy); + }, + servedBy: (data) => { + if (!data.servedBy) { + return undefined; + } + const { servedBy } = data; + delete data.servedBy; + return mapUserLookup(servedBy); + }, + responseBy: (data) => { + if (!data.responseBy) { + return undefined; + } + const { responseBy } = data; + delete data.responseBy; + return mapUserLookup(responseBy); + }, + }; + + return transformMappedData(room, map); + } + async __getCreator(user) { if (!user) { return; @@ -54,6 +154,7 @@ export class AppRoomsConverter { username: visitor.username, token: visitor.token, status: visitor.status || 'online', + activity: visitor.activity, ...(lastMessageTs && { lastMessageTs }), ...(phone && { phone }), }; @@ -200,6 +301,8 @@ export class AppRoomsConverter { description: 'description', source: 'source', closer: 'closer', + teamId: 'teamId', + isTeamMain: 'teamMain', isDefault: (room) => { const result = !!room.default; delete room.default; diff --git a/apps/meteor/app/apps/server/converters/visitors.js b/apps/meteor/app/apps/server/converters/visitors.js index c8fb0b7c4a2..00b8b3888ae 100644 --- a/apps/meteor/app/apps/server/converters/visitors.js +++ b/apps/meteor/app/apps/server/converters/visitors.js @@ -36,6 +36,7 @@ export class AppVisitorsConverter { visitorEmails: 'visitorEmails', livechatData: 'livechatData', status: 'status', + activity: 'activity', }; return transformMappedData(visitor, map); diff --git a/packages/apps-engine/src/definition/accessors/IRoomRead.ts b/packages/apps-engine/src/definition/accessors/IRoomRead.ts index 0b540d0a5c1..ae086150784 100644 --- a/packages/apps-engine/src/definition/accessors/IRoomRead.ts +++ b/packages/apps-engine/src/definition/accessors/IRoomRead.ts @@ -1,6 +1,6 @@ -import type { GetMessagesOptions } from '../../server/bridges/RoomBridge'; +import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '../../server/bridges/RoomBridge'; import type { IMessageRaw } from '../messages/index'; -import type { IRoom } from '../rooms/index'; +import type { IRoom, IRoomRaw } from '../rooms/index'; import type { IUser } from '../users/index'; /** @@ -61,6 +61,13 @@ export interface IRoomRead { */ getMembers(roomId: string): Promise>; + /** + * Retrieves rooms in the workspace, optionally filtered by type and whether they are discussions or part of teams. + * + * @returns a list of raw rooms, or undefined if the app does not have the permission to view all rooms + */ + getAllRooms(filters?: GetRoomsFilters, options?: GetRoomsOptions): Promise | undefined>; + /** * Gets a direct room with all usernames * @param usernames all usernames belonging to the direct room diff --git a/packages/apps-engine/src/definition/livechat/IVisitor.ts b/packages/apps-engine/src/definition/livechat/IVisitor.ts index 64445df8ae5..dc04777b8e8 100644 --- a/packages/apps-engine/src/definition/livechat/IVisitor.ts +++ b/packages/apps-engine/src/definition/livechat/IVisitor.ts @@ -11,6 +11,7 @@ export interface IVisitor { phone?: Array; visitorEmails?: Array; status?: string; + activity?: string[]; customFields?: { [key: string]: any }; livechatData?: { [key: string]: any }; } diff --git a/packages/apps-engine/src/definition/rooms/IRoom.ts b/packages/apps-engine/src/definition/rooms/IRoom.ts index 61c3ac23cb1..b32f71bc994 100644 --- a/packages/apps-engine/src/definition/rooms/IRoom.ts +++ b/packages/apps-engine/src/definition/rooms/IRoom.ts @@ -7,6 +7,8 @@ export interface IRoom { slugifiedName: string; type: RoomType; creator: IUser; + teamId?: string; + isTeamMain?: boolean; /** * @deprecated usernames will be removed on version 2.0.0 */ diff --git a/packages/apps-engine/src/definition/rooms/IRoomRaw.ts b/packages/apps-engine/src/definition/rooms/IRoomRaw.ts new file mode 100644 index 00000000000..1ffed4b9ada --- /dev/null +++ b/packages/apps-engine/src/definition/rooms/IRoomRaw.ts @@ -0,0 +1,43 @@ +import type { IVisitor } from '../livechat'; +import type { IOmnichannelSource, IVisitorChannelInfo } from '../livechat/ILivechatRoom'; +import type { IUserLookup } from '../users'; +import type { RoomType } from './RoomType'; + +/** + * A lightweight representation of a room without resolving relational data. + * This is intended for listing operations to avoid additional database lookups. + */ +export interface IRoomRaw { + id: string; + slugifiedName: string; + displayName?: string; + type: RoomType; + creator?: IUserLookup; + members?: Array; + userIds?: Array; + usernames?: Array; + isDefault?: boolean; + isReadOnly?: boolean; + displaySystemMessages?: boolean; + messageCount?: number; + createdAt?: Date; + updatedAt?: Date; + closedAt?: Date; + lastModifiedAt?: Date; + description?: string; + customFields?: { [key: string]: any }; + parentRoomId?: string; + teamId?: string; + isTeamMain?: boolean; + livechatData?: { [key: string]: any }; + isWaitingResponse?: boolean; + isOpen?: boolean; + closer?: 'user' | 'visitor' | 'bot'; + closedBy?: IUserLookup; + servedBy?: IUserLookup; + responseBy?: IUserLookup; + source?: IOmnichannelSource; + visitor?: Pick & IVisitorChannelInfo; + departmentId?: string; + contactId?: string; +} diff --git a/packages/apps-engine/src/definition/rooms/index.ts b/packages/apps-engine/src/definition/rooms/index.ts index 35b15d36fd1..a7212f8ee82 100644 --- a/packages/apps-engine/src/definition/rooms/index.ts +++ b/packages/apps-engine/src/definition/rooms/index.ts @@ -5,10 +5,12 @@ import { IPreRoomCreateModify } from './IPreRoomCreateModify'; import { IPreRoomCreatePrevent } from './IPreRoomCreatePrevent'; import { IPreRoomDeletePrevent } from './IPreRoomDeletePrevent'; import { IRoom } from './IRoom'; +import { IRoomRaw } from './IRoomRaw'; import { RoomType } from './RoomType'; export { IRoom, + IRoomRaw, RoomType, IPostRoomCreate, IPostRoomDeleted, diff --git a/packages/apps-engine/src/server/accessors/RoomRead.ts b/packages/apps-engine/src/server/accessors/RoomRead.ts index 9bb3f107ce2..01e10cee8b9 100644 --- a/packages/apps-engine/src/server/accessors/RoomRead.ts +++ b/packages/apps-engine/src/server/accessors/RoomRead.ts @@ -1,9 +1,9 @@ import type { IRoomRead } from '../../definition/accessors'; import type { IMessageRaw } from '../../definition/messages'; -import type { IRoom } from '../../definition/rooms'; +import type { IRoom, IRoomRaw } from '../../definition/rooms'; import type { IUser } from '../../definition/users'; import type { RoomBridge } from '../bridges'; -import { type GetMessagesOptions, GetMessagesSortableFields } from '../bridges/RoomBridge'; +import { type GetMessagesOptions, type GetRoomsFilters, type GetRoomsOptions, GetMessagesSortableFields } from '../bridges/RoomBridge'; export class RoomRead implements IRoomRead { constructor( @@ -46,6 +46,22 @@ export class RoomRead implements IRoomRead { return this.roomBridge.doGetMembers(roomId, this.appId); } + public getAllRooms(filters: GetRoomsFilters = {}, { limit = 100, skip = 0 }: GetRoomsOptions = {}): Promise | undefined> { + if (!Number.isFinite(limit) || limit <= 0 || limit > 100) { + throw new Error(`Invalid limit provided. Expected number between 1 and 100, got ${limit}`); + } + + if (!Number.isFinite(skip) || skip < 0) { + throw new Error(`Invalid skip provided. Expected number >= 0, got ${skip}`); + } + + return this.roomBridge.doGetAllRooms( + filters, + { limit, skip }, + this.appId, + ); + } + public getDirectByUsernames(usernames: Array): Promise { return this.roomBridge.doGetDirectByUsernames(usernames, this.appId); } diff --git a/packages/apps-engine/src/server/bridges/RoomBridge.ts b/packages/apps-engine/src/server/bridges/RoomBridge.ts index 7ddfb13e221..66f4ff1a878 100644 --- a/packages/apps-engine/src/server/bridges/RoomBridge.ts +++ b/packages/apps-engine/src/server/bridges/RoomBridge.ts @@ -1,6 +1,6 @@ import { BaseBridge } from './BaseBridge'; import type { IMessage, IMessageRaw } from '../../definition/messages'; -import type { IRoom } from '../../definition/rooms'; +import type { IRoom, IRoomRaw, RoomType } from '../../definition/rooms'; import type { IUser } from '../../definition/users'; import { PermissionDeniedError } from '../errors/PermissionDeniedError'; import { AppPermissionManager } from '../managers/AppPermissionManager'; @@ -15,6 +15,39 @@ export type GetMessagesOptions = { showThreadMessages: boolean; }; +/** + * Filters for querying rooms in the system. + */ +export type GetRoomsFilters = { + /** + * When specified, only rooms matching the provided types will be returned. + */ + types?: Array; + /** + * Filter to include or exclude discussion rooms. + * + * When undefined (default), discussions are included in the result set. + * + * When true, ONLY discussions are included in the result set (remove non-discussions). + * When false, discussion rooms are excluded from the result set. + */ + discussions?: boolean; + /** + * Filter to include or exclude team main rooms. + * + * When undefined (default), team main rooms are included in the result set. + * + * When true, ONLY team main rooms are included in the result set (remove non-teams). + * When false, team main rooms are excluded from the result set. + */ + teams?: boolean; +}; + +export type GetRoomsOptions = { + limit?: number; + skip?: number; +}; + export abstract class RoomBridge extends BaseBridge { public async doCreate(room: IRoom, members: Array, appId: string): Promise { if (this.hasWritePermission(appId)) { @@ -58,6 +91,12 @@ export abstract class RoomBridge extends BaseBridge { } } + public async doGetAllRooms(filters: GetRoomsFilters = {}, options: GetRoomsOptions = {}, appId: string): Promise | undefined> { + if (this.hasViewAllRoomsPermission(appId)) { + return this.getAllRooms(filters, options, appId); + } + } + public async doUpdate(room: IRoom, members: Array, appId: string): Promise { if (this.hasWritePermission(appId)) { return this.update(room, members, appId); @@ -138,6 +177,8 @@ export abstract class RoomBridge extends BaseBridge { protected abstract getMembers(roomId: string, appId: string): Promise>; + protected abstract getAllRooms(filters: GetRoomsFilters, options: GetRoomsOptions, appId: string): Promise>; + protected abstract update(room: IRoom, members: Array, appId: string): Promise; protected abstract createDiscussion( @@ -193,4 +234,19 @@ export abstract class RoomBridge extends BaseBridge { return false; } + + private hasViewAllRoomsPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.room['system-view-all'])) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.room['system-view-all']], + }), + ); + + return false; + } } diff --git a/packages/apps-engine/src/server/permissions/AppPermissions.ts b/packages/apps-engine/src/server/permissions/AppPermissions.ts index 483235286aa..138b0578585 100644 --- a/packages/apps-engine/src/server/permissions/AppPermissions.ts +++ b/packages/apps-engine/src/server/permissions/AppPermissions.ts @@ -44,6 +44,7 @@ export const AppPermissions = { 'room': { read: { name: 'room.read' }, write: { name: 'room.write' }, + 'system-view-all': { name: 'room.system.view-all' }, }, 'role': { read: { name: 'role.read' }, diff --git a/packages/apps-engine/tests/server/accessors/RoomRead.spec.ts b/packages/apps-engine/tests/server/accessors/RoomRead.spec.ts index 97927e7aeb9..83ff92c9b66 100644 --- a/packages/apps-engine/tests/server/accessors/RoomRead.spec.ts +++ b/packages/apps-engine/tests/server/accessors/RoomRead.spec.ts @@ -1,7 +1,7 @@ import { AsyncTest, Expect, SetupFixture } from 'alsatian'; import type { IMessageRaw } from '../../../src/definition/messages'; -import type { IRoom } from '../../../src/definition/rooms'; +import type { IRoom, IRoomRaw } from '../../../src/definition/rooms'; import type { IUser } from '../../../src/definition/users'; import { RoomRead } from '../../../src/server/accessors'; import type { RoomBridge } from '../../../src/server/bridges'; @@ -23,6 +23,7 @@ export class RoomReadAccessorTestFixture { @SetupFixture public setupFixture() { this.room = TestData.getRoom(); + this.room.id = this.room.id || 'room-id'; this.user = TestData.getUser(); this.messages = ['507f1f77bcf86cd799439011', '507f191e810c19729de860ea'].map((id) => TestData.getMessageRaw(id)); this.unreadRoomId = this.messages[0].roomId; @@ -35,6 +36,19 @@ export class RoomReadAccessorTestFixture { const theUnreadMsg = this.messages; const { unreadRoomId } = this; const { unreadUserId } = this; + const theRooms: IRoomRaw[] = [ + { + id: this.room.id, + slugifiedName: this.room.slugifiedName, + displayName: this.room.displayName, + type: this.room.type, + creator: { + _id: this.room.creator.id, + username: this.room.creator.username, + name: this.room.creator.name, + }, + }, + ]; this.mockRoomBridgeWithRoom = { doGetById(id, appId): Promise { return Promise.resolve(theRoom); @@ -54,6 +68,9 @@ export class RoomReadAccessorTestFixture { doGetMembers(name, appId): Promise> { return Promise.resolve([theUser]); }, + doGetAllRooms(filter, appId): Promise> { + return Promise.resolve(theRooms); + }, doGetMessages(roomId, options, appId): Promise { return Promise.resolve(theMessages); }, @@ -63,7 +80,7 @@ export class RoomReadAccessorTestFixture { } return Promise.resolve([]); }, - } as RoomBridge; + } as unknown as RoomBridge; } @AsyncTest() @@ -84,6 +101,20 @@ export class RoomReadAccessorTestFixture { Expect(await rr.getDirectByUsernames([this.user.username])).toBe(this.room); Expect(await rr.getMessages('testing')).toBeDefined(); Expect(await rr.getMessages('testing')).toBe(this.messages); + Expect(await rr.getAllRooms()).toBeDefined(); + Expect(await rr.getAllRooms()).toEqual([ + { + id: this.room.id, + slugifiedName: this.room.slugifiedName, + displayName: this.room.displayName, + type: this.room.type, + creator: { + _id: this.room.creator.id, + username: this.room.creator.username, + name: this.room.creator.name, + }, + }, + ]); Expect(await rr.getUnreadByUser(this.unreadRoomId, this.unreadUserId)).toBeDefined(); Expect(await rr.getUnreadByUser(this.unreadRoomId, this.unreadUserId)).toEqual(this.messages); @@ -101,4 +132,41 @@ export class RoomReadAccessorTestFixture { Expect((await rr.getMembers('testing')) as Array).not.toBeEmpty(); Expect((await rr.getMembers('testing'))[0]).toBe(this.user); } + + @AsyncTest() + public async validateGetAllRoomsEdgeCases() { + const rr = new RoomRead(this.mockRoomBridgeWithRoom, 'testing-app'); + + // Test negative limit + await Expect(async () => rr.getAllRooms({}, { limit: -1 })).toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { limit: -100 })).toThrowAsync(); + + // Test zero limit + await Expect(async () => rr.getAllRooms({}, { limit: 0 })).toThrowAsync(); + // Test non-finite limit values + await Expect(async () => rr.getAllRooms({}, { limit: NaN })).toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { limit: Infinity })).toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { limit: -Infinity })).toThrowAsync(); + + // Test limit > 100 (existing test case) + await Expect(async () => rr.getAllRooms({}, { limit: 101 })).toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { limit: 200 })).toThrowAsync(); + + // Test negative skip values + await Expect(async () => rr.getAllRooms({}, { skip: -1 })).toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { skip: -100 })).toThrowAsync(); + + // Test non-finite skip values + await Expect(async () => rr.getAllRooms({}, { skip: NaN })).toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { skip: Infinity })).toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { skip: -Infinity })).toThrowAsync(); + + // Test valid calls to ensure validation doesn't break normal behavior + await Expect(async () => rr.getAllRooms({}, { limit: 1 })).not.toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { limit: 50 })).not.toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { limit: 100 })).not.toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { skip: 0 })).not.toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { skip: 10 })).not.toThrowAsync(); + await Expect(async () => rr.getAllRooms({}, { limit: 50, skip: 10 })).not.toThrowAsync(); + } } diff --git a/packages/apps-engine/tests/test-data/bridges/roomBridge.ts b/packages/apps-engine/tests/test-data/bridges/roomBridge.ts index bf45661d0ca..661af82e0ad 100644 --- a/packages/apps-engine/tests/test-data/bridges/roomBridge.ts +++ b/packages/apps-engine/tests/test-data/bridges/roomBridge.ts @@ -1,8 +1,9 @@ import type { IMessage, IMessageRaw } from '../../../src/definition/messages'; import type { IRoom } from '../../../src/definition/rooms'; +import type { IRoomRaw } from '../../../src/definition/rooms/IRoomRaw'; import type { IUser } from '../../../src/definition/users'; import { RoomBridge } from '../../../src/server/bridges'; -import type { GetMessagesOptions } from '../../../src/server/bridges/RoomBridge'; +import type { GetMessagesOptions, GetRoomsOptions, GetRoomsFilters } from '../../../src/server/bridges/RoomBridge'; export class TestsRoomBridge extends RoomBridge { public create(room: IRoom, members: Array, appId: string): Promise { @@ -33,6 +34,10 @@ export class TestsRoomBridge extends RoomBridge { throw new Error('Method not implemented.'); } + public getAllRooms(filter: GetRoomsFilters, options: GetRoomsOptions, appId: string): Promise { + throw new Error('Method not implemented.'); + } + public getMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise { throw new Error('Method not implemented.'); } diff --git a/packages/apps/src/AppsEngine.ts b/packages/apps/src/AppsEngine.ts index 3b3f43e6e3b..4bffbbbc810 100644 --- a/packages/apps/src/AppsEngine.ts +++ b/packages/apps/src/AppsEngine.ts @@ -13,7 +13,7 @@ export type { IMessageRaw as IAppsMesssageRaw } from '@rocket.chat/apps-engine/d export { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; export type { IUser as IAppsUser } from '@rocket.chat/apps-engine/definition/users'; export type { IRole as IAppsRole } from '@rocket.chat/apps-engine/definition/roles'; -export type { IRoom as IAppsRoom } from '@rocket.chat/apps-engine/definition/rooms'; +export type { IRoom as IAppsRoom, IRoomRaw as IAppsRoomRaw } from '@rocket.chat/apps-engine/definition/rooms'; export type { ISetting as IAppsSetting } from '@rocket.chat/apps-engine/definition/settings'; export type { IUpload as IAppsUpload } from '@rocket.chat/apps-engine/definition/uploads'; export type { diff --git a/packages/apps/src/converters/IAppRoomsConverter.ts b/packages/apps/src/converters/IAppRoomsConverter.ts index 83b12ae4503..9a1fb4312ec 100644 --- a/packages/apps/src/converters/IAppRoomsConverter.ts +++ b/packages/apps/src/converters/IAppRoomsConverter.ts @@ -1,6 +1,6 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import type { IAppsRoom, IAppsLivechatRoom } from '../AppsEngine'; +import type { IAppsRoom, IAppsLivechatRoom, IAppsRoomRaw } from '../AppsEngine'; export interface IAppRoomsConverter { convertById(roomId: IRoom['_id']): Promise; @@ -8,6 +8,8 @@ export interface IAppRoomsConverter { convertRoom(room: undefined | null): Promise; convertRoom(room: IRoom): Promise; convertRoom(room: IRoom | undefined | null): Promise; + convertRoomRaw(room: IRoom): Promise; + convertRoomRaw(room: IRoom | undefined | null): Promise; convertAppRoom(room: undefined | null): Promise; convertAppRoom(room: IAppsRoom): Promise; convertAppRoom(room: IAppsRoom, isPartial: boolean): Promise>; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 878d8be10a9..616d58b289e 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -707,6 +707,7 @@ "Apps_Permissions_experimental_default": "Use experimental APIs", "Apps_Permissions_persistence": "Store internal data in the database", "Apps_Permissions_room_read": "Access room information", + "Apps_Permissions_room_system_view-all": "View all rooms in the workspace", "Apps_Permissions_room_write": "Create and modify rooms", "Apps_Permissions_scheduler": "Register and maintain scheduled jobs", "Apps_Permissions_server-setting_read": "Access settings in this server", diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index e0d2f411c08..6de80181b2b 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -29,7 +29,17 @@ export interface IChannelsWithNumberOfMessagesBetweenDate { } export interface IRoomsModel extends IBaseModel { + findAllByTypesAndDiscussionAndTeam( + filters?: { + types?: Array; + discussions?: boolean; + teams?: boolean; + }, + findOptions?: FindOptions, + ): FindCursor; + isAbacAttributeInUse(key: string, values: string[]): Promise; + findOneByRoomIdAndUserId(rid: IRoom['_id'], uid: IUser['_id'], options?: FindOptions): Promise; findManyByRoomIds(roomIds: Array, options?: FindOptions): FindCursor; diff --git a/packages/models/src/models/Rooms.ts b/packages/models/src/models/Rooms.ts index 34a7d6f7edc..9b78b5ac977 100644 --- a/packages/models/src/models/Rooms.ts +++ b/packages/models/src/models/Rooms.ts @@ -114,6 +114,10 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { key: { 'abacAttributes.key': 1, 'abacAttributes.values': 1 }, partialFilterExpression: { abacAttributes: { $exists: true } }, }, + { + key: { teamMain: 1 }, + sparse: true, + }, ]; } @@ -2317,6 +2321,33 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ]); } + findAllByTypesAndDiscussionAndTeam( + filters: { + types?: Array; + discussions?: boolean; + teams?: boolean; + } = {}, + options: FindOptions = {}, + ): FindCursor { + const { types, discussions, teams } = filters; + + const query: Filter = {}; + + if (types) { + query.t = { $in: types }; + } + + if (typeof discussions !== 'undefined') { + query.prid = { $exists: discussions }; + } + + if (typeof teams !== 'undefined') { + query.teamMain = { $exists: teams }; + } + + return this.find(query, options); + } + resetRoomKeyAndSetE2EEQueueByRoomId( roomId: string, e2eKeyId: string,