Skip to main content
Version: 0.0.7

API Reference

Complete reference for all easygram annotations, interfaces, model classes, and extension points.


Table of Contents

  • Structural Annotations@BotController, @BotControllerAdvice, @BotConfiguration, @BotMarkup, @BotOrder
  • Handler Routing Annotations@BotCommand, @BotText, @BotTextPattern, @BotCallbackQuery, @BotDynamicCallbackQuery, @BotContact, @BotLocation, @BotReplyButton, @BotEditedMessage, @BotInlineQuery, @BotMyChatMember, @BotChatMemberUpdate, @BotDefaultHandler, @BotExceptionHandler, … (+14 more update types)
  • Parameter Annotations@BotCommandValue, @BotTextValue, @BotCallbackQueryData, @BotCommandQueryParam, @BotInlineQueryValue, @BotChosenInlineResultId, @BotShippingPayload, @BotPreCheckoutPayload
  • Response Annotations@BotReplyMarkup, @BotClearMarkup, @BotParseMode
  • Chat State Annotations@BotChatState, @BotForwardChatState, @BotClearChatState
  • Return Types
  • Model ClassesBotRequest, BotResponse, BotMetadata, BotMarkupContext, BotDynamicCallbackData, ReplyOptions, SendReplyOptions
  • Dynamic Callbacks@BotDynamicCallbackQuery, BotDynamicCallbackData, BotDynamicCallbackQueryService
  • Extension InterfacesBotFilter, BotArgumentResolver, BotReturnTypeHandler, BotReplyAction, BotHandlerInvocationFilter, BotChatStateService, BotUpdatePublisher, BotHandlerConditionContributor, BotStartTrigger, BotHandler, BotHandlerCondition, BotInlineQueryMatcher, BotReplyButtonMatcher, BotMarkupRegistry, MarkupAware
  • Provider InterfacesEasygramTelegramClientProvider, EasygramOkHttpClientProvider, EasygramObjectMapperProvider, EasygramExecutorServiceProvider, EasygramTelegramUrlProvider, EasygramKafkaProducerFactoryProvider, EasygramKafkaConsumerFactoryProvider, EasygramRabbitConnectionFactoryProvider
  • Advanced SPIBotMetaDataResolver, BotMetaDataDefaultResolver, BotMetaDataSpecResolver, BotHandlerInvocationContext, BotHandlerException
  • i18n ServicesBotLocaleResolver, BotMessageSource, BotKeyboardFactory
  • ObservabilityBotHealthIndicator, BotInfoContributor, BotObservabilityFilter
  • Filter Order Constants
  • Invocation Filter Order Constants
  • Configuration Properties

Structural Annotations

@BotController

Package: uz.osoncode.easygram.core.annotation Target: TYPE

Marks a class as a bot controller. All handler method annotations (@BotCommand, @BotText, etc.) inside are scanned and registered at startup. Equivalent to Spring's @Component — auto-detected by component scanning.

AttributeTypeDefaultDescription
valueString""Optional bean name
@BotController
public class StartController {

@BotCommand("/start")
public String onStart(User user) {
return "Hello, " + user.getFirstName() + "!";
}
}

@BotControllerAdvice

Package: uz.osoncode.easygram.core.annotation Target: TYPE

Marks a class as a global exception handler. @BotExceptionHandler methods inside apply to all @BotController classes. Controller-local handlers always take priority over advice handlers for the same exception type.

AttributeTypeDefaultDescription
valueString""Optional bean name
basePackagesString[]{}Restrict to controllers in these packages
assignableTypesClass<?>[]{}Restrict to controllers assignable to these types
annotationsClass<? extends Annotation>[]{}Restrict to controllers annotated with these annotations
@BotControllerAdvice
public class GlobalExceptionHandler {

@BotExceptionHandler(IllegalArgumentException.class)
public String handleIllegalArg(IllegalArgumentException ex) {
return "Invalid input: " + ex.getMessage();
}
}

// Scope to a specific package
@BotControllerAdvice(basePackages = "com.example.bot.payment")
public class PaymentExceptionHandler {

@BotExceptionHandler(PaymentException.class)
public String handlePayment(PaymentException ex) {
return "Payment failed: " + ex.getMessage();
}
}

@BotConfiguration

Package: uz.osoncode.easygram.core.annotation Target: TYPE

Marks a class as a markup factory holder. Methods annotated with @BotMarkup inside are scanned and registered in BotMarkupRegistry at startup. Equivalent to Spring's @Component.

AttributeTypeDefaultDescription
valueString""Optional bean name
@BotConfiguration
public class MyMarkups {

@BotMarkup("main_menu")
public ReplyKeyboard mainMenu() {
return ReplyKeyboardMarkup.builder()
.keyboardRow(new KeyboardRow("Option 1", "Option 2"))
.resizeKeyboard(true)
.build();
}
}

@BotMarkup

Package: uz.osoncode.easygram.core.annotation Target: METHOD

Registers a method as a keyboard factory under a string ID in BotMarkupRegistry. The method must return ReplyKeyboard (or a subtype).

AttributeTypeRequiredDescription
valueStringRegistry key used with @BotReplyMarkup("id") and .withMarkup("id")

Supported method signatures:

  • () → ReplyKeyboard — static, no context
  • (BotRequest) → ReplyKeyboard — locale/user-aware
  • Any combination of parameters resolvable by the argument resolver system (User, Chat, Locale, BotRequest, etc.)
@BotConfiguration
public class Markups {

// Static keyboard
@BotMarkup("confirm_kb")
public ReplyKeyboard confirmKeyboard() {
return InlineKeyboardMarkup.builder()
.keyboardRow(List.of(
InlineKeyboardButton.builder().text(" Yes").callbackData("yes").build(),
InlineKeyboardButton.builder().text(" No").callbackData("no").build()
))
.build();
}

// Context-aware keyboard — receives params passed via PlainReply.withMarkup("id", Map)
@BotMarkup("product_kb")
public ReplyKeyboard productKeyboard(BotMarkupContext ctx) {
String productId = ctx.get("productId");
return InlineKeyboardMarkup.builder()
.keyboardRow(List.of(
InlineKeyboardButton.builder()
.text("Buy").callbackData("buy:" + productId).build()
))
.build();
}
}

@BotOrder

Package: uz.osoncode.easygram.core.annotation Target: METHOD

Controls handler execution priority within the same routing tier. Lower value = higher priority. State-specific handlers always beat state-agnostic ones at equal order.

AttributeTypeDefault
valueintInteger.MAX_VALUE
@BotCommand("/start")
@BotOrder(10)
public String highPriorityStart() { ... }

@BotCommand("/start")
@BotOrder(20)
public String lowPriorityStart() { ... }

Handler Routing Annotations

All routing annotations live in uz.osoncode.easygram.core.bind.annotation.

Injectable parameters for all handler methods: Update, User, Chat, TelegramClient, BotRequest, BotResponse, and any type provided by a registered BotArgumentResolver.


@BotCommand

Target: METHOD

Routes a command message (text starting with /). Empty value() matches any command not matched by a specific handler (same as @BotDefaultCommand).

AttributeTypeDefaultDescription
valueString[]{}Command strings (e.g. "/start"). Empty = any unmatched command
@BotCommand("/start")
public String onStart(@BotCommandValue String cmd, User user) {
return "Welcome, " + user.getFirstName() + "!";
}

@BotCommand({"/help", "/?"})
public String onHelp() {
return "Available commands: /start, /help";
}

Extra injectable parameters: @BotCommandValue String, @BotCommandQueryParam values.


@BotDefaultCommand

Target: METHOD

Fallback for any command not matched by a specific @BotCommand handler.

@BotDefaultCommand
public String onUnknownCommand(@BotCommandValue String cmd) {
return "Unknown command: " + cmd + ". Try /help.";
}

@BotText

Target: METHOD

Routes exact text messages (case-sensitive, full-string equality). Empty value() matches any text not matched by a more specific handler.

AttributeTypeDefaultDescription
valueString[]{}Exact text strings. Empty = any unmatched text
@BotText("hello")
public String onHello() { return "Hey!"; }

@BotText({"yes", "Yes", "YES"})
public String onYes() { return "Confirmed!"; }

Extra injectable parameters: @BotTextValue String.


@BotTextPattern

Target: METHOD

Routes text messages matching a regular expression. Matching uses Matcher.find() (substring). Use ^ and $ anchors for full-string matching. Patterns are compiled once at startup and cached.

AttributeTypeRequiredDescription
valueString[]Regex patterns — fires if any pattern matches
// Full-string phone number match
@BotTextPattern("^\\d{10}$")
public String onPhone(@BotTextValue String phone) {
return "Got number: " + phone;
}

// Substring match — any message containing "order"
@BotTextPattern("order \\S+")
public String onOrder(@BotTextValue String text) {
return "Processing: " + text;
}

// Multiple patterns — fires if any matches
@BotTextPattern({"^buy .+", "^purchase .+"})
public String onPurchase(@BotTextValue String text) {
return "Adding to cart: " + text;
}

@BotTextDefault

Target: METHOD

Fallback for any text message not matched by @BotText, @BotTextPattern, or @BotReplyButton.

@BotTextDefault
public String onAnyText(@BotTextValue String text) {
return "You said: " + text;
}

@BotCallbackQuery

Target: METHOD

Routes inline keyboard callback queries by exact data string. Empty value() matches any callback not matched by a specific handler.

