Это вторая статья из цикла о разработке T2M Bridge - бота для кросспостинга между Telegram-каналами и MAX-каналами.

В цикле планируются четыре части:

  1. T2M Bridge, часть 1: как я написал Java-библиотеку для MAX Bot API с помощью Codex

  2. T2M Bridge, часть 2: как я написал Java-библиотеку для Telegram Bot API вместе с Codex - уже примерно 50/50

  3. T2M Bridge, часть 3: как я своими силами собрал бота для кросспостинга между Telegram и MAX

  4. T2M Bridge, часть 4: как я выкатывал бота в прод и почему всё оказалось сложнее Docker Compose

В первой статье я рассказывал про maxlib - Java-библиотеку для MAX Bot API.

Там Codex сделал большую часть первичного каркаса. Это ускорило разработку, но привело к ожидаемой проблеме: ИИ периодически придумывал поля, объекты и структуры, которых не было в документации MAX Bot API. В итоге значительная часть времени ушла не на Java как таковую, а на сверку моделей с реальным API.

После этого я начал делать Telegram-часть для T2M Bridge уже аккуратнее.

Так появилась telegalib - Java-библиотека для разработки Telegram-ботов.

Репозиторий:

https://github.com/tardyon-soft/telegalib

В этой статье разберу, зачем мне понадобилась своя библиотека поверх Telegram Bot API, как она устроена, чем отличается подход от maxlib и как с её помощью можно написать бота на Java.

Зачем писать свою библиотеку для Telegram

В отличие от MAX, для Telegram уже есть готовые Java-библиотеки.

Поэтому вопрос логичный:

Зачем писать ещё одну?

Причина была не в том, что существующие решения плохие.

Мне нужен был одинаковый подход для двух платформ: Telegram и MAX. T2M Bridge должен был не просто отправлять сообщения, а вести пользователя по сценариям:

  • связать профиль Telegram с профилем MAX;

  • показать список каналов;

  • проверить, что бот добавлен администратором;

  • связать Telegram-канал и MAX-канал;

  • выбрать направление кросспостинга;

  • дальше обрабатывать события из двух мессенджеров в похожей модели.

Если для MAX у меня есть runtime с routing, FSM, screens и middleware, а для Telegram - совершенно другой стиль разработки, то бизнес-логика бота начинает расползаться.

Поэтому я решил сделать telegalib не просто как thin-wrapper над Telegram Bot API, а как библиотеку с похожими идеями:

  • typed client;

  • long polling и webhook;

  • dispatcher;

  • router;

  • filters;

  • middleware;

  • FSM;

  • screen API;

  • Spring Boot starter;

  • testkit;

  • demo-модули.

То есть задача была не «написать идеальную библиотеку для всех Telegram-ботов», а получить контролируемый runtime для своего проекта и при этом оформить его как отдельную переиспользуемую библиотеку.

Почему подход с Codex изменился

После maxlib стало понятно: если дать ИИ слишком широкую задачу, он быстро построит красивую архитектуру, но может начать фантазировать.

С Telegram ситуация была одновременно проще и сложнее.

Проще - потому что Telegram Bot API хорошо известен, вокруг него больше примеров, а часть сущностей давно устоялась.

Сложнее - потому что сам API большой. Там много типов update’ов, методов, вложенных объектов, вариантов медиа, callback’ов, платежей, business-сценариев и других деталей.

Поэтому во второй библиотеке я старался работать с Codex иначе.

Если для maxlib формат был ближе к:

Сгенерируй каркас библиотеки, а я потом поправлю.

То для telegalib подход стал ближе к 50/50:

  • архитектурные решения и границы модулей - вручную;

  • публичный API - вручную или с жесткой проверкой;

  • однотипный код - через Codex;

  • тесты, README и demo - частично через Codex;

  • спорные места - только после ручной проверки;

  • модели Telegram Bot API - аккуратно, без доверия к «правдоподобному» результату.

