Metadata-Version: 2.4
Name: cozygram
Version: 1.0.0
Summary: Официальная библиотека Cozygram Bot API 1.0
License: MIT
Keywords: cozygram,bot,api,messenger
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Communications :: Chat
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# Cozygram Bot API 1.0

Боты в Cozygram работают так же, как в Telegram: вы создаёте бота, получаете
токен, запускаете свою программу у себя — и бот отвечает людям в мессенджере.

---

## Главная идея: бот — это обычный пользователь

Каждый бот — это настоящая запись в `auth.users` и `profiles`, только с флагом
`is_bot = true` и владельцем `bot_owner`.

Почему это важно: ботам не нужно ничего дописывать в мессенджере. Они сразу
умеют всё, что умеют люди: переписка, вложения, реакции, ответы цитатой,
поиск по людям, мгновенная доставка через realtime. Код чатов не тронут вообще.

Отличия бота от человека всего три:

1. Входит не по паролю, а по токену.
2. Не получает push-уведомлений — вместо них очередь обновлений или webhook.
3. Не может создавать других ботов (иначе лавина аккаунтов).

---

## Что внутри

```
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, одной строкой. Она сразу выведет токен — сохраните его:

```sql
select public.bot_bootstrap('cozyfather_bot', 'CozyFather', null, '{bots:manage}');
```

Никаких тыканий в мобильном приложении не требуется: функция сама создаёт
запись в `auth.users`, профиль с 🤖 и самого бота. Пароля у такого аккаунта
нет вообще — войти в него как человек невозможно никому, включая админа.

Строку можно вызывать повторно: бот не удвоится, а получит новый токен
вместо старого. Так же восстанавливают потерянный токен.

Права существующему боту меняются отдельно:

```sql
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 неизбежно
расходятся — починишь проверку в одной, а во второй дыра останется.
Если вы разворачивали их раньше — удалите, чтобы старый адрес не отвечал:

```bash
supabase functions delete bot
supabase functions delete bot-manage
```

### 4. Адрес шлюза в мобильном приложении

Приложение ходит в тот же `/api/bot-manage`, что и веб-кабинет. Адрес лежит
в `lib/config/supabase_config.dart` и подменяется при сборке без правки кода:

```bash
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`:

```python
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.**

```bash
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 ботов. Все вызовы от имени бота пишутся в лог.

### Запуск

```bash
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<ТОКЕН>/<метод>
```

Успешно:

```json
{ "ok": true, "result": { } }
```

Ошибка:

```json
{ "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:

```python
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` выключает его |