AttributeTypeDefaultDescription
valueString[]{}Exact callback data strings. Empty = any unmatched callback
@BotCallbackQuery("btn_confirm")
public String onConfirm() { return "Confirmed!"; }

@BotCallbackQuery({"page_prev", "page_next"})
public String onPage(@BotCallbackQueryData String data) {
return "Navigating: " + data;
}

Extra injectable parameters: @BotCallbackQueryData String.


@BotDefaultCallbackQuery

Target: METHOD

Fallback for any callback query not matched by @BotCallbackQuery.

@BotDefaultCallbackQuery
public String onUnknownCallback(@BotCallbackQueryData String data) {
return "Unknown action: " + data;
}

@BotDynamicCallbackQuery

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD Since: 0.0.4

Routes inline keyboard callback queries by a type stored server-side via BotDynamicCallbackQueryService. Unlike @BotCallbackQuery, which matches the raw Telegram callback data string, this annotation routes by a type discriminator resolved from the stored BotDynamicCallbackData payload. This pattern is ideal when callback payloads exceed Telegram's 64-byte limit or when rich structured data is needed.

AttributeTypeDefaultDescription
valueString[]{}Type strings to match against BotDynamicCallbackData.getType(). Empty = any unmatched dynamic callback

Extra injectable parameters: BotDynamicCallbackData (the resolved payload).

// When building the keyboard:
String key = UUID.randomUUID().toString();
dynamicCallbackQueryService.store(key, BotDynamicCallbackData.builder()
.type("product_buy")
.put("id", productId)
.put("currency", "USD")
.build());
InlineKeyboardButton button = InlineKeyboardButton.builder()
.text("Buy")
.callbackData(key)
.build();

// Or use BotKeyboardFactory which generates the key automatically:
InlineKeyboardButton btn = keyboardFactory.dynamicInlineButton(
"btn.buy",
BotDynamicCallbackData.builder().type("product_buy").put("id", productId).build(),
request);

// When the callback fires:
@BotDynamicCallbackQuery("product_buy")
public String onBuy(BotDynamicCallbackData data) {
Long id = (Long) data.getData().get("id");
return "You selected product #" + id;
}

// Match multiple types:
@BotDynamicCallbackQuery({"product_buy", "product_view"})
public String onProduct(BotDynamicCallbackData data) {
return "Action: " + data.getType() + " on product #" + data.getData().get("id");
}

Target: METHOD

Routes messages containing a shared contact (phone number, name, etc.).

@BotContact
public String onContact(Update update) {
Contact c = update.getMessage().getContact();
return "Received: " + c.getFirstName() + " — " + c.getPhoneNumber();
}

@BotLocation

Target: METHOD

Routes messages containing a shared location.

@BotLocation
public String onLocation(Update update) {
Location loc = update.getMessage().getLocation();
return "Location: " + loc.getLatitude() + ", " + loc.getLongitude();
}

@BotReplyButton

Target: METHOD

Routes text messages matching a reply keyboard button label. With core-i18n on the classpath, value() strings are treated as MessageSource keys and resolved per request locale before matching.

AttributeTypeRequiredDescription
valueString[]Button labels or i18n message keys
@BotReplyButton(" Cancel")
@BotClearChatState
@BotClearMarkup
public String onCancel() { return "Cancelled."; }

// With core-i18n: value is a message bundle key
@BotReplyButton("button.cancel")
@BotClearChatState
public String onCancel() { return "Cancelled."; }

@BotEditedMessage

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes updates where a user edits a previously-sent text or media message. The original message is replaced in Telegram but the bot receives the edited copy.

Injectable parameters: Message (the edited message), User, Chat, Update, BotRequest, BotResponse, TelegramClient.

@BotEditedMessage
public void onEdited(Message edited, User user) {
log.info("User {} edited message {}: '{}'",
user.getId(), edited.getMessageId(), edited.getText());
}

@BotChannelPost

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes messages posted to a Telegram channel where the bot is an admin. User is not available (channel posts have no sender user); use Chat for the channel details.

Injectable parameters: Message (the channel post), Chat, Update, BotRequest, BotResponse, TelegramClient.

@BotChannelPost
public void onChannelPost(Message post, Chat channel) {
log.info("New post in channel @{}: '{}'",
channel.getUserName(), post.getText());
}

@BotEditedChannelPost

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes updates when a previously-published channel post is edited.

Injectable parameters: Message (the edited channel post), Chat, Update, BotRequest, BotResponse, TelegramClient.

@BotEditedChannelPost
public void onEditedChannelPost(Message edited, Chat channel) {
log.info("Channel @{} edited post {}", channel.getUserName(), edited.getMessageId());
}

@BotInlineQuery

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes inline queries — triggered when a user types @YourBot … in any chat. The optional value attribute filters by query text; omit it to match all inline queries.

AttributeTypeDefaultDescription
valueString[]{} (match all)Match inline query text against these strings; empty = any query

Injectable parameters: InlineQuery, @BotInlineQueryValue String (query text), User, Update, BotRequest, BotResponse, TelegramClient.

Without core-i18n — values are compared as literal strings (exact match):

// Match any inline query
@BotInlineQuery
public void onAnyInline(InlineQuery query, @BotInlineQueryValue String text) {
List<InlineQueryResult> results = searchProducts(text);
bot.execute(AnswerInlineQuery.builder()
.inlineQueryId(query.getId())
.results(results)
.build());
}

// Match only when query text is exactly "search"
@BotInlineQuery("search")
public void onSearchInline(@BotInlineQueryValue String text, InlineQuery query) {
// triggered only when user types "@YourBot search"
}

With core-i18n — values are treated as message-bundle keys resolved per user locale. A single annotation covers all languages automatically:

// messages/bot_en.properties: inline.search=search
// messages/bot_ru.properties: inline.search=поиск
@BotInlineQuery("inline.search")
public void onSearchInline(InlineQuery query, @BotInlineQueryValue String text) {
// matches "@YourBot search" for English users
// and "@YourBot поиск" for Russian users
}

The matching strategy is provided by BotInlineQueryMatcher (default: exact-text; with core-i18n: locale-aware). Override the bean to implement custom matching logic.


@BotChosenInlineResult

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes the feedback event sent by Telegram when a user selects one of the inline results your bot returned. Requires inline feedback to be enabled in BotFather.

