Skip to main content
Version: 0.0.6

Migrating from 0.0.5 to 0.0.6

Overview

Version 0.0.6 contains breaking changes (template type removal) alongside new additive features:

  1. BREAKING — PlainTextTemplate removed — migrate to PlainReply with {n} MessageFormat args
  2. BREAKING — LocalizedTemplate removed — migrate to LocalizedReply with args or BotMessageSource
  3. @BotParseMode annotation — control Telegram parse mode from any handler method
  4. sendMessage delivery optionsdisableNotification, protectContent, messageThreadId, replyParameters, linkPreviewOptions on PlainReply and LocalizedReply
  5. Configurable Telegram API URL — redirect requests to a local or self-hosted Bot API server
  6. Messaging factory provider SPI — inject custom Kafka / RabbitMQ factory instances into Easygram

BREAKING — PlainTextTemplate removed

PlainTextTemplate and BotPlainTextTemplateReturnTypeHandler have been removed. Migrate to PlainReply, which now has built-in MessageFormat arg support. Token notation changes: #{n}{n}.

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

// After
return PlainReply.of("Welcome, {0}! You have {1} messages.", name, count);

Builder and wither equivalents:

PlainReply.builder().text("Hi, {0}!").args(name).build();
PlainReply.of("Hi, {0}!").withArgs(name);

BREAKING — LocalizedTemplate removed

LocalizedTemplate and BotLocalizedTemplateReturnTypeHandler have been removed. Also update .properties bundle files to use standard {n} placeholders instead of #{n}.

Option 1 — single key with positional args:

// Before
return LocalizedTemplate.of("${register.complete} #{0}", city);

// After
return LocalizedReply.of("register.complete", city);
// messages/bot_en.properties: register.complete=Registration complete. City: {0}

Option 2 — multi-key messages (inject BotMessageSource):

// Before
return LocalizedTemplate.of("${welcome.title}\n\n${welcome.body}\n\nHello, #{0}!", name);

// After
@BotController
@RequiredArgsConstructor
public class WelcomeController {
private final BotMessageSource messageSource;

@BotCommand("/start")
public String onStart(User user, BotRequest request) {
return messageSource.getMessage("welcome.title", request) + "\n\n"
+ messageSource.getMessage("welcome.body", request) + "\n\n"
+ "Hello, " + user.getFirstName() + "!";
}
}

Bundle file migration — change #{n}{n} in every .properties file:

# Before
greeting=Hello, #{0}!
# After
greeting=Hello, {0}!

New Feature 1 — @BotParseMode Annotation

Place @BotParseMode on any handler method to set the Telegram parse_mode field on the outgoing SendMessage or EditMessageText call. Valid values are "HTML", "MarkdownV2", and "Markdown" (legacy).

With String returns

@BotParseMode("HTML")
@BotCommand("/start")
public String start(User user) {
return "<b>Hello, " + user.getFirstName() + "!</b>";
}

@BotParseMode("MarkdownV2")
@BotText("help")
public String help() {
return "*Bold* and _italic_ text";
}

With PlainReply

@BotParseMode("HTML")
@BotCommand("/menu")
public PlainReply menu() {
return PlainReply.of("<b>Choose an option:</b>")
.withMarkup("main_menu");
}

With LocalizedReply (core-i18n)

@BotParseMode("HTML")
@BotCommand("/profile")
public LocalizedReply profile() {
return LocalizedReply.of("profile.text");
// messages/bot_en.properties: profile.text=<b>Your profile</b>
}

Fluent API (without annotation)

You can also set parse mode directly on the reply object — useful when the mode depends on runtime conditions:

public PlainReply reply(boolean useHtml) {
String text = useHtml ? "<b>Bold</b>" : "**Bold**";
String mode = useHtml ? "HTML" : "MarkdownV2";
return PlainReply.of(text).withParseMode(mode);
}

Both PlainReply and LocalizedReply support .withParseMode(String):

PlainReply.of("<b>text</b>").withParseMode("HTML")
LocalizedReply.of("msg.key").withParseMode("HTML") // core-i18n

Combining with @BotReplyMarkup

@BotParseMode and @BotReplyMarkup can appear on the same method in any order:

@BotParseMode("HTML")
@BotReplyMarkup("main_menu")
@BotCommand("/start")
public String start() {
return "<b>Welcome!</b> Choose an option:";
}

New Feature 2 — sendMessage Delivery Options

PlainReply and LocalizedReply now expose five additional sendMessage control fields as wither methods and Builder setters. All are null by default — fully backward-compatible.

FieldTypePurpose
disableNotificationBooleanSend silently (no sound/vibration)
protectContentBooleanDisable forwarding and saving
messageThreadIdIntegerTarget a forum topic thread
replyParametersReplyParametersReply to a specific message
linkPreviewOptionsLinkPreviewOptionsControl link preview display
// Send silently
return PlainReply.of("Quiet update.").withDisableNotification(true);

// Reply to a message in a forum thread
return PlainReply.of("Done!")
.withMessageThreadId(topicId)
.withReplyParameters(ReplyParameters.builder().messageId(origId).build());

// Builder — protect content
return LocalizedReply.builder()
.key("welcome")
.protectContent(true)
.build();

New Feature 3 — Configurable Telegram API URL

