Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

telegrambot4j

Built for my own project and use, polished and open sourced. Use at your own risk - but it works fine for me.

Lightweight wrapper over Telegram Bot API for Java. Pure POJO - works standalone or in Spring Boot. Provides production-ready HTTP client with connection pooling, retry logic, and handler chain pattern for processing updates. No framework dependencies - only OkHttp, Jackson, and Lombok.

Features

  • Handler chain pattern - UpdateHandler returns boolean, dispatcher stops on first true
  • Production-ready HTTP client - OkHttp with connection pooling, configurable timeouts, keep-alive, exponential backoff retry, and honors Telegram's retry_after on 429 flood control instead of guessing
  • Bot-blocked detection - Update.myChatMember delivers my_chat_member updates, so you know the moment a user blocks or unblocks the bot - no need to wait for a failed sendMessage
  • Auto-adjusted polling timeout - OkHttp readTimeout automatically adjusted per-request to prevent connection drops during long polling
  • Inline keyboard support - InlineKeyboardMarkup with callback buttons and URL buttons
  • Callback query handling - CallbackQuery model + answerCallbackQuery method
  • Pure POJO - no Spring dependencies in core, but Spring-friendly
  • Long polling service - automatic offset management, immediate shutdown via thread interruption
  • Fluent registration API - HandlersRegistry for convenient handler registration
  • Auto-discovery - scanAndRegister(packageName) finds handlers via reflection

Maven Dependency

<dependency>
    <groupId>tech.nomad4</groupId>
    <artifactId>telegrambot4j</artifactId>
    <version>1.2.0</version>
</dependency>

Add GitHub Packages repository to your pom.xml:

<repositories>
    <repository>
        <id>github</id>
        <url>https://maven.pkg.github.com/nomad4tech/telegrambot4j</url>
    </repository>
</repositories>

See nomad4tech/telegrambot4j-demo for working Spring Boot bot built with this library.

Quick Start