Главный вывод после первой библиотеки был простой:

ИИ можно использовать как ускоритель, но нельзя отдавать ему право решать, как на самом деле устроен внешний API.

Что получилось

telegalib - это многомодульная Java-библиотека для разработки Telegram-ботов.

Репозиторий собран на Java 21 и Gradle.

Основные публикуемые артефакты:

implementation("ru.tardyon.botframework:telegram-bot-framework-core:<version>")
implementation("ru.tardyon.botframework:telegram-bot-framework-spring-boot-starter:<version>")

В репозитории есть несколько модулей:

  • telegram-bot-framework-core -основной runtime и client API;

  • telegram-bot-framework-spring-boot-starter - интеграция со Spring Boot;

  • telegram-bot-framework-testkit - тестовые утилиты;

  • telegram-bot-framework-demo - demo-приложение;

  • telegram-bot-framework-screen-demo - demo для screen API;

  • telegram-bot-framework-botapi-generator - tooling для генерации частей Bot API.

Основная библиотека закрывает несколько уровней:

  • TelegramApiClient для прямой работы с Telegram Bot API;

  • runtime для long polling и webhook;

  • Router, filters и middleware;

  • FSM с StateStorage;

  • screen API со стеком экранов;

  • Spring Boot starter с auto-configuration;

  • testkit для интеграционных тестов.

Vanilla Java: минимальный бот без Spring

Если не нужен Spring Boot, можно использовать только core.

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

repositories {
    mavenCentral()
}

dependencies {
    implementation("ru.tardyon.botframework:telegram-bot-framework-core:<version>")
}

Минимальный пример long polling:

import ru.tardyon.botframework.telegram.api.DefaultTelegramApiClient;
import ru.tardyon.botframework.telegram.api.TelegramApiClient;
import ru.tardyon.botframework.telegram.bot.DefaultTelegramBot;
import ru.tardyon.botframework.telegram.bot.TelegramBot;
import ru.tardyon.botframework.telegram.dispatcher.DefaultDispatcher;
import ru.tardyon.botframework.telegram.dispatcher.Router;
import ru.tardyon.botframework.telegram.dispatcher.filter.Filters;
import ru.tardyon.botframework.telegram.polling.LongPollingOptions;
import ru.tardyon.botframework.telegram.polling.LongPollingRunner;

public class VanillaBotMain {

    public static void main(String[] args) {
        TelegramApiClient client = new DefaultTelegramApiClient(System.getenv("BOT_TOKEN"));

        Router router = new Router();

        router.message(Filters.command("start"),
                (ctx, msg) -> ctx.telegramMessage().reply("Привет"));

        router.message(Filters.textEquals("ping"),
                (ctx, msg) -> ctx.telegramMessage().reply("pong"));

        router.callbackQuery(Filters.callbackDataStartsWith("menu:"),
                (ctx, cbq) -> ctx.telegramCallbackQuery().answer("OK"));

        LongPollingRunner pollingRunner = new LongPollingRunner(
                client,
                LongPollingOptions.defaults()
        );

        TelegramBot bot = new DefaultTelegramBot(
                pollingRunner,
                new DefaultDispatcher(router)
        );

        bot.startPolling();
    }
}

Здесь вручную собираются основные компоненты:

  • TelegramApiClient;

  • Router;

  • DefaultDispatcher;

  • LongPollingRunner;

  • TelegramBot.

Для небольшого бота этого достаточно.

Но в T2M Bridge мне удобнее Spring Boot, потому что остальная часть сервиса тоже строится вокруг Spring-экосистемы.

Spring Boot starter

Для Spring Boot подключается отдельный starter:

repositories {
    mavenCentral()
}

dependencies {
    implementation("ru.tardyon.botframework:telegram-bot-framework-spring-boot-starter:<version>")
}

Минимальная конфигурация polling:

