snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
286
apps/meteor/server/api/v1/omnichannel/sms.ts
Normal file
286
apps/meteor/server/api/v1/omnichannel/sms.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { OmnichannelIntegration } from '@rocket.chat/core-services';
|
||||
import type {
|
||||
ILivechatVisitor,
|
||||
IUpload,
|
||||
MessageAttachment,
|
||||
ServiceData,
|
||||
FileAttachmentProps,
|
||||
IOmnichannelRoomInfo,
|
||||
} 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';
|
||||
import { registerGuest } from '@rocket.chat/omni-core';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { API } from '../..';
|
||||
import { setCustomField } from './lib/customFields';
|
||||
import { getFileExtension } from '../../../../lib/utils/getFileExtension';
|
||||
import { FileUpload } from '../../../lib/media/file-upload';
|
||||
import type { ILivechatMessage } from '../../../lib/omnichannel/localTypes';
|
||||
import { sendMessage } from '../../../lib/omnichannel/messages';
|
||||
import { createRoom } from '../../../lib/omnichannel/rooms';
|
||||
import { settings } from '../../../settings';
|
||||
|
||||
const logger = new Logger('SMS');
|
||||
|
||||
const getUploadFile = async (details: Omit<IUpload, '_id' | '_updatedAt'>, fileUrl: string) => {
|
||||
const response = await fetch(fileUrl, {
|
||||
ignoreSsrfValidation: false,
|
||||
allowList: settings.get<string>('SSRF_Allowlist'),
|
||||
});
|
||||
|
||||
const content = Buffer.from(await response.arrayBuffer());
|
||||
|
||||
const contentSize = content.length;
|
||||
|
||||
if (response.status !== 200 || contentSize === 0) {
|
||||
throw new Meteor.Error('error-invalid-file-uploaded', 'Invalid file uploaded');
|
||||
}
|
||||
|
||||
const fileStore = FileUpload.getStore('Uploads');
|
||||
|
||||
return fileStore.insert({ ...details, size: contentSize }, content);
|
||||
};
|
||||
|
||||
const defineDepartment = async (idOrName?: string) => {
|
||||
if (!idOrName || idOrName === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const department = await LivechatDepartment.findOneByIdOrName(idOrName, { projection: { _id: 1 } });
|
||||
return department?._id;
|
||||
};
|
||||
|
||||
const defineVisitor = async (smsNumber: string, targetDepartment?: string) => {
|
||||
const visitor = await LivechatVisitors.findOneVisitorByPhone(smsNumber);
|
||||
let data: { token: string; department?: string } = {
|
||||
token: visitor?.token || Random.id(),
|
||||
};
|
||||
|
||||
if (!visitor) {
|
||||
data = Object.assign(data, {
|
||||
username: smsNumber.replace(/[^0-9]/g, ''),
|
||||
phone: {
|
||||
number: smsNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (targetDepartment) {
|
||||
data.department = targetDepartment;
|
||||
}
|
||||
|
||||
const livechatVisitor = await registerGuest(data, {
|
||||
shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle'),
|
||||
shouldConsiderOfflineAgent: settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
|
||||
});
|
||||
|
||||
if (!livechatVisitor) {
|
||||
throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor');
|
||||
}
|
||||
|
||||
return livechatVisitor;
|
||||
};
|
||||
|
||||
const normalizeLocationSharing = (payload: ServiceData) => {
|
||||
const { extra: { fromLatitude: latitude, fromLongitude: longitude } = {} } = payload;
|
||||
if (!latitude || !longitude) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Point',
|
||||
coordinates: [parseFloat(longitude), parseFloat(latitude)],
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-expect-error - this is an special endpoint that requires the return to not be wrapped as regular returns
|
||||
API.v1.addRoute('livechat/sms-incoming/:service', {
|
||||
async post() {
|
||||
const { service } = this.urlParams;
|
||||
if (!(await OmnichannelIntegration.isConfiguredSmsService(service))) {
|
||||
return API.v1.failure('Invalid service');
|
||||
}
|
||||
|
||||
const smsDepartment = settings.get<string>('SMS_Default_Omnichannel_Department');
|
||||
const SMSService = await OmnichannelIntegration.getSmsService(service);
|
||||
|
||||
if (!(await SMSService.validateRequest(this.request.clone(), this.bodyParams))) {
|
||||
return API.v1.failure('Invalid request');
|
||||
}
|
||||
|
||||
const sms = SMSService.parse(this.bodyParams);
|
||||
const { department } = this.queryParams;
|
||||
let targetDepartment = await defineDepartment(department || smsDepartment);
|
||||
if (!targetDepartment) {
|
||||
targetDepartment = await defineDepartment(smsDepartment);
|
||||
}
|
||||
|
||||
const visitor = await defineVisitor(sms.from, targetDepartment);
|
||||
if (!visitor) {
|
||||
return API.v1.success(SMSService.error(new Error('Invalid visitor')));
|
||||
}
|
||||
|
||||
const roomInfo: IOmnichannelRoomInfo = {
|
||||
sms: {
|
||||
from: sms.to,
|
||||
},
|
||||
source: {
|
||||
type: OmnichannelSourceType.SMS,
|
||||
alias: service,
|
||||
destination: sms.to,
|
||||
},
|
||||
};
|
||||
|
||||
const { token } = visitor;
|
||||
const room =
|
||||
(await LivechatRooms.findOneOpenByVisitorTokenAndDepartmentIdAndSource(token, targetDepartment, OmnichannelSourceType.SMS)) ??
|
||||
(await createRoom({
|
||||
visitor,
|
||||
roomInfo,
|
||||
}));
|
||||
const location = normalizeLocationSharing(sms);
|
||||
const rid = room?._id;
|
||||
|
||||
let file: ILivechatMessage['file'];
|
||||
const attachments: (MessageAttachment | undefined)[] = [];
|
||||
|
||||
const [media] = sms?.media || [];
|
||||
if (media) {
|
||||
const { url: smsUrl, contentType } = media;
|
||||
const details = {
|
||||
name: 'Upload File',
|
||||
type: contentType,
|
||||
rid,
|
||||
visitorToken: token,
|
||||
};
|
||||
|
||||
try {
|
||||
const uploadedFile = await getUploadFile(details, smsUrl);
|
||||
file = { _id: uploadedFile._id, name: uploadedFile.name || 'file', type: uploadedFile.type };
|
||||
const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || 'file')}`);
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
attachments.push(attachment);
|
||||
} else {
|
||||
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 });
|
||||
const attachment = {
|
||||
title: 'Attachment upload failed',
|
||||
type: 'file',
|
||||
description: 'An attachment was received, but upload to server failed',
|
||||
fields: [
|
||||
{
|
||||
title: 'User upload failed',
|
||||
value: 'An attachment was received, but upload to server failed',
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
color: 'yellow',
|
||||
};
|
||||
|
||||
attachments.push(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
const messageToSend: {
|
||||
guest: ILivechatVisitor;
|
||||
message: ILivechatMessage;
|
||||
roomInfo: IOmnichannelRoomInfo;
|
||||
} = {
|
||||
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 {
|
||||
await sendMessage(messageToSend);
|
||||
const msg = SMSService.response();
|
||||
setImmediate(async () => {
|
||||
if (sms.extra) {
|
||||
if (sms.extra.fromCountry) {
|
||||
await setCustomField(messageToSend.message.token, 'country', sms.extra.fromCountry);
|
||||
}
|
||||
if (sms.extra.fromState) {
|
||||
await setCustomField(messageToSend.message.token, 'state', sms.extra.fromState);
|
||||
}
|
||||
if (sms.extra.fromCity) {
|
||||
await setCustomField(messageToSend.message.token, 'city', sms.extra.fromCity);
|
||||
}
|
||||
if (sms.extra.fromZip) {
|
||||
await setCustomField(messageToSend.message.token, 'zip', sms.extra.fromZip);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return msg;
|
||||
} catch (e: any) {
|
||||
return SMSService.error(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user