Injectable parameters: ChosenInlineResult, @BotChosenInlineResultId String (the chosen result's ID), User, Update, BotRequest, BotResponse, TelegramClient.

@BotChosenInlineResult
public void onChosen(
ChosenInlineResult result,
@BotChosenInlineResultId String resultId,
User user) {
analytics.track(user.getId(), "inline_chosen", resultId);
}

@BotShippingQuery

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes shipping address queries sent by Telegram during the payment flow when the invoice has is_flexible = true. The bot must reply with AnswerShippingQuery to confirm or reject shipping options.

Injectable parameters: ShippingQuery, @BotShippingPayload String (invoice payload), User, Update, BotRequest, BotResponse, TelegramClient.

@BotShippingQuery
public void onShipping(
ShippingQuery query,
@BotShippingPayload String payload,
TelegramClient client) throws TelegramApiException {
List<ShippingOption> options = shippingService.getOptions(query.getShippingAddress());
client.execute(AnswerShippingQuery.builder()
.shippingQueryId(query.getId())
.ok(true)
.shippingOptions(options)
.build());
}

@BotPreCheckoutQuery

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes the pre-checkout event fired by Telegram just before a payment is completed. The bot must respond with AnswerPreCheckoutQuery within 10 seconds to confirm or cancel the transaction.

Injectable parameters: PreCheckoutQuery, @BotPreCheckoutPayload String (invoice payload), User, Update, BotRequest, BotResponse, TelegramClient.

@BotPreCheckoutQuery
public void onPreCheckout(
PreCheckoutQuery query,
@BotPreCheckoutPayload String payload,
TelegramClient client) throws TelegramApiException {
boolean valid = orderService.validate(payload, query.getTotalAmount());
client.execute(AnswerPreCheckoutQuery.builder()
.preCheckoutQueryId(query.getId())
.ok(valid)
.errorMessage(valid ? null : "Order is no longer available.")
.build());
}

@BotPoll

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes poll state-change updates. Telegram sends these when a non-anonymous poll is stopped or its vote counts change.

Injectable parameters: Poll, Update, BotRequest, BotResponse, TelegramClient.

@BotPoll
public void onPollUpdate(Poll poll) {
if (poll.getIsClosed()) {
int winner = findWinner(poll.getOptions());
log.info("Poll '{}' closed. Winner option index: {}", poll.getQuestion(), winner);
}
}

@BotPollAnswer

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes individual vote events — triggered when a user votes (or retracts their vote) in a non-anonymous poll created by the bot.

Injectable parameters: PollAnswer, User, Update, BotRequest, BotResponse, TelegramClient.

@BotPollAnswer
public void onVote(PollAnswer answer, User user) {
List<Integer> chosen = answer.getOptionIds();
log.info("User {} voted: options {}", user.getId(), chosen);
}

@BotMyChatMember

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes updates about the bot's own membership status in a chat — e.g. the bot was added to a group, promoted to admin, or removed.

Injectable parameters: ChatMemberUpdated, User (the user who made the change), Chat, Update, BotRequest, BotResponse, TelegramClient.

@BotMyChatMember
public void onBotMemberChange(ChatMemberUpdated update, Chat chat) {
ChatMember newStatus = update.getNewChatMember();
if (newStatus instanceof ChatMemberMember) {
log.info("Bot was added to chat {}", chat.getId());
} else if (newStatus instanceof ChatMemberLeft) {
log.info("Bot was removed from chat {}", chat.getId());
}
}

@BotChatMemberUpdate

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes updates about another user's membership status in a chat managed by the bot. Requires the bot to have admin rights and the chat_member update type to be subscribed. Named BotChatMemberUpdate to avoid a naming clash with the Telegram API's ChatMember class.

Injectable parameters: ChatMemberUpdated, User (the user who made the change), Chat, Update, BotRequest, BotResponse, TelegramClient.

@BotChatMemberUpdate
public void onUserMemberChange(ChatMemberUpdated event, Chat chat) {
User affected = event.getNewChatMember().getUser();
ChatMember newRole = event.getNewChatMember();
log.info("User {} status changed in chat {}: {}",
affected.getId(), chat.getId(), newRole.getStatus());
}

@BotChatJoinRequest

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes join requests when a user asks to join a channel or group that requires admin approval. The bot can approve or decline via ApproveChatJoinRequest / DeclineChatJoinRequest.

Injectable parameters: ChatJoinRequest, User (the requester), Chat, Update, BotRequest, BotResponse, TelegramClient.

@BotChatJoinRequest
public void onJoinRequest(
ChatJoinRequest request,
User requester,
TelegramClient client) throws TelegramApiException {
if (membershipService.isAllowed(requester.getId())) {
client.execute(ApproveChatJoinRequest.builder()
.chatId(request.getChat().getId())
.userId(requester.getId())
.build());
} else {
client.execute(DeclineChatJoinRequest.builder()
.chatId(request.getChat().getId())
.userId(requester.getId())
.build());
}
}

@BotBusinessConnection

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes business connection updates — fired when a Telegram Business account connects or disconnects from your bot.

Injectable parameters: BusinessConnection, Update, BotRequest, BotResponse, TelegramClient.

@BotBusinessConnection
public void onBusinessConnection(BusinessConnection connection) {
if (connection.getIsEnabled()) {
log.info("Business account {} connected", connection.getId());
businessService.activate(connection.getUser().getId());
} else {
log.info("Business account {} disconnected", connection.getId());
businessService.deactivate(connection.getUser().getId());
}
}

@BotBusinessMessage

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes messages sent by customers to a connected Telegram Business account. The bot receives these to provide automated responses on behalf of the business.

Injectable parameters: Message, User, Chat, Update, BotRequest, BotResponse, TelegramClient.

@BotBusinessMessage
public void onBusinessMessage(Message message, User customer) {
String reply = autoReplyService.generateReply(message.getText());
log.info("Business message from {}: '{}' → auto-reply: '{}'",
customer.getId(), message.getText(), reply);
}

@BotEditedBusinessMessage

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes edit events for messages that were previously received via a connected business account.

Injectable parameters: Message (the edited message), User, Chat, Update, BotRequest, BotResponse, TelegramClient.

@BotEditedBusinessMessage
public void onEditedBusinessMessage(Message edited) {
log.info("Business message {} was edited: '{}'",
edited.getMessageId(), edited.getText());
}

@BotDeletedBusinessMessages

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes bulk-deletion events for messages in a connected business account chat.

Injectable parameters: BusinessMessagesDeleted, Update, BotRequest, BotResponse, TelegramClient.

@BotDeletedBusinessMessages
public void onDeletedBusinessMessages(BusinessMessagesDeleted deleted) {
log.info("Business chat {}: {} messages deleted",
deleted.getChat().getId(),
deleted.getMessageIds().size());
auditService.recordDeletion(deleted.getBusinessConnectionId(), deleted.getMessageIds());
}

@BotPaidMediaPurchased

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Routes the event fired when a user completes a paid media purchase. Inject Update and call update.getPaidMediaPurchased() to access the purchase details.

Injectable parameters: Update, User, BotRequest, BotResponse, TelegramClient.

@BotPaidMediaPurchased
public void onPaidMediaPurchased(Update update, User buyer) {
var purchase = update.getPaidMediaPurchased();
log.info("User {} purchased paid media. Payload: '{}'",
buyer.getId(), purchase.getPaidMediaPayload());
orderService.fulfil(buyer.getId(), purchase.getPaidMediaPayload());
}

@BotDefaultHandler

Target: METHOD, TYPE

Global catch-all — matches any update not handled by a more specific handler.

@BotDefaultHandler
public String onDefault() {
return "I don't understand that. Send /help for a list of commands.";
}

@BotExceptionHandler

Target: METHOD

Handles exceptions thrown by handler methods. Declare inside @BotController (controller-scoped) or @BotControllerAdvice (global). Controller-local handlers take priority over advice handlers for the same type.

AttributeTypeRequiredDescription
valueClass<? extends Throwable>[]Exception types this handler handles
// In a controller — handles RuntimeException thrown by this controller's handlers only
@BotExceptionHandler(RuntimeException.class)
public String onRuntimeError(RuntimeException ex, User user) {
log.error("Error for user {}: {}", user.getId(), ex.getMessage());
return "Something went wrong. Please try again.";
}

// In @BotControllerAdvice — global handler
@BotExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
public String onBadInput(Throwable ex) {
return "Bad input: " + ex.getMessage();
}

Injectable parameters: Throwable (the thrown exception), User, Chat, BotRequest, BotResponse, Update, TelegramClient.


Parameter Annotations

All parameter annotations live in uz.osoncode.easygram.core.bind.annotation.


@BotCommandValue

Target: PARAMETER

Injects the matched command string (e.g. "/start").

@BotCommand("/start")
public String onStart(@BotCommandValue String cmd) {
// cmd == "/start"
return "Command was: " + cmd;
}

@BotTextValue

Target: PARAMETER

Injects the full raw message text.

@BotTextDefault
public String echo(@BotTextValue String text) {
return "You said: " + text;
}

@BotCallbackQueryData

Target: PARAMETER

Injects the callback query data string.

@BotCallbackQuery("action_buy")
public String onBuy(@BotCallbackQueryData String data) {
return "Action: " + data;
}

@BotCommandQueryParam

Target: PARAMETER

Injects a typed positional argument following a command. For /start 42, injects 42 as the declared type. Conversion is performed via Jackson.

Supported types include: String, Integer, Long, Boolean, Double, and any Jackson-deserializable type.

// User sends: /start 42
@BotCommand("/start")
public String onDeepLink(@BotCommandQueryParam Integer referralCode) {
return "Referred by: " + referralCode;
}

// User sends: /open {"userId":7,"role":"admin"}
@BotCommand("/open")
public String onOpen(@BotCommandQueryParam MyParams params) {
return "Opening for: " + params.getUserId();
}

Update-Type Parameter Annotations (0.0.2)

All annotations below are in package uz.osoncode.easygram.core.bind.annotation, target PARAMETER.

AnnotationInjectsAvailable in
@BotInlineQueryValueString — inline query text@BotInlineQuery handlers
@BotChosenInlineResultIdString — chosen result ID@BotChosenInlineResult handlers
@BotShippingPayloadString — invoice payload@BotShippingQuery handlers
@BotPreCheckoutPayloadString — invoice payload@BotPreCheckoutQuery handlers
@BotInlineQuery
public void onInline(@BotInlineQueryValue String queryText, InlineQuery query) { ... }

@BotChosenInlineResult
public void onChosen(@BotChosenInlineResultId String resultId) { ... }

@BotShippingQuery
public void onShipping(@BotShippingPayload String payload, ShippingQuery query) { ... }

@BotPreCheckoutQuery
public void onPreCheckout(@BotPreCheckoutPayload String payload, PreCheckoutQuery query) { ... }

Response Annotations

@BotReplyMarkup

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Attaches a pre-registered keyboard to the response by looking it up in BotMarkupRegistry at invocation time. Acts as a fallback — only applied if the return value itself carries no markup (i.e. PlainReply.withMarkup(...) takes precedence).

@BotCommand("/menu")
@BotReplyMarkup("main_menu")
public String onMenu() {
return "Here is the menu:";
}

Compatible with String, PlainReply, and LocalizedReply return types.


@BotClearMarkup

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Sends a ReplyKeyboardRemove along with the response. Always overrides @BotReplyMarkup when both are present.

@BotCommand("/cancel")
@BotClearChatState
@BotClearMarkup
public String onCancel() {
return "Cancelled. Keyboard removed.";
}

@BotParseMode

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Sets the Telegram parse_mode field on the outgoing SendMessage or EditMessageText call. Valid values: "HTML", "MarkdownV2", "Markdown" (legacy).

Works with all return types that produce a SendMessage: String, PlainReply, and LocalizedReply.

AttributeTypeDefaultDescription
valueString(required)Parse mode — "HTML", "MarkdownV2", or "Markdown"
@BotParseMode("HTML")
@BotCommand("/start")
public String start(User user) {
return "<b>Hello, " + user.getFirstName() + "!</b>";
}

@BotParseMode("MarkdownV2")
@BotCommand("/help")
public PlainReply help() {
return PlainReply.of("*Bold* and _italic_");
}

Can be combined with @BotReplyMarkup in any order.

All MarkupAware reply types also expose .withParseMode(String) for runtime control:

return PlainReply.of("<b>text</b>").withParseMode("HTML");
return LocalizedReply.of("msg.key").withParseMode("HTML");

Since 0.0.6


Chat State Annotations

All chat state annotations live in uz.osoncode.easygram.core.bind.annotation (and core-chatstate).


@BotChatState

Target: METHOD, TYPE

Restricts a handler to run only when the chat's current state matches one of the declared values. On a class, applies as the default for all methods in that class.

// Method-level: run only in AWAITING_PHONE state
@BotContact
@BotChatState("AWAITING_PHONE")
public String onPhone(Update update) { ... }

// Class-level: all methods default to AWAITING_NAME | AWAITING_AGE
@BotController
@BotChatState({"AWAITING_NAME", "AWAITING_AGE"})
public class RegistrationController {

@BotTextDefault
// inherits @BotChatState({"AWAITING_NAME", "AWAITING_AGE"}) from class
public String onText(@BotTextValue String text) { ... }

@BotCommand("/cancel")
@BotChatState // empty — overrides class restriction, accepts any state
public String onCancel() { return "Cancelled."; }
}

@BotForwardChatState

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Transitions the chat to a new state after the handler method returns successfully.

AttributeTypeRequiredDescription
valueStringState name to set
@BotTextDefault
@BotChatState("STEP_1")
@BotForwardChatState("STEP_2")
public String step1(@BotTextValue String name, BotRequest request) {
// After return: chatStateService.setState(request.getChat().getId(), "STEP_2")
return "Got it! Now send your age.";
}

@BotClearChatState

Package: uz.osoncode.easygram.core.bind.annotation Target: METHOD

Clears the chat state (sets to null) after the handler method returns successfully.

@BotCommand("/done")
@BotClearChatState
public String onDone() {
return "Flow complete. State cleared.";
}

Return Types

Handler methods may return any of the following types. null is treated as no-op (same as void).

Return typeModuleBehavior
voidcoreNo message sent
nullcoreNo-op — same as void
StringcoreSendMessage to current chat. @BotReplyMarkup/@BotParseMode/@BotClearMarkup applied by BotStringReturnHandler
PlainReplycoreSendMessage with optional markup, parse mode, and delivery options (since 0.0.6). Implements MarkupAware. Actions routed through BotReplyActionChain
BotApiMethod<?>coreEnqueued and executed directly by BotApiSenderFilter
Collection<BotApiMethod<?>>coreAll methods enqueued and executed in order
Collection<Object>coreEach element individually dispatched to matching BotReturnTypeHandler via supportsElement()
LocalizedReplycore-i18nMessageSource key lookup resolved to locale-aware SendMessage. Implements MarkupAware. Actions routed through BotReplyActionChain (since 0.0.6)
Removed in 0.0.6

PlainTextTemplate and LocalizedTemplate were removed in 0.0.6. Use PlainReply.of("Hello, {0}!", name) or LocalizedReply.of("msg.key", name) with standard Java MessageFormat {n} tokens instead. See the 0.0.5 → 0.0.6 migration guide for full migration instructions.

PlainReply — Fluent API

PlainReply is an immutable value object. All wither methods return new instances. A builder is also available.

PlainReply supports standard Java MessageFormat {n} positional tokens when args are provided; the framework calls MessageFormat.format(text, args) before sending. (Since 0.0.6)

PlainReply.of("Hello!")                          // text only
PlainReply.of("Hi, {0}!", user.getFirstName()) // MessageFormat arg substitution (since 0.0.6)
PlainReply.of("Choose:").withMarkup("main_menu") // attach registered keyboard
PlainReply.of("Buy?").withMarkup("product_kb", // attach keyboard with params
Map.of("productId", "42"))
PlainReply.of("Pick:").withKeyboard(myKeyboard) // attach ReplyKeyboard directly
PlainReply.of("Done.").removeMarkup() // send ReplyKeyboardRemove
PlainReply.of("Updated!").withEditMessage() // edit originating callback-query message (since 0.0.2)

// Answer callback query — reply text used as popup notification (since 0.0.5)
PlainReply.of("Confirmed!").asAnswerCallbackQuery() // toast popup
PlainReply.of("Deleted.").asAnswerCallbackQuery().withCallbackAlert() // alert dialog
PlainReply.of("Open page").asAnswerCallbackQuery().withCallbackUrl("https://example.com")
PlainReply.of("OK").asAnswerCallbackQuery().withCallbackCacheTime(10)

// Parse mode — inline or via @BotParseMode (since 0.0.6)
PlainReply.of("<b>Bold</b>").withParseMode("HTML")
PlainReply.of("*Bold*").withParseMode("MarkdownV2")

// Delivery options (since 0.0.6)
PlainReply.of("Silent!").withDisableNotification(true) // send without sound
PlainReply.of("Private.").withProtectContent(true) // disable forwarding/saving
PlainReply.of("Thread.").withMessageThreadId(123) // post into forum topic
PlainReply.of("Reply").withReplyParameters(replyParams) // reply to a specific message
PlainReply.of("Preview").withLinkPreviewOptions(previewOpts) // control link preview

// Builder
PlainReply.builder()
.text("Choose:")
.args(user.getFirstName())
.markupId("main_menu")
.parseMode("HTML") // since 0.0.6
.disableNotification(true) // since 0.0.6
.protectContent(true) // since 0.0.6
.messageThreadId(123) // since 0.0.6
.replyParameters(replyParams) // since 0.0.6
.linkPreviewOptions(previewOpts) // since 0.0.6
.editMessage(true) // edit instead of send (since 0.0.2)
.answerCallbackQuery(true) // send AnswerCallbackQuery (since 0.0.5)
.callbackAlert(true) // showAlert=true (since 0.0.5)
.callbackUrl("https://...") // optional URL (since 0.0.5)
.callbackCacheTime(10) // cache seconds (since 0.0.5)
.build()

Wither methods summary:

MethodReturnsSince
withMarkup(String)PlainReply0.0.1
withMarkup(String, Map)PlainReply0.0.1
withKeyboard(ReplyKeyboard)PlainReply0.0.1
removeMarkup()PlainReply0.0.1
withEditMessage()PlainReply0.0.2
asAnswerCallbackQuery()PlainReply0.0.5
withCallbackAlert()PlainReply0.0.5
withCallbackUrl(String)PlainReply0.0.5
withCallbackCacheTime(int)PlainReply0.0.5
withArgs(Object...)PlainReply0.0.6
withParseMode(String)PlainReply0.0.6
withDisableNotification(Boolean)PlainReply0.0.6
withProtectContent(Boolean)PlainReply0.0.6
withMessageThreadId(Integer)PlainReply0.0.6
withReplyParameters(ReplyParameters)PlainReply0.0.6
withLinkPreviewOptions(LinkPreviewOptions)PlainReply0.0.6
Removed in 0.0.6 — PlainTextTemplate

PlainTextTemplate was removed in version 0.0.6. Migrate to PlainReply with {n} MessageFormat args:

// Before (0.0.5)
PlainTextTemplate.of("Hello, #{0}! You have #{1} messages.", name, count)

// After (0.0.6)
PlainReply.of("Hello, {0}! You have {1} messages.", name, count)

See the 0.0.5 → 0.0.6 migration guide for full details.

LocalizedReply — Fluent API (core-i18n)

Key is resolved via BotMessageSource using the request locale. Positional args use standard Java MessageFormat {n} tokens.

LocalizedReply.of("welcome.message", user.getFirstName())
LocalizedReply.of("choose.option").withMarkup("main_menu")
LocalizedReply.of("confirm.prompt").withMarkup("confirm_kb", Map.of("id", itemId))
LocalizedReply.of("updated.text").withEditMessage() // edit callback-query message (since 0.0.2)

// Answer callback query — resolved i18n message used as popup text (since 0.0.5)
LocalizedReply.of("action.confirmed").asAnswerCallbackQuery()
LocalizedReply.of("item.deleted").asAnswerCallbackQuery().withCallbackAlert()
LocalizedReply.of("opening.page").asAnswerCallbackQuery().withCallbackUrl("https://example.com")

// Parse mode — inline or via @BotParseMode (since 0.0.6)
LocalizedReply.of("msg.bold").withParseMode("HTML")

// Delivery options (since 0.0.6)
LocalizedReply.of("msg.silent").withDisableNotification(true)
LocalizedReply.of("msg.private").withProtectContent(true)
LocalizedReply.of("msg.thread").withMessageThreadId(123)
LocalizedReply.of("msg.reply").withReplyParameters(replyParams)
LocalizedReply.of("msg.link").withLinkPreviewOptions(previewOpts)

// Builder
LocalizedReply.builder()
.key("welcome.message")
.args(user.getFirstName())
.markupId("main_menu")
.parseMode("HTML") // since 0.0.6
.disableNotification(true) // since 0.0.6
.protectContent(true) // since 0.0.6
.messageThreadId(123) // since 0.0.6
.replyParameters(replyParams) // since 0.0.6
.linkPreviewOptions(previewOpts) // since 0.0.6
.editMessage(true) // since 0.0.2
.answerCallbackQuery(true) // since 0.0.5
.callbackAlert(true) // showAlert=true (since 0.0.5)
.callbackUrl("https://...") // optional URL (since 0.0.5)
.callbackCacheTime(10) // cache seconds (since 0.0.5)
.build()

Wither methods summary:

MethodReturnsSince
withMarkup(String)LocalizedReply0.0.1
withMarkup(String, Map)LocalizedReply0.0.1
withKeyboard(ReplyKeyboard)LocalizedReply0.0.1
removeMarkup()LocalizedReply0.0.1
withEditMessage()LocalizedReply0.0.2
asAnswerCallbackQuery()LocalizedReply0.0.5
withCallbackAlert()LocalizedReply0.0.5
withCallbackUrl(String)LocalizedReply0.0.5
withCallbackCacheTime(int)LocalizedReply0.0.5
withParseMode(String)LocalizedReply0.0.6
withDisableNotification(Boolean)LocalizedReply0.0.6
withProtectContent(Boolean)LocalizedReply0.0.6
withMessageThreadId(Integer)LocalizedReply0.0.6
withReplyParameters(ReplyParameters)LocalizedReply0.0.6
withLinkPreviewOptions(LinkPreviewOptions)LocalizedReply0.0.6
Removed in 0.0.6 — LocalizedTemplate

LocalizedTemplate was removed in version 0.0.6. Migrate to LocalizedReply with standard {n} MessageFormat tokens in your message bundles:

# Before (0.0.5) — bot.properties using #{n} tokens
welcome.title=Welcome, #{0}!

# After (0.0.6) — standard MessageFormat {n} tokens
welcome.title=Welcome, {0}!
// Before (0.0.5)
LocalizedTemplate.of("${welcome.title}\n\nHello, #{0}!", user.getFirstName())

// After (0.0.6)
LocalizedReply.of("welcome.title", user.getFirstName())

See the 0.0.5 → 0.0.6 migration guide for full details.


Dynamic Callbacks

(Since 0.0.4) The dynamic callback system lets you store rich structured data server-side and route inline keyboard callbacks by a type discriminator, bypassing Telegram's 64-byte callbackData limit.


BotDynamicCallbackData

Package: uz.osoncode.easygram.core.dynamiccallback

Immutable record that stores a type discriminator and an arbitrary key-value data map. Instances are stored via BotDynamicCallbackQueryService and retrieved by the framework when a matching callback query arrives.

// Build a payload
BotDynamicCallbackData payload = BotDynamicCallbackData.builder()
.type("product_buy")
.put("id", 42L)
.put("currency", "USD")
.build();

// Access in a handler
@BotDynamicCallbackQuery("product_buy")
public String onBuy(BotDynamicCallbackData data) {
String type = data.getType(); // "product_buy"
Long id = (Long) data.getData().get("id"); // 42
return "Buying product #" + id;
}

API:

String getType()               // type discriminator; never null
Map<String,Object> getData() // unmodifiable data map; never null

// Record accessors
String type()
Map<String,Object> data()

static final String ATTRIBUTE_KEY = "DYNAMIC_CALLBACK_DATA" // BotRequest attribute key

static Builder builder()

Builder methods:

Builder type(String type)                    // required — type discriminator
Builder data(Map<String,Object> data) // replace entire data map
Builder put(String key, Object value) // add a single entry
BotDynamicCallbackData build()

BotDynamicCallbackQueryService

Package: uz.osoncode.easygram.core.dynamiccallback

SPI for storing and retrieving BotDynamicCallbackData payloads. The default in-memory implementation is auto-configured. Replace with a @Bean backed by Redis, JDBC, etc. for persistence across restarts.

public interface BotDynamicCallbackQueryService {

/** Looks up the payload stored under callbackData (the UUID key). Returns null if not found. */
BotDynamicCallbackData resolve(String callbackData);

/** Stores payload under callbackData, replacing any previous mapping. */
void store(String callbackData, BotDynamicCallbackData payload);

/** Removes the payload for callbackData. No-op if not found. */
void remove(String callbackData);
}
// Custom Redis-backed implementation
@Bean
public BotDynamicCallbackQueryService redisDynamicCallbackService(StringRedisTemplate redis) {
return new RedisBotDynamicCallbackQueryService(redis);
}

Model Classes

BotRequest

Package: uz.osoncode.easygram.core.model

The per-request context object. Created once per incoming Telegram update and carried through the entire filter chain into handler methods.

// Core data
Update getUpdate() // raw Telegram Update
User getUser() // resolved sender (set by BotContextSetterFilter)
Chat getChat() // resolved chat (set by BotContextSetterFilter)
TelegramClient getTelegramClient()
BotMetadata getBotMetadata() // bot id, username, token
Throwable getThrowable() // populated inside @BotExceptionHandler

// Request-scoped attribute store
void setAttribute(String key, Object value) // null value removes the key
<T> T getAttribute(String key)
Map<String,Object> getAttributes() // unmodifiable view

Sharing data between filters and handlers via attributes:

// In a BotFilter:
request.setAttribute("userId", resolvedUserId);

// In a handler or BotArgumentResolver:
Long userId = request.getAttribute("userId");

BotResponse

Package: uz.osoncode.easygram.core.model

The per-request response accumulator. Queued BotApiMethod calls are executed in insertion order by BotApiSenderFilter after the full filter chain completes.

void addBotApiMethod(BotApiMethod<?> method)
void addBotApiMethods(Collection<BotApiMethod<?>> methods)
Collection<BotApiMethod<?>> getBotApiMethods() // current queue, read-only

// Response-scoped attribute store
void setAttribute(String key, Object value)
<T> T getAttribute(String key)
Map<String,Object> getAttributes()

Building and queuing API calls from a filter:

@Override
public void doFilter(BotRequest request, BotResponse response, BotFilterChain chain) {
if (!isAllowed(request.getUser())) {
response.addBotApiMethod(
SendMessage.builder()
.chatId(request.getChat().getId())
.text(" Access denied.")
.build()
);
return; // do NOT call chain.doFilter — pipeline stops here
}
chain.doFilter(request, response);
}

BotMetadata

Package: uz.osoncode.easygram.core.model

Bot's own registration data. Available via BotRequest.getBotMetadata().

Long getId()
String getUsername()
String getToken()

BotMarkupContext

Package: uz.osoncode.easygram.core.markup

Carries parameters into a @BotMarkup factory method when the keyboard was requested with .withMarkup(id, Map). Available as a method parameter in any @BotMarkup method.

static BotMarkupContext of(Map<String,Object> params)
static BotMarkupContext empty()

<T> T get(String key)
<T> T get(String key, Class<T> type)
<T> T getOrDefault(String key, T defaultValue)
boolean has(String key)
Map<String,Object> asMap()

String REQUEST_ATTRIBUTE_KEY = "__botMarkupContext__"
// Return value passes params:
return PlainReply.of("View product:").withMarkup("product_kb", Map.of("productId", "99"));

// Factory receives them:
@BotMarkup("product_kb")
public ReplyKeyboard productKeyboard(BotMarkupContext ctx) {
String id = ctx.get("productId");
return InlineKeyboardMarkup.builder()
.keyboardRow(List.of(
InlineKeyboardButton.builder()
.text(" Buy").callbackData("buy:" + id).build()
))
.build();
}

Model Classes

ReplyOptions

Package: uz.osoncode.easygram.core.reply Module: core-api

Immutable record that groups all shared options for PlainReply and LocalizedReply. Both types hold a ReplyOptions instance and delegate all wither methods to it. (Since 0.0.6)

GroupFields
MarkupmarkupId, markupParams, keyboard, removeMarkup
BehavioreditMessage
CallbackanswerCallbackQuery, callbackAlert, callbackUrl, callbackCacheTime
DeliveryparseMode, disableNotification, protectContent, messageThreadId, replyParameters, linkPreviewOptions

ReplyOptions.DEFAULTS — singleton with all fields null / false; use as the starting point.

Typically not used directly by application code — use the PlainReply / LocalizedReply wither methods which delegate to ReplyOptions internally.


SendReplyOptions

Package: uz.osoncode.easygram.core.returntypehandler Module: core-api

Immutable record carrying the delivery options subset passed from the return-type handler into BotReplyMessageHelper when constructing SendMessage or EditMessageText calls. (Since 0.0.6)

FieldType
parseModeString
disableNotificationBoolean
protectContentBoolean
messageThreadIdInteger
replyParametersReplyParameters
linkPreviewOptionsLinkPreviewOptions

Obtained from ReplyOptions.toSendReplyOptions() inside the action implementations.


Extension Interfaces

All extension interfaces are registered as Spring @Beans (or @Components). The framework auto-collects them via List<T> injection.


BotFilter

Package: uz.osoncode.easygram.core.filter

Intercepts every update before it reaches a handler. Analogous to a Servlet Filter.

public interface BotFilter {

/** Lower value = runs earlier. Default: Integer.MAX_VALUE. */
default int getOrder() { return Integer.MAX_VALUE; }

/** Return false to skip this filter for the current request. Default: always runs. */
default boolean shouldFilter() { return true; }

/** Call chain.doFilter(request, response) to continue; omit to short-circuit. */
void doFilter(BotRequest request, BotResponse response, BotFilterChain chain);
}
@Component
public class RateLimitFilter implements BotFilter {

@Override
public int getOrder() { return BotFilterOrder.PUBLISHING + 1; }

@Override
public void doFilter(BotRequest req, BotResponse res, BotFilterChain chain) {
if (rateLimiter.tryAcquire(req.getUser().getId())) {
chain.doFilter(req, res);
} else {
res.addBotApiMethod(SendMessage.builder()
.chatId(req.getChat().getId())
.text("Slow down!").build());
}
}
}

See Custom Filters for more examples.


BotArgumentResolver

Package: uz.osoncode.easygram.core.argumentresolver

Resolves custom types or annotations into handler method parameters.

public interface BotArgumentResolver {

/** Return true to claim this parameter. Called once per handler registration. */
boolean supportsParameter(java.lang.reflect.Parameter parameter);

/** Return the resolved value. May return null for optional parameters. */
Object resolveArgument(java.lang.reflect.Parameter parameter,
BotRequest request,
BotResponse response);
}

The framework invokes supportsParameter for each unresolved parameter and delegates to the first matching resolver. Built-in resolvers handle: Update, User, Chat, TelegramClient, BotRequest, BotResponse, Throwable, Locale (core-i18n), and all parameter annotations. Custom resolvers are ordered by @Order.

@Component
public class CurrentUserResolver implements BotArgumentResolver {

@Override
public boolean supportsParameter(Parameter p) {
return p.getType() == AppUser.class;
}

@Override
public Object resolveArgument(Parameter p, BotRequest req, BotResponse res) {
return userService.findByTelegramId(req.getUser().getId());
}
}

See Custom Argument Resolvers for more examples.


BotReturnTypeHandler

Package: uz.osoncode.easygram.core.returntypehandler

Converts handler method return values into queued BotApiMethod calls.

public interface BotReturnTypeHandler {

/** Return true if this handler owns the return type of the given method. */
boolean supportsReturnType(Method method);

/**
* Return true if this handler can dispatch a single element inside a Collection<Object>.
* Enables mixed-collection routing — each element is dispatched independently.
*/
default boolean supportsElement(Object element) { return false; }

/** Process returnValue and add resulting API calls to response. */
void handleReturnType(BotRequest request, BotResponse response, Object returnValue);

/**
* Annotation-aware variant — override when you need access to @BotReplyMarkup etc.
* Default implementation delegates to the 3-arg overload.
*/
default void handleReturnType(BotRequest request, BotResponse response,
Object returnValue, Method method) {
handleReturnType(request, response, returnValue);
}
}

BotReturnTypeHandlerFactory collects all BotReturnTypeHandler beans and returns the first whose supportsReturnType returns true. Registration order matters — use @Order to control priority.

See Custom Return-Type Handlers for examples.


BotReplyAction

Package: uz.osoncode.easygram.core.returntypehandler Module: core-api

SPI interface for extensible Bot API method dispatch. The framework collects all BotReplyAction beans into a BotReplyActionChain, sorts them by getOrder(), and fires every action whose supports() returns true. (Since 0.0.6)

Three default actions ship in core:

ClassConditionOrder
SendMessageReplyActionNot an alert, not editMessage or no callback query10
EditMessageReplyActioneditMessage=true and a callback query is present10
AnswerCallbackQueryReplyActionanswerCallbackQuery=true20
public interface BotReplyAction {
boolean supports(ReplyOptions options, BotRequest request);
void execute(BotRequest request, BotResponse response,
String resolvedText, ReplyOptions options);
default int getOrder() { return 0; }
}

Register a custom action as a @Bean to add new dispatch behaviour (e.g. pin a message, send a sticker) without modifying any existing code.

@Bean
public BotReplyAction pinMessageAction() {
return new PinMessageReplyAction();
}

BotHandlerInvocationFilter

Package: uz.osoncode.easygram.core.handler.invocation

Intercepts handler method invocation after the dispatcher has selected a handler but before the method is called. Runs inside the inner invocation chain, distinct from the outer BotFilter pipeline.

public interface BotHandlerInvocationFilter {

/** Lower value = runs earlier. Default: Integer.MAX_VALUE. */
default int getOrder() { return Integer.MAX_VALUE; }

/** Call chain.proceed(ctx) to continue; omit to short-circuit. */
void invoke(BotHandlerInvocationContext ctx, BotHandlerInvocationChain chain);
}

Built-in invocation filters (executed in this order):

FilterOrder constantPurpose
MethodInvocationFilterMETHOD_INVOCATION (MIN_VALUE)Resolves arguments and invokes the controller method
MarkupApplicationFilterMARKUP_APPLICATION (MIN_VALUE + 1)Applies @BotReplyMarkup/@BotClearMarkup to the return value
ReturnTypeDispatchFilterRETURN_TYPE_DISPATCH (MIN_VALUE + 2)Routes the return value to the matching BotReturnTypeHandler
ChatStateUpdateFilterCHAT_STATE_UPDATE (MIN_VALUE + 3)Applies @BotForwardChatState/@BotClearChatState

Custom filters with getOrder() < METHOD_INVOCATION run before method execution (e.g. permission guards). Inserted between constants, they run at that stage.


BotChatStateService

Package: uz.osoncode.easygram.core.chatstate

Persistence layer for per-chat state. The default InMemoryBotChatStateService (from core-chatstate) stores state in a ConcurrentHashMap. Replace with a custom @Bean backed by Redis, JDBC, or any other store.

public interface BotChatStateService {

String getState(Long chatId);
void setState(Long chatId, String state); // throws IllegalArgumentException if state is null (since 0.0.2)
void clearState(Long chatId); // Added in 0.0.2 — removes state without null check

// Enum convenience helpers
default void setState(Long chatId, Enum<?> state) { setState(chatId, state.name()); }
default <T extends Enum<T>> T getStateAs(Long chatId, Class<T> enumClass) { ... }
}
// Custom Redis-backed implementation
@Bean
public BotChatStateService redisChatStateService(StringRedisTemplate redis) {
return new RedisBotChatStateService(redis);
}

See Chat State Backends for examples.


BotUpdatePublisher

Package: uz.osoncode.easygram.messaging

SPI for publishing raw Telegram updates to a message broker. Implement and register as a @Bean to integrate with any broker. Ready-made implementations are provided by messaging-kafka and messaging-rabbit.

public interface BotUpdatePublisher {
void publish(Update update);
}
@Component
public class MyCustomPublisher implements BotUpdatePublisher {
@Override
public void publish(Update update) {
// send to your broker
}
}

BotHandlerConditionContributor

Package: uz.osoncode.easygram.core.handler

Contributes additional match conditions to every handler at startup. Register as a @Bean. Useful for permission annotations, feature flags, or other cross-cutting routing concerns.

public interface BotHandlerConditionContributor {
List<BotHandlerCondition> contribute(Method method, Object bean);
}
@Component
public class RequiresAdminContributor implements BotHandlerConditionContributor {

@Override
public List<BotHandlerCondition> contribute(Method method, Object bean) {
if (!method.isAnnotationPresent(RequiresAdmin.class)) return List.of();
return List.of(request -> adminService.isAdmin(request.getUser().getId()));
}
}

BotStartTrigger

Package: uz.osoncode.easygram.core.trigger

Executed once at application startup after the bot's own User object and TelegramClient become available. Use for initial setup — registering webhook, sending notifications, etc.

Since 0.0.7, BotStartTrigger is a @FunctionalInterface — you can define it as a lambda in any @Bean method.

@FunctionalInterface
public interface BotStartTrigger {
void execute(User bot, TelegramClient telegramClient);
}

Class-based (all versions):

@Component
public class RegisterCommandsTrigger implements BotStartTrigger {

@Override
public void execute(User bot, TelegramClient client) {
client.execute(SetMyCommands.builder()
.command(BotCommand.builder().command("/start").description("Start the bot").build())
.command(BotCommand.builder().command("/help").description("Help").build())
.build());
}
}

Lambda-style (since 0.0.7):

@Configuration
public class BotStartupConfig {

@Bean
public BotStartTrigger registerCommandsTrigger() {
return (bot, client) -> client.execute(
SetMyCommands.builder()
.command(BotCommand.builder().command("/start").description("Start").build())
.build());
}

@Bean
public BotStartTrigger logBotInfoTrigger() {
return (bot, client) -> log.info("Bot started: @{} (id={})", bot.getUserName(), bot.getId());
}
}

Multiple BotStartTrigger beans are all executed at startup — there is no ordering constraint between them.


BotHandler

Package: uz.osoncode.easygram.core.handler

Low-level update processing strategy. The framework auto-discovers and uses @BotController methods via built-in BotHandler implementations. Only implement this interface directly for advanced custom routing that cannot be expressed with handler annotations.

public interface BotHandler extends Comparable<BotHandler> {

/** Lower value = evaluated first. Default: Integer.MAX_VALUE. */
default int getOrder() { return Integer.MAX_VALUE; }

/** Return true if this handler can process the given request. */
boolean supports(BotRequest botRequest);

/** Process the update and populate the response. */
void handle(BotRequest botRequest, BotResponse botResponse)
throws InvocationTargetException, IllegalAccessException;

/** Human-readable description for logging. */
String info();
}

BotHandlerCondition

Package: uz.osoncode.easygram.core.handler

Functional predicate that decides whether a handler should process a given request. The framework provides two built-in conditions: BotMetaDataCondition (annotation matching) and BotChatStateCondition (state guard). Custom conditions are contributed via BotHandlerConditionContributor.

@FunctionalInterface
public interface BotHandlerCondition {
boolean matches(BotRequest botRequest);
}
// Ad-hoc usage — requires admin
BotHandlerCondition adminOnly =
request -> adminService.isAdmin(request.getUser().getId());

BotInlineQueryMatcher

Package: uz.osoncode.easygram.core.handler.inlinequery

Strategy for matching @BotInlineQuery annotation values against an incoming inline-query update. The default implementation does exact-text comparison. With core-i18n on the classpath, a locale-aware implementation is registered that treats values as message-bundle keys.

@FunctionalInterface
public interface BotInlineQueryMatcher {

/**
* Returns true if the inline query matches at least one annotation value.
* An empty values array means "match all" — this method is not called in that case.
*/
boolean matches(String[] values, BotRequest request);
}
// Custom case-insensitive matcher
@Bean
public BotInlineQueryMatcher caseInsensitiveMatcher() {
return (values, request) -> {
String query = request.getUpdate().getInlineQuery().getQuery();
return Arrays.stream(values).anyMatch(v -> query.equalsIgnoreCase(v));
};
}

BotReplyButtonMatcher

Package: uz.osoncode.easygram.core.handler.message.replybutton

Strategy for matching @BotReplyButton annotation values against an incoming message text. The default implementation does exact-text comparison. With core-i18n, values are resolved as message-bundle keys per user locale.

@FunctionalInterface
public interface BotReplyButtonMatcher {

/**
* Returns true if the incoming message text matches one of the annotation values.
*/
boolean matches(String[] values, BotRequest request);
}
@Bean
public BotReplyButtonMatcher trimmedMatcher() {
return (values, request) -> {
String text = request.getUpdate().getMessage().getText().trim();
return Arrays.asList(values).contains(text);
};
}

BotMarkupRegistry

Package: uz.osoncode.easygram.core.markup

Registry that maps markup IDs (and chat-state names) to keyboard factory functions. Populated at startup by scanning @BotMarkup methods. The default implementation is InMemoryBotMarkupRegistry. Override with a custom @Bean for distributed or database-backed registries.

public interface BotMarkupRegistry {

/** Register a factory under an ID. */
void register(String id, Function<BotRequest, ReplyKeyboard> factory);

/** Resolve the keyboard for id using the given request context (may be null for static markups). */
ReplyKeyboard resolve(String id, BotRequest request);

/** Returns true if an ID is registered. */
boolean contains(String id);

/** Register as the default keyboard for a chat state (called by BotMarkupLoader). */
default void registerForState(String state, Function<BotRequest, ReplyKeyboard> factory) {}

/** Resolve the keyboard registered as default for the given chat state. */
default ReplyKeyboard resolveByState(String state, BotRequest request) { return null; }
}

MarkupAware

Package: uz.osoncode.easygram.core.markup

Marker interface implemented by all reply types that support carrying markup: PlainReply and LocalizedReply. Enables the framework to apply markup to any return type uniformly.

Markup resolution precedence:

  1. isRemoveMarkup() — sends ReplyKeyboardRemove; overrides everything (triggered by @BotClearMarkup)
  2. getKeyboard() non-null — attached directly, no registry lookup
  3. getMarkupId() non-null — registry lookup, optionally with getMarkupParams() forwarded via BotMarkupContext
  4. @BotReplyMarkup annotation — fallback; only applied when neither keyboard nor markup ID is set
public interface MarkupAware {
String getMarkupId();
Map<String, Object> getMarkupParams();
ReplyKeyboard getKeyboard();
boolean isRemoveMarkup();
boolean isEditMessage(); // true = edit callback-query message (since 0.0.2)

MarkupAware withMarkup(String markupId);
MarkupAware withMarkup(String markupId, Map<String, Object> params);
MarkupAware withKeyboard(ReplyKeyboard keyboard);
MarkupAware removeMarkup();
}

i18n Services (core-i18n)

All i18n services live in uz.osoncode.easygram.core.i18n and are auto-configured by BotI18nAutoConfiguration when core-i18n is on the classpath. All are @ConditionalOnMissingBean — override any with your own @Bean.


BotLocaleResolver

Resolves the Locale for a given BotRequest. The default implementation uses the language_code field from the Telegram User object.

public interface BotLocaleResolver {
Locale resolve(BotRequest request);
}
// Custom: resolve from a database user preference
@Bean
public BotLocaleResolver dbLocaleResolver(UserRepository users) {
return request -> users.findLocale(request.getUser().getId());
}

BotMessageSource

Locale-aware wrapper around Spring's MessageSource. Inject wherever you need to look up i18n messages outside of a handler return type.

public interface BotMessageSource {

String getMessage(String code, BotRequest request, Object... args);
String getMessage(String code, Locale locale, Object... args);
String getOrDefault(String code, String defaultMessage, BotRequest request, Object... args);
Locale resolveLocale(BotRequest request);
}
@BotController
@RequiredArgsConstructor
public class NotifyController {

private final BotMessageSource messageSource;
private final TelegramClient telegramClient;

@BotCommand("/notify")
public void onNotify(BotRequest request) {
String text = messageSource.getMessage("notification.ready", request);
telegramClient.execute(
SendMessage.builder().chatId(request.getChat().getId()).text(text).build()
);
}
}

BotKeyboardFactory

Package: uz.osoncode.easygram.core.i18n.keyboard

Fluent factory for building localized inline and reply keyboards. Inject into @BotConfiguration markup factory methods or any Spring bean.

Inline keyboard builder

// Start a builder scoped to the request locale
BotKeyboardFactory.InlineKeyboardBuilder builder = factory.inline(request);
// Or: factory.inline(locale)

builder
.row("btn.yes", "cb_yes", "btn.no", "cb_no") // alternating (messageKey, callbackData) pairs
.row(myCustomButton)
.build(); // → InlineKeyboardMarkup

.row(String... textAndCallbackPairs) — pairs are interleaved: (messageKey₁, callbackData₁, messageKey₂, callbackData₂, …).

Reply keyboard builder

BotKeyboardFactory.ReplyKeyboardBuilder builder = factory.reply(request);

builder
.row("btn.option1", "btn.option2") // message keys → resolved to localized button labels
.row("btn.cancel")
.resizeKeyboard(true) // default: true
.oneTimeKeyboard(false) // default: false
.build(); // → ReplyKeyboardMarkup

Single button factories

InlineKeyboardButton btn = factory.inlineButton("btn.details", "cb_details", request);
KeyboardButton btn = factory.replyButton("btn.share_contact", request);

// Dynamic inline buttons — stores payload via BotDynamicCallbackQueryService (since 0.0.4)
InlineKeyboardButton dynBtn = factory.dynamicInlineButton(
"btn.buy",
BotDynamicCallbackData.builder().type("product_buy").put("id", productId).build(),
request);

// Or with explicit Locale
InlineKeyboardButton dynBtn = factory.dynamicInlineButton(
"btn.buy",
BotDynamicCallbackData.builder().type("product_buy").put("id", productId).build(),
locale);

InlineKeyboardBuilder — dynamic rows

// Dynamic row — stores payloads, generates UUID keys automatically (since 0.0.4)
keyboardFactory.inline(request)
.dynamicRow(
"btn.buy", BotDynamicCallbackData.builder().type("buy").put("id", 1L).build(),
"btn.view", BotDynamicCallbackData.builder().type("view").put("id", 1L).build()
)
.build();

Example — localized markup factory:

@BotConfiguration
@RequiredArgsConstructor
public class LocalizedMarkups {

private final BotKeyboardFactory keyboardFactory;

@BotMarkup("main_menu")
public ReplyKeyboard mainMenu(BotRequest request) {
return keyboardFactory.reply(request)
.row("menu.catalog", "menu.cart", "menu.profile")
.row("menu.support")
.build();
}

@BotMarkup("confirm_kb")
public ReplyKeyboard confirmKeyboard(BotRequest request) {
return keyboardFactory.inline(request)
.row("btn.yes", "confirm_yes", "btn.no", "confirm_no")
.build();
}
}

Provider Interfaces

All provider interfaces are in uz.osoncode.easygram.core.provider and are @FunctionalInterface. Register a Spring @Bean of any provider type to replace the default implementation.


EasygramTelegramClientProvider

Supplies the TelegramClient used to send API replies. Override to use a custom HTTP client, proxy, or test stub.

@FunctionalInterface
public interface EasygramTelegramClientProvider {
/** Create or return a TelegramClient for the given bot token. */
TelegramClient provide(String botToken);
}
@Bean
public EasygramTelegramClientProvider easygramTelegramClientProvider() {
return botToken -> new OkHttpTelegramClient(
customObjectMapper(),
customHttpClient(),
botToken,
TelegramUrl.DEFAULT_URL);
}

EasygramOkHttpClientProvider

Supplies the OkHttpClient for all outbound Telegram API calls. Override to configure timeouts, interceptors, TLS, or a proxy.

@FunctionalInterface
public interface EasygramOkHttpClientProvider {
OkHttpClient provide();
}
@Bean
public EasygramOkHttpClientProvider easygramOkHttpClientProvider() {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.addInterceptor(new LoggingInterceptor())
.build();
return () -> client;
}

EasygramObjectMapperProvider

Supplies the Jackson ObjectMapper for Telegram API payload serialisation. Override to register custom modules or configure naming strategy.

@FunctionalInterface
public interface EasygramObjectMapperProvider {
ObjectMapper provide();
}
@Bean
public EasygramObjectMapperProvider easygramObjectMapperProvider() {
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return () -> mapper;
}

EasygramExecutorServiceProvider

Supplies the ExecutorService for processing incoming updates. Override to control thread-pool size, naming, or rejection policy.

@FunctionalInterface
public interface EasygramExecutorServiceProvider {
ExecutorService provide();
}
@Bean
public EasygramExecutorServiceProvider easygramExecutorServiceProvider() {
ExecutorService executor = Executors.newFixedThreadPool(4,
new ThreadFactoryBuilder().setNameFormat("bot-worker-%d").build());
return () -> executor;
}

Important: always return the same ExecutorService instance on every call — the framework calls provide() once at startup and shuts it down when the context closes.


EasygramTelegramUrlProvider

Supplies the Telegram API base URL. Override to point the bot at a local Bot API server.

@FunctionalInterface
public interface EasygramTelegramUrlProvider {
TelegramUrl provide();
}
@Bean
public EasygramTelegramUrlProvider easygramTelegramUrlProvider() {
return () -> new TelegramUrl("https://my-local-bot-api.example.com/");
}

When no custom bean is present, defaults to TelegramUrl.DEFAULT_URL.


EasygramKafkaProducerFactoryProvider

Package: uz.osoncode.easygram.messaging.kafka.provider Module: messaging-api

Supplies the ProducerFactory<Object, Object> used to create Kafka producers when publishing updates. Override to customise serializers, SSL, interceptors, or other producer properties. (Since 0.0.6)

@FunctionalInterface
public interface EasygramKafkaProducerFactoryProvider {
ProducerFactory<Object, Object> provide();
}
@Bean
public EasygramKafkaProducerFactoryProvider easygramKafkaProducerFactoryProvider() {
return () -> {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return new DefaultKafkaProducerFactory<>(props);
};
}

Registered with @ConditionalOnMissingBean — the default uses properties from easygram.kafka.*.


EasygramKafkaConsumerFactoryProvider

Package: uz.osoncode.easygram.messaging.kafka.provider Module: messaging-api

Supplies the ConsumerFactory<Object, Object> used when creating the programmatic Kafka listener container. Override to customise deserializers, group ID, SSL, or other consumer properties. (Since 0.0.6)

@FunctionalInterface
public interface EasygramKafkaConsumerFactoryProvider {
ConsumerFactory<Object, Object> provide();
}
@Bean
public EasygramKafkaConsumerFactoryProvider easygramKafkaConsumerFactoryProvider() {
return () -> {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "my-bot-group");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
return new DefaultKafkaConsumerFactory<>(props);
};
}