Easygram now supports redirecting API requests to a custom URL — useful for local Bot API servers, test environments, or corporate proxies.

Properties

easygram:
telegram-url:
host: my-local-bot-api.example.com # custom hostname; omit to use Telegram's default
port: 8443 # optional; only used when host is set
schema: https # optional; only used when host is set
test-server: false # set to true to use Telegram test environment

All four properties are optional. When host is absent, TelegramUrl.DEFAULT_URL is used (the standard api.telegram.org).

SPI override

For complete control, provide a BotTelegramUrlProvider bean — it takes precedence over the properties above:

@Bean
public BotTelegramUrlProvider customTelegramUrl() {
return () -> new TelegramUrl("https", "my-bot-api.example.com", 443);
}

New Feature 4 — Messaging Factory Provider SPI

Three new SPI interfaces let you inject a fully configured Kafka or RabbitMQ factory instance, overriding the Spring Boot auto-configured defaults.

BotKafkaProducerFactoryProvider

Replaces the ProducerFactory used to build the Kafka template:

@Bean
public BotKafkaProducerFactoryProvider kafkaProducerFactory(SslBundles sslBundles) {
return () -> {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-cluster:9093");
// custom SSL, SASL, etc.
return new DefaultKafkaProducerFactory<>(props);
};
}

BotKafkaConsumerFactoryProvider

Replaces the ConsumerFactory used to build the Kafka listener:

@Bean
public BotKafkaConsumerFactoryProvider kafkaConsumerFactory() {
return () -> {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-cluster:9093");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "my-consumer-group");
return new DefaultKafkaConsumerFactory<>(props);
};
}

BotRabbitConnectionFactoryProvider

Replaces the RabbitMQ ConnectionFactory used for both publishing and consuming:

@Bean
public BotRabbitConnectionFactoryProvider rabbitConnectionFactory() {
return () -> {
CachingConnectionFactory factory = new CachingConnectionFactory("rabbit-cluster");
factory.setVirtualHost("/my-vhost");
factory.setUsername("bot-user");
factory.setPassword("secret");
return factory;
};
}

All three providers are @ConditionalOnMissingBean — declaring any one of them replaces only that factory; the other two continue to use defaults.

Topic and exchange are now optional

easygram.messaging.kafka.topic and easygram.messaging.rabbit.queue / easygram.messaging.rabbit.exchange are now fully optional. The defaults are:

PropertyDefault
easygram.messaging.kafka.topiceasygram-updates
easygram.messaging.rabbit.queueeasygram-updates
easygram.messaging.rabbit.exchangeeasygram-exchange
easygram.messaging.rabbit.routing-keyeasygram.updates

No change is required if you were already relying on these defaults.


New Feature 5 — BotReplyAction SPI (Internal refactor, fully additive)

The internals of PlainReply and LocalizedReply dispatch have been refactored to use a filter-chain pattern. This is a non-breaking, additive change — all existing bot code continues to work without modification.

What changed internally

Previously, BotPlainReplyReturnTypeHandler and BotLocalizedReplyReturnTypeHandler contained a hard-coded if/else block that chose between sendMessage, editMessageText, and answerCallbackQuery. Extending the dispatch logic required modifying framework code.

Now both handlers delegate to BotReplyActionChain — a sorted list of BotReplyAction beans:

BotReplyActionChain (order-sorted)
├── SendMessageReplyAction order=10
├── EditMessageReplyAction order=10
└── AnswerCallbackQueryReplyAction order=20

ReplyOptions is a new immutable record that groups all 15 shared reply options (markup, delivery, callback options). Both PlainReply and LocalizedReply delegate to it internally — adding a future Telegram Bot API option only requires a new field in ReplyOptions, not changes to both reply classes.

New extension point

Add your own Telegram Bot API call to the dispatch pipeline without modifying any existing code — just register a BotReplyAction bean:

@Component
public class NotifySupervisorReplyAction implements BotReplyAction {

@Override
public boolean supports(ReplyOptions options, BotRequest request) {
return options.protectContent() != null && options.protectContent();
}

@Override
public void execute(BotRequest request, BotResponse response,
String resolvedText, ReplyOptions options) {
// send a copy to supervisor chat, log to audit, etc.
}

@Override
public int getOrder() { return 30; }
}

See the Custom Reply Actions guide for full documentation.

ReplyOptions accessor

Both PlainReply and LocalizedReply now expose a getOptions() method returning the underlying ReplyOptions. This is primarily intended for custom BotReplyAction implementations and framework-internal use.


Summary Table

ChangeTypeAction required
@BotParseMode annotation on handler methodsNew featureNone — additive
.withParseMode(String) on all MarkupAware typesNew featureNone — additive
easygram.telegram-url.* propertiesNew featureNone — additive
BotTelegramUrlProvider SPINew featureNone — additive
BotKafkaProducerFactoryProvider SPINew featureNone — additive
BotKafkaConsumerFactoryProvider SPINew featureNone — additive
BotRabbitConnectionFactoryProvider SPINew featureNone — additive
Topic / exchange properties are now optionalEnhancementNone — defaults unchanged
BotReplyAction SPI + BotReplyActionChainNew SPI / internal refactorNone — fully additive
ReplyOptions record grouping shared reply fieldsInternal refactorNone — getOptions() exposed for extension