telegram:
  bot:
    token: ${BOT_TOKEN}
    mode: polling
    transport:
      mode: cloud
    polling:
      enabled: true
      timeout: 30
      limit: 100

После этого можно описать обработчики через аннотации:

import ru.tardyon.botframework.telegram.bot.TelegramCallbackQuery;
import ru.tardyon.botframework.telegram.bot.TelegramMessage;
import ru.tardyon.botframework.telegram.spring.boot.annotation.BotController;
import ru.tardyon.botframework.telegram.spring.boot.annotation.OnCallbackQuery;
import ru.tardyon.botframework.telegram.spring.boot.annotation.OnMessage;

@BotController
public class MyBotController {

    @OnMessage(command = "start")
    public void onStart(TelegramMessage message) {
        message.reply("Привет");
    }

    @OnMessage(textEquals = "ping")
    public void onPing(TelegramMessage message) {
        message.reply("pong");
    }

    @OnCallbackQuery(callbackPrefix = "menu:")
    public void onMenu(TelegramCallbackQuery callback) {
        callback.answer("OK");
    }
}

Starter поднимает основные runtime-компоненты:

  • TelegramApiClient;

  • transport profile;

  • LongPollingOptions;

  • LongPollingRunner;

  • Router;

  • Dispatcher;

  • TelegramBot;

  • lifecycle;

  • webhook processor;

  • webhook controller;

  • StateStorage;

  • ScreenStateStorage;

  • ScreenRegistry;

  • ScreenEngine;

  • ScreenMiddleware.

Это как раз тот уровень, который мне был нужен для T2M Bridge: не собирать runtime руками в каждом сервисе, а описывать сценарии бота через контроллеры и обработчики.

Webhook mode

Для production обычно удобнее webhook, если уже есть внешний URL, TLS и reverse proxy.

Конфигурация:

telegram:
  bot:
    token: ${BOT_TOKEN}
    mode: webhook
    polling:
      enabled: false
    webhook:
      enabled: true
      path: /telegram/webhook
      public-url: ${BOT_WEBHOOK_PUBLIC_URL}
      secret-token: ${BOT_WEBHOOK_SECRET_TOKEN:}
      drop-pending-updates: true

Если задан webhook.public-url, starter при старте приложения вызывает setWebhook.

Для локальной разработки long polling проще: не нужно поднимать публичный endpoint, думать про TLS и пробрасывать локальный порт наружу.

В T2M Bridge это удобно разделяется по окружениям: локально можно запускать polling, а в production использовать webhook.

Proxy

В starter есть поддержка HTTP и SOCKS5 proxy:

telegram:
  bot:
    proxy:
      enabled: true
      type: socks5
      host: 127.0.0.1
      port: 1080
      username: ${PROXY_USER:}
      password: ${PROXY_PASSWORD:}

Для Telegram-ботов это практичная вещь.

В локальной разработке или на отдельных серверах может понадобиться явно управлять сетевым маршрутом к Telegram API.

Я не хотел зашивать это руками в прикладной код, поэтому proxy-настройки вынесены на уровень конфигурации.

Router и filters

Если не использовать аннотации, маршруты можно регистрировать вручную через Router.

Router поддерживает разные группы update-событий:

  • message;

  • callbackQuery;

  • inlineQuery;

  • chosenInlineResult;

  • myChatMember;

  • chatMember;

  • shippingQuery;

  • preCheckoutQuery;

  • businessConnection;

  • businessMessage;

  • editedBusinessMessage;

  • deletedBusinessMessages.

Пример:

Router router = new Router();

router.message(Filters.command("start"),
        (ctx, msg) -> ctx.telegramMessage().reply("Привет"));

router.message(Filters.textEquals("ping"),
        (ctx, msg) -> ctx.telegramMessage().reply("pong"));

router.callbackQuery(Filters.callbackDataStartsWith("menu:"),
        (ctx, cbq) -> ctx.telegramCallbackQuery().answer("OK"));

