Мне нужна была CRM. Bitrix24 — от 2000₽/мес. AmoCRM — от 1500₽/мес. Для 5–10 лидов в месяц — абсурд.
Потом я посмотрел на свой Telegram. Групповые чаты с топиками — это по сути тикет-система: один топик = один лид, вся переписка в одном месте, поиск, уведомления на телефон. Не хватало только входной точки — бота, который квалифицирует лид и создаёт топик автоматически.
Я написал такого бота на Python, задеплоил на Yandex Cloud Functions — и получил мини-CRM за 0 ₽/мес.
Важный дисклеймер: это не замена Bitrix. Нет воронок, нет дашбордов, нет автоматических follow-up. Это для соло-основателей и маленьких команд, которые обрабатывают до 20 лидов в месяц и не хотят платить за CRM, которую используют на 10%.
Что получится
Схема работы:
Пользователь на сайте ↓ t.me/your_bot?start=website ↓ Бот: 2 вопроса (inline-кнопки) ↓ Yandex Cloud Function ↓ Telegram-группа с топиками ├── "Иван — E-commerce — AI-агент" ├── "Мария — Услуги — Консультация" └── ...
Пользователь нажимает CTA на сайте → попадает в бот → отвечает на два вопроса кнопками → бот создаёт топик в админской группе с карточкой лида → дальше вы общаетесь через бота, а вся переписка лежит в топике.
Стоимость:
Ресурс | Стоимость |
|---|---|
Cloud Function | 0 ₽ (1 млн вызовов/мес бесплатно) |
Object Storage | 0 ₽ (1 ГБ бесплатно) |
DNS + SSL-сертификат | 0 ₽ |
Итого | 0 ₽/мес |
API Gateway не нужен — allow-unauthenticated-invoke делает функцию доступной по HTTPS напрямую, этого достаточно для Telegram webhook.
Как написать бот для приема заявок в Telegram на Python
Почему без фреймворков
aiogram, python-telegram-bot — отличные библиотеки для сложных ботов. Но для Cloud Functions они избыточны:
Cold start. Тяжёлые фреймворки добавляют 1–3 секунды к первому вызову.
httpxимпортируется мгновенно.Polling не нужен. Cloud Functions работают по webhook — фреймворк с polling просто не запустится.
Роутинг не нужен. У нас одна точка входа, два типа апдейтов.
if/elifхватает.
Весь API-слой — 57 строк кода. Вот обёртка над Telegram Bot API:
"""telegram_api.py — тонкая обёртка над Bot API.""" import os import httpx BOT_TOKEN = os.environ.get("BOT_TOKEN", "") BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}" def _call(method: str, **kwargs) -> dict: resp = httpx.post(f"{BASE_URL}/{method}", json=kwargs, timeout=10) return resp.json() def send_message(chat_id, text, reply_markup=None, message_thread_id=None): params = {"chat_id": chat_id, "text": text, "parse_mode": "HTML"} if reply_markup: params["reply_markup"] = reply_markup if message_thread_id: params["message_thread_id"] = message_thread_id return _call("sendMessage", **params) def create_forum_topic(chat_id, name): return _call("createForumTopic", chat_id=chat_id, name=name) def copy_message(chat_id, from_chat_id, message_id, message_thread_id=None): params = {"chat_id": chat_id, "from_chat_id": from_chat_id, "message_id": message_id} if message_thread_id: params["message_thread_id"] = message_thread_id return _call("copyMessage", **params)
Почему copy_message, а не forward_message? При forward Telegram добавляет заголовок “Переслано от…” — выглядит неаккуратно. copy_message отправляет чистое сообщение без маркера пересылки.
Точка входа: webhook-хендлер
Cloud Function получает HTTP-запрос от Telegram. Первое, что делаем — валидируем секретный токен:
def handler(event, context): """Yandex Cloud Function entry point.""" headers = event.get("headers", {}) secret = headers.get("X-Telegram-Bot-Api-Secret-Token") or \ headers.get("x-telegram-bot-api-secret-token", "") if secret != WEBHOOK_SECRET: return {"statusCode": 403, "body": "Forbidden"} body = event.get("body", "{}") update = json.loads(body) if isinstance(body, str) else body if "callback_query" in update: _handle_callback(update["callback_query"]) elif "message" in update: _handle_message(update["message"]) return {"statusCode": 200, "body": "ok"}
secret_token задаётся при установке webhook — Telegram присылает его в каждом запросе. Без этой проверки кто угодно может слать фейковые апдейты на URL вашей функции.
Квалификация лида: inline-кнопки
По команде /start бот задаёт два вопроса. Первый — про бизнес:
def _handle_start(chat_id, from_user): # Защита от повторных заявок if storage.get_topic_id_by_user(chat_id): tg.send_message(chat_id, "Вы уже оставили заявку. Мы свяжемся с вами в ближайшее время.\n\n" "Если хотите что-то добавить — просто напишите сообщение здесь.") return keyboard = {"inline_keyboard": [ [{"text": "E-commerce / Маркетплейсы", "callback_data": "q1:ecommerce"}], [{"text": "Услуги / Сервис", "callback_data": "q1:services"}], [{"text": "Производство", "callback_data": "q1:production"}], [{"text": "IT / Стартап", "callback_data": "q1:it"}], [{"text": "Другое", "callback_data": "q1:other"}], ]} tg.send_message(chat_id, f"Привет, {from_user.get('first_name', '')}! Я бот VexAI.\n\n" "Ответьте на 2 коротких вопроса, и мы свяжемся с вами.") tg.send_message(chat_id, "<b>Чем занимается ваш бизнес?</b>", reply_markup=keyboard)
Обратите внимание на защиту от дублей: если пользователь уже оставлял заявку, бот не создаёт второй топик, а предлагает написать в существующий тред. Один топик = один клиент.
Трюк с callback data: stateless-квалификация
Cloud Functions — stateless. Каждый вызов — чистый лист. Чтобы не лезть в хранилище на каждый клик, я кодирую ответ Q1 прямо в callback data Q2:
def _send_q2(chat_id, q1_key): keyboard = {"inline_keyboard": [ [{"text": "Автоматизация продаж", "callback_data": f"q2:sales:{q1_key}"}], [{"text": "Обработка данных", "callback_data": f"q2:data:{q1_key}"}], [{"text": "AI-агент / чат-бот", "callback_data": f"q2:agent:{q1_key}"}], [{"text": "Консультация", "callback_data": f"q2:consult:{q1_key}"}], [{"text": "Другое", "callback_data": f"q2:other:{q1_key}"}], ]} tg.send_message(chat_id, "<b>Какую задачу хотите решить с помощью AI?</b>", reply_markup=keyboard)
Когда пользователь нажимает кнопку Q2, callback приходит в виде q2:sales:ecommerce — оба ответа в одной строке. Ни одного обращения к хранилищу на happy path.
Ограничение: callback data — максимум 64 байта. Для предустановленных вариантов хватает. А для “Другое” (свободный текст) приходится использовать Object Storage — записываем временный state файл state/{chat_id}.json, который удаляется после завершения flow.
Финализация: создание топика
После двух ответов бот создаёт топик в админской группе:
def _finalize(chat_id, from_user, q1_answer, q2_answer): tg.send_message(chat_id, "Спасибо! Свяжемся с вами в ближайшее время.") full_name = f"{from_user.get('first_name', '')} {from_user.get('last_name', '')}".strip() topic_title = f"{full_name} — {q1_answer} — {q2_answer}" if len(topic_title) > 128: # лимит Telegram topic_title = topic_title[:125] + "..." result = tg.create_forum_topic(ADMIN_GROUP_ID, topic_title) topic_id = result["result"]["message_thread_id"] summary = (f"<b>Новый лид</b>\n\n" f"<b>Имя:</b> {full_name}\n" f"<b>Бизнес:</b> {q1_answer}\n" f"<b>Задача:</b> {q2_answer}\n" f"<b>Источник:</b> website") tg.send_message(ADMIN_GROUP_ID, summary, message_thread_id=topic_id) # Сохраняем маппинг для relay storage.save_topic_mapping(topic_id, chat_id) storage.save_user_mapping(chat_id, topic_id)
Двусторонний relay
Самая CRM-подобная фича. Вы отвечаете в топике — бот пересылает сообщение клиенту. Клиент отвечает боту — бот пересылает в топик.
def _handle_message(msg): chat = msg.get("chat", {}) chat_id = chat.get("id") chat_type = chat.get("type", "") # Админ отвечает в топике группы → пересылаем клиенту if chat_type in ("supergroup", "group") and chat_id == ADMIN_GROUP_ID: topic_id = msg.get("message_thread_id") if not topic_id: return if msg.get("from", {}).get("id") != ADMIN_USER_ID: return user_chat_id = storage.get_chat_id_by_topic(topic_id) if user_chat_id: tg.copy_message(chat_id=user_chat_id, from_chat_id=chat_id, message_id=msg["message_id"]) return # Клиент пишет боту → пересылаем в его топик if chat_type == "private": topic_id = storage.get_topic_id_by_user(chat_id) if topic_id: tg.copy_message(chat_id=ADMIN_GROUP_ID, from_chat_id=chat_id, message_id=msg["message_id"], message_thread_id=topic_id)
Клиент видит обычный чат с ботом. Вы видите организованный список лидов в Telegram-группе. Каждый диалог — в своём топике, с историей и поиском.
Бонус: трекинг источника через /start
Параметр /start поддерживает deep link: t.me/your_bot?start=website, ?start=habr, ?start=instagram. Бот получает это в тексте сообщения как /start website. Сейчас у меня источник захардкожен как “website”, но распарсить параметр и включить в карточку лида — 3 строчки кода.
Мини-CRM на базе Telegram: что умеет и чего не умеет
Настройка группы
Создайте supergroup в Telegram
Включите Топики (режим форума) в настройках группы
Добавьте бота как админа с правами: Управление темами + Отправка сообщений
Что вы получаете
Один топик = один лид — полная история переписки в одном месте
Поиск по всем лидам — Telegram ищет по тексту внутри всех топиков
Закреплённые сообщения — закрепите карточку лида в начале топика
Уведомления на телефон — никакого отдельного приложения
Двусторонний relay — отвечаете в топике, клиент получает в боте. И наоборот.
Чего нет
Функция | Мини-CRM | Bitrix/AmoCRM |
|---|---|---|
Приём заявок | ✓ | ✓ |
Переписка с клиентом | ✓ | ✓ |
Воронка продаж | ✗ | ✓ |
Автоматические follow-up | ✗ | ✓ |
Аналитика | ✗ | ✓ |
Командная работа | ✗ | ✓ |
Стоимость | 0 ₽ | от 1500 ₽/мес |
Для соло-основателей с 0–20 лидами в месяц — хватает. Когда нужно трекать стадии воронки или подключать команду — переезжайте на нормальную CRM.
Claude Code: от идеи до работающего бота за одну сессию
Весь этот бот — от первой строчки до работающего webhook — был написан и задеплоен в одной сессии с Claude Code. Я описал задачу: “Нужен Telegram-бот для квалификации лидов, деплой на Yandex Cloud Functions.” Дальше Claude Code:
Написал код бота (handler, API-обёртку, storage)
Собрал zip-пакет с зависимостями
Задеплоил через
ycCLI на Cloud FunctionsНастроил webhook с secret token
Claude Code использует кастомный навык (skill) для Yandex Cloud — набор команд yc CLI для работы с Object Storage, Cloud Functions, DNS и сертификатами. Навык open source — можно подключить к своему Claude Code и деплоить на Yandex Cloud голосом.
Как задеплоить Telegram-бот на Yandex Cloud Functions бесплатно
Что нужно заранее
Аккаунт Yandex Cloud (free tier)
Сервис-аккаунт с ролью
storage.editorСтатические ключи доступа:
yc iam access-key create --service-account-name <sa>Приватный бакет Object Storage для состояния бота
Токен бота от @BotFather
Chat ID группы и ваш Telegram user ID
Пошагово
1. Собрать пакет:
cd bot pip3 install -t package httpx boto3 cp *.py package/ cd package && zip -qr ../bot-function.zip . && cd .. rm -rf package
2. Загрузить в Object Storage:
yc storage s3 cp bot-function.zip s3://my-bot-data/deploy/bot-function.zip
3. Создать функцию:
yc serverless function create --name my-bot
4. Задеплоить версию:
yc serverless function version create \ --function-name my-bot \ --runtime python312 \ --entrypoint handler.handler \ --memory 128m \ --execution-timeout 10s \ --package-bucket-name my-bot-data \ --package-object-name deploy/bot-function.zip \ --environment "BOT_TOKEN=<TOKEN>,WEBHOOK_SECRET=<SECRET>,ADMIN_GROUP_ID=<GROUP_ID>,ADMIN_USER_ID=<USER_ID>,S3_BUCKET=my-bot-data,S3_ENDPOINT=https://storage.yandexcloud.net,AWS_ACCESS_KEY_ID=<KEY_ID>,AWS_SECRET_ACCESS_KEY=<KEY>" \ --service-account-id <sa-id>
5. Открыть публичный доступ:
yc serverless function allow-unauthenticated-invoke --name my-bot
6. Установить webhook:
curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" \ -H "Content-Type: application/json" \ -d '{"url":"https://functions.yandexcloud.net/<function-id>","secret_token":"<SECRET>"}'
7. Тест: откройте бота в Telegram, отправьте /start.
Подводные камни: что пошло не так
1. “Not enough rights to create a topic”
Бот добавлен как админ, но топики не создаёт. Причина: нужно явное разрешение “Управление темами” в правах администратора. Добавить как админа недостаточно — проверяйте каждое разрешение отдельно.
2. Privacy mode блокирует relay
По умолчанию боты в группах видят только команды (сообщения с /). Для relay нужно видеть все сообщения. Решение: @BotFather → /setprivacy → Disable.
3. Лимит 128 символов на название топика
Telegram молча обрезает длинные названия. Если в ответах Q1+Q2 много текста, название топика будет неинформативным. Обрезайте явно в коде:
if len(topic_title) > 128: topic_title = topic_title[:125] + "..."
4. Callback data — максимум 64 байта
Паттерн q2:sales:ecommerce влезает. Но если пользователь выбирает “Другое” и пишет свободный текст — callback не подходит. Поэтому “Другое” переключается на Object Storage для хранения промежуточного состояния.
5. Cold start ~2 секунды
Первый вызов после простоя — медленный. httpx импортируется быстро, но boto3 весит ~30 МБ и тормозит. Не критично: Telegram ждёт ответа 60 секунд. Но если хочется быстрее — можно заменить boto3 на raw HTTP к S3 API.
6. Безопасность: секреты в переменных окружения
В статье мы передаём секреты через --environment — это самый простой способ, но давайте разберём реальные риски.
Что хранится в env vars:
Секрет | Что будет, если утечёт |
|---|---|
| Полный контроль над ботом: чтение сообщений, отправка от его имени |
| Возможность слать фейковые апдейты на функцию |
| Доступ к бакету с маппингами (chat_id пользователей) |
| Не секреты — просто числа, без токена бесполезны |
Единственный по-настоящему чувствительный секрет — BOT_TOKEN. Остальное без токена бесполезно: webhook secret проверяется только в связке с ботом, а S3-ключи дают доступ к бакету с JSON-файлами, где лежат только chat_id.
Кто может увидеть env vars:
Пользователи Yandex Cloud с ролью
functions.editorилиadminна вашу папку (folder)Те, кто имеет доступ к вашей машине — команда попадает в
~/.zsh_history
Если вы единственный пользователь в Yandex Cloud и единственный, кто работает на этой машине — реальный вектор атаки отсутствует. Для соло-проекта с лид-ботом это приемлемый trade-off.
Когда стоит перейти на Yandex Lockbox:
В проекте появились другие люди с доступом к Yandex Cloud
Бот обрабатывает чувствительные данные (платежи, персональные данные)
Вы деплоите через CI/CD, где логи команд могут сохраняться
# Создать секрет в Lockbox yc lockbox secret create --name bot-secrets \ --payload '[{"key":"BOT_TOKEN","text_value":"123:ABC..."}]' # Передать в функцию — секрет инжектится в runtime, # но не хранится в метаданных функции yc serverless function version create \ --function-name my-bot \ --secret name=bot-secrets,key=BOT_TOKEN,environment-variable=BOT_TOKEN \ ...
Lockbox бесплатен для первых 3 секретов. Но для соло-бота с 5 лидами в месяц — --environment достаточно.
Итого
Мини-CRM в Telegram: бот для квалификации лидов + группа с топиками для переписки. Ноль рублей в месяц, ноль инфраструктуры, работает с телефона.
Что | Как |
|---|---|
Приём лидов | Telegram-бот с inline-кнопками |
Хранение | Топики в Telegram-группе |
Переписка | Двусторонний relay через бота |
Хостинг | Yandex Cloud Functions (бесплатно) |
Состояние | Object Storage (бесплатно) |
Навыки Claude Code для Yandex Cloud (open source): GitHub
Делаю AI-автоматизации для бизнеса: агенты, интеграции с API, чат-боты с LLM под капотом. Если есть задача — напишите в Telegram.