Registered with @ConditionalOnMissingBean — the default uses properties from easygram.kafka.*.


EasygramRabbitConnectionFactoryProvider

Package: uz.osoncode.easygram.messaging.rabbit.provider Module: messaging-api

Supplies the RabbitMQ ConnectionFactory used when creating the programmatic listener container and the RabbitTemplate for publishing. Override to customise host, port, virtual host, TLS, or AMQP connection tuning. (Since 0.0.6)

@FunctionalInterface
public interface EasygramRabbitConnectionFactoryProvider {
ConnectionFactory provide();
}
@Bean
public EasygramRabbitConnectionFactoryProvider easygramRabbitConnectionFactoryProvider() {
return () -> {
CachingConnectionFactory factory = new CachingConnectionFactory("rabbitmq.example.com");
factory.setPort(5672);
factory.setUsername("bot");
factory.setPassword("secret");
factory.setVirtualHost("/bots");
return factory;
};
}

Registered with @ConditionalOnMissingBean — the default uses properties from easygram.rabbit.*.


Advanced SPI

These types are used internally or by advanced framework extensions.


BotMetaDataResolver

Package: uz.osoncode.easygram.core.handler.metadataresolver

Generic strategy for matching a specific handler annotation against an incoming BotRequest. Each implementation binds to one annotation type T and encodes the matching logic.