В Filters есть готовые предикаты:

  • command(...);

  • commands(...);

  • textPresent();

  • textEquals(...);

  • textStartsWith(...);

  • privateChat();

  • groupChat();

  • supergroupChat();

  • channelChat();

  • fromUser(...);

  • fromChat(...);

  • callbackDataEquals(...);

  • callbackDataStartsWith(...);

  • invoicePayloadEquals(...);

  • preCheckoutPayloadEquals(...);

  • stateEquals(...);

  • inStates(...);

  • noState().

Этого достаточно, чтобы покрыть большую часть типичных сценариев.

В T2M Bridge фильтры нужны для разделения пользовательских действий:

  • команда /start;

  • callback’и экранов;

  • ввод кода связывания;

  • выбор канала;

  • настройка направления кросспостинга.

UpdateContext

В обработчики передаётся UpdateContext.

Он содержит:

  • текущий update;

  • TelegramApiClient;

  • StateStorage;

  • bot id;

  • runtime attributes.

Через него доступны удобные обёртки:

  • ctx.telegramMessage();

  • ctx.telegramCallbackQuery().

Например:

router.message(Filters.command("start"), (ctx, msg) -> {
    ctx.telegramMessage().reply("Привет");
});

Или callback:

router.callbackQuery(Filters.callbackDataStartsWith("menu:"), (ctx, cbq) -> {
    ctx.telegramCallbackQuery().answer("OK");
});

Я специально хотел, чтобы обработчик работал не с голым JSON update, а с контекстом сценария.

Когда бот растёт, прямое ковыряние update’ов быстро делает код шумным.

Middleware

DefaultDispatcher может применять цепочку middleware.

Например:

import java.util.List;
import ru.tardyon.botframework.telegram.dispatcher.DefaultDispatcher;
import ru.tardyon.botframework.telegram.dispatcher.middleware.ErrorBoundaryUpdateMiddleware;
import ru.tardyon.botframework.telegram.dispatcher.middleware.LoggingUpdateMiddleware;

DefaultDispatcher dispatcher = new DefaultDispatcher(
        router,
        List.of(
                new ErrorBoundaryUpdateMiddleware(),
                new LoggingUpdateMiddleware()
        )
);

Если нужна своя логика, можно реализовать UpdateMiddleware.

Для T2M Bridge middleware полезны для сквозных задач:

  • логирование входящих update’ов;

  • обработка ошибок;

  • загрузка текущего пользователя;

  • проверка связанного профиля;

  • enrichment контекста;

  • метрики.

Это лучше, чем размазывать одинаковые проверки по каждому обработчику.

FSM: состояние пользователя

Связывание профилей Telegram и MAX - это не одно сообщение.

Пользователь может:

  1. открыть профиль;

  2. нажать «Связать профили»;

  3. запросить код;

  4. открыть второго бота;

  5. выбрать ввод кода;

  6. отправить код текстом.

Такой сценарий требует состояния.

В telegalib FSM строится вокруг:

  • State;

  • StateKey;

  • StateStorage;

  • InMemoryStateStorage.

Пример:

import ru.tardyon.botframework.telegram.dispatcher.Router;
import ru.tardyon.botframework.telegram.dispatcher.filter.Filters;
import ru.tardyon.botframework.telegram.fsm.State;

Router router = new Router();

router.message(Filters.command("startform"), (ctx, msg) -> {
    ctx.state().setState(State.of("form.awaiting_name"));
    ctx.telegramMessage().reply("Введите имя");
});

router.message(Filters.stateEquals("form.awaiting_name"), (ctx, msg) -> {
    ctx.state().putData("name", msg.text());
    ctx.state().setState(State.of("form.awaiting_language"));
    ctx.telegramMessage().reply("Введите язык");
});

