From 9f9af42e1938e88e99b01a673ff0b58a0ede28ef Mon Sep 17 00:00:00 2001 From: Ivan Galushko Date: Tue, 21 Jan 2025 22:13:21 +0300 Subject: [PATCH] Feature/add scenario config from file (#29) * [FEATURE]: add parsing scenario from config file --- README.md | 3 - .../telegram/TelegramAutoConfiguration.java | 1 - .../drednote/telegram/TelegramProperties.java | 8 +- .../core/invoke/InvocableHandlerMethod.java | 14 +- .../ScenarioKryoSerializationService.java | 5 +- .../UpdateHandlerAutoConfiguration.java | 27 ++- .../handler/UpdateHandlerProperties.java | 2 + .../TelegramControllerBeanPostProcessor.java | 2 +- .../telegram/handler/scenario/Action.java | 2 +- .../handler/scenario/ActionContext.java | 2 + .../handler/scenario/SimpleActionContext.java | 6 + .../handler/scenario/SimpleScenario.java | 7 +- .../scenario/configurer/ScenarioBuilder.java | 3 + .../ScenarioBaseTransitionConfigurer.java | 14 +- .../ScenarioRollbackTransitionConfigurer.java | 16 +- ...impleScenarioBaseTransitionConfigurer.java | 13 +- ...eScenarioRollbackTransitionConfigurer.java | 14 +- .../SimpleScenarioTransitionConfigurer.java | 2 + .../handler/scenario/data/SimpleState.java | 22 ++- .../telegram/handler/scenario/data/State.java | 3 + .../persist/SimpleScenarioPersister.java | 2 +- .../scenario/persist/SimpleStateContext.java | 4 +- .../scenario/persist/StateContext.java | 3 + .../ScenarioFactoryBeanPostProcessor.java | 63 ++++++ .../property/ScenarioFactoryContainer.java | 49 +++++ .../property/ScenarioFactoryResolver.java | 10 + .../scenario/property/ScenarioProperties.java | 180 ++++++++++++++++++ .../ScenarioPropertiesConfigurer.java | 138 ++++++++++++++ .../scenario/property/TelegramScenario.java | 25 +++ .../property/TelegramScenarioAction.java | 59 ++++++ .../scenario/property/package-info.java | 8 + .../session/SessionAutoConfiguration.java | 1 + .../telegram/session/SessionProperties.java | 3 + .../ScenarioKryoSerializationServiceTest.java | 7 +- .../handler/scenario/ScenarioTest.java | 8 +- .../configurer/ScenarioBuilderTest.java | 21 +- .../ScenarioPropertiesConfigurerTest.java | 101 ++++++++++ .../DefaultTelegramUpdateProcessorTest.java | 4 +- .../application-scenarioproperties.yaml | 66 +++++++ 39 files changed, 871 insertions(+), 47 deletions(-) create mode 100644 src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryBeanPostProcessor.java create mode 100644 src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryContainer.java create mode 100644 src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryResolver.java create mode 100644 src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioProperties.java create mode 100644 src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioPropertiesConfigurer.java create mode 100644 src/main/java/io/github/drednote/telegram/handler/scenario/property/TelegramScenario.java create mode 100644 src/main/java/io/github/drednote/telegram/handler/scenario/property/TelegramScenarioAction.java create mode 100644 src/main/java/io/github/drednote/telegram/handler/scenario/property/package-info.java create mode 100644 src/test/java/io/github/drednote/telegram/handler/scenario/property/ScenarioPropertiesConfigurerTest.java create mode 100644 src/test/resources/application-scenarioproperties.yaml diff --git a/README.md b/README.md index 5b321fb..7a6e1b0 100644 --- a/README.md +++ b/README.md @@ -480,9 +480,6 @@ handle this, there is a component called **Response Processing**, which follows - 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** -- If you pass {@link BotApiMethod} or {@link SendMediaBotMethod} in the constructor of this class, - the 'chatId' property will be automatically set (only if it is null). If you manually - set 'chatId', nothing happens ### Exception Handling diff --git a/src/main/java/io/github/drednote/telegram/TelegramAutoConfiguration.java b/src/main/java/io/github/drednote/telegram/TelegramAutoConfiguration.java index e5fd686..48c3ee5 100644 --- a/src/main/java/io/github/drednote/telegram/TelegramAutoConfiguration.java +++ b/src/main/java/io/github/drednote/telegram/TelegramAutoConfiguration.java @@ -63,7 +63,6 @@ public TelegramAutoConfiguration(TelegramProperties properties) { @AutoConfiguration public static class BotConfig { - private static final String TELEGRAM_BOT = "TelegramBot"; /** * Configures a bean for the Telegram bot instance. diff --git a/src/main/java/io/github/drednote/telegram/TelegramProperties.java b/src/main/java/io/github/drednote/telegram/TelegramProperties.java index 583f501..0335a65 100644 --- a/src/main/java/io/github/drednote/telegram/TelegramProperties.java +++ b/src/main/java/io/github/drednote/telegram/TelegramProperties.java @@ -2,6 +2,7 @@ import io.github.drednote.telegram.filter.FilterProperties; import io.github.drednote.telegram.handler.UpdateHandlerProperties; +import io.github.drednote.telegram.handler.scenario.property.ScenarioProperties; import io.github.drednote.telegram.menu.MenuProperties; import io.github.drednote.telegram.session.SessionProperties; import lombok.Getter; @@ -16,7 +17,7 @@ @ConfigurationProperties("drednote.telegram") @EnableConfigurationProperties({ SessionProperties.class, UpdateHandlerProperties.class, - FilterProperties.class, MenuProperties.class + FilterProperties.class, MenuProperties.class, ScenarioProperties.class }) @Getter @Setter @@ -69,4 +70,9 @@ public class TelegramProperties { */ @NonNull private MenuProperties menu = new MenuProperties(); + /** + * Scenario properties + */ + @NonNull + private ScenarioProperties scenario = new ScenarioProperties(); } diff --git a/src/main/java/io/github/drednote/telegram/core/invoke/InvocableHandlerMethod.java b/src/main/java/io/github/drednote/telegram/core/invoke/InvocableHandlerMethod.java index c40900a..adb8c87 100644 --- a/src/main/java/io/github/drednote/telegram/core/invoke/InvocableHandlerMethod.java +++ b/src/main/java/io/github/drednote/telegram/core/invoke/InvocableHandlerMethod.java @@ -13,14 +13,22 @@ */ public class InvocableHandlerMethod extends HandlerMethod { + private final String beanType; + /** * Creates a new instance of the {@code InvocableHandlerMethod} class with the given handler * method. * * @param handlerMethod the handler method to invoke, not null */ + public InvocableHandlerMethod(HandlerMethod handlerMethod, String beanType) { + super(handlerMethod); + this.beanType = beanType; + } + public InvocableHandlerMethod(HandlerMethod handlerMethod) { super(handlerMethod); + this.beanType = "TelegramController"; } /** @@ -62,8 +70,8 @@ protected void assertTargetBean(Method method, Object targetBean, Object[] args) Class targetBeanClass = targetBean.getClass(); if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) { String text = "The mapped handler method class '" + methodDeclaringClass.getName() + - "' is not an instance of the actual TelegramController bean class '" + - targetBeanClass.getName() + "'. If the TelegramController requires proxying " + + "' is not an instance of the actual " + beanType + " bean class '" + + targetBeanClass.getName() + "'. If the " + beanType + " requires proxying " + "(e.g. due to @Transactional), please use class-based proxying."; throw new IllegalStateException(formatInvokeError(text, args)); } @@ -72,6 +80,6 @@ protected void assertTargetBean(Method method, Object targetBean, Object[] args) @NonNull @Override protected String formatInvokeError(String text, Object[] args) { - return super.formatInvokeError(text, args).replace("Controller", "TelegramController"); + return super.formatInvokeError(text, args).replace("Controller", beanType); } } diff --git a/src/main/java/io/github/drednote/telegram/datasource/scenario/ScenarioKryoSerializationService.java b/src/main/java/io/github/drednote/telegram/datasource/scenario/ScenarioKryoSerializationService.java index 586936f..3a7d8a3 100644 --- a/src/main/java/io/github/drednote/telegram/datasource/scenario/ScenarioKryoSerializationService.java +++ b/src/main/java/io/github/drednote/telegram/datasource/scenario/ScenarioKryoSerializationService.java @@ -13,6 +13,7 @@ import io.github.drednote.telegram.handler.scenario.persist.SimpleScenarioContext; import io.github.drednote.telegram.handler.scenario.persist.SimpleStateContext; import io.github.drednote.telegram.handler.scenario.persist.StateContext; +import java.util.HashMap; import java.util.HashSet; import java.util.Set; import org.springframework.lang.NonNull; @@ -56,6 +57,7 @@ private void writeState(Kryo kryo, Output output, StateContext state) { kryo.writeClassAndObject(output, mapping.getRequestType()); kryo.writeClassAndObject(output, new HashSet<>(mapping.getMessageTypes())); }); + kryo.writeClassAndObject(output, new HashMap<>(state.props())); } @Override @@ -77,7 +79,8 @@ public ScenarioContext read(Kryo kryo, Input input, Class> Set messageTypes = (Set) kryo.readClassAndObject(input); mappings.add(new UpdateRequestMapping(pattern, requestType, messageTypes)); } - return new SimpleStateContext<>(state, mappings, responseMessageProcessing); + HashMap props = (HashMap) kryo.readClassAndObject(input); + return new SimpleStateContext<>(state, mappings, responseMessageProcessing, props); } } } diff --git a/src/main/java/io/github/drednote/telegram/handler/UpdateHandlerAutoConfiguration.java b/src/main/java/io/github/drednote/telegram/handler/UpdateHandlerAutoConfiguration.java index 7da49df..9cfc72d 100644 --- a/src/main/java/io/github/drednote/telegram/handler/UpdateHandlerAutoConfiguration.java +++ b/src/main/java/io/github/drednote/telegram/handler/UpdateHandlerAutoConfiguration.java @@ -13,6 +13,7 @@ import io.github.drednote.telegram.handler.controller.TelegramControllerContainer; import io.github.drednote.telegram.handler.scenario.ScenarioConfig; import io.github.drednote.telegram.handler.scenario.ScenarioIdResolver; +import io.github.drednote.telegram.handler.scenario.property.ScenarioProperties; import io.github.drednote.telegram.handler.scenario.ScenarioUpdateHandler; import io.github.drednote.telegram.handler.scenario.SimpleScenarioConfig; import io.github.drednote.telegram.handler.scenario.SimpleScenarioIdResolver; @@ -25,6 +26,10 @@ import io.github.drednote.telegram.handler.scenario.persist.ScenarioFactory; import io.github.drednote.telegram.handler.scenario.persist.SimpleScenarioFactory; import io.github.drednote.telegram.handler.scenario.persist.SimpleScenarioPersister; +import io.github.drednote.telegram.handler.scenario.property.ScenarioFactoryBeanPostProcessor; +import io.github.drednote.telegram.handler.scenario.property.ScenarioFactoryContainer; +import io.github.drednote.telegram.handler.scenario.property.ScenarioFactoryResolver; +import io.github.drednote.telegram.handler.scenario.property.ScenarioPropertiesConfigurer; import io.github.drednote.telegram.session.SessionProperties; import io.github.drednote.telegram.utils.FieldProvider; import org.springframework.beans.factory.BeanCreationException; @@ -38,7 +43,7 @@ import org.springframework.context.annotation.Bean; @AutoConfiguration -@EnableConfigurationProperties(UpdateHandlerProperties.class) +@EnableConfigurationProperties({UpdateHandlerProperties.class, ScenarioProperties.class}) @AutoConfigureAfter(DataSourceAutoConfiguration.class) public class UpdateHandlerAutoConfiguration { @@ -57,7 +62,7 @@ public UpdateHandlerAutoConfiguration( that it implies sequential processing within one user. Consider disable the scenario handling, \ or set drednote.telegram.session.MaxThreadsPerUser to 1. - + You can disable this warning by setting drednote.telegram.update-handler.enabledWarningForScenario to false """; @@ -82,16 +87,30 @@ public ScenarioUpdateHandler scenarioUpdateHandler( return new ScenarioUpdateHandler(); } + @Bean + @ConditionalOnMissingBean + public ScenarioFactoryBeanPostProcessor scenarioFactoryBeanPostProcessor(ScenarioFactoryContainer container) { + return new ScenarioFactoryBeanPostProcessor(container); + } + + @Bean + @ConditionalOnMissingBean + public ScenarioFactoryContainer scenarioFactoryContainer() { + return new ScenarioFactoryContainer(); + } + @Bean @ConditionalOnMissingBean public ScenarioUpdateHandlerPopular scenarioUpdateHandlerPopular( - ScenarioConfigurerAdapter adapter, - @Autowired(required = false) ScenarioIdRepositoryAdapter scenarioIdAdapter + ScenarioConfigurerAdapter adapter, ScenarioProperties scenarioProperties, + @Autowired(required = false) ScenarioIdRepositoryAdapter scenarioIdAdapter, + ScenarioFactoryResolver scenarioFactoryResolver ) { ScenarioBuilder builder = new ScenarioBuilder<>(); adapter.onConfigure(new SimpleScenarioStateConfigurer<>(builder)); adapter.onConfigure(new SimpleScenarioConfigConfigurer<>(builder)); adapter.onConfigure(new SimpleScenarioTransitionConfigurer<>(builder)); + new ScenarioPropertiesConfigurer(scenarioProperties, scenarioFactoryResolver).configure(builder); ScenarioData data = builder.build(); ScenarioIdResolver resolver = data.resolver() == null diff --git a/src/main/java/io/github/drednote/telegram/handler/UpdateHandlerProperties.java b/src/main/java/io/github/drednote/telegram/handler/UpdateHandlerProperties.java index c7d162f..dc62fb1 100644 --- a/src/main/java/io/github/drednote/telegram/handler/UpdateHandlerProperties.java +++ b/src/main/java/io/github/drednote/telegram/handler/UpdateHandlerProperties.java @@ -15,6 +15,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; @Configuration @ConfigurationProperties("drednote.telegram.update-handler") @@ -78,6 +79,7 @@ public enum ParseMode { MARKDOWN_V2("MarkdownV2"), HTML("html"); + @Nullable private final String value; } } diff --git a/src/main/java/io/github/drednote/telegram/handler/controller/TelegramControllerBeanPostProcessor.java b/src/main/java/io/github/drednote/telegram/handler/controller/TelegramControllerBeanPostProcessor.java index f973ceb..85fc68e 100644 --- a/src/main/java/io/github/drednote/telegram/handler/controller/TelegramControllerBeanPostProcessor.java +++ b/src/main/java/io/github/drednote/telegram/handler/controller/TelegramControllerBeanPostProcessor.java @@ -41,7 +41,7 @@ public TelegramControllerBeanPostProcessor(ControllerRegistrar registrar) { * identifies annotated methods and registers them using the {@link ControllerRegistrar}. */ @Override - public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) + public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException { Class targetClass = AopUtils.getTargetClass(bean); TelegramController telegramController = AnnotationUtils.findAnnotation(targetClass, diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/Action.java b/src/main/java/io/github/drednote/telegram/handler/scenario/Action.java index 2f37368..24cd7d4 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/Action.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/Action.java @@ -19,5 +19,5 @@ public interface Action { * @return an optional result of the action execution, or null if there is no result */ @Nullable - Object execute(ActionContext context); + Object execute(ActionContext context) throws Exception; } diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/ActionContext.java b/src/main/java/io/github/drednote/telegram/handler/scenario/ActionContext.java index 470e8df..987e33e 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/ActionContext.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/ActionContext.java @@ -41,4 +41,6 @@ public interface ActionContext { * @return a map of template variable names to their corresponding values */ Map getTemplateVariables(); + + Map getProps(); } diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/SimpleActionContext.java b/src/main/java/io/github/drednote/telegram/handler/scenario/SimpleActionContext.java index f269030..20d6b6c 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/SimpleActionContext.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/SimpleActionContext.java @@ -19,6 +19,7 @@ public class SimpleActionContext implements ActionContext { private final UpdateRequest updateRequest; private final Transition transition; + private final Map props; /** * Retrieves the template variables extracted from the {@code UpdateRequest} based on the @@ -40,6 +41,11 @@ public Map getTemplateVariables() { return res; } + @Override + public Map getProps() { + return props; + } + /** * Retrieves the transition associated with this context. * diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/SimpleScenario.java b/src/main/java/io/github/drednote/telegram/handler/scenario/SimpleScenario.java index f36fac9..dcd107f 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/SimpleScenario.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/SimpleScenario.java @@ -12,7 +12,9 @@ import io.github.drednote.telegram.handler.scenario.persist.ScenarioContext; import io.github.drednote.telegram.handler.scenario.persist.ScenarioPersister; import io.github.drednote.telegram.handler.scenario.persist.StateContext; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -52,8 +54,9 @@ public ScenarioEventResult sendEvent(UpdateRequest request) { } Transition transition = optionalSTransition.get(); State target = transition.getTarget(); + Map props = target.getProps(); - var context = new SimpleActionContext<>(request, transition); + var context = new SimpleActionContext<>(request, transition, new HashMap<>(props)); try { Object response = target.execute(context); ResponseSetter.setResponse(request, response); @@ -101,7 +104,7 @@ public void resetScenario(ScenarioContext context) { private @NonNull SimpleState convertToState(StateContext stateContext) { Set mappings = stateContext.updateRequestMappings(); - SimpleState simpleState = new SimpleState<>(stateContext.id(), convert(mappings)); + SimpleState simpleState = new SimpleState<>(stateContext.id(), convert(mappings), stateContext.props()); simpleState.setResponseMessageProcessing(stateContext.responseMessageProcessing()); return simpleState; } diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/ScenarioBuilder.java b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/ScenarioBuilder.java index 7dab896..8cc8eac 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/ScenarioBuilder.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/ScenarioBuilder.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import lombok.Getter; import lombok.Setter; import org.springframework.lang.Nullable; @@ -26,6 +27,7 @@ public class ScenarioBuilder { @Nullable @Setter + @Getter private S initial; @Nullable @Setter @@ -92,6 +94,7 @@ private static void initTarget( target.setActions(transition.getActions()); target.setMappings(mappings); target.setResponseMessageProcessing(transition.isResponseMessageProcessing()); + target.setProps(transition.getProps()); } public record ScenarioData( diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/ScenarioBaseTransitionConfigurer.java b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/ScenarioBaseTransitionConfigurer.java index 1d21368..ad8174c 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/ScenarioBaseTransitionConfigurer.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/ScenarioBaseTransitionConfigurer.java @@ -3,6 +3,8 @@ import io.github.drednote.telegram.core.request.TelegramRequest; import io.github.drednote.telegram.core.request.UpdateRequestMapping; import io.github.drednote.telegram.handler.scenario.Action; +import io.github.drednote.telegram.handler.scenario.ActionContext; +import java.util.Map; /** * Interface for configuring scenario base transitions. @@ -38,8 +40,8 @@ public interface ScenarioBaseTransitionConfigurer action); /** - * Sets a condition that must be met for a given transition to be called. The matching is - * executing by {@link UpdateRequestMapping} + * Sets a condition that must be met for a given transition to be called. The matching is executing by + * {@link UpdateRequestMapping} * * @param telegramRequest the TelegramRequest to set * @return the current instance of the configurer @@ -47,6 +49,14 @@ public interface ScenarioBaseTransitionConfigurer props); + /** * Finalizes the transition configuration and returns a ScenarioTransitionConfigurer. *

diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/ScenarioRollbackTransitionConfigurer.java b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/ScenarioRollbackTransitionConfigurer.java index 6e557bb..3ab230c 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/ScenarioRollbackTransitionConfigurer.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/ScenarioRollbackTransitionConfigurer.java @@ -2,13 +2,15 @@ import io.github.drednote.telegram.core.request.TelegramRequest; import io.github.drednote.telegram.handler.scenario.Action; +import io.github.drednote.telegram.handler.scenario.ActionContext; +import java.util.Map; /** * Interface for configuring rollback transitions in scenarios. *

- * During this configurer will be created a new one transition with a reverse direction. For - * example, you create A → B transition, there will be created B → A transition with personal - * telegramRequest matching and action (you should specify it). + * During this configurer will be created a new one transition with a reverse direction. For example, you create A → B + * transition, there will be created B → A transition with personal telegramRequest matching and action (you should + * specify it). * * @param the type of the scenario * @author Ivan Galushko @@ -32,4 +34,12 @@ public interface ScenarioRollbackTransitionConfigurer extends */ ScenarioRollbackTransitionConfigurer rollbackTelegramRequest( TelegramRequest telegramRequest); + + /** + * Sets the additional props to be used during the rollback. + * + * @param props additional props to pass to {@link Action} in {@link ActionContext} + * @return the current instance of {@link ScenarioRollbackTransitionConfigurer} + */ + ScenarioRollbackTransitionConfigurer rollbackProps(Map props); } diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioBaseTransitionConfigurer.java b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioBaseTransitionConfigurer.java index ea52a7d..3e6ac34 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioBaseTransitionConfigurer.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioBaseTransitionConfigurer.java @@ -6,7 +6,9 @@ import io.github.drednote.telegram.handler.scenario.configurer.transition.SimpleScenarioTransitionConfigurer.TransitionData; import io.github.drednote.telegram.utils.Assert; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.springframework.lang.Nullable; /** @@ -27,6 +29,7 @@ public abstract class SimpleScenarioBaseTransitionConfigurer props = new HashMap<>(); /** * Constructs a SimpleScenarioBaseTransitionConfigurer with a ScenarioBuilder. @@ -65,12 +68,20 @@ public C telegramRequest(TelegramRequest telegramRequest) { return (C) this; } + @Override + public C props(Map props) { + Assert.notNull(props, "Props"); + this.props = props; + return (C) this; + } + @Override public ScenarioTransitionConfigurer and() { Assert.required(source, "Source"); Assert.required(target, "Target"); Assert.required(request, "TelegramRequest"); - TransitionData transition = new TransitionData<>(source, target, actions, request); + Assert.required(props, "Props"); + TransitionData transition = new TransitionData<>(source, target, actions, request, props); beforeAnd(transition); builder.addTransition(transition); return new SimpleScenarioTransitionConfigurer<>(builder); diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioRollbackTransitionConfigurer.java b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioRollbackTransitionConfigurer.java index 637a4ca..40ebe22 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioRollbackTransitionConfigurer.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioRollbackTransitionConfigurer.java @@ -6,7 +6,9 @@ import io.github.drednote.telegram.handler.scenario.configurer.transition.SimpleScenarioTransitionConfigurer.TransitionData; import io.github.drednote.telegram.utils.Assert; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class SimpleScenarioRollbackTransitionConfigurer extends SimpleScenarioBaseTransitionConfigurer, S> @@ -14,6 +16,7 @@ public class SimpleScenarioRollbackTransitionConfigurer private final List> rollbackActions = new ArrayList<>(); private TelegramRequest rollbackTelegramRequest; + private Map rollbackProps = new HashMap<>(); public SimpleScenarioRollbackTransitionConfigurer(ScenarioBuilder builder) { super(builder); @@ -21,22 +24,31 @@ public SimpleScenarioRollbackTransitionConfigurer(ScenarioBuilder builder) { @Override public ScenarioRollbackTransitionConfigurer rollbackAction(Action action) { + Assert.notNull(action, "Action"); this.rollbackActions.add(action); return this; } @Override public ScenarioRollbackTransitionConfigurer rollbackTelegramRequest(TelegramRequest telegramRequest) { + Assert.notNull(telegramRequest, "TelegramRequest"); this.rollbackTelegramRequest = telegramRequest; return this; } + @Override + public ScenarioRollbackTransitionConfigurer rollbackProps(Map rollbackProps) { + Assert.notNull(props, "RollbackProps"); + this.rollbackProps = rollbackProps; + return this; + } + @Override protected void beforeAnd(TransitionData data) { Assert.required(rollbackTelegramRequest, "RollbackTelegramRequest"); TransitionData transition = new TransitionData<>( - target, source, rollbackActions, rollbackTelegramRequest + target, source, rollbackActions, rollbackTelegramRequest, rollbackProps ); builder.addTransition(transition); } diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioTransitionConfigurer.java b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioTransitionConfigurer.java index 7729e1b..d78b9f2 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioTransitionConfigurer.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/configurer/transition/SimpleScenarioTransitionConfigurer.java @@ -4,6 +4,7 @@ import io.github.drednote.telegram.handler.scenario.Action; import io.github.drednote.telegram.handler.scenario.configurer.ScenarioBuilder; import java.util.List; +import java.util.Map; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -38,6 +39,7 @@ public static class TransitionData { private final S target; private final List> actions; private final TelegramRequest request; + private final Map props; private boolean responseMessageProcessing = false; } } diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/data/SimpleState.java b/src/main/java/io/github/drednote/telegram/handler/scenario/data/SimpleState.java index cc74d15..e8efdf5 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/data/SimpleState.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/data/SimpleState.java @@ -5,7 +5,9 @@ import io.github.drednote.telegram.handler.scenario.Action; import io.github.drednote.telegram.handler.scenario.ActionContext; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import lombok.Getter; import lombok.Setter; @@ -17,6 +19,8 @@ public class SimpleState extends AbstractState { @Nullable private List> actions; + @Setter + private Map props; @Getter @Setter @@ -24,10 +28,12 @@ public class SimpleState extends AbstractState { public SimpleState(S id) { super(id, null); + this.props = new HashMap<>(); } - public SimpleState(S id, Set mappings) { + public SimpleState(S id, Set mappings, Map props) { super(id, mappings); + this.props = props; } @Override @@ -39,11 +45,16 @@ public boolean matches(UpdateRequest request) { } @Override - public Object execute(ActionContext context) { + public Object execute(ActionContext context) throws Exception { if (actions == null) { return null; } - return actions.stream().map(action -> action.execute(context)).toList(); + List list = new ArrayList<>(); + for (Action action : actions) { + Object execute = action.execute(context); + list.add(execute); + } + return list; } @Nullable @@ -54,4 +65,9 @@ public List> getActions() { public void setActions(List> actions) { this.actions = new ArrayList<>(actions); } + + @Override + public Map getProps() { + return props; + } } diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/data/State.java b/src/main/java/io/github/drednote/telegram/handler/scenario/data/State.java index a365261..5d34e4e 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/data/State.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/data/State.java @@ -4,6 +4,7 @@ import io.github.drednote.telegram.core.request.UpdateRequestMappingAccessor; import io.github.drednote.telegram.handler.scenario.Action; import io.github.drednote.telegram.handler.scenario.configurer.transition.ScenarioResponseMessageTransitionConfigurer; +import java.util.Map; import java.util.Set; /** @@ -37,5 +38,7 @@ public interface State extends RequestMatcher, Action { * mappings */ Set getMappings(); + + Map getProps(); } diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/persist/SimpleScenarioPersister.java b/src/main/java/io/github/drednote/telegram/handler/scenario/persist/SimpleScenarioPersister.java index 01f8dcb..281ace9 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/persist/SimpleScenarioPersister.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/persist/SimpleScenarioPersister.java @@ -39,7 +39,7 @@ private ScenarioContext convert(Scenario scenario) { private @NonNull StateContext convertToStateContext(State state) { Set requestMappings = state.getMappings(); return new SimpleStateContext<>( - state.getId(), requestMappings, state.isResponseMessageProcessing() + state.getId(), requestMappings, state.isResponseMessageProcessing(), state.getProps() ); } } diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/persist/SimpleStateContext.java b/src/main/java/io/github/drednote/telegram/handler/scenario/persist/SimpleStateContext.java index 815a3f3..ad6a6f4 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/persist/SimpleStateContext.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/persist/SimpleStateContext.java @@ -1,10 +1,12 @@ package io.github.drednote.telegram.handler.scenario.persist; import io.github.drednote.telegram.core.request.UpdateRequestMappingAccessor; +import java.util.Map; import java.util.Set; public record SimpleStateContext( S id, Set updateRequestMappings, - boolean responseMessageProcessing + boolean responseMessageProcessing, + Map props ) implements StateContext {} diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/persist/StateContext.java b/src/main/java/io/github/drednote/telegram/handler/scenario/persist/StateContext.java index 50a23c5..059c667 100644 --- a/src/main/java/io/github/drednote/telegram/handler/scenario/persist/StateContext.java +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/persist/StateContext.java @@ -2,6 +2,7 @@ import io.github.drednote.telegram.core.request.UpdateRequestMappingAccessor; import io.github.drednote.telegram.handler.scenario.data.State; +import java.util.Map; import java.util.Set; /** @@ -18,4 +19,6 @@ public interface StateContext { boolean responseMessageProcessing(); Set updateRequestMappings(); + + Map props(); } diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryBeanPostProcessor.java b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryBeanPostProcessor.java new file mode 100644 index 0000000..98d15d1 --- /dev/null +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryBeanPostProcessor.java @@ -0,0 +1,63 @@ +package io.github.drednote.telegram.handler.scenario.property; + +import io.github.drednote.telegram.handler.scenario.ActionContext; +import io.github.drednote.telegram.utils.Assert; +import java.lang.reflect.Method; +import java.util.Map; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.MethodIntrospector; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.NonNull; + +/** + * A {@code BeanPostProcessor} that processes beans annotated with {@link TelegramScenario}. It identifies methods + * annotated with {@link TelegramScenarioAction} and registers them with the {@link ScenarioFactoryContainer}. + * + * @author Ivan Galushko + */ +public class ScenarioFactoryBeanPostProcessor implements BeanPostProcessor { + + private final ScenarioFactoryContainer registrar; + + public ScenarioFactoryBeanPostProcessor(ScenarioFactoryContainer registrar) { + Assert.required(registrar, "ScenarioFactoryContainer"); + this.registrar = registrar; + } + + /** + * Processes beans before initialization. For beans annotated with {@link TelegramScenario}, it identifies annotated + * methods and registers them using the {@link ScenarioFactoryContainer}. + */ + @Override + public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) + throws BeansException { + Class targetClass = AopUtils.getTargetClass(bean); + TelegramScenario scenarioFactory = AnnotationUtils.findAnnotation(targetClass, TelegramScenario.class); + if (scenarioFactory != null) { + var annotatedMethods = findAnnotatedMethods(targetClass); + if (!annotatedMethods.isEmpty()) { + annotatedMethods.forEach((method, annotation) -> { + if (method.getParameterTypes().length == 1 + && method.getParameterTypes()[0].equals(ActionContext.class) + ) { + Method invocableMethod = AopUtils.selectInvocableMethod(method, targetClass); + registrar.registerAction(bean, invocableMethod, annotation); + } else { + throw new IllegalStateException( + "The method annotated with TelegramScenarioAction must accept exactly one argument of type ActionContext"); + } + }); + } + } + return bean; + } + + private Map findAnnotatedMethods(Class targetClass) { + return MethodIntrospector.selectMethods(targetClass, + (MethodIntrospector.MetadataLookup) method -> + AnnotatedElementUtils.findMergedAnnotation(method, TelegramScenarioAction.class)); + } +} diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryContainer.java b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryContainer.java new file mode 100644 index 0000000..a596a69 --- /dev/null +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryContainer.java @@ -0,0 +1,49 @@ +package io.github.drednote.telegram.handler.scenario.property; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.lang.Nullable; +import org.springframework.web.method.HandlerMethod; + +public class ScenarioFactoryContainer implements ScenarioFactoryResolver { + + private final Map mappingLookup = new HashMap<>(); + + @Override + @Nullable + public HandlerMethod resolveAction(String name) { + return mappingLookup.get(name); + } + + public void registerAction(Object bean, Method method, TelegramScenarioAction action) { + HandlerMethod handlerMethod = new HandlerMethod(bean, method); + String name = resolveHandlerName(bean, method, action, handlerMethod); + HandlerMethod existingHandler = mappingLookup.get(name); + if (existingHandler != null) { + throw new IllegalStateException( + "\nAmbiguous mapping. Cannot map '" + bean + "' method \n" + + method + "\nto " + name + ": There is already '" + + existingHandler.getBean() + "' bean method\n" + existingHandler + " mapped."); + } else { + mappingLookup.put(name, handlerMethod); + } + } + + private static String resolveHandlerName( + Object bean, Method method, TelegramScenarioAction action, HandlerMethod handlerMethod + ) { + String name; + if (action.value().isEmpty()) { + if (action.fullName()) { + name = handlerMethod.toString(); + } else { + name = AopProxyUtils.ultimateTargetClass(bean).getSimpleName() + "#" + method.getName(); + } + } else { + name = action.value(); + } + return name; + } +} diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryResolver.java b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryResolver.java new file mode 100644 index 0000000..46fe04d --- /dev/null +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioFactoryResolver.java @@ -0,0 +1,10 @@ +package io.github.drednote.telegram.handler.scenario.property; + +import org.springframework.lang.Nullable; +import org.springframework.web.method.HandlerMethod; + +public interface ScenarioFactoryResolver { + + @Nullable + HandlerMethod resolveAction(String name); +} 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 new file mode 100644 index 0000000..e57606c --- /dev/null +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioProperties.java @@ -0,0 +1,180 @@ +package io.github.drednote.telegram.handler.scenario.property; + +import io.github.drednote.telegram.core.annotation.BetaApi; +import io.github.drednote.telegram.core.annotation.TelegramRequest; +import io.github.drednote.telegram.core.request.MessageType; +import io.github.drednote.telegram.core.request.RequestType; +import io.github.drednote.telegram.handler.scenario.ActionContext; +import io.github.drednote.telegram.handler.scenario.configurer.transition.ScenarioExternalTransitionConfigurer; +import io.github.drednote.telegram.handler.scenario.configurer.transition.ScenarioResponseMessageTransitionConfigurer; +import io.github.drednote.telegram.handler.scenario.configurer.transition.ScenarioRollbackTransitionConfigurer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.lang.Nullable; + +/** + * This class holds configuration properties for scenarios. + *

+ * It includes various properties related to scenarios, requests, and rollbacks. + * + * @author Ivan Galushko + */ +@ConfigurationProperties("drednote.telegram.scenario") +@Getter +@Setter +@BetaApi +public class ScenarioProperties { + + /** + * A map of scenario names to their corresponding {@link Scenario} objects. + */ + @Nullable + private Map values; + + /** + * The default rollback configuration, which applies if no another set in scenario object. + */ + @Nullable + private Rollback defaultRollback; + + /** + * Represents a scenario with its associated properties and actions. + */ + @Getter + @Setter + public static class Scenario { + + /** + * The request associated with this scenario. + */ + private Request request; + /** + * A set of action references names related to this scenario. The name must be exactly the same as in + * {@link TelegramScenarioAction}. + */ + private Set actionReferences; + /** + * The type of transition for this scenario. Defaults to {@link TransitionType#EXTERNAL}. + */ + private TransitionType type = TransitionType.EXTERNAL; + /** + * The source state identifier for this scenario. + */ + private String source; + /** + * The target state identifier for this scenario. + */ + private String target; + /** + * A list of nodes representing the graph of this scenario. The graph is explaining the connections between + * different scenarios. The path from one scenario to others. ID is the same as a key in {@link #steps} + *

+ * Example: + *

{@code   graph:
+         *     - id: get
+         *     - id: change
+         *       children:
+         *         - id: lang-change}
+         *  
+ */ + private List graph = new ArrayList<>(); + /** + * The rollback configuration, applicable only if the type is {@link TransitionType#ROLLBACK}. + */ + @Nullable + private Rollback rollback; + /** + * A map of additional properties related to this scenario. This props pass to the {@link ActionContext}. + */ + private Map props = new HashMap<>(); + /** + * A map of steps associated with this scenario, where each step is another scenario. The keys are the same as + * {@link Node#id} in {@link #graph}. + */ + private Map steps = new HashMap<>(); + + /** + * Enum representing the types of transitions for a scenario. + */ + public enum TransitionType { + /** + * The type associated with a {@link ScenarioRollbackTransitionConfigurer} + */ + ROLLBACK, + /** + * The type associated with a {@link ScenarioResponseMessageTransitionConfigurer} + */ + RESPONSE_MESSAGE_PROCESSING, + /** + * The type associated with a {@link ScenarioExternalTransitionConfigurer} + */ + EXTERNAL + } + } + + /** + * Represents a node in the scenario graph. + */ + @Getter + @Setter + public static class Node { + + /** + * The unique identifier for this node. + */ + private String id; + /** + * A list of child nodes for this node. + */ + private List children = new ArrayList<>(); + } + + /** + * Represents a request configuration for a scenario. + */ + @Getter + @Setter + public static class Request { + + /** + * @see TelegramRequest#pattern() + */ + private Set patterns; + /** + * @see TelegramRequest#requestType() + */ + private Set requestTypes; + /** + * @see TelegramRequest#messageType() + */ + private Set messageTypes = new HashSet<>(); + /** + * @see TelegramRequest#exclusiveMessageType() + */ + private boolean exclusiveMessageType = false; + } + + /** + * Represents a rollback configuration for a scenario. + */ + @Getter + @Setter + public static class Rollback { + + /** + * The request associated with the rollback. + */ + private Request request; + /** + * @see Scenario#actionReferences + */ + private Set actionReferences; + } +} 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 new file mode 100644 index 0000000..86eba2b --- /dev/null +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/property/ScenarioPropertiesConfigurer.java @@ -0,0 +1,138 @@ +package io.github.drednote.telegram.handler.scenario.property; + +import static org.apache.commons.lang3.ObjectUtils.firstNonNull; + +import io.github.drednote.telegram.core.invoke.InvocableHandlerMethod; +import io.github.drednote.telegram.core.request.RequestType; +import io.github.drednote.telegram.core.request.TelegramRequest; +import io.github.drednote.telegram.core.request.TelegramRequestImpl; +import io.github.drednote.telegram.handler.scenario.Action; +import io.github.drednote.telegram.handler.scenario.property.ScenarioProperties.Node; +import io.github.drednote.telegram.handler.scenario.property.ScenarioProperties.Request; +import io.github.drednote.telegram.handler.scenario.property.ScenarioProperties.Rollback; +import io.github.drednote.telegram.handler.scenario.property.ScenarioProperties.Scenario; +import io.github.drednote.telegram.handler.scenario.property.ScenarioProperties.Scenario.TransitionType; +import io.github.drednote.telegram.handler.scenario.configurer.ScenarioBuilder; +import io.github.drednote.telegram.handler.scenario.configurer.transition.SimpleScenarioTransitionConfigurer.TransitionData; +import io.github.drednote.telegram.utils.Assert; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.springframework.lang.Nullable; +import org.springframework.web.method.HandlerMethod; + +public class ScenarioPropertiesConfigurer { + + private final ScenarioProperties scenarioProperties; + private final ScenarioFactoryResolver scenarioFactoryResolver; + + public ScenarioPropertiesConfigurer( + ScenarioProperties scenarioProperties, + ScenarioFactoryResolver scenarioFactoryResolver + ) { + Assert.required(scenarioProperties, "ScenarioProperties"); + Assert.required(scenarioFactoryResolver, "ScenarioFactoryResolver"); + this.scenarioProperties = scenarioProperties; + this.scenarioFactoryResolver = scenarioFactoryResolver; + } + + public void configure(ScenarioBuilder scenarioBuilder) { + 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); + }); + }); + } + } + + private void doConfigure( + ScenarioBuilder scenarioBuilder, TransitionData parent, Scenario scenario, Node node + ) { + Scenario child = scenario.getSteps().get(node.getId()); + if (child == null) { + throw new IllegalArgumentException("Step '" + node.getId() + "' does not exist"); + } + TransitionData transitionData = configureTransition(scenarioBuilder, child, parent); + node.getChildren().forEach(childNode -> { + doConfigure(scenarioBuilder, transitionData, scenario, childNode); + }); + } + + @NotNull + private TransitionData configureTransition( + ScenarioBuilder scenarioBuilder, Scenario scenario, @Nullable TransitionData parent + ) { + Request request = scenario.getRequest(); + Set actionClassName = scenario.getActionReferences(); + String target = scenario.getTarget(); + Object source = parent != null ? scenario.getSource() : scenarioBuilder.getInitial(); + + Assert.required(target, "Target state"); + Assert.required(source, "Source state"); + Assert.required(scenario, "Scenario"); + Assert.required(request, "Request"); + + List> action = createAction(actionClassName); + TelegramRequest telegramRequest = createTelegramRequest(request); + + TransitionData transitionData = new TransitionData<>( + source, target, action, telegramRequest, scenario.getProps() + ); + + if (scenario.getType() == TransitionType.RESPONSE_MESSAGE_PROCESSING) { + transitionData.setResponseMessageProcessing(true); + } else if (scenario.getType() == TransitionType.ROLLBACK) { + Rollback rollback = firstNonNull(scenario.getRollback(), scenarioProperties.getDefaultRollback()); + if (parent == null || rollback == null) { + throw new IllegalArgumentException( + "Parent transition or rollback section cannot be null if transition type is Rollback"); + } + TransitionData rollbackTransitionData = new TransitionData<>( + target, source, createAction(rollback.getActionReferences()), + createTelegramRequest(rollback.getRequest()), parent.getProps()); + scenarioBuilder.addTransition((TransitionData) rollbackTransitionData); + } + + scenarioBuilder.addTransition((TransitionData) transitionData); + return (TransitionData) transitionData; + } + + private List> createAction(@Nullable Set actionReference) { + List> response = new ArrayList<>(); + if (actionReference == null) { + return response; + } + for (String name : actionReference) { + HandlerMethod handlerMethod = scenarioFactoryResolver.resolveAction(name); + if (handlerMethod == null) { + throw new IllegalArgumentException("Action class name '" + name + "' not found"); + } + InvocableHandlerMethod invocableHandlerMethod = new InvocableHandlerMethod(handlerMethod, + "ScenarioFactory"); + response.add(invocableHandlerMethod::invoke); + } + return response; + } + + @NotNull + private static TelegramRequest createTelegramRequest(Request request) { + Set pattern = request.getPatterns(); + Set requestType = request.getRequestTypes(); + + Assert.required(pattern, "At least one pattern"); + Assert.required(requestType, "At least one request type"); + + return new TelegramRequestImpl( + pattern, requestType, request.getMessageTypes(), request.isExclusiveMessageType() + ); + } +} diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/property/TelegramScenario.java b/src/main/java/io/github/drednote/telegram/handler/scenario/property/TelegramScenario.java new file mode 100644 index 0000000..527c0aa --- /dev/null +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/property/TelegramScenario.java @@ -0,0 +1,25 @@ +package io.github.drednote.telegram.handler.scenario.property; + +import io.github.drednote.telegram.core.annotation.BetaApi; +import io.github.drednote.telegram.core.annotation.TelegramAdvice; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.stereotype.Component; + +/** + * The {@code TelegramScenario} annotation is annotation that marks a class as a handler for telegram scenario actions. + * + * @author Ivan Galushko + * @see TelegramScenarioAction + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +@BetaApi +public @interface TelegramScenario { + +} diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/property/TelegramScenarioAction.java b/src/main/java/io/github/drednote/telegram/handler/scenario/property/TelegramScenarioAction.java new file mode 100644 index 0000000..c0ce29f --- /dev/null +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/property/TelegramScenarioAction.java @@ -0,0 +1,59 @@ +package io.github.drednote.telegram.handler.scenario.property; + +import io.github.drednote.telegram.core.annotation.BetaApi; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The {@code TelegramScenarioAction} annotation is annotation that marks a method as a handler for telegram scenario + * action. + *

+ * This annotation worked in a pair with {@link TelegramScenario}. The class should be marked with + * {@code TelegramScenario} and methods of this class, should be marked with {@code TelegramScenarioAction} + *

+ * Example usage: + *

+ * {@code
+ * @TelegramScenario
+ * public class TelegramSettingsFactory {
+ *
+ *     @TelegramScenarioAction
+ *     public Object returnSettingsMenu(ActionContext context) throws Exception {
+ *         // your code here
+ *     }
+ * }
+ * }
+ * 
+ * This method you can reference by {@code TelegramSettingsFactory#returnSettingsMenu}.
+ *
+ * @author Ivan Galushko
+ * @see TelegramScenarioAction
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@BetaApi
+public @interface TelegramScenarioAction {
+
+    /**
+     * Name of reference. Default {@code Class#methodName} or {@code com.example.Class#methodName(String)} if parameter
+     * {@link #fullName()} is true.
+     *
+     * @return name of reference
+     */
+    String value() default "";
+
+    /**
+     * Define when to use full name of method handler instead of simple name.
+     * 

+ * Full name - {@code com.example.Class#methodName(String)} + *

+ * Simple - {@code Class#methodName} + * + * @return to use full class name or not + */ + boolean fullName() default false; +} diff --git a/src/main/java/io/github/drednote/telegram/handler/scenario/property/package-info.java b/src/main/java/io/github/drednote/telegram/handler/scenario/property/package-info.java new file mode 100644 index 0000000..6875ea5 --- /dev/null +++ b/src/main/java/io/github/drednote/telegram/handler/scenario/property/package-info.java @@ -0,0 +1,8 @@ +@NonNullApi +@NonNullFields +@BetaApi +package io.github.drednote.telegram.handler.scenario.property; + +import io.github.drednote.telegram.core.annotation.BetaApi; +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; \ No newline at end of file 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 f75ff85..418b53c 100644 --- a/src/main/java/io/github/drednote/telegram/session/SessionAutoConfiguration.java +++ b/src/main/java/io/github/drednote/telegram/session/SessionAutoConfiguration.java @@ -160,6 +160,7 @@ private static HttpComponentsClientHttpRequestFactory configureProxy(ProxyUrl pr } @Bean + @ConditionalOnMissingBean public org.telegram.telegrambots.meta.generics.TelegramClient absSender( SessionProperties properties, TelegramProperties telegramProperties) { OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder(); diff --git a/src/main/java/io/github/drednote/telegram/session/SessionProperties.java b/src/main/java/io/github/drednote/telegram/session/SessionProperties.java index 35bbb1a..70da443 100644 --- a/src/main/java/io/github/drednote/telegram/session/SessionProperties.java +++ b/src/main/java/io/github/drednote/telegram/session/SessionProperties.java @@ -108,9 +108,12 @@ public void setProxyUrl(@Nullable String proxyUrl) { @RequiredArgsConstructor public static class ProxyUrl { + @NonNull private final String host; private final int port; + @Nullable private String userName; + @Nullable private char[] password; } diff --git a/src/test/java/io/github/drednote/telegram/datasource/scenario/ScenarioKryoSerializationServiceTest.java b/src/test/java/io/github/drednote/telegram/datasource/scenario/ScenarioKryoSerializationServiceTest.java index 6eb028f..841093c 100644 --- a/src/test/java/io/github/drednote/telegram/datasource/scenario/ScenarioKryoSerializationServiceTest.java +++ b/src/test/java/io/github/drednote/telegram/datasource/scenario/ScenarioKryoSerializationServiceTest.java @@ -6,6 +6,9 @@ import io.github.drednote.telegram.handler.scenario.persist.SimpleScenarioContext; import io.github.drednote.telegram.handler.scenario.persist.SimpleStateContext; import java.io.IOException; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.Set; import lombok.AllArgsConstructor; @@ -21,9 +24,9 @@ class ScenarioKryoSerializationServiceTest { @Test void shouldCorrectSerializeAndDeserialize() throws IOException { SimpleStateContext> source = new SimpleStateContext<>(new StateClass(State.SOURCE), - Set.of(), false); + Set.of(), false, new HashMap<>()); SimpleStateContext> target = new SimpleStateContext<>(new StateClass(State.TARGET), - Set.of(), false); + Set.of(), false, Map.of("1", BigDecimal.ONE)); ScenarioContext> scenario = new SimpleScenarioContext<>("id", target); byte[] bytes = serializationService.serialize(scenario); diff --git a/src/test/java/io/github/drednote/telegram/handler/scenario/ScenarioTest.java b/src/test/java/io/github/drednote/telegram/handler/scenario/ScenarioTest.java index d8cc157..1d6b22d 100644 --- a/src/test/java/io/github/drednote/telegram/handler/scenario/ScenarioTest.java +++ b/src/test/java/io/github/drednote/telegram/handler/scenario/ScenarioTest.java @@ -46,12 +46,12 @@ void setUp() { SimpleState initialState = new SimpleState<>("1"); nextState2 = new SimpleState<>("2"); SimpleState nextState3 = new SimpleState<>("3", - Set.of(new UpdateRequestMapping("**", RequestType.MESSAGE, Set.of()))); + Set.of(new UpdateRequestMapping("**", RequestType.MESSAGE, Set.of())), new HashMap<>()); SimpleState nextState4 = new SimpleState<>("4", - Set.of(new UpdateRequestMapping("**", RequestType.MESSAGE, Set.of()))); + Set.of(new UpdateRequestMapping("**", RequestType.MESSAGE, Set.of())), new HashMap<>()); SimpleTransition transitionFrom1To2 = new SimpleTransition<>(initialState, - new SimpleState<>("2", Set.of(new UpdateRequestMapping("**", RequestType.POLL, Set.of())))); + new SimpleState<>("2", Set.of(new UpdateRequestMapping("**", RequestType.POLL, Set.of())), new HashMap<>())); states.put(initialState, List.of( transitionFrom1To2, new SimpleTransition<>(initialState, nextState3))); states.put(nextState2, List.of(new SimpleTransition<>(nextState2, nextState4))); @@ -80,7 +80,7 @@ void shouldCorrectChooseNextStateIfPersisterNotEmpty() throws IOException { String id = "2"; adapter.save(new SimpleScenarioContext<>(id, new SimpleStateContext<>(nextState2.getId(), - Set.of(new UpdateRequestMapping("**", RequestType.POLL, Set.of())), false))); + Set.of(new UpdateRequestMapping("**", RequestType.POLL, Set.of())), false, new HashMap<>()))); Scenario scenario = factory.create(id); persister.restore(scenario, id); diff --git a/src/test/java/io/github/drednote/telegram/handler/scenario/configurer/ScenarioBuilderTest.java b/src/test/java/io/github/drednote/telegram/handler/scenario/configurer/ScenarioBuilderTest.java index 52a10bb..d556022 100644 --- a/src/test/java/io/github/drednote/telegram/handler/scenario/configurer/ScenarioBuilderTest.java +++ b/src/test/java/io/github/drednote/telegram/handler/scenario/configurer/ScenarioBuilderTest.java @@ -14,6 +14,7 @@ import io.github.drednote.telegram.handler.scenario.data.Transition; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -31,8 +32,8 @@ public void testBuildStates() { List> actions = Collections.singletonList(Mockito.mock(Action.class)); // Создание TransitionData - TransitionData transition1 = new TransitionData<>("state1", "state2", actions, requestMock); - TransitionData transition2 = new TransitionData<>("state2", "state3", actions, requestMock); + TransitionData transition1 = new TransitionData<>("state1", "state2", actions, requestMock, new HashMap<>()); + TransitionData transition2 = new TransitionData<>("state2", "state3", actions, requestMock, new HashMap<>()); List> transitionDataList = Arrays.asList(transition1, transition2); @@ -47,8 +48,8 @@ public void testBuildStates() { builder.forEach(mappings::add); SimpleState state1 = new SimpleState<>("state1"); SimpleState state2Null = new SimpleState<>("state2"); - SimpleState state2 = new SimpleState<>("state2", mappings); - SimpleState state3 = new SimpleState<>("state3", mappings); + SimpleState state2 = new SimpleState<>("state2", mappings, new HashMap<>()); + SimpleState state3 = new SimpleState<>("state3", mappings, new HashMap<>()); // Проверка переходов из state1 в state2 и из state2 в state3 assertEquals(1, result.get(state1).size()); @@ -83,8 +84,8 @@ public void testBuildStatesAmbiguousMapping() { List> actions = Collections.singletonList(Mockito.mock(Action.class)); // Создание TransitionData с одинаковым source - TransitionData transition1 = new TransitionData<>("state1", "state2", actions, requestMock); - TransitionData transition2 = new TransitionData<>("state1", "state2", actions, requestMock); + TransitionData transition1 = new TransitionData<>("state1", "state2", actions, requestMock, new HashMap<>()); + TransitionData transition2 = new TransitionData<>("state1", "state2", actions, requestMock, new HashMap<>()); List> transitionDataList = Arrays.asList(transition1, transition2); @@ -104,8 +105,8 @@ void shouldNotOverrideMappings() { List> actions = Collections.singletonList(Mockito.mock(Action.class)); // Создание TransitionData - TransitionData transition1 = new TransitionData<>("state1", "state3", actions, requestMock); - TransitionData transition2 = new TransitionData<>("state2", "state3", actions, requestMock2); + TransitionData transition1 = new TransitionData<>("state1", "state3", actions, requestMock, new HashMap<>()); + TransitionData transition2 = new TransitionData<>("state2", "state3", actions, requestMock2, new HashMap<>()); List> transitionDataList = Arrays.asList(transition1, transition2); @@ -120,9 +121,9 @@ void shouldNotOverrideMappings() { List> transitions2 = result.get(new SimpleState<>("state2")); assertThat(transitions1).hasSize(1); - assertThat(transitions1.get(0).getTarget()).isEqualTo(new SimpleState<>("state3", mappings1)); + assertThat(transitions1.get(0).getTarget()).isEqualTo(new SimpleState<>("state3", mappings1, new HashMap<>())); assertThat(transitions2).hasSize(1); - assertThat(transitions2.get(0).getTarget()).isEqualTo(new SimpleState<>("state3", mappings2)); + assertThat(transitions2.get(0).getTarget()).isEqualTo(new SimpleState<>("state3", mappings2, new HashMap<>())); } } \ No newline at end of file diff --git a/src/test/java/io/github/drednote/telegram/handler/scenario/property/ScenarioPropertiesConfigurerTest.java b/src/test/java/io/github/drednote/telegram/handler/scenario/property/ScenarioPropertiesConfigurerTest.java new file mode 100644 index 0000000..5ae6bad --- /dev/null +++ b/src/test/java/io/github/drednote/telegram/handler/scenario/property/ScenarioPropertiesConfigurerTest.java @@ -0,0 +1,101 @@ +package io.github.drednote.telegram.handler.scenario.property; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + +import io.github.drednote.telegram.core.request.MessageType; +import io.github.drednote.telegram.core.request.RequestType; +import io.github.drednote.telegram.core.request.UpdateRequest; +import io.github.drednote.telegram.core.request.UpdateRequestMapping; +import io.github.drednote.telegram.handler.scenario.ActionContext; +import io.github.drednote.telegram.handler.scenario.SimpleActionContext; +import io.github.drednote.telegram.handler.scenario.configurer.ScenarioBuilder; +import io.github.drednote.telegram.handler.scenario.configurer.ScenarioBuilder.ScenarioData; +import io.github.drednote.telegram.handler.scenario.data.State; +import io.github.drednote.telegram.handler.scenario.data.Transition; +import io.github.drednote.telegram.handler.scenario.property.ScenarioPropertiesConfigurerTest.TestScenarioFactory; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = { + ConfigurationPropertiesAutoConfiguration.class, TestScenarioFactory.class, + ScenarioFactoryContainer.class, ScenarioFactoryBeanPostProcessor.class +}) +@EnableConfigurationProperties(ScenarioProperties.class) +@ActiveProfiles("scenarioproperties") +class ScenarioPropertiesConfigurerTest { + + @Autowired + private ScenarioProperties scenarioProperties; + @Autowired + private ScenarioFactoryContainer scenarioFactoryContainer; + private static boolean executed = false; + + @Test + void shouldCorrectCreateTransitionsFromProperties() { + assertThat(scenarioFactoryContainer.resolveAction( + "io.github.drednote.telegram.handler.scenario.property.ScenarioPropertiesConfigurerTest$TestScenarioFactory#name(ActionContext)")).isNotNull(); + assertThat(scenarioFactoryContainer.resolveAction("test_name")).isNotNull(); + + ScenarioPropertiesConfigurer configurer = new ScenarioPropertiesConfigurer(scenarioProperties, + scenarioFactoryContainer); + ScenarioBuilder scenarioBuilder = new ScenarioBuilder<>(); + scenarioBuilder.setInitial(StateEnum.INITIAL); + configurer.configure(scenarioBuilder); + + ScenarioData build = scenarioBuilder.build(); + assertThat(build).isNotNull(); + assertThat(build.states()).hasSize(4); + for (Entry, List>> entry : build.states().entrySet()) { + if (entry.getKey().getId().equals(StateEnum.INITIAL)) { + assertThat(entry.getValue()).hasSize(1); + Transition transition = entry.getValue().get(0); + assertThat(transition.getSource().getId()).isEqualTo(StateEnum.INITIAL); + State target = transition.getTarget(); + assertThat(target.getId()).isEqualTo("TELEGRAM_CHOICE"); + assertThat(target.getMappings()).hasSize(1); + assertThat(target.getProps()).hasSize(2); + assertThat( + target.getMappings().contains(new UpdateRequestMapping("/telegramsettings", RequestType.MESSAGE, + Set.of(MessageType.COMMAND), false))).isTrue(); + assertThatNoException().isThrownBy(() -> target.execute(new SimpleActionContext<>( + Mockito.mock(UpdateRequest.class), Mockito.mock(Transition.class), new HashMap<>()))); + assertThat(executed).isTrue(); + } + } + } + + @TelegramScenario + @Slf4j + static class TestScenarioFactory { + + @TelegramScenarioAction(fullName = true) + public void name(ActionContext context) { + executed = true; + } + + @TelegramScenarioAction("test_name") + public void name2(ActionContext context) { + executed = true; + } + + @TelegramScenarioAction + public void name3(ActionContext context) { + executed = true; + } + } + + enum StateEnum { + INITIAL + } +} \ No newline at end of file diff --git a/src/test/java/io/github/drednote/telegram/session/DefaultTelegramUpdateProcessorTest.java b/src/test/java/io/github/drednote/telegram/session/DefaultTelegramUpdateProcessorTest.java index c659904..c209718 100644 --- a/src/test/java/io/github/drednote/telegram/session/DefaultTelegramUpdateProcessorTest.java +++ b/src/test/java/io/github/drednote/telegram/session/DefaultTelegramUpdateProcessorTest.java @@ -55,8 +55,8 @@ void setUp() { filterProperties.setUserRateLimitUnit(ChronoUnit.MILLIS); } - @RepeatedTest(20) - @Timeout(value = 10, threadMode = ThreadMode.SEPARATE_THREAD) + @RepeatedTest(5) + @Timeout(value = 2, threadMode = ThreadMode.SEPARATE_THREAD) void shouldWaitIfMaxQueueSizeExceed() throws InterruptedException { sessionProperties.setCacheLiveDuration(100); session = new DefaultTelegramUpdateProcessor(sessionProperties, filterProperties, diff --git a/src/test/resources/application-scenarioproperties.yaml b/src/test/resources/application-scenarioproperties.yaml new file mode 100644 index 0000000..607c7ec --- /dev/null +++ b/src/test/resources/application-scenarioproperties.yaml @@ -0,0 +1,66 @@ +drednote: + telegram: + scenario: + values: + telegram-settings: + type: Response_message_processing + request: + patterns: [/telegramsettings] + requestTypes: [MESSAGE] + messageTypes: [COMMAND] + action-references: + - "io.github.drednote.telegram.handler.scenario.property.ScenarioPropertiesConfigurerTest$TestScenarioFactory#name(ActionContext)" + #source: will be automatically put here because of this source is Initial + target: TELEGRAM_CHOICE + props: + text: 'choose:' + keyboard: + - - text: get + value: telegram-settings_get + - - text: change + value: telegram-settings_change + graph: + - id: get + children: + - id: test + steps: + get: + type: Rollback + request: + patterns: [telegram-settings_get] + requestTypes: [CALLBACK_QUERY] + action-references: + - "TestScenarioFactory#name3" + source: TELEGRAM_CHOICE + target: GET_TELEGRAM_SETTINGS + rollback: + action-references: + - "test_name" + request: + patterns: [rollback] + requestTypes: [CALLBACK_QUERY] + props: + text: 'settings:' + keyboard: + - - text: back + value: rollback + test: + type: Rollback + request: + patterns: [ telegram-settings_test ] + requestTypes: [ CALLBACK_QUERY ] + action-references: + - "TestScenarioFactory#name3" + source: GET_TELEGRAM_SETTINGS + target: TEST_TELEGRAM_SETTINGS + rollback: + action-references: + - "test_name" + request: + patterns: [ rollback ] + requestTypes: [ CALLBACK_QUERY ] + props: + text: 'settings:' + keyboard: + - - text: back + value: rollback \ No newline at end of file