Skip to content
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
7 changes: 4 additions & 3 deletions apps/meteor/app/livechat/server/sendMessageBySMS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -115,6 +102,69 @@ export class Twilio implements ISMSProvider {
return returnData;
}

private async getClient(rid?: string, userId?: string) {
const sid = settings.get<string>('SMS_Twilio_Account_SID');
const token = settings.get<string>('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<string> {
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,
Expand All @@ -126,37 +176,28 @@ export class Twilio implements ISMSProvider {
userId?: string;
},
): Promise<SMSProviderResult> {
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<string>('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];
}

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}

Expand Down Expand Up @@ -151,10 +151,6 @@ export class Voxtelesys implements ISMSProvider {
}
}

fileUploadMediaTypeWhiteList(): any {
throw new Error('Method not implemented.');
}

response(): SMSProviderResponse {
return {
headers: {
Expand Down