router.message(Filters.stateEquals("form.awaiting_language"), (ctx, msg) -> {
    Object name = ctx.state().getData("name").orElse("unknown");
    ctx.state().clear();
    ctx.telegramMessage().reply("Имя: " + name + ", язык: " + msg.text());
});

Для локальной разработки можно использовать in-memory storage.

Для production-сценариев состояние лучше хранить во внешнем storage, чтобы оно переживало рестарт приложения.

В Spring Boot варианте можно включить Redis:

telegram:
  bot:
    state:
      storage: redis
      redis:
        key-prefix: telegram:fsm
        ttl-seconds: 86400

spring:
  data:
    redis:
      host: localhost
      port: 6379

Screen API: бот как набор экранов

Один из важных слоёв библиотеки - screen API.

Командный интерфейс удобен для разработчика, но не всегда удобен для пользователя.

Можно сделать так:

/start
/link
/channels
/settings

Но для обычного владельца канала лучше показать экран с кнопками:

  • профиль;

  • связать профили;

  • мои каналы;

  • настройки;

  • назад.

В telegalib screen API включает:

  • ScreenEngine;

  • ScreenRegistry;

  • Screen;

  • ScreenView;

  • ScreenAction;

  • ScreenNavigator;

  • ScreenStateStorage;

  • ScreenMiddleware.

Для Spring Boot есть аннотации:

  • @ScreenController;

  • @Screen;

  • @OnScreenMessage;

  • @OnScreenCallback.

Пример экрана:

import ru.tardyon.botframework.telegram.api.model.markup.Keyboards;
import ru.tardyon.botframework.telegram.screen.ScreenAction;
import ru.tardyon.botframework.telegram.screen.ScreenContext;
import ru.tardyon.botframework.telegram.screen.ScreenView;
import ru.tardyon.botframework.telegram.spring.boot.annotation.OnScreenCallback;
import ru.tardyon.botframework.telegram.spring.boot.annotation.Screen;
import ru.tardyon.botframework.telegram.spring.boot.annotation.ScreenController;

@ScreenController
public class SettingsScreenController {

    private static final String SETTINGS = "settings";
    private static final String TOGGLE = "screen:settings:toggle";

    @Screen(id = SETTINGS, main = true)
    public ScreenView settings(ScreenContext context) {
        boolean enabled = context.screenState()
                .getData("notifications")
                .map(Boolean.class::cast)
                .orElse(false);

        return ScreenView.builder()
                .line("Экран настроек")
                .line("notifications=" + enabled)
                .replyMarkup(
                        Keyboards.inlineKeyboard()
                                .row(Keyboards.callbackButton("Переключить", TOGGLE))
                                .build()
                )
                .build();
    }

    @OnScreenCallback(screen = SETTINGS, callbackEquals = TOGGLE)
    public ScreenAction toggle(ScreenContext context) {
        boolean enabled = context.screenState()
                .getData("notifications")
                .map(Boolean.class::cast)
                .orElse(false);

        context.screenState().putData("notifications", !enabled);

        return ScreenAction.render();
    }
}

ScreenAction поддерживает типовые действия:

  • handled();

  • render();

  • push(screenId);

  • push(screenId, targetData);

  • replace(screenId);

  • replace(screenId, targetData);

  • back();

  • clear();

  • unhandled().

Для T2M Bridge это один из ключевых слоёв.

Мне нужно было сделать не просто набор команд, а понятный интерфейс внутри бота:

  • профиль пользователя;

  • связывание профилей;

  • список каналов;

  • карточка канала;

  • выбор канала с другой платформы;

  • настройки кросспостинга.

Через screen API такой сценарий получается описывать ближе к обычному UI-flow, а не к набору разрозненных callback’ов.

Widgets

Когда интерфейс строится из экранов, быстро появляются повторяющиеся части.

Например:

  • главное меню;

  • список действий;

  • блок состояния;

  • кнопка возврата;

  • общий фрагмент настроек.

