Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test PR #33116

Closed
wants to merge 5 commits into from
Closed

Test PR #33116

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/spicy-kings-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
---

Fixes multiple problems with the `processRoomAbandonment` hook. This hook is in charge of calculating the time a room has been abandoned (this means, the time that elapsed since a room was last answered by an agent until it was closed). However, when business hours were enabled and the user didn't open on one day, if an abandoned room happened to be abandoned _over_ the day there was no business hour configuration, then the process will error out.
Additionally, the values the code was calculating were not right. When business hours are enabled, this code should only count the abandonment time _while a business hour was open_. When rooms were left abandoned for days or weeks, this will also throw an error or output an invalid count.
4 changes: 4 additions & 0 deletions .github/actions/build-docker/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ inputs:
required: false
description: 'Setup node.js'
default: 'true'
NPM_TOKEN:
required: false
description: 'NPM token'

runs:
using: composite
Expand Down Expand Up @@ -65,6 +68,7 @@ runs:
node-version: ${{ inputs.node-version }}
cache-modules: true
install: true
NPM_TOKEN: ${{ inputs.NPM_TOKEN }}

- run: yarn build
if: inputs.setup == 'true'
Expand Down
4 changes: 4 additions & 0 deletions .github/actions/meteor-build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ inputs:
required: true
description: 'Node version'
type: string
NPM_TOKEN:
required: false
description: 'NPM token'

runs:
using: composite
Expand All @@ -29,6 +32,7 @@ runs:
node-version: ${{ inputs.node-version }}
cache-modules: true
install: true
NPM_TOKEN: ${{ inputs.NPM_TOKEN }}

# - name: Free disk space
# run: |
Expand Down
20 changes: 16 additions & 4 deletions .github/actions/setup-node/action.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
name: 'Setup Node'
description: 'Setup NodeJS'

inputs:
node-version:
required: true
type: string
description: 'Node version'
cache-modules:
required: false
type: boolean
description: 'Cache node_modules'
install:
required: false
type: boolean
description: 'Install dependencies'
deno-dir:
required: false
type: string
description: 'Deno directory'
default: ~/.deno-cache
NPM_TOKEN:
required: false
description: 'NPM token'

outputs:
node-version:
description: 'Node version'
value: ${{ steps.node-version.outputs.node-version }}

runs:
Expand Down Expand Up @@ -49,6 +54,13 @@ runs:
node-version: ${{ inputs.node-version }}
cache: 'yarn'

- name: yarn login
shell: bash
if: inputs.NPM_TOKEN
run: |
echo "//registry.npmjs.org/:_authToken=${{ inputs.NPM_TOKEN }}" > ~/.npmrc

- name: yarn install
if: inputs.install
shell: bash
run: yarn
1 change: 1 addition & 0 deletions .github/workflows/ci-code-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
node-version: ${{ inputs.node-version }}
cache-modules: true
install: true
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

# - name: Free disk space
# run: |
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci-test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ jobs:
node-version: ${{ inputs.node-version }}
cache-modules: true
install: true
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- uses: rharkor/[email protected]

- run: yarn build
Expand All @@ -145,6 +147,7 @@ jobs:
# the same reason we need to rebuild the docker image at this point is the reason we dont want to publish it
publish-image: false
setup: false
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Start httpbin container and wait for it to be ready
if: inputs.type == 'api'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci-test-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
node-version: ${{ inputs.node-version }}
cache-modules: true
install: true
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- uses: rharkor/[email protected]

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ jobs:
node-version: ${{ needs.release-versions.outputs.node-version }}
cache-modules: true
install: true
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Cache vite
uses: actions/cache@v3
Expand Down Expand Up @@ -253,6 +254,7 @@ jobs:
node-version: ${{ needs.release-versions.outputs.node-version }}
platform: ${{ matrix.platform }}
build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

