diff --git a/.gitignore b/.gitignore index 5c3677cd1c3..816c05071f5 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ yarn-error.log* !.yarn/releases !.yarn/sdks !.yarn/versions + +.nvmrc \ No newline at end of file diff --git a/apps/meteor/client/lib/voip/QueueAggregator.ts b/apps/meteor/client/lib/voip/QueueAggregator.ts index 68127676b97..1c930984bce 100644 --- a/apps/meteor/client/lib/voip/QueueAggregator.ts +++ b/apps/meteor/client/lib/voip/QueueAggregator.ts @@ -39,7 +39,7 @@ export class QueueAggregator { } private updateQueueInfo(queueName: string, queuedCalls: number): void { - if (!this.currentQueueMembershipStatus[queueName]) { + if (!this.currentQueueMembershipStatus?.[queueName]) { // something is wrong. Queue is not found in the membership details. return; } @@ -48,14 +48,14 @@ export class QueueAggregator { setMembership(subscription: IQueueMembershipSubscription): void { this.extension = subscription.extension; - for (let i = 0; i < subscription.queues.length; i++) { - const queue = subscription.queues[i]; + + subscription.queues.forEach((queue) => { const queueInfo: IQueueInfo = { queueName: queue.name, callsInQueue: 0, }; this.currentQueueMembershipStatus[queue.name] = queueInfo; - } + }); } queueJoined(joiningDetails: { queuename: string; callerid: { id: string }; queuedcalls: string }): void { @@ -77,7 +77,7 @@ export class QueueAggregator { memberRemoved(queue: { queuename: string; queuedcalls: string }): void { // current user is removed from the queue which has queue count |queuedcalls| - if (!this.currentQueueMembershipStatus[queue.queuename]) { + if (!this.currentQueueMembershipStatus?.[queue.queuename]) { // something is wrong. Queue is not found in the membership details. return; } @@ -89,15 +89,11 @@ export class QueueAggregator { } getCallWaitingCount(): number { - let totalCallWaitingCount = 0; - Object.entries(this.currentQueueMembershipStatus).forEach(([, value]) => { - totalCallWaitingCount += value.callsInQueue; - }); - return totalCallWaitingCount; + return Object.entries(this.currentQueueMembershipStatus).reduce((acc, [_, value]) => acc + value.callsInQueue, 0); } getCurrentQueueName(): string { - if (this.currentlyServing.queueInfo) { + if (this.currentlyServing?.queueInfo) { return this.currentlyServing.queueInfo.queueName; } @@ -105,7 +101,7 @@ export class QueueAggregator { } callRinging(queueInfo: { queuename: string; callerid: { id: string; name: string } }): void { - if (!this.currentQueueMembershipStatus[queueInfo.queuename]) { + if (!this.currentQueueMembershipStatus?.[queueInfo.queuename]) { return; } diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx index 7ea848a6cb9..1de2b14fa20 100644 --- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx +++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx @@ -1,5 +1,13 @@ -import type { IVoipRoom, IUser } from '@rocket.chat/core-typings'; -import { ICallerInfo } from '@rocket.chat/core-typings'; +import type { IVoipRoom, IUser, VoipEventDataSignature } from '@rocket.chat/core-typings'; +import { + ICallerInfo, + isVoipEventAgentCalled, + isVoipEventAgentConnected, + isVoipEventCallerJoined, + isVoipEventQueueMemberAdded, + isVoipEventQueueMemberRemoved, + isVoipEventCallAbandoned, +} from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useRoute, useUser, useSetting, useEndpoint, useStream } from '@rocket.chat/ui-contexts'; import { Random } from 'meteor/random'; @@ -65,90 +73,54 @@ export const CallProvider: FC = ({ children }) => { return; } - const handleAgentCalled = async (queue: { - queuename: string; - callerId: { id: string; name: string }; - queuedcalls: string; - }): Promise => { - queueAggregator.callRinging({ queuename: queue.queuename, callerid: queue.callerId }); - setQueueName(queueAggregator.getCurrentQueueName()); + const handleEventReceived = async (event: VoipEventDataSignature): Promise => { + if (isVoipEventAgentCalled(event)) { + const { data } = event; + queueAggregator.callRinging({ queuename: data.queue, callerid: data.callerId }); + setQueueName(queueAggregator.getCurrentQueueName()); + return; + } + if (isVoipEventAgentConnected(event)) { + const { data } = event; + queueAggregator.callPickedup({ queuename: data.queue, queuedcalls: data.queuedCalls, waittimeinqueue: data.waitTimeInQueue }); + setQueueName(queueAggregator.getCurrentQueueName()); + setQueueCounter(queueAggregator.getCallWaitingCount()); + return; + } + if (isVoipEventCallerJoined(event)) { + const { data } = event; + queueAggregator.queueJoined({ queuename: data.queue, callerid: data.callerId, queuedcalls: data.queuedCalls }); + setQueueCounter(queueAggregator.getCallWaitingCount()); + return; + } + if (isVoipEventQueueMemberAdded(event)) { + const { data } = event; + queueAggregator.memberAdded({ queuename: data.queue, queuedcalls: data.queuedCalls }); + setQueueName(queueAggregator.getCurrentQueueName()); + setQueueCounter(queueAggregator.getCallWaitingCount()); + return; + } + if (isVoipEventQueueMemberRemoved(event)) { + const { data } = event; + queueAggregator.memberRemoved({ queuename: data.queue, queuedcalls: data.queuedCalls }); + setQueueCounter(queueAggregator.getCallWaitingCount()); + return; + } + if (isVoipEventCallAbandoned(event)) { + const { data } = event; + queueAggregator.queueAbandoned({ queuename: data.queue, queuedcallafterabandon: data.queuedCallAfterAbandon }); + setQueueName(queueAggregator.getCurrentQueueName()); + setQueueCounter(queueAggregator.getCallWaitingCount()); + return; + } + + console.warn('Unknown event received'); }; - return subscribeToNotifyUser(`${user._id}/agentcalled`, handleAgentCalled); - }, [subscribeToNotifyUser, user, voipEnabled, queueAggregator]); - - useEffect(() => { - if (!voipEnabled || !user || !queueAggregator) { - return; - } - - const handleQueueJoined = async (joiningDetails: { - queuename: string; - callerid: { id: string }; - queuedcalls: string; - }): Promise => { - queueAggregator.queueJoined(joiningDetails); - setQueueCounter(queueAggregator.getCallWaitingCount()); - }; - - return subscribeToNotifyUser(`${user._id}/callerjoined`, handleQueueJoined); - }, [subscribeToNotifyUser, user, voipEnabled, queueAggregator]); - - useEffect(() => { - if (!voipEnabled || !user || !queueAggregator) { - return; - } - - const handleAgentConnected = (queue: { queuename: string; queuedcalls: string; waittimeinqueue: string }): void => { - queueAggregator.callPickedup(queue); - setQueueName(queueAggregator.getCurrentQueueName()); - setQueueCounter(queueAggregator.getCallWaitingCount()); - }; - - return subscribeToNotifyUser(`${user._id}/agentconnected`, handleAgentConnected); - }, [queueAggregator, subscribeToNotifyUser, user, voipEnabled]); - - useEffect(() => { - if (!voipEnabled || !user || !queueAggregator) { - return; - } - - const handleMemberAdded = (queue: { queuename: string; queuedcalls: string }): void => { - queueAggregator.memberAdded(queue); - setQueueName(queueAggregator.getCurrentQueueName()); - setQueueCounter(queueAggregator.getCallWaitingCount()); - }; - - return subscribeToNotifyUser(`${user._id}/queuememberadded`, handleMemberAdded); - }, [queueAggregator, subscribeToNotifyUser, user, voipEnabled]); - - useEffect(() => { - if (!voipEnabled || !user || !queueAggregator) { - return; - } - - const handleMemberRemoved = (queue: { queuename: string; queuedcalls: string }): void => { - queueAggregator.memberRemoved(queue); - setQueueCounter(queueAggregator.getCallWaitingCount()); - }; - - return subscribeToNotifyUser(`${user._id}/queuememberremoved`, handleMemberRemoved); - }, [queueAggregator, subscribeToNotifyUser, user, voipEnabled]); - - useEffect(() => { - if (!voipEnabled || !user || !queueAggregator) { - return; - } - - const handleCallAbandon = (queue: { queuename: string; queuedcallafterabandon: string }): void => { - queueAggregator.queueAbandoned(queue); - setQueueName(queueAggregator.getCurrentQueueName()); - setQueueCounter(queueAggregator.getCallWaitingCount()); - }; - - return subscribeToNotifyUser(`${user._id}/callabandoned`, handleCallAbandon); - }, [queueAggregator, subscribeToNotifyUser, user, voipEnabled]); + return subscribeToNotifyUser(`${user._id}/voip.events`, handleEventReceived); + }, [subscribeToNotifyUser, user, queueAggregator, voipEnabled]); + // This was causing event duplication before, so we'll leave this here for now useEffect(() => { if (!voipEnabled || !user || !queueAggregator) { return; @@ -159,7 +131,7 @@ export const CallProvider: FC = ({ children }) => { openWrapUpModal(); }; - return subscribeToNotifyUser(`${user._id}/call.callerhangup`, handleCallHangup); + return subscribeToNotifyUser(`${user._id}/call.hangup`, handleCallHangup); }, [openWrapUpModal, queueAggregator, subscribeToNotifyUser, user, voipEnabled]); useEffect(() => { diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 01417179b44..59ad5555034 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -278,23 +278,13 @@ export class ListenersModule { service.onEvent('banner.enabled', (bannerId): void => { notifications.notifyLoggedInThisInstance('banner-changed', { bannerId }); }); - service.onEvent('queue.agentcalled', (userId, queuename, callerId): void => { - notifications.notifyUserInThisInstance(userId, 'agentcalled', { queuename, callerId }); + + service.onEvent('voip.events', (userId, data): void => { + notifications.notifyUserInThisInstance(userId, 'voip.events', data); }); - service.onEvent('queue.agentconnected', (userId, queuename: string, queuedcalls: string, waittimeinqueue: string): void => { - notifications.notifyUserInThisInstance(userId, 'agentconnected', { queuename, queuedcalls, waittimeinqueue }); - }); - service.onEvent('queue.callerjoined', (userId, queuename, callerid, queuedcalls): void => { - notifications.notifyUserInThisInstance(userId, 'callerjoined', { queuename, callerid, queuedcalls }); - }); - service.onEvent('queue.queuememberadded', (userId, queuename: string, queuedcalls: string): void => { - notifications.notifyUserInThisInstance(userId, 'queuememberadded', { queuename, queuedcalls }); - }); - service.onEvent('queue.queuememberremoved', (userId, queuename: string, queuedcalls: string): void => { - notifications.notifyUserInThisInstance(userId, 'queuememberremoved', { queuename, queuedcalls }); - }); - service.onEvent('queue.callabandoned', (userId, queuename: string, queuedcallafterabandon: string): void => { - notifications.notifyUserInThisInstance(userId, 'callabandoned', { queuename, queuedcallafterabandon }); + + service.onEvent('call.callerhangup', (userId, data): void => { + notifications.notifyUserInThisInstance(userId, 'call.hangup', data); }); service.onEvent('notify.desktop', (uid, notification): void => { diff --git a/apps/meteor/server/modules/watchers/watchers.module.ts b/apps/meteor/server/modules/watchers/watchers.module.ts index 24fbe94c0e4..b4914d74218 100644 --- a/apps/meteor/server/modules/watchers/watchers.module.ts +++ b/apps/meteor/server/modules/watchers/watchers.module.ts @@ -344,6 +344,12 @@ export function initWatchers(models: IModelsParam, broadcast: BroadcastCallback, // TODO: Prevent flood from database on username change, what causes changes on all past messages from that user // and most of those messages are not loaded by the clients. watch(Users, ({ clientAction, id, data, diff, unset }) => { + // LivechatCount is updated each time an agent is routed to a chat. This prop is not used on the UI so we don't need + // to broadcast events originated by it when it's the only update on the user + if (diff && Object.keys(diff).length === 1 && 'livechatCount' in diff) { + return; + } + broadcast('watch.users', { clientAction, data, diff, unset, id }); }); diff --git a/apps/meteor/server/sdk/lib/Events.ts b/apps/meteor/server/sdk/lib/Events.ts index 2d22281c241..686ad20931a 100644 --- a/apps/meteor/server/sdk/lib/Events.ts +++ b/apps/meteor/server/sdk/lib/Events.ts @@ -22,6 +22,7 @@ import type { IInvite, IWebdavAccount, ICustomSound, + VoipEventDataSignature, } from '@rocket.chat/core-typings'; import { AutoUpdateRecord } from '../types/IMeteor'; @@ -123,12 +124,10 @@ export type EventSignatures = { diff?: undefined | Record; id: string; }): void; - 'queue.agentcalled'(userid: string, queuename: string, callerid: Record): void; - 'queue.agentconnected'(userid: string, queuename: string, queuedcalls: string, waittimeinqueue: string): void; - 'queue.callerjoined'(userid: string, queuename: string, callerid: Record, queuedcalls: string): void; - 'queue.queuememberadded'(userid: string, queuename: string, queuedcalls: string): void; - 'queue.queuememberremoved'(userid: string, queuename: string, queuedcalls: string): void; - 'queue.callabandoned'(userid: string, queuename: string, queuedcallafterabandon: string): void; + + // Send all events from here + 'voip.events'(userId: string, data: VoipEventDataSignature): void; + 'call.callerhangup'(userId: string, data: { roomId: string }): void; 'watch.pbxevents'(data: { clientAction: ClientAction; data: Partial; id: string }): void; 'connector.statuschanged'(enabled: boolean): void; }; diff --git a/apps/meteor/server/services/omnichannel-voip/service.ts b/apps/meteor/server/services/omnichannel-voip/service.ts index 376e60c470b..e45480bf103 100644 --- a/apps/meteor/server/services/omnichannel-voip/service.ts +++ b/apps/meteor/server/services/omnichannel-voip/service.ts @@ -28,7 +28,7 @@ import { VoipRoomsRaw } from '../../../app/models/server/raw/VoipRooms'; import { PbxEventsRaw } from '../../../app/models/server/raw/PbxEvents'; import { sendMessage } from '../../../app/lib/server/functions/sendMessage'; import { FindVoipRoomsParams } from './internalTypes'; -import { Notifications } from '../../../app/notifications/server'; +import { api } from '../../sdk/api'; export class OmnichannelVoipService extends ServiceClassInternal implements IOmnichannelVoipService { protected name = 'omnichannel-voip'; @@ -80,8 +80,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn return; } this.logger.debug(`Notifying agent ${agent._id} of hangup on room ${currentRoom._id}`); - // TODO evalute why this is 'notifyUserInThisInstance' - Notifications.notifyUserInThisInstance(agent._id, 'call.callerhangup', { roomId: currentRoom._id }); + api.broadcast('call.callerhangup', agent._id, { roomId: currentRoom._id }); } private async processAgentDisconnect(extension: string): Promise { diff --git a/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts b/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts index 8f03835b554..92e84715746 100644 --- a/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts +++ b/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts @@ -89,8 +89,8 @@ export class ContinuousMonitor extends Command { async processQueueMembershipChange(event: IQueueMemberAdded | IQueueMemberRemoved): Promise { const extension = event.interface.toLowerCase().replace('pjsip/', ''); - const queueName = event.queue; - const queueDetails = await this.getQueueDetails(queueName); + const { queue } = event; + const queueDetails = await this.getQueueDetails(queue); const { calls } = queueDetails; const user = await this.users.findOneByExtension(extension, { projection: { @@ -101,9 +101,9 @@ export class ContinuousMonitor extends Command { }); if (user) { if (isIQueueMemberAddedEvent(event)) { - api.broadcast(`queue.queuememberadded`, user._id, queueName, calls); + api.broadcast(`voip.events`, user._id, { data: { queue, queuedCalls: calls }, event: 'queue-member-added' }); } else if (isIQueueMemberRemovedEvent(event)) { - api.broadcast(`queue.queuememberremoved`, user._id, queueName, calls); + api.broadcast(`voip.events`, user._id, { event: 'queue-member-removed', data: { queue, queuedCalls: calls } }); } } } @@ -130,7 +130,8 @@ export class ContinuousMonitor extends Command { name: event.calleridname, }; - api.broadcast('queue.agentcalled', user._id, event.queue, callerId); + api.broadcast('voip.events', user._id, { event: 'agent-called', data: { callerId, queue: event.queue } }); + // api.broadcast('queue.agentcalled', user._id, event.queue, callerId); } async storePbxEvent(event: IQueueEvent | IContactStatus, eventName: string): Promise { @@ -185,7 +186,7 @@ export class ContinuousMonitor extends Command { await this.storePbxEvent(event, 'QueueCallerJoin'); this.logger.debug(`Broadcasting event queue.callerjoined to ${members.length} agents on queue ${event.queue}`); members.forEach((m) => { - api.broadcast('queue.callerjoined', m, event.queue, callerId, event.count); + api.broadcast('voip.events', m, { event: 'caller-joined', data: { callerId, queue: event.queue, queuedCalls: event.count } }); }); break; } @@ -194,7 +195,7 @@ export class ContinuousMonitor extends Command { await this.storePbxEvent(event, 'QueueCallerAbandon'); this.logger.debug(`Broadcasting event queue.callabandoned to ${members.length} agents on queue ${event.queue}`); members.forEach((m) => { - api.broadcast('queue.callabandoned', m, event.queue, calls); + api.broadcast('voip.events', m, { event: 'call-abandoned', data: { queue: event.queue, queuedCallAfterAbandon: calls } }); }); break; } @@ -205,7 +206,10 @@ export class ContinuousMonitor extends Command { this.logger.debug(`Broadcasting event queue.agentconnected to ${members.length} agents on queue ${event.queue}`); members.forEach((m) => { // event.holdtime signifies wait time in the queue. - api.broadcast('queue.agentconnected', m, event.queue, calls, event.holdtime); + api.broadcast('voip.events', m, { + event: 'agent-connected', + data: { queue: event.queue, queuedCalls: calls, waitTimeInQueue: event.holdtime }, + }); }); break; } diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 66fd24282b1..3da6e77348f 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -97,8 +97,6 @@ export * from './IOmnichannelAgent'; export * from './OmichannelRoutingConfig'; export * from './IVoipExtension'; export * from './voip'; -export * from './voip/VoIPUserConfiguration'; -export * from './voip/VoIpCallerInfo'; export * from './ACDQueues'; export * from './IVoipConnectorResult'; export * from './IVoipServerConfig'; diff --git a/packages/core-typings/src/voip/IVoipClientEvents.ts b/packages/core-typings/src/voip/IVoipClientEvents.ts new file mode 100644 index 00000000000..70bd910e3d7 --- /dev/null +++ b/packages/core-typings/src/voip/IVoipClientEvents.ts @@ -0,0 +1,54 @@ +export type VoipPropagatedEvents = + | 'agentcalled' + | 'agentconnected' + | 'callerjoined' + | 'queuememberadded' + | 'queuememberremoved' + | 'callabandoned'; + +export type VoipEventDataSignature = + | { + event: 'agent-called'; + data: { callerId: { id: string; name: string }; queue: string }; + } + | { + event: 'agent-connected'; + data: { queue: string; queuedCalls: string; waitTimeInQueue: string }; + } + | { + event: 'caller-joined'; + data: { callerId: { id: string; name: string }; queue: string; queuedCalls: string }; + } + | { + event: 'queue-member-added'; + data: { queue: string; queuedCalls: string }; + } + | { + event: 'queue-member-removed'; + data: { queue: string; queuedCalls: string }; + } + | { + event: 'call-abandoned'; + data: { queuedCallAfterAbandon: string; queue: string }; + }; + +export const isVoipEventAgentCalled = ( + data: VoipEventDataSignature, +): data is { event: 'agent-called'; data: { callerId: { id: string; name: string }; queue: string } } => data.event === 'agent-called'; +export const isVoipEventAgentConnected = ( + data: VoipEventDataSignature, +): data is { event: 'agent-connected'; data: { queue: string; queuedCalls: string; waitTimeInQueue: string } } => + data.event === 'agent-connected'; +export const isVoipEventCallerJoined = ( + data: VoipEventDataSignature, +): data is { event: 'caller-joined'; data: { callerId: { id: string; name: string }; queue: string; queuedCalls: string } } => + data.event === 'caller-joined'; +export const isVoipEventQueueMemberAdded = ( + data: VoipEventDataSignature, +): data is { event: 'queue-member-added'; data: { queue: string; queuedCalls: string } } => data.event === 'queue-member-added'; +export const isVoipEventQueueMemberRemoved = ( + data: VoipEventDataSignature, +): data is { event: 'queue-member-removed'; data: { queue: string; queuedCalls: string } } => data.event === 'queue-member-removed'; +export const isVoipEventCallAbandoned = ( + data: VoipEventDataSignature, +): data is { event: 'call-abandoned'; data: { queuedCallAfterAbandon: string; queue: string } } => data.event === 'call-abandoned'; diff --git a/packages/core-typings/src/voip/index.ts b/packages/core-typings/src/voip/index.ts index 584d349ea89..07c860310a7 100644 --- a/packages/core-typings/src/voip/index.ts +++ b/packages/core-typings/src/voip/index.ts @@ -12,3 +12,6 @@ export * from './UserState'; export * from './VoipClientEvents'; export * from './VoipEvents'; export * from './WorkflowTypes'; +export * from './IVoipClientEvents'; +export * from './VoIPUserConfiguration'; +export * from './VoIpCallerInfo';