Skip to main content
Version: 0.0.2

API Reference

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


Table of Contents


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;
}

@BotContact

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());
}
}

@BotChatMember

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.

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

@BotChatMember
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, PlainTextTemplate, 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.";
}

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/@BotClearMarkup applied by BotStringReturnHandler
PlainReplycoreSendMessage with optional markup. Implements MarkupAware
PlainTextTemplatecoreSendMessage with #{index} token substitution. Implements MarkupAware
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
LocalizedTemplatecore-i18nMixed ${key} / #{index} template resolved per locale. Implements MarkupAware

PlainReply — Fluent API

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

PlainReply.of("Hello!")                          // text only
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

// Builder
PlainReply.builder()
.text("Choose:")
.markupId("main_menu")
.build()

PlainTextTemplate — Fluent API

Uses #{index} positional tokens (0-based) for value substitution. Same markup methods as PlainReply.

PlainTextTemplate.of("Hello, #{0}! You have #{1} messages.", user.getFirstName(), count)
PlainTextTemplate.of("Order ##{0} ready.", orderId).withMarkup("order_kb")

// Builder
PlainTextTemplate.builder()
.template("Hello, #{0}! Order ##{1} is ready.")
.args(user.getFirstName(), orderId)
.markupId("order_kb")
.build()

LocalizedReply — Fluent API (core-i18n)

Key is resolved via BotMessageSource using the request locale.

LocalizedReply.of("welcome.message", user.getFirstName())
LocalizedReply.of("choose.option").withMarkup("main_menu")
LocalizedReply.of("confirm.prompt").withMarkup("confirm_kb", Map.of("id", itemId))

// Builder
LocalizedReply.builder()
.key("welcome.message")
.args(user.getFirstName())
.markupId("main_menu")
.build()

LocalizedTemplate — Fluent API (core-i18n)

Supports mixed ${messageKey} bundle lookups and #{index} positional arg substitution.

LocalizedTemplate.of("${welcome.title}\n\nHello, #{0}!", user.getFirstName())
LocalizedTemplate.of("${stats.header}\n\nMessages: #{0}", count).withMarkup("stats_menu")

// Builder
LocalizedTemplate.builder()
.template("${stats.header}\n\nMessages: #{0}\nCommands: #{1}")
.args(messages, commands)
.markupId("stats_menu")
.build()

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();
}

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.


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-api

SPI for publishing raw Telegram updates to a message broker. Implement and register as a @Bean to replace or augment the built-in Kafka/RabbitMQ publishers.

public interface BotUpdatePublisher {
void publish(BotRequest request) throws Exception;
}

See Broker Publishing for examples.


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.

public interface BotStartTrigger {
void execute(User bot, TelegramClient telegramClient);
}
@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());
}
}

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);

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();
}
}

Filter Order Constants

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

ConstantValueBuilt-in filter
CONTEXT_SETTERInteger.MIN_VALUESets User and Chat on BotRequest
OBSERVATIONMIN_VALUE + 1Micrometer observation span (core-observability)
API_SENDERMIN_VALUE + 2Executes queued BotApiMethod calls via TelegramClient
PUBLISHINGMIN_VALUE + 1000Forwards update to message broker (messaging-*)
(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:
transport: LONG_POLLING # Default
# Options: LONG_POLLING | WEBHOOK | KAFKA_CONSUMER | RABBIT_CONSUMER

Long-Polling

Long-polling has no additional configurable properties. The polling behaviour (timeout, batch size, back-off) is handled internally by the telegrambots library and cannot be overridden via application.yml.

Webhook

easygram:
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
max-connections: 40
drop-pending-updates: false
unregister-on-shutdown: false

Broker Publishing

easygram:
messaging:
forward-only: false # true = publish to broker only, skip local handler dispatch
producer:
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:
kafka:
topic: easygram-updates
group-id: my-bot-consumer
rabbit:
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.