Skip to content

About

Quick notes via Telegram bot with saving in Buildin or Notion

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

notes-bot

version

Telegram-бот для персональных заметок: принимает текст, голос, видео, документы, форварды; транскрибирует медиа, извлекает текст из файлов (txt/md/csv/pdf/docx), классифицирует через GPT-4o, сохраняет готовые страницы в Notion (по умолчанию) или Buildin — переключатель NOTES_PROVIDER=notion|buildin.

Бот заточен под личный workflow автора: набор workspace'ов, типов заметок и DB-properties — это реф-пример, не «единственный правильный дефолт». Если форкаешь под себя — отредактируй bot/domain/workspaces.py и bot/domain/note_types.py (см. раздел «Под себя» ниже).

Эволюция: отдельные ветки и Conventional-Commits-PR в main (см. AGENTS.md).

Что умеет (на 0.11.0)

  • Принимает текст, голосовые, аудио, видео, видео-кружочки, документы, фото (одиночные и альбомы), форварды.
  • Голос/аудио/видео транскрибируется через OpenAI Speech-to-Text (gpt-4o-mini-transcribe по умолчанию, ~$0.003/мин и ~6% WER на русском; для шумных записей — gpt-4o-transcribe, ~$0.006/мин и ~5% WER, переключается через OPENAI_TRANSCRIBE_MODEL). Сырой транскрипт остаётся в БД (промежуточный шаг), в провайдер уходит только готовая заметка после LLM. Без OPENAI_API_KEY — голосовые отключены, бот отвечает «транскрибация выключена».
  • Документы (txt, md, csv, pdf, docx) — прямой upload или форвард. Текст извлекается локально (pypdf для PDF, python-docx для DOCX, stdlib для остальных) и идёт в тот же LLM-pipeline. Лимит — 20 МБ (Telegram bot API). Длинные файлы (>40k символов после извлечения) проходят map-reduce: бьются на куски, каждый кусок суммаризуется отдельным LLM-вызовом, итоговая склейка классифицируется как обычно. Зашифрованные/сканированные PDF без текстового слоя помечают draft failed с понятным сообщением.
  • Для форвардов извлекаются метаданные (автор, канал, дата, подпись, оригинальный message_id) и подаются в LLM как контекст-prefix [Forwarded] От: …; Когда: …. Draft помечается kind=forward.
  • Фото и альбомы: форвард поста с одной или несколькими картинками превращается в заметку, фото вставляется image-блоком перед body в Notion / Buildin. Caption уходит в LLM как обычный текст (с тем же forward-prefix, что и для текстовых форвардов). Альбом агрегируется через AlbumMiddleware (debounce 1 c по media_group_id) — один draft на весь альбом. URL фото — Telegram file-URL, живёт ~1 час: успей открыть страницу в Notion/Buildin чтобы провайдер закэшировал превью. Photo без caption → draft с пустым body, title = Photo from <author/chat>, LLM не вызывается.
  • Создаёт черновик в SQLite до любой обработки (durability-контракт).
  • Один GPT-4o-вызов: классифицирует тип заметки и workspace (наборы заданы в реестрах bot/domain/note_types.py и bot/domain/workspaces.py — в текущем реф-сетапе 9 типов: note / task / idea / meeting / 1on1 / work / personal / book / film, и 5 workspace'ов: personal / work / family / growth / ai_path), извлекает properties (Status, Priority, DueDate, Tags, Attendees, …) и форматирует тело как markdown под template типа. Для meeting отдельно различает регулярные синки команды через extras.kind="sync": для sync LLM кладёт всю иерархию (по людям/зонам/темам) прямо в markdown_body через ### subheading + bullets — шаблон passthrough, без дублирующих секций. Для классического meeting (kind=meeting) — секции Agenda / Decisions / Action items. Для task описание задачи (контекст, мотивация) пишется в markdown_body перед ## Чек-лист. Для meeting / 1on1 LLM проставляет дату в properties.Date (ISO-формат YYYY-MM-DD, требование Notion API) и в title в скобках в человеческом формате (DD.MM.YYYY). Без OPENAI_API_KEY — stub-fallback (тип note, workspace personal, тело = исходник).
  • Превью с кнопками: 💾 Save / ✖ Cancel сверху, 🔁 Type / 📁 Workspace ниже, ряд ✏️ Edit. Для type=meeting добавляется ряд 🔄 Sync ↔ Meeting для ручного переключения подвида; рядом с типом показывается метка (sync) если kind=sync. Тап по тоглу запускает повторный LLM-проход с подсказкой force_meeting_kind, чтобы тело переразметить под нужный шаблон (Agenda/Decisions для meeting или иерархический passthrough для sync). Workspace и тип можно переопределить вручную.
  • ✏️ Edit открывает Telegram Web App — встроенную форму с предзаполненными title и body (полный текст из БД, без обрезки превью). Юзер правит оба поля сразу, тапает «Сохранить» — то же самое preview-сообщение в чате обновляется in-place, без новых сообщений и без перенабора. Требует WEBAPP_BASE_URL (поддомен с HTTPS) и /setdomain в BotFather; без env-переменной кнопка скрыта.
  • На Save — кладёт в outbox-очередь, фоновой воркер вызывает API провайдера (Buildin или Notion в зависимости от NOTES_PROVIDER).
  • Routing: per-(workspace × type) для Buildin (BUILDIN_DB_<WS>_<TYPE> → BUILDIN_DB_<TYPE> → BUILDIN_DB_DEFAULT); для Notion та же цепочка (NOTION_DB_<WS>_<TYPE> → NOTION_DB_<TYPE> → NOTION_DATABASE_ID) + lazy one-step auto-create под NOTION_PARENT_PAGE_<WS> если ничего не задано — бот создаёт full-page DB прямо в parent-page при первом hit и кэширует database_id в notion_dbs. Notion рендерит full-page DB как страницу-раздел (📝 Заметки / ✅ Задачи / …) с full-width таблицей, отдельная wrapper-page не нужна. Без токена провайдера — stub-режим.
  • Markdown тела заметки конвертируется в provider-specific blocks (параграфы, заголовки #/##/###, списки -/1., цитаты >, code-fences ```). Для Buildin длинные заметки (>100 блоков) дописываются чанками по 100 через PATCH /v1/blocks/{page_id}/children.
  • На успех — атомарно idempotency.insert + drafts.delete + outbox.delete. Превью редактируется в «✅ Открыть в Buildin/Notion» с прямой ссылкой на созданную страницу (если провайдер вернул url); иначе — fallback на <code>page_id</code>.
  • На ошибку — экспоненциальный backoff. После MAX_ATTEMPTS черновик помечается failed, остаётся видимым через /list.
  • При NOTES_PROVIDER=buildin бот на старте делает health-check (GET /v1/users/me) и предупреждает о незаданных BUILDIN_SPACE_<WS> env'ах.
  • Команды: /start, /help, /list, /retry.
  • На старте: восстановление черновиков, застрявших в saving дольше 5 минут.

Подключение Buildin

1. Integration

В Buildin: settings → integrations → создать новую integration. Скопировать токен → BUILDIN_TOKEN в .env.

2. Spaces (workspace'ы)

В реф-сетапе настроены 5 workspace'ов: personal, work, family, growth, ai_path (см. bot/domain/workspaces.py — там же поля label, description, space_env). Если форкаешь под себя — поправь реестр и env-vars из таблицы ниже соответственно. Под каждый workspace нужен отдельный Buildin space. Создайте space'ы руками, разрешите для них integration, скопируйте UUID:

Workspace Env var
personal (Личное) BUILDIN_SPACE_PERSONAL
work (Работа) BUILDIN_SPACE_WORK
family (Семья) BUILDIN_SPACE_FAMILY
growth (Куда расти?) BUILDIN_SPACE_GROWTH
ai_path (Путь ИИ) BUILDIN_SPACE_AI_PATH
blog (Блог) BUILDIN_SPACE_BLOG

3. Базы (databases)

Внутри каждого space скрипт создаёт full-page DB на каждый тип заметки. Title берётся из plural-формы реестра типов (в реф-сетапе: 📝 Заметки, ✅ Задачи, 💡 Идеи, 🤝 Митинги, 👥 1:1, 💼 Рабочее, 🌱 Личное — см. _PLURAL_TITLES в скрипте) — без префикса воркспейса, так как имя space'а уже задаёт контекст. Buildin рендерит full-page DB как страницу-раздел с full-width таблицей. Структура в Buildin:

space «Работа»
├── DB ✅ Задачи         ← full-page DB (создаст скрипт)
├── DB 📝 Заметки
└── …

Можно создать руками (тогда скопируйте UUID DB из URL в BUILDIN_DB_<WS>_<TYPE>), либо запустить автосоздание:

source .venv/bin/activate
python -m scripts.setup_buildin_dbs >> .env

Скрипт уважает уже заданные env'ы — заполняет только пустые slot'ы. Доп. флаги: --dry-run (без сетевых вызовов), --workspace=<key>, --type=<key> (только конкретный workspace или тип).

Кроме перечисленных свойств бот всегда дописывает CreatedAt (date) — оно создаётся скриптом автоматически.

4. Переключатель провайдера

NOTES_PROVIDER=buildin включает Buildin sink. Дефолт notion — для переключения на Buildin поставьте NOTES_PROVIDER=buildin.

Подключение Notion (default)

1. Интеграция

Рекомендуемый путь — per-workspace токены. В каждом Notion workspace, где лежит NOTION_PARENT_PAGE_<WS>, создать свой Internal Integration на https://www.notion.so/my-integrations и положить токен в NOTION_TOKEN_<WS>. Internal Integration привязан к одному Notion workspace — единый токен не сможет ходить между ними.

NOTION_TOKEN_PERSONAL=...
NOTION_TOKEN_WORK=...
NOTION_TOKEN_FAMILY=...
NOTION_TOKEN_GROWTH=...
NOTION_TOKEN_AI_PATH=...

Если все NOTION_PARENT_PAGE_<WS> лежат в одном Notion workspace — достаточно одного Integration и legacy NOTION_TOKEN (fallback, когда per-WS не задан):

NOTION_TOKEN=...               # legacy fallback

Per-WS токен имеет приоритет над NOTION_TOKEN.

2. Резолв DB id и lazy one-step auto-create

Бот ищет database для пары (workspace, type) по цепочке:

notion_dbs (SQLite cache)
  → NOTION_DB_<WS>_<TYPE>     (env, например NOTION_DB_WORK_TASK)
  → NOTION_DB_<TYPE>          (per-type, без workspace-разреза)
  → NOTION_DATABASE_ID        (глобальный fallback)
  → one-step auto-create под NOTION_PARENT_PAGE_<WS>:
      databases.create (full-page DB прямо в parent-page,
                        title = plural-форма типа).
    database_id кэшируется в notion_dbs.

Любой первый результат — финальный. Notion рендерит full-page DB как страницу-раздел с full-width таблицей — отдельная wrapper-page не нужна.

3. Lazy auto-create (рекомендуется)

Минимальный setup без ручного создания 35 баз. Создаёшь по одной пустой top-level странице в каждом Notion workspace — она будет parent для воркспейса бота. Расшарить страницу с integration — все вложенные DB наследуют доступ.

(Notion workspace «Личное»)
└── Notes-bot          ← parent-page (расшарена с integration)
    ├── DB 📝 Заметки  ← full-page DB (создаст бот при первом hit)
    ├── DB ✅ Задачи
    └── …

ID каждой parent-страницы (32-hex из URL) → в .env:

NOTION_PARENT_PAGE_PERSONAL=...
NOTION_PARENT_PAGE_WORK=...
NOTION_PARENT_PAGE_FAMILY=...
NOTION_PARENT_PAGE_GROWTH=...
NOTION_PARENT_PAGE_AI_PATH=...

При первом сохранении в (workspace × type) бот создаст full-page DB с plural-title типа (📝 Заметки и т.п.) под parent-страницей, со схемой типа из реестра (note_types.py), и закэширует database_id в SQLite. Структура растёт по факту использования — пустые комбинации не плодятся. Workspace, для которого NOTION_PARENT_PAGE_<WS> не задан, фолбэкнется на NOTION_DB_<TYPE> или NOTION_DATABASE_ID.

4. Ручное создание баз (опционально)

Если хочется контролировать схему — создавай руками. Schema ниже — реф-пример из текущего bot/domain/note_types.py. Если редактируешь реестр, эта таблица перестанет соответствовать твоей конфигурации:

Тип Env var (per-type) Required properties (имя — kind)
note NOTION_DB_NOTE Name (title), Tags (multi_select)
task NOTION_DB_TASK Name (title), Status (select), Priority (select), DueDate (date)
idea NOTION_DB_IDEA Name (title), Tags (multi_select)
meeting NOTION_DB_MEETING Name (title), Attendees (multi_select), Date (date)
1on1 NOTION_DB_1ON1 Name (title), With (rich_text), Date (date)
work NOTION_DB_WORK Name (title), Tags (multi_select)
personal NOTION_DB_PERSONAL Name (title), Tags (multi_select)
book NOTION_DB_BOOK Name (title), Author (rich_text), Rating (select ⭐…⭐×10)
film NOTION_DB_FILM Name (title), Director (rich_text), Year (rich_text), Genre (multi_select), Rating (select ⭐…⭐×10)

Per-(workspace × type): NOTION_DB_<WS>_<TYPE> (например NOTION_DB_WORK_TASK). Имеет высший приоритет среди env. Бот всегда дописывает CreatedAt (date) — auto-create добавляет это поле сам.

5. Шаринг

Для каждой базы / родительской страницы: … (меню в правом верхнем углу) → Connections → выбрать integration. Auto-created DB наследуют доступ от parent-page. Top-level page расшарена один раз — все вложенные DB и страницы наследуют.

6. ID

Скопировать database / page id из URL (32-символьный hex после workspace) в соответствующую переменную .env. Без любого NOTION_TOKEN_<WS> / NOTION_TOKEN бот работает в stub-режиме (логирует payload).

OpenAI

OPENAI_API_KEY в .env — включает реальный GPT-4o. Без ключа — stub (тип note, body = raw input). Модель меняется через OPENAI_MODEL (по умолчанию gpt-4o).

TMDb (для заметок типа film)

TMDB_API_KEY в .env — для заметок типа film бот ходит в TMDb (themoviedb.org) и дозаполняет пустые Director/Year/Genre. Покрывает knowledge cutoff модели (gpt-4o знает только релизы до ~окт 2023). TMDb бесплатен, лимиты для личного бота избыточны. Регистрация: https://www.themoviedb.org/settings/api → API Key (v3 auth). Пустой ключ — дозаполнение отключено, поля заполняются только из LLM-extraction.

Под себя

Реестры в bot/domain/ — это setup автора. Бот рассчитан на то, что форкер их отредактирует под свой workflow. Файлы захардкожены не из-за архитектурного ограничения, а потому что Python-dataclass'ы — самая понятная форма для одного человека, который сам себе админ.

Шаг 1. bot/domain/workspaces.py — добавить/удалить/переименовать записи в WORKSPACES. Поля Workspace:

  • key — ASCII-friendly id (попадает в env-имена и LLM-ответ).
  • label — UI-строка для TG-кнопок (RU/EN/любой).
  • description — подсказка для LLM, когда выбрать этот workspace.
  • space_env — имя env-переменной для UUID Buildin space'а.

Шаг 2. bot/domain/note_types.py — записи в TYPES. Поля NoteType:

  • key, label, description (как у workspace).
  • db_env — имя env-переменной для per-type Notion DB id (например NOTION_DB_TASK). Для Buildin берётся суффикс (NOTION_DB_TASK → BUILDIN_DB_<WS>_TASK).
  • template_id — ключ ветки в domain/templates.py.
  • properties — список колонок DB. Каждая NotionProperty: name (case-sensitive имя колонки), kind (title|rich_text|select|multi_select|date|checkbox), llm_hint (что LLM кладёт), select_options (для select/multi_select — фиксированный список значений; нужен setup-скрипту).

Шаг 3. Если меняется template_id — добавить ветку в bot/domain/templates.py (рендер markdown под этот тип).

Шаг 4. Адаптировать тесты: tests/test_workspaces.py и tests/test_note_types.py (а при правке templates — частично tests/test_templates.py) проверяют конкретный набор ключей. После кастомизации обновить тесты под новый набор — это разрешённое исключение (см. CLAUDE.md, «исключения, когда тест разрешено менять»).

Шаг 5. Перегенерировать DB: удалить устаревшие BUILDIN_DB_<WS>_<TYPE> из .env, удалить старые DB в Buildin UI, запустить:

source .venv/bin/activate
python -m scripts.setup_buildin_dbs >> .env

Скрипт читает реестры — создаст DB под обновлённый набор.

Шаг 6. Прогнать pytest -v — всё должно быть зелёным.

Бот всегда дописывает в каждую DB колонку CreatedAt (date) автоматически — объявлять её в properties не нужно.

Стек

  • Python 3.11+
  • aiogram 3.x — Telegram bot framework
  • pydantic-settings + python-dotenv — config через .env
  • aiosqlite — async-драйвер SQLite
  • httpx — async HTTP-клиент для Buildin API (тонкий клиент по openapi)
  • notion-client — Python SDK Notion API (legacy fallback провайдер)
  • openai — GPT-4o classify+format + Speech-to-Text транскрибация (gpt-4o-mini-transcribe / gpt-4o-transcribe)
  • pypdf, python-docx — извлечение текста из PDF/DOCX
  • pytest + pytest-asyncio — тесты (reportlab — dev-only, для генерации тестовых PDF в test_doc_extractor.py)
  • Docker + docker-compose

Структура проекта

bot/
├── main.py                # entry: init_db → buildin_health_check → recover_stuck → outbox worker + webapp server → polling
├── config.py              # pydantic settings
├── handlers/
│   ├── inputs.py          # text → draft → LLM → preview
│   ├── voice.py           # voice/audio/video/video_note → download → transcribe → LLM → preview
│   ├── documents.py       # document (txt/md/csv/pdf/docx) → download → extract → LLM → preview
│   ├── photo.py           # photo (single + альбом) → get_file URL → caption→LLM → image-блок в Notion/Buildin
│   ├── callbacks.py       # Save / Type / Cancel + клавиатура выбора типа
│   └── commands.py        # /start /help /list /retry
├── middlewares/
│   ├── auth.py            # whitelist Telegram user IDs
│   └── album.py           # агрегатор фото-альбома (debounce 1s по media_group_id)
├── services/
│   ├── llm_processor.py   # GPT-4o classify+format (один вызов) + map-reduce process_long + stub fallback
│   ├── doc_extractor.py   # txt/md/csv/pdf/docx → plain text (pypdf, python-docx, stdlib)
│   ├── notion_client.py   # compat-shim → sinks/notion.py
│   ├── preview.py         # refresh_preview helper (in-place edit_message_text)
│   ├── transcriber.py     # OpenAI Speech-to-Text (gpt-4o-mini-transcribe default)
│   ├── tmdb.py            # TMDb lookup для film: дозаполнение Director/Year/Genre
│   └── sinks/             # провайдеры хранилища заметок
│       ├── __init__.py    # Sink Protocol
│       ├── _properties.py # общий property builder (shape="notion"|"buildin")
│       ├── notion.py      # NotionSink — pages.create через notion-client (default)
│       ├── buildin.py     # BuildinSink — httpx + Buildin API
│       └── factory.py     # get_sink() по NOTES_PROVIDER
├── web/                   # Telegram Web App (Mini App) для правки draft
│   ├── auth.py            # validate_init_data — HMAC по схеме Telegram
│   ├── server.py          # aiohttp: GET /edit, POST /edit/submit, GET /healthz
│   └── static/editor.html # одностраничник: title + body, нативная MainButton
├── domain/
│   ├── note_types.py      # 8 типов с properties + db_env + LLM-hints + select_options
│   ├── workspaces.py      # реестр workspaces (personal/work/family/growth/ai_path)
│   └── templates.py       # per-type markdown шаблоны
├── storage/
│   ├── db.py              # aiosqlite-соединение, WAL, миграции
│   ├── drafts.py          # CRUD черновиков
│   ├── outbox.py          # очередь с экспоненциальным backoff
│   ├── idempotency.py     # dedup draft_id → page_id
│   └── save_tx.py         # атомарный commit_save
├── workers/
│   └── outbox_worker.py   # asyncio task: claim_due → save → commit
├── fsm/                   # FSM-стейты (пока пусто, нужен в Increment 3)
└── utils/
    ├── progress.py        # ProgressReporter
    ├── text_chunking.py
    ├── md_blocks.py       # parse_markdown + markdown_to_blocks (Notion) + markdown_to_blocks_buildin + image_block_*
    ├── forward.py         # извлечение метаданных forward + prefix для LLM
    └── errors.py
scripts/
├── setup_buildin_dbs.py   # авто-создание Buildin DB по реестрам
└── backup_db.sh           # online-backup SQLite + ротация (для cron на VPS)
tests/                     # pytest + pytest-asyncio, in-memory SQLite фикстура
data/                      # SQLite БД (volume в compose)

Как развернуть свой

Локально

python3.12 -m venv .venv  # python3.11+ тоже подходит
source .venv/bin/activate
pip install -r requirements-dev.txt

cp .env.example .env
# заполнить BOT_TOKEN и ALLOWED_USER_IDS

python -m bot.main

Docker

cp .env.example .env
docker compose up --build

docker-compose.yml ставит restart: unless-stopped, лимит памяти 512m (хватает для документов, проходящих map-reduce), persistent volume ./data → /app/data (SQLite-файл живёт между рестартами), healthcheck SELECT 1 к БД и ротацию логов (10 МБ × 5 файлов).

Telegram Web App (правка через Mini App)

Кнопка ✏️ Edit в превью открывает встроенную HTML-форму прямо в Telegram-клиенте: title и body предзаполнены полным текстом из БД (без обрезки превью), сабмит обновляет то же preview-сообщение in-place. Без этой настройки кнопка скрыта, всё остальное работает как раньше.

Что нужно сделать:

  1. HTTPS-поддомен, который проксирует на порт WEBAPP_PORT контейнера бота (default 8080). Reverse-proxy любой — nginx, Caddy, Traefik: важно только TLS-termination и proxy_pass http://<bot-host>:8080. Telegram WebApp-кнопки требуют HTTPS; порт у поддомена может быть нестандартным (Telegram-клиенты принимают URL вида https://host:port).

  2. .env бота:

    WEBAPP_BASE_URL=https://<твой-поддомен>[:port]
    WEBAPP_BIND_HOST=0.0.0.0
    WEBAPP_PORT=8080
    

    Бот при старте увидит WEBAPP_BASE_URL, поднимет aiohttp-сервер рядом с polling-ом и начнёт показывать кнопку ✏️ Edit в превью.

  3. Сетевой доступ от reverse-proxy к контейнеру бота. Если оба в Docker — подключить контейнер бота к сети reverse-proxy (через docker-compose.override.yml с networks: { caddy_net: { external: true } } или аналогом). Если reverse-proxy на хосте — оставить ports: ["8080:8080"] в docker-compose.yml и proxy_pass на 127.0.0.1:8080.

  4. BotFather: /setdomain → выбрать бота → ввести поддомен (без порта).

  5. Liveness-check: GET https://<поддомен>/healthz → ok.

Под капотом: Telegram-клиент открывает /edit?draft_id=X, передаёт initData (HMAC-подпись бот-токеном); aiohttp валидирует подпись + владельца draft, отдаёт editor.html с инжектнутыми title/body. На «Сохранить» фронт шлёт POST /edit/submit (initData в заголовке X-Telegram-Init-Data), бэкенд обновляет draft и перерисовывает preview-сообщение через bot.edit_message_text по сохранённому preview_msg_id — без новых сообщений в чате.

VPS (Ubuntu 22.04 / 24.04)

1. Подготовка сервера (один раз)

sudo apt update && sudo apt upgrade -y
sudo apt install -y git sqlite3 curl ca-certificates

# Docker Engine + compose plugin
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
    -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
    https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
    | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
    docker-buildx-plugin docker-compose-plugin

sudo usermod -aG docker $USER  # нужен relogin / `newgrp docker`

Минимум железа: 1 vCPU, 1 ГБ RAM, 10 ГБ SSD. Комфортно: 2 vCPU, 2 ГБ.

2. Деплой кода

sudo mkdir -p /opt/notes-bot && sudo chown $USER:$USER /opt/notes-bot
cd /opt/notes-bot
git clone <repo-url> .
mkdir -p data

# .env заполнить локально (см. разделы «Подключение Buildin / Notion»
# и «OpenAI» выше) и залить:
#   scp .env user@vps:/opt/notes-bot/.env
chmod 600 .env

docker compose up -d --build
docker compose logs -f bot

restart: unless-stopped поднимает контейнер при ребуте VPS автоматически — ничего настраивать в systemd не нужно.

3. Бэкап SQLite (cron + ротация)

scripts/backup_db.sh делает sqlite3 .backup (online, безопасно при работающем боте через WAL), сжимает gzip и удаляет старше KEEP_DAYS.

sudo mkdir -p /var/backups/notes-bot
sudo chown $USER:$USER /var/backups/notes-bot

crontab -e
# добавить строкой:
17 3 * * * /opt/notes-bot/scripts/backup_db.sh >> /var/log/notes-bot-backup.log 2>&1

Восстановление: остановить контейнер, распаковать дамп на место БД, запустить заново.

docker compose stop bot
gunzip -c /var/backups/notes-bot/notes-YYYYMMDD-HHMMSS.db.gz > data/notes.db
docker compose start bot

4. Мониторинг (healthchecks.io)

Создать на healthchecks.io check (free до 20), скопировать ping URL и в crontab -e:

*/5 * * * * docker inspect --format='{{.State.Health.Status}}' \
    $(docker compose -f /opt/notes-bot/docker-compose.yml ps -q bot) 2>/dev/null \
    | grep -q healthy && curl -fsS --retry 3 https://hc-ping.com/<uuid> > /dev/null

Контейнер падает / Docker daemon упал / VPS не поднялся — пинги прекращаются и через 10 минут healthchecks.io шлёт алерт.

5. Обновление до новой версии

cd /opt/notes-bot
git pull
docker compose up -d --build
docker compose logs --tail=100 -f bot

Откат при несовместимой миграции БД: git checkout <prev-tag>, восстановить БД из бэкапа, docker compose up -d --build.

Troubleshooting

Симптом Причина / фикс
Бот молчит на сообщения ALLOWED_USER_IDS не содержит твой TG id (AuthMiddleware режет).
Run polling for bot не появляется Неверный BOT_TOKEN или нет outbound https.
На старте лог BUILDIN_SPACE_<X>= пуст Не заданы UUID space'ов — заполнить или ограничить роутинг через BUILDIN_DB_DEFAULT.
Документ >40k символов падает по OOM Поднять deploy.resources.limits.memory в docker-compose.yml.
sqlite3: not found в backup-скрипте sudo apt install sqlite3 на хост.

Тесты

source .venv/bin/activate
pytest -v

179 тестов на момент 0.11.0 (23 файла):

  • test_config.py, test_auth.py — конфиг и middleware.
  • test_drafts.py, test_outbox.py, test_idempotency.py, test_save_tx.py — storage-слой (in-memory SQLite через фикстуру fresh_db).
  • test_outbox_worker.py — happy path, retry, idempotency после крэша, max_attempts.
  • test_note_types.py, test_templates.py, test_workspaces.py — реестры. ⚠️ Эти тесты завязаны на конкретный реф-сетап и требуют обновления при кастомизации (см. раздел Под себя).
  • test_llm_processor.py — stub + real path с моком OpenAI (включая workspace classification + fallback на ошибке и неизвестный тип/workspace).
  • test_handlers_inputs.py, test_handlers_callbacks.py — UX-логика (выбор типа + workspace).
  • test_md_blocks.py, test_md_blocks_buildin.py — markdown → blocks для Notion и Buildin shapes.
  • test_notion_client.py — Notion sink: stub/real mode, per-type DB routing, multi_select, fallback title, failure-injector.
  • test_buildin_sink.py — Buildin sink (httpx MockTransport): wire shape, workspace × type routing, chunking длинных заметок, error mapping, health-check /v1/users/me.
  • test_workspace_routing.py — settings.database_id_for() для обоих провайдеров.
  • test_sink_factory.py — get_sink() переключение по NOTES_PROVIDER.
  • test_sinks_properties.py — общий property builder для обоих shape'ов (shape="notion"|"buildin"): title/select/multi_select/date/checkbox, пустые значения, неизвестные kind.
  • test_transcriber.py — happy-path с моком OpenAI Speech-to-Text, передача FORCE_LANGUAGE_CODE, disabled-flag.
  • test_handlers_voice.py — happy path с моками download/transcribe/LLM, durability при ошибках.
  • test_forward.py — извлечение метаданных всех типов forward_origin (User/HiddenUser/Chat/Channel) + format_prefix + enrich.
  • test_doc_extractor.py — txt/md/csv/pdf/docx happy-paths, пустой PDF → EmptyDocumentError, бинарь без mime → UnsupportedDocumentError, расширение приоритетнее mime.
  • test_handlers_document.py — happy path с моками download/extract/LLM, oversized/audio/unsupported reject, durability при ошибках, forwarded → kind=forward + enrich-prefix.

Настройка .env

Переменная Назначение
BOT_TOKEN Токен Telegram-бота от @BotFather
ALLOWED_USER_IDS Разрешённые TG user IDs через запятую
DATABASE_PATH Путь к файлу SQLite (по умолчанию data/notes.db)
NOTES_PROVIDER notion (default) или buildin
BUILDIN_TOKEN Токен Buildin integration. Пусто → stub-режим
BUILDIN_SPACE_<WS> UUID space'а workspace'а (PERSONAL, WORK, FAMILY, GROWTH, AI_PATH, BLOG). Нужны для setup-script
BUILDIN_DB_<WS>_<TYPE> Per-(workspace × type) DB id. Заполняется setup-script'ом или руками
BUILDIN_DB_<TYPE> Per-type fallback DB id (без workspace-разреза)
BUILDIN_DB_DEFAULT Финальный fallback DB id
NOTION_TOKEN_<WS> Per-workspace токен Internal Integration (рекомендуемый). Заполнять для каждого NOTION_PARENT_PAGE_<WS>. Имеет приоритет над NOTION_TOKEN
NOTION_TOKEN Legacy fallback: один токен, когда все parent-страницы в одном Notion workspace. Пусто + нет ни одного NOTION_TOKEN_<WS> → stub-режим
NOTION_DATABASE_ID Default DB id (финальный fallback для всех типов и workspace'ов)
NOTION_DB_<TYPE> Per-type DB id: NOTE, TASK, IDEA, MEETING, 1ON1, WORK, PERSONAL. Все optional
NOTION_DB_<WS>_<TYPE> Per-(workspace × type) DB id, например NOTION_DB_WORK_TASK. Высший приоритет среди env. Optional
NOTION_PARENT_PAGE_<WS> Parent-page id для lazy one-step auto-create (full-page DB прямо в parent-page) конкретного workspace. Если задан — бот создаст DB при первом сохранении в (<ws> × type) и закэширует database_id. Optional
OPENAI_API_KEY Ключ OpenAI для GPT-4o classify+format и Speech-to-Text транскрибации. Пусто → stub LLM + voice/audio/video отключены
OPENAI_MODEL Имя LLM-модели для classify+format (по умолчанию gpt-4o)
OPENAI_TRANSCRIBE_MODEL STT-модель: gpt-4o-mini-transcribe (default, ~$0.003/мин, 6% WER на русском) или gpt-4o-transcribe ($0.006/мин, ~5% WER, для шумных записей)
FORCE_LANGUAGE_CODE ru, en, … Пусто → autodetect
TEMP_DIR Временные файлы для скачанных медиа (по умолчанию /tmp/notes-bot)

About

Quick notes via Telegram bot with saving in Buildin or Notion

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages