[NEW] [EE] PDF Chat transcript for Omnichannel conversations (#27572)
Co-authored-by: Kevin Aleman <kaleman960@gmail.com> Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Co-authored-by: murtaza98 <murtaza.patrawala@rocket.chat> Co-authored-by: Diego Sampaio <chinello@gmail.com>pull/28031/head
parent
e44f506918
commit
813cdfbe45
@ -0,0 +1,5 @@ |
||||
module.exports = { |
||||
helpers: { |
||||
random: () => Math.floor(3000 + (5000 - 3000) * Math.random()), |
||||
}, |
||||
}; |
||||
@ -1,46 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { callbacks } from '../../../../lib/callbacks'; |
||||
import { LivechatDepartment } from '../../../models/server'; |
||||
|
||||
const concatUnique = (...arrays) => [...new Set([].concat(...arrays.filter(Array.isArray)))]; |
||||
|
||||
const normalizeParams = (params, tags = []) => Object.assign(params, { extraData: { tags } }); |
||||
|
||||
callbacks.add( |
||||
'livechat.beforeCloseRoom', |
||||
(originalParams = {}) => { |
||||
const { room, options } = originalParams; |
||||
const { departmentId, tags: optionsTags } = room; |
||||
const { clientAction, tags: oldRoomTags } = options; |
||||
const roomTags = concatUnique(oldRoomTags, optionsTags); |
||||
|
||||
if (!departmentId) { |
||||
return normalizeParams({ ...originalParams }, roomTags); |
||||
} |
||||
|
||||
const department = LivechatDepartment.findOneById(departmentId); |
||||
if (!department) { |
||||
return normalizeParams({ ...originalParams }, roomTags); |
||||
} |
||||
|
||||
const { requestTagBeforeClosingChat, chatClosingTags } = department; |
||||
const extraRoomTags = concatUnique(roomTags, chatClosingTags); |
||||
|
||||
if (!requestTagBeforeClosingChat) { |
||||
return normalizeParams({ ...originalParams }, extraRoomTags); |
||||
} |
||||
|
||||
const checkRoomTags = !clientAction || (roomTags && roomTags.length > 0); |
||||
const checkDepartmentTags = chatClosingTags && chatClosingTags.length > 0; |
||||
if (!checkRoomTags || !checkDepartmentTags) { |
||||
throw new Meteor.Error('error-tags-must-be-assigned-before-closing-chat', 'Tag(s) must be assigned before closing the chat', { |
||||
method: 'livechat.beforeCloseRoom', |
||||
}); |
||||
} |
||||
|
||||
return normalizeParams({ ...originalParams }, extraRoomTags); |
||||
}, |
||||
callbacks.priority.HIGH, |
||||
'livechat-before-close-Room', |
||||
); |
||||
@ -0,0 +1,71 @@ |
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import { isOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import { LivechatRooms } from '@rocket.chat/models'; |
||||
|
||||
import { callbacks } from '../../../../lib/callbacks'; |
||||
import { Livechat } from '../lib/Livechat'; |
||||
import type { CloseRoomParams } from '../lib/LivechatTyped.d'; |
||||
|
||||
type LivechatCloseCallbackParams = { |
||||
room: IOmnichannelRoom; |
||||
options: CloseRoomParams['options']; |
||||
}; |
||||
|
||||
const sendEmailTranscriptOnClose = async (params: LivechatCloseCallbackParams): Promise<LivechatCloseCallbackParams> => { |
||||
const { room, options } = params; |
||||
|
||||
if (!isOmnichannelRoom(room)) { |
||||
return params; |
||||
} |
||||
|
||||
const { _id: rid, v: { token } = {} } = room; |
||||
if (!token) { |
||||
return params; |
||||
} |
||||
|
||||
const transcriptData = resolveTranscriptData(room, options); |
||||
if (!transcriptData) { |
||||
return params; |
||||
} |
||||
|
||||
const { email, subject, requestedBy: user } = transcriptData; |
||||
|
||||
await Promise.all([ |
||||
Livechat.sendTranscript({ token, rid, email, subject, user }), |
||||
LivechatRooms.unsetEmailTranscriptRequestedByRoomId(rid), |
||||
]); |
||||
|
||||
delete room.transcriptRequest; |
||||
|
||||
return { |
||||
room, |
||||
options, |
||||
}; |
||||
}; |
||||
|
||||
const resolveTranscriptData = ( |
||||
room: IOmnichannelRoom, |
||||
options: LivechatCloseCallbackParams['options'] = {}, |
||||
): IOmnichannelRoom['transcriptRequest'] | undefined => { |
||||
const { transcriptRequest: roomTranscriptRequest } = room; |
||||
|
||||
const { emailTranscript: optionsTranscriptRequest } = options; |
||||
|
||||
// Note: options.emailTranscript will override the room.transcriptRequest check
|
||||
// If options.emailTranscript is not set, then the room.transcriptRequest will be checked
|
||||
if (optionsTranscriptRequest === undefined) { |
||||
return roomTranscriptRequest; |
||||
} |
||||
|
||||
if (!optionsTranscriptRequest.sendToVisitor) { |
||||
return undefined; |
||||
} |
||||
return optionsTranscriptRequest.requestData; |
||||
}; |
||||
|
||||
callbacks.add( |
||||
'livechat.closeRoom', |
||||
(params: LivechatCloseCallbackParams) => Promise.await(sendEmailTranscriptOnClose(params)), |
||||
callbacks.priority.HIGH, |
||||
'livechat-send-email-transcript-on-close-room', |
||||
); |
||||
@ -1,20 +0,0 @@ |
||||
import { callbacks } from '../../../../lib/callbacks'; |
||||
import { Livechat } from '../lib/Livechat'; |
||||
import { LivechatRooms } from '../../../models/server'; |
||||
|
||||
const sendTranscriptOnClose = (room) => { |
||||
const { _id: rid, transcriptRequest, v: { token } = {} } = room; |
||||
if (!transcriptRequest || !token) { |
||||
return room; |
||||
} |
||||
|
||||
const { email, subject, requestedBy: user } = transcriptRequest; |
||||
// TODO: refactor this to use normal await
|
||||
Promise.await(Livechat.sendTranscript({ token, rid, email, subject, user })); |
||||
|
||||
LivechatRooms.removeTranscriptRequestByRoomId(rid); |
||||
|
||||
return LivechatRooms.findOneById(rid); |
||||
}; |
||||
|
||||
callbacks.add('livechat.closeRoom', sendTranscriptOnClose, callbacks.priority.HIGH, 'livechat-send-transcript-on-close-room'); |
||||
@ -0,0 +1,31 @@ |
||||
import type { IOmnichannelRoom, IUser, ILivechatVisitor } from '@rocket.chat/core-typings'; |
||||
|
||||
type GenericCloseRoomParams = { |
||||
room: IOmnichannelRoom; |
||||
comment?: string; |
||||
options?: { |
||||
clientAction?: boolean; |
||||
tags?: string[]; |
||||
emailTranscript?: |
||||
| { |
||||
sendToVisitor: false; |
||||
} |
||||
| { |
||||
sendToVisitor: true; |
||||
requestData: NonNullable<IOmnichannelRoom['transcriptRequest']>; |
||||
}; |
||||
pdfTranscript?: { |
||||
requestedBy: string; |
||||
}; |
||||
}; |
||||
}; |
||||
|
||||
export type CloseRoomParamsByUser = { |
||||
user: IUser; |
||||
} & GenericCloseRoomParams; |
||||
|
||||
export type CloseRoomParamsByVisitor = { |
||||
visitor: ILivechatVisitor; |
||||
} & GenericCloseRoomParams; |
||||
|
||||
export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor; |
||||
@ -0,0 +1,182 @@ |
||||
// Goal is to have a typed version of apps/meteor/app/livechat/server/lib/Livechat.js
|
||||
// This is a work in progress, and is not yet complete
|
||||
// But it is a start.
|
||||
|
||||
// Important note: Try to not use the original Livechat.js file, but use this one instead.
|
||||
// If possible, move methods from Livechat.js to this file.
|
||||
// This is because we want to slowly convert the code to typescript, and this is a good way to do it.
|
||||
import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo } from '@rocket.chat/core-typings'; |
||||
import { isOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import { LivechatDepartment, LivechatInquiry, LivechatRooms, Subscriptions } from '@rocket.chat/models'; |
||||
|
||||
import { callbacks } from '../../../../lib/callbacks'; |
||||
import { Logger } from '../../../logger/server'; |
||||
import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './LivechatTyped.d'; |
||||
import { sendMessage } from '../../../lib/server/functions/sendMessage'; |
||||
import { Apps, AppEvents } from '../../../apps/server'; |
||||
import { Messages as LegacyMessage } from '../../../models/server'; |
||||
|
||||
class LivechatClass { |
||||
logger: Logger; |
||||
|
||||
constructor() { |
||||
this.logger = new Logger('Livechat'); |
||||
} |
||||
|
||||
async closeRoom(params: CloseRoomParams): Promise<void> { |
||||
const { comment } = params; |
||||
let { room } = params; |
||||
|
||||
this.logger.debug(`Attempting to close room ${room._id}`); |
||||
if (!room || !isOmnichannelRoom(room) || !room.open) { |
||||
this.logger.debug(`Room ${room._id} is not open`); |
||||
return; |
||||
} |
||||
|
||||
const { updatedOptions: options } = await this.resolveChatTags(room, params.options); |
||||
this.logger.debug(`Resolved chat tags for room ${room._id}`); |
||||
|
||||
const now = new Date(); |
||||
const { _id: rid, servedBy, transcriptRequest } = room; |
||||
const serviceTimeDuration = servedBy && (now.getTime() - new Date(servedBy.ts).getTime()) / 1000; |
||||
|
||||
const closeData: IOmnichannelRoomClosingInfo = { |
||||
closedAt: now, |
||||
chatDuration: (now.getTime() - new Date(room.ts).getTime()) / 1000, |
||||
...(serviceTimeDuration && { serviceTimeDuration }), |
||||
...options, |
||||
}; |
||||
this.logger.debug(`Room ${room._id} was closed at ${closeData.closedAt} (duration ${closeData.chatDuration})`); |
||||
|
||||
const isRoomClosedByUserParams = (params: CloseRoomParams): params is CloseRoomParamsByUser => |
||||
(params as CloseRoomParamsByUser).user !== undefined; |
||||
const isRoomClosedByVisitorParams = (params: CloseRoomParams): params is CloseRoomParamsByVisitor => |
||||
(params as CloseRoomParamsByVisitor).visitor !== undefined; |
||||
|
||||
let chatCloser: any; |
||||
if (isRoomClosedByUserParams(params)) { |
||||
const { user } = params; |
||||
this.logger.debug(`Closing by user ${user._id}`); |
||||
closeData.closer = 'user'; |
||||
closeData.closedBy = { |
||||
_id: user._id, |
||||
username: user.username, |
||||
}; |
||||
chatCloser = user; |
||||
} else if (isRoomClosedByVisitorParams(params)) { |
||||
const { visitor } = params; |
||||
this.logger.debug(`Closing by visitor ${params.visitor._id}`); |
||||
closeData.closer = 'visitor'; |
||||
closeData.closedBy = { |
||||
_id: visitor._id, |
||||
username: visitor.username, |
||||
}; |
||||
chatCloser = visitor; |
||||
} else { |
||||
throw new Error('Error: Please provide details of the user or visitor who closed the room'); |
||||
} |
||||
|
||||
this.logger.debug(`Updating DB for room ${room._id} with close data`); |
||||
|
||||
await Promise.all([ |
||||
LivechatRooms.closeRoomById(rid, closeData), |
||||
LivechatInquiry.removeByRoomId(rid), |
||||
Subscriptions.removeByRoomId(rid), |
||||
]); |
||||
|
||||
this.logger.debug(`DB updated for room ${room._id}`); |
||||
|
||||
const message = { |
||||
t: 'livechat-close', |
||||
msg: comment, |
||||
groupable: false, |
||||
transcriptRequested: !!transcriptRequest, |
||||
}; |
||||
|
||||
// Retrieve the closed room
|
||||
room = (await LivechatRooms.findOneById(rid)) as IOmnichannelRoom; |
||||
|
||||
this.logger.debug(`Sending closing message to room ${room._id}`); |
||||
sendMessage(chatCloser, message, room); |
||||
|
||||
LegacyMessage.createCommandWithRoomIdAndUser('promptTranscript', rid, closeData.closedBy); |
||||
|
||||
this.logger.debug(`Running callbacks for room ${room._id}`); |
||||
|
||||
Meteor.defer(() => { |
||||
/** |
||||
* @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed |
||||
* in the next major version of the Apps-Engine |
||||
*/ |
||||
Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, room); |
||||
Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, room); |
||||
}); |
||||
callbacks.runAsync('livechat.closeRoom', { |
||||
room, |
||||
options, |
||||
}); |
||||
|
||||
this.logger.debug(`Room ${room._id} was closed`); |
||||
} |
||||
|
||||
private async resolveChatTags( |
||||
room: IOmnichannelRoom, |
||||
options: CloseRoomParams['options'] = {}, |
||||
): Promise<{ updatedOptions: CloseRoomParams['options'] }> { |
||||
this.logger.debug(`Resolving chat tags for room ${room._id}`); |
||||
|
||||
const concatUnique = (...arrays: (string[] | undefined)[]): string[] => [ |
||||
...new Set(([] as string[]).concat(...arrays.filter((a): a is string[] => !!a))), |
||||
]; |
||||
|
||||
const { departmentId, tags: optionsTags } = room; |
||||
const { clientAction, tags: oldRoomTags } = options; |
||||
const roomTags = concatUnique(oldRoomTags, optionsTags); |
||||
|
||||
if (!departmentId) { |
||||
return { |
||||
updatedOptions: { |
||||
...options, |
||||
...(roomTags.length && { tags: roomTags }), |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
const department = await LivechatDepartment.findOneById(departmentId); |
||||
if (!department) { |
||||
return { |
||||
updatedOptions: { |
||||
...options, |
||||
...(roomTags.length && { tags: roomTags }), |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
const { requestTagBeforeClosingChat, chatClosingTags } = department; |
||||
const extraRoomTags = concatUnique(roomTags, chatClosingTags); |
||||
|
||||
if (!requestTagBeforeClosingChat) { |
||||
return { |
||||
updatedOptions: { |
||||
...options, |
||||
...(extraRoomTags.length && { tags: extraRoomTags }), |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
const checkRoomTags = !clientAction || (roomTags && roomTags.length > 0); |
||||
const checkDepartmentTags = chatClosingTags && chatClosingTags.length > 0; |
||||
if (!checkRoomTags || !checkDepartmentTags) { |
||||
throw new Error('error-tags-must-be-assigned-before-closing-chat'); |
||||
} |
||||
|
||||
return { |
||||
updatedOptions: { |
||||
...options, |
||||
...(extraRoomTags.length && { tags: extraRoomTags }), |
||||
}, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
export const Livechat = new LivechatClass(); |
||||
@ -1,23 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import { LivechatVisitors } from '@rocket.chat/models'; |
||||
|
||||
import { settings } from '../../../settings/server'; |
||||
import { LivechatRooms } from '../../../models/server'; |
||||
import { Livechat } from '../lib/Livechat'; |
||||
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; |
||||
|
||||
Meteor.methods({ |
||||
async 'livechat:closeByVisitor'({ roomId, token }) { |
||||
methodDeprecationLogger.warn('livechat:closeByVisitor will be deprecated in future versions of Rocket.Chat'); |
||||
const visitor = await LivechatVisitors.getVisitorByToken(token); |
||||
|
||||
const language = (visitor && visitor.language) || settings.get('Language') || 'en'; |
||||
|
||||
return Livechat.closeRoom({ |
||||
visitor, |
||||
room: LivechatRooms.findOneOpenByRoomIdAndVisitorToken(roomId, token), |
||||
comment: TAPi18n.__('Closed_by_visitor', { lng: language }), |
||||
}); |
||||
}, |
||||
}); |
||||
@ -1,43 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { hasPermission } from '../../../authorization'; |
||||
import { Subscriptions, LivechatRooms } from '../../../models/server'; |
||||
import { Livechat } from '../lib/Livechat'; |
||||
|
||||
Meteor.methods({ |
||||
'livechat:closeRoom'(roomId, comment, options = {}) { |
||||
const userId = Meteor.userId(); |
||||
if (!userId || !hasPermission(userId, 'close-livechat-room')) { |
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', { |
||||
method: 'livechat:closeRoom', |
||||
}); |
||||
} |
||||
|
||||
const room = LivechatRooms.findOneById(roomId); |
||||
if (!room || room.t !== 'l') { |
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', { |
||||
method: 'livechat:closeRoom', |
||||
}); |
||||
} |
||||
|
||||
if (!room.open) { |
||||
throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:closeRoom' }); |
||||
} |
||||
|
||||
const user = Meteor.user(); |
||||
|
||||
const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { _id: 1 }); |
||||
if (!subscription && !hasPermission(userId, 'close-others-livechat-room')) { |
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', { |
||||
method: 'livechat:closeRoom', |
||||
}); |
||||
} |
||||
|
||||
return Livechat.closeRoom({ |
||||
user, |
||||
room: LivechatRooms.findOneById(roomId), |
||||
comment, |
||||
options, |
||||
}); |
||||
}, |
||||
}); |
||||
@ -0,0 +1,129 @@ |
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Users, LivechatRooms, Subscriptions as SubscriptionRaw } from '@rocket.chat/models'; |
||||
|
||||
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; |
||||
import { Livechat } from '../lib/LivechatTyped'; |
||||
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; |
||||
|
||||
type CloseRoomOptions = { |
||||
clientAction?: boolean; |
||||
tags?: string[]; |
||||
emailTranscript?: |
||||
| { |
||||
sendToVisitor: false; |
||||
} |
||||
| { |
||||
sendToVisitor: true; |
||||
requestData: Pick<NonNullable<IOmnichannelRoom['transcriptRequest']>, 'email' | 'subject'>; |
||||
}; |
||||
generateTranscriptPdf?: boolean; |
||||
}; |
||||
|
||||
type LivechatCloseRoomOptions = Omit<CloseRoomOptions, 'generateTranscriptPdf'> & { |
||||
emailTranscript?: |
||||
| { |
||||
sendToVisitor: false; |
||||
} |
||||
| { |
||||
sendToVisitor: true; |
||||
requestData: NonNullable<IOmnichannelRoom['transcriptRequest']>; |
||||
}; |
||||
pdfTranscript?: { |
||||
requestedBy: string; |
||||
}; |
||||
}; |
||||
|
||||
Meteor.methods({ |
||||
async 'livechat:closeRoom'(roomId: string, comment?: string, options?: CloseRoomOptions) { |
||||
methodDeprecationLogger.warn( |
||||
'livechat:closeRoom is deprecated and will be removed in next major version. Use /api/v1/livechat/room.closeByUser API instead.', |
||||
); |
||||
|
||||
const userId = Meteor.userId(); |
||||
if (!userId || !(await hasPermissionAsync(userId, 'close-livechat-room'))) { |
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', { |
||||
method: 'livechat:closeRoom', |
||||
}); |
||||
} |
||||
|
||||
const room = await LivechatRooms.findOneById(roomId); |
||||
if (!room || room.t !== 'l') { |
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', { |
||||
method: 'livechat:closeRoom', |
||||
}); |
||||
} |
||||
|
||||
if (!room.open) { |
||||
throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:closeRoom' }); |
||||
} |
||||
|
||||
const user = await Users.findOneById(userId); |
||||
if (!user) { |
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { |
||||
method: 'livechat:closeRoom', |
||||
}); |
||||
} |
||||
|
||||
const subscription = await SubscriptionRaw.findOneByRoomIdAndUserId(roomId, user._id, { |
||||
projection: { |
||||
_id: 1, |
||||
}, |
||||
}); |
||||
if (!subscription && !(await hasPermissionAsync(userId, 'close-others-livechat-room'))) { |
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', { |
||||
method: 'livechat:closeRoom', |
||||
}); |
||||
} |
||||
|
||||
await Livechat.closeRoom({ |
||||
user, |
||||
room, |
||||
comment, |
||||
options: resolveOptions(user, options), |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
const resolveOptions = ( |
||||
user: NonNullable<IOmnichannelRoom['transcriptRequest']>['requestedBy'], |
||||
options?: CloseRoomOptions, |
||||
): LivechatCloseRoomOptions | undefined => { |
||||
if (!options) { |
||||
return undefined; |
||||
} |
||||
|
||||
const resolvedOptions: LivechatCloseRoomOptions = { |
||||
clientAction: options.clientAction, |
||||
tags: options.tags, |
||||
}; |
||||
|
||||
if (options.generateTranscriptPdf) { |
||||
resolvedOptions.pdfTranscript = { |
||||
requestedBy: user._id, |
||||
}; |
||||
} |
||||
|
||||
if (!options?.emailTranscript) { |
||||
return resolvedOptions; |
||||
} |
||||
if (options?.emailTranscript.sendToVisitor === false) { |
||||
return { |
||||
...resolvedOptions, |
||||
emailTranscript: { |
||||
sendToVisitor: false, |
||||
}, |
||||
}; |
||||
} |
||||
return { |
||||
...resolvedOptions, |
||||
emailTranscript: { |
||||
sendToVisitor: true, |
||||
requestData: { |
||||
...options.emailTranscript.requestData, |
||||
requestedBy: user, |
||||
requestedAt: new Date(), |
||||
}, |
||||
}, |
||||
}; |
||||
}; |
||||
@ -1,32 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { check } from 'meteor/check'; |
||||
|
||||
import { hasPermission } from '../../../authorization'; |
||||
import { LivechatRooms } from '../../../models/server'; |
||||
|
||||
Meteor.methods({ |
||||
'livechat:discardTranscript'(rid) { |
||||
check(rid, String); |
||||
|
||||
if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'send-omnichannel-chat-transcript')) { |
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { |
||||
method: 'livechat:requestTranscript', |
||||
}); |
||||
} |
||||
|
||||
const room = LivechatRooms.findOneById(rid); |
||||
if (!room || !room.open) { |
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', { |
||||
method: 'livechat:requestTranscript', |
||||
}); |
||||
} |
||||
|
||||
if (!room.transcriptRequest) { |
||||
throw new Meteor.Error('error-transcript-not-requested', 'No transcript requested for this chat', { |
||||
method: 'livechat:requestTranscript', |
||||
}); |
||||
} |
||||
|
||||
return LivechatRooms.removeTranscriptRequestByRoomId(rid); |
||||
}, |
||||
}); |
||||
@ -0,0 +1,36 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { check } from 'meteor/check'; |
||||
import { LivechatRooms } from '@rocket.chat/models'; |
||||
|
||||
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; |
||||
|
||||
Meteor.methods({ |
||||
async 'livechat:discardTranscript'(rid: string) { |
||||
check(rid, String); |
||||
|
||||
const user = Meteor.userId(); |
||||
|
||||
if (!user || !(await hasPermissionAsync(user, 'send-omnichannel-chat-transcript'))) { |
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { |
||||
method: 'livechat:requestTranscript', |
||||
}); |
||||
} |
||||
|
||||
const room = await LivechatRooms.findOneById(rid); |
||||
if (!room || !room.open) { |
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', { |
||||
method: 'livechat:discardTranscript', |
||||
}); |
||||
} |
||||
|
||||
if (!room.transcriptRequest) { |
||||
throw new Meteor.Error('error-transcript-not-requested', 'No transcript requested for this chat', { |
||||
method: 'livechat:discardTranscript', |
||||
}); |
||||
} |
||||
|
||||
await LivechatRooms.unsetEmailTranscriptRequestedByRoomId(rid); |
||||
|
||||
return true; |
||||
}, |
||||
}); |
||||
@ -1,26 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { hasPermission } from '../../../authorization'; |
||||
import { LivechatRooms } from '../../../models/server'; |
||||
import { Livechat } from '../lib/Livechat'; |
||||
|
||||
Meteor.methods({ |
||||
'livechat:removeAllClosedRooms'(departmentIds) { |
||||
if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'remove-closed-livechat-rooms')) { |
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { |
||||
method: 'livechat:removeAllClosedRoom', |
||||
}); |
||||
} |
||||
|
||||
let count = 0; |
||||
// These are not debug logs since we want to know when the action is performed
|
||||
Livechat.logger.info(`User ${Meteor.userId()} is removing all closed rooms`); |
||||
LivechatRooms.findClosedRooms(departmentIds).forEach(({ _id }) => { |
||||
Livechat.removeRoom(_id); |
||||
count++; |
||||
}); |
||||
|
||||
Livechat.logger.info(`User ${Meteor.userId()} removed ${count} closed rooms`); |
||||
return count; |
||||
}, |
||||
}); |
||||
@ -0,0 +1,30 @@ |
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; |
||||
import { LivechatRooms } from '../../../models/server'; |
||||
import { Livechat } from '../lib/Livechat'; |
||||
|
||||
Meteor.methods({ |
||||
async 'livechat:removeAllClosedRooms'(departmentIds: string[]) { |
||||
const user = Meteor.userId(); |
||||
|
||||
if (!user || !(await hasPermissionAsync(user, 'remove-closed-livechat-rooms'))) { |
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { |
||||
method: 'livechat:removeAllClosedRoom', |
||||
}); |
||||
} |
||||
|
||||
// These are not debug logs since we want to know when the action is performed
|
||||
Livechat.logger.info(`User ${Meteor.userId()} is removing all closed rooms`); |
||||
|
||||
const promises: Promise<void>[] = []; |
||||
LivechatRooms.findClosedRooms(departmentIds).forEach(({ _id }: IOmnichannelRoom) => { |
||||
promises.push(Livechat.removeRoom(_id)); |
||||
}); |
||||
await Promise.all(promises); |
||||
|
||||
Livechat.logger.info(`User ${Meteor.userId()} removed ${promises.length} closed rooms`); |
||||
return promises.length; |
||||
}, |
||||
}); |
||||
@ -1,24 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { check } from 'meteor/check'; |
||||
|
||||
import { hasPermission } from '../../../authorization'; |
||||
import { Users } from '../../../models/server'; |
||||
import { Livechat } from '../lib/Livechat'; |
||||
|
||||
Meteor.methods({ |
||||
'livechat:requestTranscript'(rid, email, subject) { |
||||
check(rid, String); |
||||
check(email, String); |
||||
|
||||
if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'send-omnichannel-chat-transcript')) { |
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { |
||||
method: 'livechat:requestTranscript', |
||||
}); |
||||
} |
||||
|
||||
const user = Users.findOneById(Meteor.userId(), { |
||||
fields: { _id: 1, username: 1, name: 1, utcOffset: 1 }, |
||||
}); |
||||
return Livechat.requestTranscript({ rid, email, subject, user }); |
||||
}, |
||||
}); |
||||
@ -0,0 +1,29 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { check } from 'meteor/check'; |
||||
|
||||
import { Users } from '../../../models/server'; |
||||
import { Livechat } from '../lib/Livechat'; |
||||
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; |
||||
|
||||
Meteor.methods({ |
||||
async 'livechat:requestTranscript'(rid: string, email: string, subject: string) { |
||||
check(rid, String); |
||||
check(email, String); |
||||
|
||||
const userId = Meteor.userId(); |
||||
|
||||
if (!userId || !(await hasPermissionAsync(userId, 'send-omnichannel-chat-transcript'))) { |
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { |
||||
method: 'livechat:requestTranscript', |
||||
}); |
||||
} |
||||
|
||||
const user = Users.findOneById(userId, { |
||||
fields: { _id: 1, username: 1, name: 1, utcOffset: 1 }, |
||||
}); |
||||
|
||||
await Livechat.requestTranscript({ rid, email, subject, user }); |
||||
|
||||
return true; |
||||
}, |
||||
}); |
||||
@ -0,0 +1,68 @@ |
||||
import { ButtonGroup, Button, Box, Accordion } from '@rocket.chat/fuselage'; |
||||
import { useToastMessageDispatch, useTranslation, useEndpoint, useUserPreference } from '@rocket.chat/ui-contexts'; |
||||
import type { ReactElement } from 'react'; |
||||
import React from 'react'; |
||||
import type { UseFormRegister } from 'react-hook-form'; |
||||
import { useForm } from 'react-hook-form'; |
||||
|
||||
import Page from '../../../components/Page'; |
||||
import PreferencesConversationTranscript from './PreferencesConversationTranscript'; |
||||
|
||||
type CurrentData = { |
||||
omnichannelTranscriptPDF: boolean; |
||||
omnichannelTranscriptEmail: boolean; |
||||
}; |
||||
|
||||
export type FormSectionProps = { |
||||
register: UseFormRegister<CurrentData>; |
||||
}; |
||||
|
||||
const OmnichannelPreferencesPage = (): ReactElement => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const omnichannelTranscriptPDF = useUserPreference<boolean>('omnichannelTranscriptPDF') ?? false; |
||||
const omnichannelTranscriptEmail = useUserPreference<boolean>('omnichannelTranscriptEmail') ?? false; |
||||
|
||||
const { |
||||
handleSubmit, |
||||
register, |
||||
formState: { isDirty }, |
||||
reset, |
||||
} = useForm({ |
||||
defaultValues: { omnichannelTranscriptPDF, omnichannelTranscriptEmail }, |
||||
}); |
||||
|
||||
const saveFn = useEndpoint('POST', '/v1/users.setPreferences'); |
||||
|
||||
const handleSave = async (data: CurrentData) => { |
||||
try { |
||||
await saveFn({ data }); |
||||
reset(data); |
||||
dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}; |
||||
|
||||
return ( |
||||
<Page> |
||||
<Page.Header title={t('Omnichannel')}> |
||||
<ButtonGroup> |
||||
<Button primary disabled={!isDirty} onClick={handleSubmit(handleSave)}> |
||||
{t('Save_changes')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow is='form' onSubmit={handleSubmit(handleSave)}> |
||||
<Box maxWidth='x600' w='full' alignSelf='center'> |
||||
<Accordion> |
||||
<PreferencesConversationTranscript register={register} /> |
||||
</Accordion> |
||||
</Box> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page> |
||||
); |
||||
}; |
||||
|
||||
export default OmnichannelPreferencesPage; |
||||
@ -0,0 +1,64 @@ |
||||
import { Accordion, Box, Field, FieldGroup, Tag, ToggleSwitch } from '@rocket.chat/fuselage'; |
||||
import { useTranslation, usePermission } from '@rocket.chat/ui-contexts'; |
||||
import type { ReactElement } from 'react'; |
||||
import React from 'react'; |
||||
|
||||
import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; |
||||
import type { FormSectionProps } from './OmnichannelPreferencesPage'; |
||||
|
||||
const PreferencesConversationTranscript = ({ register }: FormSectionProps): ReactElement | null => { |
||||
const t = useTranslation(); |
||||
|
||||
const hasLicense = useHasLicenseModule('livechat-enterprise'); |
||||
const canSendTranscriptPDF = usePermission('request-pdf-transcript'); |
||||
const canSendTranscriptEmail = usePermission('send-omnichannel-chat-transcript'); |
||||
const cantSendTranscriptPDF = !canSendTranscriptPDF || !hasLicense; |
||||
|
||||
return ( |
||||
<Accordion.Item defaultExpanded title={t('Conversational_transcript')}> |
||||
<FieldGroup> |
||||
<Field> |
||||
<Box display='flex' alignItems='center' flexDirection='row' justifyContent='spaceBetween' flexGrow={1}> |
||||
<Field.Label color={cantSendTranscriptPDF ? 'disabled' : undefined}> |
||||
<Box display='flex' alignItems='center'> |
||||
{t('Omnichannel_transcript_pdf')} |
||||
<Box marginInline={4}> |
||||
{!hasLicense && <Tag variant='featured'>{t('Enterprise')}</Tag>} |
||||
{!canSendTranscriptPDF && hasLicense && <Tag>{t('No_permission')}</Tag>} |
||||
</Box> |
||||
</Box> |
||||
</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch disabled={cantSendTranscriptPDF} {...register('omnichannelTranscriptPDF')} /> |
||||
</Field.Row> |
||||
</Box> |
||||
<Field.Hint color={cantSendTranscriptPDF ? 'disabled' : undefined}> |
||||
{t('Accounts_Default_User_Preferences_omnichannelTranscriptPDF_Description')} |
||||
</Field.Hint> |
||||
</Field> |
||||
<Field> |
||||
<Box display='flex' alignItems='center' flexDirection='row' justifyContent='spaceBetween' flexGrow={1}> |
||||
<Field.Label color={!canSendTranscriptEmail ? 'disabled' : undefined}> |
||||
<Box display='flex' alignItems='center'> |
||||
{t('Omnichannel_transcript_email')} |
||||
{!canSendTranscriptEmail && ( |
||||
<Box marginInline={4}> |
||||
<Tag>{t('No_permission')}</Tag> |
||||
</Box> |
||||
)} |
||||
</Box> |
||||
</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch disabled={!canSendTranscriptEmail} {...register('omnichannelTranscriptEmail')} /> |
||||
</Field.Row> |
||||
</Box> |
||||
<Field.Hint color={!canSendTranscriptEmail ? 'disabled' : undefined}> |
||||
{t('Accounts_Default_User_Preferences_omnichannelTranscriptEmail_Description')} |
||||
</Field.Hint> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Accordion.Item> |
||||
); |
||||
}; |
||||
|
||||
export default PreferencesConversationTranscript; |
||||
@ -0,0 +1,49 @@ |
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import { Box, Dropdown, Option } from '@rocket.chat/fuselage'; |
||||
import { Header } from '@rocket.chat/ui-client'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import type { FC } from 'react'; |
||||
import React, { memo, useRef } from 'react'; |
||||
|
||||
import { useDropdownVisibility } from '../../../../../sidebar/header/hooks/useDropdownVisibility'; |
||||
import type { QuickActionsActionOptions } from '../../../lib/QuickActions'; |
||||
|
||||
type ToolBoxActionOptionsProps = { |
||||
options: QuickActionsActionOptions; |
||||
action: (id: string) => void; |
||||
room: IOmnichannelRoom; |
||||
}; |
||||
|
||||
const ToolBoxActionOptions: FC<ToolBoxActionOptionsProps> = ({ options, room, action, ...props }) => { |
||||
const t = useTranslation(); |
||||
const reference = useRef(null); |
||||
const target = useRef(null); |
||||
const { isVisible, toggle } = useDropdownVisibility({ reference, target }); |
||||
|
||||
const handleClick = (id: string) => (): void => { |
||||
toggle(); |
||||
action(id); |
||||
}; |
||||
|
||||
return ( |
||||
<> |
||||
<Header.ToolBox.Action ref={reference} onClick={(): void => toggle()} secondary={isVisible} {...props} /> |
||||
{isVisible && ( |
||||
<Dropdown reference={reference} ref={target}> |
||||
{options.map(({ id, label, validate }) => { |
||||
const { value: valid = true, tooltip } = validate?.(room) || {}; |
||||
return ( |
||||
<Option key={id} onClick={handleClick(id)} disabled={!valid} title={!valid && tooltip ? t(tooltip) : undefined}> |
||||
<Box fontScale='p2m' minWidth='180px'> |
||||
{t(label)} |
||||
</Box> |
||||
</Option> |
||||
); |
||||
})} |
||||
</Dropdown> |
||||
)} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default memo(ToolBoxActionOptions); |
||||
@ -0,0 +1,37 @@ |
||||
import { LivechatRooms } from '@rocket.chat/models'; |
||||
import { OmnichannelTranscript } from '@rocket.chat/core-services'; |
||||
|
||||
import { API } from '../../../../../app/api/server'; |
||||
import { canAccessRoomAsync } from '../../../../../app/authorization/server/functions/canAccessRoom'; |
||||
|
||||
API.v1.addRoute( |
||||
'omnichannel/:rid/request-transcript', |
||||
{ authRequired: true, permissionsRequired: ['request-pdf-transcript'] }, |
||||
{ |
||||
async post() { |
||||
const room = await LivechatRooms.findOneById(this.urlParams.rid); |
||||
if (!room) { |
||||
throw new Error('error-invalid-room'); |
||||
} |
||||
|
||||
if (!(await canAccessRoomAsync(room, { _id: this.userId }))) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
|
||||
// Flow is as follows:
|
||||
// 1. Call OmnichannelTranscript.requestTranscript()
|
||||
// 2. OmnichannelTranscript.requestTranscript() calls QueueWorker.queueWork()
|
||||
// 3. QueueWorker.queueWork() eventually calls OmnichannelTranscript.workOnPdf()
|
||||
// 4. OmnichannelTranscript.workOnPdf() calls OmnichannelTranscript.pdfComplete() when processing ends
|
||||
// 5. OmnichannelTranscript.pdfComplete() sends the messages to the user, and updates the room with the flags
|
||||
await OmnichannelTranscript.requestTranscript({ |
||||
details: { |
||||
userId: this.userId, |
||||
rid: this.urlParams.rid, |
||||
}, |
||||
}); |
||||
|
||||
return API.v1.success(); |
||||
}, |
||||
}, |
||||
); |
||||
@ -0,0 +1,42 @@ |
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import { isOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import { OmnichannelTranscript } from '@rocket.chat/core-services'; |
||||
|
||||
import { callbacks } from '../../../../../lib/callbacks'; |
||||
import type { CloseRoomParams } from '../../../../../app/livechat/server/lib/LivechatTyped.d'; |
||||
|
||||
type LivechatCloseCallbackParams = { |
||||
room: IOmnichannelRoom; |
||||
options: CloseRoomParams['options']; |
||||
}; |
||||
|
||||
const sendPdfTranscriptOnClose = async (params: LivechatCloseCallbackParams): Promise<LivechatCloseCallbackParams> => { |
||||
const { room, options } = params; |
||||
|
||||
if (!isOmnichannelRoom(room)) { |
||||
return params; |
||||
} |
||||
|
||||
const { pdfTranscript } = options || {}; |
||||
if (!pdfTranscript) { |
||||
return params; |
||||
} |
||||
|
||||
const { requestedBy } = pdfTranscript; |
||||
|
||||
await OmnichannelTranscript.requestTranscript({ |
||||
details: { |
||||
userId: requestedBy, |
||||
rid: room._id, |
||||
}, |
||||
}); |
||||
|
||||
return params; |
||||
}; |
||||
|
||||
callbacks.add( |
||||
'livechat.closeRoom', |
||||
(params: LivechatCloseCallbackParams) => Promise.await(sendPdfTranscriptOnClose(params)), |
||||
callbacks.priority.HIGH, |
||||
'livechat-send-pdf-transcript-on-close-room', |
||||
); |
||||
@ -0,0 +1,13 @@ |
||||
import type { IMessage } from '@rocket.chat/core-typings'; |
||||
import type { IMessageService } from '@rocket.chat/core-services'; |
||||
import { ServiceClassInternal } from '@rocket.chat/core-services'; |
||||
|
||||
import { executeSendMessage } from '../../../app/lib/server/methods/sendMessage'; |
||||
|
||||
export class MessageService extends ServiceClassInternal implements IMessageService { |
||||
protected name = 'message'; |
||||
|
||||
async sendMessage({ fromId, rid, msg }: { fromId: string; rid: string; msg: string }): Promise<IMessage> { |
||||
return executeSendMessage(fromId, { rid, msg }); |
||||
} |
||||
} |
||||
@ -0,0 +1,12 @@ |
||||
import type { ISettingsService } from '@rocket.chat/core-services'; |
||||
import { ServiceClassInternal } from '@rocket.chat/core-services'; |
||||
|
||||
import { settings } from '../../../app/settings/server'; |
||||
|
||||
export class SettingsService extends ServiceClassInternal implements ISettingsService { |
||||
protected name = 'settings'; |
||||
|
||||
async get<T>(settingId: string): Promise<T> { |
||||
return settings.get<T>(settingId); |
||||
} |
||||
} |
||||
@ -0,0 +1,35 @@ |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import { Settings } from '@rocket.chat/models'; |
||||
import type { IUser } from '@rocket.chat/core-typings'; |
||||
import mem from 'mem'; |
||||
import { ServiceClassInternal } from '@rocket.chat/core-services'; |
||||
import type { ITranslationService } from '@rocket.chat/core-services'; |
||||
|
||||
export class TranslationService extends ServiceClassInternal implements ITranslationService { |
||||
protected name = 'translation'; |
||||
|
||||
// Cache the server language for 1 hour
|
||||
private getServerLanguageCached = mem(this.getServerLanguage.bind(this), { maxAge: 1000 * 60 * 60 }); |
||||
|
||||
private async getServerLanguage(): Promise<string> { |
||||
return ((await Settings.findOneById('Language'))?.value as string) || 'en'; |
||||
} |
||||
|
||||
// Use translateText when you already know the language, or want to translate to a predefined language
|
||||
translateText(text: string, targetLanguage: string): Promise<string> { |
||||
return Promise.resolve(TAPi18n.__(text, { lng: targetLanguage })); |
||||
} |
||||
|
||||
// Use translate when you want to translate to the user's language, or server's as a fallback
|
||||
async translate(text: string, user: IUser): Promise<string> { |
||||
const language = user.language || (await this.getServerLanguageCached()); |
||||
|
||||
return this.translateText(text, language); |
||||
} |
||||
|
||||
async translateToServerLanguage(text: string): Promise<string> { |
||||
const language = await this.getServerLanguageCached(); |
||||
|
||||
return this.translateText(text, language); |
||||
} |
||||
} |
||||
@ -0,0 +1,58 @@ |
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import React from 'react'; |
||||
import { render, fireEvent } from '@testing-library/react'; |
||||
import { expect, spy } from 'chai'; |
||||
|
||||
import TranscriptModal from '../../../../../../client/components/Omnichannel/modals/TranscriptModal'; |
||||
|
||||
const room = { |
||||
open: true, |
||||
v: { token: '1234567890' }, |
||||
transcriptRequest: { |
||||
email: 'example@example.com', |
||||
subject: 'Transcript of livechat conversation', |
||||
}, |
||||
} as IOmnichannelRoom; |
||||
|
||||
const defaultProps = { |
||||
room, |
||||
email: 'test@example.com', |
||||
onRequest: () => null, |
||||
onSend: () => null, |
||||
onCancel: () => null, |
||||
onDiscard: () => null, |
||||
}; |
||||
|
||||
describe('components/Omnichannel/TranscriptModal', () => { |
||||
it('should show Undo request button when roomOpen is true and transcriptRequest exist', () => { |
||||
const onDiscardMock = spy(); |
||||
const { getByText } = render(<TranscriptModal {...defaultProps} onDiscard={onDiscardMock} />); |
||||
const undoRequestButton = getByText('Undo_request'); |
||||
|
||||
fireEvent.click(undoRequestButton); |
||||
|
||||
expect(onDiscardMock).to.have.been.called(); |
||||
}); |
||||
|
||||
it('should show Request button when roomOpen is true and transcriptRequest not exist', () => { |
||||
const onRequestMock = spy(); |
||||
const { getByText } = render( |
||||
<TranscriptModal {...{ ...defaultProps, room: { ...room, transcriptRequest: undefined } }} onRequest={onRequestMock} />, |
||||
); |
||||
const requestButton = getByText('Request'); |
||||
|
||||
fireEvent.click(requestButton); |
||||
|
||||
expect(onRequestMock).to.have.been.called(); |
||||
}); |
||||
|
||||
it('should show Send button when roomOpen is false', () => { |
||||
const onSendMock = spy(); |
||||
const { getByText } = render(<TranscriptModal {...{ ...defaultProps, room: { ...room, open: false } }} onSend={onSendMock} />); |
||||
const requestButton = getByText('Send'); |
||||
|
||||
fireEvent.click(requestButton); |
||||
|
||||
expect(onSendMock).to.have.been.called(); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,171 @@ |
||||
version: '3.8' |
||||
|
||||
services: |
||||
rocketchat: |
||||
platform: linux/amd64 |
||||
build: |
||||
dockerfile: ${RC_DOCKERFILE} |
||||
context: /tmp/build |
||||
image: ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${RC_DOCKER_TAG} |
||||
environment: |
||||
- TEST_MODE=true |
||||
- EXIT_UNHANDLEDPROMISEREJECTION=true |
||||
- 'MONGO_URL=${MONGO_URL}' |
||||
- 'MONGO_OPLOG_URL=${MONGO_OPLOG_URL}' |
||||
- 'TRANSPORTER=${TRANSPORTER}' |
||||
- MOLECULER_LOG_LEVEL=info |
||||
- 'ROCKETCHAT_LICENSE=${ENTERPRISE_LICENSE}' |
||||
- OVERWRITE_SETTING_Log_Level=2 |
||||
extra_hosts: |
||||
- 'host.docker.internal:host-gateway' |
||||
depends_on: |
||||
- traefik |
||||
- mongo |
||||
labels: |
||||
traefik.enable: true |
||||
traefik.http.services.rocketchat.loadbalancer.server.port: 3000 |
||||
traefik.http.routers.rocketchat.service: rocketchat |
||||
traefik.http.routers.rocketchat.rule: PathPrefix(`/`) |
||||
|
||||
authorization-service: |
||||
platform: linux/amd64 |
||||
build: |
||||
dockerfile: ee/apps/authorization-service/Dockerfile |
||||
args: |
||||
SERVICE: authorization-service |
||||
image: ghcr.io/${LOWERCASE_REPOSITORY}/authorization-service:${DOCKER_TAG} |
||||
environment: |
||||
- 'MONGO_URL=${MONGO_URL}' |
||||
- 'TRANSPORTER=${TRANSPORTER}' |
||||
- MOLECULER_LOG_LEVEL=info |
||||
extra_hosts: |
||||
- 'host.docker.internal:host-gateway' |
||||
depends_on: |
||||
- nats |
||||
|
||||
account-service: |
||||
platform: linux/amd64 |
||||
build: |
||||
dockerfile: ee/apps/account-service/Dockerfile |
||||
args: |
||||
SERVICE: account-service |
||||
image: ghcr.io/${LOWERCASE_REPOSITORY}/account-service:${DOCKER_TAG} |
||||
environment: |
||||
- MONGO_URL=${MONGO_URL} |
||||
- 'TRANSPORTER=${TRANSPORTER}' |
||||
- MOLECULER_LOG_LEVEL=info |
||||
extra_hosts: |
||||
- 'host.docker.internal:host-gateway' |
||||
depends_on: |
||||
- nats |
||||
|
||||
presence-service: |
||||
platform: linux/amd64 |
||||
build: |
||||
dockerfile: ee/apps/presence-service/Dockerfile |
||||
args: |
||||
SERVICE: presence-service |
||||
image: ghcr.io/${LOWERCASE_REPOSITORY}/presence-service:${DOCKER_TAG} |
||||
environment: |
||||
- MONGO_URL=${MONGO_URL} |
||||
- 'TRANSPORTER=${TRANSPORTER}' |
||||
- MOLECULER_LOG_LEVEL=info |
||||
extra_hosts: |
||||
- 'host.docker.internal:host-gateway' |
||||
depends_on: |
||||
- nats |
||||
|
||||
ddp-streamer-service: |
||||
platform: linux/amd64 |
||||
build: |
||||
dockerfile: ee/apps/ddp-streamer/Dockerfile |
||||
args: |
||||
SERVICE: ddp-streamer |
||||
image: ghcr.io/${LOWERCASE_REPOSITORY}/ddp-streamer-service:${DOCKER_TAG} |
||||
environment: |
||||
- MONGO_URL=${MONGO_URL} |
||||
- 'TRANSPORTER=${TRANSPORTER}' |
||||
- MOLECULER_LOG_LEVEL=info |
||||
extra_hosts: |
||||
- 'host.docker.internal:host-gateway' |
||||
depends_on: |
||||
- nats |
||||
- traefik |
||||
labels: |
||||
traefik.enable: true |
||||
traefik.http.services.ddp-streamer-service.loadbalancer.server.port: 3000 |
||||
traefik.http.routers.ddp-streamer-service.service: ddp-streamer-service |
||||
traefik.http.routers.ddp-streamer-service.rule: PathPrefix(`/websocket`) || PathPrefix(`/sockjs`) |
||||
|
||||
stream-hub-service: |
||||
platform: linux/amd64 |
||||
build: |
||||
dockerfile: ee/apps/stream-hub-service/Dockerfile |
||||
args: |
||||
SERVICE: stream-hub-service |
||||
image: ghcr.io/${LOWERCASE_REPOSITORY}/stream-hub-service:${DOCKER_TAG} |
||||
environment: |
||||
- MONGO_URL=${MONGO_URL} |
||||
- 'TRANSPORTER=${TRANSPORTER}' |
||||
- MOLECULER_LOG_LEVEL=info |
||||
extra_hosts: |
||||
- 'host.docker.internal:host-gateway' |
||||
depends_on: |
||||
- nats |
||||
|
||||
queue-worker-service: |
||||
platform: linux/amd64 |
||||
build: |
||||
dockerfile: ee/apps/queue-worker/Dockerfile |
||||
args: |
||||
SERVICE: queue-worker |
||||
image: ghcr.io/${LOWERCASE_REPOSITORY}/queue-worker-service:${DOCKER_TAG} |
||||
environment: |
||||
- MONGO_URL=${MONGO_URL} |
||||
- 'TRANSPORTER=${TRANSPORTER}' |
||||
- MOLECULER_LOG_LEVEL=info |
||||
extra_hosts: |
||||
- 'host.docker.internal:host-gateway' |
||||
depends_on: |
||||
- nats |
||||
|
||||
omnichannel-transcript-service: |
||||
platform: linux/amd64 |
||||
build: |
||||
dockerfile: ee/apps/omnichannel-transcript/Dockerfile |
||||
args: |
||||
SERVICE: omnichannel-transcript |
||||
image: ghcr.io/${LOWERCASE_REPOSITORY}/omnichannel-transcript-service:${DOCKER_TAG} |
||||
environment: |
||||
- MONGO_URL=${MONGO_URL} |
||||
- 'TRANSPORTER=${TRANSPORTER}' |
||||
- MOLECULER_LOG_LEVEL=info |
||||
extra_hosts: |
||||
- 'host.docker.internal:host-gateway' |
||||
depends_on: |
||||
- nats |
||||
|
||||
mongo: |
||||
image: docker.io/bitnami/mongodb:4.4 |
||||
restart: on-failure |
||||
environment: |
||||
MONGODB_REPLICA_SET_MODE: primary |
||||
MONGODB_REPLICA_SET_NAME: ${MONGODB_REPLICA_SET_NAME:-rs0} |
||||
MONGODB_PORT_NUMBER: ${MONGODB_PORT_NUMBER:-27017} |
||||
MONGODB_INITIAL_PRIMARY_HOST: ${MONGODB_INITIAL_PRIMARY_HOST:-mongo} |
||||
MONGODB_INITIAL_PRIMARY_PORT_NUMBER: ${MONGODB_INITIAL_PRIMARY_PORT_NUMBER:-27017} |
||||
MONGODB_ADVERTISED_HOSTNAME: ${MONGODB_ADVERTISED_HOSTNAME:-mongo} |
||||
MONGODB_ENABLE_JOURNAL: ${MONGODB_ENABLE_JOURNAL:-true} |
||||
ALLOW_EMPTY_PASSWORD: ${ALLOW_EMPTY_PASSWORD:-yes} |
||||
|
||||
nats: |
||||
image: nats:2.6-alpine |
||||
|
||||
traefik: |
||||
image: traefik:v2.8 |
||||
command: |
||||
- --providers.docker=true |
||||
ports: |
||||
- 3000:80 |
||||
volumes: |
||||
- /var/run/docker.sock:/var/run/docker.sock |
||||
@ -0,0 +1,4 @@ |
||||
{ |
||||
"extends": ["@rocket.chat/eslint-config"], |
||||
"ignorePatterns": ["**/dist"] |
||||
} |
||||
@ -0,0 +1,49 @@ |
||||
FROM node:14.21.2-alpine |
||||
|
||||
ARG SERVICE |
||||
|
||||
WORKDIR /app |
||||
|
||||
COPY ./packages/core-services/package.json packages/core-services/package.json |
||||
COPY ./packages/core-services/dist packages/core-services/dist |
||||
|
||||
COPY ./packages/core-typings/package.json packages/core-typings/package.json |
||||
COPY ./packages/core-typings/dist packages/core-typings/dist |
||||
|
||||
COPY ./packages/rest-typings/package.json packages/rest-typings/package.json |
||||
COPY ./packages/rest-typings/dist packages/rest-typings/dist |
||||
|
||||
COPY ./packages/model-typings/package.json packages/model-typings/package.json |
||||
COPY ./packages/model-typings/dist packages/model-typings/dist |
||||
|
||||
COPY ./packages/models/package.json packages/models/package.json |
||||
COPY ./packages/models/dist packages/models/dist |
||||
|
||||
COPY ./ee/packages/omnichannel-services/package.json ee/packages/omnichannel-services/package.json |
||||
COPY ./ee/packages/omnichannel-services/dist ee/packages/omnichannel-services/dist |
||||
|
||||
COPY ./ee/packages/pdf-worker/package.json ee/packages/pdf-worker/package.json |
||||
COPY ./ee/packages/pdf-worker/dist ee/packages/pdf-worker/dist |
||||
|
||||
COPY ./packages/tools/package.json packages/tools/package.json |
||||
COPY ./packages/tools/dist packages/tools/dist |
||||
|
||||
COPY ./ee/apps/${SERVICE}/dist . |
||||
|
||||
COPY ./package.json . |
||||
COPY ./yarn.lock . |
||||
COPY ./.yarnrc.yml . |
||||
COPY ./.yarn/plugins .yarn/plugins |
||||
COPY ./.yarn/releases .yarn/releases |
||||
COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json |
||||
|
||||
ENV NODE_ENV=production \ |
||||
PORT=3000 |
||||
|
||||
WORKDIR /app/ee/apps/${SERVICE} |
||||
|
||||
RUN yarn workspaces focus --production |
||||
|
||||
EXPOSE 3000 9458 |
||||
|
||||
CMD ["node", "src/service.js"] |
||||
@ -0,0 +1,57 @@ |
||||
{ |
||||
"name": "@rocket.chat/omnichannel-transcript", |
||||
"private": true, |
||||
"version": "0.1.0", |
||||
"description": "Rocket.Chat service", |
||||
"scripts": { |
||||
"build": "tsc -p tsconfig.json", |
||||
"ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", |
||||
"test": "echo \"Error: no test specified\" && exit 1", |
||||
"lint": "eslint src", |
||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" |
||||
}, |
||||
"keywords": [ |
||||
"rocketchat" |
||||
], |
||||
"author": "Rocket.Chat", |
||||
"dependencies": { |
||||
"@react-pdf/renderer": "^3.1.3", |
||||
"@rocket.chat/core-services": "workspace:^", |
||||
"@rocket.chat/core-typings": "workspace:^", |
||||
"@rocket.chat/emitter": "0.31.22", |
||||
"@rocket.chat/model-typings": "workspace:^", |
||||
"@rocket.chat/models": "workspace:^", |
||||
"@rocket.chat/omnichannel-services": "workspace:^", |
||||
"@rocket.chat/pdf-worker": "workspace:^", |
||||
"@rocket.chat/tools": "workspace:^", |
||||
"@types/node": "^14.18.21", |
||||
"ejson": "^2.2.2", |
||||
"emoji-toolkit": "^7.0.0", |
||||
"eventemitter3": "^4.0.7", |
||||
"fibers": "^5.0.3", |
||||
"mem": "^8.1.1", |
||||
"moleculer": "^0.14.21", |
||||
"moment-timezone": "^0.5.34", |
||||
"mongo-message-queue": "^1.0.0", |
||||
"mongodb": "^4.12.1", |
||||
"nats": "^2.4.0", |
||||
"pino": "^8.4.2", |
||||
"polka": "^0.5.2" |
||||
}, |
||||
"devDependencies": { |
||||
"@rocket.chat/eslint-config": "workspace:^", |
||||
"@rocket.chat/ui-contexts": "workspace:^", |
||||
"@types/eslint": "^8.4.10", |
||||
"@types/polka": "^0.5.4", |
||||
"eslint": "^8.29.0", |
||||
"ts-node": "^10.9.1", |
||||
"typescript": "~4.5.5" |
||||
}, |
||||
"main": "./dist/ee/apps/omnichannel-transcript/src/service.js", |
||||
"files": [ |
||||
"/dist" |
||||
], |
||||
"volta": { |
||||
"extends": "../../../package.json" |
||||
} |
||||
} |
||||
@ -0,0 +1,41 @@ |
||||
import type { Document } from 'mongodb'; |
||||
import polka from 'polka'; |
||||
import { api } from '@rocket.chat/core-services'; |
||||
|
||||
import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; |
||||
import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; |
||||
import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; |
||||
import { Logger } from '../../../../apps/meteor/server/lib/logger/Logger'; |
||||
|
||||
const PORT = process.env.PORT || 3036; |
||||
|
||||
(async () => { |
||||
const db = await getConnection(); |
||||
|
||||
const trash = await getCollection<Document>(Collections.Trash); |
||||
|
||||
registerServiceModels(db, trash); |
||||
|
||||
api.setBroker(broker); |
||||
|
||||
// need to import service after models are registered
|
||||
const { OmnichannelTranscript } = await import('@rocket.chat/omnichannel-services'); |
||||
|
||||
api.registerService(new OmnichannelTranscript(Logger), ['queue-worker']); |
||||
|
||||
await api.start(); |
||||
|
||||
polka() |
||||
.get('/health', async function (_req, res) { |
||||
try { |
||||
await api.nodeList(); |
||||
res.end('ok'); |
||||
} catch (err) { |
||||
console.error('Service not healthy', err); |
||||
|
||||
res.writeHead(500); |
||||
res.end('not healthy'); |
||||
} |
||||
}) |
||||
.listen(PORT); |
||||
})(); |
||||
@ -0,0 +1,11 @@ |
||||
{ |
||||
"extends": "../../../tsconfig.base.server.json", |
||||
"compilerOptions": { |
||||
"allowJs": true, |
||||
"strictPropertyInitialization": false, |
||||
"outDir": "./dist", |
||||
}, |
||||
"files": ["./src/service.ts"], |
||||
"include": ["../../../apps/meteor/definition/externals/meteor"], |
||||
"exclude": ["./dist"] |
||||
} |
||||
@ -0,0 +1,4 @@ |
||||
{ |
||||
"extends": ["@rocket.chat/eslint-config"], |
||||
"ignorePatterns": ["**/dist"] |
||||
} |
||||
@ -0,0 +1,49 @@ |
||||
FROM node:14.21.2-alpine |
||||
|
||||
ARG SERVICE |
||||
|
||||
WORKDIR /app |
||||
|
||||
COPY ./packages/core-services/package.json packages/core-services/package.json |
||||
COPY ./packages/core-services/dist packages/core-services/dist |
||||
|
||||
COPY ./packages/core-typings/package.json packages/core-typings/package.json |
||||
COPY ./packages/core-typings/dist packages/core-typings/dist |
||||
|
||||
COPY ./packages/rest-typings/package.json packages/rest-typings/package.json |
||||
COPY ./packages/rest-typings/dist packages/rest-typings/dist |
||||
|
||||
COPY ./packages/model-typings/package.json packages/model-typings/package.json |
||||
COPY ./packages/model-typings/dist packages/model-typings/dist |
||||
|
||||
COPY ./packages/models/package.json packages/models/package.json |
||||
COPY ./packages/models/dist packages/models/dist |
||||
|
||||
COPY ./ee/packages/omnichannel-services/package.json ee/packages/omnichannel-services/package.json |
||||
COPY ./ee/packages/omnichannel-services/dist ee/packages/omnichannel-services/dist |
||||
|
||||
COPY ./ee/packages/pdf-worker/package.json ee/packages/pdf-worker/package.json |
||||
COPY ./ee/packages/pdf-worker/dist ee/packages/pdf-worker/dist |
||||
|
||||
COPY ./packages/tools/package.json packages/tools/package.json |
||||
COPY ./packages/tools/dist packages/tools/dist |
||||
|
||||
COPY ./ee/apps/${SERVICE}/dist . |
||||
|
||||
COPY ./package.json . |
||||
COPY ./yarn.lock . |
||||
COPY ./.yarnrc.yml . |
||||
COPY ./.yarn/plugins .yarn/plugins |
||||
COPY ./.yarn/releases .yarn/releases |
||||
COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json |
||||
|
||||
ENV NODE_ENV=production \ |
||||
PORT=3000 |
||||
|
||||
WORKDIR /app/ee/apps/${SERVICE} |
||||
|
||||
RUN yarn workspaces focus --production |
||||
|
||||
EXPOSE 3000 9458 |
||||
|
||||
CMD ["node", "src/service.js"] |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue