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

pull/32608/merge
Guilherme Gazzo 2 years ago committed by GitHub
parent 209a062815
commit 24f7df4894
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/rare-penguins-hope.md
  2. 32
      apps/meteor/app/apps/server/bridges/livechat.ts
  3. 8
      apps/meteor/app/apps/server/bridges/rooms.ts
  4. 11
      apps/meteor/app/livechat/server/api/lib/livechat.ts
  5. 73
      apps/meteor/app/livechat/server/lib/Helper.ts
  6. 11
      apps/meteor/app/livechat/server/lib/LivechatTyped.ts
  7. 20
      apps/meteor/app/livechat/server/lib/QueueManager.ts
  8. 90
      apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewInquiry.ts
  9. 12
      apps/meteor/lib/callbacks.ts
  10. 4
      packages/core-typings/src/IRoom.ts

@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-typings": patch
---
Allow customFields on livechat creation bridge

@ -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();
@ -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'));
@ -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.

@ -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.');
}
}

@ -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';
@ -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,
@ -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;

@ -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);
@ -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;

@ -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';
@ -383,7 +384,13 @@ 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: {
@ -391,7 +398,7 @@ class LivechatClass {
[key: string]: unknown;
},
agent?: SelectedAgent,
extraData?: Record<string, unknown>,
extraData?: E,
) {
if (!this.enabled()) {
throw new Meteor.Error('error-omnichannel-is-disabled');

@ -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';
@ -65,7 +66,13 @@ 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: {
@ -73,13 +80,13 @@ type queueManager = {
[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,
@ -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');

@ -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',
);

@ -21,6 +21,7 @@ import type {
UserStatus,
ILivechatDepartment,
MessageMention,
OmnichannelSourceType,
} from '@rocket.chat/core-typings';
import type { FilterOperators } from 'mongodb';
@ -199,7 +200,15 @@ type ChainedCallbackSignatures = {
options: { forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any }; clientAction?: boolean };
},
) => Promise<(IOmnichannelRoom & { chatQueued: boolean }) | undefined>;
'livechat.beforeInquiry': (data: Pick<ILivechatInquiryRecord, 'source'>) => Pick<ILivechatInquiryRecord, 'source'>;
'livechat.beforeInquiry': (
data: Pick<ILivechatInquiryRecord, 'source'> & { sla?: string; priority?: string; [other: string]: unknown } & {
customFields?: Record<string, unknown>;
source?: OmnichannelSourceType;
},
) => Pick<ILivechatInquiryRecord, 'source'> & { sla?: string; priority?: string; [other: string]: unknown } & {
customFields?: Record<string, unknown>;
source?: OmnichannelSourceType;
};
'roomNameChanged': (room: IRoom) => void;
'roomTopicChanged': (room: IRoom) => void;
'roomAnnouncementChanged': (room: IRoom) => void;
@ -223,7 +232,6 @@ export type Hook =
| 'beforeRemoveFromRoom'
| 'beforeValidateLogin'
| 'livechat.beforeForwardRoomToDepartment'
| 'livechat.beforeRoom'
| 'livechat.beforeRouteChat'
| 'livechat.chatQueued'
| 'livechat.checkAgentBeforeTakeInquiry'

@ -150,7 +150,7 @@ export enum OmnichannelSourceType {
OTHER = 'other', // catch-all source type
}
export interface IOmnichannelGenericRoom extends Omit<IRoom, 'default' | 'featured' | 'broadcast' | ''> {
export interface IOmnichannelGenericRoom extends Omit<IRoom, 'default' | 'featured' | 'broadcast'> {
t: 'l' | 'v';
v: Pick<ILivechatVisitor, '_id' | 'username' | 'status' | 'name' | 'token' | 'activity'> & {
lastMessageTs?: Date;
@ -331,7 +331,7 @@ export const isOmnichannelRoom = (room: Pick<IRoom, 't'>): room is IOmnichannelR
export const isVoipRoom = (room: IRoom): room is IVoipRoom & IRoom => room.t === 'v';
export const isOmnichannelRoomFromAppSource = (room: IRoom): room is IOmnichannelRoomFromAppSource => {
export const isOmnichannelRoomFromAppSource = (room: IOmnichannelRoom): room is IOmnichannelRoomFromAppSource => {
if (!isOmnichannelRoom(room)) {
return false;
}

Loading…
Cancel
Save