diff --git a/apps/meteor/app/livechat/server/sendMessageBySMS.ts b/apps/meteor/app/livechat/server/sendMessageBySMS.ts index 54ac69befae26..ea220b24d1491 100644 --- a/apps/meteor/app/livechat/server/sendMessageBySMS.ts +++ b/apps/meteor/app/livechat/server/sendMessageBySMS.ts @@ -40,11 +40,12 @@ callbacks.add( return message; } - let extraData = {}; + const { rid, u: { _id: userId } = {} } = message; + let extraData = { rid, userId }; if (message.file) { message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } }; - const { fileUpload, rid, u: { _id: userId } = {} } = message; - extraData = Object.assign({}, { rid, userId, fileUpload }); + const { fileUpload } = message; + extraData = Object.assign({}, extraData, { fileUpload }); } if (message.location) { diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 3d25ebdc6f1af..44604b3785351 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2267,6 +2267,7 @@ "FileUpload_MediaTypeWhiteListDescription": "Comma-separated list of media types. Leave it blank for accepting all media types.", "FileUpload_ProtectFiles": "Protect Uploaded Files", "FileUpload_ProtectFilesDescription": "Only authenticated users will have access", + "FileUpload_ProtectFilesEnabled_JWTNotSet": "Uploaded files are protected, but JWT access is not setup, this is required for Twilio to send media messages. Setup in Settings -> FileUpload", "FileUpload_RotateImages": "Rotate images on upload", "FileUpload_RotateImages_Description": "Enabling this setting may cause image quality loss", "FileUpload_S3_Acl": "Acl", @@ -4658,6 +4659,8 @@ "SMS_Default_Omnichannel_Department": "Omnichannel Department (Default)", "SMS_Default_Omnichannel_Department_Description": "If set, all new incoming chats initiated by this integration will be routed to this department. \nThis setting can be overwritten by passing department query param in the request. \ne.g. `https://{{SERVER_URL}}/api/v1/livechat/sms-incoming/twilio?department={{Department Id or Name}}`. \nNote: if you're using Department Name, then it should be URL safe.", "SMS_Enabled": "SMS Enabled", + "SMS_Twilio_NotConfigured": "Twilio SMS is not configured yet. Go to Settings -> SMS to configure it", + "SMS_Twilio_InvalidCredentials": "Twilio SMS credentials are invalid, cannot send messages", "SMTP": "SMTP", "SMTP_Host": "SMTP Host", "SMTP_Password": "SMTP Password", diff --git a/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts b/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts index 0dc627c982c3d..3aff869a95a0c 100644 --- a/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts +++ b/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts @@ -39,27 +39,14 @@ const isTwilioData = (data: unknown): data is TwilioData => { const MAX_FILE_SIZE = 5242880; -const notifyAgent = (userId: string, rid: string, msg: string) => +const notifyAgent = (userId: string | undefined, rid: string | undefined, msg: string) => + userId && + rid && void api.broadcast('notify.ephemeralMessage', userId, rid, { msg, }); export class Twilio implements ISMSProvider { - accountSid: string; - - authToken: string; - - fileUploadEnabled: string; - - mediaTypeWhiteList: string; - - constructor() { - this.accountSid = settings.get('SMS_Twilio_Account_SID'); - this.authToken = settings.get('SMS_Twilio_authToken'); - this.fileUploadEnabled = settings.get('SMS_Twilio_FileUpload_Enabled'); - this.mediaTypeWhiteList = settings.get('SMS_Twilio_FileUpload_MediaTypeWhiteList'); - } - parse(data: unknown): ServiceData { let numMedia = 0; @@ -115,6 +102,69 @@ export class Twilio implements ISMSProvider { return returnData; } + private async getClient(rid?: string, userId?: string) { + const sid = settings.get('SMS_Twilio_Account_SID'); + const token = settings.get('SMS_Twilio_authToken'); + if (!sid || !token) { + await notifyAgent(userId, rid, i18n.t('SMS_Twilio_NotConfigured')); + return; + } + + try { + return twilio(sid, token); + } catch (error) { + await notifyAgent(userId, rid, i18n.t('SMS_Twilio_InvalidCredentials')); + SystemLogger.error(`(Twilio) -> ${error}`); + } + } + + private async validateFileUpload( + extraData: { + fileUpload?: { size: number; type: string; publicFilePath: string }; + location?: { coordinates: [number, number] }; + rid?: string; + userId?: string; + }, + lang: string, + ): Promise { + const { rid, userId, fileUpload: { size, type, publicFilePath } = { size: 0, type: 'invalid' } } = extraData; + const user = userId ? await Users.findOne({ _id: userId }, { projection: { language: 1 } }) : null; + const lng = user?.language || lang; + + let reason; + if (!settings.get('SMS_Twilio_FileUpload_Enabled')) { + reason = i18n.t('FileUpload_Disabled', { lng }); + } else if (size > MAX_FILE_SIZE) { + reason = i18n.t('File_exceeds_allowed_size_of_bytes', { + size: filesize(MAX_FILE_SIZE), + lng, + }); + } else if (!fileUploadIsValidContentType(type, settings.get('SMS_Twilio_FileUpload_MediaTypeWhiteList'))) { + reason = i18n.t('File_type_is_not_accepted', { lng }); + } else if (!publicFilePath) { + reason = i18n.t('FileUpload_NotAllowed', { lng }); + } + + // Check if JWT is set for public file uploads when protect_files is on + // If it's not, notify user upload won't go to twilio + const protectFileUploads = settings.get('FileUpload_ProtectFiles'); + const jwtEnabled = settings.get('FileUpload_Enable_json_web_token_for_files'); + const isJWTKeySet = jwtEnabled && !!settings.get('FileUpload_json_web_token_secret_for_files'); + + if (protectFileUploads && (!jwtEnabled || !isJWTKeySet)) { + reason = i18n.t('FileUpload_ProtectFilesEnabled_JWTNotSet', { lng }); + } + + if (reason) { + await notifyAgent(userId, rid, reason); + SystemLogger.error(`(Twilio) -> ${reason}`); + return ''; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return publicFilePath!; + } + async send( fromNumber: string, toNumber: string, @@ -126,37 +176,28 @@ export class Twilio implements ISMSProvider { userId?: string; }, ): Promise { - const client = twilio(this.accountSid, this.authToken); + const { rid, userId } = extraData || {}; + + const client = await this.getClient(rid, userId); + if (!client) { + return { + isSuccess: false, + resultMsg: 'Twilio not configured', + }; + } + let body = message; let mediaUrl; const defaultLanguage = settings.get('Language') || 'en'; if (extraData?.fileUpload) { - const { - rid, - userId, - fileUpload: { size, type, publicFilePath }, - } = extraData; - const user = userId ? await Users.findOne({ _id: userId }, { projection: { language: 1 } }) : null; - const lng = user?.language || defaultLanguage; - - let reason; - if (!this.fileUploadEnabled) { - reason = i18n.t('FileUpload_Disabled', { lng }); - } else if (size > MAX_FILE_SIZE) { - reason = i18n.t('File_exceeds_allowed_size_of_bytes', { - size: filesize(MAX_FILE_SIZE), - lng, - }); - } else if (!fileUploadIsValidContentType(type, this.fileUploadMediaTypeWhiteList())) { - reason = i18n.t('File_type_is_not_accepted', { lng }); - } - - if (reason) { - rid && userId && (await notifyAgent(userId, rid, reason)); - SystemLogger.error(`(Twilio) -> ${reason}`); + const publicFilePath = await this.validateFileUpload(extraData, defaultLanguage); + if (!publicFilePath) { + return { + isSuccess: false, + resultMsg: 'File upload not allowed', + }; } - mediaUrl = [publicFilePath]; } @@ -167,22 +208,31 @@ export class Twilio implements ISMSProvider { body = i18n.t('Location', { lng: defaultLanguage }); } - const result = await client.messages.create({ - to: toNumber, - from: fromNumber, - body, - ...(mediaUrl && { mediaUrl }), - ...(persistentAction && { persistentAction }), - }); - - return { - isSuccess: result.status !== 'failed', - resultMsg: result.status, - }; - } + try { + const result = await client.messages.create({ + to: toNumber, + from: fromNumber, + body, + ...(mediaUrl && { mediaUrl }), + ...(persistentAction && { persistentAction }), + }); + + if (result.errorCode) { + await notifyAgent(userId, rid, result.errorMessage); + SystemLogger.error(`(Twilio) -> ${result.errorCode}`); + } - fileUploadMediaTypeWhiteList(): any { - throw new Error('Method not implemented.'); + return { + isSuccess: result.status !== 'failed', + resultMsg: result.status, + }; + } catch (e: any) { + await notifyAgent(userId, rid, e.message); + return { + isSuccess: false, + resultMsg: e.message, + }; + } } response(): SMSProviderResponse { diff --git a/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts b/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts index 8c91d7fc2846f..3e78907bbf754 100644 --- a/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts +++ b/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts @@ -119,7 +119,7 @@ export class Voxtelesys implements ISMSProvider { size: filesize(MAX_FILE_SIZE), lng, }); - } else if (!fileUploadIsValidContentType(type, this.fileUploadMediaTypeWhiteList())) { + } else if (!fileUploadIsValidContentType(type, this.mediaTypeWhiteList)) { reason = i18n.t('File_type_is_not_accepted', { lng }); } @@ -151,10 +151,6 @@ export class Voxtelesys implements ISMSProvider { } } - fileUploadMediaTypeWhiteList(): any { - throw new Error('Method not implemented.'); - } - response(): SMSProviderResponse { return { headers: {