public interface BotMetaDataResolver<T extends Annotation> {
/** Returns the annotation type this resolver handles. */
Class<T> getAnnotationType();

/** Returns true if this annotation's handler should handle the given request. */
boolean support(BotRequest botRequest, T annotation);
}

Two sub-interfaces partition resolvers into two registries:

InterfacePurpose
BotMetaDataSpecResolver<T>Specific/non-default resolvers — used with explicit match criteria (e.g. @BotCommand("/start"))
BotMetaDataDefaultResolver<T>Default/fallback resolvers — consulted only when no specific resolver matches (e.g. @BotDefaultCommand)

Implement and register as a @Bean to support custom routing annotations.


BotHandlerInvocationContext

Package: uz.osoncode.easygram.core.handler.invocation

Mutable context passed through the BotHandlerInvocationFilter chain for a single handler method execution.

BotRequest getRequest()    // the current bot request
BotResponse getResponse() // the mutable response
Method getMethod() // the handler method being invoked
Object getBean() // the controller bean owning the method
Object getReturnValue() // value produced by the method (null before MethodInvocationFilter sets it)
void setReturnValue(Object value) // allows filters to transform the return value

BotHandlerException

Package: uz.osoncode.easygram.core.exception

Unchecked exception thrown when a bot handler method cannot be invoked via reflection. Wraps the underlying cause so it propagates through the dispatch chain without checked exception declarations.

