Repository navigation
Releases
v1.4.0
The current rules of Max, mini app backends, tools for writing bots, and production operations:
health, tracing, a WebFlux webhook, native image and non-blocking deduplication.
Added
-
Spring Boot starter: the health contributor
maxwhen Spring Boot Actuator is present
(MaxHealthAutoConfiguration). Its partbot(MaxBotHealthIndicator) isDOWNwhen long polling
ended (with the error inpollingError), when webhook mode has no endpoint or the subscription to
max.bot.webhook.urlis missing, and when the API does not answer or rejects the token; the request
to the API (getSubscriptionsorgetMyInfo) is made at most once permax.bot.health.cache-ttl
(30 seconds) and waits up tomax.bot.health.timeout(5 seconds). Its partstoragechecks the
database or Redis ofmax.bot.storage=jdbc|redis.management.health.max.enabled=falseturns it off. -
New module
maxapi-opentelemetry:MaxTracingrecords API requests as client spans
(GET /chats/{id}withhttp.request.method,url.template,http.response.status_code,
server.address,server.port,http.request.resend_count,error.type), dispatched updates as
consumer spans (process message_createdwithmessaging.system=max,max.update.type,
max.chat.id,max.update.handled,error.type) and renewed webhook subscriptions as
max.webhook.resubscribe. The span of an update is current in the coroutines of its handler, so
the requests the handler makes are its children; a webhook update handled synchronously continues
the trace of the server span. The starter createsMaxTracingwhen the application has an
OpenTelemetrybean (MaxTracingAutoConfiguration). -
MaxEventListener.updateContext(update): coroutine context elements the bot adds while it
dispatches an update, e.g. an OpenTelemetry context or an SLF4JMDCContext;compositecombines
the contexts of its listeners. TheonUpdateevent of the update is called within that context. -
Spring Boot starter: the webhook endpoint in reactive web applications (Spring WebFlux), with the
same answers as the servlet one: 200, 401, 400 and 503. Handlers run onDispatchers.Default, and
withmax.bot.webhook.async=falsethe response waits for them without blocking a thread. -
GraalVM native image:
maxapihas reflection metadata for all model classes, the bundled root CA
and the Kotlin built-ins inMETA-INF/native-image/io.github.securitytrip/maxapi; the starter
registers the same hints with Spring AOT (MaxRuntimeHintsinMETA-INF/spring/aot.factories). -
SuspendUpdateDeduplicatorwithsuspend fun markSeen(update), a non-blocking variant of
UpdateDeduplicator, set withBotConfig.suspendDeduplicator(also in the builder). The bot calls
markSeenof a deduplicator that implements both interfaces. The starter passes a
SuspendUpdateDeduplicatorbean to the bot when there is noUpdateDeduplicatorbean. -
MaxBot.isPollingandMaxBot.pollingFailure: whether long polling runs, and the error that ended it. -
Mini app backends, in the new package
ru.securitytrip.maxapi.webappofmaxapiwith no new
dependencies.WebAppData.validate(initData, botToken, maxAge)checkswindow.WebApp.initData
as https://dev.max.ru/docs/webapps/validation describes (values URL-decoded once, exactly one
hash,HMAC_SHA256(HMAC_SHA256("WebAppData", botToken), sorted key=value lines),auth_date
no older thanmaxAge, one hour by default) and returns the user, the chat,query_id,
auth_date,start_param,ipand all other fields; it throwsWebAppDataExceptionwith a
reason:MALFORMED,MISSING_HASH,DUPLICATE_HASH,INVALID_SIGNATUREorEXPIRED.
WebAppData.verifySignatureonly checks the signature and returns aBoolean. -
WebAppPhone.verifychecks the phone number fromrequestContact()of MAX Bridge
(HMAC_SHA256(botToken, "authDate=…\nphone=…\nuserId=…")), andContactAttachment.verify(also on
ContactAttachmentPayload) the contact shared with arequest_contactbutton
(HMAC_SHA256(botToken, vcf_info); checked against the live API). All hashes are compared in
constant time. -
MaxLinksbuilds deep links:bot,botStart(?start=, 1–128 characters),miniApp
(?startapp=, up to 512 characters of[A-Za-z0-9_-]) andshare(:share?text=), and throws
IllegalArgumentExceptionfor a payload Max would drop.MaxLimits.BOT_START_PAYLOAD_LENGTH,
MaxLimits.isValidOpenAppPayloadandMaxLimits.checkOpenAppPayloadare public. -
Spring Boot starter: controller parameters annotated with
@MaxWebAppUserget the checked
WebAppUserorWebAppData, and requests without validinitDataget 401.max.bot.web-app.source
says whereinitDatais (headerby default,parameterorbody), withmax.bot.web-app.header
(X-Max-Init-Data),max.bot.web-app.parameter(initData) andmax.bot.web-app.max-age(1h). -
maxapi-ktor: theMaxWebApproute plugin answers 401 to calls without validinitData(from the
X-Max-Init-Dataheader by default), andcall.maxInitData()andcall.maxWebAppUser()return
the checked data. -
examples-springhas a mini app backend with a page that calls it, anopen_appbutton, a
startapplink and a verifiedrequest_contactcontact. -
MaxLimitsholds the limits of Max, andMaxClientchecks them before sending: text of a message
or a comment up to 4000 characters; keyboards of up to 30 rows, 1 to 210 buttons and 7 buttons in
a row (3 in a row with alink,open_app,request_geo_locationorrequest_contactbutton,
as the live API enforces it); button text
of 1–128 characters,callbackandclipboardpayloads up to 1024,linkURLs up to 2048,
open_apppayloads up to 512 of[A-Za-z0-9_-]; up to 32 bot commands with names of 1–64 and
descriptions of 1–128 characters.sendMessage,editMessage,answerOnCallback,sendComment,
editComment,editMyCommandsandMaxBot.syncCommandsthrowIllegalArgumentExceptionwith
the broken limit instead of making a request. Lengths are counted in UTF-16 code units, as the
server counts them (checked against the live API: an emoji takes 2 characters). -
MaxClient.sendLongMessage(withsendLongMessageToChatandsendLongMessageToUser, in the
blocking and async facades too) andUpdateContext.replyLong(replyLongBlocking) send text
longer than 4000 characters as several messages; the keyboard and other attachments go with the
last one.TextSplitterdoes the splitting: between paragraphs, then lines, then words, never
inside a surrogate pair, and with Markdown and HTML markup closed and opened again at every cut. -
RateLimit.MAX: the limits of Max, 30 requests per second and 2 answers to button presses per
second in a chat.RateLimit.withPerChatAnswerssets the per-chat limit of answers in any
RateLimit;MaxBottells the client the chat of every press it dispatches, because
POST /answerscarries only thecallback_id. -
MaxBot.ensureWebhook(andensureWebhookBlocking) subscribes to a webhook again if Max dropped
the subscription after 8 hours without successful deliveries, and reports it to the new
MaxEventListener.onWebhookResubscribe.maxapi-micrometercounts it in
max.bot.webhook.resubscriptions. -
Spring Boot starter:
max.bot.rate-limit=none|max(defaultnone) setsRateLimit.MAX, and in
webhook mode withmax.bot.webhook.urlthe bot checks its subscription every
max.bot.webhook.check-interval(default 10 minutes,0turns it off) and subscribes again when
it is gone. -
Scenes:
scene("name") { ... }(Scene.builderin Java) describes a wizard on top of the dialog
state, so it works with anyStateStorage. Steps have a prompt, checks (validate, with an error
text or anonInvalidreaction) and an optionalinputthat reads other kinds of answers; answers
are kept inDialogState.dataunder the step names. A scene is entered withenterOnCommand,
enterOnCallbackorScene.enter(ctx, data);/backorbackButtonreturns a step,/cancelor
cancelButtonrunsonCancel, andoption(text, value)buttons answer only the current step.
BaseRouter.include(scene)adds it to a bot;SceneContextgives prompts and callbacks the handler
context withanswers. TheQuestionnaireBotexample is rewritten on a scene, with aBotTester
test. -
Keyboards:
Paginatorshows a long list page by page with a◀ 2/7 ▶row packed byCallbackData
(page and argument), andConfirmationis a "Yes / No" question withyes()andno()filters.
Both keep to the keyboard limits of Max: 7 buttons in a row, 3 with a wide button, 30 rows. -
Translationspicks texts byUpdateContext.localewith a fallback language, from maps or
ResourceBundles (Translations.fromBundles), with{0}and{name}placeholders and plural
forms (one/few/manyfor Russian, Ukrainian and Belarusian,one/otherotherwise). -
Chat registry: Max has no method that lists the chats of a bot, so with
BotConfig.chatStoreset
the bot remembers them frombot_added,bot_removed,bot_started,bot_stopped,
chat_title_changedand the first message from an unknown chat, andMaxBot.chats(vararg types)
(chatsBlocking) lists the active ones asKnownChat. Records keep the time of their event, so a
repeated or late delivery never undoes a newer one. Stores:InMemoryChatStore,
BlockingChatStorefor Java,JdbcChatStoreinmaxapi-jdbcandRedisChatStorein
maxapi-redis. These update types are requested even without handlers for them. -
Broadcasts:
Broadcaster.send(id, targets, message)(sendBlocking) sends a message, the same or
per target, to users and chats (BroadcastTarget) at up to 25 requests per second by default,
waits forRetry-Afteron 429, records 403 and 404 as unavailable without retrying, and retur...
v1.3.0
Storage for production: dialog states, the polling marker and seen updates in SQL or Redis.
Added
maxapi-jdbc:JdbcStateStorage,JdbcMarkerStoreandJdbcUpdateDeduplicatorkeep dialog
states, the polling marker and the keys of handled updates in any SQL database through a
DataSource. Only standard SQL is used;createTable()creates a missing table.maxapi-redis:RedisStateStorage(with an optional TTL),RedisMarkerStoreand
RedisUpdateDeduplicatoron Lettuce.- Spring Boot starter:
max.bot.storage=jdbc|rediswithmax.bot.jdbc.*andmax.bot.redis.*
creates those stores, andStateStorage,MarkerStoreandUpdateDeduplicatorbeans are used by
the auto-configured bot. The bot settings are now aBotConfigbean (maxBotConfig). - Both modules are in
maxapi-bom.
v1.2.0
Fixes from running the examples against the real API.
Added
MaxBot.deleteWebhook()(anddeleteWebhookBlocking()) removes every webhook subscription of
the bot.
Fixed
MessageContext.textisnullfor photos, stickers and other messages without text. Max sends an
empty string for them, sotext ?: "..."replied with an empty message (rejected with
400 proto.payload) andFilters.hasText()let such messages through.- Long polling no longer receives nothing while the bot is subscribed to a webhook: Max answers
getUpdateswith empty pages then, sostartPolling()now checks the subscriptions first and
throwsMaxException, as documented. sendMessagerejects a message without text, attachments or a forwarded message with
IllegalArgumentExceptioninstead of sending it and getting400 proto.payload: errors.required.- The Ktor example unsubscribes from the webhook when it stops and logs through
slf4j-simple. - The echo examples change the message on "Like" too, since the web client of Max does not show
callback notifications.
Changed
- Documented that the web client of Max shows neither the command menu nor callback notifications.
v1.1.0
Added
MaxTlsandMaxClientConfig.trustRussianRootCa(max.bot.trust-russian-root-cain the Spring Boot
starter): the library bundles the Russian Trusted Root CA, which the certificate of
platform-api2.max.ruis issued under, and its own HTTP client trusts it in addition to the JVM's
certificates. Connecting no longer needs a custom trust store.
v1.0.0
The public API is stable from this version on.
Added
- MIGRATION.md with the changes since 0.x, and a compatibility policy in README.
- CONTRIBUTING.md.
- Module descriptions in the API reference for the starter, Ktor, test kit and Micrometer modules.
v1.0.0-rc1
The public API is stable from this version on.
Added
- MIGRATION.md with the changes since 0.x, and a compatibility policy in README.
- CONTRIBUTING.md.
- Module descriptions in the API reference for the starter, Ktor, test kit and Micrometer modules.