Skip to content

HermanDp45/TelegramPostWriter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TelegramPostWriter

Local-first инструмент для подготовки постов в Telegram-канал. Telegram Mini App открывается из бота, владелец канала собирает контекст (форварды, заметки, ссылки, фото), генерирует через NVIDIA NIM три варианта поста, выбирает один, ставит на расписание или публикует немедленно.

Главные свойства:

  • Однопользовательский. Авторизация делается через OWNER_TELEGRAM_ID — посторонние не пройдут даже в браузере.
  • Хранилище и расписание держим на одной машине: SQLite + APScheduler в том же процессе, что и FastAPI.
  • Генератор реализован как LangGraph-машина состояний из четырёх узлов с устойчивым парсером JSON для ответов NIM.

Архитектура

┌──────────────────────┐     ┌─────────────────────────────────────────────┐
│  Telegram-клиент     │     │   FastAPI backend (single process)          │
│  (телефон / десктоп) │     │                                             │
│                      │     │  ┌──────────────┐   ┌──────────────────┐    │
│  ┌────────────────┐  │     │  │ /api/*       │   │ APScheduler      │    │
│  │ aiogram bot    │◀─┼─────┼──┤ routers      │   │ (interval job)   │    │
│  │  /start        │  │ TG  │  │ + auth dep   │   │ publish_due_*    │    │
│  │  forwarded msg │  │ Bot │  └──────┬───────┘   └────────┬─────────┘    │
│  └────────────────┘  │ API │         │ services           │              │
│           ▲          │     │  ┌──────▼───────────┐  ┌─────▼──────────┐   │
│           │ webhook  │     │  │ PostDraftAgent   │  │ TelegramPub-   │   │
│           │ /        │     │  │ (LangGraph)      │  │ lisher         │   │
│  ┌────────┴───────┐  │     │  │  ├ retrieve_ctx  │  │ (aiogram Bot)  │   │
│  │ Mini App       │◀─┼─────┼──┤  ├ summarize_img │  └────────┬───────┘   │
│  │ (React + Vite) │  │ HTTP│  │  ├ generate_var  │           │           │
│  └────────────────┘  │     │  │  └ review_var    │           ▼           │
│                      │     │  └──────┬──────────┘   ┌───────────────┐    │
└──────────────────────┘     │         │              │ Telegram      │    │
                             │  ┌──────▼──────────┐   │ channel       │    │
                             │  │ SQLAlchemy ORM  │   └───────────────┘    │
                             │  │ → SQLite file   │                        │
                             │  └─────────────────┘                        │
                             │                                             │
                             │  Outbound: NVIDIA NIM (OpenAI-compatible)   │
                             └─────────────────────────────────────────────┘

Поток данных при создании поста

User в Mini App ──► POST /api/drafts                    (status=draft)
              ──► POST /api/drafts/{id}/media * N       (вложения)
              ──► POST /api/drafts/{id}/generate        (status=generating)
                            │
                            ▼
                  PostDraftAgent (LangGraph)
                    retrieve_context ─► summarize_images
                    ─► generate_variants ─► review_variants
                            │
                            ▼  (3 GeneratedVariant в БД, status=generated)
              ──► POST /api/drafts/{id}/approve         (scheduled / approved)
              ──► POST /api/drafts/{id}/publish-now     (через TelegramPublisher)
                  ИЛИ APScheduler сам опубликует, когда наступит scheduled_at

Поток сбора контекста

Владелец пересылает / пишет боту  ─► aiogram router save_context
                                   ─► INSERT в context_items (kind=forwarded|manual)
                                   ─► Mini App видит чекбокс при создании черновика

Структура репозитория

TelegramPostWriter/
├── backend/
│   ├── app/
│   │   ├── main.py              # FastAPI factory, lifespan, статика фронта
│   │   ├── config.py            # pydantic Settings → переменные из .env
│   │   ├── database.py          # SQLAlchemy engine + get_db dependency
│   │   ├── models.py            # ORM-модели: Channel, ContextItem, Draft,
│   │   │                        # DraftMedia, GeneratedVariant, Publication
│   │   ├── schemas.py           # Pydantic-схемы запроса/ответа API
│   │   ├── auth.py              # Проверка Telegram WebApp initData (HMAC)
│   │   ├── auth_api.py          # /api/auth/debug — диагностика 401
│   │   ├── api.py               # REST-эндпоинты /api/* для Mini App
│   │   ├── services.py          # Бизнес-логика: approve / publish / cancel
│   │   ├── agent.py             # LangGraph-агент генерации + JSON-repair
│   │   ├── nvidia_client.py     # Обёртка над OpenAI-SDK c NVIDIA NIM endpoint
│   │   ├── telegram_publisher.py# Публикация в канал (текст / фото / group)
│   │   ├── storage.py           # Сохранение загружаемых файлов
│   │   ├── scheduler.py         # APScheduler-джоба process_due_publications
│   │   └── bot.py               # aiogram polling bot (/start + сохранение
│   │                            # контекста)
│   ├── tests/                   # pytest: API, agent, scheduler, auth, bot
│   ├── data/                    # SQLite + загруженные вложения (gitignored)
│   └── pyproject.toml
└── frontend/
    ├── src/
    │   ├── main.tsx             # bootstrap React, инициализация WebApp SDK
    │   ├── App.tsx              # Единый экран Mini App
    │   ├── api.ts               # fetch-обёртка над /api/* + 401-debug
    │   ├── telegram.ts          # Заголовки initData + получение пользователя
    │   ├── types.ts             # Типы доменных сущностей
    │   └── styles.css
    ├── index.html               # подключает telegram-web-app.js
    ├── vite.config.ts           # дев-прокси, VITE_ALLOWED_HOSTS из env
    └── package.json

Доменная модель

Сущность Назначение
Channel Куда публикуем. telegram_chat_id@username или числовой chat id.
ContextItem Любой материал для контекста: manual, forwarded, link, db_post.
Draft Запрос на пост: тема, ноты, целевая длина/тон/аудитория, выбранные context ids.
DraftMedia Файлы, прикрепленные к черновику (картинки идут в LLM как image_url data-URL).
GeneratedVariant Один из трёх результатов LangGraph-агента, плюс review-комментарии.
Publication Запланированная или уже опубликованная публикация (есть status и error).

Связи: Draft → Channel, Draft → DraftMedia (1:N), Draft → GeneratedVariant (1:N, approved_variant_id указывает на выбранный), Draft → Publication (1:N, обычно 1).

Статусы черновика: draft → generating → generated → approved | scheduled → published, плюс терминалы cancelled и failed.


LangGraph-агент

Файл: backend/app/agent.py. Граф собирается в PostDraftAgent._build_graph() и состоит из четырёх узлов:

  1. retrieve_context — забирает выбранные ContextItem по id и склеивает в один строковый блок для промпта.
  2. summarize_images — для каждого вложения c mime_type image/* отправляет в NIM одно vision-сообщение и собирает короткое текстовое описание.
  3. generate_variants — рассылает в NIM длинный промпт-шаблон (_GENERATION_PROMPT_TEMPLATE) и ждёт строгий JSON с тремя вариантами. Ответ проходит через цепочку JSON-repair: снять markdown-fence → отрезать до {...} → заэкранировать управляющие символы внутри строк → если всё плохо, поштучно восстановить целые объекты из массива variants.
  4. review_variants — отдельный запрос в NIM с задачей вычитать варианты. Результат прикладывается к каждой записи GeneratedVariant.review_notes.

После графа _save_variants удаляет старые варианты у этого черновика, сохраняет новые и переводит Draft.status в generated.

JSON-repair специально вынесен набор маленьких чистых функций (_strip_markdown_json_fence, _json_bounds, _escape_control_chars_inside_json_strings, _extract_json, _extract_complete_variant_objects, _parse_variants_response, _normalize_variants) — каждая отдельно покрыта unit-тестами в tests/test_agent.py.


Авторизация

require_owner в backend/app/auth.py — единая зависимость, которая повешена на роутер /api/* (backend/app/api.py:32). Логика:

  1. Если OWNER_TELEGRAM_ID не задан — дев-режим, всех пускаем.
  2. Если в заголовке x-telegram-user-id лежит число, совпадающее с владельцем, — пускаем (использование для отладки из браузера).
  3. Если есть Telegram-WebApp initData (заголовок x-telegram-init-data или Authorization: tma <init-data>), валидируем подпись HMAC-SHA-256 с секретом из TELEGRAM_BOT_TOKEN и сверяем user.id с владельцем.
  4. Иначе — 401 с понятным сообщением.

Для отладки на проде есть открытый эндпоинт /api/auth/debug — возвращает снимок того, какие именно проверки прошли/не прошли (без секретов).


REST API

Все эндпоинты префиксированы /api. Авторизация — описана выше.

Метод Путь Что делает
GET /api/channels Список каналов.
POST /api/channels Добавить канал.
GET /api/context?kind= Список контекст-карточек (последние 100).
POST /api/context Создать контекст-карточку вручную (или из ссылки).
GET /api/drafts Последние 50 черновиков с вариантами и медиа.
POST /api/drafts Создать черновик (без генерации).
GET /api/drafts/{id} Детально один черновик.
POST /api/drafts/{id}/media Загрузить фото к черновику (multipart).
POST /api/drafts/{id}/generate Запустить LangGraph-агента → три варианта.
POST /api/drafts/{id}/approve Подтвердить вариант ± задать scheduled_at.
POST /api/drafts/{id}/publish-now Опубликовать утверждённый вариант немедленно.
POST /api/drafts/{id}/cancel Отменить черновик и связанные публикации.
GET /api/auth/debug Диагностика авторизации (без 401).
GET /health Проверка живости.

Публикация и расписание

telegram_publisher.py умеет отправлять:

  • только текст;
  • одно фото с подписью (если длина ≤ 1024 символов) либо фото + отдельное сообщение с текстом;
  • группу до 10 фото (media group) + отдельное текстовое сообщение.

scheduler.py запускает APScheduler с одним interval-джобом (SCHEDULER_INTERVAL_SECONDS, по умолчанию 30с). Каждое срабатывание process_due_publications берёт все Publication.status == "scheduled" AND scheduled_at <= now() и публикует их через тот же publish_draft_now.

Заметка: время хранится наивно (без таймзоны). Это сознательное упрощение для локального MVP, в проде стоит перейти на UTC-aware datetime.


Бот

bot.py — отдельный процесс aiogram polling.

  • /start для владельца показывает inline-кнопку с WebAppInfo, ведущим на PUBLIC_WEBAPP_URL.
  • Любое сообщение от владельца (текст, подпись, фото, документ) сохраняется в context_items со ссылкой на исходное telegram message id и метадатой.
  • Любой channel_post логируется (полезно, чтобы быстро узнать numeric chat_id канала, в котором бот сидит админом).

Запуск

Бэкенд

cd backend
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
cp ../.env.example .env       # затем заполнить значения
uvicorn app.main:app --reload

Обязательные переменные для боевого запуска:

Переменная Зачем
NVIDIA_API_KEY Ключ NVIDIA NIM (нужен для генерации).
TELEGRAM_BOT_TOKEN Токен бота для авторизации Mini App и публикации.
OWNER_TELEGRAM_ID Числовой Telegram id владельца — единственный авторизованный.
PUBLIC_WEBAPP_URL HTTPS-URL, по которому Telegram откроет Mini App в WebView.

Дополнительно:

Переменная По умолчанию
NVIDIA_BASE_URL https://integrate.api.nvidia.com/v1
NVIDIA_MODEL mistralai/mistral-large-3-675b-instruct-2512
DATABASE_URL sqlite:///./data/telegram_post_writer.db
ATTACHMENTS_DIR ./data/attachments
SCHEDULER_ENABLED true
SCHEDULER_INTERVAL_SECONDS 30

Бот

cd backend
source .venv/bin/activate
python -m app.bot

Бот должен быть админом в каждом канале, куда планируется публиковать.

Фронтенд

cd frontend
npm install
npm run dev      # http://localhost:5173

Локальная разработка без Telegram-клиента: оставьте OWNER_TELEGRAM_ID пустым в .env, либо задайте в браузере localStorage["telegram-post-writer:user-id"] равным реальному OWNER_TELEGRAM_ID — этого хватит для прохождения require_owner.

Чтобы открыть как настоящий Mini App, прокиньте фронт через HTTPS-тоннель (cloudflared / ngrok), укажите этот хост в VITE_ALLOWED_HOSTS (через запятую) и поставьте PUBLIC_WEBAPP_URL равным той же ссылке.

Production-сборка фронта внутри бэкенда

cd frontend && npm run build      # → frontend/dist/
cd ../backend && uvicorn app.main:app

main.py при старте проверяет frontend_dist_dir; если папка есть, статика монтируется на /assets, а на все остальные пути отдаётся index.html (SPA fallback). Полезно для деплоя одним процессом.


Тесты

cd backend
source .venv/bin/activate
pytest              # API + agent + scheduler + auth + bot
cd frontend
npm run build       # tsc + vite build, без отдельного теста-сьюита

В тестах:

  • tests/conftest.py поднимает свежий SQLite в tmp_path и подменяет get_chat_client на FakeChatClient, который возвращает предсказуемый JSON с тремя вариантами и фейковое summary для изображений.
  • tests/test_agent.py отдельно покрывает JSON-repair: фенсированный JSON c сырыми переводами строк и обрыв ответа посреди массива.
  • tests/test_scheduler.py гоняет process_due_publications с FakePublisher и проверяет, что просроченные Publication действительно переходят в published.
  • tests/test_auth.py подписывает фейковый initData и убеждается, что Mini App auth работает.

Ограничения v1

  • Нет вычитки реальной истории канала по ссылке — только то, что владелец переслал боту или вручную ввёл как контекст.
  • scheduled_at хранится без таймзоны; интерпретируется как локальное время на сервере.
  • Один пользователь = один владелец. Многопользовательский режим вне скоупа.
  • Расписание делается через APScheduler в том же процессе uvicorn — при падении процесса джобы не выполнятся, пока бэкенд не поднимется.

Лицензия

См. LICENSE.

About

This app can help you write posts for your channel.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors