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