diff --git a/.changeset/poor-pants-suffer.md b/.changeset/poor-pants-suffer.md new file mode 100644 index 0000000000000000000000000000000000000000..c25b090ef71d61f5a1ea1638aaae92caba5fc80a --- /dev/null +++ b/.changeset/poor-pants-suffer.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: Rocket.Chat.Apps using wrong id parameter to emit settings + diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index 24dfb2a7b0ac928dc57e2f3c74c9ac0993d3746b..f6a50a9e6ab8b5bd80b9c4a038c83e526c0fee1b 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -104,7 +104,7 @@ export class AppMessageBridge extends MessageBridge { protected async typing({ scope, id, username, isTyping }: ITypingDescriptor): Promise<void> { switch (scope) { case 'room': - notifications.notifyRoom(id, 'typing', username, isTyping); + notifications.notifyRoom(id, 'typing', username!, isTyping); return; default: throw new Error('Unrecognized typing scope provided'); diff --git a/apps/meteor/app/authorization/server/methods/addUserToRole.ts b/apps/meteor/app/authorization/server/methods/addUserToRole.ts index b97935e2fbe7db77b160a89f9fa4205366cfe14b..c912c9854408803510ea8f7ddf5c366323b90e02 100644 --- a/apps/meteor/app/authorization/server/methods/addUserToRole.ts +++ b/apps/meteor/app/authorization/server/methods/addUserToRole.ts @@ -1,5 +1,5 @@ import { Meteor } from 'meteor/meteor'; -import type { IRole, IUser, IRoom } from '@rocket.chat/core-typings'; +import type { IRole, IUser } from '@rocket.chat/core-typings'; import { Roles, Users } from '@rocket.chat/models'; import { api } from '@rocket.chat/core-services'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; @@ -11,12 +11,12 @@ import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarning declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'authorization:addUserToRole'(roleId: IRole['_id'], username: IUser['username'], scope: IRoom['_id'] | undefined): Promise<boolean>; + 'authorization:addUserToRole'(roleId: IRole['_id'], username: IUser['username'], scope: string | undefined): Promise<boolean>; } } Meteor.methods<ServerMethods>({ - async 'authorization:addUserToRole'(roleId: IRole['_id'], username: IUser['username'], scope: IRoom['_id'] | undefined) { + async 'authorization:addUserToRole'(roleId: IRole['_id'], username: IUser['username'], scope) { const userId = Meteor.userId(); if (!userId || !(await hasPermissionAsync(userId, 'access-permissions'))) { diff --git a/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts b/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts index ca9c685796d896a47cfb3bcb287783e76cc85784..c2485d353868b4fcc9af5b63445ff240829528a2 100644 --- a/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts +++ b/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts @@ -11,7 +11,7 @@ import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarning declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'authorization:removeUserFromRole'(roleId: IRole['_id'], username: IUser['username'], scope: undefined): Promise<boolean>; + 'authorization:removeUserFromRole'(roleId: IRole['_id'], username: IUser['username'], scope?: string): Promise<boolean>; } } @@ -85,7 +85,7 @@ Meteor.methods<ServerMethods>({ username, }, scope, - }; + } as const; if (settings.get('UI_DisplayRoles')) { void api.broadcast('user.roleUpdate', event); } diff --git a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts index e1f716e914cf2aa167c9beb51a23459bc32b04f9..28ea23333d6410403e11816ebf8e9b253e11d5ca 100644 --- a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts +++ b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts @@ -43,7 +43,7 @@ Meteor.methods<ServerMethods>({ await IntegrationHistory.removeByIntegrationId(integrationId); - notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed' }); + notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed', id: integrationId }); return true; }, diff --git a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts index 29a7523cde8a35d04f0ad89bb305dba990e6f8e4..da19c7826d1a0f283b6dfa0af50ce7a6e362fd70 100644 --- a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts +++ b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts @@ -6,28 +6,18 @@ import { sdk } from '../../../../utils/client/lib/SDKClient'; const departments = new Set(); -type ILivechatInquiryWithType = ILivechatInquiryRecord & { type?: 'added' | 'removed' | 'changed' }; - const events = { - added: (inquiry: ILivechatInquiryWithType) => { - delete inquiry.type; + added: (inquiry: ILivechatInquiryRecord) => { departments.has(inquiry.department) && LivechatInquiry.insert({ ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); }, - changed: (inquiry: ILivechatInquiryWithType) => { + changed: (inquiry: ILivechatInquiryRecord) => { if (inquiry.status !== 'queued' || (inquiry.department && !departments.has(inquiry.department))) { return LivechatInquiry.remove(inquiry._id); } - delete inquiry.type; + LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); }, - removed: (inquiry: ILivechatInquiryWithType) => LivechatInquiry.remove(inquiry._id), -}; - -const updateCollection = (inquiry: ILivechatInquiryWithType) => { - if (!inquiry.type) { - return; - } - events[inquiry.type](inquiry); + removed: (inquiry: ILivechatInquiryRecord) => LivechatInquiry.remove(inquiry._id), }; const getInquiriesFromAPI = async () => { @@ -42,7 +32,13 @@ const removeListenerOfDepartment = (departmentId: ILivechatDepartment['_id']) => const appendListenerToDepartment = (departmentId: ILivechatDepartment['_id']) => { departments.add(departmentId); - sdk.stream('livechat-inquiry-queue-observer', [`department/${departmentId}`], updateCollection); + sdk.stream('livechat-inquiry-queue-observer', [`department/${departmentId}`], (args) => { + if (!('type' in args)) { + return; + } + const { type, ...inquiry } = args; + events[args.type](inquiry); + }); return () => removeListenerOfDepartment(departmentId); }; const addListenerForeachDepartment = (departments: ILivechatDepartment['_id'][] = []) => { @@ -61,7 +57,13 @@ const getAgentsDepartments = async (userId: IOmnichannelAgent['_id']) => { const removeGlobalListener = () => sdk.stop('livechat-inquiry-queue-observer', 'public'); const addGlobalListener = () => { - sdk.stream('livechat-inquiry-queue-observer', ['public'], updateCollection); + sdk.stream('livechat-inquiry-queue-observer', ['public'], (args) => { + if (!('type' in args)) { + return; + } + const { type, ...inquiry } = args; + events[args.type](inquiry); + }); return removeGlobalListener; }; diff --git a/apps/meteor/app/notifications/server/lib/Notifications.ts b/apps/meteor/app/notifications/server/lib/Notifications.ts index ccb9ba64c7e605e39f9ba9ddec1a2bd473445b90..7a58894addab4f37bd28d38227665ec2d988dd77 100644 --- a/apps/meteor/app/notifications/server/lib/Notifications.ts +++ b/apps/meteor/app/notifications/server/lib/Notifications.ts @@ -8,7 +8,7 @@ import { Streamer } from '../../../../server/modules/streamer/streamer.module'; import './Presence'; -class Stream extends Streamer { +class Stream extends Streamer<'local'> { registerPublication(name: string, fn: (eventName: string, options: boolean | { useCollection?: boolean; args?: any }) => void): void { Meteor.publish(name, function (eventName, options) { return fn.call(this, eventName, options); diff --git a/apps/meteor/app/notifications/server/lib/Presence.ts b/apps/meteor/app/notifications/server/lib/Presence.ts index e772671303ca7ae3cc89b2b0f36a957313395b80..304e533d599a000426c19bfcc83a59cdf8e7a6c9 100644 --- a/apps/meteor/app/notifications/server/lib/Presence.ts +++ b/apps/meteor/app/notifications/server/lib/Presence.ts @@ -19,13 +19,13 @@ const e = new Emitter<{ const clients = new WeakMap<Connection, UserPresence>(); class UserPresence { - private readonly streamer: IStreamer; + private readonly streamer: IStreamer<'user-presence'>; private readonly publication: IPublication; private readonly listeners: Set<string>; - constructor(publication: IPublication, streamer: IStreamer) { + constructor(publication: IPublication, streamer: IStreamer<'user-presence'>) { this.listeners = new Set(); this.publication = publication; this.streamer = streamer; @@ -54,7 +54,7 @@ class UserPresence { clients.delete(this.publication.connection); } - static getClient(publication: IPublication, streamer: IStreamer): [UserPresence, boolean] { + static getClient(publication: IPublication, streamer: IStreamer<'user-presence'>): [UserPresence, boolean] { const { connection } = publication; const stored = clients.get(connection); @@ -70,8 +70,8 @@ class UserPresence { export class StreamPresence { // eslint-disable-next-line @typescript-eslint/naming-convention - static getInstance(Streamer: IStreamerConstructor, name = 'user-presence'): IStreamer { - return new (class StreamPresence extends Streamer { + static getInstance(Streamer: IStreamerConstructor, name = 'user-presence'): IStreamer<'user-presence'> { + return new (class StreamPresence extends Streamer<'user-presence'> { async _publish( publication: IPublication, _eventName: string, diff --git a/apps/meteor/app/search/server/search.internalService.ts b/apps/meteor/app/search/server/search.internalService.ts index 678da377a5f685701a5bf6980cc67f222714c6f6..727d26a7268619f654707a2687ca715d3a962e71 100644 --- a/apps/meteor/app/search/server/search.internalService.ts +++ b/apps/meteor/app/search/server/search.internalService.ts @@ -13,13 +13,13 @@ class Search extends ServiceClassInternal { constructor() { super(); - this.onEvent('watch.users', async ({ clientAction, data, id }) => { + this.onEvent('watch.users', async ({ clientAction, id, ...rest }) => { if (clientAction === 'removed') { searchEventService.promoteEvent('user.delete', id, undefined); return; } - const user = data ?? (await Users.findOneById(id)); + const user = ('data' in rest && rest.data) || (await Users.findOneById(id)); searchEventService.promoteEvent('user.save', id, user); }); diff --git a/apps/meteor/client/lib/userData.ts b/apps/meteor/client/lib/userData.ts index 0d7a44f9fb40c3097acb29627a39c2d36df7f575..ba6d62303280fb5c81941c89fe4997d4e0ffe3e0 100644 --- a/apps/meteor/client/lib/userData.ts +++ b/apps/meteor/client/lib/userData.ts @@ -65,7 +65,7 @@ export const synchronizeUserData = async (uid: IUser['_id']): Promise<RawUserDat case 'inserted': // eslint-disable-next-line @typescript-eslint/no-unused-vars const { type, id, ...user } = data; - Users.insert(user as IUser); + Users.insert(user as unknown as IUser); break; case 'updated': diff --git a/apps/meteor/client/startup/incomingMessages.ts b/apps/meteor/client/startup/incomingMessages.ts index ad3b8cd82ef972a0c90ef3508d98c9f52303f518..88e83b9cf5e85eb8727ffbbcf7eef8154b4835af 100644 --- a/apps/meteor/client/startup/incomingMessages.ts +++ b/apps/meteor/client/startup/incomingMessages.ts @@ -1,4 +1,4 @@ -import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { ChatMessage } from '../../app/models/client'; @@ -20,16 +20,16 @@ Meteor.startup(() => { }); CachedCollectionManager.onLogin(() => { - Notifications.onUser('subscriptions-changed', (_action: unknown, sub: ISubscription) => { + Notifications.onUser('subscriptions-changed', (_action, sub) => { ChatMessage.update( { rid: sub.rid, - ...(sub?.ignored ? { 'u._id': { $nin: sub.ignored } } : { ignored: { $exists: true } }), + ...('ignored' in sub && sub.ignored ? { 'u._id': { $nin: sub.ignored } } : { ignored: { $exists: true } }), }, { $unset: { ignored: true } }, { multi: true }, ); - if (sub?.ignored) { + if ('ignored' in sub && sub.ignored) { ChatMessage.update( { 'rid': sub.rid, 't': { $ne: 'command' }, 'u._id': { $in: sub.ignored } }, { $set: { ignored: true } }, diff --git a/apps/meteor/client/startup/notifications/konchatNotifications.ts b/apps/meteor/client/startup/notifications/konchatNotifications.ts index 0a949953e44b8a8625c0306493e68ffea73dcbe8..20b6ba05805957026e568817adc1e2a15f654574 100644 --- a/apps/meteor/client/startup/notifications/konchatNotifications.ts +++ b/apps/meteor/client/startup/notifications/konchatNotifications.ts @@ -1,4 +1,4 @@ -import type { ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, ISubscription, IUser } from '@rocket.chat/core-typings'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; @@ -12,7 +12,7 @@ import { RoomManager } from '../../lib/RoomManager'; import { fireGlobalEvent } from '../../lib/utils/fireGlobalEvent'; import { isLayoutEmbedded } from '../../lib/utils/isLayoutEmbedded'; -const notifyNewRoom = async (sub: ISubscription): Promise<void> => { +const notifyNewRoom = async (sub: AtLeast<ISubscription, 'rid'>): Promise<void> => { const user = Meteor.user() as IUser | null; if (!user || user.status === 'busy') { return; @@ -76,7 +76,10 @@ Meteor.startup(() => { void notifyNewRoom(sub); }); - Notifications.onUser('subscriptions-changed', (_, sub) => { + Notifications.onUser('subscriptions-changed', (action, sub) => { + if (action === 'removed') { + return; + } void notifyNewRoom(sub); }); }); diff --git a/apps/meteor/client/startup/notifications/updateAvatar.ts b/apps/meteor/client/startup/notifications/updateAvatar.ts index b8d7ed6dbe4a52d324e639f1628686dbfa27db97..b26e184aca205d2212b1af4072a278cee617abc9 100644 --- a/apps/meteor/client/startup/notifications/updateAvatar.ts +++ b/apps/meteor/client/startup/notifications/updateAvatar.ts @@ -4,7 +4,9 @@ import { Notifications } from '../../../app/notifications/client'; Meteor.startup(() => { Notifications.onLogged('updateAvatar', (data) => { - const { username, etag } = data; - username && Meteor.users.update({ username }, { $set: { avatarETag: etag } }); + if ('username' in data) { + const { username, etag } = data; + username && Meteor.users.update({ username }, { $set: { avatarETag: etag } }); + } }); }); diff --git a/apps/meteor/client/startup/userRoles.ts b/apps/meteor/client/startup/userRoles.ts index 81fd1662eae5fda46502c1c972a47fff45658752..fc923989f2d77f863da9d095849365ae494425de 100644 --- a/apps/meteor/client/startup/userRoles.ts +++ b/apps/meteor/client/startup/userRoles.ts @@ -23,6 +23,9 @@ Meteor.startup(() => { Notifications.onLogged('roles-change', (role) => { if (role.type === 'added') { if (!role.scope) { + if (!role.u) { + return; + } UserRoles.upsert({ _id: role.u._id }, { $addToSet: { roles: role._id }, $set: { username: role.u.username } }); ChatMessage.update({ 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); } @@ -32,6 +35,9 @@ Meteor.startup(() => { if (role.type === 'removed') { if (!role.scope) { + if (!role.u) { + return; + } UserRoles.update({ _id: role.u._id }, { $pull: { roles: role._id } }); ChatMessage.update({ 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); } diff --git a/apps/meteor/client/views/room/components/body/hooks/useRoomRolesManagement.ts b/apps/meteor/client/views/room/components/body/hooks/useRoomRolesManagement.ts index 13811515165f3d22e2918d626bf664d18e16b92d..81710d216a6661d70fcc5bf513df5a0595449f62 100644 --- a/apps/meteor/client/views/room/components/body/hooks/useRoomRolesManagement.ts +++ b/apps/meteor/client/views/room/components/body/hooks/useRoomRolesManagement.ts @@ -1,4 +1,4 @@ -import type { IRole, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { useMethod, useStream } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; @@ -52,36 +52,25 @@ export const useRoomRolesManagement = (rid: IRoom['_id']): void => { useEffect( () => - subscribeToNotifyLoggedIn( - 'roles-change', - ({ - type, - ...role - }: { - type: 'added' | 'removed' | 'changed'; - _id: IRole['_id']; - u: { - _id: IUser['_id']; - username: IUser['username']; - name: IUser['name']; - }; - scope?: IRoom['_id']; - }) => { - if (!role.scope) { - return; - } + subscribeToNotifyLoggedIn('roles-change', ({ type, ...role }) => { + if (!role.scope) { + return; + } + + if (!role.u?._id) { + return; + } - switch (type) { - case 'added': - RoomRoles.upsert({ 'rid': role.scope, 'u._id': role.u._id }, { $setOnInsert: { u: role.u }, $addToSet: { roles: role._id } }); - break; + switch (type) { + case 'added': + RoomRoles.upsert({ 'rid': role.scope, 'u._id': role.u._id }, { $setOnInsert: { u: role.u }, $addToSet: { roles: role._id } }); + break; - case 'removed': - RoomRoles.update({ 'rid': role.scope, 'u._id': role.u._id }, { $pull: { roles: role._id } }); - break; - } - }, - ), + case 'removed': + RoomRoles.update({ 'rid': role.scope, 'u._id': role.u._id }, { $pull: { roles: role._id } }); + break; + } + }), [subscribeToNotifyLoggedIn], ); diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index a36bc688fe6ece2929d24e8ae741c277640333ba..a78e42c2d23bb6b946a5b15a80bdbc78a77ac049 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -1,4 +1,4 @@ -import type { IOmnichannelRoom, IRoom } from '@rocket.chat/core-typings'; +import type { IRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { useRoute, useStream } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; @@ -41,7 +41,7 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { return; } - return subscribeToRoom(rid, (room: IRoom | IOmnichannelRoom) => { + return subscribeToRoom(rid, (room) => { queryClient.setQueryData(['rooms', rid], room); }); }, [subscribeToRoom, rid, queryClient, room]); diff --git a/apps/meteor/definition/externals/meteor/rocketchat-streamer.d.ts b/apps/meteor/definition/externals/meteor/rocketchat-streamer.d.ts index ef863f50f453cb3e58ae73cfb44a900885c718ed..1de8f5413b9fc5725be0a4e8f4e2f7788ae6d508 100644 --- a/apps/meteor/definition/externals/meteor/rocketchat-streamer.d.ts +++ b/apps/meteor/definition/externals/meteor/rocketchat-streamer.d.ts @@ -1,4 +1,5 @@ declare module 'meteor/rocketchat:streamer' { + import type { StreamNames, StreamKeys, StreamerCallbackArgs } from '@rocket.chat/ui-contexts'; import type { Subscription } from 'meteor/meteor'; type Connection = any; @@ -20,7 +21,7 @@ declare module 'meteor/rocketchat:streamer' { }; } - type Rule = (this: IPublication, eventName: string, ...args: any) => Promise<boolean | object>; + type Rule<K extends string = string> = (this: IPublication, eventName: K, ...args: any) => Promise<boolean | object>; interface IRules { [k: string]: Rule; @@ -39,22 +40,27 @@ declare module 'meteor/rocketchat:streamer' { allowed: boolean | object, ) => string | false; - interface IStreamer { + interface IStreamer<N extends StreamNames> { serverOnly: boolean; subscriptions: Set<DDPSubscription>; subscriptionName: string; - allowEmit(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; + allowEmit<K extends StreamKeys<N>>(eventName: K, fn?: Rule | 'all' | 'none' | 'logged'): void; + allowEmit(rule: Rule<StreamKeys<N>> | 'all' | 'none' | 'logged'): void; - allowWrite(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; + allowWrite<K extends StreamKeys<N>>(eventName: K | boolean, fn: Rule | 'all' | 'none' | 'logged'): void; + allowWrite(rule: Rule<StreamKeys<N>> | 'all' | 'none' | 'logged'): void; - allowRead(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; + allowRead<K extends StreamKeys<N>>(eventName: K | boolean, fn: Rule | 'all' | 'none' | 'logged'): void; + allowRead(rule: Rule<StreamKeys<N>> | 'all' | 'none' | 'logged'): void; - emit(event: string, ...data: any[]): void; + emit<K extends StreamKeys<N>>(event: K, ...data: StreamerCallbackArgs<N, K>): void; - on(event: string, fn: (...data: any[]) => void): void; + on<K extends StreamKeys<N>>(event: K, fn: (...data: any[]) => void): void; + + on(event: '_afterPublish', fn: (streamer: this, ...data: any[]) => void): void; removeSubscription(subscription: DDPSubscription, eventName: string): void; @@ -66,7 +72,7 @@ declare module 'meteor/rocketchat:streamer' { _emit(eventName: string, args: any[], origin: Connection | undefined, broadcast: boolean, transform?: TransformMessage): boolean; - emitWithoutBroadcast(event: string, ...data: any[]): void; + emitWithoutBroadcast<K extends StreamKeys<N>>(event: K, ...data: StreamerCallbackArgs<N, K>): void; changedPayload(collection: string, id: string, fields: Record<string, any>): string | false; @@ -74,6 +80,6 @@ declare module 'meteor/rocketchat:streamer' { } interface IStreamerConstructor { - new (name: string, options?: { retransmit?: boolean; retransmitToSelf?: boolean }): IStreamer; + new <N extends StreamNames>(name: N, options?: { retransmit?: boolean; retransmitToSelf?: boolean }): IStreamer<N>; } } diff --git a/apps/meteor/ee/app/canned-responses/client/startup/responses.js b/apps/meteor/ee/app/canned-responses/client/startup/responses.js index 29bae2461924aeb2e565b64e28af87e704b86867..1720799575c7416c77fca99226a738fd9e99dae5 100644 --- a/apps/meteor/ee/app/canned-responses/client/startup/responses.js +++ b/apps/meteor/ee/app/canned-responses/client/startup/responses.js @@ -7,8 +7,7 @@ import { CannedResponse } from '../collections/CannedResponse'; import { sdk } from '../../../../../app/utils/client/lib/SDKClient'; const events = { - changed: (response) => { - delete response.type; + changed: ({ type, ...response }) => { CannedResponse.upsert({ _id: response._id }, response); }, removed: (response) => CannedResponse.remove({ _id: response._id }), diff --git a/apps/meteor/ee/app/canned-responses/server/hooks/onSaveAgentDepartment.ts b/apps/meteor/ee/app/canned-responses/server/hooks/onSaveAgentDepartment.ts index 73111ef8b910a4f0a80e1b0bcfb0bdd7776f7fa8..f8f8b442bf43568f740b294195bd6f4644ea6b12 100644 --- a/apps/meteor/ee/app/canned-responses/server/hooks/onSaveAgentDepartment.ts +++ b/apps/meteor/ee/app/canned-responses/server/hooks/onSaveAgentDepartment.ts @@ -7,7 +7,7 @@ callbacks.add( 'livechat.saveAgentDepartment', async (options: Record<string, any>): Promise<any> => { const { departmentId, agentsId } = options; - await CannedResponse.findByDepartmentId(departmentId, {}).forEach((response: any) => { + await CannedResponse.findByDepartmentId(departmentId, {}).forEach((response) => { notifications.streamCannedResponses.emit('canned-responses', { type: 'changed', ...response }, { agentsId }); }); diff --git a/apps/meteor/ee/server/apps/communication/websockets.ts b/apps/meteor/ee/server/apps/communication/websockets.ts index 8e027008c26635f3d125e24e6b2c3398a85233fb..772e1a6f8a4efda78237884d9359f506329914a6 100644 --- a/apps/meteor/ee/server/apps/communication/websockets.ts +++ b/apps/meteor/ee/server/apps/communication/websockets.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definition/settings'; import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; -import type { ISetting } from '@rocket.chat/core-typings'; import type { IStreamer } from 'meteor/rocketchat:streamer'; import { api } from '@rocket.chat/core-services'; @@ -14,13 +14,18 @@ export { AppEvents }; export class AppServerListener { private orch: AppServerOrchestrator; - engineStreamer: IStreamer; + engineStreamer: IStreamer<'apps-engine'>; - clientStreamer: IStreamer; + clientStreamer: IStreamer<'apps'>; received; - constructor(orch: AppServerOrchestrator, engineStreamer: IStreamer, clientStreamer: IStreamer, received: Map<any, any>) { + constructor( + orch: AppServerOrchestrator, + engineStreamer: IStreamer<'apps-engine'>, + clientStreamer: IStreamer<'apps'>, + received: Map<any, any>, + ) { this.orch = orch; this.engineStreamer = engineStreamer; this.clientStreamer = clientStreamer; @@ -66,8 +71,8 @@ export class AppServerListener { } } - async onAppSettingUpdated({ appId, setting }: { appId: string; setting: ISetting }): Promise<void> { - this.received.set(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting._id}`, { + async onAppSettingUpdated({ appId, setting }: { appId: string; setting: AppsSetting }): Promise<void> { + this.received.set(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting.id}`, { appId, setting, when: new Date(), @@ -76,7 +81,7 @@ export class AppServerListener { .getManager()! .getSettingsManager() .updateAppSetting(appId, setting as any); // TO-DO: fix type of `setting` - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_SETTING_UPDATED, { appId }); + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_SETTING_UPDATED, { appId, setting }); } async onAppUpdated(appId: string): Promise<void> { @@ -124,9 +129,9 @@ export class AppServerListener { } export class AppServerNotifier { - engineStreamer: IStreamer; + engineStreamer: IStreamer<'apps-engine'>; - clientStreamer: IStreamer; + clientStreamer: IStreamer<'apps'>; received: Map<any, any>; @@ -171,9 +176,9 @@ export class AppServerNotifier { void api.broadcast('apps.statusUpdate', appId, status); } - async appSettingsChange(appId: string, setting: ISetting): Promise<void> { - if (this.received.has(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting._id}`)) { - this.received.delete(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting._id}`); + async appSettingsChange(appId: string, setting: AppsSetting): Promise<void> { + if (this.received.has(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting.id}`)) { + this.received.delete(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting.id}`); return; } diff --git a/apps/meteor/ee/server/lib/roles/updateRole.ts b/apps/meteor/ee/server/lib/roles/updateRole.ts index 7271e390389ae48b5d146d02c0a914014ccc770c..9eeec6ff1485cd90e7f0cbb5fc44ea3f3367b848 100644 --- a/apps/meteor/ee/server/lib/roles/updateRole.ts +++ b/apps/meteor/ee/server/lib/roles/updateRole.ts @@ -42,6 +42,7 @@ export const updateRole = async (roleId: IRole['_id'], roleData: Omit<IRole, '_i void api.broadcast('user.roleUpdate', { type: 'changed', _id: roleId, + scope: roleData.scope, }); } diff --git a/apps/meteor/ee/server/lib/syncUserRoles.ts b/apps/meteor/ee/server/lib/syncUserRoles.ts index 542c73f959077a13765a492e78506cb87c886cf4..780710c01268c1a04190b0c824b99a95011f477e 100644 --- a/apps/meteor/ee/server/lib/syncUserRoles.ts +++ b/apps/meteor/ee/server/lib/syncUserRoles.ts @@ -30,7 +30,11 @@ function filterRoleList( return filteredRoles.filter((roleId) => rolesToFilterIn.includes(roleId)); } -function broadcastRoleChange(type: string, roleList: Array<IRole['_id']>, user: AtLeast<IUser, '_id' | 'username'>): void { +function broadcastRoleChange( + type: 'changed' | 'added' | 'removed', + roleList: Array<IRole['_id']>, + user: AtLeast<IUser, '_id' | 'username'>, +): void { if (!settings.get('UI_DisplayRoles')) { return; } diff --git a/apps/meteor/server/methods/addRoomLeader.ts b/apps/meteor/server/methods/addRoomLeader.ts index 6368506b5ac7ad8cdf258a701590d1dd46ab2d95..3f7b74b4538da2fdc3a2d64bf38fc65279295e0f 100644 --- a/apps/meteor/server/methods/addRoomLeader.ts +++ b/apps/meteor/server/methods/addRoomLeader.ts @@ -80,7 +80,6 @@ Meteor.methods<ServerMethods>({ u: { _id: user._id, username: user.username, - name: user.name, }, scope: rid, }); diff --git a/apps/meteor/server/methods/addRoomModerator.ts b/apps/meteor/server/methods/addRoomModerator.ts index 4f43db557774d45e63ca4f8d8dbd866f5a18b168..3e8d0ac240863cc431ee29e62bcb7b466090ca62 100644 --- a/apps/meteor/server/methods/addRoomModerator.ts +++ b/apps/meteor/server/methods/addRoomModerator.ts @@ -89,7 +89,7 @@ Meteor.methods<ServerMethods>({ name: fromUser.name, }, scope: rid, - }; + } as const; if (settings.get<boolean>('UI_DisplayRoles')) { void api.broadcast('user.roleUpdate', event); diff --git a/apps/meteor/server/methods/addRoomOwner.ts b/apps/meteor/server/methods/addRoomOwner.ts index 0591e28991d5bc9ae30e22be92a0a373b477971e..f74c539186d468e2c29b464aa5065cba6f585809 100644 --- a/apps/meteor/server/methods/addRoomOwner.ts +++ b/apps/meteor/server/methods/addRoomOwner.ts @@ -88,7 +88,7 @@ Meteor.methods<ServerMethods>({ name: user.name, }, scope: rid, - }; + } as const; if (settings.get('UI_DisplayRoles')) { void api.broadcast('user.roleUpdate', event); } diff --git a/apps/meteor/server/methods/removeRoomModerator.ts b/apps/meteor/server/methods/removeRoomModerator.ts index 82f1affa835101245fcba94af02bb097b8721589..0712e1ecfb4304b4cf7c2c89a2313498ab09b218 100644 --- a/apps/meteor/server/methods/removeRoomModerator.ts +++ b/apps/meteor/server/methods/removeRoomModerator.ts @@ -89,7 +89,7 @@ Meteor.methods<ServerMethods>({ name: user.name, }, scope: rid, - }; + } as const; if (settings.get('UI_DisplayRoles')) { void api.broadcast('user.roleUpdate', event); } diff --git a/apps/meteor/server/methods/removeRoomOwner.ts b/apps/meteor/server/methods/removeRoomOwner.ts index 706c2952d77af0acb897a98b11ed9ee36f4d10e6..d2fb1e0d453c5c7b4df0c1cb3d993eaf479a3df9 100644 --- a/apps/meteor/server/methods/removeRoomOwner.ts +++ b/apps/meteor/server/methods/removeRoomOwner.ts @@ -96,7 +96,7 @@ Meteor.methods<ServerMethods>({ name: user.name, }, scope: rid, - }; + } as const; if (settings.get('UI_DisplayRoles')) { void api.broadcast('user.roleUpdate', event); } diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 62b5c1b71363bfa1ab9d0970ea1f7c53b2241bb2..a1898592c5af2133da12729814b2ca04c0ede7bd 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -1,6 +1,7 @@ +import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definition/settings'; import { UserStatus, isSettingColor } from '@rocket.chat/core-typings'; import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; -import type { IUser, IRoom, VideoConference, ISetting } from '@rocket.chat/core-typings'; +import type { IUser, IRoom, VideoConference, ISetting, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { parse } from '@rocket.chat/message-parser'; import type { IServiceClass } from '@rocket.chat/core-services'; import { EnterpriseSettings } from '@rocket.chat/core-services'; @@ -10,18 +11,18 @@ import { settings } from '../../../app/settings/server/cached'; const isMessageParserDisabled = process.env.DISABLE_MESSAGE_PARSER === 'true'; -const STATUS_MAP: { [k: string]: number } = { +const STATUS_MAP = { [UserStatus.OFFLINE]: 0, [UserStatus.ONLINE]: 1, [UserStatus.AWAY]: 2, [UserStatus.BUSY]: 3, -}; +} as const; const minimongoChangeMap: Record<string, string> = { inserted: 'added', updated: 'changed', removed: 'removed', -}; +} as const; export class ListenersModule { constructor(service: IServiceClass, notifications: NotificationsModule) { @@ -61,10 +62,16 @@ export class ListenersModule { notifications.notifyUserInThisInstance(uid, 'message', { groupable: false, - ...message, + u: { + _id: 'rocket.cat', + username: 'rocket.cat', + }, + private: true, _id: message._id || String(Date.now()), rid, ts: new Date(), + _updatedAt: new Date(), + ...message, }); }); @@ -127,14 +134,16 @@ export class ListenersModule { service.onEvent('presence.status', ({ user }) => { const { _id, username, name, status, statusText, roles } = user; - if (!status) { + if (!status || !username) { return; } - notifications.notifyLoggedInThisInstance('user-status', [_id, username, STATUS_MAP[status], statusText, name, roles]); + const statusChanged = (STATUS_MAP as any)[status] as 0 | 1 | 2 | 3; + + notifications.notifyLoggedInThisInstance('user-status', [_id, username, statusChanged, statusText, name, roles]); if (_id) { - notifications.sendPresence(_id, [username, STATUS_MAP[status], statusText]); + notifications.sendPresence(_id, username, statusChanged, statusText); } }); @@ -159,12 +168,6 @@ export class ListenersModule { notifications.streamRoomMessage.emitWithoutBroadcast(message.rid, message); }); - service.onEvent('message.update', ({ message }) => { - if (message.rid) { - notifications.streamRoomMessage.emitWithoutBroadcast(message.rid, message); - } - }); - service.onEvent('watch.subscriptions', ({ clientAction, subscription }) => { if (!subscription.u?._id) { return; @@ -177,7 +180,7 @@ export class ListenersModule { notifications.streamUser.__emit(subscription.u._id, clientAction, subscription); - notifications.notifyUserInThisInstance(subscription.u._id, 'subscriptions-changed', clientAction, subscription); + notifications.notifyUserInThisInstance(subscription.u._id, 'subscriptions-changed', clientAction, subscription as any); }); service.onEvent('watch.roles', ({ clientAction, role }): void => { @@ -185,11 +188,11 @@ export class ListenersModule { type: clientAction, ...role, }; - notifications.streamRoles.emitWithoutBroadcast('roles', payload); + notifications.streamRoles.emitWithoutBroadcast('roles', payload as any); }); service.onEvent('watch.inquiries', async ({ clientAction, inquiry, diff }): Promise<void> => { - const type = minimongoChangeMap[clientAction]; + const type = minimongoChangeMap[clientAction] as 'added' | 'changed' | 'removed'; if (clientAction === 'removed') { notifications.streamLivechatQueueData.emitWithoutBroadcast(inquiry._id, { _id: inquiry._id, @@ -256,7 +259,7 @@ export class ListenersModule { properties: setting.properties, enterprise: setting.enterprise, requiredOnWizard: setting.requiredOnWizard, - }; + } as ISetting; if (setting.public === true) { notifications.notifyAllInThisInstance('public-settings-changed', clientAction, value); @@ -269,23 +272,24 @@ export class ListenersModule { // this emit will cause the user to receive a 'rooms-changed' event notifications.streamUser.__emit(room._id, clientAction, room); - notifications.streamRoomData.emitWithoutBroadcast(room._id, room); + notifications.streamRoomData.emitWithoutBroadcast(room._id, room as IOmnichannelRoom); }); - service.onEvent('watch.users', ({ clientAction, data, diff, unset, id }): void => { - switch (clientAction) { + service.onEvent('watch.users', (event): void => { + switch (event.clientAction) { case 'updated': - notifications.notifyUserInThisInstance(id, 'userData', { - diff, - unset, - type: clientAction, + notifications.notifyUserInThisInstance(event.id, 'userData', { + id: event.id, + diff: event.diff, + unset: event.unset, + type: 'updated', }); break; case 'inserted': - notifications.notifyUserInThisInstance(id, 'userData', { data, type: clientAction }); + notifications.notifyUserInThisInstance(event.id, 'userData', { id: event.id, data: event.data, type: 'inserted' }); break; case 'removed': - notifications.notifyUserInThisInstance(id, 'userData', { id, type: clientAction }); + notifications.notifyUserInThisInstance(event.id, 'userData', { id: event.id, type: 'removed' }); break; } }); @@ -406,7 +410,7 @@ export class ListenersModule { notifications.streamApps.emitWithoutBroadcast('app/statusUpdate', { appId, status }); }); - service.onEvent('apps.settingUpdated', (appId: string, setting: ISetting) => { + service.onEvent('apps.settingUpdated', (appId: string, setting: AppsSetting) => { notifications.streamApps.emitWithoutBroadcast('app/settingUpdated', { appId, setting }); }); diff --git a/apps/meteor/server/modules/notifications/notifications.module.ts b/apps/meteor/server/modules/notifications/notifications.module.ts index 0395bb7c7f84e6b8fa42af5ce492dbb282808e7e..a9fd5fadd01300e29e0361e623e0627fdece78af 100644 --- a/apps/meteor/server/modules/notifications/notifications.module.ts +++ b/apps/meteor/server/modules/notifications/notifications.module.ts @@ -2,46 +2,49 @@ import type { IStreamer, IStreamerConstructor, IPublication } from 'meteor/rocke import type { ISubscription, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users, Settings } from '@rocket.chat/models'; import { Authorization, VideoConf } from '@rocket.chat/core-services'; +import type { StreamerCallbackArgs, StreamKeys, StreamNames } from '@rocket.chat/ui-contexts'; import { emit, StreamPresence } from '../../../app/notifications/server/lib/Presence'; import { SystemLogger } from '../../lib/logger/system'; export class NotificationsModule { - public readonly streamLogged: IStreamer; + public readonly streamLogged: IStreamer<'notify-logged'>; - public readonly streamAll: IStreamer; + public readonly streamAll: IStreamer<'notify-all'>; - public readonly streamRoom: IStreamer; + public readonly streamRoom: IStreamer<'notify-room'>; - public readonly streamRoomUsers: IStreamer; + public readonly streamRoomUsers: IStreamer<'notify-room-users'>; - public readonly streamUser: IStreamer; + public readonly streamUser: IStreamer<'notify-user'> & { + on(event: string, fn: (...data: any[]) => void): void; + }; - public readonly streamRoomMessage: IStreamer; + public readonly streamRoomMessage: IStreamer<'room-messages'>; - public readonly streamImporters: IStreamer; + public readonly streamImporters: IStreamer<'importers'>; - public readonly streamRoles: IStreamer; + public readonly streamRoles: IStreamer<'roles'>; - public readonly streamApps: IStreamer; + public readonly streamApps: IStreamer<'apps'>; - public readonly streamAppsEngine: IStreamer; + public readonly streamAppsEngine: IStreamer<'apps-engine'>; - public readonly streamCannedResponses: IStreamer; + public readonly streamCannedResponses: IStreamer<'canned-responses'>; - public readonly streamIntegrationHistory: IStreamer; + public readonly streamIntegrationHistory: IStreamer<'integrationHistory'>; - public readonly streamLivechatRoom: IStreamer; + public readonly streamLivechatRoom: IStreamer<'livechat-room'>; - public readonly streamLivechatQueueData: IStreamer; + public readonly streamLivechatQueueData: IStreamer<'livechat-inquiry-queue-observer'>; - public readonly streamStdout: IStreamer; + public readonly streamStdout: IStreamer<'stdout'>; - public readonly streamRoomData: IStreamer; + public readonly streamRoomData: IStreamer<'room-data'>; - public readonly streamLocal: IStreamer; + public readonly streamLocal: IStreamer<'local'>; - public readonly streamPresence: IStreamer; + public readonly streamPresence: IStreamer<'user-presence'>; constructor(private Streamer: IStreamerConstructor) { this.streamAll = new this.Streamer('notify-all'); @@ -61,7 +64,7 @@ export class NotificationsModule { this.streamPresence = StreamPresence.getInstance(Streamer, 'user-presence'); this.streamRoomMessage = new this.Streamer('room-messages'); - this.streamRoomMessage.on('_afterPublish', async (streamer: IStreamer, publication: IPublication, eventName: string): Promise<void> => { + this.streamRoomMessage.on('_afterPublish', async (streamer, publication: IPublication, eventName: string): Promise<void> => { const { userId } = publication._session; if (!userId) { return; @@ -87,7 +90,8 @@ export class NotificationsModule { } configure(): void { - const notifyUser = this.notifyUser.bind(this); + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; this.streamRoomMessage.allowWrite('none'); this.streamRoomMessage.allowRead(async function (eventName, extraData) { @@ -140,7 +144,7 @@ export class NotificationsModule { this.streamAll.allowWrite('none'); this.streamAll.allowRead('all'); - this.streamAll.allowRead('private-settings-changed', async function () { + this.streamLogged.allowRead('private-settings-changed', async function () { if (this.userId == null) { return false; } @@ -251,14 +255,14 @@ export class NotificationsModule { }); this.streamRoomUsers.allowRead('none'); - this.streamRoomUsers.allowWrite(async function (eventName, ...args) { - const [roomId, e] = eventName.split('/'); + this.streamRoomUsers.allowWrite(async function (eventName, ...args: any[]) { + const [roomId, e] = eventName.split('/') as typeof eventName extends `${infer K}/${infer E}` ? [K, E] : never; if (!this.userId) { const room = await Rooms.findOneById<IOmnichannelRoom>(roomId, { projection: { 't': 1, 'servedBy._id': 1 }, }); if (room && room.t === 'l' && e === 'webrtc' && room.servedBy) { - notifyUser(room.servedBy._id, e, ...args); + self.notifyUser(room.servedBy._id, e, ...args); return false; } } else if ((await Subscriptions.countByRoomIdAndUserId(roomId, this.userId)) > 0) { @@ -266,18 +270,19 @@ export class NotificationsModule { projection: { 'v._id': 1, '_id': 0 }, }).toArray(); if (livechatSubscriptions && e === 'webrtc') { - livechatSubscriptions.forEach((subscription) => subscription.v && notifyUser(subscription.v._id, e, ...args)); + livechatSubscriptions.forEach((subscription) => subscription.v && self.notifyUser(subscription.v._id, e, ...args)); return false; } const subscriptions: ISubscription[] = await Subscriptions.findByRoomIdAndNotUserId(roomId, this.userId, { projection: { 'u._id': 1, '_id': 0 }, }).toArray(); - subscriptions.forEach((subscription) => notifyUser(subscription.u._id, e, ...args)); + + subscriptions.forEach((subscription) => self.notifyUser(subscription.u._id, e, ...args)); } return false; }); - this.streamUser.allowWrite(async function (eventName: string, data: unknown) { + this.streamUser.allowWrite(async function (eventName, data: unknown) { const [, e] = eventName.split('/'); if (e === 'otr' && (data === 'handshake' || data === 'acknowledge')) { const isEnable = await Settings.getValueById('OTR_Enable'); @@ -413,7 +418,7 @@ export class NotificationsModule { this.streamRoles.allowWrite('none'); this.streamRoles.allowRead('logged'); - this.streamUser.on('_afterPublish', async (streamer: IStreamer, publication: IPublication, eventName: string): Promise<void> => { + this.streamUser.on('_afterPublish', async (streamer, publication: IPublication, eventName: string): Promise<void> => { const { userId } = publication._session; if (!userId) { return; @@ -477,39 +482,55 @@ export class NotificationsModule { this.streamPresence.allowWrite('none'); } - notifyAll(eventName: string, ...args: any[]): void { + notifyAll<E extends StreamKeys<'notify-all'>>(eventName: E, ...args: StreamerCallbackArgs<'notify-all', E>): void { return this.streamAll.emit(eventName, ...args); } - notifyLogged(eventName: string, ...args: any[]): void { + notifyLogged<E extends StreamKeys<'notify-logged'>>(eventName: E, ...args: StreamerCallbackArgs<'notify-logged', E>): void { return this.streamLogged.emit(eventName, ...args); } - notifyRoom(room: string, eventName: string, ...args: any[]): void { + notifyRoom<P extends string, E extends string>( + room: P, + eventName: E extends ExtractNotifyUserEventName<'notify-room', P> ? E : never, + ...args: E extends ExtractNotifyUserEventName<'notify-room', P> ? StreamerCallbackArgs<'notify-room', `${P}/${E}`> : never + ): void { return this.streamRoom.emit(`${room}/${eventName}`, ...args); } - notifyUser(userId: string, eventName: string, ...args: any[]): void { + notifyUser<P extends string, E extends string>( + userId: P, + eventName: E extends ExtractNotifyUserEventName<'notify-user', P> ? E : never, + ...args: E extends ExtractNotifyUserEventName<'notify-user', P> ? StreamerCallbackArgs<'notify-user', `${P}/${E}`> : never + ): void { return this.streamUser.emit(`${userId}/${eventName}`, ...args); } - notifyAllInThisInstance(eventName: string, ...args: any[]): void { + notifyAllInThisInstance<E extends StreamKeys<'notify-all'>>(eventName: E, ...args: StreamerCallbackArgs<'notify-all', E>): void { return this.streamAll.emitWithoutBroadcast(eventName, ...args); } - notifyLoggedInThisInstance(eventName: string, ...args: any[]): void { + notifyLoggedInThisInstance<E extends StreamKeys<'notify-logged'>>(eventName: E, ...args: StreamerCallbackArgs<'notify-logged', E>): void { return this.streamLogged.emitWithoutBroadcast(eventName, ...args); } - notifyRoomInThisInstance(room: string, eventName: string, ...args: any[]): void { + notifyRoomInThisInstance<P extends string, E extends string>( + room: P, + eventName: E extends ExtractNotifyUserEventName<'notify-room', P> ? E : never, + ...args: E extends ExtractNotifyUserEventName<'notify-room', P> ? StreamerCallbackArgs<'notify-room', `${P}/${E}`> : never + ): void { return this.streamRoom.emitWithoutBroadcast(`${room}/${eventName}`, ...args); } - notifyUserInThisInstance(userId: string, eventName: string, ...args: any[]): void { + notifyUserInThisInstance<P extends string, E extends string>( + userId: P, + eventName: E extends ExtractNotifyUserEventName<'notify-user', P> ? E : never, + ...args: E extends ExtractNotifyUserEventName<'notify-user', P> ? StreamerCallbackArgs<'notify-user', `${P}/${E}`> : never + ): void { return this.streamUser.emitWithoutBroadcast(`${userId}/${eventName}`, ...args); } - sendPresence(uid: string, ...args: any[]): void { + sendPresence(uid: string, ...args: [username: string, statusChanged: 0 | 1 | 2 | 3, statusText: string | undefined]): void { // if (this.debug === true) { // console.log('notifyUserAndBroadcast', [userId, eventName, ...args]); // } @@ -517,7 +538,35 @@ export class NotificationsModule { return this.streamPresence.emitWithoutBroadcast(uid, ...args); } - progressUpdated(progress: { rate: number; count?: { completed: number; total: number } }): void { + progressUpdated(progress: { + rate: number; + count?: { completed: number; total: number }; + step?: + | 'importer_new' + | 'importer_uploading' + | 'importer_downloading_file' + | 'importer_file_loaded' + | 'importer_preparing_started' + | 'importer_preparing_users' + | 'importer_preparing_channels' + | 'importer_preparing_messages' + | 'importer_user_selection' + | 'importer_importing_started' + | 'importer_importing_users' + | 'importer_importing_channels' + | 'importer_importing_messages' + | 'importer_importing_files' + | 'importer_finishing' + | 'importer_done' + | 'importer_import_failed' + | 'importer_import_cancelled'; + }): void { this.streamImporters.emit('progress', progress); } } + +type ExtractNotifyUserEventName< + T extends StreamNames, + P extends string, + E extends StreamKeys<T> = StreamKeys<T>, +> = E extends `${infer X}/${infer I}` ? (P extends X ? I : never) : never; diff --git a/apps/meteor/server/modules/streamer/streamer.module.ts b/apps/meteor/server/modules/streamer/streamer.module.ts index dc33e69304fef75c2e7a8622831d00a18fa1810d..ead16cf5ae0da7dc5446c8173c0cdd17ec0840b6 100644 --- a/apps/meteor/server/modules/streamer/streamer.module.ts +++ b/apps/meteor/server/modules/streamer/streamer.module.ts @@ -1,11 +1,12 @@ import { EventEmitter } from 'eventemitter3'; import type { IPublication, Rule, Connection, DDPSubscription, IStreamer, IRules, TransformMessage } from 'meteor/rocketchat:streamer'; import { MeteorError } from '@rocket.chat/core-services'; +import type { StreamerEvents } from '@rocket.chat/ui-contexts'; import { SystemLogger } from '../../lib/logger/system'; -class StreamerCentralClass extends EventEmitter { - public instances: Record<string, Streamer> = {}; +class StreamerCentralClass<N extends keyof StreamerEvents> extends EventEmitter { + public instances: Record<string, Streamer<N>> = {}; constructor() { super(); @@ -14,7 +15,7 @@ class StreamerCentralClass extends EventEmitter { export const StreamerCentral = new StreamerCentralClass(); -export abstract class Streamer extends EventEmitter implements IStreamer { +export abstract class Streamer<N extends keyof StreamerEvents> extends EventEmitter implements IStreamer<N> { public subscriptions = new Set<DDPSubscription>(); protected subscriptionsByEventName = new Map<string, Set<DDPSubscription>>(); diff --git a/apps/meteor/server/modules/watchers/watchers.module.ts b/apps/meteor/server/modules/watchers/watchers.module.ts index 158c0457c865568da563fc1f0291802f2e13b3d8..6eb2556e7a67393ba00e97c6d7eb9b0fc4220c73 100644 --- a/apps/meteor/server/modules/watchers/watchers.module.ts +++ b/apps/meteor/server/modules/watchers/watchers.module.ts @@ -120,9 +120,60 @@ export function initWatchers(watcher: DatabaseWatcher, broadcast: BroadcastCallb } // Override data cuz we do not publish all fields - const subscription = await Subscriptions.findOneById<Pick<ISubscription, keyof typeof subscriptionFields>>(id, { + const subscription = await Subscriptions.findOneById< + Pick< + ISubscription, + | 't' + | 'ts' + | 'ls' + | 'lr' + | 'name' + | 'fname' + | 'rid' + | 'code' + | 'f' + | 'u' + | 'open' + | 'alert' + | 'roles' + | 'unread' + | 'prid' + | 'userMentions' + | 'groupMentions' + | 'archived' + | 'audioNotificationValue' + | 'desktopNotifications' + | 'mobilePushNotifications' + | 'emailNotifications' + | 'desktopPrefOrigin' + | 'mobilePrefOrigin' + | 'emailPrefOrigin' + | 'unreadAlert' + | '_updatedAt' + | 'blocked' + | 'blocker' + | 'autoTranslate' + | 'autoTranslateLanguage' + | 'disableNotifications' + | 'hideUnreadStatus' + | 'hideMentionStatus' + | 'muteGroupMentions' + | 'ignored' + | 'E2EKey' + | 'E2ESuggestedKey' + | 'tunread' + | 'tunreadGroup' + | 'tunreadUser' + + // Omnichannel fields + | 'department' + | 'v' + | 'onHold' + > + >(id, { projection: subscriptionFields, }); + if (!subscription) { return; } @@ -131,9 +182,9 @@ export function initWatchers(watcher: DatabaseWatcher, broadcast: BroadcastCallb } case 'removed': { - const trash = await Subscriptions.trashFindOneById<Pick<ISubscription, 'u' | 'rid'>>(id, { + const trash = (await Subscriptions.trashFindOneById(id, { projection: { u: 1, rid: 1 }, - }); + })) as Pick<ISubscription, 'u' | 'rid' | '_id'> | undefined; const subscription = trash || { _id: id }; void broadcast('watch.subscriptions', { clientAction, subscription }); break; @@ -147,14 +198,25 @@ export function initWatchers(watcher: DatabaseWatcher, broadcast: BroadcastCallb return; } - const role = clientAction === 'removed' ? { _id: id, name: id } : data || (await Roles.findOneById(id)); + if (clientAction === 'removed') { + void broadcast('watch.roles', { + clientAction: 'removed', + role: { + _id: id, + name: id, + }, + }); + return; + } + + const role = data || (await Roles.findOneById(id)); if (!role) { return; } void broadcast('watch.roles', { - clientAction: clientAction !== 'removed' ? ('changed' as const) : clientAction, + clientAction: 'changed', role, }); }); @@ -288,7 +350,16 @@ export function initWatchers(watcher: DatabaseWatcher, broadcast: BroadcastCallb return; } - void broadcast('watch.users', { clientAction, data, diff, unset, id }); + if (clientAction === 'removed') { + void broadcast('watch.users', { clientAction, id }); + return; + } + if (clientAction === 'inserted') { + void broadcast('watch.users', { clientAction, id, data: data! }); + return; + } + + void broadcast('watch.users', { clientAction, diff: diff!, unset: unset!, id }); }); watcher.on<ILoginServiceConfiguration>(LoginServiceConfiguration.getCollectionName(), async ({ clientAction, id }) => { diff --git a/apps/meteor/server/services/apps-engine/service.ts b/apps/meteor/server/services/apps-engine/service.ts index 4253c9d9efdfd3894fb152e2599750ece38d00ad..d19e3d428c1714a6b18aa46a3135ad2ada47dcbd 100644 --- a/apps/meteor/server/services/apps-engine/service.ts +++ b/apps/meteor/server/services/apps-engine/service.ts @@ -2,7 +2,6 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { IAppsEngineService } from '@rocket.chat/core-services'; import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; -import type { ISetting } from '@rocket.chat/core-typings'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; @@ -84,7 +83,7 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi } }); - this.onEvent('apps.settingUpdated', async (appId: string, setting: ISetting & { id: string }): Promise<void> => { + this.onEvent('apps.settingUpdated', async (appId: string, setting): Promise<void> => { Apps.getRocketChatLogger().debug(`"apps.settingUpdated" event received for app "${appId}"`, { setting }); const app = Apps.getManager()?.getOneById(appId); const oldSetting = app?.getStorageItem().settings[setting.id].value; diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts index 419e8392dfa96b7f04930b097a064d78aa41a223..509f2eb17bafd8ae2fdbf1694e0112f788508fd7 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts @@ -311,7 +311,12 @@ export class RocketChatRoomAdapter { federatedRoom: FederatedRoom, role: string, action: 'added' | 'removed', - ): Record<string, any> { + ): { + type: 'added' | 'removed' | 'changed'; + _id: string; + u?: { _id: IUser['_id']; username: IUser['username']; name: IUser['name'] }; + scope?: string; + } { return { type: action, _id: role, diff --git a/apps/meteor/server/services/meteor/service.ts b/apps/meteor/server/services/meteor/service.ts index 837c805da7fadd6c3d5047a87bd916ca2d5d30fd..50efcd09a8a274f7476e52481c3973e011c0a9ea 100644 --- a/apps/meteor/server/services/meteor/service.ts +++ b/apps/meteor/server/services/meteor/service.ts @@ -157,10 +157,10 @@ export class MeteorService extends ServiceClassInternal implements IMeteor { }); } - this.onEvent('watch.users', async ({ clientAction, id, diff }) => { + this.onEvent('watch.users', async (data) => { if (disableOplog) { - if (clientAction === 'updated' && diff) { - processOnChange(diff, id); + if (data.clientAction === 'updated' && data.diff) { + processOnChange(data.diff, data.id); } } @@ -168,14 +168,14 @@ export class MeteorService extends ServiceClassInternal implements IMeteor { return; } - if (clientAction !== 'removed' && diff && !diff.status && !diff.statusLivechat) { + if (data.clientAction !== 'removed' && 'diff' in data && !data.diff.status && !data.diff.statusLivechat) { return; } - switch (clientAction) { + switch (data.clientAction) { case 'updated': case 'inserted': - const agent = await Users.findOneAgentById<Pick<ILivechatAgent, 'status' | 'statusLivechat'>>(id, { + const agent = await Users.findOneAgentById<Pick<ILivechatAgent, 'status' | 'statusLivechat'>>(data.id, { projection: { status: 1, statusLivechat: 1, @@ -184,14 +184,14 @@ export class MeteorService extends ServiceClassInternal implements IMeteor { const serviceOnline = agent && agent.status !== 'offline' && agent.statusLivechat === 'available'; if (serviceOnline) { - return onlineAgents.add(id); + return onlineAgents.add(data.id); } - onlineAgents.remove(id); + onlineAgents.remove(data.id); break; case 'removed': - onlineAgents.remove(id); + onlineAgents.remove(data.id); break; } }); diff --git a/apps/meteor/server/services/push/service.ts b/apps/meteor/server/services/push/service.ts index 87f96a3aae24788c2e5a63c5d49dc40c0333d32e..5590d7477bc535fceed553f6573944e295c9083e 100644 --- a/apps/meteor/server/services/push/service.ts +++ b/apps/meteor/server/services/push/service.ts @@ -8,18 +8,18 @@ export class PushService extends ServiceClassInternal implements IPushService { constructor() { super(); - this.onEvent('watch.users', async ({ id, diff }) => { - if (!diff || !('services.resume.loginTokens' in diff)) { + this.onEvent('watch.users', async (data) => { + if (!('diff' in data) || !('services.resume.loginTokens' in data.diff)) { return; } - if (diff['services.resume.loginTokens'] === undefined) { - await PushToken.removeAllByUserId(id); + if (data.diff['services.resume.loginTokens'] === undefined) { + await PushToken.removeAllByUserId(data.id); return; } - const loginTokens = Array.isArray(diff['services.resume.loginTokens']) ? diff['services.resume.loginTokens'] : []; + const loginTokens = Array.isArray(data.diff['services.resume.loginTokens']) ? data.diff['services.resume.loginTokens'] : []; const tokens = loginTokens.map(({ hashedToken }: { hashedToken: string }) => hashedToken); if (tokens.length > 0) { - await PushToken.removeByUserIdExceptTokens(id, tokens); + await PushToken.removeByUserIdExceptTokens(data.id, tokens); } }); } diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index fefb7d8dfbce91f5f042849eb7f17af580d22059..ed44cee96e5ceed962ce25b3f2f82d9c4c9ecfe3 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -420,7 +420,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf private notifyVideoConfUpdate(rid: IRoom['_id'], callId: VideoConference['_id']): void { /* deprecated */ - Notifications.notifyRoom(rid, callId); + (Notifications.notifyRoom as any)(rid, callId); Notifications.notifyRoom(rid, 'videoconf', callId); } diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index 74b711e15846ace859d3a13c13546c931be78dea..715e7c13f74ec96b2b054a8072e594bd87cf6cbc 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -16,6 +16,9 @@ COPY ./packages/core-typings/dist packages/core-typings/dist COPY ./packages/rest-typings/package.json packages/rest-typings/package.json COPY ./packages/rest-typings/dist packages/rest-typings/dist +COPY ./packages/ui-contexts/package.json packages/ui-contexts/package.json +COPY ./packages/ui-contexts/dist packages/ui-contexts/dist + COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index fd94ea545d2b0c5cc06f499dcdce9e6ce986abb6..c8a8363c1fe1722a3375bd815e84b57f3bb5fc59 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -24,6 +24,7 @@ "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "next", + "@rocket.chat/ui-contexts": "workspace:^", "colorette": "^1.4.0", "ejson": "^2.2.2", "eventemitter3": "^4.0.7", diff --git a/ee/apps/ddp-streamer/src/Streamer.ts b/ee/apps/ddp-streamer/src/Streamer.ts index 60d967d7bc8e92bc775a0a54499ae7d06de8f53c..3c34cbfcb12c3ac28079e5405ef2e7303c61b2c7 100644 --- a/ee/apps/ddp-streamer/src/Streamer.ts +++ b/ee/apps/ddp-streamer/src/Streamer.ts @@ -1,6 +1,7 @@ import WebSocket from 'ws'; import type { DDPSubscription, Connection, TransformMessage } from 'meteor/rocketchat:streamer'; import { api } from '@rocket.chat/core-services'; +import type { StreamNames } from '@rocket.chat/ui-contexts'; import { server } from './configureServer'; import { DDP_EVENTS } from './constants'; @@ -11,7 +12,7 @@ StreamerCentral.on('broadcast', (name, eventName, args) => { void api.broadcast('stream', [name, eventName, args]); }); -export class Stream extends Streamer { +export class Stream<N extends StreamNames> extends Streamer<N> { registerPublication(name: string, fn: (eventName: string, options: boolean | { useCollection?: boolean; args?: any }) => void): void { server.publish(name, fn); } diff --git a/packages/core-services/src/Events.ts b/packages/core-services/src/Events.ts index c47efe1944b9f659afc5bc0174e51cf40f7067e7..ed04a2404ece5094c4421c3e09276a07dabab69a 100644 --- a/packages/core-services/src/Events.ts +++ b/packages/core-services/src/Events.ts @@ -1,4 +1,5 @@ import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definition/settings'; import type { IUIKitInteraction } from '@rocket.chat/apps-engine/definition/uikit'; import type { IEmailInbox, @@ -23,10 +24,11 @@ import type { IWebdavAccount, ICustomSound, VoipEventDataSignature, - AtLeast, UserStatus, ILivechatPriority, VideoConference, + AtLeast, + ILivechatInquiryRecord, } from '@rocket.chat/core-typings'; import type { AutoUpdateRecord } from './types/IMeteor'; @@ -53,7 +55,7 @@ export type EventSignatures = { 'notify.desktop'(uid: string, data: INotificationDesktop): void; 'notify.uiInteraction'(uid: string, data: IUIKitInteraction): void; 'notify.updateInvites'(uid: string, data: { invite: Omit<IInvite, '_updatedAt'> }): void; - 'notify.ephemeralMessage'(uid: string, rid: string, message: Partial<IMessage>): void; + 'notify.ephemeralMessage'(uid: string, rid: string, message: AtLeast<IMessage, 'msg'>): void; 'notify.webdav'( uid: string, data: @@ -82,16 +84,21 @@ export type EventSignatures = { 'notify.updateCustomSound'(data: { soundData: ICustomSound }): void; 'permission.changed'(data: { clientAction: ClientAction; data: any }): void; 'room'(data: { action: string; room: Partial<IRoom> }): void; - 'room.avatarUpdate'(room: Partial<IRoom>): void; + 'room.avatarUpdate'(room: Pick<IRoom, '_id' | 'avatarETag'>): void; 'setting'(data: { action: string; setting: Partial<ISetting> }): void; 'stream'([streamer, eventName, payload]: [string, string, any[]]): void; 'subscription'(data: { action: string; subscription: Partial<ISubscription> }): void; 'user.avatarUpdate'(user: Partial<IUser>): void; - 'user.deleted'(user: Partial<IUser>): void; + 'user.deleted'(user: Pick<IUser, '_id'>): void; 'user.deleteCustomStatus'(userStatus: IUserStatus): void; - 'user.nameChanged'(user: Partial<IUser>): void; + 'user.nameChanged'(user: Pick<IUser, '_id' | 'name' | 'username'>): void; 'user.realNameChanged'(user: Partial<IUser>): void; - 'user.roleUpdate'(update: Record<string, any>): void; + 'user.roleUpdate'(update: { + type: 'added' | 'removed' | 'changed'; + _id: string; + u?: { _id: IUser['_id']; username: IUser['username']; name?: IUser['name'] }; + scope?: string; + }): void; 'user.updateCustomStatus'(userStatus: IUserStatus): void; 'user.typing'(data: { user: Partial<IUser>; isTyping: boolean; roomId: string }): void; 'user.video-conference'(data: { @@ -107,19 +114,102 @@ export type EventSignatures = { user: Pick<IUser, '_id' | 'username' | 'status' | 'statusText' | 'name' | 'roles'>; previousStatus: UserStatus | undefined; }): void; - 'watch.messages'(data: { clientAction: ClientAction; message: Partial<IMessage> }): void; - 'watch.roles'(data: { clientAction: ClientAction; role: Partial<IRole> }): void; - 'watch.rooms'(data: { clientAction: ClientAction; room: Pick<IRoom, '_id'> & Partial<IRoom> }): void; - 'watch.subscriptions'(data: { clientAction: ClientAction; subscription: Partial<ISubscription> }): void; - 'watch.inquiries'(data: { clientAction: ClientAction; inquiry: IInquiry; diff?: undefined | Record<string, any> }): void; + 'watch.messages'(data: { clientAction: ClientAction; message: IMessage }): void; + 'watch.roles'( + data: + | { clientAction: Exclude<ClientAction, 'removed'>; role: IRole } + | { + clientAction: 'removed'; + role: { + _id: string; + name: string; + }; + }, + ): void; + 'watch.rooms'(data: { clientAction: ClientAction; room: Pick<IRoom, '_id'> | IRoom }): void; + 'watch.subscriptions'( + data: + | { + clientAction: 'updated' | 'inserted'; + subscription: Pick< + ISubscription, + | 't' + | 'ts' + | 'ls' + | 'lr' + | 'name' + | 'fname' + | 'rid' + | 'code' + | 'f' + | 'u' + | 'open' + | 'alert' + | 'roles' + | 'unread' + | 'prid' + | 'userMentions' + | 'groupMentions' + | 'archived' + | 'audioNotificationValue' + | 'desktopNotifications' + | 'mobilePushNotifications' + | 'emailNotifications' + | 'desktopPrefOrigin' + | 'mobilePrefOrigin' + | 'emailPrefOrigin' + | 'unreadAlert' + | '_updatedAt' + | 'blocked' + | 'blocker' + | 'autoTranslate' + | 'autoTranslateLanguage' + | 'disableNotifications' + | 'hideUnreadStatus' + | 'hideMentionStatus' + | 'muteGroupMentions' + | 'ignored' + | 'E2EKey' + | 'E2ESuggestedKey' + | 'tunread' + | 'tunreadGroup' + | 'tunreadUser' + + // Omnichannel fields + | 'department' + | 'v' + | 'onHold' + >; + } + | { + clientAction: 'removed'; + subscription: { + _id: string; + u?: Pick<IUser, '_id' | 'username' | 'name'>; + rid?: string; + }; + }, + ): void; + 'watch.inquiries'(data: { clientAction: ClientAction; inquiry: ILivechatInquiryRecord; diff?: undefined | Record<string, any> }): void; 'watch.settings'(data: { clientAction: ClientAction; setting: ISetting }): void; - 'watch.users'(data: { - clientAction: ClientAction; - data?: undefined | Partial<IUser>; - diff?: undefined | Record<string, any>; - unset?: undefined | Record<string, number>; - id: string; - }): void; + 'watch.users'( + data: { + id: string; + } & ( + | { + clientAction: 'inserted'; + data: IUser; + } + | { + clientAction: 'removed'; + } + | { + clientAction: 'updated'; + diff: Record<string, number>; + unset: Record<string, number>; + } + ), + ): void; 'watch.loginServiceConfiguration'(data: { clientAction: ClientAction; data: Partial<ILoginServiceConfiguration>; id: string }): void; 'watch.instanceStatus'(data: { clientAction: ClientAction; @@ -151,7 +241,6 @@ export type EventSignatures = { 'call.callerhangup'(userId: string, data: { roomId: string }): void; 'watch.pbxevents'(data: { clientAction: ClientAction; data: Partial<IPbxEvent>; id: string }): void; 'connector.statuschanged'(enabled: boolean): void; - 'message.update'(data: { message: AtLeast<IMessage, 'rid'> }): void; 'federation.userRoleChanged'(update: Record<string, any>): void; 'watch.priorities'(data: { clientAction: ClientAction; @@ -163,7 +252,7 @@ export type EventSignatures = { 'apps.removed'(appId: string): void; 'apps.updated'(appId: string): void; 'apps.statusUpdate'(appId: string, status: AppStatus): void; - 'apps.settingUpdated'(appId: string, setting: ISetting): void; + 'apps.settingUpdated'(appId: string, setting: AppsSetting): void; 'command.added'(command: string): void; 'command.disabled'(command: string): void; 'command.updated'(command: string): void; diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index 72335fae61b58f5eef5a470caaee18f09492296a..e932679aab45a06d4faad61273a7473a0f3af61d 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -193,16 +193,17 @@ export const isUserFederated = (user: Partial<IUser>) => 'federated' in user && export type IUserDataEvent = { id: unknown; } & ( - | ({ + | { type: 'inserted'; - } & IUser) + data: IUser; + } | { type: 'removed'; } | { type: 'updated'; diff: Partial<IUser>; - unset: Record<keyof IUser, boolean | 0 | 1>; + unset: Record<string, number>; } ); diff --git a/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts b/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts index 89070ab2cba8c9b4125a6279130deeb172687ac0..b0ca0922fbb9d8b5ba156ed4a53d1f838b7c8465 100644 --- a/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts +++ b/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts @@ -209,7 +209,6 @@ export class RocketchatSdkLegacyImpl extends DDPSDK implements RocketchatSDKLega // this.stream('notify-all', 'deleteEmojiCustom', (...args) => this.ev.emit('deleteEmojiCustom', args)), // this.stream('notify-all', 'updateAvatar', (...args) => this.ev.emit('updateAvatar', args)), this.stream('notify-all', 'public-settings-changed', (...args) => this.ev.emit('public-settings-changed', args)), - this.stream('notify-all', 'permissions-changed', (...args) => this.ev.emit('permissions-changed', args)), ]); } @@ -221,6 +220,7 @@ export class RocketchatSdkLegacyImpl extends DDPSDK implements RocketchatSDKLega this.stream('notify-logged', 'updateEmojiCustom', (...args) => this.ev.emit('updateEmojiCustom', args)), this.stream('notify-logged', 'deleteEmojiCustom', (...args) => this.ev.emit('deleteEmojiCustom', args)), this.stream('notify-logged', 'roles-change', (...args) => this.ev.emit('roles-change', args)), + this.stream('notify-logged', 'permissions-changed', (...args) => this.ev.emit('permissions-changed', args)), ]); } diff --git a/packages/ddp-client/src/legacy/types/SDKLegacy.ts b/packages/ddp-client/src/legacy/types/SDKLegacy.ts index 0d18242dff353ec26711cbafb1a33796a7b7f84e..5e422d3addd63862fe044f984722559ec6b66be1 100644 --- a/packages/ddp-client/src/legacy/types/SDKLegacy.ts +++ b/packages/ddp-client/src/legacy/types/SDKLegacy.ts @@ -81,7 +81,7 @@ export type RocketchatSdkLegacyEvents = { 'updateEmojiCustom': StreamerCallbackArgs<'notify-logged', 'updateEmojiCustom'>; 'deleteEmojiCustom': StreamerCallbackArgs<'notify-logged', 'deleteEmojiCustom'>; 'public-settings-changed': StreamerCallbackArgs<'notify-all', 'public-settings-changed'>; - 'permissions-changed': StreamerCallbackArgs<'notify-all', 'permissions-changed'>; + 'permissions-changed': StreamerCallbackArgs<'notify-logged', 'permissions-changed'>; 'Users:NameChanged': StreamerCallbackArgs<'notify-logged', 'Users:NameChanged'>; 'Users:Deleted': StreamerCallbackArgs<'notify-logged', 'Users:Deleted'>; 'updateAvatar': StreamerCallbackArgs<'notify-logged', 'updateAvatar'>; diff --git a/packages/ddp-client/src/livechat/LivechatClientImpl.ts b/packages/ddp-client/src/livechat/LivechatClientImpl.ts index dafb469e5383ccb7240d96a0d0402d269e6dcb13..c8c8b75ea701eb0549a246672db76adfbaccb2f9 100644 --- a/packages/ddp-client/src/livechat/LivechatClientImpl.ts +++ b/packages/ddp-client/src/livechat/LivechatClientImpl.ts @@ -54,7 +54,7 @@ export class LivechatClientImpl extends DDPSDK implements LivechatClient { return this.ev.on('message', (args) => cb(...args)); } - onTyping(cb: (username: string, activities: string) => void): () => void { + onTyping(cb: (username: string, typing: boolean) => void): () => void { return this.ev.on('typing', (args) => args[1] && cb(args[0], args[1])); } diff --git a/packages/ui-contexts/src/ServerContext/streams.ts b/packages/ui-contexts/src/ServerContext/streams.ts index 965a5331ae98e3824a4bb0ed1edfcd256234a5ff..036941a983d54ac3ee7208c80e6ee368fbdf9cc2 100644 --- a/packages/ui-contexts/src/ServerContext/streams.ts +++ b/packages/ui-contexts/src/ServerContext/streams.ts @@ -1,4 +1,5 @@ import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IMessage, IRoom, @@ -15,11 +16,12 @@ import type { VideoConference, IOmnichannelCannedResponse, IIntegrationHistory, - ILivechatInquiryRecord, IUserDataEvent, + IUserStatus, + ILivechatInquiryRecord, } from '@rocket.chat/core-typings'; -type ILivechatInquiryWithType = ILivechatInquiryRecord & { type?: 'added' | 'removed' | 'changed' }; +type ClientAction = 'inserted' | 'updated' | 'removed' | 'changed'; // eslint-disable-next-line @typescript-eslint/naming-convention export interface StreamerEvents { @@ -36,7 +38,7 @@ export interface StreamerEvents { 'notify-room': [ { key: `${string}/user-activity`; args: [username: string, activities: string[]] }, - { key: `${string}/typing`; args: [username: string, activities: string] }, + { key: `${string}/typing`; args: [username: string, typing: boolean] }, { key: `${string}/deleteMessageBulk`; args: [args: { rid: IMessage['rid']; excludePinned: boolean; ignoreDiscussion: boolean; ts: Record<string, Date>; users: string[] }]; @@ -52,12 +54,76 @@ export interface StreamerEvents { 'notify-all': [ { key: 'public-settings-changed'; args: ['inserted' | 'updated' | 'removed' | 'changed', ISetting] }, - { key: 'permissions-changed'; args: ['inserted' | 'updated' | 'removed' | 'changed', ISetting] }, + { key: 'deleteCustomSound'; args: [{ soundData: ICustomSound }] }, + { key: 'updateCustomSound'; args: [{ soundData: ICustomSound }] }, ]; 'notify-user': [ { key: `${string}/rooms-changed`; args: ['inserted' | 'updated' | 'removed' | 'changed', IRoom] }, - { key: `${string}/subscriptions-changed`; args: ['inserted' | 'updated' | 'removed' | 'changed', ISubscription] }, + { + key: `${string}/subscriptions-changed`; + args: + | [ + 'removed', + { + _id: string; + u?: Pick<IUser, '_id' | 'username' | 'name'>; + rid?: string; + }, + ] + | [ + 'inserted' | 'updated', + Pick< + ISubscription, + | 't' + | 'ts' + | 'ls' + | 'lr' + | 'name' + | 'fname' + | 'rid' + | 'code' + | 'f' + | 'u' + | 'open' + | 'alert' + | 'roles' + | 'unread' + | 'prid' + | 'userMentions' + | 'groupMentions' + | 'archived' + | 'audioNotificationValue' + | 'desktopNotifications' + | 'mobilePushNotifications' + | 'emailNotifications' + | 'desktopPrefOrigin' + | 'mobilePrefOrigin' + | 'emailPrefOrigin' + | 'unreadAlert' + | '_updatedAt' + | 'blocked' + | 'blocker' + | 'autoTranslate' + | 'autoTranslateLanguage' + | 'disableNotifications' + | 'hideUnreadStatus' + | 'hideMentionStatus' + | 'muteGroupMentions' + | 'ignored' + | 'E2EKey' + | 'E2ESuggestedKey' + | 'tunread' + | 'tunreadGroup' + | 'tunreadUser' + + // Omnichannel fields + | 'department' + | 'v' + | 'onHold' + >, + ]; + }, { key: `${string}/message`; args: [IMessage] }, { key: `${string}/force_logout`; args: [] }, @@ -77,7 +143,7 @@ export interface StreamerEvents { { key: `${string}/userData`; args: [IUserDataEvent] }, { key: `${string}/updateInvites`; args: [unknown] }, { key: `${string}/departmentAgentData`; args: [unknown] }, - { key: `${string}/webrtc`; args: [unknown] }, + { key: `${string}/webrtc`; args: unknown[] }, { key: `${string}/otr`; args: [ @@ -95,7 +161,7 @@ export interface StreamerEvents { key: 'progress'; args: [ { - step: + step?: | 'importer_new' | 'importer_uploading' | 'importer_downloading_file' @@ -124,10 +190,16 @@ export interface StreamerEvents { ]; 'notify-logged': [ - { key: 'updateCustomSound'; args: [{ soundData: ICustomSound }] }, + { + key: 'updateCustomUserStatus'; + args: [ + { + userStatusData: IUserStatus; + }, + ]; + }, + { key: 'permissions-changed'; args: ['inserted' | 'updated' | 'removed' | 'changed', ISetting] }, { key: 'deleteEmojiCustom'; args: [{ emojiData: IEmoji }] }, - { key: 'deleteCustomSound'; args: [{ soundData: ICustomSound }] }, - { key: 'private-settings-changed'; args: ['inserted' | 'updated' | 'removed' | 'changed', ISetting] }, { key: 'updateEmojiCustom'; args: [{ emojiData: IEmoji }] }, /* @deprecated */ { key: 'new-banner'; args: [{ bannerId: string }] }, @@ -151,16 +223,17 @@ export interface StreamerEvents { { type: 'added' | 'removed' | 'changed'; _id: IRole['_id']; - u: { _id: IUser['_id']; username: IUser['username']; name: IUser['name'] }; - scope?: IRoom['_id']; + u?: { _id: IUser['_id']; username: IUser['username']; name?: IUser['name'] }; + scope?: string; }, ]; }, { key: 'Users:NameChanged'; args: [Pick<IUser, '_id' | 'name'>] }, { key: 'voip.statuschanged'; args: [boolean] }, - { key: 'omnichannel.priority-changed'; args: [{ id: 'added' | 'removed' | 'changed'; name: string }] }, + { key: 'omnichannel.priority-changed'; args: [{ id: string; clientAction: ClientAction; name?: string }] }, + { key: 'private-settings-changed'; args: ['inserted' | 'updated' | 'removed' | 'changed', ISetting] }, { key: 'deleteCustomUserStatus'; args: [{ userStatusData: unknown }] }, - { key: 'user-status'; args: [[IUser['_id'], IUser['username'], string, string, IUser['name'], IUser['roles']]] }, + { key: 'user-status'; args: [[IUser['_id'], IUser['username'], 0 | 1 | 2 | 3, IUser['statusText'], IUser['name'], IUser['roles']]] }, { key: 'Users:Deleted'; args: [ @@ -169,14 +242,15 @@ export interface StreamerEvents { }, ]; }, - { key: 'updateAvatar'; args: [{ username: IUser['username']; etag: IUser['avatarETag'] }] }, - { key: 'voip.statuschanged'; args: [boolean] }, - { key: 'omnichannel.priority-changed'; args: [{ id: 'added' | 'removed' | 'changed'; name: string }] }, + { + key: 'updateAvatar'; + args: [{ username: IUser['username']; etag: IUser['avatarETag'] } | { rid: IRoom['_id']; etag: IRoom['avatarETag'] }]; + }, ]; 'stdout': [{ key: 'stdout'; args: [{ id: string; string: string; ts: Date }] }]; - 'room-data': [{ key: string; args: [IOmnichannelRoom] }]; + 'room-data': [{ key: string; args: [IOmnichannelRoom | Pick<IOmnichannelRoom, '_id'>] }]; 'notify-room-users': [ { @@ -197,35 +271,6 @@ export interface StreamerEvents { { key: `${string}/userData`; args: unknown[] }, ]; - 'apps': [ - { key: 'app/added'; args: [string] }, - { key: 'app/removed'; args: [string] }, - { key: 'app/updated'; args: [string] }, - { - key: 'app/statusUpdate'; - args: [ - { - appId: string; - status: 'auto_enabled' | 'auto_disabled' | 'manually_enabled' | 'manually_disabled'; - }, - ]; - }, - { - key: 'app/settingUpdated'; - args: [ - { - appId: string; - setting: AppsSetting; - }, - ]; - }, - { key: 'command/added'; args: [string] }, - { key: 'command/disabled'; args: [string] }, - { key: 'command/updated'; args: [string] }, - { key: 'command/removed'; args: [string] }, - { key: 'actions/changed'; args: [] }, - ]; - 'livechat-room': [ { key: string; @@ -252,7 +297,7 @@ export interface StreamerEvents { }, ]; - 'user-presence': [{ key: string; args: [unknown] }]; + 'user-presence': [{ key: string; args: [username: string, statusChanged?: 0 | 1 | 2 | 3, statusText?: string] }]; // TODO: rename to 'integration-history' 'integrationHistory': [ @@ -284,31 +329,101 @@ export interface StreamerEvents { _createdAt?: Date | undefined; }, ] - | [{ type: 'changed' } & IOmnichannelCannedResponse]; + | [{ type: 'changed' } & IOmnichannelCannedResponse, { agentsId: string }]; }, ]; 'livechat-inquiry-queue-observer': [ { key: 'public'; - args: [ILivechatInquiryWithType]; + args: [ + { + type: 'added' | 'removed' | 'changed'; + } & ILivechatInquiryRecord, + ]; }, { key: `department/${string}`; - args: [ILivechatInquiryWithType]; + args: [ + { + type: 'added' | 'removed' | 'changed'; + } & ILivechatInquiryRecord, + ]; + }, + { + key: `${string}`; + args: [ + { + _id: string; + clientAction: string; + }, + ]; + }, + ]; + + 'apps': [ + { key: 'app/added'; args: [string] }, + { key: 'app/removed'; args: [string] }, + { key: 'app/updated'; args: [string] }, + { + key: 'app/statusUpdate'; + args: [ + { + appId: string; + status: AppStatus; + }, + ]; + }, + { + key: 'app/settingUpdated'; + args: [ + { + appId: string; + setting: AppsSetting; + }, + ]; + }, + { key: 'command/added'; args: [string] }, + { key: 'command/disabled'; args: [string] }, + { key: 'command/updated'; args: [string] }, + { key: 'command/removed'; args: [string] }, + { key: 'actions/changed'; args: [] }, + ]; + + 'apps-engine': [ + { key: 'app/added'; args: [string] }, + { key: 'app/removed'; args: [string] }, + { key: 'app/updated'; args: [string] }, + { + key: 'app/statusUpdate'; + args: [ + { + appId: string; + status: AppStatus; + }, + ]; + }, + { + key: 'app/settingUpdated'; + args: [ + { + appId: string; + setting: AppsSetting; + }, + ]; + }, + { key: 'command/added'; args: [string] }, + { key: 'command/disabled'; args: [string] }, + { key: 'command/updated'; args: [string] }, + { key: 'command/removed'; args: [string] }, + { key: 'actions/changed'; args: [] }, + ]; + 'local': [ + { + key: 'broadcast'; + args: any[]; }, - // { - // key: `${string}`; - // args: [ - // { - // _id: string; - // clientAction: string; - // }, - // ]; - // }, ]; - 'apps-engine': []; - 'local': []; } export type StreamNames = keyof StreamerEvents; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index ed630b1de0b12c15f3e15f8d0a5e7e14227fc2a9..31cc3c5a35a1b2333a13b48af598ebb93460de9e 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -87,7 +87,7 @@ export { useSetOutputMediaDevice } from './hooks/useSetOutputMediaDevice'; export { useSetInputMediaDevice } from './hooks/useSetInputMediaDevice'; export { ServerMethods, ServerMethodName, ServerMethodParameters, ServerMethodReturn, ServerMethodFunction } from './ServerContext/methods'; -export { StreamerEvents } from './ServerContext/streams'; +export { StreamerEvents, StreamNames, StreamKeys, StreamerConfigs, StreamerConfig, StreamerCallbackArgs } from './ServerContext/streams'; export { UploadResult } from './ServerContext'; export { TranslationKey, TranslationLanguage } from './TranslationContext'; export { Fields } from './UserContext'; diff --git a/yarn.lock b/yarn.lock index 74ff1b4bd29ca11e45c9ef34e80488d99d1d8c00..5c55075e7827d82dee61d8af26fc8e683cea817d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6991,6 +6991,7 @@ __metadata: "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": next + "@rocket.chat/ui-contexts": "workspace:^" "@types/ejson": ^2.2.0 "@types/eslint": ^8.4.10 "@types/meteor": ^2.9.2