public class MyBot {
    public static void main(String[] args) {
        String botToken = System.getenv("BOT_TOKEN");

        TelegramApiClient apiClient = new TelegramApiClient(botToken);

        UpdateHandler startHandler = update -> {
            if (update.getMessage() != null && "/start".equals(update.getMessage().getText())) {
                try {
                    apiClient.sendMessage(update.getMessage().getChat().getId(), "Hello!");
                    return true; // handled - stop the chain
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return false; // not my update - pass to next handler
        };

        UpdateDispatcher dispatcher = new UpdateDispatcher(startHandler);

        new TelegramBotPollingService(apiClient, dispatcher, true); // autoStart = true
    }
}

Handler Example

public class StartCommandHandler implements UpdateHandler {

    private final TelegramApiClient apiClient;

    public StartCommandHandler(TelegramApiClient apiClient) {
        this.apiClient = apiClient;
    }

    @Override
    public boolean handle(Update update) {
        if (update.getMessage() == null || update.getMessage().getText() == null) {
            return false;
        }

        if ("/start".equals(update.getMessage().getText())) {
            try {
                apiClient.sendMessage(
                    update.getMessage().getChat().getId(),
                    "Welcome! Use /help for available commands."
                );
                return true; // handled
            } catch (IOException e) {
                // log error
            }
        }

        return false; // not my command
    }
}

Inline Keyboards

Send messages with inline buttons:

// Single callback button
InlineKeyboardButton button = InlineKeyboardButton.callback("Click me", "my_callback_data");

// URL button
InlineKeyboardButton urlButton = InlineKeyboardButton.url("Open link", "https://example.com");

// Single column keyboard (each button on its own row)
InlineKeyboardMarkup keyboard = InlineKeyboardMarkup.singleColumn(List.of(
    InlineKeyboardButton.callback("Option 1", "option_1"),
    InlineKeyboardButton.callback("Option 2", "option_2"),
    InlineKeyboardButton.callback("❌ Delete", "delete_item")
));

// Custom layout (rows defined manually)
InlineKeyboardMarkup keyboard = InlineKeyboardMarkup.of(List.of(
    List.of(
        InlineKeyboardButton.callback("Yes", "confirm_yes"),
        InlineKeyboardButton.callback("No", "confirm_no")
    ),
    List.of(
        InlineKeyboardButton.url("Learn more", "https://example.com")
    )
));

// Send message with keyboard
apiClient.sendMessage(chatId, "Choose an option:", keyboard);

Handling Callback Queries

When a user presses an inline button, a CallbackQuery arrives in the update:

public class MyCallbackHandler implements UpdateHandler {

    private final TelegramApiClient apiClient;

    public MyCallbackHandler(TelegramApiClient apiClient) {
        this.apiClient = apiClient;
    }

    @Override
    public boolean handle(Update update) {
        if (update.getCallbackQuery() == null) {
            return false;
        }

        CallbackQuery callback = update.getCallbackQuery();
        String data = callback.getData();
        Long chatId = callback.getMessage().getChat().getId();
        Long userId = callback.getFrom().getId();

        try {
            if ("delete_item".equals(data)) {
                // handle deletion
                apiClient.answerCallbackQuery(callback.getId(), "Deleted!", false);
            }
        } catch (IOException e) {
            // log error
        }

        return true;
    }
}

Always call answerCallbackQuery after handling — otherwise the loading spinner stays on the button.

Detecting Bot Blocked/Unblocked

Telegram sends a my_chat_member update whenever the bot's own status in a chat changes — including a user blocking or unblocking the bot in a private chat. This arrives on its own, with no extra opt-in needed and no failed sendMessage required to find out:

public class BlockedStatusHandler implements UpdateHandler {

    @Override
    public boolean handle(Update update) {
        if (update.getMyChatMember() == null) {
            return false;
        }

        ChatMemberUpdated change = update.getMyChatMember();
        Long userId = change.getChat().getId();
        String newStatus = change.getNewChatMember().getStatus();

        if ("kicked".equals(newStatus)) {
            // user blocked the bot - stop sending, mark them inactive
        } else if ("member".equals(newStatus)) {
            // user (re)started the bot / unblocked it
        }

        return true;
    }
}

How Dispatching Works

UpdateDispatcher maintains a list of UpdateHandler instances and invokes them sequentially. Each handler returns boolean: true means "I processed this update, stop the chain", false means "not mine, try next". This implements the Chain of Responsibility pattern - each update is processed by exactly one handler. The dispatcher itself implements UpdateHandler, so dispatchers can be nested.

Registering Handlers

// Via constructor
UpdateDispatcher dispatcher = new UpdateDispatcher(handler1, handler2, handler3);

// Via registry (fluent API)
dispatcher.getRegistry()
    .register(handler1)
    .register(handler2, handler3)
    .scanAndRegister("com.mybot.handlers"); // auto-discovery via reflection

Note: scanAndRegister works with filesystem classpath only. It will not work inside a fat JAR (e.g. Spring Boot uber-jar). Use explicit registration in that case.

Spring Boot Integration

@Component
public class StartCommandHandler implements UpdateHandler {

    private final TelegramApiClient apiClient;

    public StartCommandHandler(TelegramApiClient apiClient) {
        this.apiClient = apiClient;
    }

    @Override
    public boolean handle(Update update) {
        // ...
        return false;
    }
}

@Configuration
public class BotConfig {

    @Bean
    public TelegramApiClient apiClient(@Value("${bot.token}") String token) {
        return new TelegramApiClient(token);
    }

    @Bean
    public UpdateDispatcher dispatcher(List<UpdateHandler> handlers) {
        return new UpdateDispatcher(handlers); // Spring injects all UpdateHandler beans
    }

    @Bean
    public TelegramBotPollingService pollingService(TelegramApiClient client, UpdateDispatcher dispatcher) {
        return new TelegramBotPollingService(client, dispatcher, true);
    }
}

application.properties:

telegram.bot.token=${BOT_TOKEN}
spring.main.keep-alive=true

Important: spring.main.keep-alive=true prevents Spring Boot from shutting down after startup. Without it, the application will exit immediately even though the polling thread is running.

Configuration

TelegramApiConfig config = TelegramApiConfig.builder()
    .maxIdleConnections(10)
    .connectTimeout(Duration.ofSeconds(15))
    .readTimeout(Duration.ofSeconds(60))
    .writeTimeout(Duration.ofSeconds(60))
    .keepAlive(Duration.ofSeconds(60))
    .maxRetryAttempts(5)
    .build();

TelegramApiClient apiClient = new TelegramApiClient(botToken, config);

Defaults: 5 idle connections in OkHttp ConnectionPool, 30s keep-alive, 10s connect timeout, 30s read/write timeout, 3 retry attempts with exponential backoff starting at 500ms.

Note on readTimeout: For long polling (getUpdates), the library automatically adjusts OkHttp's readTimeout per-request to ensure it's always greater than the polling timeout. This prevents premature connection drops.

Or provide fully custom OkHttpClient:

OkHttpClient customClient = new OkHttpClient.Builder()
    .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.host", 8080)))
    .connectTimeout(15, TimeUnit.SECONDS)
    .build();

TelegramApiClient apiClient = new TelegramApiClient(botToken, customClient);

API Reference

TelegramApiClient methods

Method Description
sendMessage(chatId, text) Send plain text message
sendMessage(chatId, text, keyboard) Send message with inline keyboard
sendMessage(chatId, text, parseMode, replyToMessageId, replyMarkup) Send message with all parameters
answerCallbackQuery(callbackQueryId, text, showAlert) Answer inline button press
getUpdates(offset, limit, timeout) Get updates via long polling
getMe() Get bot info
executeMethod(method, params, responseType) Execute any Telegram API method

Models

Class Description
Update Incoming update (message, callback_query, etc.)
Message Text message with sender, chat, date
CallbackQuery Inline button press event
InlineKeyboardMarkup Inline keyboard layout
InlineKeyboardButton Single inline button (callback or URL)
Chat Chat info (id, type, title)
User User info (id, username, name)
ChatMemberUpdated Delivered as Update.myChatMember - a change in the bot's own chat status
ChatMember status (member, kicked, ...) + user for one side of a ChatMemberUpdated

Known limitations

  • retry_after wait is synchronous - when Telegram returns 429 with a retry_after, sendMessage/answerCallbackQuery/getMe block the calling thread via Thread.sleep for that long before retrying (up to maxRetryAttempts times). Fine for a scheduled job or a background poller thread; if you call these from a request thread, a large retry_after will hold that thread for the whole wait.

License

MIT - see LICENSE in repository root.

About

Lightweight Java library for Telegram bots - handler chain, OkHttp client, Spring Boot ready

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages