From c4db3be4cb565b9c2462691ba68fb7083c21893f Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Tue, 4 Apr 2023 17:48:27 +0200 Subject: [PATCH 01/13] feat: first implementation of help command, wip --- README.md | 8 +- pom.xml | 7 + .../polpetta/mezzotre/InjectionModule.java | 12 ++ .../i18n/TemplateContentGenerator.java | 49 +++++ .../callbackquery/SelectLanguageTutorial.java | 49 +---- .../callbackquery/di/CallbackQuery.java | 11 +- .../mezzotre/telegram/command/Help.java | 83 +++++++++ .../mezzotre/telegram/command/NotFound.java | 37 ++++ .../telegram/command/NotFoundFactory.java | 5 + .../mezzotre/telegram/command/Processor.java | 16 +- .../mezzotre/telegram/command/Router.java | 25 +-- .../mezzotre/telegram/command/Start.java | 71 +++---- .../mezzotre/telegram/command/di/Command.java | 37 +++- .../mezzotre/telegram/model/Help.java | 18 ++ src/main/resources/i18n/message.properties | 5 + src/main/resources/template/telegram/help.vm | 7 + .../selectLanguageTutorial.vm | 0 .../{command/start.0.vm => telegram/start.vm} | 0 ...SelectLanguageTutorialIntegrationTest.java | 4 +- .../telegram/command/HelpIntegrationTest.java | 173 ++++++++++++++++++ .../mezzotre/telegram/command/RouterTest.java | 19 +- .../command/StartIntegrationTest.java | 15 +- 22 files changed, 526 insertions(+), 125 deletions(-) create mode 100644 src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java create mode 100644 src/main/resources/template/telegram/help.vm rename src/main/resources/template/{callbackQuery => telegram}/selectLanguageTutorial.vm (100%) rename src/main/resources/template/{command/start.0.vm => telegram/start.vm} (100%) create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java diff --git a/README.md b/README.md index 611bb8c..72a0980 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,22 @@ curl -F "url=https://example.com/api/tg" \ Build is achieved through Maven. To build a `jar` run: ```shell -mvn package -DskipTests=true -Dmaven.test.skip=true -Dmaven.site.skip=true -Dmaven.javadoc.skip=true +./mvnw package -DskipTests=true -Dmaven.test.skip=true -Dmaven.site.skip=true -Dmaven.javadoc.skip=true ``` -In the `target/` folder wou will find an uber-jar and optionally the possibility to run it via a script and to setup +In the `target/` folder you will find an uber-jar and optionally the possibility to run it via a script and to setup auto-startup via systemd or openrc units. ## Developing ### Automatic testing -You can simply run tests with `mvn test`. This will run `UT` and `IT` tests together. +You can simply run tests with `./mvnw test`. This will run `UT` and `IT` tests together. ### Manual testing For a manual approach, just open a terminal and type `mvn jooby:run`. Assuming you have a database locally available -(check out [application.conf](conf/application.conf)) and a valid Telegram token set (maybe as enviroment variable) you +(check out [application.conf](conf/application.conf)) and a valid Telegram token set (maybe as environment variable) you can develop and see live changes of your Mezzotre on the fly. Finally, by using Postman, you can simulate incoming Telegram events. diff --git a/pom.xml b/pom.xml index 529f51d..bb22800 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,7 @@ 1.1.1 2.13.3 5.9.1 + 5.1.0 @@ -86,6 +87,12 @@ jooby-guice + + com.google.inject.extensions + guice-assistedinject + ${google-guice.version} + + io.swagger.core.v3 swagger-annotations diff --git a/src/main/java/com/github/polpetta/mezzotre/InjectionModule.java b/src/main/java/com/github/polpetta/mezzotre/InjectionModule.java index 4d0d991..528e669 100644 --- a/src/main/java/com/github/polpetta/mezzotre/InjectionModule.java +++ b/src/main/java/com/github/polpetta/mezzotre/InjectionModule.java @@ -3,9 +3,14 @@ package com.github.polpetta.mezzotre; import com.google.inject.AbstractModule; import com.google.inject.Provides; import io.jooby.Jooby; +import javax.inject.Named; +import javax.inject.Singleton; import org.slf4j.Logger; public class InjectionModule extends AbstractModule { + + // In the future we can get this name from mvn, by now it is good as it is + public static final String APPLICATION_NAME = "Mezzotre"; private final Jooby jooby; public InjectionModule(Jooby jooby) { @@ -16,4 +21,11 @@ public class InjectionModule extends AbstractModule { public Logger getLogger() { return jooby.getLog(); } + + @Named("applicationName") + @Singleton + @Provides + public String getAppName() { + return APPLICATION_NAME; + } } diff --git a/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java b/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java new file mode 100644 index 0000000..bb98afe --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java @@ -0,0 +1,49 @@ +package com.github.polpetta.mezzotre.i18n; + +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.function.Consumer; +import javax.inject.Inject; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.tools.ToolManager; +import org.apache.velocity.util.StringBuilderWriter; + +public class TemplateContentGenerator { + + private final LocalizedMessageFactory localizedMessageFactory; + + @Inject + public TemplateContentGenerator(LocalizedMessageFactory localizedMessageFactory) { + this.localizedMessageFactory = localizedMessageFactory; + } + + public String mergeTemplate( + Consumer velocityContextConsumer, + String localeAsString, + String templateName) { + final Locale locale = Locale.forLanguageTag(localeAsString); + final ToolManager toolManager = localizedMessageFactory.createVelocityToolManager(locale); + final VelocityContext velocityContext = new VelocityContext(toolManager.createContext()); + + velocityContextConsumer.accept(velocityContext); + + final StringBuilder content = new StringBuilder(); + final StringBuilderWriter stringBuilderWriter = new StringBuilderWriter(content); + + toolManager + .getVelocityEngine() + .mergeTemplate( + templateName, StandardCharsets.UTF_8.name(), velocityContext, stringBuilderWriter); + + stringBuilderWriter.close(); + return content.toString(); + } + + public String getString(Locale locale, String key) { + return localizedMessageFactory.createResourceBundle(locale).getString(key); + } + + public String getString(String localeAsString, String key) { + return getString(Locale.forLanguageTag(localeAsString), key); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java index c52ad0d..fc47ebd 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java @@ -1,6 +1,6 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; -import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; +import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QTgChat; @@ -14,9 +14,6 @@ import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.EditMessageText; import com.pengrad.telegrambot.request.SendMessage; -import io.vavr.control.Try; -import java.nio.charset.StandardCharsets; -import java.util.Locale; import java.util.NoSuchElementException; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -24,9 +21,6 @@ import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; -import org.apache.velocity.VelocityContext; -import org.apache.velocity.tools.ToolManager; -import org.apache.velocity.util.StringBuilderWriter; import org.slf4j.Logger; /** @@ -41,7 +35,7 @@ public class SelectLanguageTutorial implements Processor { public static final String EVENT_NAME = "selectLanguageTutorial"; private final Executor threadPool; - private final LocalizedMessageFactory localizedMessageFactory; + private final TemplateContentGenerator templateContentGenerator; private final Logger log; private final UUIDGenerator uuidGenerator; @@ -89,11 +83,11 @@ public class SelectLanguageTutorial implements Processor { @Inject public SelectLanguageTutorial( @Named("eventThreadPool") Executor threadPool, - LocalizedMessageFactory localizedMessageFactory, + TemplateContentGenerator templateContentGenerator, Logger log, UUIDGenerator uuidGenerator) { this.threadPool = threadPool; - this.localizedMessageFactory = localizedMessageFactory; + this.templateContentGenerator = templateContentGenerator; this.log = log; this.uuidGenerator = uuidGenerator; } @@ -148,32 +142,11 @@ public class SelectLanguageTutorial implements Processor { // If we are here then we're sure there is at least a chat associated with this callback tgChat -> { final String message = - Try.of( - () -> { - final Locale locale = Locale.forLanguageTag(tgChat.getLocale()); - final ToolManager toolManager = - localizedMessageFactory.createVelocityToolManager(locale); - final VelocityContext velocityContext = - new VelocityContext(toolManager.createContext()); - - velocityContext.put("hasHelpBeenShown", tgChat.getHasHelpBeenShown()); - - final StringBuilder content = new StringBuilder(); - final StringBuilderWriter stringBuilderWriter = - new StringBuilderWriter(content); - - toolManager - .getVelocityEngine() - .mergeTemplate( - "/template/callbackQuery/selectLanguageTutorial.vm", - StandardCharsets.UTF_8.name(), - velocityContext, - stringBuilderWriter); - - stringBuilderWriter.close(); - return content.toString(); - }) - .get(); + templateContentGenerator.mergeTemplate( + velocityContext -> + velocityContext.put("hasHelpBeenShown", tgChat.getHasHelpBeenShown()), + tgChat.getLocale(), + "/template/telegram/selectLanguageTutorial.vm"); log.trace("SelectLanguageTutorial event - message to send back: " + message); @@ -193,9 +166,7 @@ public class SelectLanguageTutorial implements Processor { if (!tgChat.getHasHelpBeenShown()) { // Add a button to show all the possible commands final String showMeTutorialString = - localizedMessageFactory - .createResourceBundle(Locale.forLanguageTag(tgChat.getLocale())) - .getString("button.showMeTutorial"); + templateContentGenerator.getString(tgChat.getLocale(), "button.showMeTutorial"); final CallbackQueryMetadata callbackQueryMetadata = new CallbackQueryMetadata.CallbackQueryMetadataBuilder() diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/di/CallbackQuery.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/di/CallbackQuery.java index 0e252d4..6dfb00d 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/di/CallbackQuery.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/di/CallbackQuery.java @@ -5,16 +5,21 @@ import com.github.polpetta.mezzotre.telegram.callbackquery.SelectLanguageTutoria import com.github.polpetta.mezzotre.telegram.callbackquery.ShowHelp; import com.google.inject.AbstractModule; import com.google.inject.Provides; -import java.util.Set; +import java.util.HashMap; +import java.util.Map; import javax.inject.Named; import javax.inject.Singleton; public class CallbackQuery extends AbstractModule { + @Provides @Singleton @Named("eventProcessors") - public Set getEventProcessor( + public Map getEventProcessor( SelectLanguageTutorial selectLanguageTutorial, ShowHelp showHelp) { - return Set.of(selectLanguageTutorial, showHelp); + final HashMap commandMap = new HashMap<>(); + commandMap.put(selectLanguageTutorial.getEventName(), selectLanguageTutorial); + commandMap.put(showHelp.getEventName(), showHelp); + return commandMap; } } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java new file mode 100644 index 0000000..c3e0b14 --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java @@ -0,0 +1,83 @@ +package com.github.polpetta.mezzotre.telegram.command; + +import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.types.json.ChatContext; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.BaseRequest; +import com.pengrad.telegrambot.request.SendMessage; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; +import javax.inject.Inject; +import javax.inject.Named; +import org.apache.commons.lang3.tuple.Pair; + +public class Help implements Processor { + + private static final String TRIGGERING_STAGING_NAME = "/help"; + + private final TemplateContentGenerator templateContentGenerator; + private final Executor threadPool; + private final Clock clock; + private final Map tgCommandProcessors; + + @Inject + public Help( + TemplateContentGenerator templateContentGenerator, + @Named("eventThreadPool") Executor threadPool, + Clock clock, + @Named("commandProcessor") Map tgCommandProcessors) { + this.templateContentGenerator = templateContentGenerator; + this.threadPool = threadPool; + this.clock = clock; + this.tgCommandProcessors = tgCommandProcessors; + } + + @Override + public Set getTriggerKeywords() { + return Set.of(TRIGGERING_STAGING_NAME); + } + + @Override + public CompletableFuture>> process(TgChat chat, Update update) { + return CompletableFuture.supplyAsync( + () -> { + final String message = + templateContentGenerator.mergeTemplate( + velocityContext -> { + velocityContext.put( + "commands", + tgCommandProcessors.values().stream() + .distinct() + .map( + p -> + Pair.of( + p.getTriggerKeywords().stream() + .sorted() + .collect(Collectors.toList()), + p.getLocaleDescriptionKeyword())) + .collect(Collectors.toList())); + }, + chat.getLocale(), + "template/telegram/help.vm"); + + final ChatContext chatContext = chat.getChatContext(); + chatContext.setStage(TRIGGERING_STAGING_NAME); + chatContext.setStep(0); + chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now()); + chat.setChatContext(chatContext); + chat.setHasHelpBeenShown(true); + chat.save(); + + // FIXME put all the buttons here + + return Optional.of(new SendMessage(chat.getId(), message)); + }, + threadPool); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java new file mode 100644 index 0000000..71714d8 --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java @@ -0,0 +1,37 @@ +package com.github.polpetta.mezzotre.telegram.command; + +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.google.inject.assistedinject.Assisted; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.BaseRequest; +import com.pengrad.telegrambot.request.SendMessage; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; + +public class NotFound implements Processor { + + private final String commandName; + + @Inject + public NotFound(@Assisted String commandName) { + this.commandName = commandName; + } + + @Override + public Set getTriggerKeywords() { + return Collections.emptySet(); + } + + @Override + public CompletableFuture>> process(TgChat chat, Update update) { + // FIXME complete it with: localization, callbackQuery to show help message + return CompletableFuture.completedFuture(update) + .thenApply( + ignored -> + Optional.of( + new SendMessage(chat.getId(), "Command " + commandName + " is not valid"))); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java new file mode 100644 index 0000000..c4e2278 --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java @@ -0,0 +1,5 @@ +package com.github.polpetta.mezzotre.telegram.command; + +public interface NotFoundFactory { + NotFound create(String commandName); +} diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java index c83673b..393c3e5 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java @@ -4,6 +4,7 @@ import com.github.polpetta.mezzotre.orm.model.TgChat; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.request.BaseRequest; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; /** @@ -21,7 +22,7 @@ public interface Processor { * * @return a {@link String} providing the keyword to trigger the current {@link Processor} */ - String getTriggerKeyword(); + Set getTriggerKeywords(); /** * Process the current update @@ -31,4 +32,17 @@ public interface Processor { * @return a {@link CompletableFuture} with the result of the computation */ CompletableFuture>> process(TgChat chat, Update update); + + /** + * Provide the key to retrieve the current processor descriptor. This is useful for help messages + * or to provide a user with a description of what this command does. Whilst a default + * implementation is provided, we suggest to rename it accordingly since the name generation is + * based on reflection and can be fragile and subject to code refactor changes. + * + * @return a {@link String} with the name of the localization key containing the command + * description + */ + default String getLocaleDescriptionKeyword() { + return this.getClass().getName().toLowerCase() + ".cmdDescription"; + } } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Router.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Router.java index 5ff9eca..2954585 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Router.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Router.java @@ -5,8 +5,8 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.request.BaseRequest; +import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import javax.inject.Named; @@ -21,15 +21,18 @@ import javax.inject.Named; @Singleton public class Router { - private final Set tgCommandProcessors; + private final Map tgCommandProcessors; private final Executor threadPool; + private final NotFoundFactory notFoundFactory; @Inject public Router( - @Named("commandProcessor") Set tgCommandProcessors, - @Named("eventThreadPool") Executor threadPool) { + @Named("commandProcessor") Map tgCommandProcessors, + @Named("eventThreadPool") Executor threadPool, + NotFoundFactory notFoundFactory) { this.tgCommandProcessors = tgCommandProcessors; this.threadPool = threadPool; + this.notFoundFactory = notFoundFactory; } /** @@ -58,15 +61,13 @@ public class Router { .map(list -> list[0]) .filter(wannabeCommand -> wannabeCommand.startsWith("/")) .or(() -> Optional.ofNullable(chat.getChatContext().getStage())) - .flatMap( + .map( command -> - tgCommandProcessors.stream() - // FIXME this is fucking stupid, why iterate over, just use a map! - // Make mapping at startup then we're gucci for the rest of the run - .filter(ex -> ex.getTriggerKeyword().equals(command)) - .findAny()) - .map(executor -> executor.process(chat, update)) - .orElse(CompletableFuture.failedFuture(new CommandNotFoundException())), + tgCommandProcessors + .getOrDefault(command, notFoundFactory.create(command)) + .process(chat, update)) + // This should never happen + .orElse(CompletableFuture.failedFuture(new IllegalStateException())), threadPool); } } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java index adeca62..2d7aac8 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java @@ -1,6 +1,6 @@ package com.github.polpetta.mezzotre.telegram.command; -import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; +import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.telegram.callbackquery.SelectLanguageTutorial; @@ -15,17 +15,13 @@ import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.SendMessage; -import io.vavr.control.Try; -import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; -import org.apache.velocity.VelocityContext; -import org.apache.velocity.tools.ToolManager; -import org.apache.velocity.util.StringBuilderWriter; import org.slf4j.Logger; /** @@ -37,29 +33,35 @@ import org.slf4j.Logger; @Singleton public class Start implements Processor { + private static final String TRIGGERING_STAGING_NAME = "/start"; + private final Executor threadPool; private final Logger log; private final UUIDGenerator uuidGenerator; private final Clock clock; - private final LocalizedMessageFactory localizedMessageFactory; + private final String applicationName; + + private final TemplateContentGenerator templateContentGenerator; @Inject public Start( - LocalizedMessageFactory localizedMessageFactory, + TemplateContentGenerator templateContentGenerator, @Named("eventThreadPool") Executor threadPool, Logger log, UUIDGenerator uuidGenerator, - Clock clock) { - this.localizedMessageFactory = localizedMessageFactory; + Clock clock, + @Named("applicationName") String applicationName) { + this.templateContentGenerator = templateContentGenerator; this.threadPool = threadPool; this.log = log; this.uuidGenerator = uuidGenerator; this.clock = clock; + this.applicationName = applicationName; } @Override - public String getTriggerKeyword() { - return "/start"; + public Set getTriggerKeywords() { + return Set.of(TRIGGERING_STAGING_NAME); } @Override @@ -73,36 +75,19 @@ public class Start implements Processor { 3 - Reply to Telegram */ final String message = - Try.of( - () -> { - final Locale locale = Locale.forLanguageTag(chat.getLocale()); - final ToolManager toolManager = - localizedMessageFactory.createVelocityToolManager(locale); - final VelocityContext context = - new VelocityContext(toolManager.createContext()); - context.put("firstName", update.message().chat().firstName()); - context.put("programName", "_Mezzotre_"); - - final StringBuilder content = new StringBuilder(); - final StringBuilderWriter stringBuilderWriter = - new StringBuilderWriter(content); - - toolManager - .getVelocityEngine() - .mergeTemplate( - "template/command/start.0.vm", - StandardCharsets.UTF_8.name(), - context, - stringBuilderWriter); - - stringBuilderWriter.close(); - return content.toString(); - }) - .get(); + templateContentGenerator.mergeTemplate( + velocityContext -> { + velocityContext.put("firstName", update.message().chat().firstName()); + // FIXME add some very cool markdown formatter instead of concatenating stuff + // this way + velocityContext.put("programName", "_" + applicationName + "_"); + }, + chat.getLocale(), + "template/telegram/start.vm"); log.trace("Start command - message to send back: " + message); final ChatContext chatContext = chat.getChatContext(); - chatContext.setStage(getTriggerKeyword()); + chatContext.setStage(TRIGGERING_STAGING_NAME); chatContext.setStep(0); chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now()); chat.setChatContext(chatContext); @@ -135,13 +120,9 @@ public class Start implements Processor { .build()); final String englishButton = - localizedMessageFactory - .createResourceBundle(Locale.US) - .getString("changeLanguage.english"); + templateContentGenerator.getString(Locale.US, "changeLanguage.english"); final String italianButton = - localizedMessageFactory - .createResourceBundle(Locale.ITALY) - .getString("changeLanguage.italian"); + templateContentGenerator.getString(Locale.ITALY, "changeLanguage.italian"); final SendMessage messageToSend = new SendMessage(chat.getId(), message) diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/di/Command.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/di/Command.java index ef0ee6b..5ce51c8 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/di/Command.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/di/Command.java @@ -1,10 +1,11 @@ package com.github.polpetta.mezzotre.telegram.command.di; -import com.github.polpetta.mezzotre.telegram.command.Processor; -import com.github.polpetta.mezzotre.telegram.command.Start; +import com.github.polpetta.mezzotre.telegram.command.*; import com.google.inject.AbstractModule; import com.google.inject.Provides; -import java.util.Set; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import java.util.HashMap; +import java.util.Map; import javax.inject.Named; import javax.inject.Singleton; import org.apache.velocity.app.VelocityEngine; @@ -12,11 +13,37 @@ import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; public class Command extends AbstractModule { + + @Override + protected void configure() { + super.configure(); + + install( + new FactoryModuleBuilder() + .implement(Processor.class, NotFound.class) + .build(NotFoundFactory.class)); + } + + private static Map mapForProcessor(Processor processor) { + final HashMap commandMap = new HashMap<>(); + processor + .getTriggerKeywords() + .forEach( + keyword -> { + commandMap.put(keyword, processor); + }); + return commandMap; + } + @Provides @Singleton @Named("commandProcessor") - public Set getCommandProcessor(Start start) { - return Set.of(start); + public Map getCommandProcessor(Start start, Help help) { + final HashMap commandMap = new HashMap<>(); + commandMap.putAll(mapForProcessor(start)); + commandMap.putAll(mapForProcessor(help)); + + return commandMap; } @Provides diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java b/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java new file mode 100644 index 0000000..7dd5e66 --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java @@ -0,0 +1,18 @@ +package com.github.polpetta.mezzotre.telegram.model; + +import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; +import javax.inject.Inject; + +public class Help { + + private final LocalizedMessageFactory localizedMessageFactory; + + @Inject + public Help(LocalizedMessageFactory localizedMessageFactory) { + this.localizedMessageFactory = localizedMessageFactory; + } + + public String generateErrorMessage() { + return ""; + } +} diff --git a/src/main/resources/i18n/message.properties b/src/main/resources/i18n/message.properties index cce17e2..6200538 100644 --- a/src/main/resources/i18n/message.properties +++ b/src/main/resources/i18n/message.properties @@ -1,11 +1,16 @@ start.helloFirstName=Hello {0}! \ud83d\udc4b start.description=This is {0}, a simple bot focused on DnD content management! Please start by choosing a language down below \ud83d\udc47 +start.cmdDescription=Trigger this very bot selectLanguageTutorial.drinkAction=*Proceeds to drink a potion with a strange, multicolor liquid* selectLanguageTutorial.setLanguage=Thanks! Now that I drank this modified potion of {0} that I''ve found at the "Crystal Fermentary" magic potion shop yesterday I can speak with you in the language that you prefer! selectLanguageTutorial.instructions=You can always change your language settings by typing /selectLanguageTutorial in the chat. changeLanguage.english=English changeLanguage.italian=Italian +changeLanguage.cmdDescription=Select the new language I will use to speak to you spell.speakWithAnimals=Speak with animals button.showMeTutorial=Show me what you can do! help.notShownYet=It seems you haven''t checked out what I can do yet! To have a complete list of my abilities, type /help in chat at any time! help.buttonBelow=Alternatively, you can click the button down below. +help.description=Here is a list of what I can do +help.buttonsToo=You can do the same operations you''d do with the commands aforementioned by selecting the corresponding button below \ud83d\udc47 +help.cmdDescription=Print the help message diff --git a/src/main/resources/template/telegram/help.vm b/src/main/resources/template/telegram/help.vm new file mode 100644 index 0000000..38a935e --- /dev/null +++ b/src/main/resources/template/telegram/help.vm @@ -0,0 +1,7 @@ +${i18n.help.description}: + +#foreach(${command} in ${commands}) +*#foreach(${key} in ${command.left}) ${key}#end: ${i18n.get(${command.right})} +#end + +${i18n.help.buttonsToo} \ No newline at end of file diff --git a/src/main/resources/template/callbackQuery/selectLanguageTutorial.vm b/src/main/resources/template/telegram/selectLanguageTutorial.vm similarity index 100% rename from src/main/resources/template/callbackQuery/selectLanguageTutorial.vm rename to src/main/resources/template/telegram/selectLanguageTutorial.vm diff --git a/src/main/resources/template/command/start.0.vm b/src/main/resources/template/telegram/start.vm similarity index 100% rename from src/main/resources/template/command/start.0.vm rename to src/main/resources/template/telegram/start.vm diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java index a4e53c1..e45e898 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java @@ -6,6 +6,7 @@ import static org.mockito.Mockito.*; import com.github.polpetta.mezzotre.helper.Loader; import com.github.polpetta.mezzotre.helper.TestConfig; import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; +import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; @@ -67,7 +68,8 @@ class SelectLanguageTutorialIntegrationTest { selectLanguageTutorial = new SelectLanguageTutorial( Executors.newSingleThreadExecutor(), - new LocalizedMessageFactory(Loader.defaultVelocityEngine()), + new TemplateContentGenerator( + new LocalizedMessageFactory(Loader.defaultVelocityEngine())), LoggerFactory.getLogger(SelectLanguageTutorial.class), fakeUUIDGenerator); } diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java new file mode 100644 index 0000000..6072f43 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java @@ -0,0 +1,173 @@ +package com.github.polpetta.mezzotre.telegram.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.github.polpetta.mezzotre.helper.Loader; +import com.github.polpetta.mezzotre.helper.TestConfig; +import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; +import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.model.query.QTgChat; +import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.types.json.ChatContext; +import com.google.gson.Gson; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.BaseRequest; +import com.pengrad.telegrambot.request.SendMessage; +import io.ebean.Database; +import java.util.HashMap; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import org.apache.velocity.app.VelocityEngine; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Tag("slow") +@Tag("database") +@Tag("velocity") +@Testcontainers +class HelpIntegrationTest { + private static Gson gson; + + @Container + private final PostgreSQLContainer postgresServer = + new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE); + + private Database database; + private VelocityEngine velocityEngine; + private Clock fakeClock; + private Help help; + + @BeforeAll + static void beforeAll() { + gson = new Gson(); + } + + @BeforeEach + void setUp() throws Exception { + database = + Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); + velocityEngine = Loader.defaultVelocityEngine(); + + final Logger log = LoggerFactory.getLogger(Start.class); + fakeClock = mock(Clock.class); + } + + @Test + void shouldProvideMessageWithButtons() throws Exception { + when(fakeClock.now()).thenReturn(42L); + + final TgChat tgChat = new TgChat(1111111L, new ChatContext(), "en-US", false); + tgChat.save(); + + final HashMap commands = new HashMap<>(); + final Processor dummy1 = + new Processor() { + @Override + public Set getTriggerKeywords() { + return Set.of("/a", "/b"); + } + + @Override + public CompletableFuture>> process( + TgChat chat, Update update) { + return null; + } + + @Override + public String getLocaleDescriptionKeyword() { + return "start.cmdDescription"; + } + }; + final Processor dummy2 = + new Processor() { + @Override + public Set getTriggerKeywords() { + return Set.of("/different"); + } + + @Override + public CompletableFuture>> process( + TgChat chat, Update update) { + return null; + } + + @Override + public String getLocaleDescriptionKeyword() { + return "help.cmdDescription"; + } + }; + commands.put("/a", dummy1); + commands.put("/b", dummy1); + commands.put("/different", dummy2); + + help = + new Help( + new TemplateContentGenerator(new LocalizedMessageFactory(velocityEngine)), + Executors.newSingleThreadExecutor(), + fakeClock, + commands); + + final Update update = + gson.fromJson( + "{\n" + + "\"update_id\":10000,\n" + + "\"message\":{\n" + + " \"date\":1441645532,\n" + + " \"chat\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"type\": \"private\",\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"message_id\":1365,\n" + + " \"from\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"text\":\"/help\"\n" + + "}\n" + + "}", + Update.class); + + final CompletableFuture>> gotFuture = help.process(tgChat, update); + final Optional> gotResponseOpt = gotFuture.get(); + final BaseRequest gotResponse = gotResponseOpt.get(); + assertInstanceOf(SendMessage.class, gotResponse); + final String message = (String) gotResponse.getParameters().get("text"); + assertEquals( + "Here is a list of what I can do:\n" + + "\n" + + "* /a /b: Trigger this very bot\n" + + "* /different: Print the help message\n" + + "\n" + + "You can do the same operations you'd do with the commands aforementioned by" + + " selecting the corresponding button below \uD83D\uDC47", + message); + + // TODO InputKeyboard assertions + fail("Add inputkeyboard assertions too"); + + final TgChat gotChat = new QTgChat().id.eq(1111111L).findOne(); + assertNotNull(gotChat); + assertTrue(gotChat.getHasHelpBeenShown()); + final ChatContext gotChatChatContext = gotChat.getChatContext(); + assertEquals(0, gotChatChatContext.getStep()); + assertEquals("/help", gotChatChatContext.getStage()); + assertEquals(42, gotChatChatContext.getPreviousMessageUnixTimestampInSeconds()); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/RouterTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/RouterTest.java index 1ca3bdf..f4bbcd7 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/command/RouterTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/RouterTest.java @@ -11,6 +11,7 @@ import com.google.gson.Gson; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.SendMessage; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -32,12 +33,12 @@ class RouterTest { gson = new Gson(); dummyEmptyExampleProcessor = mock(Processor.class); - when(dummyEmptyExampleProcessor.getTriggerKeyword()).thenReturn("/example"); + when(dummyEmptyExampleProcessor.getTriggerKeywords()).thenReturn(Set.of("/example")); when(dummyEmptyExampleProcessor.process(any(), any())) .thenReturn(CompletableFuture.completedFuture(Optional.empty())); anotherKeyWithResultProcessor = mock(Processor.class); - when(anotherKeyWithResultProcessor.getTriggerKeyword()).thenReturn("/anotherExample"); + when(anotherKeyWithResultProcessor.getTriggerKeywords()).thenReturn(Set.of("/anotherExample")); when(anotherKeyWithResultProcessor.process(any(), any())) .thenReturn( CompletableFuture.completedFuture(Optional.of(new SendMessage(1234L, "hello world")))); @@ -46,7 +47,10 @@ class RouterTest { @Test void shouldMessageExampleMessageAndGetEmptyOptional() throws Exception { final Router router = - new Router(Set.of(dummyEmptyExampleProcessor), Executors.newSingleThreadExecutor()); + new Router( + Map.of("/example", dummyEmptyExampleProcessor), + Executors.newSingleThreadExecutor(), + mock(NotFoundFactory.class)); final TgChat fakeChat = mock(TgChat.class); when(fakeChat.getChatContext()).thenReturn(new ChatContext()); final Update update = @@ -85,8 +89,13 @@ class RouterTest { void shouldSelectRightExecutorAndReturnResult() throws Exception { final Router router = new Router( - Set.of(dummyEmptyExampleProcessor, anotherKeyWithResultProcessor), - Executors.newSingleThreadExecutor()); + Map.of( + "/example", + dummyEmptyExampleProcessor, + "/anotherExample", + anotherKeyWithResultProcessor), + Executors.newSingleThreadExecutor(), + mock(NotFoundFactory.class)); final TgChat fakeChat = mock(TgChat.class); when(fakeChat.getChatContext()).thenReturn(new ChatContext()); final Update update = diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java index cfc3e90..a72a7b4 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java @@ -6,6 +6,7 @@ import static org.mockito.Mockito.*; import com.github.polpetta.mezzotre.helper.Loader; import com.github.polpetta.mezzotre.helper.TestConfig; import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; +import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QTgChat; @@ -22,8 +23,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import org.apache.velocity.app.VelocityEngine; -import org.apache.velocity.runtime.RuntimeConstants; -import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; @@ -62,12 +61,7 @@ class StartIntegrationTest { void setUp() throws Exception { database = Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); - velocityEngine = new VelocityEngine(); - velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "classpath"); - velocityEngine.setProperty( - "resource.loader.classpath.class", ClasspathResourceLoader.class.getName()); - velocityEngine.init(); - localizedMessageFactory = new LocalizedMessageFactory(velocityEngine); + velocityEngine = Loader.defaultVelocityEngine(); final Logger log = LoggerFactory.getLogger(Start.class); @@ -76,11 +70,12 @@ class StartIntegrationTest { start = new Start( - localizedMessageFactory, + new TemplateContentGenerator(new LocalizedMessageFactory(velocityEngine)), Executors.newSingleThreadExecutor(), log, fakeUUIDGenerator, - fakeClock); + fakeClock, + "Mezzotre"); } @Test -- 2.40.1 From 53cdd4aa22013182d30db02dda469bb9a7fe4d03 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Thu, 6 Apr 2023 17:57:55 +0200 Subject: [PATCH 02/13] test: complete first test for help message --- .../com/github/polpetta/mezzotre/App.java | 18 ++-- .../{InjectionModule.java => AppDI.java} | 4 +- .../mezzotre/orm/{di/Db.java => OrmDI.java} | 4 +- .../route/{di/Route.java => RouteDI.java} | 4 +- ...allbackQuery.java => CallbackQueryDI.java} | 7 +- .../telegram/callbackquery/Field.java | 37 ++++++++ .../telegram/callbackquery/Processor.java | 12 ++- .../callbackquery/SelectLanguageTutorial.java | 48 +--------- .../telegram/callbackquery/ShowHelp.java | 2 +- .../telegram/callbackquery/Value.java | 25 +++++ .../{di/Command.java => CommandDI.java} | 5 +- .../mezzotre/telegram/command/Help.java | 53 ++++++++++- .../mezzotre/telegram/command/Start.java | 19 ++-- src/main/resources/i18n/message.properties | 11 ++- .../resources/i18n/message_en_US.properties | 8 +- src/main/resources/i18n/message_it.properties | 2 +- .../resources/i18n/message_it_IT.properties | 2 +- .../helper/IntegrationAppFactory.java | 4 +- ...allbackQueryDIContextIntegrationTest.java} | 2 +- ...SelectLanguageTutorialIntegrationTest.java | 26 +++--- .../telegram/command/HelpIntegrationTest.java | 93 +++++++++++++++++-- 21 files changed, 276 insertions(+), 110 deletions(-) rename src/main/java/com/github/polpetta/mezzotre/{InjectionModule.java => AppDI.java} (86%) rename src/main/java/com/github/polpetta/mezzotre/orm/{di/Db.java => OrmDI.java} (92%) rename src/main/java/com/github/polpetta/mezzotre/route/{di/Route.java => RouteDI.java} (92%) rename src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/{di/CallbackQuery.java => CallbackQueryDI.java} (63%) create mode 100644 src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Value.java rename src/main/java/com/github/polpetta/mezzotre/telegram/command/{di/Command.java => CommandDI.java} (91%) rename src/test/java/com/github/polpetta/mezzotre/orm/model/{CallbackQueryContextIntegrationTest.java => CallbackQueryDIContextIntegrationTest.java} (98%) diff --git a/src/main/java/com/github/polpetta/mezzotre/App.java b/src/main/java/com/github/polpetta/mezzotre/App.java index 9261d48..c19f1fe 100644 --- a/src/main/java/com/github/polpetta/mezzotre/App.java +++ b/src/main/java/com/github/polpetta/mezzotre/App.java @@ -1,10 +1,10 @@ package com.github.polpetta.mezzotre; -import com.github.polpetta.mezzotre.orm.di.Db; +import com.github.polpetta.mezzotre.orm.OrmDI; +import com.github.polpetta.mezzotre.route.RouteDI; import com.github.polpetta.mezzotre.route.Telegram; -import com.github.polpetta.mezzotre.route.di.Route; -import com.github.polpetta.mezzotre.telegram.callbackquery.di.CallbackQuery; -import com.github.polpetta.mezzotre.telegram.command.di.Command; +import com.github.polpetta.mezzotre.telegram.callbackquery.CallbackQueryDI; +import com.github.polpetta.mezzotre.telegram.command.CommandDI; import com.github.polpetta.mezzotre.util.di.ThreadPool; import com.google.inject.*; import com.google.inject.Module; @@ -23,11 +23,11 @@ public class App extends Jooby { public static final Function> DEFAULT_DI_MODULES = (jooby) -> { final HashSet modules = new HashSet<>(); - modules.add(new Db()); + modules.add(new OrmDI()); modules.add(new ThreadPool()); - modules.add(new Route()); - modules.add(new Command()); - modules.add(new CallbackQuery()); + modules.add(new RouteDI()); + modules.add(new CommandDI()); + modules.add(new CallbackQueryDI()); return modules; }; @@ -42,7 +42,7 @@ public class App extends Jooby { if (modules == null || modules.size() == 0) { toInject = DEFAULT_DI_MODULES.apply(this); } - toInject.add(new InjectionModule(this)); + toInject.add(new AppDI(this)); toInject.add(new JoobyModule(this)); final Injector injector = Guice.createInjector(runningEnv, toInject); diff --git a/src/main/java/com/github/polpetta/mezzotre/InjectionModule.java b/src/main/java/com/github/polpetta/mezzotre/AppDI.java similarity index 86% rename from src/main/java/com/github/polpetta/mezzotre/InjectionModule.java rename to src/main/java/com/github/polpetta/mezzotre/AppDI.java index 528e669..650f733 100644 --- a/src/main/java/com/github/polpetta/mezzotre/InjectionModule.java +++ b/src/main/java/com/github/polpetta/mezzotre/AppDI.java @@ -7,13 +7,13 @@ import javax.inject.Named; import javax.inject.Singleton; import org.slf4j.Logger; -public class InjectionModule extends AbstractModule { +public class AppDI extends AbstractModule { // In the future we can get this name from mvn, by now it is good as it is public static final String APPLICATION_NAME = "Mezzotre"; private final Jooby jooby; - public InjectionModule(Jooby jooby) { + public AppDI(Jooby jooby) { this.jooby = jooby; } diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/di/Db.java b/src/main/java/com/github/polpetta/mezzotre/orm/OrmDI.java similarity index 92% rename from src/main/java/com/github/polpetta/mezzotre/orm/di/Db.java rename to src/main/java/com/github/polpetta/mezzotre/orm/OrmDI.java index 7119eac..48b1e87 100644 --- a/src/main/java/com/github/polpetta/mezzotre/orm/di/Db.java +++ b/src/main/java/com/github/polpetta/mezzotre/orm/OrmDI.java @@ -1,4 +1,4 @@ -package com.github.polpetta.mezzotre.orm.di; +package com.github.polpetta.mezzotre.orm; import com.google.inject.AbstractModule; import com.google.inject.Provides; @@ -10,7 +10,7 @@ import io.jooby.flyway.FlywayModule; import io.jooby.hikari.HikariModule; import javax.inject.Named; -public class Db extends AbstractModule { +public class OrmDI extends AbstractModule { /** * Returns null. This allows to fetch the configuration from file rather than fetch from other * environment diff --git a/src/main/java/com/github/polpetta/mezzotre/route/di/Route.java b/src/main/java/com/github/polpetta/mezzotre/route/RouteDI.java similarity index 92% rename from src/main/java/com/github/polpetta/mezzotre/route/di/Route.java rename to src/main/java/com/github/polpetta/mezzotre/route/RouteDI.java index dd57304..238be44 100644 --- a/src/main/java/com/github/polpetta/mezzotre/route/di/Route.java +++ b/src/main/java/com/github/polpetta/mezzotre/route/RouteDI.java @@ -1,4 +1,4 @@ -package com.github.polpetta.mezzotre.route.di; +package com.github.polpetta.mezzotre.route; import com.google.inject.AbstractModule; import com.google.inject.Provides; @@ -7,7 +7,7 @@ import io.jooby.Jooby; import java.util.Optional; import javax.inject.Singleton; -public class Route extends AbstractModule { +public class RouteDI extends AbstractModule { @Provides @Singleton public TelegramBot getTelegramBot(Jooby jooby) { diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/di/CallbackQuery.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/CallbackQueryDI.java similarity index 63% rename from src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/di/CallbackQuery.java rename to src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/CallbackQueryDI.java index 6dfb00d..74c240c 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/di/CallbackQuery.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/CallbackQueryDI.java @@ -1,8 +1,5 @@ -package com.github.polpetta.mezzotre.telegram.callbackquery.di; +package com.github.polpetta.mezzotre.telegram.callbackquery; -import com.github.polpetta.mezzotre.telegram.callbackquery.Processor; -import com.github.polpetta.mezzotre.telegram.callbackquery.SelectLanguageTutorial; -import com.github.polpetta.mezzotre.telegram.callbackquery.ShowHelp; import com.google.inject.AbstractModule; import com.google.inject.Provides; import java.util.HashMap; @@ -10,7 +7,7 @@ import java.util.Map; import javax.inject.Named; import javax.inject.Singleton; -public class CallbackQuery extends AbstractModule { +public class CallbackQueryDI extends AbstractModule { @Provides @Singleton diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java new file mode 100644 index 0000000..e2a485d --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java @@ -0,0 +1,37 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +public interface Field { + /** + * Additional fields that are related to {@code changeLanguage} event + * + * @author Davide Polonio + * @since 1.0 + */ + enum SelectLanguageTutorial { + NewLanguage("newLanguage"); + + private final String name; + + SelectLanguageTutorial(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + enum ShowHelp { + InvokedFromHelpMessage("invokedFromHelpMessage"); + + private final String name; + + ShowHelp(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java index 021a177..020d1d6 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java @@ -17,12 +17,20 @@ import java.util.concurrent.CompletableFuture; public interface Processor { /** - * The even name this processor is able to process + * The event name this processor is able to process * - * @return a {@link String} containig the name of the event supported + * @return a {@link String} containing the name of the event supported */ String getEventName(); + default boolean canBeDirectlyInvokedByTheUser() { + return false; + } + + default Optional getPrettyPrintLocaleKeyName() { + return Optional.empty(); + } + /** * Process the current event * diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java index fc47ebd..7bb327d 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java @@ -39,47 +39,6 @@ public class SelectLanguageTutorial implements Processor { private final Logger log; private final UUIDGenerator uuidGenerator; - /** - * Additional fields that are related to {@code changeLanguage} event - * - * @author Davide Polonio - * @since 1.0 - */ - public enum Field { - NewLanguage("newLanguage"); - - private final String name; - - Field(String name) { - this.name = name; - } - - public String getName() { - return name; - } - } - - /** - * Possible values for the additional {@link Field} of {@code changeLanguage} event - * - * @author Davide Polonio - * @since 1.0 - */ - public enum Language { - English("en-US"), - Italian("it-IT"); - - private final String locale; - - Language(String locale) { - this.locale = locale; - } - - public String getLocale() { - return locale; - } - } - @Inject public SelectLanguageTutorial( @Named("eventThreadPool") Executor threadPool, @@ -120,7 +79,8 @@ public class SelectLanguageTutorial implements Processor { .getFields() .getAdditionalProperties() .getOrDefault( - Field.NewLanguage.getName(), tgChat.getLocale())); + Field.SelectLanguageTutorial.NewLanguage.getName(), + tgChat.getLocale())); tgChat.save(); log.trace( "Locale for chat " @@ -166,7 +126,9 @@ public class SelectLanguageTutorial implements Processor { if (!tgChat.getHasHelpBeenShown()) { // Add a button to show all the possible commands final String showMeTutorialString = - templateContentGenerator.getString(tgChat.getLocale(), "button.showMeTutorial"); + templateContentGenerator.getString( + tgChat.getLocale(), + "selectLanguageTutorial.showMeTutorialInlineKeyboardButtonName"); final CallbackQueryMetadata callbackQueryMetadata = new CallbackQueryMetadata.CallbackQueryMetadataBuilder() diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java index 6beb58e..c7d241f 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java @@ -7,7 +7,7 @@ import com.pengrad.telegrambot.request.SendMessage; import java.util.Optional; import java.util.concurrent.CompletableFuture; -public class ShowHelp implements Processor { +class ShowHelp implements Processor { public static String EVENT_NAME = "showHelp"; diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Value.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Value.java new file mode 100644 index 0000000..046ff8b --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Value.java @@ -0,0 +1,25 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +public interface Value { + /** + * Possible values for the additional {@link Field.SelectLanguageTutorial} of {@code + * changeLanguage} event + * + * @author Davide Polonio + * @since 1.0 + */ + enum SelectLanguageTutorial { + English("en-US"), + Italian("it-IT"); + + private final String locale; + + SelectLanguageTutorial(String locale) { + this.locale = locale; + } + + public String getLocale() { + return locale; + } + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/di/Command.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/CommandDI.java similarity index 91% rename from src/main/java/com/github/polpetta/mezzotre/telegram/command/di/Command.java rename to src/main/java/com/github/polpetta/mezzotre/telegram/command/CommandDI.java index 5ce51c8..767fdab 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/di/Command.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/CommandDI.java @@ -1,6 +1,5 @@ -package com.github.polpetta.mezzotre.telegram.command.di; +package com.github.polpetta.mezzotre.telegram.command; -import com.github.polpetta.mezzotre.telegram.command.*; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.assistedinject.FactoryModuleBuilder; @@ -12,7 +11,7 @@ import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; -public class Command extends AbstractModule { +public class CommandDI extends AbstractModule { @Override protected void configure() { diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java index c3e0b14..a7a6526 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java @@ -1,10 +1,17 @@ package com.github.polpetta.mezzotre.telegram.command; import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.telegram.callbackquery.Field; import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.mezzotre.util.UUIDGenerator; +import com.github.polpetta.types.json.CallbackQueryMetadata; import com.github.polpetta.types.json.ChatContext; import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.SendMessage; import java.util.Map; @@ -25,17 +32,25 @@ public class Help implements Processor { private final Executor threadPool; private final Clock clock; private final Map tgCommandProcessors; + private final Map + eventProcessor; + private final UUIDGenerator uuidGenerator; @Inject public Help( TemplateContentGenerator templateContentGenerator, @Named("eventThreadPool") Executor threadPool, Clock clock, - @Named("commandProcessor") Map tgCommandProcessors) { + @Named("commandProcessor") Map tgCommandProcessors, + @Named("eventProcessors") + Map eventProcessor, + UUIDGenerator uuidGenerator) { this.templateContentGenerator = templateContentGenerator; this.threadPool = threadPool; this.clock = clock; this.tgCommandProcessors = tgCommandProcessors; + this.eventProcessor = eventProcessor; + this.uuidGenerator = uuidGenerator; } @Override @@ -74,9 +89,41 @@ public class Help implements Processor { chat.setHasHelpBeenShown(true); chat.save(); - // FIXME put all the buttons here + final SendMessage sendMessage = + new SendMessage(chat.getId(), message).parseMode(ParseMode.Markdown); - return Optional.of(new SendMessage(chat.getId(), message)); + final String callBackGroupId = uuidGenerator.generateAsString(); + final InlineKeyboardButton[] collect = + eventProcessor.values().stream() + .filter( + com.github.polpetta.mezzotre.telegram.callbackquery.Processor + ::canBeDirectlyInvokedByTheUser) + .filter(e -> e.getPrettyPrintLocaleKeyName().isPresent()) + .map( + eventProcessor -> { + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext( + uuidGenerator.generateAsString(), + callBackGroupId, + new CallbackQueryMetadata.CallbackQueryMetadataBuilder() + .withEvent(eventProcessor.getEventName()) + .withTelegramChatId(chat.getId()) + .withAdditionalProperty( + Field.ShowHelp.InvokedFromHelpMessage.getName(), true) + .build()); + callbackQueryContext.save(); + + return new InlineKeyboardButton( + templateContentGenerator.getString( + chat.getLocale(), + eventProcessor.getPrettyPrintLocaleKeyName().get())) + .callbackData(callbackQueryContext.getId()); + }) + .toArray(InlineKeyboardButton[]::new); + + sendMessage.replyMarkup(new InlineKeyboardMarkup(collect)); + + return Optional.of(sendMessage); }, threadPool); } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java index 2d7aac8..e3a84f9 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java @@ -3,7 +3,8 @@ package com.github.polpetta.mezzotre.telegram.command; import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; -import com.github.polpetta.mezzotre.telegram.callbackquery.SelectLanguageTutorial; +import com.github.polpetta.mezzotre.telegram.callbackquery.Field; +import com.github.polpetta.mezzotre.telegram.callbackquery.Value; import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.CallbackQueryMetadata; @@ -100,11 +101,13 @@ public class Start implements Processor { uuidGenerator.generateAsString(), groupId, new CallbackQueryMetadata.CallbackQueryMetadataBuilder() - .withEvent(SelectLanguageTutorial.EVENT_NAME) + .withEvent( + com.github.polpetta.mezzotre.telegram.callbackquery.SelectLanguageTutorial + .EVENT_NAME) .withTelegramChatId(update.message().chat().id()) .withAdditionalProperty( - SelectLanguageTutorial.Field.NewLanguage.getName(), - SelectLanguageTutorial.Language.English.getLocale()) + Field.SelectLanguageTutorial.NewLanguage.getName(), + Value.SelectLanguageTutorial.English.getLocale()) .build()); final CallbackQueryContext switchToItalian = @@ -112,11 +115,13 @@ public class Start implements Processor { uuidGenerator.generateAsString(), groupId, new CallbackQueryMetadata.CallbackQueryMetadataBuilder() - .withEvent(SelectLanguageTutorial.EVENT_NAME) + .withEvent( + com.github.polpetta.mezzotre.telegram.callbackquery.SelectLanguageTutorial + .EVENT_NAME) .withTelegramChatId(update.message().chat().id()) .withAdditionalProperty( - SelectLanguageTutorial.Field.NewLanguage.getName(), - SelectLanguageTutorial.Language.Italian.getLocale()) + Field.SelectLanguageTutorial.NewLanguage.getName(), + Value.SelectLanguageTutorial.Italian.getLocale()) .build()); final String englishButton = diff --git a/src/main/resources/i18n/message.properties b/src/main/resources/i18n/message.properties index 6200538..b0cf9e5 100644 --- a/src/main/resources/i18n/message.properties +++ b/src/main/resources/i18n/message.properties @@ -1,14 +1,17 @@ start.helloFirstName=Hello {0}! \ud83d\udc4b -start.description=This is {0}, a simple bot focused on DnD content management! Please start by choosing a language down below \ud83d\udc47 +start.description=This is {0}, a simple bot focused on DnD content management! Please start by choosing a selectLanguageTutorial down below \ud83d\udc47 start.cmdDescription=Trigger this very bot +start.inlineKeyboardButtonName=Let''s begin! +selectLanguageTutorial.inlineKeyboardButtonName=Select language selectLanguageTutorial.drinkAction=*Proceeds to drink a potion with a strange, multicolor liquid* -selectLanguageTutorial.setLanguage=Thanks! Now that I drank this modified potion of {0} that I''ve found at the "Crystal Fermentary" magic potion shop yesterday I can speak with you in the language that you prefer! -selectLanguageTutorial.instructions=You can always change your language settings by typing /selectLanguageTutorial in the chat. +selectLanguageTutorial.setLanguage=Thanks! Now that I drank this modified potion of {0} that I''ve found at the "Crystal Fermentary" magic potion shop yesterday I can speak with you in the selectLanguageTutorial that you prefer! +selectLanguageTutorial.instructions=You can always change your selectLanguageTutorial settings by typing /selectLanguageTutorial in the chat. changeLanguage.english=English changeLanguage.italian=Italian changeLanguage.cmdDescription=Select the new language I will use to speak to you +changeLanguage.inlineKeyboardButtonName=Change language spell.speakWithAnimals=Speak with animals -button.showMeTutorial=Show me what you can do! +selectLanguageTutorial.showMeTutorialInlineKeyboardButtonName=Show me what you can do! help.notShownYet=It seems you haven''t checked out what I can do yet! To have a complete list of my abilities, type /help in chat at any time! help.buttonBelow=Alternatively, you can click the button down below. help.description=Here is a list of what I can do diff --git a/src/main/resources/i18n/message_en_US.properties b/src/main/resources/i18n/message_en_US.properties index 64ca1f3..296200c 100644 --- a/src/main/resources/i18n/message_en_US.properties +++ b/src/main/resources/i18n/message_en_US.properties @@ -1,11 +1,11 @@ start.helloFirstName=Hello {0}! \ud83d\udc4b -start.description=This is {0}, a simple bot focused on DnD content management! Please start by choosing a language down below \ud83d\udc47 +start.description=This is {0}, a simple bot focused on DnD content management! Please start by choosing a selectLanguageTutorial down below \ud83d\udc47 selectLanguageTutorial.drinkAction=*Proceeds to drink a potion with a strange, multicolor liquid* -selectLanguageTutorial.setLanguage=Thanks! Now that I drank this modified potion of {0} that I''ve found at the "Crystal Fermentary" magic potion shop yesterday I can speak with you in the language that you prefer! -selectLanguageTutorial.instructions=You can always change your language settings by typing /selectLanguageTutorial in the chat. +selectLanguageTutorial.setLanguage=Thanks! Now that I drank this modified potion of {0} that I''ve found at the "Crystal Fermentary" magic potion shop yesterday I can speak with you in the selectLanguageTutorial that you prefer! +selectLanguageTutorial.instructions=You can always change your selectLanguageTutorial settings by typing /selectLanguageTutorial in the chat. selectLanguageTutorial.english=English selectLanguageTutorial.italian=Italian spell.speakWithAnimals=Speak with animals -button.showMeTutorial=Show me what you can do! +selectLanguageTutorial.showMeTutorialInlineKeyboardButtonName=Show me what you can do! help.notShownYet=It seems you haven''t checked out what I can do yet! To have a complete list of my abilities, type /help in chat at any time! help.buttonBelow=Alternatively, you can click the button down below. diff --git a/src/main/resources/i18n/message_it.properties b/src/main/resources/i18n/message_it.properties index 1c13674..dcdfadc 100644 --- a/src/main/resources/i18n/message_it.properties +++ b/src/main/resources/i18n/message_it.properties @@ -6,6 +6,6 @@ selectLanguageTutorial.instructions=Puoi sempre cambiare le preferenze della tua selectLanguageTutorial.english=Inglese selectLanguageTutorial.italian=Italiano spell.speakWithAnimals=Parlare con animali -button.showMeTutorial=Mostrami cosa puoi fare! +selectLanguageTutorial.showMeTutorialInlineKeyboardButtonName=Mostrami cosa puoi fare! help.notShownYet=Sembra tu non abbia ancora visto cosa posso fare! Per avere una lista completa delle mie abilità, scrivi /help nella chat in qualsiasi momento! help.buttonBelow=Alternativamente, puoi premere il bottone qui sotto. diff --git a/src/main/resources/i18n/message_it_IT.properties b/src/main/resources/i18n/message_it_IT.properties index 1c13674..dcdfadc 100644 --- a/src/main/resources/i18n/message_it_IT.properties +++ b/src/main/resources/i18n/message_it_IT.properties @@ -6,6 +6,6 @@ selectLanguageTutorial.instructions=Puoi sempre cambiare le preferenze della tua selectLanguageTutorial.english=Inglese selectLanguageTutorial.italian=Italiano spell.speakWithAnimals=Parlare con animali -button.showMeTutorial=Mostrami cosa puoi fare! +selectLanguageTutorial.showMeTutorialInlineKeyboardButtonName=Mostrami cosa puoi fare! help.notShownYet=Sembra tu non abbia ancora visto cosa posso fare! Per avere una lista completa delle mie abilità, scrivi /help nella chat in qualsiasi momento! help.buttonBelow=Alternativamente, puoi premere il bottone qui sotto. diff --git a/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java b/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java index dc1ebb6..147fa4d 100644 --- a/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java +++ b/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java @@ -1,7 +1,7 @@ package com.github.polpetta.mezzotre.helper; import com.github.polpetta.mezzotre.App; -import com.github.polpetta.mezzotre.route.di.Route; +import com.github.polpetta.mezzotre.route.RouteDI; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.Singleton; @@ -56,7 +56,7 @@ public class IntegrationAppFactory { final PostgreSQLContainer container = new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE); container.start(); - final Route routeModule = new Route(); + final RouteDI routeModule = new RouteDI(); final DatabaseDI databaseDI = new DatabaseDI(container); return new App(Stage.DEVELOPMENT, Set.of(databaseDI, routeModule)); } diff --git a/src/test/java/com/github/polpetta/mezzotre/orm/model/CallbackQueryContextIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/orm/model/CallbackQueryDIContextIntegrationTest.java similarity index 98% rename from src/test/java/com/github/polpetta/mezzotre/orm/model/CallbackQueryContextIntegrationTest.java rename to src/test/java/com/github/polpetta/mezzotre/orm/model/CallbackQueryDIContextIntegrationTest.java index 2c45563..148d19e 100644 --- a/src/test/java/com/github/polpetta/mezzotre/orm/model/CallbackQueryContextIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/orm/model/CallbackQueryDIContextIntegrationTest.java @@ -23,7 +23,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; @Tag("slow") @Tag("database") @Testcontainers -class CallbackQueryContextIntegrationTest { +class CallbackQueryDIContextIntegrationTest { private static ObjectMapper objectMapper; private static UUIDGenerator uuidGenerator; diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java index e45e898..17758fe 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java @@ -77,7 +77,7 @@ class SelectLanguageTutorialIntegrationTest { private static Stream getTestLocales() { return Stream.of( Arguments.of( - SelectLanguageTutorial.Language.Italian, + Value.SelectLanguageTutorial.Italian, "_*Procede a bere una pozione al cui suo interno si trova uno strano liquido" + " multicolore*_\n" + "\n" @@ -95,7 +95,7 @@ class SelectLanguageTutorialIntegrationTest { "Mostrami cosa puoi fare!", false), Arguments.of( - SelectLanguageTutorial.Language.English, + Value.SelectLanguageTutorial.English, "_*Proceeds to drink a potion with a strange, multicolor liquid*_\n" + "\n" + "Thanks! Now that I drank this modified potion of Speak with animals that I've" @@ -112,7 +112,7 @@ class SelectLanguageTutorialIntegrationTest { "Show me what you can do!", false), Arguments.of( - SelectLanguageTutorial.Language.Italian, + Value.SelectLanguageTutorial.Italian, "_*Procede a bere una pozione al cui suo interno si trova uno strano liquido" + " multicolore*_\n" + "\n" @@ -126,7 +126,7 @@ class SelectLanguageTutorialIntegrationTest { "Mostrami cosa puoi fare!", true), Arguments.of( - SelectLanguageTutorial.Language.English, + Value.SelectLanguageTutorial.English, "_*Proceeds to drink a potion with a strange, multicolor liquid*_\n" + "\n" + "Thanks! Now that I drank this modified potion of Speak with animals that I've" @@ -144,7 +144,7 @@ class SelectLanguageTutorialIntegrationTest { @Timeout(value = 1, unit = TimeUnit.MINUTES) @MethodSource("getTestLocales") void shouldProcessChangeLanguageToDesiredOneSendMessage( - SelectLanguageTutorial.Language language, + Value.SelectLanguageTutorial selectLanguageTutorial, String expectedResult, String startingLocale, String buttonLocale, @@ -184,7 +184,8 @@ class SelectLanguageTutorialIntegrationTest { .withEvent("selectLanguageTutorial") .withTelegramChatId(tgChatId) .withAdditionalProperty( - SelectLanguageTutorial.Field.NewLanguage.getName(), language.getLocale()) + Field.SelectLanguageTutorial.NewLanguage.getName(), + selectLanguageTutorial.getLocale()) .build(); final String entryGroupId = "2e67774a-e4e4-4369-a414-a7f8bfe74b80"; final CallbackQueryContext changeLanguageCallbackQueryContext = @@ -193,7 +194,7 @@ class SelectLanguageTutorialIntegrationTest { changeLanguageCallbackQueryContext.save(); final CompletableFuture>> processFuture = - selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update); + this.selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update); final Optional> gotResponseOpt = processFuture.get(); final SendMessage gotMessage = (SendMessage) gotResponseOpt.get(); assertEquals(expectedResult, gotMessage.getParameters().get("text")); @@ -218,7 +219,7 @@ class SelectLanguageTutorialIntegrationTest { final TgChat retrievedTgChat = new QTgChat().id.eq(tgChatId).findOne(); assertNotNull(retrievedTgChat); - assertEquals(language.getLocale(), retrievedTgChat.getLocale()); + assertEquals(selectLanguageTutorial.getLocale(), retrievedTgChat.getLocale()); assertEquals(0, new QCallbackQueryContext().entryGroup.eq(entryGroupId).findCount()); } @@ -227,7 +228,7 @@ class SelectLanguageTutorialIntegrationTest { @Timeout(value = 1, unit = TimeUnit.MINUTES) @MethodSource("getTestLocales") void shouldProcessChangeLanguageToDesiredOneEditMessage( - SelectLanguageTutorial.Language language, + Value.SelectLanguageTutorial selectLanguageTutorial, String expectedResult, String startingLocale, String buttonLocale, @@ -299,7 +300,8 @@ class SelectLanguageTutorialIntegrationTest { .withEvent("selectLanguageTutorial") .withTelegramChatId(tgChatId) .withAdditionalProperty( - SelectLanguageTutorial.Field.NewLanguage.getName(), language.getLocale()) + Field.SelectLanguageTutorial.NewLanguage.getName(), + selectLanguageTutorial.getLocale()) .build(); final String entryGroupId = "2e67774a-e4e4-4369-a414-a7f8bfe74b80"; final CallbackQueryContext changeLanguageCallbackQueryContext = @@ -308,7 +310,7 @@ class SelectLanguageTutorialIntegrationTest { changeLanguageCallbackQueryContext.save(); final CompletableFuture>> processFuture = - selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update); + this.selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update); final Optional> gotResponseOpt = processFuture.get(); final EditMessageText gotMessage = (EditMessageText) gotResponseOpt.get(); assertEquals(expectedResult, gotMessage.getParameters().get("text")); @@ -333,7 +335,7 @@ class SelectLanguageTutorialIntegrationTest { final TgChat retrievedTgChat = new QTgChat().id.eq(tgChatId).findOne(); assertNotNull(retrievedTgChat); - assertEquals(language.getLocale(), retrievedTgChat.getLocale()); + assertEquals(selectLanguageTutorial.getLocale(), retrievedTgChat.getLocale()); assertEquals(0, new QCallbackQueryContext().entryGroup.eq(entryGroupId).findCount()); } diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java index 6072f43..29fbc77 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java @@ -8,20 +8,23 @@ import com.github.polpetta.mezzotre.helper.Loader; import com.github.polpetta.mezzotre.helper.TestConfig; import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.orm.model.query.QTgChat; import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.ChatContext; import com.google.gson.Gson; import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.SendMessage; import io.ebean.Database; -import java.util.HashMap; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; +import java.util.stream.Stream; import org.apache.velocity.app.VelocityEngine; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -112,12 +115,68 @@ class HelpIntegrationTest { commands.put("/b", dummy1); commands.put("/different", dummy2); + final Map events = + new HashMap<>(); + + final com.github.polpetta.mezzotre.telegram.callbackquery.Processor dummyEvent1 = + new com.github.polpetta.mezzotre.telegram.callbackquery.Processor() { + @Override + public String getEventName() { + return "exampleEvent"; + } + + @Override + public boolean canBeDirectlyInvokedByTheUser() { + return true; + } + + @Override + public Optional getPrettyPrintLocaleKeyName() { + return Optional.of("changeLanguage.inlineKeyboardButtonName"); + } + + @Override + public CompletableFuture>> process( + CallbackQueryContext callbackQueryContext, Update update) { + return null; + } + }; + + final com.github.polpetta.mezzotre.telegram.callbackquery.Processor dummyEvent2 = + new com.github.polpetta.mezzotre.telegram.callbackquery.Processor() { + @Override + public String getEventName() { + return "secondExampleEvent"; + } + + @Override + public boolean canBeDirectlyInvokedByTheUser() { + return true; + } + + @Override + public Optional getPrettyPrintLocaleKeyName() { + return Optional.of("selectLanguageTutorial.inlineKeyboardButtonName"); + } + + @Override + public CompletableFuture>> process( + CallbackQueryContext callbackQueryContext, Update update) { + return null; + } + }; + + events.put(dummyEvent1.getEventName(), dummyEvent1); + events.put(dummyEvent2.getEventName(), dummyEvent2); + help = new Help( new TemplateContentGenerator(new LocalizedMessageFactory(velocityEngine)), Executors.newSingleThreadExecutor(), fakeClock, - commands); + commands, + events, + new UUIDGenerator()); final Update update = gson.fromJson( @@ -159,8 +218,30 @@ class HelpIntegrationTest { + " selecting the corresponding button below \uD83D\uDC47", message); - // TODO InputKeyboard assertions - fail("Add inputkeyboard assertions too"); + final InlineKeyboardButton[][] keyboard = + ((InlineKeyboardMarkup) + gotResponse + .getParameters() + .getOrDefault("reply_markup", new InlineKeyboardMarkup())) + .inlineKeyboard(); + + final List keyboardButtons = + Stream.of(keyboard).flatMap(Stream::of).toList(); + + assertEquals(2, keyboardButtons.size()); + + assertFalse(keyboardButtons.get(0).callbackData().isBlank()); + assertFalse(keyboardButtons.get(1).callbackData().isBlank()); + + assertTrue( + keyboardButtons.stream() + .map(InlineKeyboardButton::text) + .anyMatch("Select language"::equals)); + + assertTrue( + keyboardButtons.stream() + .map(InlineKeyboardButton::text) + .anyMatch("Change language"::equals)); final TgChat gotChat = new QTgChat().id.eq(1111111L).findOne(); assertNotNull(gotChat); -- 2.40.1 From 3e094ec72abc94c48d0f63221665effd30ea2391 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Sat, 8 Apr 2023 17:57:43 +0200 Subject: [PATCH 03/13] feat: complete show help button event * Code refactor so that help command and event use the same logic * Fix invasive string rename (ty intellij) * Fix Processor not returning right locale key name * Add tests and add todos for the one remaining --- .../callbackquery/CallbackQueryDI.java | 11 +++ .../telegram/callbackquery/Dispatcher.java | 22 ++--- .../telegram/callbackquery/NotFound.java | 38 ++++++++ .../callbackquery/NotFoundFactory.java | 5 + .../callbackquery/SelectLanguageTutorial.java | 16 +--- .../telegram/callbackquery/ShowHelp.java | 58 +++++++++++- .../mezzotre/telegram/callbackquery/Util.java | 29 ++++++ .../mezzotre/telegram/command/Help.java | 82 ++--------------- .../mezzotre/telegram/command/Processor.java | 2 +- .../mezzotre/telegram/model/Help.java | 91 +++++++++++++++++-- src/main/resources/i18n/message.properties | 6 +- .../resources/i18n/message_en_US.properties | 18 +++- src/main/resources/i18n/message_it.properties | 2 +- .../resources/i18n/message_it_IT.properties | 2 +- src/main/resources/template/telegram/help.vm | 1 + ...SelectLanguageTutorialIntegrationTest.java | 8 +- .../telegram/command/HelpIntegrationTest.java | 9 +- .../telegram/command/ProcessorTest.java | 38 ++++++++ 18 files changed, 311 insertions(+), 127 deletions(-) create mode 100644 src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundFactory.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/command/ProcessorTest.java diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/CallbackQueryDI.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/CallbackQueryDI.java index 74c240c..34927ed 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/CallbackQueryDI.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/CallbackQueryDI.java @@ -2,6 +2,7 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; import com.google.inject.AbstractModule; import com.google.inject.Provides; +import com.google.inject.assistedinject.FactoryModuleBuilder; import java.util.HashMap; import java.util.Map; import javax.inject.Named; @@ -9,6 +10,16 @@ import javax.inject.Singleton; public class CallbackQueryDI extends AbstractModule { + @Override + protected void configure() { + super.configure(); + + install( + new FactoryModuleBuilder() + .implement(Processor.class, NotFound.class) + .build(NotFoundFactory.class)); + } + @Provides @Singleton @Named("eventProcessors") diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Dispatcher.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Dispatcher.java index 3d0c3fc..469ed0a 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Dispatcher.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Dispatcher.java @@ -3,8 +3,8 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.request.BaseRequest; +import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import javax.inject.Inject; @@ -22,15 +22,18 @@ import javax.inject.Singleton; @Singleton public class Dispatcher { - private final Set tgEventProcessors; + private final Map tgEventProcessors; private final Executor threadPool; + private final NotFoundFactory notFoundFactory; @Inject public Dispatcher( - @Named("eventProcessors") Set tgEventProcessors, - @Named("eventThreadPool") Executor threadPool) { + @Named("eventProcessors") Map tgEventProcessors, + @Named("eventThreadPool") Executor threadPool, + NotFoundFactory notFoundFactory) { this.tgEventProcessors = tgEventProcessors; this.threadPool = threadPool; + this.notFoundFactory = notFoundFactory; } /** @@ -50,14 +53,11 @@ public class Dispatcher { .thenComposeAsync( ignored -> Optional.of(callbackQueryContext.getFields().getEvent()) - .flatMap( + .map( eventName -> - tgEventProcessors.stream() - // FIXME this is fucking stupid, why iterate over, just use a map! - // Make mapping at startup then we're gucci for the rest of the run - .filter(processor -> processor.getEventName().equals(eventName)) - .findAny()) - .map(processor -> processor.process(callbackQueryContext, update)) + tgEventProcessors + .getOrDefault(eventName, notFoundFactory.create(eventName)) + .process(callbackQueryContext, update)) .orElse(CompletableFuture.failedFuture(new EventProcessorNotFoundException())), threadPool); } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java new file mode 100644 index 0000000..605fd4e --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java @@ -0,0 +1,38 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.google.inject.assistedinject.Assisted; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.BaseRequest; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; +import org.slf4j.Logger; + +public class NotFound implements Processor { + + private final Logger log; + private final String eventName; + + @Inject + public NotFound(Logger logger, @Assisted String eventName) { + this.log = logger; + this.eventName = eventName; + } + + @Override + public String getEventName() { + return "eventNotFound"; + } + + @Override + public CompletableFuture>> process( + CallbackQueryContext callbackQueryContext, Update update) { + log.warn( + "A stray event was detected for callback " + + callbackQueryContext.getId() + + " event name " + + eventName); + return CompletableFuture.completedFuture(Optional.empty()); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundFactory.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundFactory.java new file mode 100644 index 0000000..fac7c21 --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundFactory.java @@ -0,0 +1,5 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +public interface NotFoundFactory { + NotFound create(String eventName); +} diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java index 7bb327d..c596a17 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java @@ -3,10 +3,8 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; -import com.github.polpetta.mezzotre.orm.model.query.QTgChat; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.CallbackQueryMetadata; -import com.pengrad.telegrambot.model.Message; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.InlineKeyboardButton; import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; @@ -61,16 +59,7 @@ public class SelectLanguageTutorial implements Processor { CallbackQueryContext callbackQueryContext, Update update) { return CompletableFuture.supplyAsync( () -> - Optional.of(callbackQueryContext.getFields().getTelegramChatId()) - .map(Double::longValue) - // If we're desperate, search in the message for the chat id - .or( - () -> - Optional.ofNullable(update.callbackQuery().message()) - .map(Message::messageId) - .map(Long::valueOf)) - .filter(chatId -> chatId != 0L && chatId != Long.MIN_VALUE) - .flatMap(chatId -> new QTgChat().id.eq(chatId).findOneOrEmpty()) + Util.extractChat(callbackQueryContext, update) .map( tgChat -> { tgChat.setLocale( @@ -119,8 +108,7 @@ public class SelectLanguageTutorial implements Processor { + " entries regarding callback group " + callBackGroupToDelete); - final Optional messageId = - Optional.ofNullable(update.callbackQuery().message()).map(Message::messageId); + final Optional messageId = Util.extractMessageId(update); BaseRequest baseRequest; Optional helpButton = Optional.empty(); if (!tgChat.getHasHelpBeenShown()) { diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java index c7d241f..baeee31 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java @@ -1,15 +1,45 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.telegram.model.Help; import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.BaseRequest; +import com.pengrad.telegrambot.request.EditMessageText; import com.pengrad.telegrambot.request.SendMessage; +import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import javax.inject.Inject; +import javax.inject.Named; class ShowHelp implements Processor { public static String EVENT_NAME = "showHelp"; + private final Executor threadPool; + private final Help modelHelp; + private final Map + tgCommandProcessors; + private final Map eventProcessor; + + // FIXME tests + @Inject + public ShowHelp( + @Named("eventThreadPool") Executor threadPool, + com.github.polpetta.mezzotre.telegram.model.Help modelHelp, + @Named("commandProcessor") + Map tgCommandProcessors, + @Named("eventProcessors") + Map + eventProcessor) { + this.threadPool = threadPool; + this.modelHelp = modelHelp; + this.tgCommandProcessors = tgCommandProcessors; + this.eventProcessor = eventProcessor; + } @Override public String getEventName() { @@ -19,8 +49,30 @@ class ShowHelp implements Processor { @Override public CompletableFuture>> process( CallbackQueryContext callbackQueryContext, Update update) { - // TODO implement this method and put `hasHelpBeenShown` in tgChat to false - return CompletableFuture.completedFuture( - Optional.of(new SendMessage(callbackQueryContext.getFields().getTelegramChatId(), "TODO"))); + return CompletableFuture.supplyAsync( + () -> + Util.extractChat(callbackQueryContext, update) + .map( + chat -> { + final String message = modelHelp.getMessage(chat, tgCommandProcessors); + final Optional messageId = Util.extractMessageId(update); + final InlineKeyboardButton[] buttons = + modelHelp.generateInlineKeyBoardButton(chat, eventProcessor); + BaseRequest request; + if (messageId.isPresent()) { + final EditMessageText editMessageText = + new EditMessageText(chat.getId(), messageId.get(), message); + editMessageText.replyMarkup(new InlineKeyboardMarkup(buttons)); + request = editMessageText; + } else { + final SendMessage sendMessage = + new SendMessage(chat.getId(), message).parseMode(ParseMode.Markdown); + sendMessage.replyMarkup(new InlineKeyboardMarkup(buttons)); + request = sendMessage; + } + + return request; + }), + threadPool); } } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java new file mode 100644 index 0000000..67ef62f --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java @@ -0,0 +1,29 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.model.query.QTgChat; +import com.pengrad.telegrambot.model.Message; +import com.pengrad.telegrambot.model.Update; +import java.util.Optional; + +public class Util { + + public static Optional extractChat( + CallbackQueryContext callbackQueryContext, Update update) { + return Optional.of(callbackQueryContext.getFields().getTelegramChatId()) + .map(Double::longValue) + // If we're desperate, search in the message for the chat id + .or( + () -> + Optional.ofNullable(update.callbackQuery().message()) + .map(Message::messageId) + .map(Long::valueOf)) + .filter(chatId -> chatId != 0L && chatId != Long.MIN_VALUE) + .flatMap(chatId -> new QTgChat().id.eq(chatId).findOneOrEmpty()); + } + + public static Optional extractMessageId(Update update) { + return Optional.ofNullable(update.callbackQuery().message()).map(Message::messageId); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java index a7a6526..286b43a 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java @@ -1,13 +1,6 @@ package com.github.polpetta.mezzotre.telegram.command; -import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; -import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; -import com.github.polpetta.mezzotre.telegram.callbackquery.Field; -import com.github.polpetta.mezzotre.util.Clock; -import com.github.polpetta.mezzotre.util.UUIDGenerator; -import com.github.polpetta.types.json.CallbackQueryMetadata; -import com.github.polpetta.types.json.ChatContext; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.InlineKeyboardButton; import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; @@ -19,38 +12,30 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; -import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; -import org.apache.commons.lang3.tuple.Pair; public class Help implements Processor { private static final String TRIGGERING_STAGING_NAME = "/help"; - private final TemplateContentGenerator templateContentGenerator; private final Executor threadPool; - private final Clock clock; private final Map tgCommandProcessors; private final Map eventProcessor; - private final UUIDGenerator uuidGenerator; + private final com.github.polpetta.mezzotre.telegram.model.Help modelHelp; @Inject public Help( - TemplateContentGenerator templateContentGenerator, @Named("eventThreadPool") Executor threadPool, - Clock clock, @Named("commandProcessor") Map tgCommandProcessors, @Named("eventProcessors") Map eventProcessor, - UUIDGenerator uuidGenerator) { - this.templateContentGenerator = templateContentGenerator; + com.github.polpetta.mezzotre.telegram.model.Help modelHelp) { this.threadPool = threadPool; - this.clock = clock; this.tgCommandProcessors = tgCommandProcessors; this.eventProcessor = eventProcessor; - this.uuidGenerator = uuidGenerator; + this.modelHelp = modelHelp; } @Override @@ -62,66 +47,17 @@ public class Help implements Processor { public CompletableFuture>> process(TgChat chat, Update update) { return CompletableFuture.supplyAsync( () -> { - final String message = - templateContentGenerator.mergeTemplate( - velocityContext -> { - velocityContext.put( - "commands", - tgCommandProcessors.values().stream() - .distinct() - .map( - p -> - Pair.of( - p.getTriggerKeywords().stream() - .sorted() - .collect(Collectors.toList()), - p.getLocaleDescriptionKeyword())) - .collect(Collectors.toList())); - }, - chat.getLocale(), - "template/telegram/help.vm"); - - final ChatContext chatContext = chat.getChatContext(); - chatContext.setStage(TRIGGERING_STAGING_NAME); - chatContext.setStep(0); - chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now()); - chat.setChatContext(chatContext); - chat.setHasHelpBeenShown(true); - chat.save(); + final String message = modelHelp.getMessage(chat, tgCommandProcessors); final SendMessage sendMessage = new SendMessage(chat.getId(), message).parseMode(ParseMode.Markdown); - final String callBackGroupId = uuidGenerator.generateAsString(); - final InlineKeyboardButton[] collect = - eventProcessor.values().stream() - .filter( - com.github.polpetta.mezzotre.telegram.callbackquery.Processor - ::canBeDirectlyInvokedByTheUser) - .filter(e -> e.getPrettyPrintLocaleKeyName().isPresent()) - .map( - eventProcessor -> { - final CallbackQueryContext callbackQueryContext = - new CallbackQueryContext( - uuidGenerator.generateAsString(), - callBackGroupId, - new CallbackQueryMetadata.CallbackQueryMetadataBuilder() - .withEvent(eventProcessor.getEventName()) - .withTelegramChatId(chat.getId()) - .withAdditionalProperty( - Field.ShowHelp.InvokedFromHelpMessage.getName(), true) - .build()); - callbackQueryContext.save(); + final InlineKeyboardButton[] buttons = + modelHelp.generateInlineKeyBoardButton(chat, eventProcessor); - return new InlineKeyboardButton( - templateContentGenerator.getString( - chat.getLocale(), - eventProcessor.getPrettyPrintLocaleKeyName().get())) - .callbackData(callbackQueryContext.getId()); - }) - .toArray(InlineKeyboardButton[]::new); - - sendMessage.replyMarkup(new InlineKeyboardMarkup(collect)); + if (buttons.length > 0) { + sendMessage.replyMarkup(new InlineKeyboardMarkup(buttons)); + } return Optional.of(sendMessage); }, diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java index 393c3e5..ed5f028 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java @@ -43,6 +43,6 @@ public interface Processor { * description */ default String getLocaleDescriptionKeyword() { - return this.getClass().getName().toLowerCase() + ".cmdDescription"; + return this.getClass().getSimpleName().toLowerCase() + ".cmdDescription"; } } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java b/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java index 7dd5e66..401c8e0 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java @@ -1,18 +1,97 @@ package com.github.polpetta.mezzotre.telegram.model; -import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; +import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.telegram.callbackquery.Field; +import com.github.polpetta.mezzotre.telegram.command.Processor; +import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.mezzotre.util.UUIDGenerator; +import com.github.polpetta.types.json.CallbackQueryMetadata; +import com.github.polpetta.types.json.ChatContext; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import java.util.Map; +import java.util.stream.Collectors; import javax.inject.Inject; +import org.apache.commons.lang3.tuple.Pair; +// FIXME tests! public class Help { - private final LocalizedMessageFactory localizedMessageFactory; + private static final String TRIGGERING_STAGING_NAME = "/help"; + + private final TemplateContentGenerator templateContentGenerator; + private final Clock clock; + private final UUIDGenerator uuidGenerator; @Inject - public Help(LocalizedMessageFactory localizedMessageFactory) { - this.localizedMessageFactory = localizedMessageFactory; + public Help( + TemplateContentGenerator templateContentGenerator, Clock clock, UUIDGenerator uuidGenerator) { + + this.templateContentGenerator = templateContentGenerator; + this.clock = clock; + this.uuidGenerator = uuidGenerator; } - public String generateErrorMessage() { - return ""; + public String getMessage(TgChat chat, Map tgCommandProcessors) { + final String message = + templateContentGenerator.mergeTemplate( + velocityContext -> { + velocityContext.put( + "commands", + tgCommandProcessors.values().stream() + .distinct() + .map( + p -> + Pair.of( + p.getTriggerKeywords().stream() + .sorted() + .collect(Collectors.toList()), + p.getLocaleDescriptionKeyword())) + .collect(Collectors.toList())); + }, + chat.getLocale(), + "template/telegram/help.vm"); + + // FIXME this shouldn't stay here. We need to move it into another method + final ChatContext chatContext = chat.getChatContext(); + chatContext.setStage(TRIGGERING_STAGING_NAME); + chatContext.setStep(0); + chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now()); + chat.setChatContext(chatContext); + chat.setHasHelpBeenShown(true); + chat.save(); + return message; + } + + public InlineKeyboardButton[] generateInlineKeyBoardButton( + TgChat chat, + Map eventProcessors) { + final String callBackGroupId = uuidGenerator.generateAsString(); + return eventProcessors.values().stream() + .filter( + com.github.polpetta.mezzotre.telegram.callbackquery.Processor + ::canBeDirectlyInvokedByTheUser) + .filter(e -> e.getPrettyPrintLocaleKeyName().isPresent()) + .map( + eventProcessor -> { + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext( + uuidGenerator.generateAsString(), + callBackGroupId, + new CallbackQueryMetadata.CallbackQueryMetadataBuilder() + .withEvent(eventProcessor.getEventName()) + .withTelegramChatId(chat.getId()) + .withAdditionalProperty( + Field.ShowHelp.InvokedFromHelpMessage.getName(), true) + .build()); + callbackQueryContext.save(); + + return new InlineKeyboardButton( + templateContentGenerator.getString( + chat.getLocale(), eventProcessor.getPrettyPrintLocaleKeyName().get())) + .callbackData(callbackQueryContext.getId()); + }) + .toArray(InlineKeyboardButton[]::new); } } diff --git a/src/main/resources/i18n/message.properties b/src/main/resources/i18n/message.properties index b0cf9e5..d022a29 100644 --- a/src/main/resources/i18n/message.properties +++ b/src/main/resources/i18n/message.properties @@ -1,11 +1,11 @@ start.helloFirstName=Hello {0}! \ud83d\udc4b -start.description=This is {0}, a simple bot focused on DnD content management! Please start by choosing a selectLanguageTutorial down below \ud83d\udc47 +start.description=This is {0}, a simple bot focused on DnD content management! Please start by choosing a language down below \ud83d\udc47 start.cmdDescription=Trigger this very bot start.inlineKeyboardButtonName=Let''s begin! selectLanguageTutorial.inlineKeyboardButtonName=Select language selectLanguageTutorial.drinkAction=*Proceeds to drink a potion with a strange, multicolor liquid* -selectLanguageTutorial.setLanguage=Thanks! Now that I drank this modified potion of {0} that I''ve found at the "Crystal Fermentary" magic potion shop yesterday I can speak with you in the selectLanguageTutorial that you prefer! -selectLanguageTutorial.instructions=You can always change your selectLanguageTutorial settings by typing /selectLanguageTutorial in the chat. +selectLanguageTutorial.setLanguage=Thanks! Now that I drank this modified potion of {0} that I''ve found at the "Crystal Fermentary" magic potion shop yesterday I can speak with you in the language that you prefer! +selectLanguageTutorial.instructions=You can always change your language settings by typing /changeLanguage in the chat. changeLanguage.english=English changeLanguage.italian=Italian changeLanguage.cmdDescription=Select the new language I will use to speak to you diff --git a/src/main/resources/i18n/message_en_US.properties b/src/main/resources/i18n/message_en_US.properties index 296200c..d022a29 100644 --- a/src/main/resources/i18n/message_en_US.properties +++ b/src/main/resources/i18n/message_en_US.properties @@ -1,11 +1,19 @@ start.helloFirstName=Hello {0}! \ud83d\udc4b -start.description=This is {0}, a simple bot focused on DnD content management! Please start by choosing a selectLanguageTutorial down below \ud83d\udc47 +start.description=This is {0}, a simple bot focused on DnD content management! Please start by choosing a language down below \ud83d\udc47 +start.cmdDescription=Trigger this very bot +start.inlineKeyboardButtonName=Let''s begin! +selectLanguageTutorial.inlineKeyboardButtonName=Select language selectLanguageTutorial.drinkAction=*Proceeds to drink a potion with a strange, multicolor liquid* -selectLanguageTutorial.setLanguage=Thanks! Now that I drank this modified potion of {0} that I''ve found at the "Crystal Fermentary" magic potion shop yesterday I can speak with you in the selectLanguageTutorial that you prefer! -selectLanguageTutorial.instructions=You can always change your selectLanguageTutorial settings by typing /selectLanguageTutorial in the chat. -selectLanguageTutorial.english=English -selectLanguageTutorial.italian=Italian +selectLanguageTutorial.setLanguage=Thanks! Now that I drank this modified potion of {0} that I''ve found at the "Crystal Fermentary" magic potion shop yesterday I can speak with you in the language that you prefer! +selectLanguageTutorial.instructions=You can always change your language settings by typing /changeLanguage in the chat. +changeLanguage.english=English +changeLanguage.italian=Italian +changeLanguage.cmdDescription=Select the new language I will use to speak to you +changeLanguage.inlineKeyboardButtonName=Change language spell.speakWithAnimals=Speak with animals selectLanguageTutorial.showMeTutorialInlineKeyboardButtonName=Show me what you can do! help.notShownYet=It seems you haven''t checked out what I can do yet! To have a complete list of my abilities, type /help in chat at any time! help.buttonBelow=Alternatively, you can click the button down below. +help.description=Here is a list of what I can do +help.buttonsToo=You can do the same operations you''d do with the commands aforementioned by selecting the corresponding button below \ud83d\udc47 +help.cmdDescription=Print the help message diff --git a/src/main/resources/i18n/message_it.properties b/src/main/resources/i18n/message_it.properties index dcdfadc..01ae788 100644 --- a/src/main/resources/i18n/message_it.properties +++ b/src/main/resources/i18n/message_it.properties @@ -2,7 +2,7 @@ start.helloFirstName=Ciao {0}! \ud83d\udc4b start.description=Questo è {0}, un semplice bot che ci concenta sulla gestione di contenuto per DnD! Per favore comincia selezionando la lingua qui sotto \ud83d\udc47 selectLanguageTutorial.drinkAction=*Procede a bere una pozione al cui suo interno si trova uno strano liquido multicolore* selectLanguageTutorial.setLanguage=Grazie! Ora che ho bevuto questa posizione modificata di {0} che ho trovato ieri al negozio di pozioni magiche la "Cristalleria Fermentatrice" posso parlare con te nel linguaggio che preferisci! -selectLanguageTutorial.instructions=Puoi sempre cambiare le preferenze della tua lingua scrivendo /selectLanguageTutorial nella chat. +selectLanguageTutorial.instructions=Puoi sempre cambiare le preferenze della tua lingua scrivendo /changeLanguage nella chat. selectLanguageTutorial.english=Inglese selectLanguageTutorial.italian=Italiano spell.speakWithAnimals=Parlare con animali diff --git a/src/main/resources/i18n/message_it_IT.properties b/src/main/resources/i18n/message_it_IT.properties index dcdfadc..01ae788 100644 --- a/src/main/resources/i18n/message_it_IT.properties +++ b/src/main/resources/i18n/message_it_IT.properties @@ -2,7 +2,7 @@ start.helloFirstName=Ciao {0}! \ud83d\udc4b start.description=Questo è {0}, un semplice bot che ci concenta sulla gestione di contenuto per DnD! Per favore comincia selezionando la lingua qui sotto \ud83d\udc47 selectLanguageTutorial.drinkAction=*Procede a bere una pozione al cui suo interno si trova uno strano liquido multicolore* selectLanguageTutorial.setLanguage=Grazie! Ora che ho bevuto questa posizione modificata di {0} che ho trovato ieri al negozio di pozioni magiche la "Cristalleria Fermentatrice" posso parlare con te nel linguaggio che preferisci! -selectLanguageTutorial.instructions=Puoi sempre cambiare le preferenze della tua lingua scrivendo /selectLanguageTutorial nella chat. +selectLanguageTutorial.instructions=Puoi sempre cambiare le preferenze della tua lingua scrivendo /changeLanguage nella chat. selectLanguageTutorial.english=Inglese selectLanguageTutorial.italian=Italiano spell.speakWithAnimals=Parlare con animali diff --git a/src/main/resources/template/telegram/help.vm b/src/main/resources/template/telegram/help.vm index 38a935e..7033d0c 100644 --- a/src/main/resources/template/telegram/help.vm +++ b/src/main/resources/template/telegram/help.vm @@ -1,6 +1,7 @@ ${i18n.help.description}: #foreach(${command} in ${commands}) +## FIXME this is not displayed correctly in telegram! *#foreach(${key} in ${command.left}) ${key}#end: ${i18n.get(${command.right})} #end diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java index 17758fe..c28e6cc 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java @@ -86,7 +86,7 @@ class SelectLanguageTutorialIntegrationTest { + " posso parlare con te nel linguaggio che preferisci!\n" + "\n" + "Puoi sempre cambiare le preferenze della tua lingua scrivendo" - + " /selectLanguageTutorial nella chat.\n" + + " /changeLanguage nella chat.\n" + "\n" + "Sembra tu non abbia ancora visto cosa posso fare! Per avere una lista completa" + " delle mie abilità, scrivi /help nella chat in qualsiasi momento!" @@ -102,7 +102,7 @@ class SelectLanguageTutorialIntegrationTest { + " found at the \"Crystal Fermentary\" magic potion shop yesterday I can speak" + " with you in the language that you prefer!\n" + "\n" - + "You can always change your language settings by typing /selectLanguageTutorial" + + "You can always change your language settings by typing /changeLanguage" + " in the chat.\n" + "\n" + "It seems you haven't checked out what I can do yet! To have a complete list of" @@ -121,7 +121,7 @@ class SelectLanguageTutorialIntegrationTest { + " posso parlare con te nel linguaggio che preferisci!\n" + "\n" + "Puoi sempre cambiare le preferenze della tua lingua scrivendo" - + " /selectLanguageTutorial nella chat.\n\n", + + " /changeLanguage nella chat.\n\n", "en-US", "Mostrami cosa puoi fare!", true), @@ -133,7 +133,7 @@ class SelectLanguageTutorialIntegrationTest { + " found at the \"Crystal Fermentary\" magic potion shop yesterday I can speak" + " with you in the language that you prefer!\n" + "\n" - + "You can always change your language settings by typing /selectLanguageTutorial" + + "You can always change your language settings by typing /changeLanguage" + " in the chat.\n\n", "it-IT", "Show me what you can do!", diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java index 29fbc77..3f472aa 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java @@ -169,15 +169,14 @@ class HelpIntegrationTest { events.put(dummyEvent1.getEventName(), dummyEvent1); events.put(dummyEvent2.getEventName(), dummyEvent2); - help = - new Help( + final com.github.polpetta.mezzotre.telegram.model.Help modelHelp = + new com.github.polpetta.mezzotre.telegram.model.Help( new TemplateContentGenerator(new LocalizedMessageFactory(velocityEngine)), - Executors.newSingleThreadExecutor(), fakeClock, - commands, - events, new UUIDGenerator()); + help = new Help(Executors.newSingleThreadExecutor(), commands, events, modelHelp); + final Update update = gson.fromJson( "{\n" diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/ProcessorTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/ProcessorTest.java new file mode 100644 index 0000000..3e1402a --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/ProcessorTest.java @@ -0,0 +1,38 @@ +package com.github.polpetta.mezzotre.telegram.command; + +import static org.junit.jupiter.api.Assertions.*; + +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.BaseRequest; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@Execution(ExecutionMode.CONCURRENT) +class ProcessorTest { + + @Test + void shouldGetSimpleNameClassIfNotDefined() { + + class Test implements Processor { + + @Override + public Set getTriggerKeywords() { + return null; + } + + @Override + public CompletableFuture>> process(TgChat chat, Update update) { + return null; + } + } + + final Test test = new Test(); + + assertEquals("test.cmdDescription", test.getLocaleDescriptionKeyword()); + } +} -- 2.40.1 From 9ff574896479f8b1856da1ac7a2269f79115c8db Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Fri, 14 Apr 2023 15:00:25 +0200 Subject: [PATCH 04/13] chore: perform refactor for db-cleaning and more testing * Add Javadoc where missing * Solve some FIXMEs here and there --- .../com/github/polpetta/mezzotre/App.java | 6 +- .../com/github/polpetta/mezzotre/AppDI.java | 14 + .../mezzotre/orm/BatchBeanCleanerService.java | 99 ++++ .../orm/CallbackQueryContextCleaner.java | 37 ++ .../mezzotre/orm/telegram/ChatUtil.java | 89 ++++ .../telegram/callbackquery/NotFound.java | 9 +- .../callbackquery/SelectLanguageTutorial.java | 31 +- .../telegram/callbackquery/ShowHelp.java | 49 +- .../mezzotre/telegram/callbackquery/Util.java | 18 +- .../mezzotre/telegram/command/Help.java | 23 +- .../mezzotre/telegram/command/NotFound.java | 9 +- .../telegram/command/NotFoundFactory.java | 2 +- .../mezzotre/telegram/command/Start.java | 22 +- .../mezzotre/telegram/model/Help.java | 51 +- .../polpetta/mezzotre/util/ServiceModule.java | 71 +++ .../github/polpetta/mezzotre/util/UtilDI.java | 43 ++ .../polpetta/mezzotre/util/di/ThreadPool.java | 25 - src/main/resources/template/telegram/help.vm | 3 +- src/main/resources/template/telegram/start.vm | 2 +- .../github/polpetta/mezzotre/UnitTest.java | 22 +- .../helper/IntegrationAppFactory.java | 20 +- ...atchBeanCleanerServiceIntegrationTest.java | 93 ++++ .../orm/telegram/ChatUtilIntegrationTest.java | 176 +++++++ .../mezzotre/orm/telegram/ChatUtilTest.java | 70 +++ .../telegram/callbackquery/NotFoundTest.java | 46 ++ ...SelectLanguageTutorialIntegrationTest.java | 49 +- .../ShowHelpIntegrationTest.java | 457 ++++++++++++++++++ .../telegram/callbackquery/ShowHelpTest.java | 314 ++++++++++++ .../telegram/command/HelpIntegrationTest.java | 14 +- .../mezzotre/telegram/command/HelpTest.java | 186 +++++++ .../command/StartIntegrationTest.java | 9 +- 31 files changed, 1930 insertions(+), 129 deletions(-) create mode 100644 src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/util/UtilDI.java delete mode 100644 src/main/java/com/github/polpetta/mezzotre/util/di/ThreadPool.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerServiceIntegrationTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilIntegrationTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpIntegrationTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpTest.java diff --git a/src/main/java/com/github/polpetta/mezzotre/App.java b/src/main/java/com/github/polpetta/mezzotre/App.java index c19f1fe..9ac428e 100644 --- a/src/main/java/com/github/polpetta/mezzotre/App.java +++ b/src/main/java/com/github/polpetta/mezzotre/App.java @@ -5,7 +5,8 @@ import com.github.polpetta.mezzotre.route.RouteDI; import com.github.polpetta.mezzotre.route.Telegram; import com.github.polpetta.mezzotre.telegram.callbackquery.CallbackQueryDI; import com.github.polpetta.mezzotre.telegram.command.CommandDI; -import com.github.polpetta.mezzotre.util.di.ThreadPool; +import com.github.polpetta.mezzotre.util.ServiceModule; +import com.github.polpetta.mezzotre.util.UtilDI; import com.google.inject.*; import com.google.inject.Module; import com.google.inject.name.Names; @@ -24,7 +25,7 @@ public class App extends Jooby { (jooby) -> { final HashSet modules = new HashSet<>(); modules.add(new OrmDI()); - modules.add(new ThreadPool()); + modules.add(new UtilDI()); modules.add(new RouteDI()); modules.add(new CommandDI()); modules.add(new CallbackQueryDI()); @@ -55,6 +56,7 @@ public class App extends Jooby { install( injector.getInstance(Key.get(Extension.class, Names.named("flyWayMigrationExtension")))); install(injector.getInstance(Key.get(Extension.class, Names.named("ebeanExtension")))); + install(injector.getInstance(Key.get(ServiceModule.class, Names.named("serviceModule")))); decorator(new AccessLogHandler()); decorator(new TransactionalRequest()); diff --git a/src/main/java/com/github/polpetta/mezzotre/AppDI.java b/src/main/java/com/github/polpetta/mezzotre/AppDI.java index 650f733..15d945f 100644 --- a/src/main/java/com/github/polpetta/mezzotre/AppDI.java +++ b/src/main/java/com/github/polpetta/mezzotre/AppDI.java @@ -1,10 +1,15 @@ package com.github.polpetta.mezzotre; +import com.github.polpetta.mezzotre.orm.BatchBeanCleanerService; +import com.google.common.util.concurrent.Service; import com.google.inject.AbstractModule; import com.google.inject.Provides; import io.jooby.Jooby; +import java.util.List; +import java.util.concurrent.TimeUnit; import javax.inject.Named; import javax.inject.Singleton; +import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; public class AppDI extends AbstractModule { @@ -28,4 +33,13 @@ public class AppDI extends AbstractModule { public String getAppName() { return APPLICATION_NAME; } + + @Provides + @Singleton + @Named("services") + public List getApplicationServices( + Logger logger, + @Named("serviceRunningCheckTime") Pair serviceRunningCheckTime) { + return List.of(new BatchBeanCleanerService(logger, serviceRunningCheckTime)); + } } diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java b/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java new file mode 100644 index 0000000..be7f970 --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java @@ -0,0 +1,99 @@ +package com.github.polpetta.mezzotre.orm; + +import com.google.common.util.concurrent.AbstractExecutionThreadService; +import io.ebean.typequery.PString; +import io.ebean.typequery.TQRootBean; +import io.vavr.Tuple; +import io.vavr.Tuple3; +import io.vavr.control.Try; +import java.util.LinkedList; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import javax.inject.Inject; +import javax.inject.Named; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; + +public class BatchBeanCleanerService extends AbstractExecutionThreadService { + + private final LinkedBlockingDeque< + Tuple3>, CompletableFuture>> + entriesToRemove; + private final Logger log; + private final Pair serviceRunningCheckTime; + private final LinkedList< + Consumer>, CompletableFuture>>> + deletionListener; + + @Inject + public BatchBeanCleanerService( + Logger logger, + @Named("serviceRunningCheckTime") Pair serviceRunningCheckTime) { + this.log = logger; + this.serviceRunningCheckTime = serviceRunningCheckTime; + this.entriesToRemove = new LinkedBlockingDeque<>(); + this.deletionListener = new LinkedList<>(); + } + + @Override + protected void run() throws Exception { + while (isRunning()) { + // This statement is blocking for 1 sec if the queue is empty, and then it goes on - this way + // we check if the service is still supposed to be up or what + Optional.ofNullable( + entriesToRemove.poll( + serviceRunningCheckTime.getLeft(), serviceRunningCheckTime.getRight())) + .ifPresent( + entryToRemove -> { + Try.of( + () -> { + final int deleted = entryToRemove._2().eq(entryToRemove._1()).delete(); + if (deleted > 0) { + log.trace( + "Bean with id " + entryToRemove._1() + " removed successfully "); + } else { + log.warn( + "Bean(s) with id " + + entryToRemove._1() + + " for query " + + entryToRemove._2() + + " was not removed from the database because it was not" + + " found"); + } + entryToRemove._3().complete(deleted); + deletionListener.parallelStream().forEach(l -> l.accept(entryToRemove)); + return null; + }) + .onFailure(ex -> entryToRemove._3().completeExceptionally(ex)); + }); + } + } + + @Override + protected void shutDown() throws Exception { + super.shutDown(); + entriesToRemove.clear(); + } + + public CompletableFuture removeAsync( + String id, PString> row) { + final CompletableFuture jobExecution = new CompletableFuture<>(); + entriesToRemove.offer(Tuple.of(id, row, jobExecution)); + return jobExecution; + } + + public void addListener( + Consumer>, CompletableFuture>> + listener) { + deletionListener.add(listener); + } + + public void removeListener( + Consumer>, CompletableFuture>> + listener) { + deletionListener.remove(listener); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java b/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java new file mode 100644 index 0000000..0119168 --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java @@ -0,0 +1,37 @@ +package com.github.polpetta.mezzotre.orm; + +import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; +import io.ebean.typequery.PString; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.slf4j.Logger; + +@Singleton +public class CallbackQueryContextCleaner { + + private static final Supplier> ENTRY_GROUP = + () -> new QCallbackQueryContext().entryGroup; + private static final Supplier> SINGLE_ENTRY = + () -> new QCallbackQueryContext().id; + + private final BatchBeanCleanerService batchBeanCleanerService; + private final Logger log; + + @Inject + public CallbackQueryContextCleaner(BatchBeanCleanerService batchBeanCleanerService, Logger log) { + this.batchBeanCleanerService = batchBeanCleanerService; + this.log = log; + } + + public CompletableFuture removeGroupAsync(String id) { + log.trace("CallbackQueryContext entry group " + id + " queued for removal"); + return batchBeanCleanerService.removeAsync(id, ENTRY_GROUP.get()); + } + + public CompletableFuture removeIdAsync(String id) { + log.trace("CallbackQueryContext single entity " + id + " queued for removal"); + return batchBeanCleanerService.removeAsync(id, SINGLE_ENTRY.get()); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java b/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java new file mode 100644 index 0000000..b722f2b --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java @@ -0,0 +1,89 @@ +package com.github.polpetta.mezzotre.orm.telegram; + +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.query.QTgChat; +import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.types.json.ChatContext; +import com.pengrad.telegrambot.model.Message; +import com.pengrad.telegrambot.model.Update; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import javax.inject.Inject; + +/** + * ChatUtil provides utilities for interacting in an easier way when manipulating chat related data. + * In particular, it provides an easy and DRY way to modify a set of frequently-accessed fields, or + * simply to extract data performing all the necessary steps + * + * @author Davide Polonio + * @since 1.0 + */ +public class ChatUtil { + + private final Clock clock; + + @Inject + public ChatUtil(Clock clock) { + this.clock = clock; + } + + /** + * Update the {@link ChatContext} with the values passed to the method. The updated values are + * then saved in the database + * + * @param chat the chat that will be updated with the new {@link ChatContext} values + * @param stepName the step name to set + * @param stageNumber the stage number to set + * @param additionalFields if there are, additional custom fields that will be added to {@link + * ChatContext}. Note that these values will have to be manually retrieved since no method is + * available for custom entries. Use {@link Collections#emptyMap()} if you don't wish to add + * any additional field + */ + public void updateChatContext( + TgChat chat, String stepName, int stageNumber, Map additionalFields) { + final ChatContext chatContext = chat.getChatContext(); + chatContext.setStage(stepName); + chatContext.setStep(stageNumber); + chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now()); + additionalFields.forEach(chatContext::setAdditionalProperty); + + chat.setChatContext(chatContext); + chat.save(); + } + + /** + * Retrieves a possible {@link TgChat} from the given {@link CallbackQueryContext} or the {@link + * Update}. Note that first the {@link CallbackQueryContext} is checked, then {@link Update} is + * used as fallback. Once the Telegram chat id is retrieved from one of these two, the persistence + * layer is queried and if there is any {@link TgChat} which the corresponding ID it is returned + * to the caller. + * + * @param callbackQueryContext the {@link CallbackQueryContext} coming from a Telegram callback + * query + * @param update the whole {@link Update} object that is sent from Telegram servers + * @return an {@link Optional} that may contain a {@link TgChat} if the ID is found in the + * persistence layer, otherwise {@link Optional#empty()} is given back either if the id is not + * present or if the chat is not present in the persistence layer. + */ + public Optional extractChat(CallbackQueryContext callbackQueryContext, Update update) { + return Optional.of(callbackQueryContext.getFields().getTelegramChatId()) + .map(Double::longValue) + // If we're desperate, search in the message for the chat id + .filter(chatId -> chatId != 0L && chatId != Long.MIN_VALUE) + .or( + () -> + Optional.ofNullable(update.callbackQuery().message()) + .map(Message::messageId) + .map(Long::valueOf)) + .filter(chatId -> chatId != 0L && chatId != Long.MIN_VALUE) + .flatMap(chatId -> new QTgChat().id.eq(chatId).findOneOrEmpty()); + } + + public T cleanCallbackQuery(T toReturn, CallbackQueryContext callbackQueryContext) { + new QCallbackQueryContext().entryGroup.eq(callbackQueryContext.getId()).delete(); + return toReturn; + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java index 605fd4e..b63a9bd 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java @@ -1,5 +1,6 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.google.inject.assistedinject.Assisted; import com.pengrad.telegrambot.model.Update; @@ -12,11 +13,16 @@ import org.slf4j.Logger; public class NotFound implements Processor { private final Logger log; + private final CallbackQueryContextCleaner callbackQueryContextCleaner; private final String eventName; @Inject - public NotFound(Logger logger, @Assisted String eventName) { + public NotFound( + Logger logger, + CallbackQueryContextCleaner callbackQueryContextCleaner, + @Assisted String eventName) { this.log = logger; + this.callbackQueryContextCleaner = callbackQueryContextCleaner; this.eventName = eventName; } @@ -33,6 +39,7 @@ public class NotFound implements Processor { + callbackQueryContext.getId() + " event name " + eventName); + callbackQueryContextCleaner.removeGroupAsync(callbackQueryContext.getEntryGroup()); return CompletableFuture.completedFuture(Optional.empty()); } } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java index c596a17..3b2abeb 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java @@ -1,8 +1,9 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; -import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.CallbackQueryMetadata; import com.pengrad.telegrambot.model.Update; @@ -36,17 +37,23 @@ public class SelectLanguageTutorial implements Processor { private final TemplateContentGenerator templateContentGenerator; private final Logger log; private final UUIDGenerator uuidGenerator; + private final ChatUtil chatUtil; + private final CallbackQueryContextCleaner callbackQueryContextCleaner; @Inject public SelectLanguageTutorial( @Named("eventThreadPool") Executor threadPool, TemplateContentGenerator templateContentGenerator, Logger log, - UUIDGenerator uuidGenerator) { + UUIDGenerator uuidGenerator, + ChatUtil chatUtil, + CallbackQueryContextCleaner callbackQueryContextCleaner) { this.threadPool = threadPool; this.templateContentGenerator = templateContentGenerator; this.log = log; this.uuidGenerator = uuidGenerator; + this.chatUtil = chatUtil; + this.callbackQueryContextCleaner = callbackQueryContextCleaner; } @Override @@ -59,7 +66,8 @@ public class SelectLanguageTutorial implements Processor { CallbackQueryContext callbackQueryContext, Update update) { return CompletableFuture.supplyAsync( () -> - Util.extractChat(callbackQueryContext, update) + chatUtil + .extractChat(callbackQueryContext, update) .map( tgChat -> { tgChat.setLocale( @@ -90,6 +98,14 @@ public class SelectLanguageTutorial implements Processor { .thenApplyAsync( // If we are here then we're sure there is at least a chat associated with this callback tgChat -> { + if (tgChat.getHasHelpBeenShown()) { + log.trace( + "Help message has already been shown for this user - no help button will be" + + " present"); + } else { + log.trace("No help message shown yet - the help button will be added"); + } + final String message = templateContentGenerator.mergeTemplate( velocityContext -> @@ -99,14 +115,7 @@ public class SelectLanguageTutorial implements Processor { log.trace("SelectLanguageTutorial event - message to send back: " + message); - final String callBackGroupToDelete = callbackQueryContext.getEntryGroup(); - final int delete = - new QCallbackQueryContext().entryGroup.eq(callBackGroupToDelete).delete(); - log.trace( - "Deleted " - + delete - + " entries regarding callback group " - + callBackGroupToDelete); + callbackQueryContextCleaner.removeGroupAsync(callbackQueryContext.getEntryGroup()); final Optional messageId = Util.extractMessageId(update); BaseRequest baseRequest; diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java index baeee31..302a931 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java @@ -1,6 +1,8 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; import com.github.polpetta.mezzotre.telegram.model.Help; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.InlineKeyboardButton; @@ -9,13 +11,28 @@ import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.EditMessageText; import com.pengrad.telegrambot.request.SendMessage; +import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Singleton; +/** + * ShowHelp callback query event edits a previous message (if available), otherwise it sends a new + * message with a list of available commands (the one implemented by {@link + * com.github.polpetta.mezzotre.telegram.command.Processor}), and a list of buttons that are + * callback queries (the ones implemented by {@link Processor}). The class is a Singleton since it + * is stateless and having multiple instances would only be a waste of memory. + * + * @author Davide Polonio + * @since 1.0 + * @see com.github.polpetta.mezzotre.telegram.command.Help same functionality but as {@link + * com.github.polpetta.mezzotre.telegram.command.Processor} command + */ +@Singleton class ShowHelp implements Processor { public static String EVENT_NAME = "showHelp"; @@ -24,8 +41,9 @@ class ShowHelp implements Processor { private final Map tgCommandProcessors; private final Map eventProcessor; + private final ChatUtil chatUtil; + private final CallbackQueryContextCleaner callbackQueryContextCleaner; - // FIXME tests @Inject public ShowHelp( @Named("eventThreadPool") Executor threadPool, @@ -33,12 +51,15 @@ class ShowHelp implements Processor { @Named("commandProcessor") Map tgCommandProcessors, @Named("eventProcessors") - Map - eventProcessor) { + Map eventProcessor, + ChatUtil chatUtil, + CallbackQueryContextCleaner callbackQueryContextCleaner) { this.threadPool = threadPool; this.modelHelp = modelHelp; this.tgCommandProcessors = tgCommandProcessors; this.eventProcessor = eventProcessor; + this.chatUtil = chatUtil; + this.callbackQueryContextCleaner = callbackQueryContextCleaner; } @Override @@ -51,26 +72,40 @@ class ShowHelp implements Processor { CallbackQueryContext callbackQueryContext, Update update) { return CompletableFuture.supplyAsync( () -> - Util.extractChat(callbackQueryContext, update) + chatUtil + .extractChat(callbackQueryContext, update) // FIXME callbackquerycontext removal? .map( chat -> { final String message = modelHelp.getMessage(chat, tgCommandProcessors); + chatUtil.updateChatContext(chat, EVENT_NAME, 0, Collections.emptyMap()); + chat.setHasHelpBeenShown(true); + chat.save(); final Optional messageId = Util.extractMessageId(update); final InlineKeyboardButton[] buttons = modelHelp.generateInlineKeyBoardButton(chat, eventProcessor); BaseRequest request; if (messageId.isPresent()) { final EditMessageText editMessageText = - new EditMessageText(chat.getId(), messageId.get(), message); - editMessageText.replyMarkup(new InlineKeyboardMarkup(buttons)); + new EditMessageText(chat.getId(), messageId.get(), message) + .parseMode(ParseMode.Markdown); + if (buttons.length > 0) { + editMessageText.replyMarkup(new InlineKeyboardMarkup(buttons)); + } request = editMessageText; } else { final SendMessage sendMessage = new SendMessage(chat.getId(), message).parseMode(ParseMode.Markdown); - sendMessage.replyMarkup(new InlineKeyboardMarkup(buttons)); + if (buttons.length > 0) { + sendMessage.replyMarkup(new InlineKeyboardMarkup(buttons)); + } request = sendMessage; } + // We don't check if the element is removed or what - we just schedule its + // removal, then it is someone else problem + callbackQueryContextCleaner.removeGroupAsync( + callbackQueryContext.getEntryGroup()); + return request; }), threadPool); diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java index 67ef62f..7f6d6f9 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java @@ -1,28 +1,12 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; -import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; -import com.github.polpetta.mezzotre.orm.model.TgChat; -import com.github.polpetta.mezzotre.orm.model.query.QTgChat; import com.pengrad.telegrambot.model.Message; import com.pengrad.telegrambot.model.Update; import java.util.Optional; public class Util { - public static Optional extractChat( - CallbackQueryContext callbackQueryContext, Update update) { - return Optional.of(callbackQueryContext.getFields().getTelegramChatId()) - .map(Double::longValue) - // If we're desperate, search in the message for the chat id - .or( - () -> - Optional.ofNullable(update.callbackQuery().message()) - .map(Message::messageId) - .map(Long::valueOf)) - .filter(chatId -> chatId != 0L && chatId != Long.MIN_VALUE) - .flatMap(chatId -> new QTgChat().id.eq(chatId).findOneOrEmpty()); - } - + // FIXME tests, doc public static Optional extractMessageId(Update update) { return Optional.ofNullable(update.callbackQuery().message()).map(Message::messageId); } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java index 286b43a..9b05c14 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java @@ -1,12 +1,14 @@ package com.github.polpetta.mezzotre.telegram.command; import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.InlineKeyboardButton; import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.SendMessage; +import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -14,8 +16,19 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Singleton; -public class Help implements Processor { +/** + * The Help command class allows the user to be informed in any moment of commands that are + * available in the system. If possible, it also provides a {@code reply_markup} keyboard ({@link + * InlineKeyboardMarkup} for easier bot interactions + * + * @author Davide Polonio + * @since 1.0 + * @see com.github.polpetta.mezzotre.telegram.callbackquery.ShowHelp for event-based help + */ +@Singleton +class Help implements Processor { private static final String TRIGGERING_STAGING_NAME = "/help"; @@ -24,6 +37,7 @@ public class Help implements Processor { private final Map eventProcessor; private final com.github.polpetta.mezzotre.telegram.model.Help modelHelp; + private final ChatUtil chatUtil; @Inject public Help( @@ -31,11 +45,13 @@ public class Help implements Processor { @Named("commandProcessor") Map tgCommandProcessors, @Named("eventProcessors") Map eventProcessor, - com.github.polpetta.mezzotre.telegram.model.Help modelHelp) { + com.github.polpetta.mezzotre.telegram.model.Help modelHelp, + ChatUtil chatUtil) { this.threadPool = threadPool; this.tgCommandProcessors = tgCommandProcessors; this.eventProcessor = eventProcessor; this.modelHelp = modelHelp; + this.chatUtil = chatUtil; } @Override @@ -48,6 +64,9 @@ public class Help implements Processor { return CompletableFuture.supplyAsync( () -> { final String message = modelHelp.getMessage(chat, tgCommandProcessors); + chatUtil.updateChatContext(chat, TRIGGERING_STAGING_NAME, 0, Collections.emptyMap()); + chat.setHasHelpBeenShown(true); + chat.save(); final SendMessage sendMessage = new SendMessage(chat.getId(), message).parseMode(ParseMode.Markdown); diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java index 71714d8..deef282 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java @@ -11,7 +11,14 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import javax.inject.Inject; -public class NotFound implements Processor { +/** + * Generates a "Command not found" message + * + * @author Davide Polonio + * @since 1.0 + * @see com.github.polpetta.mezzotre.telegram.callbackquery.NotFound for the event-based version + */ +class NotFound implements Processor { private final String commandName; diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java index c4e2278..381c449 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java @@ -1,5 +1,5 @@ package com.github.polpetta.mezzotre.telegram.command; -public interface NotFoundFactory { +interface NotFoundFactory { NotFound create(String commandName); } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java index e3a84f9..bb2b859 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java @@ -3,12 +3,11 @@ package com.github.polpetta.mezzotre.telegram.command; import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; import com.github.polpetta.mezzotre.telegram.callbackquery.Field; import com.github.polpetta.mezzotre.telegram.callbackquery.Value; -import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.CallbackQueryMetadata; -import com.github.polpetta.types.json.ChatContext; import com.google.inject.Singleton; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.InlineKeyboardButton; @@ -16,6 +15,7 @@ import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.SendMessage; +import java.util.Collections; import java.util.Locale; import java.util.Optional; import java.util.Set; @@ -32,15 +32,15 @@ import org.slf4j.Logger; * @since 1.0 */ @Singleton -public class Start implements Processor { +class Start implements Processor { private static final String TRIGGERING_STAGING_NAME = "/start"; private final Executor threadPool; private final Logger log; private final UUIDGenerator uuidGenerator; - private final Clock clock; private final String applicationName; + private final ChatUtil chatUtil; private final TemplateContentGenerator templateContentGenerator; @@ -50,14 +50,14 @@ public class Start implements Processor { @Named("eventThreadPool") Executor threadPool, Logger log, UUIDGenerator uuidGenerator, - Clock clock, - @Named("applicationName") String applicationName) { + @Named("applicationName") String applicationName, + ChatUtil chatUtil) { this.templateContentGenerator = templateContentGenerator; this.threadPool = threadPool; this.log = log; this.uuidGenerator = uuidGenerator; - this.clock = clock; this.applicationName = applicationName; + this.chatUtil = chatUtil; } @Override @@ -87,12 +87,8 @@ public class Start implements Processor { "template/telegram/start.vm"); log.trace("Start command - message to send back: " + message); - final ChatContext chatContext = chat.getChatContext(); - chatContext.setStage(TRIGGERING_STAGING_NAME); - chatContext.setStep(0); - chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now()); - chat.setChatContext(chatContext); - chat.save(); + // FIXME bug!! Show help button set to true but its fake news + chatUtil.updateChatContext(chat, TRIGGERING_STAGING_NAME, 0, Collections.emptyMap()); // To get the messageId we should send the message first, then save it in the database! final String groupId = uuidGenerator.generateAsString(); diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java b/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java index 401c8e0..83ee044 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java @@ -5,10 +5,8 @@ import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.telegram.callbackquery.Field; import com.github.polpetta.mezzotre.telegram.command.Processor; -import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.CallbackQueryMetadata; -import com.github.polpetta.types.json.ChatContext; import com.pengrad.telegrambot.model.request.InlineKeyboardButton; import java.util.Map; import java.util.stream.Collectors; @@ -21,47 +19,30 @@ public class Help { private static final String TRIGGERING_STAGING_NAME = "/help"; private final TemplateContentGenerator templateContentGenerator; - private final Clock clock; private final UUIDGenerator uuidGenerator; @Inject - public Help( - TemplateContentGenerator templateContentGenerator, Clock clock, UUIDGenerator uuidGenerator) { - + public Help(TemplateContentGenerator templateContentGenerator, UUIDGenerator uuidGenerator) { this.templateContentGenerator = templateContentGenerator; - this.clock = clock; this.uuidGenerator = uuidGenerator; } public String getMessage(TgChat chat, Map tgCommandProcessors) { - final String message = - templateContentGenerator.mergeTemplate( - velocityContext -> { - velocityContext.put( - "commands", - tgCommandProcessors.values().stream() - .distinct() - .map( - p -> - Pair.of( - p.getTriggerKeywords().stream() - .sorted() - .collect(Collectors.toList()), - p.getLocaleDescriptionKeyword())) - .collect(Collectors.toList())); - }, - chat.getLocale(), - "template/telegram/help.vm"); - - // FIXME this shouldn't stay here. We need to move it into another method - final ChatContext chatContext = chat.getChatContext(); - chatContext.setStage(TRIGGERING_STAGING_NAME); - chatContext.setStep(0); - chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now()); - chat.setChatContext(chatContext); - chat.setHasHelpBeenShown(true); - chat.save(); - return message; + return templateContentGenerator.mergeTemplate( + velocityContext -> { + velocityContext.put( + "commands", + tgCommandProcessors.values().stream() + .distinct() + .map( + p -> + Pair.of( + p.getTriggerKeywords().stream().sorted().collect(Collectors.toList()), + p.getLocaleDescriptionKeyword())) + .collect(Collectors.toList())); + }, + chat.getLocale(), + "template/telegram/help.vm"); } public InlineKeyboardButton[] generateInlineKeyBoardButton( diff --git a/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java b/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java new file mode 100644 index 0000000..e725c4d --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java @@ -0,0 +1,71 @@ +package com.github.polpetta.mezzotre.util; + +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.Service; +import com.google.common.util.concurrent.ServiceManager; +import io.jooby.Extension; +import io.jooby.Jooby; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import javax.inject.Inject; +import javax.inject.Named; +import org.jetbrains.annotations.NotNull; + +public class ServiceModule implements Extension { + + private final ServiceManager serviceManager; + + @Inject + public ServiceModule(@Named("services") List services) { + serviceManager = new ServiceManager(services); + } + + @Override + public boolean lateinit() { + return true; + } + + @Override + public void install(@NotNull Jooby application) throws Exception { + final CompletableFuture initialization = new CompletableFuture<>(); + serviceManager.addListener( + new ServiceManager.Listener() { + @Override + public void healthy() { + super.healthy(); + application.getLog().info("All internal application services are up and running"); + initialization.complete(null); + } + + @Override + public void failure(@NotNull Service service) { + super.failure(service); + initialization.completeExceptionally(service.failureCause()); + } + }, + MoreExecutors.directExecutor()); + + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + try { + serviceManager.stopAsync().awaitStopped(Duration.ofSeconds(15)); + } catch (TimeoutException ex) { + application + .getLog() + .warn( + "Unable to correctly stop all the services, got the following error" + + " while stopping: " + + ex.getMessage()); + throw new RuntimeException(ex); + } + })); + + // Blocking call to wait for all the services to be up and running + serviceManager.startAsync(); + initialization.get(); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/util/UtilDI.java b/src/main/java/com/github/polpetta/mezzotre/util/UtilDI.java new file mode 100644 index 0000000..cfdb144 --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/util/UtilDI.java @@ -0,0 +1,43 @@ +package com.github.polpetta.mezzotre.util; + +import com.google.common.util.concurrent.Service; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import javax.inject.Named; +import javax.inject.Singleton; +import org.apache.commons.lang3.tuple.Pair; + +public class UtilDI extends AbstractModule { + @Provides + @Singleton + @Named("eventThreadPool") + public Executor getEventExecutor() { + return ForkJoinPool.commonPool(); + } + + @Provides + @Singleton + @Named("longJobThreadPool") + public Executor getLongJobExecutor() { + return Executors.newCachedThreadPool(); + } + + @Provides + @Singleton + @Named("serviceModule") + public ServiceModule getServiceModule(@Named("services") List moduleList) { + return new ServiceModule(moduleList); + } + + @Provides + @Singleton + @Named("serviceRunningCheckTime") + public Pair getTime() { + return Pair.of(5, TimeUnit.SECONDS); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/util/di/ThreadPool.java b/src/main/java/com/github/polpetta/mezzotre/util/di/ThreadPool.java deleted file mode 100644 index e4ddf60..0000000 --- a/src/main/java/com/github/polpetta/mezzotre/util/di/ThreadPool.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.polpetta.mezzotre.util.di; - -import com.google.inject.AbstractModule; -import com.google.inject.Provides; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ForkJoinPool; -import javax.inject.Named; -import javax.inject.Singleton; - -public class ThreadPool extends AbstractModule { - @Provides - @Singleton - @Named("eventThreadPool") - public Executor getEventExecutor() { - return ForkJoinPool.commonPool(); - } - - @Provides - @Singleton - @Named("longJobThreadPool") - public Executor getLongJobExecutor() { - return Executors.newCachedThreadPool(); - } -} diff --git a/src/main/resources/template/telegram/help.vm b/src/main/resources/template/telegram/help.vm index 7033d0c..0ff5078 100644 --- a/src/main/resources/template/telegram/help.vm +++ b/src/main/resources/template/telegram/help.vm @@ -1,8 +1,7 @@ ${i18n.help.description}: #foreach(${command} in ${commands}) -## FIXME this is not displayed correctly in telegram! -*#foreach(${key} in ${command.left}) ${key}#end: ${i18n.get(${command.right})} +-#foreach(${key} in ${command.left}) ${key}#end: ${i18n.get(${command.right})} #end ${i18n.help.buttonsToo} \ No newline at end of file diff --git a/src/main/resources/template/telegram/start.vm b/src/main/resources/template/telegram/start.vm index 7889c0b..3d55cb5 100644 --- a/src/main/resources/template/telegram/start.vm +++ b/src/main/resources/template/telegram/start.vm @@ -1,3 +1,3 @@ -**${i18n.start.helloFirstName.insert(${firstName})}** +*${i18n.start.helloFirstName.insert(${firstName})}* ${i18n.start.description.insert(${programName})} \ No newline at end of file diff --git a/src/test/java/com/github/polpetta/mezzotre/UnitTest.java b/src/test/java/com/github/polpetta/mezzotre/UnitTest.java index 9c8a6bd..69cf976 100644 --- a/src/test/java/com/github/polpetta/mezzotre/UnitTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/UnitTest.java @@ -3,15 +3,21 @@ package com.github.polpetta.mezzotre; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; -import com.google.inject.*; +import com.github.polpetta.mezzotre.util.ServiceModule; +import com.google.inject.AbstractModule; import com.google.inject.Module; +import com.google.inject.Provides; +import com.google.inject.Stage; import io.jooby.*; import io.jooby.ebean.EbeanModule; import io.jooby.flyway.FlywayModule; import io.jooby.hikari.HikariModule; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.inject.Named; +import javax.inject.Singleton; +import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.Test; public class UnitTest { @@ -37,6 +43,20 @@ public class UnitTest { public Extension getEbeanExtension() { return mock(EbeanModule.class); } + + @Provides + @Singleton + @Named("serviceModule") + public ServiceModule getServiceModule() { + return mock(ServiceModule.class); + } + + @Provides + @Singleton + @Named("serviceRunningCheckTime") + public Pair getTime() { + return Pair.of(0, TimeUnit.MILLISECONDS); + } } @Test diff --git a/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java b/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java index 147fa4d..5d6d0c0 100644 --- a/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java +++ b/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java @@ -2,18 +2,22 @@ package com.github.polpetta.mezzotre.helper; import com.github.polpetta.mezzotre.App; import com.github.polpetta.mezzotre.route.RouteDI; +import com.github.polpetta.mezzotre.util.ServiceModule; +import com.google.common.util.concurrent.Service; import com.google.inject.AbstractModule; import com.google.inject.Provides; -import com.google.inject.Singleton; import com.google.inject.Stage; import com.zaxxer.hikari.HikariConfig; import io.jooby.Extension; import io.jooby.ebean.EbeanModule; import io.jooby.flyway.FlywayModule; import io.jooby.hikari.HikariModule; +import java.util.List; import java.util.Properties; import java.util.Set; +import java.util.concurrent.TimeUnit; import javax.inject.Named; +import javax.inject.Singleton; import org.apache.commons.lang3.tuple.Pair; import org.testcontainers.containers.PostgreSQLContainer; @@ -50,6 +54,20 @@ public class IntegrationAppFactory { public Extension getEbeanExtension() { return new EbeanModule(); } + + @Singleton + @Provides + @Named("serviceModule") + public ServiceModule getServiceModule(@Named("services") List moduleList) { + return new ServiceModule(moduleList); + } + + @Provides + @Singleton + @Named("serviceRunningCheckTime") + public Pair getTime() { + return Pair.of(0, TimeUnit.MILLISECONDS); + } } public static App loadCustomDbApplication() { diff --git a/src/test/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerServiceIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerServiceIntegrationTest.java new file mode 100644 index 0000000..40a5e78 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerServiceIntegrationTest.java @@ -0,0 +1,93 @@ +package com.github.polpetta.mezzotre.orm; + +import static org.junit.jupiter.api.Assertions.*; + +import com.github.polpetta.mezzotre.helper.Loader; +import com.github.polpetta.mezzotre.helper.TestConfig; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; +import com.github.polpetta.types.json.CallbackQueryMetadata; +import io.ebean.Database; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.*; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Tag("slow") +@Tag("database") +@Tag("velocity") +@Testcontainers +class BatchBeanCleanerServiceIntegrationTest { + + @Container + private final PostgreSQLContainer postgresServer = + new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE); + + private Database database; + private BatchBeanCleanerService batchBeanCleanerService; + + @BeforeEach + void setUp() throws Exception { + database = + Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); + batchBeanCleanerService = + new BatchBeanCleanerService( + LoggerFactory.getLogger(BatchBeanCleanerService.class), + Pair.of(0, TimeUnit.MILLISECONDS)); + } + + @AfterEach + void tearDown() throws Exception { + if (batchBeanCleanerService != null) { + batchBeanCleanerService.stopAsync().awaitTerminated(Duration.ofSeconds(10)); + } + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void shouldRemoveEntryFromDatabase() throws Exception { + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext("1234", "4567", new CallbackQueryMetadata()); + callbackQueryContext.save(); + + batchBeanCleanerService.startAsync(); + batchBeanCleanerService.awaitRunning(); + + final CompletableFuture integerCompletableFuture = + batchBeanCleanerService.removeAsync("1234", new QCallbackQueryContext().id); + + final Integer gotDeletion = integerCompletableFuture.get(); + + assertEquals(1, gotDeletion); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void shouldRemoveMultipleEntriesFromDatabase() throws Exception { + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext("1234", "4567", new CallbackQueryMetadata()); + callbackQueryContext.save(); + final CallbackQueryContext callbackQueryContext2 = + new CallbackQueryContext("4321", "4567", new CallbackQueryMetadata()); + callbackQueryContext2.save(); + + batchBeanCleanerService.startAsync(); + batchBeanCleanerService.awaitRunning(); + + final CompletableFuture integerCompletableFuture = + batchBeanCleanerService.removeAsync("4567", new QCallbackQueryContext().entryGroup); + final CompletableFuture integerCompletableFuture2 = + batchBeanCleanerService.removeAsync("4567", new QCallbackQueryContext().entryGroup); + + final Integer gotDeletion1 = integerCompletableFuture.get(); + final Integer gotDeletion2 = integerCompletableFuture2.get(); + + assertEquals(2, gotDeletion1); + assertEquals(0, gotDeletion2); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilIntegrationTest.java new file mode 100644 index 0000000..0c90d6b --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilIntegrationTest.java @@ -0,0 +1,176 @@ +package com.github.polpetta.mezzotre.orm.telegram; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +import com.github.polpetta.mezzotre.helper.Loader; +import com.github.polpetta.mezzotre.helper.TestConfig; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.types.json.CallbackQueryMetadata; +import com.github.polpetta.types.json.ChatContext; +import com.google.gson.Gson; +import com.pengrad.telegrambot.model.Update; +import io.ebean.Database; +import java.util.Optional; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Tag("slow") +@Tag("database") +@Testcontainers +class ChatUtilIntegrationTest { + + private static Gson gson; + + @Container + private final PostgreSQLContainer postgresServer = + new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE); + + private Database database; + private Clock fakeClock; + private ChatUtil chatUtil; + + @BeforeAll + static void beforeAll() { + gson = new Gson(); + } + + @BeforeEach + void setUp() throws Exception { + database = + Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); + + fakeClock = mock(Clock.class); + chatUtil = new ChatUtil(fakeClock); + } + + @Test + void shouldExtractChatFromCallBackQueryContext() { + final Update update = + gson.fromJson( + "{\n" + + "\"update_id\":10000,\n" + + "\"message\":{\n" + + " \"date\":1441645532,\n" + + " \"chat\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"type\": \"private\",\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"message_id\":1365,\n" + + " \"from\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"text\":\"/help\"\n" + + "}\n" + + "}", + Update.class); + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext( + "123", + "456", + new CallbackQueryMetadata.CallbackQueryMetadataBuilder() + .withTelegramChatId(69420L) + .build()); + + final TgChat expectedChat = new TgChat(69420L, new ChatContext()); + expectedChat.save(); + + final Optional tgChatOptional = + chatUtil.extractChat( + callbackQueryContext, null); // null just for test purposes, not usually expected + + assertEquals(expectedChat, tgChatOptional.get()); + } + + @Test + void shouldExtractChatFromUpdateIfCallbackQueryContextIsEmpty() { + final Update update = + gson.fromJson( + "{\n" + + " \"update_id\": 158712614,\n" + + " \"callback_query\": {\n" + + " \"id\": \"20496049451114620\",\n" + + " \"from\": {\n" + + " \"id\": 1111111,\n" + + " \"is_bot\": false,\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\",\n" + + " \"username\": \"Testusername\",\n" + + " \"language_code\": \"en\"\n" + + " },\n" + + " \"message\": {\n" + + " \"message_id\": 69420,\n" + + " \"from\": {\n" + + " \"id\": 244745330,\n" + + " \"is_bot\": true,\n" + + " \"first_name\": \"Dev - DavideBot\",\n" + + " \"username\": \"devdavidebot\"\n" + + " },\n" + + " \"date\": 1681218838,\n" + + " \"chat\": {\n" + + " \"id\": 1111111,\n" + + " \"type\": \"private\",\n" + + " \"username\": \"Testusername\",\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\"\n" + + " },\n" + + " \"text\": \"Hello xxxxxx! \uD83D\uDC4B\\n" + + "\\n" + + "This is Mezzotre, a simple bot focused on DnD content management! Please start" + + " by choosing a language down below \uD83D\uDC47\",\n" + + " \"entities\": [\n" + + " {\n" + + " \"type\": \"bold\",\n" + + " \"offset\": 0,\n" + + " \"length\": 16\n" + + " },\n" + + " {\n" + + " \"type\": \"italic\",\n" + + " \"offset\": 26,\n" + + " \"length\": 8\n" + + " }\n" + + " ],\n" + + " \"reply_markup\": {\n" + + " \"inline_keyboard\": [\n" + + " [\n" + + " {\n" + + " \"text\": \"English\",\n" + + " \"callback_data\": \"9a64be11-d086-4bd9-859f-720c43dedcb5\"\n" + + " },\n" + + " {\n" + + " \"text\": \"Italian\",\n" + + " \"callback_data\": \"8768d660-f05f-4f4b-bda5-3451ab573d56\"\n" + + " }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + "}", + Update.class); + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext("123", "456", new CallbackQueryMetadata()); + + final TgChat expectedChat = new TgChat(69420L, new ChatContext()); + expectedChat.save(); + + final Optional tgChatOptional = + chatUtil.extractChat( + callbackQueryContext, update); // null just for test purposes, not usually expected + + assertEquals(expectedChat, tgChatOptional.get()); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilTest.java b/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilTest.java new file mode 100644 index 0000000..7de3fc5 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilTest.java @@ -0,0 +1,70 @@ +package com.github.polpetta.mezzotre.orm.telegram; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.types.json.ChatContext; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.mockito.ArgumentCaptor; + +@Execution(ExecutionMode.CONCURRENT) +class ChatUtilTest { + private Clock fakeClock; + private ChatUtil chatUtil; + + @BeforeEach + void setUp() { + fakeClock = mock(Clock.class); + chatUtil = new ChatUtil(fakeClock); + } + + @Test + void shouldUpdateChatContext() { + final TgChat fakeTgChat = mock(TgChat.class); + final ChatContext chatContext = new ChatContext(); + when(fakeTgChat.getChatContext()).thenReturn(chatContext); + when(fakeClock.now()).thenReturn(69420L); + chatUtil.updateChatContext(fakeTgChat, "/test1", 42, Collections.emptyMap()); + + verify(fakeTgChat, times(1)).getChatContext(); + final ArgumentCaptor chatContextArgumentCaptor = + ArgumentCaptor.forClass(ChatContext.class); + verify(fakeTgChat, times(1)).setChatContext(chatContextArgumentCaptor.capture()); + verify(fakeTgChat, times(0)).setHasHelpBeenShown(eq(true)); + final ChatContext capturedChatContextValue = chatContextArgumentCaptor.getValue(); + assertEquals("/test1", capturedChatContextValue.getStage()); + assertEquals(42, capturedChatContextValue.getStep()); + assertEquals(69420L, capturedChatContextValue.getPreviousMessageUnixTimestampInSeconds()); + } + + @Test + void shouldAddMapElementsToChatContextToo() { + final TgChat fakeTgChat = mock(TgChat.class); + final ChatContext chatContext = new ChatContext(); + when(fakeTgChat.getChatContext()).thenReturn(chatContext); + when(fakeClock.now()).thenReturn(69420L); + + final Object obj1 = new Object(); + final Object obj2 = new Object(); + + chatUtil.updateChatContext(fakeTgChat, "/test1", 42, Map.of("field1", obj1, "field2", obj2)); + + verify(fakeTgChat, times(1)).getChatContext(); + final ArgumentCaptor chatContextArgumentCaptor = + ArgumentCaptor.forClass(ChatContext.class); + verify(fakeTgChat, times(1)).setChatContext(chatContextArgumentCaptor.capture()); + final ChatContext capturedChatContextValue = chatContextArgumentCaptor.getValue(); + final Map gotAdditionalProperties = + capturedChatContextValue.getAdditionalProperties(); + assertEquals(2, gotAdditionalProperties.size()); + assertEquals(obj1, gotAdditionalProperties.get("field1")); + assertEquals(obj2, gotAdditionalProperties.get("field2")); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundTest.java new file mode 100644 index 0000000..eca4652 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundTest.java @@ -0,0 +1,46 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.BaseRequest; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.slf4j.LoggerFactory; + +@Execution(ExecutionMode.CONCURRENT) +class NotFoundTest { + + private CallbackQueryContextCleaner fakeCallbackQueryCleaner; + private NotFound notFound; + + @BeforeEach + void setUp() { + + fakeCallbackQueryCleaner = mock(CallbackQueryContextCleaner.class); + + notFound = + new NotFound(LoggerFactory.getLogger(NotFound.class), fakeCallbackQueryCleaner, "anEvent"); + } + + @Test + void shouldCallCallbackQueryContextCleaner() throws Exception { + final CallbackQueryContext fakeCallbackQuery = mock(CallbackQueryContext.class); + when(fakeCallbackQuery.getId()).thenReturn("anId"); + when(fakeCallbackQuery.getEntryGroup()).thenReturn("123"); + final Update fakeUpdate = mock(Update.class); + + final CompletableFuture>> gotResult = + notFound.process(fakeCallbackQuery, fakeUpdate); + + verify(fakeCallbackQueryCleaner, times(1)).removeGroupAsync(eq("123")); + assertEquals(Optional.empty(), gotResult.get()); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java index c28e6cc..3a78cb3 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java @@ -7,10 +7,14 @@ import com.github.polpetta.mezzotre.helper.Loader; import com.github.polpetta.mezzotre.helper.TestConfig; import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; +import com.github.polpetta.mezzotre.orm.BatchBeanCleanerService; +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QTgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; +import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.CallbackQueryMetadata; import com.github.polpetta.types.json.ChatContext; @@ -22,12 +26,17 @@ import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.EditMessageText; import com.pengrad.telegrambot.request.SendMessage; import io.ebean.Database; +import io.ebean.typequery.PString; +import io.ebean.typequery.TQRootBean; +import io.vavr.Tuple3; +import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -52,6 +61,8 @@ class SelectLanguageTutorialIntegrationTest { private Database database; private SelectLanguageTutorial selectLanguageTutorial; private UUIDGenerator fakeUUIDGenerator; + private ChatUtil chatUtil; + private BatchBeanCleanerService batchBeanCleanerService; @BeforeAll static void beforeAll() { @@ -64,6 +75,12 @@ class SelectLanguageTutorialIntegrationTest { Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); fakeUUIDGenerator = mock(UUIDGenerator.class); + chatUtil = new ChatUtil(new Clock()); + batchBeanCleanerService = + new BatchBeanCleanerService( + LoggerFactory.getLogger(BatchBeanCleanerService.class), + Pair.of(0, TimeUnit.MILLISECONDS)); + batchBeanCleanerService.startAsync().awaitRunning(Duration.ofSeconds(10)); selectLanguageTutorial = new SelectLanguageTutorial( @@ -71,7 +88,16 @@ class SelectLanguageTutorialIntegrationTest { new TemplateContentGenerator( new LocalizedMessageFactory(Loader.defaultVelocityEngine())), LoggerFactory.getLogger(SelectLanguageTutorial.class), - fakeUUIDGenerator); + fakeUUIDGenerator, + chatUtil, + new CallbackQueryContextCleaner( + batchBeanCleanerService, + LoggerFactory.getLogger(CallbackQueryContextCleaner.class))); + } + + @AfterEach + void tearDown() throws Exception { + batchBeanCleanerService.stopAsync().awaitTerminated(Duration.ofSeconds(10)); } private static Stream getTestLocales() { @@ -193,6 +219,11 @@ class SelectLanguageTutorialIntegrationTest { "c018108f-6612-4848-8fca-cf301460d4eb", entryGroupId, callbackQueryMetadata); changeLanguageCallbackQueryContext.save(); + final CompletableFuture< + Tuple3>, CompletableFuture>> + callBackFuture = new CompletableFuture<>(); + batchBeanCleanerService.addListener(callBackFuture::complete); + final CompletableFuture>> processFuture = this.selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update); final Optional> gotResponseOpt = processFuture.get(); @@ -213,6 +244,7 @@ class SelectLanguageTutorialIntegrationTest { assertEquals( 1, new QCallbackQueryContext().id.eq("e86e6fa1-fdd4-4120-b85d-a5482db2e8b5").findCount()); } else { + callBackFuture.get(); // Await that callback are cleaned out first assertEquals(0, keyboardButtons.size()); assertEquals(0, new QCallbackQueryContext().findCount()); } @@ -221,6 +253,11 @@ class SelectLanguageTutorialIntegrationTest { assertNotNull(retrievedTgChat); assertEquals(selectLanguageTutorial.getLocale(), retrievedTgChat.getLocale()); + final Tuple3>, CompletableFuture> + deletionCallback = callBackFuture.get(); + + assertEquals(entryGroupId, deletionCallback._1()); + assertEquals(1, deletionCallback._3().get()); assertEquals(0, new QCallbackQueryContext().entryGroup.eq(entryGroupId).findCount()); } @@ -308,6 +345,10 @@ class SelectLanguageTutorialIntegrationTest { new CallbackQueryContext( "c018108f-6612-4848-8fca-cf301460d4eb", entryGroupId, callbackQueryMetadata); changeLanguageCallbackQueryContext.save(); + final CompletableFuture< + Tuple3>, CompletableFuture>> + callBackFuture = new CompletableFuture<>(); + batchBeanCleanerService.addListener(callBackFuture::complete); final CompletableFuture>> processFuture = this.selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update); @@ -329,6 +370,7 @@ class SelectLanguageTutorialIntegrationTest { assertEquals( 1, new QCallbackQueryContext().id.eq("e86e6fa1-fdd4-4120-b85d-a5482db2e8b5").findCount()); } else { + callBackFuture.get(); // Await that callback are cleaned out first assertEquals(0, keyboardButtons.size()); assertEquals(0, new QCallbackQueryContext().findCount()); } @@ -337,6 +379,11 @@ class SelectLanguageTutorialIntegrationTest { assertNotNull(retrievedTgChat); assertEquals(selectLanguageTutorial.getLocale(), retrievedTgChat.getLocale()); + final Tuple3>, CompletableFuture> + deletionCallback = callBackFuture.get(); + + assertEquals(entryGroupId, deletionCallback._1()); + assertEquals(1, deletionCallback._3().get()); assertEquals(0, new QCallbackQueryContext().entryGroup.eq(entryGroupId).findCount()); } } diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpIntegrationTest.java new file mode 100644 index 0000000..eba6430 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpIntegrationTest.java @@ -0,0 +1,457 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.github.polpetta.mezzotre.helper.Loader; +import com.github.polpetta.mezzotre.helper.TestConfig; +import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; +import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; +import com.github.polpetta.mezzotre.orm.BatchBeanCleanerService; +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.query.QTgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; +import com.github.polpetta.mezzotre.telegram.model.Help; +import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.mezzotre.util.UUIDGenerator; +import com.github.polpetta.types.json.CallbackQueryMetadata; +import com.github.polpetta.types.json.ChatContext; +import com.google.gson.Gson; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.request.BaseRequest; +import com.pengrad.telegrambot.request.EditMessageText; +import com.pengrad.telegrambot.request.SendMessage; +import io.ebean.Database; +import io.ebean.typequery.PString; +import io.ebean.typequery.TQRootBean; +import io.vavr.Tuple3; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Stream; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.velocity.app.VelocityEngine; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Tag("slow") +@Tag("database") +@Tag("velocity") +@Testcontainers +class ShowHelpIntegrationTest { + + private static Gson gson; + private static Logger testLog; + + @Container + private final PostgreSQLContainer postgresServer = + new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE); + + private Database database; + private VelocityEngine velocityEngine; + private UUIDGenerator fakeUUIDGenerator; + private Clock fakeClock; + private BatchBeanCleanerService batchBeanCleanerService; + + @BeforeAll + static void beforeAll() { + gson = new Gson(); + testLog = LoggerFactory.getLogger(ShowHelpIntegrationTest.class); + } + + @BeforeEach + void setUp() throws Exception { + database = + Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); + velocityEngine = Loader.defaultVelocityEngine(); + + final Logger log = LoggerFactory.getLogger(ShowHelp.class); + fakeUUIDGenerator = mock(UUIDGenerator.class); + fakeClock = mock(Clock.class); + } + + @AfterEach + void tearDown() throws Exception { + if (batchBeanCleanerService != null) { + batchBeanCleanerService.stopAsync().awaitTerminated(Duration.ofSeconds(10)); + } + } + + private ShowHelp generateShowHelpWithCustomCommands( + Map eventProcessors, + Map commandProcessors) + throws TimeoutException { + + batchBeanCleanerService = + new BatchBeanCleanerService( + LoggerFactory.getLogger(CallbackQueryContextCleaner.class), + Pair.of(0, TimeUnit.MILLISECONDS)); + batchBeanCleanerService.startAsync().awaitRunning(Duration.ofSeconds(10)); + + return new ShowHelp( + Executors.newSingleThreadExecutor(), + new Help( + new TemplateContentGenerator(new LocalizedMessageFactory(velocityEngine)), + fakeUUIDGenerator), + commandProcessors, + eventProcessors, + // We need to implement a service that is sync - no thread creation! + new ChatUtil(fakeClock), + new CallbackQueryContextCleaner( + batchBeanCleanerService, LoggerFactory.getLogger(CallbackQueryContextCleaner.class))); + } + + public static Stream getMessageIdOrNot() { + return Stream.of( + Arguments.of( + gson.fromJson( + "{\n" + + " \"update_id\": 158712614,\n" + + " \"callback_query\": {\n" + + " \"id\": \"20496049451114620\",\n" + + " \"from\": {\n" + + " \"id\": 1111111,\n" + + " \"is_bot\": false,\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\",\n" + + " \"username\": \"Testusername\",\n" + + " \"language_code\": \"en\"\n" + + " },\n" + + " \"message\": {\n" + + " \"message_id\": 2723,\n" + + " \"from\": {\n" + + " \"id\": 244745330,\n" + + " \"is_bot\": true,\n" + + " \"first_name\": \"Dev - DavideBot\",\n" + + " \"username\": \"devdavidebot\"\n" + + " },\n" + + " \"date\": 1681218838,\n" + + " \"chat\": {\n" + + " \"id\": 1111111,\n" + + " \"type\": \"private\",\n" + + " \"username\": \"Testusername\",\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\"\n" + + " },\n" + + " \"text\": \"a message\",\n" + + " \"reply_markup\": {\n" + + " \"inline_keyboard\": [\n" + + " [\n" + + " {\n" + + " \"text\": \"English\",\n" + + " \"callback_data\": \"9a64be11-d086-4bd9-859f-720c43dedcb5\"\n" + + " },\n" + + " {\n" + + " \"text\": \"Italian\",\n" + + " \"callback_data\": \"8768d660-f05f-4f4b-bda5-3451ab573d56\"\n" + + " }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + "}", + Update.class), + EditMessageText.class), + Arguments.of( + gson.fromJson( + "{\n" + + " \"update_id\": 158712614,\n" + + " \"callback_query\": {\n" + + " \"id\": \"20496049451114620\",\n" + + " \"from\": {\n" + + " \"id\": 1111111,\n" + + " \"is_bot\": false,\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\",\n" + + " \"username\": \"Testusername\",\n" + + " \"language_code\": \"en\"\n" + + " }\n" + + " }\n" + + "}", + Update.class), + SendMessage.class)); + } + + @ParameterizedTest + @Timeout(value = 1, unit = TimeUnit.MINUTES) + @MethodSource("getMessageIdOrNot") + void shouldShowHelpMessageWithButtons(Update update, Class typeOfMessage) throws Exception { + // we have 2 events + group uuid + when(fakeUUIDGenerator.generateAsString()) + .thenReturn("e86e6fa1-fdd4-4120-b85d-a5482db2e8b5") + .thenReturn("16507fbd-9f28-48a8-9de1-3ea1c943af67") + .thenReturn("0b0ac18e-f621-484e-aa8d-9b176be5b930"); + when(fakeClock.now()).thenReturn(42L); + + final HashMap commands = + new HashMap<>(); + final com.github.polpetta.mezzotre.telegram.command.Processor dummy1 = + new com.github.polpetta.mezzotre.telegram.command.Processor() { + @Override + public Set getTriggerKeywords() { + return Set.of("/a", "/b"); + } + + @Override + public CompletableFuture>> process( + TgChat chat, Update update) { + return null; + } + + @Override + public String getLocaleDescriptionKeyword() { + return "start.cmdDescription"; + } + }; + final com.github.polpetta.mezzotre.telegram.command.Processor dummy2 = + new com.github.polpetta.mezzotre.telegram.command.Processor() { + @Override + public Set getTriggerKeywords() { + return Set.of("/different"); + } + + @Override + public CompletableFuture>> process( + TgChat chat, Update update) { + return null; + } + + @Override + public String getLocaleDescriptionKeyword() { + return "help.cmdDescription"; + } + }; + commands.put("/a", dummy1); + commands.put("/b", dummy1); + commands.put("/different", dummy2); + + final Map events = new HashMap<>(); + + final Processor dummyEvent1 = + new Processor() { + @Override + public String getEventName() { + return "exampleEvent"; + } + + @Override + public boolean canBeDirectlyInvokedByTheUser() { + return true; + } + + @Override + public Optional getPrettyPrintLocaleKeyName() { + return Optional.of("changeLanguage.inlineKeyboardButtonName"); + } + + @Override + public CompletableFuture>> process( + CallbackQueryContext callbackQueryContext, Update update) { + return null; + } + }; + + final Processor dummyEvent2 = + new com.github.polpetta.mezzotre.telegram.callbackquery.Processor() { + @Override + public String getEventName() { + return "secondExampleEvent"; + } + + @Override + public boolean canBeDirectlyInvokedByTheUser() { + return true; + } + + @Override + public Optional getPrettyPrintLocaleKeyName() { + return Optional.of("selectLanguageTutorial.inlineKeyboardButtonName"); + } + + @Override + public CompletableFuture>> process( + CallbackQueryContext callbackQueryContext, Update update) { + return null; + } + }; + + events.put(dummyEvent1.getEventName(), dummyEvent1); + events.put(dummyEvent2.getEventName(), dummyEvent2); + + final ShowHelp showHelp = generateShowHelpWithCustomCommands(events, commands); + final TgChat tgChat = new TgChat(1111111L, new ChatContext()); + tgChat.save(); + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext( + "1234", + "5678", + new CallbackQueryMetadata.CallbackQueryMetadataBuilder() + .withTelegramChatId(1111111L) + .build()); + callbackQueryContext.save(); + + final CompletableFuture>> gotResultFuture = + showHelp.process(callbackQueryContext, update); + + final Optional> baseRequestOptional = gotResultFuture.get(); + final BaseRequest gotResponse = baseRequestOptional.get(); + assertInstanceOf(typeOfMessage, gotResponse); + final String message = (String) gotResponse.getParameters().get("text"); + assertEquals( + "Here is a list of what I can do:\n" + + "\n" + + "- /a /b: Trigger this very bot\n" + + "- /different: Print the help message\n" + + "\n" + + "You can do the same operations you'd do with the commands aforementioned by" + + " selecting the corresponding button below \uD83D\uDC47", + message); + final InlineKeyboardButton[][] keyboard = + ((InlineKeyboardMarkup) + gotResponse + .getParameters() + .getOrDefault("reply_markup", new InlineKeyboardMarkup())) + .inlineKeyboard(); + + final List keyboardButtons = + Stream.of(keyboard).flatMap(Stream::of).toList(); + + assertEquals(2, keyboardButtons.size()); + + assertFalse(keyboardButtons.get(0).callbackData().isBlank()); + assertFalse(keyboardButtons.get(1).callbackData().isBlank()); + + assertTrue( + keyboardButtons.stream() + .map(InlineKeyboardButton::text) + .anyMatch("Select language"::equals)); + + assertTrue( + keyboardButtons.stream() + .map(InlineKeyboardButton::text) + .anyMatch("Change language"::equals)); + + final TgChat gotChat = new QTgChat().id.eq(1111111L).findOne(); + assertNotNull(gotChat); + assertTrue(gotChat.getHasHelpBeenShown()); + final ChatContext gotChatChatContext = gotChat.getChatContext(); + assertEquals(0, gotChatChatContext.getStep()); + assertEquals("showHelp", gotChatChatContext.getStage()); + assertEquals(42, gotChatChatContext.getPreviousMessageUnixTimestampInSeconds()); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void shouldCleanUpCallbackQueryContextAfterRun() throws Exception { + when(fakeClock.now()).thenReturn(42L); + + final ShowHelp showHelp = + generateShowHelpWithCustomCommands(Collections.emptyMap(), Collections.emptyMap()); + final TgChat tgChat = new TgChat(1111111L, new ChatContext()); + tgChat.save(); + final Update update = + gson.fromJson( + "{\n" + + " \"update_id\": 158712614,\n" + + " \"callback_query\": {\n" + + " \"id\": \"20496049451114620\",\n" + + " \"from\": {\n" + + " \"id\": 1111111,\n" + + " \"is_bot\": false,\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\",\n" + + " \"username\": \"Testusername\",\n" + + " \"language_code\": \"en\"\n" + + " },\n" + + " \"message\": {\n" + + " \"message_id\": 2723,\n" + + " \"from\": {\n" + + " \"id\": 244745330,\n" + + " \"is_bot\": true,\n" + + " \"first_name\": \"Dev - DavideBot\",\n" + + " \"username\": \"devdavidebot\"\n" + + " },\n" + + " \"date\": 1681218838,\n" + + " \"chat\": {\n" + + " \"id\": 1111111,\n" + + " \"type\": \"private\",\n" + + " \"username\": \"Testusername\",\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\"\n" + + " },\n" + + " \"text\": \"Hello xxxxxx! \uD83D\uDC4B\\n" + + "\\n" + + "This is Mezzotre, a simple bot focused on DnD content management! Please start" + + " by choosing a language down below \uD83D\uDC47\",\n" + + " \"entities\": [\n" + + " {\n" + + " \"type\": \"bold\",\n" + + " \"offset\": 0,\n" + + " \"length\": 16\n" + + " },\n" + + " {\n" + + " \"type\": \"italic\",\n" + + " \"offset\": 26,\n" + + " \"length\": 8\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + "}", + Update.class); + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext( + "1234", + "5678", + new CallbackQueryMetadata.CallbackQueryMetadataBuilder() + .withTelegramChatId(1111111L) + .build()); + callbackQueryContext.save(); + // Create another CallbackQuery context of the same group + final CallbackQueryContext callbackQueryContext2 = + new CallbackQueryContext( + "7891", + "5678", + new CallbackQueryMetadata.CallbackQueryMetadataBuilder() + .withTelegramChatId(1111111L) + .build()); + callbackQueryContext2.save(); + + // I know I know, a future containing a future...but it is for testing purposes, otherwise we'd + // have to do some while/sleep thread hack which I find horrible to do + final CompletableFuture< + Tuple3>, CompletableFuture>> + callBackFuture = new CompletableFuture<>(); + batchBeanCleanerService.addListener(callBackFuture::complete); + + final CompletableFuture>> gotResultFuture = + showHelp.process(callbackQueryContext, update); + + final Optional> baseRequestOptional = gotResultFuture.get(); + assertDoesNotThrow(baseRequestOptional::get); + + final Tuple3>, CompletableFuture> + deletionCallback = callBackFuture.get(); + + assertEquals("5678", deletionCallback._1()); + assertEquals(2, deletionCallback._3().get()); + assertEquals( + 0, + new QCallbackQueryContext().findCount(), + "The CallbackQuery context group has not been cleaned properly!"); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpTest.java new file mode 100644 index 0000000..a722059 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpTest.java @@ -0,0 +1,314 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.*; + +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; +import com.github.polpetta.mezzotre.telegram.model.Help; +import com.github.polpetta.types.json.ChatContext; +import com.google.gson.Gson; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.request.BaseRequest; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@Execution(ExecutionMode.CONCURRENT) +class ShowHelpTest { + + private static Gson gson; + private ShowHelp showHelp; + private Help fakeModelHelp; + private ChatUtil fakeChatUtil; + + @BeforeAll + static void beforeAll() { + gson = new Gson(); + } + + @BeforeEach + void setUp() { + + fakeModelHelp = mock(Help.class); + fakeChatUtil = mock(ChatUtil.class); + final CallbackQueryContextCleaner fakeCallbackQueryContextCleaner = + mock(CallbackQueryContextCleaner.class); + + showHelp = + new ShowHelp( + Executors.newSingleThreadExecutor(), + fakeModelHelp, + Collections.emptyMap(), + Collections.emptyMap(), + fakeChatUtil, + fakeCallbackQueryContextCleaner); + } + + @Test + void shouldUpdateChatContext() throws Exception { + final CallbackQueryContext fakeCallbackQueryContext = mock(CallbackQueryContext.class); + final Update update = + gson.fromJson( + "{\n" + + " \"update_id\": 158712614,\n" + + " \"callback_query\": {\n" + + " \"id\": \"20496049451114620\",\n" + + " \"from\": {\n" + + " \"id\": 1111111,\n" + + " \"is_bot\": false,\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\",\n" + + " \"username\": \"Testusername\",\n" + + " \"language_code\": \"en\"\n" + + " },\n" + + " \"message\": {\n" + + " \"message_id\": 2723,\n" + + " \"from\": {\n" + + " \"id\": 244745330,\n" + + " \"is_bot\": true,\n" + + " \"first_name\": \"Dev - DavideBot\",\n" + + " \"username\": \"devdavidebot\"\n" + + " },\n" + + " \"date\": 1681218838,\n" + + " \"chat\": {\n" + + " \"id\": 1111111,\n" + + " \"type\": \"private\",\n" + + " \"username\": \"Testusername\",\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\"\n" + + " },\n" + + " \"text\": \"Hello xxxxxx! \uD83D\uDC4B\\n" + + "\\n" + + "This is Mezzotre, a simple bot focused on DnD content management! Please start" + + " by choosing a language down below \uD83D\uDC47\",\n" + + " \"entities\": [\n" + + " {\n" + + " \"type\": \"bold\",\n" + + " \"offset\": 0,\n" + + " \"length\": 16\n" + + " },\n" + + " {\n" + + " \"type\": \"italic\",\n" + + " \"offset\": 26,\n" + + " \"length\": 8\n" + + " }\n" + + " ],\n" + + " \"reply_markup\": {\n" + + " \"inline_keyboard\": [\n" + + " [\n" + + " {\n" + + " \"text\": \"English\",\n" + + " \"callback_data\": \"9a64be11-d086-4bd9-859f-720c43dedcb5\"\n" + + " },\n" + + " {\n" + + " \"text\": \"Italian\",\n" + + " \"callback_data\": \"8768d660-f05f-4f4b-bda5-3451ab573d56\"\n" + + " }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + "}", + Update.class); + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), any())).thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {}); + when(fakeChatUtil.extractChat(eq(fakeCallbackQueryContext), eq(update))) + .thenReturn(Optional.of(fakeTgChat)); + + final CompletableFuture>> gotResponseFuture = + showHelp.process(fakeCallbackQueryContext, update); + final Optional> gotResponseOpt = gotResponseFuture.get(); + + verify(fakeChatUtil, times(1)) + .updateChatContext(eq(fakeTgChat), eq("showHelp"), eq(0), eq(Collections.emptyMap())); + } + + @Test + void shouldAddInlineKeyboardIfButtonsAreReturned() throws Exception { + final CallbackQueryContext fakeCallbackQueryContext = mock(CallbackQueryContext.class); + final Update update = + gson.fromJson( + "{\n" + + " \"update_id\": 158712614,\n" + + " \"callback_query\": {\n" + + " \"id\": \"20496049451114620\",\n" + + " \"from\": {\n" + + " \"id\": 1111111,\n" + + " \"is_bot\": false,\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\",\n" + + " \"username\": \"Testusername\",\n" + + " \"language_code\": \"en\"\n" + + " },\n" + + " \"message\": {\n" + + " \"message_id\": 2723,\n" + + " \"from\": {\n" + + " \"id\": 244745330,\n" + + " \"is_bot\": true,\n" + + " \"first_name\": \"Dev - DavideBot\",\n" + + " \"username\": \"devdavidebot\"\n" + + " },\n" + + " \"date\": 1681218838,\n" + + " \"chat\": {\n" + + " \"id\": 1111111,\n" + + " \"type\": \"private\",\n" + + " \"username\": \"Testusername\",\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\"\n" + + " },\n" + + " \"text\": \"Hello xxxxxx! \uD83D\uDC4B\\n" + + "\\n" + + "This is Mezzotre, a simple bot focused on DnD content management! Please start" + + " by choosing a language down below \uD83D\uDC47\",\n" + + " \"entities\": [\n" + + " {\n" + + " \"type\": \"bold\",\n" + + " \"offset\": 0,\n" + + " \"length\": 16\n" + + " },\n" + + " {\n" + + " \"type\": \"italic\",\n" + + " \"offset\": 26,\n" + + " \"length\": 8\n" + + " }\n" + + " ],\n" + + " \"reply_markup\": {\n" + + " \"inline_keyboard\": [\n" + + " [\n" + + " {\n" + + " \"text\": \"English\",\n" + + " \"callback_data\": \"9a64be11-d086-4bd9-859f-720c43dedcb5\"\n" + + " },\n" + + " {\n" + + " \"text\": \"Italian\",\n" + + " \"callback_data\": \"8768d660-f05f-4f4b-bda5-3451ab573d56\"\n" + + " }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + "}", + Update.class); + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), any())).thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {new InlineKeyboardButton("example1")}); + when(fakeChatUtil.extractChat(eq(fakeCallbackQueryContext), eq(update))) + .thenReturn(Optional.of(fakeTgChat)); + + final CompletableFuture>> gotResponseFuture = + showHelp.process(fakeCallbackQueryContext, update); + final Optional> gotResponseOpt = gotResponseFuture.get(); + final BaseRequest gotResponse = gotResponseOpt.get(); + + final InlineKeyboardMarkup replyMarkup = + (InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup"); + assertEquals(1, replyMarkup.inlineKeyboard()[0].length); + assertEquals("example1", replyMarkup.inlineKeyboard()[0][0].text()); + } + + @Test + void shouldNotAddAnyKeyboardIfThereAreNoButtons() throws Exception { + final CallbackQueryContext fakeCallbackQueryContext = mock(CallbackQueryContext.class); + final Update update = + gson.fromJson( + "{\n" + + " \"update_id\": 158712614,\n" + + " \"callback_query\": {\n" + + " \"id\": \"20496049451114620\",\n" + + " \"from\": {\n" + + " \"id\": 1111111,\n" + + " \"is_bot\": false,\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\",\n" + + " \"username\": \"Testusername\",\n" + + " \"language_code\": \"en\"\n" + + " },\n" + + " \"message\": {\n" + + " \"message_id\": 2723,\n" + + " \"from\": {\n" + + " \"id\": 244745330,\n" + + " \"is_bot\": true,\n" + + " \"first_name\": \"Dev - DavideBot\",\n" + + " \"username\": \"devdavidebot\"\n" + + " },\n" + + " \"date\": 1681218838,\n" + + " \"chat\": {\n" + + " \"id\": 1111111,\n" + + " \"type\": \"private\",\n" + + " \"username\": \"Testusername\",\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\"\n" + + " },\n" + + " \"text\": \"Hello xxxxxx! \uD83D\uDC4B\\n" + + "\\n" + + "This is Mezzotre, a simple bot focused on DnD content management! Please start" + + " by choosing a language down below \uD83D\uDC47\",\n" + + " \"entities\": [\n" + + " {\n" + + " \"type\": \"bold\",\n" + + " \"offset\": 0,\n" + + " \"length\": 16\n" + + " },\n" + + " {\n" + + " \"type\": \"italic\",\n" + + " \"offset\": 26,\n" + + " \"length\": 8\n" + + " }\n" + + " ],\n" + + " \"reply_markup\": {\n" + + " \"inline_keyboard\": [\n" + + " [\n" + + " {\n" + + " \"text\": \"English\",\n" + + " \"callback_data\": \"9a64be11-d086-4bd9-859f-720c43dedcb5\"\n" + + " },\n" + + " {\n" + + " \"text\": \"Italian\",\n" + + " \"callback_data\": \"8768d660-f05f-4f4b-bda5-3451ab573d56\"\n" + + " }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + "}", + Update.class); + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), any())).thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {}); + when(fakeChatUtil.extractChat(eq(fakeCallbackQueryContext), eq(update))) + .thenReturn(Optional.of(fakeTgChat)); + + final CompletableFuture>> gotResponseFuture = + showHelp.process(fakeCallbackQueryContext, update); + final Optional> gotResponseOpt = gotResponseFuture.get(); + final BaseRequest gotResponse = gotResponseOpt.get(); + final InlineKeyboardMarkup replyMarkup = + (InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup"); + assertNull(replyMarkup); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java index 3f472aa..24453e2 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java @@ -11,6 +11,7 @@ import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.orm.model.query.QTgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.ChatContext; @@ -172,10 +173,15 @@ class HelpIntegrationTest { final com.github.polpetta.mezzotre.telegram.model.Help modelHelp = new com.github.polpetta.mezzotre.telegram.model.Help( new TemplateContentGenerator(new LocalizedMessageFactory(velocityEngine)), - fakeClock, new UUIDGenerator()); - help = new Help(Executors.newSingleThreadExecutor(), commands, events, modelHelp); + help = + new Help( + Executors.newSingleThreadExecutor(), + commands, + events, + modelHelp, + new ChatUtil(fakeClock)); final Update update = gson.fromJson( @@ -210,8 +216,8 @@ class HelpIntegrationTest { assertEquals( "Here is a list of what I can do:\n" + "\n" - + "* /a /b: Trigger this very bot\n" - + "* /different: Print the help message\n" + + "- /a /b: Trigger this very bot\n" + + "- /different: Print the help message\n" + "\n" + "You can do the same operations you'd do with the commands aforementioned by" + " selecting the corresponding button below \uD83D\uDC47", diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpTest.java new file mode 100644 index 0000000..7fd6de6 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpTest.java @@ -0,0 +1,186 @@ +package com.github.polpetta.mezzotre.telegram.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; +import com.github.polpetta.types.json.ChatContext; +import com.google.gson.Gson; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.request.BaseRequest; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@Execution(ExecutionMode.CONCURRENT) +class HelpTest { + + private static Gson gson; + private com.github.polpetta.mezzotre.telegram.model.Help fakeModelHelp; + private ChatUtil fakeChatUtil; + private Help help; + + @BeforeAll + static void beforeAll() { + gson = new Gson(); + } + + @BeforeEach + void setUp() { + + final Map commands = Collections.emptyMap(); + final Map events = + Collections.emptyMap(); + + fakeModelHelp = mock(com.github.polpetta.mezzotre.telegram.model.Help.class); + fakeChatUtil = mock(ChatUtil.class); + + help = + new Help( + Executors.newSingleThreadExecutor(), commands, events, fakeModelHelp, fakeChatUtil); + } + + @Test + void shouldUpdateChatContext() throws Exception { + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getLocale()).thenReturn("en-US"); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {}); + + final Update update = + gson.fromJson( + "{\n" + + "\"update_id\":10000,\n" + + "\"message\":{\n" + + " \"date\":1441645532,\n" + + " \"chat\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"type\": \"private\",\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"message_id\":1365,\n" + + " \"from\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"text\":\"/help\"\n" + + "}\n" + + "}", + Update.class); + + final CompletableFuture>> gotResponseFuture = + help.process(fakeTgChat, update); + assertDoesNotThrow(() -> gotResponseFuture.get()); + verify(fakeChatUtil, times(1)) + .updateChatContext(eq(fakeTgChat), eq("/help"), eq(0), eq(Collections.emptyMap())); + } + + @Test + void shouldAppendKeyboardIfButtonsAreReturned() throws Exception { + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getLocale()).thenReturn("en-US"); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {new InlineKeyboardButton("example1")}); + + final Update update = + gson.fromJson( + "{\n" + + "\"update_id\":10000,\n" + + "\"message\":{\n" + + " \"date\":1441645532,\n" + + " \"chat\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"type\": \"private\",\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"message_id\":1365,\n" + + " \"from\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"text\":\"/help\"\n" + + "}\n" + + "}", + Update.class); + + final CompletableFuture>> gotResponseFuture = + help.process(fakeTgChat, update); + final Optional> gotResponseOpt = gotResponseFuture.get(); + final BaseRequest gotResponse = gotResponseOpt.get(); + final InlineKeyboardMarkup replyMarkup = + (InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup"); + assertEquals(1, replyMarkup.inlineKeyboard()[0].length); + assertEquals("example1", replyMarkup.inlineKeyboard()[0][0].text()); + } + + @Test + void shouldNotAppendKeyboardIfNoButtonIsPresent() throws Exception { + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getLocale()).thenReturn("en-US"); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {}); + + final Update update = + gson.fromJson( + "{\n" + + "\"update_id\":10000,\n" + + "\"message\":{\n" + + " \"date\":1441645532,\n" + + " \"chat\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"type\": \"private\",\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"message_id\":1365,\n" + + " \"from\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"text\":\"/help\"\n" + + "}\n" + + "}", + Update.class); + + final CompletableFuture>> gotResponseFuture = + help.process(fakeTgChat, update); + final Optional> gotResponseOpt = gotResponseFuture.get(); + final BaseRequest gotResponse = gotResponseOpt.get(); + final InlineKeyboardMarkup replyMarkup = + (InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup"); + assertNull(replyMarkup); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java index a72a7b4..900db57 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java @@ -10,6 +10,7 @@ import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QTgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.ChatContext; @@ -74,8 +75,8 @@ class StartIntegrationTest { Executors.newSingleThreadExecutor(), log, fakeUUIDGenerator, - fakeClock, - "Mezzotre"); + "Mezzotre", + new ChatUtil(fakeClock)); } @Test @@ -122,7 +123,7 @@ class StartIntegrationTest { assertInstanceOf(SendMessage.class, gotMessage); final String message = (String) gotMessage.getParameters().get("text"); assertEquals( - "**Hello Test Firstname! \uD83D\uDC4B**\n\n" + "*Hello Test Firstname! \uD83D\uDC4B*\n\n" + "This is _Mezzotre_, a simple bot focused on DnD content management! Please start by" + " choosing a language down below \uD83D\uDC47", message); @@ -180,7 +181,7 @@ class StartIntegrationTest { assertInstanceOf(SendMessage.class, gotMessage); final String message = (String) gotMessage.getParameters().get("text"); assertEquals( - "**Hello Test Firstname! \uD83D\uDC4B**\n\n" + "*Hello Test Firstname! \uD83D\uDC4B*\n\n" + "This is _Mezzotre_, a simple bot focused on DnD content management! Please start by" + " choosing a language down below \uD83D\uDC47", message); -- 2.40.1 From 3baa04174529fc426eac7ae39bc96c6b74497873 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Fri, 14 Apr 2023 16:21:05 +0200 Subject: [PATCH 05/13] doc: add JavaDoc and remaining tests --- .../mezzotre/orm/BatchBeanCleanerService.java | 42 ++++- .../telegram/callbackquery/ShowHelp.java | 2 +- .../mezzotre/telegram/callbackquery/Util.java | 14 +- .../mezzotre/telegram/command/Start.java | 1 - .../mezzotre/telegram/model/Help.java | 32 +++- .../telegram/callbackquery/UtilTest.java | 100 ++++++++++++ .../telegram/model/HelpIntegrationTest.java | 154 ++++++++++++++++++ .../mezzotre/telegram/model/HelpTest.java | 42 +++++ 8 files changed, 379 insertions(+), 8 deletions(-) create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/UtilTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpIntegrationTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpTest.java diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java b/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java index be7f970..acbff5d 100644 --- a/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java +++ b/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java @@ -17,6 +17,19 @@ import javax.inject.Named; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; +/** + * This class works as a "batch" persistence entries remover - basically, it removes all the entries + * that are in the database in an async way. This allows better performance on other pieces of + * codebase, where we don't really care about checking that the removal has been successful, and + * instead we just want to issue a deletion and instantly continue with our codebase flow. + * + *