Для этого в библиотеке есть widgets.

В Spring Boot добавлены аннотации:

  • @WidgetController;

  • @Widget;

  • @OnWidgetAction.

Упрощённый пример:

import java.util.List;
import ru.tardyon.botframework.telegram.screen.ScreenAction;
import ru.tardyon.botframework.telegram.spring.boot.widget.OnWidgetAction;
import ru.tardyon.botframework.telegram.spring.boot.widget.Widget;
import ru.tardyon.botframework.telegram.spring.boot.widget.WidgetButtons;
import ru.tardyon.botframework.telegram.spring.boot.widget.WidgetController;
import ru.tardyon.botframework.telegram.spring.boot.widget.WidgetView;

@WidgetController
public class MenuWidgetController {

    record MenuItem(String label, String target) {
    }

    @Widget(id = "home_menu")
    public WidgetView homeMenu(List<MenuItem> items) {
        return WidgetView.builder()
                .line("Меню")
                .replyMarkup(
                        WidgetButtons.objectList(
                                "home_menu",
                                "open",
                                items,
                                MenuItem::label,
                                MenuItem::target
                        )
                )
                .build();
    }

    @OnWidgetAction(widget = "home_menu", action = "open")
    public ScreenAction open(String payload) {
        return ScreenAction.push(payload);
    }
}

Это не обязательная часть для простого бота.

Но если бот постепенно превращается в небольшой интерфейс с навигацией, widgets помогают не копировать одинаковые блоки между экранами.

Screen state storage

У экранов есть отдельное состояние.

Это важно, потому что FSM и состояние экрана - не одно и то же.

FSM отвечает за диалоговый сценарий: например, пользователь сейчас вводит код или название.

Screen state отвечает за состояние UI: выбранная вкладка, данные экрана, targetData, временные параметры отображения.

По умолчанию starter использует in-memory storage:

telegram:
  bot:
    screen-state:
      storage: memory

При необходимости можно включить Redis:

telegram:
  bot:
    screen-state:
      storage: redis
      redis:
        key-prefix: telegram:screen
        ttl-seconds: 86400

Для реального сервиса это полезно по той же причине, что и Redis для FSM: после рестарта приложения пользователь не должен внезапно потерять весь контекст.

Прямой вызов Telegram Bot API

Если routing-слой не нужен, можно работать через TelegramApiClient напрямую.

Пример:

import ru.tardyon.botframework.telegram.api.DefaultTelegramApiClient;
import ru.tardyon.botframework.telegram.api.TelegramApiClient;
import ru.tardyon.botframework.telegram.api.method.SendMessageRequest;

public class DirectApiExample {

    public static void main(String[] args) {
        TelegramApiClient client = new DefaultTelegramApiClient(System.getenv("BOT_TOKEN"));

        client.sendMessage(new SendMessageRequest(
                123456789L,
                "Привет",
                null
        ));
    }
}

В библиотеке реализованы разные группы Telegram Bot API:

  • базовые сообщения;

  • callback и inline API;

  • invoices;

  • shipping/pre-checkout;

  • web app query;

  • media group;

  • загрузка файлов;

  • chat/admin/member operations;

  • forum topics;

  • gifts;

  • Stars;

  • paid media;

  • business connection;

  • stories;

  • checklist.

В T2M Bridge прямой client API нужен для операций, которые не укладываются в простую обработку входящего update’а: например, отправка сообщений и медиа при кросспостинге.

Загрузка файлов

Для методов, которые принимают InputFile, доступны разные варианты:

  • InputFile.fileId(...);

  • InputFile.url(...);

  • InputFile.path(...);

  • InputFile.bytes(...);

  • InputFile.stream(...).

Это удобно для кросспостинга.

В одном сценарии файл уже может существовать в Telegram как file_id.

В другом - его нужно скачать, переложить, отправить как stream или bytes.

