О проекте
Внутренняя платформа для общения с клиентами. Трафик поступал сразу из нескольких источников: Авито, Telegram, WhatsApp, мессенджер Max. До мессенджера сотрудники работали в каждом приложении отдельно - легко было потерять сообщение или вовремя не ответить. Задача была объединить всё в одно окно браузера.
Готовые CRM-решения вроде Битрикса или amoCRM использовались, но подписка была слишком дорогой. И компания решила строить своё решение.
Архитектура
Каждый источник сообщений — отдельный микросервис. Telegram и WhatsApp требуют по одному активному приложению на аккаунт, поэтому под каждый аккаунт поднимается отдельный инстанс сервиса. Авито и Max в этом плане свободнее — там ограничений меньше.
Все сервисы общаются через RabbitMQ: входящее сообщение из любого канала попадает в очередь, основной бэкенд забирает его и доставляет в браузер сотрудников через WebSocket. Медиафайлы вынесены в MinIO — S3-совместимое объектное хранилище.
Фишка проекта
Новый источник сообщений можно подключить через стандартизированный HTTP-контракт — не нужно трогать основную кодовую базу. Любой адаптер — хоть Авито, хоть новая платформа — просто приводит входящее сообщение к единому формату и вызывает одну функцию:
def save_incoming_message(
payload: dict,
platform: str,
user_url: str = None,
ad_text: str = None,
ad_price: str = None,
ad_url: str = None,
) -> None:
payload — единый формат для всех платформ:
payload = {
"message_id": str,
"from_me": bool,
"created_at": datetime,
"message_type": str,
"text": str,
"replied_message_id": str | None,
"media": {
"file_name": str,
"file_bucket": str,
} | None,
"chat_avatar": {
"media_key": str,
"bucket_name": str,
} | None,
}
Сложности
Самым нетривиальным оказался RabbitMQ — не сам факт использования, а понимание его модели. Queue и exchange это не одно и то же, routing key работает не так как ожидаешь, а backpressure и ack нужно думать заранее, иначе сообщения теряются или дублируются. Это тот случай, когда документацию мало прочитать — нужно несколько раз ошибиться, чтобы понять как оно работает на самом деле.
Вторая сложность — нестабильность инстансов. Любой из инстансов мог отвалиться в любой момент: сессия протухла, сеть упала, платформа обновила API. Особенно страдал WhatsApp — там использовалась библиотека whatsapp-web.js, которая работает через headless-браузер и была очень нестабильна.