public class BotHandlerException extends RuntimeException {
public BotHandlerException(String message, Throwable cause) { ... }
}

BotConfigurer

Package: uz.osoncode.easygram.core.bot

Immutable record that carries shared infrastructure objects available to framework components at startup. Inject this record to inspect the active transport type or access the shared ObjectMapper.

public record BotConfigurer(
ObjectMapper objectMapper, // shared Jackson ObjectMapper
BotTransportType transportType // active transport (LONG_POLLING, WEBHOOK, etc.)
) {}
@Component
@RequiredArgsConstructor
public class MyComponent {
private final BotConfigurer botConfigurer;

public void init() {
if (botConfigurer.transportType() == BotTransportType.LONG_POLLING) {
// long-polling specific setup
}
}
}

BotTransportType

Package: uz.osoncode.easygram.core.bot

Enumerates the supported update-delivery transports. Set via easygram.update.transport.

ConstantDescription
LONG_POLLINGBot repeatedly calls getUpdates (default — omit update block entirely)
WEBHOOKTelegram pushes updates to an HTTPS endpoint

Observability (core-observability)

Auto-configured when core-observability is on the classpath via BotActuatorAutoConfiguration and ObservabilityAutoConfiguration.

ComponentDescription
BotHealthIndicatorSpring Boot actuator health check — reports UP/DOWN based on bot connectivity
BotInfoContributorActuator /info endpoint — exposes bot username, id, and active transport type
BotObservabilityFilterBotFilter at BotFilterOrder.OBSERVATION — wraps every update in a Micrometer observation span; emits easygram.update timing metric and easygram.update.error_total counter (tagged with exception class name since 0.0.7). MeterRegistry is optional since 0.0.7 — module starts gracefully without Micrometer.

