diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.js b/apps/meteor/app/livechat/imports/server/rest/sms.ts similarity index 52% rename from apps/meteor/app/livechat/imports/server/rest/sms.js rename to apps/meteor/app/livechat/imports/server/rest/sms.ts index 117407807dad..51902bb413e4 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.js +++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts @@ -1,4 +1,12 @@ import { OmnichannelIntegration } from '@rocket.chat/core-services'; +import type { + ILivechatVisitor, + IOmnichannelRoom, + IUpload, + MessageAttachment, + ServiceData, + FileAttachmentProps, +} from '@rocket.chat/core-typings'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { LivechatVisitors, LivechatRooms, LivechatDepartment } from '@rocket.chat/models'; @@ -6,14 +14,26 @@ import { Random } from '@rocket.chat/random'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Meteor } from 'meteor/meteor'; +import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; import { API } from '../../../../api/server'; import { FileUpload } from '../../../../file-upload/server'; import { settings } from '../../../../settings/server'; +import type { ILivechatMessage } from '../../../server/lib/LivechatTyped'; import { Livechat as LivechatTyped } from '../../../server/lib/LivechatTyped'; +type UploadedFile = { + _id: string; + name?: string; + type?: string; + size?: number; + description?: string; + identify?: { size: { width: number; height: number } }; + format?: string; +}; + const logger = new Logger('SMS'); -const getUploadFile = async (details, fileUrl) => { +const getUploadFile = async (details: Omit, fileUrl: string) => { const response = await fetch(fileUrl); const content = Buffer.from(await response.arrayBuffer()); @@ -29,19 +49,19 @@ const getUploadFile = async (details, fileUrl) => { return fileStore.insert({ ...details, size: contentSize }, content); }; -const defineDepartment = async (idOrName) => { +const defineDepartment = async (idOrName?: string) => { if (!idOrName || idOrName === '') { return; } - const department = await LivechatDepartment.findOneByIdOrName(idOrName); - return department && department._id; + const department = await LivechatDepartment.findOneByIdOrName(idOrName, { projection: { _id: 1 } }); + return department?._id; }; -const defineVisitor = async (smsNumber, targetDepartment) => { +const defineVisitor = async (smsNumber: string, targetDepartment?: string) => { const visitor = await LivechatVisitors.findOneVisitorByPhone(smsNumber); - let data = { - token: (visitor && visitor.token) || Random.id(), + let data: { token: string; department?: string } = { + token: visitor?.token || Random.id(), }; if (!visitor) { @@ -61,7 +81,7 @@ const defineVisitor = async (smsNumber, targetDepartment) => { return LivechatVisitors.findOneEnabledById(id); }; -const normalizeLocationSharing = (payload) => { +const normalizeLocationSharing = (payload: ServiceData) => { const { extra: { fromLatitude: latitude, fromLongitude: longitude } = {} } = payload; if (!latitude || !longitude) { return; @@ -79,7 +99,7 @@ API.v1.addRoute('livechat/sms-incoming/:service', { return API.v1.failure('Invalid service'); } - const smsDepartment = settings.get('SMS_Default_Omnichannel_Department'); + const smsDepartment = settings.get('SMS_Default_Omnichannel_Department'); const SMSService = await OmnichannelIntegration.getSmsService(this.urlParams.service); const sms = SMSService.parse(this.bodyParams); const { department } = this.queryParams; @@ -89,34 +109,35 @@ API.v1.addRoute('livechat/sms-incoming/:service', { } const visitor = await defineVisitor(sms.from, targetDepartment); + if (!visitor) { + return API.v1.success(SMSService.error(new Error('Invalid visitor'))); + } + const { token } = visitor; const room = await LivechatRooms.findOneOpenByVisitorTokenAndDepartmentIdAndSource(token, targetDepartment, OmnichannelSourceType.SMS); const roomExists = !!room; const location = normalizeLocationSharing(sms); - const rid = (room && room._id) || Random.id(); + const rid = room?._id || Random.id(); - const sendMessage = { - guest: visitor, - roomInfo: { - sms: { - from: sms.to, - }, - source: { - type: OmnichannelSourceType.SMS, - alias: this.urlParams.service, - }, + const roomInfo = { + sms: { + from: sms.to, + }, + source: { + type: OmnichannelSourceType.SMS, + alias: this.urlParams.service, }, }; // create an empty room first place, so attachments have a place to live if (!roomExists) { - await LivechatTyped.getRoom(visitor, { rid, token, msg: '' }, sendMessage.roomInfo, undefined); + await LivechatTyped.getRoom(visitor, { rid, token, msg: '' }, roomInfo, undefined); } - let file; - let attachments; + let file: UploadedFile | undefined; + const attachments: (MessageAttachment | undefined)[] = []; - const [media] = sms.media; + const [media] = sms?.media || []; if (media) { const { url: smsUrl, contentType } = media; const details = { @@ -126,40 +147,74 @@ API.v1.addRoute('livechat/sms-incoming/:service', { visitorToken: token, }; - let attachment; try { const uploadedFile = await getUploadFile(details, smsUrl); - file = { _id: uploadedFile._id, name: uploadedFile.name, type: uploadedFile.type }; - const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`); + file = { _id: uploadedFile._id, name: uploadedFile.name || 'file', type: uploadedFile.type }; + const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || 'file')}`); - attachment = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - }; + const fileType = file.type as string; + + if (/^image\/.+/.test(fileType)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + image_url: fileUrl, + image_type: fileType, + image_size: file.size, + }; + + if (file.identify?.size) { + attachment.image_dimensions = file?.identify.size; + } + + attachments.push(attachment); + } else if (/^audio\/.+/.test(fileType)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + audio_url: fileUrl, + audio_type: fileType, + audio_size: file.size, + title_link_download: true, + }; + + attachments.push(attachment); + } else if (/^video\/.+/.test(fileType)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + video_url: fileUrl, + video_type: fileType, + video_size: file.size as number, + title_link_download: true, + }; - if (/^image\/.+/.test(file.type)) { - attachment.image_url = fileUrl; - attachment.image_type = file.type; - attachment.image_size = file.size; - attachment.image_dimensions = file.identify != null ? file.identify.size : undefined; - } else if (/^audio\/.+/.test(file.type)) { - attachment.audio_url = fileUrl; - attachment.audio_type = file.type; - attachment.audio_size = file.size; - attachment.title_link_download = true; - } else if (/^video\/.+/.test(file.type)) { - attachment.video_url = fileUrl; - attachment.video_type = file.type; - attachment.video_size = file.size; - attachment.title_link_download = true; + attachments.push(attachment); } else { - attachment.title_link_download = true; + const attachment = { + title: file.name, + type: 'file', + description: file.description, + format: getFileExtension(file.name), + title_link: fileUrl, + title_link_download: true, + size: file.size as number, + }; + + attachments.push(attachment); } } catch (err) { logger.error({ msg: 'Attachment upload failed', err }); - attachment = { + const attachment = { + title: 'Attachment upload failed', + type: 'file', + description: 'An attachment was received, but upload to server failed', fields: [ { title: 'User upload failed', @@ -169,22 +224,35 @@ API.v1.addRoute('livechat/sms-incoming/:service', { ], color: 'yellow', }; + + attachments.push(attachment); } - attachments = [attachment]; } - sendMessage.message = { - _id: Random.id(), - rid, - token, - msg: sms.body, - ...(location && { location }), - ...(attachments && { attachments }), - ...(file && { file }), + const sendMessage: { + guest: ILivechatVisitor; + message: ILivechatMessage; + roomInfo: { + source?: IOmnichannelRoom['source']; + [key: string]: unknown; + }; + } = { + guest: visitor, + roomInfo, + message: { + _id: Random.id(), + rid, + token, + msg: sms.body, + ...(location && { location }), + ...(attachments && { attachments: attachments.filter((a: any): a is MessageAttachment => !!a) }), + ...(file && { file }), + }, }; try { - const msg = SMSService.response.call(this, await LivechatTyped.sendMessage(sendMessage)); + await LivechatTyped.sendMessage(sendMessage); + const msg = SMSService.response(); setImmediate(async () => { if (sms.extra) { if (sms.extra.fromCountry) { @@ -202,9 +270,9 @@ API.v1.addRoute('livechat/sms-incoming/:service', { } }); - return msg; - } catch (e) { - return SMSService.error.call(this, e); + return API.v1.success(msg); + } catch (e: any) { + return API.v1.success(SMSService.error(e)); } }, }); diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index d49bad222e3c..604f02df078d 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -368,7 +368,7 @@ class LivechatClass { async getRoom( guest: ILivechatVisitor, - message: Pick, + message: Pick, roomInfo: { source?: IOmnichannelRoom['source']; [key: string]: unknown; diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index 94adf40569fd..1423476a708b 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -1891,7 +1891,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive findOneOpenByVisitorTokenAndDepartmentIdAndSource( visitorToken: string, - departmentId: string, + departmentId?: string, source?: string, options: FindOptions = {}, ) { diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 2b380505c38a..a228a4fea864 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -176,7 +176,7 @@ export interface ILivechatRoomsModel extends IBaseModel { findOneOpenByVisitorToken(visitorToken: string, options?: FindOptions): Promise; findOneOpenByVisitorTokenAndDepartmentIdAndSource( visitorToken: string, - departmentId: string, + departmentId?: string, source?: string, options?: FindOptions, ): Promise; diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 22fc7e837f7e..d0338e3ea986 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -25,6 +25,7 @@ import type { ILivechatTriggerAction, ReportResult, ReportWithUnmatchingElements, + SMSProviderResponse, } from '@rocket.chat/core-typings'; import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; @@ -3717,6 +3718,9 @@ export type OmnichannelEndpoints = { value: string | number; }[]; }; + '/v1/livechat/sms-incoming/:service': { + POST: (params: unknown) => SMSProviderResponse; + }; } & { // EE '/v1/livechat/analytics/agents/average-service-time': {