diff --git a/README.md b/README.md index 1ddd962..1b4a527 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,11 @@ [![Codecov](https://codecov.io/gh/Drednote/spring-boot-starter-telegram/graph/badge.svg?token=4GGKDCSXH2)](https://codecov.io/gh/Drednote/spring-boot-starter-telegram) **Spring Boot Starter Telegram** is a library designed to simplify the setup of Telegram bots using -`Spring Boot` and `org.telegram:telegrambots` as the core dependency. It provides several key -features to facilitate the bot development process +`Spring Boot` and `org.telegram:telegrambots` as the core dependency. Using this library allows you to easily create your first bot with zero configuration. At the same time, it provides a highly modular architecture, enabling you to customize almost any functionality by creating your own Spring beans. Nearly every class used by this library can be replaced. You are free to implement any custom logic within the library's workflow. To help you navigate the wide range of settings and classes, a detailed description of all key features and classes is provided below, along with usage examples. ## Main Features -1. **Controller Update Handling**: Allows receiving updates from the bot via `TelegramController` - similar - to +1. **Controller Update Handling**: Allows receiving updates from the bot via `TelegramController` similar to `RestController` in Spring. This enables seamless integration of Telegram bot functionality with the existing Spring ecosystem. @@ -21,7 +18,7 @@ features to facilitate the bot development process configured via Java configuration. These scenarios allow the bot to process user interactions in a more organized and structured manner. -3. **Flexible Update Filters**: The library provides the capability to set up custom filters for +3. **Flexible Update Filters**: The library provides the ability to set up custom filters for individual bot updates. These filters are executed before and after the user-defined code, allowing for pre-processing and post-processing of updates. @@ -29,34 +26,14 @@ features to facilitate the bot development process `TelegramExceptionHandler`, the library offers a centralized approach for handling errors, similar to `ControllerAdvice` and `ExceptionHandler` in Spring. -## Getting started +## Navigation - [Requirements](#requirements) - [Installation](#installation) +- [Quick Start](#quick-start) - [Usage](#usage) - - [Quick Start](#quick-start) - - [Overall Information](#overall-information) - - [Update Handling](#update-handling) - - [Controllers](#controllers) - - [Scenario](#scenario) - - [Filters](#filters) - - [Response Processing](#response-processing) - - [Exception Handling](#exception-handling) - - [Argument Resolving](#argument-resolving) - - [Java types](#java-types) - - [TelegramPatternVariable](#telegrampatternvariable) - - [Telegram Scope](#telegram-scope) - - [Data Source](#data-source) - - [Primary Entities](#primary-entities) - - [Update](#update) - - [UpdateRequest](#updaterequest) - - [UpdateHandler](#updatehandler) - - [UpdateFilter](#updatefilter) - - [TelegramResponse](#telegramresponse) - - [TelegramScope](#telegramscope) -- [Additional Info](#additional-info) -- [Configuration](#configuration) -- [Dependencies](#dependencies) + - [Summary](#summary) + - [Details](#details) - [Contributing](#contributing) - [License](#license) - [Authors](#authors) @@ -118,13 +95,7 @@ dependencies { } ``` -## Usage - -### Examples - -Code examples you can find in the example's folder. - -### Quick Start +## Quick Start Add to `application.yml` your bot token and specify the name of bot @@ -188,623 +159,71 @@ public class Application { That's all! Enjoy your bot -### Overall information +## Usage -This library's implementation closely resembles the familiar structure of `Java HTTP servlets`. +This section describes the capabilities of the library for creating Telegram bots. We will start with a general overview of how messages from Telegram are processed and sent back. Below you will find a textual description of the process, as well as a diagram. + +### Summary - Upon application startup, a session is established to connect with the Telegram API, initiating the reception of updates. These updates are obtained either through a **long polling strategy** or by setting up a **webhook** that prompts Telegram to directly transmit updates to the application. -- Upon receiving an [Update](#update), a [UpdateRequest](#updaterequest) object is - generated. This object serves as the central entity throughout the subsequent update processing - pipeline. Initially, it contains only the information extracted from the [Update](#update) object. - As the processing continued, the [UpdateRequest](#updaterequest) - accumulates additional data, enriching its content. +- Upon receiving an `Update`, a `UpdateRequest` object is + generated. This object serves as the central entity throughout the further update processing + pipeline. Initially, it contains only the information extracted from the `Update` object. + As the processing continued, the `UpdateRequest` accumulates additional data, enriching its content. -- At the very beginning of the update processing chain, - the [UpdateRequest](#updaterequest) is stored in the context of the current - thread. This is done to create a [Telegram Scope](#telegram-scope). +- At the very beginning of the update processing chain, the `UpdateRequest` is stored in the context of the current + thread. This is done to create a `Telegram Scope`. -- After that, the main update processing starts with calls to [Filters](#filters). These filters - help in determining which updates should be processed further. Or you can put logic in the filter - that will be executed for each update, for example, log something +- After that, the main update processing starts with calls to `Filters`. These filters + help in determining which updates should be processed further, and at this stage it is determined what kind of request has arrived and how it needs to be processed. You can put any logic in the filter that will be executed for each update, for example, log something. - Once the updates are filtered, the available `UpdateHandler` are called in a specific order based - on their priority. There are different mechanisms available for handling updates, and you can find - more information about them in the [Update Handling](#update-handling) section + on their priority. There are different mechanisms available for handling updates such as controllers or scenario. -- After the successful processing of updates, the [Filters](#filters) are called again as part of +- After the successful processing of updates, the `Filters` are called again as part of the post-processing stage. This gives an opportunity for any additional filtering or actions to be applied. - Once all the processing and filtering are done, the response is processed using a specialized - mechanism called [Response Processing](#response-processing). This mechanism takes the defined + mechanism called `Response Processing`. This mechanism takes the defined response and performs necessary actions, such as sending it back to the user or performing any other desired logic. +- After sending a response to Telegram, `Filters` are called again, but now these are a special type of filters - conclusive filters. They are specially designed to process either responses from Telegram, or as the final stage of `Update` processing. + - Throughout the entire processing chain, there is a dedicated mechanism for handling errors called - [Exception Handling](#exception-handling). This ensures that any errors or exceptions that occur + `Exception Handling`. This ensures that any errors or exceptions that occur during the processing are properly handled and don't disrupt the flow of the program. - Additionally, some filters and handlers defined by this library require access to a database. For - this purpose, they can make use of the [Data Source](#data-source) functionality. This allows them + this purpose, they can make use of the `Data Source` functionality. This allows them to interact with the database and retrieve or store data as needed. -### Update Handling - -This library offers users two primary approaches for handling updates: [controllers](#controllers) -and [scenarios](#scenario). Both controllers and scenarios offer their own benefits, so you can -choose the one that suits your bot's requirements and your preferred coding style. -**But I recommend using both** - -> If you need to make your own handler, all you have to do is create a **spring bean** that will -> implement the `UpdateHandler` interface and set its execution priority using a spring -> annotations `@Order` if needed. Also, after successful processing of the message, it is necessary -> put in the object `UpdateRequest` response with type `TelegramResponse`, so that update -> processing can be considered successful. If this is not done, further update handlers will be -> called - ---- - -#### Controllers - ---- - -In order to begin receiving updates from **Telegram**, you will first need to create a controller -and specify the criteria for which updates you want to receive. - -To assist with the analysis, let's utilize the controller provided in -the [Quick Start](#quick-start) section: - -```java - -@TelegramController -public class MainController { - - @TelegramCommand("/start") - public String onStart(User user) { - return "Hello " + user.getFirstName(); - } - - @TelegramMessage - public String onMessage(UpdateRequest request) { - return "You sent message with types %s".formatted(request.getMessageTypes()); - } - - @TelegramMessage("My name is {name:.*}") - public String onPattern(@TelegramPatternVariable("name") String name) { - return "Hello " + name; - } - - @TelegramRequest - public TelegramResponse onAll(UpdateRequest request) { - return new GenericTelegramResponse("Unsupported command"); - } - -} -``` - -- In order for an application to register a class as a controller, it must be marked - annotation `@TelegramController` -- To receive updates, there is an annotation `@TelegramRequest`. Also, to make it easier to work - with [updates](#update), created several derived annotations. The main ones are `@TelegramCommand` - and `@TelegramMessage`. As the names imply, with the help of the first one you can only receive - commands **(Message#isCommand)**, and from the second any messages **(Update#getMessage)** -- The `@TelegramRequest` annotation's **pattern()** parameter takes a string that serves as a - matching condition for the text in the incoming message, whether it's just a message text or a - photo caption. For matcher uses `AntPathMatcher`, so you can specify - any [condition](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/AntPathMatcher.html) - in the string valid for `AntPathMatcher`. For example: `Hello*`, will match any string that starts - with `Hello`. If you do not specify **pattern()** it will be replaced with the `**` pattern and - matched with any text. -- If the [update](#update) matches with several patterns that you specified in different methods - marked with `@TelegramRequest`, than sorted will be applied: - - `TelegramRequest` without **requestType** has the lowest priority - - `TelegramRequest` without **messageType** has the lowest priority among the **requestType** - equal to `RequestType.MESSAGE` - - `TelegramRequest` with **exclusiveMessageType** set to true and with more elements specified - in **messageType** takes precedence over others - - If `TelegramRequest` has the same priority by the above conditions, than the `AntPathMatcher` - order applied - > This ensures that the most specific controllers are given precedence in the update - handling process -- Methods marked with `@TelegramRequest` annotation can accept a specific set of inputs - parameters as defined in the [Argument resolving](#argument-resolving) section -- Methods marked with `@TelegramRequest` annotation can return any object, as a result. The - response processing mechanism is detailed in the [Response Processing](#response-processing) - section -- Also, if you need to get some data from the user's message by a specific pattern, then you can use - the [TelegramPatternVariable](#telegrampatternvariable) - annotation - **Essentially `TelegramPatternVariable` works the same as `PathVariable`.** -- Any uncaught errors that occur during the execution of user code, are caught - by `ExceptionHandler`. More [here](#exception-handling). - ---- - -#### Scenario - ---- - -To create scenarios, you will need to implement the `ScenarioConfigurerAdapter` interface by -creating a **Spring bean**. This interface is the main tool for creating scenarios and allows you to -define and customize the behavior of your scenarios. - -Here example of a configuring scenario, for additional info you can see javadocs. - -```java - -@Configuration -@RequiredArgsConstructor -public class ScenarioConfig extends ScenarioConfigurerAdapter> { - - private final ScenarioRepository scenarioRepository; - - @Override - public void onConfigure(@NonNull ScenarioTransitionConfigurer> configurer) { - configurer.withExternal().inlineKeyboard() - .source(State.INITIAL).target(ASSISTANT_CHOICE) - .telegramRequest(command(ASSISTANT_SETTINGS)) - .action(settingsActionsFactory::returnSettingsMenu) - - .and().withExternal() - .source(State.INITIAL).target(State.TEST) - .telegramRequest(command("/test")) - .action(context -> "Test") - - .and().withRollback() - .source(ASSISTANT_CHOICE).target(GET_SETTINGS) - .telegramRequest(callbackQuery(SettingsKeyboardButton.GET_CURRENT)) - .action(settingsActionsFactory.getSettings()) - .rollbackTelegramRequest(callbackQuery(ROLLBACK)) - .rollbackAction(settingsActionsFactory.rollbackToSettingsMenu()) - - .and(); - } - - @Override - public void onConfigure(ScenarioConfigConfigurer> configurer) { - configurer - .withPersister(new JpaScenarioRepositoryAdapter<>(scenarioRepository)); - } - - @Override - public void onConfigure(ScenarioStateConfigurer> configurer) { - configurer.withInitialState(State.INITIAL); - } -} -``` - ---- - -### Filters - -Filters serve as both pre-processing and post-processing mechanisms for the primary stage of update -processing. They allow you to define specific criteria for filtering and manipulating updates before -and after they are processed. - -- Filters are needed for several main purposes: - - To filter updates - - To execute the code for each update - -- To control update filtering, filters can set some properties - in [UpdateRequest](#updaterequest), such as `response`. If any filter set - property `response` then the update is considered successful and an attempt will be made to send a - response -- Filters are called twice: before (pre-filters) the main [Update Handling](#update-handling) and - after (post-filters). It is important to note that, even if an error occurred during the main - processing of the update, post-filters will still be executed - -- There are two main interfaces for creating a filter: - - `PreUpdateFilter` - **spring beans** that implement this interface will be called **before** - the main [Update Handling](#update-handling) - - `PostUpdateFilter` - **spring beans** that implement this interface will be called **after** - the main [Update Handling](#update-handling) - - `ConclusivePostUpdateFilter` - **spring beans** that implement this interface will be called * - *after** - the response is sent to telegram. [see](#response-processing) - -- Also, for convenience, one interface are created. First one - `PriorityPreUpdateFilter` is - implemented from `PreUpdateFilter` and take precedence over `PreUpdateFilter` and is executed - earlier whatever returns **getPreOrder()**. - -- To add a filter, you need to create a **spring bean** that will implement the `PreUpdateFilter` - or `PostUpdateFilter` interface. - -- Additionally, it is possible to create a filter with [Telegram Scope](#telegram-scope). With this - approach, a unique filter instance is created for each update, and it remains active until the - processing of the update is completed. This allows for better separation and management of filters - for individual updates. Example: - -```java - -@Component -@TelegramScope -public class LoggingFilter implements PriorityPreUpdateFilter, PostUpdateFilter { - - private LocalDateTime startTime; - - @Override - public void preFilter(@NonNull UpdateRequest request) { - this.startTime = LocalDateTime.now(); - log.info("Receive request with id {}", request.getId()); - } - - @Override - public void postFilter(@NonNull UpdateRequest request) { - log.info("Request with id {} processed for {} ms", request.getId(), - ChronoUnit.MILLIS.between(startTime, LocalDateTime.now())); - } - - @Override - public int getPreOrder() { - return Ordered.HIGHEST_PRECEDENCE; - } -} - -``` - -### Response Processing - -After the update processing is complete, it is expected that a response will be sent to the user. To -handle this, there is a component called **Response Processing**, which follows certain rules. - -- The response represents by interface `TelegramResponse` -- **Response can only be sent if [Update](#update) has a `chatId`**. So if in update there is - no `chatId` than you should return `void` -- Any response will automatically be wrapped in the `TelegramResponse` interface and execute sending - method. Rules of wrapping: - - `void` or `null` will not trigger sending the response - - `String` will be wrapped in `GenericTelegramResponse` and execution method will send simple - text response (`SendMessage`) - - For `byte[]` same rule like for `String` except that `String` instance will be created - from `byte[]` (`new String(byte[])`) - - `BotApiMethod` and `SendMediaBotMethod` will be executed as is. - > `BotApiMethod` is an abstract class that represents sending object. - - > For `BotApiMethod` or `SendMediaBotMethod` the 'chatId' property will be automatically set - (only if it is null). If you manually set 'chatId', nothing happens - - A `TelegramResponse` object will be handled without wrapping. - - List of `TelegramResponse` will be wrapped in `CompositeTelegramResponse` and execute with - specified priority. - > Priority specified by `@Order` annotation - - For `java object` the `GenericTelegramResponse` will try to serialize it with `Jackson`. In - simple words will do `objectMapper.writeValueAsString(response)` - - For more information on wrapping rules, see the `ResponseSetter` and `GenericTelegramResponse` - classes - -> The exception is three types of response - `Collection`, `Stream`, `Flux`. For handling -> these types of response are created three additional implementations -> of `TelegramResponse` - `CompositeTelegramResponse`, `FluxTelegramResponse` -> and `StreamTelegramResponse` - -- You can create any implementation of `TelegramResponse` for sending response -- Any custom code can be written in `TelegramResponse`, but I strongly recommend using this - interface only for sending a response to **Telegram** - -### Exception Handling - -The purpose of the ExceptionHandler mechanism is to provide centralized error handling. Any errors -that occur during the processing will be caught and sent to the `ExceptionHandler` for further -handling. Here are some important rules to keep in mind: - -- To initiate error handling, the class must be annotated with `@TelegramAdvice`. Additionally, you - need to specify at least one method and annotate it with `@TelegramExceptionHandler`. -- If the user is not marked any method with the `@TelegramExceptionHandler` annotation, the default - error handling approach will be applied. -- In situations where multiple handlers are present for a particular error, a sorting mechanism is - implemented to determine the priority of method calls. The higher in the hierarchy the error - (throwable at the very top) that the handler expects, the lower the priority of the method call - will be compared to others. - > This ensures that the most specific error handlers are given precedence in the error handling - process -- Methods marked with `@TelegramExceptionHandler` annotation can accept a specific set of inputs - parameters as defined in the [Argument resolving](#argument-resolving) section -- Methods marked with `@TelegramExceptionHandler` annotation can return any object, as a result. The - response processing mechanism is detailed in the [Response Processing](#response-processing) - section - -### Argument resolving - -To provide maximum flexibility when calling custom code through the reflection mechanism, the input -parameters for any method are calculated dynamically according to the following rules: - -> If you need to add support for your custom argument, you need to create **spring bean** and -> implement `io.github.drednote.telegram.core.resolver.HandlerMethodArgumentResolver` interface - -#### Java types - -You can specify arguments based on a java type: - -- [Update](#update) and any top-level nested objects within it. `Message`, `Poll`, `InlineQuery`, - etc. -- [UpdateRequest](#updaterequest) -- `TelegramBot` or any subclasses if you need to consume current telegram bot instance -- `String` arguments will fill with the **text** property - of [UpdateRequest](#updaterequest) if it exists, or it will be `null` - > In almost all cases this will be the text of the `Message` -- `Long` arguments will fill with the **chatId** property - of [UpdateRequest](#updaterequest) -- `Throwable` arguments will fill with the **error** property - of [UpdateRequest](#updaterequest) if it exists. If no error than it will - be `null` - > This type should be only used in methods marked with `@TelegramExceptionHandler`. In other - > methods, it more likely will be `null` - -#### TelegramPatternVariable - -You can annotate certain arguments using `@TelegramPatternVariable`. Here are the rules to follow: - -- This annotation is only valid if there's text in the update (e.g., message text, photo caption). - Otherwise, it will be `null` -- This annotation supports only two Java types: `String` and `Map` -- When using `String`, the value of the template variable from the pattern is exposed -- In the case of `Map`, all template variables are exposed - -### Telegram Scope - -The functionality of `@TelegramScope` is similar to the Spring annotation `@Scope("request")`, with -the difference being that the context is created at the start of update processing instead of at the -request level. By marking a **spring bean** with this annotation, a new instance of the bean will be -created for each update processing. - -> It's important to note that each update handling is associated with a specific thread. In cases -> where you create sub-threads within the main thread, you will need to manually bind -> the `UpdateRequest` to the new thread. This can be achieved using -> the `UpdateRequestContext` -> class. - -### Data Source - -- For some filters and scenarios to work correctly, you need to save information to the database. - You can save data to the application memory, but then during the restart, all information will be - lost. Therefore, it is better if you configure the datasource using spring. - -- This library is fully based on Spring JPA in working with the database. Therefore, to support - different databases (postgres, mongo, etc.), using the implementations of `DataSourceAdapter` - interface -- If you want to add support for a database that currently is not supported, you should to - create entity and create repository extending `PermissionRepository`, `ScenarioRepository` - or `ScenarioIdRepository` - -> **Currently supported `JpaRepository`** - -> Note: To enable auto scan for jpa entities, you should manually pick main interfaces for entities -> and use `@EntityScan` annotation. To create spring data repository, you need to just implement one -> of the repository interfaces - -```java - -@EntityScan(basePackageClasses = {Permission.class, PersistScenario.class}) -@Configuration -public class JpaConfig { - -} -``` - -```java - -public interface PermissionRepository extends JpaPermissionRepository {} -``` - -### Primary Entities - -#### Update - -- `Update` is the main object that comes from the Telegram API. It contains all information about - the event that happened in the bot, whether it's a new message from the user, or changes in some - settings chat in which the bot is located. - -> Additional docs - Telegram API docs - -#### UpdateRequest - -`UpdateRequest` is a primary object that stores all information about update. Any change -that occurs during the processing of an update is written to it. Thus, if you get it in the user -code, you can find out all the information about the current update. For example, in this way: - -```java - -@TelegramController -public class Example { - - @TelegramRequest - public void onAll(UpdateRequest request) { - System.out.printf("request is %s", request); - } -} -``` - -Read more about [Telegram Controllers](#controllers). - -#### UpdateHandler - -- The `UpdateHandler` interface is the entry point for handling updates. More - in [Update Handling](#update-handling) section. - -#### UpdateFilter - -- `UpdateFilters` allow you to execute some code before or after the main `UpdateHandlers` call. The - main interfaces are `PreUpdateFilter` and `PostUpdateFilter`. More about filters [here](#filters) - -#### TelegramResponse - -- `TelegramResponse` represents the action that need to be executed to sent response to the user who - initiated the update processing. For this here special - mechanism [Response Processing](#response-processing) - -#### TelegramScope - -- `TelegramScope` is a specialization of `@Scope` for a component whose lifecycle is bound to the - current telegram update handling. More [here](#telegram-scope) - -## Additional Info - -- All packages are children of `io.github.drednote.telegram` marked with two annotations - - `@NonNullApi` and `@NonNullFields`. This means that by default all fields and APIs accept and - return non-null objects. If it is needed to return a nullable object, then the field or method - is annotated with `@Nullable` -- You can use `HasRole` annotation to check controller access - -## Configuration - -All settings tables contain 4 columns: - -- `Name` - the name of the variable as it is called in the code -- `Description` - a brief description of what this setting does -- `Default Value` - the default value of the variable -- `Required` - whether the variable is required - -> If the `Required` field is `true` and the value of the `Default Value` column is not equal to `-`, -> it means that you don't need to manually set the value for the variable. However, if you manually -> set it to `null` or any value that can be considered empty, the application will not start - -### Base properties - -| Name | Description | Default Value | Required | -|---------------|-------------------------------------------|----------------------------------------------------------|----------| -| enabled | Enable the bot. | true | true | -| token* | The token of a bot. | must be set by user | true | -| defaultLocale | The default locale for sending responses. | - | false | -| session | Session properties. | [Session properties](#session-properties) | | -| updateHandler | Properties of update handlers. | [Update handlers properties](#update-handler-properties) | | -| filters | Filters properties. | [Filters properties](#filters-properties) | | -| menu | Menu properties. | [Menu properties](#menu-properties) | | - -### Session properties - -| Name | Description | Default Value | Required | -|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|----------| -| maxThreadsPerUser | Max number of threads used for consumption messages from a telegram for concrete user. 0 - no restrictions | 1 | true | -| consumeMaxThreads | Max number of threads used for consumption messages from a telegram | 10 | true | -| maxMessagesInQueue | Limits the number of updates to be store in memory queue for update processing. 0 - no restrictions. Defaults to (consumeMaxThreads * 1.5). | 15 | true | -| updateStrategy | The strategy to receive updates from Telegram API. Long polling or webhooks. | LONG_POLLING | true | -| updateProcessorType | A type of `TelegramUpdateProcessor` using | DEFAULT (SCHEDULER) | true | -| backOffStrategy | Backoff strategy for failed requests to Telegram API. Impl of BackOff interface must be with public empty constructor | ExponentialBackOff | true | -| proxyType | The proxy type for executing requests to Telegram API. | NO_PROXY | true | -| proxyUrl | The proxy url in format `host:port` or if auth needed `host:port:username:password`. | - | false | -| cacheLiveDuration | Cache lifetime used in `DefaultTelegramBotSession` | 1 | true | -| cacheLiveDurationUnit | The `TimeUnit` which will be applied to `cacheLiveDuration` | hours | true | -| autoSessionStart | Automatically start session when spring context loaded. | true | true | -| schedulerProcessor | SchedulerTelegramUpdateProcessor properties. | [SchedulerTelegramUpdateProcessor properties](#SchedulerTelegramUpdateProcessor-properties) | false | -| longPolling | LongPolling properties. | [LongPolling properties](#Longpolling-properties) | false | - -#### SchedulerTelegramUpdateProcessor properties - -| Name | Description | Default Value | Required | -|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------| -| autoSessionStart | Automatically start `SchedulerTelegramUpdateProcessor` when `TelegramBotSession.start()` is called. | true | true | -| maxInterval | Maximum interval after which to check for new messages for processing (in milliseconds). | 1000 | true | -| minInterval | Minimum interval after which to check for new messages for processing (in milliseconds). | 0 | true | -| reducingIntervalAmount | How much to decrease the interval if messages are found for processing (in milliseconds). | 500 | true | -| increasingIntervalAmount | How much to increase the interval if no message is found for processing (in milliseconds). | 100 | true | -| waitInterval | Interval after which to check for new messages for processing while all threads are busy (in milliseconds). | 30 | true | -| idleInterval | Interval after which tasks are marked as `UpdateInboxStatus.TIMEOUT` (in milliseconds). | 30000 | true | -| checkIdleInterval | Interval to check if tasks are idle (in milliseconds). | 5000 | true | -| maxMessageInQueuePerUser | Limits the number of updates to be stored in memory queue for update processing per user (0 means no restrictions). Applied only for `UpdateProcessorType.SCHEDULER`. | 0 | true | - -#### LongPolling properties - -| Name | Description | Default Value | Required | -|----------------|------------------------------------------------------------------------------------------------------|---------------|----------| -| updateLimit | Limits the number of updates to be retrieved. Values between 1-100 are accepted | 100 | true | -| updateTimeout | Timeout in seconds for long polling. Should be positive, short polling (0) for testing purposes only | 50 | true | -| allowedUpdates | A JSON-serialized list of update types to receive. See RequestType for available update types. | - | false | - -Additional docs Telegram API docs - -### Update handler properties - -| Name | Description | Default Value | Required | -|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------------|----------| -| controllerEnabled | Enabled controller update handling. | true | true | -| scenarioEnabled | Enabled scenario update handling. | true | true | -| setDefaultErrorAnswer | If an exception occurs and no handler processes it, set InternalErrorTelegramResponse as response. | true | true | -| scenarioLockMs | The time that scenario executor will wait if a concurrent interaction was performed. 0 - no limit. | 0 | true | -| serializeJavaObjectWithJackson | Whether to serialize Java POJO objects with Jackson to JSON in GenericTelegramResponse. | true | true | -| enabledWarningForScenario | Throws an error with a warning about using scenario safe only when getMaxThreadsPerUser is set to 1, if value set to different value | true | true | - -### Filters properties - -| Name | Description | Default Value | Required | -|------------------------------|--------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------|----------| -| permission | Permission filter properties. | [Permission properties](#permission-properties) | | -| userRateLimit | How often each user can perform requests to bot. 0 = no rules. | 0 | true | -| userRateLimitUnit | The ChronoUnit which will be applied to userRateLimit. | SECONDS | true | -| userRateLimitCacheExpire | How long cache with rate limit bucket will not expire. This parameter needed just for delete staled buckets to free up memory. | 1 | true | -| userRateLimitCacheExpireUnit | The ChronoUnit which will be applied to userRateLimitCacheExpire. | HOURS | true | -| setDefaultAnswer | If response is null at the end of update handling and post filtering, set NotHandledTelegramResponse as response. | true | true | - -### Permission properties - -| Name | Description | Default Value | Required | -|-------------|---------------------------------------------------------------------------|---------------|----------| -| access | Define who has access to the bot. | ALL | true | -| defaultRole | If a user has no role, this role will be set by default. | NONE | true | -| roles | The list of roles with privileges. | - | false | -| assignRole | The map of [userId:[Role](#role)]. (Deprecated: Not safe for production.) | - | false | - -#### Role - -```java -public class Role { - - /** - * Boolean indicating if the role has basic interaction permission and can send requests to bot - */ - private boolean canRead; -} -``` - -### Menu properties - -| Name | Description | Default Value | Required | -|------------|--------------------------------------------------|---------------|----------| -| values | Map of [name:[CommandCls](#Command-properties)]. | - | false | -| sendPolicy | Send policy. | ON_STARTUP | false | - -#### Command Properties - -| Name | Description | Default Value | Required | -|--------------|-------------------------------------------------------------------------------|---------------|----------| -| text | Text for the button. | - | true | -| command | Command for the button. | - | true | -| scopes | Scopes of users for which the commands are relevant. | [DEFAULT] | true | -| languageCode | A two-letter ISO 639-1 language code. | - | false | -| userIds | Unique identifier of the target users to who apply commands. | - | false | -| chatIds | Unique identifier for the target chats or usernames of the target supergroup. | - | false | - -## Dependencies - -### Require - -These dependencies will automatically be included in your project - -`org.telegram:telegrambots-longpolling` - -`org.telegram:telegrambots-client` - -`org.springframework.boot:spring-boot-starter-web` - -`com.esotericsoftware:kryo` - -`com.github.vladimir-bukhtoyarov:bucket4j-core` - -`com.github.ben-manes.caffeine:caffeine` - -`org.apache.httpcomponents.client5:httpclient5` +### Details -### Optional +> All documentation is located in other files, so feel free to follow the links. -You can manually add them if you want to configure datasource. For what you should configure -datasource read in [Data Source](#data-source) block +Before we move on to the main features, I would like to introduce you to three basic entities that you need to know and understand. I strongly recommend reading about them, as they will be referenced later without further explanation. It will be much easier to understand the documentation if you are familiar with these core classes. -`org.springframework.boot:spring-boot-starter-data-jpa` +- [Update](docs/update-object.md#update) — is the main object that comes from the Telegram API +- [UpdateRequest](docs/update-object.md#updaterequest) - is a primary object that stores all information about update. +- [TelegramClient](docs/update-object.md#telegramclient) — a client that allows you to send anything to Telegram from your code. -or +Next, we will take a closer look at each aspect of the library. -`org.springframework.boot:spring-boot-starter-data-mongo` +- [Controllers](docs/controllers.md) — To get started, I recommend familiarizing yourself with basic interaction with Telegram via controllers. +- [Exception handling](docs/exception-handling.md) — No application is complete without error handling. +- [Filters](docs/filters.md) — This mechanism allows you to flexibly and easily customize the processing of updates from Telegram. +- [Responses](docs/response-processing.md) — In most cases, you won't need to manually configure the response mechanism, but this section provides a detailed explanation of how it works. +- [Database connection](docs/datasource.md) — Mechanisms such as scenarios, in my opinion, require working with a database. Also, the default session for receiving messages from Telegram has one limitation, which can be resolved by connecting a database. +- [Scenarios](docs/scenario.md) — This is a powerful mechanism built on top of the Spring State Machine. It allows you to configure entire chains of message processing rules. +- [Session](docs/session.md) — This section describes how the session for receiving messages from Telegram works, how you can configure it, and how to implement your own if needed. +- [Menu](docs/menu.md) — Creating and displaying a menu in your bot. +- [Telegram scope](docs/telegram-scope.md) — A bean scope specific to the Telegram session. +- [Permissions](docs/permissions.md) — Access settings for your bot. ## Contributing diff --git a/docs/controllers.md b/docs/controllers.md new file mode 100644 index 0000000..633f5f5 --- /dev/null +++ b/docs/controllers.md @@ -0,0 +1,254 @@ +# Controllers + +This library offers users two primary approaches for handling updates: `controllers` and `scenarios`. Both controllers +and scenarios offer their own benefits, so you can choose the one that suits your bot's requirements and your preferred +coding style. В этом разделе речь пойдет о контроллерах, как их использовать и какие возможности они дают. + +> If you need to make your own handler, all you have to do is create a **spring bean** that will +> implement the `UpdateHandler` interface and set its execution priority using a spring +> annotations `@Order` if needed. Also, after successful processing of the message, it is necessary +> put in the object `UpdateRequest` response with type `TelegramResponse`, so that update +> processing can be considered successful. If this is not done, further update handlers will be +> called + +## Overall information + +To begin receiving updates from **Telegram**, you will first need to create a controller and specify the criteria for +which updates you want to receive. + +To assist with the analysis, let's use the controller provided in the `Quick Start` section: + +```java + +@TelegramController +public class MainController { + + @TelegramCommand("/start") + public String onStart(User user) { + return "Hello " + user.getFirstName(); + } + + @TelegramMessage + public String onMessage(UpdateRequest request) { + return "You sent message with types %s".formatted(request.getMessageTypes()); + } + + @TelegramMessage("My name is {name:.*}") + public String onPattern(@TelegramPatternVariable("name") String name) { + return "Hello " + name; + } + + @TelegramRequest + public TelegramResponse onAll(UpdateRequest request) { + return new GenericTelegramResponse("Unsupported command"); + } + +} +``` + +- In order for an application to register a class as a controller, it must be marked + annotation `@TelegramController` +- To receive updates, there is an annotation `@TelegramRequest`. Also, to make it easier to work + with updates, was created several derived annotations. The main ones are `@TelegramCommand` + and `@TelegramMessage`. As the names imply, with the help of the first one you can only receive + commands **(Message#isCommand)**, and from the second any messages **(Update#getMessage)** +- The `@TelegramRequest` annotation's **pattern()** parameter takes a string that serves as a + matching condition for the text in the incoming message, whether it's just a message text or a + photo caption. For matcher uses `AntPathMatcher`, so you can specify + any [condition](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/AntPathMatcher.html) + in the string valid for `AntPathMatcher`. For example: `Hello*`, will match any string that starts + with `Hello`. If you do not specify **pattern()** it will be replaced with the `**` pattern and + matched with any text. +- If the `update` matches with several patterns that you specified in different methods + marked with `@TelegramRequest`, than sorted will be applied: + - `TelegramRequest` without **requestType** has the lowest priority + - `TelegramRequest` without **messageType** has the lowest priority among the **requestType** + equal to `RequestType.MESSAGE` + - `TelegramRequest` with **exclusiveMessageType** set to true and with more elements specified + in **messageType** takes precedence over others + - If `TelegramRequest` has the same priority by the above conditions, than the `AntPathMatcher` + order applied + > Простыми словами - чем уникальнее контроллер тем больший приоритет он имеет. +- Methods marked with `@TelegramRequest` annotation can accept a specific set of inputs + parameters as defined in the [Argument resolving](#argument-resolving) section +- Methods marked with `@TelegramRequest` annotation can return any object, as a result. Более детально данный процесс будет разобран в [Responses](#responses) +- Also, if you need to get some data from the user's message by a specific pattern, then you can use + the [TelegramPatternVariable](#telegrampatternvariable) + annotation + **Essentially `TelegramPatternVariable` works the same as `PathVariable`.** + +## Argument resolving + +Чтобы дать вам максимальное количество возможностей при работе с контроллерами, были определены несколько типов которые +вы можете указать в методе своих контроллеров и библиотека автоматически вытащит их из своего контекста и подставит вам +в метод. Далее приведен список поддерживаемых типов на данный момент: + +| Type | Source | +|---------------------|----------------------------------------------------| +| `UpdateRequest` | `UpdateRequest` | +| `TelegramClient` | `UpdateRequest.getTelegramClient()` | +| `Throwable` | `UpdateRequest.getError()` | +| `String` | `UpdateRequest.getText()` | +| `Long` | `UpdateRequest.getChatId()` | +| `Update` | `UpdateRequest.getOrigin()` | +| `Message` | `UpdateRequest.getMessage()` | +| `User` | `UpdateRequest.getUser()` | +| `Chat` | `UpdateRequest.getChat()` | +| `InlineQuery` | `UpdateRequest.getOrigin().getInlineQuery()` | +| `ChosenInlineQuery` | `UpdateRequest.getOrigin().getChosenInlineQuery()` | +| `CallbackQuery` | `UpdateRequest.getOrigin().getCallbackQuery()` | +| `ShippingQuery` | `UpdateRequest.getOrigin().getShippingQuery()` | +| `PreCheckoutQuery` | `UpdateRequest.getOrigin().getPreCheckoutQuery()` | +| `Poll` | `UpdateRequest.getOrigin().getPoll()` | +| `PollAnswer` | `UpdateRequest.getOrigin().getPollAnswer()` | +| `ChatMemberUpdated` | `UpdateRequest.getOrigin().getChatMember()` | +| `ChatJoinRequest` | `UpdateRequest.getOrigin().getChatJoinRequest()` | + +> If you need to add support for your custom argument, you need to create **spring bean** and +> implement `io.github.drednote.telegram.core.resolver.HandlerMethodArgumentResolver` interface + +## TelegramPatternVariable + +You can annotate certain arguments using `@TelegramPatternVariable`. Here are the rules to follow: + +- This annotation is only valid if there's text in the update (e.g., message text, photo caption). + Otherwise, it will be `null` +- This annotation supports only two Java types: `String` and `Map` +- When using `String`, the value of the template variable from the pattern is exposed +- In the case of `Map`, all template variables are exposed + +## Responses + +Вы можете возвращать любой тип из методов помеченных аннотацией `TelegramRequest`. Тип будет обернут в `TelegramResponse` и далее обработан. Подробнее о механизме отправки ответов в телеграм будет в разделе [Response Processing](response-processing.md). Здесь же я хочу показать несколько возможных вариантов ответов. + +### Simple + +Данный контроллер возвращает строку, которая потом оборачивается в `GenericTelegramResponse` и в таком виде существует до момента вызова отправки ответа в телеграм. + +```java + +@TelegramController +public class MainController { + + @TelegramCommand("/start") + public String onStart(User user) { + return "Hello " + user.getFirstName(); + } +} +``` + +### List + +Этот контроллер обернет ответ в `CompositeTelegramResponse` и поочередно выполнит две отправки ответа. +> Количество объектов в листе не ограничено. + +```java + +@TelegramController +public class MainController { + + @TelegramCommand("/start") + public List onStart() { + return List.of("1", "2"); + } +} +``` + +### Void + +Вы можете ничего не возвращать и тогда ничего и отправится в телеграм. Но ответ будет обернут в `EmptyTelegramResponse`. + +```java + +@TelegramController +public class MainController { + + @TelegramCommand("/something") + public void empty() { + // do something + } +} +``` + +### Flux +Вы можете использовать тип ответа `Flux` для того чтобы создавать коллбеки. Flux очень напоминает Stream API, но это все таки совершенно другое. Об этом вы можете почитать тут https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html. + +```java +@TelegramController +public class CallbackController { + + @TelegramCommand("/callback") + public Flux onCallback(UpdateRequest updateRequest) { + Mono typing = Mono.just(SendChatAction.builder() + .chatId(updateRequest.getChatId()) + .action(ChatActionType.TYPING.getValue()) + .build()); + + // create callback that will be executed when a long process is finished + Mono callback = Mono.defer(() -> { + try { + // imitate a long process + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return Mono.just("Hello World"); + }); + + return Flux.merge(typing, callback); + } +} +``` +Пример выше при вызове контроллера создает 2 параллельные задачи: поменять статус диалога у пользователя на "печатает" и имитация долгой задачи, после чего пользователю отправляется строка `Hello World`. Этот пример можно переписать на более простой и понятный пример: + +```java +@TelegramController +public class CallbackController { + + @TelegramCommand("/callback") + public List onCallback(UpdateRequest updateRequest) { + SendChatAction typing = SendChatAction.builder() + .chatId(updateRequest.getChatId()) + .action(ChatActionType.TYPING.getValue()) + .build(); + + // create callback that will be executed when a long process is finished + TelegramResponse response = request -> { + try { + // imitate a long process + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + new GenericTelegramResponse("Hello World").process(request); + }; + + return List.of(typing, response); + } +} +``` + +Тут же ответы будут отправляться строго последовательно, без гибкости которую предоставляет Flux. + +### Дополнительная информация + +Полная документация по Responses вы сможете найти в разделе посвященном обработке ответов [Response handling](response-processing.md). + +## Permissions + +Дополнительно для удобства проверки прав пользователей бота, есть аннотация `HasRole`. Ниже приведен пример ее использования: + +```java + +@TelegramController +public class MainController { + + @TelegramCommand("/admin") + @HasRole("admin") + public void empty() { + // do something + } +} +``` + +Для своей работы она использует механизм `Permissions`, который детально рассмотрен в разделе [Permissions](permissions.md). Если у пользователя который отправил запрос в бот, нет необходимой роли, то запрос не дойдет до контроллера, прервется и отправит ошибку пользователю. \ No newline at end of file diff --git a/docs/datasource.md b/docs/datasource.md new file mode 100644 index 0000000..fb74ea3 --- /dev/null +++ b/docs/datasource.md @@ -0,0 +1,32 @@ +## Datasource + +- For some filters and scenarios to work correctly, you need to save information to the database. + You can save data to the application memory, but then during the restart, all information will be + lost. Therefore, it is better if you configure the datasource using spring. + +- This library is fully based on Spring JPA in working with the database. Therefore, to support + different databases (postgres, mongo, etc.), using the implementations of `DataSourceAdapter` + interface +- If you want to add support for a database that currently is not supported, you should to + create entity and create repository extending `PermissionRepository`, `ScenarioRepository` + or `ScenarioIdRepository` + +> **Currently supported `JpaRepository`** + +> Note: To enable auto scan for jpa entities, you should manually pick main interfaces for entities +> and use `@EntityScan` annotation. To create spring data repository, you need to just implement one +> of the repository interfaces + +```java + +@EntityScan(basePackageClasses = {Permission.class, PersistScenario.class}) +@Configuration +public class JpaConfig { + +} +``` + +```java + +public interface PermissionRepository extends JpaPermissionRepository {} +``` \ No newline at end of file diff --git a/docs/exception-handling.md b/docs/exception-handling.md new file mode 100644 index 0000000..ad9f019 --- /dev/null +++ b/docs/exception-handling.md @@ -0,0 +1,21 @@ +## Exception handling + +The purpose of the ExceptionHandler mechanism is to provide centralized error handling. Any errors +that occur during the processing will be caught and sent to the `ExceptionHandler` for further +handling. Here are some important rules to keep in mind: + +- To initiate error handling, the class must be annotated with `@TelegramAdvice`. Additionally, you + need to specify at least one method and annotate it with `@TelegramExceptionHandler`. +- If the user is not marked any method with the `@TelegramExceptionHandler` annotation, the default + error handling approach will be applied. +- In situations where multiple handlers are present for a particular error, a sorting mechanism is + implemented to determine the priority of method calls. The higher in the hierarchy the error + (throwable at the very top) that the handler expects, the lower the priority of the method call + will be compared to others. + > This ensures that the most specific error handlers are given precedence in the error handling + process +- Methods marked with `@TelegramExceptionHandler` annotation can accept a specific set of inputs + parameters as defined in the [Argument resolving](controllers.md#argument-resolving) section of controllers docs. +- Methods marked with `@TelegramExceptionHandler` annotation can return any object, as a result. The + response processing mechanism is detailed in the [Response Processing](response-processing.md) + doc. \ No newline at end of file diff --git a/docs/filters.md b/docs/filters.md new file mode 100644 index 0000000..103bed1 --- /dev/null +++ b/docs/filters.md @@ -0,0 +1,66 @@ +## Filters + + +Filters serve as both pre-processing and post-processing mechanisms for the primary stage of update +processing. They allow you to define specific criteria for filtering and manipulating updates before +and after they are processed. + +- Filters are needed for several main purposes: + - To filter updates + - To execute the code for each update + +- To control update filtering, filters can set some properties + in [UpdateRequest](#updaterequest), such as `response`. If any filter set + property `response` then the update is considered successful and an attempt will be made to send a + response +- Filters are called twice: before (pre-filters) the main [Update Handling](#update-handling) and + after (post-filters). It is important to note that, even if an error occurred during the main + processing of the update, post-filters will still be executed + +- There are two main interfaces for creating a filter: + - `PreUpdateFilter` - **spring beans** that implement this interface will be called **before** + the main [Update Handling](#update-handling) + - `PostUpdateFilter` - **spring beans** that implement this interface will be called **after** + the main [Update Handling](#update-handling) + - `ConclusivePostUpdateFilter` - **spring beans** that implement this interface will be called **after** + the response is sent to telegram. [see](#response-processing) + +- Also, for convenience, one interface are created. First one - `PriorityPreUpdateFilter` is + implemented from `PreUpdateFilter` and take precedence over `PreUpdateFilter` and is executed + earlier whatever returns **getPreOrder()**. + +- To add a filter, you need to create a **spring bean** that will implement the `PreUpdateFilter` + or `PostUpdateFilter` interface. + +- Additionally, it is possible to create a filter with [Telegram Scope](#telegram-scope). With this + approach, a unique filter instance is created for each update, and it remains active until the + processing of the update is completed. This allows for better separation and management of filters + for individual updates. Example: + +```java + +@Component +@TelegramScope +public class LoggingFilter implements PriorityPreUpdateFilter, PostUpdateFilter { + + private LocalDateTime startTime; + + @Override + public void preFilter(@NonNull UpdateRequest request) { + this.startTime = LocalDateTime.now(); + log.info("Receive request with id {}", request.getId()); + } + + @Override + public void postFilter(@NonNull UpdateRequest request) { + log.info("Request with id {} processed for {} ms", request.getId(), + ChronoUnit.MILLIS.between(startTime, LocalDateTime.now())); + } + + @Override + public int getPreOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } +} + +``` diff --git a/docs/menu.md b/docs/menu.md new file mode 100644 index 0000000..03f46f5 --- /dev/null +++ b/docs/menu.md @@ -0,0 +1 @@ +## Menu \ No newline at end of file diff --git a/docs/permissions.md b/docs/permissions.md new file mode 100644 index 0000000..570a692 --- /dev/null +++ b/docs/permissions.md @@ -0,0 +1 @@ +## Permissions \ No newline at end of file diff --git a/docs/properties/Properties.md b/docs/properties/Properties.md new file mode 100644 index 0000000..495046d --- /dev/null +++ b/docs/properties/Properties.md @@ -0,0 +1,195 @@ +# Properties +All settings tables contain 5 columns: + +- `Name` - the name of the variable as it is called in the code +- `Type` - the type of the variable +- `Description` - a brief description of what this setting does +- `Default Value` - the default value of the variable +- `Required` - whether the variable is required + +> If the `Required` field is `true` and the value of the `Default Value` column is not equal to `-`, +> it means that you don't need to manually set the value for the variable. However, if you manually +> set it to `null` or any value that can be considered empty, the application will not start +## TelegramProperties + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| enabled | boolean | Enable the bot. Default: true | true | true | +| name | String | The name of a bot. Example: TheBestBot.

@deprecated not used since 0.3.0 | - | false | +| token | String | The token of a bot.

Required | - | true | +| default-locale | String | The default locale with which bot will send responses to user chats. A two-letter ISO 639-1 language code

Example: en, fr, ru. | - | false | +| session | [SessionProperties](#sessionproperties) | Session properties | SessionProperties | true | +| update-handler | [UpdateHandlerProperties](#updatehandlerproperties) | Properties of update handlers | UpdateHandlerProperties | true | +| filters | [FilterProperties](#filterproperties) | Filters properties | FilterProperties | true | +| menu | [MenuProperties](#menuproperties) | Menu properties | MenuProperties | true | +| scenario | [ScenarioProperties](#scenarioproperties) | Scenario properties | ScenarioProperties | true | +--- + +## FilterProperties + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| permission | [PermissionProperties](#permissionproperties) | Permission filter properties | PermissionProperties | true | +| user-rate-limit | long | How often each user can perform requests to bot. 0 = no rules | 0 | true | +| user-rate-limit-unit | ChronoUnit | The {@link ChronoUnit} which will be applied to {@link #userRateLimit} | Seconds | true | +| user-rate-limit-cache-expire | long | How long cache with rate limit bucket will not expire. This parameter needed just for delete staled buckets to free up memory | 1 | true | +| user-rate-limit-cache-expire-unit | ChronoUnit | The {@link ChronoUnit} which will be applied to {@link #userRateLimitCacheExpire} | Hours | true | +| set-default-answer | boolean | If at the end of update handling and post filtering, the response is null, set {@link NotHandledTelegramResponse} as response | true | true | +--- + +## PermissionProperties + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| access | Access | Define who has access to bot | ALL | true | +| default-role | String | If a user has no role, this role will be set by default | NONE | true | +| roles | {String : [Role](#role)} | The list of roles with privileges | {} | true | +| assign-role | Map | The map of [userId:role] | {} | true | +### Role + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| can-read | boolean | | false | false | +| permissions | {String : [Object](#object)} | | {} | false | + + +--- + +## ScenarioProperties + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| values | {String : [Scenario](#scenario)} | A map of scenario names to their corresponding {@link Scenario} objects. | {} | true | +| default-rollback | [Rollback](#rollback) | The default rollback configuration, which applies if no another set in scenario object. | - | false | +### Scenario + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| request | [Request](#request) | | - | false | +| action-references | [[String](#string)] | | - | false | +| type | TransitionType | | EXTERNAL | false | +| source | String | | - | false | +| target | String | | - | false | +| graph | [[Node](#node)] | | [] | false | +| rollback | [Rollback](#rollback) | | - | false | +| props | {String : [Object](#object)} | | {} | false | +| steps | {String : [Scenario](#scenario)} | | {} | false | + + +### Rollback + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| request | [Request](#request) | | - | false | +| action-references | [[String](#string)] | | - | false | + + +### Node + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| id | String | | - | false | +| children | [[Node](#node)] | | [] | false | + + +### Request + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| patterns | [[String](#string)] | | - | false | +| request-types | [[RequestType](#requesttype)] | | - | false | +| message-types | [[MessageType](#messagetype)] | | [] | false | +| exclusive-message-type | boolean | | false | false | + + +--- + +## UpdateHandlerProperties + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| controller-enabled | boolean | Enabled controller update handling | true | true | +| scenario-enabled | boolean | Enabled scenario update handling | true | true | +| set-default-error-answer | boolean | If exception is occurred and no handler has processed it, set {@link InternalErrorTelegramResponse} as response | true | true | +| serialize-java-object-with-jackson | boolean | By default, java pojo objects will be serialized with Jackson to json in {@link GenericTelegramResponse}. Set this parameter to false, if you want to disable this behavior | true | true | +| parse-mode | ParseMode | Default parse mode of a text message sent to telegram. Applies only if you return raw string from update processing ({@link UpdateHandler}) | NO | true | +| enabled-warning-for-scenario | boolean | If scenario is enabled and {@link SessionProperties#getMaxThreadsPerUser} is set value other than 1, throws an error with a warning about using scenario safe only when getMaxThreadsPerUser is set to 1. | true | true | +--- + +## MenuProperties + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| values | {String : [CommandCls](#commandcls)} | Create bean {@link BotMenu} with this commands | - | false | +| send-policy | SendPolicy | Send policy | ON_STARTUP | true | +### CommandCls + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| text | String | | - | true | +| command | String | | - | true | +| scopes | [[ScopeCommand](#scopecommand)] | | [DEFAULT] | true | +| language-code | String | | - | false | +| user-ids | [[Long](#long)] | | [] | true | +| chat-ids | [[Long](#long)] | | [] | true | + + +### ScopeCommand + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| + + +--- + +## SchedulerTelegramUpdateProcessorProperties + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| auto-session-start | boolean | Automatically start {@link SchedulerTelegramUpdateProcessor} {@link TelegramBotSession#start()} is called. If you set this parameter to false, you will be needed to manually call the {@link SchedulerTelegramUpdateProcessor#start()} to start the session and start to process messages from the Telegram. | true | true | +| max-interval | int | Maximum interval after which to check for new messages for processing. In milliseconds. | 1000 | true | +| min-interval | int | Minimum interval after which to check for new messages for processing. In milliseconds. | 0 | true | +| reducing-interval-amount | int | How much to decrease the interval if messages are found for processing. In milliseconds. | 500 | true | +| increasing-interval-amount | int | How much to increase the interval if no message is found for processing. In milliseconds. | 100 | true | +| wait-interval | int | Interval after which to check for new messages for processing while all threads are busy. In milliseconds. | 30 | true | +| idle-interval | int | Interval after tasks is marked {@link UpdateInboxStatus#TIMEOUT}. In milliseconds. | 30000 | true | +| check-idle-interval | int | Interval to check that tasks is idle. In milliseconds. | 5000 | true | +| max-message-in-queue-per-user | int | Limits the number of updates to be store in memory queue for update processing for concrete user. 0 - no restrictions.

Applied only for {@link UpdateProcessorType#SCHEDULER} | 0 | true | +--- + +## LongPollingSessionProperties + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| update-limit | int | | 100 | true | +| update-timeout | int | | 50 | true | +| allowed-updates | [[String](#string)] | | - | false | +--- + +## SessionProperties + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| long-polling | [LongPollingSessionProperties](#longpollingsessionproperties) | LongPolling properties | LongPollingSessionProperties | true | +| scheduler-processor | [SchedulerTelegramUpdateProcessorProperties](#schedulertelegramupdateprocessorproperties) | SchedulerTelegramUpdateProcessor properties. | SchedulerTelegramUpdateProcessorProperties | false | +| consume-max-threads | int | Max number of threads used for consumption messages from a telegram | 10 | true | +| max-messages-in-queue | int | Limits the number of updates to be store in memory queue for update processing. 0 - no limit. Defaults to (consumeMaxThreads 1.5). | 15 | true | +| max-threads-per-user | int | Max number of threads used for consumption messages from a telegram for concrete user. 0 - no restrictions. | 1 | true | +| cache-live-duration | int | Cache lifetime used in {@link OnFlyTelegramUpdateProcessor}. This parameter needed just to delete staled buckets to free up memory | 1 | true | +| cache-live-duration-unit | TimeUnit | The {@link TimeUnit} which will be applied to {@link #cacheLiveDuration} | HOURS | true | +| update-strategy | UpdateStrategy | The strategy to receive updates from Telegram API | LONG_POLLING | true | +| update-processor-type | UpdateProcessorType | A type of {@link TelegramUpdateProcessor} using. | DEFAULT | false | +| back-off-strategy | Class | Backoff strategy which will be applied if requests to telegram API are failed with errors | class org.telegram.telegrambots.longpolling.util.ExponentialBackOff | true | +| proxy-type | ProxyType | The proxy type for executing requests to telegram API | NO_PROXY | true | +| auto-session-start | boolean | Automatically start session when spring context loaded. If you set this parameter to false, you will be needed to manually call the {@link TelegramBotSession#start()} to start the session and start to consume messages from the Telegram. | true | false | +| proxy-url | [ProxyUrl](#proxyurl) | Proxy url in format host:port or if auth needed host:port:username:password. | - | false | +### ProxyUrl + +| Name | Type | Description | Default Value | Required | +|------|------|-------------|---------------|----------| +| user-name | String | | - | false | +| password | [char[]](#char[]) | | - | false | + + +--- + diff --git a/docs/response-processing.md b/docs/response-processing.md new file mode 100644 index 0000000..7b555da --- /dev/null +++ b/docs/response-processing.md @@ -0,0 +1,37 @@ +## Response processing + +After the update processing is complete, it is expected that a response will be sent to the user. To +handle this, there is a component called **Response Processing**, which follows certain rules. + +- The response represents by interface `TelegramResponse` +- **Response can only be sent if [Update](#update) has a `chatId`**. So if in update there is + no `chatId` than you should return `void` +- Any response will automatically be wrapped in the `TelegramResponse` interface and execute sending + method. Rules of wrapping: + - `void` or `null` will not trigger sending the response + - `String` will be wrapped in `GenericTelegramResponse` and execution method will send simple + text response (`SendMessage`) + - For `byte[]` same rule like for `String` except that `String` instance will be created + from `byte[]` (`new String(byte[])`) + - `BotApiMethod` and `SendMediaBotMethod` will be executed as is. + > `BotApiMethod` is an abstract class that represents sending object. + + > For `BotApiMethod` or `SendMediaBotMethod` the 'chatId' property will be automatically set + (only if it is null). If you manually set 'chatId', nothing happens + - A `TelegramResponse` object will be handled without wrapping. + - List of `TelegramResponse` will be wrapped in `CompositeTelegramResponse` and execute with + specified priority. + > Priority specified by `@Order` annotation + - For `java object` the `GenericTelegramResponse` will try to serialize it with `Jackson`. In + simple words will do `objectMapper.writeValueAsString(response)` + - For more information on wrapping rules, see the `ResponseSetter` and `GenericTelegramResponse` + classes + +> The exception is three types of response - `Collection`, `Stream`, `Flux`. For handling +> these types of response are created three additional implementations +> of `TelegramResponse` - `CompositeTelegramResponse`, `FluxTelegramResponse` +> and `StreamTelegramResponse` + +- You can create any implementation of `TelegramResponse` for sending response +- Any custom code can be written in `TelegramResponse`, but I strongly recommend using this + interface only for sending a response to **Telegram** diff --git a/docs/scenario.md b/docs/scenario.md new file mode 100644 index 0000000..855d4c8 --- /dev/null +++ b/docs/scenario.md @@ -0,0 +1,50 @@ +## Scenario + +To create scenarios, you will need to implement the `ScenarioConfigurerAdapter` interface by +creating a **Spring bean**. This interface is the main tool for creating scenarios and allows you to +define and customize the behavior of your scenarios. + +Here example of a configuring scenario, for additional info you can see javadocs. + +```java + +@Configuration +@RequiredArgsConstructor +public class ScenarioConfig extends ScenarioConfigurerAdapter> { + + private final ScenarioRepository scenarioRepository; + + @Override + public void onConfigure(@NonNull ScenarioTransitionConfigurer> configurer) { + configurer.withExternal().inlineKeyboard() + .source(State.INITIAL).target(ASSISTANT_CHOICE) + .telegramRequest(command(ASSISTANT_SETTINGS)) + .action(settingsActionsFactory::returnSettingsMenu) + + .and().withExternal() + .source(State.INITIAL).target(State.TEST) + .telegramRequest(command("/test")) + .action(context -> "Test") + + .and().withRollback() + .source(ASSISTANT_CHOICE).target(GET_SETTINGS) + .telegramRequest(callbackQuery(SettingsKeyboardButton.GET_CURRENT)) + .action(settingsActionsFactory.getSettings()) + .rollbackTelegramRequest(callbackQuery(ROLLBACK)) + .rollbackAction(settingsActionsFactory.rollbackToSettingsMenu()) + + .and(); + } + + @Override + public void onConfigure(ScenarioConfigConfigurer> configurer) { + configurer + .withPersister(new JpaScenarioRepositoryAdapter<>(scenarioRepository)); + } + + @Override + public void onConfigure(ScenarioStateConfigurer> configurer) { + configurer.withInitialState(State.INITIAL); + } +} +``` diff --git a/docs/session.md b/docs/session.md new file mode 100644 index 0000000..3c05f18 --- /dev/null +++ b/docs/session.md @@ -0,0 +1 @@ +## Session \ No newline at end of file diff --git a/docs/support/build.gradle b/docs/support/build.gradle new file mode 100644 index 0000000..ad60231 --- /dev/null +++ b/docs/support/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.3' + id 'io.spring.dependency-management' version '1.1.0' +} + +group = 'io.github.drednote.examples' +version = '' + +repositories { + mavenCentral() +} + +dependencies { + implementation(parent.parent) + + implementation 'com.github.javaparser:javaparser-core:3.27.0' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' +} + +test { + useJUnitPlatform() +} + +tasks.register('generatePropertiesDocs', JavaExec) { + group = 'documentation' + description = 'Generates markdown tables for all *Properties.java classes' + classpath = sourceSets.main.runtimeClasspath + mainClass = 'io.github.drednote.support.GeneratePropertiesDocs' +} diff --git a/docs/support/settings.gradle b/docs/support/settings.gradle new file mode 100644 index 0000000..9e4da99 --- /dev/null +++ b/docs/support/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'spring-boot-starter-telegram-docs-support' \ No newline at end of file diff --git a/docs/support/src/main/java/io/github/drednote/support/GeneratePropertiesDocs.java b/docs/support/src/main/java/io/github/drednote/support/GeneratePropertiesDocs.java new file mode 100644 index 0000000..51d40ad --- /dev/null +++ b/docs/support/src/main/java/io/github/drednote/support/GeneratePropertiesDocs.java @@ -0,0 +1,324 @@ +package io.github.drednote.support; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParseResult; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.VariableDeclarator; +import io.github.drednote.telegram.TelegramProperties; +import jakarta.annotation.Nonnull; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.naming.directory.SearchResult; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +public class GeneratePropertiesDocs { + + private static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + private static final String INDEX_MAIN = "java/main"; + private static final String INDEX_TEST = "java/test"; + + public static void main(String[] args) throws Exception { + List propertyClasses = search("io.github.drednote.telegram"); + propertyClasses.sort((f, s) -> { + if (TelegramProperties.class.isAssignableFrom(f.clazz)) { + return -1; + } else if (TelegramProperties.class.isAssignableFrom(s.clazz)) { + return 1; + } + return 0; + }); + + List markdownByClass = new LinkedList<>(); + Set> processed = new HashSet<>(); + + for (SearchResult searchResult : propertyClasses) { + String markdown = generateMarkdown(searchResult, null, processed); + if (markdown != null) { + markdownByClass.add(markdown); + } + } + + String property = System.getProperty("user.dir"); + Path output = Paths.get(property).getParent().resolve("properties/Properties.md"); + Files.createDirectories(output.getParent()); + + try (BufferedWriter writer = Files.newBufferedWriter(output)) { + writer.write(""" + # Properties + All settings tables contain 5 columns: + + - `Name` - the name of the variable as it is called in the code + - `Type` - the type of the variable + - `Description` - a brief description of what this setting does + - `Default Value` - the default value of the variable + - `Required` - whether the variable is required + + > If the `Required` field is `true` and the value of the `Default Value` column is not equal to `-`, + > it means that you don't need to manually set the value for the variable. However, if you manually + > set it to `null` or any value that can be considered empty, the application will not start + """); + for (String markdown : markdownByClass) { + writer.write(markdown); + } + } + + System.out.println("📄 Documentation generated to " + output.toAbsolutePath()); + } + + @Nullable + private static String generateMarkdown( + SearchResult searchResult, @Nullable List markdownContainer, Set> processed + ) throws IOException { + Class clazz = searchResult.clazz; + if (processed.contains(clazz)) { + return null; + } + processed.add(clazz); + List container = markdownContainer == null ? new LinkedList<>() : markdownContainer; + + StringBuilder sb = new StringBuilder(); + if (!clazz.getSimpleName().endsWith("Properties")) { + sb.append("### "); + } else { + sb.append("## "); + } + sb.append(clazz.getSimpleName()).append("\n\n"); + sb.append("| Name | Type | Description | Default Value | Required |\n"); + sb.append("|------|------|-------------|---------------|----------|\n"); + + for (Field field : clazz.getDeclaredFields()) { + field.setAccessible(true); + if (Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers())) { + continue; + } + + String name = field.getName().replaceAll("([a-z])([A-Z])", "$1-$2").toLowerCase(); + String description = getFirstSentenceFromJavadoc(field, searchResult); + String defaultValue = getDefaultValue(field, clazz); + String required = isRequired(field) ? "true" : "false"; + + Class type = field.getType(); + String fieldType = type.getSimpleName(); + if (!type.isPrimitive() && !type.isEnum()) { + if (Collection.class.isAssignableFrom(type)) { + ParameterizedType genericType = (ParameterizedType) field.getGenericType(); + Type argument = genericType.getActualTypeArguments()[0]; + if (argument instanceof Class collectionClass) { + if (collectionClass.getEnclosingClass() != null) { + String markdown = generateMarkdown( + new SearchResult(searchResult.classPath, searchResult.fullClassPath, collectionClass), + container, processed); + if (markdown != null) { + container.add(markdown); + } + } + fieldType = + "[[" + collectionClass.getSimpleName() + "](#" + collectionClass.getSimpleName() + .toLowerCase() + ")]"; + } + } else if (Map.class.isAssignableFrom(type)) { + ParameterizedType genericType = (ParameterizedType) field.getGenericType(); + Type argument = genericType.getActualTypeArguments()[1]; + if (argument instanceof Class mapClazz) { + if (mapClazz.getEnclosingClass() != null) { + String markdown = generateMarkdown( + new SearchResult(searchResult.classPath, searchResult.fullClassPath, mapClazz), + container, processed); + if (markdown != null) { + container.add(markdown); + } + } + fieldType = "{%s : [%s](#%s)}".formatted( + ((Class) genericType.getActualTypeArguments()[0]).getSimpleName(), + mapClazz.getSimpleName(), mapClazz.getSimpleName().toLowerCase()); + } + } else if (!type.getName().startsWith("java.")) { + if (type.getEnclosingClass() != null) { + String markdown = generateMarkdown( + new SearchResult(searchResult.classPath, searchResult.fullClassPath, type), + container, processed); + if (markdown != null) { + container.add(markdown); + } + } + fieldType = " [" + type.getSimpleName() + "](#" + type.getSimpleName().toLowerCase() + ")"; + } + } + if (defaultValue == null) { + defaultValue = "-"; + } + + sb.append("| ").append(name) + .append(" | ").append(fieldType) + .append(" | ").append(description) + .append(" | ").append(defaultValue) + .append(" | ").append(required).append(" |\n"); + } + + if (markdownContainer == null) { + Collections.reverse(container); + for (String markdown : container) { + sb.append(markdown).append("\n\n"); + } + sb.append("---\n\n"); + } + return sb.toString(); + } + + private static String getFirstSentenceFromJavadoc(Field field, SearchResult searchResult) throws IOException { + String replaced = searchResult.fullClassPath + .replace(".jar", "-sources.jar") + .replace(".class", ".java"); + int index = replaced.indexOf("$"); + if (index != -1) { + replaced = replaced.substring(0, index) + ".java"; + } + Resource resource = new PathMatchingResourcePatternResolver().getResource(replaced); + InputStream inputStream = resource.getInputStream(); + ParseResult result = new JavaParser().parse(inputStream); + CompilationUnit unit = result.getResult() + .orElseThrow(() -> new IllegalStateException("Cannot parse java class")); + + return unit.getTypes().stream() + .filter(type -> { + String id = type.getName().getId(); + String simpleName = searchResult.clazz.getSimpleName(); + return id.equals(simpleName); + }) + .findFirst().flatMap(typeDeclaration -> + typeDeclaration.getMembers().stream().filter(member -> { + List childNodes = member.getChildNodes(); + for (Node childNode : childNodes) { + if (childNode instanceof VariableDeclarator variableDeclarator) { + if (variableDeclarator.getName().getId().equals(field.getName())) { + return true; + } + } + } + return false; + }).findFirst()) + .flatMap(Node::getComment) + .map(comment -> { + String header = comment.getHeader(); + String footer = comment.getFooter(); + String text = comment.getContent() + .substring(header.length(), comment.getContent().length() - footer.length()); + int endIndex = text.indexOf("*\r\n"); + if (endIndex != -1) { + text = text.substring(0, endIndex); + } + text = text.replace("\r", "").replace("\n", "").trim(); + if (text.startsWith("*")) { + text = text.substring(1); + } + text = text.replace(" * ", " "); + return text; + }) + .orElse(""); + } + + private static String getDefaultValue(Field field, Class clazz) { + try { + Object instance = clazz.getDeclaredConstructor().newInstance(); + Object value = field.get(instance); + if (value != null) { + if (value instanceof Map map) { + return map.isEmpty() ? "{}" : map.toString(); + } else if (value instanceof Collection set) { + return set.isEmpty() ? "[]" : set.toString(); + } else if (value.getClass().getSimpleName().endsWith("Properties")) { + return value.getClass().getSimpleName(); + } else { + return value.toString(); + } + } else { + return "-"; + } + } catch (Exception e) { + return "-"; + } + } + + private static boolean isRequired(Field field) { + return field.isAnnotationPresent(NotNull.class) || field.isAnnotationPresent(Nonnull.class) + || field.isAnnotationPresent(NonNull.class); + } + + private static List search(String packageSearchPath) throws ClassNotFoundException { + List classes = new ArrayList<>(); + String toSearch = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath( + packageSearchPath) + '/' + DEFAULT_RESOURCE_PATTERN; + try { + Resource[] resources = new PathMatchingResourcePatternResolver().getResources(toSearch); + for (Resource resource : resources) { + String filename = resource.getFilename(); + if (filename == null || filename.contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) { + // Ignore CGLIB-generated classes in the classpath + continue; + } + String urlPath = resource.getURI().toString(); + String className = findPathToClass(urlPath); + if (className.endsWith("Properties")) { + classes.add(new SearchResult(className, urlPath, + loadClass(ClassUtils.convertResourcePathToClassName(className)))); + } + } + } catch (IOException e) { + throw new BeanCreationException("Failed to load package " + toSearch, e); + } + return classes; + } + + static String findPathToClass(String urlPath) { + if (urlPath == null) { + throw new IllegalArgumentException("urlPath must not be null"); + } + String result = urlPath.replace("\\", "/"); + int indexExclamation = urlPath.lastIndexOf('!'); + if (indexExclamation != -1) { + result = urlPath.substring(indexExclamation + 1); + } + int indexOfMain = result.indexOf(INDEX_MAIN); + if (indexOfMain != -1) { + result = result.substring(indexOfMain + INDEX_MAIN.length()); + } + int indexOfTest = result.indexOf(INDEX_TEST); + if (indexOfTest != -1) { + result = result.substring(indexOfTest + INDEX_TEST.length()); + } + if (result.startsWith("/")) { + result = result.substring(1); + } + return result.substring(0, result.indexOf(".class")); + } + + private static Class loadClass(String className) throws ClassNotFoundException { + return ClassUtils.forName(className, GeneratePropertiesDocs.class.getClassLoader()); + } + + record SearchResult(String classPath, String fullClassPath, Class clazz) {} +} diff --git a/docs/telegram-scope.md b/docs/telegram-scope.md new file mode 100644 index 0000000..afcc2bb --- /dev/null +++ b/docs/telegram-scope.md @@ -0,0 +1,13 @@ +## Telegram scope + +`TelegramScope` is a specialization of `@Scope` for a component whose lifecycle is bound to the +current telegram update handling. + +The functionality of `@TelegramScope` is similar to the Spring annotation `@Scope("request")`, with +the difference being that the context is created at the start of update processing instead of at the +request level. By marking a **spring bean** with this annotation, a new instance of the bean will be +created for each update processing. + +It's important to note that each update handling is associated with a specific thread. In cases +where you create sub-threads within the main thread, you will need to manually bind +the `UpdateRequest` to the new thread. This can be achieved using the `UpdateRequestContext` class. Так же вы можете установить настройку `UpdateRequestContext.setInheritable(boolean value)` в значение true, чтобы при создании дочерних потоков контекст автоматически прокидывался. Но у данного использования есть некоторые ограничения о которых вы можете дополнительно прочитать в классе `InheritableThreadLocal`. \ No newline at end of file diff --git a/docs/update-object.md b/docs/update-object.md new file mode 100644 index 0000000..c63a633 --- /dev/null +++ b/docs/update-object.md @@ -0,0 +1,35 @@ +## Update + +`Update` is the main object that comes from the Telegram API. It contains all information about +the event that happened in the bot, whether it's a new message from the user, or changes in some +settings chat in which the bot is located. + +> Additional docs - Telegram API docs + +## UpdateRequest + +`UpdateRequest` is a primary object that stores all information about [update](#update). Any change +that occurs during the processing of an update is written to it. Thus, if you get it in the user +code, you can find out all the information about the current update. For example, in this way: + +```java + +@TelegramController +public class Example { + + @TelegramRequest + public void onAll(UpdateRequest request) { + System.out.printf("request is %s", request); + } +} +``` + +При создании `UpdateRequest` происходит парсинг полученного `Update`. Любое обновление из телеграмма можно писать 3 полями. Это - text (если он есть), requestType и список messageType (если requestType == RequestType.MESSAGE). Возможные значения для requestType вы можете посмотреть в енаме `RequestType`, для messageType в енаме `MessageType`. Если requestType != RequestType.MESSAGE, тогда список MessageType будет пустым. + +> Если по каким – то причинам не удалось понять что за тип сообщения (MessageType) отражает объект `Update`, тогда будет проставлен тип MessageType.UNKNOWN. Но только если requestType == RequestType.MESSAGE. + +Информацию по полям класса вы сможете найти в javadoc `UpdateRequest`. Вы можете почти неограниченно взаимодействовать с данным классом. Предполагается что вы сами не будете в него ничего записывать, а только читать. Но если вы захотите что-то вручную изменить в данном классе - учтите что любое ваше действие может привести к непредвиденным ошибкам или непредсказуемому поведению. + +## TelegramClient + +Документация по этому классу вы можете найти в org.telegram:telegrambots. //todo добавить ссылку \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index b42e9a0..5ffd62d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ rootProject.name = 'spring-boot-starter-telegram' -include 'examples:get-started', 'examples:scenario-from-file', 'examples:scenario', 'examples:controllers' \ No newline at end of file +include 'examples:get-started', 'examples:scenario-from-file', 'examples:scenario', 'examples:controllers', 'docs:support' \ No newline at end of file diff --git a/src/main/java/io/github/drednote/telegram/TelegramAutoConfiguration.java b/src/main/java/io/github/drednote/telegram/TelegramAutoConfiguration.java index dd3de02..ac6faca 100644 --- a/src/main/java/io/github/drednote/telegram/TelegramAutoConfiguration.java +++ b/src/main/java/io/github/drednote/telegram/TelegramAutoConfiguration.java @@ -14,7 +14,6 @@ import io.github.drednote.telegram.handler.UpdateHandlerAutoConfiguration; import io.github.drednote.telegram.menu.MenuAutoConfiguration; import io.github.drednote.telegram.session.SessionAutoConfiguration; -import io.github.drednote.telegram.session.SessionProperties.UpdateStrategy; import java.util.Collection; import java.util.Locale; import org.apache.commons.lang3.StringUtils; @@ -32,10 +31,9 @@ * Autoconfiguration class for configuring various aspects of a Telegram bot application. * *

This class provides automatic configuration for different components and features of a - * Telegram bot application, including bot configuration, message source configuration, and more. - * You can disable the Autoconfiguration with the property {@code dreadnote.telegram.enabled}. - * It utilizes properties defined in the application's configuration to customize the behavior of the - * bot. + * Telegram bot application, including bot configuration, message source configuration, and more. You can disable the + * Autoconfiguration with the property {@code drednote.telegram.enabled}. It utilizes properties defined in the + * application's configuration to customize the behavior of the bot. */ @ConditionalOnProperty(prefix = "drednote.telegram", name = "enabled", havingValue = "true", matchIfMissing = true) @ImportAutoConfiguration({ @@ -67,8 +65,7 @@ public static class BotConfig { * Configures a bean for the Telegram bot instance. * * @param properties Configuration properties for the Telegram bot - * @param updateHandlers Collection of update handlers for processing incoming - * updates + * @param updateHandlers Collection of update handlers for processing incoming updates * @param exceptionHandler The ExceptionHandler instance for handling exceptions * @param updateFilterProvider The UpdateFilterProvider instance for filtering updates * @return The configured Telegram bot instance @@ -86,12 +83,8 @@ public TelegramBot telegramLongPollingBot( throw new BeanCreationException(TELEGRAM_BOT, "Consider specify drednote.telegram.token"); } - if (properties.getSession().getUpdateStrategy() == UpdateStrategy.LONG_POLLING) { - return new DefaultTelegramBot(updateHandlers, - exceptionHandler, updateFilterProvider, telegramClient, enricher); - } else { - throw new BeanCreationException(TELEGRAM_BOT, "Webhooks not implemented yet"); - } + return new DefaultTelegramBot(updateHandlers, + exceptionHandler, updateFilterProvider, telegramClient, enricher); } } diff --git a/src/main/java/io/github/drednote/telegram/TelegramProperties.java b/src/main/java/io/github/drednote/telegram/TelegramProperties.java index 43721ca..cd95d23 100644 --- a/src/main/java/io/github/drednote/telegram/TelegramProperties.java +++ b/src/main/java/io/github/drednote/telegram/TelegramProperties.java @@ -25,6 +25,7 @@ public class TelegramProperties { /** * Enable the bot. Default: true */ + @NonNull private boolean enabled = true; /** * The name of a bot. Example: TheBestBot. diff --git a/src/main/java/io/github/drednote/telegram/core/UpdateRequestContext.java b/src/main/java/io/github/drednote/telegram/core/UpdateRequestContext.java index 37a1435..4e31b6f 100644 --- a/src/main/java/io/github/drednote/telegram/core/UpdateRequestContext.java +++ b/src/main/java/io/github/drednote/telegram/core/UpdateRequestContext.java @@ -125,8 +125,7 @@ static void saveBeanName(@NonNull String name) { } @Override - public void setApplicationContext(@NonNull ApplicationContext applicationContext) - throws BeansException { + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { this.factory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory(); } diff --git a/src/main/java/io/github/drednote/telegram/core/annotation/HasRole.java b/src/main/java/io/github/drednote/telegram/core/annotation/HasRole.java index c776de6..781d1c9 100644 --- a/src/main/java/io/github/drednote/telegram/core/annotation/HasRole.java +++ b/src/main/java/io/github/drednote/telegram/core/annotation/HasRole.java @@ -17,8 +17,8 @@ * *

Supported matching strategies: *

    - *
  • {@link StrategyMatching#INTERSECTION} – at least one required role must be present.
  • - *
  • {@link StrategyMatching#COMPLETE_MATCH} – all specified roles must be present.
  • + *
  • {@link StrategyMatching#ANY} – at least one required role must be present.
  • + *
  • {@link StrategyMatching#ALL} – all specified roles must be present.
  • *
*

*

@@ -48,11 +48,11 @@ /** * Strategy used to match the user's roles against the required ones. Defaults to - * {@link StrategyMatching#INTERSECTION}. + * {@link StrategyMatching#ANY}. * * @return the matching strategy */ - StrategyMatching strategyMatching() default StrategyMatching.INTERSECTION; + StrategyMatching strategyMatching() default StrategyMatching.ANY; /** * Role matching strategy for evaluating the user's permissions. @@ -61,11 +61,11 @@ enum StrategyMatching { /** * Requires at least one of the specified roles to be present. */ - INTERSECTION, + ANY, /** * Requires all specified roles to be present. */ - COMPLETE_MATCH + ALL } } diff --git a/src/main/java/io/github/drednote/telegram/core/request/UpdateRequest.java b/src/main/java/io/github/drednote/telegram/core/request/UpdateRequest.java index 14b58cc..dbc9c6f 100644 --- a/src/main/java/io/github/drednote/telegram/core/request/UpdateRequest.java +++ b/src/main/java/io/github/drednote/telegram/core/request/UpdateRequest.java @@ -107,10 +107,6 @@ default String getUserAssociatedId() { * Returns the message associated with the request * * @return the message, or null if not applicable - * @see Update#getMessage() - * @see Update#getEditedMessage() - * @see Update#getChannelPost() - * @see Update#getEditedChannelPost() * @see AbstractUpdateRequest#AbstractUpdateRequest(Update) */ @Nullable diff --git a/src/main/java/io/github/drednote/telegram/filter/PermissionProperties.java b/src/main/java/io/github/drednote/telegram/filter/PermissionProperties.java index 8ed40c2..7905a93 100644 --- a/src/main/java/io/github/drednote/telegram/filter/PermissionProperties.java +++ b/src/main/java/io/github/drednote/telegram/filter/PermissionProperties.java @@ -1,11 +1,13 @@ package io.github.drednote.telegram.filter; +import java.util.HashMap; import java.util.Map; import java.util.Set; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; /** * Configuration properties for defining access permissions and roles. @@ -24,24 +26,29 @@ public class PermissionProperties { /** * The default role assigned to users with no roles. */ + @NonNull public static final String DEFAULT_ROLE = "NONE"; /** * Define who has access to bot */ + @NonNull private Access access = Access.ALL; /** * If a user has no role, this role will be set by default */ + @NonNull private String defaultRole = DEFAULT_ROLE; /** * The list of roles with privileges */ - private Map roles = Map.of(); + @NonNull + private Map roles = new HashMap<>(0); /** * The map of [userId:role] */ - private Map> assignRole = Map.of(); + @NonNull + private Map> assignRole = new HashMap<>(0); /** * Enumeration of access control modes. @@ -72,6 +79,6 @@ public static class Role { /** * The map of additional permissions. */ - private Map permissions = Map.of(); + private Map permissions = new HashMap<>(0); } } diff --git a/src/main/java/io/github/drednote/telegram/filter/pre/HasRoleRequestFilter.java b/src/main/java/io/github/drednote/telegram/filter/pre/HasRoleRequestFilter.java index be359a2..54db676 100644 --- a/src/main/java/io/github/drednote/telegram/filter/pre/HasRoleRequestFilter.java +++ b/src/main/java/io/github/drednote/telegram/filter/pre/HasRoleRequestFilter.java @@ -69,7 +69,7 @@ private boolean isValid(UpdateRequest request, HasRole hasRole) { Permission permission = Objects.requireNonNull(request.getPermission()); Set roles = defaultIfNull(permission.getRoles(), Set.of()); StrategyMatching strategy = hasRole.strategyMatching(); - if (strategy == StrategyMatching.INTERSECTION) { + if (strategy == StrategyMatching.ANY) { return Arrays.stream(hasRole.value()).anyMatch(roles::contains); } else { return Arrays.stream(hasRole.value()).allMatch(roles::contains); diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioProperties.java b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioProperties.java index dc8ccce..445ecbe 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioProperties.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioProperties.java @@ -16,6 +16,7 @@ import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; /** @@ -34,8 +35,8 @@ public class ScenarioProperties { /** * A map of scenario names to their corresponding {@link Scenario} objects. */ - @Nullable - private Map values; + @NonNull + private Map values = new HashMap<>(); /** * The default rollback configuration, which applies if no another set in scenario object. diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioPropertiesConfigurer.java b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioPropertiesConfigurer.java index 4667eb9..6145fc6 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioPropertiesConfigurer.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioPropertiesConfigurer.java @@ -55,10 +55,7 @@ public ScenarioPropertiesConfigurer( } public void collectStates() { - Map values = scenarioProperties.getValues(); - if (values != null) { - doCollectStates(values); - } + doCollectStates(scenarioProperties.getValues()); } private void doCollectStates(Map values) { @@ -80,18 +77,16 @@ private void doCollectStates(Map values) { public void configure() { Map values = scenarioProperties.getValues(); - if (values != null) { - values.forEach((key, scenario) -> { - Assert.required(scenario, "Scenario"); - if (scenario.getType() == TransitionType.ROLLBACK) { - throw new IllegalArgumentException("First transition cannot be of 'Rollback' type"); - } - TransitionData transitionData = configureTransition(scenarioBuilder, scenario, null); - scenario.getGraph().forEach(node -> { - doConfigure(scenarioBuilder, transitionData, scenario, node); - }); + values.forEach((key, scenario) -> { + Assert.required(scenario, "Scenario"); + if (scenario.getType() == TransitionType.ROLLBACK) { + throw new IllegalArgumentException("First transition cannot be of 'Rollback' type"); + } + TransitionData transitionData = configureTransition(scenarioBuilder, scenario, null); + scenario.getGraph().forEach(node -> { + doConfigure(scenarioBuilder, transitionData, scenario, node); }); - } + }); } private void doConfigure( diff --git a/src/main/java/io/github/drednote/telegram/session/SessionAutoConfiguration.java b/src/main/java/io/github/drednote/telegram/session/SessionAutoConfiguration.java index 7d3e2d0..785726a 100644 --- a/src/main/java/io/github/drednote/telegram/session/SessionAutoConfiguration.java +++ b/src/main/java/io/github/drednote/telegram/session/SessionAutoConfiguration.java @@ -26,8 +26,8 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -59,15 +59,12 @@ public class SessionAutoConfiguration { * Configures a bean for the Telegram bot session using long polling. And starts session * * @param telegramConsumeClient The Telegram client used to interact with the Telegram API - * @param properties Configuration properties for the session + * @param properties Configuration properties for the session * @return The configured Telegram bot session */ @Bean(destroyMethod = "stop") - @ConditionalOnProperty( - prefix = "drednote.telegram.session", - name = "type", - havingValue = "LONG_POLLING", - matchIfMissing = true + @ConditionalOnExpression( + "#{environment.getProperty('drednote.telegram.session.type') == null || environment.getProperty('drednote.telegram.session.type').equalsIgnoreCase('LONG_POLLING')}" ) @ConditionalOnMissingBean public TelegramBotSession longPollingTelegramBotSession( @@ -94,10 +91,8 @@ public TelegramBotSession longPollingTelegramBotSession( * @return The configured Telegram bot session */ @Bean(destroyMethod = "stop") - @ConditionalOnProperty( - prefix = "drednote.telegram.session", - name = "type", - havingValue = "WEBHOOKS" + @ConditionalOnExpression( + value = "#{environment.getProperty('drednote.telegram.session.type')?.equalsIgnoreCase('WEBHOOKS')}" ) @ConditionalOnMissingBean public TelegramBotSession webhooksTelegramBotSession() { diff --git a/src/test/java/io/github/drednote/telegram/handler/controller/HasRoleRequestFilterTest.java b/src/test/java/io/github/drednote/telegram/handler/controller/HasRoleRequestFilterTest.java index 73ab46c..20bd980 100644 --- a/src/test/java/io/github/drednote/telegram/handler/controller/HasRoleRequestFilterTest.java +++ b/src/test/java/io/github/drednote/telegram/handler/controller/HasRoleRequestFilterTest.java @@ -74,7 +74,7 @@ static class TestClass { public void hasRole() { } - @HasRole(value = {TEST_ROLE, SECOND_ROLE}, strategyMatching = StrategyMatching.COMPLETE_MATCH) + @HasRole(value = {TEST_ROLE, SECOND_ROLE}, strategyMatching = StrategyMatching.ALL) public void completeMatch() { }