This service provides the ability to listen to the operation in two ways: via old-fashioned + * listeners and via {@link CompletableFuture}. If the caller wants to be sure that the removal has + * effectively happened, it can sync with the given {@link CompletableFuture}. + * + * @author Davide Polonio + * @since 1.0 + */ public class BatchBeanCleanerService extends AbstractExecutionThreadService { private final LinkedBlockingDeque< @@ -24,6 +37,10 @@ public class BatchBeanCleanerService extends AbstractExecutionThreadService { entriesToRemove; private final Logger log; private final Pair serviceRunningCheckTime; + + // Accessing adding listeners while we iterate on the list inside the service thread _can_ be + // considered a race condition - but given the frequency of adding and removing listeners, the + // computational cost of putting Locks in place and check for them everytime is way greater private final LinkedList< Consumer>, CompletableFuture>>> deletionListener; @@ -40,6 +57,7 @@ public class BatchBeanCleanerService extends AbstractExecutionThreadService { @Override protected void run() throws Exception { + log.trace("BatchBeanCleanerService run() method invoked"); while (isRunning()) { // This statement is blocking for 1 sec if the queue is empty, and then it goes on - this way // we check if the service is still supposed to be up or what @@ -78,19 +96,39 @@ public class BatchBeanCleanerService extends AbstractExecutionThreadService { entriesToRemove.clear(); } + /** + * Add an entry to be removed + * + * @param id the id of the entry to remove + * @param column the column that will be used to perform the delete statement + * @return a {@link CompletableFuture} containing an {@link Integer} indicating the number of rows + * affected by the operation + */ public CompletableFuture removeAsync( - String id, PString> row) { + String id, PString> column) { final CompletableFuture jobExecution = new CompletableFuture<>(); - entriesToRemove.offer(Tuple.of(id, row, jobExecution)); + entriesToRemove.offer(Tuple.of(id, column, jobExecution)); return jobExecution; } + /** + * Add a listener that will be invoked once the entry has been removed. This listener is invoked + * after the {@link CompletableFuture} completion. Note that all the listeners are called in a + * parallel fashion, so call order can change any time. + * + * @param listener the listener to add. + */ public void addListener( Consumer>, CompletableFuture>> listener) { deletionListener.add(listener); } + /** + * Remove a listener + * + * @param listener the listener to be removed + */ public void removeListener( Consumer>, CompletableFuture>> listener) { diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java index 302a931..81d9aa1 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java @@ -73,7 +73,7 @@ class ShowHelp implements Processor { return CompletableFuture.supplyAsync( () -> chatUtil - .extractChat(callbackQueryContext, update) // FIXME callbackquerycontext removal? + .extractChat(callbackQueryContext, update) .map( chat -> { final String message = modelHelp.getMessage(chat, tgCommandProcessors); diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java index 7f6d6f9..3637292 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java @@ -4,9 +4,21 @@ import com.pengrad.telegrambot.model.Message; import com.pengrad.telegrambot.model.Update; import java.util.Optional; +/** + * A class with misc utilities + * + * @author Davide Polonio + * @since 1.0 + */ public class Util { - // FIXME tests, doc + /** + * Extract the message id of the given {@link Update} + * + * @param update the {@link Update} to check and search the message id for + * @return an {@link Optional} containing a {@link Integer} with the message id if it is present, + * otherwise a {@link Optional#empty()} if it is not found. + */ public static Optional extractMessageId(Update update) { return Optional.ofNullable(update.callbackQuery().message()).map(Message::messageId); } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java index bb2b859..390c6ea 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java @@ -87,7 +87,6 @@ class Start implements Processor { "template/telegram/start.vm"); log.trace("Start command - message to send back: " + message); - // FIXME bug!! Show help button set to true but its fake news chatUtil.updateChatContext(chat, TRIGGERING_STAGING_NAME, 0, Collections.emptyMap()); // To get the messageId we should send the message first, then save it in the database! diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java b/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java index 83ee044..2cc8e70 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java @@ -13,11 +13,17 @@ import java.util.stream.Collectors; import javax.inject.Inject; import org.apache.commons.lang3.tuple.Pair; -// FIXME tests! +/** + * This class provides the model for the Help message output. It is the business logic behind the + * message, and a way to keep the codebase DRY. + * + * @author Davide Polonio + * @since 1.0 + * @see com.github.polpetta.mezzotre.telegram.command.Help for the command help + * @see com.github.polpetta.mezzotre.telegram.callbackquery.ShowHelp for the event help + */ public class Help { - private static final String TRIGGERING_STAGING_NAME = "/help"; - private final TemplateContentGenerator templateContentGenerator; private final UUIDGenerator uuidGenerator; @@ -27,6 +33,15 @@ public class Help { this.uuidGenerator = uuidGenerator; } + /** + * Generates the message that will be sent back to the user. This takes the given {@link + * Processor} and formats them accordingly to the chat locale + * + * @param chat the {@link TgChat} conversation + * @param tgCommandProcessors a {@link Map} of all the {@link Processor} that will be printed in + * the help message + * @return a {@link String} localized ready to be sent to the user + */ public String getMessage(TgChat chat, Map tgCommandProcessors) { return templateContentGenerator.mergeTemplate( velocityContext -> { @@ -45,6 +60,17 @@ public class Help { "template/telegram/help.vm"); } + /** + * Generates {@link InlineKeyboardButton} to be returned to the user to give them the possibility + * to interact with them via events rather than commands. + * + * @param chat the current {@link TgChat} conversation + * @param eventProcessors a {@link Map} of {@link + * com.github.polpetta.mezzotre.telegram.callbackquery.Processor} that are currently used to + * process all events. Note that only the one the user can interact with will be added as + * buttons + * @return an array of {@link InlineKeyboardButton} + */ public InlineKeyboardButton[] generateInlineKeyBoardButton( TgChat chat, Map eventProcessors) { diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/UtilTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/UtilTest.java new file mode 100644 index 0000000..ffc2d78 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/UtilTest.java @@ -0,0 +1,100 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +import static org.junit.jupiter.api.Assertions.*; + +import com.google.gson.Gson; +import com.pengrad.telegrambot.model.Update; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@Execution(ExecutionMode.CONCURRENT) +class UtilTest { + + private static Gson gson; + + @BeforeAll + static void beforeAll() { + gson = new Gson(); + } + + @Test + void shouldProvideAMessageIdGivenTheUpdate() { + final Update update = + gson.fromJson( + "{\n" + + " \"update_id\": 158712614,\n" + + " \"callback_query\": {\n" + + " \"id\": \"20496049451114620\",\n" + + " \"from\": {\n" + + " \"id\": 1111111,\n" + + " \"is_bot\": false,\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\",\n" + + " \"username\": \"Testusername\",\n" + + " \"language_code\": \"en\"\n" + + " },\n" + + " \"message\": {\n" + + " \"message_id\": 2723,\n" + + " \"from\": {\n" + + " \"id\": 244745330,\n" + + " \"is_bot\": true,\n" + + " \"first_name\": \"Dev - DavideBot\",\n" + + " \"username\": \"devdavidebot\"\n" + + " },\n" + + " \"date\": 1681218838,\n" + + " \"chat\": {\n" + + " \"id\": 1111111,\n" + + " \"type\": \"private\",\n" + + " \"username\": \"Testusername\",\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\"\n" + + " },\n" + + " \"text\": \"a message\",\n" + + " \"reply_markup\": {\n" + + " \"inline_keyboard\": [\n" + + " [\n" + + " {\n" + + " \"text\": \"English\",\n" + + " \"callback_data\": \"9a64be11-d086-4bd9-859f-720c43dedcb5\"\n" + + " },\n" + + " {\n" + + " \"text\": \"Italian\",\n" + + " \"callback_data\": \"8768d660-f05f-4f4b-bda5-3451ab573d56\"\n" + + " }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + "}", + Update.class); + + assertEquals( + 2723, Util.extractMessageId(update).get(), "A message id should be returned, 2723"); + } + + @Test + void shouldGiveAnEmptyOptionalWithoutMessageId() { + final Update update = + gson.fromJson( + "{\n" + + " \"update_id\": 158712614,\n" + + " \"callback_query\": {\n" + + " \"id\": \"20496049451114620\",\n" + + " \"from\": {\n" + + " \"id\": 1111111,\n" + + " \"is_bot\": false,\n" + + " \"first_name\": \"Test Firstname\",\n" + + " \"last_name\": \"Test Lastname\",\n" + + " \"username\": \"Testusername\",\n" + + " \"language_code\": \"en\"\n" + + " }\n" + + " }\n" + + "}", + Update.class); + + assertTrue(Util.extractMessageId(update).isEmpty(), "The shouldn't be any message id"); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpIntegrationTest.java new file mode 100644 index 0000000..fba44e3 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpIntegrationTest.java @@ -0,0 +1,154 @@ +package com.github.polpetta.mezzotre.telegram.model; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.github.polpetta.mezzotre.helper.Loader; +import com.github.polpetta.mezzotre.helper.TestConfig; +import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; +import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; +import com.github.polpetta.mezzotre.telegram.callbackquery.Processor; +import com.github.polpetta.mezzotre.util.UUIDGenerator; +import com.github.polpetta.types.json.ChatContext; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.request.BaseRequest; +import io.ebean.Database; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Tag("slow") +@Tag("database") +@Tag("velocity") +@Testcontainers +class HelpIntegrationTest { + + @Container + private final PostgreSQLContainer postgresServer = + new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE); + + private TemplateContentGenerator templateContentGenerator; + private UUIDGenerator fakeUUIDGenerator; + private Help help; + private Database database; + + @BeforeEach + void setUp() throws Exception { + database = + Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); + templateContentGenerator = + new TemplateContentGenerator(new LocalizedMessageFactory(Loader.defaultVelocityEngine())); + fakeUUIDGenerator = mock(UUIDGenerator.class); + + help = new Help(templateContentGenerator, fakeUUIDGenerator); + } + + @Test + void shouldGenerateAGoodHelpMessage() { + final TgChat tgChat = new TgChat(11111L, new ChatContext()); + tgChat.save(); + + final com.github.polpetta.mezzotre.telegram.command.Processor dummyCommand1 = + new com.github.polpetta.mezzotre.telegram.command.Processor() { + @Override + public Set getTriggerKeywords() { + return Set.of("/example", "/another"); + } + + @Override + public CompletableFuture>> process( + TgChat chat, Update update) { + return CompletableFuture.completedFuture(Optional.empty()); + } + + @Override + public String getLocaleDescriptionKeyword() { + return "help.cmdDescription"; + } + }; + + final String gotMessage = + help.getMessage(tgChat, Map.of("/example", dummyCommand1, "/another", dummyCommand1)); + assertEquals( + "Here is a list of what I can do:\n" + + "\n" + + "- /another /example: Print the help message\n" + + "\n" + + "You can do the same operations you'd do with the commands aforementioned by" + + " selecting the corresponding button below \uD83D\uDC47", + gotMessage); + } + + @Test + void shouldGenerateButtonsCorrectly() { + final TgChat tgChat = new TgChat(111111L, new ChatContext()); + tgChat.save(); + when(fakeUUIDGenerator.generateAsString()) + .thenReturn("53dc6dca-1042-4bc7-beb8-ce2a34df6a54") + .thenReturn("d5d3e016-7b60-4f1a-bd79-e1a6bff32f17"); + + final Processor visibleProcessor1 = + new Processor() { + @Override + public String getEventName() { + return "eventName"; + } + + @Override + public boolean canBeDirectlyInvokedByTheUser() { + return true; + } + + @Override + public Optional getPrettyPrintLocaleKeyName() { + return Optional.of("changeLanguage.inlineKeyboardButtonName"); + } + + @Override + public CompletableFuture>> process( + CallbackQueryContext callbackQueryContext, Update update) { + return CompletableFuture.completedFuture(Optional.empty()); + } + }; + + final Processor invisibleProcessor1 = + new Processor() { + @Override + public String getEventName() { + return "invisible"; + } + + @Override + public CompletableFuture>> process( + CallbackQueryContext callbackQueryContext, Update update) { + return CompletableFuture.completedFuture(Optional.empty()); + } + }; + + final InlineKeyboardButton[] buttons = + help.generateInlineKeyBoardButton( + tgChat, + Map.of( + visibleProcessor1.getEventName(), + visibleProcessor1, + invisibleProcessor1.getEventName(), + invisibleProcessor1)); + + assertEquals(1, buttons.length); + assertEquals("Change language", buttons[0].text()); + + assertEquals(1, new QCallbackQueryContext().findCount()); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpTest.java new file mode 100644 index 0000000..fbb5eb6 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpTest.java @@ -0,0 +1,42 @@ +package com.github.polpetta.mezzotre.telegram.model; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.util.UUIDGenerator; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@Execution(ExecutionMode.CONCURRENT) +class HelpTest { + + private TemplateContentGenerator fakeTemplateContentGenerator; + private UUIDGenerator fakeUUIDGenerator; + private Help help; + + @BeforeEach + void setUp() { + + fakeTemplateContentGenerator = mock(TemplateContentGenerator.class); + fakeUUIDGenerator = mock(UUIDGenerator.class); + + help = new Help(fakeTemplateContentGenerator, fakeUUIDGenerator); + } + + @Test + void shouldCallTemplateContentGeneratorRight() { + + final TgChat fakeChat = mock(TgChat.class); + when(fakeChat.getLocale()).thenReturn("en-US"); + + final String message = help.getMessage(fakeChat, Collections.emptyMap()); + + verify(fakeTemplateContentGenerator, times(1)) + .mergeTemplate(any(), eq("en-US"), eq("template/telegram/help.vm")); + } +} -- 2.40.1 From 7f632d2b6e229a72f61a77e8cd012d34a31e7e30 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Fri, 14 Apr 2023 16:36:46 +0200 Subject: [PATCH 06/13] chore: add FIXME before PR merge --- .../polpetta/mezzotre/i18n/TemplateContentGenerator.java | 1 + .../polpetta/mezzotre/orm/CallbackQueryContextCleaner.java | 1 + .../com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java | 6 ------ .../polpetta/mezzotre/telegram/callbackquery/Field.java | 1 + .../polpetta/mezzotre/telegram/callbackquery/NotFound.java | 1 + .../polpetta/mezzotre/telegram/callbackquery/Processor.java | 1 + .../polpetta/mezzotre/telegram/command/Processor.java | 4 ++-- .../com/github/polpetta/mezzotre/util/ServiceModule.java | 1 + 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java b/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java index bb98afe..4e00a02 100644 --- a/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java +++ b/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java @@ -8,6 +8,7 @@ import org.apache.velocity.VelocityContext; import org.apache.velocity.tools.ToolManager; import org.apache.velocity.util.StringBuilderWriter; +// FIXME doc, tests public class TemplateContentGenerator { private final LocalizedMessageFactory localizedMessageFactory; diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java b/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java index 0119168..190e664 100644 --- a/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java +++ b/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java @@ -8,6 +8,7 @@ import javax.inject.Inject; import javax.inject.Singleton; import org.slf4j.Logger; +// FIXME tests, doc @Singleton public class CallbackQueryContextCleaner { diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java b/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java index b722f2b..4fc26fd 100644 --- a/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java +++ b/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java @@ -2,7 +2,6 @@ package com.github.polpetta.mezzotre.orm.telegram; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; -import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QTgChat; import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.types.json.ChatContext; @@ -81,9 +80,4 @@ public class ChatUtil { .filter(chatId -> chatId != 0L && chatId != Long.MIN_VALUE) .flatMap(chatId -> new QTgChat().id.eq(chatId).findOneOrEmpty()); } - - public T cleanCallbackQuery(T toReturn, CallbackQueryContext callbackQueryContext) { - new QCallbackQueryContext().entryGroup.eq(callbackQueryContext.getId()).delete(); - return toReturn; - } } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java index e2a485d..f66a1eb 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java @@ -1,5 +1,6 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; +// FIXME doc public interface Field { /** * Additional fields that are related to {@code changeLanguage} event diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java index b63a9bd..a35720d 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java @@ -10,6 +10,7 @@ import java.util.concurrent.CompletableFuture; import javax.inject.Inject; import org.slf4j.Logger; +// FIXME doc public class NotFound implements Processor { private final Logger log; diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java index 020d1d6..ecba2a4 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java @@ -23,6 +23,7 @@ public interface Processor { */ String getEventName(); + // FIXME javadoc default boolean canBeDirectlyInvokedByTheUser() { return false; } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java index ed5f028..c9a6216 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Processor.java @@ -17,10 +17,10 @@ import java.util.concurrent.CompletableFuture; public interface Processor { /** - * Provides the keyword to trigger this executor. Note that it must start with "/" at the + * Provides the keywords to trigger this executor. Note that it must start with "/" at the * beginning, e.g. {@code /start}. * - * @return a {@link String} providing the keyword to trigger the current {@link Processor} + * @return a {@link Set} providing the keywords to trigger the current {@link Processor} */ Set getTriggerKeywords(); diff --git a/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java b/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java index e725c4d..e9db94d 100644 --- a/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java +++ b/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java @@ -13,6 +13,7 @@ import javax.inject.Inject; import javax.inject.Named; import org.jetbrains.annotations.NotNull; +// FIXME doc, tests(?) public class ServiceModule implements Extension { private final ServiceManager serviceManager; -- 2.40.1 From 8dd547404de0c7bf3e4fd8a910b876a6d5069e52 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Sat, 15 Apr 2023 12:50:36 +0200 Subject: [PATCH 07/13] docs: add Javadoc --- .../i18n/TemplateContentGenerator.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java b/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java index 4e00a02..794e2ec 100644 --- a/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java +++ b/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java @@ -9,6 +9,14 @@ import org.apache.velocity.tools.ToolManager; import org.apache.velocity.util.StringBuilderWriter; // FIXME doc, tests + +/** + * This class aims to generate localized messages or strings either merging Velocity templates or + * taking directly the keys from {@link java.util.ResourceBundle} + * + * @author Davide Polonio + * @since 1.0 + */ public class TemplateContentGenerator { private final LocalizedMessageFactory localizedMessageFactory; @@ -18,6 +26,19 @@ public class TemplateContentGenerator { this.localizedMessageFactory = localizedMessageFactory; } + /** + * Merge a Velocity template located in the local Jar resource path with the given {@link Locale}, + * provided as a {@link String}. A {@link Consumer} is provided in order to let the user customize + * the {@link VelocityContext} with additional variables. Localization strings will be directly + * taken via the use of {@link LocalizedTool} class. + * + * @param velocityContextConsumer a lambda function that allows for the customization of {@link + * VelocityContext} instance + * @param localeAsString a {@link Locale} formatted as a {@link String}. Note that {@link + * Locale#forLanguageTag(String)} will be used for the conversion + * @param templateName the path to the local Jar resource + * @return a {@link String} containing the localized message + */ public String mergeTemplate( Consumer velocityContextConsumer, String localeAsString, -- 2.40.1 From faf7d120019e420af8bba37d32ceb61ed0569810 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Sat, 15 Apr 2023 13:05:09 +0200 Subject: [PATCH 08/13] docs: add Javadoc --- .../i18n/TemplateContentGenerator.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java b/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java index 794e2ec..33438eb 100644 --- a/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java +++ b/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java @@ -8,7 +8,7 @@ import org.apache.velocity.VelocityContext; import org.apache.velocity.tools.ToolManager; import org.apache.velocity.util.StringBuilderWriter; -// FIXME doc, tests +// FIXME tests /** * This class aims to generate localized messages or strings either merging Velocity templates or @@ -61,10 +61,27 @@ public class TemplateContentGenerator { return content.toString(); } + /** + * Get a localized {@link String} + * + * @param locale a provided {@link Locale} + * @param key the key that will be retrieved from the localized messages + * @return a localized {@link String} + * @see #getString(String, String) + */ public String getString(Locale locale, String key) { return localizedMessageFactory.createResourceBundle(locale).getString(key); } + /** + * Get a localized {@link String} + * + * @param localeAsString a provided {@link Locale} as a {@link String}. Will be converted using + * {@link Locale#forLanguageTag(String)} + * @param key the key that will be retrieved from the localized messages + * @return a localized {@link String} + * @see #getString(Locale, String) + */ public String getString(String localeAsString, String key) { return getString(Locale.forLanguageTag(localeAsString), key); } -- 2.40.1 From 97c3d50e6b750c89c254cc2686f7def708c30865 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Tue, 18 Apr 2023 17:59:56 +0200 Subject: [PATCH 09/13] fix: bunch of TODOs --- .../i18n/TemplateContentGenerator.java | 2 - .../orm/CallbackQueryContextCleaner.java | 28 ++++- .../telegram/callbackquery/Field.java | 11 +- .../telegram/callbackquery/NotFound.java | 7 +- .../telegram/callbackquery/Processor.java | 12 +- .../i18n/TemplateContentGeneratorTest.java | 103 ++++++++++++++++++ ...ackQueryContextCleanerIntegrationTest.java | 81 ++++++++++++++ 7 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/github/polpetta/mezzotre/i18n/TemplateContentGeneratorTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleanerIntegrationTest.java diff --git a/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java b/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java index 33438eb..ab8f959 100644 --- a/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java +++ b/src/main/java/com/github/polpetta/mezzotre/i18n/TemplateContentGenerator.java @@ -8,8 +8,6 @@ import org.apache.velocity.VelocityContext; import org.apache.velocity.tools.ToolManager; import org.apache.velocity.util.StringBuilderWriter; -// FIXME tests - /** * This class aims to generate localized messages or strings either merging Velocity templates or * taking directly the keys from {@link java.util.ResourceBundle} diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java b/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java index 190e664..677a6b1 100644 --- a/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java +++ b/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java @@ -1,5 +1,6 @@ package com.github.polpetta.mezzotre.orm; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; import io.ebean.typequery.PString; import java.util.concurrent.CompletableFuture; @@ -8,7 +9,14 @@ import javax.inject.Inject; import javax.inject.Singleton; import org.slf4j.Logger; -// FIXME tests, doc +/** + * This class allows the system to lazily remove {@link CallbackQueryContextCleaner} entries that + * are no more needed, using the {@link BatchBeanCleanerService} service under the hood. + * + * @author Davide Polonio + * @see BatchBeanCleanerService + * @since 1.0 + */ @Singleton public class CallbackQueryContextCleaner { @@ -26,11 +34,29 @@ public class CallbackQueryContextCleaner { this.log = log; } + /** + * Add a group of {@link com.github.polpetta.mezzotre.orm.model.CallbackQueryContext} to be + * removed + * + * @param id the group id of the {@link + * com.github.polpetta.mezzotre.orm.model.CallbackQueryContext} + * @return a {@link CompletableFuture} with an {@link Integer} indicating how many entries have + * been deleted from the persistence layer + * @see CallbackQueryContext#getEntryGroup() + */ public CompletableFuture removeGroupAsync(String id) { log.trace("CallbackQueryContext entry group " + id + " queued for removal"); return batchBeanCleanerService.removeAsync(id, ENTRY_GROUP.get()); } + /** + * Add a single {@link com.github.polpetta.mezzotre.orm.model.CallbackQueryContext} to be removed + * + * @param id the id of the {@link CallbackQueryContext} to remove + * @return a {@link CompletableFuture} with an {@link Integer} indicating how many entries have + * been deleted from the persistence layer. Can be 0 or 1. + * @see CallbackQueryContext#getId() + */ public CompletableFuture removeIdAsync(String id) { log.trace("CallbackQueryContext single entity " + id + " queued for removal"); return batchBeanCleanerService.removeAsync(id, SINGLE_ENTRY.get()); diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java index f66a1eb..14d6968 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Field.java @@ -1,6 +1,11 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; -// FIXME doc +/** + * This interface is a placeholder to keep all fields in one place + * + * @author Davide Polonio + * @since 1.0 + */ public interface Field { /** * Additional fields that are related to {@code changeLanguage} event @@ -22,6 +27,10 @@ public interface Field { } } + /** + * Enumerator with custom fields for {@link + * com.github.polpetta.mezzotre.telegram.callbackquery.ShowHelp} callback query + */ enum ShowHelp { InvokedFromHelpMessage("invokedFromHelpMessage"); diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java index a35720d..901d6f9 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java @@ -10,7 +10,12 @@ import java.util.concurrent.CompletableFuture; import javax.inject.Inject; import org.slf4j.Logger; -// FIXME doc +/** + * Callback query event that is triggered only when a corresponding event is not found in the system + * + * @author Davide Polonio + * @since 1.0 + */ public class NotFound implements Processor { private final Logger log; diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java index ecba2a4..0707a50 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Processor.java @@ -23,11 +23,21 @@ public interface Processor { */ String getEventName(); - // FIXME javadoc + /** + * Determines if the Callback Query can directly be invoked by the user, such as starting an + * interaction from this point or invoking it from an inline query button + * + * @return true if it is, false otherwise + */ default boolean canBeDirectlyInvokedByTheUser() { return false; } + /** + * Gives the ability to print a localized version of the event, in a pretty way + * + * @return the key for the corresponding localization string + */ default Optional getPrettyPrintLocaleKeyName() { return Optional.empty(); } diff --git a/src/test/java/com/github/polpetta/mezzotre/i18n/TemplateContentGeneratorTest.java b/src/test/java/com/github/polpetta/mezzotre/i18n/TemplateContentGeneratorTest.java new file mode 100644 index 0000000..2d8df23 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/i18n/TemplateContentGeneratorTest.java @@ -0,0 +1,103 @@ +package com.github.polpetta.mezzotre.i18n; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.Locale; +import java.util.ResourceBundle; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.tools.ToolContext; +import org.apache.velocity.tools.ToolManager; +import org.apache.velocity.util.StringBuilderWriter; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.mockito.ArgumentCaptor; + +@Execution(ExecutionMode.CONCURRENT) +class TemplateContentGeneratorTest { + + private LocalizedMessageFactory fakeLocalizedMessageFactory; + private TemplateContentGenerator templateContentGenerator; + + @BeforeEach + void setUp() { + fakeLocalizedMessageFactory = mock(LocalizedMessageFactory.class); + templateContentGenerator = new TemplateContentGenerator(fakeLocalizedMessageFactory); + } + + @Test + void shouldCallMergeTemplateWithRightArguments() { + final VelocityEngine fakeVelocityEngine = mock(VelocityEngine.class); + final ToolManager fakeToolManager = mock(ToolManager.class); + when(fakeToolManager.getVelocityEngine()).thenReturn(fakeVelocityEngine); + final ToolContext fakeToolContext = mock(ToolContext.class); + when(fakeToolManager.createContext()).thenReturn(fakeToolContext); + when(fakeLocalizedMessageFactory.createVelocityToolManager(Locale.forLanguageTag("en-US"))) + .thenReturn(fakeToolManager); + + templateContentGenerator.mergeTemplate( + ctx -> { + ctx.put("testtesttest", "value"); + }, + "en-US", + "a/fake/path.vm"); + + final ArgumentCaptor velocityContextArgumentCaptor = + ArgumentCaptor.forClass(VelocityContext.class); + verify(fakeVelocityEngine, times(1)) + .mergeTemplate( + eq("a/fake/path.vm"), + eq(StandardCharsets.UTF_8.name()), + velocityContextArgumentCaptor.capture(), + any(StringBuilderWriter.class)); + + final VelocityContext gotVelocityContext = velocityContextArgumentCaptor.getValue(); + assertEquals("value", gotVelocityContext.get("testtesttest")); + } + + @Test + void shouldCallLocaleFactoryWhenRetrievingKey() { + final Locale enUS = Locale.forLanguageTag("en-US"); + + class StubRB extends ResourceBundle { + + @Override + protected Object handleGetObject(@NotNull String s) { + return "testtest123"; + } + + @NotNull + @Override + public Enumeration getKeys() { + return new Enumeration() { + + private boolean next = true; + + @Override + public boolean hasMoreElements() { + final boolean toRet = next; + next = !next; + return toRet; + } + + @Override + public String nextElement() { + return "doens't matter"; + } + }; + } + } + + final StubRB stubRB = new StubRB(); + when(fakeLocalizedMessageFactory.createResourceBundle(any())).thenReturn(stubRB); + + final String got = templateContentGenerator.getString(enUS, "a.string"); + assertEquals("testtest123", got); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleanerIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleanerIntegrationTest.java new file mode 100644 index 0000000..de487f2 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleanerIntegrationTest.java @@ -0,0 +1,81 @@ +package com.github.polpetta.mezzotre.orm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import com.github.polpetta.mezzotre.helper.Loader; +import com.github.polpetta.mezzotre.helper.TestConfig; +import io.ebean.Database; +import io.ebean.typequery.PString; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.mockito.ArgumentCaptor; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Tag("slow") +@Tag("database") +@Tag("velocity") +@Testcontainers +class CallbackQueryContextCleanerIntegrationTest { + + @Container + private final PostgreSQLContainer postgresServer = + new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE); + + private Database database; + + private BatchBeanCleanerService fakeBatchBeanCleanerService; + private CallbackQueryContextCleaner callbackQueryContextCleaner; + + @BeforeEach + void setUp() throws Exception { + database = + Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); + + fakeBatchBeanCleanerService = mock(BatchBeanCleanerService.class); + + callbackQueryContextCleaner = + new CallbackQueryContextCleaner( + fakeBatchBeanCleanerService, + LoggerFactory.getLogger(CallbackQueryContextCleaner.class)); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void shouldDeleteByGroupId() throws Exception { + when(fakeBatchBeanCleanerService.removeAsync(eq("an id"), any())) + .thenReturn(CompletableFuture.completedFuture(1)); + + final Integer got = callbackQueryContextCleaner.removeGroupAsync("an id").get(); + + final ArgumentCaptor pStringArgumentCaptor = ArgumentCaptor.forClass(PString.class); + verify(fakeBatchBeanCleanerService, times(1)) + .removeAsync(eq("an id"), pStringArgumentCaptor.capture()); + assertEquals(1, got); + assertEquals("entryGroup", pStringArgumentCaptor.getValue().toString()); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void shouldDeleteById() throws Exception { + when(fakeBatchBeanCleanerService.removeAsync(eq("an id"), any())) + .thenReturn(CompletableFuture.completedFuture(1)); + + final Integer got = callbackQueryContextCleaner.removeIdAsync("an id").get(); + + final ArgumentCaptor pStringArgumentCaptor = ArgumentCaptor.forClass(PString.class); + verify(fakeBatchBeanCleanerService, times(1)) + .removeAsync(eq("an id"), pStringArgumentCaptor.capture()); + assertEquals(1, got); + assertEquals("id", pStringArgumentCaptor.getValue().toString()); + } +} -- 2.40.1 From b2a2f04ba389fcc182567442a6837cb6d64514c6 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Wed, 19 Apr 2023 18:08:22 +0200 Subject: [PATCH 10/13] feat: add error-prone library, done translations --- .mvn/jvm.config | 10 +++ pom.xml | 16 ++++- .../mezzotre/telegram/command/NotFound.java | 26 +++++-- .../polpetta/mezzotre/util/ServiceModule.java | 16 ++++- src/main/resources/i18n/message.properties | 4 ++ .../resources/i18n/message_en_US.properties | 4 ++ src/main/resources/i18n/message_it.properties | 12 ++++ .../resources/i18n/message_it_IT.properties | 12 ++++ .../resources/template/telegram/notFound.vm | 3 + .../mezzotre/util/ServiceModuleTest.java | 72 +++++++++++++++++++ 10 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 .mvn/jvm.config create mode 100644 src/main/resources/template/telegram/notFound.vm create mode 100644 src/test/java/com/github/polpetta/mezzotre/util/ServiceModuleTest.java diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000..32599ce --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1,10 @@ +--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED diff --git a/pom.xml b/pom.xml index bb22800..66b4eb0 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,7 @@ 2.13.3 5.9.1 5.1.0 + 2.18.0 @@ -240,7 +241,11 @@ 31.1-jre - + + com.google.errorprone + error_prone_annotations + ${error-prone.version} + @@ -279,12 +284,19 @@ maven-compiler-plugin - 3.6.2 + 3.8.1 -parameters + -XDcompilePolicy=simple + -Xplugin:ErrorProne + + com.google.errorprone + error_prone_core + ${error-prone.version} + io.jooby jooby-apt diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java index deef282..ad42361 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java @@ -1,5 +1,6 @@ package com.github.polpetta.mezzotre.telegram.command; +import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; import com.github.polpetta.mezzotre.orm.model.TgChat; import com.google.inject.assistedinject.Assisted; import com.pengrad.telegrambot.model.Update; @@ -9,7 +10,9 @@ import java.util.Collections; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import javax.inject.Inject; +import javax.inject.Named; /** * Generates a "Command not found" message @@ -19,12 +22,20 @@ import javax.inject.Inject; * @see com.github.polpetta.mezzotre.telegram.callbackquery.NotFound for the event-based version */ class NotFound implements Processor { + private static final String CMD_CTX_NAME = "cmdName"; private final String commandName; + private final TemplateContentGenerator templateContentGenerator; + private final Executor threadPool; @Inject - public NotFound(@Assisted String commandName) { + public NotFound( + @Assisted String commandName, + TemplateContentGenerator templateContentGenerator, + @Named("eventThreadPool") Executor threadPool) { this.commandName = commandName; + this.templateContentGenerator = templateContentGenerator; + this.threadPool = threadPool; } @Override @@ -35,10 +46,13 @@ class NotFound implements Processor { @Override public CompletableFuture>> process(TgChat chat, Update update) { // FIXME complete it with: localization, callbackQuery to show help message - return CompletableFuture.completedFuture(update) - .thenApply( - ignored -> - Optional.of( - new SendMessage(chat.getId(), "Command " + commandName + " is not valid"))); + return CompletableFuture.supplyAsync( + () -> + templateContentGenerator.mergeTemplate( + ctx -> ctx.put(CMD_CTX_NAME, commandName), + chat.getLocale(), + "template/telegram/notFound.vm"), + threadPool) + .thenApply(text -> Optional.of(new SendMessage(chat.getId(), text))); } } diff --git a/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java b/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java index e9db94d..f509088 100644 --- a/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java +++ b/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java @@ -13,7 +13,21 @@ import javax.inject.Inject; import javax.inject.Named; import org.jetbrains.annotations.NotNull; -// FIXME doc, tests(?) +/** + * This Jooby {@link Extension} allows to start and execute services that implement {@link Service} + * class. This allows implementation of this interface to be automatically started. Services need to + * me listed by the named key {@code "services"}, which is a {@link List} of services passed by the + * {@link com.google.inject.Injector}. + * + *

The module handles the services start and stop when the whole JVM is stopped, allowing a + * graceful service shutdown. + * + * @author Davide Polonio + * @since 1.0 + * @see Guava Service Wiki + * explained + * @see ServiceManager + */ public class ServiceModule implements Extension { private final ServiceManager serviceManager; diff --git a/src/main/resources/i18n/message.properties b/src/main/resources/i18n/message.properties index d022a29..f8458cd 100644 --- a/src/main/resources/i18n/message.properties +++ b/src/main/resources/i18n/message.properties @@ -10,6 +10,8 @@ changeLanguage.english=English changeLanguage.italian=Italian changeLanguage.cmdDescription=Select the new language I will use to speak to you changeLanguage.inlineKeyboardButtonName=Change language +selectLanguageTutorial.english=English +selectLanguageTutorial.italian=Italian spell.speakWithAnimals=Speak with animals selectLanguageTutorial.showMeTutorialInlineKeyboardButtonName=Show me what you can do! help.notShownYet=It seems you haven''t checked out what I can do yet! To have a complete list of my abilities, type /help in chat at any time! @@ -17,3 +19,5 @@ help.buttonBelow=Alternatively, you can click the button down below. help.description=Here is a list of what I can do help.buttonsToo=You can do the same operations you''d do with the commands aforementioned by selecting the corresponding button below \ud83d\udc47 help.cmdDescription=Print the help message +notfound.description=Mmm I''m not able to find command {0}, are you sure to have it typed correctly? +notfound.howToHelp=Let me show you what I can do by typing /help in the chat! diff --git a/src/main/resources/i18n/message_en_US.properties b/src/main/resources/i18n/message_en_US.properties index d022a29..f8458cd 100644 --- a/src/main/resources/i18n/message_en_US.properties +++ b/src/main/resources/i18n/message_en_US.properties @@ -10,6 +10,8 @@ changeLanguage.english=English changeLanguage.italian=Italian changeLanguage.cmdDescription=Select the new language I will use to speak to you changeLanguage.inlineKeyboardButtonName=Change language +selectLanguageTutorial.english=English +selectLanguageTutorial.italian=Italian spell.speakWithAnimals=Speak with animals selectLanguageTutorial.showMeTutorialInlineKeyboardButtonName=Show me what you can do! help.notShownYet=It seems you haven''t checked out what I can do yet! To have a complete list of my abilities, type /help in chat at any time! @@ -17,3 +19,5 @@ help.buttonBelow=Alternatively, you can click the button down below. help.description=Here is a list of what I can do help.buttonsToo=You can do the same operations you''d do with the commands aforementioned by selecting the corresponding button below \ud83d\udc47 help.cmdDescription=Print the help message +notfound.description=Mmm I''m not able to find command {0}, are you sure to have it typed correctly? +notfound.howToHelp=Let me show you what I can do by typing /help in the chat! diff --git a/src/main/resources/i18n/message_it.properties b/src/main/resources/i18n/message_it.properties index 01ae788..cc81ee8 100644 --- a/src/main/resources/i18n/message_it.properties +++ b/src/main/resources/i18n/message_it.properties @@ -1,11 +1,23 @@ start.helloFirstName=Ciao {0}! \ud83d\udc4b start.description=Questo è {0}, un semplice bot che ci concenta sulla gestione di contenuto per DnD! Per favore comincia selezionando la lingua qui sotto \ud83d\udc47 +start.cmdDescription=Comincia a chattare con questo bot +start.inlineKeyboardButtonName=Cominciamo! +selectLanguageTutorial.inlineKeyboardButtonName=Seleziona linguaggio selectLanguageTutorial.drinkAction=*Procede a bere una pozione al cui suo interno si trova uno strano liquido multicolore* selectLanguageTutorial.setLanguage=Grazie! Ora che ho bevuto questa posizione modificata di {0} che ho trovato ieri al negozio di pozioni magiche la "Cristalleria Fermentatrice" posso parlare con te nel linguaggio che preferisci! selectLanguageTutorial.instructions=Puoi sempre cambiare le preferenze della tua lingua scrivendo /changeLanguage nella chat. +changeLanguage.english=Inglese +changeLanguage.italian=Italiano +changeLanguage.cmdDescription=Seleziona il nuovo linguaggio che userò per parlare con te +changeLanguage.inlineKeyboardButtonName=Cambia lingua selectLanguageTutorial.english=Inglese selectLanguageTutorial.italian=Italiano spell.speakWithAnimals=Parlare con animali selectLanguageTutorial.showMeTutorialInlineKeyboardButtonName=Mostrami cosa puoi fare! help.notShownYet=Sembra tu non abbia ancora visto cosa posso fare! Per avere una lista completa delle mie abilità, scrivi /help nella chat in qualsiasi momento! help.buttonBelow=Alternativamente, puoi premere il bottone qui sotto. +help.description=Ecco una lista di quello che sono in grado di fare +help.buttonsToo=Puoi fare le stesse operazioni che faresti con i comandi elencati precedentemente cliccando il bottone corrispondente qui di sotto \ud83d\udc47 +help.cmdDescription=Stampa il messaggio d'aiuto +notfound.description=Mmm non sono in grado di trovare il comando {0}, sei sicuro di averlo scritto correttamente? +notfound.howToHelp=Lascia che ti mostri cosa posso fare, scrivi /help nella chat! diff --git a/src/main/resources/i18n/message_it_IT.properties b/src/main/resources/i18n/message_it_IT.properties index 01ae788..cc81ee8 100644 --- a/src/main/resources/i18n/message_it_IT.properties +++ b/src/main/resources/i18n/message_it_IT.properties @@ -1,11 +1,23 @@ start.helloFirstName=Ciao {0}! \ud83d\udc4b start.description=Questo è {0}, un semplice bot che ci concenta sulla gestione di contenuto per DnD! Per favore comincia selezionando la lingua qui sotto \ud83d\udc47 +start.cmdDescription=Comincia a chattare con questo bot +start.inlineKeyboardButtonName=Cominciamo! +selectLanguageTutorial.inlineKeyboardButtonName=Seleziona linguaggio selectLanguageTutorial.drinkAction=*Procede a bere una pozione al cui suo interno si trova uno strano liquido multicolore* selectLanguageTutorial.setLanguage=Grazie! Ora che ho bevuto questa posizione modificata di {0} che ho trovato ieri al negozio di pozioni magiche la "Cristalleria Fermentatrice" posso parlare con te nel linguaggio che preferisci! selectLanguageTutorial.instructions=Puoi sempre cambiare le preferenze della tua lingua scrivendo /changeLanguage nella chat. +changeLanguage.english=Inglese +changeLanguage.italian=Italiano +changeLanguage.cmdDescription=Seleziona il nuovo linguaggio che userò per parlare con te +changeLanguage.inlineKeyboardButtonName=Cambia lingua selectLanguageTutorial.english=Inglese selectLanguageTutorial.italian=Italiano spell.speakWithAnimals=Parlare con animali selectLanguageTutorial.showMeTutorialInlineKeyboardButtonName=Mostrami cosa puoi fare! help.notShownYet=Sembra tu non abbia ancora visto cosa posso fare! Per avere una lista completa delle mie abilità, scrivi /help nella chat in qualsiasi momento! help.buttonBelow=Alternativamente, puoi premere il bottone qui sotto. +help.description=Ecco una lista di quello che sono in grado di fare +help.buttonsToo=Puoi fare le stesse operazioni che faresti con i comandi elencati precedentemente cliccando il bottone corrispondente qui di sotto \ud83d\udc47 +help.cmdDescription=Stampa il messaggio d'aiuto +notfound.description=Mmm non sono in grado di trovare il comando {0}, sei sicuro di averlo scritto correttamente? +notfound.howToHelp=Lascia che ti mostri cosa posso fare, scrivi /help nella chat! diff --git a/src/main/resources/template/telegram/notFound.vm b/src/main/resources/template/telegram/notFound.vm new file mode 100644 index 0000000..18916a1 --- /dev/null +++ b/src/main/resources/template/telegram/notFound.vm @@ -0,0 +1,3 @@ +${i18n.notfound.description.insert(${cmdName})} + +${i18n.notfound.howToHelp} \ No newline at end of file diff --git a/src/test/java/com/github/polpetta/mezzotre/util/ServiceModuleTest.java b/src/test/java/com/github/polpetta/mezzotre/util/ServiceModuleTest.java new file mode 100644 index 0000000..a698b78 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/util/ServiceModuleTest.java @@ -0,0 +1,72 @@ +package com.github.polpetta.mezzotre.util; + +import static org.junit.jupiter.api.Assertions.*; + +import com.google.common.util.concurrent.Service; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@Execution(ExecutionMode.CONCURRENT) +class ServiceModuleTest { + + /** + * This test seems stupid, but it is actually important that the behavior of the extension is + * always with a {@code lateInit} set to true, otherwise services won't be alive for the whole JVM + * execution + */ + @Test + void shouldBeLateInit() { + // Necessary otherwise ServiceManager will throw exception in the constructor 🤦 + final Service dumbService = + new Service() { + @Override + public Service startAsync() { + return null; + } + + @Override + public boolean isRunning() { + return false; + } + + @Override + public State state() { + return State.NEW; + } + + @Override + public Service stopAsync() { + return null; + } + + @Override + public void awaitRunning() {} + + @Override + public void awaitRunning(long timeout, TimeUnit unit) throws TimeoutException {} + + @Override + public void awaitTerminated() {} + + @Override + public void awaitTerminated(long timeout, TimeUnit unit) throws TimeoutException {} + + @Override + public Throwable failureCause() { + return null; + } + + @Override + public void addListener(Listener listener, Executor executor) {} + }; + + final ServiceModule serviceModule = new ServiceModule(List.of(dumbService)); + + assertTrue(serviceModule.lateinit()); + } +} -- 2.40.1 From d32915cf3facee92d789950b5e52a018f3759bbc Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Thu, 20 Apr 2023 11:10:26 +0200 Subject: [PATCH 11/13] chore: remove autocommit, fix compilation --- conf/application.conf | 4 ++ .../com/github/polpetta/mezzotre/AppDI.java | 8 +--- .../orm/CallbackQueryContextCleaner.java | 3 ++ .../github/polpetta/mezzotre/orm/OrmDI.java | 2 +- .../polpetta/mezzotre/helper/Loader.java | 1 + ...ackQueryContextCleanerIntegrationTest.java | 47 +++++++++---------- 6 files changed, 33 insertions(+), 32 deletions(-) diff --git a/conf/application.conf b/conf/application.conf index dfa8f8e..3d8fa39 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -3,6 +3,10 @@ db.url = "jdbc:postgresql://localhost:5433/example" db.user = example db.password = example + +hikari.autoCommit = false +hikari.maximumPoolSize = 4 + telegram.key = akey application.lang = en en-US it it-IT diff --git a/src/main/java/com/github/polpetta/mezzotre/AppDI.java b/src/main/java/com/github/polpetta/mezzotre/AppDI.java index 15d945f..811ca05 100644 --- a/src/main/java/com/github/polpetta/mezzotre/AppDI.java +++ b/src/main/java/com/github/polpetta/mezzotre/AppDI.java @@ -6,10 +6,8 @@ import com.google.inject.AbstractModule; import com.google.inject.Provides; import io.jooby.Jooby; import java.util.List; -import java.util.concurrent.TimeUnit; import javax.inject.Named; import javax.inject.Singleton; -import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; public class AppDI extends AbstractModule { @@ -37,9 +35,7 @@ public class AppDI extends AbstractModule { @Provides @Singleton @Named("services") - public List getApplicationServices( - Logger logger, - @Named("serviceRunningCheckTime") Pair serviceRunningCheckTime) { - return List.of(new BatchBeanCleanerService(logger, serviceRunningCheckTime)); + public List getApplicationServices(BatchBeanCleanerService batchBeanCleanerService) { + return List.of(batchBeanCleanerService); } } diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java b/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java index 677a6b1..9603653 100644 --- a/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java +++ b/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java @@ -2,6 +2,7 @@ package com.github.polpetta.mezzotre.orm; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.ebean.typequery.PString; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; @@ -44,6 +45,7 @@ public class CallbackQueryContextCleaner { * been deleted from the persistence layer * @see CallbackQueryContext#getEntryGroup() */ + @CanIgnoreReturnValue public CompletableFuture removeGroupAsync(String id) { log.trace("CallbackQueryContext entry group " + id + " queued for removal"); return batchBeanCleanerService.removeAsync(id, ENTRY_GROUP.get()); @@ -57,6 +59,7 @@ public class CallbackQueryContextCleaner { * been deleted from the persistence layer. Can be 0 or 1. * @see CallbackQueryContext#getId() */ + @CanIgnoreReturnValue public CompletableFuture removeIdAsync(String id) { log.trace("CallbackQueryContext single entity " + id + " queued for removal"); return batchBeanCleanerService.removeAsync(id, SINGLE_ENTRY.get()); diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/OrmDI.java b/src/main/java/com/github/polpetta/mezzotre/orm/OrmDI.java index 48b1e87..62e2d70 100644 --- a/src/main/java/com/github/polpetta/mezzotre/orm/OrmDI.java +++ b/src/main/java/com/github/polpetta/mezzotre/orm/OrmDI.java @@ -2,13 +2,13 @@ package com.github.polpetta.mezzotre.orm; import com.google.inject.AbstractModule; import com.google.inject.Provides; -import com.google.inject.Singleton; import com.zaxxer.hikari.HikariConfig; import io.jooby.Extension; import io.jooby.ebean.EbeanModule; import io.jooby.flyway.FlywayModule; import io.jooby.hikari.HikariModule; import javax.inject.Named; +import javax.inject.Singleton; public class OrmDI extends AbstractModule { /** diff --git a/src/test/java/com/github/polpetta/mezzotre/helper/Loader.java b/src/test/java/com/github/polpetta/mezzotre/helper/Loader.java index 90bc7b9..6063993 100644 --- a/src/test/java/com/github/polpetta/mezzotre/helper/Loader.java +++ b/src/test/java/com/github/polpetta/mezzotre/helper/Loader.java @@ -30,6 +30,7 @@ public class Loader { hikariConnectionProperties.put("username", container.getUsername()); hikariConnectionProperties.put("password", container.getPassword()); hikariConnectionProperties.put("jdbcUrl", container.getJdbcUrl()); + hikariConnectionProperties.put("autoCommit", "false"); ebeanConnectionProperties.load(ebeanInputStream); ebeanConnectionProperties.put("datasource_db_username", container.getUsername()); diff --git a/src/test/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleanerIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleanerIntegrationTest.java index de487f2..afe607a 100644 --- a/src/test/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleanerIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleanerIntegrationTest.java @@ -1,21 +1,18 @@ package com.github.polpetta.mezzotre.orm; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import com.github.polpetta.mezzotre.helper.Loader; import com.github.polpetta.mezzotre.helper.TestConfig; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; +import com.github.polpetta.types.json.CallbackQueryMetadata; import io.ebean.Database; -import io.ebean.typequery.PString; -import java.util.concurrent.CompletableFuture; +import java.time.Duration; import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; -import org.mockito.ArgumentCaptor; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.*; import org.slf4j.LoggerFactory; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; @@ -33,7 +30,7 @@ class CallbackQueryContextCleanerIntegrationTest { private Database database; - private BatchBeanCleanerService fakeBatchBeanCleanerService; + private BatchBeanCleanerService batchBeanCleanerService; private CallbackQueryContextCleaner callbackQueryContextCleaner; @BeforeEach @@ -41,41 +38,41 @@ class CallbackQueryContextCleanerIntegrationTest { database = Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); - fakeBatchBeanCleanerService = mock(BatchBeanCleanerService.class); + batchBeanCleanerService = + new BatchBeanCleanerService( + LoggerFactory.getLogger(BatchBeanCleanerService.class), + Pair.of(0, TimeUnit.MILLISECONDS)); + batchBeanCleanerService.startAsync().awaitRunning(Duration.ofSeconds(10)); callbackQueryContextCleaner = new CallbackQueryContextCleaner( - fakeBatchBeanCleanerService, - LoggerFactory.getLogger(CallbackQueryContextCleaner.class)); + batchBeanCleanerService, LoggerFactory.getLogger(CallbackQueryContextCleaner.class)); + } + + @AfterEach + void tearDown() throws Exception { + batchBeanCleanerService.stopAsync().awaitTerminated(Duration.ofSeconds(10)); } @Test @Timeout(value = 1, unit = TimeUnit.MINUTES) void shouldDeleteByGroupId() throws Exception { - when(fakeBatchBeanCleanerService.removeAsync(eq("an id"), any())) - .thenReturn(CompletableFuture.completedFuture(1)); + new CallbackQueryContext("doesn't matter", "an id", new CallbackQueryMetadata()).save(); final Integer got = callbackQueryContextCleaner.removeGroupAsync("an id").get(); - final ArgumentCaptor pStringArgumentCaptor = ArgumentCaptor.forClass(PString.class); - verify(fakeBatchBeanCleanerService, times(1)) - .removeAsync(eq("an id"), pStringArgumentCaptor.capture()); assertEquals(1, got); - assertEquals("entryGroup", pStringArgumentCaptor.getValue().toString()); + assertEquals(0, new QCallbackQueryContext().findCount()); } @Test @Timeout(value = 1, unit = TimeUnit.MINUTES) void shouldDeleteById() throws Exception { - when(fakeBatchBeanCleanerService.removeAsync(eq("an id"), any())) - .thenReturn(CompletableFuture.completedFuture(1)); + new CallbackQueryContext("an id", "doesn't matter", new CallbackQueryMetadata()).save(); final Integer got = callbackQueryContextCleaner.removeIdAsync("an id").get(); - final ArgumentCaptor pStringArgumentCaptor = ArgumentCaptor.forClass(PString.class); - verify(fakeBatchBeanCleanerService, times(1)) - .removeAsync(eq("an id"), pStringArgumentCaptor.capture()); assertEquals(1, got); - assertEquals("id", pStringArgumentCaptor.getValue().toString()); + assertEquals(0, new QCallbackQueryContext().findCount()); } } -- 2.40.1 From 5895fa2910db3c55281d207f7ea59100c3dd339d Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Thu, 20 Apr 2023 11:18:24 +0200 Subject: [PATCH 12/13] chore: remove FIXME --- .../com/github/polpetta/mezzotre/telegram/command/NotFound.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java index ad42361..c1e3155 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java @@ -45,7 +45,6 @@ class NotFound implements Processor { @Override public CompletableFuture>> process(TgChat chat, Update update) { - // FIXME complete it with: localization, callbackQuery to show help message return CompletableFuture.supplyAsync( () -> templateContentGenerator.mergeTemplate( -- 2.40.1 From 7edc0af00d0c6d96eb8349370cac696bb072c493 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Thu, 20 Apr 2023 11:21:59 +0200 Subject: [PATCH 13/13] chore: translation --- src/main/resources/i18n/message_it.properties | 2 +- src/main/resources/i18n/message_it_IT.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/i18n/message_it.properties b/src/main/resources/i18n/message_it.properties index cc81ee8..6b49a81 100644 --- a/src/main/resources/i18n/message_it.properties +++ b/src/main/resources/i18n/message_it.properties @@ -18,6 +18,6 @@ help.notShownYet=Sembra tu non abbia ancora visto cosa posso fare! Per avere una help.buttonBelow=Alternativamente, puoi premere il bottone qui sotto. help.description=Ecco una lista di quello che sono in grado di fare help.buttonsToo=Puoi fare le stesse operazioni che faresti con i comandi elencati precedentemente cliccando il bottone corrispondente qui di sotto \ud83d\udc47 -help.cmdDescription=Stampa il messaggio d'aiuto +help.cmdDescription=Stampa il messaggio d''aiuto notfound.description=Mmm non sono in grado di trovare il comando {0}, sei sicuro di averlo scritto correttamente? notfound.howToHelp=Lascia che ti mostri cosa posso fare, scrivi /help nella chat! diff --git a/src/main/resources/i18n/message_it_IT.properties b/src/main/resources/i18n/message_it_IT.properties index cc81ee8..6b49a81 100644 --- a/src/main/resources/i18n/message_it_IT.properties +++ b/src/main/resources/i18n/message_it_IT.properties @@ -18,6 +18,6 @@ help.notShownYet=Sembra tu non abbia ancora visto cosa posso fare! Per avere una help.buttonBelow=Alternativamente, puoi premere il bottone qui sotto. help.description=Ecco una lista di quello che sono in grado di fare help.buttonsToo=Puoi fare le stesse operazioni che faresti con i comandi elencati precedentemente cliccando il bottone corrispondente qui di sotto \ud83d\udc47 -help.cmdDescription=Stampa il messaggio d'aiuto +help.cmdDescription=Stampa il messaggio d''aiuto notfound.description=Mmm non sono in grado di trovare il comando {0}, sei sicuro di averlo scritto correttamente? notfound.howToHelp=Lascia che ti mostri cosa posso fare, scrivi /help nella chat! -- 2.40.1