All three beans are @ConditionalOnMissingBean — replace any with a custom @Bean.


Filter Order Constants

Class: BotFilterOrder (uz.osoncode.easygram.core.filter)

ConstantValueBuilt-in filter
MDC_CONTEXTInteger.MIN_VALUESets bot.update.id and bot.transport MDC keys
CONTEXT_SETTERMIN_VALUE + 1Sets User and Chat on BotRequest; enriches MDC
OBSERVATIONMIN_VALUE + 2Micrometer observation span (core-observability)
API_SENDERMIN_VALUE + 3Executes queued BotApiMethod calls via TelegramClient
PUBLISHINGMIN_VALUE + 1000Forwards update to message broker (messaging-api)
(custom default)MAX_VALUEDefault for user-defined filters — runs last

Custom filters that need to run after context is set but before the handler should use a value between API_SENDER and PUBLISHING (e.g. 0 or 100).


Invocation Filter Order Constants

Class: BotHandlerInvocationFilterOrder (uz.osoncode.easygram.core.handler.invocation)

ConstantValueBuilt-in filter
METHOD_INVOCATIONInteger.MIN_VALUEResolves args and invokes the controller method
MARKUP_APPLICATIONMIN_VALUE + 1Applies @BotReplyMarkup / @BotClearMarkup to return value
RETURN_TYPE_DISPATCHMIN_VALUE + 2Routes return value to BotReturnTypeHandler
CHAT_STATE_UPDATEMIN_VALUE + 3Applies @BotForwardChatState / @BotClearChatState

