RocketChat/Rocket.Chat

View on GitHub
apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts

Summary

Maintainability
C
1 day
Test Coverage
import type { IOmnichannelRoom, IMessage, IBusinessHourWorkHour, ILivechatDepartment } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatBusinessHours, LivechatDepartment, Messages, LivechatRooms } from '@rocket.chat/models';
import moment from 'moment';

import { callbacks } from '../../../../lib/callbacks';
import { settings } from '../../../settings/server';
import { businessHourManager } from '../business-hour';
import type { CloseRoomParams } from '../lib/localTypes';

export const getSecondsWhenOfficeHoursIsDisabled = (room: IOmnichannelRoom, agentLastMessage: IMessage) =>
    moment(new Date(room.closedAt || new Date())).diff(moment(new Date(agentLastMessage.ts)), 'seconds');

export const parseDays = (
    acc: Record<string, { start: { day: string; time: string }; finish: { day: string; time: string }; open: boolean }>,
    day: IBusinessHourWorkHour,
) => {
    acc[day.day] = {
        start: { day: day.start.utc.dayOfWeek, time: day.start.utc.time },
        finish: { day: day.finish.utc.dayOfWeek, time: day.finish.utc.time },
        open: day.open,
    };
    return acc;
};

export const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLastMessage: IMessage) => {
    if (!settings.get('Livechat_enable_business_hours')) {
        return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
    }
    let officeDays;
    const department = room.departmentId
        ? await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'businessHourId'>>(room.departmentId, {
                projection: { businessHourId: 1 },
          })
        : null;
    if (department?.businessHourId) {
        const businessHour = await LivechatBusinessHours.findOneById(department.businessHourId);
        if (!businessHour) {
            return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
        }

        officeDays = (await businessHourManager.getBusinessHour(businessHour._id, businessHour.type))?.workHours.reduce(parseDays, {});
    } else {
        officeDays = (await businessHourManager.getBusinessHour())?.workHours.reduce(parseDays, {});
    }

    // Empty object we assume invalid config
    if (!officeDays || !Object.keys(officeDays).length) {
        return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
    }

    let totalSeconds = 0;
    const endOfConversation = moment.utc(new Date(room.closedAt || new Date()));
    const startOfInactivity = moment.utc(new Date(agentLastMessage.ts));
    const daysOfInactivity = endOfConversation.clone().startOf('day').diff(startOfInactivity.clone().startOf('day'), 'days');
    const inactivityDay = moment.utc(new Date(agentLastMessage.ts));

    for (let index = 0; index <= daysOfInactivity; index++) {
        const today = inactivityDay.clone().format('dddd');
        const officeDay = officeDays[today];

        if (!officeDay) {
            inactivityDay.add(1, 'days');
            continue;
        }

        if (!officeDay.open) {
            inactivityDay.add(1, 'days');
            continue;
        }

        if (!officeDay?.start?.time || !officeDay?.finish?.time) {
            inactivityDay.add(1, 'days');
            continue;
        }

        const [officeStartHour, officeStartMinute] = officeDay.start.time.split(':');
        const [officeCloseHour, officeCloseMinute] = officeDay.finish.time.split(':');
        // We should only take in consideration the time where the office is open and the conversation was inactive
        const todayStartOfficeHours = inactivityDay
            .clone()
            .set({ hour: parseInt(officeStartHour, 10), minute: parseInt(officeStartMinute, 10) });
        const todayEndOfficeHours = inactivityDay.clone().set({ hour: parseInt(officeCloseHour, 10), minute: parseInt(officeCloseMinute, 10) });

        // 1: Room was inactive the whole day, we add the whole time BH is inactive
        if (startOfInactivity.isBefore(todayStartOfficeHours) && endOfConversation.isAfter(todayEndOfficeHours)) {
            totalSeconds += todayEndOfficeHours.diff(todayStartOfficeHours, 'seconds');
        }

        // 2: Room was inactive before start but was closed before end of BH, we add the inactive time
        if (startOfInactivity.isBefore(todayStartOfficeHours) && endOfConversation.isBefore(todayEndOfficeHours)) {
            totalSeconds += endOfConversation.diff(todayStartOfficeHours, 'seconds');
        }

        // 3: Room was inactive after start and ended after end of BH, we add the inactive time
        if (startOfInactivity.isAfter(todayStartOfficeHours) && endOfConversation.isAfter(todayEndOfficeHours)) {
            totalSeconds += todayEndOfficeHours.diff(startOfInactivity, 'seconds');
        }

        // 4: Room was inactive after start and before end of BH, we add the inactive time
        if (startOfInactivity.isAfter(todayStartOfficeHours) && endOfConversation.isBefore(todayEndOfficeHours)) {
            totalSeconds += endOfConversation.diff(startOfInactivity, 'seconds');
        }

        inactivityDay.add(1, 'days');
    }
    return totalSeconds;
};

export const onCloseRoom = async (params: { room: IOmnichannelRoom; options: CloseRoomParams['options'] }) => {
    const { room } = params;

    if (!isOmnichannelRoom(room)) {
        return params;
    }

    const closedByAgent = room.closer !== 'visitor';
    const wasTheLastMessageSentByAgent = room.lastMessage && !room.lastMessage.token;
    if (!closedByAgent || !wasTheLastMessageSentByAgent) {
        return params;
    }

    if (!room.v?.lastMessageTs) {
        return params;
    }

    const agentLastMessage = await Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs);
    if (!agentLastMessage) {
        return params;
    }
    const secondsSinceLastAgentResponse = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
    await LivechatRooms.setVisitorInactivityInSecondsById(room._id, secondsSinceLastAgentResponse);

    return params;
};

callbacks.add('livechat.closeRoom', onCloseRoom, callbacks.priority.HIGH, 'process-room-abandonment');