Cozygram Bot API 1.0
Боты в Cozygram работают так же, как в Telegram: вы создаёте бота, получаете токен, запускаете свою программу у себя — и бот отвечает людям в мессенджере.
Главная идея: бот — это обычный пользователь
Каждый бот — это настоящая запись в auth.users и profiles, только с флагом
is_bot = true и владельцем bot_owner.
Почему это важно: ботам не нужно ничего дописывать в мессенджере. Они сразу умеют всё, что умеют люди: переписка, вложения, реакции, ответы цитатой, поиск по людям, мгновенная доставка через realtime. Код чатов не тронут вообще.
Отличия бота от человека всего три:
- Входит не по паролю, а по токену.
- Не получает push-уведомлений — вместо них очередь обновлений или webhook.
- Не может создавать других ботов (иначе лавина аккаунтов).
Что внутри
api/
README.md ← этот файл
python/
cozygram/
__init__.py ← from cozygram import Bot
client.py ← клиент + цикл опроса
types.py ← Message, User, Chat, Attachment, Update
errors.py ← ApiError, Unauthorized, NetworkError
examples/
echo_bot.py ← самый простой бот
cozyfather.py ← бот, который создаёт ботов
pyproject.toml
web/ ← сайт и САМ ШЛЮЗ (Next.js, разворачивается на Vercel)
app/api/bot/[...path]/ ← публичный Bot API
app/api/bot-manage/ ← создание ботов и токены
app/bots/ ← кабинет «Мои боты»
lib/methods/ ← реализация методов API
supabase/
bot_api.sql ← таблицы, очередь, триггеры
functions/push/index.ts ← push-уведомления (к ботам не относится)
lib/services/bot_service.dart ← клиент управления в приложении
lib/screens/bots_screen.dart ← Настройки → Боты
Установка (один раз)
1. База
В Supabase → SQL Editor выполните supabase/bot_api.sql.
Он идемпотентный — можно запускать повторно. Требует уже применённого
schema.sql.
2. Служебный бот CozyFather
Там же, в SQL Editor, одной строкой. Она сразу выведет токен — сохраните его:
select public.bot_bootstrap('cozyfather_bot', 'CozyFather', null, '{bots:manage}');
Никаких тыканий в мобильном приложении не требуется: функция сама создаёт
запись в auth.users, профиль с 🤖 и самого бота. Пароля у такого аккаунта
нет вообще — войти в него как человек невозможно никому, включая админа.
Строку можно вызывать повторно: бот не удвоится, а получит новый токен вместо старого. Так же восстанавливают потерянный токен.
Права существующему боту меняются отдельно:
select public.bot_set_scopes('cozyfather_bot', '{bots:manage}'); -- выдать
select public.bot_set_scopes('cozyfather_bot', '{}'); -- отобрать
Обе функции отозваны у anon и authenticated — вызвать их из клиента
невозможно, только вручную в SQL Editor.
3. Шлюз
Шлюз живёт в папке web/ — это приложение Next.js, которое разворачивается
на Vercel одной кнопкой:
| Маршрут | Что делает | Авторизация |
|---|---|---|
POST /api/bot/bot<ТОКЕН>/<метод> |
публичный Bot API | токен бота в адресе |
POST /api/bot-manage |
создание ботов и токены | JWT человека либо Bearer bot<токен> со scope |
Переменные окружения — в Vercel → Settings → Environment Variables,
образец лежит в web/.env.example:
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=... # только сервер, без префикса NEXT_PUBLIC_
Открытый адрес /api/bot — это не дыра: шлюз сам сверяет SHA-256 токена
с базой и без верного токена не делает ничего. Закрыть его JWT нельзя —
у бота нет сессии человека, его единственное удостоверение и есть токен.
Раньше та же логика дублировалась в edge-функциях supabase/functions/bot
и bot-manage. Их больше нет: две реализации одного API неизбежно
расходятся — починишь проверку в одной, а во второй дыра останется.
Если вы разворачивали их раньше — удалите, чтобы старый адрес не отвечал:
supabase functions delete bot
supabase functions delete bot-manage
4. Адрес шлюза в мобильном приложении
Приложение ходит в тот же /api/bot-manage, что и веб-кабинет. Адрес лежит
в lib/config/supabase_config.dart и подменяется при сборке без правки кода:
flutter build apk --dart-define=COZYGRAM_SITE=https://ваш-домен.vercel.app
Без флага берётся defaultValue из того же файла — поправьте его под себя
один раз после первого развёртывания.
Сводка, кто куда стучится:
| Кто | Адрес | Где задаётся |
|---|---|---|
| Мобильное приложение | /api/bot-manage |
--dart-define=COZYGRAM_SITE |
| Веб-кабинет | /api/bot-manage |
свой же домен, автоматически |
| Библиотека Python | /api/bot |
api_base= или COZYGRAM_API_BASE |
| CozyFather | оба | COZYGRAM_SITE |
Быстрый старт: ваш первый бот за две минуты
Шаг 1. В приложении: Настройки → Боты → +. Введите имя и @username
(обязательно оканчивается на bot). Скопируйте токен.
Шаг 2. Создайте my_bot.py:
from cozygram import Bot
# api_base — адрес вашего шлюза. Можно не передавать здесь,
# а задать переменную окружения COZYGRAM_API_BASE.
bot = Bot("7:ваш-токен", api_base="https://ваш-домен.vercel.app/api/bot")
@bot.command("start")
def start(message):
bot.reply(message, "Привет! Я живой.")
@bot.message
def echo(message):
bot.send_message(message.chat.id, message.text)
bot.run()
Шаг 3.
cd api/python
python examples/echo_bot.py # или python my_bot.py
Готово. Найдите бота в поиске по @username и напишите ему.
Зависимостей нет намеренно: библиотека живёт на чистом Python 3.9+, без
pip install. Скопировали папку cozygram/ рядом со скриптом — уже работает.
CozyFather — бот, который создаёт ботов
Аналог BotFather. Диалог один в один, как в Telegram:
/newbot → спросит имя, потом @username, выдаст токен
/mybots → список ваших ботов
/revoke → выдать новый токен
/deletebot → удалить бота
/cancel → прервать действие
Как решена проблема курицы и яйца
Первый бот действительно не может создать себя через API. Поэтому он создаётся
уровнем ниже — функцией bot_bootstrap в самой базе, как часть установки
(шаг 2 выше). Запись в auth.users напрямую — штатный способ засева
пользователей в Supabase, так же работают seed-файлы.
Никакого админа с телефоном в руках больше не нужно: развёртывание — это
три SQL-запроса и две команды deploy, воспроизводимые на любом новом проекте.
Почему ему НЕ нужен админский ключ
У бота нет JWT того, кто ему написал. Раньше это решалось service_role ключом, но это плохой обмен: такой ключ может ВООБЩЕ ВСЁ — читать любую переписку, обходить RLS, удалять аккаунты. Отдавать его ради одной функции — то же самое, что дать курьеру ключи от всего дома, чтобы он оставил посылку у двери.
Сейчас сделано так, как делают в M2M-авторизации: точечное право (scope) вместо мастер-ключа. Принцип наименьших привилегий.
| Режим | Кто использует | Авторизация | Владелец бота |
|---|---|---|---|
| Человек | приложение | JWT человека | сам человек |
| Служебный бот | CozyFather | Bearer bot<свой токен> + scope bots:manage |
owner_id, но только с доказанным делегированием |
| Админ | ручные скрипты | service_role ключ | owner_id |
Третий режим оставлен только как аварийный выход для ручных скриптов. В коде ботов он больше не используется нигде.
Доказательство делегирования
Одного scope мало: иначе CozyFather мог бы наплодить ботов на любой чужой
аккаунт, просто подставив owner_id. Поэтому сервер проверяет ещё и то,
что человек САМ написал этому боту хотя бы одно сообщение.
Сообщение в messages — и есть согласие пользователя: его нельзя подделать
со стороны бота (он не может писать от имени человека) и оно проверяется
запросом к базе. Ровно то же действие, что в Telegram: чтобы завести бота,
вы сначала пишете BotFather /start.
Итог: если токен CozyFather утечёт, вор не получит ни одной ��ужой переписки
и ни одного токена других ботов. Максимум ущерба — возня с ботами тех людей,
кто успел написать CozyFather. Лечится отзывом права одной строкой:
select public.bot_set_scopes('cozyfather_bot', '{}');
Сверху — ограничение частоты: не больше 5 новых ботов в час на владельца и всё та же общая шапка 20 ботов. Все вызовы от имени бота пишутся в лог.
Запуск
export COZYGRAM_TOKEN="1:токен-CozyFather"
export COZYGRAM_SITE="https://ваш-домен.vercel.app"
python api/python/examples/cozyfather.py
Админский ключ базы на сервере бота больше не нужен вообще. Если видите его в переменных окружения бота — это ошибка конфигурации, а не требование.
Токены
Формат: <номер бота>:<32 случайных символа>, например 7:kQ8_xR2….
Как хранится:
- в базе лежит только SHA-256 — самого токена нет нигде;
- видимый огарок
7:kQ8x…R2mнужен только чтобы узнавать свой токен в списке; - полный токен показывается один раз — при создании или перевыпуске;
- потеряли — восстановить невозможно даже админу, только выдать новый.
Токен — это полный доступ к боту. Не публикуйте его в GitHub.
Если утёк — /revoke или «Выдать новый токен», старый умирает мгновенно.
Ссылка и формат ответов
POST https://<ваш-домен>/api/bot/bot<ТОКЕН>/<метод>
Успешно:
{ "ok": true, "result": { } }
Ошибка:
{ "ok": false, "error_code": 401, "description": "Неверный токен" }
Тот же договор, что у Telegram Bot API — привычно всем, кто писал ботов.
Методы
О себе
| Метод | Описание |
|---|---|
getMe |
кто я |
setMyName |
сменить видимое имя |
setMyDescription |
описание бота |
setMyCommands / getMyCommands |
меню команд |
Получение сообщений
| Метод | Описание |
|---|---|
getUpdates |
очередь, long polling до 25 с |
setWebhook |
присылать на ваш https-адрес |
deleteWebhook |
вернуться к очереди |
getWebhookInfo |
текущие настройки |
Одновременно работает только один способ. С установленным webhook getUpdates
вернёт 409 — так же, как в Telegram.
При включённом webhook очередь не ведётся вообще: обновления уходят сразу
POST-ом, а pending_update_count честно показывает ноль. Иначе в базе копился
бы вечный хвост «необработанных» событий, которые на деле давно доставлены.
Переписка
| Метод | Описание |
|---|---|
sendMessage |
текст, ответ цитатой, вложение |
editMessageText |
править только свои сообщения |
deleteMessage |
удалить своё сообщение |
setMessageReaction |
поставить или снять реакцию |
getChat |
кто мой собеседник |
getChatHistory |
до 200 последних сообщений |
chat_id принимает и uuid, и @username — удобно при отладке вручную.
Webhook вместо опроса
Опрос прост, но держит процесс. Если есть свой сервер с https:
bot.set_webhook("https://example.com/hook", secret_token="мой-секрет")
setWebhook сначала делает пробный POST на ваш адрес с телом
{"update_id": 0, "probe": true} и сохраняет настройку, только если пришёл
ответ 2xx. Смысл: включение webhook отключает очередь, и опечатка в адресе
обернулась бы тихой потерей сообщений. Ваш обработчик должен отвечать 200
на такой пробный запрос.
Теперь база сама шлёт POST на ваш адрес при каждом сообщении, с заголовком
X-Cozygram-Bot-Api-Secret-Token. Всегда сверяйте этот заголовок, иначе ваш
адрес сможет дёрнуть любой.
Какой адрес примут, а какой отклонят
Адрес webhook — это место, куда наш сервер сам сделает запрос. Если разрешить туда внутренние адреса, бота можно превратить в прокси внутрь инфраструктуры и вытащить ключи из метаданных облака (атака SSRF). Поэтому шлюз проверяет сам адрес, а не сверяет его со списком плохих доменов — такой список обходится.
| Адрес | Результат |
|---|---|
https://example.com/hook |
принят |
https://example.com:8443/hook |
принят (443 и 8443) |
http://example.com/hook |
отказ — только https |
https://localhost/hook |
отказ — внутренний адрес |
https://10.0.0.5/hook, 192.168.*, 172.16-31.* |
отказ — локальная сеть |
https://169.254.169.254/... |
отказ — метаданные облака |
https://2130706433/hook |
отказ — тот же 127.0.0.1 числом |
https://кто-то:пароль@example.com |
отказ — логин в адресе |
Честная оговорка: проверяется текст адреса, а не то, во что его потом
разрешит DNS. Домен, указывающий на 127.0.0.1, формально пройдёт. Полностью
это закрывается только прокси типа Smokescreen, а его внутри базы не поставить.
Для личного проекта этого уровня достаточно.
Ограничение частоты запросов
Адрес /api/bot открыт всему интернету и не может быть закрыт JWT — иначе
боты не смогли бы ходить по своему токену. Токены никто не угадает, но без
счётчика поток мусорных запросов сжёг бы месячный лимит хостинга —
то есть устроил бы вам счёт или простой.
Счётчик живёт в базе (public.bot_rate и bot_rate_check()), без Redis и без
сторонних подписок. Два уровня:
| Уровень | Лимит | Смысл |
|---|---|---|
| По IP-адресу, до проверки токена | 300 запросов в минуту | поток с неверными токенами отсекается сразу |
| На бота | 120 запросов в минуту | один шумный бот не мешает остальным |
| Новые боты на человека | 5 в час | нельзя наплодить тысячу аккаунтов |
При превышении приходит 429. Цифры меняются в одном месте — константы
IP_LIMIT и BOT_LIMIT в web/app/api/bot/[...path]/route.ts, NEW_BOTS_PER_HOUR
и MAX_BOTS_PER_USER — в web/app/api/bot-manage/route.ts и web/lib/manage-actions.ts.
Опрос getUpdates держит соединение до 25 секунд, поэтому обычный бот
тратит около 3 запросов в минуту и в лимит не упирается.
Ограничения
Что есть сейчас:
- до 20 ботов на человека;
- текст до 4096 символов;
- очередь чистится через 7 дней (
bot_updates_cleanup()); - только личные чаты — групп в мессенджере пока нет.
Чего ещё нет, сознательно:
- инлайн-клавиатур и кнопок — нужна поддержка в клиенте;
- загрузки файлов прямо через API — пока передавайте готовый
attachment_url; - инлайн-режима и платежей.
Шифрование: если человек включил E2E, бот увидит шифротекст — у бота нет ключа. Это не баг, а смысл шифрования. Общайтесь с ботами в обычных чатах.
Если что-то не работает
| Симптом | Причина |
|---|---|
401 Неверный токен |
токен перевыпущен или скопирован с пробелом |
401 у живого токена |
бот выключен в Настройки → Боты |
getUpdates всегда пустой |
не применён bot_api.sql — нет триггера очереди |
409 на getUpdates |
установлен webhook, снимите deleteWebhook |
| бот не находится в поиске | ищите по @username без собаки |
| в списке «ни разу не запускался» | код бота ещё не запущен нигде |
429 на любом методе |
упёрлись в счётчик частоты — см. раздел выше |
400 Внутренние адреса запрещены на setWebhook |
нужен публичный https-домен, не localhost |
converting NULL to string is unsupported после bot_bootstrap |
старая версия bot_api.sql; примените свежий — он сам ставит пустые строки |
| список ботов в приложении пуст, хотя боты есть | вьюха my_bots с security_invoker = on; свежий bot_api.sql выключает его |
Release files for cozygram 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cozygram-1.0.0.tar.gz | 24.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cozygram-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.5 kB
Release files / cozygram-1.0.0.tar.gz
| Download URL | cozygram-1.0.0.tar.gz |
|---|---|
| Size | 24.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ba5a4373e178d1b787108b9f58e0e7be1f80096b32c7ec95e4fcf88dbefde7cb
|
|
BLAKE2b-256 checksum How to use checksums |
68c5a8f37afed9bd61785244ff2dc87081c75c3ec90627b965c7549a3596f627
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.10
|
Release files / cozygram-1.0.0-py3-none-any.whl
| Download URL | cozygram-1.0.0-py3-none-any.whl |
|---|---|
| Size | 17.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e8b72e7729dc4c19a0d3c34125039c7708026ca6b12028bc692733536e4a8ec8
|
|
BLAKE2b-256 checksum How to use checksums |
b94aeac6d07853fe20a2ab8fc7577a7dc92a45ee24d5a8262f748731512858d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.10
|