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

feat(apps-engine): add customFields on livechat creation #32328

Merged
merged 6 commits into from
Jun 25, 2024
Merged
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/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
Loading