Skip to content

Commit

Permalink
feat(apps-engine): add customFields on livechat creation (#32328)
Browse files Browse the repository at this point in the history
  • Loading branch information
ggazzo authored Jun 25, 2024
1 parent 209a062 commit 24f7df4
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 107 deletions.
6 changes: 6 additions & 0 deletions .changeset/rare-penguins-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-typings": patch
---

Allow customFields on livechat creation bridge
32 changes: 19 additions & 13 deletions apps/meteor/app/apps/server/bridges/livechat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import { getRoom } from '../../../livechat/server/api/lib/livechat';
import { type ILivechatMessage, Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped';
import { settings } from '../../../settings/server';

declare module '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator' {
interface IExtraRoomParams {
customFields?: Record<string, any>;
}
}

export class AppLivechatBridge extends LivechatBridge {
constructor(private readonly orch: IAppServerOrchestrator) {
super();
Expand Down Expand Up @@ -79,17 +85,14 @@ export class AppLivechatBridge extends LivechatBridge {
await LivechatTyped.updateMessage(data);
}

protected async createRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise<ILivechatRoom> {
protected async createRoom(
visitor: IVisitor,
agent: IUser,
appId: string,
{ source, customFields }: IExtraRoomParams = {},
): Promise<ILivechatRoom> {
this.orch.debugLog(`The App ${appId} is creating a livechat room.`);

const { source } = extraParams || {};
// `source` will likely have the properties below, so we tell TS it's alright
const { sidebarIcon, defaultIcon, label } = (source || {}) as {
sidebarIcon?: string;
defaultIcon?: string;
label?: string;
};

let agentRoom: SelectedAgent | undefined;
if (agent?.id) {
const user = await Users.getAgentInfo(agent.id, settings.get('Livechat_show_agent_email'));
Expand All @@ -108,12 +111,15 @@ export class AppLivechatBridge extends LivechatBridge {
type: OmnichannelSourceType.APP,
id: appId,
alias: this.orch.getManager()?.getOneById(appId)?.getName(),
label,
sidebarIcon,
defaultIcon,
...(source &&
source.type === 'app' && {
sidebarIcon: source.sidebarIcon,
defaultIcon: source.defaultIcon,
label: source.label,
}),
},
},
extraParams: undefined,
extraParams: customFields && { customFields },
});

// #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed.
Expand Down
8 changes: 8 additions & 0 deletions apps/meteor/app/apps/server/bridges/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,12 @@ export class AppRoomBridge extends RoomBridge {
const userConverter = this.orch.getConverters().get('users');
return users.map((user: ICoreUser) => userConverter.convertToApp(user));
}

protected getMessages(
_roomId: string,
_options: { limit: number; skip?: number; sort?: Record<string, 1 | -1> },
_appId: string,
): Promise<IMessage[]> {
throw new Error('Method not implemented.');
}
}
11 changes: 9 additions & 2 deletions apps/meteor/app/livechat/server/api/lib/livechat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
ILivechatVisitor,
IOmnichannelRoom,
SelectedAgent,
OmnichannelSourceType,
} from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { EmojiCustom, LivechatTrigger, LivechatVisitors, LivechatRooms, LivechatDepartment } from '@rocket.chat/models';
Expand Down Expand Up @@ -104,7 +105,13 @@ export async function findOpenRoom(token: string, departmentId?: string): Promis
return rooms[0];
}
}
export function getRoom({
export function getRoom<
E extends Record<string, unknown> & {
sla?: string;
customFields?: Record<string, unknown>;
source?: OmnichannelSourceType;
},
>({
guest,
rid,
roomInfo,
Expand All @@ -117,7 +124,7 @@ export function getRoom({
source?: IOmnichannelRoom['source'];
};
agent?: SelectedAgent;
extraParams?: Record<string, any>;
extraParams?: E;
}): Promise<{ room: IOmnichannelRoom; newRoom: boolean }> {
const token = guest?.token;

Expand Down
73 changes: 39 additions & 34 deletions apps/meteor/app/livechat/server/lib/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,18 @@ export const allowAgentSkipQueue = (agent: SelectedAgent) => {

return hasRoleAsync(agent.agentId, 'bot');
};
export const createLivechatRoom = async (
export const createLivechatRoom = async <
E extends Record<string, unknown> & {
sla?: string;
customFields?: Record<string, unknown>;
source?: OmnichannelSourceType;
},
>(
rid: string,
name: string,
guest: ILivechatVisitor,
roomInfo: Partial<IOmnichannelRoom> = {},
extraData = {},
extraData?: E,
) => {
check(rid, String);
check(name, String);
Expand All @@ -86,39 +92,38 @@ export const createLivechatRoom = async (
visitor: { _id, username, departmentId, status, activity },
});

const room: InsertionModel<IOmnichannelRoom> = Object.assign(
{
_id: rid,
msgs: 0,
usersCount: 1,
lm: newRoomAt,
fname: name,
t: 'l' as const,
ts: newRoomAt,
departmentId,
v: {
_id,
username,
token,
status,
...(activity?.length && { activity }),
},
cl: false,
open: true,
waitingResponse: true,
// this should be overriden by extraRoomInfo when provided
// in case it's not provided, we'll use this "default" type
source: {
type: OmnichannelSourceType.OTHER,
alias: 'unknown',
},
queuedAt: newRoomAt,

priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED,
estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE,
// TODO: Solve `u` missing issue
const room: InsertionModel<IOmnichannelRoom> = {
_id: rid,
msgs: 0,
usersCount: 1,
lm: newRoomAt,
fname: name,
t: 'l' as const,
ts: newRoomAt,
departmentId,
v: {
_id,
username,
token,
status,
...(activity?.length && { activity }),
},
extraRoomInfo,
);
cl: false,
open: true,
waitingResponse: true,
// this should be overridden by extraRoomInfo when provided
// in case it's not provided, we'll use this "default" type
source: {
type: OmnichannelSourceType.OTHER,
alias: 'unknown',
},
queuedAt: newRoomAt,
livechatData: undefined,
priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED,
estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE,
...extraRoomInfo,
} as InsertionModel<IOmnichannelRoom>;

const roomId = (await Rooms.insertOne(room)).insertedId;

Expand Down
11 changes: 9 additions & 2 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
IOmnichannelAgent,
ILivechatDepartmentAgents,
LivechatDepartmentDTO,
OmnichannelSourceType,
} from '@rocket.chat/core-typings';
import { ILivechatAgentStatus, UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings';
import { Logger, type MainLogger } from '@rocket.chat/logger';
Expand Down Expand Up @@ -383,15 +384,21 @@ class LivechatClass {
}
}

async getRoom(
async getRoom<
E extends Record<string, unknown> & {
sla?: string;
customFields?: Record<string, unknown>;
source?: OmnichannelSourceType;
},
>(
guest: ILivechatVisitor,
message: Pick<IMessage, 'rid' | 'msg' | 'token'>,
roomInfo: {
source?: IOmnichannelRoom['source'];
[key: string]: unknown;
},
agent?: SelectedAgent,
extraData?: Record<string, unknown>,
extraData?: E,
) {
if (!this.enabled()) {
throw new Meteor.Error('error-omnichannel-is-disabled');
Expand Down
20 changes: 16 additions & 4 deletions apps/meteor/app/livechat/server/lib/QueueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type IMessage,
type IOmnichannelRoom,
type SelectedAgent,
type OmnichannelSourceType,
} from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import { LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';
Expand Down Expand Up @@ -65,21 +66,27 @@ export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent
};

type queueManager = {
requestRoom: (params: {
requestRoom: <
E extends Record<string, unknown> & {
sla?: string;
customFields?: Record<string, unknown>;
source?: OmnichannelSourceType;
},
>(params: {
guest: ILivechatVisitor;
message: Pick<IMessage, 'rid' | 'msg'>;
roomInfo: {
source?: IOmnichannelRoom['source'];
[key: string]: unknown;
};
agent?: SelectedAgent;
extraData?: Record<string, unknown>;
extraData?: E;
}) => Promise<IOmnichannelRoom>;
unarchiveRoom: (archivedRoom?: IOmnichannelRoom) => Promise<IOmnichannelRoom>;
};

export const QueueManager: queueManager = {
async requestRoom({ guest, message, roomInfo, agent, extraData }) {
async requestRoom({ guest, message, roomInfo, agent, extraData: { customFields, ...extraData } = {} }) {
logger.debug(`Requesting a room for guest ${guest._id}`);
check(
message,
Expand All @@ -106,7 +113,12 @@ export const QueueManager: queueManager = {
const { rid } = message;
const name = (roomInfo?.fname as string) || guest.name || guest.username;

const room = await LivechatRooms.findOneById(await createLivechatRoom(rid, name, guest, roomInfo, extraData));
const room = await LivechatRooms.findOneById(
await createLivechatRoom(rid, name, guest, roomInfo, {
...(Boolean(customFields) && { customFields }),
...extraData,
}),
);
if (!room) {
logger.error(`Room for visitor ${guest._id} not found`);
throw new Error('room-not-found');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,49 @@ import { Meteor } from 'meteor/meteor';

import { callbacks } from '../../../../../lib/callbacks';

type Props = {
sla?: string;
priority?: string;
[other: string]: any;
};

const beforeNewInquiry = async (extraData: Props) => {
const { sla: slaSearchTerm, priority: prioritySearchTerm, ...props } = extraData;
if (!slaSearchTerm && !prioritySearchTerm) {
return extraData;
}

let sla: IOmnichannelServiceLevelAgreements | null = null;
let priority: ILivechatPriority | null = null;

if (slaSearchTerm) {
sla = await OmnichannelServiceLevelAgreements.findOneByIdOrName(slaSearchTerm, {
projection: { dueTimeInMinutes: 1 },
});
if (!sla) {
throw new Meteor.Error('error-invalid-sla', 'Invalid sla', {
function: 'livechat.beforeInquiry',
callbacks.add(
'livechat.beforeInquiry',
async (extraData) => {
const { sla: slaSearchTerm, priority: prioritySearchTerm, ...props } = extraData;
if (!slaSearchTerm && !prioritySearchTerm) {
return extraData;
}
let sla: IOmnichannelServiceLevelAgreements | null = null;
let priority: ILivechatPriority | null = null;
if (slaSearchTerm) {
sla = await OmnichannelServiceLevelAgreements.findOneByIdOrName(slaSearchTerm, {
projection: { dueTimeInMinutes: 1 },
});
if (!sla) {
throw new Meteor.Error('error-invalid-sla', 'Invalid sla', {
function: 'livechat.beforeInquiry',
});
}
}
}
if (prioritySearchTerm) {
priority = await LivechatPriority.findOneByIdOrName(prioritySearchTerm, {
projection: { _id: 1, sortItem: 1 },
});
if (!priority) {
throw new Meteor.Error('error-invalid-priority', 'Invalid priority', {
function: 'livechat.beforeInquiry',
if (prioritySearchTerm) {
priority = await LivechatPriority.findOneByIdOrName(prioritySearchTerm, {
projection: { _id: 1, sortItem: 1 },
});
if (!priority) {
throw new Meteor.Error('error-invalid-priority', 'Invalid priority', {
function: 'livechat.beforeInquiry',
});
}
}
}

const ts = new Date();
const changes: Partial<ILivechatInquiryRecord> = {
ts,
};
if (sla) {
changes.slaId = sla._id;
changes.estimatedWaitingTimeQueue = sla.dueTimeInMinutes;
}
if (priority) {
changes.priorityId = priority._id;
changes.priorityWeight = priority.sortItem;
}
return { ...props, ...changes };
};

callbacks.add('livechat.beforeInquiry', beforeNewInquiry, callbacks.priority.MEDIUM, 'livechat-before-new-inquiry');
const ts = new Date();
const changes: Partial<ILivechatInquiryRecord> = {
ts,
};
if (sla) {
changes.slaId = sla._id;
changes.estimatedWaitingTimeQueue = sla.dueTimeInMinutes;
}
if (priority) {
changes.priorityId = priority._id;
changes.priorityWeight = priority.sortItem;
}
return { ...props, ...changes };
},
callbacks.priority.MEDIUM,
'livechat-before-new-inquiry',
);
Loading

0 comments on commit 24f7df4

Please sign in to comment.