build-gh-docker:
name: 🚢 Build Docker Images for Production
Expand Down Expand Up @@ -280,6 +282,7 @@ jobs:
node-version: ${{ needs.release-versions.outputs.node-version }}
platform: ${{ matrix.platform }}
build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Rename official Docker tag to GitHub Container Registry
if: matrix.platform == 'official'
Expand Down Expand Up @@ -560,6 +563,7 @@ jobs:
release: preview
username: ${{ secrets.CR_USER }}
password: ${{ secrets.CR_PAT }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

docker-image-publish:
name: 🚀 Publish Docker Image (main)
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/new-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
node-version: 14.21.3
cache-modules: true
install: true
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- uses: rharkor/[email protected]

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr-update-description.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
node-version: 14.21.3
cache-modules: true
install: true
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- uses: rharkor/[email protected]

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
node-version: 14.21.3
cache-modules: true
install: true
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- uses: rharkor/[email protected]

Expand Down
111 changes: 66 additions & 45 deletions apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ 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';

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

const parseDays = (
export const parseDays = (
acc: Record<string, { start: { day: string; time: string }; finish: { day: string; time: string }; open: boolean }>,
day: IBusinessHourWorkHour,
) => {
Expand All @@ -22,7 +23,7 @@ const parseDays = (
return acc;
};

const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLastMessage: IMessage) => {
export const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLastMessage: IMessage) => {
if (!settings.get('Livechat_enable_business_hours')) {
return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
}
Expand All @@ -49,65 +50,85 @@ const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLas
}

let totalSeconds = 0;
const endOfConversation = moment(new Date(room.closedAt || new Date()));
const startOfInactivity = moment(new Date(agentLastMessage.ts));
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(new Date(agentLastMessage.ts));
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];
// Config doesnt have data for this day, we skip day
if (!officeDay) {
inactivityDay.add(1, 'days');
continue;
}
const startTodaysOfficeHour = moment(`${officeDay.start.day}:${officeDay.start.time}`, 'dddd:HH:mm').add(index, 'days');
const endTodaysOfficeHour = moment(`${officeDay.finish.day}:${officeDay.finish.time}`, 'dddd:HH:mm').add(index, 'days');
if (officeDays[today].open) {
const firstDayOfInactivity = startOfInactivity.clone().format('D') === inactivityDay.clone().format('D');
const lastDayOfInactivity = endOfConversation.clone().format('D') === inactivityDay.clone().format('D');

if (!firstDayOfInactivity && !lastDayOfInactivity) {
totalSeconds += endTodaysOfficeHour.clone().diff(startTodaysOfficeHour, 'seconds');
} else {
const end = endOfConversation.isBefore(endTodaysOfficeHour) ? endOfConversation : endTodaysOfficeHour;
const start = firstDayOfInactivity ? inactivityDay : startTodaysOfficeHour;
totalSeconds += end.clone().diff(start, 'seconds');
}
if (!officeDay.open) {
inactivityDay.add(1, 'days');
continue;
}
if (!officeDay?.start?.time || !officeDay?.finish?.time) {
inactivityDay.add(1, 'days');
continue;
}
inactivityDay.add(1, 'days');
}
return totalSeconds;
};

callbacks.add(
'livechat.closeRoom',
async (params) => {
const { room } = params;
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) });

if (!isOmnichannelRoom(room)) {
return params;
// 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');
}

const closedByAgent = room.closer !== 'visitor';
const wasTheLastMessageSentByAgent = room.lastMessage && !room.lastMessage.token;
if (!closedByAgent || !wasTheLastMessageSentByAgent) {
return params;
// 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');
}

if (!room.v?.lastMessageTs) {
return params;
// 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');
}

const agentLastMessage = await Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs);
if (!agentLastMessage) {
return params;
// 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');
}
const secondsSinceLastAgentResponse = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
await LivechatRooms.setVisitorInactivityInSecondsById(room._id, secondsSinceLastAgentResponse);

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;
},
callbacks.priority.HIGH,
'process-room-abandonment',
);
}

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');
10 changes: 7 additions & 3 deletions apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isEditedMessage } from '@rocket.chat/core-typings';
import { isEditedMessage, isMessageFromVisitor } from '@rocket.chat/core-typings';
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';

Expand Down Expand Up @@ -70,8 +70,12 @@ callbacks.add(
message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } };
}

const analyticsData = getAnalyticsData(room, new Date());
await LivechatRooms.getAnalyticsUpdateQueryByRoomId(room, message, analyticsData, roomUpdater);
if (isMessageFromVisitor(message)) {
LivechatRooms.getAnalyticsUpdateQueryBySentByVisitor(room, message, roomUpdater);
} else {
const analyticsData = getAnalyticsData(room, new Date());
LivechatRooms.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData, roomUpdater);
}

return message;
},
Expand Down
Loading
Loading