Configuration Properties

Required

easygram:
token: YOUR_BOT_TOKEN # From @BotFather — required for all transports

Transport

easygram:
update:
transport: LONG_POLLING # Default — omit block entirely for long-polling
# Options: LONG_POLLING | WEBHOOK

Long-Polling

Long-polling has no additional configurable properties. The polling behaviour (timeout, batch size, back-off) is handled internally by the telegrambots library.

Webhook

easygram:
update:
transport: WEBHOOK
webhook:
url: https://my-bot.example.com/webhook # Required — publicly reachable HTTPS URL
path: /webhook # Local handler path (default: /webhook)
secret-token: ${WEBHOOK_SECRET} # Recommended — validates Telegram requests
require-secret-token: false # true → fail fast at startup if secret-token is blank (since 0.0.7)
max-body-bytes: 0 # 0 = unlimited; e.g. 1048576 for 1 MB limit → HTTP 413 (since 0.0.7)
max-connections: 40
drop-pending-updates: false
unregister-on-shutdown: false

Broker Publishing

easygram:
messaging:
type: PRODUCER # Required: PRODUCER or CONSUMER
forward-only: false # true = publish to broker only, skip local handler dispatch
producer:
type: KAFKA # KAFKA | RABBIT
kafka:
topic: easygram-updates
create-if-absent: true
partitions: 1
replication-factor: 1
rabbit:
exchange: easygram-exchange
routing-key: easygram.updates
queue: easygram-updates
create-if-absent: true

Broker Consumer

easygram:
messaging:
type: CONSUMER # Required: PRODUCER or CONSUMER
consumer:
type: KAFKA # KAFKA | RABBIT
kafka:
topic: easygram-updates
group-id: my-bot-consumer # consumer group ID (default: easygram-bot)
rabbit:
exchange: easygram-exchange
queue: easygram-updates

i18n (core-i18n)

easygram:
i18n:
default-locale: en # Fallback locale when user language_code is absent

spring:
messages:
basename: messages/bot # Classpath location of .properties files (e.g. messages/bot_en.properties)
encoding: UTF-8
cache-duration: 3600

Need more detail? Check the module READMEs or open an issue on GitHub.