Для текстового бота это второстепенно. Для бота, который переносит посты между каналами, работа с медиа становится одной из центральных частей.

Testkit и demo-модули

Когда библиотека начинает отвечать не только за отправку сообщений, но и за routing, FSM, screens и middleware, ручного тестирования уже мало.

В репозитории есть telegram-bot-framework-testkit.

Он нужен, чтобы тестировать сценарии без реального Telegram API.

Также есть demo-модули:

  • telegram-bot-framework-demo;

  • telegram-bot-framework-screen-demo.

Первый показывает базовые сценарии, второй - screen API, push/back навигацию, screen state, widgets и работу с targetData.

Для библиотеки это важнее, чем может показаться.

README и unit-тесты могут быть зелёными, но только demo показывает, как API ощущается в реальном использовании.

Чем telegalib отличается от maxlib по процессу разработки

maxlib я писал в режиме «быстро получить рабочую основу для MAX».

Там Codex сделал очень много начального кода, а я потом достаточно долго вычищал галлюцинации: выдуманные поля, неверные объекты, лишние структуры и расхождения с документацией.

С telegalib подход был осторожнее.

Во-первых, я уже понимал, какие runtime-части мне нужны:

  • client;

  • dispatcher;

  • router;

  • filters;

  • middleware;

  • FSM;

  • screens;

  • Spring Boot starter;

  • testkit.

Во-вторых, я лучше разделял задачи для Codex.

Не:

Сделай библиотеку для Telegram Bot API.

А более конкретно:

Сделай обработчик для такого update-события.

Или:

Подготовь Spring Boot auto-configuration для уже существующих компонентов.

Или:

Напиши README для этого модуля по текущему коду.

Или:

Сгенерируй тесты для этого routing-сценария.

В-третьих, в Telegram-части я уже меньше доверял «правдоподобному» коду.

Если модель выглядит логично, это ещё не значит, что она соответствует Bot API.

Поэтому работа стала больше похожа на 50/50:

  • я задаю архитектуру;

  • Codex ускоряет однотипные участки;

  • я проверяю API и публичные контракты;

  • Codex помогает с тестами и документацией;

  • я принимаю финальное решение, что остаётся в библиотеке, а что удаляется.

Где ИИ помог

Codex хорошо справлялся с задачами, где контекст уже задан.

Например:

  • есть существующий интерфейс - нужно добавить реализацию;

  • есть runtime-компоненты - нужно собрать auto-configuration;

  • есть handler - нужно написать похожий handler;

  • есть тестовый сценарий - нужно добавить соседние cases;

  • есть код - нужно подготовить README;

  • есть screen API - нужно сделать demo-экран.

Такой режим работы был полезным.

ИИ не проектировал библиотеку вместо меня, но сокращал время на повторяющиеся операции.

Где ИИ всё равно мешал

Даже с Telegram API, где больше известных примеров, ИИ всё равно иногда пытался достроить недостающие детали сам.

Типичные проблемы:

  • добавляет поле, потому что «так обычно бывает»;

  • выбирает имя свойства по аналогии, а не по документации;

  • путает похожие update-типы;

  • смешивает старую и новую версии API;

  • пишет тест, который проверяет happy path, но не ловит ошибку контракта;

  • предлагает слишком обобщённую абстракцию там, где достаточно простого класса.

Поэтому правило осталось тем же:

Для внешнего API документация важнее сгенерированного кода.

Но по сравнению с maxlib я уже меньше разгребал последствия, потому что изначально давал Codex более узкие задачи.

Как telegalib используется в T2M Bridge

В T2M Bridge telegalib отвечает за Telegram-часть.

С её помощью бот:

  • принимает update’ы из Telegram;

  • обрабатывает команды и callback’и;

  • показывает пользователю экраны;

  • хранит состояние сценариев;

  • работает с каналами;

  • проверяет пользовательские действия;

  • отправляет сообщения и медиа;

  • участвует в связке Telegram-канала с MAX-каналом.

Например, когда пользователь открывает Telegram-бота, связывает профиль, добавляет бота в канал и выбирает пару каналов, значительная часть интерфейса проходит через Telegram screens и callback handlers.

Но сам кросспостинг я в этой статье подробно не разбираю.

Это тема третьей части цикла.

Там уже будет интереснее с точки зрения бизнес-логики:

  • как связать пользователя между двумя платформами;

  • как сопоставить каналы;

  • как хранить соответствия между публикациями;

  • как переносить медиа;

  • что делать с replies;

  • как обрабатывать редактирование и удаление;

  • где заканчивается библиотека и начинается логика продукта.

Что получилось удачно

Первое - единый runtime-подход.

Telegram-часть и MAX-часть T2M Bridge стали ближе по модели разработки. Это снижает когнитивную нагрузку: не нужно держать две полностью разные парадигмы для двух мессенджеров.

Второе - Spring Boot starter.

Для прикладного сервиса удобнее описывать обработчики и экраны, а не вручную собирать client, dispatcher, polling runner и storage.

Третье - screen API.

Для бота с настройками и навигацией это важнее, чем кажется. Команды подходят для developer-first интерфейса, но для владельца канала кнопки и экраны понятнее.

Четвёртое - testkit и demo.

Они помогают проверять не только отдельные методы, но и то, как библиотека используется в реальных сценариях.

Пятое - более дисциплинированная работа с Codex.

После maxlib я стал меньше просить ИИ «сделать всё» и чаще давать ему узкие задачи с понятными границами.

Что я бы сделал иначе

Если бы я начинал telegalib заново, я бы ещё раньше формализовал границу между тремя слоями:

  1. Bot API models and methods.

  2. Runtime: dispatcher, router, middleware, FSM.

  3. UI layer: screens, widgets, navigation.

Когда эти слои начинают смешиваться, библиотека быстро становится неудобной.

Ещё я бы раньше усилил contract-тесты для Bot API моделей.

Компилятор не скажет, что поле не соответствует реальному API. Он скажет только, что Java-код корректен.

Для библиотек вокруг внешних API этого мало.

Нужны тесты, которые защищают не только от синтаксических ошибок, но и от расхождения с контрактом.

Итоги

telegalib появилась не потому, что в Java-мире совсем нечем писать Telegram-ботов.

Она появилась потому, что для T2M Bridge мне нужен был единый подход к двум платформам: Telegram и MAX.

В текущем виде библиотека закрывает несколько задач:

  • прямую работу с Telegram Bot API через TelegramApiClient;

  • long polling и webhook;

  • routing update-событий;

  • filters;

  • middleware;

  • FSM;

  • screen API;

  • widgets;

  • Spring Boot auto-configuration;

  • Redis storage для state и screen state;

  • testkit и demo-модули.

Главное отличие от первой библиотеки - другой процесс разработки.

Если maxlib во многом начиналась как «быстро сгенерировать каркас и потом исправлять», то telegalib я писал уже осторожнее: примерно 50/50 между мной и Codex.

ИИ всё ещё помогал и экономил время, но архитектурные решения, публичный API и соответствие Telegram Bot API оставались под ручным контролем.

Основной вывод после двух библиотек такой:

Codex хорошо ускоряет разработку инфраструктурного кода, но чем ближе код к внешнему API, тем меньше ему можно доверять без проверки.

В следующей статье расскажу уже про сам T2M Bridge: как я связывал Telegram- и MAX-профили, как сопоставлял каналы, как переносил публикации и где начались проблемы, которые не видны на уровне отдельных библиотек.

Репозиторий telegalib:

https://github.com/tardyon-soft/telegalib

Живой MVP и документация T2M Bridge:

https://docs.t2m-bridge.ru

Новости разработки и roadmap:

https://t.me/telega2max